@wallarm-org/design-system 0.64.1 → 0.65.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.
@@ -108,7 +108,7 @@ const GameHud = ({ caught, armed, roundOver, stats, accuracy, faced, catchKey, g
108
108
  style: {
109
109
  animation: 'hud-in 0.3s ease-out'
110
110
  },
111
- children: 'Click the red anomalies \u2014 catch 5 to arm the cannon, then \u2190 \u2192 move \u00B7 space fire'
111
+ children: `Click the red anomalies \u2014 catch 5 to arm the cannon \u00B7 m sound ${soundOn ? 'off' : 'on'}`
112
112
  })
113
113
  ]
114
114
  });
@@ -1,3 +1,5 @@
1
+ import type { GamePlugins } from './game-logic';
2
+ import type { RenderPlugins } from './game-renderer';
1
3
  import type { EngineOptions, GameStats } from './types';
2
4
  /**
3
5
  * Framework-agnostic "detection sweep" canvas engine.
@@ -31,5 +33,9 @@ export interface SweepEngine {
31
33
  } | null): void;
32
34
  celebrate(score: number): void;
33
35
  setSound(on: boolean): void;
36
+ setPlugins(plugins: {
37
+ game: GamePlugins;
38
+ render: RenderPlugins;
39
+ }): void;
34
40
  }
35
41
  export declare function createSweepEngine(canvas: HTMLCanvasElement, options: EngineOptions): SweepEngine;
@@ -226,7 +226,11 @@ function createSweepEngine(canvas, options) {
226
226
  if ('halftone' !== opts.texture || !running) return;
227
227
  game.celebrate(score);
228
228
  },
229
- setSound: (on)=>game.setSound(on)
229
+ setSound: (on)=>game.setSound(on),
230
+ setPlugins: (p)=>{
231
+ game.setPlugins(p.game);
232
+ gr.setRenderPlugins(p.render);
233
+ }
230
234
  };
231
235
  }
232
236
  export { createSweepEngine };
@@ -56,6 +56,18 @@ export interface CaughtEffect {
56
56
  label: string;
57
57
  }
58
58
  export type GameMode = 'idle' | 'armed' | 'over';
59
+ /** Plugin interface for lazily-loaded game modules (sfx + celebration). */
60
+ export interface GamePlugins {
61
+ playCoin?(): void;
62
+ playZap?(): void;
63
+ playPew?(): void;
64
+ playPowerUp?(): void;
65
+ playFanfare?(): void;
66
+ startCelebration?(score: number, t: number, host: GameEngineHost): CelState | null;
67
+ stepCelebration?(cel: CelState, t: number, dt: number, host: GameEngineHost, c1: string, c2: string): void;
68
+ adjustCelebrationTimeMarkers?(cel: CelState, skip: number): void;
69
+ tierForScore?(score: number): number;
70
+ }
59
71
  export interface GameEngineHost {
60
72
  w: number;
61
73
  h: number;
@@ -98,5 +110,6 @@ export interface GameLogic {
98
110
  } | null): void;
99
111
  celebrate(score: number): void;
100
112
  setSound(on: boolean): void;
113
+ setPlugins(p: GamePlugins): void;
101
114
  }
102
115
  export declare function createGameLogic(host: GameEngineHost): GameLogic;
@@ -1,7 +1,5 @@
1
- import { adjustCelebrationTimeMarkers, startCelebration, stepCelebration, tierForScore } from "./celebration.js";
2
1
  import { ANOMALY_VIS_THRESHOLD, DOT_STEP_BASE, DOT_STEP_SCALE, IDLE_MARGIN_X, IDLE_MARGIN_Y } from "./constants.js";
3
2
  import { sweepX } from "./engine-grid.js";
4
- import { playCoin, playFanfare, playPew, playPowerUp, playZap } from "./sfx.js";
5
3
  const ROUND_ATTACKS = 100;
6
4
  const MAX_TARGETS = 8;
