/** * DeathScene — Body decomposition and transition to fractal * * GDD spec: * 1. World slows, sounds fade * 2. Body decomposes into real elements (65% O, 18% C, 10% H...) * 3. Elements absorbed into soil → rush toward Mycelium * 4. Transition to FractalScene */ import Phaser from 'phaser'; import type { MetaState, RunState } from '../run/types'; import { BODY_COMPOSITION } from '../run/types'; import { ElementRegistry } from '../chemistry/elements'; import { GAME_WIDTH, GAME_HEIGHT } from '../config'; /** A single element particle flying out during decomposition */ interface DeathParticle { x: number; y: number; vx: number; vy: number; color: number; symbol: string; alpha: number; radius: number; phase: 'explode' | 'settle' | 'absorb'; timer: number; } export class DeathScene extends Phaser.Scene { private meta!: MetaState; private runState!: RunState; private particles: DeathParticle[] = []; private graphics!: Phaser.GameObjects.Graphics; private elapsed = 0; private phaseState: 'decompose' | 'absorb' | 'fadeout' = 'decompose'; private labelTexts: Phaser.GameObjects.Text[] = []; private compositionText!: Phaser.GameObjects.Text; constructor() { super({ key: 'DeathScene' }); } init(data: { meta: MetaState; runState: RunState }): void { this.meta = data.meta; this.runState = data.runState; this.particles = []; this.elapsed = 0; this.phaseState = 'decompose'; this.labelTexts = []; } create(): void { this.cameras.main.setBackgroundColor('#020202'); this.graphics = this.add.graphics(); const cx = GAME_WIDTH / 2; const cy = GAME_HEIGHT / 2; // Player "body" — starts as a bright circle, then decomposes const bodyGlow = this.add.circle(cx, cy, 16, 0x00e5ff, 1); bodyGlow.setDepth(10); // Title text const deathText = this.add.text(cx, 60, 'Распад...', { fontSize: '20px', color: '#334455', fontFamily: 'monospace', }); deathText.setOrigin(0.5); deathText.setAlpha(0); // Composition display this.compositionText = this.add.text(cx, GAME_HEIGHT - 50, '', { fontSize: '11px', color: '#445566', fontFamily: 'monospace', }); this.compositionText.setOrigin(0.5); this.compositionText.setAlpha(0); // Phase 1: Body pulses, then explodes into elements this.tweens.add({ targets: bodyGlow, scaleX: 1.5, scaleY: 1.5, alpha: 0.7, duration: 600, yoyo: true, repeat: 2, ease: 'Sine.easeInOut', onComplete: () => { // Explode into element particles this.spawnElementParticles(cx, cy); bodyGlow.destroy(); // Show death text this.tweens.add({ targets: deathText, alpha: 0.6, duration: 1000, }); // Show composition const compText = BODY_COMPOSITION .map(e => `${e.symbol}: ${(e.fraction * 100).toFixed(1)}%`) .join(' '); this.compositionText.setText(`Элементный состав: ${compText}`); this.tweens.add({ targets: this.compositionText, alpha: 0.5, duration: 2000, delay: 500, }); }, }); // After 6 seconds, start absorb phase this.time.delayedCall(5500, () => { this.phaseState = 'absorb'; this.tweens.add({ targets: deathText, alpha: 0, duration: 800, }); }); // After 8 seconds, transition to fractal this.time.delayedCall(8000, () => { this.phaseState = 'fadeout'; this.cameras.main.fadeOut(1500, 0, 0, 0); this.cameras.main.once('camerafadeoutcomplete', () => { this.scene.start('FractalScene', { meta: this.meta, runState: this.runState, }); }); }); } private spawnElementParticles(cx: number, cy: number): void { const totalParticles = 80; for (const comp of BODY_COMPOSITION) { const count = Math.max(1, Math.round(comp.fraction * totalParticles)); const elem = ElementRegistry.getBySymbol(comp.symbol); const color = elem ? parseInt(elem.color.replace('#', ''), 16) : 0xffffff; for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 40 + Math.random() * 120; this.particles.push({ x: cx + (Math.random() - 0.5) * 6, y: cy + (Math.random() - 0.5) * 6, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, color, symbol: comp.symbol, alpha: 0.8 + Math.random() * 0.2, radius: 2 + comp.fraction * 6, phase: 'explode', timer: 0, }); } // Add a visible label for the most abundant elements if (comp.fraction >= 0.03) { const labelAngle = Math.random() * Math.PI * 2; const labelDist = 60 + comp.fraction * 200; const labelText = this.add.text( cx + Math.cos(labelAngle) * labelDist, cy + Math.sin(labelAngle) * labelDist, `${comp.symbol} ${(comp.fraction * 100).toFixed(0)}%`, { fontSize: '13px', color: `#${color.toString(16).padStart(6, '0')}`, fontFamily: 'monospace', }, ); labelText.setOrigin(0.5); labelText.setAlpha(0); this.tweens.add({ targets: labelText, alpha: 0.7, duration: 800, delay: 200 + Math.random() * 600, }); this.labelTexts.push(labelText); } } } update(_time: number, delta: number): void { this.elapsed += delta; const dt = delta / 1000; this.graphics.clear(); const cx = GAME_WIDTH / 2; const cy = GAME_HEIGHT / 2; for (const p of this.particles) { p.timer += delta; if (this.phaseState === 'decompose') { // Particles fly outward, then slow down p.vx *= 0.98; p.vy *= 0.98; p.x += p.vx * dt; p.y += p.vy * dt; } else if (this.phaseState === 'absorb') { // Particles get pulled toward center-bottom (into "soil"/Mycelium) const targetX = cx + (Math.random() - 0.5) * 20; const targetY = GAME_HEIGHT + 20; const dx = targetX - p.x; const dy = targetY - p.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist > 1) { p.vx += (dx / dist) * 200 * dt; p.vy += (dy / dist) * 200 * dt; } p.x += p.vx * dt; p.y += p.vy * dt; p.alpha *= 0.995; } else { // Fadeout — everything fades p.alpha *= 0.97; } // Draw particle if (p.alpha > 0.02) { this.graphics.fillStyle(p.color, p.alpha); this.graphics.fillCircle(p.x, p.y, p.radius); } } // Fade labels during absorb if (this.phaseState === 'absorb' || this.phaseState === 'fadeout') { for (const label of this.labelTexts) { label.setAlpha(label.alpha * 0.98); } } // Draw "Mycelium threads" rushing down during absorb if (this.phaseState === 'absorb') { this.graphics.lineStyle(1, 0x00ff88, 0.15); for (let i = 0; i < 5; i++) { const tx = cx + (Math.random() - 0.5) * 200; this.graphics.beginPath(); this.graphics.moveTo(tx, cy); let y = cy; for (let j = 0; j < 8; j++) { y += 30 + Math.random() * 20; this.graphics.lineTo(tx + (Math.random() - 0.5) * 30, y); } this.graphics.strokePath(); } } } }