castle-web-cli 0.4.1 → 0.4.2
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/api.d.ts +53 -5
- package/dist/api.js +42 -15
- package/dist/config.d.ts +2 -0
- package/dist/config.js +25 -11
- package/dist/get-deck.d.ts +3 -0
- package/dist/get-deck.js +64 -0
- package/dist/ide-client.d.ts +1 -0
- package/dist/ide-client.js +537 -0
- package/dist/ide.d.ts +16 -0
- package/dist/ide.js +546 -0
- package/dist/index.js +36 -41
- package/dist/init.d.ts +3 -1
- package/dist/init.js +170 -24
- package/dist/localPaths.d.ts +6 -0
- package/dist/localPaths.js +33 -0
- package/dist/login.js +1 -1
- package/dist/preview.d.ts +3 -0
- package/dist/preview.js +53 -34
- package/dist/save-deck.d.ts +2 -0
- package/dist/{push.js → save-deck.js} +66 -5
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +290 -27
- package/kits/basic-2d/.prettierrc +8 -0
- package/kits/basic-2d/CLAUDE.md +131 -0
- package/kits/basic-2d/behaviors/Camera.jsx +43 -0
- package/kits/basic-2d/behaviors/Collider.jsx +71 -0
- package/kits/basic-2d/behaviors/Drawing.jsx +139 -0
- package/kits/basic-2d/behaviors/Layout.jsx +16 -0
- package/kits/basic-2d/drawings/floor.drawing +70 -0
- package/kits/basic-2d/editors/App.jsx +152 -0
- package/kits/basic-2d/editors/CodeEditor.jsx +112 -0
- package/kits/basic-2d/editors/DrawingEditor.jsx +222 -0
- package/kits/basic-2d/editors/FileBrowser.jsx +143 -0
- package/kits/basic-2d/editors/PlayOnly.jsx +21 -0
- package/kits/basic-2d/editors/SceneEditor.jsx +1012 -0
- package/kits/basic-2d/editors/behaviorRegistry.js +24 -0
- package/kits/basic-2d/editors/editorHistory.js +52 -0
- package/kits/basic-2d/engine/ScenePlayer.jsx +83 -0
- package/kits/basic-2d/engine/SceneUI.jsx +67 -0
- package/kits/basic-2d/engine/TouchControls.jsx +136 -0
- package/kits/basic-2d/engine/autoInspector.jsx +51 -0
- package/kits/basic-2d/engine/files.js +62 -0
- package/kits/basic-2d/engine/scene.js +420 -0
- package/kits/basic-2d/engine/ui.jsx +344 -0
- package/kits/basic-2d/engine/ui.module.css +928 -0
- package/kits/basic-2d/eslint.config.js +50 -0
- package/kits/basic-2d/index.html +11 -0
- package/kits/basic-2d/main.jsx +10 -0
- package/kits/basic-2d/package-lock.json +2706 -0
- package/kits/basic-2d/package.json +41 -0
- package/kits/basic-2d/scenes/main.scene +108 -0
- package/kits/basic-2d/vite.config.js +1 -0
- package/kits/basic-2d-frozen/.prettierrc +8 -0
- package/kits/basic-2d-frozen/CLAUDE.md +131 -0
- package/kits/basic-2d-frozen/behaviors/Camera.jsx +43 -0
- package/kits/basic-2d-frozen/behaviors/Collider.jsx +71 -0
- package/kits/basic-2d-frozen/behaviors/Drawing.jsx +139 -0
- package/kits/basic-2d-frozen/behaviors/Layout.jsx +16 -0
- package/kits/basic-2d-frozen/drawings/floor.drawing +70 -0
- package/kits/basic-2d-frozen/editors/App.jsx +152 -0
- package/kits/basic-2d-frozen/editors/CodeEditor.jsx +112 -0
- package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +222 -0
- package/kits/basic-2d-frozen/editors/FileBrowser.jsx +143 -0
- package/kits/basic-2d-frozen/editors/PlayOnly.jsx +21 -0
- package/kits/basic-2d-frozen/editors/SceneEditor.jsx +1012 -0
- package/kits/basic-2d-frozen/editors/behaviorRegistry.js +24 -0
- package/kits/basic-2d-frozen/editors/editorHistory.js +52 -0
- package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +83 -0
- package/kits/basic-2d-frozen/engine/SceneUI.jsx +67 -0
- package/kits/basic-2d-frozen/engine/TouchControls.jsx +136 -0
- package/kits/basic-2d-frozen/engine/autoInspector.jsx +51 -0
- package/kits/basic-2d-frozen/engine/files.js +62 -0
- package/kits/basic-2d-frozen/engine/scene.js +420 -0
- package/kits/basic-2d-frozen/engine/ui.jsx +344 -0
- package/kits/basic-2d-frozen/engine/ui.module.css +928 -0
- package/kits/basic-2d-frozen/eslint.config.js +50 -0
- package/kits/basic-2d-frozen/index.html +11 -0
- package/kits/basic-2d-frozen/main.jsx +10 -0
- package/kits/basic-2d-frozen/package-lock.json +2706 -0
- package/kits/basic-2d-frozen/package.json +41 -0
- package/kits/basic-2d-frozen/scenes/main.scene +108 -0
- package/kits/basic-2d-frozen/vite.config.js +1 -0
- package/kits/rpg-2d/.prettierrc +8 -0
- package/kits/rpg-2d/behaviors/Camera.tsx +52 -0
- package/kits/rpg-2d/behaviors/Collider.tsx +98 -0
- package/kits/rpg-2d/behaviors/Dialog.tsx +184 -0
- package/kits/rpg-2d/behaviors/Drawing.tsx +161 -0
- package/kits/rpg-2d/behaviors/Friend.tsx +45 -0
- package/kits/rpg-2d/behaviors/Layout.tsx +29 -0
- package/kits/rpg-2d/behaviors/PlayerController.tsx +255 -0
- package/kits/rpg-2d/behaviors/Portal.tsx +60 -0
- package/kits/rpg-2d/behaviors/QuestLog.tsx +90 -0
- package/kits/rpg-2d/behaviors/SaveMenu.tsx +123 -0
- package/kits/rpg-2d/behaviors/Tilemap.tsx +90 -0
- package/kits/rpg-2d/drawings/bld-home.drawing +8136 -0
- package/kits/rpg-2d/drawings/env-crate.drawing +509 -0
- package/kits/rpg-2d/drawings/env-fence.drawing +536 -0
- package/kits/rpg-2d/drawings/env-flower-bed.drawing +607 -0
- package/kits/rpg-2d/drawings/env-fountain.drawing +2622 -0
- package/kits/rpg-2d/drawings/env-hedge.drawing +601 -0
- package/kits/rpg-2d/drawings/env-house-blue.drawing +1 -0
- package/kits/rpg-2d/drawings/env-house-green.drawing +1 -0
- package/kits/rpg-2d/drawings/env-tree-oak.drawing +1540 -0
- package/kits/rpg-2d/drawings/env-tree-pine.drawing +1315 -0
- package/kits/rpg-2d/drawings/floor.drawing +70 -0
- package/kits/rpg-2d/drawings/fx-sparkle.drawing +926 -0
- package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +1099 -0
- package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +4177 -0
- package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +1099 -0
- package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +4177 -0
- package/kits/rpg-2d/drawings/player-idle-down.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-idle-left.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-idle-right.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-idle-up.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-walk-down.drawing +4148 -0
- package/kits/rpg-2d/drawings/player-walk-left.drawing +4148 -0
- package/kits/rpg-2d/drawings/player-walk-right.drawing +4148 -0
- package/kits/rpg-2d/drawings/player-walk-up.drawing +4148 -0
- package/kits/rpg-2d/editors/App.tsx +163 -0
- package/kits/rpg-2d/editors/CodeEditor.tsx +120 -0
- package/kits/rpg-2d/editors/DrawingEditor.tsx +278 -0
- package/kits/rpg-2d/editors/FileBrowser.tsx +191 -0
- package/kits/rpg-2d/editors/PlayOnly.tsx +26 -0
- package/kits/rpg-2d/editors/SceneEditor.tsx +1093 -0
- package/kits/rpg-2d/editors/behaviorRegistry.ts +33 -0
- package/kits/rpg-2d/editors/editorHistory.ts +75 -0
- package/kits/rpg-2d/editors/editorProps.ts +10 -0
- package/kits/rpg-2d/engine/ScenePlayer.tsx +130 -0
- package/kits/rpg-2d/engine/SceneUI.tsx +74 -0
- package/kits/rpg-2d/engine/TouchControls.tsx +157 -0
- package/kits/rpg-2d/engine/autoInspector.tsx +111 -0
- package/kits/rpg-2d/engine/drawing.ts +81 -0
- package/kits/rpg-2d/engine/files.ts +215 -0
- package/kits/rpg-2d/engine/scene.ts +484 -0
- package/kits/rpg-2d/engine/ui.module.css +928 -0
- package/kits/rpg-2d/engine/ui.tsx +483 -0
- package/kits/rpg-2d/eslint.config.js +46 -0
- package/kits/rpg-2d/index.html +11 -0
- package/kits/rpg-2d/main.tsx +14 -0
- package/kits/rpg-2d/package-lock.json +3149 -0
- package/kits/rpg-2d/package.json +46 -0
- package/kits/rpg-2d/scenes/main.scene +203 -0
- package/kits/rpg-2d/tsconfig.json +17 -0
- package/kits/rpg-2d/vite-env.d.ts +7 -0
- package/kits/rpg-2d/vite.config.js +1 -0
- package/package.json +27 -5
- package/AGENTS.md +0 -25
- package/dist/push.d.ts +0 -1
- package/src/api.ts +0 -160
- package/src/bundle.ts +0 -28
- package/src/config.ts +0 -36
- package/src/index.ts +0 -143
- package/src/init.ts +0 -71
- package/src/login.ts +0 -24
- package/src/preview.ts +0 -94
- package/src/push.ts +0 -118
- package/src/serve.ts +0 -134
- package/tsconfig.json +0 -13
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Panel, SelectField } from '../engine/ui';
|
|
3
|
+
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
+
|
|
5
|
+
export class Drawing {
|
|
6
|
+
static behaviorName = 'Drawing';
|
|
7
|
+
|
|
8
|
+
static defaultProps = {
|
|
9
|
+
file: 'drawings/floor.drawing',
|
|
10
|
+
tint: '#ffffffff',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
constructor(props) {
|
|
14
|
+
this.props = props;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
draw(actor, scene, ctx) {
|
|
18
|
+
if (actor.runtime?.collected) return;
|
|
19
|
+
const layout = actor.components.Layout;
|
|
20
|
+
if (!layout) return;
|
|
21
|
+
const drawing = scene.drawings[this.props.file];
|
|
22
|
+
if (!drawing) return;
|
|
23
|
+
drawPixelDrawing(
|
|
24
|
+
ctx,
|
|
25
|
+
drawing,
|
|
26
|
+
layout.x,
|
|
27
|
+
layout.y,
|
|
28
|
+
layout.width,
|
|
29
|
+
layout.height,
|
|
30
|
+
this.props.tint
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static Inspector({ component, setComponent, files }) {
|
|
35
|
+
const drawingFiles = getDrawingFiles(files);
|
|
36
|
+
return (
|
|
37
|
+
<Panel title="Drawing">
|
|
38
|
+
<SelectField
|
|
39
|
+
label="File"
|
|
40
|
+
value={component.file}
|
|
41
|
+
onChange={(file) => setComponent({ file })}
|
|
42
|
+
options={drawingFiles}
|
|
43
|
+
/>
|
|
44
|
+
<AutoFields
|
|
45
|
+
defaultProps={Drawing.defaultProps}
|
|
46
|
+
component={component}
|
|
47
|
+
setComponent={setComponent}
|
|
48
|
+
only={['tint']}
|
|
49
|
+
/>
|
|
50
|
+
</Panel>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function drawPixelDrawing(ctx, drawing, x, y, width, height, tint) {
|
|
56
|
+
if (drawing.width <= 0 || drawing.height <= 0) return;
|
|
57
|
+
// Blit the cached native-resolution canvas as a single image. Painting each
|
|
58
|
+
// pixel as its own `fillRect` left hairline anti-aliased seams under a
|
|
59
|
+
// rotation transform; one `drawImage` has no inter-pixel edges.
|
|
60
|
+
const canvas = getDrawingCanvas(drawing, tint);
|
|
61
|
+
const prevSmoothing = ctx.imageSmoothingEnabled;
|
|
62
|
+
ctx.imageSmoothingEnabled = false;
|
|
63
|
+
ctx.drawImage(canvas, x, y, width, height);
|
|
64
|
+
ctx.imageSmoothingEnabled = prevSmoothing;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Cache of native-resolution offscreen canvases keyed by drawing data and
|
|
68
|
+
// tint. A drawing edit produces a fresh `DrawingData` object (see
|
|
69
|
+
// DrawingEditor's `structuredClone`), so a new object naturally misses the
|
|
70
|
+
// WeakMap and rebuilds.
|
|
71
|
+
const drawingCanvasCache = new WeakMap();
|
|
72
|
+
|
|
73
|
+
// Render the pixel art once, unrotated, at 1px per pixel into an offscreen
|
|
74
|
+
// canvas, applying the tint. Cached so the per-frame draw is a single blit.
|
|
75
|
+
function getDrawingCanvas(drawing, tint) {
|
|
76
|
+
const tintKey = tint ?? '';
|
|
77
|
+
let byTint = drawingCanvasCache.get(drawing);
|
|
78
|
+
if (!byTint) {
|
|
79
|
+
byTint = new Map();
|
|
80
|
+
drawingCanvasCache.set(drawing, byTint);
|
|
81
|
+
}
|
|
82
|
+
const cached = byTint.get(tintKey);
|
|
83
|
+
if (cached) return cached;
|
|
84
|
+
|
|
85
|
+
const canvas = document.createElement('canvas');
|
|
86
|
+
canvas.width = drawing.width;
|
|
87
|
+
canvas.height = drawing.height;
|
|
88
|
+
const octx = canvas.getContext('2d');
|
|
89
|
+
if (octx) {
|
|
90
|
+
const tintRgba = parseTint(tint);
|
|
91
|
+
for (let py = 0; py < drawing.height; py++) {
|
|
92
|
+
for (let px = 0; px < drawing.width; px++) {
|
|
93
|
+
const color = drawing.pixels[py * drawing.width + px];
|
|
94
|
+
if (!color || color === 'transparent' || color.endsWith('00')) continue;
|
|
95
|
+
octx.fillStyle = tintRgba ? applyTint(color, tintRgba) : color;
|
|
96
|
+
octx.fillRect(px, py, 1, 1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
byTint.set(tintKey, canvas);
|
|
101
|
+
return canvas;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Parse a hex color into [r, g, b, a] channels (0-255). Returns null when the
|
|
105
|
+
// string is not a hex color so callers can fall back to the raw color.
|
|
106
|
+
function parseHexColor(color) {
|
|
107
|
+
const hex = color.trim().replace(/^#/, '');
|
|
108
|
+
if (hex.length !== 6 && hex.length !== 8) return null;
|
|
109
|
+
if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
110
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
111
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
112
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
113
|
+
const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) : 255;
|
|
114
|
+
return [r, g, b, a];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Returns the tint as [r, g, b, a] channels, or null when there is no tint or
|
|
118
|
+
// the tint is white (#ffffffff) -- both meaning "no change".
|
|
119
|
+
function parseTint(tint) {
|
|
120
|
+
if (!tint) return null;
|
|
121
|
+
const rgba = parseHexColor(tint);
|
|
122
|
+
if (!rgba) return null;
|
|
123
|
+
if (rgba[0] === 255 && rgba[1] === 255 && rgba[2] === 255 && rgba[3] === 255) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return rgba;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Multiply each channel of a pixel color by the tint color (both 0-255).
|
|
130
|
+
function applyTint(color, tint) {
|
|
131
|
+
const rgba = parseHexColor(color);
|
|
132
|
+
if (!rgba) return color;
|
|
133
|
+
const out = rgba.map((channel, i) => Math.round((channel * tint[i]) / 255));
|
|
134
|
+
return '#' + out.map((channel) => channel.toString(16).padStart(2, '0')).join('');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getDrawingFiles(files) {
|
|
138
|
+
return Object.keys(files).filter((path) => path.endsWith('.drawing'));
|
|
139
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"width": 8,
|
|
3
|
+
"height": 8,
|
|
4
|
+
"pixels": [
|
|
5
|
+
"#ffffffff",
|
|
6
|
+
"#ffffffff",
|
|
7
|
+
"#ffffffff",
|
|
8
|
+
"#ffffffff",
|
|
9
|
+
"#ffffffff",
|
|
10
|
+
"#ffffffff",
|
|
11
|
+
"#ffffffff",
|
|
12
|
+
"#ffffffff",
|
|
13
|
+
"#ffffffff",
|
|
14
|
+
"#d8d8d8ff",
|
|
15
|
+
"#d8d8d8ff",
|
|
16
|
+
"#d8d8d8ff",
|
|
17
|
+
"#d8d8d8ff",
|
|
18
|
+
"#d8d8d8ff",
|
|
19
|
+
"#d8d8d8ff",
|
|
20
|
+
"#ffffffff",
|
|
21
|
+
"#ffffffff",
|
|
22
|
+
"#d8d8d8ff",
|
|
23
|
+
"#ffffffff",
|
|
24
|
+
"#ffffffff",
|
|
25
|
+
"#ffffffff",
|
|
26
|
+
"#ffffffff",
|
|
27
|
+
"#d8d8d8ff",
|
|
28
|
+
"#ffffffff",
|
|
29
|
+
"#ffffffff",
|
|
30
|
+
"#d8d8d8ff",
|
|
31
|
+
"#ffffffff",
|
|
32
|
+
"#ffffffff",
|
|
33
|
+
"#ffffffff",
|
|
34
|
+
"#ffffffff",
|
|
35
|
+
"#d8d8d8ff",
|
|
36
|
+
"#ffffffff",
|
|
37
|
+
"#ffffffff",
|
|
38
|
+
"#d8d8d8ff",
|
|
39
|
+
"#ffffffff",
|
|
40
|
+
"#ffffffff",
|
|
41
|
+
"#ffffffff",
|
|
42
|
+
"#ffffffff",
|
|
43
|
+
"#d8d8d8ff",
|
|
44
|
+
"#ffffffff",
|
|
45
|
+
"#ffffffff",
|
|
46
|
+
"#d8d8d8ff",
|
|
47
|
+
"#ffffffff",
|
|
48
|
+
"#ffffffff",
|
|
49
|
+
"#ffffffff",
|
|
50
|
+
"#ffffffff",
|
|
51
|
+
"#d8d8d8ff",
|
|
52
|
+
"#ffffffff",
|
|
53
|
+
"#ffffffff",
|
|
54
|
+
"#d8d8d8ff",
|
|
55
|
+
"#d8d8d8ff",
|
|
56
|
+
"#d8d8d8ff",
|
|
57
|
+
"#d8d8d8ff",
|
|
58
|
+
"#d8d8d8ff",
|
|
59
|
+
"#d8d8d8ff",
|
|
60
|
+
"#ffffffff",
|
|
61
|
+
"#ffffffff",
|
|
62
|
+
"#ffffffff",
|
|
63
|
+
"#ffffffff",
|
|
64
|
+
"#ffffffff",
|
|
65
|
+
"#ffffffff",
|
|
66
|
+
"#ffffffff",
|
|
67
|
+
"#ffffffff",
|
|
68
|
+
"#ffffffff"
|
|
69
|
+
]
|
|
70
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
}
|