castle-web-cli 0.4.166 → 0.4.168
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.d.ts +1 -2
- package/dist/agent.js +22 -13
- package/dist/api.js +16 -1
- package/dist/castle-host/host.d.ts +2 -0
- package/dist/castle-host/host.js +17 -3
- package/dist/imports.js +19 -4
- package/dist/localPaths.d.ts +2 -0
- package/dist/localPaths.js +6 -0
- package/dist/platformDoc.js +1 -1
- package/dist/save-deck.js +13 -1
- package/dist/serve.js +7 -1
- package/dist/serveSecurity.d.ts +12 -0
- package/dist/serveSecurity.js +42 -0
- package/dist/shell/assets/index-CVHj503j.css +1 -0
- package/dist/shell/assets/index-D-qJ5VGp.js +425 -0
- package/dist/shell/index.html +2 -2
- package/kits/base/castle.json +64 -2
- package/kits/base/editors/ImageViewer.js +259 -0
- package/kits/base/editors/MediaPlayer.js +90 -0
- package/kits/base/editors/imageViewer.css +95 -0
- package/kits/base/editors/mediaPlayer.css +58 -0
- package/kits/base/sdk/README.md +3 -1
- package/kits/base/sdk/castle.d.ts +1 -0
- package/kits/base/sdk/commands.d.ts +7 -0
- package/kits/base/sdk/multiplayer.d.ts +6 -0
- package/kits/base/sdk/multiplayer.js +6 -0
- package/kits/blank/.castle/agent/children.json +1 -0
- package/kits/blank/.castle/agent/messages.json +9 -0
- package/kits/blank/.castle/agent/pending-sends.json +1 -0
- package/kits/blank/.castle/agent/tasks/index.md +2 -0
- package/kits/blank/.castle/docs/platform.md +36 -0
- package/kits/blank/node_modules/.vite/deps/_metadata.json +8 -0
- package/kits/blank/node_modules/.vite/deps/package.json +3 -0
- package/kits/multiplayer-2d/CLAUDE.md +6 -0
- package/kits/multiplayer-2d/castle.json +3 -3
- package/kits/multiplayer-2d/code/systems/multiplayer.js +5 -0
- package/kits/multiplayer-2d/package-lock.json +4 -1
- package/kits/multiplayer-3d/CLAUDE.md +6 -1
- package/kits/multiplayer-3d/castle.json +3 -3
- package/kits/multiplayer-3d/code/systems/multiplayer.js +5 -0
- package/kits/multiplayer-3d/package-lock.json +1 -1
- package/kits/physics-2d/behaviors/Joints.jsx +11 -5
- package/kits/physics-2d/behaviors/Sprite.jsx +6 -21
- package/kits/physics-2d/castle.json +1 -61
- package/kits/physics-2d/editors/BlueprintLibrary.jsx +8 -68
- package/kits/physics-2d/editors/SingleEditor.jsx +3 -10
- package/kits/physics-2d/editors/editorRegistry.jsx +36 -2
- package/kits/physics-2d/engine/art.js +25 -1
- package/kits/physics-2d/engine/artThumbnail.jsx +92 -0
- package/kits/physics-2d/engine/files.js +4 -15
- package/kits/physics-2d/engine/spriteField.jsx +176 -0
- package/kits/physics-2d/engine/ui.module.css +136 -0
- package/kits/physics-2d/package-lock.json +5 -0
- package/kits/physics-2d/pnpm-lock.yaml +96 -3
- package/kits/physics-3d/castle.json +1 -1
- package/kits/physics-3d/package-lock.json +1 -1
- package/kits/physics-3d/pnpm-lock.yaml +88 -3
- package/kits/real-time/CLAUDE.md +5 -0
- package/kits/real-time/castle.json +2 -2
- package/kits/real-time/code/client/connection.js +4 -0
- package/kits/real-time/code/client/gameHooks.js +3 -0
- package/kits/real-time/code/client/soloBadge.js +71 -0
- package/kits/real-time/package.json +3 -0
- package/kits/turn-based/castle.json +1 -1
- package/kits/turn-based/package-lock.json +1 -1
- package/package.json +1 -1
- package/dist/shell/assets/index-CG9TrvCm.js +0 -444
- package/dist/shell/assets/index-CIRBt1qf.css +0 -1
- package/kits/base/node_modules/.package-lock.json +0 -28
- package/kits/base/node_modules/castle-web-fonts/README.md +0 -114
- package/kits/base/node_modules/castle-web-fonts/dist/catalog.d.ts +0 -18
- package/kits/base/node_modules/castle-web-fonts/dist/catalog.js +0 -142
- package/kits/base/node_modules/castle-web-fonts/dist/face.d.ts +0 -1
- package/kits/base/node_modules/castle-web-fonts/dist/face.js +0 -52
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Bore.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Bore.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/CourierPrime.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/CourierPrime.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/DMSans.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/DMSans.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Glacier.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Glacier.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Piazzolla.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Piazzolla.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Synco.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Synco.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Tektur.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Tektur.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/YatraOne.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/YatraOne.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/index.d.ts +0 -16
- package/kits/base/node_modules/castle-web-fonts/dist/index.js +0 -15
- package/kits/base/node_modules/castle-web-fonts/licenses/CC-BY-ND-4.0.txt +0 -393
- package/kits/base/node_modules/castle-web-fonts/licenses/FONTS.md +0 -64
- package/kits/base/node_modules/castle-web-fonts/licenses/OFL-1.1.txt +0 -95
- package/kits/base/node_modules/castle-web-fonts/package.json +0 -47
- package/kits/base/node_modules/castle-web-fonts/woff2/Bore.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/CourierPrime.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/DMSans.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Glacier.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/HelicoCentrica.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Piazzolla.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/SOURCE.txt +0 -2
- package/kits/base/node_modules/castle-web-fonts/woff2/Synco.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Tektur.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/YatraOne.woff2 +0 -0
- package/kits/physics-2d/editors/ImageViewer.jsx +0 -206
- package/kits/physics-2d/editors/MediaPlayer.jsx +0 -57
- package/kits/physics-2d/editors/mediaFile.js +0 -31
- package/kits/physics-2d/editors/mediaViewer.module.css +0 -128
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "YzUYA2mw",
|
|
4
|
+
"role": "assistant",
|
|
5
|
+
"text": "Welcome to an early test of Castle's new engine! You're starting with a blank deck, without the official art or scene editors, but if HTML and JavaScript can do it then I can help you build it.\n\nDo you already know what you want to make, or do you want to figure it out together?",
|
|
6
|
+
"at": "2026-08-27T19:05:49.289Z",
|
|
7
|
+
"status": "done"
|
|
8
|
+
}
|
|
9
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Castle platform
|
|
2
|
+
|
|
3
|
+
How every Castle deck works, from the Castle CLI. Written fresh at every serve
|
|
4
|
+
start; the deck's own guide (its kit's CLAUDE.md under imports/) adds what its
|
|
5
|
+
kit provides on top of this.
|
|
6
|
+
|
|
7
|
+
## Imports are read-only
|
|
8
|
+
|
|
9
|
+
- **`imports/` holds other decks' files and is locked read-only on disk.** Files are mode 0444 and directories 0555, so any shell command that writes, moves, or deletes inside `imports/` fails with `EACCES` / "Permission denied" — that is the lock working, not a broken checkout. Do not `chmod` around it, do not `sudo`, and do not retry the command a different way.
|
|
10
|
+
- **Change an import through the CLI, never the filesystem.** `castle-web add-import <deckIdOrUrl>` adds one and `castle-web update-import [alias]` re-fetches it (`--check` to see if it is outdated, `--revert` to undo). These handle the unlock/relock themselves.
|
|
11
|
+
- **`castle-web list-decks [--kind mine|saved|kits]` is how you find out WHAT can be imported** — one deck per line, starting with the id `add-import` takes, and marked when this deck already has it. `imports/` only shows what is here already; it is not a catalogue.
|
|
12
|
+
- **To change one file of an import, FORK it**: `castle-web fork-import-file <path>` takes this deck's own version of that file, and every reference to the imported path uses your version from then on (`castle-web revert-import-file <path>` drops it and goes back to the import's). Fork only what you actually need to change, and as few files as you can -- a forked file stops taking the import's updates, so the more you fork the less an `update-import` can give you. To build something of your own rather than change theirs, write an ordinary file in this deck instead -- and check first whether the kit already lets a deck file register what you want (its CLAUDE.md names those seams), since that path keeps taking updates and a fork does not. When you do fork, it is often worth forking only to add a hook or registration point and keeping the logic itself in a deck file, so later changes need no further forks; for a quick one-off tweak a plain direct fork is fine.
|
|
13
|
+
- **Deleting the deck directory itself needs the lock released first** (`chmod -R u+w` on the deck dir) — that is the one legitimate reason to touch the modes, and only for a directory being thrown away.
|
|
14
|
+
|
|
15
|
+
## Assets
|
|
16
|
+
|
|
17
|
+
- **Load static assets (drawings, audio, etc.) through the bundler — never runtime-`fetch` a loose file path.** Use a static `import`, `import.meta.glob('./drawings/*.svg', { eager: true, import: 'default' })`, or inline the asset directly. The dev serve happens to serve loose files over HTTP, so `fetch('drawings/qb.svg')` looks like it works locally — but `save-deck` bundles the whole deck into a single file, loose files are no longer served, and the fetch silently fails on every platform. Kit decks: use the kit's own drawing/asset-loading APIs instead of a raw `fetch`.
|
|
18
|
+
|
|
19
|
+
## Touch controls
|
|
20
|
+
|
|
21
|
+
- **Playable on a touchscreen, with only the controls the game actually needs.** Castle decks are played on phones, so whatever input a game does use must work by touch — direct tap/drag on the game itself wherever possible, and on-screen buttons only where the mechanics genuinely call for them. Do NOT add controls a game doesn't need: never drop in a generic d-pad or movement overlay by default. Prefer touching the game directly over an overlay that just mirrors keyboard keys. Keyboard input is fine to support on top for desktop play. Match the controls to the actual mechanics — a game with no directional movement should have no movement controls at all.
|
|
22
|
+
|
|
23
|
+
## Fit the card
|
|
24
|
+
|
|
25
|
+
- **The deck plays inside a fixed 5:7 portrait card, not the full window.** The card is sized to fit the screen (at most about 450x630px), clips overflow, and does not scroll. Design the whole layout to fit inside that portrait box: size UI relative to the card with percentages, flex/grid, `min()`, `clamp()`, or viewport-relative units instead of fixed tall panels. Let playfields scale down on smaller cards rather than overflowing; anything outside the card edges is cut off. The SDK exports `CARD_RATIO` (= 5 / 7) if you need the exact ratio.
|
|
26
|
+
- **Hand-rolled `<canvas>` elements must account for devicePixelRatio, or the game looks blurry on phones.** Size the backing store to the CSS layout size times `devicePixelRatio` (e.g. `canvas.width = rect.width * dpr`), keep the CSS width/height as the layout size, and scale the 2D context (`ctx.scale(dpr, dpr)`) so drawing code stays in CSS units — re-apply on resize. Kit decks don't need to do this by hand; the kit's engine already configures its canvas for DPR.
|
|
27
|
+
- Exception: deliberate pixel art wants a fixed low-resolution backing store with `image-rendering: pixelated` CSS instead — don't DPR-scale that; the crisp chunky look is the point.
|
|
28
|
+
|
|
29
|
+
## Formats, reloading, editors
|
|
30
|
+
|
|
31
|
+
- **Supported media formats are exactly**: images `.png` `.jpg` `.jpeg` `.gif` `.webp` `.svg`, audio `.mp3` `.wav` `.m4a`, video `.mp4` (H.264). Nothing else — a published deck inlines every asset as a `data:` URI, where the browser trusts the declared type instead of sniffing the bytes, so a format that merely works while serving (`.mov` is the classic case) can be dead once published. Convert, don't improvise.
|
|
32
|
+
- To add an npm package, put it in `package.json` `dependencies` and run `castle-web install .` (the install `init` ran; it uses whichever package manager made the lockfile).
|
|
33
|
+
- Do not reload the deck after an edit. Nothing auto-reloads: the person applies your changes when they are ready, from the Play panel's restart button or a panel's own reload control. `npm run restart` reloads every open panel at once and breaks their flow — only when asked.
|
|
34
|
+
- The deck can define its own editors: declare a file type in `castle.json` (`editor.fileTypes`: ext, label, icon, and an `editors/` module) and the Files panel opens that type in it — whole recipe under `## Editors` in the deck's guide (its kit's `CLAUDE.md` under `imports/`). Editors under `imports/` are read-only; add or fork, don't edit.
|
|
35
|
+
- An editor is for content FILES the creator keeps coming back to (catalogs, dialogue, level tables) that nothing already opens. One-off or generated data stays code.
|
|
36
|
+
- Unprompted, an editor is an offer, never a build: when the same data keeps getting edited by hand, propose one and build on the user's yes. Never during a first build.
|
|
@@ -137,6 +137,12 @@ and then, all optional, all found by root-anchored glob in
|
|
|
137
137
|
| `code/server/game.js` | the game, as six hooks on a default export |
|
|
138
138
|
| `code/server/deckComponents.js` | `export const componentDefaults = { Cargo: {...} }` |
|
|
139
139
|
|
|
140
|
+
The kit draws no HUD, with one exception: when the join fell back to solo,
|
|
141
|
+
`castle.real-time`'s `soloBadge.js` shows a small fixed badge naming why
|
|
142
|
+
(`status().soloReason`: a private deck, no published server, or plain
|
|
143
|
+
unavailable), so a creator testing their own deck is told rather than left to
|
|
144
|
+
guess. A deck's `soloBadge(status)` client hook hides (`false`) or rewords it.
|
|
145
|
+
|
|
140
146
|
The six server hooks: `ready(sim)`, `message(session, player, data)`,
|
|
141
147
|
`place(sim, handovers)`, `step(sim, handovers)`, `join(sim)`, `delta(sim)`.
|
|
142
148
|
`place` runs BEFORE the physics step, which is where a write that only reaches a
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"imports": {
|
|
16
16
|
"castle.real-time": {
|
|
17
17
|
"deckId": "xFVr-afOLuUQ",
|
|
18
|
-
"version": "2026-09-
|
|
18
|
+
"version": "2026-09-05T01:31:28.578Z"
|
|
19
19
|
},
|
|
20
20
|
"castle.physics-2d": {
|
|
21
21
|
"deckId": "ckRZGFW4iPrx",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"castle.base": {
|
|
25
25
|
"deckId": "yRcmH4_aYllE",
|
|
26
|
-
"version": "2026-09-
|
|
26
|
+
"version": "2026-09-05T01:31:11.287Z"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"autoUpdateWhenImported": true,
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
},
|
|
36
36
|
"deckId": "oiFu96LkwZ09",
|
|
37
37
|
"cardId": "nkJgrvrCby3H",
|
|
38
|
-
"publishedVersion": "2026-09-
|
|
38
|
+
"publishedVersion": "2026-09-05T01:32:45.261Z"
|
|
39
39
|
}
|
|
@@ -14,6 +14,7 @@ import * as Ownership from '@imports/castle.real-time/code/client/ownership.js';
|
|
|
14
14
|
import * as Smoothing from '@imports/castle.real-time/code/client/smooth.js';
|
|
15
15
|
import * as Poses from '../client/poses.js';
|
|
16
16
|
import { connectSession } from '@imports/castle.real-time/code/client/connection.js';
|
|
17
|
+
import { showSoloBadge, updateSoloBadge } from '@imports/castle.real-time/code/client/soloBadge.js';
|
|
17
18
|
import { STATE, WORLD } from '@imports/castle.real-time/code/client/messages.js';
|
|
18
19
|
import { game } from '@imports/castle.real-time/code/client/gameHooks.js';
|
|
19
20
|
|
|
@@ -111,6 +112,7 @@ class MultiplayerSystem {
|
|
|
111
112
|
}
|
|
112
113
|
game.afterFrame?.(scene, dt, this);
|
|
113
114
|
this.publish(scene, local);
|
|
115
|
+
updateSoloBadge(this.net.status(), game);
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
// Join the public session once. Scene resets retain this transport.
|
|
@@ -378,6 +380,9 @@ class MultiplayerSystem {
|
|
|
378
380
|
this.reset(scene);
|
|
379
381
|
this.net?.close();
|
|
380
382
|
this.net = null;
|
|
383
|
+
// The badge is a fixed element outside the scene, so a disposed runtime
|
|
384
|
+
// (an editor preview closing, say) has to take it down explicitly.
|
|
385
|
+
showSoloBadge(null);
|
|
381
386
|
}
|
|
382
387
|
}
|
|
383
388
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"../../sdk": {
|
|
31
31
|
"name": "castle-web-sdk",
|
|
32
|
-
"version": "0.4.
|
|
32
|
+
"version": "0.4.26",
|
|
33
33
|
"dev": true,
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"eslint": "^9.0.0",
|
|
@@ -151,6 +151,7 @@
|
|
|
151
151
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
|
152
152
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
|
153
153
|
"license": "MIT",
|
|
154
|
+
"peer": true,
|
|
154
155
|
"dependencies": {
|
|
155
156
|
"@dnd-kit/accessibility": "^3.1.1",
|
|
156
157
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -307,6 +308,7 @@
|
|
|
307
308
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
|
308
309
|
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
|
309
310
|
"license": "MIT",
|
|
311
|
+
"peer": true,
|
|
310
312
|
"engines": {
|
|
311
313
|
"node": ">=0.10.0"
|
|
312
314
|
}
|
|
@@ -316,6 +318,7 @@
|
|
|
316
318
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
|
317
319
|
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
|
318
320
|
"license": "MIT",
|
|
321
|
+
"peer": true,
|
|
319
322
|
"dependencies": {
|
|
320
323
|
"scheduler": "^0.27.0"
|
|
321
324
|
},
|
|
@@ -238,7 +238,12 @@ Map())` — and the next `showPlayer` respawns it.
|
|
|
238
238
|
- **The player blueprint itself.** The kit spawns a scene from the deck's own
|
|
239
239
|
root; the deck ships it. `blueprints/other-player.scene` is only the name it
|
|
240
240
|
looks for when the deck names none.
|
|
241
|
-
- **Any HUD.** Five decks wrote five HUDs with no shared lines.
|
|
241
|
+
- **Any HUD.** Five decks wrote five HUDs with no shared lines. The one
|
|
242
|
+
on-screen element the kit does draw is the solo badge
|
|
243
|
+
(`castle.real-time`'s `soloBadge.js`): when the join fell back to solo it
|
|
244
|
+
names why (`status().soloReason`), so a creator testing a private or
|
|
245
|
+
unpublished deck is told rather than left to guess. A deck's
|
|
246
|
+
`soloBadge(status)` client hook hides or rewords it.
|
|
242
247
|
- **Gameplay verbs** — carry, vehicle, round, lobby. Measured across six decks
|
|
243
248
|
and none of them survived contact; see the audit.
|
|
244
249
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"imports": {
|
|
15
15
|
"castle.real-time": {
|
|
16
16
|
"deckId": "xFVr-afOLuUQ",
|
|
17
|
-
"version": "2026-09-
|
|
17
|
+
"version": "2026-09-05T01:31:28.578Z"
|
|
18
18
|
},
|
|
19
19
|
"castle.physics-3d": {
|
|
20
20
|
"deckId": "JH0SclbPVP0y",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
},
|
|
28
28
|
"castle.base": {
|
|
29
29
|
"deckId": "yRcmH4_aYllE",
|
|
30
|
-
"version": "2026-09-
|
|
30
|
+
"version": "2026-09-05T01:31:11.287Z"
|
|
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-05T01:32:53.625Z",
|
|
37
37
|
"main": "main.jsx",
|
|
38
38
|
"server": {
|
|
39
39
|
"main": "code/server/index.js",
|
|
@@ -9,6 +9,7 @@ import * as Ownership from '@imports/castle.real-time/code/client/ownership.js';
|
|
|
9
9
|
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
|
+
import { showSoloBadge, updateSoloBadge } from '@imports/castle.real-time/code/client/soloBadge.js';
|
|
12
13
|
import { STATE, WORLD } from '@imports/castle.real-time/code/client/messages.js';
|
|
13
14
|
import { game } from '@imports/castle.real-time/code/client/gameHooks.js';
|
|
14
15
|
|
|
@@ -115,6 +116,7 @@ class MultiplayerSystem {
|
|
|
115
116
|
};
|
|
116
117
|
}
|
|
117
118
|
this.publish(scene, local);
|
|
119
|
+
updateSoloBadge(this.net.status(), game);
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
// Join the Castle public session once and retain the transport across scene loads.
|
|
@@ -394,6 +396,9 @@ class MultiplayerSystem {
|
|
|
394
396
|
this.reset(scene);
|
|
395
397
|
this.net?.close();
|
|
396
398
|
this.net = null;
|
|
399
|
+
// The badge is a fixed element outside the scene, so a disposed runtime
|
|
400
|
+
// (an editor preview closing, say) has to take it down explicitly.
|
|
401
|
+
showSoloBadge(null);
|
|
397
402
|
}
|
|
398
403
|
}
|
|
399
404
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import { Panel, SelectField, NumberField, CheckboxField, Button
|
|
2
|
+
import { Panel, SelectField, NumberField, CheckboxField, Button } from '../engine/ui';
|
|
3
|
+
import { SpriteField } from '../engine/spriteField';
|
|
3
4
|
import { centerOf, inActorWorldSpace } from '../engine/physics/controls';
|
|
4
5
|
import { JOINT_TYPES } from '../engine/physics/joints';
|
|
5
6
|
import { drawJointArt } from '../engine/physics/jointArt';
|
|
@@ -75,9 +76,12 @@ export class Joints {
|
|
|
75
76
|
});
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
static Inspector({ component, setComponent, override, beginPick, pickActive,
|
|
79
|
+
static Inspector({ component, setComponent, override, beginPick, pickActive, sprites }) {
|
|
79
80
|
const list = Array.isArray(component.list) ? component.list : [];
|
|
80
|
-
|
|
81
|
+
// Joint art is drawings-only (tile/stretch along a link); images stay out.
|
|
82
|
+
const spriteFiles = Object.keys(sprites ?? {})
|
|
83
|
+
.filter((f) => f.endsWith('.sprite') || f.endsWith('.pxart'))
|
|
84
|
+
.sort();
|
|
81
85
|
// Which entry's target-pick is live. Combined with the editor's global
|
|
82
86
|
// `pickActive` so a cancel (Esc/empty click) clears every entry's state.
|
|
83
87
|
const [pickingIndex, setPickingIndex] = useState(null);
|
|
@@ -95,6 +99,7 @@ export class Joints {
|
|
|
95
99
|
key={i}
|
|
96
100
|
index={i}
|
|
97
101
|
joint={joint}
|
|
102
|
+
sprites={sprites}
|
|
98
103
|
spriteFiles={spriteFiles}
|
|
99
104
|
active={pickActive && pickingIndex === i}
|
|
100
105
|
onPickToggle={() => onPickToggle(i)}
|
|
@@ -114,7 +119,7 @@ function defaultJoint() {
|
|
|
114
119
|
|
|
115
120
|
// One editable link in the list: type, target picker, type-specific fields, and
|
|
116
121
|
// how it renders in play (line / hidden / sprite art along the joint).
|
|
117
|
-
function JointEntry({ index, joint, spriteFiles, active, onPickToggle, onPatch, onRemove }) {
|
|
122
|
+
function JointEntry({ index, joint, sprites, spriteFiles, active, onPickToggle, onPatch, onRemove }) {
|
|
118
123
|
const [showAnchors, setShowAnchors] = useState(false);
|
|
119
124
|
const type = joint.type ?? 'spring';
|
|
120
125
|
const isPivot = type === 'weld';
|
|
@@ -180,10 +185,11 @@ function JointEntry({ index, joint, spriteFiles, active, onPickToggle, onPatch,
|
|
|
180
185
|
<SelectField label="Show in play" value={render} onChange={(v) => onPatch({ render: v })} options={RENDER_MODES} />
|
|
181
186
|
{render === 'sprite' ? (
|
|
182
187
|
<>
|
|
183
|
-
<
|
|
188
|
+
<SpriteField
|
|
184
189
|
label="Art"
|
|
185
190
|
value={joint.sprite || ''}
|
|
186
191
|
onChange={(v) => onPatch({ sprite: v })}
|
|
192
|
+
sprites={sprites}
|
|
187
193
|
files={spriteFiles}
|
|
188
194
|
allowEmpty
|
|
189
195
|
/>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { resolveDeckFile } from 'castle-web-sdk';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { artFrameCount, isImageArt,
|
|
3
|
+
import { artFrameCount, isImageArt, renderDisplayFrame } from '../engine/art';
|
|
4
4
|
import { isVectorRenderer } from '../engine/pxart';
|
|
5
|
-
import { renderSmoothCompositeFrame, renderSmoothSpriteFrame } from '../engine/pxartSmooth';
|
|
6
5
|
import { spriteDestRect } from '../engine/spriteGeometry';
|
|
7
|
-
import {
|
|
6
|
+
import { Panel, SelectField } from '../engine/ui';
|
|
7
|
+
import { SpriteField } from '../engine/spriteField';
|
|
8
8
|
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
9
9
|
import { parseTint, tintCanvas } from './tint';
|
|
10
10
|
|
|
@@ -166,14 +166,13 @@ export class Sprite {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
static Inspector({ component, setComponent, sprites, override }) {
|
|
169
|
-
const spriteFiles = getSpriteFiles(sprites);
|
|
170
169
|
return (
|
|
171
170
|
<Panel title="Sprite" overridden={override?.anyOverridden()}>
|
|
172
|
-
<
|
|
171
|
+
<SpriteField
|
|
173
172
|
label="File"
|
|
174
173
|
value={component.file}
|
|
175
174
|
onChange={(file) => setComponent({ file })}
|
|
176
|
-
|
|
175
|
+
sprites={sprites}
|
|
177
176
|
{...overrideProps(override, 'file')}
|
|
178
177
|
/>
|
|
179
178
|
<SelectField
|
|
@@ -270,15 +269,7 @@ const MAX_CACHE_LONG_SIDE = 2048;
|
|
|
270
269
|
function frameSource(sprite, frameIndex) {
|
|
271
270
|
if (isImageArt(sprite)) return sprite.ready ? sprite.image : null;
|
|
272
271
|
const canvas = document.createElement('canvas');
|
|
273
|
-
|
|
274
|
-
renderSmoothCompositeFrame(sprite, frameIndex, canvas);
|
|
275
|
-
return canvas;
|
|
276
|
-
}
|
|
277
|
-
if (sprite.cornerRadius > 0) {
|
|
278
|
-
renderSmoothSpriteFrame(sprite, frameIndex, canvas, { cornerRadius: sprite.cornerRadius });
|
|
279
|
-
return canvas;
|
|
280
|
-
}
|
|
281
|
-
return renderArtFrame(sprite, frameIndex, canvas) ? canvas : null;
|
|
272
|
+
return renderDisplayFrame(sprite, frameIndex, canvas) ? canvas : null;
|
|
282
273
|
}
|
|
283
274
|
|
|
284
275
|
function sourceSize(source) {
|
|
@@ -431,9 +422,3 @@ function advanceAnim(state, sprite, sequence, tag, dt) {
|
|
|
431
422
|
}
|
|
432
423
|
state.frameIndex = sequence[state.pos];
|
|
433
424
|
}
|
|
434
|
-
|
|
435
|
-
// Everything the deck can point a Sprite at: `.pxart` drawings and image files
|
|
436
|
-
// alike, which is exactly what the resolved art map holds.
|
|
437
|
-
function getSpriteFiles(sprites) {
|
|
438
|
-
return Object.keys(sprites ?? {}).sort();
|
|
439
|
-
}
|
|
@@ -63,66 +63,6 @@
|
|
|
63
63
|
"icon": "file",
|
|
64
64
|
"editor": "kit",
|
|
65
65
|
"data": true
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
"ext": ".png",
|
|
69
|
-
"label": "Image",
|
|
70
|
-
"icon": "image",
|
|
71
|
-
"editor": "kit"
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
"ext": ".jpg",
|
|
75
|
-
"label": "Image",
|
|
76
|
-
"icon": "image",
|
|
77
|
-
"editor": "kit"
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
"ext": ".jpeg",
|
|
81
|
-
"label": "Image",
|
|
82
|
-
"icon": "image",
|
|
83
|
-
"editor": "kit"
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"ext": ".gif",
|
|
87
|
-
"label": "Image",
|
|
88
|
-
"icon": "image",
|
|
89
|
-
"editor": "kit"
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
"ext": ".webp",
|
|
93
|
-
"label": "Image",
|
|
94
|
-
"icon": "image",
|
|
95
|
-
"editor": "kit"
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
"ext": ".svg",
|
|
99
|
-
"label": "Image",
|
|
100
|
-
"icon": "image",
|
|
101
|
-
"editor": "kit"
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
"ext": ".mp3",
|
|
105
|
-
"label": "Audio",
|
|
106
|
-
"icon": "music",
|
|
107
|
-
"editor": "kit"
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"ext": ".wav",
|
|
111
|
-
"label": "Audio",
|
|
112
|
-
"icon": "music",
|
|
113
|
-
"editor": "kit"
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
"ext": ".m4a",
|
|
117
|
-
"label": "Audio",
|
|
118
|
-
"icon": "music",
|
|
119
|
-
"editor": "kit"
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
"ext": ".mp4",
|
|
123
|
-
"label": "Video",
|
|
124
|
-
"icon": "film",
|
|
125
|
-
"editor": "kit"
|
|
126
66
|
}
|
|
127
67
|
],
|
|
128
68
|
"defaultPlayFile": "scenes/main.scene"
|
|
@@ -132,7 +72,7 @@
|
|
|
132
72
|
"main": "main.jsx",
|
|
133
73
|
"autoUpdateWhenImported": true,
|
|
134
74
|
"title": "physics-2d",
|
|
135
|
-
"publishedVersion": "2026-09-
|
|
75
|
+
"publishedVersion": "2026-09-05T00:07:35.112Z",
|
|
136
76
|
"imports": {
|
|
137
77
|
"castle.base": {
|
|
138
78
|
"deckId": "yRcmH4_aYllE",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
import { ArtThumbnail } from '../engine/artThumbnail';
|
|
3
3
|
import { screenToCard } from '../engine/scene';
|
|
4
4
|
import { countBlueprintInstances, listBlueprints } from '../engine/blueprint';
|
|
5
5
|
import { ConfirmDialog, ContextMenu, cx, Icon, styles, useWheelToHorizontal } from '../engine/ui';
|
|
@@ -235,77 +235,17 @@ function BlueprintDragPreview({ preview, sprites }) {
|
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
245
|
-
const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
246
|
-
for (let i = 3; i < data.length; i += 4) if (data[i] !== 0) return false;
|
|
247
|
-
return true;
|
|
248
|
-
} catch {
|
|
249
|
-
// Tainted or zero-sized canvas: assume it draws something rather than
|
|
250
|
-
// mislabeling real art as blank.
|
|
251
|
-
return false;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// A blueprint's slot art, falling back to its NAME when there is nothing to
|
|
256
|
-
// show -- no Sprite, a file that doesn't resolve, or art that is entirely
|
|
257
|
-
// transparent (a "Draw new" sprite nobody has drawn in yet). Without it a belt
|
|
258
|
-
// of not-yet-drawn actors is a row of identical placeholders.
|
|
259
|
-
// `showName={false}` for callers that already print the name right beside the
|
|
260
|
-
// thumbnail (the creation modal's tiles) -- otherwise a sprite-less blueprint
|
|
261
|
-
// renders its name inside the tile AND under it, reading as "GoalGoal".
|
|
238
|
+
// A blueprint's slot art — delegates to ArtThumbnail so the belt and the
|
|
239
|
+
// sprite picker share blank-name fallback. `showName={false}` for callers that
|
|
240
|
+
// already print the name right beside the thumbnail (the creation modal's
|
|
241
|
+
// tiles) -- otherwise a sprite-less blueprint renders its name inside the tile
|
|
242
|
+
// AND under it, reading as "GoalGoal".
|
|
262
243
|
export function BlueprintThumbnail({ blueprint, sprites, preview = false, showName = true }) {
|
|
263
|
-
const canvasRef = useRef(null);
|
|
264
244
|
const spritePath = blueprint.components?.Sprite?.file;
|
|
265
245
|
const sprite = spritePath ? sprites?.[spritePath] : null;
|
|
266
|
-
// Thumbnails draw on change, not on a frame loop, so an image that finishes
|
|
267
|
-
// decoding after this rendered has to say so or the slot stays blank.
|
|
268
|
-
const [loadTick, setLoadTick] = useState(0);
|
|
269
|
-
const [blank, setBlank] = useState(false);
|
|
270
|
-
useEffect(() => onArtLoaded(() => setLoadTick((tick) => tick + 1)), []);
|
|
271
|
-
useEffect(() => {
|
|
272
|
-
const canvas = canvasRef.current;
|
|
273
|
-
if (!canvas || !sprite) return;
|
|
274
|
-
const drew = renderArtFrame(sprite, 0, canvas);
|
|
275
|
-
// `false` means "not here yet" (an image still loading), not blank.
|
|
276
|
-
if (drew !== false) setBlank(rendersNothing(sprite, canvas));
|
|
277
|
-
}, [sprite, loadTick]);
|
|
278
246
|
const name = showName && typeof blueprint.name === 'string' ? blueprint.name.trim() : '';
|
|
279
|
-
// The canvas stays mounted underneath, so art arriving later clears this.
|
|
280
|
-
// The inner span is load-bearing, not decoration: bare text inside a flex
|
|
281
|
-
// container becomes an anonymous item sized to max-content, so it never
|
|
282
|
-
// "overflows a line" and `overflow-wrap` never fires -- a long single-word
|
|
283
|
-
// name silently clipped at both edges instead of wrapping. A real child can
|
|
284
|
-
// be held to the box's width, which is what gives the wrap something to
|
|
285
|
-
// break against.
|
|
286
|
-
const label = name ? (
|
|
287
|
-
<span className={cx(styles.bpSlotName, preview && styles.bpSlotNamePreview)} title={name}>
|
|
288
|
-
<span className={styles.bpSlotNameText}>{name}</span>
|
|
289
|
-
</span>
|
|
290
|
-
) : (
|
|
291
|
-
<span className={styles.bpSlotFallback}>
|
|
292
|
-
<Icon name="clone" />
|
|
293
|
-
</span>
|
|
294
|
-
);
|
|
295
|
-
if (!sprite) return label;
|
|
296
247
|
return (
|
|
297
|
-
<
|
|
298
|
-
<canvas
|
|
299
|
-
ref={canvasRef}
|
|
300
|
-
className={cx(
|
|
301
|
-
styles.bpSlotThumb,
|
|
302
|
-
preview && styles.bpPreviewThumb,
|
|
303
|
-
blank && styles.bpSlotThumbHidden
|
|
304
|
-
)}
|
|
305
|
-
aria-hidden="true"
|
|
306
|
-
/>
|
|
307
|
-
{blank ? label : null}
|
|
308
|
-
</span>
|
|
248
|
+
<ArtThumbnail art={sprite} name={name} preview={preview} fallbackIcon="clone" />
|
|
309
249
|
);
|
|
310
250
|
}
|
|
311
251
|
|
|
@@ -27,8 +27,6 @@ import { hasEditorModule, loadEditorModule } from './editorRegistry';
|
|
|
27
27
|
import { useLiveDeckFiles } from '../engine/liveReload';
|
|
28
28
|
import { collectAssets } from '../engine/assets';
|
|
29
29
|
import { MainEditor, styles } from '../engine/ui';
|
|
30
|
-
import { ImageViewer } from './ImageViewer';
|
|
31
|
-
import { MediaPlayer } from './MediaPlayer';
|
|
32
30
|
import { PxArtEditor } from './PxArtEditor';
|
|
33
31
|
import { SceneEditor } from './SceneEditor';
|
|
34
32
|
import { StyleEditor } from './StyleEditor';
|
|
@@ -321,15 +319,10 @@ export function SingleEditor({ path, editor, editorModule }) {
|
|
|
321
319
|
readOnly={readOnly}
|
|
322
320
|
/>
|
|
323
321
|
);
|
|
324
|
-
} else if (kind === 'image') {
|
|
325
|
-
// Viewers read their file's BYTES over the serve, not the text file map --
|
|
326
|
-
// `files` holds source, and an image decoded as utf-8 is nothing.
|
|
327
|
-
body = <ImageViewer path={path} />;
|
|
328
|
-
} else if (kind === 'audio' || kind === 'video') {
|
|
329
|
-
body = <MediaPlayer path={path} kind={kind} />;
|
|
330
322
|
}
|
|
331
|
-
//
|
|
332
|
-
//
|
|
323
|
+
// Image / audio / video are declared by castle.base and land in the
|
|
324
|
+
// DeclaredEditor branch above via editorModule. No `else`: code/text files
|
|
325
|
+
// are handled by the builtin shell code editor.
|
|
333
326
|
return (
|
|
334
327
|
<div className={styles.panelBare} style={{ position: 'relative' }}>
|
|
335
328
|
<MainEditor>{body}</MainEditor>
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Editors live under `editors/` by convention; that is what makes a third
|
|
11
11
|
// party's editor reachable from here without this kit knowing it exists.
|
|
12
|
+
import { createElement, useEffect, useRef } from 'react';
|
|
13
|
+
|
|
12
14
|
const modules = {
|
|
13
15
|
...import.meta.glob('/imports/*/editors/**/*.{jsx,js}'),
|
|
14
16
|
...import.meta.glob('/imports/*/code/editors/**/*.{jsx,js}'),
|
|
@@ -20,7 +22,35 @@ export function hasEditorModule(path) {
|
|
|
20
22
|
return Boolean(path && modules[`/${path}`]);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
//
|
|
25
|
+
// Base-kit (and bare-deck) editors default-export `mount(el, props)`. This kit's
|
|
26
|
+
// DeclaredEditor expects a React component. Arity distinguishes them: a mount
|
|
27
|
+
// takes the host element first; a React editor takes props only.
|
|
28
|
+
function wrapMountEditor(mount) {
|
|
29
|
+
return function MountEditorBridge(props) {
|
|
30
|
+
const ref = useRef(null);
|
|
31
|
+
const propsRef = useRef(props);
|
|
32
|
+
propsRef.current = props;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const node = ref.current;
|
|
35
|
+
if (!node) return undefined;
|
|
36
|
+
node.textContent = '';
|
|
37
|
+
// Mount once per open file. Callers that need live prop updates should
|
|
38
|
+
// read them themselves; remounting on every onChange identity would wipe
|
|
39
|
+
// viewers that hold zoom/pan state.
|
|
40
|
+
const cleanup = mount(node, propsRef.current);
|
|
41
|
+
return () => {
|
|
42
|
+
if (typeof cleanup === 'function') cleanup();
|
|
43
|
+
else node.textContent = '';
|
|
44
|
+
};
|
|
45
|
+
}, [props.path, mount]);
|
|
46
|
+
return createElement('div', {
|
|
47
|
+
ref,
|
|
48
|
+
style: { display: 'flex', flex: 1, width: '100%', height: '100%', minHeight: 0 },
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Load one, returning a React component. Throws a legible error rather than
|
|
24
54
|
// letting the caller see `undefined is not a function` three frames later.
|
|
25
55
|
export async function loadEditorModule(path) {
|
|
26
56
|
const load = modules[`/${path}`];
|
|
@@ -30,7 +60,11 @@ export async function loadEditorModule(path) {
|
|
|
30
60
|
);
|
|
31
61
|
}
|
|
32
62
|
const mod = await load();
|
|
33
|
-
const Editor = mod.default ?? mod.Editor;
|
|
63
|
+
const Editor = mod.default ?? mod.Editor ?? mod.mount;
|
|
34
64
|
if (!Editor) throw new Error(`"${path}" has no default export to render as an editor.`);
|
|
65
|
+
// Mount editors (base kit): (el, props) => void|cleanup. React editors: (props) => node.
|
|
66
|
+
if (typeof Editor === 'function' && Editor.length >= 2) {
|
|
67
|
+
return wrapMountEditor(Editor);
|
|
68
|
+
}
|
|
35
69
|
return Editor;
|
|
36
70
|
}
|