Compare commits

..

No commits in common. "c0ad354458581ee8e826631b0315eebfdb0e0bac" and "a0b3b5f063054efa88dfcbc03bdf5e069021c2c4" have entirely different histories.

18 changed files with 30 additions and 370 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -23,11 +23,5 @@
"path": null,
"frameWidth": 48,
"frameHeight": 48
},
"battleBgSheet": {
"key": "advancewars-battlebg",
"path": "assets/images/advancewars/advancewars-battlebg.png",
"frameWidth": 256,
"frameHeight": 256
}
}

View File

@ -317,7 +317,6 @@
"units": [
{
"id": "infantry",
"battleSound": "gunfire-modern",
"name": "Infantry",
"abbr": "IN",
"cost": 1000,
@ -354,7 +353,6 @@
},
{
"id": "recon",
"battleSound": "gunfire-modern",
"name": "Recon",
"abbr": "RC",
"cost": 4000,
@ -372,7 +370,6 @@
},
{
"id": "tank",
"battleSound": "tank",
"name": "Tank",
"abbr": "TK",
"cost": 7000,
@ -390,7 +387,6 @@
},
{
"id": "mdtank",
"battleSound": "cannon",
"name": "Md Tank",
"abbr": "MD",
"cost": 16000,
@ -408,7 +404,6 @@
},
{
"id": "apc",
"battleSound": "gunfire-modern",
"name": "APC",
"abbr": "AP",
"cost": 5000,
@ -431,7 +426,6 @@
},
{
"id": "artillery",
"battleSound": "cannon",
"name": "Artillery",
"abbr": "AR",
"cost": 6000,
@ -450,7 +444,6 @@
},
{
"id": "rockets",
"battleSound": "missle",
"name": "Rockets",
"abbr": "RK",
"cost": 15000,
@ -469,7 +462,6 @@
},
{
"id": "antiair",
"battleSound": "gunfire-modern",
"name": "Anti-Air",
"abbr": "AA",
"cost": 8000,
@ -487,7 +479,6 @@
},
{
"id": "missiles",
"battleSound": "missle",
"name": "Missiles",
"abbr": "MS",
"cost": 12000,
@ -506,7 +497,6 @@
},
{
"id": "fighter",
"battleSound": "gunfire-modern",
"name": "Fighter",
"abbr": "FT",
"cost": 20000,
@ -525,7 +515,6 @@
},
{
"id": "bomber",
"battleSound": "cannon",
"name": "Bomber",
"abbr": "BM",
"cost": 22000,
@ -544,7 +533,6 @@
},
{
"id": "bcopter",
"battleSound": "missle",
"name": "B-Copter",
"abbr": "BC",
"cost": 9000,
@ -563,7 +551,6 @@
},
{
"id": "tcopter",
"battleSound": "helicopter",
"name": "T-Copter",
"abbr": "TC",
"cost": 5000,
@ -586,7 +573,6 @@
},
{
"id": "battleship",
"battleSound": "cannon",
"name": "Battleship",
"abbr": "BS",
"cost": 28000,
@ -606,7 +592,6 @@
},
{
"id": "cruiser",
"battleSound": "tank",
"name": "Cruiser",
"abbr": "CR",
"cost": 18000,
@ -632,7 +617,6 @@
},
{
"id": "lander",
"battleSound": "missle",
"name": "Lander",
"abbr": "LD",
"cost": 12000,
@ -652,7 +636,6 @@
},
{
"id": "sub",
"battleSound": "submarine",
"name": "Sub",
"abbr": "SB",
"cost": 20000,

View File

@ -207,13 +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', 'battleBgSheet']),
// Battle sounds keyed by each unit's `battleSound` in advancewars-rules.json
// (played on attack by playBattleAnim in AdvanceWarsBattleAnim.js).
...['gunfire-modern', 'tank', 'cannon', 'helicopter', 'missle', 'submarine']
.map((n) => ({ type: 'audio', key: `sfx-battle-${n}`, path: `assets/fx/battle-${n}.mp3` })),
// Foot-unit march (infantry/mech), played on move in AdvanceWarsMapView.js.
{ type: 'audio', key: 'sfx-march', path: 'assets/fx/march.mp3' },
['terrainSheet', 'buildingSheet', 'unitSheet', 'uiSheet']),
(scene) => musicFrom(scene, 'nintendo-music'),
],
coloradodefense: [

View File

@ -4,190 +4,16 @@
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
import * as Logic from './AdvanceWarsLogic.js';
import { unitColorInt } from './AdvanceWarsMapView.js';
import { armyColorInt } from './AdvanceWarsMapView.js';
import { FONT } from './AdvanceWarsUI.js';
import { playSound } from '../../ui/Sounds.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' };
export 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;
}
// The engine has no separate "terrain underneath the building" value (a
// tile only ever has one terrain id), so this is an editorial pick of what
// a given building would plausibly sit on, used where a scene wants the
// bare ground rather than the building's own backdrop (e.g. the capture
// cut-in, which already shows the building itself as its squishing prop).
const BUILDING_GROUND_TERRAIN = {
city: 'plain', base: 'plain', airport: 'plain', hq: 'plain', port: 'shoal',
};
export function groundBgFrame(terrainId) {
return battleBgFrame(BUILDING_GROUND_TERRAIN[terrainId] ?? terrainId);
}
// Painted sheet if loaded, else a generated stand-in with the same frame
// map — mirrors the pattern in AdvanceWarsMapView.js's ensureSheets().
export 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). state resolves the
// terrain each fighter is standing on, to pick their backdrop image.
export function playBattleAnim(scene, rules, state, texKey, battles, onDone) {
// texKey is the unit sheet (painted or procedural).
export function playBattleAnim(scene, rules, texKey, battles, onDone) {
if (!battles.length) { onDone?.(); return null; }
const cx = GAME_WIDTH / 2 - 130, cy = GAME_HEIGHT / 2 - 40;
const objs = [];
@ -202,26 +28,19 @@ export function playBattleAnim(scene, rules, state, texKey, battles, onDone) {
.setDepth(50).setInteractive();
const panel = scene.add.rectangle(cx, cy, W, H, 0x10141f, 0.97)
.setStrokeStyle(3, 0x3a4260).setDepth(50);
objs.push(dim, panel);
const split = scene.add.rectangle(cx, cy, 4, H, 0x3a4260, 1).setDepth(50);
objs.push(dim, panel, split);
// 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 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));
const mkFighter = (side, info) => {
const spec = rules.unitById[info.type];
const img = scene.add.image(cx + side * W / 4, cy + H / 2 - 78, texKey, spec.frame)
.setScale(3.4).setOrigin(0.5, 1).setDepth(51)
.setFlipX(side > 0)
.setTint(unitColorInt(rules, info.army));
.setTint(armyColorInt(rules, info.army));
objs.push(img);
const label = scene.add.text(cx + side * W / 4, cy - H / 2 + 26,
spec.name, { fontFamily: FONT, fontSize: '26px', color: '#ffffff' })
@ -278,10 +97,6 @@ export function playBattleAnim(scene, rules, state, texKey, battles, onDone) {
const victim = fromLeft ? right : left;
timers.push(scene.time.delayedCall(delay, () => {
if (finished) return;
// keyed by the shooter for THIS volley, not the initial attacker —
// a counterattack is fired by the defender's own unit type.
const battleSound = rules.unitById[b.attacker.type]?.battleSound;
if (battleSound) playSound(scene, `sfx-battle-${battleSound}`);
scene.tweens.add({
targets: shooter.img, x: shooter.img.x + (fromLeft ? 40 : -40),
duration: 150, yoyo: true,

View File

@ -6,10 +6,8 @@
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
import * as Logic from './AdvanceWarsLogic.js';
import { armyColorInt, unitColorInt } from './AdvanceWarsMapView.js';
import { armyColorInt } from './AdvanceWarsMapView.js';
import { FONT } from './AdvanceWarsUI.js';
import { ensureBattleBgSheet, groundBgFrame } from './AdvanceWarsBattleAnim.js';
import { playSound, SFX } from '../../ui/Sounds.js';
const W = 380;
const H = 500;
@ -43,17 +41,10 @@ export function playCaptureAnim(scene, rules, view, capturing, captured, onDone)
.setStrokeStyle(3, 0x3a4260).setDepth(50);
objs.push(dim, panel);
// backdrop is the terrain the building itself sits on (plain/shoal/etc),
// not the building's own battle background — the building is already the
// scene's main squishing prop, so showing its backdrop too would just be
// a duplicate building rendered twice.
const bgSheet = ensureBattleBgSheet(scene);
const backdrop = scene.add.image(cx, cy, bgSheet, groundBgFrame(t.id))
.setDisplaySize(W - 6, H - 6).setDepth(50.3);
objs.push(backdrop);
const groundCenterY = cy + H / 2 - 50;
const groundY = groundCenterY - 40; // where the building's base sits
const ground = scene.add.rectangle(cx, groundCenterY, W - 60, 80, 0x27324a, 1).setDepth(50);
objs.push(ground);
const groundY = groundCenterY - 40; // top edge of the ground strip
const label = scene.add.text(cx, cy - H / 2 + 30, (t.name ?? 'Property').toUpperCase(),
{ fontFamily: FONT, fontSize: '26px', color: '#ffffff' }).setOrigin(0.5, 0.5).setDepth(51);
@ -70,7 +61,7 @@ export function playCaptureAnim(scene, rules, view, capturing, captured, onDone)
const unitImg = scene.add.image(cx, groundY - building.displayHeight, view.tex.units, spec.frame)
.setOrigin(0.5, 1).setScale(UNIT_SCALE).setDepth(52)
.setFlipX(unit.army !== 0)
.setTint(unitColorInt(rules, unit.army));
.setTint(armyColorInt(rules, unit.army));
objs.push(unitImg);
// large capture-points counter, floating to the unit's left, always
@ -106,7 +97,6 @@ export function playCaptureAnim(scene, rules, view, capturing, captured, onDone)
function phase3raise() {
if (finished) return;
building.setTint(armyColorInt(rules, captured.army));
playSound(scene, SFX.EIGHTBIT_MOVE);
scene.tweens.add({
targets: building, scaleY: BUILDING_SCALE, duration: 500, ease: 'Sine.easeOut',
onUpdate: () => { unitImg.y = restY(); trackUnit(); pointsText.setText(String(currentPoints())); },
@ -129,14 +119,9 @@ export function playCaptureAnim(scene, rules, view, capturing, captured, onDone)
});
}
// 1.5s: two hops in place atop the (pre-squish) building height. A jump
// sound fires as each hop leaves the ground — once on start, once again
// when the repeat kicks off the second hop.
const jumpSound = () => playSound(scene, SFX.EIGHTBIT_JUMP);
// 1.5s: two hops in place atop the (pre-squish) building height.
scene.tweens.add({
targets: unitImg, y: restY() - HOP, duration: 375, yoyo: true, repeat: 1, ease: 'Sine.easeOut',
onStart: jumpSound,
onRepeat: jumpSound,
onUpdate: trackUnit,
onComplete: phase2squish,
});

View File

@ -424,7 +424,7 @@ export default class AdvanceWarsGame extends Phaser.Scene {
const run = this.run;
run.mode = 'target';
run.targets = targets;
run.view.showAttackTiles(targets.map((t) => ({ x: t.x, y: t.y })), { pulse: true });
run.view.showAttackTiles(targets.map((t) => ({ x: t.x, y: t.y })));
}
onTargetClick(tile) {
@ -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.state, run.view.tex.units, battles, resolve));
await new Promise((resolve) => playBattleAnim(this, this.rules, run.view.tex.units, battles, resolve));
}
this.crt.pulse(0.6, 260);
}

View File

@ -11,7 +11,6 @@
import * as Phaser from 'phaser';
import * as Logic from './AdvanceWarsLogic.js';
import { playSound, SFX } from '../../ui/Sounds.js';
const CELL = 48;
const TALL = 64; // building/unit cells: 48×48 footprint + 16px headroom
@ -28,15 +27,6 @@ const UNIT_DEPTH_BIAS = 0.5;
// building rather than floating disconnected from it.
const PROP_NUMBER_OVERLAP = 5;
// Per-unit-type movement sound, looped for however long the move animation
// takes and stopped the instant it ends (see animateMove).
const MOVE_SOUND_BY_TYPE = {
infantry: 'sfx-march', mech: 'sfx-march',
tank: 'sfx-engine-heavy', mdtank: 'sfx-engine-heavy', artillery: 'sfx-engine-heavy',
rockets: 'sfx-engine-heavy', missiles: 'sfx-engine-heavy', bomber: 'sfx-engine-heavy',
recon: 'sfx-engine-fast', apc: 'sfx-engine-fast', antiair: 'sfx-engine-fast', fighter: 'sfx-engine-fast',
};
// Painted drop-in sheets and the layout their procedural stand-ins mirror.
export const SHEETS = {
terrain: { key: 'advancewars-terrain', cellH: CELL, cols: 8, frames: 8 },
@ -54,12 +44,6 @@ export function armyColorInt(rules, army) {
return parseInt(hex.slice(1), 16);
}
// Units render a little lighter than buildings of the same army, so the two
// stay visually distinguishable when a unit is standing on a building.
export function unitColorInt(rules, army) {
return lighten(armyColorInt(rules, army), 0.48);
}
// ---------------------------------------------------------------------------
// Procedural stand-in sheets
@ -262,7 +246,6 @@ export class AdvanceWarsMapView {
destroy() {
this.flipTimer?.remove();
this.atkPulseTween?.stop();
this.rt?.destroy();
this.moveG?.destroy(); this.atkG?.destroy(); this.fogG?.destroy();
this.pathG?.destroy(); this.cursor?.destroy();
@ -477,7 +460,7 @@ export class AdvanceWarsMapView {
v.c.setPosition(this.px(u.x), this.oy + (u.y + 1) * this.tile).setDepth(this.depths.actors + u.y + UNIT_DEPTH_BIAS);
v.sprite.setFrame(rules.unitById[u.type].frame + this.animStep);
v.sprite.setFlipX(u.army !== 0);
const base = unitColorInt(rules, u.army);
const base = armyColorInt(rules, u.army);
v.sprite.setTint(u.moved && u.army === this.state.turn ? darken(base, 0.5) : base);
v.sprite.setAlpha(u.dived ? 0.5 : 1);
const hpd = Logic.hpDisplay(u);
@ -509,42 +492,15 @@ export class AdvanceWarsMapView {
}
}
// pulse: true for "you must pick a target now" (entering FIRE target mode)
// — red fill + white border breathing to draw the eye. false (default)
// keeps the old static tint, used for the indirect-range preview shown
// while a unit is merely selected, before a move/target is committed to.
showAttackTiles(tiles, { pulse = false } = {}) {
this.atkPulseTween?.stop();
this.atkPulseTween = null;
showAttackTiles(tiles) {
this.atkG.clear();
if (!pulse) {
this.atkG.fillStyle(0xff3d3d, 0.42);
for (const { x, y } of tiles) {
this.atkG.fillRect(this.ox + x * this.tile + 1, this.oy + y * this.tile + 1, this.tile - 2, this.tile - 2);
}
return;
this.atkG.fillStyle(0xff3d3d, 0.42);
for (const { x, y } of tiles) {
this.atkG.fillRect(this.ox + x * this.tile + 1, this.oy + y * this.tile + 1, this.tile - 2, this.tile - 2);
}
const draw = (fillA, lineA) => {
this.atkG.clear();
this.atkG.fillStyle(0xff3d3d, fillA);
this.atkG.lineStyle(Math.max(2, this.tile * 0.06), 0xffffff, lineA);
for (const { x, y } of tiles) {
const px = this.ox + x * this.tile + 1, py = this.oy + y * this.tile + 1, s = this.tile - 2;
this.atkG.fillRect(px, py, s, s);
this.atkG.strokeRect(px, py, s, s);
}
};
const phase = { t: 0 };
draw(0.30, 0.55);
this.atkPulseTween = this.scene.tweens.add({
targets: phase, t: 1, duration: 550, yoyo: true, repeat: -1, ease: 'Sine.easeInOut',
onUpdate: () => draw(0.28 + phase.t * 0.34, 0.5 + phase.t * 0.5),
});
}
clearOverlays() {
this.atkPulseTween?.stop();
this.atkPulseTween = null;
this.moveG.clear();
this.atkG.clear();
this.pathG.clear();
@ -593,12 +549,10 @@ export class AdvanceWarsMapView {
this.setUnitVisibility(spotted);
}
// quick explosion puff at a tile — plays whenever a unit is defeated
// (destroyed in combat or crashed), regardless of the battleAnims toggle.
// quick explosion puff at a tile
boom(x, y, onDone) {
const img = this.scene.add.image(this.px(x), this.py(y), this.tex.ui, UI_FRAMES.explosion1)
.setScale(this.spriteScaleFor()).setDepth(this.depths.cursor);
playSound(this.scene, SFX.BATTLESHIP_HIT);
this.scene.time.delayedCall(200, () => img.setFrame(UI_FRAMES.explosion2));
this.scene.time.delayedCall(450, () => { img.destroy(); onDone?.(); });
}
@ -607,27 +561,10 @@ export class AdvanceWarsMapView {
animateMove(unitId, path, onDone) {
const v = this.unitViews.get(unitId);
if (!v || !path?.length) { onDone?.(); return; }
const unit = Logic.unitById(this.state, unitId);
// loops for however long the move takes (covers paths longer than one
// clip) and is stopped the instant the path finishes — never allowed to
// ring on past the move or get cut short mid-loop.
let moveSound = null;
const moveSoundKey = unit && MOVE_SOUND_BY_TYPE[unit.type];
if (moveSoundKey) {
try {
moveSound = this.scene.sound.add(moveSoundKey, { loop: true });
moveSound.play();
} catch (_) { moveSound = null; }
}
const points = path.map((s) => ({ x: this.px(s.x), y: this.oy + (s.y + 1) * this.tile, row: s.y }));
let i = 0;
const step = () => {
if (i >= points.length) {
moveSound?.stop();
moveSound?.destroy();
onDone?.();
return;
}
if (i >= points.length) { onDone?.(); return; }
const p = points[i++];
v.c.setDepth(this.depths.actors + p.row + UNIT_DEPTH_BIAS);
this.scene.tweens.add({

View File

@ -1,17 +1,13 @@
# Advance Wars — spritesheet spec
Art is split into **five independent sheets**, one per category, so each can
Art is split into **four 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 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.
**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.
## Drop-in procedure (no code changes)
@ -112,49 +108,7 @@ spare).** Full color, transparent background.
Icons 3–6 render at roughly 40% tile size as unit badges; keep them bold
and readable when small.
## 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.
## Game icon
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

View File

@ -137,7 +137,6 @@ export default class PreloadScene extends Phaser.Scene {
this.load.audio('sfx-8bit-activate', 'assets/fx/8bit-activate.mp3');
this.load.audio('sfx-8bit-action', 'assets/fx/8bit-action.mp3');
this.load.audio('sfx-8bit-move', 'assets/fx/8bit-move.mp3');
this.load.audio('sfx-8bit-jump', 'assets/fx/8bit-jump.mp3');
this.load.audio('sfx-8bit-explode', 'assets/fx/8bit-explode.mp3');
this.load.audio('sfx-8bit-explode2', 'assets/fx/8bit-explode2.mp3');
this.load.audio('sfx-8bit-count', 'assets/fx/8bit-count.mp3');

View File

@ -44,7 +44,6 @@ export const SFX = {
EIGHTBIT_ACTIVATE: 'sfx-8bit-activate',
EIGHTBIT_ACTION: 'sfx-8bit-action',
EIGHTBIT_MOVE: 'sfx-8bit-move',
EIGHTBIT_JUMP: 'sfx-8bit-jump',
EIGHTBIT_CARD: 'sfx-card-deal-8bit',
EIGHTBIT_EXPLODE: 'sfx-8bit-explode',
EIGHTBIT_EXPLODE_2: 'sfx-8bit-explode2',