@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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 6 — Formation System
|
|
3
|
+
*
|
|
4
|
+
* Pure computation module for tactical formation mechanics:
|
|
5
|
+
* - Shield walls: adjacent shield-bearers pool block coverage
|
|
6
|
+
* - Rank depth: entities sorted by forward projection; front/rear split
|
|
7
|
+
* - Casualty fill: rear-rank entities promote when front rank is lost
|
|
8
|
+
* - Push of pike: total formation momentum from mass × velocity
|
|
9
|
+
* - Formation cohesion: morale bonus / rout-contagion penalty
|
|
10
|
+
*
|
|
11
|
+
* No Entity or WorldState imports — callers extract the needed values and
|
|
12
|
+
* pass plain numbers / maps. All arithmetic is integer fixed-point.
|
|
13
|
+
*/
|
|
14
|
+
import { SCALE, q, clampQ, mulDiv, qMul } from "../../src/units.js";
|
|
15
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* Efficiency of shield coverage sharing between adjacent bearers.
|
|
18
|
+
* Each additional bearer beyond the first contributes at this fraction.
|
|
19
|
+
*/
|
|
20
|
+
export const SHIELD_SHARING_FRAC = q(0.60);
|
|
21
|
+
/** Hard cap on combined shield wall coverage. */
|
|
22
|
+
export const SHIELD_WALL_MAX_COVERAGE = SCALE.Q;
|
|
23
|
+
/** Default rank depth used to split front/rear ranks (2 m in SCALE.m). */
|
|
24
|
+
export const RANK_DEPTH_DEFAULT_m = Math.round(2.0 * SCALE.m);
|
|
25
|
+
/**
|
|
26
|
+
* Minimum intact fraction for a formation to be considered cohesive.
|
|
27
|
+
* Below this threshold the rout-contagion penalty applies instead of the
|
|
28
|
+
* morale bonus.
|
|
29
|
+
*/
|
|
30
|
+
export const FORMATION_INTACT_THRESHOLD = q(0.60);
|
|
31
|
+
/** Fear decay bonus per tick granted to entities in an intact formation. */
|
|
32
|
+
export const FORMATION_MORALE_BONUS = q(0.008);
|
|
33
|
+
/** Fear increment per tick when formation integrity has collapsed. */
|
|
34
|
+
export const FORMATION_MORALE_PENALTY = q(0.010);
|
|
35
|
+
/** Per-tick fear decay granted per alive formation ally (vs q(0.002) unaffiliated). */
|
|
36
|
+
export const FORMATION_ALLY_FEAR_DECAY = q(0.004);
|
|
37
|
+
/** Maximum number of formation allies counted for the fear decay bonus. */
|
|
38
|
+
export const FORMATION_ALLY_DECAY_CAP = 8;
|
|
39
|
+
// ─── Shield wall ──────────────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Compute the combined block coverage of a shield wall.
|
|
42
|
+
*
|
|
43
|
+
* The bearer with the highest individual coverage contributes at full strength.
|
|
44
|
+
* Each subsequent bearer (sorted descending by coverage) contributes at
|
|
45
|
+
* SHIELD_SHARING_FRAC efficiency, modelling timing gaps and partial overlap
|
|
46
|
+
* between adjacent shields.
|
|
47
|
+
*
|
|
48
|
+
* Result is capped at SHIELD_WALL_MAX_COVERAGE = q(1.0).
|
|
49
|
+
*/
|
|
50
|
+
export function computeShieldWallCoverage(coverageFracs) {
|
|
51
|
+
if (coverageFracs.length === 0)
|
|
52
|
+
return q(0);
|
|
53
|
+
// Sort descending so the highest coverage bearer contributes fully.
|
|
54
|
+
const sorted = [...coverageFracs].sort((a, b) => b - a);
|
|
55
|
+
let total = sorted[0];
|
|
56
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
57
|
+
total += mulDiv(sorted[i], SHIELD_SHARING_FRAC, SCALE.Q);
|
|
58
|
+
}
|
|
59
|
+
return clampQ(Math.round(total), 0, SHIELD_WALL_MAX_COVERAGE);
|
|
60
|
+
}
|
|
61
|
+
// ─── Rank depth ───────────────────────────────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* Split formation entity IDs into front and rear ranks.
|
|
64
|
+
*
|
|
65
|
+
* Each entity position is projected onto the facing direction. The entity
|
|
66
|
+
* with the greatest projection defines depth = 0. Entities within
|
|
67
|
+
* `rankDepth_m` of the frontmost entity are placed in the front rank; the
|
|
68
|
+
* rest go to the rear rank. Both ranks are sorted front-to-back.
|
|
69
|
+
*
|
|
70
|
+
* @param entityIds IDs to split
|
|
71
|
+
* @param positions Map: entity ID → {x, y} in SCALE.m
|
|
72
|
+
* @param facingDirQ Unit vector with components in Q units (SCALE.Q = 1.0)
|
|
73
|
+
* @param rankDepth_m Depth of front rank in SCALE.m (default: RANK_DEPTH_DEFAULT_m)
|
|
74
|
+
*/
|
|
75
|
+
export function deriveRankSplit(entityIds, positions, facingDirQ, rankDepth_m = RANK_DEPTH_DEFAULT_m) {
|
|
76
|
+
if (entityIds.length === 0)
|
|
77
|
+
return { frontRank: [], rearRank: [] };
|
|
78
|
+
// Project each entity onto the facing direction (result in SCALE.m).
|
|
79
|
+
const projections = new Map();
|
|
80
|
+
for (const id of entityIds) {
|
|
81
|
+
const pos = positions.get(id);
|
|
82
|
+
if (!pos) {
|
|
83
|
+
projections.set(id, 0);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const proj = Math.trunc((pos.x * facingDirQ.x + pos.y * facingDirQ.y) / SCALE.Q);
|
|
87
|
+
projections.set(id, proj);
|
|
88
|
+
}
|
|
89
|
+
// Frontmost entity has the largest projection.
|
|
90
|
+
let maxProj = -Infinity;
|
|
91
|
+
for (const proj of projections.values()) {
|
|
92
|
+
if (proj > maxProj)
|
|
93
|
+
maxProj = proj;
|
|
94
|
+
}
|
|
95
|
+
const frontRank = [];
|
|
96
|
+
const rearRank = [];
|
|
97
|
+
for (const id of entityIds) {
|
|
98
|
+
const proj = projections.get(id) ?? 0;
|
|
99
|
+
if (proj >= maxProj - rankDepth_m) {
|
|
100
|
+
frontRank.push(id);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
rearRank.push(id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Sort front-to-back (highest projection first) for determinism.
|
|
107
|
+
const byProjDesc = (a, b) => (projections.get(b) ?? 0) - (projections.get(a) ?? 0);
|
|
108
|
+
frontRank.sort(byProjDesc);
|
|
109
|
+
rearRank.sort(byProjDesc);
|
|
110
|
+
return { frontRank, rearRank };
|
|
111
|
+
}
|
|
112
|
+
// ─── Casualty fill ────────────────────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* Promote rear-rank entities to fill vacancies left by front-rank losses.
|
|
115
|
+
*
|
|
116
|
+
* Returns new front and rear rank arrays with dead entities removed and
|
|
117
|
+
* replacements drawn from the front of the (alive) rear rank.
|
|
118
|
+
*/
|
|
119
|
+
export function stepFormationCasualtyFill(rankSplit, deadIds) {
|
|
120
|
+
const aliveFront = rankSplit.frontRank.filter(id => !deadIds.has(id));
|
|
121
|
+
const aliveRear = rankSplit.rearRank.filter(id => !deadIds.has(id));
|
|
122
|
+
const vacancies = rankSplit.frontRank.length - aliveFront.length;
|
|
123
|
+
const replacements = aliveRear.splice(0, vacancies);
|
|
124
|
+
return { frontRank: [...aliveFront, ...replacements], rearRank: aliveRear };
|
|
125
|
+
}
|
|
126
|
+
// ─── Push of pike / formation momentum ───────────────────────────────────────
|
|
127
|
+
/**
|
|
128
|
+
* Compute the total forward momentum of a formation (push-of-pike model).
|
|
129
|
+
*
|
|
130
|
+
* `momentum_Skg_mps` is in (SCALE.kg × m/s) units — i.e. fixed-point mass
|
|
131
|
+
* multiplied by real-valued speed (speed already divided by SCALE.mps).
|
|
132
|
+
* Divide by SCALE.kg (= 1000) to obtain physical kg·m/s.
|
|
133
|
+
*
|
|
134
|
+
* Only entities with speed > 0 contribute.
|
|
135
|
+
*
|
|
136
|
+
* @param masses_Skg Entity masses in SCALE.kg units
|
|
137
|
+
* @param speeds_Smps Entity speeds (magnitude) in SCALE.mps units
|
|
138
|
+
*/
|
|
139
|
+
export function computeFormationMomentum(masses_Skg, speeds_Smps) {
|
|
140
|
+
const n = Math.min(masses_Skg.length, speeds_Smps.length);
|
|
141
|
+
let sum = 0;
|
|
142
|
+
let count = 0;
|
|
143
|
+
for (let i = 0; i < n; i++) {
|
|
144
|
+
const speed = speeds_Smps[i];
|
|
145
|
+
if (speed <= 0)
|
|
146
|
+
continue;
|
|
147
|
+
sum += Math.trunc(masses_Skg[i] * speed / SCALE.mps);
|
|
148
|
+
count++;
|
|
149
|
+
}
|
|
150
|
+
return { momentum_Skg_mps: sum, entityCount: count };
|
|
151
|
+
}
|
|
152
|
+
// ─── Formation cohesion / morale ─────────────────────────────────────────────
|
|
153
|
+
/**
|
|
154
|
+
* Derive the morale state of a formation from casualty / rout status.
|
|
155
|
+
*
|
|
156
|
+
* An entity is counted as lost if its ID appears in `deadOrRoutedIds`.
|
|
157
|
+
* When intactFrac_Q ≥ FORMATION_INTACT_THRESHOLD the formation grants a fear
|
|
158
|
+
* decay bonus; below it a rout-contagion penalty applies instead.
|
|
159
|
+
*
|
|
160
|
+
* An empty formation is considered vacuously intact but grants no bonus.
|
|
161
|
+
*/
|
|
162
|
+
export function deriveFormationCohesion(entityIds, deadOrRoutedIds) {
|
|
163
|
+
const total = entityIds.length;
|
|
164
|
+
if (total === 0) {
|
|
165
|
+
return {
|
|
166
|
+
intact: true,
|
|
167
|
+
intactFrac_Q: SCALE.Q,
|
|
168
|
+
moraleBonus_Q: q(0),
|
|
169
|
+
moralePenalty_Q: q(0),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
let lostCount = 0;
|
|
173
|
+
for (const id of entityIds) {
|
|
174
|
+
if (deadOrRoutedIds.has(id))
|
|
175
|
+
lostCount++;
|
|
176
|
+
}
|
|
177
|
+
const intactCount = total - lostCount;
|
|
178
|
+
const intactFrac_Q = clampQ(Math.round((intactCount * SCALE.Q) / total), 0, SCALE.Q);
|
|
179
|
+
const intact = intactFrac_Q >= FORMATION_INTACT_THRESHOLD;
|
|
180
|
+
return {
|
|
181
|
+
intact,
|
|
182
|
+
intactFrac_Q,
|
|
183
|
+
moraleBonus_Q: intact ? FORMATION_MORALE_BONUS : q(0),
|
|
184
|
+
moralePenalty_Q: intact ? q(0) : FORMATION_MORALE_PENALTY,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Compute the per-tick fear decay bonus from alive formation allies.
|
|
189
|
+
*
|
|
190
|
+
* Returns a larger bonus than the unaffiliated ally coefficient (q(0.002)):
|
|
191
|
+
* formation allies grant q(0.004) each, capped at FORMATION_ALLY_DECAY_CAP
|
|
192
|
+
* allies. Result is a Q value to be added to the entity's fear decay term.
|
|
193
|
+
*/
|
|
194
|
+
export function deriveFormationAllyFearDecay(aliveFormationAllyCount) {
|
|
195
|
+
const capped = Math.min(aliveFormationAllyCount, FORMATION_ALLY_DECAY_CAP);
|
|
196
|
+
return clampQ((capped * FORMATION_ALLY_FEAR_DECAY), 0, qMul(FORMATION_ALLY_FEAR_DECAY, q(FORMATION_ALLY_DECAY_CAP)));
|
|
197
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Entity } from "./entity.js";
|
|
2
|
+
import type { WorldIndex } from "./indexing.js";
|
|
3
|
+
import type { SpatialIndex } from "./spatial.js";
|
|
4
|
+
import type { WorldState } from "./world.js";
|
|
5
|
+
export interface EngagementQuery {
|
|
6
|
+
reach_m: number;
|
|
7
|
+
buffer_m: number;
|
|
8
|
+
maxTargets: number;
|
|
9
|
+
requireFrontArc?: boolean;
|
|
10
|
+
minDotQ?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function pickNearestEnemyInReach(world: WorldState | undefined, attacker: Entity, index: WorldIndex, spatial: SpatialIndex, q: EngagementQuery): Entity | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { queryNearbyIds } from "./spatial.js";
|
|
2
|
+
import { isEnemy, areEntitiesHostile } from "./team.js";
|
|
3
|
+
import { dotDirQ, normaliseDirCheapQ } from "./vec3.js"; // wherever you keep these
|
|
4
|
+
export function pickNearestEnemyInReach(world, attacker, index, spatial, q) {
|
|
5
|
+
const radius_m = q.reach_m + q.buffer_m;
|
|
6
|
+
const ids = queryNearbyIds(spatial, attacker.position_m, radius_m);
|
|
7
|
+
ids.sort((a, b) => a - b);
|
|
8
|
+
// Collect candidates
|
|
9
|
+
const cand = [];
|
|
10
|
+
for (const id of ids) {
|
|
11
|
+
if (id === attacker.id)
|
|
12
|
+
continue;
|
|
13
|
+
const e = index.byId.get(id);
|
|
14
|
+
if (!e || e.injury.dead)
|
|
15
|
+
continue;
|
|
16
|
+
const hostile = world ? areEntitiesHostile(attacker, e, world) : isEnemy(attacker, e);
|
|
17
|
+
if (!hostile)
|
|
18
|
+
continue;
|
|
19
|
+
if (q.requireFrontArc) {
|
|
20
|
+
const dx = e.position_m.x - attacker.position_m.x;
|
|
21
|
+
const dy = e.position_m.y - attacker.position_m.y;
|
|
22
|
+
const dz = e.position_m.z - attacker.position_m.z;
|
|
23
|
+
const dir = normaliseDirCheapQ({ x: dx, y: dy, z: dz });
|
|
24
|
+
const dot = dotDirQ(attacker.action.facingDirQ, dir);
|
|
25
|
+
const minDot = q.minDotQ ?? 0;
|
|
26
|
+
if (dot < minDot)
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
cand.push(e);
|
|
30
|
+
if (cand.length >= q.maxTargets) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (cand.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
// Deterministic pick: smallest distance², tie by id
|
|
37
|
+
let best = cand[0];
|
|
38
|
+
let bestD2 = dist2(attacker, best);
|
|
39
|
+
for (let i = 1; i < cand.length; i++) {
|
|
40
|
+
const e = cand[i];
|
|
41
|
+
const d2 = dist2(attacker, e);
|
|
42
|
+
if (d2 < bestD2 || (d2 === bestD2 && e.id < best.id)) {
|
|
43
|
+
best = e;
|
|
44
|
+
bestD2 = d2;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return best;
|
|
48
|
+
}
|
|
49
|
+
function dist2(a, b) {
|
|
50
|
+
const dx = b.position_m.x - a.position_m.x;
|
|
51
|
+
const dy = b.position_m.y - a.position_m.y;
|
|
52
|
+
const dz = b.position_m.z - a.position_m.z;
|
|
53
|
+
return BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy) + BigInt(dz) * BigInt(dz);
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ImpactEvent } from "./events.js";
|
|
2
|
+
import type { WorldIndex } from "./indexing.js";
|
|
3
|
+
import type { ObstacleGrid } from "./terrain.js";
|
|
4
|
+
export interface FrontageRules {
|
|
5
|
+
maxEngagersPerTarget: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function applyFrontageCap(impacts: ImpactEvent[], index: WorldIndex, rules: FrontageRules): ImpactEvent[];
|
|
8
|
+
/**
|
|
9
|
+
* Phase 32E: Compute the passable frontage width (in entity-widths) through a
|
|
10
|
+
* corridor at `position_m` perpendicular to `facingDir`.
|
|
11
|
+
*
|
|
12
|
+
* Scans cells across the perpendicular axis within `scanRange_m` and counts
|
|
13
|
+
* cells where cover < SCALE.Q (not fully impassable). Returns the count as a
|
|
14
|
+
* rough maximum engager cap. Falls back to `defaultCap` when no grid is provided.
|
|
15
|
+
*
|
|
16
|
+
* @param obstacleGrid Obstacle grid (may be undefined)
|
|
17
|
+
* @param cellSize_m Grid cell size in SCALE.m units
|
|
18
|
+
* @param position_m Centre of the corridor query
|
|
19
|
+
* @param facingDir Unit direction of movement (used to determine perpendicular)
|
|
20
|
+
* @param scanRange_m How far left/right to scan (SCALE.m)
|
|
21
|
+
* @param entityWidth_m Average entity body width (SCALE.m; default 0.5 m)
|
|
22
|
+
* @param defaultCap Cap to return when obstacleGrid is undefined
|
|
23
|
+
*/
|
|
24
|
+
export declare function computeChokeCapacity(obstacleGrid: ObstacleGrid | undefined, cellSize_m: number, position_m: {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
}, facingDir: {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
}, scanRange_m: number, entityWidth_m?: number, defaultCap?: number): number;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { SCALE } from "../units.js";
|
|
2
|
+
import { coverFractionAtPosition } from "./terrain.js";
|
|
3
|
+
export function applyFrontageCap(impacts, index, rules) {
|
|
4
|
+
const maxK = Math.max(1, rules.maxEngagersPerTarget);
|
|
5
|
+
// group by targetId
|
|
6
|
+
const byTarget = new Map();
|
|
7
|
+
for (const ev of impacts) {
|
|
8
|
+
let arr = byTarget.get(ev.targetId);
|
|
9
|
+
if (!arr) {
|
|
10
|
+
arr = [];
|
|
11
|
+
byTarget.set(ev.targetId, arr);
|
|
12
|
+
}
|
|
13
|
+
arr.push(ev);
|
|
14
|
+
}
|
|
15
|
+
const kept = [];
|
|
16
|
+
for (const [targetId, arr] of byTarget.entries()) {
|
|
17
|
+
if (arr.length <= maxK) {
|
|
18
|
+
for (const ev of arr)
|
|
19
|
+
kept.push(ev);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const target = index.byId.get(targetId);
|
|
23
|
+
if (!target)
|
|
24
|
+
continue;
|
|
25
|
+
// sort attackers by distance² then attackerId
|
|
26
|
+
arr.sort((a, b) => {
|
|
27
|
+
const da = dist2ByIds(index, a.attackerId, target);
|
|
28
|
+
const db = dist2ByIds(index, b.attackerId, target);
|
|
29
|
+
if (da < db)
|
|
30
|
+
return -1;
|
|
31
|
+
if (da > db)
|
|
32
|
+
return 1;
|
|
33
|
+
return a.attackerId - b.attackerId;
|
|
34
|
+
});
|
|
35
|
+
for (let i = 0; i < maxK; i++)
|
|
36
|
+
kept.push(arr[i]);
|
|
37
|
+
}
|
|
38
|
+
return kept;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Phase 32E: Compute the passable frontage width (in entity-widths) through a
|
|
42
|
+
* corridor at `position_m` perpendicular to `facingDir`.
|
|
43
|
+
*
|
|
44
|
+
* Scans cells across the perpendicular axis within `scanRange_m` and counts
|
|
45
|
+
* cells where cover < SCALE.Q (not fully impassable). Returns the count as a
|
|
46
|
+
* rough maximum engager cap. Falls back to `defaultCap` when no grid is provided.
|
|
47
|
+
*
|
|
48
|
+
* @param obstacleGrid Obstacle grid (may be undefined)
|
|
49
|
+
* @param cellSize_m Grid cell size in SCALE.m units
|
|
50
|
+
* @param position_m Centre of the corridor query
|
|
51
|
+
* @param facingDir Unit direction of movement (used to determine perpendicular)
|
|
52
|
+
* @param scanRange_m How far left/right to scan (SCALE.m)
|
|
53
|
+
* @param entityWidth_m Average entity body width (SCALE.m; default 0.5 m)
|
|
54
|
+
* @param defaultCap Cap to return when obstacleGrid is undefined
|
|
55
|
+
*/
|
|
56
|
+
export function computeChokeCapacity(obstacleGrid, cellSize_m, position_m, facingDir, scanRange_m, entityWidth_m = Math.trunc(0.5 * SCALE.m), defaultCap = 100) {
|
|
57
|
+
if (!obstacleGrid)
|
|
58
|
+
return defaultCap;
|
|
59
|
+
if (cellSize_m <= 0 || entityWidth_m <= 0)
|
|
60
|
+
return defaultCap;
|
|
61
|
+
// Perpendicular to facing (rotate 90°)
|
|
62
|
+
const perpX = -facingDir.y;
|
|
63
|
+
const perpY = facingDir.x;
|
|
64
|
+
let passableCells = 0;
|
|
65
|
+
const steps = Math.max(1, Math.round(scanRange_m / cellSize_m));
|
|
66
|
+
for (let i = -steps; i <= steps; i++) {
|
|
67
|
+
const cx = position_m.x + Math.trunc(perpX * i * cellSize_m);
|
|
68
|
+
const cy = position_m.y + Math.trunc(perpY * i * cellSize_m);
|
|
69
|
+
const cov = coverFractionAtPosition(obstacleGrid, cellSize_m, cx, cy);
|
|
70
|
+
if (cov < SCALE.Q)
|
|
71
|
+
passableCells++;
|
|
72
|
+
}
|
|
73
|
+
const passableWidth = passableCells * cellSize_m;
|
|
74
|
+
return Math.max(1, Math.floor(passableWidth / entityWidth_m));
|
|
75
|
+
}
|
|
76
|
+
function dist2ByIds(index, attackerId, target) {
|
|
77
|
+
const a = index.byId.get(attackerId);
|
|
78
|
+
if (!a)
|
|
79
|
+
return (1n << 62n); // big + safe, avoids magic decimal
|
|
80
|
+
const dx = target.position_m.x - a.position_m.x;
|
|
81
|
+
const dy = target.position_m.y - a.position_m.y;
|
|
82
|
+
const dz = target.position_m.z - a.position_m.z;
|
|
83
|
+
return BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy) + BigInt(dz) * BigInt(dz);
|
|
84
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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 { type Q } from "../units.js";
|
|
11
|
+
import type { Entity } from "./entity.js";
|
|
12
|
+
import type { WorldState } from "./world.js";
|
|
13
|
+
import type { WorldIndex } from "./indexing.js";
|
|
14
|
+
import type { FunctionalState } from "./impairment.js";
|
|
15
|
+
import type { TraceSink } from "./trace.js";
|
|
16
|
+
import type { SimulationTuning } from "./tuning.js";
|
|
17
|
+
import type { ImpactEvent } from "./events.js";
|
|
18
|
+
import type { Weapon } from "../equipment.js";
|
|
19
|
+
export declare const GRIP_DECAY_PER_TICK: Q;
|
|
20
|
+
export declare const GRAPPLE_THROW_WPN: Weapon;
|
|
21
|
+
export declare const GRAPPLE_JOINTLOCK_WPN: Weapon;
|
|
22
|
+
/**
|
|
23
|
+
* Compute an entity's grapple contest score in Q [0.05, 0.95].
|
|
24
|
+
*
|
|
25
|
+
* Combines:
|
|
26
|
+
* 50% peak force (normalised to human baseline)
|
|
27
|
+
* 30% technique (controlQuality × stability)
|
|
28
|
+
* 20% body mass (normalised to human baseline)
|
|
29
|
+
*
|
|
30
|
+
* The result is modulated by the entity's current functional state
|
|
31
|
+
* (injury, fatigue) via manipulationMul.
|
|
32
|
+
*
|
|
33
|
+
* A healthy average human scores ≈ q(0.47).
|
|
34
|
+
*/
|
|
35
|
+
export declare function grappleContestScore(e: Entity, func: FunctionalState): Q;
|
|
36
|
+
/**
|
|
37
|
+
* Attempt to initiate a grapple on the target.
|
|
38
|
+
*
|
|
39
|
+
* Contest: scoreA × intensity vs scoreB. Success probability centred at 0.50
|
|
40
|
+
* with ±40% swing per unit score difference (mirrors melee hit formula).
|
|
41
|
+
*
|
|
42
|
+
* On success:
|
|
43
|
+
* - Attacker's grapple.holdingTargetId and gripQ are set
|
|
44
|
+
* - Target's grapple.heldByIds is updated (sorted, deduplicated)
|
|
45
|
+
* - Overwhelming leverage differential causes immediate trip (prone + small impact)
|
|
46
|
+
*
|
|
47
|
+
* On failure: grappleCooldownTicks set, energy still drained.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveGrappleAttempt(world: WorldState, attacker: Entity, target: Entity, intensity: Q, tuning: SimulationTuning, impacts: ImpactEvent[], trace: TraceSink): void;
|
|
50
|
+
/**
|
|
51
|
+
* Attempt to throw or trip the grappled target.
|
|
52
|
+
*
|
|
53
|
+
* Requires: attacker already holds the target (holdingTargetId === target.id).
|
|
54
|
+
* Success probability based on signed leverage differential.
|
|
55
|
+
*
|
|
56
|
+
* On success: target goes prone, kinetic impact queued, grapple released.
|
|
57
|
+
* On failure: cooldown set, energy still drained.
|
|
58
|
+
*
|
|
59
|
+
* Impact energy ∝ target mass × leverage advantage × intensity (see formula in code).
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveGrappleThrow(world: WorldState, attacker: Entity, target: Entity, intensity: Q, tuning: SimulationTuning, impacts: ImpactEvent[], trace: TraceSink): void;
|
|
62
|
+
/**
|
|
63
|
+
* Apply a choke hold: accumulates suffocation on the target.
|
|
64
|
+
*
|
|
65
|
+
* Requires position !== "standing" in tactical/sim (must be on the ground).
|
|
66
|
+
* Sufficient grip quality (> 0.60) transitions the position to "pinned" and
|
|
67
|
+
* sets target.condition.pinned.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveGrappleChoke(attacker: Entity, target: Entity, intensity: Q, tuning: SimulationTuning): void;
|
|
70
|
+
/**
|
|
71
|
+
* Apply a joint-lock: structural damage to a target limb.
|
|
72
|
+
*
|
|
73
|
+
* Requires position !== "standing" in tactical/sim.
|
|
74
|
+
* Target limb selected deterministically (stable across seeds).
|
|
75
|
+
*
|
|
76
|
+
* Impact energy = peakForce × 0.05 m effective displacement × grip × intensity.
|
|
77
|
+
*/
|
|
78
|
+
export declare function resolveGrappleJointLock(world: WorldState, attacker: Entity, target: Entity, intensity: Q, tuning: SimulationTuning, impacts: ImpactEvent[]): void;
|
|
79
|
+
/**
|
|
80
|
+
* Attempt to break free from all current holders.
|
|
81
|
+
*
|
|
82
|
+
* Pair-based: each holder gets an independent contest (lower id owns the seed).
|
|
83
|
+
* On success: releaseGrapple() called for that holder.
|
|
84
|
+
* Energy drained per holder attempt regardless of outcome.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveBreakGrapple(world: WorldState, breaker: Entity, intensity: Q, tuning: SimulationTuning, index: WorldIndex, trace: TraceSink): void;
|
|
87
|
+
/**
|
|
88
|
+
* Per-tick maintenance for active grapples.
|
|
89
|
+
* Call once per entity per tick (regardless of whether a grapple command was issued).
|
|
90
|
+
*
|
|
91
|
+
* - Drains stamina from the holder
|
|
92
|
+
* - Decays gripQ by GRIP_DECAY_PER_TICK
|
|
93
|
+
* - Releases grapple when grip reaches 0 or target is dead/missing
|
|
94
|
+
*/
|
|
95
|
+
export declare function stepGrappleTick(world: WorldState, entity: Entity, index: WorldIndex): void;
|
|
96
|
+
/**
|
|
97
|
+
* Release a grapple link, updating both the holder and (optionally) the target.
|
|
98
|
+
* Safe to call with a null target (e.g. when target entity was already removed).
|
|
99
|
+
*/
|
|
100
|
+
export declare function releaseGrapple(holder: Entity, target: Entity | null): void;
|