@its-not-rocket-science/ananke 0.1.11 → 0.1.13

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 CHANGED
@@ -10,6 +10,44 @@ Versioning follows [Semantic Versioning](https://semver.org/).
10
10
 
11
11
  ### Added
12
12
 
13
+ - **Phase 70 · Stratified Political Simulation ("Vassal Web" Layer)** (`src/polity-vassals.ts`)
14
+ - `VassalNode` — intermediate layer between Entity and Polity with `territory_Q`,
15
+ `military_Q`, `treasury_cu`, and a `VassalLoyalty` block.
16
+ - Seven `LoyaltyType` variants with distinct `stepVassalLoyalty` dynamics:
17
+ `ideological` (slow, conviction-driven), `transactional` (treasury comparison),
18
+ `terrified` (instant collapse if liege appears weak), `honor_bound` (oath + grievance
19
+ spike), `opportunistic` (tracks liege/rival morale ratio), `kin_bound` (stable family
20
+ ties), `ideological_rival` (constant decay, cannot recover).
21
+ - `applyGrievanceEvent` — immutable grievance accumulation (host applies broken-promise,
22
+ tax-hike, kin-death events).
23
+ - `computeVassalContribution` — loyalty-scaled troop and treasury output; zero below
24
+ `CONTRIBUTION_FLOOR_Q` (q(0.20)), full above `CONTRIBUTION_FULL_Q` (q(0.50)).
25
+ - `computeEffectiveMilitary` — sums contributions for command-chain filtering before
26
+ passing force ratio to Phase 69 `resolveTacticalEngagement`.
27
+ - `detectRebellionRisk` — Q score (70% low-loyalty + 30% high-grievance) for AI queries.
28
+ - `resolveSuccessionCrisis` — deterministic heir-support rolls weighted by `military_Q`;
29
+ winners gain +q(0.05) loyalty, losers −q(0.08); `SuccessionResult` with `supportQ`
30
+ and per-vassal `loyaltyDeltas`.
31
+ - 40 tests in `test/polity-vassals.test.ts`; exported via `ananke/campaign` subpath.
32
+
33
+ - **Option B · Tier 2 subpath exports** — eight new named import subpaths for all
34
+ Tier 2 module groupings; deep imports remain supported as a fallback:
35
+ - `ananke/character` → aging, sleep, disease, wound-aging, thermoregulation, nutrition,
36
+ medical, toxicology, progression
37
+ - `ananke/combat` → ranged, grapple, formation-combat, mount, hazard, morale, sensory,
38
+ sensory-extended, weather, terrain, skills, biome
39
+ - `ananke/campaign` → campaign, downtime, collective-activities, settlement,
40
+ settlement-services, inventory, item-durability, world-generation, inheritance,
41
+ economy, polity (campaign layer barrel)
42
+ - `ananke/social` → dialogue, faction, relationships, relationships-effects, party,
43
+ quest, quest-generators
44
+ - `ananke/narrative` → chronicle, story-arcs, narrative-render, legend, mythology,
45
+ narrative, narrative-stress, metrics, arena
46
+ - `ananke/anatomy` → existing `src/anatomy/index.ts` barrel
47
+ - `ananke/crafting` → existing `src/crafting/index.ts` barrel
48
+ - `ananke/competence` → existing `src/competence/index.ts` barrel
49
+ - `STABLE_API.md` updated to document preferred subpath import patterns.
50
+
13
51
  - **CE-16 · Modding Support** (`src/modding.ts`)
14
52
  - Layer 1 — `hashMod(json)`: deterministic FNV-1a fingerprint (8-char hex) for any
15
53
  parsed JSON mod file; canonical key-sorted serialisation ensures order-independence.
package/STABLE_API.md CHANGED
@@ -185,21 +185,124 @@ version bump; renames require a major bump and migration guide.
185
185
  These exports are usable and tested but may change across minor versions.
186
186
  A `CHANGELOG.md` entry will document any breaking change.
187
187
 
188
+ Tier 2 modules are accessible via **named subpath exports** (preferred) or deep imports:
189
+
190
+ ```typescript
191
+ // Preferred — named subpath (stable grouping, no internal path coupling)
192
+ import { stepAging, applyAgingToAttributes } from "@its-not-rocket-science/ananke/character";
193
+ import { resolveRangedAttack, stepGrapple } from "@its-not-rocket-science/ananke/combat";
194
+ import { stepCampaignDay, createSettlement } from "@its-not-rocket-science/ananke/campaign";
195
+ import { dialogueProbability, effectiveStanding } from "@its-not-rocket-science/ananke/social";
196
+ import { addChronicleEntry, detectStoryArcs } from "@its-not-rocket-science/ananke/narrative";
197
+ import { compileAnatomyDefinition } from "@its-not-rocket-science/ananke/anatomy";
198
+ import { craftItem, getAvailableRecipes } from "@its-not-rocket-science/ananke/crafting";
199
+ import { resolveCompetence } from "@its-not-rocket-science/ananke/competence";
200
+
201
+ // Deep import (fallback — internal paths may change)
202
+ import { stepAging } from "@its-not-rocket-science/ananke/dist/src/sim/aging.js";
203
+ ```
204
+
205
+ ### AI command system
206
+
207
+ | Module | Key exports |
208
+ |--------|------------|
209
+ | `src/sim/ai/system.ts` | `buildAICommands(world, ctx)` — build a `CommandMap` for all AI-controlled entities |
210
+
211
+ ### Character lifecycle
212
+
188
213
  | Module | Key exports |
189
214
  |--------|------------|
190
- | `src/mythology.ts` | `compressMythsFromHistory`, `stepMythologyYear`, `aggregateFactionMythEffect`, `scaledMythEffect` |
191
- | `src/narrative-stress.ts` | `runNarrativeStressTest`, `scoreNarrativePush` |
192
- | `src/campaign.ts` | `Campaign`, `stepCampaignDay`, `advanceCampaignClock`, `serializeCampaign`, `deserializeCampaign` |
193
- | `src/arena.ts` | Arena scenario DSL, `runArenaTrial`, `runArenaScenario` |
194
215
  | `src/sim/aging.ts` | `applyAgingToAttributes`, `stepAging`, `deriveAgeMultipliers`, `getAgePhase` |
195
216
  | `src/sim/sleep.ts` | `applySleepToAttributes`, `stepSleep`, `deriveSleepDeprivationMuls`, `circadianAlertness` |
196
217
  | `src/sim/disease.ts` | `exposeToDisease`, `stepDiseaseForEntity`, `spreadDisease`, `computeTransmissionRisk` |
218
+ | `src/sim/wound-aging.ts` | `stepWoundAging`, `recordTraumaEvent`, `deriveFearThresholdMul`, `deriveSepsisRisk` |
219
+ | `src/sim/thermoregulation.ts` | `stepThermoregulation`, `deriveThermalComfort`, `computeMetabolicRate` |
220
+ | `src/sim/nutrition.ts` | `stepNutrition`, `computeHungerEffect`, food catalogue constants |
221
+ | `src/sim/medical.ts` | `resolveTreatment`, `computeTreatmentEffect`, medical tier definitions |
222
+ | `src/sim/toxicology.ts` | `applyVenom`, `stepActiveVenoms`, `resolveIngestedToxin` |
223
+ | `src/progression.ts` | `applyXP`, `applyTrainingDrift`, `computeMilestones` |
224
+
225
+ ### Combat extensions
226
+
227
+ | Module | Key exports |
228
+ |--------|------------|
197
229
  | `src/sim/mount.ts` | `checkMountStep`, `computeChargeBonus`, `deriveRiderHeightBonus` |
198
230
  | `src/sim/hazard.ts` | `deriveHazardEffect`, `computeHazardExposure`, `stepHazardZone` |
231
+ | `src/sim/ranged.ts` | `resolveRangedAttack`, `computeRangedAccuracy`, `computeProjectileEnergy` |
232
+ | `src/sim/grapple.ts` | `resolveGrappleContest`, `computeGrappleStrength`, `stepGrapple` |
233
+ | `src/sim/formation.ts` | `FormationConfig`, `computeFormationBonus`, `resolveFormationStep` |
234
+ | `src/sim/morale.ts` | `computeMoraleEffect`, `stepMorale`, `applyRoutEffect` |
235
+ | `src/sim/sensory.ts` | `computeVisibility`, `computeHearingRange`, `stepSensoryState` |
236
+ | `src/sim/sensory-extended.ts` | `computeEcholocationRange`, `computeOlfactionRange` — non-human senses |
237
+ | `src/sim/weather.ts` | `WeatherState`, `stepWeather`, `computeWeatherEffect` |
238
+ | `src/sim/terrain.ts` | `TerrainType`, `TERRAIN_PROFILES`, `computeTerrainTraction` |
239
+ | `src/sim/skills.ts` | `SkillId`, `SkillLevel`, `SKILL_LEVEL_MULS`, `buildSkillMap`, `getSkillMul` |
240
+ | `src/sim/biome.ts` | `BiomeType`, `BIOME_PROFILES`, `deriveBiomeEnvironment` |
241
+
242
+ ### Social and economic systems
243
+
244
+ | Module | Key exports |
245
+ |--------|------------|
199
246
  | `src/dialogue.ts` | `resolveIntimidation`, `resolvePersuasion`, `resolveDeception`, `resolveTradeNegotiation` |
200
247
  | `src/faction.ts` | `FactionRegistry`, `updateStanding`, `getFactionStanding` |
201
248
  | `src/economy.ts` | `computeItemValue`, `applyWear`, `resolveDrops`, `evaluateTradeOffer` |
202
- | `src/progression.ts` | `applyXP`, `applyTrainingDrift`, `computeMilestones` |
249
+ | `src/relationships.ts` | `createRelationshipGraph`, `establishRelationship`, `recordEvent`, `getRelationshipAffinity` |
250
+ | `src/relationships-effects.ts` | Relationship modifiers applied to dialogue, teaching, morale contexts |
251
+ | `src/party.ts` | `createPartyRegistry`, `createParty`, `addPartyMember`, `setPartyStanding` |
252
+
253
+ ### Campaign and world management
254
+
255
+ | Module | Key exports |
256
+ |--------|------------|
257
+ | `src/campaign.ts` | `Campaign`, `stepCampaignDay`, `advanceCampaignClock`, `serializeCampaign`, `deserializeCampaign` |
258
+ | `src/downtime.ts` | `stepDowntime`, `TreatmentSchedule`, `EntityRecoveryReport` |
259
+ | `src/collective-activities.ts` | `createCollectiveProject`, `contributeToCollectiveProject`, `stepRitual`, `planCaravanRoute` |
260
+ | `src/inventory.ts` | `createInventory`, `addItemToInventory`, `consumeItemsByTemplateId`, `getEncumbrancePenalty` |
261
+ | `src/item-durability.ts` | `stepWear`, `applyWearPenalty` |
262
+ | `src/settlement.ts` | `createSettlement`, `upgradeSettlement`, `getServiceBonus` |
263
+ | `src/settlement-services.ts` | Service definitions (forge, medical, market, barracks, temple) |
264
+ | `src/inheritance.ts` | `transferEquipment`, `transferRelationships`, `transferInventory` — character death succession |
265
+ | `src/world-generation.ts` | `generateWorld`, `deriveStartingRelationships`, `deriveStartingConflicts` |
266
+
267
+ ### Quest and narrative layer
268
+
269
+ | Module | Key exports |
270
+ |--------|------------|
271
+ | `src/quest.ts` | `questFactory`, `updateQuestState`, `resolveObjective`, `Quest`, `QuestObjective` |
272
+ | `src/quest-generators.ts` | `generateBountyQuest`, `generateEscortQuest`, `generateRetrievalQuest` |
273
+ | `src/chronicle.ts` | `createChronicle`, `addChronicleEntry`, `getEntriesForEntity`, `ChronicleEntry` |
274
+ | `src/story-arcs.ts` | `detectStoryArcs`, `updateDetectedArcs` — pattern detection across chronicle entries |
275
+ | `src/narrative-render.ts` | `renderEntry`, `renderArcSummary` — template-based prose from chronicle entries |
276
+ | `src/legend.ts` | `createLegendRegistry`, `createLegendFromChronicle`, `applyLegendToDialogueContext` |
277
+ | `src/mythology.ts` | `compressMythsFromHistory`, `stepMythologyYear`, `aggregateFactionMythEffect`, `scaledMythEffect` |
278
+ | `src/narrative.ts` | `narrateCombatTick`, `narrateInjury` — human-readable combat event strings |
279
+ | `src/narrative-stress.ts` | `runNarrativeStressTest`, `scoreNarrativePush` |
280
+ | `src/metrics.ts` | `extractCombatMetrics`, `summariseBattle` — analytics from trace events |
281
+ | `src/arena.ts` | Arena scenario DSL, `runArenaTrial`, `runArenaScenario` |
282
+
283
+ ### Anatomy subsystem
284
+
285
+ | Module | Key exports |
286
+ |--------|------------|
287
+ | `src/anatomy/index.ts` | Re-export barrel for the full anatomy API |
288
+ | `src/anatomy/anatomy-contracts.ts` | `CompiledAnatomyModel`, `AnatomyContracts`, `AnatomyCapabilities` — core anatomy types |
289
+ | `src/anatomy/anatomy-schema.ts` | `validateExtendedBodyPlan`, `ValidationResult` — validate JSON body plan definitions |
290
+ | `src/anatomy/anatomy-compiler.ts` | `compileAnatomyDefinition`, `compileAnatomyDefinitionOrThrow` — compile a body plan to indexed model |
291
+ | `src/anatomy/anatomy-helpers.ts` | `createAnatomyHelpers`, `summarizeFunctionalHealth`, `sampleProfile` — query compiled anatomy |
292
+
293
+ ### Competence framework
294
+
295
+ | Module | Key exports |
296
+ |--------|------------|
297
+ | `src/competence/index.ts` | Re-export barrel for the full competence API |
298
+ | `src/competence/framework.ts` | `resolveCompetence(entity, action, ctx)` — unified competence resolution dispatcher |
299
+ | `src/competence/catalogue.ts` | `CompetenceDomain`, `CompetenceTask`, predefined task catalogue entries |
300
+
301
+ ### Crafting subsystem
302
+
303
+ | Module | Key exports |
304
+ |--------|------------|
305
+ | `src/crafting/index.ts` | `craftItem`, `startManufacturing`, `advanceManufacturing`, `getAvailableRecipes` — main crafting API |
203
306
 
204
307
  ---
205
308
 
@@ -207,16 +310,72 @@ A `CHANGELOG.md` entry will document any breaking change.
207
310
 
208
311
  These are implementation details. Do not import them directly; they may change at any time.
209
312
 
313
+ ### Kernel internals
314
+
210
315
  | Module | Why internal |
211
316
  |--------|-------------|
212
317
  | `src/rng.ts` | `makeRng`, `eventSeed`, `hashString` — RNG contract is internal; seed structure may change |
318
+ | `src/sim/seeds.ts` | Seed derivation utilities |
213
319
  | `src/sim/push.ts` | Pair-based resolution internals |
214
320
  | `src/sim/kernel.ts` (non-exported functions) | Step sub-phases, internal accumulators |
215
- | `src/sim/seeds.ts` | Seed derivation utilities |
216
- | `src/sim/ai/` | AI decision internals; host applications should use `buildAICommands()` via `src/sim/ai/system.ts` |
321
+ | `src/sim/tick.ts` | Single-tick orchestration called by `stepWorld` |
322
+ | `src/sim/action.ts` | Attack cooldown and swing-momentum state machine |
323
+ | `src/sim/intent.ts` | Movement and defence intent processing |
324
+ | `src/sim/combat.ts` | Hit resolution and skill-contest internals |
325
+ | `src/sim/step/` | All sub-step modules (`push`, `energy`, `injury`, `movement`, etc.) |
326
+ | `src/sim/context.ts` | `KernelContext` type definition (re-exported via `sim/world.ts`) |
327
+ | `src/sim/events.ts` | Internal event emission — consumed by trace and bridge |
328
+ | `src/sim/indexing.ts` | Spatial index internals used by `stepWorld` |
329
+ | `src/sim/tuning.ts` | Physics constant tables — may be retuned in patch releases |
330
+ | `src/sim/impairment.ts` | Functional damage accumulator — called by the kernel step |
331
+ | `src/sim/occlusion.ts` | Internal visibility occlusion used by sensory |
332
+ | `src/sim/systemic-toxicology.ts` | Multi-substance pharmacokinetics internals |
333
+ | `src/sim/formation-unit.ts` | Squad-level unit structure used by `formation-combat.ts` |
334
+ | `src/sim/commandBuilders.ts` | Low-level command construction helpers — prefer `noMove()` from `commands.ts` |
335
+ | `src/sim/team.ts` | Team/side definitions used internally by AI and morale |
336
+ | `src/derive.ts` | Movement-force and geometry derivations used by the kernel |
337
+ | `src/lod.ts` | Level-of-detail helpers for large simulations |
338
+ | `src/debug.ts` | Visual debug extraction (motion vectors, hit traces) |
339
+
340
+ ### AI decision internals
217
341
 
218
- > `buildAICommands()` from `src/sim/ai/system.ts` is Experimental (Tier 2).
219
- > The individual sub-modules (`decide.ts`, `perception.ts`, `targeting.ts`) are Tier 3.
342
+ | Module | Why internal |
343
+ |--------|-------------|
344
+ | `src/sim/ai/decide.ts` | Decision-tree evaluation — called by `buildAICommands` |
345
+ | `src/sim/ai/perception.ts` | AI sensory processing — called by `buildAICommands` |
346
+ | `src/sim/ai/targeting.ts` | Target selection heuristics — called by `buildAICommands` |
347
+ | `src/sim/ai/personality.ts` | Personality trait modifiers on AI decisions |
348
+ | `src/sim/ai/types.ts` | AI policy and state types used only within `src/sim/ai/` |
349
+
350
+ > Use `buildAICommands(world, ctx)` from `src/sim/ai/system.ts` (Tier 2) rather than importing AI sub-modules directly.
351
+
352
+ ### Competence domain resolvers
353
+
354
+ These are called by `resolveCompetence()` and should not be imported directly.
355
+
356
+ | Module | Domain |
357
+ |--------|--------|
358
+ | `src/competence/crafting.ts` | Crafting and tool use |
359
+ | `src/competence/navigation.ts` | Wayfinding and cartography |
360
+ | `src/competence/naturalist.ts` | Tracking, foraging, taming |
361
+ | `src/competence/interspecies.ts` | Cross-species communication |
362
+ | `src/competence/language.ts` | Linguistics and translation |
363
+ | `src/competence/teaching.ts` | Knowledge transfer |
364
+ | `src/competence/willpower.ts` | Endurance and mental fortitude |
365
+ | `src/competence/engineering.ts` | Siege and structural engineering |
366
+ | `src/competence/performance.ts` | Entertainment and oratory |
367
+ | `src/competence/acoustic.ts` | Formation signalling (drums, horns) |
368
+
369
+ ### Crafting internals
370
+
371
+ These are called by `craftItem()` / `startManufacturing()` and should not be imported directly.
372
+
373
+ | Module | Role |
374
+ |--------|------|
375
+ | `src/crafting/materials.ts` | Material definitions and property calculations |
376
+ | `src/crafting/recipes.ts` | Recipe validation and feasibility |
377
+ | `src/crafting/manufacturing.ts` | Batch production mechanics |
378
+ | `src/crafting/workshops.ts` | Workshop facility definitions and output bonuses |
220
379
 
221
380
  ---
222
381
 
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module ananke/campaign
3
+ * @tier2 Experimental — breaking changes get a CHANGELOG entry.
4
+ *
5
+ * Campaign and world management: downtime activities, collective projects,
6
+ * settlements, inventory, item durability, world generation, inheritance,
7
+ * economy, and polity/socio-economic systems.
8
+ *
9
+ * Import via subpath:
10
+ * import { stepDowntime, createSettlement } from "@its-not-rocket-science/ananke/campaign"
11
+ */
12
+ export * from "./campaign.js";
13
+ export * from "./downtime.js";
14
+ export * from "./collective-activities.js";
15
+ export * from "./settlement.js";
16
+ export * from "./settlement-services.js";
17
+ export * from "./inventory.js";
18
+ export * from "./item-durability.js";
19
+ export * from "./world-generation.js";
20
+ export * from "./inheritance.js";
21
+ export * from "./economy.js";
22
+ export * from "./polity.js";
23
+ export * from "./polity-vassals.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module ananke/campaign
3
+ * @tier2 Experimental — breaking changes get a CHANGELOG entry.
4
+ *
5
+ * Campaign and world management: downtime activities, collective projects,
6
+ * settlements, inventory, item durability, world generation, inheritance,
7
+ * economy, and polity/socio-economic systems.
8
+ *
9
+ * Import via subpath:
10
+ * import { stepDowntime, createSettlement } from "@its-not-rocket-science/ananke/campaign"
11
+ */
12
+ export * from "./campaign.js";
13
+ export * from "./downtime.js";
14
+ export * from "./collective-activities.js";
15
+ export * from "./settlement.js";
16
+ export * from "./settlement-services.js";
17
+ export * from "./inventory.js";
18
+ export * from "./item-durability.js";
19
+ export * from "./world-generation.js";
20
+ export * from "./inheritance.js";
21
+ export * from "./economy.js";
22
+ export * from "./polity.js";
23
+ export * from "./polity-vassals.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module ananke/character
3
+ * @tier2 Experimental — breaking changes get a CHANGELOG entry.
4
+ *
5
+ * Character lifecycle: aging, sleep, disease, wound healing, thermoregulation,
6
+ * nutrition, medical treatment, toxicology, and skill progression.
7
+ *
8
+ * Import via subpath:
9
+ * import { stepAging, stepSleep } from "@its-not-rocket-science/ananke/character"
10
+ */
11
+ export * from "./sim/aging.js";
12
+ export * from "./sim/sleep.js";
13
+ export * from "./sim/disease.js";
14
+ export * from "./sim/wound-aging.js";
15
+ export * from "./sim/thermoregulation.js";
16
+ export * from "./sim/nutrition.js";
17
+ export * from "./sim/medical.js";
18
+ export * from "./sim/toxicology.js";
19
+ export * from "./progression.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module ananke/character
3
+ * @tier2 Experimental — breaking changes get a CHANGELOG entry.
4
+ *
5
+ * Character lifecycle: aging, sleep, disease, wound healing, thermoregulation,
6
+ * nutrition, medical treatment, toxicology, and skill progression.
7
+ *
8
+ * Import via subpath:
9
+ * import { stepAging, stepSleep } from "@its-not-rocket-science/ananke/character"
10
+ */
11
+ export * from "./sim/aging.js";
12
+ export * from "./sim/sleep.js";
13
+ export * from "./sim/disease.js";
14
+ export * from "./sim/wound-aging.js";
15
+ export * from "./sim/thermoregulation.js";
16
+ export * from "./sim/nutrition.js";
17
+ export * from "./sim/medical.js";
18
+ export * from "./sim/toxicology.js";
19
+ export * from "./progression.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module ananke/combat
3
+ * @tier2 Experimental — breaking changes get a CHANGELOG entry.
4
+ *
5
+ * Combat extensions: ranged weapons, grappling, formation tactics, mounted
6
+ * combat, environmental hazards, morale, sensory systems, weather, terrain,
7
+ * skills, and biome effects.
8
+ *
9
+ * Import via subpath:
10
+ * import { resolveRangedAttack, stepGrapple } from "@its-not-rocket-science/ananke/combat"
11
+ */
12
+ export * from "./sim/ranged.js";
13
+ export * from "./sim/grapple.js";
14
+ export * from "./sim/formation-combat.js";
15
+ export * from "./sim/mount.js";
16
+ export * from "./sim/hazard.js";
17
+ export * from "./sim/morale.js";
18
+ export * from "./sim/sensory.js";
19
+ export * from "./sim/sensory-extended.js";
20
+ export * from "./sim/weather.js";
21
+ export * from "./sim/terrain.js";
22
+ export * from "./sim/skills.js";
23
+ export * from "./sim/biome.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module ananke/combat
3
+ * @tier2 Experimental — breaking changes get a CHANGELOG entry.
4
+ *
5
+ * Combat extensions: ranged weapons, grappling, formation tactics, mounted
6
+ * combat, environmental hazards, morale, sensory systems, weather, terrain,
7
+ * skills, and biome effects.
8
+ *
9
+ * Import via subpath:
10
+ * import { resolveRangedAttack, stepGrapple } from "@its-not-rocket-science/ananke/combat"
11
+ */
12
+ export * from "./sim/ranged.js";
13
+ export * from "./sim/grapple.js";
14
+ export * from "./sim/formation-combat.js";
15
+ export * from "./sim/mount.js";
16
+ export * from "./sim/hazard.js";
17
+ export * from "./sim/morale.js";
18
+ export * from "./sim/sensory.js";
19
+ export * from "./sim/sensory-extended.js";
20
+ export * from "./sim/weather.js";
21
+ export * from "./sim/terrain.js";
22
+ export * from "./sim/skills.js";
23
+ export * from "./sim/biome.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @module ananke/narrative
3
+ * @tier2 Experimental — breaking changes get a CHANGELOG entry.
4
+ *
5
+ * Narrative and chronicle systems: event chronicles, story arc detection,
6
+ * narrative rendering, legends, mythology, stress testing, combat metrics,
7
+ * and arena scenarios.
8
+ *
9
+ * Import via subpath:
10
+ * import { addChronicleEntry, detectStoryArcs } from "@its-not-rocket-science/ananke/narrative"
11
+ */
12
+ export * from "./chronicle.js";
13
+ export * from "./story-arcs.js";
14
+ export * from "./narrative-render.js";
15
+ export * from "./legend.js";
16
+ export * from "./mythology.js";
17
+ export * from "./narrative.js";
18
+ export * from "./narrative-stress.js";
19
+ export * from "./metrics.js";
20
+ export * from "./arena.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @module ananke/narrative
3
+ * @tier2 Experimental — breaking changes get a CHANGELOG entry.
4
+ *
5
+ * Narrative and chronicle systems: event chronicles, story arc detection,
6
+ * narrative rendering, legends, mythology, stress testing, combat metrics,
7
+ * and arena scenarios.
8
+ *
9
+ * Import via subpath:
10
+ * import { addChronicleEntry, detectStoryArcs } from "@its-not-rocket-science/ananke/narrative"
11
+ */
12
+ export * from "./chronicle.js";
13
+ export * from "./story-arcs.js";
14
+ export * from "./narrative-render.js";
15
+ export * from "./legend.js";
16
+ export * from "./mythology.js";
17
+ export * from "./narrative.js";
18
+ export * from "./narrative-stress.js";
19
+ export * from "./metrics.js";
20
+ export * from "./arena.js";
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Phase 70 — Stratified Political Simulation ("Vassal Web" Layer)
3
+ *
4
+ * Introduces a `VassalNode` between the individual Entity and the Polity.
5
+ * Seven loyalty types with distinct step dynamics allow political crises
6
+ * (rebellions, succession disputes, noble defections) to emerge from
7
+ * simulation state rather than scripted events.
8
+ *
9
+ * No kernel import — pure data-management module, fixed-point arithmetic only.
10
+ */
11
+ import type { Q } from "./units.js";
12
+ import type { Polity } from "./polity.js";
13
+ /**
14
+ * The basis of a vassal's loyalty to their liege.
15
+ *
16
+ * Determines how `stepVassalLoyalty` updates `loyaltyQ` each campaign tick
17
+ * and what events cause loyalty to spike or collapse.
18
+ */
19
+ export type LoyaltyType = "ideological" | "transactional" | "terrified" | "honor_bound" | "opportunistic" | "kin_bound" | "ideological_rival";
20
+ export interface VassalLoyalty {
21
+ type: LoyaltyType;
22
+ /** Current loyalty level [0, SCALE.Q]. q(0) = open rebellion; q(1) = unconditional. */
23
+ loyaltyQ: Q;
24
+ /**
25
+ * Accumulated grievances [0, SCALE.Q].
26
+ * Drains loyalty each tick; applied by `applyGrievanceEvent` or set directly by the host.
27
+ */
28
+ grievance_Q: Q;
29
+ }
30
+ export interface VassalNode {
31
+ /** Unique identifier, e.g. "house_harlow", "guild_weavers". */
32
+ id: string;
33
+ /** The liege polity this vassal owes service to. */
34
+ polityId: string;
35
+ /** Fractional share of polity territory controlled [0, SCALE.Q]. */
36
+ territory_Q: Q;
37
+ /**
38
+ * Fractional share of polity military strength contracted from this vassal
39
+ * when loyalty is full [0, SCALE.Q].
40
+ */
41
+ military_Q: Q;
42
+ /** Vassal's own treasury reserves in cost-units (independent of polity). */
43
+ treasury_cu: number;
44
+ loyalty: VassalLoyalty;
45
+ }
46
+ export interface VassalContribution {
47
+ /** Actual troop fraction provided this tick (after loyalty scaling). */
48
+ troops_Q: Q;
49
+ /** Actual treasury contribution in cost-units this tick (after loyalty scaling). */
50
+ treasury_cu: number;
51
+ }
52
+ export interface SuccessionResult {
53
+ /** The heir identifier that was evaluated. */
54
+ heirId: string;
55
+ /** True if the heir secured majority military support; false = contested succession. */
56
+ successful: boolean;
57
+ /** Weighted military support fraction for the heir [0, SCALE.Q]. */
58
+ supportQ: Q;
59
+ /**
60
+ * Loyalty delta for each vassal this tick (id → delta Q, can be negative).
61
+ * Supporters of the winning side gain loyalty; supporters of the losing side lose it.
62
+ */
63
+ loyaltyDeltas: Map<string, Q>;
64
+ }
65
+ /** Natural grievance decay per tick for most loyalty types. */
66
+ export declare const GRIEVANCE_DECAY_Q: Q;
67
+ /** Slower natural decay for honor_bound — oaths and grudges linger. */
68
+ export declare const GRIEVANCE_DECAY_HONOR_Q: Q;
69
+ /** Hard loyalty ceiling for terrified vassals (fear ≠ devotion). */
70
+ export declare const TERRIFIED_MAX_LOYALTY_Q: Q;
71
+ /** Loyalty target kin_bound vassals gravitate toward in the absence of grievance. */
72
+ export declare const KIN_BOUND_BASE_Q: Q;
73
+ /** Constant loyalty decay per tick for ideological_rival — undermining is ceaseless. */
74
+ export declare const RIVAL_DECAY_Q: Q;
75
+ /** Below this loyalty Q, contribution drops to zero (passive defiance). */
76
+ export declare const CONTRIBUTION_FLOOR_Q: Q;
77
+ /** At or above this loyalty Q, full contribution is provided. */
78
+ export declare const CONTRIBUTION_FULL_Q: Q;
79
+ /**
80
+ * Treasury difference (in cost-units) that shifts transactional loyalty by q(0.40).
81
+ * A liege with 50 000 cu more than the richest rival earns maximum loyalty advantage.
82
+ */
83
+ export declare const TRANSACTIONAL_TREASURY_NORM = 50000;
84
+ /** eventSeed salt for succession crisis rolls. */
85
+ export declare const SUCCESSION_SALT: number;
86
+ /**
87
+ * Apply a grievance event to a vassal and return the updated node.
88
+ * Typical events: broken promise, tax hike, kin killed in service, territory seized.
89
+ *
90
+ * @param delta_Q Grievance increment [0, SCALE.Q]; positive = more aggrieved.
91
+ */
92
+ export declare function applyGrievanceEvent(node: VassalNode, delta_Q: Q): VassalNode;
93
+ /**
94
+ * Advance a vassal's loyalty state by one campaign tick.
95
+ *
96
+ * Loyalty dynamics are determined entirely by the vassal's `LoyaltyType`.
97
+ * Returns a new `VassalNode` — the input is never mutated.
98
+ *
99
+ * @param node Current vassal state.
100
+ * @param liege The liege polity.
101
+ * @param rivals All other polities the vassal might consider as alternatives.
102
+ * @param worldSeed Deterministic RNG seed (unused directly — reserved for future variance).
103
+ * @param tick Current campaign tick.
104
+ */
105
+ export declare function stepVassalLoyalty(node: VassalNode, liege: Polity, rivals: readonly Polity[], _worldSeed: number, _tick: number): VassalNode;
106
+ /**
107
+ * Compute the actual troop and treasury contribution a vassal provides this tick.
108
+ *
109
+ * - `loyaltyQ >= CONTRIBUTION_FULL_Q` (q(0.50)): full contracted contribution.
110
+ * - `loyaltyQ <= CONTRIBUTION_FLOOR_Q` (q(0.20)): zero (passive defiance).
111
+ * - Between floor and full: linear interpolation.
112
+ */
113
+ export declare function computeVassalContribution(node: VassalNode): VassalContribution;
114
+ /**
115
+ * Aggregate the effective military strength a polity can actually field,
116
+ * accounting for disloyal vassals.
117
+ *
118
+ * Pass this value as the force multiplier to `resolveTacticalEngagement` (Phase 69)
119
+ * instead of the polity's nominal `militaryStrength_Q`.
120
+ *
121
+ * ```typescript
122
+ * const effective = computeEffectiveMilitary(vassals);
123
+ * // scale polity's military by effective fraction
124
+ * const scaledForce = mulDiv(polity.militaryStrength_Q, effective, SCALE.Q);
125
+ * ```
126
+ */
127
+ export declare function computeEffectiveMilitary(vassals: readonly VassalNode[]): Q;
128
+ /**
129
+ * Compute the rebellion risk for a vassal [0, SCALE.Q].
130
+ *
131
+ * Risk = 70 % from low loyalty + 30 % from high grievance.
132
+ * Use as an AI query or host-side event trigger threshold.
133
+ */
134
+ export declare function detectRebellionRisk(node: VassalNode): Q;
135
+ /**
136
+ * Resolve a succession crisis: determine whether the intended heir secures
137
+ * enough vassal support to rule unchallenged.
138
+ *
139
+ * Each vassal "votes" based on their loyalty type, current loyalty level,
140
+ * and a deterministic roll from `eventSeed`. The vote is weighted by
141
+ * `military_Q` so powerful nobles matter more.
142
+ *
143
+ * On success: supporters gain +q(0.05) loyalty; opponents lose −q(0.08).
144
+ * On failure: deltas are inverted (the pretender's faction benefits).
145
+ *
146
+ * @param polity The polity undergoing succession.
147
+ * @param vassals Current vassal roster.
148
+ * @param heirId Identifier of the intended heir.
149
+ * @param worldSeed
150
+ * @param tick
151
+ */
152
+ export declare function resolveSuccessionCrisis(polity: Polity, vassals: readonly VassalNode[], heirId: string, worldSeed: number, tick: number): SuccessionResult;
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Phase 70 — Stratified Political Simulation ("Vassal Web" Layer)
3
+ *
4
+ * Introduces a `VassalNode` between the individual Entity and the Polity.
5
+ * Seven loyalty types with distinct step dynamics allow political crises
6
+ * (rebellions, succession disputes, noble defections) to emerge from
7
+ * simulation state rather than scripted events.
8
+ *
9
+ * No kernel import — pure data-management module, fixed-point arithmetic only.
10
+ */
11
+ import { SCALE, q, clampQ, mulDiv } from "./units.js";
12
+ import { eventSeed, hashString } from "./sim/seeds.js";
13
+ // ── Constants ─────────────────────────────────────────────────────────────────
14
+ /** Natural grievance decay per tick for most loyalty types. */
15
+ export const GRIEVANCE_DECAY_Q = q(0.010);
16
+ /** Slower natural decay for honor_bound — oaths and grudges linger. */
17
+ export const GRIEVANCE_DECAY_HONOR_Q = q(0.004);
18
+ /** Hard loyalty ceiling for terrified vassals (fear ≠ devotion). */
19
+ export const TERRIFIED_MAX_LOYALTY_Q = q(0.70);
20
+ /** Loyalty target kin_bound vassals gravitate toward in the absence of grievance. */
21
+ export const KIN_BOUND_BASE_Q = q(0.85);
22
+ /** Constant loyalty decay per tick for ideological_rival — undermining is ceaseless. */
23
+ export const RIVAL_DECAY_Q = q(0.005);
24
+ /** Below this loyalty Q, contribution drops to zero (passive defiance). */
25
+ export const CONTRIBUTION_FLOOR_Q = q(0.20);
26
+ /** At or above this loyalty Q, full contribution is provided. */
27
+ export const CONTRIBUTION_FULL_Q = q(0.50);
28
+ /**
29
+ * Treasury difference (in cost-units) that shifts transactional loyalty by q(0.40).
30
+ * A liege with 50 000 cu more than the richest rival earns maximum loyalty advantage.
31
+ */
32
+ export const TRANSACTIONAL_TREASURY_NORM = 50_000;
33
+ /** eventSeed salt for succession crisis rolls. */
34
+ export const SUCCESSION_SALT = 0x5ECC;
35
+ // ── Grievance events ──────────────────────────────────────────────────────────
36
+ /**
37
+ * Apply a grievance event to a vassal and return the updated node.
38
+ * Typical events: broken promise, tax hike, kin killed in service, territory seized.
39
+ *
40
+ * @param delta_Q Grievance increment [0, SCALE.Q]; positive = more aggrieved.
41
+ */
42
+ export function applyGrievanceEvent(node, delta_Q) {
43
+ return {
44
+ ...node,
45
+ loyalty: {
46
+ ...node.loyalty,
47
+ grievance_Q: clampQ(node.loyalty.grievance_Q + delta_Q, 0, SCALE.Q),
48
+ },
49
+ };
50
+ }
51
+ // ── Loyalty step ──────────────────────────────────────────────────────────────
52
+ /**
53
+ * Advance a vassal's loyalty state by one campaign tick.
54
+ *
55
+ * Loyalty dynamics are determined entirely by the vassal's `LoyaltyType`.
56
+ * Returns a new `VassalNode` — the input is never mutated.
57
+ *
58
+ * @param node Current vassal state.
59
+ * @param liege The liege polity.
60
+ * @param rivals All other polities the vassal might consider as alternatives.
61
+ * @param worldSeed Deterministic RNG seed (unused directly — reserved for future variance).
62
+ * @param tick Current campaign tick.
63
+ */
64
+ export function stepVassalLoyalty(node, liege, rivals, _worldSeed, _tick) {
65
+ const { loyalty } = node;
66
+ const { type, loyaltyQ, grievance_Q } = loyalty;
67
+ // ── Step 1: decay grievance naturally ─────────────────────────────────────
68
+ const decayRate = type === "honor_bound" ? GRIEVANCE_DECAY_HONOR_Q : GRIEVANCE_DECAY_Q;
69
+ const newGrievance = clampQ(grievance_Q - decayRate, 0, SCALE.Q);
70
+ // ── Step 2: compute loyalty delta by type ─────────────────────────────────
71
+ let deltaNum = 0;
72
+ switch (type) {
73
+ case "ideological": {
74
+ // Slow ideological drift: grievance has minimal effect; passive recovery when content.
75
+ if (newGrievance > q(0.30)) {
76
+ deltaNum = -mulDiv(newGrievance, q(0.08), SCALE.Q);
77
+ }
78
+ else {
79
+ // Gentle recovery toward conviction
80
+ deltaNum = loyaltyQ < SCALE.Q ? q(0.003) : 0;
81
+ }
82
+ break;
83
+ }
84
+ case "transactional": {
85
+ // Target loyalty tracks how much richer the liege is than the best-paying rival.
86
+ const maxRivalTreasury = rivals.reduce((best, r) => Math.max(best, r.treasury_cu), 0);
87
+ const diff = Math.max(-TRANSACTIONAL_TREASURY_NORM, Math.min(TRANSACTIONAL_TREASURY_NORM, liege.treasury_cu - maxRivalTreasury));
88
+ const targetQ = clampQ(q(0.50) + Math.round(diff * q(0.40) / TRANSACTIONAL_TREASURY_NORM), 0, SCALE.Q);
89
+ // Move 5 %/tick toward target; grievance also bleeds loyalty.
90
+ deltaNum = mulDiv(targetQ - loyaltyQ, q(0.05), SCALE.Q);
91
+ deltaNum -= mulDiv(newGrievance, q(0.20), SCALE.Q);
92
+ break;
93
+ }
94
+ case "terrified": {
95
+ // Instant collapse if the liege is no stronger than the vassal.
96
+ if (liege.militaryStrength_Q <= node.military_Q) {
97
+ return {
98
+ ...node,
99
+ loyalty: { type, loyaltyQ: q(0.0), grievance_Q: newGrievance },
100
+ };
101
+ }
102
+ // Slow recovery toward the terrified ceiling; grievance undermines it.
103
+ deltaNum = mulDiv(TERRIFIED_MAX_LOYALTY_Q - loyaltyQ, q(0.03), SCALE.Q);
104
+ deltaNum -= mulDiv(newGrievance, q(0.25), SCALE.Q);
105
+ break;
106
+ }
107
+ case "honor_bound": {
108
+ // Heavy grievance causes sharp loyalty drain; without it, loyalty recovers.
109
+ if (grievance_Q > q(0.40)) {
110
+ deltaNum = -mulDiv(grievance_Q, q(0.30), SCALE.Q);
111
+ }
112
+ else {
113
+ deltaNum = loyaltyQ < q(0.90) ? q(0.008) : 0;
114
+ deltaNum -= mulDiv(newGrievance, q(0.10), SCALE.Q);
115
+ }
116
+ break;
117
+ }
118
+ case "opportunistic": {
119
+ // Target loyalty = liege.moraleQ: stays loyal when the liege is thriving,
120
+ // drifts away when the liege looks weak compared to rivals.
121
+ // Rivals pull target down proportionally if any of them out-morale the liege.
122
+ let maxRivalMorale = 0;
123
+ for (const r of rivals)
124
+ if (r.moraleQ > maxRivalMorale)
125
+ maxRivalMorale = r.moraleQ;
126
+ // If a rival outperforms the liege, drag the target below liege.moraleQ.
127
+ const relativeQ = maxRivalMorale > liege.moraleQ
128
+ ? clampQ(mulDiv(liege.moraleQ, SCALE.Q, maxRivalMorale), 0, SCALE.Q)
129
+ : liege.moraleQ;
130
+ deltaNum = mulDiv(relativeQ - loyaltyQ, q(0.08), SCALE.Q);
131
+ deltaNum -= mulDiv(newGrievance, q(0.15), SCALE.Q);
132
+ break;
133
+ }
134
+ case "kin_bound": {
135
+ // Very stable; slow recovery; grievance has half the normal weight.
136
+ deltaNum = loyaltyQ < KIN_BOUND_BASE_Q ? q(0.01) : 0;
137
+ deltaNum -= mulDiv(newGrievance, q(0.10), SCALE.Q);
138
+ break;
139
+ }
140
+ case "ideological_rival": {
141
+ // Constant, inexorable decay — no incentive can reverse it.
142
+ deltaNum = -RIVAL_DECAY_Q;
143
+ break;
144
+ }
145
+ }
146
+ const rawLoyalty = clampQ(loyaltyQ + deltaNum, 0, SCALE.Q);
147
+ const newLoyalty = type === "terrified"
148
+ ? clampQ(rawLoyalty, 0, TERRIFIED_MAX_LOYALTY_Q)
149
+ : rawLoyalty;
150
+ return {
151
+ ...node,
152
+ loyalty: { type, loyaltyQ: newLoyalty, grievance_Q: newGrievance },
153
+ };
154
+ }
155
+ // ── Contribution ──────────────────────────────────────────────────────────────
156
+ /**
157
+ * Compute the actual troop and treasury contribution a vassal provides this tick.
158
+ *
159
+ * - `loyaltyQ >= CONTRIBUTION_FULL_Q` (q(0.50)): full contracted contribution.
160
+ * - `loyaltyQ <= CONTRIBUTION_FLOOR_Q` (q(0.20)): zero (passive defiance).
161
+ * - Between floor and full: linear interpolation.
162
+ */
163
+ export function computeVassalContribution(node) {
164
+ const { loyaltyQ } = node.loyalty;
165
+ const range = CONTRIBUTION_FULL_Q - CONTRIBUTION_FLOOR_Q; // q(0.30)
166
+ let factor;
167
+ if (loyaltyQ >= CONTRIBUTION_FULL_Q) {
168
+ factor = SCALE.Q;
169
+ }
170
+ else if (loyaltyQ <= CONTRIBUTION_FLOOR_Q) {
171
+ factor = 0;
172
+ }
173
+ else {
174
+ factor = Math.round((loyaltyQ - CONTRIBUTION_FLOOR_Q) * SCALE.Q / range);
175
+ }
176
+ return {
177
+ troops_Q: mulDiv(node.military_Q, factor, SCALE.Q),
178
+ treasury_cu: Math.round(node.treasury_cu * factor / SCALE.Q),
179
+ };
180
+ }
181
+ /**
182
+ * Aggregate the effective military strength a polity can actually field,
183
+ * accounting for disloyal vassals.
184
+ *
185
+ * Pass this value as the force multiplier to `resolveTacticalEngagement` (Phase 69)
186
+ * instead of the polity's nominal `militaryStrength_Q`.
187
+ *
188
+ * ```typescript
189
+ * const effective = computeEffectiveMilitary(vassals);
190
+ * // scale polity's military by effective fraction
191
+ * const scaledForce = mulDiv(polity.militaryStrength_Q, effective, SCALE.Q);
192
+ * ```
193
+ */
194
+ export function computeEffectiveMilitary(vassals) {
195
+ return clampQ(vassals.reduce((sum, v) => sum + computeVassalContribution(v).troops_Q, 0), 0, SCALE.Q);
196
+ }
197
+ // ── Rebellion risk ────────────────────────────────────────────────────────────
198
+ /**
199
+ * Compute the rebellion risk for a vassal [0, SCALE.Q].
200
+ *
201
+ * Risk = 70 % from low loyalty + 30 % from high grievance.
202
+ * Use as an AI query or host-side event trigger threshold.
203
+ */
204
+ export function detectRebellionRisk(node) {
205
+ const { loyaltyQ, grievance_Q } = node.loyalty;
206
+ const loyaltyContrib = mulDiv(SCALE.Q - loyaltyQ, q(0.70), SCALE.Q);
207
+ const grievanceContrib = mulDiv(grievance_Q, q(0.30), SCALE.Q);
208
+ return clampQ(loyaltyContrib + grievanceContrib, 0, SCALE.Q);
209
+ }
210
+ // ── Succession crisis ─────────────────────────────────────────────────────────
211
+ /**
212
+ * Resolve a succession crisis: determine whether the intended heir secures
213
+ * enough vassal support to rule unchallenged.
214
+ *
215
+ * Each vassal "votes" based on their loyalty type, current loyalty level,
216
+ * and a deterministic roll from `eventSeed`. The vote is weighted by
217
+ * `military_Q` so powerful nobles matter more.
218
+ *
219
+ * On success: supporters gain +q(0.05) loyalty; opponents lose −q(0.08).
220
+ * On failure: deltas are inverted (the pretender's faction benefits).
221
+ *
222
+ * @param polity The polity undergoing succession.
223
+ * @param vassals Current vassal roster.
224
+ * @param heirId Identifier of the intended heir.
225
+ * @param worldSeed
226
+ * @param tick
227
+ */
228
+ export function resolveSuccessionCrisis(polity, vassals, heirId, worldSeed, tick) {
229
+ const polityHash = hashString(polity.id);
230
+ const heirHash = hashString(heirId);
231
+ let supportMilitary = 0;
232
+ let totalMilitary = 0;
233
+ const supportsHeir = new Map();
234
+ for (const vassal of vassals) {
235
+ const vassalHash = hashString(vassal.id);
236
+ const seed = eventSeed(worldSeed, tick, vassalHash, heirHash, (polityHash + SUCCESSION_SALT) & 0x7fff);
237
+ const roll = seed % (SCALE.Q + 1); // 0 .. SCALE.Q
238
+ // Support threshold: vassal supports heir if roll < threshold
239
+ let threshold;
240
+ switch (vassal.loyalty.type) {
241
+ case "ideological":
242
+ // Ideologically committed to the current regime → likely backs the heir.
243
+ threshold = q(0.70);
244
+ break;
245
+ case "transactional":
246
+ // Support proportional to current loyalty (loyalty ≈ economic satisfaction).
247
+ threshold = vassal.loyalty.loyaltyQ;
248
+ break;
249
+ case "terrified":
250
+ // Terrified vassals back whoever they think will win; proxy: loyalty.
251
+ threshold = vassal.loyalty.loyaltyQ;
252
+ break;
253
+ case "honor_bound":
254
+ // Oath-bound: strong support unless grievance has eroded trust.
255
+ threshold = vassal.loyalty.grievance_Q > q(0.60) ? q(0.25) : q(0.80);
256
+ break;
257
+ case "opportunistic":
258
+ // Genuinely uncertain — 50/50 until the outcome is clear.
259
+ threshold = q(0.50);
260
+ break;
261
+ case "kin_bound":
262
+ // Family ties: strongly backs the heir.
263
+ threshold = clampQ(vassal.loyalty.loyaltyQ + q(0.10), 0, SCALE.Q);
264
+ break;
265
+ case "ideological_rival":
266
+ // Almost never supports the heir — opposition is their purpose.
267
+ threshold = q(0.10);
268
+ break;
269
+ }
270
+ const backs = roll < threshold;
271
+ supportsHeir.set(vassal.id, backs);
272
+ totalMilitary += vassal.military_Q;
273
+ if (backs)
274
+ supportMilitary += vassal.military_Q;
275
+ }
276
+ const supportQ = totalMilitary > 0
277
+ ? clampQ(Math.round(supportMilitary * SCALE.Q / totalMilitary), 0, SCALE.Q)
278
+ : q(0.0);
279
+ const successful = supportQ > q(0.50);
280
+ const loyaltyDeltas = new Map();
281
+ for (const vassal of vassals) {
282
+ const backed = supportsHeir.get(vassal.id) ?? false;
283
+ // Winners gain loyalty; losers lose it (winning/losing is relative to outcome).
284
+ const backedHeir = backed;
285
+ const onWinningSide = successful ? backedHeir : !backedHeir;
286
+ loyaltyDeltas.set(vassal.id, (onWinningSide ? q(0.05) : -q(0.08)));
287
+ }
288
+ return { heirId, successful, supportQ, loyaltyDeltas };
289
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @module ananke/social
3
+ * @tier2 Experimental — breaking changes get a CHANGELOG entry.
4
+ *
5
+ * Social systems: dialogue, factions, relationships, party management,
6
+ * quests, and quest generation.
7
+ *
8
+ * Import via subpath:
9
+ * import { dialogueProbability, effectiveStanding } from "@its-not-rocket-science/ananke/social"
10
+ */
11
+ export * from "./dialogue.js";
12
+ export * from "./faction.js";
13
+ export * from "./relationships.js";
14
+ export * from "./relationships-effects.js";
15
+ export * from "./party.js";
16
+ export * from "./quest.js";
17
+ export * from "./quest-generators.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @module ananke/social
3
+ * @tier2 Experimental — breaking changes get a CHANGELOG entry.
4
+ *
5
+ * Social systems: dialogue, factions, relationships, party management,
6
+ * quests, and quest generation.
7
+ *
8
+ * Import via subpath:
9
+ * import { dialogueProbability, effectiveStanding } from "@its-not-rocket-science/ananke/social"
10
+ */
11
+ export * from "./dialogue.js";
12
+ export * from "./faction.js";
13
+ export * from "./relationships.js";
14
+ export * from "./relationships-effects.js";
15
+ export * from "./party.js";
16
+ export * from "./quest.js";
17
+ export * from "./quest-generators.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@its-not-rocket-science/ananke",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
6
6
  "license": "MIT",
@@ -22,6 +22,38 @@
22
22
  "./catalog": {
23
23
  "import": "./dist/src/catalog.js",
24
24
  "types": "./dist/src/catalog.d.ts"
25
+ },
26
+ "./character": {
27
+ "import": "./dist/src/character.js",
28
+ "types": "./dist/src/character.d.ts"
29
+ },
30
+ "./combat": {
31
+ "import": "./dist/src/combat.js",
32
+ "types": "./dist/src/combat.d.ts"
33
+ },
34
+ "./campaign": {
35
+ "import": "./dist/src/campaign-layer.js",
36
+ "types": "./dist/src/campaign-layer.d.ts"
37
+ },
38
+ "./social": {
39
+ "import": "./dist/src/social.js",
40
+ "types": "./dist/src/social.d.ts"
41
+ },
42
+ "./narrative": {
43
+ "import": "./dist/src/narrative-layer.js",
44
+ "types": "./dist/src/narrative-layer.d.ts"
45
+ },
46
+ "./anatomy": {
47
+ "import": "./dist/src/anatomy/index.js",
48
+ "types": "./dist/src/anatomy/index.d.ts"
49
+ },
50
+ "./crafting": {
51
+ "import": "./dist/src/crafting/index.js",
52
+ "types": "./dist/src/crafting/index.d.ts"
53
+ },
54
+ "./competence": {
55
+ "import": "./dist/src/competence/index.js",
56
+ "types": "./dist/src/competence/index.d.ts"
25
57
  }
26
58
  },
27
59
  "files": [