castle-web-cli 0.4.3 → 0.4.4
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/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,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
|
-
}
|