@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,130 @@
|
|
|
1
|
+
import { SCALE, q, clampQ, qMul, mulDiv } from "../../units.js";
|
|
2
|
+
import { queryNearbyIds } from "../spatial.js";
|
|
3
|
+
import { coverFractionAtPosition } from "../terrain.js";
|
|
4
|
+
import { TraceKinds } from "../kinds.js";
|
|
5
|
+
import { FEAR_PER_SUPPRESSION_TICK, FEAR_FOR_ALLY_DEATH, FEAR_INJURY_MUL, FEAR_OUTNUMBERED, FEAR_ROUTING_CASCADE, fearDecayPerTick, isRouting, LEADER_AURA_FEAR_REDUCTION, BANNER_AURA_FEAR_REDUCTION, AURA_RADIUS_m, RALLY_COOLDOWN_TICKS, } from "../morale.js";
|
|
6
|
+
/**
|
|
7
|
+
* Per-entity morale update — accumulates fear from all sources and applies decay.
|
|
8
|
+
* Emits a MoraleRoute trace event whenever the entity crosses the routing threshold.
|
|
9
|
+
*/
|
|
10
|
+
export function stepMoraleForEntity(world, e, index, spatial, aliveBeforeTick, teamRoutingFrac, trace, ctx) {
|
|
11
|
+
if (e.injury.dead)
|
|
12
|
+
return;
|
|
13
|
+
const distressTolBase = e.attributes.resilience.distressTolerance;
|
|
14
|
+
// Phase 33: intrapersonal intelligence boosts effective distress tolerance
|
|
15
|
+
// Formula: base + intrapersonal × q(0.30); human (0.55) → +0.165; clamped to q(0.98)
|
|
16
|
+
const intrapersonal = e.attributes.cognition?.intrapersonal ?? 0;
|
|
17
|
+
const distressTol = intrapersonal
|
|
18
|
+
? clampQ((distressTolBase + Math.trunc(mulDiv(q(0.30), intrapersonal, SCALE.Q))), q(0.01), q(0.98))
|
|
19
|
+
: distressTolBase;
|
|
20
|
+
const MORALE_RADIUS_m = Math.trunc(30 * SCALE.m); // 30 m awareness radius
|
|
21
|
+
const nearbyIds = queryNearbyIds(spatial, e.position_m, MORALE_RADIUS_m);
|
|
22
|
+
let nearbyAllyCount = 0;
|
|
23
|
+
let nearbyEnemyCount = 0;
|
|
24
|
+
let allyDeathsThisTick = 0;
|
|
25
|
+
for (const nId of nearbyIds) {
|
|
26
|
+
if (nId === e.id)
|
|
27
|
+
continue;
|
|
28
|
+
const neighbor = index.byId.get(nId);
|
|
29
|
+
if (!neighbor)
|
|
30
|
+
continue;
|
|
31
|
+
if (neighbor.teamId === e.teamId) {
|
|
32
|
+
if (!neighbor.injury.dead) {
|
|
33
|
+
nearbyAllyCount++;
|
|
34
|
+
}
|
|
35
|
+
else if (aliveBeforeTick.has(nId)) {
|
|
36
|
+
allyDeathsThisTick++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (!neighbor.injury.dead) {
|
|
40
|
+
nearbyEnemyCount++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Feature 6: berserk entities ignore all fear — always clear fear and return early
|
|
44
|
+
const fearResponse = (e.attributes.resilience).fearResponse ?? "flight";
|
|
45
|
+
if (fearResponse === "berserk") {
|
|
46
|
+
e.condition.fearQ = q(0);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let fearQ = e.condition.fearQ;
|
|
50
|
+
const wasRouting = isRouting(fearQ, distressTol);
|
|
51
|
+
// 1. Suppression ticks add fear per tick — scaled by caliber multiplier (Feature 1)
|
|
52
|
+
if (e.condition.suppressedTicks > 0) {
|
|
53
|
+
const supMul = e.condition.suppressionFearMul ?? SCALE.Q;
|
|
54
|
+
fearQ = clampQ(fearQ + qMul(FEAR_PER_SUPPRESSION_TICK, supMul), 0, SCALE.Q);
|
|
55
|
+
}
|
|
56
|
+
// 2. Ally deaths this tick — with diminishing returns (Feature 2)
|
|
57
|
+
if (allyDeathsThisTick > 0) {
|
|
58
|
+
// Reset window if last death was >100 ticks ago (5s at TICK_HZ=20)
|
|
59
|
+
if (world.tick - e.condition.lastAllyDeathTick > 100) {
|
|
60
|
+
e.condition.recentAllyDeaths = 0;
|
|
61
|
+
}
|
|
62
|
+
// Multiplier: q(1.0) for first death, -q(0.15) per prior, floor q(0.40)
|
|
63
|
+
const mul = Math.max(q(0.40), q(1.0) - Math.trunc(e.condition.recentAllyDeaths * 1500));
|
|
64
|
+
fearQ = clampQ(fearQ + Math.trunc(allyDeathsThisTick * qMul(FEAR_FOR_ALLY_DEATH, mul)), 0, SCALE.Q);
|
|
65
|
+
e.condition.recentAllyDeaths += allyDeathsThisTick;
|
|
66
|
+
e.condition.lastAllyDeathTick = world.tick;
|
|
67
|
+
}
|
|
68
|
+
// 3. Self-injury (shock accumulation) adds fear per tick
|
|
69
|
+
fearQ = clampQ(fearQ + qMul(e.injury.shock, FEAR_INJURY_MUL), 0, SCALE.Q);
|
|
70
|
+
// 4. Being outnumbered by visible enemies adds fear per tick
|
|
71
|
+
// Include self in friendly count: entity + its allies vs enemies.
|
|
72
|
+
if (nearbyEnemyCount > nearbyAllyCount + 1) {
|
|
73
|
+
fearQ = clampQ(fearQ + FEAR_OUTNUMBERED, 0, SCALE.Q);
|
|
74
|
+
}
|
|
75
|
+
// 5. Routing cascade: more than half the team is already routing
|
|
76
|
+
if ((teamRoutingFrac.get(e.teamId) ?? 0) > 0.50) {
|
|
77
|
+
fearQ = clampQ(fearQ + FEAR_ROUTING_CASCADE, 0, SCALE.Q);
|
|
78
|
+
}
|
|
79
|
+
// Fear decay — faster with high tolerance and nearby allies (cohesion)
|
|
80
|
+
fearQ = clampQ(fearQ - fearDecayPerTick(distressTol, nearbyAllyCount), 0, SCALE.Q);
|
|
81
|
+
// Feature 3: leader and standard-bearer aura decay
|
|
82
|
+
// Phase 33: interpersonal intelligence scales effective aura reception radius
|
|
83
|
+
// Formula: base × (0.40 + interpersonal); human (0.60) → ×1.0
|
|
84
|
+
const interpersonal = e.attributes.cognition?.interpersonal ?? 0;
|
|
85
|
+
const effectiveAuraRadius_m = interpersonal
|
|
86
|
+
? Math.trunc(mulDiv(AURA_RADIUS_m, (4000 + interpersonal), SCALE.Q))
|
|
87
|
+
: AURA_RADIUS_m;
|
|
88
|
+
let leaderCount = 0;
|
|
89
|
+
let bannerCount = 0;
|
|
90
|
+
const auraIds = queryNearbyIds(spatial, e.position_m, effectiveAuraRadius_m);
|
|
91
|
+
for (const aId of auraIds) {
|
|
92
|
+
if (aId === e.id)
|
|
93
|
+
continue;
|
|
94
|
+
const ally = index.byId.get(aId);
|
|
95
|
+
if (!ally || ally.injury.dead || ally.teamId !== e.teamId)
|
|
96
|
+
continue;
|
|
97
|
+
const traits = ally.traits ?? [];
|
|
98
|
+
if (traits.includes("leader"))
|
|
99
|
+
leaderCount++;
|
|
100
|
+
if (traits.includes("standardBearer"))
|
|
101
|
+
bannerCount++;
|
|
102
|
+
}
|
|
103
|
+
if (leaderCount > 0) {
|
|
104
|
+
fearQ = clampQ(fearQ - leaderCount * LEADER_AURA_FEAR_REDUCTION, 0, SCALE.Q);
|
|
105
|
+
}
|
|
106
|
+
if (bannerCount > 0) {
|
|
107
|
+
fearQ = clampQ(fearQ - bannerCount * BANNER_AURA_FEAR_REDUCTION, 0, SCALE.Q);
|
|
108
|
+
}
|
|
109
|
+
// Phase 6: cover provides a psychological safety bonus
|
|
110
|
+
const moraleCellSize = ctx.cellSize_m ?? Math.trunc(4 * SCALE.m);
|
|
111
|
+
const coverForMorale = ctx.obstacleGrid
|
|
112
|
+
? coverFractionAtPosition(ctx.obstacleGrid, moraleCellSize, e.position_m.x, e.position_m.y)
|
|
113
|
+
: 0;
|
|
114
|
+
if (coverForMorale > q(0.5)) {
|
|
115
|
+
fearQ = clampQ(fearQ - q(0.01), 0, SCALE.Q);
|
|
116
|
+
}
|
|
117
|
+
// Feature 5: rally — detect routing → normal transition and set cooldown
|
|
118
|
+
const nowRouting = isRouting(fearQ, distressTol);
|
|
119
|
+
if (wasRouting && !nowRouting) {
|
|
120
|
+
e.condition.rallyCooldownTicks = RALLY_COOLDOWN_TICKS;
|
|
121
|
+
}
|
|
122
|
+
e.condition.fearQ = fearQ;
|
|
123
|
+
// Emit trace when routing state crosses threshold
|
|
124
|
+
if (!wasRouting && nowRouting) {
|
|
125
|
+
trace.onEvent({ kind: TraceKinds.MoraleRoute, tick: world.tick, entityId: e.id, fearQ });
|
|
126
|
+
}
|
|
127
|
+
else if (wasRouting && !nowRouting) {
|
|
128
|
+
trace.onEvent({ kind: TraceKinds.MoraleRally, tick: world.tick, entityId: e.id, fearQ }); // Phase 18
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Entity } from "../entity.js";
|
|
2
|
+
import type { WorldState } from "../world.js";
|
|
3
|
+
import type { KernelContext } from "../context.js";
|
|
4
|
+
import type { SimulationTuning } from "../tuning.js";
|
|
5
|
+
export declare function stepMovement(e: Entity, world: WorldState, ctx: KernelContext, tuning: SimulationTuning): void;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { eventSeed } from "../seeds.js";
|
|
2
|
+
import { DT_S } from "../tick.js";
|
|
3
|
+
import { SCALE, clampQ, q, qMul, mulDiv } from "../../units.js";
|
|
4
|
+
import { clampSpeed, scaleDirToSpeed, clampI32 } from "../kernel.js";
|
|
5
|
+
import { v3, normaliseDirCheapQ, integratePos } from "../vec3.js";
|
|
6
|
+
import { coverFractionAtPosition, slopeAtPosition, tractionAtPosition, speedMulAtPosition, } from "../terrain.js";
|
|
7
|
+
import { deriveMovementCaps } from "../../derive.js";
|
|
8
|
+
import { deriveFunctionalState } from "../impairment.js";
|
|
9
|
+
import { findExoskeleton } from "../../equipment.js";
|
|
10
|
+
export function stepMovement(e, world, ctx, tuning) {
|
|
11
|
+
const cellSize = ctx.cellSize_m ?? Math.trunc(4 * SCALE.m);
|
|
12
|
+
const traction = tractionAtPosition(ctx.terrainGrid, cellSize, e.position_m.x, e.position_m.y, ctx.tractionCoeff);
|
|
13
|
+
const caps = deriveMovementCaps(e.attributes, e.loadout, { tractionCoeff: traction });
|
|
14
|
+
const func = deriveFunctionalState(e, tuning);
|
|
15
|
+
// Capability gating
|
|
16
|
+
if (!func.canAct) {
|
|
17
|
+
// unconscious/otherwise incapable: no voluntary movement
|
|
18
|
+
e.intent.move = { dir: { x: 0, y: 0, z: 0 }, intensity: q(0), mode: "walk" };
|
|
19
|
+
}
|
|
20
|
+
if (!func.canStand) {
|
|
21
|
+
// force prone if unable to stand (tactical/sim)
|
|
22
|
+
if (tuning.realism !== "arcade")
|
|
23
|
+
e.condition.prone = true;
|
|
24
|
+
}
|
|
25
|
+
if (e.condition.unconsciousTicks > 0) {
|
|
26
|
+
e.velocity_mps = v3(0, 0, 0);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const vmax_mps = caps.maxSprintSpeed_mps;
|
|
30
|
+
const amax_mps2 = caps.maxAcceleration_mps2;
|
|
31
|
+
const controlMulBase = clampQ(q(1.0) - qMul(q(0.7), e.condition.stunned), q(0.1), q(1.0));
|
|
32
|
+
let mobilityMulBase = e.condition.prone ? q(0.25) : q(1.0);
|
|
33
|
+
// crawl tuning
|
|
34
|
+
if (e.condition.prone && e.condition.unconsciousTicks === 0 && tuning.realism !== "arcade") {
|
|
35
|
+
mobilityMulBase = qMul(mobilityMulBase, q(0.20)); // crawling is slow
|
|
36
|
+
}
|
|
37
|
+
// impairment affects control and mobility
|
|
38
|
+
const controlMul = qMul(controlMulBase, func.coordinationMul);
|
|
39
|
+
const mobilityMul = qMul(mobilityMulBase, func.mobilityMul);
|
|
40
|
+
const crowd = ctx.density?.crowdingQ.get(e.id) ?? 0;
|
|
41
|
+
const crowdMul = clampQ(q(1.0) - qMul(q(0.65), crowd), q(0.25), q(1.0));
|
|
42
|
+
const terrainSpeedMul = speedMulAtPosition(ctx.terrainGrid, cellSize, e.position_m.x, e.position_m.y);
|
|
43
|
+
// Phase 6: slope direction adjusts effective speed.
|
|
44
|
+
// uphill: −25% per grade unit, clamped [50%,95%]; downhill: +10% per grade unit, clamped [100%,120%].
|
|
45
|
+
const slope = slopeAtPosition(ctx.slopeGrid, cellSize, e.position_m.x, e.position_m.y);
|
|
46
|
+
const slopeMul = slope
|
|
47
|
+
? slope.type === "uphill"
|
|
48
|
+
? clampQ((SCALE.Q - qMul(slope.grade, q(0.25))), q(0.50), q(0.95))
|
|
49
|
+
: clampQ((SCALE.Q + qMul(slope.grade, q(0.10))), q(1.0), q(1.20))
|
|
50
|
+
: SCALE.Q;
|
|
51
|
+
// Phase 11: powered exoskeleton speed boost
|
|
52
|
+
const exo = findExoskeleton(e.loadout);
|
|
53
|
+
const exoSpeedMul = exo ? exo.speedMultiplier : SCALE.Q;
|
|
54
|
+
// Phase 8B: flight locomotion — boost sprint speed when entity can achieve flight
|
|
55
|
+
let flightSpeedMul = SCALE.Q;
|
|
56
|
+
const flightSpec = e.bodyPlan?.locomotion.flight;
|
|
57
|
+
if (flightSpec) {
|
|
58
|
+
const mass = e.attributes.morphology.mass_kg;
|
|
59
|
+
if (mass <= flightSpec.liftCapacity_kg) {
|
|
60
|
+
// Compute average wing damage
|
|
61
|
+
let wingDmgSum = 0;
|
|
62
|
+
let wingCount = 0;
|
|
63
|
+
for (const wid of flightSpec.wingSegments) {
|
|
64
|
+
const ws = e.injury.byRegion[wid];
|
|
65
|
+
if (ws) {
|
|
66
|
+
wingDmgSum += ws.structuralDamage;
|
|
67
|
+
wingCount++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const avgWingDmg = wingCount > 0 ? Math.trunc(wingDmgSum / wingCount) : q(0);
|
|
71
|
+
const flightMul = clampQ((SCALE.Q - qMul(avgWingDmg, flightSpec.wingDamagePenalty)), q(0), q(1.0));
|
|
72
|
+
// 1.5× flight speed boost, scaled by wing condition
|
|
73
|
+
flightSpeedMul = qMul(q(1.50), flightMul);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Phase 32A: locomotion mode modifiers
|
|
77
|
+
// Validate requested mode against entity's declared locomotion capacities.
|
|
78
|
+
const requestedMode = e.intent.locomotionMode;
|
|
79
|
+
const locomotionModes = e.attributes.locomotionModes;
|
|
80
|
+
const activeCapacity = requestedMode && locomotionModes
|
|
81
|
+
? locomotionModes.find(c => c.mode === requestedMode)
|
|
82
|
+
: undefined;
|
|
83
|
+
// aquatic depth check: entity without swim capacity and below ground (z < 0) cannot act
|
|
84
|
+
const isSubmerged = e.position_m.z < 0;
|
|
85
|
+
const canSwim = locomotionModes?.some(c => c.mode === "swim") ?? false;
|
|
86
|
+
if (isSubmerged && !canSwim) {
|
|
87
|
+
e.velocity_mps = v3(0, 0, 0);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Locomotion mode speed multipliers
|
|
91
|
+
let locomotionSpeedMul = SCALE.Q;
|
|
92
|
+
let skipTraction = false;
|
|
93
|
+
if (activeCapacity) {
|
|
94
|
+
switch (activeCapacity.mode) {
|
|
95
|
+
case "flight":
|
|
96
|
+
// Flight: bypass ground traction; apply cruiseAlt proportional controller
|
|
97
|
+
skipTraction = true;
|
|
98
|
+
if (activeCapacity.cruiseAlt_m !== undefined) {
|
|
99
|
+
const targetZ = activeCapacity.cruiseAlt_m;
|
|
100
|
+
const dz = targetZ - e.position_m.z;
|
|
101
|
+
const dzStep = clampI32(Math.trunc(dz), -Math.trunc(2 * SCALE.m), Math.trunc(2 * SCALE.m));
|
|
102
|
+
e.position_m = { ...e.position_m, z: e.position_m.z + dzStep };
|
|
103
|
+
}
|
|
104
|
+
// Cap at declared maxSpeed
|
|
105
|
+
if (activeCapacity.maxSpeed_mps < vmax_mps) {
|
|
106
|
+
locomotionSpeedMul = mulDiv(activeCapacity.maxSpeed_mps, SCALE.Q, vmax_mps);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case "swim":
|
|
110
|
+
// Hydrodynamic drag: ~40% of surface sprint speed
|
|
111
|
+
locomotionSpeedMul = q(0.40);
|
|
112
|
+
skipTraction = true;
|
|
113
|
+
break;
|
|
114
|
+
case "climb":
|
|
115
|
+
locomotionSpeedMul = q(0.30);
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// If skipping traction, override the traction-derived speed caps
|
|
122
|
+
const effTrackMul = skipTraction ? SCALE.Q : SCALE.Q; // traction already applied above via caps
|
|
123
|
+
const baseMul = qMul(qMul(qMul(qMul(qMul(qMul(qMul(controlMul, mobilityMul), crowdMul), skipTraction ? SCALE.Q : terrainSpeedMul), slopeMul), exoSpeedMul), flightSpeedMul), locomotionSpeedMul);
|
|
124
|
+
void effTrackMul;
|
|
125
|
+
const effVmax = mulDiv(vmax_mps, baseMul, SCALE.Q);
|
|
126
|
+
const effAmax = mulDiv(amax_mps2, baseMul, SCALE.Q);
|
|
127
|
+
let modeMul = e.intent.move.mode === "walk" ? q(0.40) :
|
|
128
|
+
e.intent.move.mode === "run" ? q(0.70) : q(1.0);
|
|
129
|
+
if (e.condition.prone && tuning.realism !== "arcade") {
|
|
130
|
+
// cannot sprint while prone
|
|
131
|
+
if (e.intent.move.mode === "sprint")
|
|
132
|
+
modeMul = q(0.40);
|
|
133
|
+
}
|
|
134
|
+
const dir = normaliseDirCheapQ(e.intent.move.dir);
|
|
135
|
+
const intensity = clampQ(e.intent.move.intensity, 0, SCALE.Q);
|
|
136
|
+
// Sim-only: stumble/fall risk when sprinting with impaired mobility/coordination
|
|
137
|
+
if (tuning.realism === "sim" && intensity > 0 && e.intent.move.mode === "sprint" && !e.condition.prone) {
|
|
138
|
+
const instability = (SCALE.Q - qMul(func.mobilityMul, func.coordinationMul));
|
|
139
|
+
const chance = clampQ(tuning.stumbleBaseChance + qMul(instability, q(0.05)), q(0), q(0.25));
|
|
140
|
+
if (chance > 0) {
|
|
141
|
+
const seed = eventSeed(world.seed, world.tick, e.id, 0, 0xF411);
|
|
142
|
+
const roll = (seed % SCALE.Q);
|
|
143
|
+
if (roll < chance) {
|
|
144
|
+
e.condition.prone = true;
|
|
145
|
+
// a small deterministic shock spike
|
|
146
|
+
e.injury.shock = clampQ(e.injury.shock + q(0.02), 0, SCALE.Q);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const vTargetMag = mulDiv(mulDiv(effVmax, intensity, SCALE.Q), modeMul, SCALE.Q);
|
|
151
|
+
const targetVel = scaleDirToSpeed(dir, vTargetMag);
|
|
152
|
+
e.velocity_mps = accelToward(e.velocity_mps, targetVel, effAmax);
|
|
153
|
+
e.velocity_mps = clampSpeed(e.velocity_mps, effVmax);
|
|
154
|
+
// Phase 6: obstacle blocking — impassable cells (coverFraction = q(1.0)) prevent entry.
|
|
155
|
+
const nextPos = integratePos(e.position_m, e.velocity_mps, DT_S);
|
|
156
|
+
if (ctx.obstacleGrid) {
|
|
157
|
+
const cov = coverFractionAtPosition(ctx.obstacleGrid, cellSize, nextPos.x, nextPos.y);
|
|
158
|
+
if (cov >= SCALE.Q) {
|
|
159
|
+
e.velocity_mps = v3(0, 0, 0);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
e.position_m = nextPos;
|
|
164
|
+
}
|
|
165
|
+
function accelToward(v, target, amax_mps2) {
|
|
166
|
+
const maxDv = Math.trunc((amax_mps2 * DT_S) / SCALE.s);
|
|
167
|
+
return {
|
|
168
|
+
x: v.x + clampI32(target.x - v.x, -maxDv, maxDv),
|
|
169
|
+
y: v.y + clampI32(target.y - v.y, -maxDv, maxDv),
|
|
170
|
+
z: v.z + clampI32(target.z - v.z, -maxDv, maxDv),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WorldState } from "../world.js";
|
|
2
|
+
import type { SpatialIndex } from "../spatial.js";
|
|
3
|
+
import type { WorldIndex } from "../indexing.js";
|
|
4
|
+
import type { Q } from "../../units.js";
|
|
5
|
+
export interface PushTuning {
|
|
6
|
+
personalRadius_m: number;
|
|
7
|
+
repelAccel_mps2: number;
|
|
8
|
+
pushTransfer: Q;
|
|
9
|
+
maxNeighbours: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function stepPushAndRepulsion(world: WorldState, index: WorldIndex, spatial: SpatialIndex, tuning: PushTuning): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { queryNearbyIds } from "../spatial.js";
|
|
2
|
+
import { SCALE, clampQ } from "../../units.js";
|
|
3
|
+
export function stepPushAndRepulsion(world, index, spatial, tuning) {
|
|
4
|
+
const R = tuning.personalRadius_m;
|
|
5
|
+
const R2 = BigInt(R) * BigInt(R);
|
|
6
|
+
// 1) collect all candidate pairs (order-independent)
|
|
7
|
+
const pairs = [];
|
|
8
|
+
// Deterministic: world.entities already sorted by id in stepWorld
|
|
9
|
+
for (const e of world.entities) {
|
|
10
|
+
if (e.injury.dead)
|
|
11
|
+
continue;
|
|
12
|
+
const ids = queryNearbyIds(spatial, e.position_m, R, tuning.maxNeighbours);
|
|
13
|
+
ids.sort((x, y) => x - y);
|
|
14
|
+
for (const id of ids) {
|
|
15
|
+
if (id === e.id)
|
|
16
|
+
continue;
|
|
17
|
+
const a = Math.min(e.id, id);
|
|
18
|
+
const b = Math.max(e.id, id);
|
|
19
|
+
pairs.push({ a, b });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// 2) de-dupe pairs deterministically
|
|
23
|
+
pairs.sort((p, q) => (p.a - q.a) || (p.b - q.b));
|
|
24
|
+
const uniq = [];
|
|
25
|
+
for (const p of pairs) {
|
|
26
|
+
const last = uniq[uniq.length - 1];
|
|
27
|
+
if (!last || last.a !== p.a || last.b !== p.b)
|
|
28
|
+
uniq.push(p);
|
|
29
|
+
}
|
|
30
|
+
// 3) compute dv per pair, accumulate into dv map (NO entity mutation here)
|
|
31
|
+
const dv = new Map();
|
|
32
|
+
for (const { a, b } of uniq) {
|
|
33
|
+
const A = index.byId.get(a);
|
|
34
|
+
const B = index.byId.get(b);
|
|
35
|
+
if (!A || !B)
|
|
36
|
+
continue;
|
|
37
|
+
if (A.injury.dead || B.injury.dead)
|
|
38
|
+
continue;
|
|
39
|
+
const dx = B.position_m.x - A.position_m.x;
|
|
40
|
+
const dy = B.position_m.y - A.position_m.y;
|
|
41
|
+
const dz = B.position_m.z - A.position_m.z;
|
|
42
|
+
const d2 = BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy) + BigInt(dz) * BigInt(dz);
|
|
43
|
+
if (d2 >= R2 || d2 === 0n)
|
|
44
|
+
continue;
|
|
45
|
+
// repel along x/y only
|
|
46
|
+
const d = approxDist(dx, dy);
|
|
47
|
+
const overlap = Math.max(0, R - d);
|
|
48
|
+
if (overlap <= 0)
|
|
49
|
+
continue;
|
|
50
|
+
const strengthQ = clampQ(Math.trunc((overlap * SCALE.Q) / R), 0, SCALE.Q);
|
|
51
|
+
const ax = Math.trunc((dx * tuning.repelAccel_mps2 * strengthQ) / (Math.max(1, d) * SCALE.Q));
|
|
52
|
+
const ay = Math.trunc((dy * tuning.repelAccel_mps2 * strengthQ) / (Math.max(1, d) * SCALE.Q));
|
|
53
|
+
// equal + opposite dv
|
|
54
|
+
addDv(dv, A.id, -ax, -ay, 0);
|
|
55
|
+
addDv(dv, B.id, ax, ay, 0);
|
|
56
|
+
}
|
|
57
|
+
// 4) apply dv in stable order (world.entities is stable-sorted)
|
|
58
|
+
for (const e of world.entities) {
|
|
59
|
+
const d = dv.get(e.id);
|
|
60
|
+
if (!d)
|
|
61
|
+
continue;
|
|
62
|
+
e.velocity_mps.x += d.x;
|
|
63
|
+
e.velocity_mps.y += d.y;
|
|
64
|
+
e.velocity_mps.z += d.z;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function addDv(dv, id, dx, dy, dz) {
|
|
68
|
+
const cur = dv.get(id) ?? { x: 0, y: 0, z: 0 };
|
|
69
|
+
cur.x += dx;
|
|
70
|
+
cur.y += dy;
|
|
71
|
+
cur.z += dz;
|
|
72
|
+
dv.set(id, cur);
|
|
73
|
+
}
|
|
74
|
+
// cheap approx: max + 0.5*min
|
|
75
|
+
function approxDist(dx, dy) {
|
|
76
|
+
const adx = dx < 0 ? -dx : dx;
|
|
77
|
+
const ady = dy < 0 ? -dy : dy;
|
|
78
|
+
return adx > ady ? adx + (ady >> 1) : ady + (adx >> 1);
|
|
79
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { q, clampQ, qMul, mulDiv, SCALE } from "../../units.js";
|
|
2
|
+
import { hasSubstanceType } from "../substance.js";
|
|
3
|
+
export function stepSubstances(e, ambientTemperature_Q) {
|
|
4
|
+
if (!e.substances || e.substances.length === 0)
|
|
5
|
+
return;
|
|
6
|
+
for (const active of e.substances) {
|
|
7
|
+
const sub = active.substance;
|
|
8
|
+
// Absorption: pendingDose → concentration
|
|
9
|
+
const absorbed = qMul(active.pendingDose, sub.absorptionRate);
|
|
10
|
+
active.pendingDose = clampQ(active.pendingDose - absorbed, q(0), q(1.0));
|
|
11
|
+
active.concentration = clampQ(active.concentration + absorbed, q(0), q(1.0));
|
|
12
|
+
// Elimination — base rate, then modifiers
|
|
13
|
+
let effectiveElimRate = sub.eliminationRate;
|
|
14
|
+
// Phase 10C: substance interactions — modify elimination rate
|
|
15
|
+
if (sub.effectType === "haemostatic" && hasSubstanceType(e, "stimulant")) {
|
|
16
|
+
// Stimulant-induced vasoconstriction antagonises haemostatic absorption: clears 30% faster
|
|
17
|
+
effectiveElimRate = qMul(effectiveElimRate, q(1.30));
|
|
18
|
+
}
|
|
19
|
+
if (sub.effectType === "anaesthetic" && hasSubstanceType(e, "stimulant")) {
|
|
20
|
+
// Stimulant partially counteracts anaesthetic: clears 25% faster
|
|
21
|
+
effectiveElimRate = qMul(effectiveElimRate, q(1.25));
|
|
22
|
+
}
|
|
23
|
+
if (sub.effectType === "haemostatic" && hasSubstanceType(e, "poison")) {
|
|
24
|
+
// Haemostatic partially counteracts poison-induced bleeding: clears 20% slower
|
|
25
|
+
effectiveElimRate = qMul(effectiveElimRate, q(0.80));
|
|
26
|
+
}
|
|
27
|
+
// Phase 10C: temperature-dependent metabolism — cold slows hepatic clearance
|
|
28
|
+
if (ambientTemperature_Q !== undefined && ambientTemperature_Q < q(0.35)) {
|
|
29
|
+
const coldFrac = Math.max(q(0.50), mulDiv(ambientTemperature_Q, SCALE.Q, q(0.35)));
|
|
30
|
+
effectiveElimRate = qMul(effectiveElimRate, coldFrac);
|
|
31
|
+
}
|
|
32
|
+
const eliminated = qMul(active.concentration, effectiveElimRate);
|
|
33
|
+
active.concentration = clampQ(active.concentration - eliminated, q(0), q(1.0));
|
|
34
|
+
// Effects — only when above threshold
|
|
35
|
+
if (active.concentration <= sub.effectThreshold)
|
|
36
|
+
continue;
|
|
37
|
+
const delta = clampQ(active.concentration - sub.effectThreshold, q(0), q(1.0));
|
|
38
|
+
// Phase 10C: anaesthetic onset/strength is reduced when a stimulant is active
|
|
39
|
+
let effectStrengthMod = sub.effectStrength;
|
|
40
|
+
if (sub.effectType === "anaesthetic" && hasSubstanceType(e, "stimulant")) {
|
|
41
|
+
effectStrengthMod = qMul(effectStrengthMod, q(0.75));
|
|
42
|
+
}
|
|
43
|
+
const effectDose = qMul(delta, effectStrengthMod);
|
|
44
|
+
switch (sub.effectType) {
|
|
45
|
+
case "stimulant":
|
|
46
|
+
// Reduces fear and slows fatigue accumulation
|
|
47
|
+
e.condition.fearQ = clampQ((e.condition.fearQ ?? 0) - qMul(effectDose, q(0.005)), q(0), q(1.0));
|
|
48
|
+
e.energy.fatigue = clampQ(e.energy.fatigue - qMul(effectDose, q(0.003)), q(0), q(1.0));
|
|
49
|
+
break;
|
|
50
|
+
case "anaesthetic":
|
|
51
|
+
// Erodes consciousness
|
|
52
|
+
e.injury.consciousness = clampQ(e.injury.consciousness - qMul(effectDose, q(0.008)), q(0), q(1.0));
|
|
53
|
+
break;
|
|
54
|
+
case "poison": {
|
|
55
|
+
// Internal damage to torso (or first region)
|
|
56
|
+
const torsoReg = e.injury.byRegion["torso"] ?? Object.values(e.injury.byRegion)[0];
|
|
57
|
+
if (torsoReg) {
|
|
58
|
+
torsoReg.internalDamage = clampQ(torsoReg.internalDamage + qMul(effectDose, q(0.002)), q(0), q(1.0));
|
|
59
|
+
}
|
|
60
|
+
e.injury.shock = clampQ(e.injury.shock + qMul(effectDose, q(0.001)), 0, SCALE.Q);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "haemostatic":
|
|
64
|
+
// Reduces bleeding rate across all regions
|
|
65
|
+
for (const reg of Object.values(e.injury.byRegion)) {
|
|
66
|
+
if (reg.bleedingRate > 0) {
|
|
67
|
+
reg.bleedingRate = clampQ(reg.bleedingRate - qMul(effectDose, q(0.003)), q(0), q(1.0));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Remove exhausted substances (keep only those with meaningful dose or concentration)
|
|
74
|
+
e.substances = e.substances.filter(a => a.pendingDose > 1 || a.concentration > 1);
|
|
75
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Q } from "../units.js";
|
|
2
|
+
import type { Entity } from "./entity.js";
|
|
3
|
+
export type SubstanceEffectType = "stimulant" | "anaesthetic" | "poison" | "haemostatic";
|
|
4
|
+
export interface Substance {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
/** Fraction of pendingDose absorbed into concentration per tick (0..1 Q). */
|
|
8
|
+
absorptionRate: Q;
|
|
9
|
+
/** Fraction of current concentration cleared per tick (0..1 Q). */
|
|
10
|
+
eliminationRate: Q;
|
|
11
|
+
/** Minimum concentration for effects to activate (0..1 Q). */
|
|
12
|
+
effectThreshold: Q;
|
|
13
|
+
/** Nature of biological effect. */
|
|
14
|
+
effectType: SubstanceEffectType;
|
|
15
|
+
/**
|
|
16
|
+
* Strength multiplier applied to the above-threshold concentration delta.
|
|
17
|
+
* A value of q(1.0) produces the standard effect magnitude.
|
|
18
|
+
*/
|
|
19
|
+
effectStrength: Q;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* An active dose of a substance present in an entity's system.
|
|
23
|
+
* Add to `entity.substances` when a substance is ingested or injected.
|
|
24
|
+
*/
|
|
25
|
+
export interface ActiveSubstance {
|
|
26
|
+
substance: Substance;
|
|
27
|
+
/** Remaining unabsorbed dose (Q 0..1); decreases each tick as absorption occurs. */
|
|
28
|
+
pendingDose: Q;
|
|
29
|
+
/** Current systemic concentration (Q 0..1); rises with absorption, falls with elimination. */
|
|
30
|
+
concentration: Q;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Phase 10C: returns true if the entity has an active substance of the given type
|
|
34
|
+
* with concentration above its effectThreshold.
|
|
35
|
+
*/
|
|
36
|
+
export declare function hasSubstanceType(e: Entity, type: SubstanceEffectType): boolean;
|
|
37
|
+
/** Ready-made substance catalogue for common game scenarios. */
|
|
38
|
+
export declare const STARTER_SUBSTANCES: Record<string, Substance>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/sim/substance.ts — Phase 10: pharmacokinetics model
|
|
2
|
+
//
|
|
3
|
+
// One-compartment model:
|
|
4
|
+
// d[concentration]/dt = absorptionRate × pendingDose − eliminationRate × concentration
|
|
5
|
+
//
|
|
6
|
+
// Effects activate when concentration exceeds effectThreshold.
|
|
7
|
+
// The engine consumes substance definitions; the host application manages which
|
|
8
|
+
// substances an entity has ingested/injected (by populating entity.substances).
|
|
9
|
+
import { q } from "../units.js";
|
|
10
|
+
/**
|
|
11
|
+
* Phase 10C: returns true if the entity has an active substance of the given type
|
|
12
|
+
* with concentration above its effectThreshold.
|
|
13
|
+
*/
|
|
14
|
+
export function hasSubstanceType(e, type) {
|
|
15
|
+
if (!e.substances)
|
|
16
|
+
return false;
|
|
17
|
+
return e.substances.some(a => a.substance.effectType === type && a.concentration > a.substance.effectThreshold);
|
|
18
|
+
}
|
|
19
|
+
/** Ready-made substance catalogue for common game scenarios. */
|
|
20
|
+
export const STARTER_SUBSTANCES = {
|
|
21
|
+
stimulant: {
|
|
22
|
+
id: "stimulant",
|
|
23
|
+
name: "Combat Stimulant",
|
|
24
|
+
absorptionRate: q(0.15), // 15%/tick — fast-acting injection
|
|
25
|
+
eliminationRate: q(0.02), // 2%/tick — clears in ~50 ticks (2.5 s)
|
|
26
|
+
effectThreshold: q(0.10),
|
|
27
|
+
effectType: "stimulant",
|
|
28
|
+
effectStrength: q(0.80),
|
|
29
|
+
},
|
|
30
|
+
anaesthetic: {
|
|
31
|
+
id: "anaesthetic",
|
|
32
|
+
name: "Anaesthetic",
|
|
33
|
+
absorptionRate: q(0.08), // slower onset
|
|
34
|
+
eliminationRate: q(0.008), // slow clearance — lasts many ticks
|
|
35
|
+
effectThreshold: q(0.05),
|
|
36
|
+
effectType: "anaesthetic",
|
|
37
|
+
effectStrength: q(1.00),
|
|
38
|
+
},
|
|
39
|
+
poison: {
|
|
40
|
+
id: "poison",
|
|
41
|
+
name: "Contact Poison",
|
|
42
|
+
absorptionRate: q(0.06), // slow skin absorption
|
|
43
|
+
eliminationRate: q(0.004), // very slow clearance
|
|
44
|
+
effectThreshold: q(0.05),
|
|
45
|
+
effectType: "poison",
|
|
46
|
+
effectStrength: q(0.80),
|
|
47
|
+
},
|
|
48
|
+
haemostatic: {
|
|
49
|
+
id: "haemostatic",
|
|
50
|
+
name: "Haemostatic Agent",
|
|
51
|
+
absorptionRate: q(0.20), // rapid absorption (injected/applied)
|
|
52
|
+
eliminationRate: q(0.03), // clears in ~33 ticks (1.6 s)
|
|
53
|
+
effectThreshold: q(0.10),
|
|
54
|
+
effectType: "haemostatic",
|
|
55
|
+
effectStrength: q(0.60),
|
|
56
|
+
},
|
|
57
|
+
};
|