@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.
Files changed (133) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/dist/ai/rvo.d.ts +106 -0
  3. package/dist/ai/rvo.js +581 -0
  4. package/dist/ai/utilityBrain.d.ts +173 -0
  5. package/dist/ai/utilityBrain.js +277 -0
  6. package/dist/combat/cover.d.ts +169 -0
  7. package/dist/combat/cover.js +264 -0
  8. package/dist/combat/damageRuntime.d.ts +150 -0
  9. package/dist/combat/damageRuntime.js +208 -0
  10. package/dist/combat/damageTable.d.ts +116 -0
  11. package/dist/combat/damageTable.js +165 -0
  12. package/dist/combat/hitZones.d.ts +70 -0
  13. package/dist/combat/hitZones.js +77 -0
  14. package/dist/combat/magazine.d.ts +8 -0
  15. package/dist/combat/magazine.js +20 -2
  16. package/dist/combat/squadCombat.d.ts +167 -0
  17. package/dist/combat/squadCombat.js +557 -0
  18. package/dist/combat/squadTactics.d.ts +73 -0
  19. package/dist/combat/squadTactics.js +49 -0
  20. package/dist/combat/statusRuntime.d.ts +142 -0
  21. package/dist/combat/statusRuntime.js +236 -0
  22. package/dist/combat/weaponDefinition.d.ts +106 -0
  23. package/dist/combat/weaponDefinition.js +108 -0
  24. package/dist/combat/weaponSystem.d.ts +189 -0
  25. package/dist/combat/weaponSystem.js +469 -0
  26. package/dist/combat.d.ts +9 -0
  27. package/dist/combat.js +9 -0
  28. package/dist/devtools/devtools.d.ts +4 -2
  29. package/dist/devtools/devtools.js +2 -1
  30. package/dist/devtools/discover.d.ts +2 -1
  31. package/dist/devtools/discover.js +38 -0
  32. package/dist/devtools/frame.d.ts +0 -2
  33. package/dist/devtools/frame.js +1 -2
  34. package/dist/devtools/logBus.d.ts +58 -0
  35. package/dist/devtools/logBus.js +79 -0
  36. package/dist/devtools/logs.d.ts +6 -3
  37. package/dist/devtools/logs.js +17 -13
  38. package/dist/devtools/report.d.ts +120 -0
  39. package/dist/devtools/report.js +49 -0
  40. package/dist/devtools/reportEndpoint.d.ts +17 -0
  41. package/dist/devtools/reportEndpoint.js +7 -0
  42. package/dist/devtools/types.d.ts +48 -11
  43. package/dist/editor/document.js +210 -0
  44. package/dist/editor/environment.d.ts +48 -1
  45. package/dist/editor/environment.js +161 -0
  46. package/dist/editor/index.d.ts +6 -2
  47. package/dist/editor/index.js +1 -1
  48. package/dist/editor/types.d.ts +54 -0
  49. package/dist/game/playableGame.d.ts +19 -2
  50. package/dist/gameplay.d.ts +9 -0
  51. package/dist/gameplay.js +9 -0
  52. package/dist/inventory/carryLoad.d.ts +48 -0
  53. package/dist/inventory/carryLoad.js +44 -0
  54. package/dist/inventory/equipment.d.ts +95 -0
  55. package/dist/inventory/equipment.js +169 -0
  56. package/dist/inventory/inventoryModel.d.ts +15 -6
  57. package/dist/inventory/inventoryModel.js +54 -5
  58. package/dist/inventory/shapedGrid.d.ts +26 -0
  59. package/dist/inventory/shapedGrid.js +47 -0
  60. package/dist/loot/lootGen.d.ts +186 -0
  61. package/dist/loot/lootGen.js +437 -0
  62. package/dist/loot/lootWeapon.d.ts +35 -0
  63. package/dist/loot/lootWeapon.js +70 -0
  64. package/dist/meta/changelog.d.ts +1 -1
  65. package/dist/meta/changelog.js +92 -1
  66. package/dist/nav/laneGraph.d.ts +178 -0
  67. package/dist/nav/laneGraph.js +671 -0
  68. package/dist/nav/navCrowd.d.ts +138 -0
  69. package/dist/nav/navCrowd.js +412 -0
  70. package/dist/nav/navRuntime.d.ts +108 -0
  71. package/dist/nav/navRuntime.js +256 -0
  72. package/dist/nav/traffic.d.ts +223 -0
  73. package/dist/nav/traffic.js +919 -0
  74. package/dist/physics/kinematicVehicle.d.ts +36 -0
  75. package/dist/physics/kinematicVehicle.js +15 -1
  76. package/dist/physics/vehicleDamage.d.ts +136 -0
  77. package/dist/physics/vehicleDamage.js +214 -0
  78. package/dist/physics/vehicleParts.d.ts +62 -0
  79. package/dist/physics/vehicleParts.js +65 -0
  80. package/dist/progression/gearScore.d.ts +56 -0
  81. package/dist/progression/gearScore.js +59 -0
  82. package/dist/progression/skillTree.d.ts +129 -0
  83. package/dist/progression/skillTree.js +265 -0
  84. package/dist/progression/xpCurve.d.ts +60 -0
  85. package/dist/progression/xpCurve.js +141 -0
  86. package/dist/render/colorPipeline.d.ts +53 -0
  87. package/dist/render/colorPipeline.js +79 -0
  88. package/dist/render/lightProbes.d.ts +97 -0
  89. package/dist/render/lightProbes.js +221 -0
  90. package/dist/render/lookPreset.js +4 -4
  91. package/dist/render/physicalSky.d.ts +39 -0
  92. package/dist/render/physicalSky.js +89 -0
  93. package/dist/render/postProcessing.d.ts +40 -8
  94. package/dist/render/postProcessing.js +1 -1
  95. package/dist/render/skyOwnedLighting.d.ts +31 -0
  96. package/dist/render/skyOwnedLighting.js +46 -0
  97. package/dist/scene/builtinSceneKinds.js +2 -0
  98. package/dist/sensor/perception.d.ts +66 -5
  99. package/dist/sensor/perception.js +167 -42
  100. package/dist/sim/nearAgents.d.ts +68 -0
  101. package/dist/sim/nearAgents.js +152 -0
  102. package/dist/sim/needs.d.ts +60 -0
  103. package/dist/sim/needs.js +108 -0
  104. package/dist/sim/offScreen.d.ts +181 -0
  105. package/dist/sim/offScreen.js +450 -0
  106. package/dist/sim/routine.d.ts +79 -0
  107. package/dist/sim/routine.js +137 -0
  108. package/dist/visibility/visibilitySystem.js +21 -0
  109. package/dist/world/decalKind.d.ts +48 -0
  110. package/dist/world/decalKind.js +86 -0
  111. package/dist/world/features.d.ts +11 -1
  112. package/dist/world/features.js +1 -0
  113. package/dist/world/grassKind.d.ts +5 -0
  114. package/dist/world/grassKind.js +3 -0
  115. package/dist/world/terraform.d.ts +8 -0
  116. package/dist/world.d.ts +10 -1
  117. package/dist/world.js +9 -0
  118. package/package.json +1 -1
  119. package/skills/jgengine/api.md +59 -6
  120. package/skills/jgengine/capabilities.md +4 -0
  121. package/skills/jgengine-combat/SKILL.md +19 -0
  122. package/skills/jgengine-combat/api.md +246 -0
  123. package/skills/jgengine-combat/capabilities.md +112 -0
  124. package/skills/jgengine-combat/reference.md +32 -0
  125. package/skills/jgengine-gameplay/SKILL.md +39 -0
  126. package/skills/jgengine-gameplay/api.md +221 -7
  127. package/skills/jgengine-gameplay/capabilities.md +64 -3
  128. package/skills/jgengine-gameplay/reference-systems.md +118 -0
  129. package/skills/jgengine-world/SKILL.md +7 -1
  130. package/skills/jgengine-world/api.md +222 -8
  131. package/skills/jgengine-world/capabilities.md +42 -2
  132. package/skills/jgengine-world/recipes/npc-senses.md +8 -2
  133. package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
