@benev/tact 0.1.0-2 → 0.1.0-3

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 (96) hide show
  1. package/README.md +120 -56
  2. package/package.json +3 -2
  3. package/s/core/{port → bindings}/action.ts +1 -1
  4. package/s/core/bindings/parts/defaults.ts +29 -0
  5. package/s/core/{port/resolution → bindings/parts}/lens-algo.ts +6 -7
  6. package/s/core/bindings/resolver.ts +96 -0
  7. package/s/core/bindings/types.ts +43 -42
  8. package/s/core/controllers/standard/gamepad.ts +1 -5
  9. package/s/core/controllers/standard/keyboard.ts +0 -3
  10. package/s/core/controllers/standard/pointer.ts +3 -9
  11. package/s/core/deck/deck.ts +40 -0
  12. package/s/core/deck/parts/bindings-depot.ts +24 -0
  13. package/s/core/deck/parts/local-storage-kv.ts +7 -0
  14. package/s/core/hub/bindings.ts +16 -19
  15. package/s/core/hub/hub.ts +2 -2
  16. package/s/core/hub/types.ts +14 -0
  17. package/s/core/index.ts +5 -3
  18. package/s/core/port/port.ts +1 -1
  19. package/s/core/testing/testing.ts +5 -5
  20. package/x/core/{port → bindings}/action.js +1 -1
  21. package/x/core/bindings/action.js.map +1 -0
  22. package/x/core/bindings/parts/defaults.d.ts +3 -0
  23. package/x/core/bindings/parts/defaults.js +23 -0
  24. package/x/core/bindings/parts/defaults.js.map +1 -0
  25. package/x/core/bindings/parts/is-pressed.js.map +1 -0
  26. package/x/core/bindings/parts/lens-algo.d.ts +2 -0
  27. package/x/core/{port/resolution → bindings/parts}/lens-algo.js +4 -4
  28. package/x/core/bindings/parts/lens-algo.js.map +1 -0
  29. package/x/core/{port/resolution → bindings}/resolver.d.ts +2 -3
  30. package/x/core/bindings/resolver.js +87 -0
  31. package/x/core/bindings/resolver.js.map +1 -0
  32. package/x/core/bindings/types.d.ts +29 -35
  33. package/x/core/bindings/types.js +0 -1
  34. package/x/core/bindings/types.js.map +1 -1
  35. package/x/core/controllers/standard/gamepad.d.ts +1 -2
  36. package/x/core/controllers/standard/gamepad.js +0 -4
  37. package/x/core/controllers/standard/gamepad.js.map +1 -1
  38. package/x/core/controllers/standard/keyboard.js +0 -3
  39. package/x/core/controllers/standard/keyboard.js.map +1 -1
  40. package/x/core/controllers/standard/pointer.js +3 -8
  41. package/x/core/controllers/standard/pointer.js.map +1 -1
  42. package/x/core/deck/deck.d.ts +16 -0
  43. package/x/core/deck/deck.js +31 -0
  44. package/x/core/deck/deck.js.map +1 -0
  45. package/x/core/deck/parts/bindings-depot.d.ts +9 -0
  46. package/x/core/deck/parts/bindings-depot.js +19 -0
  47. package/x/core/deck/parts/bindings-depot.js.map +1 -0
  48. package/x/core/deck/parts/local-storage-kv.d.ts +2 -0
  49. package/x/core/deck/parts/local-storage-kv.js +5 -0
  50. package/x/core/deck/parts/local-storage-kv.js.map +1 -0
  51. package/x/core/hub/bindings.d.ts +2 -2
  52. package/x/core/hub/bindings.js +15 -18
  53. package/x/core/hub/bindings.js.map +1 -1
  54. package/x/core/hub/hub.d.ts +3 -4
  55. package/x/core/hub/hub.js +1 -1
  56. package/x/core/hub/hub.js.map +1 -1
  57. package/x/core/hub/types.d.ts +9 -0
  58. package/x/core/hub/types.js +2 -0
  59. package/x/core/hub/types.js.map +1 -0
  60. package/x/core/index.d.ts +4 -3
  61. package/x/core/index.js +4 -3
  62. package/x/core/index.js.map +1 -1
  63. package/x/core/port/port.d.ts +1 -1
  64. package/x/core/port/port.js +1 -1
  65. package/x/core/port/port.js.map +1 -1
  66. package/x/core/testing/testing.d.ts +12 -32
  67. package/x/core/testing/testing.js +5 -5
  68. package/x/core/testing/testing.js.map +1 -1
  69. package/x/index.html +2 -2
  70. package/s/core/controllers/utils/modprefix.ts +0 -16
  71. package/s/core/port/resolution/defaults.ts +0 -30
  72. package/s/core/port/resolution/resolver.ts +0 -77
  73. package/s/core/port/resolution/types.ts +0 -9
  74. package/s/core/port/types.ts +0 -10
  75. package/x/core/controllers/utils/modprefix.d.ts +0 -1
  76. package/x/core/controllers/utils/modprefix.js +0 -16
  77. package/x/core/controllers/utils/modprefix.js.map +0 -1
  78. package/x/core/port/action.js.map +0 -1
  79. package/x/core/port/resolution/defaults.d.ts +0 -4
  80. package/x/core/port/resolution/defaults.js +0 -23
  81. package/x/core/port/resolution/defaults.js.map +0 -1
  82. package/x/core/port/resolution/lens-algo.d.ts +0 -2
  83. package/x/core/port/resolution/lens-algo.js.map +0 -1
  84. package/x/core/port/resolution/resolver.js +0 -64
  85. package/x/core/port/resolution/resolver.js.map +0 -1
  86. package/x/core/port/resolution/types.d.ts +0 -6
  87. package/x/core/port/resolution/types.js +0 -2
  88. package/x/core/port/resolution/types.js.map +0 -1
  89. package/x/core/port/types.d.ts +0 -7
  90. package/x/core/port/types.js +0 -2
  91. package/x/core/port/types.js.map +0 -1
  92. package/x/core/port/utils/is-pressed.js.map +0 -1
  93. /package/s/core/{port/utils → bindings/parts}/is-pressed.ts +0 -0
  94. /package/x/core/{port → bindings}/action.d.ts +0 -0
  95. /package/x/core/{port/utils → bindings/parts}/is-pressed.d.ts +0 -0
  96. /package/x/core/{port/utils → bindings/parts}/is-pressed.js +0 -0
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
 
