castle-web-cli 0.4.74 → 0.4.76

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 (72) hide show
  1. package/dist/agent-prompts.d.ts +4 -0
  2. package/dist/agent-prompts.js +20 -8
  3. package/dist/agent.js +301 -28
  4. package/dist/castle-host/host.js +59 -5
  5. package/dist/commonInstructions.d.ts +1 -1
  6. package/dist/commonInstructions.js +5 -1
  7. package/dist/ide.d.ts +1 -0
  8. package/dist/ide.js +250 -1
  9. package/dist/init.js +46 -4
  10. package/dist/save-deck.js +8 -1
  11. package/dist/serve.js +27 -1
  12. package/dist/shell/assets/index-DNWEQd4R.js +141 -0
  13. package/dist/shell/assets/{index-WNbOHPBj.css → index-DuKq-Grp.css} +1 -1
  14. package/dist/shell/index.html +2 -2
  15. package/kits/basic-2d/CLAUDE.md +12 -4
  16. package/kits/basic-2d/castle.json +15 -0
  17. package/kits/basic-2d/drawings/pig.pxart +22 -55
  18. package/kits/basic-2d/editors/PxArtEditor.jsx +237 -4
  19. package/kits/basic-2d/editors/SceneEditor.jsx +0 -3
  20. package/kits/basic-2d/editors/SingleEditor.jsx +6 -51
  21. package/kits/basic-2d/editors/pixelEditorChrome.jsx +17 -11
  22. package/kits/basic-2d/editors/pixelGeometry.js +95 -0
  23. package/kits/basic-2d/editors/pixelInspector.jsx +229 -51
  24. package/kits/basic-2d/editors/pxArtTools.js +109 -1
  25. package/kits/basic-2d/engine/ScenePlayer.jsx +11 -163
  26. package/kits/basic-2d/engine/ui.jsx +15 -0
  27. package/kits/basic-2d/engine/ui.module.css +44 -117
  28. package/kits/basic-2d/main.jsx +6 -10
  29. package/kits/basic-2d/pnpm-workspace.yaml +3 -0
  30. package/package.json +10 -1
  31. package/dist/shell/assets/index-Dfn29Bkt.js +0 -108
  32. package/kits/basic-2d/editors/App.jsx +0 -226
  33. package/kits/basic-2d/editors/CodeEditor.jsx +0 -79
  34. package/kits/basic-2d/editors/FileBrowser.jsx +0 -349
  35. package/kits/basic-2d/editors/codeTheme.js +0 -135
  36. package/kits/basic-2d/engine/TouchControls.jsx +0 -136
  37. package/kits/basic-2d/engine/playConsole.js +0 -66
  38. package/kits/basic-3d/.prettierrc +0 -8
  39. package/kits/basic-3d/CLAUDE.md +0 -162
  40. package/kits/basic-3d/behaviors/Camera.jsx +0 -56
  41. package/kits/basic-3d/behaviors/Collider.jsx +0 -78
  42. package/kits/basic-3d/behaviors/Mesh.jsx +0 -82
  43. package/kits/basic-3d/behaviors/Model.jsx +0 -61
  44. package/kits/basic-3d/behaviors/Transform.jsx +0 -35
  45. package/kits/basic-3d/editors/App.jsx +0 -147
  46. package/kits/basic-3d/editors/CodeEditor.jsx +0 -112
  47. package/kits/basic-3d/editors/FileBrowser.jsx +0 -143
  48. package/kits/basic-3d/editors/ModelEditor.jsx +0 -400
  49. package/kits/basic-3d/editors/PlayOnly.jsx +0 -22
  50. package/kits/basic-3d/editors/SceneEditor.jsx +0 -1087
  51. package/kits/basic-3d/editors/behaviorRegistry.js +0 -24
  52. package/kits/basic-3d/editors/editorHistory.js +0 -52
  53. package/kits/basic-3d/editors/viewportRig.js +0 -90
  54. package/kits/basic-3d/engine/ScenePlayer.jsx +0 -61
  55. package/kits/basic-3d/engine/SceneUI.jsx +0 -67
  56. package/kits/basic-3d/engine/SceneViewport.jsx +0 -102
  57. package/kits/basic-3d/engine/TouchControls.jsx +0 -136
  58. package/kits/basic-3d/engine/autoInspector.jsx +0 -51
  59. package/kits/basic-3d/engine/files.js +0 -73
  60. package/kits/basic-3d/engine/scene.js +0 -502
  61. package/kits/basic-3d/engine/threeUtil.js +0 -260
  62. package/kits/basic-3d/engine/ui.jsx +0 -352
  63. package/kits/basic-3d/engine/ui.module.css +0 -944
  64. package/kits/basic-3d/eslint.config.js +0 -51
  65. package/kits/basic-3d/index.html +0 -11
  66. package/kits/basic-3d/main.jsx +0 -10
  67. package/kits/basic-3d/models/block.model +0 -14
  68. package/kits/basic-3d/package-lock.json +0 -2713
  69. package/kits/basic-3d/package.json +0 -41
  70. package/kits/basic-3d/pnpm-lock.yaml +0 -1769
  71. package/kits/basic-3d/scenes/main.scene +0 -76
  72. package/kits/basic-3d/vite.config.js +0 -1