@@ -7,6 +7,7 @@ import type { MinimapBakeBounds } from "../world/minimapBake.js";
7
7
  import type { TerraformSnapshot } from "../world/terraform.js";
8
8
  import type { EditorGridLayer } from "./grid.js";
9
9
  import type { EnvironmentSource } from "../render/environment.js";
10
+ import type { IrradianceBakeSettings, IrradianceVolume } from "../render/lightProbes.js";
10
11
  import type { PointLightingConfig } from "../game/playableGame.js";
11
12
  /** Runtime point-light shape reused by authored editor light markers. */
12
13
  export type { PointLightingConfig } from "../game/playableGame.js";
@@ -196,6 +197,17 @@ export interface EditorBake {
196
197
  }
197
198
  /** Named sky look stored on the scene document; matches runtime `SkyEnvironmentConfig.preset`. */
198
199
  export type EditorSkyPreset = "day" | "dusk" | "night";
200
+ /**
201
+ * An irradiance volume authored on the scene document: a grid of baked SH probes over a box, plus
202
+ * the bake settings that produced them so a scene can re-bake without the editor session.
203
+ * @capability light-probes author a baked irradiance volume on the scene document
204
+ */
205
+ export interface EditorLightProbe extends IrradianceVolume {
206
+ /** Label shown in the lighting workspace. */
207
+ label?: string;
208
+ /** The authored bake inputs; absent means the volume was authored elsewhere and only the probe data matters. */
209
+ bake?: IrradianceBakeSettings;
210
+ }
199
211
  /**
200
212
  * Linear distance fog authored on the scene document — serializable subset of sky/backdrop fog.
201
213
  * Absent fields keep engine defaults when the document is applied at runtime.
@@ -205,6 +217,40 @@ export interface EditorFogConfig {
205
217
  near?: number;
206
218
  far?: number;
207
219
  }
220
+ /**
221
+ * Authored colour grading for the scene document's final image. The editor bakes a small
222
+ * display-space LUT from these controls and the shell applies it after tone mapping, so a game's
223
+ * grade is authored data like its sky, not a hardcoded pass in the game's config.
224
+ *
225
+ * The three band colours are display swatches (`#rrggbb`) that the pixels of that band are pulled
226
+ * toward, by `tint`. `saturation` and `contrast` are applied before the bands. Baking at a small cube
227
+ * size keeps the whole grade — including the LUT — a few hundred bytes of JSON.
228
+ */
229
+ export interface EditorColorGrading {
230
+ /** Cube edge length the runtime bakes, 2..16. Default 8 — small enough to read, smooth enough for a grade. */
231
+ size?: number;
232
+ /** Swatch shadows are tinted toward, e.g. `#2a3a52` for a cool night. Absent leaves shadows alone. */
233
+ shadows?: string;
234
+ /** Swatch midtones are tinted toward. Absent leaves midtones alone. */
235
+ midtones?: string;
236
+ /** Swatch highlights are tinted toward, e.g. `#ffe9c4` for a warm sun. Absent leaves highlights alone. */
237
+ highlights?: string;
238
+ /** How far each band is pulled toward its swatch, 0..1. Default 0.45. */
239
+ tint?: number;
240
+ /** Display saturation around luma. Default 1. */
241
+ saturation?: number;
242
+ /** Display contrast around mid-grey. Default 1. */
243
+ contrast?: number;
244
+ /** Clamped auto-exposure authored with the grade; absent uses the pipeline's default range. */
245
+ autoExposure?: EditorAutoExposure;
246
+ }
247
+ /** Authored clamped auto-exposure range: how far the meter may pull a dark room or a bright street. */
248
+ export interface EditorAutoExposure {
249
+ /** Lowest exposure multiplier the meter may reach. Default 0.5. */
250
+ min?: number;
251
+ /** Highest exposure multiplier the meter may reach. Default 2.5. */
252
+ max?: number;
253
+ }
208
254
  /**
209
255
  * Scene-document environment/lighting authoring (#1110): sky preset, optional time-of-day drive,
210
256
  * horizon/zenith tints, sun/ambient intensity, and fog. Serializable and genre-agnostic — games
@@ -216,8 +262,16 @@ export interface EditorFogConfig {
216
262
  export interface EditorEnvironment {
217
263
  /** Optional runtime environment map source; absent keeps the procedural gradient. */
