@its-not-rocket-science/ananke 0.1.28 → 0.1.29
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 +23 -0
- package/dist/src/faith.d.ts +123 -0
- package/dist/src/faith.js +221 -0
- package/dist/src/siege.d.ts +112 -0
- package/dist/src/siege.js +176 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.29] — 2026-03-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Phase 84 · Siege Warfare** (`src/siege.ts`)
|
|
14
|
+
- `SiegePhase`: `"investment" | "active" | "resolved"`.
|
|
15
|
+
- `SiegeOutcome`: `"attacker_victory" | "defender_holds" | "surrender"`.
|
|
16
|
+
- `SiegeState { siegeId, attackerPolityId, defenderPolityId, phase, startTick, phaseDay, wallIntegrity_Q, supplyLevel_Q, defenderMorale_Q, siegeStrength_Q, outcome? }`.
|
|
17
|
+
- `SiegeAttrition { attackerLoss_Q, defenderLoss_Q }` — daily fractional losses per phase.
|
|
18
|
+
- `createSiege(attackerPolity, defenderPolity, tick?)` — seeds from `militaryStrength_Q` and `stabilityQ`.
|
|
19
|
+
- **Investment phase** (`INVESTMENT_DAYS = 14`): encirclement; no bombardment or starvation yet.
|
|
20
|
+
- **Active phase**: wall decay = `siegeStrength_Q × WALL_DECAY_BASE_Q / SCALE.Q` per day; supply drains at `SUPPLY_DRAIN_PER_DAY_Q = q(0.004)`; morale tracks combined wall/supply weakness.
|
|
21
|
+
- **Assault**: fires when `wallIntegrity_Q < ASSAULT_WALL_THRESHOLD_Q = q(0.30)`; resolved by `eventSeed` roll weighted by siege strength and defender morale deficit.
|
|
22
|
+
- **Surrender**: fires when `supplyLevel_Q ≤ SURRENDER_SUPPLY_THRESHOLD_Q = q(0.05)` and daily probabilistic roll succeeds based on morale deficit.
|
|
23
|
+
- `stepSiege(siege, worldSeed, tick, supplyPressureBonus_Q?, siegeStrengthMul_Q?)` — Phase-83 (severed trade) and Phase-78 (winter penalty) integration via optional parameters.
|
|
24
|
+
- `computeSiegeAttrition(siege)` → `SiegeAttrition` — daily losses by phase.
|
|
25
|
+
- `runSiegeToResolution(siege, worldSeed, startTick, maxDays?)` — convenience runner.
|
|
26
|
+
- All outcomes deterministic and idempotent via `eventSeed`.
|
|
27
|
+
- Added `./siege` subpath export to `package.json`.
|
|
28
|
+
- 38 new tests; 4,465 total. Coverage maintained above all thresholds.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
9
32
|
## [0.1.28] — 2026-03-26
|
|
10
33
|
|
|
11
34
|
### Added
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Q } from "./units.js";
|
|
2
|
+
export type FaithId = string;
|
|
3
|
+
/** Definition of a named faith. */
|
|
4
|
+
export interface Faith {
|
|
5
|
+
faithId: FaithId;
|
|
6
|
+
name: string;
|
|
7
|
+
/**
|
|
8
|
+
* Proselytising energy [0, SCALE.Q].
|
|
9
|
+
* Higher fervor → faster conversion spread into other polities.
|
|
10
|
+
*/
|
|
11
|
+
fervor_Q: Q;
|
|
12
|
+
/**
|
|
13
|
+
* Tolerance for other faiths [0, SCALE.Q].
|
|
14
|
+
* Low tolerance → higher heresy risk when minority faiths are present.
|
|
15
|
+
*/
|
|
16
|
+
tolerance_Q: Q;
|
|
17
|
+
/**
|
|
18
|
+
* Exclusive faiths (monotheistic) compete: gaining adherents displaces
|
|
19
|
+
* other exclusive faiths proportionally.
|
|
20
|
+
* Syncretic faiths are additive — populations can hold multiple.
|
|
21
|
+
*/
|
|
22
|
+
exclusive: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Presence of one faith within a polity. */
|
|
25
|
+
export interface PolityFaith {
|
|
26
|
+
polityId: string;
|
|
27
|
+
faithId: FaithId;
|
|
28
|
+
/** Fraction of population following this faith [0, SCALE.Q]. */
|
|
29
|
+
adherents_Q: Q;
|
|
30
|
+
}
|
|
31
|
+
/** Central registry: faith definitions + per-polity adherent records. */
|
|
32
|
+
export interface FaithRegistry {
|
|
33
|
+
/** All defined faiths keyed by faithId. */
|
|
34
|
+
faiths: Map<FaithId, Faith>;
|
|
35
|
+
/** polityId → array of PolityFaith (one entry per faith present). */
|
|
36
|
+
polityFaiths: Map<string, PolityFaith[]>;
|
|
37
|
+
}
|
|
38
|
+
/** High-fervor monotheistic faith. */
|
|
39
|
+
export declare const SOLAR_CHURCH: Faith;
|
|
40
|
+
/** Low-fervor animistic syncretic faith. */
|
|
41
|
+
export declare const EARTH_SPIRITS: Faith;
|
|
42
|
+
/** Moderate syncretic merchant cult. */
|
|
43
|
+
export declare const MERCHANT_CULT: Faith;
|
|
44
|
+
/**
|
|
45
|
+
* Base daily conversion delta at full missionary presence and full source fervor.
|
|
46
|
+
* Actual delta = `fervor_Q × missionaryPresence_Q × CONVERSION_BASE_RATE_Q / SCALE.Q²`.
|
|
47
|
+
*/
|
|
48
|
+
export declare const CONVERSION_BASE_RATE_Q: Q;
|
|
49
|
+
/**
|
|
50
|
+
* Minority exclusive faith presence above this fraction → heresy risk fires.
|
|
51
|
+
* `computeHeresyRisk` returns non-zero only when a minority exclusive faith
|
|
52
|
+
* exceeds this threshold in a polity whose dominant faith has low tolerance.
|
|
53
|
+
*/
|
|
54
|
+
export declare const HERESY_THRESHOLD_Q: Q;
|
|
55
|
+
/** Diplomatic bonus (Q offset) when two polities share the same dominant faith. */
|
|
56
|
+
export declare const FAITH_DIPLOMATIC_BONUS_Q: Q;
|
|
57
|
+
/** Diplomatic penalty when polities hold exclusive faiths that conflict. */
|
|
58
|
+
export declare const FAITH_DIPLOMATIC_PENALTY_Q: Q;
|
|
59
|
+
export declare function createFaithRegistry(): FaithRegistry;
|
|
60
|
+
/** Register or replace a faith definition. */
|
|
61
|
+
export declare function registerFaith(registry: FaithRegistry, faith: Faith): void;
|
|
62
|
+
/** Return the faith definition, or `undefined` if unknown. */
|
|
63
|
+
export declare function getFaith(registry: FaithRegistry, faithId: FaithId): Faith | undefined;
|
|
64
|
+
/** Return all faith records for a polity (empty array if none). */
|
|
65
|
+
export declare function getPolityFaiths(registry: FaithRegistry, polityId: string): PolityFaith[];
|
|
66
|
+
/**
|
|
67
|
+
* Set the adherent fraction for a faith in a polity.
|
|
68
|
+
* Creates the record if it does not exist; updates it if it does.
|
|
69
|
+
* Clamps `adherents_Q` to [0, SCALE.Q].
|
|
70
|
+
* Does NOT normalise other faiths — call `normalisePolitFaiths` if needed.
|
|
71
|
+
*/
|
|
72
|
+
export declare function setPolityFaith(registry: FaithRegistry, polityId: string, faithId: FaithId, adherents_Q: Q): PolityFaith;
|
|
73
|
+
/** Return the faith with the highest adherents in a polity, or `undefined`. */
|
|
74
|
+
export declare function getDominantFaith(registry: FaithRegistry, polityId: string): PolityFaith | undefined;
|
|
75
|
+
/** Return `true` if both polities share the same dominant faithId. */
|
|
76
|
+
export declare function sharesDominantFaith(registry: FaithRegistry, polityAId: string, polityBId: string): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Compute the daily conversion pressure exerted on a target polity by a
|
|
79
|
+
* source faith's missionaries.
|
|
80
|
+
*
|
|
81
|
+
* Formula:
|
|
82
|
+
* pressure = fervor_Q × missionaryPresence_Q × CONVERSION_BASE_RATE_Q / SCALE.Q²
|
|
83
|
+
*
|
|
84
|
+
* Returns 0 if the faith is not registered.
|
|
85
|
+
*
|
|
86
|
+
* @param missionaryPresence_Q Strength of missionary activity [0, SCALE.Q].
|
|
87
|
+
* Callers may derive this from Phase-82 agent presence
|
|
88
|
+
* or Phase-83 trade route volume.
|
|
89
|
+
*/
|
|
90
|
+
export declare function computeConversionPressure(faith: Faith, missionaryPresence_Q: Q): Q;
|
|
91
|
+
/**
|
|
92
|
+
* Apply a conversion delta to a polity.
|
|
93
|
+
*
|
|
94
|
+
* **Exclusive faiths**: gaining `delta_Q` adherents displaces all other
|
|
95
|
+
* *exclusive* faiths proportionally, preserving their relative sizes.
|
|
96
|
+
* Non-exclusive faiths in the polity are unaffected.
|
|
97
|
+
*
|
|
98
|
+
* **Syncretic faiths**: delta is added directly; no displacement occurs.
|
|
99
|
+
*
|
|
100
|
+
* All adherent_Q values are clamped to [0, SCALE.Q] after adjustment.
|
|
101
|
+
*/
|
|
102
|
+
export declare function stepFaithConversion(registry: FaithRegistry, polityId: string, faithId: FaithId, delta_Q: Q): void;
|
|
103
|
+
/**
|
|
104
|
+
* Compute the heresy risk in a polity [0, SCALE.Q].
|
|
105
|
+
*
|
|
106
|
+
* Risk is non-zero when:
|
|
107
|
+
* - The dominant faith is exclusive and has low tolerance.
|
|
108
|
+
* - A minority exclusive faith exceeds `HERESY_THRESHOLD_Q`.
|
|
109
|
+
*
|
|
110
|
+
* Formula: `(minorityPresence - HERESY_THRESHOLD) × (SCALE.Q - tolerance) / SCALE.Q`
|
|
111
|
+
* summed over all qualifying minority faiths.
|
|
112
|
+
*/
|
|
113
|
+
export declare function computeHeresyRisk(registry: FaithRegistry, polityId: string): Q;
|
|
114
|
+
/**
|
|
115
|
+
* Compute a signed Q diplomatic modifier from faith compatibility.
|
|
116
|
+
*
|
|
117
|
+
* - Shared dominant faith → `+FAITH_DIPLOMATIC_BONUS_Q`.
|
|
118
|
+
* - Both polities have exclusive dominant faiths that differ → `−FAITH_DIPLOMATIC_PENALTY_Q`.
|
|
119
|
+
* - Otherwise (syncretic or no dominant faith) → `0`.
|
|
120
|
+
*
|
|
121
|
+
* Hosts add this to treaty strength or faction standing adjustments.
|
|
122
|
+
*/
|
|
123
|
+
export declare function computeFaithDiplomaticModifier(registry: FaithRegistry, polityAId: string, polityBId: string): number;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// src/faith.ts — Phase 85: Religion & Faith Systems
|
|
2
|
+
//
|
|
3
|
+
// Models named faiths and their presence in polities. Faith presence is
|
|
4
|
+
// expressed as a Q fraction of the polity population. Exclusive faiths
|
|
5
|
+
// (monotheistic) compete with each other; syncretic faiths stack additively.
|
|
6
|
+
//
|
|
7
|
+
// Design:
|
|
8
|
+
// - Pure data layer — no Entity fields, no kernel changes.
|
|
9
|
+
// - `FaithRegistry` stores faith definitions and per-polity adherent fractions.
|
|
10
|
+
// - Conversion pressure integrates with Phase-81 (migration drives faith spread)
|
|
11
|
+
// and Phase-80 (shared faith boosts treaty strength) via caller-supplied context.
|
|
12
|
+
// - Heresy risk integrates with Phase-82 (espionage can incite religious unrest).
|
|
13
|
+
import { q, SCALE, clampQ, mulDiv } from "./units.js";
|
|
14
|
+
// ── Built-in sample faiths ─────────────────────────────────────────────────────
|
|
15
|
+
/** High-fervor monotheistic faith. */
|
|
16
|
+
export const SOLAR_CHURCH = {
|
|
17
|
+
faithId: "solar_church",
|
|
18
|
+
name: "The Solar Church",
|
|
19
|
+
fervor_Q: q(0.80),
|
|
20
|
+
tolerance_Q: q(0.20),
|
|
21
|
+
exclusive: true,
|
|
22
|
+
};
|
|
23
|
+
/** Low-fervor animistic syncretic faith. */
|
|
24
|
+
export const EARTH_SPIRITS = {
|
|
25
|
+
faithId: "earth_spirits",
|
|
26
|
+
name: "Earth Spirits",
|
|
27
|
+
fervor_Q: q(0.30),
|
|
28
|
+
tolerance_Q: q(0.90),
|
|
29
|
+
exclusive: false,
|
|
30
|
+
};
|
|
31
|
+
/** Moderate syncretic merchant cult. */
|
|
32
|
+
export const MERCHANT_CULT = {
|
|
33
|
+
faithId: "merchant_cult",
|
|
34
|
+
name: "Merchant Cult",
|
|
35
|
+
fervor_Q: q(0.50),
|
|
36
|
+
tolerance_Q: q(0.70),
|
|
37
|
+
exclusive: false,
|
|
38
|
+
};
|
|
39
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Base daily conversion delta at full missionary presence and full source fervor.
|
|
42
|
+
* Actual delta = `fervor_Q × missionaryPresence_Q × CONVERSION_BASE_RATE_Q / SCALE.Q²`.
|
|
43
|
+
*/
|
|
44
|
+
export const CONVERSION_BASE_RATE_Q = q(0.002);
|
|
45
|
+
/**
|
|
46
|
+
* Minority exclusive faith presence above this fraction → heresy risk fires.
|
|
47
|
+
* `computeHeresyRisk` returns non-zero only when a minority exclusive faith
|
|
48
|
+
* exceeds this threshold in a polity whose dominant faith has low tolerance.
|
|
49
|
+
*/
|
|
50
|
+
export const HERESY_THRESHOLD_Q = q(0.15);
|
|
51
|
+
/** Diplomatic bonus (Q offset) when two polities share the same dominant faith. */
|
|
52
|
+
export const FAITH_DIPLOMATIC_BONUS_Q = q(0.10);
|
|
53
|
+
/** Diplomatic penalty when polities hold exclusive faiths that conflict. */
|
|
54
|
+
export const FAITH_DIPLOMATIC_PENALTY_Q = q(0.10);
|
|
55
|
+
// ── Factory ───────────────────────────────────────────────────────────────────
|
|
56
|
+
export function createFaithRegistry() {
|
|
57
|
+
return { faiths: new Map(), polityFaiths: new Map() };
|
|
58
|
+
}
|
|
59
|
+
// ── Faith management ───────────────────────────────────────────────────────────
|
|
60
|
+
/** Register or replace a faith definition. */
|
|
61
|
+
export function registerFaith(registry, faith) {
|
|
62
|
+
registry.faiths.set(faith.faithId, faith);
|
|
63
|
+
}
|
|
64
|
+
/** Return the faith definition, or `undefined` if unknown. */
|
|
65
|
+
export function getFaith(registry, faithId) {
|
|
66
|
+
return registry.faiths.get(faithId);
|
|
67
|
+
}
|
|
68
|
+
// ── Polity faith records ───────────────────────────────────────────────────────
|
|
69
|
+
/** Return all faith records for a polity (empty array if none). */
|
|
70
|
+
export function getPolityFaiths(registry, polityId) {
|
|
71
|
+
return registry.polityFaiths.get(polityId) ?? [];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Set the adherent fraction for a faith in a polity.
|
|
75
|
+
* Creates the record if it does not exist; updates it if it does.
|
|
76
|
+
* Clamps `adherents_Q` to [0, SCALE.Q].
|
|
77
|
+
* Does NOT normalise other faiths — call `normalisePolitFaiths` if needed.
|
|
78
|
+
*/
|
|
79
|
+
export function setPolityFaith(registry, polityId, faithId, adherents_Q) {
|
|
80
|
+
const clamped = clampQ(adherents_Q, 0, SCALE.Q);
|
|
81
|
+
let list = registry.polityFaiths.get(polityId);
|
|
82
|
+
if (!list) {
|
|
83
|
+
list = [];
|
|
84
|
+
registry.polityFaiths.set(polityId, list);
|
|
85
|
+
}
|
|
86
|
+
const existing = list.find(pf => pf.faithId === faithId);
|
|
87
|
+
if (existing) {
|
|
88
|
+
existing.adherents_Q = clamped;
|
|
89
|
+
return existing;
|
|
90
|
+
}
|
|
91
|
+
const pf = { polityId, faithId, adherents_Q: clamped };
|
|
92
|
+
list.push(pf);
|
|
93
|
+
return pf;
|
|
94
|
+
}
|
|
95
|
+
/** Return the faith with the highest adherents in a polity, or `undefined`. */
|
|
96
|
+
export function getDominantFaith(registry, polityId) {
|
|
97
|
+
const list = getPolityFaiths(registry, polityId);
|
|
98
|
+
if (list.length === 0)
|
|
99
|
+
return undefined;
|
|
100
|
+
return list.reduce((best, pf) => pf.adherents_Q > best.adherents_Q ? pf : best);
|
|
101
|
+
}
|
|
102
|
+
/** Return `true` if both polities share the same dominant faithId. */
|
|
103
|
+
export function sharesDominantFaith(registry, polityAId, polityBId) {
|
|
104
|
+
const a = getDominantFaith(registry, polityAId);
|
|
105
|
+
const b = getDominantFaith(registry, polityBId);
|
|
106
|
+
return a != null && b != null && a.faithId === b.faithId;
|
|
107
|
+
}
|
|
108
|
+
// ── Conversion mechanics ───────────────────────────────────────────────────────
|
|
109
|
+
/**
|
|
110
|
+
* Compute the daily conversion pressure exerted on a target polity by a
|
|
111
|
+
* source faith's missionaries.
|
|
112
|
+
*
|
|
113
|
+
* Formula:
|
|
114
|
+
* pressure = fervor_Q × missionaryPresence_Q × CONVERSION_BASE_RATE_Q / SCALE.Q²
|
|
115
|
+
*
|
|
116
|
+
* Returns 0 if the faith is not registered.
|
|
117
|
+
*
|
|
118
|
+
* @param missionaryPresence_Q Strength of missionary activity [0, SCALE.Q].
|
|
119
|
+
* Callers may derive this from Phase-82 agent presence
|
|
120
|
+
* or Phase-83 trade route volume.
|
|
121
|
+
*/
|
|
122
|
+
export function computeConversionPressure(faith, missionaryPresence_Q) {
|
|
123
|
+
const step1 = mulDiv(faith.fervor_Q, missionaryPresence_Q, SCALE.Q);
|
|
124
|
+
return clampQ(mulDiv(step1, CONVERSION_BASE_RATE_Q, SCALE.Q), 0, SCALE.Q);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Apply a conversion delta to a polity.
|
|
128
|
+
*
|
|
129
|
+
* **Exclusive faiths**: gaining `delta_Q` adherents displaces all other
|
|
130
|
+
* *exclusive* faiths proportionally, preserving their relative sizes.
|
|
131
|
+
* Non-exclusive faiths in the polity are unaffected.
|
|
132
|
+
*
|
|
133
|
+
* **Syncretic faiths**: delta is added directly; no displacement occurs.
|
|
134
|
+
*
|
|
135
|
+
* All adherent_Q values are clamped to [0, SCALE.Q] after adjustment.
|
|
136
|
+
*/
|
|
137
|
+
export function stepFaithConversion(registry, polityId, faithId, delta_Q) {
|
|
138
|
+
if (delta_Q === 0)
|
|
139
|
+
return;
|
|
140
|
+
const faith = registry.faiths.get(faithId);
|
|
141
|
+
const list = getPolityFaiths(registry, polityId);
|
|
142
|
+
// Ensure target record exists
|
|
143
|
+
let target = list.find(pf => pf.faithId === faithId);
|
|
144
|
+
if (!target) {
|
|
145
|
+
target = { polityId, faithId, adherents_Q: 0 };
|
|
146
|
+
list.push(target);
|
|
147
|
+
if (!registry.polityFaiths.has(polityId))
|
|
148
|
+
registry.polityFaiths.set(polityId, list);
|
|
149
|
+
}
|
|
150
|
+
const newTarget = clampQ(target.adherents_Q + delta_Q, 0, SCALE.Q);
|
|
151
|
+
const actualDelta = newTarget - target.adherents_Q;
|
|
152
|
+
target.adherents_Q = newTarget;
|
|
153
|
+
// Displace other exclusive faiths proportionally
|
|
154
|
+
if (faith?.exclusive && actualDelta > 0) {
|
|
155
|
+
const others = list.filter(pf => pf.faithId !== faithId && registry.faiths.get(pf.faithId)?.exclusive);
|
|
156
|
+
const totalOther = others.reduce((s, pf) => s + pf.adherents_Q, 0);
|
|
157
|
+
if (totalOther > 0) {
|
|
158
|
+
for (const other of others) {
|
|
159
|
+
const displaced = mulDiv(actualDelta, other.adherents_Q, totalOther);
|
|
160
|
+
other.adherents_Q = clampQ(other.adherents_Q - displaced, 0, SCALE.Q);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ── Heresy risk ────────────────────────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Compute the heresy risk in a polity [0, SCALE.Q].
|
|
168
|
+
*
|
|
169
|
+
* Risk is non-zero when:
|
|
170
|
+
* - The dominant faith is exclusive and has low tolerance.
|
|
171
|
+
* - A minority exclusive faith exceeds `HERESY_THRESHOLD_Q`.
|
|
172
|
+
*
|
|
173
|
+
* Formula: `(minorityPresence - HERESY_THRESHOLD) × (SCALE.Q - tolerance) / SCALE.Q`
|
|
174
|
+
* summed over all qualifying minority faiths.
|
|
175
|
+
*/
|
|
176
|
+
export function computeHeresyRisk(registry, polityId) {
|
|
177
|
+
const list = getPolityFaiths(registry, polityId);
|
|
178
|
+
const dominant = getDominantFaith(registry, polityId);
|
|
179
|
+
if (!dominant)
|
|
180
|
+
return 0;
|
|
181
|
+
const domFaith = registry.faiths.get(dominant.faithId);
|
|
182
|
+
if (!domFaith?.exclusive)
|
|
183
|
+
return 0; // syncretic dominants don't declare heresy
|
|
184
|
+
let risk = 0;
|
|
185
|
+
for (const pf of list) {
|
|
186
|
+
if (pf.faithId === dominant.faithId)
|
|
187
|
+
continue;
|
|
188
|
+
const minFaith = registry.faiths.get(pf.faithId);
|
|
189
|
+
if (!minFaith?.exclusive)
|
|
190
|
+
continue; // syncretic minorities are tolerated
|
|
191
|
+
if (pf.adherents_Q <= HERESY_THRESHOLD_Q)
|
|
192
|
+
continue;
|
|
193
|
+
const excess = pf.adherents_Q - HERESY_THRESHOLD_Q;
|
|
194
|
+
const intolerance = SCALE.Q - domFaith.tolerance_Q;
|
|
195
|
+
risk += mulDiv(excess, intolerance, SCALE.Q);
|
|
196
|
+
}
|
|
197
|
+
return clampQ(risk, 0, SCALE.Q);
|
|
198
|
+
}
|
|
199
|
+
// ── Diplomatic faith modifier ──────────────────────────────────────────────────
|
|
200
|
+
/**
|
|
201
|
+
* Compute a signed Q diplomatic modifier from faith compatibility.
|
|
202
|
+
*
|
|
203
|
+
* - Shared dominant faith → `+FAITH_DIPLOMATIC_BONUS_Q`.
|
|
204
|
+
* - Both polities have exclusive dominant faiths that differ → `−FAITH_DIPLOMATIC_PENALTY_Q`.
|
|
205
|
+
* - Otherwise (syncretic or no dominant faith) → `0`.
|
|
206
|
+
*
|
|
207
|
+
* Hosts add this to treaty strength or faction standing adjustments.
|
|
208
|
+
*/
|
|
209
|
+
export function computeFaithDiplomaticModifier(registry, polityAId, polityBId) {
|
|
210
|
+
const a = getDominantFaith(registry, polityAId);
|
|
211
|
+
const b = getDominantFaith(registry, polityBId);
|
|
212
|
+
if (!a || !b)
|
|
213
|
+
return 0;
|
|
214
|
+
if (a.faithId === b.faithId)
|
|
215
|
+
return FAITH_DIPLOMATIC_BONUS_Q;
|
|
216
|
+
const faithA = registry.faiths.get(a.faithId);
|
|
217
|
+
const faithB = registry.faiths.get(b.faithId);
|
|
218
|
+
if (faithA?.exclusive && faithB?.exclusive)
|
|
219
|
+
return -FAITH_DIPLOMATIC_PENALTY_Q;
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Polity } from "./polity.js";
|
|
2
|
+
import type { Q } from "./units.js";
|
|
3
|
+
/**
|
|
4
|
+
* Phase of the siege.
|
|
5
|
+
*
|
|
6
|
+
* - `"investment"` — attacker encircles; supply lines not yet fully cut; no bombardment.
|
|
7
|
+
* - `"active"` — bombardment + starvation running in parallel.
|
|
8
|
+
* - `"resolved"` — siege ended; `outcome` is set.
|
|
9
|
+
*/
|
|
10
|
+
export type SiegePhase = "investment" | "active" | "resolved";
|
|
11
|
+
/**
|
|
12
|
+
* How the siege ended.
|
|
13
|
+
*
|
|
14
|
+
* - `"attacker_victory"` — walls breached and assault succeeded.
|
|
15
|
+
* - `"defender_holds"` — assault repelled; walls partially repaired.
|
|
16
|
+
* - `"surrender"` — defender ran out of supply and capitulated.
|
|
17
|
+
*/
|
|
18
|
+
export type SiegeOutcome = "attacker_victory" | "defender_holds" | "surrender";
|
|
19
|
+
/** Live state of an ongoing or resolved siege. */
|
|
20
|
+
export interface SiegeState {
|
|
21
|
+
siegeId: string;
|
|
22
|
+
attackerPolityId: string;
|
|
23
|
+
defenderPolityId: string;
|
|
24
|
+
phase: SiegePhase;
|
|
25
|
+
/** Simulation tick (day) when the siege began. */
|
|
26
|
+
startTick: number;
|
|
27
|
+
/** Days elapsed in the current phase. */
|
|
28
|
+
phaseDay: number;
|
|
29
|
+
/** Defender fortification integrity [0, SCALE.Q]. Decays under bombardment. */
|
|
30
|
+
wallIntegrity_Q: Q;
|
|
31
|
+
/** Defender garrison supplies [0, SCALE.Q]. Drains each active day. */
|
|
32
|
+
supplyLevel_Q: Q;
|
|
33
|
+
/** Defender garrison morale [0, SCALE.Q]. Falls with supply and wall damage. */
|
|
34
|
+
defenderMorale_Q: Q;
|
|
35
|
+
/**
|
|
36
|
+
* Attacker siege capability [0, SCALE.Q].
|
|
37
|
+
* Derived from attacker `militaryStrength_Q`; governs wall-decay rate.
|
|
38
|
+
*/
|
|
39
|
+
siegeStrength_Q: Q;
|
|
40
|
+
/** Set when `phase === "resolved"`. */
|
|
41
|
+
outcome?: SiegeOutcome;
|
|
42
|
+
}
|
|
43
|
+
/** Result of advancing the siege by one day. */
|
|
44
|
+
export interface SiegeStepResult {
|
|
45
|
+
phaseChanged: boolean;
|
|
46
|
+
resolved: boolean;
|
|
47
|
+
outcome?: SiegeOutcome;
|
|
48
|
+
}
|
|
49
|
+
/** Daily attrition rates for both sides. */
|
|
50
|
+
export interface SiegeAttrition {
|
|
51
|
+
/** Fraction of attacker force lost per day [0, SCALE.Q]. */
|
|
52
|
+
attackerLoss_Q: Q;
|
|
53
|
+
/** Fraction of defender force lost per day [0, SCALE.Q]. */
|
|
54
|
+
defenderLoss_Q: Q;
|
|
55
|
+
}
|
|
56
|
+
/** Days spent in the investment phase before active bombardment/starvation begins. */
|
|
57
|
+
export declare const INVESTMENT_DAYS = 14;
|
|
58
|
+
/**
|
|
59
|
+
* Base wall decay per active day at maximum siege strength.
|
|
60
|
+
* Actual decay = `siegeStrength_Q × WALL_DECAY_BASE_Q / SCALE.Q`.
|
|
61
|
+
*/
|
|
62
|
+
export declare const WALL_DECAY_BASE_Q: Q;
|
|
63
|
+
/** Supply drain per active day (independent of attacker strength). */
|
|
64
|
+
export declare const SUPPLY_DRAIN_PER_DAY_Q: Q;
|
|
65
|
+
/** Rate at which defender morale decays relative to combined wall/supply weakness. */
|
|
66
|
+
export declare const MORALE_DECAY_RATE_Q: Q;
|
|
67
|
+
/** Wall integrity below this → assault is triggered and resolved. */
|
|
68
|
+
export declare const ASSAULT_WALL_THRESHOLD_Q: Q;
|
|
69
|
+
/**
|
|
70
|
+
* Base assault success probability at equal siege strength and full defender morale.
|
|
71
|
+
* Actual chance boosted by `(SCALE.Q - defenderMorale_Q) × 0.30`.
|
|
72
|
+
*/
|
|
73
|
+
export declare const ASSAULT_SUCCESS_BASE_Q: Q;
|
|
74
|
+
/** Supply below this → daily surrender check fires. */
|
|
75
|
+
export declare const SURRENDER_SUPPLY_THRESHOLD_Q: Q;
|
|
76
|
+
/**
|
|
77
|
+
* Create a new siege. `attackerPolity.militaryStrength_Q` sets siege strength;
|
|
78
|
+
* `defenderPolity.stabilityQ` seeds defender morale.
|
|
79
|
+
*/
|
|
80
|
+
export declare function createSiege(attackerPolity: Polity, defenderPolity: Polity, tick?: number): SiegeState;
|
|
81
|
+
/** Return `true` if the siege has ended. */
|
|
82
|
+
export declare function isSiegeResolved(siege: SiegeState): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Compute daily attrition fractions for both sides in the current phase.
|
|
85
|
+
* - Investment: minimal skirmishing losses.
|
|
86
|
+
* - Active: attacker takes defensive fire; defender takes bombardment damage.
|
|
87
|
+
* - Resolved: no attrition.
|
|
88
|
+
*/
|
|
89
|
+
export declare function computeSiegeAttrition(siege: SiegeState): SiegeAttrition;
|
|
90
|
+
/**
|
|
91
|
+
* Advance the siege by one simulated day.
|
|
92
|
+
*
|
|
93
|
+
* **Investment phase**: counts down `INVESTMENT_DAYS` then transitions to active.
|
|
94
|
+
*
|
|
95
|
+
* **Active phase** (each day):
|
|
96
|
+
* 1. Decay `wallIntegrity_Q` by `siegeStrength_Q × WALL_DECAY_BASE_Q / SCALE.Q`.
|
|
97
|
+
* 2. Drain `supplyLevel_Q` by `SUPPLY_DRAIN_PER_DAY_Q` (+ optional `supplyPressureBonus_Q`).
|
|
98
|
+
* 3. Decay `defenderMorale_Q` proportionally to combined wall/supply weakness.
|
|
99
|
+
* 4. If `wallIntegrity_Q < ASSAULT_WALL_THRESHOLD_Q` → resolve assault via `eventSeed`.
|
|
100
|
+
* 5. Else if `supplyLevel_Q ≤ SURRENDER_SUPPLY_THRESHOLD_Q` → daily surrender roll.
|
|
101
|
+
*
|
|
102
|
+
* @param worldSeed Global world seed for determinism.
|
|
103
|
+
* @param tick Current simulation tick.
|
|
104
|
+
* @param supplyPressureBonus_Q Extra daily supply drain (e.g., trade routes severed by Phase 83).
|
|
105
|
+
* @param siegeStrengthMul_Q Multiplier on siege strength (e.g., winter penalty from Phase 78).
|
|
106
|
+
*/
|
|
107
|
+
export declare function stepSiege(siege: SiegeState, worldSeed: number, tick: number, supplyPressureBonus_Q?: Q, siegeStrengthMul_Q?: Q): SiegeStepResult;
|
|
108
|
+
/**
|
|
109
|
+
* Run the siege forward until resolved or `maxDays` have elapsed.
|
|
110
|
+
* Returns the final step result. Useful for tests and quick simulations.
|
|
111
|
+
*/
|
|
112
|
+
export declare function runSiegeToResolution(siege: SiegeState, worldSeed: number, startTick: number, maxDays?: number): SiegeStepResult;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// src/siege.ts — Phase 84: Siege Warfare
|
|
2
|
+
//
|
|
3
|
+
// Models prolonged military operations against a fortified polity.
|
|
4
|
+
// A siege progresses through two main phases — investment then active —
|
|
5
|
+
// and resolves when walls are breached (assault) or supply is exhausted
|
|
6
|
+
// (surrender). All random outcomes use eventSeed for determinism.
|
|
7
|
+
//
|
|
8
|
+
// Design:
|
|
9
|
+
// - Pure data layer — no Entity fields, no kernel changes.
|
|
10
|
+
// - `SiegeState` is mutable; `stepSiege` advances it one simulated day.
|
|
11
|
+
// - Wall decay scales with attacker siege strength (Phase-61 militaryStrength_Q).
|
|
12
|
+
// - Defender morale tracks supply level and wall integrity.
|
|
13
|
+
// - Integrates with Phase-83 (severing trade routes raises supply drain)
|
|
14
|
+
// and Phase-78 (winter reduces attacker siege strength) via caller-supplied deltas.
|
|
15
|
+
import { eventSeed, hashString } from "./sim/seeds.js";
|
|
16
|
+
import { q, SCALE, clampQ, mulDiv } from "./units.js";
|
|
17
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
18
|
+
/** Days spent in the investment phase before active bombardment/starvation begins. */
|
|
19
|
+
export const INVESTMENT_DAYS = 14;
|
|
20
|
+
/**
|
|
21
|
+
* Base wall decay per active day at maximum siege strength.
|
|
22
|
+
* Actual decay = `siegeStrength_Q × WALL_DECAY_BASE_Q / SCALE.Q`.
|
|
23
|
+
*/
|
|
24
|
+
export const WALL_DECAY_BASE_Q = q(0.005);
|
|
25
|
+
/** Supply drain per active day (independent of attacker strength). */
|
|
26
|
+
export const SUPPLY_DRAIN_PER_DAY_Q = q(0.004);
|
|
27
|
+
/** Rate at which defender morale decays relative to combined wall/supply weakness. */
|
|
28
|
+
export const MORALE_DECAY_RATE_Q = q(0.002);
|
|
29
|
+
/** Wall integrity below this → assault is triggered and resolved. */
|
|
30
|
+
export const ASSAULT_WALL_THRESHOLD_Q = q(0.30);
|
|
31
|
+
/**
|
|
32
|
+
* Base assault success probability at equal siege strength and full defender morale.
|
|
33
|
+
* Actual chance boosted by `(SCALE.Q - defenderMorale_Q) × 0.30`.
|
|
34
|
+
*/
|
|
35
|
+
export const ASSAULT_SUCCESS_BASE_Q = q(0.50);
|
|
36
|
+
/** Supply below this → daily surrender check fires. */
|
|
37
|
+
export const SURRENDER_SUPPLY_THRESHOLD_Q = q(0.05);
|
|
38
|
+
// ── eventSeed salts ───────────────────────────────────────────────────────────
|
|
39
|
+
const ASSAULT_SALT = 1111;
|
|
40
|
+
const SURRENDER_SALT = 2222;
|
|
41
|
+
// ── Factory ───────────────────────────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Create a new siege. `attackerPolity.militaryStrength_Q` sets siege strength;
|
|
44
|
+
* `defenderPolity.stabilityQ` seeds defender morale.
|
|
45
|
+
*/
|
|
46
|
+
export function createSiege(attackerPolity, defenderPolity, tick = 0) {
|
|
47
|
+
return {
|
|
48
|
+
siegeId: `${attackerPolity.id}:${defenderPolity.id}:${tick}`,
|
|
49
|
+
attackerPolityId: attackerPolity.id,
|
|
50
|
+
defenderPolityId: defenderPolity.id,
|
|
51
|
+
phase: "investment",
|
|
52
|
+
startTick: tick,
|
|
53
|
+
phaseDay: 0,
|
|
54
|
+
wallIntegrity_Q: SCALE.Q,
|
|
55
|
+
supplyLevel_Q: SCALE.Q,
|
|
56
|
+
defenderMorale_Q: clampQ(defenderPolity.stabilityQ, 0, SCALE.Q),
|
|
57
|
+
siegeStrength_Q: clampQ(attackerPolity.militaryStrength_Q, 0, SCALE.Q),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ── Query helpers ──────────────────────────────────────────────────────────────
|
|
61
|
+
/** Return `true` if the siege has ended. */
|
|
62
|
+
export function isSiegeResolved(siege) {
|
|
63
|
+
return siege.phase === "resolved";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Compute daily attrition fractions for both sides in the current phase.
|
|
67
|
+
* - Investment: minimal skirmishing losses.
|
|
68
|
+
* - Active: attacker takes defensive fire; defender takes bombardment damage.
|
|
69
|
+
* - Resolved: no attrition.
|
|
70
|
+
*/
|
|
71
|
+
export function computeSiegeAttrition(siege) {
|
|
72
|
+
if (siege.phase === "investment") {
|
|
73
|
+
return { attackerLoss_Q: q(0.001), defenderLoss_Q: q(0.001) };
|
|
74
|
+
}
|
|
75
|
+
if (siege.phase === "active") {
|
|
76
|
+
// Attacker losses grow as walls fall (defenders become desperate)
|
|
77
|
+
const attackerLoss = clampQ(mulDiv(SCALE.Q - siege.wallIntegrity_Q, q(0.003), SCALE.Q) + q(0.001), 0, SCALE.Q);
|
|
78
|
+
// Defender losses from bombardment scale with siege strength
|
|
79
|
+
const defenderLoss = clampQ(mulDiv(siege.siegeStrength_Q, q(0.002), SCALE.Q), 0, SCALE.Q);
|
|
80
|
+
return { attackerLoss_Q: attackerLoss, defenderLoss_Q: defenderLoss };
|
|
81
|
+
}
|
|
82
|
+
return { attackerLoss_Q: 0, defenderLoss_Q: 0 };
|
|
83
|
+
}
|
|
84
|
+
// ── Siege progression ──────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Advance the siege by one simulated day.
|
|
87
|
+
*
|
|
88
|
+
* **Investment phase**: counts down `INVESTMENT_DAYS` then transitions to active.
|
|
89
|
+
*
|
|
90
|
+
* **Active phase** (each day):
|
|
91
|
+
* 1. Decay `wallIntegrity_Q` by `siegeStrength_Q × WALL_DECAY_BASE_Q / SCALE.Q`.
|
|
92
|
+
* 2. Drain `supplyLevel_Q` by `SUPPLY_DRAIN_PER_DAY_Q` (+ optional `supplyPressureBonus_Q`).
|
|
93
|
+
* 3. Decay `defenderMorale_Q` proportionally to combined wall/supply weakness.
|
|
94
|
+
* 4. If `wallIntegrity_Q < ASSAULT_WALL_THRESHOLD_Q` → resolve assault via `eventSeed`.
|
|
95
|
+
* 5. Else if `supplyLevel_Q ≤ SURRENDER_SUPPLY_THRESHOLD_Q` → daily surrender roll.
|
|
96
|
+
*
|
|
97
|
+
* @param worldSeed Global world seed for determinism.
|
|
98
|
+
* @param tick Current simulation tick.
|
|
99
|
+
* @param supplyPressureBonus_Q Extra daily supply drain (e.g., trade routes severed by Phase 83).
|
|
100
|
+
* @param siegeStrengthMul_Q Multiplier on siege strength (e.g., winter penalty from Phase 78).
|
|
101
|
+
*/
|
|
102
|
+
export function stepSiege(siege, worldSeed, tick, supplyPressureBonus_Q = 0, siegeStrengthMul_Q = SCALE.Q) {
|
|
103
|
+
if (siege.phase === "resolved") {
|
|
104
|
+
return { phaseChanged: false, resolved: true, ...(siege.outcome != null ? { outcome: siege.outcome } : {}) };
|
|
105
|
+
}
|
|
106
|
+
// ── Investment phase ────────────────────────────────────────────────────────
|
|
107
|
+
if (siege.phase === "investment") {
|
|
108
|
+
siege.phaseDay++;
|
|
109
|
+
if (siege.phaseDay >= INVESTMENT_DAYS) {
|
|
110
|
+
siege.phase = "active";
|
|
111
|
+
siege.phaseDay = 0;
|
|
112
|
+
return { phaseChanged: true, resolved: false };
|
|
113
|
+
}
|
|
114
|
+
return { phaseChanged: false, resolved: false };
|
|
115
|
+
}
|
|
116
|
+
// ── Active phase ────────────────────────────────────────────────────────────
|
|
117
|
+
const effectiveSiegeStr = clampQ(mulDiv(siege.siegeStrength_Q, siegeStrengthMul_Q, SCALE.Q), 0, SCALE.Q);
|
|
118
|
+
// 1. Wall decay
|
|
119
|
+
const wallDecay = mulDiv(effectiveSiegeStr, WALL_DECAY_BASE_Q, SCALE.Q);
|
|
120
|
+
siege.wallIntegrity_Q = clampQ(siege.wallIntegrity_Q - wallDecay, 0, SCALE.Q);
|
|
121
|
+
// 2. Supply drain (base + bonus from severed trade routes etc.)
|
|
122
|
+
const totalDrain = clampQ(SUPPLY_DRAIN_PER_DAY_Q + supplyPressureBonus_Q, 0, SCALE.Q);
|
|
123
|
+
siege.supplyLevel_Q = clampQ(siege.supplyLevel_Q - totalDrain, 0, SCALE.Q);
|
|
124
|
+
// 3. Morale decay — weighted average of supply and wall weakness
|
|
125
|
+
const wallWeakness = SCALE.Q - siege.wallIntegrity_Q;
|
|
126
|
+
const supplyWeakness = SCALE.Q - siege.supplyLevel_Q;
|
|
127
|
+
const avgWeakness = Math.round((wallWeakness + supplyWeakness) / 2);
|
|
128
|
+
const moraleDecay = mulDiv(avgWeakness, MORALE_DECAY_RATE_Q, SCALE.Q);
|
|
129
|
+
siege.defenderMorale_Q = clampQ(siege.defenderMorale_Q - moraleDecay, 0, SCALE.Q);
|
|
130
|
+
siege.phaseDay++;
|
|
131
|
+
// 4. Assault trigger (walls breached)
|
|
132
|
+
if (siege.wallIntegrity_Q < ASSAULT_WALL_THRESHOLD_Q) {
|
|
133
|
+
const seed = eventSeed(worldSeed, tick, hashString(siege.attackerPolityId), hashString(siege.defenderPolityId), ASSAULT_SALT);
|
|
134
|
+
const roll = seed % SCALE.Q;
|
|
135
|
+
// Attacker advantage = base success + morale deficit bonus
|
|
136
|
+
const moraleDeficitBonus = mulDiv(SCALE.Q - siege.defenderMorale_Q, q(0.30), SCALE.Q);
|
|
137
|
+
const successThresh = clampQ(mulDiv(effectiveSiegeStr, ASSAULT_SUCCESS_BASE_Q, SCALE.Q) + moraleDeficitBonus, 0, SCALE.Q);
|
|
138
|
+
if (roll < successThresh) {
|
|
139
|
+
siege.outcome = "attacker_victory";
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// Defenders plug the breach
|
|
143
|
+
siege.wallIntegrity_Q = clampQ(siege.wallIntegrity_Q + q(0.15), 0, SCALE.Q);
|
|
144
|
+
siege.outcome = "defender_holds";
|
|
145
|
+
}
|
|
146
|
+
siege.phase = "resolved";
|
|
147
|
+
return { phaseChanged: true, resolved: true, outcome: siege.outcome };
|
|
148
|
+
}
|
|
149
|
+
// 5. Surrender check (supply exhausted)
|
|
150
|
+
if (siege.supplyLevel_Q <= SURRENDER_SUPPLY_THRESHOLD_Q) {
|
|
151
|
+
const seed = eventSeed(worldSeed, tick, hashString(siege.defenderPolityId), hashString(siege.attackerPolityId), SURRENDER_SALT);
|
|
152
|
+
const roll = seed % SCALE.Q;
|
|
153
|
+
// Surrender chance = morale deficit × 0.70
|
|
154
|
+
const surrenderChance = mulDiv(SCALE.Q - siege.defenderMorale_Q, q(0.70), SCALE.Q);
|
|
155
|
+
if (roll < surrenderChance) {
|
|
156
|
+
siege.phase = "resolved";
|
|
157
|
+
siege.outcome = "surrender";
|
|
158
|
+
return { phaseChanged: true, resolved: true, outcome: "surrender" };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { phaseChanged: false, resolved: false };
|
|
162
|
+
}
|
|
163
|
+
// ── Convenience helpers ────────────────────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Run the siege forward until resolved or `maxDays` have elapsed.
|
|
166
|
+
* Returns the final step result. Useful for tests and quick simulations.
|
|
167
|
+
*/
|
|
168
|
+
export function runSiegeToResolution(siege, worldSeed, startTick, maxDays = 500) {
|
|
169
|
+
let result = { phaseChanged: false, resolved: false };
|
|
170
|
+
for (let d = 0; d < maxDays; d++) {
|
|
171
|
+
result = stepSiege(siege, worldSeed, startTick + d);
|
|
172
|
+
if (result.resolved)
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@its-not-rocket-science/ananke",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -98,6 +98,10 @@
|
|
|
98
98
|
"./trade-routes": {
|
|
99
99
|
"import": "./dist/src/trade-routes.js",
|
|
100
100
|
"types": "./dist/src/trade-routes.d.ts"
|
|
101
|
+
},
|
|
102
|
+
"./siege": {
|
|
103
|
+
"import": "./dist/src/siege.js",
|
|
104
|
+
"types": "./dist/src/siege.d.ts"
|
|
101
105
|
}
|
|
102
106
|
},
|
|
103
107
|
"files": [
|