@its-not-rocket-science/ananke 0.1.46 → 0.1.48
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 +34 -6
- package/dist/src/bridge/bridge-engine.js +0 -1
- package/dist/src/competence/acoustic.d.ts +1 -1
- package/dist/src/competence/acoustic.js +1 -7
- package/dist/src/competence/catalogue.js +1 -1
- package/dist/src/competence/engineering.js +0 -2
- package/dist/src/competence/framework.js +2 -4
- package/dist/src/competence/interspecies.js +0 -2
- package/dist/src/competence/language.js +0 -2
- package/dist/src/competence/naturalist.js +1 -1
- package/dist/src/crafting/index.d.ts +3 -2
- package/dist/src/crafting/index.js +17 -15
- package/dist/src/crafting/manufacturing.d.ts +13 -15
- package/dist/src/crafting/manufacturing.js +30 -21
- package/dist/src/crafting/materials.d.ts +1 -1
- package/dist/src/crafting/materials.js +14 -6
- package/dist/src/crafting/recipes.d.ts +1 -1
- package/dist/src/crafting/recipes.js +1 -1
- package/dist/src/crafting/workshops.js +8 -6
- package/dist/src/inventory.js +1 -6
- package/dist/src/item-durability.d.ts +1 -1
- package/dist/src/item-durability.js +1 -1
- package/dist/src/legend.d.ts +1 -1
- package/dist/src/legend.js +1 -1
- package/dist/src/modding.d.ts +11 -11
- package/dist/src/modding.js +10 -10
- package/dist/src/narrative-render.js +3 -3
- package/dist/src/quest-generators.js +4 -4
- package/dist/src/settlement-services.js +2 -2
- package/dist/src/settlement.d.ts +1 -1
- package/dist/src/settlement.js +2 -2
- package/dist/src/sim/ai/behavior-trees.d.ts +2 -2
- package/dist/src/sim/ai/behavior-trees.js +2 -2
- package/dist/src/sim/bodyplan.d.ts +1 -1
- package/dist/src/sim/kernel.js +1 -1
- package/dist/src/sim/step/morale.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.48] — 2026-03-28
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Crafting subsystem — TODO/placeholder items resolved:**
|
|
14
|
+
- `src/crafting/materials.ts` — `createMaterialItem`: corrected `mass_kg` (was double-scaled by `SCALE.kg`; now `quantity_kg * SCALE.kg / SCALE.Q`); `bulk` now computed proportionally from quantity instead of a fixed `q(1.0)` placeholder.
|
|
15
|
+
- `src/inventory.ts` — `findMaterialsByType`: replaced loose `templateId.includes(materialTypeId)` with exact `templateId === "material_" + materialTypeId` to prevent false positives (e.g. "iron" matching "iron_ore").
|
|
16
|
+
- `src/crafting/manufacturing.ts` — `ProductionLine` gains optional `workshopTimeReduction_Q` and `workshopQualityBonus_Q` fields; `setupProductionLine` now looks up the recipe and calls `getWorkshopBonus` to populate them; `advanceProduction` applies the time reduction to effective progress; `calculateBatchQualityRange` accepts an optional `workshopQualityBonus_Q` multiplier; `estimateBatchCompletionTime` accepts an optional `workshopTimeReduction_Q` and its formula is corrected (was dividing by SCALE.Q twice, producing near-zero results).
|
|
17
|
+
- `src/crafting/workshops.ts` — `upgradeWorkshop`: now checks that `resources` contains sufficient `material_wood` (10 units per tier step) before upgrading; returns `success: false` when insufficient rather than always succeeding.
|
|
18
|
+
- `src/crafting/index.ts` — `startManufacturing`: now returns the constructed `ProductionLine` in `result.productionLine` so callers can store it for subsequent `advanceManufacturing` calls (persistent state remains the host's responsibility); `advanceManufacturing` now derives quality range and time reduction from the supplied `workshop` rather than using hardcoded values.
|
|
19
|
+
- Build: clean. Tests: 5,261 passing. Coverage: statements 97.1 %, branches 87.83 %, functions 95.65 %, lines 97.1 %.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [0.1.47] — 2026-03-27
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Lint clean-up (zero issues)** — eliminated all 574 ESLint errors and warnings across `src/` and `test/`:
|
|
28
|
+
- Replaced all `as any` casts with proper types (`as Q`, `as TechEra`, `as WonderType`, `as unknown as TypeName`, etc.)
|
|
29
|
+
- Removed unused imports and prefixed unused locals with `_` across 50+ test files
|
|
30
|
+
- Fixed `getAvailableMaterials` TODO in `src/crafting/materials.ts` — now accepts `readonly Material[]` and derives per-type totals
|
|
31
|
+
- Removed `@ts-nocheck` from `as/injury.ts`; applied `const` fixes and removed dead imports throughout `src/`
|
|
32
|
+
- **UK English** — updated all comments, JSDoc, and documentation prose to British spelling (`armour`, `defence`, `behaviour`, `analyse`, `calibre`, `colour`); exported API identifiers unchanged
|
|
33
|
+
- Build: clean. Tests: 5,261 passing. Coverage: statements 97.12 %, branches 87.87 %, functions 95.65 %, lines 97.12 %.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
9
37
|
## [0.1.46] — 2026-03-27
|
|
10
38
|
|
|
11
39
|
### Added
|
|
@@ -41,7 +69,7 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
41
69
|
- `WonderEffects { stabilityBonus_Q, moraleBonus_Q, researchPointBonus, unrestReduction_Q, tradeIncomeBonus_Q, defenseBonus_Q, epidemicResistance_Q }` — advisory bundle.
|
|
42
70
|
- `WONDER_BASE_COST_CU`: grand_library 150k → great_pyramid 1,000k cu.
|
|
43
71
|
- `WONDER_TYPICAL_DAYS`: grand_library 180 → great_pyramid 3,650 days (10 years).
|
|
44
|
-
- `WONDER_BASE_EFFECTS`: distinct niches — great_wall highest
|
|
72
|
+
- `WONDER_BASE_EFFECTS`: distinct niches — great_wall highest defence (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
73
|
- `WONDER_DAMAGED_EFFECT_MUL = q(0.50)` — damaged wonders provide half effects.
|
|
46
74
|
- `WONDER_REPAIR_COST_FRAC = q(0.25)` — repair costs 25% of base construction cost.
|
|
47
75
|
- `createWonderProject(projectId, polityId, type, startTick)` — factory.
|
|
@@ -781,15 +809,15 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
781
809
|
- **CE-16 · Modding Support** (`src/modding.ts`)
|
|
782
810
|
- Layer 1 — `hashMod(json)`: deterministic FNV-1a fingerprint (8-char hex) for any
|
|
783
811
|
parsed JSON mod file; canonical key-sorted serialisation ensures order-independence.
|
|
784
|
-
- Layer 2 — Post-tick
|
|
812
|
+
- Layer 2 — Post-tick behaviour hooks: `registerPostTickHook / unregisterPostTickHook /
|
|
785
813
|
runPostTickHooks / listPostTickHooks / clearPostTickHooks`; hooks fire after
|
|
786
814
|
`stepWorld`, are purely observational (logging, analytics, renderer updates).
|
|
787
|
-
- Layer 3 — AI
|
|
815
|
+
- Layer 3 — AI behaviour node registry: `registerBehaviorNode / unregisterBehaviorNode /
|
|
788
816
|
getBehaviorNode / listBehaviorNodes / clearBehaviorNodes`; custom `BehaviorNode`
|
|
789
|
-
factories registered by id for scenario and
|
|
817
|
+
factories registered by id for scenario and behaviour-tree composition.
|
|
790
818
|
- Session fingerprint: `computeModManifest(catalogIds)` returns sorted id lists and a
|
|
791
819
|
single fingerprint covering all three layers for multiplayer client validation.
|
|
792
|
-
- `clearAllMods()` resets hooks and
|
|
820
|
+
- `clearAllMods()` resets hooks and behaviour nodes (catalog unchanged).
|
|
793
821
|
- 42 tests in `test/modding.test.ts`; exported via `src/index.ts`.
|
|
794
822
|
|
|
795
823
|
- **CE-14 · Socio-Economic Campaign Layer → Stable Promotion**
|
|
@@ -884,7 +912,7 @@ Adding new **optional** fields to these interfaces is never a breaking change.
|
|
|
884
912
|
|
|
885
913
|
- **CE-16 · Modding Support — HashMod, Post-tick Hooks, Behaviour Node Registry** (`src/parallel.ts`)
|
|
886
914
|
- Three-layer modding contract: FNV-1a data fingerprinting, observational
|
|
887
|
-
post-tick hooks, and named AI
|
|
915
|
+
post-tick hooks, and named AI behaviour node factories. computeModManifest()
|
|
888
916
|
provides a single session fingerprint for multiplayer client validation.
|
|
889
917
|
- exported via src/index.ts.
|
|
890
918
|
|
|
@@ -151,7 +151,6 @@ export class BridgeEngine {
|
|
|
151
151
|
// Render time at or after current snapshot
|
|
152
152
|
if (this.config.extrapolationAllowed) {
|
|
153
153
|
// Extrapolate forward using velocity
|
|
154
|
-
const delta = renderTime_s - this.currTime_s;
|
|
155
154
|
t = SCALE.Q;
|
|
156
155
|
fromTick = curr.tick;
|
|
157
156
|
toTick = curr.tick;
|
|
@@ -42,7 +42,7 @@ export interface AcousticDetectionOutcome {
|
|
|
42
42
|
* movementNoise = velocity_mps × 10
|
|
43
43
|
* equipmentNoise = based on armour material and weapon mass
|
|
44
44
|
*
|
|
45
|
-
* @param entity - The entity to
|
|
45
|
+
* @param entity - The entity to analyse.
|
|
46
46
|
* @returns Acoustic signature with all noise components.
|
|
47
47
|
*/
|
|
48
48
|
export declare function deriveAcousticSignature(entity: Entity): AcousticSignature;
|
|
@@ -10,8 +10,6 @@ import { SCALE, q, clampQ, mulDiv } from "../units.js";
|
|
|
10
10
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
11
11
|
/** Scale factor for acoustic detection formula. */
|
|
12
12
|
const SCALE_ACOUSTIC = 100; // multiplier to get useful detection ranges
|
|
13
|
-
/** Base detection range in metres at average musical intelligence. */
|
|
14
|
-
const BASE_DETECTION_RANGE_m = 50;
|
|
15
13
|
/** Maximum effective detection range. */
|
|
16
14
|
const MAX_DETECTION_RANGE_m = 500;
|
|
17
15
|
/** Range factor: clarity degrades with distance. */
|
|
@@ -23,11 +21,7 @@ const BASE_SIGNAL_LATENCY_MS = 200;
|
|
|
23
21
|
/** Latency reduction from high musical intelligence. */
|
|
24
22
|
const MUSICAL_LATENCY_REDUCTION_FACTOR = 0.5; // up to 50% faster
|
|
25
23
|
// Noise level constants
|
|
26
|
-
const NOISE_SILENT = 0;
|
|
27
|
-
const NOISE_VERY_QUIET = 10;
|
|
28
|
-
const NOISE_QUIET = 25;
|
|
29
24
|
const NOISE_NORMAL = 50;
|
|
30
|
-
const NOISE_LOUD = 75;
|
|
31
25
|
const NOISE_VERY_LOUD = 100;
|
|
32
26
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
33
27
|
/**
|
|
@@ -85,7 +79,7 @@ function calculateEquipmentNoise(entity) {
|
|
|
85
79
|
* movementNoise = velocity_mps × 10
|
|
86
80
|
* equipmentNoise = based on armour material and weapon mass
|
|
87
81
|
*
|
|
88
|
-
* @param entity - The entity to
|
|
82
|
+
* @param entity - The entity to analyse.
|
|
89
83
|
* @returns Acoustic signature with all noise components.
|
|
90
84
|
*/
|
|
91
85
|
export function deriveAcousticSignature(entity) {
|
|
@@ -181,7 +181,7 @@ const entries = [
|
|
|
181
181
|
domain: "logicalMathematical",
|
|
182
182
|
difficulty_Q: q(0.55),
|
|
183
183
|
timeBase_s: 1800, // 30 minutes
|
|
184
|
-
description: "
|
|
184
|
+
description: "Analyse tactical situation and propose solution",
|
|
185
185
|
},
|
|
186
186
|
// ── Musical (performance, signaling) ─────────────────────────────────────────
|
|
187
187
|
{
|
|
@@ -13,8 +13,6 @@ const BASE_QUALITY_MUL = q(0.70);
|
|
|
13
13
|
const MAX_QUALITY_MUL = q(1.20);
|
|
14
14
|
/** Minimum quality multiplier for success. */
|
|
15
15
|
const MIN_SUCCESS_QUALITY = q(0.30);
|
|
16
|
-
/** Time factor: rushed work reduces quality. */
|
|
17
|
-
const TIME_FACTOR_BASE = 1.0;
|
|
18
16
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
19
17
|
/**
|
|
20
18
|
* Resolve an engineering project.
|
|
@@ -31,8 +31,6 @@ const QUALITY_XP_MULTIPLIERS = {
|
|
|
31
31
|
};
|
|
32
32
|
/** Difficulty XP bonus: harder tasks grant more XP. */
|
|
33
33
|
const MAX_DIFFICULTY_BONUS = 15;
|
|
34
|
-
/** Tool quality bonus lookup (default if tool not specified). */
|
|
35
|
-
const DEFAULT_TOOL_BONUS = q(1.0);
|
|
36
34
|
// ── Helper Functions ──────────────────────────────────────────────────────────
|
|
37
35
|
/**
|
|
38
36
|
* Map domain-specific descriptors to canonical descriptors.
|
|
@@ -279,7 +277,7 @@ function resolveNaturalist(actor, task, action) {
|
|
|
279
277
|
/**
|
|
280
278
|
* Resolve inter-species tasks.
|
|
281
279
|
*/
|
|
282
|
-
function resolveInterSpecies(actor, task, action
|
|
280
|
+
function resolveInterSpecies(actor, task, action) {
|
|
283
281
|
const seed = action.seed;
|
|
284
282
|
const spec = {
|
|
285
283
|
targetSpecies: "unknown",
|
|
@@ -647,7 +645,7 @@ export function resolveCompetence(actor, action, world) {
|
|
|
647
645
|
result = resolveNaturalist(actor, task, action);
|
|
648
646
|
break;
|
|
649
647
|
case "interSpecies":
|
|
650
|
-
result = resolveInterSpecies(actor, task, action
|
|
648
|
+
result = resolveInterSpecies(actor, task, action);
|
|
651
649
|
break;
|
|
652
650
|
case "linguistic":
|
|
653
651
|
result = resolveLinguistic(actor, task, action, world);
|
|
@@ -11,8 +11,6 @@ import { makeRng } from "../rng.js";
|
|
|
11
11
|
const MAX_LATENCY_PENALTY_MS = 80 * SCALE.s / 1000; // 80ms in Q-units
|
|
12
12
|
/** Scaling factor: penalty = (1.0 − interSpecies) × MAX_LATENCY_PENALTY_MS. */
|
|
13
13
|
const LATENCY_PENALTY_SCALE = MAX_LATENCY_PENALTY_MS;
|
|
14
|
-
/** Base success probability for signaling without vocabulary. */
|
|
15
|
-
const SIGNAL_BASE_PROBABILITY = q(0.10);
|
|
16
14
|
/** Aggravation threshold: low empathy + high fear → possible hostile reaction. */
|
|
17
15
|
const AGGRAVATION_THRESHOLD = q(0.30);
|
|
18
16
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
// No kernel import — pure resolution module.
|
|
7
7
|
import { SCALE, q, clampQ, qMul, mulDiv } from "../units.js";
|
|
8
8
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
9
|
-
/** Base reception rate at linguistic q(0.50) for formation size 1. */
|
|
10
|
-
const BASE_RECEPTION_RATE = q(0.70);
|
|
11
9
|
/** Maximum formation size before reception penalties apply. */
|
|
12
10
|
const OPTIMAL_FORMATION_SIZE = 10;
|
|
13
11
|
/** Base delay divisor: linguistic × BASE_DELAY_DIVISOR. */
|
|
@@ -162,7 +162,7 @@ export function resolveTaming(entity, spec, seed) {
|
|
|
162
162
|
const fearPenalty = mulDiv(spec.animalFearQ, q(0.50), SCALE.Q);
|
|
163
163
|
// Prior successes bonus: +5% per success, max +25%
|
|
164
164
|
const experienceBonus = Math.min(spec.priorSuccesses * q(0.05), q(0.25));
|
|
165
|
-
|
|
165
|
+
const trust_Q = clampQ((baseTrust - fearPenalty + experienceBonus), q(0), q(1.0));
|
|
166
166
|
// Attack check: high fear with low trust is dangerous
|
|
167
167
|
// P(attack) = max(0, (animalFearQ − trust_Q) × 0.30)
|
|
168
168
|
const fearFloat = mulDiv(spec.animalFearQ, q(1.0), SCALE.Q);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Q } from "../units.js";
|
|
2
2
|
import type { Entity } from "../sim/entity.js";
|
|
3
3
|
import type { Inventory } from "../inventory.js";
|
|
4
|
+
import type { ItemBase } from "../equipment.js";
|
|
4
5
|
import type { Recipe } from "./recipes.js";
|
|
5
6
|
import type { Material } from "./materials.js";
|
|
6
7
|
import type { WorkshopInstance } from "./workshops.js";
|
|
@@ -22,6 +23,7 @@ export declare function startManufacturing(recipeId: string, quantity: number, w
|
|
|
22
23
|
success: boolean;
|
|
23
24
|
lineId?: string;
|
|
24
25
|
error?: string;
|
|
26
|
+
productionLine?: ProductionLine;
|
|
25
27
|
};
|
|
26
28
|
/**
|
|
27
29
|
* Advance manufacturing for a production line.
|
|
@@ -41,8 +43,7 @@ export declare function getAvailableRecipes(entity: Entity, inventory: Inventory
|
|
|
41
43
|
* Get material properties for a crafted item.
|
|
42
44
|
* Applies material property modifiers to base item stats.
|
|
43
45
|
*/
|
|
44
|
-
export declare function applyMaterialProperties(baseItem:
|
|
45
|
-
material: Material): MaterialPropertyModifier;
|
|
46
|
+
export declare function applyMaterialProperties(baseItem: ItemBase, material: Material): MaterialPropertyModifier;
|
|
46
47
|
/**
|
|
47
48
|
* Integrate crafting result into inventory.
|
|
48
49
|
* Consumes ingredients and adds crafted item.
|
|
@@ -8,7 +8,7 @@ import { consumeItemsByTemplateId, addItemToInventory } from "../inventory.js";
|
|
|
8
8
|
import { validateRecipeFeasibility, resolveRecipe, getRecipeById, getAllRecipes, } from "./recipes.js";
|
|
9
9
|
import { getMaterialTypeById, calculateMaterialEffect, createMaterialItem, } from "./materials.js";
|
|
10
10
|
import { getWorkshopBonus, validateWorkshopRequirements, createWorkshop, upgradeWorkshop, } from "./workshops.js";
|
|
11
|
-
import { setupProductionLine, advanceProduction, estimateBatchCompletionTime, isProductionLineComplete, } from "./manufacturing.js";
|
|
11
|
+
import { setupProductionLine, advanceProduction, calculateBatchQualityRange, estimateBatchCompletionTime, isProductionLineComplete, } from "./manufacturing.js";
|
|
12
12
|
// ── Main Crafting API ─────────────────────────────────────────────────────────
|
|
13
13
|
/**
|
|
14
14
|
* Craft a single item using a recipe, entity, inventory, and workshop.
|
|
@@ -107,8 +107,8 @@ export function startManufacturing(recipeId, quantity, workshop, workers, worldS
|
|
|
107
107
|
if (!skilledWorkerExists) {
|
|
108
108
|
return { success: false, error: "No worker meets skill requirements" };
|
|
109
109
|
}
|
|
110
|
-
const
|
|
111
|
-
const
|
|
110
|
+
const _primaryWorkerId = workers.length > 0 ? workers[0].id : 0;
|
|
111
|
+
const _lineSeed = eventSeed(worldSeed, tick, _primaryWorkerId, hashString(recipe.id), salt);
|
|
112
112
|
const orderId = `order_${worldSeed}_${tick}_${recipe.id}_${salt}`;
|
|
113
113
|
// Create manufacturing order
|
|
114
114
|
const order = {
|
|
@@ -117,10 +117,9 @@ export function startManufacturing(recipeId, quantity, workshop, workers, worldS
|
|
|
117
117
|
quantity,
|
|
118
118
|
workshop,
|
|
119
119
|
};
|
|
120
|
-
// Setup production line
|
|
121
|
-
const line = setupProductionLine(order, workers
|
|
122
|
-
|
|
123
|
-
return { success: true, lineId: line.lineId };
|
|
120
|
+
// Setup production line (persistent storage is the host's responsibility)
|
|
121
|
+
const line = setupProductionLine(order, workers);
|
|
122
|
+
return { success: true, lineId: line.lineId, productionLine: line };
|
|
124
123
|
}
|
|
125
124
|
/**
|
|
126
125
|
* Advance manufacturing for a production line.
|
|
@@ -131,20 +130,24 @@ export function advanceManufacturing(lineId, deltaTime_s, workers, workshop, wor
|
|
|
131
130
|
let lineIdHash = 0;
|
|
132
131
|
for (let i = 0; i < lineId.length; i++)
|
|
133
132
|
lineIdHash += lineId.charCodeAt(i);
|
|
134
|
-
const
|
|
135
|
-
//
|
|
133
|
+
const _seed = eventSeed(worldSeed, tick, lineIdHash, 0, salt);
|
|
134
|
+
// Retrieve production line from persistent state (host responsibility).
|
|
135
|
+
// Without a stored line, compute a fresh one using the available workshop and workers.
|
|
136
|
+
const workshopBonus = getWorkshopBonus(workshop, { toolRequirements: [] });
|
|
137
|
+
const qualityRange = calculateBatchQualityRange(workers, workshopBonus.qualityBonus_Q);
|
|
136
138
|
const line = {
|
|
137
139
|
lineId,
|
|
138
|
-
recipeId: "
|
|
140
|
+
recipeId: "unknown",
|
|
139
141
|
batchSize: 10,
|
|
140
142
|
itemsProduced: 0,
|
|
141
143
|
progress_Q: q(0),
|
|
142
144
|
assignedWorkers: workers.map(w => w.id),
|
|
143
145
|
priority: 1,
|
|
144
|
-
qualityRange
|
|
146
|
+
qualityRange,
|
|
147
|
+
workshopTimeReduction_Q: workshopBonus.timeReduction_Q,
|
|
148
|
+
workshopQualityBonus_Q: workshopBonus.qualityBonus_Q,
|
|
145
149
|
};
|
|
146
|
-
const result = advanceProduction(line, deltaTime_s, workers
|
|
147
|
-
// TODO: update stored line
|
|
150
|
+
const result = advanceProduction(line, deltaTime_s, workers);
|
|
148
151
|
return {
|
|
149
152
|
itemsCompleted: result.itemsCompleted,
|
|
150
153
|
totalProduced: result.totalItemsProduced,
|
|
@@ -171,8 +174,7 @@ export function getAvailableRecipes(entity, inventory, workshop) {
|
|
|
171
174
|
* Get material properties for a crafted item.
|
|
172
175
|
* Applies material property modifiers to base item stats.
|
|
173
176
|
*/
|
|
174
|
-
export function applyMaterialProperties(baseItem,
|
|
175
|
-
material) {
|
|
177
|
+
export function applyMaterialProperties(baseItem, material) {
|
|
176
178
|
return calculateMaterialEffect(baseItem, material);
|
|
177
179
|
}
|
|
178
180
|
// ── Integration with Existing Systems ─────────────────────────────────────────
|
|
@@ -3,6 +3,11 @@ import type { Entity } from "../sim/entity.js";
|
|
|
3
3
|
import type { Recipe } from "./recipes.js";
|
|
4
4
|
import type { WorkshopInstance } from "./workshops.js";
|
|
5
5
|
/** Production line state for batch manufacturing. */
|
|
6
|
+
export type ProductQualityRange = {
|
|
7
|
+
min_Q: Q;
|
|
8
|
+
max_Q: Q;
|
|
9
|
+
avg_Q: Q;
|
|
10
|
+
};
|
|
6
11
|
export interface ProductionLine {
|
|
7
12
|
lineId: string;
|
|
8
13
|
recipeId: string;
|
|
@@ -11,11 +16,9 @@ export interface ProductionLine {
|
|
|
11
16
|
progress_Q: Q;
|
|
12
17
|
assignedWorkers: number[];
|
|
13
18
|
priority: number;
|
|
14
|
-
qualityRange:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
avg_Q: Q;
|
|
18
|
-
};
|
|
19
|
+
qualityRange: ProductQualityRange;
|
|
20
|
+
workshopTimeReduction_Q?: Q;
|
|
21
|
+
workshopQualityBonus_Q?: Q;
|
|
19
22
|
}
|
|
20
23
|
/** Manufacturing order for starting batch production. */
|
|
21
24
|
export interface ManufacturingOrder {
|
|
@@ -30,11 +33,7 @@ export interface ProductionAdvanceResult {
|
|
|
30
33
|
itemsCompleted: number;
|
|
31
34
|
totalItemsProduced: number;
|
|
32
35
|
progress_Q: Q;
|
|
33
|
-
qualityRange:
|
|
34
|
-
min_Q: Q;
|
|
35
|
-
max_Q: Q;
|
|
36
|
-
avg_Q: Q;
|
|
37
|
-
};
|
|
36
|
+
qualityRange: ProductQualityRange;
|
|
38
37
|
}
|
|
39
38
|
/** Assembly step for multi-stage crafting. */
|
|
40
39
|
export interface AssemblyStep {
|
|
@@ -47,19 +46,18 @@ export interface AssemblyStep {
|
|
|
47
46
|
/**
|
|
48
47
|
* Initialize a production line for batch manufacturing.
|
|
49
48
|
*/
|
|
50
|
-
export declare function setupProductionLine(order: ManufacturingOrder, workers: Entity[]
|
|
49
|
+
export declare function setupProductionLine(order: ManufacturingOrder, workers: Entity[]): ProductionLine;
|
|
51
50
|
/**
|
|
52
51
|
* Advance production line by deltaTime seconds.
|
|
53
52
|
* Progress accumulates based on number of workers and their skills.
|
|
54
53
|
* Returns new items completed and updated progress.
|
|
55
54
|
*/
|
|
56
|
-
export declare function advanceProduction(productionLine: ProductionLine, deltaTime_s: number, workers: Entity[]
|
|
57
|
-
workshop: WorkshopInstance, seed: number): ProductionAdvanceResult;
|
|
55
|
+
export declare function advanceProduction(productionLine: ProductionLine, deltaTime_s: number, workers: Entity[]): ProductionAdvanceResult;
|
|
58
56
|
/**
|
|
59
57
|
* Calculate predicted quality range for a batch based on workers, materials, workshop.
|
|
60
58
|
* Returns min, max, and average expected quality.
|
|
61
59
|
*/
|
|
62
|
-
export declare function calculateBatchQualityRange(workers: Entity[],
|
|
60
|
+
export declare function calculateBatchQualityRange(workers: Entity[], workshopQualityBonus_Q?: Q): {
|
|
63
61
|
min_Q: Q;
|
|
64
62
|
max_Q: Q;
|
|
65
63
|
avg_Q: Q;
|
|
@@ -76,7 +74,7 @@ export declare function advanceAssemblyStep(step: AssemblyStep, worker: Entity,
|
|
|
76
74
|
completed: boolean;
|
|
77
75
|
};
|
|
78
76
|
/** Estimate time to complete a batch given workers and workshop. */
|
|
79
|
-
export declare function estimateBatchCompletionTime(batchSize: number, workers: Entity[],
|
|
77
|
+
export declare function estimateBatchCompletionTime(batchSize: number, workers: Entity[], workshopTimeReduction_Q?: Q): number;
|
|
80
78
|
/** Check if production line is complete. */
|
|
81
79
|
export declare function isProductionLineComplete(line: ProductionLine): boolean;
|
|
82
80
|
/** Get progress percentage (0–1). */
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// Batch production lines, progress accumulation, quality variance.
|
|
4
4
|
// Deterministic batch quality range based on workers, materials, workshop.
|
|
5
5
|
import { SCALE, q, clampQ, qMul, mulDiv } from "../units.js";
|
|
6
|
+
import { getWorkshopBonus } from "./workshops.js";
|
|
7
|
+
import { getRecipeById } from "./recipes.js";
|
|
6
8
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
7
|
-
/** Default progress per worker per second (Q units). */
|
|
8
|
-
const PROGRESS_PER_WORKER_PER_SECOND = q(0.001);
|
|
9
9
|
/** Quality variance factor based on number of workers (more workers → less variance). */
|
|
10
10
|
const WORKER_VARIANCE_REDUCTION = q(0.10);
|
|
11
11
|
/** Minimum quality variance per batch. */
|
|
@@ -14,9 +14,13 @@ const MIN_QUALITY_VARIANCE = q(0.05);
|
|
|
14
14
|
/**
|
|
15
15
|
* Initialize a production line for batch manufacturing.
|
|
16
16
|
*/
|
|
17
|
-
export function setupProductionLine(order, workers
|
|
17
|
+
export function setupProductionLine(order, workers) {
|
|
18
|
+
const recipe = getRecipeById(order.recipeId);
|
|
19
|
+
const workshopBonus = recipe
|
|
20
|
+
? getWorkshopBonus(order.workshop, recipe)
|
|
21
|
+
: { toolBonus_Q: q(0), timeReduction_Q: q(1.0), qualityBonus_Q: q(1.0) };
|
|
18
22
|
const workerIds = workers.map(w => w.id);
|
|
19
|
-
const qualityRange = calculateBatchQualityRange(workers,
|
|
23
|
+
const qualityRange = calculateBatchQualityRange(workers, workshopBonus.qualityBonus_Q);
|
|
20
24
|
return {
|
|
21
25
|
lineId: `line_${order.orderId}`,
|
|
22
26
|
recipeId: order.recipeId,
|
|
@@ -26,6 +30,8 @@ export function setupProductionLine(order, workers, seed) {
|
|
|
26
30
|
assignedWorkers: workerIds,
|
|
27
31
|
priority: 1,
|
|
28
32
|
qualityRange,
|
|
33
|
+
workshopTimeReduction_Q: workshopBonus.timeReduction_Q,
|
|
34
|
+
workshopQualityBonus_Q: workshopBonus.qualityBonus_Q,
|
|
29
35
|
};
|
|
30
36
|
}
|
|
31
37
|
/**
|
|
@@ -33,8 +39,7 @@ export function setupProductionLine(order, workers, seed) {
|
|
|
33
39
|
* Progress accumulates based on number of workers and their skills.
|
|
34
40
|
* Returns new items completed and updated progress.
|
|
35
41
|
*/
|
|
36
|
-
export function advanceProduction(productionLine, deltaTime_s, workers
|
|
37
|
-
workshop, seed) {
|
|
42
|
+
export function advanceProduction(productionLine, deltaTime_s, workers) {
|
|
38
43
|
if (productionLine.itemsProduced >= productionLine.batchSize) {
|
|
39
44
|
return {
|
|
40
45
|
itemsCompleted: 0,
|
|
@@ -51,8 +56,11 @@ workshop, seed) {
|
|
|
51
56
|
const workerProgress = mulDiv(skill, deltaTime_s * SCALE.Q, 3600);
|
|
52
57
|
totalProgress += workerProgress;
|
|
53
58
|
}
|
|
54
|
-
// Apply workshop time reduction
|
|
55
|
-
const
|
|
59
|
+
// Apply workshop time reduction: timeReduction_Q < SCALE.Q means faster production
|
|
60
|
+
const timeReduction = productionLine.workshopTimeReduction_Q ?? SCALE.Q;
|
|
61
|
+
const effectiveProgress = timeReduction > 0
|
|
62
|
+
? Math.round(totalProgress * SCALE.Q / timeReduction)
|
|
63
|
+
: totalProgress;
|
|
56
64
|
// Advance progress
|
|
57
65
|
let newProgress = productionLine.progress_Q + effectiveProgress;
|
|
58
66
|
let itemsCompleted = 0;
|
|
@@ -64,8 +72,7 @@ workshop, seed) {
|
|
|
64
72
|
}
|
|
65
73
|
productionLine.progress_Q = clampQ(newProgress, q(0), SCALE.Q);
|
|
66
74
|
// Update quality range based on remaining workers (variance reduces as more items produced)
|
|
67
|
-
const
|
|
68
|
-
const updatedRange = updateQualityRange(productionLine.qualityRange, workers, remainingItems, seed);
|
|
75
|
+
const updatedRange = updateQualityRange(productionLine.qualityRange);
|
|
69
76
|
return {
|
|
70
77
|
itemsCompleted,
|
|
71
78
|
totalItemsProduced: productionLine.itemsProduced,
|
|
@@ -77,7 +84,7 @@ workshop, seed) {
|
|
|
77
84
|
* Calculate predicted quality range for a batch based on workers, materials, workshop.
|
|
78
85
|
* Returns min, max, and average expected quality.
|
|
79
86
|
*/
|
|
80
|
-
export function calculateBatchQualityRange(workers,
|
|
87
|
+
export function calculateBatchQualityRange(workers, workshopQualityBonus_Q = q(1.0)) {
|
|
81
88
|
if (workers.length === 0) {
|
|
82
89
|
return { min_Q: q(0), max_Q: q(0), avg_Q: q(0) };
|
|
83
90
|
}
|
|
@@ -87,10 +94,8 @@ export function calculateBatchQualityRange(workers, workshop, seed) {
|
|
|
87
94
|
totalSkill += worker.attributes.cognition?.bodilyKinesthetic ?? q(0.50);
|
|
88
95
|
}
|
|
89
96
|
const avgSkill = totalSkill / workers.length;
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
// Base average quality = avgSkill × workshopBonus
|
|
93
|
-
const avg_Q = clampQ(qMul(avgSkill, workshopBonus), q(0), SCALE.Q);
|
|
97
|
+
// Base average quality = avgSkill × workshopQualityBonus
|
|
98
|
+
const avg_Q = clampQ(qMul(avgSkill, workshopQualityBonus_Q), q(0), SCALE.Q);
|
|
94
99
|
// Variance decreases with more workers
|
|
95
100
|
const variance = Math.max(MIN_QUALITY_VARIANCE, q(0.20) - mulDiv(WORKER_VARIANCE_REDUCTION, workers.length, 1));
|
|
96
101
|
const min_Q = clampQ((avg_Q - variance), q(0), SCALE.Q);
|
|
@@ -98,7 +103,7 @@ export function calculateBatchQualityRange(workers, workshop, seed) {
|
|
|
98
103
|
return { min_Q, max_Q, avg_Q };
|
|
99
104
|
}
|
|
100
105
|
/** Update quality range as production progresses (variance may change). */
|
|
101
|
-
function updateQualityRange(currentRange
|
|
106
|
+
function updateQualityRange(currentRange) {
|
|
102
107
|
// For simplicity, keep range constant; could adjust based on worker fatigue, etc.
|
|
103
108
|
return currentRange;
|
|
104
109
|
}
|
|
@@ -139,19 +144,23 @@ export function advanceAssemblyStep(step, worker, deltaTime_s, availableTools) {
|
|
|
139
144
|
}
|
|
140
145
|
// ── Utility Functions ────────────────────────────────────────────────────────
|
|
141
146
|
/** Estimate time to complete a batch given workers and workshop. */
|
|
142
|
-
export function estimateBatchCompletionTime(batchSize, workers,
|
|
147
|
+
export function estimateBatchCompletionTime(batchSize, workers, workshopTimeReduction_Q = q(1.0)) {
|
|
143
148
|
if (workers.length === 0)
|
|
144
149
|
return Infinity;
|
|
150
|
+
if (batchSize === 0)
|
|
151
|
+
return 0;
|
|
145
152
|
let totalSkill = 0;
|
|
146
153
|
for (const worker of workers) {
|
|
147
154
|
totalSkill += worker.attributes.cognition?.bodilyKinesthetic ?? q(0.50);
|
|
148
155
|
}
|
|
149
156
|
const avgSkill = totalSkill / workers.length;
|
|
150
|
-
|
|
157
|
+
if (avgSkill <= 0)
|
|
158
|
+
return Infinity;
|
|
159
|
+
// At skill q(1.0), one item takes baseTimePerItem_s seconds.
|
|
160
|
+
// With time reduction q(0.90), items take 90% as long (10% faster).
|
|
151
161
|
const baseTimePerItem_s = 3600;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
return effectiveTimePerItem * batchSize / SCALE.Q;
|
|
162
|
+
const timePerItem_s = Math.round(baseTimePerItem_s * workshopTimeReduction_Q / avgSkill);
|
|
163
|
+
return timePerItem_s * batchSize;
|
|
155
164
|
}
|
|
156
165
|
/** Check if production line is complete. */
|
|
157
166
|
export function isProductionLineComplete(line) {
|
|
@@ -43,7 +43,7 @@ material: Material): MaterialPropertyModifier;
|
|
|
43
43
|
* Extract materials from inventory items that are of kind "material".
|
|
44
44
|
* Returns map of materialTypeId to total quantity (kg) and average quality.
|
|
45
45
|
*/
|
|
46
|
-
export declare function getAvailableMaterials(
|
|
46
|
+
export declare function getAvailableMaterials(materials: readonly Material[]): Map<string, {
|
|
47
47
|
totalKg: number;
|
|
48
48
|
avgQuality_Q: Q;
|
|
49
49
|
}>;
|
|
@@ -74,7 +74,6 @@ material) {
|
|
|
74
74
|
if (!materialType) {
|
|
75
75
|
return {}; // No effect for unknown material
|
|
76
76
|
}
|
|
77
|
-
const qualityFactor = material.quality_Q / SCALE.Q; // 0–1
|
|
78
77
|
const modifiers = {};
|
|
79
78
|
// Strength affects durability and damage
|
|
80
79
|
modifiers.durabilityMul = clampQ(Math.round(q(0.80) + mulDiv(materialType.strength_Q, q(0.40), SCALE.Q)), q(0.50), q(1.50));
|
|
@@ -92,10 +91,19 @@ material) {
|
|
|
92
91
|
* Extract materials from inventory items that are of kind "material".
|
|
93
92
|
* Returns map of materialTypeId to total quantity (kg) and average quality.
|
|
94
93
|
*/
|
|
95
|
-
export function getAvailableMaterials(
|
|
94
|
+
export function getAvailableMaterials(materials) {
|
|
96
95
|
const map = new Map();
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
for (const mat of materials) {
|
|
97
|
+
const existing = map.get(mat.materialTypeId);
|
|
98
|
+
if (existing) {
|
|
99
|
+
const newTotalKg = existing.totalKg + mat.quantity_kg;
|
|
100
|
+
const avgQuality_Q = Math.round((existing.avgQuality_Q * existing.totalKg + mat.quality_Q * mat.quantity_kg) / newTotalKg);
|
|
101
|
+
map.set(mat.materialTypeId, { totalKg: newTotalKg, avgQuality_Q });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
map.set(mat.materialTypeId, { totalKg: mat.quantity_kg, avgQuality_Q: mat.quality_Q });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
99
107
|
return map;
|
|
100
108
|
}
|
|
101
109
|
// ── Utility Functions ────────────────────────────────────────────────────────
|
|
@@ -111,8 +119,8 @@ export function createMaterialItem(materialTypeId, quality_Q, quantity_kg, itemI
|
|
|
111
119
|
id: itemId,
|
|
112
120
|
kind: "material",
|
|
113
121
|
name: displayName,
|
|
114
|
-
mass_kg: Math.round(quantity_kg * SCALE.kg
|
|
115
|
-
bulk: q(
|
|
122
|
+
mass_kg: Math.round(quantity_kg * SCALE.kg / SCALE.Q),
|
|
123
|
+
bulk: clampQ(Math.round(quantity_kg / 10), q(0.05), 5 * SCALE.Q),
|
|
116
124
|
materialTypeId,
|
|
117
125
|
quality_Q,
|
|
118
126
|
quantity_kg,
|
|
@@ -60,7 +60,7 @@ export declare function validateRecipeFeasibility(recipe: Recipe, inventory: Inv
|
|
|
60
60
|
* Calculate crafting cost (time, material consumption) based on recipe, materials, and workshop bonuses.
|
|
61
61
|
*/
|
|
62
62
|
export declare function calculateCraftingCost(recipe: Recipe, materialQualities: Map<string, Q>, // itemId -> quality_Q
|
|
63
|
-
workshopTimeReduction_Q?: Q
|
|
63
|
+
workshopTimeReduction_Q?: Q): {
|
|
64
64
|
time_s: number;
|
|
65
65
|
materialQualityAvg_Q: Q;
|
|
66
66
|
};
|
|
@@ -116,7 +116,7 @@ function getEntitySkill(entity, skillType) {
|
|
|
116
116
|
* Calculate crafting cost (time, material consumption) based on recipe, materials, and workshop bonuses.
|
|
117
117
|
*/
|
|
118
118
|
export function calculateCraftingCost(recipe, materialQualities, // itemId -> quality_Q
|
|
119
|
-
workshopTimeReduction_Q = q(1.0)
|
|
119
|
+
workshopTimeReduction_Q = q(1.0)) {
|
|
120
120
|
// Average material quality (default q(0.50))
|
|
121
121
|
let totalQuality = 0;
|
|
122
122
|
let count = 0;
|
|
@@ -125,17 +125,19 @@ targetLevel) {
|
|
|
125
125
|
if (targetTier <= currentTier) {
|
|
126
126
|
return { success: false, upgradedWorkshop: workshop, consumedResources: new Map() };
|
|
127
127
|
}
|
|
128
|
-
//
|
|
129
|
-
|
|
128
|
+
// Check resource requirements: 10 units of "material_wood" per tier step
|
|
129
|
+
const tierSteps = targetTier - currentTier;
|
|
130
|
+
const woodRequired = 10 * tierSteps;
|
|
131
|
+
const woodAvailable = resources.get("material_wood") ?? 0;
|
|
132
|
+
if (woodAvailable < woodRequired) {
|
|
133
|
+
return { success: false, upgradedWorkshop: workshop, consumedResources: new Map() };
|
|
134
|
+
}
|
|
130
135
|
const upgradedWorkshop = {
|
|
131
136
|
...workshop,
|
|
132
137
|
facilityLevel: targetLevel,
|
|
133
138
|
};
|
|
134
|
-
// Consume resources (placeholder)
|
|
135
139
|
const consumedResources = new Map();
|
|
136
|
-
|
|
137
|
-
const tierSteps = targetTier - currentTier;
|
|
138
|
-
consumedResources.set("material_wood", 10 * tierSteps);
|
|
140
|
+
consumedResources.set("material_wood", woodRequired);
|
|
139
141
|
return { success: true, upgradedWorkshop, consumedResources };
|
|
140
142
|
}
|
|
141
143
|
// ── Workshop Creation ─────────────────────────────────────────────────────────
|
package/dist/src/inventory.js
CHANGED
|
@@ -381,12 +381,7 @@ export function findMaterialsByType(inventory, materialTypeId) {
|
|
|
381
381
|
const results = [];
|
|
382
382
|
// Helper to check and add
|
|
383
383
|
const checkItem = (item) => {
|
|
384
|
-
|
|
385
|
-
// Since ItemInstance doesn't have materialTypeId, we need to rely on the templateId mapping
|
|
386
|
-
// or assume that the item is a Material (which extends ItemBase).
|
|
387
|
-
// For now, we'll assume that templateId indicates material type (e.g., "material_iron").
|
|
388
|
-
// This is a placeholder; we need to integrate with crafting material system.
|
|
389
|
-
if (item.templateId.startsWith("material_") && item.templateId.includes(materialTypeId)) {
|
|
384
|
+
if (item.templateId === `material_${materialTypeId}`) {
|
|
390
385
|
results.push(item);
|
|
391
386
|
}
|
|
392
387
|
};
|
|
@@ -62,7 +62,7 @@ export declare function calculateRepairNeed(item: ItemInstance): {
|
|
|
62
62
|
* @param toolQuality_Q Quality of tools being used (0-1)
|
|
63
63
|
* @param seed RNG seed for determinism
|
|
64
64
|
*/
|
|
65
|
-
export declare function resolveRepair(item: ItemInstance, crafterCognitionQ: Q, toolQuality_Q: Q,
|
|
65
|
+
export declare function resolveRepair(item: ItemInstance, crafterCognitionQ: Q, toolQuality_Q: Q, _seed: number): RepairResult;
|
|
66
66
|
/** Quick repair with no skill check (field repair). */
|
|
67
67
|
export declare function fieldRepair(item: ItemInstance): RepairResult;
|
|
68
68
|
/** Calculate item value based on durability and modifications. */
|
|
@@ -161,7 +161,7 @@ export function calculateRepairNeed(item) {
|
|
|
161
161
|
* @param toolQuality_Q Quality of tools being used (0-1)
|
|
162
162
|
* @param seed RNG seed for determinism
|
|
163
163
|
*/
|
|
164
|
-
export function resolveRepair(item, crafterCognitionQ, toolQuality_Q,
|
|
164
|
+
export function resolveRepair(item, crafterCognitionQ, toolQuality_Q, _seed) {
|
|
165
165
|
if (!hasDurability(item)) {
|
|
166
166
|
return {
|
|
167
167
|
success: false,
|
package/dist/src/legend.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export declare function createLegendRegistry(): LegendRegistry;
|
|
|
62
62
|
export declare function registerLegend(registry: LegendRegistry, legend: Legend): void;
|
|
63
63
|
/** Get all legends about a specific entity. */
|
|
64
64
|
export declare function getLegendsBySubject(registry: LegendRegistry, subjectId: number): Legend[];
|
|
65
|
-
/** Derive NPC-
|
|
65
|
+
/** Derive NPC-behaviour modifiers from a legend. */
|
|
66
66
|
export declare function getLegendEffect(legend: Legend): LegendEffect;
|
|
67
67
|
/**
|
|
68
68
|
* Determine whether an NPC "knows" a legend.
|
package/dist/src/legend.js
CHANGED
|
@@ -141,7 +141,7 @@ export function getLegendsBySubject(registry, subjectId) {
|
|
|
141
141
|
.filter((l) => l !== undefined);
|
|
142
142
|
}
|
|
143
143
|
// ── Effects ───────────────────────────────────────────────────────────────────
|
|
144
|
-
/** Derive NPC-
|
|
144
|
+
/** Derive NPC-behaviour modifiers from a legend. */
|
|
145
145
|
export function getLegendEffect(legend) {
|
|
146
146
|
const f = legend.fame_Q;
|
|
147
147
|
switch (legend.reputation) {
|
package/dist/src/modding.d.ts
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* mod file. The network replication layer (CE-11) compares fingerprints across clients
|
|
9
9
|
* to guarantee all participants use identical mod definitions.
|
|
10
10
|
*
|
|
11
|
-
* **Layer 2 — Post-tick
|
|
11
|
+
* **Layer 2 — Post-tick behaviour hooks**
|
|
12
12
|
* `registerPostTickHook(id, fn)` registers an observer callback that the host fires
|
|
13
13
|
* after each `stepWorld` call via `runPostTickHooks(world)`. Hooks are purely
|
|
14
14
|
* observational — they MUST NOT mutate `WorldState` during the call. Because they run
|
|
15
15
|
* outside the kernel path they cannot break determinism.
|
|
16
16
|
*
|
|
17
|
-
* **Layer 3 — AI
|
|
17
|
+
* **Layer 3 — AI behaviour node overrides**
|
|
18
18
|
* `registerBehaviorNode(id, factory)` installs a named factory for custom
|
|
19
19
|
* `BehaviorNode` implementations. `loadScenario` (CE-3) can reference them by id in
|
|
20
20
|
* scenario JSON. AI overrides require explicit host opt-in.
|
|
@@ -75,7 +75,7 @@ export type BehaviorNodeFactory = (...args: unknown[]) => BehaviorNode;
|
|
|
75
75
|
* Register a named factory for a custom `BehaviorNode` implementation.
|
|
76
76
|
*
|
|
77
77
|
* The factory will be looked up by id when `loadScenario` (CE-3) encounters an
|
|
78
|
-
* `"aiOverride"` reference in scenario JSON, or when a host builds a
|
|
78
|
+
* `"aiOverride"` reference in scenario JSON, or when a host builds a behaviour
|
|
79
79
|
* tree programmatically:
|
|
80
80
|
*
|
|
81
81
|
* ```typescript
|
|
@@ -87,32 +87,32 @@ export type BehaviorNodeFactory = (...args: unknown[]) => BehaviorNode;
|
|
|
87
87
|
* ```
|
|
88
88
|
*
|
|
89
89
|
* **Deterministic multiplayer**: AI overrides affect simulation output. All
|
|
90
|
-
* clients must register the same
|
|
90
|
+
* clients must register the same behaviour nodes (verified via `computeModManifest`)
|
|
91
91
|
* before joining a session.
|
|
92
92
|
*
|
|
93
93
|
* Re-registering an existing id overwrites the previous factory.
|
|
94
94
|
*/
|
|
95
95
|
export declare function registerBehaviorNode(id: string, factory: BehaviorNodeFactory): void;
|
|
96
96
|
/**
|
|
97
|
-
* Remove a previously registered
|
|
97
|
+
* Remove a previously registered behaviour node factory.
|
|
98
98
|
* Returns `true` if the factory existed and was removed.
|
|
99
99
|
*/
|
|
100
100
|
export declare function unregisterBehaviorNode(id: string): boolean;
|
|
101
101
|
/**
|
|
102
|
-
* Look up a registered
|
|
102
|
+
* Look up a registered behaviour node factory by id.
|
|
103
103
|
* Returns `undefined` if not found.
|
|
104
104
|
*/
|
|
105
105
|
export declare function getBehaviorNode(id: string): BehaviorNodeFactory | undefined;
|
|
106
|
-
/** Return the ids of all registered
|
|
106
|
+
/** Return the ids of all registered behaviour node factories in registration order. */
|
|
107
107
|
export declare function listBehaviorNodes(): string[];
|
|
108
|
-
/** Remove all
|
|
108
|
+
/** Remove all behaviour node factories (useful for testing and hot-reload scenarios). */
|
|
109
109
|
export declare function clearBehaviorNodes(): void;
|
|
110
110
|
export interface ModManifest {
|
|
111
111
|
/** Sorted list of all data mod ids currently in the CE-12 catalog. */
|
|
112
112
|
dataIds: string[];
|
|
113
113
|
/** Sorted list of all registered post-tick hook ids. */
|
|
114
114
|
hookIds: string[];
|
|
115
|
-
/** Sorted list of all registered
|
|
115
|
+
/** Sorted list of all registered behaviour node ids. */
|
|
116
116
|
behaviorIds: string[];
|
|
117
117
|
/**
|
|
118
118
|
* Single fingerprint covering all three id lists.
|
|
@@ -122,7 +122,7 @@ export interface ModManifest {
|
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
124
|
* Compute a session manifest covering all active mods (CE-12 catalog entries,
|
|
125
|
-
* post-tick hooks, and AI
|
|
125
|
+
* post-tick hooks, and AI behaviour node overrides).
|
|
126
126
|
*
|
|
127
127
|
* The `fingerprint` is a deterministic 8-char hex string suitable for
|
|
128
128
|
* multiplayer session comparison. Clients are considered mod-compatible iff
|
|
@@ -133,5 +133,5 @@ export interface ModManifest {
|
|
|
133
133
|
* on `catalog.ts`.
|
|
134
134
|
*/
|
|
135
135
|
export declare function computeModManifest(catalogIds?: string[]): ModManifest;
|
|
136
|
-
/** Remove all hooks and
|
|
136
|
+
/** Remove all hooks and behaviour node factories. Does not affect the CE-12 catalog. */
|
|
137
137
|
export declare function clearAllMods(): void;
|
package/dist/src/modding.js
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* mod file. The network replication layer (CE-11) compares fingerprints across clients
|
|
9
9
|
* to guarantee all participants use identical mod definitions.
|
|
10
10
|
*
|
|
11
|
-
* **Layer 2 — Post-tick
|
|
11
|
+
* **Layer 2 — Post-tick behaviour hooks**
|
|
12
12
|
* `registerPostTickHook(id, fn)` registers an observer callback that the host fires
|
|
13
13
|
* after each `stepWorld` call via `runPostTickHooks(world)`. Hooks are purely
|
|
14
14
|
* observational — they MUST NOT mutate `WorldState` during the call. Because they run
|
|
15
15
|
* outside the kernel path they cannot break determinism.
|
|
16
16
|
*
|
|
17
|
-
* **Layer 3 — AI
|
|
17
|
+
* **Layer 3 — AI behaviour node overrides**
|
|
18
18
|
* `registerBehaviorNode(id, factory)` installs a named factory for custom
|
|
19
19
|
* `BehaviorNode` implementations. `loadScenario` (CE-3) can reference them by id in
|
|
20
20
|
* scenario JSON. AI overrides require explicit host opt-in.
|
|
@@ -117,7 +117,7 @@ const _behaviorNodes = new Map();
|
|
|
117
117
|
* Register a named factory for a custom `BehaviorNode` implementation.
|
|
118
118
|
*
|
|
119
119
|
* The factory will be looked up by id when `loadScenario` (CE-3) encounters an
|
|
120
|
-
* `"aiOverride"` reference in scenario JSON, or when a host builds a
|
|
120
|
+
* `"aiOverride"` reference in scenario JSON, or when a host builds a behaviour
|
|
121
121
|
* tree programmatically:
|
|
122
122
|
*
|
|
123
123
|
* ```typescript
|
|
@@ -129,7 +129,7 @@ const _behaviorNodes = new Map();
|
|
|
129
129
|
* ```
|
|
130
130
|
*
|
|
131
131
|
* **Deterministic multiplayer**: AI overrides affect simulation output. All
|
|
132
|
-
* clients must register the same
|
|
132
|
+
* clients must register the same behaviour nodes (verified via `computeModManifest`)
|
|
133
133
|
* before joining a session.
|
|
134
134
|
*
|
|
135
135
|
* Re-registering an existing id overwrites the previous factory.
|
|
@@ -140,30 +140,30 @@ export function registerBehaviorNode(id, factory) {
|
|
|
140
140
|
_behaviorNodes.set(id, factory);
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
143
|
-
* Remove a previously registered
|
|
143
|
+
* Remove a previously registered behaviour node factory.
|
|
144
144
|
* Returns `true` if the factory existed and was removed.
|
|
145
145
|
*/
|
|
146
146
|
export function unregisterBehaviorNode(id) {
|
|
147
147
|
return _behaviorNodes.delete(id);
|
|
148
148
|
}
|
|
149
149
|
/**
|
|
150
|
-
* Look up a registered
|
|
150
|
+
* Look up a registered behaviour node factory by id.
|
|
151
151
|
* Returns `undefined` if not found.
|
|
152
152
|
*/
|
|
153
153
|
export function getBehaviorNode(id) {
|
|
154
154
|
return _behaviorNodes.get(id);
|
|
155
155
|
}
|
|
156
|
-
/** Return the ids of all registered
|
|
156
|
+
/** Return the ids of all registered behaviour node factories in registration order. */
|
|
157
157
|
export function listBehaviorNodes() {
|
|
158
158
|
return [..._behaviorNodes.keys()];
|
|
159
159
|
}
|
|
160
|
-
/** Remove all
|
|
160
|
+
/** Remove all behaviour node factories (useful for testing and hot-reload scenarios). */
|
|
161
161
|
export function clearBehaviorNodes() {
|
|
162
162
|
_behaviorNodes.clear();
|
|
163
163
|
}
|
|
164
164
|
/**
|
|
165
165
|
* Compute a session manifest covering all active mods (CE-12 catalog entries,
|
|
166
|
-
* post-tick hooks, and AI
|
|
166
|
+
* post-tick hooks, and AI behaviour node overrides).
|
|
167
167
|
*
|
|
168
168
|
* The `fingerprint` is a deterministic 8-char hex string suitable for
|
|
169
169
|
* multiplayer session comparison. Clients are considered mod-compatible iff
|
|
@@ -181,7 +181,7 @@ export function computeModManifest(catalogIds = []) {
|
|
|
181
181
|
const fingerprint = fnv1a32(combined).toString(16).padStart(8, "0");
|
|
182
182
|
return { dataIds, hookIds, behaviorIds, fingerprint };
|
|
183
183
|
}
|
|
184
|
-
/** Remove all hooks and
|
|
184
|
+
/** Remove all hooks and behaviour node factories. Does not affect the CE-12 catalog. */
|
|
185
185
|
export function clearAllMods() {
|
|
186
186
|
_hooks.clear();
|
|
187
187
|
_behaviorNodes.clear();
|
|
@@ -31,9 +31,9 @@ export function renderArcSummary(arc) {
|
|
|
31
31
|
rise_of_hero: (a) => `The hero's journey of ${actorNames(a)} spans ${entryCount(a)} pivotal moments.`,
|
|
32
32
|
tragic_fall: (a) => `${actorNames(a)}'s descent from grace, marked by betrayal and loss.`,
|
|
33
33
|
rivalry: (a) => `An enduring rivalry between ${actorNames(a)} unfolding across ${entryCount(a)} confrontations.`,
|
|
34
|
-
great_migration: (
|
|
35
|
-
settlement_growth: (
|
|
36
|
-
fallen_settlement: (
|
|
34
|
+
great_migration: (_a) => `A mass migration that reshaped the region.`,
|
|
35
|
+
settlement_growth: (_a) => `The rise of a settlement from humble beginnings to prosperity.`,
|
|
36
|
+
fallen_settlement: (_a) => `The tragic fall of a once-great settlement.`,
|
|
37
37
|
legendary_craftsman: (a) => `${actorNames(a)}'s masterworks will be remembered for generations.`,
|
|
38
38
|
notorious_villain: (a) => `The terrifying rise of ${actorNames(a)}.`,
|
|
39
39
|
unlikely_friendship: (a) => `An unexpected bond formed between ${actorNames(a)} against all odds.`,
|
|
@@ -40,7 +40,7 @@ const DELIVERY_TEMPLATE = {
|
|
|
40
40
|
currency: 50,
|
|
41
41
|
},
|
|
42
42
|
objectiveGenerators: [
|
|
43
|
-
(
|
|
43
|
+
(_ctx) => ({
|
|
44
44
|
objectiveId: "collect_package",
|
|
45
45
|
description: "Collect the package from the quest giver",
|
|
46
46
|
type: "collect_item",
|
|
@@ -129,7 +129,7 @@ const INVESTIGATION_TEMPLATE = {
|
|
|
129
129
|
state: "available",
|
|
130
130
|
hidden: false,
|
|
131
131
|
}),
|
|
132
|
-
(
|
|
132
|
+
(_ctx) => ({
|
|
133
133
|
objectiveId: "report_findings",
|
|
134
134
|
description: "Return and report your findings",
|
|
135
135
|
type: "dialogue_choice",
|
|
@@ -185,7 +185,7 @@ const COLLECTION_TEMPLATE = {
|
|
|
185
185
|
currency: 60,
|
|
186
186
|
},
|
|
187
187
|
objectiveGenerators: [
|
|
188
|
-
(
|
|
188
|
+
(_ctx) => ({
|
|
189
189
|
objectiveId: "collect_items",
|
|
190
190
|
description: "Collect the requested items",
|
|
191
191
|
type: "collect_item",
|
|
@@ -224,7 +224,7 @@ const WAIT_TEMPLATE = {
|
|
|
224
224
|
state: "available",
|
|
225
225
|
hidden: false,
|
|
226
226
|
}),
|
|
227
|
-
(
|
|
227
|
+
(_ctx) => ({
|
|
228
228
|
objectiveId: "stand_watch",
|
|
229
229
|
description: "Stand watch for the required duration",
|
|
230
230
|
type: "wait_duration",
|
|
@@ -88,12 +88,12 @@ export function generateSettlementNeeds(settlement) {
|
|
|
88
88
|
suggestedReward: 200,
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
-
// Recent raid →
|
|
91
|
+
// Recent raid → defence need
|
|
92
92
|
if (settlement.safetyStatus.ticksSinceLastRaid < 100) {
|
|
93
93
|
needs.push({
|
|
94
94
|
type: "defense",
|
|
95
95
|
priority: 8,
|
|
96
|
-
description: "Recent raid requires improved
|
|
96
|
+
description: "Recent raid requires improved defences",
|
|
97
97
|
suggestedReward: 300,
|
|
98
98
|
});
|
|
99
99
|
}
|
package/dist/src/settlement.d.ts
CHANGED
|
@@ -135,7 +135,7 @@ export interface AvailableServices {
|
|
|
135
135
|
export declare function getAvailableServices(settlement: Settlement): AvailableServices;
|
|
136
136
|
/** Record a raid/siege on a settlement. */
|
|
137
137
|
export declare function recordRaid(settlement: Settlement, attackerFactionId: number, casualties: number, tick: number): void;
|
|
138
|
-
/** Update settlement
|
|
138
|
+
/** Update settlement defences. */
|
|
139
139
|
export declare function updateDefenses(settlement: Settlement, hasDefenses: boolean): void;
|
|
140
140
|
/** Serialize settlement to JSON-friendly format. */
|
|
141
141
|
export declare function serializeSettlement(settlement: Settlement): unknown;
|
package/dist/src/settlement.js
CHANGED
|
@@ -351,7 +351,7 @@ function getMedicalCareLevel(level) {
|
|
|
351
351
|
default: return "none";
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
|
-
// ── Settlement
|
|
354
|
+
// ── Settlement Defence ─────────────────────────────────────────────────────────
|
|
355
355
|
/** Record a raid/siege on a settlement. */
|
|
356
356
|
export function recordRaid(settlement, attackerFactionId, casualties, tick) {
|
|
357
357
|
settlement.safetyStatus.ticksSinceLastRaid = 0;
|
|
@@ -367,7 +367,7 @@ export function recordRaid(settlement, attackerFactionId, casualties, tick) {
|
|
|
367
367
|
settlement.population = Math.max(0, settlement.population - casualties);
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
|
-
/** Update settlement
|
|
370
|
+
/** Update settlement defences. */
|
|
371
371
|
export function updateDefenses(settlement, hasDefenses) {
|
|
372
372
|
settlement.safetyStatus.hasDefenses = hasDefenses;
|
|
373
373
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CE-10 — Pre-built AI
|
|
2
|
+
* CE-10 — Pre-built AI Behaviour Tree Library
|
|
3
3
|
*
|
|
4
4
|
* A thin, composable layer over the existing AI decision system. Each
|
|
5
5
|
* `BehaviorNode` receives the ticking entity, the current world state, and
|
|
@@ -31,7 +31,7 @@ import type { KernelContext } from "../context.js";
|
|
|
31
31
|
import type { Command } from "../commands.js";
|
|
32
32
|
import { type Q } from "../../units.js";
|
|
33
33
|
/**
|
|
34
|
-
* A single node in a
|
|
34
|
+
* A single node in a behaviour tree.
|
|
35
35
|
*
|
|
36
36
|
* `tick` is called once per AI frame (typically once per `stepWorld` tick).
|
|
37
37
|
* Returns a `Command` if this node produces an action, or `null` if the node's
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CE-10 — Pre-built AI
|
|
2
|
+
* CE-10 — Pre-built AI Behaviour Tree Library
|
|
3
3
|
*
|
|
4
4
|
* A thin, composable layer over the existing AI decision system. Each
|
|
5
5
|
* `BehaviorNode` receives the ticking entity, the current world state, and
|
|
@@ -302,7 +302,7 @@ export function WithProbability(probability_Q, inner, salt = 0) {
|
|
|
302
302
|
},
|
|
303
303
|
};
|
|
304
304
|
}
|
|
305
|
-
// ── Pre-built
|
|
305
|
+
// ── Pre-built behaviour tree presets ─────────────────────────────────────────
|
|
306
306
|
/**
|
|
307
307
|
* Standard aggressive attacker: attack `targetId` at full intensity.
|
|
308
308
|
* Falls back to retreat if badly shocked (shock ≥ q(0.70)).
|
|
@@ -81,7 +81,7 @@ export interface BodySegment {
|
|
|
81
81
|
*/
|
|
82
82
|
regeneratesViaMolting?: boolean;
|
|
83
83
|
/**
|
|
84
|
-
* Intrinsic structural
|
|
84
|
+
* Intrinsic structural armour resist (joules) — energy absorbed by the shell
|
|
85
85
|
* before damage channels are allocated. Distinct from worn equipment armour.
|
|
86
86
|
* Absent or 0 = no intrinsic resistance.
|
|
87
87
|
*/
|
package/dist/src/sim/kernel.js
CHANGED
|
@@ -1368,7 +1368,7 @@ export function applyImpactToInjury(target, wpn, energy_J, region, armoured, tra
|
|
|
1368
1368
|
}
|
|
1369
1369
|
}
|
|
1370
1370
|
const armourShift = armoured ? q(0.75) : q(1.0);
|
|
1371
|
-
// Phase 8C: intrinsic exoskeleton
|
|
1371
|
+
// Phase 8C: intrinsic exoskeleton armour — absorbed before damage channels are allocated
|
|
1372
1372
|
if (seg?.intrinsicArmor_J !== undefined && seg.intrinsicArmor_J > 0) {
|
|
1373
1373
|
energy_J = Math.max(0, energy_J - seg.intrinsicArmor_J);
|
|
1374
1374
|
if (energy_J === 0)
|
|
@@ -48,7 +48,7 @@ export function stepMoraleForEntity(world, e, index, spatial, aliveBeforeTick, t
|
|
|
48
48
|
}
|
|
49
49
|
let fearQ = e.condition.fearQ;
|
|
50
50
|
const wasRouting = isRouting(fearQ, distressTol);
|
|
51
|
-
// 1. Suppression ticks add fear per tick — scaled by
|
|
51
|
+
// 1. Suppression ticks add fear per tick — scaled by calibre multiplier (Feature 1)
|
|
52
52
|
if (e.condition.suppressedTicks > 0) {
|
|
53
53
|
const supMul = e.condition.suppressionFearMul ?? SCALE.Q;
|
|
54
54
|
fearQ = clampQ(fearQ + qMul(FEAR_PER_SUPPRESSION_TICK, supMul), 0, SCALE.Q);
|