354 lines
11 KiB
JavaScript
354 lines
11 KiB
JavaScript
/**
|
|
* 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
|
|
*/
|
|
|
|
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();
|
|
}
|
|
}
|