161 lines
4.6 KiB
Markdown
161 lines
4.6 KiB
Markdown
# OpenSpec Change: enterprise-gaming-dialogue
|
|
|
|
- **Change ID**: enterprise-gaming-dialogue
|
|
- **Package**: `@suaveplan/dialogue`
|
|
- **Category**: gaming/narrative
|
|
- **Target path**: `gaming/narrative/dialogue`
|
|
- **Status**: PROPOSED
|
|
- **Date**: 2026-04-24
|
|
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/dialogue.md`
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Deliver `@suaveplan/dialogue` on the genesis branch from scratch at enterprise quality. The package provides branching dialogue trees with conditional choices, a bark system for ambient NPC speech, and conversation session management. A `DialogueTree` is a directed graph of `DialogueNode` objects; a `ConversationSession` advances through nodes given player choice input, evaluates `when` conditions against a context blackboard, and fires `@suaveplan/events` on node entry/exit. A `BarkSystem` selects one-shot NPC lines from tagged pools with cooldown deduplication. Genesis adds Zod node schemas, structured errors, OTel conversation counter, injectable clock, and 100% coverage.
|
|
|
|
---
|
|
|
|
## Current-state audit (genesis)
|
|
|
|
Package does not exist on genesis yet — fresh build.
|
|
|
|
---
|
|
|
|
## Available workspace deps on genesis
|
|
|
|
| Package | Role |
|
|
|---|---|
|
|
| `@suaveplan/error` (`workspace:^`) | `DialogueError`; `DIALOGUE.*` codes |
|
|
| `@suaveplan/types` (`workspace:^`) | `Clock` |
|
|
| `@suaveplan/events` (`workspace:^`) | Dialogue event bus |
|
|
| `zod` (`catalog:forms`) | Node + tree schemas |
|
|
| `@opentelemetry/api` (`catalog:observability`) | Conversation start/end counter |
|
|
|
|
---
|
|
|
|
## Public API surface
|
|
|
|
```ts
|
|
interface DialogueNode {
|
|
id: string
|
|
speaker?: string
|
|
text: string
|
|
choices?: DialogueChoice[]
|
|
action?: string // action name fired on node entry
|
|
isExit?: boolean
|
|
}
|
|
|
|
interface DialogueChoice {
|
|
id: string
|
|
text: string
|
|
nextNodeId: string
|
|
when?: string // expression evaluated against context; e.g. "gold >= 100"
|
|
hidden?: boolean // hide choice if condition fails (vs. show disabled)
|
|
}
|
|
|
|
function createDialogueTree(nodes: DialogueNode[], rootId: string): DialogueTree
|
|
|
|
interface DialogueTree {
|
|
getNode(id: string): DialogueNode | undefined
|
|
getChoices(nodeId: string, context: Record<string, unknown>): DialogueChoice[]
|
|
}
|
|
|
|
function createConversationSession(tree: DialogueTree, options?: SessionOptions): ConversationSession
|
|
|
|
interface SessionOptions {
|
|
context?: Record<string, unknown>
|
|
clock?: Clock
|
|
}
|
|
|
|
interface ConversationSession {
|
|
readonly currentNode: DialogueNode
|
|
readonly isComplete: boolean
|
|
choose(choiceId: string): DialogueNode
|
|
advance(): void // moves to next node if no choices
|
|
setContext(key: string, value: unknown): void
|
|
reset(): void
|
|
}
|
|
|
|
// Bark system
|
|
function createBarkSystem(options?: BarkOptions): BarkSystem
|
|
|
|
interface BarkOptions {
|
|
defaultCooldown?: number // seconds between same tag, default 10
|
|
clock?: Clock
|
|
}
|
|
|
|
interface BarkSystem {
|
|
registerBarks(tag: string, lines: string[]): void
|
|
bark(speakerId: string, tag: string, rng: Rng): string | null // null if on cooldown
|
|
clearCooldown(speakerId: string, tag?: string): void
|
|
}
|
|
```
|
|
|
|
### Error codes
|
|
|
|
```ts
|
|
type DialogueErrorCode =
|
|
| 'DIALOGUE_NODE_NOT_FOUND'
|
|
| 'DIALOGUE_CHOICE_NOT_FOUND'
|
|
| 'DIALOGUE_CONDITION_ERROR'
|
|
| 'DIALOGUE_TREE_INVALID'
|
|
| 'DIALOGUE_NO_LINES'
|
|
```
|
|
|
|
---
|
|
|
|
## New additions for genesis
|
|
|
|
1. **Condition evaluator** — `when` strings parsed as simple expressions (`gold >= 100`, `hasItem("key")`); safe no-eval evaluation via token parser.
|
|
2. **Hidden vs. disabled choices** — `hidden: true` removes choice from list; false shows it greyed.
|
|
3. **Bark cooldown** — per-speaker per-tag cooldown tracked with injectable clock.
|
|
|
|
---
|
|
|
|
## File layout
|
|
|
|
```text
|
|
gaming/narrative/dialogue/
|
|
package.json
|
|
tsconfig.json
|
|
vitest.config.ts
|
|
src/
|
|
index.ts
|
|
error-codes.ts
|
|
constants/index.ts
|
|
types/common.ts
|
|
types/common.md
|
|
core/
|
|
dialogue-tree/
|
|
dialogue-tree.ts
|
|
dialogue-tree.md
|
|
dialogue-tree.types.ts
|
|
dialogue-tree.test.ts
|
|
index.ts
|
|
conversation-session/
|
|
conversation-session.ts
|
|
conversation-session.md
|
|
conversation-session.types.ts
|
|
conversation-session.test.ts
|
|
index.ts
|
|
bark-system/
|
|
bark-system.ts
|
|
bark-system.md
|
|
bark-system.types.ts
|
|
bark-system.test.ts
|
|
index.ts
|
|
utils/
|
|
condition-evaluator.ts
|
|
condition-evaluator.md
|
|
condition-evaluator.test.ts
|
|
tests/
|
|
integration.test.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Impact
|
|
|
|
**Downstream**: `@suaveplan/react-dialogue` wraps session state in React hooks. Prerequisites: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`.
|