@adatechnology/game-test-room 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,13 +18,13 @@ pnpm add @adatechnology/game-test-room three
18
18
 
19
19
  ```ts
20
20
  import * as THREE from 'three'
21
- import { createTestRoom, demoItems } from '@adatechnology/game-test-room'
21
+ import { createTestRoom } from '@adatechnology/game-test-room'
22
22
 
23
23
  const room = createTestRoom({
24
24
  THREE,
25
25
  container: document.getElementById('app')!,
26
26
  locale: 'en', // 'en' or 'pt-BR'
27
- catalog: demoItems('en'),
27
+ catalog: [], // the room ships no items: load modules (`room.loadModule`) or pass your own
28
28
  })
29
29
  room.runScenarios().then((report) => console.log(report.passed, report.failed))
30
30
  console.log(room.log.toNDJSON())
@@ -39,7 +39,6 @@ const room = createTestRoom({
39
39
  THREE,
40
40
  container,
41
41
  catalog: [
42
- ...demoItems('en'),
43
42
  { id: 'my_crate', name: 'My crate', group: 'Mine', build: (T) => buildMyCrate(T), specOptions: { /* material per mesh name */ } },
44
43
  ],
45
44
  scenarios: [
@@ -56,8 +55,48 @@ const room = createTestRoom({
56
55
  })
57
56
  ```
58
57
 
59
- `DEMO_MATERIALS` is now empty (kept for compatibility: the demo items use only core materials). `buildFromSpec` turns a `DestructibleSpec` into meshes,
60
- and `brickWallSpec` / `DEMO_ITEMS` / `SCENARIOS` are exported to reuse or extend.
58
+ `buildFromSpec` turns a `DestructibleSpec` into meshes, and `brickWallSpec` (the arena's target wall) is exported to reuse.
59
+
60
+ ## Breaking: the demo catalogue and the built-in scenarios are gone
61
+
62
+ The room used to ship 19 procedural box/cylinder items (`demoItems`, `DEMO_ITEMS`, `DEMO_MATERIALS`: chair, sofa, crate, jerrycan...) and its built-in scenarios (`SCENARIOS`, `scenariosFor`, `makeScenarios`) built on them. They were not the game's items, so they were removed: the room now holds only the generic contracts and the arena. Migrate:
63
+
64
+ - `catalog: demoItems(locale)` becomes `catalog: []`, and the items come from the SDK modules loaded with `room.loadModule` (weapons, items, household furniture, vehicles, characters...). `examples/interactables-test-area` loads all of them; `examples/test-room` opens with that set.
65
+ - `room.scenarios` is empty until the host passes `scenarios` or a module brings its own. The destruction checks the removed ones made (steel sheet, glass, concrete, fire, collapse) are covered by the tests of `game-destruction`, `game-fluids` and the module scenarios of the content packages.
66
+ - A test that needs a throwaway item should define it in its own fixture (see `tests/fixtures/demo-items.ts`, not exported).
67
+
68
+ ## Scenery
69
+
70
+ The arena holds three destructible pieces by default, hand-made from boxes: `target_wall` (a brick wall of 48 panels at z = -11), `table` (with the water tap on a post beside it, at x = 3.5) and `shelf` (x = -3.5). `scenery` is a registry keyed by arena object id, so a host can use its own pieces without touching the room:
71
+
72
+ | `scenery[id]` | Effect |
73
+ |---|---|
74
+ | a key of a default (`target_wall`, `table`, `shelf`) | replaces that piece; `position` defaults to the replaced one's |
75
+ | `null` | removes the piece (no table: no tap either) |
76
+ | a new key | adds a piece; `position` is required |
77
+
78
+ ```ts
79
+ createTestRoom({ ..., scenery: {
80
+ target_wall: { spec: wallSpec, position: [0, 0, -11], build: (THREE) => drawWall(THREE) },
81
+ table: { spec: tableSpec, build: drawTable, top: { part: 'wood_0', y: 0.78 } },
82
+ crate: { spec: crateSpec, position: [0, 0, -4], rotationY: 0.4, label: 'Crate' },
83
+ shelf: null,
84
+ } })
85
+ ```
86
+
87
+ A `SceneryObject` is `{ spec, position?, rotationY?, colors?, build?, label?, top? }`:
88
+
89
+ - `spec`: the `DestructibleSpec` the world is given. The player collider is the footprint of its parts (what lies lower than 10 cm is walked over); the `target_wall` collider also closes the arena behind the wall.
90
+ - `build(THREE)`: draws the piece as a group whose meshes are named by part id (`userData.destructionPart` too), like `buildFromSpec` does. Without it the room draws one plain mesh per part with `buildFromSpec` and `colors`.
91
+ - `top: { part, y }`: loose things rest on that part's footprint at height `y` while the part stands (`room.groundAt`), and liquids pool on it. The tap stands beside the `table` top.
92
+ - `spout`: where a water tap of the piece pours (a sink, a tub), in the spec's space. The room's tap pours from the first piece that has one, so the piece's own model is the tap; without one the tap stands on the `table` top (see `tap` below).
93
+ - `label`: the name in the inspector and the log (the defaults are translated).
94
+
95
+ `tap` is the room's water tap: omitted, the room draws a post with an arm beside the table; `{ build(THREE), spout, position?, rotationY? }` puts the host's model there (the origin where it stands, `spout` where the water leaves it; by default it stands on the table top with the spout over its right half); `null` removes it.
96
+
97
+ ## Models of thrown and placed explosives
98
+
99
+ The room knows the arsenal's ids, not the weapons' models. `projectileModel({ weapon, explosive, kind })` (`kind`: `grenade`, `molotov`, `rocket` or `charge`) is the host's hook: it answers `{ model, axis? }` (`axis`: the length of the model, default +y; a rocket points it along its flight) or `null`, and the room draws a plain primitive (a sphere, a cylinder, a box) only where it answers nothing. `@adatechnology/game-weapons-three` has one: `projectileModel(THREE, query)`.
61
100
 
62
101
  ## Loading a module
63
102
 
@@ -67,7 +106,17 @@ A game module hands the room a `TestableModule` from `@adatechnology/game-contra
67
106
  const report = room.loadModule(RENTAL_CARS) // { module, items, scenarios, ok, problems }
68
107
  ```
69
108
 
70
- The items join the catalogue under the module's group and its scenarios become buttons. The contract checks run at once and write one line per result to the log: `checkDestructible` for every item with an `impl`, `checkAnimation` for every clip (`✓ Rental car: destructible contract ok (4 parts...)` or `✗ ...: <problem>`). The animations panel lists the clips of the aimed or held item; `room.playAnimation(clipId)` plays one (poses are offsets from the rest pose, events go to the log and `sound` events to `onSound`). The example (`examples/test-room`) loads a `DEMO_MODULE` rental car that owns its damage rule, its door `open` clip and a wheel that comes off with a wrench.
109
+ The items join the catalogue under the module's group and its scenarios become buttons. The contract checks run at once and write one line per result to the log: `checkDestructible` for every item with an `impl`, `checkAnimation` for every clip (`✓ Rental car: destructible contract ok (4 parts...)` or `✗ ...: <problem>`). The animations panel lists the clips of the aimed or held item; `room.playAnimation(clipId)` plays one (poses are offsets from the rest pose, events go to the log and `sound` events to `onSound`). The example (`examples/test-room`) loads the SDK's content modules (the same set as `examples/interactables-test-area`).
110
+
111
+ ## Loading indicator
112
+
113
+ Anything slow shows a spinner or a bar with a line of text over the room, so the page never looks frozen. `room.loading` (`RoomLoading`) is the component and `room.busy(label, work)` its shortcut: `busy` shows the indicator, waits for it to be painted (a synchronous job cannot paint it itself), runs `work` and takes it down, even when `work` throws. For a job in steps use `const task = room.loading.begin(label, { total })`, `task.step(text, done)`, `await task.paint()` and `task.end()`; tasks nest and the last one to end hides the card. `await room.loading.idle()` resolves when no task is open: the way for a scenario or a test to wait for a rebuild that runs behind the indicator. `room.openAreaAsync(area)` is `openArea` behind it. The text follows the room's `locale`, a room with `ui: false` draws nothing and tells the state through `room.loading.on(...)`.
114
+
115
+ A module declares what is slow and the room does the rest: `ModuleControl.slow` (an action or a control that takes a third of a second or more) and `TestableItem.slow` (a vehicle) make the room's own panel and catalogue run the press or the spawn behind the indicator (`bench().act/set` and `spawn` stay synchronous for scenarios). A module with a job of its own (a house rebuilt after a seed change) calls `host.loading.busy(...)`. `LOADING_CSS` and `loadingMarkup` are the same card as static HTML, for a page that must show it before its scripts run (the default test room's first frame does).
116
+
117
+ ## Blasts shove what is around them
118
+
119
+ An `explosion` moves the loose items, the heavy furniture that is not fixed, and the bodies of the modules that answer it. One rule, `blastShove` of `@adatechnology/game-contracts`: `min(maxSpeed, sqrt(2 * energy * coupling / mass)) * (1 - d / r)^2` away from the centre with some lift, and nothing under `minSpeed`. The room's `blastShove` option sets the numbers (`coupling` 0.02, `maxSpeed` 20 m/s, `lift` 0.3, `minSpeed` 0.3, `falloffPower` 2) for the renderer's pieces and, as `host.blastShove`, for modules such as the characters area, which slide and knock their people down.
71
120
 
