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:
@@ -7,6 +7,7 @@ import { removeGameEntity } from '../ecs/factory';
|
||||
import { PhaserBridge } from '../ecs/bridge';
|
||||
import biomeDataArray from '../data/biomes.json';
|
||||
import speciesDataArray from '../data/creatures.json';
|
||||
import schoolsData from '../data/schools.json';
|
||||
import type { BiomeData } from '../world/types';
|
||||
import { generateWorld } from '../world/generator';
|
||||
import { createWorldTilemap } from '../world/tilemap';
|
||||
@@ -43,6 +44,18 @@ import {
|
||||
} from '../creatures/interaction';
|
||||
import { query } from 'bitecs';
|
||||
|
||||
// Run cycle imports
|
||||
import type { MetaState, SchoolData, RunState } from '../run/types';
|
||||
import { RunPhase, RUN_PHASE_NAMES_RU, PHASE_DURATIONS } from '../run/types';
|
||||
import { createRunState, advancePhase, updateEscalation, recordDiscovery } from '../run/state';
|
||||
import {
|
||||
createCrisisState,
|
||||
applyCrisisDamage,
|
||||
attemptNeutralize,
|
||||
CHEMICAL_PLAGUE,
|
||||
type CrisisState,
|
||||
} from '../run/crisis';
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
private gameWorld!: GameWorld;
|
||||
private bridge!: PhaserBridge;
|
||||
@@ -83,10 +96,25 @@ export class GameScene extends Phaser.Scene {
|
||||
private wasEDown = false;
|
||||
private wasFDown = false;
|
||||
|
||||
// Run cycle state
|
||||
private meta!: MetaState;
|
||||
private runState!: RunState;
|
||||
private crisisState: CrisisState | null = null;
|
||||
private phaseText!: Phaser.GameObjects.Text;
|
||||
private playerDead = false;
|
||||
private crisisOverlay!: Phaser.GameObjects.Rectangle;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
}
|
||||
|
||||
init(data: { meta: MetaState; schoolId: string; runId: number }): void {
|
||||
this.meta = data.meta;
|
||||
this.runState = createRunState(data.runId, data.schoolId);
|
||||
this.crisisState = null;
|
||||
this.playerDead = false;
|
||||
}
|
||||
|
||||
create(): void {
|
||||
// 1. Initialize ECS
|
||||
this.gameWorld = createGameWorld();
|
||||
@@ -124,7 +152,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.gameWorld.world, worldData.grid, biome, this.worldSeed, allSpecies,
|
||||
);
|
||||
|
||||
// 8. Create player at spawn position + inventory
|
||||
// 8. Create player at spawn position + inventory with starting kit
|
||||
const spawn = findSpawnPosition(worldData.grid, biome.tileSize, this.walkableSet);
|
||||
const spawnX = spawn?.x ?? (biome.mapWidth * biome.tileSize) / 2;
|
||||
const spawnY = spawn?.y ?? (biome.mapHeight * biome.tileSize) / 2;
|
||||
@@ -132,6 +160,9 @@ export class GameScene extends Phaser.Scene {
|
||||
this.inventory = new Inventory(500, 20);
|
||||
this.quickSlots = new QuickSlots();
|
||||
|
||||
// Give starting elements from chosen school
|
||||
this.giveStartingKit();
|
||||
|
||||
// 9. Camera — follow player, zoom via scroll wheel
|
||||
const worldPixelW = biome.mapWidth * biome.tileSize;
|
||||
const worldPixelH = biome.mapHeight * biome.tileSize;
|
||||
@@ -190,14 +221,68 @@ export class GameScene extends Phaser.Scene {
|
||||
this.interactionText.setDepth(100);
|
||||
this.interactionText.setAlpha(0);
|
||||
|
||||
// Phase indicator (top-center)
|
||||
this.phaseText = this.add.text(
|
||||
this.cameras.main.width / 2, 12, '', {
|
||||
fontSize: '12px',
|
||||
color: '#00ff88',
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: '#000000aa',
|
||||
padding: { x: 6, y: 2 },
|
||||
},
|
||||
);
|
||||
this.phaseText.setScrollFactor(0);
|
||||
this.phaseText.setOrigin(0.5, 0);
|
||||
this.phaseText.setDepth(100);
|
||||
|
||||
// Crisis overlay (full-screen tinted rectangle, hidden by default)
|
||||
this.crisisOverlay = this.add.rectangle(
|
||||
this.cameras.main.width / 2, this.cameras.main.height / 2,
|
||||
this.cameras.main.width, this.cameras.main.height,
|
||||
0x88ff88, 0,
|
||||
);
|
||||
this.crisisOverlay.setScrollFactor(0);
|
||||
this.crisisOverlay.setDepth(90);
|
||||
|
||||
// 11. Launch UIScene overlay
|
||||
this.scene.launch('UIScene');
|
||||
|
||||
// Transition from Awakening to Exploration after a moment
|
||||
this.time.delayedCall(500, () => {
|
||||
advancePhase(this.runState); // Awakening → Exploration
|
||||
});
|
||||
}
|
||||
|
||||
/** Give the player their school's starting elements */
|
||||
private giveStartingKit(): void {
|
||||
const schools = schoolsData as SchoolData[];
|
||||
const school = schools.find(s => s.id === this.runState.schoolId);
|
||||
if (!school) return;
|
||||
|
||||
for (const symbol of school.startingElements) {
|
||||
const qty = school.startingQuantities[symbol] ?? 1;
|
||||
for (let i = 0; i < qty; i++) {
|
||||
this.inventory.addItem(symbol);
|
||||
}
|
||||
this.quickSlots.autoAssign(symbol);
|
||||
|
||||
// Record discovery of starting elements
|
||||
recordDiscovery(this.runState, 'element', symbol);
|
||||
}
|
||||
}
|
||||
|
||||
update(_time: number, delta: number): void {
|
||||
// Skip updates if death transition is in progress
|
||||
if (this.playerDead) return;
|
||||
|
||||
// 1. Update world time
|
||||
updateTime(this.gameWorld, delta);
|
||||
|
||||
// 1a. Update run state timers
|
||||
this.runState.elapsed += delta;
|
||||
this.runState.phaseTimer += delta;
|
||||
this.updateRunPhase(delta);
|
||||
|
||||
// 2. Read keyboard → InputState
|
||||
const input: InputState = {
|
||||
moveX: (this.keys.D.isDown ? 1 : 0) - (this.keys.A.isDown ? 1 : 0),
|
||||
@@ -242,6 +327,8 @@ export class GameScene extends Phaser.Scene {
|
||||
if (interaction.type === 'collected' || interaction.type === 'depleted') {
|
||||
if (interaction.itemId) {
|
||||
this.quickSlots.autoAssign(interaction.itemId);
|
||||
// Record element discovery
|
||||
recordDiscovery(this.runState, 'element', interaction.itemId);
|
||||
}
|
||||
}
|
||||
this.showInteractionFeedback(interaction.type, interaction.itemId);
|
||||
@@ -261,7 +348,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.tryLaunchProjectile();
|
||||
}
|
||||
|
||||
// 9a. Creature AI
|
||||
// 9a. Creature AI — adjust aggression based on escalation
|
||||
aiSystem(
|
||||
this.gameWorld.world, delta,
|
||||
this.speciesLookup, this.gameWorld.time.tick,
|
||||
@@ -301,9 +388,27 @@ export class GameScene extends Phaser.Scene {
|
||||
// 9f. Creature attacks on player
|
||||
creatureAttackPlayerSystem(this.gameWorld.world, this.speciesLookup);
|
||||
|
||||
// 9g. Crisis damage (if active)
|
||||
if (this.crisisState?.active) {
|
||||
applyCrisisDamage(this.crisisState, delta);
|
||||
// Crisis damages player slowly
|
||||
if (this.crisisState.progress > 0.3) {
|
||||
const crisisDmg = this.crisisState.progress * 0.5 * (delta / 1000);
|
||||
Health.current[this.playerEid] = Math.max(
|
||||
0,
|
||||
(Health.current[this.playerEid] ?? 0) - crisisDmg,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Health / death
|
||||
const dead = healthSystem(this.gameWorld.world);
|
||||
let playerDied = false;
|
||||
for (const eid of dead) {
|
||||
if (eid === this.playerEid) {
|
||||
playerDied = true;
|
||||
continue; // Don't remove player entity yet
|
||||
}
|
||||
// Clean up creature tracking
|
||||
if (this.creatureData.has(eid)) {
|
||||
clearMetabolismTracking(eid);
|
||||
@@ -312,6 +417,12 @@ export class GameScene extends Phaser.Scene {
|
||||
removeGameEntity(this.gameWorld.world, eid);
|
||||
}
|
||||
|
||||
// Handle player death → transition to DeathScene
|
||||
if (playerDied) {
|
||||
this.onPlayerDeath();
|
||||
return;
|
||||
}
|
||||
|
||||
// 10. Render sync
|
||||
this.bridge.sync(this.gameWorld.world);
|
||||
|
||||
@@ -353,6 +464,10 @@ export class GameScene extends Phaser.Scene {
|
||||
energyPercent: closest.energyPercent,
|
||||
stage: closest.stage,
|
||||
});
|
||||
// Record creature observation as discovery
|
||||
if (species) {
|
||||
recordDiscovery(this.runState, 'creature', species.id);
|
||||
}
|
||||
} else {
|
||||
this.registry.set('observedCreature', null);
|
||||
}
|
||||
@@ -361,7 +476,7 @@ export class GameScene extends Phaser.Scene {
|
||||
const popCounts = countPopulations(this.gameWorld.world);
|
||||
this.registry.set('creaturePopulations', popCounts);
|
||||
|
||||
// 17. Debug stats overlay
|
||||
// 17. Debug stats overlay + phase indicator
|
||||
const fps = delta > 0 ? Math.round(1000 / delta) : 0;
|
||||
const px = Math.round(Position.x[this.playerEid]);
|
||||
const py = Math.round(Position.y[this.playerEid]);
|
||||
@@ -369,6 +484,83 @@ export class GameScene extends Phaser.Scene {
|
||||
this.statsText.setText(
|
||||
`seed: ${this.worldSeed} | ${fps} fps | pos: ${px},${py} | creatures: ${creatureCount}`,
|
||||
);
|
||||
|
||||
// Update phase indicator
|
||||
const phaseName = RUN_PHASE_NAMES_RU[this.runState.phase];
|
||||
const escalationPct = Math.round(this.runState.escalation * 100);
|
||||
const crisisInfo = this.crisisState?.active ? ` | ЧУМА: ${Math.round(this.crisisState.progress * 100)}%` : '';
|
||||
this.phaseText.setText(`${phaseName} | Эскалация: ${escalationPct}%${crisisInfo}`);
|
||||
|
||||
// Color phase text based on danger
|
||||
if (this.runState.phase >= RunPhase.Crisis) {
|
||||
this.phaseText.setColor('#ff4444');
|
||||
} else if (this.runState.phase >= RunPhase.Escalation) {
|
||||
this.phaseText.setColor('#ffaa00');
|
||||
} else {
|
||||
this.phaseText.setColor('#00ff88');
|
||||
}
|
||||
}
|
||||
|
||||
/** Manage run phase transitions and escalation */
|
||||
private updateRunPhase(delta: number): void {
|
||||
const phase = this.runState.phase;
|
||||
const timer = this.runState.phaseTimer;
|
||||
const duration = PHASE_DURATIONS[phase];
|
||||
|
||||
// Update escalation
|
||||
updateEscalation(this.runState, delta);
|
||||
|
||||
// Auto-advance timed phases
|
||||
if (duration > 0 && timer >= duration) {
|
||||
if (phase === RunPhase.Exploration) {
|
||||
advancePhase(this.runState); // → Escalation
|
||||
} else if (phase === RunPhase.Escalation) {
|
||||
advancePhase(this.runState); // → Crisis
|
||||
this.triggerCrisis();
|
||||
} else if (phase === RunPhase.Resolution) {
|
||||
// Run complete — could transition to a victory scene
|
||||
// For now, just keep playing
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger crisis when escalation hits threshold (even before phase ends)
|
||||
if (
|
||||
phase === RunPhase.Escalation &&
|
||||
!this.crisisState &&
|
||||
this.runState.escalation >= CHEMICAL_PLAGUE.triggerThreshold
|
||||
) {
|
||||
advancePhase(this.runState); // → Crisis
|
||||
this.triggerCrisis();
|
||||
}
|
||||
}
|
||||
|
||||
/** Activate the Chemical Plague crisis */
|
||||
private triggerCrisis(): void {
|
||||
this.crisisState = createCrisisState(CHEMICAL_PLAGUE);
|
||||
this.runState.crisisActive = true;
|
||||
|
||||
this.showInteractionFeedback('collected', '⚠ ХИМИЧЕСКАЯ ЧУМА! Создай CaO для нейтрализации!');
|
||||
|
||||
// Tint the world slightly toxic via overlay
|
||||
this.crisisOverlay.setFillStyle(0x88ff88, 0.08);
|
||||
}
|
||||
|
||||
/** Handle player death — start death sequence */
|
||||
private onPlayerDeath(): void {
|
||||
this.playerDead = true;
|
||||
this.runState.alive = false;
|
||||
|
||||
// Stop UIScene
|
||||
this.scene.stop('UIScene');
|
||||
|
||||
// Slow-motion effect
|
||||
this.cameras.main.fadeOut(2000, 0, 0, 0);
|
||||
this.cameras.main.once('camerafadeoutcomplete', () => {
|
||||
this.scene.start('DeathScene', {
|
||||
meta: this.meta,
|
||||
runState: this.runState,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Try to launch a projectile from active quick slot toward mouse */
|
||||
@@ -397,20 +589,38 @@ export class GameScene extends Phaser.Scene {
|
||||
itemId,
|
||||
);
|
||||
|
||||
// Check if this compound neutralizes the crisis
|
||||
if (this.crisisState?.active && itemId === CHEMICAL_PLAGUE.neutralizer) {
|
||||
attemptNeutralize(this.crisisState, itemId, 1);
|
||||
if (this.crisisState.resolved) {
|
||||
this.runState.crisisResolved = true;
|
||||
this.runState.crisisActive = false;
|
||||
this.crisisOverlay.setFillStyle(0x88ff88, 0);
|
||||
this.showInteractionFeedback('collected', '✓ Чума нейтрализована!');
|
||||
advancePhase(this.runState); // Crisis → Resolution
|
||||
} else {
|
||||
this.showInteractionFeedback('collected', `Нейтрализация: ${this.crisisState.neutralizeApplied}/${CHEMICAL_PLAGUE.neutralizeAmount}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear quick slot if inventory is now empty for this item
|
||||
if (!this.inventory.hasItem(itemId)) {
|
||||
const slotIdx = this.quickSlots.getAll().indexOf(itemId);
|
||||
if (slotIdx >= 0) this.quickSlots.assign(slotIdx, null);
|
||||
}
|
||||
|
||||
this.showInteractionFeedback('collected', `Threw ${itemId}`);
|
||||
if (!this.crisisState?.active || itemId !== CHEMICAL_PLAGUE.neutralizer) {
|
||||
this.showInteractionFeedback('collected', `Threw ${itemId}`);
|
||||
}
|
||||
}
|
||||
|
||||
private showInteractionFeedback(type: string, itemId?: string): void {
|
||||
let msg = '';
|
||||
switch (type) {
|
||||
case 'collected':
|
||||
msg = itemId?.startsWith('Threw') ? itemId : `+1 ${itemId ?? ''}`;
|
||||
msg = itemId?.startsWith('Threw') || itemId?.startsWith('⚠') || itemId?.startsWith('✓') || itemId?.startsWith('Нейтрализация')
|
||||
? itemId
|
||||
: `+1 ${itemId ?? ''}`;
|
||||
break;
|
||||
case 'depleted':
|
||||
msg = `+1 ${itemId ?? ''} (depleted)`;
|
||||
|
||||
Reference in New Issue
Block a user