@its-not-rocket-science/ananke 0.1.17 → 0.1.23

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.
@@ -0,0 +1,161 @@
1
+ // src/feudal.ts — Phase 79: Feudal Bonds & Vassal Tribute
2
+ //
3
+ // Tracks lord-vassal polity relationships including tribute, military levies,
4
+ // bond strength, and revolt risk. Integrates with Phase 61 (Polity) for
5
+ // treasury/military and Phase 75 (Renown) for oath-breaking infamy.
6
+ //
7
+ // Design:
8
+ // - Pure data layer — no Entity fields, no kernel changes.
9
+ // - `FeudalRegistry` is external to PolityRegistry; hosts maintain both.
10
+ // - Bond strength decays over time and recovers via positive events
11
+ // (kinship ties, shared victories, tribute payment).
12
+ // - `isRebellionRisk` provides a clear boolean hook for AI and event triggers.
13
+ import { getRenownRecord } from "./renown.js";
14
+ import { q, SCALE, clampQ, mulDiv } from "./units.js";
15
+ // ── Constants ─────────────────────────────────────────────────────────────────
16
+ /** Bond strength below this → `isRebellionRisk` returns true. */
17
+ export const REBELLION_THRESHOLD = q(0.25);
18
+ /** Daily strength decay for each loyalty type (per simulated day). */
19
+ export const LOYALTY_DECAY_PER_DAY = {
20
+ kin_bound: q(0.001), // very slow — family ties are resilient
21
+ oath_sworn: q(0.002),
22
+ voluntary: q(0.003),
23
+ conquered: q(0.005), // fastest — resentment grows quickly
24
+ };
25
+ /** Base strength at bond creation per loyalty type. */
26
+ export const LOYALTY_BASE_STRENGTH = {
27
+ kin_bound: q(0.90),
28
+ oath_sworn: q(0.70),
29
+ voluntary: q(0.65),
30
+ conquered: q(0.40),
31
+ };
32
+ /**
33
+ * Infamy added to the vassal's renown record when breaking an `oath_sworn` bond.
34
+ * `kin_bound` and `conquered` breaks carry no oath infamy.
35
+ */
36
+ export const OATH_BREAK_INFAMY_Q = q(0.15);
37
+ /** Tribute paid daily = `TRIBUTE_DAILY_FRAC` × annual rate × treasury_cu */
38
+ export const TRIBUTE_DAYS_PER_YEAR = 365;
39
+ // ── Factory ───────────────────────────────────────────────────────────────────
40
+ export function createFeudalRegistry() {
41
+ return { bonds: new Map() };
42
+ }
43
+ // ── Bond key ──────────────────────────────────────────────────────────────────
44
+ function bondKey(vassalId, liegeId) {
45
+ return `${vassalId}:${liegeId}`;
46
+ }
47
+ // ── Bond management ───────────────────────────────────────────────────────────
48
+ /**
49
+ * Create a vassal bond and register it.
50
+ * If a bond between this pair already exists it is overwritten.
51
+ *
52
+ * @param tributeRate_Q Annual tribute as fraction of vassal treasury (default q(0.10)).
53
+ * @param levyRate_Q Fraction of military available as levy (default q(0.20)).
54
+ * @param tick Current simulation tick.
55
+ */
56
+ export function createVassalBond(registry, vassalPolityId, liegePolityId, loyaltyType, tributeRate_Q = q(0.10), levyRate_Q = q(0.20), tick = 0) {
57
+ const bond = {
58
+ vassalPolityId,
59
+ liegePolityId,
60
+ loyaltyType,
61
+ tributeRate_Q,
62
+ levyRate_Q,
63
+ strength_Q: LOYALTY_BASE_STRENGTH[loyaltyType],
64
+ establishedTick: tick,
65
+ };
66
+ registry.bonds.set(bondKey(vassalPolityId, liegePolityId), bond);
67
+ return bond;
68
+ }
69
+ /** Return the bond from `vassalId` to `liegeId`, or `undefined` if none. */
70
+ export function getBond(registry, vassalId, liegeId) {
71
+ return registry.bonds.get(bondKey(vassalId, liegeId));
72
+ }
73
+ /** Return all active bonds where `liegeId` is the lord. */
74
+ export function getVassals(registry, liegeId) {
75
+ return [...registry.bonds.values()].filter(b => b.liegePolityId === liegeId);
76
+ }
77
+ /** Return the bond where `vassalId` is the vassal, or `undefined`. */
78
+ export function getLiege(registry, vassalId) {
79
+ return [...registry.bonds.values()].find(b => b.vassalPolityId === vassalId);
80
+ }
81
+ // ── Tribute computation ───────────────────────────────────────────────────────
82
+ /**
83
+ * Compute the tribute owed for one simulated day.
84
+ * Scales linearly: `daily = floor(treasury_cu × tributeRate_Q / SCALE.Q / DAYS_PER_YEAR)`.
85
+ * Returns 0 if the vassal treasury is empty.
86
+ */
87
+ export function computeDailyTribute(vassal, bond) {
88
+ if (vassal.treasury_cu <= 0)
89
+ return 0;
90
+ return Math.floor(vassal.treasury_cu * bond.tributeRate_Q / SCALE.Q / TRIBUTE_DAYS_PER_YEAR);
91
+ }
92
+ /**
93
+ * Apply one day of tribute: deduct from vassal treasury and add to liege treasury.
94
+ * Mutates both polity objects.
95
+ * No-op if computed tribute is 0.
96
+ */
97
+ export function applyDailyTribute(vassal, liege, bond) {
98
+ const tribute = computeDailyTribute(vassal, bond);
99
+ if (tribute <= 0)
100
+ return 0;
101
+ vassal.treasury_cu = Math.max(0, vassal.treasury_cu - tribute);
102
+ liege.treasury_cu += tribute;
103
+ return tribute;
104
+ }
105
+ // ── Levy computation ──────────────────────────────────────────────────────────
106
+ /**
107
+ * Compute the military strength available to the liege as a levy.
108
+ * = `vassal.militaryStrength_Q × levyRate_Q × bond.strength_Q`.
109
+ * A weakened bond reduces the effective levy.
110
+ */
111
+ export function computeLevyStrength(vassal, bond) {
112
+ const raw = mulDiv(mulDiv(vassal.militaryStrength_Q, bond.levyRate_Q, SCALE.Q), bond.strength_Q, SCALE.Q);
113
+ return clampQ(raw, 0, SCALE.Q);
114
+ }
115
+ // ── Bond strength ─────────────────────────────────────────────────────────────
116
+ /**
117
+ * Advance bond strength by one simulated day.
118
+ * Strength decays at `LOYALTY_DECAY_PER_DAY[loyaltyType]`.
119
+ * `boostDelta_Q` is an optional signed daily bonus (e.g., from kinship, shared victory,
120
+ * good governance). Positive = strengthen; negative = additional stress.
121
+ * Mutates `bond.strength_Q` directly.
122
+ */
123
+ export function stepBondStrength(bond, boostDelta_Q = 0) {
124
+ const decay = LOYALTY_DECAY_PER_DAY[bond.loyaltyType];
125
+ bond.strength_Q = clampQ(bond.strength_Q - decay + boostDelta_Q, 0, SCALE.Q);
126
+ }
127
+ /**
128
+ * Strengthen a bond by a fixed delta (e.g., after a kinship event or tribute payment).
129
+ * Clamps to [0, SCALE.Q].
130
+ */
131
+ export function reinforceBond(bond, deltaQ) {
132
+ bond.strength_Q = clampQ(bond.strength_Q + deltaQ, 0, SCALE.Q);
133
+ }
134
+ // ── Rebellion risk ────────────────────────────────────────────────────────────
135
+ /** Return `true` if the bond is at rebellion risk (`strength_Q < REBELLION_THRESHOLD`). */
136
+ export function isRebellionRisk(bond) {
137
+ return bond.strength_Q < REBELLION_THRESHOLD;
138
+ }
139
+ // ── Bond breaking ─────────────────────────────────────────────────────────────
140
+ /**
141
+ * Break a vassal bond and remove it from the registry.
142
+ * For `oath_sworn` bonds, adds `OATH_BREAK_INFAMY_Q` to the vassal ruler's renown
143
+ * record if `vassalRulerId` and `renownRegistry` are provided.
144
+ *
145
+ * @returns `true` if a bond was found and removed; `false` otherwise.
146
+ */
147
+ export function breakVassalBond(registry, vassalPolityId, liegePolityId, vassalRulerId, renownRegistry) {
148
+ const key = bondKey(vassalPolityId, liegePolityId);
149
+ const bond = registry.bonds.get(key);
150
+ if (!bond)
151
+ return false;
152
+ // Oath-breaking infamy
153
+ if (bond.loyaltyType === "oath_sworn" &&
154
+ vassalRulerId != null &&
155
+ renownRegistry != null) {
156
+ const record = getRenownRecord(renownRegistry, vassalRulerId);
157
+ record.infamy_Q = clampQ(record.infamy_Q + OATH_BREAK_INFAMY_Q, 0, SCALE.Q);
158
+ }
159
+ registry.bonds.delete(key);
160
+ return true;
161
+ }
@@ -0,0 +1,92 @@
1
+ import type { RenownRegistry } from "./renown.js";
2
+ import type { Q } from "./units.js";
3
+ /** A single entity's family links within the lineage graph. */
4
+ export interface LineageNode {
5
+ entityId: number;
6
+ /** 0, 1, or 2 biological parent IDs. */
7
+ parentIds: number[];
8
+ /** All children recorded via `recordBirth`. */
9
+ childIds: number[];
10
+ /** All recorded partners (may grow over time). */
11
+ partnerIds: number[];
12
+ }
13
+ /** Registry of all lineage nodes, keyed by entityId. */
14
+ export interface LineageRegistry {
15
+ nodes: Map<number, LineageNode>;
16
+ }
17
+ /** Human-readable kinship label derived from `computeKinshipDegree`. */
18
+ export type KinshipLabel = "self" | "immediate" | "close" | "extended" | "distant" | "unrelated";
19
+ /** Maximum BFS depth for kinship searches; beyond this entities are "unrelated". */
20
+ export declare const MAX_KINSHIP_DEPTH = 4;
21
+ /**
22
+ * Depth-decay factor for inherited renown.
23
+ * Each generation reduces the renown contribution by this fraction:
24
+ * depth 1 (parent) → q(0.50) × parent renown
25
+ * depth 2 (grandparent) → q(0.25) × grandparent renown
26
+ */
27
+ export declare const RENOWN_DEPTH_DECAY_Q: Q;
28
+ export declare function createLineageRegistry(): LineageRegistry;
29
+ /**
30
+ * Return the `LineageNode` for `entityId`, creating a root node (no parents,
31
+ * no children, no partners) if one does not yet exist.
32
+ */
33
+ export declare function getLineageNode(registry: LineageRegistry, entityId: number): LineageNode;
34
+ /**
35
+ * Register a birth: create a node for `childId` and link it to up to two parents.
36
+ * Parent nodes are created if they do not already exist.
37
+ * No-op if `childId` already has a node (idempotent).
38
+ */
39
+ export declare function recordBirth(registry: LineageRegistry, childId: number, parentAId: number, parentBId?: number): void;
40
+ /**
41
+ * Record a partnership between two entities.
42
+ * Partners are considered degree-1 kin (immediate).
43
+ * Idempotent: duplicate calls are safe.
44
+ */
45
+ export declare function recordPartnership(registry: LineageRegistry, entityAId: number, entityBId: number): void;
46
+ /** Return the parent IDs of `entityId` (0–2 elements). */
47
+ export declare function getParents(registry: LineageRegistry, entityId: number): number[];
48
+ /** Return the child IDs of `entityId`. */
49
+ export declare function getChildren(registry: LineageRegistry, entityId: number): number[];
50
+ /**
51
+ * Return the sibling IDs of `entityId` — entities that share at least one parent,
52
+ * excluding `entityId` itself.
53
+ */
54
+ export declare function getSiblings(registry: LineageRegistry, entityId: number): number[];
55
+ /**
56
+ * Return all ancestors of `entityId` within `maxDepth` generations.
57
+ * Uses BFS upward through parent links only.
58
+ */
59
+ export declare function findAncestors(registry: LineageRegistry, entityId: number, maxDepth?: number): Set<number>;
60
+ /**
61
+ * Compute the degree of kinship between two entities via BFS on the undirected
62
+ * family graph (parents, children, and partners are all degree-1 neighbours).
63
+ *
64
+ * Returns:
65
+ * - `0` if `entityA === entityB`
66
+ * - `1`–`MAX_KINSHIP_DEPTH` for kin within range
67
+ * - `null` if no path exists within `MAX_KINSHIP_DEPTH`
68
+ */
69
+ export declare function computeKinshipDegree(registry: LineageRegistry, entityA: number, entityB: number): number | null;
70
+ /** Whether two entities are kin within `maxDegree` (default `MAX_KINSHIP_DEPTH`). */
71
+ export declare function isKin(registry: LineageRegistry, entityA: number, entityB: number, maxDegree?: number): boolean;
72
+ /**
73
+ * Map a numeric kinship degree (or `null`) to a `KinshipLabel`.
74
+ *
75
+ * @param degree Result of `computeKinshipDegree`; pass `null` for unrelated.
76
+ */
77
+ export declare function getKinshipLabel(degree: number | null): KinshipLabel;
78
+ /**
79
+ * Compute the renown bonus an entity inherits from their ancestors.
80
+ *
81
+ * For each ancestor at depth d, contribution = `ancestor.renown_Q × decay^d`
82
+ * where `decay = RENOWN_DEPTH_DECAY_Q / SCALE.Q` (default 0.5 per generation).
83
+ * The sum is clamped to `[0, SCALE.Q]`.
84
+ *
85
+ * Entities with no renown records or no ancestors return 0.
86
+ *
87
+ * @param registry Lineage registry.
88
+ * @param entityId Entity whose ancestors are being summed.
89
+ * @param renownRegistry Phase 75 renown registry.
90
+ * @param maxDepth How many generations to look back (default 3).
91
+ */
92
+ export declare function computeInheritedRenown(lineage: LineageRegistry, entityId: number, renownRegistry: RenownRegistry, maxDepth?: number): Q;
@@ -0,0 +1,234 @@
1
+ // src/kinship.ts — Phase 76: Kinship & Lineage
2
+ //
3
+ // Tracks parent-child-partner links for entities and provides ancestry queries,
4
+ // degree-of-kinship computation, and inherited renown from the Phase 75 system.
5
+ //
6
+ // Design:
7
+ // - Separate from Entity — the lineage graph lives in a `LineageRegistry`,
8
+ // not in entity fields, so no kernel changes are required.
9
+ // - `computeKinshipDegree` uses BFS over the undirected family graph (parents,
10
+ // children, partners counted at degree 1).
11
+ // - `inheritedRenown` sums ancestor renown_Q with geometric depth-decay so that
12
+ // a mythic grandparent grants a modest but real reputation bonus.
13
+ // - Deterministic: no Math.random(); only pure data queries.
14
+ import { q, SCALE, clampQ } from "./units.js";
15
+ /** Maximum BFS depth for kinship searches; beyond this entities are "unrelated". */
16
+ export const MAX_KINSHIP_DEPTH = 4;
17
+ /**
18
+ * Depth-decay factor for inherited renown.
19
+ * Each generation reduces the renown contribution by this fraction:
20
+ * depth 1 (parent) → q(0.50) × parent renown
21
+ * depth 2 (grandparent) → q(0.25) × grandparent renown
22
+ */
23
+ export const RENOWN_DEPTH_DECAY_Q = q(0.50);
24
+ // ── Factory ───────────────────────────────────────────────────────────────────
25
+ export function createLineageRegistry() {
26
+ return { nodes: new Map() };
27
+ }
28
+ // ── Node access ───────────────────────────────────────────────────────────────
29
+ /**
30
+ * Return the `LineageNode` for `entityId`, creating a root node (no parents,
31
+ * no children, no partners) if one does not yet exist.
32
+ */
33
+ export function getLineageNode(registry, entityId) {
34
+ let node = registry.nodes.get(entityId);
35
+ if (!node) {
36
+ node = { entityId, parentIds: [], childIds: [], partnerIds: [] };
37
+ registry.nodes.set(entityId, node);
38
+ }
39
+ return node;
40
+ }
41
+ // ── Mutation helpers ──────────────────────────────────────────────────────────
42
+ /**
43
+ * Register a birth: create a node for `childId` and link it to up to two parents.
44
+ * Parent nodes are created if they do not already exist.
45
+ * No-op if `childId` already has a node (idempotent).
46
+ */
47
+ export function recordBirth(registry, childId, parentAId, parentBId) {
48
+ // Ensure child node exists with the given parents
49
+ const existing = registry.nodes.get(childId);
50
+ if (!existing) {
51
+ const parentIds = parentBId != null
52
+ ? [parentAId, parentBId]
53
+ : [parentAId];
54
+ registry.nodes.set(childId, { entityId: childId, parentIds, childIds: [], partnerIds: [] });
55
+ }
56
+ // Ensure parent nodes exist and include childId
57
+ const nodeA = getLineageNode(registry, parentAId);
58
+ if (!nodeA.childIds.includes(childId))
59
+ nodeA.childIds.push(childId);
60
+ if (parentBId != null) {
61
+ const nodeB = getLineageNode(registry, parentBId);
62
+ if (!nodeB.childIds.includes(childId))
63
+ nodeB.childIds.push(childId);
64
+ }
65
+ }
66
+ /**
67
+ * Record a partnership between two entities.
68
+ * Partners are considered degree-1 kin (immediate).
69
+ * Idempotent: duplicate calls are safe.
70
+ */
71
+ export function recordPartnership(registry, entityAId, entityBId) {
72
+ const nodeA = getLineageNode(registry, entityAId);
73
+ const nodeB = getLineageNode(registry, entityBId);
74
+ if (!nodeA.partnerIds.includes(entityBId))
75
+ nodeA.partnerIds.push(entityBId);
76
+ if (!nodeB.partnerIds.includes(entityAId))
77
+ nodeB.partnerIds.push(entityAId);
78
+ }
79
+ // ── Family queries ────────────────────────────────────────────────────────────
80
+ /** Return the parent IDs of `entityId` (0–2 elements). */
81
+ export function getParents(registry, entityId) {
82
+ return registry.nodes.get(entityId)?.parentIds ?? [];
83
+ }
84
+ /** Return the child IDs of `entityId`. */
85
+ export function getChildren(registry, entityId) {
86
+ return registry.nodes.get(entityId)?.childIds ?? [];
87
+ }
88
+ /**
89
+ * Return the sibling IDs of `entityId` — entities that share at least one parent,
90
+ * excluding `entityId` itself.
91
+ */
92
+ export function getSiblings(registry, entityId) {
93
+ const parents = getParents(registry, entityId);
94
+ const result = new Set();
95
+ for (const pid of parents) {
96
+ for (const sibId of getChildren(registry, pid)) {
97
+ if (sibId !== entityId)
98
+ result.add(sibId);
99
+ }
100
+ }
101
+ return [...result];
102
+ }
103
+ /**
104
+ * Return all ancestors of `entityId` within `maxDepth` generations.
105
+ * Uses BFS upward through parent links only.
106
+ */
107
+ export function findAncestors(registry, entityId, maxDepth = MAX_KINSHIP_DEPTH) {
108
+ const ancestors = new Set();
109
+ const queue = [{ id: entityId, depth: 0 }];
110
+ while (queue.length > 0) {
111
+ const item = queue.shift();
112
+ if (item.depth >= maxDepth)
113
+ continue;
114
+ for (const pid of getParents(registry, item.id)) {
115
+ if (!ancestors.has(pid)) {
116
+ ancestors.add(pid);
117
+ queue.push({ id: pid, depth: item.depth + 1 });
118
+ }
119
+ }
120
+ }
121
+ return ancestors;
122
+ }
123
+ /**
124
+ * Compute the degree of kinship between two entities via BFS on the undirected
125
+ * family graph (parents, children, and partners are all degree-1 neighbours).
126
+ *
127
+ * Returns:
128
+ * - `0` if `entityA === entityB`
129
+ * - `1`–`MAX_KINSHIP_DEPTH` for kin within range
130
+ * - `null` if no path exists within `MAX_KINSHIP_DEPTH`
131
+ */
132
+ export function computeKinshipDegree(registry, entityA, entityB) {
133
+ if (entityA === entityB)
134
+ return 0;
135
+ const visited = new Set([entityA]);
136
+ const queue = [{ id: entityA, depth: 0 }];
137
+ while (queue.length > 0) {
138
+ const item = queue.shift();
139
+ if (item.depth >= MAX_KINSHIP_DEPTH)
140
+ continue;
141
+ const node = registry.nodes.get(item.id);
142
+ if (!node)
143
+ continue;
144
+ // BFS over parents + children + partners (undirected)
145
+ const neighbours = [
146
+ ...node.parentIds,
147
+ ...node.childIds,
148
+ ...node.partnerIds,
149
+ ];
150
+ for (const nbr of neighbours) {
151
+ if (nbr === entityB)
152
+ return item.depth + 1;
153
+ if (!visited.has(nbr)) {
154
+ visited.add(nbr);
155
+ queue.push({ id: nbr, depth: item.depth + 1 });
156
+ }
157
+ }
158
+ }
159
+ return null; // unrelated within MAX_KINSHIP_DEPTH
160
+ }
161
+ /** Whether two entities are kin within `maxDegree` (default `MAX_KINSHIP_DEPTH`). */
162
+ export function isKin(registry, entityA, entityB, maxDegree = MAX_KINSHIP_DEPTH) {
163
+ const degree = computeKinshipDegree(registry, entityA, entityB);
164
+ return degree !== null && degree <= maxDegree;
165
+ }
166
+ // ── Label ─────────────────────────────────────────────────────────────────────
167
+ /**
168
+ * Map a numeric kinship degree (or `null`) to a `KinshipLabel`.
169
+ *
170
+ * @param degree Result of `computeKinshipDegree`; pass `null` for unrelated.
171
+ */
172
+ export function getKinshipLabel(degree) {
173
+ if (degree === null)
174
+ return "unrelated";
175
+ if (degree === 0)
176
+ return "self";
177
+ if (degree === 1)
178
+ return "immediate";
179
+ if (degree === 2)
180
+ return "close";
181
+ if (degree === 3)
182
+ return "extended";
183
+ if (degree <= MAX_KINSHIP_DEPTH)
184
+ return "distant";
185
+ return "unrelated";
186
+ }
187
+ // ── Inherited renown ──────────────────────────────────────────────────────────
188
+ /**
189
+ * Compute the renown bonus an entity inherits from their ancestors.
190
+ *
191
+ * For each ancestor at depth d, contribution = `ancestor.renown_Q × decay^d`
192
+ * where `decay = RENOWN_DEPTH_DECAY_Q / SCALE.Q` (default 0.5 per generation).
193
+ * The sum is clamped to `[0, SCALE.Q]`.
194
+ *
195
+ * Entities with no renown records or no ancestors return 0.
196
+ *
197
+ * @param registry Lineage registry.
198
+ * @param entityId Entity whose ancestors are being summed.
199
+ * @param renownRegistry Phase 75 renown registry.
200
+ * @param maxDepth How many generations to look back (default 3).
201
+ */
202
+ export function computeInheritedRenown(lineage, entityId, renownRegistry, maxDepth = 3) {
203
+ // BFS upward through parent links only (not children/partners)
204
+ let total = 0;
205
+ const visited = new Set([entityId]);
206
+ const queue = [];
207
+ for (const pid of getParents(lineage, entityId)) {
208
+ if (!visited.has(pid)) {
209
+ visited.add(pid);
210
+ queue.push({ id: pid, depth: 1 });
211
+ }
212
+ }
213
+ while (queue.length > 0) {
214
+ const item = queue.shift();
215
+ if (item.depth > maxDepth)
216
+ continue;
217
+ const record = renownRegistry.records.get(item.id);
218
+ if (record && record.renown_Q > 0) {
219
+ // decay^depth applied via repeated integer multiplication
220
+ let contribution = record.renown_Q;
221
+ for (let i = 0; i < item.depth; i++) {
222
+ contribution = Math.round(contribution * RENOWN_DEPTH_DECAY_Q / SCALE.Q);
223
+ }
224
+ total += contribution;
225
+ }
226
+ for (const pid of getParents(lineage, item.id)) {
227
+ if (!visited.has(pid)) {
228
+ visited.add(pid);
229
+ queue.push({ id: pid, depth: item.depth + 1 });
230
+ }
231
+ }
232
+ }
233
+ return clampQ(total, 0, SCALE.Q);
234
+ }
@@ -0,0 +1,58 @@
1
+ import type { ChronicleEntry } from "./chronicle.js";
2
+ import type { Chronicle } from "./chronicle.js";
3
+ import type { CultureProfile } from "./culture.js";
4
+ import type { MythArchetype, Myth } from "./mythology.js";
5
+ /**
6
+ * Voice tone used when rendering chronicle entries.
7
+ * Derived from the dominant cultural values of the originating polity.
8
+ */
9
+ export type ProseTone = "neutral" | "heroic" | "tragic" | "martial" | "spiritual" | "mercantile";
10
+ /**
11
+ * Context bundle for a tone-aware rendering pass.
12
+ * Created by `createNarrativeContext` and passed to render functions.
13
+ */
14
+ export interface NarrativeContext {
15
+ /** Map of entity id → display name. Missing ids fall back to "entity {id}". */
16
+ entityNames: Map<number, string>;
17
+ /** Prose tone for this rendering pass. */
18
+ tone: ProseTone;
19
+ /**
20
+ * Optional myth-archetype closing phrase appended to each rendered sentence.
21
+ * Produced by `mythArchetypeFrame(archetype)`.
22
+ */
23
+ mythFrame?: string;
24
+ }
25
+ /** Returns a closing phrase appropriate to the myth archetype. */
26
+ export declare function mythArchetypeFrame(archetype: MythArchetype): string;
27
+ /**
28
+ * Derive the best matching `ProseTone` from a `CultureProfile`.
29
+ *
30
+ * Uses the top-ranked cultural value; falls back to `"neutral"` for values
31
+ * without a direct tone mapping (hospitality, hierarchy, innovation, etc.).
32
+ */
33
+ export declare function deriveNarrativeTone(culture: CultureProfile): ProseTone;
34
+ /**
35
+ * Create a `NarrativeContext` for a rendering pass.
36
+ *
37
+ * @param entityNames Map of entity id → display name (numeric ids are looked up here).
38
+ * @param culture Optional culture profile — used to derive tone automatically.
39
+ * @param myth Optional myth — used to append archetype-framing suffix.
40
+ */
41
+ export declare function createNarrativeContext(entityNames: Map<number, string>, culture?: CultureProfile, myth?: Myth): NarrativeContext;
42
+ /**
43
+ * Render a single `ChronicleEntry` with cultural-tone awareness.
44
+ *
45
+ * Selects the tone variant for `entry.eventType`; falls back to `"neutral"` if
46
+ * the requested tone has no specific variant. Appends `ctx.mythFrame` if set.
47
+ *
48
+ * Does NOT mutate `entry.rendered` — call `entry.rendered = renderEntryWithTone(...)`
49
+ * manually if caching is desired.
50
+ */
51
+ export declare function renderEntryWithTone(entry: ChronicleEntry, ctx: NarrativeContext): string;
52
+ /**
53
+ * Render all entries in a `Chronicle` above `minSignificance` (default 50),
54
+ * returned in chronological order.
55
+ *
56
+ * Uses `renderEntryWithTone` for each entry.
57
+ */
58
+ export declare function renderChronicleWithTone(chronicle: Chronicle, ctx: NarrativeContext, minSignificance?: number): string[];