feat(creatures): add interaction system and GameScene integration
- Projectile-creature collision with armor-based damage reduction - Creature observation system (health/energy/stage when near player) - Creature-to-player melee attacks with cooldown - Full GameScene integration: AI, metabolism, lifecycle, reproduction, interaction - Debug overlay shows creature count - 16 new interaction tests (288 total) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import Phaser from 'phaser';
|
||||
import { createGameWorld, updateTime, type GameWorld } from '../ecs/world';
|
||||
import { Health, Position } from '../ecs/components';
|
||||
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 type { BiomeData } from '../world/types';
|
||||
import { generateWorld } from '../world/generator';
|
||||
import { createWorldTilemap } from '../world/tilemap';
|
||||
@@ -25,6 +26,22 @@ import {
|
||||
} 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';
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
private gameWorld!: GameWorld;
|
||||
@@ -55,6 +72,11 @@ export class GameScene extends Phaser.Scene {
|
||||
FOUR: Phaser.Input.Keyboard.Key;
|
||||
};
|
||||
|
||||
// Creature state
|
||||
private speciesRegistry!: SpeciesRegistry;
|
||||
private speciesLookup!: Map<number, SpeciesData>;
|
||||
private creatureData!: Map<number, CreatureInfo>;
|
||||
|
||||
// Interaction feedback
|
||||
private interactionText!: Phaser.GameObjects.Text;
|
||||
private interactionTimer = 0;
|
||||
@@ -89,7 +111,20 @@ export class GameScene extends Phaser.Scene {
|
||||
this.gameWorld.world, worldData.grid, biome, this.worldSeed,
|
||||
);
|
||||
|
||||
// 6. Create player at spawn position + inventory
|
||||
// 6. Initialize creature systems
|
||||
const allSpecies = speciesDataArray as SpeciesData[];
|
||||
this.speciesRegistry = new SpeciesRegistry(allSpecies);
|
||||
this.speciesLookup = new Map<number, SpeciesData>();
|
||||
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
|
||||
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;
|
||||
@@ -97,7 +132,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.inventory = new Inventory(500, 20);
|
||||
this.quickSlots = new QuickSlots();
|
||||
|
||||
// 7. Camera — follow player, zoom via scroll wheel
|
||||
// 9. Camera — follow player, zoom via scroll wheel
|
||||
const worldPixelW = biome.mapWidth * biome.tileSize;
|
||||
const worldPixelH = biome.mapHeight * biome.tileSize;
|
||||
setupPlayerCamera(this, worldPixelW, worldPixelH);
|
||||
@@ -226,9 +261,54 @@ export class GameScene extends Phaser.Scene {
|
||||
this.tryLaunchProjectile();
|
||||
}
|
||||
|
||||
// 9. Health / death
|
||||
// 9a. Creature AI
|
||||
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);
|
||||
|
||||
// 10. Health / death
|
||||
const dead = healthSystem(this.gameWorld.world);
|
||||
for (const eid of dead) {
|
||||
// Clean up creature tracking
|
||||
if (this.creatureData.has(eid)) {
|
||||
clearMetabolismTracking(eid);
|
||||
this.creatureData.delete(eid);
|
||||
}
|
||||
removeGameEntity(this.gameWorld.world, eid);
|
||||
}
|
||||
|
||||
@@ -262,11 +342,33 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
this.registry.set('invCounts', counts);
|
||||
|
||||
// 14. Debug stats overlay
|
||||
// 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,
|
||||
});
|
||||
} 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
|
||||
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]);
|
||||
this.statsText.setText(`seed: ${this.worldSeed} | ${fps} fps | pos: ${px},${py}`);
|
||||
const creatureCount = [...popCounts.values()].reduce((a, b) => a + b, 0);
|
||||
this.statsText.setText(
|
||||
`seed: ${this.worldSeed} | ${fps} fps | pos: ${px},${py} | creatures: ${creatureCount}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Try to launch a projectile from active quick slot toward mouse */
|
||||
|
||||
Reference in New Issue
Block a user