diff --git a/assets/fx/tents-place.mp3 b/assets/fx/tents-place.mp3 new file mode 100644 index 0000000..23c1e2c Binary files /dev/null and b/assets/fx/tents-place.mp3 differ diff --git a/assets/fx/tents-remove.mp3 b/assets/fx/tents-remove.mp3 new file mode 100644 index 0000000..6185832 Binary files /dev/null and b/assets/fx/tents-remove.mp3 differ diff --git a/assets/fx/tents-win.mp3 b/assets/fx/tents-win.mp3 new file mode 100644 index 0000000..8c6fd5e Binary files /dev/null and b/assets/fx/tents-win.mp3 differ diff --git a/assets/music/tents-intro.mp3 b/assets/music/tents-intro.mp3 new file mode 100644 index 0000000..f9b3d22 Binary files /dev/null and b/assets/music/tents-intro.mp3 differ diff --git a/assets/speech/vega/tutorial-intro-01.mp3 b/assets/speech/vega/tutorial-intro-01.mp3 new file mode 100644 index 0000000..c560544 Binary files /dev/null and b/assets/speech/vega/tutorial-intro-01.mp3 differ diff --git a/src/data/assetManifest.js b/src/data/assetManifest.js index 3acccaa..a67c5a2 100644 --- a/src/data/assetManifest.js +++ b/src/data/assetManifest.js @@ -515,6 +515,16 @@ export const MANIFEST = { { type: 'audio', key: 'sfx-zuma-win', path: 'assets/fx/zuma-win.mp3' }, { type: 'audio', key: 'sfx-zuma-lose', path: 'assets/fx/zuma-lose.mp3' }, ], + tents: [ + // Tent placement/removal cues and the win jingle (played by TentsGame + // via the SFX.TENTS_* keys in src/ui/Sounds.js). + { type: 'audio', key: 'sfx-tents-place', path: 'assets/fx/tents-place.mp3' }, + { type: 'audio', key: 'sfx-tents-remove', path: 'assets/fx/tents-remove.mp3' }, + { type: 'audio', key: 'sfx-tents-win', path: 'assets/fx/tents-win.mp3' }, + // Puzzle-start intro jingle — TentsGame.startGame ducks the soundtrack + // for its duration (same pattern as Zuma's level-start fanfare). + { type: 'audio', key: 'sfx-tents-intro', path: 'assets/music/tents-intro.mp3' }, + ], '2048': [ // hacker soundtrack (see services/soundtrack.js) — lazy-loaded here so // its audio only downloads once 2048 is actually entered. diff --git a/src/games/tents/TentsGame.js b/src/games/tents/TentsGame.js index e0ed8b0..b431bef 100644 --- a/src/games/tents/TentsGame.js +++ b/src/games/tents/TentsGame.js @@ -88,7 +88,7 @@ export default class TentsGame extends Phaser.Scene { create() { try { const { tracks, volume } = getGameSoundtrack(this); - if (tracks.length) new MusicPlayer(this, tracks, volume); + if (tracks.length) this.music = new MusicPlayer(this, tracks, volume); } catch (_) { /* no soundtrack available */ } this.input.mouse?.disableContextMenu?.(); @@ -318,6 +318,9 @@ export default class TentsGame extends Phaser.Scene { this.tweens.killTweensOf(this.sky); this.sky.t = 0; + // Puzzle-start intro — ducks the soundtrack for its own length. + this.playJingle(SFX.TENTS_INTRO); + const def = DIFFICULTIES[difficultyKey] ?? DIFFICULTIES.porch; this.view = 'play'; this.selectLayer.setVisible(false); @@ -393,11 +396,11 @@ export default class TentsGame extends Phaser.Scene { if (res.placed) { this.pop.set(keyOf(c, r), this.time.now); - playSoundEx(this, SFX.UI_ACTIVATE, { volume: 0.8 }); + playSoundEx(this, SFX.TENTS_PLACE, { volume: 0.8 }); this.checkViolationFeedback(); } else { this.pop.delete(keyOf(c, r)); - playSoundEx(this, SFX.UI_PICK, { volume: 0.7 }); + playSoundEx(this, SFX.TENTS_REMOVE, { volume: 0.7 }); } this.updateHud(); @@ -437,7 +440,7 @@ export default class TentsGame extends Phaser.Scene { this.flash.until = 0; this.dirty = true; this.updateHud(); - playSoundEx(this, SFX.UI_PICK, { volume: 0.6 }); + playSoundEx(this, SFX.TENTS_REMOVE, { volume: 0.6 }); } revealAnswer() { @@ -451,7 +454,7 @@ export default class TentsGame extends Phaser.Scene { this.g.tents.add(k); this.pop.set(k, this.time.now); this.dirty = true; - playSoundEx(this, SFX.UI_ACTIVATE, { volume: 0.55 }); + playSoundEx(this, SFX.TENTS_PLACE, { volume: 0.55 }); })); this.time.delayedCall(75 * missing.length + 180, () => { this.updateHud(); @@ -460,10 +463,25 @@ export default class TentsGame extends Phaser.Scene { this.btnAnswer.setEnabled(false); } + // Duck the soundtrack for a one-shot jingle (puzzle-start intro, win), + // then hand playback back to MusicPlayer when the jingle ends — not a + // fixed delay, since the jingle's own length is what decides when the + // music comes back (same pattern as Zuma's start/lose fanfares). + playJingle(key, volume = 0.9) { + this.music?.pause(); + try { + const sfx = this.sound.add(key, { volume }); + sfx.once(Phaser.Sound.Events.COMPLETE, () => this.music?.resume()); + sfx.play(); + } catch (_) { + this.music?.resume(); + } + } + startWin() { if (this.won) return; this.won = true; - playSoundEx(this, SFX.VICTORY_SHORT, { volume: 0.9 }); + this.playJingle(SFX.TENTS_WIN); this.toast('Every tent is pitched… the sun is going down.', 950); // Nightfall: ~6.5s, then the cozy panel. diff --git a/src/ui/Sounds.js b/src/ui/Sounds.js index 290a90c..3c4199f 100644 --- a/src/ui/Sounds.js +++ b/src/ui/Sounds.js @@ -99,6 +99,12 @@ export const SFX = { KART_SHELL: 'sfx-kart-shell', KART_STAR: 'sfx-kart-star', KART_SPINNER: 'sfx-kart-spinner', + // Tents & Trees cues (lazy-loaded via the `tents` entry in + // data/assetManifest.js when the game room is entered). + TENTS_PLACE: 'sfx-tents-place', + TENTS_REMOVE: 'sfx-tents-remove', + TENTS_WIN: 'sfx-tents-win', + TENTS_INTRO: 'sfx-tents-intro', VEGA_SELECT: 'sfx-vega-select', VEGA_BUILD: 'sfx-vega-build', VEGA_CLOSE: 'sfx-vega-close', diff --git a/tools/smokeTents.js b/tools/smokeTents.js index f5de726..25313d5 100644 --- a/tools/smokeTents.js +++ b/tools/smokeTents.js @@ -28,13 +28,18 @@ function stub() { return s; } +const sfxPlayed = []; // keys passed to scene.sound.play +const sfxAdded = []; // keys passed to scene.sound.add (one-shot jingles) const s = new TentsGame(); // Wire up scene services. s.add = stub(); s.scale = { setZoom() {} }; s.input = { mouse: { disableContextMenu() {} } }; -s.sound = { play() {} }; +s.sound = { + play(key) { sfxPlayed.push(key); }, + add(key) { sfxAdded.push(key); return { once() {}, play() {} }; }, +}; s.cache = { json: { get: () => null } }; s.scene = { start() {} }; s.events = { on() {}, once() {} }; @@ -63,6 +68,7 @@ check('select screen built', Array.isArray(s.selectLayer) === false && s.selectL // ── Start a game ── s.startGame('porch'); check('game state exists', s.g && s.g.puzzle && s.g.tents instanceof Set); +check('intro jingle queued on start', sfxAdded.includes('sfx-tents-intro'), `added=${sfxAdded}`); check('board layout computed', typeof s.cell === 'number' && s.cell > 20 && s.cell < 140); check('zones built', s.cellZones.length === s.g.puzzle.size ** 2); check('labels built', s.colLabels.length === s.g.puzzle.size && s.rowLabels.length === s.g.puzzle.size); @@ -88,7 +94,9 @@ for (let r = 0; r < p.size; r++) { } } check('all solution tents placed', s.g.tents.size === p.trees.length && placed === p.trees.length, `tents=${s.g.tents.size} placed=${placed} trees=${p.trees.length}`); +check('place cue played', sfxPlayed.includes('sfx-tents-place'), `played=${sfxPlayed}`); check('win triggered', s.won === true); +check('win jingle queued', sfxAdded.includes('sfx-tents-win'), `added=${sfxAdded}`); check('sky tween scheduled', s.tweens.tweens.some((t) => t.targets === s.sky)); // ── Nightfall: animate sky to night and render ── @@ -126,6 +134,7 @@ for (const [c, r] of half) s.onCellClick(c, r); check('partial placement ok', s.g.tents.size === 4, `tents=${s.g.tents.size}`); s.resetTents(); check('reset clears tents', s.g.tents.size === 0); +check('remove cue played', sfxPlayed.includes('sfx-tents-remove'), `played=${sfxPlayed}`); // ── All difficulties generate + solve ── for (const key of ['porch', 'clearing', 'meadow', 'deepwoods']) {