sa2kit 1.6.31 → 1.6.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/AliyunOSSProvider-4W47OFEK.mjs +6 -0
  2. package/dist/{AliyunOSSProvider-P6TOVKMM.mjs.map → AliyunOSSProvider-4W47OFEK.mjs.map} +1 -1
  3. package/dist/AliyunOSSProvider-HCNGDJL7.js +15 -0
  4. package/dist/{AliyunOSSProvider-Z5BRBCG6.js.map → AliyunOSSProvider-HCNGDJL7.js.map} +1 -1
  5. package/dist/calendar/index.js +8 -8
  6. package/dist/calendar/index.mjs +1 -1
  7. package/dist/chunk-EI27JKND.mjs +1988 -0
  8. package/dist/chunk-EI27JKND.mjs.map +1 -0
  9. package/dist/{chunk-77M5AQG3.mjs → chunk-HDMIOOZY.mjs} +3 -33
  10. package/dist/chunk-HDMIOOZY.mjs.map +1 -0
  11. package/dist/{chunk-X3UU7JHT.js → chunk-HJ6MH7J7.js} +3 -33
  12. package/dist/chunk-HJ6MH7J7.js.map +1 -0
  13. package/dist/chunk-KO73EBUT.js +80 -0
  14. package/dist/chunk-KO73EBUT.js.map +1 -0
  15. package/dist/{chunk-C54W2CMK.js → chunk-L47ZOYHL.js} +6 -80
  16. package/dist/chunk-L47ZOYHL.js.map +1 -0
  17. package/dist/{chunk-TVROG2Q4.mjs → chunk-UKT3PLON.mjs} +4 -76
  18. package/dist/chunk-UKT3PLON.mjs.map +1 -0
  19. package/dist/chunk-VVWQTO4Y.mjs +77 -0
  20. package/dist/chunk-VVWQTO4Y.mjs.map +1 -0
  21. package/dist/chunk-XGBE4SUV.js +2093 -0
  22. package/dist/chunk-XGBE4SUV.js.map +1 -0
  23. package/dist/index.d.mts +185 -100
  24. package/dist/index.d.ts +185 -100
  25. package/dist/index.js +119 -118
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +4 -3
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mikuFusionGame/index.d.mts +112 -0
  30. package/dist/mikuFusionGame/index.d.ts +112 -0
  31. package/dist/mikuFusionGame/index.js +680 -0
  32. package/dist/mikuFusionGame/index.js.map +1 -0
  33. package/dist/mikuFusionGame/index.mjs +667 -0
  34. package/dist/mikuFusionGame/index.mjs.map +1 -0
  35. package/dist/storage/index.js +13 -12
  36. package/dist/storage/index.mjs +2 -1
  37. package/dist/universalFile/server/index.js +3 -3
  38. package/dist/universalFile/server/index.mjs +2 -2
  39. package/package.json +25 -31
  40. package/dist/AliyunOSSProvider-P6TOVKMM.mjs +0 -6
  41. package/dist/AliyunOSSProvider-Z5BRBCG6.js +0 -15
  42. package/dist/chunk-5A7ERLKK.js +0 -1726
  43. package/dist/chunk-5A7ERLKK.js.map +0 -1
  44. package/dist/chunk-77M5AQG3.mjs.map +0 -1
  45. package/dist/chunk-C54W2CMK.js.map +0 -1
  46. package/dist/chunk-KIP2CERU.mjs +0 -1596
  47. package/dist/chunk-KIP2CERU.mjs.map +0 -1
  48. package/dist/chunk-TVROG2Q4.mjs.map +0 -1
  49. package/dist/chunk-X3UU7JHT.js.map +0 -1