2
- # @benev/tact
3
- > web game input library, from keypress to couch co-op
2
+ # 🎮 @benev/tact
3
+ > *web game input library, from keypress to couch co-op*
4
4
 
5
5
  ```ts
6
6
  npm install @benev/tact
7
7
  ```
8
8
 
9
- - 🛹 **deck** user input coordinator with bindings persistence
9
+ - 🛹 **deck** full setup with localstorate persistence
10
10
  - 🎮 **controllers** produce user input samples
11
11
  - 🧩 **bindings** describe how actions interpret samples
12
12
  - 🔌 **port** updates actions by interpreting samples
@@ -18,38 +18,36 @@ npm install @benev/tact
18
18
  <br/><br/>
19
19
 
20
20
  ## 🍋 tact deck
21
- > *user input coordinator with bindings persistence*
21
+ > *the full setup*
22
22
 
23
- the deck is the heart of tact.. it ties all the important parts together..
23
+ the deck is the heart of tact, tying all the pieces together and putting a bow in it for you.
24
24
 
25
25
  ### 🛹 deck setup
26
26
  - **import stuff from tact**
27
27
  ```ts
28
28
  import * as tact from "@benev/tact"
29
29
  ```
30
- - **setup your deck**
30
+ - **setup your deck, and your game's bindings**
31
31
  ```ts
32
- const deck = new tact.Deck({
33
- ports: 4,
34
- kv: tact.Deck.localStorageKv(),
35
- defaultBindings: tact.Hub.bindings({
32
+ const deck = await tact.Deck.load({
33
+
34
+ // how many players ports are possible? 1 is fine..
35
+ portCount: 4,
36
+
37
+ // where to store the user-customized bindings
38
+ kv: tact.localStorageKv(),
39
+
40
+ // default archetypal bindings for your game
41
+ bindings: {
42
+ ...tact.hubBindings(),
36
43
  walking: {
37
- forward: [
38
- {lenses: [{code: "KeyW"}]},
39
- {lenses: [{code: "gamepad.stick.left.up"}]},
40
- ],
41
- jump: [
42
- {lenses: [{code: "Space"}]},
43
- {lenses: [{code: "gamepad.a"}]},
44
- ],
44
+ forward: "KeyW",
45
+ jump: "Space",
45
46
  },
46
47
  gunning: {
47
- shoot: [
48
- {lenses: [{code: "pointer.button.left"}]},
49
- {lenses: [{code: "gamepad.trigger.right"}]},
50
- ],
48
+ shoot: ["or", "pointer.button.left", "gamepad.trigger.right"],
51
49
  },
52
- }),
50
+ },
53
51
  })
54
52
  ```
