@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
@@ -175,13 +175,14 @@ export function moveItem(from, fromSlot, to, toLayout, traits, toSlot) {
175
175
  }
176
176
  /**
177
177
  * Splits `amount` off the stack in `slot` into `toSlot` (or the first empty slot when `toSlot` is
178
- * omitted). Limit-agnostic: it does not consult {@link ItemTraits}, so a same-item target merges by
179
- * simple addition. Rejects an empty source, `amount <= 0`, `amount >= stack count` (a split must
180
- * leave both sides non-empty), no free slot, or a target holding a different item.
178
+ * omitted). Without `limit` it is limit-agnostic: a same-item target merges by simple addition.
179
+ * Pass the item's stack limit to keep the merge legal, which rejects `over-stack`. Rejects an empty
180
+ * source, `amount <= 0`, `amount >= stack count` (a split must leave both sides non-empty), no free
181
+ * slot, or a target holding a different item.
181
182
  *
182
- * @capability inventory-split split a stack into a target or first-empty slot
183
+ * @capability inventory-split split a stack into a target or first-empty slot, honoring a stack limit
183
184
  */
184
- export function splitStack(state, slot, amount, toSlot) {
185
+ export function splitStack(state, slot, amount, toSlot, limit) {
185
186
  if (!validSlot(state, slot))
186
187
  return { status: "rejected", reason: "invalid-slot" };
187
188
  const source = state.slots[slot];
@@ -206,6 +207,9 @@ export function splitStack(state, slot, amount, toSlot) {
206
207
  }
207
208
  const slots = state.slots.slice();
208
209
  const dest = slots[target];
210
+ if (limit !== undefined && dest !== null && dest !== undefined && dest.count + amount > limit) {
211
+ return { status: "rejected", reason: "over-stack" };
212
+ }
209
213
  slots[slot] = { itemId: source.itemId, count: source.count - amount };
210
214
  slots[target] =
211
215
  dest === null || dest === undefined
@@ -213,6 +217,26 @@ export function splitStack(state, slot, amount, toSlot) {
213
217
  : { itemId: source.itemId, count: dest.count + amount };
214
218
  return { status: "ok", state: { slots } };
215
219
  }
220
+ function cloneSlots(slots) {
221
+ return slots.map((slot) => (slot === null ? null : { itemId: slot.itemId, count: slot.count }));
222
+ }
223
+ function validInventoryState(state, slots) {
224
+ if (state === null || typeof state !== "object" || !Array.isArray(state.slots))
225
+ return false;
226
+ if (state.slots.length !== slots)
227
+ return false;
228
+ for (const slot of state.slots) {
229
+ if (slot === null)
230
+ continue;
231
+ if (typeof slot !== "object")
232
+ return false;
233
+ if (typeof slot.itemId !== "string")
234
+ return false;
235
+ if (!Number.isFinite(slot.count) || slot.count <= 0)
236
+ return false;
237
+ }
238
+ return true;
239
+ }
216
240
  /** Build a keyed set of named inventories from their `layouts`, sharing one `traits` table — the stateful façade over put/take/move/count. */
217
241
  export function createInventorySet(layouts, traits) {
218
242
  const states = new Map();
@@ -258,5 +282,30 @@ export function createInventorySet(layouts, traits) {
258
282
  replaceState(id, state) {
259
283
  states.set(id, state);
260
284
  },
285
+ snapshot() {
286
+ const out = {};
287
+ for (const id of Object.keys(layouts)) {
288
+ out[id] = { slots: cloneSlots(requireState(id).slots) };
289
+ }
290
+ return out;
291
+ },
292
+ restore(next) {
293
+ if (next === null || typeof next !== "object")
294
+ return false;
295
+ const staged = new Map();
296
+ for (const id of Object.keys(layouts)) {
297
+ const state = next[id];
298
+ if (state === undefined || !validInventoryState(state, layouts[id].slots))
299
+ return false;
300
+ staged.set(id, { slots: cloneSlots(state.slots) });
301
+ }
302
+ for (const [id, state] of staged)
303
+ states.set(id, state);
304
+ return true;
305
+ },
306
+ reset() {
307
+ for (const id of Object.keys(layouts))
308
+ states.set(id, createEmptyInventory(layouts[id]));
309
+ },
261
310
  };
262
311
  }
@@ -79,6 +79,32 @@ export declare function findShapedPlacement<T>(grid: ShapedGrid<T>, footprint: F
79
79
  * @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
80
80
  */
81
81
  export declare function placeShaped<T>(grid: ShapedGrid<T>, item: ShapedItem<T>, origin: Cell, rotation?: Rotation): ShapedResult<T>;
82
+ /**
83
+ * Why a first-fit placement failed: the footprint cannot fit an empty board at any offered rotation
84
+ * (`too-large`), the board is genuinely full (`no-space`), or the search hit its budget.
85
+ */
86
+ export type ShapedFitRejection = "duplicate-id" | "invalid-footprint" | "too-large" | "no-space" | "budget-exceeded";
87
+ /** A placed board, or a refusal that names why the item does not fit. */
88
+ export type ShapedFitResult<T> = {
89
+ status: "ok";
90
+ grid: ShapedGrid<T>;
91
+ origin: Cell;
92
+ rotation: Rotation;
93
+ checks: number;
94
+ } | {
95
+ status: "rejected";
96
+ reason: ShapedFitRejection;
97
+ detail: string;
98
+ checks: number;
99
+ };
100
+ /**
101
+ * Place a shaped item at the first legal spot, or say why it does not fit. Distinguishes an item that
102
+ * can never fit the board (`too-large`, with its footprint and the board size in `detail`) from a full
103
+ * board (`no-space`) and an exhausted search (`budget-exceeded`, not a full bag).
104
+ *
105
+ * @capability shaped-placement place a shaped item at its first fit and name why it does not fit
106
+ */
107
+ export declare function fitShapedItem<T>(grid: ShapedGrid<T>, item: ShapedItem<T>, options?: ShapedPlacementSearchOptions): ShapedFitResult<T>;
82
108
  /**
83
109
  * Remove one placement without mutating the source board.
84
110
  * @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
@@ -147,6 +147,53 @@ export function placeShaped(grid, item, origin, rotation = 0) {
147
147
  };
148
148
  return { status: "ok", grid: { ...grid, placements: [...grid.placements, placement] } };
149
149
  }
150
+ function footprintBounds(footprint) {
151
+ let width = 0;
152
+ let height = 0;
153
+ for (const [c, r] of footprint) {
154
+ width = Math.max(width, c + 1);
155
+ height = Math.max(height, r + 1);
156
+ }
157
+ return { width, height };
158
+ }
159
+ /**
160
+ * Place a shaped item at the first legal spot, or say why it does not fit. Distinguishes an item that
161
+ * can never fit the board (`too-large`, with its footprint and the board size in `detail`) from a full
162
+ * board (`no-space`) and an exhausted search (`budget-exceeded`, not a full bag).
163
+ *
164
+ * @capability shaped-placement place a shaped item at its first fit and name why it does not fit
165
+ */
166
+ export function fitShapedItem(grid, item, options = {}) {
167
+ if (grid.placements.some((placement) => placement.id === item.id)) {
168
+ return { status: "rejected", reason: "duplicate-id", detail: `"${item.id}" is already placed`, checks: 0 };
169
+ }
170
+ if (!validFootprint(item.footprint)) {
171
+ return { status: "rejected", reason: "invalid-footprint", detail: `footprint has ${item.footprint.length} cell(s)`, checks: 0 };
172
+ }
173
+ const rotations = options.rotations ?? [0, 1, 2, 3];
174
+ const candidates = rotations.map((rotation) => ({ rotation, bounds: footprintBounds(rotateFootprint(item.footprint, rotation)) }));
175
+ const fitsEmptyBoard = candidates.filter(({ bounds }) => bounds.width <= grid.width && bounds.height <= grid.height);
176
+ if (fitsEmptyBoard.length === 0) {
177
+ const bounds = footprintBounds(normalizeFootprint(item.footprint));
178
+ return {
179
+ status: "rejected",
180
+ reason: "too-large",
181
+ detail: `a ${bounds.width}×${bounds.height} item does not fit a ${grid.width}×${grid.height} board`,
182
+ checks: 0,
183
+ };
184
+ }
185
+ const fit = findShapedPlacement(grid, item.footprint, { ...options, rotations: fitsEmptyBoard.map((entry) => entry.rotation) });
186
+ if (fit.status !== "found") {
187
+ return fit.status === "budget-exceeded"
188
+ ? { status: "rejected", reason: "budget-exceeded", detail: `search stopped after ${fit.checks} placements`, checks: fit.checks }
189
+ : { status: "rejected", reason: "no-space", detail: `every position is covered after ${fit.checks} placements`, checks: fit.checks };
190
+ }
191
+ const placed = placeShaped(grid, item, fit.origin, fit.rotation);
192
+ if (placed.status === "rejected") {
193
+ return { status: "rejected", reason: "no-space", detail: placed.reason, checks: fit.checks };
194
+ }
195
+ return { status: "ok", grid: placed.grid, origin: fit.origin, rotation: fit.rotation, checks: fit.checks };
196
+ }
150
197
  /**
151
198
  * Remove one placement without mutating the source board.
152
199
  * @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
@@ -0,0 +1,186 @@
1
+ import type { AffixPool, ItemBaseDef, RarityTier, RolledAffix } from "../item/affix.js";
2
+ import { type MountSlotDef, type PartDef } from "../item/modularItem.js";
3
+ /** Cap on affixes one item may roll, so a bad catalog cannot make a drop cost unbounded work. */
4
+ export declare const MAX_LOOT_AFFIXES = 8;
5
+ /** A part a base can mount: its stats, a pick weight, and an optional word for the item name. */
6
+ export interface LootPartDef extends PartDef {
7
+ /** Relative pick weight among parts eligible for the same slot (default `1`). */
8
+ weight?: number;
9
+ /** Word this part adds to the generated name, e.g. `"Riveted"`. */
10
+ namePart?: string;
11
+ }
12
+ /** A base family (frame, housing, chassis — caller nouns) with its slots, stats and affix pools. */
13
+ export interface LootBaseDef extends ItemBaseDef {
14
+ /** Relative pick weight among bases a manufacturer can produce (default `1`). */
15
+ weight?: number;
16
+ /** Free-form tags for caller filters and UI. */
17
+ tags?: readonly string[];
18
+ /** Mount slots a roll fills; every declared slot receives one part. */
19
+ slots?: readonly MountSlotDef[];
20
+ }
21
+ /**
22
+ * A maker with a stat personality: its mark goes in the name, `bias` shifts stats additively,
23
+ * `scale` multiplies them, and favored affix pools weigh more when this maker rolls the item.
24
+ */
25
+ export interface LootManufacturerDef {
26
+ id: string;
27
+ /** The maker's mark as it reads in the item name. */
28
+ name: string;
29
+ /** Relative pick weight among manufacturers (default `1`). */
30
+ weight?: number;
31
+ /** Base ids this maker produces; omitted means every base. */
32
+ bases?: readonly string[];
33
+ /** Flat stat shifts applied after level scaling and rarity scaling. */
34
+ bias?: Readonly<Record<string, number>>;
35
+ /** Per-stat multipliers applied after `bias`. */
36
+ scale?: Readonly<Record<string, number>>;
37
+ /** Affix pool ids this maker favors. */
38
+ favoredPools?: readonly string[];
39
+ /** Weight multiplier for an affix from a favored pool (default `3`). */
40
+ favoredWeight?: number;
41
+ }
42
+ /** Everything a roll reads: bases, parts, affixes, rarity odds, makers and the level curve. */
43
+ export interface LootCatalog {
44
+ bases: readonly LootBaseDef[];
45
+ parts: readonly LootPartDef[];
46
+ pools: readonly AffixPool[];
47
+ rarities: readonly RarityTier[];
48
+ manufacturers: readonly LootManufacturerDef[];
49
+ /** `[level, multiplier]` points, linearly interpolated and clamped; omitted means no level scaling. */
50
+ levelScale?: readonly (readonly [number, number])[];
51
+ }
52
+ /** One part occupying one slot of a rolled item. */
53
+ export interface LootInstalledPart {
54
+ slotId: string;
55
+ partId: string;
56
+ category: string;
57
+ }
58
+ /**
59
+ * One rolled item: the base, maker, rarity, level, composed name, chosen parts, rolled affixes and
60
+ * resolved stats. Plain JSON, so a save stores it and the instance registry can key it.
61
+ */
62
+ export interface LootItem {
63
+ /** Base definition id; also the registry `baseId` a generated drop is stored under. */
64
+ baseId: string;
65
+ manufacturer: string;
66
+ rarity: string;
67
+ level: number;
68
+ name: string;
69
+ parts: readonly LootInstalledPart[];
70
+ affixes: readonly RolledAffix[];
71
+ stats: Readonly<Record<string, number>>;
72
+ }
73
+ /** Pin any part of a roll; omitted fields are drawn from the catalog. */
74
+ export interface LootRollOptions {
75
+ /** Item level, integer `>= 1` (default `1`). */
76
+ level?: number;
77
+ baseId?: string;
78
+ manufacturer?: string;
79
+ rarity?: string;
80
+ }
81
+ /** A defect found by {@link validateLootItem}. */
82
+ export type LootIssueCode = "invalid-level" | "unknown-base" | "unknown-manufacturer" | "manufacturer-base" | "unknown-rarity" | "duplicate-slot" | "unknown-slot" | "missing-slot-part" | "unknown-part" | "part-category" | "unknown-affix" | "duplicate-affix" | "invalid-affix" | "non-finite-stat" | "empty-name";
83
+ /** One reason a rolled item is not legal against its catalog, with the offending value named. */
84
+ export interface LootValidationIssue {
85
+ code: LootIssueCode;
86
+ detail: string;
87
+ }
88
+ /** A per-stat comparison entry; `delta` is `b - a`, so positive means the second item is larger. */
89
+ export interface LootStatDelta {
90
+ stat: string;
91
+ a: number;
92
+ b: number;
93
+ delta: number;
94
+ }
95
+ /** How two items compare: every stat delta plus one weighted score for each. */
96
+ export interface LootComparison {
97
+ /** Union of both items' stats, sorted by stat id. Missing stats count as `0`. */
98
+ deltas: readonly LootStatDelta[];
99
+ scoreA: number;
100
+ scoreB: number;
101
+ /** `scoreB - scoreA`. */
102
+ scoreDelta: number;
103
+ better: "a" | "b" | "equal";
104
+ }
105
+ /** Caller policy for the comparison score. */
106
+ export interface LootComparisonOptions {
107
+ /** Weight per stat in the score (default `1` each). */
108
+ weights?: Readonly<Record<string, number>>;
109
+ /** Stats where a smaller number is better, e.g. reload time; flips their score sign. */
110
+ lowerIsBetter?: readonly string[];
111
+ /** Score difference under which the items count as equal (default `1e-9`). */
112
+ tolerance?: number;
113
+ }
114
+ /** Saved counters of a {@link LootRoller}. */
115
+ export interface LootRollerState {
116
+ rolls: number;
117
+ byRarity: Readonly<Record<string, number>>;
118
+ }
119
+ /** Options for {@link createLootRoller}. */
120
+ export interface LootRollerOptions {
121
+ /** Register a devtools probe under this name reporting roll counts per rarity. */
122
+ devtoolsName?: string;
123
+ }
124
+ /**
125
+ * A roll counter over one catalog: every roll goes through {@link rollLoot}, and the handle keeps
126
+ * counters (a distribution readout in the debug overlay) that snapshot and restore as plain data.
127
+ */
128
+ export interface LootRoller {
129
+ roll(rng: () => number, options?: LootRollOptions): LootItem;
130
+ stats(): LootRollerState;
131
+ snapshot(): LootRollerState;
132
+ /** Replace counters; returns false without mutation when the state is malformed. */
133
+ restore(next: LootRollerState): boolean;
134
+ reset(): void;
135
+ /** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */
136
+ dispose(): void;
137
+ }
138
+ /** Check a catalog before rolling; throws `RangeError` naming the offending entry. */
139
+ export declare function assertLootCatalog(catalog: LootCatalog): void;
140
+ /** Stat multiplier at `level` from the catalog's level curve; `1` when the catalog declares none. */
141
+ export declare function lootLevelScale(catalog: LootCatalog, level: number): number;
142
+ /**
143
+ * Roll one item from a catalog: a base and maker, a weighted rarity, one part per slot, rarity-count
144
+ * affixes, level-scaled stats and a name composed from maker, rarity, part, affix words and base.
145
+ * Deterministic for a given `rng`; every field it reads is caller data. Validate a catalog built
146
+ * outside {@link createLootRoller} once with {@link assertLootCatalog} before rolling from it.
147
+ *
148
+ * @capability loot-generation roll a named item from bases, parts, affixes, rarity weights, manufacturer personalities and level scaling
149
+ */
150
+ export declare function rollLoot(catalog: LootCatalog, rng: () => number, options?: LootRollOptions): LootItem;
151
+ /**
152
+ * Check one rolled item against its catalog: base, maker and rarity exist, every slot holds an
153
+ * eligible part, affixes belong to the base's pools, stats are finite and the item is named.
154
+ *
155
+ * @capability loot-validation check a rolled item against its catalog and report each defect with its code
156
+ */
157
+ export declare function validateLootItem(item: LootItem, catalog: LootCatalog): LootValidationIssue[];
158
+ /**
159
+ * Per-stat weighted contribution: `value × weight`, negated for stats listed in `lowerIsBetter`.
160
+ * This is the single weight policy behind {@link scoreLootStats}, {@link compareLootItems} and a
161
+ * gear score, so an item and a whole loadout are never ranked by different arithmetic.
162
+ *
163
+ * @capability loot-score split one stat block into per-stat weighted contributions
164
+ */
165
+ export declare function lootStatContributions(stats: Readonly<Record<string, number>>, options?: Pick<LootComparisonOptions, "weights" | "lowerIsBetter">): Record<string, number>;
166
+ /**
167
+ * One stat block's weighted score: the sum of `value × weight`, negated for stats listed in
168
+ * `lowerIsBetter`. Pair it with {@link lootStatContributions} when a caller also needs the split.
169
+ *
170
+ * @capability loot-score score one stat block with caller weights and lower-is-better stats
171
+ */
172
+ export declare function scoreLootStats(stats: Readonly<Record<string, number>>, options?: Pick<LootComparisonOptions, "weights" | "lowerIsBetter">): number;
173
+ /**
174
+ * Compare two rolled items: every stat delta plus one weighted score for each, with caller-named
175
+ * weights and which stats read better when small.
176
+ *
177
+ * @capability loot-comparison compare two rolled items with per-stat deltas and a weighted score
178
+ */
179
+ export declare function compareLootItems(a: LootItem, b: LootItem, options?: LootComparisonOptions): LootComparison;
180
+ /**
181
+ * Wrap a catalog in a roll counter: same results as {@link rollLoot}, plus counts by rarity for a
182
+ * debug overlay, snapshot/restore of those counts, and an optional devtools probe.
183
+ *
184
+ * @capability loot-roller roll items from one validated catalog while counting drops per rarity for a debug probe
185
+ */
186
+ export declare function createLootRoller(catalog: LootCatalog, options?: LootRollerOptions): LootRoller;