@benev/tact 0.4.0-1 → 0.4.0-10

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 (122) hide show
  1. package/README.md +134 -46
  2. package/package.json +7 -6
  3. package/s/demo/main.css +38 -3
  4. package/s/index.html.ts +26 -20
  5. package/s/lib/index.ts +1 -1
  6. package/s/lib/input/controller.ts +4 -1
  7. package/s/lib/input/data/intent/decode.ts +2 -2
  8. package/s/lib/input/data/intent/encode.ts +2 -2
  9. package/s/lib/input/data/intent/minihash.ts +5 -0
  10. package/s/lib/input/decoder.ts +2 -2
  11. package/s/lib/input/evaluate/context.ts +5 -1
  12. package/s/lib/input/evaluate/evaluate.ts +27 -14
  13. package/s/lib/input/investigate/hash-bindings.ts +5 -3
  14. package/s/lib/input/investigate/investigate.ts +24 -9
  15. package/s/lib/input/test.ts +52 -30
  16. package/s/lib/input/testing/quickbind.ts +3 -3
  17. package/s/lib/ui/deck/auto-gamepads.ts +1 -0
  18. package/s/lib/ui/deck/controls/internal/data.ts +7 -5
  19. package/s/lib/ui/deck/hub.ts +4 -3
  20. package/s/lib/ui/deck/players.ts +73 -0
  21. package/s/lib/ui/deck/setup/deck.ts +33 -20
  22. package/s/lib/ui/deck/setup/tact.ts +26 -11
  23. package/s/lib/ui/deck/setup/types.ts +1 -0
  24. package/s/lib/ui/deck/setup/utils/broadcast-channel.ts +17 -0
  25. package/s/lib/ui/deck/setup/utils/kv.ts +20 -0
  26. package/s/lib/ui/deck/types.ts +1 -1
  27. package/s/lib/ui/views/controllers/style.css.ts +1 -1
  28. package/s/lib/ui/views/controllers/view.ts +3 -2
  29. package/s/lib/ui/views/customize/style.css.ts +1 -1
  30. package/s/lib/ui/views/customize/view.ts +6 -5
  31. package/s/lib/ui/views/deck/style.css.ts +1 -1
  32. package/s/lib/ui/views/deck/view.ts +13 -7
  33. package/s/lib/ui/views/theme.css.ts +1 -1
  34. package/s/lib/ui/views/visualizer/style.css.ts +8 -6
  35. package/s/lib/ui/views/visualizer/view.ts +1 -0
  36. package/s/lib/utils/hash.ts +47 -0
  37. package/x/demo/main.bundle.min.js +330 -326
  38. package/x/demo/main.bundle.min.js.map +4 -4
  39. package/x/demo/main.css +38 -3
  40. package/x/gamepad/index.html +2 -2
  41. package/x/gamepad/main.bundle.min.js +8 -8
  42. package/x/gamepad/main.bundle.min.js.map +4 -4
  43. package/x/index.html +28 -22
  44. package/x/index.html.js +26 -20
  45. package/x/index.html.js.map +1 -1
  46. package/x/lib/index.d.ts +1 -1
  47. package/x/lib/index.js +1 -1
  48. package/x/lib/index.js.map +1 -1
  49. package/x/lib/input/controller.js +1 -1
  50. package/x/lib/input/controller.js.map +1 -1
  51. package/x/lib/input/data/intent/decode.d.ts +1 -1
  52. package/x/lib/input/data/intent/decode.js +2 -2
  53. package/x/lib/input/data/intent/decode.js.map +1 -1
  54. package/x/lib/input/data/intent/encode.d.ts +1 -1
  55. package/x/lib/input/data/intent/encode.js +2 -2
  56. package/x/lib/input/data/intent/encode.js.map +1 -1
  57. package/x/lib/input/data/intent/minihash.d.ts +1 -0
  58. package/x/lib/input/data/intent/minihash.js +4 -0
  59. package/x/lib/input/data/intent/minihash.js.map +1 -0
  60. package/x/lib/input/decoder.d.ts +1 -1
  61. package/x/lib/input/decoder.js +2 -2
  62. package/x/lib/input/decoder.js.map +1 -1
  63. package/x/lib/input/evaluate/context.d.ts +3 -1
  64. package/x/lib/input/evaluate/context.js.map +1 -1
  65. package/x/lib/input/evaluate/evaluate.js +24 -11
  66. package/x/lib/input/evaluate/evaluate.js.map +1 -1
  67. package/x/lib/input/investigate/hash-bindings.d.ts +1 -1
  68. package/x/lib/input/investigate/hash-bindings.js +5 -3
  69. package/x/lib/input/investigate/hash-bindings.js.map +1 -1
  70. package/x/lib/input/investigate/investigate.d.ts +2 -1
  71. package/x/lib/input/investigate/investigate.js +14 -9
  72. package/x/lib/input/investigate/investigate.js.map +1 -1
  73. package/x/lib/input/test.d.ts +4 -2
  74. package/x/lib/input/test.js +49 -22
  75. package/x/lib/input/test.js.map +1 -1
  76. package/x/lib/input/testing/quickbind.d.ts +3 -2
  77. package/x/lib/input/testing/quickbind.js +3 -3
  78. package/x/lib/input/testing/quickbind.js.map +1 -1
  79. package/x/lib/ui/deck/auto-gamepads.js +1 -0
  80. package/x/lib/ui/deck/auto-gamepads.js.map +1 -1
  81. package/x/lib/ui/deck/controls/internal/data.d.ts +1 -1
  82. package/x/lib/ui/deck/controls/internal/data.js +4 -1
  83. package/x/lib/ui/deck/controls/internal/data.js.map +1 -1
  84. package/x/lib/ui/deck/hub.js +4 -3
  85. package/x/lib/ui/deck/hub.js.map +1 -1
  86. package/x/lib/ui/deck/players.d.ts +16 -0
  87. package/x/lib/ui/deck/players.js +56 -0
  88. package/x/lib/ui/deck/players.js.map +1 -0
  89. package/x/lib/ui/deck/setup/deck.js +23 -13
  90. package/x/lib/ui/deck/setup/deck.js.map +1 -1
  91. package/x/lib/ui/deck/setup/tact.d.ts +3 -0
  92. package/x/lib/ui/deck/setup/tact.js +19 -8
  93. package/x/lib/ui/deck/setup/tact.js.map +1 -1
  94. package/x/lib/ui/deck/setup/types.d.ts +1 -0
  95. package/x/lib/ui/deck/setup/utils/broadcast-channel.d.ts +4 -0
  96. package/x/lib/ui/deck/setup/utils/broadcast-channel.js +10 -0
  97. package/x/lib/ui/deck/setup/utils/broadcast-channel.js.map +1 -0
  98. package/x/lib/ui/deck/setup/utils/kv.d.ts +5 -0
  99. package/x/lib/ui/deck/setup/utils/kv.js +12 -0
  100. package/x/lib/ui/deck/setup/utils/kv.js.map +1 -0
  101. package/x/lib/ui/deck/types.d.ts +1 -0
  102. package/x/lib/ui/views/controllers/style.css.js +1 -1
  103. package/x/lib/ui/views/controllers/view.js +3 -2
  104. package/x/lib/ui/views/controllers/view.js.map +1 -1
  105. package/x/lib/ui/views/customize/style.css.js +1 -1
  106. package/x/lib/ui/views/customize/view.js +6 -5
  107. package/x/lib/ui/views/customize/view.js.map +1 -1
  108. package/x/lib/ui/views/deck/style.css.js +1 -1
  109. package/x/lib/ui/views/deck/view.js +13 -6
  110. package/x/lib/ui/views/deck/view.js.map +1 -1
  111. package/x/lib/ui/views/theme.css.js +1 -1
  112. package/x/lib/ui/views/visualizer/style.css.js +8 -6
  113. package/x/lib/ui/views/visualizer/style.css.js.map +1 -1
  114. package/x/lib/ui/views/visualizer/view.js +1 -0
  115. package/x/lib/ui/views/visualizer/view.js.map +1 -1
  116. package/x/lib/utils/hash.d.ts +1 -0
  117. package/x/lib/utils/hash.js +47 -0
  118. package/x/lib/utils/hash.js.map +1 -0
  119. package/s/lib/ui/deck/collector.ts +0 -52
  120. package/x/lib/ui/deck/collector.d.ts +0 -13
  121. package/x/lib/ui/deck/collector.js +0 -35
  122. package/x/lib/ui/deck/collector.js.map +0 -1
