@jgengine/core 0.36.0 → 0.37.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/combat/projectiles.d.ts +6 -2
  3. package/dist/combat/projectiles.js +55 -52
  4. package/dist/combat/shotSpec.d.ts +239 -0
  5. package/dist/combat/shotSpec.js +216 -0
  6. package/dist/combat/weaponSystem.js +26 -21
  7. package/dist/combat.d.ts +1 -0
  8. package/dist/combat.js +1 -0
  9. package/dist/game/playableGame.d.ts +9 -0
  10. package/dist/meta/changelog.d.ts +1 -1
  11. package/dist/meta/changelog.js +58 -1
  12. package/dist/movement/characterController.d.ts +5 -0
  13. package/dist/movement/characterController.js +8 -1
  14. package/dist/movement/movementModel.js +4 -1
  15. package/dist/movement/playerMovement.js +4 -0
  16. package/dist/physics/physicsBackendConformance.js +17 -0
  17. package/dist/physics/physicsWorld.d.ts +7 -0
  18. package/dist/physics/physicsWorld.js +27 -6
  19. package/dist/run/runLoop.d.ts +44 -1
  20. package/dist/run/runLoop.js +66 -5
  21. package/dist/world/features.d.ts +15 -1
  22. package/dist/world/features.js +51 -2
  23. package/dist/world/lod.d.ts +8 -0
  24. package/dist/world/lod.js +13 -5
  25. package/dist/world/modelLod.d.ts +44 -0
  26. package/dist/world/modelLod.js +75 -0
  27. package/dist/world.d.ts +2 -1
  28. package/dist/world.js +2 -1
  29. package/package.json +1 -1
  30. package/skills/jgengine/api.md +2 -2
  31. package/skills/jgengine-combat/SKILL.md +7 -0
  32. package/skills/jgengine-combat/api.md +61 -1
  33. package/skills/jgengine-combat/capabilities.md +4 -0
  34. package/skills/jgengine-combat/reference.md +1 -0
  35. package/skills/jgengine-gameplay/api.md +6 -0
  36. package/skills/jgengine-gameplay/capabilities.md +1 -0
  37. package/skills/jgengine-world/api.md +17 -0
@@ -44,10 +44,15 @@
44
44
  - `DEFAULT_ARMOR_HALF_VALUE` (const): const DEFAULT_ARMOR_HALF_VALUE: 100 — Default armor points at which a hit is halved.
45
45
  - `DEFAULT_CRIT_MULTIPLIER` (const): const DEFAULT_CRIT_MULTIPLIER: 2 — Default crit multiplier for a zone that does not name one.
46
46
  - `DEFAULT_EYE_HEIGHT` (const): const DEFAULT_EYE_HEIGHT: number — Shot-origin and first-person camera eye height above an entity's position: 90% of the default 1.8m hitbox top.
47
+ - `DEFAULT_FALLOFF_END_SCALE` (const): const DEFAULT_FALLOFF_END_SCALE: 0.5 — Damage multiplier left at the end of falloff when a shot names no end scale.
47
48
  - `DEFAULT_FIRE_PULSE_SECONDS` (const): const DEFAULT_FIRE_PULSE_SECONDS: 0.12 — Default `RenderCueTuning.firePulseSeconds`.
48
49
  - `DEFAULT_HIT_PULSE_SECONDS` (const): const DEFAULT_HIT_PULSE_SECONDS: 0.2 — Default `RenderCueTuning.hitPulseSeconds`.
49
50
  - `DEFAULT_MAX_MITIGATION` (const): const DEFAULT_MAX_MITIGATION: 0.9 — Default ceiling on armor mitigation, so armor never fully negates a hit.
50
51
  - `DEFAULT_RENDER_CUES` (const): const DEFAULT_RENDER_CUES: Readonly<EntityRenderCues> — Neutral starting cue set: idle, unarmed, undamaged.
52
+ - `DEFAULT_SHOT_PENETRATION` (const): const DEFAULT_SHOT_PENETRATION: 0 — Most extra hits a shot may pass through when it names no penetration.
53
+ - `DEFAULT_SHOT_RADIUS` (const): const DEFAULT_SHOT_RADIUS: 0.5 — Hit-sphere radius of a body that names none, m.
54
+ - `DEFAULT_SHOT_RANGE` (const): const DEFAULT_SHOT_RANGE: 100 — Furthest a shot resolves when the delivery names no range, m.
55
+ - `DEFAULT_SHOT_SPEED` (const): const DEFAULT_SHOT_SPEED: 15 — Launch speed of a body that names none, m/s.
51
56
  - `DEFAULT_VFX_PRESET` (const): const DEFAULT_VFX_PRESET: VfxPresetName — The fallback flavor used when a `vfx()` call names an unknown preset — so a typo still shows a spark instead of nothing.
52
57
  - `DEFAULT_WEAPON_PELLETS_IN_FLIGHT` (const): const DEFAULT_WEAPON_PELLETS_IN_FLIGHT: 32 — Default live-pellet cap for a weapon that does not name one.
53
58
  - `DamageChannelId` (type): type DamageChannelId = string — Caller-owned id for a damage channel — `"fire"`, `"ballistic"`, `"physical"`, `"corrosive"`. The engine never branches on the value; a channel is whatever a game's data declares.
@@ -123,6 +128,7 @@
123
128
  - `RenderCueTuning` (interface): interface RenderCueTuning — Tuning knobs for `advanceMotionCues` / `useEntityRenderCues`; every field has a default, override only what a weapon/rig's feel needs to differ.
124
129
  - `ResolvedMatchup` (interface): interface ResolvedMatchup — Fully-resolved matchup outputs (every axis populated) plus the contributions that produced them.
