diff --git a/public/assets/speech/convo/brad-croc.mp3 b/public/assets/speech/convo/brad-croc.mp3 new file mode 100644 index 0000000..7afbc40 Binary files /dev/null and b/public/assets/speech/convo/brad-croc.mp3 differ diff --git a/public/assets/speech/convo/brad-maurice.mp3 b/public/assets/speech/convo/brad-maurice.mp3 new file mode 100644 index 0000000..e972ec7 Binary files /dev/null and b/public/assets/speech/convo/brad-maurice.mp3 differ diff --git a/public/assets/speech/convo/brad-smasher.mp3 b/public/assets/speech/convo/brad-smasher.mp3 new file mode 100644 index 0000000..16ddd76 Binary files /dev/null and b/public/assets/speech/convo/brad-smasher.mp3 differ diff --git a/public/assets/speech/convo/croc-aiko.mp3 b/public/assets/speech/convo/croc-aiko.mp3 new file mode 100644 index 0000000..a27f242 Binary files /dev/null and b/public/assets/speech/convo/croc-aiko.mp3 differ diff --git a/public/assets/speech/convo/croc-jeff.mp3 b/public/assets/speech/convo/croc-jeff.mp3 new file mode 100644 index 0000000..613da84 Binary files /dev/null and b/public/assets/speech/convo/croc-jeff.mp3 differ diff --git a/public/assets/speech/convo/croc-terry.mp3 b/public/assets/speech/convo/croc-terry.mp3 new file mode 100644 index 0000000..ab1b9de Binary files /dev/null and b/public/assets/speech/convo/croc-terry.mp3 differ diff --git a/public/assets/speech/convo/cybro-fireball.mp3 b/public/assets/speech/convo/cybro-fireball.mp3 new file mode 100644 index 0000000..886dad4 Binary files /dev/null and b/public/assets/speech/convo/cybro-fireball.mp3 differ diff --git a/public/assets/speech/convo/cybro-nadia.mp3 b/public/assets/speech/convo/cybro-nadia.mp3 new file mode 100644 index 0000000..edb0980 Binary files /dev/null and b/public/assets/speech/convo/cybro-nadia.mp3 differ diff --git a/public/assets/speech/convo/cybro-nicole.mp3 b/public/assets/speech/convo/cybro-nicole.mp3 new file mode 100644 index 0000000..902bf44 Binary files /dev/null and b/public/assets/speech/convo/cybro-nicole.mp3 differ diff --git a/public/assets/speech/convo/ethel-brad.mp3 b/public/assets/speech/convo/ethel-brad.mp3 new file mode 100644 index 0000000..eb93e8e Binary files /dev/null and b/public/assets/speech/convo/ethel-brad.mp3 differ diff --git a/public/assets/speech/convo/ethel-jerry.mp3 b/public/assets/speech/convo/ethel-jerry.mp3 new file mode 100644 index 0000000..48bdb8b Binary files /dev/null and b/public/assets/speech/convo/ethel-jerry.mp3 differ diff --git a/public/assets/speech/convo/ethel-zanthor.mp3 b/public/assets/speech/convo/ethel-zanthor.mp3 new file mode 100644 index 0000000..feab9e5 Binary files /dev/null and b/public/assets/speech/convo/ethel-zanthor.mp3 differ diff --git a/public/assets/speech/convo/jeff-kona.mp3 b/public/assets/speech/convo/jeff-kona.mp3 new file mode 100644 index 0000000..df2c494 Binary files /dev/null and b/public/assets/speech/convo/jeff-kona.mp3 differ diff --git a/public/assets/speech/convo/jeff-michael.mp3 b/public/assets/speech/convo/jeff-michael.mp3 new file mode 100644 index 0000000..f50882c Binary files /dev/null and b/public/assets/speech/convo/jeff-michael.mp3 differ diff --git a/public/assets/speech/convo/schooner-bernie.mp3 b/public/assets/speech/convo/schooner-bernie.mp3 new file mode 100644 index 0000000..45ea86e Binary files /dev/null and b/public/assets/speech/convo/schooner-bernie.mp3 differ diff --git a/public/assets/speech/convo/schooner-blackwind.mp3 b/public/assets/speech/convo/schooner-blackwind.mp3 new file mode 100644 index 0000000..3c6e0ad Binary files /dev/null and b/public/assets/speech/convo/schooner-blackwind.mp3 differ diff --git a/public/assets/speech/convo/schooner-ethel.mp3 b/public/assets/speech/convo/schooner-ethel.mp3 new file mode 100644 index 0000000..4ff8993 Binary files /dev/null and b/public/assets/speech/convo/schooner-ethel.mp3 differ diff --git a/public/assets/speech/convo/victor-beth.mp3 b/public/assets/speech/convo/victor-beth.mp3 new file mode 100644 index 0000000..b7f89ee Binary files /dev/null and b/public/assets/speech/convo/victor-beth.mp3 differ diff --git a/public/assets/speech/convo/victor-juliet.mp3 b/public/assets/speech/convo/victor-juliet.mp3 new file mode 100644 index 0000000..a86483c Binary files /dev/null and b/public/assets/speech/convo/victor-juliet.mp3 differ diff --git a/public/assets/speech/convo/victor-klaxon.mp3 b/public/assets/speech/convo/victor-klaxon.mp3 new file mode 100644 index 0000000..6a53b66 Binary files /dev/null and b/public/assets/speech/convo/victor-klaxon.mp3 differ diff --git a/public/assets/speech/schooner-happy-03.mp3 b/public/assets/speech/schooner-happy-03.mp3 index 12a7ea6..ca5136f 100644 Binary files a/public/assets/speech/schooner-happy-03.mp3 and b/public/assets/speech/schooner-happy-03.mp3 differ diff --git a/public/assets/speech/schooner-upset-03.mp3 b/public/assets/speech/schooner-upset-03.mp3 index c5e5a43..fa4ecfa 100644 Binary files a/public/assets/speech/schooner-upset-03.mp3 and b/public/assets/speech/schooner-upset-03.mp3 differ diff --git a/public/assets/speech/schooner-upset-04.mp3 b/public/assets/speech/schooner-upset-04.mp3 index 8bfd42f..78d2e87 100644 Binary files a/public/assets/speech/schooner-upset-04.mp3 and b/public/assets/speech/schooner-upset-04.mp3 differ diff --git a/public/assets/speech/schooner-upset-05.mp3 b/public/assets/speech/schooner-upset-05.mp3 index 9a5fecc..b044edc 100644 Binary files a/public/assets/speech/schooner-upset-05.mp3 and b/public/assets/speech/schooner-upset-05.mp3 differ diff --git a/public/data/opponents.json b/public/data/opponents.json index 257fdeb..5168ca5 100644 --- a/public/data/opponents.json +++ b/public/data/opponents.json @@ -5,6 +5,12 @@ "spriteIndex": 0, "name": "Cy-Bro", "bio": "AI Bro sent from the future to show the superiority of the future.", + "voice": "Legion", + "convo": [ + "nadia", + "fireball", + "nicole" + ], "speech": { "intro": [ "cybro-intro-01", @@ -34,6 +40,12 @@ "spriteIndex": 1, "name": "Brad", "bio": "Came for the honey, stayed for the salmon!", + "voice": "Jewel Weaver - Bear", + "convo": [ + "croc", + "maurice", + "smasher" + ], "speech": { "intro": [ "brad-intro-01", @@ -63,6 +75,12 @@ "spriteIndex": 2, "name": "Victor", "bio": "Patient and calculating. Moves are made using ancient technology.", + "voice": "Starfield - Vasco", + "convo": [ + "klaxon", + "beth", + "juliet" + ], "speech": { "intro": [ "victor-intro-01", @@ -92,6 +110,12 @@ "spriteIndex": 3, "name": "Croc", "bio": "Wassssuppp everybody! I'm here for the party!!", + "voice": "Batman - Killer Croc", + "convo": [ + "jeff", + "aiko", + "terry" + ], "speech": { "intro": [ "croc-intro-01", @@ -143,6 +167,12 @@ "spriteIndex": 5, "name": "Ethel", "bio": "Thank you for visiting with me.", + "voice": "Betty White", + "convo": [ + "jerry", + "zanthor", + "brad" + ], "speech": { "intro": [ "ethel-intro-01", @@ -172,6 +202,11 @@ "spriteIndex": 6, "name": "Jeff", "bio": "I play slow, I win fast!", + "voice": "SlowAltered", + "convo": [ + "kona", + "michael" + ], "speech": { "intro": [ "jeff-intro-01", @@ -230,6 +265,7 @@ "spriteIndex": 8, "name": "Fireball", "bio": "Come on let's play! I promise not to use my x-ray eyes.", + "voice": "Zero - Borderlands 3", "speech": { "intro": [ "fireball-intro-01", @@ -281,6 +317,7 @@ "spriteIndex": 10, "name": "Jerry", "bio": "Well now yall know I want to play some them games!", + "voice": "Life is Strange - Jed Lucan", "speech": { "intro": [ "jerry-intro-01", @@ -390,6 +427,10 @@ "spriteIndex": 15, "name": "Michael", "bio": "Hey mon! Yous be knowin' I been wantin' ta play on da Fertig Games for a while now mon!", + "voice": "Djed Spence", + "convo": [ + "michael" + ], "speech": { "intro": [ "michael-intro-01", @@ -419,6 +460,7 @@ "spriteIndex": 13, "name": "Juliet", "bio": "Let's take a break on a warm summer day to spend time playing games!", + "voice": "Street Fighter 6 - Crimson Viper", "speech": { "intro": [ "juliet-intro-01", @@ -535,6 +577,7 @@ "spriteIndex": 19, "name": "Blackwind", "bio": "Aaaaaaargh! Me be playin' these games matey!", + "voice": "pirate", "speech": { "intro": [ "blackwind-intro-01", @@ -564,6 +607,7 @@ "spriteIndex": 20, "name": "Nadia", "bio": "She's here from the future to learn our games and study our culture.", + "voice": "Invincible VS - Atom Eve", "speech": { "intro": [ "nadia-intro-01", @@ -651,6 +695,7 @@ "spriteIndex": 23, "name": "Nicole", "bio": "I've disabled the firewall protections on this website. Now to hack the core!", + "voice": "Life is Strange - Rachael Amber", "speech": { "intro": [ "nicole-intro-01", @@ -738,6 +783,12 @@ "spriteIndex": 26, "name": "Schooner", "bio": "Ahoy you land lovers!", + "voice": "Defiant Calder", + "convo": [ + "blackwind", + "ethel", + "bernie" + ], "speech": { "intro": [ "schooner-intro-01", diff --git a/public/src/ui/ConvoManager.js b/public/src/ui/ConvoManager.js new file mode 100644 index 0000000..054e911 --- /dev/null +++ b/public/src/ui/ConvoManager.js @@ -0,0 +1,111 @@ +import { enqueue as enqueueSpeech } from './SpeechQueue.js'; + +// ── Opponent conversation coordinator ───────────────────────────────────────── +// Global, per-scene system: when two opponents that list each other (via the +// `convo` array in opponents.json) are both present in the same game, they can +// have a recorded conversation. Each eligible directed pair (owner→match) gets +// its own independent random 1–5 min one-shot timer and plays once per match. +// Conversations always queue (bypassing the speech-queue cap) and play +// sequentially. While playing, both portraits show an orange ring + speech +// visualizer for the clip's duration. +// +// This rides on the shared Portrait factory, so it applies to every game with +// no per-game wiring. + +const DEBUG_CONVOS = false; + +const MIN_MS = 60_000; +const MAX_MS = 300_000; +const ORANGE_RGB = [255, 122, 0]; + +function rand(min, max) { + return min + Math.random() * (max - min); +} + +class ConvoManager { + constructor(scene) { + this.scene = scene; + this.entries = []; + this.armed = false; + this._armTimer = null; + } + + register(entry) { + this.entries.push(entry); + // Portraits register synchronously during scene `create`; debounce arming + // so we only schedule timers once every portrait has registered. + if (!this.armed) { + this._armTimer?.remove(); + this._armTimer = this.scene.time.delayedCall(0, () => this._armAll()); + } + return () => this._unregister(entry); + } + + _unregister(entry) { + const i = this.entries.indexOf(entry); + if (i >= 0) this.entries.splice(i, 1); + } + + _armAll() { + if (this.armed) return; + this.armed = true; + this._armTimer = null; + + const seen = new Set(); + const debugMatches = []; + for (const a of this.entries) { + const convo = a.opponent?.convo; + if (!Array.isArray(convo)) continue; + for (const matchId of convo) { + const b = this.entries.find(e => e !== a && e.opponent?.id === matchId); + if (!b) continue; + const key = `${a.opponent.id}->${b.opponent.id}`; + if (seen.has(key)) continue; + seen.add(key); + const delay = rand(MIN_MS, MAX_MS); + if (DEBUG_CONVOS) debugMatches.push({ pair: key, file: `convo/${a.opponent.id}-${b.opponent.id}.mp3`, delayMs: delay, playsAt: `${(delay / 1000).toFixed(1)}s` }); + this.scene.time.delayedCall(delay, () => this._playConvo(a, b)); + } + } + if (DEBUG_CONVOS) { + if (debugMatches.length) { + console.log('[ConvoManager] Scheduled conversations:', debugMatches); + } else { + console.log('[ConvoManager] No conversation matches found among present opponents:', this.entries.map(e => e.opponent?.id)); + } + } + } + + _playConvo(a, b) { + if (!a.isAlive() || !b.isAlive()) return; + enqueueSpeech(`convo/${a.opponent.id}-${b.opponent.id}`, { + onStart: () => { + a.startSpeaking(ORANGE_RGB); b.startSpeaking(ORANGE_RGB); + a.showRing(); b.showRing(); + }, + onEnd: () => { + a.stopSpeaking(); b.stopSpeaking(); + a.hideRing(); b.hideRing(); + }, + }, { force: true }); + } + + destroy() { + this._armTimer?.remove(); + this._armTimer = null; + this.entries = []; + } +} + +export function registerConvoPortrait(scene, entry) { + let mgr = scene.__convoManager; + if (!mgr) { + mgr = new ConvoManager(scene); + scene.__convoManager = mgr; + scene.events.once('shutdown', () => { + mgr.destroy(); + if (scene.__convoManager === mgr) scene.__convoManager = null; + }); + } + return mgr.register(entry); +} diff --git a/public/src/ui/Portrait.js b/public/src/ui/Portrait.js index 62bae3c..917402d 100644 --- a/public/src/ui/Portrait.js +++ b/public/src/ui/Portrait.js @@ -2,6 +2,7 @@ import { COLORS } from '../config.js'; import { auth } from '../services/auth.js'; import { api } from '../services/api.js'; import { enqueue as enqueueSpeech, resetQueue as resetSpeechQueue } from './SpeechQueue.js'; +import { registerConvoPortrait } from './ConvoManager.js'; // ── Shared portrait utility ─────────────────────────────────────────────────── // Both factories add elements directly to the scene at world coordinates so @@ -126,8 +127,9 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius, rafId = requestAnimationFrame(_vizFrame); } - function startVisualizer(emotion) { - if (emotion === 'happy') vizColor = [0, 210, 60]; + function startVisualizer(emotion, color) { + if (color) vizColor = color; + else if (emotion === 'happy') vizColor = [0, 210, 60]; else if (emotion === 'upset') vizColor = [210, 60, 60]; else vizColor = [0, 200, 255]; vizActive = true; @@ -147,6 +149,18 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius, retargetTimer = null; } + // Orange conversation ring — drawn around the portrait while a convo plays + let ringG = null; + function showRing() { + if (ringG) return; + ringG = scene.add.graphics().setDepth(depth + 4); + ringG.lineStyle(6, 0xff7a00, 1); + ringG.strokeCircle(worldX, worldY, radius + 5); + } + function hideRing() { + if (ringG) { ringG.destroy(); ringG = null; } + } + let emotionPlaying = false; function playEmotion(emotion) { @@ -208,9 +222,26 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius, } let destroyed = false; + + // Register with the per-scene conversation coordinator so any two opponents + // that list each other in their `convo` arrays can talk during the match. + let unregisterConvo = null; + if (opponent?.id) { + unregisterConvo = registerConvoPortrait(scene, { + opponent, + startSpeaking: (rgb) => startVisualizer('convo', rgb), + stopSpeaking: stopVisualizer, + showRing, + hideRing, + isAlive: () => !destroyed, + }); + } + function destroy() { if (destroyed) return; destroyed = true; + unregisterConvo?.(); + hideRing(); scene.events.off('shutdown', destroy); videoEl.pause(); videoEl.src = ''; diff --git a/public/src/ui/SpeechQueue.js b/public/src/ui/SpeechQueue.js index 0033462..756a3b3 100644 --- a/public/src/ui/SpeechQueue.js +++ b/public/src/ui/SpeechQueue.js @@ -4,8 +4,8 @@ let _currentAudio = null; let _currentCbs = null; const MAX = 4; -export function enqueue(filename, callbacks) { - if (_queue.length + (_playing ? 1 : 0) >= MAX) return; +export function enqueue(filename, callbacks, opts = {}) { + if (!opts.force && _queue.length + (_playing ? 1 : 0) >= MAX) return; _queue.push({ filename, onStart: callbacks?.onStart, onEnd: callbacks?.onEnd }); _playNext(); }