@anweather/plugin-board-kit 1.7.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 +65 -0
- package/dist/TurnClockViews.d.ts +21 -0
- package/dist/TurnClockViews.d.ts.map +1 -0
- package/dist/TurnClockViews.js +48 -0
- package/dist/TurnClockViews.js.map +1 -0
- package/dist/boardShared.d.ts +27 -0
- package/dist/boardShared.d.ts.map +1 -0
- package/dist/boardShared.js +40 -0
- package/dist/boardShared.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/mountReactBoard.d.ts +24 -0
- package/dist/mountReactBoard.d.ts.map +1 -0
- package/dist/mountReactBoard.js +46 -0
- package/dist/mountReactBoard.js.map +1 -0
- package/dist/teamUtils.d.ts +4 -0
- package/dist/teamUtils.d.ts.map +1 -0
- package/dist/teamUtils.js +13 -0
- package/dist/teamUtils.js.map +1 -0
- package/dist/turnClock.d.ts +62 -0
- package/dist/turnClock.d.ts.map +1 -0
- package/dist/turnClock.js +101 -0
- package/dist/turnClock.js.map +1 -0
- package/dist/turnUtils.d.ts +21 -0
- package/dist/turnUtils.d.ts.map +1 -0
- package/dist/turnUtils.js +24 -0
- package/dist/turnUtils.js.map +1 -0
- package/dist/useCanvasCamera.d.ts +54 -0
- package/dist/useCanvasCamera.d.ts.map +1 -0
- package/dist/useCanvasCamera.js +218 -0
- package/dist/useCanvasCamera.js.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @anweather/plugin-board-kit
|
|
2
|
+
|
|
3
|
+
Board-UI primitives for any game board on web-boardgame-platform: a remote
|
|
4
|
+
plugin's frontend bundle (`boardgame-plugins`, `tactics-game-plugin`, the
|
|
5
|
+
platform's own `plugins/tic-tac-toe-remote`) and the native boards still in
|
|
6
|
+
this repo's `frontend/`. One implementation, one package, on the public npm
|
|
7
|
+
registry.
|
|
8
|
+
|
|
9
|
+
**Published with the SDK, at the SDK's version.** `publish-plugin-sdk.yml`
|
|
10
|
+
releases this package together with `@anweather/game-plugin-ssdk` and the
|
|
11
|
+
Kotlin runtime from one `sdk-v*` tag, so a plugin's board kit, SSDK and
|
|
12
|
+
runtime pin all name the same release. See `plugin-protocol/CLAUDE.md`,
|
|
13
|
+
"Publishing for cross-repo plugins".
|
|
14
|
+
|
|
15
|
+
**Why a package and not a copy.** A plugin's frontend bundles its own React
|
|
16
|
+
(nothing marked `external`). `react` and `react-dom` are *peer*
|
|
17
|
+
dependencies here, so the consumer's own `node_modules/react` is the one
|
|
18
|
+
this code binds to: exactly one React instance per bundle. The shell-script
|
|
19
|
+
vendoring this replaced existed to dodge the two-React failure a plain
|
|
20
|
+
cross-directory import produced ("Cannot read properties of null (reading
|
|
21
|
+
'useState')" at runtime), and left eight of ten copies stale.
|
|
22
|
+
|
|
23
|
+
## Contents
|
|
24
|
+
|
|
25
|
+
- `mountReactBoard(Component, { onError?, fallback? })` returns the
|
|
26
|
+
`{ mount, update, unmount }` handle a remote bundle exports across the
|
|
27
|
+
ShadowMount boundary. It wraps the board in an error boundary: a render
|
|
28
|
+
error shows a message instead of a blank rectangle, is reported through
|
|
29
|
+
`onError`, and clears on the next `update()`.
|
|
30
|
+
- `BoardShell` (centered-column layout) and `TurnIdentity` (the "You are
|
|
31
|
+
X" row, with a spectator variant). Inline styles throughout; `TurnIdentity`
|
|
32
|
+
takes a CSS `color`, never a class name, so it renders the same inside a
|
|
33
|
+
shadow root with no Tailwind in reach.
|
|
34
|
+
- `computeIsMyTurn` / `computeIsAwaiting` / `computeIsSpectator` over a
|
|
35
|
+
minimal structural `TurnState` (anything with `lifecycle`,
|
|
36
|
+
`currentTurnParticipantId`, `participants` and optionally
|
|
37
|
+
`awaitingParticipantIds`, `GameView` included). `computeIsAwaiting` is
|
|
38
|
+
what "waiting on me" means during a simultaneous phase.
|
|
39
|
+
- `teamOf` / `teamMembers` for a 4-seat, 2-team, alternating-seat game
|
|
40
|
+
(mirrors platform-domain's `TeamAssignment`).
|
|
41
|
+
- The display half of platform-domain's `TurnClock`: `useClockNow`,
|
|
42
|
+
`useClockTimeout`, `formatClock`, `remainingMs`, `clockReading`, and the
|
|
43
|
+
`ClockBadge` / `StartTurnGate` components. What running out *does* stays
|
|
44
|
+
the engine's call.
|
|
45
|
+
- `useCanvasCamera`: pinch/wheel zoom and two-finger pan for a
|
|
46
|
+
canvas-rendered board, renderer-agnostic (Canvas2D via `applyTransform`,
|
|
47
|
+
or drive a Pixi container from `.zoom`/`.offsetX`/`.offsetY`).
|
|
48
|
+
|
|
49
|
+
Not here on purpose: `nameplateAccent.ts` (this repo's
|
|
50
|
+
`frontend/src/plugins/`) hardcodes per-`gameType` colours across the native
|
|
51
|
+
catalogue, which is platform logic, not a board primitive.
|
|
52
|
+
|
|
53
|
+
## Developing
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
npm ci && npm run build && npm test && npm run typecheck
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`npm test` covers the pure helpers (turn, team and clock arithmetic) with
|
|
60
|
+
`node:test`; the components are typechecked and exercised by their
|
|
61
|
+
consumers' own Playwright suites. To try an unpublished change in a
|
|
62
|
+
consumer, `npm pack` here and install the tarball there; the consumer's
|
|
63
|
+
`package.json` should never point at this directory by path, since a
|
|
64
|
+
`file:` link resolves `react` through this directory's own `node_modules`
|
|
65
|
+
and reintroduces the two-React bundle.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type TurnClockState } from './turnClock';
|
|
2
|
+
/** A participant's clock, compact enough to sit on a scoreboard row. */
|
|
3
|
+
export declare function ClockBadge({ clock, participantId, now, finished }: {
|
|
4
|
+
clock: TurnClockState;
|
|
5
|
+
participantId: string;
|
|
6
|
+
/** From useClockNow. */
|
|
7
|
+
now: number;
|
|
8
|
+
finished: boolean;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
/** Shown in place of whatever the player could plan their move from,
|
|
11
|
+
* until they start their turn. In place of, not over: a blurred board is
|
|
12
|
+
* still in the DOM, and anyone with devtools can read it off the clock.
|
|
13
|
+
* [detail] says what running over will cost, in the game's own terms. */
|
|
14
|
+
export declare function StartTurnGate({ onStart, pending, hidden, detail }: {
|
|
15
|
+
onStart: () => void;
|
|
16
|
+
pending: boolean;
|
|
17
|
+
/** What is being kept out of sight, e.g. "board and rack". */
|
|
18
|
+
hidden: string;
|
|
19
|
+
detail?: string;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=TurnClockViews.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TurnClockViews.d.ts","sourceRoot":"","sources":["../src/TurnClockViews.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAA;AAM5E,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE;IAClE,KAAK,EAAE,cAAc,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,OAAO,CAAA;CAClB,+BAqBA;AAED;;;0EAG0E;AAC1E,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;IAClE,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,+BAwCA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { clockReading, formatClock } from './turnClock';
|
|
3
|
+
// The components half of turnClock.ts. Inline styles only, like
|
|
4
|
+
// BoardShell: this file is vendored into plugin bundles that render
|
|
5
|
+
// inside a ShadowMount with no Tailwind in reach.
|
|
6
|
+
/** A participant's clock, compact enough to sit on a scoreboard row. */
|
|
7
|
+
export function ClockBadge({ clock, participantId, now, finished }) {
|
|
8
|
+
const reading = clockReading(clock, participantId, now);
|
|
9
|
+
const ticking = reading.ticking && !finished;
|
|
10
|
+
return (_jsxs("span", { "data-testid": `turn-clock-${participantId}`, title: reading.countingDown ? 'Time left' : 'Time used', style: {
|
|
11
|
+
fontVariantNumeric: 'tabular-nums',
|
|
12
|
+
fontWeight: 600,
|
|
13
|
+
padding: '1px 6px',
|
|
14
|
+
borderRadius: 4,
|
|
15
|
+
border: `1px solid ${ticking ? '#34d399' : '#334155'}`,
|
|
16
|
+
color: reading.overrun ? '#fb7185' : '#e2e8f0',
|
|
17
|
+
whiteSpace: 'nowrap',
|
|
18
|
+
}, children: [ticking && _jsx("span", { style: { color: '#34d399', marginRight: 4 }, children: "\u25CF" }), formatClock(reading.ms)] }));
|
|
19
|
+
}
|
|
20
|
+
/** Shown in place of whatever the player could plan their move from,
|
|
21
|
+
* until they start their turn. In place of, not over: a blurred board is
|
|
22
|
+
* still in the DOM, and anyone with devtools can read it off the clock.
|
|
23
|
+
* [detail] says what running over will cost, in the game's own terms. */
|
|
24
|
+
export function StartTurnGate({ onStart, pending, hidden, detail }) {
|
|
25
|
+
return (_jsxs("div", { style: {
|
|
26
|
+
display: 'flex',
|
|
27
|
+
flexDirection: 'column',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
gap: 12,
|
|
30
|
+
padding: '40px 16px',
|
|
31
|
+
width: '100%',
|
|
32
|
+
boxSizing: 'border-box',
|
|
33
|
+
borderRadius: 8,
|
|
34
|
+
border: '1px solid #334155',
|
|
35
|
+
background: 'rgba(15, 23, 42, 0.6)',
|
|
36
|
+
textAlign: 'center',
|
|
37
|
+
}, children: [_jsxs("p", { style: { margin: 0, color: '#cbd5e1', fontSize: 14 }, children: ["Your ", hidden, " stay hidden until you start your turn."] }), _jsx("button", { onClick: onStart, disabled: pending, "data-testid": "start-turn", style: {
|
|
38
|
+
padding: '8px 20px',
|
|
39
|
+
borderRadius: 6,
|
|
40
|
+
border: 'none',
|
|
41
|
+
background: '#047857',
|
|
42
|
+
color: 'white',
|
|
43
|
+
fontWeight: 600,
|
|
44
|
+
cursor: pending ? 'not-allowed' : 'pointer',
|
|
45
|
+
opacity: pending ? 0.4 : 1,
|
|
46
|
+
}, children: pending ? 'Starting…' : 'Start my turn' }), _jsxs("p", { style: { margin: 0, color: '#64748b', fontSize: 12 }, children: ["Your clock runs from the moment you press this until you move.", detail ? ` ${detail}` : ''] })] }));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=TurnClockViews.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TurnClockViews.js","sourceRoot":"","sources":["../src/TurnClockViews.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAuB,MAAM,aAAa,CAAA;AAE5E,gEAAgE;AAChE,oEAAoE;AACpE,kDAAkD;AAElD,wEAAwE;AACxE,MAAM,UAAU,UAAU,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,QAAQ,EAM/D;IACC,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,CAAC,CAAA;IACvD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,QAAQ,CAAA;IAC5C,OAAO,CACL,+BACe,cAAc,aAAa,EAAE,EAC1C,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,EACvD,KAAK,EAAE;YACL,kBAAkB,EAAE,cAAc;YAClC,UAAU,EAAE,GAAG;YACf,OAAO,EAAE,SAAS;YAClB,YAAY,EAAE,CAAC;YACf,MAAM,EAAE,aAAa,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE;YACtD,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;YAC9C,UAAU,EAAE,QAAQ;SACrB,aAEA,OAAO,IAAI,eAAM,KAAK,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,EAAE,uBAAU,EACtE,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,IACnB,CACR,CAAA;AACH,CAAC;AAED;;;0EAG0E;AAC1E,MAAM,UAAU,aAAa,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAM/D;IACC,OAAO,CACL,eACE,KAAK,EAAE;YACL,OAAO,EAAE,MAAM;YACf,aAAa,EAAE,QAAQ;YACvB,UAAU,EAAE,QAAQ;YACpB,GAAG,EAAE,EAAE;YACP,OAAO,EAAE,WAAW;YACpB,KAAK,EAAE,MAAM;YACb,SAAS,EAAE,YAAY;YACvB,YAAY,EAAE,CAAC;YACf,MAAM,EAAE,mBAAmB;YAC3B,UAAU,EAAE,uBAAuB;YACnC,SAAS,EAAE,QAAQ;SACpB,aAED,aAAG,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,sBAAQ,MAAM,+CAA4C,EACjH,iBACE,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,OAAO,iBACL,YAAY,EACxB,KAAK,EAAE;oBACL,OAAO,EAAE,UAAU;oBACnB,YAAY,EAAE,CAAC;oBACf,MAAM,EAAE,MAAM;oBACd,UAAU,EAAE,SAAS;oBACrB,KAAK,EAAE,OAAO;oBACd,UAAU,EAAE,GAAG;oBACf,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS;oBAC3C,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;iBAC3B,YAEA,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,GACjC,EACT,aAAG,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,+EACU,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,IACvF,IACA,CACP,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/** Shared outer layout for the simple (non-card) board plugins: a centered
|
|
3
|
+
* column with consistent spacing between the identity row, the board
|
|
4
|
+
* itself, and any status/action elements below it. Inline styles, not
|
|
5
|
+
* Tailwind classNames, like everything in this package: it renders the
|
|
6
|
+
* same inside a remote plugin's ShadowMount shadow root (which the
|
|
7
|
+
* platform's Tailwind stylesheet never reaches) and on a native board. */
|
|
8
|
+
export declare function BoardShell({ children }: {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
/** The "You are <color/symbol>" row shown above most simple boards. A
|
|
12
|
+
* spectator isn't any of the game's sides, so [spectating] swaps in a
|
|
13
|
+
* neutral line instead of the caller's (necessarily wrong, since it
|
|
14
|
+
* defaults to one side or the other) color/symbol guess.
|
|
15
|
+
*
|
|
16
|
+
* [color] is a CSS colour value (`#fb7185`, `rgb(...)`, a variable), never
|
|
17
|
+
* a class name: this renders inline styles, so it looks the same inside
|
|
18
|
+
* a remote plugin's ShadowMount shadow root, which the platform's
|
|
19
|
+
* Tailwind stylesheet never reaches, as it does on a native board. The
|
|
20
|
+
* earlier `colorClass` prop took a Tailwind class and rendered an
|
|
21
|
+
* unstyled line in every remote plugin. */
|
|
22
|
+
export declare function TurnIdentity({ label, color, spectating }: {
|
|
23
|
+
label: ReactNode;
|
|
24
|
+
color?: string;
|
|
25
|
+
spectating?: boolean;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
//# sourceMappingURL=boardShared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boardShared.d.ts","sourceRoot":"","sources":["../src/boardShared.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;;;;;2EAK2E;AAC3E,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAiB/D;AAED;;;;;;;;;;4CAU4C;AAC5C,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,KAAiB,EAAE,UAAkB,EAAE,EAAE;IAC7E,KAAK,EAAE,SAAS,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,+BAUA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Shared outer layout for the simple (non-card) board plugins: a centered
|
|
3
|
+
* column with consistent spacing between the identity row, the board
|
|
4
|
+
* itself, and any status/action elements below it. Inline styles, not
|
|
5
|
+
* Tailwind classNames, like everything in this package: it renders the
|
|
6
|
+
* same inside a remote plugin's ShadowMount shadow root (which the
|
|
7
|
+
* platform's Tailwind stylesheet never reaches) and on a native board. */
|
|
8
|
+
export function BoardShell({ children }) {
|
|
9
|
+
// minWidth: 0 lets this shrink below its content's intrinsic width when
|
|
10
|
+
// the page is narrower than the board (e.g. Words with Nerds' 15-wide
|
|
11
|
+
// grid); without it, a flex item's default auto min-width refuses to
|
|
12
|
+
// shrink, so an oversized board pokes past its container and gets
|
|
13
|
+
// clipped by GamePage's overflow-hidden card instead of scrolling
|
|
14
|
+
// internally.
|
|
15
|
+
// fontFamily set explicitly (not just relying on an inherited page
|
|
16
|
+
// font) since a remote plugin's shadow root inherits nothing from the
|
|
17
|
+
// platform's own stylesheet -- without this, an otherwise-unstyled
|
|
18
|
+
// remote board falls back to the browser's default serif font instead
|
|
19
|
+
// of matching the platform's look.
|
|
20
|
+
return (_jsx("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16, minWidth: 0, width: '100%', fontFamily: 'system-ui, sans-serif' }, children: children }));
|
|
21
|
+
}
|
|
22
|
+
/** The "You are <color/symbol>" row shown above most simple boards. A
|
|
23
|
+
* spectator isn't any of the game's sides, so [spectating] swaps in a
|
|
24
|
+
* neutral line instead of the caller's (necessarily wrong, since it
|
|
25
|
+
* defaults to one side or the other) color/symbol guess.
|
|
26
|
+
*
|
|
27
|
+
* [color] is a CSS colour value (`#fb7185`, `rgb(...)`, a variable), never
|
|
28
|
+
* a class name: this renders inline styles, so it looks the same inside
|
|
29
|
+
* a remote plugin's ShadowMount shadow root, which the platform's
|
|
30
|
+
* Tailwind stylesheet never reaches, as it does on a native board. The
|
|
31
|
+
* earlier `colorClass` prop took a Tailwind class and rendered an
|
|
32
|
+
* unstyled line in every remote plugin. */
|
|
33
|
+
export function TurnIdentity({ label, color = '#ffffff', spectating = false }) {
|
|
34
|
+
const row = { fontSize: 14, lineHeight: '20px', color: '#94a3b8' };
|
|
35
|
+
if (spectating) {
|
|
36
|
+
return _jsx("div", { style: row, children: "\uD83D\uDC40 Spectating" });
|
|
37
|
+
}
|
|
38
|
+
return (_jsxs("div", { style: row, children: ["You are ", _jsx("span", { style: { fontWeight: 700, color }, children: label })] }));
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=boardShared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boardShared.js","sourceRoot":"","sources":["../src/boardShared.tsx"],"names":[],"mappings":";AAEA;;;;;2EAK2E;AAC3E,MAAM,UAAU,UAAU,CAAC,EAAE,QAAQ,EAA2B;IAC9D,wEAAwE;IACxE,sEAAsE;IACtE,qEAAqE;IACrE,kEAAkE;IAClE,kEAAkE;IAClE,cAAc;IACd,mEAAmE;IACnE,sEAAsE;IACtE,mEAAmE;IACnE,sEAAsE;IACtE,mCAAmC;IACnC,OAAO,CACL,cAAK,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,uBAAuB,EAAE,YACrJ,QAAQ,GACL,CACP,CAAA;AACH,CAAC;AAED;;;;;;;;;;4CAU4C;AAC5C,MAAM,UAAU,YAAY,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,SAAS,EAAE,UAAU,GAAG,KAAK,EAI1E;IACC,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAW,CAAA;IAC3E,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,cAAK,KAAK,EAAE,GAAG,wCAAqB,CAAA;IAC7C,CAAC;IACD,OAAO,CACL,eAAK,KAAK,EAAE,GAAG,yBACL,eAAM,KAAK,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,YAAG,KAAK,GAAQ,IAC3D,CACP,CAAA;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { mountReactBoard } from './mountReactBoard';
|
|
2
|
+
export type { RemoteBoardHandle, MountReactBoardOptions } from './mountReactBoard';
|
|
3
|
+
export { BoardShell, TurnIdentity } from './boardShared';
|
|
4
|
+
export { computeIsMyTurn, computeIsAwaiting, computeIsSpectator } from './turnUtils';
|
|
5
|
+
export type { TurnState } from './turnUtils';
|
|
6
|
+
export { teamOf, teamMembers } from './teamUtils';
|
|
7
|
+
export type { TeamId } from './teamUtils';
|
|
8
|
+
export { useClockNow, useClockTimeout, formatClock, liveUsedMs, remainingMs, clockReading } from './turnClock';
|
|
9
|
+
export type { TurnClockState } from './turnClock';
|
|
10
|
+
export { ClockBadge, StartTurnGate } from './TurnClockViews';
|
|
11
|
+
export { useCanvasCamera } from './useCanvasCamera';
|
|
12
|
+
export type { Vec2, UseCanvasCameraOptions, CanvasCamera } from './useCanvasCamera';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAClF,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACpF,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACjD,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC9G,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,IAAI,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @anweather/plugin-board-kit -- everything a game board needs from the
|
|
2
|
+
// platform side, as one package. See README.md for what each piece is and
|
|
3
|
+
// plugin-protocol/CLAUDE.md, "Publishing for cross-repo plugins", for how
|
|
4
|
+
// it is released (one semver with the SDK).
|
|
5
|
+
export { mountReactBoard } from './mountReactBoard';
|
|
6
|
+
export { BoardShell, TurnIdentity } from './boardShared';
|
|
7
|
+
export { computeIsMyTurn, computeIsAwaiting, computeIsSpectator } from './turnUtils';
|
|
8
|
+
export { teamOf, teamMembers } from './teamUtils';
|
|
9
|
+
export { useClockNow, useClockTimeout, formatClock, liveUsedMs, remainingMs, clockReading } from './turnClock';
|
|
10
|
+
export { ClockBadge, StartTurnGate } from './TurnClockViews';
|
|
11
|
+
export { useCanvasCamera } from './useCanvasCamera';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,0EAA0E;AAC1E,0EAA0E;AAC1E,4CAA4C;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAEpF,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEjD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE9G,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ComponentType, type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
+
/** The imperative mount/update/unmount contract every remote plugin's
|
|
3
|
+
* frontend bundle exports across the ShadowMount boundary (see
|
|
4
|
+
* web-boardgame-platform's plugins/CLAUDE.md "Frontend bundle" section
|
|
5
|
+
* for why it's imperative rather than a shared React tree). Single
|
|
6
|
+
* module-level root: fine for one game page mounted at a time; a second
|
|
7
|
+
* concurrent instance would need its own handle instead of sharing this
|
|
8
|
+
* closure's state. */
|
|
9
|
+
export interface RemoteBoardHandle<Props> {
|
|
10
|
+
mount(container: HTMLElement, props: Props): void;
|
|
11
|
+
update(props: Props): void;
|
|
12
|
+
unmount(): void;
|
|
13
|
+
}
|
|
14
|
+
export interface MountReactBoardOptions {
|
|
15
|
+
/** Called once per render error the board throws. The platform shell
|
|
16
|
+
* never sees an exception from inside a ShadowMount, so without this a
|
|
17
|
+
* crashed board is a blank rectangle and nothing else. */
|
|
18
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
19
|
+
/** What to show in place of a board that threw. Defaults to a plain
|
|
20
|
+
* message naming the error. */
|
|
21
|
+
fallback?: (error: Error) => ReactNode;
|
|
22
|
+
}
|
|
23
|
+
export declare function mountReactBoard<Props extends object>(BoardComponent: ComponentType<Props>, options?: MountReactBoardOptions): RemoteBoardHandle<Props>;
|
|
24
|
+
//# sourceMappingURL=mountReactBoard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mountReactBoard.d.ts","sourceRoot":"","sources":["../src/mountReactBoard.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAGrF;;;;;;uBAMuB;AACvB,MAAM,WAAW,iBAAiB,CAAC,KAAK;IACtC,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACjD,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IAC1B,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC;;+DAE2D;IAC3D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;IACjD;oCACgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,SAAS,CAAA;CACvC;AAgDD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAClD,cAAc,EAAE,aAAa,CAAC,KAAK,CAAC,EACpC,OAAO,GAAE,sBAA2B,GACnC,iBAAiB,CAAC,KAAK,CAAC,CA0B1B"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
/** A board that throws during render must not take the whole game page
|
|
5
|
+
* with it, and must recover when the next view arrives: ShadowMount has
|
|
6
|
+
* no boundary of its own on the far side of the mount() call. */
|
|
7
|
+
class BoardErrorBoundary extends Component {
|
|
8
|
+
state = { error: null, attempt: 0 };
|
|
9
|
+
static getDerivedStateFromError(error) {
|
|
10
|
+
return { error };
|
|
11
|
+
}
|
|
12
|
+
static getDerivedStateFromProps(props, state) {
|
|
13
|
+
// A new attempt (an update() with fresh props) clears the last error.
|
|
14
|
+
return props.attempt !== state.attempt ? { error: null, attempt: props.attempt } : null;
|
|
15
|
+
}
|
|
16
|
+
componentDidCatch(error, info) {
|
|
17
|
+
this.props.onError?.(error, info);
|
|
18
|
+
}
|
|
19
|
+
render() {
|
|
20
|
+
return this.state.error ? this.props.fallback(this.state.error) : this.props.children;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function defaultFallback(error) {
|
|
24
|
+
return (_jsxs("div", { role: "alert", style: { padding: 16, fontFamily: 'system-ui, sans-serif', color: '#fb7185' }, children: ["This board hit an error and could not draw itself: ", error.message] }));
|
|
25
|
+
}
|
|
26
|
+
export function mountReactBoard(BoardComponent, options = {}) {
|
|
27
|
+
let root = null;
|
|
28
|
+
let attempt = 0;
|
|
29
|
+
const fallback = options.fallback ?? defaultFallback;
|
|
30
|
+
const render = (props) => root?.render(_jsx(BoardErrorBoundary, { onError: options.onError, fallback: fallback, attempt: attempt, children: _jsx(BoardComponent, { ...props }) }));
|
|
31
|
+
return {
|
|
32
|
+
mount(container, props) {
|
|
33
|
+
root = createRoot(container);
|
|
34
|
+
render(props);
|
|
35
|
+
},
|
|
36
|
+
update(props) {
|
|
37
|
+
attempt += 1;
|
|
38
|
+
render(props);
|
|
39
|
+
},
|
|
40
|
+
unmount() {
|
|
41
|
+
root?.unmount();
|
|
42
|
+
root = null;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=mountReactBoard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mountReactBoard.js","sourceRoot":"","sources":["../src/mountReactBoard.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAsD,MAAM,OAAO,CAAA;AACrF,OAAO,EAAE,UAAU,EAAa,MAAM,kBAAkB,CAAA;AAuCxD;;kEAEkE;AAClE,MAAM,kBAAmB,SAAQ,SAAuC;IACtE,KAAK,GAAkB,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAA;IAElD,MAAM,CAAC,wBAAwB,CAAC,KAAY;QAC1C,OAAO,EAAE,KAAK,EAAE,CAAA;IAClB,CAAC;IAED,MAAM,CAAC,wBAAwB,CAAC,KAAoB,EAAE,KAAoB;QACxE,sEAAsE;QACtE,OAAO,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IACzF,CAAC;IAED,iBAAiB,CAAC,KAAY,EAAE,IAAe;QAC7C,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACnC,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAA;IACvF,CAAC;CACF;AAED,SAAS,eAAe,CAAC,KAAY;IACnC,OAAO,CACL,eAAK,IAAI,EAAC,OAAO,EAAC,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,uBAAuB,EAAE,KAAK,EAAE,SAAS,EAAE,oEACzC,KAAK,CAAC,OAAO,IAC7D,CACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,cAAoC,EACpC,UAAkC,EAAE;IAEpC,IAAI,IAAI,GAAgB,IAAI,CAAA;IAC5B,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,eAAe,CAAA;IAEpD,MAAM,MAAM,GAAG,CAAC,KAAY,EAAE,EAAE,CAC9B,IAAI,EAAE,MAAM,CACV,KAAC,kBAAkB,IAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,YAChF,KAAC,cAAc,OAAK,KAAK,GAAI,GACV,CACtB,CAAA;IAEH,OAAO;QACL,KAAK,CAAC,SAAS,EAAE,KAAK;YACpB,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,CAAA;YAC5B,MAAM,CAAC,KAAK,CAAC,CAAA;QACf,CAAC;QACD,MAAM,CAAC,KAAK;YACV,OAAO,IAAI,CAAC,CAAA;YACZ,MAAM,CAAC,KAAK,CAAC,CAAA;QACf,CAAC;QACD,OAAO;YACL,IAAI,EAAE,OAAO,EAAE,CAAA;YACf,IAAI,GAAG,IAAI,CAAA;QACb,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teamUtils.d.ts","sourceRoot":"","sources":["../src/teamUtils.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,CAAA;AAE9B,wBAAgB,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvE"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Seat-parity team assignment for a 4-seat, 2-team game where seats
|
|
2
|
+
// alternate turn by turn and every other seat shares a team (seats 0/2 vs.
|
|
3
|
+
// seats 1/3) — mirrors the backend's TeamAssignment
|
|
4
|
+
// (platform-domain/.../model/TeamAssignment.kt) exactly. Any board for a
|
|
5
|
+
// game type with GameTypeMetadataView.teamPlay === true can use this
|
|
6
|
+
// instead of re-deriving team membership from turnOrder itself.
|
|
7
|
+
export function teamOf(turnOrder, participantId) {
|
|
8
|
+
return turnOrder.indexOf(participantId) % 2 === 0 ? '1' : '2';
|
|
9
|
+
}
|
|
10
|
+
export function teamMembers(turnOrder, team) {
|
|
11
|
+
return turnOrder.filter((_, idx) => (idx % 2 === 0) === (team === '1'));
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=teamUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teamUtils.js","sourceRoot":"","sources":["../src/teamUtils.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,2EAA2E;AAC3E,oDAAoD;AACpD,yEAAyE;AACzE,qEAAqE;AACrE,gEAAgE;AAIhE,MAAM,UAAU,MAAM,CAAC,SAAmB,EAAE,aAAqB;IAC/D,OAAO,SAAS,CAAC,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;AAC/D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,SAAmB,EAAE,IAAY;IAC3D,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAA;AACzE,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** TurnClock.toMap(), as it arrives in a board's pluginState. */
|
|
2
|
+
export interface TurnClockState {
|
|
3
|
+
mode: 'count-up' | 'per-turn' | 'game-clock';
|
|
4
|
+
turnLimitMs: number;
|
|
5
|
+
poolMs: number;
|
|
6
|
+
incrementMs: number;
|
|
7
|
+
usedMs: Record<string, number>;
|
|
8
|
+
runningFor: string | null;
|
|
9
|
+
startedAt: number | null;
|
|
10
|
+
turnLog: {
|
|
11
|
+
participantId: string;
|
|
12
|
+
ms: number;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
/** The current time as the server sees it, for reading a clock against.
|
|
16
|
+
* Re-renders once a second while [running], so a live clock keeps moving
|
|
17
|
+
* between polls, and sits idle whenever nobody's turn is started, which
|
|
18
|
+
* in an async game is most of the time.
|
|
19
|
+
*
|
|
20
|
+
* Corrected by `serverNow`, a stamp the engine puts in its view: the
|
|
21
|
+
* difference from this browser's clock, measured as the view arrives, is
|
|
22
|
+
* added to every reading. A phone whose clock is a few seconds out then
|
|
23
|
+
* neither fires a timeout the server will refuse nor sits past one it
|
|
24
|
+
* would accept. The measurement absorbs the response's own latency too,
|
|
25
|
+
* which is close enough for a clock shown to the second. */
|
|
26
|
+
export declare function useClockNow(serverNow: number | undefined, running: boolean): number;
|
|
27
|
+
/** m:ss, h:mm:ss past an hour, and a leading minus once a count-down
|
|
28
|
+
* clock is overrun (a game clock plays on into overtime). */
|
|
29
|
+
export declare function formatClock(ms: number): string;
|
|
30
|
+
/** Time spent, including the turn in progress if it is theirs. */
|
|
31
|
+
export declare function liveUsedMs(clock: TurnClockState, participantId: string, now: number): number;
|
|
32
|
+
/** Mirrors TurnClock.remainingMs: the rest of this turn (per-turn), the
|
|
33
|
+
* rest of the game budget (game-clock), negative once overrun; null for
|
|
34
|
+
* count-up, which has no limit. */
|
|
35
|
+
export declare function remainingMs(clock: TurnClockState, participantId: string, now: number): number | null;
|
|
36
|
+
/** What to show for one participant: time left under a count-down mode,
|
|
37
|
+
* time used under count-up. */
|
|
38
|
+
export declare function clockReading(clock: TurnClockState, participantId: string, now: number): {
|
|
39
|
+
ms: number;
|
|
40
|
+
countingDown: boolean;
|
|
41
|
+
overrun: boolean;
|
|
42
|
+
ticking: boolean;
|
|
43
|
+
};
|
|
44
|
+
/** Sends [onTimeout] once when [participantId]'s own count-down clock
|
|
45
|
+
* reaches zero, for the modes listed in [firesOn] -- what running out
|
|
46
|
+
* means is the game's call, so which modes have a timeout move at all is
|
|
47
|
+
* too. It has to be the player's own board that asks: a timeout is an
|
|
48
|
+
* ordinary turn-gated move, and only the player to move can submit one.
|
|
49
|
+
* A player who walked away mid-turn has it land when they come back,
|
|
50
|
+
* since their clock kept running. The server re-checks the clock and
|
|
51
|
+
* refuses an early one, in which case this asks again on a later tick
|
|
52
|
+
* rather than wedging. [now] comes from useClockNow. */
|
|
53
|
+
export declare function useClockTimeout({ clock, participantId, active, firesOn, now, onTimeout, }: {
|
|
54
|
+
clock: TurnClockState | null | undefined;
|
|
55
|
+
participantId: string;
|
|
56
|
+
/** False for anyone who isn't the player to move, or once the game is over. */
|
|
57
|
+
active: boolean;
|
|
58
|
+
firesOn: TurnClockState['mode'][];
|
|
59
|
+
now: number;
|
|
60
|
+
onTimeout: () => Promise<unknown>;
|
|
61
|
+
}): void;
|
|
62
|
+
//# sourceMappingURL=turnClock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnClock.d.ts","sourceRoot":"","sources":["../src/turnClock.ts"],"names":[],"mappings":"AASA,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,YAAY,CAAA;IAC5C,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACjD;AAED;;;;;;;;;;6DAU6D;AAC7D,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAgBnF;AAED;8DAC8D;AAC9D,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAQ9C;AAMD,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE5F;AAED;;oCAEoC;AACpC,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWpG;AAED;gCACgC;AAChC,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;;;;;EAQrF;AAED;;;;;;;;yDAQyD;AACzD,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,aAAa,EACb,MAAM,EACN,OAAO,EACP,GAAG,EACH,SAAS,GACV,EAAE;IACD,KAAK,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,CAAA;IACxC,aAAa,EAAE,MAAM,CAAA;IACrB,+EAA+E;IAC/E,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,cAAc,CAAC,MAAM,CAAC,EAAE,CAAA;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAClC,GAAG,IAAI,CAeP"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
/** The current time as the server sees it, for reading a clock against.
|
|
3
|
+
* Re-renders once a second while [running], so a live clock keeps moving
|
|
4
|
+
* between polls, and sits idle whenever nobody's turn is started, which
|
|
5
|
+
* in an async game is most of the time.
|
|
6
|
+
*
|
|
7
|
+
* Corrected by `serverNow`, a stamp the engine puts in its view: the
|
|
8
|
+
* difference from this browser's clock, measured as the view arrives, is
|
|
9
|
+
* added to every reading. A phone whose clock is a few seconds out then
|
|
10
|
+
* neither fires a timeout the server will refuse nor sits past one it
|
|
11
|
+
* would accept. The measurement absorbs the response's own latency too,
|
|
12
|
+
* which is close enough for a clock shown to the second. */
|
|
13
|
+
export function useClockNow(serverNow, running) {
|
|
14
|
+
const offset = useRef({ ms: 0 });
|
|
15
|
+
const [now, setNow] = useState(() => Date.now());
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (serverNow != null && offset.current.stamp !== serverNow) {
|
|
18
|
+
offset.current = { stamp: serverNow, ms: serverNow - Date.now() };
|
|
19
|
+
}
|
|
20
|
+
const tick = () => setNow(Date.now() + offset.current.ms);
|
|
21
|
+
const first = setTimeout(tick, 0);
|
|
22
|
+
const every = running ? setInterval(tick, 1000) : undefined;
|
|
23
|
+
return () => {
|
|
24
|
+
clearTimeout(first);
|
|
25
|
+
if (every)
|
|
26
|
+
clearInterval(every);
|
|
27
|
+
};
|
|
28
|
+
}, [serverNow, running]);
|
|
29
|
+
return now;
|
|
30
|
+
}
|
|
31
|
+
/** m:ss, h:mm:ss past an hour, and a leading minus once a count-down
|
|
32
|
+
* clock is overrun (a game clock plays on into overtime). */
|
|
33
|
+
export function formatClock(ms) {
|
|
34
|
+
const sign = ms < 0 ? '-' : '';
|
|
35
|
+
const total = Math.floor(Math.abs(ms) / 1000);
|
|
36
|
+
const hours = Math.floor(total / 3600);
|
|
37
|
+
const mins = Math.floor((total % 3600) / 60);
|
|
38
|
+
const secs = total % 60;
|
|
39
|
+
const mm = hours > 0 ? String(mins).padStart(2, '0') : String(mins);
|
|
40
|
+
return `${sign}${hours > 0 ? `${hours}:` : ''}${mm}:${String(secs).padStart(2, '0')}`;
|
|
41
|
+
}
|
|
42
|
+
function currentTurnMs(clock, participantId, now) {
|
|
43
|
+
return clock.runningFor === participantId && clock.startedAt != null ? Math.max(0, now - clock.startedAt) : 0;
|
|
44
|
+
}
|
|
45
|
+
/** Time spent, including the turn in progress if it is theirs. */
|
|
46
|
+
export function liveUsedMs(clock, participantId, now) {
|
|
47
|
+
return (clock.usedMs[participantId] ?? 0) + currentTurnMs(clock, participantId, now);
|
|
48
|
+
}
|
|
49
|
+
/** Mirrors TurnClock.remainingMs: the rest of this turn (per-turn), the
|
|
50
|
+
* rest of the game budget (game-clock), negative once overrun; null for
|
|
51
|
+
* count-up, which has no limit. */
|
|
52
|
+
export function remainingMs(clock, participantId, now) {
|
|
53
|
+
switch (clock.mode) {
|
|
54
|
+
case 'per-turn':
|
|
55
|
+
return clock.turnLimitMs - currentTurnMs(clock, participantId, now);
|
|
56
|
+
case 'game-clock': {
|
|
57
|
+
const completed = clock.turnLog.filter(t => t.participantId === participantId).length;
|
|
58
|
+
return clock.poolMs + clock.incrementMs * completed - liveUsedMs(clock, participantId, now);
|
|
59
|
+
}
|
|
60
|
+
default:
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** What to show for one participant: time left under a count-down mode,
|
|
65
|
+
* time used under count-up. */
|
|
66
|
+
export function clockReading(clock, participantId, now) {
|
|
67
|
+
const left = remainingMs(clock, participantId, now);
|
|
68
|
+
return {
|
|
69
|
+
ms: left ?? liveUsedMs(clock, participantId, now),
|
|
70
|
+
countingDown: left != null,
|
|
71
|
+
overrun: left != null && left <= 0,
|
|
72
|
+
ticking: clock.runningFor === participantId && clock.startedAt != null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Sends [onTimeout] once when [participantId]'s own count-down clock
|
|
76
|
+
* reaches zero, for the modes listed in [firesOn] -- what running out
|
|
77
|
+
* means is the game's call, so which modes have a timeout move at all is
|
|
78
|
+
* too. It has to be the player's own board that asks: a timeout is an
|
|
79
|
+
* ordinary turn-gated move, and only the player to move can submit one.
|
|
80
|
+
* A player who walked away mid-turn has it land when they come back,
|
|
81
|
+
* since their clock kept running. The server re-checks the clock and
|
|
82
|
+
* refuses an early one, in which case this asks again on a later tick
|
|
83
|
+
* rather than wedging. [now] comes from useClockNow. */
|
|
84
|
+
export function useClockTimeout({ clock, participantId, active, firesOn, now, onTimeout, }) {
|
|
85
|
+
const watching = !!clock && active && firesOn.includes(clock.mode) && clock.runningFor === participantId;
|
|
86
|
+
const left = watching ? remainingMs(clock, participantId, now) : null;
|
|
87
|
+
const out = left != null && left <= 0;
|
|
88
|
+
// Which turn a timeout was already sent for, keyed by its start time,
|
|
89
|
+
// so each turn asks once and a new turn can time out again.
|
|
90
|
+
const firedFor = useRef(null);
|
|
91
|
+
const startedAt = clock?.startedAt ?? null;
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!out || startedAt == null || firedFor.current === startedAt)
|
|
94
|
+
return;
|
|
95
|
+
firedFor.current = startedAt;
|
|
96
|
+
onTimeout().catch(() => {
|
|
97
|
+
firedFor.current = null;
|
|
98
|
+
});
|
|
99
|
+
}, [out, startedAt, now, onTimeout]);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=turnClock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnClock.js","sourceRoot":"","sources":["../src/turnClock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface TurnState {
|
|
2
|
+
lifecycle: string;
|
|
3
|
+
currentTurnParticipantId: string | null;
|
|
4
|
+
awaitingParticipantIds?: string[];
|
|
5
|
+
participants: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Whether this participant may make an ordinary move -- the turn holder.
|
|
8
|
+
* A simultaneous-phase choice is gated by the board's own phase state
|
|
9
|
+
* instead; for "is the game waiting on me", use computeIsAwaiting. */
|
|
10
|
+
export declare function computeIsMyTurn(gameView: TurnState, myParticipantId: string): boolean;
|
|
11
|
+
/** Whether the game is waiting on this participant to act -- the turn
|
|
12
|
+
* holder, or anyone yet to submit during a simultaneous phase (a
|
|
13
|
+
* five-card selection, a fleet placement), which may not include the
|
|
14
|
+
* turn holder at all. What "your turn" should mean to a person. */
|
|
15
|
+
export declare function computeIsAwaiting(gameView: TurnState, participantId: string): boolean;
|
|
16
|
+
/** True once a game is loaded read-only by a viewer who isn't one of its
|
|
17
|
+
* participants (spectator mode) — the viewer's own id never appears in
|
|
18
|
+
* `participants`, so every "am I white/red/X" ternary in a board falls
|
|
19
|
+
* through to its else-branch by default unless it checks this first. */
|
|
20
|
+
export declare function computeIsSpectator(gameView: TurnState, myParticipantId: string): boolean;
|
|
21
|
+
//# sourceMappingURL=turnUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnUtils.d.ts","sourceRoot":"","sources":["../src/turnUtils.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,wBAAwB,EAAE,MAAM,GAAG,IAAI,CAAA;IAMvC,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAA;IACjC,YAAY,EAAE,MAAM,EAAE,CAAA;CACvB;AAED;;uEAEuE;AACvE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAErF;AAED;;;oEAGoE;AACpE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAIrF;AAED;;;yEAGyE;AACzE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAExF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Whether this participant may make an ordinary move -- the turn holder.
|
|
2
|
+
* A simultaneous-phase choice is gated by the board's own phase state
|
|
3
|
+
* instead; for "is the game waiting on me", use computeIsAwaiting. */
|
|
4
|
+
export function computeIsMyTurn(gameView, myParticipantId) {
|
|
5
|
+
return gameView.lifecycle === 'ACTIVE' && gameView.currentTurnParticipantId === myParticipantId;
|
|
6
|
+
}
|
|
7
|
+
/** Whether the game is waiting on this participant to act -- the turn
|
|
8
|
+
* holder, or anyone yet to submit during a simultaneous phase (a
|
|
9
|
+
* five-card selection, a fleet placement), which may not include the
|
|
10
|
+
* turn holder at all. What "your turn" should mean to a person. */
|
|
11
|
+
export function computeIsAwaiting(gameView, participantId) {
|
|
12
|
+
if (gameView.lifecycle !== 'ACTIVE')
|
|
13
|
+
return false;
|
|
14
|
+
const awaiting = gameView.awaitingParticipantIds;
|
|
15
|
+
return awaiting ? awaiting.includes(participantId) : gameView.currentTurnParticipantId === participantId;
|
|
16
|
+
}
|
|
17
|
+
/** True once a game is loaded read-only by a viewer who isn't one of its
|
|
18
|
+
* participants (spectator mode) — the viewer's own id never appears in
|
|
19
|
+
* `participants`, so every "am I white/red/X" ternary in a board falls
|
|
20
|
+
* through to its else-branch by default unless it checks this first. */
|
|
21
|
+
export function computeIsSpectator(gameView, myParticipantId) {
|
|
22
|
+
return !gameView.participants.includes(myParticipantId);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=turnUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnUtils.js","sourceRoot":"","sources":["../src/turnUtils.ts"],"names":[],"mappings":"AAgBA;;uEAEuE;AACvE,MAAM,UAAU,eAAe,CAAC,QAAmB,EAAE,eAAuB;IAC1E,OAAO,QAAQ,CAAC,SAAS,KAAK,QAAQ,IAAI,QAAQ,CAAC,wBAAwB,KAAK,eAAe,CAAA;AACjG,CAAC;AAED;;;oEAGoE;AACpE,MAAM,UAAU,iBAAiB,CAAC,QAAmB,EAAE,aAAqB;IAC1E,IAAI,QAAQ,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACjD,MAAM,QAAQ,GAAG,QAAQ,CAAC,sBAAsB,CAAA;IAChD,OAAO,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,wBAAwB,KAAK,aAAa,CAAA;AAC1G,CAAC;AAED;;;yEAGyE;AACzE,MAAM,UAAU,kBAAkB,CAAC,QAAmB,EAAE,eAAuB;IAC7E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAA;AACzD,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
export interface Vec2 {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
export interface UseCanvasCameraOptions {
|
|
7
|
+
canvasRef: RefObject<HTMLCanvasElement | null>;
|
|
8
|
+
/** World-space dimensions of the whole board, in whatever units your draw code already uses (e.g. terrain width/VIEW_HEIGHT). Camera state is expressed in these same units. */
|
|
9
|
+
worldWidth: number;
|
|
10
|
+
worldHeight: number;
|
|
11
|
+
/** Floor for zoom -- 1 means "the whole board fits exactly", matching a canvas whose CSS aspect-ratio already locks to worldWidth/worldHeight (no letterboxing to derive a floor from, unlike Pocket Tactics' own viewport-relative minZoom). Pass a smaller value if your canvas's aspect ratio can differ from the world's. */
|
|
12
|
+
minZoom?: number;
|
|
13
|
+
/** Ceiling is minZoom * this multiplier. */
|
|
14
|
+
maxZoomMultiplier?: number;
|
|
15
|
+
/** Zoom the camera starts at, clamped into [minZoom, maxZoom]. Defaults
|
|
16
|
+
* to minZoom (the whole board). Set this above minZoom for a board that
|
|
17
|
+
* is unreadable when fully fitted -- risk-remote's world map on a phone,
|
|
18
|
+
* where "fit everything" and "legible" are mutually exclusive -- so it
|
|
19
|
+
* opens on a usable crop the player can still zoom out of. Read ONCE, on
|
|
20
|
+
* the hook's first render: later changes never yank the camera out from
|
|
21
|
+
* under someone mid-game, so a consumer whose value depends on measured
|
|
22
|
+
* layout should mount this hook only once that measurement exists (see
|
|
23
|
+
* that plugin's MapCanvas, split into a measuring shell and a surface
|
|
24
|
+
* keyed on the layout it was built for). */
|
|
25
|
+
initialZoom?: number;
|
|
26
|
+
/** true only for a board whose own single-pointer interaction is tap/click, never drag -- see this file's header comment. Default false. */
|
|
27
|
+
singlePointerPan?: boolean;
|
|
28
|
+
/** Your board's own single-pointer game-interaction callbacks, in world coordinates. Never called for a pointer sequence the camera has claimed (2+ simultaneous pointers, or any single pointer at all when singlePointerPan is true). */
|
|
29
|
+
onPointerDown?: (p: Vec2, e: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
30
|
+
onPointerMove?: (p: Vec2, e: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
31
|
+
onPointerUp?: (p: Vec2, e: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
32
|
+
onPointerCancel?: (e: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface CanvasCamera {
|
|
35
|
+
zoom: number;
|
|
36
|
+
/** World coordinate that appears at the canvas's own top-left corner. */
|
|
37
|
+
offsetX: number;
|
|
38
|
+
offsetY: number;
|
|
39
|
+
/** Call once per draw, right after any device-pixel-ratio ctx.setTransform -- everything drawn after this in plain world coordinates comes out correctly zoomed/panned, no draw function needs to know the camera exists. */
|
|
40
|
+
applyTransform: (ctx: CanvasRenderingContext2D) => void;
|
|
41
|
+
/** clientX/clientY (from any PointerEvent/MouseEvent/WheelEvent) -> world coordinates, accounting for the current camera. */
|
|
42
|
+
toWorld: (clientX: number, clientY: number) => Vec2;
|
|
43
|
+
/** Recenters the viewport on a world point at the *current* zoom (doesn't change zoom) -- e.g. snapping to an active unit, or following a projectile frame by frame as its replay animates. Clamped the same as any other pan. */
|
|
44
|
+
centerOn: (worldX: number, worldY: number) => void;
|
|
45
|
+
/** Spread onto your <canvas>. Wraps your own onPointerDown/Move/Up/Cancel (from the options above) with pinch/pan gesture detection. */
|
|
46
|
+
canvasProps: {
|
|
47
|
+
onPointerDown: (e: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
48
|
+
onPointerMove: (e: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
49
|
+
onPointerUp: (e: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
50
|
+
onPointerCancel: (e: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export declare function useCanvasCamera({ canvasRef, worldWidth, worldHeight, minZoom, maxZoomMultiplier, initialZoom, singlePointerPan, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, }: UseCanvasCameraOptions): CanvasCamera;
|
|
54
|
+
//# sourceMappingURL=useCanvasCamera.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCanvasCamera.d.ts","sourceRoot":"","sources":["../src/useCanvasCamera.ts"],"names":[],"mappings":"AA8BA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEnE,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAA;IAC9C,gLAAgL;IAChL,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iUAAiU;IACjU,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4CAA4C;IAC5C,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;;;;;;iDAS6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4IAA4I;IAC5I,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,2OAA2O;IAC3O,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IAC3E,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IAC3E,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IACzE,eAAe,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;CACrE;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,6NAA6N;IAC7N,cAAc,EAAE,CAAC,GAAG,EAAE,wBAAwB,KAAK,IAAI,CAAA;IACvD,6HAA6H;IAC7H,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACnD,kOAAkO;IAClO,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,wIAAwI;IACxI,WAAW,EAAE;QACX,aAAa,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;QACjE,aAAa,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;QACjE,WAAW,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;QAC/D,eAAe,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;KACpE,CAAA;CACF;AAMD,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,UAAU,EACV,WAAW,EACX,OAAW,EACX,iBAAqB,EACrB,WAAW,EACX,gBAAwB,EACxB,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,GAChB,EAAE,sBAAsB,GAAG,YAAY,CA2LvC"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Pinch-to-zoom + wheel-to-zoom + two-finger-pan for a `<canvas>`-rendered
|
|
2
|
+
// game board, factored out of zephyrs-remote/frontend after pocket-tactics-
|
|
3
|
+
// remote (a different repo, PixiJS-based) and zephyrs-remote (raw Canvas2D)
|
|
4
|
+
// independently solved the exact same "zoom and pan a canvas inside a
|
|
5
|
+
// ShadowMount" problem. Renderer-agnostic on purpose: this hook only
|
|
6
|
+
// tracks pointer/wheel gestures and produces a zoom/offset camera state,
|
|
7
|
+
// via applyTransform's `ctx.scale`+`ctx.translate` convenience for a raw
|
|
8
|
+
// Canvas2D board (Zephyrs' case) or by reading `.zoom`/`.offsetX`/
|
|
9
|
+
// `.offsetY` directly and driving a Pixi Container's own scale/position
|
|
10
|
+
// instead (Pocket Tactics' case) -- it never touches the DOM/canvas
|
|
11
|
+
// beyond attaching listeners and (optionally) calling ctx methods you
|
|
12
|
+
// hand it.
|
|
13
|
+
//
|
|
14
|
+
// Part of @anweather/plugin-board-kit: a consumer imports it from the
|
|
15
|
+
// package, whose `react` is a peer dependency resolved from the
|
|
16
|
+
// consumer's own node_modules -- one React instance per bundle. (This
|
|
17
|
+
// file used to be vendored by a shell script for exactly that reason: a
|
|
18
|
+
// plain relative import out of one project into another resolved `react`
|
|
19
|
+
// against whichever node_modules sat above the file, built cleanly, and
|
|
20
|
+
// then threw "Cannot read properties of null (reading 'useState')" at
|
|
21
|
+
// runtime from two React instances in one bundle.)
|
|
22
|
+
//
|
|
23
|
+
// Why this can't just be "one finger pans, like Pocket Tactics does": that
|
|
24
|
+
// design assumes single-pointer drag is free for the camera to claim.
|
|
25
|
+
// It isn't, the moment a game's own primary interaction is also a
|
|
26
|
+
// single-pointer drag (Zephyrs' drag-to-aim) -- see singlePointerPan
|
|
27
|
+
// below. Default to false; only set it true for a board whose own
|
|
28
|
+
// pointerDown/Move/Up callbacks are tap/click-only (Pocket Tactics'
|
|
29
|
+
// select/move/attack are all taps), never for one that uses drag itself.
|
|
30
|
+
import { useEffect, useRef, useState } from 'react';
|
|
31
|
+
function clamp(value, min, max) {
|
|
32
|
+
return Math.min(max, Math.max(min, value));
|
|
33
|
+
}
|
|
34
|
+
export function useCanvasCamera({ canvasRef, worldWidth, worldHeight, minZoom = 1, maxZoomMultiplier = 3, initialZoom, singlePointerPan = false, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, }) {
|
|
35
|
+
const maxZoom = minZoom * maxZoomMultiplier;
|
|
36
|
+
// Math.max guards the degenerate maxZoomMultiplier < 1 (a ceiling below
|
|
37
|
+
// the floor): without it, clamp would return the ceiling and this would
|
|
38
|
+
// be the one input shape whose starting zoom changed when initialZoom was
|
|
39
|
+
// added. No consumer configures that, but it costs nothing to keep this
|
|
40
|
+
// strictly identical to the old `useState(minZoom)` for every caller that
|
|
41
|
+
// doesn't pass initialZoom.
|
|
42
|
+
const [zoom, setZoom] = useState(() => clamp(initialZoom ?? minZoom, minZoom, Math.max(minZoom, minZoom * maxZoomMultiplier)));
|
|
43
|
+
const [offsetX, setOffsetX] = useState(0);
|
|
44
|
+
const [offsetY, setOffsetY] = useState(0);
|
|
45
|
+
// Gesture bookkeeping -- refs, not state, since none of this needs to
|
|
46
|
+
// trigger a re-render on its own (zoom/offsetX/offsetY already do).
|
|
47
|
+
const pointersRef = useRef(new Map());
|
|
48
|
+
const pinchRef = useRef(null);
|
|
49
|
+
// Set the moment a second pointer joins (or, with singlePointerPan, the
|
|
50
|
+
// moment any pointer goes down) -- cleared only once every pointer has
|
|
51
|
+
// lifted, so a finger left over from a pinch can't suddenly start a
|
|
52
|
+
// fresh single-pointer game gesture from wherever it's sitting.
|
|
53
|
+
const claimedByGestureRef = useRef(false);
|
|
54
|
+
// Read inside gesture handlers via refs, not the zoom/offsetX/offsetY
|
|
55
|
+
// state directly -- pointerdown/move fire faster than React re-renders
|
|
56
|
+
// commit, so reading state there can see a stale value mid-gesture.
|
|
57
|
+
const zoomRef = useRef(zoom);
|
|
58
|
+
const offsetRef = useRef({ x: offsetX, y: offsetY });
|
|
59
|
+
zoomRef.current = zoom;
|
|
60
|
+
offsetRef.current = { x: offsetX, y: offsetY };
|
|
61
|
+
function clampOffset(offset, atZoom, dimension) {
|
|
62
|
+
const maxOffset = Math.max(0, dimension - dimension / atZoom);
|
|
63
|
+
return clamp(offset, 0, maxOffset);
|
|
64
|
+
}
|
|
65
|
+
function toView(clientX, clientY) {
|
|
66
|
+
const canvas = canvasRef.current;
|
|
67
|
+
const rect = canvas.getBoundingClientRect();
|
|
68
|
+
return {
|
|
69
|
+
x: ((clientX - rect.left) * worldWidth) / rect.width,
|
|
70
|
+
y: ((clientY - rect.top) * worldHeight) / rect.height,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function toWorld(clientX, clientY) {
|
|
74
|
+
const v = toView(clientX, clientY);
|
|
75
|
+
return { x: v.x / zoomRef.current + offsetRef.current.x, y: v.y / zoomRef.current + offsetRef.current.y };
|
|
76
|
+
}
|
|
77
|
+
/** Rescales around a fixed view-space point by `factor`, clamped to [minZoom, maxZoom], keeping that point visually fixed. */
|
|
78
|
+
function zoomAround(viewX, viewY, factor) {
|
|
79
|
+
const prevZoom = zoomRef.current;
|
|
80
|
+
const newZoom = clamp(prevZoom * factor, minZoom, maxZoom);
|
|
81
|
+
const anchorWorldX = viewX / prevZoom + offsetRef.current.x;
|
|
82
|
+
const anchorWorldY = viewY / prevZoom + offsetRef.current.y;
|
|
83
|
+
setZoom(newZoom);
|
|
84
|
+
setOffsetX(clampOffset(anchorWorldX - viewX / newZoom, newZoom, worldWidth));
|
|
85
|
+
setOffsetY(clampOffset(anchorWorldY - viewY / newZoom, newZoom, worldHeight));
|
|
86
|
+
}
|
|
87
|
+
function centerOn(worldX, worldY) {
|
|
88
|
+
const zoomNow = zoomRef.current;
|
|
89
|
+
setOffsetX(clampOffset(worldX - worldWidth / zoomNow / 2, zoomNow, worldWidth));
|
|
90
|
+
setOffsetY(clampOffset(worldY - worldHeight / zoomNow / 2, zoomNow, worldHeight));
|
|
91
|
+
}
|
|
92
|
+
// Native (non-passive) wheel listener, desktop-only zoom (mouse wheel/
|
|
93
|
+
// trackpad) -- React's synthetic onWheel is passive by default, so
|
|
94
|
+
// e.preventDefault() inside it silently no-ops in some browsers and the
|
|
95
|
+
// page scrolls instead of the board zooming. Touch is the primary input
|
|
96
|
+
// this hook is designed for (see singlePointerPan below): pinch zooms,
|
|
97
|
+
// drag pans, both via real multi-touch PointerEvents below, nothing to
|
|
98
|
+
// do with wheel at all.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const canvas = canvasRef.current;
|
|
101
|
+
if (!canvas)
|
|
102
|
+
return;
|
|
103
|
+
const handleWheel = (e) => {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
const view = toView(e.clientX, e.clientY);
|
|
106
|
+
zoomAround(view.x, view.y, e.deltaY > 0 ? 0.9 : 1.1);
|
|
107
|
+
};
|
|
108
|
+
canvas.addEventListener('wheel', handleWheel, { passive: false });
|
|
109
|
+
return () => canvas.removeEventListener('wheel', handleWheel);
|
|
110
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- zoomAround/toView read current state via refs, not closed-over values
|
|
111
|
+
}, [canvasRef, worldWidth, worldHeight]);
|
|
112
|
+
function handlePointerDown(e) {
|
|
113
|
+
// Keep receiving move/up for this pointer even if it's dragged outside
|
|
114
|
+
// the canvas (a pinch finger sliding off the element, or a drag past
|
|
115
|
+
// the edge to aim a big shot). Wrapped: some browsers throw for a
|
|
116
|
+
// pointerId their own internal active-pointer tracking doesn't
|
|
117
|
+
// recognize (seen with synthetically dispatched PointerEvents in
|
|
118
|
+
// tests) -- losing capture only risks a truncated drag, not a crash.
|
|
119
|
+
try {
|
|
120
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Non-fatal -- see comment above.
|
|
124
|
+
}
|
|
125
|
+
pointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
126
|
+
if (pointersRef.current.size === 2) {
|
|
127
|
+
if (!claimedByGestureRef.current)
|
|
128
|
+
onPointerCancel?.(e);
|
|
129
|
+
claimedByGestureRef.current = true;
|
|
130
|
+
const [a, b] = [...pointersRef.current.values()];
|
|
131
|
+
const startDist = Math.hypot(b.x - a.x, b.y - a.y);
|
|
132
|
+
const mid = toView((a.x + b.x) / 2, (a.y + b.y) / 2);
|
|
133
|
+
pinchRef.current = {
|
|
134
|
+
startDist,
|
|
135
|
+
startZoom: zoomRef.current,
|
|
136
|
+
anchorWorldX: mid.x / zoomRef.current + offsetRef.current.x,
|
|
137
|
+
anchorWorldY: mid.y / zoomRef.current + offsetRef.current.y,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
else if (pointersRef.current.size === 1) {
|
|
141
|
+
if (singlePointerPan) {
|
|
142
|
+
claimedByGestureRef.current = true;
|
|
143
|
+
const mid = toView(e.clientX, e.clientY);
|
|
144
|
+
pinchRef.current = {
|
|
145
|
+
startDist: 0,
|
|
146
|
+
startZoom: zoomRef.current,
|
|
147
|
+
anchorWorldX: mid.x / zoomRef.current + offsetRef.current.x,
|
|
148
|
+
anchorWorldY: mid.y / zoomRef.current + offsetRef.current.y,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
onPointerDown?.(toWorld(e.clientX, e.clientY), e);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function handlePointerMove(e) {
|
|
157
|
+
if (!pointersRef.current.has(e.pointerId))
|
|
158
|
+
return;
|
|
159
|
+
pointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
160
|
+
if (pointersRef.current.size >= 2 && pinchRef.current) {
|
|
161
|
+
const [a, b] = [...pointersRef.current.values()];
|
|
162
|
+
const dist = Math.hypot(b.x - a.x, b.y - a.y);
|
|
163
|
+
const mid = toView((a.x + b.x) / 2, (a.y + b.y) / 2);
|
|
164
|
+
const { startDist, startZoom, anchorWorldX, anchorWorldY } = pinchRef.current;
|
|
165
|
+
const newZoom = clamp(startZoom * (dist / Math.max(startDist, 1)), minZoom, maxZoom);
|
|
166
|
+
setZoom(newZoom);
|
|
167
|
+
setOffsetX(clampOffset(anchorWorldX - mid.x / newZoom, newZoom, worldWidth));
|
|
168
|
+
setOffsetY(clampOffset(anchorWorldY - mid.y / newZoom, newZoom, worldHeight));
|
|
169
|
+
}
|
|
170
|
+
else if (pointersRef.current.size === 1 && singlePointerPan && pinchRef.current) {
|
|
171
|
+
// Pure pan (zoom unchanged) anchored at the single point, same
|
|
172
|
+
// anchor math as the pinch branch with a fixed zoom.
|
|
173
|
+
const mid = toView(e.clientX, e.clientY);
|
|
174
|
+
const { startZoom, anchorWorldX, anchorWorldY } = pinchRef.current;
|
|
175
|
+
setOffsetX(clampOffset(anchorWorldX - mid.x / startZoom, startZoom, worldWidth));
|
|
176
|
+
setOffsetY(clampOffset(anchorWorldY - mid.y / startZoom, startZoom, worldHeight));
|
|
177
|
+
}
|
|
178
|
+
else if (pointersRef.current.size === 1 && !claimedByGestureRef.current) {
|
|
179
|
+
onPointerMove?.(toWorld(e.clientX, e.clientY), e);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function endPointer(e, kind) {
|
|
183
|
+
const wasTracked = pointersRef.current.has(e.pointerId);
|
|
184
|
+
pointersRef.current.delete(e.pointerId);
|
|
185
|
+
if (pointersRef.current.size < 2)
|
|
186
|
+
pinchRef.current = null;
|
|
187
|
+
if (pointersRef.current.size === 0) {
|
|
188
|
+
// Only deliver a genuine single-pointer up/cancel to the game-logic
|
|
189
|
+
// callbacks -- once a gesture has claimed this touch sequence, its
|
|
190
|
+
// release shouldn't also fire a game action.
|
|
191
|
+
if (wasTracked && !claimedByGestureRef.current) {
|
|
192
|
+
if (kind === 'up')
|
|
193
|
+
onPointerUp?.(toWorld(e.clientX, e.clientY), e);
|
|
194
|
+
else
|
|
195
|
+
onPointerCancel?.(e);
|
|
196
|
+
}
|
|
197
|
+
claimedByGestureRef.current = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
zoom,
|
|
202
|
+
offsetX,
|
|
203
|
+
offsetY,
|
|
204
|
+
applyTransform(ctx) {
|
|
205
|
+
ctx.scale(zoom, zoom);
|
|
206
|
+
ctx.translate(-offsetX, -offsetY);
|
|
207
|
+
},
|
|
208
|
+
toWorld,
|
|
209
|
+
centerOn,
|
|
210
|
+
canvasProps: {
|
|
211
|
+
onPointerDown: handlePointerDown,
|
|
212
|
+
onPointerMove: handlePointerMove,
|
|
213
|
+
onPointerUp: (e) => endPointer(e, 'up'),
|
|
214
|
+
onPointerCancel: (e) => endPointer(e, 'cancel'),
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=useCanvasCamera.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCanvasCamera.js","sourceRoot":"","sources":["../src/useCanvasCamera.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,4EAA4E;AAC5E,4EAA4E;AAC5E,sEAAsE;AACtE,qEAAqE;AACrE,yEAAyE;AACzE,yEAAyE;AACzE,mEAAmE;AACnE,wEAAwE;AACxE,oEAAoE;AACpE,sEAAsE;AACtE,WAAW;AACX,EAAE;AACF,sEAAsE;AACtE,gEAAgE;AAChE,sEAAsE;AACtE,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AACxE,sEAAsE;AACtE,mDAAmD;AACnD,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,kEAAkE;AAClE,qEAAqE;AACrE,kEAAkE;AAClE,oEAAoE;AACpE,yEAAyE;AAEzE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAA;AAwDnE,SAAS,KAAK,CAAC,KAAa,EAAE,GAAW,EAAE,GAAW;IACpD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAC9B,SAAS,EACT,UAAU,EACV,WAAW,EACX,OAAO,GAAG,CAAC,EACX,iBAAiB,GAAG,CAAC,EACrB,WAAW,EACX,gBAAgB,GAAG,KAAK,EACxB,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,GACQ;IACvB,MAAM,OAAO,GAAG,OAAO,GAAG,iBAAiB,CAAA;IAE3C,wEAAwE;IACxE,wEAAwE;IACxE,0EAA0E;IAC1E,wEAAwE;IACxE,0EAA0E;IAC1E,4BAA4B;IAC5B,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,WAAW,IAAI,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAA;IAC9H,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IACzC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IAEzC,sEAAsE;IACtE,oEAAoE;IACpE,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,GAAG,EAAgB,CAAC,CAAA;IACnD,MAAM,QAAQ,GAAG,MAAM,CAA8F,IAAI,CAAC,CAAA;IAC1H,wEAAwE;IACxE,uEAAuE;IACvE,oEAAoE;IACpE,gEAAgE;IAChE,MAAM,mBAAmB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IACzC,sEAAsE;IACtE,uEAAuE;IACvE,oEAAoE;IACpE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC5B,MAAM,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;IACpD,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;IACtB,SAAS,CAAC,OAAO,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAA;IAE9C,SAAS,WAAW,CAAC,MAAc,EAAE,MAAc,EAAE,SAAiB;QACpE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC,CAAA;QAC7D,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,CAAC,CAAA;IACpC,CAAC;IAED,SAAS,MAAM,CAAC,OAAe,EAAE,OAAe;QAC9C,MAAM,MAAM,GAAG,SAAS,CAAC,OAAQ,CAAA;QACjC,MAAM,IAAI,GAAG,MAAM,CAAC,qBAAqB,EAAE,CAAA;QAC3C,OAAO;YACL,CAAC,EAAE,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC,KAAK;YACpD,CAAC,EAAE,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,GAAG,IAAI,CAAC,MAAM;SACtD,CAAA;IACH,CAAC;IAED,SAAS,OAAO,CAAC,OAAe,EAAE,OAAe;QAC/C,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QAClC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,CAAA;IAC3G,CAAC;IAED,8HAA8H;IAC9H,SAAS,UAAU,CAAC,KAAa,EAAE,KAAa,EAAE,MAAc;QAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAA;QAChC,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,GAAG,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;QAC1D,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;QAC3D,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;QAC3D,OAAO,CAAC,OAAO,CAAC,CAAA;QAChB,UAAU,CAAC,WAAW,CAAC,YAAY,GAAG,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAA;QAC5E,UAAU,CAAC,WAAW,CAAC,YAAY,GAAG,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAA;IAC/E,CAAC;IAED,SAAS,QAAQ,CAAC,MAAc,EAAE,MAAc;QAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAA;QAC/B,UAAU,CAAC,WAAW,CAAC,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAA;QAC/E,UAAU,CAAC,WAAW,CAAC,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAA;IACnF,CAAC;IAED,uEAAuE;IACvE,mEAAmE;IACnE,wEAAwE;IACxE,wEAAwE;IACxE,uEAAuE;IACvE,uEAAuE;IACvE,wBAAwB;IACxB,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAA;QAChC,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,MAAM,WAAW,GAAG,CAAC,CAAa,EAAE,EAAE;YACpC,CAAC,CAAC,cAAc,EAAE,CAAA;YAClB,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;YACzC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QACtD,CAAC,CAAA;QACD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;QACjE,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;QAC7D,gIAAgI;IAClI,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC,CAAA;IAExC,SAAS,iBAAiB,CAAC,CAAwC;QACjE,uEAAuE;QACvE,qEAAqE;QACrE,kEAAkE;QAClE,+DAA+D;QAC/D,iEAAiE;QACjE,qEAAqE;QACrE,IAAI,CAAC;YACH,CAAC,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,kCAAkC;QACpC,CAAC;QACD,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAA;QAEpE,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACnC,IAAI,CAAC,mBAAmB,CAAC,OAAO;gBAAE,eAAe,EAAE,CAAC,CAAC,CAAC,CAAA;YACtD,mBAAmB,CAAC,OAAO,GAAG,IAAI,CAAA;YAClC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;YAChD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;YAClD,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;YACpD,QAAQ,CAAC,OAAO,GAAG;gBACjB,SAAS;gBACT,SAAS,EAAE,OAAO,CAAC,OAAO;gBAC1B,YAAY,EAAE,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;gBAC3D,YAAY,EAAE,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;aAC5D,CAAA;QACH,CAAC;aAAM,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAC1C,IAAI,gBAAgB,EAAE,CAAC;gBACrB,mBAAmB,CAAC,OAAO,GAAG,IAAI,CAAA;gBAClC,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;gBACxC,QAAQ,CAAC,OAAO,GAAG;oBACjB,SAAS,EAAE,CAAC;oBACZ,SAAS,EAAE,OAAO,CAAC,OAAO;oBAC1B,YAAY,EAAE,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;oBAC3D,YAAY,EAAE,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;iBAC5D,CAAA;YACH,CAAC;iBAAM,CAAC;gBACN,aAAa,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;YACnD,CAAC;QACH,CAAC;IACH,CAAC;IAED,SAAS,iBAAiB,CAAC,CAAwC;QACjE,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;YAAE,OAAM;QACjD,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAA;QAEpE,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACtD,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;YAChD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;YAC7C,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;YACpD,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAA;YAC7E,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;YACpF,OAAO,CAAC,OAAO,CAAC,CAAA;YAChB,UAAU,CAAC,WAAW,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAA;YAC5E,UAAU,CAAC,WAAW,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAA;QAC/E,CAAC;aAAM,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,IAAI,gBAAgB,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YAClF,+DAA+D;YAC/D,qDAAqD;YACrD,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;YACxC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAA;YAClE,UAAU,CAAC,WAAW,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC,GAAG,SAAS,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,CAAA;YAChF,UAAU,CAAC,WAAW,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC,GAAG,SAAS,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC,CAAA;QACnF,CAAC;aAAM,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,CAAC;YAC1E,aAAa,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;QACnD,CAAC;IACH,CAAC;IAED,SAAS,UAAU,CAAC,CAAwC,EAAE,IAAqB;QACjF,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QACvD,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAEvC,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC;YAAE,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAA;QACzD,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACnC,oEAAoE;YACpE,mEAAmE;YACnE,6CAA6C;YAC7C,IAAI,UAAU,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,CAAC;gBAC/C,IAAI,IAAI,KAAK,IAAI;oBAAE,WAAW,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;;oBAC7D,eAAe,EAAE,CAAC,CAAC,CAAC,CAAA;YAC3B,CAAC;YACD,mBAAmB,CAAC,OAAO,GAAG,KAAK,CAAA;QACrC,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI;QACJ,OAAO;QACP,OAAO;QACP,cAAc,CAAC,GAA6B;YAC1C,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;YACrB,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAA;QACnC,CAAC;QACD,OAAO;QACP,QAAQ;QACR,WAAW,EAAE;YACX,aAAa,EAAE,iBAAiB;YAChC,aAAa,EAAE,iBAAiB;YAChC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC;YACvC,eAAe,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,QAAQ,CAAC;SAChD;KACF,CAAA;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anweather/plugin-board-kit",
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "Board-UI primitives for a web-boardgame-platform game board: the mount/update/unmount handle a remote plugin's bundle exports, BoardShell/TurnIdentity layout, turn/spectator/team helpers, the TurnClock display half, and the pinch/wheel canvas camera. React is a peer dependency, so a plugin's bundle carries exactly one React.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/**"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"prepack": "npm run clean && npm run build",
|
|
23
|
+
"test": "tsx --test test/*.test.ts",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.test.json"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": "^19.0.0",
|
|
28
|
+
"react-dom": "^19.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.20.4",
|
|
32
|
+
"@types/react": "^19.2.14",
|
|
33
|
+
"@types/react-dom": "^19.2.3",
|
|
34
|
+
"react": "^19.2.6",
|
|
35
|
+
"react-dom": "^19.2.6",
|
|
36
|
+
"tsx": "^4.19.2",
|
|
37
|
+
"typescript": "~5.8.3"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|