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.
- package/dist/agent-prompts.d.ts +8 -2
- package/dist/agent-prompts.js +19 -11
- package/dist/agent.js +37 -13
- package/dist/assetCompression.d.ts +3 -0
- package/dist/assetCompression.js +30 -0
- package/dist/castleJson.d.ts +1 -1
- package/dist/castleJson.js +2 -2
- package/dist/imports.js +2 -2
- package/dist/init.js +4 -4
- package/dist/native/loop.js +9 -0
- package/dist/native/openrouter.d.ts +2 -0
- package/dist/native/openrouter.js +10 -1
- package/dist/native/types.d.ts +1 -0
- package/dist/serve.js +2 -0
- package/dist/serveSecurity.d.ts +2 -0
- package/dist/serveSecurity.js +17 -3
- package/dist/shell/assets/{index-Ws0WrCbi.js → index-DF1yMXPS.js} +4 -4
- package/dist/shell/index.html +1 -1
- package/kits/base/castle.json +1 -1
- package/kits/multiplayer-2d/castle.json +2 -2
- package/kits/multiplayer-2d/code/server/sceneBodies.js +1 -1
- package/kits/multiplayer-2d/code/server/world.js +2 -2
- package/kits/multiplayer-2d/code/systems/multiplayer.js +12 -0
- package/kits/multiplayer-2d/fonts.generated.js +1 -1
- package/kits/multiplayer-2d/package-lock.json +1 -1
- package/kits/multiplayer-3d/CLAUDE.md +5 -4
- package/kits/multiplayer-3d/castle.json +3 -3
- package/kits/multiplayer-3d/code/client/avatars.js +47 -18
- package/kits/multiplayer-3d/code/client/nameTags.js +16 -35
- package/kits/multiplayer-3d/code/server/sceneBodies.js +1 -1
- package/kits/multiplayer-3d/code/systems/multiplayer.js +14 -1
- package/kits/multiplayer-3d/fonts.generated.js +1 -1
- package/kits/multiplayer-3d/package-lock.json +1 -1
- package/kits/physics-2d/CLAUDE.md +7 -6
- package/kits/physics-2d/behaviors/Collider.jsx +7 -1
- package/kits/physics-2d/behaviors/RigidBody.jsx +6 -3
- package/kits/physics-2d/castle.json +2 -2
- package/kits/physics-2d/editors/SceneEditor.jsx +1 -0
- package/kits/physics-2d/editors/SingleEditor.jsx +4 -4
- package/kits/physics-2d/editors/{StyleEditor.jsx → ThemeEditor.jsx} +8 -8
- package/kits/physics-2d/editors/deckFont.js +2 -2
- package/kits/physics-2d/editors/{styleEditor.module.css → themeEditor.module.css} +2 -2
- package/kits/physics-2d/engine/avatarArt.js +108 -14
- package/kits/physics-2d/engine/files.js +2 -2
- package/kits/physics-2d/engine/physics/matterBridge.js +2 -2
- package/kits/physics-2d/engine/scene.js +1 -1
- package/kits/physics-2d/fonts.generated.js +1 -1
- package/kits/physics-2d/package-lock.json +1 -1
- package/kits/physics-3d/behaviors/Body.jsx +10 -1
- package/kits/physics-3d/behaviors/Door.jsx +5 -0
- package/kits/physics-3d/behaviors/Lookable.jsx +3 -0
- package/kits/physics-3d/behaviors/Model.jsx +4 -0
- package/kits/physics-3d/behaviors/Pickup.jsx +4 -0
- package/kits/physics-3d/behaviors/Player.jsx +7 -0
- package/kits/physics-3d/behaviors/Shape.jsx +13 -0
- package/kits/physics-3d/castle.json +1 -1
- package/kits/physics-3d/engine3d/editor3dInspector.jsx +1 -0
- package/kits/physics-3d/fonts.generated.js +1 -1
- package/kits/physics-3d/package-lock.json +1 -1
- package/kits/real-time/castle.json +1 -1
- package/kits/real-time/code/client/connection.js +11 -2
- package/kits/real-time/package-lock.json +1 -1
- package/kits/turn-based/castle.json +1 -1
- package/kits/turn-based/fonts.generated.js +1 -1
- package/kits/turn-based/package-lock.json +1 -1
- package/package.json +3 -1
- /package/kits/physics-2d/editors/{styleTheme.js → theme.js} +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
// Other-player actors
|
|
2
|
-
// from deck blueprints, updates their network poses,
|
|
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 {
|
|
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
|
|
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
|
|
57
|
-
// roster can arrive several frames after connection, so an empty
|
|
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
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
function showTag(avatars, scene, key, username, transform,
|
|
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
|
|
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
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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,
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
53
|
-
|
|
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
|
|
94
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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,
|
|
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:
|
|
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
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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": "
|
|
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-
|
|
75
|
+
"publishedVersion": "2026-09-10T22:05:01.629Z",
|
|
76
76
|
"imports": {
|
|
77
77
|
"castle.base": {
|
|
78
78
|
"deckId": "yRcmH4_aYllE",
|
|
@@ -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 /
|
|
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 {
|
|
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 === '
|
|
311
|
+
} else if (kind === 'theme') {
|
|
312
312
|
body = (
|
|
313
|
-
<
|
|
313
|
+
<ThemeEditor
|
|
314
314
|
path={path}
|
|
315
315
|
text={text}
|
|
316
316
|
onChange={onChange}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Deck
|
|
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 './
|
|
23
|
-
import styles from './
|
|
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
|
|
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
|
|
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
|
|
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
|
-
{
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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;
|