castle-web-cli 0.4.2 → 0.4.4
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/index.js +8 -2
- package/dist/init.js +9 -6
- package/package.json +1 -1
- package/kits/basic-2d-frozen/.prettierrc +0 -8
- package/kits/basic-2d-frozen/CLAUDE.md +0 -131
- package/kits/basic-2d-frozen/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d-frozen/behaviors/Collider.jsx +0 -71
- package/kits/basic-2d-frozen/behaviors/Drawing.jsx +0 -139
- package/kits/basic-2d-frozen/behaviors/Layout.jsx +0 -16
- package/kits/basic-2d-frozen/drawings/floor.drawing +0 -70
- package/kits/basic-2d-frozen/editors/App.jsx +0 -152
- package/kits/basic-2d-frozen/editors/CodeEditor.jsx +0 -112
- package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +0 -222
- package/kits/basic-2d-frozen/editors/FileBrowser.jsx +0 -143
- package/kits/basic-2d-frozen/editors/PlayOnly.jsx +0 -21
- package/kits/basic-2d-frozen/editors/SceneEditor.jsx +0 -1012
- package/kits/basic-2d-frozen/editors/behaviorRegistry.js +0 -24
- package/kits/basic-2d-frozen/editors/editorHistory.js +0 -52
- package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +0 -83
- package/kits/basic-2d-frozen/engine/SceneUI.jsx +0 -67
- package/kits/basic-2d-frozen/engine/TouchControls.jsx +0 -136
- package/kits/basic-2d-frozen/engine/autoInspector.jsx +0 -51
- package/kits/basic-2d-frozen/engine/files.js +0 -62
- package/kits/basic-2d-frozen/engine/scene.js +0 -420
- package/kits/basic-2d-frozen/engine/ui.jsx +0 -344
- package/kits/basic-2d-frozen/engine/ui.module.css +0 -928
- package/kits/basic-2d-frozen/eslint.config.js +0 -50
- package/kits/basic-2d-frozen/index.html +0 -11
- package/kits/basic-2d-frozen/main.jsx +0 -10
- package/kits/basic-2d-frozen/package-lock.json +0 -2706
- package/kits/basic-2d-frozen/package.json +0 -41
- package/kits/basic-2d-frozen/scenes/main.scene +0 -108
- package/kits/basic-2d-frozen/vite.config.js +0 -1
- package/kits/rpg-2d/.prettierrc +0 -8
- package/kits/rpg-2d/behaviors/Camera.tsx +0 -52
- package/kits/rpg-2d/behaviors/Collider.tsx +0 -98
- package/kits/rpg-2d/behaviors/Dialog.tsx +0 -184
- package/kits/rpg-2d/behaviors/Drawing.tsx +0 -161
- package/kits/rpg-2d/behaviors/Friend.tsx +0 -45
- package/kits/rpg-2d/behaviors/Layout.tsx +0 -29
- package/kits/rpg-2d/behaviors/PlayerController.tsx +0 -255
- package/kits/rpg-2d/behaviors/Portal.tsx +0 -60
- package/kits/rpg-2d/behaviors/QuestLog.tsx +0 -90
- package/kits/rpg-2d/behaviors/SaveMenu.tsx +0 -123
- package/kits/rpg-2d/behaviors/Tilemap.tsx +0 -90
- package/kits/rpg-2d/drawings/bld-home.drawing +0 -8136
- package/kits/rpg-2d/drawings/env-crate.drawing +0 -509
- package/kits/rpg-2d/drawings/env-fence.drawing +0 -536
- package/kits/rpg-2d/drawings/env-flower-bed.drawing +0 -607
- package/kits/rpg-2d/drawings/env-fountain.drawing +0 -2622
- package/kits/rpg-2d/drawings/env-hedge.drawing +0 -601
- package/kits/rpg-2d/drawings/env-house-blue.drawing +0 -1
- package/kits/rpg-2d/drawings/env-house-green.drawing +0 -1
- package/kits/rpg-2d/drawings/env-tree-oak.drawing +0 -1540
- package/kits/rpg-2d/drawings/env-tree-pine.drawing +0 -1315
- package/kits/rpg-2d/drawings/floor.drawing +0 -70
- package/kits/rpg-2d/drawings/fx-sparkle.drawing +0 -926
- package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +0 -1099
- package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +0 -4177
- package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +0 -1099
- package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +0 -4177
- package/kits/rpg-2d/drawings/player-idle-down.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-left.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-right.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-up.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-walk-down.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-left.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-right.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-up.drawing +0 -4148
- package/kits/rpg-2d/editors/App.tsx +0 -163
- package/kits/rpg-2d/editors/CodeEditor.tsx +0 -120
- package/kits/rpg-2d/editors/DrawingEditor.tsx +0 -278
- package/kits/rpg-2d/editors/FileBrowser.tsx +0 -191
- package/kits/rpg-2d/editors/PlayOnly.tsx +0 -26
- package/kits/rpg-2d/editors/SceneEditor.tsx +0 -1093
- package/kits/rpg-2d/editors/behaviorRegistry.ts +0 -33
- package/kits/rpg-2d/editors/editorHistory.ts +0 -75
- package/kits/rpg-2d/editors/editorProps.ts +0 -10
- package/kits/rpg-2d/engine/ScenePlayer.tsx +0 -130
- package/kits/rpg-2d/engine/SceneUI.tsx +0 -74
- package/kits/rpg-2d/engine/TouchControls.tsx +0 -157
- package/kits/rpg-2d/engine/autoInspector.tsx +0 -111
- package/kits/rpg-2d/engine/drawing.ts +0 -81
- package/kits/rpg-2d/engine/files.ts +0 -215
- package/kits/rpg-2d/engine/scene.ts +0 -484
- package/kits/rpg-2d/engine/ui.module.css +0 -928
- package/kits/rpg-2d/engine/ui.tsx +0 -483
- package/kits/rpg-2d/eslint.config.js +0 -46
- package/kits/rpg-2d/index.html +0 -11
- package/kits/rpg-2d/main.tsx +0 -14
- package/kits/rpg-2d/package-lock.json +0 -3149
- package/kits/rpg-2d/package.json +0 -46
- package/kits/rpg-2d/scenes/main.scene +0 -203
- package/kits/rpg-2d/tsconfig.json +0 -17
- package/kits/rpg-2d/vite-env.d.ts +0 -7
- package/kits/rpg-2d/vite.config.js +0 -1
package/dist/index.js
CHANGED
|
@@ -45,14 +45,20 @@ function getWsPort(dir) {
|
|
|
45
45
|
}
|
|
46
46
|
function usage() {
|
|
47
47
|
console.log(`Usage:
|
|
48
|
-
castle-web init <dir> [--kit NAME] (kits: basic-2d (default),
|
|
48
|
+
castle-web init <dir> [--kit NAME] (kits: basic-2d (default), none)
|
|
49
49
|
castle-web serve [dir] [--port PORT] [--host HOST] [--open] [--detach]
|
|
50
50
|
castle-web restart [--port PORT]
|
|
51
51
|
castle-web screenshot [--out FILE] [--port PORT]
|
|
52
52
|
castle-web save-preview-image [dir] [--port PORT] [--no-restart]
|
|
53
53
|
castle-web save-deck [dir]
|
|
54
54
|
castle-web get-deck <dir> [--deck-id ID]
|
|
55
|
-
castle-web login
|
|
55
|
+
castle-web login
|
|
56
|
+
|
|
57
|
+
Kits:
|
|
58
|
+
basic-2d Actor / behavior / scene framework for 2D games. Includes an editor and Layout / Drawing / Collider / Camera built-ins. Default if --kit is omitted.
|
|
59
|
+
none Minimal vite + canvas setup with the castle-web SDK and no kit.
|
|
60
|
+
|
|
61
|
+
After init, read the scaffolded deck's CLAUDE.md (also AGENTS.md) for the kit's full author guide.`);
|
|
56
62
|
process.exit(1);
|
|
57
63
|
}
|
|
58
64
|
async function main() {
|
package/dist/init.js
CHANGED
|
@@ -142,11 +142,14 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
142
142
|
// For a deck scaffolded from a globally-installed castle-web, rewrite to
|
|
143
143
|
// the published packages instead.
|
|
144
144
|
const sdkPath = getSdkPackagePath();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
145
|
+
// "Workspace mode" = the cli is running from a castle-experimental-web
|
|
146
|
+
// checkout (sdk/ exists next to cli/). Otherwise we're a globally-
|
|
147
|
+
// installed npm package and need to use the published refs + binary.
|
|
148
|
+
const workspaceMode = fs.existsSync(sdkPath);
|
|
149
|
+
const sdkRef = workspaceMode ? `file:${toPosixPath(sdkPath)}` : `^${PUBLISHED_SDK_VERSION}`;
|
|
150
|
+
const cliDistAbs = workspaceMode
|
|
151
|
+
? toPosixPath(path.dirname(getCliEntryPath()))
|
|
152
|
+
: null;
|
|
150
153
|
if (pkg.dependencies &&
|
|
151
154
|
typeof pkg.dependencies['castle-web-sdk'] === 'string' &&
|
|
152
155
|
pkg.dependencies['castle-web-sdk'].startsWith('file:')) {
|
|
@@ -159,7 +162,7 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
159
162
|
if (cliDistAbs) {
|
|
160
163
|
pkg.scripts[k] = pkg.scripts[k]
|
|
161
164
|
.replace(/\.\.\/\.\.\/cli\/dist/g, cliDistAbs)
|
|
162
|
-
.replace(/\.\.\/\.\.\/sdk/g,
|
|
165
|
+
.replace(/\.\.\/\.\.\/sdk/g, toPosixPath(sdkPath));
|
|
163
166
|
}
|
|
164
167
|
else {
|
|
165
168
|
// Globally-installed: route through the `castle-web` binary on PATH.
|
package/package.json
CHANGED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# basic-2d kit
|
|
2
|
-
|
|
3
|
-
Actor / behavior / scene framework on a 500×700 canvas ("card"). To build a game you (a) write behaviors in `behaviors/*.jsx`, (b) place actors that use them in `scenes/*.scene` (plain JSON). Behaviors auto-register via file glob — drop a new file and it's picked up on next reload.
|
|
4
|
-
|
|
5
|
-
This kit is plain JavaScript (no TypeScript). Use `.jsx` for files that contain JSX, `.js` otherwise. Don't add types or `.ts`/`.tsx` files.
|
|
6
|
-
|
|
7
|
-
DO NOT READ `engine/`, `editors/`, OR the built-in `behaviors/` files (`Layout.jsx`, `Drawing.jsx`, `Collider.jsx`, `Camera.jsx`) to build a game. Those are the framework itself; their complete public API (props, helpers, calling conventions) is documented inline below. Read framework source only if you are explicitly modifying the framework.
|
|
8
|
-
|
|
9
|
-
## Scope
|
|
10
|
-
|
|
11
|
-
Write the smallest game that satisfies what the user asked for. No sound, particles, menus, multi-level progression, or visual polish unless they specifically asked for it. A typical behavior is 30–80 lines — if yours is hitting 200, you're over-engineering: cut feel-good extras, fewer fields on props, fewer edge cases, fewer comments. Ship the core loop first; the user can ask for more.
|
|
12
|
-
|
|
13
|
-
## Workflow
|
|
14
|
-
|
|
15
|
-
1. Edit `behaviors/*.jsx` and `scenes/main.scene`.
|
|
16
|
-
2. `castle-web serve . --detach` (returns the URL, default port 5757). Curl once to confirm.
|
|
17
|
-
3. After every edit: `npm run restart` (no hot reload). Then re-check.
|
|
18
|
-
|
|
19
|
-
Card size is **500 wide × 700 tall** (origin top-left, +y is down).
|
|
20
|
-
|
|
21
|
-
## Behavior shape
|
|
22
|
-
|
|
23
|
-
A behavior is a class. Minimal contract:
|
|
24
|
-
|
|
25
|
-
```jsx
|
|
26
|
-
// behaviors/MyThing.jsx
|
|
27
|
-
export class MyThing {
|
|
28
|
-
static behaviorName = 'MyThing'; // must match the key used in .scene
|
|
29
|
-
static defaultProps = { speed: 200 };
|
|
30
|
-
|
|
31
|
-
constructor(props) {
|
|
32
|
-
this.props = props;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Called every frame in play mode. dt is seconds.
|
|
36
|
-
update(actor, scene, dt) {
|
|
37
|
-
const layout = actor.components.Layout;
|
|
38
|
-
layout.x += this.props.speed * dt; // mutate component in place
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Optional. Custom drawing (you usually don't need this; use a Drawing
|
|
42
|
-
// component instead). ctx is in card units already.
|
|
43
|
-
draw(actor, scene, ctx) {}
|
|
44
|
-
|
|
45
|
-
// Optional. Return React nodes for game-time HUD. Coordinates are card
|
|
46
|
-
// units. Read state your `update` set; do not start your own loops.
|
|
47
|
-
ui(actor, scene) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
A fresh `Behavior` instance is constructed per actor per frame from the actor's component props — DO NOT store per-actor state on `this`. Persist transient state on `actor.runtime` (a free-form object the framework will not serialize) or on the component props themselves.
|
|
54
|
-
|
|
55
|
-
## Scene file (`scenes/main.scene`, plain JSON)
|
|
56
|
-
|
|
57
|
-
```json
|
|
58
|
-
{
|
|
59
|
-
"background": "#1b2030",
|
|
60
|
-
"actors": [
|
|
61
|
-
{
|
|
62
|
-
"id": "paddle",
|
|
63
|
-
"components": {
|
|
64
|
-
"Layout": { "x": 210, "y": 640, "width": 80, "height": 12 },
|
|
65
|
-
"Drawing": { "file": "drawings/floor.drawing" },
|
|
66
|
-
"Collider": { "kind": "solid", "width": 80, "height": 12 },
|
|
67
|
-
"Paddle": { "speed": 360 }
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
]
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Rules: every actor needs a unique `id` (any string) and almost always a `Layout`. `components` keys are behavior names (the `static behaviorName`). Unspecified props fall back to that behavior's `defaultProps` — omit optional fields (`z`, `rotation`, actor `name`, `tint: '#ffffffff'`, scene `name`) to keep scenes compact, especially when generating many actors. Add a behavior to an actor by adding its key to `components`; remove it by deleting the key.
|
|
75
|
-
|
|
76
|
-
## Built-in behaviors
|
|
77
|
-
|
|
78
|
-
- **Layout** — `{ x, y, width, height, z?, rotation? }`. Every actor needs one. `z` orders draw (low first). `rotation` is degrees about the center.
|
|
79
|
-
- **Drawing** — `{ file: "drawings/foo.drawing", tint?: "#rrggbbaa" }`. Renders the pixel-art file scaled into the Layout box. `tint` multiplies; use white (`#ffffffff`) or omit for the original colors. For a solid-color rectangle, point `file` at `drawings/floor.drawing` (an 8×8 white pixel sprite) and set `tint` to your color.
|
|
80
|
-
- **Collider** — `{ kind: 'solid'|'pickup', width, height, offsetX?, offsetY?, debug? }`. Just a rect; the framework does NOT auto-resolve collisions for you. Use it as data:
|
|
81
|
-
|
|
82
|
-
```jsx
|
|
83
|
-
for (const other of scene.getActors()) {
|
|
84
|
-
if (other.id === actor.id) continue;
|
|
85
|
-
if (scene.overlaps(actor, other)) {
|
|
86
|
-
/* react */
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
- **Camera** — `{ target: actorId, followX, followY, roomWidth, roomHeight }`. Place on a dedicated actor; sets `scene.camera` clamped to the room. Omit entirely for a fixed view (no camera = no translation).
|
|
92
|
-
|
|
93
|
-
## SceneRuntime API (what `scene` exposes to behaviors)
|
|
94
|
-
|
|
95
|
-
- `scene.time` — seconds since start.
|
|
96
|
-
- `scene.keys` — `Set` of currently-held KeyboardEvent codes (e.g. `'ArrowLeft'`, `'KeyA'`, `'Space'`). Read in `update`.
|
|
97
|
-
- `scene.pointer` — `{ x, y, down }` in world (card) coordinates, camera-adjusted.
|
|
98
|
-
- `scene.getActor(id)` / `scene.getActors()` (sorted by Layout.z) / `scene.getComponent(actor, name)`.
|
|
99
|
-
- `scene.actorWith('GameController')` / `scene.actorsWith('Brick')` — find one / all actors carrying a given behavior. Prefer these to `getActors().find(a => a.components.X)`.
|
|
100
|
-
- `scene.colliderRect(actorOrId)` — rect from Layout + Collider, or null.
|
|
101
|
-
- `scene.overlaps(a, b)` — true when two actors/ids with Collider overlap.
|
|
102
|
-
- `scene.data` — the live scene data. Mutate `actor.components.X = {...}` to change props.
|
|
103
|
-
- `scene.spawnActor({ components: { Layout: {...}, MyBehavior: {...} } })` — add a new actor at runtime. Returns the actor (with auto-minted `id` and `runtime = {}`). Use this; don't push to `scene.data.actors` by hand.
|
|
104
|
-
- `scene.despawnActor(id)` — remove an actor at runtime. Use this; don't `splice` + `delete` by hand.
|
|
105
|
-
- `scene.status` — string you can set/read for game-state ('playing', 'gameover', ...).
|
|
106
|
-
- `actor.runtime` — per-instance scratchpad for transient state across frames (e.g. velocity, trail history). Not serialized.
|
|
107
|
-
|
|
108
|
-
## Input shortcuts
|
|
109
|
-
|
|
110
|
-
```jsx
|
|
111
|
-
if (scene.keys.has('ArrowLeft')) layout.x -= speed * dt;
|
|
112
|
-
if (scene.keys.has('ArrowRight')) layout.x += speed * dt;
|
|
113
|
-
if (scene.keys.has('Space')) /* launch ball */ ;
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
For HUD text use a behavior's `ui` hook (returns React); for in-world text or shapes, draw with `ctx` from `draw`. The deck has TouchControls overlay support out of the box for mobile play (arrow keys + a button); no setup needed.
|
|
117
|
-
|
|
118
|
-
## Common breakout-shaped recipe (sketch)
|
|
119
|
-
|
|
120
|
-
- `Paddle` behavior: read keys, clamp x to `[0, 500 - layout.width]`.
|
|
121
|
-
- `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.
|
|
122
|
-
- `Brick` behavior: typically just a marker — `kind: 'solid'` collider is enough. State (hit count) goes on `actor.runtime` or the brick's own props.
|
|
123
|
-
- `GameController` (no Layout needed if you don't draw it): tracks score / lives / status; expose HUD via `ui()`.
|
|
124
|
-
|
|
125
|
-
## Don't
|
|
126
|
-
|
|
127
|
-
- Don't `console.log` in tight loops — flood the serve log.
|
|
128
|
-
- Don't keep per-actor state on the behavior class instance; it's recreated each frame. Use `actor.runtime` or component props.
|
|
129
|
-
- Don't try to import from `editors/`; behaviors run in the play runtime too.
|
|
130
|
-
- Don't add types or `.ts`/`.tsx` files. This kit is JavaScript.
|
|
131
|
-
- Don't add a build step or change `vite.config.js` for a game — it's configured for you.
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { cardSize } from '../engine/scene';
|
|
2
|
-
|
|
3
|
-
// Follow camera: keeps the target actor centered in the viewport, clamped to
|
|
4
|
-
// the room bounds so the empty area past the room edges never scrolls in.
|
|
5
|
-
export class Camera {
|
|
6
|
-
static behaviorName = 'Camera';
|
|
7
|
-
|
|
8
|
-
static defaultProps = {
|
|
9
|
-
target: '',
|
|
10
|
-
followX: true,
|
|
11
|
-
followY: true,
|
|
12
|
-
roomWidth: 900,
|
|
13
|
-
roomHeight: 1100,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
constructor(props) {
|
|
17
|
-
this.props = props;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
update(_actor, scene) {
|
|
21
|
-
const target = this.props.target ? scene.getActor(this.props.target) : undefined;
|
|
22
|
-
const targetLayout = target?.components.Layout;
|
|
23
|
-
if (!targetLayout) return;
|
|
24
|
-
|
|
25
|
-
const centerX = targetLayout.x + targetLayout.width / 2;
|
|
26
|
-
const centerY = targetLayout.y + targetLayout.height / 2;
|
|
27
|
-
const desiredX = centerX - cardSize.width / 2;
|
|
28
|
-
const desiredY = centerY - cardSize.height / 2;
|
|
29
|
-
|
|
30
|
-
scene.camera = {
|
|
31
|
-
x: this.props.followX
|
|
32
|
-
? clamp(desiredX, 0, this.props.roomWidth - cardSize.width)
|
|
33
|
-
: 0,
|
|
34
|
-
y: this.props.followY
|
|
35
|
-
? clamp(desiredY, 0, this.props.roomHeight - cardSize.height)
|
|
36
|
-
: 0,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function clamp(value, min, max) {
|
|
42
|
-
return Math.max(min, Math.min(Math.max(min, max), value));
|
|
43
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Panel, SelectField } from '../engine/ui';
|
|
3
|
-
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
-
|
|
5
|
-
export class Collider {
|
|
6
|
-
static behaviorName = 'Collider';
|
|
7
|
-
|
|
8
|
-
static defaultProps = {
|
|
9
|
-
kind: 'solid',
|
|
10
|
-
width: 32,
|
|
11
|
-
height: 32,
|
|
12
|
-
offsetX: 0,
|
|
13
|
-
offsetY: 0,
|
|
14
|
-
debug: false,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
constructor(props) {
|
|
18
|
-
this.props = props;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
draw(actor, _scene, ctx, options) {
|
|
22
|
-
if (!options.showDebugColliders && !this.props.debug) return;
|
|
23
|
-
const rect = getColliderRect(actor);
|
|
24
|
-
if (!rect) return;
|
|
25
|
-
ctx.save();
|
|
26
|
-
ctx.strokeStyle = this.props.kind === 'pickup' ? '#ffe17a' : '#8db7ff';
|
|
27
|
-
ctx.lineWidth = 2;
|
|
28
|
-
ctx.strokeRect(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2);
|
|
29
|
-
ctx.restore();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
static Inspector({ component, setComponent }) {
|
|
33
|
-
return (
|
|
34
|
-
<Panel title="Collider">
|
|
35
|
-
<SelectField
|
|
36
|
-
label="Kind"
|
|
37
|
-
value={component.kind}
|
|
38
|
-
onChange={(kind) => setComponent({ kind: kind })}
|
|
39
|
-
options={['solid', 'pickup']}
|
|
40
|
-
/>
|
|
41
|
-
<AutoFields
|
|
42
|
-
defaultProps={Collider.defaultProps}
|
|
43
|
-
component={component}
|
|
44
|
-
setComponent={setComponent}
|
|
45
|
-
exclude={['kind']}
|
|
46
|
-
/>
|
|
47
|
-
</Panel>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function getColliderRect(actor) {
|
|
53
|
-
const layout = actor.components.Layout;
|
|
54
|
-
const collider = actor.components.Collider;
|
|
55
|
-
if (!layout || !collider) return null;
|
|
56
|
-
return {
|
|
57
|
-
x: layout.x + (collider.offsetX ?? 0),
|
|
58
|
-
y: layout.y + (collider.offsetY ?? 0),
|
|
59
|
-
width: collider.width ?? layout.width,
|
|
60
|
-
height: collider.height ?? layout.height,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function intersects(a, b) {
|
|
65
|
-
return (
|
|
66
|
-
a.x < b.x + b.width &&
|
|
67
|
-
a.x + a.width > b.x &&
|
|
68
|
-
a.y < b.y + b.height &&
|
|
69
|
-
a.y + a.height > b.y
|
|
70
|
-
);
|
|
71
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Panel, SelectField } from '../engine/ui';
|
|
3
|
-
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
-
|
|
5
|
-
export class Drawing {
|
|
6
|
-
static behaviorName = 'Drawing';
|
|
7
|
-
|
|
8
|
-
static defaultProps = {
|
|
9
|
-
file: 'drawings/floor.drawing',
|
|
10
|
-
tint: '#ffffffff',
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
constructor(props) {
|
|
14
|
-
this.props = props;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
draw(actor, scene, ctx) {
|
|
18
|
-
if (actor.runtime?.collected) return;
|
|
19
|
-
const layout = actor.components.Layout;
|
|
20
|
-
if (!layout) return;
|
|
21
|
-
const drawing = scene.drawings[this.props.file];
|
|
22
|
-
if (!drawing) return;
|
|
23
|
-
drawPixelDrawing(
|
|
24
|
-
ctx,
|
|
25
|
-
drawing,
|
|
26
|
-
layout.x,
|
|
27
|
-
layout.y,
|
|
28
|
-
layout.width,
|
|
29
|
-
layout.height,
|
|
30
|
-
this.props.tint
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
static Inspector({ component, setComponent, files }) {
|
|
35
|
-
const drawingFiles = getDrawingFiles(files);
|
|
36
|
-
return (
|
|
37
|
-
<Panel title="Drawing">
|
|
38
|
-
<SelectField
|
|
39
|
-
label="File"
|
|
40
|
-
value={component.file}
|
|
41
|
-
onChange={(file) => setComponent({ file })}
|
|
42
|
-
options={drawingFiles}
|
|
43
|
-
/>
|
|
44
|
-
<AutoFields
|
|
45
|
-
defaultProps={Drawing.defaultProps}
|
|
46
|
-
component={component}
|
|
47
|
-
setComponent={setComponent}
|
|
48
|
-
only={['tint']}
|
|
49
|
-
/>
|
|
50
|
-
</Panel>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function drawPixelDrawing(ctx, drawing, x, y, width, height, tint) {
|
|
56
|
-
if (drawing.width <= 0 || drawing.height <= 0) return;
|
|
57
|
-
// Blit the cached native-resolution canvas as a single image. Painting each
|
|
58
|
-
// pixel as its own `fillRect` left hairline anti-aliased seams under a
|
|
59
|
-
// rotation transform; one `drawImage` has no inter-pixel edges.
|
|
60
|
-
const canvas = getDrawingCanvas(drawing, tint);
|
|
61
|
-
const prevSmoothing = ctx.imageSmoothingEnabled;
|
|
62
|
-
ctx.imageSmoothingEnabled = false;
|
|
63
|
-
ctx.drawImage(canvas, x, y, width, height);
|
|
64
|
-
ctx.imageSmoothingEnabled = prevSmoothing;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Cache of native-resolution offscreen canvases keyed by drawing data and
|
|
68
|
-
// tint. A drawing edit produces a fresh `DrawingData` object (see
|
|
69
|
-
// DrawingEditor's `structuredClone`), so a new object naturally misses the
|
|
70
|
-
// WeakMap and rebuilds.
|
|
71
|
-
const drawingCanvasCache = new WeakMap();
|
|
72
|
-
|
|
73
|
-
// Render the pixel art once, unrotated, at 1px per pixel into an offscreen
|
|
74
|
-
// canvas, applying the tint. Cached so the per-frame draw is a single blit.
|
|
75
|
-
function getDrawingCanvas(drawing, tint) {
|
|
76
|
-
const tintKey = tint ?? '';
|
|
77
|
-
let byTint = drawingCanvasCache.get(drawing);
|
|
78
|
-
if (!byTint) {
|
|
79
|
-
byTint = new Map();
|
|
80
|
-
drawingCanvasCache.set(drawing, byTint);
|
|
81
|
-
}
|
|
82
|
-
const cached = byTint.get(tintKey);
|
|
83
|
-
if (cached) return cached;
|
|
84
|
-
|
|
85
|
-
const canvas = document.createElement('canvas');
|
|
86
|
-
canvas.width = drawing.width;
|
|
87
|
-
canvas.height = drawing.height;
|
|
88
|
-
const octx = canvas.getContext('2d');
|
|
89
|
-
if (octx) {
|
|
90
|
-
const tintRgba = parseTint(tint);
|
|
91
|
-
for (let py = 0; py < drawing.height; py++) {
|
|
92
|
-
for (let px = 0; px < drawing.width; px++) {
|
|
93
|
-
const color = drawing.pixels[py * drawing.width + px];
|
|
94
|
-
if (!color || color === 'transparent' || color.endsWith('00')) continue;
|
|
95
|
-
octx.fillStyle = tintRgba ? applyTint(color, tintRgba) : color;
|
|
96
|
-
octx.fillRect(px, py, 1, 1);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
byTint.set(tintKey, canvas);
|
|
101
|
-
return canvas;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Parse a hex color into [r, g, b, a] channels (0-255). Returns null when the
|
|
105
|
-
// string is not a hex color so callers can fall back to the raw color.
|
|
106
|
-
function parseHexColor(color) {
|
|
107
|
-
const hex = color.trim().replace(/^#/, '');
|
|
108
|
-
if (hex.length !== 6 && hex.length !== 8) return null;
|
|
109
|
-
if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
110
|
-
const r = parseInt(hex.slice(0, 2), 16);
|
|
111
|
-
const g = parseInt(hex.slice(2, 4), 16);
|
|
112
|
-
const b = parseInt(hex.slice(4, 6), 16);
|
|
113
|
-
const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) : 255;
|
|
114
|
-
return [r, g, b, a];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Returns the tint as [r, g, b, a] channels, or null when there is no tint or
|
|
118
|
-
// the tint is white (#ffffffff) -- both meaning "no change".
|
|
119
|
-
function parseTint(tint) {
|
|
120
|
-
if (!tint) return null;
|
|
121
|
-
const rgba = parseHexColor(tint);
|
|
122
|
-
if (!rgba) return null;
|
|
123
|
-
if (rgba[0] === 255 && rgba[1] === 255 && rgba[2] === 255 && rgba[3] === 255) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
return rgba;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Multiply each channel of a pixel color by the tint color (both 0-255).
|
|
130
|
-
function applyTint(color, tint) {
|
|
131
|
-
const rgba = parseHexColor(color);
|
|
132
|
-
if (!rgba) return color;
|
|
133
|
-
const out = rgba.map((channel, i) => Math.round((channel * tint[i]) / 255));
|
|
134
|
-
return '#' + out.map((channel) => channel.toString(16).padStart(2, '0')).join('');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getDrawingFiles(files) {
|
|
138
|
-
return Object.keys(files).filter((path) => path.endsWith('.drawing'));
|
|
139
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"width": 8,
|
|
3
|
-
"height": 8,
|
|
4
|
-
"pixels": [
|
|
5
|
-
"#ffffffff",
|
|
6
|
-
"#ffffffff",
|
|
7
|
-
"#ffffffff",
|
|
8
|
-
"#ffffffff",
|
|
9
|
-
"#ffffffff",
|
|
10
|
-
"#ffffffff",
|
|
11
|
-
"#ffffffff",
|
|
12
|
-
"#ffffffff",
|
|
13
|
-
"#ffffffff",
|
|
14
|
-
"#d8d8d8ff",
|
|
15
|
-
"#d8d8d8ff",
|
|
16
|
-
"#d8d8d8ff",
|
|
17
|
-
"#d8d8d8ff",
|
|
18
|
-
"#d8d8d8ff",
|
|
19
|
-
"#d8d8d8ff",
|
|
20
|
-
"#ffffffff",
|
|
21
|
-
"#ffffffff",
|
|
22
|
-
"#d8d8d8ff",
|
|
23
|
-
"#ffffffff",
|
|
24
|
-
"#ffffffff",
|
|
25
|
-
"#ffffffff",
|
|
26
|
-
"#ffffffff",
|
|
27
|
-
"#d8d8d8ff",
|
|
28
|
-
"#ffffffff",
|
|
29
|
-
"#ffffffff",
|
|
30
|
-
"#d8d8d8ff",
|
|
31
|
-
"#ffffffff",
|
|
32
|
-
"#ffffffff",
|
|
33
|
-
"#ffffffff",
|
|
34
|
-
"#ffffffff",
|
|
35
|
-
"#d8d8d8ff",
|
|
36
|
-
"#ffffffff",
|
|
37
|
-
"#ffffffff",
|
|
38
|
-
"#d8d8d8ff",
|
|
39
|
-
"#ffffffff",
|
|
40
|
-
"#ffffffff",
|
|
41
|
-
"#ffffffff",
|
|
42
|
-
"#ffffffff",
|
|
43
|
-
"#d8d8d8ff",
|
|
44
|
-
"#ffffffff",
|
|
45
|
-
"#ffffffff",
|
|
46
|
-
"#d8d8d8ff",
|
|
47
|
-
"#ffffffff",
|
|
48
|
-
"#ffffffff",
|
|
49
|
-
"#ffffffff",
|
|
50
|
-
"#ffffffff",
|
|
51
|
-
"#d8d8d8ff",
|
|
52
|
-
"#ffffffff",
|
|
53
|
-
"#ffffffff",
|
|
54
|
-
"#d8d8d8ff",
|
|
55
|
-
"#d8d8d8ff",
|
|
56
|
-
"#d8d8d8ff",
|
|
57
|
-
"#d8d8d8ff",
|
|
58
|
-
"#d8d8d8ff",
|
|
59
|
-
"#d8d8d8ff",
|
|
60
|
-
"#ffffffff",
|
|
61
|
-
"#ffffffff",
|
|
62
|
-
"#ffffffff",
|
|
63
|
-
"#ffffffff",
|
|
64
|
-
"#ffffffff",
|
|
65
|
-
"#ffffffff",
|
|
66
|
-
"#ffffffff",
|
|
67
|
-
"#ffffffff",
|
|
68
|
-
"#ffffffff"
|
|
69
|
-
]
|
|
70
|
-
}
|