castle-web-cli 0.4.1 → 0.4.3

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 +173 -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,222 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { basename, formatJson, parseJsonFile } from '../engine/files';
3
+ import {
4
+ cx,
5
+ EditorBody,
6
+ EditorHeader,
7
+ IconButton,
8
+ NumberField,
9
+ Panel,
10
+ SheetGrabHandle,
11
+ styles,
12
+ theme,
13
+ useMobileSheet,
14
+ } from '../engine/ui';
15
+ import { useEditHistory } from './editorHistory';
16
+ const palette = [
17
+ '#00000000',
18
+ '#000000FF',
19
+ '#050505FF',
20
+ '#111111FF',
21
+ '#242234FF',
22
+ '#444444FF',
23
+ '#777777FF',
24
+ '#ffffffFF',
25
+ '#e3e6ffFF',
26
+ '#8db7ffFF',
27
+ '#ff5d5dFF',
28
+ '#ffe17aFF',
29
+ ];
30
+ export function DrawingEditor({ path, text, onChange, onToggleFiles, filesOpen }) {
31
+ const canvasRef = useRef(null);
32
+ const strokeRef = useRef({ active: false, recorded: false });
33
+ const [color, setColor] = useState('#ffffffFF');
34
+ const [inspectorOpen, setInspectorOpen] = useState(true);
35
+ const history = useEditHistory(text, onChange);
36
+ const inspectorSheet = useInspectorSheet(inspectorOpen);
37
+ const { value: drawing, error } = parseJsonFile(path, text);
38
+ const isWide = drawing ? drawing.width >= drawing.height : true;
39
+ useEffect(() => {
40
+ if (!drawing || !canvasRef.current) return;
41
+ const canvas = canvasRef.current;
42
+ const ctx = canvas.getContext('2d');
43
+ if (!ctx) return;
44
+ canvas.width = drawing.width;
45
+ canvas.height = drawing.height;
46
+ ctx.clearRect(0, 0, drawing.width, drawing.height);
47
+ for (let y = 0; y < drawing.height; y++) {
48
+ for (let x = 0; x < drawing.width; x++) {
49
+ const pixel = drawing.pixels[y * drawing.width + x];
50
+ if (!pixel || pixel.endsWith('00')) continue;
51
+ ctx.fillStyle = pixel;
52
+ ctx.fillRect(x, y, 1, 1);
53
+ }
54
+ }
55
+ }, [drawing, text]);
56
+ function commitDrawing(nextDrawing) {
57
+ history.commit(formatJson(nextDrawing));
58
+ }
59
+ function resizeDrawing(nextWidth, nextHeight) {
60
+ if (!drawing) return;
61
+ const width = clampInteger(nextWidth, 1, 128);
62
+ const height = clampInteger(nextHeight, 1, 128);
63
+ if (width === drawing.width && height === drawing.height) return;
64
+ const pixels = Array(width * height).fill('#00000000');
65
+ const copiedWidth = Math.min(width, drawing.width);
66
+ const copiedHeight = Math.min(height, drawing.height);
67
+ for (let y = 0; y < copiedHeight; y++) {
68
+ for (let x = 0; x < copiedWidth; x++) {
69
+ pixels[y * width + x] = drawing.pixels[y * drawing.width + x] ?? '#00000000';
70
+ }
71
+ }
72
+ commitDrawing({ ...drawing, width, height, pixels });
73
+ }
74
+ function paintAt(event) {
75
+ if (!drawing) return;
76
+ const rect = event.currentTarget.getBoundingClientRect();
77
+ const x = Math.floor(((event.clientX - rect.left) / rect.width) * drawing.width);
78
+ const y = Math.floor(((event.clientY - rect.top) / rect.height) * drawing.height);
79
+ if (x < 0 || y < 0 || x >= drawing.width || y >= drawing.height) return;
80
+ if (drawing.pixels[y * drawing.width + x] === color) return;
81
+ const next = structuredClone(drawing);
82
+ next.pixels[y * next.width + x] = color;
83
+ // Record one undo snapshot per pointer stroke (not per painted pixel).
84
+ if (!strokeRef.current.recorded) {
85
+ history.recordSnapshot();
86
+ strokeRef.current.recorded = true;
87
+ }
88
+ onChange(formatJson(next));
89
+ }
90
+ function startPaint(event) {
91
+ strokeRef.current = { active: true, recorded: false };
92
+ event.currentTarget.setPointerCapture(event.pointerId);
93
+ paintAt(event);
94
+ }
95
+ function finishPaint() {
96
+ strokeRef.current = { active: false, recorded: false };
97
+ }
98
+ const canvasStyle = drawing
99
+ ? {
100
+ width: isWide ? 'min(70vh, 520px)' : 'auto',
101
+ height: isWide ? 'auto' : 'min(70vh, 520px)',
102
+ }
103
+ : undefined;
104
+ const sharedActionButtons = (
105
+ <>
106
+ <IconButton icon="undo" label="Undo" onClick={history.undo} disabled={!history.canUndo} />
107
+ <IconButton icon="redo" label="Redo" onClick={history.redo} disabled={!history.canRedo} />
108
+ </>
109
+ );
110
+ return (
111
+ <>
112
+ <EditorHeader
113
+ title={basename(path)}
114
+ subtitle={drawing ? `${drawing.width} x ${drawing.height} pixels` : error}
115
+ right={
116
+ <span className={styles.mobileOnly}>
117
+ {sharedActionButtons}
118
+ <IconButton
119
+ icon="palette"
120
+ label="Tools"
121
+ active={inspectorOpen}
122
+ onClick={() => setInspectorOpen((previous) => !previous)}
123
+ />
124
+ </span>
125
+ }
126
+ onToggleFiles={onToggleFiles}
127
+ filesOpen={filesOpen}
128
+ />
129
+ <EditorBody>
130
+ <div className={styles.drawingEditor}>
131
+ <div className={styles.drawingTools}>{sharedActionButtons}</div>
132
+ <div className={styles.drawingCanvasWrap}>
133
+ <canvas
134
+ ref={canvasRef}
135
+ className={styles.drawingCanvas}
136
+ style={canvasStyle}
137
+ onPointerDown={startPaint}
138
+ onPointerMove={(event) => {
139
+ if (strokeRef.current.active && event.buttons === 1) paintAt(event);
140
+ }}
141
+ onPointerUp={finishPaint}
142
+ onPointerCancel={finishPaint}
143
+ />
144
+ </div>
145
+ <DrawingInspector
146
+ sheet={inspectorSheet}
147
+ drawing={drawing}
148
+ color={color}
149
+ onSelectColor={setColor}
150
+ onResize={resizeDrawing}
151
+ />
152
+ </div>
153
+ </EditorBody>
154
+ </>
155
+ );
156
+ }
157
+ function useInspectorSheet(inspectorOpen) {
158
+ const [snap, setSnap] = useState('high');
159
+ useEffect(() => {
160
+ if (inspectorOpen) setSnap('high');
161
+ }, [inspectorOpen]);
162
+ const effectiveSnap = inspectorOpen ? snap : 'hidden';
163
+ return useMobileSheet({
164
+ snap: effectiveSnap,
165
+ baseClassName: styles.inspector,
166
+ onTransition: (direction) => {
167
+ if (!inspectorOpen) return;
168
+ if (direction === 'tap') setSnap((previous) => (previous === 'high' ? 'low' : 'high'));
169
+ else if (direction === 'down') setSnap('low');
170
+ else if (direction === 'up') setSnap('high');
171
+ },
172
+ });
173
+ }
174
+ function DrawingInspector({ sheet, drawing, color, onSelectColor, onResize }) {
175
+ return (
176
+ <aside {...sheet.rootProps} className={cx(sheet.rootProps.className, styles.inspectorAuto)}>
177
+ <div {...sheet.grabProps}>
178
+ <SheetGrabHandle
179
+ label="Inspector"
180
+ hint={drawing ? `${drawing.width}x${drawing.height} · color` : 'drawing tools'}
181
+ />
182
+ </div>
183
+ <div className={cx(styles.sheetBody, styles.inspectorBody)}>
184
+ <Panel title="Size">
185
+ <NumberField
186
+ label="Width"
187
+ value={drawing?.width ?? 1}
188
+ min={1}
189
+ max={128}
190
+ onChange={(width) => onResize(width, drawing?.height ?? 1)}
191
+ />
192
+ <NumberField
193
+ label="Height"
194
+ value={drawing?.height ?? 1}
195
+ min={1}
196
+ max={128}
197
+ onChange={(height) => onResize(drawing?.width ?? 1, height)}
198
+ />
199
+ </Panel>
200
+ <Panel title="Palette">
201
+ <div className={styles.palette}>
202
+ {palette.map((swatch) => (
203
+ <button
204
+ key={swatch}
205
+ className={cx(styles.swatch, color === swatch && styles.swatchSelected)}
206
+ title={swatch}
207
+ style={{
208
+ background: swatch.endsWith('00') ? theme.transparentChecker : swatch,
209
+ }}
210
+ onClick={() => onSelectColor(swatch)}
211
+ />
212
+ ))}
213
+ </div>
214
+ </Panel>
215
+ </div>
216
+ </aside>
217
+ );
218
+ }
219
+ function clampInteger(value, min, max) {
220
+ if (!Number.isFinite(value)) return min;
221
+ return Math.max(min, Math.min(max, Math.round(value)));
222
+ }
@@ -0,0 +1,143 @@
1
+ import React, { useState } from 'react';
2
+ import { basename } from '../engine/files';
3
+ import { cx, Icon, SheetGrabHandle, styles, useMobileSheet } from '../engine/ui';
4
+ export function FileBrowser({
5
+ files,
6
+ selectedPath,
7
+ onSelect,
8
+ sheetOpen = false,
9
+ onSheetOpenChange,
10
+ }) {
11
+ const tree = buildFileTree(Object.keys(files));
12
+ const [expanded, setExpanded] = useState(() => new Set(collectDirectoryPaths(tree)));
13
+ const sheet = useMobileSheet({
14
+ snap: sheetOpen ? 'high' : 'hidden',
15
+ baseClassName: styles.fileBrowser,
16
+ onTransition: (direction) => {
17
+ if (direction !== 'up') onSheetOpenChange?.(false);
18
+ },
19
+ });
20
+ function toggle(path) {
21
+ setExpanded((current) => {
22
+ const next = new Set(current);
23
+ if (next.has(path)) next.delete(path);
24
+ else next.add(path);
25
+ return next;
26
+ });
27
+ }
28
+ return (
29
+ <aside {...sheet.rootProps}>
30
+ <div {...sheet.grabProps}>
31
+ <SheetGrabHandle label="Files" hint={basename(selectedPath)} />
32
+ </div>
33
+ <div className={styles.fileBrowserHeader}>
34
+ <div>
35
+ <div className={styles.fileBrowserTitle}>Deck Files</div>
36
+ </div>
37
+ </div>
38
+ <div className={styles.fileTree}>
39
+ {tree.children.map((node) => (
40
+ <FileNode
41
+ key={node.path}
42
+ node={node}
43
+ depth={0}
44
+ expanded={expanded}
45
+ selectedPath={selectedPath}
46
+ onSelect={onSelect}
47
+ onToggle={toggle}
48
+ />
49
+ ))}
50
+ </div>
51
+ </aside>
52
+ );
53
+ }
54
+ function FileNode({ node, depth, expanded, selectedPath, onSelect, onToggle }) {
55
+ const depthStyle = { '--file-depth': depth };
56
+ if (node.type === 'directory') {
57
+ const isExpanded = expanded.has(node.path);
58
+ return (
59
+ <div className={styles.fileBranch}>
60
+ <button
61
+ className={styles.fileDirRow}
62
+ style={depthStyle}
63
+ onClick={() => onToggle(node.path)}>
64
+ <span className={styles.fileDisclosure}>
65
+ <Icon name={isExpanded ? 'chevron-down' : 'chevron-right'} />
66
+ </span>
67
+ <span>{node.name}</span>
68
+ </button>
69
+ {isExpanded ? (
70
+ <div>
71
+ {node.children.map((child) => (
72
+ <FileNode
73
+ key={child.path}
74
+ node={child}
75
+ depth={depth + 1}
76
+ expanded={expanded}
77
+ selectedPath={selectedPath}
78
+ onSelect={onSelect}
79
+ onToggle={onToggle}
80
+ />
81
+ ))}
82
+ </div>
83
+ ) : null}
84
+ </div>
85
+ );
86
+ }
87
+ // File rows nest under their directory header: extra left padding so
88
+ // file text sits to the right of the parent directory header.
89
+ const fileRowStyle = {
90
+ ...depthStyle,
91
+ paddingLeft: `calc(9px + ${depth} * 16px + 7px)`,
92
+ };
93
+ return (
94
+ <button
95
+ className={cx(styles.fileRow, selectedPath === node.path && styles.fileRowSelected)}
96
+ style={fileRowStyle}
97
+ onClick={() => onSelect(node.path)}>
98
+ <span>{basename(node.path)}</span>
99
+ </button>
100
+ );
101
+ }
102
+ function buildFileTree(paths) {
103
+ const root = {
104
+ type: 'directory',
105
+ name: '',
106
+ path: '',
107
+ children: [],
108
+ childMap: new Map(),
109
+ };
110
+ for (const path of paths) {
111
+ const parts = path.split('/');
112
+ let parent = root;
113
+ for (let index = 0; index < parts.length; index++) {
114
+ const name = parts[index];
115
+ const nodePath = parts.slice(0, index + 1).join('/');
116
+ const isFile = index === parts.length - 1;
117
+ if (!parent.childMap?.has(name)) {
118
+ const node = isFile
119
+ ? { type: 'file', name, path: nodePath }
120
+ : { type: 'directory', name, path: nodePath, children: [], childMap: new Map() };
121
+ parent.childMap?.set(name, node);
122
+ parent.children.push(node);
123
+ }
124
+ const child = parent.childMap?.get(name);
125
+ if (!child || child.type !== 'directory') break;
126
+ parent = child;
127
+ }
128
+ }
129
+ pruneChildMaps(root);
130
+ return root;
131
+ }
132
+ function pruneChildMaps(node) {
133
+ if (node.type !== 'directory') return;
134
+ for (const child of node.children) pruneChildMaps(child);
135
+ delete node.childMap;
136
+ }
137
+ function collectDirectoryPaths(node) {
138
+ if (node.type !== 'directory') return [];
139
+ return [
140
+ ...(node.path ? [node.path] : []),
141
+ ...node.children.flatMap((child) => collectDirectoryPaths(child)),
142
+ ];
143
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { initialFiles, parseJsonFile } from '../engine/files';
3
+ import { ScenePlayer } from '../engine/ScenePlayer';
4
+ import { behaviorClasses } from './behaviorRegistry';
5
+ // Play-mode entry point. Intentionally thin: it locates the start scene and
6
+ // hands it to the engine's `ScenePlayer`, which owns the runtime and input.
7
+ // Deck/game logic belongs in `scenes/` and `behaviors/`, not here.
8
+ export function PlayOnly() {
9
+ const sceneText = initialFiles['scenes/main.scene'] ?? '';
10
+ const { value: sceneData } = parseJsonFile('scenes/main.scene', sceneText);
11
+ if (!sceneData) return null;
12
+ const drawings = {};
13
+ for (const [path, text] of Object.entries(initialFiles)) {
14
+ if (!path.endsWith('.drawing')) continue;
15
+ const parsed = parseJsonFile(path, text);
16
+ if (parsed.value) drawings[path] = parsed.value;
17
+ }
18
+ return (
19
+ <ScenePlayer sceneData={sceneData} drawings={drawings} behaviorClasses={behaviorClasses} />
20
+ );
21
+ }