@scriptonita/chess-football-ui 0.8.0 → 0.10.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/README.md +53 -3
- package/dist/{chunk-LTFNSTAQ.cjs → chunk-BSE4DUXM.cjs} +18 -5
- package/dist/chunk-BSE4DUXM.cjs.map +1 -0
- package/dist/{chunk-N24LGLLO.js → chunk-C5HCN7O5.js} +18 -6
- package/dist/chunk-C5HCN7O5.js.map +1 -0
- package/dist/index.cjs +397 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +171 -8
- package/dist/index.d.ts +171 -8
- package/dist/index.js +390 -88
- package/dist/index.js.map +1 -1
- package/dist/store.cjs +7 -3
- package/dist/store.d.cts +9 -1
- package/dist/store.d.ts +9 -1
- package/dist/store.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-LTFNSTAQ.cjs.map +0 -1
- package/dist/chunk-N24LGLLO.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkBSE4DUXM_cjs = require('./chunk-BSE4DUXM.cjs');
|
|
4
|
+
var shallow = require('zustand/react/shallow');
|
|
4
5
|
var chessFootballEngine = require('@scriptonita/chess-football-engine');
|
|
5
6
|
var clsx = require('clsx');
|
|
6
7
|
var tailwindMerge = require('tailwind-merge');
|
|
@@ -43,12 +44,17 @@ var EMBOSS_FILTER = {
|
|
|
43
44
|
white: "drop-shadow(0 -0.75px 0 rgba(255,255,255,0.65)) drop-shadow(0 1.25px 1px rgba(0,0,0,0.45))",
|
|
44
45
|
black: "drop-shadow(0 -0.5px 0 rgba(255,255,255,0.3)) drop-shadow(0 1.25px 1.5px rgba(0,0,0,0.8))"
|
|
45
46
|
};
|
|
46
|
-
var
|
|
47
|
+
var MOVED_GLYPH_COLOR = {
|
|
48
|
+
white: "#8b8b94",
|
|
49
|
+
black: "#bdbdc3"
|
|
50
|
+
};
|
|
51
|
+
var PieceIcon = ({ type, side, moved = false }) => {
|
|
47
52
|
const Icon = PIECE_ICON[type] ?? lucideReact.ShieldQuestion;
|
|
48
53
|
const style = {
|
|
49
54
|
width: "clamp(16px, 7.4cqw, 44px)",
|
|
50
55
|
height: "clamp(16px, 7.4cqw, 44px)",
|
|
51
|
-
filter: EMBOSS_FILTER[side]
|
|
56
|
+
filter: EMBOSS_FILTER[side],
|
|
57
|
+
...moved ? { color: MOVED_GLYPH_COLOR[side] } : null
|
|
52
58
|
};
|
|
53
59
|
return /* @__PURE__ */ jsxRuntime.jsx(Icon, { style, strokeWidth: 2, "aria-hidden": "true" });
|
|
54
60
|
};
|
|
@@ -92,21 +98,20 @@ var ON_BOARD_CHIP_STYLE = {
|
|
|
92
98
|
].join(", ")
|
|
93
99
|
}
|
|
94
100
|
};
|
|
95
|
-
var MOVED_STYLE = {
|
|
96
|
-
filter: "saturate(0.15) brightness(0.75)"
|
|
97
|
-
};
|
|
98
101
|
function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
|
|
99
102
|
const chipStyle = ON_BOARD_CHIP_STYLE[piece.side];
|
|
100
103
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
101
104
|
"div",
|
|
102
105
|
{
|
|
103
106
|
onClick,
|
|
104
|
-
|
|
107
|
+
"data-moved": piece.hasMovedThisTurn ? "true" : "false",
|
|
105
108
|
className: cn(
|
|
106
109
|
"relative w-[88%] h-[88%] rounded-full cursor-pointer z-10 transition-transform duration-200 ease-out motion-reduce:transition-none",
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
// `--primary` was the package's one remaining shadcn token, and
|
|
111
|
+
// CrazyGames had to define it by hand just to make the selection
|
|
112
|
+
// ring visible. It is part of the design system now.
|
|
113
|
+
isSelected && "ring-4 ring-accent-green scale-110 z-30",
|
|
114
|
+
isMoving && "scale-[1.08] z-40"
|
|
110
115
|
),
|
|
111
116
|
children: [
|
|
112
117
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -128,7 +133,7 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
|
|
|
128
133
|
style: chipStyle,
|
|
129
134
|
className: "absolute inset-0 rounded-full flex items-center justify-center",
|
|
130
135
|
children: [
|
|
131
|
-
/* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side }),
|
|
136
|
+
/* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side, moved: piece.hasMovedThisTurn }),
|
|
132
137
|
hasBall && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full ring-2 ring-orange-400 pointer-events-none" }),
|
|
133
138
|
isSelected && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full animate-pulse motion-reduce:animate-none ring-2 ring-yellow-400 ring-offset-2 ring-offset-transparent" })
|
|
134
139
|
]
|
|
@@ -138,6 +143,8 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
|
|
|
138
143
|
}
|
|
139
144
|
);
|
|
140
145
|
}
|
|
146
|
+
var BALL_COLOR = "var(--ball-color, #ffffff)";
|
|
147
|
+
var BALL_SHADE = "color-mix(in srgb, var(--ball-color, #ffffff) 74%, #000000)";
|
|
141
148
|
var Football = ({ className }) => {
|
|
142
149
|
const bodyId = React.useId();
|
|
143
150
|
const hiId = `${bodyId}-h`;
|
|
@@ -151,9 +158,9 @@ var Football = ({ className }) => {
|
|
|
151
158
|
children: [
|
|
152
159
|
/* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
|
|
153
160
|
/* @__PURE__ */ jsxRuntime.jsxs("radialGradient", { id: bodyId, cx: "36%", cy: "32%", r: "68%", children: [
|
|
154
|
-
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: "#
|
|
155
|
-
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "55%", stopColor:
|
|
156
|
-
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor:
|
|
161
|
+
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", style: { stopColor: "#ffffff" } }),
|
|
162
|
+
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "55%", style: { stopColor: BALL_COLOR } }),
|
|
163
|
+
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", style: { stopColor: BALL_SHADE } })
|
|
157
164
|
] }),
|
|
158
165
|
/* @__PURE__ */ jsxRuntime.jsxs("radialGradient", { id: hiId, cx: "32%", cy: "28%", r: "22%", children: [
|
|
159
166
|
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: "0.85" }),
|
|
@@ -196,6 +203,11 @@ var STATIC_I18N_KEYS = [
|
|
|
196
203
|
"endTurnConfirmDescription",
|
|
197
204
|
"endTurnConfirmYes",
|
|
198
205
|
"endTurnKeepPlaying",
|
|
206
|
+
"hints.dismiss",
|
|
207
|
+
"hints.hasBall",
|
|
208
|
+
"hints.lastActionPoint",
|
|
209
|
+
"hints.selectPiece",
|
|
210
|
+
"hints.start",
|
|
199
211
|
"history.empty",
|
|
200
212
|
"history.goal",
|
|
201
213
|
"history.interception",
|
|
@@ -265,7 +277,7 @@ function useGameT() {
|
|
|
265
277
|
}
|
|
266
278
|
function BallHolderChip({ className }) {
|
|
267
279
|
const t = useGameT();
|
|
268
|
-
const boardState =
|
|
280
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
269
281
|
const holder = boardState?.pieces.find((p) => p.id === boardState.ball.holderId) ?? null;
|
|
270
282
|
if (!holder) return null;
|
|
271
283
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-1.5 min-h-[1.75rem]", className), "aria-live": "polite", children: [
|
|
@@ -458,20 +470,32 @@ var ROWS = 12;
|
|
|
458
470
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
459
471
|
var cellX = (x) => `${x * 100}%`;
|
|
460
472
|
var cellY = (y) => `${(ROWS - 1 - y) * 100}%`;
|
|
473
|
+
var TOUCH_CLICK_WINDOW_MS = 800;
|
|
461
474
|
var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
|
|
462
475
|
var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
|
|
463
|
-
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true }) {
|
|
476
|
+
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true, forgivingTaps = false, className }) {
|
|
464
477
|
const t = useGameT();
|
|
465
478
|
const boardId = React.useId();
|
|
466
479
|
const prefersReducedMotion = framerMotion.useReducedMotion();
|
|
467
480
|
const {
|
|
468
481
|
boardState,
|
|
469
482
|
selectedPieceId,
|
|
483
|
+
interactionMode,
|
|
470
484
|
setSelectedPieceId,
|
|
471
485
|
setInteractionMode,
|
|
472
486
|
movePiece,
|
|
473
487
|
passBall
|
|
474
|
-
} =
|
|
488
|
+
} = chunkBSE4DUXM_cjs.useGameStore(
|
|
489
|
+
shallow.useShallow((s) => ({
|
|
490
|
+
boardState: s.boardState,
|
|
491
|
+
selectedPieceId: s.selectedPieceId,
|
|
492
|
+
interactionMode: s.interactionMode,
|
|
493
|
+
setSelectedPieceId: s.setSelectedPieceId,
|
|
494
|
+
setInteractionMode: s.setInteractionMode,
|
|
495
|
+
movePiece: s.movePiece,
|
|
496
|
+
passBall: s.passBall
|
|
497
|
+
}))
|
|
498
|
+
);
|
|
475
499
|
const [cursor, setCursor] = React.useState(null);
|
|
476
500
|
const [disambiguateAt, setDisambiguateAt] = React.useState(null);
|
|
477
501
|
const [shortcutsOpen, setShortcutsOpen] = React.useState(false);
|
|
@@ -493,6 +517,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
493
517
|
setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
|
|
494
518
|
};
|
|
495
519
|
const [hoveredPassAt, setHoveredPassAt] = React.useState(null);
|
|
520
|
+
const lastTouchAtRef = React.useRef(0);
|
|
496
521
|
const [movingIds, setMovingIds] = React.useState(() => /* @__PURE__ */ new Set());
|
|
497
522
|
const setMoving = (id, moving) => setMovingIds((prev) => {
|
|
498
523
|
if (prev.has(id) === moving) return prev;
|
|
@@ -539,21 +564,21 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
539
564
|
return () => clearTimeout(id);
|
|
540
565
|
}, [replay]);
|
|
541
566
|
const validMoves = React.useMemo(() => {
|
|
542
|
-
if (!boardState) return [];
|
|
567
|
+
if (!boardState || interactionMode === "pass") return [];
|
|
543
568
|
const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
544
569
|
if (sp && !sp.hasMovedThisTurn) {
|
|
545
570
|
return chessFootballEngine.getValidMoves(sp, boardState);
|
|
546
571
|
}
|
|
547
572
|
return [];
|
|
548
|
-
}, [boardState, selectedPieceId]);
|
|
573
|
+
}, [boardState, selectedPieceId, interactionMode]);
|
|
549
574
|
const validPasses = React.useMemo(() => {
|
|
550
|
-
if (!boardState) return [];
|
|
575
|
+
if (!boardState || interactionMode === "move") return [];
|
|
551
576
|
const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
552
577
|
if (sp && boardState.ball.holderId === sp.id) {
|
|
553
578
|
return chessFootballEngine.getValidPasses(sp, boardState);
|
|
554
579
|
}
|
|
555
580
|
return [];
|
|
556
|
-
}, [boardState, selectedPieceId]);
|
|
581
|
+
}, [boardState, selectedPieceId, interactionMode]);
|
|
557
582
|
if (!boardState) return null;
|
|
558
583
|
const lastMove = boardState.lastMove;
|
|
559
584
|
const ball = boardState.ball;
|
|
@@ -578,11 +603,24 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
578
603
|
const passCarrier = boardState.pieces.find((p) => p.id === selectedPieceId && boardState.ball.holderId === p.id) ?? null;
|
|
579
604
|
const trajectoryPath = hoveredPassAt && passCarrier ? passCarrier.type === "knight" ? [hoveredPassAt] : chessFootballEngine.getPath(passCarrier.pos, hoveredPassAt) : null;
|
|
580
605
|
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)));
|
|
581
|
-
const
|
|
606
|
+
const soleAdjacentTarget = (x, y) => {
|
|
607
|
+
const near = [...validMoves, ...validPasses].filter(
|
|
608
|
+
(t2, i, all) => Math.max(Math.abs(t2.x - x), Math.abs(t2.y - y)) === 1 && all.findIndex((o) => o.x === t2.x && o.y === t2.y) === i
|
|
609
|
+
);
|
|
610
|
+
return near.length === 1 ? near[0] : null;
|
|
611
|
+
};
|
|
612
|
+
const handleSquareClick = (x, y, fromTouch = false) => {
|
|
582
613
|
if (replay) return;
|
|
583
614
|
setCursor(null);
|
|
584
615
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
585
616
|
const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
|
|
617
|
+
if (!isValidMove && !isValidPass && fromTouch && forgivingTaps && selectedPieceId) {
|
|
618
|
+
const target = soleAdjacentTarget(x, y);
|
|
619
|
+
if (target) {
|
|
620
|
+
handleSquareClick(target.x, target.y);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
586
624
|
if (isValidMove && isValidPass) {
|
|
587
625
|
setDisambiguateAt({ x, y });
|
|
588
626
|
return;
|
|
@@ -667,8 +705,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
667
705
|
activate(c.x, c.y);
|
|
668
706
|
break;
|
|
669
707
|
}
|
|
708
|
+
// Bare M / P only: Cmd+P is Print and Ctrl+M is a browser shortcut,
|
|
709
|
+
// and both used to resolve the popover behind the user's back.
|
|
670
710
|
case "m":
|
|
671
711
|
case "M":
|
|
712
|
+
if (e.ctrlKey || e.metaKey || e.altKey) break;
|
|
672
713
|
if (disambiguateAt && selectedPieceId) {
|
|
673
714
|
movePiece(selectedPieceId, disambiguateAt);
|
|
674
715
|
setDisambiguateAt(null);
|
|
@@ -676,6 +717,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
676
717
|
break;
|
|
677
718
|
case "p":
|
|
678
719
|
case "P":
|
|
720
|
+
if (e.ctrlKey || e.metaKey || e.altKey) break;
|
|
679
721
|
if (disambiguateAt) {
|
|
680
722
|
passBall(disambiguateAt);
|
|
681
723
|
setDisambiguateAt(null);
|
|
@@ -735,27 +777,35 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
735
777
|
role: "gridcell",
|
|
736
778
|
"aria-label": squareLabel(x, y, pieceAt, isValidMove, isValidPass),
|
|
737
779
|
"aria-selected": !!pieceAt && pieceAt.id === selectedPieceId,
|
|
738
|
-
onClick: () => handleSquareClick(x, y),
|
|
780
|
+
onClick: () => handleSquareClick(x, y, Date.now() - lastTouchAtRef.current < TOUCH_CLICK_WINDOW_MS),
|
|
739
781
|
onMouseEnter: () => {
|
|
740
782
|
if (isValidPass) setHoveredPassAt({ x, y });
|
|
741
783
|
},
|
|
742
784
|
onMouseLeave: () => setHoveredPassAt(null),
|
|
785
|
+
onTouchStart: () => {
|
|
786
|
+
lastTouchAtRef.current = Date.now();
|
|
787
|
+
if (isValidPass) setHoveredPassAt({ x, y });
|
|
788
|
+
},
|
|
743
789
|
className: cn(
|
|
744
790
|
pitchSquareClass(x, y),
|
|
745
791
|
"flex items-center justify-center cursor-pointer",
|
|
746
792
|
!isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
|
|
747
793
|
// §8: pulsing warning ring on enemy goal area
|
|
748
794
|
isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
795
|
+
// Move and pass used raw yellow-400/sky-400 here while
|
|
796
|
+
// the popover a few lines below used the
|
|
797
|
+
// move-highlight/pass-highlight tokens — two palettes
|
|
798
|
+
// for the same two concepts. One palette now: tokens.
|
|
799
|
+
isAmbiguous && "ring-[3px] ring-inset ring-move-highlight/90 bg-move-highlight/20",
|
|
800
|
+
!isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-move-highlight/80 bg-move-highlight/15",
|
|
801
|
+
!isAmbiguous && isValidPass && "ring-[3px] ring-inset ring-pass-highlight/80 bg-pass-highlight/15"
|
|
752
802
|
),
|
|
753
803
|
children: [
|
|
754
|
-
!isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full bg-
|
|
755
|
-
!isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full
|
|
756
|
-
isAmbiguous && !pieceAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-0.5", children: [
|
|
757
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-
|
|
758
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5 rounded-full
|
|
804
|
+
!isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "move-marker", className: "w-3 h-3 rounded-full bg-move-highlight/65" }),
|
|
805
|
+
!isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "pass-marker", className: "w-3.5 h-3.5 rounded-full border-2 border-pass-highlight/80" }),
|
|
806
|
+
isAmbiguous && !pieceAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", children: [
|
|
807
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "move-marker", className: "w-2.5 h-2.5 rounded-full bg-move-highlight/80" }),
|
|
808
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "pass-marker", className: "w-2.5 h-2.5 rounded-full border-2 border-pass-highlight/90" })
|
|
759
809
|
] }),
|
|
760
810
|
isCursor && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" }),
|
|
761
811
|
isDisambiguateTarget && selectedPieceId && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -820,10 +870,14 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
820
870
|
"data-testid": "board-root",
|
|
821
871
|
className: cn(
|
|
822
872
|
// §6: Mobile edge-to-edge — thin 2px margin, no border, no radius
|
|
823
|
-
|
|
873
|
+
// The frame was hardcoded #1a3317 in three files, hand-tuned for
|
|
874
|
+
// the webapp's darker grass and visibly wrong against the
|
|
875
|
+
// CrazyGames pitch. It is a token now (see README).
|
|
876
|
+
"w-full mx-0.5 bg-field-frame overflow-hidden",
|
|
824
877
|
// §5+§6: Desktop — centered, rounded, bordered, height-based max-width
|
|
825
|
-
"md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-
|
|
826
|
-
"md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]"
|
|
878
|
+
"md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-field-frame",
|
|
879
|
+
"md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]",
|
|
880
|
+
!showCoordinates && className
|
|
827
881
|
),
|
|
828
882
|
children: [
|
|
829
883
|
toolbar && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "hidden md:flex items-center justify-between gap-2 px-2.5 py-1", "data-testid": "board-toolbar", children: [
|
|
@@ -850,7 +904,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
850
904
|
},
|
|
851
905
|
"aria-label": t("shortcuts.buttonLabel"),
|
|
852
906
|
"aria-expanded": shortcutsOpen,
|
|
853
|
-
className: "w-
|
|
907
|
+
className: "w-11 h-11 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",
|
|
854
908
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
|
|
855
909
|
}
|
|
856
910
|
),
|
|
@@ -1020,7 +1074,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
1020
1074
|
);
|
|
1021
1075
|
if (!showCoordinates) return board;
|
|
1022
1076
|
const ranksTopToBottom = [...chessFootballEngine.RANK_LABELS].reverse();
|
|
1023
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full max-w-[540px] md:max-w-none mx-auto",
|
|
1077
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("w-full max-w-[540px] md:max-w-none mx-auto", className), children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1024
1078
|
"div",
|
|
1025
1079
|
{
|
|
1026
1080
|
className: "grid gap-1 select-none",
|
|
@@ -1052,8 +1106,20 @@ function Avatar({ className, children }) {
|
|
|
1052
1106
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("relative w-9 h-9 rounded-full overflow-hidden shrink-0", className), children });
|
|
1053
1107
|
}
|
|
1054
1108
|
function AvatarImage({ src, alt, className }) {
|
|
1055
|
-
|
|
1056
|
-
|
|
1109
|
+
const [failed, setFailed] = React.useState(false);
|
|
1110
|
+
React.useEffect(() => {
|
|
1111
|
+
setFailed(false);
|
|
1112
|
+
}, [src]);
|
|
1113
|
+
if (!src || failed) return null;
|
|
1114
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1115
|
+
"img",
|
|
1116
|
+
{
|
|
1117
|
+
src,
|
|
1118
|
+
alt: alt ?? "",
|
|
1119
|
+
onError: () => setFailed(true),
|
|
1120
|
+
className: cn("w-full h-full object-cover", className)
|
|
1121
|
+
}
|
|
1122
|
+
);
|
|
1057
1123
|
}
|
|
1058
1124
|
function AvatarFallback({ className, children }) {
|
|
1059
1125
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("absolute inset-0 flex items-center justify-center text-xs font-semibold", className), children });
|
|
@@ -1109,6 +1175,51 @@ function ActionPoints({ total, remaining, size = 20, className, kingMustRelease
|
|
|
1109
1175
|
);
|
|
1110
1176
|
}) });
|
|
1111
1177
|
}
|
|
1178
|
+
function PlayerIdentity({
|
|
1179
|
+
name,
|
|
1180
|
+
team,
|
|
1181
|
+
variant = "full",
|
|
1182
|
+
isActive = false,
|
|
1183
|
+
align = "left",
|
|
1184
|
+
adornment,
|
|
1185
|
+
className
|
|
1186
|
+
}) {
|
|
1187
|
+
if (variant === "compact") {
|
|
1188
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1189
|
+
"span",
|
|
1190
|
+
{
|
|
1191
|
+
className: cn(
|
|
1192
|
+
"font-inter text-[11px] font-medium truncate max-w-[160px]",
|
|
1193
|
+
isActive ? "text-fg-secondary" : "text-fg-muted",
|
|
1194
|
+
className
|
|
1195
|
+
),
|
|
1196
|
+
children: [
|
|
1197
|
+
name,
|
|
1198
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono uppercase tracking-[0.5px] text-fg-muted", children: [
|
|
1199
|
+
" \xB7 ",
|
|
1200
|
+
team
|
|
1201
|
+
] })
|
|
1202
|
+
]
|
|
1203
|
+
}
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col gap-1 min-w-0", align === "right" && "items-end", className), children: [
|
|
1207
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1208
|
+
"span",
|
|
1209
|
+
{
|
|
1210
|
+
className: cn(
|
|
1211
|
+
"font-inter text-[13px] font-semibold leading-none truncate transition-colors duration-300",
|
|
1212
|
+
isActive ? "text-fg-primary" : "text-fg-muted"
|
|
1213
|
+
),
|
|
1214
|
+
children: name
|
|
1215
|
+
}
|
|
1216
|
+
),
|
|
1217
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-1", align === "right" && "flex-row-reverse"), children: [
|
|
1218
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase tracking-[0.5px]", children: team }),
|
|
1219
|
+
adornment
|
|
1220
|
+
] })
|
|
1221
|
+
] });
|
|
1222
|
+
}
|
|
1112
1223
|
function initials(name) {
|
|
1113
1224
|
return (name?.split(" ").map((w) => w[0]).join("").toUpperCase() || "?").slice(0, 2);
|
|
1114
1225
|
}
|
|
@@ -1117,10 +1228,11 @@ function Scoreboard({
|
|
|
1117
1228
|
opponentUsername,
|
|
1118
1229
|
creatorAvatarUrl,
|
|
1119
1230
|
opponentAvatarUrl,
|
|
1120
|
-
userSide
|
|
1231
|
+
userSide,
|
|
1232
|
+
className
|
|
1121
1233
|
}) {
|
|
1122
1234
|
const t = useGameT();
|
|
1123
|
-
const
|
|
1235
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1124
1236
|
if (!boardState?.score) return null;
|
|
1125
1237
|
const { score, turn, actionPoints, kingMustRelease } = boardState;
|
|
1126
1238
|
const myScore = userSide === "white" ? score.white : score.black;
|
|
@@ -1134,8 +1246,8 @@ function Scoreboard({
|
|
|
1134
1246
|
const isMyTurn = turn === userSide;
|
|
1135
1247
|
const myKingMustRelease = isMyTurn && kingMustRelease === turn;
|
|
1136
1248
|
const rivalKingMustRelease = !isMyTurn && kingMustRelease === turn;
|
|
1137
|
-
const maxAP = boardState.maxActionPoints ??
|
|
1138
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", children: [
|
|
1249
|
+
const maxAP = boardState.maxActionPoints ?? chunkBSE4DUXM_cjs.DEFAULT_MAX_AP;
|
|
1250
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", className), children: [
|
|
1139
1251
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1140
1252
|
PlayerInfo,
|
|
1141
1253
|
{
|
|
@@ -1180,14 +1292,16 @@ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, maxActionPo
|
|
|
1180
1292
|
/* @__PURE__ */ jsxRuntime.jsx(AvatarFallback, { className: "bg-bg-surface-elevated text-fg-secondary text-xs font-semibold", children: initials(name) })
|
|
1181
1293
|
] }),
|
|
1182
1294
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col gap-1", align === "right" && "items-end"), children: [
|
|
1183
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1295
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1296
|
+
PlayerIdentity,
|
|
1297
|
+
{
|
|
1298
|
+
name,
|
|
1299
|
+
team,
|
|
1300
|
+
isActive,
|
|
1301
|
+
align,
|
|
1302
|
+
adornment: isActive ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-accent-green animate-pulse motion-reduce:animate-none", "aria-hidden": "true" }) : void 0
|
|
1303
|
+
}
|
|
1304
|
+
),
|
|
1191
1305
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1192
1306
|
ActionPoints,
|
|
1193
1307
|
{
|
|
@@ -1208,7 +1322,7 @@ function MiniScoreboard({
|
|
|
1208
1322
|
className
|
|
1209
1323
|
}) {
|
|
1210
1324
|
const t = useGameT();
|
|
1211
|
-
const
|
|
1325
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1212
1326
|
if (!boardState?.score) return null;
|
|
1213
1327
|
const { score, turn } = boardState;
|
|
1214
1328
|
const myScore = userSide === "white" ? score.white : score.black;
|
|
@@ -1242,14 +1356,7 @@ function MiniScoreboard({
|
|
|
1242
1356
|
}
|
|
1243
1357
|
function SideCell({ name, team, score, isActive, align }) {
|
|
1244
1358
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col min-w-0", align === "right" ? "items-end" : "items-start"), children: [
|
|
1245
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1246
|
-
"font-mono text-[10px] uppercase tracking-[0.5px] truncate max-w-[160px]",
|
|
1247
|
-
isActive ? "text-fg-secondary" : "text-fg-muted"
|
|
1248
|
-
), children: [
|
|
1249
|
-
name,
|
|
1250
|
-
" \xB7 ",
|
|
1251
|
-
team
|
|
1252
|
-
] }),
|
|
1359
|
+
/* @__PURE__ */ jsxRuntime.jsx(PlayerIdentity, { name, team, variant: "compact", isActive }),
|
|
1253
1360
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
|
|
1254
1361
|
"font-anton text-[32px] leading-none tabular-nums",
|
|
1255
1362
|
isActive ? "text-fg-primary" : "text-fg-secondary"
|
|
@@ -1393,11 +1500,9 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, on
|
|
|
1393
1500
|
}
|
|
1394
1501
|
);
|
|
1395
1502
|
}
|
|
1396
|
-
function GameControls({ isMyTurn }) {
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
endTurn
|
|
1400
|
-
} = chunkLTFNSTAQ_cjs.useGameStore();
|
|
1503
|
+
function GameControls({ isMyTurn, className }) {
|
|
1504
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1505
|
+
const endTurn = chunkBSE4DUXM_cjs.useGameStore((s) => s.endTurn);
|
|
1401
1506
|
const t = useGameT();
|
|
1402
1507
|
const [showEndTurnConfirm, setShowEndTurnConfirm] = React.useState(false);
|
|
1403
1508
|
if (!boardState) return null;
|
|
@@ -1409,7 +1514,7 @@ function GameControls({ isMyTurn }) {
|
|
|
1409
1514
|
endTurn();
|
|
1410
1515
|
}
|
|
1411
1516
|
};
|
|
1412
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", children: [
|
|
1517
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", className), children: [
|
|
1413
1518
|
isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1414
1519
|
Button,
|
|
1415
1520
|
{
|
|
@@ -1429,7 +1534,15 @@ function GameControls({ isMyTurn }) {
|
|
|
1429
1534
|
]
|
|
1430
1535
|
}
|
|
1431
1536
|
),
|
|
1432
|
-
!isMyTurn && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1537
|
+
!isMyTurn && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1538
|
+
"div",
|
|
1539
|
+
{
|
|
1540
|
+
role: "status",
|
|
1541
|
+
"aria-live": "polite",
|
|
1542
|
+
className: "flex items-center justify-center h-11 rounded-md bg-bg-surface border border-border-subtle",
|
|
1543
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-inter text-sm text-fg-muted", children: t("waitingRival") })
|
|
1544
|
+
}
|
|
1545
|
+
),
|
|
1433
1546
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1434
1547
|
ConfirmDialog,
|
|
1435
1548
|
{
|
|
@@ -1456,7 +1569,9 @@ var PIECE_ICON2 = {
|
|
|
1456
1569
|
};
|
|
1457
1570
|
function SelectedPieceDetail({ isMyTurn, className }) {
|
|
1458
1571
|
const t = useGameT();
|
|
1459
|
-
const { boardState, selectedPieceId } =
|
|
1572
|
+
const { boardState, selectedPieceId } = chunkBSE4DUXM_cjs.useGameStore(
|
|
1573
|
+
shallow.useShallow((s) => ({ boardState: s.boardState, selectedPieceId: s.selectedPieceId }))
|
|
1574
|
+
);
|
|
1460
1575
|
if (!boardState) return null;
|
|
1461
1576
|
const piece = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
1462
1577
|
if (!piece) {
|
|
@@ -1490,7 +1605,6 @@ function SelectedPieceDetail({ isMyTurn, className }) {
|
|
|
1490
1605
|
),
|
|
1491
1606
|
role: "status",
|
|
1492
1607
|
"aria-live": "polite",
|
|
1493
|
-
"aria-label": `${longName} ${square}`,
|
|
1494
1608
|
children: [
|
|
1495
1609
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1496
1610
|
"div",
|
|
@@ -1519,7 +1633,7 @@ function SelectedPieceDetail({ isMyTurn, className }) {
|
|
|
1519
1633
|
}
|
|
1520
1634
|
function MoveHistory({ scrollable = true, limit, className }) {
|
|
1521
1635
|
const t = useGameT();
|
|
1522
|
-
const
|
|
1636
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1523
1637
|
const scrollRef = React.useRef(null);
|
|
1524
1638
|
const history = boardState?.moveHistory ?? [];
|
|
1525
1639
|
const visible = limit ? history.slice(-limit) : history;
|
|
@@ -1626,9 +1740,9 @@ function HistoryRow({ entry, isLatest }) {
|
|
|
1626
1740
|
}
|
|
1627
1741
|
var COLS2 = 9;
|
|
1628
1742
|
var ROWS2 = 12;
|
|
1629
|
-
function StaticGameBoard() {
|
|
1630
|
-
const boardState = React.useMemo(() =>
|
|
1631
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-
|
|
1743
|
+
function StaticGameBoard({ className } = {}) {
|
|
1744
|
+
const boardState = React.useMemo(() => chunkBSE4DUXM_cjs.getInitialBoardState("white"), []);
|
|
1745
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-field-frame bg-field-frame", className), children: [
|
|
1632
1746
|
/* @__PURE__ */ jsxRuntime.jsx(PitchSurface, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 pointer-events-none", children: boardState.pieces.map((piece) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1633
1747
|
"div",
|
|
1634
1748
|
{
|
|
@@ -1672,9 +1786,10 @@ function StaticGameBoard() {
|
|
|
1672
1786
|
}
|
|
1673
1787
|
function TurnBanner({ isMyTurn, waitingLabel, className }) {
|
|
1674
1788
|
const t = useGameT();
|
|
1675
|
-
const
|
|
1789
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1676
1790
|
if (!boardState) return null;
|
|
1677
|
-
const { actionPoints,
|
|
1791
|
+
const { actionPoints, turnNumber } = boardState;
|
|
1792
|
+
const maxActionPoints = boardState.maxActionPoints ?? chunkBSE4DUXM_cjs.DEFAULT_MAX_AP;
|
|
1678
1793
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1679
1794
|
"div",
|
|
1680
1795
|
{
|
|
@@ -1686,16 +1801,19 @@ function TurnBanner({ isMyTurn, waitingLabel, className }) {
|
|
|
1686
1801
|
"aria-live": "polite",
|
|
1687
1802
|
"aria-atomic": "true",
|
|
1688
1803
|
children: [
|
|
1689
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1804
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2 min-w-0", children: [
|
|
1805
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1806
|
+
"span",
|
|
1807
|
+
{
|
|
1808
|
+
className: cn(
|
|
1809
|
+
"font-anton text-sm tracking-[1.5px] uppercase truncate",
|
|
1810
|
+
isMyTurn ? "text-accent-green" : "text-fg-muted"
|
|
1811
|
+
),
|
|
1812
|
+
children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
|
|
1813
|
+
}
|
|
1814
|
+
),
|
|
1815
|
+
typeof turnNumber === "number" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase tracking-[0.5px] shrink-0", children: t("history.turn", { n: turnNumber }) })
|
|
1816
|
+
] }),
|
|
1699
1817
|
isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", "aria-label": t("actionPointsAriaLabel", { remaining: actionPoints, total: maxActionPoints }), children: [
|
|
1700
1818
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { size: 12, className: "text-accent-green", "aria-hidden": "true" }),
|
|
1701
1819
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-xs text-fg-secondary tabular-nums", children: [
|
|
@@ -1717,7 +1835,7 @@ var TOAST_CONFIG = {
|
|
|
1717
1835
|
};
|
|
1718
1836
|
function EventToast({ className }) {
|
|
1719
1837
|
const t = useGameT();
|
|
1720
|
-
const
|
|
1838
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1721
1839
|
const [toast, setToast] = React.useState(null);
|
|
1722
1840
|
React.useEffect(() => {
|
|
1723
1841
|
const lastMove = boardState?.lastMove;
|
|
@@ -1756,7 +1874,7 @@ function EventToast({ className }) {
|
|
|
1756
1874
|
);
|
|
1757
1875
|
}
|
|
1758
1876
|
function MobileHistory({ className }) {
|
|
1759
|
-
const
|
|
1877
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1760
1878
|
const [open, setOpen] = React.useState(false);
|
|
1761
1879
|
const titleId = `${React.useId()}-title`;
|
|
1762
1880
|
const panelRef = useDialogA11y({ open, onClose: () => setOpen(false) });
|
|
@@ -1808,7 +1926,7 @@ function SheetHeader({ titleId, onClose }) {
|
|
|
1808
1926
|
"button",
|
|
1809
1927
|
{
|
|
1810
1928
|
onClick: onClose,
|
|
1811
|
-
className: "w-
|
|
1929
|
+
className: "w-11 h-11 -mr-2 flex items-center justify-center rounded-full bg-bg-surface hover:bg-bg-surface-elevated text-fg-muted hover:text-fg-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
|
|
1812
1930
|
"aria-label": t("history.close"),
|
|
1813
1931
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14, "aria-hidden": "true" })
|
|
1814
1932
|
}
|
|
@@ -1862,20 +1980,209 @@ function HistoryChip({ lastMove, onClick, className }) {
|
|
|
1862
1980
|
}
|
|
1863
1981
|
);
|
|
1864
1982
|
}
|
|
1983
|
+
var GAME_HINT_IDS = ["lastActionPoint", "hasBall", "selectPiece", "start"];
|
|
1984
|
+
function GameHints({ userSide, seen, onSeen, className }) {
|
|
1985
|
+
const t = useGameT();
|
|
1986
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1987
|
+
const selectedPieceId = chunkBSE4DUXM_cjs.useGameStore((s) => s.selectedPieceId);
|
|
1988
|
+
const [done, setDone] = React.useState([]);
|
|
1989
|
+
const myTurn = !!boardState && userSide !== null && boardState.turn === userSide;
|
|
1990
|
+
const selected = myTurn ? boardState.pieces.find((p) => p.id === selectedPieceId) : void 0;
|
|
1991
|
+
const applies = {
|
|
1992
|
+
lastActionPoint: myTurn && boardState.actionPoints === 1,
|
|
1993
|
+
hasBall: !!selected && boardState?.ball.holderId === selected.id,
|
|
1994
|
+
selectPiece: !!selected,
|
|
1995
|
+
start: myTurn && !selected
|
|
1996
|
+
};
|
|
1997
|
+
const current = GAME_HINT_IDS.find((id) => applies[id] && !seen.includes(id) && !done.includes(id)) ?? null;
|
|
1998
|
+
const onSeenRef = React.useRef(onSeen);
|
|
1999
|
+
onSeenRef.current = onSeen;
|
|
2000
|
+
const markSeen = (id) => {
|
|
2001
|
+
setDone((d) => d.includes(id) ? d : [...d, id]);
|
|
2002
|
+
onSeenRef.current(id);
|
|
2003
|
+
};
|
|
2004
|
+
const shownRef = React.useRef(null);
|
|
2005
|
+
React.useEffect(() => {
|
|
2006
|
+
const prev = shownRef.current;
|
|
2007
|
+
shownRef.current = current;
|
|
2008
|
+
if (prev && prev !== current && !done.includes(prev)) markSeen(prev);
|
|
2009
|
+
}, [current]);
|
|
2010
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: cn("pointer-events-none flex justify-center", className), children: current && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2011
|
+
"div",
|
|
2012
|
+
{
|
|
2013
|
+
"data-testid": "game-hint",
|
|
2014
|
+
"data-hint": current,
|
|
2015
|
+
className: "pointer-events-auto flex items-center gap-1 max-w-sm pl-4 pr-0.5 rounded-xl bg-bg-secondary/95 border border-border-subtle shadow-lg backdrop-blur-sm",
|
|
2016
|
+
children: [
|
|
2017
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-2 font-inter text-sm leading-snug text-fg-primary", children: t(`hints.${current}`) }),
|
|
2018
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2019
|
+
"button",
|
|
2020
|
+
{
|
|
2021
|
+
type: "button",
|
|
2022
|
+
onClick: () => markSeen(current),
|
|
2023
|
+
"aria-label": t("hints.dismiss"),
|
|
2024
|
+
className: "shrink-0 w-11 h-11 flex items-center justify-center rounded-full text-fg-muted hover:text-fg-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
|
|
2025
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, "aria-hidden": "true" })
|
|
2026
|
+
}
|
|
2027
|
+
)
|
|
2028
|
+
]
|
|
2029
|
+
}
|
|
2030
|
+
) });
|
|
2031
|
+
}
|
|
2032
|
+
var BOT_TURN_START_DELAY_MS = 400;
|
|
2033
|
+
var BOT_TURN_STEP_MS = 900;
|
|
2034
|
+
var BOT_TURN_SETTLE_MS = 500;
|
|
2035
|
+
var PROVIDER_DEFER_MS = 30;
|
|
2036
|
+
function useBotTurn(opts) {
|
|
2037
|
+
const [botThinking, setBotThinking] = React.useState(false);
|
|
2038
|
+
const [error, setError] = React.useState(null);
|
|
2039
|
+
const [retryNonce, setRetryNonce] = React.useState(0);
|
|
2040
|
+
const optsRef = React.useRef(opts);
|
|
2041
|
+
optsRef.current = opts;
|
|
2042
|
+
const pending = React.useRef(false);
|
|
2043
|
+
const runId = React.useRef(0);
|
|
2044
|
+
const timers = React.useRef(/* @__PURE__ */ new Set());
|
|
2045
|
+
const skipRef = React.useRef(null);
|
|
2046
|
+
const clearTimers = () => {
|
|
2047
|
+
timers.current.forEach(clearTimeout);
|
|
2048
|
+
timers.current.clear();
|
|
2049
|
+
};
|
|
2050
|
+
React.useEffect(() => () => {
|
|
2051
|
+
runId.current++;
|
|
2052
|
+
clearTimers();
|
|
2053
|
+
pending.current = false;
|
|
2054
|
+
skipRef.current = null;
|
|
2055
|
+
}, []);
|
|
2056
|
+
const { boardState, enabled = true, matchOver = false, goalPending = false } = opts;
|
|
2057
|
+
React.useEffect(() => {
|
|
2058
|
+
const o = optsRef.current;
|
|
2059
|
+
const board = o.boardState;
|
|
2060
|
+
if (!board || !enabled || board.turn !== o.botSide) return;
|
|
2061
|
+
if (pending.current || matchOver || goalPending) return;
|
|
2062
|
+
if (chessFootballEngine.checkGoal(board)) return;
|
|
2063
|
+
const run = ++runId.current;
|
|
2064
|
+
const alive = () => runId.current === run;
|
|
2065
|
+
const wait = (ms) => new Promise((resolve) => {
|
|
2066
|
+
const id = setTimeout(() => {
|
|
2067
|
+
timers.current.delete(id);
|
|
2068
|
+
resolve();
|
|
2069
|
+
}, ms);
|
|
2070
|
+
timers.current.add(id);
|
|
2071
|
+
});
|
|
2072
|
+
const settle = () => {
|
|
2073
|
+
pending.current = false;
|
|
2074
|
+
skipRef.current = null;
|
|
2075
|
+
setBotThinking(false);
|
|
2076
|
+
optsRef.current.onProcessingChange?.(false);
|
|
2077
|
+
};
|
|
2078
|
+
pending.current = true;
|
|
2079
|
+
setBotThinking(true);
|
|
2080
|
+
setError(null);
|
|
2081
|
+
o.onProcessingChange?.(true);
|
|
2082
|
+
let skipRequested = false;
|
|
2083
|
+
skipRef.current = () => {
|
|
2084
|
+
skipRequested = true;
|
|
2085
|
+
};
|
|
2086
|
+
const startDelay = o.startDelayMs ?? BOT_TURN_START_DELAY_MS;
|
|
2087
|
+
const stepMs = o.stepMs ?? BOT_TURN_STEP_MS;
|
|
2088
|
+
const settleMs = o.settleMs ?? BOT_TURN_SETTLE_MS;
|
|
2089
|
+
(async () => {
|
|
2090
|
+
try {
|
|
2091
|
+
const [turn] = await Promise.all([
|
|
2092
|
+
wait(Math.min(PROVIDER_DEFER_MS, startDelay)).then(() => o.getTurn(board, o.botSide)),
|
|
2093
|
+
wait(startDelay)
|
|
2094
|
+
]);
|
|
2095
|
+
if (!alive()) return;
|
|
2096
|
+
if (!turn?.states?.length) throw new Error("Bot turn returned no states");
|
|
2097
|
+
const { states } = turn;
|
|
2098
|
+
const last = states[states.length - 1];
|
|
2099
|
+
const shown = turn.closedByEndTurn ? states.length - 1 : states.length;
|
|
2100
|
+
const finish = () => {
|
|
2101
|
+
clearTimers();
|
|
2102
|
+
optsRef.current.onApplyState(last);
|
|
2103
|
+
settle();
|
|
2104
|
+
const scorer = turn.goalScored ?? null;
|
|
2105
|
+
if (scorer) optsRef.current.onGoal?.(scorer, last);
|
|
2106
|
+
};
|
|
2107
|
+
skipRef.current = () => {
|
|
2108
|
+
if (alive()) finish();
|
|
2109
|
+
};
|
|
2110
|
+
if (skipRequested) {
|
|
2111
|
+
finish();
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
for (let i = 0; i < shown; i++) {
|
|
2115
|
+
optsRef.current.onApplyState(states[i]);
|
|
2116
|
+
await wait(i === shown - 1 ? settleMs : stepMs);
|
|
2117
|
+
if (!alive()) return;
|
|
2118
|
+
}
|
|
2119
|
+
finish();
|
|
2120
|
+
} catch (err) {
|
|
2121
|
+
if (!alive()) return;
|
|
2122
|
+
clearTimers();
|
|
2123
|
+
settle();
|
|
2124
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2125
|
+
}
|
|
2126
|
+
})();
|
|
2127
|
+
}, [boardState?.turn, enabled, matchOver, goalPending, retryNonce]);
|
|
2128
|
+
const retry = React.useCallback(() => {
|
|
2129
|
+
setError(null);
|
|
2130
|
+
setRetryNonce((n) => n + 1);
|
|
2131
|
+
}, []);
|
|
2132
|
+
const skip = React.useCallback(() => {
|
|
2133
|
+
skipRef.current?.();
|
|
2134
|
+
}, []);
|
|
2135
|
+
return { botThinking, error, retry, skip };
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// src/tokens.ts
|
|
2139
|
+
var REQUIRED_TOKENS = [
|
|
2140
|
+
// Surfaces
|
|
2141
|
+
"--bg-primary",
|
|
2142
|
+
"--bg-secondary",
|
|
2143
|
+
"--bg-surface",
|
|
2144
|
+
"--bg-surface-elevated",
|
|
2145
|
+
// Text
|
|
2146
|
+
"--fg-primary",
|
|
2147
|
+
"--fg-secondary",
|
|
2148
|
+
"--fg-muted",
|
|
2149
|
+
// Accent (the primary CTA rests on the dark step — see Button)
|
|
2150
|
+
"--accent-green",
|
|
2151
|
+
"--accent-green-light",
|
|
2152
|
+
"--accent-green-dark",
|
|
2153
|
+
// Borders
|
|
2154
|
+
"--border-subtle",
|
|
2155
|
+
// Semantic
|
|
2156
|
+
"--danger",
|
|
2157
|
+
"--warning",
|
|
2158
|
+
// Board
|
|
2159
|
+
"--field-green-1",
|
|
2160
|
+
"--field-green-2",
|
|
2161
|
+
"--field-frame",
|
|
2162
|
+
"--move-highlight",
|
|
2163
|
+
"--pass-highlight",
|
|
2164
|
+
"--last-move-highlight",
|
|
2165
|
+
"--ball-color"
|
|
2166
|
+
];
|
|
1865
2167
|
|
|
1866
2168
|
exports.ActionPoints = ActionPoints;
|
|
1867
2169
|
exports.Avatar = Avatar;
|
|
1868
2170
|
exports.AvatarFallback = AvatarFallback;
|
|
1869
2171
|
exports.AvatarImage = AvatarImage;
|
|
2172
|
+
exports.BOT_TURN_SETTLE_MS = BOT_TURN_SETTLE_MS;
|
|
2173
|
+
exports.BOT_TURN_START_DELAY_MS = BOT_TURN_START_DELAY_MS;
|
|
2174
|
+
exports.BOT_TURN_STEP_MS = BOT_TURN_STEP_MS;
|
|
1870
2175
|
exports.BallHolderChip = BallHolderChip;
|
|
1871
2176
|
exports.Button = Button;
|
|
1872
2177
|
exports.ConfirmDialog = ConfirmDialog;
|
|
1873
2178
|
exports.EventToast = EventToast;
|
|
1874
2179
|
exports.Football = Football;
|
|
2180
|
+
exports.GAME_HINT_IDS = GAME_HINT_IDS;
|
|
1875
2181
|
exports.GAME_I18N_KEYS = GAME_I18N_KEYS;
|
|
1876
2182
|
exports.GOAL_AREA_SHADE_CLASS = GOAL_AREA_SHADE_CLASS;
|
|
1877
2183
|
exports.GameBoard = GameBoard;
|
|
1878
2184
|
exports.GameControls = GameControls;
|
|
2185
|
+
exports.GameHints = GameHints;
|
|
1879
2186
|
exports.GameI18nProvider = GameI18nProvider;
|
|
1880
2187
|
exports.GamePiece = GamePiece;
|
|
1881
2188
|
exports.GrassOverlay = GrassOverlay;
|
|
@@ -1887,6 +2194,8 @@ exports.PITCH_COLS = PITCH_COLS;
|
|
|
1887
2194
|
exports.PITCH_ROWS = PITCH_ROWS;
|
|
1888
2195
|
exports.PitchMarkings = PitchMarkings;
|
|
1889
2196
|
exports.PitchSurface = PitchSurface;
|
|
2197
|
+
exports.PlayerIdentity = PlayerIdentity;
|
|
2198
|
+
exports.REQUIRED_TOKENS = REQUIRED_TOKENS;
|
|
1890
2199
|
exports.Scoreboard = Scoreboard;
|
|
1891
2200
|
exports.SelectedPieceDetail = SelectedPieceDetail;
|
|
1892
2201
|
exports.StaticGameBoard = StaticGameBoard;
|
|
@@ -1894,6 +2203,7 @@ exports.TurnBanner = TurnBanner;
|
|
|
1894
2203
|
exports.cn = cn;
|
|
1895
2204
|
exports.isGoalAreaSquare = isGoalAreaSquare;
|
|
1896
2205
|
exports.pitchSquareClass = pitchSquareClass;
|
|
2206
|
+
exports.useBotTurn = useBotTurn;
|
|
1897
2207
|
exports.useDialogA11y = useDialogA11y;
|
|
1898
2208
|
exports.useGameT = useGameT;
|
|
1899
2209
|
//# sourceMappingURL=index.cjs.map
|