393 lines
12 KiB
JavaScript
393 lines
12 KiB
JavaScript
/**
|
|
* PrologueScene.js
|
|
* ================
|
|
* KRVAVA ŽETEV - Prologue Cutscene
|
|
*
|
|
* Story:
|
|
* Player (Kai) and twin sister (Ana) were scientists studying zombie virus
|
|
* During attack, both got infected with "Alfa" strain (hybrid virus)
|
|
* Ana was kidnapped by mysterious forces
|
|
* Kai wakes up alone, searching for his sister
|
|
*
|
|
* Features:
|
|
* - Cinematic dialogue system
|
|
* - Character portraits
|
|
* - Background transitions
|
|
* - Skip function (ESC)
|
|
* - Auto-advance option
|
|
*
|
|
* @author NovaFarma Team
|
|
* @date 2025-12-23
|
|
*/
|
|
|
|
class PrologueScene extends Phaser.Scene {
|
|
constructor() {
|
|
super({ key: 'PrologueScene' });
|
|
this.currentDialogueIndex = 0;
|
|
this.dialogueData = [];
|
|
this.canAdvance = true;
|
|
this.autoAdvance = false;
|
|
this.autoAdvanceDelay = 3000; // 3 seconds
|
|
}
|
|
|
|
preload() {
|
|
this.load.json('prologue_data', 'assets/dialogue/prologue.json');
|
|
|
|
// Dynamically load audio based on JSON content
|
|
this.load.on('filecomplete-json-prologue_data', (key, type, data) => {
|
|
if (Array.isArray(data)) {
|
|
data.forEach(line => {
|
|
if (line.id) {
|
|
// Assuming .wav format as generated by our script
|
|
this.load.audio(line.id, `assets/audio/voiceover/prologue/${line.id}.wav`);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
create() {
|
|
const width = this.cameras.main.width;
|
|
const height = this.cameras.main.height;
|
|
|
|
console.log('🎬 Starting Prologue...');
|
|
|
|
// Track current audio to stop it when advancing
|
|
this.currentVoice = null;
|
|
|
|
// Black background
|
|
this.add.rectangle(0, 0, width, height, 0x000000).setOrigin(0);
|
|
|
|
// Initialize dialogue data from JSON
|
|
this.dialogueData = this.cache.json.get('prologue_data');
|
|
|
|
if (!this.dialogueData) {
|
|
console.error('❌ Failed to load prologue dialogue data!');
|
|
this.dialogueData = [];
|
|
}
|
|
|
|
// Create UI elements
|
|
this.createDialogueUI(width, height);
|
|
|
|
// Skip instructions
|
|
const skipText = this.add.text(width - 20, 20, 'Press ESC to skip', {
|
|
fontSize: '16px',
|
|
fontFamily: 'Georgia, serif',
|
|
color: '#888888'
|
|
});
|
|
skipText.setOrigin(1, 0);
|
|
|
|
// Auto-advance toggle
|
|
const autoText = this.add.text(width - 20, 50, 'Press SPACE to toggle auto-advance', {
|
|
fontSize: '14px',
|
|
fontFamily: 'Georgia, serif',
|
|
color: '#666666'
|
|
});
|
|
autoText.setOrigin(1, 0);
|
|
|
|
// Input handlers
|
|
this.input.keyboard.on('keydown-ESC', () => {
|
|
this.skipPrologue();
|
|
});
|
|
|
|
this.input.keyboard.on('keydown-SPACE', () => {
|
|
this.autoAdvance = !this.autoAdvance;
|
|
autoText.setColor(this.autoAdvance ? '#00FF00' : '#666666');
|
|
});
|
|
|
|
this.input.keyboard.on('keydown-ENTER', () => {
|
|
this.advanceDialogue();
|
|
});
|
|
|
|
this.input.on('pointerdown', () => {
|
|
this.advanceDialogue();
|
|
});
|
|
|
|
// Start first dialogue
|
|
this.showDialogue(0);
|
|
}
|
|
|
|
createDialogueUI(width, height) {
|
|
const dialogueBoxHeight = 180;
|
|
const dialogueY = height - dialogueBoxHeight;
|
|
|
|
// Dialogue box background
|
|
this.dialogueBg = this.add.rectangle(
|
|
width / 2,
|
|
dialogueY + dialogueBoxHeight / 2,
|
|
width - 40,
|
|
dialogueBoxHeight - 20,
|
|
0x2d1b00,
|
|
0.95
|
|
);
|
|
this.dialogueBg.setStrokeStyle(3, 0xd4a574);
|
|
|
|
// Speaker name
|
|
this.speakerText = this.add.text(50, dialogueY + 20, '', {
|
|
fontSize: '22px',
|
|
fontFamily: 'Georgia, serif',
|
|
color: '#FFD700',
|
|
fontStyle: 'bold',
|
|
stroke: '#000000',
|
|
strokeThickness: 3
|
|
});
|
|
|
|
// Dialogue text
|
|
this.dialogueText = this.add.text(50, dialogueY + 55, '', {
|
|
fontSize: '18px',
|
|
fontFamily: 'Georgia, serif',
|
|
color: '#f4e4c1',
|
|
wordWrap: { width: width - 120 },
|
|
lineSpacing: 8
|
|
});
|
|
|
|
// Continue indicator
|
|
this.continueIndicator = this.add.text(width - 60, height - 40, '▼', {
|
|
fontSize: '24px',
|
|
color: '#FFD700'
|
|
});
|
|
this.continueIndicator.setOrigin(0.5);
|
|
|
|
// Pulse animation
|
|
this.tweens.add({
|
|
targets: this.continueIndicator,
|
|
alpha: 0.3,
|
|
duration: 800,
|
|
yoyo: true,
|
|
repeat: -1
|
|
});
|
|
|
|
// Portrait
|
|
this.portraitBg = this.add.rectangle(width - 150, dialogueY + 90, 120, 120, 0x4a3520, 0.9);
|
|
this.portraitBg.setStrokeStyle(2, 0xd4a574);
|
|
|
|
this.portraitText = this.add.text(width - 150, dialogueY + 90, '', {
|
|
fontSize: '60px'
|
|
});
|
|
this.portraitText.setOrigin(0.5);
|
|
|
|
// Background sprite (will be created per dialogue)
|
|
this.backgroundSprite = null;
|
|
}
|
|
|
|
showDialogue(index) {
|
|
if (index >= this.dialogueData.length) {
|
|
this.completePrologue();
|
|
return;
|
|
}
|
|
|
|
const dialogue = this.dialogueData[index];
|
|
this.currentDialogueIndex = index;
|
|
|
|
// Stop previous audio if playing
|
|
if (this.currentVoice) {
|
|
this.currentVoice.stop();
|
|
this.currentVoice = null;
|
|
}
|
|
|
|
// Play new audio
|
|
if (dialogue.id) {
|
|
try {
|
|
// Check if audio exists in cache (it might not if load failed/missing)
|
|
if (this.cache.audio.exists(dialogue.id)) {
|
|
this.currentVoice = this.sound.add(dialogue.id);
|
|
this.currentVoice.play({ volume: 1.0 });
|
|
} else {
|
|
console.warn(`🔊 Audio missing for ${dialogue.id}`);
|
|
}
|
|
} catch (err) {
|
|
console.error('Audio play error:', err);
|
|
}
|
|
}
|
|
|
|
// Update background
|
|
this.updateBackground(dialogue.background, dialogue.bgColor);
|
|
|
|
// Apply effects
|
|
if (dialogue.shake) {
|
|
this.cameras.main.shake(500, 0.01);
|
|
}
|
|
|
|
if (dialogue.flash) {
|
|
this.cameras.main.flash(1000, 255, 255, 255);
|
|
}
|
|
|
|
// Update speaker
|
|
this.speakerText.setText(dialogue.speaker);
|
|
|
|
// Typewriter effect for text
|
|
this.typewriterEffect(dialogue.text);
|
|
|
|
// Update portrait
|
|
this.updatePortrait(dialogue.portrait);
|
|
|
|
// Auto-advance if enabled
|
|
if (this.autoAdvance && index < this.dialogueData.length - 1) {
|
|
// Wait for audio to finish OR standard delay?
|
|
// Ideally wait for audio duration, but fallback to delay
|
|
let delay = this.autoAdvanceDelay;
|
|
if (this.currentVoice && this.currentVoice.duration) {
|
|
// Add a small buffer after speech ends
|
|
delay = (this.currentVoice.duration * 1000) + 1000;
|
|
}
|
|
|
|
this.time.delayedCall(delay, () => {
|
|
// Check if user hasn't already advanced manually
|
|
if (this.currentDialogueIndex === index) {
|
|
this.advanceDialogue();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
typewriterEffect(text) {
|
|
let displayText = '';
|
|
console.log('Typewriter started for:', text); // Debug
|
|
let charIndex = 0;
|
|
|
|
this.dialogueText.setText('');
|
|
|
|
// Safety check for empty text
|
|
if (!text) {
|
|
this.canAdvance = true;
|
|
return;
|
|
}
|
|
|
|
const timer = this.time.addEvent({
|
|
delay: 30, //CharactersperSeconds
|
|
callback: () => {
|
|
if (charIndex < text.length) {
|
|
displayText += text[charIndex];
|
|
this.dialogueText.setText(displayText);
|
|
charIndex++;
|
|
} else {
|
|
timer.remove();
|
|
this.canAdvance = true;
|
|
}
|
|
},
|
|
loop: true
|
|
});
|
|
|
|
this.canAdvance = false;
|
|
}
|
|
|
|
updateBackground(bgKey, bgColor) {
|
|
// Simple colored background for now
|
|
// TODO: Replace with actual background images
|
|
const width = this.cameras.main.width;
|
|
const height = this.cameras.main.height;
|
|
|
|
if (this.backgroundSprite) {
|
|
this.backgroundSprite.destroy();
|
|
}
|
|
|
|
this.backgroundSprite = this.add.rectangle(0, 0, width, height, bgColor);
|
|
this.backgroundSprite.setOrigin(0);
|
|
this.backgroundSprite.setDepth(-1);
|
|
|
|
// Add atmosphere text
|
|
let atmosphereText = '';
|
|
switch (bgKey) {
|
|
case 'lab':
|
|
atmosphereText = '🔬 Nova Lab - Research Wing';
|
|
break;
|
|
case 'lab_alarm':
|
|
atmosphereText = '⚠️ BREACH ALARM ⚠️';
|
|
break;
|
|
case 'lab_chaos':
|
|
atmosphereText = '💥 CHAOS';
|
|
break;
|
|
case 'ruins':
|
|
atmosphereText = '🏚️ Laboratory Ruins';
|
|
break;
|
|
case 'zombies':
|
|
atmosphereText = '🧟 First Encounter';
|
|
break;
|
|
case 'farm':
|
|
atmosphereText = '🌾 Abandoned Farm - New Beginning';
|
|
break;
|
|
}
|
|
|
|
if (atmosphereText) {
|
|
const atmoText = this.add.text(width / 2, 40, atmosphereText, {
|
|
fontSize: '20px',
|
|
fontFamily: 'Georgia, serif',
|
|
color: '#888888',
|
|
fontStyle: 'italic'
|
|
});
|
|
atmoText.setOrigin(0.5);
|
|
atmoText.setAlpha(0.6);
|
|
atmoText.setDepth(10);
|
|
}
|
|
}
|
|
|
|
updatePortrait(portraitKey) {
|
|
if (!portraitKey) {
|
|
this.portraitBg.setVisible(false);
|
|
this.portraitText.setVisible(false);
|
|
return;
|
|
}
|
|
|
|
this.portraitBg.setVisible(true);
|
|
this.portraitText.setVisible(true);
|
|
|
|
// Simple emoji portraits for now
|
|
// TODO: Replace with actual character art
|
|
const portraits = {
|
|
'kai_neutral': '👨',
|
|
'kai_worried': '😟',
|
|
'kai_shocked': '😱',
|
|
'kai_pain': '😫',
|
|
'kai_confused': '😕',
|
|
'kai_determined': '😠',
|
|
'kai_anger': '😡',
|
|
'kai_realization': '🤔',
|
|
'ana_excited': '👩🔬',
|
|
'ana_serious': '😐',
|
|
'ana_determined': '💪',
|
|
'ana_pain': '😣'
|
|
};
|
|
|
|
this.portraitText.setText(portraits[portraitKey] || '❓');
|
|
}
|
|
|
|
advanceDialogue() {
|
|
if (!this.canAdvance) {
|
|
// Skip typewriter effect
|
|
const dialogue = this.dialogueData[this.currentDialogueIndex];
|
|
this.dialogueText.setText(dialogue.text);
|
|
this.canAdvance = true;
|
|
return;
|
|
}
|
|
|
|
// Stop current audio before advancing
|
|
if (this.currentVoice) {
|
|
this.currentVoice.stop();
|
|
}
|
|
|
|
this.showDialogue(this.currentDialogueIndex + 1);
|
|
}
|
|
|
|
skipPrologue() {
|
|
console.log('⏭️ Skipping prologue...');
|
|
if (this.currentVoice) {
|
|
this.currentVoice.stop();
|
|
}
|
|
this.scene.start('GameScene');
|
|
}
|
|
|
|
completePrologue() {
|
|
console.log('✅ Prologue complete!');
|
|
if (this.currentVoice) {
|
|
this.currentVoice.stop();
|
|
}
|
|
|
|
// Fade out
|
|
this.cameras.main.fadeOut(2000, 0, 0, 0);
|
|
|
|
this.time.delayedCall(2000, () => {
|
|
this.scene.start('GameScene');
|
|
});
|
|
}
|
|
}
|