castle-web-cli 0.4.172 → 0.4.174

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 (67) hide show
  1. package/dist/agent-prompts.d.ts +8 -2
  2. package/dist/agent-prompts.js +19 -11
  3. package/dist/agent.js +37 -13
  4. package/dist/assetCompression.d.ts +3 -0
  5. package/dist/assetCompression.js +30 -0
  6. package/dist/castleJson.d.ts +1 -1
  7. package/dist/castleJson.js +2 -2
  8. package/dist/imports.js +2 -2
  9. package/dist/init.js +4 -4
  10. package/dist/native/loop.js +9 -0
  11. package/dist/native/openrouter.d.ts +2 -0
  12. package/dist/native/openrouter.js +10 -1
  13. package/dist/native/types.d.ts +1 -0
  14. package/dist/serve.js +2 -0
  15. package/dist/serveSecurity.d.ts +2 -0
  16. package/dist/serveSecurity.js +17 -3
  17. package/dist/shell/assets/{index-Ws0WrCbi.js → index-DF1yMXPS.js} +4 -4
  18. package/dist/shell/index.html +1 -1
  19. package/kits/base/castle.json +1 -1
  20. package/kits/multiplayer-2d/castle.json +2 -2
  21. package/kits/multiplayer-2d/code/server/sceneBodies.js +1 -1
  22. package/kits/multiplayer-2d/code/server/world.js +2 -2
  23. package/kits/multiplayer-2d/code/systems/multiplayer.js +12 -0
  24. package/kits/multiplayer-2d/fonts.generated.js +1 -1
  25. package/kits/multiplayer-2d/package-lock.json +1 -1
  26. package/kits/multiplayer-3d/CLAUDE.md +5 -4
  27. package/kits/multiplayer-3d/castle.json +3 -3
  28. package/kits/multiplayer-3d/code/client/avatars.js +47 -18
  29. package/kits/multiplayer-3d/code/client/nameTags.js +16 -35
  30. package/kits/multiplayer-3d/code/server/sceneBodies.js +1 -1
  31. package/kits/multiplayer-3d/code/systems/multiplayer.js +14 -1
  32. package/kits/multiplayer-3d/fonts.generated.js +1 -1
  33. package/kits/multiplayer-3d/package-lock.json +1 -1
  34. package/kits/physics-2d/CLAUDE.md +7 -6
  35. package/kits/physics-2d/behaviors/Collider.jsx +7 -1
  36. package/kits/physics-2d/behaviors/RigidBody.jsx +6 -3
  37. package/kits/physics-2d/castle.json +2 -2
  38. package/kits/physics-2d/editors/SceneEditor.jsx +1 -0
  39. package/kits/physics-2d/editors/SingleEditor.jsx +4 -4
  40. package/kits/physics-2d/editors/{StyleEditor.jsx → ThemeEditor.jsx} +8 -8
  41. package/kits/physics-2d/editors/deckFont.js +2 -2
  42. package/kits/physics-2d/editors/{styleEditor.module.css → themeEditor.module.css} +2 -2
  43. package/kits/physics-2d/engine/avatarArt.js +108 -14
  44. package/kits/physics-2d/engine/files.js +2 -2
  45. package/kits/physics-2d/engine/physics/matterBridge.js +2 -2
  46. package/kits/physics-2d/engine/scene.js +1 -1
  47. package/kits/physics-2d/fonts.generated.js +1 -1
  48. package/kits/physics-2d/package-lock.json +1 -1
  49. package/kits/physics-3d/behaviors/Body.jsx +10 -1
  50. package/kits/physics-3d/behaviors/Door.jsx +5 -0
  51. package/kits/physics-3d/behaviors/Lookable.jsx +3 -0
  52. package/kits/physics-3d/behaviors/Model.jsx +4 -0
  53. package/kits/physics-3d/behaviors/Pickup.jsx +4 -0
  54. package/kits/physics-3d/behaviors/Player.jsx +7 -0
  55. package/kits/physics-3d/behaviors/Shape.jsx +13 -0
  56. package/kits/physics-3d/castle.json +1 -1
  57. package/kits/physics-3d/engine3d/editor3dInspector.jsx +1 -0
  58. package/kits/physics-3d/fonts.generated.js +1 -1
  59. package/kits/physics-3d/package-lock.json +1 -1
  60. package/kits/real-time/castle.json +1 -1
  61. package/kits/real-time/code/client/connection.js +11 -2
  62. package/kits/real-time/package-lock.json +1 -1
  63. package/kits/turn-based/castle.json +1 -1
  64. package/kits/turn-based/fonts.generated.js +1 -1
  65. package/kits/turn-based/package-lock.json +1 -1
  66. package/package.json +3 -1
  67. /package/kits/physics-2d/editors/{styleTheme.js → theme.js} +0 -0
