dsh-dock 0.9.6 → 0.9.8

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 (35) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +325 -325
  3. package/client.js +42 -8
  4. package/cordis.patch.yml +2 -2
  5. package/features/animation/host.js +660 -660
  6. package/features/animation/view.jsx +2247 -2247
  7. package/features/balance/host.js +494 -494
  8. package/features/balance/view.js +351 -351
  9. package/features/games/games/breakout.jsx +227 -227
  10. package/features/games/games/gomoku.jsx +244 -244
  11. package/features/games/games/racer.jsx +169 -169
  12. package/features/games/games/shared.jsx +118 -118
  13. package/features/games/games/snake.jsx +144 -144
  14. package/features/games/games/sokoban.jsx +180 -180
  15. package/features/games/games/tank.jsx +128 -128
  16. package/features/games/games/tetris.jsx +308 -308
  17. package/features/games/games/xiangqi.jsx +277 -277
  18. package/features/games/tank-core.js +152 -152
  19. package/features/games/view.jsx +639 -639
  20. package/features/games/xiangqi-core.js +299 -299
  21. package/features/heartbeat/view.js +35 -35
  22. package/features/mobile-relay/gateway.js +472 -472
  23. package/features/mobile-relay/host.js +570 -569
  24. package/features/mobile-relay/view.jsx +300 -300
  25. package/features/modelconfig/host.js +403 -403
  26. package/features/modelconfig/view.js +490 -490
  27. package/features/theme/view.js +61 -61
  28. package/features/tokenlog/host.js +690 -678
  29. package/features/tokenlog/view.jsx +561 -561
  30. package/features/visionproxy/host.js +315 -315
  31. package/index.js +175 -118
  32. package/package.json +59 -59
  33. package/src/client.jsx +668 -668
  34. package/src/host-core.js +98 -95
  35. package/src/shared.js +167 -130