218
264
  source?: EnvironmentSource;
265
+ /** Authored clamped auto-exposure and colour grade; the shell applies it to the post chain. */
266
+ grading?: EditorColorGrading;
219
267
  /** Authored local point lights; the runtime clamps these to its bounded light budget. */
220
268
  pointLights?: readonly PointLightingConfig[];
269
+ /**
270
+ * Authored irradiance volumes: baked light probes for interiors and shaded streets. The runtime
271
+ * samples them at the camera and feeds three.js's scene light probe, so a room's ambient comes
272
+ * from its own lights instead of the sky.
273
+ */
274
+ probes?: readonly EditorLightProbe[];
221
275
  /** Fixed sky look when `timeOfDay` is off (or no clock is available). */
222
276
  preset?: EditorSkyPreset;
223
277
  /** Drive sun/sky from the world clock's day fraction instead of the fixed `preset`. */
@@ -250,9 +250,15 @@ export interface ModelConfig {
250
250
  material?: ModelMaterialOverride;
251
251
  /** `false` keeps this model's exported material params and skips the engine's default painted surface detail. Default true. */
252
252
  surfaceDetail?: boolean;
253
+ /**
254
+ * Per-model wind sway for foliage: trunk and standing geometry stay planted while the upper
255
+ * canopy drifts in the shared gust field. `strength` is metres of tip travel, `speed` the gust
256
+ * rate, `height` the world height over which sway ramps in (defaults to the model's own height).
257
+ * `false` or omitted keeps the model rigid. Compiles into each cloned material.
258
+ */
259
+ wind?: ModelWindConfig | false;
253
260
  /** Reusable authored surfaces available to this model's named slot assignments. */
254
- materialAssets?: readonly MaterialAsset[];
255
- /** Sparse per-slot assignments; unselected imported materials keep their original appearance. */
261
+ materialAssets?: readonly MaterialAsset[]; /** Sparse per-slot assignments; unselected imported materials keep their original appearance. */
256
262
  materialAssignments?: readonly MaterialAssignment[];
257
263
  /** Hide named visual subtrees on this instance before placement measurement. Exact runtime names or unique original imported names resolve without removing bones or changing the cached model; missing or ambiguous names warn and leave existing visibility unchanged. Malformed lists reject before model or texture allocation. Omit or pass [] to retain imported visibility. */
258
264
  hiddenNodes?: readonly string[];
@@ -282,6 +288,17 @@ export interface ModelConfig {
282
288
  /** Tuning for the procedural part-motion driver when any part carries a `role`; omit for defaults. */
283
289
  partMotion?: PartMotionParams;
284
290
  }
291
+ /** Wind sway for a foliage model; see {@link ModelConfig.wind}. */
292
+ export interface ModelWindConfig {
293
+ /** Tip travel in metres. Default 0.35. */
294
+ strength?: number;
295
+ /** Gust rate multiplier. Default 1. */
296
+ speed?: number;
297
+ /** World height over which sway ramps from the base to full tip travel, in metres. Defaults to the model's measured height. */
298
+ height?: number;
299
+ /** Wind heading as a two-axis direction; defaults to the engine's shared gust direction. */
300
+ direction?: readonly [number, number];
301
+ }
285
302
  /** Bone names and limits for {@link ModelConfig.ik}. */
286
303
  export interface ModelIkConfig {
287
304
  /** Thigh → shin → foot chains; omit to find them by bone name. `[]` turns foot IK off. */
@@ -81,7 +81,12 @@ export { computeEffectiveStats, createModularItem, install, isComplete, missingR
81
81
  export { createItemUse, type ItemUseHandler, type ItemUseInput } from "./item/use.js";
82
82
  export { createUseBehaviorRegistry, type BehaviorConfig, type BehaviorState, type ComposedUse, type CompositionResult, type SerializedBehaviorState, type UseBehaviorContext, type UseBehaviorDef, type UseBehaviorOutcome, type UseBehaviorRef, type UseBehaviorRegistry, type UseBehaviorRejection, } from "./item/useBehavior.js";
83
83
  export { createWeaponStats } from "./item/weapon.js";
84
+ export { MAX_LOOT_AFFIXES, assertLootCatalog, compareLootItems, createLootRoller, lootLevelScale, lootStatContributions, rollLoot, scoreLootStats, validateLootItem, type LootBaseDef, type LootCatalog, type LootComparison, type LootComparisonOptions, type LootInstalledPart, type LootIssueCode, type LootItem, type LootManufacturerDef, type LootPartDef, type LootRollOptions, type LootRoller, type LootRollerOptions, type LootRollerState, type LootStatDelta, type LootValidationIssue, } from "./loot/lootGen.js";
85
+ export { lootWeaponDefinition, type LootWeaponProjection, type LootWeaponStatMap } from "./loot/lootWeapon.js";
86
+ export { compareGearSets, gearScore, type GearScore, type GearScoreOptions, type GearSlotItem, } from "./progression/gearScore.js";
87
+ export { assertSkillTreeConfig, createSkillTree, type SkillActiveDef, type SkillNodeDef, type SkillRequirement, type SkillResolution, type SkillSpendReason, type SkillSpendResult, type SkillSynergyDef, type SkillTree, type SkillTreeConfig, type SkillTreeState, } from "./progression/skillTree.js";
84
88
  export { createStatGraph, statModifierContributions, type StatContribution, type StatContributionStep, type StatDeriveContext, type StatDerivedDef, type StatExplanation, type StatGraph, type StatGraphDef, type StatInputDef, type StatModEntry, type StatOp, type StatSheet, type StatSheetState, } from "./progression/statGraph.js";
89
+ export { assertXpCurve, defineXpCurve, type XpCurve, type XpCurveDef, type XpGrowthRule, type XpLevelStep, } from "./progression/xpCurve.js";
85
90
  export { type CellGrid } from "./puzzle/cellGrid.js";
86
91
  export { type ShapeTable } from "./puzzle/fallingPiece.js";
87
92
  export { createNameGenerator } from "./random/nameGen.js";
@@ -95,6 +100,10 @@ export { createTriggeredRuleEngine, type ActiveEffect, type EffectRef, type Firi
95
100
  export { createRing, ringSampleAt, type Ring, type RingConfig, type RingPhase } from "./session/ring.js";
96
101
  export { type RoleSpec } from "./session/roles.js";
97
102
  export { type RoundConfig, type RoundSnapshot } from "./session/roundState.js";
103
+ export { createNearAgents, type NearAgentSnapshot, type NearAgentView, type NearAgentsOptions, type NearCrowdAgents, } from "./sim/nearAgents.js";
104
+ export { createNeedSet, type NeedDefinition, type NeedReading, type NeedSet, type NeedSnapshot } from "./sim/needs.js";
105
+ export { createOffScreenWorld, type NearAgents, type NearHandoff, type NearSpawn, type OffScreenAgentView, type OffScreenMode, type OffScreenOptions, type OffScreenPolicy, type OffScreenPolicyPatch, type OffScreenResidentSpec, type OffScreenSnapshot, type OffScreenStats, type OffScreenWorld, } from "./sim/offScreen.js";
106
+ export { resolveRoutine, type RoutineConfig, type RoutinePhase, type RoutineStop, type RoutineStopSpec, type RoutineTimeline, } from "./sim/routine.js";
98
107
  export { createDecayMeterSet, decayMeterMoodles, decayMeterSnapshot, decayMeterState, decayMeters, initDecayMeters, refillMeter, type DecayMeterSet, type DecayMeterValues, type DecayModifier, } from "./survival/decayMeter.js";
99
108
  export { createMoodleStack, stackMoodles, type Moodle, type MoodleStack } from "./survival/moodle.js";
100
109
  export { createMultiRegionHealth, type MultiRegionHealth } from "./survival/regionHealth.js";
package/dist/gameplay.js CHANGED
@@ -81,7 +81,12 @@ export { computeEffectiveStats, createModularItem, install, isComplete, missingR
81
81
  export { createItemUse } from "./item/use.js";
82
82
  export { createUseBehaviorRegistry, } from "./item/useBehavior.js";
83
83
  export { createWeaponStats } from "./item/weapon.js";
84
+ export { MAX_LOOT_AFFIXES, assertLootCatalog, compareLootItems, createLootRoller, lootLevelScale, lootStatContributions, rollLoot, scoreLootStats, validateLootItem, } from "./loot/lootGen.js";
85
+ export { lootWeaponDefinition } from "./loot/lootWeapon.js";
86
+ export { compareGearSets, gearScore, } from "./progression/gearScore.js";
87
+ export { assertSkillTreeConfig, createSkillTree, } from "./progression/skillTree.js";
84
88
  export { createStatGraph, statModifierContributions, } from "./progression/statGraph.js";
89
+ export { assertXpCurve, defineXpCurve, } from "./progression/xpCurve.js";
85
90
  export {} from "./puzzle/cellGrid.js";
86
91
  export {} from "./puzzle/fallingPiece.js";
87
92
  export { createNameGenerator } from "./random/nameGen.js";
@@ -95,6 +100,10 @@ export { createTriggeredRuleEngine, } from "./rules/triggeredRules.js";
95
100
  export { createRing, ringSampleAt } from "./session/ring.js";
96
101
  export {} from "./session/roles.js";
97
102
  export {} from "./session/roundState.js";
103
+ export { createNearAgents, } from "./sim/nearAgents.js";
104
+ export { createNeedSet } from "./sim/needs.js";
105
+ export { createOffScreenWorld, } from "./sim/offScreen.js";
106
+ export { resolveRoutine, } from "./sim/routine.js";
98
107
  export { createDecayMeterSet, decayMeterMoodles, decayMeterSnapshot, decayMeterState, decayMeters, initDecayMeters, refillMeter, } from "./survival/decayMeter.js";
99
108
  export { createMoodleStack, stackMoodles } from "./survival/moodle.js";
100
109
  export { createMultiRegionHealth } from "./survival/regionHealth.js";
@@ -0,0 +1,48 @@
1
+ import { type EncumbranceConfig, type EncumbranceState, type LoadEntry } from "./encumbrance.js";
2
+ import type { InventoryState } from "./inventoryModel.js";
3
+ /** Per-item mass in the caller's weight unit; an unknown item weighs 0. */
4
+ export type WeightLookup = (itemId: string) => number;
5
+ /**
6
+ * Every filled stack in the given inventories as load entries, in slot order. Only the inventories
7
+ * the caller passes count, so a stash left out of the list adds no weight.
8
+ *
9
+ * @capability carry-load read carried stacks out of one or more inventories for a weight query
10
+ */
11
+ export declare function carriedEntries(states: readonly InventoryState[]): LoadEntry[];
12
+ /** Everything a {@link characterLoad} reading takes: carried inventories, worn gear and the capacity they are measured against. */
13
+ export interface CharacterLoadInput {
14
+ /** Carried inventories (bag, rig, pockets); a stash left out does not weigh anything. */
15
+ carried?: readonly InventoryState[];
16
+ /** Worn item ids, one unit each — `Equipment.worn()` mapped to `itemId`. */
17
+ worn?: readonly string[];
18
+ weightOf: WeightLookup;
19
+ /** Carrying capacity in the same unit as `weightOf`; the caller adds level and gear bonuses. */
20
+ capacity: number;
21
+ /** Fraction of worn gear weight that counts against capacity (default `1`). */
22
+ wornFactor?: number;
23
+ encumbrance?: EncumbranceConfig;
24
+ }
25
+ /** A carrier's load: the encumbrance state, how it splits into carried and worn, and capacity left. */
26
+ export interface CharacterLoad extends EncumbranceState {
27
+ /** Mass of the carried inventories alone. */
28
+ carriedMass: number;
29
+ /** Mass worn gear adds after `wornFactor`. */
30
+ wornMass: number;
31
+ /** Capacity left before the carrier is immobile; negative when overloaded. */
32
+ remaining: number;
33
+ }
34
+ /**
35
+ * Weigh a character: carried stacks plus worn gear against a capacity, resolved into the shared
36
+ * encumbrance bands. Movement reads `moveMultiplier`, `canSprint`, `canJump` and `immobile`; pickup
37
+ * gates read `remaining` or {@link canCarry}. The stash is not part of `carried`, so it adds nothing.
38
+ *
39
+ * @capability carry-encumbrance weigh carried inventories plus worn gear against a capacity and hand back movement penalties
40
+ */
41
+ export declare function characterLoad(input: CharacterLoadInput): CharacterLoad;
42
+ /**
43
+ * Whether `addedMass` more weight still fits under `capacity`. Pure: the caller decides what to do
44
+ * with a refusal, so an overload policy stays with the game.
45
+ *
46
+ * @capability carry-encumbrance gate a pickup against the carrier's remaining capacity
47
+ */
48
+ export declare function canCarry(load: Pick<CharacterLoad, "mass" | "capacity">, addedMass: number): boolean;
@@ -0,0 +1,44 @@
1
+ import { resolveEncumbrance, totalLoad, } from "./encumbrance.js";
2
+ /**
3
+ * Every filled stack in the given inventories as load entries, in slot order. Only the inventories
4
+ * the caller passes count, so a stash left out of the list adds no weight.
5
+ *
6
+ * @capability carry-load read carried stacks out of one or more inventories for a weight query
7
+ */
8
+ export function carriedEntries(states) {
9
+ const entries = [];
10
+ for (const state of states) {
11
+ for (const slot of state.slots) {
12
+ if (slot !== null)
13
+ entries.push({ itemId: slot.itemId, quantity: slot.count });
14
+ }
15
+ }
16
+ return entries;
17
+ }
18
+ /**
19
+ * Weigh a character: carried stacks plus worn gear against a capacity, resolved into the shared
20
+ * encumbrance bands. Movement reads `moveMultiplier`, `canSprint`, `canJump` and `immobile`; pickup
21
+ * gates read `remaining` or {@link canCarry}. The stash is not part of `carried`, so it adds nothing.
22
+ *
23
+ * @capability carry-encumbrance weigh carried inventories plus worn gear against a capacity and hand back movement penalties
24
+ */
25
+ export function characterLoad(input) {
26
+ const carriedMass = totalLoad(carriedEntries(input.carried ?? []), input.weightOf);
27
+ const factor = input.wornFactor !== undefined && Number.isFinite(input.wornFactor) && input.wornFactor >= 0
28
+ ? input.wornFactor
29
+ : 1;
30
+ const wornEntries = (input.worn ?? []).map((itemId) => ({ itemId, quantity: 1 }));
31
+ const wornMass = totalLoad(wornEntries, input.weightOf) * factor;
32
+ const base = resolveEncumbrance(carriedMass + wornMass, input.capacity, input.encumbrance);
33
+ return { ...base, carriedMass, wornMass, remaining: input.capacity - base.mass };
34
+ }
35
+ /**
36
+ * Whether `addedMass` more weight still fits under `capacity`. Pure: the caller decides what to do
37
+ * with a refusal, so an overload policy stays with the game.
38
+ *
39
+ * @capability carry-encumbrance gate a pickup against the carrier's remaining capacity
40
+ */
41
+ export function canCarry(load, addedMass) {
42
+ const added = Number.isFinite(addedMass) && addedMass > 0 ? addedMass : 0;
43
+ return load.mass + added <= load.capacity;
44
+ }
@@ -0,0 +1,95 @@
1
+ /** One body slot: the item kinds it accepts and the slots it occupies while filled. */
2
+ export interface EquipSlotDef {
3
+ /** Stable slot id, in the caller's nouns ("weapon", "rig", "boots"). */
4
+ id: string;
5
+ /** Item kinds this slot accepts; omitted accepts any kind. */
6
+ accepts?: readonly string[];
7
+ /** Slot ids this slot occupies while filled, e.g. a two-handed weapon blocking "offhand". */
8
+ blocks?: readonly string[];
9
+ }
10
+ /**
11
+ * The gear facts the equip rules read from an item. A rolled item resolves its runtime id through
12
+ * the instance registry to its base and answers from that base's data — this is not a second item type.
13
+ */
14
+ export interface EquipItemDef {
15
+ /** Slot ids this item may occupy, most preferred first. */
16
+ slots: readonly string[];
17
+ /** Kind tag checked against {@link EquipSlotDef.accepts}. */
18
+ kind?: string;
19
+ }
20
+ /** Item lookup the rules read; returns nothing for an unknown id. */
21
+ export type EquipItemLookup = (itemId: string) => EquipItemDef | null | undefined;
22
+ /** Serializable equip state: the filled slots as `[slotId, itemId]` pairs. */
23
+ export interface EquipmentState {
24
+ slots: readonly (readonly [string, string])[];
25
+ }
26
+ /** Why an equip attempt was refused; `detail` names the item and slots involved. */
27
+ export type EquipRejection = "unknown-slot" | "not-equippable" | "wrong-slot" | "already-equipped" | "blocked";
28
+ /** An equip attempt: the new worn set plus the replaced item, or the refusal with its reason and detail. */
29
+ export type EquipResult = {
30
+ status: "ok";
31
+ state: EquipmentState;
32
+ slotId: string;
33
+ replaced: string | null;
34
+ } | {
35
+ status: "rejected";
36
+ reason: EquipRejection;
37
+ detail: string;
38
+ };
39
+ /** An unequip attempt: the new worn set plus the item taken off, or the refusal with its detail. */
40
+ export type UnequipResult = {
41
+ status: "ok";
42
+ state: EquipmentState;
43
+ itemId: string;
44
+ } | {
45
+ status: "rejected";
46
+ reason: "unknown-slot" | "empty-slot";
47
+ detail: string;
48
+ };
49
+ /** One worn item and the slot holding it. */
50
+ export interface WornEntry {
51
+ slotId: string;
52
+ itemId: string;
53
+ }
54
+ /** What the debug overlay reads about a worn set. */
55
+ export interface EquipmentProbeReading {
56
+ filled: number;
57
+ slots: Readonly<Record<string, string>>;
58
+ }
59
+ /** A worn-gear set: one item per slot, rules from slot and item data, state out and back in. */
60
+ export interface Equipment {
61
+ /** Wear `itemId` in `slotId`; a filled slot is replaced and its item returned as `replaced`. */
62
+ equip(slotId: string, itemId: string): EquipResult;
63
+ /** Wear `itemId` in the first of its preferred slots that is empty, else the first slot that accepts it. */
64
+ equipBest(itemId: string): EquipResult;
65
+ /** Clear `slotId` and hand back the item it held. */
66
+ unequip(slotId: string): UnequipResult;
67
+ /** Worn items in declared slot order. */
68
+ worn(): readonly WornEntry[];
69
+ /** The slot holding `itemId`, or null when that item is not worn. */
70
+ slotOf(itemId: string): string | null;
71
+ /** Declared slot ids, in order. */
72
+ slotIds(): readonly string[];
73
+ state(): EquipmentState;
74
+ /** Replace the worn set; returns false without mutation when an entry breaks the rules. */
75
+ restore(next: EquipmentState): boolean;
76
+ reset(): void;
77
+ /** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */
78
+ dispose(): void;
79
+ }
80
+ /** Options for {@link createEquipment}. */
81
+ export interface EquipmentOptions {
82
+ /** Register a devtools probe under this name reporting which slots are filled. */
83
+ devtoolsName?: string;
84
+ }
85
+ /**
86
+ * A worn-gear set over caller slot and item data.
87
+ *
88
+ * Slot defs own what a slot accepts and which slots it blocks; item defs own which slots an item may
89
+ * occupy and its kind. Equipping into a filled slot swaps and returns the replaced item, so gear is
90
+ * never silently lost: the caller puts `replaced` back. A two-handed item and the slot it blocks
91
+ * cannot both be filled, and the rejection names the occupied slot.
92
+ *
93
+ * @capability equipment-slot-inventory wear gear by slot with item-kind rules, two-handed blocking and replace-on-equip
94
+ */
95
+ export declare function createEquipment(slotDefs: readonly EquipSlotDef[], lookup: EquipItemLookup, options?: EquipmentOptions): Equipment;
@@ -0,0 +1,169 @@
1
+ import { devtools } from "../devtools/devtools.js";
2
+ function slotAcceptsKind(slot, kind) {
3
+ if (slot.accepts === undefined)
4
+ return true;
5
+ return kind !== undefined && slot.accepts.includes(kind);
6
+ }
7
+ /**
8
+ * A worn-gear set over caller slot and item data.
9
+ *
10
+ * Slot defs own what a slot accepts and which slots it blocks; item defs own which slots an item may
11
+ * occupy and its kind. Equipping into a filled slot swaps and returns the replaced item, so gear is
12
+ * never silently lost: the caller puts `replaced` back. A two-handed item and the slot it blocks
13
+ * cannot both be filled, and the rejection names the occupied slot.
14
+ *
15
+ * @capability equipment-slot-inventory wear gear by slot with item-kind rules, two-handed blocking and replace-on-equip
16
+ */
17
+ export function createEquipment(slotDefs, lookup, options = {}) {
18
+ const byId = new Map();
19
+ for (const def of slotDefs) {
20
+ if (def.id.trim() === "")
21
+ throw new RangeError('Equipment slot field "id" must not be empty');
22
+ if (byId.has(def.id))
23
+ throw new RangeError(`Equipment slot "${def.id}" is declared twice`);
24
+ byId.set(def.id, def);
25
+ }
26
+ const order = slotDefs.map((def) => def.id);
27
+ const filled = new Map();
28
+ function readState() {
29
+ const slots = [];
30
+ for (const slotId of order) {
31
+ const itemId = filled.get(slotId);
32
+ if (itemId !== undefined)
33
+ slots.push([slotId, itemId]);
34
+ }
35
+ return { slots };
36
+ }
37
+ function conflicts(slotId, occupied = filled) {
38
+ const blockedByOwn = byId.get(slotId)?.blocks ?? [];
39
+ const blocking = order.filter((other) => other !== slotId && (byId.get(other)?.blocks ?? []).includes(slotId));
40
+ return [...new Set([...blockedByOwn, ...blocking])].filter((other) => occupied.has(other));
41
+ }
42
+ function check(slotId, itemId, occupied) {
43
+ const slot = byId.get(slotId);
44
+ if (slot === undefined) {
45
+ return { status: "rejected", reason: "unknown-slot", detail: `"${slotId}" is not a declared slot (${order.join(", ")})` };
46
+ }
47
+ const item = lookup(itemId);
48
+ if (item === null || item === undefined || item.slots.length === 0) {
49
+ return { status: "rejected", reason: "not-equippable", detail: `item "${itemId}" declares no equip slot` };
50
+ }
51
+ if (!item.slots.includes(slotId)) {
52
+ return {
53
+ status: "rejected",
54
+ reason: "wrong-slot",
55
+ detail: `item "${itemId}" belongs in ${item.slots.join(", ")}; "${slotId}" is not one of them`,
56
+ };
57
+ }
58
+ if (!slotAcceptsKind(slot, item.kind)) {
59
+ return {
60
+ status: "rejected",
61
+ reason: "wrong-slot",
62
+ detail: `slot "${slotId}" accepts ${(slot.accepts ?? []).join(", ")}; item "${itemId}" is ${item.kind ?? "untyped"}`,
63
+ };
64
+ }
65
+ const wornAt = [...occupied].find(([, worn]) => worn === itemId)?.[0];
66
+ if (wornAt !== undefined && wornAt !== slotId) {
67
+ return { status: "rejected", reason: "already-equipped", detail: `item "${itemId}" is already worn in "${wornAt}"` };
68
+ }
69
+ const conflict = conflicts(slotId, occupied).filter((other) => other !== slotId)[0];
70
+ if (conflict !== undefined) {
71
+ return {
72
+ status: "rejected",
73
+ reason: "blocked",
74
+ detail: `slot "${conflict}" is occupied and cannot be worn with "${slotId}"`,
75
+ };
76
+ }
77
+ return null;
78
+ }
79
+ function equipInto(slotId, itemId) {
80
+ const refusal = check(slotId, itemId, filled);
81
+ if (refusal !== null)
82
+ return refusal;
83
+ const replaced = filled.get(slotId) ?? null;
84
+ filled.set(slotId, itemId);
85
+ return { status: "ok", state: readState(), slotId, replaced };
86
+ }
87
+ const probe = options.devtoolsName === undefined
88
+ ? null
89
+ : devtools.probes.register(options.devtoolsName, () => ({
90
+ filled: filled.size,
91
+ slots: Object.fromEntries(filled),
92
+ }));
93
+ return {
94
+ equip: equipInto,
95
+ equipBest(itemId) {
96
+ const item = lookup(itemId);
97
+ if (item === null || item === undefined || item.slots.length === 0) {
98
+ return { status: "rejected", reason: "not-equippable", detail: `item "${itemId}" declares no equip slot` };
99
+ }
100
+ const allowed = item.slots.filter((slotId) => byId.has(slotId));
101
+ if (allowed.length === 0) {
102
+ return {
103
+ status: "rejected",
104
+ reason: "unknown-slot",
105
+ detail: `item "${itemId}" names no declared slot (${item.slots.join(", ")})`,
106
+ };
107
+ }
108
+ const preferred = allowed.filter((slotId) => check(slotId, itemId, filled) === null);
109
+ if (preferred.length === 0)
110
+ return check(allowed[0], itemId, filled);
111
+ return equipInto(preferred.find((slotId) => !filled.has(slotId)) ?? preferred[0], itemId);
112
+ },
113
+ unequip(slotId) {
114
+ if (!byId.has(slotId)) {
115
+ return { status: "rejected", reason: "unknown-slot", detail: `"${slotId}" is not a declared slot (${order.join(", ")})` };
116
+ }
117
+ const itemId = filled.get(slotId);
118
+ if (itemId === undefined) {
119
+ return { status: "rejected", reason: "empty-slot", detail: `slot "${slotId}" holds nothing` };
120
+ }
121
+ filled.delete(slotId);
122
+ return { status: "ok", state: readState(), itemId };
123
+ },
124
+ worn() {
125
+ const out = [];
126
+ for (const slotId of order) {
127
+ const itemId = filled.get(slotId);
128
+ if (itemId !== undefined)
129
+ out.push({ slotId, itemId });
130
+ }
131
+ return out;
132
+ },
133
+ slotOf(itemId) {
134
+ for (const [slotId, worn] of filled)
135
+ if (worn === itemId)
136
+ return slotId;
137
+ return null;
138
+ },
139
+ slotIds: () => [...order],
140
+ state: readState,
141
+ restore(next) {
142
+ if (next === null || typeof next !== "object" || !Array.isArray(next.slots))
143
+ return false;
144
+ const staged = new Map();
145
+ for (const entry of next.slots) {
146
+ if (!Array.isArray(entry) || entry.length !== 2)
147
+ return false;
148
+ const [slotId, itemId] = entry;
149
+ if (typeof slotId !== "string" || typeof itemId !== "string" || itemId.trim() === "")
150
+ return false;
151
+ if (staged.has(slotId))
152
+ return false;
153
+ if (check(slotId, itemId, staged) !== null)
154
+ return false;
155
+ staged.set(slotId, itemId);
156
+ }
157
+ filled.clear();
158
+ for (const [slotId, itemId] of staged)
159
+ filled.set(slotId, itemId);
160
+ return true;
161
+ },
162
+ reset() {
163
+ filled.clear();
164
+ },
165
+ dispose() {
166
+ probe?.();
167
+ },
168
+ };
169
+ }
@@ -41,7 +41,7 @@ export type SplitResult = {
41
41
  state: InventoryState;
42
42
  } | {
43
43
  status: "rejected";
44
- reason: "invalid-slot" | "empty-slot" | "invalid-amount" | "no-space" | "slot-occupied";
44
+ reason: "invalid-slot" | "empty-slot" | "invalid-amount" | "no-space" | "slot-occupied" | "over-stack";
45
45
  };
46
46
  /**
47
47
  * A bag of stackable items supporting add, remove, count, and move — the base inventory model.
@@ -63,13 +63,14 @@ export declare function hasItem(state: InventoryState, itemId: string, count: nu
63
63
  export declare function moveItem(from: InventoryState, fromSlot: number, to: InventoryState, toLayout: InventoryLayout, traits: ItemTraits, toSlot?: number): MoveResult;
64
64
  /**
65
65
  * Splits `amount` off the stack in `slot` into `toSlot` (or the first empty slot when `toSlot` is
66
- * omitted). Limit-agnostic: it does not consult {@link ItemTraits}, so a same-item target merges by
67
- * simple addition. Rejects an empty source, `amount <= 0`, `amount >= stack count` (a split must
68
- * leave both sides non-empty), no free slot, or a target holding a different item.
66
+ * omitted). Without `limit` it is limit-agnostic: a same-item target merges by simple addition.
67
+ * Pass the item's stack limit to keep the merge legal, which rejects `over-stack`. Rejects an empty
68
+ * source, `amount <= 0`, `amount >= stack count` (a split must leave both sides non-empty), no free
69
+ * slot, or a target holding a different item.
69
70
  *
70
- * @capability inventory-split split a stack into a target or first-empty slot
71
+ * @capability inventory-split split a stack into a target or first-empty slot, honoring a stack limit
71
72
  */
72
- export declare function splitStack(state: InventoryState, slot: number, amount: number, toSlot?: number): SplitResult;
73
+ export declare function splitStack(state: InventoryState, slot: number, amount: number, toSlot?: number, limit?: number): SplitResult;
73
74
  export interface InventorySet<TId extends string> {
74
75
  put(id: TId, itemId: string, count: number, options?: {
75
76
  slot?: number;
@@ -80,6 +81,14 @@ export interface InventorySet<TId extends string> {
80
81
  has(id: TId, itemId: string, count: number): boolean;
81
82
  state(id: TId): InventoryState;
82
83
  replaceState(id: TId, state: InventoryState): void;
84
+ /** Detached copy of every declared inventory — a save, never a live alias. */
85
+ snapshot(): InventorySetState<TId>;
86
+ /** Replace every declared inventory from a snapshot; returns false without mutation when an entry is malformed or incomplete. */
87
+ restore(next: InventorySetState<TId>): boolean;
88
+ /** Empty every declared inventory. */
89
+ reset(): void;
83
90
  }
91
+ /** Every inventory in a set, keyed by id, as plain slot data. */
92
+ export type InventorySetState<TId extends string> = Record<TId, InventoryState>;
84
93
  /** Build a keyed set of named inventories from their `layouts`, sharing one `traits` table — the stateful façade over put/take/move/count. */
85
94
  export declare function createInventorySet<TId extends string>(layouts: Record<TId, InventoryLayout>, traits: ItemTraits): InventorySet<TId>;