@joystyk/session 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/NOTICE +36 -0
- package/README.md +266 -2
- package/dist/assistance.d.ts +3 -0
- package/dist/assistance.js +20 -0
- package/dist/assistance.js.map +1 -0
- package/dist/browser/audio-sounds.d.ts +19 -0
- package/dist/browser/audio-sounds.js +413 -0
- package/dist/browser/audio-sounds.js.map +1 -0
- package/dist/browser/audio.d.ts +16 -0
- package/dist/browser/audio.js +182 -0
- package/dist/browser/audio.js.map +1 -0
- package/dist/browser/index.d.ts +48 -0
- package/dist/browser/index.js +140 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/preparer.d.ts +3 -0
- package/dist/browser/preparer.js +73 -0
- package/dist/browser/preparer.js.map +1 -0
- package/dist/browser/rooms.d.ts +35 -0
- package/dist/browser/rooms.js +140 -0
- package/dist/browser/rooms.js.map +1 -0
- package/dist/fixed-step.d.ts +66 -0
- package/dist/fixed-step.js +280 -0
- package/dist/fixed-step.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/local.d.ts +130 -0
- package/dist/local.js +723 -0
- package/dist/local.js.map +1 -0
- package/dist/ports.d.ts +70 -0
- package/dist/ports.js +10 -0
- package/dist/ports.js.map +1 -0
- package/dist/publisher.d.ts +11 -0
- package/dist/publisher.js +44 -0
- package/dist/publisher.js.map +1 -0
- package/dist/remote.d.ts +128 -0
- package/dist/remote.js +474 -0
- package/dist/remote.js.map +1 -0
- package/dist/replay.d.ts +65 -0
- package/dist/replay.js +199 -0
- package/dist/replay.js.map +1 -0
- package/dist/rngs.d.ts +22 -0
- package/dist/rngs.js +34 -0
- package/dist/rngs.js.map +1 -0
- package/dist/saves.d.ts +20 -0
- package/dist/saves.js +59 -0
- package/dist/saves.js.map +1 -0
- package/dist/stopwatch.d.ts +22 -0
- package/dist/stopwatch.js +51 -0
- package/dist/stopwatch.js.map +1 -0
- package/dist/values.d.ts +19 -0
- package/dist/values.js +66 -0
- package/dist/values.js.map +1 -0
- package/package.json +56 -3
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
//#region src/browser/audio-sounds.ts
|
|
2
|
+
/** Deliberate palette per game; no samples, music, network downloads or third-party recordings. */
|
|
3
|
+
var SOUND_PROFILES = {
|
|
4
|
+
"garden-crossing": "garden",
|
|
5
|
+
"tower-stacker": "wood",
|
|
6
|
+
"rock-field": "arcade",
|
|
7
|
+
"2048": "wood",
|
|
8
|
+
backgammon: "wood",
|
|
9
|
+
blackjack: "paper",
|
|
10
|
+
"brick-break": "arcade",
|
|
11
|
+
checkers: "wood",
|
|
12
|
+
chess: "wood",
|
|
13
|
+
"chinese-checkers": "glass",
|
|
14
|
+
"circuit-84": "terminal",
|
|
15
|
+
"clue-teams": "paper",
|
|
16
|
+
"code-breaker": "glass",
|
|
17
|
+
containment: "arcade",
|
|
18
|
+
"crazy-eights": "paper",
|
|
19
|
+
cribbage: "wood",
|
|
20
|
+
crossword: "paper",
|
|
21
|
+
"daily-word": "paper",
|
|
22
|
+
dominoes: "wood",
|
|
23
|
+
"dots-and-boxes": "paper",
|
|
24
|
+
"drop-four": "wood",
|
|
25
|
+
equation: "glass",
|
|
26
|
+
euchre: "paper",
|
|
27
|
+
"five-dice": "wood",
|
|
28
|
+
"four-groups": "glass",
|
|
29
|
+
freecell: "paper",
|
|
30
|
+
"gem-swap": "glass",
|
|
31
|
+
"gin-rummy": "paper",
|
|
32
|
+
go: "glass",
|
|
33
|
+
"go-fish": "paper",
|
|
34
|
+
golf: "paper",
|
|
35
|
+
greed: "wood",
|
|
36
|
+
hangman: "paper",
|
|
37
|
+
hearts: "paper",
|
|
38
|
+
honeycomb: "garden",
|
|
39
|
+
klondike: "paper",
|
|
40
|
+
"letter-drop": "wood",
|
|
41
|
+
"liars-dice": "wood",
|
|
42
|
+
"light-trails": "arcade",
|
|
43
|
+
ludo: "wood",
|
|
44
|
+
mancala: "wood",
|
|
45
|
+
"map-guess": "garden",
|
|
46
|
+
"memory-match": "paper",
|
|
47
|
+
minesweeper: "glass",
|
|
48
|
+
"mini-golf": "garden",
|
|
49
|
+
"naval-battle": "arcade",
|
|
50
|
+
nim: "wood",
|
|
51
|
+
nonogram: "paper",
|
|
52
|
+
"old-maid": "paper",
|
|
53
|
+
paddle: "arcade",
|
|
54
|
+
pig: "wood",
|
|
55
|
+
"pipe-link": "glass",
|
|
56
|
+
pyramid: "paper",
|
|
57
|
+
queens: "glass",
|
|
58
|
+
reversi: "wood",
|
|
59
|
+
"rock-paper-scissors": "paper",
|
|
60
|
+
"shift-23": "terminal",
|
|
61
|
+
"shut-the-box": "wood",
|
|
62
|
+
snake: "arcade",
|
|
63
|
+
spades: "paper",
|
|
64
|
+
spider: "paper",
|
|
65
|
+
sudoku: "paper",
|
|
66
|
+
"tic-tac-toe": "paper",
|
|
67
|
+
"tile-match": "glass",
|
|
68
|
+
tripeaks: "paper",
|
|
69
|
+
war: "paper",
|
|
70
|
+
"word-flux": "garden",
|
|
71
|
+
"word-hunt": "garden",
|
|
72
|
+
"word-ladder": "wood",
|
|
73
|
+
"word-of-the-day": "paper"
|
|
74
|
+
};
|
|
75
|
+
/** Games which already select sound effects in their own controls and drivers. */
|
|
76
|
+
var CUSTOM_AUDIO_GAMES = /* @__PURE__ */ new Set([
|
|
77
|
+
"garden-crossing",
|
|
78
|
+
"tower-stacker",
|
|
79
|
+
"rock-field",
|
|
80
|
+
"snake",
|
|
81
|
+
"paddle",
|
|
82
|
+
"light-trails",
|
|
83
|
+
"containment",
|
|
84
|
+
"circuit-84",
|
|
85
|
+
"brick-break",
|
|
86
|
+
"gem-swap",
|
|
87
|
+
"mini-golf",
|
|
88
|
+
"klondike",
|
|
89
|
+
"freecell",
|
|
90
|
+
"spider",
|
|
91
|
+
"pyramid",
|
|
92
|
+
"tripeaks",
|
|
93
|
+
"golf"
|
|
94
|
+
]);
|
|
95
|
+
var NOTES = {
|
|
96
|
+
move: [
|
|
97
|
+
220,
|
|
98
|
+
140,
|
|
99
|
+
.045
|
|
100
|
+
],
|
|
101
|
+
card: [
|
|
102
|
+
170,
|
|
103
|
+
100,
|
|
104
|
+
.045
|
|
105
|
+
],
|
|
106
|
+
clear: [
|
|
107
|
+
350,
|
|
108
|
+
120,
|
|
109
|
+
.14
|
|
110
|
+
],
|
|
111
|
+
success: [
|
|
112
|
+
660,
|
|
113
|
+
880,
|
|
114
|
+
.18
|
|
115
|
+
],
|
|
116
|
+
error: [
|
|
117
|
+
160,
|
|
118
|
+
100,
|
|
119
|
+
.11
|
|
120
|
+
],
|
|
121
|
+
lose: [
|
|
122
|
+
180,
|
|
123
|
+
70,
|
|
124
|
+
.22
|
|
125
|
+
],
|
|
126
|
+
level: [
|
|
127
|
+
520,
|
|
128
|
+
1040,
|
|
129
|
+
.25
|
|
130
|
+
],
|
|
131
|
+
start: [
|
|
132
|
+
440,
|
|
133
|
+
660,
|
|
134
|
+
.13
|
|
135
|
+
],
|
|
136
|
+
tick: [
|
|
137
|
+
900,
|
|
138
|
+
600,
|
|
139
|
+
.025
|
|
140
|
+
],
|
|
141
|
+
capture: [
|
|
142
|
+
440,
|
|
143
|
+
880,
|
|
144
|
+
.16
|
|
145
|
+
],
|
|
146
|
+
hit: [
|
|
147
|
+
260,
|
|
148
|
+
80,
|
|
149
|
+
.06
|
|
150
|
+
],
|
|
151
|
+
paddle: [
|
|
152
|
+
140,
|
|
153
|
+
80,
|
|
154
|
+
.065
|
|
155
|
+
],
|
|
156
|
+
power: [
|
|
157
|
+
440,
|
|
158
|
+
880,
|
|
159
|
+
.12
|
|
160
|
+
],
|
|
161
|
+
stroke: [
|
|
162
|
+
170,
|
|
163
|
+
65,
|
|
164
|
+
.07
|
|
165
|
+
],
|
|
166
|
+
rail: [
|
|
167
|
+
240,
|
|
168
|
+
100,
|
|
169
|
+
.07
|
|
170
|
+
],
|
|
171
|
+
water: [
|
|
172
|
+
95,
|
|
173
|
+
60,
|
|
174
|
+
.14
|
|
175
|
+
],
|
|
176
|
+
cup: [
|
|
177
|
+
450,
|
|
178
|
+
850,
|
|
179
|
+
.16
|
|
180
|
+
],
|
|
181
|
+
cascade: [
|
|
182
|
+
300,
|
|
183
|
+
170,
|
|
184
|
+
.1
|
|
185
|
+
],
|
|
186
|
+
trace: [
|
|
187
|
+
150,
|
|
188
|
+
180,
|
|
189
|
+
.035
|
|
190
|
+
],
|
|
191
|
+
life: [
|
|
192
|
+
100,
|
|
193
|
+
60,
|
|
194
|
+
.2
|
|
195
|
+
],
|
|
196
|
+
warning: [
|
|
197
|
+
220,
|
|
198
|
+
180,
|
|
199
|
+
.14
|
|
200
|
+
],
|
|
201
|
+
dice: [
|
|
202
|
+
240,
|
|
203
|
+
100,
|
|
204
|
+
.075
|
|
205
|
+
]
|
|
206
|
+
};
|
|
207
|
+
var PALETTES = {
|
|
208
|
+
wood: [
|
|
209
|
+
"triangle",
|
|
210
|
+
.8,
|
|
211
|
+
.055
|
|
212
|
+
],
|
|
213
|
+
paper: [
|
|
214
|
+
"sine",
|
|
215
|
+
.9,
|
|
216
|
+
.038
|
|
217
|
+
],
|
|
218
|
+
glass: [
|
|
219
|
+
"sine",
|
|
220
|
+
1.5,
|
|
221
|
+
.04
|
|
222
|
+
],
|
|
223
|
+
arcade: [
|
|
224
|
+
"square",
|
|
225
|
+
1,
|
|
226
|
+
.028
|
|
227
|
+
],
|
|
228
|
+
terminal: [
|
|
229
|
+
"triangle",
|
|
230
|
+
1.25,
|
|
231
|
+
.035
|
|
232
|
+
],
|
|
233
|
+
garden: [
|
|
234
|
+
"sine",
|
|
235
|
+
1.1,
|
|
236
|
+
.042
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
var tonesFor = (family, cue, variation = 0) => {
|
|
240
|
+
const notes = NOTES[cue];
|
|
241
|
+
if (notes === void 0) return [];
|
|
242
|
+
const [wave, pitch, strength] = PALETTES[family];
|
|
243
|
+
const shift = 2 ** (Math.max(-4, Math.min(8, Number.isFinite(variation) ? variation : 0)) / 12);
|
|
244
|
+
const [from, to, duration] = notes;
|
|
245
|
+
const chord = [
|
|
246
|
+
"success",
|
|
247
|
+
"level",
|
|
248
|
+
"capture",
|
|
249
|
+
"cup",
|
|
250
|
+
"power"
|
|
251
|
+
].includes(cue);
|
|
252
|
+
return (chord ? [
|
|
253
|
+
1,
|
|
254
|
+
1.25,
|
|
255
|
+
1.5
|
|
256
|
+
] : [1]).map((n, index) => ({
|
|
257
|
+
from: from * pitch * shift * n,
|
|
258
|
+
to: to * pitch * shift * n,
|
|
259
|
+
duration,
|
|
260
|
+
wave,
|
|
261
|
+
delay: index * .045,
|
|
262
|
+
strength: chord ? strength * .7 : strength
|
|
263
|
+
}));
|
|
264
|
+
};
|
|
265
|
+
/** Redacted, already-validated events only. This mapping never examines a hidden answer. */
|
|
266
|
+
var eventCue = (gameId, type) => {
|
|
267
|
+
if (gameId === "shift-23") return {
|
|
268
|
+
Drew: "card",
|
|
269
|
+
Swapped: "card",
|
|
270
|
+
Sheltered: "card",
|
|
271
|
+
Dice: "dice",
|
|
272
|
+
Shifted: "power",
|
|
273
|
+
BetTo: "move",
|
|
274
|
+
Matched: "move",
|
|
275
|
+
WentAllIn: "move",
|
|
276
|
+
Penalty: "error",
|
|
277
|
+
HandEnded: "clear"
|
|
278
|
+
}[type] ?? null;
|
|
279
|
+
if (gameId === "crossword" || gameId === "sudoku") {
|
|
280
|
+
if ([
|
|
281
|
+
"letterSet",
|
|
282
|
+
"digitSet",
|
|
283
|
+
"noteToggled",
|
|
284
|
+
"cleared"
|
|
285
|
+
].includes(type)) return "tick";
|
|
286
|
+
if (["answerFilled", "unitCompleted"].includes(type)) return "clear";
|
|
287
|
+
}
|
|
288
|
+
if (gameId === "containment") return {
|
|
289
|
+
wallStarted: "trace",
|
|
290
|
+
wallBroken: "life",
|
|
291
|
+
wallFinished: "hit",
|
|
292
|
+
sealed: "capture",
|
|
293
|
+
cleared: "level",
|
|
294
|
+
started: "start",
|
|
295
|
+
over: "lose"
|
|
296
|
+
}[type] ?? null;
|
|
297
|
+
if (gameId === "greed") return {
|
|
298
|
+
threw: "dice",
|
|
299
|
+
rolled: "dice",
|
|
300
|
+
kept: "move",
|
|
301
|
+
banked: "clear",
|
|
302
|
+
won: "success"
|
|
303
|
+
}[type] ?? null;
|
|
304
|
+
if ([
|
|
305
|
+
"placed",
|
|
306
|
+
"moved",
|
|
307
|
+
"played",
|
|
308
|
+
"dropped",
|
|
309
|
+
"added",
|
|
310
|
+
"set",
|
|
311
|
+
"painted",
|
|
312
|
+
"digitSet",
|
|
313
|
+
"letterSet",
|
|
314
|
+
"flipped",
|
|
315
|
+
"turned",
|
|
316
|
+
"selected",
|
|
317
|
+
"rotated",
|
|
318
|
+
"joined",
|
|
319
|
+
"linked",
|
|
320
|
+
"drawn",
|
|
321
|
+
"connected",
|
|
322
|
+
"marked",
|
|
323
|
+
"sowed",
|
|
324
|
+
"guessed",
|
|
325
|
+
"answered",
|
|
326
|
+
"guessMade"
|
|
327
|
+
].includes(type)) return "move";
|
|
328
|
+
if ([
|
|
329
|
+
"drew",
|
|
330
|
+
"dealerDrew",
|
|
331
|
+
"dealt",
|
|
332
|
+
"shuffled",
|
|
333
|
+
"redealt",
|
|
334
|
+
"discarded",
|
|
335
|
+
"cardPlayed"
|
|
336
|
+
].includes(type)) return "card";
|
|
337
|
+
if ([
|
|
338
|
+
"rolled",
|
|
339
|
+
"threw",
|
|
340
|
+
"roll"
|
|
341
|
+
].includes(type)) return "dice";
|
|
342
|
+
if ([
|
|
343
|
+
"removed",
|
|
344
|
+
"cleared",
|
|
345
|
+
"matched",
|
|
346
|
+
"lineCompleted",
|
|
347
|
+
"unitCompleted",
|
|
348
|
+
"scored",
|
|
349
|
+
"wordFound",
|
|
350
|
+
"found",
|
|
351
|
+
"letterFound",
|
|
352
|
+
"banked",
|
|
353
|
+
"settled",
|
|
354
|
+
"captured",
|
|
355
|
+
"merged",
|
|
356
|
+
"ate",
|
|
357
|
+
"foodEaten",
|
|
358
|
+
"bonusFound",
|
|
359
|
+
"boxCompleted",
|
|
360
|
+
"trickWon",
|
|
361
|
+
"booked",
|
|
362
|
+
"swept",
|
|
363
|
+
"allConnected"
|
|
364
|
+
].includes(type)) return "clear";
|
|
365
|
+
if ([
|
|
366
|
+
"wrong",
|
|
367
|
+
"close",
|
|
368
|
+
"mismatched",
|
|
369
|
+
"notQuite",
|
|
370
|
+
"fullButWrong",
|
|
371
|
+
"letterMissed",
|
|
372
|
+
"wordMissed",
|
|
373
|
+
"detonated",
|
|
374
|
+
"bust",
|
|
375
|
+
"busted",
|
|
376
|
+
"crashed",
|
|
377
|
+
"lifeLost"
|
|
378
|
+
].includes(type)) return "error";
|
|
379
|
+
if ([
|
|
380
|
+
"levelUp",
|
|
381
|
+
"rankReached",
|
|
382
|
+
"milestone",
|
|
383
|
+
"reached2048",
|
|
384
|
+
"levelCleared"
|
|
385
|
+
].includes(type)) return "level";
|
|
386
|
+
if ([
|
|
387
|
+
"won",
|
|
388
|
+
"solved",
|
|
389
|
+
"completed",
|
|
390
|
+
"finished",
|
|
391
|
+
"handWon",
|
|
392
|
+
"roundWon",
|
|
393
|
+
"courseFinished"
|
|
394
|
+
].includes(type)) return "success";
|
|
395
|
+
if (["lost", "overflow"].includes(type)) return "lose";
|
|
396
|
+
if ([
|
|
397
|
+
"began",
|
|
398
|
+
"started",
|
|
399
|
+
"launched"
|
|
400
|
+
].includes(type)) return "start";
|
|
401
|
+
if ([
|
|
402
|
+
"returned",
|
|
403
|
+
"paddleHit",
|
|
404
|
+
"wallBounce",
|
|
405
|
+
"brickHit",
|
|
406
|
+
"wallHit"
|
|
407
|
+
].includes(type)) return "hit";
|
|
408
|
+
return null;
|
|
409
|
+
};
|
|
410
|
+
//#endregion
|
|
411
|
+
export { CUSTOM_AUDIO_GAMES, SOUND_PROFILES, eventCue, tonesFor };
|
|
412
|
+
|
|
413
|
+
//# sourceMappingURL=audio-sounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-sounds.js","names":[],"sources":["../../src/browser/audio-sounds.ts"],"sourcesContent":["import type { SoundCue } from '@joystyk/contracts';\n\ntype Waveform = 'sine' | 'triangle' | 'square' | 'sawtooth';\nexport type SoundFamily = 'wood' | 'paper' | 'glass' | 'arcade' | 'terminal' | 'garden';\n/** Deliberate palette per game; no samples, music, network downloads or third-party recordings. */\nexport const SOUND_PROFILES: Readonly<Record<string, SoundFamily>> = {\n 'garden-crossing': 'garden',\n 'tower-stacker': 'wood',\n 'rock-field': 'arcade',\n '2048': 'wood',\n backgammon: 'wood',\n blackjack: 'paper',\n 'brick-break': 'arcade',\n checkers: 'wood',\n chess: 'wood',\n 'chinese-checkers': 'glass',\n 'circuit-84': 'terminal',\n 'clue-teams': 'paper',\n 'code-breaker': 'glass',\n containment: 'arcade',\n 'crazy-eights': 'paper',\n cribbage: 'wood',\n crossword: 'paper',\n 'daily-word': 'paper',\n dominoes: 'wood',\n 'dots-and-boxes': 'paper',\n 'drop-four': 'wood',\n equation: 'glass',\n euchre: 'paper',\n 'five-dice': 'wood',\n 'four-groups': 'glass',\n freecell: 'paper',\n 'gem-swap': 'glass',\n 'gin-rummy': 'paper',\n go: 'glass',\n 'go-fish': 'paper',\n golf: 'paper',\n greed: 'wood',\n hangman: 'paper',\n hearts: 'paper',\n honeycomb: 'garden',\n klondike: 'paper',\n 'letter-drop': 'wood',\n 'liars-dice': 'wood',\n 'light-trails': 'arcade',\n ludo: 'wood',\n mancala: 'wood',\n 'map-guess': 'garden',\n 'memory-match': 'paper',\n minesweeper: 'glass',\n 'mini-golf': 'garden',\n 'naval-battle': 'arcade',\n nim: 'wood',\n nonogram: 'paper',\n 'old-maid': 'paper',\n paddle: 'arcade',\n pig: 'wood',\n 'pipe-link': 'glass',\n pyramid: 'paper',\n queens: 'glass',\n reversi: 'wood',\n 'rock-paper-scissors': 'paper',\n 'shift-23': 'terminal',\n 'shut-the-box': 'wood',\n snake: 'arcade',\n spades: 'paper',\n spider: 'paper',\n sudoku: 'paper',\n 'tic-tac-toe': 'paper',\n 'tile-match': 'glass',\n tripeaks: 'paper',\n war: 'paper',\n 'word-flux': 'garden',\n 'word-hunt': 'garden',\n 'word-ladder': 'wood',\n 'word-of-the-day': 'paper',\n};\n\n/** Games which already select sound effects in their own controls and drivers. */\nexport const CUSTOM_AUDIO_GAMES = new Set([\n 'garden-crossing',\n 'tower-stacker',\n 'rock-field',\n 'snake',\n 'paddle',\n 'light-trails',\n 'containment',\n 'circuit-84',\n 'brick-break',\n 'gem-swap',\n 'mini-golf',\n 'klondike',\n 'freecell',\n 'spider',\n 'pyramid',\n 'tripeaks',\n 'golf',\n]);\nexport type Tone = {\n readonly from: number;\n readonly to: number;\n readonly duration: number;\n readonly wave: Waveform;\n readonly delay: number;\n readonly strength: number;\n};\nconst NOTES: Record<SoundCue, readonly [number, number, number]> = {\n move: [220, 140, 0.045],\n card: [170, 100, 0.045],\n clear: [350, 120, 0.14],\n success: [660, 880, 0.18],\n error: [160, 100, 0.11],\n lose: [180, 70, 0.22],\n level: [520, 1040, 0.25],\n start: [440, 660, 0.13],\n tick: [900, 600, 0.025],\n capture: [440, 880, 0.16],\n hit: [260, 80, 0.06],\n paddle: [140, 80, 0.065],\n power: [440, 880, 0.12],\n stroke: [170, 65, 0.07],\n rail: [240, 100, 0.07],\n water: [95, 60, 0.14],\n cup: [450, 850, 0.16],\n cascade: [300, 170, 0.1],\n trace: [150, 180, 0.035],\n life: [100, 60, 0.2],\n warning: [220, 180, 0.14],\n dice: [240, 100, 0.075],\n};\nconst PALETTES: Record<SoundFamily, readonly [Waveform, number, number]> = {\n wood: ['triangle', 0.8, 0.055],\n paper: ['sine', 0.9, 0.038],\n glass: ['sine', 1.5, 0.04],\n arcade: ['square', 1, 0.028],\n terminal: ['triangle', 1.25, 0.035],\n garden: ['sine', 1.1, 0.042],\n};\nexport const tonesFor = (family: SoundFamily, cue: SoundCue, variation = 0): readonly Tone[] => {\n const notes = NOTES[cue];\n if (notes === undefined) return [];\n const [wave, pitch, strength] = PALETTES[family];\n const shift = 2 ** (Math.max(-4, Math.min(8, Number.isFinite(variation) ? variation : 0)) / 12);\n const [from, to, duration] = notes;\n const chord = ['success', 'level', 'capture', 'cup', 'power'].includes(cue);\n return (chord ? [1, 1.25, 1.5] : [1]).map((n, index) => ({\n from: from * pitch * shift * n,\n to: to * pitch * shift * n,\n duration,\n wave,\n delay: index * 0.045,\n strength: chord ? strength * 0.7 : strength,\n }));\n};\n\n/** Redacted, already-validated events only. This mapping never examines a hidden answer. */\nexport const eventCue = (gameId: string, type: string): SoundCue | null => {\n if (gameId === 'shift-23')\n return (\n (\n {\n Drew: 'card',\n Swapped: 'card',\n Sheltered: 'card',\n Dice: 'dice',\n Shifted: 'power',\n BetTo: 'move',\n Matched: 'move',\n WentAllIn: 'move',\n Penalty: 'error',\n HandEnded: 'clear',\n } as const\n )[type as 'Drew'] ?? null\n );\n if (gameId === 'crossword' || gameId === 'sudoku') {\n if (['letterSet', 'digitSet', 'noteToggled', 'cleared'].includes(type)) return 'tick';\n if (['answerFilled', 'unitCompleted'].includes(type)) return 'clear';\n }\n if (gameId === 'containment')\n return (\n (\n {\n wallStarted: 'trace',\n wallBroken: 'life',\n wallFinished: 'hit',\n sealed: 'capture',\n cleared: 'level',\n started: 'start',\n over: 'lose',\n } as const\n )[type as 'wallStarted'] ?? null\n );\n if (gameId === 'greed')\n return (\n ({ threw: 'dice', rolled: 'dice', kept: 'move', banked: 'clear', won: 'success' } as const)[type as 'threw'] ??\n null\n );\n if (\n [\n 'placed',\n 'moved',\n 'played',\n 'dropped',\n 'added',\n 'set',\n 'painted',\n 'digitSet',\n 'letterSet',\n 'flipped',\n 'turned',\n 'selected',\n 'rotated',\n 'joined',\n 'linked',\n 'drawn',\n 'connected',\n 'marked',\n 'sowed',\n 'guessed',\n 'answered',\n 'guessMade',\n ].includes(type)\n )\n return 'move';\n if (['drew', 'dealerDrew', 'dealt', 'shuffled', 'redealt', 'discarded', 'cardPlayed'].includes(type)) return 'card';\n if (['rolled', 'threw', 'roll'].includes(type)) return 'dice';\n if (\n [\n 'removed',\n 'cleared',\n 'matched',\n 'lineCompleted',\n 'unitCompleted',\n 'scored',\n 'wordFound',\n 'found',\n 'letterFound',\n 'banked',\n 'settled',\n 'captured',\n 'merged',\n 'ate',\n 'foodEaten',\n 'bonusFound',\n 'boxCompleted',\n 'trickWon',\n 'booked',\n 'swept',\n 'allConnected',\n ].includes(type)\n )\n return 'clear';\n if (\n [\n 'wrong',\n 'close',\n 'mismatched',\n 'notQuite',\n 'fullButWrong',\n 'letterMissed',\n 'wordMissed',\n 'detonated',\n 'bust',\n 'busted',\n 'crashed',\n 'lifeLost',\n ].includes(type)\n )\n return 'error';\n if (['levelUp', 'rankReached', 'milestone', 'reached2048', 'levelCleared'].includes(type)) return 'level';\n if (['won', 'solved', 'completed', 'finished', 'handWon', 'roundWon', 'courseFinished'].includes(type))\n return 'success';\n if (['lost', 'overflow'].includes(type)) return 'lose';\n if (['began', 'started', 'launched'].includes(type)) return 'start';\n if (['returned', 'paddleHit', 'wallBounce', 'brickHit', 'wallHit'].includes(type)) return 'hit';\n // Connection, seat, setup, pass, timer and hint events are deliberately silent.\n return null;\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AnyGameSession, AudioApi, PlatformApi } from '@joystyk/contracts';
|
|
2
|
+
export interface GameAudioService extends AudioApi {
|
|
3
|
+
dispose(): void;
|
|
4
|
+
}
|
|
5
|
+
/** Browser ports are injectable so lifecycle races can be checked without pretending to hear sound. */
|
|
6
|
+
export type AudioEnvironment = {
|
|
7
|
+
readonly document: EventTarget & {
|
|
8
|
+
readonly hidden: boolean;
|
|
9
|
+
};
|
|
10
|
+
readonly window: EventTarget;
|
|
11
|
+
createContext(): AudioContext;
|
|
12
|
+
};
|
|
13
|
+
/** One context per mounted game. Muting never queues sounds for a later unmute. */
|
|
14
|
+
export declare const createGameAudio: (preferences: PlatformApi["preferences"], gameId: string, environment?: AudioEnvironment) => GameAudioService;
|
|
15
|
+
/** The frame's generic game sound path. Games with local sound controls keep their own driver. */
|
|
16
|
+
export declare const watchGameAudio: (session: AnyGameSession, platform: PlatformApi, gameId: string) => (() => void);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { decode, isRecord } from "../values.js";
|
|
2
|
+
import { CUSTOM_AUDIO_GAMES, SOUND_PROFILES, eventCue, tonesFor } from "./audio-sounds.js";
|
|
3
|
+
//#region src/browser/audio.ts
|
|
4
|
+
/** One context per mounted game. Muting never queues sounds for a later unmute. */
|
|
5
|
+
var createGameAudio = (preferences, gameId, environment = {
|
|
6
|
+
document,
|
|
7
|
+
window,
|
|
8
|
+
createContext: () => new AudioContext()
|
|
9
|
+
}) => {
|
|
10
|
+
const family = SOUND_PROFILES[gameId] ?? "wood";
|
|
11
|
+
const { document: doc, window: win } = environment;
|
|
12
|
+
let disposed = false, unlocked = false, context = null, master = null;
|
|
13
|
+
let resuming = null, epoch = 0, windowStart = -Infinity, count = 0;
|
|
14
|
+
const voices = /* @__PURE__ */ new Map();
|
|
15
|
+
const permitted = () => {
|
|
16
|
+
const p = preferences.get();
|
|
17
|
+
return !disposed && !doc.hidden && p.sound && Number.isFinite(p.volume) && p.volume > 0;
|
|
18
|
+
};
|
|
19
|
+
const silence = () => {
|
|
20
|
+
epoch += 1;
|
|
21
|
+
for (const [oscillator, gain] of voices) {
|
|
22
|
+
oscillator.onended = null;
|
|
23
|
+
try {
|
|
24
|
+
oscillator.stop();
|
|
25
|
+
} catch {}
|
|
26
|
+
oscillator.disconnect();
|
|
27
|
+
gain.disconnect();
|
|
28
|
+
}
|
|
29
|
+
voices.clear();
|
|
30
|
+
if (context?.state === "running") context.suspend().catch(() => {});
|
|
31
|
+
};
|
|
32
|
+
const applyPreferences = () => {
|
|
33
|
+
if (!permitted()) {
|
|
34
|
+
silence();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (context !== null && master !== null) master.gain.setValueAtTime(Math.max(0, Math.min(1, preferences.get().volume)), context.currentTime);
|
|
38
|
+
};
|
|
39
|
+
const wake = () => {
|
|
40
|
+
if (!unlocked || !permitted()) return Promise.resolve();
|
|
41
|
+
try {
|
|
42
|
+
if (context === null) {
|
|
43
|
+
context = environment.createContext();
|
|
44
|
+
master = context.createGain();
|
|
45
|
+
master.connect(context.destination);
|
|
46
|
+
applyPreferences();
|
|
47
|
+
}
|
|
48
|
+
if (context.state === "suspended") {
|
|
49
|
+
resuming ??= context.resume().then(() => {
|
|
50
|
+
if (!permitted()) silence();
|
|
51
|
+
}).catch(() => {}).finally(() => {
|
|
52
|
+
resuming = null;
|
|
53
|
+
});
|
|
54
|
+
return resuming;
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
};
|
|
59
|
+
const gesture = (event) => {
|
|
60
|
+
if (!event.isTrusted || event instanceof KeyboardEvent && (event.repeat || event.ctrlKey || event.metaKey || event.altKey)) return;
|
|
61
|
+
unlocked = true;
|
|
62
|
+
wake();
|
|
63
|
+
};
|
|
64
|
+
const hidden = () => {
|
|
65
|
+
if (doc.hidden) silence();
|
|
66
|
+
};
|
|
67
|
+
doc.addEventListener("pointerdown", gesture, { capture: true });
|
|
68
|
+
doc.addEventListener("keydown", gesture, { capture: true });
|
|
69
|
+
doc.addEventListener("visibilitychange", hidden);
|
|
70
|
+
win.addEventListener("pagehide", silence);
|
|
71
|
+
win.addEventListener("blur", silence);
|
|
72
|
+
const unsubscribe = preferences.subscribe(applyPreferences);
|
|
73
|
+
return {
|
|
74
|
+
play: (cue, variation = 0) => {
|
|
75
|
+
if (!unlocked || !permitted()) return;
|
|
76
|
+
const requestedAt = epoch;
|
|
77
|
+
wake().then(() => {
|
|
78
|
+
const ctx = context, bus = master;
|
|
79
|
+
if (requestedAt !== epoch || !permitted() || ctx === null || bus === null || ctx.state !== "running") return;
|
|
80
|
+
if (ctx.currentTime - windowStart >= .1) {
|
|
81
|
+
windowStart = ctx.currentTime;
|
|
82
|
+
count = 0;
|
|
83
|
+
}
|
|
84
|
+
if (count >= 8) return;
|
|
85
|
+
const tones = tonesFor(family, cue, variation);
|
|
86
|
+
if (voices.size + tones.length > 16) return;
|
|
87
|
+
count += 1;
|
|
88
|
+
try {
|
|
89
|
+
for (const tone of tones) {
|
|
90
|
+
const oscillator = ctx.createOscillator(), gain = ctx.createGain(), now = ctx.currentTime + tone.delay;
|
|
91
|
+
oscillator.type = tone.wave;
|
|
92
|
+
oscillator.frequency.setValueAtTime(tone.from, now);
|
|
93
|
+
oscillator.frequency.exponentialRampToValueAtTime(tone.to, now + tone.duration);
|
|
94
|
+
gain.gain.setValueAtTime(1e-4, now);
|
|
95
|
+
gain.gain.linearRampToValueAtTime(tone.strength, now + .004);
|
|
96
|
+
gain.gain.exponentialRampToValueAtTime(1e-4, now + tone.duration);
|
|
97
|
+
oscillator.connect(gain);
|
|
98
|
+
gain.connect(bus);
|
|
99
|
+
voices.set(oscillator, gain);
|
|
100
|
+
oscillator.onended = () => {
|
|
101
|
+
voices.delete(oscillator);
|
|
102
|
+
oscillator.disconnect();
|
|
103
|
+
gain.disconnect();
|
|
104
|
+
};
|
|
105
|
+
oscillator.start(now);
|
|
106
|
+
oscillator.stop(now + tone.duration + .01);
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
dispose: () => {
|
|
112
|
+
if (disposed) return;
|
|
113
|
+
disposed = true;
|
|
114
|
+
silence();
|
|
115
|
+
unsubscribe();
|
|
116
|
+
doc.removeEventListener("pointerdown", gesture, { capture: true });
|
|
117
|
+
doc.removeEventListener("keydown", gesture, { capture: true });
|
|
118
|
+
doc.removeEventListener("visibilitychange", hidden);
|
|
119
|
+
win.removeEventListener("pagehide", silence);
|
|
120
|
+
win.removeEventListener("blur", silence);
|
|
121
|
+
master?.disconnect();
|
|
122
|
+
master = null;
|
|
123
|
+
const closing = context;
|
|
124
|
+
context = null;
|
|
125
|
+
if (closing !== null && closing.state !== "closed") closing.close().catch(() => {});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
/** A narrow Standard Schema projection: game-specific schemas already validated the public events. */
|
|
130
|
+
var audioSnapshot = { "~standard": {
|
|
131
|
+
version: 1,
|
|
132
|
+
vendor: "joystyk",
|
|
133
|
+
validate: (raw) => {
|
|
134
|
+
const invalid = { issues: [{ message: "Invalid audio snapshot." }] };
|
|
135
|
+
if (!isRecord(raw) || !isRecord(raw["meta"])) return invalid;
|
|
136
|
+
const meta = raw["meta"], revision = meta["revision"];
|
|
137
|
+
if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 0) return invalid;
|
|
138
|
+
const types = [];
|
|
139
|
+
for (const event of Array.isArray(raw["events"]) ? raw["events"] : []) {
|
|
140
|
+
if (!isRecord(event)) return invalid;
|
|
141
|
+
const type = event["type"] ?? event["_tag"];
|
|
142
|
+
if (typeof type !== "string") return invalid;
|
|
143
|
+
types.push(type);
|
|
144
|
+
}
|
|
145
|
+
for (const beat of Array.isArray(raw["beats"]) ? raw["beats"] : []) {
|
|
146
|
+
if (!isRecord(beat) || !isRecord(beat["event"])) return invalid;
|
|
147
|
+
const type = beat["event"]["type"] ?? beat["event"]["_tag"];
|
|
148
|
+
if (typeof type !== "string") return invalid;
|
|
149
|
+
types.push(type);
|
|
150
|
+
}
|
|
151
|
+
const outcome = isRecord(meta["outcome"]) && typeof meta["outcome"]["result"] === "string" ? meta["outcome"]["result"] : null;
|
|
152
|
+
return { value: {
|
|
153
|
+
revision,
|
|
154
|
+
paused: raw["paused"] === true,
|
|
155
|
+
types,
|
|
156
|
+
outcome,
|
|
157
|
+
refused: meta["notice"] !== null && meta["notice"] !== void 0
|
|
158
|
+
} };
|
|
159
|
+
}
|
|
160
|
+
} };
|
|
161
|
+
/** The frame's generic game sound path. Games with local sound controls keep their own driver. */
|
|
162
|
+
var watchGameAudio = (session, platform, gameId) => {
|
|
163
|
+
if (platform.audio === void 0 || CUSTOM_AUDIO_GAMES.has(gameId)) return () => {};
|
|
164
|
+
let previous = null;
|
|
165
|
+
return session.subscribe((raw) => {
|
|
166
|
+
const parsed = decode(audioSnapshot, raw, "sound snapshot");
|
|
167
|
+
if (!parsed.ok) return;
|
|
168
|
+
const next = parsed.value, before = previous;
|
|
169
|
+
previous = next;
|
|
170
|
+
if (before === null || next.revision <= before.revision || next.paused) return;
|
|
171
|
+
const cues = next.types.flatMap((type) => {
|
|
172
|
+
const cue = eventCue(gameId, type);
|
|
173
|
+
return cue === null ? [] : [cue];
|
|
174
|
+
});
|
|
175
|
+
if (next.outcome !== null && before.outcome === null && !cues.some((cue) => cue === "success" || cue === "lose")) cues.push(next.outcome === "lost" || next.outcome === "abandoned" ? "lose" : "success");
|
|
176
|
+
for (const cue of cues.slice(0, 8)) platform.audio?.play(cue);
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
//#endregion
|
|
180
|
+
export { createGameAudio, watchGameAudio };
|
|
181
|
+
|
|
182
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.js","names":[],"sources":["../../src/browser/audio.ts"],"sourcesContent":["import type { AnyGameSession, AudioApi, PlatformApi, Validator } from '@joystyk/contracts';\nimport { decode, isRecord } from '../values.ts';\nimport { CUSTOM_AUDIO_GAMES, SOUND_PROFILES, eventCue, tonesFor, type SoundFamily } from './audio-sounds.ts';\n\nexport interface GameAudioService extends AudioApi {\n dispose(): void;\n}\n/** Browser ports are injectable so lifecycle races can be checked without pretending to hear sound. */\nexport type AudioEnvironment = {\n readonly document: EventTarget & { readonly hidden: boolean };\n readonly window: EventTarget;\n createContext(): AudioContext;\n};\n\n/** One context per mounted game. Muting never queues sounds for a later unmute. */\nexport const createGameAudio = (\n preferences: PlatformApi['preferences'],\n gameId: string,\n environment: AudioEnvironment = { document, window, createContext: () => new AudioContext() },\n): GameAudioService => {\n const family: SoundFamily = SOUND_PROFILES[gameId] ?? 'wood';\n const { document: doc, window: win } = environment;\n let disposed = false,\n unlocked = false,\n context: AudioContext | null = null,\n master: GainNode | null = null;\n let resuming: Promise<void> | null = null,\n epoch = 0,\n windowStart = -Infinity,\n count = 0;\n const voices = new Map<OscillatorNode, GainNode>();\n const permitted = (): boolean => {\n const p = preferences.get();\n return !disposed && !doc.hidden && p.sound && Number.isFinite(p.volume) && p.volume > 0;\n };\n const silence = (): void => {\n epoch += 1;\n for (const [oscillator, gain] of voices) {\n oscillator.onended = null;\n try {\n oscillator.stop();\n } catch {\n /* Already finished. */\n }\n oscillator.disconnect();\n gain.disconnect();\n }\n voices.clear();\n if (context?.state === 'running') void context.suspend().catch(() => {});\n };\n const applyPreferences = (): void => {\n if (!permitted()) {\n silence();\n return;\n }\n if (context !== null && master !== null)\n master.gain.setValueAtTime(Math.max(0, Math.min(1, preferences.get().volume)), context.currentTime);\n };\n const wake = (): Promise<void> => {\n if (!unlocked || !permitted()) return Promise.resolve();\n try {\n if (context === null) {\n context = environment.createContext();\n master = context.createGain();\n master.connect(context.destination);\n applyPreferences();\n }\n if (context.state === 'suspended') {\n resuming ??= context\n .resume()\n .then(() => {\n if (!permitted()) silence();\n })\n .catch(() => {})\n .finally(() => {\n resuming = null;\n });\n return resuming;\n }\n } catch {\n /* Unsupported audio and autoplay refusal leave the game playable. */\n }\n return Promise.resolve();\n };\n const gesture = (event: Event): void => {\n if (\n !event.isTrusted ||\n (event instanceof KeyboardEvent && (event.repeat || event.ctrlKey || event.metaKey || event.altKey))\n )\n return;\n unlocked = true;\n void wake();\n };\n const hidden = (): void => {\n if (doc.hidden) silence();\n };\n doc.addEventListener('pointerdown', gesture, { capture: true });\n doc.addEventListener('keydown', gesture, { capture: true });\n doc.addEventListener('visibilitychange', hidden);\n win.addEventListener('pagehide', silence);\n win.addEventListener('blur', silence);\n const unsubscribe = preferences.subscribe(applyPreferences);\n\n return {\n play: (cue, variation = 0) => {\n if (!unlocked || !permitted()) return;\n const requestedAt = epoch;\n void wake().then(() => {\n const ctx = context,\n bus = master;\n if (requestedAt !== epoch || !permitted() || ctx === null || bus === null || ctx.state !== 'running') return;\n // Dense cascades and catch-up frames remain gentle, bounded effects.\n if (ctx.currentTime - windowStart >= 0.1) {\n windowStart = ctx.currentTime;\n count = 0;\n }\n if (count >= 8) return;\n const tones = tonesFor(family, cue, variation);\n if (voices.size + tones.length > 16) return;\n count += 1;\n try {\n for (const tone of tones) {\n const oscillator = ctx.createOscillator(),\n gain = ctx.createGain(),\n now = ctx.currentTime + tone.delay;\n oscillator.type = tone.wave;\n oscillator.frequency.setValueAtTime(tone.from, now);\n oscillator.frequency.exponentialRampToValueAtTime(tone.to, now + tone.duration);\n gain.gain.setValueAtTime(0.0001, now);\n gain.gain.linearRampToValueAtTime(tone.strength, now + 0.004);\n gain.gain.exponentialRampToValueAtTime(0.0001, now + tone.duration);\n oscillator.connect(gain);\n gain.connect(bus);\n voices.set(oscillator, gain);\n oscillator.onended = () => {\n voices.delete(oscillator);\n oscillator.disconnect();\n gain.disconnect();\n };\n oscillator.start(now);\n oscillator.stop(now + tone.duration + 0.01);\n }\n } catch {\n /* An audio-device change must not turn a harmless effect into a game error. */\n }\n });\n },\n dispose: () => {\n if (disposed) return;\n disposed = true;\n silence();\n unsubscribe();\n doc.removeEventListener('pointerdown', gesture, { capture: true });\n doc.removeEventListener('keydown', gesture, { capture: true });\n doc.removeEventListener('visibilitychange', hidden);\n win.removeEventListener('pagehide', silence);\n win.removeEventListener('blur', silence);\n master?.disconnect();\n master = null;\n const closing = context;\n context = null;\n if (closing !== null && closing.state !== 'closed') void closing.close().catch(() => {});\n },\n };\n};\n\ntype AudioSnapshot = {\n readonly revision: number;\n readonly paused: boolean;\n readonly types: readonly string[];\n readonly outcome: string | null;\n readonly refused: boolean;\n};\n/** A narrow Standard Schema projection: game-specific schemas already validated the public events. */\nconst audioSnapshot: Validator<AudioSnapshot> = {\n '~standard': {\n version: 1,\n vendor: 'joystyk',\n validate: (raw) => {\n const invalid = { issues: [{ message: 'Invalid audio snapshot.' }] };\n if (!isRecord(raw) || !isRecord(raw['meta'])) return invalid;\n const meta = raw['meta'],\n revision = meta['revision'];\n if (typeof revision !== 'number' || !Number.isSafeInteger(revision) || revision < 0) return invalid;\n const types: string[] = [];\n for (const event of Array.isArray(raw['events']) ? raw['events'] : []) {\n if (!isRecord(event)) return invalid;\n const type = event['type'] ?? event['_tag'];\n if (typeof type !== 'string') return invalid;\n types.push(type);\n }\n for (const beat of Array.isArray(raw['beats']) ? raw['beats'] : []) {\n if (!isRecord(beat) || !isRecord(beat['event'])) return invalid;\n const type = beat['event']['type'] ?? beat['event']['_tag'];\n if (typeof type !== 'string') return invalid;\n types.push(type);\n }\n const outcome =\n isRecord(meta['outcome']) && typeof meta['outcome']['result'] === 'string' ? meta['outcome']['result'] : null;\n return {\n value: {\n revision,\n paused: raw['paused'] === true,\n types,\n outcome,\n refused: meta['notice'] !== null && meta['notice'] !== undefined,\n },\n };\n },\n },\n};\n\n/** The frame's generic game sound path. Games with local sound controls keep their own driver. */\nexport const watchGameAudio = (session: AnyGameSession, platform: PlatformApi, gameId: string): (() => void) => {\n if (platform.audio === undefined || CUSTOM_AUDIO_GAMES.has(gameId)) return () => {};\n let previous: AudioSnapshot | null = null;\n return session.subscribe((raw) => {\n const parsed = decode(audioSnapshot, raw, 'sound snapshot');\n if (!parsed.ok) return;\n const next = parsed.value,\n before = previous;\n previous = next;\n // Restored snapshots and repeated presentation/notice snapshots are silent.\n if (before === null || next.revision <= before.revision || next.paused) return;\n const cues = next.types.flatMap((type) => {\n const cue = eventCue(gameId, type);\n return cue === null ? [] : [cue];\n });\n if (next.outcome !== null && before.outcome === null && !cues.some((cue) => cue === 'success' || cue === 'lose'))\n cues.push(next.outcome === 'lost' || next.outcome === 'abandoned' ? 'lose' : 'success');\n for (const cue of cues.slice(0, 8)) platform.audio?.play(cue);\n });\n};\n"],"mappings":"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"}
|