feat(advance-wars): add contextual battle and movement sound effects
- Assign battle sounds to each unit type (gunfire, tank, cannon, missile, etc.) and play the appropriate sound on attack/counterattack in battle animations - Add per-unit-type movement sounds (march for infantry/mech, engine-fast, engine-heavy) that loop during move animations and stop cleanly on completion - Play jump sound during capture animation hops and activation sound on capture - Play explosion sound when units are defeated in combat or crash - Lighten unit tint relative to buildings so units remain visually distinct when standing on captured buildings - Add new audio assets: battle sounds, march, 8bit-jump, and update battle-tank
This commit is contained in:
parent
427aacae6f
commit
c0ad354458
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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