55
53
 
@@ -83,10 +81,10 @@ the deck is the heart of tact.. it ties all the important parts together..
83
81
  const [p1, p2, p3, p4] = deck.hub.poll()
84
82
 
85
83
  // check if the first player is pressing "forward" action
86
- p1.actions.walking.forward.pressed // true
84
+ p1.walking.forward.pressed // true
87
85
 
88
86
  // check how hard the second player is pulling that trigger
89
- p2.actions.gunning.shoot.value // 0.123
87
+ p2.gunning.shoot.value // 0.123
90
88
  })
91
89
  ```
92
90
 
@@ -95,7 +93,7 @@ the deck is the heart of tact.. it ties all the important parts together..
95
93
  <br/><br/>
96
94
 
97
95
  ## 🍋 tact controllers
98
- > *produces user input "samples"*
96
+ > *sources of user input "samples"*
99
97
 
100
98
  ### 🎮 polling is good, actually
101
99
  - tact operates on the basis of *polling*
@@ -117,7 +115,7 @@ the deck is the heart of tact.. it ties all the important parts together..
117
115
  // ["Space", 0]
118
116
  // ]
119
117
  ```
120
- - dispose when you're done with it
118
+ - some controllers have disposers to call when you're done with them
121
119
  ```ts
122
120
  keyboard.dispose()
123
121
  ```
@@ -135,17 +133,6 @@ the deck is the heart of tact.. it ties all the important parts together..
135
133
  - sometimes we use numbers greater then `1`, like for dots of pointer movement like in `pointer.move.up`
136
134
  - don't worry about sensitivity, deadzones, values like `0.00001` — actions will account for all that using bindings later on
137
135
 
138
- ### 🎮 sample code modprefixes
139
- - here at tact, we have this nifty `modprefix` convention
140
- - consider a keycode like `KeyA`
141
- - `ctrl-KeyA` means the "ctrl" modifier was held
142
- - `alt-KeyA` means the "alt" modifier was held
143
- - `meta-KeyA` means the "meta" modifier was held (command or windows keys)
144
- - `shift-KeyA` means the "shift" modifier was held
145
- - `ctrl-alt-meta-shift-KeyA` they can stack, but always in this order (the word `cams` helps remind me the valid order)
146
- - `x-KeyA` means *no* modifier was held (exclusive)
147
- - `KeyA` doesn't care if any modifiers were held or not
148
-
149
136
  ### 🎮 sample code reference
150
137
  - **KeyboardController**
151
138
  - any [standard keycode](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values)
@@ -153,19 +140,14 @@ the deck is the heart of tact.. it ties all the important parts together..
153
140
  - `Space`
154
141
  - `Digit2`
155
142
  - etc
156
- - plus the modprefixes
157
- - `ctrl-KeyA`
158
- - `alt-shift-Space`
159
- - `x-Digit2`
160
- - etc
161
143
  - **PointerController**
162
- - mouse buttons (plus modprefixes)
144
+ - mouse buttons
163
145
  - `pointer.button.left`
