castle-web-cli 0.4.3 → 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.
Files changed (96) hide show
  1. package/dist/index.js +8 -2
  2. package/dist/init.js +1 -1
  3. package/package.json +1 -1
  4. package/kits/basic-2d-frozen/.prettierrc +0 -8
  5. package/kits/basic-2d-frozen/CLAUDE.md +0 -131
  6. package/kits/basic-2d-frozen/behaviors/Camera.jsx +0 -43
  7. package/kits/basic-2d-frozen/behaviors/Collider.jsx +0 -71
  8. package/kits/basic-2d-frozen/behaviors/Drawing.jsx +0 -139
  9. package/kits/basic-2d-frozen/behaviors/Layout.jsx +0 -16
  10. package/kits/basic-2d-frozen/drawings/floor.drawing +0 -70
  11. package/kits/basic-2d-frozen/editors/App.jsx +0 -152
  12. package/kits/basic-2d-frozen/editors/CodeEditor.jsx +0 -112
  13. package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +0 -222
  14. package/kits/basic-2d-frozen/editors/FileBrowser.jsx +0 -143
  15. package/kits/basic-2d-frozen/editors/PlayOnly.jsx +0 -21
  16. package/kits/basic-2d-frozen/editors/SceneEditor.jsx +0 -1012
  17. package/kits/basic-2d-frozen/editors/behaviorRegistry.js +0 -24
  18. package/kits/basic-2d-frozen/editors/editorHistory.js +0 -52
  19. package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +0 -83
  20. package/kits/basic-2d-frozen/engine/SceneUI.jsx +0 -67
  21. package/kits/basic-2d-frozen/engine/TouchControls.jsx +0 -136
  22. package/kits/basic-2d-frozen/engine/autoInspector.jsx +0 -51
  23. package/kits/basic-2d-frozen/engine/files.js +0 -62
  24. package/kits/basic-2d-frozen/engine/scene.js +0 -420
  25. package/kits/basic-2d-frozen/engine/ui.jsx +0 -344
  26. package/kits/basic-2d-frozen/engine/ui.module.css +0 -928
  27. package/kits/basic-2d-frozen/eslint.config.js +0 -50
  28. package/kits/basic-2d-frozen/index.html +0 -11
  29. package/kits/basic-2d-frozen/main.jsx +0 -10
  30. package/kits/basic-2d-frozen/package-lock.json +0 -2706
  31. package/kits/basic-2d-frozen/package.json +0 -41
  32. package/kits/basic-2d-frozen/scenes/main.scene +0 -108
  33. package/kits/basic-2d-frozen/vite.config.js +0 -1
  34. package/kits/rpg-2d/.prettierrc +0 -8
  35. package/kits/rpg-2d/behaviors/Camera.tsx +0 -52
  36. package/kits/rpg-2d/behaviors/Collider.tsx +0 -98
  37. package/kits/rpg-2d/behaviors/Dialog.tsx +0 -184
  38. package/kits/rpg-2d/behaviors/Drawing.tsx +0 -161
  39. package/kits/rpg-2d/behaviors/Friend.tsx +0 -45
  40. package/kits/rpg-2d/behaviors/Layout.tsx +0 -29
  41. package/kits/rpg-2d/behaviors/PlayerController.tsx +0 -255
  42. package/kits/rpg-2d/behaviors/Portal.tsx +0 -60
  43. package/kits/rpg-2d/behaviors/QuestLog.tsx +0 -90
  44. package/kits/rpg-2d/behaviors/SaveMenu.tsx +0 -123
  45. package/kits/rpg-2d/behaviors/Tilemap.tsx +0 -90
  46. package/kits/rpg-2d/drawings/bld-home.drawing +0 -8136
  47. package/kits/rpg-2d/drawings/env-crate.drawing +0 -509
  48. package/kits/rpg-2d/drawings/env-fence.drawing +0 -536
  49. package/kits/rpg-2d/drawings/env-flower-bed.drawing +0 -607
  50. package/kits/rpg-2d/drawings/env-fountain.drawing +0 -2622
  51. package/kits/rpg-2d/drawings/env-hedge.drawing +0 -601
  52. package/kits/rpg-2d/drawings/env-house-blue.drawing +0 -1
  53. package/kits/rpg-2d/drawings/env-house-green.drawing +0 -1
  54. package/kits/rpg-2d/drawings/env-tree-oak.drawing +0 -1540
  55. package/kits/rpg-2d/drawings/env-tree-pine.drawing +0 -1315
  56. package/kits/rpg-2d/drawings/floor.drawing +0 -70
  57. package/kits/rpg-2d/drawings/fx-sparkle.drawing +0 -926
  58. package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +0 -1099
  59. package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +0 -4177
  60. package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +0 -1099
  61. package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +0 -4177
  62. package/kits/rpg-2d/drawings/player-idle-down.drawing +0 -1070
  63. package/kits/rpg-2d/drawings/player-idle-left.drawing +0 -1070
  64. package/kits/rpg-2d/drawings/player-idle-right.drawing +0 -1070
  65. package/kits/rpg-2d/drawings/player-idle-up.drawing +0 -1070
  66. package/kits/rpg-2d/drawings/player-walk-down.drawing +0 -4148
  67. package/kits/rpg-2d/drawings/player-walk-left.drawing +0 -4148
  68. package/kits/rpg-2d/drawings/player-walk-right.drawing +0 -4148
  69. package/kits/rpg-2d/drawings/player-walk-up.drawing +0 -4148
  70. package/kits/rpg-2d/editors/App.tsx +0 -163
  71. package/kits/rpg-2d/editors/CodeEditor.tsx +0 -120
  72. package/kits/rpg-2d/editors/DrawingEditor.tsx +0 -278
  73. package/kits/rpg-2d/editors/FileBrowser.tsx +0 -191
  74. package/kits/rpg-2d/editors/PlayOnly.tsx +0 -26
  75. package/kits/rpg-2d/editors/SceneEditor.tsx +0 -1093
  76. package/kits/rpg-2d/editors/behaviorRegistry.ts +0 -33
  77. package/kits/rpg-2d/editors/editorHistory.ts +0 -75
  78. package/kits/rpg-2d/editors/editorProps.ts +0 -10
  79. package/kits/rpg-2d/engine/ScenePlayer.tsx +0 -130
  80. package/kits/rpg-2d/engine/SceneUI.tsx +0 -74
  81. package/kits/rpg-2d/engine/TouchControls.tsx +0 -157
  82. package/kits/rpg-2d/engine/autoInspector.tsx +0 -111
  83. package/kits/rpg-2d/engine/drawing.ts +0 -81
  84. package/kits/rpg-2d/engine/files.ts +0 -215
  85. package/kits/rpg-2d/engine/scene.ts +0 -484
  86. package/kits/rpg-2d/engine/ui.module.css +0 -928
  87. package/kits/rpg-2d/engine/ui.tsx +0 -483
  88. package/kits/rpg-2d/eslint.config.js +0 -46
  89. package/kits/rpg-2d/index.html +0 -11
  90. package/kits/rpg-2d/main.tsx +0 -14
  91. package/kits/rpg-2d/package-lock.json +0 -3149
  92. package/kits/rpg-2d/package.json +0 -46
  93. package/kits/rpg-2d/scenes/main.scene +0 -203
  94. package/kits/rpg-2d/tsconfig.json +0 -17
  95. package/kits/rpg-2d/vite-env.d.ts +0 -7
  96. 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), rpg-2d, none)
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
@@ -162,7 +162,7 @@ function scaffoldFromKit(kit, projectDir) {
162
162
  if (cliDistAbs) {
163
163
  pkg.scripts[k] = pkg.scripts[k]
164
164
  .replace(/\.\.\/\.\.\/cli\/dist/g, cliDistAbs)
165
- .replace(/\.\.\/\.\.\/sdk/g, sdkIsLocal ? toPosixPath(sdkPath) : '');
165
+ .replace(/\.\.\/\.\.\/sdk/g, toPosixPath(sdkPath));
166
166
  }
167
167
  else {
168
168
  // Globally-installed: route through the `castle-web` binary on PATH.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"
@@ -1,8 +0,0 @@
1
- {
2
- "printWidth": 100,
3
- "tabWidth": 2,
4
- "singleQuote": true,
5
- "bracketSameLine": true,
6
- "trailingComma": "es5",
7
- "arrowParens": "always"
8
- }
@@ -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,16 +0,0 @@
1
- export class Layout {
2
- static behaviorName = 'Layout';
3
-
4
- static defaultProps = {
5
- x: 0,
6
- y: 0,
7
- width: 32,
8
- height: 32,
9
- z: 0,
10
- rotation: 0,
11
- };
12
-
13
- constructor(props) {
14
- this.props = props;
15
- }
16
- }
@@ -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
- }
@@ -1,152 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { writeFile } from 'castle-web-sdk';
3
- import {
4
- flatFileOrder,
5
- formatJson,
6
- getFileKind,
7
- initialFiles,
8
- parseJsonFile,
9
- } from '../engine/files';
10
- import { AppShell, cx, MainEditor, styles } from '../engine/ui';
11
- import { CodeEditor } from './CodeEditor';
12
- import { DrawingEditor } from './DrawingEditor';
13
- import { FileBrowser } from './FileBrowser';
14
- import { SceneEditor } from './SceneEditor';
15
- export function App() {
16
- const [files, setFiles] = useState(initialFiles);
17
- const [selectedPath, setSelectedPath] = useState('scenes/main.scene');
18
- const [filesSheetOpen, setFilesSheetOpen] = useState(false);
19
- const [selectedActorIds, setSelectedActorIds] = useState([]);
20
- const [multiSelectMode, setMultiSelectMode] = useState(false);
21
- const saveTimersRef = useRef({});
22
- const saveVersionsRef = useRef({});
23
- const drawings = {};
24
- for (const [path, text] of Object.entries(files)) {
25
- if (!path.endsWith('.drawing')) continue;
26
- const parsed = parseJsonFile(path, text);
27
- if (parsed.value) drawings[path] = parsed.value;
28
- }
29
- function updateSelectedFile(nextText) {
30
- const path = selectedPath;
31
- setFiles((current) => ({
32
- ...current,
33
- [path]: nextText,
34
- }));
35
- scheduleFileWrite(path, nextText);
36
- }
37
- function scheduleFileWrite(path, nextText) {
38
- const version = (saveVersionsRef.current[path] ?? 0) + 1;
39
- saveVersionsRef.current[path] = version;
40
- const existingTimer = saveTimersRef.current[path];
41
- if (existingTimer) window.clearTimeout(existingTimer);
42
- saveTimersRef.current[path] = window.setTimeout(() => {
43
- delete saveTimersRef.current[path];
44
- writeFile(path, nextText).catch((error) => {
45
- if (saveVersionsRef.current[path] !== version) return;
46
- const message = error instanceof Error ? error.message : String(error);
47
- console.error(`Failed to save ${path}: ${message}`);
48
- });
49
- }, 1500);
50
- }
51
- const kind = getFileKind(selectedPath);
52
- const text = files[selectedPath] ?? '';
53
- function selectFile(path) {
54
- setSelectedPath(path);
55
- setFilesSheetOpen(false);
56
- setSelectedActorIds([]);
57
- setMultiSelectMode(false);
58
- }
59
- const onToggleFiles = () => {
60
- setFilesSheetOpen((previous) => !previous);
61
- setSelectedActorIds([]);
62
- setMultiSelectMode(false);
63
- };
64
- // Alt+Up / Alt+Down steps through files in file-browser sidebar order.
65
- useEffect(() => {
66
- function onKeyDown(event) {
67
- if (!event.altKey || event.metaKey || event.ctrlKey) return;
68
- if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
69
- const active = document.activeElement;
70
- if (
71
- active &&
72
- (active.tagName === 'INPUT' ||
73
- active.tagName === 'TEXTAREA' ||
74
- active.tagName === 'SELECT' ||
75
- active.isContentEditable ||
76
- active.closest('.cm-editor'))
77
- ) {
78
- return;
79
- }
80
- const order = flatFileOrder(Object.keys(files));
81
- const index = order.indexOf(selectedPath);
82
- if (index === -1) return;
83
- const nextIndex = event.key === 'ArrowUp' ? index - 1 : index + 1;
84
- if (nextIndex < 0 || nextIndex >= order.length) return;
85
- event.preventDefault();
86
- selectFile(order[nextIndex]);
87
- }
88
- window.addEventListener('keydown', onKeyDown);
89
- return () => window.removeEventListener('keydown', onKeyDown);
90
- }, [files, selectedPath]);
91
- return (
92
- <AppShell>
93
- <FileBrowser
94
- files={files}
95
- selectedPath={selectedPath}
96
- onSelect={selectFile}
97
- sheetOpen={filesSheetOpen}
98
- onSheetOpenChange={setFilesSheetOpen}
99
- />
100
- {filesSheetOpen ? (
101
- <div
102
- className={cx(styles.sheetBackdrop, styles.mobileOnly)}
103
- onClick={() => setFilesSheetOpen(false)}
104
- />
105
- ) : null}
106
- <MainEditor>
107
- {kind === 'scene' ? (
108
- <SceneEditor
109
- path={selectedPath}
110
- text={text}
111
- files={files}
112
- drawings={drawings}
113
- onChange={updateSelectedFile}
114
- onToggleFiles={onToggleFiles}
115
- filesOpen={filesSheetOpen}
116
- selectedActorIds={selectedActorIds}
117
- onSelectActorIds={setSelectedActorIds}
118
- multiSelectMode={multiSelectMode}
119
- onSetMultiSelectMode={setMultiSelectMode}
120
- />
121
- ) : null}
122
- {kind === 'drawing' ? (
123
- <DrawingEditor
124
- path={selectedPath}
125
- text={text}
126
- onChange={updateSelectedFile}
127
- onToggleFiles={onToggleFiles}
128
- filesOpen={filesSheetOpen}
129
- />
130
- ) : null}
131
- {kind === 'code' ? (
132
- <CodeEditor
133
- path={selectedPath}
134
- text={text}
135
- onChange={updateSelectedFile}
136
- onToggleFiles={onToggleFiles}
137
- filesOpen={filesSheetOpen}
138
- />
139
- ) : null}
140
- {kind === 'text' ? (
141
- <CodeEditor
142
- path={selectedPath}
143
- text={formatJson(text)}
144
- onChange={updateSelectedFile}
145
- onToggleFiles={onToggleFiles}
146
- filesOpen={filesSheetOpen}
147
- />
148
- ) : null}
149
- </MainEditor>
150
- </AppShell>
151
- );
152
- }