castle-web-cli 0.4.169 → 0.4.171

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 (106) hide show
  1. package/dist/agent-prompts.d.ts +1 -0
  2. package/dist/agent-prompts.js +3 -0
  3. package/dist/agent.js +70 -24
  4. package/dist/atomicFile.d.ts +29 -0
  5. package/dist/atomicFile.js +120 -0
  6. package/dist/castle-host/host.js +108 -0
  7. package/dist/get-deck.js +2 -11
  8. package/dist/headlessCover.d.ts +4 -0
  9. package/dist/headlessCover.js +7 -5
  10. package/dist/ide.d.ts +10 -0
  11. package/dist/ide.js +19 -11
  12. package/dist/index.js +10 -6
  13. package/dist/init.d.ts +1 -1
  14. package/dist/init.js +11 -1
  15. package/dist/remix.js +2 -14
  16. package/dist/save-deck.d.ts +3 -0
  17. package/dist/save-deck.js +40 -73
  18. package/dist/saveCover.d.ts +18 -0
  19. package/dist/saveCover.js +61 -0
  20. package/dist/serve.js +18 -5
  21. package/dist/serveSecurity.d.ts +11 -1
  22. package/dist/serveSecurity.js +38 -4
  23. package/dist/shell/assets/index-6odVZQSZ.css +1 -0
  24. package/dist/shell/assets/index-Ws0WrCbi.js +445 -0
  25. package/dist/shell/index.html +3 -3
  26. package/dist/versionStore.js +13 -4
  27. package/kits/base/CLAUDE.md +9 -0
  28. package/kits/base/castle.json +16 -11
  29. package/kits/base/sdk/README.md +83 -2
  30. package/kits/base/sdk/commands.d.ts +37 -1
  31. package/kits/base/sdk/commands.js +9 -0
  32. package/kits/base/sdk/lifecycle.d.ts +20 -0
  33. package/kits/base/sdk/lifecycle.js +7 -1
  34. package/kits/base/sdk/resumeState.d.ts +2 -0
  35. package/kits/base/sdk/resumeState.js +84 -0
  36. package/kits/base/sdk/runtime.js +65 -0
  37. package/kits/base/sdk/transport.d.ts +3 -0
  38. package/kits/base/sdk/transport.js +39 -5
  39. package/kits/base/sdk/unloadHandshake.d.ts +6 -0
  40. package/kits/base/sdk/unloadHandshake.js +47 -0
  41. package/kits/base/sdk/user.d.ts +4 -0
  42. package/kits/base/sdk/user.js +36 -1
  43. package/kits/multiplayer-2d/CLAUDE.md +20 -9
  44. package/kits/multiplayer-2d/castle.json +4 -4
  45. package/kits/multiplayer-2d/code/server/players.js +27 -6
  46. package/kits/multiplayer-2d/code/server/world.js +24 -1
  47. package/kits/multiplayer-2d/code/systems/multiplayer.js +45 -4
  48. package/kits/multiplayer-2d/package-lock.json +2 -4
  49. package/kits/multiplayer-2d/package.json +1 -0
  50. package/kits/multiplayer-3d/CLAUDE.md +28 -9
  51. package/kits/multiplayer-3d/castle.json +5 -5
  52. package/kits/multiplayer-3d/code/server/players.js +54 -5
  53. package/kits/multiplayer-3d/code/server/world.js +45 -1
  54. package/kits/multiplayer-3d/code/systems/multiplayer.js +108 -6
  55. package/kits/multiplayer-3d/package-lock.json +2 -4
  56. package/kits/multiplayer-3d/package.json +1 -0
  57. package/kits/physics-2d/CLAUDE.md +75 -43
  58. package/kits/physics-2d/behaviors/AnalogStick.jsx +75 -45
  59. package/kits/physics-2d/behaviors/Slingshot.jsx +1 -2
  60. package/kits/physics-2d/castle.json +6 -6
  61. package/kits/physics-2d/editors/PxArtEditor.jsx +85 -182
  62. package/kits/physics-2d/editors/SceneEditor.jsx +25 -4
  63. package/kits/physics-2d/editors/StyleEditor.jsx +6 -2
  64. package/kits/physics-2d/editors/overlayCanvas.js +77 -0
  65. package/kits/physics-2d/editors/pathOverlay.js +5 -13
  66. package/kits/physics-2d/editors/pixelEditorChrome.jsx +5 -1
  67. package/kits/physics-2d/editors/pixelInspector.jsx +4 -4
  68. package/kits/physics-2d/editors/pxArtTimeline.jsx +8 -36
  69. package/kits/physics-2d/editors/pxArtTimeline.module.css +4 -28
  70. package/kits/physics-2d/editors/styleEditor.module.css +23 -4
  71. package/kits/physics-2d/editors/useArtboardZoomPan.js +322 -0
  72. package/kits/physics-2d/engine/SceneUI.jsx +6 -12
  73. package/kits/physics-2d/engine/icons.js +56 -0
  74. package/kits/physics-2d/engine/panelSorting.jsx +7 -14
  75. package/kits/physics-2d/engine/physics/PhysicsSystem.js +66 -11
  76. package/kits/physics-2d/engine/physics/controls.js +76 -12
  77. package/kits/physics-2d/engine/scene.js +47 -9
  78. package/kits/physics-2d/engine/ui.jsx +13 -125
  79. package/kits/physics-2d/engine/ui.module.css +53 -28
  80. package/kits/physics-2d/package-lock.json +0 -29
  81. package/kits/physics-2d/package.json +3 -1
  82. package/kits/physics-2d/pnpm-lock.yaml +1 -17
  83. package/kits/physics-3d/behaviors/Pickup.jsx +5 -3
  84. package/kits/physics-3d/castle.json +5 -5
  85. package/kits/physics-3d/package-lock.json +0 -29
  86. package/kits/physics-3d/package.json +0 -1
  87. package/kits/physics-3d/pnpm-lock.yaml +0 -17
  88. package/kits/real-time/CLAUDE.md +23 -4
  89. package/kits/real-time/castle.json +1 -1
  90. package/kits/real-time/code/client/connection.js +9 -4
  91. package/kits/real-time/code/client/joinOverlay.js +46 -0
  92. package/kits/real-time/code/client/messages.js +4 -0
  93. package/kits/real-time/code/server/gameHooks.js +4 -0
  94. package/kits/real-time/code/server/persist.js +105 -0
  95. package/kits/real-time/code/server/session.js +289 -19
  96. package/kits/real-time/package-lock.json +1139 -0
  97. package/kits/turn-based/CLAUDE.md +76 -20
  98. package/kits/turn-based/castle.json +3 -3
  99. package/kits/turn-based/code/server/index.js +16 -5
  100. package/kits/turn-based/package-lock.json +0 -27
  101. package/kits/turn-based/package.json +2 -2
  102. package/kits/turn-based/room.js +162 -13
  103. package/kits/turn-based/testing.js +276 -92
  104. package/package.json +5 -5
  105. package/dist/shell/assets/index-CVHj503j.css +0 -1
  106. package/dist/shell/assets/index-x_QkP3Xq.js +0 -444
