@adatechnology/game-contracts 0.22.0 → 0.24.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/dist/index.d.ts CHANGED
@@ -92,6 +92,8 @@ interface PartSpec {
92
92
  radius: number;
93
93
  /** Inside this radius (metres) the blast is lethal to a body; from here to `radius` it only hurts, less with distance. Without it a body takes the energy falling off over `radius`. */
94
94
  lethalRadius?: number;
95
+ /** Kilograms of TNT equivalent of the burst: the blast wave that pushes bodies is read from it (`knockOf`). */
96
+ tnt?: number;
95
97
  atC?: number;
96
98
  /**
97
99
  * 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
@@ -656,6 +658,8 @@ interface EventBase {
656
658
  /** Seconds since the world was created. */
657
659
  time: number;
658
660
  source?: string;
661
+ /** 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. */
662
+ blow?: DamageType;
659
663
  }
660
664
  /** Points in events are in world space. */
661
665
  type DestructionEvent = (EventBase & {
@@ -679,7 +683,8 @@ type DestructionEvent = (EventBase & {
679
683
  exitPoint?: Vec3;
680
684
  powder?: {
681
685
  color: number;
682
- };
686
+ }; /** Joules the round still carries out of the part, when the part knows. */
687
+ energyLeft?: number;
683
688
  }) | (EventBase & {
684
689
  type: 'crack';
685
690
  part: string;
@@ -754,6 +759,7 @@ type DestructionEvent = (EventBase & {
754
759
  energy: number;
755
760
  radius: number;
756
761
  lethalRadius?: number;
762
+ tnt?: number;
757
763
  effect?: string;
758
764
  })
759
765
  /** An explosive part was set off by a bullet, fire or a nearby blast and will explode `delay` seconds from now (`explodes.chain`). */
@@ -966,6 +972,13 @@ interface BlastSource {
966
972
  readonly energy: number;
967
973
  readonly radius: number;
968
974
  readonly reach?: number;
975
+ /**
976
+ * Kilograms of TNT equivalent of the charge. With it a body is pushed by the blast wave itself (`blast-impulse`), and the shove of loose items keeps this module's rule; without it the body
977
+ * takes the charge from `energy` by `BlastImpulseOptions.tntPerJoule`.
978
+ */
979
+ readonly tnt?: number;
980
+ /** Inside this radius (metres) the blast is lethal: a body there that the blast reaches (not behind cover) goes down, wounded, wherever the wave pushed it. */
981
+ readonly lethalRadius?: number;
969
982
  }
970
983
  /** What a blast does to one body: how fast it leaves (m/s), the unit direction it leaves in, and how far from the centre it was. */
971
984
  interface BlastShove {
@@ -985,6 +998,84 @@ declare function blastShove(blast: BlastSource, target: {
985
998
  readonly mass: number;
986
999
  }, options?: BlastShoveOptions): BlastShove | undefined;
987
1000
 
1001
+ /**
1002
+ * The push a blast gives a standing body, from the physics of the blast wave and not from a game coupling: the impulse of the wave per square metre at a distance from a charge of
1003
+ * `tnt` kg of TNT equivalent (Kinney and Graham's fit of the side-on impulse, scaled by the cube root of the charge), reflected on the body, over the area the body shows the wave,
1004
+ * divided by its mass. A hand grenade at 3 m gives a person a few tenths of a metre per second; what floors that person is the fragments, not the wave. A big charge close throws.
1005
+ */
1006
+ /** The numbers of the body a blast wave pushes. All have a default for a standing adult. */
1007
+ interface BlastImpulseOptions {
1008
+ /** Square metres of the body the wave meets: about 1.75 m tall by 0.4 m wide (default 0.7). */
1009
+ readonly bodyArea?: number;
1010
+ /** How much of the push goes up: the direction is `(away, lift)` normalised (default 0.15: the wave runs along the ground). */
1011
+ readonly lift?: number;
1012
+ /** Below this speed (m/s) the body does not move (default 0.3). */
1013
+ readonly minSpeed?: number;
1014
+ /**
1015
+ * Kilograms of TNT per joule of the blast `energy`, for a blast that does not say its charge (`BlastSource.tnt`). Default 0.24 / 200000: the arsenal's energy scale, where the M67 (180 g
1016
+ * of Composition B, 0.24 kg of TNT) is 200 kJ. It is a game equivalence for blasts without a charge, not a physical constant.
1017
+ */
1018
+ readonly tntPerJoule?: number;
1019
+ }
1020
+ declare const DEFAULT_BLAST_IMPULSE: Readonly<Required<BlastImpulseOptions>>;
1021
+ /**
1022
+ * Side-on (incident) specific impulse of the wave, in Pa·s, at `distance` metres from `tnt` kg of TNT in free air: Kinney and Graham, "Explosive Shocks in Air" (1985),
1023
+ * `I/W^(1/3) = 0.067 sqrt(1 + (Z/0.23)^4) / (Z^2 (1 + (Z/1.55)^3)^(1/3))` bar·ms per kg^(1/3), with the scaled distance `Z = R / W^(1/3)`.
1024
+ */
1025
+ declare function sideOnImpulse(tnt: number, distance: number): number;
1026
+ /**
1027
+ * The impulse per square metre a body facing the wave takes, Pa·s: the side-on impulse reflected. The reflection doubles a weak wave and grows up to about six times close to the charge,
1028
+ * where the wave is strong (the ratio of reflected to side-on impulse of the Kingery-Bulmash charts, fitted as `2 + 4 / (1 + Z^2)`).
1029
+ */
1030
+ declare function bodyImpulse(tnt: number, distance: number): number;
1031
+ /** The speed (m/s) a blast wave gives a free body of `mass` kg at `distance` metres from `tnt` kg of TNT, `reach` (0..1) of it getting through what stands in the way. */
1032
+ declare function blastBodySpeed(tnt: number, distance: number, mass: number, reach?: number, options?: BlastImpulseOptions): number;
1033
+
1034
+ /** From this speed (m/s) a blast takes a standing body off its feet. Below it the body is only pushed. */
1035
+ declare const KNOCK_DOWN_SPEED = 2.5;
1036
+ /** The angle of a body lying where it fell (the same tilt a dead body takes). */
1037
+ declare const KNOCKED_DOWN_ANGLE = 1.35;
1038
+ /** The share of the blast that has to get through for its fragments to reach a body: behind cover that stops half of it the fragments stop too. */
1039
+ declare const FRAGMENT_COVER = 0.5;
1040
+ /** Height of the chest above the feet: where the blast takes the body. */
1041
+ declare const KNOCK_CHEST = 1.1;
1042
+ /** 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). */
1043
+ interface Knock {
1044
+ velocity: [number, number, number];
1045
+ airborne: boolean;
1046
+ /** 0..1 */
1047
+ lying: number;
1048
+ /** Seconds left to lie on the ground after it stopped. */
1049
+ lieLeft: number;
1050
+ /** Knocked off its feet by this blast. */
1051
+ down: boolean;
1052
+ /** Floored by its wounds: inside the lethal radius and not behind cover. It lies longer and falls where it stood. */
1053
+ wounded?: boolean;
1054
+ /** Seconds of stun the blast gave the body: dizziness and ringing ears. */
1055
+ stunSeconds?: number;
1056
+ /** The horizontal unit direction away from the blast, even when the push is too weak to move the body: the way it falls. */
1057
+ away?: Vec3;
1058
+ }
1059
+ /** The mass of a body in kg from its height (m) and its build (1 = average): 70 kg at 1.75 m. */
1060
+ declare const bodyMass: (height: number, build?: number) => number;
1061
+ /**
1062
+ * What a blast does to a body standing at `feet`, of `mass` kg. Undefined when the blast does not reach it or does nothing to it.
1063
+ *
1064
+ * The body is pushed by the blast wave (`blastBodySpeed`, from the charge `blast.tnt`, or from `energy` by `tntPerJoule`), taken at the chest and cut by `reach`, the share that got through
1065
+ * cover. It is floored when that push is `KNOCK_DOWN_SPEED` or more, or when it stands inside `lethalRadius` with the blast reaching it (the fragments and the overpressure fell it where it
1066
+ * stands). It is stunned for a while inside the radius. `options` are the shove numbers of the host's loose items; only their `minSpeed` applies to a body.
1067
+ */
1068
+ declare function knockOf(blast: BlastSource, feet: Vec3, mass: number, options?: BlastShoveOptions, impulse?: BlastImpulseOptions): Knock | undefined;
1069
+ /**
1070
+ * 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
1071
+ * body is on its feet and still, and the knock can be dropped.
1072
+ */
1073
+ declare function stepKnock(knock: Knock, position: {
1074
+ x: number;
1075
+ y: number;
1076
+ z: number;
1077
+ }, dt: number, groundAt: (x: number, z: number) => number, alive: boolean): boolean;
1078
+
988
1079
  interface CostOptions {
989
1080
  /** Reads of `state()` and of `describe()` to time. Default 10 000. */
990
1081
  reads?: number;
@@ -1725,6 +1816,11 @@ interface AnimationEventSpec {
1725
1816
  /** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */
1726
1817
  type: string;
1727
1818
  data?: Record<string, unknown>;
1819
+ /**
1820
+ * 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
1821
+ * (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`.
1822
+ */
1823
+ lead?: number;
1728
1824
  }
1729
1825
  /** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */
1730
1826
  interface AnimationClipSpec {
@@ -1762,6 +1858,10 @@ interface SoundRequest {
1762
1858
  readonly seed?: number;
1763
1859
  /** Seconds from now (the hit of a blow after its swing); the host schedules it on the audio clock, not on a timer. */
1764
1860
  readonly delay?: number;
1861
+ /** 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
1862
+ * 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
1863
+ * rate is late or silent. Default false. */
1864
+ readonly urgent?: boolean;
1765
1865
  }
1766
1866
  /** What `AudioSink.play` hands back; opaque to the module, given back to `stop`. */
1767
1867
  interface SoundHandle {
@@ -2017,6 +2117,20 @@ interface Thumbnail {
2017
2117
  wear?: 0 | 1 | 2;
2018
2118
  };
2019
2119
  }
2120
+ /**
2121
+ * 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).
2122
+ * Everything is optional: without it the host picks by the material of the part.
2123
+ */
2124
+ interface ItemSounds {
2125
+ /** How it sounds picked up: `metal`, `plastic`, `glass`, `paper`, `fabric`, `wood`, `food`. Default: by its heaviest material. */
2126
+ handling?: string;
2127
+ /** 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`. */
2128
+ leaves?: Readonly<Record<string, string>>;
2129
+ /** The sound id of opening its seal (`can_open`, `bottle_open`). Default: a metal seal is a can, any other a screw cap. */
2130
+ open?: string;
2131
+ /** The sound id of each use while it is held (`drink`: `drink_gulp`, `eat`: `eat_chew`), played over and over while the use lasts. */
2132
+ uses?: Readonly<Record<string, string>>;
2133
+ }
2020
2134
  /** One thing a module can put in the test room (or any viewer): a toy car, a chair, a pistol on a table. */
2021
2135
  interface TestableItem {
2022
2136
  /** snake_case id, unique inside the module. */
@@ -2045,6 +2159,8 @@ interface TestableItem {
2045
2159
  pushable?: boolean;
2046
2160
  /** Where a body sits or lies on it: the host can seat the player there. */
2047
2161
  seats?: readonly ItemSeat[];
2162
+ /** What it sounds like in the hand (picked up, opened, used) and the sound set of its doors and lids. */
2163
+ sounds?: ItemSounds;
2048
2164
  /** 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. */
2049
2165
  slow?: boolean | string;
2050
2166
  /**
@@ -2228,6 +2344,10 @@ type HostEvent =
2228
2344
  radius: number;
2229
2345
  cause: string;
2230
2346
  explosive?: string;
2347
+ /** Kilograms of TNT equivalent of the charge, when the explosive says it: the blast wave that pushes bodies is read from it (`knockOf`). */
2348
+ tnt?: number;
2349
+ /** Inside this radius (metres) the blast is lethal to a body it reaches. */
2350
+ lethalRadius?: number;
2231
2351
  }
2232
2352
  /** The player crouched or stood up, or the camera mode changed; read the new state with `host.player()`. */
2233
2353
  | {
@@ -2342,14 +2462,37 @@ interface FrameOptions {
2342
2462
  readonly order?: number;
2343
2463
  }
2344
2464
  /**
2345
- * What the host lends a module that brings its own presentation (a weapon in the hands, sounds, ejected casings). `THREE`, `scene` and `camera`
2346
- * are the host's three.js namespace, scene and camera: `unknown` here because the contracts do not depend on three.js.
2465
+ * 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
2466
+ * monitor. The host renders the scene from `camera` into `rect` after the main view, every frame, until the returned function is called.
2347
2467
  */
2468
+ interface ModuleView {
2469
+ /** A three.js camera the module moves itself every frame. Its aspect is set by the host to the rect's. */
2470
+ camera: unknown;
2471
+ /** Where on the screen, as fractions of the canvas from its top-left corner (0..1). */
2472
+ rect: {
2473
+ readonly x: number;
2474
+ readonly y: number;
2475
+ readonly width: number;
2476
+ readonly height: number;
2477
+ };
2478
+ /** Draws it left-right flipped, as a mirror shows the world. */
2479
+ mirror?: boolean;
2480
+ /** Rounds the corners of the frame (a fraction of the shorter side, 0..0.5); 0 or absent is a plain rectangle. */
2481
+ rounded?: number;
2482
+ /** Short label the host may print on the frame. */
2483
+ label?: string;
2484
+ /** Renders it at this fraction of the canvas resolution (default 0.5): a mirror does not need full detail. */
2485
+ resolution?: number;
2486
+ }
2348
2487
  interface HostGraphics {
2349
2488
  hordeDetail(): 'auto' | number;
2350
2489
  onChange(listener: () => void): () => void;
2351
2490
  report(key: string, value: number): void;
2352
2491
  }
2492
+ /**
2493
+ * What the host lends a module that brings its own presentation (a weapon in the hands, sounds, ejected casings). `THREE`, `scene` and `camera`
2494
+ * are the host's three.js namespace, scene and camera: `unknown` here because the contracts do not depend on three.js.
2495
+ */
2353
2496
  interface ModuleHost {
2354
2497
  THREE: unknown;
2355
2498
  scene: unknown;
@@ -2428,6 +2571,20 @@ interface ModuleHost {
2428
2571
  reachFor?(reach: HostReach): void;
2429
2572
  /** Asks the tool in hand to do something (see `ToolCommand`). A host without accessories leaves it out. */
2430
2573
  command?(command: ToolCommand): ToolCommandResult;
2574
+ /**
2575
+ * 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.
2576
+ */
2577
+ addView?(view: ModuleView): () => void;
2578
+ /**
2579
+ * 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
2580
+ * that keeps its camera to itself leaves it out.
2581
+ */
2582
+ setViewCamera?(camera: unknown | null): void;
2583
+ /**
2584
+ * Holds the walking player where it stands (true) or lets it walk again (false): the movement keys then belong to the module (a vehicle the player drives). Mouse look,
2585
+ * the panels and the room's other keys keep working. A host without a walking player leaves it out.
2586
+ */
2587
+ holdPlayer?(held: boolean): void;
2431
2588
  /** Lent only to a module that declares controls, layers or metrics. */
2432
2589
  bench?: ModuleBench;
2433
2590
  /** The host's shared sound engine; a module that makes sound uses it when lent and stays silent (or brings its own) when not. */
@@ -2447,6 +2604,12 @@ interface ModuleHost {
2447
2604
  * so that it throws things the way the host throws its loose items. Absent when the host keeps the defaults.
2448
2605
  */
2449
2606
  blastShove?: BlastShoveOptions;
2607
+ /**
2608
+ * 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
2609
+ * `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).
2610
+ * Absent when the host has no world: the whole blast reaches.
2611
+ */
2612
+ blastReach?(point: Vec3, target: Vec3, ignore?: readonly string[]): number;
2450
2613
  /** 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. */
2451
2614
  loading?: BusySink;
2452
2615
  }
@@ -2561,6 +2724,12 @@ interface ToolClipPlayer {
2561
2724
  readonly clip: AnimationClipSpec;
2562
2725
  }>;
2563
2726
  }
2727
+ /**
2728
+ * 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
2729
+ * 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
2730
+ * clip's stretch does not touch it.
2731
+ */
2732
+ declare const leadClipEvents: (clip: AnimationClipSpec) => AnimationClipSpec;
2564
2733
  declare const createToolClipPlayer: (options: ToolClipPlayerOptions) => ToolClipPlayer;
2565
2734
 
2566
2735
  type Entry = {
@@ -2701,4 +2870,4 @@ declare function footprintBlocker(object: FootprintObject | (() => FootprintObje
2701
2870
  /** @deprecated Import it from `@adatechnology/game-contracts/testing`. */
2702
2871
  declare const resetWarnOnce: () => void;
2703
2872
 
2704
- 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 ItemUseDef, type JointKind, type JointSpec, type JointState, KIT_ORIGIN_KEY, type KitOrigin, 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 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, checkAnimation, checkBench, checkCategorized, checkDeferredQueue, checkDestructible, checkHit, checkSpec, checkToolClips, createEnvironmentBus, createToolClipPlayer, deepMerge, defineCategories, fillFactor, footprintBlocker, footstepSoundId, hitSoundId, ignitionChance, isSurfaceId, kitOriginOf, landSoundId, markKitOrigin, mergeById, removeIds, resetWarnOnce, resolveEntry, resolveIgnitionRules, sampleClip, scaleClip, sealBlock, sealOpener, shotExtras, shotToHit, sparkTargetFactor, surfaceOfMaterial, swingSoundId, toolAnimationStep, twistStroke, unmarkedDrawings, useToHit };
2873
+ 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 BlastImpulseOptions, 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_IMPULSE, 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, FRAGMENT_COVER, 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, blastBodySpeed, blastShove, bodyImpulse, 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, sideOnImpulse, sparkTargetFactor, stepKnock, surfaceOfMaterial, swingSoundId, toolAnimationStep, twistStroke, unmarkedDrawings, useToHit };
@@ -24,6 +24,7 @@ var GameContracts = (() => {
24
24
  BLOW_IDS: () => BLOW_IDS,
25
25
  BODY_FOLEY_IDS: () => BODY_FOLEY_IDS,
26
26
  BODY_SOUND_IDS: () => BODY_SOUND_IDS,
27
+ DEFAULT_BLAST_IMPULSE: () => DEFAULT_BLAST_IMPULSE,
27
28
  DEFAULT_BLAST_SHOVE: () => DEFAULT_BLAST_SHOVE,
28
29
  DEFAULT_ENVIRONMENT: () => DEFAULT_ENVIRONMENT,
29
30
  DEFAULT_IGNITION_RULES: () => DEFAULT_IGNITION_RULES,
@@ -31,11 +32,15 @@ var GameContracts = (() => {
31
32
  DEFERRED_PRIORITY: () => DEFERRED_PRIORITY,
32
33
  DEFERRED_QUEUE_ID: () => DEFERRED_QUEUE_ID,
33
34
  FOOTSTEP_SOUND_IDS: () => FOOTSTEP_SOUND_IDS,
35
+ FRAGMENT_COVER: () => FRAGMENT_COVER,
34
36
  FRAME_PHASES: () => FRAME_PHASES,
35
37
  GAIT_IDS: () => GAIT_IDS,
36
38
  HEAVY_LANDING_SPEED: () => HEAVY_LANDING_SPEED,
37
39
  IMPACT_CONTRACT: () => IMPACT_CONTRACT,
38
40
  KIT_ORIGIN_KEY: () => KIT_ORIGIN_KEY,
41
+ KNOCKED_DOWN_ANGLE: () => KNOCKED_DOWN_ANGLE,
42
+ KNOCK_CHEST: () => KNOCK_CHEST,
43
+ KNOCK_DOWN_SPEED: () => KNOCK_DOWN_SPEED,
39
44
  LANDING_IDS: () => LANDING_IDS,
40
45
  LAND_SOUND_IDS: () => LAND_SOUND_IDS,
41
46
  MATERIAL_SURFACES: () => MATERIAL_SURFACES,
@@ -46,7 +51,10 @@ var GameContracts = (() => {
46
51
  TOOL_CLIP: () => TOOL_CLIP,
47
52
  TWIST_LIFT: () => TWIST_LIFT,
48
53
  applyPatches: () => applyPatches,
54
+ blastBodySpeed: () => blastBodySpeed,
49
55
  blastShove: () => blastShove,
56
+ bodyImpulse: () => bodyImpulse,
57
+ bodyMass: () => bodyMass,
50
58
  checkAnimation: () => checkAnimation,
51
59
  checkBench: () => checkBench,
52
60
  checkCategorized: () => checkCategorized,
@@ -66,7 +74,9 @@ var GameContracts = (() => {
66
74
  ignitionChance: () => ignitionChance,
67
75
  isSurfaceId: () => isSurfaceId,
68
76
  kitOriginOf: () => kitOriginOf,
77
+ knockOf: () => knockOf,
69
78
  landSoundId: () => landSoundId,
79
+ leadClipEvents: () => leadClipEvents,
70
80
  markKitOrigin: () => markKitOrigin,
71
81
  mergeById: () => mergeById,
72
82
  removeIds: () => removeIds,
@@ -79,7 +89,9 @@ var GameContracts = (() => {
79
89
  sealOpener: () => sealOpener,
80
90
  shotExtras: () => shotExtras,
81
91
  shotToHit: () => shotToHit,
92
+ sideOnImpulse: () => sideOnImpulse,
82
93
  sparkTargetFactor: () => sparkTargetFactor,
94
+ stepKnock: () => stepKnock,
83
95
  surfaceOfMaterial: () => surfaceOfMaterial,
84
96
  swingSoundId: () => swingSoundId,
85
97
  toolAnimationStep: () => toolAnimationStep,
@@ -107,6 +119,106 @@ var GameContracts = (() => {
107
119
  return { speed, direction: [direction[0] / length, direction[1] / length, direction[2] / length], distance, falloff };
108
120
  }
109
121
 
122
+ // src/blast-impulse.ts
123
+ var DEFAULT_BLAST_IMPULSE = { bodyArea: 0.7, lift: 0.15, minSpeed: 0.3, tntPerJoule: 0.24 / 2e5 };
124
+ var NEAREST = 0.3;
125
+ function sideOnImpulse(tnt, distance) {
126
+ if (!(tnt > 0)) return 0;
127
+ const w3 = Math.cbrt(tnt);
128
+ const z = Math.max(distance, NEAREST) / w3;
129
+ const scaled = 0.067 * Math.sqrt(1 + (z / 0.23) ** 4) / (z * z * Math.cbrt(1 + (z / 1.55) ** 3));
130
+ return scaled * w3 * 100;
131
+ }
132
+ function bodyImpulse(tnt, distance) {
133
+ if (!(tnt > 0)) return 0;
134
+ const z = Math.max(distance, NEAREST) / Math.cbrt(tnt);
135
+ return sideOnImpulse(tnt, distance) * (2 + 4 / (1 + z * z));
136
+ }
137
+ function blastBodySpeed(tnt, distance, mass, reach = 1, options = {}) {
138
+ const area = options.bodyArea ?? DEFAULT_BLAST_IMPULSE.bodyArea;
139
+ return bodyImpulse(tnt, distance) * area * Math.max(0, reach) / Math.max(mass, 1);
140
+ }
141
+
142
+ // src/blast-knock.ts
143
+ var KNOCK_DOWN_SPEED = 2.5;
144
+ var KNOCKED_DOWN_ANGLE = 1.35;
145
+ var LIE_SECONDS = 1.6;
146
+ var WOUNDED_LIE_SECONDS = 6;
147
+ var FRAGMENT_COVER = 0.5;
148
+ var STUN_SECONDS = 4;
149
+ var STAND_UP_PER_SPEED = 0.25;
150
+ var FALL_SECONDS = 0.25;
151
+ var RISE_SECONDS = 0.6;
152
+ var GROUND_FRICTION = 7;
153
+ var GRAVITY = 9.81;
154
+ var KNOCK_CHEST = 1.1;
155
+ var CHEST = KNOCK_CHEST;
156
+ var bodyMass = (height, build = 1) => 70 * (height / 1.75) ** 2 * build;
157
+ function knockOf(blast, feet, mass, options, impulse = {}) {
158
+ const o = { ...DEFAULT_BLAST_IMPULSE, ...impulse };
159
+ const chest = [feet[0], feet[1] + CHEST, feet[2]];
160
+ const dx = chest[0] - blast.point[0];
161
+ const dy = chest[1] - blast.point[1];
162
+ const dz = chest[2] - blast.point[2];
163
+ const distance = Math.hypot(dx, dy, dz);
164
+ if (!(blast.radius > 0) || distance > blast.radius) return void 0;
165
+ const reach = Math.max(0, Math.min(1, blast.reach ?? 1));
166
+ const tnt = blast.tnt ?? blast.energy * o.tntPerJoule;
167
+ let speed = blastBodySpeed(tnt, distance, mass, reach, o);
168
+ const minSpeed = impulse.minSpeed ?? options?.minSpeed ?? o.minSpeed;
169
+ if (speed < minSpeed) speed = 0;
170
+ const struck = reach >= FRAGMENT_COVER;
171
+ const wounded = struck && blast.lethalRadius !== void 0 && distance <= blast.lethalRadius;
172
+ const edge = 1 - distance / blast.radius;
173
+ const stunSeconds = struck ? STUN_SECONDS * edge * edge * reach : 0;
174
+ if (speed === 0 && !wounded && stunSeconds < 0.25) return void 0;
175
+ const horizontal = Math.hypot(dx, dz);
176
+ const away = horizontal > 1e-4 ? [dx / horizontal, o.lift, dz / horizontal] : [0, 1, 0];
177
+ const length = Math.hypot(away[0], away[1], away[2]);
178
+ const velocity = [away[0] / length * speed, away[1] / length * speed, away[2] / length * speed];
179
+ return {
180
+ velocity,
181
+ airborne: velocity[1] > 0.5,
182
+ lying: 0,
183
+ lieLeft: wounded ? WOUNDED_LIE_SECONDS : 0,
184
+ away: horizontal > 1e-4 ? [dx / horizontal, 0, dz / horizontal] : [0, 0, 0],
185
+ down: speed >= KNOCK_DOWN_SPEED || wounded,
186
+ ...wounded ? { wounded } : {},
187
+ ...stunSeconds >= 0.25 ? { stunSeconds } : {}
188
+ };
189
+ }
190
+ function stepKnock(knock, position, dt, groundAt, alive) {
191
+ const v = knock.velocity;
192
+ position.x += v[0] * dt;
193
+ position.z += v[2] * dt;
194
+ const ground = groundAt(position.x, position.z);
195
+ if (knock.airborne || position.y > ground + 1e-4) {
196
+ v[1] -= GRAVITY * dt;
197
+ position.y += v[1] * dt;
198
+ if (position.y <= ground) {
199
+ position.y = ground;
200
+ v[1] = 0;
201
+ knock.airborne = false;
202
+ }
203
+ } else position.y = ground;
204
+ const horizontal = Math.hypot(v[0], v[2]);
205
+ if (!knock.airborne && horizontal > 0) {
206
+ const slowed = Math.max(0, horizontal - GROUND_FRICTION * dt);
207
+ v[0] *= slowed / horizontal;
208
+ v[2] *= slowed / horizontal;
209
+ }
210
+ const moving = horizontal > 0.05 || knock.airborne;
211
+ if (knock.down) {
212
+ if (moving || knock.lying < 1) knock.lying = Math.min(1, knock.lying + dt / FALL_SECONDS);
213
+ else if (alive) {
214
+ knock.lieLeft = knock.lieLeft > 0 ? knock.lieLeft : LIE_SECONDS + Math.hypot(v[0], v[2]) * STAND_UP_PER_SPEED;
215
+ knock.lieLeft -= dt;
216
+ if (knock.lieLeft <= 0) knock.down = false;
217
+ }
218
+ } else if (knock.lying > 0) knock.lying = Math.max(0, knock.lying - dt / RISE_SECONDS);
219
+ return !moving && !knock.down && knock.lying === 0;
220
+ }
221
+
110
222
  // src/impact-cost.ts
111
223
  var DEFAULT_COST_READS = 1e4;
112
224
  var DEFAULT_COST_CEILING_MS = 50;
@@ -650,6 +762,21 @@ var GameContracts = (() => {
650
762
  }
651
763
  return { ok: problems.length === 0, problems };
652
764
  };
765
+ var leadClipEvents = (clip) => {
766
+ const events = clip.events ?? [];
767
+ if (!events.some((event) => (event.lead ?? 0) > 0)) return clip;
768
+ let floor = 0;
769
+ const led = events.map((event) => {
770
+ const lead = Math.max(0, Math.min(event.t - floor, event.lead ?? 0));
771
+ const t = event.t - lead;
772
+ floor = Math.max(floor, t);
773
+ if (event.lead === void 0) return event;
774
+ const moved = { ...event, t, data: lead > 0 ? { ...event.data, lead } : { ...event.data } };
775
+ delete moved.lead;
776
+ return moved;
777
+ });
778
+ return { ...clip, events: led };
779
+ };
653
780
  var createToolClipPlayer = (options) => {
654
781
  const { clips, rig, onEvent } = options;
655
782
  let instances = [];
@@ -692,7 +819,7 @@ var GameContracts = (() => {
692
819
  }
693
820
  const found = find(step.clip, step.variant);
694
821
  if (!found) return;
695
- const clip = step.duration && step.duration > 0 ? scaleClip(found.clip, step.duration) : found.clip;
822
+ const clip = leadClipEvents(step.duration && step.duration > 0 ? scaleClip(found.clip, step.duration) : found.clip);
696
823
  const interval = options.repeatInterval?.() ?? 0.1;
697
824
  instances = instances.filter((existing) => existing.name !== found.name || existing.delay > 0);
698
825
  for (let round = 0; round < (step.repeat ?? 1); round++) {