@dreamboard-games/ui-sdk 0.0.43 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ActionButton.d.ts.map +1 -1
- package/dist/components/ActionButton.js +2 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/DiceRoller.d.ts +3 -2
- package/dist/components/DiceRoller.d.ts.map +1 -1
- package/dist/components/DiceRoller.js +4 -13
- package/dist/components/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/ErrorBoundary.js +94 -2
- package/dist/components/InteractionForm.d.ts +1 -1
- package/dist/components/InteractionForm.d.ts.map +1 -1
- package/dist/components/InteractionForm.js +29 -15
- package/dist/components/PrimaryActionButton.d.ts.map +1 -1
- package/dist/components/PrimaryActionButton.js +7 -6
- package/dist/components/ResourceCounter.d.ts +59 -25
- package/dist/components/ResourceCounter.d.ts.map +1 -1
- package/dist/components/ResourceCounter.js +106 -115
- package/dist/components/Toast.d.ts +13 -6
- package/dist/components/Toast.d.ts.map +1 -1
- package/dist/components/Toast.js +10 -5
- package/dist/components/board/HexGrid.js +6 -6
- package/dist/components/board/target-layer.d.ts +18 -2
- package/dist/components/board/target-layer.d.ts.map +1 -1
- package/dist/components/board/target-layer.js +20 -3
- package/dist/components/index.d.ts +3 -4
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -4
- package/dist/components/surfaces/InboxSurface.d.ts.map +1 -1
- package/dist/components/surfaces/InboxSurface.js +2 -6
- package/dist/components/surfaces/PlayerCardsSurface.js +2 -2
- package/dist/components/surfaces/internal/CardZoneRoutedForm.d.ts +7 -0
- package/dist/components/surfaces/internal/CardZoneRoutedForm.d.ts.map +1 -0
- package/dist/components/surfaces/internal/CardZoneRoutedForm.js +9 -0
- package/dist/components/surfaces/internal/DefaultInteractionButton.d.ts.map +1 -1
- package/dist/components/surfaces/internal/DefaultInteractionButton.js +5 -8
- package/dist/components/surfaces/internal/useCardZoneInteractions.d.ts +2 -2
- package/dist/components/surfaces/internal/useCardZoneInteractions.d.ts.map +1 -1
- package/dist/components/surfaces/internal/useCardZoneInteractions.js +19 -43
- package/dist/context/InteractionDraftContext.d.ts +11 -2
- package/dist/context/InteractionDraftContext.d.ts.map +1 -1
- package/dist/context/InteractionDraftContext.js +41 -4
- package/dist/defaults/components.d.ts +0 -5
- package/dist/defaults/components.d.ts.map +1 -1
- package/dist/defaults/components.js +7 -11
- package/dist/hooks/useBoardInteractions.d.ts +35 -12
- package/dist/hooks/useBoardInteractions.d.ts.map +1 -1
- package/dist/hooks/useBoardInteractions.js +186 -82
- package/dist/hooks/useInteractionHandle.d.ts +1 -1
- package/dist/hooks/useInteractionHandle.d.ts.map +1 -1
- package/dist/hooks/useInteractionHandle.js +12 -27
- package/dist/index.d.ts +11 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -14
- package/dist/primitives/board.d.ts +53 -3
- package/dist/primitives/board.d.ts.map +1 -1
- package/dist/primitives/board.js +65 -41
- package/dist/primitives/dialog-lifecycle.d.ts +17 -0
- package/dist/primitives/dialog-lifecycle.d.ts.map +1 -0
- package/dist/primitives/dialog-lifecycle.js +24 -0
- package/dist/primitives/dice.d.ts +31 -0
- package/dist/primitives/dice.d.ts.map +1 -0
- package/dist/primitives/dice.js +33 -0
- package/dist/primitives/game.d.ts +55 -0
- package/dist/primitives/game.d.ts.map +1 -0
- package/dist/primitives/game.js +101 -0
- package/dist/primitives/index.d.ts +7 -4
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +7 -4
- package/dist/primitives/interaction-form-binding.d.ts +12 -0
- package/dist/primitives/interaction-form-binding.d.ts.map +1 -0
- package/dist/primitives/interaction-form-binding.js +14 -0
- package/dist/primitives/interaction-submit.d.ts +23 -0
- package/dist/primitives/interaction-submit.d.ts.map +1 -0
- package/dist/primitives/interaction-submit.js +41 -0
- package/dist/primitives/interaction.d.ts +76 -6
- package/dist/primitives/interaction.d.ts.map +1 -1
- package/dist/primitives/interaction.js +210 -26
- package/dist/primitives/player-roster.d.ts +2 -1
- package/dist/primitives/player-roster.d.ts.map +1 -1
- package/dist/primitives/prompt.d.ts +36 -11
- package/dist/primitives/prompt.d.ts.map +1 -1
- package/dist/primitives/prompt.js +29 -17
- package/dist/primitives/ui.d.ts +9 -0
- package/dist/primitives/ui.d.ts.map +1 -0
- package/dist/primitives/ui.js +7 -0
- package/dist/primitives/zone.d.ts +111 -5
- package/dist/primitives/zone.d.ts.map +1 -1
- package/dist/primitives/zone.js +349 -9
- package/dist/reducer.d.ts +2 -14
- package/dist/reducer.d.ts.map +1 -1
- package/dist/reducer.js +1 -14
- package/dist/runtime/createPluginRuntimeAPI.js +1 -1
- package/dist/types/hex-color.d.ts +7 -0
- package/dist/types/hex-color.d.ts.map +1 -0
- package/dist/types/hex-color.js +13 -0
- package/dist/types/player-state.d.ts +28 -14
- package/dist/types/player-state.d.ts.map +1 -1
- package/dist/types/plugin-state.d.ts +9 -3
- package/dist/types/plugin-state.d.ts.map +1 -1
- package/dist/ui-contract.d.ts +119 -14
- package/dist/ui-contract.d.ts.map +1 -1
- package/dist/ui-contract.js +4 -3
- package/dist/ui-sdk.d.ts +1637 -1245
- package/dist/utils/interaction-inputs.d.ts +8 -5
- package/dist/utils/interaction-inputs.d.ts.map +1 -1
- package/dist/utils/interaction-inputs.js +82 -14
- package/dist/utils/interaction-router.d.ts +31 -0
- package/dist/utils/interaction-router.d.ts.map +1 -0
- package/dist/utils/interaction-router.js +114 -0
- package/package.json +2 -1
- package/src/components/ActionButton.tsx +2 -1
- package/src/components/Card.tsx +1 -1
- package/src/components/DiceRoller.tsx +13 -22
- package/src/components/ErrorBoundary.test.tsx +19 -0
- package/src/components/ErrorBoundary.tsx +113 -24
- package/src/components/InteractionForm.test.tsx +24 -0
- package/src/components/InteractionForm.tsx +48 -23
- package/src/components/PrimaryActionButton.tsx +19 -5
- package/src/components/ResourceCounter.test.tsx +13 -13
- package/src/components/ResourceCounter.tsx +238 -244
- package/src/components/Toast.tsx +23 -10
- package/src/components/__fixtures__/ResourceCounter.fixture.tsx +70 -169
- package/src/components/board/HexGrid.tsx +6 -6
- package/src/components/board/target-layer.ts +44 -5
- package/src/components/index.ts +17 -10
- package/src/components/surfaces/InboxSurface.tsx +7 -5
- package/src/components/surfaces/PlayerCardsSurface.tsx +6 -6
- package/src/components/surfaces/internal/CardZoneRoutedForm.tsx +35 -0
- package/src/components/surfaces/internal/DefaultInteractionButton.tsx +17 -7
- package/src/components/surfaces/internal/useCardZoneInteractions.ts +25 -67
- package/src/context/InteractionDraftContext.tsx +51 -5
- package/src/defaults/components.tsx +12 -50
- package/src/defaults/defaults.test.tsx +1 -50
- package/src/hooks/useBoardInteractions.test.tsx +240 -17
- package/src/hooks/useBoardInteractions.ts +330 -105
- package/src/hooks/useInteractionHandle.ts +23 -28
- package/src/index.test.ts +60 -40
- package/src/index.ts +30 -36
- package/src/primitives/board.test.tsx +73 -0
- package/src/primitives/board.tsx +191 -40
- package/src/primitives/dialog-lifecycle.ts +58 -0
- package/src/primitives/dice.test.tsx +47 -0
- package/src/primitives/dice.tsx +79 -0
- package/src/primitives/game.test.tsx +98 -0
- package/src/primitives/game.tsx +213 -0
- package/src/primitives/index.ts +84 -0
- package/src/primitives/interaction-form-binding.tsx +56 -0
- package/src/primitives/interaction-submit.ts +90 -0
- package/src/primitives/interaction.test.tsx +396 -0
- package/src/primitives/interaction.tsx +451 -31
- package/src/primitives/player-roster.tsx +2 -1
- package/src/primitives/prompt.test.tsx +94 -3
- package/src/primitives/prompt.tsx +87 -48
- package/src/primitives/ui.test.tsx +131 -0
- package/src/primitives/ui.tsx +13 -0
- package/src/primitives/zone.test.tsx +305 -0
- package/src/primitives/zone.tsx +660 -12
- package/src/reducer.ts +7 -20
- package/src/runtime/createPluginRuntimeAPI.ts +1 -1
- package/src/types/hex-color.ts +20 -0
- package/src/types/player-state.ts +36 -18
- package/src/types/plugin-state.ts +10 -3
- package/src/ui-contract.ts +253 -21
- package/src/utils/interaction-inputs.test.ts +400 -0
- package/src/utils/interaction-inputs.ts +113 -11
- package/src/utils/interaction-router.ts +200 -0
- package/type-stubs/manifest-contract.d.ts +42 -0
- package/type-stubs/manifest-contract.d.ts.map +1 -0
- package/type-stubs/manifest-contract.js +72 -0
- package/type-stubs/ui-contract.d.ts +5 -0
- package/type-stubs/ui-contract.d.ts.map +1 -0
- package/type-stubs/ui-contract.js +1 -0
package/src/primitives/board.tsx
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createContext,
|
|
3
3
|
useContext,
|
|
4
|
+
useMemo,
|
|
4
5
|
type ButtonHTMLAttributes,
|
|
5
6
|
type ReactNode,
|
|
6
7
|
} from "react";
|
|
7
|
-
import {
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
|
+
createHexBoardView,
|
|
10
|
+
type HexBoardView,
|
|
11
|
+
} from "../components/board/hex-board-view.js";
|
|
12
|
+
import {
|
|
13
|
+
HexGrid,
|
|
14
|
+
type HexGridBoardProps,
|
|
15
|
+
} from "../components/board/HexGrid.js";
|
|
16
|
+
import {
|
|
17
|
+
useBoardInteractions,
|
|
18
|
+
type BoardInteractionsContext,
|
|
19
|
+
type BoardSelectionResult,
|
|
20
|
+
type BoardTargetLayerOptions,
|
|
21
|
+
} from "../hooks/useBoardInteractions.js";
|
|
9
22
|
import { usePluginState } from "../context/PluginStateContext.js";
|
|
23
|
+
import type { AnyHexBoardInput, BoardSpaceIdOf } from "../types/tiled-board.js";
|
|
10
24
|
import type { InteractionDescriptor } from "../types/plugin-state.js";
|
|
11
25
|
import type { BoardTargetKey } from "../ui-contract.js";
|
|
12
26
|
import {
|
|
13
27
|
inputByKey,
|
|
14
28
|
inputKeyForTarget,
|
|
15
|
-
interactionInputKeys,
|
|
16
29
|
type BoardTargetKind,
|
|
17
30
|
} from "../utils/interaction-inputs.js";
|
|
18
31
|
import {
|
|
@@ -20,8 +33,10 @@ import {
|
|
|
20
33
|
renderPrimitive,
|
|
21
34
|
type PrimitiveCommonProps,
|
|
22
35
|
} from "./primitive-props.js";
|
|
36
|
+
import { runInteractionAction } from "./interaction-submit.js";
|
|
37
|
+
import { useGameActionError } from "./game.js";
|
|
23
38
|
|
|
24
|
-
type BoardContextValue =
|
|
39
|
+
type BoardContextValue = BoardInteractionsContext;
|
|
25
40
|
|
|
26
41
|
const BoardContext = createContext<BoardContextValue | null>(null);
|
|
27
42
|
|
|
@@ -51,6 +66,118 @@ export function BoardRoot({ children, targetKinds, ...props }: BoardRootProps) {
|
|
|
51
66
|
);
|
|
52
67
|
}
|
|
53
68
|
|
|
69
|
+
export interface BoardStateProps {
|
|
70
|
+
children: (board: BoardInteractionsContext) => ReactNode;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function BoardState({ children }: BoardStateProps) {
|
|
74
|
+
return <>{children(useBoardPrimitiveContext())}</>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface BoardHexViewProps<
|
|
78
|
+
TBoard extends AnyHexBoardInput,
|
|
79
|
+
TSpaceView extends { id: BoardSpaceIdOf<TBoard> },
|
|
80
|
+
> {
|
|
81
|
+
board: TBoard;
|
|
82
|
+
spaces: readonly TSpaceView[];
|
|
83
|
+
children: (view: HexBoardView<TBoard, TSpaceView>) => ReactNode;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function BoardHexView<
|
|
87
|
+
const TBoard extends AnyHexBoardInput,
|
|
88
|
+
const TSpaceView extends { id: BoardSpaceIdOf<TBoard> },
|
|
89
|
+
>({ board, spaces, children }: BoardHexViewProps<TBoard, TSpaceView>) {
|
|
90
|
+
const view = useMemo(
|
|
91
|
+
() => createHexBoardView<TBoard, TSpaceView>(board, { spaces }),
|
|
92
|
+
[board, spaces],
|
|
93
|
+
);
|
|
94
|
+
return <>{children(view)}</>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface BoardHexGridInteractions {
|
|
98
|
+
edge?: BoardTargetLayerOptions;
|
|
99
|
+
vertex?: BoardTargetLayerOptions;
|
|
100
|
+
space?: BoardTargetLayerOptions;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type BoardHexGridInteractionFilter =
|
|
104
|
+
| "auto"
|
|
105
|
+
| false
|
|
106
|
+
| {
|
|
107
|
+
edge?: readonly string[];
|
|
108
|
+
vertex?: readonly string[];
|
|
109
|
+
space?: readonly string[];
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type BoardHexGridView<
|
|
113
|
+
TBoard extends AnyHexBoardInput,
|
|
114
|
+
TSpaceView extends { id: BoardSpaceIdOf<TBoard> },
|
|
115
|
+
> = HexBoardView<TBoard, TSpaceView> & AnyHexBoardInput;
|
|
116
|
+
|
|
117
|
+
export type BoardHexGridProps<
|
|
118
|
+
TBoard extends AnyHexBoardInput,
|
|
119
|
+
TSpaceView extends { id: BoardSpaceIdOf<TBoard> },
|
|
120
|
+
> = Omit<
|
|
121
|
+
HexGridBoardProps<BoardHexGridView<TBoard, TSpaceView>>,
|
|
122
|
+
"board" | "interactiveEdges" | "interactiveVertices" | "interactiveSpaces"
|
|
123
|
+
> &
|
|
124
|
+
Omit<BoardHexViewProps<TBoard, TSpaceView>, "children"> & {
|
|
125
|
+
interactions?: BoardHexGridInteractionFilter;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export function BoardHexGrid<
|
|
129
|
+
const TBoard extends AnyHexBoardInput,
|
|
130
|
+
const TSpaceView extends { id: BoardSpaceIdOf<TBoard> },
|
|
131
|
+
>({
|
|
132
|
+
board,
|
|
133
|
+
spaces,
|
|
134
|
+
interactions = "auto",
|
|
135
|
+
...props
|
|
136
|
+
}: BoardHexGridProps<TBoard, TSpaceView>) {
|
|
137
|
+
const boardInteractions = useBoardPrimitiveContext();
|
|
138
|
+
const gameActionError = useGameActionError();
|
|
139
|
+
const edgeLayer =
|
|
140
|
+
interactions === false
|
|
141
|
+
? undefined
|
|
142
|
+
: boardInteractions.targetLayers.edge({
|
|
143
|
+
enabled: boardInteractions.eligible.edge.size > 0,
|
|
144
|
+
interactionKeys:
|
|
145
|
+
interactions === "auto" ? undefined : interactions.edge,
|
|
146
|
+
onError: gameActionError ?? undefined,
|
|
147
|
+
});
|
|
148
|
+
const vertexLayer =
|
|
149
|
+
interactions === false
|
|
150
|
+
? undefined
|
|
151
|
+
: boardInteractions.targetLayers.vertex({
|
|
152
|
+
enabled: boardInteractions.eligible.vertex.size > 0,
|
|
153
|
+
interactionKeys:
|
|
154
|
+
interactions === "auto" ? undefined : interactions.vertex,
|
|
155
|
+
onError: gameActionError ?? undefined,
|
|
156
|
+
});
|
|
157
|
+
const spaceLayer =
|
|
158
|
+
interactions === false
|
|
159
|
+
? undefined
|
|
160
|
+
: boardInteractions.targetLayers.space({
|
|
161
|
+
enabled: boardInteractions.eligible.space.size > 0,
|
|
162
|
+
interactionKeys:
|
|
163
|
+
interactions === "auto" ? undefined : interactions.space,
|
|
164
|
+
onError: gameActionError ?? undefined,
|
|
165
|
+
});
|
|
166
|
+
return (
|
|
167
|
+
<BoardHexView board={board} spaces={spaces}>
|
|
168
|
+
{(view) => (
|
|
169
|
+
<HexGrid
|
|
170
|
+
{...props}
|
|
171
|
+
board={view as BoardHexGridView<TBoard, TSpaceView>}
|
|
172
|
+
interactiveEdges={edgeLayer}
|
|
173
|
+
interactiveVertices={vertexLayer}
|
|
174
|
+
interactiveSpaces={spaceLayer}
|
|
175
|
+
/>
|
|
176
|
+
)}
|
|
177
|
+
</BoardHexView>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
54
181
|
export type BoardTargetExtraInputs =
|
|
55
182
|
| Record<string, unknown>
|
|
56
183
|
| ((targetId: string) => Record<string, unknown>);
|
|
@@ -63,7 +190,7 @@ export type BoardTargetProps<Target extends string = BoardTargetKey> =
|
|
|
63
190
|
interaction?: string;
|
|
64
191
|
input?: string;
|
|
65
192
|
extraInputs?: BoardTargetExtraInputs;
|
|
66
|
-
onSelect?: (
|
|
193
|
+
onSelect?: (result: BoardSelectionResult) => void;
|
|
67
194
|
onSelectError?: (error: unknown) => void;
|
|
68
195
|
};
|
|
69
196
|
|
|
@@ -97,6 +224,35 @@ export function BoardTarget<Target extends string = BoardTargetKey>({
|
|
|
97
224
|
return <UnambiguousBoardTarget input={input} {...props} />;
|
|
98
225
|
}
|
|
99
226
|
|
|
227
|
+
export type BoardSpaceTargetProps<Target extends string = BoardTargetKey> =
|
|
228
|
+
Omit<BoardTargetProps<Target>, "kind">;
|
|
229
|
+
|
|
230
|
+
export function BoardSpaceTarget<Target extends string = BoardTargetKey>(
|
|
231
|
+
props: BoardSpaceTargetProps<Target>,
|
|
232
|
+
) {
|
|
233
|
+
return <BoardTarget kind="space" {...props} />;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export type BoardEdgeTargetProps<Target extends string = BoardTargetKey> = Omit<
|
|
237
|
+
BoardTargetProps<Target>,
|
|
238
|
+
"kind"
|
|
239
|
+
>;
|
|
240
|
+
|
|
241
|
+
export function BoardEdgeTarget<Target extends string = BoardTargetKey>(
|
|
242
|
+
props: BoardEdgeTargetProps<Target>,
|
|
243
|
+
) {
|
|
244
|
+
return <BoardTarget kind="edge" {...props} />;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export type BoardVertexTargetProps<Target extends string = BoardTargetKey> =
|
|
248
|
+
Omit<BoardTargetProps<Target>, "kind">;
|
|
249
|
+
|
|
250
|
+
export function BoardVertexTarget<Target extends string = BoardTargetKey>(
|
|
251
|
+
props: BoardVertexTargetProps<Target>,
|
|
252
|
+
) {
|
|
253
|
+
return <BoardTarget kind="vertex" {...props} />;
|
|
254
|
+
}
|
|
255
|
+
|
|
100
256
|
function UnambiguousBoardTarget({
|
|
101
257
|
kind,
|
|
102
258
|
value,
|
|
@@ -109,6 +265,7 @@ function UnambiguousBoardTarget({
|
|
|
109
265
|
...props
|
|
110
266
|
}: Omit<BoardTargetProps, "interaction">) {
|
|
111
267
|
const board = useBoardPrimitiveContext();
|
|
268
|
+
const gameActionError = useGameActionError();
|
|
112
269
|
const eligible = board.isEligible(value, kind);
|
|
113
270
|
const ambiguous = isAmbiguousBoardTarget(board.interactions, kind, value);
|
|
114
271
|
const isDisabled = disabled ?? (!eligible || ambiguous);
|
|
@@ -126,14 +283,13 @@ function UnambiguousBoardTarget({
|
|
|
126
283
|
"data-disabled": isDisabled || undefined,
|
|
127
284
|
onClick: composeEventHandlers(onClick, () => {
|
|
128
285
|
if (isDisabled) return;
|
|
129
|
-
void
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
286
|
+
void runInteractionAction(
|
|
287
|
+
() => board.select[kind](value, resolveExtraInputs(extraInputs, value)),
|
|
288
|
+
{
|
|
289
|
+
onSuccess: onSelect,
|
|
290
|
+
onError: onSelectError ?? gameActionError ?? undefined,
|
|
291
|
+
},
|
|
292
|
+
);
|
|
137
293
|
}),
|
|
138
294
|
});
|
|
139
295
|
}
|
|
@@ -153,7 +309,8 @@ function ExplicitBoardTarget({
|
|
|
153
309
|
}: Omit<BoardTargetProps, "interaction"> & {
|
|
154
310
|
descriptor: InteractionDescriptor;
|
|
155
311
|
}) {
|
|
156
|
-
const
|
|
312
|
+
const board = useBoardPrimitiveContext();
|
|
313
|
+
const gameActionError = useGameActionError();
|
|
157
314
|
const inputKey = input ?? inputKeyForTarget(descriptor, kind, value);
|
|
158
315
|
const inputDescriptor = inputKey
|
|
159
316
|
? inputByKey(descriptor, inputKey)
|
|
@@ -182,32 +339,20 @@ function ExplicitBoardTarget({
|
|
|
182
339
|
onClick: composeEventHandlers(onClick, () => {
|
|
183
340
|
if (isDisabled || !inputKey) return;
|
|
184
341
|
const resolvedExtraInputs = resolveExtraInputs(extraInputs, value);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
for (const required of interactionInputKeys(descriptor)) {
|
|
200
|
-
if (!(required in params)) params[required] = null;
|
|
201
|
-
}
|
|
202
|
-
void handle
|
|
203
|
-
.submit(params)
|
|
204
|
-
.then(() => {
|
|
205
|
-
onSelect?.(descriptor.interactionKey);
|
|
206
|
-
})
|
|
207
|
-
.catch((error) => {
|
|
208
|
-
onSelectError?.(error);
|
|
209
|
-
if (!onSelectError) throw error;
|
|
210
|
-
});
|
|
342
|
+
void runInteractionAction(
|
|
343
|
+
() =>
|
|
344
|
+
board.selectTarget(
|
|
345
|
+
descriptor,
|
|
346
|
+
kind,
|
|
347
|
+
value,
|
|
348
|
+
inputKey,
|
|
349
|
+
resolvedExtraInputs,
|
|
350
|
+
),
|
|
351
|
+
{
|
|
352
|
+
onSuccess: onSelect,
|
|
353
|
+
onError: onSelectError ?? gameActionError ?? undefined,
|
|
354
|
+
},
|
|
355
|
+
);
|
|
211
356
|
}),
|
|
212
357
|
});
|
|
213
358
|
}
|
|
@@ -263,5 +408,11 @@ function isAmbiguousBoardTarget(
|
|
|
263
408
|
|
|
264
409
|
export const Board = {
|
|
265
410
|
Root: BoardRoot,
|
|
411
|
+
State: BoardState,
|
|
412
|
+
HexGrid: BoardHexGrid,
|
|
413
|
+
HexView: BoardHexView,
|
|
266
414
|
Target: BoardTarget,
|
|
415
|
+
SpaceTarget: BoardSpaceTarget,
|
|
416
|
+
EdgeTarget: BoardEdgeTarget,
|
|
417
|
+
VertexTarget: BoardVertexTarget,
|
|
267
418
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export type DialogLifecycleState = "open" | "minimized" | "dismissed";
|
|
4
|
+
|
|
5
|
+
export interface DialogLifecycleValue {
|
|
6
|
+
state: DialogLifecycleState;
|
|
7
|
+
open: boolean;
|
|
8
|
+
minimized: boolean;
|
|
9
|
+
dismissed: boolean;
|
|
10
|
+
setOpen: (open: boolean) => void;
|
|
11
|
+
restore: () => void;
|
|
12
|
+
minimize: () => void;
|
|
13
|
+
dismiss: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DialogLifecycleOptions {
|
|
17
|
+
defaultOpen?: boolean;
|
|
18
|
+
onStateChange?: (state: DialogLifecycleState) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useDialogLifecycle({
|
|
22
|
+
defaultOpen = true,
|
|
23
|
+
onStateChange,
|
|
24
|
+
}: DialogLifecycleOptions): DialogLifecycleValue {
|
|
25
|
+
const [state, setState] = useState<DialogLifecycleState>(
|
|
26
|
+
defaultOpen ? "open" : "minimized",
|
|
27
|
+
);
|
|
28
|
+
const updateState = useCallback(
|
|
29
|
+
(nextState: DialogLifecycleState) => {
|
|
30
|
+
setState(nextState);
|
|
31
|
+
onStateChange?.(nextState);
|
|
32
|
+
},
|
|
33
|
+
[onStateChange],
|
|
34
|
+
);
|
|
35
|
+
const restore = useCallback(() => updateState("open"), [updateState]);
|
|
36
|
+
const minimize = useCallback(() => updateState("minimized"), [updateState]);
|
|
37
|
+
const dismiss = useCallback(() => updateState("dismissed"), [updateState]);
|
|
38
|
+
const setOpen = useCallback(
|
|
39
|
+
(open: boolean) => {
|
|
40
|
+
updateState(open ? "open" : "minimized");
|
|
41
|
+
},
|
|
42
|
+
[updateState],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return useMemo<DialogLifecycleValue>(
|
|
46
|
+
() => ({
|
|
47
|
+
state,
|
|
48
|
+
open: state === "open",
|
|
49
|
+
minimized: state === "minimized",
|
|
50
|
+
dismissed: state === "dismissed",
|
|
51
|
+
setOpen,
|
|
52
|
+
restore,
|
|
53
|
+
minimize,
|
|
54
|
+
dismiss,
|
|
55
|
+
}),
|
|
56
|
+
[dismiss, minimize, restore, setOpen, state],
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { renderToString } from "react-dom/server";
|
|
3
|
+
import { Dice } from "./dice.js";
|
|
4
|
+
|
|
5
|
+
test("Dice primitives expose normalized readonly values and sum", () => {
|
|
6
|
+
const html = renderToString(
|
|
7
|
+
<Dice.Root values={[3, 4] as const} count={2}>
|
|
8
|
+
<Dice.Values>
|
|
9
|
+
{({ values, sum, diceCount, allRolled }) => (
|
|
10
|
+
<span
|
|
11
|
+
data-values={values?.join(",")}
|
|
12
|
+
data-sum={sum}
|
|
13
|
+
data-count={diceCount}
|
|
14
|
+
data-all-rolled={allRolled}
|
|
15
|
+
/>
|
|
16
|
+
)}
|
|
17
|
+
</Dice.Values>
|
|
18
|
+
</Dice.Root>,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(html).toContain('data-values="3,4"');
|
|
22
|
+
expect(html).toContain('data-sum="7"');
|
|
23
|
+
expect(html).toContain('data-count="2"');
|
|
24
|
+
expect(html).toContain('data-all-rolled="true"');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("Dice primitives preserve unrolled state without a local adapter", () => {
|
|
28
|
+
const html = renderToString(
|
|
29
|
+
<Dice.Root values={null} count={2}>
|
|
30
|
+
<Dice.Values>
|
|
31
|
+
{({ values, sum, diceCount, allRolled }) => (
|
|
32
|
+
<span
|
|
33
|
+
data-values={values?.join(",") ?? "none"}
|
|
34
|
+
data-sum={sum ?? "none"}
|
|
35
|
+
data-count={diceCount}
|
|
36
|
+
data-all-rolled={allRolled}
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
</Dice.Values>
|
|
40
|
+
</Dice.Root>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(html).toContain('data-values="none"');
|
|
44
|
+
expect(html).toContain('data-sum="none"');
|
|
45
|
+
expect(html).toContain('data-count="2"');
|
|
46
|
+
expect(html).toContain('data-all-rolled="false"');
|
|
47
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type DiceValue = number | null | undefined;
|
|
4
|
+
|
|
5
|
+
export interface DiceState {
|
|
6
|
+
values: ReadonlyArray<number | undefined> | undefined;
|
|
7
|
+
/** Undefined if any die has not been rolled yet. */
|
|
8
|
+
sum: number | undefined;
|
|
9
|
+
diceCount: number;
|
|
10
|
+
allRolled: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DiceRootProps {
|
|
14
|
+
values?: readonly DiceValue[] | null;
|
|
15
|
+
/** Used when values are not provided. */
|
|
16
|
+
count?: number;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DiceValuesProps {
|
|
21
|
+
children: (state: DiceState) => ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DiceComponents {
|
|
25
|
+
Root(props: DiceRootProps): ReactNode;
|
|
26
|
+
Values(props: DiceValuesProps): ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DiceContext = createContext<DiceState | null>(null);
|
|
30
|
+
|
|
31
|
+
export function normalizeDiceState({
|
|
32
|
+
values,
|
|
33
|
+
count = 2,
|
|
34
|
+
}: {
|
|
35
|
+
values?: readonly DiceValue[] | null;
|
|
36
|
+
count?: number;
|
|
37
|
+
}): DiceState {
|
|
38
|
+
const normalizedValues = values?.map((value) => value ?? undefined);
|
|
39
|
+
const allRolled =
|
|
40
|
+
normalizedValues?.every((value) => value !== undefined) ?? false;
|
|
41
|
+
const sum = allRolled
|
|
42
|
+
? normalizedValues?.reduce<number>(
|
|
43
|
+
(total, value) => total + (value ?? 0),
|
|
44
|
+
0,
|
|
45
|
+
)
|
|
46
|
+
: undefined;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
values: normalizedValues,
|
|
50
|
+
sum,
|
|
51
|
+
diceCount: normalizedValues?.length ?? count,
|
|
52
|
+
allRolled,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function DiceRoot({ values, count, children }: DiceRootProps) {
|
|
57
|
+
return (
|
|
58
|
+
<DiceContext.Provider value={normalizeDiceState({ values, count })}>
|
|
59
|
+
{children}
|
|
60
|
+
</DiceContext.Provider>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function useDicePrimitiveContext(): DiceState {
|
|
65
|
+
const value = useContext(DiceContext);
|
|
66
|
+
if (!value) {
|
|
67
|
+
throw new Error("Dice primitives must be rendered inside <Dice.Root>.");
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function DiceValues({ children }: DiceValuesProps) {
|
|
73
|
+
return children(useDicePrimitiveContext());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const Dice: DiceComponents = {
|
|
77
|
+
Root: DiceRoot,
|
|
78
|
+
Values: DiceValues,
|
|
79
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { renderToString } from "react-dom/server";
|
|
3
|
+
import type { PluginStateSnapshot } from "../types/plugin-state.js";
|
|
4
|
+
import type { PluginSessionState, RuntimeAPI } from "../types/runtime-api.js";
|
|
5
|
+
import { Game } from "./game.js";
|
|
6
|
+
import { GameUIProvider } from "./game-ui-provider.js";
|
|
7
|
+
|
|
8
|
+
function createSessionState(
|
|
9
|
+
overrides: Partial<PluginSessionState> = {},
|
|
10
|
+
): PluginSessionState {
|
|
11
|
+
return {
|
|
12
|
+
status: "ready",
|
|
13
|
+
sessionId: "session-1",
|
|
14
|
+
controllablePlayerIds: ["player-1", "player-2"],
|
|
15
|
+
controllingPlayerId: "player-1",
|
|
16
|
+
userId: "user-1",
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createSnapshot(): PluginStateSnapshot {
|
|
22
|
+
return {
|
|
23
|
+
view: {
|
|
24
|
+
score: 7,
|
|
25
|
+
},
|
|
26
|
+
gameplay: {
|
|
27
|
+
currentPhase: "playing",
|
|
28
|
+
currentStage: "main",
|
|
29
|
+
activePlayers: ["player-2"],
|
|
30
|
+
zones: {},
|
|
31
|
+
availableInteractions: [],
|
|
32
|
+
},
|
|
33
|
+
lobby: {
|
|
34
|
+
hostUserId: "user-1",
|
|
35
|
+
canStart: true,
|
|
36
|
+
seats: [
|
|
37
|
+
{
|
|
38
|
+
playerId: "player-1",
|
|
39
|
+
displayName: "Ada",
|
|
40
|
+
playerColor: "#d04848",
|
|
41
|
+
isHost: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
playerId: "player-2",
|
|
45
|
+
displayName: "Grace",
|
|
46
|
+
playerColor: "#2d5da1",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
notifications: [],
|
|
51
|
+
session: createSessionState(),
|
|
52
|
+
history: null,
|
|
53
|
+
syncId: 1,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createRuntime(snapshot: PluginStateSnapshot): RuntimeAPI {
|
|
58
|
+
return {
|
|
59
|
+
validateInteraction: async () => ({ valid: true }),
|
|
60
|
+
submitInteraction: async () => undefined,
|
|
61
|
+
getSessionState: () => snapshot.session,
|
|
62
|
+
disconnect: () => undefined,
|
|
63
|
+
getSnapshot: () => snapshot,
|
|
64
|
+
subscribeToState: () => () => undefined,
|
|
65
|
+
} as RuntimeAPI;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("Game.Root projects typed author-facing game, player, and turn state", () => {
|
|
69
|
+
const runtime = createRuntime(createSnapshot());
|
|
70
|
+
|
|
71
|
+
const html = renderToString(
|
|
72
|
+
<GameUIProvider runtime={runtime}>
|
|
73
|
+
<Game.Root<{ score: number }, "player-1" | "player-2", "playing">>
|
|
74
|
+
{({ view, phase, stage, me, players, turn }) => (
|
|
75
|
+
<section>
|
|
76
|
+
<span>score:{view.score}</span>
|
|
77
|
+
<span>phase:{phase}</span>
|
|
78
|
+
<span>stage:{stage}</span>
|
|
79
|
+
<span>me:{me.player?.name}</span>
|
|
80
|
+
<span>current:{players.current?.name}</span>
|
|
81
|
+
<span>active:{turn.currentPlayerId}</span>
|
|
82
|
+
<span>mine:{String(turn.isMine)}</span>
|
|
83
|
+
<span>entries:{players.entries.length}</span>
|
|
84
|
+
</section>
|
|
85
|
+
)}
|
|
86
|
+
</Game.Root>
|
|
87
|
+
</GameUIProvider>,
|
|
88
|
+
).replace(/<!-- -->/g, "");
|
|
89
|
+
|
|
90
|
+
expect(html).toContain("score:7");
|
|
91
|
+
expect(html).toContain("phase:playing");
|
|
92
|
+
expect(html).toContain("stage:main");
|
|
93
|
+
expect(html).toContain("me:Ada");
|
|
94
|
+
expect(html).toContain("current:Grace");
|
|
95
|
+
expect(html).toContain("active:player-2");
|
|
96
|
+
expect(html).toContain("mine:false");
|
|
97
|
+
expect(html).toContain("entries:2");
|
|
98
|
+
});
|