@dreamboard-games/sdk 0.5.0-alpha.3 → 0.5.0-alpha.30

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 (40) hide show
  1. package/README.md +344 -21
  2. package/dist/board-element-DXLPBVk-.d.ts +13 -0
  3. package/dist/{chunk-LRILEVKC.js → chunk-63GWNBZX.js} +2763 -2327
  4. package/dist/chunk-63GWNBZX.js.map +1 -0
  5. package/dist/{chunk-SP234IY4.js → chunk-BVWJS3AB.js} +752 -343
  6. package/dist/chunk-BVWJS3AB.js.map +1 -0
  7. package/dist/{chunk-ZPRKWPEP.js → chunk-CWANHA4C.js} +284 -138
  8. package/dist/chunk-CWANHA4C.js.map +1 -0
  9. package/dist/chunk-CZRPUWCT.js +1602 -0
  10. package/dist/chunk-CZRPUWCT.js.map +1 -0
  11. package/dist/{chunk-FMM5LVJW.js → chunk-IEMZ26FI.js} +192 -383
  12. package/dist/chunk-IEMZ26FI.js.map +1 -0
  13. package/dist/{diagnostics-B0lgnuZ6.d.ts → diagnostics-D3mzqjxL.d.ts} +2 -3
  14. package/dist/extract-Bi6_tzUr.d.ts +6899 -0
  15. package/dist/index.d.ts +2058 -385
  16. package/dist/index.js +1260 -288
  17. package/dist/index.js.map +1 -1
  18. package/dist/react.d.ts +153 -8
  19. package/dist/react.js +1396 -30
  20. package/dist/react.js.map +1 -1
  21. package/dist/reducer.d.ts +592 -776
  22. package/dist/reducer.js +4617 -4055
  23. package/dist/reducer.js.map +1 -1
  24. package/dist/shortcuts-CuQXWAMv.d.ts +824 -0
  25. package/dist/testing.d.ts +59 -53
  26. package/dist/testing.js +335 -190
  27. package/dist/testing.js.map +1 -1
  28. package/dist/types-BlvVQ9WL.d.ts +125 -0
  29. package/package.json +17 -17
  30. package/dist/chunk-DMFVAZI2.js +0 -16
  31. package/dist/chunk-DMFVAZI2.js.map +0 -1
  32. package/dist/chunk-FMM5LVJW.js.map +0 -1
  33. package/dist/chunk-LRILEVKC.js.map +0 -1
  34. package/dist/chunk-SP234IY4.js.map +0 -1
  35. package/dist/chunk-VUWNJXLJ.js +0 -52
  36. package/dist/chunk-VUWNJXLJ.js.map +0 -1
  37. package/dist/chunk-ZPRKWPEP.js.map +0 -1
  38. package/dist/definition-BaHNPZol.d.ts +0 -4639
  39. package/dist/model-BHro6yXu.d.ts +0 -288
  40. package/dist/types-DimqnJBq.d.ts +0 -86
@@ -1,9 +1,73 @@
1
1
  import {
2
- immutableCopy
3
- } from "./chunk-DMFVAZI2.js";
4
- import {
5
- inputValueInDomain
6
- } from "./chunk-VUWNJXLJ.js";
2
+ BoardProjectionSchema,
3
+ immutableCopy,
4
+ inputDomainErrors,
5
+ inputTargetInDomain,
6
+ inputValueInDomain,
7
+ inputValueKey,
8
+ requireLookup,
9
+ zonePositions
10
+ } from "./chunk-CZRPUWCT.js";
11
+
12
+ // src/headless/runtime-features.ts
13
+ var runtimeFeatures = /* @__PURE__ */ Symbol("runtimeFeatures");
14
+
15
+ // src/headless/input-control.ts
16
+ function resourceValue(domain, value) {
17
+ const record = value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
18
+ return Object.fromEntries(
19
+ domain.resources.map((resource) => {
20
+ const amount = record[resource.resourceId];
21
+ return [resource.resourceId, typeof amount === "number" ? amount : 0];
22
+ })
23
+ );
24
+ }
25
+ function createInputControl(input, props) {
26
+ const domain = input.getDomain();
27
+ const current = input.getValue();
28
+ const common = { key: input.key, disabled: props.disabled, props };
29
+ if (domain.type === "boundedNumber") {
30
+ if (domain.selection?.mode === "many")
31
+ return {
32
+ ...common,
33
+ type: "boundedNumber",
34
+ domain,
35
+ mode: "many",
36
+ value: Array.isArray(current) ? current.filter(
37
+ (value) => typeof value === "number"
38
+ ) : [],
39
+ setValue: (value) => input.setValue(value)
40
+ };
41
+ return {
42
+ ...common,
43
+ type: "boundedNumber",
44
+ domain,
45
+ mode: "single",
46
+ value: typeof current === "number" ? current : void 0,
47
+ setValue: (value) => value === void 0 ? input.clear() : input.setValue(value)
48
+ };
49
+ }
50
+ if (domain.type === "resourceMap") {
51
+ if (domain.selection?.mode === "many")
52
+ return {
53
+ ...common,
54
+ type: "resourceMap",
55
+ domain,
56
+ mode: "many",
57
+ value: Array.isArray(current) ? current.map((value) => resourceValue(domain, value)) : [],
58
+ setValue: (value) => input.setValue(value)
59
+ };
60
+ return {
61
+ ...common,
62
+ type: "resourceMap",
63
+ domain,
64
+ mode: "single",
65
+ value: current === void 0 ? void 0 : resourceValue(domain, current),
66
+ setValue: (value) => value === void 0 ? input.clear() : input.setValue(value)
67
+ };
68
+ }
69
+ return { ...common, type: "targets", options: input.getTargetOptions() };
70
+ }
7
71
 
8
72
  // src/headless/instance.ts
9
73
  import { createStore } from "@tanstack/store";
@@ -30,68 +94,13 @@ function validateInteractionInputDomains(descriptor, params) {
30
94
  for (const rawInput of descriptor.inputs) {
31
95
  const input = rawInput;
32
96
  const value = params[input.key];
33
- if (value === void 0 || value === null) continue;
34
- const selectionErrors = validateInputSelection(input, value);
35
- for (const error of selectionErrors) {
97
+ if (value === void 0) continue;
98
+ for (const error of inputDomainErrors(
99
+ input.domain,
100
+ value,
101
+ input.domain.selection
102
+ ))
36
103
  pushFieldError(fieldErrors, input.key, error);
37
- }
38
- if (input.domain.type === "choiceList") {
39
- if (!Array.isArray(value)) {
40
- pushFieldError(fieldErrors, input.key, "Expected a list of choices.");
41
- continue;
42
- }
43
- const min = input.domain.min ?? 0;
44
- const max = input.domain.max ?? input.domain.choices?.length ?? Number.POSITIVE_INFINITY;
45
- if (value.length < min) {
46
- pushFieldError(
47
- fieldErrors,
48
- input.key,
49
- `Choose at least ${min} ${pluralize("option", min)}.`
50
- );
51
- }
52
- if (value.length > max) {
53
- pushFieldError(
54
- fieldErrors,
55
- input.key,
56
- `Choose at most ${max} ${pluralize("option", max)}.`
57
- );
58
- }
59
- const allowed = new Set(
60
- input.domain.choices?.map((choice) => choice.value)
61
- );
62
- if (allowed.size > 0 && value.some((item) => !allowed.has(String(item)))) {
63
- pushFieldError(
64
- fieldErrors,
65
- input.key,
66
- "Selected choice is not eligible."
67
- );
68
- }
69
- }
70
- if (input.domain.type === "choice") {
71
- const allowed = new Set(
72
- input.domain.choices?.map((choice) => choice.value)
73
- );
74
- if (allowed.size > 0 && !allowed.has(value)) {
75
- pushFieldError(
76
- fieldErrors,
77
- input.key,
78
- "Selected choice is not eligible."
79
- );
80
- }
81
- }
82
- if (isResolvedTargetDomain(input.domain)) {
83
- const values = valuesForSelection(input.domain.selection, value);
84
- for (const item of values) {
85
- if (!input.domain.eligibleTargets.includes(String(item))) {
86
- pushFieldError(
87
- fieldErrors,
88
- input.key,
89
- "Selected target is not eligible."
90
- );
91
- break;
92
- }
93
- }
94
- }
95
104
  }
96
105
  return fieldErrors;
97
106
  }
@@ -110,10 +119,10 @@ function isManyInput(input) {
110
119
  }