@@ -1,135 +0,0 @@
1
- import { HighlightStyle } from '@codemirror/language';
2
- import { EditorView } from '@codemirror/view';
3
- import { tags } from '@lezer/highlight';
4
-
5
- // Dark theme for the in-kit CodeMirror editor. Surfaces reference the kit's
6
- // shared `--castle-*` tokens (defined on `.editor-root` in ui.module.css) so
7
- // the editor stays in sync with the rest of the dark inspector palette. Syntax
8
- // token colors are literal hex chosen to read well on the #0d0d0d background.
9
- const selectionBg = 'rgba(230, 230, 230, 0.22)';
10
-
11
- export const castleCodeTheme = EditorView.theme(
12
- {
13
- '&': {
14
- height: '100%',
15
- fontSize: '9pt',
16
- color: 'var(--castle-inspector-text)',
17
- backgroundColor: 'var(--castle-inspector-bg)',
18
- },
19
- '&.cm-editor.cm-focused': {
20
- outline: 'none',
21
- },
22
- '.cm-scroller': {
23
- overflow: 'auto',
24
- fontFamily: 'Menlo, Monaco, Lucida Console, monospace',
25
- },
26
- '.cm-content': {
27
- minHeight: '100%',
28
- caretColor: 'var(--castle-black)',
29
- color: 'var(--castle-inspector-text)',
30
- paddingBottom: '400px',
31
- paddingRight: '80px',
32
- },
33
- '.cm-cursor, .cm-dropCursor': {
34
- borderLeftColor: 'var(--castle-black)',
35
- },
36
- '&.cm-focused .cm-cursor': {
37
- borderLeftColor: 'var(--castle-black)',
38
- },
39
- '.cm-selectionBackground, .cm-content ::selection': {
40
- backgroundColor: selectionBg,
41
- },
42
- '&.cm-focused .cm-selectionBackground, &.cm-focused .cm-content ::selection': {
43
- backgroundColor: selectionBg,
44
- },
45
- '.cm-gutters': {
46
- // Gutters are hidden in this editor; keep them themed in case they are
47
- // re-enabled so they don't flash a light surface.
48
- display: 'none',
49
- backgroundColor: 'var(--castle-inspector-bg)',
50
- color: 'var(--castle-inspector-muted)',
51
- borderRight: '1px solid var(--castle-inspector-divider)',
52
- },
53
- '.cm-activeLine': {
54
- backgroundColor: 'var(--castle-inspector-input-bg)',
55
- },
56
- '.cm-activeLineGutter': {
57
- color: 'var(--castle-inspector-text)',
58
- backgroundColor: 'var(--castle-inspector-input-bg)',
59
- },
60
- '.cm-foldPlaceholder': {
61
- backgroundColor: 'var(--castle-inspector-input-bg)',
62
- border: '1px solid var(--castle-inspector-border)',
63
- color: 'var(--castle-inspector-muted)',
64
- },
65
- '.cm-matchingBracket, &.cm-focused .cm-matchingBracket': {
66
- backgroundColor: 'rgba(230, 230, 230, 0.18)',
67
- outline: '1px solid var(--castle-black)',
68
- color: 'inherit',
69
- },
70
- '.cm-nonmatchingBracket, &.cm-focused .cm-nonmatchingBracket': {
71
- backgroundColor: 'rgba(232, 106, 115, 0.35)',
72
- },
73
- '.cm-searchMatch': {
74
- backgroundColor: 'rgba(255, 203, 107, 0.25)',
75
- outline: '1px solid rgba(255, 203, 107, 0.5)',
76
- },
77
- '.cm-searchMatch.cm-searchMatch-selected': {
78
- backgroundColor: 'rgba(255, 203, 107, 0.45)',
79
- },
80
- '.cm-selectionMatch': {
81
- backgroundColor: 'rgba(230, 230, 230, 0.14)',
82
- },
83
- '.cm-panels': {
84
- backgroundColor: 'var(--castle-inspector-input-bg)',
85
- color: 'var(--castle-inspector-text)',
86
- },
87
- '.cm-panels.cm-panels-top': {
88
- borderBottom: '1px solid var(--castle-inspector-divider)',
89
- },
90
- '.cm-panels.cm-panels-bottom': {
91
- borderTop: '1px solid var(--castle-inspector-divider)',
92
- },
93
- '.cm-panel.cm-search input, .cm-panel.cm-search button, .cm-panel.cm-search label': {
94
- color: 'var(--castle-inspector-text)',
95
- },
96
- '.cm-panel.cm-search input': {
97
- backgroundColor: 'var(--castle-inspector-bg)',
98
- border: '1px solid var(--castle-inspector-border)',
99
- },
100
- '.cm-tooltip': {
101
- backgroundColor: 'var(--castle-inspector-input-bg)',
102
- border: '1px solid var(--castle-inspector-border)',
103
- color: 'var(--castle-inspector-text)',
104
- },
105
- '.cm-tooltip-autocomplete > ul > li[aria-selected]': {
106
- backgroundColor: 'var(--castle-black)',
107
- color: 'var(--castle-inspector-text)',
108
- },
109
- },
110
- { dark: true },
111
- );
112
-
113
- export const castleHighlightStyle = HighlightStyle.define([
114
- { tag: [tags.keyword, tags.modifier, tags.operatorKeyword], color: '#b3b9d1' },
115
- { tag: [tags.controlKeyword, tags.moduleKeyword], color: '#b3b9d1' },
116
- { tag: [tags.definitionKeyword, tags.self], color: '#b3b9d1' },
117
- { tag: [tags.string, tags.special(tags.string), tags.regexp], color: '#c3e88d' },
118
- { tag: tags.escape, color: '#f78c6c' },
119
- { tag: [tags.number, tags.integer, tags.float, tags.bool, tags.null], color: '#f78c6c' },
120
- { tag: [tags.literal, tags.atom, tags.constant(tags.variableName)], color: '#f78c6c' },
121
- { tag: [tags.comment, tags.lineComment, tags.blockComment, tags.docComment], color: '#676e95', fontStyle: 'italic' },
122
- { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: '#82aaff' },
123
- { tag: tags.definition(tags.variableName), color: '#e6e6e6' },
124
- { tag: [tags.variableName, tags.labelName], color: '#e6e6e6' },
125
- { tag: tags.propertyName, color: '#b2ccd6' },
126
- { tag: [tags.typeName, tags.className, tags.namespace], color: '#ffcb6b' },
127
- { tag: [tags.operator, tags.punctuation, tags.bracket, tags.angleBracket, tags.brace, tags.squareBracket], color: '#89ddff' },
128
- { tag: [tags.tagName], color: '#f07178' },
129
- { tag: [tags.attributeName], color: '#b3b9d1' },
130
- { tag: [tags.attributeValue], color: '#c3e88d' },
131
- { tag: tags.link, color: '#82aaff', textDecoration: 'underline' },
132
- { tag: tags.strong, fontWeight: 'bold' },
133
- { tag: tags.emphasis, fontStyle: 'italic' },
134
- { tag: tags.invalid, color: '#e86a73' },
135
- ]);
@@ -1,136 +0,0 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
2
- // On-screen movement controls: a 4-way d-pad feeds arrow keys onto
3
- // `scene.keys` -- the exact same keyboard keys behaviors consume, no separate
4
- // input path.
5
- const KEYS = {
6
- up: 'ArrowUp',
7
- down: 'ArrowDown',
8
- left: 'ArrowLeft',
9
- right: 'ArrowRight',
10
- };
11
- export function TouchControls({ getKeys, visible = true }) {
12
- const [isTouch, setIsTouch] = useState(false);
13
- const heldRef = useRef(new Set());
14
- useEffect(() => {
15
- const params = new URLSearchParams(window.location.search);
16
- const override = params.get('touch');
17
- if (override === '1' || override === 'true') {
18
- setIsTouch(true);
19
- return undefined;
20
- }
21
- if (override === '0' || override === 'false') {
22
- setIsTouch(false);
23
- return undefined;
24
- }
25
- const mq = window.matchMedia('(pointer: coarse)');
26
- const update = () => {
27
- setIsTouch(mq.matches || 'ontouchstart' in window || navigator.maxTouchPoints > 0);
28
- };
29
- update();
30
- mq.addEventListener?.('change', update);
31
- return () => mq.removeEventListener?.('change', update);
32
- }, []);
33
- const release = useCallback(
34
- (id) => {
35
- if (!heldRef.current.has(id)) return;
36
- heldRef.current.delete(id);
37
- getKeys()?.delete(KEYS[id]);
38
- },
39
- [getKeys]
40
- );
41
- useEffect(() => {
42
- if (!visible || !isTouch) {
43
- // Drop any keys we were holding when controls hide.
44
- const keys = getKeys();
45
- if (keys) for (const id of heldRef.current) keys.delete(KEYS[id]);
46
- heldRef.current.clear();
47
- }
48
- }, [visible, isTouch, getKeys]);
49
- if (!visible || !isTouch) return null;
50
- const press = (id) => {
51
- heldRef.current.add(id);
52
- getKeys()?.add(KEYS[id]);
53
- };
54
- const button = (id, label, extra) => (
55
- <button
56
- key={id}
57
- type="button"
58
- tabIndex={-1}
59
- aria-label={id}
60
- onTouchStart={(event) => {
61
- event.preventDefault();
62
- press(id);
63
- }}
64
- onTouchEnd={(event) => {
65
- event.preventDefault();
66
- release(id);
67
- }}
68
- onTouchCancel={(event) => {
69
- event.preventDefault();
70
- release(id);
71
- }}
72
- onMouseDown={(event) => {
73
- event.preventDefault();
74
- press(id);
75
- }}
76
- onMouseUp={() => release(id)}
77
- onMouseLeave={() => release(id)}
78
- onContextMenu={(event) => event.preventDefault()}
79
- style={{ ...buttonStyle, ...extra }}>
80
- {label}
81
- </button>
82
- );
83
- return (
84
- <div style={containerStyle}>
85
- <div style={dpadStyle}>
86
- {button('up', '▲', dpadUpStyle)}
87
- {button('left', '◀', dpadLeftStyle)}
88
- {button('right', '▶', dpadRightStyle)}
89
- {button('down', '▼', dpadDownStyle)}
90
- </div>
91
- </div>
92
- );
93
- }
94
- const containerStyle = {
95
- position: 'fixed',
96
- left: 0,
97
- right: 0,
98
- bottom: 0,
99
- display: 'flex',
100
- justifyContent: 'flex-start',
101
- alignItems: 'flex-end',
102
- padding: '12px 16px calc(env(safe-area-inset-bottom, 0px) + 12px)',
103
- pointerEvents: 'none',
104
- zIndex: 50,
105
- };
106
- // 3x3 grid -- the d-pad arms occupy the cross cells, corners stay empty.
107
- const dpadStyle = {
108
- display: 'grid',
109
- gridTemplateColumns: 'repeat(3, 64px)',
110
- gridTemplateRows: 'repeat(3, 64px)',
111
- gap: 6,
112
- pointerEvents: 'auto',
113
- };
114
- const dpadUpStyle = { gridColumn: 2, gridRow: 1 };
115
- const dpadLeftStyle = { gridColumn: 1, gridRow: 2 };
116
- const dpadRightStyle = { gridColumn: 3, gridRow: 2 };
117
- const dpadDownStyle = { gridColumn: 2, gridRow: 3 };
118
- const buttonStyle = {
119
- width: 64,
120
- height: 64,
121
- borderRadius: 12,
122
- border: '1px solid rgba(255, 255, 255, 0.35)',
123
- background: 'rgba(0, 0, 0, 0.35)',
124
- color: 'rgba(255, 255, 255, 0.92)',
125
- fontSize: 24,
126
- fontWeight: 600,
127
- display: 'flex',
128
- alignItems: 'center',
129
- justifyContent: 'center',
130
- userSelect: 'none',
131
- WebkitUserSelect: 'none',
132
- WebkitTouchCallout: 'none',
133
- WebkitTapHighlightColor: 'transparent',
134
- touchAction: 'none',
135
- cursor: 'pointer',
136
- };
@@ -1,66 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- // Captures console output from the running deck into an in-memory ring buffer and
3
- // notifies subscribers, so the on-panel play console can display it.
4
- //
5
- // The SDK already wraps console (to forward logs to the dev server over its
6
- // websocket); this wraps ON TOP and always calls through, so that forwarding and
7
- // the real devtools output are preserved. Install is idempotent.
8
- const MAX_LINES = 500;
9
- let buffer = [];
10
- let nextId = 1;
11
- let installed = false;
12
- const subscribers = new Set();
13
- function formatArgs(args) {
14
- return args
15
- .map((arg) => {
16
- if (typeof arg === 'string') return arg;
17
- if (arg instanceof Error) return arg.stack || arg.message;
18
- try {
19
- const json = JSON.stringify(arg);
20
- return json ?? String(arg);
21
- } catch {
22
- return String(arg);
23
- }
24
- })
25
- .join(' ');
26
- }
27
- function emit(level, args) {
28
- const entry = { id: nextId++, level, text: formatArgs(args) };
29
- const next = buffer.concat(entry);
30
- buffer = next.length > MAX_LINES ? next.slice(next.length - MAX_LINES) : next;
31
- for (const cb of subscribers) cb(buffer);
32
- }
33
- export function installConsoleCapture() {
34
- if (installed || typeof console === 'undefined') return;
35
- installed = true;
36
- for (const level of ['log', 'warn', 'error']) {
37
- const original = typeof console[level] === 'function' ? console[level].bind(console) : null;
38
- console[level] = (...args) => {
39
- if (original) original(...args);
40
- // Never let capture throw into the caller's logging path.
41
- try {
42
- emit(level, args);
43
- } catch {
44
- // ignore
45
- }
46
- };
47
- }
48
- }
49
- export function subscribeLogs(callback) {
50
- subscribers.add(callback);
51
- callback(buffer);
52
- return () => subscribers.delete(callback);
53
- }
54
- export function clearLogs() {
55
- buffer = [];
56
- for (const cb of subscribers) cb(buffer);
57
- }
58
- // React hook: install capture (idempotent) and track the live log buffer.
59
- export function usePlayLogs() {
60
- const [logs, setLogs] = useState(buffer);
61
- useEffect(() => {
62
- installConsoleCapture();
63
- return subscribeLogs(setLogs);
64
- }, []);
65
- return { logs, clearLogs };
66
- }
@@ -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,162 +0,0 @@
1
- # basic-3d kit
2
-
3
- Actor / behavior / scene framework on a three.js WebGL viewport. 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 (`Transform.jsx`, `Mesh.jsx`, `Model.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
- The deck is already serving when you start (`castle-web init` set that up; see `.castle/serve.json` for the URL). The user is watching that page right now. Your job is to make it interesting incrementally:
16
-
17
- 1. **Build incrementally.** Start with the smallest playable thing (one mechanic, one scene change), `npm run restart`, then add the next piece. Do NOT write the whole game in one shot.
18
- 2. **After every edit:** `npm run restart` (no hot reload). The served page refreshes and the user sees the change.
19
-
20
- World coordinates: **+Y is up, the ground plane is XZ**, units are arbitrary world units (a character is ~1–2 units tall). Rotations are degrees. `x`/`y`/`z` on Transform is the actor's CENTER.
21
-
22
- ## Behavior shape
23
-
24
- A behavior is a class. Minimal contract:
25
-
26
- ```jsx
27
- // behaviors/MyThing.jsx
28
- export class MyThing {
29
- static behaviorName = 'MyThing'; // must match the key used in .scene
30
- static defaultProps = { speed: 6 };
31
-
32
- constructor(props) {
33
- this.props = props;
34
- }
35
-
36
- // Called every frame in play mode. dt is seconds.
37
- update(actor, scene, dt) {
38
- const transform = actor.components.Transform;
39
- transform.x += this.props.speed * dt; // mutate component in place
40
- }
41
-
42
- // Optional. Custom three.js objects (you usually don't need this; use a
43
- // Mesh or Model component instead). Runs every frame in BOTH edit and play
44
- // mode. Attach objects under `scene.actorGroup(actor)` and cache them on
45
- // `actor.runtime` so you don't rebuild every frame.
46
- sync(actor, scene, dt) {}
47
-
48
- // Optional. Return React nodes for game-time HUD. Coordinates are a fixed
49
- // 500x700 design space scaled over the viewport. Read state your `update`
50
- // set; do not start your own loops.
51
- ui(actor, scene) {
52
- return null;
53
- }
54
- }
55
- ```
56
-
57
- 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.
58
-
59
- `sync` is the retained-mode analog of a draw call: the framework keeps a `THREE.Group` per actor (`scene.actorGroup(actor)`, already positioned by Transform); your sync adds/updates children. Pattern: build once, cache on `actor.runtime._myThing`, rebuild only when a props key changes (`replaceGroupChild` from `engine/threeUtil` disposes the old object). Setting `actor.runtime.hidden = true` makes the built-in Mesh/Model behaviors skip rendering.
60
-
61
- ## Scene file (`scenes/main.scene`, plain JSON)
62
-
63
- ```json
64
- {
65
- "background": "#10131c",
66
- "lighting": { "sunAzimuth": 35, "sunElevation": 55, "sunIntensity": 2.4 },
67
- "actors": [
68
- {
69
- "id": "player",
70
- "components": {
71
- "Transform": { "x": 0, "y": 0.5, "z": 0 },
72
- "Mesh": { "geometry": "capsule", "height": 1.6, "color": "#8db7ffff" },
73
- "Collider": { "width": 1, "height": 1.6, "depth": 1 },
74
- "Mover": { "speed": 6 }
75
- }
76
- }
77
- ]
78
- }
79
- ```
80
-
81
- Rules: every actor needs a unique `id` (any string) and almost always a `Transform`. `components` keys are behavior names (the `static behaviorName`). Unspecified props fall back to that behavior's `defaultProps` — omit optional fields (rotations, scales, `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.
82
-
83
- Scene-level fields: `background` (hex color), `lighting` (hemisphere + shadowed sun: `sky`, `ground`, `hemiIntensity`, `sun`, `sunIntensity`, `sunAzimuth`, `sunElevation`, `shadows`), `editor` (`snapToGrid`, `gridSize`). All optional with sensible defaults.
84
-
85
- ## Built-in behaviors
86
-
87
- - **Transform** — `{ x, y, z, rotationX?, rotationY?, rotationZ?, scaleX?, scaleY?, scaleZ? }`. Every actor needs one. Position is the actor's center; rotations are degrees.
88
- - **Mesh** — `{ geometry: 'box'|'sphere'|'cylinder'|'cone'|'capsule'|'torus'|'plane', width, height, depth, color, roughness?, metalness?, emissive?, opacity?, castShadow?, receiveShadow? }`. One sized primitive with a PBR material. `width`/`height`/`depth` are the bounding dimensions for every geometry kind. Colors are `#rrggbbaa` (alpha multiplies opacity).
89
- - **Model** — `{ file: "models/foo.model", tint?: "#rrggbbaa" }`. Renders a multi-part `.model` file (see below). `tint` multiplies every part color; use white or omit for the original colors.
90
- - **Collider** — `{ kind: 'solid'|'pickup', width, height, depth, offsetX?, offsetY?, offsetZ?, debug? }`. An axis-aligned box centered on the Transform plus offset; the framework does NOT auto-resolve collisions for you. Use it as data:
91
-
92
- ```jsx
93
- for (const other of scene.getActors()) {
94
- if (other.id === actor.id) continue;
95
- if (scene.overlaps(actor, other)) {
96
- /* react */
97
- }
98
- }
99
- ```
100
-
101
- - **Camera** — `{ target: actorId, mode: 'follow'|'orbit', distance, azimuth, elevation, fov, lookHeight?, lerp?, orbitSpeed? }`. Place on a dedicated actor; sets `scene.camera` each frame. `follow` keeps fixed angles tracking the target (`lerp` > 0 smooths, e.g. 6); `orbit` spins the azimuth at `orbitSpeed` deg/s. Low `fov` (15–25) + high `elevation` (~40–55) + large `distance` reads near-isometric. Omit the behavior entirely for a fixed default view.
102
-
103
- ## Model file (`models/*.model`, plain JSON)
104
-
105
- A model is a list of primitive parts in actor-local units (the 3d analog of a pixel `.drawing`):
106
-
107
- ```json
108
- {
109
- "parts": [
110
- { "geometry": "cylinder", "y": 0.4, "width": 0.5, "height": 0.8, "depth": 0.5, "color": "#7a5230ff" },
111
- { "geometry": "sphere", "y": 1.2, "width": 1.4, "height": 1.2, "depth": 1.4, "color": "#3e7c3aff" }
112
- ]
113
- }
114
- ```
115
-
116
- Part fields: `geometry`, `x/y/z` (offset), `rotationX/Y/Z` (degrees), `width/height/depth`, `color`, `roughness?`, `metalness?`. Edit visually in the model editor (click a part, gizmo moves/rotates it) or by hand. The actor's Transform scale resizes the whole model.
117
-
118
- ## SceneRuntime API (what `scene` exposes to behaviors)
119
-
120
- - `scene.time` — seconds since start.
121
- - `scene.keys` — `Set` of currently-held keys, both `event.key` and `event.code` forms (`'ArrowLeft'`, `'a'`, `'KeyA'`, `' '`, `'Space'`). Read in `update`.
122
- - `scene.pointer` — `{ x, y, down, ground }`. `x`/`y` are normalized device coords (-1..1); `ground` is `{ x, z }` where the pointer ray hits the y=0 plane (or null).
123
- - `scene.getActor(id)` / `scene.getActors()` / `scene.getComponent(actor, name)`.
124
- - `scene.actorWith('GameController')` / `scene.actorsWith('Tree')` — find one / all actors carrying a given behavior. Prefer these to `getActors().find(a => a.components.X)`.
125
- - `scene.colliderBox(actorOrId)` — center-based box `{ x, y, z, width, height, depth }` from Transform + Collider, or null.
126
- - `scene.overlaps(a, b)` — true when two actors/ids with Collider overlap.
127
- - `scene.data` — the live scene data. Mutate `actor.components.X = {...}` to change props.
128
- - `scene.spawnActor({ components: { Transform: {...}, 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.
129
- - `scene.despawnActor(id)` — remove an actor at runtime (disposes its three objects). Use this; don't `splice` + `delete` by hand.
130
- - `scene.status` — string you can set/read for game-state ('playing', 'gameover', ...).
131
- - `scene.actorGroup(actor)` — the actor's `THREE.Group` (for custom `sync`).
132
- - `scene.three` — the `THREE.Scene` itself (escape hatch for scene-wide effects).
133
- - `actor.runtime` — per-instance scratchpad for transient state across frames (e.g. velocity). Not serialized.
134
-
135
- ## Input shortcuts
136
-
137
- ```jsx
138
- if (scene.keys.has('ArrowLeft') || scene.keys.has('KeyA')) transform.x -= speed * dt;
139
- if (scene.keys.has('ArrowRight') || scene.keys.has('KeyD')) transform.x += speed * dt;
140
- if (scene.keys.has('KeyJ')) /* jump */ ;
141
- ```
142
-
143
- **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.
144
-
145
- For HUD text use a behavior's `ui` hook (returns React, 500x700 design space); for in-world visuals, use Mesh/Model components or a custom `sync`. The deck has TouchControls overlay support out of the box for mobile play (arrow keys d-pad); no setup needed.
146
-
147
- ## Common exploration-shaped recipe (sketch)
148
-
149
- - `Mover` behavior on the player: read keys, move on XZ, set `transform.rotationY` to the facing direction.
150
- - `Camera` actor: `{ target: 'player', mode: 'follow', distance: 30, elevation: 45, fov: 20, lerp: 6 }` for a near-isometric follow view.
151
- - Ground: a large flat `Mesh` (`geometry: 'box'`, thin height, `castShadow: false`).
152
- - Pickups: `Collider { kind: 'pickup' }` + a marker behavior; on `scene.overlaps(player, pickup)` set `pickup.runtime.hidden = true` or `scene.despawnActor(...)`.
153
- - `GameController` (Transform optional): tracks score / status; expose HUD via `ui()`.
154
-
155
- ## Don't
156
-
157
- - Don't `console.log` in tight loops — flood the serve log.
158
- - Don't keep per-actor state on the behavior class instance; it's recreated each frame. Use `actor.runtime` or component props.
159
- - Don't try to import from `editors/`; behaviors run in the play runtime too.
160
- - Don't create three.js objects every frame in `sync` — build once, cache on `actor.runtime`, rebuild on a props-key change.
161
- - Don't add types or `.ts`/`.tsx` files. This kit is JavaScript.
162
- - Don't add a build step or change `vite.config.js` for a game — it's configured for you.
@@ -1,56 +0,0 @@
1
- // Play-mode camera. Place on a dedicated actor (or the target itself). Two
2
- // modes:
3
- // - 'follow': fixed azimuth/elevation/distance offset tracking the target's
4
- // position (optionally smoothed with `lerp`, 0 = instant).
5
- // - 'orbit': same, but the azimuth spins at `orbitSpeed` degrees/second.
6
- // With no `target` the camera looks at the world origin. Omit the behavior
7
- // entirely for a fixed default view. A low `fov` + high `elevation` reads
8
- // near-isometric.
9
- export class Camera {
10
- static behaviorName = 'Camera';
11
-
12
- static defaultProps = {
13
- target: '',
14
- mode: 'follow',
15
- distance: 24,
16
- azimuth: 45,
17
- elevation: 35,
18
- fov: 50,
19
- lookHeight: 0,
20
- lerp: 0,
21
- orbitSpeed: 20,
22
- };
23
-
24
- constructor(props) {
25
- this.props = props;
26
- }
27
-
28
- update(actor, scene, dt) {
29
- const target = this.props.target ? scene.getActor(this.props.target) : null;
30
- const transform = target?.components.Transform;
31
- const goal = {
32
- x: transform?.x ?? 0,
33
- y: (transform?.y ?? 0) + this.props.lookHeight,
34
- z: transform?.z ?? 0,
35
- };
36
- const state = (actor.runtime._camera ??= { ...goal, azimuth: this.props.azimuth });
37
- if (this.props.mode === 'orbit') {
38
- state.azimuth += this.props.orbitSpeed * dt;
39
- } else {
40
- state.azimuth = this.props.azimuth;
41
- }
42
- const t = this.props.lerp > 0 ? Math.min(1, dt * this.props.lerp) : 1;
43
- state.x += (goal.x - state.x) * t;
44
- state.y += (goal.y - state.y) * t;
45
- state.z += (goal.z - state.z) * t;
46
- scene.camera = {
47
- targetX: state.x,
48
- targetY: state.y,
49
- targetZ: state.z,
50
- azimuth: state.azimuth,
51
- elevation: this.props.elevation,
52
- distance: this.props.distance,
53
- fov: this.props.fov,
54
- };
55
- }
56
- }
@@ -1,78 +0,0 @@
1
- import React from 'react';
2
- import * as THREE from 'three';
3
- import { Panel, SelectField } from '../engine/ui';
4
- import { AutoFields } from '../engine/autoInspector';
5
- import { replaceGroupChild } from '../engine/threeUtil';
6
-
7
- // An axis-aligned box, centered on the actor's Transform plus an offset. Pure
8
- // data -- the framework does NOT auto-resolve collisions; behaviors read
9
- // overlaps via `scene.overlaps(a, b)` / `scene.colliderBox(actor)`.
10
- export class Collider {
11
- static behaviorName = 'Collider';
12
-
13
- static defaultProps = {
14
- kind: 'solid',
15
- width: 1,
16
- height: 1,
17
- depth: 1,
18
- offsetX: 0,
19
- offsetY: 0,
20
- offsetZ: 0,
21
- debug: false,
22
- };
23
-
24
- constructor(props) {
25
- this.props = props;
26
- }
27
-
28
- sync(actor, scene, _dt, options) {
29
- const group = scene.actorGroup(actor);
30
- if (!group) return;
31
- const cache = actor.runtime;
32
- const show = !!(options?.showDebugColliders || this.props.debug);
33
- if (!show) {
34
- if (cache._colliderDebug) {
35
- cache._colliderDebug = replaceGroupChild(group, cache._colliderDebug, null);
36
- cache._colliderDebugKey = null;
37
- }
38
- return;
39
- }
40
- const key = JSON.stringify(this.props);
41
- if (!cache._colliderDebug || cache._colliderDebugKey !== key) {
42
- cache._colliderDebug = replaceGroupChild(group, cache._colliderDebug, makeDebugBox(this.props));
43
- cache._colliderDebugKey = key;
44
- }
45
- }
46
-
47
- static Inspector({ component, setComponent }) {
48
- return (
49
- <Panel title="Collider">
50
- <SelectField
51
- label="Kind"
52
- value={component.kind}
53
- onChange={(kind) => setComponent({ kind })}
54
- options={['solid', 'pickup']}
55
- />
56
- <AutoFields
57
- defaultProps={Collider.defaultProps}
58
- component={component}
59
- setComponent={setComponent}
60
- exclude={['kind']}
61
- />
62
- </Panel>
63
- );
64
- }
65
- }
66
-
67
- function makeDebugBox(props) {
68
- const geometry = new THREE.BoxGeometry(props.width, props.height, props.depth);
69
- const edges = new THREE.EdgesGeometry(geometry);
70
- geometry.dispose();
71
- const material = new THREE.LineBasicMaterial({
72
- color: props.kind === 'pickup' ? 0xffe17a : 0x8db7ff,
73
- });
74
- const lines = new THREE.LineSegments(edges, material);
75
- lines.position.set(props.offsetX, props.offsetY, props.offsetZ);
76
- lines.userData.editorPlaceholder = true;
77
- return lines;
78
- }