164
146
  - `pointer.button.right`
165
147
  - `pointer.button.middle`
166
148
  - `pointer.button.4`
167
149
  - `pointer.button.5`
168
- - mouse wheel (plus modprefixes)
150
+ - mouse wheel
169
151
  - `pointer.wheel.up`
170
152
  - `pointer.wheel.down`
171
153
  - `pointer.wheel.left`
@@ -212,23 +194,93 @@ the deck is the heart of tact.. it ties all the important parts together..
212
194
  > *keybindings! they describe how actions interpret samples*
213
195
 
214
196
  ### 🧩 bindings example
215
- - let's start with a small example:
197
+ - **let's start with a small example:**
216
198
  ```ts
217
199
  const bindings = tact.asBindings({
218
200
  walking: {
219
- forward: [{lenses: [{code: "KeyW"}]}],
220
- jump: [{lenses: [{code: "Space"}]}],
201
+ forward: "KeyW",
202
+ jump: "Space",
221
203
  },
222
204
  gunning: {
223
- shoot: [{lenses: [{code: "pointer.button.left"}]}],
205
+ shoot: ["or", "pointer.button.left", "gamepad.trigger.right"],
224
206
  },
225
207
  })
226
208
  ```
227
209
  - `walking` and `gunning` are **modes**
228
210
  - `forward`, `jump`, and `shoot` are **actions**
229
- - data like `[{lenses: [{code: "KeyW"}]}]` are the rules for triggering the action.
230
- - 🚨 TODO okay these rules are complex and i'm gonna rework them soon
231
- - whole modes can be enabled or disabled
211
+ - note that whole modes can be enabled or disabled during gameplay
212
+
213
+ ### 🧩 bindings are a lispy domain-specific-language
214
+ - **you can do complex stuff**
215
+ ```ts
216
+ ["or",
217
+ "KeyQ",
218
+ ["and",
219
+ "KeyA",
220
+ "KeyD",
221
+ ["not", "KeyS"],
222
+ ],
223
+ ]
224
+ ```
225
+ - press Q, or
226
+ - press A + D, while not pressing S
227
+ - **you can get really weird**
228
+ ```ts
229
+ ["cond",
230
+ ["code", "gamepad.trigger.right", {deadzone: 0.5, timing: ["tap"]}],
231
+ ["and", "gamepad.bumper.left", ["not", "gamepad.trigger.left"]],
232
+ ]
233
+ ```
234
+ - hold LB and tap RT halfway while not holding LT
235
+
236
+ ### 🧩 bindings atom reference
237
+ - **string** — strings are interpreted as "code" atoms with default settings
238
+ - **"code"** — allows you to customize the settings
239
+ ```ts
240
+ ["code", "KeyA", {
241
+ scale: 1,
242
+ invert: false,
243
+ deadzone: 0.2,
244
+ timing: ["direct"],
245
+ }]
246
+ ```
247
+ - defaults shown
248
+ - `scale` is sensitivity, the value gets multiplied by this
249
+ - `invert` will invert a value by subtracting it from 1
250
+ - `deadzone` ignores values below the threshold (and remaps to preserve the range)
251
+ - `timing` lets you specify special timing considerations
252
+ - `["direct"]` ignores timing considerations
253
+ - `["tap", 250]` only fires for taps under 250ms
254
+ - `["hold", 250]` only fires for holds over 250ms
255
+ - **"or"** — resolves to the maximum value
256
+ ```ts
257
+ ["or", "KeyA", "KeyB", "KeyC"]
258
+ ```
259
+ - **"and"** — resolves to the minimum value
260
+ ```ts
261
+ ["and", "KeyA", "KeyB", "KeyC"]
262
+ ```
263
+ - **"not"** — resolves to the opposite effect
264
+ ```ts
265
+ ["not", "KeyA"]
266
+ ```
267
+ - **"cond"** — conditional situation (example for modifiers shown)
268
+ ```ts
269
+ ["cond", "KeyA", ["and",
270
+ ["or", "ControlLeft", "ControlRight"],
271
+ ["not", ["or", "AltLeft", "AltRight"]],
272
+ ["not", ["or", "MetaLeft", "MetaRight"]],
273
+ ["not", ["or", "ShiftLeft", "ShiftRight"]],
274
+ ]]
275
+ ```
276
+ - KeyA is the value that gets used
277
+ - but only if the following condition passes
278
+ - **"mods"** — macro for modifiers
279
+ ```ts
280
+ ["mods", "KeyA", {ctrl: true}]
281
+ ```
282
+ - equivalent to the "cond" example above
283
+ - `ctrl`, `alt`, `meta`, `shift` are available
232
284
 
