@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,71 @@
|
|
|
1
|
+
import { queryNearbyIds } from "./spatial.js";
|
|
2
|
+
import { isEnemy, areEntitiesHostile } from "./team.js";
|
|
3
|
+
export function isMeleeLaneOccludedByFriendly(world, attacker, target, index, spatial, q) {
|
|
4
|
+
// Only matters if same team as attacker (friendly body blocking)
|
|
5
|
+
const laneR = q.laneRadius_m;
|
|
6
|
+
const ax = attacker.position_m.x, ay = attacker.position_m.y, az = attacker.position_m.z;
|
|
7
|
+
const tx = target.position_m.x, ty = target.position_m.y, tz = target.position_m.z;
|
|
8
|
+
const dx = tx - ax, dy = ty - ay, dz = tz - az;
|
|
9
|
+
// If target is on same team, don't treat as occlusion here (that’s friendly fire prevention elsewhere)
|
|
10
|
+
// Occlusion checks FRIENDLIES between attacker and ENEMY target
|
|
11
|
+
const hostile = world ? areEntitiesHostile(attacker, target, world) : isEnemy(attacker, target);
|
|
12
|
+
if (!hostile)
|
|
13
|
+
return false;
|
|
14
|
+
// Search near the midpoint with radius ~= half-distance + laneR
|
|
15
|
+
// This bounds the search to local neighbourhood (fast at scale)
|
|
16
|
+
const mx = ax + (dx >> 1);
|
|
17
|
+
const my = ay + (dy >> 1);
|
|
18
|
+
const mz = az + (dz >> 1);
|
|
19
|
+
// radius = half distance + lane radius
|
|
20
|
+
// sqrt not needed: use conservative bound in fixed-point
|
|
21
|
+
const halfDist = approxHalfDist(ax, ay, tx, ty); // metres scaled
|
|
22
|
+
const searchR = halfDist + laneR;
|
|
23
|
+
const ids = queryNearbyIds(spatial, { x: mx, y: my, z: mz }, searchR);
|
|
24
|
+
ids.sort((a, b) => a - b);
|
|
25
|
+
// Check for any friendly that lies between attacker and target AND within lane radius of the segment
|
|
26
|
+
for (const id of ids) {
|
|
27
|
+
if (id === attacker.id || id === target.id)
|
|
28
|
+
continue;
|
|
29
|
+
const e = index.byId.get(id);
|
|
30
|
+
if (!e || e.injury.dead)
|
|
31
|
+
continue;
|
|
32
|
+
if (e.teamId !== attacker.teamId)
|
|
33
|
+
continue; // only friendlies block
|
|
34
|
+
// must be between along the segment (0 < t < 1) and close to it
|
|
35
|
+
if (pointNearSegmentQ(e.position_m.x, e.position_m.y, ax, ay, tx, ty, laneR)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function approxHalfDist(ax, ay, tx, ty) {
|
|
42
|
+
const dx = tx - ax;
|
|
43
|
+
const dy = ty - ay;
|
|
44
|
+
const adx = dx < 0 ? -dx : dx;
|
|
45
|
+
const ady = dy < 0 ? -dy : dy;
|
|
46
|
+
// Cheap L∞/L1 mix; we just need a conservative search radius
|
|
47
|
+
const approx = adx > ady ? adx + (ady >> 1) : ady + (adx >> 1);
|
|
48
|
+
return approx >> 1;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns true if point P is within laneR of segment A->B in 2D, and lies between A and B.
|
|
52
|
+
* Uses only integer math (deterministic).
|
|
53
|
+
*/
|
|
54
|
+
function pointNearSegmentQ(px, py, ax, ay, bx, by, laneR) {
|
|
55
|
+
const vx = bx - ax;
|
|
56
|
+
const vy = by - ay;
|
|
57
|
+
const wx = px - ax;
|
|
58
|
+
const wy = py - ay;
|
|
59
|
+
const vv = BigInt(vx) * BigInt(vx) + BigInt(vy) * BigInt(vy);
|
|
60
|
+
if (vv === 0n)
|
|
61
|
+
return false;
|
|
62
|
+
const tNum = BigInt(wx) * BigInt(vx) + BigInt(wy) * BigInt(vy);
|
|
63
|
+
if (tNum <= 0n || tNum >= vv)
|
|
64
|
+
return false; // strictly between
|
|
65
|
+
// distance^2 to line = |w|^2 - (w·v)^2 / |v|^2
|
|
66
|
+
const ww = BigInt(wx) * BigInt(wx) + BigInt(wy) * BigInt(wy);
|
|
67
|
+
const proj2 = (tNum * tNum) / vv;
|
|
68
|
+
const dist2 = ww - proj2;
|
|
69
|
+
const lane2 = BigInt(laneR) * BigInt(laneR);
|
|
70
|
+
return dist2 <= lane2;
|
|
71
|
+
}
|
|
@@ -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,44 @@
|
|
|
1
|
+
import { type Q, type I32 } from "../units.js";
|
|
2
|
+
import type { RangedWeapon } from "../equipment.js";
|
|
3
|
+
export declare const TICK_HZ_RANGED = 20;
|
|
4
|
+
/**
|
|
5
|
+
* Energy remaining after ballistic drag over a given range.
|
|
6
|
+
*
|
|
7
|
+
* Linear approximation: energyFrac = max(0, 1 - range_m × dragCoeff_perM)
|
|
8
|
+
* dragCoeff_perM is a Q value: q(0.007) means 0.7% loss per metre.
|
|
9
|
+
*/
|
|
10
|
+
export declare function energyAtRange_J(launchEnergy_J: I32, dragCoeff_perM: Q, range_m: I32): I32;
|
|
11
|
+
/**
|
|
12
|
+
* Adjusted dispersion (angular error in Q) accounting for shooter skill, fatigue, and
|
|
13
|
+
* aiming intensity.
|
|
14
|
+
*
|
|
15
|
+
* controlMod: [1.0, 1.5] — poor aim widens spread
|
|
16
|
+
* fatigueMod: [1.0, 1.5] — fatigue widens spread
|
|
17
|
+
* intensityMod: [1.0, 1.9] — low intensity (snap shot) widens spread
|
|
18
|
+
*/
|
|
19
|
+
export declare function adjustedDispersionQ(baseDispQ: Q, controlQuality: Q, fineControl: Q, fatigue: Q, intensity: Q): Q;
|
|
20
|
+
/**
|
|
21
|
+
* Grouping radius (m in SCALE.m) at given range.
|
|
22
|
+
* groupingRadius_m = dispersionQ × range_m / SCALE.Q
|
|
23
|
+
*/
|
|
24
|
+
export declare function groupingRadius_m(dispersionQ: Q, range_m: I32): I32;
|
|
25
|
+
/**
|
|
26
|
+
* Launch energy (J) for thrown weapons derived from thrower's peak power.
|
|
27
|
+
* Models a ~100ms burst at 10% peak power.
|
|
28
|
+
* Calibration: 1200 W × 0.10 = 120 J for average human.
|
|
29
|
+
*/
|
|
30
|
+
export declare function thrownLaunchEnergy_J(peakPower_W: I32): I32;
|
|
31
|
+
/**
|
|
32
|
+
* Number of simulation ticks before the next shot can be fired.
|
|
33
|
+
* recycleTime_s is in SCALE.s units.
|
|
34
|
+
*/
|
|
35
|
+
export declare function recycleTicks(wpn: RangedWeapon, tickHz: number): number;
|
|
36
|
+
/**
|
|
37
|
+
* Energy cost (J) of firing a shot at the given intensity.
|
|
38
|
+
* Modelled as ~50 ms draw/snap at 8% peak power for bows/throws;
|
|
39
|
+
* negligible for firearms but still costs something (aim/recoil recovery).
|
|
40
|
+
*
|
|
41
|
+
* For weapons where launchEnergy derives from the shooter (thrown),
|
|
42
|
+
* we use a larger fraction (10%) since the throw itself burns energy.
|
|
43
|
+
*/
|
|
44
|
+
export declare function shootCost_J(wpn: RangedWeapon, intensity: Q, peakPower_W: I32): I32;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/sim/ranged.ts — Phase 3: pure ranged-combat physics (no Entity import)
|
|
2
|
+
import { SCALE, q, clampQ, qMul, mulDiv } from "../units.js";
|
|
3
|
+
export const TICK_HZ_RANGED = 20; // must match kernel TICK_HZ
|
|
4
|
+
/**
|
|
5
|
+
* Energy remaining after ballistic drag over a given range.
|
|
6
|
+
*
|
|
7
|
+
* Linear approximation: energyFrac = max(0, 1 - range_m × dragCoeff_perM)
|
|
8
|
+
* dragCoeff_perM is a Q value: q(0.007) means 0.7% loss per metre.
|
|
9
|
+
*/
|
|
10
|
+
export function energyAtRange_J(launchEnergy_J, dragCoeff_perM, range_m) {
|
|
11
|
+
if (launchEnergy_J <= 0)
|
|
12
|
+
return 0;
|
|
13
|
+
const lossFrac = mulDiv(range_m, dragCoeff_perM, SCALE.m); // Q-scaled fraction lost
|
|
14
|
+
const energyFrac = Math.max(0, SCALE.Q - lossFrac);
|
|
15
|
+
return mulDiv(launchEnergy_J, energyFrac, SCALE.Q);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Adjusted dispersion (angular error in Q) accounting for shooter skill, fatigue, and
|
|
19
|
+
* aiming intensity.
|
|
20
|
+
*
|
|
21
|
+
* controlMod: [1.0, 1.5] — poor aim widens spread
|
|
22
|
+
* fatigueMod: [1.0, 1.5] — fatigue widens spread
|
|
23
|
+
* intensityMod: [1.0, 1.9] — low intensity (snap shot) widens spread
|
|
24
|
+
*/
|
|
25
|
+
export function adjustedDispersionQ(baseDispQ, controlQuality, fineControl, fatigue, intensity) {
|
|
26
|
+
const aimSkill = clampQ(((controlQuality + fineControl) >>> 1), q(0), q(1.0));
|
|
27
|
+
// controlMod in Q: 2.0 - aimSkill, clamped [1.0, 1.5]
|
|
28
|
+
const controlMod = clampQ((2 * SCALE.Q - aimSkill), SCALE.Q, Math.round(1.5 * SCALE.Q));
|
|
29
|
+
// fatigueMod in Q: 1.0 + fatigue × 0.5, clamped [1.0, 1.5]
|
|
30
|
+
const fatigueMod = clampQ((SCALE.Q + qMul(fatigue, q(0.50))), SCALE.Q, Math.round(1.5 * SCALE.Q));
|
|
31
|
+
// intensityMod in Q: 2.0 - intensity, clamped [1.0, 1.9]
|
|
32
|
+
const intensityMod = clampQ((2 * SCALE.Q - clampQ(intensity, q(0.1), SCALE.Q)), SCALE.Q, Math.round(1.9 * SCALE.Q));
|
|
33
|
+
return qMul(qMul(qMul(baseDispQ, controlMod), fatigueMod), intensityMod);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Grouping radius (m in SCALE.m) at given range.
|
|
37
|
+
* groupingRadius_m = dispersionQ × range_m / SCALE.Q
|
|
38
|
+
*/
|
|
39
|
+
export function groupingRadius_m(dispersionQ, range_m) {
|
|
40
|
+
return mulDiv(dispersionQ, range_m, SCALE.Q);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Launch energy (J) for thrown weapons derived from thrower's peak power.
|
|
44
|
+
* Models a ~100ms burst at 10% peak power.
|
|
45
|
+
* Calibration: 1200 W × 0.10 = 120 J for average human.
|
|
46
|
+
*/
|
|
47
|
+
export function thrownLaunchEnergy_J(peakPower_W) {
|
|
48
|
+
return Math.max(10, Math.trunc(peakPower_W / 10));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Number of simulation ticks before the next shot can be fired.
|
|
52
|
+
* recycleTime_s is in SCALE.s units.
|
|
53
|
+
*/
|
|
54
|
+
export function recycleTicks(wpn, tickHz) {
|
|
55
|
+
return Math.max(1, Math.trunc((wpn.recycleTime_s * tickHz) / SCALE.s));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Energy cost (J) of firing a shot at the given intensity.
|
|
59
|
+
* Modelled as ~50 ms draw/snap at 8% peak power for bows/throws;
|
|
60
|
+
* negligible for firearms but still costs something (aim/recoil recovery).
|
|
61
|
+
*
|
|
62
|
+
* For weapons where launchEnergy derives from the shooter (thrown),
|
|
63
|
+
* we use a larger fraction (10%) since the throw itself burns energy.
|
|
64
|
+
*/
|
|
65
|
+
export function shootCost_J(wpn, intensity, peakPower_W) {
|
|
66
|
+
const fracBase = wpn.category === "thrown" ? 10 : wpn.category === "bow" ? 6 : 2;
|
|
67
|
+
const base = Math.max(5, Math.trunc(peakPower_W * fracBase / 100));
|
|
68
|
+
return Math.max(2, mulDiv(base, intensity, SCALE.Q));
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function eventSeed(worldSeed, tick, aId, bId, salt) {
|
|
2
|
+
let x = (worldSeed ^ (tick * 0x9E3779B1) ^ (aId * 0x85EBCA77) ^ (bId * 0xC2B2AE3D) ^ salt) >>> 0;
|
|
3
|
+
x ^= x >>> 16;
|
|
4
|
+
x = Math.imul(x, 0x7FEB352D) >>> 0;
|
|
5
|
+
x ^= x >>> 15;
|
|
6
|
+
x = Math.imul(x, 0x846CA68B) >>> 0;
|
|
7
|
+
x ^= x >>> 16;
|
|
8
|
+
return x >>> 0;
|
|
9
|
+
}
|
|
10
|
+
/** Deterministic hash of a string to a number (simple sum of char codes). */
|
|
11
|
+
export function hashString(s) {
|
|
12
|
+
let h = 0;
|
|
13
|
+
for (let i = 0; i < s.length; i++)
|
|
14
|
+
h += s.charCodeAt(i);
|
|
15
|
+
return h;
|
|
16
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Q } from "../units.js";
|
|
2
|
+
import type { Entity } from "./entity.js";
|
|
3
|
+
import type { SensoryEnvironment } from "./sensory.js";
|
|
4
|
+
import type { WindField, PrecipitationType } from "./weather.js";
|
|
5
|
+
/**
|
|
6
|
+
* Extra sensory capabilities attached to an entity.
|
|
7
|
+
* All fields are optional — absent = that modality is not present.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExtendedSenses {
|
|
10
|
+
/**
|
|
11
|
+
* Echolocation detection range [SCALE.m].
|
|
12
|
+
* Non-zero = entity can detect physical objects via reflected sound.
|
|
13
|
+
* Bypasses lightMul and smokeMul; degraded by high ambient noise (noiseMul).
|
|
14
|
+
*/
|
|
15
|
+
echolocationRange_m?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Electroreception detection range [SCALE.m].
|
|
18
|
+
* Non-zero = entity can detect the bioelectric fields of living creatures.
|
|
19
|
+
* Short-range (~1–4 m for real-world species); unaffected by light or noise.
|
|
20
|
+
* Dead entities have no bioelectric field — not detectable.
|
|
21
|
+
*/
|
|
22
|
+
electroreceptionRange_m?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Olfaction (scent) sensitivity (Q 0..SCALE.Q).
|
|
25
|
+
* q(0) = absent; q(1.0) = wolf-level (detects prey at 50 m downwind).
|
|
26
|
+
* Wind direction enhances (downwind) or suppresses (upwind) detection.
|
|
27
|
+
* Precipitation disperses scent.
|
|
28
|
+
*/
|
|
29
|
+
olfactionSensitivity_Q?: Q;
|
|
30
|
+
}
|
|
31
|
+
/** Detected via echolocation — returned by canDetectExtended. */
|
|
32
|
+
export declare const DETECT_ECHOLOCATION: Q;
|
|
33
|
+
/** Detected via electroreception — returned by canDetectExtended. */
|
|
34
|
+
export declare const DETECT_ELECTRORECEPTION: Q;
|
|
35
|
+
/**
|
|
36
|
+
* Compute ambient light multiplier from time of day.
|
|
37
|
+
*
|
|
38
|
+
* Uses cosine interpolation: noon (12 h) → q(1.0); midnight (0/24 h) → q(0.10).
|
|
39
|
+
* 6 h / 18 h (dawn/dusk) → q(0.55).
|
|
40
|
+
*
|
|
41
|
+
* Intended for use as a multiplier on SensoryEnvironment.lightMul:
|
|
42
|
+
* `env.lightMul = Math.trunc(env.lightMul * computeDaylightMul(hour) / SCALE.Q)`
|
|
43
|
+
*
|
|
44
|
+
* @param hourOfDay Real-valued hour, 0–24 (0 and 24 are both midnight).
|
|
45
|
+
* @returns Q multiplier in [q(0.10), q(1.0)].
|
|
46
|
+
*/
|
|
47
|
+
export declare function computeDaylightMul(hourOfDay: number): Q;
|
|
48
|
+
/**
|
|
49
|
+
* Whether observer can detect subject via echolocation.
|
|
50
|
+
*
|
|
51
|
+
* - Requires `observer.extendedSenses.echolocationRange_m > 0`.
|
|
52
|
+
* - Unaffected by light or smoke — works in total darkness.
|
|
53
|
+
* - Effective range degraded by high ambient noise: `effectiveRange = range / noiseMul × SCALE.Q`.
|
|
54
|
+
* At default (noiseMul = SCALE.Q): full range. At 2× noise: half range.
|
|
55
|
+
* - Detects physical presence (dead entities still detected).
|
|
56
|
+
*
|
|
57
|
+
* @param dist_m Distance from observer to subject [SCALE.m].
|
|
58
|
+
* @param noiseMul Ambient noise multiplier from SensoryEnvironment.
|
|
59
|
+
*/
|
|
60
|
+
export declare function canDetectByEcholocation(observer: Entity, subject: Entity, dist_m: number, noiseMul: Q): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Whether observer can detect subject via electroreception.
|
|
63
|
+
*
|
|
64
|
+
* - Requires `observer.extendedSenses.electroreceptionRange_m > 0`.
|
|
65
|
+
* - Detects the bioelectric field of living entities only (dead = no field).
|
|
66
|
+
* - Unaffected by any environmental modifier.
|
|
67
|
+
*
|
|
68
|
+
* @param dist_m Distance from observer to subject [SCALE.m].
|
|
69
|
+
*/
|
|
70
|
+
export declare function canDetectByElectroreception(observer: Entity, subject: Entity, dist_m: number): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Olfactory detection strength (Q 0..SCALE.Q) of subject by observer.
|
|
73
|
+
*
|
|
74
|
+
* Returns q(0) if observer has no olfaction or subject is out of scent range.
|
|
75
|
+
* Returns higher values when:
|
|
76
|
+
* - Observer is close to subject.
|
|
77
|
+
* - Observer is downwind of subject (wind carries scent toward observer).
|
|
78
|
+
* - Precipitation is absent or light.
|
|
79
|
+
*
|
|
80
|
+
* @param dist_m Distance [SCALE.m].
|
|
81
|
+
* @param wind Optional wind field from WeatherState.
|
|
82
|
+
* @param precipitation Optional precipitation type from WeatherState.
|
|
83
|
+
*/
|
|
84
|
+
export declare function deriveScentDetection(observer: Entity, subject: Entity, dist_m: number, wind?: WindField, precipitation?: PrecipitationType): Q;
|
|
85
|
+
/**
|
|
86
|
+
* Full detection check including Phase 4 vision/hearing and Phase 52 extended modalities.
|
|
87
|
+
*
|
|
88
|
+
* Detection quality (Q) returned:
|
|
89
|
+
* q(1.0) — vision (primary)
|
|
90
|
+
* q(0.8) — electroreception (precise position, very short range)
|
|
91
|
+
* q(0.7) — echolocation (good spatial, darkness-independent)
|
|
92
|
+
* q(0.4) — hearing (Phase 4, omnidirectional)
|
|
93
|
+
* q(0.20–0.40) — olfaction (approximate, wind/rain dependent)
|
|
94
|
+
* q(0) — undetected
|
|
95
|
+
*
|
|
96
|
+
* @param sensorBoost Phase 11C sensor equipment bonus.
|
|
97
|
+
* @param wind Optional wind for olfaction wind-alignment calculation.
|
|
98
|
+
* @param precipitation Optional precipitation for scent dispersal.
|
|
99
|
+
*/
|
|
100
|
+
export declare function canDetectExtended(observer: Entity, subject: Entity, env: SensoryEnvironment, sensorBoost?: {
|
|
101
|
+
visionRangeMul: Q;
|
|
102
|
+
hearingRangeMul: Q;
|
|
103
|
+
}, wind?: WindField, precipitation?: PrecipitationType): Q;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// src/sim/sensory-extended.ts — Phase 52: Extended Sensory Systems
|
|
2
|
+
//
|
|
3
|
+
// Adds three sensory modalities beyond Phase 4 vision/hearing:
|
|
4
|
+
// - Echolocation: darkness-independent detection (bats, cetaceans, shrews)
|
|
5
|
+
// - Electroreception: bioelectric-field detection (sharks, eels, platypus)
|
|
6
|
+
// - Olfaction: wind-aware scent tracking (wolves, dogs, bears)
|
|
7
|
+
//
|
|
8
|
+
// Also provides computeDaylightMul() for time-of-day lighting integration
|
|
9
|
+
// with SensoryEnvironment.lightMul.
|
|
10
|
+
//
|
|
11
|
+
// Data flow:
|
|
12
|
+
// canDetect (Phase 4) → canDetectExtended → extended modality checks
|
|
13
|
+
// computeDaylightMul(hourOfDay) → SensoryEnvironment.lightMul multiplier
|
|
14
|
+
import { SCALE, q, clampQ, mulDiv } from "../units.js";
|
|
15
|
+
import { canDetect } from "./sensory.js";
|
|
16
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
17
|
+
/** Reference olfaction range [SCALE.m]: q(1.0) sensitivity detects at 50 m downwind. */
|
|
18
|
+
const OLFACTION_REF_RANGE_m = 50 * SCALE.m; // 50_000
|
|
19
|
+
/** Minimum scent strength (Q) to count as olfactory detection. */
|
|
20
|
+
const OLFACTION_DETECT_THRESHOLD = q(0.20);
|
|
21
|
+
/** Detected via echolocation — returned by canDetectExtended. */
|
|
22
|
+
export const DETECT_ECHOLOCATION = q(0.70);
|
|
23
|
+
/** Detected via electroreception — returned by canDetectExtended. */
|
|
24
|
+
export const DETECT_ELECTRORECEPTION = q(0.80);
|
|
25
|
+
/** Minimum olfaction detection quality — returned by canDetectExtended. */
|
|
26
|
+
const DETECT_OLFACTION_MIN = q(0.20);
|
|
27
|
+
const DETECT_OLFACTION_MAX = q(0.40);
|
|
28
|
+
/** Daylight Q at midnight (minimum). */
|
|
29
|
+
const DAYLIGHT_NIGHT_Q = q(0.10);
|
|
30
|
+
/** Precipitation scent dispersal multiplier (Q). */
|
|
31
|
+
const PRECIP_SCENT_MUL = {
|
|
32
|
+
none: q(1.00),
|
|
33
|
+
rain: q(0.70),
|
|
34
|
+
heavy_rain: q(0.40),
|
|
35
|
+
snow: q(0.80),
|
|
36
|
+
blizzard: q(0.20),
|
|
37
|
+
hail: q(0.60),
|
|
38
|
+
};
|
|
39
|
+
// ── computeDaylightMul ────────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Compute ambient light multiplier from time of day.
|
|
42
|
+
*
|
|
43
|
+
* Uses cosine interpolation: noon (12 h) → q(1.0); midnight (0/24 h) → q(0.10).
|
|
44
|
+
* 6 h / 18 h (dawn/dusk) → q(0.55).
|
|
45
|
+
*
|
|
46
|
+
* Intended for use as a multiplier on SensoryEnvironment.lightMul:
|
|
47
|
+
* `env.lightMul = Math.trunc(env.lightMul * computeDaylightMul(hour) / SCALE.Q)`
|
|
48
|
+
*
|
|
49
|
+
* @param hourOfDay Real-valued hour, 0–24 (0 and 24 are both midnight).
|
|
50
|
+
* @returns Q multiplier in [q(0.10), q(1.0)].
|
|
51
|
+
*/
|
|
52
|
+
export function computeDaylightMul(hourOfDay) {
|
|
53
|
+
// angle: noon (12h) maps to π → cos(π) = −1 → maximum light
|
|
54
|
+
// midnight (0h) maps to 0 → cos(0) = +1 → minimum light
|
|
55
|
+
const angle = hourOfDay * Math.PI / 12;
|
|
56
|
+
const cosVal = Math.cos(angle);
|
|
57
|
+
// Map cos ∈ [+1, −1] → frac ∈ [0, 1] (0 at midnight, 1 at noon)
|
|
58
|
+
const frac = (1 - cosVal) / 2;
|
|
59
|
+
const result = Math.round(DAYLIGHT_NIGHT_Q + frac * (SCALE.Q - DAYLIGHT_NIGHT_Q));
|
|
60
|
+
return clampQ(result, DAYLIGHT_NIGHT_Q, SCALE.Q);
|
|
61
|
+
}
|
|
62
|
+
// ── canDetectByEcholocation ───────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Whether observer can detect subject via echolocation.
|
|
65
|
+
*
|
|
66
|
+
* - Requires `observer.extendedSenses.echolocationRange_m > 0`.
|
|
67
|
+
* - Unaffected by light or smoke — works in total darkness.
|
|
68
|
+
* - Effective range degraded by high ambient noise: `effectiveRange = range / noiseMul × SCALE.Q`.
|
|
69
|
+
* At default (noiseMul = SCALE.Q): full range. At 2× noise: half range.
|
|
70
|
+
* - Detects physical presence (dead entities still detected).
|
|
71
|
+
*
|
|
72
|
+
* @param dist_m Distance from observer to subject [SCALE.m].
|
|
73
|
+
* @param noiseMul Ambient noise multiplier from SensoryEnvironment.
|
|
74
|
+
*/
|
|
75
|
+
export function canDetectByEcholocation(observer, subject, dist_m, noiseMul) {
|
|
76
|
+
const range = observer.extendedSenses?.echolocationRange_m;
|
|
77
|
+
if (!range || range <= 0)
|
|
78
|
+
return false;
|
|
79
|
+
// Echolocation detects physical mass (sound reflection), not life — dead entities ARE detected.
|
|
80
|
+
// Effective range: divided by noiseMul (SCALE.Q base)
|
|
81
|
+
const effectiveRange = mulDiv(range, SCALE.Q, Math.max(noiseMul, 1));
|
|
82
|
+
return dist_m <= effectiveRange;
|
|
83
|
+
}
|
|
84
|
+
// ── canDetectByElectroreception ───────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Whether observer can detect subject via electroreception.
|
|
87
|
+
*
|
|
88
|
+
* - Requires `observer.extendedSenses.electroreceptionRange_m > 0`.
|
|
89
|
+
* - Detects the bioelectric field of living entities only (dead = no field).
|
|
90
|
+
* - Unaffected by any environmental modifier.
|
|
91
|
+
*
|
|
92
|
+
* @param dist_m Distance from observer to subject [SCALE.m].
|
|
93
|
+
*/
|
|
94
|
+
export function canDetectByElectroreception(observer, subject, dist_m) {
|
|
95
|
+
const range = observer.extendedSenses?.electroreceptionRange_m;
|
|
96
|
+
if (!range || range <= 0)
|
|
97
|
+
return false;
|
|
98
|
+
if (subject.injury.dead)
|
|
99
|
+
return false; // no bioelectric field after death
|
|
100
|
+
return dist_m <= range;
|
|
101
|
+
}
|
|
102
|
+
// ── deriveScentDetection ──────────────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Olfactory detection strength (Q 0..SCALE.Q) of subject by observer.
|
|
105
|
+
*
|
|
106
|
+
* Returns q(0) if observer has no olfaction or subject is out of scent range.
|
|
107
|
+
* Returns higher values when:
|
|
108
|
+
* - Observer is close to subject.
|
|
109
|
+
* - Observer is downwind of subject (wind carries scent toward observer).
|
|
110
|
+
* - Precipitation is absent or light.
|
|
111
|
+
*
|
|
112
|
+
* @param dist_m Distance [SCALE.m].
|
|
113
|
+
* @param wind Optional wind field from WeatherState.
|
|
114
|
+
* @param precipitation Optional precipitation type from WeatherState.
|
|
115
|
+
*/
|
|
116
|
+
export function deriveScentDetection(observer, subject, dist_m, wind, precipitation) {
|
|
117
|
+
const sens = observer.extendedSenses?.olfactionSensitivity_Q;
|
|
118
|
+
if (!sens || sens <= 0)
|
|
119
|
+
return q(0);
|
|
120
|
+
if (dist_m <= 0)
|
|
121
|
+
return SCALE.Q;
|
|
122
|
+
// Base scent strength: decays with distance (q(1.0) sensitivity → detection at 50 m downwind).
|
|
123
|
+
const strength_Q = clampQ(Math.trunc(sens * OLFACTION_REF_RANGE_m / dist_m), q(0), SCALE.Q);
|
|
124
|
+
// Wind alignment: dot(subject→observer, windDir) / (dist_m × SCALE.m) ∈ [−1, +1].
|
|
125
|
+
// +1 = observer is directly downwind (scent maximally carried to observer).
|
|
126
|
+
// −1 = observer is upwind (scent blows away from observer).
|
|
127
|
+
let windMul_Q = q(0.50); // no wind → neutral (q(0.50))
|
|
128
|
+
if (wind && wind.speed_mps > 0 && dist_m > 0) {
|
|
129
|
+
const soX = observer.position_m.x - subject.position_m.x; // subject→observer
|
|
130
|
+
const soY = observer.position_m.y - subject.position_m.y;
|
|
131
|
+
const dot = soX * wind.dx_m + soY * wind.dy_m;
|
|
132
|
+
// windMul_Q ∈ [0, SCALE.Q]: 0 = full upwind, SCALE.Q = full downwind
|
|
133
|
+
windMul_Q = clampQ((5_000 + Math.trunc(dot * 5_000 / (dist_m * SCALE.m))), q(0), SCALE.Q);
|
|
134
|
+
}
|
|
135
|
+
// Precipitation disperses scent.
|
|
136
|
+
const precipMul_Q = PRECIP_SCENT_MUL[precipitation ?? "none"] ?? SCALE.Q;
|
|
137
|
+
const combined = Math.trunc(Math.trunc(strength_Q * windMul_Q / SCALE.Q) * precipMul_Q / SCALE.Q);
|
|
138
|
+
return clampQ(combined, q(0), SCALE.Q);
|
|
139
|
+
}
|
|
140
|
+
// ── canDetectExtended ─────────────────────────────────────────────────────────
|
|
141
|
+
/**
|
|
142
|
+
* Full detection check including Phase 4 vision/hearing and Phase 52 extended modalities.
|
|
143
|
+
*
|
|
144
|
+
* Detection quality (Q) returned:
|
|
145
|
+
* q(1.0) — vision (primary)
|
|
146
|
+
* q(0.8) — electroreception (precise position, very short range)
|
|
147
|
+
* q(0.7) — echolocation (good spatial, darkness-independent)
|
|
148
|
+
* q(0.4) — hearing (Phase 4, omnidirectional)
|
|
149
|
+
* q(0.20–0.40) — olfaction (approximate, wind/rain dependent)
|
|
150
|
+
* q(0) — undetected
|
|
151
|
+
*
|
|
152
|
+
* @param sensorBoost Phase 11C sensor equipment bonus.
|
|
153
|
+
* @param wind Optional wind for olfaction wind-alignment calculation.
|
|
154
|
+
* @param precipitation Optional precipitation for scent dispersal.
|
|
155
|
+
*/
|
|
156
|
+
export function canDetectExtended(observer, subject, env, sensorBoost, wind, precipitation) {
|
|
157
|
+
// Primary senses (vision + hearing, Phase 4).
|
|
158
|
+
const primary = canDetect(observer, subject, env, sensorBoost);
|
|
159
|
+
if (primary > q(0))
|
|
160
|
+
return primary;
|
|
161
|
+
// Extended senses require knowing distance.
|
|
162
|
+
const dx = subject.position_m.x - observer.position_m.x;
|
|
163
|
+
const dy = subject.position_m.y - observer.position_m.y;
|
|
164
|
+
const dz = subject.position_m.z - observer.position_m.z;
|
|
165
|
+
const dist_m = Math.trunc(Math.sqrt(dx * dx + dy * dy + dz * dz));
|
|
166
|
+
// Electroreception (highest quality after vision — very precise, very short range).
|
|
167
|
+
if (canDetectByElectroreception(observer, subject, dist_m)) {
|
|
168
|
+
return DETECT_ELECTRORECEPTION;
|
|
169
|
+
}
|
|
170
|
+
// Echolocation (good spatial awareness, works in darkness).
|
|
171
|
+
if (canDetectByEcholocation(observer, subject, dist_m, env.noiseMul)) {
|
|
172
|
+
return DETECT_ECHOLOCATION;
|
|
173
|
+
}
|
|
174
|
+
// Olfaction (approximate, wind-dependent).
|
|
175
|
+
const scent = deriveScentDetection(observer, subject, dist_m, wind, precipitation);
|
|
176
|
+
if (scent >= OLFACTION_DETECT_THRESHOLD) {
|
|
177
|
+
// Map scent strength → detection quality in [DETECT_OLFACTION_MIN, DETECT_OLFACTION_MAX].
|
|
178
|
+
return clampQ(Math.trunc(scent * (DETECT_OLFACTION_MAX - DETECT_OLFACTION_MIN) / SCALE.Q + DETECT_OLFACTION_MIN), DETECT_OLFACTION_MIN, DETECT_OLFACTION_MAX);
|
|
179
|
+
}
|
|
180
|
+
return q(0);
|
|
181
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4: Sensory environment model.
|
|
3
|
+
*
|
|
4
|
+
* Fixed-point only. No Math.random() — all randomness via eventSeed if needed.
|
|
5
|
+
* Light, smoke, and noise modifiers are Q values (SCALE.Q = full normal conditions).
|
|
6
|
+
*/
|
|
7
|
+
import type { Q } from "../units.js";
|
|
8
|
+
import type { Entity } from "./entity.js";
|
|
9
|
+
import type { Perception } from "../types.js";
|
|
10
|
+
export declare const DEFAULT_PERCEPTION: Perception;
|
|
11
|
+
export interface SensoryEnvironment {
|
|
12
|
+
/** Multiplier on vision range: q(1.0) = daylight, q(0.1) = near-dark. */
|
|
13
|
+
lightMul: Q;
|
|
14
|
+
/** Multiplier on vision range: q(1.0) = clear, q(0.2) = dense smoke. */
|
|
15
|
+
smokeMul: Q;
|
|
16
|
+
/** Multiplier on hearing range: q(1.0) = quiet, q(2.0) = loud battle noise. */
|
|
17
|
+
noiseMul: Q;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_SENSORY_ENV: SensoryEnvironment;
|
|
20
|
+
/**
|
|
21
|
+
* Compute detection quality of `subject` by `observer`.
|
|
22
|
+
*
|
|
23
|
+
* Returns a Q value:
|
|
24
|
+
* q(1.0) = fully visible (within vision arc and range)
|
|
25
|
+
* q(0.4) = heard only (within hearing range but not vision)
|
|
26
|
+
* q(0) = undetected
|
|
27
|
+
*
|
|
28
|
+
* Vision check: dot-product of facing direction vs observer→subject vector.
|
|
29
|
+
* Hearing: omnidirectional.
|
|
30
|
+
*
|
|
31
|
+
* Pure function — no side effects.
|
|
32
|
+
*/
|
|
33
|
+
export declare function canDetect(observer: Entity, subject: Entity, env: SensoryEnvironment,
|
|
34
|
+
/** Phase 11C: optional sensor boost from the observer's loadout. */
|
|
35
|
+
sensorBoost?: {
|
|
36
|
+
visionRangeMul: Q;
|
|
37
|
+
hearingRangeMul: Q;
|
|
38
|
+
}): Q;
|