castle-web-cli 0.4.72 → 0.4.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-prompts.js +22 -3
- package/dist/agent.js +731 -313
- package/dist/init.js +1 -1
- package/dist/shell/assets/index-Dfn29Bkt.js +108 -0
- package/dist/shell/assets/{index-CVEnWuGV.css → index-WNbOHPBj.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/dist/vitePlugins.js +3 -2
- package/kits/basic-2d/CLAUDE.md +29 -8
- package/kits/basic-2d/behaviors/Collider.jsx +6 -4
- package/kits/basic-2d/behaviors/Layout.jsx +2 -2
- package/kits/basic-2d/behaviors/Sprite.jsx +210 -0
- package/kits/basic-2d/behaviors/tint.js +47 -0
- package/kits/basic-2d/docs/pxart-format.md +298 -0
- package/kits/basic-2d/drawings/pig.pxart +59 -0
- package/kits/basic-2d/editors/App.jsx +125 -76
- package/kits/basic-2d/editors/CodeEditor.jsx +9 -45
- package/kits/basic-2d/editors/FileBrowser.jsx +234 -47
- package/kits/basic-2d/editors/PlayOnly.jsx +9 -7
- package/kits/basic-2d/editors/PxArtEditor.jsx +662 -0
- package/kits/basic-2d/editors/SceneEditor.jsx +703 -222
- package/kits/basic-2d/editors/SelectionOverlay.jsx +818 -0
- package/kits/basic-2d/editors/SingleEditor.jsx +38 -20
- package/kits/basic-2d/editors/codeTheme.js +135 -0
- package/kits/basic-2d/editors/editorHistory.js +44 -17
- package/kits/basic-2d/editors/inspectorSheet.js +23 -0
- package/kits/basic-2d/editors/pixelCanvas.js +11 -0
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +55 -0
- package/kits/basic-2d/editors/pixelGeometry.js +45 -0
- package/kits/basic-2d/editors/pixelInspector.jsx +416 -0
- package/kits/basic-2d/editors/pxArtEditorModel.js +718 -0
- package/kits/basic-2d/editors/pxArtPlayback.js +92 -0
- package/kits/basic-2d/editors/pxArtTimeline.jsx +752 -0
- package/kits/basic-2d/editors/pxArtTimeline.module.css +506 -0
- package/kits/basic-2d/editors/pxArtTools.js +124 -0
- package/kits/basic-2d/editors/useArtboardFit.js +102 -0
- package/kits/basic-2d/engine/ScenePlayer.jsx +169 -13
- package/kits/basic-2d/engine/SceneUI.jsx +3 -11
- package/kits/basic-2d/engine/assets.js +15 -0
- package/kits/basic-2d/engine/files.js +57 -2
- package/kits/basic-2d/engine/playConsole.js +66 -0
- package/kits/basic-2d/engine/pxart.js +985 -0
- package/kits/basic-2d/engine/scene.js +222 -41
- package/kits/basic-2d/engine/ui.jsx +155 -26
- package/kits/basic-2d/engine/ui.module.css +1385 -332
- package/kits/basic-2d/eslint.config.js +21 -0
- package/kits/basic-2d/index.html +13 -0
- package/kits/basic-2d/package.json +1 -0
- package/kits/basic-2d/pnpm-lock.yaml +5 -5
- package/kits/basic-2d/scenes/main.scene +19 -26
- package/kits/basic-2d/scripts/draw.mjs +121 -0
- package/kits/basic-3d/editors/PlayOnly.jsx +9 -1
- package/kits/basic-3d/engine/ScenePlayer.jsx +7 -1
- package/package.json +1 -1
- package/dist/shell/assets/index-vmdKwUE1.js +0 -106
- package/kits/basic-2d/behaviors/Drawing.jsx +0 -142
- package/kits/basic-2d/drawings/block.drawing +0 -70
- package/kits/basic-2d/drawings/default.drawing +0 -70
- package/kits/basic-2d/editors/DrawingEditor.jsx +0 -224
|
@@ -1,52 +1,13 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { javascript } from '@codemirror/lang-javascript';
|
|
3
|
-
import {
|
|
3
|
+
import { indentUnit, syntaxHighlighting } from '@codemirror/language';
|
|
4
4
|
import { EditorState } from '@codemirror/state';
|
|
5
5
|
import { EditorView } from '@codemirror/view';
|
|
6
|
-
import { tags } from '@lezer/highlight';
|
|
7
6
|
import { basicSetup } from 'codemirror';
|
|
8
7
|
import { basename } from '../engine/files';
|
|
9
8
|
import { EditorBody, EditorHeader, styles } from '../engine/ui';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{ tag: tags.namespace, color: '#BB7547' },
|
|
13
|
-
{ tag: tags.keyword, color: '#BC4A9B' },
|
|
14
|
-
{ tag: [tags.literal, tags.inserted], color: '#5DAF8D' },
|
|
15
|
-
{ tag: [tags.string, tags.deleted], color: '#E86A73' },
|
|
16
|
-
{ tag: tags.comment, color: '#8B93AF', fontStyle: 'italic' },
|
|
17
|
-
]);
|
|
18
|
-
const castleCodeTheme = EditorView.theme({
|
|
19
|
-
'&': {
|
|
20
|
-
width: '100%',
|
|
21
|
-
height: '100%',
|
|
22
|
-
fontSize: '9pt',
|
|
23
|
-
backgroundColor: '#fff',
|
|
24
|
-
},
|
|
25
|
-
'&.cm-editor.cm-focused': {
|
|
26
|
-
outline: 'none',
|
|
27
|
-
},
|
|
28
|
-
'.cm-scroller': {
|
|
29
|
-
overflow: 'auto',
|
|
30
|
-
fontFamily: 'Menlo, Monaco, Lucida Console, monospace',
|
|
31
|
-
},
|
|
32
|
-
'.cm-content': {
|
|
33
|
-
minHeight: '100%',
|
|
34
|
-
color: '#322b28',
|
|
35
|
-
paddingBottom: '400px',
|
|
36
|
-
paddingRight: '16px',
|
|
37
|
-
},
|
|
38
|
-
'.cm-gutters': {
|
|
39
|
-
display: 'none',
|
|
40
|
-
},
|
|
41
|
-
'.cm-activeLine': {
|
|
42
|
-
backgroundColor: '#eeeeeea0',
|
|
43
|
-
},
|
|
44
|
-
'.cm-activeLineGutter': {
|
|
45
|
-
color: '#000',
|
|
46
|
-
backgroundColor: '#ddd',
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
export function CodeEditor({ path, text, onChange, onToggleFiles, filesOpen, bare = false }) {
|
|
9
|
+
import { castleCodeTheme, castleHighlightStyle } from './codeTheme';
|
|
10
|
+
export function CodeEditor({ path, text, onChange, onToggleFiles, filesOpen, headerHost }) {
|
|
50
11
|
const editorRef = useRef(null);
|
|
51
12
|
const viewRef = useRef(null);
|
|
52
13
|
const onChangeRef = useRef(onChange);
|
|
@@ -102,9 +63,12 @@ export function CodeEditor({ path, text, onChange, onToggleFiles, filesOpen, bar
|
|
|
102
63
|
}, [text]);
|
|
103
64
|
return (
|
|
104
65
|
<>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
66
|
+
<EditorHeader
|
|
67
|
+
title={basename(path)}
|
|
68
|
+
onToggleFiles={onToggleFiles}
|
|
69
|
+
filesOpen={filesOpen}
|
|
70
|
+
headerHost={headerHost}
|
|
71
|
+
/>
|
|
108
72
|
<EditorBody>
|
|
109
73
|
<div className={styles.codeEditor}>
|
|
110
74
|
<div ref={editorRef} className={styles.codeMirrorHost} />
|
|
@@ -1,11 +1,40 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { basename } from '../engine/files';
|
|
3
|
-
import { cx, Icon, SheetGrabHandle, styles, useMobileSheet } from '../engine/ui';
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
3
|
+
import { ContextMenu, cx, Icon, SheetGrabHandle, styles, useMobileSheet } from '../engine/ui';
|
|
4
|
+
// The file tree plus its right-click menus and inline rename. By default it
|
|
5
|
+
// renders inside an `aside` with mobile-sheet chrome (the combined editor's
|
|
6
|
+
// sidebar). Pass `bare` to drop that chrome and render just the tree + menus so
|
|
7
|
+
// it can be mounted on its own in the dockview "Files" panel.
|
|
8
|
+
//
|
|
9
|
+
// The file-op props (onAddFile / onDuplicate / onRename / onDelete) are
|
|
10
|
+
// OPTIONAL. When none are provided the browser is read-only: no context menus,
|
|
11
|
+
// no inline rename, no add affordances -- just a browsable, clickable tree.
|
|
12
|
+
export function FileBrowser({
|
|
13
|
+
files,
|
|
14
|
+
selectedPath,
|
|
15
|
+
onSelect,
|
|
16
|
+
onAddFile,
|
|
17
|
+
onDuplicate,
|
|
18
|
+
onRename,
|
|
19
|
+
onDelete,
|
|
20
|
+
sheetOpen = false,
|
|
21
|
+
onSheetOpenChange,
|
|
22
|
+
bare = false,
|
|
23
|
+
}) {
|
|
24
|
+
const canMutate = Boolean(onAddFile || onDuplicate || onRename || onDelete);
|
|
7
25
|
const tree = buildFileTree(Object.keys(files));
|
|
8
26
|
const [expanded, setExpanded] = useState(() => new Set(collectDirectoryPaths(tree)));
|
|
27
|
+
// `menu` is { x, y, node } for a row menu, or { x, y, node: null } for the
|
|
28
|
+
// panel-background menu. `renaming` holds the path whose row is in edit mode.
|
|
29
|
+
const [menu, setMenu] = useState(null);
|
|
30
|
+
const [renaming, setRenaming] = useState(null);
|
|
31
|
+
const sheet = useMobileSheet({
|
|
32
|
+
snap: sheetOpen ? 'high' : 'hidden',
|
|
33
|
+
baseClassName: styles.fileBrowser,
|
|
34
|
+
onTransition: (direction) => {
|
|
35
|
+
if (direction !== 'up') onSheetOpenChange?.(false);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
9
38
|
function toggle(path) {
|
|
10
39
|
setExpanded((current) => {
|
|
11
40
|
const next = new Set(current);
|
|
@@ -14,7 +43,49 @@ export function FileTree({ files, selectedPath, onSelect }) {
|
|
|
14
43
|
return next;
|
|
15
44
|
});
|
|
16
45
|
}
|
|
17
|
-
|
|
46
|
+
function expandDir(path) {
|
|
47
|
+
if (!path) return;
|
|
48
|
+
setExpanded((current) => new Set(current).add(path));
|
|
49
|
+
}
|
|
50
|
+
function openMenu(event, node) {
|
|
51
|
+
if (!canMutate) return;
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
event.stopPropagation();
|
|
54
|
+
setMenu({ x: event.clientX, y: event.clientY, node });
|
|
55
|
+
}
|
|
56
|
+
function handleAddFile(dirPath) {
|
|
57
|
+
onAddFile?.(dirPath);
|
|
58
|
+
expandDir(dirPath);
|
|
59
|
+
}
|
|
60
|
+
function handleDuplicate(node) {
|
|
61
|
+
const newPath = onDuplicate?.(node);
|
|
62
|
+
if (node.type === 'directory' && newPath) expandDir(newPath);
|
|
63
|
+
}
|
|
64
|
+
function submitRename(node, rawName) {
|
|
65
|
+
setRenaming(null);
|
|
66
|
+
const newName = rawName.trim();
|
|
67
|
+
if (!newName || newName === node.name) return;
|
|
68
|
+
const newPath = onRename?.(node, newName);
|
|
69
|
+
if (node.type === 'directory' && newPath && newPath !== node.path) {
|
|
70
|
+
setExpanded((current) => repathExpanded(current, node.path, newPath));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function menuItems(node) {
|
|
74
|
+
const fileOnly = [
|
|
75
|
+
{ key: 'duplicate', label: 'Duplicate', onClick: () => handleDuplicate(node) },
|
|
76
|
+
{ key: 'rename', label: 'Rename', onClick: () => setRenaming(node.path) },
|
|
77
|
+
{ key: 'delete', label: 'Delete', onClick: () => onDelete?.(node) },
|
|
78
|
+
];
|
|
79
|
+
if (!node) return [{ key: 'add', label: 'Add file', onClick: () => handleAddFile('') }];
|
|
80
|
+
if (node.type === 'directory') {
|
|
81
|
+
return [
|
|
82
|
+
{ key: 'add', label: 'Add file', onClick: () => handleAddFile(node.path) },
|
|
83
|
+
...fileOnly,
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
return fileOnly;
|
|
87
|
+
}
|
|
88
|
+
const treeNodes = (
|
|
18
89
|
<div className={styles.fileTree}>
|
|
19
90
|
{tree.children.map((node) => (
|
|
20
91
|
<FileNode
|
|
@@ -23,56 +94,89 @@ export function FileTree({ files, selectedPath, onSelect }) {
|
|
|
23
94
|
depth={0}
|
|
24
95
|
expanded={expanded}
|
|
25
96
|
selectedPath={selectedPath}
|
|
97
|
+
renaming={renaming}
|
|
26
98
|
onSelect={onSelect}
|
|
27
99
|
onToggle={toggle}
|
|
100
|
+
onContextMenu={openMenu}
|
|
101
|
+
onRenameSubmit={submitRename}
|
|
102
|
+
onRenameCancel={() => setRenaming(null)}
|
|
28
103
|
/>
|
|
29
104
|
))}
|
|
30
105
|
</div>
|
|
31
106
|
);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
});
|
|
107
|
+
const menuEl = menu ? (
|
|
108
|
+
<ContextMenu x={menu.x} y={menu.y} items={menuItems(menu.node)} onClose={() => setMenu(null)} />
|
|
109
|
+
) : null;
|
|
110
|
+
// Panel mount: no sheet/grab-handle chrome, just a full-height tree whose
|
|
111
|
+
// empty area below the rows opens the blank-area ("Add file") menu.
|
|
112
|
+
if (bare) {
|
|
113
|
+
return (
|
|
114
|
+
<div className={styles.fileBrowserBare} onContextMenu={(event) => openMenu(event, null)}>
|
|
115
|
+
{treeNodes}
|
|
116
|
+
{menuEl}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
47
120
|
return (
|
|
48
|
-
<aside {...sheet.rootProps}>
|
|
121
|
+
<aside {...sheet.rootProps} onContextMenu={(event) => openMenu(event, null)}>
|
|
49
122
|
<div {...sheet.grabProps}>
|
|
50
123
|
<SheetGrabHandle label="Files" hint={basename(selectedPath)} />
|
|
51
124
|
</div>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<div className={styles.fileBrowserTitle}>Deck Files</div>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
<FileTree files={files} selectedPath={selectedPath} onSelect={onSelect} />
|
|
125
|
+
{treeNodes}
|
|
126
|
+
{menuEl}
|
|
58
127
|
</aside>
|
|
59
128
|
);
|
|
60
129
|
}
|
|
61
|
-
function FileNode({
|
|
130
|
+
function FileNode({
|
|
131
|
+
node,
|
|
132
|
+
depth,
|
|
133
|
+
expanded,
|
|
134
|
+
selectedPath,
|
|
135
|
+
renaming,
|
|
136
|
+
onSelect,
|
|
137
|
+
onToggle,
|
|
138
|
+
onContextMenu,
|
|
139
|
+
onRenameSubmit,
|
|
140
|
+
onRenameCancel,
|
|
141
|
+
}) {
|
|
62
142
|
const depthStyle = { '--file-depth': depth };
|
|
143
|
+
const isRenaming = renaming === node.path;
|
|
63
144
|
if (node.type === 'directory') {
|
|
64
145
|
const isExpanded = expanded.has(node.path);
|
|
146
|
+
const label = isRenaming ? (
|
|
147
|
+
<span className={styles.fileLabel}>
|
|
148
|
+
<span className={cx(styles.fileIcon, styles.fileDisclosure)}>
|
|
149
|
+
<Icon name={isExpanded ? 'chevron-down' : 'chevron-right'} />
|
|
150
|
+
</span>
|
|
151
|
+
<RenameInput
|
|
152
|
+
initialName={node.name}
|
|
153
|
+
onSubmit={(value) => onRenameSubmit(node, value)}
|
|
154
|
+
onCancel={onRenameCancel}
|
|
155
|
+
/>
|
|
156
|
+
</span>
|
|
157
|
+
) : (
|
|
158
|
+
<span className={styles.fileLabel}>
|
|
159
|
+
<span className={cx(styles.fileIcon, styles.fileDisclosure)}>
|
|
160
|
+
<Icon name={isExpanded ? 'chevron-down' : 'chevron-right'} />
|
|
161
|
+
</span>
|
|
162
|
+
<span>{node.name}</span>
|
|
163
|
+
</span>
|
|
164
|
+
);
|
|
65
165
|
return (
|
|
66
166
|
<div className={styles.fileBranch}>
|
|
67
|
-
|
|
68
|
-
className={styles.fileDirRow}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
167
|
+
{isRenaming ? (
|
|
168
|
+
<div className={styles.fileDirRow} style={depthStyle}>
|
|
169
|
+
{label}
|
|
170
|
+
</div>
|
|
171
|
+
) : (
|
|
172
|
+
<button
|
|
173
|
+
className={styles.fileDirRow}
|
|
174
|
+
style={depthStyle}
|
|
175
|
+
onClick={() => onToggle(node.path)}
|
|
176
|
+
onContextMenu={(event) => onContextMenu(event, node)}>
|
|
177
|
+
{label}
|
|
178
|
+
</button>
|
|
179
|
+
)}
|
|
76
180
|
{isExpanded ? (
|
|
77
181
|
<div>
|
|
78
182
|
{node.children.map((child) => (
|
|
@@ -82,8 +186,12 @@ function FileNode({ node, depth, expanded, selectedPath, onSelect, onToggle }) {
|
|
|
82
186
|
depth={depth + 1}
|
|
83
187
|
expanded={expanded}
|
|
84
188
|
selectedPath={selectedPath}
|
|
189
|
+
renaming={renaming}
|
|
85
190
|
onSelect={onSelect}
|
|
86
191
|
onToggle={onToggle}
|
|
192
|
+
onContextMenu={onContextMenu}
|
|
193
|
+
onRenameSubmit={onRenameSubmit}
|
|
194
|
+
onRenameCancel={onRenameCancel}
|
|
87
195
|
/>
|
|
88
196
|
))}
|
|
89
197
|
</div>
|
|
@@ -91,21 +199,100 @@ function FileNode({ node, depth, expanded, selectedPath, onSelect, onToggle }) {
|
|
|
91
199
|
</div>
|
|
92
200
|
);
|
|
93
201
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
202
|
+
if (isRenaming) {
|
|
203
|
+
return (
|
|
204
|
+
<div className={styles.fileRow} style={depthStyle}>
|
|
205
|
+
<span className={styles.fileLabel}>
|
|
206
|
+
<span className={styles.fileIcon}>
|
|
207
|
+
<Icon name={fileIconName(node.path)} />
|
|
208
|
+
</span>
|
|
209
|
+
<RenameInput
|
|
210
|
+
initialName={basename(node.path)}
|
|
211
|
+
onSubmit={(value) => onRenameSubmit(node, value)}
|
|
212
|
+
onCancel={onRenameCancel}
|
|
213
|
+
/>
|
|
214
|
+
</span>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
100
218
|
return (
|
|
101
219
|
<button
|
|
102
220
|
className={cx(styles.fileRow, selectedPath === node.path && styles.fileRowSelected)}
|
|
103
|
-
style={
|
|
104
|
-
onClick={() => onSelect(node.path)}
|
|
105
|
-
|
|
221
|
+
style={depthStyle}
|
|
222
|
+
onClick={() => onSelect(node.path)}
|
|
223
|
+
onContextMenu={(event) => onContextMenu(event, node)}>
|
|
224
|
+
<span className={styles.fileLabel}>
|
|
225
|
+
<span className={styles.fileIcon}>
|
|
226
|
+
<Icon name={fileIconName(node.path)} />
|
|
227
|
+
</span>
|
|
228
|
+
<span>{basename(node.path)}</span>
|
|
229
|
+
</span>
|
|
106
230
|
</button>
|
|
107
231
|
);
|
|
108
232
|
}
|
|
233
|
+
// Inline rename field: focuses + selects on mount, commits on Enter or blur,
|
|
234
|
+
// abandons on Escape. A done-guard keeps the blur that follows an
|
|
235
|
+
// Enter/Escape from firing a second submit/cancel.
|
|
236
|
+
function RenameInput({ initialName, onSubmit, onCancel }) {
|
|
237
|
+
const [value, setValue] = useState(initialName);
|
|
238
|
+
const inputRef = useRef(null);
|
|
239
|
+
const doneRef = useRef(false);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
const el = inputRef.current;
|
|
242
|
+
if (!el) return;
|
|
243
|
+
el.focus();
|
|
244
|
+
el.select();
|
|
245
|
+
}, []);
|
|
246
|
+
function commit() {
|
|
247
|
+
if (doneRef.current) return;
|
|
248
|
+
doneRef.current = true;
|
|
249
|
+
onSubmit(value);
|
|
250
|
+
}
|
|
251
|
+
function abandon() {
|
|
252
|
+
if (doneRef.current) return;
|
|
253
|
+
doneRef.current = true;
|
|
254
|
+
onCancel();
|
|
255
|
+
}
|
|
256
|
+
return (
|
|
257
|
+
<input
|
|
258
|
+
ref={inputRef}
|
|
259
|
+
className={styles.fileRenameInput}
|
|
260
|
+
value={value}
|
|
261
|
+
onClick={(event) => event.stopPropagation()}
|
|
262
|
+
onChange={(event) => setValue(event.target.value)}
|
|
263
|
+
onBlur={commit}
|
|
264
|
+
onKeyDown={(event) => {
|
|
265
|
+
if (event.key === 'Enter') {
|
|
266
|
+
event.preventDefault();
|
|
267
|
+
commit();
|
|
268
|
+
} else if (event.key === 'Escape') {
|
|
269
|
+
event.preventDefault();
|
|
270
|
+
abandon();
|
|
271
|
+
}
|
|
272
|
+
}}
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
// Re-path a set of expanded directory paths after a folder rename so the
|
|
277
|
+
// renamed folder (and any expanded descendants) stay open in place.
|
|
278
|
+
function repathExpanded(set, oldDir, newDir) {
|
|
279
|
+
const next = new Set();
|
|
280
|
+
for (const path of set) {
|
|
281
|
+
if (path === oldDir) next.add(newDir);
|
|
282
|
+
else if (path.startsWith(`${oldDir}/`)) next.add(`${newDir}${path.slice(oldDir.length)}`);
|
|
283
|
+
else next.add(path);
|
|
284
|
+
}
|
|
285
|
+
return next;
|
|
286
|
+
}
|
|
287
|
+
// Per-file-type icon, keyed by extension. Sprites (.pxart) -> layer-group,
|
|
288
|
+
// scenes -> globe, behaviors (.jsx) -> code, anything else -> a generic file
|
|
289
|
+
// icon.
|
|
290
|
+
function fileIconName(path) {
|
|
291
|
+
if (path.endsWith('.pxart')) return 'layer-group';
|
|
292
|
+
if (path.endsWith('.scene')) return 'globe';
|
|
293
|
+
if (path.endsWith('.jsx')) return 'code';
|
|
294
|
+
return 'file';
|
|
295
|
+
}
|
|
109
296
|
function buildFileTree(paths) {
|
|
110
297
|
const root = {
|
|
111
298
|
type: 'directory',
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { Lifecycle } from 'castle-web-sdk';
|
|
2
3
|
import { initialFiles, parseJsonFile } from '../engine/files';
|
|
4
|
+
import { collectAssets } from '../engine/assets';
|
|
3
5
|
import { ScenePlayer } from '../engine/ScenePlayer';
|
|
4
6
|
import { behaviorClasses } from './behaviorRegistry';
|
|
5
7
|
// Play-mode entry point. Intentionally thin: it locates the start scene and
|
|
@@ -9,13 +11,13 @@ export function PlayOnly() {
|
|
|
9
11
|
const sceneText = initialFiles['scenes/main.scene'] ?? '';
|
|
10
12
|
const { value: sceneData } = parseJsonFile('scenes/main.scene', sceneText);
|
|
11
13
|
if (!sceneData) return null;
|
|
12
|
-
const
|
|
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
|
-
}
|
|
14
|
+
const { sprites } = collectAssets(initialFiles);
|
|
18
15
|
return (
|
|
19
|
-
<ScenePlayer
|
|
16
|
+
<ScenePlayer
|
|
17
|
+
sceneData={sceneData}
|
|
18
|
+
sprites={sprites}
|
|
19
|
+
behaviorClasses={behaviorClasses}
|
|
20
|
+
onFirstFrame={Lifecycle.ready}
|
|
21
|
+
/>
|
|
20
22
|
);
|
|
21
23
|
}
|