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 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 lines = [];
6154
+ const parts = [];
6155
6155
  let k = 0;
6156
- for (let i = 1; i < 10; i += 1) lines.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: i * 100, y1: 0, x2: i * 100, y2: 2e3 }, k++));
6157
- for (let j = 1; j < 20; j += 1) lines.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: 0, y1: j * 100, x2: 1e3, y2: j * 100 }, k++));
6158
- return lines;
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:#60a5fa;stroke-opacity:.12;stroke-width:2}@media (max-width:680px){.dgame-tetris{width:100%;gap:8px}.dgame-tetris-board{width:min(100%,170px)}}
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
- setBoard(nb);
7380
- setSel(null);
7381
- setTargets([]);
7382
- setLast([tr, tc]);
7383
- setTurn("b");
7384
- if (!hasLegalMoves(nb, "b")) {
7385
- setOver("r");
7386
- setCheck(false);
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
- setBoard(nb);
7420
- setLast([tr, tc]);
7421
- setTurn("r");
7422
- if (!hasLegalMoves(nb, "r")) {
7423
- setOver("b");
7424
- setCheck(false);
7425
- } else setCheck(inCheck(nb, "r"));
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
- }, 90);
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-pc.b{color:#1f2937;text-shadow:0 1px 0 rgba(255,255,255,.35)}
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 (!b.launched) {
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: () => focusStage(canvasRef), 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\u53D1\u7403" }),
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 (!b.launched) {
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>←/→ 或鼠标移动 · 空格发球</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>
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
- // 精确网格线(viewBox 1000×2000,轨道与方块单元格逐一对齐,杜绝背景纹理的亚像素漂移)。
30
+ // 精确网格(viewBox 1000×2000,与方块单元格逐一对齐,杜绝背景纹理的亚像素漂移):
31
+ // 每个空格画一个深色内嵌圆角坑(经典机台的"格子凹槽"),再叠亮色网格线,空格一眼可辨。
31
32
  const TETRIS_GRID = (() => {
32
- const lines = [];
33
+ const parts = [];
33
34
  let k = 0;
34
- for (let i = 1; i < 10; i += 1) lines.push(<line key={k++} x1={i * 100} y1={0} x2={i * 100} y2={2000} />);
35
- for (let j = 1; j < 20; j += 1) lines.push(<line key={k++} x1={0} y1={j * 100} x2={1000} y2={j * 100} />);
36
- return lines;
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:#60a5fa;stroke-opacity:.12;stroke-width:2}@media (max-width:680px){.dgame-tetris{width:100%;gap:8px}.dgame-tetris-board{width:min(100%,170px)}}
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
- setBoard(nb);
90
- setSel(null); setTargets([]); setLast([tr, tc]);
91
- setTurn("b");
92
- if (!hasLegalMoves(nb, "b")) { setOver("r"); setCheck(false); }
93
- else setCheck(inCheck(nb, "b"));
94
- }, [sel, pushKey]);
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
- setBoard(nb); setLast([tr, tc]); setTurn("r");
125
- if (!hasLegalMoves(nb, "r")) { setOver("b"); setCheck(false); }
126
- else setCheck(inCheck(nb, "r"));
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
- }, 90);
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-pc.b{color:#1f2937;text-shadow:0 1px 0 rgba(255,255,255,.35)}
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
  ` };
@@ -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.1",
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",