@scriptonita/chess-football-ui 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +326 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +329 -112
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -149,6 +149,7 @@ var Football = ({ className }) => {
|
|
|
149
149
|
var STATIC_I18N_KEYS = [
|
|
150
150
|
"actionPointsAriaLabel",
|
|
151
151
|
"actionPointsShort",
|
|
152
|
+
"ballHolder",
|
|
152
153
|
"endTurn",
|
|
153
154
|
"endTurnConfirm",
|
|
154
155
|
"endTurnConfirmDescription",
|
|
@@ -171,6 +172,13 @@ var STATIC_I18N_KEYS = [
|
|
|
171
172
|
"selectedPiece.atSquare",
|
|
172
173
|
"selectedPiece.empty",
|
|
173
174
|
"selectedPiece.hasBall",
|
|
175
|
+
"shortcuts.arrows",
|
|
176
|
+
"shortcuts.buttonLabel",
|
|
177
|
+
"shortcuts.cancel",
|
|
178
|
+
"shortcuts.move",
|
|
179
|
+
"shortcuts.pass",
|
|
180
|
+
"shortcuts.select",
|
|
181
|
+
"shortcuts.title",
|
|
174
182
|
"teamBlack",
|
|
175
183
|
"teamWhite",
|
|
176
184
|
"waitingRival",
|
|
@@ -202,6 +210,7 @@ var ROWS = 12;
|
|
|
202
210
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
203
211
|
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
204
212
|
const t = useGameT();
|
|
213
|
+
const prefersReducedMotion = framerMotion.useReducedMotion();
|
|
205
214
|
const {
|
|
206
215
|
boardState,
|
|
207
216
|
selectedPieceId,
|
|
@@ -212,11 +221,62 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
212
221
|
} = chunkLTFNSTAQ_cjs.useGameStore();
|
|
213
222
|
const [cursor, setCursor] = React.useState(null);
|
|
214
223
|
const [disambiguateAt, setDisambiguateAt] = React.useState(null);
|
|
224
|
+
const [shortcutsOpen, setShortcutsOpen] = React.useState(false);
|
|
225
|
+
const shortcutsRef = React.useRef(null);
|
|
226
|
+
React.useEffect(() => {
|
|
227
|
+
if (!shortcutsOpen) return;
|
|
228
|
+
const onPointerDown = (e) => {
|
|
229
|
+
if (shortcutsRef.current && !shortcutsRef.current.contains(e.target)) {
|
|
230
|
+
setShortcutsOpen(false);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
234
|
+
return () => document.removeEventListener("mousedown", onPointerDown);
|
|
235
|
+
}, [shortcutsOpen]);
|
|
236
|
+
const [invalidClickAt, setInvalidClickAt] = React.useState(null);
|
|
237
|
+
const invalidClickNonceRef = React.useRef(0);
|
|
238
|
+
const triggerInvalidClickShake = (x, y) => {
|
|
239
|
+
invalidClickNonceRef.current += 1;
|
|
240
|
+
setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
|
|
241
|
+
};
|
|
242
|
+
const [hoveredPassAt, setHoveredPassAt] = React.useState(null);
|
|
243
|
+
React.useEffect(() => {
|
|
244
|
+
if (!invalidClickAt) return;
|
|
245
|
+
const id = setTimeout(() => setInvalidClickAt(null), 100);
|
|
246
|
+
return () => clearTimeout(id);
|
|
247
|
+
}, [invalidClickAt]);
|
|
215
248
|
const prevBallRef = React.useRef(boardState?.ball);
|
|
216
249
|
const prevBall = prevBallRef.current;
|
|
217
250
|
React.useEffect(() => {
|
|
218
251
|
prevBallRef.current = boardState?.ball;
|
|
219
252
|
});
|
|
253
|
+
const prevTurnRef = React.useRef(boardState?.turn);
|
|
254
|
+
const [replay, setReplay] = React.useState(null);
|
|
255
|
+
React.useEffect(() => {
|
|
256
|
+
const prevTurn = prevTurnRef.current;
|
|
257
|
+
prevTurnRef.current = boardState?.turn;
|
|
258
|
+
if (!boardState || prevTurn === boardState.turn) return;
|
|
259
|
+
if (boardState.turn !== userSide) {
|
|
260
|
+
setReplay(null);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (prefersReducedMotion || userSide === null) return;
|
|
264
|
+
const lastEntry = boardState.moveHistory.at(-1);
|
|
265
|
+
if (!lastEntry) return;
|
|
266
|
+
const steps = boardState.moveHistory.filter(
|
|
267
|
+
(m) => m.turnNumber === lastEntry.turnNumber && m.pieceSide !== userSide
|
|
268
|
+
);
|
|
269
|
+
if (steps.length > 1) setReplay({ steps, index: 0 });
|
|
270
|
+
}, [boardState, userSide, prefersReducedMotion]);
|
|
271
|
+
React.useEffect(() => {
|
|
272
|
+
if (!replay) return;
|
|
273
|
+
if (replay.index >= replay.steps.length - 1) {
|
|
274
|
+
const id2 = setTimeout(() => setReplay(null), 350);
|
|
275
|
+
return () => clearTimeout(id2);
|
|
276
|
+
}
|
|
277
|
+
const id = setTimeout(() => setReplay((r) => r ? { ...r, index: r.index + 1 } : null), 350);
|
|
278
|
+
return () => clearTimeout(id);
|
|
279
|
+
}, [replay]);
|
|
220
280
|
const validMoves = React.useMemo(() => {
|
|
221
281
|
if (!boardState) return [];
|
|
222
282
|
const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
@@ -234,6 +294,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
234
294
|
return [];
|
|
235
295
|
}, [boardState, selectedPieceId]);
|
|
236
296
|
if (!boardState) return null;
|
|
297
|
+
const ballHolderPiece = boardState.pieces.find((p) => p.id === boardState.ball.holderId) ?? null;
|
|
237
298
|
const lastMove = boardState.lastMove;
|
|
238
299
|
const ball = boardState.ball;
|
|
239
300
|
const ballMoved = !!prevBall && (prevBall.pos.x !== ball.pos.x || prevBall.pos.y !== ball.pos.y);
|
|
@@ -254,7 +315,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
254
315
|
}
|
|
255
316
|
const ballCarrier = ball.holderId ? boardState.pieces.find((p) => p.id === ball.holderId) ?? null : null;
|
|
256
317
|
const isOffsideRisk = !!ballCarrier && ballCarrier.type !== "king" && userSide !== null && ballCarrier.side === userSide && boardState.turn === userSide && chessFootballEngine.isInEnemyArea(ballCarrier.pos, ballCarrier.side);
|
|
318
|
+
const passCarrier = boardState.pieces.find((p) => p.id === selectedPieceId && boardState.ball.holderId === p.id) ?? null;
|
|
319
|
+
const trajectoryPath = hoveredPassAt && passCarrier ? passCarrier.type === "knight" ? [hoveredPassAt] : chessFootballEngine.getPath(passCarrier.pos, hoveredPassAt) : null;
|
|
320
|
+
const trajectoryIntercepted = !!(trajectoryPath && passCarrier && trajectoryPath.some((sq) => boardState.pieces.some((p) => p.pos.x === sq.x && p.pos.y === sq.y && p.side !== passCarrier.side)));
|
|
257
321
|
const handleSquareClick = (x, y) => {
|
|
322
|
+
if (replay) return;
|
|
258
323
|
setCursor(null);
|
|
259
324
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
260
325
|
const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
|
|
@@ -265,15 +330,21 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
265
330
|
if (isValidMove && selectedPieceId) {
|
|
266
331
|
movePiece(selectedPieceId, { x, y });
|
|
267
332
|
setDisambiguateAt(null);
|
|
333
|
+
setInvalidClickAt(null);
|
|
268
334
|
} else if (isValidPass) {
|
|
269
335
|
passBall({ x, y });
|
|
270
336
|
setDisambiguateAt(null);
|
|
337
|
+
setInvalidClickAt(null);
|
|
271
338
|
} else {
|
|
339
|
+
if (selectedPieceId) {
|
|
340
|
+
triggerInvalidClickShake(x, y);
|
|
341
|
+
}
|
|
272
342
|
setDisambiguateAt(null);
|
|
273
343
|
}
|
|
274
344
|
};
|
|
275
345
|
const handlePieceClick = (pieceId, x, y, e) => {
|
|
276
346
|
e.stopPropagation();
|
|
347
|
+
if (replay) return;
|
|
277
348
|
const pieceAt = boardState.pieces.find((p) => p.id === pieceId);
|
|
278
349
|
if (!pieceAt) return;
|
|
279
350
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
@@ -287,6 +358,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
287
358
|
setSelectedPieceId(pieceAt.id);
|
|
288
359
|
}
|
|
289
360
|
setDisambiguateAt(null);
|
|
361
|
+
setInvalidClickAt(null);
|
|
362
|
+
} else if (selectedPieceId) {
|
|
363
|
+
triggerInvalidClickShake(x, y);
|
|
364
|
+
setDisambiguateAt(null);
|
|
290
365
|
}
|
|
291
366
|
};
|
|
292
367
|
const defaultCursor = () => {
|
|
@@ -303,7 +378,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
303
378
|
}
|
|
304
379
|
};
|
|
305
380
|
const handleKeyDown = (e) => {
|
|
306
|
-
if (!keyboardNav) return;
|
|
381
|
+
if (!keyboardNav || replay) return;
|
|
307
382
|
const step = (dx, dy) => {
|
|
308
383
|
e.preventDefault();
|
|
309
384
|
setCursor((c) => {
|
|
@@ -347,7 +422,9 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
347
422
|
}
|
|
348
423
|
break;
|
|
349
424
|
case "Escape":
|
|
350
|
-
if (
|
|
425
|
+
if (shortcutsOpen) {
|
|
426
|
+
setShortcutsOpen(false);
|
|
427
|
+
} else if (disambiguateAt) {
|
|
351
428
|
setDisambiguateAt(null);
|
|
352
429
|
} else {
|
|
353
430
|
setSelectedPieceId(null);
|
|
@@ -365,8 +442,9 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
365
442
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
366
443
|
const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
|
|
367
444
|
const isAmbiguous = isValidMove && isValidPass;
|
|
368
|
-
const
|
|
369
|
-
const
|
|
445
|
+
const displayedLastMove = replay ? replay.steps[replay.index] : lastMove;
|
|
446
|
+
const isLastMoveOrigin = displayedLastMove?.from && displayedLastMove.from.x === x && displayedLastMove.from.y === y;
|
|
447
|
+
const isLastMoveDest = displayedLastMove?.to && displayedLastMove.to.x === x && displayedLastMove.to.y === y;
|
|
370
448
|
const isLastMove = isLastMoveOrigin || isLastMoveDest;
|
|
371
449
|
const isEnemyGoalArea = userSide !== null && (x >= 2 && x <= 6) && (userSide === "white" ? y >= 10 && y <= 11 : y >= 0 && y <= 1);
|
|
372
450
|
const pieceAt = boardState.pieces.find((p) => p.pos.x === x && p.pos.y === y);
|
|
@@ -377,6 +455,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
377
455
|
"div",
|
|
378
456
|
{
|
|
379
457
|
onClick: () => handleSquareClick(x, y),
|
|
458
|
+
onMouseEnter: () => {
|
|
459
|
+
if (isValidPass) setHoveredPassAt({ x, y });
|
|
460
|
+
},
|
|
461
|
+
onMouseLeave: () => setHoveredPassAt(null),
|
|
380
462
|
className: cn(
|
|
381
463
|
"relative aspect-square w-full flex items-center justify-center cursor-pointer",
|
|
382
464
|
isEven ? "bg-field-green-1" : "bg-field-green-2",
|
|
@@ -449,7 +531,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
449
531
|
}
|
|
450
532
|
return squares;
|
|
451
533
|
};
|
|
452
|
-
const board = /* @__PURE__ */ jsxRuntime.
|
|
534
|
+
const board = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
453
535
|
"div",
|
|
454
536
|
{
|
|
455
537
|
className: cn(
|
|
@@ -466,104 +548,220 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
466
548
|
"aria-label": "Game board \u2014 arrow keys to move the cursor, Enter to act",
|
|
467
549
|
onKeyDown: handleKeyDown
|
|
468
550
|
} : {},
|
|
469
|
-
children:
|
|
470
|
-
/* @__PURE__ */ jsxRuntime.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
viewBox: "0 0 9 12",
|
|
475
|
-
preserveAspectRatio: "none",
|
|
476
|
-
className: "absolute inset-0 w-full h-full pointer-events-none",
|
|
477
|
-
"aria-hidden": "true",
|
|
478
|
-
children: [
|
|
479
|
-
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
480
|
-
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
481
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
|
|
482
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
483
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
|
|
484
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
|
|
485
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
|
|
486
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 3.086,2 A 1.5,1.5 0 0 0 5.914,2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
487
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 3.086,10 A 1.5,1.5 0 0 1 5.914,10", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" })
|
|
488
|
-
]
|
|
489
|
-
}
|
|
490
|
-
),
|
|
491
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
|
|
492
|
-
/* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
|
|
493
|
-
const isPickupCarrier = pickupCarrierId === piece.id;
|
|
494
|
-
const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
|
|
495
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
496
|
-
framerMotion.motion.div,
|
|
551
|
+
children: [
|
|
552
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "hidden md:flex items-center justify-between gap-2 px-2.5 py-1", children: [
|
|
553
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1.5 min-h-[1.75rem]", "aria-live": "polite", children: ballHolderPiece && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
554
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
555
|
+
"div",
|
|
497
556
|
{
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
557
|
+
style: ballHolderPiece.side === "white" ? WHITE_PIECE_STYLE : BLACK_PIECE_STYLE,
|
|
558
|
+
className: "relative w-7 h-7 flex items-center justify-center rounded-full shrink-0",
|
|
559
|
+
children: [
|
|
560
|
+
/* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: ballHolderPiece.type, side: ballHolderPiece.side }),
|
|
561
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -bottom-1 -right-1 w-3.5 h-3.5 flex items-center justify-center rounded-full bg-bg-secondary border border-border-subtle", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) }) })
|
|
562
|
+
]
|
|
563
|
+
}
|
|
564
|
+
),
|
|
565
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-inter text-[11px] text-fg-secondary", children: [
|
|
566
|
+
t("ballHolder"),
|
|
567
|
+
": ",
|
|
568
|
+
t(`pieces.${ballHolderPiece.type}`)
|
|
569
|
+
] })
|
|
570
|
+
] }) }),
|
|
571
|
+
keyboardNav && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
572
|
+
"div",
|
|
573
|
+
{
|
|
574
|
+
ref: shortcutsRef,
|
|
575
|
+
className: "relative",
|
|
576
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
577
|
+
children: [
|
|
578
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
579
|
+
"button",
|
|
513
580
|
{
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
581
|
+
type: "button",
|
|
582
|
+
onClick: (e) => {
|
|
583
|
+
e.stopPropagation();
|
|
584
|
+
setShortcutsOpen((o) => !o);
|
|
585
|
+
},
|
|
586
|
+
"aria-label": t("shortcuts.buttonLabel"),
|
|
587
|
+
"aria-expanded": shortcutsOpen,
|
|
588
|
+
className: "w-8 h-8 flex items-center justify-center rounded-full bg-bg-secondary/80 border border-border-subtle text-fg-muted hover:text-fg-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
|
|
589
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
|
|
590
|
+
}
|
|
591
|
+
),
|
|
592
|
+
shortcutsOpen && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
593
|
+
"div",
|
|
594
|
+
{
|
|
595
|
+
className: "absolute top-full right-0 mt-1 z-50 w-max max-w-[220px] bg-bg-secondary border border-border-subtle rounded-md shadow-xl p-2.5 pointer-events-auto",
|
|
596
|
+
onClick: (e) => e.stopPropagation(),
|
|
597
|
+
children: [
|
|
598
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-inter text-[11px] font-semibold text-fg-primary mb-1.5", children: t("shortcuts.title") }),
|
|
599
|
+
/* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "font-mono text-[10px] text-fg-secondary leading-relaxed", children: [
|
|
600
|
+
/* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
|
|
601
|
+
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "\u2191\u2193\u2190\u2192" }),
|
|
602
|
+
" ",
|
|
603
|
+
t("shortcuts.arrows")
|
|
604
|
+
] }),
|
|
605
|
+
/* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
|
|
606
|
+
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "Enter" }),
|
|
607
|
+
" ",
|
|
608
|
+
t("shortcuts.select")
|
|
609
|
+
] }),
|
|
610
|
+
/* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
|
|
611
|
+
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "M" }),
|
|
612
|
+
" ",
|
|
613
|
+
t("shortcuts.move")
|
|
614
|
+
] }),
|
|
615
|
+
/* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
|
|
616
|
+
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "P" }),
|
|
617
|
+
" ",
|
|
618
|
+
t("shortcuts.pass")
|
|
619
|
+
] }),
|
|
620
|
+
/* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
|
|
621
|
+
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "Esc" }),
|
|
622
|
+
" ",
|
|
623
|
+
t("shortcuts.cancel")
|
|
624
|
+
] })
|
|
625
|
+
] })
|
|
626
|
+
]
|
|
518
627
|
}
|
|
519
628
|
)
|
|
520
|
-
|
|
521
|
-
piece.id
|
|
522
|
-
);
|
|
523
|
-
}) }),
|
|
524
|
-
(() => {
|
|
525
|
-
const targetX = ball.pos.x / COLS * 100;
|
|
526
|
-
const targetY = (ROWS - 1 - ball.pos.y) / ROWS * 100;
|
|
527
|
-
let animate;
|
|
528
|
-
let transition;
|
|
529
|
-
if (isPickupOnMove && prevBall) {
|
|
530
|
-
const prevX = prevBall.pos.x / COLS * 100;
|
|
531
|
-
const prevY = (ROWS - 1 - prevBall.pos.y) / ROWS * 100;
|
|
532
|
-
const pulseStart = Math.max(0.05, pickupProportion - 0.06);
|
|
533
|
-
animate = {
|
|
534
|
-
left: [`${prevX}%`, `${prevX}%`, `${prevX}%`, `${targetX}%`],
|
|
535
|
-
top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
|
|
536
|
-
scale: [1, 1, 1.35, 1]
|
|
537
|
-
};
|
|
538
|
-
transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
|
|
539
|
-
} else {
|
|
540
|
-
animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
|
|
541
|
-
transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
|
|
629
|
+
]
|
|
542
630
|
}
|
|
543
|
-
|
|
631
|
+
)
|
|
632
|
+
] }),
|
|
633
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
|
|
634
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
|
|
635
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
636
|
+
"svg",
|
|
637
|
+
{
|
|
638
|
+
viewBox: "0 0 9 12",
|
|
639
|
+
preserveAspectRatio: "none",
|
|
640
|
+
className: "absolute inset-0 w-full h-full pointer-events-none",
|
|
641
|
+
"aria-hidden": "true",
|
|
642
|
+
children: [
|
|
643
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
644
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
645
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
|
|
646
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
647
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
|
|
648
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
|
|
649
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
|
|
650
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 3.086,2 A 1.5,1.5 0 0 0 5.914,2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
651
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 3.086,10 A 1.5,1.5 0 0 1 5.914,10", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
652
|
+
hoveredPassAt && passCarrier && /* @__PURE__ */ jsxRuntime.jsx(
|
|
653
|
+
"line",
|
|
654
|
+
{
|
|
655
|
+
"data-testid": "pass-trajectory-line",
|
|
656
|
+
x1: passCarrier.pos.x + 0.5,
|
|
657
|
+
y1: ROWS - 1 - passCarrier.pos.y + 0.5,
|
|
658
|
+
x2: hoveredPassAt.x + 0.5,
|
|
659
|
+
y2: ROWS - 1 - hoveredPassAt.y + 0.5,
|
|
660
|
+
stroke: trajectoryIntercepted ? "var(--danger)" : "var(--pass-highlight)",
|
|
661
|
+
strokeWidth: "0.08",
|
|
662
|
+
strokeDasharray: "0.15 0.1",
|
|
663
|
+
strokeLinecap: "round"
|
|
664
|
+
}
|
|
665
|
+
)
|
|
666
|
+
]
|
|
667
|
+
}
|
|
668
|
+
),
|
|
669
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
|
|
670
|
+
/* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
|
|
671
|
+
const isPickupCarrier = pickupCarrierId === piece.id;
|
|
672
|
+
const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
|
|
673
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
674
|
+
framerMotion.motion.div,
|
|
675
|
+
{
|
|
676
|
+
layout: true,
|
|
677
|
+
initial: false,
|
|
678
|
+
animate: {
|
|
679
|
+
left: `${piece.pos.x / COLS * 100}%`,
|
|
680
|
+
top: `${(ROWS - 1 - piece.pos.y) / ROWS * 100}%`
|
|
681
|
+
},
|
|
682
|
+
transition: pieceTransition,
|
|
683
|
+
style: {
|
|
684
|
+
position: "absolute",
|
|
685
|
+
width: `${100 / COLS}%`,
|
|
686
|
+
height: `${100 / ROWS}%`
|
|
687
|
+
},
|
|
688
|
+
className: "flex items-center justify-center pointer-events-auto",
|
|
689
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
690
|
+
GamePiece,
|
|
691
|
+
{
|
|
692
|
+
piece,
|
|
693
|
+
isSelected: selectedPieceId === piece.id,
|
|
694
|
+
hasBall: boardState.ball.holderId === piece.id,
|
|
695
|
+
onClick: (e) => handlePieceClick(piece.id, piece.pos.x, piece.pos.y, e)
|
|
696
|
+
}
|
|
697
|
+
)
|
|
698
|
+
},
|
|
699
|
+
piece.id
|
|
700
|
+
);
|
|
701
|
+
}) }),
|
|
702
|
+
(() => {
|
|
703
|
+
const targetX = ball.pos.x / COLS * 100;
|
|
704
|
+
const targetY = (ROWS - 1 - ball.pos.y) / ROWS * 100;
|
|
705
|
+
let animate;
|
|
706
|
+
let transition;
|
|
707
|
+
if (isPickupOnMove && prevBall) {
|
|
708
|
+
const prevX = prevBall.pos.x / COLS * 100;
|
|
709
|
+
const prevY = (ROWS - 1 - prevBall.pos.y) / ROWS * 100;
|
|
710
|
+
const pulseStart = Math.max(0.05, pickupProportion - 0.06);
|
|
711
|
+
animate = {
|
|
712
|
+
left: [`${prevX}%`, `${prevX}%`, `${prevX}%`, `${targetX}%`],
|
|
713
|
+
top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
|
|
714
|
+
scale: [1, 1, 1.35, 1]
|
|
715
|
+
};
|
|
716
|
+
transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
|
|
717
|
+
} else {
|
|
718
|
+
animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
|
|
719
|
+
transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
|
|
720
|
+
}
|
|
721
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
722
|
+
framerMotion.motion.div,
|
|
723
|
+
{
|
|
724
|
+
initial: false,
|
|
725
|
+
animate,
|
|
726
|
+
transition,
|
|
727
|
+
style: {
|
|
728
|
+
position: "absolute",
|
|
729
|
+
width: `${100 / COLS}%`,
|
|
730
|
+
height: `${100 / ROWS}%`,
|
|
731
|
+
zIndex: 50
|
|
732
|
+
},
|
|
733
|
+
className: "flex items-center justify-center pointer-events-none",
|
|
734
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[60%] h-[60%]", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) })
|
|
735
|
+
},
|
|
736
|
+
"ball"
|
|
737
|
+
);
|
|
738
|
+
})(),
|
|
739
|
+
invalidClickAt && /* @__PURE__ */ jsxRuntime.jsx(
|
|
544
740
|
framerMotion.motion.div,
|
|
545
741
|
{
|
|
742
|
+
"data-testid": "invalid-action-shake",
|
|
743
|
+
"aria-hidden": "true",
|
|
546
744
|
initial: false,
|
|
547
|
-
animate,
|
|
548
|
-
transition,
|
|
745
|
+
animate: prefersReducedMotion ? { opacity: [0.9, 0] } : { x: [0, -3, 3, -3, 3, 0] },
|
|
746
|
+
transition: { duration: 0.08 },
|
|
549
747
|
style: {
|
|
550
748
|
position: "absolute",
|
|
749
|
+
left: `${invalidClickAt.x / COLS * 100}%`,
|
|
750
|
+
top: `${(ROWS - 1 - invalidClickAt.y) / ROWS * 100}%`,
|
|
551
751
|
width: `${100 / COLS}%`,
|
|
552
|
-
height: `${100 / ROWS}
|
|
553
|
-
zIndex: 50
|
|
752
|
+
height: `${100 / ROWS}%`
|
|
554
753
|
},
|
|
555
|
-
className: "
|
|
556
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[60%] h-[60%]", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) })
|
|
754
|
+
className: "ring-2 ring-inset ring-danger/70 rounded-sm"
|
|
557
755
|
},
|
|
558
|
-
|
|
559
|
-
)
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
756
|
+
invalidClickAt.nonce
|
|
757
|
+
)
|
|
758
|
+
] }),
|
|
759
|
+
isOffsideRisk && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 px-3 py-1.5 bg-warning/20 border border-warning/50 rounded-full font-inter text-xs font-semibold text-warning pointer-events-none whitespace-nowrap backdrop-blur-sm", children: [
|
|
760
|
+
"\u26A0 ",
|
|
761
|
+
t("offsideWarning")
|
|
762
|
+
] })
|
|
565
763
|
] })
|
|
566
|
-
]
|
|
764
|
+
]
|
|
567
765
|
}
|
|
568
766
|
);
|
|
569
767
|
if (!showCoordinates) return board;
|
|
@@ -608,31 +806,50 @@ function AvatarFallback({ className, children }) {
|
|
|
608
806
|
}
|
|
609
807
|
function ActionPoints({ total, remaining, size = 20, className, kingMustRelease }) {
|
|
610
808
|
const t = useGameT();
|
|
809
|
+
const prevRemainingRef = React.useRef(remaining);
|
|
810
|
+
const [spentRange, setSpentRange] = React.useState(null);
|
|
811
|
+
React.useEffect(() => {
|
|
812
|
+
const prev = prevRemainingRef.current;
|
|
813
|
+
prevRemainingRef.current = remaining;
|
|
814
|
+
if (remaining < prev) {
|
|
815
|
+
setSpentRange([remaining, prev - 1]);
|
|
816
|
+
const id = setTimeout(() => setSpentRange(null), 300);
|
|
817
|
+
return () => clearTimeout(id);
|
|
818
|
+
}
|
|
819
|
+
setSpentRange(null);
|
|
820
|
+
}, [remaining]);
|
|
611
821
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex items-center gap-1.5", className), "aria-label": t("actionPointsAriaLabel", { remaining, total }), children: Array.from({ length: total }).map((_, i) => {
|
|
612
822
|
const isActive = i < remaining;
|
|
613
823
|
const showCrown = kingMustRelease && i === total - 1;
|
|
614
|
-
|
|
615
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
616
|
-
lucideReact.Crown,
|
|
617
|
-
{
|
|
618
|
-
size,
|
|
619
|
-
strokeWidth: 2,
|
|
620
|
-
className: "text-yellow-400 fill-yellow-400 transition-colors duration-150",
|
|
621
|
-
"aria-hidden": "true"
|
|
622
|
-
},
|
|
623
|
-
i
|
|
624
|
-
);
|
|
625
|
-
}
|
|
824
|
+
const justSpent = !!spentRange && i >= spentRange[0] && i <= spentRange[1];
|
|
626
825
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
627
|
-
|
|
826
|
+
framerMotion.motion.span,
|
|
628
827
|
{
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
className:
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
828
|
+
"data-testid": `ap-pip-${i}`,
|
|
829
|
+
"data-spent": justSpent || void 0,
|
|
830
|
+
className: "inline-flex",
|
|
831
|
+
animate: justSpent ? { scale: [1.5, 1] } : void 0,
|
|
832
|
+
transition: { duration: 0.3 },
|
|
833
|
+
children: showCrown ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
834
|
+
lucideReact.Crown,
|
|
835
|
+
{
|
|
836
|
+
size,
|
|
837
|
+
strokeWidth: 2,
|
|
838
|
+
className: "text-yellow-400 fill-yellow-400 transition-colors duration-150",
|
|
839
|
+
"aria-hidden": "true"
|
|
840
|
+
}
|
|
841
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
842
|
+
lucideReact.Zap,
|
|
843
|
+
{
|
|
844
|
+
size,
|
|
845
|
+
strokeWidth: 2,
|
|
846
|
+
className: cn(
|
|
847
|
+
"transition-colors duration-150",
|
|
848
|
+
isActive ? "text-accent-green fill-accent-green" : "text-fg-muted fill-none"
|
|
849
|
+
),
|
|
850
|
+
"aria-hidden": "true"
|
|
851
|
+
}
|
|
852
|
+
)
|
|
636
853
|
},
|
|
637
854
|
i
|
|
638
855
|
);
|