import Phaser from 'phaser'; import { createGameWorld, updateTime, type GameWorld } from '../ecs/world'; import { Health, Position, Creature, LifeCycle } from '../ecs/components'; import { movementSystem } from '../ecs/systems/movement'; import { healthSystem } from '../ecs/systems/health'; 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'; import { setupPlayerCamera } from '../world/camera'; import { Minimap } from '../world/minimap'; import { playerInputSystem } from '../player/input'; import { tileCollisionSystem, buildWalkableSet } from '../player/collision'; import { findSpawnPosition } from '../player/spawn'; import { createPlayerEntity } from '../player/factory'; import { Inventory } from '../player/inventory'; import { interactionSystem, type ResourceInfo } from '../player/interaction'; import { spawnResources } from '../world/resources'; import { launchProjectile, projectileSystem, type ProjectileData, } from '../player/projectile'; import { QuickSlots } from '../player/quickslots'; import type { InputState } from '../player/types'; import type { SpeciesData, CreatureInfo } from '../creatures/types'; import { SpeciesRegistry } from '../creatures/types'; import { aiSystem } from '../creatures/ai'; import { metabolismSystem, clearMetabolismTracking } from '../creatures/metabolism'; import { lifeCycleSystem } from '../creatures/lifecycle'; import { countPopulations, spawnInitialCreatures, reproduce, } from '../creatures/population'; import { creatureProjectileSystem, getObservableCreatures, creatureAttackPlayerSystem, } 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; private minimap!: Minimap; private statsText!: Phaser.GameObjects.Text; private worldSeed!: number; // Player state private playerEid!: number; private inventory!: Inventory; private walkableSet!: Set; private worldGrid!: number[][]; private tileSize!: number; private resourceData!: Map; private projectileData!: Map; private quickSlots!: QuickSlots; private keys!: { W: Phaser.Input.Keyboard.Key; A: Phaser.Input.Keyboard.Key; S: Phaser.Input.Keyboard.Key; D: Phaser.Input.Keyboard.Key; E: Phaser.Input.Keyboard.Key; F: Phaser.Input.Keyboard.Key; ONE: Phaser.Input.Keyboard.Key; TWO: Phaser.Input.Keyboard.Key; THREE: Phaser.Input.Keyboard.Key; FOUR: Phaser.Input.Keyboard.Key; }; // Creature state private speciesRegistry!: SpeciesRegistry; private speciesLookup!: Map; private creatureData!: Map; // Interaction feedback private interactionText!: Phaser.GameObjects.Text; private interactionTimer = 0; 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(); this.bridge = new PhaserBridge(this); this.projectileData = new Map(); // 2. Generate world const biome = biomeDataArray[0] as BiomeData; this.worldSeed = Date.now() % 1000000; const worldData = generateWorld(biome, this.worldSeed); // 3. Create tilemap createWorldTilemap(this, worldData); // 4. Build walkable set + store world data for collision this.walkableSet = buildWalkableSet(biome.tiles); this.worldGrid = worldData.grid; this.tileSize = biome.tileSize; // 5. Spawn resource entities (mineral veins, geysers) this.resourceData = spawnResources( this.gameWorld.world, worldData.grid, biome, this.worldSeed, ); // 6. Initialize creature systems const allSpecies = speciesDataArray as SpeciesData[]; this.speciesRegistry = new SpeciesRegistry(allSpecies); this.speciesLookup = new Map(); for (const s of allSpecies) { this.speciesLookup.set(s.speciesId, s); } // 7. Spawn creatures across the map this.creatureData = spawnInitialCreatures( this.gameWorld.world, worldData.grid, biome, this.worldSeed, allSpecies, ); // 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; this.playerEid = createPlayerEntity(this.gameWorld.world, spawnX, spawnY); 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; setupPlayerCamera(this, worldPixelW, worldPixelH); // Sync bridge to create sprites, then attach camera follow to player this.bridge.sync(this.gameWorld.world); const playerSprite = this.bridge.getSprite(this.playerEid); if (playerSprite) { playerSprite.setDepth(10); this.cameras.main.startFollow(playerSprite, true, 0.1, 0.1); } // 8. Keyboard input const keyboard = this.input.keyboard; if (!keyboard) throw new Error('Keyboard plugin not available'); this.keys = { W: keyboard.addKey('W'), A: keyboard.addKey('A'), S: keyboard.addKey('S'), D: keyboard.addKey('D'), E: keyboard.addKey('E'), F: keyboard.addKey('F'), ONE: keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.ONE), TWO: keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.TWO), THREE: keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.THREE), FOUR: keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.FOUR), }; // 9. Minimap this.minimap = new Minimap(this, worldData); // 10. UI overlay this.statsText = this.add.text(10, 10, '', { fontSize: '12px', color: '#00ff88', fontFamily: 'monospace', backgroundColor: '#000000aa', padding: { x: 4, y: 2 }, }); this.statsText.setScrollFactor(0); this.statsText.setDepth(100); // Interaction feedback text (center-bottom of screen) this.interactionText = this.add.text( this.cameras.main.width / 2, this.cameras.main.height - 40, '', { fontSize: '14px', color: '#ffdd44', fontFamily: 'monospace', backgroundColor: '#000000cc', padding: { x: 6, y: 3 }, }, ); this.interactionText.setScrollFactor(0); this.interactionText.setOrigin(0.5); 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 }); // Debug: expose kill method for testing death cycle (this as unknown as Record).__debugKill = () => { Health.current[this.playerEid] = 0; }; } /** 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), moveY: (this.keys.S.isDown ? 1 : 0) - (this.keys.W.isDown ? 1 : 0), interact: this.keys.E.isDown, }; // 3. Player input → velocity playerInputSystem(this.gameWorld.world, input); // 4. Movement (all entities including projectiles) movementSystem(this.gameWorld.world, delta); // 5. Tile collision (player only) tileCollisionSystem( this.gameWorld.world, delta, this.worldGrid, this.tileSize, this.walkableSet, ); // 6. Projectile system (lifetime + tile collision) projectileSystem( this.gameWorld.world, delta, this.worldGrid, this.tileSize, this.walkableSet, this.projectileData, ); // 7. Resource interaction (E key, debounced) const isEDown = this.keys.E.isDown; const justPressedE = isEDown && !this.wasEDown; this.wasEDown = isEDown; const interaction = interactionSystem( this.gameWorld.world, justPressedE, this.inventory, this.resourceData, ); if (interaction) { // Auto-assign collected items to quick slots 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); } // 8. Quick slot selection (1-4 keys) if (this.keys.ONE.isDown) this.quickSlots.setActive(0); if (this.keys.TWO.isDown) this.quickSlots.setActive(1); if (this.keys.THREE.isDown) this.quickSlots.setActive(2); if (this.keys.FOUR.isDown) this.quickSlots.setActive(3); // 9. Throw projectile (F key, debounced) — uses active quick slot const isFDown = this.keys.F.isDown; const justPressedF = isFDown && !this.wasFDown; this.wasFDown = isFDown; if (justPressedF) { this.tryLaunchProjectile(); } // 9a. Creature AI — adjust aggression based on escalation aiSystem( this.gameWorld.world, delta, this.speciesLookup, this.gameWorld.time.tick, ); // 9b. Creature metabolism (feeding, energy drain) metabolismSystem( this.gameWorld.world, delta, this.resourceData, this.gameWorld.time.elapsed, ); // 9c. Creature life cycle (aging, stage transitions) const lcEvents = lifeCycleSystem( this.gameWorld.world, delta, this.speciesLookup, ); // 9d. Handle reproduction events const populations = countPopulations(this.gameWorld.world); for (const event of lcEvents) { if (event.type === 'ready_to_reproduce') { const species = this.speciesLookup.get(event.speciesId); if (species) { const currentPop = populations.get(event.speciesId) ?? 0; reproduce( this.gameWorld.world, event.eid, species, currentPop, this.creatureData, ); } } } // 9e. Projectile-creature collision creatureProjectileSystem( this.gameWorld.world, this.projectileData, this.speciesLookup, ); // 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); this.creatureData.delete(eid); } 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); // 11. Minimap viewport this.minimap.update(this.cameras.main); // 12. Fade interaction text if (this.interactionTimer > 0) { this.interactionTimer -= delta; if (this.interactionTimer <= 0) { this.interactionText.setAlpha(0); } } // 13. Push shared state to registry for UIScene this.registry.set('health', Health.current[this.playerEid] ?? 100); this.registry.set('healthMax', Health.max[this.playerEid] ?? 100); this.registry.set('quickSlots', this.quickSlots.getAll()); this.registry.set('activeSlot', this.quickSlots.activeIndex); this.registry.set('invWeight', this.inventory.getTotalWeight()); this.registry.set('invMaxWeight', this.inventory.maxWeight); this.registry.set('invSlots', this.inventory.slotCount); // Build counts map for UIScene const counts = new Map(); for (const item of this.inventory.getItems()) { counts.set(item.id, item.count); } this.registry.set('invCounts', counts); // 15. Creature observation for UIScene const nearbyCreatures = getObservableCreatures(this.gameWorld.world); if (nearbyCreatures.length > 0) { const closest = nearbyCreatures[0]; const species = this.speciesLookup.get(closest.speciesId); this.registry.set('observedCreature', { name: species?.name ?? 'Unknown', healthPercent: closest.healthPercent, 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); } // 16. Population counts for debug const popCounts = countPopulations(this.gameWorld.world); this.registry.set('creaturePopulations', popCounts); // 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]); const creatureCount = [...popCounts.values()].reduce((a, b) => a + b, 0); 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 */ private tryLaunchProjectile(): void { const itemId = this.quickSlots.getActive(); if (!itemId || !this.inventory.hasItem(itemId)) { this.showInteractionFeedback('nothing_nearby'); return; } const removed = this.inventory.removeItem(itemId, 1); if (removed === 0) return; // Get mouse world position for direction const pointer = this.input.activePointer; const worldPoint = this.cameras.main.getWorldPoint(pointer.x, pointer.y); const px = Position.x[this.playerEid]; const py = Position.y[this.playerEid]; launchProjectile( this.gameWorld.world, this.projectileData, px, py, worldPoint.x, worldPoint.y, 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); } 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?.startsWith('⚠') || itemId?.startsWith('✓') || itemId?.startsWith('Нейтрализация') ? itemId : `+1 ${itemId ?? ''}`; break; case 'depleted': msg = `+1 ${itemId ?? ''} (depleted)`; break; case 'inventory_full': msg = `Inventory full! Can't pick up ${itemId ?? ''}`; break; case 'nothing_nearby': msg = 'Nothing to throw / interact with'; break; } this.interactionText.setText(msg); this.interactionText.setAlpha(1); this.interactionTimer = 1500; } }