# tools/derive-monster-assets.ps1 # Derives the web assets each monster needs from the hand-made art in # art/enemies and art/backgrounds. # # Per monster it writes four files: # apps/web/public/images/monsters/.png full painted artwork # apps/web/public/images/monsters/runtime/-560.jpg downscaled for the web # apps/web/public/images/combat/sprites/-.png background-free cutout # apps/web/public/images/combat/icons/-128.png medallion icon # # The generated files are committed, so this only needs re-running when the # source art changes. Re-runnable: it overwrites its own output and touches # nothing else. $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Drawing $root = Split-Path -Parent $PSScriptRoot $artEnemies = Join-Path $root 'art\enemies' $artCutouts = Join-Path $artEnemies 'transparent-background' $artBackgrounds = Join-Path $root 'art\backgrounds' $webImages = Join-Path $root 'apps\web\public\images' foreach ($dir in @( (Join-Path $webImages 'monsters\runtime'), (Join-Path $webImages 'combat\sprites'), (Join-Path $webImages 'combat\icons'), (Join-Path $webImages 'backgrounds\runtime'))) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } function Save-Scaled { param( [string] $SourcePath, [string] $TargetPath, [int] $TargetWidth, [string] $Format, [switch] $Opaque ) $img = [System.Drawing.Image]::FromFile($SourcePath) try { $height = [int][Math]::Round($img.Height * $TargetWidth / $img.Width) $bitmap = New-Object System.Drawing.Bitmap $TargetWidth, $height $graphics = [System.Drawing.Graphics]::FromImage($bitmap) if ($Opaque) { $graphics.Clear([System.Drawing.Color]::Black) } $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic $graphics.DrawImage($img, 0, 0, $TargetWidth, $height) $graphics.Dispose() if ($Format -eq 'jpg') { $codec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' } $params = New-Object System.Drawing.Imaging.EncoderParameters 1 $params.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter( [System.Drawing.Imaging.Encoder]::Quality, 82) $bitmap.Save($TargetPath, $codec, $params) } else { $bitmap.Save($TargetPath, [System.Drawing.Imaging.ImageFormat]::Png) } $bitmap.Dispose() } finally { $img.Dispose() } Write-Host "wrote $(Split-Path -Leaf $TargetPath)" } function Save-Icon { param( [string] $SourcePath, [string] $TargetPath, # Crop window as fractions of the source, chosen per monster so the icon # lands on the head rather than on whatever the centre happens to be. [double] $CropX, [double] $CropY, [double] $CropSize ) $img = [System.Drawing.Image]::FromFile($SourcePath) try { $side = [int]([Math]::Min($img.Width, $img.Height) * $CropSize) $x = [int]($img.Width * $CropX) $y = [int]($img.Height * $CropY) $bitmap = New-Object System.Drawing.Bitmap 128, 128 $graphics = [System.Drawing.Graphics]::FromImage($bitmap) $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic $graphics.DrawImage( $img, (New-Object System.Drawing.Rectangle 0, 0, 128, 128), (New-Object System.Drawing.Rectangle $x, $y, $side, $side), [System.Drawing.GraphicsUnit]::Pixel) $graphics.Dispose() $bitmap.Save($TargetPath, [System.Drawing.Imaging.ImageFormat]::Png) $bitmap.Dispose() } finally { $img.Dispose() } Write-Host "wrote $(Split-Path -Leaf $TargetPath)" } # `source` is the painted file, `cutout` the background-free one. # # NOTE the deliberate crossing on the first two rows: the file named # raider-scout depicts the heavier, plated, spear-carrying figure and is the # Veteran; raider-veteran depicts the leaner one and is the Scout. Slice 0.10 # design decision D7. $monsters = @( @{ key = 'raider-scout'; source = 'raider-veteran'; spriteHeight = 620; cropX = 0.40; cropY = 0.04; cropSize = 0.30 }, @{ key = 'raider-veteran'; source = 'raider-scout'; spriteHeight = 620; cropX = 0.40; cropY = 0.04; cropSize = 0.30 }, @{ key = 'burned-hound'; source = 'burned-hound'; spriteHeight = 760; cropX = 0.06; cropY = 0.12; cropSize = 0.34 }, @{ key = 'raider-captain'; source = 'Pluendererhauptmann'; spriteHeight = 620; cropX = 0.36; cropY = 0.02; cropSize = 0.28 } ) foreach ($monster in $monsters) { $key = $monster.key $painted = Join-Path $artEnemies "$($monster.source).png" $cutout = Join-Path $artCutouts "$($monster.source).png" Copy-Item -Force $painted (Join-Path $webImages "monsters\$key.png") Write-Host "wrote $key.png" Save-Scaled -SourcePath $painted ` -TargetPath (Join-Path $webImages "monsters\runtime\$key-560.jpg") ` -TargetWidth 560 -Format 'jpg' -Opaque Save-Scaled -SourcePath $cutout ` -TargetPath (Join-Path $webImages "combat\sprites\$key-$($monster.spriteHeight).png") ` -TargetWidth $monster.spriteHeight -Format 'png' # The Raider Captain's icon comes from hand-made art below, not a generated # crop of the body art, so skip the crop step for that key here. if ($key -ne 'raider-captain') { Save-Icon -SourcePath $cutout ` -TargetPath (Join-Path $webImages "combat\icons\$key-128.png") ` -CropX $monster.cropX -CropY $monster.cropY -CropSize $monster.cropSize } } # The Raider Captain has a hand-made icon. Authored art beats a generated # crop, but it still has to ship at icon size: resize the 1254x1254 source # down to 128x128 rather than copying it verbatim. It's already a square # portrait, so a straight bicubic resize preserves the authored composition # with no re-cropping. Save-Scaled -SourcePath (Join-Path $artEnemies 'PluendererhauptmannIcon.png') ` -TargetPath (Join-Path $webImages 'combat\icons\raider-captain-128.png') ` -TargetWidth 128 -Format 'png' foreach ($background in 'Wachturm', 'Aschengrube') { $source = Join-Path $artBackgrounds "$background.png" Copy-Item -Force $source (Join-Path $webImages "backgrounds\$background.png") Write-Host "wrote $background.png" Save-Scaled -SourcePath $source ` -TargetPath (Join-Path $webImages "backgrounds\runtime\$background-960.jpg") ` -TargetWidth 960 -Format 'jpg' -Opaque }