72
121
  ## Module bench
73
122
 
@@ -116,11 +165,33 @@ when a body (the player, an object of the room) crosses it; whatever was already
116
165
  | E | grab / drop |
117
166
  | R | rotate the held item |
118
167
  | Right button or G | throw |
119
- | F | use a fluid item (drink); Z tilt; C fill from the tap; V pour (holding a fluid item) or kick (otherwise, with the `kick` tool whatever is selected) |
168
+ | F | use a fluid item (drink); Z tilt; C fill from the tap; V pour (holding a fluid item); otherwise V toggles 1st/3rd person when a body is set (`setAvatar`), and kicks when there is none (Shift+V kicks with a body too; the `kick` tool whatever is selected) |
120
169
  | Tab | show / hide the panels |
121
170
 
122
171
  Panels: catalogue tabs (spawn, Clear, Reset all), slow motion 0.25x, time x10, quality select, inspector (material, state, hp, temperature, char, marks, joints, load and stress) and the behaviour log with filters.
123
172
 
173
+ ## The player's body and the third-person camera
174
+
175
+ The room is a bare first-person camera until you give it a body. `room.setAvatar(avatar | null)` takes anything shaped like `RoomAvatar` (the room imports no character code):
176
+
177
+ ```ts
178
+ interface RoomAvatar {
179
+ root: Object3D
180
+ update(dt: number, state: { speed: number; running: boolean; crouch: boolean; down: boolean; armed: boolean; pitch: number }): void
181
+ hand?(side: 'R' | 'L'): Object3D
182
+ firstPerson?: { root: Object3D; update(dt: number, state: { camera: Object3D; targets: HandTargets | null; stance: string; speed: number; running: boolean; crouch: boolean }): void }
183
+ strike?(clipId: string): number | null
184
+ }
185
+ ```
186
+
187
+ Every frame the room puts `root` at the player's feet turned to where the player looks (the body's front is +z, as the catalogue items'; `rotation.y` is the yaw plus pi) and calls `update` with the ground speed of the player's movement, Shift, `armed` (a firearm is the tool in hand) and the camera `pitch` (radians, positive looking up). `crouch` and `down` are not known to the room: a player module sets them with `room.setAvatarState({ crouch, down })` (`avatarState()` reads them back). `@adatechnology/game-characters-three` has such a body: `toRoomAvatar(createPlayerAvatar(THREE))`.
188
+
189
+ **First-person arms and strikes.** An avatar may carry `firstPerson` (arms drawn in the camera's space; the room shows them only in the first person, updates them every frame after the modules' frame hooks and hands them where the tool in the hands asks the hands to be) and `strike(clipId)`. The hand targets come from the module that places the tool (`host.setHandTargets(source)`, `HandTargets` of `@adatechnology/game-contracts`: world-space poses of the right and left hand plus the grip closure and the size of the hands), else from the carried item, else null; the stance is `hold` when there are targets, else `handStances[toolId]` (default `{ fists: 'fists' }`) or `relaxed`. A melee strike (`fists`, `shove`, Shift+V kick) no longer lands at the click: the room calls `avatar.strike(strikeClips[toolId])` (default `{ fists: 'punch_right', shove: 'shove_two_hands', kick: 'front_kick' }`), which plays the clip on the arms and on the body and answers the seconds to its impact, writes a `strike` line to the log, and calls the existing `meleeUse` at that second with the aim read again. A tool without a clip, or an avatar that answers null, keeps the instant blow; a strike pending blocks the same tool. `toRoomAvatar(createPlayerAvatar(THREE))` already gives all of it (`firstPerson: false` for no arms).
190
+
191
+ `room.setCameraMode('first' | 'third')` (option `cameraMode`, property `cameraMode`, event `room.on('camera', fn)`; every change is a `camera` action in the log). V toggles it when a body is set. In `first` the body is hidden; in `third` it is shown, the camera stands over the shoulder, and a carried item rides `avatar.hand('R')` instead of the camera frame. Option `thirdPerson` changes the view; the defaults are the team game's shoulder camera (`zombiada-modo-livre.html`, `cameraUpdate`, line 76546): `back` 3.8 m, `side` 0.9 m to the right, `up` 1.95 m (`crouchUp` 1.4 m), `lift` 0.1 m, three probe rays (`probes` 0 and +-0.2 m) that pull the camera in `margin` 0.3 m short of a wall (never closer than `minDistance` 0.2 m) and let it out at `recover` 4 per second.
192
+
193
+ The simulation always sees the eye: tools, grabs, scripted walks and the listener work as in first person. Only the render moves the camera (`applyCamera()` does it, a host that renders by itself calls it around its `render`). A shot or grab leaves the eye toward the point the camera ray hits, so the crosshair stays true.
194
+
124
195
  ## Log format
