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',
|
name: 'Starter Deck',
|
||||||
commander: 'imp_cmd_1',
|
commander: 'imp_cmd_1',
|
||||||
cards: [
|
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_gunner_1', 'imp_gunner_1', 'imp_gunner_1',
|
||||||
'imp_guardian_1', 'imp_guardian_1', 'imp_guardian_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 cardManager = this.registry.get('cardManager');
|
||||||
const save = this.registry.get('save');
|
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
|
// Layout constants
|
||||||
// Lanes: 4 lanes centred at x = 430, 740, 1050, 1360 (spacing 310)
|
// Lanes: 4 lanes centred at x = 430, 740, 1050, 1360 (spacing 310)
|
||||||
// Commander column: x = 150
|
// Commander column: x = 150
|
||||||
|
|
@ -374,6 +377,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
.setDisplaySize(630, 630)
|
.setDisplaySize(630, 630)
|
||||||
.setDepth(23);
|
.setDepth(23);
|
||||||
explosionSprite.play('explosion_anim');
|
explosionSprite.play('explosion_anim');
|
||||||
|
this.sound.play('sfx_destroy', { volume: 0.85 });
|
||||||
explosionSprite.once('animationcomplete', () => {
|
explosionSprite.once('animationcomplete', () => {
|
||||||
if (explosionSprite.scene) explosionSprite.destroy();
|
if (explosionSprite.scene) explosionSprite.destroy();
|
||||||
if (!obj.scene) { if (onComplete) onComplete(); return; }
|
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)
|
// Animate a card appearing on the field (scale from 0 with overshoot)
|
||||||
_animateDeploy(cardObj, onComplete) {
|
_animateDeploy(cardObj, onComplete) {
|
||||||
if (!cardObj) { onComplete(); return; }
|
if (!cardObj) { onComplete(); return; }
|
||||||
|
if (cardObj.cardData?.rarity === 'legendary') {
|
||||||
|
this.sound.play('sfx_legendary_play', { volume: 0.9 });
|
||||||
|
}
|
||||||
cardObj.setScale(0).setAlpha(0);
|
cardObj.setScale(0).setAlpha(0);
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: cardObj,
|
targets: cardObj,
|
||||||
|
|
@ -430,6 +437,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
const targetObj = _lookup(counterEvent.target.instanceId); // the original attacker
|
const targetObj = _lookup(counterEvent.target.instanceId); // the original attacker
|
||||||
|
|
||||||
if (!sourceObj?.scene || !targetObj?.scene) return;
|
if (!sourceObj?.scene || !targetObj?.scene) return;
|
||||||
|
this.sound.play('sfx_counter', { volume: 0.8 });
|
||||||
|
|
||||||
this.statusText.setText(`${counterEvent.source.name} counters ${counterEvent.target.name}!`);
|
this.statusText.setText(`${counterEvent.source.name} counters ${counterEvent.target.name}!`);
|
||||||
|
|
||||||
|
|
@ -462,6 +470,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
const hpBefore = target.currentHP + counterEvent.damage;
|
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 (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);
|
targetObj.animateHPLoss(counterEvent.damage, null, hpBefore, hpAfter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -521,6 +530,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
const targetObj = _lookup(mortarFire.target.instanceId);
|
const targetObj = _lookup(mortarFire.target.instanceId);
|
||||||
|
|
||||||
if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; }
|
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.
|
// 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
|
// The early commander refresh (and any prior render) may have already set it to
|
||||||
|
|
@ -597,6 +607,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mortarFire.damage > 0) {
|
if (mortarFire.damage > 0) {
|
||||||
|
if (hpAfter > 0) this.sound.play('sfx_damage', { volume: 0.8 });
|
||||||
targetObj.animateHPLoss(mortarFire.damage, afterImpact, mortarFire.hpBefore, hpAfter);
|
targetObj.animateHPLoss(mortarFire.damage, afterImpact, mortarFire.hpBefore, hpAfter);
|
||||||
} else {
|
} else {
|
||||||
afterImpact();
|
afterImpact();
|
||||||
|
|
@ -617,6 +628,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
const targetObj = _lookup(strikeFire.target.instanceId);
|
const targetObj = _lookup(strikeFire.target.instanceId);
|
||||||
|
|
||||||
if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; }
|
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.
|
// Immediately correct the target's HP display to the pre-strike value.
|
||||||
// Commanders are refreshed to the final HP before _onPreAttackStep runs;
|
// Commanders are refreshed to the final HP before _onPreAttackStep runs;
|
||||||
|
|
@ -690,6 +702,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (strikeFire.damage > 0) {
|
if (strikeFire.damage > 0) {
|
||||||
|
if (hpAfter > 0) this.sound.play('sfx_damage', { volume: 0.8 });
|
||||||
targetObj.animateHPLoss(strikeFire.damage, afterImpact, strikeFire.hpBefore, hpAfter);
|
targetObj.animateHPLoss(strikeFire.damage, afterImpact, strikeFire.hpBefore, hpAfter);
|
||||||
} else {
|
} else {
|
||||||
afterImpact();
|
afterImpact();
|
||||||
|
|
@ -708,6 +721,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
const targetObj = _lookup(pierceFire.target.instanceId);
|
const targetObj = _lookup(pierceFire.target.instanceId);
|
||||||
|
|
||||||
if (!sourceObj?.scene || !targetObj?.scene || pierceFire.amount <= 0) { onComplete(); return; }
|
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!`);
|
this.statusText.setText(`${attacker.name} pierces ${pierceFire.target.name}'s armor!`);
|
||||||
|
|
||||||
|
|
@ -892,6 +906,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
'attacks'
|
'attacks'
|
||||||
).setDisplaySize(480, 480).setDepth(22);
|
).setDisplaySize(480, 480).setDepth(22);
|
||||||
attackSprite.play('attack_anim');
|
attackSprite.play('attack_anim');
|
||||||
|
this.sound.play('sfx_attack', { volume: 0.7 });
|
||||||
attackSprite.once('animationcomplete', () => {
|
attackSprite.once('animationcomplete', () => {
|
||||||
if (attackSprite.scene) attackSprite.destroy();
|
if (attackSprite.scene) attackSprite.destroy();
|
||||||
this._onAttackReconcile(event, berserkEvent);
|
this._onAttackReconcile(event, berserkEvent);
|
||||||
|
|
@ -1035,6 +1050,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
const targetObj = _lookup(buff.target.instanceId);
|
const targetObj = _lookup(buff.target.instanceId);
|
||||||
if (!sourceObj?.scene || !targetObj?.scene) { onComplete(); return; }
|
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`);
|
this.statusText.setText(`${buff.source.name} rallies ${buff.target.name}! +${buff.amount} ATK`);
|
||||||
|
|
||||||
// Base scale for 160px display from 460px source frame
|
// Base scale for 160px display from 460px source frame
|
||||||
|
|
@ -1149,6 +1165,8 @@ export class BattleScene extends Phaser.Scene {
|
||||||
const ARC_HEIGHT = -160;
|
const ARC_HEIGHT = -160;
|
||||||
const progress = { t: 0 };
|
const progress = { t: 0 };
|
||||||
|
|
||||||
|
this.sound.play('sfx_siege', { volume: 0.8 });
|
||||||
|
|
||||||
// Siege missile sprite (14) — aimed at commander, points up by default
|
// Siege missile sprite (14) — aimed at commander, points up by default
|
||||||
const missileSprite = this.add.sprite(startX, startY, 'attacks', 14)
|
const missileSprite = this.add.sprite(startX, startY, 'attacks', 14)
|
||||||
.setScale(BASE_SCALE)
|
.setScale(BASE_SCALE)
|
||||||
|
|
@ -1198,6 +1216,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (siegeFire.damage > 0) {
|
if (siegeFire.damage > 0) {
|
||||||
|
if (hpAfter > 0) this.sound.play('sfx_damage', { volume: 0.8 });
|
||||||
targetObj.animateHPLoss(siegeFire.damage, afterImpact, siegeFire.hpBefore, hpAfter);
|
targetObj.animateHPLoss(siegeFire.damage, afterImpact, siegeFire.hpBefore, hpAfter);
|
||||||
} else {
|
} else {
|
||||||
afterImpact();
|
afterImpact();
|
||||||
|
|
@ -1215,6 +1234,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
const sourceObj = _lookup(protectFire.source.instanceId);
|
const sourceObj = _lookup(protectFire.source.instanceId);
|
||||||
if (!sourceObj?.scene) { onComplete(); return; }
|
if (!sourceObj?.scene) { onComplete(); return; }
|
||||||
|
|
||||||
|
this.sound.play('sfx_protect', { volume: 0.8 });
|
||||||
this.statusText.setText(`${protectFire.source.name} raises shields!`);
|
this.statusText.setText(`${protectFire.source.name} raises shields!`);
|
||||||
|
|
||||||
const BASE_SCALE = 160 / 460;
|
const BASE_SCALE = 160 / 460;
|
||||||
|
|
@ -1330,6 +1350,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
|
|
||||||
// HP loss animation on defender; on completion, trigger death if HP reached zero
|
// HP loss animation on defender; on completion, trigger death if HP reached zero
|
||||||
if (defenderObj?.scene && event.damage > 0) {
|
if (defenderObj?.scene && event.damage > 0) {
|
||||||
|
if (event.defender.currentHP > 0) this.sound.play('sfx_damage', { volume: 0.8 });
|
||||||
defenderObj.animateHPLoss(event.damage, () => {
|
defenderObj.animateHPLoss(event.damage, () => {
|
||||||
if (event.defender.currentHP <= 0 && !defenderObj.isCommander) {
|
if (event.defender.currentHP <= 0 && !defenderObj.isCommander) {
|
||||||
this._playCardDeath(defenderObj, event.defender);
|
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('missions', 'data/missions.json');
|
||||||
this.load.json('skills', 'data/skills.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
|
// Once cards.json finishes, queue card art images and resume loading
|
||||||
this.load.once('filecomplete-json-cards', (_key, _type, data) => {
|
this.load.once('filecomplete-json-cards', (_key, _type, data) => {
|
||||||
data.forEach(card => {
|
data.forEach(card => {
|
||||||
|
|
@ -67,6 +83,11 @@ export class BootScene extends Phaser.Scene {
|
||||||
this.registry.set('missions', missionsData);
|
this.registry.set('missions', missionsData);
|
||||||
this.registry.set('save', save);
|
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');
|
this.scene.start('MainMenuScene');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ export class MainMenuScene extends Phaser.Scene {
|
||||||
const save = this.registry.get('save');
|
const save = this.registry.get('save');
|
||||||
const { width, height } = this.scale;
|
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
|
// Background
|
||||||
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
|
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
|
||||||
|
|
||||||
|
|
@ -65,9 +69,9 @@ export class MainMenuScene extends Phaser.Scene {
|
||||||
fontSize: '28px', color: '#ffffff'
|
fontSize: '28px', color: '#ffffff'
|
||||||
}).setOrigin(0.5);
|
}).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('pointerout', () => bg.setFillStyle(0x1a3a5c));
|
||||||
bg.on('pointerdown', callback);
|
bg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); callback(); });
|
||||||
|
|
||||||
return { bg, text };
|
return { bg, text };
|
||||||
}
|
}
|
||||||
|
|
@ -83,9 +87,10 @@ export class MainMenuScene extends Phaser.Scene {
|
||||||
fontSize: '20px', color: '#88cc88'
|
fontSize: '20px', color: '#88cc88'
|
||||||
}).setOrigin(0.5);
|
}).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('pointerout', () => bg.setFillStyle(0x1a2a1a));
|
||||||
bg.on('pointerdown', () => {
|
bg.on('pointerdown', () => {
|
||||||
|
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||||
if (this.scale.isFullscreen) {
|
if (this.scale.isFullscreen) {
|
||||||
this.scale.stopFullscreen();
|
this.scale.stopFullscreen();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue