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,34 @@
|
|
|
1
|
+
import type { Quest, QuestObjective } from "../types/index.js";
|
|
2
|
+
export declare function createQuest(params: {
|
|
3
|
+
gameId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
objectives: Omit<QuestObjective, "id">[];
|
|
7
|
+
rewards?: string;
|
|
8
|
+
}): Quest;
|
|
9
|
+
export declare function getQuest(id: string): Quest | null;
|
|
10
|
+
export declare function updateQuest(id: string, updates: {
|
|
11
|
+
name?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
status?: Quest["status"];
|
|
14
|
+
rewards?: string;
|
|
15
|
+
}): Quest | null;
|
|
16
|
+
/**
|
|
17
|
+
* Modify quest objectives - add new objectives and/or complete existing ones in a single call.
|
|
18
|
+
*/
|
|
19
|
+
export declare function modifyObjectives(questId: string, params: {
|
|
20
|
+
add?: Array<{
|
|
21
|
+
description: string;
|
|
22
|
+
optional?: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
complete?: string[];
|
|
25
|
+
}): {
|
|
26
|
+
quest: Quest;
|
|
27
|
+
added: QuestObjective[];
|
|
28
|
+
completed: string[];
|
|
29
|
+
} | null;
|
|
30
|
+
export declare function deleteQuest(id: string): boolean;
|
|
31
|
+
export declare function listQuests(gameId: string, filter?: {
|
|
32
|
+
status?: Quest["status"];
|
|
33
|
+
}): Quest[];
|
|
34
|
+
export declare function getActiveQuests(gameId: string): Quest[];
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import { getDatabase } from "../db/connection.js";
|
|
3
|
+
import { safeJsonParse } from "../utils/json.js";
|
|
4
|
+
import { gameEvents } from "../events/emitter.js";
|
|
5
|
+
import { validateGameExists } from "./game.js";
|
|
6
|
+
export function createQuest(params) {
|
|
7
|
+
// Validate game exists to prevent orphaned records
|
|
8
|
+
validateGameExists(params.gameId);
|
|
9
|
+
const db = getDatabase();
|
|
10
|
+
const id = uuidv4();
|
|
11
|
+
const objectives = params.objectives.map((obj) => ({
|
|
12
|
+
id: uuidv4(),
|
|
13
|
+
description: obj.description,
|
|
14
|
+
completed: obj.completed || false,
|
|
15
|
+
optional: obj.optional,
|
|
16
|
+
}));
|
|
17
|
+
const stmt = db.prepare(`
|
|
18
|
+
INSERT INTO quests (id, game_id, name, description, objectives, status, rewards)
|
|
19
|
+
VALUES (?, ?, ?, ?, ?, 'active', ?)
|
|
20
|
+
`);
|
|
21
|
+
stmt.run(id, params.gameId, params.name, params.description, JSON.stringify(objectives), params.rewards || null);
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
gameId: params.gameId,
|
|
25
|
+
name: params.name,
|
|
26
|
+
description: params.description,
|
|
27
|
+
objectives,
|
|
28
|
+
status: "active",
|
|
29
|
+
rewards: params.rewards,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function getQuest(id) {
|
|
33
|
+
const db = getDatabase();
|
|
34
|
+
const stmt = db.prepare(`SELECT * FROM quests WHERE id = ?`);
|
|
35
|
+
const row = stmt.get(id);
|
|
36
|
+
if (!row)
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
id: row.id,
|
|
40
|
+
gameId: row.game_id,
|
|
41
|
+
name: row.name,
|
|
42
|
+
description: row.description,
|
|
43
|
+
objectives: safeJsonParse(row.objectives, []),
|
|
44
|
+
status: row.status,
|
|
45
|
+
rewards: row.rewards,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function updateQuest(id, updates) {
|
|
49
|
+
const db = getDatabase();
|
|
50
|
+
const current = getQuest(id);
|
|
51
|
+
if (!current)
|
|
52
|
+
return null;
|
|
53
|
+
const newName = updates.name ?? current.name;
|
|
54
|
+
const newDescription = updates.description ?? current.description;
|
|
55
|
+
const newStatus = updates.status ?? current.status;
|
|
56
|
+
const newRewards = updates.rewards ?? current.rewards;
|
|
57
|
+
const stmt = db.prepare(`
|
|
58
|
+
UPDATE quests SET name = ?, description = ?, status = ?, rewards = ? WHERE id = ?
|
|
59
|
+
`);
|
|
60
|
+
stmt.run(newName, newDescription, newStatus, newRewards || null, id);
|
|
61
|
+
const updated = {
|
|
62
|
+
...current,
|
|
63
|
+
name: newName,
|
|
64
|
+
description: newDescription,
|
|
65
|
+
status: newStatus,
|
|
66
|
+
rewards: newRewards,
|
|
67
|
+
};
|
|
68
|
+
// Emit realtime event
|
|
69
|
+
gameEvents.emit({
|
|
70
|
+
type: "quest:updated",
|
|
71
|
+
gameId: current.gameId,
|
|
72
|
+
entityId: id,
|
|
73
|
+
entityType: "quest",
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
data: { name: newName, status: newStatus },
|
|
76
|
+
});
|
|
77
|
+
return updated;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Modify quest objectives - add new objectives and/or complete existing ones in a single call.
|
|
81
|
+
*/
|
|
82
|
+
export function modifyObjectives(questId, params) {
|
|
83
|
+
const db = getDatabase();
|
|
84
|
+
const quest = getQuest(questId);
|
|
85
|
+
if (!quest)
|
|
86
|
+
return null;
|
|
87
|
+
const objectives = [...quest.objectives];
|
|
88
|
+
const added = [];
|
|
89
|
+
const completed = [];
|
|
90
|
+
// Complete objectives first
|
|
91
|
+
if (params.complete) {
|
|
92
|
+
for (const objectiveId of params.complete) {
|
|
93
|
+
const idx = objectives.findIndex((o) => o.id === objectiveId);
|
|
94
|
+
if (idx !== -1 && !objectives[idx].completed) {
|
|
95
|
+
objectives[idx] = { ...objectives[idx], completed: true };
|
|
96
|
+
completed.push(objectiveId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Add new objectives
|
|
101
|
+
if (params.add) {
|
|
102
|
+
for (const obj of params.add) {
|
|
103
|
+
const newObjective = {
|
|
104
|
+
id: uuidv4(),
|
|
105
|
+
description: obj.description,
|
|
106
|
+
completed: false,
|
|
107
|
+
optional: obj.optional,
|
|
108
|
+
};
|
|
109
|
+
objectives.push(newObjective);
|
|
110
|
+
added.push(newObjective);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Check if all required objectives are complete
|
|
114
|
+
const requiredComplete = objectives
|
|
115
|
+
.filter((o) => !o.optional)
|
|
116
|
+
.every((o) => o.completed);
|
|
117
|
+
const shouldComplete = requiredComplete && quest.status === "active";
|
|
118
|
+
const newStatus = shouldComplete ? "completed" : quest.status;
|
|
119
|
+
// Update objectives and status atomically in a single statement
|
|
120
|
+
const stmt = db.prepare(`UPDATE quests SET objectives = ?, status = ? WHERE id = ?`);
|
|
121
|
+
stmt.run(JSON.stringify(objectives), newStatus, questId);
|
|
122
|
+
// Emit event if quest was completed
|
|
123
|
+
if (shouldComplete) {
|
|
124
|
+
gameEvents.emit({
|
|
125
|
+
type: "quest:updated",
|
|
126
|
+
gameId: quest.gameId,
|
|
127
|
+
entityId: questId,
|
|
128
|
+
entityType: "quest",
|
|
129
|
+
timestamp: new Date().toISOString(),
|
|
130
|
+
data: { name: quest.name, status: "completed" },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const finalQuest = { ...quest, objectives, status: newStatus };
|
|
134
|
+
return { quest: finalQuest, added, completed };
|
|
135
|
+
}
|
|
136
|
+
export function deleteQuest(id) {
|
|
137
|
+
const db = getDatabase();
|
|
138
|
+
const stmt = db.prepare(`DELETE FROM quests WHERE id = ?`);
|
|
139
|
+
const result = stmt.run(id);
|
|
140
|
+
return result.changes > 0;
|
|
141
|
+
}
|
|
142
|
+
export function listQuests(gameId, filter) {
|
|
143
|
+
const db = getDatabase();
|
|
144
|
+
let query = `SELECT * FROM quests WHERE game_id = ?`;
|
|
145
|
+
const params = [gameId];
|
|
146
|
+
if (filter?.status) {
|
|
147
|
+
query += ` AND status = ?`;
|
|
148
|
+
params.push(filter.status);
|
|
149
|
+
}
|
|
150
|
+
const stmt = db.prepare(query);
|
|
151
|
+
const rows = stmt.all(...params);
|
|
152
|
+
return rows.map((row) => ({
|
|
153
|
+
id: row.id,
|
|
154
|
+
gameId: row.game_id,
|
|
155
|
+
name: row.name,
|
|
156
|
+
description: row.description,
|
|
157
|
+
objectives: safeJsonParse(row.objectives, []),
|
|
158
|
+
status: row.status,
|
|
159
|
+
rewards: row.rewards,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
export function getActiveQuests(gameId) {
|
|
163
|
+
return listQuests(gameId, { status: "active" });
|
|
164
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Relationship, RelationshipChange } from "../types/index.js";
|
|
2
|
+
export declare function createRelationship(params: {
|
|
3
|
+
gameId: string;
|
|
4
|
+
sourceId: string;
|
|
5
|
+
sourceType: string;
|
|
6
|
+
targetId: string;
|
|
7
|
+
targetType: string;
|
|
8
|
+
relationshipType: string;
|
|
9
|
+
value?: number;
|
|
10
|
+
label?: string;
|
|
11
|
+
notes?: string;
|
|
12
|
+
}): Relationship;
|
|
13
|
+
export declare function getRelationship(id: string): Relationship | null;
|
|
14
|
+
export declare function getRelationshipBetween(gameId: string, sourceId: string, targetId: string, relationshipType?: string): Relationship | null;
|
|
15
|
+
export declare function updateRelationship(id: string, updates: {
|
|
16
|
+
relationshipType?: string;
|
|
17
|
+
value?: number;
|
|
18
|
+
label?: string | null;
|
|
19
|
+
notes?: string;
|
|
20
|
+
}): Relationship | null;
|
|
21
|
+
export declare function modifyRelationship(params: {
|
|
22
|
+
relationshipId: string;
|
|
23
|
+
delta: number;
|
|
24
|
+
reason?: string;
|
|
25
|
+
minValue?: number;
|
|
26
|
+
maxValue?: number;
|
|
27
|
+
}): {
|
|
28
|
+
relationship: Relationship;
|
|
29
|
+
change: RelationshipChange;
|
|
30
|
+
} | null;
|
|
31
|
+
export declare function deleteRelationship(id: string): boolean;
|
|
32
|
+
export declare function listRelationships(gameId: string, filter?: {
|
|
33
|
+
entityId?: string;
|
|
34
|
+
sourceId?: string;
|
|
35
|
+
targetId?: string;
|
|
36
|
+
relationshipType?: string;
|
|
37
|
+
entityType?: string;
|
|
38
|
+
}): Relationship[];
|
|
39
|
+
export declare function getRelationshipHistory(relationshipId: string, limit?: number): RelationshipChange[];
|
|
40
|
+
/**
|
|
41
|
+
* Update a relationship value with optional metadata changes. Supports both direct set and delta modes.
|
|
42
|
+
* Logs to history when value changes.
|
|
43
|
+
*/
|
|
44
|
+
export declare function updateRelationshipValue(params: {
|
|
45
|
+
relationshipId: string;
|
|
46
|
+
mode: "delta" | "set";
|
|
47
|
+
value: number;
|
|
48
|
+
reason?: string;
|
|
49
|
+
minValue?: number;
|
|
50
|
+
maxValue?: number;
|
|
51
|
+
relationshipType?: string;
|
|
52
|
+
label?: string | null;
|
|
53
|
+
notes?: string;
|
|
54
|
+
}): {
|
|
55
|
+
relationship: Relationship;
|
|
56
|
+
change: RelationshipChange | null;
|
|
57
|
+
previousValue: number;
|
|
58
|
+
} | null;
|
|
59
|
+
export declare function getRelationshipLabel(value: number): string;
|
|
60
|
+
export declare function createBidirectionalRelationship(params: {
|
|
61
|
+
gameId: string;
|
|
62
|
+
entityA: {
|
|
63
|
+
id: string;
|
|
64
|
+
type: string;
|
|
65
|
+
};
|
|
66
|
+
entityB: {
|
|
67
|
+
id: string;
|
|
68
|
+
type: string;
|
|
69
|
+
};
|
|
70
|
+
relationshipType: string;
|
|
71
|
+
value?: number;
|
|
72
|
+
label?: string;
|
|
73
|
+
notes?: string;
|
|
74
|
+
}): [Relationship, Relationship];
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import { getDatabase, withTransaction } from "../db/connection.js";
|
|
3
|
+
import { validateGameExists } from "./game.js";
|
|
4
|
+
export function createRelationship(params) {
|
|
5
|
+
// Validate game exists to prevent orphaned records
|
|
6
|
+
validateGameExists(params.gameId);
|
|
7
|
+
const db = getDatabase();
|
|
8
|
+
const id = uuidv4();
|
|
9
|
+
const now = new Date().toISOString();
|
|
10
|
+
db.prepare(`
|
|
11
|
+
INSERT INTO relationships (id, game_id, source_id, source_type, target_id, target_type, relationship_type, value, label, notes, created_at, updated_at)
|
|
12
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
13
|
+
`).run(id, params.gameId, params.sourceId, params.sourceType, params.targetId, params.targetType, params.relationshipType, params.value ?? 0, params.label || null, params.notes || "", now, now);
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
gameId: params.gameId,
|
|
17
|
+
sourceId: params.sourceId,
|
|
18
|
+
sourceType: params.sourceType,
|
|
19
|
+
targetId: params.targetId,
|
|
20
|
+
targetType: params.targetType,
|
|
21
|
+
relationshipType: params.relationshipType,
|
|
22
|
+
value: params.value ?? 0,
|
|
23
|
+
label: params.label || null,
|
|
24
|
+
notes: params.notes || "",
|
|
25
|
+
createdAt: now,
|
|
26
|
+
updatedAt: now,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function getRelationship(id) {
|
|
30
|
+
const db = getDatabase();
|
|
31
|
+
const row = db.prepare(`SELECT * FROM relationships WHERE id = ?`).get(id);
|
|
32
|
+
if (!row)
|
|
33
|
+
return null;
|
|
34
|
+
return {
|
|
35
|
+
id: row.id,
|
|
36
|
+
gameId: row.game_id,
|
|
37
|
+
sourceId: row.source_id,
|
|
38
|
+
sourceType: row.source_type,
|
|
39
|
+
targetId: row.target_id,
|
|
40
|
+
targetType: row.target_type,
|
|
41
|
+
relationshipType: row.relationship_type,
|
|
42
|
+
value: row.value,
|
|
43
|
+
label: row.label,
|
|
44
|
+
notes: row.notes,
|
|
45
|
+
createdAt: row.created_at,
|
|
46
|
+
updatedAt: row.updated_at,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function getRelationshipBetween(gameId, sourceId, targetId, relationshipType) {
|
|
50
|
+
const db = getDatabase();
|
|
51
|
+
let query = `SELECT * FROM relationships WHERE game_id = ? AND source_id = ? AND target_id = ?`;
|
|
52
|
+
const params = [gameId, sourceId, targetId];
|
|
53
|
+
if (relationshipType) {
|
|
54
|
+
query += ` AND relationship_type = ?`;
|
|
55
|
+
params.push(relationshipType);
|
|
56
|
+
}
|
|
57
|
+
query += ` LIMIT 1`;
|
|
58
|
+
const row = db.prepare(query).get(...params);
|
|
59
|
+
if (!row)
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
id: row.id,
|
|
63
|
+
gameId: row.game_id,
|
|
64
|
+
sourceId: row.source_id,
|
|
65
|
+
sourceType: row.source_type,
|
|
66
|
+
targetId: row.target_id,
|
|
67
|
+
targetType: row.target_type,
|
|
68
|
+
relationshipType: row.relationship_type,
|
|
69
|
+
value: row.value,
|
|
70
|
+
label: row.label,
|
|
71
|
+
notes: row.notes,
|
|
72
|
+
createdAt: row.created_at,
|
|
73
|
+
updatedAt: row.updated_at,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function updateRelationship(id, updates) {
|
|
77
|
+
const db = getDatabase();
|
|
78
|
+
const current = getRelationship(id);
|
|
79
|
+
if (!current)
|
|
80
|
+
return null;
|
|
81
|
+
const now = new Date().toISOString();
|
|
82
|
+
const newType = updates.relationshipType ?? current.relationshipType;
|
|
83
|
+
const newValue = updates.value ?? current.value;
|
|
84
|
+
const newLabel = updates.label !== undefined ? updates.label : current.label;
|
|
85
|
+
const newNotes = updates.notes ?? current.notes;
|
|
86
|
+
db.prepare(`
|
|
87
|
+
UPDATE relationships
|
|
88
|
+
SET relationship_type = ?, value = ?, label = ?, notes = ?, updated_at = ?
|
|
89
|
+
WHERE id = ?
|
|
90
|
+
`).run(newType, newValue, newLabel, newNotes, now, id);
|
|
91
|
+
return {
|
|
92
|
+
...current,
|
|
93
|
+
relationshipType: newType,
|
|
94
|
+
value: newValue,
|
|
95
|
+
label: newLabel,
|
|
96
|
+
notes: newNotes,
|
|
97
|
+
updatedAt: now,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function logRelationshipChange(relationshipId, previousValue, newValue, reason) {
|
|
101
|
+
const db = getDatabase();
|
|
102
|
+
const id = uuidv4();
|
|
103
|
+
const now = new Date().toISOString();
|
|
104
|
+
db.prepare(`
|
|
105
|
+
INSERT INTO relationship_history (id, relationship_id, previous_value, new_value, reason, timestamp)
|
|
106
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
107
|
+
`).run(id, relationshipId, previousValue, newValue, reason, now);
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
relationshipId,
|
|
111
|
+
previousValue,
|
|
112
|
+
newValue,
|
|
113
|
+
reason,
|
|
114
|
+
timestamp: now,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function modifyRelationship(params) {
|
|
118
|
+
const db = getDatabase();
|
|
119
|
+
const relationship = getRelationship(params.relationshipId);
|
|
120
|
+
if (!relationship)
|
|
121
|
+
return null;
|
|
122
|
+
// Validate bounds if both are provided
|
|
123
|
+
if (params.minValue !== undefined && params.maxValue !== undefined && params.minValue > params.maxValue) {
|
|
124
|
+
throw new Error(`Invalid bounds: minValue (${params.minValue}) cannot be greater than maxValue (${params.maxValue})`);
|
|
125
|
+
}
|
|
126
|
+
const previousValue = relationship.value;
|
|
127
|
+
let newValue = previousValue + params.delta;
|
|
128
|
+
// Apply bounds (clamp to min first, then max to ensure max takes precedence)
|
|
129
|
+
if (params.minValue !== undefined) {
|
|
130
|
+
newValue = Math.max(newValue, params.minValue);
|
|
131
|
+
}
|
|
132
|
+
if (params.maxValue !== undefined) {
|
|
133
|
+
newValue = Math.min(newValue, params.maxValue);
|
|
134
|
+
}
|
|
135
|
+
const now = new Date().toISOString();
|
|
136
|
+
// The value update and its history row must land together -- otherwise a
|
|
137
|
+
// failure between the two leaves a changed relationship value with no
|
|
138
|
+
// audit trail explaining why it changed.
|
|
139
|
+
const change = withTransaction(() => {
|
|
140
|
+
db.prepare(`UPDATE relationships SET value = ?, updated_at = ? WHERE id = ?`)
|
|
141
|
+
.run(newValue, now, params.relationshipId);
|
|
142
|
+
return logRelationshipChange(params.relationshipId, previousValue, newValue, params.reason || null);
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
relationship: { ...relationship, value: newValue, updatedAt: now },
|
|
146
|
+
change,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function deleteRelationship(id) {
|
|
150
|
+
const db = getDatabase();
|
|
151
|
+
const result = db.prepare(`DELETE FROM relationships WHERE id = ?`).run(id);
|
|
152
|
+
return result.changes > 0;
|
|
153
|
+
}
|
|
154
|
+
export function listRelationships(gameId, filter) {
|
|
155
|
+
const db = getDatabase();
|
|
156
|
+
let query = `SELECT * FROM relationships WHERE game_id = ?`;
|
|
157
|
+
const params = [gameId];
|
|
158
|
+
if (filter?.entityId) {
|
|
159
|
+
query += ` AND (source_id = ? OR target_id = ?)`;
|
|
160
|
+
params.push(filter.entityId, filter.entityId);
|
|
161
|
+
}
|
|
162
|
+
if (filter?.sourceId) {
|
|
163
|
+
query += ` AND source_id = ?`;
|
|
164
|
+
params.push(filter.sourceId);
|
|
165
|
+
}
|
|
166
|
+
if (filter?.targetId) {
|
|
167
|
+
query += ` AND target_id = ?`;
|
|
168
|
+
params.push(filter.targetId);
|
|
169
|
+
}
|
|
170
|
+
if (filter?.relationshipType) {
|
|
171
|
+
query += ` AND relationship_type = ?`;
|
|
172
|
+
params.push(filter.relationshipType);
|
|
173
|
+
}
|
|
174
|
+
if (filter?.entityType) {
|
|
175
|
+
query += ` AND (source_type = ? OR target_type = ?)`;
|
|
176
|
+
params.push(filter.entityType, filter.entityType);
|
|
177
|
+
}
|
|
178
|
+
query += ` ORDER BY updated_at DESC`;
|
|
179
|
+
const rows = db.prepare(query).all(...params);
|
|
180
|
+
return rows.map(row => ({
|
|
181
|
+
id: row.id,
|
|
182
|
+
gameId: row.game_id,
|
|
183
|
+
sourceId: row.source_id,
|
|
184
|
+
sourceType: row.source_type,
|
|
185
|
+
targetId: row.target_id,
|
|
186
|
+
targetType: row.target_type,
|
|
187
|
+
relationshipType: row.relationship_type,
|
|
188
|
+
value: row.value,
|
|
189
|
+
label: row.label,
|
|
190
|
+
notes: row.notes,
|
|
191
|
+
createdAt: row.created_at,
|
|
192
|
+
updatedAt: row.updated_at,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
export function getRelationshipHistory(relationshipId, limit) {
|
|
196
|
+
const db = getDatabase();
|
|
197
|
+
let query = `SELECT * FROM relationship_history WHERE relationship_id = ? ORDER BY timestamp DESC`;
|
|
198
|
+
const params = [relationshipId];
|
|
199
|
+
if (limit) {
|
|
200
|
+
query += ` LIMIT ?`;
|
|
201
|
+
params.push(limit);
|
|
202
|
+
}
|
|
203
|
+
const rows = db.prepare(query).all(...params);
|
|
204
|
+
return rows.map(row => ({
|
|
205
|
+
id: row.id,
|
|
206
|
+
relationshipId: row.relationship_id,
|
|
207
|
+
previousValue: row.previous_value,
|
|
208
|
+
newValue: row.new_value,
|
|
209
|
+
reason: row.reason,
|
|
210
|
+
timestamp: row.timestamp,
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Update a relationship value with optional metadata changes. Supports both direct set and delta modes.
|
|
215
|
+
* Logs to history when value changes.
|
|
216
|
+
*/
|
|
217
|
+
export function updateRelationshipValue(params) {
|
|
218
|
+
const db = getDatabase();
|
|
219
|
+
const relationship = getRelationship(params.relationshipId);
|
|
220
|
+
if (!relationship)
|
|
221
|
+
return null;
|
|
222
|
+
// Validate bounds if both are provided
|
|
223
|
+
if (params.minValue !== undefined && params.maxValue !== undefined && params.minValue > params.maxValue) {
|
|
224
|
+
throw new Error(`Invalid bounds: minValue (${params.minValue}) cannot be greater than maxValue (${params.maxValue})`);
|
|
225
|
+
}
|
|
226
|
+
const previousValue = relationship.value;
|
|
227
|
+
let newValue;
|
|
228
|
+
if (params.mode === "delta") {
|
|
229
|
+
newValue = previousValue + params.value;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
newValue = params.value;
|
|
233
|
+
}
|
|
234
|
+
// Apply bounds (clamp to min first, then max to ensure max takes precedence)
|
|
235
|
+
if (params.minValue !== undefined) {
|
|
236
|
+
newValue = Math.max(newValue, params.minValue);
|
|
237
|
+
}
|
|
238
|
+
if (params.maxValue !== undefined) {
|
|
239
|
+
newValue = Math.min(newValue, params.maxValue);
|
|
240
|
+
}
|
|
241
|
+
const now = new Date().toISOString();
|
|
242
|
+
const newType = params.relationshipType ?? relationship.relationshipType;
|
|
243
|
+
const newLabel = params.label !== undefined ? params.label : relationship.label;
|
|
244
|
+
const newNotes = params.notes ?? relationship.notes;
|
|
245
|
+
// The value/metadata update and its (conditional) history row must land
|
|
246
|
+
// together, for the same reason as modifyRelationship() above.
|
|
247
|
+
const change = withTransaction(() => {
|
|
248
|
+
db.prepare(`
|
|
249
|
+
UPDATE relationships
|
|
250
|
+
SET relationship_type = ?, value = ?, label = ?, notes = ?, updated_at = ?
|
|
251
|
+
WHERE id = ?
|
|
252
|
+
`).run(newType, newValue, newLabel, newNotes, now, params.relationshipId);
|
|
253
|
+
// Log to history if value changed
|
|
254
|
+
if (newValue !== previousValue) {
|
|
255
|
+
return logRelationshipChange(params.relationshipId, previousValue, newValue, params.reason || null);
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
relationship: {
|
|
261
|
+
...relationship,
|
|
262
|
+
relationshipType: newType,
|
|
263
|
+
value: newValue,
|
|
264
|
+
label: newLabel,
|
|
265
|
+
notes: newNotes,
|
|
266
|
+
updatedAt: now,
|
|
267
|
+
},
|
|
268
|
+
change,
|
|
269
|
+
previousValue,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// Helper to get a value label based on thresholds
|
|
273
|
+
export function getRelationshipLabel(value) {
|
|
274
|
+
if (value >= 80)
|
|
275
|
+
return "devoted";
|
|
276
|
+
if (value >= 60)
|
|
277
|
+
return "friendly";
|
|
278
|
+
if (value >= 40)
|
|
279
|
+
return "warm";
|
|
280
|
+
if (value >= 20)
|
|
281
|
+
return "cordial";
|
|
282
|
+
if (value >= 0)
|
|
283
|
+
return "neutral";
|
|
284
|
+
if (value >= -20)
|
|
285
|
+
return "cool";
|
|
286
|
+
if (value >= -40)
|
|
287
|
+
return "unfriendly";
|
|
288
|
+
if (value >= -60)
|
|
289
|
+
return "hostile";
|
|
290
|
+
if (value >= -80)
|
|
291
|
+
return "hateful";
|
|
292
|
+
return "nemesis";
|
|
293
|
+
}
|
|
294
|
+
// Create a bidirectional relationship (both directions with same initial value)
|
|
295
|
+
export function createBidirectionalRelationship(params) {
|
|
296
|
+
// A "bidirectional" relationship is really two rows. If the second insert
|
|
297
|
+
// fails, we must not be left with a lopsided relationship where A knows
|
|
298
|
+
// about B but not vice versa -- so both inserts happen in one transaction.
|
|
299
|
+
return withTransaction(() => {
|
|
300
|
+
const relA = createRelationship({
|
|
301
|
+
gameId: params.gameId,
|
|
302
|
+
sourceId: params.entityA.id,
|
|
303
|
+
sourceType: params.entityA.type,
|
|
304
|
+
targetId: params.entityB.id,
|
|
305
|
+
targetType: params.entityB.type,
|
|
306
|
+
relationshipType: params.relationshipType,
|
|
307
|
+
value: params.value,
|
|
308
|
+
label: params.label,
|
|
309
|
+
notes: params.notes,
|
|
310
|
+
});
|
|
311
|
+
const relB = createRelationship({
|
|
312
|
+
gameId: params.gameId,
|
|
313
|
+
sourceId: params.entityB.id,
|
|
314
|
+
sourceType: params.entityB.type,
|
|
315
|
+
targetId: params.entityA.id,
|
|
316
|
+
targetType: params.entityA.type,
|
|
317
|
+
relationshipType: params.relationshipType,
|
|
318
|
+
value: params.value,
|
|
319
|
+
label: params.label,
|
|
320
|
+
notes: params.notes,
|
|
321
|
+
});
|
|
322
|
+
return [relA, relB];
|
|
323
|
+
});
|
|
324
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Resource, ResourceChange } from "../types/index.js";
|
|
2
|
+
export declare function createResource(params: {
|
|
3
|
+
gameId: string;
|
|
4
|
+
ownerType: "game" | "character";
|
|
5
|
+
ownerId?: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
category?: string;
|
|
9
|
+
value?: number;
|
|
10
|
+
minValue?: number;
|
|
11
|
+
maxValue?: number;
|
|
12
|
+
}): Resource;
|
|
13
|
+
export declare function getResource(id: string): Resource | null;
|
|
14
|
+
export declare function updateResource(id: string, updates: {
|
|
15
|
+
name?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
category?: string | null;
|
|
18
|
+
minValue?: number | null;
|
|
19
|
+
maxValue?: number | null;
|
|
20
|
+
}): Resource | null;
|
|
21
|
+
export declare function deleteResource(id: string): boolean;
|
|
22
|
+
export declare function listResources(gameId: string, filter?: {
|
|
23
|
+
ownerType?: "game" | "character";
|
|
24
|
+
ownerId?: string;
|
|
25
|
+
category?: string;
|
|
26
|
+
}): Resource[];
|
|
27
|
+
/**
|
|
28
|
+
* Update a resource's value - either by delta or absolute set.
|
|
29
|
+
* Use mode: "delta" to add/subtract, mode: "set" to set an absolute value.
|
|
30
|
+
*/
|
|
31
|
+
export declare function updateResourceValue(params: {
|
|
32
|
+
resourceId: string;
|
|
33
|
+
mode: "delta" | "set";
|
|
34
|
+
value: number;
|
|
35
|
+
reason?: string;
|
|
36
|
+
}): {
|
|
37
|
+
resource: Resource;
|
|
38
|
+
change: ResourceChange;
|
|
39
|
+
} | null;
|
|
40
|
+
/**
|
|
41
|
+
* Move `amount` from one resource to another, atomically. This is the ONLY
|
|
42
|
+
* write path for a resource that is a member of a declared 'conserved'
|
|
43
|
+
* constraint -- checkResourceConstraints() (constraint.ts) rejects
|
|
44
|
+
* update_resource_value against such a resource specifically because a
|
|
45
|
+
* single-resource write can't express where the counterpart delta comes
|
|
46
|
+
* from. This function is that counterpart-carrying write.
|
|
47
|
+
*
|
|
48
|
+
* WHY AN EXPLICIT TRANSFER TOOL, NOT A BALANCED MULTI-RESOURCE WRITE:
|
|
49
|
+
* A single-resource write against a conserved member is ambiguous -- the
|
|
50
|
+
* server has no way to infer where the offsetting delta should come from.
|
|
51
|
+
* Two designs resolve that ambiguity:
|
|
52
|
+
* (a) an explicit transfer(from, to, amount) tool, or
|
|
53
|
+
* (b) a "balanced write" tool taking an arbitrary { resourceId: delta }
|
|
54
|
+
* map, rejecting the whole call unless every conserved set's deltas
|
|
55
|
+
* sum to zero.
|
|
56
|
+
* (b) is more general -- it can reshuffle N members in one call -- but
|
|
57
|
+
* offloads real bookkeeping onto the caller: an LLM has to enumerate every
|
|
58
|
+
* affected resourceId, get every delta's sign right, and get the
|
|
59
|
+
* grouping-by-constraint-set right, all in one shot with no partial-progress
|
|
60
|
+
* checkpoint if it gets any of that wrong. (a) covers the overwhelming
|
|
61
|
+
* majority of real game actions directly ("trade 10 grain for gold", "the
|
|
62
|
+
* treasury pays the population") and composes: an N-member reshuffle is just
|
|
63
|
+
* several transfers, which the caller can still wrap in one withTransaction()
|
|
64
|
+
* if it needs to be all-or-nothing (see the "chains two transfers" tests in
|
|
65
|
+
* conserved.test.ts). That composability, plus the much smaller surface for
|
|
66
|
+
* a caller to get wrong, is why the narrower primitive was chosen.
|
|
67
|
+
*
|
|
68
|
+
* Scope is deliberately narrow: fromResourceId and toResourceId must already
|
|
69
|
+
* be members of the SAME declared 'conserved' constraint. This is not a
|
|
70
|
+
* general "move value between any two resources" tool -- for anything not
|
|
71
|
+
* under a 'conserved' constraint, update_resource_value remains the right
|
|
72
|
+
* tool (see checkResourceConstraints()). Keeping the two write paths
|
|
73
|
+
* mutually exclusive per resource means which one to use is never
|
|
74
|
+
* ambiguous.
|
|
75
|
+
*
|
|
76
|
+
* Never clamps. Clamping one side of a transfer would apply an uneven delta
|
|
77
|
+
* -- the source would lose less (or the destination gain less) than the
|
|
78
|
+
* other side moved by, silently creating or destroying value -- so any
|
|
79
|
+
* bound violation on either side rejects the whole transfer instead,
|
|
80
|
+
* regardless of whether a 'bounded' constraint is separately declared.
|
|
81
|
+
*/
|
|
82
|
+
export declare function transferResourceValue(params: {
|
|
83
|
+
fromResourceId: string;
|
|
84
|
+
toResourceId: string;
|
|
85
|
+
amount: number;
|
|
86
|
+
reason?: string;
|
|
87
|
+
}): {
|
|
88
|
+
from: Resource;
|
|
89
|
+
to: Resource;
|
|
90
|
+
fromChange: ResourceChange;
|
|
91
|
+
toChange: ResourceChange;
|
|
92
|
+
};
|
|
93
|
+
export declare function getResourceHistory(resourceId: string, limit?: number): ResourceChange[];
|