125
196
 
126
197
  One JSON object per line (`log.toNDJSON()`, "Baixar NDJSON"):
@@ -150,6 +221,8 @@ automáticos conferem o resultado e gravam aprovado/reprovado no mesmo registro.
150
221
  Um módulo que declara controles, camadas ou contadores ganha um painel de bancada genérico; o estado vai para a URL do exemplo (`?bench.<idDoMódulo>=`) e
151
222
  `room.bench()` / `room.metrics()` dirigem e leem a bancada por script.
152
223
 
224
+ Corpo do jogador: `room.setAvatar(corpo)` põe um corpo (qualquer objeto no formato `RoomAvatar`) nos pés do jogador; `room.setCameraMode('first' | 'third')` e a tecla V alternam entre 1ª e 3ª pessoa (câmera sobre o ombro com os números do jogo da equipe, puxada para perto de paredes). `room.setAvatarState({ crouch, down })` deixa um módulo de jogador agachar e derrubar o corpo. Com corpo, V alterna a câmera e Shift+V chuta. Em primeira pessoa o corpo pode trazer braços e mãos (`avatar.firstPerson`) que seguram a arma onde o módulo manda (`host.setHandTargets`): mão direita no punho, esquerda no guarda-mão, as duas na pistola, esquerda ao carregador na recarga; sem nada na mão ficam relaxadas, e `fists` levanta os punhos. Soco, empurrão e chute tocam o clipe (`avatar.strike`, opção `strikeClips`) e o efeito só acontece no instante do impacto, não no clique; um golpe pendente bloqueia a mesma ferramenta.
225
+
153
226
  ## Known limitations
154
227
 
155
228
  The quality select does not recreate the destruction renderer; it only changes the room's own settings. Unit tests use an injected stub renderer (no WebGL).
package/dist/index.d.ts CHANGED
@@ -1,14 +1,54 @@
1
1
  import * as THREE_NS from 'three';
2
2
  import { AudioEngine } from '@adatechnology/game-audio';
3
- import { DestructibleSpec, Vec3, DestructionWorld, BlastOptions, DestructionOptions, DestructionEvent, MaterialDef } from '@adatechnology/game-destruction';
3
+ import { Vec3, DestructionWorld, BlastOptions, DestructibleSpec, DestructionOptions, DestructionEvent } from '@adatechnology/game-destruction';
4
4
  export { DestructionEvent, Vec3 } from '@adatechnology/game-destruction';
5
- import { SpecFromObjectOptions, DestructionRenderer, Quality } from '@adatechnology/game-destruction-three';
5
+ import { BusySink, HandTargets, ModuleControl, ControlValue, ModuleLayer, EnvironmentBus, BusyTask, BusyOptions, TestableModule, DestructibleImpl, AnimationClipSpec, BlastShoveOptions } from '@adatechnology/game-contracts';
6
+ export { AnimationClipSpec, DestructibleImpl, ItemAnimationContract, ModuleScenario, TestableItem, TestableModule } from '@adatechnology/game-contracts';
7
+ import { DestructionRenderer, SpecFromObjectOptions, Quality } from '@adatechnology/game-destruction-three';
6
8
  import { Arsenal, WeaponState, WeaponDef } from '@adatechnology/game-weapons';
7
9
  import { VfxWorld } from '@adatechnology/game-vfx';
8
10
  import { FilmBridge, FluidRenderer } from '@adatechnology/game-fluids-three';
