@dreamboard-games/sdk 0.5.0-alpha.2 → 0.5.0-alpha.21

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.
Files changed (150) hide show
  1. package/README.md +462 -53
  2. package/dist/board-element-CqtxU5VZ.d.ts +13 -0
  3. package/dist/chunk-3RFGPBES.js +1792 -0
  4. package/dist/chunk-3RFGPBES.js.map +1 -0
  5. package/dist/chunk-CZRPUWCT.js +1602 -0
  6. package/dist/chunk-CZRPUWCT.js.map +1 -0
  7. package/dist/chunk-DMSXVMYY.js +7199 -0
  8. package/dist/chunk-DMSXVMYY.js.map +1 -0
  9. package/dist/chunk-U6R7FAH4.js +630 -0
  10. package/dist/chunk-U6R7FAH4.js.map +1 -0
  11. package/dist/chunk-YT3RPO5J.js +635 -0
  12. package/dist/chunk-YT3RPO5J.js.map +1 -0
  13. package/dist/diagnostics-ZAPEBlJP.d.ts +64 -0
  14. package/dist/extract-CwJWDrSD.d.ts +6850 -0
  15. package/dist/index.d.ts +5177 -1
  16. package/dist/index.js +1990 -6
  17. package/dist/index.js.map +1 -1
  18. package/dist/react.d.ts +158 -0
  19. package/dist/react.js +1119 -0
  20. package/dist/react.js.map +1 -0
  21. package/dist/reducer.d.ts +1028 -97
  22. package/dist/reducer.js +4892 -125
  23. package/dist/reducer.js.map +1 -1
  24. package/dist/shortcuts-B_RrufOM.d.ts +822 -0
  25. package/dist/testing.d.ts +529 -569
  26. package/dist/testing.js +2588 -934
  27. package/dist/testing.js.map +1 -1
  28. package/dist/types-BxXRINRF.d.ts +118 -0
  29. package/package.json +39 -144
  30. package/dist/HandView-Bi2lxzQP.d.ts +0 -79
  31. package/dist/ResourceCounter-BFxJknqp.d.ts +0 -98
  32. package/dist/ThemeProvider-CElaL2ik.d.ts +0 -99
  33. package/dist/attributes-DbvyMbXw.d.ts +0 -68
  34. package/dist/browser-interaction.d.ts +0 -910
  35. package/dist/browser-interaction.js +0 -119
  36. package/dist/browser-interaction.js.map +0 -1
  37. package/dist/chunk-32PFKDV7.js +0 -11705
  38. package/dist/chunk-32PFKDV7.js.map +0 -1
  39. package/dist/chunk-3BAD4VC2.js +0 -263
  40. package/dist/chunk-3BAD4VC2.js.map +0 -1
  41. package/dist/chunk-3SKDNDPG.js +0 -104
  42. package/dist/chunk-3SKDNDPG.js.map +0 -1
  43. package/dist/chunk-5YV2WHJ4.js +0 -25
  44. package/dist/chunk-5YV2WHJ4.js.map +0 -1
  45. package/dist/chunk-ANRB4XKA.js +0 -90
  46. package/dist/chunk-ANRB4XKA.js.map +0 -1
  47. package/dist/chunk-BI4G3PM2.js +0 -1864
  48. package/dist/chunk-BI4G3PM2.js.map +0 -1
  49. package/dist/chunk-EVCLTY4Q.js +0 -935
  50. package/dist/chunk-EVCLTY4Q.js.map +0 -1
  51. package/dist/chunk-GDPKTBUR.js +0 -564
  52. package/dist/chunk-GDPKTBUR.js.map +0 -1
  53. package/dist/chunk-H6VDGFL5.js +0 -2903
  54. package/dist/chunk-H6VDGFL5.js.map +0 -1
  55. package/dist/chunk-LR3ZTWQF.js +0 -5960
  56. package/dist/chunk-LR3ZTWQF.js.map +0 -1
  57. package/dist/chunk-PF7L4BMG.js +0 -117
  58. package/dist/chunk-PF7L4BMG.js.map +0 -1
  59. package/dist/chunk-PN5O6GG2.js +0 -32
  60. package/dist/chunk-PN5O6GG2.js.map +0 -1
  61. package/dist/chunk-PZ5AY32C.js +0 -10
  62. package/dist/chunk-PZ5AY32C.js.map +0 -1
  63. package/dist/chunk-S2JEDVWH.js +0 -7165
  64. package/dist/chunk-S2JEDVWH.js.map +0 -1
  65. package/dist/chunk-T3ZKNUZ7.js +0 -1
  66. package/dist/chunk-T3ZKNUZ7.js.map +0 -1
  67. package/dist/chunk-UGH54WZ2.js +0 -223
  68. package/dist/chunk-UGH54WZ2.js.map +0 -1
  69. package/dist/chunk-VCDWP5VL.js +0 -746
  70. package/dist/chunk-VCDWP5VL.js.map +0 -1
  71. package/dist/chunk-VDXOF4FW.js +0 -69
  72. package/dist/chunk-VDXOF4FW.js.map +0 -1
  73. package/dist/chunk-WHR5UW3F.js +0 -1988
  74. package/dist/chunk-WHR5UW3F.js.map +0 -1
  75. package/dist/chunk-Y75CFE77.js +0 -17
  76. package/dist/chunk-Y75CFE77.js.map +0 -1
  77. package/dist/chunk-Z7QCREWI.js +0 -3435
  78. package/dist/chunk-Z7QCREWI.js.map +0 -1
  79. package/dist/chunk-ZZGN2ATS.js +0 -1941
  80. package/dist/chunk-ZZGN2ATS.js.map +0 -1
  81. package/dist/components-gAbIz7hK.d.ts +0 -1284
  82. package/dist/definitions-BgmV_GhC.d.ts +0 -300
  83. package/dist/diagnostics-DIi3ina7.d.ts +0 -50
  84. package/dist/digest.d.ts +0 -8
  85. package/dist/game-CsZScpcn.d.ts +0 -763
  86. package/dist/hex-board-view-aKkblDp6.d.ts +0 -1228
  87. package/dist/index-CcT9Q7N7.d.ts +0 -193
  88. package/dist/index.d-DjzoK7zn.d.ts +0 -2101
  89. package/dist/package-set.d.ts +0 -13
  90. package/dist/package-set.js +0 -12
  91. package/dist/package-set.js.map +0 -1
  92. package/dist/player-state-Cqpyeql0.d.ts +0 -371
  93. package/dist/plugin-runtime-contract.d.ts +0 -17
  94. package/dist/plugin-runtime-contract.js +0 -92
  95. package/dist/plugin-runtime-contract.js.map +0 -1
  96. package/dist/primitive-props-BNHDkgd7.d.ts +0 -16
  97. package/dist/protocol-dYgafTYY.d.ts +0 -314
  98. package/dist/reducer/advanced.d.ts +0 -82
  99. package/dist/reducer/advanced.js +0 -51
  100. package/dist/reducer/advanced.js.map +0 -1
  101. package/dist/reducer-contract.d.ts +0 -11
  102. package/dist/reducer-contract.js +0 -16
  103. package/dist/reducer-contract.js.map +0 -1
  104. package/dist/reference-games/index.d.ts +0 -31
  105. package/dist/reference-games/index.js +0 -48
  106. package/dist/reference-games/index.js.map +0 -1
  107. package/dist/runtime/primitives.d.ts +0 -250
  108. package/dist/runtime/primitives.js +0 -189
  109. package/dist/runtime/primitives.js.map +0 -1
  110. package/dist/runtime/runtime-api.d.ts +0 -2
  111. package/dist/runtime/runtime-api.js +0 -1
  112. package/dist/runtime/runtime-api.js.map +0 -1
  113. package/dist/runtime/workspace-contract.d.ts +0 -392
  114. package/dist/runtime/workspace-contract.js +0 -28
  115. package/dist/runtime/workspace-contract.js.map +0 -1
  116. package/dist/runtime-RJ5orDJU.d.ts +0 -1699
  117. package/dist/runtime-api-Bz5pwNU_.d.ts +0 -296
  118. package/dist/runtime-json-CQ9QbLZ5.d.ts +0 -5
  119. package/dist/runtime.d.ts +0 -83
  120. package/dist/runtime.js +0 -242
  121. package/dist/runtime.js.map +0 -1
  122. package/dist/schema.d.ts +0 -379
  123. package/dist/stale-contract-artifact-error-XLaweZtF.d.ts +0 -18
  124. package/dist/testing-compiler.d.ts +0 -37
  125. package/dist/testing-compiler.js +0 -272
  126. package/dist/testing-compiler.js.map +0 -1
  127. package/dist/testing-runtime.d.ts +0 -119
  128. package/dist/testing-runtime.js +0 -212
  129. package/dist/testing-runtime.js.map +0 -1
  130. package/dist/types-DJj5MJkl.d.ts +0 -256
  131. package/dist/types-DR7DoB1x.d.ts +0 -28
  132. package/dist/types-JWCYHmu7.d.ts +0 -122
  133. package/dist/types.d.ts +0 -1958
  134. package/dist/types.js +0 -14
  135. package/dist/types.js.map +0 -1
  136. package/dist/ui/components.d.ts +0 -17
  137. package/dist/ui/components.js +0 -216
  138. package/dist/ui/components.js.map +0 -1
  139. package/dist/ui/defaults.d.ts +0 -19
  140. package/dist/ui/defaults.js +0 -104
  141. package/dist/ui/defaults.js.map +0 -1
  142. package/dist/ui/player-state.d.ts +0 -2
  143. package/dist/ui/player-state.js +0 -1
  144. package/dist/ui/player-state.js.map +0 -1
  145. package/dist/ui/plugin-styles.css +0 -2
  146. package/dist/ui-contract-GTUPkf8z.d.ts +0 -1168
  147. package/dist/ui.d.ts +0 -317
  148. package/dist/ui.js +0 -277
  149. package/dist/ui.js.map +0 -1
  150. package/dist/views-BpuWcOyN.d.ts +0 -1641
