quake2ts 0.0.49 → 0.0.50

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 (29) hide show
  1. package/apps/viewer/dist/browser/index.global.js +1 -1
  2. package/apps/viewer/dist/browser/index.global.js.map +1 -1
  3. package/apps/viewer/dist/cjs/index.cjs +33 -15
  4. package/apps/viewer/dist/cjs/index.cjs.map +1 -1
  5. package/apps/viewer/dist/esm/index.js +33 -15
  6. package/apps/viewer/dist/esm/index.js.map +1 -1
  7. package/apps/viewer/dist/tsconfig.tsbuildinfo +1 -1
  8. package/package.json +1 -1
  9. package/packages/game/dist/browser/index.global.js +1 -1
  10. package/packages/game/dist/browser/index.global.js.map +1 -1
  11. package/packages/game/dist/cjs/index.cjs +458 -390
  12. package/packages/game/dist/cjs/index.cjs.map +1 -1
  13. package/packages/game/dist/esm/index.js +454 -390
  14. package/packages/game/dist/esm/index.js.map +1 -1
  15. package/packages/game/dist/tsconfig.tsbuildinfo +1 -1
  16. package/packages/game/dist/types/entities/callbacks.d.ts +6 -0
  17. package/packages/game/dist/types/entities/callbacks.d.ts.map +1 -0
  18. package/packages/game/dist/types/entities/index.d.ts +1 -0
  19. package/packages/game/dist/types/entities/index.d.ts.map +1 -1
  20. package/packages/game/dist/types/entities/system.d.ts +5 -3
  21. package/packages/game/dist/types/entities/system.d.ts.map +1 -1
  22. package/packages/game/dist/types/inventory/playerInventory.d.ts +10 -0
  23. package/packages/game/dist/types/inventory/playerInventory.d.ts.map +1 -1
  24. package/packages/game/dist/types/save/save.d.ts +7 -2
  25. package/packages/game/dist/types/save/save.d.ts.map +1 -1
  26. package/packages/game/dist/types/save/tests/callbacks.test.d.ts +2 -0
  27. package/packages/game/dist/types/save/tests/callbacks.test.d.ts.map +1 -0
  28. package/packages/game/dist/types/save/tests/playerInventory.test.d.ts +2 -0
  29. package/packages/game/dist/types/save/tests/playerInventory.test.d.ts.map +1 -0
@@ -877,7 +877,7 @@ function boundsIntersect(a, b) {
877
877
  return !(a.min.x > b.max.x || a.max.x < b.min.x || a.min.y > b.max.y || a.max.y < b.min.y || a.min.z > b.max.z || a.max.z < b.min.z);
878
878
  }
879
879
  var SERIALIZABLE_FIELDS = ENTITY_FIELD_METADATA.filter(
880
- (field) => field.save
880
+ (field) => field.save || field.type === "callback"
881
881
  );
882
882
  var DESCRIPTORS = new Map(SERIALIZABLE_FIELDS.map((descriptor) => [descriptor.name, descriptor]));