package/README.md CHANGED
@@ -2,16 +2,22 @@
2
2
  # 🎮 @benev/tact
3
3
  > *from keypress to couch co-op.*
4
4
 
5
- **tact is a user-input toolkit for web games.**
5
+ ### tact is a user-input toolkit for web games.
6
6
 
7
7
  🌐 👉 ***https://tact.benev.gg/*** 👈 *try now*
8
-
9
8
  🖱 keyboards, mice, gamepads.
10
- 🧩 expressive bindings json dsl.
11
- 🎨 bindings are user-customizable.
12
- 🫂 multiplayer and splitscreen capable.
13
- 🧬 binary for efficient workers and networking.
14
- 🎸 ready-to-rock drop-in user-interface.
9
+ 🔥 powerful enough for multiplayer games, simple enough for any app.
10
+ 🎸 users can customize keybinds with the ready-to-rock ui.
11
+ 🧬 expressive bindings json dsl.
12
+ 📡 binary streaming for multiplayer and multithreading.
13
+ ✍️ handcrafted by a human being.
14
+
15
+ user input isn't as simple as you'd think.
16
+ after many design iterations, tact has evolved into an elegant system that handles hard problems for you like customizability, multiplayer, and multithreading. i'm trying to give you the *"it just works"* experience.
17
+
18
+ i care about the little things. all input types are boiled down to simple numbers, and packed up into efficient binary packets for streaming across networks and web workers. mouse movement precision and accuracy is holy, so there's careful design to make it resilient in the face of encoder/decoder sampling frequency mismatches. the bindings language is an infinitely capable recursive dsl so you can remap values from funky gamepads and make ridiculously complex logical hotkey combinations.
19
+
20
+
15
21
 