233
285
 
234
286
 
@@ -237,6 +289,8 @@ the deck is the heart of tact.. it ties all the important parts together..
237
289
  ## 🍋 tact port
238
290
  > *polling gives you "actions"*
239
291
 
292
+ a port represents a single playable port, and you poll it each frame to resolve actions for you to read.
293
+
240
294
  ### 🔌 port basics
241
295
  - **make a port**
242
296
  ```ts
@@ -261,6 +315,13 @@ the deck is the heart of tact.. it ties all the important parts together..
261
315
  ```ts
262
316
  port.bindings = freshBindings
263
317
  ```
318
+ - **wire up gamepad auto connect/disconnect**
319
+ ```ts
320
+ tact.autoGamepads(controller => {
321
+ port.controllers.add(controller)
322
+ return () => port.controllers.delete(controller)
323
+ })
324
+ ```
264
325
 
265
326
  ### 🔌 interrogating actions
266
327
  - **poll the port every frame**
@@ -294,11 +355,14 @@ the hub embraces that analogy, helping you coordinate the plugging and unpluggin
294
355
  ### 🛞 create a hub with ports
295
356
  - **adopt standard hub bindings**
296
357
  ```ts
297
- // transform your game's bindings into hub-friendly bindings
298
- const hubBindings = tact.Hub.bindings(bindings)
358
+ const hubBindings = {
359
+ ...yourBindings,
360
+
361
+ // mixin standard hub bindings
362
+ ...tact.hubBindings(),
363
+ }
299
364
  ```
300
- - this augments your bindings with standard hub-specific bindings
301
- - this lets players to shimmy what port their controller is plugged into
365
+ - hub bindings let players shimmy what port their controller is plugged into
302
366
  - gamepad: hold gamma (middle button) and press bumpers
303
367
  - keyboard: left bracket or right bracket
304
368
  - **make hub with multiple ports at the ready**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benev/tact",
3
- "version": "0.1.0-2",
3
+ "version": "0.1.0-3",
4
4
  "description": "keybindings and gamepad support for web games",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
@@ -33,9 +33,10 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@benev/math": "^0.2.0-1",
36
+ "@e280/kv": "^0.1.0",
36
37
  "@e280/sly": "^0.1.1",
37
38
  "@e280/strata": "^0.1.0",
38
- "@e280/stz": "^0.2.0-4"
39
+ "@e280/stz": "^0.2.0-5"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@e280/science": "^0.1.1",
@@ -1,5 +1,5 @@
1
1
 
2
- import {isPressed} from "./utils/is-pressed.js"
2
+ import {isPressed} from "./parts/is-pressed.js"
3
3
 
4
4
  export class Action {
5
5
  #value = 0
@@ -0,0 +1,29 @@
1
+
2
+ import {Code, CodeSettings, CodeState} from "../types.js"
3
+
4
+ export const defaultHoldTime = 250
5
+
6
+ export function defaultCodeState([,,settings]: Code): CodeState {
7
+ return {
8
+ lastValue: 0,
9
+ holdStart: 0,
10
+ settings: defaultifyCodeSettings(settings),
11
+ }
12
+ }
13
+
14
+ function defaultCodeSettings(): CodeSettings {
15
+ return {
16
+ scale: 1,
17
+ invert: false,
18
+ deadzone: 0.2,
19
+ timing: ["direct"],
20
+ }
21
+ }
22
+
23
+ function defaultifyCodeSettings(partial: Partial<CodeSettings> = {}): CodeSettings {
24
+ return {
25
+ ...defaultCodeSettings(),
26
+ ...partial,
27
+ }
28
+ }
29
+
@@ -1,14 +1,13 @@
1
1
 
2
2
  import {pipe} from "@e280/stz"
3
3
  import {Scalar} from "@benev/math"
4
-
5
- import {LensState} from "./types.js"
4
+ import {CodeState} from "../types.js"
5
+ import {isPressed} from "./is-pressed.js"
6
6
  import {defaultHoldTime} from "./defaults.js"
7
- import {isPressed} from "../utils/is-pressed.js"
8
7
 
9
8
  export const lensAlgo = (
10
9
  now: number,
11
- state: LensState,
10
+ state: CodeState,
12
11
  v: number,
13
12
  ) => pipe(v).line(
14
13
 
@@ -40,9 +39,9 @@ export const lensAlgo = (
40
39
  const {settings} = state
41
40
 
42
41
  const holdTime = (
43
- settings.timing.style === "direct"
42
+ settings.timing[0] === "direct"
44
43
  ? undefined
45
- : settings.timing.holdTime
44
+ : settings.timing[1]
46
45
  ) ?? defaultHoldTime
47
46
 
48
47
  const isFreshlyPressed = !isPressed(state.lastValue) && isPressed(value)
@@ -54,7 +53,7 @@ export const lensAlgo = (
54
53
 
55
54
  state.lastValue = value
56
55
 
57
- switch (settings.timing.style) {
56
+ switch (settings.timing[0]) {
58
57
  case "direct":
59
58
  return value
60
59
 
@@ -0,0 +1,96 @@
1
+
2
+ import {MapG, pub, obMap} from "@e280/stz"
3
+ import {Action} from "./action.js"
4
+ import {lensAlgo} from "./parts/lens-algo.js"
5
+ import {SampleMap} from "../controllers/types.js"
6
+ import {tmax, tmin} from "../../utils/quick-math.js"
7
+ import {defaultCodeState} from "./parts/defaults.js"
8
+ import {Actions, Atom, Bindings, CodeSettings, CodeState} from "./types.js"
9
+
10
+ export class Resolver<B extends Bindings> {
11
+ #actions: Actions<B>
12
+
13
+ #now = 0
14
+ #samples: SampleMap = new Map()
15
+ #codeStates = new MapG<number, CodeState>()
16
+ #update = pub()
17
+
18
+ constructor(public readonly bindings: B, modes: Set<keyof B>) {
19
+ this.#actions = obMap(bindings, (bracket, mode) => obMap(bracket, atom => {
20
+ const action = new Action()
21
+ this.#update.subscribe(() => {
22
+ const value = modes.has(mode)
23
+ ? this.#resolveAtom()(atom)
24
+ : 0
25
+ Action.update(action, value)
26
+ })
27
+ return action
28
+ })) as Actions<B>
29
+ }
30
+
31
+ poll(now: number, samples: SampleMap) {
32
+ this.#now = now
33
+ this.#samples = samples
34
+ this.#update()
35
+ return this.#actions
36
+ }
37
+
38
+ #resolveCode(count: number, code: string, settings?: Partial<CodeSettings>) {
39
+ const state = this.#codeStates.guarantee(
40
+ count,
41
+ () => defaultCodeState(["code", code, settings]),
42
+ )
43
+ const value = this.#samples.get(code) ?? 0
44
+ return lensAlgo(this.#now, state, value)
45
+ }
46
+
47
+ #resolveAtom = (context: {count: number} = {count: 0}) => (atom: Atom): number => {
48
+ const resolveAtom = this.#resolveAtom(context)
49
+ if (typeof atom === "string") {
50
+ return this.#resolveCode(context.count++, atom)
51
+ }
52
+ else switch (atom[0]) {
53
+ case "code": {
54
+ const [, code, settings] = atom
55
+ return this.#resolveCode(context.count++, code, settings)
56
+ }
57
+ case "and": {
58
+ const [,...atoms] = atom
59
+ const values = atoms.map(resolveAtom)
60
+ return tmin(values)
61
+ }
62
+ case "or": {
63
+ const [,...atoms] = atom
64
+ const values = atoms.map(resolveAtom)
65
+ return tmax(values)
66
+ }
67
+ case "not": {
68
+ const [, subject] = atom
69
+ const value = resolveAtom(subject)
70
+ return (value > 0) ? 0 : 1
71
+ }
72
+ case "cond": {
73
+ const [, subject, guard] = atom
74
+ return (resolveAtom(guard) > 0)
75
+ ? resolveAtom(subject)
76
+ : 0
77
+ }
78
+ case "mods": {
79
+ const [, subject, modifiers] = atom
80
+ const maybe = (x: boolean, ...codes: Atom[]): Atom => x
81
+ ? ["or", ...codes]
82
+ : ["not", ["or", ...codes]]
83
+ const guard = resolveAtom(["cond", subject, ["and",
84
+ maybe(modifiers.ctrl ?? false, "ControlLeft", "ControlRight"),
85
+ maybe(modifiers.alt ?? false, "AltLeft", "AltRight"),
86
+ maybe(modifiers.meta ?? false, "MetaLeft", "MetaRight"),
87
+ maybe(modifiers.shift ?? false, "ShiftLeft", "ShiftRight"),
88
+ ]])
89
+ return (guard > 0)
90
+ ? resolveAtom(subject)
91
+ : 0
92
+ }
93
+ }
94
+ }
95
+ }
96
+
@@ -1,56 +1,57 @@
1
1
 
2
- export type Timing = (
3
- | DirectTiming
4
- | TapTiming
5
- | HoldTiming
6
- )
7
-
8
- export type DirectTiming = {style: "direct"}
9
- export type TapTiming = {style: "tap", holdTime?: number}
10
- export type HoldTiming = {style: "hold", holdTime?: number}
2
+ import {Action} from "./action.js"
11
3
 
12
- export type LensSettings = {
13
- scale: number
14
- invert: boolean
15
- deadzone: number
16
- timing: Timing
4
+ export type Actions<B extends Bindings> = {
5
+ [Mode in keyof B]: {
6
+ [K in keyof B[Mode]]: Action
7
+ }
17
8
  }
18
9
 
19
- export type Lens = {
20
- code: string
21
- settings?: Partial<LensSettings>
22
- }
10
+ export type Bindings = {[mode: string]: Bracket}
11
+ export type Bracket = {[action: string]: Atom}
23
12
 
24
- export type Spoon = {
25
- lenses: Lens[]
26
- required?: Lens[]
27
- forbidden?: Lens[]
13
+ export type AsBindings<B extends Bindings> = B
14
+ export function asBindings<B extends Bindings>(bindings: B) {
15
+ return bindings
28
16
  }
29
17
 
30
- export type Fork = Spoon[]
18
+ export type Code = ["code", string, settings?: Partial<CodeSettings>]
19
+ export type And = ["and", ...Atom[]]
20
+ export type Or = ["or", ...Atom[]]
21
+ export type Not = ["not", Atom]
22
+ export type Cond = ["cond", Atom, guard: Atom]
23
+ export type Mods = ["mods", Atom, modifiers: Partial<Modifiers>]
24
+
25
+ export type Atom = string | (
26
+ | Code
27
+ | And
28
+ | Or
29
+ | Not
30
+ | Cond
31
+ | Mods
32
+ )
31
33
 
32
- export type Bracket = {
33
- [action: string]: Spoon[]
34
+ export type CodeSettings = {
35
+ scale: number
36
+ invert: boolean
37
+ deadzone: number
38
+ timing: (
39
+ | ["direct"]
40
+ | ["tap", holdTime?: number]
41
+ | ["hold", holdTime?: number]
42
+ )
34
43
  }
35
44
 
36
- export type Bindings = {
37
- [mode: string]: Bracket
45
+ export type CodeState = {
46
+ settings: CodeSettings
47
+ lastValue: number
48
+ holdStart: number
38
49
  }
39
50
 
40
- export type AsBindings<B extends Bindings> = B
41
-
42
- export function asBindings<B extends Bindings>(bindings: B) {
43
- return bindings
51
+ export type Modifiers = {
52
+ ctrl: boolean
53
+ alt: boolean
54
+ shift: boolean
55
+ meta: boolean
44
56
  }
45
57
 
46
- export const hubMode = "hub" as const
47
-
48
- export type AsHubBindings<B extends Bindings> = {
49
- [hubMode]: {
50
- shimmyNext: Spoon[],
51
- shimmyPrevious: Spoon[],
52
- }
53
- } & B
54
-
55
- export type HubBindings = AsHubBindings<Bindings>
56
-
@@ -1,8 +1,8 @@
1
1
 
2
+ import {Pad} from "../../../utils/gamepads.js"
2
3
  import {tmax} from "../../../utils/quick-math.js"
3
4
  import {splitAxis} from "../../../utils/split-axis.js"
4
5
  import {SamplerController} from "../infra/sampler.js"
5
- import {gamepads, Pad} from "../../../utils/gamepads.js"
6
6
 
7
7
  const gamepadButtonCodes = [
8
8
  "gamepad.a",
@@ -25,10 +25,6 @@ const gamepadButtonCodes = [
25
25
  ]
26
26
 
27
27
  export class GamepadController extends SamplerController {
28
- static on(fn: (controller: GamepadController) => () => void) {
29
- return gamepads(pad => fn(new this(pad)))
30
- }
31
-
32
28
  constructor(public pad: Pad) {
33
29
  super()
34
30
  }
@@ -2,7 +2,6 @@
2
2
  import {coalesce, ev, sub} from "@e280/stz"
3
3
  import {Sample} from "../types.js"
4
4
  import {Controller} from "../controller.js"
5
- import {modprefix} from "../utils/modprefix.js"
6
5
 
7
6
  export class KeyboardController extends Controller {
8
7
  on = sub<Sample>()
@@ -27,11 +26,9 @@ export class KeyboardController extends Controller {
27
26
  keydown: (event: KeyboardEvent) => {
28
27
  if (event.repeat) return
29
28
  down(event.code)
30
- down(modprefix(event, event.code))
31
29
  },
32
30
  keyup: (event: KeyboardEvent) => {
33
31
  up(event.code)
34
- up(modprefix(event, event.code))
35
32
  },
36
33
  }),
37
34
 
@@ -1,7 +1,6 @@
1
1
 
2
2
  import {ev} from "@e280/stz"
3
3
  import {Vec2} from "@benev/math"
4
- import {modprefix} from "../utils/modprefix.js"
5
4
  import {splitAxis} from "../../../utils/split-axis.js"
6
5
  import {SamplerController} from "../infra/sampler.js"
7
6
 
@@ -13,20 +12,15 @@ export class PointerController extends SamplerController {
13
12
  constructor(target: EventTarget = window) {
14
13
  super()
15
14
 
16
- const dispatch = (event: PointerEvent | WheelEvent, code: string, value: number) => {
17
- this.setSample(code, value)
18
- this.setSample(modprefix(event, code), value)
19
- }
20
-
21
15
  this.dispose = ev(target, {
22
16
  pointerdown: (event: PointerEvent) => {
23
17
  const code = PointerController.buttonCode(event)
24
- dispatch(event, code, 1)
18
+ this.setSample(code, 1)
25
19
  },
26
20
 
27
21
  pointerup: (event: PointerEvent) => {
28
22
  const code = PointerController.buttonCode(event)
29
- dispatch(event, code, 0)
23
+ this.setSample(code, 0)
30
24
  },
31
25
 
32
26
  pointermove: (event: PointerEvent) => {
@@ -38,7 +32,7 @@ export class PointerController extends SamplerController {
38
32
 
39
33
  wheel: (event: WheelEvent) => {
40
34
  for (const [code, value] of PointerController.wheelCodes(event))
41
- dispatch(event, code, value)
35
+ this.setSample(code, value)
42
36
  },
43
37
  })
44
38
  }