@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,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2A: Grapple resolution — deterministic close-combat control.
|
|
3
|
+
*
|
|
4
|
+
* Design rules:
|
|
5
|
+
* - All randomness via eventSeed() + salt, never Math.random()
|
|
6
|
+
* - Pair-based ordering: idLo = Math.min(a, b), idHi = Math.max(a, b)
|
|
7
|
+
* - No mutation of entities mid-resolution across independent pairs
|
|
8
|
+
* - All physical quantities in SI fixed-point (SCALE.*)
|
|
9
|
+
*/
|
|
10
|
+
import { SCALE, q, qMul, clampQ, mulDiv, to } from "../units.js";
|
|
11
|
+
import { getSkill } from "./skills.js";
|
|
12
|
+
import { eventSeed } from "./seeds.js";
|
|
13
|
+
import { deriveFunctionalState } from "./impairment.js";
|
|
14
|
+
import { TraceKinds } from "./kinds.js";
|
|
15
|
+
import { effectiveLimbForceMul } from "./limb.js";
|
|
16
|
+
import { TICK_HZ } from "./tick.js";
|
|
17
|
+
// ---------- Deterministic salts ----------
|
|
18
|
+
const SALT_ATTEMPT = 0x4A41B1;
|
|
19
|
+
const SALT_BREAK = 0xB4EA44;
|
|
20
|
+
const SALT_THROW = 0xF177C0;
|
|
21
|
+
const SALT_JOINTLOCK = 0x6B1D44;
|
|
22
|
+
// ---------- Reference values (human baseline for normalisation) ----------
|
|
23
|
+
const REF_FORCE = to.N(1840); // 184_000 fixed-point units
|
|
24
|
+
const REF_MASS = to.kg(75); // 75_000 fixed-point units
|
|
25
|
+
const REF_H = to.m(1.75); // 17_500 fixed-point units
|
|
26
|
+
// ---------- Grip decay per tick ----------
|
|
27
|
+
// 0.5% per tick → released after ~200 ticks (10 s) without maintenance
|
|
28
|
+
export const GRIP_DECAY_PER_TICK = 50; // = q(0.005)
|
|
29
|
+
// ============================================================
|
|
30
|
+
// Physics-derived cost helpers
|
|
31
|
+
// ============================================================
|
|
32
|
+
/**
|
|
33
|
+
* Effective grapple reach for an entity (m, fixed-point).
|
|
34
|
+
* Derived from stature × reachScale — arm-span is approximately equal
|
|
35
|
+
* to body height, scaled by individual proportions.
|
|
36
|
+
* Clamped to [1.0 m, 3.0 m].
|
|
37
|
+
*/
|
|
38
|
+
function grappleReach_m(e) {
|
|
39
|
+
const reach = mulDiv(e.attributes.morphology.stature_m, e.attributes.morphology.reachScale, SCALE.Q);
|
|
40
|
+
return Math.max(to.m(1.0), Math.min(to.m(3.0), reach));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Energy cost (J) of a single grapple/break attempt.
|
|
44
|
+
* Modelled as ~70 ms burst at peak power:
|
|
45
|
+
* cost ≈ peakPower_W × 0.07 s
|
|
46
|
+
* Calibration: 1200 W peak → 84 J ≈ 80 J reference.
|
|
47
|
+
* Minimum 20 J to avoid zero cost on very weak entities.
|
|
48
|
+
*/
|
|
49
|
+
function attemptCost_J(e) {
|
|
50
|
+
return Math.max(20, mulDiv(e.attributes.performance.peakPower_W, 7, 100));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Energy cost (J) of a throw attempt.
|
|
54
|
+
* ~100 ms burst at peak power: cost ≈ peakPower_W × 0.10 s
|
|
55
|
+
* Calibration: 1200 W → 120 J reference. Minimum 40 J.
|
|
56
|
+
*/
|
|
57
|
+
function throwCost_J(e) {
|
|
58
|
+
return Math.max(40, mulDiv(e.attributes.performance.peakPower_W, 10, 100));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Energy cost (J) of a joint-lock application.
|
|
62
|
+
* ~50 ms burst at peak power: cost ≈ peakPower_W × 0.05 s
|
|
63
|
+
* Calibration: 1200 W → 60 J reference. Minimum 20 J.
|
|
64
|
+
*/
|
|
65
|
+
function lockCost_J(e) {
|
|
66
|
+
return Math.max(20, mulDiv(e.attributes.performance.peakPower_W, 5, 100));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Energy cost (J) per tick of maintaining a grapple hold.
|
|
70
|
+
* Modelled as isometric hold work at continuous aerobic power:
|
|
71
|
+
* cost = continuousPower_W / TICK_HZ
|
|
72
|
+
* Calibration: 200 W / 20 Hz = 10 J/tick reference. Minimum 5 J.
|
|
73
|
+
*/
|
|
74
|
+
function tickCost_J(e) {
|
|
75
|
+
return Math.max(5, Math.trunc(e.attributes.performance.continuousPower_W / TICK_HZ));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Leverage differential threshold above which an immediate trip occurs on a
|
|
79
|
+
* successful grapple attempt.
|
|
80
|
+
*
|
|
81
|
+
* Higher (harder to trip) when the target is:
|
|
82
|
+
* - heavier relative to the attacker
|
|
83
|
+
* - more stable (higher stability coefficient)
|
|
84
|
+
*
|
|
85
|
+
* Formula: base 0.20 + 0.30 × (massRatio × targetStability)
|
|
86
|
+
* Clamped to [0.10, 0.70].
|
|
87
|
+
*
|
|
88
|
+
* Average vs average: ≈ 0.20 + 0.30 × (1.0 × 0.70) = 0.41
|
|
89
|
+
* Light/unstable target: ≈ 0.10 (easier to trip)
|
|
90
|
+
* Heavy/stable target: ≈ 0.60–0.70 (hard to trip immediately)
|
|
91
|
+
*/
|
|
92
|
+
function tripThreshold(attacker, target) {
|
|
93
|
+
const massRatioQ = clampQ(mulDiv(target.attributes.morphology.mass_kg, SCALE.Q, Math.max(1, attacker.attributes.morphology.mass_kg)), q(0.20), q(2.0));
|
|
94
|
+
const stability = target.attributes.control.stability;
|
|
95
|
+
return clampQ(q(0.20) + qMul(q(0.30), qMul(massRatioQ, stability)), q(0.10), q(0.70));
|
|
96
|
+
}
|
|
97
|
+
// ---------- Synthetic Weapon objects for grapple impacts ----------
|
|
98
|
+
// These are used only as damage-profile carriers; they never appear in a Loadout.
|
|
99
|
+
export const GRAPPLE_THROW_WPN = {
|
|
100
|
+
id: "__grapple_throw__",
|
|
101
|
+
name: "Grapple Throw",
|
|
102
|
+
mass_kg: 0,
|
|
103
|
+
bulk: q(0),
|
|
104
|
+
kind: "weapon",
|
|
105
|
+
damage: {
|
|
106
|
+
surfaceFrac: q(0.05),
|
|
107
|
+
internalFrac: q(0.35),
|
|
108
|
+
structuralFrac: q(0.60),
|
|
109
|
+
bleedFactor: q(0.10),
|
|
110
|
+
penetrationBias: q(0.0),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
export const GRAPPLE_JOINTLOCK_WPN = {
|
|
114
|
+
id: "__grapple_jointlock__",
|
|
115
|
+
name: "Joint Lock",
|
|
116
|
+
mass_kg: 0,
|
|
117
|
+
bulk: q(0),
|
|
118
|
+
kind: "weapon",
|
|
119
|
+
damage: {
|
|
120
|
+
surfaceFrac: q(0.02),
|
|
121
|
+
internalFrac: q(0.20),
|
|
122
|
+
structuralFrac: q(0.78),
|
|
123
|
+
bleedFactor: q(0.05),
|
|
124
|
+
penetrationBias: q(0.0),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
// ============================================================
|
|
128
|
+
// Score computation
|
|
129
|
+
// ============================================================
|
|
130
|
+
/**
|
|
131
|
+
* Compute an entity's grapple contest score in Q [0.05, 0.95].
|
|
132
|
+
*
|
|
133
|
+
* Combines:
|
|
134
|
+
* 50% peak force (normalised to human baseline)
|
|
135
|
+
* 30% technique (controlQuality × stability)
|
|
136
|
+
* 20% body mass (normalised to human baseline)
|
|
137
|
+
*
|
|
138
|
+
* The result is modulated by the entity's current functional state
|
|
139
|
+
* (injury, fatigue) via manipulationMul.
|
|
140
|
+
*
|
|
141
|
+
* A healthy average human scores ≈ q(0.47).
|
|
142
|
+
*/
|
|
143
|
+
export function grappleContestScore(e, func) {
|
|
144
|
+
const f = e.attributes.performance.peakForce_N;
|
|
145
|
+
const m = e.attributes.morphology.mass_kg;
|
|
146
|
+
const ctrl = e.attributes.control.controlQuality;
|
|
147
|
+
const stab = e.attributes.control.stability;
|
|
148
|
+
// Normalise to Q (q(1.0) = human baseline)
|
|
149
|
+
const forceQ = clampQ(mulDiv(f, SCALE.Q, REF_FORCE), q(0.10), q(2.50));
|
|
150
|
+
const massQ = clampQ(mulDiv(m, SCALE.Q, REF_MASS), q(0.20), q(2.50));
|
|
151
|
+
const tech = qMul(ctrl, stab);
|
|
152
|
+
// Weighted sum — result is Q-scaled
|
|
153
|
+
const raw = mulDiv(forceQ, 5, 10) +
|
|
154
|
+
mulDiv(tech, 3, 10) +
|
|
155
|
+
mulDiv(massQ, 2, 10);
|
|
156
|
+
// Apply functional impairment; clamp to [q(0.02), q(1.80)]
|
|
157
|
+
const impaired = clampQ(qMul(raw, func.manipulationMul), q(0.02), q(1.80));
|
|
158
|
+
// Phase 7: grappling.energyTransferMul applies a leverage bonus to the contest score
|
|
159
|
+
const grapSkill = getSkill(e.skills, "grappling");
|
|
160
|
+
const adjusted = clampQ(qMul(impaired, grapSkill.energyTransferMul), q(0.02), q(1.80));
|
|
161
|
+
// Linear map [q(0.02), q(1.80)] → [q(0.05), q(0.95)]
|
|
162
|
+
const range = q(1.80) - q(0.02); // 17 800
|
|
163
|
+
return clampQ(q(0.05) + mulDiv(Math.max(0, adjusted - q(0.02)), q(0.90), range), q(0.05), q(0.95));
|
|
164
|
+
}
|
|
165
|
+
// ============================================================
|
|
166
|
+
// Internal helpers
|
|
167
|
+
// ============================================================
|
|
168
|
+
/**
|
|
169
|
+
* Signed leverage differential (Q) for throw/trip outcome.
|
|
170
|
+
* Positive = attacker has the advantage.
|
|
171
|
+
* Based on F × stature × 0.35 (approximate effective arm in standing grapple).
|
|
172
|
+
*/
|
|
173
|
+
function leverageDiff(attacker, target, funcA, funcB) {
|
|
174
|
+
const fA = attacker.attributes.performance.peakForce_N;
|
|
175
|
+
const fB = target.attributes.performance.peakForce_N;
|
|
176
|
+
const hA = attacker.attributes.morphology.stature_m;
|
|
177
|
+
const hB = target.attributes.morphology.stature_m;
|
|
178
|
+
// Raw leverage ∝ F × h × 0.35 (keep BigInt-safe via mulDiv)
|
|
179
|
+
const levA = mulDiv(mulDiv(fA, hA, SCALE.m), 35, 100);
|
|
180
|
+
const levB = mulDiv(mulDiv(fB, hB, SCALE.m), 35, 100);
|
|
181
|
+
const refLev = mulDiv(mulDiv(REF_FORCE, REF_H, SCALE.m), 35, 100);
|
|
182
|
+
// Normalise and apply functional multipliers
|
|
183
|
+
const sA = clampQ(qMul(clampQ(mulDiv(levA, SCALE.Q, refLev), q(0.10), q(2.0)), funcA.manipulationMul), q(0.05), q(2.0));
|
|
184
|
+
const sB = clampQ(qMul(clampQ(mulDiv(levB, SCALE.Q, refLev), q(0.10), q(2.0)), funcB.manipulationMul), q(0.05), q(2.0));
|
|
185
|
+
return clampQ((sA - sB), q(-0.90), q(0.90));
|
|
186
|
+
}
|
|
187
|
+
function pushImpact(impacts, attackerId, targetId, region, energy_J, wpn, hitQuality) {
|
|
188
|
+
if (energy_J <= 0)
|
|
189
|
+
return;
|
|
190
|
+
impacts.push({
|
|
191
|
+
kind: "impact",
|
|
192
|
+
attackerId,
|
|
193
|
+
targetId,
|
|
194
|
+
region,
|
|
195
|
+
energy_J,
|
|
196
|
+
protectedByArmour: false,
|
|
197
|
+
blocked: false,
|
|
198
|
+
parried: false,
|
|
199
|
+
shieldBlocked: false,
|
|
200
|
+
hitQuality,
|
|
201
|
+
weaponId: wpn.id,
|
|
202
|
+
wpn,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// ============================================================
|
|
206
|
+
// Public API
|
|
207
|
+
// ============================================================
|
|
208
|
+
/**
|
|
209
|
+
* Attempt to initiate a grapple on the target.
|
|
210
|
+
*
|
|
211
|
+
* Contest: scoreA × intensity vs scoreB. Success probability centred at 0.50
|
|
212
|
+
* with ±40% swing per unit score difference (mirrors melee hit formula).
|
|
213
|
+
*
|
|
214
|
+
* On success:
|
|
215
|
+
* - Attacker's grapple.holdingTargetId and gripQ are set
|
|
216
|
+
* - Target's grapple.heldByIds is updated (sorted, deduplicated)
|
|
217
|
+
* - Overwhelming leverage differential causes immediate trip (prone + small impact)
|
|
218
|
+
*
|
|
219
|
+
* On failure: grappleCooldownTicks set, energy still drained.
|
|
220
|
+
*/
|
|
221
|
+
export function resolveGrappleAttempt(world, attacker, target, intensity, tuning, impacts, trace) {
|
|
222
|
+
if (attacker.action.grappleCooldownTicks > 0)
|
|
223
|
+
return;
|
|
224
|
+
// Reach check
|
|
225
|
+
const dx = target.position_m.x - attacker.position_m.x;
|
|
226
|
+
const dy = target.position_m.y - attacker.position_m.y;
|
|
227
|
+
const dz = target.position_m.z - attacker.position_m.z;
|
|
228
|
+
const dist2 = BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy) + BigInt(dz) * BigInt(dz);
|
|
229
|
+
const reach = grappleReach_m(attacker);
|
|
230
|
+
if (dist2 > BigInt(reach) * BigInt(reach))
|
|
231
|
+
return;
|
|
232
|
+
const funcA = deriveFunctionalState(attacker, tuning);
|
|
233
|
+
const funcB = deriveFunctionalState(target, tuning);
|
|
234
|
+
if (!funcA.canAct)
|
|
235
|
+
return;
|
|
236
|
+
const clampedIntensity = clampQ(intensity, q(0.1), q(1.0));
|
|
237
|
+
// Phase 32B: reduce contest score by active limb fraction (severed limbs excluded)
|
|
238
|
+
const limbMul = attacker.limbStates
|
|
239
|
+
? effectiveLimbForceMul(attacker.limbStates, attacker.injury)
|
|
240
|
+
: q(1.0);
|
|
241
|
+
const scoreA = clampQ(qMul(qMul(grappleContestScore(attacker, funcA), clampedIntensity), limbMul), q(0.05), q(0.95));
|
|
242
|
+
const scoreB = grappleContestScore(target, funcB);
|
|
243
|
+
const diff = (scoreA - scoreB);
|
|
244
|
+
const p = clampQ(q(0.50) + mulDiv(diff, q(0.40), SCALE.Q), q(0.05), q(0.95));
|
|
245
|
+
const seed = eventSeed(world.seed, world.tick, attacker.id, target.id, SALT_ATTEMPT);
|
|
246
|
+
const success = (seed % SCALE.Q) < p;
|
|
247
|
+
// Energy drain regardless of outcome
|
|
248
|
+
attacker.energy.reserveEnergy_J = Math.max(0, attacker.energy.reserveEnergy_J - attemptCost_J(attacker));
|
|
249
|
+
if (!success) {
|
|
250
|
+
attacker.action.grappleCooldownTicks = tuning.realism === "arcade" ? 2 : 4;
|
|
251
|
+
trace.onEvent({
|
|
252
|
+
kind: TraceKinds.Grapple,
|
|
253
|
+
tick: world.tick, attackerId: attacker.id, targetId: target.id,
|
|
254
|
+
phase: "break", strengthQ: 0,
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Grip quality: higher when score differential is larger
|
|
259
|
+
const gripQ = clampQ(q(0.20) + mulDiv(Math.max(0, diff), q(0.60), SCALE.Q), q(0.10), q(0.90));
|
|
260
|
+
attacker.grapple.holdingTargetId = target.id;
|
|
261
|
+
attacker.grapple.gripQ = gripQ;
|
|
262
|
+
attacker.grapple.position = "standing";
|
|
263
|
+
// Add to target's heldByIds (sorted, no duplicates)
|
|
264
|
+
if (!target.grapple.heldByIds.includes(attacker.id)) {
|
|
265
|
+
target.grapple.heldByIds.push(attacker.id);
|
|
266
|
+
target.grapple.heldByIds.sort((a, b) => a - b);
|
|
267
|
+
}
|
|
268
|
+
// Overwhelming leverage → immediate trip in tactical/sim
|
|
269
|
+
if (tuning.realism !== "arcade") {
|
|
270
|
+
const levDiff = leverageDiff(attacker, target, funcA, funcB);
|
|
271
|
+
if (levDiff > tripThreshold(attacker, target)) {
|
|
272
|
+
target.condition.prone = true;
|
|
273
|
+
attacker.grapple.position = "prone";
|
|
274
|
+
// Small kinetic impact from the trip (~1 J/kg of target)
|
|
275
|
+
const tripEnergy_J = mulDiv(target.attributes.morphology.mass_kg, 1, SCALE.kg);
|
|
276
|
+
pushImpact(impacts, attacker.id, target.id, "torso", tripEnergy_J, GRAPPLE_THROW_WPN, q(0.50));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
trace.onEvent({
|
|
280
|
+
kind: TraceKinds.Grapple,
|
|
281
|
+
tick: world.tick, attackerId: attacker.id, targetId: target.id,
|
|
282
|
+
phase: "start", strengthQ: gripQ,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Attempt to throw or trip the grappled target.
|
|
287
|
+
*
|
|
288
|
+
* Requires: attacker already holds the target (holdingTargetId === target.id).
|
|
289
|
+
* Success probability based on signed leverage differential.
|
|
290
|
+
*
|
|
291
|
+
* On success: target goes prone, kinetic impact queued, grapple released.
|
|
292
|
+
* On failure: cooldown set, energy still drained.
|
|
293
|
+
*
|
|
294
|
+
* Impact energy ∝ target mass × leverage advantage × intensity (see formula in code).
|
|
295
|
+
*/
|
|
296
|
+
export function resolveGrappleThrow(world, attacker, target, intensity, tuning, impacts, trace) {
|
|
297
|
+
if (attacker.grapple.holdingTargetId !== target.id)
|
|
298
|
+
return;
|
|
299
|
+
if (attacker.action.grappleCooldownTicks > 0)
|
|
300
|
+
return;
|
|
301
|
+
const funcA = deriveFunctionalState(attacker, tuning);
|
|
302
|
+
const funcB = deriveFunctionalState(target, tuning);
|
|
303
|
+
if (!funcA.canAct)
|
|
304
|
+
return;
|
|
305
|
+
// Probability centred at 30% base; leverage advantage pushes toward 90%
|
|
306
|
+
const levDiff = leverageDiff(attacker, target, funcA, funcB);
|
|
307
|
+
const levAdv = clampQ((levDiff + q(0.90)), q(0.20), q(1.80));
|
|
308
|
+
const p = clampQ(q(0.30) + mulDiv(levAdv, q(0.35), q(1.80)), q(0.05), q(0.90));
|
|
309
|
+
const seed = eventSeed(world.seed, world.tick, attacker.id, target.id, SALT_THROW);
|
|
310
|
+
const success = (seed % SCALE.Q) < p;
|
|
311
|
+
attacker.energy.reserveEnergy_J = Math.max(0, attacker.energy.reserveEnergy_J - throwCost_J(attacker));
|
|
312
|
+
attacker.action.grappleCooldownTicks = tuning.realism === "arcade" ? 4 : 6;
|
|
313
|
+
if (!success) {
|
|
314
|
+
trace.onEvent({
|
|
315
|
+
kind: TraceKinds.Grapple,
|
|
316
|
+
tick: world.tick, attackerId: attacker.id, targetId: target.id,
|
|
317
|
+
phase: "tick", strengthQ: attacker.grapple.gripQ,
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
target.condition.prone = true;
|
|
322
|
+
attacker.grapple.position = "prone";
|
|
323
|
+
// throwEnergy = targetMass_kg × 2 × levAdv × intensity
|
|
324
|
+
// mulDiv(mB, 2, SCALE.kg) converts fixed-point kg → 2×kg (Joules at ~1 m/s effective)
|
|
325
|
+
const mB = target.attributes.morphology.mass_kg;
|
|
326
|
+
const throwEnergy_J = mulDiv(mulDiv(mB, 2, SCALE.kg), qMul(levAdv, clampQ(intensity, q(0.1), q(1.0))), SCALE.Q);
|
|
327
|
+
pushImpact(impacts, attacker.id, target.id, "torso", throwEnergy_J, GRAPPLE_THROW_WPN, q(0.70));
|
|
328
|
+
releaseGrapple(attacker, target);
|
|
329
|
+
trace.onEvent({
|
|
330
|
+
kind: TraceKinds.Grapple,
|
|
331
|
+
tick: world.tick, attackerId: attacker.id, targetId: target.id,
|
|
332
|
+
phase: "break", strengthQ: 0,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Apply a choke hold: accumulates suffocation on the target.
|
|
337
|
+
*
|
|
338
|
+
* Requires position !== "standing" in tactical/sim (must be on the ground).
|
|
339
|
+
* Sufficient grip quality (> 0.60) transitions the position to "pinned" and
|
|
340
|
+
* sets target.condition.pinned.
|
|
341
|
+
*/
|
|
342
|
+
export function resolveGrappleChoke(attacker, target, intensity, tuning) {
|
|
343
|
+
if (attacker.grapple.holdingTargetId !== target.id)
|
|
344
|
+
return;
|
|
345
|
+
if (attacker.grapple.position === "standing" && tuning.realism !== "arcade")
|
|
346
|
+
return;
|
|
347
|
+
const funcA = deriveFunctionalState(attacker, tuning);
|
|
348
|
+
if (!funcA.canAct)
|
|
349
|
+
return;
|
|
350
|
+
// Choke dose = grip × intensity × technique; builds suffocation at fixed rate
|
|
351
|
+
const CHOKE_RATE = q(0.008);
|
|
352
|
+
const dose = qMul(qMul(attacker.grapple.gripQ, clampQ(intensity, q(0.1), q(1.0))), funcA.manipulationMul);
|
|
353
|
+
target.condition.suffocation = clampQ(target.condition.suffocation + qMul(dose, CHOKE_RATE), 0, SCALE.Q);
|
|
354
|
+
// Strong grip → advance to pinned
|
|
355
|
+
if (attacker.grapple.gripQ > q(0.60)) {
|
|
356
|
+
attacker.grapple.position = "pinned";
|
|
357
|
+
target.condition.pinned = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Apply a joint-lock: structural damage to a target limb.
|
|
362
|
+
*
|
|
363
|
+
* Requires position !== "standing" in tactical/sim.
|
|
364
|
+
* Target limb selected deterministically (stable across seeds).
|
|
365
|
+
*
|
|
366
|
+
* Impact energy = peakForce × 0.05 m effective displacement × grip × intensity.
|
|
367
|
+
*/
|
|
368
|
+
export function resolveGrappleJointLock(world, attacker, target, intensity, tuning, impacts) {
|
|
369
|
+
if (attacker.grapple.holdingTargetId !== target.id)
|
|
370
|
+
return;
|
|
371
|
+
if (attacker.grapple.position === "standing" && tuning.realism !== "arcade")
|
|
372
|
+
return;
|
|
373
|
+
const funcA = deriveFunctionalState(attacker, tuning);
|
|
374
|
+
if (!funcA.canAct)
|
|
375
|
+
return;
|
|
376
|
+
attacker.energy.reserveEnergy_J = Math.max(0, attacker.energy.reserveEnergy_J - lockCost_J(attacker));
|
|
377
|
+
// Energy: (F / SCALE.N) × 0.05 m × grip × intensity
|
|
378
|
+
// mulDiv(f, 5, SCALE.N * 100) = f_N × 0.05 in J (SCALE.N=100; 5/100 = 0.05/SCALE.N→J)
|
|
379
|
+
const f = attacker.attributes.performance.peakForce_N;
|
|
380
|
+
const gripEffect = qMul(qMul(attacker.grapple.gripQ, clampQ(intensity, q(0.1), q(1.0))), funcA.manipulationMul);
|
|
381
|
+
const lockEnergy_J = mulDiv(mulDiv(f, 5, SCALE.N * 100), gripEffect, SCALE.Q);
|
|
382
|
+
if (lockEnergy_J <= 0)
|
|
383
|
+
return;
|
|
384
|
+
// Deterministic region: 0=leftArm, 1=rightArm, 2=leftLeg, 3=rightLeg
|
|
385
|
+
const seed = eventSeed(world.seed, world.tick, attacker.id, target.id, SALT_JOINTLOCK);
|
|
386
|
+
const regionBit = seed & 3;
|
|
387
|
+
const region = regionBit === 0 ? "leftArm" :
|
|
388
|
+
regionBit === 1 ? "rightArm" :
|
|
389
|
+
regionBit === 2 ? "leftLeg" : "rightLeg";
|
|
390
|
+
pushImpact(impacts, attacker.id, target.id, region, lockEnergy_J, GRAPPLE_JOINTLOCK_WPN, q(0.80));
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Attempt to break free from all current holders.
|
|
394
|
+
*
|
|
395
|
+
* Pair-based: each holder gets an independent contest (lower id owns the seed).
|
|
396
|
+
* On success: releaseGrapple() called for that holder.
|
|
397
|
+
* Energy drained per holder attempt regardless of outcome.
|
|
398
|
+
*/
|
|
399
|
+
export function resolveBreakGrapple(world, breaker, intensity, tuning, index, trace) {
|
|
400
|
+
if (breaker.grapple.heldByIds.length === 0)
|
|
401
|
+
return;
|
|
402
|
+
const funcB = deriveFunctionalState(breaker, tuning);
|
|
403
|
+
const toRelease = [];
|
|
404
|
+
for (const holderId of breaker.grapple.heldByIds) {
|
|
405
|
+
const holder = index.byId.get(holderId);
|
|
406
|
+
// Auto-release stale links
|
|
407
|
+
if (!holder || holder.injury.dead || holder.grapple.holdingTargetId !== breaker.id) {
|
|
408
|
+
toRelease.push(holderId);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const funcA = deriveFunctionalState(holder, tuning);
|
|
412
|
+
const scoreBreaker = clampQ(qMul(grappleContestScore(breaker, funcB), clampQ(intensity, q(0.1), q(1.0))), q(0.05), q(0.95));
|
|
413
|
+
const scoreHolder = clampQ(qMul(grappleContestScore(holder, funcA), holder.grapple.gripQ), q(0.05), q(0.95));
|
|
414
|
+
const diff = (scoreBreaker - scoreHolder);
|
|
415
|
+
const p = clampQ(q(0.40) + mulDiv(diff, q(0.40), SCALE.Q), q(0.05), q(0.90));
|
|
416
|
+
// Stable pair seed: lower id first
|
|
417
|
+
const idLo = Math.min(breaker.id, holderId);
|
|
418
|
+
const idHi = Math.max(breaker.id, holderId);
|
|
419
|
+
const seed = eventSeed(world.seed, world.tick, idLo, idHi, SALT_BREAK);
|
|
420
|
+
const success = (seed % SCALE.Q) < p;
|
|
421
|
+
breaker.energy.reserveEnergy_J = Math.max(0, breaker.energy.reserveEnergy_J - attemptCost_J(breaker));
|
|
422
|
+
if (success) {
|
|
423
|
+
toRelease.push(holderId);
|
|
424
|
+
trace.onEvent({
|
|
425
|
+
kind: TraceKinds.Grapple,
|
|
426
|
+
tick: world.tick, attackerId: holderId, targetId: breaker.id,
|
|
427
|
+
phase: "break", strengthQ: 0,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const holderId of toRelease) {
|
|
432
|
+
const holder = index.byId.get(holderId);
|
|
433
|
+
if (holder) {
|
|
434
|
+
releaseGrapple(holder, breaker);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
breaker.grapple.heldByIds = breaker.grapple.heldByIds.filter(id => id !== holderId);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (breaker.grapple.heldByIds.length === 0) {
|
|
441
|
+
breaker.condition.pinned = false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Per-tick maintenance for active grapples.
|
|
446
|
+
* Call once per entity per tick (regardless of whether a grapple command was issued).
|
|
447
|
+
*
|
|
448
|
+
* - Drains stamina from the holder
|
|
449
|
+
* - Decays gripQ by GRIP_DECAY_PER_TICK
|
|
450
|
+
* - Releases grapple when grip reaches 0 or target is dead/missing
|
|
451
|
+
*/
|
|
452
|
+
export function stepGrappleTick(world, entity, index) {
|
|
453
|
+
if (!entity.grapple || entity.grapple.holdingTargetId === 0)
|
|
454
|
+
return;
|
|
455
|
+
const target = index.byId.get(entity.grapple.holdingTargetId);
|
|
456
|
+
if (!target || target.injury.dead) {
|
|
457
|
+
releaseGrapple(entity, target ?? null);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
entity.energy.reserveEnergy_J = Math.max(0, entity.energy.reserveEnergy_J - tickCost_J(entity));
|
|
461
|
+
entity.grapple.gripQ = clampQ(entity.grapple.gripQ - GRIP_DECAY_PER_TICK, 0, SCALE.Q);
|
|
462
|
+
if (entity.grapple.gripQ <= 0) {
|
|
463
|
+
releaseGrapple(entity, target);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Release a grapple link, updating both the holder and (optionally) the target.
|
|
468
|
+
* Safe to call with a null target (e.g. when target entity was already removed).
|
|
469
|
+
*/
|
|
470
|
+
export function releaseGrapple(holder, target) {
|
|
471
|
+
holder.grapple.holdingTargetId = 0;
|
|
472
|
+
holder.grapple.gripQ = q(0);
|
|
473
|
+
holder.grapple.position = "standing";
|
|
474
|
+
if (target) {
|
|
475
|
+
target.grapple.heldByIds = target.grapple.heldByIds.filter(id => id !== holder.id);
|
|
476
|
+
if (target.grapple.heldByIds.length === 0) {
|
|
477
|
+
target.condition.pinned = false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type Q } from "../units.js";
|
|
2
|
+
/** The five environmental threat categories. */
|
|
3
|
+
export type HazardType = "fire" | "radiation" | "toxic_gas" | "acid" | "extreme_cold";
|
|
4
|
+
/** All hazard type identifiers (useful for validation and iteration). */
|
|
5
|
+
export declare const ALL_HAZARD_TYPES: HazardType[];
|
|
6
|
+
/** A persistent circular hazard zone in world-space. */
|
|
7
|
+
export interface HazardZone {
|
|
8
|
+
id: string;
|
|
9
|
+
type: HazardType;
|
|
10
|
+
/** Centre x-coordinate [SCALE.m]. */
|
|
11
|
+
x_Sm: number;
|
|
12
|
+
/** Centre y-coordinate [SCALE.m]. */
|
|
13
|
+
y_Sm: number;
|
|
14
|
+
/** Radius beyond which exposure is zero [SCALE.m]. */
|
|
15
|
+
radius_Sm: number;
|
|
16
|
+
/** Peak intensity at the hazard centre [Q]. */
|
|
17
|
+
intensity_Q: Q;
|
|
18
|
+
/**
|
|
19
|
+
* Remaining lifetime in seconds.
|
|
20
|
+
* `-1` = permanent (never expires; stepHazardZone is a no-op).
|
|
21
|
+
* Decremented by `stepHazardZone`. Clamped to 0 at expiry.
|
|
22
|
+
*/
|
|
23
|
+
durationSeconds: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Per-second hazard effect rates.
|
|
27
|
+
*
|
|
28
|
+
* All Q fields are non-negative except `thermalDelta_Q` (negative = cooling).
|
|
29
|
+
* The host multiplies by `dt` before applying, except `thermalDelta_Q` which
|
|
30
|
+
* is a continuous ambient offset (applied as-is each tick).
|
|
31
|
+
*/
|
|
32
|
+
export interface HazardEffect {
|
|
33
|
+
/** Fatigue added per second of exposure [Q/s]. */
|
|
34
|
+
fatigueInc_Q: Q;
|
|
35
|
+
/**
|
|
36
|
+
* Thermal bias while inside the hazard [Q].
|
|
37
|
+
* Positive = heating (fire), negative = cooling (extreme_cold).
|
|
38
|
+
* Uses Phase 29/51 Q encoding: 185 Q ≈ 1 °C.
|
|
39
|
+
*/
|
|
40
|
+
thermalDelta_Q: Q;
|
|
41
|
+
/** Cumulative radiation dose per second [Q/s]. Maps to Phase 53 radiation_dose. */
|
|
42
|
+
radiationDose_Q: Q;
|
|
43
|
+
/** Surface integrity damage per second [Q/s]. */
|
|
44
|
+
surfaceDamageInc_Q: Q;
|
|
45
|
+
/** Disease profile ID to test for exposure this tick (or undefined). */
|
|
46
|
+
diseaseExposureId?: string;
|
|
47
|
+
}
|
|
48
|
+
/** A modest campfire — 3 m radius, 1-hour duration. */
|
|
49
|
+
export declare const CAMPFIRE: HazardZone;
|
|
50
|
+
/** A contaminated crater — 50 m radius, permanent. */
|
|
51
|
+
export declare const RADIATION_ZONE: HazardZone;
|
|
52
|
+
/** A drifting toxic-gas cloud — 20 m radius, 30-minute duration. */
|
|
53
|
+
export declare const MUSTARD_GAS: HazardZone;
|
|
54
|
+
/** A corrosive acid pool — 2 m radius, 2-hour duration. */
|
|
55
|
+
export declare const ACID_POOL: HazardZone;
|
|
56
|
+
/** A severe cold zone — 100 m radius, 6-hour duration. */
|
|
57
|
+
export declare const BLIZZARD_ZONE: HazardZone;
|
|
58
|
+
/** All sample hazard zones. */
|
|
59
|
+
export declare const ALL_SAMPLE_HAZARDS: HazardZone[];
|
|
60
|
+
/**
|
|
61
|
+
* Euclidean distance from a world position to the hazard centre [SCALE.m].
|
|
62
|
+
*
|
|
63
|
+
* Uses float sqrt for a one-time calculation; result is truncated to integer.
|
|
64
|
+
*/
|
|
65
|
+
export declare function computeDistToHazard(x_Sm: number, y_Sm: number, hazard: HazardZone): number;
|
|
66
|
+
/**
|
|
67
|
+
* True if the given position is within or on the hazard boundary.
|
|
68
|
+
*
|
|
69
|
+
* Uses integer squared-distance comparison to avoid float precision issues.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isInsideHazard(x_Sm: number, y_Sm: number, hazard: HazardZone): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Compute the exposure intensity at a given distance from the hazard centre.
|
|
74
|
+
*
|
|
75
|
+
* Linear falloff: `exposure = (radius − dist) × intensity / radius`
|
|
76
|
+
* Returns `q(0)` when `dist >= radius`.
|
|
77
|
+
*
|
|
78
|
+
* @param dist_Sm Distance from hazard centre [SCALE.m].
|
|
79
|
+
*/
|
|
80
|
+
export declare function computeHazardExposure(dist_Sm: number, hazard: HazardZone): Q;
|
|
81
|
+
/**
|
|
82
|
+
* Derive per-second hazard effect rates from an exposure level.
|
|
83
|
+
*
|
|
84
|
+
* `exposureQ` is the output of `computeHazardExposure` — already in [0, intensity_Q].
|
|
85
|
+
* Each base rate is scaled linearly: `rate = base × exposureQ / SCALE.Q`.
|
|
86
|
+
*
|
|
87
|
+
* `thermalDelta_Q` uses the same scaling so the thermal offset fades toward the
|
|
88
|
+
* hazard boundary.
|
|
89
|
+
*
|
|
90
|
+
* Returns a zero-effect record when `exposureQ <= 0`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function deriveHazardEffect(hazard: HazardZone, exposureQ: Q): HazardEffect;
|
|
93
|
+
/**
|
|
94
|
+
* Advance a hazard zone's lifetime by `elapsedSeconds`.
|
|
95
|
+
*
|
|
96
|
+
* Permanent hazards (`durationSeconds === -1`) are untouched.
|
|
97
|
+
* Mutates: `hazard.durationSeconds`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function stepHazardZone(hazard: HazardZone, elapsedSeconds: number): void;
|
|
100
|
+
/**
|
|
101
|
+
* True when the hazard has run out of duration and should be removed from the world.
|
|
102
|
+
* Always false for permanent hazards.
|
|
103
|
+
*/
|
|
104
|
+
export declare function isHazardExpired(hazard: HazardZone): boolean;
|