@@ -823,10 +823,10 @@
823
823
  margin-top: 14px;
824
824
  }
825
825
 
826
- /* The blueprint's thumbnail, doubling as the way into the sprite editor. The
827
- pencil sits in the corner rather than replacing the art on hover: on touch
828
- there is no hover, and the affordance has to be visible before it is
829
- touched. */
826
+ /* Thumbnail that opens the sprite editor (blueprint header + actor instance
827
+ header). The pencil sits in the corner rather than replacing the art on
828
+ hover: on touch there is no hover, and the affordance has to be visible
829
+ before it is touched. */
830
830
  .editSpriteButton {
831
831
  position: relative;
832
832
  display: inline-flex;
@@ -1161,9 +1161,9 @@
1161
1161
  height: 300%;
1162
1162
  }
1163
1163
 
1164
- /* Game-time UI overlay -- sits exactly over the canvas and clips to the card.
1165
- Behavior `ui` output lands in `.sceneUiLayer`, which is sized in card units
1166
- and scaled onto the canvas box. */
1164
+ /* Deprecated React `ui` overlay -- prefer behavior `drawUi`. Sits over the
1165
+ canvas and clips to the card. Behavior `ui` output lands in `.sceneUiLayer`,
1166
+ sized in card units and scaled onto the canvas box. */
1167
1167
  .sceneUiRoot {
1168
1168
  position: absolute;
1169
1169
  inset: 0;
@@ -1477,9 +1477,10 @@
1477
1477
  row, expanded leaves a smaller gap before the body. */
1478
1478
  padding: 10px 16px 6px;
1479
1479
  border-bottom: 0;
1480
- /* The row owns the hold-then-drag gesture; without this the browser claims
1481
- the press for scrolling before the hold completes. */
1482
- touch-action: none;
1480
+ /* pan-y: a vertical flick on the title / summary scrolls the inspector.
1481
+ Reorder lives on the grip (touch-action: none), not the whole row — that
1482
+ split is what lets scroll and reorder both work on phones. */
1483
+ touch-action: pan-y;
1483
1484
  color: var(--castle-inspector-text);
1484
1485
  font-size: 14px;
1485
1486
  font-weight: 650;
@@ -1539,9 +1540,6 @@
1539
1540
  font-weight: 400;
1540
1541
  }