16
22
  ![](https://i.imgur.com/dCAlD14.png)
17
23
 
@@ -22,8 +28,9 @@
22
28
  <br/>
23
29
 
24
30
  ## 🎮 let's play.
31
+ > *here's the fully-featured, batteries-included, ready-for-anything web game starter pack.*
25
32
 
26
- - 🍏 **install tact.**
33
+ - 🍏 **install tact into your typescript project.**
27
34
  ```sh
28
35
  npm install @benev/tact@next
29
36
  ```
@@ -37,8 +44,7 @@
37
44
  look: ["accumulate", "pointer.x"],
38
45
  })
39
46
  ```
40
- - 💡 *bindings is a tiny json dsl. see [expression.ts](./s/lib/input/expression.ts).*
41
- - 🍏 **let's have two presets available to the user. one for gamepads.**
47
+ - 🍏 **let's have two presets available to the user, with one for gamepads.**
42
48
  ```ts
43
49
  import {asPresets} from "@benev/tact"
44
50
 
@@ -51,7 +57,7 @@
51
57
  }},
52
58
  })
53
59
  ```
54
- - 🍏 **now we can setup tact and register the html elements.**
60
+ - ⚙️ **setup tact.**
55
61
  ```ts
56
62
  import {setupTact} from "@benev/tact"
57
63
 
@@ -60,70 +66,152 @@
60
66
  tact.gamepads("gamepad")
61
67
  tact.registerElements()
62
68
  ```
63
- - 🍏 **simply put the `<tact-deck>` in your html page.**
64
- ```html
65
- <tact-deck></tact-deck>
69
+ - 💡 *you can set `{maxPlayers: 1}` for a single player game.*
70
+ - 💡 *you can pass `{namespace: "falrysk"}` if you need to run multiple games on one website.*
71
+ - 💡 *you can register elements manually like `customElements.define("tact-deck-falrysk", tact.elements.TactDeck)`.*
72
+ - 💡 *you can call `tact.dispose()` to release resources.*
73
+ - 🍏 **encoding, decoding, and your game logic.**
74
+ ```ts
75
+ import {Players} from "@benev/tact"
76
+
77
+ const players = new Players(bindings)
78
+
79
+ setInterval(() => {
80
+ const buffer = tact.encodeAll()
81
+ players.decodeAll(buffer)
82
+ for (const [playerId, inputs] of players) {
83
+
84
+ // your game logic can read these handy input values.
85
+ inputs.jump.value // button-like values are numbers 0 to 1.
86
+ inputs.jump.was // previous value (to detect up/down).
87
+ inputs.jump.sum // sum of values reported since last frame.
88
+
89
+ inputs.forward.value
90
+ inputs.look.value
91
+ }
92
+ }, 16.67)
66
93
  ```
