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:
- All
leads_totargets exist - All
speakerIDs in dialogue metadata reference valid characters - All
unlocks_conversationsreference valid conversation IDs - Required fields present
- Discovery references are consistent
- No orphaned events (unreachable from start)
Typing/response delay simulation
For immersion, character responses should feel like they take time:
- Brief delay before NPC "types"
- Configurable per character via
behavior.responseDelay - UI shows typing indicator during delay
---
4. Things That Work Well (Don't Break These)
- Immutable state model — every operation returns new state
- Condition evaluator — single function for all/any/none, used everywhere
- Dialogue metadata —
speaker+moodon events is clean - LLM provider abstraction — swap providers without touching game code
- Save/load round-trip — game state is fully serializable
- Choice-based branching — works naturally through timeline events
---
5. Priority Order
- Choice-level discovery grants — bug, blocks gameplay
- Conversation unlocks from events — needed for real games
- Character visuals (avatar, portrait, expressions) — needed for dialogue UI
- Conversation behavior (response length, tokens, delay) — needed for LLM quality
- Story context + conditional overrides — needed for dynamic characters
- Conversation triggers — needed for hybrid scripted/LLM conversations
- Content validator — quality of life for creators
- Character effects on events — nice to have