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.
Files changed (167) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +79 -0
  3. package/dist/__tests__/engineVocabulary.test.d.ts +1 -0
  4. package/dist/__tests__/engineVocabulary.test.js +147 -0
  5. package/dist/db/__tests__/connection.test.d.ts +1 -0
  6. package/dist/db/__tests__/connection.test.js +72 -0
  7. package/dist/db/__tests__/testDb.d.ts +33 -0
  8. package/dist/db/__tests__/testDb.js +41 -0
  9. package/dist/db/connection.d.ts +22 -0
  10. package/dist/db/connection.js +107 -0
  11. package/dist/db/schema.d.ts +1 -0
  12. package/dist/db/schema.js +725 -0
  13. package/dist/events/emitter.d.ts +22 -0
  14. package/dist/events/emitter.js +71 -0
  15. package/dist/http/server.d.ts +3 -0
  16. package/dist/http/server.js +649 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +92 -0
  19. package/dist/register/abilities.d.ts +2 -0
  20. package/dist/register/abilities.js +165 -0
  21. package/dist/register/audio.d.ts +2 -0
  22. package/dist/register/audio.js +326 -0
  23. package/dist/register/batch.d.ts +2 -0
  24. package/dist/register/batch.js +343 -0
  25. package/dist/register/character.d.ts +2 -0
  26. package/dist/register/character.js +324 -0
  27. package/dist/register/combat.d.ts +2 -0
  28. package/dist/register/combat.js +207 -0
  29. package/dist/register/core.d.ts +2 -0
  30. package/dist/register/core.js +1040 -0
  31. package/dist/register/display.d.ts +2 -0
  32. package/dist/register/display.js +263 -0
  33. package/dist/register/factions.d.ts +2 -0
  34. package/dist/register/factions.js +186 -0
  35. package/dist/register/images.d.ts +2 -0
  36. package/dist/register/images.js +400 -0
  37. package/dist/register/inventory.d.ts +2 -0
  38. package/dist/register/inventory.js +115 -0
  39. package/dist/register/mcp-prompts.d.ts +2 -0
  40. package/dist/register/mcp-prompts.js +684 -0
  41. package/dist/register/mcp-resources.d.ts +2 -0
  42. package/dist/register/mcp-resources.js +335 -0
  43. package/dist/register/narrative.d.ts +2 -0
  44. package/dist/register/narrative.js +242 -0
  45. package/dist/register/notes.d.ts +2 -0
  46. package/dist/register/notes.js +170 -0
  47. package/dist/register/pause.d.ts +2 -0
  48. package/dist/register/pause.js +580 -0
  49. package/dist/register/quests.d.ts +2 -0
  50. package/dist/register/quests.js +118 -0
  51. package/dist/register/relationships.d.ts +2 -0
  52. package/dist/register/relationships.js +147 -0
  53. package/dist/register/resources.d.ts +2 -0
  54. package/dist/register/resources.js +277 -0
  55. package/dist/register/secrets.d.ts +2 -0
  56. package/dist/register/secrets.js +192 -0
  57. package/dist/register/status.d.ts +2 -0
  58. package/dist/register/status.js +130 -0
  59. package/dist/register/tables.d.ts +2 -0
  60. package/dist/register/tables.js +146 -0
  61. package/dist/register/tags.d.ts +2 -0
  62. package/dist/register/tags.js +114 -0
  63. package/dist/register/time.d.ts +2 -0
  64. package/dist/register/time.js +281 -0
  65. package/dist/register/world.d.ts +2 -0
  66. package/dist/register/world.js +127 -0
  67. package/dist/schemas/index.d.ts +921 -0
  68. package/dist/schemas/index.js +121 -0
  69. package/dist/test-setup.d.ts +1 -0
  70. package/dist/test-setup.js +13 -0
  71. package/dist/tools/__tests__/audio.test.d.ts +1 -0
  72. package/dist/tools/__tests__/audio.test.js +59 -0
  73. package/dist/tools/__tests__/conserved.test.d.ts +1 -0
  74. package/dist/tools/__tests__/conserved.test.js +488 -0
  75. package/dist/tools/__tests__/constraint.test.d.ts +1 -0
  76. package/dist/tools/__tests__/constraint.test.js +212 -0
  77. package/dist/tools/__tests__/expiry-consequences.test.d.ts +1 -0
  78. package/dist/tools/__tests__/expiry-consequences.test.js +110 -0
  79. package/dist/tools/__tests__/images.test.d.ts +1 -0
  80. package/dist/tools/__tests__/images.test.js +59 -0
  81. package/dist/tools/__tests__/relationship.test.d.ts +1 -0
  82. package/dist/tools/__tests__/relationship.test.js +132 -0
  83. package/dist/tools/__tests__/resource-constraints.test.d.ts +1 -0
  84. package/dist/tools/__tests__/resource-constraints.test.js +131 -0
  85. package/dist/tools/__tests__/resource.test.d.ts +1 -0
  86. package/dist/tools/__tests__/resource.test.js +190 -0
  87. package/dist/tools/__tests__/time.test.d.ts +1 -0
  88. package/dist/tools/__tests__/time.test.js +404 -0
  89. package/dist/tools/__tests__/timers.test.d.ts +1 -0
  90. package/dist/tools/__tests__/timers.test.js +426 -0
  91. package/dist/tools/__tests__/world.test.d.ts +1 -0
  92. package/dist/tools/__tests__/world.test.js +70 -0
  93. package/dist/tools/ability.d.ts +48 -0
  94. package/dist/tools/ability.js +238 -0
  95. package/dist/tools/audio.d.ts +24 -0
  96. package/dist/tools/audio.js +365 -0
  97. package/dist/tools/character.d.ts +70 -0
  98. package/dist/tools/character.js +309 -0
  99. package/dist/tools/combat.d.ts +13 -0
  100. package/dist/tools/combat.js +195 -0
  101. package/dist/tools/constraint.d.ts +132 -0
  102. package/dist/tools/constraint.js +269 -0
  103. package/dist/tools/dice.d.ts +23 -0
  104. package/dist/tools/dice.js +111 -0
  105. package/dist/tools/display.d.ts +120 -0
  106. package/dist/tools/display.js +528 -0
  107. package/dist/tools/faction.d.ts +61 -0
  108. package/dist/tools/faction.js +269 -0
  109. package/dist/tools/game.d.ts +96 -0
  110. package/dist/tools/game.js +526 -0
  111. package/dist/tools/image-prompt.d.ts +49 -0
  112. package/dist/tools/image-prompt.js +479 -0
  113. package/dist/tools/images.d.ts +47 -0
  114. package/dist/tools/images.js +449 -0
  115. package/dist/tools/inventory.d.ts +20 -0
  116. package/dist/tools/inventory.js +145 -0
  117. package/dist/tools/narrative.d.ts +58 -0
  118. package/dist/tools/narrative.js +237 -0
  119. package/dist/tools/notes.d.ts +41 -0
  120. package/dist/tools/notes.js +220 -0
  121. package/dist/tools/pause.d.ts +110 -0
  122. package/dist/tools/pause.js +1254 -0
  123. package/dist/tools/quest.d.ts +34 -0
  124. package/dist/tools/quest.js +164 -0
  125. package/dist/tools/relationship.d.ts +74 -0
  126. package/dist/tools/relationship.js +324 -0
  127. package/dist/tools/resource.d.ts +93 -0
  128. package/dist/tools/resource.js +374 -0
  129. package/dist/tools/rules.d.ts +4 -0
  130. package/dist/tools/rules.js +30 -0
  131. package/dist/tools/secrets.d.ts +49 -0
  132. package/dist/tools/secrets.js +195 -0
  133. package/dist/tools/status.d.ts +36 -0
  134. package/dist/tools/status.js +218 -0
  135. package/dist/tools/tables.d.ts +33 -0
  136. package/dist/tools/tables.js +209 -0
  137. package/dist/tools/tags.d.ts +52 -0
  138. package/dist/tools/tags.js +176 -0
  139. package/dist/tools/time.d.ts +33 -0
  140. package/dist/tools/time.js +276 -0
  141. package/dist/tools/timers.d.ts +41 -0
  142. package/dist/tools/timers.js +215 -0
  143. package/dist/tools/world.d.ts +78 -0
  144. package/dist/tools/world.js +331 -0
  145. package/dist/types/index.d.ts +969 -0
  146. package/dist/types/index.js +1 -0
  147. package/dist/utils/__tests__/json.test.d.ts +1 -0
  148. package/dist/utils/__tests__/json.test.js +55 -0
  149. package/dist/utils/__tests__/validation.test.d.ts +1 -0
  150. package/dist/utils/__tests__/validation.test.js +90 -0
  151. package/dist/utils/errors.d.ts +44 -0
  152. package/dist/utils/errors.js +121 -0
  153. package/dist/utils/json.d.ts +9 -0
  154. package/dist/utils/json.js +23 -0
  155. package/dist/utils/logger.d.ts +7 -0
  156. package/dist/utils/logger.js +50 -0
  157. package/dist/utils/output-schemas.d.ts +594 -0
  158. package/dist/utils/output-schemas.js +331 -0
  159. package/dist/utils/tool-annotations.d.ts +147 -0
  160. package/dist/utils/tool-annotations.js +98 -0
  161. package/dist/utils/validation.d.ts +34 -0
  162. package/dist/utils/validation.js +52 -0
  163. package/dist/utils/verbosity.d.ts +57 -0
  164. package/dist/utils/verbosity.js +67 -0
  165. package/dist/utils/webui.d.ts +20 -0
  166. package/dist/utils/webui.js +35 -0
  167. package/package.json +75 -0