67
- - 💡 *you also have each panel, `<tact-controllers>` and `<tact-customize>`.*
68
- - 🍏 **color the whole ui with css.**
94
+ - 💡 *maybe you'd like to stream the buffers over a network with [renraku](https://github.com/e280/renraku).*
95
+ - 💡 *maybe you'd like the tact deck on the clientside and the players on the serverside.*
96
+ - 🍏 **put the `<tact-deck>` element in your html page.**
69
97
  ```html
70
- <style>
71
- html {
72
- --tact-prime: aqua;
73
- --tact-bg: black;
74
- }
75
- </style>
98
+ <tact-deck></tact-deck>
76
99
  ```
100
+ - 💡 *or just use the sub-panels directly, `<tact-controllers>` and `<tact-customize>`.*
101
+ - 💡 *you can customize the colors with css.*
102
+ ```html
103
+ <style>
104
+ html {
105
+ --tact-prime: aqua;
106
+ }
107
+ </style>
108
+ ```
109
+ - 🎉 **you're done! your users can manage controllers and customize bindings!**
77
110
 
78
111
 
79
112
 
80
113
  <br/>
81
114
 
82
- ## 🎮 encoding, decoding, and your game logic.
115
+ ## 🎮 get down to brass tacks.
116
+ > *for simpler apps, you can use tact's composable parts more directly.*
83
117
 
84
- - 🍏 **make a collector.**
118
+ - ⚙️ **pov: your app isn't a game, no multiplayer, no gamepads.**
85
119
  ```ts
86
- import {Collector} from "@benev/tact"
120
+ import {setupTact, Decoder} from "@benev/tact"
121
+
122
+ const tact = await setupTact({presets, maxPlayers: 1})
123
+ const controller = tact.mouseKeyboard()
124
+ const decoder = new Decoder(bindings)
125
+ tact.registerElements()
126
+
127
+ setInterval(() => {
128
+ const inputs = decoder.decode(controller.encode())
129
+
130
+ // 🧙‍♂️ handle your inputs.
131
+ inputs.jump.value
87
132
 
88
- const collector = new Collector(bindings)
133
+ }, 16.67)
89
134
  ```
90
- - 🍏 **now it's time for the real action!**
135
+ - 💡 *then the only ui you need is the `<tact-customize>` panel (also available as a sly view `CustomizeView`).*
136
+ - ⚙️ **pov: your app doesn't even want any ui, or persistence.**
91
137
  ```ts
138
+ import {Controller, Decoder, KeyboardSource, PointerSource, Sources} from "@benev/tact"
139
+
140
+ const source = new Sources(new KeyboardSource(), new PointerSource())
141
+ const controller = new Controller({bindings, source})
142
+ const decoder = new Decoder(bindings)
143
+
92
144
  setInterval(() => {
93
- const buffer = tact.deck.hub.encodeAll()
94
- for (const {playerId, inputs} of collector.decodeAll(buffer)) {
145
+ const inputs = decoder.decode(controller.encode())
95
146
 
96
- // your game logic can read these handy input values.
97
- inputs.jump.value // values are numbers 0 to 1.
98
- inputs.jump.was // previous value (to detect up/down).
99
- inputs.jump.sum // sum of values reported since last frame.
147
+ // 🧙‍♂️ handle your inputs.
148
+ inputs.jump.value
100
149
 
101
- inputs.forward.value
102
- inputs.look.value
103
- }
104
150
  }, 16.67)
105
151
  ```
106
- 💡 *you could stream the buffers across the network to a remote collector.*
107
152
 
108
153
 
109
154
 
110
155
  <br/>
111
156
 
112
- ## 🎮 get down to brass tacks.
157
+ ## 🎮 bindings.
158
+ > *bindings are a little json programming language.*
113
159
 
