diff --git a/apps/web/public/images/backgrounds/Aschengrube.png b/apps/web/public/images/backgrounds/Aschengrube.png new file mode 100644 index 0000000..02fdecb Binary files /dev/null and b/apps/web/public/images/backgrounds/Aschengrube.png differ diff --git a/apps/web/public/images/backgrounds/Wachturm.png b/apps/web/public/images/backgrounds/Wachturm.png new file mode 100644 index 0000000..b64b14d Binary files /dev/null and b/apps/web/public/images/backgrounds/Wachturm.png differ diff --git a/apps/web/public/images/backgrounds/runtime/Aschengrube-960.jpg b/apps/web/public/images/backgrounds/runtime/Aschengrube-960.jpg new file mode 100644 index 0000000..7968379 Binary files /dev/null and b/apps/web/public/images/backgrounds/runtime/Aschengrube-960.jpg differ diff --git a/apps/web/public/images/backgrounds/runtime/Wachturm-960.jpg b/apps/web/public/images/backgrounds/runtime/Wachturm-960.jpg new file mode 100644 index 0000000..f2b030e Binary files /dev/null and b/apps/web/public/images/backgrounds/runtime/Wachturm-960.jpg differ diff --git a/apps/web/public/images/combat/icons/burned-hound-128.png b/apps/web/public/images/combat/icons/burned-hound-128.png new file mode 100644 index 0000000..3c870f2 Binary files /dev/null and b/apps/web/public/images/combat/icons/burned-hound-128.png differ diff --git a/apps/web/public/images/combat/icons/raider-captain-128.png b/apps/web/public/images/combat/icons/raider-captain-128.png new file mode 100644 index 0000000..24d8d00 Binary files /dev/null and b/apps/web/public/images/combat/icons/raider-captain-128.png differ diff --git a/apps/web/public/images/combat/icons/raider-scout-128.png b/apps/web/public/images/combat/icons/raider-scout-128.png new file mode 100644 index 0000000..5bb310e Binary files /dev/null and b/apps/web/public/images/combat/icons/raider-scout-128.png differ diff --git a/apps/web/public/images/combat/icons/raider-veteran-128.png b/apps/web/public/images/combat/icons/raider-veteran-128.png new file mode 100644 index 0000000..66e55f9 Binary files /dev/null and b/apps/web/public/images/combat/icons/raider-veteran-128.png differ diff --git a/apps/web/public/images/combat/sprites/burned-hound-760.png b/apps/web/public/images/combat/sprites/burned-hound-760.png new file mode 100644 index 0000000..fb73317 Binary files /dev/null and b/apps/web/public/images/combat/sprites/burned-hound-760.png differ diff --git a/apps/web/public/images/combat/sprites/raider-captain-620.png b/apps/web/public/images/combat/sprites/raider-captain-620.png new file mode 100644 index 0000000..3e22ed9 Binary files /dev/null and b/apps/web/public/images/combat/sprites/raider-captain-620.png differ diff --git a/apps/web/public/images/combat/sprites/raider-scout-620.png b/apps/web/public/images/combat/sprites/raider-scout-620.png new file mode 100644 index 0000000..eb8295a Binary files /dev/null and b/apps/web/public/images/combat/sprites/raider-scout-620.png differ diff --git a/apps/web/public/images/combat/sprites/raider-veteran-620.png b/apps/web/public/images/combat/sprites/raider-veteran-620.png new file mode 100644 index 0000000..9487bf5 Binary files /dev/null and b/apps/web/public/images/combat/sprites/raider-veteran-620.png differ diff --git a/apps/web/public/images/monsters/burned-hound.png b/apps/web/public/images/monsters/burned-hound.png new file mode 100644 index 0000000..648c6b5 Binary files /dev/null and b/apps/web/public/images/monsters/burned-hound.png differ diff --git a/apps/web/public/images/monsters/raider-captain.png b/apps/web/public/images/monsters/raider-captain.png new file mode 100644 index 0000000..001d5a7 Binary files /dev/null and b/apps/web/public/images/monsters/raider-captain.png differ diff --git a/apps/web/public/images/monsters/raider-scout.png b/apps/web/public/images/monsters/raider-scout.png new file mode 100644 index 0000000..a2d59b2 Binary files /dev/null and b/apps/web/public/images/monsters/raider-scout.png differ diff --git a/apps/web/public/images/monsters/raider-veteran.png b/apps/web/public/images/monsters/raider-veteran.png new file mode 100644 index 0000000..0edeb7b Binary files /dev/null and b/apps/web/public/images/monsters/raider-veteran.png differ diff --git a/apps/web/public/images/monsters/runtime/burned-hound-560.jpg b/apps/web/public/images/monsters/runtime/burned-hound-560.jpg new file mode 100644 index 0000000..bfa18c0 Binary files /dev/null and b/apps/web/public/images/monsters/runtime/burned-hound-560.jpg differ diff --git a/apps/web/public/images/monsters/runtime/raider-captain-560.jpg b/apps/web/public/images/monsters/runtime/raider-captain-560.jpg new file mode 100644 index 0000000..627afe5 Binary files /dev/null and b/apps/web/public/images/monsters/runtime/raider-captain-560.jpg differ diff --git a/apps/web/public/images/monsters/runtime/raider-scout-560.jpg b/apps/web/public/images/monsters/runtime/raider-scout-560.jpg new file mode 100644 index 0000000..c757bd2 Binary files /dev/null and b/apps/web/public/images/monsters/runtime/raider-scout-560.jpg differ diff --git a/apps/web/public/images/monsters/runtime/raider-veteran-560.jpg b/apps/web/public/images/monsters/runtime/raider-veteran-560.jpg new file mode 100644 index 0000000..80262f9 Binary files /dev/null and b/apps/web/public/images/monsters/runtime/raider-veteran-560.jpg differ diff --git a/apps/web/src/app/features/world/location-page/location-page.component.ts b/apps/web/src/app/features/world/location-page/location-page.component.ts index 0c437ab..b112025 100644 --- a/apps/web/src/app/features/world/location-page/location-page.component.ts +++ b/apps/web/src/app/features/world/location-page/location-page.component.ts @@ -14,6 +14,8 @@ import { LocationSidebarComponent } from '../location-sidebar/location-sidebar.c const RUNTIME_ARTWORK: Readonly> = { '/images/backgrounds/Suedtor.png': '/images/backgrounds/runtime/Suedtor-960.jpg', '/images/backgrounds/Aschestrasse.png': '/images/backgrounds/runtime/Aschestrasse-960.jpg', + '/images/backgrounds/Wachturm.png': '/images/backgrounds/runtime/Wachturm-960.jpg', + '/images/backgrounds/Aschengrube.png': '/images/backgrounds/runtime/Aschengrube-960.jpg', }; /** diff --git a/apps/web/src/app/shared/monster-artwork.spec.ts b/apps/web/src/app/shared/monster-artwork.spec.ts index 0c53e13..d381516 100644 --- a/apps/web/src/app/shared/monster-artwork.spec.ts +++ b/apps/web/src/app/shared/monster-artwork.spec.ts @@ -1,4 +1,9 @@ -import { monsterCutoutPath, monsterIconPath, runtimeMonsterArtworkPath } from './monster-artwork'; +import { + combatMonsterSpriteScale, + monsterCutoutPath, + monsterIconPath, + runtimeMonsterArtworkPath, +} from './monster-artwork'; describe('runtimeMonsterArtworkPath', () => { it('returns the optimized JPEG derivative for a known monster artwork path', () => { @@ -42,3 +47,32 @@ describe('monsterIconPath', () => { expect(monsterIconPath('dawnwolf')).toBeUndefined(); }); }); + +describe('watchpost monsters', () => { + const keys = ['raider-scout', 'raider-veteran', 'burned-hound', 'raider-captain']; + + it('has a cutout for every watchpost monster', () => { + for (const key of keys) { + expect(monsterCutoutPath(key)).toBeDefined(); + } + }); + + it('has an icon for every watchpost monster', () => { + for (const key of keys) { + expect(monsterIconPath(key)).toBeDefined(); + } + }); + + it('has a runtime derivative for every watchpost artwork', () => { + for (const key of keys) { + expect(runtimeMonsterArtworkPath(`/images/monsters/${key}.png`)).toBeDefined(); + } + }); + + it('scales the captain larger than the hound', () => { + // A hulking elite and a low-slung dog must not share a silhouette height. + expect(combatMonsterSpriteScale('raider-captain')).toBeGreaterThan( + combatMonsterSpriteScale('burned-hound'), + ); + }); +}); diff --git a/apps/web/src/app/shared/monster-artwork.ts b/apps/web/src/app/shared/monster-artwork.ts index 680cb85..dd54201 100644 --- a/apps/web/src/app/shared/monster-artwork.ts +++ b/apps/web/src/app/shared/monster-artwork.ts @@ -3,6 +3,10 @@ const RUNTIME_MONSTER_ARTWORK: Readonly> = { '/images/monsters/road-bandit.png': '/images/monsters/runtime/road-bandit-560.jpg', '/images/monsters/wild-road-dog.png': '/images/monsters/runtime/wild-road-dog-560.png', '/images/monsters/charred-looter.png': '/images/monsters/runtime/charred-looter-560.png', + '/images/monsters/raider-scout.png': '/images/monsters/runtime/raider-scout-560.jpg', + '/images/monsters/raider-veteran.png': '/images/monsters/runtime/raider-veteran-560.jpg', + '/images/monsters/burned-hound.png': '/images/monsters/runtime/burned-hound-560.jpg', + '/images/monsters/raider-captain.png': '/images/monsters/runtime/raider-captain-560.jpg', }; export function runtimeMonsterArtworkPath(artworkPath: string): string | undefined { @@ -16,6 +20,10 @@ const MONSTER_CUTOUT: Readonly> = { 'road-bandit': '/images/combat/sprites/road-bandit-620.png', 'wild-road-dog': '/images/combat/sprites/wild-road-dog-760.png', 'charred-looter': '/images/combat/sprites/charred-looter-620.png', + 'raider-scout': '/images/combat/sprites/raider-scout-620.png', + 'raider-veteran': '/images/combat/sprites/raider-veteran-620.png', + 'burned-hound': '/images/combat/sprites/burned-hound-760.png', + 'raider-captain': '/images/combat/sprites/raider-captain-620.png', }; const MONSTER_ICON: Readonly> = { @@ -23,6 +31,10 @@ const MONSTER_ICON: Readonly> = { 'road-bandit': '/images/combat/icons/road-bandit-128.png', 'wild-road-dog': '/images/combat/icons/wild-road-dog-128.png', 'charred-looter': '/images/combat/icons/charred-looter-128.png', + 'raider-scout': '/images/combat/icons/raider-scout-128.png', + 'raider-veteran': '/images/combat/icons/raider-veteran-128.png', + 'burned-hound': '/images/combat/icons/burned-hound-128.png', + 'raider-captain': '/images/combat/icons/raider-captain-128.png', }; // Share of the battlefield height each monster sprite occupies, so a hulking @@ -32,6 +44,10 @@ const COMBAT_MONSTER_SCALE: Readonly> = { 'road-bandit': 0.82, 'wild-road-dog': 0.58, 'charred-looter': 0.86, + 'raider-scout': 0.78, + 'raider-veteran': 0.84, + 'burned-hound': 0.6, + 'raider-captain': 0.9, }; const DEFAULT_MONSTER_SCALE = 0.6; diff --git a/tools/derive-monster-assets.ps1 b/tools/derive-monster-assets.ps1 new file mode 100644 index 0000000..b989941 --- /dev/null +++ b/tools/derive-monster-assets.ps1 @@ -0,0 +1,146 @@ +# 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' + + 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 crop. +Copy-Item -Force (Join-Path $artEnemies 'PluendererhauptmannIcon.png') ` + (Join-Path $webImages 'combat\icons\raider-captain-128.png') +Write-Host 'wrote raider-captain-128.png (hand-made)' + +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 +}