Merge pull request 'Add Tents sound effects and intro jingle' (#2) from Tents into main

Reviewed-on: #2
This commit is contained in:
brianfertig 2026-08-29 03:12:30 +00:00
commit 25d29c0f86
9 changed files with 50 additions and 7 deletions

BIN
assets/fx/tents-place.mp3 Normal file

Binary file not shown.

BIN
assets/fx/tents-remove.mp3 Normal file

Binary file not shown.

BIN
assets/fx/tents-win.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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.

View File

@ -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.

View File

@ -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',

View File

@ -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']) {