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 = true; const MIN_MS = 20_000; const MAX_MS = 200_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); }