feat: add speech queue system and update audio configuration
- Implement `SpeechQueue` to manage opponent dialogue clips - Add `speech` configuration to opponent data (Cy-Bro, Victor, Croc) with intro, happy, and upset states - Trigger random speech clips in `Portrait.js` based on emotion and a 60% probability - Play intro speech clips when opponent portraits are created - Update `MusicPlayer` volume to 0.15 and clean up formatting/whitespace
This commit is contained in:
parent
4b0551c3cd
commit
f6a4a3fb5d
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.
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.
|
|
@ -4,7 +4,27 @@
|
|||
"id": "cybro",
|
||||
"spriteIndex": 0,
|
||||
"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.",
|
||||
"speech": {
|
||||
"intro": [
|
||||
"cybro-intro-01",
|
||||
"cybro-intro-02"
|
||||
],
|
||||
"happy": [
|
||||
"cybro-happy-01",
|
||||
"cybro-happy-02",
|
||||
"cybro-happy-03",
|
||||
"cybro-happy-04",
|
||||
"cybro-happy-05"
|
||||
],
|
||||
"upset": [
|
||||
"cybro-upset-01",
|
||||
"cybro-upset-02",
|
||||
"cybro-upset-03",
|
||||
"cybro-upset-04",
|
||||
"cybro-upset-05"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "brad",
|
||||
|
|
@ -16,13 +36,53 @@
|
|||
"id": "victor",
|
||||
"spriteIndex": 2,
|
||||
"name": "Victor",
|
||||
"bio": "Patient and calculating. Moves are made using ancient technology."
|
||||
"bio": "Patient and calculating. Moves are made using ancient technology.",
|
||||
"speech": {
|
||||
"intro": [
|
||||
"victor-intro-01",
|
||||
"victor-intro-02"
|
||||
],
|
||||
"happy": [
|
||||
"victor-happy-01",
|
||||
"victor-happy-02",
|
||||
"victor-happy-03",
|
||||
"victor-happy-04",
|
||||
"victor-happy-05"
|
||||
],
|
||||
"upset": [
|
||||
"victor-upset-01",
|
||||
"victor-upset-02",
|
||||
"victor-upset-03",
|
||||
"victor-upset-04",
|
||||
"victor-upset-05"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "croc",
|
||||
"spriteIndex": 3,
|
||||
"name": "Croc",
|
||||
"bio": "Wassssuppp everybody! I'm here for the party!!"
|
||||
"bio": "Wassssuppp everybody! I'm here for the party!!",
|
||||
"speech": {
|
||||
"intro": [
|
||||
"croc-intro-01",
|
||||
"croc-intro-02"
|
||||
],
|
||||
"happy": [
|
||||
"croc-happy-01",
|
||||
"croc-happy-02",
|
||||
"croc-happy-03",
|
||||
"croc-happy-04",
|
||||
"croc-happy-05"
|
||||
],
|
||||
"upset": [
|
||||
"croc-upset-01",
|
||||
"croc-upset-02",
|
||||
"croc-upset-03",
|
||||
"croc-upset-04",
|
||||
"croc-upset-05"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mario",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import * as Phaser from 'phaser';
|
||||
import { COLORS, GAME_WIDTH } from '../config.js';
|
||||
|
||||
const BTN = 32;
|
||||
const GAP = 6;
|
||||
const PAD = 12;
|
||||
const BTN = 32;
|
||||
const GAP = 6;
|
||||
const PAD = 12;
|
||||
const DEPTH = 90;
|
||||
|
||||
function shuffle(arr) {
|
||||
|
|
@ -17,25 +17,25 @@ function shuffle(arr) {
|
|||
|
||||
export class MusicPlayer {
|
||||
constructor(scene, tracks) {
|
||||
this.scene = scene;
|
||||
this.scene = scene;
|
||||
this.tracks = shuffle(tracks);
|
||||
this.idx = 0;
|
||||
this.muted = false;
|
||||
this.idx = 0;
|
||||
this.muted = false;
|
||||
this._audio = null;
|
||||
this._objs = [];
|
||||
this._objs = [];
|
||||
|
||||
this._buildUI();
|
||||
this._play(0);
|
||||
|
||||
scene.events.once('shutdown', () => this.destroy());
|
||||
scene.events.once('destroy', () => this.destroy());
|
||||
scene.events.once('destroy', () => this.destroy());
|
||||
}
|
||||
|
||||
_buildUI() {
|
||||
const s = this.scene;
|
||||
const s = this.scene;
|
||||
const muteX = GAME_WIDTH - PAD - BTN / 2;
|
||||
const skipX = muteX - GAP - BTN;
|
||||
const btnY = PAD + BTN / 2;
|
||||
const btnY = PAD + BTN / 2;
|
||||
const infoY = PAD + BTN + GAP + 8;
|
||||
|
||||
this._muteX = muteX;
|
||||
|
|
@ -48,9 +48,9 @@ export class MusicPlayer {
|
|||
Phaser.Geom.Rectangle.Contains,
|
||||
);
|
||||
skipBg.input.cursor = 'pointer';
|
||||
skipBg.on('pointerover', () => this._drawBg(skipBg, skipX, btnY, true));
|
||||
skipBg.on('pointerout', () => this._drawBg(skipBg, skipX, btnY, false));
|
||||
skipBg.on('pointerdown', () => this.next());
|
||||
skipBg.on('pointerover', () => this._drawBg(skipBg, skipX, btnY, true));
|
||||
skipBg.on('pointerout', () => this._drawBg(skipBg, skipX, btnY, false));
|
||||
skipBg.on('pointerdown', () => this.next());
|
||||
|
||||
const skipIcon = s.add.text(skipX, btnY, '⏭', {
|
||||
fontFamily: 'sans-serif', fontSize: '17px', color: COLORS.textHex,
|
||||
|
|
@ -63,9 +63,9 @@ export class MusicPlayer {
|
|||
Phaser.Geom.Rectangle.Contains,
|
||||
);
|
||||
muteBg.input.cursor = 'pointer';
|
||||
muteBg.on('pointerover', () => this._drawBg(muteBg, muteX, btnY, true));
|
||||
muteBg.on('pointerout', () => this._drawBg(muteBg, muteX, btnY, false));
|
||||
muteBg.on('pointerdown', () => this.toggleMute());
|
||||
muteBg.on('pointerover', () => this._drawBg(muteBg, muteX, btnY, true));
|
||||
muteBg.on('pointerout', () => this._drawBg(muteBg, muteX, btnY, false));
|
||||
muteBg.on('pointerdown', () => this.toggleMute());
|
||||
|
||||
this._muteIcon = s.add.text(muteX, btnY, '♪', {
|
||||
fontFamily: 'sans-serif', fontSize: '17px', color: COLORS.textHex,
|
||||
|
|
@ -97,9 +97,9 @@ export class MusicPlayer {
|
|||
}
|
||||
const track = this.tracks[idx];
|
||||
this._audio = new Audio(`/assets/music/${track.file}`);
|
||||
this._audio.muted = this.muted;
|
||||
this._audio.volume = 0.7;
|
||||
this._audio.play().catch(() => {});
|
||||
this._audio.muted = this.muted;
|
||||
this._audio.volume = 0.15;
|
||||
this._audio.play().catch(() => { });
|
||||
this._audio.onended = () => this.next();
|
||||
this._updateInfo();
|
||||
}
|
||||
|
|
@ -139,7 +139,7 @@ export class MusicPlayer {
|
|||
this._audio.onended = null;
|
||||
this._audio = null;
|
||||
}
|
||||
for (const o of this._objs) { try { o.destroy(); } catch (_) {} }
|
||||
for (const o of this._objs) { try { o.destroy(); } catch (_) { } }
|
||||
this._objs = [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,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';
|
||||
|
||||
// ── Shared portrait utility ───────────────────────────────────────────────────
|
||||
// Both factories add elements directly to the scene at world coordinates so
|
||||
|
|
@ -97,6 +98,11 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius,
|
|||
videoEl.play().catch(returnToIdle);
|
||||
videoEl.onended = returnToIdle;
|
||||
videoEl.onerror = returnToIdle;
|
||||
|
||||
const speechClips = opponent?.speech?.[emotion];
|
||||
if (speechClips?.length && Math.random() < 0.6) {
|
||||
enqueueSpeech(speechClips[Math.floor(Math.random() * speechClips.length)]);
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
|
|
@ -114,10 +120,16 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius,
|
|||
function destroy() {
|
||||
videoEl.pause();
|
||||
videoEl.src = '';
|
||||
resetSpeechQueue();
|
||||
}
|
||||
|
||||
scene.events.once('shutdown', destroy);
|
||||
|
||||
if (opponent?.speech?.intro?.length) {
|
||||
const clips = opponent.speech.intro;
|
||||
enqueueSpeech(clips[Math.floor(Math.random() * clips.length)]);
|
||||
}
|
||||
|
||||
return { playEmotion, hide, show, destroy };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
let _queue = [];
|
||||
let _playing = false;
|
||||
let _currentAudio = null;
|
||||
const MAX = 4;
|
||||
|
||||
export function enqueue(filename) {
|
||||
if (_queue.length + (_playing ? 1 : 0) >= MAX) return;
|
||||
_queue.push(filename);
|
||||
_playNext();
|
||||
}
|
||||
|
||||
function _playNext() {
|
||||
if (_playing || !_queue.length) return;
|
||||
_playing = true;
|
||||
_currentAudio = new Audio(`/assets/speech/${_queue.shift()}.mp3`);
|
||||
_currentAudio.volume = 0.8;
|
||||
const done = () => { _playing = false; _currentAudio = null; _playNext(); };
|
||||
_currentAudio.onended = done;
|
||||
_currentAudio.onerror = done;
|
||||
_currentAudio.play().catch(done);
|
||||
}
|
||||
|
||||
export function resetQueue() {
|
||||
if (_currentAudio) { _currentAudio.pause(); _currentAudio = null; }
|
||||
_queue = [];
|
||||
_playing = false;
|
||||
}
|
||||
Loading…
Reference in New Issue