@adatechnology/game-contracts 0.19.0 → 0.20.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
@@ -77,6 +77,13 @@ interface PartSpec {
77
77
  */
78
78
  outflow?: Vec3;
79
79
  };
80
+ /**
81
+ * Loose dry contents of a hollow part (the flakes of a cereal box, flour, sugar, coffee): a hole through it lets out a puff of them, in `color`
82
+ * (0xRRGGBB). The renderer draws the puff; nothing is lost from the part's mass.
83
+ */
84
+ powder?: {
85
+ color: number;
86
+ };
80
87
  /** The part explodes when destroyed, or when its temperature passes `atC` (gas bottle, fuel tank, aerosol). */
81
88
  explodes?: {
82
89
  energy: number;
@@ -658,12 +665,17 @@ type DestructionEvent = (EventBase & {
658
665
  part: string;
659
666
  dent: Dent;
660
667
  worldPoint: Vec3;
661
- }) | (EventBase & {
668
+ })
669
+ /** `powder`: the part's loose dry contents (`PartSpec.powder`), which puff out of the hole. */
670
+ | (EventBase & {
662
671
  type: 'hole';
663
672
  part: string;
664
673
  hole: Hole;
665
674
  worldPoint: Vec3;
666
675
  exitPoint?: Vec3;
676
+ powder?: {
677
+ color: number;
678
+ };
667
679
  }) | (EventBase & {
668
680
  type: 'crack';
669
681
  part: string;
@@ -812,6 +824,7 @@ type DestructionEvent = (EventBase & {
812
824
  /**
813
825
  * One unit of a wall of units (`PartSpec.units`: a brick, a block) came out under shots or blows: `center` in world space, `size` in the part's own
814
826
  * axes and `rotation` the part's world rotation (the box of the unit that left), `fragments` the unit itself. The part stays: draw the gap, do not hide it.
827
+ * With `torn`, nothing left the part but what a blow tore off its face (splinters of wood, lumps of MDF): the hole and the marks around it stay drawn.
815
828
  */
816
829
  | (EventBase & {
817
830
  type: 'chip';
@@ -820,6 +833,7 @@ type DestructionEvent = (EventBase & {
820
833
  size: Vec3;
821
834
  rotation: Quat;
822
835
  fragments: readonly Fragment[];
836
+ torn?: true;
823
837
  })
824
838
  /** Rebar of a reinforced part is bare: segments in world space. Empty `bars` means the cage is gone. */
825
839
  | (EventBase & {
@@ -1321,18 +1335,50 @@ interface ItemUseDef {
1321
1335
  id: string;
1322
1336
  /** Seconds. */
1323
1337
  duration: number;
1324
- /** Keyframes: time 0..duration, the hand-relative position/rotation (Euler XYZ) of the item and the container action during that key. */
1338
+ /**
1339
+ * Keyframes: time 0..duration, the hand-relative position/rotation (Euler XYZ) of the item and the container action during that key. `eat` takes the contents
1340
+ * out as `drink` does, as food: the liquid packed with it (the brine of a tin, the oil of the sardines) goes with the food, without the effects of drinking it. `turn` is the
1341
+ * time the other hand turns a part of the item round (a screw cap, `twistStroke`); the container itself does nothing then.
1342
+ */
1325
1343
  keys: Array<{
1326
1344
  t: number;
1327
1345
  position: Vec3;
1328
1346
  rotation: Vec3;
1329
- action?: 'drink' | 'pour' | 'open' | 'close' | 'none';
1347
+ action?: 'drink' | 'eat' | 'pour' | 'open' | 'close' | 'turn' | 'none';
1330
1348
  rate?: number;
1331
1349
  }>;
1332
1350
  /** Only valid when the container holds one of these (empty = any). */
1333
1351
  liquids?: LiquidId[];
1334
1352
  }
1335
1353
 
1354
+ /**
1355
+ * The seal of a closed container (a food tin, a drink can, a jar, a bottle with a cap): what opens it and whether a breach spoils what is inside.
1356
+ * Plain data and one pure check, shared by the item rules (`resolveUse` of game-items) and any host that lets the player open and eat (the test room).
1357
+ */
1358
+ interface SealSpec {
1359
+ /** Ids of the tools that open it (items or weapons: `can_opener`, `knife`); any one of them is enough. */
1360
+ readonly openWith?: readonly string[];
1361
+ /** It opens by hand (a pull tab, a ring pull, a screw cap): no tool needed. */
1362
+ readonly byHand?: boolean;
1363
+ /** A hole through the container or a leak spoils what is inside: it can no longer be eaten or drunk. */
1364
+ readonly spoilsWhenBreached?: boolean;
1365
+ }
1366
+ /** The state of one container: opened, holed through (a shot, a blade) or leaking. */
1367
+ interface SealState {
1368
+ readonly opened?: boolean;
1369
+ readonly breached?: boolean;
1370
+ readonly leaked?: boolean;
1371
+ }
1372
+ /** Why a sealed container cannot be eaten or drunk now. */
1373
+ type SealBlock = 'sealed' | 'breached' | 'leaked';
1374
+ /** Whether it can be opened with the tools at hand, and with which one (undefined when it opens by hand). */
1375
+ declare const sealOpener: (seal: SealSpec, tools: Iterable<string>) => {
1376
+ readonly ok: boolean;
1377
+ readonly tool?: string;
1378
+ };
1379
+ /** What stops eating or drinking it now, or undefined when nothing does. A spoiled container (breached, leaked) is blocked even once open. */
1380
+ declare const sealBlock: (seal: SealSpec, state: SealState) => SealBlock | undefined;
1381
+
1336
1382
  /**
1337
1383
  * Where a hand should be, in world space. `position` is the middle of the palm; the orientation follows one convention for every hand: the hand-local +x runs from the wrist to the
1338
1384
  * knuckles (the way the fingers point when open), +y is the back of the hand and +z completes the frame (for a right hand the little finger side; the thumb is at -z). A left hand is the
@@ -1360,10 +1406,40 @@ interface HandTargets {
1360
1406
  fingers?: {
1361
1407
  index?: number;
1362
1408
  };
1409
+ /**
1410
+ * Where the elbows go: `view` puts them on screen beside the hand, out to the side, as a tool held out in front (default); `body` lets them hang down and back by the body, as an item
1411
+ * carried close (a can, a bottle), where an elbow out to the side would lift it above the shoulder.
1412
+ */
1413
+ elbows?: 'view' | 'body';
1363
1414
  }
1364
1415
  /** Read every frame by whoever draws the arms; answers null while nothing is held. */
1365
1416
  type HandTargetSource = () => HandTargets | null;
1366
1417
 
1418
+ /** How a hand turns something round in strokes (a screw cap, a tap handle, a knob): what `twistStroke` answers for one moment of the turning. */
1419
+ interface TwistStroke {
1420
+ /** Radians the wrist is turned from the middle of its stroke, positive the way the part turns. */
1421
+ readonly angle: number;
1422
+ /** Radians the part has turned in all, from 0 to `strokes * reach`. */
1423
+ readonly turned: number;
1424
+ /** How closed the fingers are, 0 open to 1 closed: closed on the part while it turns, open while the wrist turns back. */
1425
+ readonly grip: number;
1426
+ /** 0..1, how far the hand rises off the part while the wrist turns back (1 is `TWIST_LIFT` metres). */
1427
+ readonly lift: number;
1428
+ }
1429
+ interface TwistOptions {
1430
+ /** Strokes of the wrist. Default 3. */
1431
+ readonly strokes?: number;
1432
+ /** Radians the part turns in one stroke (a wrist turns about a quarter round with its hold). Default 1.6. */
1433
+ readonly reach?: number;
1434
+ }
1435
+ /** Metres a hand rises off the part between two strokes. */
1436
+ declare const TWIST_LIFT = 0.012;
1437
+ /**
1438
+ * The hand turning a part round at `share` (0..1) of the whole turning: in each stroke the wrist turns with the part, then lets go, rises a little and turns back.
1439
+ * Pure and the same for every host, so first and third person turn the same hand the same way.
1440
+ */
1441
+ declare const twistStroke: (share: number, options?: TwistOptions) => TwistStroke;
1442
+
1367
1443
  /**
1368
1444
  * 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
1369
1445
  * every change back through `ModuleHost.bench`. Nothing here is specific to a kind of module.
@@ -1685,6 +1761,36 @@ interface ModuleItemFluid {
1685
1761
  uses?: string[];
1686
1762
  offset?: Vec3;
1687
1763
  }
1764
+ /** A closed container the player opens before eating or drinking it (a food tin, a drink can): the seal, and how the item looks open. */
1765
+ interface ModuleItemSeal extends SealSpec {
1766
+ /** Draws the item opened: gets the root `build` returned (the cut lid of a tin, the mouth of a can). */
1767
+ open?: (root: unknown) => void;
1768
+ /** Shows what is left in it once open: `share` of what it held (0 eaten up), after each bite or sip. */
1769
+ contents?: (root: unknown, share: number) => void;
1770
+ /**
1771
+ * The use (an `ItemUseDef` id, such as `unscrew`) a by-hand seal is opened with: it plays before the first bite or sip, the other hand turning the part
1772
+ * during its `turn` keys, and the item opens at its `open` key. Without it a by-hand seal opens at once on the way to the mouth (a pull tab).
1773
+ */
1774
+ openUse?: string;
1775
+ /** Draws the part being opened mid-way: `turned` radians round its axis, `lift` 0..1 of the way off (a screw cap leaving its thread). */
1776
+ turn?: (root: unknown, state: {
1777
+ readonly turned: number;
1778
+ readonly lift: number;
1779
+ }) => void;
1780
+ }
1781
+ /** A place on an item where a body sits or lies (a chair, a sofa cushion, a bed), in the item's own frame, the same shape as a `game-interactables` seat. */
1782
+ interface ItemSeat {
1783
+ id: string;
1784
+ pose: 'sit' | 'lie';
1785
+ /** Hips (sit) or pelvis (lie). */
1786
+ pos: Vec3;
1787
+ /** Facing of the body, relative to the item. */
1788
+ ry: number;
1789
+ /** Eye height above `pos`. Default 0.62 sitting, 0.18 lying. */
1790
+ eye?: number;
1791
+ /** Where the body stands when it gets up. */
1792
+ exit: Vec3;
1793
+ }
1688
1794
  /** 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). */
1689
1795
  interface Thumbnail {
1690
1796
  svg?: string;
@@ -1711,12 +1817,18 @@ interface TestableItem {
1711
1817
  /** The module's own destruction: parts, hits and state are its business. The world hosts it next to its own objects. `id` is the object id the host gives it: `DestructibleImpl.id` must be that id. */
1712
1818
  impl?: (root: unknown, id: string) => DestructibleImpl;
1713
1819
  fluid?: ModuleItemFluid;
1820
+ /** The item is a sealed container: it is opened first (with a tool or by hand), and a breach may spoil it (`SealSpec`). */
1821
+ seal?: ModuleItemSeal;
1714
1822
  /** The item is also a weapon the player can equip: an id of the arsenal in use. */
1715
1823
  weapon?: string;
1716
1824
  /** Use animations this item brings (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */
1717
1825
  animations?: AnimationClipSpec[];
1718
1826
  /** Can be grabbed and thrown. */
1719
1827
  movable?: boolean;
1828
+ /** A big piece that stands on the floor (a sofa, a wardrobe, a fridge) and can be pushed by leaning on it. Default false: what goes in the hand is grabbed, never pushed. */
1829
+ pushable?: boolean;
1830
+ /** Where a body sits or lies on it: the host can seat the player there. */
1831
+ seats?: readonly ItemSeat[];
1720
1832
  /** 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. */
1721
1833
  slow?: boolean | string;
1722
1834
  /**
@@ -1981,6 +2093,21 @@ interface PlayerState {
1981
2093
  /** How the camera follows the player (`first`, `shoulder`...); the ids are the host's. */
1982
2094
  readonly cameraMode: string;
1983
2095
  }
2096
+ /** A leaf a module moves itself, for the host's hand (`ModuleHost.reachFor`). */
2097
+ interface HostReach {
2098
+ /** World point of the handle now. */
2099
+ handle(): Vec3;
2100
+ /** World point of the hinge of a turning leaf: the host stands the player beside the free edge, clear of the swing. Left out, the player stands in front of the handle. */
2101
+ hinge?(): Vec3;
2102
+ /** How long the hand stays on the handle after the grip, seconds (the module's swing). */
2103
+ readonly seconds: number;
2104
+ /** The leaf comes toward the body (true) or goes away (false). Default true. */
2105
+ readonly pull?: boolean;
2106
+ /** Locked or barred: the hand only shakes it. */
2107
+ readonly locked?: boolean;
2108
+ /** The hand is on the handle. */
2109
+ onGrip?(): void;
2110
+ }
1984
2111
  /**
1985
2112
  * What the host lends a module that brings its own presentation (a weapon in the hands, sounds, ejected casings). `THREE`, `scene` and `camera`
1986
2113
  * are the host's three.js namespace, scene and camera: `unknown` here because the contracts do not depend on three.js.
@@ -2004,6 +2131,11 @@ interface ModuleHost {
2004
2131
  * unsubscribe function. A host that never clears leaves it out.
2005
2132
  */
2006
2133
  onClear?(listener: () => void): () => void;
2134
+ /**
2135
+ * Follow the host going back to its seeded start (before each scenario): the module's own random streams, seed counters and clocks are to go back to where
2136
+ * they began, so a scenario does the same whatever ran before it. Returns the unsubscribe function. A host that never resets leaves it out.
2137
+ */
2138
+ onReset?(listener: () => void): () => void;
2007
2139
  /** Write a line to the host's behaviour log; `type` is the kind of entry (default `module_note`), so a module can log what a filter looks for (`sound`). */
2008
2140
  log(text: string, data?: Record<string, unknown>, type?: string): void;
2009
2141
  /**
@@ -2034,6 +2166,18 @@ interface ModuleHost {
2034
2166
  * frame hooks have run, and puts the hands there. Pass null (or call the returned function) to let go. A host without arms leaves it out.
2035
2167
  */
2036
2168
  setHandTargets?(source: HandTargetSource | null): () => void;
2169
+ /**
2170
+ * Where the player's body holds a tool now (world space), in every view (the weapon is the body's own): `position` is the pocket of the right shoulder a stock rests on, `quaternion` turns
2171
+ * to the aim (-z the aim, x right, y up, as a camera). A module draws the tool there at its real size. Null for a host with no body (the module puts the same pocket under the camera's eye);
2172
+ * a host without a body leaves it out.
2173
+ */
2174
+ holdFrame?(): HandPose | null;
2175
+ /**
2176
+ * Puts the player's hand on a leaf the module moves itself (a door of a house): the host plays the reach to `handle()`, calls `onGrip` when the hand is on it (the module then turns
2177
+ * the handle and swings the leaf), keeps the hand on `handle()` for `seconds` as it moves, and takes it back; the body pulls or pushes (`pull`) or shakes a locked one (`locked`).
2178
+ * `handle` answers the world point of the handle every frame. A host without a body leaves it out (the module then moves the leaf at once).
2179
+ */
2180
+ reachFor?(reach: HostReach): void;
2037
2181
  /** Asks the tool in hand to do something (see `ToolCommand`). A host without accessories leaves it out. */
2038
2182
  command?(command: ToolCommand): ToolCommandResult;
2039
2183
  /** Lent only to a module that declares controls, layers or metrics. */
@@ -2294,4 +2438,4 @@ interface FootprintObject {
2294
2438
  */
2295
2439
  declare function footprintBlocker(object: FootprintObject | (() => FootprintObject | null | undefined), footprint: Footprint | (() => Footprint | null | undefined)): (x: number, z: number, radius: number) => boolean;
2296
2440
 
2297
- export { type ActionControl, type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type AreaPlacement, type AudioListener, type AudioSink, type BallisticExtras, type BenchChange, type BenchDeclaration, type BenchReport, type BenchSheet, type BlastOptions, type BlastReport, type BlastShove, type BlastShoveOptions, type BlastSource, type BodyRegion, type BodySlot, type BooleanControl, type BusyOptions, type BusySink, type BusyTask, type CategorizedOptions, type CategorizedReport, type CategoryDef, type CategoryOptions, type CategoryRegistry, type ChainDelay, type CheckOptions, type CheckSpecOptions, type ChoiceControl, type ChoiceOption, type ConfinementOptions, type ContainerSpec, type ContractChecker, type ContractContext, type ControlPatch, type ControlValue, type Crack, DEFAULT_BLAST_SHOVE, DEFAULT_ENVIRONMENT, DEFAULT_IGNITION_RULES, type DamageStage, type DamageType, type DebrisKind, type DeepPartial, type Dent, type DestructibleImpl, type DestructibleSpec, type DestructionEvent, type DestructionEventType, type EffectHandle, type EffectRequest, type EffectSink, type EnvironmentBus, type EnvironmentKey, type EnvironmentState, type ExplosionChain, type FluidKind, type Footprint, type FootprintObject, type FractureStyle, type Fragment, type FragmentHitOptions, type FragmentMark, type GroundLayout, type GroundZone, type HandPose, type HandTargetSource, type HandTargets, type Hit, type HitIgnition, type HitResult, type Hole, type HostEvent, type HostTool, type HostToolAttachment, IMPACT_CONTRACT, type IgnitionRules, type ImpactContract, type ItemAnimationContract, type ItemUseDef, type JointKind, type JointSpec, type JointState, KIT_ORIGIN_KEY, type KitOrigin, type LiquidId, type LoopHandle, MISSING_BITS, type MaterialId, type ModuleArea, type ModuleBench, type ModuleControl, type ModuleHost, type ModuleItemFluid, type ModuleLayer, type ModuleMetric, type ModuleScenario, type NumberControl, type ObjectState, type PartBreach, type PartShape, type PartSpec, type PartState, type PartStatus, type PartUnits, type PileSpec, type PlayerState, type Quat, type RayHit, type Reinforcement, STAGE_EVENT, type SegmentKind, type ShelterOptions, type Shot, type SoilLayer, type SoilProfile, type SoilState, type SoundHandle, type SoundRequest, type SpecReport, type StructureRole, type SurfaceLayer, TOOL_CLIP, 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 UnmarkedDrawing, type Use, type Vec3, type WarmUpProbes, type WindExposure, type WindState, applyPatches, blastShove, checkAnimation, checkBench, checkCategorized, checkDestructible, checkHit, checkSpec, checkToolClips, createEnvironmentBus, createToolClipPlayer, deepMerge, defineCategories, fillFactor, footprintBlocker, ignitionChance, kitOriginOf, markKitOrigin, mergeById, removeIds, resolveEntry, resolveIgnitionRules, sampleClip, scaleClip, shotExtras, shotToHit, sparkTargetFactor, toolAnimationStep, unmarkedDrawings, useToHit };
2441
+ export { type ActionControl, type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type AreaPlacement, type AudioListener, type AudioSink, type BallisticExtras, type BenchChange, type BenchDeclaration, type BenchReport, type BenchSheet, type BlastOptions, type BlastReport, type BlastShove, type BlastShoveOptions, type BlastSource, type BodyRegion, type BodySlot, type BooleanControl, type BusyOptions, type BusySink, type BusyTask, type CategorizedOptions, type CategorizedReport, type CategoryDef, type CategoryOptions, type CategoryRegistry, type ChainDelay, type CheckOptions, type CheckSpecOptions, type ChoiceControl, type ChoiceOption, type ConfinementOptions, type ContainerSpec, type ContractChecker, type ContractContext, type ControlPatch, type ControlValue, type Crack, DEFAULT_BLAST_SHOVE, DEFAULT_ENVIRONMENT, DEFAULT_IGNITION_RULES, type DamageStage, type DamageType, type DebrisKind, type DeepPartial, type Dent, type DestructibleImpl, type DestructibleSpec, type DestructionEvent, type DestructionEventType, type EffectHandle, type EffectRequest, type EffectSink, type EnvironmentBus, type EnvironmentKey, type EnvironmentState, type ExplosionChain, type FluidKind, type Footprint, type FootprintObject, type FractureStyle, type Fragment, type FragmentHitOptions, type FragmentMark, type GroundLayout, type GroundZone, type HandPose, type HandTargetSource, type HandTargets, type Hit, type HitIgnition, type HitResult, type Hole, type HostEvent, type HostReach, type HostTool, type HostToolAttachment, IMPACT_CONTRACT, type IgnitionRules, type ImpactContract, type ItemAnimationContract, type ItemSeat, type ItemUseDef, type JointKind, type JointSpec, type JointState, KIT_ORIGIN_KEY, type KitOrigin, type LiquidId, type LoopHandle, MISSING_BITS, type MaterialId, type ModuleArea, type ModuleBench, type ModuleControl, type ModuleHost, type ModuleItemFluid, type ModuleItemSeal, type ModuleLayer, type ModuleMetric, type ModuleScenario, type NumberControl, type ObjectState, type PartBreach, type PartShape, type PartSpec, type PartState, type PartStatus, type PartUnits, type PileSpec, type PlayerState, type Quat, type RayHit, type Reinforcement, STAGE_EVENT, type SealBlock, type SealSpec, type SealState, type SegmentKind, type ShelterOptions, type Shot, type SoilLayer, type SoilProfile, type SoilState, type SoundHandle, type SoundRequest, type SpecReport, type StructureRole, type SurfaceLayer, TOOL_CLIP, TWIST_LIFT, type TensionRebar, type TestableItem, type TestableModule, type Thumbnail, type ToolAnimationStep, type ToolAnimator, type ToolClipId, type ToolClipPlayer, type ToolClipPlayerOptions, type ToolClipSet, type ToolCommand, type ToolCommandResult, type ToolKind, type ToolRig, type TunedMassDamper, type TwistOptions, type TwistStroke, type UnmarkedDrawing, type Use, type Vec3, type WarmUpProbes, type WindExposure, type WindState, applyPatches, blastShove, checkAnimation, checkBench, checkCategorized, checkDestructible, checkHit, checkSpec, checkToolClips, createEnvironmentBus, createToolClipPlayer, deepMerge, defineCategories, fillFactor, footprintBlocker, ignitionChance, kitOriginOf, markKitOrigin, mergeById, removeIds, resolveEntry, resolveIgnitionRules, sampleClip, scaleClip, sealBlock, sealOpener, shotExtras, shotToHit, sparkTargetFactor, toolAnimationStep, twistStroke, unmarkedDrawings, useToHit };
@@ -29,6 +29,7 @@ var GameContracts = (() => {
29
29
  MISSING_BITS: () => MISSING_BITS,
30
30
  STAGE_EVENT: () => STAGE_EVENT,
31
31
  TOOL_CLIP: () => TOOL_CLIP,
32
+ TWIST_LIFT: () => TWIST_LIFT,
32
33
  applyPatches: () => applyPatches,
33
34
  blastShove: () => blastShove,
34
35
  checkAnimation: () => checkAnimation,
@@ -53,10 +54,13 @@ var GameContracts = (() => {
53
54
  resolveIgnitionRules: () => resolveIgnitionRules,
54
55
  sampleClip: () => sampleClip,
55
56
  scaleClip: () => scaleClip,
57
+ sealBlock: () => sealBlock,
58
+ sealOpener: () => sealOpener,
56
59
  shotExtras: () => shotExtras,
57
60
  shotToHit: () => shotToHit,
58
61
  sparkTargetFactor: () => sparkTargetFactor,
59
62
  toolAnimationStep: () => toolAnimationStep,
63
+ twistStroke: () => twistStroke,
60
64
  unmarkedDrawings: () => unmarkedDrawings,
61
65
  useToHit: () => useToHit
62
66
  });
@@ -407,6 +411,38 @@ var GameContracts = (() => {
407
411
  return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
408
412
  };
409
413
 
414
+ // src/seal.ts
415
+ var sealOpener = (seal, tools) => {
416
+ if (seal.byHand || !seal.openWith || seal.openWith.length === 0) return { ok: true };
417
+ const have = new Set(tools);
418
+ const tool = seal.openWith.find((id) => have.has(id));
419
+ return tool === void 0 ? { ok: false } : { ok: true, tool };
420
+ };
421
+ var sealBlock = (seal, state) => {
422
+ if (seal.spoilsWhenBreached && state.breached) return "breached";
423
+ if (seal.spoilsWhenBreached && state.leaked) return "leaked";
424
+ if (!state.opened) return "sealed";
425
+ return void 0;
426
+ };
427
+
428
+ // src/twist.ts
429
+ var TWIST_LIFT = 0.012;
430
+ var TURNING = 0.7;
431
+ var smooth = (k) => k * k * (3 - 2 * k);
432
+ var twistStroke = (share, options = {}) => {
433
+ const strokes = Math.max(1, Math.round(options.strokes ?? 3));
434
+ const reach = options.reach ?? 1.6;
435
+ const along = Math.min(1, Math.max(0, share)) * strokes;
436
+ const index = Math.min(strokes - 1, Math.floor(along));
437
+ const phase = along - index;
438
+ if (phase <= TURNING) {
439
+ const k2 = smooth(phase / TURNING);
440
+ return { angle: reach * (k2 - 0.5), turned: reach * (index + k2), grip: 1, lift: 0 };
441
+ }
442
+ const k = (phase - TURNING) / (1 - TURNING);
443
+ return { angle: reach * (0.5 - smooth(k)), turned: reach * (index + 1), grip: 0.35, lift: Math.sin(k * Math.PI) };
444
+ };
445
+
410
446
  // src/animation.ts
411
447
  var finite3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number" && Number.isFinite(n));
412
448
  var checkAnimation = (clip, partIds) => {