@@ -0,0 +1,36 @@
1
+ import type { StatusEffect } from "../types/index.js";
2
+ export declare function applyStatusEffect(params: {
3
+ gameId: string;
4
+ targetId: string;
5
+ name: string;
6
+ description?: string;
7
+ effectType?: "buff" | "debuff" | "neutral";
8
+ duration?: number;
9
+ stacks?: number;
10
+ maxStacks?: number;
11
+ effects?: Record<string, number>;
12
+ sourceId?: string;
13
+ sourceType?: string;
14
+ expiresAt?: string;
15
+ }): StatusEffect;
16
+ export declare function getStatusEffect(id: string): StatusEffect | null;
17
+ export declare function removeStatusEffect(id: string): boolean;
18
+ export declare function listStatusEffects(targetId: string, filter?: {
19
+ effectType?: "buff" | "debuff" | "neutral";
20
+ }): StatusEffect[];
21
+ export interface TickResult {
22
+ expired: StatusEffect[];
23
+ remaining: StatusEffect[];
24
+ }
25
+ export declare function tickDurations(gameId: string, amount?: number): TickResult;
26
+ export declare function modifyStacks(id: string, delta: number): StatusEffect | null;
27
+ export declare function clearEffects(targetId: string, filter?: {
28
+ effectType?: "buff" | "debuff" | "neutral";
29
+ name?: string;
30
+ }): number;
31
+ export interface EffectiveModifiers {
32
+ targetId: string;
33
+ modifiers: Record<string, number>;
34
+ effects: StatusEffect[];
35
+ }
36
+ export declare function getEffectiveModifiers(targetId: string): EffectiveModifiers;
@@ -0,0 +1,218 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase } from "../db/connection.js";
3
+ import { safeJsonParse } from "../utils/json.js";
4
+ import { validateGameExists } from "./game.js";
5
+ export function applyStatusEffect(params) {
6
+ // Validate game exists to prevent orphaned records
7
+ validateGameExists(params.gameId);
8
+ const db = getDatabase();
9
+ const now = new Date().toISOString();
10
+ // Check if effect already exists on target
11
+ const existing = db.prepare(`
12
+ SELECT * FROM status_effects WHERE game_id = ? AND target_id = ? AND name = ?
13
+ `).get(params.gameId, params.targetId, params.name);
14
+ if (existing) {
15
+ // Stack the effect - use existing maxStacks (can't change stack limit after creation)
16
+ const currentStacks = existing.stacks;
17
+ const existingMaxStacks = existing.max_stacks;
18
+ const newStacks = existingMaxStacks !== null
19
+ ? Math.min(currentStacks + (params.stacks ?? 1), existingMaxStacks)
20
+ : currentStacks + (params.stacks ?? 1);
21
+ // Refresh duration if provided
22
+ const newDuration = params.duration ?? existing.duration;
23
+ db.prepare(`
24
+ UPDATE status_effects SET stacks = ?, duration = ? WHERE id = ?
25
+ `).run(newStacks, newDuration, existing.id);
26
+ const updated = getStatusEffect(existing.id);
27
+ if (!updated) {
28
+ throw new Error(`Failed to retrieve updated status effect '${existing.id}'`);
29
+ }
30
+ return updated;
31
+ }
32
+ // Create new effect
33
+ const id = uuidv4();
34
+ db.prepare(`
35
+ INSERT INTO status_effects (id, game_id, target_id, name, description, effect_type, duration, stacks, max_stacks, effects, source_id, source_type, expires_at, created_at)
36
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
37
+ `).run(id, params.gameId, params.targetId, params.name, params.description || "", params.effectType || null, params.duration ?? null, params.stacks ?? 1, params.maxStacks ?? null, JSON.stringify(params.effects || {}), params.sourceId || null, params.sourceType || null, params.expiresAt || null, now);
38
+ return {
39
+ id,
40
+ gameId: params.gameId,
41
+ targetId: params.targetId,
42
+ name: params.name,
43
+ description: params.description || "",
44
+ effectType: params.effectType || null,
45
+ duration: params.duration ?? null,
46
+ stacks: params.stacks ?? 1,
47
+ maxStacks: params.maxStacks ?? null,
48
+ effects: params.effects || {},
49
+ sourceId: params.sourceId || null,
50
+ sourceType: params.sourceType || null,
51
+ expiresAt: params.expiresAt || null,
52
+ createdAt: now,
53
+ };
54
+ }
55
+ export function getStatusEffect(id) {
56
+ const db = getDatabase();
57
+ const row = db.prepare(`SELECT * FROM status_effects WHERE id = ?`).get(id);
58
+ if (!row)
59
+ return null;
60
+ return {
61
+ id: row.id,
62
+ gameId: row.game_id,
63
+ targetId: row.target_id,
64
+ name: row.name,
65
+ description: row.description,
66
+ effectType: row.effect_type,
67
+ duration: row.duration,
68
+ stacks: row.stacks,
69
+ maxStacks: row.max_stacks,
70
+ effects: safeJsonParse(row.effects || "{}", {}),
71
+ sourceId: row.source_id,
72
+ sourceType: row.source_type,
73
+ expiresAt: row.expires_at,
74
+ createdAt: row.created_at,
75
+ };
76
+ }
77
+ export function removeStatusEffect(id) {
78
+ const db = getDatabase();
79
+ const result = db.prepare(`DELETE FROM status_effects WHERE id = ?`).run(id);
80
+ return result.changes > 0;
81
+ }
82
+ export function listStatusEffects(targetId, filter) {
83
+ const db = getDatabase();
84
+ let query = `SELECT * FROM status_effects WHERE target_id = ?`;
85
+ const params = [targetId];
86
+ if (filter?.effectType) {
87
+ query += ` AND effect_type = ?`;
88
+ params.push(filter.effectType);
89
+ }
90
+ query += ` ORDER BY created_at`;
91
+ const rows = db.prepare(query).all(...params);
92
+ return rows.map(row => ({
93
+ id: row.id,
94
+ gameId: row.game_id,
95
+ targetId: row.target_id,
96
+ name: row.name,
97
+ description: row.description,
98
+ effectType: row.effect_type,
99
+ duration: row.duration,
100
+ stacks: row.stacks,
101
+ maxStacks: row.max_stacks,
102
+ effects: safeJsonParse(row.effects || "{}", {}),
103
+ sourceId: row.source_id,
104
+ sourceType: row.source_type,
105
+ expiresAt: row.expires_at,
106
+ createdAt: row.created_at,
107
+ }));
108
+ }
109
+ // NOTE on expiry consequences: status effects do NOT support the declared
110
+ // on-expiry consequence mechanism that scheduled_events (time.ts) and
111
+ // timers (timers.ts) support. tickDurations() only decrements duration and
112
+ // deletes the row at zero, exactly as before. Status effects were left out
113
+ // deliberately -- unifying all three expiry systems behind one consequence
114
+ // mechanism is a larger refactor than this feature warrants; see the
115
+ // scheduled_events/timers implementations for the supported pattern if this
116
+ // gets extended later.
117
+ export function tickDurations(gameId, amount = 1) {
118
+ const db = getDatabase();
119
+ // Get all effects with duration
120
+ const effects = db.prepare(`
121
+ SELECT * FROM status_effects WHERE game_id = ? AND duration IS NOT NULL
122
+ `).all(gameId);
123
+ const expired = [];
124
+ const remaining = [];
125
+ for (const row of effects) {
126
+ const currentDuration = row.duration;
127
+ const newDuration = currentDuration - amount;
128
+ if (newDuration <= 0) {
129
+ // Effect expired
130
+ db.prepare(`DELETE FROM status_effects WHERE id = ?`).run(row.id);
131
+ expired.push({
132
+ id: row.id,
133
+ gameId: row.game_id,
134
+ targetId: row.target_id,
135
+ name: row.name,
136
+ description: row.description,
137
+ effectType: row.effect_type,
138
+ duration: 0,
139
+ stacks: row.stacks,
140
+ maxStacks: row.max_stacks,
141
+ effects: safeJsonParse(row.effects || "{}", {}),
142
+ sourceId: row.source_id,
143
+ sourceType: row.source_type,
144
+ expiresAt: row.expires_at,
145
+ createdAt: row.created_at,
146
+ });
147
+ }
148
+ else {
149
+ // Update duration
150
+ db.prepare(`UPDATE status_effects SET duration = ? WHERE id = ?`).run(newDuration, row.id);
151
+ remaining.push({
152
+ id: row.id,
153
+ gameId: row.game_id,
154
+ targetId: row.target_id,
155
+ name: row.name,
156
+ description: row.description,
157
+ effectType: row.effect_type,
158
+ duration: newDuration,
159
+ stacks: row.stacks,
160
+ maxStacks: row.max_stacks,
161
+ effects: safeJsonParse(row.effects || "{}", {}),
162
+ sourceId: row.source_id,
163
+ sourceType: row.source_type,
164
+ expiresAt: row.expires_at,
165
+ createdAt: row.created_at,
166
+ });
167
+ }
168
+ }
169
+ return { expired, remaining };
170
+ }
171
+ export function modifyStacks(id, delta) {
172
+ const db = getDatabase();
173
+ const effect = getStatusEffect(id);
174
+ if (!effect)
175
+ return null;
176
+ const newStacks = effect.stacks + delta;
177
+ if (newStacks <= 0) {
178
+ // Remove effect
179
+ db.prepare(`DELETE FROM status_effects WHERE id = ?`).run(id);
180
+ return { ...effect, stacks: 0 };
181
+ }
182
+ const finalStacks = effect.maxStacks !== null
183
+ ? Math.min(newStacks, effect.maxStacks)
184
+ : newStacks;
185
+ db.prepare(`UPDATE status_effects SET stacks = ? WHERE id = ?`).run(finalStacks, id);
186
+ return { ...effect, stacks: finalStacks };
187
+ }
188
+ export function clearEffects(targetId, filter) {
189
+ const db = getDatabase();
190
+ let query = `DELETE FROM status_effects WHERE target_id = ?`;
191
+ const params = [targetId];
192
+ if (filter?.effectType) {
193
+ query += ` AND effect_type = ?`;
194
+ params.push(filter.effectType);
195
+ }
196
+ if (filter?.name) {
197
+ query += ` AND name = ?`;
198
+ params.push(filter.name);
199
+ }
200
+ const result = db.prepare(query).run(...params);
201
+ return result.changes;
202
+ }
203
+ export function getEffectiveModifiers(targetId) {
204
+ const effects = listStatusEffects(targetId);
205
+ const modifiers = {};
206
+ for (const effect of effects) {
207
+ for (const [key, value] of Object.entries(effect.effects)) {
208
+ // Multiply by stacks
209
+ const totalValue = value * effect.stacks;
210
+ modifiers[key] = (modifiers[key] || 0) + totalValue;
211
+ }
212
+ }
213
+ return {
214
+ targetId,
215
+ modifiers,
216
+ effects,
217
+ };
218
+ }
@@ -0,0 +1,33 @@
1
+ import type { RandomTable, TableEntry, TableRollResult } from "../types/index.js";
2
+ export declare function createTable(params: {
3
+ gameId: string;
4
+ name: string;
5
+ description?: string;
6
+ category?: string;
7
+ entries?: TableEntry[];
8
+ rollExpression?: string;
9
+ }): RandomTable;
10
+ export declare function getTable(id: string): RandomTable | null;
11
+ export declare function updateTable(id: string, updates: {
12
+ name?: string;
13
+ description?: string;
14
+ category?: string | null;
15
+ entries?: TableEntry[];
16
+ rollExpression?: string;
17
+ }): RandomTable | null;
18
+ export declare function deleteTable(id: string): boolean;
19
+ export declare function listTables(gameId: string, category?: string): RandomTable[];
20
+ export declare function rollTable(tableId: string, modifier?: number): TableRollResult | null;
21
+ /**
22
+ * Modify table entries - add and/or remove entries in a single call.
23
+ */
24
+ export declare function modifyTableEntries(tableId: string, params: {
25
+ add?: TableEntry[];
26
+ remove?: number[];
27
+ }): {
28
+ table: RandomTable;
29
+ added: number;
30
+ removed: number;
31
+ invalidIndices?: number[];
32
+ } | null;
33
+ export declare function createSimpleTable(gameId: string, name: string, results: string[], category?: string): RandomTable;
@@ -0,0 +1,209 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase } from "../db/connection.js";
3
+ import { safeJsonParse } from "../utils/json.js";
4
+ import { validateGameExists } from "./game.js";
5
+ import { roll } from "./dice.js";
6
+ export function createTable(params) {
7
+ // Validate game exists to prevent orphaned records
8
+ validateGameExists(params.gameId);
9
+ const db = getDatabase();
10
+ const id = uuidv4();
11
+ const now = new Date().toISOString();
12
+ const entries = params.entries || [];
13
+ const rollExpression = params.rollExpression || "1d100";
14
+ db.prepare(`
15
+ INSERT INTO random_tables (id, game_id, name, description, category, entries, roll_expression, created_at)
16
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
17
+ `).run(id, params.gameId, params.name, params.description || "", params.category || null, JSON.stringify(entries), rollExpression, now);
18
+ return {
19
+ id,
20
+ gameId: params.gameId,
21
+ name: params.name,
22
+ description: params.description || "",
23
+ category: params.category || null,
24
+ entries,
25
+ rollExpression,
26
+ createdAt: now,
27
+ };
28
+ }
29
+ export function getTable(id) {
30
+ const db = getDatabase();
31
+ const row = db.prepare(`SELECT * FROM random_tables WHERE id = ?`).get(id);
32
+ if (!row)
33
+ return null;
34
+ return {
35
+ id: row.id,
36
+ gameId: row.game_id,
37
+ name: row.name,
38
+ description: row.description || "",
39
+ category: row.category,
40
+ entries: safeJsonParse(row.entries, []),
41
+ rollExpression: row.roll_expression,
42
+ createdAt: row.created_at,
43
+ };
44
+ }
45
+ export function updateTable(id, updates) {
46
+ const db = getDatabase();
47
+ const current = getTable(id);
48
+ if (!current)
49
+ return null;
50
+ const newName = updates.name ?? current.name;
51
+ const newDescription = updates.description ?? current.description;
52
+ const newCategory = updates.category !== undefined ? updates.category : current.category;
53
+ const newEntries = updates.entries ?? current.entries;
54
+ const newRollExpression = updates.rollExpression ?? current.rollExpression;
55
+ db.prepare(`
56
+ UPDATE random_tables
57
+ SET name = ?, description = ?, category = ?, entries = ?, roll_expression = ?
58
+ WHERE id = ?
59
+ `).run(newName, newDescription, newCategory, JSON.stringify(newEntries), newRollExpression, id);
60
+ return {
61
+ ...current,
62
+ name: newName,
63
+ description: newDescription,
64
+ category: newCategory,
65
+ entries: newEntries,
66
+ rollExpression: newRollExpression,
67
+ };
68
+ }
69
+ export function deleteTable(id) {
70
+ const db = getDatabase();
71
+ const result = db.prepare(`DELETE FROM random_tables WHERE id = ?`).run(id);
72
+ return result.changes > 0;
73
+ }
74
+ export function listTables(gameId, category) {
75
+ const db = getDatabase();
76
+ let query = `SELECT * FROM random_tables WHERE game_id = ?`;
77
+ const params = [gameId];
78
+ if (category) {
79
+ query += ` AND category = ?`;
80
+ params.push(category);
81
+ }
82
+ query += ` ORDER BY name`;
83
+ const rows = db.prepare(query).all(...params);
84
+ return rows.map(row => ({
85
+ id: row.id,
86
+ gameId: row.game_id,
87
+ name: row.name,
88
+ description: row.description || "",
89
+ category: row.category,
90
+ entries: safeJsonParse(row.entries, []),
91
+ rollExpression: row.roll_expression,
92
+ createdAt: row.created_at,
93
+ }));
94
+ }
95
+ export function rollTable(tableId, modifier = 0) {
96
+ const table = getTable(tableId);
97
+ if (!table || table.entries.length === 0)
98
+ return null;
99
+ const diceRoll = roll(table.rollExpression);
100
+ const rollTotal = diceRoll.total + modifier;
101
+ // Find matching entry
102
+ let matchedEntry = null;
103
+ for (const entry of table.entries) {
104
+ if (rollTotal >= entry.minRoll && rollTotal <= entry.maxRoll) {
105
+ matchedEntry = entry;
106
+ break;
107
+ }
108
+ }
109
+ // If no range match, try weighted random
110
+ if (!matchedEntry) {
111
+ const weightedEntries = table.entries.filter(e => e.weight !== undefined && e.weight > 0);
112
+ if (weightedEntries.length > 0) {
113
+ const totalWeight = weightedEntries.reduce((sum, e) => sum + (e.weight || 0), 0);
114
+ // Use cumulative comparison to avoid floating-point precision issues
115
+ const target = Math.random() * totalWeight;
116
+ let cumulative = 0;
117
+ for (const entry of weightedEntries) {
118
+ cumulative += entry.weight || 0;
119
+ if (target < cumulative) {
120
+ matchedEntry = entry;
121
+ break;
122
+ }
123
+ }
124
+ // Handle edge case where target equals totalWeight due to floating-point
125
+ if (!matchedEntry && weightedEntries.length > 0) {
126
+ matchedEntry = weightedEntries[weightedEntries.length - 1];
127
+ }
128
+ }
129
+ }
130
+ // If still no match, this indicates a table configuration error
131
+ if (!matchedEntry) {
132
+ throw new Error(`Table '${table.name}' (${tableId}) has no entry matching roll ${rollTotal} and no weighted entries. Check table configuration.`);
133
+ }
134
+ const result = {
135
+ table,
136
+ roll: diceRoll,
137
+ entry: matchedEntry,
138
+ result: matchedEntry.result,
139
+ };
140
+ // Handle subtable
141
+ if (matchedEntry.subtable) {
142
+ const subtableResult = rollTable(matchedEntry.subtable, 0);
143
+ if (subtableResult) {
144
+ result.subtableResults = [subtableResult];
145
+ }
146
+ }
147
+ return result;
148
+ }
149
+ /**
150
+ * Modify table entries - add and/or remove entries in a single call.
151
+ */
152
+ export function modifyTableEntries(tableId, params) {
153
+ const table = getTable(tableId);
154
+ if (!table)
155
+ return null;
156
+ let entries = [...table.entries];
157
+ let removedCount = 0;
158
+ const invalidIndices = [];
159
+ // Remove entries first (process in reverse order to maintain indices)
160
+ if (params.remove && params.remove.length > 0) {
161
+ const uniqueIndices = [...new Set(params.remove)];
162
+ // Track invalid indices
163
+ for (const idx of uniqueIndices) {
164
+ if (idx < 0 || idx >= entries.length) {
165
+ invalidIndices.push(idx);
166
+ }
167
+ }
168
+ const indicesToRemove = uniqueIndices
169
+ .filter(i => i >= 0 && i < entries.length)
170
+ .sort((a, b) => b - a); // Sort descending
171
+ for (const idx of indicesToRemove) {
172
+ entries.splice(idx, 1);
173
+ removedCount++;
174
+ }
175
+ }
176
+ // Add new entries
177
+ const addedCount = params.add?.length || 0;
178
+ if (params.add) {
179
+ entries = [...entries, ...params.add];
180
+ }
181
+ const updated = updateTable(tableId, { entries });
182
+ if (!updated)
183
+ return null;
184
+ const result = {
185
+ table: updated,
186
+ added: addedCount,
187
+ removed: removedCount,
188
+ };
189
+ if (invalidIndices.length > 0) {
190
+ result.invalidIndices = invalidIndices;
191
+ }
192
+ return result;
193
+ }
194
+ // Helper to create a simple d6 table quickly
195
+ export function createSimpleTable(gameId, name, results, category) {
196
+ const dieSize = results.length;
197
+ const entries = results.map((result, i) => ({
198
+ minRoll: i + 1,
199
+ maxRoll: i + 1,
200
+ result,
201
+ }));
202
+ return createTable({
203
+ gameId,
204
+ name,
205
+ category,
206
+ entries,
207
+ rollExpression: `1d${dieSize}`,
208
+ });
209
+ }
@@ -0,0 +1,52 @@
1
+ import type { Tag } from "../types/index.js";
2
+ export declare function addTag(params: {
3
+ gameId: string;
4
+ entityId: string;
5
+ entityType: string;
6
+ tag: string;
7
+ color?: string;
8
+ notes?: string;
9
+ }): Tag;
10
+ export declare function getTag(id: string): Tag | null;
11
+ export declare function removeTag(params: {
12
+ gameId: string;
13
+ entityId: string;
14
+ entityType: string;
15
+ tag: string;
16
+ }): boolean;
17
+ export interface TagCount {
18
+ tag: string;
19
+ count: number;
20
+ color: string | null;
21
+ }
22
+ export declare function listTags(gameId: string): TagCount[];
23
+ export declare function getEntityTags(entityId: string, entityType: string): Tag[];
24
+ export interface TaggedEntity {
25
+ entityId: string;
26
+ entityType: string;
27
+ tag: Tag;
28
+ }
29
+ export declare function findByTag(gameId: string, tag: string, entityType?: string): TaggedEntity[];
30
+ export declare function renameTag(gameId: string, oldTag: string, newTag: string): number;
31
+ export declare function deleteTagById(id: string): boolean;
32
+ /**
33
+ * Modify tags on an entity - add and/or remove in a single call.
34
+ * This consolidated function reduces the number of tool calls needed.
35
+ */
36
+ export declare function modifyTags(params: {
37
+ gameId: string;
38
+ entityId: string;
39
+ entityType: string;
40
+ add?: Array<{
41
+ tag: string;
42
+ color?: string;
43
+ notes?: string;
44
+ }>;
45
+ remove?: string[];
46
+ }): {
47
+ entityId: string;
48
+ entityType: string;
49
+ tags: string[];
50
+ added: string[];
51
+ removed: string[];
52
+ };