@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,243 @@
|
|
|
1
|
+
// src/sim/aging.ts — Phase 57: Aging & Lifespan
|
|
2
|
+
//
|
|
3
|
+
// Attribute curves parameterized by normalized age fraction (ageFrac = ageYears / lifespanYears).
|
|
4
|
+
// Species-agnostic: a human at 25 years and an elf at 187 years both have ageFrac ≈ 0.31 and
|
|
5
|
+
// receive the same multipliers — the underlying biology follows the same developmental arc.
|
|
6
|
+
//
|
|
7
|
+
// Seven multiplier dimensions, each modelled as a piecewise-linear Q curve:
|
|
8
|
+
// muscularStrength — peakForce, peakPower, continuousPower (peaks ~0.28 ageFrac)
|
|
9
|
+
// reactionTime — multiplier on reactionTime_s (>q(1.0) = slower; peaks ~0.28)
|
|
10
|
+
// motorControl — controlQuality, stability, fineControl (peaks ~0.28)
|
|
11
|
+
// stature — stature_m (stable adult, slight compression in ancient)
|
|
12
|
+
// cognitionFluid — logical, spatial, kinesthetic, musical (peaks ~0.28)
|
|
13
|
+
// cognitionCrystal — linguistic, interpersonal, intrapersonal (peaks ~0.58)
|
|
14
|
+
// distressTolerance — pain/fear tolerance; wisdom accumulates to middle age
|
|
15
|
+
//
|
|
16
|
+
// Public API:
|
|
17
|
+
// computeAgeFrac(ageYears, lifespanYears?) → Q [0..SCALE.Q]
|
|
18
|
+
// getAgePhase(ageYears, lifespanYears?) → AgePhase
|
|
19
|
+
// deriveAgeMultipliers(ageYears, lifespanYears?) → AgeMultipliers
|
|
20
|
+
// applyAgingToAttributes(base, ageYears, ...) → IndividualAttributes (new object)
|
|
21
|
+
// stepAging(entity, elapsedSeconds) → mutates entity.age
|
|
22
|
+
import { q, clampQ, SCALE } from "../units.js";
|
|
23
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
24
|
+
/** Seconds in one year (non-leap). */
|
|
25
|
+
export const SECONDS_PER_YEAR = 365 * 86_400; // 31 536 000
|
|
26
|
+
/** Default lifespan for entities without a species override [years]. */
|
|
27
|
+
export const HUMAN_LIFESPAN_YEARS = 80;
|
|
28
|
+
const MUSCULAR_STRENGTH_KNOTS = [
|
|
29
|
+
[q(0.00), q(0.05)], // birth
|
|
30
|
+
[q(0.15), q(0.55)], // child
|
|
31
|
+
[q(0.25), q(1.00)], // peak young adult
|
|
32
|
+
[q(0.45), q(0.95)], // maintained adult
|
|
33
|
+
[q(0.65), q(0.75)], // elder decline
|
|
34
|
+
[q(1.00), q(0.40)], // ancient
|
|
35
|
+
];
|
|
36
|
+
// reactionTime_Q > SCALE.Q means reaction is SLOWER than the archetype baseline.
|
|
37
|
+
const REACTION_TIME_KNOTS = [
|
|
38
|
+
[q(0.00), 25_000], // infant (2.5× baseline — newborn has negligible voluntary motor)
|
|
39
|
+
[q(0.15), 12_000], // adolescent (1.2×)
|
|
40
|
+
[q(0.28), 10_000], // peak (1.0× = no change from archetype)
|
|
41
|
+
[q(0.50), 10_800], // slight adult slowdown (1.08×)
|
|
42
|
+
[q(0.70), 12_500], // elder (1.25×)
|
|
43
|
+
[q(1.00), 20_000], // ancient (2.0×)
|
|
44
|
+
];
|
|
45
|
+
const MOTOR_CONTROL_KNOTS = [
|
|
46
|
+
[q(0.00), q(0.30)], // infant
|
|
47
|
+
[q(0.15), q(0.80)], // adolescent
|
|
48
|
+
[q(0.28), q(1.00)], // peak
|
|
49
|
+
[q(0.60), q(0.95)], // adult maintained
|
|
50
|
+
[q(0.85), q(0.75)], // elder
|
|
51
|
+
[q(1.00), q(0.55)], // ancient
|
|
52
|
+
];
|
|
53
|
+
const STATURE_KNOTS = [
|
|
54
|
+
[q(0.00), q(0.30)], // infant
|
|
55
|
+
[q(0.20), q(0.95)], // adolescent growth
|
|
56
|
+
[q(0.25), q(1.00)], // peak adult height
|
|
57
|
+
[q(0.70), q(1.00)], // stable through adulthood
|
|
58
|
+
[q(0.90), q(0.97)], // slight compression in elder
|
|
59
|
+
[q(1.00), q(0.94)], // ancient
|
|
60
|
+
];
|
|
61
|
+
const COGNITION_FLUID_KNOTS = [
|
|
62
|
+
[q(0.00), q(0.10)], // infant
|
|
63
|
+
[q(0.20), q(0.90)], // adolescent rapid development
|
|
64
|
+
[q(0.28), q(1.00)], // peak young adult (~22 years for human)
|
|
65
|
+
[q(0.50), q(0.90)], // adult gradual decline
|
|
66
|
+
[q(0.70), q(0.70)], // elder
|
|
67
|
+
[q(1.00), q(0.35)], // ancient
|
|
68
|
+
];
|
|
69
|
+
const COGNITION_CRYSTAL_KNOTS = [
|
|
70
|
+
[q(0.00), q(0.10)], // infant
|
|
71
|
+
[q(0.25), q(0.75)], // young adult still accumulating wisdom
|
|
72
|
+
[q(0.55), q(1.00)], // peak middle age (~44 years for human)
|
|
73
|
+
[q(0.80), q(0.98)], // elder — wisdom mostly preserved
|
|
74
|
+
[q(1.00), q(0.78)], // ancient
|
|
75
|
+
];
|
|
76
|
+
const DISTRESS_TOLERANCE_KNOTS = [
|
|
77
|
+
[q(0.00), q(0.50)], // infant
|
|
78
|
+
[q(0.20), q(0.80)], // adolescent
|
|
79
|
+
[q(0.45), q(1.00)], // peaks middle age (hard-won tolerance)
|
|
80
|
+
[q(0.75), q(1.05)], // elder slightly above baseline (wisdom bonus)
|
|
81
|
+
[q(1.00), q(0.85)], // ancient — some decline
|
|
82
|
+
];
|
|
83
|
+
// ── Core computation ──────────────────────────────────────────────────────────
|
|
84
|
+
/** Piecewise-linear interpolation between sorted knot pairs. */
|
|
85
|
+
function interpKnots(x_Q, knots) {
|
|
86
|
+
if (x_Q <= knots[0][0])
|
|
87
|
+
return knots[0][1];
|
|
88
|
+
for (let i = 1; i < knots.length; i++) {
|
|
89
|
+
const [x0, y0] = knots[i - 1];
|
|
90
|
+
const [x1, y1] = knots[i];
|
|
91
|
+
if (x_Q <= x1) {
|
|
92
|
+
const span = x1 - x0;
|
|
93
|
+
if (span === 0)
|
|
94
|
+
return y0;
|
|
95
|
+
const t = Math.round((x_Q - x0) * SCALE.Q / span);
|
|
96
|
+
return (y0 + Math.round((y1 - y0) * t / SCALE.Q));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return knots[knots.length - 1][1];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compute normalized age fraction [0..SCALE.Q] for a given age and lifespan.
|
|
103
|
+
*
|
|
104
|
+
* A 25-year-old human (lifespan 80) → q(0.3125).
|
|
105
|
+
* A 187-year-old elf (lifespan 600) → q(0.312) — effectively the same developmental stage.
|
|
106
|
+
*
|
|
107
|
+
* @param ageYears Current age in years.
|
|
108
|
+
* @param lifespanYears Expected lifespan (default: HUMAN_LIFESPAN_YEARS).
|
|
109
|
+
*/
|
|
110
|
+
export function computeAgeFrac(ageYears, lifespanYears = HUMAN_LIFESPAN_YEARS) {
|
|
111
|
+
if (lifespanYears <= 0)
|
|
112
|
+
return q(0);
|
|
113
|
+
return clampQ(Math.round(ageYears * SCALE.Q / lifespanYears), q(0), SCALE.Q);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Classify the entity's life stage from their normalized age fraction.
|
|
117
|
+
*
|
|
118
|
+
* Boundaries (ageFrac):
|
|
119
|
+
* infant 0–0.05 | child 0.05–0.15 | adolescent 0.15–0.22 |
|
|
120
|
+
* young_adult 0.22–0.38 | adult 0.38–0.62 | elder 0.62–0.88 | ancient 0.88+
|
|
121
|
+
*/
|
|
122
|
+
export function getAgePhase(ageYears, lifespanYears = HUMAN_LIFESPAN_YEARS) {
|
|
123
|
+
const f = computeAgeFrac(ageYears, lifespanYears);
|
|
124
|
+
if (f < q(0.05))
|
|
125
|
+
return "infant";
|
|
126
|
+
if (f < q(0.15))
|
|
127
|
+
return "child";
|
|
128
|
+
if (f < q(0.22))
|
|
129
|
+
return "adolescent";
|
|
130
|
+
if (f < q(0.38))
|
|
131
|
+
return "young_adult";
|
|
132
|
+
if (f < q(0.62))
|
|
133
|
+
return "adult";
|
|
134
|
+
if (f < q(0.88))
|
|
135
|
+
return "elder";
|
|
136
|
+
return "ancient";
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Derive age-based attribute multipliers from normalized age and lifespan.
|
|
140
|
+
*
|
|
141
|
+
* All returned Q values except `reactionTime_Q` are in [0, SCALE.Q].
|
|
142
|
+
* `reactionTime_Q` may exceed SCALE.Q (values > q(1.0) indicate slower reaction
|
|
143
|
+
* than the archetype baseline).
|
|
144
|
+
*/
|
|
145
|
+
export function deriveAgeMultipliers(ageYears, lifespanYears = HUMAN_LIFESPAN_YEARS) {
|
|
146
|
+
const f = computeAgeFrac(ageYears, lifespanYears);
|
|
147
|
+
return {
|
|
148
|
+
muscularStrength_Q: interpKnots(f, MUSCULAR_STRENGTH_KNOTS),
|
|
149
|
+
reactionTime_Q: interpKnots(f, REACTION_TIME_KNOTS),
|
|
150
|
+
motorControl_Q: interpKnots(f, MOTOR_CONTROL_KNOTS),
|
|
151
|
+
stature_Q: interpKnots(f, STATURE_KNOTS),
|
|
152
|
+
cognitionFluid_Q: interpKnots(f, COGNITION_FLUID_KNOTS),
|
|
153
|
+
cognitionCrystal_Q: interpKnots(f, COGNITION_CRYSTAL_KNOTS),
|
|
154
|
+
distressTolerance_Q: interpKnots(f, DISTRESS_TOLERANCE_KNOTS),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Apply age multipliers to a base attribute set, returning a new object.
|
|
159
|
+
*
|
|
160
|
+
* The input `base` is treated as the archetype peak (typically from `generateIndividual`).
|
|
161
|
+
* The caller is responsible for caching the base and recomputing aged attributes when
|
|
162
|
+
* age advances (e.g. once per in-game month for campaign simulation).
|
|
163
|
+
*
|
|
164
|
+
* Attributes affected:
|
|
165
|
+
* - morphology.stature_m
|
|
166
|
+
* - performance.peakForce_N, peakPower_W, continuousPower_W
|
|
167
|
+
* - control.reactionTime_s, controlQuality, stability, fineControl
|
|
168
|
+
* - resilience.distressTolerance
|
|
169
|
+
* - cognition (if present): fluid dims + crystal dims scaled independently
|
|
170
|
+
*
|
|
171
|
+
* All Q outputs are clamped to [0, SCALE.Q]; reactionTime_s is clamped to ≥ 1.
|
|
172
|
+
*
|
|
173
|
+
* @param base Archetype-peak attributes (unmodified).
|
|
174
|
+
* @param ageYears Current age in years.
|
|
175
|
+
* @param lifespanYears Expected lifespan (default: HUMAN_LIFESPAN_YEARS).
|
|
176
|
+
*/
|
|
177
|
+
export function applyAgingToAttributes(base, ageYears, lifespanYears = HUMAN_LIFESPAN_YEARS) {
|
|
178
|
+
const m = deriveAgeMultipliers(ageYears, lifespanYears);
|
|
179
|
+
return {
|
|
180
|
+
...base,
|
|
181
|
+
morphology: {
|
|
182
|
+
...base.morphology,
|
|
183
|
+
stature_m: Math.max(1, Math.round(base.morphology.stature_m * m.stature_Q / SCALE.Q)),
|
|
184
|
+
},
|
|
185
|
+
performance: {
|
|
186
|
+
...base.performance,
|
|
187
|
+
peakForce_N: Math.max(1, Math.round(base.performance.peakForce_N * m.muscularStrength_Q / SCALE.Q)),
|
|
188
|
+
peakPower_W: Math.max(1, Math.round(base.performance.peakPower_W * m.muscularStrength_Q / SCALE.Q)),
|
|
189
|
+
continuousPower_W: Math.max(1, Math.round(base.performance.continuousPower_W * m.muscularStrength_Q / SCALE.Q)),
|
|
190
|
+
},
|
|
191
|
+
control: {
|
|
192
|
+
...base.control,
|
|
193
|
+
reactionTime_s: Math.max(1, Math.round(base.control.reactionTime_s * m.reactionTime_Q / SCALE.Q)),
|
|
194
|
+
controlQuality: clampQ(Math.round(base.control.controlQuality * m.motorControl_Q / SCALE.Q), q(0), SCALE.Q),
|
|
195
|
+
stability: clampQ(Math.round(base.control.stability * m.motorControl_Q / SCALE.Q), q(0), SCALE.Q),
|
|
196
|
+
fineControl: clampQ(Math.round(base.control.fineControl * m.motorControl_Q / SCALE.Q), q(0), SCALE.Q),
|
|
197
|
+
},
|
|
198
|
+
resilience: {
|
|
199
|
+
...base.resilience,
|
|
200
|
+
distressTolerance: clampQ(Math.round(base.resilience.distressTolerance * m.distressTolerance_Q / SCALE.Q), q(0), SCALE.Q),
|
|
201
|
+
},
|
|
202
|
+
// exactOptionalPropertyTypes: spread present cognition, otherwise omit the key entirely.
|
|
203
|
+
...(base.cognition
|
|
204
|
+
? {
|
|
205
|
+
cognition: {
|
|
206
|
+
...base.cognition,
|
|
207
|
+
// Fluid intelligence (peaks young, declines earlier)
|
|
208
|
+
logicalMathematical: clampQ(Math.round(base.cognition.logicalMathematical * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
|
|
209
|
+
spatial: clampQ(Math.round(base.cognition.spatial * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
|
|
210
|
+
bodilyKinesthetic: clampQ(Math.round(base.cognition.bodilyKinesthetic * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
|
|
211
|
+
musical: clampQ(Math.round(base.cognition.musical * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
|
|
212
|
+
// Crystallized intelligence (peaks mid-life, persists through elder)
|
|
213
|
+
linguistic: clampQ(Math.round(base.cognition.linguistic * m.cognitionCrystal_Q / SCALE.Q), q(0), SCALE.Q),
|
|
214
|
+
interpersonal: clampQ(Math.round(base.cognition.interpersonal * m.cognitionCrystal_Q / SCALE.Q), q(0), SCALE.Q),
|
|
215
|
+
intrapersonal: clampQ(Math.round(base.cognition.intrapersonal * m.cognitionCrystal_Q / SCALE.Q), q(0), SCALE.Q),
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Advance an entity's age by `elapsedSeconds`.
|
|
223
|
+
*
|
|
224
|
+
* Initializes `entity.age` if absent. Does NOT recompute attributes — the host
|
|
225
|
+
* should call `applyAgingToAttributes` when it needs current aged stats.
|
|
226
|
+
*
|
|
227
|
+
* Mutates: `entity.age`.
|
|
228
|
+
*/
|
|
229
|
+
export function stepAging(entity, elapsedSeconds) {
|
|
230
|
+
if (!entity.age) {
|
|
231
|
+
entity.age = { ageSeconds: 0 };
|
|
232
|
+
}
|
|
233
|
+
entity.age.ageSeconds += elapsedSeconds;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Convenience helper: return the current age in fractional years from entity.age.
|
|
237
|
+
* Returns 0 if `entity.age` is absent.
|
|
238
|
+
*/
|
|
239
|
+
export function entityAgeYears(entity) {
|
|
240
|
+
if (!entity.age)
|
|
241
|
+
return 0;
|
|
242
|
+
return entity.age.ageSeconds / SECONDS_PER_YEAR;
|
|
243
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Entity } from "../entity.js";
|
|
2
|
+
import type { WorldState } from "../world.js";
|
|
3
|
+
import type { WorldIndex } from "../indexing.js";
|
|
4
|
+
import type { SpatialIndex } from "../spatial.js";
|
|
5
|
+
import type { Command } from "../commands.js";
|
|
6
|
+
import { type I32 } from "../../units.js";
|
|
7
|
+
import type { AIPolicy } from "./types.js";
|
|
8
|
+
import { type SensoryEnvironment } from "../sensory.js";
|
|
9
|
+
import { type ObstacleGrid } from "../terrain.js";
|
|
10
|
+
export declare function decideCommandsForEntity(world: WorldState, index: WorldIndex, spatial: SpatialIndex, self: Entity, policy: AIPolicy, env?: SensoryEnvironment, obstacleGrid?: ObstacleGrid, cellSize_m?: I32): readonly Command[];
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { q, clampQ, qMul, mulDiv, SCALE } from "../../units.js";
|
|
2
|
+
import { pickTarget, updateFocus } from "./targeting.js";
|
|
3
|
+
import { findWeapon } from "../../equipment.js";
|
|
4
|
+
import { v3, normaliseDirCheapQ } from "../vec3.js";
|
|
5
|
+
import { DEFAULT_PERCEPTION, DEFAULT_SENSORY_ENV } from "../sensory.js";
|
|
6
|
+
import { isRouting, moraleThreshold } from "../morale.js";
|
|
7
|
+
import { eventSeed } from "../seeds.js";
|
|
8
|
+
import { coverFractionAtPosition, terrainKey } from "../terrain.js";
|
|
9
|
+
import { getSkill } from "../skills.js";
|
|
10
|
+
import { TICK_HZ } from "../tick.js";
|
|
11
|
+
import { effectiveStanding, STANDING_FRIENDLY_THRESHOLD } from "../../faction.js";
|
|
12
|
+
import { computeEffectiveRetreatRange, computeDefenceIntensityBoost, applyLoyaltyBias, applyOpportunismBias, computeEffectiveLoyalty, } from "./personality.js";
|
|
13
|
+
export function decideCommandsForEntity(world, index, spatial, self, policy, env = DEFAULT_SENSORY_ENV, obstacleGrid, cellSize_m) {
|
|
14
|
+
if (self.injury.dead)
|
|
15
|
+
return [];
|
|
16
|
+
// Feature 4: surrendered entities are permanently passive
|
|
17
|
+
if ((self.condition).surrendered) {
|
|
18
|
+
return [
|
|
19
|
+
{ kind: "defend", mode: "none", intensity: q(0) },
|
|
20
|
+
{ kind: "setProne", prone: true },
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
// tick down AI cooldowns
|
|
24
|
+
if (!self.ai)
|
|
25
|
+
self.ai = { focusTargetId: 0, retargetCooldownTicks: 0, decisionCooldownTicks: 0 };
|
|
26
|
+
if ((self.ai).decisionCooldownTicks === undefined)
|
|
27
|
+
(self.ai).decisionCooldownTicks = 0;
|
|
28
|
+
self.ai.retargetCooldownTicks = Math.max(0, self.ai.retargetCooldownTicks - 1);
|
|
29
|
+
self.ai.decisionCooldownTicks = Math.max(0, self.ai.decisionCooldownTicks - 1);
|
|
30
|
+
// Phase 4: decision latency — while cooling down, skip replanning and repeat current intent.
|
|
31
|
+
if (self.ai.decisionCooldownTicks > 0) {
|
|
32
|
+
// Emit the same defend and move as last tick (intent is already set from previous tick).
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
// Charge latency for next decision cycle
|
|
36
|
+
const perc = (self.attributes).perception ?? DEFAULT_PERCEPTION;
|
|
37
|
+
// Phase 7: tactics.hitTimingOffset_s reduces decision latency (max 50% reduction)
|
|
38
|
+
const tacticsSkill = getSkill(self.skills, "tactics");
|
|
39
|
+
const adjustedLatency_s = Math.max(Math.trunc(perc.decisionLatency_s / 2), perc.decisionLatency_s + tacticsSkill.hitTimingOffset_s);
|
|
40
|
+
// Phase 33: logicalMathematical reduces decision latency (faster tactical processing)
|
|
41
|
+
// Formula: mul = q(1.20) − logMath × q(0.40); human (0.60) → ×0.96; Vulcan (0.95) → ×0.82
|
|
42
|
+
const logMath = self.attributes.cognition?.logicalMathematical ?? 0;
|
|
43
|
+
const logLatencyMul = logMath
|
|
44
|
+
? clampQ((q(1.20) - Math.trunc(mulDiv(q(0.40), logMath, SCALE.Q))), q(0.50), q(1.20))
|
|
45
|
+
: SCALE.Q;
|
|
46
|
+
const scaledLatency_s = logMath ? mulDiv(adjustedLatency_s, logLatencyMul, SCALE.Q) : adjustedLatency_s;
|
|
47
|
+
const latencyTicks = Math.max(1, Math.trunc((scaledLatency_s * TICK_HZ) / SCALE.s));
|
|
48
|
+
self.ai.decisionCooldownTicks = latencyTicks;
|
|
49
|
+
// Phase 5: morale states — routing flees; hesitant suppresses attacks
|
|
50
|
+
const fearQ = (self.condition).fearQ ?? q(0);
|
|
51
|
+
const distressTol = self.attributes.resilience.distressTolerance;
|
|
52
|
+
const fearResp = (self.attributes.resilience).fearResponse ?? "flight";
|
|
53
|
+
// Phase 47: personality-driven overrides
|
|
54
|
+
const personality = self.personality;
|
|
55
|
+
// Feature 6: berserk entities never route or hesitate
|
|
56
|
+
// Phase 47: high-aggression entities (> q(0.70)) also override hesitation
|
|
57
|
+
const isHesitant = fearResp !== "berserk" &&
|
|
58
|
+
!isRouting(fearQ, distressTol) &&
|
|
59
|
+
fearQ >= qMul(moraleThreshold(distressTol), q(0.70)) &&
|
|
60
|
+
(!personality || personality.aggression < q(0.70));
|
|
61
|
+
if (fearResp !== "berserk" && isRouting(fearQ, distressTol)) {
|
|
62
|
+
// Feature 6: freeze archetype routes by freezing instead of fleeing
|
|
63
|
+
if (fearResp === "freeze") {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
// Feature 4: panic action variety — seeded surrender/freeze/flee roll
|
|
67
|
+
const panicSeed = eventSeed(world.seed, world.tick, self.id, 0, 0xFA115);
|
|
68
|
+
const surrenderChance = Math.trunc(qMul(q(0.10), (SCALE.Q - distressTol)));
|
|
69
|
+
const freezeChance = Math.trunc(qMul(q(0.15), (SCALE.Q - distressTol)));
|
|
70
|
+
const r = panicSeed % SCALE.Q;
|
|
71
|
+
if (r < surrenderChance) {
|
|
72
|
+
(self.condition).surrendered = true;
|
|
73
|
+
return [
|
|
74
|
+
{ kind: "defend", mode: "none", intensity: q(0) },
|
|
75
|
+
{ kind: "setProne", prone: true },
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
if (r < surrenderChance + freezeChance) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const nearestThreat = pickTarget(world, self, index, spatial, policy, env);
|
|
82
|
+
if (nearestThreat) {
|
|
83
|
+
const fdx = self.position_m.x - nearestThreat.position_m.x;
|
|
84
|
+
const fdy = self.position_m.y - nearestThreat.position_m.y;
|
|
85
|
+
return [
|
|
86
|
+
{ kind: "defend", mode: "none", intensity: q(0) },
|
|
87
|
+
{
|
|
88
|
+
kind: "move",
|
|
89
|
+
dir: normaliseDirCheapQ(v3(fdx !== 0 || fdy !== 0 ? fdx : 1, fdy, 0)),
|
|
90
|
+
intensity: q(1.0),
|
|
91
|
+
mode: "sprint",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
return [{ kind: "defend", mode: "none", intensity: q(0) }];
|
|
96
|
+
}
|
|
97
|
+
// Phase 3 extension: suppression response — go prone when sustained under fire (low distressTol only)
|
|
98
|
+
const suppressedTicks = (self.condition).suppressedTicks ?? 0;
|
|
99
|
+
if (suppressedTicks >= 3 && distressTol < q(0.50)) {
|
|
100
|
+
const suppCmds = [{ kind: "defend", mode: "none", intensity: q(0) }];
|
|
101
|
+
if (!self.condition.prone) {
|
|
102
|
+
suppCmds.push({ kind: "setProne", prone: true });
|
|
103
|
+
}
|
|
104
|
+
return suppCmds;
|
|
105
|
+
}
|
|
106
|
+
let target = pickTarget(world, self, index, spatial, policy, env);
|
|
107
|
+
// Phase 47: personality-driven target bias (loyalty before opportunism)
|
|
108
|
+
const effectiveLoyalty = computeEffectiveLoyalty(self, world);
|
|
109
|
+
target = applyLoyaltyBias(self, world, target, effectiveLoyalty);
|
|
110
|
+
if (personality) {
|
|
111
|
+
target = applyOpportunismBias(self, world, target, personality.opportunism);
|
|
112
|
+
}
|
|
113
|
+
// Phase 24: faction standing — suppress attack on friendly entities.
|
|
114
|
+
// Self-defence override: if self has taken damage (shock > 0 or fluid loss > 0),
|
|
115
|
+
// faction check is bypassed (attacker is fought back regardless of standing).
|
|
116
|
+
if (target && self.faction) {
|
|
117
|
+
const factionRegistry = (world).__factionRegistry;
|
|
118
|
+
if (factionRegistry) {
|
|
119
|
+
const standing = effectiveStanding(factionRegistry, self, target);
|
|
120
|
+
const selfDefence = self.injury.shock > 0 || self.injury.fluidLoss > 0;
|
|
121
|
+
if (!selfDefence && standing >= STANDING_FRIENDLY_THRESHOLD) {
|
|
122
|
+
target = undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
updateFocus(self, target, policy);
|
|
127
|
+
// Default defend
|
|
128
|
+
let defendMode = "none";
|
|
129
|
+
let defendIntensity = q(0);
|
|
130
|
+
if (target) {
|
|
131
|
+
const dx = target.position_m.x - self.position_m.x;
|
|
132
|
+
const dy = target.position_m.y - self.position_m.y;
|
|
133
|
+
const d2 = BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy);
|
|
134
|
+
const threatR = Math.max(1, policy.threatRange_m);
|
|
135
|
+
const threatD2 = BigInt(threatR) * BigInt(threatR);
|
|
136
|
+
if (d2 < threatD2) {
|
|
137
|
+
defendMode = pickDefenceModeDeterministic(policy);
|
|
138
|
+
// Phase 47: caution boosts/reduces defence intensity (±q(0.20) max at extremes)
|
|
139
|
+
defendIntensity = personality
|
|
140
|
+
? computeDefenceIntensityBoost(policy.defendWhenThreatenedQ, personality.caution)
|
|
141
|
+
: clampQ(policy.defendWhenThreatenedQ, q(0), q(1.0));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const cmds = [];
|
|
145
|
+
cmds.push({ kind: "defend", mode: defendMode, intensity: defendIntensity });
|
|
146
|
+
if (!target) {
|
|
147
|
+
// still emit a “no move”
|
|
148
|
+
cmds.push({ kind: "move", dir: v3(0, 0, 0), intensity: q(0), mode: "walk" });
|
|
149
|
+
return cmds;
|
|
150
|
+
}
|
|
151
|
+
// movement: try to maintain desired range
|
|
152
|
+
const dx = target.position_m.x - self.position_m.x;
|
|
153
|
+
const dy = target.position_m.y - self.position_m.y;
|
|
154
|
+
const distApprox = approxDist(dx, dy);
|
|
155
|
+
const want = policy.desiredRange_m;
|
|
156
|
+
const engage = policy.engageRange_m;
|
|
157
|
+
// Move toward if too far, back off if too close
|
|
158
|
+
// Phase 47: aggression shifts the effective retreat range (aggressive → less retreat)
|
|
159
|
+
const effectiveRetreatRange = personality
|
|
160
|
+
? computeEffectiveRetreatRange(policy.retreatRange_m, personality.aggression)
|
|
161
|
+
: policy.retreatRange_m;
|
|
162
|
+
let dirX = 0, dirY = 0;
|
|
163
|
+
if (distApprox > want) {
|
|
164
|
+
dirX = dx;
|
|
165
|
+
dirY = dy;
|
|
166
|
+
}
|
|
167
|
+
else if (distApprox < effectiveRetreatRange) {
|
|
168
|
+
dirX = -dx;
|
|
169
|
+
dirY = -dy;
|
|
170
|
+
}
|
|
171
|
+
let moveMode = distApprox > engage ? "sprint" : "run";
|
|
172
|
+
// Phase 6: cover-seeking — if exposed to enemies with no cover, move toward the best adjacent cell.
|
|
173
|
+
const aiCellSize = cellSize_m ?? Math.trunc(4 * SCALE.m);
|
|
174
|
+
const selfCoverQ = obstacleGrid
|
|
175
|
+
? coverFractionAtPosition(obstacleGrid, aiCellSize, self.position_m.x, self.position_m.y)
|
|
176
|
+
: 0;
|
|
177
|
+
const coverThreshold = suppressedTicks > 0 ? q(0.50) : q(0.30);
|
|
178
|
+
if (selfCoverQ < coverThreshold && !isRouting(fearQ, distressTol)) {
|
|
179
|
+
const enemyCount = world.entities.filter(en => en.teamId !== self.teamId && !en.injury.dead &&
|
|
180
|
+
approxDist(en.position_m.x - self.position_m.x, en.position_m.y - self.position_m.y) < Math.trunc(30 * SCALE.m)).length;
|
|
181
|
+
if (enemyCount > 0) {
|
|
182
|
+
const coverDir = findBestCoverDir(self, obstacleGrid, aiCellSize);
|
|
183
|
+
if (coverDir) {
|
|
184
|
+
dirX = coverDir.x;
|
|
185
|
+
dirY = coverDir.y;
|
|
186
|
+
moveMode = "run";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (dirX !== 0 || dirY !== 0) {
|
|
191
|
+
cmds.push({
|
|
192
|
+
kind: "move",
|
|
193
|
+
dir: normaliseDirCheapQ(v3(dirX, dirY, 0)),
|
|
194
|
+
intensity: q(1.0),
|
|
195
|
+
mode: moveMode,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
cmds.push({
|
|
200
|
+
kind: "move",
|
|
201
|
+
dir: v3(0, 0, 0),
|
|
202
|
+
intensity: q(0),
|
|
203
|
+
mode: "walk",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// attack when within engage range — hesitant or rallying entities hold back
|
|
207
|
+
const weapon = findWeapon(self.loadout, undefined);
|
|
208
|
+
const isRallying = ((self.condition).rallyCooldownTicks ?? 0) > 0;
|
|
209
|
+
if (weapon && !isHesitant && !isRallying) {
|
|
210
|
+
const reach = weapon.reach_m ?? Math.trunc(self.attributes.morphology.stature_m * 0.45);
|
|
211
|
+
if (distApprox <= reach + Math.trunc(0.25 * SCALE.m)) {
|
|
212
|
+
cmds.push({
|
|
213
|
+
kind: "attack",
|
|
214
|
+
targetId: target.id,
|
|
215
|
+
weaponId: weapon.id,
|
|
216
|
+
intensity: q(1.0),
|
|
217
|
+
mode: "strike",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return cmds;
|
|
222
|
+
}
|
|
223
|
+
function pickDefenceModeDeterministic(policy) {
|
|
224
|
+
// Deterministic selection (no RNG): allow dodge path.
|
|
225
|
+
// If dodge preference is strong, dodge. Else if parry preference strong, parry. Else block.
|
|
226
|
+
if (policy.dodgeBiasQ > policy.parryBiasQ && policy.dodgeBiasQ > q(0.50))
|
|
227
|
+
return "dodge";
|
|
228
|
+
if (policy.parryBiasQ > q(0.35))
|
|
229
|
+
return "parry";
|
|
230
|
+
return "block";
|
|
231
|
+
}
|
|
232
|
+
function approxDist(dx, dy) {
|
|
233
|
+
const adx = dx < 0 ? -dx : dx;
|
|
234
|
+
const ady = dy < 0 ? -dy : dy;
|
|
235
|
+
return adx > ady ? adx + (ady >> 1) : ady + (adx >> 1);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Scan the 8 adjacent cells and return a direction toward the one with the highest
|
|
239
|
+
* cover fraction that is better than the current cell (and not impassable).
|
|
240
|
+
* Returns undefined if already at the local cover maximum.
|
|
241
|
+
*/
|
|
242
|
+
function findBestCoverDir(self, grid, cellSize_m) {
|
|
243
|
+
if (!grid)
|
|
244
|
+
return undefined;
|
|
245
|
+
const cs = Math.max(1, cellSize_m);
|
|
246
|
+
const cx = Math.trunc(self.position_m.x / cs);
|
|
247
|
+
const cy = Math.trunc(self.position_m.y / cs);
|
|
248
|
+
const currentCover = grid.get(terrainKey(cx, cy)) ?? 0;
|
|
249
|
+
let bestCover = currentCover;
|
|
250
|
+
let bestDx = 0, bestDy = 0;
|
|
251
|
+
for (let ddx = -1; ddx <= 1; ddx++) {
|
|
252
|
+
for (let ddy = -1; ddy <= 1; ddy++) {
|
|
253
|
+
if (ddx === 0 && ddy === 0)
|
|
254
|
+
continue;
|
|
255
|
+
const frac = grid.get(terrainKey(cx + ddx, cy + ddy)) ?? 0;
|
|
256
|
+
// Prefer higher cover but skip impassable cells (q(1.0) = SCALE.Q)
|
|
257
|
+
if (frac > bestCover && frac < SCALE.Q) {
|
|
258
|
+
bestCover = frac;
|
|
259
|
+
bestDx = ddx;
|
|
260
|
+
bestDy = ddy;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (bestDx === 0 && bestDy === 0)
|
|
265
|
+
return undefined;
|
|
266
|
+
return { x: bestDx, y: bestDy, z: 0 };
|
|
267
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Entity } from "../entity.js";
|
|
2
|
+
import type { WorldState } from "../world.js";
|
|
3
|
+
import type { WorldIndex } from "../indexing.js";
|
|
4
|
+
import type { SpatialIndex } from "../spatial.js";
|
|
5
|
+
import { type SensoryEnvironment } from "../sensory.js";
|
|
6
|
+
export interface LocalPerception {
|
|
7
|
+
enemies: Entity[];
|
|
8
|
+
allies: Entity[];
|
|
9
|
+
}
|
|
10
|
+
/** @deprecated Use LocalPerception */
|
|
11
|
+
export type Perception = LocalPerception;
|
|
12
|
+
export declare function perceiveLocal(world: WorldState | undefined, self: Entity, index: WorldIndex, spatial: SpatialIndex, radius_m: number, maxCount?: number, env?: SensoryEnvironment): LocalPerception;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { queryNearbyIds } from "../spatial.js";
|
|
2
|
+
import { isEnemy, areEntitiesHostile } from "../team.js";
|
|
3
|
+
import { q } from "../../units.js";
|
|
4
|
+
import { canDetect, DEFAULT_PERCEPTION, DEFAULT_SENSORY_ENV } from "../sensory.js";
|
|
5
|
+
import { findSensor } from "../../equipment.js";
|
|
6
|
+
export function perceiveLocal(world, self, index, spatial, radius_m, maxCount = 24, env = DEFAULT_SENSORY_ENV) {
|
|
7
|
+
const perc = (self.attributes).perception ?? DEFAULT_PERCEPTION;
|
|
8
|
+
// Use the threat horizon as the spatial query radius if it is smaller than the requested radius.
|
|
9
|
+
const effectiveRadius = Math.min(radius_m, perc.threatHorizon_m);
|
|
10
|
+
const ids = queryNearbyIds(spatial, self.position_m, effectiveRadius);
|
|
11
|
+
ids.sort((a, b) => a - b);
|
|
12
|
+
const enemies = [];
|
|
13
|
+
const allies = [];
|
|
14
|
+
for (const id of ids) {
|
|
15
|
+
if (id === self.id)
|
|
16
|
+
continue;
|
|
17
|
+
const e = index.byId.get(id);
|
|
18
|
+
if (!e || e.injury.dead)
|
|
19
|
+
continue;
|
|
20
|
+
// Phase 4: filter by sensory detection
|
|
21
|
+
// Phase 11C: derive sensor boost from loadout
|
|
22
|
+
const sensor = findSensor(self.loadout);
|
|
23
|
+
const sensorBoost = sensor
|
|
24
|
+
? { visionRangeMul: sensor.visionRangeMul, hearingRangeMul: sensor.hearingRangeMul }
|
|
25
|
+
: undefined;
|
|
26
|
+
const detQ = canDetect(self, e, env, sensorBoost);
|
|
27
|
+
if (detQ <= q(0))
|
|
28
|
+
continue;
|
|
29
|
+
const hostile = world ? areEntitiesHostile(self, e, world) : isEnemy(self, e);
|
|
30
|
+
if (hostile)
|
|
31
|
+
enemies.push(e);
|
|
32
|
+
else
|
|
33
|
+
allies.push(e);
|
|
34
|
+
if (enemies.length + allies.length >= maxCount)
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
// deterministic order: distance² then id
|
|
38
|
+
const sortByDist = (a, b) => {
|
|
39
|
+
const dxA = a.position_m.x - self.position_m.x;
|
|
40
|
+
const dyA = a.position_m.y - self.position_m.y;
|
|
41
|
+
const d2A = BigInt(dxA) * BigInt(dxA) + BigInt(dyA) * BigInt(dyA);
|
|
42
|
+
const dxB = b.position_m.x - self.position_m.x;
|
|
43
|
+
const dyB = b.position_m.y - self.position_m.y;
|
|
44
|
+
const d2B = BigInt(dxB) * BigInt(dxB) + BigInt(dyB) * BigInt(dyB);
|
|
45
|
+
if (d2A < d2B)
|
|
46
|
+
return -1;
|
|
47
|
+
if (d2A > d2B)
|
|
48
|
+
return 1;
|
|
49
|
+
return a.id - b.id;
|
|
50
|
+
};
|
|
51
|
+
enemies.sort(sortByDist);
|
|
52
|
+
allies.sort(sortByDist);
|
|
53
|
+
return { enemies, allies };
|
|
54
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Q } from "../../units.js";
|
|
2
|
+
import type { Entity } from "../entity.js";
|
|
3
|
+
import type { WorldState } from "../world.js";
|
|
4
|
+
import type { IndividualAttributes, PersonalityTraits, PersonalityId } from "../../types.js";
|
|
5
|
+
export type { PersonalityTraits, PersonalityId };
|
|
6
|
+
/**
|
|
7
|
+
* Neutral personality: q(0.50) on all axes.
|
|
8
|
+
* Produces identical behaviour to an entity with no personality set.
|
|
9
|
+
*/
|
|
10
|
+
export declare const NEUTRAL_PERSONALITY: PersonalityTraits;
|
|
11
|
+
/** Named predefined personalities. */
|
|
12
|
+
export declare const PERSONALITIES: Record<PersonalityId, PersonalityTraits>;
|
|
13
|
+
/**
|
|
14
|
+
* Derive a personality from existing cognitive and resilience attributes.
|
|
15
|
+
*
|
|
16
|
+
* Mapping rationale:
|
|
17
|
+
* aggression ← distressTolerance (pain tolerance → willing to keep fighting)
|
|
18
|
+
* caution ← intrapersonal (self-awareness → more careful and defensive)
|
|
19
|
+
* loyalty ← interpersonal (social empathy → protects allies)
|
|
20
|
+
* opportunism ← logicalMathematical (planning → targets strategically)
|
|
21
|
+
*/
|
|
22
|
+
export declare function derivePersonalityFromCognition(attrs: IndividualAttributes): PersonalityTraits;
|
|
23
|
+
/**
|
|
24
|
+
* Compute effective loyalty combining personality loyalty and companion loyalty to party leader.
|
|
25
|
+
* If entity belongs to a party and has a relationship with the leader, companion loyalty is used.
|
|
26
|
+
* Otherwise falls back to personality loyalty (or neutral q(0.50) if no personality).
|
|
27
|
+
*/
|
|
28
|
+
export declare function computeEffectiveLoyalty(self: Entity, world: WorldState): Q;
|
|
29
|
+
/**
|
|
30
|
+
* Effective retreat range after aggression bias.
|
|
31
|
+
*
|
|
32
|
+
* aggression q(0.90) → range reduced by ~0.20m (fights more aggressively)
|
|
33
|
+
* aggression q(0.50) → unchanged
|
|
34
|
+
* aggression q(0.10) → range increased by ~0.20m (retreats sooner)
|
|
35
|
+
*/
|
|
36
|
+
export declare function computeEffectiveRetreatRange(baseRange_m: number, aggression: Q): number;
|
|
37
|
+
/**
|
|
38
|
+
* Effective defence intensity after caution bias.
|
|
39
|
+
*
|
|
40
|
+
* caution q(0.90) → +q(0.20) max boost
|
|
41
|
+
* caution q(0.50) → unchanged
|
|
42
|
+
* caution q(0.10) → −q(0.20) max reduction
|
|
43
|
+
*/
|
|
44
|
+
export declare function computeDefenceIntensityBoost(baseIntensity: Q, caution: Q): Q;
|
|
45
|
+
/**
|
|
46
|
+
* Loyalty override: if an ally is in distress and has an enemy nearby, switch target to
|
|
47
|
+
* that enemy. Only triggers when loyalty > q(0.50); roll probability = loyaltyQ / SCALE.Q.
|
|
48
|
+
*/
|
|
49
|
+
export declare function applyLoyaltyBias(self: Entity, world: WorldState, currentTarget: Entity | undefined, loyaltyQ: Q): Entity | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Opportunism override: if a significantly more-wounded enemy is present, switch target.
|
|
52
|
+
* Only triggers when opportunism > q(0.50); roll probability = opportunismQ / SCALE.Q.
|
|
53
|
+
*/
|
|
54
|
+
export declare function applyOpportunismBias(self: Entity, world: WorldState, currentTarget: Entity | undefined, opportunismQ: Q): Entity | undefined;
|