Compare commits
4 Commits
a0b3b5f063
...
c0ad354458
| Author | SHA1 | Date |
|---|---|---|
|
|
c0ad354458 | |
|
|
427aacae6f | |
|
|
a02e5a1913 | |
|
|
138b078dd2 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 38 KiB |
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@
|
|||
"units": [
|
||||
{
|
||||
"id": "infantry",
|
||||
"battleSound": "gunfire-modern",
|
||||
"name": "Infantry",
|
||||
"abbr": "IN",
|
||||
"cost": 1000,
|
||||
|
|
@ -353,6 +354,7 @@
|
|||
},
|
||||
{
|
||||
"id": "recon",
|
||||
"battleSound": "gunfire-modern",
|
||||
"name": "Recon",
|
||||
"abbr": "RC",
|
||||
"cost": 4000,
|
||||
|
|
@ -370,6 +372,7 @@
|
|||
},
|
||||
{
|
||||
"id": "tank",
|
||||
"battleSound": "tank",
|
||||
"name": "Tank",
|
||||
"abbr": "TK",
|
||||
"cost": 7000,
|
||||
|
|
@ -387,6 +390,7 @@
|
|||
},
|
||||
{
|
||||
"id": "mdtank",
|
||||
"battleSound": "cannon",
|
||||
"name": "Md Tank",
|
||||
"abbr": "MD",
|
||||
"cost": 16000,
|
||||
|
|
@ -404,6 +408,7 @@
|
|||
},
|
||||
{
|
||||
"id": "apc",
|
||||
"battleSound": "gunfire-modern",
|
||||
"name": "APC",
|
||||
"abbr": "AP",
|
||||
"cost": 5000,
|
||||
|
|
@ -426,6 +431,7 @@
|
|||
},
|
||||
{
|
||||
"id": "artillery",
|
||||
"battleSound": "cannon",
|
||||
"name": "Artillery",
|
||||
"abbr": "AR",
|
||||
"cost": 6000,
|
||||
|
|
@ -444,6 +450,7 @@
|
|||
},
|
||||
{
|
||||
"id": "rockets",
|
||||
"battleSound": "missle",
|
||||
"name": "Rockets",
|
||||
"abbr": "RK",
|
||||
"cost": 15000,
|
||||
|
|
@ -462,6 +469,7 @@
|
|||
},
|
||||
{
|
||||
"id": "antiair",
|
||||
"battleSound": "gunfire-modern",
|
||||
"name": "Anti-Air",
|
||||
"abbr": "AA",
|
||||
"cost": 8000,
|
||||
|
|
@ -479,6 +487,7 @@
|
|||
},
|
||||
{
|
||||
"id": "missiles",
|
||||
"battleSound": "missle",
|
||||
"name": "Missiles",
|
||||
"abbr": "MS",
|
||||
"cost": 12000,
|
||||
|
|
@ -497,6 +506,7 @@
|
|||
},
|
||||
{
|
||||
"id": "fighter",
|
||||
"battleSound": "gunfire-modern",
|
||||
"name": "Fighter",
|
||||
"abbr": "FT",
|
||||
"cost": 20000,
|
||||
|
|
@ -515,6 +525,7 @@
|
|||
},
|
||||
{
|
||||
"id": "bomber",
|
||||
"battleSound": "cannon",
|
||||
"name": "Bomber",
|
||||
"abbr": "BM",
|
||||
"cost": 22000,
|
||||
|
|
@ -533,6 +544,7 @@
|
|||
},
|
||||
{
|
||||
"id": "bcopter",
|
||||
"battleSound": "missle",
|
||||
"name": "B-Copter",
|
||||
"abbr": "BC",
|
||||
"cost": 9000,
|
||||
|
|
@ -551,6 +563,7 @@
|
|||
},
|
||||
{
|
||||
"id": "tcopter",
|
||||
"battleSound": "helicopter",
|
||||
"name": "T-Copter",
|
||||
"abbr": "TC",
|
||||
"cost": 5000,
|
||||
|
|
@ -573,6 +586,7 @@
|
|||
},
|
||||
{
|
||||
"id": "battleship",
|
||||
"battleSound": "cannon",
|
||||
"name": "Battleship",
|
||||
"abbr": "BS",
|
||||
"cost": 28000,
|
||||
|
|
@ -592,6 +606,7 @@
|
|||
},
|
||||
{
|
||||
"id": "cruiser",
|
||||
"battleSound": "tank",
|
||||
"name": "Cruiser",
|
||||
"abbr": "CR",
|
||||
"cost": 18000,
|
||||
|
|
@ -617,6 +632,7 @@
|
|||
},
|
||||
{
|
||||
"id": "lander",
|
||||
"battleSound": "missle",
|
||||
"name": "Lander",
|
||||
"abbr": "LD",
|
||||
"cost": 12000,
|
||||
|
|
@ -636,6 +652,7 @@
|
|||
},
|
||||
{
|
||||
"id": "sub",
|
||||
"battleSound": "submarine",
|
||||
"name": "Sub",
|
||||
"abbr": "SB",
|
||||
"cost": 20000,
|
||||
|
|
|
|||
|
|
@ -207,7 +207,13 @@ 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']),
|
||||
// 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' },
|
||||
(scene) => musicFrom(scene, 'nintendo-music'),
|
||||
],
|
||||
coloradodefense: [
|
||||
|
|
|
|||
|
|
@ -4,16 +4,190 @@
|
|||
|
||||
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
|
||||
import * as Logic from './AdvanceWarsLogic.js';
|
||||
import { armyColorInt } from './AdvanceWarsMapView.js';
|
||||
import { unitColorInt } 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).
|
||||
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,19 +202,26 @@ 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];
|
||||
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(armyColorInt(rules, info.army));
|
||||
.setTint(unitColorInt(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' })
|
||||
|
|
@ -97,6 +278,10 @@ export function playBattleAnim(scene, rules, 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,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@
|
|||
|
||||
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
|
||||
import * as Logic from './AdvanceWarsLogic.js';
|
||||
import { armyColorInt } from './AdvanceWarsMapView.js';
|
||||
import { armyColorInt, unitColorInt } 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;
|
||||
|
|
@ -41,10 +43,17 @@ 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 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 groundY = groundCenterY - 40; // where the building's base sits
|
||||
|
||||
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);
|
||||
|
|
@ -61,7 +70,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(armyColorInt(rules, unit.army));
|
||||
.setTint(unitColorInt(rules, unit.army));
|
||||
objs.push(unitImg);
|
||||
|
||||
// large capture-points counter, floating to the unit's left, always
|
||||
|
|
@ -97,6 +106,7 @@ 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())); },
|
||||
|
|
@ -119,9 +129,14 @@ export function playCaptureAnim(scene, rules, view, capturing, captured, onDone)
|
|||
});
|
||||
}
|
||||
|
||||
// 1.5s: two hops in place atop the (pre-squish) building height.
|
||||
// 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);
|
||||
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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 })));
|
||||
run.view.showAttackTiles(targets.map((t) => ({ x: t.x, y: t.y })), { pulse: true });
|
||||
}
|
||||
|
||||
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.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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
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
|
||||
|
|
@ -27,6 +28,15 @@ 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 },
|
||||
|
|
@ -44,6 +54,12 @@ 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
|
||||
|
||||
|
|
@ -246,6 +262,7 @@ 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();
|
||||
|
|
@ -460,7 +477,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 = armyColorInt(rules, u.army);
|
||||
const base = unitColorInt(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);
|
||||
|
|
@ -492,15 +509,42 @@ export class AdvanceWarsMapView {
|
|||
}
|
||||
}
|
||||
|
||||
showAttackTiles(tiles) {
|
||||
// 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;
|
||||
this.atkG.clear();
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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();
|
||||
|
|
@ -549,10 +593,12 @@ export class AdvanceWarsMapView {
|
|||
this.setUnitVisibility(spotted);
|
||||
}
|
||||
|
||||
// quick explosion puff at a tile
|
||||
// quick explosion puff at a tile — plays whenever a unit is defeated
|
||||
// (destroyed in combat or crashed), regardless of the battleAnims toggle.
|
||||
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?.(); });
|
||||
}
|
||||
|
|
@ -561,10 +607,27 @@ 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) { onDone?.(); return; }
|
||||
if (i >= points.length) {
|
||||
moveSound?.stop();
|
||||
moveSound?.destroy();
|
||||
onDone?.();
|
||||
return;
|
||||
}
|
||||
const p = points[i++];
|
||||
v.c.setDepth(this.depths.actors + p.row + UNIT_DEPTH_BIAS);
|
||||
this.scene.tweens.add({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ 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');
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ 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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue