@adatechnology/game-fluids 0.0.0-stage → 0.1.1

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,177 @@
1
- # Temporary Holding Version
1
+ # @adatechnology/game-fluids
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
+ Engine-agnostic liquids for games, with no renderer. It knows nothing about any game: the host moves the containers, gives it surfaces and calls
4
+ `update(dt)`; the package answers with events and plain state that any renderer (see `@adatechnology/game-fluids-three`) can draw.
5
+
6
+ - **Containers** (cup, bottle, jerrycan, tank) with a level computed from their shape and tilt, a temperature (cools towards ambient, steams when
7
+ hot), an opening and an optional lid. Fill, pour, drink, spill over the lip when tilted, leak through holes.
8
+ - **Streams**: the jet leaving a lip or a hole. Ballistic, it ends on the first surface it hits (planes with half extents) or on the ground.
9
+ - **Stains**: grow from a flow front (16 radial samples, never a perfect disc), run downhill, soak into porous surfaces, dry from the edge in and
10
+ either vanish or leave a permanent mark (coffee, blood, oil).
11
+ - **19 built-in liquids** (`LIQUIDS`): water, coffee, tea, milk, soda, juice, beer, wine, spirits, fuel, diesel, motor_oil, cooking_oil, blood,
12
+ sap, acid, paint, bleach, honey. Each one has its own colour, viscosity, drying time, gloss and drink effects (`caffeine`, `alcohol`, `poison`,
13
+ `disgust`).
14
+ - **Item uses** (`ITEM_USES`): scripted `sip`, `drink`, `chug`, `pour`, `pour_out`, `open` and `close`, as pure keyframe data plus a player.
15
+ - Deterministic from a seed (no `Math.random`), so a LAN host can run it and send events. `serialize()` / `load()` round trip the whole world.
16
+ - Units: metres, litres, seconds, degrees Celsius. Locale table `en` / `pt-BR` for liquid names and event labels.
17
+
18
+ Contracts live in [`@adatechnology/game-contracts`](../game-contracts); the types are re-exported here, so existing imports keep working.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ pnpm add @adatechnology/game-fluids
24
+ ```
25
+
26
+ Browser without a bundler (the global is `GameFluids`):
27
+
28
+ ```html
29
+ <script src="https://unpkg.com/@adatechnology/game-fluids/dist/index.global.js"></script>
30
+ <script>
31
+ const world = GameFluids.createFluids({ seed: 1 })
32
+ </script>
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ```ts
38
+ import { createFluids, createItemUse, ITEM_USES } from '@adatechnology/game-fluids'
39
+
40
+ const fluids = createFluids({ seed: 42, locale: 'en', onEvent: (e) => console.log(fluids.describe(e)) })
41
+
42
+ fluids.addSurface({ id: 'floor', point: [0, 0, 0], normal: [0, 1, 0], halfSize: [10, 10], porosity: 0.05 })
43
+ fluids.addSurface({ id: 'rug', point: [2, 0.01, 0], normal: [0, 1, 0], halfSize: [1, 1], porosity: 0.9 })
44
+
45
+ const cup = fluids.addContainer(
46
+ {
47
+ capacity: 0.25,
48
+ liquid: 'coffee',
49
+ liters: 0.25,
50
+ temperature: 90,
51
+ shape: { kind: 'cylinder', radius: 0.04, height: 0.1 }, // inner shape, origin at the inner bottom centre, +Y up
52
+ opening: 0.04,
53
+ },
54
+ { id: 'cup', position: [0, 0.8, 0] },
55
+ )
56
+
57
+ // every frame: keep the container in sync with the object that carries it, then step
58
+ fluids.setTransform(cup, object.position, object.quaternion, velocity)
59
+ for (const event of fluids.update(dt)) {
60
+ if (event.type === 'drink' && event.effects.includes('caffeine')) player.energy += event.thirst * 0.1
61
+ }
62
+ ```
63
+
64
+ Events from direct calls (`drink`, `fill`, `puncture`, `splash`...) are delivered once: to the listeners at once and, for `drink`, as its return value; `update` never repeats them. `update` returns the events it produced, in order, and also fires `onEvent` and the listeners registered with
65
+ `fluids.on(type | '*', fn)`. Use `fluids.describe(event)` for a localised one-liner, handy for a debug panel or a log.
66
+
67
+ Draw from the state, never from the events: `container(id)`, `containers()`, `streams()` and `stains()` return plain copies.
68
+
69
+ ### Spilling, pouring and holes
70
+
71
+ Tilt the container (`setTransform` with a rotated quaternion): the liquid surface keeps its volume and stays horizontal; when it rises above the
72
+ lowest point of the lip, liquid spills over it with a rate proportional to the excess and the opening (Torricelli, divided by the square root of the
73
+ viscosity), creating a `spill` stream. A closed container never spills or pours. `fluids.pour(id, dt, into?)` forces flow out of the opening for
74
+ `dt` seconds (optionally into another container). `puncture(id, point, normal, diameter)` makes a hole: it leaks (one `leak` stream per hole) while
75
+ the liquid is above it and stops once the level falls below.
76
+
77
+ ### Taps
78
+
79
+ A tap is a fixed source (a sink, a pipe, a hose end):
80
+
81
+ ```ts
82
+ const sink = fluids.addTap({ position: [0, 1.0, -2], liquid: 'water', rate: 0.1 }) // direction [0, -1, 0], on by default
83
+ fluids.setTap(sink, false) // off; fluids.setTap(sink, true, 0.2) turns it on at 0.2 l/s
84
+ fluids.removeTap(sink)
85
+ ```
86
+
87
+ While on it is a `tap` stream (`streams()` lists it with `tap: <id>`): it fills any container whose opening it lands in (the same logic as pouring
88
+ into a cup) and otherwise makes a stain where it lands. Switching emits `{ type: 'tap', tap, on }`. `taps()` returns the state of every tap, and
89
+ taps are part of `serialize` / `load`.
90
+
91
+ ### Using it with `@adatechnology/game-destruction`
92
+
93
+ The damage model reports `leak` events for parts that hold a fluid. Map them one to one onto `puncture`; the container then loses liquid by
94
+ Torricelli and the stream and stains follow:
95
+
96
+ ```ts
97
+ import type { DamageEvent } from '@adatechnology/game-destruction'
98
+
99
+ destruction.on('leak', (e) => {
100
+ // e.part is the damaged part, e.point the world hit point of the hole, e.direction where the fluid leaves
101
+ const id = containerOfPart(e.part)
102
+ if (!id) return
103
+ const local = worldToContainer(id, e.point) // container space: origin at the inner bottom centre
104
+ const normal = worldDirToContainer(id, e.direction)
105
+ fluids.puncture(id, local, normal, 0.009) // hole diameter in metres (e.g. a 9 mm bullet)
106
+ })
107
+ ```
108
+
109
+ Keep `fluids.setTransform` in sync with the destructible object so the hole follows it. A fuel tank punctured near the bottom leaks until the level
110
+ falls under the hole; the fuel stain (`flammable` liquid) can be handed to the fire system by the host.
111
+
112
+ ### Item uses
113
+
114
+ ```ts
115
+ const use = createItemUse(fluids, 'cup', ITEM_USES.find((u) => u.id === 'sip')!, () => ({ position: handPos, rotation: handQuat }))
116
+ // every frame
117
+ for (const event of use.update(dt)) log(fluids.describe(event))
118
+ viewmodel.position.set(...use.pose().position) // hand-relative pose, Euler XYZ for the rotation
119
+ if (use.done) /* ... */
120
+ ```
121
+
122
+ | id | duration | what it does |
123
+ |---|---|---|
124
+ | `sip` | 1.2 s | raise to the mouth, small tilt, drinks 0.03 l |
125
+ | `drink` | 2.5 s | raise, tilt about 70 degrees, drinks about 0.2 l, back down |
126
+ | `chug` | 4 s | long tilt, drinks until empty or 0.5 l |
127
+ | `pour` | 3 s | tilt 100 degrees outward, `pour` action |
128
+ | `pour_out` | 4 s | empties the container on the floor |
129
+ | `open` / `close` | 1 s | removes / puts the lid |
130
+
131
+ While the current key has `action: 'drink'` the player calls `world.drink(rate × dt)` (litres per second in `rate`); with `'pour'` it calls
132
+ `world.pour(dt × rate)`; `'open'` and `'close'` fire once when their key is reached. If a `getHandTransform` callback is given, the container follows
133
+ hand ∘ pose; otherwise the host animates its own viewmodel from `pose()`. `def.liquids` restricts the use to some liquids. Write your own
134
+ `ItemUseDef` to add `eat`, `wipe` and so on.
135
+
136
+ ## API
137
+
138
+ | Export | What it is |
139
+ |---|---|
140
+ | `createFluids(options?)` | Creates a `FluidWorld`. Options: `liquids`, `locale`, `seed`, `ambientC`, `gravity`, `groundAt(x, z)`, `mergeDistance`, `maxStains`, `onEvent` |
141
+ | `LIQUIDS` | The 19 built-in `LiquidDef`s (`id`, `color`, `opacity`, `stainColor`, `viscosity`, `density`, `dryTime`, `leavesMark`, `gloss`, `drink`...) |
142
+ | `ITEM_USES` | The built-in `ItemUseDef`s |
143
+ | `createItemUse(world, containerId, def, getHandTransform?)` | Returns an `ItemUsePlayer` (`pose()`, `update(dt)`, `cancel()`, `time`, `done`) |
144
+ | `sampleStream(stream, gravity, t)` | Point of a stream's path at time `t` (0 to `flightTime`) |
145
+ | `stainDirection(stain, i)`, `STAIN_SAMPLES` | World direction of the front sample `i` of a stain (16 samples) |
146
+ | `describeEvent(strings, event)`, `resolveStrings(locale)`, `locales` | Localised event text and liquid names |
147
+
148
+ `FluidWorld` methods: `addContainer`, `removeContainer`, `setTransform`, `open`, `close`, `fill`, `pour`, `drink`, `puncture`, `heat`, `addSurface`,
149
+ `removeSurface`, `addTap`, `setTap`, `removeTap`, `taps`, `splash`, `clearStains`, `update`, `container`, `containers`, `streams`, `stains`, `on`, `describe`, `serialize`, `load`, plus
150
+ `liquids` and `gravity`.
151
+
152
+ Events (`FluidEvent.type`): `fill`, `pour`, `spill`, `leak`, `drink`, `empty`, `stream_end`, `splash`, `stain`, `dried`, `steam`, `mix`, `tap`.
153
+
154
+ Notes: liquid ids and event types are snake_case data ids and part of the public contract. Colours in `liquids.json` are `0xRRGGBB` stored as
155
+ integers; density is in kg/m³ and viscosity is relative to water. The surface id `ground` is reserved for the implicit ground (`groundAt`).
156
+
157
+ ## Português
158
+
159
+ Líquidos sem renderizador, para qualquer engine: copos, garrafas, galões e tanques com nível calculado pelo formato e pela inclinação,
160
+ temperatura (esfria e solta vapor), beber, despejar, derramar ao inclinar e vazar por furos (ligue os eventos `leak` do
161
+ `@adatechnology/game-destruction` em `fluids.puncture`). O líquido que cai vira mancha: cresce a partir de uma frente de fluxo (nunca um disco
162
+ perfeito), escorre ladeira abaixo, é absorvido por superfícies porosas e seca de fora para dentro, deixando marca permanente (café, sangue, óleo)
163
+ ou sumindo (água). Determinístico por semente, pronto para LAN.
164
+
165
+ ```ts
166
+ const fluids = createFluids({ seed: 1, locale: 'pt-BR' })
167
+ const use = createItemUse(fluids, 'copo', ITEM_USES.find((u) => u.id === 'drink')!)
168
+ for (const e of use.update(dt)) console.log(fluids.describe(e)) // "bebeu: copo café 0.01 l [caffeine]"
169
+ ```
170
+
171
+ Com `locale: 'pt-BR'`, `describe` usa os nomes em português ("água", "café", "chá", "leite", "refrigerante", "suco", "cerveja", "vinho",
172
+ "destilado", "gasolina", "diesel", "óleo de motor", "óleo de cozinha", "sangue", "seiva", "ácido", "tinta", "água sanitária", "mel") e os rótulos
173
+ dos eventos ("bebeu", "derramou", "vazando", "encheu", "esvaziou", "mancha", "secou"...). Os usos de item prontos são `sip`, `drink`, `chug`,
174
+ `pour`, `pour_out`, `open` e `close`.
175
+
176
+ Torneiras: `fluids.addTap({ position, liquid: 'water', rate: 0.1 })` cria uma fonte fixa (pia, cano); `setTap(id, on, rate?)` liga e desliga e
177
+ `removeTap(id)` remove. Ligada, é um fluxo `tap` que enche o recipiente em que cai ou deixa uma mancha, e emite o evento `tap`.
@@ -0,0 +1,353 @@
1
+ import { LiquidId, Vec3, ContainerSpec, Quat, ItemUseDef } from '@adatechnology/game-contracts';
2
+ export { ContainerSpec, ItemUseDef, LiquidId, Quat, Vec3 } from '@adatechnology/game-contracts';
3
+
4
+ /**
5
+ * Public types of @adatechnology/game-fluids.
6
+ *
7
+ * Renderer-free liquids. Three things:
8
+ * - Containers: a cup, a bottle, a jerrycan, a fuel tank, a water tank. They hold one liquid with a level and a temperature, know their opening
9
+ * and their tilt, and can be filled, poured, drunk from, spilled (tilted past the lip) or leak through holes (from a damage model).
10
+ * - Streams: liquid leaving a container or a hole. Each stream is a falling jet that ends on a surface.
11
+ * - Stains: what a stream or a spill leaves on a surface. A stain grows from a flow front (never a perfect disc), soaks into porous surfaces,
12
+ * and dries from the edge in. Each liquid is its own material (water, coffee, fuel, oil, blood...).
13
+ *
14
+ * Units: metres, litres, seconds, °C. Deterministic from the seed (a LAN host can run it and send events).
15
+ */
16
+
17
+ interface LiquidDef {
18
+ id: LiquidId;
19
+ /** Colour `0xRRGGBB` and opacity 0..1 of the body of liquid. */
20
+ color: number;
21
+ opacity: number;
22
+ /** Colour of a wet/dried stain (default darker `color`). */
23
+ stainColor?: number;
24
+ /** Viscosity relative to water (water 1, oil 60, honey 3000). Slows streams and stain spread, thickens drops. */
25
+ viscosity: number;
26
+ density: number;
27
+ /** Seconds a thin stain takes to dry at 20 °C (water 600, fuel 120, oil never = 0). */
28
+ dryTime: number;
29
+ /** Leaves a permanent mark after drying (coffee, blood, oil, paint). */
30
+ leavesMark?: boolean;
31
+ flammable?: boolean;
32
+ /** Gloss of a wet surface 0..1 (water 0.9, oil 1, milk 0.5). */
33
+ gloss: number;
34
+ /** Steams when hotter than this (default 60 for drinks). */
35
+ steamC?: number;
36
+ /** Drinkable: thirst points per litre (positive) and any effect key the host maps (e.g. `caffeine`, `alcohol`, `poison`). */
37
+ drink?: {
38
+ thirst: number;
39
+ effects?: string[];
40
+ };
41
+ tags?: string[];
42
+ }
43
+ interface ContainerState {
44
+ id: string;
45
+ liquid: LiquidId | null;
46
+ liters: number;
47
+ capacity: number;
48
+ temperature: number;
49
+ closed: boolean;
50
+ /** Height of the liquid surface above the inner bottom along world up, metres (with tilt). */
51
+ surfaceHeight: number;
52
+ /** Liquid surface plane in world space (for the renderer): a point and the normal (always world up while still; sloshes when moved). */
53
+ surfacePoint: Vec3;
54
+ surfaceNormal: Vec3;
55
+ /** Holes from damage, in local space. */
56
+ holes: Array<{
57
+ point: Vec3;
58
+ normal: Vec3;
59
+ diameter: number;
60
+ }>;
61
+ /** Flowing out now (litres per second) through the opening (pour/spill) and holes. */
62
+ outflow: number;
63
+ steaming: boolean;
64
+ /** Inner shape and effective lip radius (metres), so a renderer can draw the liquid body without knowing the spec. */
65
+ shape?: ContainerSpec['shape'];
66
+ lipRadius?: number;
67
+ /** World transform the container was last given with `setTransform`. */
68
+ position?: Vec3;
69
+ rotation?: Quat;
70
+ }
71
+ type StreamCause = 'pour' | 'spill' | 'leak' | 'tap';
72
+ /** A fixed water source (a tap, a pipe, a hose end) that runs a stream while it is on. See `FluidWorld.addTap`. */
73
+ interface TapSpec {
74
+ /** Id (default `tap_<n>`). Adding a tap with an id that already exists replaces it. */
75
+ id?: string;
76
+ /** World position of the nozzle. */
77
+ position: Vec3;
78
+ /** Direction the liquid leaves in (normalised; default straight down `[0, -1, 0]`). */
79
+ direction?: Vec3;
80
+ liquid: LiquidId;
81
+ /** Litres per second while on (a kitchen tap is about 0.1). */
82
+ rate: number;
83
+ /** Temperature of the liquid in °C (default the ambient temperature). */
84
+ temperature?: number;
85
+ /** Starts running (default `true`). */
86
+ on?: boolean;
87
+ }
88
+ interface TapState {
89
+ id: string;
90
+ position: Vec3;
91
+ direction: Vec3;
92
+ liquid: LiquidId;
93
+ rate: number;
94
+ temperature: number;
95
+ on: boolean;
96
+ /** Id of the `tap` stream while it runs. */
97
+ stream?: string;
98
+ }
99
+ interface StreamState {
100
+ id: string;
101
+ liquid: LiquidId;
102
+ cause: StreamCause;
103
+ container?: string;
104
+ /** The tap that feeds it (`cause: 'tap'`). */
105
+ tap?: string;
106
+ /** World start point and initial velocity. */
107
+ origin: Vec3;
108
+ velocity: Vec3;
109
+ /** Litres per second. */
110
+ rate: number;
111
+ /** Where it lands (null while still in the air or nothing below). */
112
+ landing: Vec3 | null;
113
+ /** Thin trickle (drops) or a jet. */
114
+ thin: boolean;
115
+ /** Seconds the jet flies before it lands (capped when it lands nowhere). Sample the path with `sampleStream`. */
116
+ flightTime?: number;
117
+ }
118
+ interface SurfaceSpec {
119
+ id: string;
120
+ /** Horizontal rectangle (floors, tables) or any plane: a point, a normal and its half extents along two axes. */
121
+ point: Vec3;
122
+ normal: Vec3;
123
+ /** Half extents along `tangent` and normal × tangent. */
124
+ halfSize: [number, number];
125
+ tangent?: Vec3;
126
+ /** 0 = sealed (tile, glass, metal), 1 = soaks fast (fabric, soil, wood). Soaking shrinks the stain area and darkens it. */
127
+ porosity: number;
128
+ }
129
+ interface StainState {
130
+ id: string;
131
+ surface: string;
132
+ liquid: LiquidId;
133
+ /** Centre in world space and the flow front: radii per direction (16 samples around the centre) so the outline is irregular. */
134
+ center: Vec3;
135
+ front: number[];
136
+ /** Litres on the surface (pooled) and soaked. */
137
+ liters: number;
138
+ soaked: number;
139
+ /** 0..1: 1 = wet, 0 = dry; a dried `leavesMark` stain stays as a mark. */
140
+ wetness: number;
141
+ /** The wet area recedes from the edge in: radius fraction still wet. */
142
+ wetRadius: number;
143
+ /** Surface normal and tangent of the plane the stain lies on; `front[i]` points along `cos(2πi/16)·tangent + sin(2πi/16)·(normal × tangent)`. */
144
+ normal?: Vec3;
145
+ tangent?: Vec3;
146
+ /** Unit direction (in the surface plane) the liquid arrived from moving towards, when it landed with horizontal speed (the impact shows where it came from). */
147
+ flow?: Vec3;
148
+ /** True once the wet part dried and only the permanent mark of a `leavesMark` liquid remains. */
149
+ mark?: boolean;
150
+ /** Seconds since the stain was created. */
151
+ age?: number;
152
+ }
153
+ type FluidEvent = {
154
+ type: 'fill';
155
+ container: string;
156
+ liquid: LiquidId;
157
+ liters: number;
158
+ } | {
159
+ type: 'pour';
160
+ container: string;
161
+ liquid: LiquidId;
162
+ rate: number;
163
+ stream: string;
164
+ } | {
165
+ type: 'spill';
166
+ container: string;
167
+ liquid: LiquidId;
168
+ rate: number;
169
+ stream: string;
170
+ } | {
171
+ type: 'leak';
172
+ container: string;
173
+ liquid: LiquidId;
174
+ rate: number;
175
+ stream: string;
176
+ point: Vec3;
177
+ } | {
178
+ type: 'drink';
179
+ container: string;
180
+ liquid: LiquidId;
181
+ liters: number;
182
+ temperature: number;
183
+ thirst: number;
184
+ effects: string[];
185
+ burn: boolean;
186
+ } | {
187
+ type: 'empty';
188
+ container: string;
189
+ } | {
190
+ type: 'stream_end';
191
+ stream: string;
192
+ } | {
193
+ type: 'splash';
194
+ liquid: LiquidId;
195
+ point: Vec3;
196
+ intensity: number;
197
+ } | {
198
+ type: 'stain';
199
+ stain: string;
200
+ surface: string;
201
+ liquid: LiquidId;
202
+ point: Vec3;
203
+ } | {
204
+ type: 'dried';
205
+ stain: string;
206
+ mark: boolean;
207
+ } | {
208
+ type: 'steam';
209
+ container: string;
210
+ on: boolean;
211
+ } | {
212
+ type: 'mix';
213
+ container: string;
214
+ from: LiquidId;
215
+ into: LiquidId;
216
+ }
217
+ /** A tap was switched on or off (`setTap`, `addTap` with `on`). */
218
+ | {
219
+ type: 'tap';
220
+ tap: string;
221
+ on: boolean;
222
+ };
223
+ interface FluidOptions {
224
+ /** Extra or replacing liquid definitions (matched by `id`) on top of the built-in `LIQUIDS`. */
225
+ liquids?: LiquidDef[];
226
+ /** Locale of the strings returned by `describe` (`en` default, `pt-BR`; unknown locales fall back to `en`). */
227
+ locale?: string;
228
+ seed?: number;
229
+ ambientC?: number;
230
+ gravity?: number;
231
+ /** Ground height for streams that hit no surface (default 0). */
232
+ groundAt?: (x: number, z: number) => number;
233
+ /** Merge stains closer than this (default 0.15 m). Upper bound of live stains (default 64; oldest dry ones go first). */
234
+ mergeDistance?: number;
235
+ maxStains?: number;
236
+ onEvent?: (event: FluidEvent) => void;
237
+ }
238
+ interface FluidWorld {
239
+ readonly liquids: ReadonlyMap<LiquidId, LiquidDef>;
240
+ /** Gravity in m/s² used by streams (so a renderer can sample the same path). */
241
+ readonly gravity: number;
242
+ /** Human readable one-liner of an event in the configured locale, for logs and debug panels. */
243
+ describe(event: FluidEvent): string;
244
+ addContainer(spec: ContainerSpec, options?: {
245
+ id?: string;
246
+ position?: Vec3;
247
+ rotation?: Quat;
248
+ }): string;
249
+ removeContainer(id: string): void;
250
+ /** Keep in sync with the object that carries it (tilt decides spilling and the surface plane; velocity makes it slosh). */
251
+ setTransform(id: string, position: Vec3, rotation: Quat, velocity?: Vec3): void;
252
+ open(id: string): void;
253
+ close(id: string): void;
254
+ /** Add liquid (from a tap, another container); mixing different liquids keeps the bigger one and emits `mix`. Returns litres accepted. */
255
+ fill(id: string, liquid: LiquidId, liters: number, temperature?: number): number;
256
+ /** Pour on purpose for `dt` seconds (tilts the flow out of the opening even when not tilted far). Returns litres out. */
257
+ pour(id: string, dt: number, into?: string): number;
258
+ /** Drink `liters` (sip ~0.03 l); returns the drink event or null when empty/closed. Too hot (> 65 °C) sets `burn`. */
259
+ drink(id: string, liters: number): Extract<FluidEvent, {
260
+ type: 'drink';
261
+ }> | null;
262
+ /** Punch a hole (local space) — call it from a damage model's `hole`/`leak` events. */
263
+ puncture(id: string, point: Vec3, normal: Vec3, diameter: number): void;
264
+ /** Heat or cool the content (a stove under a kettle, a fire). */
265
+ heat(id: string, deltaC: number): void;
266
+ addSurface(spec: SurfaceSpec): void;
267
+ removeSurface(id: string): void;
268
+ /**
269
+ * Add a tap: while `on` it runs a `tap` stream (cause `'tap'`) from `position` along `direction` at `rate` litres per second. The stream fills any
270
+ * container whose opening it lands in (the same logic as a pour) and otherwise makes a stain on the surface below. Returns the tap id.
271
+ * Switching on or off emits a `tap` event.
272
+ */
273
+ addTap(spec: TapSpec): string;
274
+ /** Switch a tap on or off, optionally changing its `rate`. Unknown ids are ignored. */
275
+ setTap(id: string, on: boolean, rate?: number): void;
276
+ /** Remove a tap (and end its stream). */
277
+ removeTap(id: string): void;
278
+ taps(): TapState[];
279
+ /** Pour liquid straight onto a point (a broken bottle, a blood splash). */
280
+ splash(liquid: LiquidId, point: Vec3, liters: number, velocity?: Vec3): void;
281
+ /** Wipe or burn stains in a radius (fire dries water and burns fuel stains). */
282
+ clearStains(point: Vec3, radius: number): void;
283
+ update(dt: number): FluidEvent[];
284
+ container(id: string): ContainerState | null;
285
+ containers(): ContainerState[];
286
+ streams(): StreamState[];
287
+ stains(): StainState[];
288
+ on(type: FluidEvent['type'] | '*', fn: (event: FluidEvent) => void): () => void;
289
+ serialize(): unknown;
290
+ load(snapshot: unknown): void;
291
+ }
292
+ interface ItemUsePlayer {
293
+ readonly def: ItemUseDef;
294
+ readonly time: number;
295
+ readonly done: boolean;
296
+ /** Current hand-relative pose (interpolated). */
297
+ pose(): {
298
+ position: Vec3;
299
+ rotation: Vec3;
300
+ };
301
+ /** Advance; applies the container actions to the fluid world and returns the events it produced. */
302
+ update(dt: number): FluidEvent[];
303
+ cancel(): void;
304
+ }
305
+
306
+ declare function createFluids(options?: FluidOptions): FluidWorld;
307
+
308
+ /**
309
+ * Built-in liquids (data in `data/liquids.json`; colours are `0xRRGGBB` stored as integers).
310
+ * Viscosity is relative to water, density in kg/m³, `dryTime` in seconds for a thin film at 20 °C (0 = never dries).
311
+ */
312
+ declare const LIQUIDS: LiquidDef[];
313
+
314
+ /** Built-in scripted uses of a held item (data in `data/item-uses.json`): `sip`, `drink`, `chug`, `pour`, `pour_out`, `open`, `close`. */
315
+ declare const ITEM_USES: ItemUseDef[];
316
+ /** World transform of the hand (or any holder) the item pose is relative to. */
317
+ type HandTransform = () => {
318
+ position: Vec3;
319
+ rotation: Quat;
320
+ };
321
+ /**
322
+ * Plays an item use on a container: interpolates the keys, applies `drink` (rate × dt litres) and `pour` (`world.pour`, `rate` multiplies the
323
+ * time) while the current key has that action, and fires `open` / `close` once when their key is reached. When `getHandTransform` is given, the
324
+ * container's world transform follows hand ∘ pose every update; otherwise the host animates the viewmodel from `pose()`.
325
+ */
326
+ declare function createItemUse(world: FluidWorld, containerId: string, def: ItemUseDef, getHandTransform?: HandTransform): ItemUsePlayer;
327
+
328
+ /** Locales shipped with the package. Unknown locales fall back to `en`. */
329
+ type FluidLocale = 'en' | 'pt-BR';
330
+ interface FluidStrings {
331
+ /** Display name per liquid id (unknown ids fall back to the id itself). */
332
+ liquids: Record<LiquidId, string>;
333
+ /** Short label per event type. */
334
+ events: Record<FluidEvent['type'], string>;
335
+ on: string;
336
+ off: string;
337
+ }
338
+ declare const locales: Record<FluidLocale, FluidStrings>;
339
+ declare const resolveStrings: (locale: string | undefined) => FluidStrings;
340
+ /** One-line description of an event for logs and debug panels, e.g. `drank: coffee 0.03 l`. */
341
+ declare const describeEvent: (strings: FluidStrings, event: FluidEvent) => string;
342
+
343
+ /** Number of radial samples of the flow front. */
344
+ declare const SAMPLES = 16;
345
+ /** Unit direction (world space) of front sample `i` of a stain, from its `normal` and `tangent`. */
346
+ declare const stainDirection: (s: Pick<StainState, "normal" | "tangent">, i: number) => Vec3;
347
+
348
+ /** Position of a ballistic jet `t` seconds after leaving its origin. */
349
+ declare const ballistic: (origin: Vec3, velocity: Vec3, gravity: number, t: number) => Vec3;
350
+ /** Point of a stream's path `t` seconds after its origin (use with `StreamState.flightTime` and the world's `gravity`). */
351
+ declare const sampleStream: (stream: Pick<StreamState, "origin" | "velocity">, gravity: number, t: number) => Vec3;
352
+
353
+ export { type ContainerState, type FluidEvent, type FluidLocale, type FluidOptions, type FluidStrings, type FluidWorld, type HandTransform, ITEM_USES, type ItemUsePlayer, LIQUIDS, type LiquidDef, SAMPLES as STAIN_SAMPLES, type StainState, type StreamCause, type StreamState, type SurfaceSpec, type TapSpec, type TapState, ballistic, createFluids, createItemUse, describeEvent, locales, resolveStrings, sampleStream, stainDirection };