@adatechnology/game-contracts 0.21.0 → 0.23.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
@@ -58,6 +58,8 @@ Hits arrive in world space and events go out in world space. `checkDestructible`
58
58
  well-formed and JSON-serializable. The contracts package does not know your materials: pass their ids (the world's `world.materials.keys()`) or
59
59
  leave `materials` out to skip that check.
60
60
 
61
+ **Cost contract (D8):** the host reads `describe()` and `state()` per part per frame, so both are O(1), allocation-free and cached between changes (the same object comes back until a hit, tool, fire or pose change). `checkDestructible(impl, { cost: true })` (or `cost: { reads, ceilingMs, allocationBytes }`) reads each 10 000 times and fails on a new object per read, on time over the ceiling (default 50 ms) and, in Node, on heap growth over the ceiling (default 1 MiB).
62
+
61
63
  ### Weapons
62
64
 
63
65
  ```ts
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { w as warnOnce } from './testing-CJY18EZK.js';
2
+
1
3
  /**
2
4
  * Destruction contract: the types a game module and a destruction world share. A module (vehicle, furniture, plant, house, weapon) describes
3
5
  * itself with these and never needs the default destruction world; the world, the renderer and the test room consume them.
@@ -405,6 +407,8 @@ interface Hit {
405
407
  duration?: number;
406
408
  /** `world` (default) or `local` to the object. */
407
409
  space?: 'world' | 'local';
410
+ /** The normal of the surface at `point`, when the ray that found it knows it. */
411
+ normal?: Vec3;
408
412
  /** Who caused it (passed through to events). */
409
413
  source?: string;
410
414
  /** Projectile mass in grams (ballistic): the penetration of a body needs it, the energy alone does not say how much a bullet pushes. */
@@ -652,6 +656,8 @@ interface EventBase {
652
656
  /** Seconds since the world was created. */
653
657
  time: number;
654
658
  source?: string;
659
+ /** The kind of hit the event came out of (a shot, a blow, a cut, a blast...), when it came out of a hit; the sound of an impact is chosen by it. */
660
+ blow?: DamageType;
655
661
  }
656
662
  /** Points in events are in world space. */
657
663
  type DestructionEvent = (EventBase & {
@@ -675,7 +681,8 @@ type DestructionEvent = (EventBase & {
675
681
  exitPoint?: Vec3;
676
682
  powder?: {
677
683
  color: number;
678
- };
684
+ }; /** Joules the round still carries out of the part, when the part knows. */
685
+ energyLeft?: number;
679
686
  }) | (EventBase & {
680
687
  type: 'crack';
681
688
  part: string;
@@ -981,6 +988,49 @@ declare function blastShove(blast: BlastSource, target: {
981
988
  readonly mass: number;
982
989
  }, options?: BlastShoveOptions): BlastShove | undefined;
983
990
 
991
+ /** From this speed (m/s) a blast takes a standing body off its feet. Below it the body is only pushed. */
992
+ declare const KNOCK_DOWN_SPEED = 2.5;
993
+ /** The angle of a body lying where it fell (the same tilt a dead body takes). */
994
+ declare const KNOCKED_DOWN_ANGLE = 1.35;
995
+ /** Height of the chest above the feet: where the blast takes the body. */
996
+ declare const KNOCK_CHEST = 1.1;
997
+ /** What a blast is doing to one body: its velocity (m/s), whether it is in the air, and how far down it is (0 on its feet, 1 lying). */
998
+ interface Knock {
999
+ velocity: [number, number, number];
1000
+ airborne: boolean;
1001
+ /** 0..1 */
1002
+ lying: number;
1003
+ /** Seconds left to lie on the ground after it stopped. */
1004
+ lieLeft: number;
1005
+ /** Knocked off its feet by this blast. */
1006
+ down: boolean;
1007
+ }
1008
+ /** The mass of a body in kg from its height (m) and its build (1 = average): 70 kg at 1.75 m. */
1009
+ declare const bodyMass: (height: number, build?: number) => number;
1010
+ /**
1011
+ * What a blast does to a body standing at `feet`: the shove of `game-contracts` taken at the chest, a hop when it goes up, and a fall when it is strong enough. Undefined when the blast does not
1012
+ * reach it. The numbers are the host's (`ModuleHost.blastShove`), the same ones that throw its loose items.
1013
+ */
1014
+ declare function knockOf(blast: BlastSource, feet: Vec3, mass: number, options?: BlastShoveOptions): Knock | undefined;
1015
+ /**
1016
+ * Moves a body one step: it slides on the ground and slows, falls when it was knocked down, lies a while and gets up (a dead body stays down). Moves `position` in place; returns true when the
1017
+ * body is on its feet and still, and the knock can be dropped.
1018
+ */
1019
+ declare function stepKnock(knock: Knock, position: {
1020
+ x: number;
1021
+ y: number;
1022
+ z: number;
1023
+ }, dt: number, groundAt: (x: number, z: number) => number, alive: boolean): boolean;
1024
+
1025
+ interface CostOptions {
1026
+ /** Reads of `state()` and of `describe()` to time. Default 10 000. */
1027
+ reads?: number;
1028
+ /** Ceiling, in milliseconds, for all the reads of one method. Default 50 (5 microseconds per read). */
1029
+ ceilingMs?: number;
1030
+ /** Ceiling of heap growth, in bytes, over all the reads of one method (only where `process.memoryUsage` exists). Default 1 MiB. */
1031
+ allocationBytes?: number;
1032
+ }
1033
+
984
1034
  /**
985
1035
  * What a game module implements for an object whose behaviour it owns (a vehicle with its own panel deformation, a creature, a machine). The
986
1036
  * world (`DestructionWorld.register`) hosts it next to its own objects: raycast, shoot, explode, hit, disassemble, extinguish, update, state,
@@ -990,17 +1040,47 @@ declare function blastShove(blast: BlastSource, target: {
990
1040
  * `setTransform` is only used by the world for raycasts and blast range). Every event must carry `object === id` and name parts listed by
991
1041
  * `describe()`.
992
1042
  */
1043
+ /** World-space ray for `DestructibleImpl.surfaceRay`. */
1044
+ interface SurfaceRay {
1045
+ readonly origin: Vec3;
1046
+ readonly direction: Vec3;
1047
+ readonly maxDistance: number;
1048
+ ignored?(part: string): boolean;
1049
+ }
1050
+ interface SurfaceRayHit {
1051
+ readonly part: string;
1052
+ readonly point: Vec3;
1053
+ readonly normal: Vec3;
1054
+ readonly distance: number;
1055
+ /** Thickness the ray crosses in this part, metres. */
1056
+ readonly pathLen: number;
1057
+ }
993
1058
  interface DestructibleImpl {
994
1059
  readonly id: string;
995
1060
  readonly name?: string;
996
- /** Parts, joints and structure in the impl's local space, for raycast, inspector and log. Called often: keep it cheap. */
1061
+ /**
1062
+ * Parts, joints and structure in the impl's local space, for raycast, inspector and log.
1063
+ * COST CONTRACT (D8): called per frame by the renderer, so it is O(1), allocation-free and cached between changes: the same object comes
1064
+ * back until a hit, tool, fire or pose change alters it. `checkDestructible(impl, { cost: true })` measures it.
1065
+ */
997
1066
  describe(): DestructibleSpec;
998
1067
  /** Apply one hit (see `IMPACT_CONTRACT` for the fields per type) and return the events it caused. */
999
1068
  hit(hit: Hit): DestructionEvent[];
1069
+ /**
1070
+ * The real surface a ray meets, for an impl whose drawn shape is not its boxes (a car body): the boxes of `describe()` only say where to look, and a shot that crosses
1071
+ * them without touching the drawn surface goes on. `ignored` answers for the parts the round already went through. Null when the ray meets nothing.
1072
+ */
1073
+ surfaceRay?(ray: SurfaceRay): SurfaceRayHit | null;
1000
1074
  /** Advance fire, melting, collapse and other timed behaviour. */
1001
1075
  update?(dt: number): DestructionEvent[];
1002
- state(): ObjectState;
1076
+ /**
1077
+ * COST CONTRACT (D8): the host reads it per part per frame, so it is O(1), allocation-free and cached between changes (see `describe()`).
1078
+ * The cached object is shared with every reader: treat it as read-only, copy before changing it.
1079
+ */
1080
+ state(): Readonly<ObjectState>;
1003
1081
  reset?(): void;
1082
+ /** The world took the object out (`DestructionWorld.remove`): the impl lets go of what it joined (shared queues, listeners). */
1083
+ dispose?(): void;
1004
1084
  /** Same contract as `DestructionWorld.disassemble`: progress 0..1, or -1 when the tool cannot undo the joint. */
1005
1085
  disassemble?(jointId: string, tool: ToolKind, dt: number): number;
1006
1086
  extinguish?(partId?: string): void;
@@ -1025,11 +1105,13 @@ interface CheckOptions {
1025
1105
  * `world.materials`, or the built-in plus your own). When omitted, the material check is skipped.
1026
1106
  */
1027
1107
  materials?: Iterable<string>;
1108
+ /** Opt-in cost measurement (D8): `true` for the defaults, or the limits to use. */
1109
+ cost?: boolean | CostOptions;
1028
1110
  }
1029
1111
  /**
1030
1112
  * 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()`
1031
1113
  * matches `describe()`, hit points never go negative, `update(0.016)` returns an array and every event is JSON-serializable. It hits the impl
1032
- * (and calls `reset()` afterwards when there is one).
1114
+ * (and calls `reset()` afterwards when there is one). With `options.cost` it also times `state()` and `describe()` (the cost contract of D8).
1033
1115
  */
1034
1116
  declare const checkDestructible: (impl: DestructibleImpl, options?: CheckOptions) => {
1035
1117
  ok: boolean;
@@ -1680,6 +1762,11 @@ interface AnimationEventSpec {
1680
1762
  /** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */
1681
1763
  type: string;
1682
1764
  data?: Record<string, unknown>;
1765
+ /**
1766
+ * Seconds of real time the event fires before `t`, not stretched with the clip: a recorded sound whose hit comes that long after its start
1767
+ * (the click of a magazine seating) starts early so the hit lands on `t`. The player passes the lead it could apply as `data.lead`.
1768
+ */
1769
+ lead?: number;
1683
1770
  }
1684
1771
  /** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */
1685
1772
  interface AnimationClipSpec {
@@ -1715,6 +1802,12 @@ interface SoundRequest {
1715
1802
  readonly params?: Readonly<Record<string, number>>;
1716
1803
  /** Seed for the sound's own random stream: same seed, same sound. */
1717
1804
  readonly seed?: number;
1805
+ /** Seconds from now (the hit of a blow after its swing); the host schedules it on the audio clock, not on a timer. */
1806
+ readonly delay?: number;
1807
+ /** Built now instead of waiting in the engine's deferred start queue: for a sound that has to land on the frame the player sees it (a blow's
1808
+ * contact, a heel strike, a landing). The queue builds a voice on the next frame budget and drops it after its latency, which at a low frame
1809
+ * rate is late or silent. Default false. */
1810
+ readonly urgent?: boolean;
1718
1811
  }
1719
1812
  /** What `AudioSink.play` hands back; opaque to the module, given back to `stop`. */
1720
1813
  interface SoundHandle {
@@ -1785,6 +1878,143 @@ interface EnvironmentBus {
1785
1878
  /** A plain store that implements the bus; the test room and tests use it, a game may bring its own. */
1786
1879
  declare function createEnvironmentBus(initial?: Partial<EnvironmentState>): EnvironmentBus;
1787
1880
 
1881
+ /**
1882
+ * The sounds of a body (footsteps, jumps, landings, blows, hurt), as ids every package agrees on: the rule that knows what the body did
1883
+ * (`game-player`, a host's own player) names the sound, and the bank that knows how it sounds (`game-audio`'s `BODY_FOLEY_BANK`) answers it.
1884
+ * A game swaps the sound of an id (a sample instead of the synthesis) without touching the rule.
1885
+ */
1886
+ /** What the feet touch, by how it sounds (not by what it is made of: a wooden floor and a plywood board both sound `wood`). */
1887
+ declare const SURFACE_IDS: readonly ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
1888
+ type SurfaceId = (typeof SURFACE_IDS)[number];
1889
+ /** The surface a body stands on when nobody says (the test room's floor is a concrete slab). */
1890
+ declare const DEFAULT_SURFACE: SurfaceId;
1891
+ /** How the feet move: a run hits harder and shorter; a crouched step is the walk, quieter. */
1892
+ declare const GAIT_IDS: readonly ["walk", "run"];
1893
+ type GaitId = (typeof GAIT_IDS)[number];
1894
+ /** How hard a landing is: `light` for a jump on the spot or a step down (up to about 5 m/s), `heavy` for a drop of more than a metre and a half. */
1895
+ declare const LANDING_IDS: readonly ["light", "heavy"];
1896
+ type LandingId = (typeof LANDING_IDS)[number];
1897
+ /** The fall speed (m/s) from which a landing is `heavy`. */
1898
+ declare const HEAVY_LANDING_SPEED = 5.5;
1899
+ type FootstepSoundId = `footstep_${SurfaceId}_${GaitId}`;
1900
+ type LandSoundId = `land_${SurfaceId}_${LandingId}`;
1901
+ /**
1902
+ * How a blow is struck, by the body part or the tool that does it. `blade` stabs or slashes, `blunt` is a club, a bat, a pipe or a hammer, `chop` an
1903
+ * axe or a machete driven down.
1904
+ */
1905
+ declare const BLOW_IDS: readonly ["punch", "kick", "shove", "blade", "blunt", "chop"];
1906
+ type BlowId = (typeof BLOW_IDS)[number];
1907
+ /** What the blow meets: a body (flesh and bone, the head apart), something else (the material answers with its own impact), or nothing. */
1908
+ type BlowTarget = 'body' | 'head' | 'object' | 'air';
1909
+ /** The sounds of the body that are not a footstep or a landing. */
1910
+ declare const BODY_SOUND_IDS: readonly ["jump", "cloth_rustle", "body_fall", "fall_damage", "player_hurt", "player_breath_run", "player_death", "bite", "grab", "punch_swing", "punch_hit_body", "punch_hit_face", "punch_hit_object", "kick_swing", "kick_hit", "kick_hit_object", "shove_two_hands", "knife_swing", "knife_stab", "knife_slash", "knife_hit_object", "blunt_swing", "blunt_hit", "blunt_hit_object", "axe_swing", "axe_chop", "axe_hit_object"];
1911
+ type BodySoundId = (typeof BODY_SOUND_IDS)[number];
1912
+ /** Every sound of a body: what a host plays for the player, a survivor or a zombie. */
1913
+ type BodyFoleyId = FootstepSoundId | LandSoundId | BodySoundId;
1914
+ declare const footstepSoundId: (surface: SurfaceId, gait: GaitId) => FootstepSoundId;
1915
+ declare const landSoundId: (surface: SurfaceId, fallSpeed: number) => LandSoundId;
1916
+ /** Every footstep and landing id, in the order of the surfaces. */
1917
+ declare const FOOTSTEP_SOUND_IDS: readonly FootstepSoundId[];
1918
+ declare const LAND_SOUND_IDS: readonly LandSoundId[];
1919
+ declare const BODY_FOLEY_IDS: readonly BodyFoleyId[];
1920
+ /**
1921
+ * Seconds from the start of a swing's sound to its loudest point (the limb or the tool at full speed, just before the contact). A host that knows when the
1922
+ * blow lands starts the swing this long before; one that does not plays the swing now and the hit this long after.
1923
+ */
1924
+ declare const SWING_LEAD_SECONDS: Readonly<Record<BlowId, number>>;
1925
+ /** The swing through the air, played as the blow starts (`null`: the blow has none). */
1926
+ declare const swingSoundId: (blow: BlowId) => BodySoundId | null;
1927
+ /**
1928
+ * The body's side of a blow that lands: the knuckles, the shoe, the blade or the club meeting what it hit. On an object the material's own impact
1929
+ * plays too (the wood, the metal); this is the part that says it was a punch and not a shot.
1930
+ */
1931
+ declare const hitSoundId: (blow: BlowId, target: Exclude<BlowTarget, "air">) => BodySoundId;
1932
+ /**
1933
+ * The surface a material sounds like underfoot, by the material ids of `game-destruction` and `game-terrain` (unknown ids: `fallback`). Data, so
1934
+ * a game with its own materials passes its own table to `surfaceOfMaterial`.
1935
+ */
1936
+ declare const MATERIAL_SURFACES: Readonly<Record<string, SurfaceId>>;
1937
+ declare const isSurfaceId: (value: unknown) => value is SurfaceId;
1938
+ /** The surface of a material id; a surface id passes through. */
1939
+ declare function surfaceOfMaterial(material: string | undefined, table?: Readonly<Record<string, SurfaceId>>, fallback?: SurfaceId): SurfaceId;
1940
+
1941
+ declare const FRAME_PHASES: readonly ["input", "fixedSimulate", "lateSimulate", "present", "render", "idle"];
1942
+ type FramePhase = (typeof FRAME_PHASES)[number];
1943
+ /**
1944
+ * Presentation work a system leaves for later frames, run in small units within a shared time budget.
1945
+ * Never simulation: nothing that consumes a seeded generator or decides an outcome may wait in a queue.
1946
+ */
1947
+ interface DeferredQueue {
1948
+ /** Stable name for stats and logs (see `DEFERRED_QUEUE_ID`). */
1949
+ readonly id: string;
1950
+ /**
1951
+ * Units waiting. 0 means idle; the budget skips the queue without calling `run`. `frame` is the budget's frame counter: a queue may
1952
+ * report 0 for the rest of a frame (the crease queue does in the frame a panel was torn). Same value for repeated calls in one frame.
1953
+ */
1954
+ pending(frame?: number): number;
1955
+ /**
1956
+ * Runs units in order until `clock() >= deadline`. Contract: when `pending() > 0` it runs **at least one** unit, even with a deadline
1957
+ * already past; it never runs a unit after it saw the deadline pass. Returns how many units it ran.
1958
+ */
1959
+ run(deadline: number, clock: () => number): number;
1960
+ /** Runs everything now (synchronous step, clear, dispose). */
1961
+ flush(): void;
1962
+ /**
1963
+ * Called by `budget.register`: from now on the owner stops draining on its own (no MessageChannel, no drain inside `update`).
1964
+ * Never called by a getter; a queue nobody registered keeps today's internal draining.
1965
+ */
1966
+ attach?(): void;
1967
+ /** Called by the unregister function and by `budget.dispose`: the owner goes back to its internal draining. */
1968
+ detach?(): void;
1969
+ }
1970
+ /** Where a module registers a deferred queue with the host's budget. */
1971
+ interface DeferredQueueOptions {
1972
+ /** Higher runs first. See DEFERRED_PRIORITY. */
1973
+ readonly priority: number;
1974
+ /** `present` runs before the render (the work must show this frame); `idle` runs after it. Default `idle`. */
1975
+ readonly phase?: 'present' | 'idle';
1976
+ /** Starvation guard: the queue runs at least one unit every this many frames, even with the budget spent. Default 4. */
1977
+ readonly minEveryFrames?: number;
1978
+ /** Milliseconds of every frame kept for this queue before the others share the rest (when it has work). Default 0. */
1979
+ readonly reserveMs?: number;
1980
+ }
1981
+ /** The ids of the kit's queues: what `DeferredQueue.id` says and what `DEFERRED_PRIORITY` is keyed by. */
1982
+ declare const DEFERRED_QUEUE_ID: {
1983
+ readonly DESTRUCTION_EVENTS: "destruction.events";
1984
+ readonly BREACH_PAINT: "floorplan.breach-paint";
1985
+ readonly AUDIO_VOICES: "audio.voices";
1986
+ readonly VEHICLE_CREASE: "vehicles.crease";
1987
+ };
1988
+ /** Default priorities and reserves of the kit's queues; a host may override any of them. */
1989
+ declare const DEFERRED_PRIORITY: {
1990
+ readonly "destruction.events": {
1991
+ readonly priority: 300;
1992
+ readonly phase: "present";
1993
+ };
1994
+ readonly "floorplan.breach-paint": {
1995
+ readonly priority: 250;
1996
+ readonly phase: "present";
1997
+ };
1998
+ readonly "audio.voices": {
1999
+ readonly priority: 200;
2000
+ readonly phase: "idle";
2001
+ readonly reserveMs: 1;
2002
+ };
2003
+ readonly "vehicles.crease": {
2004
+ readonly priority: 100;
2005
+ readonly phase: "idle";
2006
+ };
2007
+ };
2008
+ interface DeferredQueueReport {
2009
+ readonly ok: boolean;
2010
+ readonly problems: readonly string[];
2011
+ }
2012
+ /**
2013
+ * Checks the `DeferredQueue` contract. `fill` leaves the queue with at least two units waiting (the same call is made on a fresh queue).
2014
+ * It cannot prove the queue only carries presentation work: that is a rule of review.
2015
+ */
2016
+ declare function checkDeferredQueue(queue: DeferredQueue, fill: () => void): DeferredQueueReport;
2017
+
1788
2018
  /** 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. */
1789
2019
  interface ModuleItemFluid {
1790
2020
  container: ContainerSpec;
@@ -1833,6 +2063,20 @@ interface Thumbnail {
1833
2063
  wear?: 0 | 1 | 2;
1834
2064
  };
1835
2065
  }
2066
+ /**
2067
+ * What an item sounds like when the hand works it, as names of sound sets the host knows (the kit's are `LEAF_FOLEY_SETS` and `PICKUP_CLASSES` of game-audio).
2068
+ * Everything is optional: without it the host picks by the material of the part.
2069
+ */
2070
+ interface ItemSounds {
2071
+ /** How it sounds picked up: `metal`, `plastic`, `glass`, `paper`, `fabric`, `wood`, `food`. Default: by its heaviest material. */
2072
+ handling?: string;
2073
+ /** The sound set of each leaf worked by hand, by part id, `*` for every other leaf: `car_door`, `car_trunk`, `fridge`, `microwave`, `drawer`, `wood_door`, `metal_door`. */
2074
+ leaves?: Readonly<Record<string, string>>;
2075
+ /** The sound id of opening its seal (`can_open`, `bottle_open`). Default: a metal seal is a can, any other a screw cap. */
2076
+ open?: string;
2077
+ /** The sound id of each use while it is held (`drink`: `drink_gulp`, `eat`: `eat_chew`), played over and over while the use lasts. */
2078
+ uses?: Readonly<Record<string, string>>;
2079
+ }
1836
2080
  /** One thing a module can put in the test room (or any viewer): a toy car, a chair, a pistol on a table. */
1837
2081
  interface TestableItem {
1838
2082
  /** snake_case id, unique inside the module. */
@@ -1861,6 +2105,8 @@ interface TestableItem {
1861
2105
  pushable?: boolean;
1862
2106
  /** Where a body sits or lies on it: the host can seat the player there. */
1863
2107
  seats?: readonly ItemSeat[];
2108
+ /** What it sounds like in the hand (picked up, opened, used) and the sound set of its doors and lids. */
2109
+ sounds?: ItemSounds;
1864
2110
  /** Building it takes a noticeable time (a vehicle, a district): the host's catalogue shows its loading indicator while it spawns one. `true`, or the text to show. */
1865
2111
  slow?: boolean | string;
1866
2112
  /**
@@ -2048,6 +2294,17 @@ type HostEvent =
2048
2294
  /** The player crouched or stood up, or the camera mode changed; read the new state with `host.player()`. */
2049
2295
  | {
2050
2296
  type: 'player';
2297
+ }
2298
+ /**
2299
+ * A foot of the player's body touched the ground, at the frame the body's walk puts it down (the host's avatar, or the host's own stride without one):
2300
+ * `surface` is what is under it. A module that owns the footsteps (their sound and their noise) steps on this instead of its own timer.
2301
+ */
2302
+ | {
2303
+ type: 'footfall';
2304
+ foot: 'left' | 'right';
2305
+ surface: SurfaceId;
2306
+ gait: GaitId;
2307
+ position: Vec3;
2051
2308
  };
2052
2309
  /** The tool in hand as the host sees it. */
2053
2310
  interface HostTool {
@@ -2140,6 +2397,40 @@ interface HostReach {
2140
2397
  /** The hand is on the handle. */
2141
2398
  onGrip?(): void;
2142
2399
  }
2400
+ interface FrameOptions {
2401
+ /** Default `lateSimulate`, which is where every `onFrame` runs without options. */
2402
+ readonly phase?: Exclude<FramePhase, 'render'>;
2403
+ /** Lower runs first inside the phase; ties keep the order of registration. Default 0. */
2404
+ readonly order?: number;
2405
+ }
2406
+ /**
2407
+ * An extra view the host draws over its own, for a module that needs to see from somewhere else at the same time: a rear-view mirror, a side mirror, a reversing camera, a security
2408
+ * monitor. The host renders the scene from `camera` into `rect` after the main view, every frame, until the returned function is called.
2409
+ */
2410
+ interface ModuleView {
2411
+ /** A three.js camera the module moves itself every frame. Its aspect is set by the host to the rect's. */
2412
+ camera: unknown;
2413
+ /** Where on the screen, as fractions of the canvas from its top-left corner (0..1). */
2414
+ rect: {
2415
+ readonly x: number;
2416
+ readonly y: number;
2417
+ readonly width: number;
2418
+ readonly height: number;
2419
+ };
2420
+ /** Draws it left-right flipped, as a mirror shows the world. */
2421
+ mirror?: boolean;
2422
+ /** Rounds the corners of the frame (a fraction of the shorter side, 0..0.5); 0 or absent is a plain rectangle. */
2423
+ rounded?: number;
2424
+ /** Short label the host may print on the frame. */
2425
+ label?: string;
2426
+ /** Renders it at this fraction of the canvas resolution (default 0.5): a mirror does not need full detail. */
2427
+ resolution?: number;
2428
+ }
2429
+ interface HostGraphics {
2430
+ hordeDetail(): 'auto' | number;
2431
+ onChange(listener: () => void): () => void;
2432
+ report(key: string, value: number): void;
2433
+ }
2143
2434
  /**
2144
2435
  * What the host lends a module that brings its own presentation (a weapon in the hands, sounds, ejected casings). `THREE`, `scene` and `camera`
2145
2436
  * are the host's three.js namespace, scene and camera: `unknown` here because the contracts do not depend on three.js.
@@ -2151,13 +2442,17 @@ interface ModuleHost {
2151
2442
  locale: string;
2152
2443
  /** Height of whatever things rest on at (x, z): the floor, a table, a counter. */
2153
2444
  groundAt(x: number, z: number): number;
2445
+ /** Material id of the floor at (x, z) (`concrete`, `soil`, `wood_hard`...): what a dropped case or a footstep lands on. Optional; undefined where the host does not know. */
2446
+ surfaceAt?(x: number, z: number): string | undefined;
2154
2447
  tool(): HostTool;
2155
2448
  /** The player now: where, which way, crouching, how fast and how the camera follows. A `player` event tells when crouching or the camera mode changes. */
2156
2449
  player(): PlayerState;
2157
2450
  /** Follow the player's tool events. Returns the unsubscribe function. */
2158
2451
  on(listener: (event: HostEvent) => void): () => void;
2159
- /** Run every simulated frame with the frame's seconds. Returns the unsubscribe function. */
2160
- onFrame(update: (dt: number) => void): () => void;
2452
+ /** Run every simulated frame with the frame's seconds (in the given phase, `lateSimulate` by default). Returns the unsubscribe function. */
2453
+ onFrame(update: (dt: number) => void, options?: FrameOptions): () => void;
2454
+ /** Registers a deferred queue with the host's frame budget. Returns the function that takes it out. A host without a budget leaves it out. */
2455
+ defer?(queue: DeferredQueue, options: DeferredQueueOptions): () => void;
2161
2456
  /**
2162
2457
  * Follow the host clearing its area (`clear()`, opening an area, running scenarios): everything the module put in the world on its own (not as items) is to be taken away or put back. Returns the
2163
2458
  * unsubscribe function. A host that never clears leaves it out.
@@ -2170,6 +2465,12 @@ interface ModuleHost {
2170
2465
  onReset?(listener: () => void): () => void;
2171
2466
  /** Write a line to the host's behaviour log; `type` is the kind of entry (default `module_note`), so a module can log what a filter looks for (`sound`). */
2172
2467
  log(text: string, data?: Record<string, unknown>, type?: string): void;
2468
+ /**
2469
+ * The player's graphics settings, for what a module draws on its own (a horde): `hordeDetail` is `auto` (the module's renderer adapts by itself) or the share of
2470
+ * its level-of-detail budget the player fixed, 0.1 to 1. `onChange` tells when it changes; `report` says what an adapting renderer draws now, so the settings
2471
+ * screen can show it (`hordeDetail`: the share in use). A host without graphics settings leaves it out.
2472
+ */
2473
+ graphics?: HostGraphics;
2173
2474
  /**
2174
2475
  * Sets what the host lets a module tune on its tools, keyed by the id of the module's own control (`ModuleControl.id`) with the id of the option as the value; the tool in hand changes at once and the
2175
2476
  * ones taken later start that way. A host applies the ids it knows and ignores the rest. The test room knows `condition` (how worn the tools are) and `ammo_quality` (how good what they load is).
@@ -2212,6 +2513,15 @@ interface ModuleHost {
2212
2513
  reachFor?(reach: HostReach): void;
2213
2514
  /** Asks the tool in hand to do something (see `ToolCommand`). A host without accessories leaves it out. */
2214
2515
  command?(command: ToolCommand): ToolCommandResult;
2516
+ /**
2517
+ * Draws an extra view over the host's own (mirrors, a reversing camera). Returns the function that takes it away. A host that cannot draw more than one view leaves it out.
2518
+ */
2519
+ addView?(view: ModuleView): () => void;
2520
+ /**
2521
+ * Takes over the main view with the module's camera (a vehicle's chase, cockpit or hood camera) until it is called again with null. The player's own camera comes back then. A host
2522
+ * that keeps its camera to itself leaves it out.
2523
+ */
2524
+ setViewCamera?(camera: unknown | null): void;
2215
2525
  /** Lent only to a module that declares controls, layers or metrics. */
2216
2526
  bench?: ModuleBench;
2217
2527
  /** The host's shared sound engine; a module that makes sound uses it when lent and stays silent (or brings its own) when not. */
@@ -2220,11 +2530,23 @@ interface ModuleHost {
2220
2530
  environment?: EnvironmentBus;
2221
2531
  /** The host's effects (smoke, sparks, blast, impacts): a module asks for them by id instead of drawing its own. Absent when the host has none. */
2222
2532
  effects?: EffectSink;
2533
+ /** The most loose bodies a module may keep alive at once (ejected casings, dropped magazines), read at each spawn: the host's one budget. Absent when the host keeps no budget of its own. */
2534
+ particleLimit?: () => number | undefined;
2535
+ /** The host's light budget (`LightBudget` of `@adatechnology/game-render-three`; `unknown` here because the contracts do not depend on three.js): the one owner of point and spot lights. Read it with `isLightBudget`. Absent when the host has none. */
2536
+ lights?: unknown;
2537
+ /** The host's pools of geometries and materials (`RenderPools` of `@adatechnology/game-render-three`; `unknown` here because the contracts do not depend on three.js): what an effect asks for instead of making its own, so its shader programs stay linked. Read it with `isRenderPools`. Absent when the host has none. */
2538
+ pools?: unknown;
2223
2539
  /**
2224
2540
  * The numbers of the shove a blast gives to a body. A module that moves bodies of its own (people, vehicles) answers the `explosion` host event with `blastShove(event, body, host.blastShove)`
2225
2541
  * so that it throws things the way the host throws its loose items. Absent when the host keeps the defaults.
2226
2542
  */
2227
2543
  blastShove?: BlastShoveOptions;
2544
+ /**
2545
+ * The share (0..1) of a blast at `point` that reaches `target` through the cover between them (walls, cars, furniture of the host's world): what a module passes as
2546
+ * `BlastSource.reach` to `blastShove`, so a body behind a wall is not thrown like one in the open. `ignore` lists world objects that never block (the explosive).
2547
+ * Absent when the host has no world: the whole blast reaches.
2548
+ */
2549
+ blastReach?(point: Vec3, target: Vec3, ignore?: readonly string[]): number;
2228
2550
  /** The host's loading indicator: a module with a slow job of its own (building a house, laying a district) shows it through this instead of freezing the page. Absent when the host has none. */
2229
2551
  loading?: BusySink;
2230
2552
  }
@@ -2339,6 +2661,12 @@ interface ToolClipPlayer {
2339
2661
  readonly clip: AnimationClipSpec;
2340
2662
  }>;
2341
2663
  }
2664
+ /**
2665
+ * The clip with every event that has a `lead` moved that many seconds earlier, the lead it got in `data.lead`. A led event never goes before 0
2666
+ * nor before the event written ahead of it (a fast clip keeps its order: the lead shrinks). Run after `scaleClip`: the lead is real time, the
2667
+ * clip's stretch does not touch it.
2668
+ */
2669
+ declare const leadClipEvents: (clip: AnimationClipSpec) => AnimationClipSpec;
2342
2670
  declare const createToolClipPlayer: (options: ToolClipPlayerOptions) => ToolClipPlayer;
2343
2671
 
2344
2672
  type Entry = {
@@ -2352,8 +2680,9 @@ type DeepPartial<TValue> = TValue extends (...args: never[]) => unknown ? TValue
2352
2680
  declare const deepMerge: <TBase extends object>(base: TBase, patch: object | undefined) => TBase;
2353
2681
  /** The shipped entries with the ones from `extra` added or, when the id exists, replaced. */
2354
2682
  declare const mergeById: <TEntry extends Entry>(shipped: readonly TEntry[], extra: readonly TEntry[] | undefined) => Map<string, TEntry>;
2355
- /** Applies `patch` by id; a patch for an id that does not exist is ignored. */
2683
+ /** Applies `patch` by id; a patch for an id that does not exist is ignored (and warned about once, in development). */
2356
2684
  declare const applyPatches: <TEntry extends Entry>(entries: Map<string, TEntry>, patches: Readonly<Record<string, DeepPartial<TEntry> | Partial<TEntry>>> | undefined) => void;
2685
+ /** Takes the ids out; an id that does not exist is ignored (and warned about once, in development). */
2357
2686
  declare const removeIds: (entries: Map<string, unknown>, ids: readonly string[] | undefined) => void;
2358
2687
 
2359
2688
  /** One category (kind) of items. Defaults and required contracts are inherited from the `extends` chain. */
@@ -2470,4 +2799,12 @@ interface FootprintObject {
2470
2799
  */
2471
2800
  declare function footprintBlocker(object: FootprintObject | (() => FootprintObject | null | undefined), footprint: Footprint | (() => Footprint | null | undefined)): (x: number, z: number, radius: number) => boolean;
2472
2801
 
2473
- export { type ActionControl, type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type AreaPlacement, type AudioListener, type AudioSink, type BallisticExtras, type BenchChange, type BenchDeclaration, type BenchReport, type BenchSheet, type BlastOptions, type BlastReport, type BlastShove, type BlastShoveOptions, type BlastSource, type BodyRegion, type BodySlot, type BooleanControl, type BusyOptions, type BusySink, type BusyTask, type CategorizedOptions, type CategorizedReport, type CategoryDef, type CategoryOptions, type CategoryRegistry, type ChainDelay, type CheckOptions, type CheckSpecOptions, type ChoiceControl, type ChoiceOption, type ConfinementOptions, type ContainerSpec, type ContractChecker, type ContractContext, type ControlPatch, type ControlValue, type Crack, DEFAULT_BLAST_SHOVE, DEFAULT_ENVIRONMENT, DEFAULT_IGNITION_RULES, type DamageStage, type DamageType, type DebrisKind, type DeepPartial, type Dent, type DestructibleImpl, type DestructibleSpec, type DestructionEvent, type DestructionEventType, type EffectHandle, type EffectRequest, type EffectSink, type Enclosure, type EnclosureBox, type EnvironmentBus, type EnvironmentKey, type EnvironmentState, type ExplosionChain, type FluidKind, type Footprint, type FootprintObject, type FractureStyle, type Fragment, type FragmentHitOptions, type FragmentMark, type GroundLayout, type GroundZone, type HandPose, type HandTargetSource, type HandTargets, type Hit, type HitIgnition, type HitResult, type Hole, type HostEvent, type HostReach, type HostTool, type HostToolAttachment, IMPACT_CONTRACT, type IgnitionRules, type ImpactContract, type ItemAnimationContract, type ItemSeat, type ItemUseDef, type JointKind, type JointSpec, type JointState, KIT_ORIGIN_KEY, type KitOrigin, type LiquidId, type LoopHandle, MISSING_BITS, type MaterialId, type ModuleArea, type ModuleBench, type ModuleControl, type ModuleHost, type ModuleItemFluid, type ModuleItemSeal, type ModuleLayer, type ModuleMetric, type ModuleScenario, type NumberControl, type ObjectState, type PartBreach, type PartShape, type PartSpec, type PartState, type PartStatus, type PartUnits, type PileSpec, type PlayerState, type Quat, type RayHit, type Reinforcement, STAGE_EVENT, type SealBlock, type SealSpec, type SealState, type SegmentKind, type ShelterOptions, type Shot, type SoilLayer, type SoilProfile, type SoilState, type SoundHandle, type SoundRequest, type SpecReport, type StructureRole, type SurfaceLayer, TOOL_CLIP, TWIST_LIFT, type TensionRebar, type TestableItem, type TestableModule, type Thumbnail, type ToolAnimationStep, type ToolAnimator, type ToolClipId, type ToolClipPlayer, type ToolClipPlayerOptions, type ToolClipSet, type ToolCommand, type ToolCommandResult, type ToolKind, type ToolRig, type TunedMassDamper, type TwistOptions, type TwistStroke, type UnmarkedDrawing, type Use, type Vec3, type WarmUpProbes, type WindExposure, type WindState, applyPatches, blastShove, checkAnimation, checkBench, checkCategorized, checkDestructible, checkHit, checkSpec, checkToolClips, createEnvironmentBus, createToolClipPlayer, deepMerge, defineCategories, fillFactor, footprintBlocker, ignitionChance, kitOriginOf, markKitOrigin, mergeById, removeIds, resolveEntry, resolveIgnitionRules, sampleClip, scaleClip, sealBlock, sealOpener, shotExtras, shotToHit, sparkTargetFactor, toolAnimationStep, twistStroke, unmarkedDrawings, useToHit };
2802
+ /**
2803
+ * @adatechnology/game-contracts: the contracts every game module (vehicles, furniture, plants, houses, weapons) depends on, and nothing else.
2804
+ * Types plus tiny pure checkers, no runtime dependencies. The default destruction world, the renderers and the test room are optional.
2805
+ */
2806
+
2807
+ /** @deprecated Import it from `@adatechnology/game-contracts/testing`. */
2808
+ declare const resetWarnOnce: () => void;
2809
+
2810
+ export { type ActionControl, type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type AreaPlacement, type AudioListener, type AudioSink, BLOW_IDS, BODY_FOLEY_IDS, BODY_SOUND_IDS, type BallisticExtras, type BenchChange, type BenchDeclaration, type BenchReport, type BenchSheet, type BlastOptions, type BlastReport, type BlastShove, type BlastShoveOptions, type BlastSource, type BlowId, type BlowTarget, type BodyFoleyId, type BodyRegion, type BodySlot, type BodySoundId, type BooleanControl, type BusyOptions, type BusySink, type BusyTask, type CategorizedOptions, type CategorizedReport, type CategoryDef, type CategoryOptions, type CategoryRegistry, type ChainDelay, type CheckOptions, type CheckSpecOptions, type ChoiceControl, type ChoiceOption, type ConfinementOptions, type ContainerSpec, type ContractChecker, type ContractContext, type ControlPatch, type ControlValue, type CostOptions, type Crack, DEFAULT_BLAST_SHOVE, DEFAULT_ENVIRONMENT, DEFAULT_IGNITION_RULES, DEFAULT_SURFACE, DEFERRED_PRIORITY, DEFERRED_QUEUE_ID, type DamageStage, type DamageType, type DebrisKind, type DeepPartial, type DeferredQueue, type DeferredQueueOptions, type DeferredQueueReport, type Dent, type DestructibleImpl, type DestructibleSpec, type DestructionEvent, type DestructionEventType, type EffectHandle, type EffectRequest, type EffectSink, type Enclosure, type EnclosureBox, type EnvironmentBus, type EnvironmentKey, type EnvironmentState, type ExplosionChain, FOOTSTEP_SOUND_IDS, FRAME_PHASES, type FluidKind, type Footprint, type FootprintObject, type FootstepSoundId, type FractureStyle, type Fragment, type FragmentHitOptions, type FragmentMark, type FrameOptions, type FramePhase, GAIT_IDS, type GaitId, type GroundLayout, type GroundZone, HEAVY_LANDING_SPEED, type HandPose, type HandTargetSource, type HandTargets, type Hit, type HitIgnition, type HitResult, type Hole, type HostEvent, type HostGraphics, type HostReach, type HostTool, type HostToolAttachment, IMPACT_CONTRACT, type IgnitionRules, type ImpactContract, type ItemAnimationContract, type ItemSeat, type ItemSounds, type ItemUseDef, type JointKind, type JointSpec, type JointState, KIT_ORIGIN_KEY, KNOCKED_DOWN_ANGLE, KNOCK_CHEST, KNOCK_DOWN_SPEED, type KitOrigin, type Knock, LANDING_IDS, LAND_SOUND_IDS, type LandSoundId, type LandingId, type LiquidId, type LoopHandle, MATERIAL_SURFACES, MISSING_BITS, type MaterialId, type ModuleArea, type ModuleBench, type ModuleControl, type ModuleHost, type ModuleItemFluid, type ModuleItemSeal, type ModuleLayer, type ModuleMetric, type ModuleScenario, type ModuleView, type NumberControl, type ObjectState, type PartBreach, type PartShape, type PartSpec, type PartState, type PartStatus, type PartUnits, type PileSpec, type PlayerState, type Quat, type RayHit, type Reinforcement, STAGE_EVENT, SURFACE_IDS, SWING_LEAD_SECONDS, type SealBlock, type SealSpec, type SealState, type SegmentKind, type ShelterOptions, type Shot, type SoilLayer, type SoilProfile, type SoilState, type SoundHandle, type SoundRequest, type SpecReport, type StructureRole, type SurfaceId, type SurfaceLayer, type SurfaceRay, type SurfaceRayHit, TOOL_CLIP, TWIST_LIFT, type TensionRebar, type TestableItem, type TestableModule, type Thumbnail, type ToolAnimationStep, type ToolAnimator, type ToolClipId, type ToolClipPlayer, type ToolClipPlayerOptions, type ToolClipSet, type ToolCommand, type ToolCommandResult, type ToolKind, type ToolRig, type TunedMassDamper, type TwistOptions, type TwistStroke, type UnmarkedDrawing, type Use, type Vec3, type WarmUpProbes, type WindExposure, type WindState, applyPatches, blastShove, bodyMass, checkAnimation, checkBench, checkCategorized, checkDeferredQueue, checkDestructible, checkHit, checkSpec, checkToolClips, createEnvironmentBus, createToolClipPlayer, deepMerge, defineCategories, fillFactor, footprintBlocker, footstepSoundId, hitSoundId, ignitionChance, isSurfaceId, kitOriginOf, knockOf, landSoundId, leadClipEvents, markKitOrigin, mergeById, removeIds, resetWarnOnce, resolveEntry, resolveIgnitionRules, sampleClip, scaleClip, sealBlock, sealOpener, shotExtras, shotToHit, sparkTargetFactor, stepKnock, surfaceOfMaterial, swingSoundId, toolAnimationStep, twistStroke, unmarkedDrawings, useToHit };