@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,269 @@
|
|
|
1
|
+
// src/legend.ts — Phase 50: Mythology & Legend
|
|
2
|
+
//
|
|
3
|
+
// Chronicle entries of sufficient significance crystallise into Legends —
|
|
4
|
+
// persistent reputation objects that NPCs query when deciding how to treat
|
|
5
|
+
// an entity. Legends affect dialogue (persuasion / intimidation probability),
|
|
6
|
+
// NPC fear, and ally morale.
|
|
7
|
+
//
|
|
8
|
+
// Data flow:
|
|
9
|
+
// Chronicle (Phase 45) → createLegendFromChronicle → Legend
|
|
10
|
+
// Legend + NPC → npcKnowsLegend (fame roll) → LegendEffect modifiers
|
|
11
|
+
// LegendEffect → applyLegendToDialogueContext → bonus Q values for dialogue.ts
|
|
12
|
+
import { SCALE, q, clampQ, mulDiv } from "./units.js";
|
|
13
|
+
import { eventSeed } from "./sim/seeds.js";
|
|
14
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
15
|
+
/** Minimum significance score for a chronicle entry to contribute to a legend. */
|
|
16
|
+
export const LEGEND_MIN_SIGNIFICANCE = 60;
|
|
17
|
+
/** Denominator for fame_Q computation: 7 entries at max significance → q(1.0). */
|
|
18
|
+
const FAME_SIGNIFICANCE_DIVISOR = 700;
|
|
19
|
+
/** Fame floor for "legendary" reputation — never decays below this. */
|
|
20
|
+
const LEGENDARY_FAME_FLOOR = q(0.50);
|
|
21
|
+
/** Fame below this threshold → reputation becomes "forgotten". */
|
|
22
|
+
const FORGOTTEN_FAME_THRESHOLD = q(0.10);
|
|
23
|
+
/** Fame decay per 1000 ticks (slow, long-term erosion). */
|
|
24
|
+
const FAME_DECAY_PER_1000_TICKS = 5;
|
|
25
|
+
// ── Tag derivation ────────────────────────────────────────────────────────────
|
|
26
|
+
const EVENT_TAGS = {
|
|
27
|
+
legendary_deed: "legendary_deed",
|
|
28
|
+
combat_victory: "warrior",
|
|
29
|
+
masterwork_crafted: "craftsman",
|
|
30
|
+
settlement_founded: "builder",
|
|
31
|
+
settlement_upgraded: "builder",
|
|
32
|
+
first_contact: "explorer",
|
|
33
|
+
quest_completed: "quester",
|
|
34
|
+
entity_death: "slayer",
|
|
35
|
+
rank_promotion: "officer",
|
|
36
|
+
};
|
|
37
|
+
const ARC_TAGS = {
|
|
38
|
+
rise_of_hero: "hero",
|
|
39
|
+
tragic_fall: "fallen_hero",
|
|
40
|
+
rivalry: "rival",
|
|
41
|
+
legendary_craftsman: "master_craftsman",
|
|
42
|
+
notorious_villain: "villain",
|
|
43
|
+
unlikely_friendship: "friend",
|
|
44
|
+
betrayal_and_redemption: "redeemed",
|
|
45
|
+
};
|
|
46
|
+
/** Derive thematic tags from chronicle entries and arc types. */
|
|
47
|
+
function deriveTagsFromEntries(entries, arcTypes) {
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
for (const e of entries) {
|
|
50
|
+
const tag = EVENT_TAGS[e.eventType];
|
|
51
|
+
if (tag)
|
|
52
|
+
seen.add(tag);
|
|
53
|
+
}
|
|
54
|
+
for (const arc of arcTypes) {
|
|
55
|
+
const tag = ARC_TAGS[arc];
|
|
56
|
+
if (tag)
|
|
57
|
+
seen.add(tag);
|
|
58
|
+
}
|
|
59
|
+
return Array.from(seen);
|
|
60
|
+
}
|
|
61
|
+
// ── Reputation classification ──────────────────────────────────────────────────
|
|
62
|
+
/** Classify reputation from entries, arcs, and computed fame. */
|
|
63
|
+
function classifyReputation(entries, arcTypes, fame_Q) {
|
|
64
|
+
if (fame_Q < FORGOTTEN_FAME_THRESHOLD)
|
|
65
|
+
return "forgotten";
|
|
66
|
+
const positiveArcs = [
|
|
67
|
+
"rise_of_hero", "settlement_growth", "legendary_craftsman", "unlikely_friendship",
|
|
68
|
+
"betrayal_and_redemption",
|
|
69
|
+
];
|
|
70
|
+
const negativeArcs = ["notorious_villain", "tragic_fall"];
|
|
71
|
+
const hasPositiveArc = arcTypes.some(a => positiveArcs.includes(a));
|
|
72
|
+
const hasNegativeArc = arcTypes.some(a => negativeArcs.includes(a));
|
|
73
|
+
const hasLegendaryDeed = entries.some(e => e.eventType === "legendary_deed");
|
|
74
|
+
// Legendary = both positive and negative arcs, or legendary deed + high fame
|
|
75
|
+
if ((hasPositiveArc && hasNegativeArc) || (hasLegendaryDeed && fame_Q >= q(0.60))) {
|
|
76
|
+
return "legendary";
|
|
77
|
+
}
|
|
78
|
+
if (hasNegativeArc)
|
|
79
|
+
return "notorious";
|
|
80
|
+
return "heroic";
|
|
81
|
+
}
|
|
82
|
+
// ── Fame computation ──────────────────────────────────────────────────────────
|
|
83
|
+
/** Compute fame_Q from total significance of qualifying entries. */
|
|
84
|
+
function computeFameQ(entries, minSignificance) {
|
|
85
|
+
const total = entries
|
|
86
|
+
.filter(e => e.significance >= minSignificance)
|
|
87
|
+
.reduce((sum, e) => sum + e.significance, 0);
|
|
88
|
+
return clampQ(Math.trunc(total * SCALE.Q / FAME_SIGNIFICANCE_DIVISOR), q(0), SCALE.Q);
|
|
89
|
+
}
|
|
90
|
+
// ── Legend creation ───────────────────────────────────────────────────────────
|
|
91
|
+
/**
|
|
92
|
+
* Promote chronicle entries above `minSignificance` into a Legend.
|
|
93
|
+
*
|
|
94
|
+
* Returns `undefined` if no qualifying entries exist.
|
|
95
|
+
* Considers only entries where `subjectId` appears in the actors array.
|
|
96
|
+
*/
|
|
97
|
+
export function createLegendFromChronicle(chronicle, subjectId, subjectName, minSignificance = LEGEND_MIN_SIGNIFICANCE) {
|
|
98
|
+
const qualifyingEntries = chronicle.entries.filter(e => e.significance >= minSignificance && e.actors.includes(subjectId));
|
|
99
|
+
if (qualifyingEntries.length === 0)
|
|
100
|
+
return undefined;
|
|
101
|
+
const fame_Q = computeFameQ(qualifyingEntries, minSignificance);
|
|
102
|
+
const arcTypes = chronicle.detectedArcs.flatMap(arc => arc.primaryActors.includes(subjectId) ? [arc.arcType] : []);
|
|
103
|
+
const tags = deriveTagsFromEntries(qualifyingEntries, arcTypes);
|
|
104
|
+
const reputation = classifyReputation(qualifyingEntries, arcTypes, fame_Q);
|
|
105
|
+
const createdAtTick = qualifyingEntries[qualifyingEntries.length - 1].tick;
|
|
106
|
+
const legendId = `legend_${subjectId}_${createdAtTick}`;
|
|
107
|
+
return {
|
|
108
|
+
legendId,
|
|
109
|
+
subjectId,
|
|
110
|
+
subjectName,
|
|
111
|
+
reputation,
|
|
112
|
+
fame_Q,
|
|
113
|
+
tags,
|
|
114
|
+
sourceEntryIds: qualifyingEntries.map(e => e.entryId),
|
|
115
|
+
sourceArcTypes: arcTypes,
|
|
116
|
+
createdAtTick,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
120
|
+
/** Create a new empty legend registry. */
|
|
121
|
+
export function createLegendRegistry() {
|
|
122
|
+
return { legends: new Map(), bySubject: new Map() };
|
|
123
|
+
}
|
|
124
|
+
/** Register a legend in the registry. Overwrites any existing legend with the same ID. */
|
|
125
|
+
export function registerLegend(registry, legend) {
|
|
126
|
+
registry.legends.set(legend.legendId, legend);
|
|
127
|
+
let ids = registry.bySubject.get(legend.subjectId);
|
|
128
|
+
if (!ids) {
|
|
129
|
+
ids = new Set();
|
|
130
|
+
registry.bySubject.set(legend.subjectId, ids);
|
|
131
|
+
}
|
|
132
|
+
ids.add(legend.legendId);
|
|
133
|
+
}
|
|
134
|
+
/** Get all legends about a specific entity. */
|
|
135
|
+
export function getLegendsBySubject(registry, subjectId) {
|
|
136
|
+
const ids = registry.bySubject.get(subjectId);
|
|
137
|
+
if (!ids)
|
|
138
|
+
return [];
|
|
139
|
+
return Array.from(ids)
|
|
140
|
+
.map(id => registry.legends.get(id))
|
|
141
|
+
.filter((l) => l !== undefined);
|
|
142
|
+
}
|
|
143
|
+
// ── Effects ───────────────────────────────────────────────────────────────────
|
|
144
|
+
/** Derive NPC-behavior modifiers from a legend. */
|
|
145
|
+
export function getLegendEffect(legend) {
|
|
146
|
+
const f = legend.fame_Q;
|
|
147
|
+
switch (legend.reputation) {
|
|
148
|
+
case "heroic":
|
|
149
|
+
return {
|
|
150
|
+
persuasionBonus_Q: mulDiv(f, q(0.20), SCALE.Q),
|
|
151
|
+
intimidationBonus_Q: q(0),
|
|
152
|
+
fearBonus_Q: q(0),
|
|
153
|
+
moraleBonus_Q: mulDiv(f, q(0.10), SCALE.Q),
|
|
154
|
+
};
|
|
155
|
+
case "notorious":
|
|
156
|
+
return {
|
|
157
|
+
persuasionBonus_Q: q(0),
|
|
158
|
+
intimidationBonus_Q: mulDiv(f, q(0.25), SCALE.Q),
|
|
159
|
+
fearBonus_Q: mulDiv(f, q(0.15), SCALE.Q),
|
|
160
|
+
moraleBonus_Q: q(0),
|
|
161
|
+
};
|
|
162
|
+
case "legendary":
|
|
163
|
+
return {
|
|
164
|
+
persuasionBonus_Q: mulDiv(f, q(0.25), SCALE.Q),
|
|
165
|
+
intimidationBonus_Q: mulDiv(f, q(0.20), SCALE.Q),
|
|
166
|
+
fearBonus_Q: mulDiv(f, q(0.10), SCALE.Q),
|
|
167
|
+
moraleBonus_Q: mulDiv(f, q(0.15), SCALE.Q),
|
|
168
|
+
};
|
|
169
|
+
case "forgotten":
|
|
170
|
+
default:
|
|
171
|
+
return {
|
|
172
|
+
persuasionBonus_Q: q(0),
|
|
173
|
+
intimidationBonus_Q: q(0),
|
|
174
|
+
fearBonus_Q: q(0),
|
|
175
|
+
moraleBonus_Q: q(0),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ── NPC awareness ─────────────────────────────────────────────────────────────
|
|
180
|
+
/**
|
|
181
|
+
* Determine whether an NPC "knows" a legend.
|
|
182
|
+
*
|
|
183
|
+
* Deterministic: same (legend, npcId, worldSeed, tick) → same result.
|
|
184
|
+
* Probability = `legend.fame_Q / SCALE.Q`.
|
|
185
|
+
*/
|
|
186
|
+
export function npcKnowsLegend(legend, npcId, worldSeed, tick) {
|
|
187
|
+
if (legend.fame_Q <= q(0))
|
|
188
|
+
return false;
|
|
189
|
+
if (legend.fame_Q >= SCALE.Q)
|
|
190
|
+
return true;
|
|
191
|
+
// Deterministic salt from legendId characters
|
|
192
|
+
const legendSalt = legend.legendId
|
|
193
|
+
.split("")
|
|
194
|
+
.reduce((acc, c) => (acc + c.charCodeAt(0)) & 0xFFFFFF, 0);
|
|
195
|
+
const seed = eventSeed(worldSeed, tick, npcId, 0, legendSalt);
|
|
196
|
+
return (seed % SCALE.Q) < legend.fame_Q;
|
|
197
|
+
}
|
|
198
|
+
// ── Dialogue integration ──────────────────────────────────────────────────────
|
|
199
|
+
/**
|
|
200
|
+
* Aggregate legend effects for initiatorId, filtered by what targetId (NPC) knows.
|
|
201
|
+
*
|
|
202
|
+
* Called before dialogue resolution to get bonus Q values that shift
|
|
203
|
+
* persuasion/intimidation/fear probabilities.
|
|
204
|
+
*/
|
|
205
|
+
export function applyLegendToDialogueContext(initiatorId, targetId, registry, worldSeed, tick) {
|
|
206
|
+
let persuasionBonus = 0;
|
|
207
|
+
let intimidationBonus = 0;
|
|
208
|
+
let fearBonus = 0;
|
|
209
|
+
for (const legend of getLegendsBySubject(registry, initiatorId)) {
|
|
210
|
+
if (!npcKnowsLegend(legend, targetId, worldSeed, tick))
|
|
211
|
+
continue;
|
|
212
|
+
const effect = getLegendEffect(legend);
|
|
213
|
+
persuasionBonus += effect.persuasionBonus_Q;
|
|
214
|
+
intimidationBonus += effect.intimidationBonus_Q;
|
|
215
|
+
fearBonus += effect.fearBonus_Q;
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
persuasionBonus_Q: clampQ(persuasionBonus, q(0), q(0.50)),
|
|
219
|
+
intimidationBonus_Q: clampQ(intimidationBonus, q(0), q(0.50)),
|
|
220
|
+
fearBonus_Q: clampQ(fearBonus, q(0), q(0.50)),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// ── Fame decay ────────────────────────────────────────────────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* Decay fame on all registered legends over `deltaTicks` time.
|
|
226
|
+
*
|
|
227
|
+
* - "legendary" reputation has a hard floor at q(0.50).
|
|
228
|
+
* - All other reputations decay freely.
|
|
229
|
+
* - Legends whose fame_Q falls below FORGOTTEN_FAME_THRESHOLD are reclassified as "forgotten".
|
|
230
|
+
* - fame_Q never goes below 0.
|
|
231
|
+
*/
|
|
232
|
+
export function stepLegendFame(registry, deltaTicks) {
|
|
233
|
+
if (deltaTicks <= 0)
|
|
234
|
+
return;
|
|
235
|
+
const decay = Math.trunc(deltaTicks * FAME_DECAY_PER_1000_TICKS / 1000);
|
|
236
|
+
if (decay <= 0)
|
|
237
|
+
return;
|
|
238
|
+
for (const legend of registry.legends.values()) {
|
|
239
|
+
const floor = legend.reputation === "legendary" ? LEGENDARY_FAME_FLOOR : q(0);
|
|
240
|
+
legend.fame_Q = clampQ((legend.fame_Q - decay), floor, SCALE.Q);
|
|
241
|
+
if (legend.fame_Q < FORGOTTEN_FAME_THRESHOLD && legend.reputation !== "legendary") {
|
|
242
|
+
legend.reputation = "forgotten";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// ── Serialization ─────────────────────────────────────────────────────────────
|
|
247
|
+
/** Serialize legend registry to JSON-friendly object. */
|
|
248
|
+
export function serializeLegendRegistry(registry) {
|
|
249
|
+
return {
|
|
250
|
+
legends: Array.from(registry.legends.entries()),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** Deserialize legend registry. */
|
|
254
|
+
export function deserializeLegendRegistry(data) {
|
|
255
|
+
const registry = createLegendRegistry();
|
|
256
|
+
const d = data;
|
|
257
|
+
if (Array.isArray(d.legends)) {
|
|
258
|
+
for (const [id, legend] of d.legends) {
|
|
259
|
+
registry.legends.set(id, legend);
|
|
260
|
+
let ids = registry.bySubject.get(legend.subjectId);
|
|
261
|
+
if (!ids) {
|
|
262
|
+
ids = new Set();
|
|
263
|
+
registry.bySubject.set(legend.subjectId, ids);
|
|
264
|
+
}
|
|
265
|
+
ids.add(id);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return registry;
|
|
269
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IndividualAttributes } from "./types.js";
|
|
2
|
+
import { type Q } from "./units.js";
|
|
3
|
+
export interface SquadAggregate {
|
|
4
|
+
count: number;
|
|
5
|
+
mean: IndividualAttributes;
|
|
6
|
+
cohesion: Q;
|
|
7
|
+
training: Q;
|
|
8
|
+
}
|
|
9
|
+
export declare function aggregateSquad(members: IndividualAttributes[]): SquadAggregate;
|
package/dist/src/lod.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { q } from "./units.js";
|
|
2
|
+
import { DEFAULT_PERCEPTION } from "./sim/sensory.js";
|
|
3
|
+
export function aggregateSquad(members) {
|
|
4
|
+
const n = Math.max(1, members.length);
|
|
5
|
+
const sum = {
|
|
6
|
+
morphology: { stature_m: 0, mass_kg: 0, actuatorMass_kg: 0, actuatorScale: 0, structureScale: 0, reachScale: 0 },
|
|
7
|
+
performance: { peakForce_N: 0, peakPower_W: 0, continuousPower_W: 0, reserveEnergy_J: 0, conversionEfficiency: 0 },
|
|
8
|
+
control: { controlQuality: 0, reactionTime_s: 0, stability: 0, fineControl: 0 },
|
|
9
|
+
resilience: {
|
|
10
|
+
surfaceIntegrity: 0, bulkIntegrity: 0, structureIntegrity: 0,
|
|
11
|
+
distressTolerance: 0, shockTolerance: 0, concussionTolerance: 0,
|
|
12
|
+
heatTolerance: 0, coldTolerance: 0,
|
|
13
|
+
fatigueRate: 0, recoveryRate: 0,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
for (const m of members) {
|
|
17
|
+
sum.morphology.stature_m += m.morphology.stature_m;
|
|
18
|
+
sum.morphology.mass_kg += m.morphology.mass_kg;
|
|
19
|
+
sum.morphology.actuatorMass_kg += m.morphology.actuatorMass_kg;
|
|
20
|
+
sum.morphology.actuatorScale += m.morphology.actuatorScale;
|
|
21
|
+
sum.morphology.structureScale += m.morphology.structureScale;
|
|
22
|
+
sum.morphology.reachScale += m.morphology.reachScale;
|
|
23
|
+
sum.performance.peakForce_N += m.performance.peakForce_N;
|
|
24
|
+
sum.performance.peakPower_W += m.performance.peakPower_W;
|
|
25
|
+
sum.performance.continuousPower_W += m.performance.continuousPower_W;
|
|
26
|
+
sum.performance.reserveEnergy_J += m.performance.reserveEnergy_J;
|
|
27
|
+
sum.performance.conversionEfficiency += m.performance.conversionEfficiency;
|
|
28
|
+
sum.control.controlQuality += m.control.controlQuality;
|
|
29
|
+
sum.control.reactionTime_s += m.control.reactionTime_s;
|
|
30
|
+
sum.control.stability += m.control.stability;
|
|
31
|
+
sum.control.fineControl += m.control.fineControl;
|
|
32
|
+
sum.resilience.surfaceIntegrity += m.resilience.surfaceIntegrity;
|
|
33
|
+
sum.resilience.bulkIntegrity += m.resilience.bulkIntegrity;
|
|
34
|
+
sum.resilience.structureIntegrity += m.resilience.structureIntegrity;
|
|
35
|
+
sum.resilience.distressTolerance += m.resilience.distressTolerance;
|
|
36
|
+
sum.resilience.shockTolerance += m.resilience.shockTolerance;
|
|
37
|
+
sum.resilience.concussionTolerance += m.resilience.concussionTolerance;
|
|
38
|
+
sum.resilience.heatTolerance += m.resilience.heatTolerance;
|
|
39
|
+
sum.resilience.coldTolerance += m.resilience.coldTolerance;
|
|
40
|
+
sum.resilience.fatigueRate += m.resilience.fatigueRate;
|
|
41
|
+
sum.resilience.recoveryRate += m.resilience.recoveryRate;
|
|
42
|
+
}
|
|
43
|
+
const div = (x) => Math.trunc(x / n);
|
|
44
|
+
const divQ = (x) => Math.trunc(x / n);
|
|
45
|
+
const mean = {
|
|
46
|
+
morphology: {
|
|
47
|
+
stature_m: div(sum.morphology.stature_m),
|
|
48
|
+
mass_kg: div(sum.morphology.mass_kg),
|
|
49
|
+
actuatorMass_kg: div(sum.morphology.actuatorMass_kg),
|
|
50
|
+
actuatorScale: divQ(sum.morphology.actuatorScale),
|
|
51
|
+
structureScale: divQ(sum.morphology.structureScale),
|
|
52
|
+
reachScale: divQ(sum.morphology.reachScale),
|
|
53
|
+
},
|
|
54
|
+
performance: {
|
|
55
|
+
peakForce_N: div(sum.performance.peakForce_N),
|
|
56
|
+
peakPower_W: div(sum.performance.peakPower_W),
|
|
57
|
+
continuousPower_W: div(sum.performance.continuousPower_W),
|
|
58
|
+
reserveEnergy_J: div(sum.performance.reserveEnergy_J),
|
|
59
|
+
conversionEfficiency: divQ(sum.performance.conversionEfficiency),
|
|
60
|
+
},
|
|
61
|
+
control: {
|
|
62
|
+
controlQuality: divQ(sum.control.controlQuality),
|
|
63
|
+
reactionTime_s: divQ(sum.control.reactionTime_s),
|
|
64
|
+
stability: divQ(sum.control.stability),
|
|
65
|
+
fineControl: div(sum.control.fineControl),
|
|
66
|
+
},
|
|
67
|
+
resilience: {
|
|
68
|
+
surfaceIntegrity: divQ(sum.resilience.surfaceIntegrity),
|
|
69
|
+
bulkIntegrity: divQ(sum.resilience.bulkIntegrity),
|
|
70
|
+
structureIntegrity: divQ(sum.resilience.structureIntegrity),
|
|
71
|
+
distressTolerance: divQ(sum.resilience.distressTolerance),
|
|
72
|
+
shockTolerance: divQ(sum.resilience.shockTolerance),
|
|
73
|
+
concussionTolerance: divQ(sum.resilience.concussionTolerance),
|
|
74
|
+
heatTolerance: divQ(sum.resilience.heatTolerance),
|
|
75
|
+
coldTolerance: divQ(sum.resilience.coldTolerance),
|
|
76
|
+
fatigueRate: divQ(sum.resilience.fatigueRate),
|
|
77
|
+
recoveryRate: div(sum.resilience.recoveryRate),
|
|
78
|
+
},
|
|
79
|
+
// Perception is a species characteristic, not averaged across individuals.
|
|
80
|
+
// Use DEFAULT_PERCEPTION as a neutral aggregate placeholder.
|
|
81
|
+
perception: DEFAULT_PERCEPTION,
|
|
82
|
+
};
|
|
83
|
+
return { count: n, mean, cohesion: q(0.75), training: q(0.65) };
|
|
84
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { TraceEvent, TraceSink } from "./sim/trace.js";
|
|
2
|
+
/** Accumulated metrics derived from a sequence of trace events. */
|
|
3
|
+
export interface CombatMetrics {
|
|
4
|
+
/** Total energy (J) delivered by each attacker entity (melee + ranged). */
|
|
5
|
+
damageDealt: Map<number, number>;
|
|
6
|
+
/** Number of successful hit events (melee Attack or ranged hit) per attacker. */
|
|
7
|
+
hitsLanded: Map<number, number>;
|
|
8
|
+
/** Number of times each entity was hit. */
|
|
9
|
+
hitsTaken: Map<number, number>;
|
|
10
|
+
/** First tick at which each entity received a KO event. */
|
|
11
|
+
tickOfKO: Map<number, number>;
|
|
12
|
+
/** First tick at which each entity died. */
|
|
13
|
+
tickOfDeath: Map<number, number>;
|
|
14
|
+
/**
|
|
15
|
+
* First tick at which each entity was incapacitated (KO or death, whichever came first).
|
|
16
|
+
* Entities that remained combat-capable throughout are absent from this map.
|
|
17
|
+
*/
|
|
18
|
+
tickToIncapacitation: Map<number, number>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Derive combat metrics from a flat array of trace events.
|
|
22
|
+
* Events from any number of ticks may be mixed; ordering is not required.
|
|
23
|
+
*/
|
|
24
|
+
export declare function collectMetrics(events: readonly TraceEvent[]): CombatMetrics;
|
|
25
|
+
/**
|
|
26
|
+
* Fraction of `entityIds` that were never incapacitated (KO or death) in `events`.
|
|
27
|
+
* Returns 1.0 if `entityIds` is empty.
|
|
28
|
+
*/
|
|
29
|
+
export declare function survivalRate(events: readonly TraceEvent[], entityIds: readonly number[]): number;
|
|
30
|
+
/**
|
|
31
|
+
* Mean tick-to-incapacitation across the given entities.
|
|
32
|
+
* Entities that were never incapacitated contribute `totalTicks` to the average
|
|
33
|
+
* (i.e. they survived the full duration).
|
|
34
|
+
*
|
|
35
|
+
* Returns `totalTicks` if no entity was incapacitated.
|
|
36
|
+
*/
|
|
37
|
+
export declare function meanTimeToIncapacitation(events: readonly TraceEvent[], entityIds: readonly number[], totalTicks: number): number;
|
|
38
|
+
/**
|
|
39
|
+
* A TraceSink that accumulates all events into an array for later analysis.
|
|
40
|
+
*
|
|
41
|
+
* Usage:
|
|
42
|
+
* const tracer = new CollectingTrace();
|
|
43
|
+
* stepWorld(world, cmds, { ...ctx, trace: tracer });
|
|
44
|
+
* const metrics = collectMetrics(tracer.events);
|
|
45
|
+
*/
|
|
46
|
+
export declare class CollectingTrace implements TraceSink {
|
|
47
|
+
readonly events: TraceEvent[];
|
|
48
|
+
onEvent(ev: TraceEvent): void;
|
|
49
|
+
/** Remove all accumulated events. */
|
|
50
|
+
clear(): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/metrics.ts — Phase 13: combat metrics and analytics
|
|
2
|
+
import { TraceKinds } from "./sim/kinds.js";
|
|
3
|
+
// ── Core analytics ────────────────────────────────────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* Derive combat metrics from a flat array of trace events.
|
|
6
|
+
* Events from any number of ticks may be mixed; ordering is not required.
|
|
7
|
+
*/
|
|
8
|
+
export function collectMetrics(events) {
|
|
9
|
+
const damageDealt = new Map();
|
|
10
|
+
const hitsLanded = new Map();
|
|
11
|
+
const hitsTaken = new Map();
|
|
12
|
+
const tickOfKO = new Map();
|
|
13
|
+
const tickOfDeath = new Map();
|
|
14
|
+
for (const ev of events) {
|
|
15
|
+
if (ev.kind === TraceKinds.Attack) {
|
|
16
|
+
damageDealt.set(ev.attackerId, (damageDealt.get(ev.attackerId) ?? 0) + ev.energy_J);
|
|
17
|
+
hitsLanded.set(ev.attackerId, (hitsLanded.get(ev.attackerId) ?? 0) + 1);
|
|
18
|
+
hitsTaken.set(ev.targetId, (hitsTaken.get(ev.targetId) ?? 0) + 1);
|
|
19
|
+
}
|
|
20
|
+
else if (ev.kind === TraceKinds.ProjectileHit && ev.hit) {
|
|
21
|
+
damageDealt.set(ev.shooterId, (damageDealt.get(ev.shooterId) ?? 0) + ev.energyAtImpact_J);
|
|
22
|
+
hitsLanded.set(ev.shooterId, (hitsLanded.get(ev.shooterId) ?? 0) + 1);
|
|
23
|
+
if (ev.targetId !== undefined) {
|
|
24
|
+
hitsTaken.set(ev.targetId, (hitsTaken.get(ev.targetId) ?? 0) + 1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (ev.kind === TraceKinds.KO) {
|
|
28
|
+
if (!tickOfKO.has(ev.entityId))
|
|
29
|
+
tickOfKO.set(ev.entityId, ev.tick);
|
|
30
|
+
}
|
|
31
|
+
else if (ev.kind === TraceKinds.Death) {
|
|
32
|
+
if (!tickOfDeath.has(ev.entityId))
|
|
33
|
+
tickOfDeath.set(ev.entityId, ev.tick);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// tickToIncapacitation = min(tickOfKO, tickOfDeath) per entity
|
|
37
|
+
const tickToIncapacitation = new Map();
|
|
38
|
+
for (const [id, t] of tickOfKO)
|
|
39
|
+
tickToIncapacitation.set(id, t);
|
|
40
|
+
for (const [id, t] of tickOfDeath) {
|
|
41
|
+
const prev = tickToIncapacitation.get(id);
|
|
42
|
+
tickToIncapacitation.set(id, prev === undefined ? t : Math.min(prev, t));
|
|
43
|
+
}
|
|
44
|
+
return { damageDealt, hitsLanded, hitsTaken, tickOfKO, tickOfDeath, tickToIncapacitation };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fraction of `entityIds` that were never incapacitated (KO or death) in `events`.
|
|
48
|
+
* Returns 1.0 if `entityIds` is empty.
|
|
49
|
+
*/
|
|
50
|
+
export function survivalRate(events, entityIds) {
|
|
51
|
+
if (entityIds.length === 0)
|
|
52
|
+
return 1.0;
|
|
53
|
+
const { tickToIncapacitation } = collectMetrics(events);
|
|
54
|
+
const incapCount = entityIds.filter(id => tickToIncapacitation.has(id)).length;
|
|
55
|
+
return (entityIds.length - incapCount) / entityIds.length;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Mean tick-to-incapacitation across the given entities.
|
|
59
|
+
* Entities that were never incapacitated contribute `totalTicks` to the average
|
|
60
|
+
* (i.e. they survived the full duration).
|
|
61
|
+
*
|
|
62
|
+
* Returns `totalTicks` if no entity was incapacitated.
|
|
63
|
+
*/
|
|
64
|
+
export function meanTimeToIncapacitation(events, entityIds, totalTicks) {
|
|
65
|
+
if (entityIds.length === 0)
|
|
66
|
+
return totalTicks;
|
|
67
|
+
const { tickToIncapacitation } = collectMetrics(events);
|
|
68
|
+
const total = entityIds.reduce((sum, id) => {
|
|
69
|
+
return sum + (tickToIncapacitation.get(id) ?? totalTicks);
|
|
70
|
+
}, 0);
|
|
71
|
+
return total / entityIds.length;
|
|
72
|
+
}
|
|
73
|
+
// ── CollectingTrace ───────────────────────────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* A TraceSink that accumulates all events into an array for later analysis.
|
|
76
|
+
*
|
|
77
|
+
* Usage:
|
|
78
|
+
* const tracer = new CollectingTrace();
|
|
79
|
+
* stepWorld(world, cmds, { ...ctx, trace: tracer });
|
|
80
|
+
* const metrics = collectMetrics(tracer.events);
|
|
81
|
+
*/
|
|
82
|
+
export class CollectingTrace {
|
|
83
|
+
events = [];
|
|
84
|
+
onEvent(ev) {
|
|
85
|
+
this.events.push(ev);
|
|
86
|
+
}
|
|
87
|
+
/** Remove all accumulated events. */
|
|
88
|
+
clear() {
|
|
89
|
+
this.events.length = 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { WorldState } from "./sim/world.js";
|
|
2
|
+
import type { Entity } from "./sim/entity.js";
|
|
3
|
+
import type { GrapplePosition } from "./sim/entity.js";
|
|
4
|
+
import type { Q } from "./units.js";
|
|
5
|
+
/** Per-segment mass and its fraction of total body mass. */
|
|
6
|
+
export interface SegmentMass {
|
|
7
|
+
segmentId: string;
|
|
8
|
+
/** Fixed-point kg (SCALE.kg = 1000; 5 kg = 5000). */
|
|
9
|
+
mass_kg: number;
|
|
10
|
+
/** Fraction of total body mass (Q; q(1.0) = 100% of body mass). */
|
|
11
|
+
fractionQ: Q;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Mass distribution and estimated centre of gravity for an entity.
|
|
15
|
+
* CoG is in real metres above the entity's foot position (world y = 0).
|
|
16
|
+
*/
|
|
17
|
+
export interface MassDistribution {
|
|
18
|
+
/** Total body mass in fixed-point kg (SCALE.kg = 1000). */
|
|
19
|
+
totalMass_kg: number;
|
|
20
|
+
segments: SegmentMass[];
|
|
21
|
+
/**
|
|
22
|
+
* Estimated centre of gravity in real metres.
|
|
23
|
+
* y = height above feet; x = lateral offset (negative = anatomical left).
|
|
24
|
+
*/
|
|
25
|
+
cogOffset_m: {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Derive mass distribution and centre of gravity from entity body plan.
|
|
32
|
+
* Falls back to a single "body" segment at the geometric midpoint when no
|
|
33
|
+
* body plan is present.
|
|
34
|
+
*/
|
|
35
|
+
export declare function deriveMassDistribution(entity: Entity): MassDistribution;
|
|
36
|
+
/**
|
|
37
|
+
* Simplified diagonal inertia tensor about the entity's principal axes
|
|
38
|
+
* (kg·m²). z offsets are assumed zero (planar estimation).
|
|
39
|
+
*/
|
|
40
|
+
export interface InertiaTensor {
|
|
41
|
+
/** About vertical (yaw) axis — governs turn rate. */
|
|
42
|
+
yaw_kgm2: number;
|
|
43
|
+
/** About lateral (pitch) axis — governs forward lean. */
|
|
44
|
+
pitch_kgm2: number;
|
|
45
|
+
/** About fore-aft (roll) axis — governs side lean. */
|
|
46
|
+
roll_kgm2: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Derive a simplified diagonal inertia tensor from entity body plan.
|
|
50
|
+
* Falls back to a solid-sphere approximation when no body plan is present.
|
|
51
|
+
*/
|
|
52
|
+
export declare function deriveInertiaTensor(entity: Entity): InertiaTensor;
|
|
53
|
+
/**
|
|
54
|
+
* Animation blend weights and state flags derived from entity physical state.
|
|
55
|
+
* Locomotion weights (idle/walk/run/sprint/crawl) are mutually exclusive;
|
|
56
|
+
* exactly one is SCALE.Q when the entity is mobile.
|
|
57
|
+
* All Q values in [0, SCALE.Q].
|
|
58
|
+
*/
|
|
59
|
+
export interface AnimationHints {
|
|
60
|
+
/** Locomotion blend — mutually exclusive. */
|
|
61
|
+
idle: Q;
|
|
62
|
+
walk: Q;
|
|
63
|
+
run: Q;
|
|
64
|
+
sprint: Q;
|
|
65
|
+
crawl: Q;
|
|
66
|
+
/** Active defence blend weight (derived from intent.defence.intensity). */
|
|
67
|
+
guardingQ: Q;
|
|
68
|
+
/**
|
|
69
|
+
* Attack blend weight — nonzero while attack cooldown is active, indicating
|
|
70
|
+
* the entity is mid-swing or recovering from a strike.
|
|
71
|
+
*/
|
|
72
|
+
attackingQ: Q;
|
|
73
|
+
/** Physiological condition overlays (direct pass-through from entity state). */
|
|
74
|
+
shockQ: Q;
|
|
75
|
+
fearQ: Q;
|
|
76
|
+
/** Positional state flags. */
|
|
77
|
+
prone: boolean;
|
|
78
|
+
unconscious: boolean;
|
|
79
|
+
dead: boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Derive animation hints from entity intent, condition, and injury state.
|
|
83
|
+
*/
|
|
84
|
+
export declare function deriveAnimationHints(entity: Entity): AnimationHints;
|
|
85
|
+
/**
|
|
86
|
+
* Per-region injury state as a deformation blend weight for the host rig.
|
|
87
|
+
* A host renderer maps each segmentId to a skeleton bone and drives blend
|
|
88
|
+
* shape or constraint weights from impairmentQ.
|
|
89
|
+
*/
|
|
90
|
+
export interface PoseModifier {
|
|
91
|
+
segmentId: string;
|
|
92
|
+
/** Overall deformation blend: max(structuralQ, surfaceQ). */
|
|
93
|
+
impairmentQ: Q;
|
|
94
|
+
structuralQ: Q;
|
|
95
|
+
surfaceQ: Q;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Derive per-region pose modifiers from entity injury state.
|
|
99
|
+
* Returns one entry per injury region (byRegion keys).
|
|
100
|
+
*/
|
|
101
|
+
export declare function derivePoseModifiers(entity: Entity): PoseModifier[];
|
|
102
|
+
/**
|
|
103
|
+
* Grapple relationship for pose-constraint solving.
|
|
104
|
+
* A host renderer uses this to lock relative pose between grappling entities.
|
|
105
|
+
*/
|
|
106
|
+
export interface GrapplePoseConstraint {
|
|
107
|
+
isHolder: boolean;
|
|
108
|
+
holdingEntityId?: number;
|
|
109
|
+
isHeld: boolean;
|
|
110
|
+
heldByIds: number[];
|
|
111
|
+
position: GrapplePosition;
|
|
112
|
+
gripQ: Q;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Derive grapple pose constraint from entity grapple state.
|
|
116
|
+
*/
|
|
117
|
+
export declare function deriveGrappleConstraint(entity: Entity): GrapplePoseConstraint;
|
|
118
|
+
/**
|
|
119
|
+
* Complete per-entity rig data for a single simulation tick.
|
|
120
|
+
* Aggregates all Phase 14 outputs for convenient host consumption.
|
|
121
|
+
* Call extractRigSnapshots once per tick after stepWorld.
|
|
122
|
+
*/
|
|
123
|
+
export interface RigSnapshot {
|
|
124
|
+
entityId: number;
|
|
125
|
+
teamId: number;
|
|
126
|
+
tick: number;
|
|
127
|
+
mass: MassDistribution;
|
|
128
|
+
inertia: InertiaTensor;
|
|
129
|
+
animation: AnimationHints;
|
|
130
|
+
pose: PoseModifier[];
|
|
131
|
+
grapple: GrapplePoseConstraint;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Extract a full rig snapshot for every entity in the world.
|
|
135
|
+
* Combine with extractMotionVectors and extractConditionSamples from
|
|
136
|
+
* src/debug.ts for a complete per-tick visualisation feed.
|
|
137
|
+
*/
|
|
138
|
+
export declare function extractRigSnapshots(world: WorldState): RigSnapshot[];
|