114
- - 🍏 **use the core facilities directly,** without the ui or multiplayer.
160
+ - 🧬 **see [expression.ts](./s/lib/input/expression.ts) to get an idea of what's possible.**
161
+ - 🧬 **arbitrary nesting is valid.**
115
162
  ```ts
116
- import {Controller, InputDecoder, KeyboardSource, PointerSource, Sources} from "@benev/tact"
163
+ asBindings("KeyW")
117
164
 
118
- const controller = new Controller({
119
- bindings,
120
- source: new Sources(new KeyboardSource(), new PointerSource()),
165
+ inputs.value // 1
166
+ ```
167
+ ```ts
168
+ asBindings({
169
+ special: {
170
+ magic: {
171
+ castSpell: ["*", ["mod", "shift", "alt"], "KeyR"],
172
+ }
173
+ }
121
174
  })
122
175
 
123
- const decoder = new InputDecoder(bindings)
176
+ inputs.special.magic.castSpell.value // 1
177
+ ```
178
+ - 🧬 **you can differentiate taps versus holds on the same key.**
179
+ ```ts
180
+ asBindings({
181
+
182
+ // tapping E yoinks a weapon into your inventory.
183
+ grab: ["taps", 1, 250, "KeyE"],
184
+
185
+ // holding E equips the weapon as your primary.
186
+ equip: ["held", 250, "KeyE"],
187
+ })
188
+ ```
189
+ - 🧬 **you can "accumulate" mouse inputs accurately,** to avoid losses in sampling frequency mismatches.
190
+ ```ts
191
+ asBindings({
192
+ lookX: ["accumulate", "pointer.x"],
193
+ })
124
194
 
125
- // call this in your gameloop:
126
- const inputs = decoder.decode(controller.encode())
195
+ inputs.lookX.sum // use sum instead of value.
196
+ ```
197
+ - 🧬 **gamepads thumbstick inputs are best done with some math.**
198
+ ```ts
199
+ asBindings({
200
+ forward: ["*",
201
+
202
+ // user-customizable sensitivity,
203
+ // equivalent with pointer-pixels-per-second.
204
+ 1234,
205
+
206
+ // delta time factor,
207
+ // to ensure sensitivity is framerate-independent.
208
+ ["dt"],
209
+
210
+ // remapping the ranges,
211
+ // because different gamepads represent the values differently.
212
+ ["remap", 0, -1, 0, 1, "gamepad.axis.1"],
213
+ ],
214
+ })
127
215
  ```
128
216
 
129
217
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benev/tact",
3
- "version": "0.4.0-1",
3
+ "version": "0.4.0-10",
4
4
  "description": "🎮 from keypress to couch co-op",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
@@ -21,21 +21,22 @@
21
21
  "count": "find s -path '*/_archive' -prune -o -name '*.ts' -exec wc -l {} +"
22
22
  },
23
23
  "peerDependencies": {
24
- "@e280/sly": "^0.4.9",
25
- "@e280/strata": "^0.5.6",
24
+ "@e280/sly": "^0.4.14",
25
+ "@e280/strata": "^0.5.7",
26
26
  "lit": "^3.3.3"
27
27
  },
28
28
  "dependencies": {
29
+ "@awasm/noble": "^0.1.4",
29
30
  "@benev/math": "0.4.0-9",
30
- "@e280/kv": "^0.2.4",
31
+ "@e280/kv": "^0.2.6",
31
32
  "@e280/shiny": "^0.1.1",
32
- "@e280/stz": "^0.4.14"
33
+ "@e280/stz": "^0.4.16"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@e280/hottie": "^0.1.7",
36
37
  "@e280/octo": "^0.1.7",
37
38
  "@e280/science": "^0.1.14",
38
- "@e280/scute": "^0.5.4",
39
+ "@e280/scute": "^0.5.5",
39
40
  "http-server": "^14.1.1",
40
41
  "npm-run-all": "^4.1.5",
41
42
  "typescript": "^7.0.2"
package/s/demo/main.css CHANGED
@@ -53,6 +53,10 @@
53
53
  background: var(--bg);
54
54
  }
55
55
 
56
+ html {
57
+ scrollbar-gutter: stable;
58
+ }
59
+
56
60
  body {
57
61
  display: flex;
58
62
  flex-direction: column;
@@ -67,10 +71,34 @@
67
71
  }
68
72
 
