@its-not-rocket-science/ananke 0.1.0
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 +135 -0
- package/LICENSE +21 -0
- package/README.md +2199 -0
- package/STABLE_API.md +266 -0
- package/dist/src/anatomy/anatomy-compiler.d.ts +14 -0
- package/dist/src/anatomy/anatomy-compiler.js +277 -0
- package/dist/src/anatomy/anatomy-contracts.d.ts +94 -0
- package/dist/src/anatomy/anatomy-contracts.js +1 -0
- package/dist/src/anatomy/anatomy-helpers.d.ts +82 -0
- package/dist/src/anatomy/anatomy-helpers.js +233 -0
- package/dist/src/anatomy/anatomy-schema.d.ts +28 -0
- package/dist/src/anatomy/anatomy-schema.js +388 -0
- package/dist/src/anatomy/index.d.ts +4 -0
- package/dist/src/anatomy/index.js +4 -0
- package/dist/src/archetypes.d.ts +87 -0
- package/dist/src/archetypes.js +285 -0
- package/dist/src/arena.d.ts +173 -0
- package/dist/src/arena.js +695 -0
- package/dist/src/bridge/bridge-engine.d.ts +46 -0
- package/dist/src/bridge/bridge-engine.js +252 -0
- package/dist/src/bridge/index.d.ts +4 -0
- package/dist/src/bridge/index.js +5 -0
- package/dist/src/bridge/interpolation.d.ts +64 -0
- package/dist/src/bridge/interpolation.js +130 -0
- package/dist/src/bridge/mapping.d.ts +33 -0
- package/dist/src/bridge/mapping.js +54 -0
- package/dist/src/bridge/types.d.ts +94 -0
- package/dist/src/bridge/types.js +2 -0
- package/dist/src/campaign.d.ts +141 -0
- package/dist/src/campaign.js +235 -0
- package/dist/src/channels.d.ts +15 -0
- package/dist/src/channels.js +20 -0
- package/dist/src/chronicle.d.ts +124 -0
- package/dist/src/chronicle.js +232 -0
- package/dist/src/collective-activities.d.ts +154 -0
- package/dist/src/collective-activities.js +247 -0
- package/dist/src/competence/acoustic.d.ts +101 -0
- package/dist/src/competence/acoustic.js +242 -0
- package/dist/src/competence/catalogue.d.ts +30 -0
- package/dist/src/competence/catalogue.js +241 -0
- package/dist/src/competence/crafting.d.ts +35 -0
- package/dist/src/competence/crafting.js +88 -0
- package/dist/src/competence/engineering.d.ts +53 -0
- package/dist/src/competence/engineering.js +108 -0
- package/dist/src/competence/framework.d.ts +68 -0
- package/dist/src/competence/framework.js +694 -0
- package/dist/src/competence/index.d.ts +12 -0
- package/dist/src/competence/index.js +13 -0
- package/dist/src/competence/interspecies.d.ts +81 -0
- package/dist/src/competence/interspecies.js +108 -0
- package/dist/src/competence/language.d.ts +79 -0
- package/dist/src/competence/language.js +115 -0
- package/dist/src/competence/naturalist.d.ts +97 -0
- package/dist/src/competence/naturalist.js +187 -0
- package/dist/src/competence/navigation.d.ts +24 -0
- package/dist/src/competence/navigation.js +48 -0
- package/dist/src/competence/performance.d.ts +125 -0
- package/dist/src/competence/performance.js +210 -0
- package/dist/src/competence/teaching.d.ts +64 -0
- package/dist/src/competence/teaching.js +121 -0
- package/dist/src/competence/willpower.d.ts +74 -0
- package/dist/src/competence/willpower.js +114 -0
- package/dist/src/crafting/index.d.ts +55 -0
- package/dist/src/crafting/index.js +229 -0
- package/dist/src/crafting/manufacturing.d.ts +83 -0
- package/dist/src/crafting/manufacturing.js +165 -0
- package/dist/src/crafting/materials.d.ts +53 -0
- package/dist/src/crafting/materials.js +120 -0
- package/dist/src/crafting/recipes.d.ts +75 -0
- package/dist/src/crafting/recipes.js +233 -0
- package/dist/src/crafting/workshops.d.ts +61 -0
- package/dist/src/crafting/workshops.js +170 -0
- package/dist/src/debug.d.ts +86 -0
- package/dist/src/debug.js +76 -0
- package/dist/src/derive.d.ts +21 -0
- package/dist/src/derive.js +88 -0
- package/dist/src/describe.d.ts +29 -0
- package/dist/src/describe.js +276 -0
- package/dist/src/dialogue.d.ts +122 -0
- package/dist/src/dialogue.js +266 -0
- package/dist/src/dist.d.ts +20 -0
- package/dist/src/dist.js +39 -0
- package/dist/src/downtime.d.ts +89 -0
- package/dist/src/downtime.js +391 -0
- package/dist/src/economy.d.ts +116 -0
- package/dist/src/economy.js +182 -0
- package/dist/src/emotional-contagion.d.ts +142 -0
- package/dist/src/emotional-contagion.js +274 -0
- package/dist/src/equipment.d.ts +206 -0
- package/dist/src/equipment.js +598 -0
- package/dist/src/faction.d.ts +102 -0
- package/dist/src/faction.js +237 -0
- package/dist/src/generate.d.ts +35 -0
- package/dist/src/generate.js +166 -0
- package/dist/src/index.d.ts +42 -0
- package/dist/src/index.js +54 -0
- package/dist/src/inheritance.d.ts +69 -0
- package/dist/src/inheritance.js +136 -0
- package/dist/src/inventory.d.ts +194 -0
- package/dist/src/inventory.js +637 -0
- package/dist/src/item-durability.d.ts +69 -0
- package/dist/src/item-durability.js +308 -0
- package/dist/src/legend.d.ts +97 -0
- package/dist/src/legend.js +269 -0
- package/dist/src/lod.d.ts +9 -0
- package/dist/src/lod.js +84 -0
- package/dist/src/metrics.d.ts +51 -0
- package/dist/src/metrics.js +91 -0
- package/dist/src/model3d.d.ts +138 -0
- package/dist/src/model3d.js +214 -0
- package/dist/src/mythology.d.ts +101 -0
- package/dist/src/mythology.js +308 -0
- package/dist/src/narrative-render.d.ts +42 -0
- package/dist/src/narrative-render.js +194 -0
- package/dist/src/narrative-stress.d.ts +123 -0
- package/dist/src/narrative-stress.js +183 -0
- package/dist/src/narrative.d.ts +44 -0
- package/dist/src/narrative.js +257 -0
- package/dist/src/party.d.ts +70 -0
- package/dist/src/party.js +226 -0
- package/dist/src/polity.d.ts +262 -0
- package/dist/src/polity.js +398 -0
- package/dist/src/presets.d.ts +42 -0
- package/dist/src/presets.js +170 -0
- package/dist/src/progression.d.ts +170 -0
- package/dist/src/progression.js +256 -0
- package/dist/src/quest-generators.d.ts +76 -0
- package/dist/src/quest-generators.js +534 -0
- package/dist/src/quest.d.ts +239 -0
- package/dist/src/quest.js +520 -0
- package/dist/src/relationships-effects.d.ts +75 -0
- package/dist/src/relationships-effects.js +219 -0
- package/dist/src/relationships.d.ts +104 -0
- package/dist/src/relationships.js +347 -0
- package/dist/src/replay.d.ts +47 -0
- package/dist/src/replay.js +82 -0
- package/dist/src/rng.d.ts +9 -0
- package/dist/src/rng.js +37 -0
- package/dist/src/settlement-services.d.ts +67 -0
- package/dist/src/settlement-services.js +267 -0
- package/dist/src/settlement.d.ts +143 -0
- package/dist/src/settlement.js +419 -0
- package/dist/src/sim/action.d.ts +28 -0
- package/dist/src/sim/action.js +12 -0
- package/dist/src/sim/aging.d.ts +95 -0
- package/dist/src/sim/aging.js +243 -0
- package/dist/src/sim/ai/decide.d.ts +10 -0
- package/dist/src/sim/ai/decide.js +267 -0
- package/dist/src/sim/ai/perception.d.ts +12 -0
- package/dist/src/sim/ai/perception.js +54 -0
- package/dist/src/sim/ai/personality.d.ts +54 -0
- package/dist/src/sim/ai/personality.js +202 -0
- package/dist/src/sim/ai/presets.d.ts +2 -0
- package/dist/src/sim/ai/presets.js +28 -0
- package/dist/src/sim/ai/system.d.ts +6 -0
- package/dist/src/sim/ai/system.js +13 -0
- package/dist/src/sim/ai/targeting.d.ts +8 -0
- package/dist/src/sim/ai/targeting.js +42 -0
- package/dist/src/sim/ai/types.d.ts +14 -0
- package/dist/src/sim/ai/types.js +1 -0
- package/dist/src/sim/body.d.ts +9 -0
- package/dist/src/sim/body.js +32 -0
- package/dist/src/sim/bodyplan.d.ts +161 -0
- package/dist/src/sim/bodyplan.js +677 -0
- package/dist/src/sim/capability.d.ts +135 -0
- package/dist/src/sim/capability.js +8 -0
- package/dist/src/sim/combat.d.ts +21 -0
- package/dist/src/sim/combat.js +77 -0
- package/dist/src/sim/commandBuilders.d.ts +11 -0
- package/dist/src/sim/commandBuilders.js +39 -0
- package/dist/src/sim/commands.d.ts +71 -0
- package/dist/src/sim/commands.js +8 -0
- package/dist/src/sim/condition.d.ts +35 -0
- package/dist/src/sim/condition.js +21 -0
- package/dist/src/sim/cone.d.ts +40 -0
- package/dist/src/sim/cone.js +44 -0
- package/dist/src/sim/context.d.ts +68 -0
- package/dist/src/sim/context.js +1 -0
- package/dist/src/sim/density.d.ts +14 -0
- package/dist/src/sim/density.js +33 -0
- package/dist/src/sim/disease.d.ts +141 -0
- package/dist/src/sim/disease.js +353 -0
- package/dist/src/sim/entity.d.ts +251 -0
- package/dist/src/sim/entity.js +19 -0
- package/dist/src/sim/events.d.ts +25 -0
- package/dist/src/sim/events.js +5 -0
- package/dist/src/sim/explosion.d.ts +40 -0
- package/dist/src/sim/explosion.js +40 -0
- package/dist/src/sim/formation-unit.d.ts +138 -0
- package/dist/src/sim/formation-unit.js +197 -0
- package/dist/src/sim/formation.d.ts +12 -0
- package/dist/src/sim/formation.js +54 -0
- package/dist/src/sim/frontage.d.ts +30 -0
- package/dist/src/sim/frontage.js +84 -0
- package/dist/src/sim/grapple.d.ts +100 -0
- package/dist/src/sim/grapple.js +480 -0
- package/dist/src/sim/hazard.d.ts +104 -0
- package/dist/src/sim/hazard.js +201 -0
- package/dist/src/sim/hydrostatic.d.ts +58 -0
- package/dist/src/sim/hydrostatic.js +117 -0
- package/dist/src/sim/impairment.d.ts +20 -0
- package/dist/src/sim/impairment.js +162 -0
- package/dist/src/sim/indexing.d.ts +7 -0
- package/dist/src/sim/indexing.js +7 -0
- package/dist/src/sim/injury.d.ts +54 -0
- package/dist/src/sim/injury.js +66 -0
- package/dist/src/sim/intent.d.ts +26 -0
- package/dist/src/sim/intent.js +7 -0
- package/dist/src/sim/kernel.d.ts +45 -0
- package/dist/src/sim/kernel.js +1992 -0
- package/dist/src/sim/kinds.d.ts +64 -0
- package/dist/src/sim/kinds.js +56 -0
- package/dist/src/sim/knockback.d.ts +50 -0
- package/dist/src/sim/knockback.js +82 -0
- package/dist/src/sim/limb.d.ts +48 -0
- package/dist/src/sim/limb.js +78 -0
- package/dist/src/sim/medical.d.ts +32 -0
- package/dist/src/sim/medical.js +33 -0
- package/dist/src/sim/morale.d.ts +69 -0
- package/dist/src/sim/morale.js +92 -0
- package/dist/src/sim/mount.d.ts +150 -0
- package/dist/src/sim/mount.js +225 -0
- package/dist/src/sim/nutrition.d.ts +74 -0
- package/dist/src/sim/nutrition.js +168 -0
- package/dist/src/sim/occlusion.d.ts +8 -0
- package/dist/src/sim/occlusion.js +71 -0
- package/dist/src/sim/push.d.ts +11 -0
- package/dist/src/sim/push.js +79 -0
- package/dist/src/sim/ranged.d.ts +44 -0
- package/dist/src/sim/ranged.js +69 -0
- package/dist/src/sim/seeds.d.ts +3 -0
- package/dist/src/sim/seeds.js +16 -0
- package/dist/src/sim/sensory-extended.d.ts +103 -0
- package/dist/src/sim/sensory-extended.js +181 -0
- package/dist/src/sim/sensory.d.ts +38 -0
- package/dist/src/sim/sensory.js +109 -0
- package/dist/src/sim/skills.d.ts +70 -0
- package/dist/src/sim/skills.js +69 -0
- package/dist/src/sim/sleep.d.ts +107 -0
- package/dist/src/sim/sleep.js +215 -0
- package/dist/src/sim/spatial.d.ts +8 -0
- package/dist/src/sim/spatial.js +59 -0
- package/dist/src/sim/step/capability.d.ts +8 -0
- package/dist/src/sim/step/capability.js +77 -0
- package/dist/src/sim/step/concentration.d.ts +9 -0
- package/dist/src/sim/step/concentration.js +25 -0
- package/dist/src/sim/step/effects.d.ts +17 -0
- package/dist/src/sim/step/effects.js +96 -0
- package/dist/src/sim/step/energy.d.ts +3 -0
- package/dist/src/sim/step/energy.js +31 -0
- package/dist/src/sim/step/hazards.d.ts +4 -0
- package/dist/src/sim/step/hazards.js +19 -0
- package/dist/src/sim/step/injury.d.ts +10 -0
- package/dist/src/sim/step/injury.js +353 -0
- package/dist/src/sim/step/morale.d.ts +11 -0
- package/dist/src/sim/step/morale.js +130 -0
- package/dist/src/sim/step/movement.d.ts +5 -0
- package/dist/src/sim/step/movement.js +172 -0
- package/dist/src/sim/step/push.d.ts +11 -0
- package/dist/src/sim/step/push.js +79 -0
- package/dist/src/sim/step/substances.d.ts +3 -0
- package/dist/src/sim/step/substances.js +75 -0
- package/dist/src/sim/substance.d.ts +38 -0
- package/dist/src/sim/substance.js +57 -0
- package/dist/src/sim/systemic-toxicology.d.ts +109 -0
- package/dist/src/sim/systemic-toxicology.js +263 -0
- package/dist/src/sim/team.d.ts +9 -0
- package/dist/src/sim/team.js +37 -0
- package/dist/src/sim/tech.d.ts +36 -0
- package/dist/src/sim/tech.js +46 -0
- package/dist/src/sim/terrain.d.ts +121 -0
- package/dist/src/sim/terrain.js +141 -0
- package/dist/src/sim/testing.d.ts +13 -0
- package/dist/src/sim/testing.js +100 -0
- package/dist/src/sim/thermoregulation.d.ts +77 -0
- package/dist/src/sim/thermoregulation.js +161 -0
- package/dist/src/sim/tick.d.ts +3 -0
- package/dist/src/sim/tick.js +3 -0
- package/dist/src/sim/toxicology.d.ts +52 -0
- package/dist/src/sim/toxicology.js +104 -0
- package/dist/src/sim/trace.d.ts +141 -0
- package/dist/src/sim/trace.js +1 -0
- package/dist/src/sim/tuning.d.ts +16 -0
- package/dist/src/sim/tuning.js +42 -0
- package/dist/src/sim/vec3.d.ts +14 -0
- package/dist/src/sim/vec3.js +31 -0
- package/dist/src/sim/weapon_dynamics.d.ts +102 -0
- package/dist/src/sim/weapon_dynamics.js +142 -0
- package/dist/src/sim/weather.d.ts +95 -0
- package/dist/src/sim/weather.js +105 -0
- package/dist/src/sim/world.d.ts +52 -0
- package/dist/src/sim/world.js +1 -0
- package/dist/src/sim/wound-aging.d.ts +120 -0
- package/dist/src/sim/wound-aging.js +223 -0
- package/dist/src/species.d.ts +106 -0
- package/dist/src/species.js +664 -0
- package/dist/src/story-arcs.d.ts +17 -0
- package/dist/src/story-arcs.js +276 -0
- package/dist/src/tech-diffusion.d.ts +80 -0
- package/dist/src/tech-diffusion.js +185 -0
- package/dist/src/traits.d.ts +25 -0
- package/dist/src/traits.js +178 -0
- package/dist/src/types.d.ts +117 -0
- package/dist/src/types.js +1 -0
- package/dist/src/units.d.ts +41 -0
- package/dist/src/units.js +64 -0
- package/dist/src/weapons.d.ts +20 -0
- package/dist/src/weapons.js +824 -0
- package/dist/src/world-generation.d.ts +52 -0
- package/dist/src/world-generation.js +301 -0
- package/package.json +74 -0
package/dist/src/dist.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SCALE, clampQ, mulDiv, qMul } from "./units.js";
|
|
2
|
+
// half of SCALE.Q — the natural boundary of triSym output
|
|
3
|
+
const HALF_Q = SCALE.Q >>> 1;
|
|
4
|
+
export function tri01(rng) {
|
|
5
|
+
const u = rng.q01();
|
|
6
|
+
const v = rng.q01();
|
|
7
|
+
return (u + v) >>> 1;
|
|
8
|
+
}
|
|
9
|
+
export function triSym(rng) {
|
|
10
|
+
return (tri01(rng) - (SCALE.Q >>> 1));
|
|
11
|
+
}
|
|
12
|
+
export function mulFromVariation(variationSym, amplitude) {
|
|
13
|
+
const delta = mulDiv(variationSym, amplitude, SCALE.Q);
|
|
14
|
+
return clampQ((SCALE.Q + delta), 0, 3 * SCALE.Q);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Symmetric triangular sample shifted by `bias × 0.5` of half-range, then
|
|
18
|
+
* clamped back to the natural triSym bounds `[−SCALE.Q/2, SCALE.Q/2]`.
|
|
19
|
+
*
|
|
20
|
+
* `bias` ∈ [−1, 1]:
|
|
21
|
+
* +1 strongly skews toward high-end attribute values
|
|
22
|
+
* −1 strongly skews toward low-end attribute values
|
|
23
|
+
* 0 is identical to an unbiased `triSym(rng)` call
|
|
24
|
+
*
|
|
25
|
+
* Used by `generateIndividual` to implement `NarrativeBias`.
|
|
26
|
+
*/
|
|
27
|
+
export function biasedTriSym(rng, bias) {
|
|
28
|
+
const raw = triSym(rng);
|
|
29
|
+
if (bias === 0)
|
|
30
|
+
return raw;
|
|
31
|
+
const shift = Math.round(bias * HALF_Q * 0.5);
|
|
32
|
+
return Math.max(-HALF_Q, Math.min(HALF_Q, raw + shift));
|
|
33
|
+
}
|
|
34
|
+
export function skewUp(mult, steps) {
|
|
35
|
+
let out = mult;
|
|
36
|
+
for (let i = 0; i < steps; i++)
|
|
37
|
+
out = qMul(out, mult);
|
|
38
|
+
return (SCALE.Q + ((out - SCALE.Q) >>> 1));
|
|
39
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type Q } from "./units.js";
|
|
2
|
+
import type { InjuryState } from "./sim/injury.js";
|
|
3
|
+
import type { MedicalTier } from "./sim/medical.js";
|
|
4
|
+
import type { WorldState } from "./sim/world.js";
|
|
5
|
+
export interface MedicalResource {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
tier: MedicalTier;
|
|
9
|
+
/** Abstract cost units — host maps to currency of choice. */
|
|
10
|
+
costUnits: number;
|
|
11
|
+
massGrams: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const MEDICAL_RESOURCES: MedicalResource[];
|
|
14
|
+
/**
|
|
15
|
+
* Preset care levels: what treatment is available and applied automatically.
|
|
16
|
+
*/
|
|
17
|
+
export type CareLevel = "none" | "first_aid" | "field_medicine" | "hospital" | "autodoc";
|
|
18
|
+
export interface TreatmentSchedule {
|
|
19
|
+
careLevel: CareLevel;
|
|
20
|
+
/** Seconds post-combat before first treatment can be applied. Default: 0. */
|
|
21
|
+
onsetDelay_s?: number;
|
|
22
|
+
/** Item inventory; if undefined, assume unlimited supply. */
|
|
23
|
+
inventory?: Map<string, number>;
|
|
24
|
+
/**
|
|
25
|
+
* Phase 34: surgeon's bodily-kinesthetic precision multiplier on surgery rate.
|
|
26
|
+
* Use computeSurgicalPrecision(surgeon) from src/competence/crafting.ts.
|
|
27
|
+
* Default q(1.0) (no effect).
|
|
28
|
+
*/
|
|
29
|
+
surgicalPrecisionMul?: Q;
|
|
30
|
+
}
|
|
31
|
+
export interface DowntimeConfig {
|
|
32
|
+
/** entityId → treatment schedule. Entities not in this map are skipped. */
|
|
33
|
+
treatments: Map<number, TreatmentSchedule>;
|
|
34
|
+
ambientTemperature_Q?: Q;
|
|
35
|
+
/** Entities at rest heal 1.5× faster. */
|
|
36
|
+
rest: boolean;
|
|
37
|
+
/** Phase 29: ambient temperature in Phase 29 Q encoding (q(0.5) = 37°C).
|
|
38
|
+
* When present, stepCoreTemp is called once per simulated second. */
|
|
39
|
+
thermalAmbient_Q?: Q;
|
|
40
|
+
}
|
|
41
|
+
export interface ResourceUsage {
|
|
42
|
+
resourceId: string;
|
|
43
|
+
name: string;
|
|
44
|
+
count: number;
|
|
45
|
+
totalCost: number;
|
|
46
|
+
}
|
|
47
|
+
/** Lightweight injury snapshot for start/end comparison. */
|
|
48
|
+
export interface InjurySummary {
|
|
49
|
+
dead: boolean;
|
|
50
|
+
consciousness: number;
|
|
51
|
+
fluidLoss: number;
|
|
52
|
+
shock: number;
|
|
53
|
+
activeBleedingRegions: string[];
|
|
54
|
+
fracturedRegions: string[];
|
|
55
|
+
infectedRegions: string[];
|
|
56
|
+
maxStructuralDamage: number;
|
|
57
|
+
}
|
|
58
|
+
export interface EntityRecoveryReport {
|
|
59
|
+
entityId: number;
|
|
60
|
+
elapsedSeconds: number;
|
|
61
|
+
injuryAtStart: InjurySummary;
|
|
62
|
+
injuryAtEnd: InjurySummary;
|
|
63
|
+
died: boolean;
|
|
64
|
+
bleedingStopped: boolean;
|
|
65
|
+
infectionCleared: boolean;
|
|
66
|
+
fracturesSet: boolean;
|
|
67
|
+
combatReadyAt_s: number | null;
|
|
68
|
+
fullRecoveryAt_s: number | null;
|
|
69
|
+
resourcesUsed: ResourceUsage[];
|
|
70
|
+
totalCostUnits: number;
|
|
71
|
+
log: Array<{
|
|
72
|
+
second: number;
|
|
73
|
+
text: string;
|
|
74
|
+
}>;
|
|
75
|
+
/**
|
|
76
|
+
* Full final injury state after the simulated recovery period.
|
|
77
|
+
* Use this in campaign/persistence layers to update `entity.injury`
|
|
78
|
+
* with the healed state between sessions.
|
|
79
|
+
*/
|
|
80
|
+
finalInjury?: InjuryState;
|
|
81
|
+
/** Phase 29: final core temperature Q after simulated recovery period. */
|
|
82
|
+
finalCoreTemp_Q?: Q;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Simulate wound recovery over `elapsedSeconds` at 1 Hz.
|
|
86
|
+
* Reads entity injury states from `world`; returns per-entity recovery reports.
|
|
87
|
+
* Does NOT mutate the world.
|
|
88
|
+
*/
|
|
89
|
+
export declare function stepDowntime(world: WorldState, elapsedSeconds: number, config: DowntimeConfig): EntityRecoveryReport[];
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// src/downtime.ts — Phase 19: Downtime & Recovery Simulation
|
|
2
|
+
//
|
|
3
|
+
// Time-scale bridge between 20 Hz combat and hours-to-days wound recovery.
|
|
4
|
+
// Each 1-second simulation step applies the same physics as 1 kernel tick,
|
|
5
|
+
// giving 20× slower wall-clock healing than real-time combat — appropriate
|
|
6
|
+
// for the hours-to-days recovery scale.
|
|
7
|
+
//
|
|
8
|
+
// No kernel import — only types and rate constants are used from engine modules.
|
|
9
|
+
import { q, clampQ, qMul, mulDiv, SCALE } from "./units.js";
|
|
10
|
+
import { FRACTURE_THRESHOLD } from "./sim/injury.js";
|
|
11
|
+
import { TIER_MUL } from "./sim/medical.js";
|
|
12
|
+
import { DT_S } from "./sim/tick.js";
|
|
13
|
+
import { computeNewCoreQ, sumArmourInsulation, CORE_TEMP_NORMAL_Q, } from "./sim/thermoregulation.js";
|
|
14
|
+
import { stepNutrition } from "./sim/nutrition.js";
|
|
15
|
+
import { stepToxicology } from "./sim/toxicology.js";
|
|
16
|
+
export const MEDICAL_RESOURCES = [
|
|
17
|
+
{ id: "bandage", name: "Field bandage", tier: "bandage", costUnits: 1, massGrams: 50 },
|
|
18
|
+
{ id: "suture_kit", name: "Suture kit", tier: "bandage", costUnits: 8, massGrams: 100 },
|
|
19
|
+
{ id: "surgical_kit", name: "Surgical kit", tier: "surgicalKit", costUnits: 60, massGrams: 2000 },
|
|
20
|
+
{ id: "antibiotic_dose", name: "Antibiotic dose", tier: "surgicalKit", costUnits: 15, massGrams: 50 },
|
|
21
|
+
{ id: "iv_fluid_bag", name: "IV fluid bag", tier: "autodoc", costUnits: 25, massGrams: 500 },
|
|
22
|
+
{ id: "autodoc_pack", name: "Autodoc consumable", tier: "autodoc", costUnits: 250, massGrams: 500 },
|
|
23
|
+
{ id: "nanomed_dose", name: "Nanomed dose", tier: "nanomedicine", costUnits: 2000, massGrams: 50 },
|
|
24
|
+
];
|
|
25
|
+
const RESOURCE_BY_ID = new Map(MEDICAL_RESOURCES.map(r => [r.id, r]));
|
|
26
|
+
// ── Internal rate constants ───────────────────────────────────────────────────
|
|
27
|
+
//
|
|
28
|
+
// Each 1-second downtime step uses the same constants as 1 kernel tick.
|
|
29
|
+
// Sources: src/sim/step/injury.ts (clotting, infection), src/sim/kernel.ts (treatment).
|
|
30
|
+
const CLOT_RATE = q(0.0002); // structureIntegrity × this per step → bleed reduction
|
|
31
|
+
const INFECT_ONSET_SEC = 100; // continuous-bleed seconds before infection can start
|
|
32
|
+
const INFECT_DMG_RATE = q(0.0003); // internal damage added per step while infected
|
|
33
|
+
const BANDAGE_RATE = q(0.0050); // base bleed reduction per step (bandage tier)
|
|
34
|
+
const SURGERY_RATE = q(0.0020); // base structural repair per step (surgicalKit tier)
|
|
35
|
+
const FLUID_REPL_RATE = q(0.0050); // base fluid restoration per step (autodoc tier)
|
|
36
|
+
const SHOCK_FROM_FLUID = q(0.0040); // shock += fluidLoss × this per step
|
|
37
|
+
const SHOCK_FROM_INT = q(0.0020); // shock += torsoInternal × this per step
|
|
38
|
+
const CONSC_FROM_SHOCK = q(0.0100); // consciousness -= shock × this per step
|
|
39
|
+
const FATAL_FLUID = q(0.80);
|
|
40
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
41
|
+
function cloneInjury(inj) {
|
|
42
|
+
const byRegion = {};
|
|
43
|
+
for (const [r, ri] of Object.entries(inj.byRegion))
|
|
44
|
+
byRegion[r] = { ...ri };
|
|
45
|
+
return { ...inj, byRegion };
|
|
46
|
+
}
|
|
47
|
+
function captureInjurySummary(inj) {
|
|
48
|
+
const activeBleedingRegions = [];
|
|
49
|
+
const fracturedRegions = [];
|
|
50
|
+
const infectedRegions = [];
|
|
51
|
+
let maxStr = 0;
|
|
52
|
+
for (const [r, ri] of Object.entries(inj.byRegion)) {
|
|
53
|
+
if (ri.bleedingRate > 0)
|
|
54
|
+
activeBleedingRegions.push(r);
|
|
55
|
+
if (ri.fractured)
|
|
56
|
+
fracturedRegions.push(r);
|
|
57
|
+
if (ri.infectedTick >= 0)
|
|
58
|
+
infectedRegions.push(r);
|
|
59
|
+
if (ri.structuralDamage > maxStr)
|
|
60
|
+
maxStr = ri.structuralDamage;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
dead: inj.dead,
|
|
64
|
+
consciousness: inj.consciousness / SCALE.Q,
|
|
65
|
+
fluidLoss: inj.fluidLoss / SCALE.Q,
|
|
66
|
+
shock: inj.shock / SCALE.Q,
|
|
67
|
+
activeBleedingRegions,
|
|
68
|
+
fracturedRegions,
|
|
69
|
+
infectedRegions,
|
|
70
|
+
maxStructuralDamage: maxStr / SCALE.Q,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Treatment tier for most actions at a given care level. */
|
|
74
|
+
function careTier(care) {
|
|
75
|
+
switch (care) {
|
|
76
|
+
case "first_aid": return "bandage";
|
|
77
|
+
case "field_medicine": return "surgicalKit";
|
|
78
|
+
case "hospital": return "surgicalKit";
|
|
79
|
+
case "autodoc": return "autodoc";
|
|
80
|
+
default: return "none";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Treatment tier for fluid replacement (needs autodoc regardless of care level). */
|
|
84
|
+
function fluidTier(care) {
|
|
85
|
+
return (care === "hospital" || care === "autodoc") ? "autodoc" : "none";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Try to consume one unit of a resource from the inventory.
|
|
89
|
+
* Unlimited (undefined inventory) always succeeds and still tracks usage.
|
|
90
|
+
*/
|
|
91
|
+
function tryConsume(state, resourceId) {
|
|
92
|
+
if (state.inventory === undefined) {
|
|
93
|
+
state.usageMap.set(resourceId, (state.usageMap.get(resourceId) ?? 0) + 1);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const avail = state.inventory.get(resourceId) ?? 0;
|
|
97
|
+
if (avail <= 0)
|
|
98
|
+
return false;
|
|
99
|
+
state.inventory.set(resourceId, avail - 1);
|
|
100
|
+
state.usageMap.set(resourceId, (state.usageMap.get(resourceId) ?? 0) + 1);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
// ── Per-second simulation step ────────────────────────────────────────────────
|
|
104
|
+
function stepSecond(state, second, config) {
|
|
105
|
+
const inj = state.injury;
|
|
106
|
+
if (inj.dead)
|
|
107
|
+
return;
|
|
108
|
+
const onset = state.schedule.onsetDelay_s ?? 0;
|
|
109
|
+
const restMul = config.rest ? q(1.50) : q(1.0);
|
|
110
|
+
// ── 1. Natural clotting + infection tracking ──────────────────────────────
|
|
111
|
+
for (const [region, reg] of Object.entries(inj.byRegion)) {
|
|
112
|
+
if (reg.bleedingRate > 0) {
|
|
113
|
+
const integrity = clampQ((SCALE.Q - reg.structuralDamage), 0, SCALE.Q);
|
|
114
|
+
const clot = mulDiv(qMul(integrity, CLOT_RATE), restMul, SCALE.Q);
|
|
115
|
+
reg.bleedingRate = clampQ((reg.bleedingRate - clot), q(0), q(1.0));
|
|
116
|
+
const dur = (state.bleedDurationSec.get(region) ?? 0) + 1;
|
|
117
|
+
state.bleedDurationSec.set(region, dur);
|
|
118
|
+
if (dur >= INFECT_ONSET_SEC
|
|
119
|
+
&& reg.internalDamage > q(0.10)
|
|
120
|
+
&& reg.infectedTick < 0) {
|
|
121
|
+
reg.infectedTick = second;
|
|
122
|
+
state.log.push({ second, text: `infection onset: ${region}` });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const dur = state.bleedDurationSec.get(region) ?? 0;
|
|
127
|
+
if (dur > 0)
|
|
128
|
+
state.bleedDurationSec.set(region, dur - 1);
|
|
129
|
+
}
|
|
130
|
+
// Infection damage progression
|
|
131
|
+
if (reg.infectedTick >= 0) {
|
|
132
|
+
reg.internalDamage = clampQ((reg.internalDamage + INFECT_DMG_RATE), 0, SCALE.Q);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// ── 2. Treatment (after onset delay) ──────────────────────────────────────
|
|
136
|
+
if (second >= onset && state.schedule.careLevel !== "none") {
|
|
137
|
+
applyTreatment(state, second, restMul);
|
|
138
|
+
}
|
|
139
|
+
// ── 3. Fluid loss from bleeding (same formula as kernel per tick) ──────────
|
|
140
|
+
let totalFluidThisSec = q(0);
|
|
141
|
+
for (const reg of Object.values(inj.byRegion)) {
|
|
142
|
+
if (reg.bleedingRate > 0) {
|
|
143
|
+
totalFluidThisSec = clampQ((totalFluidThisSec + mulDiv(reg.bleedingRate, DT_S, SCALE.s)), 0, SCALE.Q);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
inj.fluidLoss = clampQ((inj.fluidLoss + totalFluidThisSec), 0, SCALE.Q);
|
|
147
|
+
// ── 4. Shock accumulation ─────────────────────────────────────────────────
|
|
148
|
+
const torsoInt = inj.byRegion["torso"]?.internalDamage
|
|
149
|
+
?? (Object.values(inj.byRegion)[0]?.internalDamage ?? q(0));
|
|
150
|
+
const shockInc = clampQ((qMul(inj.fluidLoss, SHOCK_FROM_FLUID) + qMul(torsoInt, SHOCK_FROM_INT)), 0, SCALE.Q);
|
|
151
|
+
inj.shock = clampQ((inj.shock + shockInc), 0, SCALE.Q);
|
|
152
|
+
// ── 5. Consciousness loss ─────────────────────────────────────────────────
|
|
153
|
+
const conscLoss = qMul(inj.shock, CONSC_FROM_SHOCK);
|
|
154
|
+
inj.consciousness = clampQ((inj.consciousness - conscLoss), 0, SCALE.Q);
|
|
155
|
+
// ── 6. Death check ────────────────────────────────────────────────────────
|
|
156
|
+
if (inj.fluidLoss >= FATAL_FLUID || inj.shock >= SCALE.Q || inj.consciousness === 0) {
|
|
157
|
+
inj.dead = true;
|
|
158
|
+
inj.consciousness = q(0);
|
|
159
|
+
state.log.push({ second, text: "entity died" });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function applyTreatment(state, second, restMul) {
|
|
163
|
+
const inj = state.injury;
|
|
164
|
+
const care = state.schedule.careLevel;
|
|
165
|
+
const tier = careTier(care);
|
|
166
|
+
const effectMul = TIER_MUL[tier];
|
|
167
|
+
const precMul = (state.schedule.surgicalPrecisionMul ?? SCALE.Q);
|
|
168
|
+
const bleedRed = mulDiv(qMul(BANDAGE_RATE, restMul), effectMul, SCALE.Q);
|
|
169
|
+
const surgRed = mulDiv(mulDiv(qMul(SURGERY_RATE, restMul), effectMul, SCALE.Q), precMul, SCALE.Q);
|
|
170
|
+
for (const [region, reg] of Object.entries(inj.byRegion)) {
|
|
171
|
+
// Tourniquet: first time a region is seen bleeding, apply one bandage
|
|
172
|
+
if (reg.bleedingRate > 0 && !state.tourniquetedRegions.has(region)) {
|
|
173
|
+
if (tryConsume(state, "bandage")) {
|
|
174
|
+
state.tourniquetedRegions.add(region);
|
|
175
|
+
reg.bleedingRate = q(0);
|
|
176
|
+
reg.bleedDuration_ticks = 0;
|
|
177
|
+
state.bleedDurationSec.set(region, 0);
|
|
178
|
+
state.log.push({ second, text: `tourniquet: ${region}` });
|
|
179
|
+
continue; // no further bleed processing this step
|
|
180
|
+
}
|
|
181
|
+
// Out of bandages — fall through to rate reduction only
|
|
182
|
+
}
|
|
183
|
+
// Ongoing bleed reduction (bandage-rate)
|
|
184
|
+
if (reg.bleedingRate > 0) {
|
|
185
|
+
reg.bleedingRate = clampQ((reg.bleedingRate - bleedRed), q(0), q(1.0));
|
|
186
|
+
}
|
|
187
|
+
// Surgery for fractures (field_medicine+)
|
|
188
|
+
if ((care === "field_medicine" || care === "hospital" || care === "autodoc")
|
|
189
|
+
&& reg.fractured && !state.surgeryStarted.has(region)) {
|
|
190
|
+
if (tryConsume(state, "surgical_kit")) {
|
|
191
|
+
state.surgeryStarted.add(region);
|
|
192
|
+
state.log.push({ second, text: `surgery started: ${region}` });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (state.surgeryStarted.has(region)) {
|
|
196
|
+
const newStr = clampQ((reg.structuralDamage - surgRed), reg.permanentDamage, SCALE.Q);
|
|
197
|
+
reg.structuralDamage = newStr;
|
|
198
|
+
// Surgery also reduces bleeding
|
|
199
|
+
reg.bleedingRate = clampQ((reg.bleedingRate - bleedRed), q(0), q(1.0));
|
|
200
|
+
// Clear fracture once structural drops below threshold
|
|
201
|
+
if (reg.fractured && reg.structuralDamage < FRACTURE_THRESHOLD) {
|
|
202
|
+
reg.fractured = false;
|
|
203
|
+
state.log.push({ second, text: `fracture cleared: ${region}` });
|
|
204
|
+
}
|
|
205
|
+
// Surgery also clears infection
|
|
206
|
+
if (reg.infectedTick >= 0) {
|
|
207
|
+
reg.infectedTick = -1;
|
|
208
|
+
state.antibioticsApplied.add(region);
|
|
209
|
+
state.log.push({ second, text: `infection cleared (surgery): ${region}` });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Antibiotics for infection (field_medicine+, not already cleared)
|
|
213
|
+
if ((care === "field_medicine" || care === "hospital" || care === "autodoc")
|
|
214
|
+
&& reg.infectedTick >= 0 && !state.antibioticsApplied.has(region)) {
|
|
215
|
+
if (tryConsume(state, "antibiotic_dose")) {
|
|
216
|
+
state.antibioticsApplied.add(region);
|
|
217
|
+
reg.infectedTick = -1;
|
|
218
|
+
state.log.push({ second, text: `antibiotics: ${region}` });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// IV fluid replacement (hospital+)
|
|
223
|
+
if ((care === "hospital" || care === "autodoc") && inj.fluidLoss > 0) {
|
|
224
|
+
if (!state.fluidReplStarted) {
|
|
225
|
+
if (tryConsume(state, "iv_fluid_bag")) {
|
|
226
|
+
state.fluidReplStarted = true;
|
|
227
|
+
state.log.push({ second, text: "IV fluid replacement started" });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (state.fluidReplStarted) {
|
|
231
|
+
const fMul = TIER_MUL[fluidTier(care)];
|
|
232
|
+
const fluidRec = mulDiv(qMul(FLUID_REPL_RATE, restMul), fMul, SCALE.Q);
|
|
233
|
+
inj.fluidLoss = clampQ((inj.fluidLoss - fluidRec), q(0), SCALE.Q);
|
|
234
|
+
inj.shock = clampQ((inj.shock - q(0.002)), q(0), SCALE.Q);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Autodoc consumable (autodoc only, consumed once per session)
|
|
238
|
+
if (care === "autodoc" && !state.autodocStarted) {
|
|
239
|
+
if (tryConsume(state, "autodoc_pack")) {
|
|
240
|
+
state.autodocStarted = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// ── Recovery projection ───────────────────────────────────────────────────────
|
|
245
|
+
function projectRecovery(state, endSummary, elapsedSec, config) {
|
|
246
|
+
if (endSummary.dead)
|
|
247
|
+
return { combatReadyAt_s: null, fullRecoveryAt_s: null };
|
|
248
|
+
const inj = state.injury;
|
|
249
|
+
const care = state.schedule.careLevel;
|
|
250
|
+
const restF = config.rest ? 1.5 : 1.0; // real multiplier for projection math
|
|
251
|
+
// ── Combat-ready: no active bleeding and shock < 30% ──────────────────────
|
|
252
|
+
const alreadyCombatReady = endSummary.activeBleedingRegions.length === 0 && endSummary.shock < 0.30;
|
|
253
|
+
let combatReadyAt_s;
|
|
254
|
+
if (alreadyCombatReady) {
|
|
255
|
+
combatReadyAt_s = elapsedSec;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// Estimate based on slowest-clotting bleeding region
|
|
259
|
+
let worstBleedStopSec = 0;
|
|
260
|
+
for (const r of endSummary.activeBleedingRegions) {
|
|
261
|
+
const reg = inj.byRegion[r];
|
|
262
|
+
if (!reg || reg.bleedingRate <= 0)
|
|
263
|
+
continue;
|
|
264
|
+
const integrityF = Math.max(0, SCALE.Q - reg.structuralDamage) / SCALE.Q;
|
|
265
|
+
const clotPerSec = integrityF * (CLOT_RATE / SCALE.Q) * restF;
|
|
266
|
+
const secToStop = clotPerSec > 0
|
|
267
|
+
? (reg.bleedingRate / SCALE.Q) / clotPerSec
|
|
268
|
+
: 3600;
|
|
269
|
+
worstBleedStopSec = Math.max(worstBleedStopSec, secToStop);
|
|
270
|
+
}
|
|
271
|
+
const shockBuffer = endSummary.shock >= 0.30 ? 120 : 0;
|
|
272
|
+
combatReadyAt_s = elapsedSec + Math.ceil(worstBleedStopSec) + shockBuffer;
|
|
273
|
+
}
|
|
274
|
+
// ── Full recovery: all structural damage at permanentDamage floor ──────────
|
|
275
|
+
if (care === "none" || care === "first_aid") {
|
|
276
|
+
// No structural treatment — can't estimate a full recovery date
|
|
277
|
+
return { combatReadyAt_s, fullRecoveryAt_s: null };
|
|
278
|
+
}
|
|
279
|
+
let totalRemainingFP = 0;
|
|
280
|
+
for (const reg of Object.values(inj.byRegion)) {
|
|
281
|
+
totalRemainingFP += Math.max(0, reg.structuralDamage - reg.permanentDamage);
|
|
282
|
+
}
|
|
283
|
+
if (totalRemainingFP <= 0) {
|
|
284
|
+
return { combatReadyAt_s, fullRecoveryAt_s: elapsedSec };
|
|
285
|
+
}
|
|
286
|
+
const tierMulF = TIER_MUL[careTier(care)] / SCALE.Q;
|
|
287
|
+
const surgPerSec = (SURGERY_RATE / SCALE.Q) * tierMulF * restF;
|
|
288
|
+
const secsToHeal = surgPerSec > 0
|
|
289
|
+
? (totalRemainingFP / SCALE.Q) / surgPerSec
|
|
290
|
+
: null;
|
|
291
|
+
return {
|
|
292
|
+
combatReadyAt_s,
|
|
293
|
+
fullRecoveryAt_s: secsToHeal !== null
|
|
294
|
+
? elapsedSec + Math.ceil(secsToHeal)
|
|
295
|
+
: null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
299
|
+
/**
|
|
300
|
+
* Simulate wound recovery over `elapsedSeconds` at 1 Hz.
|
|
301
|
+
* Reads entity injury states from `world`; returns per-entity recovery reports.
|
|
302
|
+
* Does NOT mutate the world.
|
|
303
|
+
*/
|
|
304
|
+
export function stepDowntime(world, elapsedSeconds, config) {
|
|
305
|
+
const reports = [];
|
|
306
|
+
for (const entity of world.entities) {
|
|
307
|
+
const schedule = config.treatments.get(entity.id);
|
|
308
|
+
if (!schedule)
|
|
309
|
+
continue;
|
|
310
|
+
const injClone = cloneInjury(entity.injury);
|
|
311
|
+
const injuryAtStart = captureInjurySummary(injClone);
|
|
312
|
+
const state = {
|
|
313
|
+
entityId: entity.id,
|
|
314
|
+
injury: injClone,
|
|
315
|
+
schedule,
|
|
316
|
+
inventory: schedule.inventory ? new Map(schedule.inventory) : undefined,
|
|
317
|
+
usageMap: new Map(),
|
|
318
|
+
log: [],
|
|
319
|
+
tourniquetedRegions: new Set(),
|
|
320
|
+
antibioticsApplied: new Set(),
|
|
321
|
+
surgeryStarted: new Set(),
|
|
322
|
+
fluidReplStarted: false,
|
|
323
|
+
autodocStarted: false,
|
|
324
|
+
bleedDurationSec: new Map(),
|
|
325
|
+
};
|
|
326
|
+
// Pre-populate bleed duration for regions already bleeding on entry
|
|
327
|
+
for (const [region, reg] of Object.entries(injClone.byRegion)) {
|
|
328
|
+
if (reg.bleedDuration_ticks > 0) {
|
|
329
|
+
// Convert existing combat ticks → simulated seconds (1 sec = 1 tick here)
|
|
330
|
+
state.bleedDurationSec.set(region, reg.bleedDuration_ticks);
|
|
331
|
+
}
|
|
332
|
+
if (reg.infectedTick >= 0) {
|
|
333
|
+
// Already infected — mark bleed duration as past threshold
|
|
334
|
+
state.bleedDurationSec.set(region, INFECT_ONSET_SEC);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Phase 29: initialise core temperature tracking (entity is resting during recovery)
|
|
338
|
+
const massReal_kg = entity.attributes.morphology.mass_kg / SCALE.kg;
|
|
339
|
+
const armourInsul = sumArmourInsulation(entity.loadout.items);
|
|
340
|
+
let coreTempQ = (entity.condition).coreTemp_Q ?? CORE_TEMP_NORMAL_Q;
|
|
341
|
+
// Simulation loop
|
|
342
|
+
for (let sec = 0; sec < elapsedSeconds; sec++) {
|
|
343
|
+
if (injClone.dead)
|
|
344
|
+
break;
|
|
345
|
+
stepSecond(state, sec, config);
|
|
346
|
+
// Phase 29: step core temperature once per simulated second (always resting during downtime)
|
|
347
|
+
if (config.thermalAmbient_Q !== undefined) {
|
|
348
|
+
coreTempQ = computeNewCoreQ(coreTempQ, massReal_kg, armourInsul, false /* resting */, config.thermalAmbient_Q, 1.0);
|
|
349
|
+
}
|
|
350
|
+
// Phase 30: nutrition drain once per simulated second (entity resting, activity = 0)
|
|
351
|
+
stepNutrition(entity, 1.0, q(0));
|
|
352
|
+
// Phase 32C: toxicology tick (venoms progress during downtime)
|
|
353
|
+
if (entity.activeVenoms?.length)
|
|
354
|
+
stepToxicology(entity, 1.0);
|
|
355
|
+
}
|
|
356
|
+
const injuryAtEnd = captureInjurySummary(injClone);
|
|
357
|
+
const bleedingStopped = injuryAtEnd.activeBleedingRegions.length === 0;
|
|
358
|
+
const infectionCleared = injuryAtEnd.infectedRegions.length === 0;
|
|
359
|
+
const fracturesSet = state.surgeryStarted.size > 0;
|
|
360
|
+
const { combatReadyAt_s, fullRecoveryAt_s } = projectRecovery(state, injuryAtEnd, elapsedSeconds, config);
|
|
361
|
+
// Collate resource usage
|
|
362
|
+
const resourcesUsed = [];
|
|
363
|
+
let totalCostUnits = 0;
|
|
364
|
+
for (const [rid, count] of state.usageMap) {
|
|
365
|
+
if (count === 0)
|
|
366
|
+
continue;
|
|
367
|
+
const res = RESOURCE_BY_ID.get(rid);
|
|
368
|
+
const cost = (res?.costUnits ?? 0) * count;
|
|
369
|
+
resourcesUsed.push({ resourceId: rid, name: res?.name ?? rid, count, totalCost: cost });
|
|
370
|
+
totalCostUnits += cost;
|
|
371
|
+
}
|
|
372
|
+
reports.push({
|
|
373
|
+
entityId: entity.id,
|
|
374
|
+
elapsedSeconds,
|
|
375
|
+
injuryAtStart,
|
|
376
|
+
injuryAtEnd,
|
|
377
|
+
died: injClone.dead,
|
|
378
|
+
bleedingStopped,
|
|
379
|
+
infectionCleared,
|
|
380
|
+
fracturesSet,
|
|
381
|
+
combatReadyAt_s,
|
|
382
|
+
fullRecoveryAt_s,
|
|
383
|
+
resourcesUsed,
|
|
384
|
+
totalCostUnits,
|
|
385
|
+
log: state.log,
|
|
386
|
+
finalInjury: injClone,
|
|
387
|
+
...(config.thermalAmbient_Q !== undefined ? { finalCoreTemp_Q: coreTempQ } : {}),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return reports;
|
|
391
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Q } from "./units.js";
|
|
2
|
+
import type { Weapon, Item } from "./equipment.js";
|
|
3
|
+
import type { MedicalResource } from "./downtime.js";
|
|
4
|
+
import type { Entity } from "./sim/entity.js";
|
|
5
|
+
export interface ItemValue {
|
|
6
|
+
itemId: string;
|
|
7
|
+
baseValue: number;
|
|
8
|
+
condition_Q: Q;
|
|
9
|
+
sellFraction: number;
|
|
10
|
+
}
|
|
11
|
+
export interface DropTable {
|
|
12
|
+
guaranteed: string[];
|
|
13
|
+
probabilistic: Array<{
|
|
14
|
+
itemId: string;
|
|
15
|
+
chance_Q: Q;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export interface TradeOffer {
|
|
19
|
+
give: Array<{
|
|
20
|
+
itemId: string;
|
|
21
|
+
count: number;
|
|
22
|
+
unitValue: number;
|
|
23
|
+
}>;
|
|
24
|
+
want: Array<{
|
|
25
|
+
itemId: string;
|
|
26
|
+
count: number;
|
|
27
|
+
unitValue: number;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export interface TradeEvaluation {
|
|
31
|
+
/** Positive = advantageous for the accepting party. */
|
|
32
|
+
netValue: number;
|
|
33
|
+
/** True when the accepting party has all "want" items in sufficient quantity. */
|
|
34
|
+
feasible: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Campaign-level item inventory: maps itemId → { count, unitValue }.
|
|
38
|
+
* `unitValue` is in cost units; used by totalInventoryValue.
|
|
39
|
+
*/
|
|
40
|
+
export type ItemInventory = Map<string, {
|
|
41
|
+
count: number;
|
|
42
|
+
unitValue: number;
|
|
43
|
+
}>;
|
|
44
|
+
export interface WearResult {
|
|
45
|
+
wear_Q: Q;
|
|
46
|
+
broke: boolean;
|
|
47
|
+
fumble: boolean;
|
|
48
|
+
penaltyActive: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Base wear increment at full intensity (q(1.0) per strike). */
|
|
51
|
+
export declare const WEAR_BASE: Q;
|
|
52
|
+
/** Wear threshold at which a −5% effective-mass penalty begins. */
|
|
53
|
+
export declare const WEAR_PENALTY_THRESHOLD: Q;
|
|
54
|
+
/** Wear threshold at which a 20% fumble chance triggers. */
|
|
55
|
+
export declare const WEAR_FUMBLE_THRESHOLD: Q;
|
|
56
|
+
/**
|
|
57
|
+
* Derive economic metadata for any item or medical resource.
|
|
58
|
+
*
|
|
59
|
+
* `wear_Q` is the caller's current wear value for the item.
|
|
60
|
+
* - For melee weapons: use `weapon.wear_Q ?? q(0)`.
|
|
61
|
+
* - For armour: derive via `armourConditionQ(resist_J, resistRemaining_J)`.
|
|
62
|
+
* - Omit (defaults to `q(0)`) for consumables or new items.
|
|
63
|
+
*
|
|
64
|
+
* `condition_Q = q(1.0) − wear_Q`, clamped to [0, SCALE.Q].
|
|
65
|
+
*/
|
|
66
|
+
export declare function computeItemValue(item: Item | MedicalResource, wear_Q?: Q): ItemValue;
|
|
67
|
+
/**
|
|
68
|
+
* Convert armour degradation state to a wear_Q fraction.
|
|
69
|
+
*
|
|
70
|
+
* ```
|
|
71
|
+
* wear = q(1.0) − resistRemaining_J / resist_J
|
|
72
|
+
* ```
|
|
73
|
+
* Used to derive `condition_Q` for ablative armour via `computeItemValue`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function armourConditionQ(resist_J: number, resistRemaining_J: number): Q;
|
|
76
|
+
/**
|
|
77
|
+
* Apply one strike's worth of use-wear to a melee weapon.
|
|
78
|
+
*
|
|
79
|
+
* @param weapon - The weapon being used (reads `wear_Q` field if present).
|
|
80
|
+
* @param actionIntensity_Q - Strike intensity (q(0)..q(1.0)); higher = more wear.
|
|
81
|
+
* Use q(1.0) for strikes against hard targets (plate armour),
|
|
82
|
+
* lower values for soft/unarmoured opponents.
|
|
83
|
+
* @param seed - Optional entropy seed for the deterministic fumble roll.
|
|
84
|
+
* Must be supplied when checking fumble; otherwise fumble = false.
|
|
85
|
+
*
|
|
86
|
+
* The returned `wear_Q` should be written back to `weapon.wear_Q` by the caller.
|
|
87
|
+
*/
|
|
88
|
+
export declare function applyWear(weapon: Weapon, actionIntensity_Q: Q, seed?: number): WearResult;
|
|
89
|
+
/**
|
|
90
|
+
* Compute the list of item IDs dropped by an entity on death or incapacitation.
|
|
91
|
+
*
|
|
92
|
+
* Default behaviour:
|
|
93
|
+
* - Dead entity → all equipped weapons, ranged weapons, and armour drop (guaranteed).
|
|
94
|
+
* - Incapacitated but living → nothing drops (use `config.dropOnIncapacitated = true` to override).
|
|
95
|
+
*
|
|
96
|
+
* Additional items from `extra.guaranteed` always drop (when applicable).
|
|
97
|
+
* `extra.probabilistic` items are rolled deterministically from `seed`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveDrops(entity: Entity, seed: number, extra?: DropTable, config?: {
|
|
100
|
+
dropOnIncapacitated?: boolean;
|
|
101
|
+
}): string[];
|
|
102
|
+
/**
|
|
103
|
+
* Evaluate a trade offer from the accepting party's perspective.
|
|
104
|
+
*
|
|
105
|
+
* - `offer.give`: items the proposing party puts on the table.
|
|
106
|
+
* - `offer.want`: items the proposing party asks for in return.
|
|
107
|
+
* - `inventory`: accepting party's current stock.
|
|
108
|
+
*
|
|
109
|
+
* `netValue` > 0 → advantageous for the accepting party (they receive more than they give).
|
|
110
|
+
* `feasible` — the accepting party has all `want` items in sufficient quantities.
|
|
111
|
+
*/
|
|
112
|
+
export declare function evaluateTradeOffer(offer: TradeOffer, inventory: ItemInventory): TradeEvaluation;
|
|
113
|
+
/**
|
|
114
|
+
* Sum the total value of all items in an inventory (count × unitValue for each entry).
|
|
115
|
+
*/
|
|
116
|
+
export declare function totalInventoryValue(inventory: ItemInventory): number;
|