feat(systems): add buildingSystem with structure progress tracking
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { World } from '../../ecs/World.js';
|
||||
import { GameMap } from '../../map/GameMap.js';
|
||||
import { buildingSystem, type BuildingState, type StructureData } from '../buildingSystem.js';
|
||||
import type { Needs, Movement, NPCBrain, Position, Stats, StatModifiers } from '@dflike/shared';
|
||||
import { PRODUCTIVITY_RECOVERY_RATE } from '@dflike/shared';
|
||||
|
||||
function createBuildingNPC(world: World, x: number, y: number, overrides?: { productivity?: number }) {
|
||||
const e = world.createEntity();
|
||||
world.addComponent<Position>(e, 'position', { x, y });
|
||||
world.addComponent<Needs>(e, 'needs', { hunger: 80, energy: 80, productivity: overrides?.productivity ?? 30 });
|
||||
world.addComponent<Movement>(e, 'movement', { state: 'idle', target: null, path: [], direction: 0, moveProgress: 0 });
|
||||
world.addComponent<NPCBrain>(e, 'npcBrain', { currentGoal: 'build', goalQueue: [] });
|
||||
world.addComponent<Map<string, number>>(e, 'inventory', new Map([['log', 10], ['stone', 5]]));
|
||||
world.addComponent<Stats>(e, 'stats', {
|
||||
strength: 10, dexterity: 10, constitution: 10, intelligence: 10, perception: 10,
|
||||
sociability: 10, courage: 10, curiosity: 10, empathy: 10, temperament: 10,
|
||||
});
|
||||
world.addComponent<StatModifiers>(e, 'statModifiers', { modifiers: [] });
|
||||
return e;
|
||||
}
|
||||
|
||||
describe('buildingSystem', () => {
|
||||
it('progresses building when NPC has buildingState', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createBuildingNPC(world, 5, 5);
|
||||
const structureEntity = world.createEntity();
|
||||
world.addComponent<Position>(structureEntity, 'position', { x: 5, y: 4 });
|
||||
world.addComponent<StructureData>(structureEntity, 'structure', {
|
||||
type: 'stockpile',
|
||||
subtype: 'general',
|
||||
inventory: new Map(),
|
||||
buildProgress: 0,
|
||||
builderEntityId: e,
|
||||
});
|
||||
world.addComponent<BuildingState>(e, 'buildingState', {
|
||||
structureEntityId: structureEntity,
|
||||
recipeId: 'build_stockpile',
|
||||
});
|
||||
buildingSystem(world, map);
|
||||
const structure = world.getComponent<StructureData>(structureEntity, 'structure')!;
|
||||
expect(structure.buildProgress).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('recovers productivity while building', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createBuildingNPC(world, 5, 5, { productivity: 30 });
|
||||
const structureEntity = world.createEntity();
|
||||
world.addComponent<Position>(structureEntity, 'position', { x: 5, y: 4 });
|
||||
world.addComponent<StructureData>(structureEntity, 'structure', {
|
||||
type: 'stockpile',
|
||||
subtype: 'general',
|
||||
inventory: new Map(),
|
||||
buildProgress: 0,
|
||||
builderEntityId: e,
|
||||
});
|
||||
world.addComponent<BuildingState>(e, 'buildingState', {
|
||||
structureEntityId: structureEntity,
|
||||
recipeId: 'build_stockpile',
|
||||
});
|
||||
buildingSystem(world, map);
|
||||
const needs = world.getComponent<Needs>(e, 'needs')!;
|
||||
expect(needs.productivity).toBeCloseTo(30 + PRODUCTIVITY_RECOVERY_RATE);
|
||||
});
|
||||
|
||||
it('completes building when progress reaches 1', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createBuildingNPC(world, 5, 5);
|
||||
const structureEntity = world.createEntity();
|
||||
world.addComponent<Position>(structureEntity, 'position', { x: 5, y: 4 });
|
||||
world.addComponent<StructureData>(structureEntity, 'structure', {
|
||||
type: 'stockpile',
|
||||
subtype: 'general',
|
||||
inventory: new Map(),
|
||||
buildProgress: 0.99,
|
||||
builderEntityId: e,
|
||||
});
|
||||
world.addComponent<BuildingState>(e, 'buildingState', {
|
||||
structureEntityId: structureEntity,
|
||||
recipeId: 'build_stockpile',
|
||||
});
|
||||
buildingSystem(world, map);
|
||||
const structure = world.getComponent<StructureData>(structureEntity, 'structure')!;
|
||||
expect(structure.buildProgress).toBeUndefined();
|
||||
expect(structure.builderEntityId).toBeUndefined();
|
||||
expect(world.getComponent<BuildingState>(e, 'buildingState')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips NPC whose goal is not build', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createBuildingNPC(world, 5, 5);
|
||||
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
|
||||
brain.currentGoal = 'wander';
|
||||
const structureEntity = world.createEntity();
|
||||
world.addComponent<Position>(structureEntity, 'position', { x: 5, y: 4 });
|
||||
world.addComponent<StructureData>(structureEntity, 'structure', {
|
||||
type: 'stockpile',
|
||||
subtype: 'general',
|
||||
inventory: new Map(),
|
||||
buildProgress: 0,
|
||||
builderEntityId: e,
|
||||
});
|
||||
world.addComponent<BuildingState>(e, 'buildingState', {
|
||||
structureEntityId: structureEntity,
|
||||
recipeId: 'build_stockpile',
|
||||
});
|
||||
buildingSystem(world, map);
|
||||
const structure = world.getComponent<StructureData>(structureEntity, 'structure')!;
|
||||
expect(structure.buildProgress).toBe(0);
|
||||
});
|
||||
|
||||
it('resets goal to wander when productivity satisfied after building completes', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createBuildingNPC(world, 5, 5, { productivity: 70 });
|
||||
const structureEntity = world.createEntity();
|
||||
world.addComponent<Position>(structureEntity, 'position', { x: 5, y: 4 });
|
||||
world.addComponent<StructureData>(structureEntity, 'structure', {
|
||||
type: 'stockpile',
|
||||
subtype: 'general',
|
||||
inventory: new Map(),
|
||||
buildProgress: 0.99,
|
||||
builderEntityId: e,
|
||||
});
|
||||
world.addComponent<BuildingState>(e, 'buildingState', {
|
||||
structureEntityId: structureEntity,
|
||||
recipeId: 'build_stockpile',
|
||||
});
|
||||
buildingSystem(world, map);
|
||||
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
|
||||
expect(brain.currentGoal).toBe('wander');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
PRODUCTIVITY_RECOVERY_RATE, PRODUCTIVITY_THRESHOLD,
|
||||
type Needs, type NPCBrain,
|
||||
} from '@dflike/shared';
|
||||
import type { World } from '../ecs/World.js';
|
||||
import type { GameMap } from '../map/GameMap.js';
|
||||
import { industryConfig } from '../config/industryConfig.js';
|
||||
|
||||
export interface StructureData {
|
||||
type: 'stockpile' | 'workshop';
|
||||
subtype: string; // e.g. 'general', 'workbench', 'kiln'
|
||||
inventory: Map<string, number>;
|
||||
buildProgress?: number; // 0-1, undefined = complete
|
||||
builderEntityId?: number;
|
||||
}
|
||||
|
||||
export interface BuildingState {
|
||||
structureEntityId: number;
|
||||
recipeId: string;
|
||||
}
|
||||
|
||||
export function buildingSystem(world: World, _map: GameMap): void {
|
||||
for (const entity of world.query('npcBrain', 'needs')) {
|
||||
const brain = world.getComponent<NPCBrain>(entity, 'npcBrain')!;
|
||||
if (brain.currentGoal !== 'build') continue;
|
||||
|
||||
const bs = world.getComponent<BuildingState>(entity, 'buildingState');
|
||||
if (!bs) continue;
|
||||
|
||||
const needs = world.getComponent<Needs>(entity, 'needs')!;
|
||||
const structure = world.getComponent<StructureData>(bs.structureEntityId, 'structure');
|
||||
if (!structure || structure.buildProgress === undefined) {
|
||||
world.removeComponent(entity, 'buildingState');
|
||||
brain.currentGoal = 'wander';
|
||||
continue;
|
||||
}
|
||||
|
||||
structure.buildProgress += industryConfig.buildProgressPerTick;
|
||||
needs.productivity = Math.min(100, needs.productivity + PRODUCTIVITY_RECOVERY_RATE);
|
||||
|
||||
if (structure.buildProgress >= 1) {
|
||||
structure.buildProgress = undefined;
|
||||
structure.builderEntityId = undefined;
|
||||
world.removeComponent(entity, 'buildingState');
|
||||
|
||||
if (needs.productivity >= PRODUCTIVITY_THRESHOLD) {
|
||||
brain.currentGoal = 'wander';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user