@adatechnology/game-destruction 0.3.2 → 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 +10 -1
- package/dist/index.d.ts +26 -3
- package/dist/index.global.js +82 -7
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +82 -7
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
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,
|
|
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 };
|
package/dist/index.global.js
CHANGED
|
@@ -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
|
-
|
|
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 &&
|
|
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"
|