package/dist/index.js CHANGED
@@ -1,12 +1,1996 @@
1
1
  import {
2
- DREAMBOARD_SDK_PACKAGES,
3
- DREAMBOARD_SDK_PACKAGE_SET,
4
- DREAMBOARD_SDK_VERSION
5
- } from "./chunk-Y75CFE77.js";
6
- import "./chunk-PZ5AY32C.js";
2
+ AmbiguousTargetError,
3
+ GESTURE_THRESHOLDS,
4
+ createGameInstance,
5
+ createGestureRecognizer,
6
+ isSameDropTarget,
7
+ magneticDropPoint,
8
+ runtimeFeatures
9
+ } from "./chunk-3RFGPBES.js";
10
+ import {
11
+ CancelInteractionCommandSchema,
12
+ DREAMBOARD_PLUGIN_PROTOCOL,
13
+ DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
14
+ GameEventDetailSchema,
15
+ GameEventSchema,
16
+ GameOutcomeSchema,
17
+ GameplayBasisSchema,
18
+ HostToPluginEnvelopeSchema,
19
+ HostToPluginPayloadSchema,
20
+ InteractionResultSchema,
21
+ PluginGameplayFrameSchema,
22
+ PluginPlayerSummarySchema,
23
+ PluginSessionDescriptorSchema,
24
+ PluginToHostEnvelopeSchema,
25
+ PluginToHostPayloadSchema,
26
+ SeatFrameSchema,
27
+ SeatProjectionBundleSchema,
28
+ SimultaneousPhaseSnapshotSchema,
29
+ SubmitInteractionCommandSchema,
30
+ SystemActionEventSchema,
31
+ ZoneHandlesSnapshotSchema,
32
+ computePluginActionSetVersion,
33
+ createPluginProtocolEnvelopeSchema,
34
+ createSourceLifecycle,
35
+ digestPluginCommandRequest,
36
+ digestPluginGameplayFrame,
37
+ materializePluginGameplayFrame,
38
+ parseHostToPluginEnvelope,
39
+ parsePluginGameplayFrame,
40
+ parsePluginSessionDescriptor,
41
+ parsePluginToHostEnvelope,
42
+ staticSource
43
+ } from "./chunk-YT3RPO5J.js";
44
+ import {
45
+ InputDomainSchema,
46
+ InteractionAvailabilitySchema,
47
+ InteractionCommitPolicySchema,
48
+ InteractionDescriptorSchema,
49
+ InteractionInputDescriptorSchema,
50
+ ViewCardSchema,
51
+ boardSpaceHostId,
52
+ isHiddenCardId,
53
+ parseBoardSpaceHostId
54
+ } from "./chunk-U6R7FAH4.js";
55
+ import {
56
+ RuntimeJsonSchema,
57
+ SeatBoardTopologySchema,
58
+ boardEdgeId,
59
+ boardVertexId,
60
+ canonicalizePluginRuntimeJson,
61
+ createHexTopology,
62
+ createHexTopologyCache,
63
+ digestPluginRuntimeJson,
64
+ encodeCanonicalPluginRuntimeJson,
65
+ getPublicTileFootprint,
66
+ immutableCopy,
67
+ inputValueInDomain,
68
+ parseBoardElementId,
69
+ parsePerPlayerInstanceId,
70
+ perPlayerInstanceId,
71
+ requireLookup,
72
+ zonePositions
73
+ } from "./chunk-CZRPUWCT.js";
74
+
75
+ // src/package-set.ts
76
+ var DREAMBOARD_SDK_VERSION = true ? "0.5.0-alpha.21" : "0.0.0-development";
77
+ var DREAMBOARD_SDK_PACKAGES = {
78
+ "@dreamboard-games/sdk": DREAMBOARD_SDK_VERSION
79
+ };
80
+ var DREAMBOARD_SDK_PACKAGE_SET = {
81
+ version: 1,
82
+ sdkVersion: DREAMBOARD_SDK_VERSION,
83
+ packages: DREAMBOARD_SDK_PACKAGES
84
+ };
85
+
86
+ // src/headless/sources/iframe.ts
87
+ function iframeSource(options = {}) {
88
+ const target = window;
89
+ const parent = target.parent;
90
+ let channel = null;
91
+ let sequence = 0;
92
+ let receivedSequence = -1;
93
+ const send = (payload) => {
94
+ if (!channel) throw new Error("Iframe source is not initialized.");
95
+ parent.postMessage(
96
+ {
97
+ protocol: DREAMBOARD_PLUGIN_PROTOCOL,
98
+ version: DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
99
+ channelId: channel.id,
100
+ sequence: ++sequence,
101
+ payload
102
+ },
103
+ channel.origin
104
+ );
105
+ };
106
+ const lifecycle = createSourceLifecycle({
107
+ timeoutMs: options.timeoutMs,
108
+ followHostSeat: options.followHostSeat,
109
+ send,
110
+ recover: () => send({ type: "runtime.resume" }),
111
+ close: () => target.removeEventListener("message", receive)
112
+ });
113
+ const isOpen = () => {
114
+ const { connection } = lifecycle.source.store.get();
115
+ return connection !== "closed" && connection !== "failed";
116
+ };
117
+ function receive(event) {
118
+ if (event.source !== parent) return;
119
+ const parsed = HostToPluginEnvelopeSchema.safeParse(event.data);
120
+ if (!parsed.success) {
121
+ const data = event.data;
122
+ if (channel && event.origin === channel.origin && typeof data === "object" && data !== null && "channelId" in data && data.channelId === channel.id) {
123
+ try {
124
+ send({
125
+ type: "runtime.error",
126
+ code: "host-runtime-protocol-mismatch",
127
+ message: "Iframe source received an invalid host envelope."
128
+ });
129
+ } finally {
130
+ lifecycle.fail(new Error("Invalid iframe host envelope."));
131
+ }
132
+ }
133
+ return;
134
+ }
135
+ const envelope = parsed.data;
136
+ if (!channel) {
137
+ if (envelope.payload.type !== "runtime.init") return;
138
+ channel = { id: envelope.channelId, origin: event.origin };
139
+ }
140
+ if (channel.id !== envelope.channelId || channel.origin !== event.origin || envelope.sequence <= receivedSequence)
141
+ return;
142
+ receivedSequence = envelope.sequence;
143
+ const payload = envelope.payload;
144
+ if (payload.type === "runtime.init") {
145
+ lifecycle.session(payload.session);
146
+ if (isOpen()) send({ type: "runtime.ready" });
147
+ } else if (payload.type === "gameplay.frame")
148
+ lifecycle.frame(payload.frame);
149
+ else lifecycle.result(payload);
150
+ if (isOpen()) send({ type: "runtime.ack", sequence: envelope.sequence });
151
+ }
152
+ target.addEventListener("message", receive);
153
+ return lifecycle.source;
154
+ }
155
+
156
+ // src/shared/protocol/gameplay-wire.ts
157
+ import * as z from "zod";
158
+ var GameplayCloseCode = {
159
+ GoingAway: 1001,
160
+ CredentialExpired: 4e3,
161
+ CredentialInvalid: 4001,
162
+ RefreshContextMismatch: 4002,
163
+ PermissionDenied: 4100,
164
+ ProtocolViolation: 4200,
165
+ FrameBeforeAuth: 4201,
166
+ SlowConsumer: 4300,
167
+ TooManyRequests: 4301
168
+ };
169
+ var GameplayCredentialSchema = z.discriminatedUnion("kind", [
170
+ z.object({ kind: z.literal("user"), token: z.string().min(1).max(16384) }).strict(),
171
+ z.object({ kind: z.literal("demo"), secret: z.string().min(1).max(1024) }).strict()
172
+ ]);
173
+ var AuthConnectFrameSchema = z.object({
174
+ type: z.literal("auth.connect"),
175
+ credential: GameplayCredentialSchema,
176
+ sessionId: z.string().uuid(),
177
+ playerId: z.string().min(1).max(128)
178
+ }).strict();
179
+ var AuthRefreshFrameSchema = z.object({
180
+ type: z.literal("auth.refresh"),
181
+ credential: GameplayCredentialSchema
182
+ }).strict();
183
+ var SessionResumeFrameSchema = z.object({
184
+ type: z.literal("session.resume"),
185
+ lastSeenLogCursor: z.number().int().nonnegative().nullable(),
186
+ unacknowledgedClientActionIds: z.array(z.string().min(1)).max(32)
187
+ });
188
+ var AuthAcceptedFrameSchema = z.object({
189
+ type: z.literal("auth.accepted"),
190
+ expiresAt: z.string().datetime()
191
+ });
192
+ var GameplayBackpressureReasonSchema = z.enum([
193
+ "queue_full",
194
+ "capacity_full"
195
+ ]);
196
+ var GameplayBackpressureOperationSchema = z.enum([
197
+ "session.resume",
198
+ "interaction.submit",
199
+ "interaction.cancel"
200
+ ]);
201
+ var GameplayBackpressureFrameSchema = z.object({
202
+ type: z.literal("gameplay.backpressure"),
203
+ reason: GameplayBackpressureReasonSchema,
204
+ retryAfterMs: z.number().int().positive(),
205
+ message: z.string().min(1),
206
+ operation: GameplayBackpressureOperationSchema,
207
+ clientActionId: z.string().min(1).optional()
208
+ });
209
+ var SessionSnapshotFrameSchema = z.object({
210
+ type: z.literal("session.snapshot"),
211
+ frame: PluginGameplayFrameSchema
212
+ });
213
+ var ClientGameplayFrameSchema = z.discriminatedUnion("type", [
214
+ AuthConnectFrameSchema,
215
+ AuthRefreshFrameSchema,
216
+ SessionResumeFrameSchema,
217
+ SubmitInteractionCommandSchema,
218
+ CancelInteractionCommandSchema
219
+ ]);
220
+ var ServerGameplayFrameSchema = z.discriminatedUnion("type", [
221
+ AuthAcceptedFrameSchema,
222
+ GameplayBackpressureFrameSchema,
223
+ SessionSnapshotFrameSchema,
224
+ InteractionResultSchema
225
+ ]);
226
+
227
+ // src/headless/sources/host-websocket.ts
228
+ function hostSource(options) {
229
+ const { url, playerId, getCredential } = options;
230
+ const sessionId = options.session.sessionId;
231
+ let socket = null;
232
+ let generation = 0;
233
+ let disposed = false;
234
+ let authenticated = false;
235
+ let queued = null;
236
+ let reconnects = 0;
237
+ let refreshTimer;
238
+ let snapshotTimer;
239
+ let openTimer;
240
+ let cleanup = () => {
241
+ };
242
+ const timeoutMs = options.timeoutMs ?? 1e4;
243
+ const send = (frame) => {
244
+ if (!socket || socket.readyState !== WebSocket.OPEN)
245
+ throw new Error("Gameplay socket is not open.");
246
+ socket.send(JSON.stringify(ClientGameplayFrameSchema.parse(frame)));
247
+ };
248
+ const resume = () => send({
249
+ type: "session.resume",
250
+ lastSeenLogCursor: null,
251
+ unacknowledgedClientActionIds: queued ? [queued.clientActionId] : []
252
+ });
253
+ const lifecycle = createSourceLifecycle({
254
+ context: {
255
+ sessionId,
256
+ playerId
257
+ },
258
+ timeoutMs,
259
+ send(command) {
260
+ if (!authenticated) {
261
+ queued = command;
262
+ return;
263
+ }
264
+ send(command);
265
+ },
266
+ recover() {
267
+ if (authenticated) resume();
268
+ else if (!openTimer) void connect();
269
+ },
270
+ close() {
271
+ disposed = true;
272
+ generation++;
273
+ clearTimeout(refreshTimer);
274
+ clearTimeout(openTimer);
275
+ clearTimeout(snapshotTimer);
276
+ cleanup();
277
+ socket?.close();
278
+ socket = null;
279
+ queued = null;
280
+ }
281
+ });
282
+ lifecycle.session(options.session);
283
+ async function connect() {
284
+ if (disposed) return;
285
+ const current = ++generation;
286
+ authenticated = false;
287
+ clearTimeout(refreshTimer);
288
+ clearTimeout(openTimer);
289
+ clearTimeout(snapshotTimer);
290
+ cleanup();
291
+ socket?.close();
292
+ socket = null;
293
+ openTimer = setTimeout(
294
+ () => lifecycle.fail(new Error("Gameplay socket authentication timed out.")),
295
+ timeoutMs
296
+ );
297
+ try {
298
+ const credential = await getCredential();
299
+ if (disposed || current !== generation) return;
300
+ const next = new WebSocket(url);
301
+ socket = next;
302
+ const active = () => !disposed && current === generation;
303
+ const onOpen = () => {
304
+ if (!active()) return;
305
+ try {
306
+ send({
307
+ type: "auth.connect",
308
+ credential,
309
+ sessionId,
310
+ playerId
311
+ });
312
+ } catch (error) {
313
+ lifecycle.fail(asError(error));
314
+ }
315
+ };
316
+ const onMessage = (event) => {
317
+ if (!active()) return;
318
+ try {
319
+ const value = JSON.parse(String(event.data));
320
+ if (typeof value !== "object" || value === null || !("type" in value))
321
+ throw new Error("Invalid gameplay frame.");
322
+ if (![
323
+ "auth.accepted",
324
+ "session.snapshot",
325
+ "interaction.result",
326
+ "gameplay.backpressure"
327
+ ].includes(String(value.type)))
328
+ return;
329
+ const frame = ServerGameplayFrameSchema.parse(value);
330
+ if (frame.type === "auth.accepted") {
331
+ clearTimeout(openTimer);
332
+ openTimer = void 0;
333
+ const first = !authenticated;
334
+ authenticated = true;
335
+ if (first) {
336
+ snapshotTimer = setTimeout(
337
+ () => lifecycle.fail(new Error("Gameplay snapshot timed out.")),
338
+ timeoutMs
339
+ );
340
+ resume();
341
+ if (queued) {
342
+ send(queued);
343
+ queued = null;
344
+ }
345
+ }
346
+ clearTimeout(refreshTimer);
347
+ if (credential.kind === "user") {
348
+ const ttl = Date.parse(frame.expiresAt) - Date.now();
349
+ if (ttl <= 0)
350
+ throw new Error("Gameplay credential is already expired.");
351
+ refreshTimer = setTimeout(
352
+ () => {
353
+ openTimer = setTimeout(
354
+ () => lifecycle.fail(
355
+ new Error("Gameplay credential refresh timed out.")
356
+ ),
357
+ timeoutMs
358
+ );
359
+ void getCredential().then((updated) => {
360
+ if (active())
361
+ send({ type: "auth.refresh", credential: updated });
362
+ }).catch((error) => {
363
+ if (active()) lifecycle.fail(asError(error));
364
+ });
365
+ },
366
+ Math.max(0, ttl - Math.min(6e4, ttl * 0.2))
367
+ );
368
+ }
369
+ } else if (!authenticated)
370
+ throw new Error("Gameplay frame arrived before authentication.");
371
+ else if (frame.type === "session.snapshot") {
372
+ clearTimeout(snapshotTimer);
373
+ snapshotTimer = void 0;
374
+ reconnects = 0;
375
+ lifecycle.frame(frame.frame);
376
+ } else if (frame.type === "interaction.result")
377
+ lifecycle.result(frame);
378
+ else lifecycle.fail(new Error(frame.message));
379
+ } catch (error) {
380
+ lifecycle.fail(asError(error));
381
+ }
382
+ };
383
+ const onClose = (event) => {
384
+ if (!active()) return;
385
+ authenticated = false;
386
+ lifecycle.recovering();
387
+ if ((/* @__PURE__ */ new Set([
388
+ GameplayCloseCode.CredentialInvalid,
389
+ GameplayCloseCode.RefreshContextMismatch,
390
+ GameplayCloseCode.PermissionDenied,
391
+ GameplayCloseCode.ProtocolViolation,
392
+ GameplayCloseCode.FrameBeforeAuth
393
+ ])).has(event.code) || reconnects++ > 0) {
394
+ lifecycle.fail(new Error(`Gameplay socket closed (${event.code}).`));
395
+ return;
396
+ }
397
+ void connect();
398
+ lifecycle.retry();
399
+ };
400
+ const onError = () => {
401
+ if (active()) next.close();
402
+ };
403
+ next.addEventListener("open", onOpen);
404
+ next.addEventListener("message", onMessage);
405
+ next.addEventListener("close", onClose);
406
+ next.addEventListener("error", onError);
407
+ cleanup = () => {
408
+ next.removeEventListener("open", onOpen);
409
+ next.removeEventListener("message", onMessage);
410
+ next.removeEventListener("close", onClose);
411
+ next.removeEventListener("error", onError);
412
+ };
413
+ } catch (error) {
414
+ if (!disposed && current === generation) lifecycle.fail(asError(error));
415
+ }
416
+ }
417
+ void connect();
418
+ return lifecycle.source;
419
+ }
420
+ function asError(error) {
421
+ return error instanceof Error ? error : new Error(String(error));
422
+ }
423
+
424
+ // src/headless/features/shortcuts.ts
425
+ import { createStore } from "@tanstack/store";
426
+ function shortcutsFeature(_game, context) {
427
+ const game = context[runtimeFeatures].game;
428
+ const configuration = createStore(null);
429
+ let registration = null;
430
+ function bindings() {
431
+ return configuration.get()?.bindings ?? [];
432
+ }
433
+ let disposed = false;
434
+ function resolve(binding, key, target, activity) {
435
+ if (disposed || game.connection !== "ready") return null;
436
+ if (binding.kind === "local") {
437
+ if (!activity || activity.pointerActive) return null;
438
+ const focused = activity.focusedTarget;
439
+ const card = focused?.kind === "card" ? game.cards.find(focused.value) : void 0;
440
+ const zone = focused?.kind === "zone" ? focused : card ? { kind: "zone", zoneId: card.zone, hostId: card.hostId } : null;
441
+ if (!zone || zone.zoneId !== binding.zoneId || !game.zones.find(zone.zoneId, zone.hostId))
442
+ return null;
443
+ const action = { key, target: zone };
444
+ return binding.getIsAvailable?.(action) === false ? null : { kind: "local", binding, action };
445
+ }
446
+ if (!target || game.request || target.kind !== binding.target) return null;
447
+ const routes = target.kind === "card" ? game.cards.find(target.value)?.getInteractions() ?? [] : game.interactions.list();
448
+ const interaction = routes.find((item) => item.key === binding.interaction);
449
+ if (!interaction || interaction.getStatus() !== "open") return null;
450
+ if (target.kind === "zone") {
451
+ if (target.zoneId !== binding.zoneId || !game.zones.find(target.zoneId, target.hostId))
452
+ return null;
453
+ } else {
454
+ const input = interaction.getInputs().find((item) => item.key === binding.input);
455
+ if (!input || !input.getIsEligible(target.value)) return null;
456
+ const domain = input.getDomain();
457
+ if (target.kind === "card" && input.kind !== "card") return null;
458
+ if (target.kind === "tile" && input.kind !== "tile") return null;
459
+ if (target.kind === "space" || target.kind === "edge" || target.kind === "vertex") {
460
+ if (domain.type !== "boardTarget" || domain.targetKind !== target.kind || domain.valueKind !== target.valueKind)
461
+ return null;
462
+ if (domain.valueKind === "board-id" && target.valueKind === "board-id" && domain.boardId !== target.boardId)
463
+ return null;
464
+ }
465
+ }
466
+ const params = binding.inputs({ key, target });
467
+ return interaction.getIsAvailable(params) && interaction.getIsReady(params) ? { kind: "interaction", interaction, params } : null;
468
+ }
469
+ const controller = {
470
+ configuration,
471
+ register(options) {
472
+ if (registration)
473
+ throw new Error("Mount useGameShortcuts once per GameProvider.");
474
+ const owned = {};
475
+ registration = owned;
476
+ configuration.setState(() => options);
477
+ return {
478
+ update(options2) {
479
+ if (registration !== owned) return;
480
+ configuration.setState(() => options2);
481
+ },
482
+ dispose() {
483
+ if (registration !== owned) return;
484
+ registration = null;
485
+ configuration.setState(() => null);
486
+ }
487
+ };
488
+ },
489
+ handle(key, target, activity) {
490
+ const candidates = bindings().filter((binding) => binding.keys.includes(key)).flatMap((binding) => {
491
+ const resolved = resolve(binding, key, target, activity);
492
+ return resolved ? [resolved] : [];
493
+ });
494
+ if (candidates.length !== 1) return null;
495
+ const candidate = candidates[0];
496
+ if (candidate.kind === "interaction")
497
+ return candidate.interaction.submit(candidate.params).then((result) => ({ kind: "interaction", result }));
498
+ try {
499
+ return Promise.resolve(candidate.binding.run(candidate.action)).then(
500
+ () => ({ kind: "local" })
501
+ );
502
+ } catch (cause) {
503
+ return Promise.reject(cause);
504
+ }
505
+ },
506
+ getHints(target, activity) {
507
+ const eligible = bindings().map((binding) => ({
508
+ binding,
509
+ keys: binding.keys.filter(
510
+ (key) => resolve(binding, key, target, activity)
511
+ )
512
+ }));
513
+ const counts = /* @__PURE__ */ new Map();
514
+ for (const { keys } of eligible)
515
+ for (const key of new Set(keys))
516
+ counts.set(key, (counts.get(key) ?? 0) + 1);
517
+ return eligible.flatMap(({ binding, keys }) => {
518
+ const unique = [...new Set(keys)].filter(
519
+ (key) => counts.get(key) === 1
520
+ );
521
+ return unique.length ? [
522
+ {
523
+ keys: unique,
524
+ label: binding.label,
525
+ ...binding.kind === "interaction" ? {
526
+ kind: "interaction",
527
+ interaction: binding.interaction
528
+ } : { kind: "local" }
529
+ }
530
+ ] : [];
531
+ });
532
+ }
533
+ };
534
+ return {
535
+ root: { shortcuts: controller },
536
+ dispose() {
537
+ disposed = true;
538
+ registration = null;
539
+ configuration.setState(() => null);
540
+ }
541
+ };
542
+ }
543
+
544
+ // src/headless/features/hand.ts
545
+ var seatCardId = (card) => card.id;
546
+ function handFeature(game, context, options = {}) {
547
+ const zones = /* @__PURE__ */ new Map();
548
+ for (const [id, value] of Object.entries(options.zones ?? {})) {
549
+ if (value === void 0) continue;
550
+ const sorts = new Map(
551
+ Object.entries(value.sorts).map(([mode, sort]) => [mode, sort.compare])
552
+ );
553
+ const defaultSort = value.defaultSort ?? null;
554
+ if (defaultSort !== null && !sorts.has(defaultSort)) {
555
+ throw new Error(
556
+ `Unknown default hand sort "${defaultSort}" for zone "${id}".`
557
+ );
558
+ }
559
+ zones.set(id, {
560
+ modes: Object.freeze(Object.keys(value.sorts)),
561
+ defaultSort,
562
+ sorts
563
+ });
564
+ }
565
+ const keyOf = (zone) => JSON.stringify([zone.id, zone.hostId]);
566
+ let source = game.getOptions().source;
567
+ let seat = game.snapshot?.me;
568
+ let selected = /* @__PURE__ */ new Map();
569
+ let lifetime = {};
570
+ let disposed = false;
571
+ let branch = snapshot();
572
+ function syncLifetime() {
573
+ const nextSource = game.getOptions().source;
574
+ const nextSeat = game.snapshot?.me;
575
+ if (nextSource !== source || nextSeat !== seat) {
576
+ source = nextSource;
577
+ seat = nextSeat;
578
+ selected = /* @__PURE__ */ new Map();
579
+ lifetime = {};
580
+ branch = snapshot();
581
+ }
582
+ }
583
+ function snapshot() {
584
+ const captured = selected;
585
+ const capturedLifetime = lifetime;
586
+ const getMode = (zone) => captured.get(keyOf(zone)) ?? zones.get(zone.id)?.defaultSort ?? null;
587
+ return Object.freeze({
588
+ getSortModes(zone) {
589
+ return zones.get(zone.id)?.modes ?? Object.freeze([]);
590
+ },
591
+ getSortMode(zone) {
592
+ return getMode(zone);
593
+ },
594
+ setSortMode(zone, mode) {
595
+ syncLifetime();
596
+ if (disposed || lifetime !== capturedLifetime) return;
597
+ if (!zones.get(zone.id)?.sorts.has(mode)) return;
598
+ if ((selected.get(keyOf(zone)) ?? zones.get(zone.id)?.defaultSort ?? null) === mode)
599
+ return;
600
+ selected = new Map(selected).set(keyOf(zone), mode);
601
+ branch = snapshot();
602
+ context.invalidate();
603
+ },
604
+ getSortedCardIds(zone) {
605
+ const mode = getMode(zone);
606
+ const compare = mode === null ? void 0 : zones.get(zone.id)?.sorts.get(mode);
607
+ return Object.freeze(
608
+ zone.getCards({ sort: compare }).map(seatCardId)
609
+ );
610
+ }
611
+ });
612
+ }
613
+ return {
614
+ root: {
615
+ get hand() {
616
+ syncLifetime();
617
+ return branch;
618
+ }
619
+ },
620
+ zone: {
621
+ getSelectedCardIds() {
622
+ return this.getCards().filter((card) => card.getIsSelected()).map(seatCardId);
623
+ },
624
+ getSelectableCardIds() {
625
+ return this.getCards().filter((card) => card.getCanSelect()).map(seatCardId);
626
+ }
627
+ },
628
+ dispose() {
629
+ disposed = true;
630
+ }
631
+ };
632
+ }
633
+
634
+ // src/shared/tile-board-layout.ts
635
+ function assertFinite(values) {
636
+ if (!values.every(Number.isFinite))
637
+ throw new Error("Derived tile layout geometry must be finite.");
638
+ }
639
+ var frozenPoint = (point) => {
640
+ assertFinite([point.x, point.y]);
641
+ return Object.freeze({ ...point });
642
+ };
643
+ function outlines(cells) {
644
+ const sides = /* @__PURE__ */ new Map();
645
+ for (const cell of cells) {
646
+ for (const [index, from] of cell.corners.entries()) {
647
+ const to = cell.corners[(index + 1) % cell.corners.length];
648
+ const key = [from.key, to.key].sort().join("|");
649
+ if (sides.has(key)) sides.delete(key);
650
+ else sides.set(key, { from, to });
651
+ }
652
+ }
653
+ const outgoing = /* @__PURE__ */ new Map();
654
+ for (const side of sides.values()) {
655
+ const next = outgoing.get(side.from.key) ?? [];
656
+ next.push(side);
657
+ outgoing.set(side.from.key, next);
658
+ }
659
+ const loops = [];
660
+ while (sides.size) {
661
+ const first = sides.values().next().value;
662
+ const start = first.from.key;
663
+ let side = first;
664
+ const loop = [];
665
+ while (true) {
666
+ loop.push(frozenPoint(side.from.point));
667
+ sides.delete([side.from.key, side.to.key].sort().join("|"));
668
+ const remaining = outgoing.get(side.from.key);
669
+ remaining.splice(remaining.indexOf(side), 1);
670
+ if (side.to.key === start) break;
671
+ const candidates = [...outgoing.get(side.to.key) ?? []];
672
+ if (!candidates.length)
673
+ throw new Error("Tile footprint boundary is not closed.");
674
+ const angle = Math.atan2(
675
+ side.to.point.y - side.from.point.y,
676
+ side.to.point.x - side.from.point.x
677
+ );
678
+ const turn = (next) => {
679
+ const delta = Math.atan2(
680
+ next.to.point.y - next.from.point.y,
681
+ next.to.point.x - next.from.point.x
682
+ ) - angle;
683
+ return Math.atan2(Math.sin(delta), Math.cos(delta));
684
+ };
685
+ candidates.sort(
686
+ (a, b) => turn(b) - turn(a) || (a.to.key < b.to.key ? -1 : a.to.key > b.to.key ? 1 : 0)
687
+ );
688
+ side = candidates[0];
689
+ }
690
+ loops.push(Object.freeze(loop));
691
+ }
692
+ return Object.freeze(loops);
693
+ }
694
+ function hexCells(boardId, orientation, coordinates, size, origin) {
695
+ const topology = createHexTopology({
696
+ id: boardId,
697
+ orientation,
698
+ spaces: coordinates.map((coordinate, index) => ({
699
+ ...coordinate,
700
+ id: String(index)
701
+ }))
702
+ });
703
+ const layout = topology.getLayout({ hexSize: size, origin });
704
+ const vertices = new Map(
705
+ layout.vertices.map((vertex) => [vertex.id, vertex.center])
706
+ );
707
+ return layout.spaces.map((space) => ({
708
+ center: space.center,
709
+ corners: topology.verticesOf(space.id).map((key) => ({ key, point: vertices.get(key) })).sort(
710
+ (a, b) => Math.atan2(a.point.y - space.center.y, a.point.x - space.center.x) - Math.atan2(b.point.y - space.center.y, b.point.x - space.center.x)
711
+ )
712
+ }));
713
+ }
714
+ function squareCells(coordinates, size, origin) {
715
+ return coordinates.map(({ col, row }) => ({
716
+ center: {
717
+ x: origin.x + (col + 0.5) * size,
718
+ y: origin.y + (row + 0.5) * size
719
+ },
720
+ corners: [
721
+ [col, row],
722
+ [col + 1, row],
723
+ [col + 1, row + 1],
724
+ [col, row + 1]
725
+ ].map(([x, y]) => ({
726
+ key: `${x},${y}`,
727
+ point: { x: origin.x + x * size, y: origin.y + y * size }
728
+ }))
729
+ }));
730
+ }
731
+ function build(board, size, origin) {
732
+ if (board.layout === "generic")
733
+ throw new Error("Generic boards have no tile layout.");
734
+ const visibleByTile = /* @__PURE__ */ new Map();
735
+ const group = (spaces, cells) => {
736
+ spaces.forEach((space, index) => {
737
+ const grouped = visibleByTile.get(space.tileRef) ?? {
738
+ cells: [],
739
+ ids: []
740
+ };
741
+ grouped.cells.push(cells[index]);
742
+ grouped.ids.push(space.id);
743
+ visibleByTile.set(space.tileRef, grouped);
744
+ });
745
+ };
746
+ if (board.layout === "hex") {
747
+ const spaces = Object.values(board.spaces);
748
+ group(spaces, hexCells(board.id, board.orientation, spaces, size, origin));
749
+ } else {
750
+ const spaces = Object.values(board.spaces);
751
+ group(spaces, squareCells(spaces, size, origin));
752
+ }
753
+ const tiles = board.tiles.map((tile) => {
754
+ const visible = visibleByTile.get(tile.ref);
755
+ const footprint = tile.disclosure === "concealed" ? getPublicTileFootprint(tile.appearance, tile.placement) : null;
756
+ let cells;
757
+ let anchor;
758
+ if (board.layout === "hex" && tile.placement.layout === "hex") {
759
+ const placement = tile.placement;
760
+ cells = tile.disclosure === "visible" ? visible?.cells ?? [] : footprint?.layout === "hex" ? hexCells(
761
+ board.id,
762
+ board.orientation,
763
+ footprint.cells,
764
+ size,
765
+ origin
766
+ ) : [];
767
+ anchor = hexCells(
768
+ board.id,
769
+ board.orientation,
770
+ [{ q: placement.q, r: placement.r }],
771
+ size,
772
+ origin
773
+ )[0].center;
774
+ } else if (board.layout === "square" && tile.placement.layout === "square") {
775
+ const placement = tile.placement;
776
+ cells = tile.disclosure === "visible" ? visible?.cells ?? [] : footprint?.layout === "square" ? squareCells(footprint.cells, size, origin) : [];
777
+ anchor = squareCells(
778
+ [{ col: placement.col, row: placement.row }],
779
+ size,
780
+ origin
781
+ )[0].center;
782
+ } else throw new Error("Tile placement layout does not match board.");
783
+ if (!cells.length)
784
+ throw new Error("A projected tile requires a nonempty footprint.");
785
+ const center = {
786
+ x: cells.reduce((sum, cell) => sum + cell.center.x, 0) / cells.length,
787
+ y: cells.reduce((sum, cell) => sum + cell.center.y, 0) / cells.length
788
+ };
789
+ return Object.freeze({
790
+ ref: tile.ref,
791
+ data: tile,
792
+ spaceIds: Object.freeze(visible?.ids ?? []),
793
+ outlines: outlines(cells),
794
+ center: frozenPoint(center),
795
+ anchor: frozenPoint(anchor),
796
+ rotationDegrees: tile.placement.rotation * (board.layout === "hex" ? 60 : 90)
797
+ });
798
+ });
799
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
800
+ for (const tile of tiles)
801
+ for (const loop of tile.outlines)
802
+ for (const point of loop) {
803
+ minX = Math.min(minX, point.x);
804
+ minY = Math.min(minY, point.y);
805
+ maxX = Math.max(maxX, point.x);
806
+ maxY = Math.max(maxY, point.y);
807
+ }
808
+ const viewBox = {
809
+ x: tiles.length ? minX : 0,
810
+ y: tiles.length ? minY : 0,
811
+ width: tiles.length ? maxX - minX : 0,
812
+ height: tiles.length ? maxY - minY : 0
813
+ };
814
+ assertFinite([viewBox.x, viewBox.y, viewBox.width, viewBox.height]);
815
+ return Object.freeze({
816
+ tiles: Object.freeze(tiles),
817
+ viewBox: Object.freeze(viewBox)
818
+ });
819
+ }
820
+ var cache = /* @__PURE__ */ new WeakMap();
821
+ function createTileBoardLayout(board, size, suppliedOrigin = { x: 0, y: 0 }) {
822
+ if (!Number.isFinite(size) || size <= 0)
823
+ throw new Error("Tile cell size must be positive and finite.");
824
+ if (![suppliedOrigin.x, suppliedOrigin.y].every(Number.isFinite))
825
+ throw new Error("Tile layout origin must be finite.");
826
+ const key = JSON.stringify([size, suppliedOrigin.x, suppliedOrigin.y]);
827
+ const cached = cache.get(board);
828
+ if (cached?.key === key) return cached.value;
829
+ const value = build(
830
+ immutableCopy(SeatBoardTopologySchema.parse(board)),
831
+ size,
832
+ {
833
+ ...suppliedOrigin
834
+ }
835
+ );
836
+ cache.set(board, { key, value });
837
+ return value;
838
+ }
839
+
840
+ // src/shared/square-board-layout.ts
841
+ function createSquareBoardLayout(board, size, origin) {
842
+ if (!Number.isFinite(size) || size <= 0)
843
+ throw new Error("Square cell size must be positive and finite.");
844
+ if (![origin.x, origin.y].every(Number.isFinite))
845
+ throw new Error("Layout origin must be finite.");
846
+ const spaces = Object.values(board.spaces).map((space) => {
847
+ const x2 = origin.x + space.col * size;
848
+ const y2 = origin.y + space.row * size;
849
+ return {
850
+ id: space.id,
851
+ center: { x: x2 + size / 2, y: y2 + size / 2 },
852
+ corners: [
853
+ { x: x2, y: y2 },
854
+ { x: x2 + size, y: y2 },
855
+ { x: x2 + size, y: y2 + size },
856
+ { x: x2, y: y2 + size }
857
+ ]
858
+ };
859
+ });
860
+ const vertices = board.vertices.map((vertex) => {
861
+ const identity = parseBoardElementId(vertex.id);
862
+ if (!identity || identity.kind !== "vertex" || identity.layout !== "square" || identity.boardId !== board.id)
863
+ throw new Error(
864
+ `Square vertex '${vertex.id}' does not belong to board '${board.id}'.`
865
+ );
866
+ const [col, row] = identity.latticeId.split(",").map(Number);
867
+ return {
868
+ id: vertex.id,
869
+ center: { x: origin.x + col * size, y: origin.y + row * size }
870
+ };
871
+ });
872
+ const vertexPoints = new Map(
873
+ vertices.map((vertex) => [vertex.id, vertex.center])
874
+ );
875
+ const edges = board.edges.map((edge) => {
876
+ const from = vertexPoints.get(edge.vertexIds[0]);
877
+ const to = vertexPoints.get(edge.vertexIds[1]);
878
+ if (!from || !to)
879
+ throw new Error(`Square edge '${edge.id}' references a missing vertex.`);
880
+ return { id: edge.id, from, to };
881
+ });
882
+ let minX = Infinity;
883
+ let minY = Infinity;
884
+ let maxX = -Infinity;
885
+ let maxY = -Infinity;
886
+ for (const space of spaces) {
887
+ for (const point of space.corners) {
888
+ minX = Math.min(minX, point.x);
889
+ minY = Math.min(minY, point.y);
890
+ maxX = Math.max(maxX, point.x);
891
+ maxY = Math.max(maxY, point.y);
892
+ }
893
+ }
894
+ const x = spaces.length ? minX : 0;
895
+ const y = spaces.length ? minY : 0;
896
+ return {
897
+ viewBox: {
898
+ x,
899
+ y,
900
+ width: spaces.length ? maxX - x : 0,
901
+ height: spaces.length ? maxY - y : 0
902
+ },
903
+ spaces,
904
+ edges,
905
+ vertices,
906
+ pointToSpace(point) {
907
+ const col = Math.floor((point.x - origin.x) / size);
908
+ const row = Math.floor((point.y - origin.y) / size);
909
+ return Object.values(board.spaces).find(
910
+ (space) => space.col === col && space.row === row
911
+ )?.id;
912
+ }
913
+ };
914
+ }
915
+
916
+ // src/headless/features/board.ts
917
+ function createRuntimeBoardFeature(context) {
918
+ const game = context.game;
919
+ const captures = /* @__PURE__ */ new WeakMap();
920
+ const cachedHexTopology = createHexTopologyCache();
921
+ let cached;
922
+ function collection() {
923
+ const model = game.getSnapshot();
924
+ const source = game.getOptions().source;
925
+ const projectedBoards = context.getBoards();
926
+ if (cached && cached.view === model.view && cached.boards === projectedBoards && cached.interactions === model.interactions && cached.source === source)
927
+ return cached.collection;
928
+ const boards = Object.values(projectedBoards).map((data) => {
929
+ const board = context.createBoard(data);
930
+ const geometry = data.layout === "hex" ? cachedHexTopology({
931
+ id: data.id,
932
+ orientation: data.orientation,
933
+ spaces: Object.values(data.spaces)
934
+ }) : void 0;
935
+ captures.set(board, { model, source, geometry });
936
+ return board;
937
+ });
938
+ const byId = new Map(boards.map((board) => [board.id, board]));
939
+ const result = Object.freeze({
940
+ get: (id) => requireLookup(byId.get(id), "Board", id),
941
+ find: (id) => byId.get(id),
942
+ getAll: () => Object.freeze(boards)
943
+ });
944
+ cached = {
945
+ view: model.view,
946
+ boards: projectedBoards,
947
+ interactions: model.interactions,
948
+ source,
949
+ collection: result
950
+ };
951
+ return result;
952
+ }
953
+ function targetsFor(owner) {
954
+ const board = owner.data;
955
+ const captured = captures.get(owner);
956
+ function matchingTargets(model, kind, id) {
957
+ return model.interactions.list().flatMap(
958
+ (interaction) => interaction.getInputs().filter((input) => {
959
+ const domain = input.getDomain();
960
+ return domain.type === "boardTarget" && domain.targetKind === kind && (domain.valueKind === "board-space" ? board.scope === "perPlayer" && domain.boardBaseId === board.baseId : domain.boardId === board.id);
961
+ }).map((input) => {
962
+ const domain = input.getDomain();
963
+ return {
964
+ interaction,
965
+ input,
966
+ value: domain.type === "boardTarget" && domain.valueKind === "board-space" ? Object.freeze({
967
+ boardId: board.id,
968
+ spaceId: id
969
+ }) : id
970
+ };
971
+ })
972
+ );
973
+ }
974
+ function target(kind, id) {
975
+ const matches = matchingTargets(captured.model, kind, id);
976
+ const eligible = matches.some(
977
+ ({ input, value }) => input.getIsEligible(value)
978
+ );
979
+ const selected = matches.some(
980
+ ({ input, value }) => input.getIsSelected(value)
981
+ );
982
+ const selectable = matches.some(
983
+ ({ input, value }) => !input.getTargetProps(value).disabled
984
+ );
985
+ function select(options) {
986
+ if (game.getOptions().source !== captured.source || game.snapshot?.me !== captured.model.snapshot?.me)
987
+ return;
988
+ const matching = matchingTargets(game.getSnapshot(), kind, id).filter(
989
+ ({ interaction, input, value: value2 }) => (!options?.interaction || options.interaction === interaction.key) && (!options?.input || options.input === input.key) && !input.getTargetProps(value2).disabled
990
+ );
991
+ if (!matching.length) return;
992
+ if (matching.length > 1) throw new AmbiguousTargetError(id);
993
+ const domain = matching[0].input.getDomain();
994
+ if (domain.type !== "boardTarget") return;
995
+ const value = matching[0].value;
996
+ const target2 = domain.valueKind === "board-space" && typeof value !== "string" ? { kind: "space", valueKind: domain.valueKind, value } : {
997
+ kind,
998
+ valueKind: "board-id",
999
+ value: id,
1000
+ boardId: board.id
1001
+ };
1002
+ context.routeTarget(target2, {
1003
+ interaction: matching[0].interaction.key,
1004
+ input: matching[0].input.key
1005
+ });
1006
+ }
1007
+ return {
1008
+ id,
1009
+ getIsEligible: () => eligible,
1010
+ getIsSelectable: () => selectable,
1011
+ getIsSelected: () => selected,
1012
+ getSelectHandler: (options) => () => select(options),
1013
+ getTargetProps(options) {
1014
+ const candidates = matches.filter(
1015
+ ({ interaction, input, value }) => (!options?.interaction || interaction.key === options.interaction) && (!options?.input || options.input === input.key) && !input.getTargetProps(value).disabled
1016
+ );
1017
+ const only = candidates.length === 1 ? candidates[0] : void 0;
1018
+ return {
1019
+ type: "button",
1020
+ disabled: candidates.length === 0,
1021
+ "data-action": "select",
1022
+ "data-value": only && typeof only.value !== "string" ? JSON.stringify(only.value) : id,
1023
+ "data-board": board.id,
1024
+ "data-interaction": options?.interaction ?? only?.interaction.key,
1025
+ "data-input": only?.input.key,
1026
+ "data-eligible": eligible,
1027
+ "data-selected": selected,
1028
+ "data-disabled": candidates.length === 0,
1029
+ onClick: () => select(options)
1030
+ };
1031
+ }
1032
+ };
1033
+ }
1034
+ return target;
1035
+ }
1036
+ const spaceCollections = /* @__PURE__ */ new WeakMap();
1037
+ function spacesFor(owner) {
1038
+ const cached2 = spaceCollections.get(owner);
1039
+ if (cached2) return cached2;
1040
+ const target = targetsFor(owner);
1041
+ const data = owner.data;
1042
+ const values = Object.freeze(
1043
+ Object.values(data.spaces).map(
1044
+ (space) => Object.freeze({
1045
+ ...target("space", space.id),
1046
+ data: space,
1047
+ board: owner
1048
+ })
1049
+ )
1050
+ );
1051
+ const byId = new Map(values.map((space) => [space.id, space]));
1052
+ const spaces = Object.freeze({
1053
+ get: (id) => requireLookup(byId.get(id), `Space on board ${owner.id}`, id),
1054
+ find: (id) => byId.get(id),
1055
+ getAll: () => values
1056
+ });
1057
+ spaceCollections.set(owner, spaces);
1058
+ return spaces;
1059
+ }
1060
+ return {
1061
+ root: {
1062
+ get boards() {
1063
+ return collection();
1064
+ }
1065
+ },
1066
+ board: {
1067
+ get spaces() {
1068
+ return spacesFor(this);
1069
+ },
1070
+ getLayout(options) {
1071
+ const board = this.data;
1072
+ if (board.layout === "generic")
1073
+ throw new Error(
1074
+ `Board '${board.id}' has no spatial geometry; use board.spaces for semantic selection.`
1075
+ );
1076
+ const {
1077
+ hexSize,
1078
+ origin: suppliedOrigin = { x: 0, y: 0 },
1079
+ viewport: suppliedViewport = { x: 0, y: 0, scale: 1 }
1080
+ } = options;
1081
+ const origin = { ...suppliedOrigin };
1082
+ const viewport = { ...suppliedViewport };
1083
+ if (!Number.isFinite(hexSize) || hexSize <= 0)
1084
+ throw new Error("hexSize must be positive and finite.");
1085
+ if (![origin.x, origin.y, viewport.x, viewport.y, viewport.scale].every(
1086
+ Number.isFinite
1087
+ ) || viewport.scale <= 0)
1088
+ throw new Error(
1089
+ "Layout transform must be finite with positive scale."
1090
+ );
1091
+ const captured = captures.get(this);
1092
+ const geometry = board.layout === "hex" ? captured.geometry.getLayout({ hexSize, origin }) : createSquareBoardLayout(board, hexSize, origin);
1093
+ const point = (value) => {
1094
+ const transformed = {
1095
+ x: value.x * viewport.scale + viewport.x,
1096
+ y: value.y * viewport.scale + viewport.y
1097
+ };
1098
+ if (![transformed.x, transformed.y].every(Number.isFinite))
1099
+ throw new Error("Derived layout transform must be finite.");
1100
+ return Object.freeze(transformed);
1101
+ };
1102
+ const tileGeometry = createTileBoardLayout(board, hexSize, origin);
1103
+ const tiles = Object.freeze(
1104
+ tileGeometry.tiles.map(
1105
+ (tile) => Object.freeze({
1106
+ ...tile,
1107
+ center: point(tile.center),
1108
+ anchor: point(tile.anchor),
1109
+ outlines: Object.freeze(
1110
+ tile.outlines.map((loop) => Object.freeze(loop.map(point)))
1111
+ )
1112
+ })
1113
+ )
1114
+ );
1115
+ const target = targetsFor(this);
1116
+ const semanticSpaces = spacesFor(this);
1117
+ const spaces = Object.freeze(
1118
+ geometry.spaces.map((space) => {
1119
+ const corners = Object.freeze(space.corners.map(point));
1120
+ const center = point(space.center);
1121
+ return Object.freeze({
1122
+ ...semanticSpaces.get(space.id),
1123
+ center,
1124
+ points: () => corners,
1125
+ transform: `translate(${center.x} ${center.y})`
1126
+ });
1127
+ })
1128
+ );
1129
+ const edges = Object.freeze(
1130
+ geometry.edges.map(
1131
+ (edge) => Object.freeze({
1132
+ ...target("edge", edge.id),
1133
+ data: board.edges.find((data) => data.id === edge.id),
1134
+ center: point({
1135
+ x: (edge.from.x + edge.to.x) / 2,
1136
+ y: (edge.from.y + edge.to.y) / 2
1137
+ }),
1138
+ line: Object.freeze([point(edge.from), point(edge.to)])
1139
+ })
1140
+ )
1141
+ );
1142
+ const vertices = Object.freeze(
1143
+ geometry.vertices.map(
1144
+ (vertex) => Object.freeze({
1145
+ ...target("vertex", vertex.id),
1146
+ data: board.vertices.find((data) => data.id === vertex.id),
1147
+ center: point(vertex.center)
1148
+ })
1149
+ )
1150
+ );
1151
+ return Object.freeze({
1152
+ viewBox: tileGeometry.viewBox,
1153
+ getTiles: () => tiles,
1154
+ getSpaces: () => spaces,
1155
+ getEdges: () => edges,
1156
+ getVertices: () => vertices,
1157
+ pointToSpace(x, y) {
1158
+ return geometry.pointToSpace({
1159
+ x: (x - viewport.x) / viewport.scale,
1160
+ y: (y - viewport.y) / viewport.scale
1161
+ });
1162
+ }
1163
+ });
1164
+ }
1165
+ }
1166
+ };
1167
+ }
1168
+ function boardFeature(_game, context) {
1169
+ return createRuntimeBoardFeature(
1170
+ context[runtimeFeatures]
1171
+ );
1172
+ }
1173
+
1174
+ // src/headless/features/drag.ts
1175
+ function createRuntimeDragFeature(context, getSelection) {
1176
+ const game = context.game;
1177
+ let active = null;
1178
+ let selection;
1179
+ let source = game.getOptions().source;
1180
+ let seat = game.snapshot?.me;
1181
+ let disposed = false;
1182
+ let version = game.snapshot?.version;
1183
+ let lastInteractions = game.interactions;
1184
+ let lastCards = game.cards;
1185
+ let branch = snapshot();
1186
+ function update(next) {
1187
+ if (disposed) return;
1188
+ active = next === null ? null : Object.freeze(next);
1189
+ branch = snapshot();
1190
+ context.invalidate();
1191
+ }
1192
+ function targets(cardId = active?.cardId, selected = selection) {
1193
+ if (!cardId || game.connection !== "ready") return [];
1194
+ const options = selected;
1195
+ const group = active?.cardId === cardId && active.group ? active.cardIds : getSelection?.(cardId, options);
1196
+ const resolved = (game.cards.find(cardId)?.getInteractions() ?? []).flatMap(
1197
+ (candidate) => {
1198
+ if (!candidate.getIsAvailable() || options?.interaction && candidate.key !== options.interaction)
1199
+ return [];
1200
+ const inputs = candidate.getInputs();
1201
+ const cardInputs = inputs.filter((input) => {
1202
+ const domain = input.getDomain();
1203
+ return domain.type === "cardTarget" && (!options?.input || input.key === options.input) && (group ? domain.selection?.mode === "many" && inputValueInDomain(domain, group, domain.selection) : input.getIsEligible(cardId));
1204
+ });
1205
+ const positions = cardInputs.flatMap(
1206
+ (cardInput) => inputs.flatMap((input) => {
1207
+ const domain = input.getDomain();
1208
+ return domain.type === "zonePosition" ? zonePositions(domain.zones).filter((value) => !input.getTargetProps(value).disabled).map(
1209
+ (value) => Object.freeze({
1210
+ kind: "position",
1211
+ interactionKey: candidate.key,
1212
+ cardInputKey: cardInput.key,
1213
+ inputKey: input.key,
1214
+ value
1215
+ })
1216
+ ) : [];
1217
+ })
1218
+ );
1219
+ if (!inputs.some((input) => input.getDomain().type === "boardTarget"))
1220
+ return [
1221
+ ...cardInputs.map(
1222
+ (cardInput) => Object.freeze({
1223
+ kind: "interaction",
1224
+ interactionKey: candidate.key,
1225
+ cardInputKey: cardInput.key
1226
+ })
1227
+ ),
1228
+ ...positions
1229
+ ];
1230
+ const boardTargets = cardInputs.flatMap(
1231
+ (cardInput) => inputs.flatMap((input) => {
1232
+ const domain = input.getDomain();
1233
+ if (domain.type !== "boardTarget") return [];
1234
+ const route = {
1235
+ interactionKey: candidate.key,
1236
+ cardInputKey: cardInput.key,
1237
+ inputKey: input.key
1238
+ };
1239
+ if (domain.valueKind === "board-space")
1240
+ return domain.eligibleTargets.filter((value) => !input.getTargetProps(value).disabled).map(
1241
+ (value) => Object.freeze({
1242
+ kind: "space",
1243
+ valueKind: domain.valueKind,
1244
+ value,
1245
+ ...route
1246
+ })
1247
+ );
1248
+ return domain.eligibleTargets.filter((value) => !input.getTargetProps(value).disabled).map(
1249
+ (value) => Object.freeze({
1250
+ kind: domain.targetKind,
1251
+ valueKind: domain.valueKind,
1252
+ value,
1253
+ boardId: domain.boardId,
1254
+ ...route
1255
+ })
1256
+ );
1257
+ })
1258
+ );
1259
+ return [...boardTargets, ...positions];
1260
+ }
1261
+ );
1262
+ return resolved;
1263
+ }
1264
+ function isEligible(target) {
1265
+ if (!active || disposed) return false;
1266
+ const route = target.kind === "interaction" ? { ...target, params: void 0 } : target;
1267
+ return targets().some((candidate) => isSameDropTarget(candidate, route)) && (active.group ? context.getCanDropCardGroup(active.cardIds, target) : context.getCanDropCard(active.cardId, target));
1268
+ }
1269
+ function cancel() {
1270
+ if (active) update(null);
1271
+ }
1272
+ function snapshot() {
1273
+ const captured = active;
1274
+ const dropTargets = Object.freeze(targets());
1275
+ return Object.freeze({
1276
+ active: captured,
1277
+ getCanDrag: (cardId, options) => !disposed && targets(cardId, options).length > 0,
1278
+ begin(cardId, options) {
1279
+ if (disposed || !targets(cardId, options).length) return false;
1280
+ selection = options;
1281
+ const selected = getSelection?.(cardId, options);
1282
+ const cardIds = Object.freeze(selected ? [...selected] : [cardId]);
1283
+ if (!cardIds.includes(cardId)) return false;
1284
+ update({
1285
+ cardId,
1286
+ cardIds,
1287
+ group: selected !== void 0,
1288
+ target: null
1289
+ });
1290
+ return true;
1291
+ },
1292
+ getDropTargets: () => dropTargets,
1293
+ getIsDropTarget: isEligible,
1294
+ setDropTarget(target) {
1295
+ if (!active || disposed) return;
1296
+ if (target && !isEligible(target)) {
1297
+ update({ ...active, target: null });
1298
+ return;
1299
+ }
1300
+ update({ ...active, target });
1301
+ },
1302
+ drop() {
1303
+ const finished = active;
1304
+ const eligible = finished?.target && isEligible(finished.target);
1305
+ cancel();
1306
+ if (!disposed && finished?.target && eligible)
1307
+ if (finished.group)
1308
+ context.routeCardGroupDrop(finished.cardIds, finished.target);
1309
+ else context.routeCardDrop(finished.cardId, finished.target);
1310
+ },
1311
+ cancel
1312
+ });
1313
+ }
1314
+ const unsubscribe = game.subscribe(() => {
1315
+ const nextSource = game.getOptions().source;
1316
+ const nextSeat = game.snapshot?.me;
1317
+ const nextVersion = game.snapshot?.version;
1318
+ if (nextSource !== source || nextSeat !== seat || nextVersion !== version || game.connection !== "ready") {
1319
+ source = nextSource;
1320
+ seat = nextSeat;
1321
+ version = nextVersion;
1322
+ cancel();
1323
+ }
1324
+ });
1325
+ return {
1326
+ root: {
1327
+ get drag() {
1328
+ if (lastInteractions !== game.interactions || lastCards !== game.cards) {
1329
+ lastInteractions = game.interactions;
1330
+ lastCards = game.cards;
1331
+ branch = snapshot();
1332
+ }
1333
+ return branch;
1334
+ }
1335
+ },
1336
+ dispose() {
1337
+ unsubscribe();
1338
+ cancel();
1339
+ disposed = true;
1340
+ }
1341
+ };
1342
+ }
1343
+ function dragFeature(_game, context, options) {
1344
+ return createRuntimeDragFeature(
1345
+ context[runtimeFeatures],
1346
+ options?.getSelection
1347
+ );
1348
+ }
1349
+
1350
+ // src/headless/features/pointer-session.ts
1351
+ function createPointerSession(callbacks) {
1352
+ let disposed = false;
1353
+ let active;
1354
+ function value() {
1355
+ const { origin, point } = active;
1356
+ return {
1357
+ origin,
1358
+ point,
1359
+ delta: { x: point.x - origin.x, y: point.y - origin.y }
1360
+ };
1361
+ }
1362
+ function release() {
1363
+ const previous = active;
1364
+ active = void 0;
1365
+ if (previous?.surface.hasPointerCapture(previous.id)) {
1366
+ previous.surface.releasePointerCapture(previous.id);
1367
+ }
1368
+ }
1369
+ function cancel() {
1370
+ if (!active) return;
1371
+ release();
1372
+ callbacks.cancel();
1373
+ }
1374
+ return {
1375
+ start(event) {
1376
+ if (disposed || active || event.button !== 0) return false;
1377
+ event.currentTarget.setPointerCapture(event.pointerId);
1378
+ const point = { x: event.clientX, y: event.clientY };
1379
+ active = {
1380
+ id: event.pointerId,
1381
+ surface: event.currentTarget,
1382
+ origin: point,
1383
+ point
1384
+ };
1385
+ event.preventDefault();
1386
+ return true;
1387
+ },
1388
+ move(event) {
1389
+ if (event.pointerId !== active?.id) return;
1390
+ active.point = { x: event.clientX, y: event.clientY };
1391
+ event.preventDefault();
1392
+ callbacks.move(value());
1393
+ },
1394
+ end(event) {
1395
+ if (event.pointerId !== active?.id) return;
1396
+ active.point = { x: event.clientX, y: event.clientY };
1397
+ const result = value();
1398
+ release();
1399
+ callbacks.end(result);
1400
+ },
1401
+ cancel(event) {
1402
+ if (event && event.pointerId !== active?.id) return;
1403
+ cancel();
1404
+ },
1405
+ dispose() {
1406
+ disposed = true;
1407
+ cancel();
1408
+ }
1409
+ };
1410
+ }
1411
+
1412
+ // src/headless/features/pan-zoom.ts
1413
+ function panZoomFeature(game, context, options = {}) {
1414
+ const minScale = options.minScale ?? 0.25;
1415
+ const maxScale = options.maxScale ?? 4;
1416
+ if (!Number.isFinite(minScale) || !Number.isFinite(maxScale) || minScale <= 0 || maxScale < minScale) {
1417
+ throw new Error(
1418
+ "Viewport scale limits must be positive, finite, and ordered."
1419
+ );
1420
+ }
1421
+ const initial = options.initial ?? { x: 0, y: 0, scale: 1 };
1422
+ let disposed = false;
1423
+ let source = game.getOptions().source;
1424
+ let seat = game.snapshot?.me;
1425
+ let dragOrigin = initial;
1426
+ let transform = admit(initial);
1427
+ let branch = snapshot();
1428
+ function admit(value) {
1429
+ if (![value.x, value.y, value.scale].every(Number.isFinite) || value.scale <= 0) {
1430
+ throw new Error(
1431
+ "Viewport transform must have finite coordinates and positive scale."
1432
+ );
1433
+ }
1434
+ return Object.freeze({
1435
+ x: value.x,
1436
+ y: value.y,
1437
+ scale: Math.min(maxScale, Math.max(minScale, value.scale))
1438
+ });
1439
+ }
1440
+ function update(value) {
1441
+ if (disposed) return;
1442
+ const next = admit(value);
1443
+ if (next.x === transform.x && next.y === transform.y && next.scale === transform.scale)
1444
+ return;
1445
+ transform = next;
1446
+ branch = snapshot();
1447
+ context.invalidate();
1448
+ }
1449
+ const pointer = createPointerSession({
1450
+ move: ({ delta }) => update({
1451
+ ...dragOrigin,
1452
+ x: dragOrigin.x + delta.x,
1453
+ y: dragOrigin.y + delta.y
1454
+ }),
1455
+ end: ({ delta }) => update({
1456
+ ...dragOrigin,
1457
+ x: dragOrigin.x + delta.x,
1458
+ y: dragOrigin.y + delta.y
1459
+ }),
1460
+ cancel() {
1461
+ }
1462
+ });
1463
+ function props() {
1464
+ return {
1465
+ style: { touchAction: "none" },
1466
+ onPointerDown(event) {
1467
+ if (pointer.start(event)) dragOrigin = transform;
1468
+ },
1469
+ onPointerMove: pointer.move,
1470
+ onPointerUp: pointer.end,
1471
+ onPointerCancel: pointer.cancel,
1472
+ onLostPointerCapture: pointer.cancel,
1473
+ onWheel(event) {
1474
+ if (disposed) return;
1475
+ event.preventDefault();
1476
+ const rect = event.currentTarget.getBoundingClientRect();
1477
+ const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? rect.height : 1;
1478
+ const scale = Math.min(
1479
+ maxScale,
1480
+ Math.max(
1481
+ minScale,
1482
+ transform.scale * Math.exp(-event.deltaY * unit * 2e-3)
1483
+ )
1484
+ );
1485
+ const x = event.clientX - rect.left;
1486
+ const y = event.clientY - rect.top;
1487
+ const ratio = scale / transform.scale;
1488
+ update({
1489
+ x: x - (x - transform.x) * ratio,
1490
+ y: y - (y - transform.y) * ratio,
1491
+ scale
1492
+ });
1493
+ }
1494
+ };
1495
+ }
1496
+ function snapshot() {
1497
+ const captured = transform;
1498
+ return Object.freeze({
1499
+ transform: captured,
1500
+ getTransform: () => captured,
1501
+ getProps: props,
1502
+ setTransform: update,
1503
+ reset: () => {
1504
+ pointer.cancel();
1505
+ update(initial);
1506
+ }
1507
+ });
1508
+ }
1509
+ const unsubscribe = game.subscribe(() => {
1510
+ const nextSource = game.getOptions().source;
1511
+ const nextSeat = game.snapshot?.me;
1512
+ if (nextSource !== source || nextSeat !== seat) {
1513
+ source = nextSource;
1514
+ seat = nextSeat;
1515
+ pointer.cancel();
1516
+ update(initial);
1517
+ }
1518
+ });
1519
+ return {
1520
+ root: {
1521
+ get viewport() {
1522
+ return branch;
1523
+ }
1524
+ },
1525
+ dispose() {
1526
+ disposed = true;
1527
+ unsubscribe();
1528
+ pointer.dispose();
1529
+ }
1530
+ };
1531
+ }
1532
+
1533
+ // src/headless/features/origins.ts
1534
+ var flatten = (zones) => Object.fromEntries(
1535
+ Object.entries(zones).flatMap(
1536
+ ([zoneId, hosts]) => Object.entries(hosts).map(([hostId, zone]) => [
1537
+ JSON.stringify([zoneId, hostId]),
1538
+ zone
1539
+ ])
1540
+ )
1541
+ );
1542
+ var hiddenCount = (zones, zone) => zones[zone]?.cardIds.filter(isHiddenCardId).length ?? 0;
1543
+ function singleOrigin(departures, count) {
1544
+ const origin = departures[0];
1545
+ return origin && departures.length >= count && departures.every(
1546
+ (card) => card.zone === origin.zone && card.hidden === origin.hidden
1547
+ ) ? Object.freeze(origin) : null;
1548
+ }
1549
+ function findCardOrigins(previousNested, nextNested, mover) {
1550
+ const previous = flatten(previousNested);
1551
+ const next = flatten(nextNested);
1552
+ const shownBefore = /* @__PURE__ */ new Map();
1553
+ for (const [zone, { cardIds }] of Object.entries(previous))
1554
+ for (const cardId of cardIds)
1555
+ if (!isHiddenCardId(cardId)) shownBefore.set(cardId, zone);
1556
+ const shownNow = new Set(
1557
+ Object.values(next).flatMap(
1558
+ ({ cardIds }) => cardIds.filter((cardId) => !isHiddenCardId(cardId))
1559
+ )
1560
+ );
1561
+ let left = Object.entries(previous).flatMap(([zone, { cardIds }]) => [
1562
+ ...cardIds.filter((cardId) => !isHiddenCardId(cardId) && !shownNow.has(cardId)).map(() => ({ zone, hidden: false })),
1563
+ ...Array.from(
1564
+ {
1565
+ length: Math.max(
1566
+ 0,
1567
+ hiddenCount(previous, zone) - hiddenCount(next, zone)
1568
+ )
1569
+ },
1570
+ () => ({ zone, hidden: true })
1571
+ )
1572
+ ]);
1573
+ const origins = /* @__PURE__ */ new Map();
1574
+ const shownArrivals = [];
1575
+ const hiddenArrivals = [];
1576
+ for (const [zone, { cardIds }] of Object.entries(next)) {
1577
+ for (const cardId of cardIds) {
1578
+ if (isHiddenCardId(cardId)) continue;
1579
+ const from = shownBefore.get(cardId);
1580
+ if (from === void 0) shownArrivals.push(cardId);
1581
+ else if (from !== zone)
1582
+ origins.set(cardId, Object.freeze({ zone: from, hidden: false }));
1583
+ }
1584
+ const hidden = cardIds.filter(isHiddenCardId);
1585
+ const before = hiddenCount(previous, zone);
1586
+ hiddenArrivals.push(
1587
+ ...Array.from(
1588
+ { length: Math.max(0, hidden.length - before) },
1589
+ (_, index) => before === 0 ? hidden[index] : null
1590
+ )
1591
+ );
1592
+ }
1593
+ if (left.some(
1594
+ (card) => !card.hidden && hiddenCount(previous, card.zone) > 0 && hiddenCount(next, card.zone) > 0
1595
+ ))
1596
+ return decodedOrigins(origins);
1597
+ const hiddenLeft = left.filter((card) => card.hidden);
1598
+ if (shownArrivals.length) {
1599
+ const origin = singleOrigin(hiddenLeft, shownArrivals.length);
1600
+ if (origin) {
1601
+ for (const cardId of shownArrivals) origins.set(cardId, origin);
1602
+ left = [
1603
+ ...left.filter((card) => !card.hidden),
1604
+ ...hiddenLeft.slice(shownArrivals.length)
1605
+ ];
1606
+ } else if (hiddenLeft.length) {
1607
+ return decodedOrigins(origins);
1608
+ } else if (mover !== null) {
1609
+ const origin2 = Object.freeze({ player: mover, hidden: true });
1610
+ for (const cardId of shownArrivals) origins.set(cardId, origin2);
1611
+ }
1612
+ }
1613
+ const hiddenOrigin = singleOrigin(left, hiddenArrivals.length) ?? (left.length === 0 && mover !== null ? Object.freeze({ player: mover, hidden: true }) : null);
1614
+ if (hiddenOrigin) {
1615
+ for (const cardId of hiddenArrivals)
1616
+ if (cardId !== null) origins.set(cardId, hiddenOrigin);
1617
+ }
1618
+ return decodedOrigins(origins);
1619
+ }
1620
+ function moverOf(snapshot) {
1621
+ const active = snapshot.frame.flow.activePlayers;
1622
+ return active.length === 1 && active[0] !== snapshot.me ? active[0] : null;
1623
+ }
1624
+ function originsFeature(game) {
1625
+ let frame = game.snapshot;
1626
+ let source = game.getOptions().source;
1627
+ let origins = /* @__PURE__ */ new Map();
1628
+ function sync() {
1629
+ const next = game.snapshot;
1630
+ const nextSource = game.getOptions().source;
1631
+ if (next?.version === frame?.version && next?.me === frame?.me && nextSource === source)
1632
+ return;
1633
+ origins = frame && next && next.me === frame.me && nextSource === source && next.version === frame.version + 1 ? findCardOrigins(
1634
+ frame.frame.zones,
1635
+ next.frame.zones,
1636
+ moverOf(frame)
1637
+ ) : /* @__PURE__ */ new Map();
1638
+ frame = next;
1639
+ source = nextSource;
1640
+ }
1641
+ const unsubscribe = game.subscribe(sync);
1642
+ return {
1643
+ card: {
1644
+ getOrigin() {
1645
+ sync();
1646
+ return origins.get(this.id) ?? null;
1647
+ }
1648
+ },
1649
+ dispose: unsubscribe
1650
+ };
1651
+ }
1652
+ function decodedOrigins(origins) {
1653
+ return new Map(
1654
+ [...origins].map(([id, origin]) => {
1655
+ if (!("zone" in origin)) return [id, origin];
1656
+ const [zone, hostId] = JSON.parse(origin.zone);
1657
+ return [id, Object.freeze({ ...origin, zone, hostId })];
1658
+ })
1659
+ );
1660
+ }
1661
+
1662
+ // src/headless/fan.ts
1663
+ var RADIANS = Math.PI / 180;
1664
+ function fanLayout({
1665
+ count,
1666
+ width,
1667
+ cardWidth,
1668
+ cardHeight,
1669
+ step = cardWidth * 0.6,
1670
+ angle = 5,
1671
+ maxSpread = 30
1672
+ }) {
1673
+ if (count < 1) return { cards: [], width: 0, height: 0, step: 0 };
1674
+ const gaps = count - 1;
1675
+ const delta = gaps ? Math.min(angle, maxSpread / gaps) * RADIANS : 0;
1676
+ const edge = gaps * delta / 2;
1677
+ const span = delta ? 2 * Math.sin(edge) / delta : gaps;
1678
+ const ends = cardWidth * Math.cos(edge) + cardHeight * Math.sin(edge);
1679
+ const spacing = span ? Math.max(0, Math.min(step, (width - ends) / span)) : 0;
1680
+ const radius = delta ? spacing / delta : 0;
1681
+ const centres = Array.from({ length: count }, (_, index) => {
1682
+ if (!delta) return { theta: 0, x: (index - gaps / 2) * spacing, y: 0 };
1683
+ const theta = (index - gaps / 2) * delta;
1684
+ return {
1685
+ theta,
1686
+ x: radius * Math.sin(theta),
1687
+ y: radius * (1 - Math.cos(theta))
1688
+ };
1689
+ });
1690
+ const halfHeight = (theta) => cardHeight / 2 * Math.cos(theta) + cardWidth / 2 * Math.abs(Math.sin(theta));
1691
+ const top = Math.min(
1692
+ ...centres.map((card) => card.y - halfHeight(card.theta))
1693
+ );
1694
+ const bottom = Math.max(
1695
+ ...centres.map((card) => card.y + halfHeight(card.theta))
1696
+ );
1697
+ const fanWidth = spacing * span + ends;
1698
+ return {
1699
+ cards: centres.map((card) => ({
1700
+ x: card.x + (fanWidth - cardWidth) / 2,
1701
+ y: card.y - top - cardHeight / 2,
1702
+ rotate: card.theta / RADIANS
1703
+ })),
1704
+ width: fanWidth,
1705
+ height: bottom - top,
1706
+ step: spacing
1707
+ };
1708
+ }
1709
+ function liftFanCard(card, distance) {
1710
+ const theta = card.rotate * RADIANS;
1711
+ return {
1712
+ x: card.x + distance * Math.sin(theta),
1713
+ y: card.y - distance * Math.cos(theta),
1714
+ rotate: card.rotate
1715
+ };
1716
+ }
1717
+
1718
+ // src/headless/hand-fan.ts
1719
+ var handFanPresets = {
1720
+ /** Whole resting cards; focusing enlarges the card. */
1721
+ open: {
1722
+ tuck: 0,
1723
+ focusScale: 2,
1724
+ focusMaxHeight: 0.55,
1725
+ push: [0.29, 0.19, 0.1],
1726
+ exposed: 0.35
1727
+ },
1728
+ /**
1729
+ * Larger cards tucked below the bottom edge and spaced like a dealt hand;
1730
+ * focusing mostly reveals the card.
1731
+ */
1732
+ tucked: {
1733
+ spacing: 0.75,
1734
+ tuck: 0.15,
1735
+ focusScale: 1.4,
1736
+ focusMaxHeight: 0.4,
1737
+ push: [0.29, 0.19, 0.1],
1738
+ exposed: 0.35
1739
+ },
1740
+ /** Coarse pointers: a shallower tuck and wider strips to tap. */
1741
+ touch: {
1742
+ spacing: 0.75,
1743
+ tuck: 0.1,
1744
+ focusScale: 1.6,
1745
+ focusMaxHeight: 0.5,
1746
+ push: [0.35, 0.2, 0.1],
1747
+ exposed: 0.45
1748
+ }
1749
+ };
1750
+ function handFan({
1751
+ count,
1752
+ width,
1753
+ cardWidth,
1754
+ cardHeight,
1755
+ focused = -1,
1756
+ visible,
1757
+ windowHeight = Infinity,
1758
+ room = Infinity,
1759
+ options
1760
+ }) {
1761
+ const o = { ...handFanPresets.open, ...options };
1762
+ const fan = fanLayout({
1763
+ count,
1764
+ width,
1765
+ cardWidth,
1766
+ cardHeight,
1767
+ angle: o.angle,
1768
+ maxSpread: o.maxSpread,
1769
+ step: o.spacing === void 0 ? void 0 : o.spacing * cardWidth
1770
+ });
1771
+ const band = o.tuck ? Math.min(fan.height, (1 - o.tuck) * cardHeight) : fan.height;
1772
+ const view = visible ?? {
1773
+ left: (fan.width - width) / 2,
1774
+ width
1775
+ };
1776
+ const exposed = cardWidth * o.exposed;
1777
+ const largest = Math.max(
1778
+ cardWidth,
1779
+ Math.min(
1780
+ cardWidth * o.focusScale,
1781
+ windowHeight * o.focusMaxHeight * cardWidth / cardHeight,
1782
+ room * cardWidth / cardHeight,
1783
+ view.width
1784
+ )
1785
+ );
1786
+ const headroom = Math.max(0, largest * cardHeight / cardWidth - band);
1787
+ const cards = fan.cards.map((card, index) => ({
1788
+ ...card,
1789
+ scale: 1,
1790
+ layer: index
1791
+ }));
1792
+ if (focused < 0 || focused >= count)
1793
+ return { cards, width: fan.width, height: band, headroom };
1794
+ const leftStrip = focused > 0 ? exposed : 0;
1795
+ const rightStrip = focused < count - 1 ? exposed : 0;
1796
+ const face = Math.max(
1797
+ cardWidth,
1798
+ Math.min(largest, view.width - leftStrip - rightStrip)
1799
+ );
1800
+ const scale = face / cardWidth;
1801
+ const half = face / 2;
1802
+ const centre = Math.max(
1803
+ view.left + leftStrip + half,
1804
+ Math.min(
1805
+ view.left + view.width - rightStrip - half,
1806
+ cards[focused].x + cardWidth / 2
1807
+ )
1808
+ );
1809
+ cards[focused] = {
1810
+ x: centre - cardWidth / 2,
1811
+ y: band - cardHeight * (scale + 1) / 2,
1812
+ rotate: 0,
1813
+ scale,
1814
+ layer: count
1815
+ };
1816
+ const extra = Math.max(0, half - cardWidth / 2);
1817
+ const reach = o.push.length;
1818
+ for (const direction of [-1, 1]) {
1819
+ let previous = centre;
1820
+ for (let distance = 1, index = focused + direction; index >= 0 && index < count; distance++, index += direction) {
1821
+ const card = cards[index];
1822
+ const shift = (o.push[distance - 1] ?? 0) * cardWidth + (reach ? extra * Math.max(0, 1 - (distance - 1) / reach) : 0);
1823
+ const desired = card.x + cardWidth / 2 + direction * shift;
1824
+ const angle = card.rotate * Math.PI / 180;
1825
+ const halfWidth = (cardWidth * Math.cos(angle) + cardHeight * Math.abs(Math.sin(angle))) / 2;
1826
+ const middleHalfWidth = cardWidth * Math.cos(angle) / 2;
1827
+ const clearance = distance === 1 ? Math.max(exposed, half - middleHalfWidth + exposed) : exposed;
1828
+ const boundary = direction === 1 && distance > 1 ? Math.max(previous + clearance, centre + half + exposed + halfWidth) : previous + direction * clearance;
1829
+ let next = direction === 1 ? Math.max(desired, boundary) : Math.min(desired, boundary);
1830
+ if (distance === 1)
1831
+ next = direction === 1 ? Math.min(
1832
+ next,
1833
+ view.left + view.width - middleHalfWidth,
1834
+ centre + half + middleHalfWidth
1835
+ ) : Math.max(
1836
+ next,
1837
+ view.left + middleHalfWidth,
1838
+ centre - half - middleHalfWidth
1839
+ );
1840
+ cards[index] = { ...card, x: next - cardWidth / 2 };
1841
+ previous = next;
1842
+ }
1843
+ }
1844
+ return { cards, width: fan.width, height: band, headroom };
1845
+ }
1846
+ function exponentialOut(progress) {
1847
+ return (1 - Math.exp(-4.5 * progress)) / (1 - Math.exp(-4.5));
1848
+ }
1849
+ var handFanTiming = {
1850
+ /** The focused face rises: 90% in about 45ms. */
1851
+ focus: { duration: 0.09, ease: exponentialOut },
1852
+ /** Sideways travel, neighbours and returning cards: 90% in about 300ms. */
1853
+ settle: { duration: 0.6, ease: exponentialOut }
1854
+ };
1855
+ function cssEasing(ease, samples = 24) {
1856
+ const points = Array.from(
1857
+ { length: samples + 1 },
1858
+ (_, index) => Number(ease(index / samples).toFixed(4))
1859
+ );
1860
+ return `linear(${points.join(", ")})`;
1861
+ }
1862
+
1863
+ // src/headless/features/card-selection.ts
1864
+ function cardSelectionFeature(game, context) {
1865
+ let ids = Object.freeze([]);
1866
+ let source = game.getOptions().source;
1867
+ let snapshot = game.snapshot;
1868
+ let disposed = false;
1869
+ function set(values) {
1870
+ if (disposed) return;
1871
+ const next = [...new Set(values)].filter((id) => game.cards.find(id));
1872
+ if (next.length === ids.length && next.every((id, index) => id === ids[index]))
1873
+ return;
1874
+ ids = Object.freeze(next);
1875
+ branch = capture();
1876
+ context.invalidate();
1877
+ }
1878
+ function capture() {
1879
+ return Object.freeze({
1880
+ cardIds: ids,
1881
+ set,
1882
+ toggle(id) {
1883
+ set(
1884
+ ids.includes(id) ? ids.filter((value) => value !== id) : [...ids, id]
1885
+ );
1886
+ },
1887
+ clear: () => set([])
1888
+ });
1889
+ }
1890
+ let branch = capture();
1891
+ const unsubscribe = game.subscribe(() => {
1892
+ if (source !== game.getOptions().source || snapshot !== game.snapshot) {
1893
+ source = game.getOptions().source;
1894
+ snapshot = game.snapshot;
1895
+ set([]);
1896
+ }
1897
+ });
1898
+ return {
1899
+ root: {
1900
+ get cardSelection() {
1901
+ return branch;
1902
+ }
1903
+ },
1904
+ dispose() {
1905
+ disposed = true;
1906
+ unsubscribe();
1907
+ }
1908
+ };
1909
+ }
7
1910
  export {
1911
+ AmbiguousTargetError,
1912
+ AuthAcceptedFrameSchema,
1913
+ AuthConnectFrameSchema,
1914
+ AuthRefreshFrameSchema,
1915
+ CancelInteractionCommandSchema,
1916
+ ClientGameplayFrameSchema,
1917
+ DREAMBOARD_PLUGIN_PROTOCOL,
1918
+ DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
8
1919
  DREAMBOARD_SDK_PACKAGES,
9
1920
  DREAMBOARD_SDK_PACKAGE_SET,
10
- DREAMBOARD_SDK_VERSION
1921
+ DREAMBOARD_SDK_VERSION,
1922
+ GESTURE_THRESHOLDS,
1923
+ GameEventDetailSchema,
1924
+ GameEventSchema,
1925
+ GameOutcomeSchema,
1926
+ GameplayBackpressureFrameSchema,
1927
+ GameplayBackpressureOperationSchema,
1928
+ GameplayBackpressureReasonSchema,
1929
+ GameplayBasisSchema,
1930
+ GameplayCloseCode,
1931
+ GameplayCredentialSchema,
1932
+ HostToPluginEnvelopeSchema,
1933
+ HostToPluginPayloadSchema,
1934
+ InputDomainSchema,
1935
+ InteractionAvailabilitySchema,
1936
+ InteractionCommitPolicySchema,
1937
+ InteractionDescriptorSchema,
1938
+ InteractionInputDescriptorSchema,
1939
+ InteractionResultSchema,
1940
+ PluginGameplayFrameSchema,
1941
+ PluginPlayerSummarySchema,
1942
+ PluginSessionDescriptorSchema,
1943
+ PluginToHostEnvelopeSchema,
1944
+ PluginToHostPayloadSchema,
1945
+ RuntimeJsonSchema,
1946
+ SeatFrameSchema,
1947
+ SeatProjectionBundleSchema,
1948
+ ServerGameplayFrameSchema,
1949
+ SessionResumeFrameSchema,
1950
+ SessionSnapshotFrameSchema,
1951
+ SimultaneousPhaseSnapshotSchema,
1952
+ SubmitInteractionCommandSchema,
1953
+ SystemActionEventSchema,
1954
+ ViewCardSchema,
1955
+ ZoneHandlesSnapshotSchema,
1956
+ boardEdgeId,
1957
+ boardFeature,
1958
+ boardSpaceHostId,
1959
+ boardVertexId,
1960
+ canonicalizePluginRuntimeJson,
1961
+ cardSelectionFeature,
1962
+ computePluginActionSetVersion,
1963
+ createGameInstance,
1964
+ createGestureRecognizer,
1965
+ createPluginProtocolEnvelopeSchema,
1966
+ cssEasing,
1967
+ digestPluginCommandRequest,
1968
+ digestPluginGameplayFrame,
1969
+ digestPluginRuntimeJson,
1970
+ dragFeature,
1971
+ encodeCanonicalPluginRuntimeJson,
1972
+ exponentialOut,
1973
+ fanLayout,
1974
+ handFan,
1975
+ handFanPresets,
1976
+ handFanTiming,
1977
+ handFeature,
1978
+ hostSource,
1979
+ iframeSource,
1980
+ liftFanCard,
1981
+ magneticDropPoint,
1982
+ materializePluginGameplayFrame,
1983
+ originsFeature,
1984
+ panZoomFeature,
1985
+ parseBoardElementId,
1986
+ parseBoardSpaceHostId,
1987
+ parseHostToPluginEnvelope,
1988
+ parsePerPlayerInstanceId,
1989
+ parsePluginGameplayFrame,
1990
+ parsePluginSessionDescriptor,
1991
+ parsePluginToHostEnvelope,
1992
+ perPlayerInstanceId,
1993
+ shortcutsFeature,
1994
+ staticSource
11
1995
  };
12
1996
  //# sourceMappingURL=index.js.map