docs: carry capacity and dropoff implementation plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-09 21:47:27 +00:00
parent d66db04832
commit 7a15a3257a
@@ -0,0 +1,346 @@
# Carry Capacity & Dropoff Priority Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Fix NPCs never dropping off gathered resources by adding strength-based carry capacity and fixing dropoff goal priority in the brain system.
**Architecture:** Add `getTotalItemCount()` to inventory helpers and `getCarryCapacity()` to stat helpers. Fix the missing `continue` in npcBrainSystem's dropoff block and add capacity-aware goal priority. Update gatheringSystem to use carry capacity instead of productivity threshold for dropoff decisions.
**Tech Stack:** TypeScript, vitest, ECS component system
---
### Task 1: Add `getTotalItemCount` helper
**Files:**
- Modify: `server/src/industry/inventoryHelpers.ts:25` (add after `getItemCount`)
- Modify: `server/src/industry/__tests__/inventoryHelpers.test.ts:64` (add tests)
**Step 1: Write the failing test**
Add to end of `server/src/industry/__tests__/inventoryHelpers.test.ts` (before the closing `});`):
```typescript
it('getTotalItemCount returns 0 for empty inventory', () => {
const inv = new Map<string, number>();
expect(getTotalItemCount(inv)).toBe(0);
});
it('getTotalItemCount sums all item quantities', () => {
const inv = new Map<string, number>();
addItem(inv, 'log', 3);
addItem(inv, 'stone', 2);
expect(getTotalItemCount(inv)).toBe(5);
});
```
Update the import on line 2 to include `getTotalItemCount`:
```typescript
import { addItem, removeItem, hasItem, getItemCount, getTotalItemCount } from '../inventoryHelpers.js';
```
**Step 2: Run test to verify it fails**
Run: `npm -w server run test -- --run src/industry/__tests__/inventoryHelpers.test.ts`
Expected: FAIL — `getTotalItemCount` is not exported
**Step 3: Write minimal implementation**
Add to end of `server/src/industry/inventoryHelpers.ts`:
```typescript
export function getTotalItemCount(inv: Inventory): number {
let total = 0;
for (const qty of inv.values()) total += qty;
return total;
}
```
**Step 4: Run test to verify it passes**
Run: `npm -w server run test -- --run src/industry/__tests__/inventoryHelpers.test.ts`
Expected: PASS (all 10 tests)
**Step 5: Commit**
```
feat: add getTotalItemCount inventory helper
```
---
### Task 2: Add `getCarryCapacity` helper
**Files:**
- Modify: `server/src/systems/statHelpers.ts:15` (add after `getEffectiveStat`)
- Modify: `server/src/systems/__tests__/statHelpers.test.ts:82` (add tests)
**Step 1: Write the failing tests**
Add to end of `server/src/systems/__tests__/statHelpers.test.ts` (before the closing `});`):
```typescript
it('getCarryCapacity returns floor(strength / 3)', () => {
const world = new World();
const e = world.createEntity();
addStats(world, e, { strength: 10 });
expect(getCarryCapacity(world, e)).toBe(3);
});
it('getCarryCapacity minimum is 1 at STR 3', () => {
const world = new World();
const e = world.createEntity();
addStats(world, e, { strength: 3 });
expect(getCarryCapacity(world, e)).toBe(1);
});
it('getCarryCapacity maximum is 6 at STR 18', () => {
const world = new World();
const e = world.createEntity();
addStats(world, e, { strength: 18 });
expect(getCarryCapacity(world, e)).toBe(6);
});
```
Update the import on line 3 to include `getCarryCapacity`:
```typescript
import { getEffectiveStat, getCarryCapacity } from '../statHelpers.js';
```
**Step 2: Run test to verify it fails**
Run: `npm -w server run test -- --run src/systems/__tests__/statHelpers.test.ts`
Expected: FAIL — `getCarryCapacity` is not exported
**Step 3: Write minimal implementation**
Add to end of `server/src/systems/statHelpers.ts`:
```typescript
export function getCarryCapacity(world: World, entity: number): number {
return Math.floor(getEffectiveStat(world, entity, 'strength') / 3);
}
```
**Step 4: Run test to verify it passes**
Run: `npm -w server run test -- --run src/systems/__tests__/statHelpers.test.ts`
Expected: PASS (all 10 tests)
**Step 5: Commit**
```
feat: add getCarryCapacity stat helper
```
---
### Task 3: Fix npcBrainSystem dropoff preservation and add capacity-aware priority
**Files:**
- Modify: `server/src/systems/npcBrainSystem.ts:1-235`
This is the core bug fix + new behavior. Three changes in npcBrainSystem:
**Step 1: Fix dropoff preservation — add pathfinding and `continue`**
In `server/src/systems/npcBrainSystem.ts`, add imports at top:
```typescript
import { getCarryCapacity } from './statHelpers.js';
import { getTotalItemCount } from '../industry/inventoryHelpers.js';
```
Replace the dropoff preservation block (lines 120-135):
```typescript
// Preserve dropoff goal — NPC should finish delivering items before switching
if (brain.currentGoal === 'dropoff') {
// Check if any completed stockpile exists
let target: Position | null = null;
for (const sEntity of world.query('structure', 'position')) {
const sData = world.getComponent<StructureData>(sEntity, 'structure')!;
if (sData.type !== 'stockpile' || sData.buildProgress !== undefined) continue;
const sPos = world.getComponent<Position>(sEntity, 'position')!;
const d = Math.abs(pos.x - sPos.x) + Math.abs(pos.y - sPos.y);
if (!target || d < (Math.abs(pos.x - target.x) + Math.abs(pos.y - target.y))) {
target = { x: sPos.x, y: sPos.y };
}
}
if (target) {
const path = findPath(map, pos, target);
if (path && path.length > 0) {
movement.state = 'walking';
movement.target = target;
movement.path = path;
movement.direction = directionTo(pos, path[0]);
}
continue;
}
brain.currentGoal = 'wander';
}
```
**Step 2: Add capacity-based dropoff priority in goal assignment**
Replace the goal assignment block (lines 155-163, after the pickup `continue`):
```typescript
// Determine new goal based on needs priority
const prevGoal = brain.currentGoal;
let goal: GoalType = 'wander';
if (needs.energy < ENERGY_THRESHOLD) goal = 'sleep';
else if (isNighttime(gameTime) && needs.energy < SLEEP_VOLUNTARY_ENERGY_THRESHOLD) goal = 'sleep';
else if (needs.hunger < HUNGER_THRESHOLD) goal = 'eat';
else {
// Check if NPC is carrying items at or over capacity → dropoff priority
const inv = world.getComponent<Map<string, number>>(entity, 'inventory');
const totalItems = inv ? getTotalItemCount(inv) : 0;
if (totalItems > 0) {
const capacity = getCarryCapacity(world, entity);
if (totalItems >= capacity) {
goal = 'dropoff';
} else if (needs.productivity < PRODUCTIVITY_THRESHOLD) {
goal = 'gather';
} else {
// Productivity satisfied, drop off remaining items before wandering
goal = 'dropoff';
}
} else if (needs.productivity < PRODUCTIVITY_THRESHOLD) {
goal = 'gather';
}
}
brain.currentGoal = goal;
```
**Step 3: Run all tests**
Run: `npm -w server run test -- --run`
Expected: Some gathering tests may fail (Task 4 will update them). npcBrain has no dedicated test file, so no direct failures there.
**Step 4: Commit**
```
fix: preserve dropoff goal in npcBrainSystem and add carry capacity priority
```
---
### Task 4: Update gatheringSystem to use carry capacity
**Files:**
- Modify: `server/src/systems/gatheringSystem.ts:1-82`
- Modify: `server/src/systems/__tests__/gatheringSystem.test.ts:115-126`
**Step 1: Update gatheringSystem**
Add imports to `server/src/systems/gatheringSystem.ts`:
```typescript
import { getCarryCapacity } from './statHelpers.js';
import { getTotalItemCount } from '../industry/inventoryHelpers.js';
```
Replace the post-gathering decision block (lines 41-55) with:
```typescript
// Decide next action: drop off if at carry capacity
let hasStockpile = false;
for (const sEntity of world.query('structure')) {
const sData = world.getComponent<StructureData>(sEntity, 'structure');
if (sData && sData.type === 'stockpile' && sData.buildProgress === undefined) {
hasStockpile = true;
break;
}
}
const totalItems = inv ? getTotalItemCount(inv) : 0;
const capacity = getCarryCapacity(world, entity);
if (hasStockpile && totalItems >= capacity) {
brain.currentGoal = 'dropoff';
}
// else: stay on 'gather', npcBrain will pick new target or handle dropoff
```
**Step 2: Update the gathering test that checks for wander transition**
Replace the test at line 115 in `server/src/systems/__tests__/gatheringSystem.test.ts`:
```typescript
it('sets dropoff goal when at carry capacity after gathering', () => {
const world = new World();
const map = new GameMap(10, 10);
map.resourceTiles = [{ x: 3, y: 3, resourceType: 'log' }];
// STR 6 → capacity = floor(6/3) = 2, pre-fill inventory with 1 log
const e = createGatheringNPC(world, 3, 3, { strength: 6, productivity: 30 });
const inv = world.getComponent<Map<string, number>>(e, 'inventory')!;
inv.set('log', 1); // already carrying 1
// Create a completed stockpile
const s = world.createEntity();
world.addComponent(s, 'position', { x: 5, y: 5 });
world.addComponent(s, 'structure', { type: 'stockpile', subtype: 'general', inventory: new Map() });
gatheringSystem(world, map);
const gs = world.getComponent<GatheringState>(e, 'gatheringState')!;
gs.ticksRemaining = 1;
gatheringSystem(world, map);
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
expect(brain.currentGoal).toBe('dropoff');
});
it('stays on gather when under carry capacity', () => {
const world = new World();
const map = new GameMap(10, 10);
map.resourceTiles = [{ x: 3, y: 3, resourceType: 'log' }];
// STR 10 → capacity = floor(10/3) = 3, will have 1 item after gather
const e = createGatheringNPC(world, 3, 3, { strength: 10, productivity: 30 });
const s = world.createEntity();
world.addComponent(s, 'position', { x: 5, y: 5 });
world.addComponent(s, 'structure', { type: 'stockpile', subtype: 'general', inventory: new Map() });
gatheringSystem(world, map);
const gs = world.getComponent<GatheringState>(e, 'gatheringState')!;
gs.ticksRemaining = 1;
gatheringSystem(world, map);
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
expect(brain.currentGoal).toBe('gather');
});
```
Note: the `createGatheringNPC` helper needs updating to accept `strength` — it already does via `overrides.strength`.
**Step 3: Add import for StructureData in gathering test**
Update imports at top of `server/src/systems/__tests__/gatheringSystem.test.ts` to include:
```typescript
import type { StructureData } from '../buildingSystem.js';
```
(Note: it's not imported in the existing test file but is needed for the stockpile structure component.)
Wait — the test helper uses `world.addComponent` with inline type. Since `StructureData` is only used for the type assertion, we can use the inline object directly without importing. Actually, looking at the `dropoffSystem.test.ts` pattern, it does import `StructureData`. Follow that pattern.
**Step 4: Run all tests**
Run: `npm -w server run test -- --run`
Expected: PASS (all tests)
**Step 5: Commit**
```
feat: gatheringSystem uses carry capacity for dropoff decision
```
---
### Task 5: Run full test suite and verify
**Step 1: Run full test suite**
Run: `npm -w server run test -- --run`
Expected: All tests pass
**Step 2: Verify no regressions with a quick manual check**
Run: `npm -w server run dev -- --new-world` and observe NPC behavior for a few seconds to confirm NPCs are gathering, filling up, and dropping off.
**Step 3: Final commit if any cleanup needed**
---