Compare commits
No commits in common. "c0ad354458581ee8e826631b0315eebfdb0e0bac" and "a0b3b5f063054efa88dfcbc03bdf5e069021c2c4" have entirely different histories.
c0ad354458
...
a0b3b5f063
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.
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 |
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue