@adatechnology/game-contracts 0.7.0 → 0.9.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 +9 -0
- package/dist/index.d.ts +296 -47
- package/dist/index.global.js +80 -0
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,15 @@ A module can also declare a bench as data: `controls` (number, boolean, choice,
|
|
|
107
107
|
`ModuleBench` in `attach` (`host.bench`): `value(id)`, `layer(id)`, `onChange(listener)` (right after `attach` it gets one change per control and
|
|
108
108
|
layer, in order), `patch(id, { hidden, disabled, options, value })` (no echo) and `look(eye, target)`. `checkBench({ controls, layers, metrics })`
|
|
109
109
|
checks the declaration (snake_case ids unique across the three, `min < max`, `step > 0`, defaults in range and among the options, `max > 0`).
|
|
110
|
+
A choice's options may carry a `group` (the host lists them under that heading; the options of a group sit next to each other, and `checkBench` says
|
|
111
|
+
so when they do not). `ModuleBench.sheet(sheet)` puts a card on the host's panel (`BenchSheet`: kicker, title, body, facts, steps, colour swatches, note and a
|
|
112
|
+
timeline of segments whose playhead follows a module metric); a host with no sheet area leaves it out.
|
|
113
|
+
|
|
114
|
+
### Effects the host lends
|
|
115
|
+
|
|
116
|
+
`ModuleHost.effects` is an `EffectSink`: `play({ effect, point, direction?, normal?, seed?, param?, scale? })` starts an effect of the host's catalogue by id
|
|
117
|
+
and answers a handle (or `null` when the host does not know it), `stop(handle)` ends a looping one, and `configure`, `stats` and `catalog` are optional. The
|
|
118
|
+
`explosion` host event carries `explosive` (the id of the explosive), `PartSpec.explodes.effect` and the `explode` event name the effect of a part's blast.
|
|
110
119
|
|
|
111
120
|
```ts
|
|
112
121
|
controls: () => [
|
package/dist/index.d.ts
CHANGED
|
@@ -83,6 +83,8 @@ interface PartSpec {
|
|
|
83
83
|
* Each part goes off once. Without `chain`, it explodes only when destroyed (or at `atC`).
|
|
84
84
|
*/
|
|
85
85
|
chain?: ExplosionChain;
|
|
86
|
+
/** Id of the effect the host plays for the blast (`explosion_fuel_barrel`); without it the host picks a generic one by `energy` and `radius`. */
|
|
87
|
+
effect?: string;
|
|
86
88
|
};
|
|
87
89
|
/** Fixed to the world (a wall-mounted cabinet, a tree trunk in the ground). Groups that keep an anchored part stay in place. */
|
|
88
90
|
anchored?: boolean;
|
|
@@ -97,7 +99,54 @@ interface PartSpec {
|
|
|
97
99
|
role: StructureRole;
|
|
98
100
|
capacity?: number;
|
|
99
101
|
};
|
|
102
|
+
/**
|
|
103
|
+
* Coats over the core, outermost first (plaster, tile, the paper of a drywall board). `thickness` of the part is the core only; each coat adds
|
|
104
|
+
* `material.penetration x thickness` to what a bullet must get through, and a hit spalls the outer coat at the impact (`spall` event) before it
|
|
105
|
+
* reaches the core.
|
|
106
|
+
*/
|
|
107
|
+
layers?: readonly SurfaceLayer[];
|
|
108
|
+
/** Steel inside a concrete part: when the part reaches 0 hp the concrete crumbles but the cage stays (`expose`, `PartState.stripped`). */
|
|
109
|
+
reinforcement?: Reinforcement;
|
|
110
|
+
/** Masonry made of units (bricks, blocks, panels): fragments of a breach or a fracture are the size of one unit, laid in `bond`. */
|
|
111
|
+
units?: PartUnits;
|
|
112
|
+
/**
|
|
113
|
+
* Opts the part into damage stages (`PartState.stage`, `stage` events). Through damage (through holes, blunt and explosive damage) within
|
|
114
|
+
* `radius` metres of a point that adds up to `area` square metres opens the part: stage 2, the part stays with an opening.
|
|
115
|
+
*/
|
|
116
|
+
breach?: PartBreach;
|
|
117
|
+
}
|
|
118
|
+
/** A coat over a part's core. `faces` says on which side(s) of the part's thinnest axis it lies (`'+'` and `'-'` are the local +/- directions). */
|
|
119
|
+
interface SurfaceLayer {
|
|
120
|
+
material: MaterialId;
|
|
121
|
+
/** Metres. */
|
|
122
|
+
thickness: number;
|
|
123
|
+
faces: 'both' | '+' | '-';
|
|
124
|
+
}
|
|
125
|
+
/** Rebar of a concrete part. Plates get two perpendicular grids at mid thickness, long members (columns, beams) bars along their length. */
|
|
126
|
+
interface Reinforcement {
|
|
127
|
+
material: MaterialId;
|
|
128
|
+
/** Metres between bars (0.18). */
|
|
129
|
+
spacing: number;
|
|
130
|
+
/** Bar diameter in metres (0.007 to 0.010). */
|
|
131
|
+
diameter: number;
|
|
132
|
+
/** Fraction of the part's structural capacity the bare cage keeps (default 0.15). */
|
|
133
|
+
residualCapacity?: number;
|
|
134
|
+
}
|
|
135
|
+
/** Size of one masonry unit in the part's axes (`size[2]` is along the part's thinnest axis, clamped to it) and how rows are laid. */
|
|
136
|
+
interface PartUnits {
|
|
137
|
+
size: Vec3;
|
|
138
|
+
bond: 'running' | 'stack';
|
|
139
|
+
mortar: MaterialId;
|
|
140
|
+
}
|
|
141
|
+
interface PartBreach {
|
|
142
|
+
/** Square metres of through damage inside the breach radius that open the part (default 0.25). */
|
|
143
|
+
area?: number;
|
|
100
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Damage stage of a part with `breach`/`layers`/`units`/`reinforcement`: 0 intact, 1 marked (first hole or crack), 2 breached (an opening you can
|
|
147
|
+
* pass through), 3 gone (fractured or collapsed).
|
|
148
|
+
*/
|
|
149
|
+
type DamageStage = 0 | 1 | 2 | 3;
|
|
101
150
|
/**
|
|
102
151
|
* 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
|
|
103
152
|
* breaking it never drops anything.
|
|
@@ -174,6 +223,12 @@ interface Hit {
|
|
|
174
223
|
space?: 'world' | 'local';
|
|
175
224
|
/** Who caused it (passed through to events). */
|
|
176
225
|
source?: string;
|
|
226
|
+
/** Projectile mass in grams (ballistic): the penetration of a body needs it, the energy alone does not say how much a bullet pushes. */
|
|
227
|
+
mass?: number;
|
|
228
|
+
/** 0..1, how much of the blow is an edge that cuts through (slash 0.85, blunt 0, explosive 1; a bullet carries its own). */
|
|
229
|
+
cut?: number;
|
|
230
|
+
/** A part the caller already resolved, by id (a crowd raycast against capsules); the impl then skips its own part search. */
|
|
231
|
+
part?: string;
|
|
177
232
|
}
|
|
178
233
|
type PartStatus = 'intact' | 'damaged'
|
|
179
234
|
/** the part lost its support path to the ground and fell with its group (reported by a `collapse` event); it still exists as a piece */
|
|
@@ -235,6 +290,15 @@ interface PartState {
|
|
|
235
290
|
*/
|
|
236
291
|
load?: number;
|
|
237
292
|
stress?: number;
|
|
293
|
+
/** Damage stage; only present on parts that opted in (see `PartSpec.breach`). */
|
|
294
|
+
stage?: DamageStage;
|
|
295
|
+
/** Reinforced part whose concrete is gone: only the rebar cage is left (capacity x `residualCapacity`); it is still part of the structure. */
|
|
296
|
+
stripped?: boolean;
|
|
297
|
+
/** Opening a breach left, in object-local space (stage 2). Raycasts and shots pass through it. */
|
|
298
|
+
opening?: {
|
|
299
|
+
center: Vec3;
|
|
300
|
+
radius: number;
|
|
301
|
+
};
|
|
238
302
|
/** Index into `ObjectState.groups` of the piece this part belongs to (-1 when the part is broken, melted or ash). */
|
|
239
303
|
group: number;
|
|
240
304
|
}
|
|
@@ -266,6 +330,14 @@ interface ObjectState {
|
|
|
266
330
|
groups: string[][];
|
|
267
331
|
/** Every part is broken, melted or ash. */
|
|
268
332
|
destroyed: boolean;
|
|
333
|
+
/** Only objects that are a living body (a character, an animal): the summary the crowd and the HUD read. */
|
|
334
|
+
body?: {
|
|
335
|
+
alive: boolean;
|
|
336
|
+
vitality: number;
|
|
337
|
+
missing: number;
|
|
338
|
+
limp: boolean;
|
|
339
|
+
crawl: boolean;
|
|
340
|
+
};
|
|
269
341
|
}
|
|
270
342
|
/** A fragment for the renderer: an oriented box (or shard) with initial motion. Coordinates in world space. */
|
|
271
343
|
interface Fragment {
|
|
@@ -280,6 +352,14 @@ interface Fragment {
|
|
|
280
352
|
velocity: Vec3;
|
|
281
353
|
angularVelocity: Vec3;
|
|
282
354
|
mass: number;
|
|
355
|
+
/** Holes the part had where this fragment came from, in the fragment's own axes: the renderer keeps them on the debris. */
|
|
356
|
+
marks?: readonly FragmentMark[];
|
|
357
|
+
}
|
|
358
|
+
interface FragmentMark {
|
|
359
|
+
/** Relative to the fragment's centre, in the fragment's axes. */
|
|
360
|
+
point: Vec3;
|
|
361
|
+
normal: Vec3;
|
|
362
|
+
diameter: number;
|
|
283
363
|
}
|
|
284
364
|
interface EventBase {
|
|
285
365
|
object: string;
|
|
@@ -375,6 +455,7 @@ type DestructionEvent = (EventBase & {
|
|
|
375
455
|
point: Vec3;
|
|
376
456
|
energy: number;
|
|
377
457
|
radius: number;
|
|
458
|
+
effect?: string;
|
|
378
459
|
})
|
|
379
460
|
/** An explosive part was set off by a bullet, fire or a nearby blast and will explode `delay` seconds from now (`explodes.chain`). */
|
|
380
461
|
| (EventBase & {
|
|
@@ -396,6 +477,43 @@ type DestructionEvent = (EventBase & {
|
|
|
396
477
|
type: 'collapse';
|
|
397
478
|
parts: string[];
|
|
398
479
|
mass: number;
|
|
480
|
+
})
|
|
481
|
+
/** The outer coat of a layered part came off at `point` (`layer` indexes `PartSpec.layers`; `area` in m²). */
|
|
482
|
+
| (EventBase & {
|
|
483
|
+
type: 'spall';
|
|
484
|
+
part: string;
|
|
485
|
+
layer: number;
|
|
486
|
+
point: Vec3;
|
|
487
|
+
area: number;
|
|
488
|
+
})
|
|
489
|
+
/**
|
|
490
|
+
* A staged part changed stage. At stage 2 `opening` is where the breach is and `fragments` the unit-sized pieces that came out (the part is not
|
|
491
|
+
* removed: draw the fragments and cut the opening, do not hide the part). Stage 3 follows a `fracture` or a `collapse`.
|
|
492
|
+
*/
|
|
493
|
+
| (EventBase & {
|
|
494
|
+
type: 'stage';
|
|
495
|
+
part: string;
|
|
496
|
+
stage: DamageStage;
|
|
497
|
+
opening?: {
|
|
498
|
+
center: Vec3;
|
|
499
|
+
radius: number;
|
|
500
|
+
};
|
|
501
|
+
fragments?: readonly Fragment[];
|
|
502
|
+
})
|
|
503
|
+
/** Rebar of a reinforced part is bare: segments in world space. Empty `bars` means the cage is gone. */
|
|
504
|
+
| (EventBase & {
|
|
505
|
+
type: 'expose';
|
|
506
|
+
part: string;
|
|
507
|
+
bars: readonly [Vec3, Vec3][];
|
|
508
|
+
})
|
|
509
|
+
/** A living body changed: a wound, a broken or severed limb, a destroyed segment, a split torso, limp, crawl, stun or death. `below` lists the parts that fall with a severed one. */
|
|
510
|
+
| (EventBase & {
|
|
511
|
+
type: 'body';
|
|
512
|
+
part?: string;
|
|
513
|
+
change: 'wound' | 'broken' | 'severed' | 'destroyed' | 'split' | 'limp' | 'crawl' | 'stunned' | 'killed';
|
|
514
|
+
cause?: 'head' | 'vitality' | 'bleed' | 'explosion';
|
|
515
|
+
below?: string[];
|
|
516
|
+
exit?: Vec3;
|
|
399
517
|
}) | (EventBase & {
|
|
400
518
|
type: 'destroyed';
|
|
401
519
|
});
|
|
@@ -477,6 +595,26 @@ declare const checkDestructible: (impl: DestructibleImpl, options?: CheckOptions
|
|
|
477
595
|
problems: string[];
|
|
478
596
|
};
|
|
479
597
|
|
|
598
|
+
/** The slots of a body an item can cover (the item slot vocabulary: game-items imports it from here). */
|
|
599
|
+
type BodySlot = 'head' | 'eyes' | 'torso' | 'arms' | 'hands' | 'legs' | 'feet' | 'over';
|
|
600
|
+
/** The 11 segments of the detailed body model (the Arsenal ids). */
|
|
601
|
+
type SegmentKind = 'head' | 'jaw' | 'neck' | 'torso' | 'pelvis' | 'upperArm' | 'forearm' | 'hand' | 'thigh' | 'shin' | 'foot';
|
|
602
|
+
/** The 6 regions of the crowd model (the game regions 0..5). */
|
|
603
|
+
type BodyRegion = 'head' | 'torso' | 'armL' | 'armR' | 'legL' | 'legR';
|
|
604
|
+
/** Bits of the `missing` mask of a body. The instanced body shader reads them, so the values are part of the contract. */
|
|
605
|
+
declare const MISSING_BITS: {
|
|
606
|
+
readonly head: 1;
|
|
607
|
+
readonly armL: 2;
|
|
608
|
+
readonly armR: 4;
|
|
609
|
+
readonly legL: 8;
|
|
610
|
+
readonly legR: 16;
|
|
611
|
+
readonly armLBroken: 32;
|
|
612
|
+
readonly armRBroken: 64;
|
|
613
|
+
readonly legLBroken: 128;
|
|
614
|
+
readonly legRBroken: 256;
|
|
615
|
+
readonly jaw: 512;
|
|
616
|
+
};
|
|
617
|
+
|
|
480
618
|
interface CheckSpecOptions {
|
|
481
619
|
/** Known material ids. When given, every part's material must be one of them. */
|
|
482
620
|
materials?: Iterable<string>;
|
|
@@ -511,6 +649,8 @@ interface Shot {
|
|
|
511
649
|
caliberId: string;
|
|
512
650
|
/** Muzzle speed in m/s. */
|
|
513
651
|
speed: number;
|
|
652
|
+
/** Projectile mass in grams (one pellet for a shotgun load). */
|
|
653
|
+
mass?: number;
|
|
514
654
|
/** Energy at a distance in metres, with the calibre's drag. */
|
|
515
655
|
energyAt(distance: number): number;
|
|
516
656
|
pellet: number;
|
|
@@ -683,50 +823,6 @@ interface ItemUseDef {
|
|
|
683
823
|
liquids?: LiquidId[];
|
|
684
824
|
}
|
|
685
825
|
|
|
686
|
-
/** 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. */
|
|
687
|
-
interface AnimationPose {
|
|
688
|
-
position?: Vec3;
|
|
689
|
-
rotation?: Vec3;
|
|
690
|
-
}
|
|
691
|
-
interface AnimationKey {
|
|
692
|
-
/** Seconds from the start of the clip. */
|
|
693
|
-
t: number;
|
|
694
|
-
/** Pose of the item's root. */
|
|
695
|
-
position?: Vec3;
|
|
696
|
-
rotation?: Vec3;
|
|
697
|
-
/** Poses of named parts: the key is a destruction part id (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */
|
|
698
|
-
parts?: Record<string, AnimationPose>;
|
|
699
|
-
}
|
|
700
|
-
interface AnimationEventSpec {
|
|
701
|
-
/** Seconds from the start of the clip. */
|
|
702
|
-
t: number;
|
|
703
|
-
/** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */
|
|
704
|
-
type: string;
|
|
705
|
-
data?: Record<string, unknown>;
|
|
706
|
-
}
|
|
707
|
-
/** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */
|
|
708
|
-
interface AnimationClipSpec {
|
|
709
|
-
/** snake_case: `reload`, `open`, `drink`... Unique within the item. */
|
|
710
|
-
id: string;
|
|
711
|
-
/** Seconds. */
|
|
712
|
-
duration: number;
|
|
713
|
-
keys: AnimationKey[];
|
|
714
|
-
events?: AnimationEventSpec[];
|
|
715
|
-
}
|
|
716
|
-
/** What an item exposes when it brings its own animations. */
|
|
717
|
-
interface ItemAnimationContract {
|
|
718
|
-
animations: AnimationClipSpec[];
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* 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
|
|
722
|
-
* `[0, duration]` and not going backwards, finite vectors, every named part one of `partIds`, event types that are non-empty strings and
|
|
723
|
-
* JSON-serializable event data.
|
|
724
|
-
*/
|
|
725
|
-
declare const checkAnimation: (clip: AnimationClipSpec, partIds: Iterable<string>) => {
|
|
726
|
-
ok: boolean;
|
|
727
|
-
problems: string[];
|
|
728
|
-
};
|
|
729
|
-
|
|
730
826
|
/**
|
|
731
827
|
* The bench of a module: controls, layers and counters it declares as data. The host keeps the values, draws them in a generic way and hands
|
|
732
828
|
* every change back through `ModuleHost.bench`. Nothing here is specific to a kind of module.
|
|
@@ -735,6 +831,8 @@ type ControlValue = number | boolean | string;
|
|
|
735
831
|
interface ChoiceOption {
|
|
736
832
|
readonly id: string;
|
|
737
833
|
readonly name: string;
|
|
834
|
+
/** Heading the host lists the option under (`optgroup`, a grouped row of buttons). Options of a group must sit next to each other. */
|
|
835
|
+
readonly group?: string;
|
|
738
836
|
}
|
|
739
837
|
interface ControlBase {
|
|
740
838
|
/** snake_case, unique across the module's controls, layers and metrics. */
|
|
@@ -811,6 +909,41 @@ interface ModuleBench {
|
|
|
811
909
|
patch(id: string, patch: ControlPatch): void;
|
|
812
910
|
/** Move the host camera (a near / middle / far choice). */
|
|
813
911
|
look(eye: Vec3, target: Vec3): void;
|
|
912
|
+
/** Shows a sheet on the host's panel, or takes it down with `null`. A host without a sheet area ignores it. */
|
|
913
|
+
sheet?(sheet: BenchSheet | null): void;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* A card of text the module puts on the host's panel (the sheet of an effect, of a weapon): what it is, its numbers, its steps, its colours and,
|
|
917
|
+
* optionally, a strip of segments over time. Names and texts are already translated for the host's locale.
|
|
918
|
+
*/
|
|
919
|
+
interface BenchSheet {
|
|
920
|
+
readonly kicker?: string;
|
|
921
|
+
readonly title: string;
|
|
922
|
+
readonly body?: string;
|
|
923
|
+
readonly facts?: readonly {
|
|
924
|
+
readonly label: string;
|
|
925
|
+
readonly value: string;
|
|
926
|
+
}[];
|
|
927
|
+
readonly steps?: readonly {
|
|
928
|
+
readonly at: string;
|
|
929
|
+
readonly text: string;
|
|
930
|
+
}[];
|
|
931
|
+
readonly swatches?: readonly {
|
|
932
|
+
readonly color: string;
|
|
933
|
+
readonly name: string;
|
|
934
|
+
}[];
|
|
935
|
+
readonly note?: string;
|
|
936
|
+
/** A strip of named segments (seconds), with tick marks and a playhead that follows the module metric `metric`. */
|
|
937
|
+
readonly timeline?: {
|
|
938
|
+
readonly segments: readonly {
|
|
939
|
+
readonly name: string;
|
|
940
|
+
readonly start: number;
|
|
941
|
+
readonly end: number;
|
|
942
|
+
readonly color: string;
|
|
943
|
+
}[];
|
|
944
|
+
readonly ticks: readonly number[];
|
|
945
|
+
readonly metric: string;
|
|
946
|
+
};
|
|
814
947
|
}
|
|
815
948
|
interface BenchReport {
|
|
816
949
|
readonly ok: boolean;
|
|
@@ -821,9 +954,88 @@ interface BenchDeclaration {
|
|
|
821
954
|
readonly layers?: readonly ModuleLayer[];
|
|
822
955
|
readonly metrics?: readonly ModuleMetric[];
|
|
823
956
|
}
|
|
824
|
-
/** snake_case ids unique across controls, layers and metrics; min < max; step > 0; default in range; options non-empty, unique, default among them; metric max > 0. */
|
|
957
|
+
/** snake_case ids unique across controls, layers and metrics; min < max; step > 0; default in range; options non-empty, unique, default among them, each group in one run; metric max > 0. */
|
|
825
958
|
declare function checkBench(declared: BenchDeclaration): BenchReport;
|
|
826
959
|
|
|
960
|
+
/** What a module asks the host to show: an effect by id (`explosion_m67`, `impact_concrete`, `muzzle_ak`), where, and how. */
|
|
961
|
+
interface EffectRequest {
|
|
962
|
+
/** An id of the host's effect catalogue (`EffectSink.catalog`). A host that does not know it plays nothing. */
|
|
963
|
+
readonly effect: string;
|
|
964
|
+
readonly point: Vec3;
|
|
965
|
+
/** Where the effect is aimed (a cone, a jet). */
|
|
966
|
+
readonly direction?: Vec3;
|
|
967
|
+
/** The surface normal at `point` (an impact, a decal). */
|
|
968
|
+
readonly normal?: Vec3;
|
|
969
|
+
/** Makes the visual repeatable: the same seed draws the same effect, so a network event of a few bytes is enough on every client. */
|
|
970
|
+
readonly seed?: number;
|
|
971
|
+
/** The variation of the effect (an age, a material, a condition): an option index or an option id of the effect. */
|
|
972
|
+
readonly param?: number | string;
|
|
973
|
+
/** Multiplies the effect's size and amount. */
|
|
974
|
+
readonly scale?: number;
|
|
975
|
+
}
|
|
976
|
+
/** Identifies a running effect so it can be stopped. */
|
|
977
|
+
type EffectHandle = number;
|
|
978
|
+
/**
|
|
979
|
+
* What the host lends a module to show effects (smoke, sparks, blast, blood mist) without bringing its own particles. Nothing here is specific
|
|
980
|
+
* to a kind of effect or to a renderer.
|
|
981
|
+
*/
|
|
982
|
+
interface EffectSink {
|
|
983
|
+
/** Starts the effect; `null` when the host does not know it, the layer is off or the budget is spent. */
|
|
984
|
+
play(request: EffectRequest): EffectHandle | null;
|
|
985
|
+
/** Ends a looping effect now (a fire, a trail). A handle that already ended is ignored. */
|
|
986
|
+
stop(handle: EffectHandle): void;
|
|
987
|
+
/** Sets what the host lets a module tune (`quality`, `lod_band`...): the id of the setting and its value. A host applies the ids it knows. */
|
|
988
|
+
configure?(settings: Readonly<Record<string, ControlValue>>): void;
|
|
989
|
+
/** Counters of what is alive (`puffs`, `sparks`, `lights`...), for a bench. */
|
|
990
|
+
stats?(): Readonly<Record<string, number>>;
|
|
991
|
+
/** The ids `play` accepts. */
|
|
992
|
+
catalog?(): readonly string[];
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** 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. */
|
|
996
|
+
interface AnimationPose {
|
|
997
|
+
position?: Vec3;
|
|
998
|
+
rotation?: Vec3;
|
|
999
|
+
}
|
|
1000
|
+
interface AnimationKey {
|
|
1001
|
+
/** Seconds from the start of the clip. */
|
|
1002
|
+
t: number;
|
|
1003
|
+
/** Pose of the item's root. */
|
|
1004
|
+
position?: Vec3;
|
|
1005
|
+
rotation?: Vec3;
|
|
1006
|
+
/** Poses of named parts: the key is a destruction part id (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */
|
|
1007
|
+
parts?: Record<string, AnimationPose>;
|
|
1008
|
+
}
|
|
1009
|
+
interface AnimationEventSpec {
|
|
1010
|
+
/** Seconds from the start of the clip. */
|
|
1011
|
+
t: number;
|
|
1012
|
+
/** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */
|
|
1013
|
+
type: string;
|
|
1014
|
+
data?: Record<string, unknown>;
|
|
1015
|
+
}
|
|
1016
|
+
/** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */
|
|
1017
|
+
interface AnimationClipSpec {
|
|
1018
|
+
/** snake_case: `reload`, `open`, `drink`... Unique within the item. */
|
|
1019
|
+
id: string;
|
|
1020
|
+
/** Seconds. */
|
|
1021
|
+
duration: number;
|
|
1022
|
+
keys: AnimationKey[];
|
|
1023
|
+
events?: AnimationEventSpec[];
|
|
1024
|
+
}
|
|
1025
|
+
/** What an item exposes when it brings its own animations. */
|
|
1026
|
+
interface ItemAnimationContract {
|
|
1027
|
+
animations: AnimationClipSpec[];
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* 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
|
|
1031
|
+
* `[0, duration]` and not going backwards, finite vectors, every named part one of `partIds`, event types that are non-empty strings and
|
|
1032
|
+
* JSON-serializable event data.
|
|
1033
|
+
*/
|
|
1034
|
+
declare const checkAnimation: (clip: AnimationClipSpec, partIds: Iterable<string>) => {
|
|
1035
|
+
ok: boolean;
|
|
1036
|
+
problems: string[];
|
|
1037
|
+
};
|
|
1038
|
+
|
|
827
1039
|
/** 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. */
|
|
828
1040
|
interface ModuleItemFluid {
|
|
829
1041
|
container: ContainerSpec;
|
|
@@ -832,6 +1044,16 @@ interface ModuleItemFluid {
|
|
|
832
1044
|
uses?: string[];
|
|
833
1045
|
offset?: Vec3;
|
|
834
1046
|
}
|
|
1047
|
+
/** A picture of an item for catalogues and inventories: a hand-made `svg`, or `render` to have the host draw the 3D model (camera angles in radians, `wear` 0 new, 1 worn, 2 ruined). */
|
|
1048
|
+
interface Thumbnail {
|
|
1049
|
+
svg?: string;
|
|
1050
|
+
render?: true | {
|
|
1051
|
+
yaw?: number;
|
|
1052
|
+
pitch?: number;
|
|
1053
|
+
distance?: number;
|
|
1054
|
+
wear?: 0 | 1 | 2;
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
835
1057
|
/** One thing a module can put in the test room (or any viewer): a toy car, a chair, a pistol on a table. */
|
|
836
1058
|
interface TestableItem {
|
|
837
1059
|
/** snake_case id, unique inside the module. */
|
|
@@ -912,6 +1134,12 @@ type HostEvent =
|
|
|
912
1134
|
rounds?: number;
|
|
913
1135
|
mode?: string;
|
|
914
1136
|
suppressed?: boolean;
|
|
1137
|
+
/**
|
|
1138
|
+
* Where the muzzle is in the world and where it points, for a shot the viewer sees from outside (another player, the third-person camera). A module
|
|
1139
|
+
* then shows the shooter's flash and casing through `host.effects`. Absent for the player's own shot, which the weapon in the hands already draws.
|
|
1140
|
+
*/
|
|
1141
|
+
origin?: Vec3;
|
|
1142
|
+
direction?: Vec3;
|
|
915
1143
|
}
|
|
916
1144
|
/** The trigger was pulled, the round was spent and nothing left the tool (a dud cartridge): no projectile, no recoil. */
|
|
917
1145
|
| {
|
|
@@ -1006,7 +1234,10 @@ type HostEvent =
|
|
|
1006
1234
|
on: boolean;
|
|
1007
1235
|
duration?: number;
|
|
1008
1236
|
}
|
|
1009
|
-
/**
|
|
1237
|
+
/**
|
|
1238
|
+
* Something blew up: a thrown or launched explosive, a charge, or a part of the world (a fuel barrel); `source` is the weapon or the object, `cause` what set it off.
|
|
1239
|
+
* `explosive` is the id of the explosive when there is one (`m67`, `c4`, `fuel_barrel`): the host picks the blast by it.
|
|
1240
|
+
*/
|
|
1010
1241
|
| {
|
|
1011
1242
|
type: 'explosion';
|
|
1012
1243
|
source: string;
|
|
@@ -1014,6 +1245,7 @@ type HostEvent =
|
|
|
1014
1245
|
energy: number;
|
|
1015
1246
|
radius: number;
|
|
1016
1247
|
cause: string;
|
|
1248
|
+
explosive?: string;
|
|
1017
1249
|
};
|
|
1018
1250
|
/** The tool in hand as the host sees it. */
|
|
1019
1251
|
interface HostTool {
|
|
@@ -1103,6 +1335,8 @@ interface ModuleHost {
|
|
|
1103
1335
|
command?(command: ToolCommand): ToolCommandResult;
|
|
1104
1336
|
/** Lent only to a module that declares controls, layers or metrics. */
|
|
1105
1337
|
bench?: ModuleBench;
|
|
1338
|
+
/** 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. */
|
|
1339
|
+
effects?: EffectSink;
|
|
1106
1340
|
}
|
|
1107
1341
|
/** What a module hands the test room: its items and, optionally, scenarios that exercise them, its area and its own presentation. */
|
|
1108
1342
|
interface TestableModule<Context = unknown> {
|
|
@@ -1209,4 +1443,19 @@ interface ToolClipPlayer {
|
|
|
1209
1443
|
}
|
|
1210
1444
|
declare const createToolClipPlayer: (options: ToolClipPlayerOptions) => ToolClipPlayer;
|
|
1211
1445
|
|
|
1212
|
-
|
|
1446
|
+
type Entry = {
|
|
1447
|
+
readonly id: string;
|
|
1448
|
+
};
|
|
1449
|
+
/** Recursively optional; arrays and functions are replaced as a whole, objects are merged. */
|
|
1450
|
+
type DeepPartial<TValue> = TValue extends (...args: never[]) => unknown ? TValue : TValue extends readonly unknown[] ? TValue : TValue extends object ? {
|
|
1451
|
+
[TKey in keyof TValue]?: DeepPartial<TValue[TKey]>;
|
|
1452
|
+
} : TValue;
|
|
1453
|
+
/** Merges `patch` over `base`: plain objects merge field by field, arrays, functions and primitives replace. `undefined` in the patch is ignored. */
|
|
1454
|
+
declare const deepMerge: <TBase extends object>(base: TBase, patch: object | undefined) => TBase;
|
|
1455
|
+
/** The shipped entries with the ones from `extra` added or, when the id exists, replaced. */
|
|
1456
|
+
declare const mergeById: <TEntry extends Entry>(shipped: readonly TEntry[], extra: readonly TEntry[] | undefined) => Map<string, TEntry>;
|
|
1457
|
+
/** Applies `patch` by id; a patch for an id that does not exist is ignored. */
|
|
1458
|
+
declare const applyPatches: <TEntry extends Entry>(entries: Map<string, TEntry>, patches: Readonly<Record<string, DeepPartial<TEntry> | Partial<TEntry>>> | undefined) => void;
|
|
1459
|
+
declare const removeIds: (entries: Map<string, unknown>, ids: readonly string[] | undefined) => void;
|
|
1460
|
+
|
|
1461
|
+
export { type ActionControl, type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type AreaPlacement, type BenchChange, type BenchDeclaration, type BenchReport, type BenchSheet, type BodyRegion, type BodySlot, type BooleanControl, type ChainDelay, type CheckOptions, type CheckSpecOptions, type ChoiceControl, type ChoiceOption, type ContainerSpec, type ControlPatch, type ControlValue, type Crack, 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 ExplosionChain, type FluidKind, type FractureStyle, type Fragment, type FragmentMark, type Hit, type HitResult, type Hole, type HostEvent, type HostTool, type HostToolAttachment, IMPACT_CONTRACT, type ImpactContract, type ItemAnimationContract, type ItemUseDef, type JointKind, type JointSpec, type JointState, type LiquidId, MISSING_BITS, type MaterialId, type ModuleArea, type ModuleBench, type ModuleControl, type ModuleHost, type ModuleItemFluid, type ModuleLayer, type ModuleMetric, type ModuleScenario, type NumberControl, type ObjectState, type PartBreach, type PartShape, type PartSpec, type PartState, type PartStatus, type PartUnits, type Quat, type RayHit, type Reinforcement, STAGE_EVENT, type SegmentKind, type Shot, type SpecReport, type StructureRole, type SurfaceLayer, TOOL_CLIP, 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 Use, type Vec3, applyPatches, checkAnimation, checkBench, checkDestructible, checkHit, checkSpec, checkToolClips, createToolClipPlayer, deepMerge, mergeById, removeIds, sampleClip, scaleClip, shotToHit, toolAnimationStep };
|
package/dist/index.global.js
CHANGED
|
@@ -22,8 +22,10 @@ var GameContracts = (() => {
|
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
24
|
IMPACT_CONTRACT: () => IMPACT_CONTRACT,
|
|
25
|
+
MISSING_BITS: () => MISSING_BITS,
|
|
25
26
|
STAGE_EVENT: () => STAGE_EVENT,
|
|
26
27
|
TOOL_CLIP: () => TOOL_CLIP,
|
|
28
|
+
applyPatches: () => applyPatches,
|
|
27
29
|
checkAnimation: () => checkAnimation,
|
|
28
30
|
checkBench: () => checkBench,
|
|
29
31
|
checkDestructible: () => checkDestructible,
|
|
@@ -31,6 +33,9 @@ var GameContracts = (() => {
|
|
|
31
33
|
checkSpec: () => checkSpec,
|
|
32
34
|
checkToolClips: () => checkToolClips,
|
|
33
35
|
createToolClipPlayer: () => createToolClipPlayer,
|
|
36
|
+
deepMerge: () => deepMerge,
|
|
37
|
+
mergeById: () => mergeById,
|
|
38
|
+
removeIds: () => removeIds,
|
|
34
39
|
sampleClip: () => sampleClip,
|
|
35
40
|
scaleClip: () => scaleClip,
|
|
36
41
|
shotToHit: () => shotToHit,
|
|
@@ -146,6 +151,20 @@ var GameContracts = (() => {
|
|
|
146
151
|
return { ok: problems.length === 0, problems };
|
|
147
152
|
};
|
|
148
153
|
|
|
154
|
+
// src/body.ts
|
|
155
|
+
var MISSING_BITS = {
|
|
156
|
+
head: 1,
|
|
157
|
+
armL: 2,
|
|
158
|
+
armR: 4,
|
|
159
|
+
legL: 8,
|
|
160
|
+
legR: 16,
|
|
161
|
+
armLBroken: 32,
|
|
162
|
+
armRBroken: 64,
|
|
163
|
+
legLBroken: 128,
|
|
164
|
+
legRBroken: 256,
|
|
165
|
+
jaw: 512
|
|
166
|
+
};
|
|
167
|
+
|
|
149
168
|
// src/spec.ts
|
|
150
169
|
var STRUCTURE_ROLES = ["foundation", "column", "beam", "slab", "wall", "partition", "roof", "stair", "none"];
|
|
151
170
|
var BEARS = [true, false, "a_on_b", "b_on_a"];
|
|
@@ -153,6 +172,31 @@ var GameContracts = (() => {
|
|
|
153
172
|
var positive = (n) => finite(n) && n > 0;
|
|
154
173
|
var vec3 = (v) => Array.isArray(v) && v.length === 3 && v.every(finite);
|
|
155
174
|
var nonNegative = (n) => n === void 0 || finite(n) && n >= 0;
|
|
175
|
+
var FACES = ["both", "+", "-"];
|
|
176
|
+
var BONDS = ["running", "stack"];
|
|
177
|
+
var checkStagedFields = (p, label, materials, problems) => {
|
|
178
|
+
const known = (m, what) => {
|
|
179
|
+
if (materials && !materials.has(m)) problems.push(`${label} ${what} has unknown material "${String(m)}"`);
|
|
180
|
+
};
|
|
181
|
+
(p?.layers ?? []).forEach((l, i) => {
|
|
182
|
+
known(l?.material, `layer ${i}`);
|
|
183
|
+
if (!positive(l?.thickness)) problems.push(`${label} layer ${i} thickness must be finite and positive`);
|
|
184
|
+
if (!FACES.includes(l?.faces)) problems.push(`${label} layer ${i} has an invalid faces "${String(l?.faces)}"`);
|
|
185
|
+
});
|
|
186
|
+
const r = p?.reinforcement;
|
|
187
|
+
if (r) {
|
|
188
|
+
known(r.material, "reinforcement");
|
|
189
|
+
if (!positive(r.spacing) || !positive(r.diameter)) problems.push(`${label} reinforcement spacing and diameter must be finite and positive`);
|
|
190
|
+
if (r.residualCapacity !== void 0 && !(finite(r.residualCapacity) && r.residualCapacity >= 0 && r.residualCapacity <= 1)) problems.push(`${label} reinforcement residualCapacity must be between 0 and 1`);
|
|
191
|
+
}
|
|
192
|
+
const u = p?.units;
|
|
193
|
+
if (u) {
|
|
194
|
+
known(u.mortar, "units mortar");
|
|
195
|
+
if (!vec3(u.size) || !u.size.every(positive)) problems.push(`${label} units size must be three finite positive numbers`);
|
|
196
|
+
if (!BONDS.includes(u.bond)) problems.push(`${label} units has an invalid bond "${String(u.bond)}"`);
|
|
197
|
+
}
|
|
198
|
+
if (p?.breach && p.breach.area !== void 0 && !positive(p.breach.area)) problems.push(`${label} breach area must be finite and positive`);
|
|
199
|
+
};
|
|
156
200
|
var checkSpec = (spec, options = {}) => {
|
|
157
201
|
const problems = [];
|
|
158
202
|
const warnings = [];
|
|
@@ -185,6 +229,7 @@ var GameContracts = (() => {
|
|
|
185
229
|
if (!nonNegative(p.structure.capacity)) problems.push(`${label} structure capacity must be a finite number >= 0`);
|
|
186
230
|
}
|
|
187
231
|
if (p?.contains && !nonNegative(p.contains.liters)) problems.push(`${label} contains.liters must be a finite number >= 0`);
|
|
232
|
+
checkStagedFields(p, label, materials, problems);
|
|
188
233
|
}
|
|
189
234
|
const jointIds = /* @__PURE__ */ new Set();
|
|
190
235
|
const edges = [];
|
|
@@ -237,6 +282,7 @@ var GameContracts = (() => {
|
|
|
237
282
|
direction: [shot.direction[0], shot.direction[1], shot.direction[2]],
|
|
238
283
|
energy: shot.energyAt(distance),
|
|
239
284
|
caliber: shot.caliber,
|
|
285
|
+
...shot.mass === void 0 ? {} : { mass: shot.mass },
|
|
240
286
|
source: shot.source ?? shot.weapon
|
|
241
287
|
};
|
|
242
288
|
};
|
|
@@ -459,6 +505,34 @@ var GameContracts = (() => {
|
|
|
459
505
|
};
|
|
460
506
|
};
|
|
461
507
|
|
|
508
|
+
// src/registry.ts
|
|
509
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
510
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
511
|
+
var deepMerge = (base, patch) => {
|
|
512
|
+
if (!patch) return base;
|
|
513
|
+
const result = { ...base };
|
|
514
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
515
|
+
if (value === void 0 || UNSAFE_KEYS.has(key)) continue;
|
|
516
|
+
const current = result[key];
|
|
517
|
+
result[key] = isPlainObject(current) && isPlainObject(value) ? deepMerge(current, value) : value;
|
|
518
|
+
}
|
|
519
|
+
return result;
|
|
520
|
+
};
|
|
521
|
+
var mergeById = (shipped, extra) => {
|
|
522
|
+
const entries = /* @__PURE__ */ new Map();
|
|
523
|
+
for (const entry of [...shipped, ...extra ?? []]) entries.set(entry.id, entry);
|
|
524
|
+
return entries;
|
|
525
|
+
};
|
|
526
|
+
var applyPatches = (entries, patches) => {
|
|
527
|
+
for (const [id, patch] of Object.entries(patches ?? {})) {
|
|
528
|
+
const entry = entries.get(id);
|
|
529
|
+
if (entry) entries.set(id, deepMerge(entry, patch));
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
var removeIds = (entries, ids) => {
|
|
533
|
+
for (const id of ids ?? []) entries.delete(id);
|
|
534
|
+
};
|
|
535
|
+
|
|
462
536
|
// src/bench.ts
|
|
463
537
|
var SNAKE_CASE = /^[a-z][a-z0-9_]*$/;
|
|
464
538
|
var finite2 = (n) => typeof n === "number" && Number.isFinite(n);
|
|
@@ -476,9 +550,15 @@ var GameContracts = (() => {
|
|
|
476
550
|
return;
|
|
477
551
|
}
|
|
478
552
|
const seen = /* @__PURE__ */ new Set();
|
|
553
|
+
const groups = /* @__PURE__ */ new Set();
|
|
554
|
+
let previousGroup;
|
|
479
555
|
for (const option of control.options) {
|
|
480
556
|
if (seen.has(option.id)) problems.push(`${label} repeats option "${option.id}"`);
|
|
481
557
|
seen.add(option.id);
|
|
558
|
+
if (option.group !== void 0 && (typeof option.group !== "string" || option.group === "")) problems.push(`${label} option "${option.id}" has an empty group`);
|
|
559
|
+
else if (option.group !== void 0 && option.group !== previousGroup && groups.has(option.group)) problems.push(`${label} group "${option.group}" is split: its options must be next to each other`);
|
|
560
|
+
if (typeof option.group === "string" && option.group !== "") groups.add(option.group);
|
|
561
|
+
previousGroup = option.group;
|
|
482
562
|
}
|
|
483
563
|
if (!seen.has(control.default)) problems.push(`${label} default "${control.default}" is not among its options`);
|
|
484
564
|
}
|