feat: Add sound effects for battle actions and menu interactions
Integrate audio feedback across the game by adding new SFX assets and loading them in `BootScene`. Key changes include: - Added 12 new sound effect files (attacks, abilities, UI) and a main menu music track. - Implemented SFX playback in `BattleScene` for actions like attacks, counters, mortar/strike/siege/pierce fires, buffs (rally), shields (protect), damage, destructions, and legendary card plays. - Stopped main menu music during battles to avoid audio overlap and resumed it upon returning to `MainMenuScene`. - Added hover and select sounds for UI buttons in `MainMenuScene`. - Fixed deck composition in `DeckManager` by removing one 'imp_trooper_1' from the starter deck.
This commit is contained in:
parent
3c92d58055
commit
850a32c33f
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -27,7 +27,7 @@ export class DeckManager {
|
|||
name: 'Starter Deck',
|
||||
commander: 'imp_cmd_1',
|
||||
cards: [
|
||||
'imp_trooper_1', 'imp_trooper_1', 'imp_trooper_1', 'imp_trooper_1',
|
||||
'imp_trooper_1', 'imp_trooper_1', 'imp_trooper_1',
|
||||
'imp_gunner_1', 'imp_gunner_1', 'imp_gunner_1',
|
||||
'imp_guardian_1', 'imp_guardian_1', 'imp_guardian_1',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ export class BattleScene extends Phaser.Scene {
|
|||
const cardManager = this.registry.get('cardManager');
|
||||
const save = this.registry.get('save');
|
||||
|
||||
// Stop main menu music for the duration of the battle
|
||||
this.registry.get('music_main_menu')?.stop();
|
||||
|
||||
// Layout constants
|
||||
// Lanes: 4 lanes centred at x = 430, 740, 1050, 1360 (spacing 310)
|
||||
// Commander column: x = 150
|
||||
|
|
@ -312,8 +315,8 @@ export class BattleScene extends Phaser.Scene {
|
|||
}
|
||||
|
||||
const events = this.engine.processNextAttack();
|
||||
const preAttackEvent = events.find(e => e.type === 'preAttack');
|
||||
const attackEvent = events.find(e => e.type === 'attack');
|
||||
const preAttackEvent = events.find(e => e.type === 'preAttack');
|
||||
const attackEvent = events.find(e => e.type === 'attack');
|
||||
const postAttackEvent = events.find(e => e.type === 'postAttack');
|
||||
|
||||
if (!attackEvent) {
|
||||
|
|
@ -329,8 +332,8 @@ export class BattleScene extends Phaser.Scene {
|
|||
this.commanderObjects.get(state.player.commander.instanceId)?.refresh();
|
||||
this.commanderObjects.get(state.opponent.commander.instanceId)?.refresh();
|
||||
|
||||
const berserkEvent = events.find(e => e.type === 'berserk');
|
||||
const counterEvent = events.find(e => e.type === 'counter');
|
||||
const berserkEvent = events.find(e => e.type === 'berserk');
|
||||
const counterEvent = events.find(e => e.type === 'counter');
|
||||
|
||||
// Fire pre-attack hook, then proceed to attack animation.
|
||||
// Counter fires at impact (cards still enlarged at center) via onImpact callback.
|
||||
|
|
@ -374,6 +377,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
.setDisplaySize(630, 630)
|
||||
.setDepth(23);
|
||||
explosionSprite.play('explosion_anim');
|
||||
this.sound.play('sfx_destroy', { volume: 0.85 });
|
||||
explosionSprite.once('animationcomplete', () => {
|
||||
if (explosionSprite.scene) explosionSprite.destroy();
|
||||
if (!obj.scene) { if (onComplete) onComplete(); return; }
|
||||
|
|
@ -394,6 +398,9 @@ export class BattleScene extends Phaser.Scene {
|
|||
// Animate a card appearing on the field (scale from 0 with overshoot)
|
||||
_animateDeploy(cardObj, onComplete) {
|
||||
if (!cardObj) { onComplete(); return; }
|
||||
if (cardObj.cardData?.rarity === 'legendary') {
|
||||
this.sound.play('sfx_legendary_play', { volume: 0.9 });
|
||||
}
|
||||
cardObj.setScale(0).setAlpha(0);
|
||||
this.tweens.add({
|
||||
targets: cardObj,
|
||||
|
|
@ -430,6 +437,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
const targetObj = _lookup(counterEvent.target.instanceId); // the original attacker
|
||||
|
||||
if (!sourceObj?.scene || !targetObj?.scene) return;
|
||||
this.sound.play('sfx_counter', { volume: 0.8 });
|
||||
|
||||
this.statusText.setText(`${counterEvent.source.name} counters ${counterEvent.target.name}!`);
|
||||
|
||||
|
|
@ -460,8 +468,9 @@ export class BattleScene extends Phaser.Scene {
|
|||
if (counterEvent.damage > 0) {
|
||||
const target = counterEvent.target;
|
||||
const hpBefore = target.currentHP + counterEvent.damage;
|
||||
const hpAfter = Math.max(0, target.currentHP);
|
||||
const hpAfter = Math.max(0, target.currentHP);
|
||||
if (targetObj.hpText) targetObj.hpText.setText(`${hpBefore}`);
|
||||
if (target.currentHP > 0) this.sound.play('sfx_damage', { volume: 0.8 });
|
||||
targetObj.animateHPLoss(counterEvent.damage, null, hpBefore, hpAfter);
|
||||
}
|
||||
}
|
||||
|
|
@ -521,6 +530,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
const targetObj = _lookup(mortarFire.target.instanceId);
|
||||
|
||||
if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; }
|
||||
this.sound.play('sfx_mortar', { volume: 0.8 });
|
||||
|
||||
// Immediately correct the target's HP display to the pre-mortar value.
|
||||
// The early commander refresh (and any prior render) may have already set it to
|
||||
|
|
@ -545,7 +555,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
// Brief pause so the rotated mortar is visible on the card before launching
|
||||
this.time.delayedCall(450, () => {
|
||||
const startX = sourceObj.x, startY = sourceObj.y;
|
||||
const endX = targetObj.x, endY = targetObj.y;
|
||||
const endX = targetObj.x, endY = targetObj.y;
|
||||
const ARC_HEIGHT = -180;
|
||||
const progress = { t: 0 };
|
||||
|
||||
|
|
@ -597,6 +607,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
};
|
||||
|
||||
if (mortarFire.damage > 0) {
|
||||
if (hpAfter > 0) this.sound.play('sfx_damage', { volume: 0.8 });
|
||||
targetObj.animateHPLoss(mortarFire.damage, afterImpact, mortarFire.hpBefore, hpAfter);
|
||||
} else {
|
||||
afterImpact();
|
||||
|
|
@ -617,6 +628,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
const targetObj = _lookup(strikeFire.target.instanceId);
|
||||
|
||||
if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; }
|
||||
this.sound.play('sfx_strike', { volume: 0.8 });
|
||||
|
||||
// Immediately correct the target's HP display to the pre-strike value.
|
||||
// Commanders are refreshed to the final HP before _onPreAttackStep runs;
|
||||
|
|
@ -641,7 +653,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
// Brief pause so the aimed missile is visible before launch
|
||||
this.time.delayedCall(300, () => {
|
||||
const startX = sourceObj.x, startY = sourceObj.y;
|
||||
const endX = targetObj.x, endY = targetObj.y;
|
||||
const endX = targetObj.x, endY = targetObj.y;
|
||||
const ARC_HEIGHT = -70; // missiles fly flatter than mortar shells
|
||||
const progress = { t: 0 };
|
||||
|
||||
|
|
@ -690,6 +702,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
};
|
||||
|
||||
if (strikeFire.damage > 0) {
|
||||
if (hpAfter > 0) this.sound.play('sfx_damage', { volume: 0.8 });
|
||||
targetObj.animateHPLoss(strikeFire.damage, afterImpact, strikeFire.hpBefore, hpAfter);
|
||||
} else {
|
||||
afterImpact();
|
||||
|
|
@ -708,6 +721,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
const targetObj = _lookup(pierceFire.target.instanceId);
|
||||
|
||||
if (!sourceObj?.scene || !targetObj?.scene || pierceFire.amount <= 0) { onComplete(); return; }
|
||||
this.sound.play('sfx_pierce', { volume: 0.8 });
|
||||
|
||||
this.statusText.setText(`${attacker.name} pierces ${pierceFire.target.name}'s armor!`);
|
||||
|
||||
|
|
@ -892,6 +906,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
'attacks'
|
||||
).setDisplaySize(480, 480).setDepth(22);
|
||||
attackSprite.play('attack_anim');
|
||||
this.sound.play('sfx_attack', { volume: 0.7 });
|
||||
attackSprite.once('animationcomplete', () => {
|
||||
if (attackSprite.scene) attackSprite.destroy();
|
||||
this._onAttackReconcile(event, berserkEvent);
|
||||
|
|
@ -985,8 +1000,8 @@ export class BattleScene extends Phaser.Scene {
|
|||
|
||||
// Called once per preBattle step. Handles buff animations, siege fires, then protect fires.
|
||||
_onPreBattleStep(event, onComplete) {
|
||||
const hasBuffs = event.buffs?.length > 0;
|
||||
const hasSiege = event.siegeFires?.length > 0;
|
||||
const hasBuffs = event.buffs?.length > 0;
|
||||
const hasSiege = event.siegeFires?.length > 0;
|
||||
const hasProtect = event.protectFires?.length > 0;
|
||||
|
||||
if (hasBuffs || hasSiege || hasProtect) {
|
||||
|
|
@ -1035,6 +1050,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
const targetObj = _lookup(buff.target.instanceId);
|
||||
if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; }
|
||||
|
||||
this.sound.play('sfx_rally', { volume: 0.8 });
|
||||
this.statusText.setText(`${buff.source.name} rallies ${buff.target.name}! +${buff.amount} ATK`);
|
||||
|
||||
// Base scale for 160px display from 460px source frame
|
||||
|
|
@ -1070,8 +1086,8 @@ export class BattleScene extends Phaser.Scene {
|
|||
// Arc flight — tween a progress value and compute position + scale each frame
|
||||
const startX = sourceObj.x;
|
||||
const startY = sourceObj.y;
|
||||
const endX = targetObj.x;
|
||||
const endY = targetObj.y;
|
||||
const endX = targetObj.x;
|
||||
const endY = targetObj.y;
|
||||
const ARC_HEIGHT = -200; // px upward at the midpoint
|
||||
|
||||
const progress = { t: 0 };
|
||||
|
|
@ -1145,10 +1161,12 @@ export class BattleScene extends Phaser.Scene {
|
|||
if (!launcherSprite.scene) { onComplete(); return; }
|
||||
|
||||
const startX = sourceObj.x, startY = sourceObj.y;
|
||||
const endX = targetObj.x, endY = targetObj.y;
|
||||
const endX = targetObj.x, endY = targetObj.y;
|
||||
const ARC_HEIGHT = -160;
|
||||
const progress = { t: 0 };
|
||||
|
||||
this.sound.play('sfx_siege', { volume: 0.8 });
|
||||
|
||||
// Siege missile sprite (14) — aimed at commander, points up by default
|
||||
const missileSprite = this.add.sprite(startX, startY, 'attacks', 14)
|
||||
.setScale(BASE_SCALE)
|
||||
|
|
@ -1198,6 +1216,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
};
|
||||
|
||||
if (siegeFire.damage > 0) {
|
||||
if (hpAfter > 0) this.sound.play('sfx_damage', { volume: 0.8 });
|
||||
targetObj.animateHPLoss(siegeFire.damage, afterImpact, siegeFire.hpBefore, hpAfter);
|
||||
} else {
|
||||
afterImpact();
|
||||
|
|
@ -1215,21 +1234,22 @@ export class BattleScene extends Phaser.Scene {
|
|||
const sourceObj = _lookup(protectFire.source.instanceId);
|
||||
if (!sourceObj?.scene) { onComplete(); return; }
|
||||
|
||||
this.sound.play('sfx_protect', { volume: 0.8 });
|
||||
this.statusText.setText(`${protectFire.source.name} raises shields!`);
|
||||
|
||||
const BASE_SCALE = 160 / 460;
|
||||
const NEIGHBOR_SCALE = BASE_SCALE * (2 / 3);
|
||||
const RISE_DURATION = 600;
|
||||
const FLY_DURATION = 500;
|
||||
const SETTLE_DELAY = 650; // ms before ARM gain animations start
|
||||
const FLY_DURATION = 500;
|
||||
const SETTLE_DELAY = 650; // ms before ARM gain animations start
|
||||
|
||||
// Resolve the target CardObjects and separate self from neighbors
|
||||
const self = protectFire.targets.find(t => t.laneOffset === 0);
|
||||
const left = protectFire.targets.find(t => t.laneOffset === -1);
|
||||
const right = protectFire.targets.find(t => t.laneOffset === 1);
|
||||
const self = protectFire.targets.find(t => t.laneOffset === 0);
|
||||
const left = protectFire.targets.find(t => t.laneOffset === -1);
|
||||
const right = protectFire.targets.find(t => t.laneOffset === 1);
|
||||
|
||||
const selfObj = self ? _lookup(self.card.instanceId) : null;
|
||||
const leftObj = left ? _lookup(left.card.instanceId) : null;
|
||||
const selfObj = self ? _lookup(self.card.instanceId) : null;
|
||||
const leftObj = left ? _lookup(left.card.instanceId) : null;
|
||||
const rightObj = right ? _lookup(right.card.instanceId) : null;
|
||||
|
||||
const sprites = [];
|
||||
|
|
@ -1251,7 +1271,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
|
||||
// 2/3-size shields for each neighbor — fly from source to neighbor card
|
||||
for (const { neighbor, neighborObj } of [
|
||||
{ neighbor: left, neighborObj: leftObj },
|
||||
{ neighbor: left, neighborObj: leftObj },
|
||||
{ neighbor: right, neighborObj: rightObj }
|
||||
]) {
|
||||
if (!neighbor || !neighborObj?.scene) continue;
|
||||
|
|
@ -1315,7 +1335,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
if (event.debuffs?.length > 0) {
|
||||
for (const debuff of event.debuffs) {
|
||||
const obj = this.cardObjects.get(debuff.target.instanceId)
|
||||
?? this.commanderObjects?.get(debuff.target.instanceId);
|
||||
?? this.commanderObjects?.get(debuff.target.instanceId);
|
||||
obj?.refresh();
|
||||
}
|
||||
}
|
||||
|
|
@ -1330,6 +1350,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
|
||||
// HP loss animation on defender; on completion, trigger death if HP reached zero
|
||||
if (defenderObj?.scene && event.damage > 0) {
|
||||
if (event.defender.currentHP > 0) this.sound.play('sfx_damage', { volume: 0.8 });
|
||||
defenderObj.animateHPLoss(event.damage, () => {
|
||||
if (event.defender.currentHP <= 0 && !defenderObj.isCommander) {
|
||||
this._playCardDeath(defenderObj, event.defender);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,22 @@ export class BootScene extends Phaser.Scene {
|
|||
this.load.json('missions', 'data/missions.json');
|
||||
this.load.json('skills', 'data/skills.json');
|
||||
|
||||
this.load.audio('sfx_attack', 'assets/audio/fx/attack.mp3');
|
||||
this.load.audio('sfx_damage', 'assets/audio/fx/damage.mp3');
|
||||
this.load.audio('sfx_mortar', 'assets/audio/fx/mortar.mp3');
|
||||
this.load.audio('sfx_strike', 'assets/audio/fx/strike.mp3');
|
||||
this.load.audio('sfx_pierce', 'assets/audio/fx/pierce.mp3');
|
||||
this.load.audio('sfx_protect', 'assets/audio/fx/protect.mp3');
|
||||
this.load.audio('sfx_legendary_play', 'assets/audio/fx/legendary-play.mp3');
|
||||
this.load.audio('sfx_destroy', 'assets/audio/fx/destroy.mp3');
|
||||
this.load.audio('sfx_rally', 'assets/audio/fx/rally.mp3');
|
||||
this.load.audio('sfx_siege', 'assets/audio/fx/siege.mp3');
|
||||
this.load.audio('sfx_counter', 'assets/audio/fx/counter.mp3');
|
||||
this.load.audio('sfx_menu_select', 'assets/audio/fx/menu_select.mp3');
|
||||
this.load.audio('sfx_menu_hover', 'assets/audio/fx/menu_hover.mp3');
|
||||
|
||||
this.load.audio('music_main_menu', 'assets/audio/music/main_menu.mp3');
|
||||
|
||||
// Once cards.json finishes, queue card art images and resume loading
|
||||
this.load.once('filecomplete-json-cards', (_key, _type, data) => {
|
||||
data.forEach(card => {
|
||||
|
|
@ -67,6 +83,11 @@ export class BootScene extends Phaser.Scene {
|
|||
this.registry.set('missions', missionsData);
|
||||
this.registry.set('save', save);
|
||||
|
||||
// Start main menu music looping; store reference so other scenes can stop/resume it
|
||||
const menuMusic = this.sound.add('music_main_menu', { loop: true, volume: 0.5 });
|
||||
menuMusic.play();
|
||||
this.registry.set('music_main_menu', menuMusic);
|
||||
|
||||
this.scene.start('MainMenuScene');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ export class MainMenuScene extends Phaser.Scene {
|
|||
const save = this.registry.get('save');
|
||||
const { width, height } = this.scale;
|
||||
|
||||
// Resume main menu music if it was stopped by a battle
|
||||
const menuMusic = this.registry.get('music_main_menu');
|
||||
if (menuMusic && !menuMusic.isPlaying) menuMusic.play();
|
||||
|
||||
// Background
|
||||
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
|
||||
|
||||
|
|
@ -65,9 +69,9 @@ export class MainMenuScene extends Phaser.Scene {
|
|||
fontSize: '28px', color: '#ffffff'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
bg.on('pointerover', () => bg.setFillStyle(0x2a5a8c));
|
||||
bg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); bg.setFillStyle(0x2a5a8c); });
|
||||
bg.on('pointerout', () => bg.setFillStyle(0x1a3a5c));
|
||||
bg.on('pointerdown', callback);
|
||||
bg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); callback(); });
|
||||
|
||||
return { bg, text };
|
||||
}
|
||||
|
|
@ -83,9 +87,10 @@ export class MainMenuScene extends Phaser.Scene {
|
|||
fontSize: '20px', color: '#88cc88'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
bg.on('pointerover', () => bg.setFillStyle(0x2a3f2a));
|
||||
bg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); bg.setFillStyle(0x2a3f2a); });
|
||||
bg.on('pointerout', () => bg.setFillStyle(0x1a2a1a));
|
||||
bg.on('pointerdown', () => {
|
||||
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||
if (this.scale.isFullscreen) {
|
||||
this.scale.stopFullscreen();
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in New Issue