@adatechnology/game-destruction 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +197 -2
- package/dist/index.d.ts +352 -0
- package/dist/index.global.js +3117 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +2985 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anderson Fernandes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,198 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @adatechnology/game-destruction
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Engine-agnostic destruction model. An object is an assembly of **parts** (box, cylinder or sphere with a material) held together by **joints**.
|
|
4
|
+
Materials decide what a hit does: steel dents and gets holes, glass cracks and shatters, wood splinters and burns to ash, concrete crumbles,
|
|
5
|
+
aluminium and plastic melt and drip. Joints break under impulse, open when their parts are destroyed (the object breaks apart) or are undone with
|
|
6
|
+
the right tool (the object is taken apart). The world reports everything as **events** that a renderer draws. No dependencies, no renderer, no game.
|
|
7
|
+
|
|
8
|
+
Units: metres, kilograms, seconds, joules, degrees Celsius.
|
|
9
|
+
|
|
10
|
+
Contracts live in [`@adatechnology/game-contracts`](../game-contracts); the types are re-exported here, so existing imports keep working.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add @adatechnology/game-destruction
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
In a browser without a bundler, load `dist/index.global.js`; it exposes the `GameDestruction` global.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createDestruction } from '@adatechnology/game-destruction'
|
|
24
|
+
|
|
25
|
+
const world = createDestruction({ seed: 42 })
|
|
26
|
+
world.on('*', (e) => draw(e)) // dent, hole, crack, fracture, detach, ignite, melt, leak, explode...
|
|
27
|
+
|
|
28
|
+
const chair = world.add({
|
|
29
|
+
name: 'chair',
|
|
30
|
+
parts: [
|
|
31
|
+
{ id: 'seat', material: 'wood_soft', shape: { kind: 'box', size: [0.4, 0.04, 0.4] }, position: [0, 0.42, 0] },
|
|
32
|
+
{ id: 'leg_fl', material: 'wood_soft', shape: { kind: 'box', size: [0.04, 0.4, 0.04] }, position: [0.18, 0.2, 0.18] },
|
|
33
|
+
// ...joints are inferred from touching parts unless you pass `joints`
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
world.shoot([0, 1, 5], [0, 0, -1], 500, 9) // 9 mm, 500 J: raycast, hit, continue through
|
|
38
|
+
world.hit(chair, { type: 'blunt', point: [0.2, 0.2, 0.18], direction: [-1, 0, 0], energy: 300, radius: 0.05 })
|
|
39
|
+
world.hit(chair, { type: 'fire', point: [0, 0.4, 0], temperature: 1000, radius: 0.5, duration: 2 })
|
|
40
|
+
|
|
41
|
+
world.disassemble(chair, 'seat~leg_fl', 'screwdriver', dt) // progress 0..1, -1 for the wrong tool
|
|
42
|
+
for (const e of world.update(dt)) draw(e) // burning, melting, heat transfer, leaks, delayed explosions
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The renderer or physics engine owns transforms: call `setTransform` to keep the world in sync, and after a `detach` use `splitGroup` to turn a loose
|
|
46
|
+
piece into its own object (and body).
|
|
47
|
+
|
|
48
|
+
## Materials
|
|
49
|
+
|
|
50
|
+
26 built-in materials in `src/data/materials.json` (override or extend with the `materials` option). Hit points per kilogram, penetration energy per
|
|
51
|
+
millimetre, ductility and fracture style drive the model.
|
|
52
|
+
|
|
53
|
+
| Family | Ids | Behaviour |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Metal | `steel`, `cast_iron`, `aluminium`, `copper` | dent, holes, sparks, ricochet, glow when hot; melt at 1450 / 1200 / 660 / 1085 °C. Never fracture (cast iron cracks and snaps) |
|
|
56
|
+
| Wood | `wood_soft`, `wood_hard`, `plywood`, `mdf` | splinter or snap, ignite near 300 °C, burn, char, turn to ash |
|
|
57
|
+
| Glass and ceramic | `glass`, `tempered_glass`, `ceramic` | crack, then `shatter` into shards (`tempered_glass` is `crumble` into tiny chunks); thermal shock |
|
|
58
|
+
| Masonry and soil | `concrete`, `brick`, `plaster`, `stone`, `soil` | `crumble` with dust and chips; do not burn |
|
|
59
|
+
| Plastics | `plastic`, `rubber`, `foam`, `wax` | plastic snaps and melts near 200 °C, rubber and foam burn with black smoke, wax melts at 60 °C |
|
|
60
|
+
| Soft | `fabric`, `leather`, `paper` | `tear`, ignite easily and burn fast |
|
|
61
|
+
| Plants | `foliage`, `green_wood`, `cactus`, `flesh_fruit` | `tear` / `splinter`, hard to ignite (high water content) |
|
|
62
|
+
|
|
63
|
+
`guessMaterial(name)` maps a mesh name to a material id from the keyword table in `src/data/material-hints.json` (English keywords, longest match
|
|
64
|
+
wins): `guessMaterial('Chrome_Bumper') === 'steel'`.
|
|
65
|
+
|
|
66
|
+
### Tuning
|
|
67
|
+
|
|
68
|
+
A hit removes `energy * 0.02 * damageScale` hit points. A 9 mm (500 J) breaks a 0.6 m glass pane in 5 hits, tempered glass in 11, a wooden chair leg in 3,
|
|
69
|
+
and never breaks steel (it collects holes). A projectile goes through when `energy > penetration (J/mm) * thickness (mm)`: 9 mm through 1 mm steel
|
|
70
|
+
makes a through hole, through 5 cm concrete it only cracks and lodges. Grazing hits count a longer path through the material and can ricochet.
|
|
71
|
+
|
|
72
|
+
## Damage types
|
|
73
|
+
|
|
74
|
+
| Type | Inputs | Effect |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `ballistic` | `energy`, `caliber` | hole (through or lodged), dent, crack, ricochet, debris, joint wear next to the impact |
|
|
77
|
+
| `blunt` | `energy`, `radius` | dents on ductile parts, cracks on brittle ones, wears every joint of the part (a hard kick pulls a leg off) |
|
|
78
|
+
| `slash` | `energy` | like blunt, with extra damage on fabric, foliage and wood; opens fabric |
|
|
79
|
+
| `explosive` | `energy`, `radius` | falloff `(1 - d/r)^2`, damage, dents facing the blast, joints break, fragments fly (capped at 40 m/s), flammables ignite |
|
|
80
|
+
| `fire` | `temperature`, `radius`, `duration` | heats towards the flame temperature and ignites at the material's ignition point |
|
|
81
|
+
| `heat` | same | heat without flame (cutting torch, hot plate); melts what it can reach |
|
|
82
|
+
|
|
83
|
+
`hit(objectId, hit)` finds the part along `direction`; `hitPart` targets a known part; `shoot` and `raycast` work in world space.
|
|
84
|
+
|
|
85
|
+
## Fire, melting and leaks
|
|
86
|
+
|
|
87
|
+
`update(dt)` advances everything that is hot. Burning parts consume fuel (`burnRate * surface`), lose hit points, char and finally turn to ashes
|
|
88
|
+
(`fuelFraction >= 0.8`) or stay charred. Heat flows through closed joints by conductivity and to parts of other objects within `fireSpread`. Heat
|
|
89
|
+
only moves towards a source temperature, so a wood fire (800 °C) never melts steel (1450 °C) but melts aluminium and plastic, and a 3000 °C torch
|
|
90
|
+
melts anything. Melting emits `melt` and `drip`; a melted part is lost and its joints open. Pressurized gas that leaks near fire burns (or explodes
|
|
91
|
+
when the part has `explodes`). `contains` parts leak through punctures: the `leak` event carries the hole's world point, the outward normal as
|
|
92
|
+
`direction` and the rate in litres per second, and `PartState.liters` drops in `update` (`setLiters` fills or empties a container).
|
|
93
|
+
|
|
94
|
+
## Joints and tools
|
|
95
|
+
|
|
96
|
+
| Kind | Strength | Tools | Seconds |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| `screw` | 80 | screwdriver | 2.5 |
|
|
99
|
+
| `bolt` | 200 | wrench | 4 |
|
|
100
|
+
| `nail` | 60 | pry_bar, hammer | 2 |
|
|
101
|
+
| `weld` | 600 | cutting_torch, saw | 8 |
|
|
102
|
+
| `glue` | 50 | pry_bar, knife | 3 |
|
|
103
|
+
| `hinge` | 120 | screwdriver | 3 |
|
|
104
|
+
| `clip` | 20 | hand | 0.6 |
|
|
105
|
+
| `tie` | 15 | knife, hand | 1 |
|
|
106
|
+
| `mortar` | 400 | hammer | 6 |
|
|
107
|
+
| `fused` | 1000 | none (can only break) | 0 |
|
|
108
|
+
| `stem` | 40 | knife, saw, hand | 1.5 |
|
|
109
|
+
| `rebar` | 1500 | cutting_torch, saw | 20 |
|
|
110
|
+
| `resting` | 30 | hand | 1 |
|
|
111
|
+
|
|
112
|
+
`inferJoints` joins parts whose bounds touch (default tolerance 1 cm) with the kind from `JOINT_BY_MATERIALS`. Joints open when their hit points
|
|
113
|
+
run out (`detach` with `by: 'break'`), when a part is lost (`'part_lost'`) or when a tool finishes the work (`'tool'`). `ObjectState.groups` lists
|
|
114
|
+
the connected pieces; index 0 keeps the object's id (the anchored one, else the heaviest).
|
|
115
|
+
|
|
116
|
+
## Structural integrity
|
|
117
|
+
|
|
118
|
+
Give parts a `structure: { role, capacity? }` (`foundation`, `column`, `beam`, `slab`, `wall`, `partition`, `roof`, `stair`, `none`) and the world
|
|
119
|
+
solves loads: each structural part carries its own mass plus what rests on it, down to the anchored parts (`foundation` implies `anchored`).
|
|
120
|
+
`PartState.load` is in kg and `PartState.stress` is `load / capacity`. Parts without a role (windows, doors, furniture) carry nothing. Objects
|
|
121
|
+
without any `structure` behave exactly as before.
|
|
122
|
+
|
|
123
|
+
- **Capacity** defaults to `material.compressive` (kg per m²: concrete 444 000, brick 40 000, softwood 100 000, steel 2 000 000) times the section:
|
|
124
|
+
the two smallest dimensions for columns, beams, roofs and stairs (foundations x10), thickness times longest side for slabs, thickness times
|
|
125
|
+
horizontal length for walls. A 0.3 m concrete column carries 40 t, a 0.2 m brick wall 8 t per metre, a 0.1 x 0.2 m softwood beam 2 t.
|
|
126
|
+
- **Joints** carry load when `bears` is true (default when both parts have a role). `bears: 'a_on_b'` means part `a` rests on `b`: load flows only
|
|
127
|
+
that way. A `partition` is supported but supports nothing, so breaking it never drops anything.
|
|
128
|
+
- **Overload**: above stress 1 a part takes `(stress - 1) * maxHp * 0.5` damage per second, emits `stress` (once per 0.5 s) and cascades; brittle
|
|
129
|
+
parts `crack` from stress 0.8. Metal never fractures, so overloaded steel only reports stress.
|
|
130
|
+
- **Collapse**: a part with no support path to an anchored part falls: it is cut from the supported pieces into its own group (index 1 or more) and
|
|
131
|
+
one `collapse` event lists the group. Non-structural parts that hang only on collapsed parts fall with them. Make the group its own object with
|
|
132
|
+
`splitGroup`.
|
|
133
|
+
- `buildFrame({ spanX, spanZ, columnsX, columnsZ, height, floors, walls, roof, columnCapacity, ... })` returns a test structure: columns on
|
|
134
|
+
foundations (`rebar`), beams, a slab per floor, optional brick walls (`mortar`) with a door opening, and a roof that rests (`resting`) on the top beams.
|
|
135
|
+
|
|
136
|
+
## Contracts: implement your own or use the default world
|
|
137
|
+
|
|
138
|
+
The kit defines contracts; each game module implements them for the objects it owns, and the world in this package is the optional default
|
|
139
|
+
implementation. A module implements `DestructibleImpl` (`describe`, `hit`, `update`, `state`, `reset`, `disassemble`, `extinguish`,
|
|
140
|
+
`serialize`, `load`) and registers it:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const id = world.register(myVehicleDoor, { position: [10, 0, 0] })
|
|
144
|
+
world.shoot(origin, dir, 500, 9) // passes through default objects and impls in order
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The world routes raycast, shoot, explode, hit, disassemble, extinguish, update, state, reset, remove and `on` to the impl; its events go through the
|
|
148
|
+
same listeners in order. The impl receives hits in world space and returns events in world space (the transform given to `register` and
|
|
149
|
+
`setTransform` is only used for raycast and blast range). `splitGroup` on an impl throws. After a shot that goes through an impl the world assumes
|
|
150
|
+
half of the energy continues when a through `hole` was reported. `checkDestructible(impl, { materials })` returns `{ ok, problems }`: `describe()`
|
|
151
|
+
valid (unique ids, known materials, joints reference parts), a 500 J ballistic hit returns events of this impl naming known parts, `state()` matches
|
|
152
|
+
`describe()`, hit points never go negative, `update(0.016)` returns an array, events are finite JSON.
|
|
153
|
+
|
|
154
|
+
### Impact contract (what weapons must produce)
|
|
155
|
+
|
|
156
|
+
| `type` | Required | Optional | Notes |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| `ballistic` | `point`, `direction`, `energy`, `caliber` | `space`, `source` | joules, diameter in millimetres |
|
|
159
|
+
| `blunt` | `point`, `energy` | `direction`, `radius`, `space`, `source` | contact radius in metres |
|
|
160
|
+
| `slash` | `point`, `energy` | `direction`, `radius`, `space`, `source` | axes, knives |
|
|
161
|
+
| `explosive` | `point`, `energy`, `radius` | `space`, `source` | `point` is the blast centre |
|
|
162
|
+
| `fire` | `point`, `temperature`, `radius` | `duration`, `space`, `source` | call every frame with `duration: dt` |
|
|
163
|
+
| `heat` | `point`, `temperature`, `radius` | `duration`, `space`, `source` | cutting torch, hot plate |
|
|
164
|
+
|
|
165
|
+
`IMPACT_CONTRACT` holds this table as data and `checkHit(hit)` returns the problems of a hit.
|
|
166
|
+
|
|
167
|
+
## Events
|
|
168
|
+
|
|
169
|
+
`damage`, `dent`, `hole`, `crack`, `ricochet`, `debris`, `fracture` (with oriented `fragments` in world space), `detach`, `disassemble_progress`,
|
|
170
|
+
`ignite`, `extinguish`, `burnout`, `melt`, `drip`, `leak`, `explode`, `stress`, `collapse`, `destroyed`. All points are in world space. Subscribe with
|
|
171
|
+
`world.on(type | '*', fn)` or the `onEvent` option; `hit` and `update` also return their events.
|
|
172
|
+
|
|
173
|
+
## Determinism and LAN
|
|
174
|
+
|
|
175
|
+
Every random choice comes from the world seed (mulberry32), never `Math.random`, and the generator state is part of the snapshot. A LAN host can
|
|
176
|
+
run the world and send only the inputs (hits, `update(dt)` calls), or the events. `serialize()` / `load()` give a plain JSON round trip: a loaded
|
|
177
|
+
world continues with the same events as the original. The loading world must use the same materials.
|
|
178
|
+
|
|
179
|
+
## API
|
|
180
|
+
|
|
181
|
+
`createDestruction(options)` returns a `DestructionWorld`: `add`, `remove`, `has`, `ids`, `setTransform`, `splitGroup`, `hit`, `hitPart`, `explode`,
|
|
182
|
+
`raycast`, `shoot`, `disassemble`, `workableJoints`, `nearestJoint`, `extinguish`, `setLiters`, `update`, `state`, `part`, `reset`, `on`,
|
|
183
|
+
`register`, `serialize`, `load`, `time`. `state()` and `part()` return live views; do not mutate them.
|
|
184
|
+
|
|
185
|
+
Also exported: `buildFrame`, `partCapacity`, `checkDestructible`, `checkHit`, `IMPACT_CONTRACT`, `MATERIALS`, `JOINT_DEFAULTS`, `JOINT_BY_MATERIALS`, `inferJoints`, `guessMaterial`, `partVolume`, `partMass`, `partThickness`, the
|
|
186
|
+
helpers `vec`, `quat`, `quatFromEuler`, `rotateVec`, `toLocal`, `toWorld`, the locale (`locales`, `resolveStrings`, `DestructionStrings`) and all types.
|
|
187
|
+
|
|
188
|
+
The locale table has `en` and `pt-BR` strings for material names, part statuses, joint kinds, tools, damage types, event labels and fracture styles:
|
|
189
|
+
`resolveStrings('pt-BR').materialNames.glass === 'vidro'`. Unknown locales fall back to `en`.
|
|
190
|
+
|
|
191
|
+
## Português
|
|
192
|
+
|
|
193
|
+
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
|
|
194
|
+
material decide o efeito: o aço amassa e fura, o vidro trinca e estilhaça, a madeira lasca e vira cinza, o concreto esfarela, o alumínio e o
|
|
195
|
+
plástico derretem e pingam. As juntas cedem com impacto, quando a peça é destruída ou quando se usa a ferramenta certa (chave de fenda para
|
|
196
|
+
parafuso, chave inglesa para porca e parafuso, maçarico para solda). O mundo devolve tudo como eventos para o renderizador desenhar. É
|
|
197
|
+
determinístico (semente fixa) e serializável, então um host de LAN pode rodar o mundo e enviar só as entradas. Os nomes em português
|
|
198
|
+
(materiais, ferramentas, juntas, eventos) estão em `resolveStrings('pt-BR')`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
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';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public types of @adatechnology/game-destruction.
|
|
6
|
+
*
|
|
7
|
+
* The model is renderer-free. An object is an assembly of parts (simple shapes with a material) held together by joints. Materials decide what a
|
|
8
|
+
* hit does: ductile parts dent and get holes, brittle parts crack and shatter, wood splinters, masonry crumbles, flammable parts ignite, char and
|
|
9
|
+
* turn to ash, fusible parts melt and drip. Joints break under impulse or when their parts are destroyed (the object breaks apart), or are undone
|
|
10
|
+
* with the right tool (the object is taken apart). The world reports everything as events that a renderer draws.
|
|
11
|
+
*
|
|
12
|
+
* Units: metres, kilograms, seconds, joules, degrees Celsius. Every random choice comes from the world seed, so the same inputs give the same
|
|
13
|
+
* events on every machine (a LAN host can run the world and send only the inputs, or the events).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface MaterialThermal {
|
|
17
|
+
/** Temperature at which the part catches fire. Omit for non-flammable. */
|
|
18
|
+
ignitionC?: number;
|
|
19
|
+
/** Flame temperature while burning (default 800). */
|
|
20
|
+
flameC?: number;
|
|
21
|
+
/** Fraction of the part's mass that can burn, 0..1 (wood 0.9, fabric 1, plastic 0.8). */
|
|
22
|
+
fuelFraction?: number;
|
|
23
|
+
/** Burn speed in kg per second per square metre of surface (wood ~0.012, fabric ~0.05, foliage ~0.08). */
|
|
24
|
+
burnRate?: number;
|
|
25
|
+
/** Temperature at which the part starts to melt. Omit for materials that do not melt in game (wood, fabric). */
|
|
26
|
+
meltC?: number;
|
|
27
|
+
/** Melt speed: fraction of the part per second per 100 °C above `meltC` (default 0.05). */
|
|
28
|
+
meltRate?: number;
|
|
29
|
+
/** Heat transfer to touching parts, 0..1 (copper 1, steel 0.5, wood 0.05). */
|
|
30
|
+
conductivity: number;
|
|
31
|
+
/** Temperature change per second per unit of heat input; lower means it heats and cools faster (default 1). */
|
|
32
|
+
heatCapacity?: number;
|
|
33
|
+
/** Brittle parts crack when their temperature changes faster than this many °C per second (glass ~150). */
|
|
34
|
+
thermalShockC?: number;
|
|
35
|
+
/** Smoke the material gives while burning (`black` for plastic and rubber). */
|
|
36
|
+
smoke?: 'white' | 'grey' | 'black';
|
|
37
|
+
}
|
|
38
|
+
interface MaterialDef {
|
|
39
|
+
id: MaterialId;
|
|
40
|
+
/** Density in kg/m³ (used when a part has no `mass`). */
|
|
41
|
+
density: number;
|
|
42
|
+
/** Structural hit points per kilogram. */
|
|
43
|
+
hpPerKg: number;
|
|
44
|
+
/**
|
|
45
|
+
* Energy in joules a projectile needs to go through 1 mm of this material. A projectile that carries more than `penetration * thicknessMm` goes
|
|
46
|
+
* through and keeps the rest; one that carries less stops (dent, crack or lodge).
|
|
47
|
+
*/
|
|
48
|
+
penetration: number;
|
|
49
|
+
/** 0 = brittle (glass), 1 = very ductile (soft steel). Ductile parts dent and bend before they break; brittle parts crack and shatter. */
|
|
50
|
+
ductility: number;
|
|
51
|
+
/** How the part fails when its hp reaches 0. */
|
|
52
|
+
fracture: FractureStyle;
|
|
53
|
+
/** Fragment size range in metres (default `[0.04, 0.2]`). */
|
|
54
|
+
fragmentSize?: [number, number];
|
|
55
|
+
/** Damage multipliers per damage type (default 1). */
|
|
56
|
+
damageScale?: Partial<Record<DamageType, number>>;
|
|
57
|
+
/** Debris emitted on a hit that does not break the part. */
|
|
58
|
+
hitDebris: DebrisKind;
|
|
59
|
+
/** Debris emitted when the part fractures. */
|
|
60
|
+
breakDebris: DebrisKind;
|
|
61
|
+
/** A ballistic hit at a grazing angle (below this many degrees from the surface) ricochets; 0 = never (default 0). */
|
|
62
|
+
ricochetDeg?: number;
|
|
63
|
+
thermal: MaterialThermal;
|
|
64
|
+
/** Hints for the renderer. Colours are `0xRRGGBB`. */
|
|
65
|
+
visual?: {
|
|
66
|
+
/** Colour a fully charred surface turns to (default 0x1a1410). */
|
|
67
|
+
charColor?: number;
|
|
68
|
+
/** The material glows red to white when hot (metals, glass). */
|
|
69
|
+
glows?: boolean;
|
|
70
|
+
/** Transparent (glass): cracks are drawn as a star on the surface. */
|
|
71
|
+
transparent?: boolean;
|
|
72
|
+
};
|
|
73
|
+
/** Free tags, e.g. `metal`, `organic`, `conductive`. */
|
|
74
|
+
tags?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* Load a structural part of this material carries per square metre of cross-section, in kg (game-tuned: concrete 444 000, brick 40 000,
|
|
77
|
+
* softwood 100 000, steel 2 000 000). Default 20 000. See `PartSpec.structure`.
|
|
78
|
+
*/
|
|
79
|
+
compressive?: number;
|
|
80
|
+
}
|
|
81
|
+
interface DestructionOptions {
|
|
82
|
+
/** Extra or overriding materials (merged over `MATERIALS` by id). */
|
|
83
|
+
materials?: MaterialDef[];
|
|
84
|
+
/** Seed for every random choice (default 1). */
|
|
85
|
+
seed?: number;
|
|
86
|
+
/** Ambient temperature (default 20). */
|
|
87
|
+
ambientC?: number;
|
|
88
|
+
/** Called for every event, in order. */
|
|
89
|
+
onEvent?: (event: DestructionEvent) => void;
|
|
90
|
+
/** Maximum fragments in one `fracture` event (default 24). The renderer can draw fewer. */
|
|
91
|
+
maxFragments?: number;
|
|
92
|
+
/** Maximum dents kept per part; older nearby dents merge (default 24). */
|
|
93
|
+
maxDents?: number;
|
|
94
|
+
/** Maximum holes kept per part (default 32). */
|
|
95
|
+
maxHoles?: number;
|
|
96
|
+
/** Distance at which a burning part heats parts of other objects, in metres (default 0.6). */
|
|
97
|
+
fireSpread?: number;
|
|
98
|
+
}
|
|
99
|
+
interface AddOptions {
|
|
100
|
+
/** Object id (default a generated `obj_1`, `obj_2`...). */
|
|
101
|
+
id?: string;
|
|
102
|
+
position?: Vec3;
|
|
103
|
+
rotation?: Quat;
|
|
104
|
+
}
|
|
105
|
+
interface DestructionWorld {
|
|
106
|
+
readonly materials: ReadonlyMap<MaterialId, MaterialDef>;
|
|
107
|
+
/** Add an object and return its id. */
|
|
108
|
+
add(spec: DestructibleSpec, options?: AddOptions): string;
|
|
109
|
+
/**
|
|
110
|
+
* Host an object whose behaviour a game module owns (see `DestructibleImpl`) next to the world's own objects. Returns its id (`options.id`, else
|
|
111
|
+
* `impl.id`). Raycast, shoot, explode, hit, disassemble, extinguish, update, state, reset, remove and `on` route to it; its events go through the
|
|
112
|
+
* same listeners in order. `splitGroup` on it throws.
|
|
113
|
+
*/
|
|
114
|
+
register(impl: DestructibleImpl, options?: AddOptions): string;
|
|
115
|
+
remove(id: string): void;
|
|
116
|
+
has(id: string): boolean;
|
|
117
|
+
ids(): string[];
|
|
118
|
+
/** Move an object (the renderer or physics owns transforms; keep the world in sync so world-space hits land). */
|
|
119
|
+
setTransform(id: string, position: Vec3, rotation: Quat): void;
|
|
120
|
+
/**
|
|
121
|
+
* Split a loose group into its own object (after a `detach`, when the renderer makes the piece a separate body). The parts move with their
|
|
122
|
+
* state; the new object starts at the old object's transform. Returns the new id.
|
|
123
|
+
*/
|
|
124
|
+
splitGroup(id: string, group: number, newId?: string): string;
|
|
125
|
+
/** Apply a hit to one object (to the part it lands on, or for `explosive`/`fire`/`heat`, to every part in `radius`). */
|
|
126
|
+
hit(objectId: string, hit: Hit): HitResult;
|
|
127
|
+
/** Apply a hit to a known part (e.g. after the host's own raycast). */
|
|
128
|
+
hitPart(objectId: string, partId: string, hit: Hit): HitResult;
|
|
129
|
+
/** A blast in world space against every object in range. */
|
|
130
|
+
explode(point: Vec3, energy: number, radius: number, source?: string): HitResult[];
|
|
131
|
+
/** Raycast against every part shape (world space). */
|
|
132
|
+
raycast(origin: Vec3, direction: Vec3, maxDistance?: number): RayHit | null;
|
|
133
|
+
/** Ballistic shot: raycast, hit, and continue through with the exit energy (up to 4 objects/parts). */
|
|
134
|
+
shoot(origin: Vec3, direction: Vec3, energy: number, caliber: number, source?: string): HitResult[];
|
|
135
|
+
/**
|
|
136
|
+
* Work on a joint with a tool for `dt` seconds (call every frame while the player holds the button). Returns the progress 0..1, or -1 when the
|
|
137
|
+
* tool cannot undo this joint. At 1 the joint opens (`detach` with `by: 'tool'`).
|
|
138
|
+
*/
|
|
139
|
+
disassemble(objectId: string, jointId: string, tool: ToolKind, dt: number): number;
|
|
140
|
+
/** Joints of an object that `tool` can undo now (open joints excluded). */
|
|
141
|
+
workableJoints(objectId: string, tool: ToolKind): JointState[];
|
|
142
|
+
/** Joint nearest to a world point (for "aim at the screw"). */
|
|
143
|
+
nearestJoint(objectId: string, point: Vec3, maxDistance?: number): JointState | null;
|
|
144
|
+
/** Put out fire on a part or the whole object, and cool it by `coolC` (default 200). */
|
|
145
|
+
extinguish(objectId: string, partId?: string, coolC?: number): void;
|
|
146
|
+
/** Advance fire, heat transfer, cooling, melting, leaks and delayed explosions. */
|
|
147
|
+
update(dt: number): DestructionEvent[];
|
|
148
|
+
state(id: string): ObjectState | null;
|
|
149
|
+
part(id: string, partId: string): PartState | null;
|
|
150
|
+
/** Repair an object to its spec (test benches use it for "reset"). */
|
|
151
|
+
reset(id: string): void;
|
|
152
|
+
on(type: DestructionEventType | '*', fn: (event: DestructionEvent) => void): () => void;
|
|
153
|
+
/** Plain JSON of every object (spec + state + transform) and the rng state. */
|
|
154
|
+
serialize(): DestructionSnapshot;
|
|
155
|
+
load(snapshot: DestructionSnapshot): void;
|
|
156
|
+
/**
|
|
157
|
+
* Fill or empty a container part (`PartSpec.contains`); `liters` is clamped to `[0, contains.liters]` of its spec. Used by fluid simulations
|
|
158
|
+
* that drain or refill the part. Does nothing when the part has no `contains`.
|
|
159
|
+
*/
|
|
160
|
+
setLiters(objectId: string, partId: string, liters: number): void;
|
|
161
|
+
/** Seconds since creation. */
|
|
162
|
+
readonly time: number;
|
|
163
|
+
}
|
|
164
|
+
/** Internal bookkeeping of a part that is saved with the world so that a loaded world continues identically. Treat as opaque. */
|
|
165
|
+
interface PartRuntime {
|
|
166
|
+
/** Last melt fraction reported in a `melt` event. */
|
|
167
|
+
meltEmitted: number;
|
|
168
|
+
/** Seconds since the last `drip`. */
|
|
169
|
+
dripT: number;
|
|
170
|
+
/** Temperature at the end of the previous update (thermal shock). */
|
|
171
|
+
prevTemp: number;
|
|
172
|
+
/** Seconds left before another thermal crack is allowed. */
|
|
173
|
+
shockT: number;
|
|
174
|
+
/** A leaking flammable fluid is burning on this part. */
|
|
175
|
+
jet: boolean;
|
|
176
|
+
/** Seconds left before the next `stress` event. */
|
|
177
|
+
stressT?: number;
|
|
178
|
+
}
|
|
179
|
+
/** Opaque bookkeeping of an object (see `PartRuntime`). `joints` holds the last disassembly step reported per joint id. */
|
|
180
|
+
interface ObjectRuntime {
|
|
181
|
+
parts: Record<string, PartRuntime>;
|
|
182
|
+
joints: Record<string, number>;
|
|
183
|
+
/** Parts of groups that already reported a `collapse`. */
|
|
184
|
+
collapsed?: string[];
|
|
185
|
+
}
|
|
186
|
+
interface DestructionSnapshot {
|
|
187
|
+
version: 1;
|
|
188
|
+
time: number;
|
|
189
|
+
rng: number;
|
|
190
|
+
/** Counter for generated object ids (`obj_1`...). */
|
|
191
|
+
counter?: number;
|
|
192
|
+
objects: Array<{
|
|
193
|
+
id: string;
|
|
194
|
+
spec: DestructibleSpec;
|
|
195
|
+
state: ObjectState;
|
|
196
|
+
runtime?: ObjectRuntime;
|
|
197
|
+
}>;
|
|
198
|
+
/** Registered impls: transform and `serialize()` data. `load` restores them into impls registered under the same ids. */
|
|
199
|
+
impls?: Array<{
|
|
200
|
+
id: string;
|
|
201
|
+
position: Vec3;
|
|
202
|
+
rotation: Quat;
|
|
203
|
+
data?: unknown;
|
|
204
|
+
}>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
declare const createDestruction: (options?: DestructionOptions) => DestructionWorld;
|
|
208
|
+
|
|
209
|
+
/** Built-in materials (data in `src/data/materials.json`). Override or extend with the `materials` option of `createDestruction`. */
|
|
210
|
+
declare const MATERIALS: MaterialDef[];
|
|
211
|
+
/** Default joint strength (joint hit points), tools that undo it and seconds of tool work. */
|
|
212
|
+
declare const JOINT_DEFAULTS: Record<JointKind, {
|
|
213
|
+
strength: number;
|
|
214
|
+
tools: ToolKind[];
|
|
215
|
+
seconds: number;
|
|
216
|
+
}>;
|
|
217
|
+
/**
|
|
218
|
+
* The joint kind that fits two materials: glass is glued (clipped to plastic), soft things are tied, plants share a stem, masonry gets mortar,
|
|
219
|
+
* metal is welded, wood is screwed, plastic clips. `inferJoints` refines weld to bolt for thick metal and screw to nail for thin wood.
|
|
220
|
+
*/
|
|
221
|
+
declare const JOINT_BY_MATERIALS: (a: MaterialDef, b: MaterialDef) => JointKind;
|
|
222
|
+
/** Guess a material id from a mesh or node name (case-insensitive substring; the longest keyword wins). */
|
|
223
|
+
declare const guessMaterial: (name: string) => MaterialId | undefined;
|
|
224
|
+
/** Volume of a shape in m³. */
|
|
225
|
+
declare const partVolume: (shape: PartShape) => number;
|
|
226
|
+
/** Mass of a part in kg: its `mass`, or density × volume × fill. */
|
|
227
|
+
declare const partMass: (part: PartSpec, material: MaterialDef) => number;
|
|
228
|
+
/** Wall thickness of a part in metres (see `PartSpec.thickness`). */
|
|
229
|
+
declare const partThickness: (part: PartSpec) => number;
|
|
230
|
+
/**
|
|
231
|
+
* Joints for touching parts. Two parts touch when their axis-aligned bounds (after rotation), grown by `tolerance` metres, overlap. The kind comes
|
|
232
|
+
* from `JOINT_BY_MATERIALS`; thick metal pairs (smallest dimension of both >= 2 cm) get a `bolt` instead of a `weld`, and thin wood pairs (either
|
|
233
|
+
* under 2 cm) a `nail` instead of a `screw`.
|
|
234
|
+
*/
|
|
235
|
+
declare const inferJoints: (parts: PartSpec[], tolerance?: number, materials?: ReadonlyMap<string, MaterialDef>) => JointSpec[];
|
|
236
|
+
/**
|
|
237
|
+
* Load in kg a structural part carries before it fails: `structure.capacity`, else `material.compressive` (default 20 000 kg/m²) times the
|
|
238
|
+
* section. The section is the product of the two smallest dimensions (thickness times longest dimension for slabs; thickness times horizontal length for walls); foundations get x10.
|
|
239
|
+
*/
|
|
240
|
+
declare const partCapacity: (part: PartSpec, material: MaterialDef) => number;
|
|
241
|
+
/**
|
|
242
|
+
* Multiplier of blunt damage on brittle parts (ductility < 0.3): up to x4.7 for fully brittle materials (glass, ceramic, stone), times a thinness
|
|
243
|
+
* term that follows bending stress (1 / thickness squared, x1 at 2 cm or more, at most x25). A 3 mm glass bottle breaks from a 1.5 m drop; a
|
|
244
|
+
* concrete block (thick) is only ever x4.4.
|
|
245
|
+
*/
|
|
246
|
+
declare const fragility: (ductility: number, thickness: number) => number;
|
|
247
|
+
|
|
248
|
+
interface FrameOptions {
|
|
249
|
+
/** Total length along x between the outer column centres, metres (default 8). */
|
|
250
|
+
spanX?: number;
|
|
251
|
+
/** Total length along z (default 8). */
|
|
252
|
+
spanZ?: number;
|
|
253
|
+
/** Columns along x (at least 2, default 3). */
|
|
254
|
+
columnsX?: number;
|
|
255
|
+
/** Columns along z (at least 2, default 3). */
|
|
256
|
+
columnsZ?: number;
|
|
257
|
+
/** Height of one floor from slab top to the underside of the next slab, metres (default 3). */
|
|
258
|
+
height?: number;
|
|
259
|
+
/** Number of floors (default 1). */
|
|
260
|
+
floors?: number;
|
|
261
|
+
/** Brick walls on every perimeter bay of every floor; the first bay of the ground floor on the -z side gets a door opening (default false). */
|
|
262
|
+
walls?: boolean;
|
|
263
|
+
/** A thin roof slab resting on the top beams (default true); without it the top floor is open. */
|
|
264
|
+
roof?: boolean;
|
|
265
|
+
columnSize?: number;
|
|
266
|
+
beamHeight?: number;
|
|
267
|
+
slabThickness?: number;
|
|
268
|
+
wallThickness?: number;
|
|
269
|
+
doorWidth?: number;
|
|
270
|
+
doorHeight?: number;
|
|
271
|
+
/** Material of foundations, columns, beams, slabs and roof (default `concrete`). */
|
|
272
|
+
material?: MaterialId;
|
|
273
|
+
wallMaterial?: MaterialId;
|
|
274
|
+
/** Override the load capacity of every column, kg (see `PartState.load`). */
|
|
275
|
+
columnCapacity?: number;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* A parametric test structure: a grid of columns on foundations, beams between the column tops, one slab per floor (the last one a roof), and
|
|
279
|
+
* optional brick walls with a door. Joints are explicit and directional: columns rest on foundations (or on the slab below) with `rebar`, beams on
|
|
280
|
+
* columns, slabs on beams, the roof rests on the top beams, walls hang on their columns with `mortar`. Part ids: `foundation_i_j`, `column_f_i_j`,
|
|
281
|
+
* `beam_x_f_i_j`, `beam_z_f_i_j`, `slab_f`, `roof`, `wall_*`, `door`. Ground is y = 0 (the top of the foundations).
|
|
282
|
+
*/
|
|
283
|
+
declare const buildFrame: (o?: FrameOptions) => DestructibleSpec;
|
|
284
|
+
|
|
285
|
+
/** Small vector helpers (arrays of three numbers). Pure functions; they allocate a new array. */
|
|
286
|
+
declare const vec: {
|
|
287
|
+
add: (a: Vec3, b: Vec3) => Vec3;
|
|
288
|
+
sub: (a: Vec3, b: Vec3) => Vec3;
|
|
289
|
+
scale: (a: Vec3, s: number) => Vec3;
|
|
290
|
+
dot: (a: Vec3, b: Vec3) => number;
|
|
291
|
+
cross: (a: Vec3, b: Vec3) => Vec3;
|
|
292
|
+
length: (a: Vec3) => number;
|
|
293
|
+
distance: (a: Vec3, b: Vec3) => number;
|
|
294
|
+
normalize: (a: Vec3, fallback?: Vec3) => Vec3;
|
|
295
|
+
lerp: (a: Vec3, b: Vec3, t: number) => Vec3;
|
|
296
|
+
/** `a + b * s` */
|
|
297
|
+
madd: (a: Vec3, b: Vec3, s: number) => Vec3;
|
|
298
|
+
};
|
|
299
|
+
/** Quaternion helpers (`[x, y, z, w]`). */
|
|
300
|
+
declare const quat: {
|
|
301
|
+
identity: () => Quat;
|
|
302
|
+
/** Hamilton product `a * b` (apply `b` first, then `a`). */
|
|
303
|
+
mul: (a: Quat, b: Quat) => Quat;
|
|
304
|
+
conjugate: (q: Quat) => Quat;
|
|
305
|
+
normalize: (q: Quat) => Quat;
|
|
306
|
+
/** Rotation of `angle` radians around a unit `axis`. */
|
|
307
|
+
fromAxisAngle: (axis: Vec3, angle: number) => Quat;
|
|
308
|
+
/** Rotation matrix, row-major 3x3. */
|
|
309
|
+
toMatrix: (q: Quat) => number[];
|
|
310
|
+
/** Quaternion from three orthonormal columns (the images of the x, y and z axes). */
|
|
311
|
+
fromColumns: (cx: Vec3, cy: Vec3, cz: Vec3) => Quat;
|
|
312
|
+
};
|
|
313
|
+
/** Quaternion from Euler angles in radians, order XYZ (the three.js default). */
|
|
314
|
+
declare const quatFromEuler: (x: number, y: number, z: number) => Quat;
|
|
315
|
+
/** Rotate a vector by a quaternion. */
|
|
316
|
+
declare const rotateVec: (q: Quat, v: Vec3) => Vec3;
|
|
317
|
+
/** World point to the local space of a transform (`position`, `rotation`). */
|
|
318
|
+
declare const toLocal: (position: Vec3, rotation: Quat, p: Vec3) => Vec3;
|
|
319
|
+
/** Local point of a transform (`position`, `rotation`) to world space. */
|
|
320
|
+
declare const toWorld: (position: Vec3, rotation: Quat, p: Vec3) => Vec3;
|
|
321
|
+
|
|
322
|
+
/** Locales shipped with the package. Unknown locales fall back to `en`. */
|
|
323
|
+
type DestructionLocale = 'en' | 'pt-BR';
|
|
324
|
+
interface DestructionStrings {
|
|
325
|
+
/** Display name per material id (see `MATERIALS`). Unknown ids are not listed. */
|
|
326
|
+
materialNames: Record<string, string>;
|
|
327
|
+
partStatus: Record<PartStatus, string>;
|
|
328
|
+
jointKinds: Record<JointKind, string>;
|
|
329
|
+
toolKinds: Record<ToolKind, string>;
|
|
330
|
+
damageTypes: Record<DamageType, string>;
|
|
331
|
+
/** Short label per event type, for logs and kill-feed style text. */
|
|
332
|
+
eventLabels: Record<DestructionEventType, string>;
|
|
333
|
+
fractureStyles: Record<FractureStyle, string>;
|
|
334
|
+
structureRoles: Record<StructureRole, string>;
|
|
335
|
+
}
|
|
336
|
+
declare const locales: Record<DestructionLocale, DestructionStrings>;
|
|
337
|
+
declare const resolveStrings: (locale: string | undefined) => DestructionStrings;
|
|
338
|
+
|
|
339
|
+
interface CheckOptions {
|
|
340
|
+
/** Ids of custom materials the impl uses besides the built-in `MATERIALS`. */
|
|
341
|
+
materials?: string[];
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* `checkDestructible` of `@adatechnology/game-contracts`, with the built-in `MATERIALS` (plus `options.materials`) as the known material ids.
|
|
345
|
+
* It hits the impl (and calls `reset()` afterwards when there is one).
|
|
346
|
+
*/
|
|
347
|
+
declare const checkDestructible: (impl: DestructibleImpl, options?: CheckOptions) => {
|
|
348
|
+
ok: boolean;
|
|
349
|
+
problems: string[];
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
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, buildFrame, checkDestructible, createDestruction, fragility, guessMaterial, inferJoints, locales, partCapacity, partMass, partThickness, partVolume, quat, quatFromEuler, resolveStrings, rotateVec, toLocal, toWorld, vec };
|