docs: add daytime nap implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
357
docs/superpowers/plans/2026-03-11-daytime-nap.md
Normal file
357
docs/superpowers/plans/2026-03-11-daytime-nap.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# Daytime Nap System Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make NPCs take short naps during the day instead of full sleeps, so they wake with just enough energy to reach nightfall and naturally resynchronize to a nocturnal sleep schedule.
|
||||
|
||||
**Architecture:** Add a `NAP_BUFFER` constant and modify the sleep wake logic in `npcBrainSystem` to calculate a daytime nap target based on remaining daylight. The nap target uses existing energy decay math to predict how much energy the NPC needs to stay awake until nightfall. No new components or systems needed.
|
||||
|
||||
**Tech Stack:** TypeScript, vitest
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: Implementation
|
||||
|
||||
### Task 1: Add NAP_BUFFER constant
|
||||
|
||||
**Files:**
|
||||
- Modify: `shared/src/constants.ts:21-26` (sleep system constants section)
|
||||
- Modify: `shared/src/types.ts:274-305` (TunableConstants interface)
|
||||
- Modify: `server/src/config/runtimeConstants.ts:2-22` (imports and DEFAULTS)
|
||||
|
||||
- [ ] **Step 1: Add NAP_BUFFER to shared constants**
|
||||
|
||||
In `shared/src/constants.ts`, add after line 25 (`SLEEP_VOLUNTARY_ENERGY_THRESHOLD`):
|
||||
|
||||
```typescript
|
||||
export const NAP_BUFFER = 15; // energy buffer above minimum for daytime naps
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add NAP_BUFFER to TunableConstants interface**
|
||||
|
||||
In `shared/src/types.ts`, add inside the `TunableConstants` interface after `SLEEP_VOLUNTARY_ENERGY_THRESHOLD`:
|
||||
|
||||
```typescript
|
||||
NAP_BUFFER: number;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add NAP_BUFFER to runtimeConstants imports and DEFAULTS**
|
||||
|
||||
In `server/src/config/runtimeConstants.ts`, add `NAP_BUFFER` to the import from `@dflike/shared` (line 5, in the sleep-related group):
|
||||
|
||||
```typescript
|
||||
SLEEP_ENERGY_RECOVERY_PER_TICK, SLEEP_HUNGER_DECAY_MULTIPLIER, SLEEP_WAKE_THRESHOLD, SLEEP_VOLUNTARY_ENERGY_THRESHOLD, NAP_BUFFER,
|
||||
```
|
||||
|
||||
Add `NAP_BUFFER` to the DEFAULTS object (line 15, in the sleep-related group):
|
||||
|
||||
```typescript
|
||||
SLEEP_ENERGY_RECOVERY_PER_TICK, SLEEP_HUNGER_DECAY_MULTIPLIER, SLEEP_WAKE_THRESHOLD, SLEEP_VOLUNTARY_ENERGY_THRESHOLD, NAP_BUFFER,
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Rebuild shared types**
|
||||
|
||||
Run: `npx -w shared tsc`
|
||||
Expected: Clean compilation, no errors.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add shared/src/constants.ts shared/src/types.ts server/src/config/runtimeConstants.ts
|
||||
git commit -m "feat: add NAP_BUFFER constant for daytime nap system"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Write failing tests for daytime nap wake logic
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/systems/__tests__/systems.test.ts`
|
||||
|
||||
Tests go in the existing `describe('npcBrainSystem', ...)` block. They use the existing `createNPC`, `addStats`, and `rc` helpers already defined in the test file. Each test must declare its own `const world = new World()` and `const map = new GameMap(10, 10)` following the existing pattern.
|
||||
|
||||
- [ ] **Step 1: Write test — daytime nap wakes at calculated target**
|
||||
|
||||
This test verifies that when an NPC is sleeping during the day (gameTime < 0.667), they wake up at the nap target energy instead of the normal 85-100 range. We set gameTime to 0.5 (afternoon) and energy to a value that would be above the nap target for that time.
|
||||
|
||||
The nap target formula:
|
||||
- `cycleTicks = Math.round((100 / 0.03) * (1 + 1/2))` = 5000
|
||||
- `remainingDayTicks = (0.667 - 0.5) * 5000` = 835
|
||||
- `energyNeeded = 835 * 0.03 * 1.0` = 25.05 (CON 10 → multiplier 1.0)
|
||||
- `napTarget = 20 + 25.05 + 15` = 60.05
|
||||
|
||||
So at gameTime 0.5, an NPC with CON 10 needs ~60 energy to nap-wake. Set energy to 61 and verify they wake.
|
||||
|
||||
```typescript
|
||||
it('wakes from daytime nap at calculated energy target', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createNPC(world, 5, 5, 80, 61);
|
||||
addStats(world, e, { constitution: 10 });
|
||||
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
|
||||
brain.currentGoal = 'sleep';
|
||||
npcBrainSystem(world, map, 0.5, undefined, rc);
|
||||
expect(brain.currentGoal).not.toBe('sleep');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write test — stays asleep during daytime nap when below target**
|
||||
|
||||
Same setup but energy is below the nap target. NPC should stay asleep.
|
||||
|
||||
```typescript
|
||||
it('stays asleep during daytime nap when energy below target', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createNPC(world, 5, 5, 80, 55);
|
||||
addStats(world, e, { constitution: 10 });
|
||||
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
|
||||
brain.currentGoal = 'sleep';
|
||||
npcBrainSystem(world, map, 0.5, undefined, rc);
|
||||
expect(brain.currentGoal).toBe('sleep');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write test — guard: near nightfall falls through to full sleep**
|
||||
|
||||
When gameTime is close to nightfall (e.g., 0.63), the nap target would be low (below 60), so the guard triggers and the NPC uses normal full sleep wake logic (85-100 range). Set energy to 70 — above a would-be low nap target but below 85 — and verify the NPC stays asleep (normal wake logic requires >= 85).
|
||||
|
||||
At gameTime 0.63:
|
||||
- `remainingDayTicks = (0.667 - 0.63) * 5000` = 185
|
||||
- `energyNeeded = 185 * 0.03 * 1.0` = 5.55
|
||||
- `napTarget = 20 + 5.55 + 15` = 40.55 (< 60, guard triggers)
|
||||
|
||||
```typescript
|
||||
it('uses full sleep when nap target below voluntary threshold (near nightfall)', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createNPC(world, 5, 5, 80, 70);
|
||||
addStats(world, e, { constitution: 10 });
|
||||
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
|
||||
brain.currentGoal = 'sleep';
|
||||
// gameTime 0.63 is close to nightfall (0.667) — nap target < 60, guard triggers
|
||||
npcBrainSystem(world, map, 0.63, undefined, rc);
|
||||
expect(brain.currentGoal).toBe('sleep'); // stays asleep, uses normal 85-100 wake
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Write test — nighttime sleep uses normal wake logic (unchanged)**
|
||||
|
||||
Verify existing nighttime behavior is preserved. At nighttime (gameTime >= 0.667), nap logic does not apply.
|
||||
|
||||
```typescript
|
||||
it('uses normal wake logic during nighttime sleep (nap does not apply)', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createNPC(world, 5, 5, 80, 70);
|
||||
addStats(world, e, { constitution: 10 });
|
||||
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
|
||||
brain.currentGoal = 'sleep';
|
||||
npcBrainSystem(world, map, 0.8, undefined, rc); // nighttime
|
||||
expect(brain.currentGoal).toBe('sleep'); // 70 < 85, stays asleep in normal mode
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Write test — CON affects nap target (high CON = shorter nap)**
|
||||
|
||||
High CON (14) → conMultiplier = 1 - (14-10)*0.03 = 0.88. Lower decay means less energy needed.
|
||||
|
||||
At gameTime 0.5:
|
||||
- `energyNeeded = 835 * 0.03 * 0.88` = 22.04
|
||||
- `napTarget = 20 + 22.04 + 15` = 57.04 (< 60, guard triggers → full sleep!)
|
||||
|
||||
Use gameTime 0.4 instead:
|
||||
- `remainingDayTicks = (0.667 - 0.4) * 5000` = 1335
|
||||
- `energyNeeded = 1335 * 0.03 * 0.88` = 35.24
|
||||
- `napTarget = 20 + 35.24 + 15` = 70.24
|
||||
|
||||
Low CON (6) → conMultiplier = 1 - (6-10)*0.03 = 1.12:
|
||||
- `energyNeeded = 1335 * 0.03 * 1.12` = 44.86
|
||||
- `napTarget = 20 + 44.86 + 15` = 79.86
|
||||
|
||||
Set energy to 75. High-CON NPC (napTarget ~70) should wake. Low-CON NPC (napTarget ~80) should stay asleep.
|
||||
|
||||
```typescript
|
||||
it('high CON NPC has lower nap target (shorter nap)', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createNPC(world, 5, 5, 80, 75);
|
||||
addStats(world, e, { constitution: 14 });
|
||||
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
|
||||
brain.currentGoal = 'sleep';
|
||||
npcBrainSystem(world, map, 0.4, undefined, rc);
|
||||
expect(brain.currentGoal).not.toBe('sleep'); // napTarget ~70, energy 75 > 70
|
||||
});
|
||||
|
||||
it('low CON NPC has higher nap target (longer nap)', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createNPC(world, 5, 5, 80, 75);
|
||||
addStats(world, e, { constitution: 6 });
|
||||
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
|
||||
brain.currentGoal = 'sleep';
|
||||
npcBrainSystem(world, map, 0.4, undefined, rc);
|
||||
expect(brain.currentGoal).toBe('sleep'); // napTarget ~80, energy 75 < 80
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Write test — early morning crash clamps nap target to 100**
|
||||
|
||||
At gameTime 0.1 (early morning):
|
||||
- `remainingDayTicks = (0.667 - 0.1) * 5000` = 2835
|
||||
- `energyNeeded = 2835 * 0.03 * 1.0` = 85.05
|
||||
- `napTarget = min(20 + 85.05 + 15, 100)` = 100 (clamped)
|
||||
|
||||
An NPC at energy 99 should stay asleep (napTarget is 100, handled by the `energy >= 100` check at the top).
|
||||
|
||||
```typescript
|
||||
it('clamps nap target to 100 for early morning crashes', () => {
|
||||
const world = new World();
|
||||
const map = new GameMap(10, 10);
|
||||
const e = createNPC(world, 5, 5, 80, 99);
|
||||
addStats(world, e, { constitution: 10 });
|
||||
const brain = world.getComponent<NPCBrain>(e, 'npcBrain')!;
|
||||
brain.currentGoal = 'sleep';
|
||||
npcBrainSystem(world, map, 0.1, undefined, rc);
|
||||
expect(brain.currentGoal).toBe('sleep'); // napTarget clamped to 100, energy 99 < 100
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Run tests to verify they fail**
|
||||
|
||||
Run: `npm -w server run test -- --run`
|
||||
Expected: 7 new tests FAIL (nap logic not yet implemented). All existing tests should still pass.
|
||||
|
||||
- [ ] **Step 8: Commit failing tests**
|
||||
|
||||
```bash
|
||||
git add server/src/systems/__tests__/systems.test.ts
|
||||
git commit -m "test: add failing tests for daytime nap wake logic"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Implement daytime nap wake logic
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/systems/npcBrainSystem.ts:1-16` (imports)
|
||||
- Modify: `server/src/systems/npcBrainSystem.ts:111-156` (sleep wake block)
|
||||
|
||||
- [ ] **Step 1: Add imports**
|
||||
|
||||
In `npcBrainSystem.ts`, add `getEffectiveStat` to the import from `./statHelpers.js` (line 14):
|
||||
|
||||
```typescript
|
||||
import { getCarryCapacity, getEffectiveStat } from './statHelpers.js';
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the sleep wake logic**
|
||||
|
||||
Replace the sleep wake block (lines 136-156) — from `if (brain.currentGoal === 'sleep') {` to its closing `}` — with:
|
||||
|
||||
```typescript
|
||||
// Handle sleeping NPCs: check wake condition
|
||||
if (brain.currentGoal === 'sleep') {
|
||||
// Always wake at full energy
|
||||
if (needs.energy >= 100) {
|
||||
brain.currentGoal = null;
|
||||
} else if (!isNighttime(gameTime) && rc) {
|
||||
// Daytime nap: calculate energy needed to reach nightfall
|
||||
const energyDecay = rc.get('ENERGY_DECAY_PER_TICK');
|
||||
const dayNightRatio = rc.get('DAY_NIGHT_RATIO');
|
||||
const cycleTicks = Math.round((100 / energyDecay) * (1 + 1 / dayNightRatio));
|
||||
const remainingDayTicks = (SLEEP_NIGHT_START - gameTime) * cycleTicks;
|
||||
const con = getEffectiveStat(world, entity, 'constitution');
|
||||
const conMultiplier = 1 - (con - 10) * 0.03;
|
||||
const energyNeeded = remainingDayTicks * energyDecay * conMultiplier;
|
||||
const napTarget = Math.min(ENERGY_THRESHOLD + energyNeeded + rc.get('NAP_BUFFER'), 100);
|
||||
|
||||
if (napTarget >= SLEEP_VOLUNTARY_ENERGY_THRESHOLD) {
|
||||
// Nap target is meaningful — wake immediately when reached
|
||||
if (needs.energy >= napTarget) {
|
||||
brain.currentGoal = null;
|
||||
if (eventMemoryService) {
|
||||
const name = world.getComponent<string>(entity, 'name') ?? 'Unknown';
|
||||
eventMemoryService.record(entity, {
|
||||
type: 'need_recovery', tick: 0, need: 'energy',
|
||||
detail: `${name} woke up from a nap`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Too close to nightfall — use normal full sleep wake logic
|
||||
if (needs.energy >= SLEEP_WAKE_THRESHOLD) {
|
||||
const wakeChance = (needs.energy - SLEEP_WAKE_THRESHOLD) / (100 - SLEEP_WAKE_THRESHOLD);
|
||||
if (Math.random() < wakeChance) {
|
||||
brain.currentGoal = null;
|
||||
if (eventMemoryService) {
|
||||
const name = world.getComponent<string>(entity, 'name') ?? 'Unknown';
|
||||
eventMemoryService.record(entity, {
|
||||
type: 'need_recovery', tick: 0, need: 'energy',
|
||||
detail: `${name} woke up feeling rested`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if (needs.energy >= SLEEP_WAKE_THRESHOLD) {
|
||||
// Nighttime or no rc: use normal probabilistic wake
|
||||
const wakeChance = (needs.energy - SLEEP_WAKE_THRESHOLD) / (100 - SLEEP_WAKE_THRESHOLD);
|
||||
if (Math.random() < wakeChance) {
|
||||
brain.currentGoal = null;
|
||||
if (eventMemoryService) {
|
||||
const name = world.getComponent<string>(entity, 'name') ?? 'Unknown';
|
||||
eventMemoryService.record(entity, {
|
||||
type: 'need_recovery', tick: 0, need: 'energy',
|
||||
detail: `${name} woke up feeling rested`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run tests**
|
||||
|
||||
Run: `npm -w server run test -- --run`
|
||||
Expected: All tests pass, including the 7 new nap tests.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/systems/npcBrainSystem.ts
|
||||
git commit -m "feat: implement daytime nap wake logic for circadian rhythm"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Verify and clean up
|
||||
|
||||
- [ ] **Step 1: Run full test suite**
|
||||
|
||||
Run: `npm -w server run test -- --run`
|
||||
Expected: All tests pass (existing + new).
|
||||
|
||||
- [ ] **Step 2: Build shared types**
|
||||
|
||||
Run: `npx -w shared tsc`
|
||||
Expected: Clean compilation.
|
||||
|
||||
- [ ] **Step 3: Build client**
|
||||
|
||||
Run: `npm -w client run build`
|
||||
Expected: Clean build, no errors.
|
||||
|
||||
- [ ] **Step 4: Final commit (if any cleanup needed)**
|
||||
|
||||
Only if previous steps revealed issues that needed fixing.
|
||||
Reference in New Issue
Block a user