@jgengine/core 0.25.0 → 0.27.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 +109 -0
- package/dist/ai/rvo.d.ts +106 -0
- package/dist/ai/rvo.js +581 -0
- package/dist/ai/utilityBrain.d.ts +173 -0
- package/dist/ai/utilityBrain.js +277 -0
- package/dist/combat/cover.d.ts +169 -0
- package/dist/combat/cover.js +264 -0
- package/dist/combat/damageRuntime.d.ts +150 -0
- package/dist/combat/damageRuntime.js +208 -0
- package/dist/combat/damageTable.d.ts +116 -0
- package/dist/combat/damageTable.js +165 -0
- package/dist/combat/hitZones.d.ts +70 -0
- package/dist/combat/hitZones.js +77 -0
- package/dist/combat/magazine.d.ts +8 -0
- package/dist/combat/magazine.js +20 -2
- package/dist/combat/squadCombat.d.ts +167 -0
- package/dist/combat/squadCombat.js +557 -0
- package/dist/combat/squadTactics.d.ts +73 -0
- package/dist/combat/squadTactics.js +49 -0
- package/dist/combat/statusRuntime.d.ts +142 -0
- package/dist/combat/statusRuntime.js +236 -0
- package/dist/combat/weaponDefinition.d.ts +106 -0
- package/dist/combat/weaponDefinition.js +108 -0
- package/dist/combat/weaponSystem.d.ts +189 -0
- package/dist/combat/weaponSystem.js +469 -0
- package/dist/combat.d.ts +9 -0
- package/dist/combat.js +9 -0
- package/dist/devtools/devtools.d.ts +4 -2
- package/dist/devtools/devtools.js +2 -1
- package/dist/devtools/discover.d.ts +2 -1
- package/dist/devtools/discover.js +38 -0
- package/dist/devtools/frame.d.ts +0 -2
- package/dist/devtools/frame.js +1 -2
- package/dist/devtools/logBus.d.ts +58 -0
- package/dist/devtools/logBus.js +79 -0
- package/dist/devtools/logs.d.ts +6 -3
- package/dist/devtools/logs.js +17 -13
- package/dist/devtools/report.d.ts +120 -0
- package/dist/devtools/report.js +49 -0
- package/dist/devtools/reportEndpoint.d.ts +17 -0
- package/dist/devtools/reportEndpoint.js +7 -0
- package/dist/devtools/types.d.ts +48 -11
- package/dist/editor/document.js +210 -0
- package/dist/editor/environment.d.ts +48 -1
- package/dist/editor/environment.js +161 -0
- package/dist/editor/index.d.ts +6 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +54 -0
- package/dist/game/playableGame.d.ts +19 -2
- package/dist/gameplay.d.ts +9 -0
- package/dist/gameplay.js +9 -0
- package/dist/inventory/carryLoad.d.ts +48 -0
- package/dist/inventory/carryLoad.js +44 -0
- package/dist/inventory/equipment.d.ts +95 -0
- package/dist/inventory/equipment.js +169 -0
- package/dist/inventory/inventoryModel.d.ts +15 -6
- package/dist/inventory/inventoryModel.js +54 -5
- package/dist/inventory/shapedGrid.d.ts +26 -0
- package/dist/inventory/shapedGrid.js +47 -0
- package/dist/loot/lootGen.d.ts +186 -0
- package/dist/loot/lootGen.js +437 -0
- package/dist/loot/lootWeapon.d.ts +35 -0
- package/dist/loot/lootWeapon.js +70 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +92 -1
- package/dist/nav/laneGraph.d.ts +178 -0
- package/dist/nav/laneGraph.js +671 -0
- package/dist/nav/navCrowd.d.ts +138 -0
- package/dist/nav/navCrowd.js +412 -0
- package/dist/nav/navRuntime.d.ts +108 -0
- package/dist/nav/navRuntime.js +256 -0
- package/dist/nav/traffic.d.ts +223 -0
- package/dist/nav/traffic.js +919 -0
- package/dist/physics/kinematicVehicle.d.ts +36 -0
- package/dist/physics/kinematicVehicle.js +15 -1
- package/dist/physics/vehicleDamage.d.ts +136 -0
- package/dist/physics/vehicleDamage.js +214 -0
- package/dist/physics/vehicleParts.d.ts +62 -0
- package/dist/physics/vehicleParts.js +65 -0
- package/dist/progression/gearScore.d.ts +56 -0
- package/dist/progression/gearScore.js +59 -0
- package/dist/progression/skillTree.d.ts +129 -0
- package/dist/progression/skillTree.js +265 -0
- package/dist/progression/xpCurve.d.ts +60 -0
- package/dist/progression/xpCurve.js +141 -0
- package/dist/render/colorPipeline.d.ts +53 -0
- package/dist/render/colorPipeline.js +79 -0
- package/dist/render/lightProbes.d.ts +97 -0
- package/dist/render/lightProbes.js +221 -0
- package/dist/render/lookPreset.js +4 -4
- package/dist/render/physicalSky.d.ts +39 -0
- package/dist/render/physicalSky.js +89 -0
- package/dist/render/postProcessing.d.ts +40 -8
- package/dist/render/postProcessing.js +1 -1
- package/dist/render/skyOwnedLighting.d.ts +31 -0
- package/dist/render/skyOwnedLighting.js +46 -0
- package/dist/scene/builtinSceneKinds.js +2 -0
- package/dist/sensor/perception.d.ts +66 -5
- package/dist/sensor/perception.js +167 -42
- package/dist/sim/nearAgents.d.ts +68 -0
- package/dist/sim/nearAgents.js +152 -0
- package/dist/sim/needs.d.ts +60 -0
- package/dist/sim/needs.js +108 -0
- package/dist/sim/offScreen.d.ts +181 -0
- package/dist/sim/offScreen.js +450 -0
- package/dist/sim/routine.d.ts +79 -0
- package/dist/sim/routine.js +137 -0
- package/dist/visibility/visibilitySystem.js +21 -0
- package/dist/world/decalKind.d.ts +48 -0
- package/dist/world/decalKind.js +86 -0
- package/dist/world/features.d.ts +11 -1
- package/dist/world/features.js +1 -0
- package/dist/world/grassKind.d.ts +5 -0
- package/dist/world/grassKind.js +3 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world.d.ts +10 -1
- package/dist/world.js +9 -0
- package/package.json +1 -1
- package/skills/jgengine/api.md +59 -6
- package/skills/jgengine/capabilities.md +4 -0
- package/skills/jgengine-combat/SKILL.md +19 -0
- package/skills/jgengine-combat/api.md +246 -0
- package/skills/jgengine-combat/capabilities.md +112 -0
- package/skills/jgengine-combat/reference.md +32 -0
- package/skills/jgengine-gameplay/SKILL.md +39 -0
- package/skills/jgengine-gameplay/api.md +221 -7
- package/skills/jgengine-gameplay/capabilities.md +64 -3
- package/skills/jgengine-gameplay/reference-systems.md +118 -0
- package/skills/jgengine-world/SKILL.md +7 -1
- package/skills/jgengine-world/api.md +222 -8
- package/skills/jgengine-world/capabilities.md +42 -2
- package/skills/jgengine-world/recipes/npc-senses.md +8 -2
- package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
|
@@ -12,6 +12,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
12
12
|
|
|
13
13
|
- `createAntiOneShotPolicy` (function) · `import { createAntiOneShotPolicy } from "@jgengine/core/combat"`
|
|
14
14
|
|
|
15
|
+
## armor-bypass — ask whether a damage type ignores armor points
|
|
16
|
+
|
|
17
|
+
- `typeBypassesArmor` (function) · `import { typeBypassesArmor } from "@jgengine/core/combat"`
|
|
18
|
+
|
|
19
|
+
## armor-mitigation — read the fraction of a hit armor removes at a given armor value
|
|
20
|
+
|
|
21
|
+
- `armorMitigation` (function) · `import { armorMitigation } from "@jgengine/core/combat"`
|
|
22
|
+
|
|
15
23
|
## capture-bonus-levels — convert wild level and capture effectiveness into bonus levels
|
|
16
24
|
|
|
17
25
|
- `captureBonusLevels` (function) · `import { captureBonusLevels } from "@jgengine/core/stats/spawnLevelStats"`
|
|
@@ -40,6 +48,26 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
40
48
|
|
|
41
49
|
- `createComboPoints` (function) · `import { createComboPoints } from "@jgengine/core/combat"`
|
|
42
50
|
|
|
51
|
+
## cover-field — authored-geometry cover with a bounded nearest query and rebuild on edit
|
|
52
|
+
|
|
53
|
+
- `createCoverField` (function) · `import { createCoverField } from "@jgengine/core/combat"`
|
|
54
|
+
|
|
55
|
+
## cover-points-from-solids — derive stand-able cover positions from authored solid boxes
|
|
56
|
+
|
|
57
|
+
- `coverPointsFromSolids` (function) · `import { coverPointsFromSolids } from "@jgengine/core/combat"`
|
|
58
|
+
|
|
59
|
+
## cover-points-near — bounded nearest-cover query
|
|
60
|
+
|
|
61
|
+
- `coverPointsNear` (function) · `import { coverPointsNear } from "@jgengine/core/combat"`
|
|
62
|
+
|
|
63
|
+
## cover-score — threat-driven cover point scoring
|
|
64
|
+
|
|
65
|
+
- `scoreCoverPoint` (function) · `import { scoreCoverPoint } from "@jgengine/core/combat"`
|
|
66
|
+
|
|
67
|
+
## cover-select — select cover by threat, role range and flank angle
|
|
68
|
+
|
|
69
|
+
- `selectCover` (function) · `import { selectCover } from "@jgengine/core/combat"`
|
|
70
|
+
|
|
43
71
|
## damage-clamp — cap incoming damage per hit at a configured maximum
|
|
44
72
|
|
|
45
73
|
- `createDamageClamp` (function) · `import { createDamageClamp } from "@jgengine/core/combat"`
|
|
@@ -60,6 +88,42 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
60
88
|
|
|
61
89
|
- `createDamagePipeline` (function) · `import { createDamagePipeline } from "@jgengine/core/combat"`
|
|
62
90
|
|
|
91
|
+
## damage-runtime — resolve hits through a damage table, hit zones, and armor, and tick data-defined statuses with declared stacking
|
|
92
|
+
|
|
93
|
+
- `createDamageRuntime` (function) · `import { createDamageRuntime } from "@jgengine/core/combat"`
|
|
94
|
+
|
|
95
|
+
## damage-table — score a data-defined damage type against a resistance class, with the outcome readable as a table
|
|
96
|
+
|
|
97
|
+
- `typeMultiplier` (function) · `import { typeMultiplier } from "@jgengine/core/combat"`
|
|
98
|
+
|
|
99
|
+
## damage-table-format — render damage-table rows as fixed-width lines for a probe, a CLI dump, or a test
|
|
100
|
+
|
|
101
|
+
- `formatDamageTable` (function) · `import { formatDamageTable } from "@jgengine/core/combat"`
|
|
102
|
+
|
|
103
|
+
## damage-table-read — read a damage table as rows of type × resistance × armor mitigation, with final damage per base
|
|
104
|
+
|
|
105
|
+
- `readDamageTable` (function) · `import { readDamageTable } from "@jgengine/core/combat"`
|
|
106
|
+
|
|
107
|
+
## damage-table-resolve — resolve a damage type against a resistance class and armor into a multiplier, mitigation, and final damage
|
|
108
|
+
|
|
109
|
+
- `resolveTableDamage` (function) · `import { resolveTableDamage } from "@jgengine/core/combat"`
|
|
110
|
+
|
|
111
|
+
## damage-table-validate — reject a damage table with duplicate types or non-finite multipliers before combat runs
|
|
112
|
+
|
|
113
|
+
- `assertDamageTable` (function) · `import { assertDamageTable } from "@jgengine/core/combat"`
|
|
114
|
+
|
|
115
|
+
## damage-target-unknown — reject a hit on a target with no registered resistance or armor profile
|
|
116
|
+
|
|
117
|
+
- `UnknownTargetProfileError` (class) · `import { UnknownTargetProfileError } from "@jgengine/core/combat"`
|
|
118
|
+
|
|
119
|
+
## damage-type-duplicate — reject a damage table that defines one type twice
|
|
120
|
+
|
|
121
|
+
- `DuplicateDamageTypeError` (class) · `import { DuplicateDamageTypeError } from "@jgengine/core/combat"`
|
|
122
|
+
|
|
123
|
+
## damage-type-unknown — reject a hit whose damage type the table does not define instead of dealing 1×
|
|
124
|
+
|
|
125
|
+
- `UnknownDamageTypeError` (class) · `import { UnknownDamageTypeError } from "@jgengine/core/combat"`
|
|
126
|
+
|
|
63
127
|
## death-system — resolve entity death and its on-death consequences
|
|
64
128
|
|
|
65
129
|
- `createDeathSystem` (function) · `import { createDeathSystem } from "@jgengine/core/combat"`
|
|
@@ -132,6 +196,22 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
132
196
|
|
|
133
197
|
- `createAbilityKit` (function) · `import { createAbilityKit } from "@jgengine/core/combat"`
|
|
134
198
|
|
|
199
|
+
## hit-zone — resolve a hit zone into one damage multiplier, crit by zone with an injected RNG
|
|
200
|
+
|
|
201
|
+
- `resolveHitZone` (function) · `import { resolveHitZone } from "@jgengine/core/combat"`
|
|
202
|
+
|
|
203
|
+
## hit-zone-multiplier — quote a zone's multiplier for a known crit outcome without rolling
|
|
204
|
+
|
|
205
|
+
- `hitZoneMultiplier` (function) · `import { hitZoneMultiplier } from "@jgengine/core/combat"`
|
|
206
|
+
|
|
207
|
+
## hit-zone-unknown — reject a hit zone table whose named default does not exist
|
|
208
|
+
|
|
209
|
+
- `UnknownHitZoneError` (class) · `import { UnknownHitZoneError } from "@jgengine/core/combat"`
|
|
210
|
+
|
|
211
|
+
## hit-zone-validate — reject a hit-zone table with duplicate zones or a missing default
|
|
212
|
+
|
|
213
|
+
- `assertHitZoneTable` (function) · `import { assertHitZoneTable } from "@jgengine/core/combat"`
|
|
214
|
+
|
|
135
215
|
## hitscan-shot — aim a hitscan shot (shoot a gun): resolve origin and direction from the eye or muzzle for a raycast
|
|
136
216
|
|
|
137
217
|
- `resolveShot` (function) · `import { resolveShot } from "@jgengine/core/combat"`
|
|
@@ -199,6 +279,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
199
279
|
|
|
200
280
|
- `resolveSpawnStats` (function) · `import { resolveSpawnStats } from "@jgengine/core/stats/spawnLevelStats"`
|
|
201
281
|
|
|
282
|
+
## squad-combat — threat-driven cover, flanking, suppression, retreat and role-driven fire discipline
|
|
283
|
+
|
|
284
|
+
- `createSquadCombat` (function) · `import { createSquadCombat } from "@jgengine/core/combat"`
|
|
285
|
+
|
|
286
|
+
## squad-roles — deterministic role assignment over a caller-authored composition
|
|
287
|
+
|
|
288
|
+
- `assignSquadRoles` (function) · `import { assignSquadRoles } from "@jgengine/core/combat"`
|
|
289
|
+
|
|
202
290
|
## stagger-meter — accumulate an ailment buildup (bleed, freeze) until it procs
|
|
203
291
|
|
|
204
292
|
- `createBuildupMeter` (function) · `import { createBuildupMeter } from "@jgengine/core/combat"`
|
|
@@ -236,6 +324,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
236
324
|
|
|
237
325
|
- `createEffectSystem` (function) · `import { createEffectSystem } from "@jgengine/core/combat"`
|
|
238
326
|
|
|
327
|
+
## status-runtime — tick data-defined statuses (burn, shock, slow, corrode) with declared stacking rules and per-tick effects
|
|
328
|
+
|
|
329
|
+
- `createStatusRuntime` (function) · `import { createStatusRuntime } from "@jgengine/core/combat"`
|
|
330
|
+
|
|
331
|
+
## status-unknown — reject a status id the runtime has no definition for
|
|
332
|
+
|
|
333
|
+
- `UnknownStatusError` (class) · `import { UnknownStatusError } from "@jgengine/core/combat"`
|
|
334
|
+
|
|
335
|
+
## threats-from-memory — turn perception memory into a bounded, ranked threat list
|
|
336
|
+
|
|
337
|
+
- `threatsFromMemory` (function) · `import { threatsFromMemory } from "@jgengine/core/combat"`
|
|
338
|
+
|
|
239
339
|
## threshold-crossings — emit ordered crossing events as a numeric source passes named thresholds with hysteresis and once/repeat policy
|
|
240
340
|
|
|
241
341
|
- `createThresholdTracker` (function) · `import { createThresholdTracker } from "@jgengine/core/combat"`
|
|
@@ -248,6 +348,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
248
348
|
|
|
249
349
|
- `vfxPresets` (const) · `import { vfxPresets } from "@jgengine/core/combat"`
|
|
250
350
|
|
|
351
|
+
## weapon-cadence — convert a rounds-per-minute weapon rate into the interval a fire gate takes
|
|
352
|
+
|
|
353
|
+
- `weaponIntervalMs` (function) · `import { weaponIntervalMs } from "@jgengine/core/combat"`
|
|
354
|
+
|
|
355
|
+
## weapon-definition-validate — reject a malformed weapon definition at construction with the field named
|
|
356
|
+
|
|
357
|
+
- `assertWeaponDefinition` (function) · `import { assertWeaponDefinition } from "@jgengine/core/combat"`
|
|
358
|
+
|
|
251
359
|
## weapon-feedback — turn weapon shots, recoil kick and impacts into per-tick feedback-mixer signals for camera shake, audio and rumble
|
|
252
360
|
|
|
253
361
|
- `createWeaponFeedbackSignals` (function) · `import { createWeaponFeedbackSignals } from "@jgengine/core/combat/weaponFeedback"`
|
|
@@ -267,3 +375,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
267
375
|
## weapon-runtime — compose cadence, magazine, a caller-owned raycast, and portable damage resolution into a fire controller
|
|
268
376
|
|
|
269
377
|
- `createWeaponRuntime` (function) · `import { createWeaponRuntime } from "@jgengine/core/combat"`
|
|
378
|
+
|
|
379
|
+
## weapon-system — run a data-defined weapon: fire modes, recoil and bloom, hitscan or ballistic projectiles, interrupted reloads and readable ammo, all resolving through the damage runtime
|
|
380
|
+
|
|
381
|
+
- `createWeaponSystem` (function) · `import { createWeaponSystem } from "@jgengine/core/combat"`
|
|
@@ -141,6 +141,27 @@ Genre systems layered over the same effects/projectiles/targeting/loot primitive
|
|
|
141
141
|
- **Resistance matrix** (`combat/resistance`) — a damage-category × target-property table distinct from `combat/attackTags` (those are defense tags). `resolveResistance(matrix, category, targetProperties)` → `{ verdict: "immune" | "resist" | "normal" | "vulnerable", multiplier, immune }`; `resistanceScale` returns just the multiplier (default `immune:0, resist:0.5, normal:1, vulnerable:2`, overridable). Immune on any property wins; resists stack multiplicatively. Sits over the `receive` gate — the handler multiplies its effect `amount` by the scale (Bloons lead/camo immunities, elemental RPG weaknesses).
|
|
142
142
|
- **Run draft** (`game/runDraft`, built on `world/scatterItems` `pickWeighted` + `stats/statModifiers`) — `createRunModifierStack(offers)` accumulates stacking picks (`add`, `count`, `atMax`, `total(stat)` aggregating adds × exponentiated multiplies, `apply(stats)` onto a `createStats` source). `createRunDraft({ offers, rng? })` adds the pause-flow: `present(n)` draws N distinct weighted offers (excluding maxed-out ones), `choose(id)` applies to the stack. Pause/resume is game-side (`ctx.time.pause()` before `present`, `play()` after `choose`). Vampire Survivors level-up picks, Hades boons, Risk of Rain stacking items.
|
|
143
143
|
|
|
144
|
+
## Damage model, armor, crits, and statuses
|
|
145
|
+
|
|
146
|
+
`createDamageRuntime` (`combat/damageRuntime`) is the stateful in-process composition of the table, the zones, the armor curve, and carried statuses. A number reads as `base × zone multiplier × type multiplier × (1 − armor mitigation)`; `hit()` returns every stage, `tick()` ticks statuses through the same table and armor, `timeToKill()` quotes a fight, and `table()` reads the whole chart. `snapshot()`/`restore()` carry registered targets and carried statuses; `retune()` swaps table, zones, or definitions without dropping either. For multiplayer authority, keep the pure stages (`resolveMatchup`, `resolveReceivedDamage`, `resolveStatusApplication`) and replicate their results instead.
|
|
147
|
+
|
|
148
|
+
- **Type × resistance table** (`combat/damageTable`) — `{ types: [{ id, vs: { armor: 2, flesh: 0.5 }, bypassArmor? }], armor: { halfValue, maxMitigation }, defaultMultiplier? }`. `typeMultiplier` reads one pair; `resolveTableDamage` folds type, armor, and base into a row; `readDamageTable` returns every row at given armor values and `formatDamageTable` prints them. Armor uses `armor / (armor + halfValue)` — armor equal to `halfValue` halves the hit — so a designer reads the number rather than guessing a percentage per point. Unknown types throw; unknown resistance classes use the default.
|
|
149
|
+
- **Hit zones and crits** (`combat/hitZones`) — `{ zones: [{ id, multiplier?, critChance?, critMultiplier? }], defaultZone, critChance?, critMultiplier? }`. `resolveHitZone(table, zone, rng)` rolls once and returns **one** multiplier: a crit replaces the zone's normal multiplier (head 1.5× normal, 2.5× crit → 2.5×, never 3.75×). `hitZoneMultiplier(table, zone, crit)` quotes the same number without a roll, which is what `timeToKill({ crit: true })` uses.
|
|
150
|
+
- **Statuses** (`combat/statusRuntime`) — `createStatusRuntime({ statuses, rng?, applyDamage? })`; each `StatusDef` declares `{ id, stack, durationMs, intervalMs?, magnitude?, effect?, chance? }`. `stack` is the existing `StatusStackPolicy` (`refresh` / `stack` to `max` / `replace` / `ignore`), and it is the only thing that changes stacks: a tick deals `magnitude × stacks`, so two burns at 4 per tick deal 8, never 16. `effect` is `{ kind: "damage", channel }` (routed through `applyDamage`, so resistances and armor apply) or `{ kind: "stat", stat }` (read with `statDelta`, e.g. negative `moveSpeed` for slow or negative `armor` for corrode). Reapplication restarts the tick clock instead of banking a burst; ticks per call are bounded and the remainder carries. `dotField` stays the cheap no-stacking scheduler.
|
|
151
|
+
|
|
152
|
+
## Weapons as data
|
|
153
|
+
|
|
154
|
+
`createWeaponSystem` (`combat/weaponSystem`) runs one `WeaponDefinition` (`combat/weaponDefinition`): plain JSON with `roundsPerMinute`, `fireMode` (`single` / `auto` / `burst` + `burst.count`), `delivery` (`hitscan` + `maxRange`, or `projectile` + `speed` / `gravity` / `drag` / `lifetime`), optional `magazine`, `handling` (the same recoil bloom and ADS tuning `createWeaponHandling` takes), `damage` (`type`, `base`, optional `statuses`), optional `falloff`, `pellets`, `maxInFlight` and `autoReload`. Nothing in it is a function, so a generator rolls it and a save stores it; `assertWeaponDefinition` rejects a bad roll at construction with the field named.
|
|
155
|
+
|
|
156
|
+
- **Loop.** `setAim(yaw, pitch)` from look input, `setTrigger(down)`, `tick(dt, stance)` → `{ shots, impacts }`. A press survives to the next tick that can use it; a tap does not repeat. Recoil is applied to the aim before the shot leaves, so `record.directions` and the world query see the kicked vector, and `aim()` reports it with recovery folded in.
|
|
157
|
+
- **Spread and bounds.** Each pellet samples the cone uniformly over solid angle from the injected `rng`; zero spread draws nothing. One tick fires at most `maxShotsPerTick` (default 4, clamp 16) catch-up shots and keeps the remaining budget, so a stalled frame cannot turn into a long burst.
|
|
158
|
+
- **Delivery.** A hitscan calls your `raycast(from, direction, maxRange)` on the firing tick. A projectile is stepped with semi-implicit Euler (drag, then gravity), swept as one segment per tick so it cannot tunnel, and resolved when `sweep(from, to)` reports a contact. Both build the same `DamageRuntimeHitInput` for `createDamageRuntime().hit()`, so zone, type, armor, status rolls and provenance are identical; only the distance differs. Falloff scales `base` by the distance the pellet actually travelled.
|
|
159
|
+
- **Live pellets are capped** by `maxInFlight` (default 32, max 256); excess pellets of a shot are counted in `stats().pelletsDropped` rather than queued.
|
|
160
|
+
- **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.
|
|
161
|
+
- **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.
|
|
162
|
+
- **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`.
|
|
163
|
+
- **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.
|
|
164
|
+
|
|
144
165
|
## Encounter sequencing
|
|
145
166
|
|
|
146
167
|
`combat/encounterSequence` is the genre-agnostic hierarchical sequencer behind arenas, raids, tutorials, races, defense phases, and scripted objectives — a serializable state machine over caller-defined phase `data` with **no built-in wave/boss concept**. State is plain JSON (the working tree, per-node runtime, active path, elapsed) so a mid-encounter save round-trips; the predicate/spawn registries live in the config you re-supply alongside it, the way `advanceSpawnDirector` pairs config with state.
|
|
@@ -149,3 +170,14 @@ Genre systems layered over the same effects/projectiles/targeting/loot primitive
|
|
|
149
170
|
- **Completion predicates** — `completion: { kind, params }` names a registered evaluator; `BUILTIN_COMPLETION_PREDICATES` ships `immediate`, `manual`/`flag`, `timer` (phase seconds), `cleared` (tagged count drained), `metric` (live threshold), `event` (cumulative count), and `quorum` (N of nested predicates). Signals (`counts`/`metrics`/`events`/`flags`) come in through `ctx.signals`; evaluation is deterministic. Register your own via `config.predicates`.
|
|
150
171
|
- **Spawn providers** — spawning is one optional node adapter: `spawn: { kind, params }` invoked on enter. `BUILTIN_SPAWN_PROVIDERS.points` reads a named authored scene point list from `ctx.spawnPoints` (coordinates stay in the scene, referenced by key — never embedded in the sequence); `list` echoes inline requests. Register catalog/director-backed providers via `config.spawnProviders`.
|
|
151
172
|
- **Injection & scripted control** — `injectPhase(state, phase, { after | before | childOf })` inserts a phase or branch at runtime (e.g. a boss after the final wave) into the serializable tree without disturbing active/completed nodes; `forceCompletePhase` completes the active phase regardless of its predicate for scripted skips and failure/retry, restoring the authored predicate afterward.
|
|
173
|
+
|
|
174
|
+
## Enemy squad AI
|
|
175
|
+
|
|
176
|
+
`createSquadCombat` (`combat/squadCombat`) is the combat half of an NPC squad: it reads the perception memory the game already keeps (`createPerception`, B1.4) and moves through the nav crowd it already owns (`createNavCrowd`, B1.2). It never sees a global player position — a member reacts to what its own memory or a squadmate's shared sighting says, so losing contact means losing the fight.
|
|
177
|
+
|
|
178
|
+
- **Cover comes from geometry.** `createCoverField({ solids })` turns authored solid boxes (a `WorldSolid` list drops in) into stand points on every vertical face, clear of the corners and filtered by `walkable: (p) => nav.findPolygon(p) !== null`; `rebuild(solids)` re-derives after an editor edit. A game with its own spatial index supplies the narrower `CoverSource` instead: `near(position, radius, limit)`, bounded per call. Never author a coordinate list for cover.
|
|
179
|
+
- **Selection is by threat.** `selectCover(points, query)` scores each point on `hidden` (confidence-weighted share of remembered threats whose line of sight it blocks, including a body-sized footprint), the role's `preferredRange`, its `flankAngle` around the threat, distance, a squadmate's claim, and whether the walk to it is a detour. A point that hides from one of two shooters is not cover; a memory below `minConfidence` stops steering.
|
|
180
|
+
- **Roles are content.** `assignSquadRoles(members, { roles })` maps names and counts to members deterministically; a role carries `preferredRange`, `flankAngle`, `retreatBelow`, `holdCover`, `discipline` (hold/peek/peek distance/fire interval) and `cover` weights. The engine has no role list, and `"assault"`, `"support"`, `"flanker"` exist only in the game's composition.
|
|
181
|
+
- **Behavior over a tick.** A member picks cover, walks to it, leans out for `peekSeconds` to shoot (`peekSpot` steps around the corner it hides behind), then drops back. `underFire(memberId, amount)` raises suppression, which pins the member to its cover and cancels its peeks until the meter decays. Below a role's `retreatBelow`, the member falls back to the point furthest from the threat. After `relocateAfterSeconds` of continuous exposure it stops trusting its point and moves, which is what breaks a long spell in the open.
|
|
182
|
+
- **Wiring.** `engage(memberId, targetId, distance)` is the paced shot: resolve damage in your own pipeline, call `underFire` on the victim, push a `damage` stimulus at the shooter's position so a member learns about a shooter it cannot see. `healthOf` reads authoritative health, `teamOf` decides who is hostile, `blocked` is your collision layer. `snapshot()`/`restore()` carry member decisions, the crowd and the tuning, so a save mid-fight resumes the same fight.
|
|
183
|
+
- **Evidence.** `combat/squadCombat.soak.test.ts` is the reference run: 2v4 in an authored arena, 90 s, exposure measured every tick against the walls from live positions. Worst exposure 2.33 s against a 3 s bar, 0 of 38 exposure spells over 3 s, p50 0.05 ms per tick. Copy its shape (authored boxes, nav grid with the same holes, perception observing live positions) rather than trusting a flag.
|
|
@@ -38,6 +38,37 @@ Pins consume no choice RNG and must satisfy the same pool and constraints;
|
|
|
38
38
|
incompatible pins backtrack earlier unpinned choices or return `unsatisfiable`
|
|
39
39
|
within the configured attempt and pick budgets.
|
|
40
40
|
|
|
41
|
+
Roll whole named gear from an authored catalog with `rollLoot`
|
|
42
|
+
(`loot/lootGen`): bases with mount slots, parts, affix pools, weighted rarity
|
|
43
|
+
tiers, manufacturer stat personalities and a level curve, all caller data.
|
|
44
|
+
`validateLootItem` names each defect, `compareLootItems` returns per-stat deltas
|
|
45
|
+
and a weighted score, and `lootWeaponDefinition` (`loot/lootWeapon`) projects a
|
|
46
|
+
rolled item onto a `WeaponDefinition` for `createWeaponSystem`. `createLootRoller`
|
|
47
|
+
counts drops per rarity for a debug probe and snapshots its counters. Sample
|
|
48
|
+
catalog, distribution check and generated-weapon proof are in
|
|
49
|
+
[reference-systems.md](reference-systems.md#procedural-gear).
|
|
50
|
+
|
|
51
|
+
Resolve an authored XP curve with `defineXpCurve` (`progression/xpCurve`): one growth rule
|
|
52
|
+
(`flat`, `linear`, `power`, `geometric`, `table`), a `maxLevel` cap and a stated `rule` string;
|
|
53
|
+
`table()` and `totalXpFor()` read the same data, and `track()` hands those thresholds to
|
|
54
|
+
`leveling`. Spend a character's points in `createSkillTree` (`progression/skillTree`): nodes carry
|
|
55
|
+
a point `cost`, `requires`, `excludes`, `passives` and `actives`, `synergies` pay off two or more
|
|
56
|
+
ranked nodes together, `respec()` refunds exactly the points spent, and `resolved()` returns the
|
|
57
|
+
`StatModifierSet` plus ability-slot-shaped actives to feed `createStats` / `createAbilityKit`.
|
|
58
|
+
Score a whole loadout with `gearScore` (`progression/gearScore`) on the same weight policy as
|
|
59
|
+
`compareLootItems`. [reference-systems.md](reference-systems.md#progression-and-gear-score) shows
|
|
60
|
+
the data shapes and a two-build comparison.
|
|
61
|
+
|
|
62
|
+
Wear gear with `createEquipment` (`inventory/equipment`): slot defs own accepted kinds and the slots
|
|
63
|
+
they block, item defs own the slots an item may occupy, a filled slot swaps and hands back the
|
|
64
|
+
replaced item, and `state()` / `restore()` round-trip the worn set. Weigh a character with
|
|
65
|
+
`characterLoad` (`inventory/carryLoad`), which turns carried inventories plus worn gear into the
|
|
66
|
+
shared encumbrance bands (`moveMultiplier`, sprint/jump gates, `remaining`), so `canCarry` gates a
|
|
67
|
+
pickup. Declare a stash as its own inventory (`tier: "banked"`) and move items with
|
|
68
|
+
`InventorySet.move`; `snapshot()` / `restore()` save both as detached data. The bag and stash render
|
|
69
|
+
through `<InventoryGrid>` with `<EquipmentSlots>` above it. See
|
|
70
|
+
[inventory, equipment and weight](reference-systems.md#inventory-equipment-and-weight).
|
|
71
|
+
|
|
41
72
|
Validate generated quest catalogs with `validateQuestCatalog` (`game/questCatalog`).
|
|
42
73
|
Follow the [quest authoring recipe](recipes/quest-authoring.md) for bounded batches,
|
|
43
74
|
canonical facts, external unlock declarations, and precise repair diagnostics.
|
|
@@ -63,6 +94,14 @@ bounded batches, repair located issues and require explicit semantic approval.
|
|
|
63
94
|
|
|
64
95
|
Prefer `defineSystem` and `defineGame({ systems })` for scheduled capabilities. Keep boot/join in the game loop only when it is truly lifecycle glue. System ordering, frequency, and serialization are explicit; avoid a giant per-frame callback.
|
|
65
96
|
|
|
97
|
+
## Days and off-screen agents
|
|
98
|
+
|
|
99
|
+
`resolveRoutine` (`sim/routine`) resolves an authored day (home, work, meal) against the world clock: injected travel time makes each stop depart early enough to arrive on time, and an arrival that runs late slips instead of teleporting. The timeline is pure, so a save, a forecast and a live agent read the same day.
|
|
100
|
+
|
|
101
|
+
`createOffScreenWorld` (`sim/offScreen`) keeps every resident either near or cheap. Near residents spawn into your own simulation through `NearAgents`; far ones are a position, a routine phase and a needs record that only move when the clock crosses a routine boundary — no navigation, no needs tick, no utility decision. The distance radius, optional cost model and per-tick switch budget are game config. Crossing back in spawns the resident where the cheap state says it is, with its schedule progress and needs intact, so it cannot pop in at the origin.
|
|
102
|
+
|
|
103
|
+
`createNearAgents` (`sim/nearAgents`) is the default near half: a nav crowd with a need set and a utility brain per agent, counting its decisions for tests and probes. Give it the same need definitions the off-screen world decays, or the two halves disagree at the boundary.
|
|
104
|
+
|
|
66
105
|
For staffed production, use `stationOutputRate` (`work/staffedStation`) to resolve
|
|
67
106
|
caller-owned worker stats and efficiency, then feed the rate to `tickProduction`.
|
|
68
107
|
Keep staffing and production progress in the same serializable store; the
|