diff --git a/public/assets/speech/bernie-happy-01.mp3 b/public/assets/speech/bernie-happy-01.mp3 new file mode 100644 index 0000000..ff332b3 Binary files /dev/null and b/public/assets/speech/bernie-happy-01.mp3 differ diff --git a/public/assets/speech/bernie-happy-02.mp3 b/public/assets/speech/bernie-happy-02.mp3 new file mode 100644 index 0000000..9db9eb9 Binary files /dev/null and b/public/assets/speech/bernie-happy-02.mp3 differ diff --git a/public/assets/speech/bernie-intro-01.mp3 b/public/assets/speech/bernie-intro-01.mp3 new file mode 100644 index 0000000..fb8c836 Binary files /dev/null and b/public/assets/speech/bernie-intro-01.mp3 differ diff --git a/public/assets/speech/bernie-upset-01.mp3 b/public/assets/speech/bernie-upset-01.mp3 new file mode 100644 index 0000000..8d43a83 Binary files /dev/null and b/public/assets/speech/bernie-upset-01.mp3 differ diff --git a/public/assets/speech/bernie-upset-02.mp3 b/public/assets/speech/bernie-upset-02.mp3 new file mode 100644 index 0000000..c90f654 Binary files /dev/null and b/public/assets/speech/bernie-upset-02.mp3 differ diff --git a/public/assets/speech/ethel-happy-01.mp3 b/public/assets/speech/ethel-happy-01.mp3 new file mode 100644 index 0000000..cc5c113 Binary files /dev/null and b/public/assets/speech/ethel-happy-01.mp3 differ diff --git a/public/assets/speech/ethel-happy-02.mp3 b/public/assets/speech/ethel-happy-02.mp3 new file mode 100644 index 0000000..05f71d4 Binary files /dev/null and b/public/assets/speech/ethel-happy-02.mp3 differ diff --git a/public/assets/speech/ethel-happy-03.mp3 b/public/assets/speech/ethel-happy-03.mp3 new file mode 100644 index 0000000..7f914cd Binary files /dev/null and b/public/assets/speech/ethel-happy-03.mp3 differ diff --git a/public/assets/speech/ethel-happy-04.mp3 b/public/assets/speech/ethel-happy-04.mp3 new file mode 100644 index 0000000..d65bffa Binary files /dev/null and b/public/assets/speech/ethel-happy-04.mp3 differ diff --git a/public/assets/speech/ethel-happy-05.mp3 b/public/assets/speech/ethel-happy-05.mp3 new file mode 100644 index 0000000..0548fcd Binary files /dev/null and b/public/assets/speech/ethel-happy-05.mp3 differ diff --git a/public/assets/speech/ethel-intro-01.mp3 b/public/assets/speech/ethel-intro-01.mp3 new file mode 100644 index 0000000..39d30cd Binary files /dev/null and b/public/assets/speech/ethel-intro-01.mp3 differ diff --git a/public/assets/speech/ethel-intro-02.mp3 b/public/assets/speech/ethel-intro-02.mp3 new file mode 100644 index 0000000..eb964bd Binary files /dev/null and b/public/assets/speech/ethel-intro-02.mp3 differ diff --git a/public/assets/speech/ethel-upset-01.mp3 b/public/assets/speech/ethel-upset-01.mp3 new file mode 100644 index 0000000..c21f154 Binary files /dev/null and b/public/assets/speech/ethel-upset-01.mp3 differ diff --git a/public/assets/speech/ethel-upset-02.mp3 b/public/assets/speech/ethel-upset-02.mp3 new file mode 100644 index 0000000..5ecc69e Binary files /dev/null and b/public/assets/speech/ethel-upset-02.mp3 differ diff --git a/public/assets/speech/ethel-upset-03.mp3 b/public/assets/speech/ethel-upset-03.mp3 new file mode 100644 index 0000000..789a638 Binary files /dev/null and b/public/assets/speech/ethel-upset-03.mp3 differ diff --git a/public/assets/speech/ethel-upset-04.mp3 b/public/assets/speech/ethel-upset-04.mp3 new file mode 100644 index 0000000..fae4de5 Binary files /dev/null and b/public/assets/speech/ethel-upset-04.mp3 differ diff --git a/public/assets/speech/ethel-upset-05.mp3 b/public/assets/speech/ethel-upset-05.mp3 new file mode 100644 index 0000000..3efc83d Binary files /dev/null and b/public/assets/speech/ethel-upset-05.mp3 differ diff --git a/public/assets/videos/brad-happy-01.mp3 b/public/assets/videos/brad-happy-01.mp3 new file mode 100644 index 0000000..4927e50 Binary files /dev/null and b/public/assets/videos/brad-happy-01.mp3 differ diff --git a/public/assets/videos/brad-happy-02.mp3 b/public/assets/videos/brad-happy-02.mp3 new file mode 100644 index 0000000..01d748a Binary files /dev/null and b/public/assets/videos/brad-happy-02.mp3 differ diff --git a/public/assets/videos/brad-happy-03.mp3 b/public/assets/videos/brad-happy-03.mp3 new file mode 100644 index 0000000..ba5f220 Binary files /dev/null and b/public/assets/videos/brad-happy-03.mp3 differ diff --git a/public/assets/videos/brad-happy-04.mp3 b/public/assets/videos/brad-happy-04.mp3 new file mode 100644 index 0000000..545958e Binary files /dev/null and b/public/assets/videos/brad-happy-04.mp3 differ diff --git a/public/assets/videos/brad-happy-05.mp3 b/public/assets/videos/brad-happy-05.mp3 new file mode 100644 index 0000000..3853619 Binary files /dev/null and b/public/assets/videos/brad-happy-05.mp3 differ diff --git a/public/assets/videos/brad-intro-01.mp3 b/public/assets/videos/brad-intro-01.mp3 new file mode 100644 index 0000000..705d805 Binary files /dev/null and b/public/assets/videos/brad-intro-01.mp3 differ diff --git a/public/assets/videos/brad-intro-02.mp3 b/public/assets/videos/brad-intro-02.mp3 new file mode 100644 index 0000000..26dc5b4 Binary files /dev/null and b/public/assets/videos/brad-intro-02.mp3 differ diff --git a/public/assets/videos/brad-unhappy.mp3 b/public/assets/videos/brad-unhappy.mp3 new file mode 100644 index 0000000..d2462d1 Binary files /dev/null and b/public/assets/videos/brad-unhappy.mp3 differ diff --git a/public/data/opponents.json b/public/data/opponents.json index dcb30b2..f0e4e68 100644 --- a/public/data/opponents.json +++ b/public/data/opponents.json @@ -94,7 +94,27 @@ "id": "ethel", "spriteIndex": 5, "name": "Ethel", - "bio": "Thank you for visiting with me." + "bio": "Thank you for visiting with me.", + "speech": { + "intro": [ + "ethel-intro-01", + "ethel-intro-02" + ], + "happy": [ + "ethel-happy-01", + "ethel-happy-02", + "ethel-happy-03", + "ethel-happy-04", + "ethel-happy-05" + ], + "upset": [ + "ethel-upset-01", + "ethel-upset-02", + "ethel-upset-03", + "ethel-upset-04", + "ethel-upset-05" + ] + } }, { "id": "jeff", @@ -118,7 +138,20 @@ "id": "bernie", "spriteIndex": 9, "name": "Bernie", - "bio": "Having fun and playing is the name of the game for me!" + "bio": "Having fun and playing is the name of the game for me!", + "speech": { + "intro": [ + "bernie-intro-01" + ], + "happy": [ + "bernie-happy-01", + "bernie-happy-02" + ], + "upset": [ + "bernie-upset-01", + "bernie-upset-02" + ] + } } ] } \ No newline at end of file diff --git a/public/src/ui/Portrait.js b/public/src/ui/Portrait.js index eb6dc83..fa8c1b4 100644 --- a/public/src/ui/Portrait.js +++ b/public/src/ui/Portrait.js @@ -75,6 +75,78 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius, const domEl = scene.add.dom(worldX, worldY, videoEl).setDepth(depth + 2); + // Speech visualizer canvas — overlays the portrait when audio is playing + const canvasEl = document.createElement('canvas'); + canvasEl.width = size; + canvasEl.height = size; + canvasEl.style.cssText = `width:${size}px;height:${size}px;border-radius:50%;pointer-events:none;opacity:0;transition:opacity 0.2s ease;`; + const canvasDom = scene.add.dom(worldX, worldY, canvasEl).setDepth(depth + 3); + const ctx = canvasEl.getContext('2d'); + + const BAR_COUNT = 6; + const bars = Array.from({ length: BAR_COUNT }, () => ({ cur: 0.1, target: 0.5 })); + let vizActive = false; + let rafId = null; + let retargetTimer = null; + let vizColor = [0, 200, 255]; + + function _vizFrame() { + if (!vizActive) { rafId = null; ctx.clearRect(0, 0, size, size); return; } + + ctx.clearRect(0, 0, size, size); + + const gy = size * 0.58; + const grad = ctx.createLinearGradient(0, gy, 0, size); + grad.addColorStop(0, 'rgba(0,0,0,0)'); + grad.addColorStop(1, 'rgba(0,0,0,0.38)'); + ctx.fillStyle = grad; + ctx.fillRect(0, gy, size, size - gy); + + const [r, g, b] = vizColor; + const barW = size * 0.072; + const gap = size * 0.03; + const totalW = BAR_COUNT * barW + (BAR_COUNT - 1) * gap; + const startX = (size - totalW) / 2; + const maxH = size * 0.36; + const baseY = size * 0.88; + + bars.forEach((bar, i) => { + bar.cur += (bar.target - bar.cur) * 0.14; + const h = Math.max(bar.cur * maxH, size * 0.04); + const x = startX + i * (barW + gap); + ctx.shadowColor = `rgba(${r},${g},${b},0.7)`; + ctx.shadowBlur = 5; + ctx.fillStyle = `rgba(${r},${g},${b},0.88)`; + ctx.beginPath(); + ctx.roundRect(x, baseY - h, barW, h, 2); + ctx.fill(); + }); + ctx.shadowBlur = 0; + + rafId = requestAnimationFrame(_vizFrame); + } + + function startVisualizer(emotion) { + if (emotion === 'happy') vizColor = [0, 210, 60]; + else if (emotion === 'upset') vizColor = [210, 60, 60]; + else vizColor = [0, 200, 255]; + vizActive = true; + canvasEl.style.opacity = '1'; + if (!retargetTimer) { + retargetTimer = setInterval(() => { + bars.forEach(b => { b.target = 0.2 + Math.random() * 0.8; }); + }, 120); + } + if (!rafId) rafId = requestAnimationFrame(_vizFrame); + } + + function stopVisualizer() { + vizActive = false; + canvasEl.style.opacity = '0'; + clearInterval(retargetTimer); + retargetTimer = null; + } + let emotionPlaying = false; function playEmotion(emotion) { @@ -101,7 +173,10 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius, const speechClips = opponent?.speech?.[emotion]; if (speechClips?.length && Math.random() < 0.6) { - enqueueSpeech(speechClips[Math.floor(Math.random() * speechClips.length)]); + enqueueSpeech(speechClips[Math.floor(Math.random() * speechClips.length)], { + onStart: () => startVisualizer(emotion), + onEnd: stopVisualizer, + }); } } @@ -121,13 +196,21 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius, videoEl.pause(); videoEl.src = ''; resetSpeechQueue(); + vizActive = false; + if (rafId) { cancelAnimationFrame(rafId); rafId = null; } + clearInterval(retargetTimer); + retargetTimer = null; + canvasDom.destroy(); } scene.events.once('shutdown', destroy); if (opponent?.speech?.intro?.length) { const clips = opponent.speech.intro; - enqueueSpeech(clips[Math.floor(Math.random() * clips.length)]); + enqueueSpeech(clips[Math.floor(Math.random() * clips.length)], { + onStart: () => startVisualizer('intro'), + onEnd: stopVisualizer, + }); } return { playEmotion, hide, show, destroy }; diff --git a/public/src/ui/SpeechQueue.js b/public/src/ui/SpeechQueue.js index 6e94ca7..0033462 100644 --- a/public/src/ui/SpeechQueue.js +++ b/public/src/ui/SpeechQueue.js @@ -1,27 +1,41 @@ let _queue = []; let _playing = false; let _currentAudio = null; +let _currentCbs = null; const MAX = 4; -export function enqueue(filename) { +export function enqueue(filename, callbacks) { if (_queue.length + (_playing ? 1 : 0) >= MAX) return; - _queue.push(filename); + _queue.push({ filename, onStart: callbacks?.onStart, onEnd: callbacks?.onEnd }); _playNext(); } function _playNext() { if (_playing || !_queue.length) return; _playing = true; - _currentAudio = new Audio(`/assets/speech/${_queue.shift()}.mp3`); + const entry = _queue.shift(); + _currentCbs = entry; + _currentAudio = new Audio(`/assets/speech/${entry.filename}.mp3`); _currentAudio.volume = 0.8; - const done = () => { _playing = false; _currentAudio = null; _playNext(); }; + const done = () => { + entry.onEnd?.(); + _playing = false; + _currentAudio = null; + _currentCbs = null; + _playNext(); + }; _currentAudio.onended = done; _currentAudio.onerror = done; - _currentAudio.play().catch(done); + _currentAudio.play().then(() => entry.onStart?.()).catch(done); } export function resetQueue() { - if (_currentAudio) { _currentAudio.pause(); _currentAudio = null; } + if (_currentAudio) { + _currentCbs?.onEnd?.(); + _currentAudio.pause(); + _currentAudio = null; + } + _currentCbs = null; _queue = []; _playing = false; } diff --git a/server/db/game.db b/server/db/game.db new file mode 100644 index 0000000..e69de29