castle-web-cli 0.4.3 → 0.4.5
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/index.js +8 -2
- package/dist/init.js +1 -1
- package/kits/basic-2d/CLAUDE.md +3 -3
- package/package.json +1 -1
- package/kits/basic-2d-frozen/.prettierrc +0 -8
- package/kits/basic-2d-frozen/CLAUDE.md +0 -131
- package/kits/basic-2d-frozen/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d-frozen/behaviors/Collider.jsx +0 -71
- package/kits/basic-2d-frozen/behaviors/Drawing.jsx +0 -139
- package/kits/basic-2d-frozen/behaviors/Layout.jsx +0 -16
- package/kits/basic-2d-frozen/drawings/floor.drawing +0 -70
- package/kits/basic-2d-frozen/editors/App.jsx +0 -152
- package/kits/basic-2d-frozen/editors/CodeEditor.jsx +0 -112
- package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +0 -222
- package/kits/basic-2d-frozen/editors/FileBrowser.jsx +0 -143
- package/kits/basic-2d-frozen/editors/PlayOnly.jsx +0 -21
- package/kits/basic-2d-frozen/editors/SceneEditor.jsx +0 -1012
- package/kits/basic-2d-frozen/editors/behaviorRegistry.js +0 -24
- package/kits/basic-2d-frozen/editors/editorHistory.js +0 -52
- package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +0 -83
- package/kits/basic-2d-frozen/engine/SceneUI.jsx +0 -67
- package/kits/basic-2d-frozen/engine/TouchControls.jsx +0 -136
- package/kits/basic-2d-frozen/engine/autoInspector.jsx +0 -51
- package/kits/basic-2d-frozen/engine/files.js +0 -62
- package/kits/basic-2d-frozen/engine/scene.js +0 -420
- package/kits/basic-2d-frozen/engine/ui.jsx +0 -344
- package/kits/basic-2d-frozen/engine/ui.module.css +0 -928
- package/kits/basic-2d-frozen/eslint.config.js +0 -50
- package/kits/basic-2d-frozen/index.html +0 -11
- package/kits/basic-2d-frozen/main.jsx +0 -10
- package/kits/basic-2d-frozen/package-lock.json +0 -2706
- package/kits/basic-2d-frozen/package.json +0 -41
- package/kits/basic-2d-frozen/scenes/main.scene +0 -108
- package/kits/basic-2d-frozen/vite.config.js +0 -1
- package/kits/rpg-2d/.prettierrc +0 -8
- package/kits/rpg-2d/behaviors/Camera.tsx +0 -52
- package/kits/rpg-2d/behaviors/Collider.tsx +0 -98
- package/kits/rpg-2d/behaviors/Dialog.tsx +0 -184
- package/kits/rpg-2d/behaviors/Drawing.tsx +0 -161
- package/kits/rpg-2d/behaviors/Friend.tsx +0 -45
- package/kits/rpg-2d/behaviors/Layout.tsx +0 -29
- package/kits/rpg-2d/behaviors/PlayerController.tsx +0 -255
- package/kits/rpg-2d/behaviors/Portal.tsx +0 -60
- package/kits/rpg-2d/behaviors/QuestLog.tsx +0 -90
- package/kits/rpg-2d/behaviors/SaveMenu.tsx +0 -123
- package/kits/rpg-2d/behaviors/Tilemap.tsx +0 -90
- package/kits/rpg-2d/drawings/bld-home.drawing +0 -8136
- package/kits/rpg-2d/drawings/env-crate.drawing +0 -509
- package/kits/rpg-2d/drawings/env-fence.drawing +0 -536
- package/kits/rpg-2d/drawings/env-flower-bed.drawing +0 -607
- package/kits/rpg-2d/drawings/env-fountain.drawing +0 -2622
- package/kits/rpg-2d/drawings/env-hedge.drawing +0 -601
- package/kits/rpg-2d/drawings/env-house-blue.drawing +0 -1
- package/kits/rpg-2d/drawings/env-house-green.drawing +0 -1
- package/kits/rpg-2d/drawings/env-tree-oak.drawing +0 -1540
- package/kits/rpg-2d/drawings/env-tree-pine.drawing +0 -1315
- package/kits/rpg-2d/drawings/floor.drawing +0 -70
- package/kits/rpg-2d/drawings/fx-sparkle.drawing +0 -926
- package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +0 -1099
- package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +0 -4177
- package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +0 -1099
- package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +0 -4177
- package/kits/rpg-2d/drawings/player-idle-down.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-left.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-right.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-up.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-walk-down.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-left.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-right.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-up.drawing +0 -4148
- package/kits/rpg-2d/editors/App.tsx +0 -163
- package/kits/rpg-2d/editors/CodeEditor.tsx +0 -120
- package/kits/rpg-2d/editors/DrawingEditor.tsx +0 -278
- package/kits/rpg-2d/editors/FileBrowser.tsx +0 -191
- package/kits/rpg-2d/editors/PlayOnly.tsx +0 -26
- package/kits/rpg-2d/editors/SceneEditor.tsx +0 -1093
- package/kits/rpg-2d/editors/behaviorRegistry.ts +0 -33
- package/kits/rpg-2d/editors/editorHistory.ts +0 -75
- package/kits/rpg-2d/editors/editorProps.ts +0 -10
- package/kits/rpg-2d/engine/ScenePlayer.tsx +0 -130
- package/kits/rpg-2d/engine/SceneUI.tsx +0 -74
- package/kits/rpg-2d/engine/TouchControls.tsx +0 -157
- package/kits/rpg-2d/engine/autoInspector.tsx +0 -111
- package/kits/rpg-2d/engine/drawing.ts +0 -81
- package/kits/rpg-2d/engine/files.ts +0 -215
- package/kits/rpg-2d/engine/scene.ts +0 -484
- package/kits/rpg-2d/engine/ui.module.css +0 -928
- package/kits/rpg-2d/engine/ui.tsx +0 -483
- package/kits/rpg-2d/eslint.config.js +0 -46
- package/kits/rpg-2d/index.html +0 -11
- package/kits/rpg-2d/main.tsx +0 -14
- package/kits/rpg-2d/package-lock.json +0 -3149
- package/kits/rpg-2d/package.json +0 -46
- package/kits/rpg-2d/scenes/main.scene +0 -203
- package/kits/rpg-2d/tsconfig.json +0 -17
- package/kits/rpg-2d/vite-env.d.ts +0 -7
- package/kits/rpg-2d/vite.config.js +0 -1
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { writeFile } from 'castle-web-sdk';
|
|
3
|
-
import {
|
|
4
|
-
flatFileOrder,
|
|
5
|
-
formatJson,
|
|
6
|
-
getFileKind,
|
|
7
|
-
initialFiles,
|
|
8
|
-
parseJsonFile,
|
|
9
|
-
} from '../engine/files';
|
|
10
|
-
import { AppShell, cx, MainEditor, styles } from '../engine/ui';
|
|
11
|
-
import { CodeEditor } from './CodeEditor';
|
|
12
|
-
import { DrawingEditor } from './DrawingEditor';
|
|
13
|
-
import { FileBrowser } from './FileBrowser';
|
|
14
|
-
import { SceneEditor } from './SceneEditor';
|
|
15
|
-
export function App() {
|
|
16
|
-
const [files, setFiles] = useState(initialFiles);
|
|
17
|
-
const [selectedPath, setSelectedPath] = useState('scenes/main.scene');
|
|
18
|
-
const [filesSheetOpen, setFilesSheetOpen] = useState(false);
|
|
19
|
-
const [selectedActorIds, setSelectedActorIds] = useState([]);
|
|
20
|
-
const [multiSelectMode, setMultiSelectMode] = useState(false);
|
|
21
|
-
const saveTimersRef = useRef({});
|
|
22
|
-
const saveVersionsRef = useRef({});
|
|
23
|
-
const drawings = {};
|
|
24
|
-
for (const [path, text] of Object.entries(files)) {
|
|
25
|
-
if (!path.endsWith('.drawing')) continue;
|
|
26
|
-
const parsed = parseJsonFile(path, text);
|
|
27
|
-
if (parsed.value) drawings[path] = parsed.value;
|
|
28
|
-
}
|
|
29
|
-
function updateSelectedFile(nextText) {
|
|
30
|
-
const path = selectedPath;
|
|
31
|
-
setFiles((current) => ({
|
|
32
|
-
...current,
|
|
33
|
-
[path]: nextText,
|
|
34
|
-
}));
|
|
35
|
-
scheduleFileWrite(path, nextText);
|
|
36
|
-
}
|
|
37
|
-
function scheduleFileWrite(path, nextText) {
|
|
38
|
-
const version = (saveVersionsRef.current[path] ?? 0) + 1;
|
|
39
|
-
saveVersionsRef.current[path] = version;
|
|
40
|
-
const existingTimer = saveTimersRef.current[path];
|
|
41
|
-
if (existingTimer) window.clearTimeout(existingTimer);
|
|
42
|
-
saveTimersRef.current[path] = window.setTimeout(() => {
|
|
43
|
-
delete saveTimersRef.current[path];
|
|
44
|
-
writeFile(path, nextText).catch((error) => {
|
|
45
|
-
if (saveVersionsRef.current[path] !== version) return;
|
|
46
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
-
console.error(`Failed to save ${path}: ${message}`);
|
|
48
|
-
});
|
|
49
|
-
}, 1500);
|
|
50
|
-
}
|
|
51
|
-
const kind = getFileKind(selectedPath);
|
|
52
|
-
const text = files[selectedPath] ?? '';
|
|
53
|
-
function selectFile(path) {
|
|
54
|
-
setSelectedPath(path);
|
|
55
|
-
setFilesSheetOpen(false);
|
|
56
|
-
setSelectedActorIds([]);
|
|
57
|
-
setMultiSelectMode(false);
|
|
58
|
-
}
|
|
59
|
-
const onToggleFiles = () => {
|
|
60
|
-
setFilesSheetOpen((previous) => !previous);
|
|
61
|
-
setSelectedActorIds([]);
|
|
62
|
-
setMultiSelectMode(false);
|
|
63
|
-
};
|
|
64
|
-
// Alt+Up / Alt+Down steps through files in file-browser sidebar order.
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
function onKeyDown(event) {
|
|
67
|
-
if (!event.altKey || event.metaKey || event.ctrlKey) return;
|
|
68
|
-
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
|
|
69
|
-
const active = document.activeElement;
|
|
70
|
-
if (
|
|
71
|
-
active &&
|
|
72
|
-
(active.tagName === 'INPUT' ||
|
|
73
|
-
active.tagName === 'TEXTAREA' ||
|
|
74
|
-
active.tagName === 'SELECT' ||
|
|
75
|
-
active.isContentEditable ||
|
|
76
|
-
active.closest('.cm-editor'))
|
|
77
|
-
) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const order = flatFileOrder(Object.keys(files));
|
|
81
|
-
const index = order.indexOf(selectedPath);
|
|
82
|
-
if (index === -1) return;
|
|
83
|
-
const nextIndex = event.key === 'ArrowUp' ? index - 1 : index + 1;
|
|
84
|
-
if (nextIndex < 0 || nextIndex >= order.length) return;
|
|
85
|
-
event.preventDefault();
|
|
86
|
-
selectFile(order[nextIndex]);
|
|
87
|
-
}
|
|
88
|
-
window.addEventListener('keydown', onKeyDown);
|
|
89
|
-
return () => window.removeEventListener('keydown', onKeyDown);
|
|
90
|
-
}, [files, selectedPath]);
|
|
91
|
-
return (
|
|
92
|
-
<AppShell>
|
|
93
|
-
<FileBrowser
|
|
94
|
-
files={files}
|
|
95
|
-
selectedPath={selectedPath}
|
|
96
|
-
onSelect={selectFile}
|
|
97
|
-
sheetOpen={filesSheetOpen}
|
|
98
|
-
onSheetOpenChange={setFilesSheetOpen}
|
|
99
|
-
/>
|
|
100
|
-
{filesSheetOpen ? (
|
|
101
|
-
<div
|
|
102
|
-
className={cx(styles.sheetBackdrop, styles.mobileOnly)}
|
|
103
|
-
onClick={() => setFilesSheetOpen(false)}
|
|
104
|
-
/>
|
|
105
|
-
) : null}
|
|
106
|
-
<MainEditor>
|
|
107
|
-
{kind === 'scene' ? (
|
|
108
|
-
<SceneEditor
|
|
109
|
-
path={selectedPath}
|
|
110
|
-
text={text}
|
|
111
|
-
files={files}
|
|
112
|
-
drawings={drawings}
|
|
113
|
-
onChange={updateSelectedFile}
|
|
114
|
-
onToggleFiles={onToggleFiles}
|
|
115
|
-
filesOpen={filesSheetOpen}
|
|
116
|
-
selectedActorIds={selectedActorIds}
|
|
117
|
-
onSelectActorIds={setSelectedActorIds}
|
|
118
|
-
multiSelectMode={multiSelectMode}
|
|
119
|
-
onSetMultiSelectMode={setMultiSelectMode}
|
|
120
|
-
/>
|
|
121
|
-
) : null}
|
|
122
|
-
{kind === 'drawing' ? (
|
|
123
|
-
<DrawingEditor
|
|
124
|
-
path={selectedPath}
|
|
125
|
-
text={text}
|
|
126
|
-
onChange={updateSelectedFile}
|
|
127
|
-
onToggleFiles={onToggleFiles}
|
|
128
|
-
filesOpen={filesSheetOpen}
|
|
129
|
-
/>
|
|
130
|
-
) : null}
|
|
131
|
-
{kind === 'code' ? (
|
|
132
|
-
<CodeEditor
|
|
133
|
-
path={selectedPath}
|
|
134
|
-
text={text}
|
|
135
|
-
onChange={updateSelectedFile}
|
|
136
|
-
onToggleFiles={onToggleFiles}
|
|
137
|
-
filesOpen={filesSheetOpen}
|
|
138
|
-
/>
|
|
139
|
-
) : null}
|
|
140
|
-
{kind === 'text' ? (
|
|
141
|
-
<CodeEditor
|
|
142
|
-
path={selectedPath}
|
|
143
|
-
text={formatJson(text)}
|
|
144
|
-
onChange={updateSelectedFile}
|
|
145
|
-
onToggleFiles={onToggleFiles}
|
|
146
|
-
filesOpen={filesSheetOpen}
|
|
147
|
-
/>
|
|
148
|
-
) : null}
|
|
149
|
-
</MainEditor>
|
|
150
|
-
</AppShell>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef } from 'react';
|
|
2
|
-
import { javascript } from '@codemirror/lang-javascript';
|
|
3
|
-
import { HighlightStyle, indentUnit, syntaxHighlighting } from '@codemirror/language';
|
|
4
|
-
import { EditorState } from '@codemirror/state';
|
|
5
|
-
import { EditorView } from '@codemirror/view';
|
|
6
|
-
import { tags } from '@lezer/highlight';
|
|
7
|
-
import { basicSetup } from 'codemirror';
|
|
8
|
-
import { basename } from '../engine/files';
|
|
9
|
-
import { EditorBody, EditorHeader, styles } from '../engine/ui';
|
|
10
|
-
const castleHighlightStyle = HighlightStyle.define([
|
|
11
|
-
{ tag: tags.strong, color: '#285CC4' },
|
|
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
|
-
height: '100%',
|
|
21
|
-
fontSize: '9pt',
|
|
22
|
-
backgroundColor: '#fff',
|
|
23
|
-
},
|
|
24
|
-
'&.cm-editor.cm-focused': {
|
|
25
|
-
outline: 'none',
|
|
26
|
-
},
|
|
27
|
-
'.cm-scroller': {
|
|
28
|
-
overflow: 'auto',
|
|
29
|
-
fontFamily: 'Menlo, Monaco, Lucida Console, monospace',
|
|
30
|
-
},
|
|
31
|
-
'.cm-content': {
|
|
32
|
-
minHeight: '100%',
|
|
33
|
-
color: '#322b28',
|
|
34
|
-
paddingBottom: '400px',
|
|
35
|
-
paddingRight: '80px',
|
|
36
|
-
},
|
|
37
|
-
'.cm-gutters': {
|
|
38
|
-
display: 'none',
|
|
39
|
-
},
|
|
40
|
-
'.cm-activeLine': {
|
|
41
|
-
backgroundColor: '#eeeeeea0',
|
|
42
|
-
},
|
|
43
|
-
'.cm-activeLineGutter': {
|
|
44
|
-
color: '#000',
|
|
45
|
-
backgroundColor: '#ddd',
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
export function CodeEditor({ path, text, onChange, onToggleFiles, filesOpen }) {
|
|
49
|
-
const editorRef = useRef(null);
|
|
50
|
-
const viewRef = useRef(null);
|
|
51
|
-
const onChangeRef = useRef(onChange);
|
|
52
|
-
const initialTextRef = useRef(text);
|
|
53
|
-
const applyingExternalTextRef = useRef(false);
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
onChangeRef.current = onChange;
|
|
56
|
-
}, [onChange]);
|
|
57
|
-
// The editor is created once per `path`; the initial doc comes from a ref
|
|
58
|
-
// so `text` is not a dep of this effect (subsequent `text` updates flow
|
|
59
|
-
// through the second effect below). On `path` change we read whatever the
|
|
60
|
-
// latest `text` is at the moment of mount.
|
|
61
|
-
initialTextRef.current = text;
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (!editorRef.current) return undefined;
|
|
64
|
-
const view = new EditorView({
|
|
65
|
-
parent: editorRef.current,
|
|
66
|
-
state: EditorState.create({
|
|
67
|
-
doc: initialTextRef.current,
|
|
68
|
-
extensions: [
|
|
69
|
-
basicSetup,
|
|
70
|
-
javascript({ jsx: true, typescript: true }),
|
|
71
|
-
indentUnit.of(' '),
|
|
72
|
-
castleCodeTheme,
|
|
73
|
-
syntaxHighlighting(castleHighlightStyle),
|
|
74
|
-
EditorView.updateListener.of((update) => {
|
|
75
|
-
if (!update.docChanged || applyingExternalTextRef.current) return;
|
|
76
|
-
onChangeRef.current(update.state.doc.toString());
|
|
77
|
-
}),
|
|
78
|
-
],
|
|
79
|
-
}),
|
|
80
|
-
});
|
|
81
|
-
viewRef.current = view;
|
|
82
|
-
return () => {
|
|
83
|
-
view.destroy();
|
|
84
|
-
viewRef.current = null;
|
|
85
|
-
};
|
|
86
|
-
}, [path]);
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
const view = viewRef.current;
|
|
89
|
-
if (!view) return;
|
|
90
|
-
const currentText = view.state.doc.toString();
|
|
91
|
-
if (currentText === text) return;
|
|
92
|
-
applyingExternalTextRef.current = true;
|
|
93
|
-
view.dispatch({
|
|
94
|
-
changes: {
|
|
95
|
-
from: 0,
|
|
96
|
-
to: currentText.length,
|
|
97
|
-
insert: text,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
applyingExternalTextRef.current = false;
|
|
101
|
-
}, [text]);
|
|
102
|
-
return (
|
|
103
|
-
<>
|
|
104
|
-
<EditorHeader title={basename(path)} onToggleFiles={onToggleFiles} filesOpen={filesOpen} />
|
|
105
|
-
<EditorBody>
|
|
106
|
-
<div className={styles.codeEditor}>
|
|
107
|
-
<div ref={editorRef} className={styles.codeMirrorHost} />
|
|
108
|
-
</div>
|
|
109
|
-
</EditorBody>
|
|
110
|
-
</>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
@@ -1,222 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
}
|