diff --git a/server/src/systems/__tests__/industryIntegration.test.ts b/server/src/systems/__tests__/industryIntegration.test.ts new file mode 100644 index 0000000..ceee97d --- /dev/null +++ b/server/src/systems/__tests__/industryIntegration.test.ts @@ -0,0 +1,109 @@ +import { describe, it, expect } from 'vitest'; +import { World } from '../../ecs/World.js'; +import { GameMap } from '../../map/GameMap.js'; +import { industrySystem } from '../industrySystem.js'; +import { craftingSystem, type CraftingState } from '../craftingSystem.js'; +import { buildingSystem, type BuildingState, type StructureData } from '../buildingSystem.js'; +import { gatheringSystem, type GatheringState } from '../gatheringSystem.js'; +import { dropoffSystem } from '../dropoffSystem.js'; +import { RecipeRegistry } from '../../industry/recipeRegistry.js'; +import type { Needs, Movement, NPCBrain, Position, Stats, StatModifiers } from '@dflike/shared'; + +function createTestWorld(): { world: World; map: GameMap } { + const world = new World(); + world.setSingleton('recipeRegistry', RecipeRegistry.createDefault()); + const map = new GameMap(20, 20); + map.resourceTiles = [ + { x: 3, y: 3, resourceType: 'log' }, + { x: 7, y: 7, resourceType: 'stone' }, + ]; + return { world, map }; +} + +function createTestNPC(world: World, x: number, y: number, inv?: Map) { + const e = world.createEntity(); + world.addComponent(e, 'position', { x, y }); + world.addComponent(e, 'needs', { hunger: 80, energy: 80, productivity: 20 }); + world.addComponent(e, 'movement', { state: 'idle', target: null, path: [], direction: 0, moveProgress: 0 }); + world.addComponent(e, 'npcBrain', { currentGoal: 'wander', goalQueue: [] }); + world.addComponent>(e, 'inventory', inv ?? new Map()); + world.addComponent(e, 'stats', { + strength: 10, dexterity: 10, constitution: 10, intelligence: 10, perception: 10, + sociability: 10, courage: 10, curiosity: 10, empathy: 10, temperament: 10, + }); + world.addComponent(e, 'statModifiers', { modifiers: [] }); + return e; +} + +describe('industry integration', () => { + it('NPC with materials crafts a tool', () => { + const { world, map } = createTestWorld(); + const inv = new Map([['log', 4], ['stone', 2]]); + const e = createTestNPC(world, 5, 5, inv); + + industrySystem(world, map); + const brain = world.getComponent(e, 'npcBrain')!; + expect(brain.currentGoal).toBe('craft'); + + const cs = world.getComponent(e, 'craftingState')!; + const ticks = cs.ticksRemaining; + for (let i = 0; i < ticks + 1; i++) { + craftingSystem(world); + } + + const finalInv = world.getComponent>(e, 'inventory')!; + expect(finalInv.has('wooden_axe') || finalInv.has('hammer')).toBe(true); + }); + + it('NPC builds a stockpile when it has materials and no stockpile exists', () => { + const { world, map } = createTestWorld(); + const inv = new Map([['log', 4]]); + const e = createTestNPC(world, 5, 5, inv); + + industrySystem(world, map); + const brain = world.getComponent(e, 'npcBrain')!; + expect(brain.currentGoal).toBe('build'); + + const bs = world.getComponent(e, 'buildingState')!; + for (let i = 0; i < 60; i++) { + buildingSystem(world, map); + } + + const structure = world.getComponent(bs.structureEntityId, 'structure')!; + expect(structure.buildProgress).toBeUndefined(); + }); + + it('NPC drops off items at stockpile after gathering', () => { + const { world, map } = createTestWorld(); + const e = createTestNPC(world, 5, 5, new Map([['log', 2]])); + + const stockpileEntity = world.createEntity(); + world.addComponent(stockpileEntity, 'position', { x: 5, y: 5 }); + world.addComponent(stockpileEntity, 'structure', { + type: 'stockpile', + subtype: 'general', + inventory: new Map(), + }); + + const brain = world.getComponent(e, 'npcBrain')!; + brain.currentGoal = 'dropoff'; + + dropoffSystem(world, map); + + const npcInv = world.getComponent>(e, 'inventory')!; + expect(npcInv.size).toBe(0); + + const stockpile = world.getComponent(stockpileEntity, 'structure')!; + expect(stockpile.inventory.get('log')).toBe(2); + expect(brain.currentGoal).toBe('wander'); + }); + + it('full lifecycle: no materials → gather fallback', () => { + const { world, map } = createTestWorld(); + const e = createTestNPC(world, 5, 5); + + industrySystem(world, map); + const brain = world.getComponent(e, 'npcBrain')!; + expect(brain.currentGoal).toBe('gather'); + }); +});