@adatechnology/game-destruction 0.13.2 → 0.14.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/README.md CHANGED
@@ -383,6 +383,11 @@ helpers `vec`, `quat`, `quatFromEuler`, `rotateVec`, `toLocal`, `toWorld`, the l
383
383
  The locale table has `en` and `pt-BR` strings for material names, part statuses, joint kinds, tools, damage types, event labels and fracture styles:
384
384
  `resolveStrings('pt-BR').materialNames.glass === 'vidro'`. Unknown locales fall back to `en`.
385
385
 
386
+ ## Assemblies by instance
387
+
388
+ `createAssemblies({ packs, patch, remove })` makes a set of assemblies of its own: the built-in ones plus `packs`. `packs` is a list of lists of `AssemblyDef` (`[[wallDef, doorDef], [extraDef]]`); a def with the id of a built-in replaces it.
389
+ `patch` merges fields by assembly id, `remove` lists ids to take out. The global `ASSEMBLIES`, `registerAssembly` and the rest are deprecated delegates of `defaultAssemblies()`.
390
+
386
391
  ## Português
387
392
 
388
393
  Modelo de destruição independente de engine. Um objeto é um conjunto de peças (caixa, cilindro ou esfera com um material) unidas por juntas. O
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Vec3, Quat, MaterialId, FractureStyle, DamageType, DebrisKind, DestructionEvent, StructureRole, SoilProfile, WindExposure, IgnitionRules, BlastOptions, DestructibleSpec, ObjectState, DestructibleImpl, Hit, HitResult, RayHit, BallisticExtras, ToolKind, JointState, PartState, DestructionEventType, ConfinementOptions, FragmentHitOptions, ShelterOptions, ExplosionChain, PartSpec, JointKind, JointSpec, PartShape, SurfaceLayer, PartUnits, Reinforcement, PartBreach, PileSpec, PartStatus } from '@adatechnology/game-contracts';
1
+ import { Vec3, Quat, MaterialId, FractureStyle, DamageType, DebrisKind, DestructionEvent, StructureRole, SoilProfile, WindExposure, IgnitionRules, BlastOptions, DestructibleSpec, ObjectState, DestructibleImpl, Hit, HitResult, RayHit, BallisticExtras, ToolKind, JointState, PartState, DestructionEventType, ConfinementOptions, FragmentHitOptions, ShelterOptions, ExplosionChain, PartSpec, JointKind, JointSpec, PartShape, SurfaceLayer, PartUnits, Reinforcement, PartBreach, DeepPartial, PileSpec, PartStatus, CostOptions } from '@adatechnology/game-contracts';
2
2
  export { BallisticExtras, BlastOptions, BlastReport, ChainDelay, ConfinementOptions, Crack, DamageStage, DamageType, DebrisKind, Dent, DestructibleImpl, DestructibleSpec, DestructionEvent, DestructionEventType, ExplosionChain, FluidKind, FractureStyle, Fragment, FragmentHitOptions, FragmentMark, Hit, HitIgnition, HitResult, Hole, IMPACT_CONTRACT, ImpactContract, JointKind, JointSpec, JointState, MaterialId, ObjectState, PartBreach, PartShape, PartSpec, PartState, PartStatus, PartUnits, PileSpec, Quat, RayHit, Reinforcement, ShelterOptions, SoilLayer, SoilProfile, SoilState, StructureRole, SurfaceLayer, TensionRebar, ToolKind, TunedMassDamper, Vec3, WindExposure, WindState, checkHit } from '@adatechnology/game-contracts';
3
3
 
4
4
  /**
@@ -324,6 +324,11 @@ interface DestructionOptions {
324
324
  maxDents?: number;
325
325
  /** Maximum holes kept per part (default 32). */
326
326
  maxHoles?: number;
327
+ /**
328
+ * Most primed explosives that go off in one `update` (default: no cap). The rest wait, ordered by due time, then older object, then part
329
+ * order, and go off in the next updates, so a long chain spreads over steps by count and not by the speed of the machine.
330
+ */
331
+ chainLinksPerUpdate?: number;
327
332
  /** Distance at which a burning part heats parts of other objects, in metres (default 0.6). */
328
333
  fireSpread?: number;
329
334
  /**
@@ -711,16 +716,38 @@ interface AssemblyPartOptions {
711
716
  /** Materials to read densities from (default the built-in `MATERIALS`). */