@@ -0,0 +1,667 @@
1
+ import { useLocalStorage } from '../chunk-VVWQTO4Y.mjs';
2
+ import '../chunk-WEEXCPSE.mjs';
3
+ import '../chunk-BJTO5JO5.mjs';
4
+ import React3, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
5
+
6
+ // src/mikuFusionGame/constants.ts
7
+ var BASE_RADIUS = 22;
8
+ var RADIUS_STEP = 4;
9
+ var LEVEL_LABEL_PREFIX = "M";
10
+ var DEFAULT_MIKU_FUSION_CONFIG = {
11
+ width: 390,
12
+ height: 700,
13
+ gravity: 1450,
14
+ damping: 0.995,
15
+ collisionDamping: 0.92,
16
+ spawnY: 82,
17
+ lossLineY: 132,
18
+ maxLevel: 10,
19
+ maxOrbs: 90,
20
+ maxMergesPerTick: 6,
21
+ gameOverAgeThreshold: 1.1,
22
+ spawnWeights: [0.62, 0.28, 0.1],
23
+ theme: {
24
+ backgroundTop: "#dafaff",
25
+ backgroundBottom: "#86e5ef",
26
+ aimLine: "#14b8a6",
27
+ lossLine: "#ef4444",
28
+ orbColors: [
29
+ "#67e8f9",
30
+ "#22d3ee",
31
+ "#2dd4bf",
32
+ "#5eead4",
33
+ "#34d399",
34
+ "#a7f3d0",
35
+ "#99f6e4",
36
+ "#0ea5e9",
37
+ "#06b6d4",
38
+ "#14b8a6"
39
+ ]
40
+ }
41
+ };
42
+
43
+ // src/mikuFusionGame/engine/collision.ts
44
+ function resolveCircleCollisions(orbs, config) {
45
+ const next = orbs.map((orb) => ({ ...orb }));
46
+ for (let i = 0; i < next.length; i += 1) {
47
+ for (let j = i + 1; j < next.length; j += 1) {
48
+ const a = next[i];
49
+ const b = next[j];
50
+ const dx = b.x - a.x;
51
+ const dy = b.y - a.y;
52
+ const distance = Math.sqrt(dx * dx + dy * dy) || 1e-4;
53
+ const minDistance = a.radius + b.radius;
54
+ if (distance >= minDistance) {
55
+ continue;
56
+ }
57
+ const nx = dx / distance;
58
+ const ny = dy / distance;
59
+ const overlap = minDistance - distance;
60
+ a.x -= nx * overlap * 0.5;
61
+ a.y -= ny * overlap * 0.5;
62
+ b.x += nx * overlap * 0.5;
63
+ b.y += ny * overlap * 0.5;
64
+ const rvx = b.vx - a.vx;
65
+ const rvy = b.vy - a.vy;
66
+ const velocityAlongNormal = rvx * nx + rvy * ny;
67
+ if (velocityAlongNormal > 0) {
68
+ continue;
69
+ }
70
+ const restitution = config.collisionDamping;
71
+ const impulse = -(1 + restitution) * velocityAlongNormal / 2;
72
+ const impulseX = impulse * nx;
73
+ const impulseY = impulse * ny;
74
+ a.vx -= impulseX;
75
+ a.vy -= impulseY;
76
+ b.vx += impulseX;
77
+ b.vy += impulseY;
78
+ }
79
+ }
80
+ return next;
81
+ }
82
+
83
+ // src/mikuFusionGame/engine/scoring.ts
84
+ function getMergeScore(newLevel, chainIndex) {
85
+ const base = 10 * Math.max(1, newLevel) * Math.max(1, newLevel);
86
+ const chainMultiplier = 1 + Math.max(0, chainIndex - 1) * 0.15;
87
+ return Math.round(base * chainMultiplier);
88
+ }
89
+
90
+ // src/mikuFusionGame/engine/physics.ts
91
+ function getRadiusByLevel(level) {
92
+ return BASE_RADIUS + (Math.max(1, level) - 1) * RADIUS_STEP;
93
+ }
94
+ function stepPhysics(orbs, config, dt) {
95
+ return orbs.map((orb) => {
96
+ const next = { ...orb };
97
+ next.vy += config.gravity * dt;
98
+ next.vx *= config.damping;
99
+ next.vy *= config.damping;
100
+ next.x += next.vx * dt;
101
+ next.y += next.vy * dt;
102
+ next.age += dt;
103
+ if (next.x - next.radius < 0) {
104
+ next.x = next.radius;
105
+ next.vx = Math.abs(next.vx) * config.collisionDamping;
106
+ } else if (next.x + next.radius > config.width) {
107
+ next.x = config.width - next.radius;
108
+ next.vx = -Math.abs(next.vx) * config.collisionDamping;
109
+ }
110
+ if (next.y + next.radius > config.height) {
111
+ next.y = config.height - next.radius;
112
+ next.vy = -Math.abs(next.vy) * config.collisionDamping;
113
+ }
114
+ return next;
115
+ });
116
+ }
117
+
118
+ // src/mikuFusionGame/engine/merge.ts
119
+ function canMerge(a, b, config) {
120
+ if (a.level !== b.level) {
121
+ return false;
122
+ }
123
+ if (a.level >= config.maxLevel) {
124
+ return false;
125
+ }
126
+ const dx = b.x - a.x;
127
+ const dy = b.y - a.y;
128
+ const distance = Math.sqrt(dx * dx + dy * dy);
129
+ const threshold = a.radius + b.radius + 0.75;
130
+ return distance <= threshold;
131
+ }
132
+ function mergeSameLevelOrbs(orbs, config) {
133
+ const mergedIds = /* @__PURE__ */ new Set();
134
+ const spawned = [];
135
+ let scoreGain = 0;
136
+ let chainIndex = 0;
137
+ for (let i = 0; i < orbs.length; i += 1) {
138
+ if (mergedIds.has(orbs[i].id)) {
139
+ continue;
140
+ }
141
+ for (let j = i + 1; j < orbs.length; j += 1) {
142
+ if (mergedIds.has(orbs[j].id)) {
143
+ continue;
144
+ }
145
+ const a = orbs[i];
146
+ const b = orbs[j];
147
+ if (!canMerge(a, b, config)) {
148
+ continue;
149
+ }
150
+ mergedIds.add(a.id);
151
+ mergedIds.add(b.id);
152
+ chainIndex += 1;
153
+ const nextLevel = Math.min(config.maxLevel, a.level + 1);
154
+ const mergedOrb = {
155
+ id: `orb-${a.id}-${b.id}-${chainIndex}`,
156
+ x: (a.x + b.x) / 2,
157
+ y: (a.y + b.y) / 2,
158
+ vx: (a.vx + b.vx) / 2 * 0.4,
159
+ vy: (a.vy + b.vy) / 2 * 0.4,
160
+ radius: getRadiusByLevel(nextLevel),
161
+ level: nextLevel,
162
+ age: Math.min(a.age, b.age)
163
+ };
164
+ spawned.push(mergedOrb);
165
+ scoreGain += getMergeScore(nextLevel, chainIndex);
166
+ if (chainIndex >= config.maxMergesPerTick) {
167
+ break;
168
+ }
169
+ }
170
+ if (chainIndex >= config.maxMergesPerTick) {
171
+ break;
172
+ }
173
+ }
174
+ if (mergedIds.size === 0) {
175
+ return {
176
+ orbs,
177
+ scoreGain: 0,
178
+ mergeCount: 0
179
+ };
180
+ }
181
+ const survivors = orbs.filter((orb) => !mergedIds.has(orb.id));
182
+ const mergedOrbs = survivors.concat(spawned);
183
+ return {
184
+ orbs: mergedOrbs,
185
+ scoreGain,
186
+ mergeCount: chainIndex
187
+ };
188
+ }
189
+
190
+ // src/mikuFusionGame/engine/stateMachine.ts
191
+ var ALLOWED_TRANSITIONS = {
192
+ ready: ["playing"],
193
+ playing: ["paused", "gameOver", "ready"],
194
+ paused: ["playing", "ready"],
195
+ gameOver: ["ready", "playing"]
196
+ };
197
+ function canTransition(from, to) {
198
+ return ALLOWED_TRANSITIONS[from].includes(to);
199
+ }
200
+
201
+ // src/mikuFusionGame/hooks/useMikuFusionGame.ts
202
+ function clamp(value, min, max) {
203
+ return Math.max(min, Math.min(max, value));
204
+ }
205
+ function nextOrbLevel(weights) {
206
+ const random = Math.random();
207
+ let cumulative = 0;
208
+ for (let i = 0; i < weights.length; i += 1) {
209
+ cumulative += weights[i] ?? 0;
210
+ if (random <= cumulative) {
211
+ return i + 1;
212
+ }
213
+ }
214
+ return Math.max(1, weights.length);
215
+ }
216
+ function getOrbColor(level, config) {
217
+ const index = Math.max(0, Math.min(config.theme.orbColors.length - 1, level - 1));
218
+ return config.theme.orbColors[index] || "#22d3ee";
219
+ }
220
+ function drawScene(context, width, height, config, orbs, aimX, nextLevel, status) {
221
+ const gradient = context.createLinearGradient(0, 0, 0, height);
222
+ gradient.addColorStop(0, config.theme.backgroundTop);
223
+ gradient.addColorStop(1, config.theme.backgroundBottom);
224
+ context.fillStyle = gradient;
225
+ context.fillRect(0, 0, width, height);
226
+ context.lineWidth = 2;
227
+ context.strokeStyle = config.theme.lossLine;
228
+ context.setLineDash([6, 6]);
229
+ context.beginPath();
230
+ context.moveTo(0, config.lossLineY);
231
+ context.lineTo(width, config.lossLineY);
232
+ context.stroke();
233
+ context.setLineDash([]);
234
+ context.strokeStyle = config.theme.aimLine;
235
+ context.lineWidth = 1.5;
236
+ context.beginPath();
237
+ context.moveTo(aimX, 0);
238
+ context.lineTo(aimX, config.spawnY);
239
+ context.stroke();
240
+ const previewRadius = getRadiusByLevel(nextLevel);
241
+ context.fillStyle = getOrbColor(nextLevel, config);
242
+ context.beginPath();
243
+ context.arc(aimX, config.spawnY - previewRadius - 4, previewRadius, 0, Math.PI * 2);
244
+ context.fill();
245
+ orbs.forEach((orb) => {
246
+ context.beginPath();
247
+ context.fillStyle = getOrbColor(orb.level, config);
248
+ context.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2);
249
+ context.fill();
250
+ context.fillStyle = "#0f172a";
251
+ context.font = "bold 12px system-ui";
252
+ context.textAlign = "center";
253
+ context.textBaseline = "middle";
254
+ context.fillText(`${LEVEL_LABEL_PREFIX}${orb.level}`, orb.x, orb.y);
255
+ });
256
+ if (status === "paused") {
257
+ context.fillStyle = "rgba(15, 23, 42, 0.5)";
258
+ context.fillRect(0, 0, width, height);
259
+ context.fillStyle = "#ffffff";
260
+ context.font = "bold 30px system-ui";
261
+ context.textAlign = "center";
262
+ context.fillText("Paused", width / 2, height / 2);
263
+ }
264
+ }
265
+ function normalizeConfig(config) {
266
+ const mergedTheme = {
267
+ ...DEFAULT_MIKU_FUSION_CONFIG.theme,
268
+ ...config?.theme ?? {}
269
+ };
270
+ const merged = {
271
+ ...DEFAULT_MIKU_FUSION_CONFIG,
272
+ ...config ?? {},
273
+ theme: mergedTheme
274
+ };
275
+ const totalWeight = merged.spawnWeights.reduce((sum, current) => sum + current, 0);
276
+ if (totalWeight <= 0) {
277
+ merged.spawnWeights = [...DEFAULT_MIKU_FUSION_CONFIG.spawnWeights];
278
+ } else if (Math.abs(totalWeight - 1) > 1e-3) {
279
+ merged.spawnWeights = merged.spawnWeights.map((weight) => weight / totalWeight);
280
+ }
281
+ return merged;
282
+ }
283
+ function useMikuFusionGame(options = {}) {
284
+ const configRef = useRef(normalizeConfig(options));
285
+ const config = configRef.current;
286
+ const onScoreChangeRef = useRef(options.onScoreChange);
287
+ const onGameOverRef = useRef(options.onGameOver);
288
+ useEffect(() => {
289
+ onScoreChangeRef.current = options.onScoreChange;
290
+ onGameOverRef.current = options.onGameOver;
291
+ }, [options.onGameOver, options.onScoreChange]);
292
+ const [bestScore, setBestScore] = useLocalStorage(
293
+ options.storageKey || "sa2kit:mikuFusionGame:bestScore",
294
+ 0
295
+ );
296
+ const canvasRef = useRef(null);
297
+ const rafRef = useRef(null);
298
+ const lastTsRef = useRef(0);
299
+ const idRef = useRef(0);
300
+ const orbsRef = useRef([]);
301
+ const aimXRef = useRef(config.width / 2);
302
+ const scoreRef = useRef(0);
303
+ const [status, setStatus] = useState("ready");
304
+ const [score, setScore] = useState(0);
305
+ const [nextLevel, setNextLevel] = useState(() => nextOrbLevel(config.spawnWeights));
306
+ const draw = useCallback(
307
+ (nextStatus = status) => {
308
+ const canvas = canvasRef.current;
309
+ if (!canvas) {
310
+ return;
311
+ }
312
+ const context = canvas.getContext("2d");
313
+ if (!context) {
314
+ return;
315
+ }
316
+ drawScene(
317
+ context,
318
+ config.width,
319
+ config.height,
320
+ config,
321
+ orbsRef.current,
322
+ aimXRef.current,
323
+ nextLevel,
324
+ nextStatus
325
+ );
326
+ },
327
+ [config, nextLevel, status]
328
+ );
329
+ const transitionStatus = useCallback((nextStatus) => {
330
+ setStatus((previous) => canTransition(previous, nextStatus) ? nextStatus : previous);
331
+ }, []);
332
+ const spawnOrb = useCallback(() => {
333
+ if (status === "paused" || status === "gameOver") {
334
+ return;
335
+ }
336
+ if (status === "ready") {
337
+ transitionStatus("playing");
338
+ }
339
+ idRef.current += 1;
340
+ const radius = getRadiusByLevel(nextLevel);
341
+ const x = clamp(aimXRef.current, radius, config.width - radius);
342
+ const created = {
343
+ id: `${idRef.current}`,
344
+ x,
345
+ y: config.spawnY,
346
+ vx: 0,
347
+ vy: 0,
348
+ radius,
349
+ level: nextLevel,
350
+ age: 0
351
+ };
352
+ const nextOrbs = orbsRef.current.concat(created);
353
+ orbsRef.current = nextOrbs.slice(-config.maxOrbs);
354
+ setNextLevel(nextOrbLevel(config.spawnWeights));
355
+ }, [config, nextLevel, status, transitionStatus]);
356
+ const restart = useCallback(() => {
357
+ orbsRef.current = [];
358
+ scoreRef.current = 0;
359
+ setScore(0);
360
+ setNextLevel(nextOrbLevel(config.spawnWeights));
361
+ transitionStatus("ready");
362
+ draw("ready");
363
+ }, [config.spawnWeights, draw, transitionStatus]);
364
+ const togglePause = useCallback(() => {
365
+ setStatus((previous) => {
366
+ if (previous === "playing" && canTransition(previous, "paused")) {
367
+ return "paused";
368
+ }
369
+ if (previous === "paused" && canTransition(previous, "playing")) {
370
+ return "playing";
371
+ }
372
+ return previous;
373
+ });
374
+ }, []);
375
+ const setAimFromClientX = useCallback((clientX, rect) => {
376
+ const ratio = config.width / rect.width;
377
+ const x = (clientX - rect.left) * ratio;
378
+ aimXRef.current = clamp(x, 0, config.width);
379
+ }, [config.width]);
380
+ const handleDrop = useCallback(
381
+ (clientX, rect) => {
382
+ setAimFromClientX(clientX, rect);
383
+ spawnOrb();
384
+ },
385
+ [setAimFromClientX, spawnOrb]
386
+ );
387
+ useEffect(() => {
388
+ draw(status);
389
+ }, [draw, status]);
390
+ useEffect(() => {
391
+ if (status !== "playing") {
392
+ if (rafRef.current) {
393
+ window.cancelAnimationFrame(rafRef.current);
394
+ }
395
+ lastTsRef.current = 0;
396
+ draw(status);
397
+ return;
398
+ }
399
+ const frame = (timestamp) => {
400
+ if (lastTsRef.current === 0) {
401
+ lastTsRef.current = timestamp;
402
+ }
403
+ const dt = Math.min((timestamp - lastTsRef.current) / 1e3, 0.033);
404
+ lastTsRef.current = timestamp;
405
+ let orbs = stepPhysics(orbsRef.current, config, dt);
406
+ orbs = resolveCircleCollisions(orbs, config);
407
+ const merged = mergeSameLevelOrbs(orbs, config);
408
+ orbs = merged.orbs;
409
+ orbsRef.current = orbs.slice(-config.maxOrbs);
410
+ if (merged.scoreGain > 0) {
411
+ scoreRef.current += merged.scoreGain;
412
+ setScore(scoreRef.current);
413
+ onScoreChangeRef.current?.(scoreRef.current);
414
+ }
415
+ const gameOver = orbs.some(
416
+ (orb) => orb.age > config.gameOverAgeThreshold && orb.y - orb.radius <= config.lossLineY && Math.abs(orb.vy) < 90
417
+ );
418
+ if (gameOver) {
419
+ transitionStatus("gameOver");
420
+ const latestBest = Math.max(bestScore, scoreRef.current);
421
+ if (latestBest !== bestScore) {
422
+ setBestScore(latestBest);
423
+ }
424
+ onGameOverRef.current?.(scoreRef.current, latestBest);
425
+ draw("gameOver");
426
+ return;
427
+ }
428
+ draw("playing");
429
+ rafRef.current = window.requestAnimationFrame(frame);
430
+ };
431
+ rafRef.current = window.requestAnimationFrame(frame);
432
+ return () => {
433
+ if (rafRef.current) {
434
+ window.cancelAnimationFrame(rafRef.current);
435
+ }
436
+ };
437
+ }, [
438
+ bestScore,
439
+ config,
440
+ draw,
441
+ setBestScore,
442
+ status,
443
+ transitionStatus
444
+ ]);
445
+ useEffect(() => {
446
+ if (score > bestScore) {
447
+ setBestScore(score);
448
+ }
449
+ }, [bestScore, score, setBestScore]);
450
+ return {
451
+ canvasRef,
452
+ status,
453
+ score,
454
+ bestScore: Math.max(bestScore, score),
455
+ nextLevel,
456
+ config,
457
+ setAimFromClientX,
458
+ handleDrop,
459
+ togglePause,
460
+ restart
461
+ };
462
+ }
463
+ function useResponsiveCanvas(logicalWidth, logicalHeight, containerRef) {
464
+ const [size, setSize] = useState({
465
+ displayWidth: logicalWidth,
466
+ displayHeight: logicalHeight,
467
+ scale: 1
468
+ });
469
+ useEffect(() => {
470
+ const updateSize = () => {
471
+ const container2 = containerRef.current;
472
+ if (!container2 || typeof window === "undefined") {
473
+ return;
474
+ }
475
+ const rect = container2.getBoundingClientRect();
476
+ const maxWidth = Math.max(280, rect.width);
477
+ const maxHeight = Math.max(420, Math.floor(window.innerHeight * 0.78));
478
+ const scale = Math.min(maxWidth / logicalWidth, maxHeight / logicalHeight, 1);
479
+ setSize({
480
+ displayWidth: Math.floor(logicalWidth * scale),
481
+ displayHeight: Math.floor(logicalHeight * scale),
482
+ scale
483
+ });
484
+ };
485
+ updateSize();
486
+ const container = containerRef.current;
487
+ const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(updateSize) : null;
488
+ if (container && observer) {
489
+ observer.observe(container);
490
+ }
491
+ window.addEventListener("resize", updateSize);
492
+ return () => {
493
+ observer?.disconnect();
494
+ window.removeEventListener("resize", updateSize);
495
+ };
496
+ }, [containerRef, logicalWidth, logicalHeight]);
497
+ return size;
498
+ }
499
+ function GameCanvas({
500
+ canvasRef,
501
+ width,
502
+ height,
503
+ displayWidth,
504
+ displayHeight,
505
+ onPointerMove,
506
+ onPointerDown,
507
+ onPointerUp
508
+ }) {
509
+ return /* @__PURE__ */ React3.createElement(
510
+ "canvas",
511
+ {
512
+ ref: canvasRef,
513
+ width,
514
+ height,
515
+ className: "rounded-xl border border-cyan-200 bg-cyan-50 shadow-sm",
516
+ style: {
517
+ width: `${displayWidth}px`,
518
+ height: `${displayHeight}px`,
519
+ maxWidth: "100%",
520
+ touchAction: "none"
521
+ },
522
+ onPointerMove,
523
+ onPointerDown,
524
+ onPointerUp,
525
+ onContextMenu: (event) => event.preventDefault()
526
+ }
527
+ );
528
+ }
529
+ function GameControls({ isPaused, isPlaying, onTogglePause, onRestart }) {
530
+ return /* @__PURE__ */ React3.createElement("div", { className: "grid grid-cols-2 gap-2" }, /* @__PURE__ */ React3.createElement(
531
+ "button",
532
+ {
533
+ type: "button",
534
+ className: "rounded-lg bg-cyan-600 px-4 py-3 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50",
535
+ onClick: onTogglePause,
536
+ disabled: !isPlaying
537
+ },
538
+ isPaused ? "\u7EE7\u7EED" : "\u6682\u505C"
539
+ ), /* @__PURE__ */ React3.createElement(
540
+ "button",
541
+ {
542
+ type: "button",
543
+ className: "rounded-lg bg-emerald-600 px-4 py-3 text-sm font-semibold text-white",
544
+ onClick: onRestart
545
+ },
546
+ "\u91CD\u5F00"
547
+ ));
548
+ }
549
+ function GameHUD({ score, bestScore, nextLevel, statusText }) {
550
+ return /* @__PURE__ */ React3.createElement("div", { className: "flex items-center justify-between gap-2 rounded-lg border border-cyan-200 bg-white/80 p-3" }, /* @__PURE__ */ React3.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React3.createElement("div", { className: "text-xs text-slate-500" }, "\u5F53\u524D\u5206\u6570"), /* @__PURE__ */ React3.createElement("div", { className: "text-xl font-bold text-cyan-700" }, score), /* @__PURE__ */ React3.createElement("div", { className: "text-xs text-slate-500" }, "\u6700\u9AD8\u5206 ", bestScore)), /* @__PURE__ */ React3.createElement("div", { className: "text-right" }, /* @__PURE__ */ React3.createElement("div", { className: "text-xs text-slate-500" }, "\u4E0B\u4E00\u4E2A"), /* @__PURE__ */ React3.createElement("div", { className: "text-base font-semibold text-emerald-700" }, "M", nextLevel), /* @__PURE__ */ React3.createElement("div", { className: "text-xs font-medium text-slate-600" }, statusText)));
551
+ }
552
+ function GameResultModal({ open, score, bestScore, onRestart }) {
553
+ if (!open) {
554
+ return null;
555
+ }
556
+ return /* @__PURE__ */ React3.createElement("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-slate-900/50 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "pointer-events-auto w-full max-w-xs rounded-xl bg-white p-5 shadow-2xl" }, /* @__PURE__ */ React3.createElement("h3", { className: "text-lg font-semibold text-slate-900" }, "\u6E38\u620F\u7ED3\u675F"), /* @__PURE__ */ React3.createElement("p", { className: "mt-2 text-sm text-slate-600" }, "\u5F53\u524D\u5206\u6570\uFF1A", score), /* @__PURE__ */ React3.createElement("p", { className: "text-sm text-slate-600" }, "\u6700\u9AD8\u5206\uFF1A", bestScore), /* @__PURE__ */ React3.createElement(
557
+ "button",
558
+ {
559
+ type: "button",
560
+ className: "mt-4 w-full rounded-lg bg-cyan-600 px-4 py-2 text-sm font-semibold text-white",
561
+ onClick: onRestart
562
+ },
563
+ "\u518D\u6765\u4E00\u5C40"
564
+ )));
565
+ }
566
+
567
+ // src/mikuFusionGame/components/MikuFusionGame.tsx
568
+ function MikuFusionGame({ className, storageKey, ...options }) {
569
+ const containerRef = useRef(null);
570
+ const {
571
+ canvasRef,
572
+ status,
573
+ score,
574
+ bestScore,
575
+ nextLevel,
576
+ config,
577
+ setAimFromClientX,
578
+ handleDrop,
579
+ togglePause,
580
+ restart
581
+ } = useMikuFusionGame({ ...options, storageKey });
582
+ const { displayWidth, displayHeight } = useResponsiveCanvas(
583
+ config.width,
584
+ config.height,
585
+ containerRef
586
+ );
587
+ const statusText = useMemo(() => {
588
+ if (status === "ready") {
589
+ return "\u5F85\u5F00\u59CB";
590
+ }
591
+ if (status === "playing") {
592
+ return "\u8FDB\u884C\u4E2D";
593
+ }
594
+ if (status === "paused") {
595
+ return "\u5DF2\u6682\u505C";
596
+ }
597
+ return "\u5DF2\u7ED3\u675F";
598
+ }, [status]);
599
+ const handlePointerMove = (event) => {
600
+ const rect = event.currentTarget.getBoundingClientRect();
601
+ setAimFromClientX(event.clientX, rect);
602
+ };
603
+ const handlePointerDown = (event) => {
604
+ event.currentTarget.setPointerCapture(event.pointerId);
605
+ const rect = event.currentTarget.getBoundingClientRect();
606
+ setAimFromClientX(event.clientX, rect);
607
+ };
608
+ const handlePointerUp = (event) => {
609
+ const rect = event.currentTarget.getBoundingClientRect();
610
+ handleDrop(event.clientX, rect);
611
+ event.currentTarget.releasePointerCapture(event.pointerId);
612
+ };
613
+ const handleKeyDown = (event) => {
614
+ if (event.key.toLowerCase() === "r") {
615
+ restart();
616
+ }
617
+ if (event.key.toLowerCase() === "p") {
618
+ togglePause();
619
+ }
620
+ };
621
+ return /* @__PURE__ */ React3.createElement(
622
+ "div",
623
+ {
624
+ ref: containerRef,
625
+ tabIndex: 0,
626
+ onKeyDown: handleKeyDown,
627
+ className: `relative mx-auto flex w-full max-w-xl flex-col gap-3 p-3 md:p-4 ${className || ""}`,
628
+ style: {
629
+ paddingBottom: "max(0.75rem, env(safe-area-inset-bottom))"
630
+ }
631
+ },
632
+ /* @__PURE__ */ React3.createElement(GameHUD, { score, bestScore, nextLevel, statusText }),
633
+ /* @__PURE__ */ React3.createElement("div", { className: "relative mx-auto" }, /* @__PURE__ */ React3.createElement(
634
+ GameCanvas,
635
+ {
636
+ canvasRef,
637
+ width: config.width,
638
+ height: config.height,
639
+ displayWidth,
640
+ displayHeight,
641
+ onPointerMove: handlePointerMove,
642
+ onPointerDown: handlePointerDown,
643
+ onPointerUp: handlePointerUp
644
+ }
645
+ ), /* @__PURE__ */ React3.createElement(GameResultModal, { open: status === "gameOver", score, bestScore, onRestart: restart })),
646
+ /* @__PURE__ */ React3.createElement(
647
+ GameControls,
648
+ {
649
+ isPaused: status === "paused",
650
+ isPlaying: status === "playing" || status === "paused",
651
+ onTogglePause: togglePause,
652
+ onRestart: restart
653
+ }
654
+ )
655
+ );
656
+ }
657
+
658
+ // src/mikuFusionGame/content/index.ts
659
+ var defaultContentRegistry = {
660
+ producers: [],
661
+ songs: [],
662
+ themes: []
663
+ };
664
+
665
+ export { BASE_RADIUS, DEFAULT_MIKU_FUSION_CONFIG, LEVEL_LABEL_PREFIX, MikuFusionGame, RADIUS_STEP, defaultContentRegistry, useMikuFusionGame, useResponsiveCanvas };
666
+ //# sourceMappingURL=index.mjs.map
667
+ //# sourceMappingURL=index.mjs.map