1541
1542
 
1542
- /* Grab handle, left of the disclosure. `touch-action: none` is what makes the
1543
- hold-then-drag possible on touch: without it the browser claims the gesture
1544
- for scrolling before the hold completes and the drag never starts. */
1545
1543
  /* The behavior's source file. Sits at the right end of the header row, inset
1546
1544
  far enough to clear the remove button the inspector positions absolutely over
1547
1545
  the panel's top-right corner. */
@@ -1640,28 +1638,37 @@
1640
1638
  flex: 0 0 auto;
1641
1639
  }
1642
1640
 
1643
- /* Affordance only: the whole header row carries the drag listeners, so this is
1644
- a hint rather than a target and can be small. */
1641
+ /* Drag handle for panel reorder. Owns the gesture (`touch-action: none`) so
1642
+ the browser does not steal it for scroll; the rest of the header stays
1643
+ pan-y. Hit target is larger than the glyph for fingers. */
1645
1644
  .panelGrip {
1646
1645
  display: inline-flex;
1647
1646
  align-items: center;
1648
1647
  justify-content: center;
1649
1648
  flex: 0 0 auto;
1650
- margin-left: -2px;
1651
- font-size: 7px;
1649
+ box-sizing: content-box;
1650
+ margin: -6px 0 -6px -6px;
1651
+ padding: 10px 8px;
1652
+ border: 0;
1653
+ background: none;
1654
+ /* Icon sizes itself in `em` via an inline style. */
1655
+ font-size: 11px;
1652
1656
  color: var(--castle-inspector-muted);
1653
- opacity: 0.4;
1657
+ opacity: 0.55;
1658
+ cursor: grab;
1659
+ touch-action: none;
1660
+ -webkit-user-select: none;
1661
+ user-select: none;
1654
1662
  }
1655
1663
 
1656
- .panelGrip:hover {
1664
+ .panelGrip:hover,
1665
+ .panelGrip:focus-visible {
1657
1666
  opacity: 1;
1667
+ color: var(--castle-text);
1658
1668
  }
1659
1669
 
1660
- /* `Icon` sizes itself in `em` via an inline style, which no class rule can
1661
- beat -- so every one of these is sized by the button's font-size, not by a
1662
- width on the svg. */
1663
- .panelGrip {
1664
- font-size: 9px;
1670
+ .panelGrip:active {
1671
+ cursor: grabbing;
1665
1672
  }
1666
1673
 
1667
1674
  /* Every behavior row. Positioned so the lifted one can be raised above its
@@ -2786,12 +2793,17 @@
2786
2793
  linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
2787
2794
  linear-gradient(-45deg, transparent 75%, #1a1a1a 75%);
2788
2795
  background-color: #121212;
2789
- background-size: 16px 16px;
2796
+ /* The check travels with the artboard: PxArtEditor overrides --checker-size
2797
+ with the fit size times the current zoom, so a pinch scales the
2798
+ transparency pattern along with the art instead of re-tiling a fixed 16px
2799
+ grid underneath it. */
2800
+ --checker-size: 16px;
2801
+ background-size: var(--checker-size) var(--checker-size);
2790
2802
  background-position:
2791
2803
  0 0,
2792
- 0 8px,
2793
- 8px -8px,
2794
- -8px 0;
2804
+ 0 calc(var(--checker-size) / 2),
2805
+ calc(var(--checker-size) / 2) calc(var(--checker-size) / -2),
2806
+ calc(var(--checker-size) / -2) 0;
2795
2807
  border: 1px solid var(--castle-inspector-border);
2796
2808
  }
2797
2809
 
@@ -2803,6 +2815,19 @@
2803
2815
  pointer-events: none;
2804
2816
  }
2805
2817
 
