@adatechnology/game-contracts 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,6 +16,30 @@ function blastShove(blast, target, options = {}) {
16
16
  return { speed, direction: [direction[0] / length, direction[1] / length, direction[2] / length], distance, falloff };
17
17
  }
18
18
 
19
+ // src/impact-cost.ts
20
+ var DEFAULT_COST_READS = 1e4;
21
+ var DEFAULT_COST_CEILING_MS = 50;
22
+ var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
23
+ var heapUsedBytes = () => {
24
+ const nodeProcess = globalThis.process;
25
+ return nodeProcess?.memoryUsage?.().heapUsed;
26
+ };
27
+ var checkReadCost = ({ name, read, options, problems }) => {
28
+ const reads = options.reads ?? DEFAULT_COST_READS;
29
+ const ceilingMs = options.ceilingMs ?? DEFAULT_COST_CEILING_MS;
30
+ const allocationBytes = options.allocationBytes ?? DEFAULT_COST_ALLOCATION_BYTES;
31
+ if (read() !== read()) problems.push(`${name}() returns a new object on each read: cache it between changes (cost contract)`);
32
+ for (let i = 0; i < reads; i++) read();
33
+ const heapBefore = heapUsedBytes();
34
+ const started = performance.now();
35
+ for (let i = 0; i < reads; i++) read();
36
+ const elapsedMs = performance.now() - started;
37
+ const heapAfter = heapUsedBytes();
38
+ if (elapsedMs > ceilingMs) problems.push(`${name}() took ${elapsedMs.toFixed(1)} ms for ${reads} reads, over the ${ceilingMs} ms ceiling (cost contract)`);
39
+ if (heapBefore !== void 0 && heapAfter !== void 0 && heapAfter - heapBefore > allocationBytes)
40
+ problems.push(`${name}() grew the heap by ${heapAfter - heapBefore} bytes over ${reads} reads, over the ${allocationBytes} byte ceiling (cost contract)`);
41
+ };
42
+
19
43
  // src/impact.ts
20
44
  var IMPACT_CONTRACT = {
21
45
  ballistic: { required: ["type", "point", "direction", "energy", "caliber"], optional: ["space", "source"], note: "bullets and pellets: joules and diameter in millimetres" },
@@ -122,6 +146,15 @@ var checkDestructible = (impl, options = {}) => {
122
146
  } catch (e) {
123
147
  problems.push(`reset() threw: ${String(e)}`);
124
148
  }
149
+ if (options.cost) {
150
+ const cost = options.cost === true ? {} : options.cost;
151
+ try {
152
+ checkReadCost({ name: "state", read: () => impl.state(), options: cost, problems });
153
+ checkReadCost({ name: "describe", read: () => impl.describe(), options: cost, problems });
154
+ } catch (e) {
155
+ problems.push(`cost measurement threw: ${String(e)}`);
156
+ }
157
+ }
125
158
  return { ok: problems.length === 0, problems };
126
159
  };
127
160
 
@@ -597,6 +630,66 @@ var createToolClipPlayer = (options) => {
597
630
  };
598
631
  };
599
632
 
