@wallarm-org/design-system 0.58.1 → 0.59.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/AnimatedBackground.js +18 -82
- package/dist/components/AnimatedBackground/GameHud.d.ts +1 -0
- package/dist/components/AnimatedBackground/GameHud.js +2 -2
- package/dist/components/AnimatedBackground/module/celebration-renderer.d.ts +5 -0
- package/dist/components/AnimatedBackground/module/celebration-renderer.js +60 -0
- package/dist/components/AnimatedBackground/module/celebration.d.ts +102 -0
- package/dist/components/AnimatedBackground/module/celebration.js +628 -0
- package/dist/components/AnimatedBackground/module/engine-grid.d.ts +8 -1
- package/dist/components/AnimatedBackground/module/engine-grid.js +19 -5
- package/dist/components/AnimatedBackground/module/engine.d.ts +2 -0
- package/dist/components/AnimatedBackground/module/engine.js +24 -5
- package/dist/components/AnimatedBackground/module/game-logic.d.ts +8 -0
- package/dist/components/AnimatedBackground/module/game-logic.js +81 -37
- package/dist/components/AnimatedBackground/module/game-renderer.d.ts +1 -0
- package/dist/components/AnimatedBackground/module/game-renderer.js +51 -12
- package/dist/components/AnimatedBackground/module/index.d.ts +1 -0
- package/dist/components/AnimatedBackground/module/index.js +2 -1
- package/dist/components/AnimatedBackground/module/math.d.ts +4 -0
- package/dist/components/AnimatedBackground/module/math.js +10 -0
- package/dist/components/AnimatedBackground/module/sfx.d.ts +15 -0
- package/dist/components/AnimatedBackground/module/sfx.js +143 -0
- package/dist/components/AnimatedBackground/module/useGame.d.ts +22 -0
- package/dist/components/AnimatedBackground/module/useGame.js +112 -0
- package/dist/components/AnimatedBackground/module/useGameKeyboard.d.ts +1 -1
- package/dist/components/AnimatedBackground/module/useGameKeyboard.js +23 -14
- package/dist/components/Flex/Flex.d.ts +1 -1
- package/dist/components/SegmentedControl/SegmentedControlSeparator.d.ts +1 -1
- package/dist/components/Separator/Separator.d.ts +1 -1
- package/dist/components/Skeleton/Skeleton.d.ts +1 -1
- package/dist/components/Stack/Stack.d.ts +1 -1
- package/dist/metadata/components.json +2 -2
- package/package.json +1 -1
|
@@ -14,6 +14,8 @@ function createSweepEngine(canvas, options) {
|
|
|
14
14
|
w: 0,
|
|
15
15
|
h: 0,
|
|
16
16
|
dots: [],
|
|
17
|
+
gridCols: 0,
|
|
18
|
+
gridSp: 0,
|
|
17
19
|
opts,
|
|
18
20
|
tanTilt: Math.tan(options.tilt * Math.PI / 180),
|
|
19
21
|
exclusionBox: null
|
|
@@ -57,7 +59,10 @@ function createSweepEngine(canvas, options) {
|
|
|
57
59
|
canvas.width = Math.round(host.w * dpr);
|
|
58
60
|
canvas.height = Math.round(host.h * dpr);
|
|
59
61
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
60
|
-
|
|
62
|
+
const grid = buildGrid(host.w, host.h, opts.spacing);
|
|
63
|
+
host.dots = grid.dots;
|
|
64
|
+
host.gridCols = grid.cols;
|
|
65
|
+
host.gridSp = grid.sp;
|
|
61
66
|
host.tanTilt = Math.tan(opts.tilt * Math.PI / 180);
|
|
62
67
|
resolveColors();
|
|
63
68
|
game.cannonX = Math.max(CANNON_HALF_W, Math.min(host.w - CANNON_HALF_W, game.cannonX));
|
|
@@ -119,9 +124,10 @@ function createSweepEngine(canvas, options) {
|
|
|
119
124
|
ctx.fillRect(0, 0, host.w, host.h);
|
|
120
125
|
if ('halftone' === opts.texture) {
|
|
121
126
|
drawHalftone(t);
|
|
122
|
-
gr.drawCaughtEffects(t);
|
|
123
127
|
gr.drawCannon(t);
|
|
124
128
|
gr.drawBullets();
|
|
129
|
+
gr.drawCelOverlay(t);
|
|
130
|
+
gr.drawCaughtEffects(t);
|
|
125
131
|
} else drawClean(t);
|
|
126
132
|
}
|
|
127
133
|
const DT_CLAMP = 0.05;
|
|
@@ -137,7 +143,10 @@ function createSweepEngine(canvas, options) {
|
|
|
137
143
|
if (lastLatch > -1 / 0) lastLatch += skip;
|
|
138
144
|
}
|
|
139
145
|
lastFrameT = now;
|
|
140
|
-
if ('halftone' === opts.texture)
|
|
146
|
+
if ('halftone' === opts.texture) {
|
|
147
|
+
game.stepCel(now, dt, rc.caughtPalette[rc.caughtPalette.length - 1], rc.dotPalette[rc.dotPalette.length - 1]);
|
|
148
|
+
game.gameSim(now, dt);
|
|
149
|
+
}
|
|
141
150
|
game.pruneCaughtEffects(now);
|
|
142
151
|
frame(now);
|
|
143
152
|
rafId = requestAnimationFrame(tick);
|
|
@@ -182,7 +191,12 @@ function createSweepEngine(canvas, options) {
|
|
|
182
191
|
};
|
|
183
192
|
host.opts = opts;
|
|
184
193
|
host.tanTilt = Math.tan(opts.tilt * Math.PI / 180);
|
|
185
|
-
if (spacingChanged)
|
|
194
|
+
if (spacingChanged) {
|
|
195
|
+
const grid = buildGrid(host.w, host.h, opts.spacing);
|
|
196
|
+
host.dots = grid.dots;
|
|
197
|
+
host.gridCols = grid.cols;
|
|
198
|
+
host.gridSp = grid.sp;
|
|
199
|
+
}
|
|
186
200
|
resolveColors();
|
|
187
201
|
if (!running) renderStatic();
|
|
188
202
|
}
|
|
@@ -207,7 +221,12 @@ function createSweepEngine(canvas, options) {
|
|
|
207
221
|
setCannonDir: (dir)=>game.setCannonDir(dir),
|
|
208
222
|
setFiring: (on)=>game.setFiring(on),
|
|
209
223
|
onStats: (cb)=>game.onStats(cb),
|
|
210
|
-
setExclusion: (box)=>game.setExclusion(box)
|
|
224
|
+
setExclusion: (box)=>game.setExclusion(box),
|
|
225
|
+
celebrate: (score)=>{
|
|
226
|
+
if ('halftone' !== opts.texture || !running) return;
|
|
227
|
+
game.celebrate(score);
|
|
228
|
+
},
|
|
229
|
+
setSound: (on)=>game.setSound(on)
|
|
211
230
|
};
|
|
212
231
|
}
|
|
213
232
|
export { createSweepEngine };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CelState } from './celebration';
|
|
1
2
|
import type { Dot } from './engine-grid';
|
|
2
3
|
import type { EngineOptions, GameStats } from './types';
|
|
3
4
|
export declare const ROUND_ATTACKS = 100;
|
|
@@ -59,6 +60,8 @@ export interface GameEngineHost {
|
|
|
59
60
|
w: number;
|
|
60
61
|
h: number;
|
|
61
62
|
dots: Dot[];
|
|
63
|
+
gridCols: number;
|
|
64
|
+
gridSp: number;
|
|
62
65
|
opts: EngineOptions;
|
|
63
66
|
tanTilt: number;
|
|
64
67
|
exclusionBox: {
|
|
@@ -75,7 +78,10 @@ export interface GameLogic {
|
|
|
75
78
|
cannonX: number;
|
|
76
79
|
armT: number;
|
|
77
80
|
fontLoaded: boolean;
|
|
81
|
+
cel: CelState | null;
|
|
82
|
+
cannonAway: boolean;
|
|
78
83
|
gameSim(t: number, dt: number): void;
|
|
84
|
+
stepCel(t: number, dt: number, caughtColor: string, dotColor: string): void;
|
|
79
85
|
pruneCaughtEffects(t: number): void;
|
|
80
86
|
adjustTimeMarkers(skip: number): void;
|
|
81
87
|
setGameActive(active: boolean): void;
|
|
@@ -90,5 +96,7 @@ export interface GameLogic {
|
|
|
90
96
|
width: number;
|
|
91
97
|
height: number;
|
|
92
98
|
} | null): void;
|
|
99
|
+
celebrate(score: number): void;
|
|
100
|
+
setSound(on: boolean): void;
|
|
93
101
|
}
|
|
94
102
|
export declare function createGameLogic(host: GameEngineHost): GameLogic;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { adjustCelebrationTimeMarkers, startCelebration, stepCelebration, tierForScore } from "./celebration.js";
|
|
1
2
|
import { ANOMALY_VIS_THRESHOLD, DOT_STEP_BASE, DOT_STEP_SCALE, IDLE_MARGIN_X, IDLE_MARGIN_Y } from "./constants.js";
|
|
2
3
|
import { sweepX } from "./engine-grid.js";
|
|
4
|
+
import { playCoin, playFanfare, playPew, playPowerUp, playZap } from "./sfx.js";
|
|
3
5
|
const ROUND_ATTACKS = 100;
|
|
4
6
|
const MAX_TARGETS = 8;
|
|
5
7
|
const MAX_BULLETS = 24;
|
|
@@ -43,7 +45,9 @@ function createGameLogic(host) {
|
|
|
43
45
|
gameMode: 'idle',
|
|
44
46
|
cannonX: 0,
|
|
45
47
|
armT: 0,
|
|
46
|
-
fontLoaded: false
|
|
48
|
+
fontLoaded: false,
|
|
49
|
+
cel: null,
|
|
50
|
+
cannonAway: false
|
|
47
51
|
};
|
|
48
52
|
const anomalies = [];
|
|
49
53
|
const bullets = [];
|
|
@@ -57,11 +61,15 @@ function createGameLogic(host) {
|
|
|
57
61
|
let roundSpawned = 0;
|
|
58
62
|
let roundDone = false;
|
|
59
63
|
let lastSpawn = -1 / 0;
|
|
64
|
+
let soundOn = false;
|
|
60
65
|
let statsCb = null;
|
|
61
66
|
let exclusionBox = null;
|
|
62
|
-
|
|
63
|
-
state.fontLoaded
|
|
64
|
-
|
|
67
|
+
function loadFont() {
|
|
68
|
+
if (state.fontLoaded || "u" < typeof document) return;
|
|
69
|
+
document.fonts.load('9px "Press Start 2P"').then(()=>{
|
|
70
|
+
state.fontLoaded = true;
|
|
71
|
+
}).catch(()=>{});
|
|
72
|
+
}
|
|
65
73
|
function emitStats() {
|
|
66
74
|
statsCb?.({
|
|
67
75
|
kills: killTotal,
|
|
@@ -181,7 +189,10 @@ function createGameLogic(host) {
|
|
|
181
189
|
}
|
|
182
190
|
function recordKill() {
|
|
183
191
|
killTotal += 1;
|
|
184
|
-
if ('armed' === state.gameMode)
|
|
192
|
+
if ('armed' === state.gameMode) {
|
|
193
|
+
roundKills += 1;
|
|
194
|
+
if (soundOn) playZap();
|
|
195
|
+
} else if (soundOn) playCoin();
|
|
185
196
|
emitStats();
|
|
186
197
|
}
|
|
187
198
|
function endRound() {
|
|
@@ -190,6 +201,11 @@ function createGameLogic(host) {
|
|
|
190
201
|
bullets.length = 0;
|
|
191
202
|
firing = false;
|
|
192
203
|
cannonDir = 0;
|
|
204
|
+
const faced = roundKills + roundEscaped;
|
|
205
|
+
const accuracy = faced > 0 ? Math.round(roundKills / faced * 100) : 100;
|
|
206
|
+
const now = performance.now() / 1000;
|
|
207
|
+
state.cel = startCelebration(accuracy, now, host);
|
|
208
|
+
if (soundOn && tierForScore(accuracy) >= 1) playFanfare();
|
|
193
209
|
emitStats();
|
|
194
210
|
}
|
|
195
211
|
function isHittable(anomaly, t) {
|
|
@@ -210,6 +226,7 @@ function createGameLogic(host) {
|
|
|
210
226
|
anomalies.length = kept;
|
|
211
227
|
}
|
|
212
228
|
function idleSim(t) {
|
|
229
|
+
if (state.cel) return void pruneExpired(t);
|
|
213
230
|
let liveCount = 0;
|
|
214
231
|
for (const anomaly of anomalies)if (!anomaly.caught) liveCount++;
|
|
215
232
|
if (liveCount < 2 && t - lastSpawn > host.opts.anomalyInterval) {
|
|
@@ -233,6 +250,7 @@ function createGameLogic(host) {
|
|
|
233
250
|
y: host.h - CANNON_BARREL_Y
|
|
234
251
|
});
|
|
235
252
|
lastFire = t;
|
|
253
|
+
if (soundOn) playPew();
|
|
236
254
|
}
|
|
237
255
|
}
|
|
238
256
|
function moveBullets(dt) {
|
|
@@ -327,25 +345,42 @@ function createGameLogic(host) {
|
|
|
327
345
|
for (const effect of caughtEffects)effect.t0 += skip;
|
|
328
346
|
if (state.armT > 0) state.armT += skip;
|
|
329
347
|
if (lastSpawn > -1 / 0 && 0 !== lastSpawn) lastSpawn += skip;
|
|
348
|
+
if (state.cel) adjustCelebrationTimeMarkers(state.cel, skip);
|
|
349
|
+
}
|
|
350
|
+
function resetRoundState() {
|
|
351
|
+
roundKills = 0;
|
|
352
|
+
roundEscaped = 0;
|
|
353
|
+
roundSpawned = 0;
|
|
354
|
+
roundDone = false;
|
|
355
|
+
bullets.length = 0;
|
|
356
|
+
anomalies.length = 0;
|
|
357
|
+
caughtEffects.length = 0;
|
|
358
|
+
firing = false;
|
|
359
|
+
cannonDir = 0;
|
|
360
|
+
state.cel = null;
|
|
361
|
+
state.cannonAway = false;
|
|
330
362
|
}
|
|
331
363
|
function setGameActive(active) {
|
|
332
364
|
if (state.gameActive === active) return;
|
|
333
365
|
state.gameActive = active;
|
|
334
|
-
if (active)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
366
|
+
if (active) {
|
|
367
|
+
lastSpawn = -1 / 0;
|
|
368
|
+
loadFont();
|
|
369
|
+
} else {
|
|
370
|
+
resetRoundState();
|
|
339
371
|
state.gameMode = 'idle';
|
|
340
372
|
killTotal = 0;
|
|
341
|
-
roundKills = 0;
|
|
342
|
-
roundEscaped = 0;
|
|
343
|
-
roundSpawned = 0;
|
|
344
|
-
roundDone = false;
|
|
345
|
-
firing = false;
|
|
346
|
-
cannonDir = 0;
|
|
347
373
|
}
|
|
348
374
|
}
|
|
375
|
+
function celebrate(score) {
|
|
376
|
+
const now = performance.now() / 1000;
|
|
377
|
+
state.cel = null;
|
|
378
|
+
state.cannonAway = false;
|
|
379
|
+
anomalies.length = 0;
|
|
380
|
+
state.armT = now;
|
|
381
|
+
state.cel = startCelebration(score, now, host);
|
|
382
|
+
if (soundOn && tierForScore(score) >= 1) playFanfare();
|
|
383
|
+
}
|
|
349
384
|
function catchAt(x, y, running) {
|
|
350
385
|
if (!running) return false;
|
|
351
386
|
const now = performance.now() / 1000;
|
|
@@ -372,38 +407,24 @@ function createGameLogic(host) {
|
|
|
372
407
|
if ('idle' === mode) {
|
|
373
408
|
state.gameMode = 'idle';
|
|
374
409
|
lastSpawn = -1 / 0;
|
|
410
|
+
state.cel = null;
|
|
411
|
+
state.cannonAway = false;
|
|
375
412
|
} else state.gameMode = 'armed';
|
|
376
413
|
}
|
|
377
414
|
function startRound() {
|
|
378
|
-
const { w } = host;
|
|
379
415
|
const now = performance.now() / 1000;
|
|
380
|
-
|
|
381
|
-
roundEscaped = 0;
|
|
382
|
-
roundSpawned = 0;
|
|
383
|
-
roundDone = false;
|
|
416
|
+
resetRoundState();
|
|
384
417
|
state.gameMode = 'armed';
|
|
385
|
-
bullets.length = 0;
|
|
386
|
-
anomalies.length = 0;
|
|
387
|
-
caughtEffects.length = 0;
|
|
388
|
-
firing = false;
|
|
389
|
-
cannonDir = 0;
|
|
390
418
|
state.armT = now;
|
|
391
419
|
lastSpawn = now;
|
|
392
|
-
state.cannonX = w / 2;
|
|
420
|
+
state.cannonX = host.w / 2;
|
|
421
|
+
if (soundOn) playPowerUp();
|
|
393
422
|
emitStats();
|
|
394
423
|
}
|
|
395
424
|
function exitGame() {
|
|
396
|
-
|
|
397
|
-
roundKills = 0;
|
|
398
|
-
roundEscaped = 0;
|
|
399
|
-
roundSpawned = 0;
|
|
400
|
-
roundDone = false;
|
|
425
|
+
resetRoundState();
|
|
401
426
|
state.gameMode = 'idle';
|
|
402
|
-
|
|
403
|
-
anomalies.length = 0;
|
|
404
|
-
caughtEffects.length = 0;
|
|
405
|
-
firing = false;
|
|
406
|
-
cannonDir = 0;
|
|
427
|
+
killTotal = 0;
|
|
407
428
|
lastSpawn = -1 / 0;
|
|
408
429
|
emitStats();
|
|
409
430
|
}
|
|
@@ -419,6 +440,9 @@ function createGameLogic(host) {
|
|
|
419
440
|
function onStats(cb) {
|
|
420
441
|
statsCb = cb;
|
|
421
442
|
}
|
|
443
|
+
function setSound(on) {
|
|
444
|
+
soundOn = on;
|
|
445
|
+
}
|
|
422
446
|
function setExclusion(box) {
|
|
423
447
|
if (!box) {
|
|
424
448
|
exclusionBox = null;
|
|
@@ -432,6 +456,11 @@ function createGameLogic(host) {
|
|
|
432
456
|
exclusionBox = val;
|
|
433
457
|
host.exclusionBox = val;
|
|
434
458
|
}
|
|
459
|
+
function stepCel(t, dt, caughtColor, dotColor) {
|
|
460
|
+
if (!state.cel) return;
|
|
461
|
+
stepCelebration(state.cel, t, dt, host, caughtColor, dotColor);
|
|
462
|
+
if (state.cel.liftStarted) state.cannonAway = true;
|
|
463
|
+
}
|
|
435
464
|
return {
|
|
436
465
|
anomalies,
|
|
437
466
|
bullets,
|
|
@@ -439,6 +468,7 @@ function createGameLogic(host) {
|
|
|
439
468
|
gameSim,
|
|
440
469
|
pruneCaughtEffects,
|
|
441
470
|
adjustTimeMarkers,
|
|
471
|
+
stepCel,
|
|
442
472
|
setGameActive,
|
|
443
473
|
catchAt,
|
|
444
474
|
setMode,
|
|
@@ -448,6 +478,20 @@ function createGameLogic(host) {
|
|
|
448
478
|
setFiring,
|
|
449
479
|
onStats,
|
|
450
480
|
setExclusion,
|
|
481
|
+
celebrate,
|
|
482
|
+
setSound,
|
|
483
|
+
get cel () {
|
|
484
|
+
return state.cel;
|
|
485
|
+
},
|
|
486
|
+
set cel (v){
|
|
487
|
+
state.cel = v;
|
|
488
|
+
},
|
|
489
|
+
get cannonAway () {
|
|
490
|
+
return state.cannonAway;
|
|
491
|
+
},
|
|
492
|
+
set cannonAway (v){
|
|
493
|
+
state.cannonAway = v;
|
|
494
|
+
},
|
|
451
495
|
get gameActive () {
|
|
452
496
|
return state.gameActive;
|
|
453
497
|
},
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CEL_CAUGHT_COL, celDotEffect, computeCelFrameParams } from "./celebration.js";
|
|
2
|
+
import { drawCelebrationOverlay, getCelCannonOffset } from "./celebration-renderer.js";
|
|
3
|
+
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";
|
|
2
4
|
import { ALPHA_STEPS, alphaIdx } from "./engine-colors.js";
|
|
3
5
|
import { sweepX } from "./engine-grid.js";
|
|
4
6
|
import { ANOMALY_R, ANOMALY_R_SQ, ARM_RISE, CANNON_BASE_OFFSET, CANNON_HALF_W, CAUGHT_DUR, REVEAL_DELAY } from "./game-logic.js";
|
|
@@ -6,7 +8,7 @@ function createGameRenderer(rc, game, host) {
|
|
|
6
8
|
let envCache = [];
|
|
7
9
|
let revealedCache = [];
|
|
8
10
|
function drawGameDots(t) {
|
|
9
|
-
const { ctx, dotPalette, accentPalette } = rc;
|
|
11
|
+
const { ctx, dotPalette, accentPalette, caughtPalette } = rc;
|
|
10
12
|
const { w, h, dots, opts, tanTilt, exclusionBox } = host;
|
|
11
13
|
const sx = sweepX(t, w, opts.sweepPeriod);
|
|
12
14
|
const intensity = opts.intensity;
|
|
@@ -14,6 +16,9 @@ function createGameRenderer(rc, game, host) {
|
|
|
14
16
|
const liveAnomalies = game.anomalies;
|
|
15
17
|
const aLen = liveAnomalies.length;
|
|
16
18
|
const isIdle = 'idle' === game.gameMode;
|
|
19
|
+
const cel = game.cel;
|
|
20
|
+
let celParams = null;
|
|
21
|
+
if (cel) celParams = computeCelFrameParams(cel, t, w, h);
|
|
17
22
|
const exL = exclusionBox ? (w - exclusionBox.width) / 2 : 0;
|
|
18
23
|
const exR = exclusionBox ? (w + exclusionBox.width) / 2 : 0;
|
|
19
24
|
const exT = exclusionBox ? (h - exclusionBox.height) / 2 : 0;
|
|
@@ -32,10 +37,11 @@ function createGameRenderer(rc, game, host) {
|
|
|
32
37
|
revealedCache[i] = isIdle ? true : anomaly.x < sx || t - anomaly.t0 > REVEAL_DELAY;
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
|
-
for
|
|
40
|
+
for(let di = 0; di < dots.length; di++){
|
|
41
|
+
const dot = dots[di];
|
|
36
42
|
if (exclusionBox && dot.x >= exL && dot.x <= exR && dot.y >= exT && dot.y <= exB) continue;
|
|
37
43
|
const sxAt = sx + (h / 2 - dot.y) * tanTilt;
|
|
38
|
-
const amb = AMB_SCALE * (0.5 + 0.5 * Math.sin(dot.
|
|
44
|
+
const amb = AMB_SCALE * (0.5 + 0.5 * Math.sin(dot.noiseSpatial + t * NOISE_FREQ_T));
|
|
39
45
|
const distToSweep = Math.abs(dot.x - sxAt);
|
|
40
46
|
const bloom = distToSweep < opts.bloomRadius ? HALFTONE_BLOOM_PEAK * (1 - distToSweep / opts.bloomRadius) : 0;
|
|
41
47
|
let maxAo = 0;
|
|
@@ -54,12 +60,32 @@ function createGameRenderer(rc, game, host) {
|
|
|
54
60
|
const ao = env * (1 - dist / ANOMALY_R);
|
|
55
61
|
if (ao > maxAo) maxAo = ao;
|
|
56
62
|
}
|
|
63
|
+
let celBoost = 0;
|
|
64
|
+
let celCol = null;
|
|
65
|
+
if (celParams) {
|
|
66
|
+
const eff = celDotEffect(di, dot.x, dot.y, celParams, w, h);
|
|
67
|
+
if (eff) {
|
|
68
|
+
celBoost = eff.celBoost;
|
|
69
|
+
celCol = eff.celCol;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
57
72
|
const val = Math.min(1, Math.max(amb + bloom, maxAo));
|
|
58
|
-
if (val < MIN_DOT_VALUE) continue;
|
|
59
|
-
const
|
|
73
|
+
if (val < MIN_DOT_VALUE && celBoost < 0.02) continue;
|
|
74
|
+
const effVal = Math.min(1, val + celBoost);
|
|
75
|
+
const step = Math.round(5 * effVal);
|
|
60
76
|
const half = Math.min(step * DOT_STEP_SCALE + DOT_STEP_BASE, halfCap);
|
|
61
77
|
if (maxAo > ANOMALY_VIS_THRESHOLD) ctx.fillStyle = accentPalette[alphaIdx(Math.min(1, ANOMALY_ALPHA_BASE + maxAo))];
|
|
62
|
-
else
|
|
78
|
+
else if (celBoost > 0.02) {
|
|
79
|
+
const celAlpha = Math.min(1, 0.15 + 0.85 * effVal);
|
|
80
|
+
if (celCol === CEL_CAUGHT_COL) ctx.fillStyle = caughtPalette[alphaIdx(celAlpha)];
|
|
81
|
+
else if (celCol) {
|
|
82
|
+
ctx.globalAlpha = celAlpha;
|
|
83
|
+
ctx.fillStyle = celCol;
|
|
84
|
+
ctx.fillRect(dot.x - half, dot.y - half, 2 * half, 2 * half);
|
|
85
|
+
ctx.globalAlpha = 1;
|
|
86
|
+
continue;
|
|
87
|
+
} else ctx.fillStyle = caughtPalette[alphaIdx(celAlpha)];
|
|
88
|
+
} else ctx.fillStyle = dotPalette[alphaIdx((HALFTONE_BASE_ALPHA + val * (opts.bloomAlpha - HALFTONE_BASE_ALPHA)) * intensity)];
|
|
63
89
|
ctx.fillRect(dot.x - half, dot.y - half, 2 * half, 2 * half);
|
|
64
90
|
}
|
|
65
91
|
}
|
|
@@ -94,8 +120,11 @@ function createGameRenderer(rc, game, host) {
|
|
|
94
120
|
}
|
|
95
121
|
function drawCannon(t) {
|
|
96
122
|
const { ctx, dotPalette } = rc;
|
|
97
|
-
|
|
98
|
-
|
|
123
|
+
const cel = game.cel;
|
|
124
|
+
const hasCel = null !== cel;
|
|
125
|
+
if ('armed' !== game.gameMode && 'over' !== game.gameMode && !hasCel) return;
|
|
126
|
+
const liftOffset = getCelCannonOffset(cel, game.cannonAway, t, host.h);
|
|
127
|
+
if ('armed' === game.gameMode && !hasCel) {
|
|
99
128
|
const riseP = Math.min(1, (t - game.armT) / ARM_RISE);
|
|
100
129
|
const ease = 1 - (1 - riseP) * (1 - riseP);
|
|
101
130
|
const offsetY = (1 - ease) * 40;
|
|
@@ -103,24 +132,34 @@ function createGameRenderer(rc, game, host) {
|
|
|
103
132
|
ctx.globalAlpha = ease;
|
|
104
133
|
ctx.translate(0, offsetY);
|
|
105
134
|
}
|
|
106
|
-
const baseY = host.h - CANNON_BASE_OFFSET;
|
|
135
|
+
const baseY = host.h - CANNON_BASE_OFFSET - liftOffset;
|
|
136
|
+
if (baseY < -40) {
|
|
137
|
+
if ('armed' === game.gameMode && !hasCel) ctx.restore();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
107
140
|
const cx = Math.round(game.cannonX);
|
|
108
141
|
ctx.fillStyle = dotPalette[ALPHA_STEPS];
|
|
109
142
|
ctx.fillRect(cx - CANNON_HALF_W, baseY, 2 * CANNON_HALF_W, 8);
|
|
110
143
|
ctx.fillRect(cx - 6, baseY - 6, 12, 6);
|
|
111
144
|
ctx.fillRect(cx - 2, baseY - 12, 4, 6);
|
|
112
|
-
if ('armed' === game.gameMode) ctx.restore();
|
|
145
|
+
if ('armed' === game.gameMode && !hasCel) ctx.restore();
|
|
113
146
|
}
|
|
114
147
|
function drawBullets() {
|
|
115
148
|
const { ctx, dotPalette } = rc;
|
|
116
149
|
ctx.fillStyle = dotPalette[ALPHA_STEPS];
|
|
117
150
|
for (const bullet of game.bullets)ctx.fillRect(bullet.x - 2, bullet.y - 7, 4, 14);
|
|
118
151
|
}
|
|
152
|
+
function drawCelOverlay(t) {
|
|
153
|
+
const cel = game.cel;
|
|
154
|
+
if (!cel) return;
|
|
155
|
+
drawCelebrationOverlay(rc, cel, t, host, game.fontLoaded);
|
|
156
|
+
}
|
|
119
157
|
return {
|
|
120
158
|
drawGameDots,
|
|
121
159
|
drawCaughtEffects,
|
|
122
160
|
drawCannon,
|
|
123
|
-
drawBullets
|
|
161
|
+
drawBullets,
|
|
162
|
+
drawCelOverlay
|
|
124
163
|
};
|
|
125
164
|
}
|
|
126
165
|
export { createGameRenderer };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createSweepEngine, type SweepEngine } from './engine';
|
|
2
2
|
export { resolveOptions } from './lib';
|
|
3
3
|
export type { AnimatedBackgroundProps, EngineOptions, GameStats, Texture } from './types';
|
|
4
|
+
export { useGame } from './useGame';
|
|
4
5
|
export { useGameKeyboard } from './useGameKeyboard';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createSweepEngine } from "./engine.js";
|
|
2
2
|
import { resolveOptions } from "./lib.js";
|
|
3
|
+
import { useGame } from "./useGame.js";
|
|
3
4
|
import { useGameKeyboard } from "./useGameKeyboard.js";
|
|
4
|
-
export { createSweepEngine, resolveOptions, useGameKeyboard };
|
|
5
|
+
export { createSweepEngine, resolveOptions, useGame, useGameKeyboard };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesized 8-bit SFX for the shooter easter egg.
|
|
3
|
+
*
|
|
4
|
+
* All sounds use the Web Audio API — square waves + band-passed noise,
|
|
5
|
+
* the way original hardware made them. No audio files, no deps, no bundle weight.
|
|
6
|
+
*
|
|
7
|
+
* SSR-safe: nothing runs at import time (typeof window guard).
|
|
8
|
+
* Autoplay-safe: AudioContext created lazily on the first play call,
|
|
9
|
+
* which is always downstream of a user gesture.
|
|
10
|
+
*/
|
|
11
|
+
export declare function playPew(): void;
|
|
12
|
+
export declare function playZap(): void;
|
|
13
|
+
export declare function playCoin(): void;
|
|
14
|
+
export declare function playPowerUp(): void;
|
|
15
|
+
export declare function playFanfare(): void;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const MASTER_VOLUME = 0.06;
|
|
2
|
+
let ctx = null;
|
|
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();
|
|
9
|
+
master.gain.value = MASTER_VOLUME;
|
|
10
|
+
master.connect(ctx.destination);
|
|
11
|
+
}
|
|
12
|
+
if ('suspended' === ctx.state) ctx.resume();
|
|
13
|
+
if (!master) return null;
|
|
14
|
+
return {
|
|
15
|
+
ac: ctx,
|
|
16
|
+
out: master
|
|
17
|
+
};
|
|
18
|
+
}
|
|
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;
|
|
35
|
+
}
|
|
36
|
+
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);
|
|
52
|
+
}
|
|
53
|
+
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);
|
|
85
|
+
}
|
|
86
|
+
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);
|
|
112
|
+
}
|
|
113
|
+
function playPowerUp() {
|
|
114
|
+
const audio = ensureAudio();
|
|
115
|
+
if (!audio) return;
|
|
116
|
+
scheduleNotes(audio.ac, audio.out, [
|
|
117
|
+
392,
|
|
118
|
+
523,
|
|
119
|
+
659,
|
|
120
|
+
784
|
|
121
|
+
], 0.06);
|
|
122
|
+
}
|
|
123
|
+
function playFanfare() {
|
|
124
|
+
const audio = ensureAudio();
|
|
125
|
+
if (!audio) return;
|
|
126
|
+
const { ac, out } = audio;
|
|
127
|
+
const finalT = scheduleNotes(ac, out, [
|
|
128
|
+
523,
|
|
129
|
+
659,
|
|
130
|
+
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);
|
|
142
|
+
}
|
|
143
|
+
export { playCoin, playFanfare, playPew, playPowerUp, playZap };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PointerEvent, ReactElement, RefObject } from 'react';
|
|
2
|
+
import type { SweepEngine } from './index';
|
|
3
|
+
interface UseGameParams {
|
|
4
|
+
game: boolean;
|
|
5
|
+
isHalftone: boolean;
|
|
6
|
+
excludeCardSize?: {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
};
|
|
10
|
+
canvasRef: RefObject<HTMLCanvasElement | null>;
|
|
11
|
+
}
|
|
12
|
+
interface UseGameReturn {
|
|
13
|
+
gameActive: boolean;
|
|
14
|
+
onPointerDown: ((e: PointerEvent<HTMLCanvasElement>) => void) | undefined;
|
|
15
|
+
hudElement: ReactElement | null;
|
|
16
|
+
onEngineCreated: (engine: SweepEngine) => void;
|
|
17
|
+
onEngineDestroyed: () => void;
|
|
18
|
+
soundOn: boolean;
|
|
19
|
+
toggleSound: () => void;
|
|
20
|
+
}
|
|
21
|
+
export declare const useGame: ({ game, isHalftone, excludeCardSize, canvasRef, }: UseGameParams) => UseGameReturn;
|
|
22
|
+
export {};
|