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,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
|
+
};
|