diff --git a/docs/plans/2026-03-08-npc-inner-monologue-design.md b/docs/plans/2026-03-08-npc-inner-monologue-design.md new file mode 100644 index 0000000..06255df --- /dev/null +++ b/docs/plans/2026-03-08-npc-inner-monologue-design.md @@ -0,0 +1,136 @@ +# Task 1.4: NPC Inner Monologue / Thought Bubbles + +## Overview + +Periodically generate inner thoughts for NPCs based on their needs, relationships, and recent events. Display as italic first-person text in the NPC info panel and mood-contextual emoji bubbles floating above sprites. + +## Architecture + +### Data Flow + +1. `ThoughtSystem` runs each tick, tracks per-NPC cooldowns and a global generation timer +2. Every ~90s, collects followed NPCs + event-triggered NPCs into a batch +3. Sends one batched LLM request for up to 8 NPCs +4. Parses numbered responses, stores as `thought` component on each entity +5. Broadcasts thoughts to clients via `npc-thought` socket event +6. Client: info panel shows italic first-person text; sprite shows floating emoji + +### New ECS Component + +```typescript +interface Thought { + text: string; // First-person thought, e.g. "I could really use something to eat..." + emoji: string; // Mood emoji, e.g. "🍖" + tick: number; // When generated +} +``` + +### New Server Systems/Services + +- **`ThoughtSystem`** — per-tick system managing timing, cooldowns, batch collection +- **`ThoughtGenerator`** — batched LLM prompt builder + response parser (in `server/src/llm/`) + +### Triggers + +Each trigger respects a per-NPC cooldown of ~90s minimum. + +| Trigger | Condition | +|---------|-----------| +| Periodic | Every ~90s for followed NPCs (fallback if no event-driven thought recently) | +| Need critical | Hunger or energy below threshold | +| Post-interaction | Social outcome just fired | +| Relationship tier change | Classification changed | +| Idle | NPC wandering with nothing notable happening | + +### Batching Strategy + +- Collect up to 8 pending thought requests per cycle +- Single LLM call with numbered NPC contexts (name, personality, state, recent events) +- Parse numbered responses back to individual NPCs +- If parsing fails for a line, skip that NPC (no crash, no retry) +- Batch size, timer interval, and cooldown are configurable constants + +### Rate Budget + +- Target: ~20 thought requests/hour (leaving ~20/hour for backstories + narrations) +- Batch size 8 means 20 requests can serve ~160 individual thoughts/hour +- Per-NPC cooldown prevents any single NPC from dominating +- Daily budget: ~480 thought requests/day well within 1000/day limit shared across all features +- All timing constants configurable for tuning + +### Emoji Mapping + +Derived from thought context before LLM call: + +| Emoji | Condition | +|-------|-----------| +| 🍖 | Hungry (low hunger need) | +| 😴 | Tired (low energy need) | +| 😊 | Positive interaction / good mood | +| 😤 | Negative interaction / frustrated | +| 🤔 | Idle / pondering / curious personality | +| 💭 | Generic fallback | + +### Socket Events + +**New server → client event:** + +- `npc-thought` — `{ entityId: EntityId, text: string, emoji: string }` + +Sent to all clients when a thought is generated. Clients decide whether to display based on local follow state and visibility. + +### Client Display + +**Info Panel (NpcInfoPanel.ts):** +- New "Thought" section above Recent Events +- Italic first-person text, e.g. *"I could really use something to eat..."* +- Shows only the most recent thought, replaced when a new one arrives +- Hidden when no thought exists for this NPC + +**Map Sprite (GameScene.ts):** +- Small emoji rendered above NPC sprite head +- Fade-in over ~0.5s, hold ~4s, fade-out over ~0.5s +- Only one emoji at a time per NPC (new replaces old) + +### Prompt Template + +Batched template asking for multiple NPC thoughts in one request: + +``` +System: You write brief NPC inner thoughts in first person. One sentence each, grounded and specific. No purple prose. Respond with numbered lines matching the input. + +User: +Generate a brief inner thought for each NPC: + +1. Bjorn — Personality: SOC:6, EMP:7, TMP:13, CUR:11. State: very hungry, just argued with Helga (rival). +2. Helga — Personality: SOC:14, EMP:15, TMP:8, CUR:9. State: content, recently befriended Sven. +3. Sven — Personality: SOC:10, EMP:12, TMP:10, CUR:16. State: idle, wandering. +``` + +Expected response: +``` +1. My stomach is killing me... and that smug look on Helga's face isn't helping. +2. It's nice having someone like Sven around — someone who actually listens. +3. I wonder what's past those hills to the north... +``` + +### Priority Rules + +1. Followed NPCs always eligible for thoughts +2. Event-triggered thoughts (need critical, tier change) eligible regardless of follow status — but only if the NPC is followed (to conserve budget) +3. Non-followed NPCs do not generate thoughts (can revisit later) + +### Configuration + +All tuning values in a config object (similar to `relationshipConfig.ts`): + +```typescript +export const thoughtConfig = { + periodicIntervalTicks: number; // ~90s worth of ticks + perNpcCooldownTicks: number; // ~90s worth of ticks + maxBatchSize: number; // 8 + needCriticalThreshold: number; // e.g. 20 (out of 100) + emojiFadeDurationMs: number; // 500 + emojiHoldDurationMs: number; // 4000 +}; +```