feat(puzzle): add stats completion sound loop and refine glow/timing logic

- Load new `sfx_stats.mp3` audio for puzzle completion stats display
- Increase glow delay from 500ms to 700ms for smoother visual transition
- Fix indentation in edge detection filter and normalize spacing in CSS/style assignments
- Stop and destroy stats sound on overlay cleanup or completion typing finish
- Start looping stats sound when typing puzzle completion stats, ensuring clean lifecycle management
This commit is contained in:
Brian Fertig 2026-04-06 19:54:46 -06:00
parent ada2535b62
commit 02e0e70b99
2 changed files with 22 additions and 15 deletions

BIN
assets/audio/fx/stats.mp3 Normal file

Binary file not shown.

View File

@ -108,6 +108,9 @@ class PuzzleScene extends Phaser.Scene {
if (!this.cache.audio.exists('sfx_grab')) { if (!this.cache.audio.exists('sfx_grab')) {
this.load.audio('sfx_grab', 'assets/audio/fx/grab.mp3'); this.load.audio('sfx_grab', 'assets/audio/fx/grab.mp3');
} }
if (!this.cache.audio.exists('sfx_stats')) {
this.load.audio('sfx_stats', 'assets/audio/fx/stats.mp3');
}
// Music track list // Music track list
if (!this.cache.json.exists('music_tracks')) { if (!this.cache.json.exists('music_tracks')) {
@ -365,7 +368,7 @@ class PuzzleScene extends Phaser.Scene {
this._edgeIds = this._pieces this._edgeIds = this._pieces
.filter(p => p.edges.top === 'flat' || p.edges.bottom === 'flat' || .filter(p => p.edges.top === 'flat' || p.edges.bottom === 'flat' ||
p.edges.left === 'flat' || p.edges.right === 'flat') p.edges.left === 'flat' || p.edges.right === 'flat')
.map(p => p.id); .map(p => p.id);
const midCol = Math.ceil(this._cols / 2); const midCol = Math.ceil(this._cols / 2);
@ -1228,7 +1231,7 @@ class PuzzleScene extends Phaser.Scene {
if (!this._glowingPieceIds) this._glowingPieceIds = new Set(); if (!this._glowingPieceIds) this._glowingPieceIds = new Set();
if (!this._glowTimers) this._glowTimers = new Map(); if (!this._glowTimers) this._glowTimers = new Map();
const GLOW_DELAY = 500; // ms a neighbor must stay in range before glow appears const GLOW_DELAY = 700; // ms a neighbor must stay in range before glow appears
const now = Date.now(); const now = Date.now();
// Track which neighbors are in proximity this frame // Track which neighbors are in proximity this frame
@ -1264,8 +1267,8 @@ class PuzzleScene extends Phaser.Scene {
[ [
[20, 0.06], [20, 0.06],
[14, 0.12], [14, 0.12],
[8, 0.30], [8, 0.30],
[4, 0.70], [4, 0.70],
].forEach(([baseLW, baseAlpha]) => { ].forEach(([baseLW, baseAlpha]) => {
const lw = Math.max(1, baseLW * intensity); const lw = Math.max(1, baseLW * intensity);
const alpha = baseAlpha * intensity; const alpha = baseAlpha * intensity;
@ -2176,9 +2179,9 @@ class PuzzleScene extends Phaser.Scene {
if (!this._uiLayer) return; if (!this._uiLayer) return;
const rect = this.sys.game.canvas.getBoundingClientRect(); const rect = this.sys.game.canvas.getBoundingClientRect();
Object.assign(this._uiLayer.style, { Object.assign(this._uiLayer.style, {
left: rect.left + 'px', left: rect.left + 'px',
top: rect.top + 'px', top: rect.top + 'px',
transform: `scale(${rect.width / 1920}, ${rect.height / 1080})`, transform: `scale(${rect.width / 1920}, ${rect.height / 1080})`,
}); });
} }
@ -2188,13 +2191,13 @@ class PuzzleScene extends Phaser.Scene {
// except on the interactive children. // except on the interactive children.
this._uiLayer = document.createElement('div'); this._uiLayer = document.createElement('div');
Object.assign(this._uiLayer.style, { Object.assign(this._uiLayer.style, {
position: 'fixed', position: 'fixed',
width: '1920px', width: '1920px',
height: '1080px', height: '1080px',
transformOrigin: 'top left', transformOrigin: 'top left',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: '10', zIndex: '10',
fontFamily: 'Arial, sans-serif', fontFamily: 'Arial, sans-serif',
}); });
document.body.appendChild(this._uiLayer); document.body.appendChild(this._uiLayer);
this._syncOverlayToCanvas(); this._syncOverlayToCanvas();
@ -2811,11 +2814,13 @@ class PuzzleScene extends Phaser.Scene {
const typeNext = () => { const typeNext = () => {
if (this._completionOverlay === null) { if (this._completionOverlay === null) {
clearInterval(cursorBlink); clearInterval(cursorBlink);
if (this._statsSound) { this._statsSound.stop(); this._statsSound.destroy(); this._statsSound = null; }
return; return;
} }
if (lineIdx >= lines.length) { if (lineIdx >= lines.length) {
// Done — keep cursor blinking at end // Done — stop stats sound, keep cursor blinking at end
if (this._statsSound) { this._statsSound.stop(); this._statsSound.destroy(); this._statsSound = null; }
return; return;
} }
@ -2855,7 +2860,9 @@ class PuzzleScene extends Phaser.Scene {
} }
}; };
// Start typing after a short delay // Start stats sound loop and typing after a short delay
this._statsSound = this.sound.add('sfx_stats', { loop: true, volume: 0.5 });
this._statsSound.play();
setTimeout(typeNext, 500); setTimeout(typeNext, 500);
} }