run-dmcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +79 -0
- package/dist/__tests__/engineVocabulary.test.d.ts +1 -0
- package/dist/__tests__/engineVocabulary.test.js +147 -0
- package/dist/db/__tests__/connection.test.d.ts +1 -0
- package/dist/db/__tests__/connection.test.js +72 -0
- package/dist/db/__tests__/testDb.d.ts +33 -0
- package/dist/db/__tests__/testDb.js +41 -0
- package/dist/db/connection.d.ts +22 -0
- package/dist/db/connection.js +107 -0
- package/dist/db/schema.d.ts +1 -0
- package/dist/db/schema.js +725 -0
- package/dist/events/emitter.d.ts +22 -0
- package/dist/events/emitter.js +71 -0
- package/dist/http/server.d.ts +3 -0
- package/dist/http/server.js +649 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +92 -0
- package/dist/register/abilities.d.ts +2 -0
- package/dist/register/abilities.js +165 -0
- package/dist/register/audio.d.ts +2 -0
- package/dist/register/audio.js +326 -0
- package/dist/register/batch.d.ts +2 -0
- package/dist/register/batch.js +343 -0
- package/dist/register/character.d.ts +2 -0
- package/dist/register/character.js +324 -0
- package/dist/register/combat.d.ts +2 -0
- package/dist/register/combat.js +207 -0
- package/dist/register/core.d.ts +2 -0
- package/dist/register/core.js +1040 -0
- package/dist/register/display.d.ts +2 -0
- package/dist/register/display.js +263 -0
- package/dist/register/factions.d.ts +2 -0
- package/dist/register/factions.js +186 -0
- package/dist/register/images.d.ts +2 -0
- package/dist/register/images.js +400 -0
- package/dist/register/inventory.d.ts +2 -0
- package/dist/register/inventory.js +115 -0
- package/dist/register/mcp-prompts.d.ts +2 -0
- package/dist/register/mcp-prompts.js +684 -0
- package/dist/register/mcp-resources.d.ts +2 -0
- package/dist/register/mcp-resources.js +335 -0
- package/dist/register/narrative.d.ts +2 -0
- package/dist/register/narrative.js +242 -0
- package/dist/register/notes.d.ts +2 -0
- package/dist/register/notes.js +170 -0
- package/dist/register/pause.d.ts +2 -0
- package/dist/register/pause.js +580 -0
- package/dist/register/quests.d.ts +2 -0
- package/dist/register/quests.js +118 -0
- package/dist/register/relationships.d.ts +2 -0
- package/dist/register/relationships.js +147 -0
- package/dist/register/resources.d.ts +2 -0
- package/dist/register/resources.js +277 -0
- package/dist/register/secrets.d.ts +2 -0
- package/dist/register/secrets.js +192 -0
- package/dist/register/status.d.ts +2 -0
- package/dist/register/status.js +130 -0
- package/dist/register/tables.d.ts +2 -0
- package/dist/register/tables.js +146 -0
- package/dist/register/tags.d.ts +2 -0
- package/dist/register/tags.js +114 -0
- package/dist/register/time.d.ts +2 -0
- package/dist/register/time.js +281 -0
- package/dist/register/world.d.ts +2 -0
- package/dist/register/world.js +127 -0
- package/dist/schemas/index.d.ts +921 -0
- package/dist/schemas/index.js +121 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +13 -0
- package/dist/tools/__tests__/audio.test.d.ts +1 -0
- package/dist/tools/__tests__/audio.test.js +59 -0
- package/dist/tools/__tests__/conserved.test.d.ts +1 -0
- package/dist/tools/__tests__/conserved.test.js +488 -0
- package/dist/tools/__tests__/constraint.test.d.ts +1 -0
- package/dist/tools/__tests__/constraint.test.js +212 -0
- package/dist/tools/__tests__/expiry-consequences.test.d.ts +1 -0
- package/dist/tools/__tests__/expiry-consequences.test.js +110 -0
- package/dist/tools/__tests__/images.test.d.ts +1 -0
- package/dist/tools/__tests__/images.test.js +59 -0
- package/dist/tools/__tests__/relationship.test.d.ts +1 -0
- package/dist/tools/__tests__/relationship.test.js +132 -0
- package/dist/tools/__tests__/resource-constraints.test.d.ts +1 -0
- package/dist/tools/__tests__/resource-constraints.test.js +131 -0
- package/dist/tools/__tests__/resource.test.d.ts +1 -0
- package/dist/tools/__tests__/resource.test.js +190 -0
- package/dist/tools/__tests__/time.test.d.ts +1 -0
- package/dist/tools/__tests__/time.test.js +404 -0
- package/dist/tools/__tests__/timers.test.d.ts +1 -0
- package/dist/tools/__tests__/timers.test.js +426 -0
- package/dist/tools/__tests__/world.test.d.ts +1 -0
- package/dist/tools/__tests__/world.test.js +70 -0
- package/dist/tools/ability.d.ts +48 -0
- package/dist/tools/ability.js +238 -0
- package/dist/tools/audio.d.ts +24 -0
- package/dist/tools/audio.js +365 -0
- package/dist/tools/character.d.ts +70 -0
- package/dist/tools/character.js +309 -0
- package/dist/tools/combat.d.ts +13 -0
- package/dist/tools/combat.js +195 -0
- package/dist/tools/constraint.d.ts +132 -0
- package/dist/tools/constraint.js +269 -0
- package/dist/tools/dice.d.ts +23 -0
- package/dist/tools/dice.js +111 -0
- package/dist/tools/display.d.ts +120 -0
- package/dist/tools/display.js +528 -0
- package/dist/tools/faction.d.ts +61 -0
- package/dist/tools/faction.js +269 -0
- package/dist/tools/game.d.ts +96 -0
- package/dist/tools/game.js +526 -0
- package/dist/tools/image-prompt.d.ts +49 -0
- package/dist/tools/image-prompt.js +479 -0
- package/dist/tools/images.d.ts +47 -0
- package/dist/tools/images.js +449 -0
- package/dist/tools/inventory.d.ts +20 -0
- package/dist/tools/inventory.js +145 -0
- package/dist/tools/narrative.d.ts +58 -0
- package/dist/tools/narrative.js +237 -0
- package/dist/tools/notes.d.ts +41 -0
- package/dist/tools/notes.js +220 -0
- package/dist/tools/pause.d.ts +110 -0
- package/dist/tools/pause.js +1254 -0
- package/dist/tools/quest.d.ts +34 -0
- package/dist/tools/quest.js +164 -0
- package/dist/tools/relationship.d.ts +74 -0
- package/dist/tools/relationship.js +324 -0
- package/dist/tools/resource.d.ts +93 -0
- package/dist/tools/resource.js +374 -0
- package/dist/tools/rules.d.ts +4 -0
- package/dist/tools/rules.js +30 -0
- package/dist/tools/secrets.d.ts +49 -0
- package/dist/tools/secrets.js +195 -0
- package/dist/tools/status.d.ts +36 -0
- package/dist/tools/status.js +218 -0
- package/dist/tools/tables.d.ts +33 -0
- package/dist/tools/tables.js +209 -0
- package/dist/tools/tags.d.ts +52 -0
- package/dist/tools/tags.js +176 -0
- package/dist/tools/time.d.ts +33 -0
- package/dist/tools/time.js +276 -0
- package/dist/tools/timers.d.ts +41 -0
- package/dist/tools/timers.js +215 -0
- package/dist/tools/world.d.ts +78 -0
- package/dist/tools/world.js +331 -0
- package/dist/types/index.d.ts +969 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/__tests__/json.test.d.ts +1 -0
- package/dist/utils/__tests__/json.test.js +55 -0
- package/dist/utils/__tests__/validation.test.d.ts +1 -0
- package/dist/utils/__tests__/validation.test.js +90 -0
- package/dist/utils/errors.d.ts +44 -0
- package/dist/utils/errors.js +121 -0
- package/dist/utils/json.d.ts +9 -0
- package/dist/utils/json.js +23 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.js +50 -0
- package/dist/utils/output-schemas.d.ts +594 -0
- package/dist/utils/output-schemas.js +331 -0
- package/dist/utils/tool-annotations.d.ts +147 -0
- package/dist/utils/tool-annotations.js +98 -0
- package/dist/utils/validation.d.ts +34 -0
- package/dist/utils/validation.js +52 -0
- package/dist/utils/verbosity.d.ts +57 -0
- package/dist/utils/verbosity.js +67 -0
- package/dist/utils/webui.d.ts +20 -0
- package/dist/utils/webui.js +35 -0
- package/package.json +75 -0
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import { getDatabase } from "../db/connection.js";
|
|
3
|
+
import { safeJsonParse } from "../utils/json.js";
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// PAUSE PREPARATION
|
|
6
|
+
// ============================================================================
|
|
7
|
+
/**
|
|
8
|
+
* Prepares for a game pause by returning current state, a comprehensive
|
|
9
|
+
* game state audit, persistence reminders, and a checklist of context
|
|
10
|
+
* that should be saved. This helps agents understand what needs to be
|
|
11
|
+
* captured before ending a game.
|
|
12
|
+
*/
|
|
13
|
+
export function preparePause(gameId) {
|
|
14
|
+
const db = getDatabase();
|
|
15
|
+
// Get game
|
|
16
|
+
const gameRow = db
|
|
17
|
+
.prepare(`SELECT * FROM games WHERE id = ?`)
|
|
18
|
+
.get(gameId);
|
|
19
|
+
if (!gameRow)
|
|
20
|
+
return null;
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// COMPREHENSIVE GAME STATE AUDIT
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Characters
|
|
25
|
+
const characterStats = db
|
|
26
|
+
.prepare(`
|
|
27
|
+
SELECT
|
|
28
|
+
COUNT(*) as total,
|
|
29
|
+
SUM(CASE WHEN is_player = 1 THEN 1 ELSE 0 END) as players,
|
|
30
|
+
SUM(CASE WHEN is_player = 0 THEN 1 ELSE 0 END) as npcs,
|
|
31
|
+
SUM(CASE WHEN notes IS NOT NULL AND notes != '' THEN 1 ELSE 0 END) as with_notes
|
|
32
|
+
FROM characters WHERE game_id = ?
|
|
33
|
+
`)
|
|
34
|
+
.get(gameId);
|
|
35
|
+
// Characters with conditions
|
|
36
|
+
const charactersWithConditions = db
|
|
37
|
+
.prepare(`
|
|
38
|
+
SELECT COUNT(DISTINCT c.id) as count
|
|
39
|
+
FROM characters c
|
|
40
|
+
JOIN status_effects se ON se.target_id = c.id
|
|
41
|
+
WHERE c.game_id = ?
|
|
42
|
+
`)
|
|
43
|
+
.get(gameId);
|
|
44
|
+
// Locations
|
|
45
|
+
const locationStats = db
|
|
46
|
+
.prepare(`SELECT COUNT(*) as total FROM locations WHERE game_id = ?`)
|
|
47
|
+
.get(gameId);
|
|
48
|
+
// Count locations that have exits (exits are stored in properties JSON)
|
|
49
|
+
const locationRows = db
|
|
50
|
+
.prepare(`SELECT properties FROM locations WHERE game_id = ?`)
|
|
51
|
+
.all(gameId);
|
|
52
|
+
let connectedCount = 0;
|
|
53
|
+
for (const row of locationRows) {
|
|
54
|
+
const props = safeJsonParse(row.properties, { exits: [] });
|
|
55
|
+
if (props.exits && props.exits.length > 0) {
|
|
56
|
+
connectedCount++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const connectedLocations = { count: connectedCount };
|
|
60
|
+
// Quests
|
|
61
|
+
const questStats = db
|
|
62
|
+
.prepare(`
|
|
63
|
+
SELECT
|
|
64
|
+
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
|
|
65
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
|
66
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
|
|
67
|
+
FROM quests WHERE game_id = ?
|
|
68
|
+
`)
|
|
69
|
+
.get(gameId);
|
|
70
|
+
// Items
|
|
71
|
+
const itemStats = db
|
|
72
|
+
.prepare(`
|
|
73
|
+
SELECT
|
|
74
|
+
COUNT(*) as total,
|
|
75
|
+
SUM(CASE WHEN owner_type = 'character' THEN 1 ELSE 0 END) as in_inventories,
|
|
76
|
+
SUM(CASE WHEN owner_type = 'location' THEN 1 ELSE 0 END) as in_locations
|
|
77
|
+
FROM items WHERE game_id = ?
|
|
78
|
+
`)
|
|
79
|
+
.get(gameId);
|
|
80
|
+
// Combat
|
|
81
|
+
const combatRow = db
|
|
82
|
+
.prepare(`SELECT * FROM combats WHERE game_id = ? AND status = 'active' LIMIT 1`)
|
|
83
|
+
.get(gameId);
|
|
84
|
+
// Resources
|
|
85
|
+
const resourceStats = db
|
|
86
|
+
.prepare(`
|
|
87
|
+
SELECT
|
|
88
|
+
SUM(CASE WHEN owner_type = 'game' THEN 1 ELSE 0 END) as game_level,
|
|
89
|
+
SUM(CASE WHEN owner_type = 'character' THEN 1 ELSE 0 END) as character_level
|
|
90
|
+
FROM resources WHERE game_id = ?
|
|
91
|
+
`)
|
|
92
|
+
.get(gameId);
|
|
93
|
+
// Timers
|
|
94
|
+
const timerStats = db
|
|
95
|
+
.prepare(`
|
|
96
|
+
SELECT
|
|
97
|
+
SUM(CASE WHEN triggered = 0 THEN 1 ELSE 0 END) as active,
|
|
98
|
+
SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered
|
|
99
|
+
FROM timers WHERE game_id = ?
|
|
100
|
+
`)
|
|
101
|
+
.get(gameId);
|
|
102
|
+
// Scheduled events
|
|
103
|
+
const eventStats = db
|
|
104
|
+
.prepare(`
|
|
105
|
+
SELECT
|
|
106
|
+
SUM(CASE WHEN triggered = 0 THEN 1 ELSE 0 END) as pending,
|
|
107
|
+
SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered
|
|
108
|
+
FROM scheduled_events WHERE game_id = ?
|
|
109
|
+
`)
|
|
110
|
+
.get(gameId);
|
|
111
|
+
// Relationships
|
|
112
|
+
const relationshipCount = db
|
|
113
|
+
.prepare(`SELECT COUNT(*) as count FROM relationships WHERE game_id = ?`)
|
|
114
|
+
.get(gameId);
|
|
115
|
+
// Secrets
|
|
116
|
+
const secretStats = db
|
|
117
|
+
.prepare(`
|
|
118
|
+
SELECT
|
|
119
|
+
COUNT(*) as total,
|
|
120
|
+
SUM(CASE WHEN is_public = 1 THEN 1 ELSE 0 END) as revealed
|
|
121
|
+
FROM secrets WHERE game_id = ?
|
|
122
|
+
`)
|
|
123
|
+
.get(gameId);
|
|
124
|
+
// Factions
|
|
125
|
+
const factionStats = db
|
|
126
|
+
.prepare(`
|
|
127
|
+
SELECT
|
|
128
|
+
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
|
|
129
|
+
SUM(CASE WHEN status = 'disbanded' THEN 1 ELSE 0 END) as disbanded
|
|
130
|
+
FROM factions WHERE game_id = ?
|
|
131
|
+
`)
|
|
132
|
+
.get(gameId);
|
|
133
|
+
// Abilities
|
|
134
|
+
const abilityStats = db
|
|
135
|
+
.prepare(`
|
|
136
|
+
SELECT
|
|
137
|
+
SUM(CASE WHEN owner_type = 'template' THEN 1 ELSE 0 END) as templates,
|
|
138
|
+
SUM(CASE WHEN owner_type = 'character' THEN 1 ELSE 0 END) as character_owned
|
|
139
|
+
FROM abilities WHERE game_id = ?
|
|
140
|
+
`)
|
|
141
|
+
.get(gameId);
|
|
142
|
+
// Notes
|
|
143
|
+
const noteStats = db
|
|
144
|
+
.prepare(`
|
|
145
|
+
SELECT
|
|
146
|
+
COUNT(*) as total,
|
|
147
|
+
SUM(CASE WHEN pinned = 1 THEN 1 ELSE 0 END) as pinned
|
|
148
|
+
FROM notes WHERE game_id = ?
|
|
149
|
+
`)
|
|
150
|
+
.get(gameId);
|
|
151
|
+
// Status effects
|
|
152
|
+
const statusStats = db
|
|
153
|
+
.prepare(`
|
|
154
|
+
SELECT
|
|
155
|
+
COUNT(*) as count,
|
|
156
|
+
COUNT(DISTINCT target_id) as affected
|
|
157
|
+
FROM status_effects se
|
|
158
|
+
JOIN characters c ON c.id = se.target_id
|
|
159
|
+
WHERE c.game_id = ?
|
|
160
|
+
`)
|
|
161
|
+
.get(gameId);
|
|
162
|
+
// Tags
|
|
163
|
+
const tagCount = db
|
|
164
|
+
.prepare(`SELECT COUNT(DISTINCT tag) as count FROM tags WHERE game_id = ?`)
|
|
165
|
+
.get(gameId);
|
|
166
|
+
// Random tables
|
|
167
|
+
const tableCount = db
|
|
168
|
+
.prepare(`SELECT COUNT(*) as count FROM random_tables WHERE game_id = ?`)
|
|
169
|
+
.get(gameId);
|
|
170
|
+
// Narrative events
|
|
171
|
+
const narrativeStats = db
|
|
172
|
+
.prepare(`
|
|
173
|
+
SELECT
|
|
174
|
+
COUNT(*) as total,
|
|
175
|
+
SUM(CASE WHEN timestamp > datetime('now', '-1 hour') THEN 1 ELSE 0 END) as recent_hour
|
|
176
|
+
FROM narrative_events WHERE game_id = ?
|
|
177
|
+
`)
|
|
178
|
+
.get(gameId);
|
|
179
|
+
// Images
|
|
180
|
+
const imageCount = db
|
|
181
|
+
.prepare(`SELECT COUNT(*) as count FROM stored_images WHERE game_id = ?`)
|
|
182
|
+
.get(gameId);
|
|
183
|
+
// Calendar/Time
|
|
184
|
+
const calendarRow = db
|
|
185
|
+
.prepare(`SELECT * FROM game_time WHERE game_id = ?`)
|
|
186
|
+
.get(gameId);
|
|
187
|
+
// Get current location name
|
|
188
|
+
let playerLocation = null;
|
|
189
|
+
if (gameRow.current_location_id) {
|
|
190
|
+
const locRow = db
|
|
191
|
+
.prepare(`SELECT name FROM locations WHERE id = ?`)
|
|
192
|
+
.get(gameRow.current_location_id);
|
|
193
|
+
playerLocation = locRow?.name || null;
|
|
194
|
+
}
|
|
195
|
+
// Build the game state audit
|
|
196
|
+
const gameStateAudit = {
|
|
197
|
+
characters: {
|
|
198
|
+
total: characterStats.total || 0,
|
|
199
|
+
players: characterStats.players || 0,
|
|
200
|
+
npcs: characterStats.npcs || 0,
|
|
201
|
+
withNotes: characterStats.with_notes || 0,
|
|
202
|
+
withConditions: charactersWithConditions.count || 0,
|
|
203
|
+
},
|
|
204
|
+
locations: {
|
|
205
|
+
total: locationStats.total || 0,
|
|
206
|
+
connected: connectedLocations.count || 0,
|
|
207
|
+
},
|
|
208
|
+
quests: {
|
|
209
|
+
active: questStats.active || 0,
|
|
210
|
+
completed: questStats.completed || 0,
|
|
211
|
+
failed: questStats.failed || 0,
|
|
212
|
+
},
|
|
213
|
+
items: {
|
|
214
|
+
total: itemStats.total || 0,
|
|
215
|
+
inInventories: itemStats.in_inventories || 0,
|
|
216
|
+
inLocations: itemStats.in_locations || 0,
|
|
217
|
+
},
|
|
218
|
+
combat: {
|
|
219
|
+
active: !!combatRow,
|
|
220
|
+
combatId: combatRow?.id || null,
|
|
221
|
+
round: combatRow?.round || null,
|
|
222
|
+
participantCount: combatRow
|
|
223
|
+
? (JSON.parse(combatRow.participants || "[]")).length
|
|
224
|
+
: 0,
|
|
225
|
+
},
|
|
226
|
+
resources: {
|
|
227
|
+
gameLevel: resourceStats.game_level || 0,
|
|
228
|
+
characterLevel: resourceStats.character_level || 0,
|
|
229
|
+
},
|
|
230
|
+
timers: {
|
|
231
|
+
active: timerStats.active || 0,
|
|
232
|
+
triggered: timerStats.triggered || 0,
|
|
233
|
+
},
|
|
234
|
+
scheduledEvents: {
|
|
235
|
+
pending: eventStats.pending || 0,
|
|
236
|
+
triggered: eventStats.triggered || 0,
|
|
237
|
+
},
|
|
238
|
+
relationships: {
|
|
239
|
+
total: relationshipCount.count || 0,
|
|
240
|
+
},
|
|
241
|
+
secrets: {
|
|
242
|
+
total: secretStats.total || 0,
|
|
243
|
+
revealed: secretStats.revealed || 0,
|
|
244
|
+
hidden: (secretStats.total || 0) - (secretStats.revealed || 0),
|
|
245
|
+
},
|
|
246
|
+
factions: {
|
|
247
|
+
active: factionStats.active || 0,
|
|
248
|
+
disbanded: factionStats.disbanded || 0,
|
|
249
|
+
},
|
|
250
|
+
abilities: {
|
|
251
|
+
templates: abilityStats.templates || 0,
|
|
252
|
+
characterOwned: abilityStats.character_owned || 0,
|
|
253
|
+
},
|
|
254
|
+
notes: {
|
|
255
|
+
total: noteStats.total || 0,
|
|
256
|
+
pinned: noteStats.pinned || 0,
|
|
257
|
+
},
|
|
258
|
+
statusEffects: {
|
|
259
|
+
activeCount: statusStats.count || 0,
|
|
260
|
+
affectedCharacters: statusStats.affected || 0,
|
|
261
|
+
},
|
|
262
|
+
tags: {
|
|
263
|
+
uniqueTags: tagCount.count || 0,
|
|
264
|
+
},
|
|
265
|
+
randomTables: {
|
|
266
|
+
total: tableCount.count || 0,
|
|
267
|
+
},
|
|
268
|
+
narrativeEvents: {
|
|
269
|
+
total: narrativeStats.total || 0,
|
|
270
|
+
recentHour: narrativeStats.recent_hour || 0,
|
|
271
|
+
},
|
|
272
|
+
images: {
|
|
273
|
+
total: imageCount.count || 0,
|
|
274
|
+
},
|
|
275
|
+
time: {
|
|
276
|
+
hasCalendar: !!calendarRow,
|
|
277
|
+
currentTime: calendarRow
|
|
278
|
+
? JSON.stringify(safeJsonParse(calendarRow.current_time, null))
|
|
279
|
+
: null,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// PERSISTENCE REMINDERS
|
|
284
|
+
// ============================================================================
|
|
285
|
+
const persistenceReminders = [];
|
|
286
|
+
// CRITICAL: Active combat must be resolved or saved
|
|
287
|
+
if (gameStateAudit.combat.active && gameStateAudit.combat.combatId) {
|
|
288
|
+
persistenceReminders.push({
|
|
289
|
+
category: "critical",
|
|
290
|
+
entityType: "combat",
|
|
291
|
+
tool: "end_combat OR get_combat",
|
|
292
|
+
reminder: "ACTIVE COMBAT IN PROGRESS",
|
|
293
|
+
reason: `Combat is active (Round ${gameStateAudit.combat.round}). Either resolve it before pausing or ensure the full combat state is captured in your pause notes.`,
|
|
294
|
+
entityIds: [gameStateAudit.combat.combatId],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// CRITICAL: Status effects that may expire
|
|
298
|
+
if (gameStateAudit.statusEffects.activeCount > 0) {
|
|
299
|
+
persistenceReminders.push({
|
|
300
|
+
category: "critical",
|
|
301
|
+
entityType: "status_effects",
|
|
302
|
+
tool: "list_status_effects",
|
|
303
|
+
reminder: "Active status effects on characters",
|
|
304
|
+
reason: `${gameStateAudit.statusEffects.activeCount} status effect(s) on ${gameStateAudit.statusEffects.affectedCharacters} character(s). Note any that are narratively important.`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// IMPORTANT: Recent narrative events should be logged
|
|
308
|
+
if (narrativeStats.recent_hour === 0) {
|
|
309
|
+
persistenceReminders.push({
|
|
310
|
+
category: "important",
|
|
311
|
+
entityType: "narrative_events",
|
|
312
|
+
tool: "log_event",
|
|
313
|
+
reminder: "No events logged in the last hour",
|
|
314
|
+
reason: "Consider logging key story moments before pausing. This helps with recaps and continuity.",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// IMPORTANT: Active timers need context
|
|
318
|
+
if (gameStateAudit.timers.active > 0) {
|
|
319
|
+
persistenceReminders.push({
|
|
320
|
+
category: "important",
|
|
321
|
+
entityType: "timers",
|
|
322
|
+
tool: "list_timers",
|
|
323
|
+
reminder: "Active timers running",
|
|
324
|
+
reason: `${gameStateAudit.timers.active} timer(s) are active. Document their narrative significance in your pause state.`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// IMPORTANT: Pending scheduled events
|
|
328
|
+
if (gameStateAudit.scheduledEvents.pending > 0) {
|
|
329
|
+
persistenceReminders.push({
|
|
330
|
+
category: "important",
|
|
331
|
+
entityType: "scheduled_events",
|
|
332
|
+
tool: "list_scheduled_events",
|
|
333
|
+
reminder: "Pending scheduled events",
|
|
334
|
+
reason: `${gameStateAudit.scheduledEvents.pending} event(s) are scheduled. Note any that are imminent.`,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// IMPORTANT: Characters without notes
|
|
338
|
+
if (gameStateAudit.characters.npcs > 0 && gameStateAudit.characters.withNotes < gameStateAudit.characters.npcs) {
|
|
339
|
+
const npcsWithoutNotes = gameStateAudit.characters.npcs - gameStateAudit.characters.withNotes;
|
|
340
|
+
if (npcsWithoutNotes > 0) {
|
|
341
|
+
persistenceReminders.push({
|
|
342
|
+
category: "suggested",
|
|
343
|
+
entityType: "characters",
|
|
344
|
+
tool: "update_character",
|
|
345
|
+
reminder: "NPCs missing character notes",
|
|
346
|
+
reason: `${npcsWithoutNotes} NPC(s) have no notes. Consider updating notes for NPCs the player interacted with.`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// SUGGESTED: Quest progress
|
|
351
|
+
if (gameStateAudit.quests.active > 0) {
|
|
352
|
+
persistenceReminders.push({
|
|
353
|
+
category: "suggested",
|
|
354
|
+
entityType: "quests",
|
|
355
|
+
tool: "complete_objective OR update_quest",
|
|
356
|
+
reminder: "Review active quest progress",
|
|
357
|
+
reason: `${gameStateAudit.quests.active} quest(s) active. Update objectives if any were completed or failed.`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
// SUGGESTED: Relationship changes
|
|
361
|
+
if (gameStateAudit.relationships.total > 0) {
|
|
362
|
+
persistenceReminders.push({
|
|
363
|
+
category: "suggested",
|
|
364
|
+
entityType: "relationships",
|
|
365
|
+
tool: "modify_relationship",
|
|
366
|
+
reminder: "Update relationship values if attitudes changed",
|
|
367
|
+
reason: `${gameStateAudit.relationships.total} relationship(s) tracked. Adjust values if NPCs' feelings toward the player changed.`,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
// SUGGESTED: Secrets revealed or discovered
|
|
371
|
+
if (gameStateAudit.secrets.hidden > 0) {
|
|
372
|
+
persistenceReminders.push({
|
|
373
|
+
category: "suggested",
|
|
374
|
+
entityType: "secrets",
|
|
375
|
+
tool: "reveal_secret",
|
|
376
|
+
reminder: "Update secrets if any were discovered",
|
|
377
|
+
reason: `${gameStateAudit.secrets.hidden} secret(s) still hidden. Mark any as revealed if the player discovered them.`,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
// SUGGESTED: Resources changed
|
|
381
|
+
if (gameStateAudit.resources.gameLevel + gameStateAudit.resources.characterLevel > 0) {
|
|
382
|
+
persistenceReminders.push({
|
|
383
|
+
category: "suggested",
|
|
384
|
+
entityType: "resources",
|
|
385
|
+
tool: "modify_resource",
|
|
386
|
+
reminder: "Update resources if any changed",
|
|
387
|
+
reason: "Review gold, reputation, or other tracked resources for changes during the game.",
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
// SUGGESTED: Items gained or lost
|
|
391
|
+
if (gameStateAudit.items.total > 0) {
|
|
392
|
+
persistenceReminders.push({
|
|
393
|
+
category: "suggested",
|
|
394
|
+
entityType: "items",
|
|
395
|
+
tool: "create_item OR transfer_item OR delete_item",
|
|
396
|
+
reminder: "Update inventory for items gained/lost",
|
|
397
|
+
reason: "Ensure any items picked up, dropped, or used are reflected in the game state.",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
// SUGGESTED: Create a DM note for the game
|
|
401
|
+
persistenceReminders.push({
|
|
402
|
+
category: "suggested",
|
|
403
|
+
entityType: "notes",
|
|
404
|
+
tool: "create_note",
|
|
405
|
+
reminder: "Consider creating a game recap note",
|
|
406
|
+
reason: "A pinned recap note can help with continuity across sessions.",
|
|
407
|
+
});
|
|
408
|
+
// SUGGESTED: Update the in-game time
|
|
409
|
+
if (gameStateAudit.time.hasCalendar) {
|
|
410
|
+
persistenceReminders.push({
|
|
411
|
+
category: "suggested",
|
|
412
|
+
entityType: "time",
|
|
413
|
+
tool: "advance_time OR set_time",
|
|
414
|
+
reminder: "Update the in-game calendar",
|
|
415
|
+
reason: "Ensure the in-game time reflects how much time passed during this game.",
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
// Check for existing pause state
|
|
419
|
+
const existingPause = getPauseState(gameId);
|
|
420
|
+
// Build the ephemeral context checklist
|
|
421
|
+
const checklist = [
|
|
422
|
+
// Required items
|
|
423
|
+
{
|
|
424
|
+
category: "Scene Context",
|
|
425
|
+
item: "currentScene",
|
|
426
|
+
description: "Describe where we are in the story - what scene/location/moment is active",
|
|
427
|
+
required: true,
|
|
428
|
+
example: "The party is in the merchant's shop, having just discovered the hidden basement entrance",
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
category: "Scene Context",
|
|
432
|
+
item: "immediateSituation",
|
|
433
|
+
description: "What is happening RIGHT NOW - the exact moment we're pausing at",
|
|
434
|
+
required: true,
|
|
435
|
+
example: "Kira has her hand on the trapdoor handle, asking the party if they should descend",
|
|
436
|
+
},
|
|
437
|
+
// Important context
|
|
438
|
+
{
|
|
439
|
+
category: "Scene Context",
|
|
440
|
+
item: "sceneAtmosphere",
|
|
441
|
+
description: "Mood, lighting, ambient sounds, emotional tension level",
|
|
442
|
+
required: false,
|
|
443
|
+
example: "Tense and dusty, afternoon light through grimy windows, smell of old spices",
|
|
444
|
+
},
|
|
445
|
+
// Pending interactions
|
|
446
|
+
{
|
|
447
|
+
category: "Player Interaction",
|
|
448
|
+
item: "pendingPlayerAction",
|
|
449
|
+
description: "What action is the player considering or about to take?",
|
|
450
|
+
required: false,
|
|
451
|
+
example: "Player was deciding whether to open the trapdoor or search for traps first",
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
category: "Player Interaction",
|
|
455
|
+
item: "awaitingResponseTo",
|
|
456
|
+
description: "What question or prompt is awaiting the player's response?",
|
|
457
|
+
required: false,
|
|
458
|
+
example: "Asked player 'Do you want to descend first, or send someone else?'",
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
category: "Player Interaction",
|
|
462
|
+
item: "presentedChoices",
|
|
463
|
+
description: "Any formal choices that were presented to the player",
|
|
464
|
+
required: false,
|
|
465
|
+
},
|
|
466
|
+
// Narrative threads
|
|
467
|
+
{
|
|
468
|
+
category: "Narrative Threads",
|
|
469
|
+
item: "activeThreads",
|
|
470
|
+
description: "Ongoing storylines, investigations, or subplots that are 'in play'",
|
|
471
|
+
required: false,
|
|
472
|
+
example: "[{name: 'Missing Merchant', status: 'active', urgency: 'high'}]",
|
|
473
|
+
},
|
|
474
|
+
// DM plans
|
|
475
|
+
{
|
|
476
|
+
category: "DM Notes",
|
|
477
|
+
item: "dmShortTermPlans",
|
|
478
|
+
description: "What was about to happen next? Any planned encounters or reveals?",
|
|
479
|
+
required: false,
|
|
480
|
+
example: "If they descend, they'll find the merchant's body and trigger the ghost encounter",
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
category: "DM Notes",
|
|
484
|
+
item: "dmLongTermPlans",
|
|
485
|
+
description: "Major plot arcs being developed or built toward",
|
|
486
|
+
required: false,
|
|
487
|
+
example: "Building toward reveal that the merchant guild is a front for the cult",
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
category: "DM Notes",
|
|
491
|
+
item: "upcomingReveals",
|
|
492
|
+
description: "Secrets that are close to being discovered",
|
|
493
|
+
required: false,
|
|
494
|
+
},
|
|
495
|
+
// NPC state
|
|
496
|
+
{
|
|
497
|
+
category: "NPC Context",
|
|
498
|
+
item: "npcAttitudes",
|
|
499
|
+
description: "Current emotional states/attitudes of relevant NPCs (especially if shifted from baseline)",
|
|
500
|
+
required: false,
|
|
501
|
+
example: "{'guard_captain_id': 'suspicious of party after tavern incident'}",
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
category: "NPC Context",
|
|
505
|
+
item: "activeConversations",
|
|
506
|
+
description: "Any ongoing conversations and where they left off",
|
|
507
|
+
required: false,
|
|
508
|
+
},
|
|
509
|
+
// Player context
|
|
510
|
+
{
|
|
511
|
+
category: "Player Context",
|
|
512
|
+
item: "playerApparentGoals",
|
|
513
|
+
description: "What does the player seem to be trying to accomplish?",
|
|
514
|
+
required: false,
|
|
515
|
+
example: "Seems focused on finding the missing merchant, ignoring side hooks",
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
category: "Player Context",
|
|
519
|
+
item: "unresolvedHooks",
|
|
520
|
+
description: "Plot hooks the player noticed but hasn't pursued yet",
|
|
521
|
+
required: false,
|
|
522
|
+
example: "['The innkeeper's warning about the old mine', 'Strange lights in forest']",
|
|
523
|
+
},
|
|
524
|
+
// Tone
|
|
525
|
+
{
|
|
526
|
+
category: "Tone",
|
|
527
|
+
item: "recentTone",
|
|
528
|
+
description: "Recent narrative tone - was it tense? Comedic? Romantic? Action-heavy?",
|
|
529
|
+
required: false,
|
|
530
|
+
example: "Suspenseful mystery with moments of dark humor",
|
|
531
|
+
},
|
|
532
|
+
];
|
|
533
|
+
// Build comprehensive instructions
|
|
534
|
+
const criticalCount = persistenceReminders.filter(r => r.category === "critical").length;
|
|
535
|
+
const importantCount = persistenceReminders.filter(r => r.category === "important").length;
|
|
536
|
+
const instructions = `
|
|
537
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
538
|
+
PAUSE PREPARATION CHECKLIST
|
|
539
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
540
|
+
|
|
541
|
+
This checklist helps you persist EVERYTHING before ending the game.
|
|
542
|
+
|
|
543
|
+
${criticalCount > 0 ? `⚠️ ${criticalCount} CRITICAL item(s) require immediate attention!` : ""}
|
|
544
|
+
${importantCount > 0 ? `📋 ${importantCount} IMPORTANT item(s) should be reviewed.` : ""}
|
|
545
|
+
|
|
546
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
547
|
+
STEP 1: PERSIST GAME DATA
|
|
548
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
549
|
+
|
|
550
|
+
Review the PERSISTENCE REMINDERS section. These are things that exist in the
|
|
551
|
+
database but may need updating based on what happened during play:
|
|
552
|
+
|
|
553
|
+
${persistenceReminders.filter(r => r.category === "critical").map(r => `🔴 [CRITICAL] ${r.reminder}`).join("\n ") || "No critical items"}
|
|
554
|
+
${persistenceReminders.filter(r => r.category === "important").map(r => `🟡 [IMPORTANT] ${r.reminder}`).join("\n ") || "No important items"}
|
|
555
|
+
${persistenceReminders.filter(r => r.category === "suggested").map(r => `🟢 [SUGGESTED] ${r.reminder}`).join("\n ") || "No suggestions"}
|
|
556
|
+
|
|
557
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
558
|
+
STEP 2: SAVE DM CONTEXT
|
|
559
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
560
|
+
|
|
561
|
+
After persisting game data, call save_pause_state with your ephemeral context.
|
|
562
|
+
This captures what's in your "head" that ISN'T in the database:
|
|
563
|
+
|
|
564
|
+
REQUIRED:
|
|
565
|
+
• currentScene - Where we are in the story
|
|
566
|
+
• immediateSituation - What's happening RIGHT NOW (be specific!)
|
|
567
|
+
|
|
568
|
+
OPTIONAL (fill in what's relevant):
|
|
569
|
+
• Scene atmosphere, tone, pending player actions
|
|
570
|
+
• Active narrative threads and DM plans
|
|
571
|
+
• NPC attitudes and ongoing conversations
|
|
572
|
+
• Player goals and unresolved plot hooks
|
|
573
|
+
|
|
574
|
+
TIPS:
|
|
575
|
+
• Be specific about the EXACT moment - "hand on door handle" not "at the door"
|
|
576
|
+
• Capture emotional states and atmosphere for resumption
|
|
577
|
+
• Note what YOU were planning, not just what happened
|
|
578
|
+
• Write as if briefing a replacement DM mid-session
|
|
579
|
+
|
|
580
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
581
|
+
STEP 3: VERIFY
|
|
582
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
583
|
+
|
|
584
|
+
After saving, the game can be resumed using get_resume_context which will
|
|
585
|
+
provide the next DM (or you in a new context window) with everything needed
|
|
586
|
+
to continue seamlessly.
|
|
587
|
+
|
|
588
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
589
|
+
`.trim();
|
|
590
|
+
return {
|
|
591
|
+
gameId,
|
|
592
|
+
gameName: gameRow.name,
|
|
593
|
+
currentState: {
|
|
594
|
+
playerLocation,
|
|
595
|
+
activeQuests: questStats.active || 0,
|
|
596
|
+
activeCombat: !!combatRow,
|
|
597
|
+
activeTimers: timerStats.active || 0,
|
|
598
|
+
pendingEvents: eventStats.pending || 0,
|
|
599
|
+
recentEventCount: narrativeStats.recent_hour || 0,
|
|
600
|
+
},
|
|
601
|
+
gameStateAudit,
|
|
602
|
+
persistenceReminders,
|
|
603
|
+
checklist,
|
|
604
|
+
existingPauseState: existingPause,
|
|
605
|
+
instructions,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
export function savePauseState(params) {
|
|
609
|
+
const db = getDatabase();
|
|
610
|
+
const id = uuidv4();
|
|
611
|
+
const now = new Date().toISOString();
|
|
612
|
+
// Delete existing pause state if any (only one per session)
|
|
613
|
+
db.prepare(`DELETE FROM pause_states WHERE game_id = ?`).run(params.gameId);
|
|
614
|
+
const stmt = db.prepare(`
|
|
615
|
+
INSERT INTO pause_states (
|
|
616
|
+
id, game_id,
|
|
617
|
+
current_scene, scene_atmosphere, immediate_situation,
|
|
618
|
+
pending_player_action, awaiting_response_to, presented_choices,
|
|
619
|
+
active_threads,
|
|
620
|
+
dm_short_term_plans, dm_long_term_plans, upcoming_reveals,
|
|
621
|
+
npc_attitudes, active_conversations,
|
|
622
|
+
recent_tone, player_apparent_goals, unresolved_hooks,
|
|
623
|
+
pause_reason, created_at, model_used
|
|
624
|
+
)
|
|
625
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
626
|
+
`);
|
|
627
|
+
stmt.run(id, params.gameId, params.currentScene, params.sceneAtmosphere || null, params.immediateSituation, params.pendingPlayerAction || null, params.awaitingResponseTo || null, params.presentedChoices ? JSON.stringify(params.presentedChoices) : null, JSON.stringify(params.activeThreads || []), params.dmShortTermPlans || null, params.dmLongTermPlans || null, JSON.stringify(params.upcomingReveals || []), JSON.stringify(params.npcAttitudes || {}), JSON.stringify(params.activeConversations || []), params.recentTone || null, params.playerApparentGoals || null, JSON.stringify(params.unresolvedHooks || []), params.pauseReason || null, now, params.modelUsed || null);
|
|
628
|
+
// Also log a narrative event for the pause
|
|
629
|
+
const eventStmt = db.prepare(`
|
|
630
|
+
INSERT INTO narrative_events (id, game_id, event_type, content, metadata, timestamp)
|
|
631
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
632
|
+
`);
|
|
633
|
+
eventStmt.run(uuidv4(), params.gameId, "game_paused", `Game paused: ${params.immediateSituation}`, JSON.stringify({
|
|
634
|
+
pauseReason: params.pauseReason,
|
|
635
|
+
modelUsed: params.modelUsed,
|
|
636
|
+
pauseStateId: id,
|
|
637
|
+
}), now);
|
|
638
|
+
return {
|
|
639
|
+
id,
|
|
640
|
+
gameId: params.gameId,
|
|
641
|
+
currentScene: params.currentScene,
|
|
642
|
+
sceneAtmosphere: params.sceneAtmosphere || null,
|
|
643
|
+
immediateSituation: params.immediateSituation,
|
|
644
|
+
pendingPlayerAction: params.pendingPlayerAction || null,
|
|
645
|
+
awaitingResponseTo: params.awaitingResponseTo || null,
|
|
646
|
+
presentedChoices: params.presentedChoices || null,
|
|
647
|
+
activeThreads: params.activeThreads || [],
|
|
648
|
+
dmShortTermPlans: params.dmShortTermPlans || null,
|
|
649
|
+
dmLongTermPlans: params.dmLongTermPlans || null,
|
|
650
|
+
upcomingReveals: params.upcomingReveals || [],
|
|
651
|
+
npcAttitudes: params.npcAttitudes || {},
|
|
652
|
+
activeConversations: params.activeConversations || [],
|
|
653
|
+
recentTone: params.recentTone || null,
|
|
654
|
+
playerApparentGoals: params.playerApparentGoals || null,
|
|
655
|
+
unresolvedHooks: params.unresolvedHooks || [],
|
|
656
|
+
pauseReason: params.pauseReason || null,
|
|
657
|
+
createdAt: now,
|
|
658
|
+
modelUsed: params.modelUsed || null,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
// ============================================================================
|
|
662
|
+
// GET PAUSE STATE
|
|
663
|
+
// ============================================================================
|
|
664
|
+
export function getPauseState(gameId) {
|
|
665
|
+
const db = getDatabase();
|
|
666
|
+
const row = db
|
|
667
|
+
.prepare(`SELECT * FROM pause_states WHERE game_id = ?`)
|
|
668
|
+
.get(gameId);
|
|
669
|
+
if (!row)
|
|
670
|
+
return null;
|
|
671
|
+
return {
|
|
672
|
+
id: row.id,
|
|
673
|
+
gameId: row.game_id,
|
|
674
|
+
currentScene: row.current_scene,
|
|
675
|
+
sceneAtmosphere: row.scene_atmosphere,
|
|
676
|
+
immediateSituation: row.immediate_situation,
|
|
677
|
+
pendingPlayerAction: row.pending_player_action,
|
|
678
|
+
awaitingResponseTo: row.awaiting_response_to,
|
|
679
|
+
presentedChoices: row.presented_choices
|
|
680
|
+
? JSON.parse(row.presented_choices)
|
|
681
|
+
: null,
|
|
682
|
+
activeThreads: JSON.parse(row.active_threads || "[]"),
|
|
683
|
+
dmShortTermPlans: row.dm_short_term_plans,
|
|
684
|
+
dmLongTermPlans: row.dm_long_term_plans,
|
|
685
|
+
upcomingReveals: JSON.parse(row.upcoming_reveals || "[]"),
|
|
686
|
+
npcAttitudes: JSON.parse(row.npc_attitudes || "{}"),
|
|
687
|
+
activeConversations: JSON.parse(row.active_conversations || "[]"),
|
|
688
|
+
recentTone: row.recent_tone,
|
|
689
|
+
playerApparentGoals: row.player_apparent_goals,
|
|
690
|
+
unresolvedHooks: JSON.parse(row.unresolved_hooks || "[]"),
|
|
691
|
+
pauseReason: row.pause_reason,
|
|
692
|
+
createdAt: row.created_at,
|
|
693
|
+
modelUsed: row.model_used,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
// ============================================================================
|
|
697
|
+
// GET RESUME CONTEXT
|
|
698
|
+
// ============================================================================
|
|
699
|
+
/**
|
|
700
|
+
* Returns everything needed to resume a paused game seamlessly.
|
|
701
|
+
* Includes pause state, full game state, and a ready-to-use resume prompt.
|
|
702
|
+
*/
|
|
703
|
+
export function getResumeContext(gameId) {
|
|
704
|
+
const db = getDatabase();
|
|
705
|
+
// Get pause state
|
|
706
|
+
const pauseState = getPauseState(gameId);
|
|
707
|
+
if (!pauseState)
|
|
708
|
+
return null;
|
|
709
|
+
// Get game
|
|
710
|
+
const gameRow = db
|
|
711
|
+
.prepare(`SELECT * FROM games WHERE id = ?`)
|
|
712
|
+
.get(gameId);
|
|
713
|
+
if (!gameRow)
|
|
714
|
+
return null;
|
|
715
|
+
const game = {
|
|
716
|
+
id: gameRow.id,
|
|
717
|
+
name: gameRow.name,
|
|
718
|
+
setting: gameRow.setting,
|
|
719
|
+
style: gameRow.style,
|
|
720
|
+
rules: gameRow.rules ? JSON.parse(gameRow.rules) : null,
|
|
721
|
+
preferences: gameRow.preferences
|
|
722
|
+
? JSON.parse(gameRow.preferences)
|
|
723
|
+
: null,
|
|
724
|
+
currentLocationId: gameRow.current_location_id,
|
|
725
|
+
titleImageId: gameRow.title_image_id,
|
|
726
|
+
faviconImageId: gameRow.favicon_image_id,
|
|
727
|
+
createdAt: gameRow.created_at,
|
|
728
|
+
updatedAt: gameRow.updated_at,
|
|
729
|
+
};
|
|
730
|
+
// Get player character
|
|
731
|
+
const playerRow = db
|
|
732
|
+
.prepare(`SELECT * FROM characters WHERE game_id = ? AND is_player = 1 LIMIT 1`)
|
|
733
|
+
.get(gameId);
|
|
734
|
+
const playerCharacter = playerRow
|
|
735
|
+
? {
|
|
736
|
+
id: playerRow.id,
|
|
737
|
+
gameId: playerRow.game_id,
|
|
738
|
+
name: playerRow.name,
|
|
739
|
+
isPlayer: true,
|
|
740
|
+
attributes: JSON.parse(playerRow.attributes || "{}"),
|
|
741
|
+
skills: JSON.parse(playerRow.skills || "{}"),
|
|
742
|
+
status: JSON.parse(playerRow.status || "{}"),
|
|
743
|
+
locationId: playerRow.location_id,
|
|
744
|
+
notes: playerRow.notes,
|
|
745
|
+
voice: playerRow.voice
|
|
746
|
+
? JSON.parse(playerRow.voice)
|
|
747
|
+
: null,
|
|
748
|
+
imageGen: playerRow.image_gen
|
|
749
|
+
? JSON.parse(playerRow.image_gen)
|
|
750
|
+
: null,
|
|
751
|
+
createdAt: playerRow.created_at,
|
|
752
|
+
}
|
|
753
|
+
: null;
|
|
754
|
+
// Get current location
|
|
755
|
+
const locationRow = game.currentLocationId
|
|
756
|
+
? db
|
|
757
|
+
.prepare(`SELECT * FROM locations WHERE id = ?`)
|
|
758
|
+
.get(game.currentLocationId)
|
|
759
|
+
: null;
|
|
760
|
+
const currentLocation = locationRow
|
|
761
|
+
? {
|
|
762
|
+
id: locationRow.id,
|
|
763
|
+
gameId: locationRow.game_id,
|
|
764
|
+
name: locationRow.name,
|
|
765
|
+
description: locationRow.description,
|
|
766
|
+
properties: JSON.parse(locationRow.properties || "{}"),
|
|
767
|
+
imageGen: locationRow.image_gen
|
|
768
|
+
? JSON.parse(locationRow.image_gen)
|
|
769
|
+
: null,
|
|
770
|
+
}
|
|
771
|
+
: null;
|
|
772
|
+
// Get active quests
|
|
773
|
+
const questRows = db
|
|
774
|
+
.prepare(`SELECT * FROM quests WHERE game_id = ? AND status = 'active'`)
|
|
775
|
+
.all(gameId);
|
|
776
|
+
const activeQuests = questRows.map((row) => ({
|
|
777
|
+
id: row.id,
|
|
778
|
+
gameId: row.game_id,
|
|
779
|
+
name: row.name,
|
|
780
|
+
description: row.description,
|
|
781
|
+
objectives: JSON.parse(row.objectives || "[]"),
|
|
782
|
+
status: row.status,
|
|
783
|
+
rewards: row.rewards,
|
|
784
|
+
}));
|
|
785
|
+
// Get active combat
|
|
786
|
+
const combatRow = db
|
|
787
|
+
.prepare(`SELECT * FROM combats WHERE game_id = ? AND status = 'active' LIMIT 1`)
|
|
788
|
+
.get(gameId);
|
|
789
|
+
const activeCombat = combatRow
|
|
790
|
+
? {
|
|
791
|
+
id: combatRow.id,
|
|
792
|
+
gameId: combatRow.game_id,
|
|
793
|
+
locationId: combatRow.location_id,
|
|
794
|
+
participants: JSON.parse(combatRow.participants || "[]"),
|
|
795
|
+
currentTurn: combatRow.current_turn,
|
|
796
|
+
round: combatRow.round,
|
|
797
|
+
status: combatRow.status,
|
|
798
|
+
log: JSON.parse(combatRow.log || "[]"),
|
|
799
|
+
}
|
|
800
|
+
: null;
|
|
801
|
+
// Get recent events (last 10)
|
|
802
|
+
const eventRows = db
|
|
803
|
+
.prepare(`SELECT * FROM narrative_events WHERE game_id = ? ORDER BY timestamp DESC LIMIT 10`)
|
|
804
|
+
.all(gameId);
|
|
805
|
+
const recentEvents = eventRows.map((row) => ({
|
|
806
|
+
id: row.id,
|
|
807
|
+
gameId: row.game_id,
|
|
808
|
+
eventType: row.event_type,
|
|
809
|
+
content: row.content,
|
|
810
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
811
|
+
timestamp: row.timestamp,
|
|
812
|
+
}));
|
|
813
|
+
// Get active timers
|
|
814
|
+
const timerRows = db
|
|
815
|
+
.prepare(`SELECT * FROM timers WHERE game_id = ? AND triggered = 0`)
|
|
816
|
+
.all(gameId);
|
|
817
|
+
const activeTimers = timerRows.map((row) => ({
|
|
818
|
+
id: row.id,
|
|
819
|
+
gameId: row.game_id,
|
|
820
|
+
name: row.name,
|
|
821
|
+
description: row.description,
|
|
822
|
+
timerType: row.timer_type,
|
|
823
|
+
currentValue: row.current_value,
|
|
824
|
+
maxValue: row.max_value,
|
|
825
|
+
direction: row.direction,
|
|
826
|
+
triggerAt: row.trigger_at,
|
|
827
|
+
triggered: false,
|
|
828
|
+
unit: row.unit,
|
|
829
|
+
visibleToPlayers: Boolean(row.visible_to_players),
|
|
830
|
+
createdAt: row.created_at,
|
|
831
|
+
consequence: row.consequence ? safeJsonParse(row.consequence, null) : null,
|
|
832
|
+
}));
|
|
833
|
+
// Get pending scheduled events
|
|
834
|
+
const scheduledRows = db
|
|
835
|
+
.prepare(`SELECT * FROM scheduled_events WHERE game_id = ? AND triggered = 0`)
|
|
836
|
+
.all(gameId);
|
|
837
|
+
const pendingScheduledEvents = scheduledRows.map((row) => ({
|
|
838
|
+
id: row.id,
|
|
839
|
+
gameId: row.game_id,
|
|
840
|
+
name: row.name,
|
|
841
|
+
description: row.description,
|
|
842
|
+
triggerTime: JSON.parse(row.trigger_time),
|
|
843
|
+
recurring: row.recurring,
|
|
844
|
+
triggered: false,
|
|
845
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
846
|
+
consequence: row.consequence ? safeJsonParse(row.consequence, null) : null,
|
|
847
|
+
}));
|
|
848
|
+
// Generate narrative summary from recent events
|
|
849
|
+
const narrativeSummary = recentEvents
|
|
850
|
+
.slice()
|
|
851
|
+
.reverse()
|
|
852
|
+
.map((e) => `[${e.eventType}] ${e.content}`)
|
|
853
|
+
.join("\n");
|
|
854
|
+
// Build resume prompt
|
|
855
|
+
const warnings = [];
|
|
856
|
+
if (activeCombat) {
|
|
857
|
+
warnings.push(`ACTIVE COMBAT: Round ${activeCombat.round}, waiting for turn ${activeCombat.currentTurn}`);
|
|
858
|
+
}
|
|
859
|
+
if (activeTimers.length > 0) {
|
|
860
|
+
warnings.push(`${activeTimers.length} active timer(s) - check if any are urgent`);
|
|
861
|
+
}
|
|
862
|
+
const pauseAge = Math.floor((Date.now() - new Date(pauseState.createdAt).getTime()) / 1000 / 60);
|
|
863
|
+
if (pauseAge > 60) {
|
|
864
|
+
warnings.push(`Pause state is ${Math.floor(pauseAge / 60)} hours old - player may need recap`);
|
|
865
|
+
}
|
|
866
|
+
const resumePrompt = `
|
|
867
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
868
|
+
GAME RESUME BRIEFING
|
|
869
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
870
|
+
|
|
871
|
+
GAME: ${game.name}
|
|
872
|
+
SETTING: ${game.setting} (${game.style})
|
|
873
|
+
PAUSED: ${pauseState.createdAt}${pauseState.modelUsed ? ` (by ${pauseState.modelUsed})` : ""}
|
|
874
|
+
${pauseState.pauseReason ? `REASON: ${pauseState.pauseReason}` : ""}
|
|
875
|
+
|
|
876
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
877
|
+
CURRENT SCENE
|
|
878
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
879
|
+
${pauseState.currentScene}
|
|
880
|
+
|
|
881
|
+
ATMOSPHERE: ${pauseState.sceneAtmosphere || "Not specified"}
|
|
882
|
+
TONE: ${pauseState.recentTone || "Not specified"}
|
|
883
|
+
|
|
884
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
885
|
+
IMMEDIATE SITUATION (Resume from here)
|
|
886
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
887
|
+
${pauseState.immediateSituation}
|
|
888
|
+
|
|
889
|
+
${pauseState.pendingPlayerAction ? `PLAYER WAS ABOUT TO: ${pauseState.pendingPlayerAction}` : ""}
|
|
890
|
+
${pauseState.awaitingResponseTo ? `AWAITING RESPONSE TO: ${pauseState.awaitingResponseTo}` : ""}
|
|
891
|
+
${pauseState.presentedChoices ? `CHOICES PRESENTED: ${pauseState.presentedChoices.join(" | ")}` : ""}
|
|
892
|
+
|
|
893
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
894
|
+
PLAYER CONTEXT
|
|
895
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
896
|
+
CHARACTER: ${playerCharacter?.name || "Unknown"}
|
|
897
|
+
LOCATION: ${currentLocation?.name || "Unknown"}
|
|
898
|
+
APPARENT GOALS: ${pauseState.playerApparentGoals || "Not noted"}
|
|
899
|
+
|
|
900
|
+
${pauseState.unresolvedHooks.length > 0 ? `UNRESOLVED HOOKS:\n${pauseState.unresolvedHooks.map((h) => ` • ${h}`).join("\n")}` : ""}
|
|
901
|
+
|
|
902
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
903
|
+
ACTIVE QUESTS (${activeQuests.length})
|
|
904
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
905
|
+
${activeQuests.map((q) => `• ${q.name}: ${q.description.substring(0, 80)}...`).join("\n") || "None"}
|
|
906
|
+
|
|
907
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
908
|
+
DM NOTES (Previous DM's Plans)
|
|
909
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
910
|
+
SHORT-TERM: ${pauseState.dmShortTermPlans || "None recorded"}
|
|
911
|
+
LONG-TERM: ${pauseState.dmLongTermPlans || "None recorded"}
|
|
912
|
+
${pauseState.upcomingReveals.length > 0 ? `UPCOMING REVEALS:\n${pauseState.upcomingReveals.map((r) => ` • ${r}`).join("\n")}` : ""}
|
|
913
|
+
|
|
914
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
915
|
+
NARRATIVE THREADS
|
|
916
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
917
|
+
${pauseState.activeThreads.map((t) => `• [${t.status.toUpperCase()}/${t.urgency}] ${t.name}: ${t.description}`).join("\n") || "None recorded"}
|
|
918
|
+
|
|
919
|
+
${Object.keys(pauseState.npcAttitudes).length > 0 ? `───────────────────────────────────────────────────────────────────────────────
|
|
920
|
+
NPC ATTITUDES
|
|
921
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
922
|
+
${Object.entries(pauseState.npcAttitudes).map(([id, attitude]) => `• ${id}: ${attitude}`).join("\n")}` : ""}
|
|
923
|
+
|
|
924
|
+
${warnings.length > 0 ? `───────────────────────────────────────────────────────────────────────────────
|
|
925
|
+
⚠️ WARNINGS
|
|
926
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
927
|
+
${warnings.map((w) => `• ${w}`).join("\n")}` : ""}
|
|
928
|
+
|
|
929
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
930
|
+
RESUME INSTRUCTIONS
|
|
931
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
932
|
+
|
|
933
|
+
1. Welcome the player back warmly
|
|
934
|
+
2. Provide a brief "Previously..." recap if significant time has passed
|
|
935
|
+
3. Re-establish the scene atmosphere
|
|
936
|
+
4. Resume from the IMMEDIATE SITUATION - pick up exactly where we left off
|
|
937
|
+
5. If choices were pending, re-present them naturally in the narrative
|
|
938
|
+
|
|
939
|
+
Remember: You're continuing mid-scene. Don't start fresh - pick up the thread!
|
|
940
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
941
|
+
`.trim();
|
|
942
|
+
return {
|
|
943
|
+
pauseState,
|
|
944
|
+
gameState: {
|
|
945
|
+
game,
|
|
946
|
+
playerCharacter,
|
|
947
|
+
currentLocation,
|
|
948
|
+
activeQuests,
|
|
949
|
+
activeCombat,
|
|
950
|
+
recentEvents,
|
|
951
|
+
activeTimers,
|
|
952
|
+
pendingScheduledEvents,
|
|
953
|
+
},
|
|
954
|
+
narrativeSummary,
|
|
955
|
+
resumePrompt,
|
|
956
|
+
warnings,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Quick, lightweight context save for use during play.
|
|
961
|
+
* Agents should call this after significant moments.
|
|
962
|
+
* Updates only the most volatile fields without full pause ceremony.
|
|
963
|
+
*/
|
|
964
|
+
export function saveContextSnapshot(params) {
|
|
965
|
+
const db = getDatabase();
|
|
966
|
+
// Check if pause state exists
|
|
967
|
+
const existing = getPauseState(params.gameId);
|
|
968
|
+
if (existing) {
|
|
969
|
+
// Update volatile fields only
|
|
970
|
+
const stmt = db.prepare(`
|
|
971
|
+
UPDATE pause_states SET
|
|
972
|
+
immediate_situation = ?,
|
|
973
|
+
player_apparent_goals = COALESCE(?, player_apparent_goals),
|
|
974
|
+
npc_attitudes = COALESCE(?, npc_attitudes),
|
|
975
|
+
created_at = ?
|
|
976
|
+
WHERE game_id = ?
|
|
977
|
+
`);
|
|
978
|
+
stmt.run(params.situation, params.playerIntent, params.npcMood ? JSON.stringify(params.npcMood) : null, new Date().toISOString(), params.gameId);
|
|
979
|
+
return {
|
|
980
|
+
success: true,
|
|
981
|
+
message: "Context snapshot updated",
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
// Create minimal pause state
|
|
986
|
+
const id = uuidv4();
|
|
987
|
+
const now = new Date().toISOString();
|
|
988
|
+
const stmt = db.prepare(`
|
|
989
|
+
INSERT INTO pause_states (
|
|
990
|
+
id, game_id,
|
|
991
|
+
current_scene, immediate_situation,
|
|
992
|
+
player_apparent_goals, npc_attitudes,
|
|
993
|
+
active_threads, upcoming_reveals, active_conversations, unresolved_hooks,
|
|
994
|
+
created_at
|
|
995
|
+
)
|
|
996
|
+
VALUES (?, ?, ?, ?, ?, ?, '[]', '[]', '[]', '[]', ?)
|
|
997
|
+
`);
|
|
998
|
+
stmt.run(id, params.gameId, params.notes || "Snapshot taken during play", params.situation, params.playerIntent || null, params.npcMood ? JSON.stringify(params.npcMood) : "{}", now);
|
|
999
|
+
return {
|
|
1000
|
+
success: true,
|
|
1001
|
+
message: "Context snapshot created",
|
|
1002
|
+
suggestion: "Consider using prepare_pause and save_pause_state before ending the game for a complete save.",
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
// ============================================================================
|
|
1007
|
+
// DELETE PAUSE STATE
|
|
1008
|
+
// ============================================================================
|
|
1009
|
+
export function deletePauseState(gameId) {
|
|
1010
|
+
const db = getDatabase();
|
|
1011
|
+
const result = db
|
|
1012
|
+
.prepare(`DELETE FROM pause_states WHERE game_id = ?`)
|
|
1013
|
+
.run(gameId);
|
|
1014
|
+
return result.changes > 0;
|
|
1015
|
+
}
|
|
1016
|
+
// ============================================================================
|
|
1017
|
+
// CHECK IF CONTEXT NEEDS SAVE
|
|
1018
|
+
// ============================================================================
|
|
1019
|
+
/**
|
|
1020
|
+
* Returns a reminder if context hasn't been saved recently.
|
|
1021
|
+
* Use this to nudge agents to save context during long sessions.
|
|
1022
|
+
*/
|
|
1023
|
+
export function checkContextFreshness(gameId) {
|
|
1024
|
+
const pauseState = getPauseState(gameId);
|
|
1025
|
+
if (!pauseState) {
|
|
1026
|
+
return {
|
|
1027
|
+
needsSave: true,
|
|
1028
|
+
lastSaved: null,
|
|
1029
|
+
minutesSinceLastSave: null,
|
|
1030
|
+
suggestion: "No context has been saved for this game. Consider calling save_context_snapshot to preserve current state.",
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
const lastSaved = new Date(pauseState.createdAt);
|
|
1034
|
+
const minutesSince = Math.floor((Date.now() - lastSaved.getTime()) / 1000 / 60);
|
|
1035
|
+
if (minutesSince > 30) {
|
|
1036
|
+
return {
|
|
1037
|
+
needsSave: true,
|
|
1038
|
+
lastSaved: pauseState.createdAt,
|
|
1039
|
+
minutesSinceLastSave: minutesSince,
|
|
1040
|
+
suggestion: `Context is ${minutesSince} minutes old. Consider a quick save_context_snapshot to capture recent developments.`,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
return {
|
|
1044
|
+
needsSave: false,
|
|
1045
|
+
lastSaved: pauseState.createdAt,
|
|
1046
|
+
minutesSinceLastSave: minutesSince,
|
|
1047
|
+
suggestion: "Context is fresh - no immediate save needed.",
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Push an update from an external agent (research agent, worldbuilder, etc.)
|
|
1052
|
+
* The primary DM agent will receive this update and can incorporate it.
|
|
1053
|
+
*/
|
|
1054
|
+
export function pushExternalUpdate(params) {
|
|
1055
|
+
const db = getDatabase();
|
|
1056
|
+
const id = uuidv4();
|
|
1057
|
+
const now = new Date().toISOString();
|
|
1058
|
+
const stmt = db.prepare(`
|
|
1059
|
+
INSERT INTO external_updates (
|
|
1060
|
+
id, game_id,
|
|
1061
|
+
source_agent, source_description,
|
|
1062
|
+
update_type, category, title, content, structured_data,
|
|
1063
|
+
target_entity_id, target_entity_type,
|
|
1064
|
+
priority, status, created_at
|
|
1065
|
+
)
|
|
1066
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)
|
|
1067
|
+
`);
|
|
1068
|
+
stmt.run(id, params.gameId, params.sourceAgent, params.sourceDescription || null, params.updateType, params.category || null, params.title, params.content, params.structuredData ? JSON.stringify(params.structuredData) : null, params.targetEntityId || null, params.targetEntityType || null, params.priority || "normal", now);
|
|
1069
|
+
return {
|
|
1070
|
+
id,
|
|
1071
|
+
gameId: params.gameId,
|
|
1072
|
+
sourceAgent: params.sourceAgent,
|
|
1073
|
+
sourceDescription: params.sourceDescription || null,
|
|
1074
|
+
updateType: params.updateType,
|
|
1075
|
+
category: params.category || null,
|
|
1076
|
+
title: params.title,
|
|
1077
|
+
content: params.content,
|
|
1078
|
+
structuredData: params.structuredData || null,
|
|
1079
|
+
targetEntityId: params.targetEntityId || null,
|
|
1080
|
+
targetEntityType: params.targetEntityType || null,
|
|
1081
|
+
priority: params.priority || "normal",
|
|
1082
|
+
status: "pending",
|
|
1083
|
+
createdAt: now,
|
|
1084
|
+
acknowledgedAt: null,
|
|
1085
|
+
appliedAt: null,
|
|
1086
|
+
dmNotes: null,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Get all pending updates for a game.
|
|
1091
|
+
* Call this to check for new information from external agents.
|
|
1092
|
+
*/
|
|
1093
|
+
export function getPendingUpdates(gameId) {
|
|
1094
|
+
const db = getDatabase();
|
|
1095
|
+
const rows = db
|
|
1096
|
+
.prepare(`SELECT * FROM external_updates
|
|
1097
|
+
WHERE game_id = ? AND status = 'pending'
|
|
1098
|
+
ORDER BY
|
|
1099
|
+
CASE priority
|
|
1100
|
+
WHEN 'urgent' THEN 1
|
|
1101
|
+
WHEN 'high' THEN 2
|
|
1102
|
+
WHEN 'normal' THEN 3
|
|
1103
|
+
WHEN 'low' THEN 4
|
|
1104
|
+
END,
|
|
1105
|
+
created_at DESC`)
|
|
1106
|
+
.all(gameId);
|
|
1107
|
+
const updates = rows.map((row) => ({
|
|
1108
|
+
id: row.id,
|
|
1109
|
+
gameId: row.game_id,
|
|
1110
|
+
sourceAgent: row.source_agent,
|
|
1111
|
+
sourceDescription: row.source_description,
|
|
1112
|
+
updateType: row.update_type,
|
|
1113
|
+
category: row.category,
|
|
1114
|
+
title: row.title,
|
|
1115
|
+
content: row.content,
|
|
1116
|
+
structuredData: row.structured_data
|
|
1117
|
+
? JSON.parse(row.structured_data)
|
|
1118
|
+
: null,
|
|
1119
|
+
targetEntityId: row.target_entity_id,
|
|
1120
|
+
targetEntityType: row.target_entity_type,
|
|
1121
|
+
priority: row.priority,
|
|
1122
|
+
status: row.status,
|
|
1123
|
+
createdAt: row.created_at,
|
|
1124
|
+
acknowledgedAt: row.acknowledged_at,
|
|
1125
|
+
appliedAt: row.applied_at,
|
|
1126
|
+
dmNotes: row.dm_notes,
|
|
1127
|
+
}));
|
|
1128
|
+
const urgentCount = updates.filter((u) => u.priority === "urgent").length;
|
|
1129
|
+
let suggestion = "";
|
|
1130
|
+
if (urgentCount > 0) {
|
|
1131
|
+
suggestion = `⚠️ ${urgentCount} URGENT update(s) require immediate attention!`;
|
|
1132
|
+
}
|
|
1133
|
+
else if (updates.length > 0) {
|
|
1134
|
+
suggestion = `${updates.length} pending update(s) from external agents. Review and incorporate as appropriate.`;
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
suggestion = "No pending updates from external agents.";
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
gameId,
|
|
1141
|
+
pendingCount: updates.length,
|
|
1142
|
+
urgentCount,
|
|
1143
|
+
updates,
|
|
1144
|
+
hasUrgent: urgentCount > 0,
|
|
1145
|
+
suggestion,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Acknowledge an update (mark as seen by DM).
|
|
1150
|
+
*/
|
|
1151
|
+
export function acknowledgeUpdate(updateId) {
|
|
1152
|
+
const db = getDatabase();
|
|
1153
|
+
const now = new Date().toISOString();
|
|
1154
|
+
db.prepare(`UPDATE external_updates SET status = 'acknowledged', acknowledged_at = ? WHERE id = ?`).run(now, updateId);
|
|
1155
|
+
return getExternalUpdate(updateId);
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Mark an update as applied (incorporated into the narrative).
|
|
1159
|
+
*/
|
|
1160
|
+
export function applyUpdate(updateId, dmNotes) {
|
|
1161
|
+
const db = getDatabase();
|
|
1162
|
+
const now = new Date().toISOString();
|
|
1163
|
+
db.prepare(`UPDATE external_updates
|
|
1164
|
+
SET status = 'applied', applied_at = ?, dm_notes = COALESCE(?, dm_notes)
|
|
1165
|
+
WHERE id = ?`).run(now, dmNotes || null, updateId);
|
|
1166
|
+
return getExternalUpdate(updateId);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Reject an update (not appropriate for the narrative).
|
|
1170
|
+
*/
|
|
1171
|
+
export function rejectUpdate(updateId, dmNotes) {
|
|
1172
|
+
const db = getDatabase();
|
|
1173
|
+
db.prepare(`UPDATE external_updates
|
|
1174
|
+
SET status = 'rejected', dm_notes = COALESCE(?, dm_notes)
|
|
1175
|
+
WHERE id = ?`).run(dmNotes || null, updateId);
|
|
1176
|
+
return getExternalUpdate(updateId);
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Get a specific external update by ID.
|
|
1180
|
+
*/
|
|
1181
|
+
export function getExternalUpdate(updateId) {
|
|
1182
|
+
const db = getDatabase();
|
|
1183
|
+
const row = db
|
|
1184
|
+
.prepare(`SELECT * FROM external_updates WHERE id = ?`)
|
|
1185
|
+
.get(updateId);
|
|
1186
|
+
if (!row)
|
|
1187
|
+
return null;
|
|
1188
|
+
return {
|
|
1189
|
+
id: row.id,
|
|
1190
|
+
gameId: row.game_id,
|
|
1191
|
+
sourceAgent: row.source_agent,
|
|
1192
|
+
sourceDescription: row.source_description,
|
|
1193
|
+
updateType: row.update_type,
|
|
1194
|
+
category: row.category,
|
|
1195
|
+
title: row.title,
|
|
1196
|
+
content: row.content,
|
|
1197
|
+
structuredData: row.structured_data
|
|
1198
|
+
? JSON.parse(row.structured_data)
|
|
1199
|
+
: null,
|
|
1200
|
+
targetEntityId: row.target_entity_id,
|
|
1201
|
+
targetEntityType: row.target_entity_type,
|
|
1202
|
+
priority: row.priority,
|
|
1203
|
+
status: row.status,
|
|
1204
|
+
createdAt: row.created_at,
|
|
1205
|
+
acknowledgedAt: row.acknowledged_at,
|
|
1206
|
+
appliedAt: row.applied_at,
|
|
1207
|
+
dmNotes: row.dm_notes,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* List all updates for a game with optional status filter.
|
|
1212
|
+
*/
|
|
1213
|
+
export function listExternalUpdates(gameId, status) {
|
|
1214
|
+
const db = getDatabase();
|
|
1215
|
+
let query = `SELECT * FROM external_updates WHERE game_id = ?`;
|
|
1216
|
+
const params = [gameId];
|
|
1217
|
+
if (status) {
|
|
1218
|
+
query += ` AND status = ?`;
|
|
1219
|
+
params.push(status);
|
|
1220
|
+
}
|
|
1221
|
+
query += ` ORDER BY created_at DESC`;
|
|
1222
|
+
const rows = db.prepare(query).all(...params);
|
|
1223
|
+
return rows.map((row) => ({
|
|
1224
|
+
id: row.id,
|
|
1225
|
+
gameId: row.game_id,
|
|
1226
|
+
sourceAgent: row.source_agent,
|
|
1227
|
+
sourceDescription: row.source_description,
|
|
1228
|
+
updateType: row.update_type,
|
|
1229
|
+
category: row.category,
|
|
1230
|
+
title: row.title,
|
|
1231
|
+
content: row.content,
|
|
1232
|
+
structuredData: row.structured_data
|
|
1233
|
+
? JSON.parse(row.structured_data)
|
|
1234
|
+
: null,
|
|
1235
|
+
targetEntityId: row.target_entity_id,
|
|
1236
|
+
targetEntityType: row.target_entity_type,
|
|
1237
|
+
priority: row.priority,
|
|
1238
|
+
status: row.status,
|
|
1239
|
+
createdAt: row.created_at,
|
|
1240
|
+
acknowledgedAt: row.acknowledged_at,
|
|
1241
|
+
appliedAt: row.applied_at,
|
|
1242
|
+
dmNotes: row.dm_notes,
|
|
1243
|
+
}));
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Delete an external update.
|
|
1247
|
+
*/
|
|
1248
|
+
export function deleteExternalUpdate(updateId) {
|
|
1249
|
+
const db = getDatabase();
|
|
1250
|
+
const result = db
|
|
1251
|
+
.prepare(`DELETE FROM external_updates WHERE id = ?`)
|
|
1252
|
+
.run(updateId);
|
|
1253
|
+
return result.changes > 0;
|
|
1254
|
+
}
|