@@ -1,10 +1,12 @@
1
- // Other-player actors and their floating name tags. This module creates actors
2
- // from deck blueprints, updates their network poses, and removes their scene
3
- // resources when players leave.
1
+ // Other-player actors, their floating name tags, and the avatar on their head.
2
+ // This module creates actors from deck blueprints, updates their network poses,
3
+ // and removes their scene resources when players leave.
4
4
 
5
5
  import * as NameTags from './nameTags.js';
6
6
  import { applyPose } from './poses.js';
7
- import { avatarCanvas } from '@imports/castle.physics-2d/engine/avatarArt.js';
7
+ import { artReady } from '@imports/castle.physics-2d/engine/art.js';
8
+ import { avatarFacePath } from '@imports/castle.physics-2d/engine/avatarArt.js';
9
+ import { makeShapeMesh } from '@imports/castle.physics-3d/engine3d/materials.js';
8
10
 
9
11
  // The blueprint spawned when a caller names none. A deck's own file wins; a deck
10
12
  // scaffolded from this kit has no blueprints yet, so the kit's copy stands in
@@ -35,7 +37,7 @@ export function makeAvatars() {
35
37
  // A `username` of null draws no label. Options are `blueprint`, the scene to
36
38
  // spawn from (default `blueprints/other-player.scene`, the deck's own or the kit's),
37
39
  // `tagHeight`, how far over the body the label floats, and `userId`, whose
38
- // avatar the label carries.
40
+ // avatar goes on the model's head.
39
41
  export function showPlayer(avatars, scene, playerId, username, pose, options = {}) {
40
42
  // The actor keeps the blueprint used at creation; `ensureActor` only spawns
41
43
  // when this player has no live actor in the current scene.
@@ -48,20 +50,22 @@ export function showPlayer(avatars, scene, playerId, username, pose, options = {
48
50
  if (username === null) {
49
51
  dropTag(avatars, scene, playerId);
50
52
  } else {
51
- showTag(avatars, scene, playerId, username, transform, options);
53
+ showTag(avatars, scene, playerId, username, transform, options.tagHeight);
52
54
  }
55
+ dressHead(scene, actor.id, options.userId);
53
56
  return actor;
54
57
  }
55
58
 
56
- // Update the local player's label over the engine's local Player actor. The SDK
57
- // roster can arrive several frames after connection, so an empty username
58
- // leaves the label absent until the roster supplies one. Options are
59
+ // Update the local player's label and head over the engine's local Player
60
+ // actor. The SDK roster can arrive several frames after connection, so an empty
61
+ // username leaves the label absent until the roster supplies one. Options are
59
62
  // `tagHeight` and `userId`, as for `showPlayer`.
60
- export function showSelf(avatars, scene, username, transform, options = {}) {
63
+ export function showSelf(avatars, scene, local, username, transform, options = {}) {
64
+ dressHead(scene, local?.id, options.userId);
61
65
  if (!username) {
62
66
  return;
63
67
  }
64
- showTag(avatars, scene, 'self', username, transform, options);
68
+ showTag(avatars, scene, 'self', username, transform, options.tagHeight);
65
69
  }
66
70
 
67
71
  // Despawn the actor and dispose the tag of every player absent from `present`.
@@ -109,21 +113,20 @@ function ensureActor(avatars, scene, playerId, pose, blueprint) {
109
113
  return spawned;
110
114
  }
111
115
 
112
- // Create a tag when the displayed text or the avatar changes, then place it over
113
- // the actor. Each redraw allocates a canvas, texture, and sprite, so an unchanged
114
- // tag reuses the existing resources.
115
- function showTag(avatars, scene, key, username, transform, { tagHeight, userId } = {}) {
116
+ // Create a tag when the displayed text changes, then place it over the actor.
117
+ // Each redraw allocates a canvas, texture, and sprite, so unchanged text reuses
118
+ // the existing resources.
119
+ function showTag(avatars, scene, key, username, transform, tagHeight) {
116
120
  const label = NameTags.shortName(username);
117
- const avatar = avatarCanvas(userId);
118
121
  let tag = avatars.tags.get(key);
119
122
 
120
123
  // Compare against the shortened text stored on the tag. An
121
124
  // `anonymous-user-<uuid>` differs from the `anon-xxxxxx` string on the sprite.
122
- if (!tag || tag.text !== label || tag.avatar !== avatar) {
125
+ if (!tag || tag.text !== label) {
123
126
  if (tag) {
124
127
  NameTags.disposeNameTag(scene.three, tag);
125
128
  }
126
- tag = NameTags.makeNameTag(scene.three, label, avatar);
129
+ tag = NameTags.makeNameTag(scene.three, label);
127
130
  avatars.tags.set(key, tag);
128
131
  }
129
132
 
@@ -131,6 +134,32 @@ function showTag(avatars, scene, key, username, transform, { tagHeight, userId }
131
134
  NameTags.moveNameTag(tag, transform, scene.three?.camera, tagHeight);
132
135
  }
133
136
 
137
+ // Put the player's photo, square and edge to edge, on the four side faces of
138
+ // their model's head. The part is the one named `head`, the same convention
139
+ // `pxmodel.js` animates by; a model without one shows no avatar. The materials
140
+ // are rebuilt once per picture, when its art has loaded.
141
+ function dressHead(scene, actorId, userId) {
142
+ const path = avatarFacePath(scene, userId);
143
+ const head = scene.three?.nodes?.get(actorId)?.node.userData.modelParts?.get('head');
144
+ const mesh = head?.children.find((child) => child.userData.partMesh === 'head');
145
+ if (!path || !mesh || mesh.userData.avatarPath === path || !artReady(scene.sprites[path])) {
146
+ return;
147
+ }
148
+
149
+ // White under the picture, so the head's skin color does not tint it.
150
+ const { width, height, depth } = mesh.geometry.parameters;
151
+ const dressed = makeShapeMesh(
152
+ { kind: 'box', width, height, depth, color: '#ffffff', art: { sides: path, front: path } },
153
+ scene.sprites,
154
+ );
155
+ for (const material of [].concat(mesh.material)) {
156
+ material.dispose();
157
+ }
158
+ mesh.material = dressed.material;
159
+ dressed.geometry.dispose();
160
+ mesh.userData.avatarPath = path;
161
+ }
162
+
134
163
  function dropTag(avatars, scene, key) {
135
164
  const tag = avatars.tags.get(key);
136
165
  if (tag) {
@@ -1,8 +1,6 @@
1
- // Floating name tags for other players and the local player: the player's round
2
- // avatar over their username. This module draws them to a canvas, attaches the
3
- // resulting sprite directly to the engine's three.js scene, scales it by camera
4
- // distance, and disposes its GPU resources. A sprite always faces the camera,
5
- // so the avatar reads from every side.
1
+ // Floating username-label sprites for other players and the local player. This module
2
+ // draws text to a canvas, attaches the resulting sprite directly to the engine's
3
+ // three.js scene, scales it by camera distance, and disposes its GPU resources.
6
4
 
7
5
  import * as THREE from 'three';
8
6
 
@@ -10,11 +8,6 @@ import * as THREE from 'three';
10
8
  const HEIGHT_ABOVE = 1.25;
11
9
  const PIXELS_PER_UNIT = 200;
12
10
 
13
- // Canvas pixels of the name pill, and of the avatar drawn above it.
14
- const LABEL_PX = 68;
15
- const AVATAR_PX = 112;
16
- const GAP_PX = 8;
17
-
18
11
  // The distance at which the label is the size it was drawn to be: 68 canvas
19
12
  // pixels over 200 per unit is 0.34m, which on a 500x700 card at fov 55 is 29px.
20
13
  // This reference distance matches the common camera placement in kit decks.
@@ -23,12 +16,11 @@ const SAME_SIZE_M = 8;
23
16
  // The minimum scale keeps a label readable when the camera is near the player.
24
17
  const MIN_SCALE = 0.35;
25
18
 
26
- // Create `{ sprite, text, avatar, width, height, lift }` and add the sprite to
27
- // the engine's three.js scene. `avatar` is the canvas from `avatarArt.js`, or
28
- // null for a name alone. The engine actor-to-mesh sync does not update these
29
- // directly attached sprites, so callers move them each frame with `moveNameTag`.
30
- export function makeNameTag(world, text, avatar = null) {
31
- const canvas = drawLabel(text, avatar);
19
+ // Create `{ sprite, text, width, height }` and add the sprite to the engine's
20
+ // three.js scene. The engine actor-to-mesh sync does not update these directly
21
+ // attached sprites, so callers move them each frame with `moveNameTag`.
22
+ export function makeNameTag(world, text) {
23
+ const canvas = drawLabel(text);
32
24
  const texture = new THREE.CanvasTexture(canvas);
33
25
  texture.colorSpace = THREE.SRGBColorSpace;
34
26
  const sprite = new THREE.Sprite(
@@ -49,11 +41,8 @@ export function makeNameTag(world, text, avatar = null) {
49
41
  world.scene.add(sprite);
50
42
 
51
43
  // `text` is the string actually drawn, not the username it came from. The
52
- // caller compares against it, and against `avatar`, to decide whether to
53
- // redraw. `lift` raises the sprite's centre so the name pill stays at
54
- // `heightAbove` when an avatar is stacked over it.
55
- const lift = (height - LABEL_PX / PIXELS_PER_UNIT) / 2;
56
- return { sprite, text, avatar, width, height, lift };
44
+ // caller compares against it to decide whether a name changed.
45
+ return { sprite, text, width, height };
57
46
  }
58
47
 
59
48
  // Move and scale a tag after its player pose is drawn for the current frame.
@@ -72,7 +61,6 @@ export function moveNameTag(tag, transform, camera, heightAbove = HEIGHT_ABOVE)
72
61
  // 0.8m 286px -> 100, 2m 114px -> 40, 4m 57px -> 29, 8m 29px -> 29, 20m 11 -> 11.
73
62
  const scale = Math.max(MIN_SCALE, Math.min(1, away / SAME_SIZE_M));
74
63
  tag.sprite.scale.set(tag.width * scale, tag.height * scale, 1);
75
- tag.sprite.position.y += tag.lift * scale;
76
64
  }
77
65
 
78
66
  // Remove a tag from the three.js scene and release its texture and material.
@@ -90,31 +78,24 @@ export function shortName(username) {
90
78
  return anon ? `anon-${anon[1].replace(/-/g, '').slice(-6)}` : (username ?? 'player');
91
79
  }
92
80
 
93
- // Rasterize one display name, with the avatar centred above it, into the canvas
94
- // used as the sprite texture.
95
- function drawLabel(text, avatar) {
81
+ // Rasterize one display name into the canvas used as the sprite texture.
82
+ function drawLabel(text) {
96
83
  const canvas = document.createElement('canvas');
97
84
  const ctx = canvas.getContext('2d');
98
85
  const font = '600 44px Inter, system-ui, sans-serif';
99
86
  ctx.font = font;
100
- const pillWidth = Math.ceil(ctx.measureText(text).width) + 40;
101
- const width = avatar ? Math.max(pillWidth, AVATAR_PX) : pillWidth;
102
- const top = avatar ? AVATAR_PX + GAP_PX : 0;
87
+ const width = Math.ceil(ctx.measureText(text).width) + 40;
103
88
 
104
89
  // Canvas resizing clears the context state, so measure before resizing and
105
90
  // restore the font before drawing.
106
91
  canvas.width = width;
107
- canvas.height = top + LABEL_PX;
92
+ canvas.height = 68;
108
93
  ctx.font = font;
109
- if (avatar) {
110
- ctx.drawImage(avatar, (width - AVATAR_PX) / 2, 0, AVATAR_PX, AVATAR_PX);
111
- }
112
- const left = (width - pillWidth) / 2;
113
94
  ctx.fillStyle = 'rgba(12,12,16,0.72)';
114
- ctx.roundRect(left, top, pillWidth, LABEL_PX, 16);
95
+ ctx.roundRect(0, 0, width, 68, 16);
115
96
  ctx.fill();
116
97
  ctx.fillStyle = '#f2f2f5';
117
98
  ctx.textBaseline = 'middle';
118
- ctx.fillText(text, left + 20, top + LABEL_PX / 2);
99
+ ctx.fillText(text, 20, 36);
119
100
  return canvas;
120
101
  }
@@ -31,7 +31,7 @@ const blueprintTexts = import.meta.glob(['/blueprints/*.scene', '/imports/*/blue
31
31
  const DEFAULTS = {
32
32
  Transform: { x: 0, y: 0, z: 0, rotationX: 0, rotationY: 0, rotationZ: 0 },
33
33
  Shape: { kind: 'box', width: 1, height: 1, depth: 1, radius: 0.5, solid: false },
34
- Body: { type: 'fixed', friction: 0.6, restitution: 0, gravityScale: 1, lockRotations: false },
34
+ Body: { type: 'fixed', friction: 0.6, restitution: 0, gravityScale: 0, lockRotations: false },
35
35
  Door: { range: 3, slide: 2.1, speed: 2.5 },
36
36
 
37
37
  // `Ball` marks a fast-moving actor. `world.js` enables continuous collision
@@ -98,6 +98,19 @@ class MultiplayerSystem {
98
98
  showJoinOverlay(this.joiningOverlayWanted());
99
99
  return void this.start();
100
100
  }
101
+
102
+ // The session ended under this connection (the server was replaced, or the
103
+ // platform closed it). Everything drawn from it goes, and the next frame
104
+ // joins afresh behind the overlay.
105
+ if (this.net.closed()) {
106
+ this.reset(scene);
107
+ this.net = null;
108
+ this.joined = false;
109
+ this.joinStartedAt = 0;
110
+ this.starting = false;
111
+ this.sentState = '';
112
+ return;
113
+ }
101
114
  const local = scene.actorWith('Player');
102
115
 
103
116
  // Drain all queued messages before choosing the render time for this frame.
@@ -351,7 +364,7 @@ class MultiplayerSystem {
351
364
  if (this.net.sendMove(Poses.packMove(me), owned, claims, releases, state) && state) {
352
365
  this.sentState = stateJson;
353
366
  }
354
- Avatars.showSelf(this.avatars, scene, this.net.status().you, me, {
367
+ Avatars.showSelf(this.avatars, scene, local, this.net.status().you, me, {
355
368
  userId: this.net.userId(this.net.selfId()),
356
369
  });
357
370
  }
@@ -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
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "../../sdk": {
34
34
  "name": "castle-web-sdk",
35
- "version": "0.4.27",
35
+ "version": "0.4.28",
36
36
  "dev": true,
37
37
  "devDependencies": {
38
38
  "eslint": "^9.0.0",
@@ -22,12 +22,12 @@ Do you already know what you want to make, or do you want to figure it out toget
22
22
  - Every actor is an instance of a **blueprint** (`blueprints/*.scene`) — see `## Blueprints` below. Always author the blueprint file yourself and reference it via `"blueprint"`. Don't write inline actors (full `components`, no `blueprint` field): the editor auto-migrates each one into its own new blueprint file on open, one per actor with no dedup, which litters the deck with junk blueprints.
23
23
  - Real game objects and scenery should usually be editable sprites: generate `.sprite` with `npm run draw -- name`, then place it via a `Sprite` component pointing at `drawings/name.sprite`. Dynamic UI/effects stay procedural.
24
24
  - A deck can also hold uploaded media (the Files panel's Upload button): `Sprite` draws image files as well as `.sprite`, and `Video` plays video files. Point their `file` at the deck path (`assets/logo.png`). For audio see `## Sound` below -- sound effects need no component at all.
25
- - The deck's body/UI font is picked in the Style editor (`theme.style`): canvas text reads `scene.font`. A second display face is imported in the behavior that uses it. See `## Deck font` below.
25
+ - The deck's body/UI font is picked in the Theme editor (`theme.style`): canvas text reads `scene.font`. A second display face is imported in the behavior that uses it. See `## Deck font` below.
26
26
  - This kit is plain JavaScript. Use `.jsx` for files with JSX, `.js` otherwise; do not add TypeScript files or a new build step.
27
27
  - Space is reserved by the editor for play/stop; do not bind Space to gameplay.
28
28
  - Two render hooks: `draw(ctx)` is world space and scrolls with `Camera`; `drawUi(ctx)` is screen space (card units, camera-free). On-screen controls, gauges and HUD go in `drawUi`, never `draw`. The React `ui()` hook is deprecated — prefer `drawUi`.
29
29
  - Do not read `engine/`, `editors/`, or built-in behaviors (`Layout.jsx`, `Sprite.jsx`, `Collider.jsx`, `Camera.jsx`, and the physics ones — `RigidBody.jsx`, `Joints.jsx`, `Draggable.jsx`, `Slingshot.jsx`, `AnalogStick.jsx`) to build a game. Their public API is documented below. Forking is the exception: when you're deliberately copying a kit file into the deck to change it — an editor (see `## Editors`) or a behavior you're shadowing — read the original. `engine/` stays off-limits either way.
30
- - An editor is for content FILES the creator keeps coming back to (catalogs, dialogue, level tables) that nothing already opens. Scenes, sprites and styles have editors; per-actor settings belong on behavior props in the inspector — not in a new editor.
30
+ - An editor is for content FILES the creator keeps coming back to (catalogs, dialogue, level tables) that nothing already opens. Scenes, sprites and themes have editors; per-actor settings belong on behavior props in the inspector — not in a new editor.
31
31
  - Details below: `## Behavior shape`, `## Scene file`, `## Blueprints`, `## Built-in behaviors`, `## Creating pixel art`, `## SceneRuntime API`, and `## Input shortcuts`.
32
32
 
33
33
  ## Editors
@@ -440,7 +440,7 @@ Name a sound by its filename without the extension, or by its full deck path. Tw
440
440
 
441
441
  ## Deck font
442
442
 
443
- A physics-2d deck picks its **deck font** in the Style editor — open
443
+ A physics-2d deck picks its **deck font** in the Theme editor — open
444
444
  `theme.style` and choose one of the nine Castle faces, or None. That is the
445
445
  deck's body and UI face: `scene.font` returns it for `draw` / `drawUi`.
446
446
 
@@ -525,7 +525,8 @@ make it move.**
525
525
  - `bodyType: 'dynamic' | 'static' | 'kinematic'`. `dynamic` = simulated
526
526
  (gravity, collisions, forces). `static` = never moves (same as a lone
527
527
  Collider). `kinematic` = unpushable, but you move it by writing its Layout.
528
- - `gravityScale` — multiplies world gravity for this body (`0` = floats).
528
+ - `gravityScale` — multiplies world gravity for this body (`0` = floats;
529
+ default, so newly added bodies don't fall until you opt in).
529
530
  - `drag` — linear damping (air resistance). `angularDrag` — spin damping.
530
531
  - `freezeRotation` — keep it from rotating (stays upright).
531
532
  - `velocityX` / `velocityY` — initial velocity, applied once when play starts.
@@ -649,7 +650,7 @@ see the SceneRuntime API below.
649
650
  ### Recipe: a launch-and-bounce game
650
651
 
651
652
  - Ball blueprint: `Layout` (square), `Sprite`, `Collider {shape:'circle',
652
- bounciness:0.6}`, `RigidBody {bodyType:'dynamic'}`, `Slingshot {}`.
653
+ bounciness:0.6}`, `RigidBody {bodyType:'dynamic', gravityScale:1}`, `Slingshot {}`.
653
654
  - Walls/floor blueprint: `Layout`, `Collider {bounciness:0.4}` (no RigidBody →
654
655
  static).
655
656
  - Goal blueprint: `Collider {isTrigger:true}` + a `Goal` behavior with
@@ -835,7 +836,7 @@ To generate path art, emit an svg of **shapes** rather than per-pixel rects and
835
836
  - `scene.spawnFromBlueprint('blueprints/enemy.scene', { components: { Layout: { x, y } } })` — spawn an instance of a blueprint, same merge semantics as a placed instance. Prefer this over `spawnActor` when you're spawning copies of something that has (or should have) a blueprint, e.g. `scene.spawnFromBlueprint(actor.blueprint, { components: { Layout: { x: actor.components.Layout.x, y: actor.components.Layout.y } } })` to spawn another of the same kind as an existing actor.
836
837
  - `scene.despawnActor(id)` — remove an actor at runtime. Use this; don't `splice` + `delete` by hand.
837
838
  - `scene.sound` — the deck's sound library: `play(name, opts)`, `stopAll()`, `isLoaded`, `whenLoaded`, `has(name)`, `voiceCount`. See `## Sound`.
838
- - `scene.font` — the deck's font as a CSS family, for `ctx.font` in `draw` / `drawUi`. Always usable: the face picked in the Style editor, or a sans stack. See `## Deck font`.
839
+ - `scene.font` — the deck's font as a CSS family, for `ctx.font` in `draw` / `drawUi`. Always usable: the face picked in the Theme editor, or a sans stack. See `## Deck font`.
839
840
  - `scene.status` — string you can set/read for game-state ('playing', 'gameover', ...).
840
841
  - `scene.load(sceneData)` — replace the running scene with the given scene data object.
841
842
  - `scene.readFromFile(name)` — read and parse a scene file (`'gameover.scene'`, `'levels/2.scene'`), returning its scene data. Use this instead of importing a `.scene` file.
@@ -304,7 +304,13 @@ export class Collider {
304
304
  </div>
305
305
 
306
306
  {/* Material (collider-wide) */}
307
- <NumberField label="Density" value={density} onChange={(v) => setComponent({ density: v })} />
307
+ <NumberField
308
+ label="Density"
309
+ value={density}
310
+ min={0}
311
+ step={0.001}
312
+ onChange={(v) => setComponent({ density: v })}
313
+ />
308
314
  <AutoFields
309
315
  defaultProps={Collider.defaultProps}
310
316
  meta={Collider.propertyMeta}
@@ -24,7 +24,9 @@ export class RigidBody {
24
24
 
25
25
  static defaultProps = {
26
26
  bodyType: 'dynamic',
27
- gravityScale: 1,
27
+ // New bodies float until the author opts into gravity — falling immediately
28
+ // on add is the common surprise when placing a dynamic body to push around.
29
+ gravityScale: 0,
28
30
  drag: 0.01,
29
31
  angularDrag: 0,
30
32
  freezeRotation: false,
@@ -44,10 +46,11 @@ export class RigidBody {
44
46
  // `drag`/`angularDrag` are matter's air friction, where values above 1 flip
45
47
  // the velocity sign and destabilize the body -- so 1 is a real ceiling, not a
46
48
  // convenience. `gravityScale` deliberately has none: negative is legitimate
47
- // (it inverts gravity), as is scaling well past 1.
49
+ // (it inverts gravity), as is scaling well past 1. Drag steps at 0.005 so the
50
+ // scrubber lands on the default (0.01) and the finer authored values kits use.
48
51
  static propertyMeta = {
49
52
  gravityScale: { step: 0.1 },
50
- drag: DAMPING,
53
+ drag: { ...DAMPING, step: 0.005 },
51
54
  angularDrag: DAMPING,
52
55
  angularVelocity: { step: 0.1 },
53
56
  };
@@ -59,7 +59,7 @@
59
59
  },
60
60
  {
61
61
  "ext": ".style",
62
- "label": "Style",
62
+ "label": "Theme",
63
63
  "icon": "palette",
64
64
  "editor": "kit",
65
65
  "data": true
@@ -72,7 +72,7 @@
72
72
  "main": "main.jsx",
73
73
  "autoUpdateWhenImported": true,
74
74
  "title": "physics-2d",
75
- "publishedVersion": "2026-09-10T20:28:41.310Z",
75
+ "publishedVersion": "2026-09-10T22:05:01.629Z",
76
76
  "imports": {
77
77
  "castle.base": {
78
78
  "deckId": "yRcmH4_aYllE",
@@ -2303,6 +2303,7 @@ function BehaviorRow({
2303
2303
  component={component}
2304
2304
  setComponent={setComponent}
2305
2305
  override={override}
2306
+ meta={Behavior?.propertyMeta}
2306
2307
  />
2307
2308
  );
2308
2309
  // Layout is core to every actor -- it cannot be removed. A component an
@@ -1,7 +1,7 @@
1
1
  // Single-editor mount for the panel system. The shell (dockview) opens this as
2
2
  // its own iframe via the deck route in main.jsx:
3
3
  // ?file=<path>[&editor=<id>] -> <SingleEditor> (one editor for one file)
4
- // The shell only routes the kit's RICH file types here (scene / pxart / style);
4
+ // The shell only routes the kit's RICH file types here (scene / pxart / theme);
5
5
  // file browsing and the code/text editor are now builtin shell panels.
6
6
 
7
7
  import React, { useCallback, useEffect, useRef, useState } from 'react';
@@ -29,7 +29,7 @@ import { collectAssets } from '../engine/assets';
29
29
  import { MainEditor, styles } from '../engine/ui';
30
30
  import { PxArtEditor } from './PxArtEditor';
31
31
  import { SceneEditor } from './SceneEditor';
32
- import { StyleEditor } from './StyleEditor';
32
+ import { ThemeEditor } from './ThemeEditor';
33
33
 
34
34
  // Debounce per path on top of the SDK's save queue: this decides WHEN a change
35
35
  // is worth writing (a drag shouldn't write every frame), the queue decides that
@@ -308,9 +308,9 @@ export function SingleEditor({ path, editor, editorModule }) {
308
308
  body = (
309
309
  <PxArtEditor path={path} text={text} onChange={onChange} files={files} readOnly={readOnly} />
310
310
  );
311
- } else if (kind === 'style') {
311
+ } else if (kind === 'theme') {
312
312
  body = (
313
- <StyleEditor
313
+ <ThemeEditor
314
314
  path={path}
315
315
  text={text}
316
316
  onChange={onChange}
@@ -1,4 +1,4 @@
1
- // Deck style picker for `theme.style`: a palette and a font.
1
+ // Deck theme picker for `theme.style`: a palette and a font.
2
2
  //
3
3
  // Palettes: official entries come from OFFICIAL_PALETTES; a custom
4
4
  // `{ name, colors }` in the file shows as a read-only selected card.
@@ -19,8 +19,8 @@ import { OFFICIAL_PALETTES, parseThemeData, resolveDeckPalette } from '../engine
19
19
  import { EditorBody } from '../engine/ui';
20
20
  import { writeDeckFontModule } from './deckFont';
21
21
  import { usePreviewFaces } from './fontPreview';
22
- import { applyOfficialFont, applyOfficialPalette } from './styleTheme';
23
- import styles from './styleEditor.module.css';
22
+ import { applyOfficialFont, applyOfficialPalette } from './theme';
23
+ import styles from './themeEditor.module.css';
24
24
 
25
25
  function cardClassName(selected, clickable) {
26
26
  return [styles.card, selected ? styles.cardSelected : '', clickable ? '' : styles.cardStatic]
@@ -30,7 +30,7 @@ function cardClassName(selected, clickable) {
30
30
 
31
31
  // One card, palette or font: a head (name + meta) over whatever the type shows
32
32
  // of itself. A button when it can be picked, a plain div when it can't.
33
- function styleCard({ cardKey, name, meta, preview, selected, onPick }) {
33
+ function themeCard({ cardKey, name, meta, preview, selected, onPick }) {
34
34
  // Inner wrapper is the flex column, not the <button>. WebKit still wraps
35
35
  // button contents in a shrink-to-fit anonymous box, so a flex button never
36
36
  // stretches its children on some iOS versions — the palette strip stays
@@ -70,7 +70,7 @@ function swatchStrip(colors) {
70
70
  }
71
71
 
72
72
  function paletteCard({ palette, selected, badge, onPick }) {
73
- return styleCard({
73
+ return themeCard({
74
74
  cardKey: palette.id ?? 'custom',
75
75
  name: palette.name,
76
76
  meta: (
@@ -92,7 +92,7 @@ function paletteCard({ palette, selected, badge, onPick }) {
92
92
  // arrives it renders in the panel font, which reads as an ordinary label rather
93
93
  // than as a broken card.
94
94
  function fontCard({ face, loaded, selected, onPick }) {
95
- return styleCard({
95
+ return themeCard({
96
96
  cardKey: face.name,
97
97
  name: face.label,
98
98
  preview: (
@@ -161,7 +161,7 @@ function FontSection({ text, onChange, onChangeFile, readOnly }) {
161
161
  code asks for another face.
162
162
  </p>
163
163
  <div className={styles.list}>
164
- {styleCard({
164
+ {themeCard({
165
165
  cardKey: 'none',
166
166
  name: 'None',
167
167
  preview: <div className={styles.specimen}>The quick brown fox</div>,
@@ -181,7 +181,7 @@ function FontSection({ text, onChange, onChangeFile, readOnly }) {
181
181
  );
182
182
  }
183
183
 
184
- export function StyleEditor({ text, onChange, onChangeFile, readOnly = false }) {
184
+ export function ThemeEditor({ text, onChange, onChangeFile, readOnly = false }) {
185
185
  // An editor host too old to hand over `onChangeFile` can still pick a
186
186
  // palette; a font needs a second file written, so it stays read-only there.
187
187
  const canPickFont = Boolean(onChangeFile) && !readOnly;
@@ -24,7 +24,7 @@ const deleteFile = castleSdk.deleteFile ?? null;
24
24
 
25
25
  /** The whole content of `fonts.generated.js` for one face. */
26
26
  export function deckFontModuleSource(name) {
27
- return `// Generated by the Style editor. Do not edit -- picking a font rewrites it.
27
+ return `// Generated by the Theme editor. Do not edit -- picking a font rewrites it.
28
28
  //
29
29
  // \`theme.style\` records that this deck's font is ${name}; this module is what
30
30
  // puts ${name}'s bytes in the published bundle. It has to name the face in a
@@ -39,7 +39,7 @@ export const deckFont = '${name}';
39
39
  /** An empty module, for the SDK too old to delete a file. Renders as no deck
40
40
  * font, same as the file being absent. */
41
41
  function emptyModuleSource() {
42
- return `// Generated by the Style editor. Do not edit.
42
+ return `// Generated by the Theme editor. Do not edit.
43
43
  //
44
44
  // This deck has no font of its own; text renders in the default sans stack.
45
45
  export const deckFont = null;
@@ -1,4 +1,4 @@
1
- /* Deck style picker for theme.style: palette + font. Tokens match
1
+ /* Deck theme picker for theme.style: palette + font. Tokens match
2
2
  engine/ui.module.css. */
3
3
 
4
4
  .root {
@@ -56,7 +56,7 @@
56
56
 
57
57
  .card {
58
58
  /* Block, not flex: WebKit's button anonymous box shrink-wraps flex
59
- children (style editor palette strips on some iOS). appearance wipe
59
+ children (theme editor palette strips on some iOS). appearance wipe
60
60
  matches other kit buttons. */
61
61
  display: block;
62
62
  box-sizing: border-box;