69
73
  > header {
74
+ display: flex;
75
+ flex-wrap: wrap;
76
+ justify-content: space-between;
77
+ align-items: end;
78
+ gap: 1em;
70
79
  padding: 0 2em;
71
- h1 small { font-size: 0.6em; font-weight: normal; }
72
- * + * { margin-top: 0.5em; }
73
- label { padding-left: 1em; }
80
+
81
+ h1 small {
82
+ font-size: 0.6em;
83
+ font-weight: normal;
84
+ }
85
+
86
+ blockquote {
87
+ opacity: 0.5;
88
+ font-style: italic;
89
+ padding-left: 1em;
90
+ }
91
+
92
+ label {
93
+ display: flex;
94
+ flex-direction: column;
95
+ align-items: end;
96
+ gap: 0.1em;
97
+ }
98
+
99
+ .links span {
100
+ opacity: 0.5;
101
+ }
74
102
  }
75
103
 
76
104
  > section {
@@ -79,6 +107,13 @@
79
107
  padding-left: 1em;
80
108
  }
81
109
  }
110
+
111
+ > footer {
112
+ padding: 1em 2em;
113
+ p {
114
+ opacity: 0.5;
115
+ }
116
+ }
82
117
  }
83
118
  }
84
119
 
package/s/index.html.ts CHANGED
@@ -4,7 +4,7 @@ import {template, html, dataSvgEmoji, socialCard} from "@e280/scute"
4
4
  const title = "@benev/tact"
5
5
  const domain = "tact.benev.gg"
6
6
  const favicon = "/assets/b.png"
7
- const description = "🎮 keybindings and couch co-op"
7
+ const description = "🎮 from keypress to couch co-op"
8
8
 