883
883
  function serializeVec3(vec) {
@@ -908,12 +908,18 @@ function deserializeInventory(value) {
908
908
  return parsed;
909
909
  }
910
910
  var EntitySystem = class {
911
- constructor(maxEntities) {
911
+ constructor(maxEntities, callbackRegistry) {
912
912
  this.targetNameIndex = /* @__PURE__ */ new Map();
913
913
  this.random = createRandomGenerator();
914
914
  this.currentTimeSeconds = 0;
915
915
  this.pool = new EntityPool(maxEntities);
916
916
  this.thinkScheduler = new ThinkScheduler();
917
+ this.callbackToName = /* @__PURE__ */ new Map();
918
+ if (callbackRegistry) {
919
+ for (const [name, fn] of callbackRegistry.entries()) {
920
+ this.callbackToName.set(fn, name);
921
+ }
922
+ }
917
923
  }
918
924
  get world() {
919
925
  return this.pool.world;
@@ -1038,6 +1044,9 @@ var EntitySystem = class {
1038
1044
  case "inventory":
1039
1045
  fields[descriptor.name] = serializeInventory(value);
1040
1046
  break;
1047
+ case "callback":
1048
+ fields[descriptor.name] = value ? this.callbackToName.get(value) ?? null : null;
1049
+ break;
1041
1050
  default:
1042
1051
  fields[descriptor.name] = value ?? null;
1043
1052
  break;
@@ -1055,7 +1064,7 @@ var EntitySystem = class {
1055
1064
  thinks: this.thinkScheduler.snapshot()
1056
1065
  };
1057
1066
  }
1058
- restore(snapshot) {
1067
+ restore(snapshot, callbackRegistry) {
1059
1068
  this.currentTimeSeconds = snapshot.timeSeconds;
1060
1069
  this.pool.restore(snapshot.pool);
1061
1070
  const indexToEntity = /* @__PURE__ */ new Map();
@@ -1090,6 +1099,14 @@ var EntitySystem = class {
1090
1099
  case "boolean":
1091
1100
  assignField(entity, name, Boolean(value));
1092
1101
  break;
1102
+ case "callback":
1103
+ if (value) {
1104
+ const callback = callbackRegistry?.get(value);
1105
+ if (callback) {
1106
+ assignField(entity, name, callback);
1107
+ }
1108
+ }
1109
+ break;
1093
1110
  default:
1094
1111
  assignField(entity, name, value);
1095
1112
  break;
@@ -1931,6 +1948,17 @@ function createDefaultSpawnRegistry() {
1931
1948
  return registry;
1932
1949
  }
1933
1950
 
1951
+ // src/entities/callbacks.ts
1952
+ function createCallbackRegistry() {
1953
+ return /* @__PURE__ */ new Map();
1954
+ }
1955
+ function registerCallback(registry, name, fn) {
1956
+ if (registry.has(name)) {
1957
+ return;
1958
+ }
1959
+ registry.set(name, fn);
1960
+ }
1961
+
1934
1962
  // src/loop.ts
1935
1963
  var orderedStageNames = [
1936
1964
  "prep",
@@ -2443,126 +2471,430 @@ function hashGameState(state) {
2443
2471
  return hash >>> 0;
2444
2472
  }
2445
2473
 
2446
- // src/save/save.ts
2447
- var SAVE_FORMAT_VERSION = 1;
2448
- var MIN_SUPPORTED_VERSION = 1;
2449
- function ensureObject(value, label) {
2450
- if (!value || typeof value !== "object" || Array.isArray(value)) {
2451
- throw new Error(`${label} must be an object`);
2452
- }
2453
- return value;
2474
+ // src/inventory/ammo.ts
2475
+ var AmmoType = /* @__PURE__ */ ((AmmoType3) => {
2476
+ AmmoType3[AmmoType3["Bullets"] = 0] = "Bullets";
2477
+ AmmoType3[AmmoType3["Shells"] = 1] = "Shells";
2478
+ AmmoType3[AmmoType3["Rockets"] = 2] = "Rockets";
2479
+ AmmoType3[AmmoType3["Grenades"] = 3] = "Grenades";
2480
+ AmmoType3[AmmoType3["Cells"] = 4] = "Cells";
2481
+ AmmoType3[AmmoType3["Slugs"] = 5] = "Slugs";
2482
+ return AmmoType3;
2483
+ })(AmmoType || {});
2484
+ var AMMO_TYPE_COUNT = Object.keys(AmmoType).length / 2;
2485
+ var AmmoItemId = /* @__PURE__ */ ((AmmoItemId3) => {
2486
+ AmmoItemId3["Shells"] = "ammo_shells";
2487
+ AmmoItemId3["Bullets"] = "ammo_bullets";
2488
+ AmmoItemId3["Rockets"] = "ammo_rockets";
2489
+ AmmoItemId3["Grenades"] = "ammo_grenades";
2490
+ AmmoItemId3["Cells"] = "ammo_cells";
2491
+ AmmoItemId3["Slugs"] = "ammo_slugs";
2492
+ return AmmoItemId3;
2493
+ })(AmmoItemId || {});
2494
+ var AMMO_ITEM_DEFINITIONS = {
2495
+ ["ammo_shells" /* Shells */]: { id: "ammo_shells" /* Shells */, ammoType: 1 /* Shells */, quantity: 10, weaponAmmo: false },
2496
+ ["ammo_bullets" /* Bullets */]: { id: "ammo_bullets" /* Bullets */, ammoType: 0 /* Bullets */, quantity: 50, weaponAmmo: false },
2497
+ ["ammo_rockets" /* Rockets */]: { id: "ammo_rockets" /* Rockets */, ammoType: 2 /* Rockets */, quantity: 5, weaponAmmo: false },
2498
+ ["ammo_grenades" /* Grenades */]: { id: "ammo_grenades" /* Grenades */, ammoType: 3 /* Grenades */, quantity: 5, weaponAmmo: true },
2499
+ ["ammo_cells" /* Cells */]: { id: "ammo_cells" /* Cells */, ammoType: 4 /* Cells */, quantity: 50, weaponAmmo: false },
2500
+ ["ammo_slugs" /* Slugs */]: { id: "ammo_slugs" /* Slugs */, ammoType: 5 /* Slugs */, quantity: 10, weaponAmmo: false }
2501
+ };
2502
+ function getAmmoItemDefinition(id) {
2503
+ return AMMO_ITEM_DEFINITIONS[id];
2454
2504
  }
2455
- function ensureNumber(value, label) {
2456
- if (typeof value !== "number" || !Number.isFinite(value)) {
2457
- throw new Error(`${label} must be a finite number`);
2458
- }
2459
- return value;
2505
+ function createAmmoInventory(caps = createBaseAmmoCaps()) {
2506
+ return { caps: caps.slice(), counts: Array(AMMO_TYPE_COUNT).fill(0) };
2460
2507
  }
2461
- function ensureNumberOrDefault(value, label, defaultValue) {
2462
- if (value === void 0) {
2463
- return defaultValue;
2508
+ function createBaseAmmoCaps() {
2509
+ const caps = Array(AMMO_TYPE_COUNT).fill(50);
2510
+ caps[0 /* Bullets */] = 200;
2511
+ caps[1 /* Shells */] = 100;
2512
+ caps[4 /* Cells */] = 200;
2513
+ return caps;
2514
+ }
2515
+ function clampAmmoCounts(counts, caps) {
2516
+ const limit = Math.min(counts.length, caps.length);
2517
+ const clamped = counts.slice(0, limit);
2518
+ for (let i = 0; i < limit; i++) {
2519
+ const cap = caps[i];
2520
+ if (cap !== void 0) {
2521
+ clamped[i] = Math.min(counts[i], cap);
2522
+ }
2464
2523
  }
2465
- return ensureNumber(value, label);
2524
+ return clamped;
2466
2525
  }
2467
- function ensureString(value, label) {
2468
- if (typeof value !== "string") {
2469
- throw new Error(`${label} must be a string`);
2526
+ function addAmmo(inventory, ammoType, amount) {
2527
+ const cap = inventory.caps[ammoType];
2528
+ const current = inventory.counts[ammoType] ?? 0;
2529
+ if (cap !== void 0 && current >= cap) {
2530
+ return { ammoType, added: 0, newCount: current, capped: cap, pickedUp: false };
2470
2531
  }
2471
- return value;
2532
+ const uncapped = current + amount;
2533
+ const newCount = cap === void 0 ? uncapped : Math.min(uncapped, cap);
2534
+ const added = newCount - current;
2535
+ inventory.counts[ammoType] = newCount;
2536
+ return { ammoType, added, newCount, capped: cap ?? Number.POSITIVE_INFINITY, pickedUp: added > 0 };
2472
2537
  }
2473
- function ensureNumberArray(value, label) {
2474
- if (!Array.isArray(value)) {
2475
- throw new Error(`${label} must be an array`);
2538
+ function pickupAmmo(inventory, itemId, options = {}) {
2539
+ const def = getAmmoItemDefinition(itemId);
2540
+ const amount = options.countOverride ?? def.quantity;
2541
+ return addAmmo(inventory, def.ammoType, amount);
2542
+ }
2543
+
2544
+ // src/combat/damageFlags.ts
2545
+ var DamageFlags = /* @__PURE__ */ ((DamageFlags2) => {
2546
+ DamageFlags2[DamageFlags2["NONE"] = 0] = "NONE";
2547
+ DamageFlags2[DamageFlags2["RADIUS"] = 1] = "RADIUS";
2548
+ DamageFlags2[DamageFlags2["NO_ARMOR"] = 2] = "NO_ARMOR";
2549
+ DamageFlags2[DamageFlags2["ENERGY"] = 4] = "ENERGY";
2550
+ DamageFlags2[DamageFlags2["NO_KNOCKBACK"] = 8] = "NO_KNOCKBACK";
2551
+ DamageFlags2[DamageFlags2["BULLET"] = 16] = "BULLET";
2552
+ DamageFlags2[DamageFlags2["NO_PROTECTION"] = 32] = "NO_PROTECTION";
2553
+ DamageFlags2[DamageFlags2["DESTROY_ARMOR"] = 64] = "DESTROY_ARMOR";
2554
+ DamageFlags2[DamageFlags2["NO_REG_ARMOR"] = 128] = "NO_REG_ARMOR";
2555
+ DamageFlags2[DamageFlags2["NO_POWER_ARMOR"] = 256] = "NO_POWER_ARMOR";
2556
+ DamageFlags2[DamageFlags2["NO_INDICATOR"] = 512] = "NO_INDICATOR";
2557
+ return DamageFlags2;
2558
+ })(DamageFlags || {});
2559
+ function hasAnyDamageFlag(flags, mask) {
2560
+ return (flags & mask) !== 0;
2561
+ }
2562
+
2563
+ // src/combat/armor.ts
2564
+ var ArmorType = /* @__PURE__ */ ((ArmorType3) => {
2565
+ ArmorType3["BODY"] = "body";
2566
+ ArmorType3["COMBAT"] = "combat";
2567
+ ArmorType3["JACKET"] = "jacket";
2568
+ return ArmorType3;
2569
+ })(ArmorType || {});
2570
+ var ARMOR_INFO = {
2571
+ ["jacket" /* JACKET */]: {
2572
+ baseCount: 25,
2573
+ maxCount: 50,
2574
+ normalProtection: 0.3,
2575
+ energyProtection: 0
2576
+ },
2577
+ ["combat" /* COMBAT */]: {
2578
+ baseCount: 50,
2579
+ maxCount: 100,
2580
+ normalProtection: 0.6,
2581
+ energyProtection: 0.3
2582
+ },
2583
+ ["body" /* BODY */]: {
2584
+ baseCount: 100,
2585
+ maxCount: 200,
2586
+ normalProtection: 0.8,
2587
+ energyProtection: 0.6
2476
2588
  }
2477
- for (const element of value) {
2478
- ensureNumber(element, `${label} element`);
2589
+ };
2590
+ function applyRegularArmor(damage, flags, state) {
2591
+ if (damage <= 0 || hasAnyDamageFlag(flags, 2 /* NO_ARMOR */ | 128 /* NO_REG_ARMOR */) || !state.armorType || state.armorCount <= 0) {
2592
+ return { saved: 0, remainingArmor: state.armorCount };
2479
2593
  }
2480
- return value;
2481
- }
2482
- function parseLevelState(raw) {
2483
- if (raw === void 0) {
2484
- return { frameNumber: 0, timeSeconds: 0, previousTimeSeconds: 0, deltaSeconds: 0 };
2594
+ const info = ARMOR_INFO[state.armorType];
2595
+ const protection = hasAnyDamageFlag(flags, 4 /* ENERGY */) ? info.energyProtection : info.normalProtection;
2596
+ let saved = Math.ceil(protection * damage);
2597
+ if (saved >= state.armorCount) {
2598
+ saved = state.armorCount;
2485
2599
  }
2486
- const level = ensureObject(raw, "level");
2487
- return {
2488
- frameNumber: ensureNumberOrDefault(level.frameNumber, "level.frameNumber", 0),
2489
- timeSeconds: ensureNumberOrDefault(level.timeSeconds, "level.timeSeconds", 0),
2490
- previousTimeSeconds: ensureNumberOrDefault(level.previousTimeSeconds, "level.previousTimeSeconds", 0),
2491
- deltaSeconds: ensureNumberOrDefault(level.deltaSeconds, "level.deltaSeconds", 0)
2492
- };
2493
- }
2494
- function parseRngState(raw) {
2495
- if (raw === void 0) {
2496
- return new RandomGenerator().getState();
2600
+ if (saved <= 0) {
2601
+ return { saved: 0, remainingArmor: state.armorCount };
2497
2602
  }
2498
- const rng = ensureObject(raw, "rng");
2499
- const mt = ensureObject(rng.mt, "rng.mt");
2500
- const state = ensureNumberArray(mt.state, "rng.mt.state");
2501
- return {
2502
- mt: {
2503
- index: ensureNumber(mt.index, "rng.mt.index"),
2504
- state
2505
- }
2506
- };
2603
+ return { saved, remainingArmor: state.armorCount - saved };
2507
2604
  }
2508
- function parseThinkEntries(raw) {
2509
- if (raw === void 0) {
2510
- return [];
2605
+ function applyPowerArmor(damage, flags, hitPoint, _hitNormal, state, options = {}) {
2606
+ if (state.health <= 0 || damage <= 0) {
2607
+ return { saved: 0, remainingCells: state.cellCount };
2511
2608
  }
2512
- if (!Array.isArray(raw)) {
2513
- throw new Error("thinks must be an array");
2609
+ if (hasAnyDamageFlag(flags, 2 /* NO_ARMOR */ | 256 /* NO_POWER_ARMOR */)) {
2610
+ return { saved: 0, remainingCells: state.cellCount };
2514
2611
  }
2515
- return raw.map((entry, idx) => {
2516
- const think = ensureObject(entry, `thinks[${idx}]`);
2517
- return {
2518
- time: ensureNumber(think.time, `thinks[${idx}].time`),
2519
- entityIndex: ensureNumber(think.entityIndex, `thinks[${idx}].entityIndex`)
2520
- };
2521
- });
2522
- }
2523
- function parseEntityFields(raw) {
2524
- if (raw === void 0) {
2525
- return {};
2612
+ if (!state.type || state.cellCount <= 0) {
2613
+ return { saved: 0, remainingCells: state.cellCount };
2526
2614
  }
2527
- const fields = ensureObject(raw, "entity.fields");
2528
- const parsed = {};
2529
- for (const [name, value] of Object.entries(fields)) {
2530
- if (value === null) {
2531
- parsed[name] = null;
2532
- continue;
2533
- }
2534
- switch (typeof value) {
2535
- case "number":
2536
- case "string":
2537
- case "boolean":
2538
- parsed[name] = value;
2539
- break;
2540
- default: {
2541
- if (!Array.isArray(value)) {
2542
- const object = ensureObject(value, name);
2543
- const inventory = {};
2544
- for (const [entryName, entryValue] of Object.entries(object)) {
2545
- inventory[entryName] = ensureNumber(entryValue, `${name}.${entryName}`);
2546
- }
2547
- parsed[name] = inventory;
2548
- break;
2549
- }
2550
- if (Array.isArray(value) && value.length === 3) {
2551
- const [x, y, z] = value;
2552
- parsed[name] = [
2553
- ensureNumber(x, `${name}[0]`),
2554
- ensureNumber(y, `${name}[1]`),
2555
- ensureNumber(z, `${name}[2]`)
2556
- ];
2557
- break;
2558
- }
2559
- throw new Error(`Unsupported entity field value for ${name}`);
2560
- }
2615
+ const { forward } = angleVectors(state.angles);
2616
+ const toImpact = {
2617
+ x: hitPoint.x - state.origin.x,
2618
+ y: hitPoint.y - state.origin.y,
2619
+ z: hitPoint.z - state.origin.z
2620
+ };
2621
+ const toImpactLength = Math.hypot(toImpact.x, toImpact.y, toImpact.z);
2622
+ if (state.type === "screen" && toImpactLength > 0) {
2623
+ const dir = {
2624
+ x: toImpact.x / toImpactLength,
2625
+ y: toImpact.y / toImpactLength,
2626
+ z: toImpact.z / toImpactLength
2627
+ };
2628
+ const dot = dir.x * forward.x + dir.y * forward.y + dir.z * forward.z;
2629
+ if (dot <= 0.3) {
2630
+ return { saved: 0, remainingCells: state.cellCount };
2561
2631
  }
2562
2632
  }
2563
- return parsed;
2564
- }
2565
- function parseEntities(raw) {
2633
+ const ctfMode = options.ctfMode ?? false;
2634
+ const damagePerCell = state.type === "screen" ? 1 : ctfMode ? 1 : 2;
2635
+ let adjustedDamage = state.type === "screen" ? damage / 3 : 2 * damage / 3;
2636
+ adjustedDamage = Math.max(1, adjustedDamage);
2637
+ let saved = state.cellCount * damagePerCell;
2638
+ if (hasAnyDamageFlag(flags, 4 /* ENERGY */)) {
2639
+ saved = Math.max(1, Math.floor(saved / 2));
2640
+ }
2641
+ if (saved > adjustedDamage) {
2642
+ saved = Math.floor(adjustedDamage);
2643
+ }
2644
+ let powerUsed = saved / damagePerCell;
2645
+ if (hasAnyDamageFlag(flags, 4 /* ENERGY */)) {
2646
+ powerUsed *= 2;
2647
+ }
2648
+ powerUsed = Math.max(1, Math.floor(powerUsed));
2649
+ const cellsSpent = Math.max(damagePerCell, powerUsed);
2650
+ const remainingCells = Math.max(0, state.cellCount - cellsSpent);
2651
+ return { saved, remainingCells };
2652
+ }
2653
+
2654
+ // src/inventory/playerInventory.ts
2655
+ var WeaponId = /* @__PURE__ */ ((WeaponId2) => {
2656
+ WeaponId2["Blaster"] = "blaster";
2657
+ WeaponId2["Shotgun"] = "shotgun";
2658
+ WeaponId2["SuperShotgun"] = "super_shotgun";
2659
+ WeaponId2["Machinegun"] = "machinegun";
2660
+ WeaponId2["Chaingun"] = "chaingun";
2661
+ WeaponId2["GrenadeLauncher"] = "grenade_launcher";
2662
+ WeaponId2["RocketLauncher"] = "rocket_launcher";
2663
+ WeaponId2["HyperBlaster"] = "hyperblaster";
2664
+ WeaponId2["Railgun"] = "railgun";
2665
+ WeaponId2["BFG10K"] = "bfg10k";
2666
+ return WeaponId2;
2667
+ })(WeaponId || {});
2668
+ var PowerupId = /* @__PURE__ */ ((PowerupId2) => {
2669
+ PowerupId2["QuadDamage"] = "quad";
2670
+ PowerupId2["Invulnerability"] = "invulnerability";
2671
+ PowerupId2["EnviroSuit"] = "enviro_suit";
2672
+ PowerupId2["Rebreather"] = "rebreather";
2673
+ PowerupId2["Silencer"] = "silencer";
2674
+ return PowerupId2;
2675
+ })(PowerupId || {});
2676
+ var KeyId = /* @__PURE__ */ ((KeyId2) => {
2677
+ KeyId2["Blue"] = "blue";
2678
+ KeyId2["Red"] = "red";
2679
+ KeyId2["Green"] = "green";
2680
+ KeyId2["Yellow"] = "yellow";
2681
+ return KeyId2;
2682
+ })(KeyId || {});
2683
+ function createPlayerInventory(options = {}) {
2684
+ const ammo = createAmmoInventory(options.ammoCaps);
2685
+ const ownedWeapons = new Set(options.weapons ?? []);
2686
+ const powerups = new Map(options.powerups ?? []);
2687
+ const keys = new Set(options.keys ?? []);
2688
+ return {
2689
+ ammo,
2690
+ ownedWeapons,
2691
+ currentWeapon: options.currentWeapon,
2692
+ armor: options.armor ?? null,
2693
+ powerups,
2694
+ keys
2695
+ };
2696
+ }
2697
+ function giveAmmo(inventory, ammoType, amount) {
2698
+ return addAmmo(inventory.ammo, ammoType, amount);
2699
+ }
2700
+ function giveAmmoItem(inventory, itemId, options) {
2701
+ return pickupAmmo(inventory.ammo, itemId, options);
2702
+ }
2703
+ function giveWeapon(inventory, weapon, select = false) {
2704
+ const hadWeapon = inventory.ownedWeapons.has(weapon);
2705
+ inventory.ownedWeapons.add(weapon);
2706
+ if (select || !inventory.currentWeapon) {
2707
+ inventory.currentWeapon = weapon;
2708
+ }
2709
+ return !hadWeapon;
2710
+ }
2711
+ function hasWeapon(inventory, weapon) {
2712
+ return inventory.ownedWeapons.has(weapon);
2713
+ }
2714
+ function selectWeapon(inventory, weapon) {
2715
+ if (!inventory.ownedWeapons.has(weapon)) {
2716
+ return false;
2717
+ }
2718
+ inventory.currentWeapon = weapon;
2719
+ return true;
2720
+ }
2721
+ function equipArmor(inventory, armorType, amount) {
2722
+ if (!armorType || amount <= 0) {
2723
+ inventory.armor = null;
2724
+ return null;
2725
+ }
2726
+ const info = ARMOR_INFO[armorType];
2727
+ const armorCount = Math.min(amount, info.maxCount);
2728
+ inventory.armor = { armorType, armorCount };
2729
+ return inventory.armor;
2730
+ }
2731
+ function addPowerup(inventory, powerup, expiresAt) {
2732
+ inventory.powerups.set(powerup, expiresAt);
2733
+ }
2734
+ function hasPowerup(inventory, powerup) {
2735
+ return inventory.powerups.has(powerup);
2736
+ }
2737
+ function clearExpiredPowerups(inventory, nowMs) {
2738
+ for (const [id, expiresAt] of inventory.powerups.entries()) {
2739
+ if (expiresAt !== null && expiresAt <= nowMs) {
2740
+ inventory.powerups.delete(id);
2741
+ }
2742
+ }
2743
+ }
2744
+ function addKey(inventory, key) {
2745
+ const before = inventory.keys.size;
2746
+ inventory.keys.add(key);
2747
+ return inventory.keys.size > before;
2748
+ }
2749
+ function hasKey(inventory, key) {
2750
+ return inventory.keys.has(key);
2751
+ }
2752
+ function serializePlayerInventory(inventory) {
2753
+ return {
2754
+ ammo: inventory.ammo.counts,
2755
+ ownedWeapons: [...inventory.ownedWeapons],
2756
+ currentWeapon: inventory.currentWeapon,
2757
+ armor: inventory.armor ? { ...inventory.armor } : null,
2758
+ powerups: [...inventory.powerups.entries()],
2759
+ keys: [...inventory.keys]
2760
+ };
2761
+ }
2762
+ function deserializePlayerInventory(serialized, options = {}) {
2763
+ const ammo = createAmmoInventory(options.ammoCaps);
2764
+ const limit = Math.min(ammo.counts.length, serialized.ammo.length);
2765
+ for (let i = 0; i < limit; i++) {
2766
+ ammo.counts[i] = serialized.ammo[i];
2767
+ }
2768
+ return {
2769
+ ammo,
2770
+ ownedWeapons: new Set(serialized.ownedWeapons),
2771
+ currentWeapon: serialized.currentWeapon,
2772
+ armor: serialized.armor ? { ...serialized.armor } : null,
2773
+ powerups: new Map(serialized.powerups),
2774
+ keys: new Set(serialized.keys)
2775
+ };
2776
+ }
2777
+
2778
+ // src/save/save.ts
2779
+ var SAVE_FORMAT_VERSION = 2;
2780
+ var MIN_SUPPORTED_VERSION = 1;
2781
+ function ensureObject(value, label) {
2782
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2783
+ throw new Error(`${label} must be an object`);
2784
+ }
2785
+ return value;
2786
+ }
2787
+ function ensureNumber(value, label) {
2788
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2789
+ throw new Error(`${label} must be a finite number`);
2790
+ }
2791
+ return value;
2792
+ }
2793
+ function ensureNumberOrDefault(value, label, defaultValue) {
2794
+ if (value === void 0) {
2795
+ return defaultValue;
2796
+ }
2797
+ return ensureNumber(value, label);
2798
+ }
2799
+ function ensureString(value, label) {
2800
+ if (typeof value !== "string") {
2801
+ throw new Error(`${label} must be a string`);
2802
+ }
2803
+ return value;
2804
+ }
2805
+ function ensureNumberArray(value, label) {
2806
+ if (!Array.isArray(value)) {
2807
+ throw new Error(`${label} must be an array`);
2808
+ }
2809
+ for (const element of value) {
2810
+ ensureNumber(element, `${label} element`);
2811
+ }
2812
+ return value;
2813
+ }
2814
+ function parseLevelState(raw) {
2815
+ if (raw === void 0) {
2816
+ return { frameNumber: 0, timeSeconds: 0, previousTimeSeconds: 0, deltaSeconds: 0 };
2817
+ }
2818
+ const level = ensureObject(raw, "level");
2819
+ return {
2820
+ frameNumber: ensureNumberOrDefault(level.frameNumber, "level.frameNumber", 0),
2821
+ timeSeconds: ensureNumberOrDefault(level.timeSeconds, "level.timeSeconds", 0),
2822
+ previousTimeSeconds: ensureNumberOrDefault(level.previousTimeSeconds, "level.previousTimeSeconds", 0),
2823
+ deltaSeconds: ensureNumberOrDefault(level.deltaSeconds, "level.deltaSeconds", 0)
2824
+ };
2825
+ }
2826
+ function parseRngState(raw) {
2827
+ if (raw === void 0) {
2828
+ return new RandomGenerator().getState();
2829
+ }
2830
+ const rng = ensureObject(raw, "rng");
2831
+ const mt = ensureObject(rng.mt, "rng.mt");
2832
+ const state = ensureNumberArray(mt.state, "rng.mt.state");
2833
+ return {
2834
+ mt: {
2835
+ index: ensureNumber(mt.index, "rng.mt.index"),
2836
+ state
2837
+ }
2838
+ };
2839
+ }
2840
+ function parseThinkEntries(raw) {
2841
+ if (raw === void 0) {
2842
+ return [];
2843
+ }
2844
+ if (!Array.isArray(raw)) {
2845
+ throw new Error("thinks must be an array");
2846
+ }
2847
+ return raw.map((entry, idx) => {
2848
+ const think = ensureObject(entry, `thinks[${idx}]`);
2849
+ return {
2850
+ time: ensureNumber(think.time, `thinks[${idx}].time`),
2851
+ entityIndex: ensureNumber(think.entityIndex, `thinks[${idx}].entityIndex`)
2852
+ };
2853
+ });
2854
+ }
2855
+ function parseEntityFields(raw) {
2856
+ if (raw === void 0) {
2857
+ return {};
2858
+ }
2859
+ const fields = ensureObject(raw, "entity.fields");
2860
+ const parsed = {};
2861
+ for (const [name, value] of Object.entries(fields)) {
2862
+ if (value === null) {
2863
+ parsed[name] = null;
2864
+ continue;
2865
+ }
2866
+ switch (typeof value) {
2867
+ case "number":
2868
+ case "string":
2869
+ case "boolean":
2870
+ parsed[name] = value;
2871
+ break;
2872
+ default: {
2873
+ if (!Array.isArray(value)) {
2874
+ const object = ensureObject(value, name);
2875
+ const inventory = {};
2876
+ for (const [entryName, entryValue] of Object.entries(object)) {
2877
+ inventory[entryName] = ensureNumber(entryValue, `${name}.${entryName}`);
2878
+ }
2879
+ parsed[name] = inventory;
2880
+ break;
2881
+ }
2882
+ if (Array.isArray(value) && value.length === 3) {
2883
+ const [x, y, z] = value;
2884
+ parsed[name] = [
2885
+ ensureNumber(x, `${name}[0]`),
2886
+ ensureNumber(y, `${name}[1]`),
2887
+ ensureNumber(z, `${name}[2]`)
2888
+ ];
2889
+ break;
2890
+ }
2891
+ throw new Error(`Unsupported entity field value for ${name}`);
2892
+ }
2893
+ }
2894
+ }
2895
+ return parsed;
2896
+ }
2897
+ function parseEntities(raw) {
2566
2898
  if (!Array.isArray(raw)) {
2567
2899
  throw new Error("entities must be an array");
2568
2900
  }
@@ -2661,7 +2993,8 @@ function createSaveFile(options) {
2661
2993
  configstrings = [],
2662
2994
  cvars,
2663
2995
  gameState = {},
2664
- timestamp = Date.now()
2996
+ timestamp = Date.now(),
2997
+ player
2665
2998
  } = options;
2666
2999
  return {
2667
3000
  version: SAVE_FORMAT_VERSION,
@@ -2674,7 +3007,8 @@ function createSaveFile(options) {
2674
3007
  rng: cloneRngState(rngState),
2675
3008
  entities: entitySystem.createSnapshot(),
2676
3009
  cvars: serializeCvars(cvars),
2677
- configstrings: [...configstrings]
3010
+ configstrings: [...configstrings],
3011
+ player: player ? serializePlayerInventory(player) : void 0
2678
3012
  };
2679
3013
  }
2680
3014
  function parseSaveFile(serialized, options = {}) {
@@ -2700,14 +3034,19 @@ function parseSaveFile(serialized, options = {}) {
2700
3034
  rng: parseRngState(save.rng),
2701
3035
  entities: parseEntitySnapshot(save.entities),
2702
3036
  cvars: parseCvars(save.cvars),
2703
- configstrings: parseConfigstrings(save.configstrings)
3037
+ configstrings: parseConfigstrings(save.configstrings),
3038
+ player: save.player ? save.player : void 0
2704
3039
  };
2705
3040
  }
2706
3041
  function applySaveFile(save, targets) {
2707
3042
  targets.levelClock.restore(save.level);
2708
- targets.entitySystem.restore(save.entities);
3043
+ targets.entitySystem.restore(save.entities, targets.callbackRegistry);
2709
3044
  targets.rng.setState(save.rng);
2710
3045
  applyCvars(save.cvars, targets.cvars);
3046
+ if (save.player && targets.player) {
3047
+ const deserialized = deserializePlayerInventory(save.player);
3048
+ Object.assign(targets.player, deserialized);
3049
+ }
2711
3050
  }
2712
3051
 
2713
3052
  // src/save/rerelease.ts
@@ -3099,116 +3438,6 @@ _SaveStorage.DEFAULT_STORE = "saves";
3099
3438
  _SaveStorage.QUICK_SLOT = "quicksave";
3100
3439
  var SaveStorage = _SaveStorage;
3101
3440
 
3102
- // src/combat/damageFlags.ts
3103
- var DamageFlags = /* @__PURE__ */ ((DamageFlags2) => {
3104
- DamageFlags2[DamageFlags2["NONE"] = 0] = "NONE";
3105
- DamageFlags2[DamageFlags2["RADIUS"] = 1] = "RADIUS";
3106
- DamageFlags2[DamageFlags2["NO_ARMOR"] = 2] = "NO_ARMOR";
3107
- DamageFlags2[DamageFlags2["ENERGY"] = 4] = "ENERGY";
3108
- DamageFlags2[DamageFlags2["NO_KNOCKBACK"] = 8] = "NO_KNOCKBACK";
3109
- DamageFlags2[DamageFlags2["BULLET"] = 16] = "BULLET";
3110
- DamageFlags2[DamageFlags2["NO_PROTECTION"] = 32] = "NO_PROTECTION";
3111
- DamageFlags2[DamageFlags2["DESTROY_ARMOR"] = 64] = "DESTROY_ARMOR";
3112
- DamageFlags2[DamageFlags2["NO_REG_ARMOR"] = 128] = "NO_REG_ARMOR";
3113
- DamageFlags2[DamageFlags2["NO_POWER_ARMOR"] = 256] = "NO_POWER_ARMOR";
3114
- DamageFlags2[DamageFlags2["NO_INDICATOR"] = 512] = "NO_INDICATOR";
3115
- return DamageFlags2;
3116
- })(DamageFlags || {});
3117
- function hasAnyDamageFlag(flags, mask) {
3118
- return (flags & mask) !== 0;
3119
- }
3120
-
3121
- // src/combat/armor.ts
3122
- var ArmorType = /* @__PURE__ */ ((ArmorType3) => {
3123
- ArmorType3["BODY"] = "body";
3124
- ArmorType3["COMBAT"] = "combat";
3125
- ArmorType3["JACKET"] = "jacket";
3126
- return ArmorType3;
3127
- })(ArmorType || {});
3128
- var ARMOR_INFO = {
3129
- ["jacket" /* JACKET */]: {
3130
- baseCount: 25,
3131
- maxCount: 50,
3132
- normalProtection: 0.3,
3133
- energyProtection: 0
3134
- },
3135
- ["combat" /* COMBAT */]: {
3136
- baseCount: 50,
3137
- maxCount: 100,
3138
- normalProtection: 0.6,
3139
- energyProtection: 0.3
3140
- },
3141
- ["body" /* BODY */]: {
3142
- baseCount: 100,
3143
- maxCount: 200,
3144
- normalProtection: 0.8,
3145
- energyProtection: 0.6
3146
- }
3147
- };
3148
- function applyRegularArmor(damage, flags, state) {
3149
- if (damage <= 0 || hasAnyDamageFlag(flags, 2 /* NO_ARMOR */ | 128 /* NO_REG_ARMOR */) || !state.armorType || state.armorCount <= 0) {
3150
- return { saved: 0, remainingArmor: state.armorCount };
3151
- }
3152
- const info = ARMOR_INFO[state.armorType];
3153
- const protection = hasAnyDamageFlag(flags, 4 /* ENERGY */) ? info.energyProtection : info.normalProtection;
3154
- let saved = Math.ceil(protection * damage);
3155
- if (saved >= state.armorCount) {
3156
- saved = state.armorCount;
3157
- }
3158
- if (saved <= 0) {
3159
- return { saved: 0, remainingArmor: state.armorCount };
3160
- }
3161
- return { saved, remainingArmor: state.armorCount - saved };
3162
- }
3163
- function applyPowerArmor(damage, flags, hitPoint, _hitNormal, state, options = {}) {
3164
- if (state.health <= 0 || damage <= 0) {
3165
- return { saved: 0, remainingCells: state.cellCount };
3166
- }
3167
- if (hasAnyDamageFlag(flags, 2 /* NO_ARMOR */ | 256 /* NO_POWER_ARMOR */)) {
3168
- return { saved: 0, remainingCells: state.cellCount };
3169
- }
3170
- if (!state.type || state.cellCount <= 0) {
3171
- return { saved: 0, remainingCells: state.cellCount };
3172
- }
3173
- const { forward } = angleVectors(state.angles);
3174
- const toImpact = {
3175
- x: hitPoint.x - state.origin.x,
3176
- y: hitPoint.y - state.origin.y,
3177
- z: hitPoint.z - state.origin.z
3178
- };
3179
- const toImpactLength = Math.hypot(toImpact.x, toImpact.y, toImpact.z);
3180
- if (state.type === "screen" && toImpactLength > 0) {
3181
- const dir = {
3182
- x: toImpact.x / toImpactLength,
3183
- y: toImpact.y / toImpactLength,
3184
- z: toImpact.z / toImpactLength
3185
- };
3186
- const dot = dir.x * forward.x + dir.y * forward.y + dir.z * forward.z;
3187
- if (dot <= 0.3) {
3188
- return { saved: 0, remainingCells: state.cellCount };
3189
- }
3190
- }
3191
- const ctfMode = options.ctfMode ?? false;
3192
- const damagePerCell = state.type === "screen" ? 1 : ctfMode ? 1 : 2;
3193
- let adjustedDamage = state.type === "screen" ? damage / 3 : 2 * damage / 3;
3194
- adjustedDamage = Math.max(1, adjustedDamage);
3195
- let saved = state.cellCount * damagePerCell;
3196
- if (hasAnyDamageFlag(flags, 4 /* ENERGY */)) {
3197
- saved = Math.max(1, Math.floor(saved / 2));
3198
- }
3199
- if (saved > adjustedDamage) {
3200
- saved = Math.floor(adjustedDamage);
3201
- }
3202
- let powerUsed = saved / damagePerCell;
3203
- if (hasAnyDamageFlag(flags, 4 /* ENERGY */)) {
3204
- powerUsed *= 2;
3205
- }
3206
- powerUsed = Math.max(1, Math.floor(powerUsed));
3207
- const cellsSpent = Math.max(damagePerCell, powerUsed);
3208
- const remainingCells = Math.max(0, state.cellCount - cellsSpent);
3209
- return { saved, remainingCells };
3210
- }
3211
-
3212
3441
  // src/combat/damage.ts
3213
3442
  var EntityDamageFlags = /* @__PURE__ */ ((EntityDamageFlags2) => {
3214
3443
  EntityDamageFlags2[EntityDamageFlags2["GODMODE"] = 1] = "GODMODE";
@@ -3611,175 +3840,6 @@ function killBox(teleporter, targets, options = {}) {
3611
3840
  return { events, cleared };
3612
3841
  }
3613
3842
 
3614
- // src/inventory/ammo.ts
3615
- var AmmoType = /* @__PURE__ */ ((AmmoType3) => {
3616
- AmmoType3[AmmoType3["Bullets"] = 0] = "Bullets";
3617
- AmmoType3[AmmoType3["Shells"] = 1] = "Shells";
3618
- AmmoType3[AmmoType3["Rockets"] = 2] = "Rockets";
3619
- AmmoType3[AmmoType3["Grenades"] = 3] = "Grenades";
3620
- AmmoType3[AmmoType3["Cells"] = 4] = "Cells";
3621
- AmmoType3[AmmoType3["Slugs"] = 5] = "Slugs";
3622
- return AmmoType3;
3623
- })(AmmoType || {});
3624
- var AMMO_TYPE_COUNT = Object.keys(AmmoType).length / 2;
3625
- var AmmoItemId = /* @__PURE__ */ ((AmmoItemId3) => {
3626
- AmmoItemId3["Shells"] = "ammo_shells";
3627
- AmmoItemId3["Bullets"] = "ammo_bullets";
3628
- AmmoItemId3["Rockets"] = "ammo_rockets";
3629
- AmmoItemId3["Grenades"] = "ammo_grenades";
3630
- AmmoItemId3["Cells"] = "ammo_cells";
3631
- AmmoItemId3["Slugs"] = "ammo_slugs";
3632
- return AmmoItemId3;
3633
- })(AmmoItemId || {});
3634
- var AMMO_ITEM_DEFINITIONS = {
3635
- ["ammo_shells" /* Shells */]: { id: "ammo_shells" /* Shells */, ammoType: 1 /* Shells */, quantity: 10, weaponAmmo: false },
3636
- ["ammo_bullets" /* Bullets */]: { id: "ammo_bullets" /* Bullets */, ammoType: 0 /* Bullets */, quantity: 50, weaponAmmo: false },
3637
- ["ammo_rockets" /* Rockets */]: { id: "ammo_rockets" /* Rockets */, ammoType: 2 /* Rockets */, quantity: 5, weaponAmmo: false },
3638
- ["ammo_grenades" /* Grenades */]: { id: "ammo_grenades" /* Grenades */, ammoType: 3 /* Grenades */, quantity: 5, weaponAmmo: true },
3639
- ["ammo_cells" /* Cells */]: { id: "ammo_cells" /* Cells */, ammoType: 4 /* Cells */, quantity: 50, weaponAmmo: false },
3640
- ["ammo_slugs" /* Slugs */]: { id: "ammo_slugs" /* Slugs */, ammoType: 5 /* Slugs */, quantity: 10, weaponAmmo: false }
3641
- };
3642
- function getAmmoItemDefinition(id) {
3643
- return AMMO_ITEM_DEFINITIONS[id];
3644
- }
3645
- function createAmmoInventory(caps = createBaseAmmoCaps()) {
3646
- return { caps: caps.slice(), counts: Array(AMMO_TYPE_COUNT).fill(0) };
3647
- }
3648
- function createBaseAmmoCaps() {
3649
- const caps = Array(AMMO_TYPE_COUNT).fill(50);
3650
- caps[0 /* Bullets */] = 200;
3651
- caps[1 /* Shells */] = 100;
3652
- caps[4 /* Cells */] = 200;
3653
- return caps;
3654
- }
3655
- function clampAmmoCounts(counts, caps) {
3656
- const limit = Math.min(counts.length, caps.length);
3657
- const clamped = counts.slice(0, limit);
3658
- for (let i = 0; i < limit; i++) {
3659
- const cap = caps[i];
3660
- if (cap !== void 0) {
3661
- clamped[i] = Math.min(counts[i], cap);
3662
- }
3663
- }
3664
- return clamped;
3665
- }
3666
- function addAmmo(inventory, ammoType, amount) {
3667
- const cap = inventory.caps[ammoType];
3668
- const current = inventory.counts[ammoType] ?? 0;
3669
- if (cap !== void 0 && current >= cap) {
3670
- return { ammoType, added: 0, newCount: current, capped: cap, pickedUp: false };
3671
- }
3672
- const uncapped = current + amount;
3673
- const newCount = cap === void 0 ? uncapped : Math.min(uncapped, cap);
3674
- const added = newCount - current;
3675
- inventory.counts[ammoType] = newCount;
3676
- return { ammoType, added, newCount, capped: cap ?? Number.POSITIVE_INFINITY, pickedUp: added > 0 };
3677
- }
3678
- function pickupAmmo(inventory, itemId, options = {}) {
3679
- const def = getAmmoItemDefinition(itemId);
3680
- const amount = options.countOverride ?? def.quantity;
3681
- return addAmmo(inventory, def.ammoType, amount);
3682
- }
3683
-
3684
- // src/inventory/playerInventory.ts
3685
- var WeaponId = /* @__PURE__ */ ((WeaponId2) => {
3686
- WeaponId2["Blaster"] = "blaster";
3687
- WeaponId2["Shotgun"] = "shotgun";
3688
- WeaponId2["SuperShotgun"] = "super_shotgun";
3689
- WeaponId2["Machinegun"] = "machinegun";
3690
- WeaponId2["Chaingun"] = "chaingun";
3691
- WeaponId2["GrenadeLauncher"] = "grenade_launcher";
3692
- WeaponId2["RocketLauncher"] = "rocket_launcher";
3693
- WeaponId2["HyperBlaster"] = "hyperblaster";
3694
- WeaponId2["Railgun"] = "railgun";
3695
- WeaponId2["BFG10K"] = "bfg10k";
3696
- return WeaponId2;
3697
- })(WeaponId || {});
3698
- var PowerupId = /* @__PURE__ */ ((PowerupId2) => {
3699
- PowerupId2["QuadDamage"] = "quad";
3700
- PowerupId2["Invulnerability"] = "invulnerability";
3701
- PowerupId2["EnviroSuit"] = "enviro_suit";
3702
- PowerupId2["Rebreather"] = "rebreather";
3703
- PowerupId2["Silencer"] = "silencer";
3704
- return PowerupId2;
3705
- })(PowerupId || {});
3706
- var KeyId = /* @__PURE__ */ ((KeyId2) => {
3707
- KeyId2["Blue"] = "blue";
3708
- KeyId2["Red"] = "red";
3709
- KeyId2["Green"] = "green";
3710
- KeyId2["Yellow"] = "yellow";
3711
- return KeyId2;
3712
- })(KeyId || {});
3713
- function createPlayerInventory(options = {}) {
3714
- const ammo = createAmmoInventory(options.ammoCaps);
3715
- const ownedWeapons = new Set(options.weapons ?? []);
3716
- const powerups = new Map(options.powerups ?? []);
3717
- const keys = new Set(options.keys ?? []);
3718
- return {
3719
- ammo,
3720
- ownedWeapons,
3721
- currentWeapon: options.currentWeapon,
3722
- armor: options.armor ?? null,
3723
- powerups,
3724
- keys
3725
- };
3726
- }
3727
- function giveAmmo(inventory, ammoType, amount) {
3728
- return addAmmo(inventory.ammo, ammoType, amount);
3729
- }
3730
- function giveAmmoItem(inventory, itemId, options) {
3731
- return pickupAmmo(inventory.ammo, itemId, options);
3732
- }
3733
- function giveWeapon(inventory, weapon, select = false) {
3734
- const hadWeapon = inventory.ownedWeapons.has(weapon);
3735
- inventory.ownedWeapons.add(weapon);
3736
- if (select || !inventory.currentWeapon) {
3737
- inventory.currentWeapon = weapon;
3738
- }
3739
- return !hadWeapon;
3740
- }
3741
- function hasWeapon(inventory, weapon) {
3742
- return inventory.ownedWeapons.has(weapon);
3743
- }
3744
- function selectWeapon(inventory, weapon) {
3745
- if (!inventory.ownedWeapons.has(weapon)) {
3746
- return false;
3747
- }
3748
- inventory.currentWeapon = weapon;
3749
- return true;
3750
- }
3751
- function equipArmor(inventory, armorType, amount) {
3752
- if (!armorType || amount <= 0) {
3753
- inventory.armor = null;
3754
- return null;
3755
- }
3756
- const info = ARMOR_INFO[armorType];
3757
- const armorCount = Math.min(amount, info.maxCount);
3758
- inventory.armor = { armorType, armorCount };
3759
- return inventory.armor;
3760
- }
3761
- function addPowerup(inventory, powerup, expiresAt) {
3762
- inventory.powerups.set(powerup, expiresAt);
3763
- }
3764
- function hasPowerup(inventory, powerup) {
3765
- return inventory.powerups.has(powerup);
3766
- }
3767
- function clearExpiredPowerups(inventory, nowMs) {
3768
- for (const [id, expiresAt] of inventory.powerups.entries()) {
3769
- if (expiresAt !== null && expiresAt <= nowMs) {
3770
- inventory.powerups.delete(id);
3771
- }
3772
- }
3773
- }
3774
- function addKey(inventory, key) {
3775
- const before = inventory.keys.size;
3776
- inventory.keys.add(key);
3777
- return inventory.keys.size > before;
3778
- }
3779
- function hasKey(inventory, key) {
3780
- return inventory.keys.has(key);
3781
- }
3782
-
3783
3843
  // src/index.ts
3784
3844
  var ZERO_VEC32 = { x: 0, y: 0, z: 0 };
3785
3845
  function createGame(engine, options) {
@@ -3907,11 +3967,13 @@ export {
3907
3967
  convertRereleaseSaveToGameSave,
3908
3968
  createAmmoInventory,
3909
3969
  createBaseAmmoCaps,
3970
+ createCallbackRegistry,
3910
3971
  createDefaultSpawnRegistry,
3911
3972
  createGame,
3912
3973
  createPlayerInventory,
3913
3974
  createSaveFile,
3914
3975
  damageModName,
3976
+ deserializePlayerInventory,
3915
3977
  equipArmor,
3916
3978
  facingIdeal,
3917
3979
  findTarget,
@@ -3934,8 +3996,10 @@ export {
3934
3996
  parseSaveFile,
3935
3997
  pickupAmmo,
3936
3998
  rangeTo,
3999
+ registerCallback,
3937
4000
  registerDefaultSpawns,
3938
4001
  selectWeapon,
4002
+ serializePlayerInventory,
3939
4003
  serializeRereleaseSave,
3940
4004
  setMovedir,
3941
4005
  spawnEntitiesFromText,