2818
+ /* Behind the smooth canvas, so the path-layer snap lattice paints directly on
2819
+ the frame's own background and under everything drawn. Negative z-index
2820
+ children paint in z order after the element's background and before in-flow
2821
+ content, so -2 lands between the frame background and .drawingSmooth. */
2822
+ .drawingLattice {
2823
+ position: absolute;
2824
+ inset: 0;
2825
+ width: 100%;
2826
+ height: 100%;
2827
+ pointer-events: none;
2828
+ z-index: -2;
2829
+ }
2830
+
2806
2831
  /* Behind the main canvas (negative z-index paints before static content in
2807
2832
  the same containing block) — holds the corner-rounded render for "smooth"
2808
2833
  sprites while the main canvas above it stays transparent except for tool
@@ -15,7 +15,6 @@
15
15
  "@dnd-kit/modifiers": "^9.0.0",
16
16
  "@dnd-kit/sortable": "^10.0.0",
17
17
  "@dnd-kit/utilities": "^3.2.2",
18
- "@fortawesome/free-solid-svg-icons": "^5.15.4",
19
18
  "@lezer/highlight": "^1.2.3",
20
19
  "castle-web-fonts": "^1.0.0",
21
20
  "codemirror": "^6.0.2",
@@ -216,7 +215,6 @@
216
215
  "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
217
216
  "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
218
217
  "license": "MIT",
219
- "peer": true,
220
218
  "dependencies": {
221
219
  "@dnd-kit/accessibility": "^3.1.1",
222
220
  "@dnd-kit/utilities": "^3.2.2",
@@ -411,29 +409,6 @@
411
409
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
412
410
  }
413
411
  },
414
- "node_modules/@fortawesome/fontawesome-common-types": {
415
- "version": "0.2.36",
416
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
417
- "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==",
418
- "hasInstallScript": true,
419
- "license": "MIT",
420
- "engines": {
421
- "node": ">=6"
422
- }
423
- },
424
- "node_modules/@fortawesome/free-solid-svg-icons": {
425
- "version": "5.15.4",
426
- "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
427
- "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
428
- "hasInstallScript": true,
429
- "license": "(CC-BY-4.0 AND MIT)",
430
- "dependencies": {
431
- "@fortawesome/fontawesome-common-types": "^0.2.36"
432
- },
433
- "engines": {
434
- "node": ">=6"
435
- }
436
- },
437
412
  "node_modules/@humanfs/core": {
438
413
  "version": "0.19.2",
439
414
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -670,7 +645,6 @@
670
645
  "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
671
646
  "dev": true,
672
647
  "license": "MIT",
673
- "peer": true,
674
648
  "bin": {
675
649
  "acorn": "bin/acorn"
676
650
  },
@@ -1131,7 +1105,6 @@
1131
1105
  "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
1132
1106
  "dev": true,
1133
1107
  "license": "MIT",
1134
- "peer": true,
1135
1108
  "dependencies": {
1136
1109
  "@eslint-community/eslint-utils": "^4.8.0",
1137
1110
  "@eslint-community/regexpp": "^4.12.1",
@@ -2441,7 +2414,6 @@
2441
2414
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
2442
2415
  "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
2443
2416
  "license": "MIT",
2444
- "peer": true,
2445
2417
  "engines": {
2446
2418
  "node": ">=0.10.0"
2447
2419
  }
@@ -2451,7 +2423,6 @@
2451
2423
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
2452
2424
  "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
2453
2425
  "license": "MIT",
2454
- "peer": true,
2455
2426
  "dependencies": {
2456
2427
  "scheduler": "^0.27.0"
2457
2428
  },
@@ -16,6 +16,9 @@
16
16
  "behaviors"
17
17
  ],
18
18
  "threshold": 0,
19
+ "ignore": [
20
+ "engine/icons.js"
21
+ ],
19
22
  "reporters": [
20
23
  "consoleFull"
21
24
  ]
@@ -30,7 +33,6 @@
30
33
  "@dnd-kit/modifiers": "^9.0.0",
31
34
  "@dnd-kit/sortable": "^10.0.0",
32
35
  "@dnd-kit/utilities": "^3.2.2",
33
- "@fortawesome/free-solid-svg-icons": "^5.15.4",
34
36
  "@lezer/highlight": "^1.2.3",
35
37
  "castle-web-fonts": "^1.0.0",
36
38
  "codemirror": "^6.0.2",
@@ -35,9 +35,6 @@ importers:
35
35
  '@dnd-kit/utilities':
36
36
  specifier: ^3.2.2
37
37
  version: 3.2.2(react@19.2.7)
38
- '@fortawesome/free-solid-svg-icons':
39
- specifier: ^5.15.4
40
- version: 5.15.4
41
38
  '@lezer/highlight':
42
39
  specifier: ^1.2.3
43
40
  version: 1.2.3
@@ -186,14 +183,6 @@ packages:
186
183
  resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
187
184
  engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
188
185
 
189
- '@fortawesome/fontawesome-common-types@0.2.36':
190
- resolution: {integrity: sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==}
191
- engines: {node: '>=6'}
192
-
193
- '@fortawesome/free-solid-svg-icons@5.15.4':
194
- resolution: {integrity: sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==}
195
- engines: {node: '>=6'}
196
-
197
186
  '@humanfs/core@0.19.2':
198
187
  resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
199
188
  engines: {node: '>=18.18.0'}
@@ -447,6 +436,7 @@ packages:
447
436
  eslint@9.39.4:
448
437
  resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==}
449
438
  engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
439
+ deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
450
440
  hasBin: true
451
441
  peerDependencies:
452
442
  jiti: '*'
@@ -1081,12 +1071,6 @@ snapshots:
1081
1071
  '@eslint/core': 0.17.0
1082
1072
  levn: 0.4.1
1083
1073
 
1084
- '@fortawesome/fontawesome-common-types@0.2.36': {}
1085
-
1086
- '@fortawesome/free-solid-svg-icons@5.15.4':
1087
- dependencies:
1088
- '@fortawesome/fontawesome-common-types': 0.2.36
1089
-
1090
1074
  '@humanfs/core@0.19.2':
1091
1075
  dependencies:
1092
1076
  '@humanfs/types': 0.15.0
@@ -4,7 +4,9 @@
4
4
  //
5
5
  // Collection is remembered in the persistent store keyed by scene + actor id,
6
6
  // so re-entering a scene doesn't respawn (and double-count) shards
7
- // -- scene loads otherwise reset the world to its authored state.
7
+ // -- scene loads otherwise reset the world to its authored state. The store is
8
+ // a plain object, not a Set: `scene.persistent` has to survive JSON, since a
9
+ // multiplayer kit saves it per player.
8
10
  function pickupKey(scene, actor) {
9
11
  return `${scene.currentScenePath ?? ''}:${actor.id}`;
10
12
  }
@@ -18,7 +20,7 @@ export class Pickup {
18
20
  }
19
21
 
20
22
  update(actor, scene, dt) {
21
- if (scene.persistent?.collected?.has(pickupKey(scene, actor))) {
23
+ if (scene.persistent?.collected?.[pickupKey(scene, actor)]) {
22
24
  scene.despawnActor(actor.id);
23
25
  return;
24
26
  }
@@ -33,7 +35,7 @@ export class Pickup {
33
35
  if (!other.components.Player) return;
34
36
  const per = (scene.persistent ??= {});
35
37
  per.score = (per.score ?? 0) + 1;
36
- (per.collected ??= new Set()).add(pickupKey(scene, actor));
38
+ (per.collected ??= {})[pickupKey(scene, actor)] = true;
37
39
  scene.despawnActor(actor.id);
38
40
  }
39
41
  }
@@ -42,7 +42,7 @@
42
42
  "ext": ".pxmodel",
43
43
  "label": "Model",
44
44
  "new": "New model",
45
- "icon": "clone",
45
+ "icon": "cube-transparent",
46
46
  "editor": "kit",
47
47
  "data": true
48
48
  },
@@ -50,7 +50,7 @@
50
50
  "ext": ".jsx",
51
51
  "label": "Behavior",
52
52
  "new": "New behavior",
53
- "icon": "code"
53
+ "icon": "file-code"
54
54
  }
55
55
  ],
56
56
  "defaultPlayFile": "scenes/main.scene"
@@ -58,16 +58,16 @@
58
58
  "imports": {
59
59
  "castle.physics-2d": {
60
60
  "deckId": "ckRZGFW4iPrx",
61
- "version": "2026-08-31T02:59:16.016Z"
61
+ "version": "2026-09-10T00:57:31.408Z"
62
62
  },
63
63
  "castle.base": {
64
64
  "deckId": "yRcmH4_aYllE",
65
- "version": "2026-09-01T01:02:34.326Z"
65
+ "version": "2026-09-10T00:56:59.388Z"
66
66
  }
67
67
  },
68
68
  "main": "main.jsx",
69
69
  "autoUpdateWhenImported": true,
70
70
  "deckId": "JH0SclbPVP0y",
71
71
  "cardId": "eXfAaUdbSU5A",
72
- "publishedVersion": "2026-09-05T00:07:37.655Z"
72
+ "publishedVersion": "2026-09-10T20:10:28.916Z"
73
73
  }
@@ -16,7 +16,6 @@
16
16
  "@dnd-kit/modifiers": "^9.0.0",
17
17
  "@dnd-kit/sortable": "^10.0.0",
18
18
  "@dnd-kit/utilities": "^3.2.2",
19
- "@fortawesome/free-solid-svg-icons": "^5.15.4",
20
19
  "@lezer/highlight": "^1.2.3",
21
20
  "castle-web-fonts": "^1.0.0",
22
21
  "codemirror": "^6.0.2",
@@ -224,7 +223,6 @@
224
223
  "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
225
224
  "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
226
225
  "license": "MIT",
227
- "peer": true,
228
226
  "dependencies": {
229
227
  "@dnd-kit/accessibility": "^3.1.1",
230
228
  "@dnd-kit/utilities": "^3.2.2",
@@ -419,29 +417,6 @@
419
417
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
420
418
  }
421
419
  },
422
- "node_modules/@fortawesome/fontawesome-common-types": {
423
- "version": "0.2.36",
424
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
425
- "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==",
426
- "hasInstallScript": true,
427
- "license": "MIT",
428
- "engines": {
429
- "node": ">=6"
430
- }
431
- },
432
- "node_modules/@fortawesome/free-solid-svg-icons": {
433
- "version": "5.15.4",
434
- "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
435
- "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
436
- "hasInstallScript": true,
437
- "license": "(CC-BY-4.0 AND MIT)",
438
- "dependencies": {
439
- "@fortawesome/fontawesome-common-types": "^0.2.36"
440
- },
441
- "engines": {
442
- "node": ">=6"
443
- }
444
- },
445
420
  "node_modules/@humanfs/core": {
446
421
  "version": "0.19.2",
447
422
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -678,7 +653,6 @@
678
653
  "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
679
654
  "dev": true,
680
655
  "license": "MIT",
681
- "peer": true,
682
656
  "bin": {
683
657
  "acorn": "bin/acorn"
684
658
  },
@@ -1139,7 +1113,6 @@
1139
1113
  "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
1140
1114
  "dev": true,
1141
1115
  "license": "MIT",
1142
- "peer": true,
1143
1116
  "dependencies": {
1144
1117
  "@eslint-community/eslint-utils": "^4.8.0",
1145
1118
  "@eslint-community/regexpp": "^4.12.1",
@@ -2449,7 +2422,6 @@
2449
2422
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
2450
2423
  "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
2451
2424
  "license": "MIT",
2452
- "peer": true,
2453
2425
  "engines": {
2454
2426
  "node": ">=0.10.0"
2455
2427
  }
@@ -2459,7 +2431,6 @@
2459
2431
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
2460
2432
  "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
2461
2433
  "license": "MIT",
2462
- "peer": true,
2463
2434
  "dependencies": {
2464
2435
  "scheduler": "^0.27.0"
2465
2436
  },
@@ -29,7 +29,6 @@
29
29
  "@dnd-kit/modifiers": "^9.0.0",
30
30
  "@dnd-kit/sortable": "^10.0.0",
31
31
  "@dnd-kit/utilities": "^3.2.2",
32
- "@fortawesome/free-solid-svg-icons": "^5.15.4",
33
32
  "@lezer/highlight": "^1.2.3",
34
33
  "castle-web-fonts": "^1.0.0",
35
34
  "codemirror": "^6.0.2",
@@ -38,9 +38,6 @@ importers:
38
38
  '@dnd-kit/utilities':
39
39
  specifier: ^3.2.2
40
40
  version: 3.2.2(react@19.2.8)
41
- '@fortawesome/free-solid-svg-icons':
42
- specifier: ^5.15.4
43
- version: 5.15.4
44
41
  '@lezer/highlight':
45
42
  specifier: ^1.2.3
46
43
  version: 1.2.3
@@ -195,14 +192,6 @@ packages:
195
192
  resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
196
193
  engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
197
194
 
198
- '@fortawesome/fontawesome-common-types@0.2.36':
199
- resolution: {integrity: sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==}
200
- engines: {node: '>=6'}
201
-
202
- '@fortawesome/free-solid-svg-icons@5.15.4':
203
- resolution: {integrity: sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==}
204
- engines: {node: '>=6'}
205
-
206
195
  '@humanfs/core@0.19.2':
207
196
  resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
208
197
  engines: {node: '>=18.18.0'}
@@ -1096,12 +1085,6 @@ snapshots:
1096
1085
  '@eslint/core': 0.17.0
1097
1086
  levn: 0.4.1
1098
1087
 
1099
- '@fortawesome/fontawesome-common-types@0.2.36': {}
1100
-
1101
- '@fortawesome/free-solid-svg-icons@5.15.4':
1102
- dependencies:
1103
- '@fortawesome/fontawesome-common-types': 0.2.36
1104
-
1105
1088
  '@humanfs/core@0.19.2':
1106
1089
  dependencies:
1107
1090
  '@humanfs/types': 0.15.0
@@ -20,6 +20,16 @@ imports these modules from here directly.
20
20
  - **A deck normally writes nothing against it.** The multiplayer kit it came with
21
21
  already drives all of this; a deck's own code goes in `code/server/game.js` and
22
22
  `code/client/game.js`, which that kit documents.
23
+ - **What persists.** Object poses, the ids of scene actors a client despawned,
24
+ each player's last pose and state block by account, and whatever the optional
25
+ `save(sim)` hook returns. Written every ten seconds when something moved and
26
+ at a clean shutdown. Nothing to configure.
27
+ - **A returning player starts where they left off.** A second connection from
28
+ the same account while the first is still playing gets a new character at the
29
+ spawn. The room is `session.sessionId`; every `public` shard shares one.
30
+ - `restore(sim, saved)` receives the last `save` result before `ready`. Storage
31
+ is keyed by deck, so the deck must be published; `castle-web serve` brokers it
32
+ as the deck's creator.
23
33
  - A pose is an opaque array of numbers here, so anything reading inside one takes
24
34
  the kit's `code/client/poses.js` as an argument. That is the seam to use when
25
35
  something genuinely needs a lower level.
@@ -59,10 +69,19 @@ never learns the format.
59
69
  values of a player pose are geometry before the `clamped` flag, and `epsilonAt`,
60
70
  which a kit whose pose values are all one unit leaves undefined.
61
71
 
62
- `soloBadge.js` is the one file here that touches the DOM: a fixed badge that
63
- names why a session fell back to solo (`status().soloReason` from
64
- `connection.js`), drawn by both kits each frame through `updateSoloBadge`. It
65
- stays dependency-free and hides itself when a deck's `soloBadge` hook says so.
72
+ Persistence adds to that contract: `world.restorePoses(sim, poses)` puts saved
73
+ poses back on the bodies the scene still has, and `players.addPlayer` takes the
74
+ saved pose a returning player starts from. Optional beside those:
75
+ `world.removeActor(sim, id)` drops a despawned actor's body, and
76
+ `players.setState` / `players.stateOf` keep the block a client reports.
77
+
78
+ Two files here touch the DOM, both dependency-free. `soloBadge.js` is a fixed
79
+ badge that names why a session fell back to solo (`status().soloReason` from
80
+ `connection.js`), drawn by both kits each frame through `updateSoloBadge`; a
81
+ deck's `soloBadge` hook hides it. `joinOverlay.js` covers the card with
82
+ "Joining…" from the first frame until the session's first snapshot places the
83
+ player, so nobody plays from the scene's spawn before the saved spot arrives;
84
+ it gives up after ten seconds with a console warning.
66
85
 
67
86
  ## Rules for editing this kit
68
87
 
@@ -15,5 +15,5 @@
15
15
  "autoUpdateWhenImported": true,
16
16
  "deckId": "xFVr-afOLuUQ",
17
17
  "cardId": "w6G45sy3_P_m",
18
- "publishedVersion": "2026-09-05T01:31:28.578Z"
18
+ "publishedVersion": "2026-09-10T20:10:30.302Z"
19
19
  }
@@ -81,7 +81,8 @@ export async function connectSession({ onLog = () => {} } = {}) {
81
81
  // the transport handles every dimensional format in the same way.
82
82
  // The client system calls this every frame; most calls only accumulate the
83
83
  // ownership requests, which are held until the next rate-limited send.
84
- sendMove: (pose, owned, claims, releases) => sendMove(net, pose, owned, claims, releases),
84
+ sendMove: (pose, owned, claims, releases, state) =>
85
+ sendMove(net, pose, owned, claims, releases, state),
85
86
 
86
87
  // Send deck-specific data. The server routes these to `code/server/game.js`.
87
88
  send: (data) => net.connection.send(data),
@@ -106,8 +107,10 @@ export async function connectSession({ onLog = () => {} } = {}) {
106
107
  };
107
108
  }
108
109
 
109
- // Collect ownership requests from every frame and send at `MOVE_HZ`.
110
- function sendMove(net, pose, owned, claims, releases) {
110
+ // Collect ownership requests from every frame and send at `MOVE_HZ`. `state` is
111
+ // the player's own block, sent along when the caller has one to send. Returns
112
+ // whether a packet went out.
113
+ function sendMove(net, pose, owned, claims, releases, state) {
111
114
  // Gathered on every frame, including the ones that send nothing, so a claim
112
115
  // made in between is still in the next message. The sets also collapse
113
116
  // repeated requests for the same object over that interval.
@@ -119,7 +122,7 @@ function sendMove(net, pose, owned, claims, releases) {
119
122
  }
120
123
  const now = performance.now();
121
124
  if (now - net.sentAt < SEND_MS) {
122
- return;
125
+ return false;
123
126
  }
124
127
  net.sentAt = now;
125
128
 
@@ -130,9 +133,11 @@ function sendMove(net, pose, owned, claims, releases) {
130
133
  c: [...net.claims],
131
134
  r: [...net.releases],
132
135
  o: owned,
136
+ ...(state === undefined ? {} : { s: state }),
133
137
  });
134
138
 
135
139
  // Clear only after a packet; rate-limited returns above retain the requests.
136
140
  net.claims.clear();
137
141
  net.releases.clear();
142
+ return true;
138
143
  }
@@ -0,0 +1,46 @@
1
+ // A full-card overlay shown from connecting until the server's first snapshot
2
+ // places this player. Under it the local character may still be standing at the
3
+ // scene's spawn; the overlay hides that and takes the pointer, so nothing is
4
+ // played until the session says where things are.
5
+ //
6
+ // It mounts inside the card when the sdk has marked one, so a capture of the
7
+ // card (a cover, a playtest frame) includes it; otherwise it covers the page.
8
+ //
9
+ // Dependency-free DOM, like the solo badge: the kits draw their scenes
10
+ // differently and share no HUD.
11
+
12
+ let element = null;
13
+
14
+ // Show or hide the overlay. Idempotent; cheap enough to call every frame.
15
+ export function showJoinOverlay(visible) {
16
+ if (typeof document === 'undefined') {
17
+ return;
18
+ }
19
+ if (!visible) {
20
+ element?.remove();
21
+ element = null;
22
+ return;
23
+ }
24
+ if (element) {
25
+ return;
26
+ }
27
+ const card = document.querySelector('#castle-card, [data-castle-card]');
28
+ if (card && getComputedStyle(card).position === 'static') {
29
+ card.style.position = 'relative';
30
+ }
31
+ element = document.createElement('div');
32
+ element.setAttribute('data-castle-join-overlay', '');
33
+ element.style.cssText = [
34
+ card ? 'position:absolute' : 'position:fixed',
35
+ 'inset:0',
36
+ 'display:flex',
37
+ 'align-items:center',
38
+ 'justify-content:center',
39
+ 'background:#0a0a0a',
40
+ 'color:#fff',
41
+ 'font:500 15px/1.4 system-ui,sans-serif',
42
+ 'z-index:2147483646',
43
+ ].join(';');
44
+ element.textContent = 'Joining…';
45
+ (card ?? document.body).appendChild(element);
46
+ }
@@ -15,6 +15,10 @@ export const MOVE = 'i';
15
15
  export const WORLD = 'w';
16
16
  export const STATE = 's';
17
17
 
18
+ // A client reports that it removed a scene actor: `{ k: GONE, id }`. The
19
+ // session removes it for everyone, for good.
20
+ export const GONE = 'g';
21
+
18
22
  // Movement reports run below the render rate and below the platform's roughly
19
23
  // 30-message-per-second client cap. A second report inside one server tick would
20
24
  // be overwritten before the simulation read it.
@@ -21,8 +21,12 @@ const bodyModules = {
21
21
  // `session.js` receives this value from the dimensional kit's server entry and
22
22
  // calls the hooks at the stages listed below.
23
23
  //
24
+ // restore(sim, saved) the value the last `save` returned for this
25
+ // room, or null. Runs before `ready`
24
26
  // ready(sim) after the world and its scene bodies exist,
25
27
  // before the first server tick
28
+ // save(sim) JSON kept for this room, written with the
29
+ // world every ten seconds and at shutdown
26
30
  // message(session, player, d) client data that is not a pose report. It
27
31
  // receives no handover list, so a world change
28
32
  // is kept and applied later by `place` or `step`