@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.
- package/dist/components/AnimatedBackground/GameHud.js +1 -1
- package/dist/components/AnimatedBackground/module/engine.d.ts +6 -0
- package/dist/components/AnimatedBackground/module/engine.js +5 -1
- package/dist/components/AnimatedBackground/module/game-logic.d.ts +13 -0
- package/dist/components/AnimatedBackground/module/game-logic.js +14 -12
- package/dist/components/AnimatedBackground/module/game-plugins.d.ts +6 -0
- package/dist/components/AnimatedBackground/module/game-plugins.js +31 -0
- package/dist/components/AnimatedBackground/module/game-renderer.d.ts +13 -0
- package/dist/components/AnimatedBackground/module/game-renderer.js +11 -8
- package/dist/components/AnimatedBackground/module/sfx.js +60 -119
- package/dist/components/AnimatedBackground/module/useGame.d.ts +1 -1
- package/dist/components/AnimatedBackground/module/useGame.js +43 -13
- package/dist/components/AnimatedBackground/module/useGameKeyboard.js +12 -0
- package/dist/metadata/components.json +2 -2
- package/package.json +1 -1
|
@@ -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:
|
|
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,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
|
|
2
|
+
let ac = null;
|
|
3
3
|
let master = null;
|
|
4
|
-
function
|
|
5
|
-
if ("u" < typeof window) return null;
|
|
6
|
-
if (!
|
|
7
|
-
|
|
8
|
-
master =
|
|
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(
|
|
10
|
+
master.connect(ac.destination);
|
|
11
11
|
}
|
|
12
|
-
if ('suspended' ===
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
ac: ctx,
|
|
16
|
-
out: master
|
|
17
|
-
};
|
|
12
|
+
if ('suspended' === ac.state) ac.resume().catch(()=>{});
|
|
13
|
+
return ac;
|
|
18
14
|
}
|
|
19
|
-
function
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
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
|
-
]
|
|
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
|
|
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
|
-
]
|
|
132
|
-
|
|
133
|
-
|
|
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,7 +1,10 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
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(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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) {
|