/** * RenewalScene — Great Renewal (Великое Обновление) * * GDD spec: * After every 7th run, the world experiences a Great Renewal: * - World generation fundamentally changes * - New lore layer unlocks * - Mycelium "matures" — opens new capabilities * - Previous 7 runs leave traces in the next cycle * * Visual: A special transition between great cycles with * pulsing Mycelium, cycle counter, and narrative theme reveal. */ import Phaser from 'phaser'; import type { MetaState, CycleTheme } from '../run/types'; import { CYCLE_THEME_NAMES_RU } from '../run/types'; import { GAME_WIDTH, GAME_HEIGHT } from '../config'; import narrativeData from '../data/cycle-narrative.json'; interface ThemeNarrative { nameRu: string; cradleQuoteRu: string; loreFrag: { textRu: string }[]; } export class RenewalScene extends Phaser.Scene { private meta!: MetaState; private previousCycle!: number; private newCycle!: number; private newTheme!: CycleTheme; private elapsed = 0; private readonly duration = 15000; // 15 seconds private graphics!: Phaser.GameObjects.Graphics; private particles: { x: number; y: number; vx: number; vy: number; r: number; alpha: number; color: number }[] = []; constructor() { super({ key: 'RenewalScene' }); } init(data: { meta: MetaState; previousCycle: number }): void { this.meta = data.meta; this.previousCycle = data.previousCycle; this.newCycle = this.meta.greatCycle.cycleNumber; this.newTheme = this.meta.greatCycle.theme; this.elapsed = 0; this.particles = []; } create(): void { this.cameras.main.setBackgroundColor('#010204'); this.graphics = this.add.graphics(); const cx = GAME_WIDTH / 2; const cy = GAME_HEIGHT / 2; // Create Mycelium pulse particles (more than fractal — representing the network) for (let i = 0; i < 150; i++) { const angle = Math.random() * Math.PI * 2; const dist = Math.random() * 350; this.particles.push({ x: cx + Math.cos(angle) * dist, y: cy + Math.sin(angle) * dist, vx: (Math.random() - 0.5) * 30, vy: (Math.random() - 0.5) * 30, r: 1 + Math.random() * 3, alpha: Math.random() * 0.4, color: Phaser.Display.Color.HSLToColor( 0.35 + Math.random() * 0.15, // green-teal hues 0.5 + Math.random() * 0.3, 0.2 + Math.random() * 0.3, ).color, }); } // ─── Staged Text Reveals ───────────────────────────────── // 1. "Великое Обновление" title (fades in at 0s) const renewalMsg = this.pickRenewalMessage(); const titleText = this.add.text(cx, cy - 100, 'ВЕЛИКОЕ ОБНОВЛЕНИЕ', { fontSize: '30px', color: '#00ff88', fontFamily: 'monospace', fontStyle: 'bold', }).setOrigin(0.5).setAlpha(0).setDepth(10); this.tweens.add({ targets: titleText, alpha: 1, duration: 2000, }); // 2. Renewal flavor message (fades in at 2s) const flavorText = this.add.text(cx, cy - 55, renewalMsg, { fontSize: '13px', color: '#448866', fontFamily: 'monospace', }).setOrigin(0.5).setAlpha(0).setDepth(10); this.tweens.add({ targets: flavorText, alpha: 0.8, duration: 1500, delay: 2000, }); // 3. Cycle counter "Цикл 1 → Цикл 2" (fades in at 4s) const cycleText = this.add.text(cx, cy, `Цикл ${this.previousCycle} → Цикл ${this.newCycle}`, { fontSize: '22px', color: '#ffffff', fontFamily: 'monospace', fontStyle: 'bold', }).setOrigin(0.5).setAlpha(0).setDepth(10); this.tweens.add({ targets: cycleText, alpha: 1, duration: 1500, delay: 4000, }); // 4. New theme name (fades in at 6s) const themeName = CYCLE_THEME_NAMES_RU[this.newTheme]; const themeText = this.add.text(cx, cy + 40, `Тема: ${themeName}`, { fontSize: '18px', color: '#44ddaa', fontFamily: 'monospace', }).setOrigin(0.5).setAlpha(0).setDepth(10); this.tweens.add({ targets: themeText, alpha: 1, duration: 1500, delay: 6000, }); // 5. Lore fragment from the new theme (fades in at 8s) const narrative = this.getThemeNarrative(this.newTheme); if (narrative && narrative.loreFrag.length > 0) { const fragIdx = this.newCycle % narrative.loreFrag.length; const loreText = this.add.text(cx, cy + 90, `«${narrative.loreFrag[fragIdx].textRu}»`, { fontSize: '12px', color: '#668866', fontFamily: 'monospace', fontStyle: 'italic', align: 'center', wordWrap: { width: GAME_WIDTH * 0.7 }, }).setOrigin(0.5).setAlpha(0).setDepth(10); this.tweens.add({ targets: loreText, alpha: 0.7, duration: 2000, delay: 8000, }); } // 6. Mycelium maturation info (fades in at 10s) const matPct = Math.round(this.meta.greatCycle.myceliumMaturation * 100); const matText = this.add.text(cx, cy + 140, `Мицелий созревает... (${matPct}%)`, { fontSize: '12px', color: '#33aa66', fontFamily: 'monospace', }).setOrigin(0.5).setAlpha(0).setDepth(10); this.tweens.add({ targets: matText, alpha: 0.6, duration: 1500, delay: 10000, }); // Skip hint const skipText = this.add.text(cx, GAME_HEIGHT - 25, '[ click to skip ]', { fontSize: '10px', color: '#333333', fontFamily: 'monospace', }).setOrigin(0.5).setDepth(100); // Click to skip after minimum 5 seconds this.input.on('pointerdown', () => { if (this.elapsed > 5000) { this.transitionToCradle(); } }); } update(_time: number, delta: number): void { this.elapsed += delta; // Animate Mycelium particles this.graphics.clear(); const cx = GAME_WIDTH / 2; const cy = GAME_HEIGHT / 2; const t = this.elapsed / 1000; for (const p of this.particles) { // Spiral inward then outward (breathing effect) const dx = cx - p.x; const dy = cy - p.y; const dist = Math.sqrt(dx * dx + dy * dy) || 1; const breathe = Math.sin(t * 0.5) * 0.3; p.vx += (dx / dist) * breathe; p.vy += (dy / dist) * breathe; p.x += p.vx * (delta / 1000); p.y += p.vy * (delta / 1000); // Damping p.vx *= 0.98; p.vy *= 0.98; // Pulsing alpha p.alpha = 0.1 + Math.sin(t * 1.5 + dist * 0.01) * 0.2; this.graphics.fillStyle(p.color, Math.max(0.02, p.alpha)); this.graphics.fillCircle(p.x, p.y, p.r); // Mycelium threads — connecting nearby particles if (Math.random() < 0.01) { const other = this.particles[Math.floor(Math.random() * this.particles.length)]; const threadDist = Math.hypot(p.x - other.x, p.y - other.y); if (threadDist < 80) { this.graphics.lineStyle(1, 0x33aa66, 0.08); this.graphics.beginPath(); this.graphics.moveTo(p.x, p.y); this.graphics.lineTo(other.x, other.y); this.graphics.strokePath(); } } } // Auto-transition after duration if (this.elapsed >= this.duration) { this.transitionToCradle(); } } private pickRenewalMessage(): string { const messages = narrativeData.renewalMessages; const idx = this.previousCycle % messages.length; return messages[idx].textRu; } private getThemeNarrative(theme: CycleTheme): ThemeNarrative | null { const themes = narrativeData.themes as Record; return themes[theme] ?? null; } private transitionToCradle(): void { if ((this as unknown as { _transitioning?: boolean })._transitioning) return; (this as unknown as { _transitioning?: boolean })._transitioning = true; this.cameras.main.fadeOut(2000, 0, 0, 0); this.cameras.main.once('camerafadeoutcomplete', () => { this.scene.start('CradleScene', { meta: this.meta }); }); } }