7
5
  const MAX_BULLETS = 24;
@@ -62,6 +60,7 @@ function createGameLogic(host) {
62
60
  let roundDone = false;
63
61
  let lastSpawn = -1 / 0;
64
62
  let soundOn = false;
63
+ let plugins = {};
65
64
  let statsCb = null;
66
65
  let exclusionBox = null;
67
66
  function loadFont() {
@@ -191,8 +190,8 @@ function createGameLogic(host) {
191
190
  killTotal += 1;
192
191
  if ('armed' === state.gameMode) {
193
192
  roundKills += 1;
194
- if (soundOn) playZap();
195
- } else if (soundOn) playCoin();
193
+ if (soundOn) plugins.playZap?.();
194
+ } else if (soundOn) plugins.playCoin?.();
196
195
  emitStats();
197
196
  }
198
197
  function endRound() {
@@ -204,8 +203,8 @@ function createGameLogic(host) {
204
203
  const faced = roundKills + roundEscaped;
205
204
  const accuracy = faced > 0 ? Math.round(roundKills / faced * 100) : 100;
206
205
  const now = performance.now() / 1000;
207
- state.cel = startCelebration(accuracy, now, host);
208
- if (soundOn && tierForScore(accuracy) >= 1) playFanfare();
206
+ state.cel = plugins.startCelebration?.(accuracy, now, host) ?? null;
207
+ if (soundOn && (plugins.tierForScore?.(accuracy) ?? 0) >= 1) plugins.playFanfare?.();
209
208
  emitStats();
210
209
  }
211
210
  function isHittable(anomaly, t) {
@@ -250,7 +249,7 @@ function createGameLogic(host) {
250
249
  y: host.h - CANNON_BARREL_Y
251
250
  });
252
251
  lastFire = t;
253
- if (soundOn) playPew();
252
+ if (soundOn) plugins.playPew?.();
254
253
  }
255
254
  }
