@adatechnology/game-destruction 0.13.2 → 0.15.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, ShelterOptions, RayHit, BallisticExtras, ToolKind, JointState, PartState, DestructionEventType, ConfinementOptions, FragmentHitOptions, 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
  /**
@@ -452,6 +457,16 @@ interface DestructionWorld {
452
457
  * that target, and a `blast` event sums it up.
453
458
  */
454
459
  explode(point: Vec3, energy: number, radius: number, source?: string, options?: BlastOptions): HitResult[];
460
+ /**
461
+ * The share (0..1) of a blast at `point` that reaches `target` through what stands between them: every part the straight line crosses lets
462
+ * `exp(-r / k)` through (`ShelterOptions`; `r` the hit points of the slab per square metre), a broken part or a hole lets it all by. Always on, whatever
463
+ * `BlastOptions.shelter` says: the shove of a blast (people, the player, loose things) is blocked by cover in every room. `ignore` lists objects that never
464
+ * block (the explosive itself, the body being shoved).
465
+ */
466
+ blastReach(point: Vec3, target: Vec3, options?: {
467
+ readonly ignore?: readonly string[];
468
+ readonly shelter?: ShelterOptions;
469
+ }): number;
455
470
  /** Cost of the last `explode`: parts in range of the broad phase (`partsTested`) and those whose bounding sphere the blast reached (`partsNear`). */
