@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,194 @@
|
|
|
1
|
+
// src/narrative-render.ts — Phase 45: Emergent Story Generation — Narrative Rendering
|
|
2
|
+
//
|
|
3
|
+
// Template-based prose generation for chronicle entries and story arcs.
|
|
4
|
+
/** Static templates for each event type. */
|
|
5
|
+
const EVENT_TEMPLATES = {
|
|
6
|
+
entity_death: (v) => `${v.actorName} died${v.cause ? ` from ${v.cause}` : ""}${v.location ? ` in ${v.location}` : ""}.`,
|
|
7
|
+
entity_birth: (v) => `${v.entityName} was born${v.parents ? ` to ${v.parents}` : ""}${v.settlement ? ` in ${v.settlement}` : ""}.`,
|
|
8
|
+
relationship_formed: (v) => `${v.actorA} and ${v.actorB} formed a ${v.bondType || "relationship"}${v.context ? ` after ${v.context}` : ""}.`,
|
|
9
|
+
relationship_broken: (v) => `${v.actorA} and ${v.actorB}'s ${v.bondType || "relationship"} ended${v.reason ? ` due to ${v.reason}` : ""}.`,
|
|
10
|
+
relationship_betrayal: (v) => `${v.betrayer} betrayed ${v.victim}${v.context ? ` during ${v.context}` : ""}, destroying their trust forever.`,
|
|
11
|
+
quest_completed: (v) => `${v.actorName} completed the quest "${v.questName}"${v.reward ? ` and received ${v.reward}` : ""}.`,
|
|
12
|
+
quest_failed: (v) => `${v.actorName} failed the quest "${v.questName}"${v.reason ? `: ${v.reason}` : ""}.`,
|
|
13
|
+
quest_accepted: (v) => `${v.actorName} accepted the quest "${v.questName}"${v.giver ? ` from ${v.giver}` : ""}.`,
|
|
14
|
+
settlement_founded: (v) => `The settlement of ${v.settlementName} was founded${v.founder ? ` by ${v.founder}` : ""}.`,
|
|
15
|
+
settlement_upgraded: (v) => `${v.settlementName} grew from ${v.oldTier} to ${v.newTier}.`,
|
|
16
|
+
settlement_raided: (v) => `${v.settlementName} was raided by ${v.raiders}${v.damage ? `, suffering ${v.damage}` : ""}.`,
|
|
17
|
+
settlement_destroyed: (v) => `${v.settlementName} fell to ${v.destroyer}, ending its ${v.age} history.`,
|
|
18
|
+
facility_completed: (v) => `A new ${v.facilityType} was completed in ${v.settlementName}.`,
|
|
19
|
+
masterwork_crafted: (v) => `${v.crafterName} forged ${v.itemName}, a masterwork of exceptional quality.`,
|
|
20
|
+
first_contact: (v) => `${v.factionA} made first contact with ${v.factionB}${v.location ? ` at ${v.location}` : ""}.`,
|
|
21
|
+
combat_victory: (v) => `${v.victor} defeated ${v.defeated}${v.method ? ` by ${v.method}` : ""}.`,
|
|
22
|
+
combat_defeat: (v) => `${v.defeated} was overcome by ${v.victor}${v.location ? ` in ${v.location}` : ""}.`,
|
|
23
|
+
rank_promotion: (v) => `${v.actorName} rose to the rank of ${v.newRank}${v.faction ? ` in ${v.faction}` : ""}.`,
|
|
24
|
+
legendary_deed: (v) => `${v.hero} performed a legendary deed: ${v.deedDescription}`,
|
|
25
|
+
tragic_event: (v) => `Tragedy struck when ${v.description}`,
|
|
26
|
+
};
|
|
27
|
+
// ── Arc Summary Templates ─────────────────────────────────────────────────────
|
|
28
|
+
/** Generate a summary for a story arc. */
|
|
29
|
+
export function renderArcSummary(arc) {
|
|
30
|
+
const typeSummaries = {
|
|
31
|
+
rise_of_hero: (a) => `The hero's journey of ${actorNames(a)} spans ${entryCount(a)} pivotal moments.`,
|
|
32
|
+
tragic_fall: (a) => `${actorNames(a)}'s descent from grace, marked by betrayal and loss.`,
|
|
33
|
+
rivalry: (a) => `An enduring rivalry between ${actorNames(a)} unfolding across ${entryCount(a)} confrontations.`,
|
|
34
|
+
great_migration: (a) => `A mass migration that reshaped the region.`,
|
|
35
|
+
settlement_growth: (a) => `The rise of a settlement from humble beginnings to prosperity.`,
|
|
36
|
+
fallen_settlement: (a) => `The tragic fall of a once-great settlement.`,
|
|
37
|
+
legendary_craftsman: (a) => `${actorNames(a)}'s masterworks will be remembered for generations.`,
|
|
38
|
+
notorious_villain: (a) => `The terrifying rise of ${actorNames(a)}.`,
|
|
39
|
+
unlikely_friendship: (a) => `An unexpected bond formed between ${actorNames(a)} against all odds.`,
|
|
40
|
+
betrayal_and_redemption: (a) => `A tale of treachery, guilt, and ultimately, redemption for ${actorNames(a)}.`,
|
|
41
|
+
};
|
|
42
|
+
const renderer = typeSummaries[arc.arcType];
|
|
43
|
+
return renderer ? renderer(arc) : arc.description;
|
|
44
|
+
}
|
|
45
|
+
function actorNames(arc) {
|
|
46
|
+
return arc.primaryActors.join(" and ");
|
|
47
|
+
}
|
|
48
|
+
function entryCount(arc) {
|
|
49
|
+
return arc.entryIds.length;
|
|
50
|
+
}
|
|
51
|
+
// ── Entry Rendering ───────────────────────────────────────────────────────────
|
|
52
|
+
/** Render a chronicle entry to prose. */
|
|
53
|
+
export function renderEntry(entry) {
|
|
54
|
+
// Return cached render if available
|
|
55
|
+
if (entry.rendered)
|
|
56
|
+
return entry.rendered;
|
|
57
|
+
const templateFn = EVENT_TEMPLATES[entry.eventType];
|
|
58
|
+
if (!templateFn) {
|
|
59
|
+
return `Event: ${entry.eventType} (tick ${entry.tick})`;
|
|
60
|
+
}
|
|
61
|
+
return templateFn(entry.variables);
|
|
62
|
+
}
|
|
63
|
+
/** Render an entry with full context. */
|
|
64
|
+
export function renderEntryVerbose(entry) {
|
|
65
|
+
const base = renderEntry(entry);
|
|
66
|
+
const significance = ` [Significance: ${entry.significance}/100]`;
|
|
67
|
+
const actors = entry.actors.length > 0 ? ` (Actors: ${entry.actors.join(", ")})` : "";
|
|
68
|
+
const settlement = entry.settlementId ? ` @${entry.settlementId}` : "";
|
|
69
|
+
return `${base}${significance}${actors}${settlement}`;
|
|
70
|
+
}
|
|
71
|
+
/** Render multiple entries to a narrative. */
|
|
72
|
+
export function renderChronicle(entries, options = {}) {
|
|
73
|
+
const { showSignificance = false, showActors = false, showSettlements = false, minSignificance = 0, format = "prose", } = options;
|
|
74
|
+
const filtered = entries.filter(e => e.significance >= minSignificance);
|
|
75
|
+
if (filtered.length === 0) {
|
|
76
|
+
return "No events recorded.";
|
|
77
|
+
}
|
|
78
|
+
switch (format) {
|
|
79
|
+
case "compact":
|
|
80
|
+
return filtered.map(e => `[Tick ${e.tick}] ${renderEntry(e)}`).join("\n");
|
|
81
|
+
case "chronological":
|
|
82
|
+
return filtered
|
|
83
|
+
.map(e => {
|
|
84
|
+
let line = `Turn ${e.tick}: ${renderEntry(e)}`;
|
|
85
|
+
if (showSignificance)
|
|
86
|
+
line += ` [${e.significance}]`;
|
|
87
|
+
if (showActors && e.actors.length)
|
|
88
|
+
line += ` {${e.actors.join(",")}}`;
|
|
89
|
+
if (showSettlements && e.settlementId)
|
|
90
|
+
line += ` @${e.settlementId}`;
|
|
91
|
+
return line;
|
|
92
|
+
})
|
|
93
|
+
.join("\n");
|
|
94
|
+
case "prose":
|
|
95
|
+
default: {
|
|
96
|
+
const paragraphs = [];
|
|
97
|
+
let currentParagraph = "";
|
|
98
|
+
for (const entry of filtered) {
|
|
99
|
+
const sentence = renderEntry(entry);
|
|
100
|
+
// Start new paragraph on significant events
|
|
101
|
+
if (entry.significance >= 70 && currentParagraph) {
|
|
102
|
+
paragraphs.push(currentParagraph.trim());
|
|
103
|
+
currentParagraph = sentence + " ";
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
currentParagraph += sentence + " ";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (currentParagraph) {
|
|
110
|
+
paragraphs.push(currentParagraph.trim());
|
|
111
|
+
}
|
|
112
|
+
return paragraphs.join("\n\n");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ── Arc Rendering ─────────────────────────────────────────────────────────────
|
|
117
|
+
/** Render a complete story arc with its entries. */
|
|
118
|
+
export function renderArcNarrative(arc, entryMap) {
|
|
119
|
+
const lines = [];
|
|
120
|
+
// Header
|
|
121
|
+
lines.push(`═══ ${arc.arcType.toUpperCase().replace(/_/g, " ")} ═══`);
|
|
122
|
+
lines.push(`Significance: ${arc.significance}/100 | Duration: Tick ${arc.startTick} to ${arc.endTick ?? "ongoing"}`);
|
|
123
|
+
lines.push("");
|
|
124
|
+
// Arc summary
|
|
125
|
+
lines.push(renderArcSummary(arc));
|
|
126
|
+
lines.push("");
|
|
127
|
+
// Entries in arc
|
|
128
|
+
lines.push("Key Events:");
|
|
129
|
+
for (const entryId of arc.entryIds) {
|
|
130
|
+
const entry = entryMap.get(entryId);
|
|
131
|
+
if (entry) {
|
|
132
|
+
lines.push(` [${entry.tick}] ${renderEntry(entry)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
/** Render all arcs in a chronicle. */
|
|
138
|
+
export function renderAllArcs(arcs, entryMap) {
|
|
139
|
+
if (arcs.length === 0) {
|
|
140
|
+
return "No story arcs detected.";
|
|
141
|
+
}
|
|
142
|
+
return arcs
|
|
143
|
+
.sort((a, b) => b.significance - a.significance)
|
|
144
|
+
.map(arc => renderArcNarrative(arc, entryMap))
|
|
145
|
+
.join("\n\n");
|
|
146
|
+
}
|
|
147
|
+
// ── Custom Templates ──────────────────────────────────────────────────────────
|
|
148
|
+
/** Register a custom template for an event type. */
|
|
149
|
+
export function registerTemplate(eventType, templateFn) {
|
|
150
|
+
EVENT_TEMPLATES[eventType] = templateFn;
|
|
151
|
+
}
|
|
152
|
+
/** Register multiple templates at once. */
|
|
153
|
+
export function registerTemplates(templates) {
|
|
154
|
+
for (const [eventType, fn] of Object.entries(templates)) {
|
|
155
|
+
if (fn) {
|
|
156
|
+
registerTemplate(eventType, fn);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Generate a complete narrative from a set of arcs and entries. */
|
|
161
|
+
export function generateNarrative(arcs, entries) {
|
|
162
|
+
const sortedArcs = arcs.sort((a, b) => b.significance - a.significance);
|
|
163
|
+
const topArc = sortedArcs[0];
|
|
164
|
+
// Collect key figures from all arcs
|
|
165
|
+
const keyFigures = [...new Set(arcs.flatMap(a => a.primaryActors))];
|
|
166
|
+
// Title based on top arc
|
|
167
|
+
const title = topArc
|
|
168
|
+
? `The ${topArc.arcType.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase())}`
|
|
169
|
+
: "Chronicle of Events";
|
|
170
|
+
// Summary paragraph
|
|
171
|
+
const summaryParts = [];
|
|
172
|
+
if (topArc) {
|
|
173
|
+
summaryParts.push(renderArcSummary(topArc));
|
|
174
|
+
}
|
|
175
|
+
if (sortedArcs.length > 1) {
|
|
176
|
+
summaryParts.push(`${sortedArcs.length - 1} other story arcs intertwine with this narrative.`);
|
|
177
|
+
}
|
|
178
|
+
const summary = summaryParts.join(" ") || "A series of unconnected events.";
|
|
179
|
+
// Build full text
|
|
180
|
+
const entryMap = new Map(entries.map(e => [e.entryId, e]));
|
|
181
|
+
const fullText = renderAllArcs(sortedArcs, entryMap);
|
|
182
|
+
// Drama estimate based on arc significance and variety
|
|
183
|
+
const significanceSum = arcs.reduce((sum, a) => sum + a.significance, 0);
|
|
184
|
+
const varietyBonus = Math.min(20, arcs.length * 5);
|
|
185
|
+
const estimatedDrama = Math.min(100, significanceSum / Math.max(1, arcs.length) + varietyBonus);
|
|
186
|
+
return {
|
|
187
|
+
title,
|
|
188
|
+
summary,
|
|
189
|
+
fullText,
|
|
190
|
+
keyFigures,
|
|
191
|
+
keyEvents: topArc?.entryIds ?? [],
|
|
192
|
+
estimatedDrama,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { WorldState } from "./sim/world.js";
|
|
2
|
+
import type { CommandMap } from "./sim/commands.js";
|
|
3
|
+
import type { KernelContext } from "./sim/context.js";
|
|
4
|
+
import { type Q } from "./units.js";
|
|
5
|
+
/**
|
|
6
|
+
* A function that returns the commands to issue on each tick.
|
|
7
|
+
* Receives the current WorldState so AI or scripted logic can react to it.
|
|
8
|
+
*/
|
|
9
|
+
export type CommandProvider = (world: WorldState) => CommandMap;
|
|
10
|
+
/**
|
|
11
|
+
* A single expected story outcome that must become true within a tick window.
|
|
12
|
+
* A beat "passes" in a given run the first time its predicate returns true
|
|
13
|
+
* at any tick in [tickWindow[0], tickWindow[1]] (inclusive, post-step).
|
|
14
|
+
*/
|
|
15
|
+
export interface NarrativeBeat {
|
|
16
|
+
/** [firstTick, lastTick] inclusive range checked after each stepWorld call. */
|
|
17
|
+
tickWindow: [number, number];
|
|
18
|
+
/** Returns true when this beat's condition is satisfied. */
|
|
19
|
+
predicate: (world: WorldState) => boolean;
|
|
20
|
+
/** Human-readable label shown in reports. */
|
|
21
|
+
description: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A complete narrative scenario: world factory, command provider, and beats.
|
|
25
|
+
*
|
|
26
|
+
* Each trial overrides `world.seed` so the same setup runs deterministically
|
|
27
|
+
* across a range of seeds — no two trials share state.
|
|
28
|
+
*/
|
|
29
|
+
export interface NarrativeScenario {
|
|
30
|
+
name: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Returns a fresh WorldState. `world.seed` is overridden per trial. */
|
|
33
|
+
setup: () => WorldState;
|
|
34
|
+
/** Supplies commands each tick (AI, scripted, or mixed). */
|
|
35
|
+
commands: CommandProvider;
|
|
36
|
+
/** All beats must pass for a run to count as a success. */
|
|
37
|
+
beats: NarrativeBeat[];
|
|
38
|
+
/** Maximum ticks per trial. Default: 600 (30 s at 20 Hz). */
|
|
39
|
+
maxTicks?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Per-beat aggregate across all trials. */
|
|
42
|
+
export interface BeatResult {
|
|
43
|
+
description: string;
|
|
44
|
+
/** Fraction of runs where this beat was satisfied within its window. */
|
|
45
|
+
passRate: number;
|
|
46
|
+
/**
|
|
47
|
+
* Per-beat narrative push: `1 − passRate`.
|
|
48
|
+
* Identifies the bottleneck beat — the one that most resists the story.
|
|
49
|
+
*/
|
|
50
|
+
beatPush: number;
|
|
51
|
+
}
|
|
52
|
+
/** Aggregate result of a full stress test run. */
|
|
53
|
+
export interface StressTestResult {
|
|
54
|
+
scenarioName: string;
|
|
55
|
+
runsTotal: number;
|
|
56
|
+
/**
|
|
57
|
+
* Fraction of runs where ALL beats were satisfied.
|
|
58
|
+
* 1.0 = story happens every time; 0.0 = story never happens.
|
|
59
|
+
*/
|
|
60
|
+
successRate: number;
|
|
61
|
+
/**
|
|
62
|
+
* Narrative push required to make the story happen: `1 − successRate`.
|
|
63
|
+
* 0.00 = no push needed (plausible);
|
|
64
|
+
* 1.00 = miracle required (extreme plot armour).
|
|
65
|
+
*/
|
|
66
|
+
narrativePush: number;
|
|
67
|
+
/**
|
|
68
|
+
* Deus Ex score: `narrativePush × 10`, rounded to one decimal place.
|
|
69
|
+
* A 0–10 scale for quick human communication:
|
|
70
|
+
* 0.0–1.0 = plausible (no authorial help needed)
|
|
71
|
+
* 1.0–4.0 = light touch
|
|
72
|
+
* 4.0–7.0 = moderate intervention
|
|
73
|
+
* 7.0–9.0 = heavy plot armour
|
|
74
|
+
* 9.0–10.0 = miracle required
|
|
75
|
+
*/
|
|
76
|
+
deusExScore: number;
|
|
77
|
+
/** Per-beat breakdown — identify which beat is the bottleneck. */
|
|
78
|
+
beatResults: BeatResult[];
|
|
79
|
+
/**
|
|
80
|
+
* Seeds that produced successful runs.
|
|
81
|
+
* Use these to replay and visually inspect a "canonical" version of the scene.
|
|
82
|
+
*/
|
|
83
|
+
successSeeds: number[];
|
|
84
|
+
}
|
|
85
|
+
/** Consciousness threshold below which an entity is considered defeated. */
|
|
86
|
+
export declare const DEFEATED_CONSCIOUSNESS: Q;
|
|
87
|
+
/**
|
|
88
|
+
* Beat passes when entity `entityId` is dead or unconscious
|
|
89
|
+
* (consciousness ≤ DEFEATED_CONSCIOUSNESS).
|
|
90
|
+
*/
|
|
91
|
+
export declare function beatEntityDefeated(entityId: number): (world: WorldState) => boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Beat passes when entity `entityId` is alive and conscious
|
|
94
|
+
* (not dead, consciousness > DEFEATED_CONSCIOUSNESS).
|
|
95
|
+
*/
|
|
96
|
+
export declare function beatEntitySurvives(entityId: number): (world: WorldState) => boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Beat passes when ALL entities on `teamId` are defeated.
|
|
99
|
+
*/
|
|
100
|
+
export declare function beatTeamDefeated(teamId: number): (world: WorldState) => boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Beat passes when entity `entityId` shock exceeds `thresholdQ`.
|
|
103
|
+
* Useful for "hero takes a serious hit but survives" beats.
|
|
104
|
+
*/
|
|
105
|
+
export declare function beatEntityShockExceeds(entityId: number, thresholdQ: Q): (world: WorldState) => boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Beat passes when entity `entityId` fatigue exceeds `thresholdQ`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function beatEntityFatigued(entityId: number, thresholdQ: Q): (world: WorldState) => boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Run `scenario` once per seed in `seeds`, checking each beat's predicate
|
|
112
|
+
* after every tick within its declared window.
|
|
113
|
+
*
|
|
114
|
+
* A run "succeeds" only when every beat passes at least once within its window.
|
|
115
|
+
* `successRate` is the fraction of successful runs; `narrativePush = 1 − successRate`.
|
|
116
|
+
*/
|
|
117
|
+
export declare function runNarrativeStressTest(scenario: NarrativeScenario, seeds: readonly number[], options?: {
|
|
118
|
+
ctx?: KernelContext;
|
|
119
|
+
}): StressTestResult;
|
|
120
|
+
/**
|
|
121
|
+
* Format a stress test result as a human-readable text report.
|
|
122
|
+
*/
|
|
123
|
+
export declare function formatStressTestReport(result: StressTestResult): string;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// src/narrative-stress.ts — Phase 63: Narrative Stress Test
|
|
2
|
+
//
|
|
3
|
+
// Runs a scenario across many deterministic seeds and measures how probable
|
|
4
|
+
// a sequence of expected story beats is. The inverse of that probability is
|
|
5
|
+
// the "narrative push" — the authorial effort required to make the story happen.
|
|
6
|
+
//
|
|
7
|
+
// Uses only existing infrastructure: stepWorld, makeRng/eventSeed, no new
|
|
8
|
+
// simulation primitives required.
|
|
9
|
+
import { stepWorld } from "./sim/kernel.js";
|
|
10
|
+
import { q } from "./units.js";
|
|
11
|
+
import { TUNING } from "./sim/tuning.js";
|
|
12
|
+
import { TICK_HZ } from "./sim/tick.js";
|
|
13
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
14
|
+
const DEFAULT_MAX_TICKS = 30 * TICK_HZ; // 30 s × 20 Hz = 600 ticks
|
|
15
|
+
/** Consciousness threshold below which an entity is considered defeated. */
|
|
16
|
+
export const DEFEATED_CONSCIOUSNESS = q(0.10);
|
|
17
|
+
// ─── Beat predicate helpers ───────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Beat passes when entity `entityId` is dead or unconscious
|
|
20
|
+
* (consciousness ≤ DEFEATED_CONSCIOUSNESS).
|
|
21
|
+
*/
|
|
22
|
+
export function beatEntityDefeated(entityId) {
|
|
23
|
+
return (world) => {
|
|
24
|
+
const entity = world.entities.find(e => e.id === entityId);
|
|
25
|
+
if (!entity)
|
|
26
|
+
return false;
|
|
27
|
+
return entity.injury.dead || entity.injury.consciousness <= DEFEATED_CONSCIOUSNESS;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Beat passes when entity `entityId` is alive and conscious
|
|
32
|
+
* (not dead, consciousness > DEFEATED_CONSCIOUSNESS).
|
|
33
|
+
*/
|
|
34
|
+
export function beatEntitySurvives(entityId) {
|
|
35
|
+
return (world) => {
|
|
36
|
+
const entity = world.entities.find(e => e.id === entityId);
|
|
37
|
+
if (!entity)
|
|
38
|
+
return false;
|
|
39
|
+
return !entity.injury.dead && entity.injury.consciousness > DEFEATED_CONSCIOUSNESS;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Beat passes when ALL entities on `teamId` are defeated.
|
|
44
|
+
*/
|
|
45
|
+
export function beatTeamDefeated(teamId) {
|
|
46
|
+
return (world) => {
|
|
47
|
+
const team = world.entities.filter(e => e.teamId === teamId);
|
|
48
|
+
if (team.length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
return team.every(e => e.injury.dead || e.injury.consciousness <= DEFEATED_CONSCIOUSNESS);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Beat passes when entity `entityId` shock exceeds `thresholdQ`.
|
|
55
|
+
* Useful for "hero takes a serious hit but survives" beats.
|
|
56
|
+
*/
|
|
57
|
+
export function beatEntityShockExceeds(entityId, thresholdQ) {
|
|
58
|
+
return (world) => {
|
|
59
|
+
const entity = world.entities.find(e => e.id === entityId);
|
|
60
|
+
if (!entity)
|
|
61
|
+
return false;
|
|
62
|
+
return entity.injury.shock > thresholdQ;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Beat passes when entity `entityId` fatigue exceeds `thresholdQ`.
|
|
67
|
+
*/
|
|
68
|
+
export function beatEntityFatigued(entityId, thresholdQ) {
|
|
69
|
+
return (world) => {
|
|
70
|
+
const entity = world.entities.find(e => e.id === entityId);
|
|
71
|
+
if (!entity)
|
|
72
|
+
return false;
|
|
73
|
+
return entity.energy.fatigue > thresholdQ;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// ─── Core runner ──────────────────────────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* Run `scenario` once per seed in `seeds`, checking each beat's predicate
|
|
79
|
+
* after every tick within its declared window.
|
|
80
|
+
*
|
|
81
|
+
* A run "succeeds" only when every beat passes at least once within its window.
|
|
82
|
+
* `successRate` is the fraction of successful runs; `narrativePush = 1 − successRate`.
|
|
83
|
+
*/
|
|
84
|
+
export function runNarrativeStressTest(scenario, seeds, options) {
|
|
85
|
+
const maxTicks = scenario.maxTicks ?? DEFAULT_MAX_TICKS;
|
|
86
|
+
const ctx = {
|
|
87
|
+
tractionCoeff: q(1.0),
|
|
88
|
+
tuning: TUNING.tactical,
|
|
89
|
+
...options?.ctx,
|
|
90
|
+
};
|
|
91
|
+
const nBeats = scenario.beats.length;
|
|
92
|
+
const beatPassCounts = new Array(nBeats).fill(0);
|
|
93
|
+
let successCount = 0;
|
|
94
|
+
const successSeeds = [];
|
|
95
|
+
for (const seed of seeds) {
|
|
96
|
+
const world = scenario.setup();
|
|
97
|
+
world.seed = seed;
|
|
98
|
+
const beatPassed = new Array(nBeats).fill(false);
|
|
99
|
+
let allPassed = false;
|
|
100
|
+
for (let t = 0; t < maxTicks; t++) {
|
|
101
|
+
const commands = scenario.commands(world);
|
|
102
|
+
stepWorld(world, commands, ctx);
|
|
103
|
+
const tick = world.tick;
|
|
104
|
+
for (let b = 0; b < nBeats; b++) {
|
|
105
|
+
if (beatPassed[b])
|
|
106
|
+
continue;
|
|
107
|
+
const beat = scenario.beats[b];
|
|
108
|
+
if (tick >= beat.tickWindow[0] && tick <= beat.tickWindow[1]) {
|
|
109
|
+
if (beat.predicate(world)) {
|
|
110
|
+
beatPassed[b] = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Early exit: all beats have passed
|
|
115
|
+
allPassed = beatPassed.every(Boolean);
|
|
116
|
+
if (allPassed)
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
for (let b = 0; b < nBeats; b++) {
|
|
120
|
+
if (beatPassed[b])
|
|
121
|
+
beatPassCounts[b]++;
|
|
122
|
+
}
|
|
123
|
+
if (allPassed) {
|
|
124
|
+
successCount++;
|
|
125
|
+
successSeeds.push(seed);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const runsTotal = seeds.length;
|
|
129
|
+
const successRate = runsTotal > 0 ? successCount / runsTotal : 0;
|
|
130
|
+
const narrativePush = runsTotal > 0
|
|
131
|
+
? Math.round((1 - successRate) * 10000) / 10000
|
|
132
|
+
: 0;
|
|
133
|
+
const deusExScore = Math.round(narrativePush * 100) / 10; // 0–10, 1 d.p.
|
|
134
|
+
return {
|
|
135
|
+
scenarioName: scenario.name,
|
|
136
|
+
runsTotal,
|
|
137
|
+
successRate,
|
|
138
|
+
narrativePush,
|
|
139
|
+
deusExScore,
|
|
140
|
+
beatResults: scenario.beats.map((beat, b) => {
|
|
141
|
+
const passRate = runsTotal > 0 ? beatPassCounts[b] / runsTotal : 0;
|
|
142
|
+
return {
|
|
143
|
+
description: beat.description,
|
|
144
|
+
passRate,
|
|
145
|
+
beatPush: Math.round((1 - passRate) * 10000) / 10000,
|
|
146
|
+
};
|
|
147
|
+
}),
|
|
148
|
+
successSeeds,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// ─── Report formatter ─────────────────────────────────────────────────────────
|
|
152
|
+
/**
|
|
153
|
+
* Format a stress test result as a human-readable text report.
|
|
154
|
+
*/
|
|
155
|
+
export function formatStressTestReport(result) {
|
|
156
|
+
const pct = (v) => `${(v * 100).toFixed(1)}%`;
|
|
157
|
+
const pushLabel = result.narrativePush < 0.10 ? "none — plausible" :
|
|
158
|
+
result.narrativePush < 0.40 ? "light" :
|
|
159
|
+
result.narrativePush < 0.70 ? "moderate" :
|
|
160
|
+
result.narrativePush < 0.90 ? "heavy" : "extreme — plot armour";
|
|
161
|
+
const lines = [
|
|
162
|
+
`Narrative Stress Test: ${result.scenarioName}`,
|
|
163
|
+
"─".repeat(52),
|
|
164
|
+
`Runs: ${result.runsTotal}`,
|
|
165
|
+
`Success rate: ${pct(result.successRate)}`,
|
|
166
|
+
`Narrative push: ${result.narrativePush.toFixed(4)} (${pushLabel})`,
|
|
167
|
+
`Deus Ex score: ${result.deusExScore.toFixed(1)} / 10`,
|
|
168
|
+
"",
|
|
169
|
+
"Beat breakdown:",
|
|
170
|
+
];
|
|
171
|
+
for (const b of result.beatResults) {
|
|
172
|
+
const icon = b.passRate >= 0.90 ? "✓" : b.passRate >= 0.50 ? "~" : "✗";
|
|
173
|
+
const pushStr = `[push ${b.beatPush.toFixed(2)}]`;
|
|
174
|
+
lines.push(` ${icon} ${pct(b.passRate).padStart(6)} ${pushStr} ${b.description}`);
|
|
175
|
+
}
|
|
176
|
+
const shown = result.successSeeds.slice(0, 10);
|
|
177
|
+
if (shown.length > 0) {
|
|
178
|
+
const suffix = result.successSeeds.length > 10
|
|
179
|
+
? ` … (${result.successSeeds.length} total)` : "";
|
|
180
|
+
lines.push("", `Success seeds: ${shown.join(", ")}${suffix}`);
|
|
181
|
+
}
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Q } from "./units.js";
|
|
2
|
+
import type { WeaponDamageProfile } from "./equipment.js";
|
|
3
|
+
import type { InjuryState } from "./sim/injury.js";
|
|
4
|
+
import type { TraceEvent } from "./sim/trace.js";
|
|
5
|
+
export interface NarrativeConfig {
|
|
6
|
+
/** How much detail to emit.
|
|
7
|
+
* terse — KO, Death, route/rally, fracture, blast, hits only
|
|
8
|
+
* normal — adds blocked/parried notes, misses, weapon bind, grapple start/break
|
|
9
|
+
* verbose — adds grapple ticks, capability events, treatment events
|
|
10
|
+
*/
|
|
11
|
+
verbosity: "terse" | "normal" | "verbose";
|
|
12
|
+
/** Display names keyed by entity id. Falls back to "combatant {id}".
|
|
13
|
+
* Set an entity's name to "you" for second-person verb conjugation. */
|
|
14
|
+
nameMap?: Map<number, string>;
|
|
15
|
+
/** Damage profiles keyed by weapon id; enables verb selection for melee/ranged events. */
|
|
16
|
+
weaponProfiles?: Map<string, WeaponDamageProfile>;
|
|
17
|
+
}
|
|
18
|
+
/** Minimal entity summary needed for describeCombatOutcome. */
|
|
19
|
+
export interface CombatantSummary {
|
|
20
|
+
id: number;
|
|
21
|
+
teamId: number;
|
|
22
|
+
injury: {
|
|
23
|
+
dead: boolean;
|
|
24
|
+
consciousness: Q;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Narrate a single trace event.
|
|
29
|
+
* Returns null for events that should be omitted at the current verbosity level.
|
|
30
|
+
*/
|
|
31
|
+
export declare function narrateEvent(ev: TraceEvent, cfg: NarrativeConfig): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Convert a sequence of trace events into a list of narrative lines.
|
|
34
|
+
* Events that return null from narrateEvent are omitted.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildCombatLog(events: TraceEvent[], cfg: NarrativeConfig): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Summarise an entity's injury state as a short descriptive phrase.
|
|
39
|
+
*/
|
|
40
|
+
export declare function describeInjuries(injury: InjuryState): string;
|
|
41
|
+
/**
|
|
42
|
+
* Produce a one-line outcome summary for a completed engagement.
|
|
43
|
+
*/
|
|
44
|
+
export declare function describeCombatOutcome(combatants: CombatantSummary[], tickCount?: number): string;
|