256
255
  function moveBullets(dt) {
@@ -345,7 +344,7 @@ function createGameLogic(host) {
345
344
  for (const effect of caughtEffects)effect.t0 += skip;
346
345
  if (state.armT > 0) state.armT += skip;
347
346
  if (lastSpawn > -1 / 0 && 0 !== lastSpawn) lastSpawn += skip;
348
- if (state.cel) adjustCelebrationTimeMarkers(state.cel, skip);
347
+ if (state.cel) plugins.adjustCelebrationTimeMarkers?.(state.cel, skip);
349
348
  }
350
349
  function resetRoundState() {
351
350
  roundKills = 0;
@@ -378,8 +377,8 @@ function createGameLogic(host) {
378
377
  state.cannonAway = false;
379
378
  anomalies.length = 0;
380
379
  state.armT = now;
381
- state.cel = startCelebration(score, now, host);
382
- if (soundOn && tierForScore(score) >= 1) playFanfare();
380
+ state.cel = plugins.startCelebration?.(score, now, host) ?? null;
381
+ if (soundOn && (plugins.tierForScore?.(score) ?? 0) >= 1) plugins.playFanfare?.();
383
382
  }
384
383
  function catchAt(x, y, running) {
385
384
  if (!running) return false;
@@ -418,7 +417,7 @@ function createGameLogic(host) {
418
417
  state.armT = now;
419
418
  lastSpawn = now;
420
419
  state.cannonX = host.w / 2;
421
- if (soundOn) playPowerUp();
420
+ if (soundOn) plugins.playPowerUp?.();
422
421
  emitStats();
423
422
  }
424
423
  function exitGame() {
@@ -458,7 +457,7 @@ function createGameLogic(host) {
458
457
  }
459
458
  function stepCel(t, dt, caughtColor, dotColor) {
460
459
  if (!state.cel) return;
461
- stepCelebration(state.cel, t, dt, host, caughtColor, dotColor);
460
+ plugins.stepCelebration?.(state.cel, t, dt, host, caughtColor, dotColor);
462
461
  if (state.cel.liftStarted) state.cannonAway = true;
463
462
  }
464
463
  return {
@@ -480,6 +479,9 @@ function createGameLogic(host) {
480
479
  setExclusion,
481
480
  celebrate,
482
481
  setSound,
482
+ setPlugins (p) {
483
+ plugins = p;
484
+ },
483
485
  get cel () {
484
486
  return state.cel;
485
487
  },
@@ -0,0 +1,6 @@
1
+ import type { GamePlugins } from './game-logic';
2
+ import type { RenderPlugins } from './game-renderer';
3
+ export declare function loadGamePlugins(): Promise<{
4
+ game: GamePlugins;
5
+ render: RenderPlugins;
6
+ }>;
@@ -0,0 +1,31 @@
1
+ let cached = null;
2
+ async function loadGamePlugins() {
3
+ if (cached) return cached;
4
+ const [sfx, cel, celR] = await Promise.all([
5
+ import("./sfx.js"),
6
+ import("./celebration.js"),
7
+ import("./celebration-renderer.js")
8
+ ]);
9
+ cached = {
10
+ game: {
11
+ playCoin: sfx.playCoin,
12
+ playZap: sfx.playZap,
13
+ playPew: sfx.playPew,
14
+ playPowerUp: sfx.playPowerUp,
15
+ playFanfare: sfx.playFanfare,
16
+ startCelebration: cel.startCelebration,
17
+ stepCelebration: cel.stepCelebration,
18
+ adjustCelebrationTimeMarkers: cel.adjustCelebrationTimeMarkers,
19
+ tierForScore: cel.tierForScore
20
+ },
21
+ render: {
22
+ CEL_CAUGHT_COL: cel.CEL_CAUGHT_COL,
23
+ celDotEffect: cel.celDotEffect,
24
+ computeCelFrameParams: cel.computeCelFrameParams,
25
+ drawCelebrationOverlay: celR.drawCelebrationOverlay,
26
+ getCelCannonOffset: celR.getCelCannonOffset
27
+ }
28
+ };
29
+ return cached;
30
+ }
31
+ export { loadGamePlugins };
@@ -1,3 +1,4 @@
1
+ import type { CelDotParams, CelState } from './celebration';
1
2
  import type { RGB } from './engine-colors';
2
3
  import type { GameEngineHost, GameLogic } from './game-logic';
3
4
  export interface GameRenderCtx {
@@ -8,10 +9,22 @@ export interface GameRenderCtx {
8
9
  caughtRgb: RGB;
9
10
  shadowPalette: string[];
10
11
  }
12
+ /** Plugin interface for lazily-loaded celebration rendering modules. */
13
+ export interface RenderPlugins {
14
+ CEL_CAUGHT_COL?: string;
15
+ celDotEffect?(di: number, x: number, y: number, params: CelDotParams, w: number, h: number): {
16
+ celBoost: number;
17
+ celCol: string | null;
18
+ } | null;
19
+ computeCelFrameParams?(cel: CelState, t: number, w: number, h: number): CelDotParams | null;
20
+ drawCelebrationOverlay?(rc: GameRenderCtx, cel: CelState, t: number, host: GameEngineHost, fontLoaded: boolean): void;
21
+ getCelCannonOffset?(cel: CelState | null, cannonAway: boolean, t: number, h: number): number;
22
+ }
11
23
  export declare function createGameRenderer(rc: GameRenderCtx, game: GameLogic, host: GameEngineHost): {
12
24
  drawGameDots: (t: number) => void;
13
25
  drawCaughtEffects: (t: number) => void;
14
26
  drawCannon: (t: number) => void;
15
27
  drawBullets: () => void;
16
28
  drawCelOverlay: (t: number) => void;
29
+ setRenderPlugins: (p: RenderPlugins) => void;
17
30
  };
@@ -1,5 +1,3 @@
1
- import { CEL_CAUGHT_COL, celDotEffect, computeCelFrameParams } from "./celebration.js";
2
- import { drawCelebrationOverlay, getCelCannonOffset } from "./celebration-renderer.js";
3
1
  import { AMB_SCALE, ANOMALY_ALPHA_BASE, ANOMALY_VIS_THRESHOLD, DOT_STEP_BASE, DOT_STEP_SCALE, HALFTONE_BASE_ALPHA, HALFTONE_BLOOM_PEAK, LABEL_ALPHA_BOOST, LABEL_DRIFT, LABEL_MIN_Y, LABEL_OFFSET_Y, LABEL_SHADOW_ALPHA, MIN_DOT_VALUE, NOISE_FREQ_T } from "./constants.js";
4
2
  import { ALPHA_STEPS, alphaIdx } from "./engine-colors.js";
5
3
  import { sweepX } from "./engine-grid.js";
@@ -7,6 +5,7 @@ import { ANOMALY_R, ANOMALY_R_SQ, ARM_RISE, CANNON_BASE_OFFSET, CANNON_HALF_W, C
7
5
  function createGameRenderer(rc, game, host) {
8
6
  let envCache = [];
9
7
  let revealedCache = [];
8
+ let plugins = {};
10
9
  function drawGameDots(t) {
11
10
  const { ctx, dotPalette, accentPalette, caughtPalette } = rc;
12
11
  const { w, h, dots, opts, tanTilt, exclusionBox } = host;
@@ -18,7 +17,7 @@ function createGameRenderer(rc, game, host) {
18
17
  const isIdle = 'idle' === game.gameMode;
19
18
  const cel = game.cel;
20
19
  let celParams = null;
21
- if (cel) celParams = computeCelFrameParams(cel, t, w, h);
20
+ if (cel) celParams = plugins.computeCelFrameParams?.(cel, t, w, h) ?? null;
22
21
  const exL = exclusionBox ? (w - exclusionBox.width) / 2 : 0;
23
22
  const exR = exclusionBox ? (w + exclusionBox.width) / 2 : 0;
24
23
  const exT = exclusionBox ? (h - exclusionBox.height) / 2 : 0;
@@ -63,7 +62,7 @@ function createGameRenderer(rc, game, host) {
63
62
  let celBoost = 0;
64
63
  let celCol = null;
65
64
  if (celParams) {
66
- const eff = celDotEffect(di, dot.x, dot.y, celParams, w, h);
65
+ const eff = plugins.celDotEffect?.(di, dot.x, dot.y, celParams, w, h);
67
66
  if (eff) {
68
67
  celBoost = eff.celBoost;
69
68
  celCol = eff.celCol;
@@ -77,7 +76,7 @@ function createGameRenderer(rc, game, host) {
77
76
  if (maxAo > ANOMALY_VIS_THRESHOLD) ctx.fillStyle = accentPalette[alphaIdx(Math.min(1, ANOMALY_ALPHA_BASE + maxAo))];
78
77
  else if (celBoost > 0.02) {
79
78
  const celAlpha = Math.min(1, 0.15 + 0.85 * effVal);
80
- if (celCol === CEL_CAUGHT_COL) ctx.fillStyle = caughtPalette[alphaIdx(celAlpha)];
79
+ if (celCol === plugins.CEL_CAUGHT_COL) ctx.fillStyle = caughtPalette[alphaIdx(celAlpha)];
81
80
  else if (celCol) {
82
81
  ctx.globalAlpha = celAlpha;
83
82
  ctx.fillStyle = celCol;
@@ -123,7 +122,7 @@ function createGameRenderer(rc, game, host) {
123
122
  const cel = game.cel;
124
123
  const hasCel = null !== cel;
125
124
  if ('armed' !== game.gameMode && 'over' !== game.gameMode && !hasCel) return;
126
- const liftOffset = getCelCannonOffset(cel, game.cannonAway, t, host.h);
125
+ const liftOffset = plugins.getCelCannonOffset?.(cel, game.cannonAway, t, host.h) ?? 0;
127
126
  if ('armed' === game.gameMode && !hasCel) {
128
127
  const riseP = Math.min(1, (t - game.armT) / ARM_RISE);
129
128
  const ease = 1 - (1 - riseP) * (1 - riseP);
@@ -152,14 +151,18 @@ function createGameRenderer(rc, game, host) {
152
151
  function drawCelOverlay(t) {
153
152
  const cel = game.cel;
154
153
  if (!cel) return;
155
- drawCelebrationOverlay(rc, cel, t, host, game.fontLoaded);
154
+ plugins.drawCelebrationOverlay?.(rc, cel, t, host, game.fontLoaded);
155
+ }
156
+ function setRenderPlugins(p) {
157
+ plugins = p;
156
158
  }
157
159
  return {
158
160
  drawGameDots,
159
161
  drawCaughtEffects,
160
162
  drawCannon,
161
163
  drawBullets,
162
- drawCelOverlay
164
+ drawCelOverlay,
165
+ setRenderPlugins
163
166
  };
164
167
  }
165
168
  export { createGameRenderer };
@@ -1,143 +1,84 @@
1
1
  const MASTER_VOLUME = 0.06;
2
- let ctx = null;
2
+ let ac = null;
3
3
  let master = null;
4
- function ensureAudio() {
5
- if ("u" < typeof window) return null;
6
- if (!ctx) {
7
- ctx = new AudioContext();
8
- master = ctx.createGain();
4
+ function ctx() {
5
+ if ("u" < typeof window || !window.AudioContext) return null;
6
+ if (!ac) {
7
+ ac = new window.AudioContext();
8
+ master = ac.createGain();
9
9
  master.gain.value = MASTER_VOLUME;
10
- master.connect(ctx.destination);
10
+ master.connect(ac.destination);
11
11
  }
12
- if ('suspended' === ctx.state) ctx.resume();
13
- if (!master) return null;
14
- return {
15
- ac: ctx,
16
- out: master
17
- };
12
+ if ('suspended' === ac.state) ac.resume().catch(()=>{});
13
+ return ac;
18
14
  }
19
- function scheduleNotes(ac, out, notes, step, volume = 0.4) {
20
- const now = ac.currentTime;
21
- for(let i = 0; i < notes.length; i++){
22
- const t = now + i * step;
23
- const osc = ac.createOscillator();
24
- osc.type = 'square';
25
- osc.frequency.value = notes[i];
26
- const gain = ac.createGain();
27
- gain.gain.setValueAtTime(volume, t);
28
- gain.gain.setValueAtTime(volume, t + 0.85 * step);
29
- gain.gain.linearRampToValueAtTime(0, t + step);
30
- osc.connect(gain).connect(out);
31
- osc.start(t);
32
- osc.stop(t + step);
33
- }
34
- return now + notes.length * step;
15
+ function tone(type, f0, f1, dur, vol, delay = 0) {
16
+ const a = ctx();
17
+ if (!a || !master) return;
18
+ const t0 = a.currentTime + delay;
19
+ const osc = a.createOscillator();
20
+ const gain = a.createGain();
21
+ osc.type = type;
22
+ osc.frequency.setValueAtTime(Math.max(1, f0), t0);
23
+ if (f1 !== f0) osc.frequency.exponentialRampToValueAtTime(Math.max(1, f1), t0 + dur);
24
+ gain.gain.setValueAtTime(vol, t0);
25
+ gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
26
+ osc.connect(gain);
27
+ gain.connect(master);
28
+ osc.start(t0);
29
+ osc.stop(t0 + dur + 0.02);
30
+ }
31
+ function hiss(dur, vol, f0, f1, delay = 0) {
32
+ const a = ctx();
33
+ if (!a || !master) return;
34
+ const t0 = a.currentTime + delay;
35
+ const n = Math.floor(a.sampleRate * dur);
36
+ const buf = a.createBuffer(1, n, a.sampleRate);
37
+ const data = buf.getChannelData(0);
38
+ for(let i = 0; i < n; i++)data[i] = 2 * Math.random() - 1;
39
+ const src = a.createBufferSource();
40
+ src.buffer = buf;
41
+ const filter = a.createBiquadFilter();
42
+ filter.type = 'bandpass';
43
+ filter.Q.value = 1.2;
44
+ filter.frequency.setValueAtTime(Math.max(1, f0), t0);
45
+ filter.frequency.exponentialRampToValueAtTime(Math.max(40, f1), t0 + dur);
46
+ const gain = a.createGain();
47
+ gain.gain.setValueAtTime(vol, t0);
48
+ gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
49
+ src.connect(filter);
50
+ filter.connect(gain);
51
+ gain.connect(master);
52
+ src.start(t0);
53
+ src.stop(t0 + dur + 0.02);
35
54
  }
36
55
  function playPew() {
37
- const audio = ensureAudio();
38
- if (!audio) return;
39
- const { ac, out } = audio;
40
- const now = ac.currentTime;
41
- const dur = 0.08;
42
- const osc = ac.createOscillator();
43
- osc.type = 'square';
44
- osc.frequency.setValueAtTime(980, now);
45
- osc.frequency.linearRampToValueAtTime(180, now + dur);
46
- const gain = ac.createGain();
47
- gain.gain.setValueAtTime(0.5, now);
48
- gain.gain.linearRampToValueAtTime(0, now + dur);
49
- osc.connect(gain).connect(out);
50
- osc.start(now);
51
- osc.stop(now + dur);
56
+ tone('square', 980, 180, 0.08, 0.5);
52
57
  }
53
58
  function playZap() {
54
- const audio = ensureAudio();
55
- if (!audio) return;
56
- const { ac, out } = audio;
57
- const now = ac.currentTime;
58
- const osc = ac.createOscillator();
59
- osc.type = 'square';
60
- osc.frequency.setValueAtTime(320, now);
61
- osc.frequency.linearRampToValueAtTime(70, now + 0.06);
62
- const oscGain = ac.createGain();
63
- oscGain.gain.setValueAtTime(0.45, now);
64
- oscGain.gain.linearRampToValueAtTime(0, now + 0.06);
65
- osc.connect(oscGain).connect(out);
66
- osc.start(now);
67
- osc.stop(now + 0.06);
68
- const bufLen = Math.ceil(0.07 * ac.sampleRate);
69
- const buf = ac.createBuffer(1, bufLen, ac.sampleRate);
70
- const data = buf.getChannelData(0);
71
- for(let i = 0; i < bufLen; i++)data[i] = 2 * Math.random() - 1;
72
- const noise = ac.createBufferSource();
73
- noise.buffer = buf;
74
- const bp = ac.createBiquadFilter();
75
- bp.type = 'bandpass';
76
- bp.frequency.setValueAtTime(1400, now);
77
- bp.frequency.linearRampToValueAtTime(300, now + 0.07);
78
- bp.Q.value = 2;
79
- const noiseGain = ac.createGain();
80
- noiseGain.gain.setValueAtTime(0.4, now);
81
- noiseGain.gain.linearRampToValueAtTime(0, now + 0.07);
82
- noise.connect(bp).connect(noiseGain).connect(out);
83
- noise.start(now);
84
- noise.stop(now + 0.07);
59
+ tone('square', 320, 70, 0.06, 0.45);
60
+ hiss(0.07, 0.3, 1400, 300);
85
61
  }
86
62
  function playCoin() {
87
- const audio = ensureAudio();
88
- if (!audio) return;
89
- const { ac, out } = audio;
90
- const now = ac.currentTime;
91
- const osc1 = ac.createOscillator();
92
- osc1.type = 'square';
93
- osc1.frequency.value = 988;
94
- const gain1 = ac.createGain();
95
- gain1.gain.setValueAtTime(0.4, now);
96
- gain1.gain.setValueAtTime(0.4, now + 0.07);
97
- gain1.gain.linearRampToValueAtTime(0, now + 0.08);
98
- osc1.connect(gain1).connect(out);
99
- osc1.start(now);
100
- osc1.stop(now + 0.08);
101
- const osc2 = ac.createOscillator();
102
- osc2.type = 'square';
103
- osc2.frequency.value = 1319;
104
- const gain2 = ac.createGain();
105
- gain2.gain.setValueAtTime(0, now);
106
- gain2.gain.setValueAtTime(0.4, now + 0.08);
107
- gain2.gain.setValueAtTime(0.4, now + 0.38);
108
- gain2.gain.linearRampToValueAtTime(0, now + 0.46);
109
- osc2.connect(gain2).connect(out);
110
- osc2.start(now + 0.08);
111
- osc2.stop(now + 0.46);
63
+ tone('square', 988, 988, 0.08, 0.4);
64
+ tone('square', 1319, 1319, 0.38, 0.4, 0.08);
112
65
  }
113
66
  function playPowerUp() {
114
- const audio = ensureAudio();
115
- if (!audio) return;
116
- scheduleNotes(audio.ac, audio.out, [
67
+ const notes = [
117
68
  392,
118
69
  523,
119
70
  659,
120
71
  784
121
- ], 0.06);
72
+ ];
73
+ for(let i = 0; i < notes.length; i++)tone('square', notes[i], notes[i], 0.07, 0.4, 0.06 * i);
122
74
  }
123
75
  function playFanfare() {
124
- const audio = ensureAudio();
125
- if (!audio) return;
126
- const { ac, out } = audio;
127
- const finalT = scheduleNotes(ac, out, [
76
+ const notes = [
128
77
  523,
129
78
  659,
130
79
  784
131
- ], 0.09);
132
- const osc = ac.createOscillator();
133
- osc.type = 'square';
134
- osc.frequency.value = 1046;
135
- const gain = ac.createGain();
136
- gain.gain.setValueAtTime(0.45, finalT);
137
- gain.gain.setValueAtTime(0.45, finalT + 0.16);
138
- gain.gain.linearRampToValueAtTime(0, finalT + 0.22);
139
- osc.connect(gain).connect(out);
140
- osc.start(finalT);
141
- osc.stop(finalT + 0.22);
80
+ ];
81
+ for(let i = 0; i < notes.length; i++)tone('square', notes[i], notes[i], 0.09, 0.4, 0.09 * i);
82
+ tone('square', 1046, 1046, 0.22, 0.4, 0.27);
142
83
  }
143
84
  export { playCoin, playFanfare, playPew, playPowerUp, playZap };
@@ -1,4 +1,4 @@
1
- import type { PointerEvent, ReactElement, RefObject } from 'react';
1
+ import { type PointerEvent, type ReactElement, type RefObject } from 'react';
2
2
  import type { SweepEngine } from './index';
3
3
  interface UseGameParams {
4
4
  game: boolean;
@@ -1,7 +1,10 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { GameHud } from "../GameHud.js";
2
+ import { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react";
3
+ import { loadGamePlugins } from "./game-plugins.js";
4
4
  import { useGameKeyboard } from "./useGameKeyboard.js";
5
+ const LazyGameHud = /*#__PURE__*/ lazy(()=>import("../GameHud.js").then((m)=>({
6
+ default: m.GameHud
7
+ })));
5
8
  const GATE_TARGET = 5;
6
9
  const useGame = ({ game, isHalftone, excludeCardSize, canvasRef })=>{
7
10
  const engineRef = useRef(null);
@@ -25,8 +28,19 @@ const useGame = ({ game, isHalftone, excludeCardSize, canvasRef })=>{
25
28
  const roundOver = armed && stats.done;
26
29
  const faced = stats.stopped + stats.escaped;
27
30
  const accuracy = faced > 0 ? Math.round(stats.stopped / faced * 100) : 100;
31
+ const soundOnRef = useRef(soundOn);
32
+ const isHalftoneRef = useRef(isHalftone);
33
+ useEffect(()=>{
34
+ soundOnRef.current = soundOn;
35
+ isHalftoneRef.current = isHalftone;
36
+ });
28
37
  const onEngineCreated = useCallback((engine)=>{
29
38
  engineRef.current = engine;
39
+ engine.setSound(gameRef.current && soundOnRef.current);
40
+ engine.setGameActive(gameRef.current && isHalftoneRef.current);
41
+ if (gameRef.current) loadGamePlugins().then((plugins)=>{
42
+ if (engineRef.current === engine) engine.setPlugins(plugins);
43
+ });
30
44
  engine.onStats((s)=>{
31
45
  setStats(s);
32
46
  if (gameRef.current && !s.done) setCatchKey((prev)=>prev + 1);
@@ -41,6 +55,19 @@ const useGame = ({ game, isHalftone, excludeCardSize, canvasRef })=>{
41
55
  game,
42
56
  isHalftone
43
57
  ]);
58
+ useEffect(()=>{
59
+ if (!game) return;
60
+ let cancelled = false;
61
+ loadGamePlugins().then((plugins)=>{
62
+ if (cancelled) return;
63
+ engineRef.current?.setPlugins(plugins);
64
+ });
65
+ return ()=>{
66
+ cancelled = true;
67
+ };
68
+ }, [
69
+ game
70
+ ]);
44
71
  const exW = excludeCardSize?.width;
45
72
  const exH = excludeCardSize?.height;
46
73
  useEffect(()=>{
@@ -87,17 +114,20 @@ const useGame = ({ game, isHalftone, excludeCardSize, canvasRef })=>{
87
114
  const handleTryAgain = useCallback(()=>{
88
115
  engineRef.current?.startRound();
89
116
  }, []);
90
- const hudElement = /*#__PURE__*/ jsx(GameHud, {
91
- caught: caught,
92
- armed: armed,
93
- roundOver: roundOver,
94
- stats: stats,
95
- accuracy: accuracy,
96
- faced: faced,
97
- catchKey: catchKey,
98
- gateTarget: GATE_TARGET,
99
- onTryAgain: handleTryAgain,
100
- soundOn: soundOn
117
+ const hudElement = /*#__PURE__*/ jsx(Suspense, {
118
+ fallback: null,
119
+ children: /*#__PURE__*/ jsx(LazyGameHud, {
120
+ caught: caught,
121
+ armed: armed,
122
+ roundOver: roundOver,
123
+ stats: stats,
124
+ accuracy: accuracy,
125
+ faced: faced,
126
+ catchKey: catchKey,
127
+ gateTarget: GATE_TARGET,
128
+ onTryAgain: handleTryAgain,
129
+ soundOn: soundOn
130
+ })
101
131
  });
102
132
  return {
103
133
  gameActive,
@@ -66,6 +66,18 @@ const useGameKeyboard = (engineRef, game, armed, roundOver, hasStartedRoundRef,
66
66
  engineRef,
67
67
  handleCommonKey
68
68
  ]);
69
+ useEffect(()=>{
70
+ if (!game || armed) return;
71
+ function onKeyDown(e) {
72
+ handleCommonKey(e);
73
+ }
74
+ window.addEventListener('keydown', onKeyDown);
75
+ return ()=>window.removeEventListener('keydown', onKeyDown);
76
+ }, [
77
+ game,
78
+ armed,
79
+ handleCommonKey
80
+ ]);
69
81
  useEffect(()=>{
70
82
  if (!game || !roundOver) return;
71
83
  function onKeyDown(e) {
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.64.0",
3
- "generatedAt": "2026-06-19T13:10:46.249Z",
2
+ "version": "0.64.1",
3
+ "generatedAt": "2026-06-19T16:10:22.219Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.64.1",
3
+ "version": "0.65.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",