@adatechnology/game-contracts 0.21.0 → 0.23.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,83 @@ 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/blast-knock.ts
20
+ var KNOCK_DOWN_SPEED = 2.5;
21
+ var KNOCKED_DOWN_ANGLE = 1.35;
22
+ var LIE_SECONDS = 1.6;
23
+ var STAND_UP_PER_SPEED = 0.25;
24
+ var FALL_SECONDS = 0.25;
25
+ var RISE_SECONDS = 0.6;
26
+ var GROUND_FRICTION = 7;
27
+ var GRAVITY = 9.81;
28
+ var KNOCK_CHEST = 1.1;
29
+ var CHEST = KNOCK_CHEST;
30
+ var bodyMass = (height, build = 1) => 70 * (height / 1.75) ** 2 * build;
31
+ function knockOf(blast, feet, mass, options) {
32
+ const hit = blastShove(blast, { position: [feet[0], feet[1] + CHEST, feet[2]], mass }, options);
33
+ if (!hit) return void 0;
34
+ const [dx, dy, dz] = hit.direction;
35
+ return { velocity: [dx * hit.speed, Math.max(0, dy * hit.speed), dz * hit.speed], airborne: dy > 0, lying: 0, lieLeft: 0, down: hit.speed >= KNOCK_DOWN_SPEED };
36
+ }
37
+ function stepKnock(knock, position, dt, groundAt, alive) {
38
+ const v = knock.velocity;
39
+ position.x += v[0] * dt;
40
+ position.z += v[2] * dt;
41
+ const ground = groundAt(position.x, position.z);
42
+ if (knock.airborne || position.y > ground + 1e-4) {
43
+ v[1] -= GRAVITY * dt;
44
+ position.y += v[1] * dt;
45
+ if (position.y <= ground) {
46
+ position.y = ground;
47
+ v[1] = 0;
48
+ knock.airborne = false;
49
+ }
50
+ } else position.y = ground;
51
+ const horizontal = Math.hypot(v[0], v[2]);
52
+ if (!knock.airborne && horizontal > 0) {
53
+ const slowed = Math.max(0, horizontal - GROUND_FRICTION * dt);
54
+ v[0] *= slowed / horizontal;
55
+ v[2] *= slowed / horizontal;
56
+ }
57
+ const moving = horizontal > 0.05 || knock.airborne;
58
+ if (knock.down) {
59
+ if (moving) knock.lying = Math.min(1, knock.lying + dt / FALL_SECONDS);
60
+ else {
61
+ knock.lying = 1;
62
+ if (alive) {
63
+ knock.lieLeft = knock.lieLeft > 0 ? knock.lieLeft : LIE_SECONDS + Math.hypot(v[0], v[2]) * STAND_UP_PER_SPEED;
64
+ knock.lieLeft -= dt;
65
+ if (knock.lieLeft <= 0) knock.down = false;
66
+ }
67
+ }
68
+ } else if (knock.lying > 0) knock.lying = Math.max(0, knock.lying - dt / RISE_SECONDS);
69
+ return !moving && !knock.down && knock.lying === 0;
70
+ }
71
+
72
+ // src/impact-cost.ts
73
+ var DEFAULT_COST_READS = 1e4;
74
+ var DEFAULT_COST_CEILING_MS = 50;
75
+ var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
76
+ var heapUsedBytes = () => {
77
+ const nodeProcess = globalThis.process;
78
+ return nodeProcess?.memoryUsage?.().heapUsed;
79
+ };
80
+ var checkReadCost = ({ name, read, options, problems }) => {
81
+ const reads = options.reads ?? DEFAULT_COST_READS;
82
+ const ceilingMs = options.ceilingMs ?? DEFAULT_COST_CEILING_MS;
83
+ const allocationBytes = options.allocationBytes ?? DEFAULT_COST_ALLOCATION_BYTES;
84
+ if (read() !== read()) problems.push(`${name}() returns a new object on each read: cache it between changes (cost contract)`);
85
+ for (let i = 0; i < reads; i++) read();
86
+ const heapBefore = heapUsedBytes();
87
+ const started = performance.now();
88
+ for (let i = 0; i < reads; i++) read();
89
+ const elapsedMs = performance.now() - started;
90
+ const heapAfter = heapUsedBytes();
91
+ if (elapsedMs > ceilingMs) problems.push(`${name}() took ${elapsedMs.toFixed(1)} ms for ${reads} reads, over the ${ceilingMs} ms ceiling (cost contract)`);
92
+ if (heapBefore !== void 0 && heapAfter !== void 0 && heapAfter - heapBefore > allocationBytes)
93
+ problems.push(`${name}() grew the heap by ${heapAfter - heapBefore} bytes over ${reads} reads, over the ${allocationBytes} byte ceiling (cost contract)`);
94
+ };
95
+
19
96
  // src/impact.ts
