@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,598 @@
|
|
|
1
|
+
import { SCALE, q, clampQ, qMul, mulDiv, to } from "./units.js";
|
|
2
|
+
import { DamageChannel, channelMask } from "./channels.js";
|
|
3
|
+
import { ALL_REGIONS, DEFAULT_REGION_WEIGHTS, weightedMean01 } from "./sim/body.js";
|
|
4
|
+
export const DEFAULT_CARRY_RULES = {
|
|
5
|
+
capacityFactor: q(0.25),
|
|
6
|
+
bulkToMassFactor: q(0.06),
|
|
7
|
+
};
|
|
8
|
+
export function computeLoadoutTotals(loadout, armourIsWorn = true) {
|
|
9
|
+
let mass = 0;
|
|
10
|
+
let bulk = 0;
|
|
11
|
+
let wornMass = 0;
|
|
12
|
+
let wornBulk = 0;
|
|
13
|
+
for (const it of loadout.items) {
|
|
14
|
+
mass += it.mass_kg;
|
|
15
|
+
bulk = (bulk + it.bulk) | 0;
|
|
16
|
+
if (armourIsWorn && it.kind === "armour") {
|
|
17
|
+
wornMass += it.mass_kg;
|
|
18
|
+
wornBulk = (wornBulk + it.bulk) | 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
carriedMass_kg: mass,
|
|
23
|
+
carriedBulk: bulk,
|
|
24
|
+
wornMass_kg: wornMass,
|
|
25
|
+
wornBulk: wornBulk,
|
|
26
|
+
carriedMassFracOfBody: q(0),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function deriveCarryCapacityMass_kg(a, rules = DEFAULT_CARRY_RULES) {
|
|
30
|
+
const peakForceScaled = a.performance.peakForce_N;
|
|
31
|
+
const numerator = BigInt(peakForceScaled) * BigInt(SCALE.kg) * BigInt(rules.capacityFactor);
|
|
32
|
+
const denom = BigInt(SCALE.N) * BigInt(SCALE.Q) * 9810n;
|
|
33
|
+
const kgScaled = Number(numerator / denom);
|
|
34
|
+
return Math.max(1, kgScaled);
|
|
35
|
+
}
|
|
36
|
+
export function computeEncumbrance(a, loadout, rules = DEFAULT_CARRY_RULES) {
|
|
37
|
+
const totals = computeLoadoutTotals(loadout);
|
|
38
|
+
const bodyMass = Math.max(1, a.morphology.mass_kg);
|
|
39
|
+
totals.carriedMassFracOfBody = mulDiv(totals.carriedMass_kg * SCALE.Q, SCALE.kg, bodyMass);
|
|
40
|
+
const capacity_kg = Math.max(1, deriveCarryCapacityMass_kg(a, rules));
|
|
41
|
+
const massRatio = mulDiv(totals.carriedMass_kg * SCALE.Q, 1, capacity_kg);
|
|
42
|
+
const bulkAbove1 = Math.max(0, totals.carriedBulk - SCALE.Q);
|
|
43
|
+
const bulkTerm = qMul(bulkAbove1, rules.bulkToMassFactor);
|
|
44
|
+
const r = clampQ((massRatio + bulkTerm), 0, 5 * SCALE.Q);
|
|
45
|
+
const penalties = encumbranceCurve(r, a);
|
|
46
|
+
return { totals, penalties };
|
|
47
|
+
}
|
|
48
|
+
function encumbranceCurve(r, a) {
|
|
49
|
+
const overloaded = r > q(1.5);
|
|
50
|
+
const speedMul = piecewiseMul(r, q(1.0), q(0.92), q(0.78), q(0.55));
|
|
51
|
+
const accelMul = piecewiseMul(r, q(1.0), q(0.88), q(0.70), q(0.45));
|
|
52
|
+
const jumpMul = piecewiseMul(r, q(1.0), q(0.90), q(0.68), q(0.40));
|
|
53
|
+
const baseDemand = piecewiseMul(r, q(1.0), q(1.10), q(1.30), q(1.65));
|
|
54
|
+
const energyDemandMul = clampQ(qMul(baseDemand, a.resilience.fatigueRate), q(0.5), q(3.0));
|
|
55
|
+
const controlMul = piecewiseMul(r, q(1.0), q(0.96), q(0.88), q(0.75));
|
|
56
|
+
const stabilityMul = piecewiseMul(r, q(1.0), q(0.94), q(0.82), q(0.65));
|
|
57
|
+
return { speedMul, accelMul, jumpMul, energyDemandMul, controlMul, stabilityMul, encumbranceRatio: r, overloaded };
|
|
58
|
+
}
|
|
59
|
+
function piecewiseMul(r, a, b, c, d) {
|
|
60
|
+
const r05 = q(0.5), r10 = q(1.0), r15 = q(1.5);
|
|
61
|
+
if (r <= r05)
|
|
62
|
+
return a;
|
|
63
|
+
if (r <= r10) {
|
|
64
|
+
const t = mulDiv((r - r05), SCALE.Q, (r10 - r05));
|
|
65
|
+
return (a + mulDiv((b - a), t, SCALE.Q));
|
|
66
|
+
}
|
|
67
|
+
if (r <= r15) {
|
|
68
|
+
const t = mulDiv((r - r10), SCALE.Q, (r15 - r10));
|
|
69
|
+
return (b + mulDiv((c - b), t, SCALE.Q));
|
|
70
|
+
}
|
|
71
|
+
return d;
|
|
72
|
+
}
|
|
73
|
+
function emptyCoverage() {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const r of ALL_REGIONS)
|
|
76
|
+
out[r] = q(0);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
export function deriveArmourProfile(loadout, armourState) {
|
|
80
|
+
const items = [...loadout.items].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
81
|
+
let protects = 0;
|
|
82
|
+
let protectedMul = q(1.0);
|
|
83
|
+
let mobilityMul = q(1.0);
|
|
84
|
+
let fatigueMul = q(1.0);
|
|
85
|
+
const coverageByRegion = emptyCoverage();
|
|
86
|
+
let resist_J = 0;
|
|
87
|
+
let reflectivity = q(0);
|
|
88
|
+
const channelResistMul = {};
|
|
89
|
+
for (const it of items) {
|
|
90
|
+
if (it.kind !== "armour")
|
|
91
|
+
continue;
|
|
92
|
+
protects |= it.protects;
|
|
93
|
+
protectedMul = qMul(protectedMul, it.protectedDamageMul);
|
|
94
|
+
mobilityMul = qMul(mobilityMul, it.mobilityMul ?? q(1.0));
|
|
95
|
+
fatigueMul = qMul(fatigueMul, it.fatigueMul ?? q(1.0));
|
|
96
|
+
for (const r of ALL_REGIONS) {
|
|
97
|
+
const c = it.coverageByRegion[r] ?? q(0);
|
|
98
|
+
const oneMinus = q(1.0) - c;
|
|
99
|
+
coverageByRegion[r] = (q(1.0) - qMul(q(1.0) - coverageByRegion[r], oneMinus));
|
|
100
|
+
}
|
|
101
|
+
// Phase 11C: ablative — use remaining resist if tracked, else full
|
|
102
|
+
const effectiveResist = (it.ablative && armourState?.has(it.id))
|
|
103
|
+
? armourState.get(it.id).resistRemaining_J
|
|
104
|
+
: it.resist_J;
|
|
105
|
+
resist_J += effectiveResist;
|
|
106
|
+
// Phase 11C: reflectivity — take the maximum across all items
|
|
107
|
+
if (it.reflectivity && it.reflectivity > reflectivity) {
|
|
108
|
+
reflectivity = it.reflectivity;
|
|
109
|
+
}
|
|
110
|
+
if (it.channelResistMul) {
|
|
111
|
+
for (const k of Object.keys(it.channelResistMul)) {
|
|
112
|
+
const ch = Number(k);
|
|
113
|
+
const mul = it.channelResistMul[ch];
|
|
114
|
+
channelResistMul[ch] = channelResistMul[ch] ? qMul(channelResistMul[ch], mul) : mul;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
protects,
|
|
120
|
+
coverageByRegion: coverageByRegion,
|
|
121
|
+
coverageOverall: weightedMean01(coverageByRegion, DEFAULT_REGION_WEIGHTS),
|
|
122
|
+
resist_J: Math.max(0, resist_J),
|
|
123
|
+
protectedDamageMul: clampQ(protectedMul, q(0.05), q(1.0)),
|
|
124
|
+
mobilityMul: clampQ(mobilityMul, q(0.30), q(1.0)),
|
|
125
|
+
fatigueMul: clampQ(fatigueMul, q(0.80), q(3.0)),
|
|
126
|
+
channelResistMul,
|
|
127
|
+
reflectivity,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function findWeapon(loadout, weaponId) {
|
|
131
|
+
const weapons = loadout.items
|
|
132
|
+
.filter((x) => x.kind === "weapon");
|
|
133
|
+
if (weapons.length === 0)
|
|
134
|
+
return null;
|
|
135
|
+
if (!weaponId)
|
|
136
|
+
return weapons[0];
|
|
137
|
+
return weapons.find(w => w.id === weaponId) ?? weapons[0];
|
|
138
|
+
}
|
|
139
|
+
export function findRangedWeapon(loadout, weaponId) {
|
|
140
|
+
const ranged = loadout.items.filter((x) => x.kind === "ranged");
|
|
141
|
+
if (ranged.length === 0)
|
|
142
|
+
return null;
|
|
143
|
+
if (!weaponId)
|
|
144
|
+
return ranged[0];
|
|
145
|
+
return ranged.find(w => w.id === weaponId) ?? ranged[0];
|
|
146
|
+
}
|
|
147
|
+
export function findShield(loadout) {
|
|
148
|
+
return loadout.items.find(item => item?.kind === "shield");
|
|
149
|
+
}
|
|
150
|
+
export function findExoskeleton(loadout) {
|
|
151
|
+
return (loadout.items.find((it) => it.kind === "exoskeleton") ?? null);
|
|
152
|
+
}
|
|
153
|
+
/** Phase 11C: return the first Sensor in the loadout, or null. */
|
|
154
|
+
export function findSensor(loadout) {
|
|
155
|
+
return (loadout.items.find((it) => it.kind === "sensor") ?? null);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Phase 11: check that every item in a loadout is usable in the given TechContext.
|
|
159
|
+
* Returns an array of error messages; empty array means the loadout is valid.
|
|
160
|
+
*/
|
|
161
|
+
export function validateLoadout(loadout, ctx) {
|
|
162
|
+
const errors = [];
|
|
163
|
+
for (const item of loadout.items) {
|
|
164
|
+
if (!item.requiredCapabilities)
|
|
165
|
+
continue;
|
|
166
|
+
for (const cap of item.requiredCapabilities) {
|
|
167
|
+
if (!ctx.available.has(cap)) {
|
|
168
|
+
errors.push(`"${item.id}" requires capability "${cap}"`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return errors;
|
|
173
|
+
}
|
|
174
|
+
export function deriveWeaponHandling(w, ownerStature_m) {
|
|
175
|
+
const reach = w.reach_m ?? Math.trunc(ownerStature_m * 0.45);
|
|
176
|
+
return {
|
|
177
|
+
handedness: w.handedness ?? "oneHand",
|
|
178
|
+
momentArm_m: w.momentArm_m ?? Math.trunc(reach * 0.55),
|
|
179
|
+
handlingLoadMul: w.handlingLoadMul ?? q(1.0),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export const STARTER_WEAPONS = [
|
|
183
|
+
{
|
|
184
|
+
id: "wpn_club",
|
|
185
|
+
kind: "weapon",
|
|
186
|
+
name: "Wooden club",
|
|
187
|
+
mass_kg: Math.round(1.2 * SCALE.kg),
|
|
188
|
+
bulk: q(1.4),
|
|
189
|
+
reach_m: Math.round(0.7 * SCALE.m),
|
|
190
|
+
handedness: "oneHand",
|
|
191
|
+
momentArm_m: Math.round(0.45 * SCALE.m),
|
|
192
|
+
handlingMul: q(1.10),
|
|
193
|
+
strikeEffectiveMassFrac: q(0.18),
|
|
194
|
+
strikeSpeedMul: q(0.95),
|
|
195
|
+
damage: {
|
|
196
|
+
surfaceFrac: q(0.35),
|
|
197
|
+
internalFrac: q(0.20),
|
|
198
|
+
structuralFrac: q(0.45),
|
|
199
|
+
bleedFactor: q(0.25),
|
|
200
|
+
penetrationBias: q(0.10),
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "wpn_knife",
|
|
205
|
+
kind: "weapon",
|
|
206
|
+
name: "Knife",
|
|
207
|
+
mass_kg: Math.round(0.3 * SCALE.kg),
|
|
208
|
+
bulk: q(1.1),
|
|
209
|
+
reach_m: Math.round(0.2 * SCALE.m),
|
|
210
|
+
handedness: "oneHand",
|
|
211
|
+
momentArm_m: Math.round(0.18 * SCALE.m),
|
|
212
|
+
handlingMul: q(0.85),
|
|
213
|
+
strikeEffectiveMassFrac: q(0.10),
|
|
214
|
+
strikeSpeedMul: q(1.05),
|
|
215
|
+
damage: {
|
|
216
|
+
surfaceFrac: q(0.30),
|
|
217
|
+
internalFrac: q(0.60),
|
|
218
|
+
structuralFrac: q(0.10),
|
|
219
|
+
bleedFactor: q(0.95),
|
|
220
|
+
penetrationBias: q(0.85),
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: "wpn_longsword",
|
|
225
|
+
kind: "weapon",
|
|
226
|
+
name: "Longsword",
|
|
227
|
+
mass_kg: Math.round(1.5 * SCALE.kg),
|
|
228
|
+
bulk: q(1.5),
|
|
229
|
+
reach_m: Math.round(0.90 * SCALE.m),
|
|
230
|
+
handedness: "twoHand",
|
|
231
|
+
momentArm_m: Math.round(0.55 * SCALE.m),
|
|
232
|
+
handlingMul: q(1.05),
|
|
233
|
+
strikeEffectiveMassFrac: q(0.15),
|
|
234
|
+
strikeSpeedMul: q(1.00),
|
|
235
|
+
readyTime_s: to.s(0.75),
|
|
236
|
+
damage: {
|
|
237
|
+
surfaceFrac: q(0.35),
|
|
238
|
+
internalFrac: q(0.45),
|
|
239
|
+
structuralFrac: q(0.20),
|
|
240
|
+
bleedFactor: q(0.70),
|
|
241
|
+
penetrationBias: q(0.40),
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
// Phase 15: boxing gloves — concussive, minimal cutting, fast punches
|
|
245
|
+
{
|
|
246
|
+
id: "wpn_boxing_gloves",
|
|
247
|
+
kind: "weapon",
|
|
248
|
+
name: "Boxing Gloves",
|
|
249
|
+
mass_kg: Math.round(0.28 * SCALE.kg), // 10 oz ≈ 0.28 kg
|
|
250
|
+
bulk: q(0.9),
|
|
251
|
+
reach_m: Math.trunc(0.32 * SCALE.m), // 0.32 m effective reach
|
|
252
|
+
handedness: "oneHand",
|
|
253
|
+
momentArm_m: Math.trunc(0.22 * SCALE.m),
|
|
254
|
+
handlingMul: q(0.95),
|
|
255
|
+
strikeEffectiveMassFrac: q(0.07), // padding distributes force over larger area
|
|
256
|
+
strikeSpeedMul: q(1.20), // fast punches
|
|
257
|
+
damage: {
|
|
258
|
+
surfaceFrac: q(0.10), // padding minimises cuts
|
|
259
|
+
internalFrac: q(0.60), // concussive — brain/organ shock dominates
|
|
260
|
+
structuralFrac: q(0.08),
|
|
261
|
+
bleedFactor: q(0.04), // almost no bleeding
|
|
262
|
+
penetrationBias: q(0.03),
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
// Phase 11: powered exoskeletons
|
|
267
|
+
export const STARTER_EXOSKELETONS = [
|
|
268
|
+
{
|
|
269
|
+
id: "exo_combat",
|
|
270
|
+
kind: "exoskeleton",
|
|
271
|
+
name: "Combat exoskeleton",
|
|
272
|
+
mass_kg: Math.round(25.0 * SCALE.kg),
|
|
273
|
+
bulk: q(2.5),
|
|
274
|
+
requiredCapabilities: ["PoweredExoskeleton"],
|
|
275
|
+
speedMultiplier: q(1.25), // +25% effective sprint speed
|
|
276
|
+
forceMultiplier: q(1.40), // +40% melee strike energy
|
|
277
|
+
powerDrain_W: 200, // equivalent to a second continuous-power budget
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: "exo_heavy",
|
|
281
|
+
kind: "exoskeleton",
|
|
282
|
+
name: "Heavy assault exoskeleton",
|
|
283
|
+
mass_kg: Math.round(45.0 * SCALE.kg),
|
|
284
|
+
bulk: q(3.0),
|
|
285
|
+
requiredCapabilities: ["PoweredExoskeleton"],
|
|
286
|
+
speedMultiplier: q(1.10), // heavy — modest speed gain
|
|
287
|
+
forceMultiplier: q(1.80), // massive force amplification
|
|
288
|
+
powerDrain_W: 400,
|
|
289
|
+
},
|
|
290
|
+
];
|
|
291
|
+
export const STARTER_ARMOUR = [
|
|
292
|
+
{
|
|
293
|
+
id: "arm_leather",
|
|
294
|
+
kind: "armour",
|
|
295
|
+
name: "Leather armour",
|
|
296
|
+
mass_kg: Math.round(6.0 * SCALE.kg),
|
|
297
|
+
bulk: q(1.6),
|
|
298
|
+
protects: channelMask(DamageChannel.Kinetic, DamageChannel.Thermal),
|
|
299
|
+
coverageByRegion: {
|
|
300
|
+
head: q(0.10),
|
|
301
|
+
torso: q(0.70),
|
|
302
|
+
leftArm: q(0.45),
|
|
303
|
+
rightArm: q(0.45),
|
|
304
|
+
leftLeg: q(0.25),
|
|
305
|
+
rightLeg: q(0.25),
|
|
306
|
+
},
|
|
307
|
+
resist_J: 150,
|
|
308
|
+
protectedDamageMul: q(0.85),
|
|
309
|
+
channelResistMul: { [DamageChannel.Thermal]: q(1.10) },
|
|
310
|
+
mobilityMul: q(0.95),
|
|
311
|
+
fatigueMul: q(1.08),
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: "arm_mail",
|
|
315
|
+
kind: "armour",
|
|
316
|
+
name: "Mail armour",
|
|
317
|
+
mass_kg: Math.round(10.0 * SCALE.kg),
|
|
318
|
+
bulk: q(1.9),
|
|
319
|
+
requiredCapabilities: ["MetallicArmour"],
|
|
320
|
+
protects: channelMask(DamageChannel.Kinetic),
|
|
321
|
+
coverageByRegion: {
|
|
322
|
+
head: q(0.05),
|
|
323
|
+
torso: q(0.78),
|
|
324
|
+
leftArm: q(0.55),
|
|
325
|
+
rightArm: q(0.55),
|
|
326
|
+
leftLeg: q(0.20),
|
|
327
|
+
rightLeg: q(0.20),
|
|
328
|
+
},
|
|
329
|
+
resist_J: 350,
|
|
330
|
+
protectedDamageMul: q(0.75),
|
|
331
|
+
mobilityMul: q(0.90),
|
|
332
|
+
fatigueMul: q(1.15),
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
id: "arm_plate",
|
|
336
|
+
kind: "armour",
|
|
337
|
+
name: "Plate armour",
|
|
338
|
+
mass_kg: Math.round(20.0 * SCALE.kg),
|
|
339
|
+
bulk: q(2.2),
|
|
340
|
+
requiredCapabilities: ["MetallicArmour"],
|
|
341
|
+
protects: channelMask(DamageChannel.Kinetic, DamageChannel.Thermal),
|
|
342
|
+
coverageByRegion: {
|
|
343
|
+
head: q(0.75),
|
|
344
|
+
torso: q(0.90),
|
|
345
|
+
leftArm: q(0.80),
|
|
346
|
+
rightArm: q(0.80),
|
|
347
|
+
leftLeg: q(0.70),
|
|
348
|
+
rightLeg: q(0.70),
|
|
349
|
+
},
|
|
350
|
+
resist_J: 800,
|
|
351
|
+
protectedDamageMul: q(0.60),
|
|
352
|
+
mobilityMul: q(0.82),
|
|
353
|
+
fatigueMul: q(1.25),
|
|
354
|
+
},
|
|
355
|
+
];
|
|
356
|
+
export const STARTER_SHIELDS = [
|
|
357
|
+
{
|
|
358
|
+
id: "shd_small",
|
|
359
|
+
kind: "shield",
|
|
360
|
+
name: "Small shield",
|
|
361
|
+
mass_kg: Math.round(3.0 * SCALE.kg),
|
|
362
|
+
bulk: q(1.2),
|
|
363
|
+
coverageQ: q(0.65),
|
|
364
|
+
blockResist_J: 120,
|
|
365
|
+
deflectQ: q(0.30),
|
|
366
|
+
arcDeg: 90,
|
|
367
|
+
regions: ["torso", "leftArm", "rightArm"],
|
|
368
|
+
covers: ["torso", "arm", "head"],
|
|
369
|
+
coverageProfileId: "shield_small_default",
|
|
370
|
+
manipulationMul: q(0.95),
|
|
371
|
+
mobilityMul: q(0.98),
|
|
372
|
+
fatigueMul: q(1.05),
|
|
373
|
+
},
|
|
374
|
+
];
|
|
375
|
+
// Phase 3: starter ranged weapons
|
|
376
|
+
// damage profile: projectiles are penetrating; surface fraction is low.
|
|
377
|
+
const PROJECTILE_DAMAGE = {
|
|
378
|
+
surfaceFrac: q(0.20),
|
|
379
|
+
internalFrac: q(0.55),
|
|
380
|
+
structuralFrac: q(0.25),
|
|
381
|
+
bleedFactor: q(0.75),
|
|
382
|
+
penetrationBias: q(0.70),
|
|
383
|
+
};
|
|
384
|
+
export const STARTER_RANGED_WEAPONS = [
|
|
385
|
+
{
|
|
386
|
+
id: "rng_sling",
|
|
387
|
+
kind: "ranged",
|
|
388
|
+
name: "Sling",
|
|
389
|
+
category: "thrown",
|
|
390
|
+
mass_kg: Math.round(0.1 * SCALE.kg),
|
|
391
|
+
bulk: q(0.8),
|
|
392
|
+
launchEnergy_J: 0, // derived from thrower peakPower_W
|
|
393
|
+
projectileMass_kg: Math.round(0.08 * SCALE.kg),
|
|
394
|
+
dragCoeff_perM: q(0.012), // 1.2% energy loss per metre
|
|
395
|
+
dispersionQ: q(0.012), // 12 mrad base
|
|
396
|
+
recycleTime_s: to.s(2.0),
|
|
397
|
+
damage: PROJECTILE_DAMAGE,
|
|
398
|
+
suppressionFearMul: q(0.5),
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: "rng_shortbow",
|
|
402
|
+
kind: "ranged",
|
|
403
|
+
name: "Short bow",
|
|
404
|
+
category: "bow",
|
|
405
|
+
mass_kg: Math.round(0.8 * SCALE.kg),
|
|
406
|
+
bulk: q(1.3),
|
|
407
|
+
launchEnergy_J: 60,
|
|
408
|
+
projectileMass_kg: Math.round(0.025 * SCALE.kg),
|
|
409
|
+
dragCoeff_perM: q(0.007), // 0.7% loss/m
|
|
410
|
+
dispersionQ: q(0.012),
|
|
411
|
+
recycleTime_s: to.s(1.5),
|
|
412
|
+
damage: PROJECTILE_DAMAGE,
|
|
413
|
+
suppressionFearMul: q(1.0),
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: "rng_longbow",
|
|
417
|
+
kind: "ranged",
|
|
418
|
+
name: "Long bow",
|
|
419
|
+
category: "bow",
|
|
420
|
+
mass_kg: Math.round(1.2 * SCALE.kg),
|
|
421
|
+
bulk: q(1.6),
|
|
422
|
+
launchEnergy_J: 90,
|
|
423
|
+
projectileMass_kg: Math.round(0.025 * SCALE.kg),
|
|
424
|
+
dragCoeff_perM: q(0.005), // 0.5% loss/m
|
|
425
|
+
dispersionQ: q(0.008),
|
|
426
|
+
recycleTime_s: to.s(2.0),
|
|
427
|
+
damage: PROJECTILE_DAMAGE,
|
|
428
|
+
suppressionFearMul: q(1.5),
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: "rng_crossbow",
|
|
432
|
+
kind: "ranged",
|
|
433
|
+
name: "Crossbow",
|
|
434
|
+
category: "bow",
|
|
435
|
+
mass_kg: Math.round(3.5 * SCALE.kg),
|
|
436
|
+
bulk: q(2.0),
|
|
437
|
+
launchEnergy_J: 120,
|
|
438
|
+
projectileMass_kg: Math.round(0.040 * SCALE.kg),
|
|
439
|
+
dragCoeff_perM: q(0.004),
|
|
440
|
+
dispersionQ: q(0.006),
|
|
441
|
+
recycleTime_s: to.s(5.0),
|
|
442
|
+
damage: PROJECTILE_DAMAGE,
|
|
443
|
+
suppressionFearMul: q(1.5),
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: "rng_pistol",
|
|
447
|
+
kind: "ranged",
|
|
448
|
+
name: "Pistol",
|
|
449
|
+
category: "firearm",
|
|
450
|
+
mass_kg: Math.round(1.2 * SCALE.kg),
|
|
451
|
+
bulk: q(1.1),
|
|
452
|
+
requiredCapabilities: ["FirearmsPropellant"],
|
|
453
|
+
launchEnergy_J: 400,
|
|
454
|
+
projectileMass_kg: Math.round(0.015 * SCALE.kg),
|
|
455
|
+
dragCoeff_perM: q(0.002),
|
|
456
|
+
dispersionQ: q(0.015),
|
|
457
|
+
recycleTime_s: to.s(12.0),
|
|
458
|
+
damage: PROJECTILE_DAMAGE,
|
|
459
|
+
suppressionFearMul: q(2.0),
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
id: "rng_musket",
|
|
463
|
+
kind: "ranged",
|
|
464
|
+
name: "Musket",
|
|
465
|
+
category: "firearm",
|
|
466
|
+
mass_kg: Math.round(4.5 * SCALE.kg),
|
|
467
|
+
bulk: q(2.2),
|
|
468
|
+
requiredCapabilities: ["FirearmsPropellant"],
|
|
469
|
+
launchEnergy_J: 600,
|
|
470
|
+
projectileMass_kg: Math.round(0.030 * SCALE.kg),
|
|
471
|
+
dragCoeff_perM: q(0.0015), // 0.15% loss/m
|
|
472
|
+
dispersionQ: q(0.010),
|
|
473
|
+
recycleTime_s: to.s(18.0),
|
|
474
|
+
damage: PROJECTILE_DAMAGE,
|
|
475
|
+
suppressionFearMul: q(3.0),
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
id: "rng_plasma_rifle",
|
|
479
|
+
kind: "ranged",
|
|
480
|
+
name: "Plasma rifle",
|
|
481
|
+
category: "firearm",
|
|
482
|
+
mass_kg: Math.round(3.8 * SCALE.kg),
|
|
483
|
+
bulk: q(1.8),
|
|
484
|
+
requiredCapabilities: ["EnergyWeapons"],
|
|
485
|
+
launchEnergy_J: 2000,
|
|
486
|
+
projectileMass_kg: Math.round(0.001 * SCALE.kg),
|
|
487
|
+
dragCoeff_perM: q(0.0005), // near-negligible beam divergence
|
|
488
|
+
dispersionQ: q(0.004),
|
|
489
|
+
recycleTime_s: to.s(2.0),
|
|
490
|
+
energyType: "plasma", // Phase 11C: Energy channel; resisted by reflectivity
|
|
491
|
+
suppressionFearMul: q(1.0),
|
|
492
|
+
damage: {
|
|
493
|
+
surfaceFrac: q(0.35),
|
|
494
|
+
internalFrac: q(0.45),
|
|
495
|
+
structuralFrac: q(0.20),
|
|
496
|
+
bleedFactor: q(0.15), // plasma cauterises, low bleed
|
|
497
|
+
penetrationBias: q(0.90),
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
];
|
|
501
|
+
// ── Phase 11C starter items ────────────────────────────────────────────────────
|
|
502
|
+
/** Reflective/ablative armour items for energy and kinetic threats. */
|
|
503
|
+
export const STARTER_ARMOUR_11C = [
|
|
504
|
+
{
|
|
505
|
+
id: "arm_reflective",
|
|
506
|
+
kind: "armour",
|
|
507
|
+
name: "Reflective coating",
|
|
508
|
+
mass_kg: Math.round(0.5 * SCALE.kg),
|
|
509
|
+
bulk: q(0.3),
|
|
510
|
+
requiredCapabilities: ["EnergyWeapons"],
|
|
511
|
+
protects: channelMask(DamageChannel.Energy),
|
|
512
|
+
coverageByRegion: {
|
|
513
|
+
head: q(0.50), torso: q(0.80),
|
|
514
|
+
leftArm: q(0.50), rightArm: q(0.50),
|
|
515
|
+
leftLeg: q(0.30), rightLeg: q(0.30),
|
|
516
|
+
},
|
|
517
|
+
resist_J: 0,
|
|
518
|
+
protectedDamageMul: q(1.0),
|
|
519
|
+
reflectivity: q(0.40), // deflects 40% of energy weapon damage
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
id: "arm_reactive",
|
|
523
|
+
kind: "armour",
|
|
524
|
+
name: "Reactive plating",
|
|
525
|
+
mass_kg: Math.round(3.0 * SCALE.kg),
|
|
526
|
+
bulk: q(1.5),
|
|
527
|
+
requiredCapabilities: ["ReactivePlating"],
|
|
528
|
+
protects: channelMask(DamageChannel.Kinetic),
|
|
529
|
+
coverageByRegion: {
|
|
530
|
+
head: q(0.40), torso: q(0.85),
|
|
531
|
+
leftArm: q(0.40), rightArm: q(0.40),
|
|
532
|
+
leftLeg: q(0.20), rightLeg: q(0.20),
|
|
533
|
+
},
|
|
534
|
+
resist_J: 1500,
|
|
535
|
+
protectedDamageMul: q(0.65),
|
|
536
|
+
ablative: true, // degrades with use; tracked in entity.armourState
|
|
537
|
+
},
|
|
538
|
+
];
|
|
539
|
+
/** Phase 11C: sensor suites that boost vision and hearing range. */
|
|
540
|
+
export const STARTER_SENSORS = [
|
|
541
|
+
{
|
|
542
|
+
id: "sens_nightvision",
|
|
543
|
+
kind: "sensor",
|
|
544
|
+
name: "Night-vision goggles",
|
|
545
|
+
mass_kg: Math.round(0.3 * SCALE.kg),
|
|
546
|
+
bulk: q(1.1),
|
|
547
|
+
requiredCapabilities: ["BallisticArmour"],
|
|
548
|
+
visionRangeMul: q(1.5), // +50% vision range
|
|
549
|
+
hearingRangeMul: q(1.0),
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: "sens_tactical",
|
|
553
|
+
kind: "sensor",
|
|
554
|
+
name: "Tactical sensor suite",
|
|
555
|
+
mass_kg: Math.round(0.8 * SCALE.kg),
|
|
556
|
+
bulk: q(1.3),
|
|
557
|
+
requiredCapabilities: ["PoweredExoskeleton"],
|
|
558
|
+
visionRangeMul: q(2.0), // double vision range
|
|
559
|
+
hearingRangeMul: q(1.5), // +50% hearing range
|
|
560
|
+
},
|
|
561
|
+
];
|
|
562
|
+
// ── Phase 3 extension: starter ammo types ─────────────────────────────────────
|
|
563
|
+
/** Armour-piercing projectile damage profile: increased penetration, lower energy. */
|
|
564
|
+
const AP_DAMAGE = {
|
|
565
|
+
surfaceFrac: q(0.10),
|
|
566
|
+
internalFrac: q(0.60),
|
|
567
|
+
structuralFrac: q(0.30),
|
|
568
|
+
bleedFactor: q(0.50),
|
|
569
|
+
penetrationBias: q(0.95),
|
|
570
|
+
};
|
|
571
|
+
/** Hollow-point projectile damage profile: maximum bleeding, lower penetration. */
|
|
572
|
+
const HP_DAMAGE = {
|
|
573
|
+
surfaceFrac: q(0.40),
|
|
574
|
+
internalFrac: q(0.55),
|
|
575
|
+
structuralFrac: q(0.05),
|
|
576
|
+
bleedFactor: q(0.95),
|
|
577
|
+
penetrationBias: q(0.20),
|
|
578
|
+
};
|
|
579
|
+
export const STARTER_AMMO = [
|
|
580
|
+
{
|
|
581
|
+
id: "ammo_ap",
|
|
582
|
+
name: "Armour-Piercing",
|
|
583
|
+
damage: AP_DAMAGE,
|
|
584
|
+
launchEnergyMul: q(0.90), // slightly lower velocity than ball
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
id: "ammo_hv",
|
|
588
|
+
name: "High-Velocity",
|
|
589
|
+
launchEnergyMul: q(1.20), // +20% muzzle energy
|
|
590
|
+
dragCoeff_perM: q(0.002), // streamlined projectile
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
id: "ammo_hollow",
|
|
594
|
+
name: "Hollow-Point",
|
|
595
|
+
damage: HP_DAMAGE,
|
|
596
|
+
launchEnergyMul: q(0.95), // slightly heavier
|
|
597
|
+
},
|
|
598
|
+
];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Q } from "./units.js";
|
|
2
|
+
import type { Entity } from "./sim/entity.js";
|
|
3
|
+
import type { WorldState } from "./sim/world.js";
|
|
4
|
+
import type { TraceEvent } from "./sim/trace.js";
|
|
5
|
+
/** A named group of entities with defined relationships to other factions. */
|
|
6
|
+
export interface Faction {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
/** Faction ids with default hostile standing (q(0.20)). */
|
|
10
|
+
rivals: Set<string>;
|
|
11
|
+
/** Faction ids with default friendly standing (q(0.70)). */
|
|
12
|
+
allies: Set<string>;
|
|
13
|
+
}
|
|
14
|
+
/** A reputation-relevant event witnessed by a faction member. */
|
|
15
|
+
export interface WitnessEvent {
|
|
16
|
+
actorId: number;
|
|
17
|
+
eventType: "kill" | "assault" | "theft" | "aid" | "surrender";
|
|
18
|
+
targetId: number;
|
|
19
|
+
/** Faction that cares about this event (typically the target's faction). */
|
|
20
|
+
factionId: string;
|
|
21
|
+
/** Signed reputation delta for the actor within `factionId`. */
|
|
22
|
+
delta: Q;
|
|
23
|
+
tick: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Persistent faction state for a scenario or campaign.
|
|
27
|
+
*
|
|
28
|
+
* `globalStanding` — faction-to-faction base standing (initialised from rival/ally sets).
|
|
29
|
+
* `entityReputations` — entity-level standing within factions; updated by `applyWitnessEvent`.
|
|
30
|
+
*/
|
|
31
|
+
export interface FactionRegistry {
|
|
32
|
+
factions: Map<string, Faction>;
|
|
33
|
+
globalStanding: Map<string, Map<string, Q>>;
|
|
34
|
+
entityReputations: Map<number, Map<string, Q>>;
|
|
35
|
+
}
|
|
36
|
+
export declare const STANDING_EXALTED: Q;
|
|
37
|
+
export declare const STANDING_ALLY: Q;
|
|
38
|
+
export declare const STANDING_NEUTRAL: Q;
|
|
39
|
+
export declare const STANDING_RIVAL: Q;
|
|
40
|
+
export declare const STANDING_KOS: Q;
|
|
41
|
+
/** Standing below this → AI treats target as hostile. */
|
|
42
|
+
export declare const STANDING_HOSTILE_THRESHOLD: Q;
|
|
43
|
+
/** Standing above this → AI will not initiate combat. */
|
|
44
|
+
export declare const STANDING_FRIENDLY_THRESHOLD: Q;
|
|
45
|
+
/** Minimum detection quality for an entity to witness an event. */
|
|
46
|
+
export declare const WITNESS_DETECTION_THRESHOLD: Q;
|
|
47
|
+
/**
|
|
48
|
+
* Create a FactionRegistry pre-populated with rival/ally default standings.
|
|
49
|
+
*
|
|
50
|
+
* Only direct relations need to be specified; symmetric standings are NOT
|
|
51
|
+
* applied automatically (enemy of A is not necessarily enemy of B).
|
|
52
|
+
*/
|
|
53
|
+
export declare function createFactionRegistry(factions: Faction[]): FactionRegistry;
|
|
54
|
+
/**
|
|
55
|
+
* Compute effective standing of entity `a` toward entity `b`.
|
|
56
|
+
*
|
|
57
|
+
* Priority (highest first):
|
|
58
|
+
* 1. Same faction → STANDING_EXALTED
|
|
59
|
+
* 2. Entity-level reputation (`registry.entityReputations.get(a.id)?.get(b.faction)`)
|
|
60
|
+
* combined with faction default — max of the two is used.
|
|
61
|
+
* 3. Global faction-to-faction standing
|
|
62
|
+
* 4. Rival / ally default from faction definition
|
|
63
|
+
* 5. STANDING_NEUTRAL (q(0.50)) for all unknown combinations
|
|
64
|
+
*/
|
|
65
|
+
export declare function effectiveStanding(registry: FactionRegistry, a: Entity, b: Entity): Q;
|
|
66
|
+
/**
|
|
67
|
+
* Apply a witness event: adjust the actor's standing within the specified faction.
|
|
68
|
+
*
|
|
69
|
+
* Deltas are clamped to [0, SCALE.Q]. A kill of a faction member reduces the
|
|
70
|
+
* actor's standing with that faction; aiding a member increases it.
|
|
71
|
+
*/
|
|
72
|
+
export declare function applyWitnessEvent(registry: FactionRegistry, event: WitnessEvent): void;
|
|
73
|
+
/**
|
|
74
|
+
* Scan a TraceEvent stream and produce WitnessEvents for reputation-relevant
|
|
75
|
+
* actions (kills, assaults, aid).
|
|
76
|
+
*
|
|
77
|
+
* Only events where at least one bystander entity (not the actor or target) can
|
|
78
|
+
* detect the actor (`detectionQ ≥ WITNESS_DETECTION_THRESHOLD`) are included.
|
|
79
|
+
*
|
|
80
|
+
* Deduplication: at most one event per (actorId, eventType) per tick.
|
|
81
|
+
*
|
|
82
|
+
* @param factions Map of entityId → factionId for the current scenario.
|
|
83
|
+
*/
|
|
84
|
+
export declare function extractWitnessEvents(events: TraceEvent[], world: WorldState, factions: Map<number, string>): WitnessEvent[];
|
|
85
|
+
/**
|
|
86
|
+
* Adjust the global faction-to-faction standing of `factionAId` toward
|
|
87
|
+
* `factionBId` by `delta`, clamped to [0, SCALE.Q].
|
|
88
|
+
*
|
|
89
|
+
* Used by the Polity diplomacy system (Phase 61) to apply `standingDelta`
|
|
90
|
+
* from `resolveDiplomacy`. The relation is one-directional; call twice with
|
|
91
|
+
* swapped arguments for a symmetric update.
|
|
92
|
+
*/
|
|
93
|
+
export declare function applyFactionStanding(registry: FactionRegistry, factionAId: string, factionBId: string, delta: Q): void;
|
|
94
|
+
/**
|
|
95
|
+
* Serialise a FactionRegistry to a JSON string.
|
|
96
|
+
* Handles all nested Map and Set fields (rivals, allies, globalStanding, entityReputations).
|
|
97
|
+
*/
|
|
98
|
+
export declare function serialiseFactionRegistry(registry: FactionRegistry): string;
|
|
99
|
+
/**
|
|
100
|
+
* Deserialise a FactionRegistry from a JSON string produced by `serialiseFactionRegistry`.
|
|
101
|
+
*/
|
|
102
|
+
export declare function deserialiseFactionRegistry(json: string): FactionRegistry;
|