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