20
97
  var IMPACT_CONTRACT = {
21
98
  ballistic: { required: ["type", "point", "direction", "energy", "caliber"], optional: ["space", "source"], note: "bullets and pellets: joules and diameter in millimetres" },
@@ -122,6 +199,15 @@ var checkDestructible = (impl, options = {}) => {
122
199
  } catch (e) {
123
200
  problems.push(`reset() threw: ${String(e)}`);
124
201
  }
202
+ if (options.cost) {
203
+ const cost = options.cost === true ? {} : options.cost;
204
+ try {
205
+ checkReadCost({ name: "state", read: () => impl.state(), options: cost, problems });
206
+ checkReadCost({ name: "describe", read: () => impl.describe(), options: cost, problems });
207
+ } catch (e) {
208
+ problems.push(`cost measurement threw: ${String(e)}`);
209
+ }
210
+ }
125
211
  return { ok: problems.length === 0, problems };
126
212
  };
127
213
 
@@ -526,6 +612,21 @@ var checkToolClips = (clips, partIds) => {
526
612
  }
527
613
  return { ok: problems.length === 0, problems };
528
614
  };
615
+ var leadClipEvents = (clip) => {
616
+ const events = clip.events ?? [];
617
+ if (!events.some((event) => (event.lead ?? 0) > 0)) return clip;
618
+ let floor = 0;
619
+ const led = events.map((event) => {
620
+ const lead = Math.max(0, Math.min(event.t - floor, event.lead ?? 0));
621
+ const t = event.t - lead;
622
+ floor = Math.max(floor, t);
623
+ if (event.lead === void 0) return event;
624
+ const moved = { ...event, t, data: lead > 0 ? { ...event.data, lead } : { ...event.data } };
625
+ delete moved.lead;
626
+ return moved;
627
+ });
628
+ return { ...clip, events: led };
629
+ };
529
630
  var createToolClipPlayer = (options) => {
530
631
  const { clips, rig, onEvent } = options;
531
632
  let instances = [];
@@ -568,7 +669,7 @@ var createToolClipPlayer = (options) => {
568
669
  }
569
670
  const found = find(step.clip, step.variant);
570
671
  if (!found) return;
571
- const clip = step.duration && step.duration > 0 ? scaleClip(found.clip, step.duration) : found.clip;
672
+ const clip = leadClipEvents(step.duration && step.duration > 0 ? scaleClip(found.clip, step.duration) : found.clip);
572
673
  const interval = options.repeatInterval?.() ?? 0.1;
573
674
  instances = instances.filter((existing) => existing.name !== found.name || existing.delay > 0);
574
675
  for (let round = 0; round < (step.repeat ?? 1); round++) {
@@ -597,6 +698,66 @@ var createToolClipPlayer = (options) => {
597
698
  };
598
699
  };
599
700
 
701
+ // src/frame.ts
702
+ var FRAME_PHASES = ["input", "fixedSimulate", "lateSimulate", "present", "render", "idle"];
703
+ var DEFERRED_QUEUE_ID = {
704
+ DESTRUCTION_EVENTS: "destruction.events",
705
+ BREACH_PAINT: "floorplan.breach-paint",
706
+ AUDIO_VOICES: "audio.voices",
707
+ VEHICLE_CREASE: "vehicles.crease"
708
+ };
709
+ var DEFERRED_PRIORITY = {
710
+ [DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
711
+ [DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
712
+ [DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
713
+ [DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
714
+ };
715
+ function checkDeferredQueue(queue, fill) {
716
+ const problems = [];
717
+ fill();
718
+ const waiting = queue.pending(1);
719
+ if (waiting < 2) problems.push(`fill() must leave at least 2 units waiting (pending is ${waiting})`);
720
+ if (queue.pending(1) !== waiting) problems.push("pending(frame) changed between two calls in the same frame");
721
+ const ran = queue.run(0, () => 1);
722
+ if (ran !== 1) problems.push(`run with a deadline already past must run exactly one unit (ran ${ran})`);
723
+ if (waiting >= 2 && queue.pending(2) !== waiting - 1) problems.push(`one unit ran but pending went from ${waiting} to ${queue.pending(2)}`);
724
+ try {
725
+ queue.attach?.();
726
+ queue.attach?.();
727
+ queue.detach?.();
728
+ queue.detach?.();
729
+ } catch (error) {
730
+ problems.push(`attach/detach must be idempotent: ${error instanceof Error ? error.message : String(error)}`);
731
+ }
732
+ queue.flush();
733
+ if (queue.pending(3) !== 0) problems.push(`flush must leave nothing pending (pending is ${queue.pending(3)})`);
734
+ if (queue.run(0, () => 1) !== 0) problems.push("run on an empty queue must run no unit");
735
+ return { ok: problems.length === 0, problems };
736
+ }
737
+
738
+ // src/warn.ts
739
+ var WARNED_KEY = /* @__PURE__ */ Symbol.for("@adatechnology/game-contracts.warned");
740
+ var warnedKeys = () => {
741
+ const holder = globalThis;
742
+ return holder[WARNED_KEY] ?? (holder[WARNED_KEY] = /* @__PURE__ */ new Set());
743
+ };
744
+ var isProduction = () => {
745
+ try {
746
+ return process.env.NODE_ENV === "production";
747
+ } catch {
748
+ return false;
749
+ }
750
+ };
751
+ var isQuiet = () => globalThis.__GAME_KIT_QUIET__ === true;
752
+ var warnOnce = (key, message) => {
753
+ if (isQuiet() || isProduction()) return;
754
+ const seen = warnedKeys();
755
+ if (seen.has(key)) return;
756
+ seen.add(key);
757
+ console.warn(`[game-kit] ${message}`);
758
+ };
759
+ var resetWarnOnce = () => warnedKeys().clear();
760
+
600
761
  // src/registry.ts
601
762
  var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
602
763
  var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
@@ -619,10 +780,13 @@ var applyPatches = (entries, patches) => {
619
780
  for (const [id, patch] of Object.entries(patches ?? {})) {
620
781
  const entry = entries.get(id);
621
782
  if (entry) entries.set(id, deepMerge(entry, patch));
783
+ else warnOnce(`registry.patch.${id}`, `patch for "${id}" ignored: no entry has that id`);
622
784
  }
623
785
  };
624
786
  var removeIds = (entries, ids) => {
625
- for (const id of ids ?? []) entries.delete(id);
787
+ for (const id of ids ?? []) {
788
+ if (!entries.delete(id)) warnOnce(`registry.remove.${id}`, `remove of "${id}" ignored: no entry has that id`);
789
+ }
626
790
  };
627
791
 
628
792
  // src/bench.ts
@@ -861,21 +1025,147 @@ function footprintBlocker(object, footprint) {
861
1025
  return lx > fp.minX - radius && lx < fp.maxX + radius && lz > fp.minZ - radius && lz < fp.maxZ + radius;
862
1026
  };
863
1027
  }
1028
+
1029
+ // src/foley.ts
1030
+ var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
1031
+ var DEFAULT_SURFACE = "concrete";
1032
+ var GAIT_IDS = ["walk", "run"];
1033
+ var LANDING_IDS = ["light", "heavy"];
1034
+ var HEAVY_LANDING_SPEED = 5.5;
1035
+ var BLOW_IDS = ["punch", "kick", "shove", "blade", "blunt", "chop"];
1036
+ var BODY_SOUND_IDS = [
1037
+ "jump",
1038
+ "cloth_rustle",
1039
+ "body_fall",
1040
+ "fall_damage",
1041
+ "player_hurt",
1042
+ "player_breath_run",
1043
+ "player_death",
1044
+ "bite",
1045
+ "grab",
1046
+ "punch_swing",
1047
+ "punch_hit_body",
1048
+ "punch_hit_face",
1049
+ "punch_hit_object",
1050
+ "kick_swing",
1051
+ "kick_hit",
1052
+ "kick_hit_object",
1053
+ "shove_two_hands",
1054
+ "knife_swing",
1055
+ "knife_stab",
1056
+ "knife_slash",
1057
+ "knife_hit_object",
1058
+ "blunt_swing",
1059
+ "blunt_hit",
1060
+ "blunt_hit_object",
1061
+ "axe_swing",
1062
+ "axe_chop",
1063
+ "axe_hit_object"
1064
+ ];
1065
+ var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
1066
+ var landSoundId = (surface, fallSpeed) => `land_${surface}_${fallSpeed >= HEAVY_LANDING_SPEED ? "heavy" : "light"}`;
1067
+ var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
1068
+ var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing) => `land_${surface}_${landing}`));
1069
+ var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
1070
+ var SWING = {
1071
+ punch: "punch_swing",
1072
+ kick: "kick_swing",
1073
+ // two hands pushed out do not whistle: the shove is one sound, at the contact
1074
+ shove: null,
1075
+ blade: "knife_swing",
1076
+ blunt: "blunt_swing",
1077
+ chop: "axe_swing"
1078
+ };
1079
+ var SWING_LEAD_SECONDS = { punch: 0.07, kick: 0.09, shove: 0, blade: 0.05, blunt: 0.1, chop: 0.11 };
1080
+ var swingSoundId = (blow) => SWING[blow];
1081
+ var HIT = {
1082
+ punch: { body: "punch_hit_body", head: "punch_hit_face", object: "punch_hit_object" },
1083
+ kick: { body: "kick_hit", head: "kick_hit", object: "kick_hit_object" },
1084
+ shove: { body: "shove_two_hands", head: "shove_two_hands", object: "shove_two_hands" },
1085
+ blade: { body: "knife_stab", head: "knife_stab", object: "knife_hit_object" },
1086
+ blunt: { body: "blunt_hit", head: "blunt_hit", object: "blunt_hit_object" },
1087
+ chop: { body: "axe_chop", head: "axe_chop", object: "axe_hit_object" }
1088
+ };
1089
+ var hitSoundId = (blow, target) => HIT[blow][target];
1090
+ var MATERIAL_SURFACES = {
1091
+ concrete: "concrete",
1092
+ mortar: "concrete",
1093
+ stone: "concrete",
1094
+ brick: "concrete",
1095
+ plaster: "concrete",
1096
+ asphalt: "asphalt",
1097
+ wood_soft: "wood",
1098
+ wood_hard: "wood",
1099
+ green_wood: "wood",
1100
+ plywood: "wood",
1101
+ mdf: "wood",
1102
+ ceramic: "tile",
1103
+ plastic: "tile",
1104
+ soil: "dirt",
1105
+ wax: "dirt",
1106
+ flesh_fruit: "dirt",
1107
+ turf: "grass",
1108
+ foliage: "grass",
1109
+ cactus: "grass",
1110
+ gravel: "gravel",
1111
+ sand: "sand",
1112
+ steel: "metal",
1113
+ cast_iron: "metal",
1114
+ aluminium: "metal",
1115
+ copper: "metal",
1116
+ water: "water",
1117
+ glass: "glass",
1118
+ tempered_glass: "glass",
1119
+ fabric: "carpet",
1120
+ foam: "carpet",
1121
+ leather: "carpet",
1122
+ rubber: "carpet",
1123
+ paper: "carpet"
1124
+ };
1125
+ var isSurfaceId = (value) => typeof value === "string" && SURFACE_IDS.includes(value);
1126
+ function surfaceOfMaterial(material, table = MATERIAL_SURFACES, fallback = DEFAULT_SURFACE) {
1127
+ if (material === void 0) return fallback;
1128
+ if (isSurfaceId(material)) return material;
1129
+ return table[material] ?? fallback;
1130
+ }
1131
+
1132
+ // src/index.ts
1133
+ var resetWarnOnce2 = resetWarnOnce;
864
1134
  export {
1135
+ BLOW_IDS,
1136
+ BODY_FOLEY_IDS,
1137
+ BODY_SOUND_IDS,
865
1138
  DEFAULT_BLAST_SHOVE,
866
1139
  DEFAULT_ENVIRONMENT,
867
1140
  DEFAULT_IGNITION_RULES,
1141
+ DEFAULT_SURFACE,
1142
+ DEFERRED_PRIORITY,
1143
+ DEFERRED_QUEUE_ID,
1144
+ FOOTSTEP_SOUND_IDS,
1145
+ FRAME_PHASES,
1146
+ GAIT_IDS,
1147
+ HEAVY_LANDING_SPEED,
868
1148
  IMPACT_CONTRACT,
869
1149
  KIT_ORIGIN_KEY,
1150
+ KNOCKED_DOWN_ANGLE,
1151
+ KNOCK_CHEST,
1152
+ KNOCK_DOWN_SPEED,
1153
+ LANDING_IDS,
1154
+ LAND_SOUND_IDS,
1155
+ MATERIAL_SURFACES,
870
1156
  MISSING_BITS,
871
1157
  STAGE_EVENT,
1158
+ SURFACE_IDS,
1159
+ SWING_LEAD_SECONDS,
872
1160
  TOOL_CLIP,
873
1161
  TWIST_LIFT,
874
1162
  applyPatches,
875
1163
  blastShove,
1164
+ bodyMass,
876
1165
  checkAnimation,
877
1166
  checkBench,
878
1167
  checkCategorized,
1168
+ checkDeferredQueue,
879
1169
  checkDestructible,
880
1170
  checkHit,
881
1171
  checkSpec,
@@ -886,11 +1176,18 @@ export {
886
1176
  defineCategories,
887
1177
  fillFactor,
888
1178
  footprintBlocker,
1179
+ footstepSoundId,
1180
+ hitSoundId,
889
1181
  ignitionChance,
1182
+ isSurfaceId,
890
1183
  kitOriginOf,
1184
+ knockOf,
1185
+ landSoundId,
1186
+ leadClipEvents,
891
1187
  markKitOrigin,
892
1188
  mergeById,
893
1189
  removeIds,
1190
+ resetWarnOnce2 as resetWarnOnce,
894
1191
  resolveEntry,
895
1192
  resolveIgnitionRules,
896
1193
  sampleClip,
@@ -900,9 +1197,13 @@ export {
900
1197
  shotExtras,
901
1198
  shotToHit,
902
1199
  sparkTargetFactor,
1200
+ stepKnock,
1201
+ surfaceOfMaterial,
1202
+ swingSoundId,
903
1203
  toolAnimationStep,
904
1204
  twistStroke,
905
1205
  unmarkedDrawings,
906
- useToHit
1206
+ useToHit,
1207
+ warnOnce
907
1208
  };
908
1209
  //# sourceMappingURL=index.js.map