import { describe, it, expect, beforeEach } from 'vitest'; import { createWorld, addEntity, addComponent, query } from 'bitecs'; import type { World } from '../src/ecs/world'; import { Position, Velocity, Health, SpriteRef, Creature, AI, Metabolism, LifeCycle, Resource, PlayerTag, } from '../src/ecs/components'; import { SpeciesId, AIState, LifeStage, SpeciesRegistry, } from '../src/creatures/types'; import type { SpeciesData, CreatureInfo } from '../src/creatures/types'; import { createCreatureEntity } from '../src/creatures/factory'; import { aiSystem } from '../src/creatures/ai'; import { metabolismSystem, resetMetabolismTracking, } from '../src/creatures/metabolism'; import { lifeCycleSystem } from '../src/creatures/lifecycle'; import { countPopulations, reproduce } from '../src/creatures/population'; import speciesDataArray from '../src/data/creatures.json'; // ─── Test Helpers ──────────────────────────────────────────────── const allSpecies = speciesDataArray as SpeciesData[]; function getSpecies(id: string): SpeciesData { const s = allSpecies.find(s => s.id === id); if (!s) throw new Error(`Species not found: ${id}`); return s; } function buildSpeciesLookup(): Map { const map = new Map(); for (const s of allSpecies) { map.set(s.speciesId, s); } return map; } function createResourceEntity(world: World, x: number, y: number, qty: number): number { const eid = addEntity(world); addComponent(world, eid, Position); addComponent(world, eid, Resource); Position.x[eid] = x; Position.y[eid] = y; Resource.quantity[eid] = qty; Resource.interactRange[eid] = 40; return eid; } function createPlayerEntity(world: World, x: number, y: number): number { const eid = addEntity(world); addComponent(world, eid, Position); addComponent(world, eid, PlayerTag); Position.x[eid] = x; Position.y[eid] = y; return eid; } // ─── Species Data ──────────────────────────────────────────────── describe('Species Data', () => { it('loads 9 species from JSON (3 per biome)', () => { expect(allSpecies).toHaveLength(9); }); it('has Crystallid with correct properties', () => { const c = getSpecies('crystallid'); expect(c.speciesId).toBe(SpeciesId.Crystallid); expect(c.health).toBe(120); expect(c.speed).toBe(30); expect(c.armor).toBe(0.3); expect(c.diet).toBe('mineral'); expect(c.excretionElement).toBe(14); // Si }); it('has Acidophile with correct properties', () => { const a = getSpecies('acidophile'); expect(a.speciesId).toBe(SpeciesId.Acidophile); expect(a.health).toBe(80); expect(a.speed).toBe(50); expect(a.diet).toBe('mineral'); expect(a.excretionElement).toBe(17); // Cl }); it('has Reagent as predator', () => { const r = getSpecies('reagent'); expect(r.speciesId).toBe(SpeciesId.Reagent); expect(r.speed).toBe(80); expect(r.diet).toBe('creature'); expect(r.damage).toBe(20); }); it('all species have valid life cycle durations', () => { for (const species of allSpecies) { expect(species.eggDuration).toBeGreaterThan(0); expect(species.youthDuration).toBeGreaterThan(0); expect(species.matureDuration).toBeGreaterThan(0); expect(species.agingDuration).toBeGreaterThan(0); } }); it('all species have valid color hex strings', () => { for (const species of allSpecies) { expect(species.color).toMatch(/^#[0-9a-fA-F]{6}$/); } }); }); // ─── Species Registry ──────────────────────────────────────────── describe('Species Registry', () => { let registry: SpeciesRegistry; beforeEach(() => { registry = new SpeciesRegistry(allSpecies); }); it('has correct count', () => { expect(registry.count).toBe(9); }); it('looks up by string ID', () => { const c = registry.get('crystallid'); expect(c?.name).toBe('Crystallid'); }); it('looks up by numeric ID', () => { const a = registry.getByNumericId(SpeciesId.Acidophile); expect(a?.id).toBe('acidophile'); }); it('returns undefined for unknown ID', () => { expect(registry.get('nonexistent')).toBeUndefined(); expect(registry.getByNumericId(99 as SpeciesId)).toBeUndefined(); }); it('returns all species', () => { const all = registry.getAll(); expect(all).toHaveLength(9); }); it('can look up new Phase 9 species', () => { expect(registry.get('pendulum')?.biome).toBe('kinetic-mountains'); expect(registry.get('mechanoid')?.biome).toBe('kinetic-mountains'); expect(registry.get('resonator')?.biome).toBe('kinetic-mountains'); expect(registry.get('symbiote')?.biome).toBe('verdant-forests'); expect(registry.get('mimic')?.biome).toBe('verdant-forests'); expect(registry.get('spore-bearer')?.biome).toBe('verdant-forests'); }); it('each biome has exactly 3 species', () => { const all = registry.getAll(); const byBiome = new Map(); for (const s of all) { byBiome.set(s.biome, (byBiome.get(s.biome) ?? 0) + 1); } expect(byBiome.get('catalytic-wastes')).toBe(3); expect(byBiome.get('kinetic-mountains')).toBe(3); expect(byBiome.get('verdant-forests')).toBe(3); }); }); // ─── Creature Factory ──────────────────────────────────────────── describe('Creature Factory', () => { let world: World; beforeEach(() => { world = createWorld(); }); it('creates creature with all components', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 200); expect(Position.x[eid]).toBe(100); expect(Position.y[eid]).toBe(200); expect(Creature.speciesId[eid]).toBe(SpeciesId.Crystallid); expect(LifeCycle.stage[eid]).toBe(LifeStage.Egg); expect(AI.state[eid]).toBe(AIState.Idle); expect(Metabolism.energyMax[eid]).toBe(species.energyMax); }); it('starts as egg with reduced health', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 0, 0, LifeStage.Egg); expect(Health.current[eid]).toBe(Math.round(species.health * 0.3)); expect(LifeCycle.stage[eid]).toBe(LifeStage.Egg); expect(SpriteRef.radius[eid]).toBe(species.radiusYouth); }); it('creates mature creature with full stats', () => { const species = getSpecies('acidophile'); const eid = createCreatureEntity(world, species, 50, 50, LifeStage.Mature); expect(Health.current[eid]).toBe(species.health); expect(Health.max[eid]).toBe(species.health); expect(LifeCycle.stage[eid]).toBe(LifeStage.Mature); expect(SpriteRef.radius[eid]).toBe(species.radius); expect(AI.state[eid]).toBe(AIState.Wander); // mature starts wandering }); it('sets home position to spawn position', () => { const species = getSpecies('reagent'); const eid = createCreatureEntity(world, species, 300, 400); expect(AI.homeX[eid]).toBe(300); expect(AI.homeY[eid]).toBe(400); }); it('sets metabolism from species data', () => { const species = getSpecies('reagent'); const eid = createCreatureEntity(world, species, 0, 0, LifeStage.Mature); expect(Metabolism.energyMax[eid]).toBe(species.energyMax); expect(Metabolism.drainRate[eid]).toBe(species.energyDrainPerSecond); expect(Metabolism.feedAmount[eid]).toBe(species.energyPerFeed); expect(Metabolism.energy[eid]).toBe(species.energyMax * 0.7); }); it('queries creature entities correctly', () => { const s1 = getSpecies('crystallid'); const s2 = getSpecies('reagent'); const e1 = createCreatureEntity(world, s1, 0, 0); const e2 = createCreatureEntity(world, s2, 100, 100); const creatures = [...query(world, [Creature])]; expect(creatures).toContain(e1); expect(creatures).toContain(e2); expect(creatures).toHaveLength(2); }); }); // ─── AI System ─────────────────────────────────────────────────── describe('AI System', () => { let world: World; const speciesLookup = buildSpeciesLookup(); beforeEach(() => { world = createWorld(); }); it('eggs do not move', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Egg); aiSystem(world, 16, speciesLookup, 1); expect(Velocity.vx[eid]).toBe(0); expect(Velocity.vy[eid]).toBe(0); }); it('idle creatures have zero velocity', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.state[eid] = AIState.Idle; AI.stateTimer[eid] = 5000; aiSystem(world, 16, speciesLookup, 1); expect(Velocity.vx[eid]).toBe(0); expect(Velocity.vy[eid]).toBe(0); }); it('wandering creatures have non-zero velocity', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.state[eid] = AIState.Wander; AI.stateTimer[eid] = 5000; aiSystem(world, 16, speciesLookup, 1); const speed = Math.sqrt(Velocity.vx[eid] ** 2 + Velocity.vy[eid] ** 2); expect(speed).toBeGreaterThan(0); }); it('creature flees from nearby player', () => { const species = getSpecies('crystallid'); // fleeRadius = 80 const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.state[eid] = AIState.Wander; AI.stateTimer[eid] = 5000; // Place player within flee radius createPlayerEntity(world, 130, 100); // 30px away, within 80 aiSystem(world, 16, speciesLookup, 1); expect(AI.state[eid]).toBe(AIState.Flee); }); it('Reagents do not flee from player', () => { const species = getSpecies('reagent'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.state[eid] = AIState.Wander; AI.stateTimer[eid] = 5000; createPlayerEntity(world, 110, 100); // very close aiSystem(world, 16, speciesLookup, 1); expect(AI.state[eid]).not.toBe(AIState.Flee); }); it('hungry creature enters Feed state', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.state[eid] = AIState.Wander; AI.stateTimer[eid] = 5000; // Set energy below hunger threshold Metabolism.energy[eid] = species.energyMax * 0.2; // well below 0.4 threshold aiSystem(world, 16, speciesLookup, 1); expect(AI.state[eid]).toBe(AIState.Feed); }); it('Reagent attacks nearby non-Reagent creature when hungry', () => { const reagent = getSpecies('reagent'); const crystallid = getSpecies('crystallid'); const predator = createCreatureEntity(world, reagent, 100, 100, LifeStage.Mature); const prey = createCreatureEntity(world, crystallid, 120, 100, LifeStage.Mature); // 20px away, within aggressionRadius 150 // Make predator hungry Metabolism.energy[predator] = reagent.energyMax * 0.3; AI.state[predator] = AIState.Wander; AI.stateTimer[predator] = 5000; aiSystem(world, 16, speciesLookup, 1); expect(AI.state[predator]).toBe(AIState.Attack); expect(AI.targetEid[predator]).toBe(prey); }); it('attacking creature deals damage when in range', () => { const reagent = getSpecies('reagent'); const crystallid = getSpecies('crystallid'); const predator = createCreatureEntity(world, reagent, 100, 100, LifeStage.Mature); const prey = createCreatureEntity(world, crystallid, 110, 100, LifeStage.Mature); // 10px, within attackRange 18 const preyHpBefore = Health.current[prey]; AI.state[predator] = AIState.Attack; AI.targetEid[predator] = prey; AI.stateTimer[predator] = 5000; AI.attackCooldown[predator] = 0; Metabolism.energy[predator] = reagent.energyMax * 0.3; aiSystem(world, 16, speciesLookup, 1); expect(Health.current[prey]).toBeLessThan(preyHpBefore); }); it('state transitions on timer expiry (idle → wander)', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.state[eid] = AIState.Idle; AI.stateTimer[eid] = 10; // almost expired aiSystem(world, 20, speciesLookup, 1); // 20ms > 10ms timer expect(AI.state[eid]).toBe(AIState.Wander); }); it('wander → idle on timer expiry', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.state[eid] = AIState.Wander; AI.stateTimer[eid] = 10; aiSystem(world, 20, speciesLookup, 1); expect(AI.state[eid]).toBe(AIState.Idle); }); it('creature returns to home when too far', () => { const species = getSpecies('crystallid'); // wanderRadius = 200 const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.homeX[eid] = 100; AI.homeY[eid] = 100; // Move creature far from home Position.x[eid] = 500; // 400px away, beyond wanderRadius 200 Position.y[eid] = 100; AI.state[eid] = AIState.Wander; AI.stateTimer[eid] = 5000; aiSystem(world, 16, speciesLookup, 1); // Should be moving toward home (negative vx toward 100) expect(Velocity.vx[eid]).toBeLessThan(0); }); }); // ─── Metabolism System ─────────────────────────────────────────── describe('Metabolism System', () => { let world: World; const emptyResourceData = new Map(); beforeEach(() => { world = createWorld(); resetMetabolismTracking(); }); it('drains energy over time', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); const energyBefore = Metabolism.energy[eid]; metabolismSystem(world, 1000, emptyResourceData, 1000); // 1 second expect(Metabolism.energy[eid]).toBeLessThan(energyBefore); expect(Metabolism.energy[eid]).toBeCloseTo( energyBefore - species.energyDrainPerSecond, 1, ); }); it('eggs do not metabolize', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Egg); const energyBefore = Metabolism.energy[eid]; metabolismSystem(world, 1000, emptyResourceData, 1000); expect(Metabolism.energy[eid]).toBe(energyBefore); }); it('starvation deals damage when energy = 0', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); Metabolism.energy[eid] = 0; const hpBefore = Health.current[eid]; metabolismSystem(world, 1000, emptyResourceData, 1000); expect(Health.current[eid]).toBeLessThan(hpBefore); }); it('creature feeds from nearby resource', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.state[eid] = AIState.Feed; Metabolism.energy[eid] = 20; // low energy // Place resource next to creature const resEid = createResourceEntity(world, 110, 100, 5); metabolismSystem(world, 16, emptyResourceData, 5000); // Should have gained energy expect(Metabolism.energy[eid]).toBeGreaterThan(20); // Resource should be depleted by 1 expect(Resource.quantity[resEid]).toBe(4); }); it('returns excretion events after feeding', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.state[eid] = AIState.Feed; Metabolism.energy[eid] = 20; createResourceEntity(world, 110, 100, 5); const excretions = metabolismSystem(world, 16, emptyResourceData, 5000); expect(excretions.length).toBeGreaterThan(0); expect(excretions[0][0]).toBe(eid); // creature eid expect(excretions[0][1]).toBe(SpeciesId.Crystallid); }); it('energy does not exceed max', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); AI.state[eid] = AIState.Feed; Metabolism.energy[eid] = species.energyMax - 1; // almost full createResourceEntity(world, 110, 100, 5); metabolismSystem(world, 16, emptyResourceData, 5000); expect(Metabolism.energy[eid]).toBe(species.energyMax); }); it('youth drains energy slower (0.7x)', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Youth); const energyBefore = Metabolism.energy[eid]; metabolismSystem(world, 1000, emptyResourceData, 1000); const drained = energyBefore - Metabolism.energy[eid]; expect(drained).toBeCloseTo(species.energyDrainPerSecond * 0.7, 1); }); it('aging drains energy faster (1.3x)', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Aging); const energyBefore = Metabolism.energy[eid]; metabolismSystem(world, 1000, emptyResourceData, 1000); const drained = energyBefore - Metabolism.energy[eid]; expect(drained).toBeCloseTo(species.energyDrainPerSecond * 1.3, 1); }); }); // ─── Life Cycle System ─────────────────────────────────────────── describe('Life Cycle System', () => { let world: World; const speciesLookup = buildSpeciesLookup(); beforeEach(() => { world = createWorld(); }); it('advances age over time', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); lifeCycleSystem(world, 1000, speciesLookup); expect(LifeCycle.age[eid]).toBe(1000); }); it('egg hatches to youth when timer expires', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Egg); // Fast-forward past egg duration const events = lifeCycleSystem(world, species.eggDuration + 100, speciesLookup); expect(LifeCycle.stage[eid]).toBe(LifeStage.Youth); expect(events.some(e => e.type === 'stage_advance' && e.newStage === LifeStage.Youth)).toBe(true); }); it('youth grows to mature', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Youth); const events = lifeCycleSystem(world, species.youthDuration + 100, speciesLookup); expect(LifeCycle.stage[eid]).toBe(LifeStage.Mature); expect(Health.current[eid]).toBe(species.health); // full health expect(SpriteRef.radius[eid]).toBe(species.radius); // full size expect(events.some(e => e.type === 'stage_advance' && e.newStage === LifeStage.Mature)).toBe(true); }); it('mature ages to aging', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); const events = lifeCycleSystem(world, species.matureDuration + 100, speciesLookup); expect(LifeCycle.stage[eid]).toBe(LifeStage.Aging); expect(Health.max[eid]).toBe(Math.round(species.health * 0.7)); // reduced expect(events.some(e => e.type === 'stage_advance' && e.newStage === LifeStage.Aging)).toBe(true); }); it('aging leads to natural death', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Aging); const events = lifeCycleSystem(world, species.agingDuration + 100, speciesLookup); expect(Health.current[eid]).toBe(0); expect(events.some(e => e.type === 'natural_death')).toBe(true); }); it('reports ready_to_reproduce for mature creatures with high energy', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); Metabolism.energy[eid] = species.energyMax * 0.9; // above 0.8 threshold // Don't let timer expire LifeCycle.stageTimer[eid] = 99999; const events = lifeCycleSystem(world, 100, speciesLookup); expect(events.some(e => e.type === 'ready_to_reproduce' && e.eid === eid)).toBe(true); }); it('does not report reproduction for youth', () => { const species = getSpecies('crystallid'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Youth); Metabolism.energy[eid] = species.energyMax; // full energy but youth LifeCycle.stageTimer[eid] = 99999; const events = lifeCycleSystem(world, 100, speciesLookup); expect(events.some(e => e.type === 'ready_to_reproduce' && e.eid === eid)).toBe(false); }); it('full lifecycle: egg → youth → mature → aging → death', () => { const species = getSpecies('acidophile'); const eid = createCreatureEntity(world, species, 100, 100, LifeStage.Egg); // Egg → Youth lifeCycleSystem(world, species.eggDuration + 1, speciesLookup); expect(LifeCycle.stage[eid]).toBe(LifeStage.Youth); // Youth → Mature lifeCycleSystem(world, species.youthDuration + 1, speciesLookup); expect(LifeCycle.stage[eid]).toBe(LifeStage.Mature); // Mature → Aging lifeCycleSystem(world, species.matureDuration + 1, speciesLookup); expect(LifeCycle.stage[eid]).toBe(LifeStage.Aging); // Aging → Death const events = lifeCycleSystem(world, species.agingDuration + 1, speciesLookup); expect(Health.current[eid]).toBe(0); expect(events.some(e => e.type === 'natural_death')).toBe(true); }); }); // ─── Population Dynamics ───────────────────────────────────────── describe('Population Dynamics', () => { let world: World; beforeEach(() => { world = createWorld(); }); it('counts populations correctly', () => { const s1 = getSpecies('crystallid'); const s2 = getSpecies('reagent'); createCreatureEntity(world, s1, 0, 0, LifeStage.Mature); createCreatureEntity(world, s1, 100, 0, LifeStage.Mature); createCreatureEntity(world, s2, 200, 0, LifeStage.Mature); const counts = countPopulations(world); expect(counts.get(SpeciesId.Crystallid)).toBe(2); expect(counts.get(SpeciesId.Reagent)).toBe(1); expect(counts.get(SpeciesId.Acidophile)).toBeUndefined(); }); it('reproduce creates offspring near parent', () => { const species = getSpecies('crystallid'); // offspringCount = 2 const parent = createCreatureEntity(world, species, 200, 200, LifeStage.Mature); const creatureData = new Map(); const newEids = reproduce(world, parent, species, 1, creatureData); expect(newEids).toHaveLength(2); for (const eid of newEids) { // Offspring should be eggs near parent expect(LifeCycle.stage[eid]).toBe(LifeStage.Egg); const dx = Math.abs(Position.x[eid] - 200); const dy = Math.abs(Position.y[eid] - 200); expect(dx).toBeLessThanOrEqual(25); expect(dy).toBeLessThanOrEqual(25); // Should be tracked in creature data expect(creatureData.has(eid)).toBe(true); } }); it('reproduce respects population cap', () => { const species = getSpecies('reagent'); // maxPopulation = 8, offspringCount = 1 const parent = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); const creatureData = new Map(); // At max population const newEids = reproduce(world, parent, species, 8, creatureData); expect(newEids).toHaveLength(0); }); it('reproduce limits offspring to not exceed cap', () => { const species = getSpecies('crystallid'); // maxPopulation = 12, offspringCount = 2 const parent = createCreatureEntity(world, species, 100, 100, LifeStage.Mature); const creatureData = new Map(); // Population at 11, only room for 1 offspring const newEids = reproduce(world, parent, species, 11, creatureData); expect(newEids).toHaveLength(1); }); }); // ─── Integration ───────────────────────────────────────────────── describe('Creature Integration', () => { let world: World; const speciesLookup = buildSpeciesLookup(); beforeEach(() => { world = createWorld(); resetMetabolismTracking(); }); it('creature lifecycle: hatch → feed → reproduce → age → die', () => { const species = getSpecies('acidophile'); const eid = createCreatureEntity(world, species, 200, 200, LifeStage.Egg); // 1. Hatch lifeCycleSystem(world, species.eggDuration + 1, speciesLookup); expect(LifeCycle.stage[eid]).toBe(LifeStage.Youth); // 2. Grow to mature lifeCycleSystem(world, species.youthDuration + 1, speciesLookup); expect(LifeCycle.stage[eid]).toBe(LifeStage.Mature); // 3. AI works — creature wanders aiSystem(world, 16, speciesLookup, 1); const speed = Math.sqrt(Velocity.vx[eid] ** 2 + Velocity.vy[eid] ** 2); expect(speed).toBeGreaterThan(0); // 4. Metabolism drains energy const energyBefore = Metabolism.energy[eid]; metabolismSystem(world, 1000, new Map(), 10000); expect(Metabolism.energy[eid]).toBeLessThan(energyBefore); // 5. Age to aging lifeCycleSystem(world, species.matureDuration + 1, speciesLookup); expect(LifeCycle.stage[eid]).toBe(LifeStage.Aging); // 6. Natural death const events = lifeCycleSystem(world, species.agingDuration + 1, speciesLookup); expect(events.some(e => e.type === 'natural_death')).toBe(true); }); it('predator-prey interaction: reagent kills crystallid', () => { const reagent = getSpecies('reagent'); const crystallid = getSpecies('crystallid'); const pred = createCreatureEntity(world, reagent, 100, 100, LifeStage.Mature); const prey = createCreatureEntity(world, crystallid, 110, 100, LifeStage.Mature); // Set predator to attack AI.state[pred] = AIState.Attack; AI.targetEid[pred] = prey; AI.stateTimer[pred] = 10000; AI.attackCooldown[pred] = 0; Metabolism.energy[pred] = reagent.energyMax * 0.3; const initialHp = Health.current[prey]; // Simulate several attack cycles for (let i = 0; i < 20; i++) { aiSystem(world, 1000, speciesLookup, i); AI.attackCooldown[pred] = 0; // reset cooldown for test } expect(Health.current[prey]).toBeLessThanOrEqual(0); }); it('creature ecosystem with multiple species runs without errors', () => { const species = allSpecies; // Spawn several of each for (const s of species) { for (let i = 0; i < 3; i++) { createCreatureEntity(world, s, 100 + i * 50, 100 + s.speciesId * 100, LifeStage.Mature); } } // Run 100 ticks for (let tick = 0; tick < 100; tick++) { aiSystem(world, 16, speciesLookup, tick); metabolismSystem(world, 16, new Map(), tick * 16); lifeCycleSystem(world, 16, speciesLookup); } // Should still have creatures alive const counts = countPopulations(world); const total = [...counts.values()].reduce((a, b) => a + b, 0); expect(total).toBeGreaterThan(0); }); });