@adatechnology/game-destruction 0.3.1 → 0.4.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
@@ -94,6 +94,15 @@ melts anything. Melting emits `melt` and `drip`; a melted part is lost and its j
94
94
  when the part has `explodes`). `contains` parts leak through punctures: the `leak` event carries the hole's world point, the outward normal as
95
95
  `direction` and the rate in litres per second, and `PartState.liters` drops in `update` (`setLiters` fills or empties a container).
96
96
 
97
+ ### Chain reaction
98
+
99
+ A part with `explodes.chain` (fuel barrel, gas bottle, car tank, gas pump, station tank) answers to what happens around it: `shot` (a bullet, once it has taken off
100
+ `shotDamageFraction` of the hit points), `fire` (the part reaches `explodes.atC`, 300 °C by default) and `explosion` (a blast reaches it). Each gives the seconds until the
101
+ blast, fixed or `[min, max]` drawn from the world seed. The world emits `primed` when it arms a part and `explode` when the delay ends; a bullet or a blast arms the part
102
+ instead of tearing it apart on the spot, and a part goes off once. Blasts resolve in order of time, then object, then part, and the delay counts from the blast that armed the
103
+ part, so the timeline does not depend on the `dt` you pass to `update`. `explosivePart(explosive, part)` builds the part from an `ExplosiveDef` of `@adatechnology/game-weapons`
104
+ (`explosives.get('fuel_barrel')`), so energy, reach and delays come from one place and a game changes them with `patch.explosives`.
105
+
97
106
  ## Joints and tools
98
107
 
99
108
  | Kind | Strength | Tools | Seconds |
@@ -177,7 +186,7 @@ valid (unique ids, known materials, joints reference parts), a 500 J ballistic h
177
186
  ## Events
178
187
 
179
188
  `damage`, `dent`, `hole`, `crack`, `ricochet`, `debris`, `fracture` (with oriented `fragments` in world space), `detach`, `disassemble_progress`,
180
- `ignite`, `extinguish`, `burnout`, `melt`, `drip`, `leak`, `explode`, `stress`, `collapse`, `destroyed`. All points are in world space. Subscribe with
189
+ `ignite`, `extinguish`, `burnout`, `melt`, `drip`, `leak`, `explode`, `primed`, `stress`, `collapse`, `destroyed`. All points are in world space. Subscribe with
181
190
  `world.on(type | '*', fn)` or the `onEvent` option; `hit` and `update` also return their events.
182
191
 
183
192
  ## Determinism and LAN
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Vec3, Quat, MaterialId, FractureStyle, DamageType, DebrisKind, DestructionEvent, DestructibleSpec, ObjectState, DestructibleImpl, Hit, HitResult, RayHit, ToolKind, JointState, PartState, DestructionEventType, JointKind, PartSpec, JointSpec, PartShape, PartStatus, StructureRole } from '@adatechnology/game-contracts';
2
- export { Crack, DamageType, DebrisKind, Dent, DestructibleImpl, DestructibleSpec, DestructionEvent, DestructionEventType, FluidKind, FractureStyle, Fragment, Hit, HitResult, Hole, IMPACT_CONTRACT, ImpactContract, JointKind, JointSpec, JointState, MaterialId, ObjectState, PartShape, PartSpec, PartState, PartStatus, Quat, RayHit, StructureRole, ToolKind, Vec3, checkHit } from '@adatechnology/game-contracts';
1
+ import { Vec3, Quat, MaterialId, FractureStyle, DamageType, DebrisKind, DestructionEvent, DestructibleSpec, ObjectState, DestructibleImpl, Hit, HitResult, RayHit, ToolKind, JointState, PartState, DestructionEventType, ExplosionChain, PartSpec, JointKind, JointSpec, PartShape, PartStatus, StructureRole } from '@adatechnology/game-contracts';
2
+ export { ChainDelay, Crack, DamageType, DebrisKind, Dent, DestructibleImpl, DestructibleSpec, DestructionEvent, DestructionEventType, ExplosionChain, FluidKind, FractureStyle, Fragment, Hit, HitResult, Hole, IMPACT_CONTRACT, ImpactContract, JointKind, JointSpec, JointState, MaterialId, ObjectState, PartShape, PartSpec, PartState, PartStatus, Quat, RayHit, StructureRole, ToolKind, Vec3, checkHit } from '@adatechnology/game-contracts';
3
3
 
