feat: berry bush depletion on gather and regrowth over time

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-10 20:41:01 +00:00
parent b5c8bdeea9
commit b7865c9265
2 changed files with 66 additions and 3 deletions

View File

@@ -6,13 +6,14 @@ import type { Needs, Movement, NPCBrain, Position, Stats, StatModifiers } from '
import type { StructureData } from '../buildingSystem.js';
import { PRODUCTIVITY_RECOVERY_RATE } from '@dflike/shared';
import { createRuntimeConstants } from '../../config/runtimeConstants.js';
import { industryConfig } from '../../config/industryConfig.js';
const rc = createRuntimeConstants();
function createGatheringNPC(world: World, x: number, y: number, overrides?: { strength?: number; 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<Needs>(e, 'needs', { hunger: 80, thirst: 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: 'gather', goalQueue: [] });
world.addComponent<Map<string, number>>(e, 'inventory', new Map());
@@ -191,4 +192,44 @@ describe('gatheringSystem', () => {
const needs = world.getComponent<Needs>(e, 'needs')!;
expect(needs.productivity).toBeGreaterThan(30);
});
it('depletes berry bush on gather completion', () => {
const world = new World();
const map = new GameMap(10, 10);
map.resourceTiles = [{ x: 3, y: 3, resourceType: 'berries' }];
map.initBerryBush(3, 3, 5);
const e = createGatheringNPC(world, 3, 3);
// Start gathering
gatheringSystem(world, map, rc);
const gs = world.getComponent<GatheringState>(e, 'gatheringState')!;
expect(gs).toBeDefined();
// Fast-forward to completion
gs.ticksRemaining = 1;
gatheringSystem(world, map, rc);
expect(map.getBerryBush(3, 3)!.berryCount).toBe(4);
});
it('skips depleted berry bush when starting gather', () => {
const world = new World();
const map = new GameMap(10, 10);
map.resourceTiles = [{ x: 3, y: 3, resourceType: 'berries' }];
map.initBerryBush(3, 3, 5);
map.getBerryBush(3, 3)!.berryCount = 0;
const e = createGatheringNPC(world, 3, 3);
gatheringSystem(world, map, rc);
const gs = world.getComponent<GatheringState>(e, 'gatheringState');
expect(gs).toBeUndefined();
});
it('regrows berries over time', () => {
const world = new World();
const map = new GameMap(10, 10);
map.initBerryBush(3, 3, 5);
map.getBerryBush(3, 3)!.berryCount = 3;
// Simulate regrowth ticks
for (let i = 0; i < industryConfig.berryRegrowthTicks; i++) {
gatheringSystem(world, map, rc);
}
expect(map.getBerryBush(3, 3)!.berryCount).toBe(4);
});
});

View File

@@ -13,6 +13,17 @@ export interface GatheringState {
}
export function gatheringSystem(world: World, map: GameMap, rc: RuntimeConstants): void {
// Berry bush regrowth
for (const [, bush] of map.berryBushes) {
if (bush.berryCount < bush.maxBerries) {
bush.ticksSinceLastRegrowth++;
if (bush.ticksSinceLastRegrowth >= industryConfig.berryRegrowthTicks) {
bush.berryCount++;
bush.ticksSinceLastRegrowth = 0;
}
}
}
for (const entity of world.query('npcBrain', 'position', 'needs', 'movement')) {
const brain = world.getComponent<NPCBrain>(entity, 'npcBrain')!;
@@ -34,6 +45,12 @@ export function gatheringSystem(world: World, map: GameMap, rc: RuntimeConstants
if (inv) {
addItem(inv, gs.resourceType, industryConfig.gatherYield);
}
// Deplete berry bush
if (gs.resourceType === 'berries') {
const bush = map.getBerryBush(pos.x, pos.y);
if (bush) bush.berryCount = Math.max(0, bush.berryCount - 1);
}
world.removeComponent(entity, 'gatheringState');
// Decide next action: drop off if at carry capacity
@@ -58,9 +75,14 @@ export function gatheringSystem(world: World, map: GameMap, rc: RuntimeConstants
// Not yet gathering — check if at or adjacent to resource tile and idle
if (movement.state !== 'idle') continue;
const resourceTile = map.resourceTiles.find(r => r.x === pos.x && r.y === pos.y)
const hasBerries = (tile: { x: number; y: number; resourceType: string }) => {
if (tile.resourceType !== 'berries') return true;
const bush = map.getBerryBush(tile.x, tile.y);
return bush != null && bush.berryCount > 0;
};
const resourceTile = map.resourceTiles.find(r => r.x === pos.x && r.y === pos.y && hasBerries(r))
?? map.resourceTiles.find(r =>
Math.abs(r.x - pos.x) + Math.abs(r.y - pos.y) === 1 && !map.isWalkable(r.x, r.y)
Math.abs(r.x - pos.x) + Math.abs(r.y - pos.y) === 1 && !map.isWalkable(r.x, r.y) && hasBerries(r)
);
if (!resourceTile) continue;