@its-not-rocket-science/ananke 0.1.43 → 0.1.46
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 +76 -0
- package/dist/src/monetary.d.ts +104 -0
- package/dist/src/monetary.js +158 -0
- package/dist/src/wonders.d.ts +125 -0
- package/dist/src/wonders.js +264 -0
- package/package.json +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,82 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.46] — 2026-03-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Phase 101 · Currency & Monetary Policy** (`src/monetary.ts`)
|
|
14
|
+
- `CoinagePolicy`: `"stable" | "slight_debasement" | "heavy_debasement" | "emergency_printing"`.
|
|
15
|
+
- `MonetaryState { polityId, coinPurity_Q, inflationLevel_Q, monetaryCrisis }` — per-polity mutable tracker stored externally.
|
|
16
|
+
- `coinPurity_Q` [0, SCALE.Q]: intrinsic metal content; trade partners check this. Starts at SCALE.Q.
|
|
17
|
+
- `inflationLevel_Q` [0, SCALE.Q]: accumulated price inflation; drives purchasing power loss and unrest. Starts at 0.
|
|
18
|
+
- `monetaryCrisis`: activates when `inflationLevel_Q >= MONETARY_CRISIS_THRESHOLD_Q = q(0.60)`.
|
|
19
|
+
- `POLICY_PURITY_DELTA_PER_DAY`: stable +3 (recovery) → emergency_printing −40/day.
|
|
20
|
+
- `POLICY_INFLATION_DELTA_PER_DAY`: stable −3 (deflation) → emergency_printing +50/day.
|
|
21
|
+
- `POLICY_DAILY_MINT_FRAC_Q`: stable 0 → emergency_printing 30/SCALE.Q (+110%/year).
|
|
22
|
+
- `computePurchasingPower_Q(state)` → `coinPurity × (1 − inflation) / SCALE.Q`; floor q(0.05).
|
|
23
|
+
- `computeMonetaryTradeMultiplier_Q(state)` → `[MONETARY_TRADE_FLOOR_Q, SCALE.Q]`; based on purity; feeds Phase-92.
|
|
24
|
+
- `computeMonetaryUnrest_Q(state)` → `[0, MONETARY_MAX_UNREST_Q=q(0.25)]`; linear on inflation; feeds Phase-90.
|
|
25
|
+
- `computeDebasementGain_cu(polity, policy, elapsedDays)` → advisory preview of mint gain.
|
|
26
|
+
- `stepMonetary(polity, state, policy, elapsedDays)` — mints extra treasury, updates purity/inflation, sets crisis flag.
|
|
27
|
+
- `isMonetaryCrisis(state)` / `isCoinageSound(state, threshold_Q?)` — predicates.
|
|
28
|
+
- Added `./monetary` subpath export to `package.json`.
|
|
29
|
+
- 45 new tests; 5,261 total. Coverage: 100% statements/branches/functions/lines on `monetary.ts`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## [0.1.45] — 2026-03-27
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **Phase 100 · Wonders & Monuments** (`src/wonders.ts`)
|
|
38
|
+
- `WonderType`: `"great_pyramid" | "colosseum" | "grand_library" | "great_wall" | "grand_harbour" | "aqueduct_system" | "grand_temple"`.
|
|
39
|
+
- `WonderProject { projectId, polityId, type, progress_Q, investedCost_cu, startTick }` — in-progress construction.
|
|
40
|
+
- `Wonder { wonderId, polityId, type, completedAtTick, damaged }` — completed monument.
|
|
41
|
+
- `WonderEffects { stabilityBonus_Q, moraleBonus_Q, researchPointBonus, unrestReduction_Q, tradeIncomeBonus_Q, defenseBonus_Q, epidemicResistance_Q }` — advisory bundle.
|
|
42
|
+
- `WONDER_BASE_COST_CU`: grand_library 150k → great_pyramid 1,000k cu.
|
|
43
|
+
- `WONDER_TYPICAL_DAYS`: grand_library 180 → great_pyramid 3,650 days (10 years).
|
|
44
|
+
- `WONDER_BASE_EFFECTS`: distinct niches — great_wall highest defense (q(0.20)), grand_harbour highest trade (q(0.25)), aqueduct_system highest epidemic resistance (q(0.15)), colosseum highest unrest reduction (q(0.12)), grand_library +3 RP/day, great_pyramid highest stability (q(0.08)).
|
|
45
|
+
- `WONDER_DAMAGED_EFFECT_MUL = q(0.50)` — damaged wonders provide half effects.
|
|
46
|
+
- `WONDER_REPAIR_COST_FRAC = q(0.25)` — repair costs 25% of base construction cost.
|
|
47
|
+
- `createWonderProject(projectId, polityId, type, startTick)` — factory.
|
|
48
|
+
- `contributeToWonder(project, polity, contribution_cu)` — deducts treasury, advances progress_Q; capped by treasury and remaining cost; returns new progress.
|
|
49
|
+
- `isWonderProjectComplete(project)` → `progress_Q >= SCALE.Q`.
|
|
50
|
+
- `completeWonder(project, tick)` → `Wonder`.
|
|
51
|
+
- `damageWonder(wonder)` — set by Phase-96 earthquake or Phase-93 siege callers.
|
|
52
|
+
- `repairWonder(wonder, polity)` → `boolean` — spends repair cost; returns false if funds insufficient.
|
|
53
|
+
- `computeWonderEffects(wonder)` — full or half effects based on damage state.
|
|
54
|
+
- `aggregateWonderEffects(wonders)` — sums Q fields (clamped to SCALE.Q); sums researchPointBonus uncapped.
|
|
55
|
+
- `isWonderIntact(wonder)` / `computeRepairCost(type)` — helpers.
|
|
56
|
+
- Added `./wonders` subpath export to `package.json`.
|
|
57
|
+
- 43 new tests; 5,216 total. Coverage: 100% statements/branches/functions/lines on `wonders.ts`.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## [0.1.44] — 2026-03-27
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- **Phase 99 · Mercenaries & Hired Forces** (`src/mercenaries.ts`)
|
|
66
|
+
- `MercenaryBand { bandId, name, size, quality_Q, dailyWagePerSoldier_cu }` — immutable descriptor.
|
|
67
|
+
- `MercenaryContract { contractId, polityId, bandId, daysActive, loyalty_Q, arrears_cu }` — mutable live state stored externally.
|
|
68
|
+
- `MercenaryStepResult { wagePaid_cu, arrearsAdded_cu, loyaltyDelta, deserted }` — step outcome.
|
|
69
|
+
- `DESERT_LOYALTY_THRESHOLD_Q = q(0.25)` — below this, desertion roll fires.
|
|
70
|
+
- `LOYALTY_DECAY_PER_DAY_UNPAID = 80` — loyalty drops 0.8%/day when wages owed.
|
|
71
|
+
- `LOYALTY_GROWTH_PER_DAY_PAID = 20` — loyalty grows 0.2%/day when fully paid.
|
|
72
|
+
- `MAX_MERC_STRENGTH_BONUS_Q = q(0.30)` — caps advisory strength contribution.
|
|
73
|
+
- `computeMercenaryWage(band, elapsedDays)` — `size × dailyWage × days`.
|
|
74
|
+
- `computeMercenaryStrengthContribution(band, contract)` → Q — `size × quality × loyalty / SCALE.Q²`; capped at q(0.30); add to Phase-93 battle strength.
|
|
75
|
+
- `stepMercenaryContract(contract, band, polity, elapsedDays, worldSeed, tick)` — pays wages from treasury, accrues arrears, grows/decays loyalty, rolls desertion via `eventSeed` (deterministic).
|
|
76
|
+
- `applyVictoryLoyaltyBonus(contract)` — q(0.10) boost after campaign victory.
|
|
77
|
+
- `hireMercenaries(contractId, polityId, band, initialLoyalty_Q?)` — factory; default loyalty q(0.70).
|
|
78
|
+
- `isMercenaryReliable(contract)` / `hasMercenaryArrears(contract)` — predicates.
|
|
79
|
+
- Three sample bands: `BAND_LIGHT_CAVALRY` (400 soldiers, q(0.65), 3 cu/day), `BAND_HEAVY_INFANTRY` (600, q(0.85), 5 cu/day), `BAND_SIEGE_ENGINEERS` (200, q(0.75), 8 cu/day).
|
|
80
|
+
- Added `./mercenaries` subpath export to `package.json`.
|
|
81
|
+
- 44 new tests; 5,173 total. Coverage: 100% statements/branches/functions/lines on `mercenaries.ts`.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
9
85
|
## [0.1.43] — 2026-03-26
|
|
10
86
|
|
|
11
87
|
### Added
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Q } from "./units.js";
|
|
2
|
+
import type { Polity } from "./polity.js";
|
|
3
|
+
/** Polity monetary policy tier. */
|
|
4
|
+
export type CoinagePolicy = "stable" | "slight_debasement" | "heavy_debasement" | "emergency_printing";
|
|
5
|
+
/**
|
|
6
|
+
* Per-polity monetary state.
|
|
7
|
+
* Store externally (e.g. `Map<string, MonetaryState>`); pass to step each tick.
|
|
8
|
+
*/
|
|
9
|
+
export interface MonetaryState {
|
|
10
|
+
polityId: string;
|
|
11
|
+
/**
|
|
12
|
+
* Intrinsic coin purity [0, SCALE.Q].
|
|
13
|
+
* Starts at SCALE.Q (pure silver/gold). Debasement reduces it; stable
|
|
14
|
+
* policy restores it slowly. Trade partners assess coins by purity.
|
|
15
|
+
*/
|
|
16
|
+
coinPurity_Q: Q;
|
|
17
|
+
/**
|
|
18
|
+
* Accumulated price inflation [0, SCALE.Q].
|
|
19
|
+
* Starts at 0 (no inflation). Rises with debasement; falls slowly under
|
|
20
|
+
* stable policy. Drives purchasing power loss and unrest.
|
|
21
|
+
*/
|
|
22
|
+
inflationLevel_Q: Q;
|
|
23
|
+
/**
|
|
24
|
+
* `true` when `inflationLevel_Q >= MONETARY_CRISIS_THRESHOLD_Q`.
|
|
25
|
+
* In crisis: purchasing power collapses; trade partners sharply discount coins.
|
|
26
|
+
*/
|
|
27
|
+
monetaryCrisis: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** Inflation level at which monetary crisis activates [Q]. */
|
|
30
|
+
export declare const MONETARY_CRISIS_THRESHOLD_Q: Q;
|
|
31
|
+
/**
|
|
32
|
+
* Maximum unrest pressure from inflation at full inflation level [Q].
|
|
33
|
+
* Scales linearly: 0 at no inflation, `MONETARY_MAX_UNREST_Q` at SCALE.Q.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MONETARY_MAX_UNREST_Q: Q;
|
|
36
|
+
/**
|
|
37
|
+
* Minimum trade acceptance multiplier even with near-zero coin purity [Q].
|
|
38
|
+
* Barter / commodity exchange prevents complete trade collapse.
|
|
39
|
+
*/
|
|
40
|
+
export declare const MONETARY_TRADE_FLOOR_Q: Q;
|
|
41
|
+
/**
|
|
42
|
+
* Coin purity change per day by policy [out of SCALE.Q].
|
|
43
|
+
* Positive = recovery; negative = degradation.
|
|
44
|
+
*/
|
|
45
|
+
export declare const POLICY_PURITY_DELTA_PER_DAY: Record<CoinagePolicy, number>;
|
|
46
|
+
/**
|
|
47
|
+
* Inflation change per day by policy [out of SCALE.Q].
|
|
48
|
+
* Positive = inflation rising; negative = inflation falling.
|
|
49
|
+
*/
|
|
50
|
+
export declare const POLICY_INFLATION_DELTA_PER_DAY: Record<CoinagePolicy, number>;
|
|
51
|
+
/**
|
|
52
|
+
* Extra coins minted per day as a fraction of current treasury [out of SCALE.Q].
|
|
53
|
+
* `gain_cu = round(treasury_cu × mintFrac × elapsedDays / SCALE.Q)`
|
|
54
|
+
*/
|
|
55
|
+
export declare const POLICY_DAILY_MINT_FRAC_Q: Record<CoinagePolicy, number>;
|
|
56
|
+
/** Create a fresh `MonetaryState` with full purity and zero inflation. */
|
|
57
|
+
export declare function createMonetaryState(polityId: string): MonetaryState;
|
|
58
|
+
/**
|
|
59
|
+
* Compute the effective purchasing power of treasury coins [0, SCALE.Q].
|
|
60
|
+
*
|
|
61
|
+
* `purchasingPower = coinPurity_Q × (SCALE.Q − inflationLevel_Q) / SCALE.Q`
|
|
62
|
+
*
|
|
63
|
+
* Use to scale the real value of treasury income, mercenary wages, and
|
|
64
|
+
* construction costs. Returns q(0.05) minimum to avoid zero.
|
|
65
|
+
*/
|
|
66
|
+
export declare function computePurchasingPower_Q(state: MonetaryState): Q;
|
|
67
|
+
/**
|
|
68
|
+
* Compute the trade acceptance multiplier [MONETARY_TRADE_FLOOR_Q, SCALE.Q].
|
|
69
|
+
*
|
|
70
|
+
* Foreign trade partners check coin purity:
|
|
71
|
+
* `multiplier = TRADE_FLOOR + mulDiv(SCALE.Q − TRADE_FLOOR, coinPurity_Q, SCALE.Q)`
|
|
72
|
+
*
|
|
73
|
+
* Pass as a multiplier on Phase-92 trade income.
|
|
74
|
+
*/
|
|
75
|
+
export declare function computeMonetaryTradeMultiplier_Q(state: MonetaryState): Q;
|
|
76
|
+
/**
|
|
77
|
+
* Compute unrest pressure from inflation [0, MONETARY_MAX_UNREST_Q].
|
|
78
|
+
*
|
|
79
|
+
* `unrest = mulDiv(MONETARY_MAX_UNREST_Q, inflationLevel_Q, SCALE.Q)`
|
|
80
|
+
*
|
|
81
|
+
* Pass to Phase-90 `computeUnrestLevel`.
|
|
82
|
+
*/
|
|
83
|
+
export declare function computeMonetaryUnrest_Q(state: MonetaryState): Q;
|
|
84
|
+
/**
|
|
85
|
+
* Compute the extra treasury that would be minted by a debasement step
|
|
86
|
+
* without mutating state. Advisory / preview function.
|
|
87
|
+
*/
|
|
88
|
+
export declare function computeDebasementGain_cu(polity: Polity, policy: CoinagePolicy, elapsedDays: number): number;
|
|
89
|
+
/**
|
|
90
|
+
* Advance monetary state by `elapsedDays` under the given policy.
|
|
91
|
+
*
|
|
92
|
+
* 1. Mints extra coins: `treasury += computeDebasementGain_cu(polity, policy, elapsedDays)`.
|
|
93
|
+
* 2. Updates `coinPurity_Q` by `POLICY_PURITY_DELTA_PER_DAY × elapsedDays`; clamped [0, SCALE.Q].
|
|
94
|
+
* 3. Updates `inflationLevel_Q` by `POLICY_INFLATION_DELTA_PER_DAY × elapsedDays`; clamped [0, SCALE.Q].
|
|
95
|
+
* 4. Sets `monetaryCrisis = inflationLevel_Q >= MONETARY_CRISIS_THRESHOLD_Q`.
|
|
96
|
+
*
|
|
97
|
+
* Mutates `polity.treasury_cu`, `state.coinPurity_Q`, `state.inflationLevel_Q`,
|
|
98
|
+
* and `state.monetaryCrisis`.
|
|
99
|
+
*/
|
|
100
|
+
export declare function stepMonetary(polity: Polity, state: MonetaryState, policy: CoinagePolicy, elapsedDays: number): void;
|
|
101
|
+
/** Return `true` when the polity is in monetary crisis (high inflation). */
|
|
102
|
+
export declare function isMonetaryCrisis(state: MonetaryState): boolean;
|
|
103
|
+
/** Return `true` when coin purity is at or above the given threshold. */
|
|
104
|
+
export declare function isCoinageSound(state: MonetaryState, threshold_Q?: Q): boolean;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/monetary.ts — Phase 101: Currency & Monetary Policy
|
|
2
|
+
//
|
|
3
|
+
// Models coin purity, inflation, and monetary crises at polity scale.
|
|
4
|
+
// Rulers can debase coinage (mint extra coins at lower purity) for short-term
|
|
5
|
+
// treasury gain, but sustained debasement causes inflation, trade rejection,
|
|
6
|
+
// and civil unrest.
|
|
7
|
+
//
|
|
8
|
+
// Design:
|
|
9
|
+
// - Pure data layer — no Entity fields, no kernel changes.
|
|
10
|
+
// - `MonetaryState` stores coin purity and inflation level separately:
|
|
11
|
+
// coinPurity_Q — intrinsic metal content; affects trade acceptance.
|
|
12
|
+
// inflationLevel_Q — accumulated price inflation; affects purchasing power and unrest.
|
|
13
|
+
// - `stepMonetary` mints extra coins (treasury gain), degrades purity, and accrues
|
|
14
|
+
// inflation; stable policy slowly restores both.
|
|
15
|
+
// - All derived metrics (`purchasingPower_Q`, `tradeMultiplier_Q`, `unrestPressure_Q`)
|
|
16
|
+
// are advisory; callers pass them to Phases 90/92/93 as needed.
|
|
17
|
+
//
|
|
18
|
+
// Integration:
|
|
19
|
+
// Phase 90 (Unrest): computeMonetaryUnrest_Q → unrestPressure_Q.
|
|
20
|
+
// Phase 92 (Taxation): computePurchasingPower_Q → real value of tax revenue.
|
|
21
|
+
// Phase 93 (Campaign): computeMonetaryTradeMultiplier_Q → tribute / supply costs.
|
|
22
|
+
// Phase 99 (Mercenaries): high inflation → real wage cost rises; host adjusts.
|
|
23
|
+
// Phase 100 (Wonders): construction costs inflated; host scales contribution.
|
|
24
|
+
import { q, SCALE, clampQ, mulDiv } from "./units.js";
|
|
25
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
26
|
+
/** Inflation level at which monetary crisis activates [Q]. */
|
|
27
|
+
export const MONETARY_CRISIS_THRESHOLD_Q = q(0.60);
|
|
28
|
+
/**
|
|
29
|
+
* Maximum unrest pressure from inflation at full inflation level [Q].
|
|
30
|
+
* Scales linearly: 0 at no inflation, `MONETARY_MAX_UNREST_Q` at SCALE.Q.
|
|
31
|
+
*/
|
|
32
|
+
export const MONETARY_MAX_UNREST_Q = q(0.25);
|
|
33
|
+
/**
|
|
34
|
+
* Minimum trade acceptance multiplier even with near-zero coin purity [Q].
|
|
35
|
+
* Barter / commodity exchange prevents complete trade collapse.
|
|
36
|
+
*/
|
|
37
|
+
export const MONETARY_TRADE_FLOOR_Q = q(0.40);
|
|
38
|
+
/**
|
|
39
|
+
* Coin purity change per day by policy [out of SCALE.Q].
|
|
40
|
+
* Positive = recovery; negative = degradation.
|
|
41
|
+
*/
|
|
42
|
+
export const POLICY_PURITY_DELTA_PER_DAY = {
|
|
43
|
+
stable: +3,
|
|
44
|
+
slight_debasement: -5,
|
|
45
|
+
heavy_debasement: -18,
|
|
46
|
+
emergency_printing: -40,
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Inflation change per day by policy [out of SCALE.Q].
|
|
50
|
+
* Positive = inflation rising; negative = inflation falling.
|
|
51
|
+
*/
|
|
52
|
+
export const POLICY_INFLATION_DELTA_PER_DAY = {
|
|
53
|
+
stable: -3,
|
|
54
|
+
slight_debasement: +6,
|
|
55
|
+
heavy_debasement: +20,
|
|
56
|
+
emergency_printing: +50,
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Extra coins minted per day as a fraction of current treasury [out of SCALE.Q].
|
|
60
|
+
* `gain_cu = round(treasury_cu × mintFrac × elapsedDays / SCALE.Q)`
|
|
61
|
+
*/
|
|
62
|
+
export const POLICY_DAILY_MINT_FRAC_Q = {
|
|
63
|
+
stable: 0,
|
|
64
|
+
slight_debasement: 3, // +0.03%/day ≈ +11%/year
|
|
65
|
+
heavy_debasement: 10, // +0.10%/day ≈ +37%/year
|
|
66
|
+
emergency_printing: 30, // +0.30%/day ≈ +110%/year
|
|
67
|
+
};
|
|
68
|
+
// ── Factory ───────────────────────────────────────────────────────────────────
|
|
69
|
+
/** Create a fresh `MonetaryState` with full purity and zero inflation. */
|
|
70
|
+
export function createMonetaryState(polityId) {
|
|
71
|
+
return {
|
|
72
|
+
polityId,
|
|
73
|
+
coinPurity_Q: SCALE.Q,
|
|
74
|
+
inflationLevel_Q: 0,
|
|
75
|
+
monetaryCrisis: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// ── Advisory metrics ──────────────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Compute the effective purchasing power of treasury coins [0, SCALE.Q].
|
|
81
|
+
*
|
|
82
|
+
* `purchasingPower = coinPurity_Q × (SCALE.Q − inflationLevel_Q) / SCALE.Q`
|
|
83
|
+
*
|
|
84
|
+
* Use to scale the real value of treasury income, mercenary wages, and
|
|
85
|
+
* construction costs. Returns q(0.05) minimum to avoid zero.
|
|
86
|
+
*/
|
|
87
|
+
export function computePurchasingPower_Q(state) {
|
|
88
|
+
const deflated = clampQ(SCALE.Q - state.inflationLevel_Q, 0, SCALE.Q);
|
|
89
|
+
const pp = mulDiv(state.coinPurity_Q, deflated, SCALE.Q);
|
|
90
|
+
return clampQ(pp, q(0.05), SCALE.Q);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Compute the trade acceptance multiplier [MONETARY_TRADE_FLOOR_Q, SCALE.Q].
|
|
94
|
+
*
|
|
95
|
+
* Foreign trade partners check coin purity:
|
|
96
|
+
* `multiplier = TRADE_FLOOR + mulDiv(SCALE.Q − TRADE_FLOOR, coinPurity_Q, SCALE.Q)`
|
|
97
|
+
*
|
|
98
|
+
* Pass as a multiplier on Phase-92 trade income.
|
|
99
|
+
*/
|
|
100
|
+
export function computeMonetaryTradeMultiplier_Q(state) {
|
|
101
|
+
const floor = MONETARY_TRADE_FLOOR_Q;
|
|
102
|
+
const range = SCALE.Q - floor;
|
|
103
|
+
return clampQ(floor + mulDiv(range, state.coinPurity_Q, SCALE.Q), floor, SCALE.Q);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Compute unrest pressure from inflation [0, MONETARY_MAX_UNREST_Q].
|
|
107
|
+
*
|
|
108
|
+
* `unrest = mulDiv(MONETARY_MAX_UNREST_Q, inflationLevel_Q, SCALE.Q)`
|
|
109
|
+
*
|
|
110
|
+
* Pass to Phase-90 `computeUnrestLevel`.
|
|
111
|
+
*/
|
|
112
|
+
export function computeMonetaryUnrest_Q(state) {
|
|
113
|
+
return clampQ(mulDiv(MONETARY_MAX_UNREST_Q, state.inflationLevel_Q, SCALE.Q), 0, MONETARY_MAX_UNREST_Q);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Compute the extra treasury that would be minted by a debasement step
|
|
117
|
+
* without mutating state. Advisory / preview function.
|
|
118
|
+
*/
|
|
119
|
+
export function computeDebasementGain_cu(polity, policy, elapsedDays) {
|
|
120
|
+
const mintFrac = POLICY_DAILY_MINT_FRAC_Q[policy];
|
|
121
|
+
if (mintFrac === 0)
|
|
122
|
+
return 0;
|
|
123
|
+
return Math.round(polity.treasury_cu * mintFrac * elapsedDays / SCALE.Q);
|
|
124
|
+
}
|
|
125
|
+
// ── State step ────────────────────────────────────────────────────────────────
|
|
126
|
+
/**
|
|
127
|
+
* Advance monetary state by `elapsedDays` under the given policy.
|
|
128
|
+
*
|
|
129
|
+
* 1. Mints extra coins: `treasury += computeDebasementGain_cu(polity, policy, elapsedDays)`.
|
|
130
|
+
* 2. Updates `coinPurity_Q` by `POLICY_PURITY_DELTA_PER_DAY × elapsedDays`; clamped [0, SCALE.Q].
|
|
131
|
+
* 3. Updates `inflationLevel_Q` by `POLICY_INFLATION_DELTA_PER_DAY × elapsedDays`; clamped [0, SCALE.Q].
|
|
132
|
+
* 4. Sets `monetaryCrisis = inflationLevel_Q >= MONETARY_CRISIS_THRESHOLD_Q`.
|
|
133
|
+
*
|
|
134
|
+
* Mutates `polity.treasury_cu`, `state.coinPurity_Q`, `state.inflationLevel_Q`,
|
|
135
|
+
* and `state.monetaryCrisis`.
|
|
136
|
+
*/
|
|
137
|
+
export function stepMonetary(polity, state, policy, elapsedDays) {
|
|
138
|
+
// Mint gain
|
|
139
|
+
const gain = computeDebasementGain_cu(polity, policy, elapsedDays);
|
|
140
|
+
polity.treasury_cu += gain;
|
|
141
|
+
// Purity update
|
|
142
|
+
const purityDelta = POLICY_PURITY_DELTA_PER_DAY[policy] * elapsedDays;
|
|
143
|
+
state.coinPurity_Q = clampQ(state.coinPurity_Q + purityDelta, 0, SCALE.Q);
|
|
144
|
+
// Inflation update
|
|
145
|
+
const inflDelta = POLICY_INFLATION_DELTA_PER_DAY[policy] * elapsedDays;
|
|
146
|
+
state.inflationLevel_Q = clampQ(state.inflationLevel_Q + inflDelta, 0, SCALE.Q);
|
|
147
|
+
// Crisis flag
|
|
148
|
+
state.monetaryCrisis = state.inflationLevel_Q >= MONETARY_CRISIS_THRESHOLD_Q;
|
|
149
|
+
}
|
|
150
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
151
|
+
/** Return `true` when the polity is in monetary crisis (high inflation). */
|
|
152
|
+
export function isMonetaryCrisis(state) {
|
|
153
|
+
return state.monetaryCrisis;
|
|
154
|
+
}
|
|
155
|
+
/** Return `true` when coin purity is at or above the given threshold. */
|
|
156
|
+
export function isCoinageSound(state, threshold_Q = q(0.80)) {
|
|
157
|
+
return state.coinPurity_Q >= threshold_Q;
|
|
158
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Q } from "./units.js";
|
|
2
|
+
import type { Polity } from "./polity.js";
|
|
3
|
+
/** Classification of wonder. */
|
|
4
|
+
export type WonderType = "great_pyramid" | "colosseum" | "grand_library" | "great_wall" | "grand_harbour" | "aqueduct_system" | "grand_temple";
|
|
5
|
+
/** In-progress wonder construction. */
|
|
6
|
+
export interface WonderProject {
|
|
7
|
+
projectId: string;
|
|
8
|
+
polityId: string;
|
|
9
|
+
type: WonderType;
|
|
10
|
+
/** Current build progress [0, SCALE.Q]. Complete at SCALE.Q. */
|
|
11
|
+
progress_Q: Q;
|
|
12
|
+
/** Cumulative treasury invested so far [cu]. */
|
|
13
|
+
investedCost_cu: number;
|
|
14
|
+
/** Tick at which construction started. */
|
|
15
|
+
startTick: number;
|
|
16
|
+
}
|
|
17
|
+
/** A completed wonder. */
|
|
18
|
+
export interface Wonder {
|
|
19
|
+
wonderId: string;
|
|
20
|
+
polityId: string;
|
|
21
|
+
type: WonderType;
|
|
22
|
+
/** Tick at which construction finished. */
|
|
23
|
+
completedAtTick: number;
|
|
24
|
+
/**
|
|
25
|
+
* Whether this wonder has been damaged by an earthquake (Phase-96) or
|
|
26
|
+
* siege (Phase-93). Damaged wonders provide `WONDER_DAMAGED_EFFECT_MUL`
|
|
27
|
+
* fraction of their normal effects until repaired.
|
|
28
|
+
*/
|
|
29
|
+
damaged: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Advisory effect bundle from a wonder.
|
|
33
|
+
* Pass individual fields into the relevant downstream phase calls.
|
|
34
|
+
* All Q fields are [0, SCALE.Q]; `researchPointBonus` is raw points/day.
|
|
35
|
+
*/
|
|
36
|
+
export interface WonderEffects {
|
|
37
|
+
/** Add to `polity.stabilityQ`. */
|
|
38
|
+
stabilityBonus_Q: Q;
|
|
39
|
+
/** Add to `polity.moraleQ`. */
|
|
40
|
+
moraleBonus_Q: Q;
|
|
41
|
+
/** Additional research points per day. Pass to Phase-91. */
|
|
42
|
+
researchPointBonus: number;
|
|
43
|
+
/** Subtract from Phase-90 unrest level. */
|
|
44
|
+
unrestReduction_Q: Q;
|
|
45
|
+
/** Trade income multiplier bonus. Pass to Phase-92. */
|
|
46
|
+
tradeIncomeBonus_Q: Q;
|
|
47
|
+
/** Additive defender strength bonus. Pass to Phase-93. */
|
|
48
|
+
defenseBonus_Q: Q;
|
|
49
|
+
/** Add to `healthCapacity_Q` in Phase-88 `stepEpidemic`. */
|
|
50
|
+
epidemicResistance_Q: Q;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Total treasury cost to construct each wonder type [cu].
|
|
54
|
+
* Grand library is fastest; great pyramid is a generational project.
|
|
55
|
+
*/
|
|
56
|
+
export declare const WONDER_BASE_COST_CU: Record<WonderType, number>;
|
|
57
|
+
/**
|
|
58
|
+
* Estimated build time in days at average investment rate.
|
|
59
|
+
* Informational only — actual duration depends on how fast the host invests.
|
|
60
|
+
*/
|
|
61
|
+
export declare const WONDER_TYPICAL_DAYS: Record<WonderType, number>;
|
|
62
|
+
/**
|
|
63
|
+
* Full effects for each wonder type at q(1.0) effectiveness.
|
|
64
|
+
* Damaged wonders multiply each field by `WONDER_DAMAGED_EFFECT_MUL`.
|
|
65
|
+
*/
|
|
66
|
+
export declare const WONDER_BASE_EFFECTS: Record<WonderType, WonderEffects>;
|
|
67
|
+
/**
|
|
68
|
+
* Effect multiplier for a damaged wonder [0, SCALE.Q].
|
|
69
|
+
* Damaged wonders still provide partial benefit; repair restores full effects.
|
|
70
|
+
*/
|
|
71
|
+
export declare const WONDER_DAMAGED_EFFECT_MUL: Q;
|
|
72
|
+
/**
|
|
73
|
+
* Treasury cost to repair a damaged wonder, as a fraction of `WONDER_BASE_COST_CU`.
|
|
74
|
+
* Repair = `round(baseCost × WONDER_REPAIR_COST_FRAC / SCALE.Q)`.
|
|
75
|
+
*/
|
|
76
|
+
export declare const WONDER_REPAIR_COST_FRAC: Q;
|
|
77
|
+
/** Create a new wonder construction project. */
|
|
78
|
+
export declare function createWonderProject(projectId: string, polityId: string, type: WonderType, startTick: number): WonderProject;
|
|
79
|
+
/**
|
|
80
|
+
* Invest treasury into a wonder project.
|
|
81
|
+
*
|
|
82
|
+
* Deducts up to `contribution_cu` from `polity.treasury_cu` (capped by available
|
|
83
|
+
* treasury and remaining cost), advances `progress_Q`, and returns the new progress.
|
|
84
|
+
*
|
|
85
|
+
* Does not auto-complete — call `isWonderProjectComplete` then `completeWonder`.
|
|
86
|
+
*/
|
|
87
|
+
export declare function contributeToWonder(project: WonderProject, polity: Polity, contribution_cu: number): Q;
|
|
88
|
+
/** Return `true` when the project has reached full completion. */
|
|
89
|
+
export declare function isWonderProjectComplete(project: WonderProject): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Finalise a completed project into a standing `Wonder`.
|
|
92
|
+
*
|
|
93
|
+
* The caller is responsible for checking `isWonderProjectComplete` first.
|
|
94
|
+
*/
|
|
95
|
+
export declare function completeWonder(project: WonderProject, tick: number): Wonder;
|
|
96
|
+
/**
|
|
97
|
+
* Mark a wonder as damaged (earthquake, siege).
|
|
98
|
+
* Damaged wonders yield `WONDER_DAMAGED_EFFECT_MUL` fraction of full effects.
|
|
99
|
+
*/
|
|
100
|
+
export declare function damageWonder(wonder: Wonder): void;
|
|
101
|
+
/**
|
|
102
|
+
* Repair a damaged wonder, spending `WONDER_REPAIR_COST_FRAC` of base cost.
|
|
103
|
+
*
|
|
104
|
+
* Mutates `polity.treasury_cu` and clears `wonder.damaged`.
|
|
105
|
+
* Returns `true` if repaired; `false` if the polity lacked funds.
|
|
106
|
+
* No-op if wonder is not damaged.
|
|
107
|
+
*/
|
|
108
|
+
export declare function repairWonder(wonder: Wonder, polity: Polity): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Compute the `WonderEffects` advisory bundle for a single wonder.
|
|
111
|
+
*
|
|
112
|
+
* Damaged wonders: each numeric field is scaled by `WONDER_DAMAGED_EFFECT_MUL / SCALE.Q`.
|
|
113
|
+
*/
|
|
114
|
+
export declare function computeWonderEffects(wonder: Wonder): WonderEffects;
|
|
115
|
+
/**
|
|
116
|
+
* Aggregate effects from multiple wonders.
|
|
117
|
+
*
|
|
118
|
+
* Q fields are summed and clamped to SCALE.Q.
|
|
119
|
+
* `researchPointBonus` is summed without capping.
|
|
120
|
+
*/
|
|
121
|
+
export declare function aggregateWonderEffects(wonders: Wonder[]): WonderEffects;
|
|
122
|
+
/** Return `true` when the wonder is standing and undamaged. */
|
|
123
|
+
export declare function isWonderIntact(wonder: Wonder): boolean;
|
|
124
|
+
/** Compute treasury cost to repair a damaged wonder [cu]. */
|
|
125
|
+
export declare function computeRepairCost(type: WonderType): number;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// src/wonders.ts — Phase 100: Wonders & Monuments
|
|
2
|
+
//
|
|
3
|
+
// Unique prestige constructions that define great civilisations — pyramids,
|
|
4
|
+
// colosseums, grand libraries, great walls, harbours, aqueducts, temples.
|
|
5
|
+
// Unlike Phase-89 functional infrastructure (roads, markets, apothecaries),
|
|
6
|
+
// wonders are one-of-a-kind, take years to complete, and give civilisation-
|
|
7
|
+
// level bonuses to stability, morale, research, defence, trade, and health.
|
|
8
|
+
//
|
|
9
|
+
// Design:
|
|
10
|
+
// - Pure data layer — no Entity fields, no kernel changes.
|
|
11
|
+
// - `WonderProject` tracks construction progress; `Wonder` is the completed record.
|
|
12
|
+
// - Progress = investedCost_cu / WONDER_BASE_COST_CU; completion when progress_Q = SCALE.Q.
|
|
13
|
+
// - `WonderEffects` is an advisory bundle; callers pass fields into Phases 88–93.
|
|
14
|
+
// - Damaged wonders (Phase-96 earthquake / Phase-93 siege) provide half effects
|
|
15
|
+
// until repaired at half the base cost.
|
|
16
|
+
// - Only one wonder of each type per polity (enforced by naming convention;
|
|
17
|
+
// host is responsible for uniqueness).
|
|
18
|
+
//
|
|
19
|
+
// Integration:
|
|
20
|
+
// Phase 88 (Epidemic): epidemicResistance_Q adds to healthCapacity_Q (aqueduct).
|
|
21
|
+
// Phase 90 (Unrest): unrestReduction_Q reduces computeUnrestLevel (colosseum, temple).
|
|
22
|
+
// Phase 91 (Research): researchPointBonus adds daily research points (library).
|
|
23
|
+
// Phase 92 (Taxation): tradeIncomeBonus_Q scales trade income (harbour).
|
|
24
|
+
// Phase 93 (Military Camp): defenseBonus_Q adds to defender strength (great_wall).
|
|
25
|
+
// Phase 96 (Climate): earthquake → caller calls damageWonder().
|
|
26
|
+
import { q, SCALE, clampQ, mulDiv } from "./units.js";
|
|
27
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
28
|
+
/**
|
|
29
|
+
* Total treasury cost to construct each wonder type [cu].
|
|
30
|
+
* Grand library is fastest; great pyramid is a generational project.
|
|
31
|
+
*/
|
|
32
|
+
export const WONDER_BASE_COST_CU = {
|
|
33
|
+
grand_library: 150_000,
|
|
34
|
+
aqueduct_system: 200_000,
|
|
35
|
+
grand_temple: 250_000,
|
|
36
|
+
grand_harbour: 300_000,
|
|
37
|
+
colosseum: 500_000,
|
|
38
|
+
great_wall: 600_000,
|
|
39
|
+
great_pyramid: 1_000_000,
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Estimated build time in days at average investment rate.
|
|
43
|
+
* Informational only — actual duration depends on how fast the host invests.
|
|
44
|
+
*/
|
|
45
|
+
export const WONDER_TYPICAL_DAYS = {
|
|
46
|
+
grand_library: 180,
|
|
47
|
+
aqueduct_system: 365,
|
|
48
|
+
grand_temple: 365,
|
|
49
|
+
grand_harbour: 365,
|
|
50
|
+
colosseum: 730,
|
|
51
|
+
great_wall: 1095,
|
|
52
|
+
great_pyramid: 3650,
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Full effects for each wonder type at q(1.0) effectiveness.
|
|
56
|
+
* Damaged wonders multiply each field by `WONDER_DAMAGED_EFFECT_MUL`.
|
|
57
|
+
*/
|
|
58
|
+
export const WONDER_BASE_EFFECTS = {
|
|
59
|
+
great_pyramid: {
|
|
60
|
+
stabilityBonus_Q: q(0.08), // generational prestige
|
|
61
|
+
moraleBonus_Q: q(0.05),
|
|
62
|
+
researchPointBonus: 0,
|
|
63
|
+
unrestReduction_Q: 0,
|
|
64
|
+
tradeIncomeBonus_Q: 0,
|
|
65
|
+
defenseBonus_Q: 0,
|
|
66
|
+
epidemicResistance_Q: 0,
|
|
67
|
+
},
|
|
68
|
+
colosseum: {
|
|
69
|
+
stabilityBonus_Q: q(0.03),
|
|
70
|
+
moraleBonus_Q: q(0.10), // entertainment
|
|
71
|
+
researchPointBonus: 0,
|
|
72
|
+
unrestReduction_Q: q(0.12), // bread and circuses
|
|
73
|
+
tradeIncomeBonus_Q: 0,
|
|
74
|
+
defenseBonus_Q: 0,
|
|
75
|
+
epidemicResistance_Q: 0,
|
|
76
|
+
},
|
|
77
|
+
grand_library: {
|
|
78
|
+
stabilityBonus_Q: q(0.03),
|
|
79
|
+
moraleBonus_Q: q(0.02),
|
|
80
|
+
researchPointBonus: 3, // +3 RP/day
|
|
81
|
+
unrestReduction_Q: 0,
|
|
82
|
+
tradeIncomeBonus_Q: 0,
|
|
83
|
+
defenseBonus_Q: 0,
|
|
84
|
+
epidemicResistance_Q: 0,
|
|
85
|
+
},
|
|
86
|
+
great_wall: {
|
|
87
|
+
stabilityBonus_Q: q(0.05),
|
|
88
|
+
moraleBonus_Q: 0,
|
|
89
|
+
researchPointBonus: 0,
|
|
90
|
+
unrestReduction_Q: 0,
|
|
91
|
+
tradeIncomeBonus_Q: 0,
|
|
92
|
+
defenseBonus_Q: q(0.20), // major defensive advantage
|
|
93
|
+
epidemicResistance_Q: 0,
|
|
94
|
+
},
|
|
95
|
+
grand_harbour: {
|
|
96
|
+
stabilityBonus_Q: 0,
|
|
97
|
+
moraleBonus_Q: q(0.03),
|
|
98
|
+
researchPointBonus: 0,
|
|
99
|
+
unrestReduction_Q: 0,
|
|
100
|
+
tradeIncomeBonus_Q: q(0.25), // major trade multiplier
|
|
101
|
+
defenseBonus_Q: 0,
|
|
102
|
+
epidemicResistance_Q: 0,
|
|
103
|
+
},
|
|
104
|
+
aqueduct_system: {
|
|
105
|
+
stabilityBonus_Q: q(0.02),
|
|
106
|
+
moraleBonus_Q: q(0.04), // quality of life
|
|
107
|
+
researchPointBonus: 0,
|
|
108
|
+
unrestReduction_Q: 0,
|
|
109
|
+
tradeIncomeBonus_Q: 0,
|
|
110
|
+
defenseBonus_Q: 0,
|
|
111
|
+
epidemicResistance_Q: q(0.15), // clean water reduces disease
|
|
112
|
+
},
|
|
113
|
+
grand_temple: {
|
|
114
|
+
stabilityBonus_Q: q(0.06), // divine legitimacy
|
|
115
|
+
moraleBonus_Q: q(0.08),
|
|
116
|
+
researchPointBonus: 0,
|
|
117
|
+
unrestReduction_Q: q(0.08),
|
|
118
|
+
tradeIncomeBonus_Q: 0,
|
|
119
|
+
defenseBonus_Q: 0,
|
|
120
|
+
epidemicResistance_Q: 0,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Effect multiplier for a damaged wonder [0, SCALE.Q].
|
|
125
|
+
* Damaged wonders still provide partial benefit; repair restores full effects.
|
|
126
|
+
*/
|
|
127
|
+
export const WONDER_DAMAGED_EFFECT_MUL = q(0.50);
|
|
128
|
+
/**
|
|
129
|
+
* Treasury cost to repair a damaged wonder, as a fraction of `WONDER_BASE_COST_CU`.
|
|
130
|
+
* Repair = `round(baseCost × WONDER_REPAIR_COST_FRAC / SCALE.Q)`.
|
|
131
|
+
*/
|
|
132
|
+
export const WONDER_REPAIR_COST_FRAC = q(0.25);
|
|
133
|
+
// ── Factory ───────────────────────────────────────────────────────────────────
|
|
134
|
+
/** Create a new wonder construction project. */
|
|
135
|
+
export function createWonderProject(projectId, polityId, type, startTick) {
|
|
136
|
+
return {
|
|
137
|
+
projectId,
|
|
138
|
+
polityId,
|
|
139
|
+
type,
|
|
140
|
+
progress_Q: 0,
|
|
141
|
+
investedCost_cu: 0,
|
|
142
|
+
startTick,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// ── Construction ──────────────────────────────────────────────────────────────
|
|
146
|
+
/**
|
|
147
|
+
* Invest treasury into a wonder project.
|
|
148
|
+
*
|
|
149
|
+
* Deducts up to `contribution_cu` from `polity.treasury_cu` (capped by available
|
|
150
|
+
* treasury and remaining cost), advances `progress_Q`, and returns the new progress.
|
|
151
|
+
*
|
|
152
|
+
* Does not auto-complete — call `isWonderProjectComplete` then `completeWonder`.
|
|
153
|
+
*/
|
|
154
|
+
export function contributeToWonder(project, polity, contribution_cu) {
|
|
155
|
+
const totalCost = WONDER_BASE_COST_CU[project.type];
|
|
156
|
+
const remaining = Math.max(0, totalCost - project.investedCost_cu);
|
|
157
|
+
const actual = Math.min(contribution_cu, polity.treasury_cu, remaining);
|
|
158
|
+
polity.treasury_cu -= actual;
|
|
159
|
+
project.investedCost_cu += actual;
|
|
160
|
+
project.progress_Q = clampQ(Math.round(project.investedCost_cu * SCALE.Q / totalCost), 0, SCALE.Q);
|
|
161
|
+
return project.progress_Q;
|
|
162
|
+
}
|
|
163
|
+
/** Return `true` when the project has reached full completion. */
|
|
164
|
+
export function isWonderProjectComplete(project) {
|
|
165
|
+
return project.progress_Q >= SCALE.Q;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Finalise a completed project into a standing `Wonder`.
|
|
169
|
+
*
|
|
170
|
+
* The caller is responsible for checking `isWonderProjectComplete` first.
|
|
171
|
+
*/
|
|
172
|
+
export function completeWonder(project, tick) {
|
|
173
|
+
return {
|
|
174
|
+
wonderId: project.projectId,
|
|
175
|
+
polityId: project.polityId,
|
|
176
|
+
type: project.type,
|
|
177
|
+
completedAtTick: tick,
|
|
178
|
+
damaged: false,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// ── Damage & repair ───────────────────────────────────────────────────────────
|
|
182
|
+
/**
|
|
183
|
+
* Mark a wonder as damaged (earthquake, siege).
|
|
184
|
+
* Damaged wonders yield `WONDER_DAMAGED_EFFECT_MUL` fraction of full effects.
|
|
185
|
+
*/
|
|
186
|
+
export function damageWonder(wonder) {
|
|
187
|
+
wonder.damaged = true;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Repair a damaged wonder, spending `WONDER_REPAIR_COST_FRAC` of base cost.
|
|
191
|
+
*
|
|
192
|
+
* Mutates `polity.treasury_cu` and clears `wonder.damaged`.
|
|
193
|
+
* Returns `true` if repaired; `false` if the polity lacked funds.
|
|
194
|
+
* No-op if wonder is not damaged.
|
|
195
|
+
*/
|
|
196
|
+
export function repairWonder(wonder, polity) {
|
|
197
|
+
if (!wonder.damaged)
|
|
198
|
+
return true;
|
|
199
|
+
const baseCost = WONDER_BASE_COST_CU[wonder.type];
|
|
200
|
+
const repairCost = Math.round(mulDiv(baseCost, WONDER_REPAIR_COST_FRAC, SCALE.Q));
|
|
201
|
+
if (polity.treasury_cu < repairCost)
|
|
202
|
+
return false;
|
|
203
|
+
polity.treasury_cu -= repairCost;
|
|
204
|
+
wonder.damaged = false;
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
// ── Effect computation ────────────────────────────────────────────────────────
|
|
208
|
+
/**
|
|
209
|
+
* Compute the `WonderEffects` advisory bundle for a single wonder.
|
|
210
|
+
*
|
|
211
|
+
* Damaged wonders: each numeric field is scaled by `WONDER_DAMAGED_EFFECT_MUL / SCALE.Q`.
|
|
212
|
+
*/
|
|
213
|
+
export function computeWonderEffects(wonder) {
|
|
214
|
+
const base = WONDER_BASE_EFFECTS[wonder.type];
|
|
215
|
+
if (!wonder.damaged)
|
|
216
|
+
return { ...base };
|
|
217
|
+
const m = WONDER_DAMAGED_EFFECT_MUL;
|
|
218
|
+
return {
|
|
219
|
+
stabilityBonus_Q: mulDiv(base.stabilityBonus_Q, m, SCALE.Q),
|
|
220
|
+
moraleBonus_Q: mulDiv(base.moraleBonus_Q, m, SCALE.Q),
|
|
221
|
+
researchPointBonus: Math.round(base.researchPointBonus * m / SCALE.Q),
|
|
222
|
+
unrestReduction_Q: mulDiv(base.unrestReduction_Q, m, SCALE.Q),
|
|
223
|
+
tradeIncomeBonus_Q: mulDiv(base.tradeIncomeBonus_Q, m, SCALE.Q),
|
|
224
|
+
defenseBonus_Q: mulDiv(base.defenseBonus_Q, m, SCALE.Q),
|
|
225
|
+
epidemicResistance_Q: mulDiv(base.epidemicResistance_Q, m, SCALE.Q),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Aggregate effects from multiple wonders.
|
|
230
|
+
*
|
|
231
|
+
* Q fields are summed and clamped to SCALE.Q.
|
|
232
|
+
* `researchPointBonus` is summed without capping.
|
|
233
|
+
*/
|
|
234
|
+
export function aggregateWonderEffects(wonders) {
|
|
235
|
+
let stab = 0, morale = 0, rp = 0, unrest = 0, trade = 0, def = 0, epi = 0;
|
|
236
|
+
for (const w of wonders) {
|
|
237
|
+
const fx = computeWonderEffects(w);
|
|
238
|
+
stab += fx.stabilityBonus_Q;
|
|
239
|
+
morale += fx.moraleBonus_Q;
|
|
240
|
+
rp += fx.researchPointBonus;
|
|
241
|
+
unrest += fx.unrestReduction_Q;
|
|
242
|
+
trade += fx.tradeIncomeBonus_Q;
|
|
243
|
+
def += fx.defenseBonus_Q;
|
|
244
|
+
epi += fx.epidemicResistance_Q;
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
stabilityBonus_Q: clampQ(stab, 0, SCALE.Q),
|
|
248
|
+
moraleBonus_Q: clampQ(morale, 0, SCALE.Q),
|
|
249
|
+
researchPointBonus: rp,
|
|
250
|
+
unrestReduction_Q: clampQ(unrest, 0, SCALE.Q),
|
|
251
|
+
tradeIncomeBonus_Q: clampQ(trade, 0, SCALE.Q),
|
|
252
|
+
defenseBonus_Q: clampQ(def, 0, SCALE.Q),
|
|
253
|
+
epidemicResistance_Q: clampQ(epi, 0, SCALE.Q),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
257
|
+
/** Return `true` when the wonder is standing and undamaged. */
|
|
258
|
+
export function isWonderIntact(wonder) {
|
|
259
|
+
return !wonder.damaged;
|
|
260
|
+
}
|
|
261
|
+
/** Compute treasury cost to repair a damaged wonder [cu]. */
|
|
262
|
+
export function computeRepairCost(type) {
|
|
263
|
+
return Math.round(mulDiv(WONDER_BASE_COST_CU[type], WONDER_REPAIR_COST_FRAC, SCALE.Q));
|
|
264
|
+
}
|
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.46",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -158,6 +158,18 @@
|
|
|
158
158
|
"./containment": {
|
|
159
159
|
"import": "./dist/src/containment.js",
|
|
160
160
|
"types": "./dist/src/containment.d.ts"
|
|
161
|
+
},
|
|
162
|
+
"./mercenaries": {
|
|
163
|
+
"import": "./dist/src/mercenaries.js",
|
|
164
|
+
"types": "./dist/src/mercenaries.d.ts"
|
|
165
|
+
},
|
|
166
|
+
"./wonders": {
|
|
167
|
+
"import": "./dist/src/wonders.js",
|
|
168
|
+
"types": "./dist/src/wonders.d.ts"
|
|
169
|
+
},
|
|
170
|
+
"./monetary": {
|
|
171
|
+
"import": "./dist/src/monetary.js",
|
|
172
|
+
"types": "./dist/src/monetary.d.ts"
|
|
161
173
|
}
|
|
162
174
|
},
|
|
163
175
|
"files": [
|