castle-web-cli 0.4.1 → 0.4.2

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 (158) hide show
  1. package/dist/api.d.ts +53 -5
  2. package/dist/api.js +42 -15
  3. package/dist/config.d.ts +2 -0
  4. package/dist/config.js +25 -11
  5. package/dist/get-deck.d.ts +3 -0
  6. package/dist/get-deck.js +64 -0
  7. package/dist/ide-client.d.ts +1 -0
  8. package/dist/ide-client.js +537 -0
  9. package/dist/ide.d.ts +16 -0
  10. package/dist/ide.js +546 -0
  11. package/dist/index.js +36 -41
  12. package/dist/init.d.ts +3 -1
  13. package/dist/init.js +170 -24
  14. package/dist/localPaths.d.ts +6 -0
  15. package/dist/localPaths.js +33 -0
  16. package/dist/login.js +1 -1
  17. package/dist/preview.d.ts +3 -0
  18. package/dist/preview.js +53 -34
  19. package/dist/save-deck.d.ts +2 -0
  20. package/dist/{push.js → save-deck.js} +66 -5
  21. package/dist/serve.d.ts +2 -0
  22. package/dist/serve.js +290 -27
  23. package/kits/basic-2d/.prettierrc +8 -0
  24. package/kits/basic-2d/CLAUDE.md +131 -0
  25. package/kits/basic-2d/behaviors/Camera.jsx +43 -0
  26. package/kits/basic-2d/behaviors/Collider.jsx +71 -0
  27. package/kits/basic-2d/behaviors/Drawing.jsx +139 -0
  28. package/kits/basic-2d/behaviors/Layout.jsx +16 -0
  29. package/kits/basic-2d/drawings/floor.drawing +70 -0
  30. package/kits/basic-2d/editors/App.jsx +152 -0
  31. package/kits/basic-2d/editors/CodeEditor.jsx +112 -0
  32. package/kits/basic-2d/editors/DrawingEditor.jsx +222 -0
  33. package/kits/basic-2d/editors/FileBrowser.jsx +143 -0
  34. package/kits/basic-2d/editors/PlayOnly.jsx +21 -0
  35. package/kits/basic-2d/editors/SceneEditor.jsx +1012 -0
  36. package/kits/basic-2d/editors/behaviorRegistry.js +24 -0
  37. package/kits/basic-2d/editors/editorHistory.js +52 -0
  38. package/kits/basic-2d/engine/ScenePlayer.jsx +83 -0
  39. package/kits/basic-2d/engine/SceneUI.jsx +67 -0
  40. package/kits/basic-2d/engine/TouchControls.jsx +136 -0
  41. package/kits/basic-2d/engine/autoInspector.jsx +51 -0
  42. package/kits/basic-2d/engine/files.js +62 -0
  43. package/kits/basic-2d/engine/scene.js +420 -0
  44. package/kits/basic-2d/engine/ui.jsx +344 -0
  45. package/kits/basic-2d/engine/ui.module.css +928 -0
  46. package/kits/basic-2d/eslint.config.js +50 -0
  47. package/kits/basic-2d/index.html +11 -0
  48. package/kits/basic-2d/main.jsx +10 -0
  49. package/kits/basic-2d/package-lock.json +2706 -0
  50. package/kits/basic-2d/package.json +41 -0
  51. package/kits/basic-2d/scenes/main.scene +108 -0
  52. package/kits/basic-2d/vite.config.js +1 -0
  53. package/kits/basic-2d-frozen/.prettierrc +8 -0
  54. package/kits/basic-2d-frozen/CLAUDE.md +131 -0
  55. package/kits/basic-2d-frozen/behaviors/Camera.jsx +43 -0
  56. package/kits/basic-2d-frozen/behaviors/Collider.jsx +71 -0
  57. package/kits/basic-2d-frozen/behaviors/Drawing.jsx +139 -0
  58. package/kits/basic-2d-frozen/behaviors/Layout.jsx +16 -0
  59. package/kits/basic-2d-frozen/drawings/floor.drawing +70 -0
  60. package/kits/basic-2d-frozen/editors/App.jsx +152 -0
  61. package/kits/basic-2d-frozen/editors/CodeEditor.jsx +112 -0
  62. package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +222 -0
  63. package/kits/basic-2d-frozen/editors/FileBrowser.jsx +143 -0
  64. package/kits/basic-2d-frozen/editors/PlayOnly.jsx +21 -0
  65. package/kits/basic-2d-frozen/editors/SceneEditor.jsx +1012 -0
  66. package/kits/basic-2d-frozen/editors/behaviorRegistry.js +24 -0
  67. package/kits/basic-2d-frozen/editors/editorHistory.js +52 -0
  68. package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +83 -0
  69. package/kits/basic-2d-frozen/engine/SceneUI.jsx +67 -0
  70. package/kits/basic-2d-frozen/engine/TouchControls.jsx +136 -0
  71. package/kits/basic-2d-frozen/engine/autoInspector.jsx +51 -0
  72. package/kits/basic-2d-frozen/engine/files.js +62 -0
  73. package/kits/basic-2d-frozen/engine/scene.js +420 -0
  74. package/kits/basic-2d-frozen/engine/ui.jsx +344 -0
  75. package/kits/basic-2d-frozen/engine/ui.module.css +928 -0
  76. package/kits/basic-2d-frozen/eslint.config.js +50 -0
  77. package/kits/basic-2d-frozen/index.html +11 -0
  78. package/kits/basic-2d-frozen/main.jsx +10 -0
  79. package/kits/basic-2d-frozen/package-lock.json +2706 -0
  80. package/kits/basic-2d-frozen/package.json +41 -0
  81. package/kits/basic-2d-frozen/scenes/main.scene +108 -0
  82. package/kits/basic-2d-frozen/vite.config.js +1 -0
  83. package/kits/rpg-2d/.prettierrc +8 -0
  84. package/kits/rpg-2d/behaviors/Camera.tsx +52 -0
  85. package/kits/rpg-2d/behaviors/Collider.tsx +98 -0
  86. package/kits/rpg-2d/behaviors/Dialog.tsx +184 -0
  87. package/kits/rpg-2d/behaviors/Drawing.tsx +161 -0
  88. package/kits/rpg-2d/behaviors/Friend.tsx +45 -0
  89. package/kits/rpg-2d/behaviors/Layout.tsx +29 -0
  90. package/kits/rpg-2d/behaviors/PlayerController.tsx +255 -0
  91. package/kits/rpg-2d/behaviors/Portal.tsx +60 -0
  92. package/kits/rpg-2d/behaviors/QuestLog.tsx +90 -0
  93. package/kits/rpg-2d/behaviors/SaveMenu.tsx +123 -0
  94. package/kits/rpg-2d/behaviors/Tilemap.tsx +90 -0
  95. package/kits/rpg-2d/drawings/bld-home.drawing +8136 -0
  96. package/kits/rpg-2d/drawings/env-crate.drawing +509 -0
  97. package/kits/rpg-2d/drawings/env-fence.drawing +536 -0
  98. package/kits/rpg-2d/drawings/env-flower-bed.drawing +607 -0
  99. package/kits/rpg-2d/drawings/env-fountain.drawing +2622 -0
  100. package/kits/rpg-2d/drawings/env-hedge.drawing +601 -0
  101. package/kits/rpg-2d/drawings/env-house-blue.drawing +1 -0
  102. package/kits/rpg-2d/drawings/env-house-green.drawing +1 -0
  103. package/kits/rpg-2d/drawings/env-tree-oak.drawing +1540 -0
  104. package/kits/rpg-2d/drawings/env-tree-pine.drawing +1315 -0
  105. package/kits/rpg-2d/drawings/floor.drawing +70 -0
  106. package/kits/rpg-2d/drawings/fx-sparkle.drawing +926 -0
  107. package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +1099 -0
  108. package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +4177 -0
  109. package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +1099 -0
  110. package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +4177 -0
  111. package/kits/rpg-2d/drawings/player-idle-down.drawing +1070 -0
  112. package/kits/rpg-2d/drawings/player-idle-left.drawing +1070 -0
  113. package/kits/rpg-2d/drawings/player-idle-right.drawing +1070 -0
  114. package/kits/rpg-2d/drawings/player-idle-up.drawing +1070 -0
  115. package/kits/rpg-2d/drawings/player-walk-down.drawing +4148 -0
  116. package/kits/rpg-2d/drawings/player-walk-left.drawing +4148 -0
  117. package/kits/rpg-2d/drawings/player-walk-right.drawing +4148 -0
  118. package/kits/rpg-2d/drawings/player-walk-up.drawing +4148 -0
  119. package/kits/rpg-2d/editors/App.tsx +163 -0
  120. package/kits/rpg-2d/editors/CodeEditor.tsx +120 -0
  121. package/kits/rpg-2d/editors/DrawingEditor.tsx +278 -0
  122. package/kits/rpg-2d/editors/FileBrowser.tsx +191 -0
  123. package/kits/rpg-2d/editors/PlayOnly.tsx +26 -0
  124. package/kits/rpg-2d/editors/SceneEditor.tsx +1093 -0
  125. package/kits/rpg-2d/editors/behaviorRegistry.ts +33 -0
  126. package/kits/rpg-2d/editors/editorHistory.ts +75 -0
  127. package/kits/rpg-2d/editors/editorProps.ts +10 -0
  128. package/kits/rpg-2d/engine/ScenePlayer.tsx +130 -0
  129. package/kits/rpg-2d/engine/SceneUI.tsx +74 -0
  130. package/kits/rpg-2d/engine/TouchControls.tsx +157 -0
  131. package/kits/rpg-2d/engine/autoInspector.tsx +111 -0
  132. package/kits/rpg-2d/engine/drawing.ts +81 -0
  133. package/kits/rpg-2d/engine/files.ts +215 -0
  134. package/kits/rpg-2d/engine/scene.ts +484 -0
  135. package/kits/rpg-2d/engine/ui.module.css +928 -0
  136. package/kits/rpg-2d/engine/ui.tsx +483 -0
  137. package/kits/rpg-2d/eslint.config.js +46 -0
  138. package/kits/rpg-2d/index.html +11 -0
  139. package/kits/rpg-2d/main.tsx +14 -0
  140. package/kits/rpg-2d/package-lock.json +3149 -0
  141. package/kits/rpg-2d/package.json +46 -0
  142. package/kits/rpg-2d/scenes/main.scene +203 -0
  143. package/kits/rpg-2d/tsconfig.json +17 -0
  144. package/kits/rpg-2d/vite-env.d.ts +7 -0
  145. package/kits/rpg-2d/vite.config.js +1 -0
  146. package/package.json +27 -5
  147. package/AGENTS.md +0 -25
  148. package/dist/push.d.ts +0 -1
  149. package/src/api.ts +0 -160
  150. package/src/bundle.ts +0 -28
  151. package/src/config.ts +0 -36
  152. package/src/index.ts +0 -143
  153. package/src/init.ts +0 -71
  154. package/src/login.ts +0 -24
  155. package/src/preview.ts +0 -94
  156. package/src/push.ts +0 -118
  157. package/src/serve.ts +0 -134
  158. package/tsconfig.json +0 -13
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "basic-2d",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "serve": "node ../../cli/dist/index.js serve . --open",
7
+ "restart": "node ../../cli/dist/index.js restart .",
8
+ "screenshot": "node ../../cli/dist/index.js screenshot .",
9
+ "check": "eslint . && jscpd && node --input-type=module -e \"const { bundleProject } = await import('../../cli/dist/bundle.js'); await bundleProject('.');\""
10
+ },
11
+ "jscpd": {
12
+ "path": [
13
+ "engine",
14
+ "editors",
15
+ "behaviors"
16
+ ],
17
+ "threshold": 0,
18
+ "reporters": [
19
+ "consoleFull"
20
+ ]
21
+ },
22
+ "dependencies": {
23
+ "@codemirror/commands": "^6.10.3",
24
+ "@codemirror/lang-javascript": "^6.2.5",
25
+ "@codemirror/language": "^6.12.3",
26
+ "@codemirror/state": "^6.6.0",
27
+ "@codemirror/view": "^6.41.1",
28
+ "@fortawesome/free-solid-svg-icons": "^5.15.4",
29
+ "@lezer/highlight": "^1.2.3",
30
+ "castle-web-sdk": "file:../../sdk",
31
+ "codemirror": "^6.0.2",
32
+ "react": "^19.2.4",
33
+ "react-dom": "^19.2.4"
34
+ },
35
+ "devDependencies": {
36
+ "eslint": "^9.0.0",
37
+ "eslint-plugin-react-hooks": "^5.0.0",
38
+ "jscpd": "^4.0.5",
39
+ "prettier": "^3.8.3"
40
+ }
41
+ }
@@ -0,0 +1,108 @@
1
+ {
2
+ "name": "Main Scene",
3
+ "background": "#1b2030",
4
+ "actors": [
5
+ {
6
+ "id": "actor_1",
7
+ "components": {
8
+ "Layout": {
9
+ "x": 80,
10
+ "y": 115,
11
+ "width": 64,
12
+ "height": 64,
13
+ "z": 0,
14
+ "rotation": 0
15
+ },
16
+ "Drawing": {
17
+ "file": "drawings/floor.drawing",
18
+ "tint": "#ff6b6bff"
19
+ }
20
+ }
21
+ },
22
+ {
23
+ "id": "actor_2",
24
+ "components": {
25
+ "Layout": {
26
+ "x": 172,
27
+ "y": 160,
28
+ "width": 48,
29
+ "height": 48,
30
+ "z": 0,
31
+ "rotation": 0
32
+ },
33
+ "Drawing": {
34
+ "file": "drawings/floor.drawing",
35
+ "tint": "#ffd166ff"
36
+ }
37
+ }
38
+ },
39
+ {
40
+ "id": "actor_3",
41
+ "components": {
42
+ "Layout": {
43
+ "x": 229,
44
+ "y": 63,
45
+ "width": 80,
46
+ "height": 56,
47
+ "z": 0,
48
+ "rotation": 0
49
+ },
50
+ "Drawing": {
51
+ "file": "drawings/floor.drawing",
52
+ "tint": "#06d6a0ff"
53
+ }
54
+ }
55
+ },
56
+ {
57
+ "id": "actor_4",
58
+ "components": {
59
+ "Layout": {
60
+ "x": 184,
61
+ "y": 239,
62
+ "width": 56,
63
+ "height": 56,
64
+ "z": 0,
65
+ "rotation": 0
66
+ },
67
+ "Drawing": {
68
+ "file": "drawings/floor.drawing",
69
+ "tint": "#4dabf7ff"
70
+ }
71
+ }
72
+ },
73
+ {
74
+ "id": "actor_5",
75
+ "components": {
76
+ "Layout": {
77
+ "x": 324,
78
+ "y": 259,
79
+ "width": 64,
80
+ "height": 80,
81
+ "z": 0,
82
+ "rotation": 0
83
+ },
84
+ "Drawing": {
85
+ "file": "drawings/floor.drawing",
86
+ "tint": "#b197fcff"
87
+ }
88
+ }
89
+ },
90
+ {
91
+ "id": "actor_6",
92
+ "components": {
93
+ "Layout": {
94
+ "x": 292,
95
+ "y": 389,
96
+ "width": 96,
97
+ "height": 64,
98
+ "z": 0,
99
+ "rotation": 0
100
+ },
101
+ "Drawing": {
102
+ "file": "drawings/floor.drawing",
103
+ "tint": "#f06595ff"
104
+ }
105
+ }
106
+ }
107
+ ]
108
+ }
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1,8 @@
1
+ {
2
+ "printWidth": 100,
3
+ "tabWidth": 2,
4
+ "singleQuote": true,
5
+ "bracketSameLine": true,
6
+ "trailingComma": "es5",
7
+ "arrowParens": "always"
8
+ }
@@ -0,0 +1,131 @@
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.
@@ -0,0 +1,43 @@
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
+ }
@@ -0,0 +1,71 @@
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
+ }
@@ -0,0 +1,139 @@
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
+ }
@@ -0,0 +1,16 @@
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
+ }
@@ -0,0 +1,70 @@
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
+ }