125
130
  - `ResolvedShot` (interface): interface ResolvedShot — A shot's resolved firing geometry in world space: `origin` is the point the projectile/ray starts from and `direction` is its normalized aim vector, both computed by {@link resolveShot} from the shooter's position, facing, and the chosen origin policy.
131
+ - `ResolvedShotSpec` (interface): interface ResolvedShotSpec — A spec with every default filled in. Read `mode` and the branch it names, never the raw spec.
126
132
  - `ResourcePool` (interface): interface ResourcePool { current(): number; max(): number; setMax(value: number): void; fraction(): number; canSpend(amount: number): boolean; spend(amount: number): boolean; gain(amount: number): number; set(value: number): void; tick(dtSeconds: number): void; i… — ⚠ undocumented · used by `createResourcePool`: A regenerating resource pool — mana, stamina, energy — that actions spend from and that refills over time.
127
133
  - `RewardClaimSpec` (interface): interface RewardClaimSpec — How a `claimed` allocation exposes its pool: open first-come (`first`) or `reserved` to one recipient.
128
134
  - `RewardGrant` (interface): interface RewardGrant — An authoritative, serializable award of one result stack to one recipient — carries `via` (the policy that produced it) and `private` (replication scope) so provenance and visibility survive save/load and viewer projection. Applying a grant twice with the same identity is the caller's idempotency contract; this seam only decides who is owed what.
@@ -130,7 +136,23 @@
130
136
  - `RewardRecipient` (interface): interface RewardRecipient — A participant that may receive rewards — an opaque recipient id (player, team, party slot) with an optional split `weight` (shared policy), eligibility `tags`, and an `eligible` gate. Ineligible recipients are excluded before any allocation decision so they never perturb others' streams.
131
137
  - `RewardResult` (interface): interface RewardResult — One indivisible reward to distribute among participants — an opaque result id (a rolled drop, a currency bundle, a quest token) with an optional stack `count` and free-form `tags` for eligibility and provenance. Allocation never interprets the id; generation (loot tables, roll formulas) owns what it means, keeping the seam genre-agnostic.
132
138
  - `RewardShare` (interface): interface RewardShare — A `{ result, count }` slice of a reward — the unit shared pools and grants are expressed in.
139
+ - `SHOT_GRAVITY` (const): const SHOT_GRAVITY: 9.8 — Standard gravity used to scale `ShotBody.gravity`, m/s².
140
+ - `ShotArc` (interface): interface ShotArc extends ShotLaunch — Where a body lands and how long it took, in the same numbers the runtime simulates.
141
+ - `ShotBody` (interface): interface ShotBody — A travelling body simulated over time. Every dimension is stated; nothing is read off another field, so a slow arrow with a splash and a fast bullet without one are the same shape.
142
+ - `ShotContact` (interface): interface ShotContact — What a shot does on contact: damage, falloff, penetration, splash, ricochet, statuses.
143
+ - `ShotDamage` (interface): interface ShotDamage — One damage channel a shot applies on contact.
144
+ - `ShotDelivery` (type): type ShotDelivery = ShotTrace | ShotBody — How the shot reaches its target.
145
+ - `ShotFalloff` (interface): interface ShotFalloff — Linear damage falloff between two distances.
146
+ - `ShotHitOutcome` (interface): interface ShotHitOutcome — What one contact does: whether the shot still carries damage, at what scale, and whether it goes on.
147
+ - `ShotLaunch` (interface): interface ShotLaunch — Straight-line flight of a body, for a caller that only needs where it starts and how fast.
133
148
  - `ShotOriginPolicy` (type): type ShotOriginPolicy = | { kind: "converge"; muzzle?: EntityPosition; height?: number } | { kind: "eye"; height?: number } | { kind: "legacy" } | { kind: "entity" } | { kind: "entityOffset"; offset: EntityPosition } | { kind: "muzzle"; offset?: EntityPosition } | { kind: "camera"; origin: EntityPos… — How a shot's world-space origin (and optional direction) is resolved before prediction/settlement. - `converge` — the shot leaves the gun `muzzle` yet still passes through whatever the shooter's sightline (crosshair) covers: origin is the muzzle offset, direction is bent from the muzzle to the eye ray's aim point. The projectile system's default for a free `{ yaw, pitch }` aim, so a bullet visibly comes from the barrel without missing the reticle. Needs a scene raycast to find the aim point (`convergeShot`); a bare `resolveShot` degrades to a straight muzzle ray. Passes an explicit `{ origin, direction }` aim through untouched. - `eye` — `aim.origin` when present, else the shooter's entity position raised to eye height; the shot traces the shooter's sightline, so what the crosshair covers is what gets hit. - `legacy` — `aim.origin` when present, else the shooter's raw entity position (feet). - `entity` — always the shooter's entity position. - `entityOffset` / `muzzle` — entity-local offset rotated by the shooter's yaw (muzzle on a weapon model). - `camera` — explicit camera/reticle world origin (and optional direction override). - `world` — absolute world origin.
149
+ - `ShotPenetration` (interface): interface ShotPenetration — Extra targets a hit passes through.
150
+ - `ShotRicochet` (interface): interface ShotRicochet — Bounces off the first surface contacted.
151
+ - `ShotSpec` (interface): interface ShotSpec — A shot as plain data: how it travels and what it does on arrival. Every field but `delivery.kind` is optional with a documented default, so a plain gun is three lines.
152
+ - `ShotSplash` (interface): interface ShotSplash — Area damage around the contact point.
153
+ - `ShotStatReader` (type): type ShotStatReader = (stat: string) => number | null — Reads a numeric stat by name; the shape every item registry already exposes.
154
+ - `ShotTrace` (interface): interface ShotTrace — An instant trace: the shot resolves on the tick it is fired and never travels.
155
+ - `ShotVec3` (type): type ShotVec3 = readonly [number, number, number] — One description of how a shot travels and what it does when it arrives, plus the single resolver that turns that description into behaviour. A weapon is data: a rifle, a grenade, a mortar and an arrow differ by fields here, never by a code path, a weapon id or a classifier that reads one stat as proof of another.
134
156
  - `SpawnProvider` (type): type SpawnProvider = ( params: Readonly<Record<string, unknown>>, ctx: SpawnProviderContext, ) => readonly EncounterSpawnRequest[] — A spawn provider: given the phase's `params` and context, returns the spawn requests to emit on enter. Deterministic; reads authored scene points from `ctx.spawnPoints` rather than embedding coordinates.
135
157
  - `SpawnProviderContext` (interface): interface SpawnProviderContext — Evaluation context a spawn provider receives when its phase is entered.
136
158
  - `SquadCombat` (interface): interface SquadCombat — Drive a squad: cover by threat, flanking, fire discipline, suppression and retreat.
@@ -290,12 +312,18 @@
290
312
  - `resolveReceivedDamage` (function): function resolveReceivedDamage(input: ReceivedDamageInput): ReceivedDamageResult — Run receiver-side modifiers over an incoming hit, in ascending `order`, recording every step. Amplification, reduction, caps, conversion, redirection, and immunity are all data — the engine applies whichever policies the target's modifiers declare and never branches on channel names. Pure over serializable data with bounded work (one pass over the modifier list); an `immune` policy short-circuits the remaining modifiers.
291
313
  - `resolveResistance` (function): function resolveResistance<TCategory extends string = string, TProperty extends string = string>(matrix: ResistanceMatrix<TCategory, TProperty>, category: TCategory | string, targetProperties: readonly (TProperty | string)[]): ResistanceResult — ⚠ undocumented
292
314
  - `resolveShot` (function): function resolveShot(deps: ShotOriginDeps, from: string, aim: Aim, policy: ShotOriginPolicy = { kind: "eye" }): ResolvedShot | null — Resolves a shot's world origin and direction from the shooter's eye or muzzle and an {@link Aim}, ready for `ctx.scene.raycast` — the aim half of a hitscan weapon.
315
+ - `resolveShotSpec` (function): function resolveShotSpec(spec: ShotSpec): ResolvedShotSpec — Fill every default and return the one shape firing paths act on. This is the only place that decides how a shot travels, so a delivery can never be guessed from a damage or splash field.
293
316
  - `resolveStatusApplication` (function): function resolveStatusApplication(input: StatusApplicationInput): StatusApplicationOutcome — Resolve one status application: reject on immunity, roll landing chance against injected RNG, scale magnitude/duration by the matchup, then fold into any existing instance per the stacking policy. Deterministic given a seeded `rng`; pure over serializable data, so authority can run it and replicate only the returned `instance`.
294
317
  - `resolveTableDamage` (function): function resolveTableDamage(table: DamageTableDef, input: { type: string; resistance: string; armor?: number; base?: number }): DamageTableRow — Resolve one hit through the table: type multiplier, then armor mitigation, and the base scaled by both. Formula order matters only for the numbers a UI shows — the product is the same.
295
318
  - `resolveVfxPreset` (function): function resolveVfxPreset(name: string | undefined, overrides?: Partial<VfxPreset>): VfxPreset — Resolve a named visual flavor into a concrete `{ kind, color, durationMs?, radius? }`, with any provided `overrides` winning field by field. An unknown name resolves to {@link DEFAULT_VFX_PRESET} rather than throwing, so a flavor typo degrades to a visible spark. Used by `ctx.scene.entity.vfx({ preset })`; call it directly when you need the raw numbers (e.g. to seed a retained `vfxInstance` from the same vocabulary).
296
319
  - `rollCheck` (function): function rollCheck(input: CheckInput, rng?: () => number): CheckResult — Resolve a tabletop-style pass/fail roll against a target number with modifiers and crit/fumble bands.
297
320
  - `scoreCoverPoint` (function): function scoreCoverPoint(point: CoverPoint, query: CoverQuery): CoverChoice | null — Score one stand point against the threats the member remembers. `hidden` is the confidence-weighted share of remembered threats that cannot see the point, so a point that hides from the shooter that matters scores over one that hides from a fading one. Returns `null` when nothing it can hide from is worth considering.
298
321
  - `selectCover` (function): function selectCover(points: readonly CoverPoint[], query: CoverQuery): CoverChoice | null — The best of `points` under one {@link CoverQuery}, ties broken by point id. Pass the candidate set the injected query already bounded; this scores what it is given.
322
+ - `shotArc` (function): function shotArc(shot: ResolvedShotSpec, origin: ShotVec3, direction: ShotVec3): ShotArc — Solve the path of a body from a launch pose. A trace has no path and fails loudly instead of returning a straight line a caller would mistake for an arc.
323
+ - `shotFalloffAt` (function): function shotFalloffAt(shot: ResolvedShotSpec, distance: number): number — Damage multiplier from falloff at `distance`; `1` with no falloff.
324
+ - `shotFuseSeconds` (function): function shotFuseSeconds(shot: ResolvedShotSpec): number | null — Number of seconds the body flies before its fuse ends the flight, or `null` when it flies to contact.
325
+ - `shotHitOutcome` (function): function shotHitOutcome(shot: ResolvedShotSpec, distance: number, hitIndex: number): ShotHitOutcome — What a contact at `distance` does to the `hitIndex`-th target it meets, counting the first as `0`. A trace or body stops once its penetration slots are spent, so penetration is a number, not a branch.
326
+ - `shotSpecFromStats` (function): function shotSpecFromStats(read: ShotStatReader): ShotSpec — Adapt the numeric stat keys older item data carries to a spec. This is the only place those keys are read, and delivery is decided by the keys that describe travel (`projectile.*`) alone: a splash radius or a damage number never turns a trace into a body.
299
327
  - `startEncounter` (function): function startEncounter<TData>(config: EncounterConfig<TData>, state: EncounterState<TData>, ctx: EncounterContext = {}): EncounterStep<TData> — Start a created encounter: enter the first root phase, descending to its first leaf, and return the initial enter/spawn events. A no-op (with no events) on an already-started or empty encounter. Mutates and returns `state`.
300
328
  - `statusEffectRemainingFraction` (function): function statusEffectRemainingFraction(view: StatusEffectView): number — Fraction of the countdown still remaining, clamped to `[0,1]` — the value a countdown ring fills to. Returns `0` when the duration is non-positive. Pure; safe to call every frame.
301
329
  - `threatsFromMemory` (function): function threatsFromMemory(perception: PerceptionService, observerId: string, nowMs: number, options: ThreatReadOptions = {}): ThreatReading[] — Read one member's threat list out of its perception memory — the only source of threat. Sorted by confidence, then recency, then id, and bounded by `max`, so a crowded memory cannot grow the tick.
@@ -627,7 +655,7 @@
627
655
  - `ProjectileObjectsDeps` (interface): interface ProjectileObjectsDeps { list(): readonly {instanceId: string; catalogId: string; position: readonly [number, number, number]; rotationY?: number;}[]; inBox?(min: EntityPosition, max: EntityPosition): readonly {instanceId: string; catalogId: string; position: rea… — ⚠ undocumented
628
656
  - `ProjectilePrediction` (interface): interface ProjectilePrediction { hits: ProjectileHit[]; blocked?: boolean; origin?: EntityPosition; direction?: EntityPosition; firstImpact?: ProjectileHit | null } — ⚠ undocumented
629
657
  - `ProjectileSettleReport` (interface): interface ProjectileSettleReport — Exactly-once settlement with launch identity and actual applied effects.
630
- - `ProjectileShotInput` (interface): interface ProjectileShotInput { from: string; via: EffectVia; aim: Aim; effect: string; originPolicy?: ShotOriginPolicy; travel?: ProjectileTravelConfig } — ⚠ undocumented
658
+ - `ProjectileShotInput` (interface): interface ProjectileShotInput { from: string; via: EffectVia; spec?: ShotSpec; aim: Aim; effect: string; originPolicy?: ShotOriginPolicy; travel?: ProjectileTravelConfig } — ⚠ undocumented
631
659
  - `ProjectileShotState` (interface): interface ProjectileShotState — Serializable captured launch and settlement state.
632
660
  - `ProjectileSystem` (interface): interface ProjectileSystem { willHitProjectile(input: ProjectileShotInput): ProjectilePrediction; fireProjectile(input: ProjectileShotInput): string; settleProjectile(shotId: string): SettleResult; advanceProjectiles(dt: number, time: number): SettleResult[]; activeP… — ⚠ undocumented · used by `createProjectileSystem`: Spawn and advance projectiles each frame, resolving travel, lifetime, and hits.
633
661
  - `ProjectileSystemDeps` (interface): interface ProjectileSystemDeps { effects: EffectSystem; spatial: CombatSpatialDeps; getStat(itemId: string, stat: string): number | null; raycast?: Raycast; sceneRaycast?: SceneRaycastApi; objects?: ProjectileObjectsDeps; entityCollidersOf?(instanceId: string): EntityCol… — ⚠ undocumented · used by `createProjectileSystem`: Spawn and advance projectiles each frame, resolving travel, lifetime, and hits.
@@ -721,6 +749,38 @@
721
749
  - `eyeHeightFromColliders` (function): function eyeHeightFromColliders(set: EntityColliderSet | null | undefined): number — Eye height derived from a collider set: 90% of the tallest hitbox top, or the humanoid default when unknown.
722
750
  - `resolveShot` (function): function resolveShot(deps: ShotOriginDeps, from: string, aim: Aim, policy: ShotOriginPolicy = { kind: "eye" }): ResolvedShot | null — Resolves a shot's world origin and direction from the shooter's eye or muzzle and an {@link Aim}, ready for `ctx.scene.raycast` — the aim half of a hitscan weapon.
723
751
 
752
+ ## @jgengine/core/combat/shotSpec
753
+
754
+ - `DEFAULT_FALLOFF_END_SCALE` (const): const DEFAULT_FALLOFF_END_SCALE: 0.5 — Damage multiplier left at the end of falloff when a shot names no end scale.
755
+ - `DEFAULT_SHOT_PENETRATION` (const): const DEFAULT_SHOT_PENETRATION: 0 — Most extra hits a shot may pass through when it names no penetration.
756
+ - `DEFAULT_SHOT_RADIUS` (const): const DEFAULT_SHOT_RADIUS: 0.5 — Hit-sphere radius of a body that names none, m.
757
+ - `DEFAULT_SHOT_RANGE` (const): const DEFAULT_SHOT_RANGE: 100 — Furthest a shot resolves when the delivery names no range, m.
758
+ - `DEFAULT_SHOT_SPEED` (const): const DEFAULT_SHOT_SPEED: 15 — Launch speed of a body that names none, m/s.
759
+ - `ResolvedShotSpec` (interface): interface ResolvedShotSpec — A spec with every default filled in. Read `mode` and the branch it names, never the raw spec.
760
+ - `SHOT_GRAVITY` (const): const SHOT_GRAVITY: 9.8 — Standard gravity used to scale `ShotBody.gravity`, m/s².
761
+ - `ShotArc` (interface): interface ShotArc extends ShotLaunch — Where a body lands and how long it took, in the same numbers the runtime simulates.
762
+ - `ShotBody` (interface): interface ShotBody — A travelling body simulated over time. Every dimension is stated; nothing is read off another field, so a slow arrow with a splash and a fast bullet without one are the same shape.
763
+ - `ShotContact` (interface): interface ShotContact — What a shot does on contact: damage, falloff, penetration, splash, ricochet, statuses.
764
+ - `ShotDamage` (interface): interface ShotDamage — One damage channel a shot applies on contact.
765
+ - `ShotDelivery` (type): type ShotDelivery = ShotTrace | ShotBody — How the shot reaches its target.
766
+ - `ShotFalloff` (interface): interface ShotFalloff — Linear damage falloff between two distances.
767
+ - `ShotHitOutcome` (interface): interface ShotHitOutcome — What one contact does: whether the shot still carries damage, at what scale, and whether it goes on.
768
+ - `ShotLaunch` (interface): interface ShotLaunch — Straight-line flight of a body, for a caller that only needs where it starts and how fast.
769
+ - `ShotPenetration` (interface): interface ShotPenetration — Extra targets a hit passes through.
770
+ - `ShotRicochet` (interface): interface ShotRicochet — Bounces off the first surface contacted.
771
+ - `ShotSpec` (interface): interface ShotSpec — A shot as plain data: how it travels and what it does on arrival. Every field but `delivery.kind` is optional with a documented default, so a plain gun is three lines.
772
+ - `ShotSplash` (interface): interface ShotSplash — Area damage around the contact point.
773
+ - `ShotStatReader` (type): type ShotStatReader = (stat: string) => number | null — Reads a numeric stat by name; the shape every item registry already exposes.
774
+ - `ShotTrace` (interface): interface ShotTrace — An instant trace: the shot resolves on the tick it is fired and never travels.
775
+ - `ShotVec3` (type): type ShotVec3 = readonly [number, number, number] — One description of how a shot travels and what it does when it arrives, plus the single resolver that turns that description into behaviour. A weapon is data: a rifle, a grenade, a mortar and an arrow differ by fields here, never by a code path, a weapon id or a classifier that reads one stat as proof of another.
776
+ - `resolveShotSpec` (function): function resolveShotSpec(spec: ShotSpec): ResolvedShotSpec — Fill every default and return the one shape firing paths act on. This is the only place that decides how a shot travels, so a delivery can never be guessed from a damage or splash field.
777
+ - `shotArc` (function): function shotArc(shot: ResolvedShotSpec, origin: ShotVec3, direction: ShotVec3): ShotArc — Solve the path of a body from a launch pose. A trace has no path and fails loudly instead of returning a straight line a caller would mistake for an arc.
778
+ - `shotFalloffAt` (function): function shotFalloffAt(shot: ResolvedShotSpec, distance: number): number — Damage multiplier from falloff at `distance`; `1` with no falloff.
779
+ - `shotFuseSeconds` (function): function shotFuseSeconds(shot: ResolvedShotSpec): number | null — Number of seconds the body flies before its fuse ends the flight, or `null` when it flies to contact.
780
+ - `shotHitOutcome` (function): function shotHitOutcome(shot: ResolvedShotSpec, distance: number, hitIndex: number): ShotHitOutcome — What a contact at `distance` does to the `hitIndex`-th target it meets, counting the first as `0`. A trace or body stops once its penetration slots are spent, so penetration is a number, not a branch.
781
+ - `shotSpecFromStats` (function): function shotSpecFromStats(read: ShotStatReader): ShotSpec — Adapt the numeric stat keys older item data carries to a spec. This is the only place those keys are read, and delivery is decided by the keys that describe travel (`projectile.*`) alone: a splash radius or a damage number never turns a trace into a body.
782
+ - `shotSpecFromWeapon` (function): function shotSpecFromWeapon(definition: { delivery: | { kind: "hitscan"; maxRange: number } | { kind: "projectile"; speed: number; gravity?: number; drag?: number; lifetime: number; maxRange?: number }; damage: { type: string; base: number; statuses?: readonly string[] }; falloff?: ShotFalloff; }): … — Project an existing {@link WeaponDefinition} onto a spec, so a weapon authored as a definition and one authored as a spec reach the same firing paths.
783
+
724
784
  ## @jgengine/core/combat/squadCombat
725
785
 
726
786
  - `SquadCombat` (interface): interface SquadCombat — Drive a squad: cover by threat, flanking, fire discipline, suppression and retreat.
@@ -263,6 +263,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
263
263
  - `filterOutcomeFor` (function) · `import { filterOutcomeFor } from "@jgengine/core/combat"`
264
264
  - `resolveClaim` (function) · `import { resolveClaim } from "@jgengine/core/combat"`
265
265
 
266
+ ## shot-spec — resolve one shot description into explicit travel and contact behaviour
267
+
268
+ - `resolveShotSpec` (function) · `import { resolveShotSpec } from "@jgengine/core/combat"`
269
+
266
270
  ## spawn-instance-restore — rehydrate a creature stat instance from decoded snapshot data
267
271
 
268
272
  - `restoreSpawnInstance` (function) · `import { restoreSpawnInstance } from "@jgengine/core/stats/spawnLevelStats"`
@@ -164,6 +164,7 @@ With `events: ctx.game.events` and `healthOf`, every hit and status tick raises
164
164
  - **Reload states.** `reload()` starts or continues; `interruptReload()` suspends through `Magazine.pauseReload` keeping progress; `resumeReload()` continues from it; `cancelReload()` discards. `ammo()` returns loaded / capacity / reserve / state / reload fraction / burst remaining / cadence fraction, and `stats()` carries shots, rounds spent, pulls, reloads, drops and damage dealt — the whole ammo economy is readable without touching internals.
165
165
  - **State.** `snapshot()` / `restore()` carry aim, handling, magazine, trigger, burst, pellets and counters as plain data; `retune(definition)` swaps rate, feel, damage and capacity without losing ammo or aim (a shrinking magazine discards rounds above the new capacity); a `devtoolsName` registers a one-line probe.
166
166
  - **When to use which.** `createWeaponRuntime` (`combat/weaponFire`) is the minimal plumbing for a game that already owns its fire loop and raycaster; `createWeaponSystem` is the definition-driven version with fire modes, ballistics, reload states and ammo accounting. Both leave models, audio, muzzle flash, camera and input to the game; feel numbers come from `measureWeapon` and presentation from `weaponPresentation`.
167
+ - **Anything that is not a gun** is a `ShotSpec` (`combat/shotSpec`): `delivery` is `{ kind: "trace", range? }` or `{ kind: "body", speed?, gravity?, drag?, lifetime?, radius?, fuse?: number | "impact", homing?, range? }`, and `contact` carries `damage`, `falloff`, `penetration { count, keepDamage? }`, `splash { radius, falloff?, damageScale? }`, `ricochet { count, angleDeg?, damageScale? }`. Every field but `delivery.kind` has a documented default, so a rifle is `{ delivery: { kind: "trace" }, contact: { damage: { type: "kinetic", base: 24 }, falloff: { start: 25, end: 75, endScale: 0.4 } } }` and a grenade is a body with `fuse: 2` and `splash`. `resolveShotSpec` fills the defaults once, `shotArc` solves the path, and `shotFalloffAt` / `shotHitOutcome` answer damage per distance and per penetrated target. `ProjectileShotInput.spec` takes one directly; `shotSpecFromStats` adapts older numeric item keys in one place and never reads a splash or damage field to decide delivery.
167
168
  - **Evidence** (`combat/weaponSystem.test.ts`): the test fixture at 600 rpm with a 30-round magazine empties in 2.9 s, climbs 17.0° pitch and 3.1° yaw over the magazine, is back to half climb in 0.37 s and within 5% in 0.6 s; bloom runs 2.0 → 8.0 → 12.0 mrad (cap) by the tenth shot and settles to base 0.63 s after the last shot. A hitscan and a projectile contact at the same distance produce the same 20 damage (30 on a 1.5x head zone), and one second of held fire cannot exceed four shots.
168
169
 
169
170
  ## Encounter sequencing
@@ -922,6 +922,8 @@
922
922
  - `ModelMaterialOverride` (interface): interface ModelMaterialOverride extends MaterialSurfaceParameters — Per-entity PBR material override (#151.3) applied to every standard or physical material in the model's cloned scene graph.
923
923
  - `ModelPart` (interface): interface ModelPart — Static child model stacked at a fixed local offset under its parent's transform — no bone/rig resolution, unlike `ModelAttachment`. Assembles a compound entity (e.g. a modular castle wall + tower + roof) from several single-mesh kit pieces.
924
924
  - `ModelRimLight` (interface): interface ModelRimLight — Fresnel rim-light term added on top of a model's lighting — see {@link ModelMaterialOverride.rim}.
925
+ - `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB and how far it draws.
926
+ - `ModelVariantLodConfig` (interface): interface ModelVariantLodConfig — Ordered mesh detail levels for one model, switched per instance by camera distance. The first level is the full mesh the model's own `url` names; every later level is a cheaper asset. A tree line uses this so near trees keep their full mesh while far ones draw a decimated copy of the same species.
925
927
  - `ModelWindConfig` (interface): interface ModelWindConfig — Wind sway for a foliage model; see {@link ModelConfig.wind}.
926
928
  - `MovementCommitFrame` (interface): interface MovementCommitFrame — One frame's movement resolution handed to `PlayerMovementConfig.beforeCommit`.
927
929
  - `MovementFeelConfig` (interface): interface MovementFeelConfig — Feel knobs for the built-in walk controller; see {@link PlayerMovementConfig.feel}.
@@ -2685,13 +2687,17 @@
2685
2687
  ## @jgengine/core/run/runLoop
2686
2688
 
2687
2689
  - `RunChange` (interface): interface RunChange<T> — One change the next run can open with. `T` is the game's own payload (a tighter rival line, an unlocked part, a route modifier) - the loop only draws and remembers which id was drawn.
2690
+ - `RunChoice` (interface): interface RunChoice<T> — One change a run could open with, in the shape a choice screen shows: the id and its payload.
2688
2691
  - `RunLoop` (interface): interface RunLoop<T> — A run's lifecycle: what it carries and can lose, and the change it hands the next run.
2689
2692
  - `RunLoopConfig` (interface): interface RunLoopConfig<T> — Configuration for {@link createRunLoop}.
2690
2693
  - `RunLoopState` (interface): interface RunLoopState — Everything a run loop stores as plain JSON: the run in progress plus lifetime totals.
2691
2694
  - `RunOffer` (interface): interface RunOffer<T> — The change a run opens with, and which run it is.
2692
2695
  - `RunOutcome` (type): type RunOutcome = "banked" | "lost" — How a run ended: what it carried reached the bank, or it was lost.
2696
+ - `RunProbe` (interface): interface RunProbe — Read-only handle a capture or devtools uses to prove a run loop is live, not merely imported.
2697
+ - `RunReadout` (interface): interface RunReadout — What the loop reports about the run it is playing, for a capture or devtools read.
2693
2698
  - `RunSettlement` (interface): interface RunSettlement<T> — What one closed run banked or lost, and what the next run changes.
2694
2699
  - `createRunLoop` (function): function createRunLoop<T>(config: RunLoopConfig<T>): RunLoop<T> — Bank-or-lose between runs, with one deterministic change per run drawn from an authored table.
2700
+ - `installRunProbe` (function): function installRunProbe<T>(loop: RunLoop<T>): () => void — Publish the loop's own readout on `window.__jgengineRun`, so a capture or devtools probe can state the live run, what it carries, what it banked or lost and the change it offers. Reads only; it never starts, settles or changes a run. Returns the uninstaller.
2695
2701
 
2696
2702
  ## @jgengine/core/session/contestedChannel
2697
2703
 
@@ -652,6 +652,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
652
652
  ## run-loop — bank or lose what a run carries, with one deterministic change per run
653
653
 
654
654
  - `createRunLoop` (function) · `import { createRunLoop } from "@jgengine/core/run/runLoop"`
655
+ - `installRunProbe` (function) · `import { installRunProbe } from "@jgengine/core/run/runLoop"`
655
656
 
656
657
  ## run-modifiers — a roguelike run built from stacking drafted modifier picks
657
658
 
@@ -2501,6 +2501,7 @@
2501
2501
  - `DEFAULT_POLICE_TIERS` (const): const DEFAULT_POLICE_TIERS: readonly PoliceTier[] — The starting tier table: patrol until heat arrives, then investigate, pursue and finally cordon.
2502
2502
  - `DEFAULT_POLICE_TUNING` (const): const DEFAULT_POLICE_TUNING: PoliceTuning — The starting policy: linear decay, a six-second escape and a search area that grows at 2 m/s.
2503
2503
  - `DEFAULT_REPUTATION_TIERS` (const): const DEFAULT_REPUTATION_TIERS: readonly ReputationTier[] — ⚠ undocumented
2504
+ - `DEFAULT_VARIANT_HYSTERESIS` (const): const DEFAULT_VARIANT_HYSTERESIS: 6 — Metres an instance must travel past a switch boundary before it changes level unless the model declares its own.
2504
2505
  - `DamageDirectionOptions` (interface): interface DamageDirectionOptions — Options for {@link createDamageDirectionTracker}.
2505
2506
  - `DamageDirectionSnapshot` (interface): interface DamageDirectionSnapshot — Serializable state for save/restore — the clock reading plus every live indicator.
2506
2507
  - `DamageDirectionTracker` (interface): interface DamageDirectionTracker — A serializable, allocation-aware brain that tracks where recent damage came from. Each registered hit becomes a directional indicator that fades over a fixed duration on the injected clock; {@link DamageDirectionTracker.active} reports the live ones with their eased current strength.
@@ -2654,6 +2655,10 @@
2654
2655
  - `ModelAssetRef` (interface): interface ModelAssetRef { url: string; anchor?: "center" | "origin"; dims?: ModelDims; collisionMesh?: CollisionMeshData; space?: AssetSpace; clips?: readonly string[] } — ⚠ undocumented · used by `createHostedWorldSessionAsync` (@jgengine/core/runtime/hostedWorldSession): Build a hosted session from an asynchronous persistence backend.
2655
2656
  - `ModelDims` (interface): interface ModelDims — Measured horizontal footprint, footprint center, and lowest Y of a model in model space.
2656
2657
  - `ModelNode` (interface): interface ModelNode — Generic named-socket reader for loaded 3D models. Walks a node tree (any object with `.name`, `.position`, and `.children` — structurally satisfied by `THREE.Object3D`) and collects the local offsets of nodes whose name marks an attachment point. Genre-agnostic: wire anchors on a pylon, muzzle/hand mounts on a character, hardpoints on a ship, seat/decal slots on furniture — anything an artist tags with an empty in the GLB. Pure data (no three.js import), so it lives in core.
2658
+ - `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB and how far it draws.
2659
+ - `ModelVariantLodConfig` (interface): interface ModelVariantLodConfig — Ordered mesh detail levels for one model, switched per instance by camera distance. The first level is the full mesh the model's own `url` names; every later level is a cheaper asset. A tree line uses this so near trees keep their full mesh while far ones draw a decimated copy of the same species.
2660
+ - `ModelVariantSelector` (interface): interface ModelVariantSelector — Per-instance distance switch over a model's detail levels: one answer per id, held across a boundary until the instance clears it.
2661
+ - `ModelVariantSelectorState` (interface): interface ModelVariantSelectorState — Plain JSON state of a {@link ModelVariantSelector}: each id's current level index.
2657
2662
  - `MountController` (class): class MountController — Mount / rideable control-transfer (issue #83). Registers rideables (each with one or more seats — a control seat drives, the rest ride) and tracks who is on what. It owns no camera or physics: game code reads `cameraTarget(riderId)` to point the follow camera at the mount, and `driveTarget(riderId)` to route that rider's {@link import("../physics/vehicleBody").AxisInput}-driven input at the mount's movement kit — the same seam a horse, a truck, or a shared multi-seat ship all plug into.
2658
2663
  - `MoveOrderPayload` (interface): interface MoveOrderPayload — Payload for a move order: go to a point and complete on arrival.
2659
2664
  - `MovementPose` (type): type MovementPose = "standing" | "crouch" | "prone" | "running" — ⚠ undocumented
@@ -3109,6 +3114,7 @@
3109
3114
  - `createLodScheduler` (function): function createLodScheduler(config: LodSchedulerConfig): LodScheduler — ⚠ undocumented
3110
3115
  - `createMarkerSet` (function): function createMarkerSet<TMeta = unknown>(now: () => number = Date.now): MarkerSet<TMeta> — ⚠ undocumented
3111
3116
  - `createMarkerSource` (function): function createMarkerSource<TEntity, TMarker extends MarkerView = MarkerView>(options: MarkerSourceOptions<TEntity, TMarker>): MarkerSource<TMarker> — Adapt a caller-owned array/store to an observable marker source. Projection is cached between source changes, so React reads do not copy the collection per frame. The caller retains ownership of entities, updates, persistence, and subscription scheduling.
3117
+ - `createModelVariantSelector` (function): function createModelVariantSelector(config: ModelVariantLodConfig): ModelVariantSelector — Builds the per-instance distance switch for a model's variant levels.
3112
3118
  - `createMountController` (function): function createMountController(): MountController — ⚠ undocumented
3113
3119
  - `createNavCrowd` (function): function createNavCrowd(options: NavCrowdOptions & { nav: NavRuntime }): NavCrowd — Compose {@link createNavRuntime} and {@link createRvoCrowd} into a moving crowd: plan on the mesh, follow the smoothed corridor, avoid each other. Deterministic — agents advance in insertion order and the avoidance solve breaks ties by slot, not chance.
3114
3120
  - `createNavGrid` (function): function createNavGrid(config: NavGridConfig): NavGrid — ⚠ undocumented
@@ -3213,6 +3219,7 @@
3213
3219
  - `leaveVehicle` (function): function leaveVehicle(ctx: GameContext, seats: VehicleSeats, riderId: string, options: LeaveVehicleOptions = {}): ExitVehicleResult | { ok: false; reason: "unknown_rider" | "unknown_vehicle" } — Apply dismount placement, restore rider movement/visibility and the configured camera rig.
3214
3220
  - `lineFormation` (function): function lineFormation(options: LineFormationOptions): FormationSlotGenerator — A single rank abreast, centered on the destination and facing forward — a skirmish line or a chorus row. Slots run left→right along the group's right axis.
3215
3221
  - `listTriggerActions` (function): function listTriggerActions(target?: TriggerSourceKind): TriggerActionDefinition[] — Every registered action, optionally filtered by target collection.
3222
+ - `lodBandIndex` (function): function lodBandIndex(bands: readonly { maxDistance: number }[], distance: number): number — Index of the band `distance` falls in, `-1` past the last band. Bands must already be sorted near to far. The scheduler and `createModelVariantSelector` both read distances through this one function, so simulation throttling and mesh detail cannot disagree about what band an id is in.
3216
3223
  - `mapLayerColor` (function): function mapLayerColor(tone: MapLayerTone | undefined): string — ⚠ undocumented
3217
3224
  - `markerCatalogId` (function): function markerCatalogId(marker: AuthoredObjectMarkerLike): string | null — Catalog id for a marker: first-class `catalogId` field, else `meta.catalogId` migration alias. Returns null when the marker is not an authored catalog prop (spawn, mob, generator, …).
3218
3225
  - `markerKindStyle` (function): function markerKindStyle(kind: string | undefined, styles: Record<string, MarkerKindStyle> = DEFAULT_MARKER_KINDS): MarkerKindStyle — ⚠ undocumented
@@ -3942,6 +3949,7 @@
3942
3949
  - `LodSchedulerConfig` (interface): interface LodSchedulerConfig { bands: readonly LodBand[]; beyondInterval?: number | null; stagger?: boolean } — ⚠ undocumented
3943
3950
  - `LodSchedulerState` (interface): interface LodSchedulerState — Plain JSON state of a {@link LodScheduler}: each id's accumulated seconds.
3944
3951
  - `createLodScheduler` (function): function createLodScheduler(config: LodSchedulerConfig): LodScheduler — ⚠ undocumented
3952
+ - `lodBandIndex` (function): function lodBandIndex(bands: readonly { maxDistance: number }[], distance: number): number — Index of the band `distance` falls in, `-1` past the last band. Bands must already be sorted near to far. The scheduler and `createModelVariantSelector` both read distances through this one function, so simulation throttling and mesh detail cannot disagree about what band an id is in.
3945
3953
 
3946
3954
  ## @jgengine/core/world/mapAnnotations
3947
3955
 
@@ -4030,6 +4038,15 @@
4030
4038
  - `TrackAxis` (interface): interface TrackAxis — A world-space segment defining a linear "track" minimap — points are projected onto the `from`→`to` axis.
4031
4039
  - `trackFraction` (function): function trackFraction(point: WorldXZ | readonly [number, number, number], axis: TrackAxis): number — Project a world point onto the `from`→`to` segment of `axis` and return the clamped 0..1 fraction of how far along the track it lies (0 at `from`, 1 at `to`). The linear analogue of `projectToMinimap` — feeds a horizontal corridor/route progress rail. Accepts XZ (`[x, z]`) or XYZ (`[x, y, z]`) points (index 0 = x, index 2 when length 3 else index 1 = z). A zero-length axis returns 0.
4032
4040
 
4041
+ ## @jgengine/core/world/modelLod
4042
+
4043
+ - `DEFAULT_VARIANT_HYSTERESIS` (const): const DEFAULT_VARIANT_HYSTERESIS: 6 — Metres an instance must travel past a switch boundary before it changes level unless the model declares its own.
4044
+ - `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB and how far it draws.
4045
+ - `ModelVariantLodConfig` (interface): interface ModelVariantLodConfig — Ordered mesh detail levels for one model, switched per instance by camera distance. The first level is the full mesh the model's own `url` names; every later level is a cheaper asset. A tree line uses this so near trees keep their full mesh while far ones draw a decimated copy of the same species.
4046
+ - `ModelVariantSelector` (interface): interface ModelVariantSelector — Per-instance distance switch over a model's detail levels: one answer per id, held across a boundary until the instance clears it.
4047
+ - `ModelVariantSelectorState` (interface): interface ModelVariantSelectorState — Plain JSON state of a {@link ModelVariantSelector}: each id's current level index.
4048
+ - `createModelVariantSelector` (function): function createModelVariantSelector(config: ModelVariantLodConfig): ModelVariantSelector — Builds the per-instance distance switch for a model's variant levels.
4049
+
4033
4050
  ## @jgengine/core/world/pathInstances
4034
4051
 
4035
4052
  - `PathInstance` (interface): interface PathInstance — One placed transform along a path: grounded position, facing yaw, and its ordinal + arc distance.