@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,214 @@
|
|
|
1
|
+
// src/model3d.ts
|
|
2
|
+
//
|
|
3
|
+
// Phase 14 — 3D Model Integration
|
|
4
|
+
//
|
|
5
|
+
// Pure data-extraction functions for driving 3D character rigs. No kernel
|
|
6
|
+
// changes, no simulation state mutations. The host renderer maps the output
|
|
7
|
+
// types to its own skeleton/animation system.
|
|
8
|
+
import { q, SCALE } from "./units.js";
|
|
9
|
+
import { DefenceModes } from "./sim/kinds.js";
|
|
10
|
+
function lateralSign(id) {
|
|
11
|
+
if (id.includes("left") || id.endsWith("_l"))
|
|
12
|
+
return -1;
|
|
13
|
+
if (id.includes("right") || id.endsWith("_r"))
|
|
14
|
+
return 1;
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Canonical vertical and lateral offset for a segment, derived from its ID
|
|
19
|
+
* by keyword matching. Used for CoG and inertia estimation.
|
|
20
|
+
*/
|
|
21
|
+
function getCanonicalOffset(segId) {
|
|
22
|
+
const id = segId.toLowerCase();
|
|
23
|
+
const lat = lateralSign(id);
|
|
24
|
+
if (/head|skull|cranium/.test(id))
|
|
25
|
+
return { xFrac: 0, yFrac: 0.94 };
|
|
26
|
+
if (/neck/.test(id))
|
|
27
|
+
return { xFrac: 0, yFrac: 0.80 };
|
|
28
|
+
if (/thorax|torso|chest|trunk/.test(id))
|
|
29
|
+
return { xFrac: 0, yFrac: 0.63 };
|
|
30
|
+
if (/abdomen|belly/.test(id))
|
|
31
|
+
return { xFrac: 0, yFrac: 0.52 };
|
|
32
|
+
if (/pelvis|hip(?!bone)/.test(id))
|
|
33
|
+
return { xFrac: 0, yFrac: 0.43 };
|
|
34
|
+
if (/shoulder/.test(id))
|
|
35
|
+
return { xFrac: lat * 0.20, yFrac: 0.72 };
|
|
36
|
+
if (/forearm|lower.?arm/.test(id))
|
|
37
|
+
return { xFrac: lat * 0.22, yFrac: 0.56 };
|
|
38
|
+
if (/upper.?arm/.test(id))
|
|
39
|
+
return { xFrac: lat * 0.20, yFrac: 0.68 };
|
|
40
|
+
if (/arm/.test(id))
|
|
41
|
+
return { xFrac: lat * 0.20, yFrac: 0.62 };
|
|
42
|
+
if (/hand/.test(id))
|
|
43
|
+
return { xFrac: lat * 0.25, yFrac: 0.44 };
|
|
44
|
+
if (/thigh|upper.?leg/.test(id))
|
|
45
|
+
return { xFrac: lat * 0.07, yFrac: 0.33 };
|
|
46
|
+
if (/shin|calf|lower.?leg|foreleg|midleg|hindleg/.test(id))
|
|
47
|
+
return { xFrac: lat * 0.07, yFrac: 0.17 };
|
|
48
|
+
if (/leg/.test(id))
|
|
49
|
+
return { xFrac: lat * 0.07, yFrac: 0.25 };
|
|
50
|
+
if (/foot|hoof|paw/.test(id))
|
|
51
|
+
return { xFrac: lat * 0.07, yFrac: 0.03 };
|
|
52
|
+
if (/tail/.test(id))
|
|
53
|
+
return { xFrac: 0, yFrac: 0.35 };
|
|
54
|
+
if (/wing/.test(id))
|
|
55
|
+
return { xFrac: lat * 0.50, yFrac: 0.65 };
|
|
56
|
+
return { xFrac: 0, yFrac: 0.50 }; // unknown: geometric midpoint
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Derive mass distribution and centre of gravity from entity body plan.
|
|
60
|
+
* Falls back to a single "body" segment at the geometric midpoint when no
|
|
61
|
+
* body plan is present.
|
|
62
|
+
*/
|
|
63
|
+
export function deriveMassDistribution(entity) {
|
|
64
|
+
const stature_m = entity.attributes.morphology.stature_m / SCALE.m;
|
|
65
|
+
if (entity.bodyPlan) {
|
|
66
|
+
const segs = entity.bodyPlan.segments;
|
|
67
|
+
const totalMass_kg = segs.reduce((s, seg) => s + seg.mass_kg, 0);
|
|
68
|
+
const totalReal = totalMass_kg / SCALE.kg;
|
|
69
|
+
let cogX = 0;
|
|
70
|
+
let cogY = 0;
|
|
71
|
+
const segments = segs.map(seg => {
|
|
72
|
+
const off = getCanonicalOffset(seg.id);
|
|
73
|
+
const mass_r = seg.mass_kg / SCALE.kg;
|
|
74
|
+
cogX += mass_r * off.xFrac * stature_m;
|
|
75
|
+
cogY += mass_r * off.yFrac * stature_m;
|
|
76
|
+
const fractionQ = (totalMass_kg > 0
|
|
77
|
+
? Math.min(SCALE.Q, Math.round((seg.mass_kg / totalMass_kg) * SCALE.Q))
|
|
78
|
+
: 0);
|
|
79
|
+
return { segmentId: seg.id, mass_kg: seg.mass_kg, fractionQ };
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
totalMass_kg,
|
|
83
|
+
segments,
|
|
84
|
+
cogOffset_m: {
|
|
85
|
+
x: totalReal > 0 ? cogX / totalReal : 0,
|
|
86
|
+
y: totalReal > 0 ? cogY / totalReal : stature_m / 2,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// No body plan: single point mass at geometric midpoint
|
|
91
|
+
const totalMass_kg = entity.attributes.morphology.mass_kg;
|
|
92
|
+
return {
|
|
93
|
+
totalMass_kg,
|
|
94
|
+
segments: [{ segmentId: "body", mass_kg: totalMass_kg, fractionQ: SCALE.Q }],
|
|
95
|
+
cogOffset_m: { x: 0, y: stature_m / 2 },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Derive a simplified diagonal inertia tensor from entity body plan.
|
|
100
|
+
* Falls back to a solid-sphere approximation when no body plan is present.
|
|
101
|
+
*/
|
|
102
|
+
export function deriveInertiaTensor(entity) {
|
|
103
|
+
const stature_m = entity.attributes.morphology.stature_m / SCALE.m;
|
|
104
|
+
if (entity.bodyPlan) {
|
|
105
|
+
let I_yaw = 0, I_pitch = 0, I_roll = 0;
|
|
106
|
+
for (const seg of entity.bodyPlan.segments) {
|
|
107
|
+
const off = getCanonicalOffset(seg.id);
|
|
108
|
+
const m = seg.mass_kg / SCALE.kg;
|
|
109
|
+
const x = off.xFrac * stature_m;
|
|
110
|
+
const y = off.yFrac * stature_m;
|
|
111
|
+
// I = m × r_perp² for rotation about each axis (z = 0)
|
|
112
|
+
I_yaw += m * (x * x);
|
|
113
|
+
I_pitch += m * (y * y);
|
|
114
|
+
I_roll += m * (x * x + y * y);
|
|
115
|
+
}
|
|
116
|
+
return { yaw_kgm2: I_yaw, pitch_kgm2: I_pitch, roll_kgm2: I_roll };
|
|
117
|
+
}
|
|
118
|
+
// Solid-sphere approximation: I = 2/5 × m × r²
|
|
119
|
+
const m = entity.attributes.morphology.mass_kg / SCALE.kg;
|
|
120
|
+
const r = stature_m / 4; // effective radius ≈ stature / 4
|
|
121
|
+
const I = 0.4 * m * r * r;
|
|
122
|
+
return { yaw_kgm2: I, pitch_kgm2: I, roll_kgm2: I };
|
|
123
|
+
}
|
|
124
|
+
/** Consciousness threshold below which the entity is treated as unconscious for animation. */
|
|
125
|
+
const ANIM_UNCONSCIOUS_THRESHOLD = q(0.20);
|
|
126
|
+
/**
|
|
127
|
+
* Derive animation hints from entity intent, condition, and injury state.
|
|
128
|
+
*/
|
|
129
|
+
export function deriveAnimationHints(entity) {
|
|
130
|
+
const dead = entity.injury.dead;
|
|
131
|
+
const unconscious = !dead && entity.injury.consciousness < ANIM_UNCONSCIOUS_THRESHOLD;
|
|
132
|
+
const prone = entity.intent.prone
|
|
133
|
+
|| entity.grapple.position === "prone"
|
|
134
|
+
|| entity.grapple.position === "pinned";
|
|
135
|
+
// Locomotion — zero out when not mobile
|
|
136
|
+
const mobile = !dead && !unconscious;
|
|
137
|
+
let idle = q(0);
|
|
138
|
+
let walk = q(0);
|
|
139
|
+
let run = q(0);
|
|
140
|
+
let sprint = q(0);
|
|
141
|
+
let crawl = q(0);
|
|
142
|
+
if (mobile) {
|
|
143
|
+
if (entity.intent.move.intensity === 0)
|
|
144
|
+
idle = SCALE.Q;
|
|
145
|
+
else if (entity.intent.move.mode === "walk")
|
|
146
|
+
walk = SCALE.Q;
|
|
147
|
+
else if (entity.intent.move.mode === "run")
|
|
148
|
+
run = SCALE.Q;
|
|
149
|
+
else if (entity.intent.move.mode === "sprint")
|
|
150
|
+
sprint = SCALE.Q;
|
|
151
|
+
else if (entity.intent.move.mode === "crawl")
|
|
152
|
+
crawl = SCALE.Q;
|
|
153
|
+
else
|
|
154
|
+
idle = SCALE.Q;
|
|
155
|
+
}
|
|
156
|
+
const guardingQ = (!dead && entity.intent.defence.mode !== DefenceModes.None
|
|
157
|
+
? entity.intent.defence.intensity
|
|
158
|
+
: q(0));
|
|
159
|
+
const attackingQ = (!dead && entity.action.attackCooldownTicks > 0
|
|
160
|
+
? SCALE.Q : 0);
|
|
161
|
+
return {
|
|
162
|
+
idle, walk, run, sprint, crawl,
|
|
163
|
+
guardingQ,
|
|
164
|
+
attackingQ,
|
|
165
|
+
shockQ: entity.injury.shock,
|
|
166
|
+
fearQ: entity.condition.fearQ ?? 0,
|
|
167
|
+
prone,
|
|
168
|
+
unconscious,
|
|
169
|
+
dead,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Derive per-region pose modifiers from entity injury state.
|
|
174
|
+
* Returns one entry per injury region (byRegion keys).
|
|
175
|
+
*/
|
|
176
|
+
export function derivePoseModifiers(entity) {
|
|
177
|
+
return Object.entries(entity.injury.byRegion).map(([segmentId, region]) => ({
|
|
178
|
+
segmentId,
|
|
179
|
+
structuralQ: region.structuralDamage,
|
|
180
|
+
surfaceQ: region.surfaceDamage,
|
|
181
|
+
impairmentQ: Math.max(region.structuralDamage, region.surfaceDamage),
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Derive grapple pose constraint from entity grapple state.
|
|
186
|
+
*/
|
|
187
|
+
export function deriveGrappleConstraint(entity) {
|
|
188
|
+
const g = entity.grapple;
|
|
189
|
+
return {
|
|
190
|
+
isHolder: g.holdingTargetId !== 0,
|
|
191
|
+
...(g.holdingTargetId !== 0 ? { holdingEntityId: g.holdingTargetId } : {}),
|
|
192
|
+
isHeld: g.heldByIds.length > 0,
|
|
193
|
+
heldByIds: [...g.heldByIds],
|
|
194
|
+
position: g.position,
|
|
195
|
+
gripQ: g.gripQ,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Extract a full rig snapshot for every entity in the world.
|
|
200
|
+
* Combine with extractMotionVectors and extractConditionSamples from
|
|
201
|
+
* src/debug.ts for a complete per-tick visualisation feed.
|
|
202
|
+
*/
|
|
203
|
+
export function extractRigSnapshots(world) {
|
|
204
|
+
return world.entities.map(e => ({
|
|
205
|
+
entityId: e.id,
|
|
206
|
+
teamId: e.teamId,
|
|
207
|
+
tick: world.tick,
|
|
208
|
+
mass: deriveMassDistribution(e),
|
|
209
|
+
inertia: deriveInertiaTensor(e),
|
|
210
|
+
animation: deriveAnimationHints(e),
|
|
211
|
+
pose: derivePoseModifiers(e),
|
|
212
|
+
grapple: deriveGrappleConstraint(e),
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type Q } from "./units.js";
|
|
2
|
+
import type { LegendRegistry } from "./legend.js";
|
|
3
|
+
import type { ChronicleEntry } from "./chronicle.js";
|
|
4
|
+
/** Six narrative archetypes produced by the compression pass. */
|
|
5
|
+
export type MythArchetype = "hero" | "monster" | "great_plague" | "divine_wrath" | "golden_age" | "trickster";
|
|
6
|
+
/**
|
|
7
|
+
* Cultural effects a myth applies to every member of a believing faction.
|
|
8
|
+
* All fields are signed Q deltas; 0 = no effect.
|
|
9
|
+
*/
|
|
10
|
+
export interface MythEffect {
|
|
11
|
+
/**
|
|
12
|
+
* Additive fear-threshold modifier for faction members in combat.
|
|
13
|
+
* Positive = braver (higher threshold before fleeing).
|
|
14
|
+
* Range advisory: ±q(0.10).
|
|
15
|
+
*/
|
|
16
|
+
fearThresholdMod_Q: Q;
|
|
17
|
+
/**
|
|
18
|
+
* Additive modifier to diplomacy success probability vs. outsiders.
|
|
19
|
+
* Positive = more trusted; negative = feared/distrusted.
|
|
20
|
+
*/
|
|
21
|
+
diplomacyMod_Q: Q;
|
|
22
|
+
/**
|
|
23
|
+
* Morale bonus when a faction member fights on behalf of the myth.
|
|
24
|
+
* (e.g., "fighting for the legacy of the hero").
|
|
25
|
+
*/
|
|
26
|
+
moraleBonus_Q: Q;
|
|
27
|
+
/**
|
|
28
|
+
* Technology research speed modifier for believing factions.
|
|
29
|
+
* Positive = golden-age ambition; negative = fatalistic stagnation.
|
|
30
|
+
*/
|
|
31
|
+
techMod_Q: Q;
|
|
32
|
+
}
|
|
33
|
+
/** An in-world myth held by one or more factions. */
|
|
34
|
+
export interface Myth {
|
|
35
|
+
id: string;
|
|
36
|
+
archetype: MythArchetype;
|
|
37
|
+
/** Display name, e.g. "The Hero of the Eastern March". */
|
|
38
|
+
name: string;
|
|
39
|
+
/** One-sentence description of the myth's content. */
|
|
40
|
+
description: string;
|
|
41
|
+
/** Legend IDs and chronicle entry IDs that seeded this myth. */
|
|
42
|
+
sourceIds: string[];
|
|
43
|
+
/** Faction IDs that currently hold this belief. */
|
|
44
|
+
believingFactionIds: string[];
|
|
45
|
+
/** Simulated days since the myth crystallised. */
|
|
46
|
+
ageInDays: number;
|
|
47
|
+
/** How widely and deeply the myth is believed [0, SCALE.Q]. */
|
|
48
|
+
belief_Q: Q;
|
|
49
|
+
/** Cultural modifiers for believing factions. */
|
|
50
|
+
effects: MythEffect;
|
|
51
|
+
}
|
|
52
|
+
/** Collection of all myths in a world. */
|
|
53
|
+
export interface MythRegistry {
|
|
54
|
+
myths: Map<string, Myth>;
|
|
55
|
+
}
|
|
56
|
+
/** Minimum number of related entries/legends required to trigger a myth. */
|
|
57
|
+
export declare const MYTH_MIN_ENTRIES = 3;
|
|
58
|
+
/** Maximum tick window (days) within which a death cluster counts as a "plague". */
|
|
59
|
+
export declare const PLAGUE_WINDOW_DAYS = 30;
|
|
60
|
+
/** Minimum death count in window to trigger great_plague myth. */
|
|
61
|
+
export declare const PLAGUE_MIN_DEATHS = 3;
|
|
62
|
+
/** Min run of constructive events to trigger golden_age myth. */
|
|
63
|
+
export declare const GOLDEN_AGE_MIN_EVENTS = 5;
|
|
64
|
+
/** Annual belief decay (fraction of belief_Q lost per simulated year). */
|
|
65
|
+
export declare const BELIEF_DECAY_PER_YEAR_Q: Q;
|
|
66
|
+
/** Belief floor — myths never fall below this once formed. */
|
|
67
|
+
export declare const BELIEF_FLOOR_Q: Q;
|
|
68
|
+
export declare function createMythRegistry(): MythRegistry;
|
|
69
|
+
export declare function registerMyth(registry: MythRegistry, myth: Myth): void;
|
|
70
|
+
export declare function getMythsByFaction(registry: MythRegistry, factionId: string): Myth[];
|
|
71
|
+
/**
|
|
72
|
+
* Run the narrative compression pass over a LegendRegistry and chronicle entry
|
|
73
|
+
* list, returning any myths that emerge.
|
|
74
|
+
*
|
|
75
|
+
* @param legendRegistry Phase 50 legend registry.
|
|
76
|
+
* @param entries Flat list of chronicle entries (all scopes combined).
|
|
77
|
+
* @param believingFactionIds Faction IDs that will adopt the resulting myths.
|
|
78
|
+
* @param ticksPerDay How many simulation ticks equal one simulated day
|
|
79
|
+
* (used for time-window calculations). Default: 20.
|
|
80
|
+
*/
|
|
81
|
+
export declare function compressMythsFromHistory(legendRegistry: LegendRegistry, entries: ReadonlyArray<ChronicleEntry>, believingFactionIds: string[], ticksPerDay?: number): Myth[];
|
|
82
|
+
/**
|
|
83
|
+
* Age all myths by one simulated year and decay belief.
|
|
84
|
+
* Myths below BELIEF_FLOOR_Q are NOT removed — they linger as faded folklore.
|
|
85
|
+
* Returns the updated registry (mutates in place).
|
|
86
|
+
*/
|
|
87
|
+
export declare function stepMythologyYear(registry: MythRegistry): void;
|
|
88
|
+
/**
|
|
89
|
+
* Scale a MythEffect by the current belief_Q of the myth.
|
|
90
|
+
* A barely-believed myth (belief_Q = q(0.10)) contributes only 10% of its
|
|
91
|
+
* face-value effect.
|
|
92
|
+
*/
|
|
93
|
+
export declare function scaledMythEffect(myth: Myth): MythEffect;
|
|
94
|
+
/**
|
|
95
|
+
* Aggregate the net cultural effect on a faction from all its myths.
|
|
96
|
+
* Each myth's effect is scaled by its current belief_Q.
|
|
97
|
+
*
|
|
98
|
+
* The host applies these deltas to polity morale, faction diplomacy weights,
|
|
99
|
+
* and tech-advance probability each polity-day.
|
|
100
|
+
*/
|
|
101
|
+
export declare function aggregateFactionMythEffect(registry: MythRegistry, factionId: string): MythEffect;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// src/mythology.ts — Phase 66: Generative Mythology
|
|
2
|
+
//
|
|
3
|
+
// Applies narrative compression to a Legend/Chronicle log, crystallising
|
|
4
|
+
// recurring patterns into in-world myths held by factions.
|
|
5
|
+
//
|
|
6
|
+
// The compression pass scans for five archetypal patterns:
|
|
7
|
+
// hero — heroic legend(s) of a named individual
|
|
8
|
+
// monster — notorious legend(s) with high menace
|
|
9
|
+
// great_plague — cluster of entity_death / tragic events in a short window
|
|
10
|
+
// divine_wrath — settlement_destroyed coinciding with mass deaths
|
|
11
|
+
// golden_age — run of masterwork_crafted / settlement_founded with no conflict
|
|
12
|
+
// trickster — relationship_betrayal + quest_failed pattern
|
|
13
|
+
//
|
|
14
|
+
// Each myth carries cultural effects (MythEffect) that modifiers faction
|
|
15
|
+
// behaviour: fear threshold, diplomacy probability, battle morale, and
|
|
16
|
+
// technological ambition. Effects are applied by the host each polity-day.
|
|
17
|
+
//
|
|
18
|
+
// Phase hooks:
|
|
19
|
+
// Phase 50 (Legend): Legend, LegendRegistry — source material for hero/monster
|
|
20
|
+
// Phase 56 (Disease): entity_death cluster triggers great_plague myth
|
|
21
|
+
// Phase 60 (Hazard): settlement_destroyed triggers divine_wrath
|
|
22
|
+
// Phase 24 (Faction): believingFactionIds gates who is influenced
|
|
23
|
+
// Phase 47 (Personality): legend tags map to myth personality impact
|
|
24
|
+
import { q, clampQ, qMul, SCALE } from "./units.js";
|
|
25
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
26
|
+
/** Minimum number of related entries/legends required to trigger a myth. */
|
|
27
|
+
export const MYTH_MIN_ENTRIES = 3;
|
|
28
|
+
/** Maximum tick window (days) within which a death cluster counts as a "plague". */
|
|
29
|
+
export const PLAGUE_WINDOW_DAYS = 30;
|
|
30
|
+
/** Minimum death count in window to trigger great_plague myth. */
|
|
31
|
+
export const PLAGUE_MIN_DEATHS = 3;
|
|
32
|
+
/** Min run of constructive events to trigger golden_age myth. */
|
|
33
|
+
export const GOLDEN_AGE_MIN_EVENTS = 5;
|
|
34
|
+
/** Annual belief decay (fraction of belief_Q lost per simulated year). */
|
|
35
|
+
export const BELIEF_DECAY_PER_YEAR_Q = q(0.12);
|
|
36
|
+
/** Belief floor — myths never fall below this once formed. */
|
|
37
|
+
export const BELIEF_FLOOR_Q = q(0.10);
|
|
38
|
+
// ── Effect profiles per archetype ─────────────────────────────────────────────
|
|
39
|
+
const EFFECTS = {
|
|
40
|
+
hero: {
|
|
41
|
+
fearThresholdMod_Q: q(0.08), // believers are braver
|
|
42
|
+
diplomacyMod_Q: q(0.05), // hero's fame aids negotiation
|
|
43
|
+
moraleBonus_Q: q(0.10), // fighting in the hero's name
|
|
44
|
+
techMod_Q: q(0),
|
|
45
|
+
},
|
|
46
|
+
monster: {
|
|
47
|
+
fearThresholdMod_Q: q(-0.10), // monster story makes foes scarier
|
|
48
|
+
diplomacyMod_Q: q(-0.05), // fear of outsiders
|
|
49
|
+
moraleBonus_Q: q(0.05), // "slaying the monster" narrative
|
|
50
|
+
techMod_Q: q(0),
|
|
51
|
+
},
|
|
52
|
+
great_plague: {
|
|
53
|
+
fearThresholdMod_Q: q(-0.05),
|
|
54
|
+
diplomacyMod_Q: q(-0.08), // blame outsiders for plague
|
|
55
|
+
moraleBonus_Q: q(0),
|
|
56
|
+
techMod_Q: q(-0.05), // fatalistic stagnation
|
|
57
|
+
},
|
|
58
|
+
divine_wrath: {
|
|
59
|
+
fearThresholdMod_Q: q(-0.08),
|
|
60
|
+
diplomacyMod_Q: q(0.03), // appeasing gods through ritual
|
|
61
|
+
moraleBonus_Q: q(-0.05),
|
|
62
|
+
techMod_Q: q(-0.03),
|
|
63
|
+
},
|
|
64
|
+
golden_age: {
|
|
65
|
+
fearThresholdMod_Q: q(0.04),
|
|
66
|
+
diplomacyMod_Q: q(0.08), // pride of civilisation
|
|
67
|
+
moraleBonus_Q: q(0.06),
|
|
68
|
+
techMod_Q: q(0.10), // ambition to recapture greatness
|
|
69
|
+
},
|
|
70
|
+
trickster: {
|
|
71
|
+
fearThresholdMod_Q: q(0.03), // wariness, not fear
|
|
72
|
+
diplomacyMod_Q: q(-0.06), // distrust of deals
|
|
73
|
+
moraleBonus_Q: q(0),
|
|
74
|
+
techMod_Q: q(0.03), // cunning seen as virtue
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
// ── Registry helpers ──────────────────────────────────────────────────────────
|
|
78
|
+
export function createMythRegistry() {
|
|
79
|
+
return { myths: new Map() };
|
|
80
|
+
}
|
|
81
|
+
export function registerMyth(registry, myth) {
|
|
82
|
+
registry.myths.set(myth.id, myth);
|
|
83
|
+
}
|
|
84
|
+
export function getMythsByFaction(registry, factionId) {
|
|
85
|
+
const result = [];
|
|
86
|
+
for (const myth of registry.myths.values()) {
|
|
87
|
+
if (myth.believingFactionIds.includes(factionId))
|
|
88
|
+
result.push(myth);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
// ── Myth construction helpers ─────────────────────────────────────────────────
|
|
93
|
+
let _nextId = 1;
|
|
94
|
+
function nextMythId() { return `myth_${_nextId++}`; }
|
|
95
|
+
function mkMyth(archetype, name, description, sourceIds, believingFactionIds, belief_Q = q(0.80)) {
|
|
96
|
+
return {
|
|
97
|
+
id: nextMythId(),
|
|
98
|
+
archetype,
|
|
99
|
+
name,
|
|
100
|
+
description,
|
|
101
|
+
sourceIds,
|
|
102
|
+
believingFactionIds,
|
|
103
|
+
ageInDays: 0,
|
|
104
|
+
belief_Q,
|
|
105
|
+
effects: EFFECTS[archetype],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// ── Pattern detectors ─────────────────────────────────────────────────────────
|
|
109
|
+
/**
|
|
110
|
+
* Derive hero myths from `heroic` legends in the LegendRegistry.
|
|
111
|
+
* One hero myth per heroic legend with fame > q(0.30).
|
|
112
|
+
*/
|
|
113
|
+
function detectHeroMyths(legendRegistry, believingFactionIds) {
|
|
114
|
+
const myths = [];
|
|
115
|
+
for (const legend of legendRegistry.legends.values()) {
|
|
116
|
+
if (legend.reputation !== "heroic" && legend.reputation !== "legendary")
|
|
117
|
+
continue;
|
|
118
|
+
if (legend.fame_Q < q(0.30))
|
|
119
|
+
continue;
|
|
120
|
+
myths.push(mkMyth("hero", `The ${legend.reputation === "legendary" ? "Legend" : "Hero"} of ${legend.subjectName}`, `Tales of ${legend.subjectName}'s deeds pass from mouth to mouth, shaping the values of the people.`, [legend.legendId], believingFactionIds, clampQ(legend.fame_Q, q(0.30), SCALE.Q)));
|
|
121
|
+
}
|
|
122
|
+
return myths;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Derive monster myths from `notorious` legends.
|
|
126
|
+
*/
|
|
127
|
+
function detectMonsterMyths(legendRegistry, believingFactionIds) {
|
|
128
|
+
const myths = [];
|
|
129
|
+
for (const legend of legendRegistry.legends.values()) {
|
|
130
|
+
if (legend.reputation !== "notorious")
|
|
131
|
+
continue;
|
|
132
|
+
if (legend.fame_Q < q(0.20))
|
|
133
|
+
continue;
|
|
134
|
+
myths.push(mkMyth("monster", `The Shadow of ${legend.subjectName}`, `Fear of what ${legend.subjectName} wrought lingers in the communal memory.`, [legend.legendId], believingFactionIds, clampQ(legend.fame_Q, q(0.20), SCALE.Q)));
|
|
135
|
+
}
|
|
136
|
+
return myths;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Detect great_plague myth: PLAGUE_MIN_DEATHS entity_death / tragic entries
|
|
140
|
+
* within PLAGUE_WINDOW_DAYS of each other.
|
|
141
|
+
*
|
|
142
|
+
* Tick parameter is "ticks per day" so windows can be compared.
|
|
143
|
+
*/
|
|
144
|
+
function detectPlagueMyth(entries, believingFactionIds, ticksPerDay) {
|
|
145
|
+
const deathTypes = new Set(["entity_death", "tragic_event"]);
|
|
146
|
+
const deaths = entries.filter(e => deathTypes.has(e.eventType))
|
|
147
|
+
.sort((a, b) => a.tick - b.tick);
|
|
148
|
+
if (deaths.length < PLAGUE_MIN_DEATHS)
|
|
149
|
+
return null;
|
|
150
|
+
const windowTicks = PLAGUE_WINDOW_DAYS * ticksPerDay;
|
|
151
|
+
let maxCluster = 0;
|
|
152
|
+
let clusterEntries = [];
|
|
153
|
+
for (let i = 0; i < deaths.length; i++) {
|
|
154
|
+
const window = deaths.filter(e => e.tick >= deaths[i].tick && e.tick <= deaths[i].tick + windowTicks);
|
|
155
|
+
if (window.length > maxCluster) {
|
|
156
|
+
maxCluster = window.length;
|
|
157
|
+
clusterEntries = window;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (maxCluster < PLAGUE_MIN_DEATHS)
|
|
161
|
+
return null;
|
|
162
|
+
return mkMyth("great_plague", "The Great Pestilence", `A time when death walked among the people, and no healer could stem the tide.`, clusterEntries.map(e => e.entryId), believingFactionIds, clampQ(Math.round(maxCluster * SCALE.Q / 10), q(0.40), SCALE.Q));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Detect divine_wrath myth: settlement_destroyed entry + death cluster within
|
|
166
|
+
* the same tick window.
|
|
167
|
+
*/
|
|
168
|
+
function detectDivineWrathMyth(entries, believingFactionIds, ticksPerDay) {
|
|
169
|
+
const destructions = entries.filter(e => e.eventType === "settlement_destroyed");
|
|
170
|
+
if (destructions.length === 0)
|
|
171
|
+
return null;
|
|
172
|
+
const windowTicks = 14 * ticksPerDay; // 14-day window around destruction
|
|
173
|
+
for (const destruction of destructions) {
|
|
174
|
+
const nearby = entries.filter(e => e.eventType === "entity_death" &&
|
|
175
|
+
Math.abs(e.tick - destruction.tick) <= windowTicks);
|
|
176
|
+
if (nearby.length >= 2) {
|
|
177
|
+
return mkMyth("divine_wrath", "The Wrath That Fell", `When the settlement crumbled and the dead lay unburied, the people spoke of divine judgement.`, [destruction.entryId, ...nearby.map(e => e.entryId)], believingFactionIds);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Detect golden_age myth: GOLDEN_AGE_MIN_EVENTS consecutive constructive events
|
|
184
|
+
* (masterwork_crafted, settlement_founded, settlement_upgraded, facility_completed)
|
|
185
|
+
* without any combat_defeat or settlement_raided in the same window.
|
|
186
|
+
*/
|
|
187
|
+
function detectGoldenAgeMyth(entries, believingFactionIds) {
|
|
188
|
+
const positiveTypes = new Set([
|
|
189
|
+
"masterwork_crafted", "settlement_founded", "settlement_upgraded", "facility_completed",
|
|
190
|
+
]);
|
|
191
|
+
const negativeTypes = new Set([
|
|
192
|
+
"combat_defeat", "settlement_raided", "settlement_destroyed",
|
|
193
|
+
]);
|
|
194
|
+
const sorted = [...entries].sort((a, b) => a.tick - b.tick);
|
|
195
|
+
let streak = [];
|
|
196
|
+
for (const entry of sorted) {
|
|
197
|
+
if (negativeTypes.has(entry.eventType)) {
|
|
198
|
+
streak = [];
|
|
199
|
+
}
|
|
200
|
+
else if (positiveTypes.has(entry.eventType)) {
|
|
201
|
+
streak.push(entry);
|
|
202
|
+
if (streak.length >= GOLDEN_AGE_MIN_EVENTS) {
|
|
203
|
+
return mkMyth("golden_age", "The Golden Age of Craft", `A remembered era of flourishing — settlements rose, masters plied their arts, and the land prospered.`, streak.map(e => e.entryId), believingFactionIds);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Detect trickster myth: at least one relationship_betrayal + one quest_failed
|
|
211
|
+
* anywhere in the chronicle.
|
|
212
|
+
*/
|
|
213
|
+
function detectTricksterMyth(entries, believingFactionIds) {
|
|
214
|
+
const betrayals = entries.filter(e => e.eventType === "relationship_betrayal");
|
|
215
|
+
const failures = entries.filter(e => e.eventType === "quest_failed");
|
|
216
|
+
if (betrayals.length === 0 || failures.length === 0)
|
|
217
|
+
return null;
|
|
218
|
+
const sourceIds = [
|
|
219
|
+
...betrayals.slice(0, 2).map(e => e.entryId),
|
|
220
|
+
...failures.slice(0, 2).map(e => e.entryId),
|
|
221
|
+
];
|
|
222
|
+
return mkMyth("trickster", "The Deceiver of Pacts", `Stories of broken oaths and failed ventures teach that trust must be earned, never assumed.`, sourceIds, believingFactionIds, q(0.60));
|
|
223
|
+
}
|
|
224
|
+
// ── Main compression pass ─────────────────────────────────────────────────────
|
|
225
|
+
/**
|
|
226
|
+
* Run the narrative compression pass over a LegendRegistry and chronicle entry
|
|
227
|
+
* list, returning any myths that emerge.
|
|
228
|
+
*
|
|
229
|
+
* @param legendRegistry Phase 50 legend registry.
|
|
230
|
+
* @param entries Flat list of chronicle entries (all scopes combined).
|
|
231
|
+
* @param believingFactionIds Faction IDs that will adopt the resulting myths.
|
|
232
|
+
* @param ticksPerDay How many simulation ticks equal one simulated day
|
|
233
|
+
* (used for time-window calculations). Default: 20.
|
|
234
|
+
*/
|
|
235
|
+
export function compressMythsFromHistory(legendRegistry, entries, believingFactionIds, ticksPerDay = 20) {
|
|
236
|
+
const myths = [];
|
|
237
|
+
myths.push(...detectHeroMyths(legendRegistry, believingFactionIds));
|
|
238
|
+
myths.push(...detectMonsterMyths(legendRegistry, believingFactionIds));
|
|
239
|
+
const plague = detectPlagueMyth(entries, believingFactionIds, ticksPerDay);
|
|
240
|
+
if (plague)
|
|
241
|
+
myths.push(plague);
|
|
242
|
+
const wrath = detectDivineWrathMyth(entries, believingFactionIds, ticksPerDay);
|
|
243
|
+
if (wrath)
|
|
244
|
+
myths.push(wrath);
|
|
245
|
+
const golden = detectGoldenAgeMyth(entries, believingFactionIds);
|
|
246
|
+
if (golden)
|
|
247
|
+
myths.push(golden);
|
|
248
|
+
const trickster = detectTricksterMyth(entries, believingFactionIds);
|
|
249
|
+
if (trickster)
|
|
250
|
+
myths.push(trickster);
|
|
251
|
+
return myths;
|
|
252
|
+
}
|
|
253
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────────
|
|
254
|
+
/**
|
|
255
|
+
* Age all myths by one simulated year and decay belief.
|
|
256
|
+
* Myths below BELIEF_FLOOR_Q are NOT removed — they linger as faded folklore.
|
|
257
|
+
* Returns the updated registry (mutates in place).
|
|
258
|
+
*/
|
|
259
|
+
export function stepMythologyYear(registry) {
|
|
260
|
+
for (const myth of registry.myths.values()) {
|
|
261
|
+
myth.ageInDays += 365;
|
|
262
|
+
const decayed = qMul(myth.belief_Q, SCALE.Q - BELIEF_DECAY_PER_YEAR_Q);
|
|
263
|
+
myth.belief_Q = clampQ(decayed, BELIEF_FLOOR_Q, SCALE.Q);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Scale a MythEffect by the current belief_Q of the myth.
|
|
268
|
+
* A barely-believed myth (belief_Q = q(0.10)) contributes only 10% of its
|
|
269
|
+
* face-value effect.
|
|
270
|
+
*/
|
|
271
|
+
export function scaledMythEffect(myth) {
|
|
272
|
+
const scale = myth.belief_Q;
|
|
273
|
+
const s = (v) => Math.round(v * scale / SCALE.Q);
|
|
274
|
+
return {
|
|
275
|
+
fearThresholdMod_Q: s(myth.effects.fearThresholdMod_Q),
|
|
276
|
+
diplomacyMod_Q: s(myth.effects.diplomacyMod_Q),
|
|
277
|
+
moraleBonus_Q: s(myth.effects.moraleBonus_Q),
|
|
278
|
+
techMod_Q: s(myth.effects.techMod_Q),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Aggregate the net cultural effect on a faction from all its myths.
|
|
283
|
+
* Each myth's effect is scaled by its current belief_Q.
|
|
284
|
+
*
|
|
285
|
+
* The host applies these deltas to polity morale, faction diplomacy weights,
|
|
286
|
+
* and tech-advance probability each polity-day.
|
|
287
|
+
*/
|
|
288
|
+
export function aggregateFactionMythEffect(registry, factionId) {
|
|
289
|
+
let fearMod = 0;
|
|
290
|
+
let diploMod = 0;
|
|
291
|
+
let morale = 0;
|
|
292
|
+
let tech = 0;
|
|
293
|
+
for (const myth of registry.myths.values()) {
|
|
294
|
+
if (!myth.believingFactionIds.includes(factionId))
|
|
295
|
+
continue;
|
|
296
|
+
const eff = scaledMythEffect(myth);
|
|
297
|
+
fearMod += eff.fearThresholdMod_Q;
|
|
298
|
+
diploMod += eff.diplomacyMod_Q;
|
|
299
|
+
morale += eff.moraleBonus_Q;
|
|
300
|
+
tech += eff.techMod_Q;
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
fearThresholdMod_Q: clampQ(fearMod, -SCALE.Q, SCALE.Q),
|
|
304
|
+
diplomacyMod_Q: clampQ(diploMod, -SCALE.Q, SCALE.Q),
|
|
305
|
+
moraleBonus_Q: clampQ(morale, 0, SCALE.Q),
|
|
306
|
+
techMod_Q: clampQ(tech, -SCALE.Q, SCALE.Q),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ChronicleEntry, ChronicleEventType, StoryArc } from "./chronicle.js";
|
|
2
|
+
/** A template function that generates prose from entry variables. */
|
|
3
|
+
type TemplateFn = (vars: Record<string, string | number>) => string;
|
|
4
|
+
/** Generate a summary for a story arc. */
|
|
5
|
+
export declare function renderArcSummary(arc: StoryArc): string;
|
|
6
|
+
/** Render a chronicle entry to prose. */
|
|
7
|
+
export declare function renderEntry(entry: ChronicleEntry): string;
|
|
8
|
+
/** Render an entry with full context. */
|
|
9
|
+
export declare function renderEntryVerbose(entry: ChronicleEntry): string;
|
|
10
|
+
export interface RenderOptions {
|
|
11
|
+
/** Include significance scores. */
|
|
12
|
+
showSignificance?: boolean;
|
|
13
|
+
/** Include actor IDs. */
|
|
14
|
+
showActors?: boolean;
|
|
15
|
+
/** Include settlement IDs. */
|
|
16
|
+
showSettlements?: boolean;
|
|
17
|
+
/** Minimum significance to include. */
|
|
18
|
+
minSignificance?: number;
|
|
19
|
+
/** Format: prose | chronological | compact */
|
|
20
|
+
format?: "prose" | "chronological" | "compact";
|
|
21
|
+
}
|
|
22
|
+
/** Render multiple entries to a narrative. */
|
|
23
|
+
export declare function renderChronicle(entries: ChronicleEntry[], options?: RenderOptions): string;
|
|
24
|
+
/** Render a complete story arc with its entries. */
|
|
25
|
+
export declare function renderArcNarrative(arc: StoryArc, entryMap: Map<string, ChronicleEntry>): string;
|
|
26
|
+
/** Render all arcs in a chronicle. */
|
|
27
|
+
export declare function renderAllArcs(arcs: StoryArc[], entryMap: Map<string, ChronicleEntry>): string;
|
|
28
|
+
/** Register a custom template for an event type. */
|
|
29
|
+
export declare function registerTemplate(eventType: ChronicleEventType, templateFn: TemplateFn): void;
|
|
30
|
+
/** Register multiple templates at once. */
|
|
31
|
+
export declare function registerTemplates(templates: Partial<Record<ChronicleEventType, TemplateFn>>): void;
|
|
32
|
+
export interface GeneratedNarrative {
|
|
33
|
+
title: string;
|
|
34
|
+
summary: string;
|
|
35
|
+
fullText: string;
|
|
36
|
+
keyFigures: number[];
|
|
37
|
+
keyEvents: string[];
|
|
38
|
+
estimatedDrama: number;
|
|
39
|
+
}
|
|
40
|
+
/** Generate a complete narrative from a set of arcs and entries. */
|
|
41
|
+
export declare function generateNarrative(arcs: StoryArc[], entries: ChronicleEntry[]): GeneratedNarrative;
|
|
42
|
+
export {};
|