9
- import { ContainerSpec, FluidWorld, LiquidDef, ItemUseDef, SurfaceSpec } from '@adatechnology/game-fluids';
10
- import { ModuleControl, ControlValue, ModuleLayer, DestructibleImpl, AnimationClipSpec, EnvironmentBus, TestableModule } from '@adatechnology/game-contracts';
11
- export { AnimationClipSpec, DestructibleImpl, ItemAnimationContract, ModuleScenario, TestableItem, TestableModule } from '@adatechnology/game-contracts';
11
+ import { FluidWorld, ContainerSpec, LiquidDef, ItemUseDef, SurfaceSpec } from '@adatechnology/game-fluids';
12
+
13
+ /** What the indicator shows now: the label of the task on top, its line of text, and how far it is (the bar) when it knows. */
14
+ interface LoadingState {
15
+ readonly label: string;
16
+ readonly text: string;
17
+ readonly done: number;
18
+ readonly total: number | undefined;
19
+ /** The task hides what is behind the indicator. */
20
+ readonly opaque: boolean;
21
+ /** Tasks open at once (an area that builds a house inside a slow action counts two). */
22
+ readonly depth: number;
23
+ }
24
+ /**
25
+ * The loading indicator of the room: an overlay with a spinner or a bar and a line of text, shown for any slow job so that the page never looks frozen. `begin` shows it at once, `busy` also waits for the
26
+ * paint before it runs the work (the only way to show a bar before a heavy synchronous job), and the last task to end takes it down. The room lends it to modules as `ModuleHost.loading`.
27
+ */
28
+ interface RoomLoading extends BusySink {
29
+ /** True while a task is open. */
30
+ readonly active: boolean;
31
+ /** Resolves once no task is open (at once when none is): a scenario or test awaits it after a change that rebuilds behind the indicator, instead of sleeping. */
32
+ idle(): Promise<void>;
33
+ /** What it shows now, or undefined when nothing is open. */
34
+ state(): LoadingState | undefined;
35
+ /** Follows it: called whenever a task opens, steps or ends, with `undefined` once none is left. Returns the function that stops following. */
36
+ on(listener: (state: LoadingState | undefined) => void): () => void;
37
+ /** The overlay element (undefined in a room with `ui: false`, where the page draws its own from `on`). */
38
+ readonly element: HTMLElement | undefined;
39
+ dispose(): void;
40
+ }
41
+ interface LoadingOptions {
42
+ /** The room's container: the overlay covers it. */
43
+ readonly container: HTMLElement;
44
+ /** False for a room with `ui: false`: the state is kept and told through `on`, nothing is drawn. Default true. */
45
+ readonly draw?: boolean;
46
+ /** Text of a task that has none yet (already translated). */
47
+ readonly working: string;
48
+ }
49
+ /** Resolves once the browser has painted a frame (a frame callback, then a timer), or after a short wait when frames are not running (a hidden tab, a test). */
50
+ declare const nextPaint: () => Promise<void>;
51
+ declare function createLoading(options: LoadingOptions): RoomLoading;
12
52
 
13
53
  type NewEntry = NewLogEntry;
14
54
  /** The log the room writes to (a `TestRoomLog` plus the write side). */
@@ -26,7 +66,7 @@ declare function createLog(options?: LogOptions): WritableLog;
26
66
  /** Locales shipped with the package. Unknown locales fall back to `en`. */
27
67
  type TestRoomLocale = 'en' | 'pt-BR';
