castle-web-cli 0.4.176 → 0.4.178
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-failures.js +4 -4
- package/dist/agent-prompts.d.ts +9 -1
- package/dist/agent-prompts.js +15 -2
- package/dist/agent.js +98 -34
- package/dist/deckLocatorShape.d.ts +16 -0
- package/dist/deckLocatorShape.js +28 -0
- package/dist/editorConfig.d.ts +1 -0
- package/dist/serve.js +3 -1
- package/dist/shell/assets/index-C890YbXX.css +1 -0
- package/dist/shell/assets/index-z6shfW4S.js +447 -0
- package/dist/shell/index.html +2 -2
- package/kits/base/CLAUDE.md +3 -0
- package/kits/base/castle.json +21 -11
- package/kits/physics-2d/CLAUDE.md +41 -8
- package/kits/physics-2d/behaviors/AnalogStick.jsx +80 -4
- package/kits/physics-2d/behaviors/Slingshot.jsx +14 -2
- package/kits/physics-2d/behaviors/Sprite.jsx +17 -8
- package/kits/physics-2d/behaviors/Style.jsx +270 -0
- package/kits/physics-2d/behaviors/Text.jsx +213 -0
- package/kits/physics-2d/behaviors/Video.jsx +8 -4
- package/kits/physics-2d/blueprints/text.scene +12 -0
- package/kits/physics-2d/castle.json +10 -6
- package/kits/physics-2d/editors/SceneEditor.jsx +41 -0
- package/kits/physics-2d/editors/deckFont.js +65 -55
- package/kits/physics-2d/editors/fontPreview.js +6 -38
- package/kits/physics-2d/editors/pixelInspector.jsx +4 -154
- package/kits/physics-2d/engine/blueprint.js +28 -0
- package/kits/physics-2d/engine/fonts.js +125 -23
- package/kits/physics-2d/engine/paletteField.jsx +235 -0
- package/kits/physics-2d/engine/physics/controls.js +5 -81
- package/kits/physics-2d/engine/popoverDismiss.js +17 -0
- package/kits/physics-2d/engine/scene.js +12 -3
- package/kits/physics-2d/engine/spriteField.jsx +2 -15
- package/kits/physics-2d/engine/tap.js +90 -0
- package/kits/physics-2d/engine/text.js +94 -0
- package/kits/physics-2d/engine/ui.jsx +20 -0
- package/kits/physics-2d/engine/ui.module.css +10 -2
- package/kits/physics-3d/castle.json +4 -2
- package/package.json +7 -3
- package/dist/shell/assets/index-BWOEraUy.js +0 -447
- package/dist/shell/assets/index-BkVF1OXc.css +0 -1
package/dist/shell/index.html
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
<link rel="icon" type="image/png" sizes="32x32" href="/__castle/ide/favicon-32x32.png" />
|
|
11
11
|
<link rel="icon" type="image/png" sizes="16x16" href="/__castle/ide/favicon-16x16.png" />
|
|
12
12
|
<link rel="icon" href="/__castle/ide/favicon.ico" sizes="any" />
|
|
13
|
-
<script type="module" crossorigin src="/__castle/ide/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/__castle/ide/assets/index-z6shfW4S.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-C890YbXX.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/kits/base/CLAUDE.md
CHANGED
|
@@ -122,6 +122,9 @@ bottleneck.
|
|
|
122
122
|
- `unsupported` — a refusal: why this deck must NOT be built on this
|
|
123
123
|
format, shown when someone tries to upload one. Only for formats you
|
|
124
124
|
recognize and reject.
|
|
125
|
+
- `width` — preferred card width in px when the shell opens this type's
|
|
126
|
+
editor in a new pane. A layout hint: the shell caps it to the window
|
|
127
|
+
and ignores it on phones. Absent means the shell's default.
|
|
125
128
|
|
|
126
129
|
2. **Write the module** at the declared path. It is handed the file and a way
|
|
127
130
|
to save it; how it renders is yours.
|
package/kits/base/castle.json
CHANGED
|
@@ -12,61 +12,71 @@
|
|
|
12
12
|
"ext": ".png",
|
|
13
13
|
"label": "Image",
|
|
14
14
|
"icon": "file-image",
|
|
15
|
-
"editor": "editors/ImageViewer.js"
|
|
15
|
+
"editor": "editors/ImageViewer.js",
|
|
16
|
+
"width": 480
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
19
|
"ext": ".jpg",
|
|
19
20
|
"label": "Image",
|
|
20
21
|
"icon": "file-image",
|
|
21
|
-
"editor": "editors/ImageViewer.js"
|
|
22
|
+
"editor": "editors/ImageViewer.js",
|
|
23
|
+
"width": 480
|
|
22
24
|
},
|
|
23
25
|
{
|
|
24
26
|
"ext": ".jpeg",
|
|
25
27
|
"label": "Image",
|
|
26
28
|
"icon": "file-image",
|
|
27
|
-
"editor": "editors/ImageViewer.js"
|
|
29
|
+
"editor": "editors/ImageViewer.js",
|
|
30
|
+
"width": 480
|
|
28
31
|
},
|
|
29
32
|
{
|
|
30
33
|
"ext": ".gif",
|
|
31
34
|
"label": "Image",
|
|
32
35
|
"icon": "file-image",
|
|
33
|
-
"editor": "editors/ImageViewer.js"
|
|
36
|
+
"editor": "editors/ImageViewer.js",
|
|
37
|
+
"width": 480
|
|
34
38
|
},
|
|
35
39
|
{
|
|
36
40
|
"ext": ".webp",
|
|
37
41
|
"label": "Image",
|
|
38
42
|
"icon": "file-image",
|
|
39
|
-
"editor": "editors/ImageViewer.js"
|
|
43
|
+
"editor": "editors/ImageViewer.js",
|
|
44
|
+
"width": 480
|
|
40
45
|
},
|
|
41
46
|
{
|
|
42
47
|
"ext": ".svg",
|
|
43
48
|
"label": "Image",
|
|
44
49
|
"icon": "file-image",
|
|
45
|
-
"editor": "editors/ImageViewer.js"
|
|
50
|
+
"editor": "editors/ImageViewer.js",
|
|
51
|
+
"width": 480
|
|
46
52
|
},
|
|
47
53
|
{
|
|
48
54
|
"ext": ".mp3",
|
|
49
55
|
"label": "Audio",
|
|
50
56
|
"icon": "file-audio",
|
|
51
|
-
"editor": "editors/MediaPlayer.js"
|
|
57
|
+
"editor": "editors/MediaPlayer.js",
|
|
58
|
+
"width": 360
|
|
52
59
|
},
|
|
53
60
|
{
|
|
54
61
|
"ext": ".wav",
|
|
55
62
|
"label": "Audio",
|
|
56
63
|
"icon": "file-audio",
|
|
57
|
-
"editor": "editors/MediaPlayer.js"
|
|
64
|
+
"editor": "editors/MediaPlayer.js",
|
|
65
|
+
"width": 360
|
|
58
66
|
},
|
|
59
67
|
{
|
|
60
68
|
"ext": ".m4a",
|
|
61
69
|
"label": "Audio",
|
|
62
70
|
"icon": "file-audio",
|
|
63
|
-
"editor": "editors/MediaPlayer.js"
|
|
71
|
+
"editor": "editors/MediaPlayer.js",
|
|
72
|
+
"width": 360
|
|
64
73
|
},
|
|
65
74
|
{
|
|
66
75
|
"ext": ".mp4",
|
|
67
76
|
"label": "Video",
|
|
68
77
|
"icon": "file-video",
|
|
69
|
-
"editor": "editors/MediaPlayer.js"
|
|
78
|
+
"editor": "editors/MediaPlayer.js",
|
|
79
|
+
"width": 560
|
|
70
80
|
}
|
|
71
81
|
]
|
|
72
82
|
},
|
|
@@ -74,7 +84,7 @@
|
|
|
74
84
|
"title": "base",
|
|
75
85
|
"deckId": "yRcmH4_aYllE",
|
|
76
86
|
"cardId": "zkFdingmIm3m",
|
|
77
|
-
"publishedVersion": "2026-09-
|
|
87
|
+
"publishedVersion": "2026-09-11T20:21:04.335Z",
|
|
78
88
|
"provides": [
|
|
79
89
|
"castle-web-sdk"
|
|
80
90
|
]
|
|
@@ -22,11 +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
|
+
- **Text** — place the kit preset `@imports/castle.physics-2d/blueprints/text.scene`, or add a `Text` component (optional `font`; chrome via sibling `Style`). Dynamic text is `actor.components.Text.text = …`. A sibling `onTap(actor, scene)` fires when that actor is tapped — no extra behavior.
|
|
25
26
|
- 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
27
|
- This kit is plain JavaScript. Use `.jsx` for files with JSX, `.js` otherwise; do not add TypeScript files or a new build step.
|
|
27
28
|
- Space is reserved by the editor for play/stop; do not bind Space to gameplay.
|
|
28
29
|
- 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
|
-
- 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
|
+
- Do not read `engine/`, `editors/`, or built-in behaviors (`Layout.jsx`, `Sprite.jsx`, `Text.jsx`, `Style.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
31
|
- 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
32
|
- Details below: `## Behavior shape`, `## Scene file`, `## Blueprints`, `## Built-in behaviors`, `## Creating pixel art`, `## SceneRuntime API`, and `## Input shortcuts`.
|
|
32
33
|
|
|
@@ -74,6 +75,9 @@ These are ALL the fields that exist — do not invent others:
|
|
|
74
75
|
- `unsupported` — a refusal: why this deck must NOT be built on this
|
|
75
76
|
format, shown when someone tries to upload one. Only for formats you
|
|
76
77
|
recognize and reject.
|
|
78
|
+
- `width` — preferred card width in px when the shell opens this type's
|
|
79
|
+
editor in a new pane. A layout hint: the shell caps it to the window
|
|
80
|
+
and ignores it on phones. Absent means the shell's default.
|
|
77
81
|
|
|
78
82
|
2. **Write the module** at the declared path, default-exporting a component:
|
|
79
83
|
|
|
@@ -291,6 +295,10 @@ export class MyThing {
|
|
|
291
295
|
// For letting go of anything held outside your own props.
|
|
292
296
|
onDisable(actor, scene) {}
|
|
293
297
|
onEnable(actor, scene) {}
|
|
298
|
+
|
|
299
|
+
// Optional. Fired on pointer release inside this actor (Collider, else
|
|
300
|
+
// Layout box). No extra component — implementing the method is the opt-in.
|
|
301
|
+
onTap(actor, scene) {}
|
|
294
302
|
}
|
|
295
303
|
```
|
|
296
304
|
|
|
@@ -383,12 +391,16 @@ Template `components` keys are behavior names (the `static behaviorName`); add a
|
|
|
383
391
|
- **Never write inline actors** (full `components`, no `blueprint` field). They aren't a lighter-weight alternative: the editor auto-migrates every such actor into its OWN new blueprint file the first time a human opens the scene — mechanically, one blueprint per actor, no dedup. That migration is a compatibility net for pre-blueprint decks, not an authoring workflow; leaning on it turns 10 inline enemies into 10 junk blueprints.
|
|
384
392
|
- Never invent your own `blueprint` path pointing at a file you didn't also create — a dangling reference resolves to "no template" (the instance's own sparse `components` render alone, missing whatever it expected to inherit).
|
|
385
393
|
|
|
394
|
+
The kit also ships a few presets under `blueprints/` (e.g. `text.scene`, `ball.scene`, `cauldron.scene`). Reference one from a deck as `"blueprint": "@imports/castle.physics-2d/blueprints/text.scene"`.
|
|
395
|
+
|
|
386
396
|
## Built-in behaviors
|
|
387
397
|
|
|
388
398
|
- **Layout** — `{ x, y, width, height, z?, rotation? }`. Every actor needs one. `z` orders draw (low first). `rotation` is degrees about the center.
|
|
389
399
|
- **Sprite** — `{ file: "drawings/foo.sprite", tint?: "#rrggbbaa", playing?: true, tag?: "", mode?: 'cover'|'fit'|'stretch'|'tile', tileSize?: 50 }`. Renders the deck's 2D art into the Layout box. `file` is either a `.sprite` pixel-art sprite or an uploaded **image file** (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`) — every prop below works the same on both, except that an image is a single still (no frames or tags) and is drawn smoothed rather than nearest-neighbour, which is what you want for a photo and not for pixel art. Draw modes: scaled up preserving the art's aspect ratio to fill the whole box, cropping whatever overflows a mismatched box (`mode: 'cover'`, the default -- CSS object-fit: cover), scaled preserving the art's aspect ratio and centered so the whole sprite stays visible, letterboxing a mismatched box instead of cropping (`mode: 'fit'` -- CSS object-fit: contain), scaled to fill the box exactly and distorting the art when the aspect ratios don't match (`mode: 'stretch'`), or repeated (`mode: 'tile'`). `tint` multiplies; use white (`#ffffffff`) or omit for the original colors. Animated sprites play automatically; set `playing: false` to hold the first frame, or `tag` to play a named animation tag. Tile mode repeats the art at a fixed cell size (`tileSize` card units tall, width scaled by the art's aspect) instead of stretching it across the Layout box.
|
|
390
400
|
- **Missing sprites fall back to a placeholder.** If `file` names a sprite that doesn't exist yet, it renders the fallback `drawings/cauldron.sprite` — so you can give an actor its REAL intended sprite name (`drawings/paddle.sprite`) right away and it shows the placeholder until that file is created. Reference real names from the start; don't wait for the art.
|
|
391
401
|
- **Fill the box, don't distort.** The default `mode: 'cover'` fills the Layout box with the art undistorted, cropping whatever overflows when the box aspect doesn't match the art's — so treat the Layout box like an image frame in a design tool: size it to frame what matters and keep the sprite's important content toward the center, since the edges may be cropped. Keeping an actor's Layout aspect ratio close to its sprite's native aspect ratio minimizes how much gets cropped. When the whole sprite must stay visible (nothing is safe to crop), use `mode: 'fit'`, which preserves the art and letterboxes the mismatched box instead. Avoid `mode: 'stretch'` for pixel art — filling a long/tall box by distorting wrecks the pixels (a brick sprite stretched into a wall ruins the bricks). For long surfaces (walls, floors, platforms), use `mode: 'tile'` on one actor to repeat the art at a fixed cell size instead of stretching or cropping a single sprite.
|
|
402
|
+
- **Text** — `{ text, font?: '', size?: 24, color?: '#131313', align?: 'left'|'center'|'right', verticalAlign?: 'top'|'middle'|'bottom', lineHeight?: 1.25, space?: 'world'|'screen', outlineWidth?: 0, outlineColor?: '#ffffff' }`. Draws wrapped text into the Layout box (wrap width = box minus sibling `Style.padding`; lines may overflow vertically — no clipping, no auto-resize). `font: ''` (default) uses the deck font; pick any Castle face from the inspector and it is added to `fonts.generated.js`. `space: 'world'` scrolls with the camera; `space: 'screen'` is HUD via `drawUi`. `color` / `outlineColor` are deck-palette hexes (`''` = none — skip fill or stroke). Fill/border/shadow/opacity/padding live on sibling **Style**. Hide with `"$enabled": false`. Kit preset: `@imports/castle.physics-2d/blueprints/text.scene`. Change the string from deck code with `actor.components.Text.text = '…'`.
|
|
403
|
+
- **Style** — `{ background?: '', border?: '', borderWidth?: 0, borderRadius?: 0, padding?: 0, shadowX?: 0, shadowY?: 0, shadowBlur?: 0, shadowColor?: '', opacity?: 1 }`. Shared chrome for **Text** and **Sprite**: background fill, border, corner radius, drop shadow (`shadowColor` + offsets + blur; empty color = no shadow), content padding, and opacity. Draws behind content. `background` / `border` are palette hexes (`''` = none). Add it beside Text or Sprite from the inspector.
|
|
392
404
|
- **Collider** — `{ kind: 'solid'|'pickup', mode?: 'auto'|'manual', width, height, offsetX?, offsetY?, debug? }`. Just a rect; `offsetX`/`offsetY` nudge the collider from its default position (so `0,0` is the default). `mode: 'auto'` (the default) fits the rect to the actor's Sprite's opaque pixels, mapped through the Sprite's draw mode (`fit`'s letterboxed dest rect, `stretch`'s full Layout box, `cover`'s filled box clipped to the Layout box; `tile` mode and a missing/unresolved Sprite use the whole Layout box instead). `width`/`height` only apply in `mode: 'manual'` (a rect of that size, centered in the Layout box); they're ignored in `auto`. The framework does NOT auto-resolve collisions for you. Use it as data:
|
|
393
405
|
|
|
394
406
|
```jsx
|
|
@@ -492,8 +504,10 @@ behavior imported for itself — before the first frame, so `ctx.font` is safe t
|
|
|
492
504
|
set from the very first `draw`. Each face costs its own 12–90 KB in the bundle,
|
|
493
505
|
so two or three in total is the shape to aim for.
|
|
494
506
|
|
|
495
|
-
|
|
496
|
-
|
|
507
|
+
Use the **Text** behavior for labels and HUD strings (optional per-actor `font`,
|
|
508
|
+
plus sibling **Style** for fill/border/padding). Hand-drawn canvas text
|
|
509
|
+
(`ctx.fillText` in `draw` / `drawUi`) is for per-glyph effects or text that
|
|
510
|
+
follows a body in a way the Layout box cannot.
|
|
497
511
|
|
|
498
512
|
## Physics
|
|
499
513
|
|
|
@@ -525,8 +539,7 @@ make it move.**
|
|
|
525
539
|
- `bodyType: 'dynamic' | 'static' | 'kinematic'`. `dynamic` = simulated
|
|
526
540
|
(gravity, collisions, forces). `static` = never moves (same as a lone
|
|
527
541
|
Collider). `kinematic` = unpushable, but you move it by writing its Layout.
|
|
528
|
-
- `gravityScale` — multiplies world gravity for this body (`0` = floats
|
|
529
|
-
default, so newly added bodies don't fall until you opt in).
|
|
542
|
+
- `gravityScale` — multiplies world gravity for this body (`0` = floats).
|
|
530
543
|
- `drag` — linear damping (air resistance). `angularDrag` — spin damping.
|
|
531
544
|
- `freezeRotation` — keep it from rotating (stays upright).
|
|
532
545
|
- `velocityX` / `velocityY` — initial velocity, applied once when play starts.
|
|
@@ -601,6 +614,26 @@ export class StickSlowDownToggle {
|
|
|
601
614
|
}
|
|
602
615
|
```
|
|
603
616
|
|
|
617
|
+
### Tap hook
|
|
618
|
+
|
|
619
|
+
Any behavior may implement `onTap(actor, scene)`. No extra component: if an
|
|
620
|
+
enabled behavior on the actor has the method, a pointer release inside the hit
|
|
621
|
+
zone fires it on every sibling that implements it (Collider when present, else
|
|
622
|
+
the Layout box; screen-space Text uses screen coords). The finger is claimed so
|
|
623
|
+
it does not also drive a stick or a grab-anywhere sling. A targeted control
|
|
624
|
+
(Draggable, a sling pressed on its own body) still wins. An actor with no
|
|
625
|
+
`onTap` does not claim the finger.
|
|
626
|
+
|
|
627
|
+
```jsx
|
|
628
|
+
export class TapCounter {
|
|
629
|
+
static behaviorName = 'TapCounter';
|
|
630
|
+
onTap(actor, scene) {
|
|
631
|
+
const text = actor.components.Text;
|
|
632
|
+
if (text) text.text = String(Number(text.text || 0) + 1);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
604
637
|
### Built-in controls (add these behaviors, no code needed)
|
|
605
638
|
|
|
606
639
|
These read input and drive the actor through the physics system, so the actor
|
|
@@ -650,7 +683,7 @@ see the SceneRuntime API below.
|
|
|
650
683
|
### Recipe: a launch-and-bounce game
|
|
651
684
|
|
|
652
685
|
- Ball blueprint: `Layout` (square), `Sprite`, `Collider {shape:'circle',
|
|
653
|
-
bounciness:0.6}`, `RigidBody {bodyType:'dynamic'
|
|
686
|
+
bounciness:0.6}`, `RigidBody {bodyType:'dynamic'}`, `Slingshot {}`.
|
|
654
687
|
- Walls/floor blueprint: `Layout`, `Collider {bounciness:0.4}` (no RigidBody →
|
|
655
688
|
static).
|
|
656
689
|
- Goal blueprint: `Collider {isTrigger:true}` + a `Goal` behavior with
|
|
@@ -853,14 +886,14 @@ if (scene.keys.has('KeyX')) /* launch ball */ ;
|
|
|
853
886
|
|
|
854
887
|
**Space is reserved** — the editor binds it to the play/stop toggle, so don't bind Space to a gameplay action (jump / shoot / launch / ...). Use arrows, WASD, letter keys, or on-screen buttons instead.
|
|
855
888
|
|
|
856
|
-
For HUD text
|
|
889
|
+
For HUD text prefer a `Text` actor with `space: 'screen'` (or `drawUi` for custom canvas HUD). Only in-world text or shapes go in `draw`. Both read `scene.font` — see `## Deck font`. A tappable label is `Text` plus a sibling with `onTap`. The React `ui()` hook is deprecated.
|
|
857
890
|
|
|
858
891
|
## Common breakout-shaped recipe (sketch)
|
|
859
892
|
|
|
860
893
|
- `Paddle` behavior: read keys, clamp x to `[0, 500 - layout.width]`.
|
|
861
894
|
- `Ball` behavior: store `vx, vy` on `actor.runtime`; integrate; bounce off wall edges (`x<0`, `x+w>500`, `y<0`); on `scene.overlaps(actor, paddle)`, flip `vy`; for each `brick` in `scene.actorsWith('Brick')` check `scene.overlaps(actor, brick)` → flip `vy` and `scene.despawnActor(brick.id)`; if `y > 700` lose a life.
|
|
862
895
|
- `Brick` behavior: typically just a marker — `kind: 'solid'` collider is enough. State (hit count) goes on `actor.runtime` or the brick's own props.
|
|
863
|
-
- `GameController` (no Layout needed if you don't draw it): tracks score / lives / status; expose HUD via `drawUi()`.
|
|
896
|
+
- `GameController` (no Layout needed if you don't draw it): tracks score / lives / status; expose HUD via `Text` with `space: 'screen'` or `drawUi()`.
|
|
864
897
|
|
|
865
898
|
## Don't
|
|
866
899
|
|
|
@@ -4,11 +4,8 @@ import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
|
4
4
|
import {
|
|
5
5
|
GREEDY_CLAIM,
|
|
6
6
|
acquirePress,
|
|
7
|
+
clampLength,
|
|
7
8
|
length,
|
|
8
|
-
screenPoint,
|
|
9
|
-
stickBrake,
|
|
10
|
-
stickDrive,
|
|
11
|
-
stickVector,
|
|
12
9
|
} from '../engine/physics/controls';
|
|
13
10
|
|
|
14
11
|
// AnalogStick: on-screen virtual joystick (press anywhere) that drives the
|
|
@@ -26,6 +23,85 @@ import {
|
|
|
26
23
|
//
|
|
27
24
|
// Sibling hooks (fired on this actor only): onAnalogStickBegin / onAnalogStickEnd
|
|
28
25
|
// (actor, scene) — begin on first nonzero deflection, end on release or disable.
|
|
26
|
+
//
|
|
27
|
+
// Force-mode math lives here (only consumer): stickDrive / stickBrake return
|
|
28
|
+
// velocity deltas via applyImpulse — mass-independent, composable, and
|
|
29
|
+
// restricted to driven axes so gravity on an undriven axis is never clamped.
|
|
30
|
+
|
|
31
|
+
// Stick displacement from origin, clamped to maxRadius.
|
|
32
|
+
function stickVector(pointer, origin, maxRadius = 60) {
|
|
33
|
+
return clampLength({ x: pointer.x - origin.x, y: pointer.y - origin.y }, maxRadius);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Card coords before the camera — for screen-fixed controls reading a pointer.
|
|
37
|
+
function screenPoint(pointer) {
|
|
38
|
+
return { x: pointer.screenX, y: pointer.screenY };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Keep only the axes the stick is allowed to drive. An undriven axis (gravity
|
|
42
|
+
// in a platformer) must never be clamped or braked by the stick.
|
|
43
|
+
function drivenAxes(v, axes) {
|
|
44
|
+
return {
|
|
45
|
+
x: axes === 'vertical' ? 0 : v.x,
|
|
46
|
+
y: axes === 'horizontal' ? 0 : v.y,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Smallest signed angle from `from` to `to`, wrapped into [−π, π].
|
|
51
|
+
function angleDelta(from, to) {
|
|
52
|
+
let d = Math.atan2(to.y, to.x) - Math.atan2(from.y, from.x);
|
|
53
|
+
if (d > Math.PI) d -= 2 * Math.PI;
|
|
54
|
+
if (d < -Math.PI) d += 2 * Math.PI;
|
|
55
|
+
return d;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Velocity delta while held: accelerate toward deflection × speed with turn
|
|
59
|
+
// boost on reversals, then clamp driven axes to `speed`. Impulse only.
|
|
60
|
+
function stickDrive(velocity, deflection, props = {}, dt) {
|
|
61
|
+
const speed = props.speed ?? 6;
|
|
62
|
+
const rampTime = props.rampTime ?? 0.2;
|
|
63
|
+
const turnBoost = props.turnBoost ?? 3;
|
|
64
|
+
const axes = props.axes ?? 'both';
|
|
65
|
+
const vDriven = drivenAxes(velocity, axes);
|
|
66
|
+
|
|
67
|
+
let boost = 1;
|
|
68
|
+
if (turnBoost > 0 && length(velocity) > 0 && length(deflection) > 0) {
|
|
69
|
+
boost = 1 + turnBoost * (Math.abs(angleDelta(velocity, deflection)) / Math.PI);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let delta;
|
|
73
|
+
if (rampTime <= 0) {
|
|
74
|
+
const target = drivenAxes(
|
|
75
|
+
{ x: deflection.x * speed, y: deflection.y * speed },
|
|
76
|
+
axes,
|
|
77
|
+
);
|
|
78
|
+
delta = { x: target.x - vDriven.x, y: target.y - vDriven.y };
|
|
79
|
+
} else {
|
|
80
|
+
const accel = speed / rampTime;
|
|
81
|
+
delta = drivenAxes(
|
|
82
|
+
{ x: deflection.x * accel * boost * dt, y: deflection.y * accel * boost * dt },
|
|
83
|
+
axes,
|
|
84
|
+
);
|
|
85
|
+
const capped = clampLength({ x: vDriven.x + delta.x, y: vDriven.y + delta.y }, speed);
|
|
86
|
+
delta = { x: capped.x - vDriven.x, y: capped.y - vDriven.y };
|
|
87
|
+
}
|
|
88
|
+
return delta;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Velocity delta while idle: Coulomb brake on driven axes only.
|
|
92
|
+
// `slowDown` is speed units lost per second (px/step per second). 0 = coast.
|
|
93
|
+
function stickBrake(velocity, props = {}, dt) {
|
|
94
|
+
const slowDown = props.slowDown ?? 20;
|
|
95
|
+
if (slowDown <= 0) return { x: 0, y: 0 };
|
|
96
|
+
const axes = props.axes ?? 'both';
|
|
97
|
+
const vDriven = drivenAxes(velocity, axes);
|
|
98
|
+
const len = length(vDriven);
|
|
99
|
+
if (len === 0) return { x: 0, y: 0 };
|
|
100
|
+
const reduce = Math.min(len, slowDown * dt);
|
|
101
|
+
const s = -reduce / len;
|
|
102
|
+
return { x: vDriven.x * s, y: vDriven.y * s };
|
|
103
|
+
}
|
|
104
|
+
|
|
29
105
|
function fireStickHook(actor, scene, hook) {
|
|
30
106
|
scene.forEachBehavior(actor, (inst) => inst[hook]?.(actor, scene));
|
|
31
107
|
}
|
|
@@ -62,6 +62,14 @@ export class Slingshot {
|
|
|
62
62
|
// Anchor at the press point (like castle-client): the aim/launch depends
|
|
63
63
|
// on the drag gesture, not where the press landed relative to the actor.
|
|
64
64
|
rt._slingAnchor = { x: press.x, y: press.y };
|
|
65
|
+
// A same-frame tap (down already false) is not an aim — cancel before we
|
|
66
|
+
// can launch with a zero pull. A TARGETED control that updates later this
|
|
67
|
+
// frame may still steal a greedy claim; we must not freeze velocity yet.
|
|
68
|
+
if (!press.down) {
|
|
69
|
+
rt._slinging = false;
|
|
70
|
+
rt._slingPointerId = null;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
if (!rt._slinging) return;
|
|
@@ -77,8 +85,12 @@ export class Slingshot {
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
if (p.down) {
|
|
80
|
-
// Hold still while aiming
|
|
81
|
-
|
|
88
|
+
// Hold still while aiming — but not on the press frame. A greedy claim can
|
|
89
|
+
// still lose to a higher claim that hasn't updated yet (e.g. a Draggable
|
|
90
|
+
// on the pressed body); freezing here would hitch a mid-air body for one frame
|
|
91
|
+
// and then drop the shot with no launch. Matches claimPointer's contract
|
|
92
|
+
// that the press frame costs a greedy control nothing visible.
|
|
93
|
+
if (!p.justPressed) scene.physics.setVelocity(actor, { x: 0, y: 0 });
|
|
82
94
|
rt._slingAim = { x: p.x, y: p.y };
|
|
83
95
|
} else {
|
|
84
96
|
scene.physics.setVelocity(actor, slingLaunch(rt._slingAnchor, p, this.props));
|
|
@@ -7,6 +7,7 @@ import { Panel, SelectField } from '../engine/ui';
|
|
|
7
7
|
import { SpriteField } from '../engine/spriteField';
|
|
8
8
|
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
9
9
|
import { parseTint, tintCanvas } from './tint';
|
|
10
|
+
import { paintActorStyle, styleContentBox, styleOpacity } from './Style';
|
|
10
11
|
|
|
11
12
|
// Runtime behavior for a deck's 2D art: the pixel-art `.pxart` format, or an
|
|
12
13
|
// image file (png / jpg / gif / webp / svg) the deck uploaded. Both come out of
|
|
@@ -81,6 +82,9 @@ export class Sprite {
|
|
|
81
82
|
if (actor.runtime?.collected) return;
|
|
82
83
|
const layout = actor.components.Layout;
|
|
83
84
|
if (!layout) return;
|
|
85
|
+
// Style chrome first so background/border sit behind the blit even when
|
|
86
|
+
// the Style key is after Sprite on the components object.
|
|
87
|
+
paintActorStyle(actor, ctx);
|
|
84
88
|
const sprite = this.resolveSprite(scene);
|
|
85
89
|
if (!sprite) return;
|
|
86
90
|
const total = artFrameCount(sprite);
|
|
@@ -99,6 +103,8 @@ export class Sprite {
|
|
|
99
103
|
const smoothing =
|
|
100
104
|
isVectorRenderer(sprite.renderer) || sprite.cornerRadius > 0 || isImageArt(sprite);
|
|
101
105
|
|
|
106
|
+
const box = styleContentBox(actor, layout);
|
|
107
|
+
|
|
102
108
|
// Snap the destination rect to whole device pixels so tiles that abut
|
|
103
109
|
// exactly in card units (e.g. adjacent 100-unit-wide tiles) land on the
|
|
104
110
|
// same device-pixel edge instead of each getting its own antialiased
|
|
@@ -117,7 +123,9 @@ export class Sprite {
|
|
|
117
123
|
// Everything downstream (snapping, tiling's clip box) works off `dest`: the
|
|
118
124
|
// Layout box itself for every mode except `fit`/`cover`, which resize to the
|
|
119
125
|
// art's own aspect ratio (see `spriteDestRect`).
|
|
120
|
-
const dest = spriteDestRect(this.props.mode,
|
|
126
|
+
const dest = spriteDestRect(this.props.mode, box, artSize);
|
|
127
|
+
ctx.save();
|
|
128
|
+
ctx.globalAlpha *= styleOpacity(actor);
|
|
121
129
|
if (transform.b === 0 && transform.c === 0) {
|
|
122
130
|
const x0 = dest.x * transform.a + transform.e;
|
|
123
131
|
const y0 = dest.y * transform.d + transform.f;
|
|
@@ -133,12 +141,12 @@ export class Sprite {
|
|
|
133
141
|
if (tiling) {
|
|
134
142
|
drawTilesSnapped(ctx, canvas, artSize, this.props.tileSize, dest, transform, x0, y0, rx0, ry0, rx1, ry1);
|
|
135
143
|
} else {
|
|
136
|
-
// Clip cover's overflow to the snapped
|
|
144
|
+
// Clip cover's overflow to the snapped content box (undone by restore).
|
|
137
145
|
if (covering) {
|
|
138
|
-
const lx0 = Math.round(
|
|
139
|
-
const ly0 = Math.round(
|
|
140
|
-
const lx1 = Math.round((
|
|
141
|
-
const ly1 = Math.round((
|
|
146
|
+
const lx0 = Math.round(box.x * transform.a + transform.e);
|
|
147
|
+
const ly0 = Math.round(box.y * transform.d + transform.f);
|
|
148
|
+
const lx1 = Math.round((box.x + box.width) * transform.a + transform.e);
|
|
149
|
+
const ly1 = Math.round((box.y + box.height) * transform.d + transform.f);
|
|
142
150
|
ctx.beginPath();
|
|
143
151
|
ctx.rect(lx0, ly0, lx1 - lx0, ly1 - ly0);
|
|
144
152
|
ctx.clip();
|
|
@@ -151,10 +159,10 @@ export class Sprite {
|
|
|
151
159
|
if (tiling) {
|
|
152
160
|
drawTilesUnsnapped(ctx, canvas, artSize, this.props.tileSize, dest);
|
|
153
161
|
} else if (covering) {
|
|
154
|
-
// Rotated: clip cover's overflow to the
|
|
162
|
+
// Rotated: clip cover's overflow to the content box in card units.
|
|
155
163
|
ctx.save();
|
|
156
164
|
ctx.beginPath();
|
|
157
|
-
ctx.rect(
|
|
165
|
+
ctx.rect(box.x, box.y, box.width, box.height);
|
|
158
166
|
ctx.clip();
|
|
159
167
|
ctx.drawImage(canvas, dest.x, dest.y, dest.width, dest.height);
|
|
160
168
|
ctx.restore();
|
|
@@ -163,6 +171,7 @@ export class Sprite {
|
|
|
163
171
|
}
|
|
164
172
|
ctx.imageSmoothingEnabled = prevSmoothing;
|
|
165
173
|
}
|
|
174
|
+
ctx.restore();
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
static Inspector({ component, setComponent, sprites, override }) {
|