@@ -1,169 +1,169 @@
1
- /* 趣味游戏 · 极速赛车(经典机台)——纯 Client;canvas 渲染,躲避来车、收集金币,越开越快。 */
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { focusStage, useGameControls } from "./shared.jsx";
4
-
5
- const W = 300, H = 420;
6
- const ROAD_W = 232, ROAD_X = (W - ROAD_W) / 2;
7
- const CAR_W = 34, CAR_H = 52;
8
- const CAR_COLORS = ["#f87171", "#fb923c", "#c084fc", "#38bdf8", "#4ade80", "#fde047"];
9
- function laneX(lane) { return ROAD_X + ROAD_W * (lane + 0.5) / 3 - CAR_W / 2; }
10
- function overlap(a, b) {
11
- return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
12
- }
13
- function rect(ctx, x, y, w, h, r, fill) {
14
- ctx.fillStyle = fill;
15
- ctx.beginPath();
16
- ctx.moveTo(x + r, y);
17
- ctx.arcTo(x + w, y, x + w, y + h, r);
18
- ctx.arcTo(x + w, y + h, x, y + h, r);
19
- ctx.arcTo(x, y + h, x, y, r);
20
- ctx.arcTo(x, y, x + w, y, r);
21
- ctx.closePath();
22
- ctx.fill();
23
- }
24
-
25
- export function RacerGame(props) {
26
- const canvasRef = useRef(null);
27
- const [status, setStatus] = useState("play"); // play | over
28
- const game = useRef(null);
29
- const keys = useRef({});
30
- const statusRef = useRef("play"); statusRef.current = status;
31
- const pausedRef = useRef(!!(props && props.paused)); pausedRef.current = !!(props && props.paused);
32
-
33
- const reset = useCallback(() => {
34
- game.current = {
35
- stars: Array.from({ length: 50 }, () => ({ x: Math.random() * W, y: Math.random() * H, a: .2 + Math.random() * .5 })),
36
- player: { x: laneX(1), y: H - CAR_H - 16, w: CAR_W, h: CAR_H },
37
- enemies: [], coins: [],
38
- dist: 0, coinsGot: 0, spawn: 0, coinSpawn: 0,
39
- speed: 3.4, over: false,
40
- };
41
- }, []);
42
-
43
- useEffect(() => { reset(); setStatus("play"); }, [reset]);
44
-
45
- const launch = useCallback(() => {
46
- reset(); setStatus("play");
47
- }, [reset]);
48
-
49
- useEffect(() => {
50
- const draw = () => {
51
- const g = game.current, ctx = canvasRef.current && canvasRef.current.getContext("2d");
52
- if (!g || !ctx) return;
53
- // 背景
54
- ctx.fillStyle = "#0b1024"; ctx.fillRect(0, 0, W, H);
55
- ctx.fillStyle = "#1b2bb0";
56
- for (const s of g.stars) { ctx.globalAlpha = s.a; ctx.fillRect(s.x, s.y, 1.5, 1.5); }
57
- ctx.globalAlpha = 1;
58
- // 路面
59
- ctx.fillStyle = "#20243a"; ctx.fillRect(ROAD_X, 0, ROAD_W, H);
60
- ctx.fillStyle = "rgba(255,255,255,.09)"; ctx.fillRect(ROAD_X + 6, 0, 4, H); ctx.fillRect(ROAD_X + ROAD_W - 10, 0, 4, H);
61
- // 车道虚线
62
- ctx.fillStyle = "rgba(255,255,255,.16)";
63
- for (let i = 1; i < 3; i += 1) {
64
- const lx = ROAD_X + ROAD_W * i / 3 - 1;
65
- for (let yy = (g.dist % 60); yy < H; yy += 60) ctx.fillRect(lx, yy, 2, 26);
66
- }
67
- if (pausedRef.current || g.over) { drawHUD(ctx, g); return; }
68
- // 挡板移动(玩家车)
69
- if (keys.current.left) g.player.x -= 5;
70
- if (keys.current.right) g.player.x += 5;
71
- g.player.x = Math.max(ROAD_X + 2, Math.min(ROAD_X + ROAD_W - CAR_W - 2, g.player.x));
72
- g.dist += g.speed * .16;
73
- // 生成来车
74
- g.spawn += 1;
75
- if (g.spawn > Math.max(20, 56 - g.dist / 600)) {
76
- g.spawn = 0;
77
- const lane = Math.floor(Math.random() * 3);
78
- const v = g.speed * (1.1 + Math.random() * .6);
79
- g.enemies.push({ x: laneX(lane), y: -CAR_H - Math.random() * 60, w: CAR_W, h: CAR_H, v, c: CAR_COLORS[Math.floor(Math.random() * CAR_COLORS.length)] });
80
- }
81
- // 生成金币
82
- g.coinSpawn += 1;
83
- if (g.coinSpawn > Math.max(30, 70 - g.dist / 500)) {
84
- g.coinSpawn = 0;
85
- g.coins.push({ x: laneX(Math.floor(Math.random() * 3)), y: -30, r: 11, got: false });
86
- }
87
- // 更新来车/金币
88
- for (const e of g.enemies) e.y += e.v;
89
- for (const c of g.coins) c.y += g.speed;
90
- g.enemies = g.enemies.filter((e) => e.y < H + 20);
91
- g.coins = g.coins.filter((c) => c.y < H + 20);
92
- // 碰撞检测
93
- for (const e of g.enemies) {
94
- if (overlap(g.player, e)) { g.over = true; setStatus("over"); statusRef.current = "over"; }
95
- }
96
- for (const c of g.coins) {
97
- if (!c.got && Math.abs(c.x + c.r - (g.player.x + CAR_W / 2)) < CAR_W / 2 + 8 && Math.abs(c.y - (g.player.y + CAR_H / 2)) < CAR_H / 2 + 10) {
98
- c.got = true; g.coinsGot += 1;
99
- }
100
- }
101
- // 画来车
102
- for (const e of g.enemies) { rect(ctx, e.x, e.y, e.w, e.h, 7, e.c); ctx.fillStyle = "rgba(255,255,255,.25)"; ctx.fillRect(e.x + 6, e.y + 7, e.w - 12, 12); }
103
- // 画金币
104
- ctx.fillStyle = "#fde047";
105
- for (const c of g.coins) { ctx.beginPath(); ctx.arc(c.x + c.r, c.y, c.r, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "rgba(120,53,15,.4)"; ctx.beginPath(); ctx.arc(c.x + c.r, c.y, c.r * .5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "#fde047"; }
106
- // 画玩家车
107
- drawCar(ctx, g.player.x, g.player.y, "#22d3ee");
108
- drawHUD(ctx, g);
109
- };
110
- const drawHUD = (ctx, g) => {
111
- ctx.fillStyle = "#e7ecff";
112
- ctx.font = "bold 13px system-ui, sans-serif";
113
- ctx.textBaseline = "top";
114
- ctx.fillText("距离 " + Math.floor(g.dist / 60) + " m", 10, 10);
115
- ctx.fillStyle = "#fde047";
116
- ctx.fillText("金币 × " + g.coinsGot, W - 70, 10);
117
- };
118
- function drawCar(ctx, x, y, c) {
119
- rect(ctx, x, y, CAR_W, CAR_H, 9, c);
120
- ctx.fillStyle = "rgba(255,255,255,.3)"; rect(ctx, x + 5, y + 6, CAR_W - 10, 13, 4, "rgba(255,255,255,.3)");
121
- ctx.fillStyle = "rgba(15,23,42,.5)"; rect(ctx, x + 5, y + CAR_H - 16, CAR_W - 10, 9, 3, "rgba(15,23,42,.5)");
122
- ctx.fillStyle = "rgba(255,255,255,.45)";
123
- rect(ctx, x + 3, y + CAR_H - 4, 6, 5, 2, "#fde047");
124
- rect(ctx, x + CAR_W - 9, y + CAR_H - 4, 6, 5, 2, "#fde047");
125
- }
126
- let raf = requestAnimationFrame(function loop() { draw(); raf = requestAnimationFrame(loop); });
127
- return () => cancelAnimationFrame(raf);
128
- }, []);
129
-
130
- const onKeyDown = useCallback((event) => {
131
- if (pausedRef.current) return;
132
- if (event.key === "ArrowLeft") { event.preventDefault(); keys.current.left = true; }
133
- else if (event.key === "ArrowRight") { event.preventDefault(); keys.current.right = true; }
134
- else if (event.key.toLowerCase() === "r") { event.preventDefault(); launch(); }
135
- }, [launch]);
136
- const onKeyUp = useCallback((event) => {
137
- if (event.key === "ArrowLeft") keys.current.left = false;
138
- else if (event.key === "ArrowRight") keys.current.right = false;
139
- }, []);
140
-
141
- const onPointerMove = useCallback((e) => {
142
- const g = game.current, cv = canvasRef.current;
143
- if (!g || !cv) return;
144
- const rect0 = cv.getBoundingClientRect();
145
- const scale = rect0.width / W;
146
- g.player.x = Math.max(ROAD_X + 2, Math.min(ROAD_X + ROAD_W - CAR_W - 2, (e.clientX - rect0.left) / scale - CAR_W / 2));
147
- }, []);
148
- useGameControls(canvasRef, pausedRef.current, onKeyDown, onKeyUp);
149
-
150
- return <section className="dgame-game" aria-label="极速赛车">
151
- <div className="dgame-game-head"><div><h3>极速赛车</h3><p>左右移动避开迎面来车、收集金币,越开越快;撞车即结束。距离与金币实时显示在游戏画面左上/右上。</p></div><div className="dgame-score"><span>操作</span><span>←/→ 或鼠标</span></div></div>
152
- <div className="dgame-racer">
153
- <canvas ref={canvasRef} width={W} height={H} tabIndex={0} onKeyDown={onKeyDown} onKeyUp={onKeyUp} onPointerMove={onPointerMove} onClick={() => focusStage(canvasRef)} className="dgame-racer-canvas" aria-label="极速赛车游戏区域,左右方向键或鼠标控制方向" />
154
- {status === "over" ? <div className="dgame-over"><strong>撞车了 · 行驶 {(game.current && Math.floor(game.current.dist / 60)) || 0} 米</strong><button type="button" onClick={launch}>重新出发</button></div> : null}
155
- </div>
156
- <div className="dgame-controls"><button type="button" aria-label="向左" onClick={() => { keys.current.left = false; if (game.current) game.current.player.x = Math.max(ROAD_X + 2, game.current.player.x - 14); }}>← 左移</button><span>←/→ 或鼠标移动 · R 重开</span><button type="button" aria-label="向右" onClick={() => { keys.current.right = false; if (game.current) game.current.player.x = Math.min(ROAD_X + ROAD_W - CAR_W - 2, game.current.player.x + 14); }}>右移 {'->'}</button></div>
157
- </section>;
158
- }
159
-
160
- export function RacerPreview() {
161
- return <span className="dgcov-prev dgcov-prev-racer" aria-hidden="true">
162
- <i style={{ left: "20%", top: "8%" }} /><i style={{ left: "55%", top: "22%" }} /><i style={{ left: "20%", top: "42%" }} /><i style={{ left: "64%", top: "58%" }} /><i style={{ left: "38%", top: "74%" }} />
163
- <b />
164
- </span>;
165
- }
166
-
167
- export const racerGame = { Game: RacerGame, Preview: RacerPreview, css: `
168
- .dgame-racer{position:relative;align-self:center;width:min(100%,300px);margin:0 auto}.dgame-racer-canvas{display:block;width:100%;aspect-ratio:300/420;border:1px solid color-mix(in srgb,rgb(34 211 238) 32%,var(--dsw-alias-border-l1));border-radius:10px;outline:none;background:#0b1024;box-shadow:0 4px 18px rgb(0 0 0/.4)}.dgame-racer-canvas:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,rgb(34 211 238) 35%,transparent)}.dgame-racer .dgame-over{position:absolute;inset:0;border-radius:10px}.dgame-racer-stat{color:var(--dsw-alias-label-tertiary)}
169
- ` };
1
+ /* 趣味游戏 · 极速赛车(经典机台)——纯 Client;canvas 渲染,躲避来车、收集金币,越开越快。 */
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { focusStage, useGameControls } from "./shared.jsx";
4
+
5
+ const W = 300, H = 420;
6
+ const ROAD_W = 232, ROAD_X = (W - ROAD_W) / 2;
7
+ const CAR_W = 34, CAR_H = 52;
8
+ const CAR_COLORS = ["#f87171", "#fb923c", "#c084fc", "#38bdf8", "#4ade80", "#fde047"];
9
+ function laneX(lane) { return ROAD_X + ROAD_W * (lane + 0.5) / 3 - CAR_W / 2; }
10
+ function overlap(a, b) {
11
+ return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
12
+ }
13
+ function rect(ctx, x, y, w, h, r, fill) {
14
+ ctx.fillStyle = fill;
15
+ ctx.beginPath();
16
+ ctx.moveTo(x + r, y);
17
+ ctx.arcTo(x + w, y, x + w, y + h, r);
18
+ ctx.arcTo(x + w, y + h, x, y + h, r);
19
+ ctx.arcTo(x, y + h, x, y, r);
20
+ ctx.arcTo(x, y, x + w, y, r);
21
+ ctx.closePath();
22
+ ctx.fill();
23
+ }
24
+
25
+ export function RacerGame(props) {
26
+ const canvasRef = useRef(null);
27
+ const [status, setStatus] = useState("play"); // play | over
28
+ const game = useRef(null);
29
+ const keys = useRef({});
30
+ const statusRef = useRef("play"); statusRef.current = status;
31
+ const pausedRef = useRef(!!(props && props.paused)); pausedRef.current = !!(props && props.paused);
32
+
33
+ const reset = useCallback(() => {
34
+ game.current = {
35
+ stars: Array.from({ length: 50 }, () => ({ x: Math.random() * W, y: Math.random() * H, a: .2 + Math.random() * .5 })),
36
+ player: { x: laneX(1), y: H - CAR_H - 16, w: CAR_W, h: CAR_H },
37
+ enemies: [], coins: [],
38
+ dist: 0, coinsGot: 0, spawn: 0, coinSpawn: 0,
39
+ speed: 3.4, over: false,
40
+ };
41
+ }, []);
42
+
43
+ useEffect(() => { reset(); setStatus("play"); }, [reset]);
44
+
45
+ const launch = useCallback(() => {
46
+ reset(); setStatus("play");
47
+ }, [reset]);
48
+
49
+ useEffect(() => {
50
+ const draw = () => {
51
+ const g = game.current, ctx = canvasRef.current && canvasRef.current.getContext("2d");
52
+ if (!g || !ctx) return;
53
+ // 背景
54
+ ctx.fillStyle = "#0b1024"; ctx.fillRect(0, 0, W, H);
55
+ ctx.fillStyle = "#1b2bb0";
56
+ for (const s of g.stars) { ctx.globalAlpha = s.a; ctx.fillRect(s.x, s.y, 1.5, 1.5); }
57
+ ctx.globalAlpha = 1;
58
+ // 路面
59
+ ctx.fillStyle = "#20243a"; ctx.fillRect(ROAD_X, 0, ROAD_W, H);
60
+ ctx.fillStyle = "rgba(255,255,255,.09)"; ctx.fillRect(ROAD_X + 6, 0, 4, H); ctx.fillRect(ROAD_X + ROAD_W - 10, 0, 4, H);
61
+ // 车道虚线
62
+ ctx.fillStyle = "rgba(255,255,255,.16)";
63
+ for (let i = 1; i < 3; i += 1) {
64
+ const lx = ROAD_X + ROAD_W * i / 3 - 1;
65
+ for (let yy = (g.dist % 60); yy < H; yy += 60) ctx.fillRect(lx, yy, 2, 26);
66
+ }
67
+ if (pausedRef.current || g.over) { drawHUD(ctx, g); return; }
68
+ // 挡板移动(玩家车)
69
+ if (keys.current.left) g.player.x -= 5;
70
+ if (keys.current.right) g.player.x += 5;
71
+ g.player.x = Math.max(ROAD_X + 2, Math.min(ROAD_X + ROAD_W - CAR_W - 2, g.player.x));
72
+ g.dist += g.speed * .16;
73
+ // 生成来车
74
+ g.spawn += 1;
75
+ if (g.spawn > Math.max(20, 56 - g.dist / 600)) {
76
+ g.spawn = 0;
77
+ const lane = Math.floor(Math.random() * 3);
78
+ const v = g.speed * (1.1 + Math.random() * .6);
79
+ g.enemies.push({ x: laneX(lane), y: -CAR_H - Math.random() * 60, w: CAR_W, h: CAR_H, v, c: CAR_COLORS[Math.floor(Math.random() * CAR_COLORS.length)] });
80
+ }
81
+ // 生成金币
82
+ g.coinSpawn += 1;
83
+ if (g.coinSpawn > Math.max(30, 70 - g.dist / 500)) {
84
+ g.coinSpawn = 0;
85
+ g.coins.push({ x: laneX(Math.floor(Math.random() * 3)), y: -30, r: 11, got: false });
86
+ }
87
+ // 更新来车/金币
88
+ for (const e of g.enemies) e.y += e.v;
89
+ for (const c of g.coins) c.y += g.speed;
90
+ g.enemies = g.enemies.filter((e) => e.y < H + 20);
91
+ g.coins = g.coins.filter((c) => c.y < H + 20);
92
+ // 碰撞检测
93
+ for (const e of g.enemies) {
94
+ if (overlap(g.player, e)) { g.over = true; setStatus("over"); statusRef.current = "over"; }
95
+ }
96
+ for (const c of g.coins) {
97
+ if (!c.got && Math.abs(c.x + c.r - (g.player.x + CAR_W / 2)) < CAR_W / 2 + 8 && Math.abs(c.y - (g.player.y + CAR_H / 2)) < CAR_H / 2 + 10) {
98
+ c.got = true; g.coinsGot += 1;
99
+ }
100
+ }
101
+ // 画来车
102
+ for (const e of g.enemies) { rect(ctx, e.x, e.y, e.w, e.h, 7, e.c); ctx.fillStyle = "rgba(255,255,255,.25)"; ctx.fillRect(e.x + 6, e.y + 7, e.w - 12, 12); }
103
+ // 画金币
104
+ ctx.fillStyle = "#fde047";
105
+ for (const c of g.coins) { ctx.beginPath(); ctx.arc(c.x + c.r, c.y, c.r, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "rgba(120,53,15,.4)"; ctx.beginPath(); ctx.arc(c.x + c.r, c.y, c.r * .5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "#fde047"; }
106
+ // 画玩家车
107
+ drawCar(ctx, g.player.x, g.player.y, "#22d3ee");
108
+ drawHUD(ctx, g);
109
+ };
110
+ const drawHUD = (ctx, g) => {
111
+ ctx.fillStyle = "#e7ecff";
112
+ ctx.font = "bold 13px system-ui, sans-serif";
113
+ ctx.textBaseline = "top";
114
+ ctx.fillText("距离 " + Math.floor(g.dist / 60) + " m", 10, 10);
115
+ ctx.fillStyle = "#fde047";
116
+ ctx.fillText("金币 × " + g.coinsGot, W - 70, 10);
117
+ };
118
+ function drawCar(ctx, x, y, c) {
119
+ rect(ctx, x, y, CAR_W, CAR_H, 9, c);
120
+ ctx.fillStyle = "rgba(255,255,255,.3)"; rect(ctx, x + 5, y + 6, CAR_W - 10, 13, 4, "rgba(255,255,255,.3)");
121
+ ctx.fillStyle = "rgba(15,23,42,.5)"; rect(ctx, x + 5, y + CAR_H - 16, CAR_W - 10, 9, 3, "rgba(15,23,42,.5)");
122
+ ctx.fillStyle = "rgba(255,255,255,.45)";
123
+ rect(ctx, x + 3, y + CAR_H - 4, 6, 5, 2, "#fde047");
124
+ rect(ctx, x + CAR_W - 9, y + CAR_H - 4, 6, 5, 2, "#fde047");
125
+ }
126
+ let raf = requestAnimationFrame(function loop() { draw(); raf = requestAnimationFrame(loop); });
127
+ return () => cancelAnimationFrame(raf);
128
+ }, []);
129
+
130
+ const onKeyDown = useCallback((event) => {
131
+ if (pausedRef.current) return;
132
+ if (event.key === "ArrowLeft") { event.preventDefault(); keys.current.left = true; }
133
+ else if (event.key === "ArrowRight") { event.preventDefault(); keys.current.right = true; }
134
+ else if (event.key.toLowerCase() === "r") { event.preventDefault(); launch(); }
135
+ }, [launch]);
136
+ const onKeyUp = useCallback((event) => {
137
+ if (event.key === "ArrowLeft") keys.current.left = false;
138
+ else if (event.key === "ArrowRight") keys.current.right = false;
139
+ }, []);
140
+
141
+ const onPointerMove = useCallback((e) => {
142
+ const g = game.current, cv = canvasRef.current;
143
+ if (!g || !cv) return;
144
+ const rect0 = cv.getBoundingClientRect();
145
+ const scale = rect0.width / W;
146
+ g.player.x = Math.max(ROAD_X + 2, Math.min(ROAD_X + ROAD_W - CAR_W - 2, (e.clientX - rect0.left) / scale - CAR_W / 2));
147
+ }, []);
148
+ useGameControls(canvasRef, pausedRef.current, onKeyDown, onKeyUp);
149
+
150
+ return <section className="dgame-game" aria-label="极速赛车">
151
+ <div className="dgame-game-head"><div><h3>极速赛车</h3><p>左右移动避开迎面来车、收集金币,越开越快;撞车即结束。距离与金币实时显示在游戏画面左上/右上。</p></div><div className="dgame-score"><span>操作</span><span>←/→ 或鼠标</span></div></div>
152
+ <div className="dgame-racer">
153
+ <canvas ref={canvasRef} width={W} height={H} tabIndex={0} onKeyDown={onKeyDown} onKeyUp={onKeyUp} onPointerMove={onPointerMove} onClick={() => focusStage(canvasRef)} className="dgame-racer-canvas" aria-label="极速赛车游戏区域,左右方向键或鼠标控制方向" />
154
+ {status === "over" ? <div className="dgame-over"><strong>撞车了 · 行驶 {(game.current && Math.floor(game.current.dist / 60)) || 0} 米</strong><button type="button" onClick={launch}>重新出发</button></div> : null}
155
+ </div>
156
+ <div className="dgame-controls"><button type="button" aria-label="向左" onClick={() => { keys.current.left = false; if (game.current) game.current.player.x = Math.max(ROAD_X + 2, game.current.player.x - 14); }}>← 左移</button><span>←/→ 或鼠标移动 · R 重开</span><button type="button" aria-label="向右" onClick={() => { keys.current.right = false; if (game.current) game.current.player.x = Math.min(ROAD_X + ROAD_W - CAR_W - 2, game.current.player.x + 14); }}>右移 {'->'}</button></div>
157
+ </section>;
158
+ }
159
+
160
+ export function RacerPreview() {
161
+ return <span className="dgcov-prev dgcov-prev-racer" aria-hidden="true">
162
+ <i style={{ left: "20%", top: "8%" }} /><i style={{ left: "55%", top: "22%" }} /><i style={{ left: "20%", top: "42%" }} /><i style={{ left: "64%", top: "58%" }} /><i style={{ left: "38%", top: "74%" }} />
163
+ <b />
164
+ </span>;
165
+ }
166
+
167
+ export const racerGame = { Game: RacerGame, Preview: RacerPreview, css: `
168
+ .dgame-racer{position:relative;align-self:center;width:min(100%,300px);margin:0 auto}.dgame-racer-canvas{display:block;width:100%;aspect-ratio:300/420;border:1px solid color-mix(in srgb,rgb(34 211 238) 32%,var(--dsw-alias-border-l1));border-radius:10px;outline:none;background:#0b1024;box-shadow:0 4px 18px rgb(0 0 0/.4)}.dgame-racer-canvas:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,rgb(34 211 238) 35%,transparent)}.dgame-racer .dgame-over{position:absolute;inset:0;border-radius:10px}.dgame-racer-stat{color:var(--dsw-alias-label-tertiary)}
169
+ ` };
@@ -1,118 +1,118 @@
1
- /* 趣味游戏 · 游戏公共工具:键盘焦点与全局按键监听 */
2
- import { useEffect, useRef } from "react";
3
-
4
- function isTypingTarget(event) {
5
- const tag = (event.target && event.target.tagName) || "";
6
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
7
- if (event.target && event.target.isContentEditable) return true;
8
- return false;
9
- }
10
-
11
- /**
12
- * 让游戏舞台自动聚焦并在点击时重新夺回焦点;
13
- * 同时注册 window 级 keydown,确保方向键/WASD 等即使舞台因弹窗/按钮暂时失焦也能响应。
14
- * paused:来自 GameWindow 的暂停/最小化/选择层状态;暂停时不监听按键、不抢焦点。
15
- */
16
- export function useGameControls(stageRef, paused, onKeyDown, onKeyUp) {
17
- const pausedRef = useRef(!!paused);
18
- pausedRef.current = !!paused;
19
-
20
- // 自动聚焦舞台(延时确保 DOM 已渲染且窗口已显示)
21
- useEffect(() => {
22
- if (pausedRef.current) return;
23
- const el = stageRef && stageRef.current;
24
- if (!el) return;
25
- const t = setTimeout(() => { try { el.focus({ preventScroll: true }); } catch { /* ignore */ } }, 60);
26
- return () => clearTimeout(t);
27
- }, [paused]);
28
-
29
- // 全局按键监听:只在窗口打开、未暂停、且不在输入框中时响应。
30
- // 事件目标落在游戏舞台(或其子元素,如棋盘格按钮)内时跳过——
31
- // 舞台自身的 onKeyDown 会通过冒泡处理该键,避免双重触发。
32
- useEffect(() => {
33
- const inStage = (event) => {
34
- const el = stageRef && stageRef.current;
35
- return !!el && (el === event.target || el.contains(event.target));
36
- };
37
- const handleDown = (event) => {
38
- if (pausedRef.current) return;
39
- if (isTypingTarget(event)) return;
40
- if (inStage(event)) return;
41
- if (typeof onKeyDown === "function") onKeyDown(event);
42
- };
43
- const handleUp = (event) => {
44
- if (pausedRef.current) return;
45
- if (isTypingTarget(event)) return;
46
- if (inStage(event)) return;
47
- if (typeof onKeyUp === "function") onKeyUp(event);
48
- };
49
- window.addEventListener("keydown", handleDown);
50
- window.addEventListener("keyup", handleUp);
51
- return () => {
52
- window.removeEventListener("keydown", handleDown);
53
- window.removeEventListener("keyup", handleUp);
54
- };
55
- }, [onKeyDown, onKeyUp]);
56
- }
57
-
58
- /** 点击舞台时重新聚焦(配合 onClick) */
59
- export function focusStage(stageRef) {
60
- if (stageRef && stageRef.current) {
61
- try { stageRef.current.focus({ preventScroll: true }); } catch { /* ignore */ }
62
- }
63
- }
64
-
65
- /* ---------- 游戏音效:WebAudio 现场合成,零素材文件;首次交互后惰性初始化 ---------- */
66
- let _ctx = null;
67
- function audioCtx() {
68
- if (typeof window === "undefined") return null;
69
- const AC = window.AudioContext || window.webkitAudioContext;
70
- if (!AC) return null;
71
- try {
72
- if (!_ctx) _ctx = new AC();
73
- if (_ctx.state === "suspended") _ctx.resume().catch(() => {});
74
- return _ctx;
75
- } catch { return null; }
76
- }
77
- // name → 音色参数。t 波形 · g 音量 · d 单音时长 · f2 滑向频率 · seq 连奏序列 [频率, 延迟秒]
78
- const SFX_DEFS = {
79
- move: { f: 240, t: "square", d: 0.035, g: 0.07 },
80
- rotate: { f: 340, t: "square", d: 0.05, g: 0.08 },
81
- lock: { f: 150, t: "triangle", d: 0.08, g: 0.15 },
82
- step: { f: 190, t: "triangle", d: 0.03, g: 0.06 },
83
- push: { f: 115, t: "triangle", d: 0.07, g: 0.14 },
84
- stone: { f: 520, t: "triangle", d: 0.05, g: 0.13, f2: 430 },
85
- capture: { f: 175, t: "square", d: 0.09, g: 0.15, f2: 105 },
86
- check: { t: "square", g: 0.12, d: 0.24, seq: [[660, 0], [660, 0.14]] },
87
- eat: { f: 620, t: "square", d: 0.06, g: 0.11, f2: 920 },
88
- bounce: { f: 420, t: "square", d: 0.03, g: 0.07 },
89
- brick: { f: 740, t: "square", d: 0.05, g: 0.1, f2: 500 },
90
- clear: { t: "sine", g: 0.15, d: 0.28, seq: [[523, 0], [659, 0.09], [784, 0.18], [1047, 0.27]] },
91
- win: { t: "sine", g: 0.14, d: 0.34, seq: [[523, 0], [659, 0.11], [784, 0.22], [1047, 0.33], [784, 0.47], [1047, 0.56]] },
92
- over: { t: "sawtooth", g: 0.11, d: 0.38, seq: [[392, 0], [311, 0.13], [233, 0.27], [185, 0.44]] },
93
- };
94
- export function playSfx(name) {
95
- const def = SFX_DEFS[name];
96
- const ac = def && audioCtx();
97
- if (!ac) return;
98
- const t0 = ac.currentTime;
99
- const events = def.seq || [[def.f, 0]];
100
- for (const [freq, dt] of events) {
101
- let osc, gain;
102
- try {
103
- osc = ac.createOscillator();
104
- gain = ac.createGain();
105
- osc.type = def.t || "square";
106
- osc.frequency.setValueAtTime(freq, t0 + dt);
107
- if (def.f2) osc.frequency.exponentialRampToValueAtTime(Math.max(30, def.f2), t0 + dt + def.d);
108
- gain.gain.setValueAtTime(0.0001, t0 + dt);
109
- gain.gain.exponentialRampToValueAtTime(def.g, t0 + dt + 0.008);
110
- gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dt + def.d);
111
- osc.connect(gain).connect(ac.destination);
112
- osc.start(t0 + dt);
113
- osc.stop(t0 + dt + def.d + 0.03);
114
- } catch {
115
- if (osc) try { osc.stop(); } catch { /* ignore */ }
116
- }
117
- }
118
- }
1
+ /* 趣味游戏 · 游戏公共工具:键盘焦点与全局按键监听 */
2
+ import { useEffect, useRef } from "react";
3
+
4
+ function isTypingTarget(event) {
5
+ const tag = (event.target && event.target.tagName) || "";
6
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
7
+ if (event.target && event.target.isContentEditable) return true;
8
+ return false;
9
+ }
10
+
11
+ /**
12
+ * 让游戏舞台自动聚焦并在点击时重新夺回焦点;
13
+ * 同时注册 window 级 keydown,确保方向键/WASD 等即使舞台因弹窗/按钮暂时失焦也能响应。
14
+ * paused:来自 GameWindow 的暂停/最小化/选择层状态;暂停时不监听按键、不抢焦点。
15
+ */
16
+ export function useGameControls(stageRef, paused, onKeyDown, onKeyUp) {
17
+ const pausedRef = useRef(!!paused);
18
+ pausedRef.current = !!paused;
19
+
20
+ // 自动聚焦舞台(延时确保 DOM 已渲染且窗口已显示)
21
+ useEffect(() => {
22
+ if (pausedRef.current) return;
23
+ const el = stageRef && stageRef.current;
24
+ if (!el) return;
25
+ const t = setTimeout(() => { try { el.focus({ preventScroll: true }); } catch { /* ignore */ } }, 60);
26
+ return () => clearTimeout(t);
27
+ }, [paused]);
28
+
29
+ // 全局按键监听:只在窗口打开、未暂停、且不在输入框中时响应。
30
+ // 事件目标落在游戏舞台(或其子元素,如棋盘格按钮)内时跳过——
31
+ // 舞台自身的 onKeyDown 会通过冒泡处理该键,避免双重触发。
32
+ useEffect(() => {
33
+ const inStage = (event) => {
34
+ const el = stageRef && stageRef.current;
35
+ return !!el && (el === event.target || el.contains(event.target));
36
+ };
37
+ const handleDown = (event) => {
38
+ if (pausedRef.current) return;
39
+ if (isTypingTarget(event)) return;
40
+ if (inStage(event)) return;
41
+ if (typeof onKeyDown === "function") onKeyDown(event);
42
+ };
43
+ const handleUp = (event) => {
44
+ if (pausedRef.current) return;
45
+ if (isTypingTarget(event)) return;
46
+ if (inStage(event)) return;
47
+ if (typeof onKeyUp === "function") onKeyUp(event);
48
+ };
49
+ window.addEventListener("keydown", handleDown);
50
+ window.addEventListener("keyup", handleUp);
51
+ return () => {
52
+ window.removeEventListener("keydown", handleDown);
53
+ window.removeEventListener("keyup", handleUp);
54
+ };
55
+ }, [onKeyDown, onKeyUp]);
56
+ }
57
+
58
+ /** 点击舞台时重新聚焦(配合 onClick) */
59
+ export function focusStage(stageRef) {
60
+ if (stageRef && stageRef.current) {
61
+ try { stageRef.current.focus({ preventScroll: true }); } catch { /* ignore */ }
62
+ }
63
+ }
64
+
65
+ /* ---------- 游戏音效:WebAudio 现场合成,零素材文件;首次交互后惰性初始化 ---------- */
66
+ let _ctx = null;
67
+ function audioCtx() {
68
+ if (typeof window === "undefined") return null;
69
+ const AC = window.AudioContext || window.webkitAudioContext;
70
+ if (!AC) return null;
71
+ try {
72
+ if (!_ctx) _ctx = new AC();
73
+ if (_ctx.state === "suspended") _ctx.resume().catch(() => {});
74
+ return _ctx;
75
+ } catch { return null; }
76
+ }
77
+ // name → 音色参数。t 波形 · g 音量 · d 单音时长 · f2 滑向频率 · seq 连奏序列 [频率, 延迟秒]
78
+ const SFX_DEFS = {
79
+ move: { f: 240, t: "square", d: 0.035, g: 0.07 },
80
+ rotate: { f: 340, t: "square", d: 0.05, g: 0.08 },
81
+ lock: { f: 150, t: "triangle", d: 0.08, g: 0.15 },
82
+ step: { f: 190, t: "triangle", d: 0.03, g: 0.06 },
83
+ push: { f: 115, t: "triangle", d: 0.07, g: 0.14 },
84
+ stone: { f: 520, t: "triangle", d: 0.05, g: 0.13, f2: 430 },
85
+ capture: { f: 175, t: "square", d: 0.09, g: 0.15, f2: 105 },
86
+ check: { t: "square", g: 0.12, d: 0.24, seq: [[660, 0], [660, 0.14]] },
87
+ eat: { f: 620, t: "square", d: 0.06, g: 0.11, f2: 920 },
88
+ bounce: { f: 420, t: "square", d: 0.03, g: 0.07 },
89
+ brick: { f: 740, t: "square", d: 0.05, g: 0.1, f2: 500 },
90
+ clear: { t: "sine", g: 0.15, d: 0.28, seq: [[523, 0], [659, 0.09], [784, 0.18], [1047, 0.27]] },
91
+ win: { t: "sine", g: 0.14, d: 0.34, seq: [[523, 0], [659, 0.11], [784, 0.22], [1047, 0.33], [784, 0.47], [1047, 0.56]] },
92
+ over: { t: "sawtooth", g: 0.11, d: 0.38, seq: [[392, 0], [311, 0.13], [233, 0.27], [185, 0.44]] },
93
+ };
94
+ export function playSfx(name) {
95
+ const def = SFX_DEFS[name];
96
+ const ac = def && audioCtx();
97
+ if (!ac) return;
98
+ const t0 = ac.currentTime;
99
+ const events = def.seq || [[def.f, 0]];
100
+ for (const [freq, dt] of events) {
101
+ let osc, gain;
102
+ try {
103
+ osc = ac.createOscillator();
104
+ gain = ac.createGain();
105
+ osc.type = def.t || "square";
106
+ osc.frequency.setValueAtTime(freq, t0 + dt);
107
+ if (def.f2) osc.frequency.exponentialRampToValueAtTime(Math.max(30, def.f2), t0 + dt + def.d);
108
+ gain.gain.setValueAtTime(0.0001, t0 + dt);
109
+ gain.gain.exponentialRampToValueAtTime(def.g, t0 + dt + 0.008);
110
+ gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dt + def.d);
111
+ osc.connect(gain).connect(ac.destination);
112
+ osc.start(t0 + dt);
113
+ osc.stop(t0 + dt + def.d + 0.03);
114
+ } catch {
115
+ if (osc) try { osc.stop(); } catch { /* ignore */ }
116
+ }
117
+ }
118
+ }