@scriptonita/chess-football-ui 0.9.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 +39 -0
- package/dist/index.cjs +203 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -6
- package/dist/index.d.ts +118 -6
- package/dist/index.js +200 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -67,6 +67,45 @@ expect(REQUIRED_TOKENS.filter(t => !css.includes(`${t}:`))).toEqual([])
|
|
|
67
67
|
`--field-frame` is new in 0.9.0: the board frame was hardcoded `#1a3317` in three places,
|
|
68
68
|
hand-tuned for the webapp's grass and visibly wrong against the CrazyGames pitch.
|
|
69
69
|
|
|
70
|
+
`--ball-color` joins the contract in 0.10.0: the ball was a hardcoded sky blue, the same
|
|
71
|
+
hue as `--pass-highlight`, while both apps defined a `--ball-color` nobody read.
|
|
72
|
+
|
|
73
|
+
## Bot turn
|
|
74
|
+
|
|
75
|
+
`useBotTurn` owns a bot's turn end to end — entry gates (turn, pending goal, match over),
|
|
76
|
+
timed replay, cancellation on unmount, error + retry, and skip. The app only says where the
|
|
77
|
+
turn comes from, so the same hook serves a local search and a server call:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { useBotTurn } from '@scriptonita/chess-football-ui'
|
|
81
|
+
import { playBotTurn } from '@scriptonita/chess-football-engine'
|
|
82
|
+
|
|
83
|
+
const { botThinking, error, retry, skip } = useBotTurn({
|
|
84
|
+
boardState, botSide,
|
|
85
|
+
getTurn: (board, side) => playBotTurn(bot, board, side), // local
|
|
86
|
+
// getTurn: (board) => fetch('/api/turn', …).then(r => r.json()), // server
|
|
87
|
+
onApplyState: setBoardState,
|
|
88
|
+
matchOver: !!matchResult,
|
|
89
|
+
goalPending: !!goalScored,
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Pacing is fixed in one place: `BOT_TURN_START_DELAY_MS` (400) before the first action,
|
|
94
|
+
`BOT_TURN_STEP_MS` (900) between actions and `BOT_TURN_SETTLE_MS` (500) for the last one to
|
|
95
|
+
land. The closing end-of-turn state takes no step of its own, so a full 5-AP turn lasts 4.5 s.
|
|
96
|
+
|
|
97
|
+
## First-match hints
|
|
98
|
+
|
|
99
|
+
`<GameHints userSide seen onSeen />` shows one dismissible sentence the first time each
|
|
100
|
+
situation happens (`GAME_HINT_IDS`: first turn, selecting a piece, holding the ball, last
|
|
101
|
+
action point). The texts are `hints.*` keys of `GAME_I18N_KEYS`. The app decides when to mount
|
|
102
|
+
it and where `seen` is stored.
|
|
103
|
+
|
|
104
|
+
## Forgiving taps
|
|
105
|
+
|
|
106
|
+
`<GameBoard forgivingTaps />` resolves a touch on an empty square that borders exactly one
|
|
107
|
+
legal target to that target. Opt-in, touch only, never when the tap is ambiguous.
|
|
108
|
+
|
|
70
109
|
## Peer dependencies
|
|
71
110
|
|
|
72
111
|
`react`, `react-dom`, `framer-motion`, `lucide-react`.
|
package/dist/index.cjs
CHANGED
|
@@ -44,12 +44,17 @@ var EMBOSS_FILTER = {
|
|
|
44
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))",
|
|
45
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))"
|
|
46
46
|
};
|
|
47
|
-
var
|
|
47
|
+
var MOVED_GLYPH_COLOR = {
|
|
48
|
+
white: "#8b8b94",
|
|
49
|
+
black: "#bdbdc3"
|
|
50
|
+
};
|
|
51
|
+
var PieceIcon = ({ type, side, moved = false }) => {
|
|
48
52
|
const Icon = PIECE_ICON[type] ?? lucideReact.ShieldQuestion;
|
|
49
53
|
const style = {
|
|
50
54
|
width: "clamp(16px, 7.4cqw, 44px)",
|
|
51
55
|
height: "clamp(16px, 7.4cqw, 44px)",
|
|
52
|
-
filter: EMBOSS_FILTER[side]
|
|
56
|
+
filter: EMBOSS_FILTER[side],
|
|
57
|
+
...moved ? { color: MOVED_GLYPH_COLOR[side] } : null
|
|
53
58
|
};
|
|
54
59
|
return /* @__PURE__ */ jsxRuntime.jsx(Icon, { style, strokeWidth: 2, "aria-hidden": "true" });
|
|
55
60
|
};
|
|
@@ -93,24 +98,20 @@ var ON_BOARD_CHIP_STYLE = {
|
|
|
93
98
|
].join(", ")
|
|
94
99
|
}
|
|
95
100
|
};
|
|
96
|
-
var MOVED_STYLE = {
|
|
97
|
-
filter: "saturate(0.15) brightness(0.75)"
|
|
98
|
-
};
|
|
99
101
|
function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
|
|
100
102
|
const chipStyle = ON_BOARD_CHIP_STYLE[piece.side];
|
|
101
103
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
102
104
|
"div",
|
|
103
105
|
{
|
|
104
106
|
onClick,
|
|
105
|
-
|
|
107
|
+
"data-moved": piece.hasMovedThisTurn ? "true" : "false",
|
|
106
108
|
className: cn(
|
|
107
109
|
"relative w-[88%] h-[88%] rounded-full cursor-pointer z-10 transition-transform duration-200 ease-out motion-reduce:transition-none",
|
|
108
110
|
// `--primary` was the package's one remaining shadcn token, and
|
|
109
111
|
// CrazyGames had to define it by hand just to make the selection
|
|
110
112
|
// ring visible. It is part of the design system now.
|
|
111
113
|
isSelected && "ring-4 ring-accent-green scale-110 z-30",
|
|
112
|
-
isMoving && "scale-[1.08] z-40"
|
|
113
|
-
piece.hasMovedThisTurn && "opacity-70"
|
|
114
|
+
isMoving && "scale-[1.08] z-40"
|
|
114
115
|
),
|
|
115
116
|
children: [
|
|
116
117
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -132,7 +133,7 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
|
|
|
132
133
|
style: chipStyle,
|
|
133
134
|
className: "absolute inset-0 rounded-full flex items-center justify-center",
|
|
134
135
|
children: [
|
|
135
|
-
/* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side }),
|
|
136
|
+
/* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side, moved: piece.hasMovedThisTurn }),
|
|
136
137
|
hasBall && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full ring-2 ring-orange-400 pointer-events-none" }),
|
|
137
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" })
|
|
138
139
|
]
|
|
@@ -142,6 +143,8 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
|
|
|
142
143
|
}
|
|
143
144
|
);
|
|
144
145
|
}
|
|
146
|
+
var BALL_COLOR = "var(--ball-color, #ffffff)";
|
|
147
|
+
var BALL_SHADE = "color-mix(in srgb, var(--ball-color, #ffffff) 74%, #000000)";
|
|
145
148
|
var Football = ({ className }) => {
|
|
146
149
|
const bodyId = React.useId();
|
|
147
150
|
const hiId = `${bodyId}-h`;
|
|
@@ -155,9 +158,9 @@ var Football = ({ className }) => {
|
|
|
155
158
|
children: [
|
|
156
159
|
/* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
|
|
157
160
|
/* @__PURE__ */ jsxRuntime.jsxs("radialGradient", { id: bodyId, cx: "36%", cy: "32%", r: "68%", children: [
|
|
158
|
-
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: "#
|
|
159
|
-
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "55%", stopColor:
|
|
160
|
-
/* @__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 } })
|
|
161
164
|
] }),
|
|
162
165
|
/* @__PURE__ */ jsxRuntime.jsxs("radialGradient", { id: hiId, cx: "32%", cy: "28%", r: "22%", children: [
|
|
163
166
|
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: "0.85" }),
|
|
@@ -200,6 +203,11 @@ var STATIC_I18N_KEYS = [
|
|
|
200
203
|
"endTurnConfirmDescription",
|
|
201
204
|
"endTurnConfirmYes",
|
|
202
205
|
"endTurnKeepPlaying",
|
|
206
|
+
"hints.dismiss",
|
|
207
|
+
"hints.hasBall",
|
|
208
|
+
"hints.lastActionPoint",
|
|
209
|
+
"hints.selectPiece",
|
|
210
|
+
"hints.start",
|
|
203
211
|
"history.empty",
|
|
204
212
|
"history.goal",
|
|
205
213
|
"history.interception",
|
|
@@ -462,9 +470,10 @@ var ROWS = 12;
|
|
|
462
470
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
463
471
|
var cellX = (x) => `${x * 100}%`;
|
|
464
472
|
var cellY = (y) => `${(ROWS - 1 - y) * 100}%`;
|
|
473
|
+
var TOUCH_CLICK_WINDOW_MS = 800;
|
|
465
474
|
var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
|
|
466
475
|
var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
|
|
467
|
-
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true, className }) {
|
|
476
|
+
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true, forgivingTaps = false, className }) {
|
|
468
477
|
const t = useGameT();
|
|
469
478
|
const boardId = React.useId();
|
|
470
479
|
const prefersReducedMotion = framerMotion.useReducedMotion();
|
|
@@ -508,6 +517,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
508
517
|
setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
|
|
509
518
|
};
|
|
510
519
|
const [hoveredPassAt, setHoveredPassAt] = React.useState(null);
|
|
520
|
+
const lastTouchAtRef = React.useRef(0);
|
|
511
521
|
const [movingIds, setMovingIds] = React.useState(() => /* @__PURE__ */ new Set());
|
|
512
522
|
const setMoving = (id, moving) => setMovingIds((prev) => {
|
|
513
523
|
if (prev.has(id) === moving) return prev;
|
|
@@ -593,11 +603,24 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
593
603
|
const passCarrier = boardState.pieces.find((p) => p.id === selectedPieceId && boardState.ball.holderId === p.id) ?? null;
|
|
594
604
|
const trajectoryPath = hoveredPassAt && passCarrier ? passCarrier.type === "knight" ? [hoveredPassAt] : chessFootballEngine.getPath(passCarrier.pos, hoveredPassAt) : null;
|
|
595
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)));
|
|
596
|
-
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) => {
|
|
597
613
|
if (replay) return;
|
|
598
614
|
setCursor(null);
|
|
599
615
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
600
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
|
+
}
|
|
601
624
|
if (isValidMove && isValidPass) {
|
|
602
625
|
setDisambiguateAt({ x, y });
|
|
603
626
|
return;
|
|
@@ -754,12 +777,13 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
754
777
|
role: "gridcell",
|
|
755
778
|
"aria-label": squareLabel(x, y, pieceAt, isValidMove, isValidPass),
|
|
756
779
|
"aria-selected": !!pieceAt && pieceAt.id === selectedPieceId,
|
|
757
|
-
onClick: () => handleSquareClick(x, y),
|
|
780
|
+
onClick: () => handleSquareClick(x, y, Date.now() - lastTouchAtRef.current < TOUCH_CLICK_WINDOW_MS),
|
|
758
781
|
onMouseEnter: () => {
|
|
759
782
|
if (isValidPass) setHoveredPassAt({ x, y });
|
|
760
783
|
},
|
|
761
784
|
onMouseLeave: () => setHoveredPassAt(null),
|
|
762
785
|
onTouchStart: () => {
|
|
786
|
+
lastTouchAtRef.current = Date.now();
|
|
763
787
|
if (isValidPass) setHoveredPassAt({ x, y });
|
|
764
788
|
},
|
|
765
789
|
className: cn(
|
|
@@ -880,7 +904,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
880
904
|
},
|
|
881
905
|
"aria-label": t("shortcuts.buttonLabel"),
|
|
882
906
|
"aria-expanded": shortcutsOpen,
|
|
883
|
-
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",
|
|
884
908
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
|
|
885
909
|
}
|
|
886
910
|
),
|
|
@@ -1902,7 +1926,7 @@ function SheetHeader({ titleId, onClose }) {
|
|
|
1902
1926
|
"button",
|
|
1903
1927
|
{
|
|
1904
1928
|
onClick: onClose,
|
|
1905
|
-
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",
|
|
1906
1930
|
"aria-label": t("history.close"),
|
|
1907
1931
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14, "aria-hidden": "true" })
|
|
1908
1932
|
}
|
|
@@ -1956,6 +1980,160 @@ function HistoryChip({ lastMove, onClick, className }) {
|
|
|
1956
1980
|
}
|
|
1957
1981
|
);
|
|
1958
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
|
+
}
|
|
1959
2137
|
|
|
1960
2138
|
// src/tokens.ts
|
|
1961
2139
|
var REQUIRED_TOKENS = [
|
|
@@ -1983,22 +2161,28 @@ var REQUIRED_TOKENS = [
|
|
|
1983
2161
|
"--field-frame",
|
|
1984
2162
|
"--move-highlight",
|
|
1985
2163
|
"--pass-highlight",
|
|
1986
|
-
"--last-move-highlight"
|
|
2164
|
+
"--last-move-highlight",
|
|
2165
|
+
"--ball-color"
|
|
1987
2166
|
];
|
|
1988
2167
|
|
|
1989
2168
|
exports.ActionPoints = ActionPoints;
|
|
1990
2169
|
exports.Avatar = Avatar;
|
|
1991
2170
|
exports.AvatarFallback = AvatarFallback;
|
|
1992
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;
|
|
1993
2175
|
exports.BallHolderChip = BallHolderChip;
|
|
1994
2176
|
exports.Button = Button;
|
|
1995
2177
|
exports.ConfirmDialog = ConfirmDialog;
|
|
1996
2178
|
exports.EventToast = EventToast;
|
|
1997
2179
|
exports.Football = Football;
|
|
2180
|
+
exports.GAME_HINT_IDS = GAME_HINT_IDS;
|
|
1998
2181
|
exports.GAME_I18N_KEYS = GAME_I18N_KEYS;
|
|
1999
2182
|
exports.GOAL_AREA_SHADE_CLASS = GOAL_AREA_SHADE_CLASS;
|
|
2000
2183
|
exports.GameBoard = GameBoard;
|
|
2001
2184
|
exports.GameControls = GameControls;
|
|
2185
|
+
exports.GameHints = GameHints;
|
|
2002
2186
|
exports.GameI18nProvider = GameI18nProvider;
|
|
2003
2187
|
exports.GamePiece = GamePiece;
|
|
2004
2188
|
exports.GrassOverlay = GrassOverlay;
|
|
@@ -2019,6 +2203,7 @@ exports.TurnBanner = TurnBanner;
|
|
|
2019
2203
|
exports.cn = cn;
|
|
2020
2204
|
exports.isGoalAreaSquare = isGoalAreaSquare;
|
|
2021
2205
|
exports.pitchSquareClass = pitchSquareClass;
|
|
2206
|
+
exports.useBotTurn = useBotTurn;
|
|
2022
2207
|
exports.useDialogA11y = useDialogA11y;
|
|
2023
2208
|
exports.useGameT = useGameT;
|
|
2024
2209
|
//# sourceMappingURL=index.cjs.map
|