9
9
  export default template(import.meta.url, async orb => html`
10
10
  <!doctype html>
@@ -31,30 +31,36 @@ export default template(import.meta.url, async orb => html`
31
31
  </head>
32
32
  <body>
33
33
  <header>
34
- <h1>
35
- <strong>@benev/tact</strong>
36
- <small>v${orb.packageVersion()}</small>
37
- </h1>
38
34
  <div>
39
- <blockquote>"from keypress to couch co-op"</blockquote>
40
- <p class=links>
41
- <a href="./gamepad/">gamepad visualizer</a>
42
- —
43
- <a href="https://github.com/benev-gg/tact">github</a>
44
- —
45
- <a href="https://benev.gg/">benev.gg</a>
46
- </p>
47
- <p>
48
- <label>
49
- <span>➣ choose your color:</span>
50
- <input type="color"/>
51
- </label>
52
- </p>
35
+ <h1>
36
+ <strong>@benev/tact</strong>
37
+ <small>v${orb.packageVersion()}</small>
38
+ </h1>
39
+ <div>
40
+ <blockquote>"from keypress to couch co-op"</blockquote>
41
+ <p class=links>
42
+ <a href="./gamepad/">gamepad visualizer</a>
43
+ <span>—</span>
44
+ <a href="https://github.com/benev-gg/tact">github</a>
45
+ <span>—</span>
46
+ <a href="https://benev.gg/">benev.gg</a>
47
+ </p>
48
+ </div>
49
+ </div>
50
+ <div>
51
+ <label>
52
+ <input type="color"/>
53
+ <span>choose your color ☝️</span>
54
+ </label>
53
55
  </div>
54
56
  </header>
55
57
 
56
58
  <tact-deck></tact-deck>
57
- <tact-visualizer></tact-visualizer>
59
+
60
+ <footer>
61
+ <p>binary data visualizer</p>
62
+ <tact-visualizer></tact-visualizer>
63
+ </footer>
58
64
  </body>
59
65
  </html>
60
66
  `)
package/s/lib/index.ts CHANGED
@@ -19,7 +19,7 @@ export * from "./ui/deck/controls/types.js"
19
19
  export * from "./ui/deck/setup/deck.js"
20
20
  export * from "./ui/deck/setup/tact.js"
21
21
  export * from "./ui/deck/auto-gamepads.js"
22
- export * from "./ui/deck/collector.js"
22
+ export * from "./ui/deck/players.js"
23
23
  export * from "./ui/deck/hub.js"
24
24
  export * from "./ui/deck/types.js"
25
25
  export * from "./ui/views/customize/view.js"
@@ -58,7 +58,10 @@ export class Controller<B extends Bindings = any> {
58
58
  }
59
59
 
60
60
  encode(time = performance.now()) {
61
- return encodeIntents(this.#investigation.hash, [...this.evaluate(time)])
61
+ return encodeIntents(
62
+ this.#investigation.minihash,
63
+ [...this.evaluate(time)],
64
+ )
62
65
  }
63
66
 
64
67
  clear() {
@@ -8,7 +8,7 @@ export function decodeIntents(buffer: ArrayBuffer) {
8
8
  const dv = new DataView(buffer)
9
9
  let cursor = 0
10
10
 
11
- const hash = dv.getUint32(cursor, endian)
11
+ const minihash = dv.getUint32(cursor, endian)
12
12
  cursor += sizeHash
13
13
 
14
14
  while (cursor < buffer.byteLength) {
@@ -21,6 +21,6 @@ export function decodeIntents(buffer: ArrayBuffer) {
21
21
  intents.push({id, value})
22
22
  }
23
23
 
24
- return {hash, intents}
24
+ return {minihash, intents}
25
25
  }
26
26
 
@@ -3,13 +3,13 @@ import {Intent} from "./types.js"
3
3
  import {endian} from "../endian.js"
4
4
  import {sizeHash, sizeId, sizeIntent, sizeValue} from "./sizes.js"
5
5
 
6
- export function encodeIntents(hash: number, intents: Intent[]) {
6
+ export function encodeIntents(minihash: number, intents: Intent[]) {
7
7
  const byteCount = sizeHash + (intents.length * sizeIntent)
8
8
  const buffer = new ArrayBuffer(byteCount)
9
9
  const dv = new DataView(buffer)
10
10
  let cursor = 0
11
11
 
12
- dv.setUint32(cursor, hash, endian)
12
+ dv.setUint32(cursor, minihash, endian)
13
13
  cursor += sizeHash
14
14
 
15
15
  for (const intent of intents) {
@@ -0,0 +1,5 @@
1
+
2
+ export function minihash(hash: string) {
3
+ return Number.parseInt(hash.slice(0, 8), 16)
4
+ }
5
+
@@ -3,7 +3,7 @@ import {Inputs, Bindings} from "./types.js"
3
3
  import {decodeIntents} from "./data/intent/decode.js"
4
4
  import {investigate} from "./investigate/investigate.js"
5
5
 
6
- export class InputDecoder<B extends Bindings> {
6
+ export class Decoder<B extends Bindings> {
7
7
  #investigation
8
8
 
9
9
  constructor(bindings: B) {
@@ -25,7 +25,7 @@ export class InputDecoder<B extends Bindings> {
25
25
  for (const buffer of buffers) {
26
26
  const decoded = decodeIntents(buffer)
27
27
 
28
- if (decoded.hash !== this.#investigation.hash)
28
+ if (decoded.minihash !== this.#investigation.minihash)
29
29
  throw new Error("intent bindings hash mismatch")
30
30
 
31
31
  for (const input of inputs)
@@ -11,6 +11,10 @@ export class EvaluationContext {
11
11
  previousValues = new Map<string, number>()
12
12
 
13
13
  holding = new Map<object, {start: number}>()
14
- tapping = new Map<object, {events: number[]}>()
14
+ tapping = new Map<object, {
15
+ wasDown: boolean
16
+ startedAt: number | null
17
+ count: number
18
+ }>()
15
19
  }
16
20
 
@@ -1,6 +1,6 @@
1
1
 
2
2
  import {remap} from "@benev/math"
3
- import {arraylimit, guarantee, isHappy, isNumber, isString} from "@e280/stz"
3
+ import {guarantee, isHappy, isNumber, isString} from "@e280/stz"
4
4
 
5
5
  import {isDown} from "../utils/is-down.js"
6
6
  import {EvaluationContext} from "./context.js"
@@ -181,27 +181,40 @@ export function evaluate(
181
181
 
182
182
  case "taps": {
183
183
  const [, taps, ms, subexpression] = expression
184
- const presses = e(subexpression)
184
+ const down = isDown(e(subexpression))
185
185
  const now = context.clock.time
186
186
 
187
- const tapped = guarantee(context.tapping, expression, () => ({
188
- events: [] as number[],
187
+ const state = guarantee(context.tapping, expression, () => ({
188
+ wasDown: false,
189
+ startedAt: null as number | null,
190
+ count: 0,
189
191
  }))
190
192
 
191
- for (let i = 0; i < presses; i++)
192
- tapped.events.push(now)
193
+ const pressed = down && !state.wasDown
194
+ const released = !down && state.wasDown
193
195
 
194
- tapped.events = arraylimit(
195
- tapped.events.filter(time => time >= now - ms),
196
- 128,
197
- )
196
+ if (state.startedAt !== null && now - state.startedAt >= ms) {
197
+ state.startedAt = null
198
+ state.count = 0
199
+ }
200
+
201
+ let result = 0
198
202
 
199
- if (tapped.events.length >= taps) {
200
- tapped.events = []
201
- return 1
203
+ if (pressed && state.startedAt === null)
204
+ state.startedAt = now
205
+
206
+ if (released && state.startedAt !== null) {
207
+ state.count++
208
+
209
+ if (state.count === taps) {
210
+ result = 1
211
+ state.count = 0
212
+ state.startedAt = null
213
+ }
202
214
  }
203
215
 
204
- return 0
216
+ state.wasDown = down
217
+ return result
205
218
  }
206
219
 
207
220
  case "mod": {
@@ -1,6 +1,7 @@
1
1
 
2
- import {hash32} from "@e280/stz"
2
+ import {hex} from "@e280/stz"
3
3
  import {Bindings} from "../types.js"
4
+ import {hash} from "../../utils/hash.js"
4
5
  import {isExpression} from "./is-expression.js"
5
6
 
6
7
  export function hashBindings(bindings: Bindings) {
@@ -8,7 +9,7 @@ export function hashBindings(bindings: Bindings) {
8
9
 
9
10
  function recurse(b: Bindings, path: string[]) {
10
11
  if (isExpression(b)) {
11
- paths.push(path.join("."))
12
+ paths.push(JSON.stringify(path))
12
13
  }
13
14
  else {
14
15
  for (const [key, value] of Object.entries(b))
@@ -17,6 +18,7 @@ export function hashBindings(bindings: Bindings) {
17
18
  }
18
19
 
19
20
  recurse(bindings, ["b"])
20
- return hash32(...paths)
21
+ paths.sort()
22
+ return hex(hash("tact/bindings/v1", ...paths).slice(16))
21
23
  }
22
24
 
@@ -5,31 +5,46 @@ import {Expression} from "../expression.js"
5
5
  import {hashBindings} from "./hash-bindings.js"
6
6
  import {isExpression} from "./is-expression.js"
7
7
  import {Bindings, Input, Inputs} from "../types.js"
8
+ import {minihash} from "../data/intent/minihash.js"
8
9
 
9
10
  export function investigate<B extends Bindings>(bindings: B) {
10
11
  const hash = hashBindings(bindings)
11
- const inputList: Input[] = []
12
- const rootList: Expression[] = []
13
12
 
14
- function makeInputs(b: Bindings): unknown {
13
+ const leaves: {
14
+ path: string
15
+ input: Input
16
+ expression: Expression
17
+ }[] = []
18
+
19
+ function makeInputs(b: Bindings, path: string[]): unknown {
15
20
  if (isExpression(b)) {
16
21
  const input = makeInput()
17
- inputList.push(input)
18
- rootList.push(b)
22
+ leaves.push({
23
+ path: JSON.stringify(path),
24
+ input,
25
+ expression: b,
26
+ })
19
27
  return input
20
28
  }
21
29
  else {
22
- return obmap(b, makeInputs)
30
+ return obmap(b, (value, key) =>
31
+ makeInputs(value, [...path, key as string])
32
+ )
23
33
  }
24
34
  }
25
35
 
26
- const inputs = makeInputs(bindings) as Inputs<B>
36
+ const inputs = makeInputs(bindings, ["b"]) as Inputs<B>
37
+
38
+ leaves.sort((a, b) =>
39
+ a.path < b.path ? -1 : a.path > b.path ? 1 : 0
40
+ )
27
41
 
28
42
  return {
29
43
  hash,
44
+ minihash: minihash(hash),
30
45
  inputs,
31
- inputList,
32
- rootList,
46
+ inputList: leaves.map(l => l.input),
47
+ rootList: leaves.map(l => l.expression),
33
48
  }
34
49
  }
35
50