castle-web-cli 0.4.171 → 0.4.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/devSessionServer.js +9 -5
- package/dist/imports.js +2 -2
- package/dist/init.js +4 -4
- package/dist/serve.js +2 -0
- package/dist/serveSecurity.d.ts +2 -0
- package/dist/serveSecurity.js +17 -3
- package/kits/multiplayer-2d/CLAUDE.md +4 -0
- package/kits/multiplayer-2d/behaviors/Box.jsx +32 -9
- package/kits/multiplayer-2d/castle.json +3 -3
- package/kits/multiplayer-2d/code/client/avatars.js +6 -1
- 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 +14 -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 -0
- package/kits/multiplayer-3d/castle.json +3 -3
- package/kits/multiplayer-3d/code/client/avatars.js +42 -9
- package/kits/multiplayer-3d/code/server/sceneBodies.js +1 -1
- package/kits/multiplayer-3d/code/systems/multiplayer.js +17 -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 +185 -0
- 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 +17 -2
- package/kits/real-time/code/server/session.js +12 -6
- package/kits/real-time/package-lock.json +1 -1
- package/kits/turn-based/CLAUDE.md +4 -0
- package/kits/turn-based/castle.json +2 -2
- package/kits/turn-based/code/behaviors/Board.jsx +15 -5
- package/kits/turn-based/code/server/index.js +1 -0
- package/kits/turn-based/fonts.generated.js +1 -1
- package/kits/turn-based/package-lock.json +1 -1
- package/kits/turn-based/room.js +2 -0
- package/package.json +3 -1
- /package/kits/physics-2d/editors/{styleTheme.js → theme.js} +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
|
|
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;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// A player's avatar for drawing in a scene, read through `User.get` once per
|
|
2
|
+
// user id and kept for the page. Two pictures: the round one, the photo clipped
|
|
3
|
+
// to a circle with their frame over it, and the face, the photo alone, square,
|
|
4
|
+
// for a surface it should cover edge to edge. A player with no photo has no
|
|
5
|
+
// avatar, and a caller keeps its default look.
|
|
6
|
+
//
|
|
7
|
+
// `avatarCanvas` is for code that draws with a canvas context, like a Box.
|
|
8
|
+
// `avatarSpritePath` and `avatarFacePath` register a picture as image art in
|
|
9
|
+
// `scene.sprites`, so a `Sprite` or a model face can show it by file path.
|
|
10
|
+
|
|
11
|
+
import { User } from 'castle-web-sdk';
|
|
12
|
+
import { imageArt } from './art';
|
|
13
|
+
|
|
14
|
+
const SIZE = 128;
|
|
15
|
+
|
|
16
|
+
// A frame is drawn around a photo two thirds its size, the proportion Castle's
|
|
17
|
+
// own profile views use.
|
|
18
|
+
const FRAMED_INSET = SIZE / 6;
|
|
19
|
+
|
|
20
|
+
// userId -> { canvas, face }, present from the first ask; both fill in when the
|
|
21
|
+
// read completes.
|
|
22
|
+
const avatars = new Map();
|
|
23
|
+
|
|
24
|
+
// The avatar for one user id, or null until it is loaded or when there is none.
|
|
25
|
+
export function avatarCanvas(userId) {
|
|
26
|
+
return entryFor(userId)?.canvas ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The user's photo alone, square, or null as for `avatarCanvas`.
|
|
30
|
+
export function avatarFace(userId) {
|
|
31
|
+
return entryFor(userId)?.face ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function entryFor(userId) {
|
|
35
|
+
if (!userId) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
let entry = avatars.get(userId);
|
|
39
|
+
if (!entry) {
|
|
40
|
+
entry = { canvas: null, face: null };
|
|
41
|
+
avatars.set(userId, entry);
|
|
42
|
+
void load(userId, entry);
|
|
43
|
+
}
|
|
44
|
+
return entry;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The `scene.sprites` path holding this user's round avatar, or null while there
|
|
48
|
+
// is none. A `Sprite` with this `file` draws it.
|
|
49
|
+
export function avatarSpritePath(scene, userId) {
|
|
50
|
+
return registered(scene, `avatars/${userId}.png`, avatarCanvas(userId));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The same for the square face.
|
|
54
|
+
export function avatarFacePath(scene, userId) {
|
|
55
|
+
return registered(scene, `avatars/${userId}-face.png`, avatarFace(userId));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function registered(scene, path, canvas) {
|
|
59
|
+
if (!canvas) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (!scene.sprites[path]) {
|
|
63
|
+
scene.sprites[path] = imageArt(path, canvas.toDataURL());
|
|
64
|
+
}
|
|
65
|
+
return path;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function load(userId, entry) {
|
|
69
|
+
try {
|
|
70
|
+
const user = await User.get(userId);
|
|
71
|
+
const [photo, frame] = await Promise.all([loadImage(user?.photoUrl), loadImage(user?.frameUrl)]);
|
|
72
|
+
if (photo) {
|
|
73
|
+
entry.canvas = compose(photo, frame);
|
|
74
|
+
entry.face = square(photo);
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// A failed read leaves the entry without a canvas; the next page asks again.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadImage(url) {
|
|
82
|
+
if (!url) {
|
|
83
|
+
return Promise.resolve(null);
|
|
84
|
+
}
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const image = new Image();
|
|
87
|
+
image.onload = () => resolve(image);
|
|
88
|
+
image.onerror = () => resolve(null);
|
|
89
|
+
image.src = url;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// How much of the opaque box is cut from each side, so a round photo's edge and
|
|
94
|
+
// its anti-aliased rim stay off the face.
|
|
95
|
+
const FACE_INSET = 0.12;
|
|
96
|
+
|
|
97
|
+
// The opaque box is never taken smaller than this share of the photo on either
|
|
98
|
+
// axis. Transparency reaching far inward would otherwise blow a few pixels up
|
|
99
|
+
// to the whole face.
|
|
100
|
+
const FACE_MIN = 0.55;
|
|
101
|
+
|
|
102
|
+
// The photo cropped to its opaque pixels and then inset, filling the square.
|
|
103
|
+
// Profile photos are circles on a transparent ground, and a face texture wants
|
|
104
|
+
// no corners and no rim.
|
|
105
|
+
function square(photo) {
|
|
106
|
+
const canvas = document.createElement('canvas');
|
|
107
|
+
canvas.width = SIZE;
|
|
108
|
+
canvas.height = SIZE;
|
|
109
|
+
const ctx = canvas.getContext('2d');
|
|
110
|
+
ctx.drawImage(photo, 0, 0, SIZE, SIZE);
|
|
111
|
+
const box = atLeast(opaqueBox(ctx.getImageData(0, 0, SIZE, SIZE)), SIZE * FACE_MIN, SIZE);
|
|
112
|
+
const inset = Math.round(Math.min(box.width, box.height) * FACE_INSET);
|
|
113
|
+
const face = {
|
|
114
|
+
x: box.x + inset,
|
|
115
|
+
y: box.y + inset,
|
|
116
|
+
width: box.width - 2 * inset,
|
|
117
|
+
height: box.height - 2 * inset,
|
|
118
|
+
};
|
|
119
|
+
if (face.width <= 0 || face.height <= 0) {
|
|
120
|
+
return canvas;
|
|
121
|
+
}
|
|
122
|
+
const source = document.createElement('canvas');
|
|
123
|
+
source.width = SIZE;
|
|
124
|
+
source.height = SIZE;
|
|
125
|
+
source.getContext('2d').drawImage(canvas, 0, 0);
|
|
126
|
+
ctx.clearRect(0, 0, SIZE, SIZE);
|
|
127
|
+
ctx.drawImage(source, face.x, face.y, face.width, face.height, 0, 0, SIZE, SIZE);
|
|
128
|
+
return canvas;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The box grown around its centre to at least `min` on each axis, kept inside
|
|
132
|
+
// `size`.
|
|
133
|
+
function atLeast(box, min, size) {
|
|
134
|
+
const out = { ...box };
|
|
135
|
+
for (const [at, extent] of [
|
|
136
|
+
['x', 'width'],
|
|
137
|
+
['y', 'height'],
|
|
138
|
+
]) {
|
|
139
|
+
if (out[extent] < min) {
|
|
140
|
+
out[at] = Math.round(Math.min(Math.max(0, out[at] + out[extent] / 2 - min / 2), size - min));
|
|
141
|
+
out[extent] = Math.round(min);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The smallest box holding every pixel with alpha over a small threshold.
|
|
148
|
+
function opaqueBox({ data, width, height }) {
|
|
149
|
+
let left = width;
|
|
150
|
+
let top = height;
|
|
151
|
+
let right = -1;
|
|
152
|
+
let bottom = -1;
|
|
153
|
+
for (let y = 0; y < height; y += 1) {
|
|
154
|
+
for (let x = 0; x < width; x += 1) {
|
|
155
|
+
if (data[(y * width + x) * 4 + 3] > 16) {
|
|
156
|
+
left = Math.min(left, x);
|
|
157
|
+
top = Math.min(top, y);
|
|
158
|
+
right = Math.max(right, x);
|
|
159
|
+
bottom = Math.max(bottom, y);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (right < 0) {
|
|
164
|
+
return { x: 0, y: 0, width, height };
|
|
165
|
+
}
|
|
166
|
+
return { x: left, y: top, width: right - left + 1, height: bottom - top + 1 };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function compose(photo, frame) {
|
|
170
|
+
const canvas = document.createElement('canvas');
|
|
171
|
+
canvas.width = SIZE;
|
|
172
|
+
canvas.height = SIZE;
|
|
173
|
+
const ctx = canvas.getContext('2d');
|
|
174
|
+
const inset = frame ? FRAMED_INSET : 0;
|
|
175
|
+
ctx.save();
|
|
176
|
+
ctx.beginPath();
|
|
177
|
+
ctx.arc(SIZE / 2, SIZE / 2, SIZE / 2 - inset, 0, Math.PI * 2);
|
|
178
|
+
ctx.clip();
|
|
179
|
+
ctx.drawImage(photo, inset, inset, SIZE - 2 * inset, SIZE - 2 * inset);
|
|
180
|
+
ctx.restore();
|
|
181
|
+
if (frame) {
|
|
182
|
+
ctx.drawImage(frame, 0, 0, SIZE, SIZE);
|
|
183
|
+
}
|
|
184
|
+
return canvas;
|
|
185
|
+
}
|
|
@@ -119,13 +119,13 @@ export function mediaFilesOfKind(kind) {
|
|
|
119
119
|
}
|
|
120
120
|
// Which editor a file opens in when no `editorModule` was resolved. Media
|
|
121
121
|
// viewing (image / audio / video) lives on castle.base now; this only covers
|
|
122
|
-
// types this kit still routes itself (scene / sprite /
|
|
122
|
+
// types this kit still routes itself (scene / sprite / theme). Each extension
|
|
123
123
|
// gets its own line: legacy tooling matched one `endsWith` per `return`.
|
|
124
124
|
export function getFileKind(path) {
|
|
125
125
|
if (path.endsWith('.scene')) return 'scene';
|
|
126
126
|
if (path.endsWith('.sprite')) return 'pxart';
|
|
127
127
|
if (path.endsWith('.pxart')) return 'pxart';
|
|
128
|
-
if (path.endsWith('.style')) return '
|
|
128
|
+
if (path.endsWith('.style')) return 'theme';
|
|
129
129
|
if (path.endsWith('.js') || path.endsWith('.jsx')) return 'code';
|
|
130
130
|
return 'text';
|
|
131
131
|
}
|
|
@@ -147,7 +147,7 @@ export function patchBody(body, actor) {
|
|
|
147
147
|
// used the retired `kind: 'pickup'` label are still honored as sensors.
|
|
148
148
|
body.isSensor = Boolean(collider.isTrigger || collider.kind === 'pickup');
|
|
149
149
|
body.frictionAir = rb?.drag ?? 0.01;
|
|
150
|
-
body.plugin.gravityScale = rb?.gravityScale ??
|
|
150
|
+
body.plugin.gravityScale = rb?.gravityScale ?? 0;
|
|
151
151
|
body.plugin.angularDrag = rb?.angularDrag ?? 0;
|
|
152
152
|
// Density sets mass (= density x area) -- DYNAMIC bodies only. On a static or
|
|
153
153
|
// kinematic body, setDensity would replace the infinite mass that setStatic
|
|
@@ -172,7 +172,7 @@ export function syncBodyToPoint(body, point, angleDeg) {
|
|
|
172
172
|
export function applyWorldGravity(bodies, gravity) {
|
|
173
173
|
for (const body of bodies) {
|
|
174
174
|
if (body.isStatic || body.isSleeping) continue;
|
|
175
|
-
const scale = (body.plugin.gravityScale ??
|
|
175
|
+
const scale = (body.plugin.gravityScale ?? 0) * gravity.scale;
|
|
176
176
|
body.force.x += body.mass * gravity.x * scale;
|
|
177
177
|
body.force.y += body.mass * gravity.y * scale;
|
|
178
178
|
}
|
|
@@ -66,7 +66,7 @@ export class SceneRuntime {
|
|
|
66
66
|
this.camera = undefined;
|
|
67
67
|
this.status = undefined;
|
|
68
68
|
// The deck's font as a CSS font-family value, always usable: the face
|
|
69
|
-
// picked in the
|
|
69
|
+
// picked in the Theme editor, or a plain sans stack when there is none.
|
|
70
70
|
// For `draw` / `drawUi` hooks -- `ctx.font = `20px ${scene.font}`` -- since
|
|
71
71
|
// canvas takes a family name, not a class. Prefer `drawUi` for HUD text;
|
|
72
72
|
// the deprecated React `ui` overlay still inherits this face via CSS.
|
|
@@ -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
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import { SelectField } from '@imports/castle.physics-2d/engine/ui';
|
|
8
8
|
import { AutoFields, overrideProps } from '@imports/castle.physics-2d/engine/autoInspector';
|
|
9
|
+
import { POSITIVE } from '@imports/castle.physics-2d/engine/propertyRanges';
|
|
9
10
|
|
|
10
11
|
const TYPE_OPTIONS = [
|
|
11
12
|
{ value: 'fixed', label: 'Fixed' },
|
|
@@ -25,6 +26,7 @@ function BodyInspector({ component, setComponent, override }) {
|
|
|
25
26
|
/>
|
|
26
27
|
<AutoFields
|
|
27
28
|
defaultProps={Body.defaultProps}
|
|
29
|
+
meta={Body.propertyMeta}
|
|
28
30
|
component={component}
|
|
29
31
|
setComponent={setComponent}
|
|
30
32
|
exclude={['type']}
|
|
@@ -41,10 +43,17 @@ export class Body {
|
|
|
41
43
|
type: 'fixed', // 'fixed' | 'dynamic' | 'kinematic'
|
|
42
44
|
friction: 0.6,
|
|
43
45
|
restitution: 0,
|
|
44
|
-
|
|
46
|
+
// Match physics-2d RigidBody: newly added dynamics float until opted in.
|
|
47
|
+
gravityScale: 0,
|
|
45
48
|
lockRotations: false,
|
|
46
49
|
};
|
|
47
50
|
|
|
51
|
+
static propertyMeta = {
|
|
52
|
+
friction: POSITIVE,
|
|
53
|
+
restitution: POSITIVE,
|
|
54
|
+
gravityScale: { step: 0.1 },
|
|
55
|
+
};
|
|
56
|
+
|
|
48
57
|
constructor(props) {
|
|
49
58
|
this.props = props;
|
|
50
59
|
}
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
export class Door {
|
|
5
5
|
static behaviorName = 'Door';
|
|
6
6
|
static defaultProps = { range: 3, slide: 2.1, speed: 2.5 };
|
|
7
|
+
static propertyMeta = {
|
|
8
|
+
range: { min: 0, step: 0.1 },
|
|
9
|
+
slide: { min: 0, step: 0.1 },
|
|
10
|
+
speed: { min: 0, step: 0.1 },
|
|
11
|
+
};
|
|
7
12
|
|
|
8
13
|
constructor(props) {
|
|
9
14
|
this.props = props;
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
export class Model {
|
|
8
8
|
static behaviorName = 'Model';
|
|
9
9
|
static defaultProps = { file: '', scale: 1, yOffset: 0 };
|
|
10
|
+
static propertyMeta = {
|
|
11
|
+
scale: { min: 0, step: 0.05 },
|
|
12
|
+
yOffset: { step: 0.05 },
|
|
13
|
+
};
|
|
10
14
|
|
|
11
15
|
constructor(props) {
|
|
12
16
|
this.props = props;
|
|
@@ -14,6 +14,10 @@ function pickupKey(scene, actor) {
|
|
|
14
14
|
export class Pickup {
|
|
15
15
|
static behaviorName = 'Pickup';
|
|
16
16
|
static defaultProps = { bob: 0.12, spin: 120 };
|
|
17
|
+
static propertyMeta = {
|
|
18
|
+
bob: { min: 0, step: 0.01 },
|
|
19
|
+
spin: { min: 0, step: 5 },
|
|
20
|
+
};
|
|
17
21
|
|
|
18
22
|
constructor(props) {
|
|
19
23
|
this.props = props;
|
|
@@ -112,6 +112,13 @@ function findLookText(scene, actor, t, facing) {
|
|
|
112
112
|
export class Player {
|
|
113
113
|
static behaviorName = 'Player';
|
|
114
114
|
static defaultProps = { speed: 6, turnSpeed: 2.4, jumpSpeed: 8, camDistance: 9, camHeight: 7 };
|
|
115
|
+
static propertyMeta = {
|
|
116
|
+
speed: { min: 0, step: 0.5 },
|
|
117
|
+
turnSpeed: { min: 0, step: 0.1 },
|
|
118
|
+
jumpSpeed: { min: 0, step: 0.5 },
|
|
119
|
+
camDistance: { min: 0, step: 0.5 },
|
|
120
|
+
camHeight: { min: 0, step: 0.5 },
|
|
121
|
+
};
|
|
115
122
|
|
|
116
123
|
constructor(props) {
|
|
117
124
|
this.props = props;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import { SelectField, TextField } from '@imports/castle.physics-2d/engine/ui';
|
|
10
10
|
import { AutoFields, overrideProps } from '@imports/castle.physics-2d/engine/autoInspector';
|
|
11
|
+
import { POSITIVE, UNIT } from '@imports/castle.physics-2d/engine/propertyRanges';
|
|
11
12
|
|
|
12
13
|
const KIND_OPTIONS = [
|
|
13
14
|
{ value: 'box', label: 'Box' },
|
|
@@ -39,6 +40,7 @@ function ShapeInspector({ component, setComponent, override }) {
|
|
|
39
40
|
)}
|
|
40
41
|
<AutoFields
|
|
41
42
|
defaultProps={Shape.defaultProps}
|
|
43
|
+
meta={Shape.propertyMeta}
|
|
42
44
|
component={component}
|
|
43
45
|
setComponent={setComponent}
|
|
44
46
|
exclude={['kind', 'art', 'artRepeat']}
|
|
@@ -69,6 +71,17 @@ export class Shape {
|
|
|
69
71
|
solid: false,
|
|
70
72
|
};
|
|
71
73
|
|
|
74
|
+
static propertyMeta = {
|
|
75
|
+
width: POSITIVE,
|
|
76
|
+
height: POSITIVE,
|
|
77
|
+
depth: POSITIVE,
|
|
78
|
+
radius: POSITIVE,
|
|
79
|
+
roughness: UNIT,
|
|
80
|
+
metalness: UNIT,
|
|
81
|
+
clearcoat: UNIT,
|
|
82
|
+
emissiveIntensity: POSITIVE,
|
|
83
|
+
};
|
|
84
|
+
|
|
72
85
|
constructor(props) {
|
|
73
86
|
this.props = props;
|
|
74
87
|
}
|
|
@@ -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
|