phase 6: scene flow — Cradle, Death, Fractal scenes + run integration
- Add CradleScene (Spore Cradle): school selection UI with spore particles - Add DeathScene: body decomposes into real elements with labels - Add FractalScene: WebGL Mandelbrot/Julia shader + canvas fallback - Integrate RunState into GameScene: phase management, escalation, crisis - Give starting kit from chosen school on run start - Player death triggers DeathScene → FractalScene → CradleScene loop - Track element/creature discoveries during gameplay - Chemical Plague crisis: tinted overlay, player damage, neutralization - BootScene loads meta from IndexedDB, goes to CradleScene - Update version to v0.6.0 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
261
src/scenes/DeathScene.ts
Normal file
261
src/scenes/DeathScene.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user