@its-not-rocket-science/ananke 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/CHANGELOG.md +135 -0
- package/LICENSE +21 -0
- package/README.md +2199 -0
- package/STABLE_API.md +266 -0
- package/dist/src/anatomy/anatomy-compiler.d.ts +14 -0
- package/dist/src/anatomy/anatomy-compiler.js +277 -0
- package/dist/src/anatomy/anatomy-contracts.d.ts +94 -0
- package/dist/src/anatomy/anatomy-contracts.js +1 -0
- package/dist/src/anatomy/anatomy-helpers.d.ts +82 -0
- package/dist/src/anatomy/anatomy-helpers.js +233 -0
- package/dist/src/anatomy/anatomy-schema.d.ts +28 -0
- package/dist/src/anatomy/anatomy-schema.js +388 -0
- package/dist/src/anatomy/index.d.ts +4 -0
- package/dist/src/anatomy/index.js +4 -0
- package/dist/src/archetypes.d.ts +87 -0
- package/dist/src/archetypes.js +285 -0
- package/dist/src/arena.d.ts +173 -0
- package/dist/src/arena.js +695 -0
- package/dist/src/bridge/bridge-engine.d.ts +46 -0
- package/dist/src/bridge/bridge-engine.js +252 -0
- package/dist/src/bridge/index.d.ts +4 -0
- package/dist/src/bridge/index.js +5 -0
- package/dist/src/bridge/interpolation.d.ts +64 -0
- package/dist/src/bridge/interpolation.js +130 -0
- package/dist/src/bridge/mapping.d.ts +33 -0
- package/dist/src/bridge/mapping.js +54 -0
- package/dist/src/bridge/types.d.ts +94 -0
- package/dist/src/bridge/types.js +2 -0
- package/dist/src/campaign.d.ts +141 -0
- package/dist/src/campaign.js +235 -0
- package/dist/src/channels.d.ts +15 -0
- package/dist/src/channels.js +20 -0
- package/dist/src/chronicle.d.ts +124 -0
- package/dist/src/chronicle.js +232 -0
- package/dist/src/collective-activities.d.ts +154 -0
- package/dist/src/collective-activities.js +247 -0
- package/dist/src/competence/acoustic.d.ts +101 -0
- package/dist/src/competence/acoustic.js +242 -0
- package/dist/src/competence/catalogue.d.ts +30 -0
- package/dist/src/competence/catalogue.js +241 -0
- package/dist/src/competence/crafting.d.ts +35 -0
- package/dist/src/competence/crafting.js +88 -0
- package/dist/src/competence/engineering.d.ts +53 -0
- package/dist/src/competence/engineering.js +108 -0
- package/dist/src/competence/framework.d.ts +68 -0
- package/dist/src/competence/framework.js +694 -0
- package/dist/src/competence/index.d.ts +12 -0
- package/dist/src/competence/index.js +13 -0
- package/dist/src/competence/interspecies.d.ts +81 -0
- package/dist/src/competence/interspecies.js +108 -0
- package/dist/src/competence/language.d.ts +79 -0
- package/dist/src/competence/language.js +115 -0
- package/dist/src/competence/naturalist.d.ts +97 -0
- package/dist/src/competence/naturalist.js +187 -0
- package/dist/src/competence/navigation.d.ts +24 -0
- package/dist/src/competence/navigation.js +48 -0
- package/dist/src/competence/performance.d.ts +125 -0
- package/dist/src/competence/performance.js +210 -0
- package/dist/src/competence/teaching.d.ts +64 -0
- package/dist/src/competence/teaching.js +121 -0
- package/dist/src/competence/willpower.d.ts +74 -0
- package/dist/src/competence/willpower.js +114 -0
- package/dist/src/crafting/index.d.ts +55 -0
- package/dist/src/crafting/index.js +229 -0
- package/dist/src/crafting/manufacturing.d.ts +83 -0
- package/dist/src/crafting/manufacturing.js +165 -0
- package/dist/src/crafting/materials.d.ts +53 -0
- package/dist/src/crafting/materials.js +120 -0
- package/dist/src/crafting/recipes.d.ts +75 -0
- package/dist/src/crafting/recipes.js +233 -0
- package/dist/src/crafting/workshops.d.ts +61 -0
- package/dist/src/crafting/workshops.js +170 -0
- package/dist/src/debug.d.ts +86 -0
- package/dist/src/debug.js +76 -0
- package/dist/src/derive.d.ts +21 -0
- package/dist/src/derive.js +88 -0
- package/dist/src/describe.d.ts +29 -0
- package/dist/src/describe.js +276 -0
- package/dist/src/dialogue.d.ts +122 -0
- package/dist/src/dialogue.js +266 -0
- package/dist/src/dist.d.ts +20 -0
- package/dist/src/dist.js +39 -0
- package/dist/src/downtime.d.ts +89 -0
- package/dist/src/downtime.js +391 -0
- package/dist/src/economy.d.ts +116 -0
- package/dist/src/economy.js +182 -0
- package/dist/src/emotional-contagion.d.ts +142 -0
- package/dist/src/emotional-contagion.js +274 -0
- package/dist/src/equipment.d.ts +206 -0
- package/dist/src/equipment.js +598 -0
- package/dist/src/faction.d.ts +102 -0
- package/dist/src/faction.js +237 -0
- package/dist/src/generate.d.ts +35 -0
- package/dist/src/generate.js +166 -0
- package/dist/src/index.d.ts +42 -0
- package/dist/src/index.js +54 -0
- package/dist/src/inheritance.d.ts +69 -0
- package/dist/src/inheritance.js +136 -0
- package/dist/src/inventory.d.ts +194 -0
- package/dist/src/inventory.js +637 -0
- package/dist/src/item-durability.d.ts +69 -0
- package/dist/src/item-durability.js +308 -0
- package/dist/src/legend.d.ts +97 -0
- package/dist/src/legend.js +269 -0
- package/dist/src/lod.d.ts +9 -0
- package/dist/src/lod.js +84 -0
- package/dist/src/metrics.d.ts +51 -0
- package/dist/src/metrics.js +91 -0
- package/dist/src/model3d.d.ts +138 -0
- package/dist/src/model3d.js +214 -0
- package/dist/src/mythology.d.ts +101 -0
- package/dist/src/mythology.js +308 -0
- package/dist/src/narrative-render.d.ts +42 -0
- package/dist/src/narrative-render.js +194 -0
- package/dist/src/narrative-stress.d.ts +123 -0
- package/dist/src/narrative-stress.js +183 -0
- package/dist/src/narrative.d.ts +44 -0
- package/dist/src/narrative.js +257 -0
- package/dist/src/party.d.ts +70 -0
- package/dist/src/party.js +226 -0
- package/dist/src/polity.d.ts +262 -0
- package/dist/src/polity.js +398 -0
- package/dist/src/presets.d.ts +42 -0
- package/dist/src/presets.js +170 -0
- package/dist/src/progression.d.ts +170 -0
- package/dist/src/progression.js +256 -0
- package/dist/src/quest-generators.d.ts +76 -0
- package/dist/src/quest-generators.js +534 -0
- package/dist/src/quest.d.ts +239 -0
- package/dist/src/quest.js +520 -0
- package/dist/src/relationships-effects.d.ts +75 -0
- package/dist/src/relationships-effects.js +219 -0
- package/dist/src/relationships.d.ts +104 -0
- package/dist/src/relationships.js +347 -0
- package/dist/src/replay.d.ts +47 -0
- package/dist/src/replay.js +82 -0
- package/dist/src/rng.d.ts +9 -0
- package/dist/src/rng.js +37 -0
- package/dist/src/settlement-services.d.ts +67 -0
- package/dist/src/settlement-services.js +267 -0
- package/dist/src/settlement.d.ts +143 -0
- package/dist/src/settlement.js +419 -0
- package/dist/src/sim/action.d.ts +28 -0
- package/dist/src/sim/action.js +12 -0
- package/dist/src/sim/aging.d.ts +95 -0
- package/dist/src/sim/aging.js +243 -0
- package/dist/src/sim/ai/decide.d.ts +10 -0
- package/dist/src/sim/ai/decide.js +267 -0
- package/dist/src/sim/ai/perception.d.ts +12 -0
- package/dist/src/sim/ai/perception.js +54 -0
- package/dist/src/sim/ai/personality.d.ts +54 -0
- package/dist/src/sim/ai/personality.js +202 -0
- package/dist/src/sim/ai/presets.d.ts +2 -0
- package/dist/src/sim/ai/presets.js +28 -0
- package/dist/src/sim/ai/system.d.ts +6 -0
- package/dist/src/sim/ai/system.js +13 -0
- package/dist/src/sim/ai/targeting.d.ts +8 -0
- package/dist/src/sim/ai/targeting.js +42 -0
- package/dist/src/sim/ai/types.d.ts +14 -0
- package/dist/src/sim/ai/types.js +1 -0
- package/dist/src/sim/body.d.ts +9 -0
- package/dist/src/sim/body.js +32 -0
- package/dist/src/sim/bodyplan.d.ts +161 -0
- package/dist/src/sim/bodyplan.js +677 -0
- package/dist/src/sim/capability.d.ts +135 -0
- package/dist/src/sim/capability.js +8 -0
- package/dist/src/sim/combat.d.ts +21 -0
- package/dist/src/sim/combat.js +77 -0
- package/dist/src/sim/commandBuilders.d.ts +11 -0
- package/dist/src/sim/commandBuilders.js +39 -0
- package/dist/src/sim/commands.d.ts +71 -0
- package/dist/src/sim/commands.js +8 -0
- package/dist/src/sim/condition.d.ts +35 -0
- package/dist/src/sim/condition.js +21 -0
- package/dist/src/sim/cone.d.ts +40 -0
- package/dist/src/sim/cone.js +44 -0
- package/dist/src/sim/context.d.ts +68 -0
- package/dist/src/sim/context.js +1 -0
- package/dist/src/sim/density.d.ts +14 -0
- package/dist/src/sim/density.js +33 -0
- package/dist/src/sim/disease.d.ts +141 -0
- package/dist/src/sim/disease.js +353 -0
- package/dist/src/sim/entity.d.ts +251 -0
- package/dist/src/sim/entity.js +19 -0
- package/dist/src/sim/events.d.ts +25 -0
- package/dist/src/sim/events.js +5 -0
- package/dist/src/sim/explosion.d.ts +40 -0
- package/dist/src/sim/explosion.js +40 -0
- package/dist/src/sim/formation-unit.d.ts +138 -0
- package/dist/src/sim/formation-unit.js +197 -0
- package/dist/src/sim/formation.d.ts +12 -0
- package/dist/src/sim/formation.js +54 -0
- package/dist/src/sim/frontage.d.ts +30 -0
- package/dist/src/sim/frontage.js +84 -0
- package/dist/src/sim/grapple.d.ts +100 -0
- package/dist/src/sim/grapple.js +480 -0
- package/dist/src/sim/hazard.d.ts +104 -0
- package/dist/src/sim/hazard.js +201 -0
- package/dist/src/sim/hydrostatic.d.ts +58 -0
- package/dist/src/sim/hydrostatic.js +117 -0
- package/dist/src/sim/impairment.d.ts +20 -0
- package/dist/src/sim/impairment.js +162 -0
- package/dist/src/sim/indexing.d.ts +7 -0
- package/dist/src/sim/indexing.js +7 -0
- package/dist/src/sim/injury.d.ts +54 -0
- package/dist/src/sim/injury.js +66 -0
- package/dist/src/sim/intent.d.ts +26 -0
- package/dist/src/sim/intent.js +7 -0
- package/dist/src/sim/kernel.d.ts +45 -0
- package/dist/src/sim/kernel.js +1992 -0
- package/dist/src/sim/kinds.d.ts +64 -0
- package/dist/src/sim/kinds.js +56 -0
- package/dist/src/sim/knockback.d.ts +50 -0
- package/dist/src/sim/knockback.js +82 -0
- package/dist/src/sim/limb.d.ts +48 -0
- package/dist/src/sim/limb.js +78 -0
- package/dist/src/sim/medical.d.ts +32 -0
- package/dist/src/sim/medical.js +33 -0
- package/dist/src/sim/morale.d.ts +69 -0
- package/dist/src/sim/morale.js +92 -0
- package/dist/src/sim/mount.d.ts +150 -0
- package/dist/src/sim/mount.js +225 -0
- package/dist/src/sim/nutrition.d.ts +74 -0
- package/dist/src/sim/nutrition.js +168 -0
- package/dist/src/sim/occlusion.d.ts +8 -0
- package/dist/src/sim/occlusion.js +71 -0
- package/dist/src/sim/push.d.ts +11 -0
- package/dist/src/sim/push.js +79 -0
- package/dist/src/sim/ranged.d.ts +44 -0
- package/dist/src/sim/ranged.js +69 -0
- package/dist/src/sim/seeds.d.ts +3 -0
- package/dist/src/sim/seeds.js +16 -0
- package/dist/src/sim/sensory-extended.d.ts +103 -0
- package/dist/src/sim/sensory-extended.js +181 -0
- package/dist/src/sim/sensory.d.ts +38 -0
- package/dist/src/sim/sensory.js +109 -0
- package/dist/src/sim/skills.d.ts +70 -0
- package/dist/src/sim/skills.js +69 -0
- package/dist/src/sim/sleep.d.ts +107 -0
- package/dist/src/sim/sleep.js +215 -0
- package/dist/src/sim/spatial.d.ts +8 -0
- package/dist/src/sim/spatial.js +59 -0
- package/dist/src/sim/step/capability.d.ts +8 -0
- package/dist/src/sim/step/capability.js +77 -0
- package/dist/src/sim/step/concentration.d.ts +9 -0
- package/dist/src/sim/step/concentration.js +25 -0
- package/dist/src/sim/step/effects.d.ts +17 -0
- package/dist/src/sim/step/effects.js +96 -0
- package/dist/src/sim/step/energy.d.ts +3 -0
- package/dist/src/sim/step/energy.js +31 -0
- package/dist/src/sim/step/hazards.d.ts +4 -0
- package/dist/src/sim/step/hazards.js +19 -0
- package/dist/src/sim/step/injury.d.ts +10 -0
- package/dist/src/sim/step/injury.js +353 -0
- package/dist/src/sim/step/morale.d.ts +11 -0
- package/dist/src/sim/step/morale.js +130 -0
- package/dist/src/sim/step/movement.d.ts +5 -0
- package/dist/src/sim/step/movement.js +172 -0
- package/dist/src/sim/step/push.d.ts +11 -0
- package/dist/src/sim/step/push.js +79 -0
- package/dist/src/sim/step/substances.d.ts +3 -0
- package/dist/src/sim/step/substances.js +75 -0
- package/dist/src/sim/substance.d.ts +38 -0
- package/dist/src/sim/substance.js +57 -0
- package/dist/src/sim/systemic-toxicology.d.ts +109 -0
- package/dist/src/sim/systemic-toxicology.js +263 -0
- package/dist/src/sim/team.d.ts +9 -0
- package/dist/src/sim/team.js +37 -0
- package/dist/src/sim/tech.d.ts +36 -0
- package/dist/src/sim/tech.js +46 -0
- package/dist/src/sim/terrain.d.ts +121 -0
- package/dist/src/sim/terrain.js +141 -0
- package/dist/src/sim/testing.d.ts +13 -0
- package/dist/src/sim/testing.js +100 -0
- package/dist/src/sim/thermoregulation.d.ts +77 -0
- package/dist/src/sim/thermoregulation.js +161 -0
- package/dist/src/sim/tick.d.ts +3 -0
- package/dist/src/sim/tick.js +3 -0
- package/dist/src/sim/toxicology.d.ts +52 -0
- package/dist/src/sim/toxicology.js +104 -0
- package/dist/src/sim/trace.d.ts +141 -0
- package/dist/src/sim/trace.js +1 -0
- package/dist/src/sim/tuning.d.ts +16 -0
- package/dist/src/sim/tuning.js +42 -0
- package/dist/src/sim/vec3.d.ts +14 -0
- package/dist/src/sim/vec3.js +31 -0
- package/dist/src/sim/weapon_dynamics.d.ts +102 -0
- package/dist/src/sim/weapon_dynamics.js +142 -0
- package/dist/src/sim/weather.d.ts +95 -0
- package/dist/src/sim/weather.js +105 -0
- package/dist/src/sim/world.d.ts +52 -0
- package/dist/src/sim/world.js +1 -0
- package/dist/src/sim/wound-aging.d.ts +120 -0
- package/dist/src/sim/wound-aging.js +223 -0
- package/dist/src/species.d.ts +106 -0
- package/dist/src/species.js +664 -0
- package/dist/src/story-arcs.d.ts +17 -0
- package/dist/src/story-arcs.js +276 -0
- package/dist/src/tech-diffusion.d.ts +80 -0
- package/dist/src/tech-diffusion.js +185 -0
- package/dist/src/traits.d.ts +25 -0
- package/dist/src/traits.js +178 -0
- package/dist/src/types.d.ts +117 -0
- package/dist/src/types.js +1 -0
- package/dist/src/units.d.ts +41 -0
- package/dist/src/units.js +64 -0
- package/dist/src/weapons.d.ts +20 -0
- package/dist/src/weapons.js +824 -0
- package/dist/src/world-generation.d.ts +52 -0
- package/dist/src/world-generation.js +301 -0
- package/package.json +74 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// src/narrative.ts — Phase 18: Combat Narrative Layer
|
|
2
|
+
//
|
|
3
|
+
// Pure translation module — no sim/kernel dependencies.
|
|
4
|
+
// Converts TraceEvent streams and injury snapshots into human-readable text.
|
|
5
|
+
import { SCALE, q } from "./units.js";
|
|
6
|
+
import { TraceKinds } from "./sim/kinds.js";
|
|
7
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
8
|
+
function nameOf(id, cfg) {
|
|
9
|
+
return cfg.nameMap?.get(id) ?? `combatant ${id}`;
|
|
10
|
+
}
|
|
11
|
+
function isYouSubject(id, cfg) {
|
|
12
|
+
return cfg.nameMap?.get(id) === "you";
|
|
13
|
+
}
|
|
14
|
+
// Verb stems and third-person conjugations
|
|
15
|
+
const THIRD_PERSON = {
|
|
16
|
+
strike: "strikes",
|
|
17
|
+
slash: "slashes",
|
|
18
|
+
stab: "stabs",
|
|
19
|
+
bludgeon: "bludgeons",
|
|
20
|
+
shoot: "shoots",
|
|
21
|
+
snipe: "snipes",
|
|
22
|
+
blast: "blasts",
|
|
23
|
+
grapple: "grapples",
|
|
24
|
+
};
|
|
25
|
+
function conjugate(stem, youSubject) {
|
|
26
|
+
if (youSubject)
|
|
27
|
+
return stem;
|
|
28
|
+
return THIRD_PERSON[stem] ?? stem + "s";
|
|
29
|
+
}
|
|
30
|
+
function meleeStem(profile) {
|
|
31
|
+
if (!profile)
|
|
32
|
+
return "strike";
|
|
33
|
+
if (profile.penetrationBias >= q(0.65))
|
|
34
|
+
return "stab";
|
|
35
|
+
if (profile.structuralFrac >= q(0.50))
|
|
36
|
+
return "bludgeon";
|
|
37
|
+
if (profile.surfaceFrac >= q(0.50))
|
|
38
|
+
return "slash";
|
|
39
|
+
return "strike";
|
|
40
|
+
}
|
|
41
|
+
function rangedStem(profile) {
|
|
42
|
+
if (!profile)
|
|
43
|
+
return "shoot";
|
|
44
|
+
if (profile.penetrationBias >= q(0.80))
|
|
45
|
+
return "snipe";
|
|
46
|
+
if (profile.surfaceFrac >= q(0.55))
|
|
47
|
+
return "blast";
|
|
48
|
+
return "shoot";
|
|
49
|
+
}
|
|
50
|
+
function regionPhrase(region) {
|
|
51
|
+
switch (region) {
|
|
52
|
+
case "head": return "in the head";
|
|
53
|
+
case "torso": return "in the torso";
|
|
54
|
+
case "arm": return "in the arm";
|
|
55
|
+
case "leg": return "in the leg";
|
|
56
|
+
default: return `on the ${region}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function energyQualifier(energy_J) {
|
|
60
|
+
if (energy_J >= 500)
|
|
61
|
+
return "devastatingly";
|
|
62
|
+
if (energy_J >= 200)
|
|
63
|
+
return "powerfully";
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
function narrateAttack(ev, cfg) {
|
|
67
|
+
const att = nameOf(ev.attackerId, cfg);
|
|
68
|
+
const tgt = nameOf(ev.targetId, cfg);
|
|
69
|
+
if (ev.blocked) {
|
|
70
|
+
if (cfg.verbosity === "terse")
|
|
71
|
+
return null;
|
|
72
|
+
return `${att} attacks ${tgt} — blocked`;
|
|
73
|
+
}
|
|
74
|
+
if (ev.parried) {
|
|
75
|
+
if (cfg.verbosity === "terse")
|
|
76
|
+
return null;
|
|
77
|
+
return `${att} attacks ${tgt} — parried`;
|
|
78
|
+
}
|
|
79
|
+
if (ev.shieldBlocked) {
|
|
80
|
+
if (cfg.verbosity === "terse")
|
|
81
|
+
return null;
|
|
82
|
+
return `${att} attacks ${tgt} — hits shield`;
|
|
83
|
+
}
|
|
84
|
+
// Landed hit
|
|
85
|
+
if (ev.energy_J < 10) {
|
|
86
|
+
const armourNote = ev.armoured ? " (armoured)" : "";
|
|
87
|
+
return `${att} barely grazes ${tgt} ${regionPhrase(ev.region)}${armourNote}`;
|
|
88
|
+
}
|
|
89
|
+
const profile = ev.weaponId ? cfg.weaponProfiles?.get(ev.weaponId) : undefined;
|
|
90
|
+
const isYou = isYouSubject(ev.attackerId, cfg);
|
|
91
|
+
const stem = meleeStem(profile);
|
|
92
|
+
const verb = conjugate(stem, isYou);
|
|
93
|
+
const qual = energyQualifier(ev.energy_J);
|
|
94
|
+
const region = regionPhrase(ev.region);
|
|
95
|
+
const armour = ev.armoured ? " (armoured)" : "";
|
|
96
|
+
return qual
|
|
97
|
+
? `${att} ${qual} ${verb} ${tgt} ${region}${armour}`
|
|
98
|
+
: `${att} ${verb} ${tgt} ${region}${armour}`;
|
|
99
|
+
}
|
|
100
|
+
function narrateProjectileHit(ev, cfg) {
|
|
101
|
+
const shooter = nameOf(ev.shooterId, cfg);
|
|
102
|
+
const target = nameOf(ev.targetId, cfg);
|
|
103
|
+
const distM = (ev.distance_m / SCALE.m).toFixed(0);
|
|
104
|
+
if (!ev.hit) {
|
|
105
|
+
if (cfg.verbosity === "terse")
|
|
106
|
+
return null;
|
|
107
|
+
if (ev.suppressed)
|
|
108
|
+
return `${shooter} fires at ${target} (suppressive, ${distM}m)`;
|
|
109
|
+
return `${shooter} misses ${target} at ${distM}m`;
|
|
110
|
+
}
|
|
111
|
+
const profile = ev.weaponId ? cfg.weaponProfiles?.get(ev.weaponId) : undefined;
|
|
112
|
+
const isYou = isYouSubject(ev.shooterId, cfg);
|
|
113
|
+
const stem = rangedStem(profile);
|
|
114
|
+
const verb = conjugate(stem, isYou);
|
|
115
|
+
const region = ev.region ? ` in the ${ev.region}` : "";
|
|
116
|
+
return `${shooter} ${verb} ${target}${region} at ${distM}m`;
|
|
117
|
+
}
|
|
118
|
+
function narrateGrapple(ev, cfg) {
|
|
119
|
+
const att = nameOf(ev.attackerId, cfg);
|
|
120
|
+
const tgt = nameOf(ev.targetId, cfg);
|
|
121
|
+
switch (ev.phase) {
|
|
122
|
+
case "start":
|
|
123
|
+
if (cfg.verbosity === "terse")
|
|
124
|
+
return null;
|
|
125
|
+
return `${att} grapples ${tgt}`;
|
|
126
|
+
case "tick":
|
|
127
|
+
if (cfg.verbosity !== "verbose")
|
|
128
|
+
return null;
|
|
129
|
+
return `${att} maintains grapple on ${tgt}`;
|
|
130
|
+
case "break":
|
|
131
|
+
if (cfg.verbosity === "terse")
|
|
132
|
+
return null;
|
|
133
|
+
return `${tgt} breaks free from ${att}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
137
|
+
/**
|
|
138
|
+
* Narrate a single trace event.
|
|
139
|
+
* Returns null for events that should be omitted at the current verbosity level.
|
|
140
|
+
*/
|
|
141
|
+
export function narrateEvent(ev, cfg) {
|
|
142
|
+
switch (ev.kind) {
|
|
143
|
+
case TraceKinds.Attack:
|
|
144
|
+
return narrateAttack(ev, cfg);
|
|
145
|
+
case TraceKinds.ProjectileHit:
|
|
146
|
+
return narrateProjectileHit(ev, cfg);
|
|
147
|
+
case TraceKinds.KO:
|
|
148
|
+
return `${nameOf(ev.entityId, cfg)} is knocked unconscious`;
|
|
149
|
+
case TraceKinds.Death:
|
|
150
|
+
return `${nameOf(ev.entityId, cfg)} dies`;
|
|
151
|
+
case TraceKinds.MoraleRoute:
|
|
152
|
+
return `${nameOf(ev.entityId, cfg)} breaks and flees`;
|
|
153
|
+
case TraceKinds.MoraleRally:
|
|
154
|
+
return `${nameOf(ev.entityId, cfg)} rallies`;
|
|
155
|
+
case TraceKinds.Grapple:
|
|
156
|
+
return narrateGrapple(ev, cfg);
|
|
157
|
+
case TraceKinds.WeaponBind:
|
|
158
|
+
if (cfg.verbosity === "terse")
|
|
159
|
+
return null;
|
|
160
|
+
return `${nameOf(ev.attackerId, cfg)} and ${nameOf(ev.targetId, cfg)} blades lock (${ev.durationTicks} ticks)`;
|
|
161
|
+
case TraceKinds.WeaponBindBreak:
|
|
162
|
+
if (cfg.verbosity === "terse")
|
|
163
|
+
return null;
|
|
164
|
+
return `${nameOf(ev.entityId, cfg)} breaks the bind (${ev.reason})`;
|
|
165
|
+
case TraceKinds.Fracture:
|
|
166
|
+
return `${nameOf(ev.entityId, cfg)}'s ${ev.region} is fractured`;
|
|
167
|
+
case TraceKinds.BlastHit:
|
|
168
|
+
return `${nameOf(ev.entityId, cfg)} caught in explosion (${ev.blastEnergy_J}J, ${ev.fragHits} fragments)`;
|
|
169
|
+
case TraceKinds.TreatmentApplied:
|
|
170
|
+
if (cfg.verbosity !== "verbose")
|
|
171
|
+
return null;
|
|
172
|
+
return `${nameOf(ev.treaterId, cfg)} treats ${nameOf(ev.targetId, cfg)} (${ev.action}${ev.regionId ? ` — ${ev.regionId}` : ""})`;
|
|
173
|
+
case TraceKinds.CapabilityActivated:
|
|
174
|
+
if (cfg.verbosity !== "verbose")
|
|
175
|
+
return null;
|
|
176
|
+
return `${nameOf(ev.entityId, cfg)} activates ${ev.effectId}`;
|
|
177
|
+
case TraceKinds.CapabilitySuppressed:
|
|
178
|
+
if (cfg.verbosity !== "verbose")
|
|
179
|
+
return null;
|
|
180
|
+
return `${nameOf(ev.entityId, cfg)}'s ${ev.effectId} is suppressed`;
|
|
181
|
+
case TraceKinds.CastInterrupted:
|
|
182
|
+
if (cfg.verbosity !== "verbose")
|
|
183
|
+
return null;
|
|
184
|
+
return `${nameOf(ev.entityId, cfg)}'s concentration breaks`;
|
|
185
|
+
default:
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Convert a sequence of trace events into a list of narrative lines.
|
|
191
|
+
* Events that return null from narrateEvent are omitted.
|
|
192
|
+
*/
|
|
193
|
+
export function buildCombatLog(events, cfg) {
|
|
194
|
+
const lines = [];
|
|
195
|
+
for (const ev of events) {
|
|
196
|
+
const line = narrateEvent(ev, cfg);
|
|
197
|
+
if (line !== null)
|
|
198
|
+
lines.push(line);
|
|
199
|
+
}
|
|
200
|
+
return lines;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Summarise an entity's injury state as a short descriptive phrase.
|
|
204
|
+
*/
|
|
205
|
+
export function describeInjuries(injury) {
|
|
206
|
+
if (injury.dead)
|
|
207
|
+
return "Fatal";
|
|
208
|
+
const lines = [];
|
|
209
|
+
if (injury.consciousness < q(0.20))
|
|
210
|
+
lines.push("Unconscious");
|
|
211
|
+
else if (injury.consciousness < q(0.50))
|
|
212
|
+
lines.push("Semi-conscious");
|
|
213
|
+
if (injury.fluidLoss > q(0.60))
|
|
214
|
+
lines.push("Severe hemorrhage");
|
|
215
|
+
else if (injury.fluidLoss > q(0.30))
|
|
216
|
+
lines.push("Significant blood loss");
|
|
217
|
+
else if (injury.fluidLoss > q(0.10))
|
|
218
|
+
lines.push("Minor bleeding");
|
|
219
|
+
if (injury.shock > q(0.60))
|
|
220
|
+
lines.push("Deep shock");
|
|
221
|
+
else if (injury.shock > q(0.30))
|
|
222
|
+
lines.push("Shock");
|
|
223
|
+
for (const [region, ri] of Object.entries(injury.byRegion)) {
|
|
224
|
+
if (ri.fractured)
|
|
225
|
+
lines.push(`${region} fractured`);
|
|
226
|
+
}
|
|
227
|
+
if (lines.length === 0)
|
|
228
|
+
return "No significant injuries";
|
|
229
|
+
return lines.join("; ");
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Produce a one-line outcome summary for a completed engagement.
|
|
233
|
+
*/
|
|
234
|
+
export function describeCombatOutcome(combatants, tickCount) {
|
|
235
|
+
const teams = new Map();
|
|
236
|
+
for (const c of combatants) {
|
|
237
|
+
if (!teams.has(c.teamId))
|
|
238
|
+
teams.set(c.teamId, { alive: 0, total: 0 });
|
|
239
|
+
const t = teams.get(c.teamId);
|
|
240
|
+
t.total++;
|
|
241
|
+
if (!c.injury.dead && c.injury.consciousness > q(0.20))
|
|
242
|
+
t.alive++;
|
|
243
|
+
}
|
|
244
|
+
const surviving = [...teams.entries()].filter(([, t]) => t.alive > 0);
|
|
245
|
+
const defeated = [...teams.entries()].filter(([, t]) => t.alive === 0);
|
|
246
|
+
const suffix = tickCount !== undefined ? ` (${tickCount} ticks)` : "";
|
|
247
|
+
if (surviving.length === 0)
|
|
248
|
+
return "All combatants down" + suffix;
|
|
249
|
+
if (defeated.length === 0) {
|
|
250
|
+
const parts = [...teams.entries()]
|
|
251
|
+
.map(([id, t]) => `Team ${id}: ${t.alive}/${t.total} standing`);
|
|
252
|
+
return parts.join("; ") + suffix;
|
|
253
|
+
}
|
|
254
|
+
const winners = surviving.map(([id]) => `Team ${id}`).join(", ");
|
|
255
|
+
const losers = defeated.map(([id]) => `Team ${id}`).join(", ");
|
|
256
|
+
return `${winners} wins — ${losers} defeated${suffix}`;
|
|
257
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Q } from "./units.js";
|
|
2
|
+
import type { Entity } from "./sim/entity.js";
|
|
3
|
+
import type { Inventory } from "./inventory.js";
|
|
4
|
+
/** A named adventuring party with a leader and members. */
|
|
5
|
+
export interface Party {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
leaderId: number;
|
|
9
|
+
memberIds: Set<number>;
|
|
10
|
+
sharedInventory?: Inventory | undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Party-to-party standing (similar to faction standing). */
|
|
13
|
+
export type PartyStanding = Q;
|
|
14
|
+
/** Registry of all parties and their relationships. */
|
|
15
|
+
export interface PartyRegistry {
|
|
16
|
+
parties: Map<string, Party>;
|
|
17
|
+
/** partyId → (partyId → standing) */
|
|
18
|
+
relationships: Map<string, Map<string, PartyStanding>>;
|
|
19
|
+
}
|
|
20
|
+
export declare const PARTY_STANDING_HOSTILE: Q;
|
|
21
|
+
export declare const PARTY_STANDING_RIVAL: Q;
|
|
22
|
+
export declare const PARTY_STANDING_NEUTRAL: Q;
|
|
23
|
+
export declare const PARTY_STANDING_ALLY: Q;
|
|
24
|
+
export declare const PARTY_STANDING_ALLIED: Q;
|
|
25
|
+
/** Standing below this → parties treat each other as hostile. */
|
|
26
|
+
export declare const PARTY_HOSTILE_THRESHOLD: Q;
|
|
27
|
+
/** Standing above this → parties will not initiate combat. */
|
|
28
|
+
export declare const PARTY_FRIENDLY_THRESHOLD: Q;
|
|
29
|
+
/** Create an empty party registry. */
|
|
30
|
+
export declare function createPartyRegistry(): PartyRegistry;
|
|
31
|
+
/** Create a new party and add it to the registry. */
|
|
32
|
+
export declare function createParty(registry: PartyRegistry, partyId: string, name: string, leaderId: number): Party;
|
|
33
|
+
/** Add an entity to a party (also sets entity.party if entity is mutable). */
|
|
34
|
+
export declare function addPartyMember(registry: PartyRegistry, partyId: string, entityId: number, entity?: Entity): void;
|
|
35
|
+
/** Remove an entity from a party. */
|
|
36
|
+
export declare function removePartyMember(registry: PartyRegistry, partyId: string, entityId: number, entity?: Entity): void;
|
|
37
|
+
/** Change party leader. */
|
|
38
|
+
export declare function setPartyLeader(registry: PartyRegistry, partyId: string, newLeaderId: number): void;
|
|
39
|
+
/** Get party for an entity, if any. */
|
|
40
|
+
export declare function getPartyForEntity(registry: PartyRegistry, entityId: number): Party | undefined;
|
|
41
|
+
/** Get party ID for an entity, if any. */
|
|
42
|
+
export declare function getPartyIdForEntity(registry: PartyRegistry, entityId: number): string | undefined;
|
|
43
|
+
/** Get standing between two parties (default NEUTRAL). */
|
|
44
|
+
export declare function getPartyStanding(registry: PartyRegistry, partyAId: string, partyBId: string): PartyStanding;
|
|
45
|
+
/** Set standing between two parties (clamped). */
|
|
46
|
+
export declare function setPartyStanding(registry: PartyRegistry, partyAId: string, partyBId: string, standing: PartyStanding): void;
|
|
47
|
+
/** Set mutual standing between two parties (same both ways). */
|
|
48
|
+
export declare function setMutualPartyStanding(registry: PartyRegistry, partyAId: string, partyBId: string, standing: PartyStanding): void;
|
|
49
|
+
/** Determine if two parties are hostile based on standing threshold. */
|
|
50
|
+
export declare function arePartiesHostile(registry: PartyRegistry, partyAId: string, partyBId: string): boolean;
|
|
51
|
+
/** Determine if two parties are friendly (won't attack). */
|
|
52
|
+
export declare function arePartiesFriendly(registry: PartyRegistry, partyAId: string, partyBId: string): boolean;
|
|
53
|
+
/** Get standing between two entities based on party membership. */
|
|
54
|
+
export declare function getPartyStandingBetweenEntities(registry: PartyRegistry, a: Entity, b: Entity): PartyStanding | undefined;
|
|
55
|
+
/** Check if two entities are hostile based on party standing. */
|
|
56
|
+
export declare function areEntitiesHostileByParty(registry: PartyRegistry, a: Entity, b: Entity): boolean;
|
|
57
|
+
/** Check if two entities are friendly based on party standing. */
|
|
58
|
+
export declare function areEntitiesFriendlyByParty(registry: PartyRegistry, a: Entity, b: Entity): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Compute loyalty of a companion (entity) towards its party leader.
|
|
61
|
+
* Loyalty is derived from relationship affinity and trust (if relationship exists).
|
|
62
|
+
* Returns Q in [0, SCALE.Q] where 0 =背叛 (betrayal imminent), SCALE.Q = absolute loyalty.
|
|
63
|
+
*/
|
|
64
|
+
export declare function computeCompanionLoyalty(companion: Entity, leaderId: number, relationshipGraph?: import("./relationships.js").RelationshipGraph): Q;
|
|
65
|
+
export declare function getPartySharedInventory(registry: PartyRegistry, partyId: string): Inventory | undefined;
|
|
66
|
+
export declare function ensurePartySharedInventory(registry: PartyRegistry, partyId: string): Inventory;
|
|
67
|
+
/** Serialize party registry to JSON string. */
|
|
68
|
+
export declare function serializePartyRegistry(registry: PartyRegistry): string;
|
|
69
|
+
/** Deserialize party registry from JSON string. */
|
|
70
|
+
export declare function deserializePartyRegistry(json: string): PartyRegistry;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// src/party.ts — Phase 48: Multi-Party Dynamics
|
|
2
|
+
//
|
|
3
|
+
// Party system for adventuring groups, companion loyalty, and inter-party conflict.
|
|
4
|
+
// Parties are sub-groups within factions (or cross-faction) that share goals,
|
|
5
|
+
// inventory, and have relationships with other parties.
|
|
6
|
+
import { SCALE, q, clampQ } from "./units.js";
|
|
7
|
+
import { createInventory, addContainer, createContainer } from "./inventory.js";
|
|
8
|
+
// ── Standing constants ────────────────────────────────────────────────────────
|
|
9
|
+
export const PARTY_STANDING_HOSTILE = q(0.0);
|
|
10
|
+
export const PARTY_STANDING_RIVAL = q(0.20);
|
|
11
|
+
export const PARTY_STANDING_NEUTRAL = q(0.50);
|
|
12
|
+
export const PARTY_STANDING_ALLY = q(0.70);
|
|
13
|
+
export const PARTY_STANDING_ALLIED = q(1.0);
|
|
14
|
+
/** Standing below this → parties treat each other as hostile. */
|
|
15
|
+
export const PARTY_HOSTILE_THRESHOLD = q(0.30);
|
|
16
|
+
/** Standing above this → parties will not initiate combat. */
|
|
17
|
+
export const PARTY_FRIENDLY_THRESHOLD = q(0.70);
|
|
18
|
+
// ── Factory ───────────────────────────────────────────────────────────────────
|
|
19
|
+
/** Create an empty party registry. */
|
|
20
|
+
export function createPartyRegistry() {
|
|
21
|
+
return {
|
|
22
|
+
parties: new Map(),
|
|
23
|
+
relationships: new Map(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Create a new party and add it to the registry. */
|
|
27
|
+
export function createParty(registry, partyId, name, leaderId) {
|
|
28
|
+
if (registry.parties.has(partyId)) {
|
|
29
|
+
throw new Error(`Party ${partyId} already exists`);
|
|
30
|
+
}
|
|
31
|
+
const memberIds = new Set([leaderId]);
|
|
32
|
+
const party = { id: partyId, name, leaderId, memberIds, sharedInventory: undefined };
|
|
33
|
+
registry.parties.set(partyId, party);
|
|
34
|
+
// Initialize self-standing as allied
|
|
35
|
+
const selfRow = new Map();
|
|
36
|
+
selfRow.set(partyId, PARTY_STANDING_ALLIED);
|
|
37
|
+
registry.relationships.set(partyId, selfRow);
|
|
38
|
+
return party;
|
|
39
|
+
}
|
|
40
|
+
/** Add an entity to a party (also sets entity.party if entity is mutable). */
|
|
41
|
+
export function addPartyMember(registry, partyId, entityId, entity) {
|
|
42
|
+
const party = registry.parties.get(partyId);
|
|
43
|
+
if (!party) {
|
|
44
|
+
throw new Error(`Party ${partyId} not found`);
|
|
45
|
+
}
|
|
46
|
+
party.memberIds.add(entityId);
|
|
47
|
+
if (entity) {
|
|
48
|
+
entity.party = partyId;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Remove an entity from a party. */
|
|
52
|
+
export function removePartyMember(registry, partyId, entityId, entity) {
|
|
53
|
+
const party = registry.parties.get(partyId);
|
|
54
|
+
if (!party)
|
|
55
|
+
return;
|
|
56
|
+
party.memberIds.delete(entityId);
|
|
57
|
+
if (entity && entity.party === partyId) {
|
|
58
|
+
entity.party = undefined;
|
|
59
|
+
}
|
|
60
|
+
// If party becomes empty, remove it? Keep for now.
|
|
61
|
+
}
|
|
62
|
+
/** Change party leader. */
|
|
63
|
+
export function setPartyLeader(registry, partyId, newLeaderId) {
|
|
64
|
+
const party = registry.parties.get(partyId);
|
|
65
|
+
if (!party) {
|
|
66
|
+
throw new Error(`Party ${partyId} not found`);
|
|
67
|
+
}
|
|
68
|
+
if (!party.memberIds.has(newLeaderId)) {
|
|
69
|
+
throw new Error(`New leader ${newLeaderId} is not a member of party ${partyId}`);
|
|
70
|
+
}
|
|
71
|
+
party.leaderId = newLeaderId;
|
|
72
|
+
}
|
|
73
|
+
/** Get party for an entity, if any. */
|
|
74
|
+
export function getPartyForEntity(registry, entityId) {
|
|
75
|
+
for (const party of registry.parties.values()) {
|
|
76
|
+
if (party.memberIds.has(entityId)) {
|
|
77
|
+
return party;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/** Get party ID for an entity, if any. */
|
|
83
|
+
export function getPartyIdForEntity(registry, entityId) {
|
|
84
|
+
for (const [partyId, party] of registry.parties.entries()) {
|
|
85
|
+
if (party.memberIds.has(entityId)) {
|
|
86
|
+
return partyId;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
// ── Relationship Management ───────────────────────────────────────────────────
|
|
92
|
+
/** Get standing between two parties (default NEUTRAL). */
|
|
93
|
+
export function getPartyStanding(registry, partyAId, partyBId) {
|
|
94
|
+
if (partyAId === partyBId)
|
|
95
|
+
return PARTY_STANDING_ALLIED;
|
|
96
|
+
const row = registry.relationships.get(partyAId);
|
|
97
|
+
if (!row)
|
|
98
|
+
return PARTY_STANDING_NEUTRAL;
|
|
99
|
+
const standing = row.get(partyBId);
|
|
100
|
+
return standing ?? PARTY_STANDING_NEUTRAL;
|
|
101
|
+
}
|
|
102
|
+
/** Set standing between two parties (clamped). */
|
|
103
|
+
export function setPartyStanding(registry, partyAId, partyBId, standing) {
|
|
104
|
+
if (partyAId === partyBId)
|
|
105
|
+
return;
|
|
106
|
+
let row = registry.relationships.get(partyAId);
|
|
107
|
+
if (!row) {
|
|
108
|
+
row = new Map();
|
|
109
|
+
registry.relationships.set(partyAId, row);
|
|
110
|
+
}
|
|
111
|
+
row.set(partyBId, clampQ(standing, 0, SCALE.Q));
|
|
112
|
+
// Ensure symmetric row exists for partyB (but may have different standing? we keep asymmetric)
|
|
113
|
+
let rowB = registry.relationships.get(partyBId);
|
|
114
|
+
if (!rowB) {
|
|
115
|
+
rowB = new Map();
|
|
116
|
+
registry.relationships.set(partyBId, rowB);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Set mutual standing between two parties (same both ways). */
|
|
120
|
+
export function setMutualPartyStanding(registry, partyAId, partyBId, standing) {
|
|
121
|
+
setPartyStanding(registry, partyAId, partyBId, standing);
|
|
122
|
+
setPartyStanding(registry, partyBId, partyAId, standing);
|
|
123
|
+
}
|
|
124
|
+
/** Determine if two parties are hostile based on standing threshold. */
|
|
125
|
+
export function arePartiesHostile(registry, partyAId, partyBId) {
|
|
126
|
+
const standing = getPartyStanding(registry, partyAId, partyBId);
|
|
127
|
+
return standing < PARTY_HOSTILE_THRESHOLD;
|
|
128
|
+
}
|
|
129
|
+
/** Determine if two parties are friendly (won't attack). */
|
|
130
|
+
export function arePartiesFriendly(registry, partyAId, partyBId) {
|
|
131
|
+
const standing = getPartyStanding(registry, partyAId, partyBId);
|
|
132
|
+
return standing >= PARTY_FRIENDLY_THRESHOLD;
|
|
133
|
+
}
|
|
134
|
+
// ── Entity‑Level Queries ──────────────────────────────────────────────────────
|
|
135
|
+
/** Get standing between two entities based on party membership. */
|
|
136
|
+
export function getPartyStandingBetweenEntities(registry, a, b) {
|
|
137
|
+
const partyAId = a.party;
|
|
138
|
+
const partyBId = b.party;
|
|
139
|
+
if (!partyAId || !partyBId)
|
|
140
|
+
return undefined;
|
|
141
|
+
return getPartyStanding(registry, partyAId, partyBId);
|
|
142
|
+
}
|
|
143
|
+
/** Check if two entities are hostile based on party standing. */
|
|
144
|
+
export function areEntitiesHostileByParty(registry, a, b) {
|
|
145
|
+
const partyAId = a.party;
|
|
146
|
+
const partyBId = b.party;
|
|
147
|
+
if (!partyAId || !partyBId)
|
|
148
|
+
return false; // no party relationship
|
|
149
|
+
return arePartiesHostile(registry, partyAId, partyBId);
|
|
150
|
+
}
|
|
151
|
+
/** Check if two entities are friendly based on party standing. */
|
|
152
|
+
export function areEntitiesFriendlyByParty(registry, a, b) {
|
|
153
|
+
const partyAId = a.party;
|
|
154
|
+
const partyBId = b.party;
|
|
155
|
+
if (!partyAId || !partyBId)
|
|
156
|
+
return false;
|
|
157
|
+
return arePartiesFriendly(registry, partyAId, partyBId);
|
|
158
|
+
}
|
|
159
|
+
// ── Companion Loyalty ─────────────────────────────────────────────────────────
|
|
160
|
+
/**
|
|
161
|
+
* Compute loyalty of a companion (entity) towards its party leader.
|
|
162
|
+
* Loyalty is derived from relationship affinity and trust (if relationship exists).
|
|
163
|
+
* Returns Q in [0, SCALE.Q] where 0 =背叛 (betrayal imminent), SCALE.Q = absolute loyalty.
|
|
164
|
+
*/
|
|
165
|
+
export function computeCompanionLoyalty(companion, leaderId, relationshipGraph) {
|
|
166
|
+
// If no relationship graph, assume neutral loyalty (0.5)
|
|
167
|
+
if (!relationshipGraph)
|
|
168
|
+
return PARTY_STANDING_NEUTRAL;
|
|
169
|
+
const rel = relationshipGraph.relationships.get(`${Math.min(companion.id, leaderId)}:${Math.max(companion.id, leaderId)}`);
|
|
170
|
+
if (!rel)
|
|
171
|
+
return PARTY_STANDING_NEUTRAL;
|
|
172
|
+
// Loyalty = weighted combination of affinity (shifted to 0..1) and trust
|
|
173
|
+
const affinityNormalized = (rel.affinity_Q + SCALE.Q) / (2 * SCALE.Q); // -1..1 → 0..1
|
|
174
|
+
const loyalty = (affinityNormalized * 0.6 + rel.trust_Q / SCALE.Q * 0.4) * SCALE.Q;
|
|
175
|
+
return clampQ(Math.round(loyalty), 0, SCALE.Q);
|
|
176
|
+
}
|
|
177
|
+
// ── Shared Inventory ──────────────────────────────────────────────────────
|
|
178
|
+
export function getPartySharedInventory(registry, partyId) {
|
|
179
|
+
const party = registry.parties.get(partyId);
|
|
180
|
+
return party?.sharedInventory;
|
|
181
|
+
}
|
|
182
|
+
export function ensurePartySharedInventory(registry, partyId) {
|
|
183
|
+
const party = registry.parties.get(partyId);
|
|
184
|
+
if (!party)
|
|
185
|
+
throw new Error(`Party ${partyId} not found`);
|
|
186
|
+
if (!party.sharedInventory) {
|
|
187
|
+
party.sharedInventory = createInventory(-1);
|
|
188
|
+
// Add a default container
|
|
189
|
+
const container = createContainer(`shared-${partyId}`, "Shared Storage", 100 * SCALE.kg, // 100 kg capacity
|
|
190
|
+
0);
|
|
191
|
+
addContainer(party.sharedInventory, container);
|
|
192
|
+
}
|
|
193
|
+
return party.sharedInventory;
|
|
194
|
+
}
|
|
195
|
+
// ── Serialization ─────────────────────────────────────────────────────────────
|
|
196
|
+
const MAP_MARKER = "__ananke_map__";
|
|
197
|
+
const SET_MARKER = "__ananke_set__";
|
|
198
|
+
function replacer(_key, value) {
|
|
199
|
+
if (value instanceof Map) {
|
|
200
|
+
return { [MAP_MARKER]: true, entries: [...value.entries()] };
|
|
201
|
+
}
|
|
202
|
+
if (value instanceof Set) {
|
|
203
|
+
return { [SET_MARKER]: true, values: [...value.values()] };
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
function reviver(_key, value) {
|
|
208
|
+
if (value !== null && typeof value === "object") {
|
|
209
|
+
const v = value;
|
|
210
|
+
if (v[MAP_MARKER] === true) {
|
|
211
|
+
return new Map(v.entries);
|
|
212
|
+
}
|
|
213
|
+
if (v[SET_MARKER] === true) {
|
|
214
|
+
return new Set(v.values);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
/** Serialize party registry to JSON string. */
|
|
220
|
+
export function serializePartyRegistry(registry) {
|
|
221
|
+
return JSON.stringify(registry, replacer);
|
|
222
|
+
}
|
|
223
|
+
/** Deserialize party registry from JSON string. */
|
|
224
|
+
export function deserializePartyRegistry(json) {
|
|
225
|
+
return JSON.parse(json, reviver);
|
|
226
|
+
}
|