melissa kate
<< Doppelgänger

Engine Improvements — Pre-Phase 4

Issues found during testing + features that need fleshing out before the plugin system.

---

1. Bugs & Missing Engine Features

Choice-level discovery grants not applied

Choices can have grants_discoveries but the engine only applies event-level grants_discoveries in applyEventEffects(). When a player picks a choice, any discoveries on that specific choice should also be granted.

Fix: Engine.executeEvent needs to check which choice was picked and apply its discoveries.

No way to unlock conversations from story events

The bartender conversation is locked: true but nothing in the timeline can unlock it. Need a mechanism for events to unlock conversations — either via grants_discoveries (engine checks for unlock_conversation:bartender pattern) or a dedicated field on timeline events.

Proposed: Add unlocks_conversations: string[] to TimelineEvent, similar to grants_discoveries.

Conversation list doesn't reflect history messages in count

ConversationService.getConversationList() shows message count but when messages are added via addMessage(), the conversation list entry's lastMessage and messageCount should include those.

Status: Should already work since getMessages() combines scripted + dynamic + history. Verify.

Save/load doesn't restore conversation view state

If you're in a conversation, save, reload, and load — you're back at the story view. The current view (story/conversations/chat) isn't part of game state. This is arguably correct (UI state ≠ game state), but worth noting.

Command resolution should handle choice grants

When a choice has grants_discoveries AND leads_to, the discoveries from the choice itself need to be applied before executing the target event.

---

2. Character System — Needs Fleshing Out

The current CharacterProfile is minimal. Based on the Python NPC profiles and what real games need, here's what the full character system should look like:

2a. Visual Identity

interface CharacterVisuals {
  /** Primary avatar image — shown in conversation lists, dialogue headers */
  avatar?: string;
  /** Portrait image — larger, shown during dialogue scenes */
  portrait?: string;
  /** Map of expression/mood to image path — skins can use these */
  expressions?: Record<string, string>;
  // e.g., { happy: '/chars/maya/happy.png', angry: '/chars/maya/angry.png' }
  /** Color associated with this character — for UI theming */
  accentColor?: string;
}

This ties into the DialogueMetadata.mood and DialogueMetadata.expression fields — the UI can look up the right image: character.visuals.expressions[dialogue.mood].

2b. Conversation Behavior

interface ConversationBehavior {
  /** How long should LLM responses be? */
  responseLength: 'brief' | 'moderate' | 'verbose';
  // brief = 1-2 sentences (texting style, like "Unavailable" who speaks in 2 words)
  // moderate = 2-4 sentences (normal conversation)  
  // verbose = paragraph+ (storytellers, expositors like Jane Levitan)

  /** Max tokens for this character's LLM responses (overrides engine default) */
  maxTokens?: number;

  /** Temperature for this character (lower = more predictable/formal) */
  temperature?: number;

  /** Does this character use emojis? */
  usesEmojis: boolean;

  /** Does this character use text abbreviations (lol, r u, etc)? */
  usesAbbreviations: boolean;

  /** How quickly does this character "respond"? (for simulated typing delay) */
  responseDelay?: 'instant' | 'fast' | 'normal' | 'slow';
}

This solves the "how long should responses be" question at the character level. Jane Levitan writes formal paragraphs. The Unavailable number sends 2-word messages. Maya texts in bursts. Each character's behavior config tells the LLM and the UI how to handle them.

2c. Story Context (dynamic, changes during gameplay)

interface CharacterStoryContext {
  /** Current emotional state — can change based on discoveries */
  currentMood: string;

  /** What does this character know? Affects what they'll reveal */
  knowledgeFlags: string[];
  // e.g., ['knows_about_doppelganger', 'knows_player_was_missing']

  /** How truthful is this character? 0 = lies freely, 1 = always honest */
  truthfulness: number;

  /** Topics this character will NOT discuss (LLM instruction) */
  forbiddenTopics?: string[];

  /** Topics this character WANTS to bring up (LLM instruction) */
  agendaTopics?: string[];
}

The tricky part: storyContext needs to be dynamic — it should change based on game state. For example, after the player discovers doppelganger_exists, Maya's mood should shift from "worried" to "terrified".

Proposed: Story context can include conditional overrides:

interface ConditionalContext {
  condition: Condition;  // same all/any/none system
  overrides: Partial<CharacterStoryContext>;
}

So a character definition could say:

{
  "storyContext": {
    "currentMood": "worried",
    "truthfulness": 0.9
  },
  "contextOverrides": [
    {
      "condition": { "all": ["doppelganger_confirmed"] },
      "overrides": { "currentMood": "terrified", "truthfulness": 1.0 }
    }
  ]
}

2d. Conversation Triggers

From the Python code — certain player messages trigger scripted responses + discovery grants. This is the hybrid model: mostly LLM, but specific keywords/topics fire scripted replies that advance the story.

interface ConversationTrigger {
  id: string;
  /** Conditions that must be met for this trigger to be active */
  conditions: Condition;
  /** Keywords/patterns that activate this trigger */
  triggerPatterns: string[];
  /** The scripted response to send instead of LLM */
  scriptedResponse: string;
  /** Discoveries granted when this trigger fires */
  grantsDiscoveries?: string[];
  /** Conversations to unlock */
  unlocksConversations?: string[];
}

This is powerful: LLM handles the freeform conversation, but when the player says something that matches a story-critical moment, the engine intercepts and delivers a scripted response that advances the plot.

2e. Social Media Profiles

Characters can have social media presence (Spacebook in Doppelganger). This is game-specific but common enough to be in the engine:

interface SocialProfile {
  platform: string;  // 'spacebook', 'twitter', etc — game-defined
  username: string;
  bio: string;
  location?: string;
}

2f. Revised Full CharacterProfile

interface CharacterProfile {
  id: string;
  name: string;

  // Visual identity
  visuals: CharacterVisuals;

  // Who they are
  personality: CharacterPersonality;   // existing: summary, traits, writingStyle, etc.
  background: CharacterBackground;     // existing: description, history, relationship

  // How they communicate
  behavior: ConversationBehavior;

  // Story state (can change during gameplay)
  storyContext: CharacterStoryContext;
  contextOverrides?: ConditionalContext[];

  // Story-critical conversation moments
  conversationTriggers?: ConversationTrigger[];

  // Social media (optional, game-specific)
  socialProfiles?: SocialProfile[];
}

---

3. Engine Features Needed

Event-driven conversation unlocks

Timeline events should be able to unlock conversations:

{
  "id": "REACH_BAR",
  "grants_discoveries": ["reached_bar"],
  "unlocks_conversations": ["bartender"]
}

Event-driven mood/context changes

Timeline events should be able to change character mood:

{
  "id": "DOPPELGANGER_REVEAL",
  "character_effects": {
    "maya": { "currentMood": "terrified" },
    "devin": { "currentMood": "suspicious" }
  }
}

Content validator CLI

A tool that validates a GameDefinition before running:

Typing/response delay simulation

For immersion, character responses should feel like they take time:

---

4. Things That Work Well (Don't Break These)

---

5. Priority Order

  1. Choice-level discovery grants — bug, blocks gameplay
  2. Conversation unlocks from events — needed for real games
  3. Character visuals (avatar, portrait, expressions) — needed for dialogue UI
  4. Conversation behavior (response length, tokens, delay) — needed for LLM quality
  5. Story context + conditional overrides — needed for dynamic characters
  6. Conversation triggers — needed for hybrid scripted/LLM conversations
  7. Content validator — quality of life for creators
  8. Character effects on events — nice to have