@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4: Sensory environment model.
|
|
3
|
+
*
|
|
4
|
+
* Fixed-point only. No Math.random() — all randomness via eventSeed if needed.
|
|
5
|
+
* Light, smoke, and noise modifiers are Q values (SCALE.Q = full normal conditions).
|
|
6
|
+
*/
|
|
7
|
+
import { SCALE, q, mulDiv } from "../units.js";
|
|
8
|
+
import { getSkill } from "./skills.js";
|
|
9
|
+
// Default perception — used as init guard for entities without Phase 4 attributes.
|
|
10
|
+
export const DEFAULT_PERCEPTION = {
|
|
11
|
+
visionRange_m: Math.trunc(200 * SCALE.m),
|
|
12
|
+
visionArcDeg: 120,
|
|
13
|
+
halfArcCosQ: Math.round(Math.cos((120 / 2) * (Math.PI / 180)) * SCALE.Q),
|
|
14
|
+
hearingRange_m: Math.trunc(50 * SCALE.m),
|
|
15
|
+
decisionLatency_s: Math.trunc(0.5 * SCALE.s),
|
|
16
|
+
attentionDepth: 4,
|
|
17
|
+
threatHorizon_m: Math.trunc(40 * SCALE.m),
|
|
18
|
+
};
|
|
19
|
+
export const DEFAULT_SENSORY_ENV = {
|
|
20
|
+
lightMul: q(1.0),
|
|
21
|
+
smokeMul: q(1.0),
|
|
22
|
+
noiseMul: q(1.0),
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Compute detection quality of `subject` by `observer`.
|
|
26
|
+
*
|
|
27
|
+
* Returns a Q value:
|
|
28
|
+
* q(1.0) = fully visible (within vision arc and range)
|
|
29
|
+
* q(0.4) = heard only (within hearing range but not vision)
|
|
30
|
+
* q(0) = undetected
|
|
31
|
+
*
|
|
32
|
+
* Vision check: dot-product of facing direction vs observer→subject vector.
|
|
33
|
+
* Hearing: omnidirectional.
|
|
34
|
+
*
|
|
35
|
+
* Pure function — no side effects.
|
|
36
|
+
*/
|
|
37
|
+
export function canDetect(observer, subject, env,
|
|
38
|
+
/** Phase 11C: optional sensor boost from the observer's loadout. */
|
|
39
|
+
sensorBoost) {
|
|
40
|
+
const perc = (observer.attributes).perception ?? DEFAULT_PERCEPTION;
|
|
41
|
+
// Phase 10C: blinded observer cannot see
|
|
42
|
+
const isBlind = (observer.condition).blindTicks > 0;
|
|
43
|
+
const dx = subject.position_m.x - observer.position_m.x;
|
|
44
|
+
const dy = subject.position_m.y - observer.position_m.y;
|
|
45
|
+
const dz = subject.position_m.z - observer.position_m.z;
|
|
46
|
+
// Squared distance (still in SCALE.m² fixed-point)
|
|
47
|
+
const dist2 = BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy) + BigInt(dz) * BigInt(dz);
|
|
48
|
+
// ---- Vision ----
|
|
49
|
+
// Phase 10C: blinded observer has zero effective vision range
|
|
50
|
+
let effectiveVision = isBlind ? 0 : mulDiv(mulDiv(perc.visionRange_m, env.lightMul, SCALE.Q), env.smokeMul, SCALE.Q);
|
|
51
|
+
if (!isBlind && sensorBoost)
|
|
52
|
+
effectiveVision = mulDiv(effectiveVision, sensorBoost.visionRangeMul, SCALE.Q);
|
|
53
|
+
const visionR2 = BigInt(effectiveVision) * BigInt(effectiveVision);
|
|
54
|
+
if (dist2 <= visionR2) {
|
|
55
|
+
// Check if subject is within observer's facing arc.
|
|
56
|
+
// For 360° arc (visionArcDeg >= 360) skip the arc check.
|
|
57
|
+
if (perc.visionArcDeg >= 360)
|
|
58
|
+
return q(1.0);
|
|
59
|
+
// Dot product of normalized facing vs direction to subject.
|
|
60
|
+
// We compare in fixed-point Q units using the pre-computed halfArcCosQ.
|
|
61
|
+
const facing = observer.action.facingDirQ;
|
|
62
|
+
const dotQ = dotQ3(facing, dx, dy, dz, dist2);
|
|
63
|
+
if (dotQ >= perc.halfArcCosQ)
|
|
64
|
+
return q(1.0);
|
|
65
|
+
}
|
|
66
|
+
// ---- Hearing ----
|
|
67
|
+
// Phase 7: stealth.dispersionMul reduces subject's acoustic signature
|
|
68
|
+
// (multiplied into observer's effective hearing range for this subject)
|
|
69
|
+
const stealthSkill = getSkill(subject.skills, "stealth");
|
|
70
|
+
let effectiveHearing = mulDiv(mulDiv(perc.hearingRange_m, env.noiseMul, SCALE.Q), stealthSkill.dispersionMul, SCALE.Q);
|
|
71
|
+
if (sensorBoost)
|
|
72
|
+
effectiveHearing = mulDiv(effectiveHearing, sensorBoost.hearingRangeMul, SCALE.Q);
|
|
73
|
+
const hearingR2 = BigInt(effectiveHearing) * BigInt(effectiveHearing);
|
|
74
|
+
if (dist2 <= hearingR2)
|
|
75
|
+
return q(0.4);
|
|
76
|
+
return q(0);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Dot product of a normalized facing direction (Q components) against an unnormalized
|
|
80
|
+
* vector (dx, dy, dz) with squared magnitude dist2. Returns a Q value.
|
|
81
|
+
*
|
|
82
|
+
* facing is already in Q units (each component is Q scaled, magnitude ≈ SCALE.Q).
|
|
83
|
+
* dx/dy/dz are in SCALE.m units; we normalise them into Q using dist_m.
|
|
84
|
+
*
|
|
85
|
+
* Result in Q: positive = same direction, negative = opposite.
|
|
86
|
+
*/
|
|
87
|
+
function dotQ3(facing, dx, dy, dz, dist2) {
|
|
88
|
+
if (dist2 === 0n)
|
|
89
|
+
return q(0);
|
|
90
|
+
// Approximate dist_m via integer sqrt of dist2
|
|
91
|
+
let r = dist2;
|
|
92
|
+
let r1 = (r + 1n) >> 1n;
|
|
93
|
+
while (r1 < r) {
|
|
94
|
+
r = r1;
|
|
95
|
+
r1 = (r + dist2 / r) >> 1n;
|
|
96
|
+
}
|
|
97
|
+
const dist_m = Number(r);
|
|
98
|
+
if (dist_m === 0)
|
|
99
|
+
return q(0);
|
|
100
|
+
// Normalise dx/dy/dz into Q space
|
|
101
|
+
const ndx = mulDiv(dx, SCALE.Q, dist_m);
|
|
102
|
+
const ndy = mulDiv(dy, SCALE.Q, dist_m);
|
|
103
|
+
const ndz = mulDiv(dz, SCALE.Q, dist_m);
|
|
104
|
+
// Both facing and n* are in Q units; dot product → divide by SCALE.Q
|
|
105
|
+
const raw = mulDiv(facing.x, ndx, SCALE.Q)
|
|
106
|
+
+ mulDiv(facing.y, ndy, SCALE.Q)
|
|
107
|
+
+ mulDiv(facing.z, ndz, SCALE.Q);
|
|
108
|
+
return Math.max(-SCALE.Q, Math.min(SCALE.Q, raw));
|
|
109
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type Q, type I32 } from "../units.js";
|
|
2
|
+
export declare const SKILL_IDS: readonly ["meleeCombat", "meleeDefence", "grappling", "rangedCombat", "throwingWeapons", "shieldCraft", "medical", "athleticism", "tactics", "stealth"];
|
|
3
|
+
export type SkillId = (typeof SKILL_IDS)[number];
|
|
4
|
+
/**
|
|
5
|
+
* A SkillLevel is a set of physical outcome modifiers for one skill domain.
|
|
6
|
+
* All fields default to the neutral value (no effect on simulation output).
|
|
7
|
+
*/
|
|
8
|
+
export interface SkillLevel {
|
|
9
|
+
/**
|
|
10
|
+
* Timing offset (SCALE.s units). Negative = faster action or reaction.
|
|
11
|
+
* meleeCombat: reduces attack recovery time (fewer ticks until next attack).
|
|
12
|
+
* tactics: reduces AI decision latency (faster plan revisions).
|
|
13
|
+
*/
|
|
14
|
+
hitTimingOffset_s: I32;
|
|
15
|
+
/**
|
|
16
|
+
* Efficiency multiplier (Q). > q(1.0) = beneficial.
|
|
17
|
+
* meleeCombat: multiplied into strike impact energy.
|
|
18
|
+
* meleeDefence: multiplied into effective defence skill (parry / block quality).
|
|
19
|
+
* grappling: multiplied into grapple contest score (leverage bonus).
|
|
20
|
+
* throwingWeapons: multiplied into thrown weapon launch energy.
|
|
21
|
+
* shieldCraft: multiplied into effective defence skill when blocking with a shield.
|
|
22
|
+
*/
|
|
23
|
+
energyTransferMul: Q;
|
|
24
|
+
/**
|
|
25
|
+
* Dispersion multiplier (Q). < q(1.0) = tighter spread or smaller signature.
|
|
26
|
+
* rangedCombat: multiplied into adjusted dispersion (more accurate fire).
|
|
27
|
+
* stealth: multiplied into the observer's effective hearing range for this
|
|
28
|
+
* entity (reduces acoustic signature — harder to detect by hearing).
|
|
29
|
+
*/
|
|
30
|
+
dispersionMul: Q;
|
|
31
|
+
/**
|
|
32
|
+
* Treatment rate multiplier (Q). > q(1.0) = better self-care.
|
|
33
|
+
* medical: divides the effective bleed-to-fluid-loss increment each tick
|
|
34
|
+
* (passive wound management — slower fluid loss from bleeding).
|
|
35
|
+
*/
|
|
36
|
+
treatmentRateMul: Q;
|
|
37
|
+
/**
|
|
38
|
+
* Fatigue rate multiplier (Q). < q(1.0) = less fatigue per tick.
|
|
39
|
+
* athleticism: multiplied into the fatigue delta each energy tick.
|
|
40
|
+
*/
|
|
41
|
+
fatigueRateMul: Q;
|
|
42
|
+
}
|
|
43
|
+
export type SkillMap = Map<SkillId, SkillLevel>;
|
|
44
|
+
/** Returns a SkillLevel with all fields at the neutral (no-effect) value. */
|
|
45
|
+
export declare function defaultSkillLevel(): SkillLevel;
|
|
46
|
+
/**
|
|
47
|
+
* Build a SkillMap from a partial record.
|
|
48
|
+
* Any missing fields in each entry default to the neutral values.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildSkillMap(entries: Partial<Record<SkillId, Partial<SkillLevel>>>): SkillMap;
|
|
51
|
+
/** Look up a skill level; returns neutral defaults when the map is absent or the skill is not set. */
|
|
52
|
+
export declare function getSkill(skills: SkillMap | undefined, id: SkillId): SkillLevel;
|
|
53
|
+
/**
|
|
54
|
+
* Combine two SkillLevels into one composite level.
|
|
55
|
+
*
|
|
56
|
+
* Use this in the host application to express synergy bonuses or to composite
|
|
57
|
+
* a base skill with a situational modifier before building the SkillMap.
|
|
58
|
+
* The engine itself has no concept of synergies — compositing happens outside.
|
|
59
|
+
*
|
|
60
|
+
* Combination rules:
|
|
61
|
+
* hitTimingOffset_s — additive (both offsets reduce timing independently)
|
|
62
|
+
* energyTransferMul — qMul (efficiency gains multiply)
|
|
63
|
+
* dispersionMul — qMul (tighter spreads multiply)
|
|
64
|
+
* treatmentRateMul — qMul (healing bonuses multiply)
|
|
65
|
+
* fatigueRateMul — qMul (fatigue reductions multiply)
|
|
66
|
+
*
|
|
67
|
+
* Example: meleeCombat synergised with an athleticism bonus
|
|
68
|
+
* buildSkillMap({ meleeCombat: combineSkillLevels(baseMelee, athleticismSynergyBonus) })
|
|
69
|
+
*/
|
|
70
|
+
export declare function combineSkillLevels(a: SkillLevel, b: SkillLevel): SkillLevel;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/sim/skills.ts — Phase 7: Skill System
|
|
2
|
+
//
|
|
3
|
+
// Skills are learned technique modifiers separate from physical attributes.
|
|
4
|
+
// They adjust physical outcomes rather than providing abstract point bonuses.
|
|
5
|
+
// The engine consumes skill values; progression is managed by the host application.
|
|
6
|
+
import { q } from "../units.js";
|
|
7
|
+
export const SKILL_IDS = [
|
|
8
|
+
"meleeCombat",
|
|
9
|
+
"meleeDefence",
|
|
10
|
+
"grappling",
|
|
11
|
+
"rangedCombat",
|
|
12
|
+
"throwingWeapons",
|
|
13
|
+
"shieldCraft",
|
|
14
|
+
"medical",
|
|
15
|
+
"athleticism",
|
|
16
|
+
"tactics",
|
|
17
|
+
"stealth",
|
|
18
|
+
];
|
|
19
|
+
/** Returns a SkillLevel with all fields at the neutral (no-effect) value. */
|
|
20
|
+
export function defaultSkillLevel() {
|
|
21
|
+
return {
|
|
22
|
+
hitTimingOffset_s: 0,
|
|
23
|
+
energyTransferMul: q(1.0),
|
|
24
|
+
dispersionMul: q(1.0),
|
|
25
|
+
treatmentRateMul: q(1.0),
|
|
26
|
+
fatigueRateMul: q(1.0),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a SkillMap from a partial record.
|
|
31
|
+
* Any missing fields in each entry default to the neutral values.
|
|
32
|
+
*/
|
|
33
|
+
export function buildSkillMap(entries) {
|
|
34
|
+
const m = new Map();
|
|
35
|
+
for (const [k, v] of Object.entries(entries)) {
|
|
36
|
+
m.set(k, { ...defaultSkillLevel(), ...v });
|
|
37
|
+
}
|
|
38
|
+
return m;
|
|
39
|
+
}
|
|
40
|
+
/** Look up a skill level; returns neutral defaults when the map is absent or the skill is not set. */
|
|
41
|
+
export function getSkill(skills, id) {
|
|
42
|
+
return skills?.get(id) ?? defaultSkillLevel();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Combine two SkillLevels into one composite level.
|
|
46
|
+
*
|
|
47
|
+
* Use this in the host application to express synergy bonuses or to composite
|
|
48
|
+
* a base skill with a situational modifier before building the SkillMap.
|
|
49
|
+
* The engine itself has no concept of synergies — compositing happens outside.
|
|
50
|
+
*
|
|
51
|
+
* Combination rules:
|
|
52
|
+
* hitTimingOffset_s — additive (both offsets reduce timing independently)
|
|
53
|
+
* energyTransferMul — qMul (efficiency gains multiply)
|
|
54
|
+
* dispersionMul — qMul (tighter spreads multiply)
|
|
55
|
+
* treatmentRateMul — qMul (healing bonuses multiply)
|
|
56
|
+
* fatigueRateMul — qMul (fatigue reductions multiply)
|
|
57
|
+
*
|
|
58
|
+
* Example: meleeCombat synergised with an athleticism bonus
|
|
59
|
+
* buildSkillMap({ meleeCombat: combineSkillLevels(baseMelee, athleticismSynergyBonus) })
|
|
60
|
+
*/
|
|
61
|
+
export function combineSkillLevels(a, b) {
|
|
62
|
+
return {
|
|
63
|
+
hitTimingOffset_s: (a.hitTimingOffset_s + b.hitTimingOffset_s),
|
|
64
|
+
energyTransferMul: Math.trunc(a.energyTransferMul * b.energyTransferMul / 10_000),
|
|
65
|
+
dispersionMul: Math.trunc(a.dispersionMul * b.dispersionMul / 10_000),
|
|
66
|
+
treatmentRateMul: Math.trunc(a.treatmentRateMul * b.treatmentRateMul / 10_000),
|
|
67
|
+
fatigueRateMul: Math.trunc(a.fatigueRateMul * b.fatigueRateMul / 10_000),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type Q } from "../units.js";
|
|
2
|
+
import type { IndividualAttributes } from "../types.js";
|
|
3
|
+
import type { Entity } from "./entity.js";
|
|
4
|
+
/** Current sleep phase. "awake" when the entity is not sleeping. */
|
|
5
|
+
export type SleepPhase = "awake" | "light" | "deep" | "rem";
|
|
6
|
+
/** Deprivation-driven attribute multipliers (all Q). */
|
|
7
|
+
export interface SleepDeprivationMuls {
|
|
8
|
+
/** Fluid cognition multiplier: degrades fastest under sleep loss [Q]. */
|
|
9
|
+
cognitionFluid_Q: Q;
|
|
10
|
+
/** Reaction time multiplier: > q(1.0) = slower reaction [Q]. */
|
|
11
|
+
reactionTime_Q: Q;
|
|
12
|
+
/** Balance / postural stability multiplier [Q]. */
|
|
13
|
+
stability_Q: Q;
|
|
14
|
+
/** Distress tolerance multiplier: emotional dysregulation [Q]. */
|
|
15
|
+
distressTolerance_Q: Q;
|
|
16
|
+
}
|
|
17
|
+
/** Per-entity sleep state stored on `entity.sleep`. */
|
|
18
|
+
export interface SleepState {
|
|
19
|
+
/** Current sleep phase ("awake" when not sleeping). */
|
|
20
|
+
phase: SleepPhase;
|
|
21
|
+
/** Seconds spent in the current phase. */
|
|
22
|
+
phaseSeconds: number;
|
|
23
|
+
/** Cumulative sleep deficit in seconds (capped at MAX_SLEEP_DEBT_S). */
|
|
24
|
+
sleepDebt_s: number;
|
|
25
|
+
/** Continuous seconds since last sleep bout. Resets to 0 on sleep onset. */
|
|
26
|
+
awakeSeconds: number;
|
|
27
|
+
}
|
|
28
|
+
/** Optimal sleep duration per 24-hour period [s]. */
|
|
29
|
+
export declare const OPTIMAL_SLEEP_S: number;
|
|
30
|
+
/** Optimal waking duration per 24-hour period [s]. */
|
|
31
|
+
export declare const OPTIMAL_AWAKE_S: number;
|
|
32
|
+
/** Continuous wake time above which cognitive/motor impairment begins [s]. */
|
|
33
|
+
export declare const IMPAIR_THRESHOLD_S: number;
|
|
34
|
+
/** Maximum sleep debt tracked (3 days of total sleep deprivation) [s]. */
|
|
35
|
+
export declare const MAX_SLEEP_DEBT_S: number;
|
|
36
|
+
/** Coefficient for cognition fluid degradation per unit impair fraction [numeric]. */
|
|
37
|
+
export declare const COGNITION_FLUID_COEFF = 0.798;
|
|
38
|
+
/** Coefficient for reaction time slowdown per unit impair fraction [numeric]. */
|
|
39
|
+
export declare const REACTION_TIME_COEFF = 0.45;
|
|
40
|
+
/** Coefficient for stability degradation per unit impair fraction [numeric]. */
|
|
41
|
+
export declare const STABILITY_COEFF = 0.25;
|
|
42
|
+
/** Coefficient for distress tolerance degradation per unit impair fraction [numeric]. */
|
|
43
|
+
export declare const DISTRESS_TOLERANCE_COEFF = 0.35;
|
|
44
|
+
/** Duration of the light-sleep (NREM-1/2) phase per cycle [s]. */
|
|
45
|
+
export declare const LIGHT_PHASE_S: number;
|
|
46
|
+
/** Duration of the deep-sleep (slow-wave) phase per cycle [s]. */
|
|
47
|
+
export declare const DEEP_PHASE_S: number;
|
|
48
|
+
/** Duration of the REM phase per cycle [s]. */
|
|
49
|
+
export declare const REM_PHASE_S: number;
|
|
50
|
+
/**
|
|
51
|
+
* Circadian alertness at a given time of day.
|
|
52
|
+
*
|
|
53
|
+
* @param hourOfDay Float in [0, 24). Values outside this range are normalised.
|
|
54
|
+
* @returns Q in [q(0.30), q(1.0)]: q(1.0) at ~17:00, q(0.30) at ~03:00.
|
|
55
|
+
*/
|
|
56
|
+
export declare function circadianAlertness(hourOfDay: number): Q;
|
|
57
|
+
/**
|
|
58
|
+
* Derive sleep-deprivation attribute multipliers from the entity's sleep state.
|
|
59
|
+
*
|
|
60
|
+
* Impairment is driven by the greater of:
|
|
61
|
+
* - `awakeSeconds` — continuous wake duration (resets on sleep)
|
|
62
|
+
* - `sleepDebt_s` — cumulative shortfall from prior nights
|
|
63
|
+
*
|
|
64
|
+
* Below IMPAIR_THRESHOLD_S (17 h) both drivers produce no impairment.
|
|
65
|
+
* Full impairment is reached at MAX_SLEEP_DEBT_S (72 h).
|
|
66
|
+
*
|
|
67
|
+
* Multiplier ranges at max deprivation:
|
|
68
|
+
* cognitionFluid_Q: q(1.0) → q(0.202) (−79.8%)
|
|
69
|
+
* reactionTime_Q: q(1.0) → q(1.45) (+45% slower)
|
|
70
|
+
* stability_Q: q(1.0) → q(0.75) (−25%)
|
|
71
|
+
* distressTolerance_Q: q(1.0) → q(0.65) (−35%)
|
|
72
|
+
*/
|
|
73
|
+
export declare function deriveSleepDeprivationMuls(state: SleepState): SleepDeprivationMuls;
|
|
74
|
+
/**
|
|
75
|
+
* Advance an entity's sleep state by `elapsedSeconds`.
|
|
76
|
+
*
|
|
77
|
+
* When `isSleeping = false` (awake):
|
|
78
|
+
* - `awakeSeconds` accumulates.
|
|
79
|
+
* - `sleepDebt_s` accrues at ½ s/s for each second spent beyond OPTIMAL_AWAKE_S.
|
|
80
|
+
* (16 h waking × ½ = 8 h debt — exactly one night's repayment if sleep was ideal.)
|
|
81
|
+
* - Phase stays or transitions to "awake".
|
|
82
|
+
*
|
|
83
|
+
* When `isSleeping = true`:
|
|
84
|
+
* - On sleep onset (phase was "awake"): `awakeSeconds` resets to 0; phase enters "light".
|
|
85
|
+
* - `sleepDebt_s` decrements 1:1 with elapsed sleep time (floored at 0).
|
|
86
|
+
* - Phase cycles: light → deep → rem → light (90-minute NREM/REM cycle).
|
|
87
|
+
*
|
|
88
|
+
* Mutates: `entity.sleep`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function stepSleep(entity: Entity, elapsedSeconds: number, isSleeping: boolean): void;
|
|
91
|
+
/**
|
|
92
|
+
* Apply sleep-deprivation multipliers to a base attribute set, returning a new object.
|
|
93
|
+
*
|
|
94
|
+
* Attributes affected:
|
|
95
|
+
* - control.reactionTime_s, stability
|
|
96
|
+
* - resilience.distressTolerance
|
|
97
|
+
* - cognition (if present): fluid dimensions (logical, spatial, kinesthetic, musical)
|
|
98
|
+
*
|
|
99
|
+
* Immutable — does not mutate `base`.
|
|
100
|
+
* Pattern matches `applyAgingToAttributes` (Phase 57).
|
|
101
|
+
*/
|
|
102
|
+
export declare function applySleepToAttributes(base: IndividualAttributes, state: SleepState): IndividualAttributes;
|
|
103
|
+
/**
|
|
104
|
+
* Return the entity's accumulated sleep debt in hours.
|
|
105
|
+
* Returns 0 if `entity.sleep` is absent.
|
|
106
|
+
*/
|
|
107
|
+
export declare function entitySleepDebt_h(entity: Entity): number;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// src/sim/sleep.ts — Phase 58: Sleep & Circadian Rhythm
|
|
2
|
+
//
|
|
3
|
+
// Models circadian alertness, sleep-phase cycling, and attribute degradation from
|
|
4
|
+
// sleep deprivation. Designed as a companion to Phase 57 (Aging & Lifespan):
|
|
5
|
+
// same fixed-point arithmetic, same immutable apply… pattern.
|
|
6
|
+
//
|
|
7
|
+
// Two-factor impairment model:
|
|
8
|
+
// awakeSeconds — continuous wake duration since last sleep (primary driver)
|
|
9
|
+
// sleepDebt_s — cumulative shortfall from prior nights (secondary; persists across sleep)
|
|
10
|
+
//
|
|
11
|
+
// Sleep phase cycle (90 min): light (45 min) → deep (25 min) → rem (20 min) → light …
|
|
12
|
+
//
|
|
13
|
+
// Public API:
|
|
14
|
+
// circadianAlertness(hourOfDay) → Q [0..SCALE.Q]
|
|
15
|
+
// deriveSleepDeprivationMuls(state) → SleepDeprivationMuls
|
|
16
|
+
// stepSleep(entity, elapsedSeconds, sleeping) → mutates entity.sleep
|
|
17
|
+
// applySleepToAttributes(base, state) → IndividualAttributes (new object)
|
|
18
|
+
// entitySleepDebt_h(entity) → number
|
|
19
|
+
import { q, clampQ, SCALE } from "../units.js";
|
|
20
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
21
|
+
/** Optimal sleep duration per 24-hour period [s]. */
|
|
22
|
+
export const OPTIMAL_SLEEP_S = 8 * 3600; // 28 800
|
|
23
|
+
/** Optimal waking duration per 24-hour period [s]. */
|
|
24
|
+
export const OPTIMAL_AWAKE_S = 16 * 3600; // 57 600
|
|
25
|
+
/** Continuous wake time above which cognitive/motor impairment begins [s]. */
|
|
26
|
+
export const IMPAIR_THRESHOLD_S = 17 * 3600; // 61 200
|
|
27
|
+
/** Maximum sleep debt tracked (3 days of total sleep deprivation) [s]. */
|
|
28
|
+
export const MAX_SLEEP_DEBT_S = 72 * 3600; // 259 200
|
|
29
|
+
/** Coefficient for cognition fluid degradation per unit impair fraction [numeric]. */
|
|
30
|
+
export const COGNITION_FLUID_COEFF = 0.798;
|
|
31
|
+
/** Coefficient for reaction time slowdown per unit impair fraction [numeric]. */
|
|
32
|
+
export const REACTION_TIME_COEFF = 0.45;
|
|
33
|
+
/** Coefficient for stability degradation per unit impair fraction [numeric]. */
|
|
34
|
+
export const STABILITY_COEFF = 0.25;
|
|
35
|
+
/** Coefficient for distress tolerance degradation per unit impair fraction [numeric]. */
|
|
36
|
+
export const DISTRESS_TOLERANCE_COEFF = 0.35;
|
|
37
|
+
/** Duration of the light-sleep (NREM-1/2) phase per cycle [s]. */
|
|
38
|
+
export const LIGHT_PHASE_S = 45 * 60; // 2 700
|
|
39
|
+
/** Duration of the deep-sleep (slow-wave) phase per cycle [s]. */
|
|
40
|
+
export const DEEP_PHASE_S = 25 * 60; // 1 500
|
|
41
|
+
/** Duration of the REM phase per cycle [s]. */
|
|
42
|
+
export const REM_PHASE_S = 20 * 60; // 1 200
|
|
43
|
+
// ── Circadian alertness ───────────────────────────────────────────────────────
|
|
44
|
+
// Piecewise-linear hourly alertness table [hourOfDay → Q].
|
|
45
|
+
// Peaks at ~17:00 (afternoon), nadir at ~03:00 (pre-dawn), secondary dip at 14:00.
|
|
46
|
+
const CIRCADIAN_KNOTS = [
|
|
47
|
+
[0, q(0.45)],
|
|
48
|
+
[3, q(0.30)], // nadir ~03:00
|
|
49
|
+
[6, q(0.60)], // morning rise
|
|
50
|
+
[10, q(0.95)], // morning peak
|
|
51
|
+
[14, q(0.80)], // post-lunch dip
|
|
52
|
+
[17, q(1.00)], // afternoon peak
|
|
53
|
+
[21, q(0.70)], // evening decline
|
|
54
|
+
[24, q(0.45)], // back to midnight
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* Circadian alertness at a given time of day.
|
|
58
|
+
*
|
|
59
|
+
* @param hourOfDay Float in [0, 24). Values outside this range are normalised.
|
|
60
|
+
* @returns Q in [q(0.30), q(1.0)]: q(1.0) at ~17:00, q(0.30) at ~03:00.
|
|
61
|
+
*/
|
|
62
|
+
export function circadianAlertness(hourOfDay) {
|
|
63
|
+
const h = ((hourOfDay % 24) + 24) % 24;
|
|
64
|
+
for (let i = 1; i < CIRCADIAN_KNOTS.length; i++) {
|
|
65
|
+
const [x0, y0] = CIRCADIAN_KNOTS[i - 1];
|
|
66
|
+
const [x1, y1] = CIRCADIAN_KNOTS[i];
|
|
67
|
+
if (h <= x1) {
|
|
68
|
+
const span = x1 - x0;
|
|
69
|
+
if (span === 0)
|
|
70
|
+
return y0;
|
|
71
|
+
const t = Math.round((h - x0) * SCALE.Q / span);
|
|
72
|
+
return (y0 + Math.round((y1 - y0) * t / SCALE.Q));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return CIRCADIAN_KNOTS[CIRCADIAN_KNOTS.length - 1][1];
|
|
76
|
+
}
|
|
77
|
+
// ── Sleep deprivation ─────────────────────────────────────────────────────────
|
|
78
|
+
/**
|
|
79
|
+
* Derive sleep-deprivation attribute multipliers from the entity's sleep state.
|
|
80
|
+
*
|
|
81
|
+
* Impairment is driven by the greater of:
|
|
82
|
+
* - `awakeSeconds` — continuous wake duration (resets on sleep)
|
|
83
|
+
* - `sleepDebt_s` — cumulative shortfall from prior nights
|
|
84
|
+
*
|
|
85
|
+
* Below IMPAIR_THRESHOLD_S (17 h) both drivers produce no impairment.
|
|
86
|
+
* Full impairment is reached at MAX_SLEEP_DEBT_S (72 h).
|
|
87
|
+
*
|
|
88
|
+
* Multiplier ranges at max deprivation:
|
|
89
|
+
* cognitionFluid_Q: q(1.0) → q(0.202) (−79.8%)
|
|
90
|
+
* reactionTime_Q: q(1.0) → q(1.45) (+45% slower)
|
|
91
|
+
* stability_Q: q(1.0) → q(0.75) (−25%)
|
|
92
|
+
* distressTolerance_Q: q(1.0) → q(0.65) (−35%)
|
|
93
|
+
*/
|
|
94
|
+
export function deriveSleepDeprivationMuls(state) {
|
|
95
|
+
const effectiveS = Math.max(state.awakeSeconds, state.sleepDebt_s);
|
|
96
|
+
const raw = Math.max(0, effectiveS - IMPAIR_THRESHOLD_S);
|
|
97
|
+
const range = MAX_SLEEP_DEBT_S - IMPAIR_THRESHOLD_S; // 198 000
|
|
98
|
+
const impairFrac_Q = clampQ(Math.round(raw * SCALE.Q / range), q(0), SCALE.Q);
|
|
99
|
+
const cognitionFluid_Q = clampQ((SCALE.Q - Math.round(impairFrac_Q * COGNITION_FLUID_COEFF)), q(0), SCALE.Q);
|
|
100
|
+
// > SCALE.Q means slower than baseline (mirrors aging reactionTime_Q convention)
|
|
101
|
+
const reactionTime_Q = (SCALE.Q + Math.round(impairFrac_Q * REACTION_TIME_COEFF));
|
|
102
|
+
const stability_Q = clampQ((SCALE.Q - Math.round(impairFrac_Q * STABILITY_COEFF)), q(0), SCALE.Q);
|
|
103
|
+
const distressTolerance_Q = clampQ((SCALE.Q - Math.round(impairFrac_Q * DISTRESS_TOLERANCE_COEFF)), q(0), SCALE.Q);
|
|
104
|
+
return { cognitionFluid_Q, reactionTime_Q, stability_Q, distressTolerance_Q };
|
|
105
|
+
}
|
|
106
|
+
// ── stepSleep ─────────────────────────────────────────────────────────────────
|
|
107
|
+
/**
|
|
108
|
+
* Advance an entity's sleep state by `elapsedSeconds`.
|
|
109
|
+
*
|
|
110
|
+
* When `isSleeping = false` (awake):
|
|
111
|
+
* - `awakeSeconds` accumulates.
|
|
112
|
+
* - `sleepDebt_s` accrues at ½ s/s for each second spent beyond OPTIMAL_AWAKE_S.
|
|
113
|
+
* (16 h waking × ½ = 8 h debt — exactly one night's repayment if sleep was ideal.)
|
|
114
|
+
* - Phase stays or transitions to "awake".
|
|
115
|
+
*
|
|
116
|
+
* When `isSleeping = true`:
|
|
117
|
+
* - On sleep onset (phase was "awake"): `awakeSeconds` resets to 0; phase enters "light".
|
|
118
|
+
* - `sleepDebt_s` decrements 1:1 with elapsed sleep time (floored at 0).
|
|
119
|
+
* - Phase cycles: light → deep → rem → light (90-minute NREM/REM cycle).
|
|
120
|
+
*
|
|
121
|
+
* Mutates: `entity.sleep`.
|
|
122
|
+
*/
|
|
123
|
+
export function stepSleep(entity, elapsedSeconds, isSleeping) {
|
|
124
|
+
if (!entity.sleep) {
|
|
125
|
+
entity.sleep = { phase: "awake", phaseSeconds: 0, sleepDebt_s: 0, awakeSeconds: 0 };
|
|
126
|
+
}
|
|
127
|
+
const s = entity.sleep;
|
|
128
|
+
if (!isSleeping) {
|
|
129
|
+
if (s.phase !== "awake") {
|
|
130
|
+
s.phase = "awake";
|
|
131
|
+
s.phaseSeconds = 0;
|
|
132
|
+
}
|
|
133
|
+
const prevAwake = s.awakeSeconds;
|
|
134
|
+
s.awakeSeconds += elapsedSeconds;
|
|
135
|
+
s.phaseSeconds += elapsedSeconds;
|
|
136
|
+
// Debt accrues only for time spent beyond the optimal waking window
|
|
137
|
+
const debtStart = Math.max(prevAwake, OPTIMAL_AWAKE_S);
|
|
138
|
+
const debtEnd = s.awakeSeconds;
|
|
139
|
+
if (debtEnd > debtStart) {
|
|
140
|
+
s.sleepDebt_s = Math.min(MAX_SLEEP_DEBT_S, s.sleepDebt_s + Math.round((debtEnd - debtStart) / 2));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
if (s.phase === "awake") {
|
|
145
|
+
// Sleep onset: enter light phase and reset continuous wake timer
|
|
146
|
+
s.phase = "light";
|
|
147
|
+
s.phaseSeconds = 0;
|
|
148
|
+
s.awakeSeconds = 0;
|
|
149
|
+
}
|
|
150
|
+
// Repay debt 1:1 (cannot go below 0)
|
|
151
|
+
s.sleepDebt_s = Math.max(0, s.sleepDebt_s - elapsedSeconds);
|
|
152
|
+
s.phaseSeconds += elapsedSeconds;
|
|
153
|
+
// Advance through NREM/REM cycle (phase is "light"|"deep"|"rem" at this point)
|
|
154
|
+
for (;;) {
|
|
155
|
+
const dur = s.phase === "light" ? LIGHT_PHASE_S
|
|
156
|
+
: s.phase === "deep" ? DEEP_PHASE_S
|
|
157
|
+
: REM_PHASE_S;
|
|
158
|
+
if (s.phaseSeconds < dur)
|
|
159
|
+
break;
|
|
160
|
+
s.phaseSeconds -= dur;
|
|
161
|
+
s.phase = s.phase === "light" ? "deep"
|
|
162
|
+
: s.phase === "deep" ? "rem"
|
|
163
|
+
: "light"; // rem → back to light
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// ── applySleepToAttributes ────────────────────────────────────────────────────
|
|
168
|
+
/**
|
|
169
|
+
* Apply sleep-deprivation multipliers to a base attribute set, returning a new object.
|
|
170
|
+
*
|
|
171
|
+
* Attributes affected:
|
|
172
|
+
* - control.reactionTime_s, stability
|
|
173
|
+
* - resilience.distressTolerance
|
|
174
|
+
* - cognition (if present): fluid dimensions (logical, spatial, kinesthetic, musical)
|
|
175
|
+
*
|
|
176
|
+
* Immutable — does not mutate `base`.
|
|
177
|
+
* Pattern matches `applyAgingToAttributes` (Phase 57).
|
|
178
|
+
*/
|
|
179
|
+
export function applySleepToAttributes(base, state) {
|
|
180
|
+
const m = deriveSleepDeprivationMuls(state);
|
|
181
|
+
return {
|
|
182
|
+
...base,
|
|
183
|
+
control: {
|
|
184
|
+
...base.control,
|
|
185
|
+
reactionTime_s: Math.max(1, Math.round(base.control.reactionTime_s * m.reactionTime_Q / SCALE.Q)),
|
|
186
|
+
stability: clampQ(Math.round(base.control.stability * m.stability_Q / SCALE.Q), q(0), SCALE.Q),
|
|
187
|
+
},
|
|
188
|
+
resilience: {
|
|
189
|
+
...base.resilience,
|
|
190
|
+
distressTolerance: clampQ(Math.round(base.resilience.distressTolerance * m.distressTolerance_Q / SCALE.Q), q(0), SCALE.Q),
|
|
191
|
+
},
|
|
192
|
+
// exactOptionalPropertyTypes: spread present cognition, otherwise omit the key entirely.
|
|
193
|
+
...(base.cognition
|
|
194
|
+
? {
|
|
195
|
+
cognition: {
|
|
196
|
+
...base.cognition,
|
|
197
|
+
logicalMathematical: clampQ(Math.round(base.cognition.logicalMathematical * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
|
|
198
|
+
spatial: clampQ(Math.round(base.cognition.spatial * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
|
|
199
|
+
bodilyKinesthetic: clampQ(Math.round(base.cognition.bodilyKinesthetic * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
|
|
200
|
+
musical: clampQ(Math.round(base.cognition.musical * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
: {}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// ── Entity convenience ────────────────────────────────────────────────────────
|
|
207
|
+
/**
|
|
208
|
+
* Return the entity's accumulated sleep debt in hours.
|
|
209
|
+
* Returns 0 if `entity.sleep` is absent.
|
|
210
|
+
*/
|
|
211
|
+
export function entitySleepDebt_h(entity) {
|
|
212
|
+
if (!entity.sleep)
|
|
213
|
+
return 0;
|
|
214
|
+
return entity.sleep.sleepDebt_s / 3600;
|
|
215
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Vec3 } from "./vec3.js";
|
|
2
|
+
import type { WorldState } from "./world.js";
|
|
3
|
+
export interface SpatialIndex {
|
|
4
|
+
cell_m: number;
|
|
5
|
+
cells: Map<number, number[]>;
|
|
6
|
+
}
|
|
7
|
+
export declare function buildSpatialIndex(world: WorldState, cellSize_m: number): SpatialIndex;
|
|
8
|
+
export declare function queryNearbyIds(index: SpatialIndex, pos: Vec3, radius_m: number, maxCount?: number): number[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
function cellCoord(pos_m, cell_m) {
|
|
2
|
+
// floor division that behaves for negative coordinates too
|
|
3
|
+
const q = Math.trunc(pos_m / cell_m);
|
|
4
|
+
return pos_m < 0 && pos_m % cell_m !== 0 ? q - 1 : q;
|
|
5
|
+
}
|
|
6
|
+
function pack(cx, cy) {
|
|
7
|
+
// deterministic 32-bit packing (cx,cy limited by map bounds in practice)
|
|
8
|
+
// offset to avoid negative mixing issues
|
|
9
|
+
const ax = (cx & 0xffff) >>> 0;
|
|
10
|
+
const ay = (cy & 0xffff) >>> 0;
|
|
11
|
+
return ((ax << 16) | ay) >>> 0;
|
|
12
|
+
}
|
|
13
|
+
export function buildSpatialIndex(world, cellSize_m) {
|
|
14
|
+
const cell_m = Math.max(1, Math.trunc(cellSize_m)); // already in fixed-point metres
|
|
15
|
+
const cells = new Map();
|
|
16
|
+
for (const e of world.entities) {
|
|
17
|
+
if (e.injury.dead)
|
|
18
|
+
continue;
|
|
19
|
+
const cx = cellCoord(e.position_m.x, cell_m);
|
|
20
|
+
const cy = cellCoord(e.position_m.y, cell_m);
|
|
21
|
+
const key = pack(cx, cy);
|
|
22
|
+
let arr = cells.get(key);
|
|
23
|
+
if (!arr) {
|
|
24
|
+
arr = [];
|
|
25
|
+
cells.set(key, arr);
|
|
26
|
+
}
|
|
27
|
+
arr.push(e.id);
|
|
28
|
+
}
|
|
29
|
+
// deterministic: sort IDs inside each cell
|
|
30
|
+
for (const arr of cells.values())
|
|
31
|
+
arr.sort((a, b) => a - b);
|
|
32
|
+
return { cell_m, cells };
|
|
33
|
+
}
|
|
34
|
+
export function queryNearbyIds(index, pos, radius_m, maxCount) {
|
|
35
|
+
const cell_m = index.cell_m;
|
|
36
|
+
const r = Math.max(0, radius_m);
|
|
37
|
+
const cx0 = cellCoord(pos.x - r, cell_m);
|
|
38
|
+
const cx1 = cellCoord(pos.x + r, cell_m);
|
|
39
|
+
const cy0 = cellCoord(pos.y - r, cell_m);
|
|
40
|
+
const cy1 = cellCoord(pos.y + r, cell_m);
|
|
41
|
+
const out = [];
|
|
42
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
43
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
44
|
+
const key = pack(cx, cy);
|
|
45
|
+
const ids = index.cells.get(key);
|
|
46
|
+
if (!ids)
|
|
47
|
+
continue;
|
|
48
|
+
// already sorted
|
|
49
|
+
for (const id of ids) {
|
|
50
|
+
out.push(id);
|
|
51
|
+
if (maxCount !== undefined && out.length >= maxCount)
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// deterministic overall order: sort once (cheap; neighbourhood small)
|
|
57
|
+
out.sort((a, b) => a - b);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Entity } from "../entity.js";
|
|
2
|
+
import type { WorldState } from "../world.js";
|
|
3
|
+
import type { KernelContext } from "../context.js";
|
|
4
|
+
/**
|
|
5
|
+
* Per-entity per-tick regen of all capability sources.
|
|
6
|
+
* Called after stepMovement so velocity is current.
|
|
7
|
+
*/
|
|
8
|
+
export declare function stepCapabilitySources(e: Entity, world: WorldState, ctx: KernelContext): void;
|