@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.d.cts
CHANGED
|
@@ -159,7 +159,7 @@ type GameTranslator = (key: string, values?: Record<string, unknown>) => string;
|
|
|
159
159
|
* grep -rhoE "t\('[a-zA-Z0-9_.]+'" src/components | sed -E "s/t\('//;s/'//" | sort -u
|
|
160
160
|
* (the dynamic `pieces.*` / `eventToast.*` families are derived separately below).
|
|
161
161
|
*/
|
|
162
|
-
declare const STATIC_I18N_KEYS: readonly ["actionPointsAriaLabel", "actionPointsShort", "endTurn", "endTurnConfirm", "endTurnConfirmDescription", "endTurnConfirmYes", "endTurnKeepPlaying", "history.empty", "history.goal", "history.interception", "history.move", "history.offside", "history.pass", "history.tackle", "history.title", "history.turn", "history.viewAll", "move", "offsideWarning", "pass", "selectedPiece.alreadyMoved", "selectedPiece.atSquare", "selectedPiece.empty", "selectedPiece.hasBall", "teamBlack", "teamWhite", "waitingRival", "yourTurn"];
|
|
162
|
+
declare const STATIC_I18N_KEYS: readonly ["actionPointsAriaLabel", "actionPointsShort", "ballHolder", "endTurn", "endTurnConfirm", "endTurnConfirmDescription", "endTurnConfirmYes", "endTurnKeepPlaying", "history.empty", "history.goal", "history.interception", "history.move", "history.offside", "history.pass", "history.tackle", "history.title", "history.turn", "history.viewAll", "move", "offsideWarning", "pass", "selectedPiece.alreadyMoved", "selectedPiece.atSquare", "selectedPiece.empty", "selectedPiece.hasBall", "shortcuts.arrows", "shortcuts.buttonLabel", "shortcuts.cancel", "shortcuts.move", "shortcuts.pass", "shortcuts.select", "shortcuts.title", "teamBlack", "teamWhite", "waitingRival", "yourTurn"];
|
|
163
163
|
/** `pieces.*` — one key per short and long piece label the engine exposes (move-history, selected-piece-detail). */
|
|
164
164
|
type PieceI18nKey = `pieces.${PieceShortKey | PieceLongKey}`;
|
|
165
165
|
/** `eventToast.*` — one key per toastable event (see `ToastEvent` in `event-toast.tsx`). */
|
package/dist/index.d.ts
CHANGED
|
@@ -159,7 +159,7 @@ type GameTranslator = (key: string, values?: Record<string, unknown>) => string;
|
|
|
159
159
|
* grep -rhoE "t\('[a-zA-Z0-9_.]+'" src/components | sed -E "s/t\('//;s/'//" | sort -u
|
|
160
160
|
* (the dynamic `pieces.*` / `eventToast.*` families are derived separately below).
|
|
161
161
|
*/
|
|
162
|
-
declare const STATIC_I18N_KEYS: readonly ["actionPointsAriaLabel", "actionPointsShort", "endTurn", "endTurnConfirm", "endTurnConfirmDescription", "endTurnConfirmYes", "endTurnKeepPlaying", "history.empty", "history.goal", "history.interception", "history.move", "history.offside", "history.pass", "history.tackle", "history.title", "history.turn", "history.viewAll", "move", "offsideWarning", "pass", "selectedPiece.alreadyMoved", "selectedPiece.atSquare", "selectedPiece.empty", "selectedPiece.hasBall", "teamBlack", "teamWhite", "waitingRival", "yourTurn"];
|
|
162
|
+
declare const STATIC_I18N_KEYS: readonly ["actionPointsAriaLabel", "actionPointsShort", "ballHolder", "endTurn", "endTurnConfirm", "endTurnConfirmDescription", "endTurnConfirmYes", "endTurnKeepPlaying", "history.empty", "history.goal", "history.interception", "history.move", "history.offside", "history.pass", "history.tackle", "history.title", "history.turn", "history.viewAll", "move", "offsideWarning", "pass", "selectedPiece.alreadyMoved", "selectedPiece.atSquare", "selectedPiece.empty", "selectedPiece.hasBall", "shortcuts.arrows", "shortcuts.buttonLabel", "shortcuts.cancel", "shortcuts.move", "shortcuts.pass", "shortcuts.select", "shortcuts.title", "teamBlack", "teamWhite", "waitingRival", "yourTurn"];
|
|
163
163
|
/** `pieces.*` — one key per short and long piece label the engine exposes (move-history, selected-piece-detail). */
|
|
164
164
|
type PieceI18nKey = `pieces.${PieceShortKey | PieceLongKey}`;
|
|
165
165
|
/** `eventToast.*` — one key per toastable event (see `ToastEvent` in `event-toast.tsx`). */
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useGameStore, getInitialBoardState } from './chunk-N24LGLLO.js';
|
|
2
|
-
import { SHORT_KEY, LONG_KEY, getValidMoves, getValidPasses, isInEnemyArea, RANK_LABELS, FILE_LABELS, squareName } from '@scriptonita/chess-football-engine';
|
|
2
|
+
import { SHORT_KEY, LONG_KEY, getValidMoves, getValidPasses, isInEnemyArea, getPath, RANK_LABELS, FILE_LABELS, squareName } from '@scriptonita/chess-football-engine';
|
|
3
3
|
import { clsx } from 'clsx';
|
|
4
4
|
import { twMerge } from 'tailwind-merge';
|
|
5
|
-
import { MotionConfig, AnimatePresence, motion } from 'framer-motion';
|
|
6
|
-
import { Crown, Zap, Flag, ShieldQuestion, ChessKnight, ChessBishop, ChessRook, ChessQueen, ChessKing, X, Move } from 'lucide-react';
|
|
5
|
+
import { MotionConfig, useReducedMotion, AnimatePresence, motion } from 'framer-motion';
|
|
6
|
+
import { HelpCircle, Crown, Zap, Flag, ShieldQuestion, ChessKnight, ChessBishop, ChessRook, ChessQueen, ChessKing, X, Move } from 'lucide-react';
|
|
7
7
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
8
|
import * as React from 'react';
|
|
9
9
|
import { createContext, useId, useContext, useState, useRef, useEffect, useMemo } from 'react';
|
|
@@ -128,6 +128,7 @@ var Football = ({ className }) => {
|
|
|
128
128
|
var STATIC_I18N_KEYS = [
|
|
129
129
|
"actionPointsAriaLabel",
|
|
130
130
|
"actionPointsShort",
|
|
131
|
+
"ballHolder",
|
|
131
132
|
"endTurn",
|
|
132
133
|
"endTurnConfirm",
|
|
133
134
|
"endTurnConfirmDescription",
|
|
@@ -150,6 +151,13 @@ var STATIC_I18N_KEYS = [
|
|
|
150
151
|
"selectedPiece.atSquare",
|
|
151
152
|
"selectedPiece.empty",
|
|
152
153
|
"selectedPiece.hasBall",
|
|
154
|
+
"shortcuts.arrows",
|
|
155
|
+
"shortcuts.buttonLabel",
|
|
156
|
+
"shortcuts.cancel",
|
|
157
|
+
"shortcuts.move",
|
|
158
|
+
"shortcuts.pass",
|
|
159
|
+
"shortcuts.select",
|
|
160
|
+
"shortcuts.title",
|
|
153
161
|
"teamBlack",
|
|
154
162
|
"teamWhite",
|
|
155
163
|
"waitingRival",
|
|
@@ -181,6 +189,7 @@ var ROWS = 12;
|
|
|
181
189
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
182
190
|
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
183
191
|
const t = useGameT();
|
|
192
|
+
const prefersReducedMotion = useReducedMotion();
|
|
184
193
|
const {
|
|
185
194
|
boardState,
|
|
186
195
|
selectedPieceId,
|
|
@@ -191,11 +200,62 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
191
200
|
} = useGameStore();
|
|
192
201
|
const [cursor, setCursor] = useState(null);
|
|
193
202
|
const [disambiguateAt, setDisambiguateAt] = useState(null);
|
|
203
|
+
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
|
204
|
+
const shortcutsRef = useRef(null);
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!shortcutsOpen) return;
|
|
207
|
+
const onPointerDown = (e) => {
|
|
208
|
+
if (shortcutsRef.current && !shortcutsRef.current.contains(e.target)) {
|
|
209
|
+
setShortcutsOpen(false);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
213
|
+
return () => document.removeEventListener("mousedown", onPointerDown);
|
|
214
|
+
}, [shortcutsOpen]);
|
|
215
|
+
const [invalidClickAt, setInvalidClickAt] = useState(null);
|
|
216
|
+
const invalidClickNonceRef = useRef(0);
|
|
217
|
+
const triggerInvalidClickShake = (x, y) => {
|
|
218
|
+
invalidClickNonceRef.current += 1;
|
|
219
|
+
setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
|
|
220
|
+
};
|
|
221
|
+
const [hoveredPassAt, setHoveredPassAt] = useState(null);
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (!invalidClickAt) return;
|
|
224
|
+
const id = setTimeout(() => setInvalidClickAt(null), 100);
|
|
225
|
+
return () => clearTimeout(id);
|
|
226
|
+
}, [invalidClickAt]);
|
|
194
227
|
const prevBallRef = useRef(boardState?.ball);
|
|
195
228
|
const prevBall = prevBallRef.current;
|
|
196
229
|
useEffect(() => {
|
|
197
230
|
prevBallRef.current = boardState?.ball;
|
|
198
231
|
});
|
|
232
|
+
const prevTurnRef = useRef(boardState?.turn);
|
|
233
|
+
const [replay, setReplay] = useState(null);
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
const prevTurn = prevTurnRef.current;
|
|
236
|
+
prevTurnRef.current = boardState?.turn;
|
|
237
|
+
if (!boardState || prevTurn === boardState.turn) return;
|
|
238
|
+
if (boardState.turn !== userSide) {
|
|
239
|
+
setReplay(null);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (prefersReducedMotion || userSide === null) return;
|
|
243
|
+
const lastEntry = boardState.moveHistory.at(-1);
|
|
244
|
+
if (!lastEntry) return;
|
|
245
|
+
const steps = boardState.moveHistory.filter(
|
|
246
|
+
(m) => m.turnNumber === lastEntry.turnNumber && m.pieceSide !== userSide
|
|
247
|
+
);
|
|
248
|
+
if (steps.length > 1) setReplay({ steps, index: 0 });
|
|
249
|
+
}, [boardState, userSide, prefersReducedMotion]);
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (!replay) return;
|
|
252
|
+
if (replay.index >= replay.steps.length - 1) {
|
|
253
|
+
const id2 = setTimeout(() => setReplay(null), 350);
|
|
254
|
+
return () => clearTimeout(id2);
|
|
255
|
+
}
|
|
256
|
+
const id = setTimeout(() => setReplay((r) => r ? { ...r, index: r.index + 1 } : null), 350);
|
|
257
|
+
return () => clearTimeout(id);
|
|
258
|
+
}, [replay]);
|
|
199
259
|
const validMoves = useMemo(() => {
|
|
200
260
|
if (!boardState) return [];
|
|
201
261
|
const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
@@ -213,6 +273,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
213
273
|
return [];
|
|
214
274
|
}, [boardState, selectedPieceId]);
|
|
215
275
|
if (!boardState) return null;
|
|
276
|
+
const ballHolderPiece = boardState.pieces.find((p) => p.id === boardState.ball.holderId) ?? null;
|
|
216
277
|
const lastMove = boardState.lastMove;
|
|
217
278
|
const ball = boardState.ball;
|
|
218
279
|
const ballMoved = !!prevBall && (prevBall.pos.x !== ball.pos.x || prevBall.pos.y !== ball.pos.y);
|
|
@@ -233,7 +294,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
233
294
|
}
|
|
234
295
|
const ballCarrier = ball.holderId ? boardState.pieces.find((p) => p.id === ball.holderId) ?? null : null;
|
|
235
296
|
const isOffsideRisk = !!ballCarrier && ballCarrier.type !== "king" && userSide !== null && ballCarrier.side === userSide && boardState.turn === userSide && isInEnemyArea(ballCarrier.pos, ballCarrier.side);
|
|
297
|
+
const passCarrier = boardState.pieces.find((p) => p.id === selectedPieceId && boardState.ball.holderId === p.id) ?? null;
|
|
298
|
+
const trajectoryPath = hoveredPassAt && passCarrier ? passCarrier.type === "knight" ? [hoveredPassAt] : getPath(passCarrier.pos, hoveredPassAt) : null;
|
|
299
|
+
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)));
|
|
236
300
|
const handleSquareClick = (x, y) => {
|
|
301
|
+
if (replay) return;
|
|
237
302
|
setCursor(null);
|
|
238
303
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
239
304
|
const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
|
|
@@ -244,15 +309,21 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
244
309
|
if (isValidMove && selectedPieceId) {
|
|
245
310
|
movePiece(selectedPieceId, { x, y });
|
|
246
311
|
setDisambiguateAt(null);
|
|
312
|
+
setInvalidClickAt(null);
|
|
247
313
|
} else if (isValidPass) {
|
|
248
314
|
passBall({ x, y });
|
|
249
315
|
setDisambiguateAt(null);
|
|
316
|
+
setInvalidClickAt(null);
|
|
250
317
|
} else {
|
|
318
|
+
if (selectedPieceId) {
|
|
319
|
+
triggerInvalidClickShake(x, y);
|
|
320
|
+
}
|
|
251
321
|
setDisambiguateAt(null);
|
|
252
322
|
}
|
|
253
323
|
};
|
|
254
324
|
const handlePieceClick = (pieceId, x, y, e) => {
|
|
255
325
|
e.stopPropagation();
|
|
326
|
+
if (replay) return;
|
|
256
327
|
const pieceAt = boardState.pieces.find((p) => p.id === pieceId);
|
|
257
328
|
if (!pieceAt) return;
|
|
258
329
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
@@ -266,6 +337,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
266
337
|
setSelectedPieceId(pieceAt.id);
|
|
267
338
|
}
|
|
268
339
|
setDisambiguateAt(null);
|
|
340
|
+
setInvalidClickAt(null);
|
|
341
|
+
} else if (selectedPieceId) {
|
|
342
|
+
triggerInvalidClickShake(x, y);
|
|
343
|
+
setDisambiguateAt(null);
|
|
269
344
|
}
|
|
270
345
|
};
|
|
271
346
|
const defaultCursor = () => {
|
|
@@ -282,7 +357,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
282
357
|
}
|
|
283
358
|
};
|
|
284
359
|
const handleKeyDown = (e) => {
|
|
285
|
-
if (!keyboardNav) return;
|
|
360
|
+
if (!keyboardNav || replay) return;
|
|
286
361
|
const step = (dx, dy) => {
|
|
287
362
|
e.preventDefault();
|
|
288
363
|
setCursor((c) => {
|
|
@@ -326,7 +401,9 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
326
401
|
}
|
|
327
402
|
break;
|
|
328
403
|
case "Escape":
|
|
329
|
-
if (
|
|
404
|
+
if (shortcutsOpen) {
|
|
405
|
+
setShortcutsOpen(false);
|
|
406
|
+
} else if (disambiguateAt) {
|
|
330
407
|
setDisambiguateAt(null);
|
|
331
408
|
} else {
|
|
332
409
|
setSelectedPieceId(null);
|
|
@@ -344,8 +421,9 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
344
421
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
345
422
|
const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
|
|
346
423
|
const isAmbiguous = isValidMove && isValidPass;
|
|
347
|
-
const
|
|
348
|
-
const
|
|
424
|
+
const displayedLastMove = replay ? replay.steps[replay.index] : lastMove;
|
|
425
|
+
const isLastMoveOrigin = displayedLastMove?.from && displayedLastMove.from.x === x && displayedLastMove.from.y === y;
|
|
426
|
+
const isLastMoveDest = displayedLastMove?.to && displayedLastMove.to.x === x && displayedLastMove.to.y === y;
|
|
349
427
|
const isLastMove = isLastMoveOrigin || isLastMoveDest;
|
|
350
428
|
const isEnemyGoalArea = userSide !== null && (x >= 2 && x <= 6) && (userSide === "white" ? y >= 10 && y <= 11 : y >= 0 && y <= 1);
|
|
351
429
|
const pieceAt = boardState.pieces.find((p) => p.pos.x === x && p.pos.y === y);
|
|
@@ -356,6 +434,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
356
434
|
"div",
|
|
357
435
|
{
|
|
358
436
|
onClick: () => handleSquareClick(x, y),
|
|
437
|
+
onMouseEnter: () => {
|
|
438
|
+
if (isValidPass) setHoveredPassAt({ x, y });
|
|
439
|
+
},
|
|
440
|
+
onMouseLeave: () => setHoveredPassAt(null),
|
|
359
441
|
className: cn(
|
|
360
442
|
"relative aspect-square w-full flex items-center justify-center cursor-pointer",
|
|
361
443
|
isEven ? "bg-field-green-1" : "bg-field-green-2",
|
|
@@ -428,7 +510,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
428
510
|
}
|
|
429
511
|
return squares;
|
|
430
512
|
};
|
|
431
|
-
const board = /* @__PURE__ */
|
|
513
|
+
const board = /* @__PURE__ */ jsxs(
|
|
432
514
|
"div",
|
|
433
515
|
{
|
|
434
516
|
className: cn(
|
|
@@ -445,104 +527,220 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
445
527
|
"aria-label": "Game board \u2014 arrow keys to move the cursor, Enter to act",
|
|
446
528
|
onKeyDown: handleKeyDown
|
|
447
529
|
} : {},
|
|
448
|
-
children:
|
|
449
|
-
/* @__PURE__ */
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
viewBox: "0 0 9 12",
|
|
454
|
-
preserveAspectRatio: "none",
|
|
455
|
-
className: "absolute inset-0 w-full h-full pointer-events-none",
|
|
456
|
-
"aria-hidden": "true",
|
|
457
|
-
children: [
|
|
458
|
-
/* @__PURE__ */ jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
459
|
-
/* @__PURE__ */ jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
460
|
-
/* @__PURE__ */ jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
|
|
461
|
-
/* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
462
|
-
/* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
|
|
463
|
-
/* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
|
|
464
|
-
/* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
|
|
465
|
-
/* @__PURE__ */ 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" }),
|
|
466
|
-
/* @__PURE__ */ 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" })
|
|
467
|
-
]
|
|
468
|
-
}
|
|
469
|
-
),
|
|
470
|
-
/* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
|
|
471
|
-
/* @__PURE__ */ jsx(AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
|
|
472
|
-
const isPickupCarrier = pickupCarrierId === piece.id;
|
|
473
|
-
const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
|
|
474
|
-
return /* @__PURE__ */ jsx(
|
|
475
|
-
motion.div,
|
|
530
|
+
children: [
|
|
531
|
+
/* @__PURE__ */ jsxs("div", { className: "hidden md:flex items-center justify-between gap-2 px-2.5 py-1", children: [
|
|
532
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1.5 min-h-[1.75rem]", "aria-live": "polite", children: ballHolderPiece && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
533
|
+
/* @__PURE__ */ jsxs(
|
|
534
|
+
"div",
|
|
476
535
|
{
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
536
|
+
style: ballHolderPiece.side === "white" ? WHITE_PIECE_STYLE : BLACK_PIECE_STYLE,
|
|
537
|
+
className: "relative w-7 h-7 flex items-center justify-center rounded-full shrink-0",
|
|
538
|
+
children: [
|
|
539
|
+
/* @__PURE__ */ jsx(PieceIcon, { type: ballHolderPiece.type, side: ballHolderPiece.side }),
|
|
540
|
+
/* @__PURE__ */ 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__ */ jsx("div", { className: "w-2.5 h-2.5", children: /* @__PURE__ */ jsx(Football, {}) }) })
|
|
541
|
+
]
|
|
542
|
+
}
|
|
543
|
+
),
|
|
544
|
+
/* @__PURE__ */ jsxs("span", { className: "font-inter text-[11px] text-fg-secondary", children: [
|
|
545
|
+
t("ballHolder"),
|
|
546
|
+
": ",
|
|
547
|
+
t(`pieces.${ballHolderPiece.type}`)
|
|
548
|
+
] })
|
|
549
|
+
] }) }),
|
|
550
|
+
keyboardNav && /* @__PURE__ */ jsxs(
|
|
551
|
+
"div",
|
|
552
|
+
{
|
|
553
|
+
ref: shortcutsRef,
|
|
554
|
+
className: "relative",
|
|
555
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
556
|
+
children: [
|
|
557
|
+
/* @__PURE__ */ jsx(
|
|
558
|
+
"button",
|
|
492
559
|
{
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
560
|
+
type: "button",
|
|
561
|
+
onClick: (e) => {
|
|
562
|
+
e.stopPropagation();
|
|
563
|
+
setShortcutsOpen((o) => !o);
|
|
564
|
+
},
|
|
565
|
+
"aria-label": t("shortcuts.buttonLabel"),
|
|
566
|
+
"aria-expanded": shortcutsOpen,
|
|
567
|
+
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",
|
|
568
|
+
children: /* @__PURE__ */ jsx(HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
|
|
569
|
+
}
|
|
570
|
+
),
|
|
571
|
+
shortcutsOpen && /* @__PURE__ */ jsxs(
|
|
572
|
+
"div",
|
|
573
|
+
{
|
|
574
|
+
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",
|
|
575
|
+
onClick: (e) => e.stopPropagation(),
|
|
576
|
+
children: [
|
|
577
|
+
/* @__PURE__ */ jsx("p", { className: "font-inter text-[11px] font-semibold text-fg-primary mb-1.5", children: t("shortcuts.title") }),
|
|
578
|
+
/* @__PURE__ */ jsxs("ul", { className: "font-mono text-[10px] text-fg-secondary leading-relaxed", children: [
|
|
579
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
580
|
+
/* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "\u2191\u2193\u2190\u2192" }),
|
|
581
|
+
" ",
|
|
582
|
+
t("shortcuts.arrows")
|
|
583
|
+
] }),
|
|
584
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
585
|
+
/* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "Enter" }),
|
|
586
|
+
" ",
|
|
587
|
+
t("shortcuts.select")
|
|
588
|
+
] }),
|
|
589
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
590
|
+
/* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "M" }),
|
|
591
|
+
" ",
|
|
592
|
+
t("shortcuts.move")
|
|
593
|
+
] }),
|
|
594
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
595
|
+
/* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "P" }),
|
|
596
|
+
" ",
|
|
597
|
+
t("shortcuts.pass")
|
|
598
|
+
] }),
|
|
599
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
600
|
+
/* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "Esc" }),
|
|
601
|
+
" ",
|
|
602
|
+
t("shortcuts.cancel")
|
|
603
|
+
] })
|
|
604
|
+
] })
|
|
605
|
+
]
|
|
497
606
|
}
|
|
498
607
|
)
|
|
499
|
-
|
|
500
|
-
piece.id
|
|
501
|
-
);
|
|
502
|
-
}) }),
|
|
503
|
-
(() => {
|
|
504
|
-
const targetX = ball.pos.x / COLS * 100;
|
|
505
|
-
const targetY = (ROWS - 1 - ball.pos.y) / ROWS * 100;
|
|
506
|
-
let animate;
|
|
507
|
-
let transition;
|
|
508
|
-
if (isPickupOnMove && prevBall) {
|
|
509
|
-
const prevX = prevBall.pos.x / COLS * 100;
|
|
510
|
-
const prevY = (ROWS - 1 - prevBall.pos.y) / ROWS * 100;
|
|
511
|
-
const pulseStart = Math.max(0.05, pickupProportion - 0.06);
|
|
512
|
-
animate = {
|
|
513
|
-
left: [`${prevX}%`, `${prevX}%`, `${prevX}%`, `${targetX}%`],
|
|
514
|
-
top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
|
|
515
|
-
scale: [1, 1, 1.35, 1]
|
|
516
|
-
};
|
|
517
|
-
transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
|
|
518
|
-
} else {
|
|
519
|
-
animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
|
|
520
|
-
transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
|
|
608
|
+
]
|
|
521
609
|
}
|
|
522
|
-
|
|
610
|
+
)
|
|
611
|
+
] }),
|
|
612
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
|
|
613
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
|
|
614
|
+
/* @__PURE__ */ jsxs(
|
|
615
|
+
"svg",
|
|
616
|
+
{
|
|
617
|
+
viewBox: "0 0 9 12",
|
|
618
|
+
preserveAspectRatio: "none",
|
|
619
|
+
className: "absolute inset-0 w-full h-full pointer-events-none",
|
|
620
|
+
"aria-hidden": "true",
|
|
621
|
+
children: [
|
|
622
|
+
/* @__PURE__ */ jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
623
|
+
/* @__PURE__ */ jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
624
|
+
/* @__PURE__ */ jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
|
|
625
|
+
/* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
626
|
+
/* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
|
|
627
|
+
/* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
|
|
628
|
+
/* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
|
|
629
|
+
/* @__PURE__ */ 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" }),
|
|
630
|
+
/* @__PURE__ */ 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" }),
|
|
631
|
+
hoveredPassAt && passCarrier && /* @__PURE__ */ jsx(
|
|
632
|
+
"line",
|
|
633
|
+
{
|
|
634
|
+
"data-testid": "pass-trajectory-line",
|
|
635
|
+
x1: passCarrier.pos.x + 0.5,
|
|
636
|
+
y1: ROWS - 1 - passCarrier.pos.y + 0.5,
|
|
637
|
+
x2: hoveredPassAt.x + 0.5,
|
|
638
|
+
y2: ROWS - 1 - hoveredPassAt.y + 0.5,
|
|
639
|
+
stroke: trajectoryIntercepted ? "var(--danger)" : "var(--pass-highlight)",
|
|
640
|
+
strokeWidth: "0.08",
|
|
641
|
+
strokeDasharray: "0.15 0.1",
|
|
642
|
+
strokeLinecap: "round"
|
|
643
|
+
}
|
|
644
|
+
)
|
|
645
|
+
]
|
|
646
|
+
}
|
|
647
|
+
),
|
|
648
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
|
|
649
|
+
/* @__PURE__ */ jsx(AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
|
|
650
|
+
const isPickupCarrier = pickupCarrierId === piece.id;
|
|
651
|
+
const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
|
|
652
|
+
return /* @__PURE__ */ jsx(
|
|
653
|
+
motion.div,
|
|
654
|
+
{
|
|
655
|
+
layout: true,
|
|
656
|
+
initial: false,
|
|
657
|
+
animate: {
|
|
658
|
+
left: `${piece.pos.x / COLS * 100}%`,
|
|
659
|
+
top: `${(ROWS - 1 - piece.pos.y) / ROWS * 100}%`
|
|
660
|
+
},
|
|
661
|
+
transition: pieceTransition,
|
|
662
|
+
style: {
|
|
663
|
+
position: "absolute",
|
|
664
|
+
width: `${100 / COLS}%`,
|
|
665
|
+
height: `${100 / ROWS}%`
|
|
666
|
+
},
|
|
667
|
+
className: "flex items-center justify-center pointer-events-auto",
|
|
668
|
+
children: /* @__PURE__ */ jsx(
|
|
669
|
+
GamePiece,
|
|
670
|
+
{
|
|
671
|
+
piece,
|
|
672
|
+
isSelected: selectedPieceId === piece.id,
|
|
673
|
+
hasBall: boardState.ball.holderId === piece.id,
|
|
674
|
+
onClick: (e) => handlePieceClick(piece.id, piece.pos.x, piece.pos.y, e)
|
|
675
|
+
}
|
|
676
|
+
)
|
|
677
|
+
},
|
|
678
|
+
piece.id
|
|
679
|
+
);
|
|
680
|
+
}) }),
|
|
681
|
+
(() => {
|
|
682
|
+
const targetX = ball.pos.x / COLS * 100;
|
|
683
|
+
const targetY = (ROWS - 1 - ball.pos.y) / ROWS * 100;
|
|
684
|
+
let animate;
|
|
685
|
+
let transition;
|
|
686
|
+
if (isPickupOnMove && prevBall) {
|
|
687
|
+
const prevX = prevBall.pos.x / COLS * 100;
|
|
688
|
+
const prevY = (ROWS - 1 - prevBall.pos.y) / ROWS * 100;
|
|
689
|
+
const pulseStart = Math.max(0.05, pickupProportion - 0.06);
|
|
690
|
+
animate = {
|
|
691
|
+
left: [`${prevX}%`, `${prevX}%`, `${prevX}%`, `${targetX}%`],
|
|
692
|
+
top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
|
|
693
|
+
scale: [1, 1, 1.35, 1]
|
|
694
|
+
};
|
|
695
|
+
transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
|
|
696
|
+
} else {
|
|
697
|
+
animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
|
|
698
|
+
transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
|
|
699
|
+
}
|
|
700
|
+
return /* @__PURE__ */ jsx(
|
|
701
|
+
motion.div,
|
|
702
|
+
{
|
|
703
|
+
initial: false,
|
|
704
|
+
animate,
|
|
705
|
+
transition,
|
|
706
|
+
style: {
|
|
707
|
+
position: "absolute",
|
|
708
|
+
width: `${100 / COLS}%`,
|
|
709
|
+
height: `${100 / ROWS}%`,
|
|
710
|
+
zIndex: 50
|
|
711
|
+
},
|
|
712
|
+
className: "flex items-center justify-center pointer-events-none",
|
|
713
|
+
children: /* @__PURE__ */ jsx("div", { className: "w-[60%] h-[60%]", children: /* @__PURE__ */ jsx(Football, {}) })
|
|
714
|
+
},
|
|
715
|
+
"ball"
|
|
716
|
+
);
|
|
717
|
+
})(),
|
|
718
|
+
invalidClickAt && /* @__PURE__ */ jsx(
|
|
523
719
|
motion.div,
|
|
524
720
|
{
|
|
721
|
+
"data-testid": "invalid-action-shake",
|
|
722
|
+
"aria-hidden": "true",
|
|
525
723
|
initial: false,
|
|
526
|
-
animate,
|
|
527
|
-
transition,
|
|
724
|
+
animate: prefersReducedMotion ? { opacity: [0.9, 0] } : { x: [0, -3, 3, -3, 3, 0] },
|
|
725
|
+
transition: { duration: 0.08 },
|
|
528
726
|
style: {
|
|
529
727
|
position: "absolute",
|
|
728
|
+
left: `${invalidClickAt.x / COLS * 100}%`,
|
|
729
|
+
top: `${(ROWS - 1 - invalidClickAt.y) / ROWS * 100}%`,
|
|
530
730
|
width: `${100 / COLS}%`,
|
|
531
|
-
height: `${100 / ROWS}
|
|
532
|
-
zIndex: 50
|
|
731
|
+
height: `${100 / ROWS}%`
|
|
533
732
|
},
|
|
534
|
-
className: "
|
|
535
|
-
children: /* @__PURE__ */ jsx("div", { className: "w-[60%] h-[60%]", children: /* @__PURE__ */ jsx(Football, {}) })
|
|
733
|
+
className: "ring-2 ring-inset ring-danger/70 rounded-sm"
|
|
536
734
|
},
|
|
537
|
-
|
|
538
|
-
)
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
735
|
+
invalidClickAt.nonce
|
|
736
|
+
)
|
|
737
|
+
] }),
|
|
738
|
+
isOffsideRisk && /* @__PURE__ */ 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: [
|
|
739
|
+
"\u26A0 ",
|
|
740
|
+
t("offsideWarning")
|
|
741
|
+
] })
|
|
544
742
|
] })
|
|
545
|
-
]
|
|
743
|
+
]
|
|
546
744
|
}
|
|
547
745
|
);
|
|
548
746
|
if (!showCoordinates) return board;
|
|
@@ -587,31 +785,50 @@ function AvatarFallback({ className, children }) {
|
|
|
587
785
|
}
|
|
588
786
|
function ActionPoints({ total, remaining, size = 20, className, kingMustRelease }) {
|
|
589
787
|
const t = useGameT();
|
|
788
|
+
const prevRemainingRef = useRef(remaining);
|
|
789
|
+
const [spentRange, setSpentRange] = useState(null);
|
|
790
|
+
useEffect(() => {
|
|
791
|
+
const prev = prevRemainingRef.current;
|
|
792
|
+
prevRemainingRef.current = remaining;
|
|
793
|
+
if (remaining < prev) {
|
|
794
|
+
setSpentRange([remaining, prev - 1]);
|
|
795
|
+
const id = setTimeout(() => setSpentRange(null), 300);
|
|
796
|
+
return () => clearTimeout(id);
|
|
797
|
+
}
|
|
798
|
+
setSpentRange(null);
|
|
799
|
+
}, [remaining]);
|
|
590
800
|
return /* @__PURE__ */ jsx("div", { className: cn("flex items-center gap-1.5", className), "aria-label": t("actionPointsAriaLabel", { remaining, total }), children: Array.from({ length: total }).map((_, i) => {
|
|
591
801
|
const isActive = i < remaining;
|
|
592
802
|
const showCrown = kingMustRelease && i === total - 1;
|
|
593
|
-
|
|
594
|
-
return /* @__PURE__ */ jsx(
|
|
595
|
-
Crown,
|
|
596
|
-
{
|
|
597
|
-
size,
|
|
598
|
-
strokeWidth: 2,
|
|
599
|
-
className: "text-yellow-400 fill-yellow-400 transition-colors duration-150",
|
|
600
|
-
"aria-hidden": "true"
|
|
601
|
-
},
|
|
602
|
-
i
|
|
603
|
-
);
|
|
604
|
-
}
|
|
803
|
+
const justSpent = !!spentRange && i >= spentRange[0] && i <= spentRange[1];
|
|
605
804
|
return /* @__PURE__ */ jsx(
|
|
606
|
-
|
|
805
|
+
motion.span,
|
|
607
806
|
{
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
className:
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
807
|
+
"data-testid": `ap-pip-${i}`,
|
|
808
|
+
"data-spent": justSpent || void 0,
|
|
809
|
+
className: "inline-flex",
|
|
810
|
+
animate: justSpent ? { scale: [1.5, 1] } : void 0,
|
|
811
|
+
transition: { duration: 0.3 },
|
|
812
|
+
children: showCrown ? /* @__PURE__ */ jsx(
|
|
813
|
+
Crown,
|
|
814
|
+
{
|
|
815
|
+
size,
|
|
816
|
+
strokeWidth: 2,
|
|
817
|
+
className: "text-yellow-400 fill-yellow-400 transition-colors duration-150",
|
|
818
|
+
"aria-hidden": "true"
|
|
819
|
+
}
|
|
820
|
+
) : /* @__PURE__ */ jsx(
|
|
821
|
+
Zap,
|
|
822
|
+
{
|
|
823
|
+
size,
|
|
824
|
+
strokeWidth: 2,
|
|
825
|
+
className: cn(
|
|
826
|
+
"transition-colors duration-150",
|
|
827
|
+
isActive ? "text-accent-green fill-accent-green" : "text-fg-muted fill-none"
|
|
828
|
+
),
|
|
829
|
+
"aria-hidden": "true"
|
|
830
|
+
}
|
|
831
|
+
)
|
|
615
832
|
},
|
|
616
833
|
i
|
|
617
834
|
);
|