@typeonce/ritmo-browser 0.10.0 → 0.11.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +6 -1
  3. package/dist/Controls.d.ts +495 -0
  4. package/dist/Controls.d.ts.map +1 -0
  5. package/dist/Controls.js +343 -0
  6. package/dist/Controls.js.map +1 -0
  7. package/dist/Gamepad.d.ts +375 -0
  8. package/dist/Gamepad.d.ts.map +1 -0
  9. package/dist/Gamepad.js +452 -0
  10. package/dist/Gamepad.js.map +1 -0
  11. package/dist/InputCapture.d.ts +46 -16
  12. package/dist/InputCapture.d.ts.map +1 -1
  13. package/dist/InputCapture.js +1 -1
  14. package/dist/InputCapture.js.map +1 -1
  15. package/dist/Keyboard.d.ts +184 -21
  16. package/dist/Keyboard.d.ts.map +1 -1
  17. package/dist/Keyboard.js +257 -91
  18. package/dist/Keyboard.js.map +1 -1
  19. package/dist/Pointer.d.ts +231 -62
  20. package/dist/Pointer.d.ts.map +1 -1
  21. package/dist/Pointer.js +205 -145
  22. package/dist/Pointer.js.map +1 -1
  23. package/dist/Touch.d.ts +73 -27
  24. package/dist/Touch.d.ts.map +1 -1
  25. package/dist/Touch.js +25 -30
  26. package/dist/Touch.js.map +1 -1
  27. package/dist/index.d.ts +10 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +10 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/internal/buttons.d.ts +52 -0
  32. package/dist/internal/buttons.d.ts.map +1 -0
  33. package/dist/internal/buttons.js +211 -0
  34. package/dist/internal/buttons.js.map +1 -0
  35. package/dist/internal/introspect.d.ts +14 -0
  36. package/dist/internal/introspect.d.ts.map +1 -0
  37. package/dist/internal/introspect.js +19 -0
  38. package/dist/internal/introspect.js.map +1 -0
  39. package/dist/internal/used.d.ts +8 -0
  40. package/dist/internal/used.d.ts.map +1 -0
  41. package/dist/internal/used.js +14 -0
  42. package/dist/internal/used.js.map +1 -0
  43. package/dist/internal/vectors.d.ts +24 -0
  44. package/dist/internal/vectors.d.ts.map +1 -0
  45. package/dist/internal/vectors.js +22 -0
  46. package/dist/internal/vectors.js.map +1 -0
  47. package/package.json +4 -3
  48. package/src/Controls.ts +799 -0
  49. package/src/Gamepad.ts +747 -0
  50. package/src/InputCapture.ts +43 -24
  51. package/src/Keyboard.ts +395 -68
  52. package/src/Pointer.ts +392 -164
  53. package/src/Touch.ts +111 -54
  54. package/src/index.ts +10 -1
  55. package/src/internal/buttons.ts +222 -0
  56. package/src/internal/introspect.ts +23 -0
  57. package/src/internal/used.ts +14 -0
  58. package/src/internal/vectors.ts +36 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # @typeonce/ritmo-browser
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 778301a: **Draining a service is declared, checked at compile time, and observed at run time.**
8
+
9
+ - **`System.Consumes<"snapshot">`** marks the methods of a service type that drain it. A system that declares the service with `Game.System.service` sees those methods as a compile error naming the fix (`snapshot() drains Game/Controls: declare it with Game.System.consumeService, or read the resource its capture writes`); `Game.System.consumeService` calls them. Its other methods stay available through `service` (a settings screen can still `rebind`).
10
+ - Every input source is marked: `Keyboard.Actions`, `Keyboard.Presses`, `Gamepad.Actions`, `Gamepad.Pads`, `Touch.Actions`, `Pointer.Actions`, `Controls.Device`, `Controls.Controls`, `Controls.Pool`, and `InputCapture.Source`. **Breaking:** a system that called `snapshot()` through `Game.System.service` no longer compiles; read the resource `InputCapture.system` writes instead (or, for a system that must drain the source itself, declare it with `Game.System.consumeService`).
11
+ - `Game.System.service` returns `ServiceRead<D, false>` and `consumeService` returns `ServiceRead<D, true>`.
12
+ - The `service-consumed-twice` lint also reports what a debug runtime saw: a consumed service whose consumers ran at more than one placement in the session, in unnamed schedules too (`Game/Drain (schedule#2, step 0, unnamed)`). Named schedules are reported once, by the static lint.
13
+
14
+ - 866bcff: **`Controls` merges several input devices into one source, every device snapshot shares one shape, and capturing input is checked at compile time and by `describe()`.**
15
+
16
+ - **`Controls.actions({ buttons: [...] })`** declares the game's actions once. Its `merge({ keyboard, gamepad, touch })` builds one input source: an action is `held` while any device holds it, `pressed` when any device pressed it, and `released` when a device released it and none holds it now (the same rules as one device with all the bindings). Each device binds every declared action; a device that cannot is passed as `lacking(device, ["pause"])`, which must name exactly the actions it lacks (autocomplete offers them). A device binding an undeclared action, a device missing an action, and an action no device binds do not compile.
17
+ - The merged snapshot has `actions`, `vectors`, `available` (whether the last used device binds each action, so a game can offer a lacking action another way), `devices` (each device's own snapshot, keyed by the merge names: sticks, triggers, touch aim), and `lastUsed` (`null` until a device is used). The merged source exposes `rebind.<device>` for devices that can rebind, and `dispose()`. `idle({ keyboard: Keyboard.idle(...), ... })` initializes the input resource. Reading an action on a device that lacks it does not compile.
18
+ - **Breaking: `Keyboard.Snapshot` is `{ actions, vectors, used }`.** Read `input.actions.jump.pressed` instead of `input.jump.pressed`. Hand-written timelines and bindings are unchanged.
19
+ - **Breaking: `Touch.Snapshot` renames `buttons` to `actions`** (each still a `ButtonState` with `aim` and `cancelled`) **and replaces `touched` with `used`.** Read `input.actions.fire` instead of `input.buttons.fire`. `Layout.buttons` and `View.buttons` are unchanged.
20
+ - Every device snapshot (`Keyboard`, `Gamepad`, `Pointer`, `Touch`) has `used`: something pressed in this window or held, or a stick or swipe in use. Releases do not count, so a blur or a disconnect does not change `lastUsed`.
21
+ - **`Game.System.consumeService(Service)`** declares a service a system drains on each run (an input source's `snapshot()`, a message queue); it is the only way to call the draining methods (see `System.Consumes`). `describe()` warns with the new `service-consumed-twice` lint when a consumed service has more than one placement, for example the same input captured in a fixed and a render schedule, where each capture sees only the presses since the other ran. `SystemDescription.consumes` lists them.
22
+ - **Breaking: `InputCapture.system` keeps its declared access**: it consumes the source and writes the resource, so a `Game.System.thisTick` reader of the resource placed before the capture no longer compiles (the error names both systems and the fix: place the reader after the capture), and capturing one source twice is reported by `describe()`. The exported `InputCapture.CaptureSystem` type is removed: let the system's type be inferred.
23
+ - **`Debug.serviceLints`** lets a service value add findings to `describe()` lints, reported as `service-lint` with the service as subject; `Controls` uses it to list every device that lacks an action.
24
+
25
+ - deaed5a: **`Gamepad` reads controllers as named actions, and keyboard and gamepad bindings can change at runtime.**
26
+
27
+ - **`Gamepad.actions(window, bindings, options)`** binds actions to buttons named by the browser's standard mapping (`"south"`, `"west"`, `"rightTrigger"`, `"dpadUp"`, `"start"`...). Each snapshot has `actions` (as `Keyboard`'s), every `buttons` state by name whatever the bindings (for menu navigation and for a settings screen waiting for the button to bind), the `left` and `right` sticks with a radial dead zone (`y` grows down, as `Touch`'s stick), how far each trigger is pulled, and the followed pad's `id`. It follows the pad used last and releases everything held when that pad disconnects. It reads the pads on every animation frame (and on every snapshot) until `dispose()`, so taps shorter than an update count with nothing to call; the lifecycle is `snapshot` / `rebind` / `dispose`, as for `Keyboard`. `Gamepad.idle`, `scripted`, `recording`, `parseTimeline`, and `isButton` match the other devices; timelines name buttons, so a replay follows the bindings, and an entry's `connected` records a disconnect or reconnect, so a replay drops and regains the pad on the same frames.
28
+ - **`rebind(bindings)`** on `Keyboard.actions(...)` and `Gamepad.actions(...)` binds the same actions to new keys or buttons, for players who remap their controls. An action held only through a key it lost reports `released`; a key held across the rebind holds a newly bound action only after a fresh press, so an action never turns held without a press. It takes exactly the actions the source was made with: an extra action fails to compile with `<name> is not an action of these bindings`, even from an object not written inline. Scripted keyboard actions ignore it (their timeline names actions).
29
+ - **`Keyboard.presses(window)`** reports every key that went down since the previous snapshot, bound or not, as `{ key, code }`, so a settings screen can wait for the key to bind (the gamepad equivalent is the snapshot's `buttons`).
30
+ - **`Keyboard.parseRebinding(bindings, data)`** and **`Gamepad.parseRebinding(bindings, data)`** validate saved bindings (for example from `localStorage`) as a `Result` ready for `rebind`, with the offending `path` on failure.
31
+ - **Timelines replay a release and press inside one update.** An action (keyboard) or button (gamepad) in both `press` and `release` of one entry is now a release then a new press when it was held, and still a tap when it was up, so `recording(...)` captures a quick double tap and the replay matches.
32
+ - **Breaking: `InputCapture.system` requires the resource's value type to be exactly the source's snapshot type.** A resource that only matched both ways (for example one with an extra optional field) now fails with a message naming the fix: declare it as `Descriptor.TransientResource<InputCapture.SnapshotOf<typeof Source>>()`.
33
+ - **Breaking for hand-written sources:** `Keyboard.Actions` now requires `rebind`. An object implementing it by hand adds `rebind() {}`.
34
+
35
+ - 57430bb: **Local multiplayer: players join slots by pressing a button on any pooled device.**
36
+
37
+ - **`Controls.players(Actions, { slots, join, leave })`** declares the players: the slots (the most at once, in join order), the button action that joins (or `"any"` for any button), and the one that leaves while joining is open. A misspelled or vector action, no slot, a slot named `joining`, and a button action named `any` (it would read as `join: "any"`) do not compile.
38
+ - **`pool(devices)`** on the declared players builds one input source from devices nobody holds yet. Pressing join on one gives it the first free slot; that snapshot reports `event: "joined"` with the player's input idle, so the press does nothing else (joining with the jump button does not jump). One device holds one slot; slots keep their names when another empties, and the next player fills the gap. Each pooled device binds every action (or is passed through `lacking`), and the join action always.
39
+ - The snapshot has every slot by name (`input.p2`): `joined`, `device` (what it joined with), `connected`, `event` (`"joined"`, `"left"`, or `null`), and the player's `actions`, `vectors`, and `available`, idle while the slot is empty. A device's own snapshot is behind a check of `device` (`if (input.p2.device === "pads") input.p2.devices.pads.left`). The declared players' `idle(...)` initializes the resource.
40
+ - Systems call `setJoining(open)` (closed at the start: open it on a join screen, or once for drop-in play), `seat(slot, device)` (a player in from the start, or picked on a menu: a pool name, or `{ pads: index }` for one pad; returns `{ ok: false, reason }` when the slot or device is taken), `leave(slot)` (a pause menu), and `rebind.<device>` through a plain service. Leaving by button works only while joining is open; otherwise `leave` is an ordinary action.
41
+ - **`Gamepad.pads(host, bindings, max)`** reads every pad on its own, for a pool: each pad joins as its own player. An unplugged pad keeps its slot (`connected: false`) and returns to it when it comes back; two pads of one model share an id, so they are told apart by their place in the pad list. `Gamepad.scriptedPads(bindings, timelines)` drives one timeline per pad in tests.
42
+ - `describe()` lints two keyboard devices that bind the same key, a device taken by two sources (a merge and a pool), and joining never opened.
43
+
44
+ - 97e39c5: **`Pointer` binds actions like `Keyboard` and `Gamepad` and merges into `Controls`, and `Controls.group` makes devices used together count as one.**
45
+
46
+ - **Breaking: `Pointer.track(target, blurHost, options?)` is `Pointer.actions(target, blurHost, bindings, options?)`.** Bindings map actions to buttons (`{ fire: ["primary"] }`); a misspelled button or an empty list does not compile. Pass `{}` for position and raw buttons only. The `Pointer.Pointer` type is `Pointer.Actions<B>` and `TrackOptions` is `ActionsOptions`.
47
+ - **Breaking: the snapshot is `{ actions, vectors, buttons, position, movement, wheel, over, locked, used }`.** Read `input.buttons.primary` (or a bound action) instead of `input.primary`. `used` is a press, a hold, or movement over the element; releases do not count. `movement`, `wheel`, and `locked` come with the wheel and the pointer lock.
48
+ - **The `back` and `forward` side buttons** (`PointerEvent.button` 3 and 4) are reported and bindable. `Pointer.buttonNames` and `Pointer.isButton` list and check them.
49
+ - `rebind(...)` swaps the buttons of the same actions; `Pointer.parseRebinding(bindings, data)` validates saved ones.
50
+ - **Breaking: `Pointer.idle(bindings, position?)` and `Pointer.scripted(bindings, timeline, start?)`** take the bindings first. Timelines still name buttons, so a replay follows the bindings as live play does.
51
+ - **`actions.group({ keyboard, mouse })`** groups devices into one merge entry: it binds what its devices bind between them, and `lastUsed` and `available` follow it as a whole, so walking with the keyboard while aiming with the mouse stays on one device. Grouped devices keep their own names in `devices`, `rebind`, and `idle`; `lastUsed` names the group. Pass the group to `lacking(...)` for actions none of its devices binds. A group of fewer than two devices, a `lacking` device or a group inside a group, a device binding an undeclared action, and a name used twice across devices and groups do not compile.
52
+
53
+ - bb371d4: **`Pointer` reads the wheel and the pointer lock, and `Touch.track` is `Touch.actions`, so every device is built the same way.**
54
+
55
+ - **The wheel**, read two ways for two jobs. `wheel` is how far it scrolled since the previous snapshot, in CSS pixels (lines and pages are converted), for zoom. The `wheelUp`, `wheelDown`, `wheelLeft`, and `wheelRight` buttons pulse (pressed and released in one window) once per `Pointer.wheelStep` (100 px, one notch), so a stepping action binds them (`nextWeapon: ["wheelDown"]`) and merges with a gamepad bumper in `Controls`. The page does not scroll over the element unless `preventScroll: false`. `PointerTarget` gains the `wheel` listener.
56
+ - **Mouse look:** `movement` is the raw motion since the previous snapshot (`movementX`/`movementY`), locked or not. With `lock: document`, a click on the element locks the pointer (the browser grants a lock only inside a click, so the module asks in its own listener); that click is not reported as a press. `locked` turns `false` when the player leaves with Escape, which releases what was held. `used` also counts motion and scrolling.
57
+ - Timelines record and replay `wheel`, `movement`, and `locked`. Their `press`/`release` take `Pointer.PhysicalButton`s; wheel pulses come from `wheel`.
58
+ - **Breaking: `Touch.track(target, blurHost, layout, options?)` is `Touch.actions(...)`, the `Touch.Touch<B>` type is `Touch.Actions<B>`, and `Touch.TrackOptions` is `Touch.ActionsOptions`,** matching `Keyboard.actions`, `Gamepad.actions`, and `Pointer.actions`. Arguments and behavior are unchanged.
59
+
60
+ - b3b16fd: **Movement is one vector action: declared once, bound per device, merged by `Controls`, and checked like every other action.**
61
+
62
+ - **`Controls.actions({ buttons: [...], vectors: [...] })`** declares both kinds. A button action is pressed and held (jump); a vector action is a direction with length at most 1, `y` growing down (move). A name declared as both kinds does not compile. The declared names are listed in the set's `buttons` and `vectors`.
63
+ - **`Keyboard.vector({ up, down, left, right })`** binds a vector to keys: opposite keys cancel and a diagonal has length 1. **`Gamepad.vector({ stick, up, down, left, right })`** binds it to a stick, the d-pad, or both (the longer wins). **A `Touch` layout's `stick.vector`** names the vector the stick drives. Every device snapshot has `vectors`; read `input.vectors.move` (`Pointer`'s is empty: it reports `position` and `movement`).
64
+ - `Controls` merges a vector as the longest any device reports, so devices never add up past 1 or cancel each other. Vectors work with `lacking`, `available`, `idle`, and `lastUsed` (a vector in use counts as use). Binding a vector action as a button (or the reverse), missing a vector, and reading `actions.move` or `vectors.jump` do not compile, each with an error naming the fix.
65
+ - `rebind` and `parseRebinding` take vector bindings (`Gamepad` can swap sticks).
66
+ - Keyboard timelines name a vector by direction, `press: ["move.right"]`; `parseTimeline` validates it. Button actions are named as before.
67
+ - **Breaking: `Touch.idle` and `Touch.scripted` take the names as an object**, `{ buttons, vector? }` (`vector` as the layout's `stick.vector`):
68
+
69
+ ```diff
70
+ -Touch.idle(["fire"])
71
+ -Touch.scripted(["fire"], timeline)
72
+ +Touch.idle({ buttons: ["fire"] })
73
+ +Touch.scripted({ buttons: ["fire"] }, timeline)
74
+ ```
75
+
76
+ - Directions that step one tile at a time (a grid game reading taps) stay button actions.
77
+
78
+ ### Patch Changes
79
+
80
+ - e46f284: **Documentation pass over every public module.** Every export has JSDoc saying what it is for and which neighbouring API to use instead; examples that no longer compiled (`Session.make` without `hostClock`, `Replay.session`, the `Pointer` rebind) are fixed; `Debug.Lint` lists every lint code the runtime emits. The `stale-target` lint's fix now names `queries.x.getHandle(handle)`.
81
+ - Updated dependencies [778301a]
82
+ - Updated dependencies [866bcff]
83
+ - Updated dependencies [e46f284]
84
+ - @typeonce/ritmo@0.11.0
85
+
3
86
  ## 0.10.0
4
87
 
5
88
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # `@typeonce/ritmo-browser`
2
2
 
3
- Browser integrations for ritmo: animation frames as a tick source for the core fixed-step loop (`BrowserTickSource`), keyboard actions with press/release edges, bound by character or by physical key (`Keyboard`), mouse and pen position and button edges over an element (`Pointer`), on-screen touch controls with a floating stick and drag-to-aim buttons (`Touch`), scripted and recorded keyboard and pointer timelines for tests and replays, and a system that copies input into a resource (`InputCapture`).
3
+ Browser integrations for ritmo:
4
+
5
+ - `BrowserTickSource`: animation frames as a tick source for the core fixed-step loop.
6
+ - Devices, each built with `X.actions(...)` from its host and bindings (a layout for `Touch`) and reporting a snapshot of `actions`, `vectors`, and `used`: `Keyboard` (keys by character or physical key), `Gamepad` (standard-mapping button names, sticks, and triggers), `Pointer` (mouse and pen buttons, position, wheel, pointer lock), and `Touch` (on-screen controls with a floating stick and drag-to-aim buttons). Keyboard, gamepad, and pointer bindings are rebindable at runtime for a controls settings screen (validate saved ones with `parseRebinding`); every device has scripted timelines for tests, and keyboard, gamepad, and pointer also record them for replays.
7
+ - `Controls`: the game's actions declared once, as buttons and vectors (movement with length at most 1, from keys, d-pad, stick, or touch stick), and the devices merged into one input source; devices used together (keyboard and mouse) are grouped as one. `Controls.players` and `Gamepad.pads`: local multiplayer, where players join a slot by pressing a button on any pooled device, every pad on its own.
8
+ - `InputCapture`: the system that drains an input source once per update into a resource. Systems read that resource; a system that calls `snapshot()` itself declares the source with `Game.System.consumeService`.
4
9
 
5
10
  ```sh
6
11
  pnpm add @typeonce/ritmo-browser @typeonce/ritmo
@@ -0,0 +1,495 @@
1
+ /**
2
+ * One input source merged from several devices, so gameplay reads each
3
+ * action once whatever the player uses.
4
+ *
5
+ * Declare the game's actions once with `Controls.actions({ buttons: [...] })`, bind them
6
+ * on every device (`Keyboard.actions`, `Gamepad.actions`, `Pointer.actions`,
7
+ * `Touch.actions`), and merge the devices with the declaration's `merge`.
8
+ * Merged devices
9
+ * behave like one device with all their bindings: an action is `held` while
10
+ * any device holds it, `pressed` when any device pressed it, and `released`
11
+ * when a device released it and none holds it now. A vector action (movement)
12
+ * is the longest direction any device reports, so a stick and keys never add
13
+ * up past length 1 or cancel each other. Device-specific data
14
+ * (sticks, triggers, touch aim) stays in `devices`, under the names given to
15
+ * `merge`; `lastUsed` names the device the player used last, for button
16
+ * prompts.
17
+ *
18
+ * Devices the player uses together (keyboard and mouse) go in one
19
+ * `group(...)`: the group is one entry of the merge, so walking with the
20
+ * keyboard while aiming with the mouse keeps `lastUsed` on the group, and
21
+ * `available` follows what the group binds between its devices. Grouped
22
+ * devices keep their own names in `devices` and `rebind`.
23
+ *
24
+ * Every entry binds every action. A device that cannot (a touch layout
25
+ * without a pause button) is passed as `actions.lacking(device, [...])`,
26
+ * which names exactly the actions it lacks. Reading a lacking action on that
27
+ * device does not compile; `available` reports whether the last used device
28
+ * has each action, and `describe()` lists every gap as a `service-lint`.
29
+ *
30
+ * Capture the merged source with `InputCapture.system(...)` and build the
31
+ * devices inside the `merge(...)` call, so nothing else takes their
32
+ * snapshots. A game with one device captures that device directly instead.
33
+ *
34
+ * A merge is one player on every device. For local multiplayer, where each
35
+ * device belongs to the player who joined with it, declare
36
+ * `Controls.players(Actions, { slots, join, leave })` and capture its
37
+ * `pool(...)` instead.
38
+ *
39
+ * @module Controls
40
+ * @docGroup browser
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const Actions = Controls.actions({ buttons: ["jump", "pause"], vectors: ["move"] })
45
+ *
46
+ * const keyBindings = {
47
+ * move: Keyboard.vector({ up: ["ArrowUp", "w"], down: ["ArrowDown", "s"], left: ["ArrowLeft", "a"], right: ["ArrowRight", "d"] }),
48
+ * jump: [" "],
49
+ * pause: ["Escape"]
50
+ * } as const
51
+ * const padBindings = {
52
+ * move: Gamepad.vector({ stick: "left", up: ["dpadUp"], down: ["dpadDown"], left: ["dpadLeft"], right: ["dpadRight"] }),
53
+ * jump: ["south"],
54
+ * pause: ["start"]
55
+ * } as const
56
+ *
57
+ * // One function builds the controls, for the browser and for tests.
58
+ * export const makeControls = (devices: {
59
+ * readonly keyboard: Keyboard.Actions<typeof keyBindings>
60
+ * readonly gamepad: Gamepad.Actions<typeof padBindings>
61
+ * }) => Actions.merge(devices)
62
+ *
63
+ * const ControlsInput = Descriptor.Service<ReturnType<typeof makeControls>>()("Game/Controls")
64
+ * const Input = Descriptor.TransientResource<InputCapture.SnapshotOf<typeof ControlsInput>>()("Game/Input")
65
+ * const CaptureInput = InputCapture.system(Game, { name: "Game/CaptureInput", source: ControlsInput, resource: Input })
66
+ *
67
+ * const runtime = Game.Runtime.make({
68
+ * services: Game.Runtime.services(Game.Runtime.service(ControlsInput, makeControls({
69
+ * keyboard: Keyboard.actions(window, keyBindings),
70
+ * gamepad: Gamepad.actions(window, padBindings)
71
+ * }))),
72
+ * resources: { Input: Actions.idle({ keyboard: Keyboard.idle(keyBindings), gamepad: Gamepad.idle(padBindings) }) }
73
+ * }).value
74
+ *
75
+ * // In a system:
76
+ * const input = resources.input.get()
77
+ * walk(input.vectors.move) // length at most 1: keys, d-pad, or stick
78
+ * if (input.actions.jump.pressed) jump()
79
+ * const prompt = input.lastUsed === "gamepad" ? "Ⓐ" : "Space"
80
+ * ```
81
+ */
82
+ import type * as System from "@typeonce/ritmo/System";
83
+ import type { ActionState } from "./Keyboard.ts";
84
+ import type { Vector } from "./internal/vectors.ts";
85
+ export type { Vector };
86
+ /**
87
+ * The part of a device's snapshot `Controls` merges: every button action's
88
+ * state, every vector action's direction, and whether the player used the
89
+ * device in this window.
90
+ */
91
+ export interface DeviceSnapshot<A extends string, V extends string = never> {
92
+ readonly actions: {
93
+ readonly [K in A]: ActionState;
94
+ };
95
+ readonly vectors: {
96
+ readonly [K in V]: Vector;
97
+ };
98
+ readonly used: boolean;
99
+ }
100
+ /** A device `Controls` can merge: `Keyboard.actions`, `Gamepad.actions`, `Pointer.actions`, `Touch.actions`, or their scripted forms. */
101
+ export interface Device<A extends string = string, V extends string = string> extends System.Consumes<"snapshot"> {
102
+ snapshot(): DeviceSnapshot<A, V>;
103
+ dispose(): void;
104
+ }
105
+ /**
106
+ * Devices the player uses together as one (keyboard and mouse), built with
107
+ * {@link ActionSet.group}. The group is one entry of the merge: it binds
108
+ * what its devices bind between them, and `lastUsed` and `available` follow
109
+ * it as a whole. Its devices keep their own names in `devices` and `rebind`.
110
+ */
111
+ export interface Group<Members> {
112
+ readonly _tag: "Group";
113
+ readonly members: Members;
114
+ }
115
+ /**
116
+ * Every pad on its own, for a player pool: `Gamepad.pads(...)`. Each pad
117
+ * joins as its own player.
118
+ */
119
+ export interface Pads {
120
+ readonly _tag: "Pads";
121
+ readonly max: number;
122
+ snapshot(): ReadonlyArray<DeviceSnapshot<string, string> & {
123
+ readonly id: string | null;
124
+ readonly connected: boolean;
125
+ }>;
126
+ dispose(): void;
127
+ }
128
+ /** A device passed with the actions it lacks; build it with {@link ActionSet.lacking}. */
129
+ export interface Lacking<D extends Unit | Pads, Missing extends string> {
130
+ readonly _tag: "Lacking";
131
+ readonly device: D;
132
+ readonly lacking: ReadonlyArray<Missing>;
133
+ }
134
+ /** One entry of a merge before `lacking`: a device or a group of devices. */
135
+ type Unit = Device | Group<Readonly<Record<string, Device>>>;
136
+ type Entry = Unit | Lacking<Unit, string>;
137
+ /** One entry of a player pool: a device, or every pad, maybe with the actions it lacks. */
138
+ type PoolEntry = Device | Pads | Lacking<Device | Pads, string>;
139
+ /** The button actions a device, or the devices of a group, bind. */
140
+ type ActionsOf<D> = D extends Group<infer M> ? {
141
+ [K in keyof M]: ActionsOf<M[K]>;
142
+ }[keyof M] : D extends {
143
+ readonly _tag: "Pads";
144
+ snapshot(): ReadonlyArray<{
145
+ readonly actions: infer Actions;
146
+ }>;
147
+ } ? keyof Actions & string : D extends {
148
+ snapshot(): {
149
+ readonly actions: infer Actions;
150
+ };
151
+ } ? keyof Actions & string : never;
152
+ /** The vector actions a device, or the devices of a group, bind. */
153
+ type VectorsOf<D> = D extends Group<infer M> ? {
154
+ [K in keyof M]: VectorsOf<M[K]>;
155
+ }[keyof M] : D extends {
156
+ readonly _tag: "Pads";
157
+ snapshot(): ReadonlyArray<{
158
+ readonly vectors: infer Vectors;
159
+ }>;
160
+ } ? keyof Vectors & string : D extends {
161
+ snapshot(): {
162
+ readonly vectors: infer Vectors;
163
+ };
164
+ } ? keyof Vectors & string : never;
165
+ /** Every action a device binds, of either kind. */
166
+ type BoundOf<D> = ActionsOf<D> | VectorsOf<D>;
167
+ /**
168
+ * Why a device's bindings do not fit the declared buttons `A` and vectors
169
+ * `V` (an action of the wrong kind, or one not declared), or `never`.
170
+ */
171
+ type KindIssue<A extends string, V extends string, Name extends string, D> = [ActionsOf<D> & V] extends [never] ? [VectorsOf<D> & A] extends [never] ? [Exclude<BoundOf<D>, A | V>] extends [never] ? never : `${Name} binds ${Exclude<BoundOf<D>, A | V>}, which is not one of the actions` : `${Name} binds ${VectorsOf<D> & A} as a vector, but it is a button action: bind it to keys or buttons` : `${Name} binds ${ActionsOf<D> & V} as a button, but it is a vector action: bind it with vector(...)`;
172
+ type DeviceOf<E> = E extends Lacking<infer D, string> ? D : E;
173
+ type SnapshotOf<D> = D extends {
174
+ snapshot(): infer S;
175
+ } ? S : never;
176
+ /** The devices one merge entry contributes, by name: a group's members, or the device under the entry's name. */
177
+ type MembersOf<K, U> = U extends Group<infer M> ? M : {
178
+ readonly [P in K & string]: U;
179
+ };
180
+ /** Every device name of a merge, with groups flattened. */
181
+ type DeviceNames<R> = {
182
+ [K in keyof R]: keyof MembersOf<K, DeviceOf<R[K]>>;
183
+ }[keyof R] & string;
184
+ /** The device a merge names `N`, wherever it is. */
185
+ type DeviceNamed<R, N> = {
186
+ [K in keyof R]: MembersOf<K, DeviceOf<R[K]>> extends infer M ? N extends keyof M ? M[N] : never : never;
187
+ }[keyof R];
188
+ /** Each device's full snapshot, by device name (group members under their own names). */
189
+ type DevicesOf<R> = {
190
+ readonly [N in DeviceNames<R>]: SnapshotOf<DeviceNamed<R, N>>;
191
+ };
192
+ /** The `rebind` of each merged device that has one, by device name. */
193
+ type RebindOf<R> = {
194
+ readonly [N in DeviceNames<R> as DeviceNamed<R, N> extends {
195
+ readonly rebind: unknown;
196
+ } ? N : never]: DeviceNamed<R, N> extends {
197
+ readonly rebind: infer Rebind;
198
+ } ? Rebind : never;
199
+ };
200
+ /** The names one entry takes: its own, and its members' for a group. */
201
+ type NamesOf<K, U> = U extends Group<infer M> ? K | keyof M : K;
202
+ /** A device or group name the merge uses twice. */
203
+ type Repeated<R> = {
204
+ [K in keyof R]: (NamesOf<K, DeviceOf<R[K]>> & {
205
+ [J in Exclude<keyof R, K>]: NamesOf<J, DeviceOf<R[J]>>;
206
+ }[Exclude<keyof R, K>]) | (DeviceOf<R[K]> extends Group<infer M> ? K & keyof M : never);
207
+ }[keyof R] & string;
208
+ /** Rejects a merge that uses a name twice: `devices`, `rebind`, and `lastUsed` need one meaning per name. */
209
+ type UniqueNames<R> = [Repeated<R>] extends [never] ? unknown : `the name ${Repeated<R>} is used twice: give every device and group its own name`;
210
+ /** A group member when it is a plain device binding only declared actions, otherwise an error naming the fix. */
211
+ type CheckMember<A extends string, V extends string, Name extends string, D> = D extends Lacking<Unit, string> ? "lacking applies to the whole group: pass lacking(group(...), [...])" : D extends Group<unknown> ? "groups do not nest: list every device in one group" : [KindIssue<A, V, Name, D>] extends [never] ? D : KindIssue<A, V, Name, D>;
212
+ /** Rejects a group of fewer than two devices. */
213
+ type TwoOrMore<M> = [keyof M] extends [never] ? "a group needs at least two devices" : {
214
+ [K in keyof M]: [Exclude<keyof M, K>] extends [never] ? K : never;
215
+ }[keyof M] extends infer Alone extends string ? [Alone] extends [never] ? unknown : `a group needs at least two devices: pass ${Alone} to merge directly` : unknown;
216
+ /** The entry when it covers exactly the declared actions, otherwise an error naming the device and the fix. */
217
+ type CheckEntry<A extends string, V extends string, Name extends string, E> = E extends Lacking<infer D, infer Missing> ? [KindIssue<A, V, Name, D>] extends [never] ? [Exclude<A | V, BoundOf<D> | Missing>] extends [never] ? E : `${Name} has no action ${Exclude<A | V, BoundOf<D> | Missing>}: bind it on ${Name}, or list it in its lacking(...)` : KindIssue<A, V, Name, D> : [KindIssue<A, V, Name, E>] extends [never] ? [Exclude<A | V, BoundOf<E>>] extends [never] ? E : `${Name} has no action ${Exclude<A | V, BoundOf<E>>}: bind it on ${Name}, or pass lacking(${Name}, [...]) from the action set` : KindIssue<A, V, Name, E>;
218
+ /** The actions some merged device binds. */
219
+ type BoundBy<R> = {
220
+ [K in keyof R]: BoundOf<DeviceOf<R[K]>>;
221
+ }[keyof R];
222
+ /** Rejects a merge where no device binds an action. */
223
+ type EveryActionBound<A extends string, R> = [Exclude<A, BoundBy<R>>] extends [never] ? unknown : `no device binds ${Exclude<A, BoundBy<R>>}: bind it on one device, or remove it from Controls.actions`;
224
+ /**
225
+ * The merged input. `actions` merges every device; `devices` holds each
226
+ * device's own snapshot under the name it was merged under.
227
+ */
228
+ export interface Snapshot<A extends string, V extends string, Devices, Used extends string = keyof Devices & string> {
229
+ readonly actions: {
230
+ readonly [K in A]: ActionState;
231
+ };
232
+ /**
233
+ * Each vector action (movement) as a direction with length at most 1, `y`
234
+ * growing down: the longest any device reports, so devices never add up
235
+ * past 1 or cancel each other.
236
+ */
237
+ readonly vectors: {
238
+ readonly [K in V]: Vector;
239
+ };
240
+ /**
241
+ * Whether the last used device binds each action: `false` for the actions
242
+ * it was merged as lacking, so the game can offer them another way (an
243
+ * on-screen pause button). Every action is available before any device is
244
+ * used.
245
+ */
246
+ readonly available: {
247
+ readonly [K in A | V]: boolean;
248
+ };
249
+ readonly devices: Devices;
250
+ /**
251
+ * The device (or group) that reported input most recently, or `null`
252
+ * before any did. When several report input in one window it stays on the
253
+ * current one if it is among them, otherwise it takes the first in merge
254
+ * order.
255
+ */
256
+ readonly lastUsed: Used | null;
257
+ }
258
+ /** The merged source, for `InputCapture.system(...)`. */
259
+ export interface Controls<A extends string, V extends string, R> extends System.Consumes<"snapshot"> {
260
+ /** Takes every device's snapshot, starting a new press window on each, and merges them. */
261
+ snapshot(): Snapshot<A, V, DevicesOf<R>, keyof R & string>;
262
+ /** Each merged device's `rebind`, for a settings screen; devices without one (touch) are not listed. */
263
+ readonly rebind: RebindOf<R>;
264
+ /** Disposes every merged device. */
265
+ dispose(): void;
266
+ }
267
+ /**
268
+ * The game's actions, declared once. Every merge, gap, and idle snapshot is
269
+ * built from it, so action names are checked against one list.
270
+ */
271
+ export interface ActionSet<A extends string, V extends string = never> {
272
+ /** The declared button actions, in order, for listing them on a settings screen. */
273
+ readonly buttons: ReadonlyArray<A>;
274
+ /** The declared vector actions, in order. */
275
+ readonly vectors: ReadonlyArray<V>;
276
+ /**
277
+ * Marks the actions `device` cannot report, for {@link merge}. List
278
+ * exactly the declared actions it does not bind: autocomplete offers them,
279
+ * and a missing or extra name does not compile.
280
+ */
281
+ lacking<D extends Unit | Pads, const Missing extends ReadonlyArray<Exclude<A | V, BoundOf<D>>>>(device: D & ([
282
+ KindIssue<A, V, "This device", D>
283
+ ] extends [never] ? [Exclude<A | V, BoundOf<D>>] extends [never] ? "This device binds every action: pass it to merge without lacking" : unknown : KindIssue<A, V, "This device", D>), lacking: Missing & ([Exclude<A | V, BoundOf<D> | Missing[number]>] extends [never] ? unknown : `This device also lacks ${Exclude<A | V, BoundOf<D> | Missing[number]>}: bind it, or list it here`)): Lacking<D, Missing[number]>;
284
+ /**
285
+ * Groups devices the player uses together (keyboard and mouse), for
286
+ * {@link merge}: the group binds what its devices bind between them, and
287
+ * `lastUsed` and `available` follow it as a whole, so walking with the
288
+ * keyboard while aiming with the mouse is one device in use. A group needs
289
+ * two or more plain devices, each binding only declared actions; pass the
290
+ * group to {@link lacking} for the actions none of them binds.
291
+ */
292
+ group<const M extends Readonly<Record<string, Entry | Group<unknown>>>>(members: {
293
+ readonly [K in keyof M]: CheckMember<A, V, K & string, M[K]>;
294
+ } & TwoOrMore<M>): Group<M>;
295
+ /**
296
+ * Merges `devices` into one source. Each entry (a device or a
297
+ * {@link group}) binds every declared action, or is passed through
298
+ * {@link lacking}; every action is bound by at least one device. Entry
299
+ * names key `lastUsed`; device names (a group's members under their own)
300
+ * key `devices` and `rebind`.
301
+ */
302
+ merge<const R extends Readonly<Record<string, Entry>>>(devices: {
303
+ readonly [K in keyof R]: CheckEntry<A, V, K & string, R[K]>;
304
+ } & EveryActionBound<A | V, R> & UniqueNames<R>): Controls<A, V, R>;
305
+ /**
306
+ * A merged snapshot with nothing held and no device used yet, for
307
+ * initializing the input resource: pass each device's idle snapshot
308
+ * (`Keyboard.idle(...)`, ...) under its device name.
309
+ */
310
+ idle<const Devices extends Readonly<Record<string, DeviceSnapshot<string, string>>>>(devices: Devices): Snapshot<A, V, Devices, never>;
311
+ }
312
+ /** Rejects a name declared both as a button and as a vector. */
313
+ type DistinctKinds<A extends string, V extends string> = [A & V] extends [never] ? unknown : `${A & V} is declared as both a button and a vector action: give it one kind`;
314
+ /**
315
+ * Declares the game's actions: `buttons` (pressed and held: jump, pause)
316
+ * and `vectors` (a direction with length at most 1: move), each bound on
317
+ * every device. Build the merged source with the returned set's `merge`.
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * const Actions = Controls.actions({ buttons: ["jump", "pause"], vectors: ["move"] })
322
+ * ```
323
+ */
324
+ export declare const actions: <const Buttons extends readonly [string, ...Array<string>], const Vectors extends ReadonlyArray<string> = readonly []>(names: {
325
+ readonly buttons: Buttons;
326
+ readonly vectors?: Vectors;
327
+ } & DistinctKinds<Buttons[number], Vectors[number]>) => ActionSet<Buttons[number], Vectors[number]>;
328
+ /** A slot's input without the device: what every player snapshot has, joined or not. */
329
+ export interface SlotInput<A extends string, V extends string> {
330
+ readonly actions: {
331
+ readonly [K in A]: ActionState;
332
+ };
333
+ readonly vectors: {
334
+ readonly [K in V]: Vector;
335
+ };
336
+ /** Whether the slot's device binds each action: every one while the slot is empty. */
337
+ readonly available: {
338
+ readonly [K in A | V]: boolean;
339
+ };
340
+ }
341
+ /** A slot nobody holds: its input is idle. */
342
+ export interface EmptySlot<A extends string, V extends string> extends SlotInput<A, V> {
343
+ readonly joined: false;
344
+ readonly device: null;
345
+ readonly connected: false;
346
+ /** `"left"` in the snapshot right after its player left, otherwise `null`. */
347
+ readonly event: "left" | null;
348
+ readonly devices: {
349
+ readonly [K in never]: never;
350
+ };
351
+ }
352
+ /** A slot held by a player, by the device they joined with. Check `device` to read that device's own snapshot. */
353
+ export type JoinedSlot<A extends string, V extends string, Devices> = {
354
+ readonly [D in keyof Devices & string]: SlotInput<A, V> & {
355
+ readonly joined: true;
356
+ readonly device: D;
357
+ /** `false` while the player's pad is unplugged: the slot is kept, and the same pad coming back returns to it. */
358
+ readonly connected: boolean;
359
+ /** `"joined"` in the snapshot where the player joined (their input is idle there, so the press that joins does nothing else), otherwise `null`. */
360
+ readonly event: "joined" | null;
361
+ readonly devices: {
362
+ readonly [P in D]: Devices[D];
363
+ };
364
+ };
365
+ }[keyof Devices & string];
366
+ /** One player slot: empty, or held through one of the pool's devices. */
367
+ export type Slot<A extends string, V extends string, Devices> = EmptySlot<A, V> | JoinedSlot<A, V, Devices>;
368
+ /** Every slot by name, and whether joining is open. */
369
+ export type PlayersSnapshot<A extends string, V extends string, S extends string, Devices> = {
370
+ readonly joining: boolean;
371
+ } & {
372
+ readonly [K in S]: Slot<A, V, Devices>;
373
+ };
374
+ /** What one pool entry hands a player: a pad's snapshot for `Gamepad.pads`, the device's otherwise. */
375
+ type UnitSnapshotOf<D> = D extends {
376
+ readonly _tag: "Pads";
377
+ snapshot(): ReadonlyArray<infer S>;
378
+ } ? S : SnapshotOf<D>;
379
+ type PoolDevicesOf<R> = {
380
+ readonly [K in keyof R]: UnitSnapshotOf<DeviceOf<R[K]>>;
381
+ };
382
+ /** The entry when it fits the actions and binds the join action, otherwise an error naming the fix. */
383
+ type CheckPoolEntry<A extends string, V extends string, J extends string, Name extends string, E> = E extends Lacking<infer _D, infer Missing> ? [Missing & J] extends [never] ? CheckEntry<A, V, Name, E> : `${Name} lacks ${Missing & J}: every pooled device binds ${J}, or no one can join with it` : CheckEntry<A, V, Name, E>;
384
+ /**
385
+ * A pooled device to seat a player on: its pool name, or `{ [name]: index }`
386
+ * for one pad of a `Gamepad.pads` entry (its place in the pad list, from 0).
387
+ */
388
+ export type SeatDevice<R> = {
389
+ readonly [K in keyof R & string]: DeviceOf<R[K]> extends Pads ? never : K;
390
+ }[keyof R & string] | {
391
+ readonly [K in keyof R & string]: DeviceOf<R[K]> extends Pads ? {
392
+ readonly [P in K]: number;
393
+ } : never;
394
+ }[keyof R & string];
395
+ /**
396
+ * Whether {@link Pool.seat} seated the player: `slot-taken` when someone
397
+ * holds the slot, `device-taken` when another slot holds the device, and
398
+ * `no-such-pad` when the index is not a place in the pad list.
399
+ */
400
+ export type SeatResult = {
401
+ readonly ok: true;
402
+ } | {
403
+ readonly ok: false;
404
+ readonly reason: "slot-taken" | "device-taken" | "no-such-pad";
405
+ };
406
+ /** The pooled input source, for `InputCapture.system(...)`; systems call the rest through a plain service. */
407
+ export interface Pool<A extends string, V extends string, S extends string, R> extends System.Consumes<"snapshot"> {
408
+ /** Takes every device's snapshot, applies joins, leaves, and reconnects, and reports every slot. */
409
+ snapshot(): PlayersSnapshot<A, V, S, PoolDevicesOf<R>>;
410
+ /** Opens or closes joining: open on a join screen or for drop-in play, closed during a round. Closed at the start. */
411
+ setJoining(open: boolean): void;
412
+ /**
413
+ * Gives `slot` to `device` without a press, joining or not: a player who
414
+ * is in from the start (call it from a setup system), or one picked on a
415
+ * menu. The next snapshot reports `event: "joined"`, as a join does. A
416
+ * pad not plugged in yet holds the slot with `connected: false`.
417
+ */
418
+ seat(slot: S, device: SeatDevice<R>): SeatResult;
419
+ /** Frees a slot (a pause menu's "Leave", "Continue without P2"); its device goes back to the pool. */
420
+ leave(slot: S): void;
421
+ /** Each pooled device's `rebind`, by its pool name (`Gamepad.pads` rebinds every pad). */
422
+ readonly rebind: {
423
+ readonly [K in keyof R as DeviceOf<R[K]> extends {
424
+ readonly rebind: unknown;
425
+ } ? K : never]: DeviceOf<R[K]> extends {
426
+ readonly rebind: infer Rebind;
427
+ } ? Rebind : never;
428
+ };
429
+ /** Disposes every pooled device. */
430
+ dispose(): void;
431
+ }
432
+ /** The players of a local multiplayer game, declared once. */
433
+ export interface PlayerSet<A extends string, V extends string, S extends string, J extends string> {
434
+ /** The slots, in join order. */
435
+ readonly slots: ReadonlyArray<S>;
436
+ /**
437
+ * Pools `devices`: none belongs to a player until someone presses join on
438
+ * it. Each binds every action (or is passed through `lacking`), and the
439
+ * join action always: `Gamepad.pads(...)` lets every pad join on its own,
440
+ * and two `Keyboard.actions` over different keys are two keyboard players.
441
+ */
442
+ pool<const R extends Readonly<Record<string, PoolEntry>>>(devices: {
443
+ readonly [K in keyof R]: CheckPoolEntry<A, V, J, K & string, R[K]>;
444
+ }): Pool<A, V, S, R>;
445
+ /**
446
+ * Every slot empty and joining closed, for initializing the input
447
+ * resource: pass each device's idle snapshot (`Keyboard.idle(...)`,
448
+ * `Gamepad.idle(...)` for pads) under its pool name.
449
+ */
450
+ idle<const Devices extends Readonly<Record<string, DeviceSnapshot<string, string>>>>(devices: Devices): PlayersSnapshot<A, V, S, Devices>;
451
+ }
452
+ /** Rejects a slot named like the snapshot's own field. */
453
+ type SlotNames<S extends string> = [S & "joining"] extends [never] ? unknown : "joining is the snapshot's own field: name the slot something else";
454
+ /** Rejects a button action named like `join: "any"`, which would read as any button. */
455
+ type NoAnyAction<A extends string> = [A & "any"] extends [never] ? unknown : "an action named any reads as join: \"any\" (any button): rename the action";
456
+ /**
457
+ * Declares the players of a local multiplayer game over `actions`: the
458
+ * `slots` (the most players at once, in join order), the button action
459
+ * that joins (`"any"` for any button action but `leave`, or any vector
460
+ * moved), and the optional one that leaves while joining is open. Build the
461
+ * input source with the returned set's `pool`. For one player on several
462
+ * devices, use the action set's `merge` instead.
463
+ *
464
+ * Pressing join on a device nobody holds gives it the first free slot; that
465
+ * snapshot reports `event: "joined"` with the player's input idle, so the
466
+ * press does nothing else. A slot keeps its name when another empties. A
467
+ * pad that disconnects keeps its slot (`connected: false`) and returns to
468
+ * it when it comes back (two pads of one model share an id, so they are
469
+ * told apart by their place in the pad list: each returns to the slot
470
+ * holding that place). Leaving by button works only while joining is
471
+ * open; otherwise `leave` is an ordinary action (a pause menu), and
472
+ * `pool.leave(slot)` frees the slot. `pool.seat(slot, device)` puts a
473
+ * player in without a press: one who is in from the start.
474
+ *
475
+ * @example
476
+ * ```ts
477
+ * const Players = Controls.players(Actions, { slots: ["p1", "p2"], join: "join", leave: "back" })
478
+ * const makePlayers = (devices: {
479
+ * readonly left: Keyboard.Actions<typeof leftKeys>
480
+ * readonly right: Keyboard.Actions<typeof rightKeys>
481
+ * readonly pads: Gamepad.Pads<typeof padBindings>
482
+ * }) => Players.pool(devices)
483
+ *
484
+ * // In a system:
485
+ * const input = resources.input.get()
486
+ * if (input.p2.event === "joined") spawnSecondPlayer()
487
+ * const move = input.p2.vectors.move
488
+ * ```
489
+ */
490
+ export declare const players: <A extends string, V extends string, const S extends readonly [string, ...Array<string>], const J extends A | "any", const L extends A = never>(actions: ActionSet<A, V> & NoAnyAction<A>, options: {
491
+ readonly slots: S & SlotNames<S[number]>;
492
+ readonly join: J;
493
+ readonly leave?: L;
494
+ }) => PlayerSet<A, V, S[number], Exclude<J, "any">>;
495
+ //# sourceMappingURL=Controls.d.ts.map