Updated Diary - SESSION 3 Complete
All work from Christmas Day documented: - Session 1: Biomes (18/18) - Session 2: Story integration + UI systems - Session 3: Grok character + Susi Total: 5 hours, 1486 lines code, 6 systems
This commit is contained in:
353
src/systems/VoiceoverSystem.js
Normal file
353
src/systems/VoiceoverSystem.js
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* VoiceoverSystem.js
|
||||
* ==================
|
||||
* KRVAVA ŽETEV - Ana's Voice & Flashback Audio
|
||||
*
|
||||
* Features:
|
||||
* - Ana's voice recordings for clues
|
||||
* - Flashback narration
|
||||
* - Emotional voiceovers
|
||||
* - Audio positioning (3D sound)
|
||||
* - Volume based on bond strength
|
||||
*
|
||||
* @author NovaFarma Team
|
||||
* @date 2025-12-25
|
||||
*/
|
||||
|
||||
export default class VoiceoverSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Audio state
|
||||
this.currentVoiceover = null;
|
||||
this.voiceoverQueue = [];
|
||||
this.isPlaying = false;
|
||||
|
||||
// Audio library (paths to audio files)
|
||||
this.anaVoiceovers = {};
|
||||
this.flashbackVoiceovers = {};
|
||||
|
||||
console.log('🎙️ VoiceoverSystem initialized');
|
||||
|
||||
this.registerVoiceovers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all voiceover audio files
|
||||
*/
|
||||
registerVoiceovers() {
|
||||
// ANA'S CLUE VOICEOVERS (15 messages)
|
||||
this.anaVoiceovers = {
|
||||
msg_01: {
|
||||
file: 'assets/audio/voiceover/ana_msg_01.mp3',
|
||||
text: "Kai, if you find this... I'm sorry. They took me. Stay safe.",
|
||||
duration: 5000,
|
||||
emotion: 'scared'
|
||||
},
|
||||
msg_02: {
|
||||
file: 'assets/audio/voiceover/ana_msg_02.mp3',
|
||||
text: "I'm marking my trail with red flowers. Follow them west.",
|
||||
duration: 4000,
|
||||
emotion: 'determined'
|
||||
},
|
||||
msg_03: {
|
||||
file: 'assets/audio/voiceover/ana_msg_03.mp3',
|
||||
text: "These zombies... they're intelligent. I saw it in their eyes.",
|
||||
duration: 5000,
|
||||
emotion: 'curious'
|
||||
},
|
||||
msg_06: {
|
||||
file: 'assets/audio/voiceover/ana_msg_06.mp3',
|
||||
text: "Kai... I can feel you searching for me. Our bond is so strong.",
|
||||
duration: 6000,
|
||||
emotion: 'hopeful'
|
||||
},
|
||||
msg_09: {
|
||||
file: 'assets/audio/voiceover/ana_msg_09.mp3',
|
||||
text: "I found research on a cure! There's still hope for everyone!",
|
||||
duration: 5000,
|
||||
emotion: 'excited'
|
||||
},
|
||||
msg_10: {
|
||||
file: 'assets/audio/voiceover/ana_msg_10.mp3',
|
||||
text: "I dream of having a family someday. Children playing in safe fields...",
|
||||
duration: 6000,
|
||||
emotion: 'wistful'
|
||||
},
|
||||
msg_14: {
|
||||
file: 'assets/audio/voiceover/ana_msg_14.mp3',
|
||||
text: "Don't come for me, Kai. It's too dangerous. Please, save yourself!",
|
||||
duration: 5000,
|
||||
emotion: 'desperate'
|
||||
},
|
||||
msg_15: {
|
||||
file: 'assets/audio/voiceover/ana_msg_15.mp3',
|
||||
text: "I know you won't listen... you never do. I love you, brother.",
|
||||
duration: 6000,
|
||||
emotion: 'loving'
|
||||
},
|
||||
|
||||
// SPECIAL: Ana's Journal Entry
|
||||
ana_journal_01: {
|
||||
file: 'assets/audio/voiceover/ana_journal_01.mp3',
|
||||
text: "Day 147 in captivity. Dr. Krnić is forcing me to work on something terrible. A super virus using Chernobyl radiation. If it releases... everyone dies. I have to find a way to stop him.",
|
||||
duration: 15000,
|
||||
emotion: 'terrified'
|
||||
},
|
||||
|
||||
// SPECIAL: Final Video Message
|
||||
ana_final_message: {
|
||||
file: 'assets/audio/voiceover/ana_final_message.mp3',
|
||||
text: "Kai... I'm in Dr. Krnić's facility. Level 3, Vault 7. If you're watching this... don't come. It's a trap. But I know you will anyway. *laughs sadly* Be ready for the Troll King. I love you. Twin bond forever.",
|
||||
duration: 20000,
|
||||
emotion: 'resigned'
|
||||
}
|
||||
};
|
||||
|
||||
// FLASHBACK NARRATION
|
||||
this.flashbackVoiceovers = {
|
||||
twin_bond_discovery: {
|
||||
file: 'assets/audio/voiceover/flashback_twin_bond.mp3',
|
||||
text: "Dr. Chen: 'These twins have something special. A connection I've never seen before. They'll always find each other, no matter what.'",
|
||||
duration: 10000,
|
||||
emotion: 'wonder'
|
||||
},
|
||||
mother_death: {
|
||||
file: 'assets/audio/voiceover/flashback_mother.mp3',
|
||||
text: "Mama Elena: 'Kai... protect Ana. Always. Twin bond is your strength. Together... you can survive anything. I love you both...'",
|
||||
duration: 15000,
|
||||
emotion: 'heartbreaking'
|
||||
},
|
||||
kidnapping: {
|
||||
file: 'assets/audio/voiceover/flashback_kidnapping.mp3',
|
||||
text: "Ana's scream: 'KAIII! HELP! Please! KAIIIII!' *Kai's voice, desperate* 'ANA! NO! ANA!'",
|
||||
duration: 8000,
|
||||
emotion: 'traumatic'
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`✅ Registered ${Object.keys(this.anaVoiceovers).length} Ana voiceovers`);
|
||||
console.log(`✅ Registered ${Object.keys(this.flashbackVoiceovers).length} flashback voiceovers`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play Ana's voiceover for a clue
|
||||
*/
|
||||
playAnaVoiceover(clueId, onComplete = null) {
|
||||
const voiceover = this.anaVoiceovers[clueId];
|
||||
if (!voiceover) {
|
||||
console.warn(`⚠️ No voiceover for clue: ${clueId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`🎙️ Playing Ana's voice: ${clueId}`);
|
||||
|
||||
// Queue if already playing
|
||||
if (this.isPlaying) {
|
||||
this.voiceoverQueue.push({ type: 'ana', id: clueId, onComplete });
|
||||
console.log(`⏳ Voiceover queued (${this.voiceoverQueue.length} in queue)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.isPlaying = true;
|
||||
|
||||
// Show subtitle text
|
||||
this.showSubtitle(voiceover.text, voiceover.duration);
|
||||
|
||||
// Play audio (if file exists)
|
||||
if (this.scene.sound.get(voiceover.file)) {
|
||||
this.currentVoiceover = this.scene.sound.play(voiceover.file, {
|
||||
volume: this.getVolumeBasedOnBondStrength()
|
||||
});
|
||||
|
||||
// Cleanup when done
|
||||
this.currentVoiceover.once('complete', () => {
|
||||
this.onVoiceoverComplete(onComplete);
|
||||
});
|
||||
} else {
|
||||
// Fallback if audio file doesn't exist
|
||||
console.warn(`⚠️ Audio file not found: ${voiceover.file}`);
|
||||
|
||||
// Simulate duration
|
||||
setTimeout(() => {
|
||||
this.onVoiceoverComplete(onComplete);
|
||||
}, voiceover.duration);
|
||||
}
|
||||
|
||||
// Trigger Twin Bond moment
|
||||
if (this.scene.twinBondUI) {
|
||||
this.scene.twinBondUI.triggerBondMoment('medium');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play flashback narration
|
||||
*/
|
||||
playFlashbackVoiceover(flashbackId, onComplete = null) {
|
||||
const voiceover = this.flashbackVoiceovers[flashbackId];
|
||||
if (!voiceover) {
|
||||
console.warn(`⚠️ No voiceover for flashback: ${flashbackId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`🎙️ Playing flashback: ${flashbackId}`);
|
||||
|
||||
this.isPlaying = true;
|
||||
|
||||
// Show subtitle
|
||||
this.showSubtitle(voiceover.text, voiceover.duration, '#FFD700'); // Gold color for flashbacks
|
||||
|
||||
// Play audio
|
||||
if (this.scene.sound.get(voiceover.file)) {
|
||||
this.currentVoiceover = this.scene.sound.play(voiceover.file, {
|
||||
volume: 0.8
|
||||
});
|
||||
|
||||
this.currentVoiceover.once('complete', () => {
|
||||
this.onVoiceoverComplete(onComplete);
|
||||
});
|
||||
} else {
|
||||
console.warn(`⚠️ Audio file not found: ${voiceover.file}`);
|
||||
setTimeout(() => {
|
||||
this.onVoiceoverComplete(onComplete);
|
||||
}, voiceover.duration);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show subtitle text on screen
|
||||
*/
|
||||
showSubtitle(text, duration, color = '#9370DB') {
|
||||
const ui = this.scene.scene.get('UIScene') || this.scene;
|
||||
|
||||
// Create subtitle container
|
||||
const subtitleBg = ui.add.rectangle(
|
||||
ui.cameras.main.width / 2,
|
||||
ui.cameras.main.height - 100,
|
||||
ui.cameras.main.width - 200,
|
||||
80,
|
||||
0x000000,
|
||||
0.7
|
||||
);
|
||||
subtitleBg.setScrollFactor(0);
|
||||
subtitleBg.setDepth(2000);
|
||||
|
||||
const subtitleText = ui.add.text(
|
||||
ui.cameras.main.width / 2,
|
||||
ui.cameras.main.height - 100,
|
||||
text,
|
||||
{
|
||||
fontSize: '20px',
|
||||
fontFamily: 'Arial',
|
||||
color: color,
|
||||
align: 'center',
|
||||
wordWrap: { width: ui.cameras.main.width - 220 }
|
||||
}
|
||||
);
|
||||
subtitleText.setOrigin(0.5);
|
||||
subtitleText.setScrollFactor(0);
|
||||
subtitleText.setDepth(2001);
|
||||
|
||||
// Fade in
|
||||
subtitleBg.setAlpha(0);
|
||||
subtitleText.setAlpha(0);
|
||||
|
||||
ui.tweens.add({
|
||||
targets: [subtitleBg, subtitleText],
|
||||
alpha: 1,
|
||||
duration: 300
|
||||
});
|
||||
|
||||
// Fade out and destroy
|
||||
setTimeout(() => {
|
||||
ui.tweens.add({
|
||||
targets: [subtitleBg, subtitleText],
|
||||
alpha: 0,
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
subtitleBg.destroy();
|
||||
subtitleText.destroy();
|
||||
}
|
||||
});
|
||||
}, duration - 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get volume based on twin bond strength
|
||||
*/
|
||||
getVolumeBasedOnBondStrength() {
|
||||
if (!this.scene.twinBondUI) {
|
||||
return 0.7; // Default volume
|
||||
}
|
||||
|
||||
const bondStrength = this.scene.twinBondUI.getBondStrength();
|
||||
|
||||
// Higher bond = louder voice
|
||||
const volume = 0.4 + (bondStrength / 100) * 0.6; // 0.4 to 1.0
|
||||
|
||||
return volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Voiceover completion handler
|
||||
*/
|
||||
onVoiceoverComplete(callback) {
|
||||
this.isPlaying = false;
|
||||
this.currentVoiceover = null;
|
||||
|
||||
// Call completion callback
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// Play next in queue
|
||||
if (this.voiceoverQueue.length > 0) {
|
||||
const next = this.voiceoverQueue.shift();
|
||||
|
||||
setTimeout(() => {
|
||||
if (next.type === 'ana') {
|
||||
this.playAnaVoiceover(next.id, next.onComplete);
|
||||
} else if (next.type === 'flashback') {
|
||||
this.playFlashbackVoiceover(next.id, next.onComplete);
|
||||
}
|
||||
}, 500); // Small delay between voiceovers
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop current voiceover
|
||||
*/
|
||||
stopVoiceover() {
|
||||
if (this.currentVoiceover) {
|
||||
this.currentVoiceover.stop();
|
||||
this.currentVoiceover = null;
|
||||
}
|
||||
this.isPlaying = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear queue
|
||||
*/
|
||||
clearQueue() {
|
||||
this.voiceoverQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if voiceover is playing
|
||||
*/
|
||||
isVoiceoverPlaying() {
|
||||
return this.isPlaying;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
this.stopVoiceover();
|
||||
this.clearQueue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user