diff --git a/assets/images/advancewars/advancewars-battlebg.png b/assets/images/advancewars/advancewars-battlebg.png new file mode 100644 index 0000000..897391c Binary files /dev/null and b/assets/images/advancewars/advancewars-battlebg.png differ diff --git a/assets/images/advancewars/advancewars-battlebg.psd b/assets/images/advancewars/advancewars-battlebg.psd new file mode 100644 index 0000000..0a20dd4 Binary files /dev/null and b/assets/images/advancewars/advancewars-battlebg.psd differ diff --git a/data/advancewars-artwork.json b/data/advancewars-artwork.json index d20851a..c54fe60 100644 --- a/data/advancewars-artwork.json +++ b/data/advancewars-artwork.json @@ -23,5 +23,11 @@ "path": null, "frameWidth": 48, "frameHeight": 48 + }, + "battleBgSheet": { + "key": "advancewars-battlebg", + "path": "assets/images/advancewars/advancewars-battlebg.png", + "frameWidth": 256, + "frameHeight": 256 } } diff --git a/src/data/assetManifest.js b/src/data/assetManifest.js index b999a4d..2930fb3 100644 --- a/src/data/assetManifest.js +++ b/src/data/assetManifest.js @@ -207,7 +207,7 @@ export const MANIFEST = { { type: 'json', key: 'advancewars-rules', path: 'data/advancewars-rules.json' }, { type: 'json', key: 'advancewars-campaign', path: 'data/advancewars-campaign.json' }, (scene) => sheetsFrom(scene, 'advancewars-artwork', - ['terrainSheet', 'buildingSheet', 'unitSheet', 'uiSheet']), + ['terrainSheet', 'buildingSheet', 'unitSheet', 'uiSheet', 'battleBgSheet']), (scene) => musicFrom(scene, 'nintendo-music'), ], coloradodefense: [ diff --git a/src/games/advancewars/AdvanceWarsBattleAnim.js b/src/games/advancewars/AdvanceWarsBattleAnim.js index 8f3588a..c14aaf3 100644 --- a/src/games/advancewars/AdvanceWarsBattleAnim.js +++ b/src/games/advancewars/AdvanceWarsBattleAnim.js @@ -10,10 +10,170 @@ import { FONT } from './AdvanceWarsUI.js'; const W = 760; const H = 380; +// --------------------------------------------------------------------------- +// Battle backgrounds: one backdrop image per terrain/building a fighter can +// stand on. Frame order (index i is frame i in the sheet, left-to-right, +// top-to-bottom) — see sprites.md "Battle background sheet". +export const BATTLE_BG_KEYS = [ + 'plain', 'wood', 'mountain', 'sea', 'reef', + 'shoal', 'river', 'bridge', 'road', + 'city', 'base', 'airport', 'port', 'hq', +]; +const BG_FRAME = 256; +const BG_COLS = 5; +const BG_SHEET_KEY = 'advancewars-battlebg'; + +// A property tile's own terrain id (city/base/airport/port/hq) already IS +// its building type, so a unit standing on a building needs no separate +// "terrain underneath" lookup — the tile only ever has one terrain id, and +// that id already picks the right (building) background automatically. +// bridgeh/bridgev share the single 'bridge' backdrop — orientation doesn't +// matter for a static background behind a fighter. +const TERRAIN_TO_BG = { bridgeh: 'bridge', bridgev: 'bridge' }; + +function battleBgFrame(terrainId) { + const key = TERRAIN_TO_BG[terrainId] ?? terrainId; + const i = BATTLE_BG_KEYS.indexOf(key); + return i < 0 ? BATTLE_BG_KEYS.indexOf('plain') : i; +} + +// Painted sheet if loaded, else a generated stand-in with the same frame +// map — mirrors the pattern in AdvanceWarsMapView.js's ensureSheets(). +function ensureBattleBgSheet(scene) { + if (scene.textures.exists(BG_SHEET_KEY)) return BG_SHEET_KEY; + const procKey = `${BG_SHEET_KEY}-proc`; + if (!scene.textures.exists(procKey)) paintProceduralBattleBg(scene, procKey); + return procKey; +} + +function paintProceduralBattleBg(scene, key) { + const cols = BG_COLS; + const rows = Math.ceil(BATTLE_BG_KEYS.length / cols); + const tex = scene.textures.createCanvas(key, cols * BG_FRAME, rows * BG_FRAME); + const ctx = tex.getContext(); + const at = (i, draw) => { + ctx.save(); + ctx.translate((i % cols) * BG_FRAME, Math.floor(i / cols) * BG_FRAME); + draw(ctx); + ctx.restore(); + }; + // simple sky/ground split backdrop, tinted per terrain; groundY is where + // the horizon sits (higher groundY = more sky visible, e.g. for water). + const skyGround = (sky, ground, groundY = 150) => (c) => { + c.fillStyle = sky; c.fillRect(0, 0, BG_FRAME, BG_FRAME); + c.fillStyle = ground; c.fillRect(0, groundY, BG_FRAME, BG_FRAME - groundY); + }; + const SKY = '#bfe0f0'; + + at(BATTLE_BG_KEYS.indexOf('plain'), skyGround(SKY, '#a8d060')); + + at(BATTLE_BG_KEYS.indexOf('wood'), (c) => { + skyGround(SKY, '#8cc456')(c); + c.fillStyle = '#2f6b22'; + for (const [tx, ty] of [[60, 150], [150, 130], [200, 160], [90, 190]]) { + c.beginPath(); c.moveTo(tx - 30, ty + 60); c.lineTo(tx + 30, ty + 60); c.lineTo(tx, ty - 30); c.fill(); + c.fillRect(tx - 8, ty + 60, 16, 20); + } + }); + + at(BATTLE_BG_KEYS.indexOf('mountain'), (c) => { + skyGround(SKY, '#b5d16b')(c); + c.fillStyle = '#8a6a3c'; + c.beginPath(); c.moveTo(20, 240); c.lineTo(128, 40); c.lineTo(236, 240); c.fill(); + c.fillStyle = '#f0ede2'; + c.beginPath(); c.moveTo(100, 90); c.lineTo(128, 40); c.lineTo(156, 90); c.lineTo(128, 108); c.fill(); + }); + + at(BATTLE_BG_KEYS.indexOf('sea'), (c) => { + skyGround(SKY, '#3d6fd6', 60)(c); + c.strokeStyle = '#6f9ae8'; c.lineWidth = 4; + for (let i = 0; i < 4; i++) { + c.beginPath(); c.moveTo(10, 110 + i * 36); c.quadraticCurveTo(70, 90 + i * 36, 130, 110 + i * 36); + c.quadraticCurveTo(190, 130 + i * 36, 246, 110 + i * 36); c.stroke(); + } + }); + + at(BATTLE_BG_KEYS.indexOf('reef'), (c) => { + skyGround(SKY, '#3d6fd6', 60)(c); + c.fillStyle = '#24488f'; + for (const [px, py] of [[60, 140], [150, 110], [110, 190], [200, 170], [180, 220]]) { + c.beginPath(); c.arc(px, py, 20, 0, 7); c.fill(); + } + }); + + at(BATTLE_BG_KEYS.indexOf('shoal'), (c) => { + c.fillStyle = SKY; c.fillRect(0, 0, BG_FRAME, BG_FRAME); + c.fillStyle = '#8fb8e8'; c.fillRect(0, 90, BG_FRAME, 40); + c.fillStyle = '#e8d9a0'; c.fillRect(0, 130, BG_FRAME, BG_FRAME - 130); + }); + + at(BATTLE_BG_KEYS.indexOf('river'), (c) => { + skyGround(SKY, '#3d6fd6', 130)(c); + c.strokeStyle = '#6f9ae8'; c.lineWidth = 4; + for (let i = 0; i < 3; i++) { + c.beginPath(); c.moveTo(20, 170 + i * 32); c.quadraticCurveTo(128, 150 + i * 32, 236, 170 + i * 32); c.stroke(); + } + }); + + at(BATTLE_BG_KEYS.indexOf('bridge'), (c) => { + skyGround(SKY, '#3d6fd6', 60)(c); + c.fillStyle = '#b0a89a'; c.fillRect(0, 150, BG_FRAME, 60); + c.fillStyle = '#8a8276'; + for (let x = 10; x < BG_FRAME; x += 26) c.fillRect(x, 150, 6, 60); + }); + + at(BATTLE_BG_KEYS.indexOf('road'), (c) => { + skyGround(SKY, '#d8cfc0')(c); + c.fillStyle = '#a89c88'; c.fillRect(0, 150, BG_FRAME, 8); + }); + + const building = (icon) => (c) => { + skyGround(SKY, '#a8d060')(c); + c.fillStyle = '#ececec'; c.strokeStyle = '#3a3a3a'; c.lineWidth = 3; + c.fillRect(48, 30, 160, 190); c.strokeRect(48, 30, 160, 190); + icon(c); + }; + at(BATTLE_BG_KEYS.indexOf('city'), building((c) => { + c.fillStyle = '#8a8a8a'; + c.fillRect(72, 60, 42, 130); c.fillRect(140, 90, 42, 100); + c.fillStyle = '#3a3a3a'; + for (let i = 0; i < 4; i++) { c.fillRect(80, 70 + i * 28, 26, 12); c.fillRect(148, 100 + i * 22, 26, 10); } + })); + at(BATTLE_BG_KEYS.indexOf('base'), building((c) => { + c.fillStyle = '#8a8a8a'; + c.fillRect(64, 100, 128, 80); + c.beginPath(); c.moveTo(64, 100); c.lineTo(96, 40); c.lineTo(128, 100); c.lineTo(160, 40); c.lineTo(192, 100); c.fill(); + c.fillStyle = '#3a3a3a'; c.fillRect(112, 130, 32, 50); + })); + at(BATTLE_BG_KEYS.indexOf('airport'), building((c) => { + c.fillStyle = '#8a8a8a'; + c.beginPath(); + c.moveTo(128, 30); c.lineTo(148, 70); c.lineTo(200, 90); c.lineTo(148, 110); + c.lineTo(128, 170); c.lineTo(108, 110); c.lineTo(56, 90); c.lineTo(108, 70); c.fill(); + })); + at(BATTLE_BG_KEYS.indexOf('port'), building((c) => { + c.strokeStyle = '#8a8a8a'; c.lineWidth = 18; + c.beginPath(); c.arc(128, 100, 46, 0.5, Math.PI - 0.5, false); c.stroke(); + c.beginPath(); c.moveTo(128, 30); c.lineTo(128, 130); c.stroke(); + c.beginPath(); c.moveTo(96, 55); c.lineTo(160, 55); c.stroke(); + })); + at(BATTLE_BG_KEYS.indexOf('hq'), building((c) => { + c.fillStyle = '#8a8a8a'; c.fillRect(108, 20, 18, 170); + c.fillStyle = '#e05050'; + c.beginPath(); c.moveTo(126, 24); c.lineTo(200, 50); c.lineTo(126, 76); c.fill(); + })); + + tex.refresh(); + for (let i = 0; i < BATTLE_BG_KEYS.length; i++) { + tex.add(i, 0, (i % cols) * BG_FRAME, Math.floor(i / cols) * BG_FRAME, BG_FRAME, BG_FRAME); + } +} + // battles: array of engine 'battle' events (1 or 2 volleys). // Shows attacker left, defender right; each volley flashes and drains HP. -// texKey is the unit sheet (painted or procedural). -export function playBattleAnim(scene, rules, texKey, battles, onDone) { +// texKey is the unit sheet (painted or procedural). state resolves the +// terrain each fighter is standing on, to pick their backdrop image. +export function playBattleAnim(scene, rules, state, texKey, battles, onDone) { if (!battles.length) { onDone?.(); return null; } const cx = GAME_WIDTH / 2 - 130, cy = GAME_HEIGHT / 2 - 40; const objs = []; @@ -28,12 +188,19 @@ export function playBattleAnim(scene, rules, texKey, battles, onDone) { .setDepth(50).setInteractive(); const panel = scene.add.rectangle(cx, cy, W, H, 0x10141f, 0.97) .setStrokeStyle(3, 0x3a4260).setDepth(50); - const split = scene.add.rectangle(cx, cy, 4, H, 0x3a4260, 1).setDepth(50); - objs.push(dim, panel, split); + objs.push(dim, panel); - const ground = (side, color) => scene.add - .rectangle(cx + side * W / 4, cy + H / 2 - 40, W / 2 - 8, 80, color, 1).setDepth(50); - objs.push(ground(-1, 0x27324a), ground(1, 0x27324a)); + // backdrop per side — the terrain/building each fighter is actually + // standing on (property tiles' own terrain id already IS their building + // type, so no separate override lookup is needed, see battleBgFrame). + const bgSheet = ensureBattleBgSheet(scene); + const backdrop = (side, info) => scene.add + .image(cx + side * W / 4, cy, bgSheet, battleBgFrame(Logic.terrainAt(rules, state, info.x, info.y).id)) + .setDisplaySize(W / 2 - 6, H - 6).setFlipX(side > 0).setDepth(50.3); + objs.push(backdrop(-1, leftSide), backdrop(1, rightSide)); + + const split = scene.add.rectangle(cx, cy, 4, H, 0x3a4260, 1).setDepth(50.6); + objs.push(split); const mkFighter = (side, info) => { const spec = rules.unitById[info.type]; diff --git a/src/games/advancewars/AdvanceWarsGame.js b/src/games/advancewars/AdvanceWarsGame.js index fa1170e..f957ff6 100644 --- a/src/games/advancewars/AdvanceWarsGame.js +++ b/src/games/advancewars/AdvanceWarsGame.js @@ -529,7 +529,7 @@ export default class AdvanceWarsGame extends Phaser.Scene { const anyVisible = !run.state.fog || battles.some((b) => Logic.computeVision(this.rules, run.state, 0).has(Logic.tileKey(run.state, b.defender.x, b.defender.y))); if (this.battleAnims && anyVisible) { - await new Promise((resolve) => playBattleAnim(this, this.rules, run.view.tex.units, battles, resolve)); + await new Promise((resolve) => playBattleAnim(this, this.rules, run.state, run.view.tex.units, battles, resolve)); } this.crt.pulse(0.6, 260); } diff --git a/src/games/advancewars/sprites.md b/src/games/advancewars/sprites.md index 8b43a50..faa42ed 100644 --- a/src/games/advancewars/sprites.md +++ b/src/games/advancewars/sprites.md @@ -1,13 +1,17 @@ # Advance Wars — spritesheet spec -Art is split into **four independent sheets**, one per category, so each can +Art is split into **five independent sheets**, one per category, so each can be painted and dropped in separately. Until a sheet is painted, the game renders a procedural stand-in with the exact same frame map — everything is fully playable art-free, and you can drop in one sheet at a time. -**Rivers and roads need NO art**: they are drawn in code (Graphics) from -tile connection masks, so there are no variant frames to paint. Shoreline -foam where sea meets land is also drawn in code. +**Rivers and roads need NO art on the map itself**: they are drawn in code +(Graphics) from tile connection masks, so there are no variant frames to +paint for the *terrain sheet*. Shoreline foam where sea meets land is also +drawn in code. This exemption does **not** extend to the battle background +sheet (§5) — that one needs a full backdrop scene for every occupiable +terrain, river/road/bridge included, since it's a standalone illustration +rather than a tileable ground texture. ## Drop-in procedure (no code changes) @@ -108,7 +112,49 @@ spare).** Full color, transparent background. Icons 3–6 render at roughly 40% tile size as unit badges; keep them bold and readable when small. -## Game icon +## 5. Battle background sheet — `advancewars-battlebg.png` + +**256×256 cells, 5 columns × 3 rows = 1280×768 px (14 frames used, last cell +spare).** Full color, full-bleed illustration — no transparency needed, +these fill the entire backdrop behind each fighter in the battle cut-in +window (`AdvanceWarsBattleAnim.js`). Landscape/scene paintings, not tile +icons: think "diorama behind the two combatants," not "48×48 ground swatch." + +One frame per terrain type a unit can actually stand on, **plus** one per +building type. A property tile's own terrain id (city/base/airport/port/hq) +*is* its building type — there's no separate "terrain underneath the +building" to paint over, since a tile only ever has one terrain value at a +time. So each frame below is a straight lookup, not a layered composite. + +| frame | content | +|-------|---------| +| 0 | Plain (grass field) | +| 1 | Wood (forest clearing) | +| 2 | Mountain | +| 3 | Sea (open water) | +| 4 | Reef (rocky shallows) | +| 5 | Shoal (beach) | +| 6 | River | +| 7 | Bridge — used for **both** horizontal and vertical bridge tiles; orientation doesn't matter for a static backdrop, so this one frame covers both `bridgeh` and `bridgev` | +| 8 | Road | +| 9 | City | +| 10 | Base (land factory) | +| 11 | Airport | +| 12 | Port | +| 13 | HQ | + +Each fighter's backdrop is picked by looking up the terrain tile they're +actually standing on when the battle happens (from the engine's `battle` +event coordinates) — so, e.g., a tank parked on a captured city shows the +City background, not Plain, even though a tank could also be fought on open +grass elsewhere. The defender's backdrop is horizontally mirrored (matching +the mirrored defender unit sprite) — avoid art with strong left/right +asymmetry (e.g. a building silhouette that visibly leans one way) unless +you're fine with it flipping on the right-hand side. + +Unlike the other four sheets, this one is painted in full color and is +**not tinted** — buildings here don't need to react to owner army color +the way the small map-icon building sheet does. Also paint **frame 86 of `assets/images/game-icons.png`** (44×44, row 5 col 11 of that 15-per-row sheet). Suggested motif: a small orange tank on a