456
471
  blastProbe(): {
457
472
  partsTested: number;
@@ -711,16 +726,38 @@ interface AssemblyPartOptions {
711
726
  /** Materials to read densities from (default the built-in `MATERIALS`). */
712
727
  materials?: ReadonlyMap<MaterialId, MaterialDef>;
713
728
  }
714
- /** Built-in assemblies; `registerAssembly` adds to this list. */
729
+ /**
730
+ * Built-in assemblies; `registerAssembly` adds to this list.
731
+ * @deprecated the list every page shares. Use `createAssemblies({ packs, patch, remove })`, or `defaultAssemblies()` for this one.
732
+ */
715
733
  declare const ASSEMBLIES: AssemblyDef[];
734
+ interface AssembliesOptions {
735
+ /** Assemblies added to the built-in ones; one with the id of a built-in replaces it. */
736
+ readonly packs?: readonly (readonly AssemblyDef[])[];
737
+ /** Changes by id, merged field by field over what is there. */
738
+ readonly patch?: Readonly<Record<string, DeepPartial<AssemblyDef>>>;
739
+ /** Ids taken out. */
740
+ readonly remove?: readonly string[];
741
+ }
742
+ interface Assemblies {
743
+ list(): readonly AssemblyDef[];
744
+ get(id: string): AssemblyDef | undefined;
745
+ /** Adds an assembly, or replaces the one with the same id. */
746
+ register(def: AssemblyDef): void;
747
+ /** The wall as a `PartSpec`; throws for an id this set does not have. */
748
+ part(assemblyId: string, options: AssemblyPartOptions): PartSpec;
749
+ }
750
+ /** 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. */
751
+ declare const createAssemblies: (options?: AssembliesOptions) => Assemblies;
752
+ /** The set behind `ASSEMBLIES`, `registerAssembly`, `getAssembly` and `assemblyPart`, created on first use. */
753
+ declare const defaultAssemblies: () => Assemblies;
754
+ /** @deprecated use `createAssemblies(...).register`. */
716
755
  declare const registerAssembly: (def: AssemblyDef) => void;
756
+ /** @deprecated use `createAssemblies(...).get`. */
717
757
  declare const getAssembly: (id: string) => AssemblyDef | undefined;
718
758
  /** Physical depth of the wall in metres. */
719
759
  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
- */
760
+ /** @deprecated use `createAssemblies(...).part`. */
724
761
  declare const assemblyPart: (assemblyId: string, options: AssemblyPartOptions) => PartSpec;
725
762
 
726
763
  interface FrameOptions {
@@ -1086,6 +1123,8 @@ declare const resolveStrings: (locale: string | undefined) => DestructionStrings
1086
1123
  interface CheckOptions {
1087
1124
  /** Ids of custom materials the impl uses besides the built-in `MATERIALS`. */
1088
1125
  materials?: string[];
1126
+ /** Opt-in cost measurement of `state()` and `describe()` (D8). */
1127
+ cost?: boolean | CostOptions;
1089
1128
  }
1090
1129
  /**
1091
1130
  * `checkDestructible` of `@adatechnology/game-contracts`, with the built-in `MATERIALS` (plus `options.materials`) as the known material ids.
@@ -1361,4 +1400,4 @@ declare const tensionCapacity: (dims: Vec3, o: {
1361
1400
  tensileMPa?: number;
1362
1401
  }) => number;
1363
1402
 
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 };
1403
+ 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;
@@ -3079,7 +3210,13 @@ var GameDestruction = (() => {
3079
3210
  const typed = listeners.get(e.type);
3080
3211
  if (typed) for (const fn of [...typed]) fn(e);
3081
3212
  };
3082
- const base = (o, source) => source === void 0 ? { object: o.id, time } : { object: o.id, time, source };
3213
+ let blow;
3214
+ const base = (o, source) => ({
3215
+ object: o.id,
3216
+ time,
3217
+ ...source === void 0 ? {} : { source },
3218
+ ...blow === void 0 ? {} : { blow }
3219
+ });
3083
3220
  const pendingCollapse = /* @__PURE__ */ new Map();
3084
3221
  let runDepth = 0;
3085
3222
  const drainBlasts = () => {
@@ -4004,7 +4141,7 @@ var GameDestruction = (() => {
4004
4141
  if (disc < 0) return true;
4005
4142
  return -b - Math.sqrt(disc) > maxDist;
4006
4143
  };
4007
- const raycastInternal = (origin, dir, maxDist, ignore, worldOnly = false) => {
4144
+ const raycastInternal = (origin, dir, maxDist, ignore, worldOnly = false, precise = false) => {
4008
4145
  const d = vec.normalize(dir);
4009
4146
  let best = null;
4010
4147
  for (const o of objects.values()) {
@@ -4019,13 +4156,25 @@ var GameDestruction = (() => {
4019
4156
  }
4020
4157
  }
4021
4158
  for (const im of worldOnly ? [] : impls.values()) {
4159
+ let boxHit = null;
4022
4160
  for (const ps of im.impl.describe().parts) {
4023
4161
  if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
4024
4162
  const ov = im.poses.get(ps.id);
4025
4163
  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 };
4164
+ if (!r || r.t1 < 0 || r.t0 > maxDist) continue;
4165
+ if (r.t0 < 0) {
4166
+ if (precise && im.impl.surfaceRay) boxHit = boxHit ?? { object: im.impl.id, part: ps.id, point: origin, normal: r.n, distance: 0, pathLen: r.t1 };
4167
+ continue;
4168
+ }
4169
+ 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
4170
  }
4171
+ if (!boxHit) continue;
4172
+ if (precise && im.impl.surfaceRay) {
4173
+ const far = best ? Math.min(maxDist, best.distance) : maxDist;
4174
+ const implId = im.impl.id;
4175
+ const s = im.impl.surfaceRay({ origin, direction: d, maxDistance: far, ...ignore ? { ignored: (part) => ignore.has(`${implId}\0${part}`) } : {} });
4176
+ 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 };
4177
+ } else if (!best || boxHit.distance < best.distance) best = boxHit;
4029
4178
  }
4030
4179
  return best;
4031
4180
  };
@@ -5212,7 +5361,17 @@ var GameDestruction = (() => {
5212
5361
  res.events = cur.slice(start);
5213
5362
  return res;
5214
5363
  };
5215
- const doHit = (o, hit, only) => {
5364
+ const withBlow = (type, fn) => () => {
5365
+ const outer = blow;
5366
+ blow = type;
5367
+ try {
5368
+ return fn();
5369
+ } finally {
5370
+ blow = outer;
5371
+ }
5372
+ };
5373
+ const doHit = (o, hit, only) => withBlow(hit.type, () => resolveHit(o, hit, only))();
5374
+ const resolveHit = (o, hit, only) => {
5216
5375
  let point = hit.point;
5217
5376
  let dir = hit.direction;
5218
5377
  if (hit.space === "local") {
@@ -5509,7 +5668,9 @@ var GameDestruction = (() => {
5509
5668
  };
5510
5669
  const detonatePrimed = (until) => {
5511
5670
  const end = time;
5512
- for (let next = nextPrimed(until); next; next = nextPrimed(until)) {
5671
+ let fired = 0;
5672
+ for (let next = nextPrimed(until); next && fired < chainLinkCap; next = nextPrimed(until)) {
5673
+ fired++;
5513
5674
  primed.delete(next.p);
5514
5675
  time = Math.max(0, Math.min(end, next.p.rt.chainDue));
5515
5676
  next.p.rt.chainDue = void 0;
@@ -5579,6 +5740,7 @@ var GameDestruction = (() => {
5579
5740
  updateBound(o);
5580
5741
  },
5581
5742
  remove(id) {
5743
+ impls.get(id)?.impl.dispose?.();
5582
5744
  objects.delete(id);
5583
5745
  impls.delete(id);
5584
5746
  },
@@ -5648,13 +5810,51 @@ var GameDestruction = (() => {
5648
5810
  blastProbe() {
5649
5811
  return { ...blastProbe };
5650
5812
  },
5813
+ blastReach(point, target, options2 = {}) {
5814
+ const L = vec.distance(point, target);
5815
+ if (L < 1e-4) return 1;
5816
+ const shelter = { ...SHELTER_DEFAULTS, ...options2.shelter };
5817
+ const ignore = new Set(options2.ignore ?? []);
5818
+ const d = vec.scale(vec.sub(target, point), 1 / L);
5819
+ let t = 1;
5820
+ for (const o of objects.values()) {
5821
+ if (ignore.has(o.id) || raySphereMiss(point, d, toWorld(o.st.position, o.st.rotation, o.bc), o.br, L)) continue;
5822
+ for (const p of o.parts) {
5823
+ if (isLost(p) || p.st.stripped) continue;
5824
+ const r = rayPart(o, p, point, d);
5825
+ if (!r) continue;
5826
+ const t0 = Math.max(r.t0, 0);
5827
+ const path = Math.min(r.t1, L) - t0;
5828
+ if (path < shelter.minPath) continue;
5829
+ if (p.st.opening && insideOpening(o, p, toLocal(o.st.position, o.st.rotation, vec.madd(point, d, t0 + path / 2)))) continue;
5830
+ t *= Math.exp(-(p.mat.hpPerKg * p.mat.density * path * (p.mat.damageScale?.shelter ?? 1)) / shelter.k);
5831
+ if (t < 1e-4) return 0;
5832
+ }
5833
+ }
5834
+ for (const im of impls.values()) {
5835
+ if (ignore.has(im.impl.id)) continue;
5836
+ for (const ps of im.impl.describe().parts) {
5837
+ const mat = materials.get(ps.material);
5838
+ if (!mat) continue;
5839
+ const ov = im.poses.get(ps.id);
5840
+ const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), point, d, ov?.pos);
5841
+ if (!r || r.t1 < 0 || r.t0 > L) continue;
5842
+ let path = Math.min(r.t1, L) - Math.max(r.t0, 0);
5843
+ if (ps.thickness !== void 0 && ps.thickness > 0) path = Math.min(path, 2 * ps.thickness);
5844
+ else if (path < shelter.minPath) continue;
5845
+ t *= Math.exp(-(mat.hpPerKg * mat.density * path * (mat.damageScale?.shelter ?? 1)) / shelter.k);
5846
+ if (t < 1e-4) return 0;
5847
+ }
5848
+ }
5849
+ return t;
5850
+ },
5651
5851
  explode(point, energy, radius, source, blast) {
5652
5852
  blastProbe.partsTested = 0;
5653
5853
  blastProbe.partsNear = 0;
5654
5854
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
5655
5855
  },
5656
5856
  raycast(origin, direction, maxDistance = Infinity) {
5657
- const r = raycastInternal(origin, direction, maxDistance);
5857
+ const r = raycastInternal(origin, direction, maxDistance, void 0, false, true);
5658
5858
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
5659
5859
  },
5660
5860
  sweepBox(box, direction, distance, options2 = {}) {
@@ -5672,21 +5872,23 @@ var GameDestruction = (() => {
5672
5872
  },
5673
5873
  shoot(origin, direction, energy, caliber, source, round) {
5674
5874
  const extras = typeof round === "number" ? { piercing: round } : round ?? {};
5675
- return withCollapses(run(() => {
5875
+ return withCollapses(run(withBlow("ballistic", () => {
5676
5876
  const results = [];
5677
5877
  const d = vec.normalize(direction);
5678
5878
  const ignore = /* @__PURE__ */ new Set();
5679
5879
  let org = origin;
5680
5880
  let E = energy;
5681
5881
  for (let i = 0; i < 4 && E > 0; i++) {
5682
- const rh = raycastInternal(org, d, Infinity, ignore);
5882
+ const rh = raycastInternal(org, d, Infinity, ignore, false, true);
5683
5883
  if (!rh) break;
5684
5884
  const im = impls.get(rh.object);
5685
5885
  if (im) {
5686
- const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...extras, ...source !== void 0 ? { source } : {} });
5886
+ const scaled = extras.penetration && E < energy ? { ...extras, penetration: Object.fromEntries(Object.entries(extras.penetration).map(([k, mm]) => [k, mm * (E / energy)])) } : extras;
5887
+ const r = implHit(im, { type: "ballistic", point: rh.point, part: rh.part, normal: rh.normal, direction: d, energy: E, caliber, ...scaled, ...source !== void 0 ? { source } : {} });
5687
5888
  r.part = rh.part;
5688
5889
  r.point = rh.point;
5689
- r.exitEnergy = r.penetrated ? E * 0.5 : 0;
5890
+ const left = r.events.find((e) => e.type === "hole" && e.energyLeft !== void 0);
5891
+ r.exitEnergy = r.penetrated ? left?.energyLeft ?? E * 0.5 : 0;
5690
5892
  results.push(r);
5691
5893
  ignore.add(`${im.impl.id}\0${rh.part}`);
5692
5894
  if (!r.penetrated) break;
@@ -5705,7 +5907,7 @@ var GameDestruction = (() => {
5705
5907
  E = out.exitEnergy;
5706
5908
  }
5707
5909
  return results;
5708
- }));
5910
+ })));
5709
5911
  },
5710
5912
  disassemble(objectId, jointId, tool, dt) {
5711
5913
  const im = impls.get(objectId);
@@ -6009,17 +6211,34 @@ var GameDestruction = (() => {
6009
6211
  ];
6010
6212
 
6011
6213
  // src/assemblies.ts
6214
+ var SHIPPED = structuredClone(assemblies_default);
6012
6215
  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);
6216
+ var assembliesOver = (defs) => {
6217
+ const get = (id) => defs.find((a) => a.id === id);
6218
+ return {
6219
+ list: () => defs,
6220
+ get,
6221
+ register(def) {
6222
+ const at = defs.findIndex((a) => a.id === def.id);
6223
+ if (at >= 0) defs[at] = def;
6224
+ else defs.push(def);
6225
+ },
6226
+ part: (assemblyId, options) => buildAssemblyPart(get(assemblyId), assemblyId, options)
6227
+ };
6228
+ };
6229
+ var createAssemblies = (options = {}) => {
6230
+ const entries = mergeById(SHIPPED.map((def) => structuredClone(def)), (options.packs ?? []).flat());
6231
+ applyPatches(entries, options.patch);
6232
+ removeIds(entries, options.remove);
6233
+ return assembliesOver([...entries.values()]);
6017
6234
  };
6018
- var getAssembly = (id) => ASSEMBLIES.find((a) => a.id === id);
6235
+ var defaultSet;
6236
+ var defaultAssemblies = () => defaultSet ?? (defaultSet = assembliesOver(ASSEMBLIES));
6237
+ var registerAssembly = (def) => defaultAssemblies().register(def);
6238
+ var getAssembly = (id) => defaultAssemblies().get(id);
6019
6239
  var faceCount = (layer) => layer.faces === "both" ? 2 : 1;
6020
6240
  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);
6241
+ var buildAssemblyPart = (def, assemblyId, options) => {
6023
6242
  if (!def) throw new Error(`unknown assembly "${assemblyId}"`);
6024
6243
  const densityOf = (id) => {
6025
6244
  const m = options.materials?.get(id) ?? MATERIALS.find((x) => x.id === id);
@@ -6046,6 +6265,7 @@ var GameDestruction = (() => {
6046
6265
  ...options.extra
6047
6266
  };
6048
6267
  };
6268
+ var assemblyPart = (assemblyId, options) => defaultAssemblies().part(assemblyId, options);
6049
6269
 
6050
6270
  // src/frame.ts
6051
6271
  var buildFrame = (o = {}) => {
@@ -6483,7 +6703,7 @@ var GameDestruction = (() => {
6483
6703
 
6484
6704
  // src/contracts.ts
6485
6705
  var checkDestructible2 = (impl, options = {}) => {
6486
- const contractOptions = { materials: [...MATERIALS.map((m) => m.id), ...options.materials ?? []] };
6706
+ const contractOptions = { materials: [...MATERIALS.map((m) => m.id), ...options.materials ?? []], ...options.cost === void 0 ? {} : { cost: options.cost } };
6487
6707
  return checkDestructible(impl, contractOptions);
6488
6708
  };
6489
6709
  return __toCommonJS(index_exports);