712
717
  materials?: ReadonlyMap<MaterialId, MaterialDef>;
713
718
  }
714
- /** Built-in assemblies; `registerAssembly` adds to this list. */
719
+ /**
720
+ * Built-in assemblies; `registerAssembly` adds to this list.
721
+ * @deprecated the list every page shares. Use `createAssemblies({ packs, patch, remove })`, or `defaultAssemblies()` for this one.
722
+ */
715
723
  declare const ASSEMBLIES: AssemblyDef[];
724
+ interface AssembliesOptions {
725
+ /** Assemblies added to the built-in ones; one with the id of a built-in replaces it. */
726
+ readonly packs?: readonly (readonly AssemblyDef[])[];
727
+ /** Changes by id, merged field by field over what is there. */
728
+ readonly patch?: Readonly<Record<string, DeepPartial<AssemblyDef>>>;
729
+ /** Ids taken out. */
730
+ readonly remove?: readonly string[];
731
+ }
732
+ interface Assemblies {
733
+ list(): readonly AssemblyDef[];
734
+ get(id: string): AssemblyDef | undefined;
735
+ /** Adds an assembly, or replaces the one with the same id. */
736
+ register(def: AssemblyDef): void;
737
+ /** The wall as a `PartSpec`; throws for an id this set does not have. */
738
+ part(assemblyId: string, options: AssemblyPartOptions): PartSpec;
739
+ }
740
+ /** A set of assemblies of its own: the built-in ones (copied) plus `packs`, `patch` and `remove`. Nothing is shared with any other set on the page. */
741
+ declare const createAssemblies: (options?: AssembliesOptions) => Assemblies;
742
+ /** The set behind `ASSEMBLIES`, `registerAssembly`, `getAssembly` and `assemblyPart`, created on first use. */
743
+ declare const defaultAssemblies: () => Assemblies;
744
+ /** @deprecated use `createAssemblies(...).register`. */
716
745
  declare const registerAssembly: (def: AssemblyDef) => void;
746
+ /** @deprecated use `createAssemblies(...).get`. */
717
747
  declare const getAssembly: (id: string) => AssemblyDef | undefined;
718
748
  /** Physical depth of the wall in metres. */
719
749
  declare const assemblyDepth: (def: AssemblyDef) => number;
720
- /**
721
- * The wall as a box `[width, height, depth]` (thin along local z) with the assembly's layers, units, rebar and breach. Mass is the core (times `fill`)
722
- * plus the coats; `thickness` is the core's, so each coat adds its own `penetration x thickness` to what a bullet must cross.
723
- */
750
+ /** @deprecated use `createAssemblies(...).part`. */
724
751
  declare const assemblyPart: (assemblyId: string, options: AssemblyPartOptions) => PartSpec;
725
752
 
