diff --git a/assets/fx/8bit-jump.mp3 b/assets/fx/8bit-jump.mp3 new file mode 100644 index 0000000..d796261 Binary files /dev/null and b/assets/fx/8bit-jump.mp3 differ diff --git a/assets/fx/battle-tank.mp3 b/assets/fx/battle-tank.mp3 index f796a99..0669c1c 100644 Binary files a/assets/fx/battle-tank.mp3 and b/assets/fx/battle-tank.mp3 differ diff --git a/assets/fx/march.mp3 b/assets/fx/march.mp3 new file mode 100644 index 0000000..c2eeb3d Binary files /dev/null and b/assets/fx/march.mp3 differ diff --git a/data/advancewars-rules.json b/data/advancewars-rules.json index 1aac125..4263d29 100644 --- a/data/advancewars-rules.json +++ b/data/advancewars-rules.json @@ -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, diff --git a/src/data/assetManifest.js b/src/data/assetManifest.js index 2930fb3..4fe0b7c 100644 --- a/src/data/assetManifest.js +++ b/src/data/assetManifest.js @@ -208,6 +208,12 @@ export const MANIFEST = { { 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' }, (scene) => musicFrom(scene, 'nintendo-music'), ], coloradodefense: [ diff --git a/src/games/advancewars/AdvanceWarsBattleAnim.js b/src/games/advancewars/AdvanceWarsBattleAnim.js index afab6f7..62f129c 100644 --- a/src/games/advancewars/AdvanceWarsBattleAnim.js +++ b/src/games/advancewars/AdvanceWarsBattleAnim.js @@ -4,8 +4,9 @@ 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; @@ -220,7 +221,7 @@ export function playBattleAnim(scene, rules, state, texKey, battles, onDone) { 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' }) @@ -277,6 +278,10 @@ 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, diff --git a/src/games/advancewars/AdvanceWarsCaptureAnim.js b/src/games/advancewars/AdvanceWarsCaptureAnim.js index 15c91d2..631c9b4 100644 --- a/src/games/advancewars/AdvanceWarsCaptureAnim.js +++ b/src/games/advancewars/AdvanceWarsCaptureAnim.js @@ -6,9 +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; @@ -69,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 @@ -105,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())); }, @@ -127,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, }); diff --git a/src/games/advancewars/AdvanceWarsMapView.js b/src/games/advancewars/AdvanceWarsMapView.js index 5ce4fbc..974e695 100644 --- a/src/games/advancewars/AdvanceWarsMapView.js +++ b/src/games/advancewars/AdvanceWarsMapView.js @@ -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 @@ -461,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); @@ -577,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?.(); }); } @@ -589,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({ diff --git a/src/scenes/PreloadScene.js b/src/scenes/PreloadScene.js index fb7d223..df59486 100644 --- a/src/scenes/PreloadScene.js +++ b/src/scenes/PreloadScene.js @@ -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'); diff --git a/src/ui/Sounds.js b/src/ui/Sounds.js index 1c7c990..1744a43 100644 --- a/src/ui/Sounds.js +++ b/src/ui/Sounds.js @@ -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',