fertig-classic-games/src/ui/ConvoManager.js

112 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 15 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);
}