dsh-dock 0.9.1 → 0.9.2
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/client.js +124 -44
- package/features/games/games/breakout.jsx +3 -3
- package/features/games/games/racer.jsx +1 -1
- package/features/games/games/tetris.jsx +12 -6
- package/features/games/games/xiangqi.jsx +98 -26
- package/features/games/view.jsx +1 -1
- package/features/mobile-relay/host.js +53 -0
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -6151,11 +6151,16 @@ function rotations(shape) {
|
|
|
6151
6151
|
}
|
|
6152
6152
|
var PIECES = SHAPES.map((s) => ({ color: s.color, rots: rotations(s) }));
|
|
6153
6153
|
var TETRIS_GRID = (() => {
|
|
6154
|
-
const
|
|
6154
|
+
const parts = [];
|
|
6155
6155
|
let k = 0;
|
|
6156
|
-
for (let
|
|
6157
|
-
|
|
6158
|
-
|
|
6156
|
+
for (let j = 0; j < 20; j += 1) {
|
|
6157
|
+
for (let i = 0; i < 10; i += 1) {
|
|
6158
|
+
parts.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { className: "dgame-tetris-pit", x: i * 100 + 7, y: j * 100 + 7, width: 86, height: 86, rx: 10 }, k++));
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
for (let i = 1; i < 10; i += 1) parts.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: i * 100, y1: 0, x2: i * 100, y2: 2e3 }, k++));
|
|
6162
|
+
for (let j = 1; j < 20; j += 1) parts.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: 0, y1: j * 100, x2: 1e3, y2: j * 100 }, k++));
|
|
6163
|
+
return parts;
|
|
6159
6164
|
})();
|
|
6160
6165
|
function emptyBoard() {
|
|
6161
6166
|
return Array.from({ length: ROWS }, () => Array(COLS).fill(null));
|
|
@@ -6461,7 +6466,7 @@ function TetrisPreview() {
|
|
|
6461
6466
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dgcov-prev dgcov-prev-tetris", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dgp-tet-grid", children: ROWS3.flatMap((row, r) => row.split("").map((ch, c) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("i", { className: COLORS2[ch] ? "c" + COLORS2[ch] + (isFall(r) ? " fall" : "") : "" }, r + "-" + c))) }) });
|
|
6462
6467
|
}
|
|
6463
6468
|
var tetrisGame = { Game: TetrisGame, Preview: TetrisPreview, css: `
|
|
6464
|
-
.dgame-tetris{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-start;align-self:center;width:fit-content;max-width:100%;margin-inline:auto;justify-content:center}.dgame-tetris-board{position:relative;flex:none;display:grid;grid-template-columns:repeat(10,1fr);grid-template-rows:repeat(20,1fr);width:min(100%,188px);aspect-ratio:10/20;border:1px solid color-mix(in srgb,rgb(34 211 238) 35%,var(--dsw-alias-border-l1));border-radius:8px;overflow:hidden;outline:none;background:linear-gradient(180deg,#0c1024,#131a3d);padding:0;box-shadow:inset 0 0 30px rgb(0 0 0/.45)}.dgame-tetris-board:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,rgb(34 211 238) 40%,transparent)}.dgame-tetris-cell{display:block;background:transparent;border-radius:2px;margin:1px}.dgame-tetris-nextbox{display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(2,1fr);width:60px;height:32px}.dgame-tetris-side{flex:1 1 96px;display:flex;flex-direction:column;gap:9px;min-width:0;max-width:220px;color:var(--dsw-alias-label-tertiary);font-size:11px}.dgame-tetris-side b{color:var(--dsw-alias-label-secondary);font-size:12px}.dgame-tetris-side span{font-size:11px}.dgame-tetris-nextbox .dgame-tetris-cell{margin:1px}.dgame-tetris-keys span{display:block;line-height:1.6;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dgame-tetris-next{display:flex;flex-direction:column;gap:5px;align-items:center}.dgame-tetris-next span{color:var(--dsw-alias-label-secondary)}.dgame-tetris-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.dgame-tetris-lines line{stroke
|
|
6469
|
+
.dgame-tetris{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-start;align-self:center;width:fit-content;max-width:100%;margin-inline:auto;justify-content:center}.dgame-tetris-board{position:relative;flex:none;display:grid;grid-template-columns:repeat(10,1fr);grid-template-rows:repeat(20,1fr);width:min(100%,188px);aspect-ratio:10/20;border:1px solid color-mix(in srgb,rgb(34 211 238) 35%,var(--dsw-alias-border-l1));border-radius:8px;overflow:hidden;outline:none;background:linear-gradient(180deg,#0c1024,#131a3d);padding:0;box-shadow:inset 0 0 30px rgb(0 0 0/.45)}.dgame-tetris-board:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,rgb(34 211 238) 40%,transparent)}.dgame-tetris-cell{position:relative;z-index:1;display:block;background:transparent;border-radius:2px;margin:1px}.dgame-tetris-nextbox{display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(2,1fr);width:60px;height:32px}.dgame-tetris-side{flex:1 1 96px;display:flex;flex-direction:column;gap:9px;min-width:0;max-width:220px;color:var(--dsw-alias-label-tertiary);font-size:11px}.dgame-tetris-side b{color:var(--dsw-alias-label-secondary);font-size:12px}.dgame-tetris-side span{font-size:11px}.dgame-tetris-nextbox .dgame-tetris-cell{margin:1px}.dgame-tetris-keys span{display:block;line-height:1.6;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dgame-tetris-next{display:flex;flex-direction:column;gap:5px;align-items:center}.dgame-tetris-next span{color:var(--dsw-alias-label-secondary)}.dgame-tetris-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.dgame-tetris-lines line{stroke:rgb(96 165 250/.28);stroke-width:3}.dgame-tetris-lines .dgame-tetris-pit{fill:rgb(0 0 0/.42);stroke:rgb(255 255 255/.07);stroke-width:2}@media (max-width:680px){.dgame-tetris{width:100%;gap:8px}.dgame-tetris-board{width:min(100%,170px)}}
|
|
6465
6470
|
` };
|
|
6466
6471
|
|
|
6467
6472
|
// features/games/games/sokoban.jsx
|
|
@@ -7314,6 +7319,18 @@ var XIANGQI_LINES = (() => {
|
|
|
7314
7319
|
}
|
|
7315
7320
|
return parts;
|
|
7316
7321
|
})();
|
|
7322
|
+
var CN_NUMS = ["\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u4E03", "\u516B", "\u4E5D"];
|
|
7323
|
+
function moveNotation(board, fr, fc, tr, tc) {
|
|
7324
|
+
const p = board[fr][fc];
|
|
7325
|
+
if (!p) return "";
|
|
7326
|
+
const red = p.c === "r";
|
|
7327
|
+
const num = (n) => red ? CN_NUMS[n - 1] : String(n);
|
|
7328
|
+
const file = (c) => red ? 9 - c : c + 1;
|
|
7329
|
+
const forward = red ? tr < fr : tr > fr;
|
|
7330
|
+
const verb = fr === tr ? "\u5E73" : forward ? "\u8FDB" : "\u9000";
|
|
7331
|
+
const to = fr === tr || p.t === "h" || p.t === "e" || p.t === "a" ? file(tc) : Math.abs(tr - fr);
|
|
7332
|
+
return pieceChar(p) + num(file(fc)) + verb + num(to);
|
|
7333
|
+
}
|
|
7317
7334
|
function XiangqiGame(props) {
|
|
7318
7335
|
const [board, setBoard] = (0, import_react13.useState)(initialBoard);
|
|
7319
7336
|
const [turn, setTurn] = (0, import_react13.useState)("r");
|
|
@@ -7322,6 +7339,9 @@ function XiangqiGame(props) {
|
|
|
7322
7339
|
const [over, setOver] = (0, import_react13.useState)(null);
|
|
7323
7340
|
const [check, setCheck] = (0, import_react13.useState)(false);
|
|
7324
7341
|
const [last, setLast] = (0, import_react13.useState)(null);
|
|
7342
|
+
const [lastMove, setLastMove] = (0, import_react13.useState)(null);
|
|
7343
|
+
const [moveLog, setMoveLog] = (0, import_react13.useState)([]);
|
|
7344
|
+
const [moving, setMoving] = (0, import_react13.useState)(null);
|
|
7325
7345
|
const [thinking, setThinking] = (0, import_react13.useState)(false);
|
|
7326
7346
|
const [history, setHistory] = (0, import_react13.useState)([]);
|
|
7327
7347
|
const [repKeys, setRepKeys] = (0, import_react13.useState)(() => [posKey(initialBoard())]);
|
|
@@ -7337,7 +7357,43 @@ function XiangqiGame(props) {
|
|
|
7337
7357
|
thinkingRef.current = thinking;
|
|
7338
7358
|
const historyRef = (0, import_react13.useRef)(history);
|
|
7339
7359
|
historyRef.current = history;
|
|
7360
|
+
const movingRef = (0, import_react13.useRef)(false);
|
|
7361
|
+
const pendingRef = (0, import_react13.useRef)(null);
|
|
7362
|
+
const animTimerRef = (0, import_react13.useRef)(null);
|
|
7363
|
+
const animateMove = (0, import_react13.useCallback)((b, fr, fc, tr, tc, after) => {
|
|
7364
|
+
const piece = boardRef.current[fr][fc];
|
|
7365
|
+
const capture = !!boardRef.current[tr][tc];
|
|
7366
|
+
pendingRef.current = { board: b, after };
|
|
7367
|
+
movingRef.current = true;
|
|
7368
|
+
setSel(null);
|
|
7369
|
+
setTargets([]);
|
|
7370
|
+
setMoving({ fr, fc, tr, tc, piece, capture });
|
|
7371
|
+
playSfx(capture ? "capture" : "step");
|
|
7372
|
+
if (animTimerRef.current) clearTimeout(animTimerRef.current);
|
|
7373
|
+
animTimerRef.current = setTimeout(() => {
|
|
7374
|
+
animTimerRef.current = null;
|
|
7375
|
+
const pend = pendingRef.current;
|
|
7376
|
+
pendingRef.current = null;
|
|
7377
|
+
movingRef.current = false;
|
|
7378
|
+
setMoving(null);
|
|
7379
|
+
if (pend) {
|
|
7380
|
+
const text = moveNotation(boardRef.current, fr, fc, tr, tc);
|
|
7381
|
+
boardRef.current = pend.board;
|
|
7382
|
+
setBoard(pend.board);
|
|
7383
|
+
setLastMove({ fr, fc, tr, tc, text });
|
|
7384
|
+
setMoveLog((log) => log.concat({ side: piece.c, text }));
|
|
7385
|
+
setLast([tr, tc]);
|
|
7386
|
+
if (pend.after) pend.after();
|
|
7387
|
+
}
|
|
7388
|
+
}, 260);
|
|
7389
|
+
}, []);
|
|
7340
7390
|
const restart = (0, import_react13.useCallback)(() => {
|
|
7391
|
+
if (animTimerRef.current) {
|
|
7392
|
+
clearTimeout(animTimerRef.current);
|
|
7393
|
+
animTimerRef.current = null;
|
|
7394
|
+
}
|
|
7395
|
+
pendingRef.current = null;
|
|
7396
|
+
movingRef.current = false;
|
|
7341
7397
|
const b = initialBoard();
|
|
7342
7398
|
boardRef.current = b;
|
|
7343
7399
|
setBoard(b);
|
|
@@ -7349,10 +7405,13 @@ function XiangqiGame(props) {
|
|
|
7349
7405
|
setLast(null);
|
|
7350
7406
|
setThinking(false);
|
|
7351
7407
|
setHistory([]);
|
|
7408
|
+
setLastMove(null);
|
|
7409
|
+
setMoveLog([]);
|
|
7410
|
+
setMoving(null);
|
|
7352
7411
|
setRepKeys([posKey(b)]);
|
|
7353
7412
|
}, []);
|
|
7354
7413
|
const undo2 = (0, import_react13.useCallback)(() => {
|
|
7355
|
-
if (thinkingRef.current || overRef.current || turnRef.current !== "r") return;
|
|
7414
|
+
if (thinkingRef.current || overRef.current || turnRef.current !== "r" || movingRef.current) return;
|
|
7356
7415
|
const h = historyRef.current;
|
|
7357
7416
|
if (h.length < 2) return;
|
|
7358
7417
|
const hist = h.slice(0, -2);
|
|
@@ -7363,31 +7422,28 @@ function XiangqiGame(props) {
|
|
|
7363
7422
|
setSel(null);
|
|
7364
7423
|
setTargets([]);
|
|
7365
7424
|
setLast(null);
|
|
7425
|
+
setLastMove(null);
|
|
7426
|
+
setMoveLog((prevLog) => prevLog.slice(0, -2));
|
|
7366
7427
|
setOver(null);
|
|
7367
7428
|
setCheck(inCheck(b, "r"));
|
|
7368
7429
|
setRepKeys(hist.map(posKey).concat(posKey(b)));
|
|
7369
7430
|
}, []);
|
|
7370
7431
|
const moveRed = (0, import_react13.useCallback)((tr, tc) => {
|
|
7371
|
-
if (turnRef.current !== "r" || overRef.current || !sel) return;
|
|
7432
|
+
if (turnRef.current !== "r" || overRef.current || !sel || movingRef.current) return;
|
|
7372
7433
|
const fr = sel[0], fc = sel[1];
|
|
7373
|
-
if (boardRef.current[tr][tc]) playSfx("capture");
|
|
7374
|
-
else playSfx("step");
|
|
7375
7434
|
const nb = applyMove(boardRef.current, fr, fc, tr, tc);
|
|
7376
7435
|
setHistory((h) => [...h, boardRef.current]);
|
|
7377
|
-
boardRef.current = nb;
|
|
7378
7436
|
pushKey(nb);
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
} else setCheck(inCheck(nb, "b"));
|
|
7388
|
-
}, [sel, pushKey]);
|
|
7437
|
+
animateMove(nb, fr, fc, tr, tc, () => {
|
|
7438
|
+
setTurn("b");
|
|
7439
|
+
if (!hasLegalMoves(nb, "b")) {
|
|
7440
|
+
setOver("r");
|
|
7441
|
+
setCheck(false);
|
|
7442
|
+
} else setCheck(inCheck(nb, "b"));
|
|
7443
|
+
});
|
|
7444
|
+
}, [sel, pushKey, animateMove]);
|
|
7389
7445
|
const select = (0, import_react13.useCallback)((r, c) => {
|
|
7390
|
-
if (overRef.current || turnRef.current !== "r" || thinkingRef.current) return;
|
|
7446
|
+
if (overRef.current || turnRef.current !== "r" || thinkingRef.current || movingRef.current) return;
|
|
7391
7447
|
const p = boardRef.current[r][c];
|
|
7392
7448
|
if (p && p.c === "r") {
|
|
7393
7449
|
setSel([r, c]);
|
|
@@ -7403,32 +7459,29 @@ function XiangqiGame(props) {
|
|
|
7403
7459
|
setTargets([]);
|
|
7404
7460
|
}, [sel, targets, moveRed]);
|
|
7405
7461
|
(0, import_react13.useEffect)(() => {
|
|
7406
|
-
if (turn !== "b" || over || props.paused) return;
|
|
7462
|
+
if (turn !== "b" || over || props.paused || movingRef.current) return;
|
|
7407
7463
|
const t = setTimeout(() => {
|
|
7408
7464
|
const seenCounts = /* @__PURE__ */ new Map();
|
|
7409
7465
|
for (const k of repKeys) seenCounts.set(k, (seenCounts.get(k) || 0) + 1);
|
|
7410
7466
|
const mv = bestMove(boardRef.current, "b", 3, seenCounts);
|
|
7411
7467
|
if (mv) {
|
|
7412
7468
|
const [fr, fc, tr, tc] = mv;
|
|
7413
|
-
if (boardRef.current[tr][tc]) playSfx("capture");
|
|
7414
|
-
else playSfx("step");
|
|
7415
7469
|
const nb = applyMove(boardRef.current, fr, fc, tr, tc);
|
|
7416
7470
|
setHistory((h) => [...h, boardRef.current]);
|
|
7417
|
-
boardRef.current = nb;
|
|
7418
7471
|
pushKey(nb);
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
setCheck(
|
|
7425
|
-
}
|
|
7472
|
+
animateMove(nb, fr, fc, tr, tc, () => {
|
|
7473
|
+
setTurn("r");
|
|
7474
|
+
if (!hasLegalMoves(nb, "r")) {
|
|
7475
|
+
setOver("b");
|
|
7476
|
+
setCheck(false);
|
|
7477
|
+
} else setCheck(inCheck(nb, "r"));
|
|
7478
|
+
});
|
|
7426
7479
|
} else setOver("r");
|
|
7427
7480
|
setThinking(false);
|
|
7428
|
-
},
|
|
7481
|
+
}, 420);
|
|
7429
7482
|
setThinking(true);
|
|
7430
7483
|
return () => clearTimeout(t);
|
|
7431
|
-
}, [turn, over, props.paused, repKeys, pushKey]);
|
|
7484
|
+
}, [turn, over, props.paused, repKeys, pushKey, animateMove]);
|
|
7432
7485
|
const onKeyDown = (0, import_react13.useCallback)((event) => {
|
|
7433
7486
|
if (event.key.toLowerCase() === "r") {
|
|
7434
7487
|
event.preventDefault();
|
|
@@ -7439,7 +7492,7 @@ function XiangqiGame(props) {
|
|
|
7439
7492
|
}
|
|
7440
7493
|
}, [restart, undo2]);
|
|
7441
7494
|
useGameControls(stageRef, props.paused, onKeyDown);
|
|
7442
|
-
const status = over === "r" ? "\u7EA2\u65B9\u80DC \xB7 \u606D\u559C" : over === "b" ? "\u9ED1\u65B9\u80DC \xB7 AI \u8D62" : thinking ? "AI \u601D\u8003\u4E2D\u2026" : check ? "\u5C06\u519B\uFF01" : turn === "r" ? "\u8F6E\u5230\u4F60\uFF08\u7EA2\u65B9\uFF09" : "\u8F6E\u5230 AI\uFF08\u9ED1\u65B9\uFF09";
|
|
7495
|
+
const status = over === "r" ? "\u7EA2\u65B9\u80DC \xB7 \u606D\u559C" : over === "b" ? "\u9ED1\u65B9\u80DC \xB7 AI \u8D62" : moving ? moving.piece.c === "r" ? "\u7EA2\u65B9\u8D70\u5B50\u2026" : "AI \u8D70\u5B50\u2026" : thinking ? "AI \u601D\u8003\u4E2D\u2026" : check ? "\u5C06\u519B\uFF01" : turn === "r" ? "\u8F6E\u5230\u4F60\uFF08\u7EA2\u65B9\uFF09" : "\u8F6E\u5230 AI\uFF08\u9ED1\u65B9\uFF09";
|
|
7443
7496
|
const prevCheck = (0, import_react13.useRef)(false);
|
|
7444
7497
|
(0, import_react13.useEffect)(() => {
|
|
7445
7498
|
if (over === "r") playSfx("win");
|
|
@@ -7447,6 +7500,9 @@ function XiangqiGame(props) {
|
|
|
7447
7500
|
else if (!thinking && check && !prevCheck.current) playSfx("check");
|
|
7448
7501
|
prevCheck.current = !thinking && check;
|
|
7449
7502
|
}, [check, thinking, over]);
|
|
7503
|
+
(0, import_react13.useEffect)(() => () => {
|
|
7504
|
+
if (animTimerRef.current) clearTimeout(animTimerRef.current);
|
|
7505
|
+
}, []);
|
|
7450
7506
|
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { className: "dgame-game", "aria-label": "\u8C61\u68CB", children: [
|
|
7451
7507
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dgame-game-head", children: [
|
|
7452
7508
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
|
|
@@ -7467,26 +7523,47 @@ function XiangqiGame(props) {
|
|
|
7467
7523
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("text", { className: "dgame-xiangqi-rivertext", x: 670, y: 505, children: "\u6F22 \u754C" })
|
|
7468
7524
|
] }),
|
|
7469
7525
|
board.map((row, r) => row.map((p, c) => {
|
|
7526
|
+
const mv = moving;
|
|
7527
|
+
const isFlyFrom = mv && mv.fr === r && mv.fc === c;
|
|
7528
|
+
const isDying = mv && mv.capture && mv.tr === r && mv.tc === c;
|
|
7470
7529
|
const isSel = sel && sel[0] === r && sel[1] === c;
|
|
7471
7530
|
const isTargetMark = targets.some(([tr, tc]) => tr === r && tc === c);
|
|
7531
|
+
const isLastMoveFrom = lastMove && lastMove.fr === r && lastMove.fc === c;
|
|
7532
|
+
const isLand = !mv && lastMove && lastMove.tr === r && lastMove.tc === c;
|
|
7472
7533
|
const isLast = last && last[0] === r && last[1] === c;
|
|
7473
7534
|
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
7474
7535
|
"button",
|
|
7475
7536
|
{
|
|
7476
7537
|
type: "button",
|
|
7477
7538
|
role: "gridcell",
|
|
7478
|
-
className: "dgame-xiangqi-cell" + (isSel ? " sel" : "") + (isTargetMark ? " target" : "") + (isLast ? " last" : ""),
|
|
7539
|
+
className: "dgame-xiangqi-cell" + (isSel ? " sel" : "") + (isTargetMark ? " target" : "") + (isLastMoveFrom ? " from" : "") + (isLand ? " land" : "") + (isLast ? " last" : ""),
|
|
7479
7540
|
"aria-label": "\u7B2C" + (r + 1) + "\u884C\u7B2C" + (c + 1) + "\u5217" + (p ? pieceChar(p) : " \u7A7A"),
|
|
7480
7541
|
onClick: () => {
|
|
7481
7542
|
focusStage(stageRef);
|
|
7482
7543
|
select(r, c);
|
|
7483
7544
|
},
|
|
7484
|
-
children: p ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dgame-xiangqi-pc " + p.c, children: pieceChar(p) }) : isTargetMark ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dgame-xiangqi-dot", "aria-hidden": "true" }) : null
|
|
7545
|
+
children: p && !isFlyFrom ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dgame-xiangqi-pc " + p.c + (isDying ? " dying" : ""), children: pieceChar(p) }) : !p && isTargetMark ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dgame-xiangqi-dot", "aria-hidden": "true" }) : null
|
|
7485
7546
|
},
|
|
7486
7547
|
r + "-" + c
|
|
7487
7548
|
);
|
|
7488
|
-
}))
|
|
7549
|
+
})),
|
|
7550
|
+
moving ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
7551
|
+
"span",
|
|
7552
|
+
{
|
|
7553
|
+
className: "dgame-xiangqi-pc dgame-xiangqi-fly " + moving.piece.c,
|
|
7554
|
+
style: {
|
|
7555
|
+
"--fx0": (moving.fc + 0.5) / 9,
|
|
7556
|
+
"--fy0": (moving.fr + 0.5) / 10,
|
|
7557
|
+
"--fx1": (moving.tc + 0.5) / 9,
|
|
7558
|
+
"--fy1": (moving.tr + 0.5) / 10
|
|
7559
|
+
},
|
|
7560
|
+
"aria-hidden": "true",
|
|
7561
|
+
children: pieceChar(moving.piece)
|
|
7562
|
+
},
|
|
7563
|
+
moving.fr + "-" + moving.fc + "-" + moving.tr + "-" + moving.tc + "-" + moving.piece.c
|
|
7564
|
+
) : null
|
|
7489
7565
|
] }),
|
|
7566
|
+
moveLog.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dgame-xiangqi-log", role: "status", "aria-live": "polite", children: moveLog.slice(-4).map((m, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: m.side, children: m.text }, moveLog.length - 4 + i)) }) : null,
|
|
7490
7567
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dgame-controls", children: [
|
|
7491
7568
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: undo2, children: "\u64A4\u9500" }),
|
|
7492
7569
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
@@ -7524,7 +7601,7 @@ function XiangqiPreview() {
|
|
|
7524
7601
|
] }) });
|
|
7525
7602
|
}
|
|
7526
7603
|
var xiangqiGame = { Game: XiangqiGame, Preview: XiangqiPreview, css: `
|
|
7527
|
-
.dgame-xiangqi-board{position:relative;display:grid;grid-template-columns:repeat(9,1fr);grid-template-rows:repeat(10,1fr);align-self:center;width:min(100%,min(90vw,340px));aspect-ratio:9/10;padding:13px;border-radius:12px;border:1px solid color-mix(in srgb,#7c4f16 65%,transparent);box-shadow:0 12px 32px rgb(0 0 0/.5),inset 0 1px 0 rgba(255,236,200,.55),inset 0 -3px 9px rgba(90,50,10,.35),inset 3px 0 8px rgba(255,224,170,.28),inset -3px 0 8px rgba(120,70,20,.25);outline:none;background:repeating-linear-gradient(94deg,rgba(124,79,22,.05) 0 2px,transparent 2px 11px),repeating-linear-gradient(87deg,rgba(255,238,196,.05) 0 2px,transparent 2px 14px),radial-gradient(130% 105% at 26% 8%,#f0cd86,#ddb067 48%,#cb9a51 78%,#bb8844)}.dgame-xiangqi-lines{position:absolute;inset:13px;pointer-events:none}.dgame-xiangqi-lines line,.dgame-xiangqi-lines path{stroke:#66431a;stroke-width:3.4;fill:none;stroke-linecap:square}.dgame-xiangqi-lines .dgame-xiangqi-mark{stroke-width:2.6}.dgame-xiangqi-rivertext{fill:rgba(102,67,26,.7);font-family:KaiTi,"Kaiti SC","STKaiti","FangSong",serif;font-size:46px;letter-spacing:7px;text-anchor:middle;dominant-baseline:central}.dgame-xiangqi-board:focus-visible{box-shadow:0 0 0 3px rgba(0,0,0,.25)}.dgame-xiangqi-cell{position:relative;z-index:1;background:transparent;border:none;cursor:pointer;padding:0;margin:0;outline:none;display:grid;place-items:center;min-width:0;min-height:0}.dgame-xiangqi-cell.target .dgame-xiangqi-dot{width:12px;height:12px;border-radius:50%;background:rgba(34,211,238,.6);box-shadow:0 0 10px rgba(34,211,238,.65),0 0 0 2px rgba(255,255,255,.28)}.dgame-xiangqi-cell.target .dgame-xiangqi-pc{box-shadow:0 0 0 2px rgba(248,113,113,.85),0 0 10px rgba(248,113,113,.5),0 2px 4px rgb(0 0 0/.4),inset 0 0 0 1.5px rgba(96,62,18,.5),inset 0 1px 2px rgba(255,255,255,.95),inset 0 -2px 3px rgba(120,80,20,.4)}.dgame-xiangqi-cell.sel .dgame-xiangqi-pc::before{content:'';position:absolute;inset:-3px;border-radius:50%;border:2.5px solid rgba(251,191,36,.95);box-shadow:0 0 12px rgba(251,191,36,.6)}.dgame-xiangqi-cell.last .dgame-xiangqi-pc::after{content:'';position:absolute;right:5%;bottom:5%;width:5px;height:5px;border-radius:50%;background:#ef4444;box-shadow:0 0 4px rgba(239,68,68,.8)}.dgame-xiangqi-pc{position:relative;display:grid;place-items:center;z-index:1;width:min(80%,30px);aspect-ratio:1/1;border-radius:50%;box-sizing:border-box;font-family:KaiTi,"Kaiti SC","STKaiti","FangSong","SimSun",serif;font-size:clamp(12px,1.4vw,15px);font-weight:700;line-height:1;background:radial-gradient(circle at 34% 28%,#fff3d4,#eedaa6 46%,#d8b675 72%,#bd9650);box-shadow:0 2px 4px rgb(0 0 0/.45),inset 0 0 0 1.5px rgba(96,62,18,.5),inset 0 1px 2px rgba(255,255,255,.95),inset 0 -2px 3px rgba(120,80,20,.4)}.dgame-xiangqi-pc.r{color:#b91c1c;text-shadow:0 1px 0 rgba(255,255,255,.5)}.dgame-xiangqi-
|
|
7604
|
+
.dgame-xiangqi-board{position:relative;display:grid;grid-template-columns:repeat(9,1fr);grid-template-rows:repeat(10,1fr);align-self:center;width:min(100%,min(90vw,340px));aspect-ratio:9/10;padding:13px;border-radius:12px;border:1px solid color-mix(in srgb,#7c4f16 65%,transparent);box-shadow:0 12px 32px rgb(0 0 0/.5),inset 0 1px 0 rgba(255,236,200,.55),inset 0 -3px 9px rgba(90,50,10,.35),inset 3px 0 8px rgba(255,224,170,.28),inset -3px 0 8px rgba(120,70,20,.25);outline:none;background:repeating-linear-gradient(94deg,rgba(124,79,22,.05) 0 2px,transparent 2px 11px),repeating-linear-gradient(87deg,rgba(255,238,196,.05) 0 2px,transparent 2px 14px),radial-gradient(130% 105% at 26% 8%,#f0cd86,#ddb067 48%,#cb9a51 78%,#bb8844)}.dgame-xiangqi-lines{position:absolute;inset:13px;pointer-events:none}.dgame-xiangqi-lines line,.dgame-xiangqi-lines path,.dgame-xiangqi-lines rect{stroke:#66431a;stroke-width:3.4;fill:none;stroke-linecap:square}.dgame-xiangqi-lines rect{stroke-width:7}.dgame-xiangqi-lines .dgame-xiangqi-mark{stroke-width:2.6}.dgame-xiangqi-rivertext{fill:rgba(102,67,26,.7);font-family:KaiTi,"Kaiti SC","STKaiti","FangSong",serif;font-size:46px;letter-spacing:7px;text-anchor:middle;dominant-baseline:central}.dgame-xiangqi-board:focus-visible{box-shadow:0 0 0 3px rgba(0,0,0,.25)}.dgame-xiangqi-cell{position:relative;z-index:1;background:transparent;border:none;cursor:pointer;padding:0;margin:0;outline:none;display:grid;place-items:center;min-width:0;min-height:0}.dgame-xiangqi-cell.target .dgame-xiangqi-dot{width:12px;height:12px;border-radius:50%;background:rgba(34,211,238,.6);box-shadow:0 0 10px rgba(34,211,238,.65),0 0 0 2px rgba(255,255,255,.28)}.dgame-xiangqi-cell.target .dgame-xiangqi-pc{box-shadow:0 0 0 2px rgba(248,113,113,.85),0 0 10px rgba(248,113,113,.5),0 2px 4px rgb(0 0 0/.4),inset 0 0 0 1.5px rgba(96,62,18,.5),inset 0 1px 2px rgba(255,255,255,.95),inset 0 -2px 3px rgba(120,80,20,.4)}.dgame-xiangqi-cell.sel .dgame-xiangqi-pc::before{content:'';position:absolute;inset:-3px;border-radius:50%;border:2.5px solid rgba(251,191,36,.95);box-shadow:0 0 12px rgba(251,191,36,.6)}.dgame-xiangqi-cell.from::before{content:'';position:absolute;inset:50% auto auto 50%;width:34%;height:34%;transform:translate(-50%,-50%);border-radius:50%;border:2.5px dashed rgba(102,67,26,.75);pointer-events:none}.dgame-xiangqi-cell.land .dgame-xiangqi-pc{animation:dgame-xq-land .28s cubic-bezier(.2,.85,.3,1.25)}.dgame-xiangqi-cell.last .dgame-xiangqi-pc::after{content:'';position:absolute;right:5%;bottom:5%;width:5px;height:5px;border-radius:50%;background:#ef4444;box-shadow:0 0 4px rgba(239,68,68,.8)}.dgame-xiangqi-pc{position:relative;display:grid;place-items:center;z-index:1;width:min(80%,30px);aspect-ratio:1/1;border-radius:50%;box-sizing:border-box;font-family:KaiTi,"Kaiti SC","STKaiti","FangSong","SimSun",serif;font-size:clamp(12px,1.4vw,15px);font-weight:700;line-height:1;background:radial-gradient(circle at 34% 28%,#fff3d4,#eedaa6 46%,#d8b675 72%,#bd9650);box-shadow:0 2px 4px rgb(0 0 0/.45),inset 0 0 0 1.5px rgba(96,62,18,.5),inset 0 1px 2px rgba(255,255,255,.95),inset 0 -2px 3px rgba(120,80,20,.4)}.dgame-xiangqi-pc.dying{animation:dgame-xq-die .26s ease forwards}.dgame-xiangqi-fly{position:absolute;z-index:5;width:calc((100% - 26px)*.0889);aspect-ratio:1/1;border-radius:50%;display:grid;place-items:center;box-sizing:border-box;font-family:KaiTi,"Kaiti SC","STKaiti","FangSong","SimSun",serif;font-size:clamp(12px,1.4vw,15px);font-weight:700;line-height:1;pointer-events:none;background:radial-gradient(circle at 34% 28%,#fff3d4,#eedaa6 46%,#d8b675 72%,#bd9650);box-shadow:0 6px 14px rgb(0 0 0/.5),0 0 0 1.5px rgba(96,62,18,.35),inset 0 0 0 1.5px rgba(96,62,18,.5),inset 0 1px 2px rgba(255,255,255,.95),inset 0 -2px 3px rgba(120,80,20,.4);animation:dgame-xq-hop .26s cubic-bezier(.3,.7,.4,1) forwards}.dgame-xiangqi-fly.r{color:#b91c1c;text-shadow:0 1px 0 rgba(255,255,255,.5)}.dgame-xiangqi-fly.b{color:#1f2937;text-shadow:0 1px 0 rgba(255,255,255,.35)}@keyframes dgame-xq-hop{0%{left:calc(13px + var(--fx0)*(100% - 26px));top:calc(13px + var(--fy0)*(100% - 26px));transform:translate(-50%,-50%) scale(.9);opacity:.85}55%{transform:translate(-50%,-64%) scale(1.16);opacity:1}100%{left:calc(13px + var(--fx1)*(100% - 26px));top:calc(13px + var(--fy1)*(100% - 26px));transform:translate(-50%,-50%) scale(1)}}@keyframes dgame-xq-land{0%{transform:scale(1.18)}100%{transform:scale(1)}}@keyframes dgame-xq-die{to{opacity:0;transform:scale(.55)}}.dgame-xiangqi-log{display:flex;flex-wrap:wrap;justify-content:center;gap:5px 12px;align-self:center;min-height:22px;color:var(--dsw-alias-label-secondary);font-family:KaiTi,"Kaiti SC","STKaiti","FangSong",serif;font-size:13px}.dgame-xiangqi-log span{opacity:.65}.dgame-xiangqi-log span:last-child{opacity:1;font-weight:700}.dgame-xiangqi-log .r{color:#c03434}.dgame-xiangqi-log .b{color:var(--dsw-alias-label-primary)}
|
|
7528
7605
|
` };
|
|
7529
7606
|
|
|
7530
7607
|
// features/games/games/snake.jsx
|
|
@@ -7858,7 +7935,7 @@ function BreakoutGame(props) {
|
|
|
7858
7935
|
if (keys.current.right) g.paddle.x += 5.5;
|
|
7859
7936
|
g.paddle.x = Math.max(0, Math.min(W - PADDLE_W, g.paddle.x));
|
|
7860
7937
|
const b = g.ball;
|
|
7861
|
-
if (!
|
|
7938
|
+
if (!g.launched) {
|
|
7862
7939
|
b.x = g.paddle.x + PADDLE_W / 2;
|
|
7863
7940
|
b.y = g.paddle.y - BALL_R - 1;
|
|
7864
7941
|
} else {
|
|
@@ -7985,7 +8062,10 @@ function BreakoutGame(props) {
|
|
|
7985
8062
|
] })
|
|
7986
8063
|
] }),
|
|
7987
8064
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dgame-breakout", children: [
|
|
7988
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("canvas", { ref: canvasRef, width: W, height: H, tabIndex: 0, onKeyDown, onKeyUp, onPointerMove, onClick: () =>
|
|
8065
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("canvas", { ref: canvasRef, width: W, height: H, tabIndex: 0, onKeyDown, onKeyUp, onPointerMove, onClick: (e) => {
|
|
8066
|
+
focusStage(canvasRef);
|
|
8067
|
+
launchBall();
|
|
8068
|
+
}, className: "dgame-breakout-canvas", "aria-label": "\u6253\u7816\u5757\u6E38\u620F\u533A\u57DF\uFF0C\u5DE6\u53F3\u65B9\u5411\u952E\u6216\u9F20\u6807\u79FB\u52A8\u6321\u677F\uFF0C\u7A7A\u683C\u6216\u70B9\u51FB\u53D1\u7403" }),
|
|
7989
8069
|
status === "win" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dgame-over", children: [
|
|
7990
8070
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("strong", { children: [
|
|
7991
8071
|
"\u5168\u6D88\uFF01\u8FDB\u5165\u7B2C ",
|
|
@@ -8019,7 +8099,7 @@ function BreakoutGame(props) {
|
|
|
8019
8099
|
keys.current.left = false;
|
|
8020
8100
|
game.current && (game.current.paddle.x = Math.max(0, game.current.paddle.x - 12));
|
|
8021
8101
|
}, children: "\u2190 \u5DE6\u79FB" }),
|
|
8022
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "\u2190/\u2192 \u6216\u9F20\u6807\u79FB\u52A8 \xB7 \u7A7A\u683C\u53D1\u7403" }),
|
|
8102
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "\u2190/\u2192 \u6216\u9F20\u6807\u79FB\u52A8 \xB7 \u7A7A\u683C/\u70B9\u51FB\u53D1\u7403" }),
|
|
8023
8103
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("button", { type: "button", "aria-label": "\u5411\u53F3\u79FB\u52A8", onClick: () => {
|
|
8024
8104
|
keys.current.right = false;
|
|
8025
8105
|
game.current && (game.current.paddle.x = Math.min(W - PADDLE_W, game.current.paddle.x + 12));
|
|
@@ -8083,7 +8163,7 @@ function RacerGame(props) {
|
|
|
8083
8163
|
const reset = (0, import_react16.useCallback)(() => {
|
|
8084
8164
|
game.current = {
|
|
8085
8165
|
stars: Array.from({ length: 50 }, () => ({ x: Math.random() * W2, y: Math.random() * H2, a: 0.2 + Math.random() * 0.5 })),
|
|
8086
|
-
player: { x: laneX(1), y: H2 - CAR_H - 16 },
|
|
8166
|
+
player: { x: laneX(1), y: H2 - CAR_H - 16, w: CAR_W, h: CAR_H },
|
|
8087
8167
|
enemies: [],
|
|
8088
8168
|
coins: [],
|
|
8089
8169
|
dist: 0,
|
|
@@ -8955,7 +9035,7 @@ var GAMES = [
|
|
|
8955
9035
|
cat: "classic",
|
|
8956
9036
|
accent: "#38bdf8",
|
|
8957
9037
|
desc: "\u79FB\u52A8\u6321\u677F\u53CD\u5F39\u5C0F\u7403\uFF0C\u51FB\u788E\u5168\u90E8\u7816\u5757\uFF1B\u7403\u6389\u5230\u5E95\u90E8\u4E22\u4E00\u6761\u547D\u3002",
|
|
8958
|
-
tip: "\u2190/\u2192 \u6216\u9F20\u6807\u79FB\u52A8 \xB7 \u7A7A\u683C\u53D1\u7403",
|
|
9038
|
+
tip: "\u2190/\u2192 \u6216\u9F20\u6807\u79FB\u52A8 \xB7 \u7A7A\u683C/\u70B9\u51FB\u53D1\u7403",
|
|
8959
9039
|
Game: breakoutGame.Game,
|
|
8960
9040
|
Preview: breakoutGame.Preview
|
|
8961
9041
|
},
|
|
@@ -123,7 +123,7 @@ export function BreakoutGame(props) {
|
|
|
123
123
|
g.paddle.x = Math.max(0, Math.min(W - PADDLE_W, g.paddle.x));
|
|
124
124
|
// 球
|
|
125
125
|
const b = g.ball;
|
|
126
|
-
if (!
|
|
126
|
+
if (!g.launched) {
|
|
127
127
|
b.x = g.paddle.x + PADDLE_W / 2;
|
|
128
128
|
b.y = g.paddle.y - BALL_R - 1;
|
|
129
129
|
} else {
|
|
@@ -203,11 +203,11 @@ export function BreakoutGame(props) {
|
|
|
203
203
|
return <section className="dgame-game" aria-label="打砖块">
|
|
204
204
|
<div className="dgame-game-head"><div><h3>打砖块</h3><p>移动挡板反弹小球,击碎所有砖块;球落到底部扣一条命。</p></div><div className="dgame-score"><span>得分 <strong>{score}</strong></span><span>命 {lives} · 第 {level} 关</span></div></div>
|
|
205
205
|
<div className="dgame-breakout">
|
|
206
|
-
<canvas ref={canvasRef} width={W} height={H} tabIndex={0} onKeyDown={onKeyDown} onKeyUp={onKeyUp} onPointerMove={onPointerMove} onClick={() => focusStage(canvasRef)} className="dgame-breakout-canvas" aria-label="
|
|
206
|
+
<canvas ref={canvasRef} width={W} height={H} tabIndex={0} onKeyDown={onKeyDown} onKeyUp={onKeyUp} onPointerMove={onPointerMove} onClick={(e) => { focusStage(canvasRef); launchBall(); }} className="dgame-breakout-canvas" aria-label="打砖块游戏区域,左右方向键或鼠标移动挡板,空格或点击发球" />
|
|
207
207
|
{status === "win" ? <div className="dgame-over"><strong>全消!进入第 {level} 关</strong><button type="button" onClick={() => { start(level); setStatus("play"); statusRef.current = "play"; }}>下一关</button></div> : null}
|
|
208
208
|
{status === "over" ? <div className="dgame-over"><strong>球掉光了 · 得 {score} 分</strong><button type="button" onClick={() => { start(1); setScore(0); setLives(3); setStatus("play"); statusRef.current = "play"; livesRef.current = 3; }}>重新开始</button></div> : null}
|
|
209
209
|
</div>
|
|
210
|
-
<div className="dgame-controls"><button type="button" aria-label="向左移动" onClick={() => { keys.current.left = false; game.current && (game.current.paddle.x = Math.max(0, game.current.paddle.x - 12)); }}>← 左移</button><span>←/→ 或鼠标移动 ·
|
|
210
|
+
<div className="dgame-controls"><button type="button" aria-label="向左移动" onClick={() => { keys.current.left = false; game.current && (game.current.paddle.x = Math.max(0, game.current.paddle.x - 12)); }}>← 左移</button><span>←/→ 或鼠标移动 · 空格/点击发球</span><button type="button" aria-label="向右移动" onClick={() => { keys.current.right = false; game.current && (game.current.paddle.x = Math.min(W - PADDLE_W, game.current.paddle.x + 12)); }}>右移 {'->'}</button></div>
|
|
211
211
|
</section>;
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -33,7 +33,7 @@ export function RacerGame(props) {
|
|
|
33
33
|
const reset = useCallback(() => {
|
|
34
34
|
game.current = {
|
|
35
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 },
|
|
36
|
+
player: { x: laneX(1), y: H - CAR_H - 16, w: CAR_W, h: CAR_H },
|
|
37
37
|
enemies: [], coins: [],
|
|
38
38
|
dist: 0, coinsGot: 0, spawn: 0, coinSpawn: 0,
|
|
39
39
|
speed: 3.4, over: false,
|
|
@@ -27,13 +27,19 @@ function rotations(shape) {
|
|
|
27
27
|
}
|
|
28
28
|
const PIECES = SHAPES.map((s) => ({ color: s.color, rots: rotations(s) }));
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// 精确网格(viewBox 1000×2000,与方块单元格逐一对齐,杜绝背景纹理的亚像素漂移):
|
|
31
|
+
// 每个空格画一个深色内嵌圆角坑(经典机台的"格子凹槽"),再叠亮色网格线,空格一眼可辨。
|
|
31
32
|
const TETRIS_GRID = (() => {
|
|
32
|
-
const
|
|
33
|
+
const parts = [];
|
|
33
34
|
let k = 0;
|
|
34
|
-
for (let
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
for (let j = 0; j < 20; j += 1) {
|
|
36
|
+
for (let i = 0; i < 10; i += 1) {
|
|
37
|
+
parts.push(<rect key={k++} className="dgame-tetris-pit" x={i * 100 + 7} y={j * 100 + 7} width={86} height={86} rx={10} />);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (let i = 1; i < 10; i += 1) parts.push(<line key={k++} x1={i * 100} y1={0} x2={i * 100} y2={2000} />);
|
|
41
|
+
for (let j = 1; j < 20; j += 1) parts.push(<line key={k++} x1={0} y1={j * 100} x2={1000} y2={j * 100} />);
|
|
42
|
+
return parts;
|
|
37
43
|
})();
|
|
38
44
|
|
|
39
45
|
function emptyBoard() {
|
|
@@ -298,5 +304,5 @@ export function TetrisPreview() {
|
|
|
298
304
|
}
|
|
299
305
|
|
|
300
306
|
export const tetrisGame = { Game: TetrisGame, Preview: TetrisPreview, css: `
|
|
301
|
-
.dgame-tetris{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-start;align-self:center;width:fit-content;max-width:100%;margin-inline:auto;justify-content:center}.dgame-tetris-board{position:relative;flex:none;display:grid;grid-template-columns:repeat(10,1fr);grid-template-rows:repeat(20,1fr);width:min(100%,188px);aspect-ratio:10/20;border:1px solid color-mix(in srgb,rgb(34 211 238) 35%,var(--dsw-alias-border-l1));border-radius:8px;overflow:hidden;outline:none;background:linear-gradient(180deg,#0c1024,#131a3d);padding:0;box-shadow:inset 0 0 30px rgb(0 0 0/.45)}.dgame-tetris-board:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,rgb(34 211 238) 40%,transparent)}.dgame-tetris-cell{display:block;background:transparent;border-radius:2px;margin:1px}.dgame-tetris-nextbox{display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(2,1fr);width:60px;height:32px}.dgame-tetris-side{flex:1 1 96px;display:flex;flex-direction:column;gap:9px;min-width:0;max-width:220px;color:var(--dsw-alias-label-tertiary);font-size:11px}.dgame-tetris-side b{color:var(--dsw-alias-label-secondary);font-size:12px}.dgame-tetris-side span{font-size:11px}.dgame-tetris-nextbox .dgame-tetris-cell{margin:1px}.dgame-tetris-keys span{display:block;line-height:1.6;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dgame-tetris-next{display:flex;flex-direction:column;gap:5px;align-items:center}.dgame-tetris-next span{color:var(--dsw-alias-label-secondary)}.dgame-tetris-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.dgame-tetris-lines line{stroke
|
|
307
|
+
.dgame-tetris{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-start;align-self:center;width:fit-content;max-width:100%;margin-inline:auto;justify-content:center}.dgame-tetris-board{position:relative;flex:none;display:grid;grid-template-columns:repeat(10,1fr);grid-template-rows:repeat(20,1fr);width:min(100%,188px);aspect-ratio:10/20;border:1px solid color-mix(in srgb,rgb(34 211 238) 35%,var(--dsw-alias-border-l1));border-radius:8px;overflow:hidden;outline:none;background:linear-gradient(180deg,#0c1024,#131a3d);padding:0;box-shadow:inset 0 0 30px rgb(0 0 0/.45)}.dgame-tetris-board:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,rgb(34 211 238) 40%,transparent)}.dgame-tetris-cell{position:relative;z-index:1;display:block;background:transparent;border-radius:2px;margin:1px}.dgame-tetris-nextbox{display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(2,1fr);width:60px;height:32px}.dgame-tetris-side{flex:1 1 96px;display:flex;flex-direction:column;gap:9px;min-width:0;max-width:220px;color:var(--dsw-alias-label-tertiary);font-size:11px}.dgame-tetris-side b{color:var(--dsw-alias-label-secondary);font-size:12px}.dgame-tetris-side span{font-size:11px}.dgame-tetris-nextbox .dgame-tetris-cell{margin:1px}.dgame-tetris-keys span{display:block;line-height:1.6;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dgame-tetris-next{display:flex;flex-direction:column;gap:5px;align-items:center}.dgame-tetris-next span{color:var(--dsw-alias-label-secondary)}.dgame-tetris-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.dgame-tetris-lines line{stroke:rgb(96 165 250/.28);stroke-width:3}.dgame-tetris-lines .dgame-tetris-pit{fill:rgb(0 0 0/.42);stroke:rgb(255 255 255/.07);stroke-width:2}@media (max-width:680px){.dgame-tetris{width:100%;gap:8px}.dgame-tetris-board{width:min(100%,170px)}}
|
|
302
308
|
` };
|
|
@@ -37,6 +37,21 @@ const XIANGQI_LINES = (() => {
|
|
|
37
37
|
return parts;
|
|
38
38
|
})();
|
|
39
39
|
|
|
40
|
+
// 传统棋谱着法(如"炮二平五"/"馬8进7"):红方用中文数字、黑方用阿拉伯数字,各自从己方右侧数起。
|
|
41
|
+
const CN_NUMS = ["一", "二", "三", "四", "五", "六", "七", "八", "九"];
|
|
42
|
+
function moveNotation(board, fr, fc, tr, tc) {
|
|
43
|
+
const p = board[fr][fc];
|
|
44
|
+
if (!p) return "";
|
|
45
|
+
const red = p.c === "r";
|
|
46
|
+
const num = (n) => (red ? CN_NUMS[n - 1] : String(n));
|
|
47
|
+
const file = (c) => (red ? 9 - c : c + 1);
|
|
48
|
+
const forward = red ? tr < fr : tr > fr;
|
|
49
|
+
const verb = fr === tr ? "平" : (forward ? "进" : "退");
|
|
50
|
+
// 直行棋子进/退记步数;斜行棋子(马/象/士)进/退记目标线位。
|
|
51
|
+
const to = fr === tr || p.t === "h" || p.t === "e" || p.t === "a" ? file(tc) : Math.abs(tr - fr);
|
|
52
|
+
return pieceChar(p) + num(file(fc)) + verb + num(to);
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
export function XiangqiGame(props) {
|
|
41
56
|
const [board, setBoard] = useState(initialBoard);
|
|
42
57
|
const [turn, setTurn] = useState("r");
|
|
@@ -45,6 +60,11 @@ export function XiangqiGame(props) {
|
|
|
45
60
|
const [over, setOver] = useState(null); // 'r' | 'b' | 'draw'
|
|
46
61
|
const [check, setCheck] = useState(false);
|
|
47
62
|
const [last, setLast] = useState(null);
|
|
63
|
+
// 上一手的起点/终点/棋谱文本:起点画虚线圈、终点画红点,棋谱行让玩家看清对方走了什么。
|
|
64
|
+
const [lastMove, setLastMove] = useState(null); // { fr, fc, tr, tc, text }
|
|
65
|
+
const [moveLog, setMoveLog] = useState([]); // [{ side, text }] 双方着法流水
|
|
66
|
+
// 跳跃动画:moving = { fr, fc, tr, tc, piece, capture },动画结束后落定为普通棋子。
|
|
67
|
+
const [moving, setMoving] = useState(null);
|
|
48
68
|
const [thinking, setThinking] = useState(false);
|
|
49
69
|
const [history, setHistory] = useState([]);
|
|
50
70
|
// 已出现过的局面指纹:AI 搜索时排除重复局面,杜绝"反复将军无限循环"。
|
|
@@ -57,44 +77,78 @@ export function XiangqiGame(props) {
|
|
|
57
77
|
const overRef = useRef(over); overRef.current = over;
|
|
58
78
|
const thinkingRef = useRef(thinking); thinkingRef.current = thinking;
|
|
59
79
|
const historyRef = useRef(history); historyRef.current = history;
|
|
80
|
+
// 动画期间锁输入与 AI 回合,结束后调用 settle 落定真正的棋盘状态。
|
|
81
|
+
const movingRef = useRef(false);
|
|
82
|
+
const pendingRef = useRef(null);
|
|
83
|
+
|
|
84
|
+
// 起跳动画:先在起点渲染飞行棋子,260ms 后落地并把棋盘推进到新局面。
|
|
85
|
+
const animTimerRef = useRef(null);
|
|
86
|
+
const animateMove = useCallback((b, fr, fc, tr, tc, after) => {
|
|
87
|
+
// 被移动/被吃的棋子须从旧盘(boardRef)读取:b 已是落子后的新盘。
|
|
88
|
+
const piece = boardRef.current[fr][fc];
|
|
89
|
+
const capture = !!boardRef.current[tr][tc];
|
|
90
|
+
pendingRef.current = { board: b, after };
|
|
91
|
+
movingRef.current = true;
|
|
92
|
+
setSel(null); setTargets([]);
|
|
93
|
+
setMoving({ fr, fc, tr, tc, piece, capture });
|
|
94
|
+
playSfx(capture ? "capture" : "step");
|
|
95
|
+
if (animTimerRef.current) clearTimeout(animTimerRef.current);
|
|
96
|
+
animTimerRef.current = setTimeout(() => {
|
|
97
|
+
animTimerRef.current = null;
|
|
98
|
+
const pend = pendingRef.current;
|
|
99
|
+
pendingRef.current = null;
|
|
100
|
+
movingRef.current = false;
|
|
101
|
+
setMoving(null);
|
|
102
|
+
if (pend) {
|
|
103
|
+
const text = moveNotation(boardRef.current, fr, fc, tr, tc);
|
|
104
|
+
boardRef.current = pend.board;
|
|
105
|
+
setBoard(pend.board);
|
|
106
|
+
setLastMove({ fr, fc, tr, tc, text });
|
|
107
|
+
setMoveLog((log) => log.concat({ side: piece.c, text }));
|
|
108
|
+
setLast([tr, tc]);
|
|
109
|
+
if (pend.after) pend.after();
|
|
110
|
+
}
|
|
111
|
+
}, 260);
|
|
112
|
+
}, []);
|
|
60
113
|
|
|
61
114
|
const restart = useCallback(() => {
|
|
115
|
+
if (animTimerRef.current) { clearTimeout(animTimerRef.current); animTimerRef.current = null; }
|
|
116
|
+
pendingRef.current = null; movingRef.current = false;
|
|
62
117
|
const b = initialBoard();
|
|
63
118
|
boardRef.current = b;
|
|
64
119
|
setBoard(b); setTurn("r"); setSel(null); setTargets([]); setOver(null); setCheck(false); setLast(null); setThinking(false); setHistory([]);
|
|
65
|
-
setRepKeys([posKey(b)]);
|
|
120
|
+
setLastMove(null); setMoveLog([]); setMoving(null); setRepKeys([posKey(b)]);
|
|
66
121
|
}, []);
|
|
67
122
|
|
|
68
123
|
const undo = useCallback(() => {
|
|
69
|
-
if (thinkingRef.current || overRef.current || turnRef.current !== "r") return;
|
|
124
|
+
if (thinkingRef.current || overRef.current || turnRef.current !== "r" || movingRef.current) return;
|
|
70
125
|
const h = historyRef.current;
|
|
71
126
|
if (h.length < 2) return;
|
|
72
127
|
const hist = h.slice(0, -2);
|
|
73
128
|
const b = hist.length ? cloneBoard(hist[hist.length - 1]) : initialBoard();
|
|
74
129
|
boardRef.current = b;
|
|
75
|
-
setBoard(b); setHistory(hist); setSel(null); setTargets([]); setLast(null); setOver(null);
|
|
130
|
+
setBoard(b); setHistory(hist); setSel(null); setTargets([]); setLast(null); setLastMove(null); setMoveLog((prevLog) => prevLog.slice(0, -2)); setOver(null);
|
|
76
131
|
setCheck(inCheck(b, "r"));
|
|
77
132
|
// 重建局面指纹(历史各局面 + 当前)。
|
|
78
133
|
setRepKeys(hist.map(posKey).concat(posKey(b)));
|
|
79
134
|
}, []);
|
|
80
135
|
|
|
81
136
|
const moveRed = useCallback((tr, tc) => {
|
|
82
|
-
if (turnRef.current !== "r" || overRef.current || !sel) return;
|
|
137
|
+
if (turnRef.current !== "r" || overRef.current || !sel || movingRef.current) return;
|
|
83
138
|
const fr = sel[0], fc = sel[1];
|
|
84
|
-
if (boardRef.current[tr][tc]) playSfx("capture"); else playSfx("step");
|
|
85
139
|
const nb = applyMove(boardRef.current, fr, fc, tr, tc);
|
|
86
140
|
setHistory((h) => [...h, boardRef.current]);
|
|
87
|
-
boardRef.current = nb;
|
|
88
141
|
pushKey(nb);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
142
|
+
animateMove(nb, fr, fc, tr, tc, () => {
|
|
143
|
+
// 动画落地后再移交回合:让 AI 的 useEffect 依赖到最新 turn 才启动搜索。
|
|
144
|
+
setTurn("b");
|
|
145
|
+
if (!hasLegalMoves(nb, "b")) { setOver("r"); setCheck(false); }
|
|
146
|
+
else setCheck(inCheck(nb, "b"));
|
|
147
|
+
});
|
|
148
|
+
}, [sel, pushKey, animateMove]);
|
|
95
149
|
|
|
96
150
|
const select = useCallback((r, c) => {
|
|
97
|
-
if (overRef.current || turnRef.current !== "r" || thinkingRef.current) return;
|
|
151
|
+
if (overRef.current || turnRef.current !== "r" || thinkingRef.current || movingRef.current) return;
|
|
98
152
|
const p = boardRef.current[r][c];
|
|
99
153
|
if (p && p.c === "r") {
|
|
100
154
|
setSel([r, c]);
|
|
@@ -106,9 +160,9 @@ export function XiangqiGame(props) {
|
|
|
106
160
|
setSel(null); setTargets([]);
|
|
107
161
|
}, [sel, targets, moveRed]);
|
|
108
162
|
|
|
109
|
-
// AI
|
|
163
|
+
// AI 回合(执黑):先想棋(thinking 期间玩家能看到"AI 思考中"),落子走同款跳跃动画。
|
|
110
164
|
useEffect(() => {
|
|
111
|
-
if (turn !== "b" || over || props.paused) return;
|
|
165
|
+
if (turn !== "b" || over || props.paused || movingRef.current) return;
|
|
112
166
|
const t = setTimeout(() => {
|
|
113
167
|
// 局面出现次数表:AI 优先走新局面;全部重复时也倾向去往出现最少的局面。
|
|
114
168
|
const seenCounts = new Map();
|
|
@@ -116,20 +170,20 @@ export function XiangqiGame(props) {
|
|
|
116
170
|
const mv = bestMove(boardRef.current, "b", 3, seenCounts);
|
|
117
171
|
if (mv) {
|
|
118
172
|
const [fr, fc, tr, tc] = mv;
|
|
119
|
-
if (boardRef.current[tr][tc]) playSfx("capture"); else playSfx("step");
|
|
120
173
|
const nb = applyMove(boardRef.current, fr, fc, tr, tc);
|
|
121
174
|
setHistory((h) => [...h, boardRef.current]);
|
|
122
|
-
boardRef.current = nb;
|
|
123
175
|
pushKey(nb);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
176
|
+
animateMove(nb, fr, fc, tr, tc, () => {
|
|
177
|
+
setTurn("r");
|
|
178
|
+
if (!hasLegalMoves(nb, "r")) { setOver("b"); setCheck(false); }
|
|
179
|
+
else setCheck(inCheck(nb, "r"));
|
|
180
|
+
});
|
|
127
181
|
} else setOver("r");
|
|
128
182
|
setThinking(false);
|
|
129
|
-
},
|
|
183
|
+
}, 420);
|
|
130
184
|
setThinking(true);
|
|
131
185
|
return () => clearTimeout(t);
|
|
132
|
-
}, [turn, over, props.paused, repKeys, pushKey]);
|
|
186
|
+
}, [turn, over, props.paused, repKeys, pushKey, animateMove]);
|
|
133
187
|
|
|
134
188
|
const onKeyDown = useCallback((event) => {
|
|
135
189
|
if (event.key.toLowerCase() === "r") { event.preventDefault(); restart(); }
|
|
@@ -137,7 +191,7 @@ export function XiangqiGame(props) {
|
|
|
137
191
|
}, [restart, undo]);
|
|
138
192
|
useGameControls(stageRef, props.paused, onKeyDown);
|
|
139
193
|
|
|
140
|
-
const status = over === "r" ? "红方胜 · 恭喜" : over === "b" ? "黑方胜 · AI 赢" : (thinking ? "AI 思考中…" : (check ? "将军!" : (turn === "r" ? "轮到你(红方)" : "轮到 AI(黑方)")));
|
|
194
|
+
const status = over === "r" ? "红方胜 · 恭喜" : over === "b" ? "黑方胜 · AI 赢" : (moving ? (moving.piece.c === "r" ? "红方走子…" : "AI 走子…") : (thinking ? "AI 思考中…" : (check ? "将军!" : (turn === "r" ? "轮到你(红方)" : "轮到 AI(黑方)"))));
|
|
141
195
|
const prevCheck = useRef(false);
|
|
142
196
|
useEffect(() => {
|
|
143
197
|
if (over === "r") playSfx("win");
|
|
@@ -145,6 +199,8 @@ export function XiangqiGame(props) {
|
|
|
145
199
|
else if (!thinking && check && !prevCheck.current) playSfx("check");
|
|
146
200
|
prevCheck.current = !thinking && check;
|
|
147
201
|
}, [check, thinking, over]);
|
|
202
|
+
// 组件卸载时清掉未落地的动画定时器,避免对已卸载组件 setState。
|
|
203
|
+
useEffect(() => () => { if (animTimerRef.current) clearTimeout(animTimerRef.current); }, []);
|
|
148
204
|
|
|
149
205
|
return <section className="dgame-game" aria-label="象棋">
|
|
150
206
|
<div className="dgame-game-head"><div><h3>中国象棋</h3><p>你执红先行,AI 执黑;吃掉对方将/帅即胜。点击选中红子,再点高亮落点走棋。</p></div><div className="dgame-score"><span>当前</span><strong className="dg-gomoku-status">{status}</strong><button type="button" onClick={undo}>撤销</button><button type="button" onClick={restart}>重开</button></div></div>
|
|
@@ -155,17 +211,33 @@ export function XiangqiGame(props) {
|
|
|
155
211
|
<text className="dgame-xiangqi-rivertext" x={670} y={505}>漢 界</text>
|
|
156
212
|
</svg>
|
|
157
213
|
{board.map((row, r) => row.map((p, c) => {
|
|
214
|
+
const mv = moving;
|
|
215
|
+
// 动画期间:飞行棋子由独立层渲染(起点格不再画它);被吃棋子原地淡出。
|
|
216
|
+
const isFlyFrom = mv && mv.fr === r && mv.fc === c;
|
|
217
|
+
const isDying = mv && mv.capture && mv.tr === r && mv.tc === c;
|
|
158
218
|
const isSel = sel && sel[0] === r && sel[1] === c;
|
|
159
219
|
const isTargetMark = targets.some(([tr, tc]) => tr === r && tc === c);
|
|
220
|
+
const isLastMoveFrom = lastMove && lastMove.fr === r && lastMove.fc === c;
|
|
221
|
+
const isLand = !mv && lastMove && lastMove.tr === r && lastMove.tc === c;
|
|
160
222
|
const isLast = last && last[0] === r && last[1] === c;
|
|
161
223
|
return <button type="button" key={r + "-" + c} role="gridcell"
|
|
162
|
-
className={"dgame-xiangqi-cell" + (isSel ? " sel" : "") + (isTargetMark ? " target" : "") + (isLast ? " last" : "")}
|
|
224
|
+
className={"dgame-xiangqi-cell" + (isSel ? " sel" : "") + (isTargetMark ? " target" : "") + (isLastMoveFrom ? " from" : "") + (isLand ? " land" : "") + (isLast ? " last" : "")}
|
|
163
225
|
aria-label={"第" + (r + 1) + "行第" + (c + 1) + "列" + (p ? pieceChar(p) : " 空")}
|
|
164
226
|
onClick={() => { focusStage(stageRef); select(r, c); }}>
|
|
165
|
-
{p ? <span className={"dgame-xiangqi-pc " + p.c}>{pieceChar(p)}</span> : (isTargetMark ? <span className="dgame-xiangqi-dot" aria-hidden="true" /> : null)}
|
|
227
|
+
{p && !isFlyFrom ? <span className={"dgame-xiangqi-pc " + p.c + (isDying ? " dying" : "")}>{pieceChar(p)}</span> : (!p && isTargetMark ? <span className="dgame-xiangqi-dot" aria-hidden="true" /> : null)}
|
|
166
228
|
</button>;
|
|
167
229
|
}))}
|
|
230
|
+
{moving ? <span key={moving.fr + "-" + moving.fc + "-" + moving.tr + "-" + moving.tc + "-" + moving.piece.c}
|
|
231
|
+
className={"dgame-xiangqi-pc dgame-xiangqi-fly " + moving.piece.c}
|
|
232
|
+
style={{
|
|
233
|
+
"--fx0": (moving.fc + 0.5) / 9, "--fy0": (moving.fr + 0.5) / 10,
|
|
234
|
+
"--fx1": (moving.tc + 0.5) / 9, "--fy1": (moving.tr + 0.5) / 10,
|
|
235
|
+
}}
|
|
236
|
+
aria-hidden="true">{pieceChar(moving.piece)}</span> : null}
|
|
168
237
|
</div>
|
|
238
|
+
{moveLog.length ? <div className="dgame-xiangqi-log" role="status" aria-live="polite">
|
|
239
|
+
{moveLog.slice(-4).map((m, i) => <span key={moveLog.length - 4 + i} className={m.side}>{m.text}</span>)}
|
|
240
|
+
</div> : null}
|
|
169
241
|
<div className="dgame-controls"><button type="button" onClick={undo}>撤销</button><span>{status} · R 重开 · U 撤销</span><button type="button" onClick={restart}>重开</button></div>
|
|
170
242
|
</section>;
|
|
171
243
|
}
|
|
@@ -201,5 +273,5 @@ export function XiangqiPreview() {
|
|
|
201
273
|
}
|
|
202
274
|
|
|
203
275
|
export const xiangqiGame = { Game: XiangqiGame, Preview: XiangqiPreview, css: `
|
|
204
|
-
.dgame-xiangqi-board{position:relative;display:grid;grid-template-columns:repeat(9,1fr);grid-template-rows:repeat(10,1fr);align-self:center;width:min(100%,min(90vw,340px));aspect-ratio:9/10;padding:13px;border-radius:12px;border:1px solid color-mix(in srgb,#7c4f16 65%,transparent);box-shadow:0 12px 32px rgb(0 0 0/.5),inset 0 1px 0 rgba(255,236,200,.55),inset 0 -3px 9px rgba(90,50,10,.35),inset 3px 0 8px rgba(255,224,170,.28),inset -3px 0 8px rgba(120,70,20,.25);outline:none;background:repeating-linear-gradient(94deg,rgba(124,79,22,.05) 0 2px,transparent 2px 11px),repeating-linear-gradient(87deg,rgba(255,238,196,.05) 0 2px,transparent 2px 14px),radial-gradient(130% 105% at 26% 8%,#f0cd86,#ddb067 48%,#cb9a51 78%,#bb8844)}.dgame-xiangqi-lines{position:absolute;inset:13px;pointer-events:none}.dgame-xiangqi-lines line,.dgame-xiangqi-lines path{stroke:#66431a;stroke-width:3.4;fill:none;stroke-linecap:square}.dgame-xiangqi-lines .dgame-xiangqi-mark{stroke-width:2.6}.dgame-xiangqi-rivertext{fill:rgba(102,67,26,.7);font-family:KaiTi,"Kaiti SC","STKaiti","FangSong",serif;font-size:46px;letter-spacing:7px;text-anchor:middle;dominant-baseline:central}.dgame-xiangqi-board:focus-visible{box-shadow:0 0 0 3px rgba(0,0,0,.25)}.dgame-xiangqi-cell{position:relative;z-index:1;background:transparent;border:none;cursor:pointer;padding:0;margin:0;outline:none;display:grid;place-items:center;min-width:0;min-height:0}.dgame-xiangqi-cell.target .dgame-xiangqi-dot{width:12px;height:12px;border-radius:50%;background:rgba(34,211,238,.6);box-shadow:0 0 10px rgba(34,211,238,.65),0 0 0 2px rgba(255,255,255,.28)}.dgame-xiangqi-cell.target .dgame-xiangqi-pc{box-shadow:0 0 0 2px rgba(248,113,113,.85),0 0 10px rgba(248,113,113,.5),0 2px 4px rgb(0 0 0/.4),inset 0 0 0 1.5px rgba(96,62,18,.5),inset 0 1px 2px rgba(255,255,255,.95),inset 0 -2px 3px rgba(120,80,20,.4)}.dgame-xiangqi-cell.sel .dgame-xiangqi-pc::before{content:'';position:absolute;inset:-3px;border-radius:50%;border:2.5px solid rgba(251,191,36,.95);box-shadow:0 0 12px rgba(251,191,36,.6)}.dgame-xiangqi-cell.last .dgame-xiangqi-pc::after{content:'';position:absolute;right:5%;bottom:5%;width:5px;height:5px;border-radius:50%;background:#ef4444;box-shadow:0 0 4px rgba(239,68,68,.8)}.dgame-xiangqi-pc{position:relative;display:grid;place-items:center;z-index:1;width:min(80%,30px);aspect-ratio:1/1;border-radius:50%;box-sizing:border-box;font-family:KaiTi,"Kaiti SC","STKaiti","FangSong","SimSun",serif;font-size:clamp(12px,1.4vw,15px);font-weight:700;line-height:1;background:radial-gradient(circle at 34% 28%,#fff3d4,#eedaa6 46%,#d8b675 72%,#bd9650);box-shadow:0 2px 4px rgb(0 0 0/.45),inset 0 0 0 1.5px rgba(96,62,18,.5),inset 0 1px 2px rgba(255,255,255,.95),inset 0 -2px 3px rgba(120,80,20,.4)}.dgame-xiangqi-pc.r{color:#b91c1c;text-shadow:0 1px 0 rgba(255,255,255,.5)}.dgame-xiangqi-
|
|
276
|
+
.dgame-xiangqi-board{position:relative;display:grid;grid-template-columns:repeat(9,1fr);grid-template-rows:repeat(10,1fr);align-self:center;width:min(100%,min(90vw,340px));aspect-ratio:9/10;padding:13px;border-radius:12px;border:1px solid color-mix(in srgb,#7c4f16 65%,transparent);box-shadow:0 12px 32px rgb(0 0 0/.5),inset 0 1px 0 rgba(255,236,200,.55),inset 0 -3px 9px rgba(90,50,10,.35),inset 3px 0 8px rgba(255,224,170,.28),inset -3px 0 8px rgba(120,70,20,.25);outline:none;background:repeating-linear-gradient(94deg,rgba(124,79,22,.05) 0 2px,transparent 2px 11px),repeating-linear-gradient(87deg,rgba(255,238,196,.05) 0 2px,transparent 2px 14px),radial-gradient(130% 105% at 26% 8%,#f0cd86,#ddb067 48%,#cb9a51 78%,#bb8844)}.dgame-xiangqi-lines{position:absolute;inset:13px;pointer-events:none}.dgame-xiangqi-lines line,.dgame-xiangqi-lines path,.dgame-xiangqi-lines rect{stroke:#66431a;stroke-width:3.4;fill:none;stroke-linecap:square}.dgame-xiangqi-lines rect{stroke-width:7}.dgame-xiangqi-lines .dgame-xiangqi-mark{stroke-width:2.6}.dgame-xiangqi-rivertext{fill:rgba(102,67,26,.7);font-family:KaiTi,"Kaiti SC","STKaiti","FangSong",serif;font-size:46px;letter-spacing:7px;text-anchor:middle;dominant-baseline:central}.dgame-xiangqi-board:focus-visible{box-shadow:0 0 0 3px rgba(0,0,0,.25)}.dgame-xiangqi-cell{position:relative;z-index:1;background:transparent;border:none;cursor:pointer;padding:0;margin:0;outline:none;display:grid;place-items:center;min-width:0;min-height:0}.dgame-xiangqi-cell.target .dgame-xiangqi-dot{width:12px;height:12px;border-radius:50%;background:rgba(34,211,238,.6);box-shadow:0 0 10px rgba(34,211,238,.65),0 0 0 2px rgba(255,255,255,.28)}.dgame-xiangqi-cell.target .dgame-xiangqi-pc{box-shadow:0 0 0 2px rgba(248,113,113,.85),0 0 10px rgba(248,113,113,.5),0 2px 4px rgb(0 0 0/.4),inset 0 0 0 1.5px rgba(96,62,18,.5),inset 0 1px 2px rgba(255,255,255,.95),inset 0 -2px 3px rgba(120,80,20,.4)}.dgame-xiangqi-cell.sel .dgame-xiangqi-pc::before{content:'';position:absolute;inset:-3px;border-radius:50%;border:2.5px solid rgba(251,191,36,.95);box-shadow:0 0 12px rgba(251,191,36,.6)}.dgame-xiangqi-cell.from::before{content:'';position:absolute;inset:50% auto auto 50%;width:34%;height:34%;transform:translate(-50%,-50%);border-radius:50%;border:2.5px dashed rgba(102,67,26,.75);pointer-events:none}.dgame-xiangqi-cell.land .dgame-xiangqi-pc{animation:dgame-xq-land .28s cubic-bezier(.2,.85,.3,1.25)}.dgame-xiangqi-cell.last .dgame-xiangqi-pc::after{content:'';position:absolute;right:5%;bottom:5%;width:5px;height:5px;border-radius:50%;background:#ef4444;box-shadow:0 0 4px rgba(239,68,68,.8)}.dgame-xiangqi-pc{position:relative;display:grid;place-items:center;z-index:1;width:min(80%,30px);aspect-ratio:1/1;border-radius:50%;box-sizing:border-box;font-family:KaiTi,"Kaiti SC","STKaiti","FangSong","SimSun",serif;font-size:clamp(12px,1.4vw,15px);font-weight:700;line-height:1;background:radial-gradient(circle at 34% 28%,#fff3d4,#eedaa6 46%,#d8b675 72%,#bd9650);box-shadow:0 2px 4px rgb(0 0 0/.45),inset 0 0 0 1.5px rgba(96,62,18,.5),inset 0 1px 2px rgba(255,255,255,.95),inset 0 -2px 3px rgba(120,80,20,.4)}.dgame-xiangqi-pc.dying{animation:dgame-xq-die .26s ease forwards}.dgame-xiangqi-fly{position:absolute;z-index:5;width:calc((100% - 26px)*.0889);aspect-ratio:1/1;border-radius:50%;display:grid;place-items:center;box-sizing:border-box;font-family:KaiTi,"Kaiti SC","STKaiti","FangSong","SimSun",serif;font-size:clamp(12px,1.4vw,15px);font-weight:700;line-height:1;pointer-events:none;background:radial-gradient(circle at 34% 28%,#fff3d4,#eedaa6 46%,#d8b675 72%,#bd9650);box-shadow:0 6px 14px rgb(0 0 0/.5),0 0 0 1.5px rgba(96,62,18,.35),inset 0 0 0 1.5px rgba(96,62,18,.5),inset 0 1px 2px rgba(255,255,255,.95),inset 0 -2px 3px rgba(120,80,20,.4);animation:dgame-xq-hop .26s cubic-bezier(.3,.7,.4,1) forwards}.dgame-xiangqi-fly.r{color:#b91c1c;text-shadow:0 1px 0 rgba(255,255,255,.5)}.dgame-xiangqi-fly.b{color:#1f2937;text-shadow:0 1px 0 rgba(255,255,255,.35)}@keyframes dgame-xq-hop{0%{left:calc(13px + var(--fx0)*(100% - 26px));top:calc(13px + var(--fy0)*(100% - 26px));transform:translate(-50%,-50%) scale(.9);opacity:.85}55%{transform:translate(-50%,-64%) scale(1.16);opacity:1}100%{left:calc(13px + var(--fx1)*(100% - 26px));top:calc(13px + var(--fy1)*(100% - 26px));transform:translate(-50%,-50%) scale(1)}}@keyframes dgame-xq-land{0%{transform:scale(1.18)}100%{transform:scale(1)}}@keyframes dgame-xq-die{to{opacity:0;transform:scale(.55)}}.dgame-xiangqi-log{display:flex;flex-wrap:wrap;justify-content:center;gap:5px 12px;align-self:center;min-height:22px;color:var(--dsw-alias-label-secondary);font-family:KaiTi,"Kaiti SC","STKaiti","FangSong",serif;font-size:13px}.dgame-xiangqi-log span{opacity:.65}.dgame-xiangqi-log span:last-child{opacity:1;font-weight:700}.dgame-xiangqi-log .r{color:#c03434}.dgame-xiangqi-log .b{color:var(--dsw-alias-label-primary)}
|
|
205
277
|
` };
|
package/features/games/view.jsx
CHANGED
|
@@ -210,7 +210,7 @@ const GAMES = [
|
|
|
210
210
|
{
|
|
211
211
|
id: "breakout", name: "打砖块", cat: "classic", accent: "#38bdf8",
|
|
212
212
|
desc: "移动挡板反弹小球,击碎全部砖块;球掉到底部丢一条命。",
|
|
213
|
-
tip: "←/→ 或鼠标移动 ·
|
|
213
|
+
tip: "←/→ 或鼠标移动 · 空格/点击发球",
|
|
214
214
|
Game: breakoutGame.Game, Preview: breakoutGame.Preview,
|
|
215
215
|
},
|
|
216
216
|
{
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// 配对状态、在线设备与接力备注只存内存;会话数据始终只有 ~/.dsh 这一份。
|
|
17
17
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
18
18
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { stat } from 'node:fs/promises'
|
|
19
20
|
import { homedir } from 'node:os'
|
|
20
21
|
import { dirname, join } from 'node:path'
|
|
21
22
|
import yaml from 'js-yaml'
|
|
@@ -193,6 +194,58 @@ export const feature = {
|
|
|
193
194
|
|
|
194
195
|
disposers.push(ctx.inject(['settings'], (sctx) => { settingsCtx = sctx }))
|
|
195
196
|
|
|
197
|
+
// ── 浏览模式目录选择器的 Windows 跨盘补全 ──
|
|
198
|
+
// 官方 browse 后端从主目录起步,只能列出当前盘内的子目录,面包屑在主目录之上
|
|
199
|
+
// 全部折叠——Windows 开着远程访问时,选工作区根本到不了其他磁盘。
|
|
200
|
+
// 这里在运行时包装 browse 能力的 list():主目录层级注入全部盘符入口,盘根目录
|
|
201
|
+
// 注入其他盘符入口,远程浏览即可像资源管理器一样跨盘选工作区。非 Windows 或
|
|
202
|
+
// 非浏览模式(原生对话框本身可跨盘)零改动。
|
|
203
|
+
const DRIVE_SCAN_TTL = 5000
|
|
204
|
+
let driveScan = { at: 0, letters: null }
|
|
205
|
+
function sameWinPath(a, b) {
|
|
206
|
+
return a.replace(/[\\/]+/g, '\\').toLowerCase() === b.replace(/[\\/]+/g, '\\').toLowerCase()
|
|
207
|
+
}
|
|
208
|
+
function winDriveLetterOf(p) {
|
|
209
|
+
return /^[A-Za-z]:[\\/]?$/.test(p) ? p.slice(0, 1).toUpperCase() : null
|
|
210
|
+
}
|
|
211
|
+
async function windowsDrivePaths() {
|
|
212
|
+
if (driveScan.letters && Date.now() - driveScan.at < DRIVE_SCAN_TTL) return driveScan.letters
|
|
213
|
+
const probes = []
|
|
214
|
+
for (const letter of 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') {
|
|
215
|
+
probes.push(stat(letter + ':\\').then(() => letter, () => null))
|
|
216
|
+
}
|
|
217
|
+
const letters = (await Promise.all(probes)).filter(Boolean).sort()
|
|
218
|
+
driveScan = { at: Date.now(), letters }
|
|
219
|
+
return letters
|
|
220
|
+
}
|
|
221
|
+
disposers.push(ctx.inject(['directoryPicker'], (dpCtx) => {
|
|
222
|
+
if (process.platform !== 'win32') return
|
|
223
|
+
const picker = dpCtx.directoryPicker
|
|
224
|
+
if (!picker || typeof picker.capability !== 'function') return
|
|
225
|
+
let cap
|
|
226
|
+
try { cap = picker.capability() } catch { return }
|
|
227
|
+
if (!cap || cap.kind !== 'browse' || typeof cap.list !== 'function' || cap.__dshDockDrives) return
|
|
228
|
+
cap.__dshDockDrives = true
|
|
229
|
+
const origList = cap.list
|
|
230
|
+
cap.list = async (path, signal) => {
|
|
231
|
+
const listing = await origList(path, signal)
|
|
232
|
+
try {
|
|
233
|
+
const target = String((listing && listing.path) || '')
|
|
234
|
+
const atHome = sameWinPath(target, String(listing.home || '\u0000'))
|
|
235
|
+
const driveRoot = winDriveLetterOf(target)
|
|
236
|
+
// 只在主目录与盘根两级注入:这两级是跨盘的必经之地,其余层级保持官方原样
|
|
237
|
+
if (atHome || driveRoot) {
|
|
238
|
+
const letters = await windowsDrivePaths()
|
|
239
|
+
const rows = letters
|
|
240
|
+
.filter((letter) => !driveRoot || letter !== driveRoot)
|
|
241
|
+
.map((letter) => ({ name: letter + ':\\', path: letter + ':\\', hidden: false }))
|
|
242
|
+
if (rows.length) listing.entries = atHome ? rows.concat(listing.entries) : listing.entries.concat(rows)
|
|
243
|
+
}
|
|
244
|
+
} catch { /* 盘符探测失败保持原列表,不让补全拖垮选目录 */ }
|
|
245
|
+
return listing
|
|
246
|
+
}
|
|
247
|
+
}))
|
|
248
|
+
|
|
196
249
|
// ── 远程访问账号(settings 持久化;密码只存加盐哈希) ──
|
|
197
250
|
function hashPassword(salt, password) {
|
|
198
251
|
return createHash('sha256').update(String(salt) + '\u0000' + String(password), 'utf8').digest('hex')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dock",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计)、模型设置、图片理解代理、模型余额、任务动画(8 种氛围动效 + 桌面伙伴 3D 场景、完成通知与提示音、钉钉/飞书推送,开关独立、配置持久化);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|