28
68
  interface RoomStrings {
29
- /** Display name per demo item id (see `DEMO_ITEMS`). */
69
+ /** Display name per item id the room itself names (none: the items come from the modules). */
30
70
  items: Record<string, string>;
31
71
  /** Display name per demo catalogue group. */
32
72
  groups: Record<string, string>;
@@ -107,6 +147,98 @@ interface HudState {
107
147
  };
108
148
  quality: string;
109
149
  objects: number;
150
+ /** The view when the room has a body (`null`: no body, no shortcut). */
151
+ camera: 'first' | 'third' | null;
152
+ }
153
+
154
+ /**
155
+ * The player's body in the room and the third-person camera. Generic: the room knows an avatar only by this interface (a body from any package
156
+ * that fits it, such as `createPlayerAvatar` of `@adatechnology/game-characters-three`), and no character code is imported here.
157
+ */
158
+
159
+ /** What the room tells the avatar every frame. */
160
+ interface RoomAvatarState {
161
+ /** Ground speed of the player, m/s (from the movement of the feet, jumps ignored). */
162
+ speed: number;
163
+ /** Shift held. */
164
+ running: boolean;
165
+ crouch: boolean;
166
+ down: boolean;
167
+ /** A firearm is the tool in hand. */
168
+ armed: boolean;
169
+ /** Camera pitch, radians, positive looking up (the room's own convention). */
170
+ pitch: number;
171
+ }
172
+ /** What the room tells the first-person arms every frame, after the modules have placed the tool in the hands. */
173
+ interface RoomFirstPersonState {
174
+ /** The player's eye: the arms are drawn in its space. */
175
+ camera: THREE_NS.Object3D;
176
+ /** Where the tool in the hands asks them to be (world space): the module's, else the carried item's, else null. */
177
+ targets: HandTargets | null;
178
+ /** `hold` when something is held (targets), else the rest stance of the tool in hand (`relaxed`, `fists`...; `RoomOptions.handStances`). */
179
+ stance: string;
180
+ /** How far the index finger of the shooting hand is curled, 0 extended along the frame to 1 pulled on the trigger; null when no tool asks for it. */
181
+ triggerFinger: number | null;
182
+ speed: number;
183
+ running: boolean;
184
+ crouch: boolean;
185
+ /** The distance along a ray (world space) to the first thing in the room, or null: what the arms use to stop at a wall or a wardrobe. */
186
+ contact(origin: [number, number, number], direction: [number, number, number], maxDistance: number): number | null;
187
+ }
188
+ /** A body the room can show. The room puts `root` at the player's feet turned to where the player looks: the body's front is +z (as the catalogue items'), so `rotation.y` is the player's yaw plus pi. */
189
+ interface RoomAvatar {
190
+ root: THREE_NS.Object3D;
191
+ update(dt: number, state: RoomAvatarState): void;
192
+ /** A socket at a hand: the carried item follows `hand('R')` in third person. */
193
+ hand?(side: 'R' | 'L'): THREE_NS.Object3D;
194
+ /**
195
+ * The arms of the first-person view. The room shows `root` only in first person (the body only in third), updates it every frame after the modules'
196
+ * frame hooks (so the tool in the hands is already placed) and before the render.
197
+ */
198
+ firstPerson?: {
199
+ root: THREE_NS.Object3D;
200
+ update(dt: number, state: RoomFirstPersonState): void;
201
+ };
202
+ /**
203
+ * Plays the strike `clipId` (on the body and on the arms) and answers the seconds until its impact, or null when this avatar has no such clip. The room then waits that long
204
+ * before the blow lands (`strikeClips`); an avatar without `strike`, or one that answers null, gets the blow at the click.
205
+ */
206
+ strike?(clipId: string, options?: {
207
+ leg?: 'left' | 'right';
208
+ }): number | null;
209
+ }
210
+ /** Which leg a kick uses: `alternate` swaps at each kick (right first). */
211
+ type KickLeg = 'alternate' | 'left' | 'right';
212
+ type CameraMode = 'first' | 'third';
213
+ /** Which clip a melee tool plays before its blow lands (`TestRoomOptions.strikeClips`). */
214
+ declare const DEFAULT_STRIKE_CLIPS: Readonly<Record<string, string>>;
215
+ /** The first-person stance of the tools that choose one (`TestRoomOptions.handStances`). */
216
+ declare const DEFAULT_HAND_STANCES: Readonly<Record<string, string>>;
217
+ /**
218
+ * The over-the-shoulder view, metres. Defaults are the numbers of the team game's shoulder camera
219
+ * (`site/pages/jogo/zombiada-modo-livre.html`, `cameraUpdate`, line 76546): `view.cb` 3.8 back, `view.cs` 0.9 to the side, `view.cu` 1.95 high
220
+ * (1.4 crouched), the `+ 0.1` lift of the camera at line 76551, and three probe rays (centre and +-0.2 m, line 76557) that pull the camera in
221
+ * 0.3 m short of a wall (`h.t - 0.3`, never closer than 0.2 m) and let it out again slowly (`dt * 4`, line 76558).
222
+ */
223
+ interface ThirdPersonOptions {
224
+ /** Distance behind the pivot. Default 3.8. */
225
+ back?: number;
226
+ /** Pivot offset to the right of the player (negative: left shoulder). Default 0.9. */
227
+ side?: number;
228
+ /** Pivot height above the feet. Default 1.95. */
229
+ up?: number;
230
+ /** Pivot height above the feet while crouched. Default 1.4. */
231
+ crouchUp?: number;
232
+ /** Extra height of the camera over the line from the pivot. Default 0.1. */
233
+ lift?: number;
234
+ /** How far short of a wall the camera stops. Default 0.3. */
235
+ margin?: number;
236
+ /** Closest the camera comes to the pivot. Default 0.2. */
237
+ minDistance?: number;
238
+ /** Sideways offsets of the probe rays. Default `[0, 0.2, -0.2]`. */
239
+ probes?: readonly number[];
240
+ /** Per second rate at which the camera returns after a wall is gone. Default 4. */
241
+ recover?: number;
110
242
  }
111
243
 
112
244
  /**
@@ -138,6 +270,8 @@ interface TestRoomItem {
138
270
  movable?: boolean;
139
271
  /** Short note shown in the inspector. */
140
272
  note?: string;
273
+ /** Building it takes a noticeable time (a vehicle): the catalogue shows the loading indicator while it spawns one. `true`, or the text to show. */
274
+ slow?: boolean | string;
141
275
  /**
142
276
  * The item holds a liquid (a cup of coffee, a bottle, a jerrycan, a fuel tank). `part` is the destruction part that is the container (a
143
277
  * bullet hole in it makes it leak). `uses` are the item uses offered while it is held (`drink`, `pour`...; defaults from `ITEM_USES`).
@@ -162,6 +296,61 @@ interface TestRoomItem {
162
296
  /** Author and licence of the thumbnail, shown as the tooltip of the picture in the inspector. */
163
297
  thumbnailCredit?: string;
164
298
  }