633
+ // src/frame.ts
634
+ var FRAME_PHASES = ["input", "fixedSimulate", "lateSimulate", "present", "render", "idle"];
635
+ var DEFERRED_QUEUE_ID = {
636
+ DESTRUCTION_EVENTS: "destruction.events",
637
+ BREACH_PAINT: "floorplan.breach-paint",
638
+ AUDIO_VOICES: "audio.voices",
639
+ VEHICLE_CREASE: "vehicles.crease"
640
+ };
641
+ var DEFERRED_PRIORITY = {
642
+ [DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
643
+ [DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
644
+ [DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
645
+ [DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
646
+ };
647
+ function checkDeferredQueue(queue, fill) {
648
+ const problems = [];
649
+ fill();
650
+ const waiting = queue.pending(1);
651
+ if (waiting < 2) problems.push(`fill() must leave at least 2 units waiting (pending is ${waiting})`);
652
+ if (queue.pending(1) !== waiting) problems.push("pending(frame) changed between two calls in the same frame");
653
+ const ran = queue.run(0, () => 1);
654
+ if (ran !== 1) problems.push(`run with a deadline already past must run exactly one unit (ran ${ran})`);
655
+ if (waiting >= 2 && queue.pending(2) !== waiting - 1) problems.push(`one unit ran but pending went from ${waiting} to ${queue.pending(2)}`);
656
+ try {
657
+ queue.attach?.();
658
+ queue.attach?.();
659
+ queue.detach?.();
660
+ queue.detach?.();
661
+ } catch (error) {
662
+ problems.push(`attach/detach must be idempotent: ${error instanceof Error ? error.message : String(error)}`);
663
+ }
664
+ queue.flush();
665
+ if (queue.pending(3) !== 0) problems.push(`flush must leave nothing pending (pending is ${queue.pending(3)})`);
666
+ if (queue.run(0, () => 1) !== 0) problems.push("run on an empty queue must run no unit");
667
+ return { ok: problems.length === 0, problems };
668
+ }
669
+
670
+ // src/warn.ts
671
+ var WARNED_KEY = /* @__PURE__ */ Symbol.for("@adatechnology/game-contracts.warned");
672
+ var warnedKeys = () => {
673
+ const holder = globalThis;
674
+ return holder[WARNED_KEY] ?? (holder[WARNED_KEY] = /* @__PURE__ */ new Set());
675
+ };
676
+ var isProduction = () => {
677
+ try {
678
+ return process.env.NODE_ENV === "production";
679
+ } catch {
680
+ return false;
681
+ }
682
+ };
683
+ var isQuiet = () => globalThis.__GAME_KIT_QUIET__ === true;
684
+ var warnOnce = (key, message) => {
685
+ if (isQuiet() || isProduction()) return;
686
+ const seen = warnedKeys();
687
+ if (seen.has(key)) return;
688
+ seen.add(key);
689
+ console.warn(`[game-kit] ${message}`);
690
+ };
691
+ var resetWarnOnce = () => warnedKeys().clear();
692
+
600
693
  // src/registry.ts
601
694
  var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
602
695
  var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
@@ -619,10 +712,13 @@ var applyPatches = (entries, patches) => {
619
712
  for (const [id, patch] of Object.entries(patches ?? {})) {
620
713
  const entry = entries.get(id);
621
714
  if (entry) entries.set(id, deepMerge(entry, patch));
715
+ else warnOnce(`registry.patch.${id}`, `patch for "${id}" ignored: no entry has that id`);
622
716
  }
623
717
  };
624
718
  var removeIds = (entries, ids) => {
625
- for (const id of ids ?? []) entries.delete(id);
719
+ for (const id of ids ?? []) {
720
+ if (!entries.delete(id)) warnOnce(`registry.remove.${id}`, `remove of "${id}" ignored: no entry has that id`);
721
+ }
626
722
  };
627
723
 
628
724
  // src/bench.ts
@@ -861,14 +957,135 @@ function footprintBlocker(object, footprint) {
861
957
  return lx > fp.minX - radius && lx < fp.maxX + radius && lz > fp.minZ - radius && lz < fp.maxZ + radius;
862
958
  };
863
959
  }
960
+
961
+ // src/foley.ts
962
+ var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
963
+ var DEFAULT_SURFACE = "concrete";
964
+ var GAIT_IDS = ["walk", "run"];
965
+ var LANDING_IDS = ["light", "heavy"];
966
+ var HEAVY_LANDING_SPEED = 5.5;
967
+ var BLOW_IDS = ["punch", "kick", "shove", "blade", "blunt", "chop"];
968
+ var BODY_SOUND_IDS = [
969
+ "jump",
970
+ "cloth_rustle",
971
+ "body_fall",
972
+ "fall_damage",
973
+ "player_hurt",
974
+ "player_breath_run",
975
+ "player_death",
976
+ "bite",
977
+ "grab",
978
+ "punch_swing",
979
+ "punch_hit_body",
980
+ "punch_hit_face",
981
+ "punch_hit_object",
982
+ "kick_swing",
983
+ "kick_hit",
984
+ "kick_hit_object",
985
+ "shove_two_hands",
986
+ "knife_swing",
987
+ "knife_stab",
988
+ "knife_slash",
989
+ "knife_hit_object",
990
+ "blunt_swing",
991
+ "blunt_hit",
992
+ "blunt_hit_object",
993
+ "axe_swing",
994
+ "axe_chop",
995
+ "axe_hit_object"
996
+ ];
997
+ var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
998
+ var landSoundId = (surface, fallSpeed) => `land_${surface}_${fallSpeed >= HEAVY_LANDING_SPEED ? "heavy" : "light"}`;
999
+ var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
1000
+ var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing) => `land_${surface}_${landing}`));
1001
+ var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
1002
+ var SWING = {
1003
+ punch: "punch_swing",
1004
+ kick: "kick_swing",
1005
+ // two hands pushed out do not whistle: the shove is one sound, at the contact
1006
+ shove: null,
1007
+ blade: "knife_swing",
1008
+ blunt: "blunt_swing",
1009
+ chop: "axe_swing"
1010
+ };
1011
+ var SWING_LEAD_SECONDS = { punch: 0.07, kick: 0.09, shove: 0, blade: 0.05, blunt: 0.1, chop: 0.11 };
1012
+ var swingSoundId = (blow) => SWING[blow];
1013
+ var HIT = {
1014
+ punch: { body: "punch_hit_body", head: "punch_hit_face", object: "punch_hit_object" },
1015
+ kick: { body: "kick_hit", head: "kick_hit", object: "kick_hit_object" },
1016
+ shove: { body: "shove_two_hands", head: "shove_two_hands", object: "shove_two_hands" },
1017
+ blade: { body: "knife_stab", head: "knife_stab", object: "knife_hit_object" },
1018
+ blunt: { body: "blunt_hit", head: "blunt_hit", object: "blunt_hit_object" },
1019
+ chop: { body: "axe_chop", head: "axe_chop", object: "axe_hit_object" }
1020
+ };
1021
+ var hitSoundId = (blow, target) => HIT[blow][target];
1022
+ var MATERIAL_SURFACES = {
1023
+ concrete: "concrete",
1024
+ mortar: "concrete",
1025
+ stone: "concrete",
1026
+ brick: "concrete",
1027
+ plaster: "concrete",
1028
+ asphalt: "asphalt",
1029
+ wood_soft: "wood",
1030
+ wood_hard: "wood",
1031
+ green_wood: "wood",
1032
+ plywood: "wood",
1033
+ mdf: "wood",
1034
+ ceramic: "tile",
1035
+ plastic: "tile",
1036
+ soil: "dirt",
1037
+ wax: "dirt",
1038
+ flesh_fruit: "dirt",
1039
+ turf: "grass",
1040
+ foliage: "grass",
1041
+ cactus: "grass",
1042
+ gravel: "gravel",
1043
+ sand: "sand",
1044
+ steel: "metal",
1045
+ cast_iron: "metal",
1046
+ aluminium: "metal",
1047
+ copper: "metal",
1048
+ water: "water",
1049
+ glass: "glass",
1050
+ tempered_glass: "glass",
1051
+ fabric: "carpet",
1052
+ foam: "carpet",
1053
+ leather: "carpet",
1054
+ rubber: "carpet",
1055
+ paper: "carpet"
1056
+ };
1057
+ var isSurfaceId = (value) => typeof value === "string" && SURFACE_IDS.includes(value);
1058
+ function surfaceOfMaterial(material, table = MATERIAL_SURFACES, fallback = DEFAULT_SURFACE) {
1059
+ if (material === void 0) return fallback;
1060
+ if (isSurfaceId(material)) return material;
1061
+ return table[material] ?? fallback;
1062
+ }
1063
+
1064
+ // src/index.ts
1065
+ var resetWarnOnce2 = resetWarnOnce;
864
1066
  export {
1067
+ BLOW_IDS,
1068
+ BODY_FOLEY_IDS,
1069
+ BODY_SOUND_IDS,
865
1070
  DEFAULT_BLAST_SHOVE,
866
1071
  DEFAULT_ENVIRONMENT,
867
1072
  DEFAULT_IGNITION_RULES,
1073
+ DEFAULT_SURFACE,
1074
+ DEFERRED_PRIORITY,
1075
+ DEFERRED_QUEUE_ID,
1076
+ FOOTSTEP_SOUND_IDS,
1077
+ FRAME_PHASES,
1078
+ GAIT_IDS,
1079
+ HEAVY_LANDING_SPEED,
868
1080
  IMPACT_CONTRACT,
869
1081
  KIT_ORIGIN_KEY,
1082
+ LANDING_IDS,
1083
+ LAND_SOUND_IDS,
1084
+ MATERIAL_SURFACES,
870
1085
  MISSING_BITS,
871
1086
  STAGE_EVENT,
1087
+ SURFACE_IDS,
1088
+ SWING_LEAD_SECONDS,
872
1089
  TOOL_CLIP,
873
1090
  TWIST_LIFT,
874
1091
  applyPatches,
@@ -876,6 +1093,7 @@ export {
876
1093
  checkAnimation,
877
1094
  checkBench,
878
1095
  checkCategorized,
1096
+ checkDeferredQueue,
879
1097
  checkDestructible,
880
1098
  checkHit,
881
1099
  checkSpec,
@@ -886,11 +1104,16 @@ export {
886
1104
  defineCategories,
887
1105
  fillFactor,
888
1106
  footprintBlocker,
1107
+ footstepSoundId,
1108
+ hitSoundId,
889
1109
  ignitionChance,
1110
+ isSurfaceId,
890
1111
  kitOriginOf,
1112
+ landSoundId,
891
1113
  markKitOrigin,
892
1114
  mergeById,
893
1115
  removeIds,
1116
+ resetWarnOnce2 as resetWarnOnce,
894
1117
  resolveEntry,
895
1118
  resolveIgnitionRules,
896
1119
  sampleClip,
@@ -900,9 +1123,12 @@ export {
900
1123
  shotExtras,
901
1124
  shotToHit,
902
1125
  sparkTargetFactor,
1126
+ surfaceOfMaterial,
1127
+ swingSoundId,
903
1128
  toolAnimationStep,
904
1129
  twistStroke,
905
1130
  unmarkedDrawings,
906
- useToHit
1131
+ useToHit,
1132
+ warnOnce
907
1133
  };
908
1134
  //# sourceMappingURL=index.js.map