feat: enhance Shift backgrounds and add commander death SFX for TA

- Add background image support for Shift game menu screens
- Remove redundant title/subtitle text in Shift difficulty select
- Add nuclear explosion SFX for Total Annihilation commander deaths
- Extend _throttledSfx to support optional volume parameter
This commit is contained in:
Brian Fertig 2026-07-26 16:01:54 -06:00
parent 6e9cc84f6a
commit 9f54556ba9
4 changed files with 34 additions and 16 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -156,7 +156,10 @@ export const MANIFEST = {
image('spireclimb-act3', 'assets/images/spireclimb-act3.png'),
(scene) => sheetsFrom(scene, 'spireclimb-artwork', ['cardSheet', 'creatureSheet', 'playerSheets']),
],
shift: (scene) => imagesFrom(scene, 'shift-artwork'),
shift: [
image('shift-background', 'assets/images/background-shift.png'),
(scene) => imagesFrom(scene, 'shift-artwork'),
],
slots: (scene) => imagesFrom(scene, 'slots-artwork'),
dungeonboss: (scene) => sheetsFrom(scene, 'dungeonboss-artwork',
['roomSheet', 'heroSheet', 'spellSheet', 'bossSheet', 'deckBackSheet', 'iconSheet']),
@ -278,7 +281,7 @@ export const MANIFEST = {
{ type: 'audio', key: 'sfx-scifi-launch', path: 'assets/fx/scifi-launch.mp3' },
// TA-specific sfx: machine guns per weapon.sound, rocket variants (random pick between the
// two on fire, see TotalAnnihilationGame._onSimEvent), and unit/vehicle death cues.
...['50cal', 'machinegun', 'rocket-1', 'rocket-2', 'unit-loss', 'vehicle-loss']
...['50cal', 'machinegun', 'rocket-1', 'rocket-2', 'nuclear', 'unit-loss', 'vehicle-loss']
.map((n) => ({ type: 'audio', key: `sfx-ta-${n}`, path: `assets/fx/ta-${n}.mp3` })),
(scene) => musicFrom(scene, 'hacker-music'),
],

View File

@ -83,24 +83,26 @@ export default class ShiftGame extends Phaser.Scene {
this.bestText = null;
}
_drawMenuBackground() {
if (this.textures.exists('shift-background')) {
this.add.image(GAME_WIDTH / 2, GAME_HEIGHT / 2, 'shift-background')
.setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(D.bg);
}
}
// ── Difficulty select ─────────────────────────────────────────────────────────
showDifficultySelect() {
this.view = 'difficulty';
this.overlayUp = false;
this.clearLayer();
this._drawMenuBackground();
const cx = GAME_WIDTH / 2;
const title = this.add.text(cx, 160, 'SHIFT', {
fontFamily: 'Righteous', fontSize: '96px', color: COLORS.goldHex,
}).setOrigin(0.5);
const sub = this.add.text(cx, 268, 'Slide tiles into the empty space to restore the image.', {
fontFamily: '"Julius Sans One"', fontSize: '30px', color: COLORS.mutedHex,
}).setOrigin(0.5);
const pick = this.add.text(cx, 356, 'Choose a difficulty', {
const pick = this.add.text(cx, 390, 'Choose a difficulty', {
fontFamily: 'Righteous', fontSize: '34px', color: COLORS.textHex,
}).setOrigin(0.5);
this.layer.add([title, sub, pick]);
this.layer.add(pick);
const TILE_W = 430;
const TILE_H = 400;
@ -218,6 +220,7 @@ export default class ShiftGame extends Phaser.Scene {
this.view = 'artwork';
this.overlayUp = false;
this.clearLayer();
this._drawMenuBackground();
const cx = GAME_WIDTH / 2;
const title = this.add.text(cx, 120, 'Choose Your Image', {

View File

@ -13,7 +13,7 @@ import * as Phaser from 'phaser';
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
import { MusicPlayer } from '../../ui/MusicPlayer.js';
import { getGameSoundtrack } from '../../services/soundtrack.js';
import { playSound } from '../../ui/Sounds.js';
import { playSound, playSoundEx } from '../../ui/Sounds.js';
import { api } from '../../services/api.js';
import { compileRules } from './TARules.js';
import { generateMap, decodeMap } from './TAMapGen.js';
@ -263,8 +263,13 @@ export default class TotalAnnihilationGame extends Phaser.Scene {
if (ev.t === 'weaponFired' && ev.sound) this._throttledSfx(this._pickSound(ev.sound), 90);
if (ev.t === 'impact' && ev.sound) this._throttledSfx(this._pickSound(ev.sound), 90);
if (ev.t === 'unitDestroyed' && !ev.isBuilding) {
const moveClass = this.rules.defById[ev.defId]?.moveClass;
if (moveClass) this._throttledSfx(moveClass === 'foot' ? 'sfx-ta-unit-loss' : 'sfx-ta-vehicle-loss', 120);
const def = this.rules.defById[ev.defId];
if (def?.isCommander) {
// Play the nuclear explosion at full volume for maximum impact
this._throttledSfx('sfx-ta-nuclear', 300, 1);
} else if (def?.moveClass) {
this._throttledSfx(def.moveClass === 'foot' ? 'sfx-ta-unit-loss' : 'sfx-ta-vehicle-loss', 120);
}
}
if (ev.t === 'buildingComplete' && ev.army === this.playerArmy) {
// Terrain under a finished structure changes, so its chunk has to be restamped.
@ -290,12 +295,19 @@ export default class TotalAnnihilationGame extends Phaser.Scene {
}
/** Gate repeated plays of the same sound key so a volley of units firing at once doesn't
* stack a dozen overlapping instances of the same clip. */
_throttledSfx(key, gapMs) {
* stack a dozen overlapping instances of the same clip.
* @param {string} key - Sound key
* @param {number} gapMs - Minimum ms between plays
* @param {number} [volume] - Optional volume 0-1 (defaults to normal via playSound) */
_throttledSfx(key, gapMs, volume) {
const now = this.time.now;
if ((this.lastSfxAt[key] ?? -1e9) + gapMs > now) return;
this.lastSfxAt[key] = now;
playSound(this, key);
if (volume !== undefined) {
playSoundEx(this, key, { volume });
} else {
playSound(this, key);
}
}
_finish(over) {