snapkitty
hardware
erlang
File size: 4,285 Bytes
b8a03fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { clamp } from './deterministic.js';
import { emitWorldEvent, findRequired, trimHistory } from './state-utils.js';
import type { CommandResult, DialogueSessionState, EntityId, UniverseState } from './types.js';

export class DialogueSystem {
  constructor(private state: UniverseState) {}

  replaceState(state: UniverseState): void {
    this.state = state;
  }

  start(npcId: EntityId): CommandResult {
    const npc = findRequired(this.state.npcs, npcId, 'NPC');
    if (!npc.alive) return this.failure(`${npc.name} cannot converse.`);
    if (npc.currentLocationId !== this.state.player.locationId) return this.failure(`${npc.name} is not in the player's location.`);
    const firstNode = this.state.dialogue.nodes.find((node) => node.id === npc.dialogueStateId);
    if (!firstNode) return this.failure(`${npc.name} has no reachable dialogue state.`);
    const session: DialogueSessionState = {
      id: `dialogue-session-${npc.id}-${this.state.tick}-${this.state.dialogue.sessions.length + 1}`,
      npcId: npc.id,
      currentNodeId: firstNode.id,
      visitedNodeIds: [firstNode.id],
      active: true,
    };
    this.state.dialogue.sessions.push(session);
    trimHistory(this.state.dialogue.sessions, 24);
    const event = emitWorldEvent(this.state, 'dialogue-started', this.state.player.id, npc.id, `Conversation started with ${npc.name}.`, { persistent: false, data: { sessionId: session.id } });
    return { ok: true, message: firstNode.text, eventIds: [event.id] };
  }

  getSession(sessionId: EntityId): DialogueSessionState {
    return findRequired(this.state.dialogue.sessions, sessionId, 'Dialogue session');
  }

  availableChoices(sessionId: EntityId) {
    const session = this.getSession(sessionId);
    const npc = findRequired(this.state.npcs, session.npcId, 'NPC');
    const node = findRequired(this.state.dialogue.nodes, session.currentNodeId, 'Dialogue node');
    const trust = npc.trustValues[this.state.player.id] ?? 0;
    return node.choices.filter((choice) => trust >= choice.requiredTrust);
  }

  choose(sessionId: EntityId, choiceId: EntityId): CommandResult {
    const session = this.getSession(sessionId);
    if (!session.active) return this.failure('Dialogue session is closed.');
    const npc = findRequired(this.state.npcs, session.npcId, 'NPC');
    const node = findRequired(this.state.dialogue.nodes, session.currentNodeId, 'Dialogue node');
    const choice = node.choices.find((candidate) => candidate.id === choiceId);
    if (!choice) return this.failure(`Choice '${choiceId}' is unavailable at this dialogue node.`);
    const trust = npc.trustValues[this.state.player.id] ?? 0;
    if (trust < choice.requiredTrust) return this.failure(`${npc.name} does not trust the player enough for that response.`);
    const nextTrust = clamp(trust + choice.trustDelta, -100, 100);
    npc.trustValues[this.state.player.id] = nextTrust;
    npc.playerReaction = nextTrust <= -60 ? 'hostile' : nextTrust < -20 ? 'wary' : nextTrust >= 70 ? 'loyal' : nextTrust >= 25 ? 'friendly' : 'neutral';
    npc.memories.push({
      id: `memory-${npc.id}-dialogue-${this.state.tick}-${npc.memories.length + 1}`,
      tick: this.state.tick,
      subjectId: this.state.player.id,
      summary: `Player said: ${choice.text}`,
      emotionalWeight: clamp(choice.trustDelta / 10, -1, 1),
      confidence: 1,
      tags: ['dialogue', choice.eventType ?? 'conversation'],
    });
    trimHistory(npc.memories, 64);
    if (choice.nextNodeId) {
      const next = findRequired(this.state.dialogue.nodes, choice.nextNodeId, 'Next dialogue node');
      session.currentNodeId = next.id;
      session.visitedNodeIds.push(next.id);
    } else {
      session.active = false;
    }
    const event = emitWorldEvent(this.state, choice.eventType ?? 'dialogue-choice', this.state.player.id, npc.id, `${npc.name} responded to '${choice.text}'.`, { data: { choiceId, trustDelta: choice.trustDelta, sessionId } });
    const responseText = choice.nextNodeId ? findRequired(this.state.dialogue.nodes, choice.nextNodeId, 'Dialogue node').text : 'Conversation ended.';
    return { ok: true, message: responseText, eventIds: [event.id] };
  }

  private failure(message: string): CommandResult {
    return { ok: false, message, eventIds: [] };
  }
}