feat(advancewars): add terrain-based battle scene backdrops
Introduce a new battle background spritesheet (advancewars-battlebg) that displays context-appropriate backdrops behind each fighter during battle cut-ins. The backdrop is selected based on the terrain/building type the fighter is standing on (city, base, airport, port, plain, wood, mountain, sea, reef, shoal, river, bridge, road, hq). - Add battleBgSheet to artwork.json and asset manifest - Implement playBattleAnim() backdrop system with terrain lookup - Provide procedural fallback matching the real sheet's frame map for art-free playability - Mirror defender backdrops to match mirrored defender sprites - Document the 14-frame sheet spec in sprites.md (256×256 cells, 5 cols)
This commit is contained in:
parent
138b078dd2
commit
a02e5a1913
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue