@adatechnology/game-contracts 0.4.0 → 0.6.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 +2 -1
- package/dist/index.d.ts +295 -3
- package/dist/index.global.js +186 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +186 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,7 +118,8 @@ metrics: () => [{ id: 'particles', name: 'Particles', max: 400, read: () => fx.p
|
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
Tool events are named after a tool's cycle, not a firearm: `mode`, `fault`, `clear` (start / end) and `safety`, and `shot` carries `rounds`
|
|
121
|
-
(cartridges spent) next to `shots` (projectiles) and the `mode
|
|
121
|
+
(cartridges spent) next to `shots` (projectiles, always 1 or more) and the `mode`; a dud round is its own `misfire` event, not a `shot` of zero. `HostTool` may report `mode`, `fault`, `safety` and `heat` (0..1).
|
|
122
|
+
`host.configureTool(settings)` takes a `Record<string, string>` keyed by the id of the module's own control; the test room applies `condition` and `ammo_quality` and ignores other ids.
|
|
122
123
|
|
|
123
124
|
## API
|
|
124
125
|
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,16 @@ type PartShape = {
|
|
|
37
37
|
radius: number;
|
|
38
38
|
};
|
|
39
39
|
type FluidKind = 'water' | 'fuel' | 'oil' | 'gas' | 'sap' | 'acid' | 'blood' | string;
|
|
40
|
+
/** Seconds from a trigger to the blast: fixed, or `[min, max]` drawn from the world seed. */
|
|
41
|
+
type ChainDelay = number | readonly [number, number];
|
|
42
|
+
/** How an explosive part is set off by what happens around it. See `PartSpec.explodes`. */
|
|
43
|
+
interface ExplosionChain {
|
|
44
|
+
shot?: ChainDelay;
|
|
45
|
+
fire?: ChainDelay;
|
|
46
|
+
explosion?: ChainDelay;
|
|
47
|
+
/** Fraction of the part's hit points a shot must have taken off before it sets the part off (default 0: any hit). */
|
|
48
|
+
shotDamageFraction?: number;
|
|
49
|
+
}
|
|
40
50
|
interface PartSpec {
|
|
41
51
|
/** Unique inside the object. */
|
|
42
52
|
id: string;
|
|
@@ -67,6 +77,12 @@ interface PartSpec {
|
|
|
67
77
|
energy: number;
|
|
68
78
|
radius: number;
|
|
69
79
|
atC?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Chain reaction: what sets the part off before it is destroyed and the seconds from that to the blast (a number, or `[min, max]` drawn
|
|
82
|
+
* from the world seed). `shot`: a bullet hit; `fire`: the part gets to `atC` (default 300 °C) or burns; `explosion`: a blast reaches it.
|
|
83
|
+
* Each part goes off once. Without `chain`, it explodes only when destroyed (or at `atC`).
|
|
84
|
+
*/
|
|
85
|
+
chain?: ExplosionChain;
|
|
70
86
|
};
|
|
71
87
|
/** Fixed to the world (a wall-mounted cabinet, a tree trunk in the ground). Groups that keep an anchored part stay in place. */
|
|
72
88
|
anchored?: boolean;
|
|
@@ -360,6 +376,14 @@ type DestructionEvent = (EventBase & {
|
|
|
360
376
|
energy: number;
|
|
361
377
|
radius: number;
|
|
362
378
|
})
|
|
379
|
+
/** An explosive part was set off by a bullet, fire or a nearby blast and will explode `delay` seconds from now (`explodes.chain`). */
|
|
380
|
+
| (EventBase & {
|
|
381
|
+
type: 'primed';
|
|
382
|
+
part: string;
|
|
383
|
+
point: Vec3;
|
|
384
|
+
trigger: 'shot' | 'fire' | 'explosion';
|
|
385
|
+
delay: number;
|
|
386
|
+
})
|
|
363
387
|
/** A structural part is overloaded (`stress` > 1); throttled to once per 0.5 s per part. Brittle parts also `crack` at stress > 0.8. */
|
|
364
388
|
| (EventBase & {
|
|
365
389
|
type: 'stress';
|
|
@@ -532,6 +556,53 @@ type Use = {
|
|
|
532
556
|
radius: number;
|
|
533
557
|
seconds: number;
|
|
534
558
|
};
|
|
559
|
+
/** Explosive id (`m67`, `molotov`, `pg7v`...): the host looks the numbers up in the arsenal. Absent on a plain thrown object. */
|
|
560
|
+
explosive?: string;
|
|
561
|
+
/** What sets it off: `timer` after `fuse` seconds, `impact` on the first hit once armed. */
|
|
562
|
+
trigger?: 'timer' | 'impact';
|
|
563
|
+
/** Metres the projectile must fly before an impact sets it off; closer, it hits without exploding (rocket launchers). */
|
|
564
|
+
armDistance?: number;
|
|
565
|
+
/** Radius in metres of the wounding area (`radius` is the lethal one). */
|
|
566
|
+
injuryRadius?: number;
|
|
567
|
+
/** Seconds the blast stuns whoever is inside the injury radius (flashbang). */
|
|
568
|
+
stunSeconds?: number;
|
|
569
|
+
/** Seconds the thrower held it after pulling the pin: `fuse` is what was left of the full fuse when it left the hand. */
|
|
570
|
+
cooked?: number;
|
|
571
|
+
/** Went off in the thrower's hand (held past the fuse): no velocity, `fuse` is 0 and the blast is at `origin`. */
|
|
572
|
+
inHand?: boolean;
|
|
573
|
+
}
|
|
574
|
+
/** An explosive set down where the trigger is pulled (mine, C4, charge): it waits for its trigger instead of flying. */
|
|
575
|
+
| {
|
|
576
|
+
kind: 'place';
|
|
577
|
+
explosive: string;
|
|
578
|
+
/** `wire`: something trips it; `remote`: the player fires it; `timer`: after `fuse` seconds. */
|
|
579
|
+
triggers: readonly ('timer' | 'wire' | 'remote')[];
|
|
580
|
+
energy: number;
|
|
581
|
+
/** Lethal radius in metres. */
|
|
582
|
+
radius: number;
|
|
583
|
+
injuryRadius: number;
|
|
584
|
+
/** Cone the blast goes through, in front of `direction` (mine); absent for a blast in every direction. */
|
|
585
|
+
cone?: {
|
|
586
|
+
halfAngleDeg: number;
|
|
587
|
+
length: number;
|
|
588
|
+
};
|
|
589
|
+
/** The wire a `wire` charge stretches from where it is set down: metres out, degrees from its facing (counter-clockwise from above) and metres above it. */
|
|
590
|
+
tripwire?: {
|
|
591
|
+
length: number;
|
|
592
|
+
angleDeg: number;
|
|
593
|
+
height: number;
|
|
594
|
+
};
|
|
595
|
+
fuse?: number;
|
|
596
|
+
origin: Vec3;
|
|
597
|
+
direction: Vec3;
|
|
598
|
+
}
|
|
599
|
+
/** The blast behind a rocket launcher: dangerous inside `radius` metres, in the cone around `direction` (pointing away from the target). */
|
|
600
|
+
| {
|
|
601
|
+
kind: 'backblast';
|
|
602
|
+
radius: number;
|
|
603
|
+
halfAngleDeg: number;
|
|
604
|
+
origin: Vec3;
|
|
605
|
+
direction: Vec3;
|
|
535
606
|
} | {
|
|
536
607
|
kind: 'tool';
|
|
537
608
|
tool: string;
|
|
@@ -817,9 +888,12 @@ interface ModuleArea {
|
|
|
817
888
|
* What the player did with the tool in hand, as the host reports it. Hosts emit what they support; modules ignore what they do not need. The
|
|
818
889
|
* names are of a tool's cycle, not of a firearm: a nail gun, a chainsaw or a hose has modes, faults and a safety too.
|
|
819
890
|
*/
|
|
820
|
-
type HostEvent =
|
|
891
|
+
type HostEvent =
|
|
892
|
+
/** The tool is put in hand; `duration` is the seconds the rule takes to ready it (absent: no draw time). */
|
|
893
|
+
{
|
|
821
894
|
type: 'tool';
|
|
822
895
|
weapon: string;
|
|
896
|
+
duration?: number;
|
|
823
897
|
}
|
|
824
898
|
/** `shots` are projectiles (a shotgun shell is many), `rounds` the cartridges spent (a burst is several). */
|
|
825
899
|
| {
|
|
@@ -829,10 +903,20 @@ type HostEvent = {
|
|
|
829
903
|
recoil: [number, number];
|
|
830
904
|
rounds?: number;
|
|
831
905
|
mode?: string;
|
|
832
|
-
|
|
906
|
+
suppressed?: boolean;
|
|
907
|
+
}
|
|
908
|
+
/** The trigger was pulled, the round was spent and nothing left the tool (a dud cartridge): no projectile, no recoil. */
|
|
909
|
+
| {
|
|
910
|
+
type: 'misfire';
|
|
911
|
+
weapon: string;
|
|
912
|
+
}
|
|
913
|
+
/** `kind` is how the rule reloads (`tactical`, `empty`, `one_by_one`); `duration` the seconds of the whole reload, on `start`. */
|
|
914
|
+
| {
|
|
833
915
|
type: 'reload';
|
|
834
916
|
weapon: string;
|
|
835
917
|
phase: 'start' | 'end';
|
|
918
|
+
kind?: string;
|
|
919
|
+
duration?: number;
|
|
836
920
|
} | {
|
|
837
921
|
type: 'empty';
|
|
838
922
|
weapon: string;
|
|
@@ -849,10 +933,79 @@ type HostEvent = {
|
|
|
849
933
|
weapon: string;
|
|
850
934
|
fault: string;
|
|
851
935
|
phase: 'start' | 'end';
|
|
936
|
+
duration?: number;
|
|
852
937
|
} | {
|
|
853
938
|
type: 'safety';
|
|
854
939
|
weapon: string;
|
|
855
940
|
on: boolean;
|
|
941
|
+
}
|
|
942
|
+
/** The action works between shots (a pump, a bolt); `duration` is the seconds of the cycle, on `start`. */
|
|
943
|
+
| {
|
|
944
|
+
type: 'cycle';
|
|
945
|
+
weapon: string;
|
|
946
|
+
phase: 'start' | 'end';
|
|
947
|
+
duration?: number;
|
|
948
|
+
}
|
|
949
|
+
/** A step of a longer phase (a reload's magazine out, magazine in, charge), at the moment the rule reaches it. */
|
|
950
|
+
| {
|
|
951
|
+
type: 'stage';
|
|
952
|
+
weapon: string;
|
|
953
|
+
stage: string;
|
|
954
|
+
}
|
|
955
|
+
/** One more round went into the tool while it loads round by round. */
|
|
956
|
+
| {
|
|
957
|
+
type: 'feed';
|
|
958
|
+
weapon: string;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* A throwable with a timer fuse is cooked: the trigger pulls the pin (`start`, `fuse` is the whole fuse), releasing throws it with what is left of
|
|
962
|
+
* the fuse (`throw`), and holding past the fuse sets it off in the hand (`burst`, `fuse` 0).
|
|
963
|
+
*/
|
|
964
|
+
| {
|
|
965
|
+
type: 'cook';
|
|
966
|
+
weapon: string;
|
|
967
|
+
phase: 'start' | 'throw' | 'burst';
|
|
968
|
+
fuse: number;
|
|
969
|
+
}
|
|
970
|
+
/** An accessory went on (`fit`), came off (`remove`) or broke (`break`) at `mount`; `duration` is the seconds the work takes, on `fit` and `remove`. */
|
|
971
|
+
| {
|
|
972
|
+
type: 'attachment';
|
|
973
|
+
weapon: string;
|
|
974
|
+
action: 'fit' | 'remove' | 'break';
|
|
975
|
+
attachment: string;
|
|
976
|
+
mount: string;
|
|
977
|
+
duration?: number;
|
|
978
|
+
}
|
|
979
|
+
/** A light or a laser was turned on or off. */
|
|
980
|
+
| {
|
|
981
|
+
type: 'toggle';
|
|
982
|
+
weapon: string;
|
|
983
|
+
attachment: string;
|
|
984
|
+
on: boolean;
|
|
985
|
+
}
|
|
986
|
+
/** The stock was folded or slid in (`collapsed`) or opened (`extended`); `kind` is how it moves (`fold`, `retract`), `duration` the seconds it takes. */
|
|
987
|
+
| {
|
|
988
|
+
type: 'stock';
|
|
989
|
+
weapon: string;
|
|
990
|
+
state: 'collapsed' | 'extended';
|
|
991
|
+
kind?: string;
|
|
992
|
+
duration?: number;
|
|
993
|
+
}
|
|
994
|
+
/** The weapon in hand changed between the main weapon and the one under its barrel (`on`: the under-barrel one is in use); `duration` is the seconds of the switch. */
|
|
995
|
+
| {
|
|
996
|
+
type: 'underbarrel';
|
|
997
|
+
weapon: string;
|
|
998
|
+
on: boolean;
|
|
999
|
+
duration?: number;
|
|
1000
|
+
}
|
|
1001
|
+
/** 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. */
|
|
1002
|
+
| {
|
|
1003
|
+
type: 'explosion';
|
|
1004
|
+
source: string;
|
|
1005
|
+
point: Vec3;
|
|
1006
|
+
energy: number;
|
|
1007
|
+
radius: number;
|
|
1008
|
+
cause: string;
|
|
856
1009
|
};
|
|
857
1010
|
/** The tool in hand as the host sees it. */
|
|
858
1011
|
interface HostTool {
|
|
@@ -868,6 +1021,52 @@ interface HostTool {
|
|
|
868
1021
|
safety?: boolean;
|
|
869
1022
|
/** 0..1 */
|
|
870
1023
|
heat?: number;
|
|
1024
|
+
/** What is on the tool (factory parts included): the accessory, where it sits, and whether a light or laser is on. */
|
|
1025
|
+
attachments?: readonly HostToolAttachment[];
|
|
1026
|
+
/** Where the stock is, for a tool with one that folds or slides. */
|
|
1027
|
+
stock?: 'collapsed' | 'extended';
|
|
1028
|
+
/** True while a suppressor is on: the shot sound is the muffled one. */
|
|
1029
|
+
suppressed?: boolean;
|
|
1030
|
+
/** True while the trigger and the reload go to the launcher under the barrel. */
|
|
1031
|
+
underbarrelActive?: boolean;
|
|
1032
|
+
/** Kg: the tool with what is on it. */
|
|
1033
|
+
mass?: number;
|
|
1034
|
+
/** Seconds to bring the tool to the sights, with what is on it. */
|
|
1035
|
+
aimSeconds?: number;
|
|
1036
|
+
}
|
|
1037
|
+
interface HostToolAttachment {
|
|
1038
|
+
readonly id: string;
|
|
1039
|
+
readonly mount: string;
|
|
1040
|
+
readonly integral?: boolean;
|
|
1041
|
+
readonly on?: boolean;
|
|
1042
|
+
}
|
|
1043
|
+
/** What a module asks of the tool in hand (accessories, stock, the launcher under the barrel, how it is held). The work takes the seconds the rule gives it and shows as host events. */
|
|
1044
|
+
type ToolCommand = {
|
|
1045
|
+
readonly type: 'fit';
|
|
1046
|
+
readonly attachment: string;
|
|
1047
|
+
readonly mount?: string;
|
|
1048
|
+
} | {
|
|
1049
|
+
readonly type: 'remove';
|
|
1050
|
+
readonly attachment: string;
|
|
1051
|
+
} | {
|
|
1052
|
+
readonly type: 'toggle';
|
|
1053
|
+
readonly attachment: string;
|
|
1054
|
+
} | {
|
|
1055
|
+
readonly type: 'stock';
|
|
1056
|
+
readonly state: 'collapsed' | 'extended';
|
|
1057
|
+
} | {
|
|
1058
|
+
readonly type: 'underbarrel';
|
|
1059
|
+
readonly on: boolean;
|
|
1060
|
+
} | {
|
|
1061
|
+
readonly type: 'stance';
|
|
1062
|
+
readonly aiming?: boolean;
|
|
1063
|
+
readonly supported?: boolean;
|
|
1064
|
+
};
|
|
1065
|
+
/** `ok`, or why nothing happened (the rule's refusal: `no_mount`, `interface`, `occupied`, `busy`...); `seconds` is how long the work takes. */
|
|
1066
|
+
interface ToolCommandResult {
|
|
1067
|
+
readonly ok: boolean;
|
|
1068
|
+
readonly reason?: string;
|
|
1069
|
+
readonly seconds?: number;
|
|
871
1070
|
}
|
|
872
1071
|
/**
|
|
873
1072
|
* What the host lends a module that brings its own presentation (a weapon in the hands, sounds, ejected casings). `THREE`, `scene` and `camera`
|
|
@@ -887,6 +1086,13 @@ interface ModuleHost {
|
|
|
887
1086
|
onFrame(update: (dt: number) => void): () => void;
|
|
888
1087
|
/** Write a line to the host's behaviour log. */
|
|
889
1088
|
log(text: string, data?: Record<string, unknown>): void;
|
|
1089
|
+
/**
|
|
1090
|
+
* 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
|
|
1091
|
+
* 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).
|
|
1092
|
+
*/
|
|
1093
|
+
configureTool?(settings: Readonly<Record<string, string>>): void;
|
|
1094
|
+
/** Asks the tool in hand to do something (see `ToolCommand`). A host without accessories leaves it out. */
|
|
1095
|
+
command?(command: ToolCommand): ToolCommandResult;
|
|
890
1096
|
/** Lent only to a module that declares controls, layers or metrics. */
|
|
891
1097
|
bench?: ModuleBench;
|
|
892
1098
|
}
|
|
@@ -909,4 +1115,90 @@ interface TestableModule<Context = unknown> {
|
|
|
909
1115
|
attach?(host: ModuleHost): () => void;
|
|
910
1116
|
}
|
|
911
1117
|
|
|
912
|
-
|
|
1118
|
+
/** Clips a tool can bring, one per move of its cycle. A tool brings the ones it has; a missing clip is simply not played. */
|
|
1119
|
+
declare const TOOL_CLIP: {
|
|
1120
|
+
readonly DRAW: "draw";
|
|
1121
|
+
readonly SHOT: "shot";
|
|
1122
|
+
readonly MISFIRE: "misfire";
|
|
1123
|
+
readonly EMPTY: "empty";
|
|
1124
|
+
readonly CYCLE: "cycle";
|
|
1125
|
+
readonly RELOAD: "reload";
|
|
1126
|
+
readonly FEED: "feed";
|
|
1127
|
+
readonly FAULT: "fault";
|
|
1128
|
+
readonly CLEAR: "clear";
|
|
1129
|
+
readonly SAFETY_ON: "safety_on";
|
|
1130
|
+
readonly SAFETY_OFF: "safety_off";
|
|
1131
|
+
readonly MODE: "mode";
|
|
1132
|
+
/** A throwable with the pin out, held until it is thrown. */
|
|
1133
|
+
readonly COOK: "cook";
|
|
1134
|
+
/** The stock folds, unfolds, slides in or out (variants `fold`, `unfold`, `collapse`, `extend`). */
|
|
1135
|
+
readonly STOCK: "stock";
|
|
1136
|
+
/** A light or a laser is switched (variants `on`, `off`). */
|
|
1137
|
+
readonly TOGGLE: "toggle";
|
|
1138
|
+
};
|
|
1139
|
+
type ToolClipId = (typeof TOOL_CLIP)[keyof typeof TOOL_CLIP];
|
|
1140
|
+
/** What a renderer does when a rule event arrives. `variant` picks `<clip>_<variant>` when the tool has it, else `<clip>`. */
|
|
1141
|
+
type ToolAnimationStep = {
|
|
1142
|
+
readonly type: 'play';
|
|
1143
|
+
readonly clip: ToolClipId;
|
|
1144
|
+
readonly variant?: string;
|
|
1145
|
+
readonly duration?: number;
|
|
1146
|
+
readonly repeat?: number;
|
|
1147
|
+
}
|
|
1148
|
+
/** Move the clip that is playing to the marker (a clip event `stage` with `data.stage`) the rule just reached. */
|
|
1149
|
+
| {
|
|
1150
|
+
readonly type: 'sync';
|
|
1151
|
+
readonly stage: string;
|
|
1152
|
+
}
|
|
1153
|
+
/** The rule says the phase is over: the clip jumps to its end if it is still running. */
|
|
1154
|
+
| {
|
|
1155
|
+
readonly type: 'finish';
|
|
1156
|
+
readonly clip: ToolClipId;
|
|
1157
|
+
};
|
|
1158
|
+
/** The move a host event asks of the tool in hand, or null when it has no motion. */
|
|
1159
|
+
declare const toolAnimationStep: (event: HostEvent) => ToolAnimationStep | null;
|
|
1160
|
+
/** Where a renderer puts the poses: every part of the tool by id (the ids the clips move), and the whole tool. Poses are offsets from rest. */
|
|
1161
|
+
interface ToolRig {
|
|
1162
|
+
setPartPose(partId: string, pose: AnimationPose): void;
|
|
1163
|
+
setRootPose?(pose: AnimationPose): void;
|
|
1164
|
+
}
|
|
1165
|
+
/** Clips by id, with the variants as `<clip>_<variant>`. */
|
|
1166
|
+
type ToolClipSet = Readonly<Record<string, AnimationClipSpec>>;
|
|
1167
|
+
/** A renderer's tool animator: feed it the host events and the frame time. */
|
|
1168
|
+
interface ToolAnimator {
|
|
1169
|
+
handle(event: HostEvent): void;
|
|
1170
|
+
update(dt: number): void;
|
|
1171
|
+
/** Ids of the clips playing now. */
|
|
1172
|
+
readonly playing: readonly string[];
|
|
1173
|
+
dispose(): void;
|
|
1174
|
+
}
|
|
1175
|
+
declare const STAGE_EVENT = "stage";
|
|
1176
|
+
/** The clip with its times (keys and events) scaled so that it lasts `duration` seconds. */
|
|
1177
|
+
declare const scaleClip: (clip: AnimationClipSpec, duration: number) => AnimationClipSpec;
|
|
1178
|
+
/** Pose of the whole tool (`partId` undefined) or of one part at `time`, linear between keys; a key that does not name it means rest. */
|
|
1179
|
+
declare const sampleClip: (clip: AnimationClipSpec, partId: string | undefined, time: number) => {
|
|
1180
|
+
position: [number, number, number];
|
|
1181
|
+
rotation: [number, number, number];
|
|
1182
|
+
};
|
|
1183
|
+
/** Problems with a clip set (empty when valid): each key is the clip id (or `<clip>_<variant>`), every clip passes `checkAnimation`, and a `stage` event names its stage. */
|
|
1184
|
+
declare const checkToolClips: (clips: ToolClipSet, partIds: Iterable<string>) => {
|
|
1185
|
+
ok: boolean;
|
|
1186
|
+
problems: string[];
|
|
1187
|
+
};
|
|
1188
|
+
interface ToolClipPlayerOptions {
|
|
1189
|
+
clips: ToolClipSet;
|
|
1190
|
+
rig: ToolRig;
|
|
1191
|
+
/** Called for each clip event as its time passes (`eject_casing`, `drop_magazine`, `stage`...). */
|
|
1192
|
+
onEvent?(event: AnimationEventSpec, clip: string): void;
|
|
1193
|
+
/** Seconds between the rounds of a `repeat` (a burst): default 0.1. */
|
|
1194
|
+
repeatInterval?(): number;
|
|
1195
|
+
}
|
|
1196
|
+
/** The default player of a tool's clips: runs the steps of `toolAnimationStep` against a rig, scaled to the duration the rule gives. */
|
|
1197
|
+
interface ToolClipPlayer {
|
|
1198
|
+
apply(step: ToolAnimationStep): void;
|
|
1199
|
+
update(dt: number): void;
|
|
1200
|
+
readonly playing: readonly string[];
|
|
1201
|
+
}
|
|
1202
|
+
declare const createToolClipPlayer: (options: ToolClipPlayerOptions) => ToolClipPlayer;
|
|
1203
|
+
|
|
1204
|
+
export { type ActionControl, type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type AreaPlacement, type BenchChange, type BenchDeclaration, type BenchReport, type BooleanControl, type ChainDelay, type CheckOptions, type CheckSpecOptions, type ChoiceControl, type ChoiceOption, type ContainerSpec, type ControlPatch, type ControlValue, type Crack, type DamageType, type DebrisKind, type Dent, type DestructibleImpl, type DestructibleSpec, type DestructionEvent, type DestructionEventType, type ExplosionChain, type FluidKind, type FractureStyle, type Fragment, 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, type MaterialId, type ModuleArea, type ModuleBench, type ModuleControl, type ModuleHost, type ModuleItemFluid, type ModuleLayer, type ModuleMetric, type ModuleScenario, type NumberControl, type ObjectState, type PartShape, type PartSpec, type PartState, type PartStatus, type Quat, type RayHit, STAGE_EVENT, type Shot, type SpecReport, type StructureRole, TOOL_CLIP, type TestableItem, type TestableModule, type ToolAnimationStep, type ToolAnimator, type ToolClipId, type ToolClipPlayer, type ToolClipPlayerOptions, type ToolClipSet, type ToolCommand, type ToolCommandResult, type ToolKind, type ToolRig, type Use, type Vec3, checkAnimation, checkBench, checkDestructible, checkHit, checkSpec, checkToolClips, createToolClipPlayer, sampleClip, scaleClip, shotToHit, toolAnimationStep };
|
package/dist/index.global.js
CHANGED
|
@@ -22,12 +22,19 @@ var GameContracts = (() => {
|
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
24
|
IMPACT_CONTRACT: () => IMPACT_CONTRACT,
|
|
25
|
+
STAGE_EVENT: () => STAGE_EVENT,
|
|
26
|
+
TOOL_CLIP: () => TOOL_CLIP,
|
|
25
27
|
checkAnimation: () => checkAnimation,
|
|
26
28
|
checkBench: () => checkBench,
|
|
27
29
|
checkDestructible: () => checkDestructible,
|
|
28
30
|
checkHit: () => checkHit,
|
|
29
31
|
checkSpec: () => checkSpec,
|
|
30
|
-
|
|
32
|
+
checkToolClips: () => checkToolClips,
|
|
33
|
+
createToolClipPlayer: () => createToolClipPlayer,
|
|
34
|
+
sampleClip: () => sampleClip,
|
|
35
|
+
scaleClip: () => scaleClip,
|
|
36
|
+
shotToHit: () => shotToHit,
|
|
37
|
+
toolAnimationStep: () => toolAnimationStep
|
|
31
38
|
});
|
|
32
39
|
|
|
33
40
|
// src/impact.ts
|
|
@@ -274,6 +281,184 @@ var GameContracts = (() => {
|
|
|
274
281
|
return { ok: problems.length === 0, problems };
|
|
275
282
|
};
|
|
276
283
|
|
|
284
|
+
// src/tool-animation.ts
|
|
285
|
+
var TOOL_CLIP = {
|
|
286
|
+
DRAW: "draw",
|
|
287
|
+
SHOT: "shot",
|
|
288
|
+
MISFIRE: "misfire",
|
|
289
|
+
EMPTY: "empty",
|
|
290
|
+
CYCLE: "cycle",
|
|
291
|
+
RELOAD: "reload",
|
|
292
|
+
FEED: "feed",
|
|
293
|
+
FAULT: "fault",
|
|
294
|
+
CLEAR: "clear",
|
|
295
|
+
SAFETY_ON: "safety_on",
|
|
296
|
+
SAFETY_OFF: "safety_off",
|
|
297
|
+
MODE: "mode",
|
|
298
|
+
/** A throwable with the pin out, held until it is thrown. */
|
|
299
|
+
COOK: "cook",
|
|
300
|
+
/** The stock folds, unfolds, slides in or out (variants `fold`, `unfold`, `collapse`, `extend`). */
|
|
301
|
+
STOCK: "stock",
|
|
302
|
+
/** A light or a laser is switched (variants `on`, `off`). */
|
|
303
|
+
TOGGLE: "toggle"
|
|
304
|
+
};
|
|
305
|
+
var stockVariant = (event) => {
|
|
306
|
+
const isFolding = event.kind === void 0 ? false : event.kind.includes("fold");
|
|
307
|
+
if (event.state === "collapsed") return isFolding ? "fold" : "collapse";
|
|
308
|
+
return isFolding ? "unfold" : "extend";
|
|
309
|
+
};
|
|
310
|
+
var toolAnimationStep = (event) => {
|
|
311
|
+
switch (event.type) {
|
|
312
|
+
case "tool":
|
|
313
|
+
return { type: "play", clip: TOOL_CLIP.DRAW, ...event.duration === void 0 ? {} : { duration: event.duration } };
|
|
314
|
+
case "shot":
|
|
315
|
+
return { type: "play", clip: TOOL_CLIP.SHOT, repeat: Math.max(1, event.rounds ?? 1) };
|
|
316
|
+
case "misfire":
|
|
317
|
+
return { type: "play", clip: TOOL_CLIP.MISFIRE };
|
|
318
|
+
case "empty":
|
|
319
|
+
return { type: "play", clip: TOOL_CLIP.EMPTY };
|
|
320
|
+
case "mode":
|
|
321
|
+
return { type: "play", clip: TOOL_CLIP.MODE, variant: event.mode };
|
|
322
|
+
case "fault":
|
|
323
|
+
return { type: "play", clip: TOOL_CLIP.FAULT, variant: event.fault };
|
|
324
|
+
case "clear":
|
|
325
|
+
if (event.phase === "end") return { type: "finish", clip: TOOL_CLIP.CLEAR };
|
|
326
|
+
return { type: "play", clip: TOOL_CLIP.CLEAR, variant: event.fault, ...event.duration === void 0 ? {} : { duration: event.duration } };
|
|
327
|
+
case "safety":
|
|
328
|
+
return { type: "play", clip: event.on ? TOOL_CLIP.SAFETY_ON : TOOL_CLIP.SAFETY_OFF };
|
|
329
|
+
case "cycle":
|
|
330
|
+
if (event.phase === "end") return { type: "finish", clip: TOOL_CLIP.CYCLE };
|
|
331
|
+
return { type: "play", clip: TOOL_CLIP.CYCLE, ...event.duration === void 0 ? {} : { duration: event.duration } };
|
|
332
|
+
case "reload":
|
|
333
|
+
if (event.phase === "end") return { type: "finish", clip: TOOL_CLIP.RELOAD };
|
|
334
|
+
return { type: "play", clip: TOOL_CLIP.RELOAD, ...event.kind === void 0 ? {} : { variant: event.kind }, ...event.duration === void 0 ? {} : { duration: event.duration } };
|
|
335
|
+
case "stage":
|
|
336
|
+
return { type: "sync", stage: event.stage };
|
|
337
|
+
case "feed":
|
|
338
|
+
return { type: "play", clip: TOOL_CLIP.FEED };
|
|
339
|
+
case "cook":
|
|
340
|
+
if (event.phase === "start") return { type: "play", clip: TOOL_CLIP.COOK, duration: event.fuse };
|
|
341
|
+
return { type: "finish", clip: TOOL_CLIP.COOK };
|
|
342
|
+
case "stock":
|
|
343
|
+
return { type: "play", clip: TOOL_CLIP.STOCK, variant: stockVariant(event), ...event.duration === void 0 ? {} : { duration: event.duration } };
|
|
344
|
+
case "toggle":
|
|
345
|
+
return { type: "play", clip: TOOL_CLIP.TOGGLE, variant: event.on ? "on" : "off" };
|
|
346
|
+
default:
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
var STAGE_EVENT = "stage";
|
|
351
|
+
var scaleClip = (clip, duration) => {
|
|
352
|
+
const factor = clip.duration > 0 ? duration / clip.duration : 1;
|
|
353
|
+
return {
|
|
354
|
+
...clip,
|
|
355
|
+
duration,
|
|
356
|
+
keys: clip.keys.map((key) => ({ ...key, t: key.t * factor })),
|
|
357
|
+
...clip.events ? { events: clip.events.map((event) => ({ ...event, t: event.t * factor })) } : {}
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
var ZERO = [0, 0, 0];
|
|
361
|
+
var mix = (from, to, fraction) => [
|
|
362
|
+
(from[0] ?? 0) + ((to[0] ?? 0) - (from[0] ?? 0)) * fraction,
|
|
363
|
+
(from[1] ?? 0) + ((to[1] ?? 0) - (from[1] ?? 0)) * fraction,
|
|
364
|
+
(from[2] ?? 0) + ((to[2] ?? 0) - (from[2] ?? 0)) * fraction
|
|
365
|
+
];
|
|
366
|
+
var sampleClip = (clip, partId, time) => {
|
|
367
|
+
const points = clip.keys.map((key) => {
|
|
368
|
+
const pose = partId === void 0 ? key : key.parts?.[partId];
|
|
369
|
+
return { t: key.t, position: pose?.position ?? ZERO, rotation: pose?.rotation ?? ZERO };
|
|
370
|
+
});
|
|
371
|
+
const first = points[0];
|
|
372
|
+
if (!first || first.t > 0) points.unshift({ t: 0, position: ZERO, rotation: ZERO });
|
|
373
|
+
const last = points[points.length - 1];
|
|
374
|
+
if (!last) return { position: ZERO, rotation: ZERO };
|
|
375
|
+
if (time >= last.t) return { position: mix(last.position, last.position, 0), rotation: mix(last.rotation, last.rotation, 0) };
|
|
376
|
+
const index = Math.max(0, points.findIndex((point, i) => time >= point.t && time < (points[i + 1]?.t ?? Infinity)));
|
|
377
|
+
const from = points[index];
|
|
378
|
+
const to = points[index + 1];
|
|
379
|
+
if (!from || !to) return { position: ZERO, rotation: ZERO };
|
|
380
|
+
const fraction = to.t > from.t ? (time - from.t) / (to.t - from.t) : 0;
|
|
381
|
+
return { position: mix(from.position, to.position, fraction), rotation: mix(from.rotation, to.rotation, fraction) };
|
|
382
|
+
};
|
|
383
|
+
var checkToolClips = (clips, partIds) => {
|
|
384
|
+
const parts = [...partIds];
|
|
385
|
+
const problems = [];
|
|
386
|
+
for (const [key, clip] of Object.entries(clips)) {
|
|
387
|
+
if (clip.id !== key) problems.push(`clip "${key}" has the id "${clip.id}"`);
|
|
388
|
+
if (!(clip.duration > 0)) problems.push(`clip "${key}" has no duration`);
|
|
389
|
+
problems.push(...checkAnimation(clip, parts).problems);
|
|
390
|
+
for (const event of clip.events ?? []) if (event.type === STAGE_EVENT && typeof event.data?.stage !== "string") problems.push(`clip "${key}" has a stage event without data.stage`);
|
|
391
|
+
}
|
|
392
|
+
return { ok: problems.length === 0, problems };
|
|
393
|
+
};
|
|
394
|
+
var createToolClipPlayer = (options) => {
|
|
395
|
+
const { clips, rig, onEvent } = options;
|
|
396
|
+
let instances = [];
|
|
397
|
+
const find = (clip, variant) => {
|
|
398
|
+
const name = variant !== void 0 && clips[`${clip}_${variant}`] ? `${clip}_${variant}` : clip;
|
|
399
|
+
const found = clips[name];
|
|
400
|
+
return found ? { name, clip: found } : void 0;
|
|
401
|
+
};
|
|
402
|
+
const render = (instance) => {
|
|
403
|
+
const time = Math.min(instance.time, instance.clip.duration);
|
|
404
|
+
const root = sampleClip(instance.clip, void 0, time);
|
|
405
|
+
if (instance.clip.keys.some((key) => key.position || key.rotation)) rig.setRootPose?.(root);
|
|
406
|
+
const touched = new Set(instance.clip.keys.flatMap((key) => Object.keys(key.parts ?? {})));
|
|
407
|
+
for (const id of touched) rig.setPartPose(id, sampleClip(instance.clip, id, time));
|
|
408
|
+
};
|
|
409
|
+
const fireEvents = (instance, upTo) => {
|
|
410
|
+
const events = instance.clip.events ?? [];
|
|
411
|
+
while (instance.fired < events.length && (events[instance.fired]?.t ?? Infinity) <= upTo + 1e-9) {
|
|
412
|
+
const event = events[instance.fired];
|
|
413
|
+
instance.fired += 1;
|
|
414
|
+
if (event) onEvent?.(event, instance.name);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
return {
|
|
418
|
+
apply(step) {
|
|
419
|
+
if (step.type === "sync") {
|
|
420
|
+
for (const instance of instances) {
|
|
421
|
+
const marker = (instance.clip.events ?? []).find((event) => event.type === STAGE_EVENT && event.data?.stage === step.stage);
|
|
422
|
+
if (marker && instance.time < marker.t) instance.time = marker.t;
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (step.type === "finish") {
|
|
427
|
+
for (const instance of instances) {
|
|
428
|
+
if (instance.name !== step.clip && !instance.name.startsWith(`${step.clip}_`)) continue;
|
|
429
|
+
fireEvents(instance, instance.clip.duration);
|
|
430
|
+
instance.time = instance.clip.duration;
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const found = find(step.clip, step.variant);
|
|
435
|
+
if (!found) return;
|
|
436
|
+
const clip = step.duration && step.duration > 0 ? scaleClip(found.clip, step.duration) : found.clip;
|
|
437
|
+
const interval = options.repeatInterval?.() ?? 0.1;
|
|
438
|
+
instances = instances.filter((existing) => existing.name !== found.name || existing.delay > 0);
|
|
439
|
+
for (let round = 0; round < (step.repeat ?? 1); round++) {
|
|
440
|
+
instances.push({ name: found.name, clip, time: 0, delay: round * interval, fired: 0 });
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
update(dt) {
|
|
444
|
+
for (const instance of instances) {
|
|
445
|
+
const carried = instance.delay > 0 ? dt - instance.delay : dt;
|
|
446
|
+
if (instance.delay > 0) {
|
|
447
|
+
instance.delay = Math.max(0, instance.delay - dt);
|
|
448
|
+
if (instance.delay > 0) continue;
|
|
449
|
+
}
|
|
450
|
+
instance.time += carried;
|
|
451
|
+
fireEvents(instance, instance.time);
|
|
452
|
+
render(instance);
|
|
453
|
+
}
|
|
454
|
+
instances = instances.filter((instance) => instance.delay > 0 || instance.time < instance.clip.duration);
|
|
455
|
+
},
|
|
456
|
+
get playing() {
|
|
457
|
+
return instances.map((instance) => instance.name);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
|
|
277
462
|
// src/bench.ts
|
|
278
463
|
var SNAKE_CASE = /^[a-z][a-z0-9_]*$/;
|
|
279
464
|
var finite2 = (n) => typeof n === "number" && Number.isFinite(n);
|