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.
- package/dist/agent-prompts.d.ts +1 -0
- package/dist/agent-prompts.js +3 -0
- package/dist/agent.js +70 -24
- package/dist/atomicFile.d.ts +29 -0
- package/dist/atomicFile.js +120 -0
- package/dist/castle-host/host.js +108 -0
- package/dist/get-deck.js +2 -11
- package/dist/headlessCover.d.ts +4 -0
- package/dist/headlessCover.js +7 -5
- package/dist/ide.d.ts +10 -0
- package/dist/ide.js +19 -11
- package/dist/index.js +10 -6
- package/dist/init.d.ts +1 -1
- package/dist/init.js +11 -1
- package/dist/remix.js +2 -14
- package/dist/save-deck.d.ts +3 -0
- package/dist/save-deck.js +40 -73
- package/dist/saveCover.d.ts +18 -0
- package/dist/saveCover.js +61 -0
- package/dist/serve.js +18 -5
- package/dist/serveSecurity.d.ts +11 -1
- package/dist/serveSecurity.js +38 -4
- package/dist/shell/assets/index-6odVZQSZ.css +1 -0
- package/dist/shell/assets/index-Ws0WrCbi.js +445 -0
- package/dist/shell/index.html +3 -3
- package/dist/versionStore.js +13 -4
- package/kits/base/CLAUDE.md +9 -0
- package/kits/base/castle.json +16 -11
- package/kits/base/sdk/README.md +83 -2
- package/kits/base/sdk/commands.d.ts +37 -1
- package/kits/base/sdk/commands.js +9 -0
- package/kits/base/sdk/lifecycle.d.ts +20 -0
- package/kits/base/sdk/lifecycle.js +7 -1
- package/kits/base/sdk/resumeState.d.ts +2 -0
- package/kits/base/sdk/resumeState.js +84 -0
- package/kits/base/sdk/runtime.js +65 -0
- package/kits/base/sdk/transport.d.ts +3 -0
- package/kits/base/sdk/transport.js +39 -5
- package/kits/base/sdk/unloadHandshake.d.ts +6 -0
- package/kits/base/sdk/unloadHandshake.js +47 -0
- package/kits/base/sdk/user.d.ts +4 -0
- package/kits/base/sdk/user.js +36 -1
- package/kits/multiplayer-2d/CLAUDE.md +20 -9
- package/kits/multiplayer-2d/castle.json +4 -4
- package/kits/multiplayer-2d/code/server/players.js +27 -6
- package/kits/multiplayer-2d/code/server/world.js +24 -1
- package/kits/multiplayer-2d/code/systems/multiplayer.js +45 -4
- package/kits/multiplayer-2d/package-lock.json +2 -4
- package/kits/multiplayer-2d/package.json +1 -0
- package/kits/multiplayer-3d/CLAUDE.md +28 -9
- package/kits/multiplayer-3d/castle.json +5 -5
- package/kits/multiplayer-3d/code/server/players.js +54 -5
- package/kits/multiplayer-3d/code/server/world.js +45 -1
- package/kits/multiplayer-3d/code/systems/multiplayer.js +108 -6
- package/kits/multiplayer-3d/package-lock.json +2 -4
- package/kits/multiplayer-3d/package.json +1 -0
- package/kits/physics-2d/CLAUDE.md +75 -43
- package/kits/physics-2d/behaviors/AnalogStick.jsx +75 -45
- package/kits/physics-2d/behaviors/Slingshot.jsx +1 -2
- package/kits/physics-2d/castle.json +6 -6
- package/kits/physics-2d/editors/PxArtEditor.jsx +85 -182
- package/kits/physics-2d/editors/SceneEditor.jsx +25 -4
- package/kits/physics-2d/editors/StyleEditor.jsx +6 -2
- package/kits/physics-2d/editors/overlayCanvas.js +77 -0
- package/kits/physics-2d/editors/pathOverlay.js +5 -13
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +5 -1
- package/kits/physics-2d/editors/pixelInspector.jsx +4 -4
- package/kits/physics-2d/editors/pxArtTimeline.jsx +8 -36
- package/kits/physics-2d/editors/pxArtTimeline.module.css +4 -28
- package/kits/physics-2d/editors/styleEditor.module.css +23 -4
- package/kits/physics-2d/editors/useArtboardZoomPan.js +322 -0
- package/kits/physics-2d/engine/SceneUI.jsx +6 -12
- package/kits/physics-2d/engine/icons.js +56 -0
- package/kits/physics-2d/engine/panelSorting.jsx +7 -14
- package/kits/physics-2d/engine/physics/PhysicsSystem.js +66 -11
- package/kits/physics-2d/engine/physics/controls.js +76 -12
- package/kits/physics-2d/engine/scene.js +47 -9
- package/kits/physics-2d/engine/ui.jsx +13 -125
- package/kits/physics-2d/engine/ui.module.css +53 -28
- package/kits/physics-2d/package-lock.json +0 -29
- package/kits/physics-2d/package.json +3 -1
- package/kits/physics-2d/pnpm-lock.yaml +1 -17
- package/kits/physics-3d/behaviors/Pickup.jsx +5 -3
- package/kits/physics-3d/castle.json +5 -5
- package/kits/physics-3d/package-lock.json +0 -29
- package/kits/physics-3d/package.json +0 -1
- package/kits/physics-3d/pnpm-lock.yaml +0 -17
- package/kits/real-time/CLAUDE.md +23 -4
- package/kits/real-time/castle.json +1 -1
- package/kits/real-time/code/client/connection.js +9 -4
- package/kits/real-time/code/client/joinOverlay.js +46 -0
- package/kits/real-time/code/client/messages.js +4 -0
- package/kits/real-time/code/server/gameHooks.js +4 -0
- package/kits/real-time/code/server/persist.js +105 -0
- package/kits/real-time/code/server/session.js +289 -19
- package/kits/real-time/package-lock.json +1139 -0
- package/kits/turn-based/CLAUDE.md +76 -20
- package/kits/turn-based/castle.json +3 -3
- package/kits/turn-based/code/server/index.js +16 -5
- package/kits/turn-based/package-lock.json +0 -27
- package/kits/turn-based/package.json +2 -2
- package/kits/turn-based/room.js +162 -13
- package/kits/turn-based/testing.js +276 -92
- package/package.json +5 -5
- package/dist/shell/assets/index-CVHj503j.css +0 -1
- package/dist/shell/assets/index-x_QkP3Xq.js +0 -444
|
@@ -14,26 +14,26 @@
|
|
|
14
14
|
"imports": {
|
|
15
15
|
"castle.real-time": {
|
|
16
16
|
"deckId": "xFVr-afOLuUQ",
|
|
17
|
-
"version": "2026-09-
|
|
17
|
+
"version": "2026-09-10T19:46:34.652Z"
|
|
18
18
|
},
|
|
19
19
|
"castle.physics-3d": {
|
|
20
20
|
"deckId": "JH0SclbPVP0y",
|
|
21
|
-
"version": "2026-09-
|
|
21
|
+
"version": "2026-09-10T19:46:37.323Z"
|
|
22
22
|
},
|
|
23
23
|
"castle.physics-2d": {
|
|
24
24
|
"deckId": "ckRZGFW4iPrx",
|
|
25
|
-
"version": "2026-09-
|
|
25
|
+
"version": "2026-09-10T00:57:31.408Z",
|
|
26
26
|
"via": "castle.physics-3d"
|
|
27
27
|
},
|
|
28
28
|
"castle.base": {
|
|
29
29
|
"deckId": "yRcmH4_aYllE",
|
|
30
|
-
"version": "2026-09-
|
|
30
|
+
"version": "2026-09-10T00:56:59.388Z"
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"autoUpdateWhenImported": true,
|
|
34
34
|
"deckId": "ZwA_P_-VO-Qh",
|
|
35
35
|
"cardId": "bvRmCzyUpt_a",
|
|
36
|
-
"publishedVersion": "2026-09-
|
|
36
|
+
"publishedVersion": "2026-09-10T20:10:41.318Z",
|
|
37
37
|
"main": "main.jsx",
|
|
38
38
|
"server": {
|
|
39
39
|
"main": "code/server/index.js",
|
|
@@ -7,6 +7,9 @@ import { earned, number } from '@imports/castle.real-time/code/server/limits.js'
|
|
|
7
7
|
import * as Limits from './limits.js';
|
|
8
8
|
import { createBody } from './world.js';
|
|
9
9
|
|
|
10
|
+
// The largest state block a client may report, as JSON text.
|
|
11
|
+
const STATE_BYTES = 4096;
|
|
12
|
+
|
|
10
13
|
// An entry in `sim.players`:
|
|
11
14
|
//
|
|
12
15
|
// body the kinematic capsule in the rapier world
|
|
@@ -14,18 +17,26 @@ import { createBody } from './world.js';
|
|
|
14
17
|
// facing radians, as the client sent them
|
|
15
18
|
// clamped true only on the tick a move was refused
|
|
16
19
|
// movedAt simulation time of that acceptance
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
// settled whether this connection has ever reported a pose the server took
|
|
21
|
+
// state the block the client last reported: the engine's `scene.persistent`
|
|
22
|
+
// (score, camera angle, collected pickups). Saved with the pose and
|
|
23
|
+
// handed back on rejoin. The server never reads inside it.
|
|
24
|
+
function newPlayer(body, at, movedAt) {
|
|
25
|
+
return { body, at, facing: 0, clamped: false, movedAt, settled: false, state: null };
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
// Add a kinematic capsule for a session player. `world.step` places it at
|
|
22
29
|
// `player.at` before physics; zero friction prevents a capsule leaning against a
|
|
23
30
|
// wall from remaining attached to it.
|
|
24
|
-
|
|
31
|
+
//
|
|
32
|
+
// `pose` is where a returning player left off, as `[x, y, z, facing]`. Without
|
|
33
|
+
// one the capsule starts on the spawn ring.
|
|
34
|
+
export function addPlayer(sim, playerId, pose) {
|
|
25
35
|
// The shape comes off the scene's own player actor, so the server collides
|
|
26
36
|
// with the same body the browser draws. The default is a person-sized capsule.
|
|
27
37
|
const shape = sim.template?.shape ?? { kind: 'capsule', radius: 0.35, height: 1.4 };
|
|
28
|
-
const
|
|
38
|
+
const resumed = pose ? Limits.readPoint(pose) : null;
|
|
39
|
+
const at = resumed ?? spawnPoint(sim);
|
|
29
40
|
const body = createBody(
|
|
30
41
|
sim,
|
|
31
42
|
{ components: { Solid: {} } },
|
|
@@ -33,7 +44,15 @@ export function addPlayer(sim, playerId) {
|
|
|
33
44
|
shape,
|
|
34
45
|
{ type: 'kinematic', friction: 0 },
|
|
35
46
|
);
|
|
36
|
-
|
|
47
|
+
|
|
48
|
+
// The movement allowance runs from now. A body that starts where a player left
|
|
49
|
+
// off would otherwise let that player's first report cover half a second of
|
|
50
|
+
// travel back to wherever their own copy of the scene put them.
|
|
51
|
+
const player = newPlayer(body, at, sim.timeMs);
|
|
52
|
+
if (resumed) {
|
|
53
|
+
player.facing = number(pose[3]);
|
|
54
|
+
}
|
|
55
|
+
sim.players.set(playerId, player);
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
// Remove a departing player's Rapier body and simulation entry.
|
|
@@ -64,6 +83,9 @@ export function setPose(sim, playerId, message) {
|
|
|
64
83
|
// changes the rendered model orientation and has no effect on the upright
|
|
65
84
|
// capsule's movement or collision shape.
|
|
66
85
|
player.facing = number(message?.p?.[3]);
|
|
86
|
+
if (message?.s !== undefined) {
|
|
87
|
+
setState(sim, playerId, message.s);
|
|
88
|
+
}
|
|
67
89
|
|
|
68
90
|
// Combine elapsed-time movement allowance with fixed delivery slack.
|
|
69
91
|
const reach = Limits.MAX_SPEED * earned(sim, player.movedAt) + Limits.MOVE_SLACK;
|
|
@@ -77,6 +99,15 @@ export function setPose(sim, playerId, message) {
|
|
|
77
99
|
if (distance <= reach) {
|
|
78
100
|
player.at = asked;
|
|
79
101
|
player.clamped = false;
|
|
102
|
+
player.settled = true;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The first report moves nothing. A client starts its own body from the scene
|
|
107
|
+
// and does not know yet where the session put this player, so the server
|
|
108
|
+
// position stands until the client has agreed with it once.
|
|
109
|
+
if (!player.settled) {
|
|
110
|
+
player.clamped = true;
|
|
80
111
|
return;
|
|
81
112
|
}
|
|
82
113
|
|
|
@@ -92,6 +123,24 @@ export function setPose(sim, playerId, message) {
|
|
|
92
123
|
player.clamped = true;
|
|
93
124
|
}
|
|
94
125
|
|
|
126
|
+
// Keep a player's state block. Only a plain object under `STATE_BYTES` of JSON
|
|
127
|
+
// is kept; anything else leaves the block as it was.
|
|
128
|
+
export function setState(sim, playerId, state) {
|
|
129
|
+
const player = sim.players.get(playerId);
|
|
130
|
+
if (!player || typeof state !== 'object' || Array.isArray(state)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (state !== null && JSON.stringify(state).length > STATE_BYTES) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
player.state = state;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A player's state block, or null.
|
|
140
|
+
export function stateOf(sim, playerId) {
|
|
141
|
+
return sim.players.get(playerId)?.state ?? null;
|
|
142
|
+
}
|
|
143
|
+
|
|
95
144
|
// Pack current Rapier body poses for a WORLD or STATE message. Physics contacts
|
|
96
145
|
// can make the body position differ from the last accepted target in `player.at`.
|
|
97
146
|
export function playerPoses(sim) {
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import RAPIER from '@dimforge/rapier3d-compat';
|
|
6
6
|
import { packPose } from '../client/poses.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
MAX_STEPS_PER_TICK,
|
|
9
|
+
STEP,
|
|
10
|
+
clamp,
|
|
11
|
+
number,
|
|
12
|
+
} from '@imports/castle.real-time/code/server/limits.js';
|
|
8
13
|
import { readScene } from './sceneBodies.js';
|
|
9
14
|
import * as Limits from './limits.js';
|
|
10
15
|
|
|
@@ -13,6 +18,7 @@ const DEG = Math.PI / 180;
|
|
|
13
18
|
// The simulation every hook is handed as `sim`:
|
|
14
19
|
//
|
|
15
20
|
// world the rapier world
|
|
21
|
+
// bodies actor id -> rapier body, for every scene actor with a shape
|
|
16
22
|
// objects actor id -> { body, home, claimable, fast, owner, reportedAt,
|
|
17
23
|
// claimedAt }
|
|
18
24
|
// doors [{ body, props, baseY, openT }]
|
|
@@ -27,6 +33,7 @@ function emptySim() {
|
|
|
27
33
|
return {
|
|
28
34
|
world: new RAPIER.World({ x: 0, y: -9.81, z: 0 }),
|
|
29
35
|
objects: new Map(),
|
|
36
|
+
bodies: new Map(),
|
|
30
37
|
doors: [],
|
|
31
38
|
players: new Map(),
|
|
32
39
|
spawn: { x: 0, y: 1, z: 0 },
|
|
@@ -76,6 +83,7 @@ function buildScene(sim) {
|
|
|
76
83
|
continue;
|
|
77
84
|
}
|
|
78
85
|
const rb = createBody(sim, actor, t, shape, body);
|
|
86
|
+
sim.bodies.set(actor.id, rb);
|
|
79
87
|
if (door) {
|
|
80
88
|
sim.doors.push({ body: rb, props: door, baseY: t.y, openT: 0 });
|
|
81
89
|
}
|
|
@@ -102,6 +110,19 @@ function buildScene(sim) {
|
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
// Take a scene actor out of the simulation for good. An id with no body here
|
|
114
|
+
// (a gem, say, that the engine handles as a sensor) is a no-op.
|
|
115
|
+
export function removeActor(sim, id) {
|
|
116
|
+
const rb = sim.bodies.get(id);
|
|
117
|
+
if (!rb) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
sim.world.removeRigidBody(rb);
|
|
121
|
+
sim.bodies.delete(id);
|
|
122
|
+
sim.objects.delete(id);
|
|
123
|
+
sim.doors = sim.doors.filter((door) => door.body !== rb);
|
|
124
|
+
}
|
|
125
|
+
|
|
105
126
|
// Create one Rapier body and collider from engine component data. These mappings
|
|
106
127
|
// mirror the engine's `bodyDescFor` and `colliderDescFor`, so server and browser
|
|
107
128
|
// worlds use the same body type, material values, shape, and sensor state.
|
|
@@ -289,6 +310,29 @@ export function objectPoses(sim) {
|
|
|
289
310
|
return out;
|
|
290
311
|
}
|
|
291
312
|
|
|
313
|
+
// Put saved poses back on the bodies they were read from. An id the scene no
|
|
314
|
+
// longer has is skipped. Velocities are cleared: a restored body starts at rest.
|
|
315
|
+
// Returns how many poses were applied.
|
|
316
|
+
export function restorePoses(sim, poses) {
|
|
317
|
+
let applied = 0;
|
|
318
|
+
for (const pose of poses) {
|
|
319
|
+
const entry = sim.objects.get(pose?.[0]);
|
|
320
|
+
const at = entry ? Limits.readPoint(pose.slice(1)) : null;
|
|
321
|
+
if (!at) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
entry.body.setTranslation(at, true);
|
|
325
|
+
entry.body.setRotation(
|
|
326
|
+
{ x: number(pose[4]), y: number(pose[5]), z: number(pose[6]), w: number(pose[7]) },
|
|
327
|
+
true,
|
|
328
|
+
);
|
|
329
|
+
entry.body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
|
330
|
+
entry.body.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
|
331
|
+
applied += 1;
|
|
332
|
+
}
|
|
333
|
+
return applied;
|
|
334
|
+
}
|
|
335
|
+
|
|
292
336
|
// Convert the engine Transform's degree Euler angles to a Rapier quaternion.
|
|
293
337
|
function quatFromEuler(t) {
|
|
294
338
|
const cx = Math.cos((t.rotationX ?? 0) * DEG * 0.5);
|
|
@@ -10,7 +10,11 @@ import * as Smoothing from '@imports/castle.real-time/code/client/smooth.js';
|
|
|
10
10
|
import * as Poses from '../client/poses.js';
|
|
11
11
|
import { connectSession } from '@imports/castle.real-time/code/client/connection.js';
|
|
12
12
|
import { showSoloBadge, updateSoloBadge } from '@imports/castle.real-time/code/client/soloBadge.js';
|
|
13
|
-
import {
|
|
13
|
+
import { showJoinOverlay } from '@imports/castle.real-time/code/client/joinOverlay.js';
|
|
14
|
+
import { GONE, STATE, WORLD } from '@imports/castle.real-time/code/client/messages.js';
|
|
15
|
+
|
|
16
|
+
// How long the joining overlay may hold the card before giving up.
|
|
17
|
+
const JOIN_WAIT_MS = 10000;
|
|
14
18
|
import { game } from '@imports/castle.real-time/code/client/gameHooks.js';
|
|
15
19
|
|
|
16
20
|
// Apply this fraction of a server player correction per frame. Corrections over
|
|
@@ -46,6 +50,11 @@ class MultiplayerSystem {
|
|
|
46
50
|
constructor() {
|
|
47
51
|
// The Castle transport is created on the first stepped frame.
|
|
48
52
|
this.net = null;
|
|
53
|
+
// False until a server snapshot carries this player's pose. The overlay
|
|
54
|
+
// stays up from the first stepped frame until then, or until JOIN_WAIT_MS
|
|
55
|
+
// after the join began if the session never answers.
|
|
56
|
+
this.joined = false;
|
|
57
|
+
this.joinStartedAt = 0;
|
|
49
58
|
|
|
50
59
|
// `starting` prevents concurrent join attempts while the promise is pending.
|
|
51
60
|
this.starting = false;
|
|
@@ -68,6 +77,16 @@ class MultiplayerSystem {
|
|
|
68
77
|
|
|
69
78
|
// A clamp stores the last server position that rejected a local move.
|
|
70
79
|
this.clampedTo = null;
|
|
80
|
+
|
|
81
|
+
// Scene actors the session has removed for good, and the actor ids present
|
|
82
|
+
// at the end of the last frame. An id that was there and is not now was
|
|
83
|
+
// despawned by a behavior this frame, and is reported.
|
|
84
|
+
this.gone = new Set();
|
|
85
|
+
this.seen = null;
|
|
86
|
+
|
|
87
|
+
// The JSON of the last state block sent, so a block goes out only when it
|
|
88
|
+
// changes.
|
|
89
|
+
this.sentState = '';
|
|
71
90
|
}
|
|
72
91
|
|
|
73
92
|
// Run the network frame after engine behaviors and local physics. Connection,
|
|
@@ -76,12 +95,15 @@ class MultiplayerSystem {
|
|
|
76
95
|
// Connecting on the first stepped frame keeps editor-only runtimes out of
|
|
77
96
|
// multiplayer sessions because the scene editor constructs but never steps them.
|
|
78
97
|
if (!this.net) {
|
|
98
|
+
showJoinOverlay(this.joiningOverlayWanted());
|
|
79
99
|
return void this.start();
|
|
80
100
|
}
|
|
81
101
|
const local = scene.actorWith('Player');
|
|
82
102
|
|
|
83
103
|
// Drain all queued messages before choosing the render time for this frame.
|
|
84
104
|
this.receive(scene);
|
|
105
|
+
this.applyGone(scene);
|
|
106
|
+
this.reportGone(scene);
|
|
85
107
|
|
|
86
108
|
// Draw at a fixed delay behind the server clock so interpolation normally has
|
|
87
109
|
// samples on both sides. The local engine keeps stepping before the first
|
|
@@ -98,7 +120,9 @@ class MultiplayerSystem {
|
|
|
98
120
|
// Deck code can make a final local adjustment after engine Player behavior
|
|
99
121
|
// and physics, then optionally replace the kit's standard report.
|
|
100
122
|
game.beforeReport?.(scene, local, this);
|
|
101
|
-
if (
|
|
123
|
+
if (!this.joined) {
|
|
124
|
+
// Nothing reported until the session has placed this player.
|
|
125
|
+
} else if (game.report) {
|
|
102
126
|
game.report(scene, local, this);
|
|
103
127
|
} else {
|
|
104
128
|
this.report(scene, local);
|
|
@@ -117,6 +141,7 @@ class MultiplayerSystem {
|
|
|
117
141
|
}
|
|
118
142
|
this.publish(scene, local);
|
|
119
143
|
updateSoloBadge(this.net.status(), game);
|
|
144
|
+
showJoinOverlay(this.joiningOverlayWanted());
|
|
120
145
|
}
|
|
121
146
|
|
|
122
147
|
// Join the Castle public session once and retain the transport across scene loads.
|
|
@@ -125,6 +150,7 @@ class MultiplayerSystem {
|
|
|
125
150
|
return;
|
|
126
151
|
}
|
|
127
152
|
this.starting = true;
|
|
153
|
+
this.joinStartedAt ||= performance.now();
|
|
128
154
|
try {
|
|
129
155
|
this.net = await connectSession({ onLog: (text) => console.log(`[multiplayer] ${text}`) });
|
|
130
156
|
} catch (err) {
|
|
@@ -145,6 +171,21 @@ class MultiplayerSystem {
|
|
|
145
171
|
|
|
146
172
|
// Pair simulation time with the page arrival time before filing samples.
|
|
147
173
|
Clock.noteArrival(this.clock, msg.t, msg.localMs);
|
|
174
|
+
if (!this.joined && (msg.k === WORLD || (msg.ps ?? []).some((pose) => pose[0] === selfId))) {
|
|
175
|
+
this.joined = true;
|
|
176
|
+
console.log('[multiplayer] placed by the session');
|
|
177
|
+
}
|
|
178
|
+
for (const id of msg.gone ?? []) {
|
|
179
|
+
this.gone.add(id);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The WORLD snapshot carries this player's own saved block. It goes back
|
|
183
|
+
// on `scene.persistent`, which is where the engine's Player behavior keeps
|
|
184
|
+
// the camera angle and score, before that behavior runs again.
|
|
185
|
+
if (msg.k === WORLD && msg.me) {
|
|
186
|
+
Object.assign((scene.persistent ??= {}), msg.me);
|
|
187
|
+
this.sentState = JSON.stringify(scene.persistent);
|
|
188
|
+
}
|
|
148
189
|
|
|
149
190
|
// Apply ownership first so an object granted this tick stops receiving
|
|
150
191
|
// network poses on the same frame local simulation begins.
|
|
@@ -152,9 +193,11 @@ class MultiplayerSystem {
|
|
|
152
193
|
for (const [id, x, y, z, facing, clamped] of msg.ps ?? []) {
|
|
153
194
|
if (id !== selfId) {
|
|
154
195
|
sample(this.players, msg, id, [x, y, z, facing]);
|
|
155
|
-
} else if (clamped) {
|
|
156
|
-
// The local player's returned pose is used
|
|
157
|
-
//
|
|
196
|
+
} else if (clamped || msg.k === WORLD) {
|
|
197
|
+
// The local player's returned pose is used when the server marks it
|
|
198
|
+
// clamped, and on the WORLD snapshot that starts a session: that one
|
|
199
|
+
// carries where this player left off. Ordinary local movement remains
|
|
200
|
+
// client-simulated.
|
|
158
201
|
this.clampedTo = { x, y, z };
|
|
159
202
|
}
|
|
160
203
|
}
|
|
@@ -300,7 +343,13 @@ class MultiplayerSystem {
|
|
|
300
343
|
|
|
301
344
|
// `sendMove` only goes out at MOVE_HZ; claims are held until it does. The
|
|
302
345
|
// transport takes a packed pose, so the Transform is encoded here.
|
|
303
|
-
|
|
346
|
+
// The state block rides in a move packet when it changed since the last one
|
|
347
|
+
// that went out.
|
|
348
|
+
const stateJson = JSON.stringify(scene.persistent ?? {});
|
|
349
|
+
const state = stateJson === this.sentState ? undefined : (scene.persistent ?? {});
|
|
350
|
+
if (this.net.sendMove(Poses.packMove(me), owned, claims, releases, state) && state) {
|
|
351
|
+
this.sentState = stateJson;
|
|
352
|
+
}
|
|
304
353
|
Avatars.showSelf(this.avatars, scene, this.net.status().you, me);
|
|
305
354
|
}
|
|
306
355
|
|
|
@@ -388,6 +437,56 @@ class MultiplayerSystem {
|
|
|
388
437
|
this.own = Ownership.makeOwnership();
|
|
389
438
|
this.shown = Smoothing.makeSmoothing();
|
|
390
439
|
this.clampedTo = null;
|
|
440
|
+
this.seen = null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
//
|
|
444
|
+
// Actors removed for good
|
|
445
|
+
//
|
|
446
|
+
|
|
447
|
+
// Despawn every actor the session says is gone. Runs each frame, so a scene
|
|
448
|
+
// load that brings an actor back from the file loses it again at once.
|
|
449
|
+
applyGone(scene) {
|
|
450
|
+
for (const id of this.gone) {
|
|
451
|
+
if (scene.actors.has(id)) {
|
|
452
|
+
scene.despawnActor(id);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Report actors a behavior despawned this frame (a collected pickup, say).
|
|
458
|
+
// Other players' avatars are the kit's own and are not reported. Nothing is
|
|
459
|
+
// reported before the session has placed this player.
|
|
460
|
+
reportGone(scene) {
|
|
461
|
+
const ids = new Set(scene.actors.keys());
|
|
462
|
+
if (this.seen && this.joined) {
|
|
463
|
+
const avatars = new Set(this.avatars.actors.values());
|
|
464
|
+
for (const id of this.seen) {
|
|
465
|
+
if (ids.has(id) || this.gone.has(id) || avatars.has(id)) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
this.gone.add(id);
|
|
469
|
+
this.net.send({ k: GONE, id });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
this.seen = ids;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Whether the overlay should be up: joining or connected, not solo, and not
|
|
476
|
+
// yet placed. A session that never answers releases it after JOIN_WAIT_MS so
|
|
477
|
+
// the deck is not stuck behind it.
|
|
478
|
+
joiningOverlayWanted() {
|
|
479
|
+
if (this.joined || this.net?.status().solo) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
if (this.joinStartedAt && performance.now() - this.joinStartedAt > JOIN_WAIT_MS) {
|
|
483
|
+
if (!this.joinWarned) {
|
|
484
|
+
this.joinWarned = true;
|
|
485
|
+
console.warn('[multiplayer] no snapshot from the session yet; playing unplaced');
|
|
486
|
+
}
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
return true;
|
|
391
490
|
}
|
|
392
491
|
|
|
393
492
|
// Release scene resources and leave the session when the engine runtime ends.
|
|
@@ -396,6 +495,9 @@ class MultiplayerSystem {
|
|
|
396
495
|
this.reset(scene);
|
|
397
496
|
this.net?.close();
|
|
398
497
|
this.net = null;
|
|
498
|
+
this.joined = false;
|
|
499
|
+
this.joinStartedAt = 0;
|
|
500
|
+
showJoinOverlay(false);
|
|
399
501
|
// The badge is a fixed element outside the scene, so a disposed runtime
|
|
400
502
|
// (an editor preview closing, say) has to take it down explicitly.
|
|
401
503
|
showSoloBadge(null);
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
|
20
20
|
"@lezer/highlight": "^1.2.3",
|
|
21
21
|
"castle-web-fonts": "^1.0.0",
|
|
22
|
+
"castle-web-sdk": "file:../../sdk",
|
|
22
23
|
"codemirror": "^6.0.2",
|
|
23
24
|
"matter-js": "^0.20.0",
|
|
24
25
|
"react": "^19.2.4",
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
},
|
|
32
33
|
"../../sdk": {
|
|
33
34
|
"name": "castle-web-sdk",
|
|
34
|
-
"version": "0.4.
|
|
35
|
+
"version": "0.4.27",
|
|
35
36
|
"dev": true,
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"eslint": "^9.0.0",
|
|
@@ -159,7 +160,6 @@
|
|
|
159
160
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
|
160
161
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
|
161
162
|
"license": "MIT",
|
|
162
|
-
"peer": true,
|
|
163
163
|
"dependencies": {
|
|
164
164
|
"@dnd-kit/accessibility": "^3.1.1",
|
|
165
165
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -316,7 +316,6 @@
|
|
|
316
316
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
|
317
317
|
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
|
318
318
|
"license": "MIT",
|
|
319
|
-
"peer": true,
|
|
320
319
|
"engines": {
|
|
321
320
|
"node": ">=0.10.0"
|
|
322
321
|
}
|
|
@@ -326,7 +325,6 @@
|
|
|
326
325
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
|
327
326
|
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
|
328
327
|
"license": "MIT",
|
|
329
|
-
"peer": true,
|
|
330
328
|
"dependencies": {
|
|
331
329
|
"scheduler": "^0.27.0"
|
|
332
330
|
},
|