726
753
  interface FrameOptions {
@@ -1086,6 +1113,8 @@ declare const resolveStrings: (locale: string | undefined) => DestructionStrings
1086
1113
  interface CheckOptions {
1087
1114
  /** Ids of custom materials the impl uses besides the built-in `MATERIALS`. */
1088
1115
  materials?: string[];
1116
+ /** Opt-in cost measurement of `state()` and `describe()` (D8). */
1117
+ cost?: boolean | CostOptions;
1089
1118
  }
1090
1119
  /**
1091
1120
  * `checkDestructible` of `@adatechnology/game-contracts`, with the built-in `MATERIALS` (plus `options.materials`) as the known material ids.
@@ -1361,4 +1390,4 @@ declare const tensionCapacity: (dims: Vec3, o: {
1361
1390
  tensileMPa?: number;
1362
1391
  }) => number;
1363
1392
 
1364
- export { ASSEMBLIES, type AddOptions, type AssemblyDef, type AssemblyPartOptions, type BearingWallOptions, type BendGeometry, type BendSection, CONFINEMENT_DEFAULTS, type ChainTrigger, type CheckOptions, type DestructionLocale, type DestructionOptions, type DestructionSnapshot, type DestructionStrings, type DestructionWorld, type ExplosivePartOptions, FIRE_PRIME_C, FLEXURE_DEFAULTS, FRAGMENT_HIT_DEFAULTS, type FlexureOptions, type FoundationSolve, type FrameOptions, GRAVITY, type Gust, JOINT_BY_MATERIALS, JOINT_DEFAULTS, MASONRY_DEFAULTS, MATERIALS, type MasonryOptions, type MaterialDef, type MaterialHit, type MaterialStrength, type MaterialThermal, type ObjectRuntime, type PartRuntime, type ResolvedWind, SHELTER_DEFAULTS, SOILS, SOIL_DEFAULTS, type SectionMoments, type SizeStructureOptions, type SoilDef, type SoilKind, type SoilOptions, type SoilSpring, type SwayState, type SwaySystem, type TensionSteel, WIND_DEFAULTS, WIND_TERRAIN, type WallFrameSolution, type WindAnalysis, type WindCase, type WindOptions, type WindPartInput, type WindTerrainDef, type WorldExplosive, analyzeWind, assemblyDepth, assemblyPart, bearingWallThickness, bendGeometry, blastImpulse, buildFrame, checkDestructible, crackingMoment, createDestruction, damperAmplification, damperTuning, elasticEnergy, explosivePart, exposureOf, footingArea, footingSoil, fragility, getAssembly, guessMaterial, gustAt, heightFactor, impulseEnergy, inferJoints, layersBetween, locales, makeGust, meanHeightFactor, momentNodes, momentOfEnergy, partCapacity, partMass, partThickness, partVolume, pileCapacity, pileLength, pileStiffness, profileOf, quat, quatFromEuler, registerAssembly, resolveStrings, resolveWind, rotateVec, sectionMoments, shapeAt, shearNodes, sizeRebar, sizeStructure, slendernessFactor, soilAt, solveFoundation, solveWallFrame, spanMoment, spanOf, steelMoment, stepSway, stiffnessCentre, subgradeModulus, tBeamInertia, tensionCapacity, tensionSteel, toLocal, toWorld, turbulenceIntensity, vec, windClass, windPressure };
1393
+ export { ASSEMBLIES, type AddOptions, type Assemblies, type AssembliesOptions, type AssemblyDef, type AssemblyPartOptions, type BearingWallOptions, type BendGeometry, type BendSection, CONFINEMENT_DEFAULTS, type ChainTrigger, type CheckOptions, type DestructionLocale, type DestructionOptions, type DestructionSnapshot, type DestructionStrings, type DestructionWorld, type ExplosivePartOptions, FIRE_PRIME_C, FLEXURE_DEFAULTS, FRAGMENT_HIT_DEFAULTS, type FlexureOptions, type FoundationSolve, type FrameOptions, GRAVITY, type Gust, JOINT_BY_MATERIALS, JOINT_DEFAULTS, MASONRY_DEFAULTS, MATERIALS, type MasonryOptions, type MaterialDef, type MaterialHit, type MaterialStrength, type MaterialThermal, type ObjectRuntime, type PartRuntime, type ResolvedWind, SHELTER_DEFAULTS, SOILS, SOIL_DEFAULTS, type SectionMoments, type SizeStructureOptions, type SoilDef, type SoilKind, type SoilOptions, type SoilSpring, type SwayState, type SwaySystem, type TensionSteel, WIND_DEFAULTS, WIND_TERRAIN, type WallFrameSolution, type WindAnalysis, type WindCase, type WindOptions, type WindPartInput, type WindTerrainDef, type WorldExplosive, analyzeWind, assemblyDepth, assemblyPart, bearingWallThickness, bendGeometry, blastImpulse, buildFrame, checkDestructible, crackingMoment, createAssemblies, createDestruction, damperAmplification, damperTuning, defaultAssemblies, elasticEnergy, explosivePart, exposureOf, footingArea, footingSoil, fragility, getAssembly, guessMaterial, gustAt, heightFactor, impulseEnergy, inferJoints, layersBetween, locales, makeGust, meanHeightFactor, momentNodes, momentOfEnergy, partCapacity, partMass, partThickness, partVolume, pileCapacity, pileLength, pileStiffness, profileOf, quat, quatFromEuler, registerAssembly, resolveStrings, resolveWind, rotateVec, sectionMoments, shapeAt, shearNodes, sizeRebar, sizeStructure, slendernessFactor, soilAt, solveFoundation, solveWallFrame, spanMoment, spanOf, steelMoment, stepSway, stiffnessCentre, subgradeModulus, tBeamInertia, tensionCapacity, tensionSteel, toLocal, toWorld, turbulenceIntensity, vec, windClass, windPressure };
@@ -47,9 +47,11 @@ var GameDestruction = (() => {
47
47
  checkDestructible: () => checkDestructible2,
48
48
  checkHit: () => checkHit,
49
49
  crackingMoment: () => crackingMoment,
50
+ createAssemblies: () => createAssemblies,
50
51
  createDestruction: () => createDestruction,
51
52
  damperAmplification: () => damperAmplification,
52
53
  damperTuning: () => damperTuning,
54
+ defaultAssemblies: () => defaultAssemblies,
53
55
  elasticEnergy: () => elasticEnergy,
54
56
  explosivePart: () => explosivePart,
55
57
  exposureOf: () => exposureOf,
@@ -2780,6 +2782,28 @@ var GameDestruction = (() => {
2780
2782
  };
2781
2783
 
2782
2784
  // ../game-contracts/dist/index.js
2785
+ var DEFAULT_COST_READS = 1e4;
2786
+ var DEFAULT_COST_CEILING_MS = 50;
2787
+ var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
2788
+ var heapUsedBytes = () => {
2789
+ const nodeProcess = globalThis.process;
2790
+ return nodeProcess?.memoryUsage?.().heapUsed;
2791
+ };
2792
+ var checkReadCost = ({ name, read, options, problems }) => {
2793
+ const reads = options.reads ?? DEFAULT_COST_READS;
2794
+ const ceilingMs = options.ceilingMs ?? DEFAULT_COST_CEILING_MS;
2795
+ const allocationBytes = options.allocationBytes ?? DEFAULT_COST_ALLOCATION_BYTES;
2796
+ if (read() !== read()) problems.push(`${name}() returns a new object on each read: cache it between changes (cost contract)`);
2797
+ for (let i = 0; i < reads; i++) read();
2798
+ const heapBefore = heapUsedBytes();
2799
+ const started = performance.now();
2800
+ for (let i = 0; i < reads; i++) read();
2801
+ const elapsedMs = performance.now() - started;
2802
+ const heapAfter = heapUsedBytes();
2803
+ if (elapsedMs > ceilingMs) problems.push(`${name}() took ${elapsedMs.toFixed(1)} ms for ${reads} reads, over the ${ceilingMs} ms ceiling (cost contract)`);
2804
+ if (heapBefore !== void 0 && heapAfter !== void 0 && heapAfter - heapBefore > allocationBytes)
2805
+ problems.push(`${name}() grew the heap by ${heapAfter - heapBefore} bytes over ${reads} reads, over the ${allocationBytes} byte ceiling (cost contract)`);
2806
+ };
2783
2807
  var IMPACT_CONTRACT = {
2784
2808
  ballistic: { required: ["type", "point", "direction", "energy", "caliber"], optional: ["space", "source"], note: "bullets and pellets: joules and diameter in millimetres" },
2785
2809
  blunt: { required: ["type", "point", "energy"], optional: ["direction", "radius", "space", "source"], note: "contact radius in metres (default 3 cm)" },
@@ -2885,6 +2909,15 @@ var GameDestruction = (() => {
2885
2909
  } catch (e) {
2886
2910
  problems.push(`reset() threw: ${String(e)}`);
2887
2911
  }
2912
+ if (options.cost) {
2913
+ const cost = options.cost === true ? {} : options.cost;
2914
+ try {
2915
+ checkReadCost({ name: "state", read: () => impl.state(), options: cost, problems });
2916
+ checkReadCost({ name: "describe", read: () => impl.describe(), options: cost, problems });
2917
+ } catch (e) {
2918
+ problems.push(`cost measurement threw: ${String(e)}`);
2919
+ }
2920
+ }
2888
2921
  return { ok: problems.length === 0, problems };
2889
2922
  };
2890
2923
  var DEFAULT_IGNITION_RULES = {
@@ -2930,6 +2963,103 @@ var GameDestruction = (() => {
2930
2963
  const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k));
2931
2964
  return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
2932
2965
  };
2966
+ var DEFERRED_QUEUE_ID = {
2967
+ DESTRUCTION_EVENTS: "destruction.events",
2968
+ BREACH_PAINT: "floorplan.breach-paint",
2969
+ AUDIO_VOICES: "audio.voices",
2970
+ VEHICLE_CREASE: "vehicles.crease"
2971
+ };
2972
+ var DEFERRED_PRIORITY = {
2973
+ [DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
2974
+ [DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
2975
+ [DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
2976
+ [DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
2977
+ };
2978
+ var WARNED_KEY = /* @__PURE__ */ Symbol.for("@adatechnology/game-contracts.warned");
2979
+ var warnedKeys = () => {
2980
+ const holder = globalThis;
2981
+ return holder[WARNED_KEY] ?? (holder[WARNED_KEY] = /* @__PURE__ */ new Set());
2982
+ };
2983
+ var isProduction = () => {
2984
+ try {
2985
+ return process.env.NODE_ENV === "production";
2986
+ } catch {
2987
+ return false;
2988
+ }
2989
+ };
2990
+ var isQuiet = () => globalThis.__GAME_KIT_QUIET__ === true;
2991
+ var warnOnce = (key, message) => {
2992
+ if (isQuiet() || isProduction()) return;
2993
+ const seen = warnedKeys();
2994
+ if (seen.has(key)) return;
2995
+ seen.add(key);
2996
+ console.warn(`[game-kit] ${message}`);
2997
+ };
2998
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2999
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
3000
+ var deepMerge = (base, patch) => {
3001
+ if (!patch) return base;
3002
+ const result = { ...base };
3003
+ for (const [key, value] of Object.entries(patch)) {
3004
+ if (value === void 0 || UNSAFE_KEYS.has(key)) continue;
3005
+ const current = result[key];
3006
+ result[key] = isPlainObject(current) && isPlainObject(value) ? deepMerge(current, value) : value;
3007
+ }
3008
+ return result;
3009
+ };
3010
+ var mergeById = (shipped, extra) => {
3011
+ const entries = /* @__PURE__ */ new Map();
3012
+ for (const entry of [...shipped, ...extra ?? []]) entries.set(entry.id, entry);
3013
+ return entries;
3014
+ };
3015
+ var applyPatches = (entries, patches) => {
3016
+ for (const [id, patch] of Object.entries(patches ?? {})) {
3017
+ const entry = entries.get(id);
3018
+ if (entry) entries.set(id, deepMerge(entry, patch));
3019
+ else warnOnce(`registry.patch.${id}`, `patch for "${id}" ignored: no entry has that id`);
3020
+ }
3021
+ };
3022
+ var removeIds = (entries, ids) => {
3023
+ for (const id of ids ?? []) {
3024
+ if (!entries.delete(id)) warnOnce(`registry.remove.${id}`, `remove of "${id}" ignored: no entry has that id`);
3025
+ }
3026
+ };
3027
+ var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
3028
+ var GAIT_IDS = ["walk", "run"];
3029
+ var LANDING_IDS = ["light", "heavy"];
3030
+ var BODY_SOUND_IDS = [
3031
+ "jump",
3032
+ "cloth_rustle",
3033
+ "body_fall",
3034
+ "fall_damage",
3035
+ "player_hurt",
3036
+ "player_breath_run",
3037
+ "player_death",
3038
+ "bite",
3039
+ "grab",
3040
+ "punch_swing",
3041
+ "punch_hit_body",
3042
+ "punch_hit_face",
3043
+ "punch_hit_object",
3044
+ "kick_swing",
3045
+ "kick_hit",
3046
+ "kick_hit_object",
3047
+ "shove_two_hands",
3048
+ "knife_swing",
3049
+ "knife_stab",
3050
+ "knife_slash",
3051
+ "knife_hit_object",
3052
+ "blunt_swing",
3053
+ "blunt_hit",
3054
+ "blunt_hit_object",
3055
+ "axe_swing",
3056
+ "axe_chop",
3057
+ "axe_hit_object"
3058
+ ];
3059
+ var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
3060
+ var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
3061
+ var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing) => `land_${surface}_${landing}`));
3062
+ var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
2933
3063
 