111
120
  function toggleManyValue(current, value, selection) {
112
121
  if (selection.mode !== "many") return [value];
113
- const previous = Array.isArray(current) ? current.map((item) => String(item)) : [];
114
- const existing = previous.indexOf(value);
122
+ const previous = Array.isArray(current) ? current : [];
123
+ const existing = previous.findIndex((item) => sameValue(item, value));
115
124
  if (existing >= 0) {
116
- return previous.filter((item) => item !== value);
125
+ return previous.filter((item) => !sameValue(item, value));
117
126
  }
118
127
  if (selection.max !== void 0 && previous.length >= selection.max) {
119
128
  return previous;
@@ -123,9 +132,8 @@ function toggleManyValue(current, value, selection) {
123
132
  function isManyTargetSelectable(input, current, targetId) {
124
133
  const selection = inputSelection(input);
125
134
  if (selection?.mode !== "many") return true;
126
- const currentValues = Array.isArray(current) ? current.map((item) => String(item)) : [];
127
- if (currentValues.includes(targetId)) return true;
128
- if (selection.distinct && currentValues.includes(targetId)) return false;
135
+ const currentValues = Array.isArray(current) ? current : [];
136
+ if (currentValues.some((item) => sameValue(item, targetId))) return true;
129
137
  return selection.max === void 0 || currentValues.length < selection.max;
130
138
  }
131
139
  function hasInteractionFieldErrors(fieldErrors) {
@@ -136,52 +144,21 @@ function hasInteractionFieldErrors(fieldErrors) {
136
144
  function inputByKey(descriptor, key) {
137
145
  return descriptor.inputs.find((input) => input.key === key);
138
146
  }
139
- function isTargetDomain(domain) {
140
- if (!domain) return false;
141
- return domain.type === "cardTarget" || domain.type === "boardTarget";
142
- }
143
- function isResolvedTargetDomain(domain) {
144
- return (domain.type === "cardTarget" || domain.type === "boardTarget") && domain.projection === "resolved";
145
- }
146
- function validateInputSelection(input, value) {
147
- const selection = inputSelection(input);
148
- if (selection?.mode !== "many") return [];
149
- if (!Array.isArray(value)) return ["Expected a list of values."];
150
- const errors = [];
151
- const min = selection.min;
152
- if (value.length < min) {
153
- errors.push(`Choose at least ${min} ${pluralize("value", min)}.`);
154
- }
155
- if (selection.max !== void 0 && value.length > selection.max) {
156
- errors.push(
157
- `Choose at most ${selection.max} ${pluralize("value", selection.max)}.`
158
- );
159
- }
160
- if (selection.distinct) {
161
- const seen = /* @__PURE__ */ new Set();
162
- for (const item of value) {
163
- const key = String(item);
164
- if (seen.has(key)) {
165
- errors.push("Choose each value only once.");
166
- break;
167
- }
168
- seen.add(key);
169
- }
170
- }
171
- return errors;
172
- }
173
- function valuesForSelection(selection, value) {
174
- if (selection?.mode === "many") return Array.isArray(value) ? value : [];
175
- return [value];
176
- }
177
147
  function inputSelection(input) {
178
- return "selection" in input.domain ? input.domain.selection : void 0;
148
+ if (input.domain.selection) return input.domain.selection;
149
+ if (input.domain.type === "choiceList")
150
+ return {
151
+ mode: "many",
152
+ min: input.domain.min ?? 0,
153
+ max: input.domain.max ?? input.domain.choices.length
154
+ };
155
+ return void 0;
179
156
  }
180
157
  function pushFieldError(fieldErrors, key, message) {
181
158
  fieldErrors[key] = [...fieldErrors[key] ?? [], message];
182
159
  }
183
- function pluralize(word, count) {
184
- return count === 1 ? word : `${word}s`;
160
+ function sameValue(left, right) {
161
+ return inputValueKey(left) === inputValueKey(right);
185
162
  }
186
163
 
187
164
  // src/headless/interaction-router.ts
@@ -236,8 +213,12 @@ function getInteractionDraftReadiness(descriptor, draft) {
236
213
  function routeInteractionTarget(store, descriptor, target) {
237
214
  const input = inputByKey(descriptor, target.inputKey);
238
215
  const currentDraft = store.getDraft(descriptor.interactionKey);
239
- const selection = isTargetDomain(input?.domain) ? input.domain.selection : void 0;
240
- const targetValue = selection?.mode === "many" ? toggleManyValue(currentDraft[target.inputKey], target.value, selection) : target.value;
216
+ const selection = input ? inputSelection(input) : void 0;
217
+ const targetValue = selection?.mode === "many" ? toggleManyValue(
218
+ currentDraft[target.inputKey] ?? input?.defaultValue,
219
+ target.value,
220
+ selection
221
+ ) : target.value;
241
222
  const params = applyInteractionDraftMutation(store, descriptor, [
242
223
  ...Object.entries(target.extraInputs ?? {}).map(([key, value]) => ({
243
224
  key,
@@ -258,18 +239,24 @@ function routeCardInputIntent(store, descriptor, intent) {
258
239
  });
259
240
  }
260
241
  function shouldAutoSubmitInteraction(descriptor) {
261
- return descriptor.commit.mode === "autoWhenReady" && !descriptor.inputs.some((input) => isManyInput(input));
242
+ return descriptor.commit.mode === "autoWhenReady" && !descriptor.inputs.some((input) => input.domain.selection?.mode === "many");
262
243
  }
263
244
 
264
245
  // src/headless/instance.ts
265
246
  var EMPTY = Object.freeze({});
266
247
  var EMPTY_DRAFTS = Object.freeze({});
267
- var equalValue = (a, b) => Object.is(a, b) || typeof a === "object" && typeof b === "object" && JSON.stringify(a) === JSON.stringify(b);
248
+ var equalValue = (a, b) => Object.is(a, b) || typeof a === "object" && typeof b === "object" && inputValueKey(a) === inputValueKey(b);
268
249
  function inDomain(input, value, partial = false) {
269
250
  return inputValueInDomain(input.domain, value, input.domain.selection, {
270
251
  ignoreMinimum: partial
271
252
  });
272
253
  }
254
+ function matchesBoardTarget(input, target) {
255
+ const domain = input.domain;
256
+ return domain.type === "boardTarget" && domain.valueKind === target.valueKind && domain.targetKind === target.kind && (domain.valueKind === "board-id" && target.valueKind === "board-id" ? domain.boardId === target.boardId : domain.valueKind === "board-space" && target.valueKind === "board-space" && domain.eligibleTargets.some(
257
+ (candidate) => candidate.boardId === target.value.boardId
258
+ ));
259
+ }
273
260
  function immutableValues(value) {
274
261
  return immutableCopy(value);
275
262
  }
@@ -286,7 +273,7 @@ function data(action, interaction, input, value) {
286
273
  var AmbiguousTargetError = class extends Error {
287
274
  constructor(id) {
288
275
  super(
289
- `Target '${id}' belongs to more than one interaction. Choose an interaction explicitly.`
276
+ `Target '${id}' belongs to more than one eligible input. Choose an interaction and input explicitly.`
290
277
  );
291
278
  this.name = "AmbiguousTargetError";
292
279
  }
@@ -338,8 +325,9 @@ var InteractionObject = class {
338
325
  getAvailability() {
339
326
  return this.descriptor.availability;
340
327
  }
341
- getIsAvailable() {
342
- return this.descriptor.availability.status === "available";
328
+ getIsAvailable(params) {
329
+ const descriptor = params === void 0 ? this.descriptor : this.owner.descriptorFor(this.descriptor, params);
330
+ return descriptor.availability.status === "available";
343
331
  }
344
332
  getUnavailableReason() {
345
333
  return this.descriptor.availability.status === "available" ? null : this.descriptor.availability.reason;
@@ -351,22 +339,26 @@ var InteractionObject = class {
351
339
  return this.descriptor.step?.index ?? null;
352
340
  }
353
341
  getInput(key) {
342
+ return requireLookup(
343
+ this.findInput(key),
344
+ `Input in interaction ${this.key}`,
345
+ key
346
+ );
347
+ }
348
+ findInput(key) {
354
349
  return this.inputObjects.find((input) => input.key === key);
355
350
  }
356
351
  getInputs() {
357
352
  return this.inputObjects;
358
353
  }
359
- readiness() {
360
- const readiness = getInteractionDraftReadiness(this.descriptor, this.draft);
361
- return {
362
- ...readiness,
363
- ready: readiness.ready && this.descriptor.inputs.every(
364
- (input) => inDomain(input, readiness.values[input.key])
365
- )
366
- };
354
+ readiness(params = this.draft) {
355
+ return getInteractionDraftReadiness(
356
+ this.owner.descriptorFor(this.descriptor, params),
357
+ params
358
+ );
367
359
  }
368
- getIsReady() {
369
- return this.readiness().ready;
360
+ getIsReady(params) {
361
+ return this.readiness(params).ready;
370
362
  }
371
363
  getMissingInputs() {
372
364
  return this.readiness().missingInputs;
@@ -377,8 +369,8 @@ var InteractionObject = class {
377
369
  currentLifetime() {
378
370
  return this.source === this.owner.options.source && this.epoch === this.owner.epoch && !this.owner.disposed;
379
371
  }
380
- submit() {
381
- return this.currentLifetime() ? this.owner.submit(this.key, false) : Promise.resolve({ accepted: false, errorCode: "CLOSED" });
372
+ submit(params) {
373
+ return this.currentLifetime() ? this.owner.submit(this.key, false, params) : Promise.resolve({ accepted: false, errorCode: "CLOSED" });
382
374
  }
383
375
  cancel() {
384
376
  return this.currentLifetime() ? this.owner.submit(this.key, true) : Promise.resolve({ accepted: false, errorCode: "CLOSED" });
@@ -389,39 +381,119 @@ var InteractionObject = class {
389
381
  activate() {
390
382
  if (this.currentLifetime()) this.owner.activate(this.key);
391
383
  }
392
- getSubmitHandler() {
384
+ getSubmitHandler(params) {
393
385
  return (event) => {
394
386
  event?.preventDefault();
395
- this.owner.handle(this.submit());
387
+ this.owner.handle(() => this.submit(params));
396
388
  };
397
389
  }
398
- getSubmitProps() {
399
- const disabled = !this.getIsAvailable() || !this.getIsReady() || this.status !== "open" || !this.connected;
390
+ getSubmitProps(params) {
391
+ const disabled = !this.getIsAvailable(params) || !this.getIsReady(params) || this.status !== "open" || !this.connected;
400
392
  return {
401
393
  ...data("submit", this.key),
402
394
  "data-seat": this.seat,
403
395
  type: "button",
404
396
  disabled,
405
- "data-ready": this.getIsReady(),
397
+ "data-ready": this.getIsReady(params),
406
398
  "data-disabled": disabled,
407
- onClick: this.getSubmitHandler()
399
+ onClick: this.getSubmitHandler(params)
408
400
  };
409
401
  }
410
402
  };
403
+ function createInputTargetLabels(domain, snapshot, boards) {
404
+ const targetLabels = /* @__PURE__ */ new Map();
405
+ const hosts = Object.values(snapshot?.frame.zones ?? {}).flatMap(
406
+ (zones) => Object.values(zones)
407
+ );
408
+ if (domain.type === "cardTarget") {
409
+ domain.eligibleTargets.forEach((id, index) => {
410
+ const card = hosts.map((zone) => zone.cardViewsById[id]).find((card2) => card2 !== void 0);
411
+ targetLabels.set(
412
+ inputValueKey(id),
413
+ card ? card.name || `Card ${index + 1}` : `Card back ${index + 1}`
414
+ );
415
+ });
416
+ }
417
+ if (domain.type === "zonePosition")
418
+ for (const value of zonePositions(domain.zones)) {
419
+ const zone = snapshot?.frame.zones[value.zoneId]?.[value.hostId];
420
+ const id = zone?.cardIds[value.index];
421
+ const name = id === void 0 ? void 0 : zone?.cardViewsById[id]?.name;
422
+ const place = id === void 0 ? "At the end" : `Before ${name || `card ${value.index + 1}`}`;
423
+ targetLabels.set(
424
+ inputValueKey(value),
425
+ domain.zones.length === 1 ? place : `${value.zoneId} (${value.hostId}): ${place}`
426
+ );
427
+ }
428
+ if (domain.type === "tileTarget" || domain.type === "boardTarget") {
429
+ if (domain.type === "tileTarget") {
430
+ const presentations = [
431
+ ...hosts.flatMap((zone) => zone.tiles),
432
+ ...Object.values(boards).flatMap(
433
+ (board) => board.layout === "generic" ? [] : board.tiles
434
+ )
435
+ ];
436
+ domain.eligibleTargets.forEach((ref, index) => {
437
+ const tile = presentations.find((tile2) => tile2.ref === ref);
438
+ targetLabels.set(
439
+ inputValueKey(ref),
440
+ tile?.disclosure === "visible" ? tile.name || `Tile ${index + 1}` : `Tile back ${index + 1}`
441
+ );
442
+ });
443
+ } else {
444
+ domain.eligibleTargets.forEach((value, index) => {
445
+ const boardId = typeof value === "string" ? domain.valueKind === "board-id" ? domain.boardId : void 0 : value.boardId;
446
+ const id = typeof value === "string" ? value : value.spaceId;
447
+ const board = boardId === void 0 ? void 0 : boards[boardId];
448
+ const name = domain.targetKind === "space" ? board?.spaces[id]?.name : board && board.layout !== "generic" ? (domain.targetKind === "edge" ? board.edges : board.vertices).find((target) => target.id === id)?.label : void 0;
449
+ const kind = domain.targetKind === "space" ? "Space" : domain.targetKind === "edge" ? "Edge" : "Vertex";
450
+ targetLabels.set(inputValueKey(value), name || `${kind} ${index + 1}`);
451
+ });
452
+ }
453
+ }
454
+ return targetLabels;
455
+ }
411
456
  var InputObject = class {
412
457
  constructor(interaction, descriptor) {
413
458
  this.interaction = interaction;
414
459
  this.descriptor = descriptor;
415
460
  this.key = descriptor.key;
416
461
  this.kind = descriptor.kind;
462
+ const snapshot = interaction.owner.sourceState.snapshot;
463
+ this.snapshot = snapshot;
464
+ this.targetLabels = createInputTargetLabels(
465
+ descriptor.domain,
466
+ snapshot,
467
+ descriptor.domain.type === "tileTarget" || descriptor.domain.type === "boardTarget" ? interaction.owner.getBoardProjections(snapshot?.frame.view) : {}
468
+ );
417
469
  }
418
470
  interaction;
419
471
  descriptor;
472
+ targetLabels;
473
+ snapshot;
420
474
  key;
421
475
  kind;
476
+ currentLifetime() {
477
+ return this.interaction.currentLifetime() && (this.descriptor.domain.type !== "tileTarget" || this.snapshot === this.interaction.owner.sourceState.snapshot);
478
+ }
422
479
  get game() {
423
480
  return this.interaction.game;
424
481
  }
482
+ get domainType() {
483
+ return this.descriptor.domain.type;
484
+ }
485
+ get selectionMode() {
486
+ return isManyInput(this.descriptor) ? "many" : "single";
487
+ }
488
+ getTargetOptions() {
489
+ const domain = this.getDomain();
490
+ return this.getEligibleTargets().map((value) => ({
491
+ value,
492
+ label: this.targetLabels.get(inputValueKey(value)) ?? (domain.type === "choice" || domain.type === "choiceList" ? domain.choices.find((choice) => Object.is(choice.value, value))?.label : void 0) ?? (value === null ? "None" : typeof value === "object" ? JSON.stringify(value) : String(value)),
493
+ selected: this.getIsSelected(value),
494
+ props: this.getTargetProps(value)
495
+ }));
496
+ }
425
497
  getDomain() {
426
498
  return this.descriptor.domain;
427
499
  }
@@ -429,11 +501,11 @@ var InputObject = class {
429
501
  return this.interaction.draft[this.key] ?? (Object.hasOwn(this.interaction.draft, this.key) ? null : this.descriptor.defaultValue);
430
502
  }
431
503
  setValue(value) {
432
- if (!this.interaction.currentLifetime()) return;
504
+ if (!this.currentLifetime()) return;
433
505
  this.interaction.owner.setInput(this.interaction.key, this.key, value);
434
506
  }
435
507
  clear() {
436
- if (!this.interaction.currentLifetime()) return;
508
+ if (!this.currentLifetime()) return;
437
509
  this.interaction.owner.clearInput(this.interaction.key, this.key);
438
510
  }
439
511
  getIsReady() {
@@ -443,23 +515,20 @@ var InputObject = class {
443
515
  };
444
516
  return getInteractionDraftReadiness(descriptor, {
445
517
  [this.key]: this.getValue()
446
- }).ready && inDomain(this.descriptor, this.getValue());
518
+ }).ready;
447
519
  }
448
520
  getEligibleTargets() {
449
521
  const domain = this.descriptor.domain;
450
- if (domain.type === "cardTarget" || domain.type === "boardTarget")
522
+ if (domain.type === "cardTarget" || domain.type === "boardTarget" || domain.type === "tileTarget")
451
523
  return domain.eligibleTargets;
452
524
  if (domain.type === "choice" || domain.type === "choiceList")
453
525
  return domain.choices.filter((choice) => !choice.disabled).map((choice) => choice.value);
526
+ if (domain.type === "zonePosition") return zonePositions(domain.zones);
454
527
  return [];
455
528
  }
456
529
  getIsEligible(value) {
457
530
  const input = this.descriptor;
458
- const selection = input.domain.selection;
459
- return inDomain(
460
- { ...input, domain: { ...input.domain, selection: void 0 } },
461
- value
462
- ) && (!selection || selection.mode !== "many" || isManyTargetSelectable(input, this.getValue(), String(value)));
531
+ return inputTargetInDomain(input.domain, value) && isManyTargetSelectable(input, this.getValue(), value);
463
532
  }
464
533
  getIsSelected(value) {
465
534
  const current = this.getValue();
@@ -467,7 +536,7 @@ var InputObject = class {
467
536
  }
468
537
  getSelectHandler(value) {
469
538
  return () => {
470
- if (this.interaction.currentLifetime())
539
+ if (this.currentLifetime())
471
540
  this.interaction.owner.select(this.interaction.key, this.key, value);
472
541
  };
473
542
  }
@@ -485,49 +554,43 @@ var InputObject = class {
485
554
  onClick: () => this.getSelectHandler(value)()
486
555
  };
487
556
  }
488
- getFieldProps() {
557
+ getControl() {
489
558
  const disabled = !this.interaction.getIsAvailable() || this.interaction.status !== "open" || !this.interaction.connected;
490
- return {
559
+ return createInputControl(this, {
491
560
  ...data("option", this.interaction.key, this.key),
492
561
  "data-seat": this.interaction.seat,
493
- value: this.getValue() ?? "",
494
562
  disabled,
495
- "data-disabled": disabled,
496
- onChange: (event) => {
497
- if (event.currentTarget.value === "") {
498
- this.clear();
499
- return;
500
- }
501
- this.setValue(
502
- this.descriptor.domain.type === "boundedNumber" ? Number(event.currentTarget.value) : event.currentTarget.value
503
- );
504
- }
505
- };
563
+ "data-disabled": disabled
564
+ });
506
565
  }
507
566
  };
508
567
  var CardObject = class {
509
- constructor(owner, id, zone, index, encoded, routes) {
568
+ constructor(owner, id, zone, hostId, index, view, backImage, routes) {
510
569
  this.owner = owner;
511
570
  this.id = id;
512
571
  this.zone = zone;
572
+ this.hostId = hostId;
513
573
  this.index = index;
514
574
  this.routes = routes;
515
575
  this.epoch = owner.epoch;
516
576
  this.seat = owner.sourceState.snapshot?.players.findIndex(
517
577
  (player) => player.playerId === owner.sourceState.snapshot?.me
518
578
  ) ?? -1;
519
- this.view = encoded ? immutableCopy(JSON.parse(encoded)) : null;
579
+ this.view = view ?? null;
520
580
  this.hidden = this.view === null;
581
+ this.backImage = backImage ?? null;
521
582
  }
522
583
  owner;
523
584
  id;
524
585
  zone;
586
+ hostId;
525
587
  index;
526
588
  routes;
527
589
  epoch;
528
590
  seat;
529
591
  view;
530
592
  hidden;
593
+ backImage;
531
594
  get game() {
532
595
  return this.owner.instance;
533
596
  }
@@ -544,16 +607,16 @@ var CardObject = class {
544
607
  )
545
608
  );
546
609
  }
547
- getCanSelect() {
610
+ getCanSelect(options) {
548
611
  return this.routes.some(
549
- (route) => route.getIsAvailable() && route.status === "open" && route.connected && route.getInputs().some(
550
- (input) => input.descriptor.domain.type === "cardTarget" && input.getIsEligible(this.id)
612
+ (route) => route.getIsAvailable() && (!options?.interaction || route.key === options.interaction) && route.status === "open" && route.connected && route.getInputs().some(
613
+ (input) => input.descriptor.domain.type === "cardTarget" && (!options?.input || input.key === options.input) && input.getIsEligible(this.id)
551
614
  )
552
615
  );
553
616
  }
554
617
  select(options) {
555
618
  if (this.epoch !== this.owner.epoch || this.owner.disposed) return;
556
- this.owner.selectCard(this.id, options?.interaction);
619
+ this.owner.selectCard(this.id, options);
557
620
  }
558
621
  getSelectHandler(options) {
559
622
  return () => this.select(options);
@@ -561,38 +624,129 @@ var CardObject = class {
561
624
  getProps(options) {
562
625
  return {
563
626
  type: "button",
564
- disabled: !this.getCanSelect(),
627
+ disabled: !this.getCanSelect(options),
565
628
  "data-action": "select",
566
629
  "data-value": this.id,
567
630
  "data-interaction": options?.interaction,
631
+ "data-input": options?.input,
568
632
  "data-eligible": this.getIsEligible(),
569
633
  "data-selected": this.getIsSelected(),
570
- "data-disabled": !this.getCanSelect(),
634
+ "data-disabled": !this.getCanSelect(options),
571
635
  "data-hidden": this.hidden,
572
636
  "data-seat": this.seat,
573
637
  onClick: this.getSelectHandler(options)
574
638
  };
575
639
  }
576
640
  };
641
+ var TileObject = class {
642
+ constructor(owner, data2, zone, hostId, index, routes) {
643
+ this.owner = owner;
644
+ this.data = data2;
645
+ this.zone = zone;
646
+ this.hostId = hostId;
647
+ this.index = index;
648
+ this.routes = routes;
649
+ this.epoch = owner.epoch;
650
+ this.snapshot = owner.sourceState.snapshot;
651
+ }
652
+ owner;
653
+ data;
654
+ zone;
655
+ hostId;
656
+ index;
657
+ routes;
658
+ epoch;
659
+ snapshot;
660
+ get ref() {
661
+ return this.data.ref;
662
+ }
663
+ get game() {
664
+ return this.owner.instance;
665
+ }
666
+ getInteractions() {
667
+ return this.routes;
668
+ }
669
+ getIsEligible() {
670
+ return this.routes.some(
671
+ (route) => route.getInputs().some(
672
+ (input) => input.descriptor.domain.type === "tileTarget" && input.getIsEligible(this.ref)
673
+ )
674
+ );
675
+ }
676
+ getIsSelected() {
677
+ return this.routes.some(
678
+ (route) => route.getInputs().some(
679
+ (input) => input.descriptor.domain.type === "tileTarget" && input.getIsSelected(this.ref)
680
+ )
681
+ );
682
+ }
683
+ getCanSelect(options) {
684
+ return this.routes.some(
685
+ (route) => route.getIsAvailable() && (!options?.interaction || route.key === options.interaction) && route.getInputs().some(
686
+ (input) => input.descriptor.domain.type === "tileTarget" && (!options?.input || input.key === options.input) && !input.getTargetProps(this.ref).disabled
687
+ )
688
+ );
689
+ }
690
+ select(options) {
691
+ if (this.owner.disposed || this.epoch !== this.owner.epoch || this.snapshot !== this.owner.sourceState.snapshot)
692
+ return;
693
+ this.owner.routeTarget({ kind: "tile", value: this.ref }, options);
694
+ }
695
+ getSelectHandler(options) {
696
+ return () => this.select(options);
697
+ }
698
+ getTargetProps(options) {
699
+ const disabled = !this.getCanSelect(options);
700
+ return {
701
+ type: "button",
702
+ disabled,
703
+ "data-action": "select",
704
+ "data-value": this.ref,
705
+ "data-interaction": options?.interaction,
706
+ "data-input": options?.input,
707
+ "data-eligible": this.getIsEligible(),
708
+ "data-selected": this.getIsSelected(),
709
+ "data-disabled": disabled,
710
+ "data-hidden": this.data.disclosure === "concealed",
711
+ onClick: this.getSelectHandler(options)
712
+ };
713
+ }
714
+ };
577
715
  var ZoneObject = class {
578
- constructor(owner, id, cards) {
716
+ constructor(owner, id, hostId, cards, tiles) {
579
717
  this.owner = owner;
580
718
  this.id = id;
719
+ this.hostId = hostId;
581
720
  this.cards = cards;
721
+ this.tiles = tiles;
582
722
  }
583
723
  owner;
584
724
  id;
725
+ hostId;
585
726
  cards;
727
+ tiles;
586
728
  get game() {
587
729
  return this.owner.instance;
588
730
  }
589
731
  get count() {
590
- return this.cards.length;
732
+ return this.cards.length + this.tiles.length;
591
733
  }
592
734
  getIsEmpty() {
593
735
  return this.count === 0;
594
736
  }
737
+ getTiles() {
738
+ return this.tiles;
739
+ }
740
+ getTile(ref) {
741
+ return requireLookup(this.findTile(ref), `Tile in zone ${this.id}`, ref);
742
+ }
743
+ findTile(ref) {
744
+ return this.tiles.find((tile) => tile.ref === ref);
745
+ }
595
746
  getCard(id) {
747
+ return requireLookup(this.findCard(id), `Card in zone ${this.id}`, id);
748
+ }
749
+ findCard(id) {
596
750
  return this.cards.find((card) => card.id === id);
597
751
  }
598
752
  getCards(options) {
@@ -633,6 +787,17 @@ var PhaseObject = class {
633
787
  }
634
788
  };
635
789
  var Controller = class {
790
+ boardProjections = /* @__PURE__ */ new WeakMap();
791
+ getBoardProjections(view) {
792
+ if (view === null || typeof view !== "object" || Array.isArray(view))
793
+ return {};
794
+ const cached = this.boardProjections.get(view);
795
+ if (cached) return cached;
796
+ const payload = Reflect.get(view, "boards");
797
+ const boards = payload === void 0 ? {} : immutableCopy(BoardProjectionSchema.parse(payload));
798
+ this.boardProjections.set(view, boards);
799
+ return boards;
800
+ }
636
801
  epoch = 0;
637
802
  options;
638
803
  localDrafts;
@@ -649,6 +814,7 @@ var Controller = class {
649
814
  };
650
815
  subscription;
651
816
  sourceState;
817
+ reportedFailures = /* @__PURE__ */ new WeakMap();
652
818
  pending = null;
653
819
  building;
654
820
  rootProperties = /* @__PURE__ */ new Map();
@@ -675,7 +841,60 @@ var Controller = class {
675
841
  this.localDrafts = options.initialState?.drafts ?? EMPTY_DRAFTS;
676
842
  this.localActive = options.initialState?.activeInteraction ?? null;
677
843
  this.sourceState = options.source.store.get();
844
+ const current = () => this.building ?? this.store.get();
678
845
  const root = {
846
+ get snapshot() {
847
+ return current().snapshot;
848
+ },
849
+ get view() {
850
+ return current().view;
851
+ },
852
+ get version() {
853
+ return current().version;
854
+ },
855
+ get connection() {
856
+ return current().connection;
857
+ },
858
+ get failure() {
859
+ return current().failure;
860
+ },
861
+ get request() {
862
+ return current().request;
863
+ },
864
+ get state() {
865
+ return current().state;
866
+ },
867
+ get phase() {
868
+ return current().phase;
869
+ },
870
+ get turn() {
871
+ return current().turn;
872
+ },
873
+ get me() {
874
+ return current().me;
875
+ },
876
+ get players() {
877
+ return current().players;
878
+ },
879
+ get interactions() {
880
+ return current().interactions;
881
+ },
882
+ get inputs() {
883
+ return current().inputs;
884
+ },
885
+ get zones() {
886
+ return current().zones;
887
+ },
888
+ get cards() {
889
+ return current().cards;
890
+ },
891
+ get events() {
892
+ return current().events;
893
+ },
894
+ store: {
895
+ get: () => this.store.get(),
896
+ subscribe: (listener) => this.store.subscribe(listener)
897
+ },
679
898
  getSnapshot: () => this.building ?? this.store.get(),
680
899
  getOptions: () => this.options,
681
900
  setOptions: (next) => this.setOptions(next),
@@ -684,37 +903,25 @@ var Controller = class {
684
903
  return () => sub.unsubscribe();
685
904
  },
686
905
  dispose: () => this.dispose(),
687
- assertCoverage: () => this.assertCoverage(),
688
- inspect: () => this.store.get()
906
+ assertCoverage: () => this.assertCoverage()
689
907
  };
690
- this.instance = root;
691
- for (const key of [
692
- "snapshot",
693
- "view",
694
- "version",
695
- "connection",
696
- "request",
697
- "state",
698
- "phase",
699
- "turn",
700
- "me",
701
- "players",
702
- "interactions",
703
- "inputs",
704
- "zones",
705
- "cards",
706
- "events"
707
- ])
708
- Object.defineProperty(root, key, {
709
- get: () => (this.building ?? this.store.get())[key]
710
- });
711
- this.store = createStore(Object.freeze(this.build()));
712
- Object.defineProperty(root, "store", {
713
- value: {
714
- get: () => this.store.get(),
715
- subscribe: (listener) => this.store.subscribe(listener)
908
+ for (const [key, descriptor] of Object.entries(
909
+ Object.getOwnPropertyDescriptors(root)
910
+ )) {
911
+ if (descriptor.get) {
912
+ Object.defineProperty(root, key, {
913
+ enumerable: false,
914
+ configurable: false
915
+ });
716
916
  }
917
+ }
918
+ Object.defineProperty(root, "store", {
919
+ enumerable: false,
920
+ configurable: false,
921
+ writable: false
717
922
  });
923
+ this.instance = root;
924
+ this.store = createStore(Object.freeze(this.build()));
718
925
  if ("apply" in options.source)
719
926
  Object.defineProperty(root, "apply", {
720
927
  value: (action) => this.options.source.apply(action)
@@ -723,18 +930,24 @@ var Controller = class {
723
930
  Object.defineProperty(root, "explore", {
724
931
  value: (...args) => this.options.source.explore(...args)
725
932
  });
726
- const context = {
727
- createBoard: (id, data2) => this.object("board", {
728
- id,
933
+ const runtime = {
934
+ game: this.instance,
935
+ getBoards: () => this.getBoardProjections(this.instance.view),
936
+ createBoard: (data2) => this.object("board", {
937
+ id: data2.id,
729
938
  data: immutableCopy(data2),
730
939
  game: this.instance
731
940
  }),
732
- routeTarget: (kind, id, options2) => this.routeTarget(kind, id, options2?.interaction, options2?.boardId),
941
+ routeTarget: (target, options2) => this.routeTarget(target, options2),
942
+ getCanDropCard: (cardId, target) => this.prepareCardDrop(cardId, target) !== void 0,
733
943
  routeCardDrop: (cardId, target) => this.routeCardDrop(cardId, target),
944
+ getCanDropCardGroup: (cardIds, target) => !!this.prepareCardDrop(cardIds, target),
945
+ routeCardGroupDrop: (cardIds, target) => this.routeCardDrop(cardIds, target),
734
946
  invalidate: () => {
735
947
  if (!this.disposed) this.refresh();
736
948
  }
737
949
  };
950
+ const context = { ...runtime, [runtimeFeatures]: runtime };
738
951
  for (const feature of Object.values(
739
952
  features?.(this.instance, context) ?? {}
740
953
  )) {
@@ -762,6 +975,7 @@ var Controller = class {
762
975
  }
763
976
  this.refresh();
764
977
  this.subscribeSource();
978
+ this.reportSourceFailure();
765
979
  }
766
980
  install(target, extension) {
767
981
  if (!extension) return;
@@ -796,7 +1010,8 @@ var Controller = class {
796
1010
  );
797
1011
  }
798
1012
  object(hook, value) {
799
- return Object.freeze(Object.setPrototypeOf(value, this.prototypes[hook]));
1013
+ Object.setPrototypeOf(value, this.prototypes[hook]);
1014
+ return Object.freeze(value);
800
1015
  }
801
1016
  drafts() {
802
1017
  if (this.options.state && this.options.state === this.invalidControlledState)
@@ -833,7 +1048,9 @@ var Controller = class {
833
1048
  const card = draft[input.key];
834
1049
  if (input.domain.type !== "cardTarget" || typeof card !== "string")
835
1050
  continue;
836
- for (const zone of Object.values(zones)) {
1051
+ for (const zone of Object.values(zones).flatMap(
1052
+ (hosts) => Object.values(hosts)
1053
+ )) {
837
1054
  const route = zone.playableByCardId[card]?.find(
838
1055
  (value) => value.interactionKey === base.interactionKey
839
1056
  );
@@ -843,7 +1060,7 @@ var Controller = class {
843
1060
  return base;
844
1061
  }
845
1062
  current(key) {
846
- return this.store.get().interactions.get(key);
1063
+ return this.store.get().interactions.find(key);
847
1064
  }
848
1065
  editable(key) {
849
1066
  const interaction = this.current(key);
@@ -851,11 +1068,11 @@ var Controller = class {
851
1068
  }
852
1069
  setInput(key, inputKey, value) {
853
1070
  const interaction = this.editable(key);
854
- if (!interaction?.getInput(inputKey)) return;
1071
+ if (!interaction?.findInput(inputKey)) return;
855
1072
  this.writeDraft(key, { ...this.drafts()[key], [inputKey]: value });
856
1073
  }
857
1074
  clearInput(key, inputKey) {
858
- if (!this.editable(key)?.getInput(inputKey)) return;
1075
+ if (!this.editable(key)?.findInput(inputKey)) return;
859
1076
  const value = { ...this.drafts()[key] };
860
1077
  delete value[inputKey];
861
1078
  this.writeDraft(key, value);
@@ -869,9 +1086,9 @@ var Controller = class {
869
1086
  const interaction = route ?? this.editable(key);
870
1087
  if (route && (!route.currentLifetime() || !route.getIsAvailable() || this.sourceState.connection !== "ready" || this.sourceState.request || this.pending))
871
1088
  return;
872
- const input = interaction?.getInput(inputKey);
1089
+ const input = interaction?.findInput(inputKey);
873
1090
  if (!interaction || !input?.getIsEligible(value)) return;
874
- if (typeof value === "string" && (input.descriptor.domain.type === "cardTarget" || input.descriptor.domain.type === "boardTarget")) {
1091
+ if (isManyInput(input.descriptor) || typeof value === "string" && (input.descriptor.domain.type === "cardTarget" || input.descriptor.domain.type === "boardTarget" || input.descriptor.domain.type === "tileTarget")) {
875
1092
  let next = { ...this.drafts()[key] };
876
1093
  routeInteractionTarget(
877
1094
  {
@@ -891,61 +1108,67 @@ var Controller = class {
891
1108
  this.activate(key);
892
1109
  const current = this.current(key);
893
1110
  if (current && Object.hasOwn(this.drafts()[key] ?? EMPTY, inputKey) && shouldAutoSubmitInteraction(current.descriptor) && current.getIsReady())
894
- this.handle(this.submit(key, false));
895
- }
896
- selectCard(id, explicit) {
897
- const card = this.store.get().cards.get(id);
898
- if (!card) return;
899
- let routes = card.routes.filter(
900
- (route2) => route2.getIsAvailable() && route2.currentLifetime() && !this.sourceState.request && this.sourceState.connection === "ready" && route2.getInputs().some(
901
- (input2) => input2.descriptor.domain.type === "cardTarget" && input2.getIsEligible(id)
902
- )
903
- );
904
- if (explicit) routes = routes.filter((route2) => route2.key === explicit);
905
- if (routes.length > 1) throw new AmbiguousTargetError(id);
906
- const route = routes[0];
907
- const input = route?.getInputs().find(
908
- (input2) => input2.descriptor.domain.type === "cardTarget" && input2.getIsEligible(id)
909
- );
910
- if (route && input) this.select(route.key, input.key, id, route);
1111
+ this.handle(() => this.submit(key, false));
911
1112
  }
912
- handle(promise) {
913
- void promise.then(
1113
+ selectCard(id, options) {
1114
+ const card = this.store.get().cards.find(id);
1115
+ if (!card || this.sourceState.request || this.sourceState.connection !== "ready")
1116
+ return;
1117
+ const candidates = card.routes.flatMap((interaction) => {
1118
+ if (!interaction.getIsAvailable() || !interaction.currentLifetime() || options?.interaction && interaction.key !== options.interaction)
1119
+ return [];
1120
+ return interaction.getInputs().filter(
1121
+ (input) => input.descriptor.domain.type === "cardTarget" && (!options?.input || input.key === options.input) && input.getIsEligible(id)
1122
+ ).map((input) => ({ interaction, input }));
1123
+ });
1124
+ if (candidates.length > 1) throw new AmbiguousTargetError(id);
1125
+ const candidate = candidates[0];
1126
+ if (candidate)
1127
+ this.select(
1128
+ candidate.interaction.key,
1129
+ candidate.input.key,
1130
+ id,
1131
+ candidate.interaction
1132
+ );
1133
+ }
1134
+ handle(submit) {
1135
+ const source = this.options.source;
1136
+ const active = () => !this.disposed && this.options.source === source;
1137
+ void submit().then(
914
1138
  (result) => {
915
- if (!result.accepted)
1139
+ if (active() && !result.accepted)
916
1140
  this.options.onError?.(
917
1141
  new Error(result.message ?? result.errorCode, { cause: result })
918
1142
  );
919
1143
  },
920
- (error) => this.options.onError?.(error)
1144
+ (error) => {
1145
+ if (!active()) return;
1146
+ const state = source.store.get();
1147
+ if (state.failure === error || state.connection === "closed") return;
1148
+ this.options.onError?.(error);
1149
+ }
921
1150
  );
922
1151
  }
923
- async submit(key, cancel) {
1152
+ async submit(key, cancel, params) {
924
1153
  const source = this.options.source;
925
1154
  const interaction = this.current(key);
926
1155
  if (this.disposed || this.sourceState.connection !== "ready" || this.sourceState.request || this.pending)
927
1156
  return { accepted: false, errorCode: "BUSY" };
928
1157
  if (!interaction) return { accepted: false, errorCode: "UNAVAILABLE" };
929
- if (cancel ? !interaction.getStep()?.canCancel : !interaction.getIsAvailable() || !interaction.getIsReady())
1158
+ if (cancel ? !interaction.getStep()?.canCancel : !interaction.getIsAvailable(params) || !interaction.getIsReady(params))
930
1159
  return { accepted: false, errorCode: "UNAVAILABLE" };
931
1160
  if (!("submit" in source) || !("cancel" in source))
932
1161
  return { accepted: false, errorCode: "READ_ONLY" };
933
1162
  const commandSource = source;
934
1163
  const operation = {
935
- source,
936
1164
  key,
937
- draft: this.drafts()[key] ?? EMPTY,
938
- revision: this.revisions.get(key) ?? 0,
939
- version: this.sourceState.snapshot.version,
940
- epoch: this.epoch,
941
- accepted: false,
942
- cancel
1165
+ draftRevision: params === void 0 ? this.revisions.get(key) ?? 0 : void 0
943
1166
  };
944
1167
  this.pending = operation;
945
1168
  try {
946
1169
  const result = await (cancel ? commandSource.cancel(interaction.id) : commandSource.submit(
947
1170
  interaction.id,
948
- interaction.readiness().values
1171
+ interaction.readiness(params).values
949
1172
  ));
950
1173
  if (this.pending !== operation) return result;
951
1174
  if (!result.accepted) {
@@ -953,8 +1176,10 @@ var Controller = class {
953
1176
  this.refresh();
954
1177
  return result;
955
1178
  }
956
- operation.accepted = true;
957
- this.finishBarrier();
1179
+ this.pending = null;
1180
+ if (operation.draftRevision !== void 0 && (this.revisions.get(operation.key) ?? 0) === operation.draftRevision)
1181
+ this.reset(operation.key);
1182
+ else this.refresh();
958
1183
  return result;
959
1184
  } catch (error) {
960
1185
  if (this.pending === operation) {
@@ -964,21 +1189,12 @@ var Controller = class {
964
1189
  throw error;
965
1190
  }
966
1191
  }
967
- finishBarrier() {
968
- const operation = this.pending;
969
- if (!operation?.accepted || this.sourceState.request || operation.source !== this.options.source || operation.epoch !== this.epoch)
970
- return;
971
- if (this.sourceState.connection === "closed") {
972
- this.pending = null;
973
- this.refresh();
974
- return;
975
- }
976
- if (this.sourceState.connection !== "ready" || (this.sourceState.snapshot?.version ?? -1) <= operation.version)
1192
+ reportSourceFailure() {
1193
+ const failure = this.sourceState.failure;
1194
+ if (!failure || !this.options.onError || this.reportedFailures.get(this.options.source) === failure)
977
1195
  return;
978
- this.pending = null;
979
- if ((this.revisions.get(operation.key) ?? 0) === operation.revision)
980
- this.reset(operation.key);
981
- else this.refresh();
1196
+ this.reportedFailures.set(this.options.source, failure);
1197
+ this.options.onError?.(failure);
982
1198
  }
983
1199
  subscribeSource() {
984
1200
  const source = this.options.source;
@@ -998,7 +1214,7 @@ var Controller = class {
998
1214
  }
999
1215
  this.reconcile();
1000
1216
  this.refresh();
1001
- this.finishBarrier();
1217
+ this.reportSourceFailure();
1002
1218
  });
1003
1219
  }
1004
1220
  setOptions(next) {
@@ -1022,6 +1238,7 @@ var Controller = class {
1022
1238
  }
1023
1239
  this.reconcile();
1024
1240
  this.refresh();
1241
+ this.reportSourceFailure();
1025
1242
  }
1026
1243
  reconcile() {
1027
1244
  const snapshot = this.sourceState.snapshot;
@@ -1047,11 +1264,12 @@ var Controller = class {
1047
1264
  for (const [name, value] of Object.entries(values)) {
1048
1265
  const input = descriptor.inputs.find((input2) => input2.key === name);
1049
1266
  if (!input && this.pending?.key === key) continue;
1050
- if (input?.domain.selection?.mode === "many" && Array.isArray(value)) {
1267
+ const selection = input && inputSelection(input);
1268
+ if (input && selection?.mode === "many" && Array.isArray(value)) {
1051
1269
  const eligible = value.filter(
1052
- (item) => inputValueInDomain(input.domain, item)
1270
+ (item) => inputTargetInDomain(input.domain, item)
1053
1271
  );
1054
- const max = input.domain.selection.max;
1272
+ const max = selection.max;
1055
1273
  const retained = max === void 0 ? eligible : eligible.slice(0, max);
1056
1274
  if (retained.length !== value.length) {
1057
1275
  kept[name] = retained;
@@ -1068,7 +1286,13 @@ var Controller = class {
1068
1286
  }
1069
1287
  if (fieldChanged) next[key] = immutableValues(kept);
1070
1288
  }
1071
- if (changed) this.writeDrafts(Object.freeze(next));
1289
+ if (changed) {
1290
+ const operation = this.pending;
1291
+ const submitted = operation?.draftRevision !== void 0 && (this.revisions.get(operation.key) ?? 0) === operation.draftRevision;
1292
+ this.writeDrafts(Object.freeze(next));
1293
+ if (submitted)
1294
+ operation.draftRevision = this.revisions.get(operation.key);
1295
+ }
1072
1296
  const active = this.active();
1073
1297
  if (active && !snapshot.frame.availableInteractions.some(
1074
1298
  (value) => value.interactionKey === active
@@ -1082,11 +1306,7 @@ var Controller = class {
1082
1306
  this.subscription?.unsubscribe();
1083
1307
  this.pending = null;
1084
1308
  this.options.source.dispose();
1085
- this.sourceState = {
1086
- ...this.sourceState,
1087
- connection: "closed",
1088
- request: null
1089
- };
1309
+ this.sourceState = this.options.source.store.get();
1090
1310
  this.refresh();
1091
1311
  }
1092
1312
  assertCoverage() {
@@ -1096,63 +1316,98 @@ var Controller = class {
1096
1316
  if (missing.length)
1097
1317
  throw new Error(`Interactions not read: ${missing.join(", ")}`);
1098
1318
  }
1099
- routeTarget(kind, id, explicit, boardId) {
1100
- if (kind === "card") {
1101
- this.selectCard(id, explicit);
1319
+ routeTarget(target, options) {
1320
+ if (target.kind === "card") {
1321
+ this.selectCard(target.value, options);
1102
1322
  return;
1103
1323
  }
1104
1324
  const candidates = this.lastInteractions.flatMap(
1105
1325
  (interaction) => interaction.getInputs().filter(
1106
- (input) => input.descriptor.domain.type === "boardTarget" && (input.descriptor.domain.targetKind === kind || kind === "space" && input.descriptor.domain.targetKind === "tile") && (!boardId || input.descriptor.domain.boardId === boardId) && input.getIsEligible(id) && interaction.getIsAvailable() && (!explicit || interaction.key === explicit)
1326
+ (input) => (target.kind === "tile" ? input.descriptor.domain.type === "tileTarget" : matchesBoardTarget(input.descriptor, target)) && input.getIsEligible(target.value) && interaction.getIsAvailable() && (!options?.interaction || interaction.key === options.interaction) && (!options?.input || input.key === options.input)
1107
1327
  ).map((input) => ({ interaction, input }))
1108
1328
  );
1109
- if (candidates.length > 1) throw new AmbiguousTargetError(id);
1329
+ if (candidates.length > 1)
1330
+ throw new AmbiguousTargetError(JSON.stringify(target.value));
1110
1331
  const match = candidates[0];
1111
- if (match) this.select(match.interaction.key, match.input.key, id);
1332
+ if (match)
1333
+ this.select(match.interaction.key, match.input.key, target.value);
1112
1334
  }
1113
- routeCardDrop(cardId, target) {
1335
+ prepareCardDrop(cards, target) {
1114
1336
  if (this.disposed || this.sourceState.request || this.pending || this.sourceState.connection !== "ready")
1115
1337
  return;
1116
- const card = this.store.get().cards.get(cardId);
1117
- if (!card) return;
1118
- const candidates = card.routes.flatMap((interaction2) => {
1119
- if (!interaction2.getIsAvailable() || target.interaction && interaction2.key !== target.interaction)
1120
- return [];
1121
- const cardInput2 = interaction2.getInputs().find(
1122
- (input2) => input2.descriptor.domain.type === "cardTarget" && input2.getIsEligible(cardId)
1123
- );
1124
- if (!cardInput2) return [];
1125
- return interaction2.getInputs().filter(
1126
- (input2) => input2.descriptor.domain.type === "boardTarget" && (input2.descriptor.domain.targetKind === target.kind || target.kind === "space" && input2.descriptor.domain.targetKind === "tile") && (!target.boardId || input2.descriptor.domain.boardId === target.boardId) && input2.getIsEligible(target.id)
1127
- ).map((input2) => ({ interaction: interaction2, cardInput: cardInput2, input: input2 }));
1128
- });
1129
- if (candidates.length > 1) throw new AmbiguousTargetError(target.id);
1130
- const candidate = candidates[0];
1131
- if (!candidate) return;
1132
- const { interaction, cardInput, input } = candidate;
1133
- let next = { ...this.drafts()[interaction.key] };
1134
- routeCardInputIntent(
1135
- {
1136
- getDraft: () => next,
1137
- setInput: (_key, key, value) => {
1138
- next = { ...next, [key]: value };
1338
+ const group = typeof cards !== "string";
1339
+ const cardIds = group ? cards : [cards];
1340
+ const cardId = cardIds[0];
1341
+ if (!cardId || new Set(cardIds).size !== cardIds.length) return;
1342
+ const card = this.store.get().cards.find(cardId);
1343
+ const interaction = card?.routes.find(
1344
+ (route) => route.key === target.interactionKey
1345
+ );
1346
+ if (!interaction?.currentLifetime() || !interaction.getIsAvailable())
1347
+ return;
1348
+ const cardInput = interaction.findInput(target.cardInputKey);
1349
+ if (!cardInput || cardInput.descriptor.domain.type !== "cardTarget" || (group ? !isManyInput(cardInput.descriptor) || !inputValueInDomain(
1350
+ cardInput.descriptor.domain,
1351
+ cardIds,
1352
+ cardInput.descriptor.domain.selection
1353
+ ) : !cardInput.getIsEligible(cardId)))
1354
+ return;
1355
+ const input = target.kind === "interaction" ? void 0 : interaction.findInput(target.inputKey);
1356
+ if (target.kind !== "interaction" && (!input || !(target.kind === "position" ? input.descriptor.domain.type === "zonePosition" : matchesBoardTarget(input.descriptor, target)) || !input.getIsEligible(target.value)))
1357
+ return;
1358
+ const params = target.kind === "interaction" ? target.params : void 0;
1359
+ const submitBound = group || params !== void 0 || target.kind === "position";
1360
+ if (params && Object.keys(params).some(
1361
+ (key) => key === cardInput.key || !interaction.findInput(key)
1362
+ ))
1363
+ return;
1364
+ let next = {
1365
+ ...this.drafts()[interaction.key],
1366
+ ...params
1367
+ };
1368
+ const chosen = next[cardInput.key];
1369
+ const alreadyChosen = !submitBound && !input && isManyInput(cardInput.descriptor) && Array.isArray(chosen) && chosen.includes(cardId);
1370
+ if (submitBound) {
1371
+ next[cardInput.key] = isManyInput(cardInput.descriptor) ? [...cardIds] : cardId;
1372
+ if (input && target.kind !== "interaction")
1373
+ next[input.key] = target.value;
1374
+ } else if (!alreadyChosen)
1375
+ routeCardInputIntent(
1376
+ {
1377
+ getDraft: () => next,
1378
+ setInput: (_key, key, value) => {
1379
+ next = { ...next, [key]: value };
1380
+ },
1381
+ clearInput: (_key, key) => {
1382
+ if (key) delete next[key];
1383
+ }
1139
1384
  },
1140
- clearInput: (_key, key) => {
1141
- if (key) delete next[key];
1385
+ interaction.descriptor,
1386
+ {
1387
+ cardId,
1388
+ cardInputKey: cardInput.key,
1389
+ dropTarget: input && target.kind !== "interaction" ? { inputKey: input.key, value: target.value } : void 0
1142
1390
  }
1143
- },
1144
- interaction.descriptor,
1145
- {
1146
- cardId,
1147
- cardInputKey: cardInput.key,
1148
- dropTarget: { inputKey: input.key, value: target.id }
1149
- }
1150
- );
1151
- this.writeDraft(interaction.key, next);
1391
+ );
1392
+ if (submitBound && (!interaction.getIsAvailable(next) || !interaction.getIsReady(next)))
1393
+ return;
1394
+ return {
1395
+ interaction,
1396
+ next,
1397
+ alreadyChosen,
1398
+ submitBound
1399
+ };
1400
+ }
1401
+ routeCardDrop(cardId, target) {
1402
+ const prepared = this.prepareCardDrop(cardId, target);
1403
+ if (!prepared) return;
1404
+ const { interaction, next, alreadyChosen, submitBound } = prepared;
1405
+ if (!alreadyChosen || target.kind === "interaction" && target.params !== void 0)
1406
+ this.writeDraft(interaction.key, next);
1152
1407
  this.activate(interaction.key);
1153
1408
  const current = this.current(interaction.key);
1154
- if (current && shouldAutoSubmitInteraction(current.descriptor) && current.getIsReady())
1155
- this.handle(current.submit());
1409
+ if (!alreadyChosen && current && (submitBound || shouldAutoSubmitInteraction(current.descriptor)) && current.getIsReady())
1410
+ this.handle(() => current.submit());
1156
1411
  }
1157
1412
  warnUnread() {
1158
1413
  const environment = import.meta.env?.DEV ?? globalThis.process?.env?.NODE_ENV === "development";
@@ -1170,7 +1425,7 @@ var Controller = class {
1170
1425
  }
1171
1426
  build(previous) {
1172
1427
  this.trackDrafts();
1173
- const { snapshot, connection, request } = this.sourceState;
1428
+ const { snapshot, connection, request, failure } = this.sourceState;
1174
1429
  const old = previous?.snapshot;
1175
1430
  const drafts = this.drafts();
1176
1431
  const active = this.active();
@@ -1193,11 +1448,20 @@ var Controller = class {
1193
1448
  );
1194
1449
  const players = sameActors && previous ? previous.players : {
1195
1450
  order: Object.freeze(playerObjects.map((p) => p.id)),
1196
- get: (id) => playerObjects.find((p) => p.id === id),
1451
+ get: (id) => requireLookup(
1452
+ playerObjects.find((p) => p.id === id),
1453
+ "Player",
1454
+ id
1455
+ ),
1456
+ find: (id) => playerObjects.find((p) => p.id === id),
1197
1457
  getAll: () => playerObjects,
1198
1458
  next: (id) => {
1199
1459
  const index = playerObjects.findIndex((p) => p.id === id);
1200
- return index < 0 ? void 0 : playerObjects[(index + 1) % playerObjects.length];
1460
+ return requireLookup(
1461
+ index < 0 ? void 0 : playerObjects[(index + 1) % playerObjects.length],
1462
+ "Next player after",
1463
+ id
1464
+ );
1201
1465
  }
1202
1466
  };
1203
1467
  const me = sameActors && previous ? previous.me : snapshot ? {
@@ -1212,7 +1476,7 @@ var Controller = class {
1212
1476
  const key = base.interactionKey;
1213
1477
  const draft = drafts[key] ?? EMPTY;
1214
1478
  const descriptor = this.descriptorFor(base, draft);
1215
- const status = request || this.pending ? request?.interactionId === descriptor.interactionId && request.phase === "awaiting-frame" ? "submitted" : "submitting" : "open";
1479
+ const status = request || this.pending ? "submitting" : "open";
1216
1480
  const oldObject = this.lastInteractions.find(
1217
1481
  (value) => value.key === key
1218
1482
  );
@@ -1233,6 +1497,14 @@ var Controller = class {
1233
1497
  this.lastInteractions = Object.freeze(interactionObjects);
1234
1498
  const interactions = interactionsSame ? previous.interactions : {
1235
1499
  get: (key) => {
1500
+ this.observed.add(key);
1501
+ return requireLookup(
1502
+ interactionObjects.find((value) => value.key === key),
1503
+ "Interaction",
1504
+ key
1505
+ );
1506
+ },
1507
+ find: (key) => {
1236
1508
  this.observed.add(key);
1237
1509
  return interactionObjects.find((value) => value.key === key);
1238
1510
  },
@@ -1249,37 +1521,65 @@ var Controller = class {
1249
1521
  }
1250
1522
  };
1251
1523
  const inputs = interactionsSame && previous ? previous.inputs : {
1252
- get: (key, name) => interactions.get(key)?.getInput(name)
1524
+ get: (key, name) => requireLookup(
1525
+ interactions.find(key)?.findInput(name),
1526
+ `Input in interaction ${key}`,
1527
+ name
1528
+ ),
1529
+ find: (key, name) => interactions.find(key)?.findInput(name)
1253
1530
  };
1254
1531
  const zonesSame = sameSnapshot && interactionsSame && previous;
1255
1532
  const zones = zonesSame ? previous.zones : (() => {
1256
- const objects = Object.entries(snapshot?.frame.zones ?? {}).map(
1257
- ([id, zone]) => this.object(
1258
- "zone",
1259
- new ZoneObject(
1260
- this,
1261
- id,
1262
- Object.freeze(
1263
- zone.cardIds.map(
1264
- (cardId, index) => this.object(
1265
- "card",
1266
- new CardObject(
1533
+ const objects = Object.entries(snapshot?.frame.zones ?? {}).flatMap(
1534
+ ([id, hosts]) => Object.entries(hosts).map(
1535
+ ([hostId, zone]) => this.object(
1536
+ "zone",
1537
+ new ZoneObject(
1538
+ this,
1539
+ id,
1540
+ hostId,
1541
+ Object.freeze(
1542
+ zone.cardIds.map(
1543
+ (cardId, index) => this.object(
1544
+ "card",
1545
+ new CardObject(
1546
+ this,
1547
+ cardId,
1548
+ id,
1549
+ hostId,
1550
+ index,
1551
+ zone.cardViewsById[cardId],
1552
+ zone.cardBacksById[cardId],
1553
+ Object.freeze(
1554
+ (zone.playableByCardId[cardId] ?? []).map(
1555
+ (descriptor) => this.object(
1556
+ "interaction",
1557
+ new InteractionObject(
1558
+ this,
1559
+ descriptor,
1560
+ drafts[descriptor.interactionKey] ?? EMPTY,
1561
+ request || this.pending ? "submitting" : "open",
1562
+ connection === "ready"
1563
+ )
1564
+ )
1565
+ )
1566
+ )
1567
+ )
1568
+ )
1569
+ )
1570
+ ),
1571
+ Object.freeze(
1572
+ zone.tiles.map(
1573
+ (tile, index) => new TileObject(
1267
1574
  this,
1268
- cardId,
1575
+ tile,
1269
1576
  id,
1577
+ hostId,
1270
1578
  index,
1271
- zone.cardViewsById[cardId],
1272
1579
  Object.freeze(
1273
- (zone.playableByCardId[cardId] ?? []).map(
1274
- (descriptor) => this.object(
1275
- "interaction",
1276
- new InteractionObject(
1277
- this,
1278
- descriptor,
1279
- drafts[descriptor.interactionKey] ?? EMPTY,
1280
- request || this.pending ? "submitting" : "open",
1281
- connection === "ready"
1282
- )
1580
+ interactionObjects.filter(
1581
+ (interaction) => interaction.getInputs().some(
1582
+ (input) => input.descriptor.domain.type === "tileTarget" && input.getIsEligible(tile.ref)
1283
1583
  )
1284
1584
  )
1285
1585
  )
@@ -1290,13 +1590,20 @@ var Controller = class {
1290
1590
  )
1291
1591
  )
1292
1592
  );
1593
+ const find = (id, hostId) => objects.find((zone) => zone.id === id && zone.hostId === hostId);
1293
1594
  return {
1294
- get: (id) => objects.find((zone) => zone.id === id),
1595
+ get: (id, hostId) => requireLookup(find(id, hostId), "Zone", id),
1596
+ find,
1295
1597
  getAll: () => Object.freeze(objects)
1296
1598
  };
1297
1599
  })();
1298
1600
  const cards = zonesSame ? previous.cards : {
1299
- get: (id) => zones.getAll().map((zone) => zone.getCard(id)).find((card) => card !== void 0)
1601
+ get: (id) => requireLookup(
1602
+ zones.getAll().map((zone) => zone.findCard(id)).find((card) => card !== void 0),
1603
+ "Card",
1604
+ id
1605
+ ),
1606
+ find: (id) => zones.getAll().map((zone) => zone.findCard(id)).find((card) => card !== void 0)
1300
1607
  };
1301
1608
  const capturedState = this.lastSnapshotDrafts === drafts && previous?.state.activeInteraction === active ? previous.state : immutableCopy({ drafts, activeInteraction: active });
1302
1609
  this.lastSnapshotDrafts = drafts;
@@ -1308,6 +1615,7 @@ var Controller = class {
1308
1615
  version: snapshot?.version ?? null,
1309
1616
  connection,
1310
1617
  request,
1618
+ failure,
1311
1619
  state: capturedState,
1312
1620
  phase: sameSnapshot && previous ? previous.phase : Object.freeze(
1313
1621
  new PhaseObject(snapshot?.frame.flow.currentPhase ?? null)
@@ -1356,8 +1664,109 @@ function createGameInstance() {
1356
1664
  };
1357
1665
  }
1358
1666
 
1667
+ // src/headless/gesture.ts
1668
+ var GESTURE_THRESHOLDS = Object.freeze({
1669
+ /** Movement, in CSS pixels, before a press becomes a drag or a browse. */
1670
+ slop: 8,
1671
+ /** A touch or pen held this long without moving inspects the card. */
1672
+ holdMs: 350,
1673
+ /** A mouse resting on a card this long inspects it. */
1674
+ hoverMs: 250,
1675
+ /** A finger drags when vertical travel exceeds this multiple of sideways travel. */
1676
+ axisBias: 1.25
1677
+ });
1678
+ function createGestureRecognizer(down, callbacks, options = {}) {
1679
+ const origin = { x: down.clientX, y: down.clientY };
1680
+ const mouse = down.pointerType === "mouse";
1681
+ let kind = "tap";
1682
+ let finished = false;
1683
+ let pull = origin;
1684
+ const holdTimer = mouse ? void 0 : setTimeout(() => {
1685
+ kind = "hold";
1686
+ callbacks.hold();
1687
+ }, GESTURE_THRESHOLDS.holdMs);
1688
+ const point = (event) => ({
1689
+ x: event.clientX,
1690
+ y: event.clientY
1691
+ });
1692
+ const ours = (event) => !finished && (!event || event.pointerId === down.pointerId);
1693
+ function finish() {
1694
+ finished = true;
1695
+ clearTimeout(holdTimer);
1696
+ }
1697
+ const steep = (from, to) => Math.abs(to.y - from.y) > GESTURE_THRESHOLDS.axisBias * Math.abs(to.x - from.x);
1698
+ function drag(from, to) {
1699
+ if (!callbacks.dragStart(from)) return false;
1700
+ kind = "drag";
1701
+ callbacks.dragMove(to);
1702
+ return true;
1703
+ }
1704
+ return {
1705
+ move(event) {
1706
+ if (!ours(event)) return;
1707
+ const at = point(event);
1708
+ if (kind === "drag") return callbacks.dragMove(at);
1709
+ if (kind === "browse") {
1710
+ if (!options.scrub) return;
1711
+ callbacks.browse?.(at);
1712
+ if (!steep(pull, at) || Math.abs(at.y - pull.y) >= GESTURE_THRESHOLDS.slop && !drag(pull, at))
1713
+ pull = at;
1714
+ return;
1715
+ }
1716
+ if (kind !== "tap" || Math.hypot(at.x - origin.x, at.y - origin.y) < GESTURE_THRESHOLDS.slop)
1717
+ return;
1718
+ clearTimeout(holdTimer);
1719
+ const lifts = mouse || options.dragDirection === "any" || steep(origin, at);
1720
+ if (lifts && drag(origin, at)) return;
1721
+ kind = "browse";
1722
+ pull = at;
1723
+ if (options.scrub) callbacks.browse?.(at);
1724
+ },
1725
+ up(event) {
1726
+ if (!ours(event)) return;
1727
+ finish();
1728
+ callbacks.end(kind, point(event));
1729
+ },
1730
+ cancel(event) {
1731
+ if (!ours(event)) return;
1732
+ finish();
1733
+ callbacks.cancel(kind);
1734
+ }
1735
+ };
1736
+ }
1737
+ function magneticDropPoint(point, box, retained, coarse) {
1738
+ const radius = coarse ? retained ? 52 : 28 : retained ? 36 : 18;
1739
+ if (box.width <= 0 || box.height <= 0) return null;
1740
+ const x = Math.max(box.x, Math.min(point.x, box.x + box.width));
1741
+ const y = Math.max(box.y, Math.min(point.y, box.y + box.height));
1742
+ return Math.hypot(point.x - x, point.y - y) <= radius ? { x, y } : null;
1743
+ }
1744
+
1745
+ // src/headless/drop-targets.ts
1746
+ function isSameDropTarget(left, right) {
1747
+ if (left.interactionKey !== right.interactionKey || left.cardInputKey !== right.cardInputKey)
1748
+ return false;
1749
+ if (left.kind === "interaction" || right.kind === "interaction")
1750
+ return left.kind === "interaction" && right.kind === "interaction" && sameParams(left.params, right.params);
1751
+ if (left.kind === "position" || right.kind === "position")
1752
+ return left.kind === "position" && right.kind === "position" && left.inputKey === right.inputKey && inputValueKey(left.value) === inputValueKey(right.value);
1753
+ return left.kind === right.kind && left.valueKind === right.valueKind && inputValueKey(left.value) === inputValueKey(right.value) && (left.valueKind !== "board-id" || right.valueKind === "board-id" && left.boardId === right.boardId) && left.inputKey === right.inputKey;
1754
+ }
1755
+ function sameParams(left, right) {
1756
+ if (left === void 0 || right === void 0) return left === right;
1757
+ const keys = Object.keys(left);
1758
+ return keys.length === Object.keys(right).length && keys.every(
1759
+ (key) => key in right && inputValueKey(left[key]) === inputValueKey(right[key])
1760
+ );
1761
+ }
1762
+
1359
1763
  export {
1764
+ runtimeFeatures,
1360
1765
  AmbiguousTargetError,
1361
- createGameInstance
1766
+ createGameInstance,
1767
+ isSameDropTarget,
1768
+ GESTURE_THRESHOLDS,
1769
+ createGestureRecognizer,
1770
+ magneticDropPoint
1362
1771
  };
1363
- //# sourceMappingURL=chunk-SP234IY4.js.map
1772
+ //# sourceMappingURL=chunk-BVWJS3AB.js.map