diff --git a/docs/plans/2026-03-10-day-counter-indicator-plan.md b/docs/plans/2026-03-10-day-counter-indicator-plan.md new file mode 100644 index 0000000..ad47faf --- /dev/null +++ b/docs/plans/2026-03-10-day-counter-indicator-plan.md @@ -0,0 +1,339 @@ +# Day Counter + 2x Indicator Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add a "Day N" counter to the left of the day/night indicator and double the indicator's size. + +**Architecture:** Server computes `dayNumber` centrally in GameLoop, broadcasts it in StateUpdate. Client TimeIndicator renders it. Existing inline day calculation in inventionSystem replaced with centralized method. + +**Tech Stack:** TypeScript, Socket.io, HTML/CSS DOM (TimeIndicator), shared types + +--- + +### Task 1: Add `dayNumber` to StateUpdate type + +**Files:** +- Modify: `shared/src/types.ts:178-182` + +**Step 1: Add dayNumber field to StateUpdate** + +In `shared/src/types.ts`, add `dayNumber` to the `StateUpdate` interface: + +```typescript +export interface StateUpdate { + entities: EntityState[]; + tick: number; + gameTime: number; // 0.0-1.0 normalized position in day/night cycle + dayNumber: number; // 1-indexed day counter +} +``` + +**Step 2: Rebuild shared types** + +Run: `npx -w shared tsc` +Expected: Clean compile, no errors + +**Step 3: Commit** + +```bash +git add shared/src/types.ts +git commit -m "feat: add dayNumber to StateUpdate type" +``` + +--- + +### Task 2: Add getCycleTicks() and getDayNumber() to GameLoop + +**Files:** +- Modify: `server/src/game/GameLoop.ts:342-347` + +**Step 1: Add getCycleTicks() private helper and getDayNumber() method** + +Replace the existing `getGameTime()` method and add new methods after it in `server/src/game/GameLoop.ts`: + +```typescript + private getCycleTicks(): number { + const dayTicks = 100 / this.runtimeConstants.get('ENERGY_DECAY_PER_TICK'); + const nightTicks = dayTicks / this.runtimeConstants.get('DAY_NIGHT_RATIO'); + return Math.round(dayTicks + nightTicks); + } + + getGameTime(): number { + const cycleTicks = this.getCycleTicks(); + return (this.tick % cycleTicks) / cycleTicks; + } + + getDayNumber(): number { + return Math.floor(this.tick / this.getCycleTicks()) + 1; + } +``` + +**Step 2: Verify server compiles** + +Run: `npx -w server tsc --noEmit` +Expected: Clean compile + +**Step 3: Commit** + +```bash +git add server/src/game/GameLoop.ts +git commit -m "feat: add getCycleTicks() and getDayNumber() to GameLoop" +``` + +--- + +### Task 3: Include dayNumber in state broadcast + +**Files:** +- Modify: `server/src/network/stateSerializer.ts:100-102` +- Modify: `server/src/network/SocketServer.ts:36` + +**Step 1: Update serializeStateUpdate to accept and include dayNumber** + +In `server/src/network/stateSerializer.ts`, change the function signature and return value: + +```typescript +export function serializeStateUpdate(world: World, tick: number, gameTime: number, dayNumber: number): StateUpdate { + const entities = world.query('position').map(id => serializeEntity(world, id)); + return { entities, tick, gameTime, dayNumber }; +} +``` + +**Step 2: Update SocketServer broadcast call** + +In `server/src/network/SocketServer.ts` line 36, add `getDayNumber()`: + +```typescript +const update = serializeStateUpdate(this.gameLoop.world, this.gameLoop.getTick(), this.gameLoop.getGameTime(), this.gameLoop.getDayNumber()); +``` + +**Step 3: Verify server compiles** + +Run: `npx -w server tsc --noEmit` +Expected: Clean compile + +**Step 4: Commit** + +```bash +git add server/src/network/stateSerializer.ts server/src/network/SocketServer.ts +git commit -m "feat: broadcast dayNumber in state updates" +``` + +--- + +### Task 4: Replace inline day calculation in inventionSystem + +**Files:** +- Modify: `server/src/systems/inventionSystem.ts:14,21-26,148-152` + +**Step 1: Pass getDayNumber callback into createInventionSystem** + +Change the factory function signature to accept a `getDayNumber` callback: + +```typescript +export function createInventionSystem( + llmService: LlmService, + narrationService: NarrationService, + eventMemoryService: EventMemoryService, + onInventionCreated?: (summary: InventionSummary) => void, + getDayNumber?: () => number, +): InventionSystem { +``` + +**Step 2: Replace inline day calculation** + +Replace lines 148-152 (the inline day calculation): + +```typescript + // Compute current day + const dayTicks = 100 / ENERGY_DECAY_PER_TICK; + const nightTicks = dayTicks / DAY_NIGHT_RATIO; + const cycleTicks = Math.round(dayTicks + nightTicks); + const day = Math.floor(tick / cycleTicks) + 1; +``` + +With: + +```typescript + const day = getDayNumber ? getDayNumber() : 1; +``` + +**Step 3: Remove unused imports** + +Remove `ENERGY_DECAY_PER_TICK` and `DAY_NIGHT_RATIO` from the import on line 14 (if no longer used elsewhere in file): + +```typescript +// Remove this line: +import { ENERGY_DECAY_PER_TICK, DAY_NIGHT_RATIO } from '@dflike/shared'; +``` + +**Step 4: Update GameLoop to pass getDayNumber** + +In `server/src/game/GameLoop.ts` around line 75, update the `createInventionSystem` call: + +```typescript + this.inventionSystem = createInventionSystem( + this.llmService, + this.narrationService, + this.eventMemoryService, + (summary) => this.onInventionCreated?.(summary), + () => this.getDayNumber(), + ); +``` + +**Step 5: Run tests** + +Run: `npm -w server run test` +Expected: All 408 tests pass + +**Step 6: Commit** + +```bash +git add server/src/systems/inventionSystem.ts server/src/game/GameLoop.ts +git commit -m "refactor: use centralized getDayNumber() in inventionSystem" +``` + +--- + +### Task 5: Double TimeIndicator dimensions and add day counter + +**Files:** +- Modify: `client/src/ui/TimeIndicator.ts` (full file) + +**Step 1: Add dayLabel field and update constructor** + +Add a `dayLabel` property and a new DOM element for it. Update all dimensions to 2x. The full updated constructor and fields: + +Add new field: +```typescript + private dayLabel: HTMLDivElement; + private lastDayNumber = 0; +``` + +In the constructor, update these CSS values: +- Container gap: `5px` → `10px` +- Sun icon font-size: `10px` → `20px`, text-shadow blur `4px` → `8px` +- Track height: `8px` → `16px`, border: `2px` → `4px`, box-shadow blur `6px` → `12px` +- Day section width: `dayFraction * 120` → `dayFraction * 240` +- Divider width: `1px` → `2px` +- Night section width: `(1 - dayFraction) * 120` → `(1 - dayFraction) * 240` +- Marker width: `3px` → `6px`, top: `-1px` → `-2px`, height calc: `calc(100% + 2px)` → `calc(100% + 4px)`, box-shadow blur `4px` → `8px` +- Moon icon font-size: `9px` → `18px`, text-shadow blur `4px` → `8px` + +Add day label element before appending children: + +```typescript + // Day counter label + this.dayLabel = document.createElement('div'); + this.dayLabel.style.cssText = ` + font-size: 10px; + color: #e0d0b0; + text-shadow: 0 0 4px #00000088; + line-height: 1; + white-space: nowrap; + `; + this.dayLabel.textContent = 'Day 1'; +``` + +Update container children order — day label first: + +```typescript + this.container.appendChild(this.dayLabel); + this.container.appendChild(this.sunIcon); + this.container.appendChild(track); + this.container.appendChild(this.moonIcon); +``` + +**Step 2: Update the update() method** + +Change signature to accept dayNumber and update the track width constant: + +```typescript + update(gameTime: number, dayNumber: number): void { + // Update day label when day changes + if (dayNumber !== this.lastDayNumber) { + this.lastDayNumber = dayNumber; + this.dayLabel.textContent = `Day ${dayNumber}`; + } + + const key = Math.round(gameTime * 200).toString(); + if (key === this.lastTimeKey) return; + this.lastTimeKey = key; + + const totalWidth = 240 + 2; // 240px sections + 2px divider + const px = gameTime * totalWidth; + // ... rest unchanged +``` + +**Step 3: Commit** + +```bash +git add client/src/ui/TimeIndicator.ts +git commit -m "feat: double TimeIndicator size and add day counter" +``` + +--- + +### Task 6: Pass dayNumber from GameScene to TimeIndicator + +**Files:** +- Modify: `client/src/scenes/GameScene.ts` + +**Step 1: Add dayNumber tracking field** + +Find where `targetGameTime` and `currentGameTime` are declared and add: + +```typescript + private currentDayNumber = 1; +``` + +**Step 2: Store dayNumber from state updates** + +In `handleStateUpdate()` (around line 674-675), add: + +```typescript + private handleStateUpdate(update: StateUpdate): void { + this.targetGameTime = update.gameTime; + this.currentDayNumber = update.dayNumber; +``` + +**Step 3: Pass dayNumber to TimeIndicator.update()** + +In the `update()` method (around line 799), change: + +```typescript + this.timeIndicator.update(this.currentGameTime, this.currentDayNumber); +``` + +**Step 4: Verify client builds** + +Run: `npm -w client run build` +Expected: Clean build + +**Step 5: Commit** + +```bash +git add client/src/scenes/GameScene.ts +git commit -m "feat: pass dayNumber from state updates to TimeIndicator" +``` + +--- + +### Task 7: Manual verification + +**Step 1: Start server and client** + +Run server: `npm -w server run dev` +Run client: `npm -w client run dev` + +**Step 2: Verify in browser** + +- Day/night indicator should be approximately 2x larger +- "Day N" text should appear to the left of the sun icon +- Day counter should increment when the day/night cycle completes +- Marker should still move smoothly across the track + +**Step 3: Run all server tests** + +Run: `npm -w server run test` +Expected: All tests pass