2934
3064
  // src/world.ts
2935
3065
  var THIN_SHEET_DISHING = 5e-3;
@@ -3057,6 +3187,7 @@ var GameDestruction = (() => {
3057
3187
  const ignitionRules = resolveIgnitionRules(options.ignition);
3058
3188
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
3059
3189
  const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
3190
+ const chainLinkCap = options.chainLinksPerUpdate !== void 0 && options.chainLinksPerUpdate >= 1 ? Math.floor(options.chainLinksPerUpdate) : Infinity;
3060
3191
  const rng = createRng(options.seed ?? 1);
3061
3192
  const rnd = () => rng.next();
3062
3193
  let time = 0;
@@ -4004,7 +4135,7 @@ var GameDestruction = (() => {
4004
4135
  if (disc < 0) return true;
4005
4136
  return -b - Math.sqrt(disc) > maxDist;
4006
4137
  };
4007
- const raycastInternal = (origin, dir, maxDist, ignore, worldOnly = false) => {
4138
+ const raycastInternal = (origin, dir, maxDist, ignore, worldOnly = false, precise = false) => {
4008
4139
  const d = vec.normalize(dir);
4009
4140
  let best = null;
4010
4141
  for (const o of objects.values()) {
@@ -4019,13 +4150,25 @@ var GameDestruction = (() => {
4019
4150
  }
4020
4151
  }
4021
4152
  for (const im of worldOnly ? [] : impls.values()) {
4153
+ let boxHit = null;
4022
4154
  for (const ps of im.impl.describe().parts) {
4023
4155
  if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
4024
4156
  const ov = im.poses.get(ps.id);
4025
4157
  const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), origin, d, ov?.pos);
4026
- if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
4027
- if (!best || r.t0 < best.distance) best = { object: im.impl.id, part: ps.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
4158
+ if (!r || r.t1 < 0 || r.t0 > maxDist) continue;
4159
+ if (r.t0 < 0) {
4160
+ if (precise && im.impl.surfaceRay) boxHit = boxHit ?? { object: im.impl.id, part: ps.id, point: origin, normal: r.n, distance: 0, pathLen: r.t1 };
4161
+ continue;
4162
+ }
4163
+ if (!boxHit || r.t0 < boxHit.distance) boxHit = { object: im.impl.id, part: ps.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
4028
4164
  }
4165
+ if (!boxHit) continue;
4166
+ if (precise && im.impl.surfaceRay) {
4167
+ const far = best ? Math.min(maxDist, best.distance) : maxDist;
4168
+ const implId = im.impl.id;
4169
+ const s = im.impl.surfaceRay({ origin, direction: d, maxDistance: far, ...ignore ? { ignored: (part) => ignore.has(`${implId}\0${part}`) } : {} });
4170
+ if (s && (!best || s.distance < best.distance)) best = { object: implId, part: s.part, point: s.point, normal: s.normal, distance: s.distance, pathLen: s.pathLen };
4171
+ } else if (!best || boxHit.distance < best.distance) best = boxHit;
4029
4172
  }
4030
4173
  return best;
4031
4174
  };
@@ -5509,7 +5652,9 @@ var GameDestruction = (() => {
5509
5652
  };
5510
5653
  const detonatePrimed = (until) => {
5511
5654
  const end = time;
5512
- for (let next = nextPrimed(until); next; next = nextPrimed(until)) {
5655
+ let fired = 0;
5656
+ for (let next = nextPrimed(until); next && fired < chainLinkCap; next = nextPrimed(until)) {
5657
+ fired++;
5513
5658
  primed.delete(next.p);
5514
5659
  time = Math.max(0, Math.min(end, next.p.rt.chainDue));
5515
5660
  next.p.rt.chainDue = void 0;
@@ -5579,6 +5724,7 @@ var GameDestruction = (() => {
5579
5724
  updateBound(o);
5580
5725
  },
5581
5726
  remove(id) {
5727
+ impls.get(id)?.impl.dispose?.();
5582
5728
  objects.delete(id);
5583
5729
  impls.delete(id);
5584
5730
  },
@@ -5654,7 +5800,7 @@ var GameDestruction = (() => {
5654
5800
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
5655
5801
  },
5656
5802
  raycast(origin, direction, maxDistance = Infinity) {
5657
- const r = raycastInternal(origin, direction, maxDistance);
5803
+ const r = raycastInternal(origin, direction, maxDistance, void 0, false, true);
5658
5804
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
5659
5805
  },
5660
5806
  sweepBox(box, direction, distance, options2 = {}) {
@@ -5679,11 +5825,11 @@ var GameDestruction = (() => {
5679
5825
  let org = origin;
5680
5826
  let E = energy;
5681
5827
  for (let i = 0; i < 4 && E > 0; i++) {
5682
- const rh = raycastInternal(org, d, Infinity, ignore);
5828
+ const rh = raycastInternal(org, d, Infinity, ignore, false, true);
5683
5829
  if (!rh) break;
5684
5830
  const im = impls.get(rh.object);
5685
5831
  if (im) {
5686
- const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...extras, ...source !== void 0 ? { source } : {} });
5832
+ const r = implHit(im, { type: "ballistic", point: rh.point, part: rh.part, normal: rh.normal, direction: d, energy: E, caliber, ...extras, ...source !== void 0 ? { source } : {} });
5687
5833
  r.part = rh.part;
5688
5834
  r.point = rh.point;
5689
5835
  r.exitEnergy = r.penetrated ? E * 0.5 : 0;
@@ -6009,17 +6155,34 @@ var GameDestruction = (() => {
6009
6155
  ];
6010
6156
 
6011
6157
  // src/assemblies.ts
6158
+ var SHIPPED = structuredClone(assemblies_default);
6012
6159
  var ASSEMBLIES = assemblies_default;
6013
- var registerAssembly = (def) => {
6014
- const at = ASSEMBLIES.findIndex((a) => a.id === def.id);
6015
- if (at >= 0) ASSEMBLIES[at] = def;
6016
- else ASSEMBLIES.push(def);
6160
+ var assembliesOver = (defs) => {
6161
+ const get = (id) => defs.find((a) => a.id === id);
6162
+ return {
6163
+ list: () => defs,
6164
+ get,
6165
+ register(def) {
6166
+ const at = defs.findIndex((a) => a.id === def.id);
6167
+ if (at >= 0) defs[at] = def;
6168
+ else defs.push(def);
6169
+ },
6170
+ part: (assemblyId, options) => buildAssemblyPart(get(assemblyId), assemblyId, options)
6171
+ };
6172
+ };
6173
+ var createAssemblies = (options = {}) => {
6174
+ const entries = mergeById(SHIPPED.map((def) => structuredClone(def)), (options.packs ?? []).flat());
6175
+ applyPatches(entries, options.patch);
6176
+ removeIds(entries, options.remove);
6177
+ return assembliesOver([...entries.values()]);
6017
6178
  };
6018
- var getAssembly = (id) => ASSEMBLIES.find((a) => a.id === id);
6179
+ var defaultSet;
6180
+ var defaultAssemblies = () => defaultSet ?? (defaultSet = assembliesOver(ASSEMBLIES));
6181
+ var registerAssembly = (def) => defaultAssemblies().register(def);
6182
+ var getAssembly = (id) => defaultAssemblies().get(id);
6019
6183
  var faceCount = (layer) => layer.faces === "both" ? 2 : 1;
6020
6184
  var assemblyDepth = (def) => def.depth ?? def.core.thickness + (def.layers ?? []).reduce((sum, l) => sum + l.thickness * faceCount(l), 0);
6021
- var assemblyPart = (assemblyId, options) => {
6022
- const def = getAssembly(assemblyId);
6185
+ var buildAssemblyPart = (def, assemblyId, options) => {
6023
6186
  if (!def) throw new Error(`unknown assembly "${assemblyId}"`);
6024
6187
  const densityOf = (id) => {
6025
6188
  const m = options.materials?.get(id) ?? MATERIALS.find((x) => x.id === id);
@@ -6046,6 +6209,7 @@ var GameDestruction = (() => {
6046
6209
  ...options.extra
6047
6210
  };
6048
6211
  };
6212
+ var assemblyPart = (assemblyId, options) => defaultAssemblies().part(assemblyId, options);
6049
6213
 
6050
6214
  // src/frame.ts
6051
6215
  var buildFrame = (o = {}) => {
@@ -6483,7 +6647,7 @@ var GameDestruction = (() => {
6483
6647
 
6484
6648
  // src/contracts.ts
6485
6649
  var checkDestructible2 = (impl, options = {}) => {
6486
- const contractOptions = { materials: [...MATERIALS.map((m) => m.id), ...options.materials ?? []] };
6650
+ const contractOptions = { materials: [...MATERIALS.map((m) => m.id), ...options.materials ?? []], ...options.cost === void 0 ? {} : { cost: options.cost } };
6487
6651
  return checkDestructible(impl, contractOptions);
6488
6652
  };
6489
6653
  return __toCommonJS(index_exports);