castle-web-cli 0.4.171 → 0.4.173

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 (65) hide show
  1. package/dist/assetCompression.d.ts +3 -0
  2. package/dist/assetCompression.js +30 -0
  3. package/dist/castleJson.d.ts +1 -1
  4. package/dist/castleJson.js +2 -2
  5. package/dist/devSessionServer.js +9 -5
  6. package/dist/imports.js +2 -2
  7. package/dist/init.js +4 -4
  8. package/dist/serve.js +2 -0
  9. package/dist/serveSecurity.d.ts +2 -0
  10. package/dist/serveSecurity.js +17 -3
  11. package/kits/multiplayer-2d/CLAUDE.md +4 -0
  12. package/kits/multiplayer-2d/behaviors/Box.jsx +32 -9
  13. package/kits/multiplayer-2d/castle.json +3 -3
  14. package/kits/multiplayer-2d/code/client/avatars.js +6 -1
  15. package/kits/multiplayer-2d/code/server/sceneBodies.js +1 -1
  16. package/kits/multiplayer-2d/code/server/world.js +2 -2
  17. package/kits/multiplayer-2d/code/systems/multiplayer.js +14 -0
  18. package/kits/multiplayer-2d/fonts.generated.js +1 -1
  19. package/kits/multiplayer-2d/package-lock.json +1 -1
  20. package/kits/multiplayer-3d/CLAUDE.md +5 -0
  21. package/kits/multiplayer-3d/castle.json +3 -3
  22. package/kits/multiplayer-3d/code/client/avatars.js +42 -9
  23. package/kits/multiplayer-3d/code/server/sceneBodies.js +1 -1
  24. package/kits/multiplayer-3d/code/systems/multiplayer.js +17 -1
  25. package/kits/multiplayer-3d/fonts.generated.js +1 -1
  26. package/kits/multiplayer-3d/package-lock.json +1 -1
  27. package/kits/physics-2d/CLAUDE.md +7 -6
  28. package/kits/physics-2d/behaviors/Collider.jsx +7 -1
  29. package/kits/physics-2d/behaviors/RigidBody.jsx +6 -3
  30. package/kits/physics-2d/castle.json +2 -2
  31. package/kits/physics-2d/editors/SceneEditor.jsx +1 -0
  32. package/kits/physics-2d/editors/SingleEditor.jsx +4 -4
  33. package/kits/physics-2d/editors/{StyleEditor.jsx → ThemeEditor.jsx} +8 -8
  34. package/kits/physics-2d/editors/deckFont.js +2 -2
  35. package/kits/physics-2d/editors/{styleEditor.module.css → themeEditor.module.css} +2 -2
  36. package/kits/physics-2d/engine/avatarArt.js +185 -0
  37. package/kits/physics-2d/engine/files.js +2 -2
  38. package/kits/physics-2d/engine/physics/matterBridge.js +2 -2
  39. package/kits/physics-2d/engine/scene.js +1 -1
  40. package/kits/physics-2d/fonts.generated.js +1 -1
  41. package/kits/physics-2d/package-lock.json +1 -1
  42. package/kits/physics-3d/behaviors/Body.jsx +10 -1
  43. package/kits/physics-3d/behaviors/Door.jsx +5 -0
  44. package/kits/physics-3d/behaviors/Lookable.jsx +3 -0
  45. package/kits/physics-3d/behaviors/Model.jsx +4 -0
  46. package/kits/physics-3d/behaviors/Pickup.jsx +4 -0
  47. package/kits/physics-3d/behaviors/Player.jsx +7 -0
  48. package/kits/physics-3d/behaviors/Shape.jsx +13 -0
  49. package/kits/physics-3d/castle.json +1 -1
  50. package/kits/physics-3d/engine3d/editor3dInspector.jsx +1 -0
  51. package/kits/physics-3d/fonts.generated.js +1 -1
  52. package/kits/physics-3d/package-lock.json +1 -1
  53. package/kits/real-time/castle.json +1 -1
  54. package/kits/real-time/code/client/connection.js +17 -2
  55. package/kits/real-time/code/server/session.js +12 -6
  56. package/kits/real-time/package-lock.json +1 -1
  57. package/kits/turn-based/CLAUDE.md +4 -0
  58. package/kits/turn-based/castle.json +2 -2
  59. package/kits/turn-based/code/behaviors/Board.jsx +15 -5
  60. package/kits/turn-based/code/server/index.js +1 -0
  61. package/kits/turn-based/fonts.generated.js +1 -1
  62. package/kits/turn-based/package-lock.json +1 -1
  63. package/kits/turn-based/room.js +2 -0
  64. package/package.json +3 -1
  65. /package/kits/physics-2d/editors/{styleTheme.js → theme.js} +0 -0
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "../../sdk": {
36
36
  "name": "castle-web-sdk",
37
- "version": "0.4.26",
37
+ "version": "0.4.28",
38
38
  "dev": true,
39
39
  "devDependencies": {
40
40
  "eslint": "^9.0.0",
@@ -15,5 +15,5 @@
15
15
  "autoUpdateWhenImported": true,
16
16
  "deckId": "xFVr-afOLuUQ",
17
17
  "cardId": "w6G45sy3_P_m",
18
- "publishedVersion": "2026-09-10T20:10:30.302Z"
18
+ "publishedVersion": "2026-09-10T22:05:27.337Z"
19
19
  }
@@ -12,6 +12,7 @@ const SEND_MS = 1000 / Messages.MOVE_HZ;
12
12
  // connection the SDK connection
13
13
  // inbox arrived messages, each with `localMs` added
14
14
  // roster playerId -> username, as the platform last reported it
15
+ // userIds playerId -> userId, from the same roster
15
16
  // you the SDK's entry for the local player, null until a roster has it
16
17
  // sentAt `performance.now()` of the last move, so sends stay at MOVE_HZ
17
18
  // claims ids asked for since the last send, held until it goes
@@ -21,6 +22,7 @@ function makeNet(connection) {
21
22
  connection,
22
23
  inbox: [],
23
24
  roster: new Map(),
25
+ userIds: new Map(),
24
26
  you: null,
25
27
  sentAt: 0,
26
28
  claims: new Set(),
@@ -48,6 +50,7 @@ export async function connectSession({ onLog = () => {} } = {}) {
48
50
 
49
51
  // Replace the map from the complete SDK roster so departures disappear.
50
52
  net.roster = new Map(players.map((player) => [player.playerId, player.username]));
53
+ net.userIds = new Map(players.map((player) => [player.playerId, player.userId]));
51
54
  net.you = you;
52
55
  });
53
56
 
@@ -61,6 +64,9 @@ export async function connectSession({ onLog = () => {} } = {}) {
61
64
  // A stable fallback until the SDK roster includes this player.
62
65
  username: (playerId) => net.roster.get(playerId) ?? 'player',
63
66
 
67
+ // The account behind a player, for `User.get`; null until the roster has it.
68
+ userId: (playerId) => net.userIds.get(playerId) ?? null,
69
+
64
70
  // Every other player's roster entry. The local player is simulated by this client,
65
71
  // so the deck draws only these from network samples.
66
72
  others: () => new Map([...net.roster].filter(([id]) => id !== connection.playerId)),
@@ -85,7 +91,16 @@ export async function connectSession({ onLog = () => {} } = {}) {
85
91
  sendMove(net, pose, owned, claims, releases, state),
86
92
 
87
93
  // Send deck-specific data. The server routes these to `code/server/game.js`.
88
- send: (data) => net.connection.send(data),
94
+ send: (data) => {
95
+ if (connection.state !== 'closed') {
96
+ net.connection.send(data);
97
+ }
98
+ },
99
+
100
+ // Whether the session has ended under this connection: the server was
101
+ // replaced, the platform closed it, or `close` was called. Nothing is sent
102
+ // on it again; the client system joins afresh.
103
+ closed: () => connection.state === 'closed',
89
104
 
90
105
  // Connection and presence data for a deck status line.
91
106
  status: () => ({
@@ -121,7 +136,7 @@ function sendMove(net, pose, owned, claims, releases, state) {
121
136
  net.releases.add(id);
122
137
  }
123
138
  const now = performance.now();
124
- if (now - net.sentAt < SEND_MS) {
139
+ if (now - net.sentAt < SEND_MS || net.connection.state === 'closed') {
125
140
  return false;
126
141
  }
127
142
  net.sentAt = now;
@@ -80,12 +80,18 @@ function newServer(parts) {
80
80
  export function makeSession(parts) {
81
81
  const server = newServer(parts);
82
82
 
83
- // Castle's handlers remain available while `createSimulation` is pending.
83
+ // Castle's handlers remain available while `createSimulation` is pending. A
84
+ // build that fails is logged here; `buildWorld` sees the same rejection when
85
+ // the first tick awaits it.
84
86
  server.building = server.world.createSimulation();
87
+ server.building.catch((err) => {
88
+ console.error(`[${server.label}] simulation failed to build: ${err?.stack ?? err}`);
89
+ });
85
90
  return {
86
- onStart(session) {
87
- attach(server, session);
88
- },
91
+ // The world is built from the first tick or join, not here: `onStart` runs
92
+ // while the process is still booting, and a storage read made then can go
93
+ // unanswered.
94
+ onStart() {},
89
95
 
90
96
  // Joins are logged here; the simulation's players are reconciled from the
91
97
  // full session roster on the next tick.
@@ -124,8 +130,8 @@ export function makeSession(parts) {
124
130
  };
125
131
  }
126
132
 
127
- // Keep the first session a callback carries and build the world with it. The
128
- // room the session persists under is not known before this.
133
+ // Keep the first session a tick or join carries and build the world with it.
134
+ // The room the session persists under is not known before this.
129
135
  function attach(server, session) {
130
136
  if (server.session) {
131
137
  return;
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "../../sdk": {
18
18
  "name": "castle-web-sdk",
19
- "version": "0.4.27",
19
+ "version": "0.4.28",
20
20
  "dev": true,
21
21
  "devDependencies": {
22
22
  "eslint": "^9.0.0",
@@ -41,6 +41,10 @@ This kit adds no engine and no editors. What it holds:
41
41
  `{ table }`. One table per `session.sessionId`, made on demand. `joinAsync` is
42
42
  the join to use: it reads the saved table first, which the synchronous
43
43
  `room.join` cannot.
44
+ - **A claimed square shows its holder's avatar.** The state carries `users`,
45
+ seat -> userId, and `Board.jsx` swaps the square's `Sprite.file` for the
46
+ avatar from `castle.physics-2d`'s `engine/avatarArt.js`; a holder with no
47
+ photo keeps the seat tint.
44
48
  - **A room comes back.** `table.game`, the seat each account held, and the time
45
49
  of the last accepted move are saved to deck storage on every accepted move, on
46
50
  every leave, and from `room.save(session)` in `onShutdown`. Storage belongs to
@@ -17,7 +17,7 @@
17
17
  "imports": {
18
18
  "castle.physics-2d": {
19
19
  "deckId": "ckRZGFW4iPrx",
20
- "version": "2026-09-10T00:57:31.408Z"
20
+ "version": "2026-09-10T20:21:12.665Z"
21
21
  },
22
22
  "castle.base": {
23
23
  "deckId": "yRcmH4_aYllE",
@@ -28,7 +28,7 @@
28
28
  "autoUpdateWhenImported": true,
29
29
  "deckId": "K6-Xs7dCoMVi",
30
30
  "cardId": "Kh_08IYATpC3",
31
- "publishedVersion": "2026-09-10T20:10:44.202Z",
31
+ "publishedVersion": "2026-09-10T22:05:30.856Z",
32
32
  "server": {
33
33
  "main": "code/server/index.js",
34
34
  "maxPlayers": 8,
@@ -3,8 +3,10 @@
3
3
  // multiplayer session, send square claims, tint actors and format status text.
4
4
 
5
5
  import { Multiplayer } from 'castle-web-sdk';
6
+ import { avatarSpritePath } from '@imports/castle.physics-2d/engine/avatarArt';
6
7
 
7
- // Sprite tint multiplies these seat colors over each square's own artwork.
8
+ // A claimed square shows its holder's avatar. Until that has loaded, or when the
9
+ // holder has none, the seat's tint multiplies the square's own artwork instead.
8
10
  const SEAT_TINT = { A: '#ea323cff', B: '#0cf1ffff' };
9
11
  const UNCLAIMED_TINT = '#ffffffff';
10
12
 
@@ -130,10 +132,18 @@ function paintSquares(scene, state) {
130
132
  for (const squareActor of scene.actorsWith('Square')) {
131
133
  // Square.index selects the same slot in the server's sixteen-element array.
132
134
  const seat = state.squares[squareActor.components.Square.index];
133
- // Sprite tint multiplies the editable `drawings/square.sprite` pixels.
134
- // White preserves their colors; a seat tint displays the square's owner.
135
- // A game with separate pieces can spawn piece actors over these square actors.
136
- squareActor.components.Sprite.tint = seat ? SEAT_TINT[seat] : UNCLAIMED_TINT;
135
+ const sprite = squareActor.components.Sprite;
136
+
137
+ // The square's own art is remembered the first time, since `file` is
138
+ // swapped for an avatar below and back again when the square is cleared.
139
+ squareActor.runtime.squareFile ??= sprite.file;
140
+ const avatar = seat ? avatarSpritePath(scene, state.users?.[seat]) : null;
141
+ sprite.file = avatar ?? squareActor.runtime.squareFile;
142
+
143
+ // Sprite tint multiplies the pixels. White preserves their colors; a seat
144
+ // tint displays the owner of a square without an avatar. A game with
145
+ // separate pieces can spawn piece actors over these square actors.
146
+ sprite.tint = seat && !avatar ? SEAT_TINT[seat] : UNCLAIMED_TINT;
137
147
  }
138
148
  }
139
149
 
@@ -91,6 +91,7 @@ function stateFor(session, table, playerId) {
91
91
  turn: table.game.turn,
92
92
  winner: table.game.winner,
93
93
  names: table.seatNames(),
94
+ users: table.seatUsers(),
94
95
  watching: table.spectatorCount(session),
95
96
  you: table.seatOf(playerId),
96
97
  };
@@ -1,4 +1,4 @@
1
- // Generated by the Style editor. Do not edit -- picking a font rewrites it.
1
+ // Generated by the Theme editor. Do not edit -- picking a font rewrites it.
2
2
  //
3
3
  // `theme.style` records that this deck's font is DMSans; this module is what
4
4
  // puts DMSans's bytes in the published bundle. It has to name the face in a
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "../../sdk": {
30
30
  "name": "castle-web-sdk",
31
- "version": "0.4.26",
31
+ "version": "0.4.28",
32
32
  "dev": true,
33
33
  "devDependencies": {
34
34
  "eslint": "^9.0.0",
@@ -178,6 +178,8 @@ function createTable(settings, session) {
178
178
  playerAt: (seat) => table.seats[seat] ?? null,
179
179
  seatNames: () =>
180
180
  Object.fromEntries(settings.seats.map((seat) => [seat, table.seats[seat]?.name ?? null])),
181
+ seatUsers: () =>
182
+ Object.fromEntries(settings.seats.map((seat) => [seat, table.seats[seat]?.userId ?? null])),
181
183
  spectatorCount: (session) => spectatorCount(table, session),
182
184
 
183
185
  // Call this past every rejection, never before one. A refused message is not
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.171",
3
+ "version": "0.4.173",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"
@@ -41,6 +41,7 @@
41
41
  "@xterm/headless": "^6.0.0",
42
42
  "@xterm/xterm": "^6.0.0",
43
43
  "codemirror": "^6.0.2",
44
+ "compression": "^1.8.1",
44
45
  "html2canvas": "^1.4.1",
45
46
  "marked": "^18.0.5",
46
47
  "nanoid": "^5.1.7",
@@ -54,6 +55,7 @@
54
55
  "ws": "^8.20.0"
55
56
  },
56
57
  "devDependencies": {
58
+ "@types/compression": "^1.8.1",
57
59
  "@types/node": "^20.0.0",
58
60
  "@types/picomatch": "^4.0.2",
59
61
  "@types/react": "^18.3.31",