@dreamboard-games/ui-sdk 0.0.43 → 0.0.45
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 +1 -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
|
@@ -5,23 +5,29 @@ import { usePluginSession } from "../context/PluginSessionContext.js";
|
|
|
5
5
|
import { usePluginState } from "../context/PluginStateContext.js";
|
|
6
6
|
import { useRuntimeContext } from "../context/RuntimeContext.js";
|
|
7
7
|
import { validationErrorFromUnknown } from "../errors/ValidationError.js";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
InteractiveTargetLayer,
|
|
10
|
+
InteractiveTargetState,
|
|
11
|
+
} from "../components/board/target-layer.js";
|
|
9
12
|
import type { InteractionDescriptor } from "../types/plugin-state.js";
|
|
10
13
|
import {
|
|
14
|
+
applyInteractionInputDefaults,
|
|
11
15
|
eligibleTargetsByBoardKind,
|
|
12
16
|
eligibleTargetsForInput,
|
|
13
17
|
hasBoardTargetInput,
|
|
14
18
|
hasCardTargetInput,
|
|
15
|
-
hasInteractionFieldErrors,
|
|
16
19
|
inputByTarget,
|
|
17
20
|
inputKeyForTarget,
|
|
18
|
-
isInputValueReady,
|
|
19
|
-
interactionArmScope,
|
|
20
|
-
interactionInputKeys,
|
|
21
|
-
toggleManyValue,
|
|
22
|
-
validateInteractionInputDomains,
|
|
23
21
|
type BoardTargetKind,
|
|
24
22
|
} from "../utils/interaction-inputs.js";
|
|
23
|
+
import {
|
|
24
|
+
claimInteractionSubmit,
|
|
25
|
+
clearInteractionRoute,
|
|
26
|
+
getInteractionDraftReadiness,
|
|
27
|
+
markInteractionPending,
|
|
28
|
+
routeInteractionTarget,
|
|
29
|
+
shouldRouteInteractionPending,
|
|
30
|
+
} from "../utils/interaction-router.js";
|
|
25
31
|
|
|
26
32
|
export type BoardEligibleTargets = Readonly<
|
|
27
33
|
Record<BoardTargetKind, ReadonlySet<string>>
|
|
@@ -29,6 +35,7 @@ export type BoardEligibleTargets = Readonly<
|
|
|
29
35
|
|
|
30
36
|
export interface BoardTargetLayerOptions {
|
|
31
37
|
enabled?: boolean;
|
|
38
|
+
interactionKeys?: readonly string[];
|
|
32
39
|
extraInputs?:
|
|
33
40
|
| Record<string, unknown>
|
|
34
41
|
| ((targetId: string) => Record<string, unknown>);
|
|
@@ -40,6 +47,27 @@ export type BoardTargetLayerFactory = (
|
|
|
40
47
|
options?: BoardTargetLayerOptions,
|
|
41
48
|
) => InteractiveTargetLayer;
|
|
42
49
|
|
|
50
|
+
export type BoardSelectionResult<I extends string = string> =
|
|
51
|
+
| { status: "none" }
|
|
52
|
+
| {
|
|
53
|
+
status: "pending";
|
|
54
|
+
interactionKey: I;
|
|
55
|
+
descriptor: InteractionDescriptor<I>;
|
|
56
|
+
missingInputs: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
status: "submitted";
|
|
60
|
+
interactionKey: I;
|
|
61
|
+
descriptor: InteractionDescriptor<I>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface BoardPendingInteraction<I extends string = string> {
|
|
65
|
+
interactionKey: I;
|
|
66
|
+
descriptor: InteractionDescriptor<I>;
|
|
67
|
+
missingInputs: readonly string[];
|
|
68
|
+
clear(): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
43
71
|
export interface BoardInteractionsOptions {
|
|
44
72
|
/**
|
|
45
73
|
* Target kinds the hook pulls interactions from. Defaults to every board
|
|
@@ -106,23 +134,24 @@ export interface BoardInteractionsContext<I extends string = string> {
|
|
|
106
134
|
* Target-kind dispatch. Routes by board geometry (`edge`, `vertex`,
|
|
107
135
|
* `space`, `tile`) rather than authored input-key strings.
|
|
108
136
|
*/
|
|
137
|
+
pendingInteraction: BoardPendingInteraction<I> | null;
|
|
109
138
|
select: {
|
|
110
139
|
edge(
|
|
111
140
|
targetId: string,
|
|
112
141
|
extraInputs?: Record<string, unknown>,
|
|
113
|
-
): Promise<
|
|
142
|
+
): Promise<BoardSelectionResult<I>>;
|
|
114
143
|
vertex(
|
|
115
144
|
targetId: string,
|
|
116
145
|
extraInputs?: Record<string, unknown>,
|
|
117
|
-
): Promise<
|
|
146
|
+
): Promise<BoardSelectionResult<I>>;
|
|
118
147
|
space(
|
|
119
148
|
targetId: string,
|
|
120
149
|
extraInputs?: Record<string, unknown>,
|
|
121
|
-
): Promise<
|
|
150
|
+
): Promise<BoardSelectionResult<I>>;
|
|
122
151
|
tile(
|
|
123
152
|
targetId: string,
|
|
124
153
|
extraInputs?: Record<string, unknown>,
|
|
125
|
-
): Promise<
|
|
154
|
+
): Promise<BoardSelectionResult<I>>;
|
|
126
155
|
};
|
|
127
156
|
/**
|
|
128
157
|
* Reducer-aware target layers for board primitives. Pass these directly
|
|
@@ -135,6 +164,18 @@ export interface BoardInteractionsContext<I extends string = string> {
|
|
|
135
164
|
space: BoardTargetLayerFactory;
|
|
136
165
|
tile: BoardTargetLayerFactory;
|
|
137
166
|
};
|
|
167
|
+
targetState(
|
|
168
|
+
targetKind: BoardTargetKind,
|
|
169
|
+
targetId: string,
|
|
170
|
+
options?: Pick<BoardTargetLayerOptions, "enabled" | "interactionKeys">,
|
|
171
|
+
): InteractiveTargetState;
|
|
172
|
+
selectTarget(
|
|
173
|
+
descriptor: InteractionDescriptor<I>,
|
|
174
|
+
targetKind: BoardTargetKind,
|
|
175
|
+
targetId: string,
|
|
176
|
+
inputKey: string,
|
|
177
|
+
extraInputs?: Record<string, unknown>,
|
|
178
|
+
): Promise<BoardSelectionResult<I>>;
|
|
138
179
|
}
|
|
139
180
|
|
|
140
181
|
/**
|
|
@@ -142,9 +183,9 @@ export interface BoardInteractionsContext<I extends string = string> {
|
|
|
142
183
|
* wiring many `useInteractionById(...)` calls, merging their
|
|
143
184
|
* eligibility sets, and dispatching clicks to the right handle.
|
|
144
185
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
186
|
+
* Internal board primitive controller for games that keep multiple board
|
|
187
|
+
* interactions live simultaneously and dispatch by target geometry. The
|
|
188
|
+
* typical
|
|
148
189
|
* Catan-class shape:
|
|
149
190
|
*
|
|
150
191
|
* ```tsx
|
|
@@ -154,15 +195,16 @@ export interface BoardInteractionsContext<I extends string = string> {
|
|
|
154
195
|
* <HexGrid
|
|
155
196
|
* interactiveVertices={board.targetLayers.vertex()}
|
|
156
197
|
* interactiveEdges={board.targetLayers.edge()}
|
|
157
|
-
* onTileClick={(id) =>
|
|
158
|
-
* board.select.space(id, { stealFromPlayerId: null })
|
|
159
|
-
* }
|
|
198
|
+
* onTileClick={(id) => board.select.space(id)}
|
|
160
199
|
* />
|
|
161
200
|
* );
|
|
162
201
|
* ```
|
|
163
202
|
*
|
|
203
|
+
* Mount generated interaction routes with `<Interaction.Switch routes={...}>`
|
|
204
|
+
* for interactions that need more input after a board target is selected.
|
|
205
|
+
*
|
|
164
206
|
* Eligibility and availability remain authoritative on reducer-projected
|
|
165
|
-
* descriptors. Armed
|
|
207
|
+
* descriptors. Armed routed descriptors beat ambient board descriptors.
|
|
166
208
|
* Multiple unarmed matches are ambiguous and throw
|
|
167
209
|
* {@link BoardInteractionConflictError}.
|
|
168
210
|
*/
|
|
@@ -175,7 +217,13 @@ export function useBoardInteractions<I extends string = string>(
|
|
|
175
217
|
const { controllingPlayerId } = usePluginSession();
|
|
176
218
|
const store = useInteractionUiStore();
|
|
177
219
|
const subscribedArmedBySurface = useStore(store, (state) => state.arms);
|
|
220
|
+
const subscribedDrafts = useStore(store, (state) => state.drafts);
|
|
221
|
+
const pendingInteractionKey = useStore(
|
|
222
|
+
store,
|
|
223
|
+
(state) => state.pendingInteractionKey,
|
|
224
|
+
);
|
|
178
225
|
const armedBySurface = store.getState().arms ?? subscribedArmedBySurface;
|
|
226
|
+
const drafts = store.getState().drafts ?? subscribedDrafts;
|
|
179
227
|
const descriptors = usePluginState(
|
|
180
228
|
(state) => state.gameplay.availableInteractions ?? [],
|
|
181
229
|
);
|
|
@@ -190,16 +238,28 @@ export function useBoardInteractions<I extends string = string>(
|
|
|
190
238
|
);
|
|
191
239
|
|
|
192
240
|
const interactions = useMemo<ReadonlyArray<InteractionDescriptor<I>>>(() => {
|
|
193
|
-
return descriptors.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
241
|
+
return descriptors.flatMap(
|
|
242
|
+
(descriptor): Array<InteractionDescriptor<I>> => {
|
|
243
|
+
if (armedIds.has(descriptor.interactionKey)) {
|
|
244
|
+
return [
|
|
245
|
+
{ ...descriptor, interactionKey: descriptor.interactionKey as I },
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
if (hasCardTargetInput(descriptor)) return [];
|
|
249
|
+
if (!hasBoardTargetInput(descriptor)) return [];
|
|
250
|
+
const include =
|
|
251
|
+
!targetKinds || targetKinds.length === 0
|
|
252
|
+
? true
|
|
253
|
+
: (
|
|
254
|
+
Object.keys(
|
|
255
|
+
eligibleTargetsByBoardKind(descriptor),
|
|
256
|
+
) as BoardTargetKind[]
|
|
257
|
+
).some((kind) => targetKindSet.has(kind));
|
|
258
|
+
return include
|
|
259
|
+
? [{ ...descriptor, interactionKey: descriptor.interactionKey as I }]
|
|
260
|
+
: [];
|
|
261
|
+
},
|
|
262
|
+
);
|
|
203
263
|
}, [armedIds, descriptors, targetKindSet, targetKinds]);
|
|
204
264
|
|
|
205
265
|
const eligible = useMemo<BoardEligibleTargets>(() => {
|
|
@@ -211,7 +271,10 @@ export function useBoardInteractions<I extends string = string>(
|
|
|
211
271
|
};
|
|
212
272
|
for (const descriptor of interactions) {
|
|
213
273
|
if (!descriptor.available) continue;
|
|
214
|
-
const targetsByKind = eligibleTargetsByBoardKind(
|
|
274
|
+
const targetsByKind = eligibleTargetsByBoardKind(
|
|
275
|
+
descriptor,
|
|
276
|
+
drafts[descriptor.interactionKey] ?? {},
|
|
277
|
+
);
|
|
215
278
|
for (const [targetKind, ids] of Object.entries(targetsByKind) as Array<
|
|
216
279
|
[BoardTargetKind, readonly string[] | undefined]
|
|
217
280
|
>) {
|
|
@@ -221,7 +284,7 @@ export function useBoardInteractions<I extends string = string>(
|
|
|
221
284
|
}
|
|
222
285
|
}
|
|
223
286
|
return acc;
|
|
224
|
-
}, [interactions]);
|
|
287
|
+
}, [drafts, interactions]);
|
|
225
288
|
|
|
226
289
|
const isEligible = useCallback(
|
|
227
290
|
(targetId: string, targetKind?: BoardTargetKind) => {
|
|
@@ -236,18 +299,93 @@ export function useBoardInteractions<I extends string = string>(
|
|
|
236
299
|
[eligible],
|
|
237
300
|
);
|
|
238
301
|
|
|
239
|
-
const
|
|
302
|
+
const clearPendingInteraction = useCallback(
|
|
303
|
+
(descriptor: InteractionDescriptor) => {
|
|
304
|
+
clearInteractionRoute(store, descriptor);
|
|
305
|
+
},
|
|
306
|
+
[store],
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const resolveSelection = useCallback(
|
|
240
310
|
async (
|
|
241
|
-
|
|
311
|
+
descriptor: InteractionDescriptor<I>,
|
|
312
|
+
inputKey: string,
|
|
242
313
|
targetId: string,
|
|
243
314
|
extraInputs?: Record<string, unknown>,
|
|
244
|
-
): Promise<
|
|
245
|
-
if (!controllingPlayerId) return
|
|
315
|
+
): Promise<BoardSelectionResult<I>> => {
|
|
316
|
+
if (!controllingPlayerId) return { status: "none" };
|
|
317
|
+
const { params, readiness } = routeInteractionTarget(store, descriptor, {
|
|
318
|
+
inputKey,
|
|
319
|
+
value: targetId,
|
|
320
|
+
extraInputs,
|
|
321
|
+
});
|
|
322
|
+
if (shouldRouteInteractionPending(descriptor, readiness)) {
|
|
323
|
+
markInteractionPending(store, descriptor);
|
|
324
|
+
return {
|
|
325
|
+
status: "pending",
|
|
326
|
+
interactionKey: descriptor.interactionKey as I,
|
|
327
|
+
descriptor,
|
|
328
|
+
missingInputs: readiness.missingInputs,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const submitParams = applyInteractionInputDefaults<
|
|
333
|
+
Record<string, unknown>
|
|
334
|
+
>(descriptor, params);
|
|
335
|
+
if (!claimInteractionSubmit(store, descriptor)) {
|
|
336
|
+
return {
|
|
337
|
+
status: "submitted",
|
|
338
|
+
interactionKey: descriptor.interactionKey as I,
|
|
339
|
+
descriptor,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
await runtime.submitInteraction(
|
|
344
|
+
controllingPlayerId,
|
|
345
|
+
descriptor.interactionId,
|
|
346
|
+
submitParams as Record<string, unknown>,
|
|
347
|
+
);
|
|
348
|
+
clearPendingInteraction(descriptor);
|
|
349
|
+
return {
|
|
350
|
+
status: "submitted",
|
|
351
|
+
interactionKey: descriptor.interactionKey as I,
|
|
352
|
+
descriptor,
|
|
353
|
+
};
|
|
354
|
+
} catch (error) {
|
|
355
|
+
throw validationErrorFromUnknown(error);
|
|
356
|
+
} finally {
|
|
357
|
+
store.setSubmitting(descriptor.interactionKey, false);
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
[clearPendingInteraction, controllingPlayerId, runtime, store],
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const resolveTargetMatches = useCallback(
|
|
364
|
+
(
|
|
365
|
+
targetKind: BoardTargetKind,
|
|
366
|
+
targetId: string,
|
|
367
|
+
interactionKeys?: readonly string[],
|
|
368
|
+
): Array<MatchingDescriptor<I>> => {
|
|
369
|
+
const allowedInteractionKeys = interactionKeys
|
|
370
|
+
? new Set(interactionKeys)
|
|
371
|
+
: null;
|
|
246
372
|
const matches = interactions.flatMap((descriptor) => {
|
|
373
|
+
if (
|
|
374
|
+
allowedInteractionKeys &&
|
|
375
|
+
!allowedInteractionKeys.has(descriptor.interactionKey)
|
|
376
|
+
) {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
247
379
|
if (!descriptor.available) return [];
|
|
248
|
-
const
|
|
380
|
+
const draft = store.getDraft(descriptor.interactionKey);
|
|
381
|
+
const inputKey = inputKeyForTarget(
|
|
382
|
+
descriptor,
|
|
383
|
+
targetKind,
|
|
384
|
+
targetId,
|
|
385
|
+
draft,
|
|
386
|
+
);
|
|
249
387
|
if (!inputKey) return [];
|
|
250
|
-
const input = inputByTarget(descriptor, targetKind, targetId);
|
|
388
|
+
const input = inputByTarget(descriptor, targetKind, targetId, draft);
|
|
251
389
|
if (
|
|
252
390
|
input?.domain.type === "target" &&
|
|
253
391
|
!isTargetSelectable(
|
|
@@ -258,7 +396,7 @@ export function useBoardInteractions<I extends string = string>(
|
|
|
258
396
|
) {
|
|
259
397
|
return [];
|
|
260
398
|
}
|
|
261
|
-
const targets = eligibleTargetsForInput(descriptor, inputKey);
|
|
399
|
+
const targets = eligibleTargetsForInput(descriptor, inputKey, draft);
|
|
262
400
|
if (!targets || !targets.includes(targetId)) return [];
|
|
263
401
|
return [
|
|
264
402
|
{
|
|
@@ -268,62 +406,32 @@ export function useBoardInteractions<I extends string = string>(
|
|
|
268
406
|
},
|
|
269
407
|
];
|
|
270
408
|
});
|
|
409
|
+
return matches;
|
|
410
|
+
},
|
|
411
|
+
[armedIds, interactions, store],
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const selectByKind = useCallback(
|
|
415
|
+
async (
|
|
416
|
+
targetKind: BoardTargetKind,
|
|
417
|
+
targetId: string,
|
|
418
|
+
extraInputs?: Record<string, unknown>,
|
|
419
|
+
interactionKeys?: readonly string[],
|
|
420
|
+
): Promise<BoardSelectionResult<I>> => {
|
|
421
|
+
if (!controllingPlayerId) return { status: "none" };
|
|
422
|
+
const matches = resolveTargetMatches(
|
|
423
|
+
targetKind,
|
|
424
|
+
targetId,
|
|
425
|
+
interactionKeys,
|
|
426
|
+
);
|
|
271
427
|
const selected = selectDispatchCandidate(matches, targetKind, targetId);
|
|
272
428
|
if (selected) {
|
|
273
429
|
const { descriptor, inputKey } = selected;
|
|
274
|
-
|
|
275
|
-
const currentDraft = store.getDraft(descriptor.interactionKey);
|
|
276
|
-
const selection =
|
|
277
|
-
input?.domain.type === "target" ? input.domain.selection : undefined;
|
|
278
|
-
const nextTargetValue =
|
|
279
|
-
selection?.mode === "many"
|
|
280
|
-
? toggleManyValue(currentDraft[inputKey], targetId, selection)
|
|
281
|
-
: targetId;
|
|
282
|
-
const params: Record<string, unknown> = {
|
|
283
|
-
...currentDraft,
|
|
284
|
-
...(extraInputs ?? {}),
|
|
285
|
-
[inputKey]: nextTargetValue,
|
|
286
|
-
};
|
|
287
|
-
for (const [key, value] of Object.entries(extraInputs ?? {})) {
|
|
288
|
-
store.setInput(descriptor.interactionKey, key, value);
|
|
289
|
-
}
|
|
290
|
-
store.setInput(descriptor.interactionKey, inputKey, nextTargetValue);
|
|
291
|
-
if (
|
|
292
|
-
descriptor.commit.mode !== "autoWhenReady" ||
|
|
293
|
-
(selection?.mode === "many" && !isDraftReady(descriptor, params))
|
|
294
|
-
) {
|
|
295
|
-
store.arm(interactionArmScope(descriptor), descriptor.interactionKey);
|
|
296
|
-
return descriptor.interactionKey;
|
|
297
|
-
}
|
|
298
|
-
// Fill any remaining required inputs with `null` so the reducer
|
|
299
|
-
// bundle can decide whether to reject (missing) or accept
|
|
300
|
-
// (optional). This matches the Catan "spaceId + null stealee"
|
|
301
|
-
// pattern without forcing callers to memorise each interaction's
|
|
302
|
-
// full input shape.
|
|
303
|
-
for (const required of interactionInputKeys(descriptor)) {
|
|
304
|
-
if (!(required in params)) params[required] = null;
|
|
305
|
-
}
|
|
306
|
-
try {
|
|
307
|
-
await runtime.submitInteraction(
|
|
308
|
-
controllingPlayerId,
|
|
309
|
-
descriptor.interactionId,
|
|
310
|
-
params,
|
|
311
|
-
);
|
|
312
|
-
store.clearInput(descriptor.interactionKey);
|
|
313
|
-
if (
|
|
314
|
-
store.getArmed(interactionArmScope(descriptor)) ===
|
|
315
|
-
descriptor.interactionKey
|
|
316
|
-
) {
|
|
317
|
-
store.arm(interactionArmScope(descriptor), null);
|
|
318
|
-
}
|
|
319
|
-
return descriptor.interactionKey;
|
|
320
|
-
} catch (error) {
|
|
321
|
-
throw validationErrorFromUnknown(error);
|
|
322
|
-
}
|
|
430
|
+
return resolveSelection(descriptor, inputKey, targetId, extraInputs);
|
|
323
431
|
}
|
|
324
|
-
return
|
|
432
|
+
return { status: "none" };
|
|
325
433
|
},
|
|
326
|
-
[
|
|
434
|
+
[controllingPlayerId, resolveSelection, resolveTargetMatches],
|
|
327
435
|
);
|
|
328
436
|
|
|
329
437
|
const select = useMemo(
|
|
@@ -340,11 +448,55 @@ export function useBoardInteractions<I extends string = string>(
|
|
|
340
448
|
[selectByKind],
|
|
341
449
|
);
|
|
342
450
|
|
|
451
|
+
const targetState = useCallback(
|
|
452
|
+
(
|
|
453
|
+
targetKind: BoardTargetKind,
|
|
454
|
+
targetId: string,
|
|
455
|
+
options: Pick<
|
|
456
|
+
BoardTargetLayerOptions,
|
|
457
|
+
"enabled" | "interactionKeys"
|
|
458
|
+
> = {},
|
|
459
|
+
): InteractiveTargetState => {
|
|
460
|
+
const enabled = options.enabled !== false;
|
|
461
|
+
const matches = enabled
|
|
462
|
+
? resolveTargetMatches(targetKind, targetId, options.interactionKeys)
|
|
463
|
+
: [];
|
|
464
|
+
const armed = matches.filter((match) => match.armed);
|
|
465
|
+
const candidates = armed.length > 0 ? armed : matches;
|
|
466
|
+
const selected = candidates[0] ?? null;
|
|
467
|
+
const conflict = candidates.length > 1;
|
|
468
|
+
const pending = selected
|
|
469
|
+
? pendingInteractionKey === selected.descriptor.interactionKey
|
|
470
|
+
: false;
|
|
471
|
+
const eligible = enabled && !!selected && !conflict;
|
|
472
|
+
return {
|
|
473
|
+
kind: targetKind,
|
|
474
|
+
id: targetId,
|
|
475
|
+
eligible,
|
|
476
|
+
selectable: eligible && !!controllingPlayerId,
|
|
477
|
+
hovered: false,
|
|
478
|
+
interactionKey: selected?.descriptor.interactionKey,
|
|
479
|
+
interactionId: selected?.descriptor.interactionId,
|
|
480
|
+
inputKey: selected?.inputKey,
|
|
481
|
+
pending,
|
|
482
|
+
conflict,
|
|
483
|
+
unavailableReason: candidateUnavailableReason(selected, conflict),
|
|
484
|
+
};
|
|
485
|
+
},
|
|
486
|
+
[controllingPlayerId, pendingInteractionKey, resolveTargetMatches],
|
|
487
|
+
);
|
|
488
|
+
|
|
343
489
|
const targetLayers = useMemo(() => {
|
|
344
490
|
const createLayer =
|
|
345
491
|
(targetKind: BoardTargetKind): BoardTargetLayerFactory =>
|
|
346
492
|
(layerOptions = {}) => {
|
|
347
|
-
const {
|
|
493
|
+
const {
|
|
494
|
+
enabled,
|
|
495
|
+
interactionKeys,
|
|
496
|
+
extraInputs,
|
|
497
|
+
onBeforeSelect,
|
|
498
|
+
onError,
|
|
499
|
+
} = layerOptions;
|
|
348
500
|
const resolveExtraInputs = (targetId: string) =>
|
|
349
501
|
typeof extraInputs === "function"
|
|
350
502
|
? extraInputs(targetId)
|
|
@@ -352,14 +504,39 @@ export function useBoardInteractions<I extends string = string>(
|
|
|
352
504
|
return {
|
|
353
505
|
enabled,
|
|
354
506
|
eligible: eligible[targetKind],
|
|
507
|
+
targetState: (targetId: string) => ({
|
|
508
|
+
...targetState(targetKind, targetId, { enabled, interactionKeys }),
|
|
509
|
+
select: async () => {
|
|
510
|
+
if (enabled === false) return { status: "none" };
|
|
511
|
+
onBeforeSelect?.();
|
|
512
|
+
try {
|
|
513
|
+
return await selectByKind(
|
|
514
|
+
targetKind,
|
|
515
|
+
targetId,
|
|
516
|
+
resolveExtraInputs(targetId),
|
|
517
|
+
interactionKeys,
|
|
518
|
+
);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
onError?.(error);
|
|
521
|
+
if (!onError) throw error;
|
|
522
|
+
return { status: "none" };
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
}),
|
|
355
526
|
selectTargetId: async (targetId: string) => {
|
|
356
|
-
if (enabled === false) return;
|
|
527
|
+
if (enabled === false) return { status: "none" };
|
|
357
528
|
onBeforeSelect?.();
|
|
358
529
|
try {
|
|
359
|
-
await
|
|
530
|
+
return await selectByKind(
|
|
531
|
+
targetKind,
|
|
532
|
+
targetId,
|
|
533
|
+
resolveExtraInputs(targetId),
|
|
534
|
+
interactionKeys,
|
|
535
|
+
);
|
|
360
536
|
} catch (error) {
|
|
361
537
|
onError?.(error);
|
|
362
538
|
if (!onError) throw error;
|
|
539
|
+
return { status: "none" };
|
|
363
540
|
}
|
|
364
541
|
},
|
|
365
542
|
};
|
|
@@ -370,11 +547,57 @@ export function useBoardInteractions<I extends string = string>(
|
|
|
370
547
|
space: createLayer("space"),
|
|
371
548
|
tile: createLayer("tile"),
|
|
372
549
|
};
|
|
373
|
-
}, [eligible,
|
|
550
|
+
}, [eligible, selectByKind, targetState]);
|
|
551
|
+
|
|
552
|
+
const pendingInteraction = useMemo<BoardPendingInteraction<I> | null>(() => {
|
|
553
|
+
if (!pendingInteractionKey) return null;
|
|
554
|
+
const descriptor = interactions.find(
|
|
555
|
+
(candidate) => candidate.interactionKey === pendingInteractionKey,
|
|
556
|
+
);
|
|
557
|
+
if (!descriptor) return null;
|
|
558
|
+
return {
|
|
559
|
+
interactionKey: descriptor.interactionKey as I,
|
|
560
|
+
descriptor,
|
|
561
|
+
missingInputs: missingInputsForDraft(
|
|
562
|
+
descriptor,
|
|
563
|
+
drafts[descriptor.interactionKey] ?? {},
|
|
564
|
+
),
|
|
565
|
+
clear: () => clearPendingInteraction(descriptor),
|
|
566
|
+
};
|
|
567
|
+
}, [clearPendingInteraction, drafts, interactions, pendingInteractionKey]);
|
|
568
|
+
|
|
569
|
+
const selectTarget = useCallback(
|
|
570
|
+
(
|
|
571
|
+
descriptor: InteractionDescriptor<I>,
|
|
572
|
+
_targetKind: BoardTargetKind,
|
|
573
|
+
targetId: string,
|
|
574
|
+
inputKey: string,
|
|
575
|
+
extraInputs?: Record<string, unknown>,
|
|
576
|
+
) => resolveSelection(descriptor, inputKey, targetId, extraInputs),
|
|
577
|
+
[resolveSelection],
|
|
578
|
+
);
|
|
374
579
|
|
|
375
580
|
return useMemo(
|
|
376
|
-
() => ({
|
|
377
|
-
|
|
581
|
+
() => ({
|
|
582
|
+
interactions,
|
|
583
|
+
eligible,
|
|
584
|
+
isEligible,
|
|
585
|
+
pendingInteraction,
|
|
586
|
+
select,
|
|
587
|
+
targetLayers,
|
|
588
|
+
targetState,
|
|
589
|
+
selectTarget,
|
|
590
|
+
}),
|
|
591
|
+
[
|
|
592
|
+
interactions,
|
|
593
|
+
eligible,
|
|
594
|
+
isEligible,
|
|
595
|
+
pendingInteraction,
|
|
596
|
+
select,
|
|
597
|
+
targetLayers,
|
|
598
|
+
targetState,
|
|
599
|
+
selectTarget,
|
|
600
|
+
],
|
|
378
601
|
);
|
|
379
602
|
}
|
|
380
603
|
|
|
@@ -392,19 +615,11 @@ function isTargetSelectable(
|
|
|
392
615
|
return selection.max === undefined || current.length < selection.max;
|
|
393
616
|
}
|
|
394
617
|
|
|
395
|
-
function
|
|
618
|
+
function missingInputsForDraft(
|
|
396
619
|
descriptor: InteractionDescriptor,
|
|
397
620
|
draft: Readonly<Record<string, unknown>>,
|
|
398
|
-
):
|
|
399
|
-
|
|
400
|
-
if (hasInteractionFieldErrors(fieldErrors)) return false;
|
|
401
|
-
return interactionInputKeys(descriptor).every((key) => {
|
|
402
|
-
const input = descriptor.inputs.find((candidate) => candidate.key === key);
|
|
403
|
-
const value =
|
|
404
|
-
draft[key] ??
|
|
405
|
-
(input && "defaultValue" in input ? input.defaultValue : undefined);
|
|
406
|
-
return input ? isInputValueReady(input, value) : value !== undefined;
|
|
407
|
-
});
|
|
621
|
+
): string[] {
|
|
622
|
+
return [...getInteractionDraftReadiness(descriptor, draft).missingInputs];
|
|
408
623
|
}
|
|
409
624
|
|
|
410
625
|
interface MatchingDescriptor<I extends string> {
|
|
@@ -432,3 +647,13 @@ function selectDispatchCandidate<I extends string>(
|
|
|
432
647
|
}
|
|
433
648
|
return candidates[0] ?? null;
|
|
434
649
|
}
|
|
650
|
+
|
|
651
|
+
function candidateUnavailableReason<I extends string>(
|
|
652
|
+
match: MatchingDescriptor<I> | null,
|
|
653
|
+
conflict: boolean,
|
|
654
|
+
): string | undefined {
|
|
655
|
+
if (conflict) return "conflict";
|
|
656
|
+
if (!match) return "unavailable";
|
|
657
|
+
if (!match.descriptor.available) return "unavailable";
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|