299
+ /**
300
+ * One piece of destructible scenery of the arena (the target wall, the table, the shelf, or any piece the host adds): a spec, where it stands and how it is drawn.
301
+ * Keyed by arena object id in `TestRoomOptions.scenery`.
302
+ */
303
+ interface SceneryObject {
304
+ /** What the destruction world is given. */
305
+ spec: DestructibleSpec;
306
+ /** World position of the spec's origin. Required for a new id; for a default id it defaults to the object it replaces. */
307
+ position?: Vec3;
308
+ /** Turn about the vertical axis, radians (default 0). */
309
+ rotationY?: number;
310
+ /** Colour per part id, then per material id, for the default drawing (`buildFromSpec`). */
311
+ colors?: Record<string, number>;
312
+ /**
313
+ * Draws the object: a group whose meshes are named by part id (and carry `userData.destructionPart`), in the space of the spec. Without it the room
314
+ * draws one plain mesh per part with `buildFromSpec`.
315
+ */
316
+ build?: (THREE: ThreeNamespace) => THREE_NS.Object3D;
317
+ /** Name shown in the inspector and the log (default the translated name of a default id, else the id). */
318
+ label?: string;
319
+ /**
320
+ * A top that loose things rest on (and liquids pool on) while that part stands: the part id and the height of its upper face in the spec's space.
321
+ * The rectangle is the part's footprint. Without it nothing rests on the object.
322
+ */
323
+ top?: {
324
+ part: string;
325
+ y: number;
326
+ };
327
+ /**
328
+ * Where a water tap of the piece pours (a sink, a tub), in the spec's space: the room's tap (its stream and its HUD button) pours there, and the piece's own model is the tap. The first piece with a `spout` wins over the table tap.
329
+ */
330
+ spout?: Vec3;
331
+ }
332
+ /** What the room asks when something thrown or set down needs a model: the tool in the hand, the explosive it carries (an id of the arsenal) and how it is used. */
333
+ interface ProjectileQuery {
334
+ weapon: string;
335
+ explosive?: string;
336
+ kind: 'grenade' | 'molotov' | 'rocket' | 'charge';
337
+ }
338
+ /** A model for a projectile: the object, and the axis along its length (default +y, what the room's own primitives use; a rocket points that axis along its flight). */
339
+ interface ProjectileModel {
340
+ model: THREE_NS.Object3D;
341
+ axis?: Vec3;
342
+ }
343
+ /** The room's tap (a stream of water): how it is drawn and where the water leaves it. */
344
+ interface SceneryTap {
345
+ /** Draws the tap in its own space (the origin where it stands). */
346
+ build: (THREE: ThreeNamespace) => THREE_NS.Object3D;
347
+ /** Where the water leaves the spout, in the space of the model. */
348
+ spout: Vec3;
349
+ /** World position of the model's origin. Default: standing on the `table` top, with the spout over its right half (no table, no tap). */
350
+ position?: Vec3;
351
+ /** Turn about the vertical axis, radians (default 0). */
352
+ rotationY?: number;
353
+ }
165
354
  interface TestRoomOptions {
166
355
  THREE: ThreeNamespace;
167
356
  /** Element the bench fills (canvas + HUD). */
@@ -195,6 +384,19 @@ interface TestRoomOptions {
195
384
  maxCarryKg?: number;
196
385
  /** Arena half size in metres (default 14). */
197
386
  arenaSize?: number;
387
+ /**
388
+ * The destructible scenery of the arena, by object id. The defaults are `target_wall` (a brick wall of panels 11 m ahead of the start), `table` (with the
389
+ * water tap beside it) and `shelf`. A key that matches a default replaces it (the position defaults to the replaced one), `null` removes it, a new key adds
390
+ * a piece (its `position` is required). The player cannot walk through any of them: the collider is the footprint of the spec's parts.
391
+ */
392
+ scenery?: Record<string, SceneryObject | null>;
393
+ /** The tap of the room. Omitted: a tap drawn from primitives on a post beside the table; an object: that model (see `SceneryTap`); `null`: no tap. */
394
+ tap?: SceneryTap | null;
395
+ /**
396
+ * The model of something thrown (a grenade, a molotov, a rocket) or set down (a charge). The room only knows the arsenal's ids, so the host's weapons package
397
+ * supplies the real model; where it answers nothing the room draws a plain primitive (a sphere, a cylinder, a box).
398
+ */
399
+ projectileModel?: (query: ProjectileQuery) => ProjectileModel | null | undefined;
198
400
  /** Spawn these catalogue ids on start, in a row in front of the player. */
199
401
  initial?: string[];
200
402
  /** Open on one catalogue group (`furniture`, `structure`, `weapons`…): see `TestRoom.openArea`. */
@@ -211,10 +413,30 @@ interface TestRoomOptions {
211
413
  environment?: EnvironmentBus;
212
414
  /** Show the built-in HUD and panels (default true). */
213
415
  ui?: boolean;
416
+ /** The player's body (see `TestRoom.setAvatar`). */
417
+ avatar?: RoomAvatar;
418
+ /** The view the room starts in (default `first`; the V key and `TestRoom.setCameraMode` change it). */
419
+ cameraMode?: CameraMode;
420
+ /**
421
+ * The strike clip each melee tool plays before its blow lands, by tool id. The blow (`meleeUse`, with the aim read again) falls at the clip's impact second, which
422
+ * the avatar's `strike` answers; a tool that is not here, or an avatar that cannot play the clip, strikes at the click. Default `{ fists: 'punch_right', shove: 'shove_two_hands', kick: 'front_kick' }`.
423
+ */
424
+ strikeClips?: Readonly<Record<string, string>>;
425
+ /** Which leg the kick uses: `alternate` (default) swaps at each kick starting with the right, or always `left` or `right`. */
426
+ kickLeg?: KickLeg;
427
+ /** The first-person stance of the arms by tool id, for the tools that hold nothing the module places (default `{ fists: 'fists' }`; any other tool holds, or is `relaxed` with nothing to hold). */
428
+ handStances?: Readonly<Record<string, string>>;
429
+ /** The over-the-shoulder view of the `third` mode: distance, shoulder, height, wall probes. Defaults are the team game's. */
430
+ thirdPerson?: ThirdPersonOptions;
214
431
  /** Room counters the bench panel shows and `TestRoom.metrics()` returns (default all); `false` for none. */
215
432
  metrics?: readonly RoomMetricId[] | false;
216
433
  /** Blast options every explosion of the room starts with (`shelter`, `fragmentHits`, `confinement`; default all off). The HUD toggles them and `TestRoom.setBlast` changes them. */
217
434
  blast?: BlastOptions;
435
+ /**
436
+ * The numbers of the shove an explosion gives to what is around it: loose items, furniture, and the bodies of the modules (people), which read them from `ModuleHost.blastShove`.
437
+ * Default: the SDK's (`DEFAULT_BLAST_SHOVE` of game-contracts: 2 % of the energy becomes speed, 20 m/s at most, falloff `(1 - d / r)^2`).
438
+ */
439
+ blastShove?: BlastShoveOptions;
218
440
  /**
219
441
  * Module bench state. `state` maps a module id to its encoded state, restored when that module is first loaded (a module reloaded
220
442
  * keeps its live state instead) before its first changes are sent; ids are per module, so two modules may share a control id.
@@ -303,7 +525,7 @@ interface TestRoom {
303
525
  update(dt: number): void;
304
526
  /** Advance frames until the tool in hand has no phase left (the draw `setTool` plays, a reload, a cycle) or `maxSeconds` pass; plus one frame so the next pull is not inside the phase; returns the seconds waited. */
305
527
  waitUntilReady(maxSeconds?: number): number;
306
- on(type: 'spawn' | 'tool' | 'grab' | 'throw' | 'event', fn: (data: unknown) => void): () => void;
528
+ on(type: 'spawn' | 'tool' | 'grab' | 'throw' | 'event' | 'camera', fn: (data: unknown) => void): () => void;
307
529
  /** Tilt the held item by `rad` radians around its own x axis (hold Z in the room). Pouring and spilling follow from the tilt. */
308
530
  tilt(rad: number): void;
309
531
  /** Open or close the tap over the table (default: toggle). Returns whether it is open. */
@@ -328,6 +550,15 @@ interface TestRoom {
328
550
  * that brings an `area` is laid out by it; any other group is laid in rows (small items in front, fixed big ones behind). Unknown: nothing.
329
551
  */
330
552
  openArea(group: string): SpawnedObject[];
553
+ /**
554
+ * `openArea` with the loading indicator: shows "Opening <area>…", waits for it to be painted and then lays the area (an area with hundreds of items takes a second or more and would
555
+ * otherwise look frozen). Resolves with the spawned objects. Use it where a person waits for the result (a page that opens an area on load); scenarios call `openArea`, which is synchronous.
556
+ */
557
+ openAreaAsync(group: string): Promise<SpawnedObject[]>;
558
+ /** The loading indicator of the room: `begin`, `busy`, `state`, `on` (see `RoomLoading`); modules get it as `ModuleHost.loading`. */
559
+ readonly loading: RoomLoading;
560
+ /** `loading.busy`: shows the indicator, waits for it to be painted, runs `work` and takes it down. The way to run a slow, synchronous job. */
561
+ busy<T>(label: string, work: (task: BusyTask) => T | Promise<T>, options?: BusyOptions): Promise<T>;
331
562
  /** The scenarios the panel offers: the built-in `SCENARIOS` in the room locale plus `TestRoomOptions.scenarios`. */
332
563
  readonly scenarios: TestRoomScenario[];
333
564
  /** The liquid renderer of `@adatechnology/game-fluids-three` (containers, streams, stains). */
@@ -342,6 +573,12 @@ interface TestRoom {
342
573
  * clip) writing a check line (pass or fail) per result to the log. Loading the same module twice replaces its items and scenarios.
343
574
  */
344
575
  loadModule(module: TestableModule<ScenarioContext>): ModuleReport;
576
+ /**
577
+ * Compiles the shader programs that the first hit, decal or effect would otherwise compile in the middle of a frame, then draws a throw-away blast (its particle materials
578
+ * and lights make the materials recompile the first time they appear) and a fire and clears them, so a loading screen can pay those first-frame costs. Nothing of it stays
579
+ * in the world, the log or the sound. Call it once the lights of the scene are set (a module that adds lights changes the programs).
580
+ */
581
+ warmUp(): void;
345
582
  /** Animations of the aimed (or held) item: what the animations panel shows. */
346
583
  animations(): AnimationInfo | null;
347
584
  /**
@@ -351,6 +588,37 @@ interface TestRoom {
351
588
  playAnimation(clipId: string, objectId?: string): boolean;
352
589
  /** The bench of a loaded module (default the active one: the module of the last `openArea`, else the last loaded with a bench), or null. */
353
590
  bench(moduleId?: string): BenchHandle | null;
591
+ /**
592
+ * Show a body for the player (or none with null). The room adds `root` to the scene, puts it at the player's feet with the player's yaw every
593
+ * frame and calls `update` with the speed of the player's movement, Shift, the crouch/down set by `setAvatarState`, whether a firearm is in hand
594
+ * and the camera pitch. Hidden in first person, shown in third. In third person the carried item follows `avatar.hand('R')`. The previous
595
+ * avatar is taken out of the scene, not disposed.
596
+ */
597
+ setAvatar(avatar: RoomAvatar | null): void;
598
+ readonly avatar: RoomAvatar | null;
599
+ /** Crouch and knocked-down state a player module drives (merged over what is set; the room fills the rest). `armed` forces the armed arms (`null`: follow the tool in hand again). */
600
+ setAvatarState(state: {
601
+ crouch?: boolean;
602
+ down?: boolean;
603
+ armed?: boolean | null;
604
+ }): void;
605
+ /** What the first-person arms were last given: the stance, how far the trigger finger is curled (0 extended to 1 pulled) and the targets. Read it on each shot to see the finger. */
606
+ handState(): {
607
+ stance: string;
608
+ triggerFinger: number | null;
609
+ targets: HandTargets | null;
610
+ };
611
+ /** Draws the room from another camera (a debug view: the player's camera, the arms and the tool stay as they are) until it is given null. Look at the arms from the side or at a hand from 25 cm. */
612
+ setDebugCamera(view: {
613
+ position: Vec3;
614
+ target: Vec3;
615
+ fov?: number;
616
+ } | null): void;
617
+ /** What the avatar was last given. */
618
+ avatarState(): RoomAvatarState;
619
+ /** `first` (the camera is the eye) or `third` (over the shoulder). V toggles it when a body is set. Logged as a `camera` action. */
620
+ setCameraMode(mode: CameraMode): void;
621
+ readonly cameraMode: CameraMode;
354
622
  /** Room counters and the active module's metrics, read now. */
355
623
  /**
356
624
  * Shakes the camera for the render that follows, by the shake of the effects world, and answers the function that puts it back. The room's own loop
@@ -484,29 +752,8 @@ interface ScenarioReport {
484
752
  */
485
753
  declare function createTestRoom(options: TestRoomOptions): TestRoom;
486
754
 
487
- /**
488
- * Extra materials the demo items need on top of the core ones. None any more (the bottle uses the core `glass`, which now breaks from a drop);
489
- * kept so that code passing it to `worldOptions.materials` keeps working.
490
- */
491
- declare const DEMO_MATERIALS: MaterialDef[];
492
755
  /** Spec of the brick target wall: `cols` x `rows` brick panels, the bottom row anchored, mortar joints between neighbours. */
493
756
  declare function brickWallSpec(cols?: number, rows?: number, panel?: Vec3): DestructibleSpec;
494
- /**
495
- * The generic demo catalogue, names and groups translated for `locale` (`en` or `pt-BR`). Every item is a procedural low-poly build (one mesh per
496
- * destruction part, drawn by `buildFromSpec`) plus an explicit destruction spec. Containers carry a fluid container where it makes sense.
497
- */
498
- declare function demoItems(locale?: string, extra?: TestRoomItem[]): TestRoomItem[];
499
- /** The demo catalogue in English (see `demoItems` for other locales). */
500
- declare const DEMO_ITEMS: TestRoomItem[];
501
-
502
- type T = (key: string, vars?: Record<string, string | number>) => string;
503
- type Num = (n: number, digits?: number) => string;
504
- /** Built-in scripted checks over the generic `DEMO_ITEMS`. Names and check texts come from the room locale. */
505
- declare function makeScenarios(t: T, num: Num, _locale?: string): TestRoomScenario[];
506
- /** The built-in scenarios for a locale (`en` or `pt-BR`), standalone: no room needed to list them. */
507
- declare function scenariosFor(locale?: string): TestRoomScenario[];
508
- /** The built-in scenarios in English. The room offers them in its own locale. */
509
- declare const SCENARIOS: TestRoomScenario[];
510
757
 
511
758
  /** Default look of each built-in destruction material (`0xRRGGBB`). */
512
759
  declare const MATERIAL_COLORS: Record<string, number>;
@@ -525,9 +772,24 @@ interface BuildFromSpecOptions {
525
772
  */
526
773
  declare function buildFromSpec(THREE: typeof THREE_NS, spec: DestructibleSpec, options?: BuildFromSpecOptions): THREE_NS.Group;
527
774
 
775
+ /**
776
+ * The look of the loading indicator, with no dependency: a card with a title, a bar and a line of text over a dim backdrop. The room draws it (`createLoading`), and a page that must show it before any
777
+ * script has run (the default test room's first frame) writes the same markup and styles from `loadingMarkup` and `LOADING_CSS`, so the screen the page starts with and the one the room shows later are one.
778
+ */
779
+ declare const LOADING_CLASS = "tr-loading";
780
+ declare const LOADING_CSS: string;
781
+ /** The markup of the indicator as a string, for a page that writes it into its HTML. `solid` is the opaque backdrop of a first screen, `fixed` covers the window instead of the room. */
782
+ declare function loadingMarkup(options: {
783
+ title: string;
784
+ step?: string;
785
+ solid?: boolean;
786
+ fixed?: boolean;
787
+ id?: string;
788
+ }): string;
789
+
528
790
  /** `id=value;id=value`, values with `encodeURIComponent`, booleans and layers as `1` / `0`. */
529
791
  declare function encodeBenchState(controls: readonly BenchControl[], layers: readonly BenchLayer[]): string;
530
792
  /** Reads an encoded state as hostile input: unknown ids, NaN, options not offered and malformed pairs are dropped; numbers are clamped. */
531
793
  declare function decodeBenchState(encoded: string, controls: readonly ModuleControl[], layers: readonly ModuleLayer[]): Map<string, ControlValue>;
532
794
 
533
- export { type AnimationInfo, type BenchControl, type BenchHandle, type BenchLayer, type BuildFromSpecOptions, DEMO_ITEMS, DEMO_MATERIALS, type HudState, type InspectInfo, type LogEntry, MATERIAL_COLORS, type ModuleReport, type NewEntry, type NewLogEntry, type RoomMetricId, type RoomStrings, SCENARIOS, type ScenarioContext, type ScenarioReport, type SpawnedObject, type TestRoom, type TestRoomItem, type TestRoomLocale, type TestRoomLog, type TestRoomOptions, type TestRoomScenario, type ThreeNamespace, type WritableLog, brickWallSpec, buildFromSpec, createLog, createTestRoom, decodeBenchState, demoItems, encodeBenchState, locales, makeScenarios, resolveStrings, scenariosFor };
795
+ export { type AnimationInfo, type BenchControl, type BenchHandle, type BenchLayer, type BuildFromSpecOptions, type CameraMode, DEFAULT_HAND_STANCES, DEFAULT_STRIKE_CLIPS, type HudState, type InspectInfo, type KickLeg, LOADING_CLASS, LOADING_CSS, type LoadingOptions, type LoadingState, type LogEntry, MATERIAL_COLORS, type ModuleReport, type NewEntry, type NewLogEntry, type ProjectileModel, type ProjectileQuery, type RoomAvatar, type RoomAvatarState, type RoomFirstPersonState, type RoomLoading, type RoomMetricId, type RoomStrings, type ScenarioContext, type ScenarioReport, type SceneryObject, type SceneryTap, type SpawnedObject, type TestRoom, type TestRoomItem, type TestRoomLocale, type TestRoomLog, type TestRoomOptions, type TestRoomScenario, type ThirdPersonOptions, type ThreeNamespace, type WritableLog, brickWallSpec, buildFromSpec, createLoading, createLog, createTestRoom, decodeBenchState, encodeBenchState, loadingMarkup, locales, nextPaint, resolveStrings };