@ai-rpg-engine/modules 1.0.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/dist/belief-provenance.d.ts +41 -0
- package/dist/belief-provenance.d.ts.map +1 -0
- package/dist/belief-provenance.js +254 -0
- package/dist/belief-provenance.js.map +1 -0
- package/dist/belief-provenance.test.d.ts +2 -0
- package/dist/belief-provenance.test.d.ts.map +1 -0
- package/dist/belief-provenance.test.js +139 -0
- package/dist/belief-provenance.test.js.map +1 -0
- package/dist/cognition-core.d.ts +89 -0
- package/dist/cognition-core.d.ts.map +1 -0
- package/dist/cognition-core.js +370 -0
- package/dist/cognition-core.js.map +1 -0
- package/dist/cognition-core.test.d.ts +2 -0
- package/dist/cognition-core.test.d.ts.map +1 -0
- package/dist/cognition-core.test.js +215 -0
- package/dist/cognition-core.test.js.map +1 -0
- package/dist/combat-core.d.ts +11 -0
- package/dist/combat-core.d.ts.map +1 -0
- package/dist/combat-core.js +152 -0
- package/dist/combat-core.js.map +1 -0
- package/dist/dialogue-core.d.ts +10 -0
- package/dist/dialogue-core.d.ts.map +1 -0
- package/dist/dialogue-core.js +172 -0
- package/dist/dialogue-core.js.map +1 -0
- package/dist/district-core.d.ts +59 -0
- package/dist/district-core.d.ts.map +1 -0
- package/dist/district-core.js +224 -0
- package/dist/district-core.js.map +1 -0
- package/dist/district-core.test.d.ts +2 -0
- package/dist/district-core.test.d.ts.map +1 -0
- package/dist/district-core.test.js +157 -0
- package/dist/district-core.test.js.map +1 -0
- package/dist/environment-core.d.ts +78 -0
- package/dist/environment-core.d.ts.map +1 -0
- package/dist/environment-core.js +229 -0
- package/dist/environment-core.js.map +1 -0
- package/dist/environment-core.test.d.ts +2 -0
- package/dist/environment-core.test.d.ts.map +1 -0
- package/dist/environment-core.test.js +252 -0
- package/dist/environment-core.test.js.map +1 -0
- package/dist/faction-cognition.d.ts +39 -0
- package/dist/faction-cognition.d.ts.map +1 -0
- package/dist/faction-cognition.js +139 -0
- package/dist/faction-cognition.js.map +1 -0
- package/dist/faction-cognition.test.d.ts +2 -0
- package/dist/faction-cognition.test.d.ts.map +1 -0
- package/dist/faction-cognition.test.js +166 -0
- package/dist/faction-cognition.test.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory-core.d.ts +10 -0
- package/dist/inventory-core.d.ts.map +1 -0
- package/dist/inventory-core.js +69 -0
- package/dist/inventory-core.js.map +1 -0
- package/dist/knowledge-decay.test.d.ts +2 -0
- package/dist/knowledge-decay.test.d.ts.map +1 -0
- package/dist/knowledge-decay.test.js +143 -0
- package/dist/knowledge-decay.test.js.map +1 -0
- package/dist/narrative-authority.d.ts +40 -0
- package/dist/narrative-authority.d.ts.map +1 -0
- package/dist/narrative-authority.js +89 -0
- package/dist/narrative-authority.js.map +1 -0
- package/dist/narrative-authority.test.d.ts +2 -0
- package/dist/narrative-authority.test.d.ts.map +1 -0
- package/dist/narrative-authority.test.js +237 -0
- package/dist/narrative-authority.test.js.map +1 -0
- package/dist/observer-presentation.d.ts +74 -0
- package/dist/observer-presentation.d.ts.map +1 -0
- package/dist/observer-presentation.js +277 -0
- package/dist/observer-presentation.js.map +1 -0
- package/dist/observer-presentation.test.d.ts +2 -0
- package/dist/observer-presentation.test.d.ts.map +1 -0
- package/dist/observer-presentation.test.js +156 -0
- package/dist/observer-presentation.test.js.map +1 -0
- package/dist/perception-filter.d.ts +46 -0
- package/dist/perception-filter.d.ts.map +1 -0
- package/dist/perception-filter.js +281 -0
- package/dist/perception-filter.js.map +1 -0
- package/dist/perception-filter.test.d.ts +2 -0
- package/dist/perception-filter.test.d.ts.map +1 -0
- package/dist/perception-filter.test.js +308 -0
- package/dist/perception-filter.test.js.map +1 -0
- package/dist/progression-core.d.ts +63 -0
- package/dist/progression-core.d.ts.map +1 -0
- package/dist/progression-core.js +232 -0
- package/dist/progression-core.js.map +1 -0
- package/dist/progression-core.test.d.ts +2 -0
- package/dist/progression-core.test.d.ts.map +1 -0
- package/dist/progression-core.test.js +328 -0
- package/dist/progression-core.test.js.map +1 -0
- package/dist/rumor-propagation.d.ts +31 -0
- package/dist/rumor-propagation.d.ts.map +1 -0
- package/dist/rumor-propagation.js +180 -0
- package/dist/rumor-propagation.js.map +1 -0
- package/dist/rumor-propagation.test.d.ts +2 -0
- package/dist/rumor-propagation.test.d.ts.map +1 -0
- package/dist/rumor-propagation.test.js +176 -0
- package/dist/rumor-propagation.test.js.map +1 -0
- package/dist/simulation-inspector.d.ts +78 -0
- package/dist/simulation-inspector.d.ts.map +1 -0
- package/dist/simulation-inspector.js +263 -0
- package/dist/simulation-inspector.js.map +1 -0
- package/dist/simulation-inspector.test.d.ts +2 -0
- package/dist/simulation-inspector.test.d.ts.map +1 -0
- package/dist/simulation-inspector.test.js +152 -0
- package/dist/simulation-inspector.test.js.map +1 -0
- package/dist/status-core.d.ts +17 -0
- package/dist/status-core.d.ts.map +1 -0
- package/dist/status-core.js +106 -0
- package/dist/status-core.js.map +1 -0
- package/dist/traversal-core.d.ts +3 -0
- package/dist/traversal-core.d.ts.map +1 -0
- package/dist/traversal-core.js +93 -0
- package/dist/traversal-core.js.map +1 -0
- package/package.json +34 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"combat-core.d.ts","sourceRoot":"","sources":["../src/combat-core.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAEZ,UAAU,EAEV,WAAW,EACZ,MAAM,qBAAqB,CAAC;AAG7B,MAAM,MAAM,cAAc,GAAG;IAC3B,8FAA8F;IAC9F,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC;IACtF,+DAA+D;IAC/D,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC;CACpF,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,cAAc,GAAG,YAAY,CAcxE;AAED,gDAAgD;AAChD,eAAO,MAAM,UAAU,EAAE,YAAiC,CAAC"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// combat-core — attack, damage, defeat
|
|
2
|
+
import { nextId } from '@ai-rpg-engine/core';
|
|
3
|
+
export function createCombatCore(formulas) {
|
|
4
|
+
return {
|
|
5
|
+
id: 'combat-core',
|
|
6
|
+
version: '0.1.0',
|
|
7
|
+
register(ctx) {
|
|
8
|
+
ctx.actions.registerVerb('attack', (action, world) => attackHandler(action, world, formulas));
|
|
9
|
+
ctx.persistence.registerNamespace('combat-core', {
|
|
10
|
+
inCombat: false,
|
|
11
|
+
combatants: [],
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Default combat module with basic formulas */
|
|
17
|
+
export const combatCore = createCombatCore();
|
|
18
|
+
function attackHandler(action, world, formulas) {
|
|
19
|
+
const events = [];
|
|
20
|
+
const attacker = world.entities[action.actorId];
|
|
21
|
+
const targetId = action.targetIds?.[0];
|
|
22
|
+
if (!attacker) {
|
|
23
|
+
return [makeEvent(action, 'action.rejected', { reason: 'attacker not found' })];
|
|
24
|
+
}
|
|
25
|
+
if (!targetId) {
|
|
26
|
+
return [makeEvent(action, 'action.rejected', { reason: 'no target specified' })];
|
|
27
|
+
}
|
|
28
|
+
const target = world.entities[targetId];
|
|
29
|
+
if (!target) {
|
|
30
|
+
return [makeEvent(action, 'action.rejected', { reason: `target ${targetId} not found` })];
|
|
31
|
+
}
|
|
32
|
+
// Check same zone
|
|
33
|
+
if (attacker.zoneId !== target.zoneId) {
|
|
34
|
+
return [makeEvent(action, 'action.rejected', { reason: 'target not in same zone' })];
|
|
35
|
+
}
|
|
36
|
+
// Check target is alive
|
|
37
|
+
if ((target.resources.hp ?? 0) <= 0) {
|
|
38
|
+
return [makeEvent(action, 'action.rejected', { reason: 'target is already defeated' })];
|
|
39
|
+
}
|
|
40
|
+
// Stamina cost
|
|
41
|
+
const staminaCost = 1;
|
|
42
|
+
const currentStamina = attacker.resources.stamina ?? 0;
|
|
43
|
+
if (currentStamina < staminaCost) {
|
|
44
|
+
return [makeEvent(action, 'action.rejected', { reason: 'not enough stamina' })];
|
|
45
|
+
}
|
|
46
|
+
attacker.resources.stamina = currentStamina - staminaCost;
|
|
47
|
+
events.push(makeEvent(action, 'resource.changed', {
|
|
48
|
+
entityId: attacker.id,
|
|
49
|
+
resource: 'stamina',
|
|
50
|
+
previous: currentStamina,
|
|
51
|
+
current: attacker.resources.stamina,
|
|
52
|
+
delta: -staminaCost,
|
|
53
|
+
}));
|
|
54
|
+
// Hit check
|
|
55
|
+
const hitChance = formulas?.hitChance
|
|
56
|
+
? formulas.hitChance(attacker, target, world)
|
|
57
|
+
: defaultHitChance(attacker, target);
|
|
58
|
+
// Use a simple deterministic roll based on tick + entity IDs
|
|
59
|
+
const roll = simpleRoll(world.meta.tick, attacker.id, target.id);
|
|
60
|
+
if (roll > hitChance) {
|
|
61
|
+
events.push(makeEvent(action, 'combat.contact.miss', {
|
|
62
|
+
attackerId: attacker.id,
|
|
63
|
+
targetId: target.id,
|
|
64
|
+
roll,
|
|
65
|
+
hitChance,
|
|
66
|
+
}, {
|
|
67
|
+
targetIds: [target.id],
|
|
68
|
+
presentation: { channels: ['objective'], priority: 'normal' },
|
|
69
|
+
}));
|
|
70
|
+
return events;
|
|
71
|
+
}
|
|
72
|
+
// Damage
|
|
73
|
+
const damage = formulas?.damage
|
|
74
|
+
? formulas.damage(attacker, target, world)
|
|
75
|
+
: defaultDamage(attacker);
|
|
76
|
+
const previousHp = target.resources.hp ?? 0;
|
|
77
|
+
target.resources.hp = Math.max(0, previousHp - damage);
|
|
78
|
+
events.push(makeEvent(action, 'combat.contact.hit', {
|
|
79
|
+
attackerId: attacker.id,
|
|
80
|
+
targetId: target.id,
|
|
81
|
+
roll,
|
|
82
|
+
hitChance,
|
|
83
|
+
}, {
|
|
84
|
+
targetIds: [target.id],
|
|
85
|
+
presentation: { channels: ['objective'], priority: 'normal' },
|
|
86
|
+
}));
|
|
87
|
+
events.push(makeEvent(action, 'combat.damage.applied', {
|
|
88
|
+
attackerId: attacker.id,
|
|
89
|
+
targetId: target.id,
|
|
90
|
+
damage,
|
|
91
|
+
previousHp,
|
|
92
|
+
currentHp: target.resources.hp,
|
|
93
|
+
}, {
|
|
94
|
+
targetIds: [target.id],
|
|
95
|
+
presentation: {
|
|
96
|
+
channels: ['objective'],
|
|
97
|
+
priority: 'high',
|
|
98
|
+
soundCues: ['combat.hit'],
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
events.push(makeEvent(action, 'resource.changed', {
|
|
102
|
+
entityId: target.id,
|
|
103
|
+
resource: 'hp',
|
|
104
|
+
previous: previousHp,
|
|
105
|
+
current: target.resources.hp,
|
|
106
|
+
delta: -damage,
|
|
107
|
+
}));
|
|
108
|
+
// Defeat check
|
|
109
|
+
if (target.resources.hp <= 0) {
|
|
110
|
+
events.push(makeEvent(action, 'combat.entity.defeated', {
|
|
111
|
+
entityId: target.id,
|
|
112
|
+
entityName: target.name,
|
|
113
|
+
defeatedBy: attacker.id,
|
|
114
|
+
}, {
|
|
115
|
+
targetIds: [target.id],
|
|
116
|
+
presentation: {
|
|
117
|
+
channels: ['objective', 'narrator'],
|
|
118
|
+
priority: 'critical',
|
|
119
|
+
soundCues: ['combat.defeat'],
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
return events;
|
|
124
|
+
}
|
|
125
|
+
function defaultHitChance(attacker, target) {
|
|
126
|
+
const attackerInstinct = attacker.stats.instinct ?? 5;
|
|
127
|
+
const targetInstinct = target.stats.instinct ?? 5;
|
|
128
|
+
return Math.min(95, Math.max(5, 50 + attackerInstinct * 5 - targetInstinct * 3));
|
|
129
|
+
}
|
|
130
|
+
function defaultDamage(attacker) {
|
|
131
|
+
const vigor = attacker.stats.vigor ?? 3;
|
|
132
|
+
return Math.max(1, vigor);
|
|
133
|
+
}
|
|
134
|
+
/** Simple deterministic roll 1-100 based on tick and IDs */
|
|
135
|
+
function simpleRoll(tick, attackerId, targetId) {
|
|
136
|
+
let hash = tick * 2654435761;
|
|
137
|
+
for (const char of attackerId + targetId) {
|
|
138
|
+
hash = ((hash << 5) - hash + char.charCodeAt(0)) | 0;
|
|
139
|
+
}
|
|
140
|
+
return (Math.abs(hash) % 100) + 1;
|
|
141
|
+
}
|
|
142
|
+
function makeEvent(action, type, payload, extra) {
|
|
143
|
+
return {
|
|
144
|
+
id: nextId('evt'),
|
|
145
|
+
tick: action.issuedAtTick,
|
|
146
|
+
type,
|
|
147
|
+
actorId: action.actorId,
|
|
148
|
+
payload,
|
|
149
|
+
...extra,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=combat-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"combat-core.js","sourceRoot":"","sources":["../src/combat-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EngineModule } from '@ai-rpg-engine/core';
|
|
2
|
+
import type { DialogueDefinition } from '@ai-rpg-engine/content-schema';
|
|
3
|
+
export type DialogueState = {
|
|
4
|
+
activeDialogue: string | null;
|
|
5
|
+
activeNodeId: string | null;
|
|
6
|
+
speakerId: string | null;
|
|
7
|
+
};
|
|
8
|
+
export type DialogueRegistry = Map<string, DialogueDefinition>;
|
|
9
|
+
export declare function createDialogueCore(dialogues: DialogueDefinition[]): EngineModule;
|
|
10
|
+
//# sourceMappingURL=dialogue-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialogue-core.d.ts","sourceRoot":"","sources":["../src/dialogue-core.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAKb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,kBAAkB,EAAkC,MAAM,+BAA+B,CAAC;AAExG,MAAM,MAAM,aAAa,GAAG;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;AAE/D,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,kBAAkB,EAAE,GAAG,YAAY,CAqBhF"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// dialogue-core — NPC dialogue trees with choices and conditions
|
|
2
|
+
import { nextId } from '@ai-rpg-engine/core';
|
|
3
|
+
export function createDialogueCore(dialogues) {
|
|
4
|
+
const registry = new Map();
|
|
5
|
+
for (const d of dialogues) {
|
|
6
|
+
registry.set(d.id, d);
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
id: 'dialogue-core',
|
|
10
|
+
version: '0.1.0',
|
|
11
|
+
register(ctx) {
|
|
12
|
+
ctx.actions.registerVerb('speak', (action, world) => speakHandler(action, world, registry));
|
|
13
|
+
ctx.actions.registerVerb('choose', (action, world) => chooseHandler(action, world, registry));
|
|
14
|
+
ctx.persistence.registerNamespace('dialogue-core', {
|
|
15
|
+
activeDialogue: null,
|
|
16
|
+
activeNodeId: null,
|
|
17
|
+
speakerId: null,
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function speakHandler(action, world, registry) {
|
|
23
|
+
const targetId = action.targetIds?.[0];
|
|
24
|
+
if (!targetId) {
|
|
25
|
+
return [makeEvent(action, 'action.rejected', { reason: 'no one to speak to' })];
|
|
26
|
+
}
|
|
27
|
+
const target = world.entities[targetId];
|
|
28
|
+
if (!target) {
|
|
29
|
+
return [makeEvent(action, 'action.rejected', { reason: `${targetId} not found` })];
|
|
30
|
+
}
|
|
31
|
+
// Find dialogue for this NPC
|
|
32
|
+
const dialogueId = action.parameters?.dialogueId;
|
|
33
|
+
let dialogue;
|
|
34
|
+
if (dialogueId) {
|
|
35
|
+
dialogue = registry.get(dialogueId);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Find first dialogue that includes this speaker
|
|
39
|
+
for (const d of registry.values()) {
|
|
40
|
+
if (d.speakers.includes(targetId)) {
|
|
41
|
+
dialogue = d;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!dialogue) {
|
|
47
|
+
return [makeEvent(action, 'action.rejected', { reason: `${target.name} has nothing to say` })];
|
|
48
|
+
}
|
|
49
|
+
const entryNode = dialogue.nodes[dialogue.entryNodeId];
|
|
50
|
+
if (!entryNode) {
|
|
51
|
+
return [makeEvent(action, 'action.rejected', { reason: 'dialogue has no entry node' })];
|
|
52
|
+
}
|
|
53
|
+
// Set dialogue state
|
|
54
|
+
const dState = {
|
|
55
|
+
activeDialogue: dialogue.id,
|
|
56
|
+
activeNodeId: dialogue.entryNodeId,
|
|
57
|
+
speakerId: targetId,
|
|
58
|
+
};
|
|
59
|
+
world.modules['dialogue-core'] = dState;
|
|
60
|
+
const events = [
|
|
61
|
+
makeEvent(action, 'dialogue.started', {
|
|
62
|
+
dialogueId: dialogue.id,
|
|
63
|
+
speakerId: targetId,
|
|
64
|
+
speakerName: target.name,
|
|
65
|
+
}),
|
|
66
|
+
];
|
|
67
|
+
events.push(...renderNode(action, entryNode, world));
|
|
68
|
+
return events;
|
|
69
|
+
}
|
|
70
|
+
function chooseHandler(action, world, registry) {
|
|
71
|
+
const dState = world.modules['dialogue-core'];
|
|
72
|
+
if (!dState?.activeDialogue || !dState.activeNodeId) {
|
|
73
|
+
return [makeEvent(action, 'action.rejected', { reason: 'no active dialogue' })];
|
|
74
|
+
}
|
|
75
|
+
const dialogue = registry.get(dState.activeDialogue);
|
|
76
|
+
if (!dialogue) {
|
|
77
|
+
return [makeEvent(action, 'action.rejected', { reason: 'dialogue not found' })];
|
|
78
|
+
}
|
|
79
|
+
const currentNode = dialogue.nodes[dState.activeNodeId];
|
|
80
|
+
if (!currentNode?.choices) {
|
|
81
|
+
return [makeEvent(action, 'action.rejected', { reason: 'no choices available' })];
|
|
82
|
+
}
|
|
83
|
+
const choiceId = action.parameters?.choiceId;
|
|
84
|
+
const choiceIndex = action.parameters?.choiceIndex;
|
|
85
|
+
let choice;
|
|
86
|
+
if (choiceId) {
|
|
87
|
+
choice = currentNode.choices.find(c => c.id === choiceId);
|
|
88
|
+
}
|
|
89
|
+
else if (choiceIndex !== undefined) {
|
|
90
|
+
const available = currentNode.choices.filter(c => !c.condition || evaluateCondition(c.condition, world));
|
|
91
|
+
choice = available[choiceIndex];
|
|
92
|
+
}
|
|
93
|
+
if (!choice) {
|
|
94
|
+
return [makeEvent(action, 'action.rejected', { reason: 'invalid choice' })];
|
|
95
|
+
}
|
|
96
|
+
const events = [
|
|
97
|
+
makeEvent(action, 'dialogue.choice.selected', {
|
|
98
|
+
dialogueId: dialogue.id,
|
|
99
|
+
choiceId: choice.id,
|
|
100
|
+
choiceText: choice.text,
|
|
101
|
+
}),
|
|
102
|
+
];
|
|
103
|
+
// Apply choice effects
|
|
104
|
+
if (choice.effects) {
|
|
105
|
+
for (const effect of choice.effects) {
|
|
106
|
+
events.push(...applyDialogueEffect(action, effect, world));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Navigate to next node
|
|
110
|
+
const nextNode = dialogue.nodes[choice.nextNodeId];
|
|
111
|
+
if (nextNode) {
|
|
112
|
+
dState.activeNodeId = choice.nextNodeId;
|
|
113
|
+
events.push(...renderNode(action, nextNode, world));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// End dialogue
|
|
117
|
+
dState.activeDialogue = null;
|
|
118
|
+
dState.activeNodeId = null;
|
|
119
|
+
dState.speakerId = null;
|
|
120
|
+
events.push(makeEvent(action, 'dialogue.ended', { dialogueId: dialogue.id }));
|
|
121
|
+
}
|
|
122
|
+
return events;
|
|
123
|
+
}
|
|
124
|
+
function renderNode(action, node, world) {
|
|
125
|
+
const text = typeof node.text === 'string' ? node.text : node.text[0]?.text ?? '';
|
|
126
|
+
const availableChoices = node.choices
|
|
127
|
+
?.filter(c => !c.condition || evaluateCondition(c.condition, world))
|
|
128
|
+
.map((c, i) => ({ id: c.id, text: c.text, index: i }));
|
|
129
|
+
return [
|
|
130
|
+
makeEvent(action, 'dialogue.node.entered', {
|
|
131
|
+
nodeId: node.id,
|
|
132
|
+
speaker: node.speaker,
|
|
133
|
+
text,
|
|
134
|
+
choices: availableChoices ?? [],
|
|
135
|
+
hasChoices: (availableChoices?.length ?? 0) > 0,
|
|
136
|
+
}, {
|
|
137
|
+
presentation: {
|
|
138
|
+
channels: ['dialogue'],
|
|
139
|
+
priority: 'high',
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
function applyDialogueEffect(action, effect, world) {
|
|
145
|
+
if (effect.type === 'set-global') {
|
|
146
|
+
const key = effect.params.key;
|
|
147
|
+
const value = effect.params.value;
|
|
148
|
+
world.globals[key] = value;
|
|
149
|
+
return [makeEvent(action, 'world.flag.changed', { key, value })];
|
|
150
|
+
}
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
function evaluateCondition(condition, world) {
|
|
154
|
+
if (condition.type === 'global-equals') {
|
|
155
|
+
return world.globals[condition.params.key] === condition.params.value;
|
|
156
|
+
}
|
|
157
|
+
if (condition.type === 'global-set') {
|
|
158
|
+
return world.globals[condition.params.key] !== undefined;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
function makeEvent(action, type, payload, extra) {
|
|
163
|
+
return {
|
|
164
|
+
id: nextId('evt'),
|
|
165
|
+
tick: action.issuedAtTick,
|
|
166
|
+
type,
|
|
167
|
+
actorId: action.actorId,
|
|
168
|
+
payload,
|
|
169
|
+
...extra,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=dialogue-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialogue-core.js","sourceRoot":"","sources":["../src/dialogue-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { EngineModule, WorldState } from '@ai-rpg-engine/core';
|
|
2
|
+
export type DistrictMetrics = {
|
|
3
|
+
/** Rises from combat and hostile events, decays over time */
|
|
4
|
+
alertPressure: number;
|
|
5
|
+
/** Rises from rumor propagation events */
|
|
6
|
+
rumorDensity: number;
|
|
7
|
+
/** Rises from non-faction entity sightings in the district */
|
|
8
|
+
intruderLikelihood: number;
|
|
9
|
+
/** Faction presence strength — higher means more watchful */
|
|
10
|
+
surveillance: number;
|
|
11
|
+
/** Aggregated environmental stability from constituent zones */
|
|
12
|
+
stability: number;
|
|
13
|
+
};
|
|
14
|
+
export type DistrictDefinition = {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
zoneIds: string[];
|
|
18
|
+
tags: string[];
|
|
19
|
+
controllingFaction?: string;
|
|
20
|
+
baseMetrics?: Partial<DistrictMetrics>;
|
|
21
|
+
};
|
|
22
|
+
export type DistrictState = DistrictMetrics & {
|
|
23
|
+
lastUpdateTick: number;
|
|
24
|
+
eventCount: number;
|
|
25
|
+
};
|
|
26
|
+
export type DistrictDecayConfig = {
|
|
27
|
+
/** Base decay rate per tick (default: 1) */
|
|
28
|
+
decayRate: number;
|
|
29
|
+
/** Minimum metric value (default: 0) */
|
|
30
|
+
floor: number;
|
|
31
|
+
};
|
|
32
|
+
export type DistrictCoreConfig = {
|
|
33
|
+
districts: DistrictDefinition[];
|
|
34
|
+
decay?: Partial<DistrictDecayConfig>;
|
|
35
|
+
};
|
|
36
|
+
export declare function createDistrictCore(config: DistrictCoreConfig): EngineModule;
|
|
37
|
+
/** Get the district a zone belongs to */
|
|
38
|
+
export declare function getDistrictForZone(world: WorldState, zoneId: string): string | undefined;
|
|
39
|
+
/** Get district state */
|
|
40
|
+
export declare function getDistrictState(world: WorldState, districtId: string): DistrictState | undefined;
|
|
41
|
+
/** Get district definition */
|
|
42
|
+
export declare function getDistrictDefinition(world: WorldState, districtId: string): DistrictDefinition | undefined;
|
|
43
|
+
/** Get all district IDs */
|
|
44
|
+
export declare function getAllDistrictIds(world: WorldState): string[];
|
|
45
|
+
/** Get a specific district metric */
|
|
46
|
+
export declare function getDistrictMetric(world: WorldState, districtId: string, metric: keyof DistrictMetrics): number;
|
|
47
|
+
/** Manually modify a district metric */
|
|
48
|
+
export declare function modifyDistrictMetric(world: WorldState, districtId: string, metric: keyof DistrictMetrics, delta: number): void;
|
|
49
|
+
/**
|
|
50
|
+
* Check if a district is on high alert.
|
|
51
|
+
* Useful for AI intent selection — patrol tendency increases in alert districts.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isDistrictOnAlert(world: WorldState, districtId: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Get the combined threat level of a district.
|
|
56
|
+
* Factors: alert pressure, intruder likelihood, rumor density.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getDistrictThreatLevel(world: WorldState, districtId: string): number;
|
|
59
|
+
//# sourceMappingURL=district-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"district-core.d.ts","sourceRoot":"","sources":["../src/district-core.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EAGX,MAAM,qBAAqB,CAAC;AAM7B,MAAM,MAAM,eAAe,GAAG;IAC5B,6DAA6D;IAC7D,aAAa,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,eAAe,GAAG;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,kBAAkB,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACtC,CAAC;AAuBF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CA6F3E;AAYD,yCAAyC;AACzC,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExF;AAED,yBAAyB;AACzB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAEjG;AAED,8BAA8B;AAC9B,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAE3G;AAED,2BAA2B;AAC3B,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE,CAE7D;AAED,qCAAqC;AACrC,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,eAAe,GAC5B,MAAM,CAGR;AAED,wCAAwC;AACxC,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,eAAe,EAC7B,KAAK,EAAE,MAAM,GACZ,IAAI,CAIN;AAID;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAGhF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQpF"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// district-core — spatial memory layer
|
|
2
|
+
// Districts aggregate zone-level signals into persistent informational state.
|
|
3
|
+
// Alert pressure, rumor density, intruder likelihood, and surveillance
|
|
4
|
+
// rise from events and decay over time, giving the world spatial memory.
|
|
5
|
+
import { getZoneProperty } from './environment-core.js';
|
|
6
|
+
import { getEntityFaction, getFactionCognition } from './faction-cognition.js';
|
|
7
|
+
const DEFAULT_METRICS = {
|
|
8
|
+
alertPressure: 0,
|
|
9
|
+
rumorDensity: 0,
|
|
10
|
+
intruderLikelihood: 0,
|
|
11
|
+
surveillance: 0,
|
|
12
|
+
stability: 5,
|
|
13
|
+
};
|
|
14
|
+
const DEFAULT_DECAY = {
|
|
15
|
+
decayRate: 1,
|
|
16
|
+
floor: 0,
|
|
17
|
+
};
|
|
18
|
+
// --- Module ---
|
|
19
|
+
export function createDistrictCore(config) {
|
|
20
|
+
const decayConfig = { ...DEFAULT_DECAY, ...config.decay };
|
|
21
|
+
const initialState = {
|
|
22
|
+
districts: {},
|
|
23
|
+
zoneToDistrict: {},
|
|
24
|
+
definitions: {},
|
|
25
|
+
};
|
|
26
|
+
for (const def of config.districts) {
|
|
27
|
+
initialState.definitions[def.id] = def;
|
|
28
|
+
initialState.districts[def.id] = {
|
|
29
|
+
...DEFAULT_METRICS,
|
|
30
|
+
...def.baseMetrics,
|
|
31
|
+
lastUpdateTick: 0,
|
|
32
|
+
eventCount: 0,
|
|
33
|
+
};
|
|
34
|
+
for (const zoneId of def.zoneIds) {
|
|
35
|
+
initialState.zoneToDistrict[zoneId] = def.id;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
id: 'district-core',
|
|
40
|
+
version: '0.1.0',
|
|
41
|
+
dependsOn: ['environment-core'],
|
|
42
|
+
register(ctx) {
|
|
43
|
+
ctx.persistence.registerNamespace('district-core', initialState);
|
|
44
|
+
// Combat raises alert pressure in the district
|
|
45
|
+
ctx.events.on('combat.*', (event, world) => {
|
|
46
|
+
const districtId = getEventDistrictId(event, world);
|
|
47
|
+
if (!districtId)
|
|
48
|
+
return;
|
|
49
|
+
const state = getModuleState(world);
|
|
50
|
+
const district = state.districts[districtId];
|
|
51
|
+
if (!district)
|
|
52
|
+
return;
|
|
53
|
+
const delta = event.type === 'combat.entity.defeated' ? 8 : 4;
|
|
54
|
+
district.alertPressure = Math.min(100, district.alertPressure + delta);
|
|
55
|
+
district.eventCount++;
|
|
56
|
+
district.lastUpdateTick = event.tick;
|
|
57
|
+
});
|
|
58
|
+
// Zone entry raises intruder likelihood if entity is not from controlling faction
|
|
59
|
+
ctx.events.on('world.zone.entered', (event, world) => {
|
|
60
|
+
const zoneId = event.payload.zoneId;
|
|
61
|
+
const districtId = getDistrictForZone(world, zoneId);
|
|
62
|
+
if (!districtId)
|
|
63
|
+
return;
|
|
64
|
+
const state = getModuleState(world);
|
|
65
|
+
const district = state.districts[districtId];
|
|
66
|
+
const def = state.definitions[districtId];
|
|
67
|
+
if (!district || !def)
|
|
68
|
+
return;
|
|
69
|
+
const actorId = event.actorId;
|
|
70
|
+
if (!actorId)
|
|
71
|
+
return;
|
|
72
|
+
// Non-faction members raise intruder likelihood
|
|
73
|
+
if (def.controllingFaction) {
|
|
74
|
+
const actorFaction = getEntityFaction(world, actorId);
|
|
75
|
+
if (actorFaction !== def.controllingFaction) {
|
|
76
|
+
district.intruderLikelihood = Math.min(100, district.intruderLikelihood + 10);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
district.eventCount++;
|
|
80
|
+
district.lastUpdateTick = event.tick;
|
|
81
|
+
});
|
|
82
|
+
// Rumor events raise rumor density
|
|
83
|
+
ctx.events.on('rumor.belief.propagated', (event, world) => {
|
|
84
|
+
const factionId = event.payload.factionId;
|
|
85
|
+
// Find districts controlled by this faction
|
|
86
|
+
const state = getModuleState(world);
|
|
87
|
+
for (const [dId, def] of Object.entries(state.definitions)) {
|
|
88
|
+
if (def.controllingFaction === factionId) {
|
|
89
|
+
const district = state.districts[dId];
|
|
90
|
+
if (district) {
|
|
91
|
+
district.rumorDensity = Math.min(100, district.rumorDensity + 5);
|
|
92
|
+
district.lastUpdateTick = event.tick;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// District tick: decay metrics, update surveillance, sync stability
|
|
98
|
+
ctx.actions.registerVerb('district-tick', (_action, world) => {
|
|
99
|
+
processDistrictTick(world, decayConfig);
|
|
100
|
+
return [];
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// --- State Access ---
|
|
106
|
+
function getModuleState(world) {
|
|
107
|
+
return (world.modules['district-core'] ?? {
|
|
108
|
+
districts: {},
|
|
109
|
+
zoneToDistrict: {},
|
|
110
|
+
definitions: {},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Get the district a zone belongs to */
|
|
114
|
+
export function getDistrictForZone(world, zoneId) {
|
|
115
|
+
return getModuleState(world).zoneToDistrict[zoneId];
|
|
116
|
+
}
|
|
117
|
+
/** Get district state */
|
|
118
|
+
export function getDistrictState(world, districtId) {
|
|
119
|
+
return getModuleState(world).districts[districtId];
|
|
120
|
+
}
|
|
121
|
+
/** Get district definition */
|
|
122
|
+
export function getDistrictDefinition(world, districtId) {
|
|
123
|
+
return getModuleState(world).definitions[districtId];
|
|
124
|
+
}
|
|
125
|
+
/** Get all district IDs */
|
|
126
|
+
export function getAllDistrictIds(world) {
|
|
127
|
+
return Object.keys(getModuleState(world).districts);
|
|
128
|
+
}
|
|
129
|
+
/** Get a specific district metric */
|
|
130
|
+
export function getDistrictMetric(world, districtId, metric) {
|
|
131
|
+
const state = getModuleState(world).districts[districtId];
|
|
132
|
+
return state?.[metric] ?? 0;
|
|
133
|
+
}
|
|
134
|
+
/** Manually modify a district metric */
|
|
135
|
+
export function modifyDistrictMetric(world, districtId, metric, delta) {
|
|
136
|
+
const state = getModuleState(world).districts[districtId];
|
|
137
|
+
if (!state)
|
|
138
|
+
return;
|
|
139
|
+
state[metric] = Math.max(0, Math.min(100, state[metric] + delta));
|
|
140
|
+
}
|
|
141
|
+
// --- District-Aware Hooks (A4) ---
|
|
142
|
+
/**
|
|
143
|
+
* Check if a district is on high alert.
|
|
144
|
+
* Useful for AI intent selection — patrol tendency increases in alert districts.
|
|
145
|
+
*/
|
|
146
|
+
export function isDistrictOnAlert(world, districtId) {
|
|
147
|
+
const state = getModuleState(world).districts[districtId];
|
|
148
|
+
return (state?.alertPressure ?? 0) > 30;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get the combined threat level of a district.
|
|
152
|
+
* Factors: alert pressure, intruder likelihood, rumor density.
|
|
153
|
+
*/
|
|
154
|
+
export function getDistrictThreatLevel(world, districtId) {
|
|
155
|
+
const state = getModuleState(world).districts[districtId];
|
|
156
|
+
if (!state)
|
|
157
|
+
return 0;
|
|
158
|
+
return Math.min(100, Math.round(state.alertPressure * 0.4 +
|
|
159
|
+
state.intruderLikelihood * 0.35 +
|
|
160
|
+
state.rumorDensity * 0.25));
|
|
161
|
+
}
|
|
162
|
+
// --- Tick Processing ---
|
|
163
|
+
function processDistrictTick(world, decayConfig) {
|
|
164
|
+
const state = getModuleState(world);
|
|
165
|
+
for (const [districtId, district] of Object.entries(state.districts)) {
|
|
166
|
+
const def = state.definitions[districtId];
|
|
167
|
+
if (!def)
|
|
168
|
+
continue;
|
|
169
|
+
// Decay metrics toward floor
|
|
170
|
+
district.alertPressure = Math.max(decayConfig.floor, district.alertPressure - decayConfig.decayRate);
|
|
171
|
+
district.rumorDensity = Math.max(decayConfig.floor, district.rumorDensity - decayConfig.decayRate * 0.5);
|
|
172
|
+
district.intruderLikelihood = Math.max(decayConfig.floor, district.intruderLikelihood - decayConfig.decayRate * 0.8);
|
|
173
|
+
// Surveillance: count faction members present in district zones
|
|
174
|
+
if (def.controllingFaction) {
|
|
175
|
+
let factionPresence = 0;
|
|
176
|
+
for (const zoneId of def.zoneIds) {
|
|
177
|
+
for (const entity of Object.values(world.entities)) {
|
|
178
|
+
if (entity.zoneId === zoneId && entity.ai) {
|
|
179
|
+
const faction = getEntityFaction(world, entity.id);
|
|
180
|
+
if (faction === def.controllingFaction) {
|
|
181
|
+
factionPresence++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
district.surveillance = factionPresence * 15;
|
|
187
|
+
}
|
|
188
|
+
// Stability: average stability of constituent zones
|
|
189
|
+
let totalStability = 0;
|
|
190
|
+
let zoneCount = 0;
|
|
191
|
+
for (const zoneId of def.zoneIds) {
|
|
192
|
+
if (world.zones[zoneId]) {
|
|
193
|
+
totalStability += getZoneProperty(world, zoneId, 'stability') || 5;
|
|
194
|
+
zoneCount++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (zoneCount > 0) {
|
|
198
|
+
district.stability = totalStability / zoneCount;
|
|
199
|
+
}
|
|
200
|
+
// District-aware faction hook: high intruder likelihood boosts faction alert
|
|
201
|
+
if (def.controllingFaction && district.intruderLikelihood > 20) {
|
|
202
|
+
const factionCog = getFactionCognition(world, def.controllingFaction);
|
|
203
|
+
const boost = Math.round(district.intruderLikelihood * 0.1);
|
|
204
|
+
factionCog.alertLevel = Math.min(100, factionCog.alertLevel + boost);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// --- Internal Helpers ---
|
|
209
|
+
function getEventDistrictId(event, world) {
|
|
210
|
+
const zoneId = getEventZoneId(event, world);
|
|
211
|
+
if (!zoneId)
|
|
212
|
+
return undefined;
|
|
213
|
+
return getDistrictForZone(world, zoneId);
|
|
214
|
+
}
|
|
215
|
+
function getEventZoneId(event, world) {
|
|
216
|
+
if (event.payload.zoneId)
|
|
217
|
+
return event.payload.zoneId;
|
|
218
|
+
if (event.actorId)
|
|
219
|
+
return world.entities[event.actorId]?.zoneId;
|
|
220
|
+
if (event.targetIds?.[0])
|
|
221
|
+
return world.entities[event.targetIds[0]]?.zoneId;
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=district-core.js.map
|