@adatechnology/game-items 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,790 @@
1
+ "use strict";
2
+ var GameItems = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ ARSENAL_CATEGORIES: () => ARSENAL_CATEGORIES,
25
+ BODY_SLOTS: () => BODY_SLOTS,
26
+ DEFAULT_RULES: () => DEFAULT_RULES,
27
+ DURABILITY_KINDS: () => DURABILITY_KINDS,
28
+ ITEM_CATEGORIES: () => ITEM_CATEGORIES,
29
+ RARITIES: () => RARITIES,
30
+ advanceAge: () => advanceAge,
31
+ bagOption: () => bagOption,
32
+ blockChance: () => blockChance,
33
+ canCraft: () => canCraft,
34
+ canTake: () => canTake,
35
+ carriedKg: () => carriedKg,
36
+ chargeLevel: () => chargeLevel,
37
+ checkItems: () => checkItems,
38
+ craft: () => craft,
39
+ createFindsProvider: () => createFindsProvider,
40
+ createItems: () => createItems,
41
+ depletionFactor: () => depletionFactor,
42
+ failsToStart: () => failsToStart,
43
+ freshness: () => freshness,
44
+ itemsFromArsenal: () => itemsFromArsenal,
45
+ loadState: () => loadState,
46
+ maxLoadKg: () => maxLoadKg,
47
+ moveToStorage: () => moveToStorage,
48
+ pieceBlock: () => pieceBlock,
49
+ placeFinds: () => placeFinds,
50
+ repairPiece: () => repairPiece,
51
+ resolveUse: () => resolveUse,
52
+ rollByCategory: () => rollByCategory,
53
+ rollExplicitPool: () => rollExplicitPool,
54
+ scaleCount: () => scaleCount,
55
+ shelfRatio: () => shelfRatio,
56
+ spawnAgeDays: () => spawnAgeDays,
57
+ spawnData: () => spawnData,
58
+ storageMultiplier: () => storageMultiplier,
59
+ toLootTables: () => toLootTables,
60
+ wearBlocked: () => wearBlocked
61
+ });
62
+
63
+ // ../game-contracts/dist/index.js
64
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
65
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
66
+ var deepMerge = (base, patch) => {
67
+ if (!patch) return base;
68
+ const result = { ...base };
69
+ for (const [key, value] of Object.entries(patch)) {
70
+ if (value === void 0 || UNSAFE_KEYS.has(key)) continue;
71
+ const current = result[key];
72
+ result[key] = isPlainObject(current) && isPlainObject(value) ? deepMerge(current, value) : value;
73
+ }
74
+ return result;
75
+ };
76
+ var mergeById = (shipped, extra) => {
77
+ const entries = /* @__PURE__ */ new Map();
78
+ for (const entry of [...shipped, ...extra ?? []]) entries.set(entry.id, entry);
79
+ return entries;
80
+ };
81
+ var applyPatches = (entries, patches) => {
82
+ for (const [id, patch] of Object.entries(patches ?? {})) {
83
+ const entry = entries.get(id);
84
+ if (entry) entries.set(id, deepMerge(entry, patch));
85
+ }
86
+ };
87
+ var removeIds = (entries, ids) => {
88
+ for (const id of ids ?? []) entries.delete(id);
89
+ };
90
+
91
+ // src/categories.ts
92
+ var ITEM_CATEGORIES = [
93
+ { id: "food", name: "Food" },
94
+ { id: "drink", name: "Drink" },
95
+ { id: "medical", name: "Medical" },
96
+ { id: "tools", name: "Tools" },
97
+ { id: "melee", name: "Melee" },
98
+ { id: "materials", name: "Materials" },
99
+ { id: "clothing", name: "Clothing" },
100
+ { id: "bags", name: "Bags" },
101
+ { id: "electronics", name: "Electronics" },
102
+ { id: "fuel", name: "Fuel" },
103
+ { id: "outdoors", name: "Outdoors" },
104
+ { id: "books", name: "Books" },
105
+ { id: "collectibles", name: "Collectibles" },
106
+ { id: "keys", name: "Keys" }
107
+ ];
108
+
109
+ // src/types.ts
110
+ var RARITIES = ["common", "uncommon", "rare", "very_rare", "legendary", "unique"];
111
+ var BODY_SLOTS = ["head", "eyes", "torso", "arms", "hands", "legs", "feet", "over"];
112
+ var DURABILITY_KINDS = ["uses", "portions", "doses", "hits", "cuts", "shots", "condition", "charges", "seconds", "days", "infinite", "container"];
113
+
114
+ // src/check-items.ts
115
+ var findDuplicateIds = (kind, entries) => {
116
+ const seen = /* @__PURE__ */ new Set();
117
+ const problems = [];
118
+ for (const { id } of entries ?? []) {
119
+ if (seen.has(id)) problems.push(`${kind} "${id}" is listed more than once`);
120
+ seen.add(id);
121
+ }
122
+ return problems;
123
+ };
124
+ var isFraction = (value) => value === void 0 || value >= 0 && value <= 1;
125
+ var isPositive = (value) => Number.isFinite(value) && value > 0;
126
+ var checkItem = (item, { categories, places }) => {
127
+ const problems = [];
128
+ const issue = (text) => problems.push(`item "${item.id}" ${text}`);
129
+ if (!categories.has(item.category)) issue(`has unknown category "${item.category}"`);
130
+ if (!RARITIES.includes(item.rarity)) issue(`has unknown rarity "${item.rarity}"`);
131
+ if (item.size.length !== 3 || !item.size.every(isPositive)) issue(`has an invalid size [${item.size.join(", ")}]`);
132
+ if (!(item.weight >= 0) || !Number.isFinite(item.weight)) issue(`has a weight of ${item.weight} kg`);
133
+ if (item.volume !== void 0 && !isPositive(item.volume)) issue(`has a volume of ${item.volume} L`);
134
+ if (item.shelfLife !== void 0 && !isPositive(item.shelfLife)) issue(`has a shelf life of ${item.shelfLife} days`);
135
+ if (item.portions !== void 0 && !(Number.isInteger(item.portions) && item.portions > 0)) issue(`has ${item.portions} portions`);
136
+ if (item.durability && !DURABILITY_KINDS.includes(item.durability.kind)) issue(`has unknown durability kind "${item.durability.kind}"`);
137
+ const { protection } = item;
138
+ if (protection) {
139
+ for (const slot of protection.slots) if (!BODY_SLOTS.includes(slot)) issue(`protects unknown body slot "${slot}"`);
140
+ if (protection.slots.length === 0) issue("has protection but covers no body slot");
141
+ if (![protection.bite, protection.scratch, protection.warmth].every(isFraction)) issue("has a protection value outside 0..1");
142
+ }
143
+ if (item.carry && !(item.carry.capacityKg > 0 && isFraction(item.carry.weightReduction))) issue("has an invalid carry (capacity above 0, reduction within 0..1)");
144
+ if (places.size > 0) {
145
+ for (const place of item.finds?.places ?? []) if (!places.has(place)) issue(`is found in unknown place "${place}"`);
146
+ }
147
+ return problems;
148
+ };
149
+ var checkContainer = (container, { categories }) => {
150
+ const problems = [];
151
+ for (const category of Object.keys(container.categories ?? {})) {
152
+ if (!categories.has(category)) problems.push(`container "${container.id}" draws from unknown category "${category}"`);
153
+ }
154
+ const [low, high] = container.count ?? [0, 0];
155
+ if (low < 0 || high < low) problems.push(`container "${container.id}" has an invalid count [${low}, ${high}]`);
156
+ return problems;
157
+ };
158
+ var checkRules = ({ rarity, freshness: freshness2, storage, carry, protection }) => {
159
+ const problems = [];
160
+ const hasNegativeWeight = RARITIES.some((id) => !(rarity.weights[id] >= 0));
161
+ if (hasNegativeWeight || !RARITIES.some((id) => rarity.weights[id] > 0 && !rarity.fixedOnly.includes(id))) {
162
+ problems.push("rules.rarity.weights must be non-negative with at least one rollable rarity above 0");
163
+ }
164
+ if (!(freshness2.stale > 0 && freshness2.stale < freshness2.spoiled && freshness2.spoiled < freshness2.rotten)) problems.push("rules.freshness must rise: 0 < stale < spoiled < rotten");
165
+ for (const [id, rule] of Object.entries(storage)) if (!isPositive(rule.multiplier) || rule.limitDays !== void 0 && !isPositive(rule.afterLimit ?? 0)) problems.push(`rules.storage "${id}" is invalid`);
166
+ if (!(isPositive(carry.baseKg) && carry.perStrengthKg >= 0 && isPositive(carry.factor) && carry.encumbered > 0 && carry.overloaded > carry.encumbered)) problems.push("rules.carry is invalid");
167
+ if (!(protection.cap > 0 && protection.cap <= 1 && isFraction(protection.tornShare))) problems.push("rules.protection is invalid");
168
+ return problems;
169
+ };
170
+ var checkItems = (parts) => [
171
+ ...[...parts.items.values()].flatMap((item) => checkItem(item, parts)),
172
+ ...[...parts.containers.values()].flatMap((container) => checkContainer(container, parts)),
173
+ ...checkRules(parts.rules)
174
+ ];
175
+
176
+ // src/rules.ts
177
+ var DEFAULT_RULES = {
178
+ rarity: {
179
+ weights: { common: 60, uncommon: 28, rare: 9, very_rare: 2.5, legendary: 0.5, unique: 0 },
180
+ fixedOnly: ["legendary", "unique"]
181
+ },
182
+ freshness: { stale: 0.5, spoiled: 1, rotten: 1.5 },
183
+ storage: {
184
+ fridge: { multiplier: 0.33, needsPower: true },
185
+ freezer: { multiplier: 0.1, needsPower: true },
186
+ cooler: { multiplier: 0.5, limitDays: 2, afterLimit: 1 },
187
+ delivery_bag: { multiplier: 0.8 },
188
+ cellar: { multiplier: 0.66 },
189
+ ambient: { multiplier: 1 },
190
+ hot: { multiplier: 2 }
191
+ },
192
+ carry: { baseKg: 8, perStrengthKg: 2.5, factor: 1.2, encumbered: 1, overloaded: 1.5 },
193
+ protection: { cap: 0.9, wear: { bite: 8, scratch: 3 }, tornShare: 0.25, repairMargin: 10 },
194
+ aging: { thawShelfDays: { freezer: 2 } },
195
+ inventory: { defaultSlots: 10, encumberedRunFactor: 0.75 },
196
+ use: {
197
+ freshness: {
198
+ stale: { satietyFactor: 0.7, poisonChance: 0.1, morale: 0, isEdible: true },
199
+ spoiled: { satietyFactor: 0.7, poisonChance: 0.5, morale: -20, isEdible: true },
200
+ rotten: { satietyFactor: 0, poisonChance: 0, morale: 0, isEdible: false }
201
+ },
202
+ cookedPoisonFactor: 0.25
203
+ },
204
+ spawn: {
205
+ retailNew: true,
206
+ retailPlaces: ["supermarket", "pharmacy", "mall", "gas_station", "gun_shop"],
207
+ retailContainers: ["store_shelf", "stockroom"],
208
+ condition: { common: [30, 90], uncommon: [40, 90], rare: [50, 100], very_rare: [50, 100], legendary: [50, 100], unique: [50, 100] },
209
+ ageShare: [0, 0.6],
210
+ charge: [0.2, 1],
211
+ fillable: ["fuel_jerrycan"],
212
+ fill: [0, 0.6]
213
+ },
214
+ finds: {
215
+ fallbackContainer: "zombie_corpse",
216
+ rollExcluded: ["ammo_7_62x39"],
217
+ depletion: [
218
+ { places: ["pharmacy", "gun_shop", "police_station"], perDay: 0.05, floor: 0.3 },
219
+ { places: ["supermarket", "gas_station"], perDay: 0.04, floor: 0.35 },
220
+ { places: ["mall", "hospital"], perDay: 0.025, floor: 0.5 },
221
+ { places: ["house", "apartment"], perDay: 0.01, floor: 0.7 },
222
+ { places: ["farm", "warehouse", "mine", "dam", "lodge", "church"], perDay: 5e-3, floor: 0.85 },
223
+ { places: ["military_base"], perDay: 0, floor: 1 }
224
+ ],
225
+ regionMultiplier: {},
226
+ states: { normal: "normal", fortified: "fortified", abandoned: "abandoned" },
227
+ pool: {
228
+ defaultFootprint: 0.5,
229
+ offset: 1.5,
230
+ perSquareMetre: 3,
231
+ jitter: 1.5,
232
+ min: 2,
233
+ max: 5,
234
+ fortifiedExtra: 2,
235
+ supplyWeight: 2.5,
236
+ supplyExtraQty: 1,
237
+ abandonedEmptyChance: 0.4,
238
+ abandonedKept: 1,
239
+ junkChance: 0.5,
240
+ junkItem: "food_packaging",
241
+ spoiledFoodItem: "leftover_meal",
242
+ coldContainers: ["geladeira", "refrig_exp", "freezer"]
243
+ }
244
+ }
245
+ };
246
+
247
+ // src/create-items.ts
248
+ var LITRES_PER_CUBIC_METRE = 1e3;
249
+ var withDerivedFields = (item) => item.volume === void 0 ? { ...item, volume: item.size[0] * item.size[1] * item.size[2] * LITRES_PER_CUBIC_METRE } : item;
250
+ var buildPools = (options, packs) => {
251
+ const pools = /* @__PURE__ */ new Map();
252
+ for (const source of [...packs, options]) for (const [key, entries] of Object.entries(source.pools ?? {})) pools.set(key, entries);
253
+ const problems = [];
254
+ for (const source of [...packs, options]) {
255
+ for (const [alias, key] of Object.entries(source.poolAliases ?? {})) {
256
+ const entries = pools.get(key);
257
+ if (entries) pools.set(alias, entries);
258
+ else problems.push(`pool alias "${alias}" points to unknown pool "${key}"`);
259
+ }
260
+ }
261
+ if (problems.length > 0) throw new Error(`game-items: the catalog is not consistent:
262
+ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
263
+ return pools;
264
+ };
265
+ var groupBy = (items, keysOf) => {
266
+ const groups = /* @__PURE__ */ new Map();
267
+ for (const item of items) for (const key of keysOf(item)) groups.set(key, [...groups.get(key) ?? [], item]);
268
+ return groups;
269
+ };
270
+ var createItems = (options = {}) => {
271
+ const packs = options.packs ?? [];
272
+ const items = mergeById([], packs.flatMap((pack) => pack.items));
273
+ applyPatches(items, options.patch);
274
+ removeIds(items, options.remove);
275
+ if (options.include) {
276
+ for (const [id, item] of items) if (!options.include(item)) items.delete(id);
277
+ }
278
+ for (const [id, item] of items) items.set(id, withDerivedFields(item));
279
+ const categories = mergeById(ITEM_CATEGORIES, [...packs.flatMap((pack) => pack.categories ?? []), ...options.categories ?? []]);
280
+ const places = mergeById([], [...packs.flatMap((pack) => pack.places ?? []), ...options.places ?? []]);
281
+ const containers = mergeById([], [...packs.flatMap((pack) => pack.containers ?? []), ...options.containers ?? []]);
282
+ const useEffects = [...mergeById([], [...packs.flatMap((pack) => pack.useEffects ?? []), ...options.useEffects ?? []]).values()];
283
+ const recipes = mergeById([], [...packs.flatMap((pack) => pack.recipes ?? []), ...options.recipes ?? []]);
284
+ const pools = buildPools(options, packs);
285
+ const rules = deepMerge(DEFAULT_RULES, options.rules);
286
+ const problems = [
287
+ ...packs.flatMap((pack) => findDuplicateIds(`item in pack "${pack.id}"`, pack.items)),
288
+ ...findDuplicateIds("category", options.categories),
289
+ ...findDuplicateIds("place", options.places),
290
+ ...findDuplicateIds("container", options.containers),
291
+ ...findDuplicateIds("use effect", options.useEffects),
292
+ ...findDuplicateIds("recipe", options.recipes),
293
+ ...checkItems({ items, categories, places, containers, rules })
294
+ ];
295
+ if (problems.length > 0) throw new Error(`game-items: the catalog is not consistent:
296
+ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
297
+ const byCategory = groupBy(items.values(), (item) => [item.category]);
298
+ const byPlace = groupBy(items.values(), (item) => item.finds?.places ?? []);
299
+ const byTag = groupBy(items.values(), (item) => item.tags ?? []);
300
+ return {
301
+ items,
302
+ categories,
303
+ places,
304
+ containers,
305
+ pools,
306
+ useEffects,
307
+ recipes,
308
+ rules,
309
+ has: (id) => items.has(id),
310
+ get(id) {
311
+ const item = items.get(id);
312
+ if (!item) throw new Error(`game-items: unknown item "${id}"`);
313
+ return item;
314
+ },
315
+ list: () => [...items.values()],
316
+ byCategory: (category) => [...byCategory.get(category) ?? []],
317
+ byPlace: (place) => [...byPlace.get(place) ?? []],
318
+ byTag: (tag) => [...byTag.get(tag) ?? []]
319
+ };
320
+ };
321
+
322
+ // src/decay.ts
323
+ var storageRuleOf = (rules, storage) => {
324
+ const rule = rules.storage[storage];
325
+ if (!rule) throw new Error(`game-items: unknown storage "${storage}"`);
326
+ return rule;
327
+ };
328
+ var storageMultiplier = (storage, options = {}) => {
329
+ const { powered = true, rules = DEFAULT_RULES, heldDays = 0 } = options;
330
+ const rule = storageRuleOf(rules, storage);
331
+ if (rule.needsPower && !powered) return 1;
332
+ if (rule.limitDays !== void 0 && heldDays >= rule.limitDays) return rule.afterLimit ?? 1;
333
+ return rule.multiplier;
334
+ };
335
+ var accruedDays = (storage, window, options) => {
336
+ const { powered = true, rules = DEFAULT_RULES } = options;
337
+ const rule = storageRuleOf(rules, storage);
338
+ const elapsed = window.to - window.from;
339
+ if (elapsed <= 0) return 0;
340
+ if (rule.needsPower && !powered) return elapsed;
341
+ if (rule.limitDays === void 0) return elapsed * rule.multiplier;
342
+ const limitEnd = window.storedAt + rule.limitDays;
343
+ const early = Math.max(0, Math.min(window.to, limitEnd) - Math.max(window.from, window.storedAt));
344
+ return early * rule.multiplier + (elapsed - early) * (rule.afterLimit ?? 1);
345
+ };
346
+ var advanceAge = (state, now, options = {}) => {
347
+ const { powered = true, rules = DEFAULT_RULES } = options;
348
+ const rule = storageRuleOf(rules, state.storage);
349
+ const thawDays = rules.aging.thawShelfDays[state.storage];
350
+ const isThawing = rule.needsPower === true && !powered && thawDays !== void 0 && state.shelfLife === void 0;
351
+ const base = isThawing ? { ...state, ageDays: 0, shelfLife: thawDays } : state;
352
+ const ageDays = base.ageDays + accruedDays(state.storage, { storedAt: state.storedAt, from: state.since, to: now }, options);
353
+ return { ...base, ageDays, since: Math.max(now, state.since) };
354
+ };
355
+ var moveToStorage = (state, now, storage, options = {}) => ({
356
+ ...advanceAge(state, now, options),
357
+ storage,
358
+ storedAt: now
359
+ });
360
+ var shelfRatio = (item, state) => {
361
+ const shelfLife = state.shelfLife ?? item.shelfLife;
362
+ return shelfLife === void 0 ? 0 : state.ageDays / shelfLife;
363
+ };
364
+ var freshness = (item, state, now, options = {}) => {
365
+ const { rules = DEFAULT_RULES } = options;
366
+ const ratio = shelfRatio(item, advanceAge(state, now, options));
367
+ if (ratio >= rules.freshness.rotten) return "rotten";
368
+ if (ratio >= rules.freshness.spoiled) return "spoiled";
369
+ if (ratio >= rules.freshness.stale) return "stale";
370
+ return "fresh";
371
+ };
372
+ var chargeLevel = (item, initialCharge, ageDays) => {
373
+ if (!item.charge) return initialCharge;
374
+ return Math.max(0, initialCharge - item.charge.lossPerMonth * (ageDays / 30));
375
+ };
376
+ var failsToStart = (item, ageDays, random) => {
377
+ if (!item.agingFault || ageDays <= item.agingFault.afterDays) return false;
378
+ return random() < item.agingFault.chance;
379
+ };
380
+
381
+ // src/protection.ts
382
+ var FULL_CONDITION = 100;
383
+ var covers = (piece, slot) => piece.item.protection?.slots.includes(slot) ?? false;
384
+ var pieceBlock = (piece, attack, rules = DEFAULT_RULES) => {
385
+ const value = piece.item.protection?.[attack] ?? 0;
386
+ const share = piece.condition <= 0 ? rules.protection.tornShare : piece.condition / FULL_CONDITION;
387
+ return value * share;
388
+ };
389
+ var blockChance = (pieces, slot, attack, rules = DEFAULT_RULES) => {
390
+ const unblocked = pieces.filter((piece) => covers(piece, slot)).reduce((product, piece) => product * (1 - pieceBlock(piece, attack, rules)), 1);
391
+ return Math.min(rules.protection.cap, 1 - unblocked);
392
+ };
393
+ var wearBlocked = (pieces, slot, attack, rules = DEFAULT_RULES) => pieces.map((piece) => {
394
+ if (!covers(piece, slot) || pieceBlock(piece, attack, rules) <= 0) return piece;
395
+ return { ...piece, condition: Math.max(0, piece.condition - rules.protection.wear[attack]) };
396
+ });
397
+ var repairPiece = (piece, rules = DEFAULT_RULES, maxCondition = FULL_CONDITION) => ({
398
+ ...piece,
399
+ condition: Math.max(piece.condition, maxCondition - rules.protection.repairMargin)
400
+ });
401
+
402
+ // src/carry.ts
403
+ var maxLoadKg = (strength, rules = DEFAULT_RULES) => (rules.carry.baseKg + rules.carry.perStrengthKg * strength) * rules.carry.factor;
404
+ var sumKg = (entries) => entries.reduce((total, entry) => total + entry.item.weight * entry.count, 0);
405
+ var carriedKg = (parts) => sumKg(parts.loose) + sumKg(parts.packed ?? []) * (1 - (parts.weightReduction ?? 0));
406
+ var loadState = (loadKg, maxKg, rules = DEFAULT_RULES) => {
407
+ const ratio = maxKg > 0 ? loadKg / maxKg : Number.POSITIVE_INFINITY;
408
+ if (ratio > rules.carry.overloaded) return { ratio, level: "overloaded", runFactor: 0, canRun: false };
409
+ if (ratio > rules.carry.encumbered) return { ratio, level: "encumbered", runFactor: rules.inventory.encumberedRunFactor, canRun: true };
410
+ return { ratio, level: "normal", runFactor: 1, canRun: true };
411
+ };
412
+ var bagOption = (bag, characterMaxKg, rules = DEFAULT_RULES) => ({
413
+ slots: bag?.carry ? Math.round(bag.carry.capacityKg) : rules.inventory.defaultSlots,
414
+ capacityKg: characterMaxKg
415
+ });
416
+ var canTake = (item, bag, used) => {
417
+ if (used.slots >= bag.slots) return "bag_full";
418
+ if (used.loadKg + item.weight > bag.capacityKg) return "too_heavy";
419
+ return "ok";
420
+ };
421
+
422
+ // src/use-effect.ts
423
+ var FRESH_NUTRITION = { satietyFactor: 1, poisonChance: 0, morale: 0, isEdible: true };
424
+ var nutritionOf = (item, context, rules) => {
425
+ const state = context.freshness ?? "fresh";
426
+ const effect = state === "fresh" ? FRESH_NUTRITION : rules.use.freshness[state];
427
+ const poisonShare = context.isCooked ? rules.use.cookedPoisonFactor : 1;
428
+ return {
429
+ satiety: (item.nutrition?.satiety ?? 0) * effect.satietyFactor,
430
+ hydration: item.nutrition?.hydration ?? 0,
431
+ morale: effect.morale,
432
+ poisonChance: effect.poisonChance * poisonShare,
433
+ isEdible: effect.isEdible
434
+ };
435
+ };
436
+ var findEffect = (item, effects) => effects.find((entry) => entry.items?.includes(item.id)) ?? effects.find((entry) => entry.category?.includes(item.category));
437
+ var resolveUse = (item, effects, context = {}, rules = DEFAULT_RULES) => {
438
+ const def = findEffect(item, effects);
439
+ if (!def) return void 0;
440
+ const base = { useId: def.id, effect: def.effect, params: def.params ?? {}, consumes: def.consumes };
441
+ if (def.effect !== "consume_nutrition") return base;
442
+ const nutrition = nutritionOf(item, context, rules);
443
+ return { ...base, consumes: def.consumes && nutrition.isEdible, nutrition };
444
+ };
445
+
446
+ // src/recipes.ts
447
+ var countOf = (context, id) => context.inventory.get(id) ?? 0;
448
+ var candidatesOf = (input, context) => {
449
+ if (input.item !== void 0) return [input.item];
450
+ if (input.any) return [...input.any];
451
+ if (!input.match) return [];
452
+ const { category, cookable } = input.match;
453
+ return context.catalog.list().filter((item) => (category === void 0 || item.category === category) && (cookable === void 0 || item.cookable === cookable)).map((item) => item.id);
454
+ };
455
+ var describeInput = (input) => {
456
+ if (input.item !== void 0) return `item:${input.item}`;
457
+ if (input.any) return `any:${input.any.join("|")}`;
458
+ if (input.match) return `match:${input.match.category ?? "cookable"}`;
459
+ if (input.fluid !== void 0) return `fluid:${input.fluid}`;
460
+ return `world:${(input.world ?? []).join("|")}`;
461
+ };
462
+ var takeUnits = (candidates, needed, context) => {
463
+ const taken = [];
464
+ let remaining = needed;
465
+ for (const id of candidates) {
466
+ if (remaining <= 0) break;
467
+ const count = Math.min(remaining, countOf(context, id));
468
+ if (count > 0) taken.push({ item: id, count });
469
+ remaining -= count;
470
+ }
471
+ return remaining > 0 ? void 0 : taken;
472
+ };
473
+ var consumptionFor = (input, context) => {
474
+ if (input.world) return input.world.some((thing) => context.world?.has(thing)) ? [] : void 0;
475
+ if (input.fluid !== void 0) {
476
+ const liters = input.liters ?? 1;
477
+ return (context.fluids?.[input.fluid] ?? 0) >= liters ? [{ fluid: input.fluid, liters }] : void 0;
478
+ }
479
+ const taken = takeUnits(candidatesOf(input, context), input.qty ?? 1, context);
480
+ if (!taken || input.meters === void 0) return taken;
481
+ return taken.map((entry) => "item" in entry ? { ...entry, meters: input.meters } : entry);
482
+ };
483
+ var toolMissing = (recipe, context) => recipe.tools.flatMap((tool) => {
484
+ const missing = [];
485
+ if (tool.anyOf.length > 0 && !tool.anyOf.some((id) => countOf(context, id) > 0)) missing.push(`tool:${tool.anyOf.join("|")}`);
486
+ if (tool.world && !tool.world.some((station) => context.world?.has(station))) missing.push(`world:${tool.world.join("|")}`);
487
+ return missing;
488
+ });
489
+ var gateMissing = (recipe, context) => {
490
+ const missing = [];
491
+ if (context.skills && recipe.skill && (context.skills[recipe.skill.id] ?? 0) < recipe.skill.level) missing.push(`skill:${recipe.skill.id}:${recipe.skill.level}`);
492
+ if (context.unlocked && recipe.unlockedBy !== void 0 && !context.unlocked.has(recipe.unlockedBy)) missing.push(`unlock:${recipe.unlockedBy}`);
493
+ return missing;
494
+ };
495
+ var canCraft = (recipe, context) => {
496
+ const missing = [
497
+ ...gateMissing(recipe, context),
498
+ ...toolMissing(recipe, context),
499
+ ...recipe.inputs.filter((input) => consumptionFor(input, context) === void 0).map(describeInput)
500
+ ];
501
+ return { isPossible: missing.length === 0, missing };
502
+ };
503
+ var outcomeOf = (output, random) => {
504
+ if ("effect" in output) return { effect: output.effect, params: output.params, ...output.text === void 0 ? {} : { text: output.text } };
505
+ const [low, high] = output.qty;
506
+ return { item: output.item, count: low + Math.floor(random() * (high - low + 1)) };
507
+ };
508
+ var craft = (recipe, context, random) => {
509
+ const check = canCraft(recipe, context);
510
+ if (!check.isPossible) return { isCrafted: false, missing: check.missing };
511
+ const consumed = recipe.inputs.flatMap((input) => consumptionFor(input, context) ?? []);
512
+ return { isCrafted: true, seconds: recipe.seconds, consumed, output: outcomeOf(recipe.output, random) };
513
+ };
514
+
515
+ // src/finds/depletion.ts
516
+ var depletionFactor = (place, day, rules) => {
517
+ if (day <= 0) return 1;
518
+ const rule = rules.finds.depletion.find((entry) => entry.places.includes(place));
519
+ return rule ? Math.max(rule.floor, 1 - rule.perDay * day) : 1;
520
+ };
521
+ var scaleCount = (count, scale, rules) => {
522
+ const region = scale.region === void 0 ? 1 : rules.finds.regionMultiplier[scale.region] ?? 1;
523
+ return Math.round(count * depletionFactor(scale.place, scale.day, rules) * region);
524
+ };
525
+
526
+ // src/finds/rarity-pool.ts
527
+ var rollableItems = (catalog, category, place) => {
528
+ const { rules } = catalog;
529
+ const rollable = catalog.byCategory(category).filter((item) => !rules.rarity.fixedOnly.includes(item.rarity) && !rules.finds.rollExcluded.includes(item.id));
530
+ const here = rollable.filter((item) => item.finds?.places.includes(place));
531
+ return here.length > 0 ? here : rollable;
532
+ };
533
+ var rarityWeight = (catalog, item) => catalog.rules.rarity.weights[item.rarity];
534
+
535
+ // src/finds/category-roll.ts
536
+ var pickCategory = (weights, random) => {
537
+ let remaining = random();
538
+ const categories = Object.keys(weights);
539
+ for (const category of categories) {
540
+ remaining -= weights[category];
541
+ if (remaining <= 0) return category;
542
+ }
543
+ return categories[0];
544
+ };
545
+ var rollByCategory = (catalog, query) => {
546
+ const { rules } = catalog;
547
+ const table = catalog.containers.get(query.container) ?? catalog.containers.get(rules.finds.fallbackContainer);
548
+ if (!table) return [];
549
+ const [low, high] = table.count ?? [0, 0];
550
+ const drawn = low + Math.floor(query.rng() * (high - low + 1));
551
+ const count = scaleCount(drawn, { place: query.place, day: query.day ?? 0, ...query.region === void 0 ? {} : { region: query.region } }, rules);
552
+ const weights = table.categories ?? table.categoriesByPlace?.[query.place] ?? table.categoriesByPlace?.["*"];
553
+ if (!weights) return [];
554
+ const picked = [];
555
+ for (let stack = 0; stack < count; stack++) {
556
+ const category = pickCategory(weights, query.rng);
557
+ const pool = category === void 0 ? [] : rollableItems(catalog, category, query.place);
558
+ if (pool.length === 0) continue;
559
+ const total = pool.reduce((sum, item) => sum + rarityWeight(catalog, item), 0);
560
+ let remaining = query.rng() * total;
561
+ for (const item of pool) {
562
+ remaining -= rarityWeight(catalog, item);
563
+ if (remaining <= 0) {
564
+ picked.push(item.id);
565
+ break;
566
+ }
567
+ }
568
+ }
569
+ return picked;
570
+ };
571
+
572
+ // src/finds/explicit-pool.ts
573
+ var weightedOrder = (candidates, random) => {
574
+ const remaining = [...candidates];
575
+ const ordered = [];
576
+ while (remaining.length > 0) {
577
+ const total = remaining.reduce((sum, candidate) => sum + candidate.weight, 0);
578
+ let x = random() * total;
579
+ let index = 0;
580
+ for (; index < remaining.length - 1; index++) {
581
+ x -= remaining[index].weight;
582
+ if (x < 0) break;
583
+ }
584
+ ordered.push(...remaining.splice(index, 1));
585
+ }
586
+ return ordered;
587
+ };
588
+ var countFor = (pool, state, footprint, query, rules) => {
589
+ const settings = rules.finds.pool;
590
+ const base = Math.max(settings.min, Math.min(settings.max, Math.round(settings.offset + footprint * settings.perSquareMetre + query.rng() * settings.jitter)));
591
+ if (state === "fortified") return Math.min(pool.length, base + settings.fortifiedExtra);
592
+ if (state === "abandoned") return query.rng() < settings.abandonedEmptyChance ? 0 : settings.abandonedKept;
593
+ return base;
594
+ };
595
+ var rollExplicitPool = (pool, state, query, rules) => {
596
+ const settings = rules.finds.pool;
597
+ const n = countFor(pool, state, query.footprint ?? settings.defaultFootprint, query, rules);
598
+ const isFortified = state === "fortified";
599
+ const candidates = pool.map((entry) => ({ entry, weight: isFortified && entry.isSupply ? entry.weight * settings.supplyWeight : entry.weight }));
600
+ const picks = [];
601
+ for (const { entry } of weightedOrder(candidates, query.rng).slice(0, n)) {
602
+ const [low, high] = entry.qty;
603
+ const bonus = isFortified && entry.isSupply ? settings.supplyExtraQty : 0;
604
+ picks.push({ itemId: entry.item, count: low + Math.floor(query.rng() * (high - low + 1)) + bonus, entry });
605
+ }
606
+ if (state === "abandoned") {
607
+ if (picks.length === 0 && query.rng() < settings.junkChance) picks.push({ itemId: settings.junkItem, count: 1 });
608
+ if (settings.coldContainers.includes(query.container) || query.key !== void 0 && settings.coldContainers.includes(query.key)) picks.push({ itemId: settings.spoiledFoodItem, count: 1 });
609
+ }
610
+ return picks;
611
+ };
612
+
613
+ // src/finds/spawn-state.ts
614
+ var WEARING_DURABILITY = ["condition", "hits", "cuts"];
615
+ var hasCondition = (item) => item.protection !== void 0 || item.weapon !== void 0 || WEARING_DURABILITY.includes(item.durability?.kind ?? "");
616
+ var between = (range, random) => range[0] + random() * (range[1] - range[0]);
617
+ var spawnAgeDays = (item, outbreakDays, random, catalog) => item.shelfLife === void 0 ? void 0 : outbreakDays + between(catalog.rules.spawn.ageShare, random) * item.shelfLife;
618
+ var isRetailStock = (query, catalog) => {
619
+ const { spawn } = catalog.rules;
620
+ const isRetail = spawn.retailNew && (query.day ?? 0) <= 0 && spawn.retailPlaces.includes(query.place);
621
+ return isRetail && (spawn.retailContainers.length === 0 || spawn.retailContainers.includes(query.container));
622
+ };
623
+ var spawnData = (item, query, catalog) => {
624
+ const { spawn } = catalog.rules;
625
+ const data = {};
626
+ const ageDays = spawnAgeDays(item, query.day ?? 0, query.rng, catalog);
627
+ if (ageDays !== void 0) data.ageDays = ageDays;
628
+ if (hasCondition(item)) data.condition = isRetailStock(query, catalog) ? 100 : Math.round(between(spawn.condition[item.rarity], query.rng));
629
+ if (item.charge) data.charge = between(spawn.charge, query.rng);
630
+ if (spawn.fillable.includes(item.id)) data.fill = between(spawn.fill, query.rng);
631
+ return Object.keys(data).length > 0 ? data : void 0;
632
+ };
633
+
634
+ // src/finds/provider.ts
635
+ function createFindsProvider(catalog, options = {}) {
636
+ const rules = options.rules ? deepMerge(catalog.rules, options.rules) : catalog.rules;
637
+ const scoped = rules === catalog.rules ? catalog : { ...catalog, rules };
638
+ const usesPools = (options.pools ?? "team") === "team";
639
+ const aliases = options.placeAliases ?? {};
640
+ const poolPlaces = options.poolPlaces;
641
+ const passes = options.unknownContainers === "pass";
642
+ const choose = (query) => {
643
+ const pool = usesPools && (poolPlaces === void 0 || poolPlaces.includes(query.place)) ? scoped.pools.get(query.key ?? query.container) ?? scoped.pools.get(query.container) : void 0;
644
+ if (!pool && passes && !scoped.containers.has(query.container)) return null;
645
+ if (!pool) return rollByCategory(scoped, query).map((itemId) => ({ itemId, count: 1 }));
646
+ return rollExplicitPool(pool, rules.finds.states[query.state] ?? "normal", query, rules).map(({ itemId, count }) => ({ itemId, count }));
647
+ };
648
+ return {
649
+ findsFor(asked) {
650
+ const query = aliases[asked.place] === void 0 ? asked : { ...asked, place: aliases[asked.place] };
651
+ const chosen = choose(query);
652
+ if (chosen === null) return null;
653
+ return chosen.filter((stack) => scoped.has(stack.itemId)).map((stack) => {
654
+ const data = spawnData(scoped.get(stack.itemId), query, scoped);
655
+ return data ? { ...stack, data } : stack;
656
+ });
657
+ }
658
+ };
659
+ }
660
+
661
+ // ../game-interactables/dist/index.js
662
+ function fitsSize(size, max) {
663
+ const eps = 1e-9;
664
+ if (size[1] > max[1] + eps) return false;
665
+ return size[0] <= max[0] + eps && size[2] <= max[2] + eps || size[2] <= max[0] + eps && size[0] <= max[2] + eps;
666
+ }
667
+
668
+ // src/finds/place-finds.ts
669
+ var placeFinds = (stacks, slots, catalog) => {
670
+ const contents = slots.map(() => null);
671
+ const leftover = [];
672
+ for (const stack of stacks) {
673
+ const item = catalog.get(stack.itemId);
674
+ const stackMax = item.stack ?? 1;
675
+ let remaining = stack.count;
676
+ while (remaining > 0) {
677
+ const index = slots.findIndex((slot, position) => contents[position] === null && fitsSize(item.size, slot.max));
678
+ if (index < 0) break;
679
+ const qty = Math.min(remaining, stackMax);
680
+ contents[index] = { type: item.id, qty, ...stack.data ? { data: { ...stack.data } } : {} };
681
+ remaining -= qty;
682
+ }
683
+ if (remaining > 0) leftover.push({ ...stack, count: remaining });
684
+ }
685
+ return { contents, leftover };
686
+ };
687
+
688
+ // src/finds/loot-tables.ts
689
+ var weightsOf = (container, place) => container.categories ?? container.categoriesByPlace?.[place] ?? container.categoriesByPlace?.["*"];
690
+ var tableFor = (catalog, container, query) => {
691
+ const weights = weightsOf(container, query.place);
692
+ if (!weights) return void 0;
693
+ const entries = Object.entries(weights).flatMap(([category, categoryWeight]) => {
694
+ const pool = rollableItems(catalog, category, query.place);
695
+ const total = pool.reduce((sum, item) => sum + rarityWeight(catalog, item), 0);
696
+ return total > 0 ? pool.map((item) => ({ type: item.id, weight: categoryWeight * rarityWeight(catalog, item) / total })) : [];
697
+ });
698
+ const [low, high] = container.count ?? [0, 0];
699
+ const region = query.region === void 0 ? 1 : catalog.rules.finds.regionMultiplier[query.region] ?? 1;
700
+ const expected = (low + high) / 2 * depletionFactor(query.place, query.day ?? 0, catalog.rules) * region;
701
+ return entries.length > 0 && high > 0 ? { fill: Math.min(1, expected / high), entries } : void 0;
702
+ };
703
+ var toLootTables = (catalog, query) => {
704
+ const tables = {};
705
+ for (const container of catalog.containers.values()) {
706
+ if (container.places && container.places.length > 0 && !container.places.includes(query.place)) continue;
707
+ const table = tableFor(catalog, container, query);
708
+ if (table) tables[container.id] = table;
709
+ }
710
+ return tables;
711
+ };
712
+
713
+ // src/arsenal-items.ts
714
+ var ARSENAL_CATEGORIES = [
715
+ { id: "ammo", name: "Ammo" },
716
+ { id: "firearms", name: "Firearms" },
717
+ { id: "throwables", name: "Throwables" }
718
+ ];
719
+ var MM = 1e-3;
720
+ var BOX_TARE_KG = 0.03;
721
+ var BOX_WALL = 6 * MM;
722
+ var ROUNDS_BY_CASE = { tiny: 50, pistol: 50, rifle: 20, shell: 25, big: 10 };
723
+ var DEFAULT_ROUNDS = 12;
724
+ var CARTRIDGE_MASS_FACTOR = 2.3;
725
+ var SHELL_MASS_FACTOR = 1.5;
726
+ var BOX_DENSITY_KG_PER_M3 = 600;
727
+ var THROWABLE_DENSITY_KG_PER_M3 = 1500;
728
+ var cubeSide = (mass, density) => Math.cbrt(mass / density);
729
+ var estimateBox = (caliber) => {
730
+ const rounds = ROUNDS_BY_CASE[caliber.caseType ?? ""] ?? DEFAULT_ROUNDS;
731
+ const factor = caliber.caseType === "shell" ? SHELL_MASS_FACTOR : CARTRIDGE_MASS_FACTOR;
732
+ const weight = Number((rounds * caliber.bulletGrams * (caliber.pellets ?? 1) * factor * 1e-3 + BOX_TARE_KG).toFixed(3));
733
+ return { rounds, weight, size: boxSize(caliber, rounds, weight) };
734
+ };
735
+ var boxSize = (caliber, rounds, weight) => {
736
+ if (!caliber.cartridge) {
737
+ const side = cubeSide(weight, BOX_DENSITY_KG_PER_M3);
738
+ return [side, side, side];
739
+ }
740
+ const rows = Math.ceil(Math.sqrt(rounds));
741
+ const columns = Math.ceil(rounds / rows);
742
+ const { caseDiameter, overallLength } = caliber.cartridge;
743
+ return [rows * caseDiameter * MM + BOX_WALL, overallLength * MM + BOX_WALL, columns * caseDiameter * MM + BOX_WALL];
744
+ };
745
+ var ammoItem = (caliber, record) => {
746
+ const estimate = estimateBox(caliber);
747
+ const weight = record?.weight ?? estimate.weight;
748
+ return {
749
+ id: record?.id ?? `ammo_${caliber.id}`,
750
+ category: "ammo",
751
+ size: record?.weight === void 0 ? estimate.size : boxSize(caliber, record.rounds ?? estimate.rounds, weight),
752
+ weight,
753
+ stack: 4,
754
+ shape: "box",
755
+ rarity: record?.rarity ?? "uncommon"
756
+ };
757
+ };
758
+ var firearmSize = (weapon) => {
759
+ const length = weapon.dimensions?.length ?? 0.15 + 0.2 * weapon.mass;
760
+ return [length, 0.08 + 0.12 * length, 0.02 + 0.03 * length];
761
+ };
762
+ var weaponItem = (weapon, record) => {
763
+ const isFirearm = weapon.action.kind === "firearm";
764
+ const side = cubeSide(weapon.mass, THROWABLE_DENSITY_KG_PER_M3);
765
+ return {
766
+ id: record?.id ?? `${isFirearm ? "firearm" : "throwable"}_${weapon.id}`,
767
+ category: isFirearm ? "firearms" : "throwables",
768
+ size: isFirearm ? firearmSize(weapon) : [side, side, side],
769
+ weight: record?.weight ?? weapon.mass,
770
+ stack: 1,
771
+ shape: isFirearm ? "long_tool" : "jar",
772
+ rarity: record?.rarity ?? "unique",
773
+ weapon: weapon.id
774
+ };
775
+ };
776
+ var recordFor = (records, match) => records.find(match);
777
+ var itemsFromArsenal = (arsenal, options = {}) => {
778
+ const records = options.records ?? [];
779
+ const items = [];
780
+ for (const caliber of arsenal.calibers.values()) items.push(ammoItem(caliber, recordFor(records, (record) => record.kind === "ammo_box" && record.caliber === caliber.id)));
781
+ for (const weapon of arsenal.weapons.values()) {
782
+ const kind = weapon.action.kind;
783
+ if (kind !== "firearm" && kind !== "throwable") continue;
784
+ items.push(weaponItem(weapon, recordFor(records, (record) => record.kind === kind && (record.weapon === weapon.id || record.weapon === weapon.action.explosive))));
785
+ }
786
+ return { id: "arsenal", items, categories: ARSENAL_CATEGORIES };
787
+ };
788
+ return __toCommonJS(index_exports);
789
+ })();
790
+ //# sourceMappingURL=index.global.js.map