@adatechnology/game-contracts 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.
@@ -0,0 +1,702 @@
1
+ /**
2
+ * Destruction contract: the types a game module and a destruction world share. A module (vehicle, furniture, plant, house, weapon) describes
3
+ * itself with these and never needs the default destruction world; the world, the renderer and the test room consume them.
4
+ *
5
+ * Units: metres, kilograms, seconds, joules, degrees Celsius.
6
+ */
7
+ type Vec3 = [number, number, number];
8
+ /** Quaternion `[x, y, z, w]`. */
9
+ type Quat = [number, number, number, number];
10
+ /** snake_case material id, e.g. `steel`, `wood_soft`, `glass`, `concrete`. Part of the saved data contract. */
11
+ type MaterialId = string;
12
+ /** How a part comes apart when it fails. */
13
+ type FractureStyle =
14
+ /** glass, ceramic: sharp flat shards radiating from the impact */
15
+ 'shatter'
16
+ /** concrete, brick, plaster: many small chunks plus dust */
17
+ | 'crumble'
18
+ /** wood: long splinters along the grain (`grain` axis of the part) */
19
+ | 'splinter'
20
+ /** fabric, leather, foliage, paper: torn into flat scraps */
21
+ | 'tear'
22
+ /** plastic, hard wood boards: a few large pieces */
23
+ | 'snap'
24
+ /** metal: never fractures; it dents, bends, gets holes and melts */
25
+ | 'none';
26
+ /** Debris the renderer should emit for an event. */
27
+ type DebrisKind = 'sparks' | 'splinters' | 'dust' | 'shards' | 'fibers' | 'leaves' | 'drips' | 'ash' | 'chips' | 'smoke' | 'steam';
28
+ type PartShape = {
29
+ kind: 'box';
30
+ size: Vec3;
31
+ } | {
32
+ kind: 'cylinder';
33
+ radius: number;
34
+ height: number;
35
+ } | {
36
+ kind: 'sphere';
37
+ radius: number;
38
+ };
39
+ type FluidKind = 'water' | 'fuel' | 'oil' | 'gas' | 'sap' | 'acid' | 'blood' | string;
40
+ interface PartSpec {
41
+ /** Unique inside the object. */
42
+ id: string;
43
+ material: MaterialId;
44
+ /** Shape in object-local space, centred on `position`. Cylinders are along local +Y. */
45
+ shape: PartShape;
46
+ position: Vec3;
47
+ /** Euler angles in radians, order XYZ (default none). */
48
+ rotation?: Vec3;
49
+ /** Mass in kg (default density × volume × `fill`). */
50
+ mass?: number;
51
+ /** Solid fraction of the shape, 0..1 (default 1). A hollow sheet-metal box is ~0.03, a drawer 0.1. */
52
+ fill?: number;
53
+ /** Wall thickness in metres for penetration (default: smallest dimension of the shape × `fill`, at most the smallest dimension). */
54
+ thickness?: number;
55
+ /** Hit points override (default mass × material.hpPerKg). */
56
+ hp?: number;
57
+ /** Axis of the grain for `splinter` materials, in local space (default the longest axis). */
58
+ grain?: 'x' | 'y' | 'z';
59
+ /** Fluid inside the part; punctures make it leak (a `leak` event per hole, the level drops in `update`). */
60
+ contains?: {
61
+ fluid: FluidKind;
62
+ liters: number;
63
+ pressurized?: boolean;
64
+ };
65
+ /** The part explodes when destroyed, or when its temperature passes `atC` (gas bottle, fuel tank, aerosol). */
66
+ explodes?: {
67
+ energy: number;
68
+ radius: number;
69
+ atC?: number;
70
+ };
71
+ /** Fixed to the world (a wall-mounted cabinet, a tree trunk in the ground). Groups that keep an anchored part stay in place. */
72
+ anchored?: boolean;
73
+ /** Free tags the host can use (`door`, `wheel`, `screen`, `trunk`, `leaf`...). */
74
+ tags?: string[];
75
+ /**
76
+ * Structural role. Structural parts (any role but `none`) carry their own mass plus the load of the parts resting on them down to the anchored
77
+ * parts, over the joints that `bears`. `foundation` implies `anchored`. `none` (and parts without `structure`) never carry load: a window, a door
78
+ * or furniture is not part of the structure. Objects without any structural part behave as before.
79
+ */
80
+ structure?: {
81
+ role: StructureRole;
82
+ capacity?: number;
83
+ };
84
+ }
85
+ /**
86
+ * Role of a part in the structure of a building. `partition` is a non-load-bearing wall: it is supported by other parts and carries nothing, so
87
+ * breaking it never drops anything.
88
+ */
89
+ type StructureRole = 'foundation' | 'column' | 'beam' | 'slab' | 'wall' | 'partition' | 'roof' | 'stair' | 'none';
90
+ /** How two parts are attached. Decides the default strength and the tool that undoes it. */
91
+ type JointKind = 'screw' | 'bolt' | 'nail' | 'weld' | 'glue' | 'hinge' | 'clip' | 'tie' | 'mortar' | 'fused' | 'stem' | 'rebar' | 'resting';
92
+ /** A tool that can undo joints (`hand` means no tool). */
93
+ type ToolKind = 'hand' | 'screwdriver' | 'wrench' | 'pry_bar' | 'saw' | 'cutting_torch' | 'knife' | 'hammer';
94
+ interface JointSpec {
95
+ /** Unique inside the object (default `${a}~${b}`). */
96
+ id?: string;
97
+ a: string;
98
+ b: string;
99
+ kind: JointKind;
100
+ /** Joint hit points (default from `JOINT_DEFAULTS[kind].strength`). Impulses and hits next to the joint wear it down. */
101
+ strength?: number;
102
+ /** Tools that undo it (default from `JOINT_DEFAULTS[kind].tools`). An empty list means it can only be broken. */
103
+ tools?: ToolKind[];
104
+ /** Seconds of tool work to undo it (default from `JOINT_DEFAULTS[kind].seconds`). */
105
+ seconds?: number;
106
+ /**
107
+ * The joint transfers load (default true when both parts have a structure role other than `none`, false otherwise). `'a_on_b'` means part `a`
108
+ * rests on part `b` (load flows from `a` to `b` only), `'b_on_a'` the opposite. With `true` the load is shared with the neighbours closer to an
109
+ * anchored part. A part that loses every support path to an anchored part collapses, even if it is still connected sideways to other parts.
110
+ */
111
+ bears?: boolean | 'a_on_b' | 'b_on_a';
112
+ }
113
+ interface DestructibleSpec {
114
+ /** Display name (free text; translated by the host). */
115
+ name?: string;
116
+ parts: PartSpec[];
117
+ /**
118
+ * Joints. When omitted, `inferJoints` is used: touching parts are joined with the kind that fits their materials
119
+ * (see `JOINT_BY_MATERIALS`).
120
+ */
121
+ joints?: JointSpec[];
122
+ /** Free tags (`furniture`, `vehicle`, `plant`, `structure`, `weapon`, `item`...). */
123
+ tags?: string[];
124
+ }
125
+ type DamageType =
126
+ /** bullets and pellets: `energy`, `caliber`; makes holes, dents, cracks, ricochets */
127
+ 'ballistic'
128
+ /** hammers, falls, thrown objects, vehicle impacts: `energy`, `radius` of contact */
129
+ | 'blunt'
130
+ /** axes, knives, machetes: `energy`; cuts and splinters, opens fabric */
131
+ | 'slash'
132
+ /** blasts: `energy`, `radius`; damage and impulse fall off with distance, joints break, fragments fly */
133
+ | 'explosive'
134
+ /** open flame (lighter, molotov, flamethrower): `temperature`, `radius`, `duration`; ignites */
135
+ | 'fire'
136
+ /** heat without flame (cutting torch, hot plate): `temperature`, `radius`, `duration`; heats and melts */
137
+ | 'heat';
138
+ interface Hit {
139
+ type: DamageType;
140
+ /** Point of the hit (world space unless `space: 'local'`). For `explosive` it is the centre of the blast. */
141
+ point: Vec3;
142
+ /** Direction the hit travels in (ballistic, blunt, slash). */
143
+ direction?: Vec3;
144
+ /** Kinetic or blast energy in joules. */
145
+ energy?: number;
146
+ /** Bullet diameter in millimetres (hole size). */
147
+ caliber?: number;
148
+ /** Radius of effect in metres (blast radius, flame radius, blunt contact radius). */
149
+ radius?: number;
150
+ /** Source temperature for `fire` and `heat`. */
151
+ temperature?: number;
152
+ /** Seconds the heat is applied for (a flamethrower calls `hit` every frame with `duration: dt`). */
153
+ duration?: number;
154
+ /** `world` (default) or `local` to the object. */
155
+ space?: 'world' | 'local';
156
+ /** Who caused it (passed through to events). */
157
+ source?: string;
158
+ }
159
+ type PartStatus = 'intact' | 'damaged'
160
+ /** hp reached 0; the part fractured into fragments (it no longer exists as one piece) */
161
+ | 'broken'
162
+ /** melted into a puddle */
163
+ | 'melted'
164
+ /** burned to ash */
165
+ | 'ashes';
166
+ /** A dent in local space of the part's object. */
167
+ interface Dent {
168
+ point: Vec3;
169
+ normal: Vec3;
170
+ radius: number;
171
+ depth: number;
172
+ }
173
+ interface Hole {
174
+ point: Vec3;
175
+ normal: Vec3;
176
+ diameter: number;
177
+ /** The projectile went through. */
178
+ through: boolean;
179
+ }
180
+ interface Crack {
181
+ point: Vec3;
182
+ normal: Vec3;
183
+ /** Length of the longest crack line in metres. */
184
+ length: number;
185
+ /** Number of radial lines. */
186
+ rays: number;
187
+ }
188
+ interface PartState {
189
+ id: string;
190
+ material: MaterialId;
191
+ status: PartStatus;
192
+ hp: number;
193
+ maxHp: number;
194
+ /** Current mass (burning and melting reduce it). */
195
+ mass: number;
196
+ temperature: number;
197
+ burning: boolean;
198
+ /** Burnable mass left, kg. */
199
+ fuel: number;
200
+ /** 0..1, how charred the surface is. */
201
+ char: number;
202
+ /** 0..1, how much of the part has melted. */
203
+ melt: number;
204
+ dents: Dent[];
205
+ holes: Hole[];
206
+ cracks: Crack[];
207
+ /** Fluid left, when the part `contains` one. */
208
+ liters?: number;
209
+ /**
210
+ * Structural parts only. Load in kg: own mass plus what rests on the part. `stress` = `load / capacity`; above 1 the part takes damage every
211
+ * `update` (`(stress - 1) * maxHp * 0.5` per second), which can cascade. Capacity is `structure.capacity`, else `compressive * section` of the
212
+ * material, where the section is the product of the two smallest dimensions (columns, beams, roofs, stairs, foundations x10), thickness times longest dimension (slabs) or
213
+ * thickness times horizontal length (walls). Example: a 0.3 m concrete column carries 40 t, a 0.2 m brick wall 8 t per metre, a 0.1x0.2 m softwood beam 2 t.
214
+ */
215
+ load?: number;
216
+ stress?: number;
217
+ /** Index into `ObjectState.groups` of the piece this part belongs to (-1 when the part is broken, melted or ash). */
218
+ group: number;
219
+ }
220
+ interface JointState {
221
+ id: string;
222
+ a: string;
223
+ b: string;
224
+ kind: JointKind;
225
+ hp: number;
226
+ maxHp: number;
227
+ /** 0..1 of tool work done towards undoing it. */
228
+ progress: number;
229
+ /** Broken or undone. */
230
+ open: boolean;
231
+ /** How it opened. */
232
+ openedBy?: 'break' | 'tool' | 'part_lost';
233
+ }
234
+ interface ObjectState {
235
+ id: string;
236
+ name?: string;
237
+ position: Vec3;
238
+ rotation: Quat;
239
+ parts: PartState[];
240
+ joints: JointState[];
241
+ /**
242
+ * Pieces: connected components of remaining parts over closed joints. Index 0 is the piece that keeps the object's id (the one with an
243
+ * anchored part, else the heaviest). The others are loose pieces the renderer should make into separate bodies.
244
+ */
245
+ groups: string[][];
246
+ /** Every part is broken, melted or ash. */
247
+ destroyed: boolean;
248
+ }
249
+ /** A fragment for the renderer: an oriented box (or shard) with initial motion. Coordinates in world space. */
250
+ interface Fragment {
251
+ id: string;
252
+ part: string;
253
+ material: MaterialId;
254
+ shape: 'chunk' | 'shard' | 'splinter' | 'scrap' | 'slab';
255
+ center: Vec3;
256
+ /** Full size of the fragment box. Shards are thin (`size[2]` is the thickness), splinters long along x. */
257
+ size: Vec3;
258
+ rotation: Quat;
259
+ velocity: Vec3;
260
+ angularVelocity: Vec3;
261
+ mass: number;
262
+ }
263
+ interface EventBase {
264
+ object: string;
265
+ /** Seconds since the world was created. */
266
+ time: number;
267
+ source?: string;
268
+ }
269
+ /** Points in events are in world space. */
270
+ type DestructionEvent = (EventBase & {
271
+ type: 'damage';
272
+ part: string;
273
+ amount: number;
274
+ hp: number;
275
+ damageType: DamageType;
276
+ }) | (EventBase & {
277
+ type: 'dent';
278
+ part: string;
279
+ dent: Dent;
280
+ worldPoint: Vec3;
281
+ }) | (EventBase & {
282
+ type: 'hole';
283
+ part: string;
284
+ hole: Hole;
285
+ worldPoint: Vec3;
286
+ exitPoint?: Vec3;
287
+ }) | (EventBase & {
288
+ type: 'crack';
289
+ part: string;
290
+ crack: Crack;
291
+ worldPoint: Vec3;
292
+ }) | (EventBase & {
293
+ type: 'ricochet';
294
+ part: string;
295
+ point: Vec3;
296
+ direction: Vec3;
297
+ energy: number;
298
+ }) | (EventBase & {
299
+ type: 'debris';
300
+ part: string;
301
+ kind: DebrisKind;
302
+ point: Vec3;
303
+ normal: Vec3;
304
+ count: number;
305
+ speed: number;
306
+ }) | (EventBase & {
307
+ type: 'fracture';
308
+ part: string;
309
+ style: FractureStyle;
310
+ fragments: Fragment[];
311
+ point: Vec3;
312
+ }) | (EventBase & {
313
+ type: 'detach';
314
+ joint: string;
315
+ parts: [string, string];
316
+ by: 'break' | 'tool' | 'part_lost';
317
+ groups: string[][];
318
+ }) | (EventBase & {
319
+ type: 'disassemble_progress';
320
+ joint: string;
321
+ tool: ToolKind;
322
+ progress: number;
323
+ }) | (EventBase & {
324
+ type: 'ignite';
325
+ part: string;
326
+ point: Vec3;
327
+ }) | (EventBase & {
328
+ type: 'extinguish';
329
+ part: string;
330
+ }) | (EventBase & {
331
+ type: 'burnout';
332
+ part: string;
333
+ point: Vec3;
334
+ }) | (EventBase & {
335
+ type: 'melt';
336
+ part: string;
337
+ melt: number;
338
+ }) | (EventBase & {
339
+ type: 'drip';
340
+ part: string;
341
+ point: Vec3;
342
+ material: MaterialId;
343
+ }) | (EventBase & {
344
+ type: 'leak';
345
+ part: string;
346
+ fluid: FluidKind;
347
+ point: Vec3;
348
+ direction: Vec3;
349
+ rate: number;
350
+ pressurized: boolean;
351
+ }) | (EventBase & {
352
+ type: 'explode';
353
+ part: string;
354
+ point: Vec3;
355
+ energy: number;
356
+ radius: number;
357
+ })
358
+ /** A structural part is overloaded (`stress` > 1); throttled to once per 0.5 s per part. Brittle parts also `crack` at stress > 0.8. */
359
+ | (EventBase & {
360
+ type: 'stress';
361
+ part: string;
362
+ stress: number;
363
+ load: number;
364
+ })
365
+ /** A group with structural parts lost every anchored part (an anchored part exists in the object): it falls. Reported once per group. */
366
+ | (EventBase & {
367
+ type: 'collapse';
368
+ parts: string[];
369
+ mass: number;
370
+ }) | (EventBase & {
371
+ type: 'destroyed';
372
+ });
373
+ type DestructionEventType = DestructionEvent['type'];
374
+ /** What a single `hit` did. */
375
+ interface HitResult {
376
+ object: string;
377
+ /** First part the hit landed on, if any. */
378
+ part?: string;
379
+ /** World point where it landed. */
380
+ point?: Vec3;
381
+ /** Damage dealt in total (all parts). */
382
+ damage: number;
383
+ /** Ballistic: the projectile came out the other side with `exitEnergy` left. */
384
+ penetrated: boolean;
385
+ exitEnergy: number;
386
+ events: DestructionEvent[];
387
+ }
388
+ interface RayHit {
389
+ object: string;
390
+ part: string;
391
+ point: Vec3;
392
+ normal: Vec3;
393
+ distance: number;
394
+ }
395
+
396
+ /**
397
+ * What a game module implements for an object whose behaviour it owns (a vehicle with its own panel deformation, a creature, a machine). The
398
+ * world (`DestructionWorld.register`) hosts it next to its own objects: raycast, shoot, explode, hit, disassemble, extinguish, update, state,
399
+ * reset and `on` all route to it.
400
+ *
401
+ * Space: the impl receives hits in WORLD space and returns events in WORLD space (it owns its transform; the transform given to `register` or
402
+ * `setTransform` is only used by the world for raycasts and blast range). Every event must carry `object === id` and name parts listed by
403
+ * `describe()`.
404
+ */
405
+ interface DestructibleImpl {
406
+ readonly id: string;
407
+ readonly name?: string;
408
+ /** Parts, joints and structure in the impl's local space, for raycast, inspector and log. Called often: keep it cheap. */
409
+ describe(): DestructibleSpec;
410
+ /** Apply one hit (see `IMPACT_CONTRACT` for the fields per type) and return the events it caused. */
411
+ hit(hit: Hit): DestructionEvent[];
412
+ /** Advance fire, melting, collapse and other timed behaviour. */
413
+ update?(dt: number): DestructionEvent[];
414
+ state(): ObjectState;
415
+ reset?(): void;
416
+ /** Same contract as `DestructionWorld.disassemble`: progress 0..1, or -1 when the tool cannot undo the joint. */
417
+ disassemble?(jointId: string, tool: ToolKind, dt: number): number;
418
+ extinguish?(partId?: string): void;
419
+ /** Optional: an animation moved a part; `position` / `rotation` (Euler XYZ, radians) are object-local. Keep raycast and hit shapes in sync. */
420
+ setPartPose?(partId: string, position: Vec3, rotation?: Vec3): void;
421
+ /** Plain JSON of the impl's own state, stored in `DestructionSnapshot.impls`. */
422
+ serialize?(): unknown;
423
+ load?(data: unknown): void;
424
+ }
425
+ /** The fields a weapon or hazard must put in a `Hit`, per damage type. THE impact contract. */
426
+ interface ImpactContract {
427
+ required: string[];
428
+ optional: string[];
429
+ note: string;
430
+ }
431
+ declare const IMPACT_CONTRACT: Record<DamageType, ImpactContract>;
432
+ /** Problems with a hit according to the impact contract (empty when it is valid). */
433
+ declare const checkHit: (hit: Hit) => string[];
434
+ interface CheckOptions {
435
+ /**
436
+ * Ids of every material the impl may use (the contracts package does not know the materials of your world: pass the ids of
437
+ * `world.materials`, or the built-in plus your own). When omitted, the material check is skipped.
438
+ */
439
+ materials?: Iterable<string>;
440
+ }
441
+ /**
442
+ * Contract checker for the test room (it needs no world): `describe()` is valid, a 500 J ballistic hit at a part centre returns well-formed events of this impl, `state()`
443
+ * matches `describe()`, hit points never go negative, `update(0.016)` returns an array and every event is JSON-serializable. It hits the impl
444
+ * (and calls `reset()` afterwards when there is one).
445
+ */
446
+ declare const checkDestructible: (impl: DestructibleImpl, options?: CheckOptions) => {
447
+ ok: boolean;
448
+ problems: string[];
449
+ };
450
+
451
+ interface CheckSpecOptions {
452
+ /** Known material ids. When given, every part's material must be one of them. */
453
+ materials?: Iterable<string>;
454
+ }
455
+ interface SpecReport {
456
+ /** False when there is at least one problem (warnings do not count). */
457
+ ok: boolean;
458
+ problems: string[];
459
+ /** Things that are legal but probably wrong, e.g. structural parts that cannot reach an anchored part. */
460
+ warnings: string[];
461
+ }
462
+ /**
463
+ * Static checks on a `DestructibleSpec` (no world needed), for items that give a `spec` instead of an `impl`: unique part ids, known materials
464
+ * (when `options.materials` is given), joints that reference existing, different parts, finite positive sizes, non-negative mass / hp / capacity,
465
+ * valid structure roles and `bears` directions. A structural part with no path of joints to an anchored part is a warning, not a problem: it
466
+ * is allowed, it just falls.
467
+ */
468
+ declare const checkSpec: (spec: DestructibleSpec, options?: CheckSpecOptions) => SpecReport;
469
+
470
+ /**
471
+ * A ballistic shot (one per pellet) as a weapon module produces it. Plain data: any damage model can consume it (`shotToHit` makes the `Hit`).
472
+ */
473
+ interface Shot {
474
+ origin: Vec3;
475
+ /** Unit vector. */
476
+ direction: Vec3;
477
+ /** Muzzle energy in joules. */
478
+ energy: number;
479
+ /** Bullet diameter in millimetres. */
480
+ caliber: number;
481
+ /** snake_case calibre id, e.g. `9x19`. */
482
+ caliberId: string;
483
+ /** Muzzle speed in m/s. */
484
+ speed: number;
485
+ /** Energy at a distance in metres, with the calibre's drag. */
486
+ energyAt(distance: number): number;
487
+ pellet: number;
488
+ /** snake_case weapon id. */
489
+ weapon: string;
490
+ source?: string;
491
+ }
492
+ /** One use of a non-firearm (melee swing, flame tick, heat tick, throw, tool tick, extinguisher tick). */
493
+ type Use = {
494
+ kind: 'melee';
495
+ damage: 'blunt' | 'slash';
496
+ energy: number;
497
+ radius: number;
498
+ reach: number;
499
+ origin: Vec3;
500
+ direction: Vec3;
501
+ } | {
502
+ kind: 'flame';
503
+ temperature: number;
504
+ radius: number;
505
+ range: number;
506
+ cone: boolean;
507
+ duration: number;
508
+ origin: Vec3;
509
+ direction: Vec3;
510
+ } | {
511
+ kind: 'heat';
512
+ temperature: number;
513
+ radius: number;
514
+ range: number;
515
+ duration: number;
516
+ origin: Vec3;
517
+ direction: Vec3;
518
+ } | {
519
+ kind: 'throw';
520
+ energy: number;
521
+ radius: number;
522
+ fuse: number;
523
+ velocity: Vec3;
524
+ origin: Vec3;
525
+ fire?: {
526
+ temperature: number;
527
+ radius: number;
528
+ seconds: number;
529
+ };
530
+ } | {
531
+ kind: 'tool';
532
+ tool: string;
533
+ reach: number;
534
+ duration: number;
535
+ origin: Vec3;
536
+ direction: Vec3;
537
+ } | {
538
+ kind: 'extinguish';
539
+ range: number;
540
+ coolC: number;
541
+ duration: number;
542
+ origin: Vec3;
543
+ direction: Vec3;
544
+ } | {
545
+ kind: 'hand';
546
+ reach: number;
547
+ origin: Vec3;
548
+ direction: Vec3;
549
+ };
550
+ /**
551
+ * The `Hit` a ballistic shot makes at `point` (world space): the energy is what is left of the shot at that distance from its origin
552
+ * (`shot.energyAt`), the direction and calibre come from the shot. `normal` (the surface normal at the point, when the caller knows it) does not
553
+ * change the hit; it is accepted so that callers can pass what their raycast returned and so that a stricter implementation could use it.
554
+ */
555
+ declare const shotToHit: (shot: Shot, point: Vec3, normal?: Vec3) => Hit;
556
+
557
+ /**
558
+ * Fluid contract: what a module says about the liquid its item holds, and how the item is used. Types only: the liquids themselves, their
559
+ * simulation and their rendering live in `@adatechnology/game-fluids` (optional).
560
+ */
561
+ /** snake_case liquid id: `water`, `coffee`, `milk`, `soda`, `beer`, `fuel`, `diesel`, `oil`, `blood`, `sap`, `acid`, `paint`... */
562
+ type LiquidId = string;
563
+ interface ContainerSpec {
564
+ /** Inner capacity in litres. */
565
+ capacity: number;
566
+ /** Liquid it starts with (default none). */
567
+ liquid?: LiquidId;
568
+ liters?: number;
569
+ temperature?: number;
570
+ /**
571
+ * Inner shape in the container's local space, used for the surface height and the spill point. Origin at the inner bottom centre, +Y up.
572
+ * `cylinder` (cups, cans, bottles approximated) or `box` (jerrycans, tanks, sinks).
573
+ */
574
+ shape: {
575
+ kind: 'cylinder';
576
+ radius: number;
577
+ height: number;
578
+ } | {
579
+ kind: 'box';
580
+ size: [number, number, number];
581
+ };
582
+ /** Opening at the top: radius in metres (a bottle neck is 0.012, a cup 0.04). Pour rate grows with it. 0 = closed (needs `open()`). */
583
+ opening: number;
584
+ /** Starts closed (bottle cap, jerrycan lid). */
585
+ closed?: boolean;
586
+ /** The container is insulated (thermos): cools 10× slower. */
587
+ insulated?: boolean;
588
+ }
589
+ /**
590
+ * A scripted use of a held item (drink a coffee, pour water, eat, wipe). Pure data with time-keyed poses so a test room or a game can play it
591
+ * and check what happened. The host maps poses to its hand/viewmodel animation.
592
+ */
593
+ interface ItemUseDef {
594
+ /** snake_case: `drink`, `sip`, `pour`, `fill_from_tap`, `pour_out`, `eat`, `wipe`... */
595
+ id: string;
596
+ /** Seconds. */
597
+ duration: number;
598
+ /** Keyframes: time 0..duration, the hand-relative position/rotation (Euler XYZ) of the item and the container action during that key. */
599
+ keys: Array<{
600
+ t: number;
601
+ position: Vec3;
602
+ rotation: Vec3;
603
+ action?: 'drink' | 'pour' | 'open' | 'close' | 'none';
604
+ rate?: number;
605
+ }>;
606
+ /** Only valid when the container holds one of these (empty = any). */
607
+ liquids?: LiquidId[];
608
+ }
609
+
610
+ /** Pose of one part (or of the whole item) at a key: position offset in metres and Euler XYZ rotation in radians, both relative to the rest pose. */
611
+ interface AnimationPose {
612
+ position?: Vec3;
613
+ rotation?: Vec3;
614
+ }
615
+ interface AnimationKey {
616
+ /** Seconds from the start of the clip. */
617
+ t: number;
618
+ /** Pose of the item's root. */
619
+ position?: Vec3;
620
+ rotation?: Vec3;
621
+ /** Poses of named parts: the key is a destruction part id (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */
622
+ parts?: Record<string, AnimationPose>;
623
+ }
624
+ interface AnimationEventSpec {
625
+ /** Seconds from the start of the clip. */
626
+ t: number;
627
+ /** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */
628
+ type: string;
629
+ data?: Record<string, unknown>;
630
+ }
631
+ /** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */
632
+ interface AnimationClipSpec {
633
+ /** snake_case: `reload`, `open`, `drink`... Unique within the item. */
634
+ id: string;
635
+ /** Seconds. */
636
+ duration: number;
637
+ keys: AnimationKey[];
638
+ events?: AnimationEventSpec[];
639
+ }
640
+ /** What an item exposes when it brings its own animations. */
641
+ interface ItemAnimationContract {
642
+ animations: AnimationClipSpec[];
643
+ }
644
+ /**
645
+ * Problems with a clip (empty when it is valid): a snake_case id, a positive finite duration, at least one key, key and event times inside
646
+ * `[0, duration]` and not going backwards, finite vectors, every named part one of `partIds`, event types that are non-empty strings and
647
+ * JSON-serializable event data.
648
+ */
649
+ declare const checkAnimation: (clip: AnimationClipSpec, partIds: Iterable<string>) => {
650
+ ok: boolean;
651
+ problems: string[];
652
+ };
653
+
654
+ /** An item that holds a liquid (a cup, a bottle, a fuel tank). `part` is the destruction part that is the container; a hole in it makes it leak. */
655
+ interface ModuleItemFluid {
656
+ container: ContainerSpec;
657
+ part?: string;
658
+ /** Item uses offered while held (`drink`, `pour`...). */
659
+ uses?: string[];
660
+ offset?: Vec3;
661
+ }
662
+ /** One thing a module can put in the test room (or any viewer): a toy car, a chair, a pistol on a table. */
663
+ interface TestableItem {
664
+ /** snake_case id, unique inside the module. */
665
+ id: string;
666
+ /** Display name, already translated for the locale passed to `TestableModule.items`. */
667
+ name: string;
668
+ /**
669
+ * Builds the 3D object: `THREE` is the host's three.js namespace, the result is its `Object3D` (origin at the centre of the footprint, on the
670
+ * floor, front facing +z). The contracts package does not depend on three.js, so both are `unknown` here; the host narrows them.
671
+ */
672
+ build: (THREE: unknown) => unknown;
673
+ /** Destruction spec for the default destruction world. A module that owns its destruction gives `impl` instead (or as well). */
674
+ spec?: DestructibleSpec | ((root: unknown) => DestructibleSpec);
675
+ /** The module's own destruction: parts, hits and state are its business. The world hosts it next to its own objects. `id` is the object id the host gives it: `DestructibleImpl.id` must be that id. */
676
+ impl?: (root: unknown, id: string) => DestructibleImpl;
677
+ fluid?: ModuleItemFluid;
678
+ /** The item is also a weapon the player can equip: an id of the arsenal in use. */
679
+ weapon?: string;
680
+ /** Use animations this item brings (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */
681
+ animations?: AnimationClipSpec[];
682
+ /** Can be grabbed and thrown. */
683
+ movable?: boolean;
684
+ }
685
+ /** A scripted check a module brings: the host calls `run` with its own context (the test room passes a `ScenarioContext`). */
686
+ interface ModuleScenario<Context = unknown> {
687
+ id: string;
688
+ name: string;
689
+ run(context: Context): void | Promise<void>;
690
+ }
691
+ /** What a module hands the test room: its items and, optionally, scenarios that exercise them. */
692
+ interface TestableModule<Context = unknown> {
693
+ /** snake_case id of the module. */
694
+ id: string;
695
+ name: string;
696
+ /** Catalogue tab the items go under. */
697
+ group: string;
698
+ items(locale: string): TestableItem[];
699
+ scenarios?(locale: string): Array<ModuleScenario<Context>>;
700
+ }
701
+
702
+ export { type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type CheckOptions, type CheckSpecOptions, type ContainerSpec, type Crack, type DamageType, type DebrisKind, type Dent, type DestructibleImpl, type DestructibleSpec, type DestructionEvent, type DestructionEventType, type FluidKind, type FractureStyle, type Fragment, type Hit, type HitResult, type Hole, IMPACT_CONTRACT, type ImpactContract, type ItemAnimationContract, type ItemUseDef, type JointKind, type JointSpec, type JointState, type LiquidId, type MaterialId, type ModuleItemFluid, type ModuleScenario, type ObjectState, type PartShape, type PartSpec, type PartState, type PartStatus, type Quat, type RayHit, type Shot, type SpecReport, type StructureRole, type TestableItem, type TestableModule, type ToolKind, type Use, type Vec3, checkAnimation, checkDestructible, checkHit, checkSpec, shotToHit };