4
4
  /**
5
5
  * Public types of @adatechnology/game-destruction.
@@ -204,6 +204,8 @@ interface PartRuntime {
204
204
  jet: boolean;
205
205
  /** Seconds left before the next `stress` event. */
206
206
  stressT?: number;
207
+ /** World time at which a primed explosive part goes off (`explodes.chain`). */
208
+ chainDue?: number;
207
209
  }
208
210
  /** Opaque bookkeeping of an object (see `PartRuntime`). `joints` holds the last disassembly step reported per joint id. */
209
211
  interface ObjectRuntime {
@@ -235,6 +237,27 @@ interface DestructionSnapshot {
235
237
 
236
238
  declare const createDestruction: (options?: DestructionOptions) => DestructionWorld;
237
239
 
240
+ /** What can set an explosive part off from outside, in the order a hit can carry it. */
241
+ type ChainTrigger = 'shot' | 'fire' | 'explosion';
242
+ /** Temperature in °C that primes a part with `chain.fire` when its `explodes.atC` is not set. */
243
+ declare const FIRE_PRIME_C = 300;
244
+ /** The numbers of an explosive that sits in the world, as `game-weapons` gives them (`ExplosiveDef` fits). */
245
+ interface WorldExplosive {
246
+ readonly energy: number;
247
+ /** Metres within which the blast reaches other parts (and primes theirs). */
248
+ readonly injuryRadius: number;
249
+ readonly chain?: ExplosionChain;
250
+ }
251
+ interface ExplosivePartOptions {
252
+ /** Temperature in °C at which fire primes (or sets off) the part; default `FIRE_PRIME_C` when the explosive answers to fire. */
253
+ readonly atC?: number;
254
+ }
255
+ /**
256
+ * A part that explodes with the energy and reach of `explosive` and answers to what `explosive.chain` says: a bullet, fire or a blast
257
+ * nearby primes it and it goes off after the delay. `part` is everything else about the part (shape, material, position, fluid inside).
258
+ */
259
+ declare const explosivePart: (explosive: WorldExplosive, part: Omit<PartSpec, "explodes">, options?: ExplosivePartOptions) => PartSpec;
260
+
238
261
  /** Built-in materials (data in `src/data/materials.json`). Override or extend with the `materials` option of `createDestruction`. */
239
262
  declare const MATERIALS: MaterialDef[];
240
263
  /** Default joint strength (joint hit points), tools that undo it and seconds of tool work. */
@@ -387,4 +410,4 @@ declare const checkDestructible: (impl: DestructibleImpl, options?: CheckOptions
387
410
  problems: string[];
388
411
  };
389
412
 
390
- export { type AddOptions, type CheckOptions, type DestructionLocale, type DestructionOptions, type DestructionSnapshot, type DestructionStrings, type DestructionWorld, type FrameOptions, JOINT_BY_MATERIALS, JOINT_DEFAULTS, MATERIALS, type MaterialDef, type MaterialThermal, type ObjectRuntime, type PartRuntime, type SizeStructureOptions, buildFrame, checkDestructible, createDestruction, fragility, guessMaterial, inferJoints, locales, partCapacity, partMass, partThickness, partVolume, quat, quatFromEuler, resolveStrings, rotateVec, sizeStructure, toLocal, toWorld, vec };
413
+ export { type AddOptions, type ChainTrigger, type CheckOptions, type DestructionLocale, type DestructionOptions, type DestructionSnapshot, type DestructionStrings, type DestructionWorld, type ExplosivePartOptions, FIRE_PRIME_C, type FrameOptions, JOINT_BY_MATERIALS, JOINT_DEFAULTS, MATERIALS, type MaterialDef, type MaterialThermal, type ObjectRuntime, type PartRuntime, type SizeStructureOptions, type WorldExplosive, buildFrame, checkDestructible, createDestruction, explosivePart, fragility, guessMaterial, inferJoints, locales, partCapacity, partMass, partThickness, partVolume, quat, quatFromEuler, resolveStrings, rotateVec, sizeStructure, toLocal, toWorld, vec };
@@ -21,6 +21,7 @@ var GameDestruction = (() => {
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ FIRE_PRIME_C: () => FIRE_PRIME_C,
24
25
  IMPACT_CONTRACT: () => IMPACT_CONTRACT,
25
26
  JOINT_BY_MATERIALS: () => JOINT_BY_MATERIALS,
26
27
  JOINT_DEFAULTS: () => JOINT_DEFAULTS,
@@ -29,6 +30,7 @@ var GameDestruction = (() => {
29
30
  checkDestructible: () => checkDestructible2,
30
31
  checkHit: () => checkHit,
31
32
  createDestruction: () => createDestruction,
33
+ explosivePart: () => explosivePart,
32
34
  fragility: () => fragility,
33
35
  guessMaterial: () => guessMaterial,
34
36
  inferJoints: () => inferJoints,
@@ -47,6 +49,25 @@ var GameDestruction = (() => {
47
49
  vec: () => vec
48
50
  });
49
51
 
52
+ // src/chain.ts
53
+ var FIRE_PRIME_C = 300;
54
+ var drawChainDelay = (delay, random) => {
55
+ if (typeof delay === "number") return delay;
56
+ const [min, max] = delay;
57
+ return min + random() * (max - min);
58
+ };
59
+ var chainDelayOf = (spec, trigger) => spec.explodes?.chain?.[trigger];
60
+ var shotDamageFractionOf = (spec) => spec.explodes?.chain?.shotDamageFraction ?? 0;
61
+ var explosivePart = (explosive, part, options = {}) => ({
62
+ ...part,
63
+ explodes: {
64
+ energy: explosive.energy,
65
+ radius: explosive.injuryRadius,
66
+ ...options.atC !== void 0 ? { atC: options.atC } : {},
67
+ ...explosive.chain ? { chain: { ...explosive.chain } } : {}
68
+ }
69
+ });
70
+
50
71
  // src/math.ts
51
72
  var EPS = 1e-9;
52
73
  var vec = {
@@ -1490,6 +1511,8 @@ var GameDestruction = (() => {
1490
1511
  const listeners = /* @__PURE__ */ new Map();
1491
1512
  let cur = [];
1492
1513
  const blasts = [];
1514
+ const primed = /* @__PURE__ */ new Map();
1515
+ let orderCounter = 0;
1493
1516
  const impls = /* @__PURE__ */ new Map();
1494
1517
  const taken = (id) => objects.has(id) || impls.has(id);
1495
1518
  const emit = (e) => {
@@ -1503,6 +1526,9 @@ var GameDestruction = (() => {
1503
1526
  const base = (o, source) => source === void 0 ? { object: o.id, time } : { object: o.id, time, source };
1504
1527
  const pendingCollapse = /* @__PURE__ */ new Map();
1505
1528
  let runDepth = 0;
1529
+ const drainBlasts = () => {
1530
+ for (let blast = blasts.shift(); blast; blast = blasts.shift()) blastWorld(blast.point, blast.energy, blast.radius, blast.source);
1531
+ };
1506
1532
  const run = (fn) => {
1507
1533
  const prev = cur;
1508
1534
  const mine = [];
@@ -1510,11 +1536,7 @@ var GameDestruction = (() => {
1510
1536
  runDepth++;
1511
1537
  try {
1512
1538
  const value = fn();
1513
- let guard = 0;
1514
- while (blasts.length && guard++ < 64) {
1515
- const b = blasts.shift();
1516
- blastWorld(b.point, b.energy, b.radius, b.source);
1517
- }
1539
+ drainBlasts();
1518
1540
  if (runDepth === 1) flushCollapse();
1519
1541
  return { value, events: mine };
1520
1542
  } finally {
@@ -1574,7 +1596,8 @@ var GameDestruction = (() => {
1574
1596
  maxStress: 0,
1575
1597
  collapsed: new Set(carry?.collapsed ?? []),
1576
1598
  unsup: [],
1577
- anyAnchor: false
1599
+ anyAnchor: false,
1600
+ order: ++orderCounter
1578
1601
  };
1579
1602
  for (const ps of spec.parts) {
1580
1603
  if (o.byId.has(ps.id)) throw new Error(`duplicate part id "${ps.id}"`);
@@ -1621,6 +1644,7 @@ var GameDestruction = (() => {
1621
1644
  cap: ps.structure ? partCapacity(ps, mat) : 0
1622
1645
  };
1623
1646
  if (p.structural) o.hasStructure = true;
1647
+ if (p.rt.chainDue !== void 0 && !isLost(p)) primed.set(p, o);
1624
1648
  o.idx.set(ps.id, o.parts.length);
1625
1649
  o.parts.push(p);
1626
1650
  o.byId.set(ps.id, p);
@@ -2058,15 +2082,29 @@ var GameDestruction = (() => {
2058
2082
  }
2059
2083
  partLost(o, p, src);
2060
2084
  };
2085
+ const prime = (o, p, trigger, src) => {
2086
+ const spec = chainDelayOf(p.spec, trigger);
2087
+ if (spec === void 0 || isLost(p) || p.rt.chainDue !== void 0) return;
2088
+ const delay = drawChainDelay(spec, rnd);
2089
+ p.rt.chainDue = time + delay;
2090
+ primed.set(p, o);
2091
+ o.active = true;
2092
+ emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
2093
+ };
2094
+ const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
2061
2095
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
2062
2096
  if (isLost(p) || amount <= 0) return 0;
2063
2097
  const prev = p.st.hp;
2064
2098
  p.st.hp -= amount;
2065
2099
  markDamaged(p);
2100
+ const trigger = CHAIN_TRIGGER_OF[type];
2101
+ const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
2102
+ if (chained && p.st.hp <= 0 && p.rt.chainDue === void 0) p.st.hp = Math.min(prev, 1);
2066
2103
  if (p.st.hp <= 0 && p.mat.fracture === "none" && !p.spec.explodes) p.st.hp = Math.min(prev, 1);
2067
2104
  const done = prev - p.st.hp;
2068
2105
  if (done > 0) emit({ ...base(o, src), type: "damage", part: p.spec.id, amount: done, hp: Math.max(0, p.st.hp), damageType: type });
2069
2106
  if (p.st.hp <= 0) failPart(o, p, origin, dir, fragEnergy, cap, src);
2107
+ else if (chained && (trigger === "explosion" || 1 - p.st.hp / p.st.maxHp >= shotDamageFractionOf(p.spec))) prime(o, p, trigger, src);
2070
2108
  return done;
2071
2109
  };
2072
2110
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src) => {
@@ -2367,7 +2405,9 @@ var GameDestruction = (() => {
2367
2405
  if (isLost(p)) continue;
2368
2406
  checkIgnite(o, p);
2369
2407
  const ex = p.spec.explodes;
2370
- if (ex && ex.atC !== void 0 && s.temperature >= ex.atC) {
2408
+ if (ex && chainDelayOf(p.spec, "fire") !== void 0) {
2409
+ if (s.temperature >= (ex.atC ?? FIRE_PRIME_C)) prime(o, p, "fire");
2410
+ } else if (ex && ex.atC !== void 0 && s.temperature >= ex.atC) {
2371
2411
  failPart(o, p, null, null, 0, 40);
2372
2412
  continue;
2373
2413
  }
@@ -2379,6 +2419,10 @@ var GameDestruction = (() => {
2379
2419
  s.liters = Math.max(0, (s.liters ?? 0) - rate * dt);
2380
2420
  hot = true;
2381
2421
  if (FLAMMABLE_FLUIDS.includes(cont.fluid) && s.temperature >= 350 && !p.rt.jet) {
2422
+ if (ex && chainDelayOf(p.spec, "fire") !== void 0) {
2423
+ prime(o, p, "fire");
2424
+ continue;
2425
+ }
2382
2426
  if (ex) {
2383
2427
  failPart(o, p, null, null, 0, 40);
2384
2428
  continue;
@@ -2462,6 +2506,34 @@ var GameDestruction = (() => {
2462
2506
  }
2463
2507
  }
2464
2508
  };
2509
+ const nextPrimed = (until) => {
2510
+ let best = null;
2511
+ for (const [p, o] of primed) {
2512
+ if (objects.get(o.id) !== o || isLost(p)) {
2513
+ primed.delete(p);
2514
+ continue;
2515
+ }
2516
+ const due = p.rt.chainDue;
2517
+ if (due > until) continue;
2518
+ if (!best) best = { o, p };
2519
+ else {
2520
+ const bestDue = best.p.rt.chainDue;
2521
+ if (due < bestDue || due === bestDue && (o.order < best.o.order || o.order === best.o.order && (o.idx.get(p.spec.id) ?? 0) < (best.o.idx.get(best.p.spec.id) ?? 0))) best = { o, p };
2522
+ }
2523
+ }
2524
+ return best;
2525
+ };
2526
+ const detonatePrimed = (until) => {
2527
+ const end = time;
2528
+ for (let next = nextPrimed(until); next; next = nextPrimed(until)) {
2529
+ primed.delete(next.p);
2530
+ time = Math.max(0, Math.min(end, next.p.rt.chainDue));
2531
+ next.p.rt.chainDue = void 0;
2532
+ failPart(next.o, next.p, null, null, 0, 40);
2533
+ drainBlasts();
2534
+ }
2535
+ time = end;
2536
+ };
2465
2537
  const withCollapses = (r) => {
2466
2538
  const last = r.value[r.value.length - 1];
2467
2539
  if (!last) return r.value;
@@ -2712,6 +2784,7 @@ var GameDestruction = (() => {
2712
2784
  const sources = [];
2713
2785
  for (const o of [...objects.values()]) if (o.active) stepObject(o, dt, sources);
2714
2786
  if (sources.length) spread(sources, dt);
2787
+ detonatePrimed(time);
2715
2788
  for (const im of [...impls.values()]) {
2716
2789
  if (!im.impl.update) continue;
2717
2790
  for (const e of im.impl.update(dt) ?? []) emit(e.time === void 0 ? { ...e, time } : e);
@@ -3005,6 +3078,7 @@ var GameDestruction = (() => {
3005
3078
  drip: "dripping",
3006
3079
  leak: "leak",
3007
3080
  explode: "exploded",
3081
+ primed: "about to explode",
3008
3082
  stress: "overload",
3009
3083
  collapse: "collapsed",
3010
3084
  destroyed: "destroyed"
@@ -3086,6 +3160,7 @@ var GameDestruction = (() => {
3086
3160
  drip: "pingando",
3087
3161
  leak: "vazamento",
3088
3162
  explode: "explodiu",
3163
+ primed: "prestes a explodir",
3089
3164
  stress: "sobrecarga",
3090
3165
  collapse: "desabou",
3091
3166
  destroyed: "destru\xEDdo"