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
@@ -81,11 +81,13 @@ __export(index_exports, {
81
81
  convertRereleaseSaveToGameSave: () => convertRereleaseSaveToGameSave,
82
82
  createAmmoInventory: () => createAmmoInventory,
83
83
  createBaseAmmoCaps: () => createBaseAmmoCaps,
84
+ createCallbackRegistry: () => createCallbackRegistry,
84
85
  createDefaultSpawnRegistry: () => createDefaultSpawnRegistry,
85
86
  createGame: () => createGame,
86
87
  createPlayerInventory: () => createPlayerInventory,
87
88
  createSaveFile: () => createSaveFile,
88
89
  damageModName: () => damageModName,
90
+ deserializePlayerInventory: () => deserializePlayerInventory,
89
91
  equipArmor: () => equipArmor,
90
92
  facingIdeal: () => facingIdeal,
91
93
  findTarget: () => findTarget,
@@ -108,8 +110,10 @@ __export(index_exports, {
108
110
  parseSaveFile: () => parseSaveFile,
109
111
  pickupAmmo: () => pickupAmmo,
110
112
  rangeTo: () => rangeTo,
113
+ registerCallback: () => registerCallback,
111
114
  registerDefaultSpawns: () => registerDefaultSpawns,
112
115
  selectWeapon: () => selectWeapon,
116
+ serializePlayerInventory: () => serializePlayerInventory,
113
117
  serializeRereleaseSave: () => serializeRereleaseSave,
114
118
  setMovedir: () => setMovedir,
115
119
  spawnEntitiesFromText: () => spawnEntitiesFromText,
@@ -999,7 +1003,7 @@ function boundsIntersect(a, b) {
999
1003
  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);
1000
1004
  }
1001
1005
  var SERIALIZABLE_FIELDS = ENTITY_FIELD_METADATA.filter(
1002
- (field) => field.save
1006
+ (field) => field.save || field.type === "callback"
1003
1007
  );
1004
1008
  var DESCRIPTORS = new Map(SERIALIZABLE_FIELDS.map((descriptor) => [descriptor.name, descriptor]));
1005
1009
  function serializeVec3(vec) {
@@ -1030,12 +1034,18 @@ function deserializeInventory(value) {
1030
1034
  return parsed;
1031
1035
  }
1032
1036
  var EntitySystem = class {
1033
- constructor(maxEntities) {
1037
+ constructor(maxEntities, callbackRegistry) {
1034
1038
  this.targetNameIndex = /* @__PURE__ */ new Map();
1035
1039
  this.random = createRandomGenerator();
1036
1040
  this.currentTimeSeconds = 0;
1037
1041
  this.pool = new EntityPool(maxEntities);
1038
1042
  this.thinkScheduler = new ThinkScheduler();
1043
+ this.callbackToName = /* @__PURE__ */ new Map();
1044
+ if (callbackRegistry) {
1045
+ for (const [name, fn] of callbackRegistry.entries()) {
1046
+ this.callbackToName.set(fn, name);
1047
+ }
1048
+ }
1039
1049
  }
1040
1050
  get world() {
1041
1051
  return this.pool.world;
@@ -1160,6 +1170,9 @@ var EntitySystem = class {
1160
1170
  case "inventory":
1161
1171
  fields[descriptor.name] = serializeInventory(value);
1162
1172
  break;
1173
+ case "callback":
1174
+ fields[descriptor.name] = value ? this.callbackToName.get(value) ?? null : null;
1175
+ break;
1163
1176
  default:
1164
1177
  fields[descriptor.name] = value ?? null;
1165
1178
  break;
@@ -1177,7 +1190,7 @@ var EntitySystem = class {
1177
1190
  thinks: this.thinkScheduler.snapshot()
1178
1191
  };
1179
1192
  }
1180
- restore(snapshot) {
1193
+ restore(snapshot, callbackRegistry) {
1181
1194
  this.currentTimeSeconds = snapshot.timeSeconds;
1182
1195
  this.pool.restore(snapshot.pool);
1183
1196
  const indexToEntity = /* @__PURE__ */ new Map();
@@ -1212,6 +1225,14 @@ var EntitySystem = class {
1212
1225
  case "boolean":
1213
1226
  assignField(entity, name, Boolean(value));
1214
1227
  break;
1228
+ case "callback":
1229
+ if (value) {
1230
+ const callback = callbackRegistry?.get(value);
1231
+ if (callback) {
1232
+ assignField(entity, name, callback);
1233
+ }
1234
+ }
1235
+ break;
1215
1236
  default:
1216
1237
  assignField(entity, name, value);
1217
1238
  break;
@@ -2053,6 +2074,17 @@ function createDefaultSpawnRegistry() {
2053
2074
  return registry;
2054
2075
  }
2055
2076
 
2077
+ // src/entities/callbacks.ts
2078
+ function createCallbackRegistry() {
2079
+ return /* @__PURE__ */ new Map();
2080
+ }
2081
+ function registerCallback(registry, name, fn) {
2082
+ if (registry.has(name)) {
2083
+ return;
2084
+ }
2085
+ registry.set(name, fn);
2086
+ }
2087
+
2056
2088
  // src/loop.ts
2057
2089
  var orderedStageNames = [
2058
2090
  "prep",
@@ -2565,126 +2597,430 @@ function hashGameState(state) {
2565
2597
  return hash >>> 0;
2566
2598
  }
2567
2599
 
2568
- // src/save/save.ts
2569
- var SAVE_FORMAT_VERSION = 1;
2570
- var MIN_SUPPORTED_VERSION = 1;
2571
- function ensureObject(value, label) {
2572
- if (!value || typeof value !== "object" || Array.isArray(value)) {
2573
- throw new Error(`${label} must be an object`);
2574
- }
2575
- return value;
2600
+ // src/inventory/ammo.ts
2601
+ var AmmoType = /* @__PURE__ */ ((AmmoType3) => {
2602
+ AmmoType3[AmmoType3["Bullets"] = 0] = "Bullets";
2603
+ AmmoType3[AmmoType3["Shells"] = 1] = "Shells";
2604
+ AmmoType3[AmmoType3["Rockets"] = 2] = "Rockets";
2605
+ AmmoType3[AmmoType3["Grenades"] = 3] = "Grenades";
2606
+ AmmoType3[AmmoType3["Cells"] = 4] = "Cells";
2607
+ AmmoType3[AmmoType3["Slugs"] = 5] = "Slugs";
2608
+ return AmmoType3;
2609
+ })(AmmoType || {});
2610
+ var AMMO_TYPE_COUNT = Object.keys(AmmoType).length / 2;
2611
+ var AmmoItemId = /* @__PURE__ */ ((AmmoItemId3) => {
2612
+ AmmoItemId3["Shells"] = "ammo_shells";
2613
+ AmmoItemId3["Bullets"] = "ammo_bullets";
2614
+ AmmoItemId3["Rockets"] = "ammo_rockets";
2615
+ AmmoItemId3["Grenades"] = "ammo_grenades";
2616
+ AmmoItemId3["Cells"] = "ammo_cells";
2617
+ AmmoItemId3["Slugs"] = "ammo_slugs";
2618
+ return AmmoItemId3;
2619
+ })(AmmoItemId || {});
2620
+ var AMMO_ITEM_DEFINITIONS = {
2621
+ ["ammo_shells" /* Shells */]: { id: "ammo_shells" /* Shells */, ammoType: 1 /* Shells */, quantity: 10, weaponAmmo: false },
2622
+ ["ammo_bullets" /* Bullets */]: { id: "ammo_bullets" /* Bullets */, ammoType: 0 /* Bullets */, quantity: 50, weaponAmmo: false },
2623
+ ["ammo_rockets" /* Rockets */]: { id: "ammo_rockets" /* Rockets */, ammoType: 2 /* Rockets */, quantity: 5, weaponAmmo: false },
2624
+ ["ammo_grenades" /* Grenades */]: { id: "ammo_grenades" /* Grenades */, ammoType: 3 /* Grenades */, quantity: 5, weaponAmmo: true },
2625
+ ["ammo_cells" /* Cells */]: { id: "ammo_cells" /* Cells */, ammoType: 4 /* Cells */, quantity: 50, weaponAmmo: false },
2626
+ ["ammo_slugs" /* Slugs */]: { id: "ammo_slugs" /* Slugs */, ammoType: 5 /* Slugs */, quantity: 10, weaponAmmo: false }
2627
+ };
2628
+ function getAmmoItemDefinition(id) {
2629
+ return AMMO_ITEM_DEFINITIONS[id];
2576
2630
  }
2577
- function ensureNumber(value, label) {
2578
- if (typeof value !== "number" || !Number.isFinite(value)) {
2579
- throw new Error(`${label} must be a finite number`);
2580
- }
2581
- return value;
2631
+ function createAmmoInventory(caps = createBaseAmmoCaps()) {
2632
+ return { caps: caps.slice(), counts: Array(AMMO_TYPE_COUNT).fill(0) };
2582
2633
  }
2583
- function ensureNumberOrDefault(value, label, defaultValue) {
2584
- if (value === void 0) {
2585
- return defaultValue;
2634
+ function createBaseAmmoCaps() {
2635
+ const caps = Array(AMMO_TYPE_COUNT).fill(50);
2636
+ caps[0 /* Bullets */] = 200;
2637
+ caps[1 /* Shells */] = 100;
2638
+ caps[4 /* Cells */] = 200;
2639
+ return caps;
2640
+ }
2641
+ function clampAmmoCounts(counts, caps) {
2642
+ const limit = Math.min(counts.length, caps.length);
2643
+ const clamped = counts.slice(0, limit);
2644
+ for (let i = 0; i < limit; i++) {
2645
+ const cap = caps[i];
2646
+ if (cap !== void 0) {
2647
+ clamped[i] = Math.min(counts[i], cap);
2648
+ }
2586
2649
  }
2587
- return ensureNumber(value, label);
2650
+ return clamped;
2588
2651
  }
2589
- function ensureString(value, label) {
2590
- if (typeof value !== "string") {
2591
- throw new Error(`${label} must be a string`);
2652
+ function addAmmo(inventory, ammoType, amount) {
2653
+ const cap = inventory.caps[ammoType];
2654
+ const current = inventory.counts[ammoType] ?? 0;
2655
+ if (cap !== void 0 && current >= cap) {
2656
+ return { ammoType, added: 0, newCount: current, capped: cap, pickedUp: false };
2592
2657
  }
2593
- return value;
2658
+ const uncapped = current + amount;
2659
+ const newCount = cap === void 0 ? uncapped : Math.min(uncapped, cap);
2660
+ const added = newCount - current;
2661
+ inventory.counts[ammoType] = newCount;
2662
+ return { ammoType, added, newCount, capped: cap ?? Number.POSITIVE_INFINITY, pickedUp: added > 0 };
2594
2663
  }
2595
- function ensureNumberArray(value, label) {
2596
- if (!Array.isArray(value)) {
2597
- throw new Error(`${label} must be an array`);
2664
+ function pickupAmmo(inventory, itemId, options = {}) {
2665
+ const def = getAmmoItemDefinition(itemId);
2666
+ const amount = options.countOverride ?? def.quantity;
2667
+ return addAmmo(inventory, def.ammoType, amount);
2668
+ }
2669
+
2670
+ // src/combat/damageFlags.ts
2671
+ var DamageFlags = /* @__PURE__ */ ((DamageFlags2) => {
2672
+ DamageFlags2[DamageFlags2["NONE"] = 0] = "NONE";
2673
+ DamageFlags2[DamageFlags2["RADIUS"] = 1] = "RADIUS";
2674
+ DamageFlags2[DamageFlags2["NO_ARMOR"] = 2] = "NO_ARMOR";
2675
+ DamageFlags2[DamageFlags2["ENERGY"] = 4] = "ENERGY";
2676
+ DamageFlags2[DamageFlags2["NO_KNOCKBACK"] = 8] = "NO_KNOCKBACK";
2677
+ DamageFlags2[DamageFlags2["BULLET"] = 16] = "BULLET";
2678
+ DamageFlags2[DamageFlags2["NO_PROTECTION"] = 32] = "NO_PROTECTION";
2679
+ DamageFlags2[DamageFlags2["DESTROY_ARMOR"] = 64] = "DESTROY_ARMOR";
2680
+ DamageFlags2[DamageFlags2["NO_REG_ARMOR"] = 128] = "NO_REG_ARMOR";
2681
+ DamageFlags2[DamageFlags2["NO_POWER_ARMOR"] = 256] = "NO_POWER_ARMOR";
2682
+ DamageFlags2[DamageFlags2["NO_INDICATOR"] = 512] = "NO_INDICATOR";
2683
+ return DamageFlags2;
2684
+ })(DamageFlags || {});
2685
+ function hasAnyDamageFlag(flags, mask) {
2686
+ return (flags & mask) !== 0;
2687
+ }
2688
+
2689
+ // src/combat/armor.ts
2690
+ var ArmorType = /* @__PURE__ */ ((ArmorType3) => {
2691
+ ArmorType3["BODY"] = "body";
2692
+ ArmorType3["COMBAT"] = "combat";
2693
+ ArmorType3["JACKET"] = "jacket";
2694
+ return ArmorType3;
2695
+ })(ArmorType || {});
2696
+ var ARMOR_INFO = {
2697
+ ["jacket" /* JACKET */]: {
2698
+ baseCount: 25,
2699
+ maxCount: 50,
2700
+ normalProtection: 0.3,
2701
+ energyProtection: 0
2702
+ },
2703
+ ["combat" /* COMBAT */]: {
2704
+ baseCount: 50,
2705
+ maxCount: 100,
2706
+ normalProtection: 0.6,
2707
+ energyProtection: 0.3
2708
+ },
2709
+ ["body" /* BODY */]: {
2710
+ baseCount: 100,
2711
+ maxCount: 200,
2712
+ normalProtection: 0.8,
2713
+ energyProtection: 0.6
2598
2714
  }
2599
- for (const element of value) {
2600
- ensureNumber(element, `${label} element`);
2715
+ };
2716
+ function applyRegularArmor(damage, flags, state) {
2717
+ if (damage <= 0 || hasAnyDamageFlag(flags, 2 /* NO_ARMOR */ | 128 /* NO_REG_ARMOR */) || !state.armorType || state.armorCount <= 0) {
2718
+ return { saved: 0, remainingArmor: state.armorCount };
2601
2719
  }
2602
- return value;
2603
- }
2604
- function parseLevelState(raw) {
2605
- if (raw === void 0) {
2606
- return { frameNumber: 0, timeSeconds: 0, previousTimeSeconds: 0, deltaSeconds: 0 };
2720
+ const info = ARMOR_INFO[state.armorType];
2721
+ const protection = hasAnyDamageFlag(flags, 4 /* ENERGY */) ? info.energyProtection : info.normalProtection;
2722
+ let saved = Math.ceil(protection * damage);
2723
+ if (saved >= state.armorCount) {
2724
+ saved = state.armorCount;
2607
2725
  }
2608
- const level = ensureObject(raw, "level");
2609
- return {
2610
- frameNumber: ensureNumberOrDefault(level.frameNumber, "level.frameNumber", 0),
2611
- timeSeconds: ensureNumberOrDefault(level.timeSeconds, "level.timeSeconds", 0),
2612
- previousTimeSeconds: ensureNumberOrDefault(level.previousTimeSeconds, "level.previousTimeSeconds", 0),
2613
- deltaSeconds: ensureNumberOrDefault(level.deltaSeconds, "level.deltaSeconds", 0)
2614
- };
2615
- }
2616
- function parseRngState(raw) {
2617
- if (raw === void 0) {
2618
- return new RandomGenerator().getState();
2726
+ if (saved <= 0) {
2727
+ return { saved: 0, remainingArmor: state.armorCount };
2619
2728
  }
2620
- const rng = ensureObject(raw, "rng");
2621
- const mt = ensureObject(rng.mt, "rng.mt");
2622
- const state = ensureNumberArray(mt.state, "rng.mt.state");
2623
- return {
2624
- mt: {
2625
- index: ensureNumber(mt.index, "rng.mt.index"),
2626
- state
2627
- }
2628
- };
2729
+ return { saved, remainingArmor: state.armorCount - saved };
2629
2730
  }
2630
- function parseThinkEntries(raw) {
2631
- if (raw === void 0) {
2632
- return [];
2731
+ function applyPowerArmor(damage, flags, hitPoint, _hitNormal, state, options = {}) {
2732
+ if (state.health <= 0 || damage <= 0) {
2733
+ return { saved: 0, remainingCells: state.cellCount };
2633
2734
  }
2634
- if (!Array.isArray(raw)) {
2635
- throw new Error("thinks must be an array");
2735
+ if (hasAnyDamageFlag(flags, 2 /* NO_ARMOR */ | 256 /* NO_POWER_ARMOR */)) {
2736
+ return { saved: 0, remainingCells: state.cellCount };
2636
2737
  }
2637
- return raw.map((entry, idx) => {
2638
- const think = ensureObject(entry, `thinks[${idx}]`);
2639
- return {
2640
- time: ensureNumber(think.time, `thinks[${idx}].time`),
2641
- entityIndex: ensureNumber(think.entityIndex, `thinks[${idx}].entityIndex`)
2642
- };
2643
- });
2644
- }
2645
- function parseEntityFields(raw) {
2646
- if (raw === void 0) {
2647
- return {};
2738
+ if (!state.type || state.cellCount <= 0) {
2739
+ return { saved: 0, remainingCells: state.cellCount };
2648
2740
  }
2649
- const fields = ensureObject(raw, "entity.fields");
2650
- const parsed = {};
2651
- for (const [name, value] of Object.entries(fields)) {
2652
- if (value === null) {
2653
- parsed[name] = null;
2654
- continue;
2655
- }
2656
- switch (typeof value) {
2657
- case "number":
2658
- case "string":
2659
- case "boolean":
2660
- parsed[name] = value;
2661
- break;
2662
- default: {
2663
- if (!Array.isArray(value)) {
2664
- const object = ensureObject(value, name);
2665
- const inventory = {};
2666
- for (const [entryName, entryValue] of Object.entries(object)) {
2667
- inventory[entryName] = ensureNumber(entryValue, `${name}.${entryName}`);
2668
- }
2669
- parsed[name] = inventory;
2670
- break;
2671
- }
2672
- if (Array.isArray(value) && value.length === 3) {
2673
- const [x, y, z] = value;
2674
- parsed[name] = [
2675
- ensureNumber(x, `${name}[0]`),
2676
- ensureNumber(y, `${name}[1]`),
2677
- ensureNumber(z, `${name}[2]`)
2678
- ];
2679
- break;
2680
- }
2681
- throw new Error(`Unsupported entity field value for ${name}`);
2682
- }
2741
+ const { forward } = angleVectors(state.angles);
2742
+ const toImpact = {
2743
+ x: hitPoint.x - state.origin.x,
2744
+ y: hitPoint.y - state.origin.y,
2745
+ z: hitPoint.z - state.origin.z
2746
+ };
2747
+ const toImpactLength = Math.hypot(toImpact.x, toImpact.y, toImpact.z);
2748
+ if (state.type === "screen" && toImpactLength > 0) {
2749
+ const dir = {
2750
+ x: toImpact.x / toImpactLength,
2751
+ y: toImpact.y / toImpactLength,
2752
+ z: toImpact.z / toImpactLength
2753
+ };
2754
+ const dot = dir.x * forward.x + dir.y * forward.y + dir.z * forward.z;
2755
+ if (dot <= 0.3) {
2756
+ return { saved: 0, remainingCells: state.cellCount };
2683
2757
  }
2684
2758
  }
2685
- return parsed;
2686
- }
2687
- function parseEntities(raw) {
2759
+ const ctfMode = options.ctfMode ?? false;
2760
+ const damagePerCell = state.type === "screen" ? 1 : ctfMode ? 1 : 2;
2761
+ let adjustedDamage = state.type === "screen" ? damage / 3 : 2 * damage / 3;
2762
+ adjustedDamage = Math.max(1, adjustedDamage);
2763
+ let saved = state.cellCount * damagePerCell;
2764
+ if (hasAnyDamageFlag(flags, 4 /* ENERGY */)) {
2765
+ saved = Math.max(1, Math.floor(saved / 2));
2766
+ }
2767
+ if (saved > adjustedDamage) {
2768
+ saved = Math.floor(adjustedDamage);
2769
+ }
2770
+ let powerUsed = saved / damagePerCell;
2771
+ if (hasAnyDamageFlag(flags, 4 /* ENERGY */)) {
2772
+ powerUsed *= 2;
2773
+ }
2774
+ powerUsed = Math.max(1, Math.floor(powerUsed));
2775
+ const cellsSpent = Math.max(damagePerCell, powerUsed);
2776
+ const remainingCells = Math.max(0, state.cellCount - cellsSpent);
2777
+ return { saved, remainingCells };
2778
+ }
2779
+
2780
+ // src/inventory/playerInventory.ts
2781
+ var WeaponId = /* @__PURE__ */ ((WeaponId2) => {
2782
+ WeaponId2["Blaster"] = "blaster";
2783
+ WeaponId2["Shotgun"] = "shotgun";
2784
+ WeaponId2["SuperShotgun"] = "super_shotgun";
2785
+ WeaponId2["Machinegun"] = "machinegun";
2786
+ WeaponId2["Chaingun"] = "chaingun";
2787
+ WeaponId2["GrenadeLauncher"] = "grenade_launcher";
2788
+ WeaponId2["RocketLauncher"] = "rocket_launcher";
2789
+ WeaponId2["HyperBlaster"] = "hyperblaster";
2790
+ WeaponId2["Railgun"] = "railgun";
2791
+ WeaponId2["BFG10K"] = "bfg10k";
2792
+ return WeaponId2;
2793
+ })(WeaponId || {});
2794
+ var PowerupId = /* @__PURE__ */ ((PowerupId2) => {
2795
+ PowerupId2["QuadDamage"] = "quad";
2796
+ PowerupId2["Invulnerability"] = "invulnerability";
2797
+ PowerupId2["EnviroSuit"] = "enviro_suit";
2798
+ PowerupId2["Rebreather"] = "rebreather";
2799
+ PowerupId2["Silencer"] = "silencer";
2800
+ return PowerupId2;
2801
+ })(PowerupId || {});
2802
+ var KeyId = /* @__PURE__ */ ((KeyId2) => {
2803
+ KeyId2["Blue"] = "blue";
2804
+ KeyId2["Red"] = "red";
2805
+ KeyId2["Green"] = "green";
2806
+ KeyId2["Yellow"] = "yellow";
2807
+ return KeyId2;
2808
+ })(KeyId || {});
2809
+ function createPlayerInventory(options = {}) {
2810
+ const ammo = createAmmoInventory(options.ammoCaps);
2811
+ const ownedWeapons = new Set(options.weapons ?? []);
2812
+ const powerups = new Map(options.powerups ?? []);
2813
+ const keys = new Set(options.keys ?? []);
2814
+ return {
2815
+ ammo,
2816
+ ownedWeapons,
2817
+ currentWeapon: options.currentWeapon,
2818
+ armor: options.armor ?? null,
2819
+ powerups,
2820
+ keys
2821
+ };
2822
+ }
2823
+ function giveAmmo(inventory, ammoType, amount) {
2824
+ return addAmmo(inventory.ammo, ammoType, amount);
2825
+ }
2826
+ function giveAmmoItem(inventory, itemId, options) {
2827
+ return pickupAmmo(inventory.ammo, itemId, options);
2828
+ }
2829
+ function giveWeapon(inventory, weapon, select = false) {
2830
+ const hadWeapon = inventory.ownedWeapons.has(weapon);
2831
+ inventory.ownedWeapons.add(weapon);
2832
+ if (select || !inventory.currentWeapon) {
2833
+ inventory.currentWeapon = weapon;
2834
+ }
2835
+ return !hadWeapon;
2836
+ }
2837
+ function hasWeapon(inventory, weapon) {
2838
+ return inventory.ownedWeapons.has(weapon);
2839
+ }
2840
+ function selectWeapon(inventory, weapon) {
2841
+ if (!inventory.ownedWeapons.has(weapon)) {
2842
+ return false;
2843
+ }
2844
+ inventory.currentWeapon = weapon;
2845
+ return true;
2846
+ }
2847
+ function equipArmor(inventory, armorType, amount) {
2848
+ if (!armorType || amount <= 0) {
2849
+ inventory.armor = null;
2850
+ return null;
2851
+ }
2852
+ const info = ARMOR_INFO[armorType];
2853
+ const armorCount = Math.min(amount, info.maxCount);
2854
+ inventory.armor = { armorType, armorCount };
2855
+ return inventory.armor;
2856
+ }
2857
+ function addPowerup(inventory, powerup, expiresAt) {
2858
+ inventory.powerups.set(powerup, expiresAt);
2859
+ }
2860
+ function hasPowerup(inventory, powerup) {
2861
+ return inventory.powerups.has(powerup);
2862
+ }
2863
+ function clearExpiredPowerups(inventory, nowMs) {
2864
+ for (const [id, expiresAt] of inventory.powerups.entries()) {
2865
+ if (expiresAt !== null && expiresAt <= nowMs) {
2866
+ inventory.powerups.delete(id);
2867
+ }
2868
+ }
2869
+ }
2870
+ function addKey(inventory, key) {
2871
+ const before = inventory.keys.size;
2872
+ inventory.keys.add(key);
2873
+ return inventory.keys.size > before;
2874
+ }
2875
+ function hasKey(inventory, key) {
2876
+ return inventory.keys.has(key);
2877
+ }
2878
+ function serializePlayerInventory(inventory) {
2879
+ return {
2880
+ ammo: inventory.ammo.counts,
2881
+ ownedWeapons: [...inventory.ownedWeapons],
2882
+ currentWeapon: inventory.currentWeapon,
2883
+ armor: inventory.armor ? { ...inventory.armor } : null,
2884
+ powerups: [...inventory.powerups.entries()],
2885
+ keys: [...inventory.keys]
2886
+ };
2887
+ }
2888
+ function deserializePlayerInventory(serialized, options = {}) {
2889
+ const ammo = createAmmoInventory(options.ammoCaps);
2890
+ const limit = Math.min(ammo.counts.length, serialized.ammo.length);
2891
+ for (let i = 0; i < limit; i++) {
2892
+ ammo.counts[i] = serialized.ammo[i];
2893
+ }
2894
+ return {
2895
+ ammo,
2896
+ ownedWeapons: new Set(serialized.ownedWeapons),
2897
+ currentWeapon: serialized.currentWeapon,
2898
+ armor: serialized.armor ? { ...serialized.armor } : null,
2899
+ powerups: new Map(serialized.powerups),
2900
+ keys: new Set(serialized.keys)
2901
+ };
2902
+ }
2903
+
2904
+ // src/save/save.ts
2905
+ var SAVE_FORMAT_VERSION = 2;
2906
+ var MIN_SUPPORTED_VERSION = 1;
2907
+ function ensureObject(value, label) {
2908
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2909
+ throw new Error(`${label} must be an object`);
2910
+ }
2911
+ return value;
2912
+ }
2913
+ function ensureNumber(value, label) {
2914
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2915
+ throw new Error(`${label} must be a finite number`);
2916
+ }
2917
+ return value;
2918
+ }
2919
+ function ensureNumberOrDefault(value, label, defaultValue) {
2920
+ if (value === void 0) {
2921
+ return defaultValue;
2922
+ }
2923
+ return ensureNumber(value, label);
2924
+ }
2925
+ function ensureString(value, label) {
2926
+ if (typeof value !== "string") {
2927
+ throw new Error(`${label} must be a string`);
2928
+ }
2929
+ return value;
2930
+ }
2931
+ function ensureNumberArray(value, label) {
2932
+ if (!Array.isArray(value)) {
2933
+ throw new Error(`${label} must be an array`);
2934
+ }
2935
+ for (const element of value) {
2936
+ ensureNumber(element, `${label} element`);
2937
+ }
2938
+ return value;
2939
+ }
2940
+ function parseLevelState(raw) {
2941
+ if (raw === void 0) {
2942
+ return { frameNumber: 0, timeSeconds: 0, previousTimeSeconds: 0, deltaSeconds: 0 };
2943
+ }
2944
+ const level = ensureObject(raw, "level");
2945
+ return {
2946
+ frameNumber: ensureNumberOrDefault(level.frameNumber, "level.frameNumber", 0),
2947
+ timeSeconds: ensureNumberOrDefault(level.timeSeconds, "level.timeSeconds", 0),
2948
+ previousTimeSeconds: ensureNumberOrDefault(level.previousTimeSeconds, "level.previousTimeSeconds", 0),
2949
+ deltaSeconds: ensureNumberOrDefault(level.deltaSeconds, "level.deltaSeconds", 0)
2950
+ };
2951
+ }
2952
+ function parseRngState(raw) {
2953
+ if (raw === void 0) {
2954
+ return new RandomGenerator().getState();
2955
+ }
2956
+ const rng = ensureObject(raw, "rng");
2957
+ const mt = ensureObject(rng.mt, "rng.mt");
2958
+ const state = ensureNumberArray(mt.state, "rng.mt.state");
2959
+ return {
2960
+ mt: {
2961
+ index: ensureNumber(mt.index, "rng.mt.index"),
2962
+ state
2963
+ }
2964
+ };
2965
+ }
2966
+ function parseThinkEntries(raw) {
2967
+ if (raw === void 0) {
2968
+ return [];
2969
+ }
2970
+ if (!Array.isArray(raw)) {
2971
+ throw new Error("thinks must be an array");
2972
+ }
2973
+ return raw.map((entry, idx) => {
2974
+ const think = ensureObject(entry, `thinks[${idx}]`);
2975
+ return {
2976
+ time: ensureNumber(think.time, `thinks[${idx}].time`),
2977
+ entityIndex: ensureNumber(think.entityIndex, `thinks[${idx}].entityIndex`)
2978
+ };
2979
+ });
2980
+ }
2981
+ function parseEntityFields(raw) {
2982
+ if (raw === void 0) {
2983
+ return {};
2984
+ }
2985
+ const fields = ensureObject(raw, "entity.fields");
2986
+ const parsed = {};
2987
+ for (const [name, value] of Object.entries(fields)) {
2988
+ if (value === null) {
2989
+ parsed[name] = null;
2990
+ continue;
2991
+ }
2992
+ switch (typeof value) {
2993
+ case "number":
2994
+ case "string":
2995
+ case "boolean":
2996
+ parsed[name] = value;
2997
+ break;
2998
+ default: {
2999
+ if (!Array.isArray(value)) {
3000
+ const object = ensureObject(value, name);
3001
+ const inventory = {};
3002
+ for (const [entryName, entryValue] of Object.entries(object)) {
3003
+ inventory[entryName] = ensureNumber(entryValue, `${name}.${entryName}`);
3004
+ }
3005
+ parsed[name] = inventory;
3006
+ break;
3007
+ }
3008
+ if (Array.isArray(value) && value.length === 3) {
3009
+ const [x, y, z] = value;
3010
+ parsed[name] = [
3011
+ ensureNumber(x, `${name}[0]`),
3012
+ ensureNumber(y, `${name}[1]`),
3013
+ ensureNumber(z, `${name}[2]`)
3014
+ ];
3015
+ break;
3016
+ }
3017
+ throw new Error(`Unsupported entity field value for ${name}`);
3018
+ }
3019
+ }
3020
+ }
3021
+ return parsed;
3022
+ }
3023
+ function parseEntities(raw) {
2688
3024
  if (!Array.isArray(raw)) {
2689
3025
  throw new Error("entities must be an array");
2690
3026
  }
@@ -2783,7 +3119,8 @@ function createSaveFile(options) {
2783
3119
  configstrings = [],
2784
3120
  cvars,
2785
3121
  gameState = {},
2786
- timestamp = Date.now()
3122
+ timestamp = Date.now(),
3123
+ player
2787
3124
  } = options;
2788
3125
  return {
2789
3126
  version: SAVE_FORMAT_VERSION,
@@ -2796,7 +3133,8 @@ function createSaveFile(options) {
2796
3133
  rng: cloneRngState(rngState),
2797
3134
  entities: entitySystem.createSnapshot(),
2798
3135
  cvars: serializeCvars(cvars),
2799
- configstrings: [...configstrings]
3136
+ configstrings: [...configstrings],
3137
+ player: player ? serializePlayerInventory(player) : void 0
2800
3138
  };
2801
3139
  }
2802
3140
  function parseSaveFile(serialized, options = {}) {
@@ -2822,14 +3160,19 @@ function parseSaveFile(serialized, options = {}) {
2822
3160
  rng: parseRngState(save.rng),
2823
3161
  entities: parseEntitySnapshot(save.entities),
2824
3162
  cvars: parseCvars(save.cvars),
2825
- configstrings: parseConfigstrings(save.configstrings)
3163
+ configstrings: parseConfigstrings(save.configstrings),
3164
+ player: save.player ? save.player : void 0
2826
3165
  };
2827
3166
  }
2828
3167
  function applySaveFile(save, targets) {
2829
3168
  targets.levelClock.restore(save.level);
2830
- targets.entitySystem.restore(save.entities);
3169
+ targets.entitySystem.restore(save.entities, targets.callbackRegistry);
2831
3170
  targets.rng.setState(save.rng);
2832
3171
  applyCvars(save.cvars, targets.cvars);
3172
+ if (save.player && targets.player) {
3173
+ const deserialized = deserializePlayerInventory(save.player);
3174
+ Object.assign(targets.player, deserialized);
3175
+ }
2833
3176
  }
2834
3177
 
2835
3178
  // src/save/rerelease.ts
@@ -3221,116 +3564,6 @@ _SaveStorage.DEFAULT_STORE = "saves";
3221
3564
  _SaveStorage.QUICK_SLOT = "quicksave";
3222
3565
  var SaveStorage = _SaveStorage;
3223
3566
 
3224
- // src/combat/damageFlags.ts
3225
- var DamageFlags = /* @__PURE__ */ ((DamageFlags2) => {
3226
- DamageFlags2[DamageFlags2["NONE"] = 0] = "NONE";
3227
- DamageFlags2[DamageFlags2["RADIUS"] = 1] = "RADIUS";
3228
- DamageFlags2[DamageFlags2["NO_ARMOR"] = 2] = "NO_ARMOR";
3229
- DamageFlags2[DamageFlags2["ENERGY"] = 4] = "ENERGY";
3230
- DamageFlags2[DamageFlags2["NO_KNOCKBACK"] = 8] = "NO_KNOCKBACK";
3231
- DamageFlags2[DamageFlags2["BULLET"] = 16] = "BULLET";
3232
- DamageFlags2[DamageFlags2["NO_PROTECTION"] = 32] = "NO_PROTECTION";
3233
- DamageFlags2[DamageFlags2["DESTROY_ARMOR"] = 64] = "DESTROY_ARMOR";
3234
- DamageFlags2[DamageFlags2["NO_REG_ARMOR"] = 128] = "NO_REG_ARMOR";
3235
- DamageFlags2[DamageFlags2["NO_POWER_ARMOR"] = 256] = "NO_POWER_ARMOR";
3236
- DamageFlags2[DamageFlags2["NO_INDICATOR"] = 512] = "NO_INDICATOR";
3237
- return DamageFlags2;
3238
- })(DamageFlags || {});
3239
- function hasAnyDamageFlag(flags, mask) {
3240
- return (flags & mask) !== 0;
3241
- }
3242
-
3243
- // src/combat/armor.ts
3244
- var ArmorType = /* @__PURE__ */ ((ArmorType3) => {
3245
- ArmorType3["BODY"] = "body";
3246
- ArmorType3["COMBAT"] = "combat";
3247
- ArmorType3["JACKET"] = "jacket";
3248
- return ArmorType3;
3249
- })(ArmorType || {});
3250
- var ARMOR_INFO = {
3251
- ["jacket" /* JACKET */]: {
3252
- baseCount: 25,
3253
- maxCount: 50,
3254
- normalProtection: 0.3,
3255
- energyProtection: 0
3256
- },
3257
- ["combat" /* COMBAT */]: {
3258
- baseCount: 50,
3259
- maxCount: 100,
3260
- normalProtection: 0.6,
3261
- energyProtection: 0.3
3262
- },
3263
- ["body" /* BODY */]: {
3264
- baseCount: 100,
3265
- maxCount: 200,
3266
- normalProtection: 0.8,
3267
- energyProtection: 0.6
3268
- }
3269
- };
3270
- function applyRegularArmor(damage, flags, state) {
3271
- if (damage <= 0 || hasAnyDamageFlag(flags, 2 /* NO_ARMOR */ | 128 /* NO_REG_ARMOR */) || !state.armorType || state.armorCount <= 0) {
3272
- return { saved: 0, remainingArmor: state.armorCount };
3273
- }
3274
- const info = ARMOR_INFO[state.armorType];
3275
- const protection = hasAnyDamageFlag(flags, 4 /* ENERGY */) ? info.energyProtection : info.normalProtection;
3276
- let saved = Math.ceil(protection * damage);
3277
- if (saved >= state.armorCount) {
3278
- saved = state.armorCount;
3279
- }
3280
- if (saved <= 0) {
3281
- return { saved: 0, remainingArmor: state.armorCount };
3282
- }
3283
- return { saved, remainingArmor: state.armorCount - saved };
3284
- }
3285
- function applyPowerArmor(damage, flags, hitPoint, _hitNormal, state, options = {}) {
3286
- if (state.health <= 0 || damage <= 0) {
3287
- return { saved: 0, remainingCells: state.cellCount };
3288
- }
3289
- if (hasAnyDamageFlag(flags, 2 /* NO_ARMOR */ | 256 /* NO_POWER_ARMOR */)) {
3290
- return { saved: 0, remainingCells: state.cellCount };
3291
- }
3292
- if (!state.type || state.cellCount <= 0) {
3293
- return { saved: 0, remainingCells: state.cellCount };
3294
- }
3295
- const { forward } = angleVectors(state.angles);
3296
- const toImpact = {
3297
- x: hitPoint.x - state.origin.x,
3298
- y: hitPoint.y - state.origin.y,
3299
- z: hitPoint.z - state.origin.z
3300
- };
3301
- const toImpactLength = Math.hypot(toImpact.x, toImpact.y, toImpact.z);
3302
- if (state.type === "screen" && toImpactLength > 0) {
3303
- const dir = {
3304
- x: toImpact.x / toImpactLength,
3305
- y: toImpact.y / toImpactLength,
3306
- z: toImpact.z / toImpactLength
3307
- };
3308
- const dot = dir.x * forward.x + dir.y * forward.y + dir.z * forward.z;
3309
- if (dot <= 0.3) {
3310
- return { saved: 0, remainingCells: state.cellCount };
3311
- }
3312
- }
3313
- const ctfMode = options.ctfMode ?? false;
3314
- const damagePerCell = state.type === "screen" ? 1 : ctfMode ? 1 : 2;
3315
- let adjustedDamage = state.type === "screen" ? damage / 3 : 2 * damage / 3;
3316
- adjustedDamage = Math.max(1, adjustedDamage);
3317
- let saved = state.cellCount * damagePerCell;
3318
- if (hasAnyDamageFlag(flags, 4 /* ENERGY */)) {
3319
- saved = Math.max(1, Math.floor(saved / 2));
3320
- }
3321
- if (saved > adjustedDamage) {
3322
- saved = Math.floor(adjustedDamage);
3323
- }
3324
- let powerUsed = saved / damagePerCell;
3325
- if (hasAnyDamageFlag(flags, 4 /* ENERGY */)) {
3326
- powerUsed *= 2;
3327
- }
3328
- powerUsed = Math.max(1, Math.floor(powerUsed));
3329
- const cellsSpent = Math.max(damagePerCell, powerUsed);
3330
- const remainingCells = Math.max(0, state.cellCount - cellsSpent);
3331
- return { saved, remainingCells };
3332
- }
3333
-
3334
3567
  // src/combat/damage.ts
3335
3568
  var EntityDamageFlags = /* @__PURE__ */ ((EntityDamageFlags2) => {
3336
3569
  EntityDamageFlags2[EntityDamageFlags2["GODMODE"] = 1] = "GODMODE";
@@ -3733,175 +3966,6 @@ function killBox(teleporter, targets, options = {}) {
3733
3966
  return { events, cleared };
3734
3967
  }
3735
3968
 
3736
- // src/inventory/ammo.ts
3737
- var AmmoType = /* @__PURE__ */ ((AmmoType3) => {
3738
- AmmoType3[AmmoType3["Bullets"] = 0] = "Bullets";
3739
- AmmoType3[AmmoType3["Shells"] = 1] = "Shells";
3740
- AmmoType3[AmmoType3["Rockets"] = 2] = "Rockets";
3741
- AmmoType3[AmmoType3["Grenades"] = 3] = "Grenades";
3742
- AmmoType3[AmmoType3["Cells"] = 4] = "Cells";
3743
- AmmoType3[AmmoType3["Slugs"] = 5] = "Slugs";
3744
- return AmmoType3;
3745
- })(AmmoType || {});
3746
- var AMMO_TYPE_COUNT = Object.keys(AmmoType).length / 2;
3747
- var AmmoItemId = /* @__PURE__ */ ((AmmoItemId3) => {
3748
- AmmoItemId3["Shells"] = "ammo_shells";
3749
- AmmoItemId3["Bullets"] = "ammo_bullets";
3750
- AmmoItemId3["Rockets"] = "ammo_rockets";
3751
- AmmoItemId3["Grenades"] = "ammo_grenades";
3752
- AmmoItemId3["Cells"] = "ammo_cells";
3753
- AmmoItemId3["Slugs"] = "ammo_slugs";
3754
- return AmmoItemId3;
3755
- })(AmmoItemId || {});
3756
- var AMMO_ITEM_DEFINITIONS = {
3757
- ["ammo_shells" /* Shells */]: { id: "ammo_shells" /* Shells */, ammoType: 1 /* Shells */, quantity: 10, weaponAmmo: false },
3758
- ["ammo_bullets" /* Bullets */]: { id: "ammo_bullets" /* Bullets */, ammoType: 0 /* Bullets */, quantity: 50, weaponAmmo: false },
3759
- ["ammo_rockets" /* Rockets */]: { id: "ammo_rockets" /* Rockets */, ammoType: 2 /* Rockets */, quantity: 5, weaponAmmo: false },
3760
- ["ammo_grenades" /* Grenades */]: { id: "ammo_grenades" /* Grenades */, ammoType: 3 /* Grenades */, quantity: 5, weaponAmmo: true },
3761
- ["ammo_cells" /* Cells */]: { id: "ammo_cells" /* Cells */, ammoType: 4 /* Cells */, quantity: 50, weaponAmmo: false },
3762
- ["ammo_slugs" /* Slugs */]: { id: "ammo_slugs" /* Slugs */, ammoType: 5 /* Slugs */, quantity: 10, weaponAmmo: false }
3763
- };
3764
- function getAmmoItemDefinition(id) {
3765
- return AMMO_ITEM_DEFINITIONS[id];
3766
- }
3767
- function createAmmoInventory(caps = createBaseAmmoCaps()) {
3768
- return { caps: caps.slice(), counts: Array(AMMO_TYPE_COUNT).fill(0) };
3769
- }
3770
- function createBaseAmmoCaps() {
3771
- const caps = Array(AMMO_TYPE_COUNT).fill(50);
3772
- caps[0 /* Bullets */] = 200;
3773
- caps[1 /* Shells */] = 100;
3774
- caps[4 /* Cells */] = 200;
3775
- return caps;
3776
- }
3777
- function clampAmmoCounts(counts, caps) {
3778
- const limit = Math.min(counts.length, caps.length);
3779
- const clamped = counts.slice(0, limit);
3780
- for (let i = 0; i < limit; i++) {
3781
- const cap = caps[i];
3782
- if (cap !== void 0) {
3783
- clamped[i] = Math.min(counts[i], cap);
3784
- }
3785
- }
3786
- return clamped;
3787
- }
3788
- function addAmmo(inventory, ammoType, amount) {
3789
- const cap = inventory.caps[ammoType];
3790
- const current = inventory.counts[ammoType] ?? 0;
3791
- if (cap !== void 0 && current >= cap) {
3792
- return { ammoType, added: 0, newCount: current, capped: cap, pickedUp: false };
3793
- }
3794
- const uncapped = current + amount;
3795
- const newCount = cap === void 0 ? uncapped : Math.min(uncapped, cap);
3796
- const added = newCount - current;
3797
- inventory.counts[ammoType] = newCount;
3798
- return { ammoType, added, newCount, capped: cap ?? Number.POSITIVE_INFINITY, pickedUp: added > 0 };
3799
- }
3800
- function pickupAmmo(inventory, itemId, options = {}) {
3801
- const def = getAmmoItemDefinition(itemId);
3802
- const amount = options.countOverride ?? def.quantity;
3803
- return addAmmo(inventory, def.ammoType, amount);
3804
- }
3805
-
3806
- // src/inventory/playerInventory.ts
3807
- var WeaponId = /* @__PURE__ */ ((WeaponId2) => {
3808
- WeaponId2["Blaster"] = "blaster";
3809
- WeaponId2["Shotgun"] = "shotgun";
3810
- WeaponId2["SuperShotgun"] = "super_shotgun";
3811
- WeaponId2["Machinegun"] = "machinegun";
3812
- WeaponId2["Chaingun"] = "chaingun";
3813
- WeaponId2["GrenadeLauncher"] = "grenade_launcher";
3814
- WeaponId2["RocketLauncher"] = "rocket_launcher";
3815
- WeaponId2["HyperBlaster"] = "hyperblaster";
3816
- WeaponId2["Railgun"] = "railgun";
3817
- WeaponId2["BFG10K"] = "bfg10k";
3818
- return WeaponId2;
3819
- })(WeaponId || {});
3820
- var PowerupId = /* @__PURE__ */ ((PowerupId2) => {
3821
- PowerupId2["QuadDamage"] = "quad";
3822
- PowerupId2["Invulnerability"] = "invulnerability";
3823
- PowerupId2["EnviroSuit"] = "enviro_suit";
3824
- PowerupId2["Rebreather"] = "rebreather";
3825
- PowerupId2["Silencer"] = "silencer";
3826
- return PowerupId2;
3827
- })(PowerupId || {});
3828
- var KeyId = /* @__PURE__ */ ((KeyId2) => {
3829
- KeyId2["Blue"] = "blue";
3830
- KeyId2["Red"] = "red";
3831
- KeyId2["Green"] = "green";
3832
- KeyId2["Yellow"] = "yellow";
3833
- return KeyId2;
3834
- })(KeyId || {});
3835
- function createPlayerInventory(options = {}) {
3836
- const ammo = createAmmoInventory(options.ammoCaps);
3837
- const ownedWeapons = new Set(options.weapons ?? []);
3838
- const powerups = new Map(options.powerups ?? []);
3839
- const keys = new Set(options.keys ?? []);
3840
- return {
3841
- ammo,
3842
- ownedWeapons,
3843
- currentWeapon: options.currentWeapon,
3844
- armor: options.armor ?? null,
3845
- powerups,
3846
- keys
3847
- };
3848
- }
3849
- function giveAmmo(inventory, ammoType, amount) {
3850
- return addAmmo(inventory.ammo, ammoType, amount);
3851
- }
3852
- function giveAmmoItem(inventory, itemId, options) {
3853
- return pickupAmmo(inventory.ammo, itemId, options);
3854
- }
3855
- function giveWeapon(inventory, weapon, select = false) {
3856
- const hadWeapon = inventory.ownedWeapons.has(weapon);
3857
- inventory.ownedWeapons.add(weapon);
3858
- if (select || !inventory.currentWeapon) {
3859
- inventory.currentWeapon = weapon;
3860
- }
3861
- return !hadWeapon;
3862
- }
3863
- function hasWeapon(inventory, weapon) {
3864
- return inventory.ownedWeapons.has(weapon);
3865
- }
3866
- function selectWeapon(inventory, weapon) {
3867
- if (!inventory.ownedWeapons.has(weapon)) {
3868
- return false;
3869
- }
3870
- inventory.currentWeapon = weapon;
3871
- return true;
3872
- }
3873
- function equipArmor(inventory, armorType, amount) {
3874
- if (!armorType || amount <= 0) {
3875
- inventory.armor = null;
3876
- return null;
3877
- }
3878
- const info = ARMOR_INFO[armorType];
3879
- const armorCount = Math.min(amount, info.maxCount);
3880
- inventory.armor = { armorType, armorCount };
3881
- return inventory.armor;
3882
- }
3883
- function addPowerup(inventory, powerup, expiresAt) {
3884
- inventory.powerups.set(powerup, expiresAt);
3885
- }
3886
- function hasPowerup(inventory, powerup) {
3887
- return inventory.powerups.has(powerup);
3888
- }
3889
- function clearExpiredPowerups(inventory, nowMs) {
3890
- for (const [id, expiresAt] of inventory.powerups.entries()) {
3891
- if (expiresAt !== null && expiresAt <= nowMs) {
3892
- inventory.powerups.delete(id);
3893
- }
3894
- }
3895
- }
3896
- function addKey(inventory, key) {
3897
- const before = inventory.keys.size;
3898
- inventory.keys.add(key);
3899
- return inventory.keys.size > before;
3900
- }
3901
- function hasKey(inventory, key) {
3902
- return inventory.keys.has(key);
3903
- }
3904
-
3905
3969
  // src/index.ts
3906
3970
  var ZERO_VEC32 = { x: 0, y: 0, z: 0 };
3907
3971
  function createGame(engine, options) {
@@ -4030,11 +4094,13 @@ function createGame(engine, options) {
4030
4094
  convertRereleaseSaveToGameSave,
4031
4095
  createAmmoInventory,
4032
4096
  createBaseAmmoCaps,
4097
+ createCallbackRegistry,
4033
4098
  createDefaultSpawnRegistry,
4034
4099
  createGame,
4035
4100
  createPlayerInventory,
4036
4101
  createSaveFile,
4037
4102
  damageModName,
4103
+ deserializePlayerInventory,
4038
4104
  equipArmor,
4039
4105
  facingIdeal,
4040
4106
  findTarget,
@@ -4057,8 +4123,10 @@ function createGame(engine, options) {
4057
4123
  parseSaveFile,
4058
4124
  pickupAmmo,
4059
4125
  rangeTo,
4126
+ registerCallback,
4060
4127
  registerDefaultSpawns,
4061
4128
  selectWeapon,
4129
+ serializePlayerInventory,
4062
4130
  serializeRereleaseSave,
4063
4131
  setMovedir,
4064
4132
  spawnEntitiesFromText,