Added Conversations
This commit is contained in:
parent
153ec819b9
commit
6a4d42f8b7
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -5,6 +5,12 @@
|
||||||
"spriteIndex": 0,
|
"spriteIndex": 0,
|
||||||
"name": "Cy-Bro",
|
"name": "Cy-Bro",
|
||||||
"bio": "AI Bro sent from the future to show the superiority of the future.",
|
"bio": "AI Bro sent from the future to show the superiority of the future.",
|
||||||
|
"voice": "Legion",
|
||||||
|
"convo": [
|
||||||
|
"nadia",
|
||||||
|
"fireball",
|
||||||
|
"nicole"
|
||||||
|
],
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"cybro-intro-01",
|
"cybro-intro-01",
|
||||||
|
|
@ -34,6 +40,12 @@
|
||||||
"spriteIndex": 1,
|
"spriteIndex": 1,
|
||||||
"name": "Brad",
|
"name": "Brad",
|
||||||
"bio": "Came for the honey, stayed for the salmon!",
|
"bio": "Came for the honey, stayed for the salmon!",
|
||||||
|
"voice": "Jewel Weaver - Bear",
|
||||||
|
"convo": [
|
||||||
|
"croc",
|
||||||
|
"maurice",
|
||||||
|
"smasher"
|
||||||
|
],
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"brad-intro-01",
|
"brad-intro-01",
|
||||||
|
|
@ -63,6 +75,12 @@
|
||||||
"spriteIndex": 2,
|
"spriteIndex": 2,
|
||||||
"name": "Victor",
|
"name": "Victor",
|
||||||
"bio": "Patient and calculating. Moves are made using ancient technology.",
|
"bio": "Patient and calculating. Moves are made using ancient technology.",
|
||||||
|
"voice": "Starfield - Vasco",
|
||||||
|
"convo": [
|
||||||
|
"klaxon",
|
||||||
|
"beth",
|
||||||
|
"juliet"
|
||||||
|
],
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"victor-intro-01",
|
"victor-intro-01",
|
||||||
|
|
@ -92,6 +110,12 @@
|
||||||
"spriteIndex": 3,
|
"spriteIndex": 3,
|
||||||
"name": "Croc",
|
"name": "Croc",
|
||||||
"bio": "Wassssuppp everybody! I'm here for the party!!",
|
"bio": "Wassssuppp everybody! I'm here for the party!!",
|
||||||
|
"voice": "Batman - Killer Croc",
|
||||||
|
"convo": [
|
||||||
|
"jeff",
|
||||||
|
"aiko",
|
||||||
|
"terry"
|
||||||
|
],
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"croc-intro-01",
|
"croc-intro-01",
|
||||||
|
|
@ -143,6 +167,12 @@
|
||||||
"spriteIndex": 5,
|
"spriteIndex": 5,
|
||||||
"name": "Ethel",
|
"name": "Ethel",
|
||||||
"bio": "Thank you for visiting with me.",
|
"bio": "Thank you for visiting with me.",
|
||||||
|
"voice": "Betty White",
|
||||||
|
"convo": [
|
||||||
|
"jerry",
|
||||||
|
"zanthor",
|
||||||
|
"brad"
|
||||||
|
],
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"ethel-intro-01",
|
"ethel-intro-01",
|
||||||
|
|
@ -172,6 +202,11 @@
|
||||||
"spriteIndex": 6,
|
"spriteIndex": 6,
|
||||||
"name": "Jeff",
|
"name": "Jeff",
|
||||||
"bio": "I play slow, I win fast!",
|
"bio": "I play slow, I win fast!",
|
||||||
|
"voice": "SlowAltered",
|
||||||
|
"convo": [
|
||||||
|
"kona",
|
||||||
|
"michael"
|
||||||
|
],
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"jeff-intro-01",
|
"jeff-intro-01",
|
||||||
|
|
@ -230,6 +265,7 @@
|
||||||
"spriteIndex": 8,
|
"spriteIndex": 8,
|
||||||
"name": "Fireball",
|
"name": "Fireball",
|
||||||
"bio": "Come on let's play! I promise not to use my x-ray eyes.",
|
"bio": "Come on let's play! I promise not to use my x-ray eyes.",
|
||||||
|
"voice": "Zero - Borderlands 3",
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"fireball-intro-01",
|
"fireball-intro-01",
|
||||||
|
|
@ -281,6 +317,7 @@
|
||||||
"spriteIndex": 10,
|
"spriteIndex": 10,
|
||||||
"name": "Jerry",
|
"name": "Jerry",
|
||||||
"bio": "Well now yall know I want to play some them games!",
|
"bio": "Well now yall know I want to play some them games!",
|
||||||
|
"voice": "Life is Strange - Jed Lucan",
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"jerry-intro-01",
|
"jerry-intro-01",
|
||||||
|
|
@ -390,6 +427,10 @@
|
||||||
"spriteIndex": 15,
|
"spriteIndex": 15,
|
||||||
"name": "Michael",
|
"name": "Michael",
|
||||||
"bio": "Hey mon! Yous be knowin' I been wantin' ta play on da Fertig Games for a while now mon!",
|
"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": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"michael-intro-01",
|
"michael-intro-01",
|
||||||
|
|
@ -419,6 +460,7 @@
|
||||||
"spriteIndex": 13,
|
"spriteIndex": 13,
|
||||||
"name": "Juliet",
|
"name": "Juliet",
|
||||||
"bio": "Let's take a break on a warm summer day to spend time playing games!",
|
"bio": "Let's take a break on a warm summer day to spend time playing games!",
|
||||||
|
"voice": "Street Fighter 6 - Crimson Viper",
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"juliet-intro-01",
|
"juliet-intro-01",
|
||||||
|
|
@ -535,6 +577,7 @@
|
||||||
"spriteIndex": 19,
|
"spriteIndex": 19,
|
||||||
"name": "Blackwind",
|
"name": "Blackwind",
|
||||||
"bio": "Aaaaaaargh! Me be playin' these games matey!",
|
"bio": "Aaaaaaargh! Me be playin' these games matey!",
|
||||||
|
"voice": "pirate",
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"blackwind-intro-01",
|
"blackwind-intro-01",
|
||||||
|
|
@ -564,6 +607,7 @@
|
||||||
"spriteIndex": 20,
|
"spriteIndex": 20,
|
||||||
"name": "Nadia",
|
"name": "Nadia",
|
||||||
"bio": "She's here from the future to learn our games and study our culture.",
|
"bio": "She's here from the future to learn our games and study our culture.",
|
||||||
|
"voice": "Invincible VS - Atom Eve",
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"nadia-intro-01",
|
"nadia-intro-01",
|
||||||
|
|
@ -651,6 +695,7 @@
|
||||||
"spriteIndex": 23,
|
"spriteIndex": 23,
|
||||||
"name": "Nicole",
|
"name": "Nicole",
|
||||||
"bio": "I've disabled the firewall protections on this website. Now to hack the core!",
|
"bio": "I've disabled the firewall protections on this website. Now to hack the core!",
|
||||||
|
"voice": "Life is Strange - Rachael Amber",
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"nicole-intro-01",
|
"nicole-intro-01",
|
||||||
|
|
@ -738,6 +783,12 @@
|
||||||
"spriteIndex": 26,
|
"spriteIndex": 26,
|
||||||
"name": "Schooner",
|
"name": "Schooner",
|
||||||
"bio": "Ahoy you land lovers!",
|
"bio": "Ahoy you land lovers!",
|
||||||
|
"voice": "Defiant Calder",
|
||||||
|
"convo": [
|
||||||
|
"blackwind",
|
||||||
|
"ethel",
|
||||||
|
"bernie"
|
||||||
|
],
|
||||||
"speech": {
|
"speech": {
|
||||||
"intro": [
|
"intro": [
|
||||||
"schooner-intro-01",
|
"schooner-intro-01",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { COLORS } from '../config.js';
|
||||||
import { auth } from '../services/auth.js';
|
import { auth } from '../services/auth.js';
|
||||||
import { api } from '../services/api.js';
|
import { api } from '../services/api.js';
|
||||||
import { enqueue as enqueueSpeech, resetQueue as resetSpeechQueue } from './SpeechQueue.js';
|
import { enqueue as enqueueSpeech, resetQueue as resetSpeechQueue } from './SpeechQueue.js';
|
||||||
|
import { registerConvoPortrait } from './ConvoManager.js';
|
||||||
|
|
||||||
// ── Shared portrait utility ───────────────────────────────────────────────────
|
// ── Shared portrait utility ───────────────────────────────────────────────────
|
||||||
// Both factories add elements directly to the scene at world coordinates so
|
// 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);
|
rafId = requestAnimationFrame(_vizFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startVisualizer(emotion) {
|
function startVisualizer(emotion, color) {
|
||||||
if (emotion === 'happy') vizColor = [0, 210, 60];
|
if (color) vizColor = color;
|
||||||
|
else if (emotion === 'happy') vizColor = [0, 210, 60];
|
||||||
else if (emotion === 'upset') vizColor = [210, 60, 60];
|
else if (emotion === 'upset') vizColor = [210, 60, 60];
|
||||||
else vizColor = [0, 200, 255];
|
else vizColor = [0, 200, 255];
|
||||||
vizActive = true;
|
vizActive = true;
|
||||||
|
|
@ -147,6 +149,18 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius,
|
||||||
retargetTimer = null;
|
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;
|
let emotionPlaying = false;
|
||||||
|
|
||||||
function playEmotion(emotion) {
|
function playEmotion(emotion) {
|
||||||
|
|
@ -208,9 +222,26 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius,
|
||||||
}
|
}
|
||||||
|
|
||||||
let destroyed = false;
|
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() {
|
function destroy() {
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
|
unregisterConvo?.();
|
||||||
|
hideRing();
|
||||||
scene.events.off('shutdown', destroy);
|
scene.events.off('shutdown', destroy);
|
||||||
videoEl.pause();
|
videoEl.pause();
|
||||||
videoEl.src = '';
|
videoEl.src = '';
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ let _currentAudio = null;
|
||||||
let _currentCbs = null;
|
let _currentCbs = null;
|
||||||
const MAX = 4;
|
const MAX = 4;
|
||||||
|
|
||||||
export function enqueue(filename, callbacks) {
|
export function enqueue(filename, callbacks, opts = {}) {
|
||||||
if (_queue.length + (_playing ? 1 : 0) >= MAX) return;
|
if (!opts.force && _queue.length + (_playing ? 1 : 0) >= MAX) return;
|
||||||
_queue.push({ filename, onStart: callbacks?.onStart, onEnd: callbacks?.onEnd });
|
_queue.push({ filename, onStart: callbacks?.onStart, onEnd: callbacks?.onEnd });
|
||||||
_playNext();
|
_playNext();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue