diff --git a/assets/images/background-shift.png b/assets/images/background-shift.png new file mode 100644 index 0000000..ef353dc Binary files /dev/null and b/assets/images/background-shift.png differ diff --git a/src/data/assetManifest.js b/src/data/assetManifest.js index 1c863ec..026be7a 100644 --- a/src/data/assetManifest.js +++ b/src/data/assetManifest.js @@ -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'), ], diff --git a/src/games/shift/ShiftGame.js b/src/games/shift/ShiftGame.js index e1ade98..43d2df3 100644 --- a/src/games/shift/ShiftGame.js +++ b/src/games/shift/ShiftGame.js @@ -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', { diff --git a/src/games/totalannihilation/TotalAnnihilationGame.js b/src/games/totalannihilation/TotalAnnihilationGame.js index a862527..a07ed07 100644 --- a/src/games/totalannihilation/TotalAnnihilationGame.js +++ b/src/games/totalannihilation/TotalAnnihilationGame.js @@ -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) {