@adatechnology/game-destruction-three 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 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,136 @@
1
- # Temporary Holding Version
1
+ # @adatechnology/game-destruction-three
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ three.js renderer for [`@adatechnology/game-destruction`](../game-destruction). The core decides what a hit does and reports it as events; this
4
+ package draws them on your meshes: vertex dents, bullet-hole and crack decals, char and heat glow, melting and puddles, fragments with light
5
+ physics, loose pieces after a part comes off, and pooled particles (fire, smoke, sparks, dust, shards, splinters, ...).
6
+
7
+ - Nothing is changed in place: geometry and materials are cloned on the first visual change of a mesh, and `restore` / `clear` give the originals back.
8
+ - Two pooled `THREE.Points` (additive and normal blending) draw every particle, with a soft sprite computed in the shader (no texture asset).
9
+ - `three` is a peer dependency used for types only: you pass your own `THREE` instance at runtime.
10
+ - Sounds are not played: you get keys (`impact_metal`, `shatter_glass`, ...) through `onSound`.
11
+
12
+ Contracts live in [`@adatechnology/game-contracts`](../game-contracts); the types are re-exported here, so existing imports keep working.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ pnpm add @adatechnology/game-destruction-three @adatechnology/game-destruction three
18
+ ```
19
+
20
+ Browser without a bundler (the global is `GameDestructionThree`; it bundles the core, so you only need three.js and this file):
21
+
22
+ ```html
23
+ <script src="https://unpkg.com/@adatechnology/game-destruction-three/dist/index.global.js"></script>
24
+ <script>
25
+ const { spec, parts } = GameDestructionThree.specFromObject3D(THREE, model)
26
+ </script>
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```ts
32
+ import * as THREE from 'three'
33
+ import { createDestruction } from '@adatechnology/game-destruction'
34
+ import { createDestructionRenderer, specFromObject3D } from '@adatechnology/game-destruction-three'
35
+
36
+ const scene = new THREE.Scene()
37
+ const world = createDestruction({ seed: 7 })
38
+ const renderer = createDestructionRenderer(THREE, {
39
+ scene,
40
+ world,
41
+ quality: 'medium',
42
+ groundAt: (x, z) => terrain.heightAt(x, z), // default: flat at y = 0
43
+ onPiece: (piece) => grabbable.push(piece),
44
+ onSound: (key, point, volume) => audio.play(key, point, volume),
45
+ })
46
+
47
+ // 1. describe a model: one part per mesh, materials guessed from names, joints inferred from touching parts
48
+ scene.add(chairModel)
49
+ const { spec, parts } = specFromObject3D(THREE, chairModel, { name: 'chair', anchorBelow: 0.01 })
50
+
51
+ // 2. register it in the world and bind it to the model (the world follows chairModel's transform)
52
+ const id = world.add(spec, { position: [2, 0, -3] })
53
+ renderer.bind(id, chairModel, { parts })
54
+
55
+ // 3. hit it, burn it, take it apart
56
+ world.shoot([0, 1.5, 0], [0, 0, -1], 2500, 9)
57
+ world.hit(id, { type: 'fire', point: [2, 0.4, -3], temperature: 900, radius: 0.3, duration: 0.2 })
58
+
59
+ // 4. every frame
60
+ function frame(dt: number) {
61
+ world.update(dt)
62
+ renderer.update(dt)
63
+ }
64
+
65
+ // make a bound object a thrown prop
66
+ const piece = renderer.makePiece(id, { mass: 6 })
67
+ renderer.impulse(id, [0, 40, -120], [2, 0.6, -3])
68
+ ```
69
+
70
+ `bind` finds the meshes of each part from `options.parts` (what `specFromObject3D` returns), else from `userData.destructionPart` or a mesh name that
71
+ equals a part id, else by the part box that contains the mesh centre. `specFromObject3D` reads `userData.destructionMaterial`,
72
+ `destructionPart`, `destructionIgnore`, `destructionTags` and `hollow`.
73
+
74
+ ## What each event draws
75
+
76
+ | Event | Visual | Sound key |
77
+ |---|---|---|
78
+ | `dent` | vertices within `dent.radius` pushed in by `depth * (1 - (d/r)^2)^2` (skipped on `low` for meshes over 20k vertices); sparks on metal | `impact_*` |
79
+ | `hole` | dark disc decal on the surface (and on the exit side of a through hole); a crack star on transparent materials; sparks on metal | `impact_*` |
80
+ | `crack` | star of jagged lines, white on glass, dark otherwise | `impact_*` |
81
+ | `ricochet` | sparks along the reflected direction | `ricochet` |
82
+ | `debris` | a burst of the requested kind (`sparks`, `dust`, `shards`, `splinters`, `chips`, `fibers`, `leaves`, `drips`, `ash`, `smoke`, `steam`) | |
83
+ | `fracture` | part hidden; fragments (boxes, thin irregular prisms for shards) with gravity, ground bounce, settling, sleep and shrink-away; debris burst by style | `shatter_glass`, `crumble`, `splinter`, `impact_*` |
84
+ | `detach` | with `autoSplit`, each loose group becomes its own `Group` (world transforms kept) and a `Piece` (`onPiece`) | `detach` |
85
+ | `disassemble_progress` | | `unscrew` (throttled) |
86
+ | `ignite` / `extinguish` / `burnout` | fire and smoke from the surface while `part.burning`; steam when put out; ash at burnout; char lerps the colour to the material `charColor`; status `ashes` hides the part and leaves an ash pile | `ignite`, `burn_loop`, `extinguish` |
87
+ | heat (`damage` of type heat/fire, or any hot glowing part) | emissive `heatColor(temperature)` for materials with `visual.glows` | |
88
+ | `melt` | meshes sag (Y down to 25 %, a bit wider, noisy vertices, bottom stays put); status `melted` hides the part and leaves a puddle | `melt_sizzle` |
89
+ | `drip` | falling drop that leaves a flat puddle (merged, capped) | |
90
+ | `leak` | stream of drops (or a faint puff for gas) from the hole while the part still has liters | |
91
+ | `explode` | flash, fire, smoke, dust and sparks, a pooled `PointLight` pulse, impulses on pieces and fragments in range | `explode` |
92
+
93
+ Pieces report hard landings: above 3 m/s the renderer calls `world.hit(id, { type: 'blunt', energy: 0.5 m v^2, radius: 0.08, direction: [0, -1, 0], source: 'fall' })`
94
+ (throttled to once per 0.15 s per piece; the object's transform is synced to the world first, so the hit lands on the right spot), so a dropped glass
95
+ pane shatters and a thrown chair can lose a leg. `Piece.localCenter` is the centre of mass relative to the root origin (it moves when a part breaks off).
96
+
97
+ ## Performance
98
+
99
+ | Budget | `medium` / `high` | `low` |
100
+ |---|---|---|
101
+ | Particles (`maxParticles`) | 2500 | 1250 |
102
+ | Particle burst counts | 100 % | 60 % |
103
+ | Fragment meshes (`maxFragments`) | 160 | 80, and every second fragment of an event is skipped |
104
+ | Vertex dents / melt noise on meshes over 20k vertices | yes | skipped |
105
+
106
+ - Particles are two fixed-size ring buffers (additive 40 %, normal 60 %), one draw call each; dead particles cost a loop iteration, nothing else.
107
+ - Fragments share one box geometry, six shard geometries and one material per source material id and colour; they shrink away instead of fading.
108
+ - Decals share one disc geometry and one material; a mesh keeps at most 48 and the renderer 600.
109
+ - Heat, char and melt are only read for "active" parts (hot, burning, melting) plus a scan of all parts every 0.4 s.
110
+ - The explosion light is created on the first explosion (one light in the scene afterwards, intensity 0 at rest) so plan for the one-time shader recompile.
111
+ - Melt assumes upright meshes (it scales the mesh along its own Y).
112
+
113
+ ## API
114
+
115
+ ```ts
116
+ createDestructionRenderer(THREE, options: RendererOptions): DestructionRenderer
117
+ specFromObject3D(THREE, root, options?: SpecFromObjectOptions): { spec, parts }
118
+ heatColor(temperatureC: number): number // 0xRRGGBB, 0 below ~450 C, dark red ~500, orange ~800, yellow ~1100, white from 1400
119
+ ```
120
+
121
+ `RendererOptions`: `scene`, `world`, `quality`, `maxFragments`, `maxParticles`, `fragmentLife`, `groundAt`, `gravity`, `autoSplit`, `onPiece`, `onSound`,
122
+ `explosionLight`, `pointScale` (point size in pixels per metre at 1 m; about `viewportHeight / (2 tan(fov / 2))`, default 700).
123
+
124
+ `DestructionRenderer`: `bind`, `unbind`, `makePiece`, `pieces`, `piece`, `impulse`, `update`, `emit`, `clear`, `restore`, `stats`, `dispose`.
125
+ `clear()` removes fragments, decals, puddles and particles from the scene and restores every bound mesh; `dispose()` also unsubscribes from the world.
126
+
127
+ ## Português
128
+
129
+ Renderizador three.js para o `@adatechnology/game-destruction`. O núcleo decide o que cada golpe faz e devolve eventos; este pacote desenha: amassados
130
+ nos vértices, furos de bala e rachaduras, carbonização e brilho de calor, derretimento e poças, fragmentos com física leve, peças soltas e partículas
131
+ em pool (fogo, fumaça, faíscas, poeira, cacos, lascas). Geometria e materiais só são clonados na primeira mudança visual de uma malha, e `restore` /
132
+ `clear` devolvem os originais. Os sons não são tocados: o jogo recebe chaves (`impact_metal`, `shatter_glass`, ...) em `onSound`.
133
+
134
+ Uso: `specFromObject3D(THREE, modelo)` gera a especificação e o mapa parte -> malhas; registre no mundo com `world.add(spec)`, ligue com
135
+ `renderer.bind(id, modelo, { parts })` e chame `world.update(dt)` e `renderer.update(dt)` a cada quadro. Com `quality: 'low'` os orçamentos de
136
+ partículas e fragmentos caem pela metade.
@@ -0,0 +1,145 @@
1
+ import * as THREE_NS from 'three';
2
+ import { Vec3, DebrisKind, DestructionWorld, MaterialId, DestructibleSpec } from '@adatechnology/game-destruction';
3
+ export { Quat, Vec3 } from '@adatechnology/game-destruction';
4
+
5
+ /**
6
+ * Public types of @adatechnology/game-destruction-three.
7
+ *
8
+ * `three` is only imported as a type: the host passes its own THREE namespace. The renderer listens to a `DestructionWorld` and draws what it
9
+ * reports on the host's meshes: vertex dents, hole and crack decals, char and heat glow through vertex colours / emissive, melting (sag and
10
+ * drips), fragments with light physics, loose pieces after a `detach`, and pooled particles.
11
+ */
12
+
13
+ type ThreeNamespace = typeof THREE_NS;
14
+ type Object3D = THREE_NS.Object3D;
15
+ type Mesh = THREE_NS.Mesh;
16
+ type Quality = 'low' | 'medium' | 'high';
17
+ interface RendererOptions {
18
+ /** Scene that receives fragments, loose pieces, decals and particles. */
19
+ scene: THREE_NS.Scene;
20
+ world: DestructionWorld;
21
+ /** `low` halves particle and fragment budgets and skips vertex dents on meshes over 20k vertices (default `medium`). */
22
+ quality?: Quality;
23
+ /** Upper bound of live fragment meshes (default 160; oldest fade first). */
24
+ maxFragments?: number;
25
+ /** Upper bound of live particles (default 2500). */
26
+ maxParticles?: number;
27
+ /** Seconds a resting fragment stays before it fades (default 12; 0 = forever). */
28
+ fragmentLife?: number;
29
+ /** Ground height at (x, z) for fragments, pieces and drips (default 0). */
30
+ groundAt?: (x: number, z: number) => number;
31
+ gravity?: number;
32
+ /** Turn a loose group into a separate physical piece automatically (default true). */
33
+ autoSplit?: boolean;
34
+ /** Called when a loose piece is created (the bench registers it as grabbable). */
35
+ onPiece?: (piece: Piece) => void;
36
+ /** Called when a sound should play; the host maps keys to audio (e.g. `impact_metal`, `shatter_glass`, `ignite`, `explode`). */
37
+ onSound?: (key: string, point: Vec3, volume: number) => void;
38
+ /** A point light flashes on explosions (default true; costs one light). */
39
+ explosionLight?: boolean;
40
+ /**
41
+ * Particle size scale in pixels per metre at one metre from the camera, roughly `viewportHeight / (2 * tan(fov / 2))` (default 700). Set it
42
+ * when the viewport is much smaller or larger than about 720 px tall so smoke and fire keep their physical size.
43
+ */
44
+ pointScale?: number;
45
+ }
46
+ /** Options of `bind`. */
47
+ interface BindOptions {
48
+ /**
49
+ * Part id -> meshes that draw it. When omitted, every Mesh under the root with `userData.destructionPart` (or whose name equals a part id) is
50
+ * used; meshes with neither are matched to the part whose box contains the mesh's bounding-box centre.
51
+ */
52
+ parts?: Record<string, Object3D | Object3D[]>;
53
+ /** Keep the world transform in sync from the root's matrixWorld every update (default true). */
54
+ syncTransform?: boolean;
55
+ }
56
+ /** A body the renderer moves with its light physics: a loose piece of an object after a detach (or a whole object handed to it). */
57
+ interface Piece {
58
+ /** World object id of this piece. */
59
+ objectId: string;
60
+ root: Object3D;
61
+ velocity: Vec3;
62
+ angularVelocity: Vec3;
63
+ /** Half extents of the local bounding box used for ground contact and grabbing. */
64
+ halfExtents: Vec3;
65
+ /** Centre of mass relative to the root's origin, in the root's local space (metres). It moves when a part breaks off the piece. */
66
+ localCenter: Vec3;
67
+ mass: number;
68
+ sleeping: boolean;
69
+ /** While held by a player the renderer does not integrate it. */
70
+ held: boolean;
71
+ }
72
+ interface DestructionRenderer {
73
+ /** Attach a world object to its three.js root. */
74
+ bind(objectId: string, root: Object3D, options?: BindOptions): void;
75
+ unbind(objectId: string): void;
76
+ /** Make a bound object a physical piece (bench items that can be thrown). */
77
+ makePiece(objectId: string, options?: {
78
+ mass?: number;
79
+ velocity?: Vec3;
80
+ }): Piece;
81
+ pieces(): Piece[];
82
+ piece(objectId: string): Piece | null;
83
+ /** Push a piece (throws, blasts). */
84
+ impulse(objectId: string, impulse: Vec3, point?: Vec3): void;
85
+ /** Integrate fragments, pieces and particles; apply pending visual changes. Call once per frame after `world.update`. */
86
+ update(dt: number): void;
87
+ /** Emit particles directly (muzzle flashes, extinguisher foam, tool sparks). */
88
+ emit(kind: DebrisKind | 'fire' | 'flash' | 'foam', point: Vec3, normal: Vec3, count: number, speed?: number): void;
89
+ /** Remove every fragment, decal and particle, restore every bound mesh to its original geometry and colours. */
90
+ clear(): void;
91
+ /** Restore one object (after `world.reset`). */
92
+ restore(objectId: string): void;
93
+ stats(): {
94
+ fragments: number;
95
+ particles: number;
96
+ pieces: number;
97
+ decals: number;
98
+ };
99
+ dispose(): void;
100
+ }
101
+ interface SpecFromObjectOptions {
102
+ /** Material of a mesh. Default: `userData.destructionMaterial`, else `guessMaterial` on the mesh and material names, else `fallback`. */
103
+ materialOf?: (mesh: Mesh) => MaterialId | undefined;
104
+ /** Material when nothing matches (default `plastic`). */
105
+ fallback?: MaterialId;
106
+ /** Merge meshes smaller than this (largest box side, metres) into the part they touch (default 0.015). */
107
+ minSize?: number;
108
+ /** Infer joints between touching parts (default true). */
109
+ joints?: boolean;
110
+ /** Parts whose box bottom is within this distance of y = 0 in root space are `anchored` (default -1 = none). */
111
+ anchorBelow?: number;
112
+ /** Fill per material for hollow shells (default: sheet metal 0.04 when the box is thin, 1 otherwise). */
113
+ fillOf?: (mesh: Mesh, material: MaterialId) => number | undefined;
114
+ name?: string;
115
+ tags?: string[];
116
+ }
117
+ /** Result of `specFromObject3D`: the spec and which mesh became which part (pass `parts` to `bind`). */
118
+ interface SpecFromObjectResult {
119
+ spec: DestructibleSpec;
120
+ parts: Record<string, Object3D[]>;
121
+ }
122
+
123
+ /** Create the renderer. See `DestructionRenderer`. */
124
+ declare function createDestructionRenderer(THREE: ThreeNamespace, options: RendererOptions): DestructionRenderer;
125
+
126
+ /**
127
+ * Build a `DestructibleSpec` from a three.js object: one part per mesh (meshes sharing `userData.destructionPart` become one part), each an
128
+ * axis-aligned box in the root's space (rotation omitted), with the material taken from `options.materialOf`, `userData.destructionMaterial`
129
+ * or a guess from the names. Tiny meshes are merged into the part they overlap most; joints are inferred from touching parts.
130
+ *
131
+ * Meshes that are invisible or have `userData.destructionIgnore` (or sit under such an object) are skipped. The result also tells which meshes
132
+ * became which part, ready to be passed to `bind`.
133
+ */
134
+ declare function specFromObject3D(THREE: ThreeNamespace, root: Object3D, options?: SpecFromObjectOptions): SpecFromObjectResult;
135
+
136
+ /**
137
+ * Glow colour of a hot surface as `0xRRGGBB`: black (no glow) below about 450 °C, dark red around 500, orange around 800, yellow around 1100 and
138
+ * white from 1400 °C. Brightness never decreases as the temperature rises. Use it as an emissive colour.
139
+ */
140
+ declare function heatColor(temperatureC: number): number;
141
+
142
+ /** Every particle kind the renderer can emit. */
143
+ type ParticleKind = DebrisKind | 'fire' | 'flash' | 'foam';
144
+
145
+ export { type BindOptions, type DestructionRenderer, type Mesh, type Object3D, type ParticleKind, type Piece, type Quality, type RendererOptions, type SpecFromObjectOptions, type SpecFromObjectResult, type ThreeNamespace, createDestructionRenderer, heatColor, specFromObject3D };