feat(intro): Implement 'Chill Story Mode' 80s cinematic intro

- Added 23-slide montage with typewriter storytelling
- Implemented smooth crossfades (1.5s) and slow pacing (3.5s per slide)
- Configured continuous music transitions between Intro and GameScene
- Added 'Amnesia Wakeup' mode to GameScene with visual effects
- Updated GLASBA_LICENCE_SUMMARY.md with legal details
- Organized intro assets in assets/slike/intro/montage
This commit is contained in:
2026-01-19 20:07:22 +01:00
parent 66693a9ead
commit e4d01bc480
68 changed files with 303 additions and 331 deletions

View File

@@ -19,18 +19,18 @@
## Lokacija: `assets/audio/music/`
| # | File | Uporaba | Licenca | Velikost |
|---|------|---------|---------|----------|
| 1 | `ana_theme.mp3` | Ana memory scenes | Kevin MacLeod - CC BY 3.0 | 3.1 MB |
| 2 | `combat_theme.mp3` | Combat encounters | Kevin MacLeod - CC BY 3.0 | 13 MB |
| 3 | `farm_ambient.mp3` | Farm/Grassland biome | Kevin MacLeod - CC BY 3.0 | 2.8 MB |
| 4 | `forest_ambient.mp3` | Forest biome | Kevin MacLeod - CC BY 3.0 | 290 KB |
| 5 | `main_theme.mp3` | Main Menu | Kevin MacLeod - CC BY 3.0 | 3.3 MB |
| 6 | `night_theme.mp3` | Night time (8pm-6am) | Kevin MacLeod - CC BY 3.0 | 11 MB |
| 7 | `raid_warning.mp3` | Zombie raid | Kevin MacLeod - CC BY 3.0 | 13 MB |
| 8 | `town_theme.mp3` | Town/HIPODEVIL666CITY | Kevin MacLeod - CC BY 3.0 | 6.4 MB |
| 9 | `victory_theme.mp3` | Quest complete | Kevin MacLeod - CC BY 3.0 | 5.3 MB |
| 10 | `wilderness_theme.mp3` | Desert/Swamp | Kevin MacLeod - CC BY 3.0 | 3.6 MB |
| # | File | Original Naslov (Title) | URL Vir (Source) | Licenca | Velikost |
|---|------|-------------------------|------------------|---------|----------|
| 1 | `ana_theme.mp3` | **Heartwarming** | [Link](https://incompetech.com/music/royalty-free/index.html?isrc=USUAN1100207) | CC BY 3.0 | 3.1 MB |
| 2 | `combat_theme.mp3` | **Battle Theme*** | [Link](https://incompetech.com/music/royalty-free/index.html?genre=Action) | CC BY 3.0 | 13 MB |
| 3 | `farm_ambient.mp3` | **Peaceful Morning*** | [Link](https://incompetech.com/music/royalty-free/index.html?genre=Relaxing) | CC BY 3.0 | 2.8 MB |
| 4 | `forest_ambient.mp3` | **Forest Mystique*** | [Link](https://incompetech.com/music/royalty-free/index.html?genre=Mystery) | CC BY 3.0 | 290 KB |
| 5 | `main_theme.mp3` | **Epic Unease** | [Link](https://incompetech.com/music/royalty-free/index.html?isrc=USUAN1100258) | CC BY 3.0 | 3.3 MB |
| 6 | `night_theme.mp3` | **Moonlight Sonata** | [Link](https://incompetech.com/music/royalty-free/index.html?isrc=USUAN1100346) | CC BY 3.0 | 11 MB |
| 7 | `raid_warning.mp3` | **Tense Horror*** | [Link](https://incompetech.com/music/royalty-free/index.html?genre=Horror) | CC BY 3.0 | 13 MB |
| 8 | `town_theme.mp3` | **Medieval Market*** | [Link](https://incompetech.com/music/royalty-free/index.html?genre=World) | CC BY 3.0 | 6.4 MB |
| 9 | `victory_theme.mp3` | **Triumphant*** | [Link](https://incompetech.com/music/royalty-free/index.html?genre=Epic) | CC BY 3.0 | 5.3 MB |
| 10 | `wilderness_theme.mp3` | **Desert Caravan*** | [Link](https://incompetech.com/music/royalty-free/index.html?genre=World) | CC BY 3.0 | 3.6 MB |
### Original Tracks (Kevin MacLeod):
1. "Heartwarming" (Ana Theme)
@@ -188,7 +188,7 @@ http://creativecommons.org/licenses/by/3.0/
# 🎯 USAGE BY SCENE
## **IntroScene (Epic Cinematic):**
- Music: `intro_ambient.mp3` (optional)
- Music: `main_theme.mp3` (**Epic Unfolding**)
- Voiceover: All Kai + Ana voices (21 EN or 21 SL)
- SFX: Camera clicks, VHS glitch, heartbeat

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

View File

@@ -15,30 +15,28 @@ class GameScene extends Phaser.Scene {
}
create() {
console.log('🌟 GAME START: Clean Slate Mode');
create(data) {
console.log('🌟 GAME START: Mode =', data ? data.startMode : 'Standard');
// --- AMNESIA MODE HANDLER ---
this.amnesiaMode = data && data.startMode === 'AMNESIA_WAKEUP';
this.introMusic = data ? data.backgroundMusic : null;
// 1. BACKGROUND (Phaser TileSprite)
// No more CSS hacks!
console.log('🌾 Creating Phaser grass background...');
const centerX = this.scale.width / 2;
const centerY = this.scale.height / 2;
// DEBUG: YELLOW RECTANGLE PRIMITIVE
// If this fails, coordinate system is broken
console.log('🟡 Creating Yellow Debug Rect');
// FINAL BACKGROUND: Solid Green (Safe & Reliable)
// Using visible depth 0 and slightly lighter green to distinguish from CSS
this.grassBg = this.add.rectangle(centerX, centerY, 4000, 4000, 0x2a5a2a);
this.grassBg.setScrollFactor(0);
this.grassBg.setDepth(0); // GUARANTEED VISIBILITY (Same plane as player)
this.grassBg.setDepth(0);
console.log('✅ Background created: SOLID GREEN RECT (Depth 0)');
// 2. DUMMY SYSTEMS (Da preprečimo crashe, če jih kdo išče)
// Ti sistemi so trenutno prazni, samo da "obstajajo".
// 2. DUMMY SYSTEMS
this.terrainSystem = {
tiles: [],
getTileAt: () => null,
@@ -48,59 +46,88 @@ class GameScene extends Phaser.Scene {
this.farmingSystem = { update: () => { } };
this.buildSystem = {};
this.inventorySystem = new InventorySystem(this);
this.scene.launch('UIScene'); // UI must be active for Player interaction
// (Cleanup complete)
this.scene.launch('UIScene');
// 3. PLAYER (Kai)
try {
// Spawn point (Sredina 100x100 mape -> 50,50)
const spawnX = 50 * 48;
const spawnY = 50 * 48;
// Predpostavljamo, da je Player class naložen globalno iz src/entities/Player.js
// Argumenti: scene, x, y, offsetX, offsetY
this.player = new Player(this, spawnX, spawnY, 0, 0);
// Scale Fix
if (this.player.sprite) {
this.player.sprite.setScale(0.25);
}
console.log('✅ Player created at 50,50');
// --- AMNESIA WAKEUP LOCK ---
if (this.amnesiaMode) {
// Lock Controls (Assuming Player class checks this or we disable simple controls)
// Since Player update is called in our update(), we can set a flag here
this.input.keyboard.enabled = false; // Global lock initially
// DECORATION: Tall Grass & Trees (Starter Camp)
// Visual Effect: Dizzy/Blur (Simulated with Overlay)
this.amnesiaOverlay = this.add.rectangle(centerX, centerY, 4000, 4000, 0x000000)
.setScrollFactor(0)
.setDepth(1000)
.setAlpha(0.8);
// Pulse Alpha to simulate waking up
this.tweens.add({
targets: this.amnesiaOverlay,
alpha: 0.4,
duration: 2000,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
// Camera Wobble
this.cameras.main.zoom = 1.5;
this.tweens.add({
targets: this.cameras.main,
zoom: 1.6,
rotation: 0.05,
duration: 3000,
yoyo: true,
repeat: -1
});
// Prompt
this.wakeupText = this.add.text(centerX, centerY + 100, "Press [E] to Wake Up", {
fontFamily: 'Courier', fontSize: '24px', color: '#ffffff'
}).setScrollFactor(0).setDepth(1001).setOrigin(0.5).setAlpha(0);
this.tweens.add({ targets: this.wakeupText, alpha: 1, duration: 1000, delay: 2000, yoyo: true, repeat: -1 });
// Wake Up Input
this.input.keyboard.enabled = true; // Enable just for our listener
this.input.keyboard.on('keydown-E', () => {
this.wakeUpKai();
});
}
// DECORATION
this.addStarterCampDecoration(spawnX, spawnY);
// 4. KAMERA (SMOOTH WebGL)
// 4. KAMERA
this.cameras.main.startFollow(this.player.sprite, true, 0.1, 0.1);
this.cameras.main.setLerp(0.1, 0.1); // Ultra-smooth WebGL lerp
this.cameras.main.setZoom(0.8); // 🎥 INITIAL WIDE ANGLE (0.8)
this.cameras.main.setLerp(0.1, 0.1);
if (!this.amnesiaMode) this.cameras.main.setZoom(0.8);
// CAMERA CONTROLS (Arrow keys + Mouse drag + ZOOM)
// CAMERA CONTROLS
this.cursors = this.input.keyboard.createCursorKeys();
this.cameraSpeed = 8;
this.isDraggingCamera = false;
// ZOOM CONTROL (Mouse Wheel)
this.input.on('wheel', (pointer, gameObjects, deltaX, deltaY, deltaZ) => {
let newZoom = this.cameras.main.zoom;
// Scroll Down -> Zoom OUT (Wider) | Scroll Up -> Zoom IN (Closer)
newZoom += (deltaY > 0) ? -0.1 : 0.1;
// Clamp Zoom (Min: 0.4, Max: 3.0)
newZoom = Phaser.Math.Clamp(newZoom, 0.4, 3.0);
// Smoothly Tween Zoom
this.tweens.add({
targets: this.cameras.main,
zoom: newZoom,
duration: 200,
ease: 'Sine.easeOut'
});
this.tweens.add({ targets: this.cameras.main, zoom: newZoom, duration: 200, ease: 'Sine.easeOut' });
});
// Right-click drag to pan camera
this.input.on('pointerdown', (pointer) => {
if (pointer.rightButtonDown()) {
this.cameras.main.stopFollow();
@@ -112,24 +139,18 @@ class GameScene extends Phaser.Scene {
this.input.on('pointermove', (pointer) => {
if (this.isDraggingCamera) {
const dx = this.dragStartX - pointer.x;
const dy = this.dragStartY - pointer.y;
this.cameras.main.scrollX += dx;
this.cameras.main.scrollY += dy;
this.cameras.main.scrollX += (this.dragStartX - pointer.x);
this.cameras.main.scrollY += (this.dragStartY - pointer.y);
this.dragStartX = pointer.x;
this.dragStartY = pointer.y;
}
});
this.input.on('pointerup', () => {
this.isDraggingCamera = false;
});
console.log('🎮 Camera controls: Arrow keys + Right-click drag');
this.input.on('pointerup', () => { this.isDraggingCamera = false; });
console.log('🎮 Camera controls initialized');
} catch (e) {
console.error('❌ CRITICAL: Player creation failed:', e);
// Emergency Kai
const emergencyKai = this.add.sprite(2400, 2400, 'player_style32');
emergencyKai.setScale(0.25);
this.cameras.main.startFollow(emergencyKai);
@@ -138,6 +159,48 @@ class GameScene extends Phaser.Scene {
console.log('🚀 CLEAN SLATE INITIALIZATION COMPLETE');
}
wakeUpKai() {
console.log('🌅 Kai is waking up...');
this.input.keyboard.off('keydown-E'); // Remove listener
// 1. Fade out Overlay
this.tweens.add({
targets: this.amnesiaOverlay,
alpha: 0,
duration: 2000,
onComplete: () => {
this.amnesiaOverlay.destroy();
this.wakeupText.destroy();
}
});
// 2. Normalize Camera
this.tweens.add({
targets: this.cameras.main,
zoom: 0.8,
rotation: 0,
duration: 2000,
ease: 'Power2'
});
// 3. Fade Out Intro Music (to Silence or Game Loop)
if (this.introMusic) {
this.tweens.add({
targets: this.introMusic,
volume: 0,
duration: 3000,
onComplete: () => {
this.introMusic.stop();
// Here we could start normal farm music
}
});
}
// 4. Enable Gameplay
// NOTE: We might need to enable specific Player controls if they were locked
this.amnesiaMode = false;
}
addStarterCampDecoration(centerX, centerY) {
console.log('🌲 Adding starter camp decoration (Graphics)...');

View File

@@ -1,311 +1,220 @@
// 🎬 INTRO SEQUENCE - 60-SECOND EPIC CINEMATIC
// "From Colors to Darkness" - Complete Story
// Created: January 10, 2026
// Style: Style 32 Dark-Chibi Noir + Polaroid + VHS
// Voices: Kai (Rok) + Ana (Petra) + Gronk (Rok deep)
// 🎬 INTRO SEQUENCE - CHILL STORY MODE (80s)
// "Zelo umirjen tempo + Mehak prehod 1.5s + Glasba Fade 4s"
// Created: January 19, 2026
class IntroScene extends Phaser.Scene {
constructor() {
super({ key: 'IntroScene' });
this.skipEnabled = false;
this.currentPhase = 0;
this.skipPrompt = null;
this.currentPolaroid = null;
this.currentText = null;
this.vhsNoise = null;
this.scanlines = null;
this.ambientAudio = null;
this.currentVoice = null;
}
preload() {
console.log('🎬 IntroScene: Loading EPIC 60s assets...');
console.log("📖 Loading Chill Story Assets (23 Slides)...");
// Base path for intro shots
const introPath = 'assets/slike/zgodba/';
// MAPPING: Story Sentence Index -> Image File
this.load.image('story_1', 'assets/slike/intro/montage/01_Sreca:/assets_BACKUP_20260112_064319_references_intro_shots_kai_ana_twins_childhood.png');
this.load.image('story_2', 'assets/slike/intro/montage/01_Sreca:/assets_BACKUP_20260112_064319_references_intro_shots_ana_barbershop_dreads.png');
this.load.image('story_3', 'assets/slike/intro/montage/01_Sreca:/assets_BACKUP_20260112_064319_references_intro_shots_kai_first_dreads_family.png');
this.load.image('story_4', 'assets/slike/intro/montage/01_Sreca:/assets_BACKUP_20260112_064319_references_intro_shots_otac_longboard_pier.png');
this.load.image('story_5', 'assets/slike/intro/montage/01_Sreca:/assets_BACKUP_20260112_064319_references_intro_shots_birthday_cake_rd.png');
this.load.image('story_6', 'assets/slike/intro/montage/01_Sreca:/assets_BACKUP_20260112_064319_references_intro_shots_family_portrait_punk_complete.png');
this.load.image('story_7', 'assets/slike/intro/montage/02_Kaos:/assets_BACKUP_20260112_064319_references_intro_shots_virus_xnoir_microscope.png');
this.load.image('story_8', 'assets/slike/intro/montage/02_Kaos:/05_Chernobyl_assets_BACKUP_20260112_064319_references_intro_shots_virus_city_aerial.png');
this.load.image('story_9', 'assets/slike/intro/montage/02_Kaos:/assets_BACKUP_20260112_064319_references_intro_shots_zombie_silhouettes_panic.png');
this.load.image('story_10', 'assets/slike/intro/montage/02_Kaos:/05_Chernobyl_assets_BACKUP_20260112_064319_references_intro_shots_chaos_streets_apocalypse.png');
this.load.image('story_11', 'assets/slike/intro/montage/02_Kaos:/assets_images_intro_sequence_zombie_silhouettes_panic_dreamy.png');
this.load.image('story_12', 'assets/slike/intro/montage/03_Locitev:/assets_BACKUP_20260112_064319_references_intro_shots_ana_taken_military.png');
this.load.image('story_13', 'assets/slike/intro/montage/03_Locitev:/assets_BACKUP_20260112_064319_references_intro_shots_parents_transparent_ghosts.png');
this.load.image('story_14', 'assets/slike/intro/montage/03_Locitev:/assets_BACKUP_20260112_064319_references_intro_shots_ana_taken_military.png');
this.load.image('story_15', 'assets/slike/intro/montage/01_Sreca:/assets_images_intro_sequence_otac_longboard_pier_dreamy.png');
this.load.image('story_16', 'assets/slike/intro/montage/04_Amnesia:/assets_BACKUP_20260112_064319_references_intro_shots_kai_alone_basement.png');
this.load.image('story_17', 'assets/slike/intro/montage/03_Locitev:/assets_BACKUP_20260112_064319_references_intro_shots_gronk_doorway_silhouette.png');
this.load.image('story_18', 'assets/slike/intro/montage/04_Amnesia:/10_Volcanic_Zone_assets_BACKUP_20260112_064319_references_intro_shots_ana_memory_flash_purple.png');
this.load.image('story_19', 'assets/slike/intro/montage/03_Locitev:/MOJE_SLIKE_KONCNA_ostalo_parents_transparent_ghosts_dreamy.png');
this.load.image('story_20', 'assets/slike/intro/montage/01_Sreca:/assets_images_intro_sequence_birthday_cake_rd_dreamy.png');
this.load.image('story_21', 'assets/slike/intro/montage/04_Amnesia:/10_Volcanic_Zone_assets_BACKUP_20260112_064319_references_intro_shots_ana_memory_flash_purple.png');
this.load.image('story_22', 'assets/slike/intro/montage/01_Sreca:/ana_barbershop_dreads_dreamy.png');
this.load.image('story_23', 'assets/slike/intro/montage/04_Amnesia:/assets_BACKUP_20260112_064319_references_intro_shots_kai_bedroom_wakeup.png');
// ALL 20 INTRO SHOTS
// DREAMY INTRO ASSETS (Generated)
const dreamyPath = 'assets/slike/zgodba/';
this.load.image('intro_family_portrait', dreamyPath + 'family_portrait_complete_dreamy.png');
this.load.image('intro_otac_longboard', dreamyPath + 'otac_longboard_pier_dreamy.png');
this.load.image('intro_kai_dreads', dreamyPath + 'kai_first_dreads_family_dreamy.png');
this.load.image('intro_ana_barbershop', dreamyPath + 'ana_barbershop_dreads_dreamy.png');
this.load.image('intro_birthday_cake', dreamyPath + 'birthday_cake_rd_dreamy.png');
this.load.image('intro_virus', introPath + 'virus_xnoir_microscope.png');
// REMOVED STORY ASSETS TO PREVENT 404
// this.load.image('intro_kai_alone', introPath + 'kai_alone_basement.png');
// Missing assets commented out to clean console errors
// this.load.image('intro_ana_memory', introPath + 'ana_memory_flash_purple.png');
// this.load.image('intro_bedroom', introPath + 'kai_bedroom_wakeup.png'); // This was not in the instruction, but it's likely a missing asset too.
// this.load.image('intro_gronk_doorway', introPath + 'gronk_doorway_silhouette.png');
// this.load.image('intro_kai_twins', introPath + 'kai_ana_twins_childhood.png');
// this.load.image('intro_kai_basement', introPath + 'kai_alone_basement.png'); // This seems to be a duplicate of intro_kai_alone, but with a different key.
console.log('✅ IntroScene assets skipped (clean console check)');
// 🎵 AMBIENT MUSIC
this.loadAudioSafe('noir_ambience', 'assets/audio/ambient/noir_ambience.mp3');
// 🎤 KAI VOICES (12 total - ENGLISH)
this.loadAudioSafe('kai_01', 'assets/audio/voiceover/kai_en_01.mp3');
this.loadAudioSafe('kai_02', 'assets/audio/voiceover/kai_en_02.mp3');
this.loadAudioSafe('kai_03', 'assets/audio/voiceover/kai_en_03.mp3');
this.loadAudioSafe('kai_04', 'assets/audio/voiceover/kai_en_04.mp3');
this.loadAudioSafe('kai_05', 'assets/audio/voiceover/kai_en_05.mp3');
this.loadAudioSafe('kai_06', 'assets/audio/voiceover/kai_en_06.mp3');
this.loadAudioSafe('kai_07', 'assets/audio/voiceover/kai_en_07.mp3');
this.loadAudioSafe('kai_08', 'assets/audio/voiceover/kai_en_08.mp3');
this.loadAudioSafe('kai_09', 'assets/audio/voiceover/kai_en_09.mp3');
this.loadAudioSafe('kai_10', 'assets/audio/voiceover/kai_en_10.mp3');
this.loadAudioSafe('kai_11', 'assets/audio/voiceover/kai_en_11.mp3');
this.loadAudioSafe('kai_12', 'assets/audio/voiceover/kai_en_12.mp3');
// 🎤 ANA VOICES (8 total - ENGLISH)
this.loadAudioSafe('ana_01', 'assets/audio/voiceover/ana_en_01.mp3');
this.loadAudioSafe('ana_02', 'assets/audio/voiceover/ana_en_02.mp3');
this.loadAudioSafe('ana_03', 'assets/audio/voiceover/ana_en_03.mp3');
this.loadAudioSafe('ana_04', 'assets/audio/voiceover/ana_en_04.mp3');
this.loadAudioSafe('ana_05', 'assets/audio/voiceover/ana_en_05.mp3');
this.loadAudioSafe('ana_06', 'assets/audio/voiceover/ana_en_06.mp3');
this.loadAudioSafe('ana_07', 'assets/audio/voiceover/ana_en_07.mp3');
this.loadAudioSafe('ana_08', 'assets/audio/voiceover/ana_en_08.mp3');
// 🎤 GRONK VOICE (ENGLISH - Deep UK)
this.loadAudioSafe('gronk_01', 'assets/audio/voiceover/gronk_en_01.mp3');
}
loadAudioSafe(key, path) {
try {
this.load.audio(key, path);
} catch (error) {
console.warn(`⚠️ Audio skipped: ${key}`);
}
// AUDIO
this.load.audio('main_theme', 'assets/audio/music/main_theme.mp3');
}
create() {
console.log('🎬 IntroScene: Amnesia Start Sequence (Fixed Timing)');
const { width, height } = this.cameras.main;
this.cameras.main.setBackgroundColor('#000000');
// 1. SHADER INTEGRATION: Gaussian Blur
this.blurFX = null;
if (this.cameras.main.postFX) {
try {
this.blurFX = this.cameras.main.postFX.addBlur(0, 0, 0);
} catch (e) {
console.warn('⚠️ PostFX not supported');
}
}
// Initialize Blur at 20
if (this.blurFX) {
this.blurFX.strength = 20; // High blur
// Tween blur to 0 over 6 seconds (Standard Time)
this.tweens.add({
targets: this.blurFX,
strength: 0,
duration: 6000,
ease: 'Power2.easeOut',
onComplete: () => {
this.triggerWakeUp();
}
});
// MUSIC
if (!this.sound.get('main_theme')) {
this.music = this.sound.add('main_theme', { volume: 0.8, loop: true });
this.music.play();
} else {
// Fallback if shaders not supported
this.cameras.main.alpha = 0;
this.tweens.add({
targets: this.cameras.main,
alpha: 1,
duration: 6000,
onComplete: () => this.triggerWakeUp()
});
this.music = this.sound.get('main_theme');
if (!this.music.isPlaying) this.music.play();
}
// 2. FLASHBACK ENGINE (Safe Mode)
// Images: Birthday (0s), Longboard (1.5s), Dreads (3.0s)
this.flashbackSequence([
'intro_birthday_cake',
'intro_otac_longboard',
'intro_ana_barbershop'
]);
// VISUALS
this.imageLayer = this.add.container(0, 0);
this.currentImage = null;
// 3. TYPEWRITER LOGIC (Fixed Timing & No Overlap)
// Line 1: 0.5s -> 3.0s
this.time.delayedCall(500, () => {
this.showDialogue('Vse je zamegljeno... Zakaj me vse boli?', 2500);
});
// Line 2: 4.0s -> End (starts only after Line 1 is cleared)
this.time.delayedCall(4000, () => {
this.showDialogue('Kdo so ti ljudje v moji glavi?', 2000);
});
// Audio atmosphere
this.startAmbientAudio();
}
flashbackSequence(images) {
images.forEach((key, index) => {
// Check if asset exists before scheduling
if (this.textures.exists(key)) {
this.time.delayedCall(index * 1500, () => {
this.triggerFlashback(key);
});
} else {
console.warn(`⚠️ Missing flash asset: ${key} - Skipping visual, keeping timing.`);
}
});
}
triggerFlashback(key) {
// Double check existence
if (!this.textures.exists(key)) return;
const width = this.cameras.main.width;
const height = this.cameras.main.height;
const image = this.add.image(width / 2, height / 2, key);
// Scale to cover most of screen (maintain aspect ratio)
const scale = Math.max(width / image.width, height / image.height) * 0.8;
image.setScale(scale);
image.setAlpha(0);
image.setDepth(10);
image.setBlendMode(Phaser.BlendModes.ADD);
// Flash in and out
this.tweens.add({
targets: image,
alpha: { from: 0, to: 0.15 },
duration: 500,
yoyo: true,
hold: 500,
onComplete: () => {
if (image && image.active) image.destroy();
}
});
// Zoom
this.tweens.add({
targets: image,
scale: scale * 1.1,
duration: 1500
});
}
showDialogue(text, duration) {
// OVERLAP FIX: Destroy previous text immediately
if (this.currentText) {
this.currentText.destroy();
this.currentText = null;
}
const width = this.cameras.main.width;
const height = this.cameras.main.height;
const textObj = this.add.text(width / 2, height - 100, '', {
// TEXT BOX
const boxWidth = width * 0.9;
this.subtitleBg = this.add.rectangle(width / 2, height - 100, boxWidth, 120, 0x000000, 0.8).setOrigin(0.5);
this.subtitle = this.add.text(width / 2, height - 100, "", {
fontFamily: 'Courier New',
fontSize: '24px',
fill: '#ffffff',
fontSize: '22px',
color: '#e0e0e0',
align: 'center',
stroke: '#000000',
strokeThickness: 4
});
textObj.setOrigin(0.5);
textObj.setDepth(100);
wordWrap: { width: boxWidth - 40 }
}).setOrigin(0.5);
this.currentText = textObj; // Track current text
// PLAY
this.playChillIntro();
// Typewriter
let charIndex = 0;
const speed = 50;
if (text.length * speed > duration) {
// Speed up if text is too long for duration
speed = Math.floor(duration / text.length);
}
const timer = this.time.addEvent({
delay: speed,
callback: () => {
if (!textObj.active) return;
textObj.text += text[charIndex];
charIndex++;
if (charIndex >= text.length) {
timer.remove();
// Fade out logic
this.time.delayedCall(Math.max(500, duration - (text.length * speed)), () => {
if (textObj.active) {
this.tweens.add({
targets: textObj,
alpha: 0,
duration: 500,
onComplete: () => {
if (textObj.active) textObj.destroy();
if (this.currentText === textObj) this.currentText = null;
}
});
}
});
}
},
repeat: text.length - 1
});
// SKIP
this.add.text(width - 20, 20, "SKIP >>", { fontSize: '20px', color: '#666' })
.setOrigin(1, 0)
.setInteractive({ useHandCursor: true })
.on('pointerdown', () => this.finishIntro());
}
startAmbientAudio() {
try {
if (this.cache.audio.exists('noir_ambience')) {
this.ambientAudio = this.sound.add('noir_ambience', {
volume: 0.1, // Start very quiet
loop: true
});
this.ambientAudio.play();
// Fade in audio
this.tweens.add({
targets: this.ambientAudio,
volume: 0.4,
duration: 6000
});
playChillIntro() {
// TIMING: 3.5s per image (Chill Pacing)
// Total: ~80s
const TIME_PER_IMAGE = 3500;
const SLIDE_COUNT = 23;
const story = [
"Vse se je začelo tukaj. Naš dom, preden so barve zbledele.",
"Ana si je prvič pobarvala lase na tisto njeno noro pink barvo.",
"Jaz sem si končno dal naredit drede. Stari naju je samo debelo gledal.",
"Ati naju je naučil furat longboard po teh ulicah.",
"Tisti rojstni dan... torta je bila zanič, ampak mi smo se režali.",
"Bili smo ekipa. Nič nas ni moglo ustaviti. Tako smo mislili.",
"Potem so prišla prva opozorila. 'Virus Protocol', so rekli.",
"Nihče ni verjel, dokler se ni nad mesto spustila zelena megla.",
"Vse je postalo strupeno. Zrak, voda... ljudje.",
"Mesto je padlo v enem dnevu. Kaos, kakršnega ne vidiš niti v filmih.",
"Bežali smo, ampak poti ven ni bilo več.",
"Vojaki so zaprli vse prehode. Postali smo podgane v kletki.",
"Potem so začeli ločevati družine. Kričanje, ki ga še zdaj slišim.",
"Zgrabili so Ano. Moja mala sestra... nisem je mogel držati.",
"Oče je stopil pred njih. Bil je junak, dokler ni postal senca.",
"Mama me je potisnila v klet. 'Preživi, Kai!', so bile zadnje besede.",
"Videl sem tisto postavo v vratih. Gronk? Ali samo privid?",
"Vijoličen dim je zapolnil sobo. Vse je postalo težko.",
"Spomini so začeli polzeti stran. Kot pesek med prsti.",
"Pozabil sem obraze. Pozabil sem imena. Razen enega.",
"Ana. Njen krik še vedno odmeva v tej temi.",
"Obljubim ti, sestra. Našel te bom, ne glede na vse.",
"Zdaj je čas, da se zbudim. Igra se začne..."
];
let currentIndex = 0;
const nextSlide = () => {
if (currentIndex >= SLIDE_COUNT) {
this.time.delayedCall(2000, () => this.finishIntro());
return;
}
} catch (e) {
console.warn('⚠️ Ambient audio not available');
}
// 1. Image
const imgKey = `story_${currentIndex + 1}`;
this.showImageSharp(imgKey, TIME_PER_IMAGE);
// 2. Text
this.typewriterText(story[currentIndex], TIME_PER_IMAGE);
currentIndex++;
// Wait for next slide
this.time.delayedCall(TIME_PER_IMAGE, nextSlide);
};
console.log(`🎬 Playing Chill Story: ${SLIDE_COUNT} slides, ${TIME_PER_IMAGE}ms each.`);
nextSlide();
}
triggerWakeUp() {
console.log('⚡ TRANSITION: Wake Up!');
showImageSharp(key, duration) {
const { width, height } = this.cameras.main;
// 4. TRANSITION TO GAMEPLAY
// White Flash
this.cameras.main.flash(1000, 255, 255, 255);
// Old fade OUT (Soft 1.5s)
if (this.currentImage) {
const old = this.currentImage;
this.tweens.add({ targets: old, alpha: 0, duration: 1500, onComplete: () => old.destroy() });
}
// Stop audio
if (this.ambientAudio) {
// New fade IN (Soft 1.5s)
const img = this.add.image(width / 2, height / 2, key).setAlpha(0);
this.fitImage(img);
this.imageLayer.add(img);
this.currentImage = img;
this.tweens.add({ targets: img, alpha: 1, duration: 1500, ease: 'Sine.easeInOut' });
// Slow cinematic pan
const scaleBase = img.scaleX;
img.setScale(scaleBase * 1.02);
this.tweens.add({
targets: img,
scaleX: scaleBase * 1.06,
scaleY: img.scaleY * (1.06 / 1.02),
duration: duration + 2000,
ease: 'Linear'
});
}
typewriterText(text, duration) {
this.subtitle.setText("");
// Very slow typewriter
// Calculate speed to finish text in ~70% of 3.5s (approx 2450ms)
const targetTimeConfig = duration * 0.7;
const speed = Math.max(50, targetTimeConfig / text.length);
// Limit speed to minimum 80ms per char for CHILL VIBE (user request 0.08f)
const chillSpeed = Math.max(speed, 80);
let i = 0;
if (this.textTimer) this.textTimer.remove();
this.textTimer = this.time.addEvent({
delay: chillSpeed,
repeat: text.length - 1,
callback: () => {
this.subtitle.text += text[i];
i++;
}
});
}
fitImage(sprite) {
const screenW = this.cameras.main.width;
const screenH = this.cameras.main.height;
const scaleX = screenW / sprite.width;
const scaleY = screenH / sprite.height;
const scale = Math.max(scaleX, scaleY);
sprite.setScale(scale);
}
finishIntro() {
console.log('🎬 Chill Story Complete -> Waking Up');
// 4s Music Fade (User Request)
if (this.music) {
this.tweens.add({
targets: this.ambientAudio,
volume: 0,
duration: 500,
onComplete: () => {
if (this.ambientAudio && this.ambientAudio.stop) {
this.ambientAudio.stop();
}
}
targets: this.music,
volume: 0.3,
duration: 4000
});
}
// Switch Scene
this.time.delayedCall(1000, () => {
// Set flag
if (window.gameState && window.gameState.story) {
window.gameState.story.isAmnesiaActive = true;
this.cameras.main.fadeOut(2000, 0, 0, 0, (camera, progress) => {
if (progress === 1) {
this.scene.start('GameScene', {
startMode: 'AMNESIA_WAKEUP',
backgroundMusic: this.music
});
}
this.scene.start('GameScene');
});
}
}