Files
novafarma/src/scenes/PrologueScene.js

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');
});
}
}