@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.
- package/README.md +462 -53
- package/dist/board-element-CqtxU5VZ.d.ts +13 -0
- package/dist/chunk-3RFGPBES.js +1792 -0
- package/dist/chunk-3RFGPBES.js.map +1 -0
- package/dist/chunk-CZRPUWCT.js +1602 -0
- package/dist/chunk-CZRPUWCT.js.map +1 -0
- package/dist/chunk-DMSXVMYY.js +7199 -0
- package/dist/chunk-DMSXVMYY.js.map +1 -0
- package/dist/chunk-U6R7FAH4.js +630 -0
- package/dist/chunk-U6R7FAH4.js.map +1 -0
- package/dist/chunk-YT3RPO5J.js +635 -0
- package/dist/chunk-YT3RPO5J.js.map +1 -0
- package/dist/diagnostics-ZAPEBlJP.d.ts +64 -0
- package/dist/extract-CwJWDrSD.d.ts +6850 -0
- package/dist/index.d.ts +5177 -1
- package/dist/index.js +1990 -6
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +158 -0
- package/dist/react.js +1119 -0
- package/dist/react.js.map +1 -0
- package/dist/reducer.d.ts +1028 -97
- package/dist/reducer.js +4892 -125
- package/dist/reducer.js.map +1 -1
- package/dist/shortcuts-B_RrufOM.d.ts +822 -0
- package/dist/testing.d.ts +529 -569
- package/dist/testing.js +2588 -934
- package/dist/testing.js.map +1 -1
- package/dist/types-BxXRINRF.d.ts +118 -0
- package/package.json +39 -144
- package/dist/HandView-Bi2lxzQP.d.ts +0 -79
- package/dist/ResourceCounter-BFxJknqp.d.ts +0 -98
- package/dist/ThemeProvider-CElaL2ik.d.ts +0 -99
- package/dist/attributes-DbvyMbXw.d.ts +0 -68
- package/dist/browser-interaction.d.ts +0 -910
- package/dist/browser-interaction.js +0 -119
- package/dist/browser-interaction.js.map +0 -1
- package/dist/chunk-32PFKDV7.js +0 -11705
- package/dist/chunk-32PFKDV7.js.map +0 -1
- package/dist/chunk-3BAD4VC2.js +0 -263
- package/dist/chunk-3BAD4VC2.js.map +0 -1
- package/dist/chunk-3SKDNDPG.js +0 -104
- package/dist/chunk-3SKDNDPG.js.map +0 -1
- package/dist/chunk-5YV2WHJ4.js +0 -25
- package/dist/chunk-5YV2WHJ4.js.map +0 -1
- package/dist/chunk-ANRB4XKA.js +0 -90
- package/dist/chunk-ANRB4XKA.js.map +0 -1
- package/dist/chunk-BI4G3PM2.js +0 -1864
- package/dist/chunk-BI4G3PM2.js.map +0 -1
- package/dist/chunk-EVCLTY4Q.js +0 -935
- package/dist/chunk-EVCLTY4Q.js.map +0 -1
- package/dist/chunk-GDPKTBUR.js +0 -564
- package/dist/chunk-GDPKTBUR.js.map +0 -1
- package/dist/chunk-H6VDGFL5.js +0 -2903
- package/dist/chunk-H6VDGFL5.js.map +0 -1
- package/dist/chunk-LR3ZTWQF.js +0 -5960
- package/dist/chunk-LR3ZTWQF.js.map +0 -1
- package/dist/chunk-PF7L4BMG.js +0 -117
- package/dist/chunk-PF7L4BMG.js.map +0 -1
- package/dist/chunk-PN5O6GG2.js +0 -32
- package/dist/chunk-PN5O6GG2.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js +0 -10
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-S2JEDVWH.js +0 -7165
- package/dist/chunk-S2JEDVWH.js.map +0 -1
- package/dist/chunk-T3ZKNUZ7.js +0 -1
- package/dist/chunk-T3ZKNUZ7.js.map +0 -1
- package/dist/chunk-UGH54WZ2.js +0 -223
- package/dist/chunk-UGH54WZ2.js.map +0 -1
- package/dist/chunk-VCDWP5VL.js +0 -746
- package/dist/chunk-VCDWP5VL.js.map +0 -1
- package/dist/chunk-VDXOF4FW.js +0 -69
- package/dist/chunk-VDXOF4FW.js.map +0 -1
- package/dist/chunk-WHR5UW3F.js +0 -1988
- package/dist/chunk-WHR5UW3F.js.map +0 -1
- package/dist/chunk-Y75CFE77.js +0 -17
- package/dist/chunk-Y75CFE77.js.map +0 -1
- package/dist/chunk-Z7QCREWI.js +0 -3435
- package/dist/chunk-Z7QCREWI.js.map +0 -1
- package/dist/chunk-ZZGN2ATS.js +0 -1941
- package/dist/chunk-ZZGN2ATS.js.map +0 -1
- package/dist/components-gAbIz7hK.d.ts +0 -1284
- package/dist/definitions-BgmV_GhC.d.ts +0 -300
- package/dist/diagnostics-DIi3ina7.d.ts +0 -50
- package/dist/digest.d.ts +0 -8
- package/dist/game-CsZScpcn.d.ts +0 -763
- package/dist/hex-board-view-aKkblDp6.d.ts +0 -1228
- package/dist/index-CcT9Q7N7.d.ts +0 -193
- package/dist/index.d-DjzoK7zn.d.ts +0 -2101
- package/dist/package-set.d.ts +0 -13
- package/dist/package-set.js +0 -12
- package/dist/package-set.js.map +0 -1
- package/dist/player-state-Cqpyeql0.d.ts +0 -371
- package/dist/plugin-runtime-contract.d.ts +0 -17
- package/dist/plugin-runtime-contract.js +0 -92
- package/dist/plugin-runtime-contract.js.map +0 -1
- package/dist/primitive-props-BNHDkgd7.d.ts +0 -16
- package/dist/protocol-dYgafTYY.d.ts +0 -314
- package/dist/reducer/advanced.d.ts +0 -82
- package/dist/reducer/advanced.js +0 -51
- package/dist/reducer/advanced.js.map +0 -1
- package/dist/reducer-contract.d.ts +0 -11
- package/dist/reducer-contract.js +0 -16
- package/dist/reducer-contract.js.map +0 -1
- package/dist/reference-games/index.d.ts +0 -31
- package/dist/reference-games/index.js +0 -48
- package/dist/reference-games/index.js.map +0 -1
- package/dist/runtime/primitives.d.ts +0 -250
- package/dist/runtime/primitives.js +0 -189
- package/dist/runtime/primitives.js.map +0 -1
- package/dist/runtime/runtime-api.d.ts +0 -2
- package/dist/runtime/runtime-api.js +0 -1
- package/dist/runtime/runtime-api.js.map +0 -1
- package/dist/runtime/workspace-contract.d.ts +0 -392
- package/dist/runtime/workspace-contract.js +0 -28
- package/dist/runtime/workspace-contract.js.map +0 -1
- package/dist/runtime-RJ5orDJU.d.ts +0 -1699
- package/dist/runtime-api-Bz5pwNU_.d.ts +0 -296
- package/dist/runtime-json-CQ9QbLZ5.d.ts +0 -5
- package/dist/runtime.d.ts +0 -83
- package/dist/runtime.js +0 -242
- package/dist/runtime.js.map +0 -1
- package/dist/schema.d.ts +0 -379
- package/dist/stale-contract-artifact-error-XLaweZtF.d.ts +0 -18
- package/dist/testing-compiler.d.ts +0 -37
- package/dist/testing-compiler.js +0 -272
- package/dist/testing-compiler.js.map +0 -1
- package/dist/testing-runtime.d.ts +0 -119
- package/dist/testing-runtime.js +0 -212
- package/dist/testing-runtime.js.map +0 -1
- package/dist/types-DJj5MJkl.d.ts +0 -256
- package/dist/types-DR7DoB1x.d.ts +0 -28
- package/dist/types-JWCYHmu7.d.ts +0 -122
- package/dist/types.d.ts +0 -1958
- package/dist/types.js +0 -14
- package/dist/types.js.map +0 -1
- package/dist/ui/components.d.ts +0 -17
- package/dist/ui/components.js +0 -216
- package/dist/ui/components.js.map +0 -1
- package/dist/ui/defaults.d.ts +0 -19
- package/dist/ui/defaults.js +0 -104
- package/dist/ui/defaults.js.map +0 -1
- package/dist/ui/player-state.d.ts +0 -2
- package/dist/ui/player-state.js +0 -1
- package/dist/ui/player-state.js.map +0 -1
- package/dist/ui/plugin-styles.css +0 -2
- package/dist/ui-contract-GTUPkf8z.d.ts +0 -1168
- package/dist/ui.d.ts +0 -317
- package/dist/ui.js +0 -277
- package/dist/ui.js.map +0 -1
- package/dist/views-BpuWcOyN.d.ts +0 -1641
package/dist/index.js
CHANGED
|
@@ -1,12 +1,1996 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|