castle-web-cli 0.4.59 → 0.4.60
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 +3 -2
- package/dist/agent.d.ts +4 -4
- package/dist/agent.js +342 -239
- package/dist/api.d.ts +9 -0
- package/dist/api.js +1 -1
- package/dist/castle-host/host.d.ts +47 -0
- package/dist/castle-host/host.js +373 -0
- package/dist/chat-client.js +199 -96
- package/dist/ide.d.ts +3 -3
- package/dist/ide.js +92 -64
- package/dist/serve.js +42 -1
- package/kits/basic-2d/CLAUDE.md +4 -3
- package/kits/basic-2d/behaviors/Drawing.jsx +5 -2
- package/kits/basic-2d/drawings/default.drawing +70 -0
- package/package.json +5 -2
package/dist/ide.js
CHANGED
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
// Backend pattern ported from castle-cli's `ide` branch: @lydell/node-pty for
|
|
7
7
|
// the PTY, an @xterm/headless screen + @xterm/addon-serialize so a reconnecting
|
|
8
8
|
// client gets a full replay of the current screen + scrollback.
|
|
9
|
-
import * as fs from
|
|
10
|
-
import * as path from
|
|
11
|
-
import { createRequire } from
|
|
12
|
-
import { fileURLToPath } from
|
|
13
|
-
import { spawn as spawnPty } from
|
|
14
|
-
import headlessPkg from
|
|
15
|
-
import { SerializeAddon } from
|
|
16
|
-
import { WebSocketServer } from
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { spawn as spawnPty } from "@lydell/node-pty";
|
|
14
|
+
import headlessPkg from "@xterm/headless";
|
|
15
|
+
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
16
|
+
import { WebSocketServer } from "ws";
|
|
17
17
|
const HeadlessTerminal = headlessPkg.Terminal;
|
|
18
18
|
const require_ = createRequire(import.meta.url);
|
|
19
19
|
const DIST_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const PTY_TERM =
|
|
20
|
+
const PTY_TERM = "xterm-256color";
|
|
21
21
|
const INITIAL_COLS = 80;
|
|
22
22
|
const INITIAL_ROWS = 24;
|
|
23
23
|
const SCROLLBACK = 4000;
|
|
@@ -25,8 +25,8 @@ const SCROLLBACK = 4000;
|
|
|
25
25
|
// into the iframe at `/index.html`); `/__castle/ide/<asset>` are the shell's
|
|
26
26
|
// static assets; `/__castle/pty` is the PTY WebSocket (handled via a direct
|
|
27
27
|
// upgrade handler on Vite's HTTP server).
|
|
28
|
-
export const IDE_ASSET_PREFIX =
|
|
29
|
-
export const PTY_WS_PATH =
|
|
28
|
+
export const IDE_ASSET_PREFIX = "/__castle/ide/";
|
|
29
|
+
export const PTY_WS_PATH = "/__castle/pty";
|
|
30
30
|
// Injected into every served deck's <head>. When the deck runs embedded in
|
|
31
31
|
// the IDE shell iframe, Ctrl+T moves keyboard focus out to the panel (the
|
|
32
32
|
// chat input or the terminal, whichever view is active) -- but while the
|
|
@@ -38,20 +38,20 @@ export const DECK_FOCUS_SCRIPT = `<script>(function(){if(window.parent===window)
|
|
|
38
38
|
`e.preventDefault();try{parent.postMessage({type:'castle-ide-toggle-focus'},'*');}catch(_){}}` +
|
|
39
39
|
`},true);})();</script>`;
|
|
40
40
|
function defaultShell() {
|
|
41
|
-
if (process.platform ===
|
|
42
|
-
return { command: process.env.COMSPEC ??
|
|
41
|
+
if (process.platform === "win32") {
|
|
42
|
+
return { command: process.env.COMSPEC ?? "cmd.exe", args: [] };
|
|
43
43
|
}
|
|
44
|
-
return { command: process.env.SHELL ??
|
|
44
|
+
return { command: process.env.SHELL ?? "/bin/zsh", args: ["-l"] };
|
|
45
45
|
}
|
|
46
46
|
function ptyEnv() {
|
|
47
47
|
const env = {
|
|
48
48
|
...process.env,
|
|
49
49
|
TERM: PTY_TERM,
|
|
50
|
-
COLORTERM:
|
|
51
|
-
CLICOLOR:
|
|
52
|
-
CLICOLOR_FORCE:
|
|
53
|
-
FORCE_COLOR: process.env.FORCE_COLOR ===
|
|
54
|
-
TERM_PROGRAM:
|
|
50
|
+
COLORTERM: "truecolor",
|
|
51
|
+
CLICOLOR: "1",
|
|
52
|
+
CLICOLOR_FORCE: "1",
|
|
53
|
+
FORCE_COLOR: process.env.FORCE_COLOR === "0" ? "3" : (process.env.FORCE_COLOR ?? "3"),
|
|
54
|
+
TERM_PROGRAM: "castle-web-ide",
|
|
55
55
|
};
|
|
56
56
|
delete env.NO_COLOR;
|
|
57
57
|
delete env.NODE_DISABLE_COLORS;
|
|
@@ -65,10 +65,10 @@ function clampSize(value, fallback) {
|
|
|
65
65
|
}
|
|
66
66
|
export function rawDataToString(data) {
|
|
67
67
|
if (Array.isArray(data))
|
|
68
|
-
return Buffer.concat(data).toString(
|
|
68
|
+
return Buffer.concat(data).toString("utf8");
|
|
69
69
|
if (Buffer.isBuffer(data))
|
|
70
|
-
return data.toString(
|
|
71
|
-
return Buffer.from(new Uint8Array(data)).toString(
|
|
70
|
+
return data.toString("utf8");
|
|
71
|
+
return Buffer.from(new Uint8Array(data)).toString("utf8");
|
|
72
72
|
}
|
|
73
73
|
function send(socket, body) {
|
|
74
74
|
if (socket.readyState === socket.OPEN)
|
|
@@ -80,7 +80,7 @@ function queueScreenOp(session, op) {
|
|
|
80
80
|
session.renderQueue = session.renderQueue
|
|
81
81
|
.catch(() => undefined)
|
|
82
82
|
.then(op)
|
|
83
|
-
.catch((err) => console.error(
|
|
83
|
+
.catch((err) => console.error("[ide] screen render failed", err));
|
|
84
84
|
}
|
|
85
85
|
async function waitForStableScreen(session) {
|
|
86
86
|
while (true) {
|
|
@@ -129,16 +129,16 @@ export function createIdeServer(opts) {
|
|
|
129
129
|
};
|
|
130
130
|
pty.onData((data) => {
|
|
131
131
|
queueScreenOp(s, () => new Promise((done) => s.screen.write(data, done)));
|
|
132
|
-
broadcast(s, { type:
|
|
132
|
+
broadcast(s, { type: "output", data });
|
|
133
133
|
});
|
|
134
134
|
pty.onExit(({ exitCode, signal }) => {
|
|
135
135
|
s.exited = { exitCode, signal };
|
|
136
|
-
broadcast(s, { type:
|
|
136
|
+
broadcast(s, { type: "exit", exitCode, signal });
|
|
137
137
|
// Clients get the `exit` message above (which disables their reconnect),
|
|
138
138
|
// then we close the sockets so they don't sit half-open.
|
|
139
139
|
for (const socket of s.clients) {
|
|
140
140
|
try {
|
|
141
|
-
socket.close(1000,
|
|
141
|
+
socket.close(1000, "shell exited");
|
|
142
142
|
}
|
|
143
143
|
catch {
|
|
144
144
|
/* ignore */
|
|
@@ -177,17 +177,21 @@ export function createIdeServer(opts) {
|
|
|
177
177
|
if (socket.readyState !== socket.OPEN)
|
|
178
178
|
return;
|
|
179
179
|
send(socket, {
|
|
180
|
-
type:
|
|
180
|
+
type: "replay",
|
|
181
181
|
data: s.serializeAddon.serialize({ scrollback: SCROLLBACK }),
|
|
182
182
|
});
|
|
183
183
|
if (s.exited) {
|
|
184
|
-
send(socket, {
|
|
184
|
+
send(socket, {
|
|
185
|
+
type: "exit",
|
|
186
|
+
exitCode: s.exited.exitCode,
|
|
187
|
+
signal: s.exited.signal,
|
|
188
|
+
});
|
|
185
189
|
}
|
|
186
190
|
}
|
|
187
191
|
catch (err) {
|
|
188
192
|
send(socket, {
|
|
189
|
-
type:
|
|
190
|
-
error: err instanceof Error ? err.message :
|
|
193
|
+
type: "error",
|
|
194
|
+
error: err instanceof Error ? err.message : "could not restore screen",
|
|
191
195
|
});
|
|
192
196
|
}
|
|
193
197
|
finally {
|
|
@@ -195,7 +199,7 @@ export function createIdeServer(opts) {
|
|
|
195
199
|
s.clients.add(socket);
|
|
196
200
|
}
|
|
197
201
|
})();
|
|
198
|
-
socket.on(
|
|
202
|
+
socket.on("message", (raw) => {
|
|
199
203
|
let msg;
|
|
200
204
|
try {
|
|
201
205
|
msg = JSON.parse(rawDataToString(raw));
|
|
@@ -203,30 +207,33 @@ export function createIdeServer(opts) {
|
|
|
203
207
|
catch {
|
|
204
208
|
return;
|
|
205
209
|
}
|
|
206
|
-
if (msg.type ===
|
|
210
|
+
if (msg.type === "input" && typeof msg.data === "string") {
|
|
207
211
|
if (!s.exited)
|
|
208
212
|
s.pty.write(msg.data);
|
|
209
213
|
}
|
|
210
|
-
else if (msg.type ===
|
|
214
|
+
else if (msg.type === "resize") {
|
|
211
215
|
resizeSession(s, Number(msg.cols), Number(msg.rows));
|
|
212
216
|
}
|
|
213
217
|
});
|
|
214
|
-
socket.on(
|
|
218
|
+
socket.on("close", () => {
|
|
215
219
|
s.clients.delete(socket);
|
|
216
220
|
});
|
|
217
221
|
}
|
|
218
222
|
const wss = new WebSocketServer({ noServer: true });
|
|
219
223
|
function handleUpgrade(req, socket, head) {
|
|
220
|
-
const url = new URL(req.url ??
|
|
224
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
221
225
|
if (url.pathname !== PTY_WS_PATH)
|
|
222
226
|
return false;
|
|
223
227
|
wss.handleUpgrade(req, socket, head, (ws) => attachClient(ws));
|
|
224
228
|
return true;
|
|
225
229
|
}
|
|
226
230
|
function handleHttpRequest(_req, res, reqPath) {
|
|
227
|
-
if (reqPath ===
|
|
231
|
+
if (reqPath === "/") {
|
|
228
232
|
const html = renderIdePage(deckLabel);
|
|
229
|
-
res.writeHead(200, {
|
|
233
|
+
res.writeHead(200, {
|
|
234
|
+
"content-type": "text/html; charset=utf-8",
|
|
235
|
+
"cache-control": "no-store",
|
|
236
|
+
});
|
|
230
237
|
res.end(html);
|
|
231
238
|
return true;
|
|
232
239
|
}
|
|
@@ -238,8 +245,8 @@ export function createIdeServer(opts) {
|
|
|
238
245
|
return true;
|
|
239
246
|
}
|
|
240
247
|
res.writeHead(200, {
|
|
241
|
-
|
|
242
|
-
|
|
248
|
+
"content-type": resolved.contentType,
|
|
249
|
+
"cache-control": "no-store",
|
|
243
250
|
});
|
|
244
251
|
fs.createReadStream(resolved.filePath).pipe(res);
|
|
245
252
|
return true;
|
|
@@ -256,7 +263,7 @@ export function createIdeServer(opts) {
|
|
|
256
263
|
}
|
|
257
264
|
for (const socket of session.clients) {
|
|
258
265
|
try {
|
|
259
|
-
socket.close(1001,
|
|
266
|
+
socket.close(1001, "serve shutting down");
|
|
260
267
|
}
|
|
261
268
|
catch {
|
|
262
269
|
/* ignore */
|
|
@@ -272,17 +279,26 @@ export function createIdeServer(opts) {
|
|
|
272
279
|
// file by tsc into the same dist directory.
|
|
273
280
|
function resolveIdeAsset(asset) {
|
|
274
281
|
const assets = {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
282
|
+
"xterm.js": {
|
|
283
|
+
spec: "@xterm/xterm/lib/xterm.js",
|
|
284
|
+
contentType: "text/javascript; charset=utf-8",
|
|
285
|
+
},
|
|
286
|
+
"xterm.css": {
|
|
287
|
+
spec: "@xterm/xterm/css/xterm.css",
|
|
288
|
+
contentType: "text/css; charset=utf-8",
|
|
289
|
+
},
|
|
290
|
+
"addon-fit.js": {
|
|
291
|
+
spec: "@xterm/addon-fit/lib/addon-fit.js",
|
|
292
|
+
contentType: "text/javascript; charset=utf-8",
|
|
280
293
|
},
|
|
281
294
|
};
|
|
282
295
|
const fromModule = assets[asset];
|
|
283
296
|
if (fromModule) {
|
|
284
297
|
try {
|
|
285
|
-
return {
|
|
298
|
+
return {
|
|
299
|
+
filePath: require_.resolve(fromModule.spec),
|
|
300
|
+
contentType: fromModule.contentType,
|
|
301
|
+
};
|
|
286
302
|
}
|
|
287
303
|
catch {
|
|
288
304
|
return null;
|
|
@@ -291,9 +307,12 @@ function resolveIdeAsset(asset) {
|
|
|
291
307
|
// React's package "exports" hides umd/ from require.resolve; resolve the
|
|
292
308
|
// package root via its package.json (always exported) and join from there.
|
|
293
309
|
const umdAssets = {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
310
|
+
"react.js": { pkg: "react", rel: "umd/react.production.min.js" },
|
|
311
|
+
"react-dom.js": {
|
|
312
|
+
pkg: "react-dom",
|
|
313
|
+
rel: "umd/react-dom.production.min.js",
|
|
314
|
+
},
|
|
315
|
+
"marked.js": { pkg: "marked", rel: "lib/marked.umd.js" },
|
|
297
316
|
};
|
|
298
317
|
const fromUmd = umdAssets[asset];
|
|
299
318
|
if (fromUmd) {
|
|
@@ -301,17 +320,17 @@ function resolveIdeAsset(asset) {
|
|
|
301
320
|
const pkgRoot = path.dirname(require_.resolve(`${fromUmd.pkg}/package.json`));
|
|
302
321
|
const filePath = path.join(pkgRoot, fromUmd.rel);
|
|
303
322
|
return fs.existsSync(filePath)
|
|
304
|
-
? { filePath, contentType:
|
|
323
|
+
? { filePath, contentType: "text/javascript; charset=utf-8" }
|
|
305
324
|
: null;
|
|
306
325
|
}
|
|
307
326
|
catch {
|
|
308
327
|
return null;
|
|
309
328
|
}
|
|
310
329
|
}
|
|
311
|
-
if (asset ===
|
|
330
|
+
if (asset === "ide-client.js" || asset === "chat-client.js") {
|
|
312
331
|
const filePath = path.join(DIST_DIR, asset);
|
|
313
332
|
return fs.existsSync(filePath)
|
|
314
|
-
? { filePath, contentType:
|
|
333
|
+
? { filePath, contentType: "text/javascript; charset=utf-8" }
|
|
315
334
|
: null;
|
|
316
335
|
}
|
|
317
336
|
return null;
|
|
@@ -632,9 +651,24 @@ const IDE_STYLES = `
|
|
|
632
651
|
}
|
|
633
652
|
/* Live stream of a running task agent (text lines + [tool labels]) shown
|
|
634
653
|
on expand -- capped height, auto-scrolled, fading at both scroll edges. */
|
|
654
|
+
.task-feed-tool {
|
|
655
|
+
padding-left: 14px;
|
|
656
|
+
color: #8c959f;
|
|
657
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
658
|
+
font-size: 11px;
|
|
659
|
+
}
|
|
660
|
+
.task-feed-msg > :first-child { margin-top: 0; }
|
|
661
|
+
.task-feed-msg > :last-child { margin-bottom: 0; }
|
|
662
|
+
.task-feed-msg p { margin: 0.25em 0; }
|
|
663
|
+
.task-feed-msg code {
|
|
664
|
+
background: rgba(175, 184, 193, 0.2);
|
|
665
|
+
border-radius: 3px;
|
|
666
|
+
padding: 0 3px;
|
|
667
|
+
font-size: 11px;
|
|
668
|
+
}
|
|
635
669
|
.task-feed {
|
|
636
670
|
margin-top: 6px;
|
|
637
|
-
max-height:
|
|
671
|
+
max-height: 65px;
|
|
638
672
|
overflow-y: auto;
|
|
639
673
|
font-size: 12px;
|
|
640
674
|
line-height: 1.5;
|
|
@@ -644,17 +678,10 @@ const IDE_STYLES = `
|
|
|
644
678
|
-webkit-mask-image: linear-gradient(
|
|
645
679
|
to bottom,
|
|
646
680
|
transparent,
|
|
647
|
-
#000000
|
|
648
|
-
#000000
|
|
649
|
-
transparent
|
|
650
|
-
);
|
|
651
|
-
mask-image: linear-gradient(
|
|
652
|
-
to bottom,
|
|
653
|
-
transparent,
|
|
654
|
-
#000000 14px,
|
|
655
|
-
#000000 calc(100% - 14px),
|
|
656
|
-
transparent
|
|
681
|
+
#000000 5px,
|
|
682
|
+
#000000 100%
|
|
657
683
|
);
|
|
684
|
+
mask-image: linear-gradient(to bottom, transparent, #000000 5px, #000000 100%);
|
|
658
685
|
}
|
|
659
686
|
.task-row { display: flex; align-items: center; gap: 8px; }
|
|
660
687
|
.task-title {
|
|
@@ -667,6 +694,7 @@ const IDE_STYLES = `
|
|
|
667
694
|
.task-title::first-letter { text-transform: uppercase; }
|
|
668
695
|
.task-status { color: #6e7781; font-size: 12px; flex: 0 0 auto; }
|
|
669
696
|
.task-status.failed, .task-status.interrupted { color: #a40e26; }
|
|
697
|
+
.task-time { color: #6e7781; font-size: 12px; flex: 0 0 auto; font-variant-numeric: tabular-nums; }
|
|
670
698
|
.task-pie {
|
|
671
699
|
width: 15px;
|
|
672
700
|
height: 15px;
|
|
@@ -862,5 +890,5 @@ function renderIdePage(deckLabel) {
|
|
|
862
890
|
`;
|
|
863
891
|
}
|
|
864
892
|
function escapeHtml(value) {
|
|
865
|
-
return value.replace(/[&<>"]/g, (c) => ({
|
|
893
|
+
return value.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c] ?? c);
|
|
866
894
|
}
|
package/dist/serve.js
CHANGED
|
@@ -8,6 +8,8 @@ import { createIdeServer, DECK_FOCUS_SCRIPT } from './ide.js';
|
|
|
8
8
|
import { createAgentServer } from './agent.js';
|
|
9
9
|
import { sceneFilesPlugin } from './vitePlugins.js';
|
|
10
10
|
import * as config from './config.js';
|
|
11
|
+
import { graphql as castleGraphql } from './api.js';
|
|
12
|
+
import { executeCommand } from './castle-host/host.js';
|
|
11
13
|
function isPortFree(port) {
|
|
12
14
|
return new Promise((resolve) => {
|
|
13
15
|
const srv = net.createServer();
|
|
@@ -29,7 +31,7 @@ function castlePlugin(wsPort, ideServer, agentServer) {
|
|
|
29
31
|
transformIndexHtml: {
|
|
30
32
|
order: 'pre',
|
|
31
33
|
handler(html) {
|
|
32
|
-
return html.replace(/<head(\s[^>]*)?>/i, (match) => `${match}\n <script>window.CastleEmbed={edit:true};</script>\n ${DECK_FOCUS_SCRIPT}`);
|
|
34
|
+
return html.replace(/<head(\s[^>]*)?>/i, (match) => `${match}\n <script>window.CastleEmbed={edit:true,host:'dev'};</script>\n ${DECK_FOCUS_SCRIPT}`);
|
|
33
35
|
},
|
|
34
36
|
},
|
|
35
37
|
configureServer(server) {
|
|
@@ -64,6 +66,38 @@ function castlePlugin(wsPort, ideServer, agentServer) {
|
|
|
64
66
|
},
|
|
65
67
|
};
|
|
66
68
|
}
|
|
69
|
+
// The dev author's username, fetched once (config stores token+userId but not
|
|
70
|
+
// username). Cached for the serve lifetime so User.getCurrent works in dev.
|
|
71
|
+
let cachedDevUsername;
|
|
72
|
+
async function devUsername() {
|
|
73
|
+
if (cachedDevUsername !== undefined)
|
|
74
|
+
return cachedDevUsername;
|
|
75
|
+
try {
|
|
76
|
+
const res = await castleGraphql('query { me { username } }', {});
|
|
77
|
+
const me = res.data?.me;
|
|
78
|
+
cachedDevUsername = me?.username ?? null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
cachedDevUsername = null;
|
|
82
|
+
}
|
|
83
|
+
return cachedDevUsername;
|
|
84
|
+
}
|
|
85
|
+
async function devHostContext(projectDir) {
|
|
86
|
+
const ctx = readDeckContext(projectDir);
|
|
87
|
+
return {
|
|
88
|
+
deckId: ctx.deckId ?? null,
|
|
89
|
+
sessionId: null,
|
|
90
|
+
userId: config.getUserId(),
|
|
91
|
+
username: await devUsername(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function handleCastleCommand(ws, projectDir, requestId, command, params) {
|
|
95
|
+
const ctx = await devHostContext(projectDir);
|
|
96
|
+
const result = await executeCommand(ctx, command, params, castleGraphql);
|
|
97
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
98
|
+
ws.send(JSON.stringify({ type: 'castle_command_response', castleSdk: 1, requestId, ...result }));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
67
101
|
function readDeckContext(projectDir) {
|
|
68
102
|
try {
|
|
69
103
|
const raw = fs.readFileSync(path.join(projectDir, 'castle.json'), 'utf8');
|
|
@@ -407,6 +441,13 @@ function startWSServer(port, projectDir, logFile, screenshotsDir, viteHolder) {
|
|
|
407
441
|
...response,
|
|
408
442
|
}));
|
|
409
443
|
}
|
|
444
|
+
// SDK command from a locally-served deck. The dev server is the host:
|
|
445
|
+
// it runs the command via the SDK host executor with the CLI's login
|
|
446
|
+
// token, then replies to just this socket. See castle-web-sdk's
|
|
447
|
+
// transport.ts (local channel) + host.ts.
|
|
448
|
+
if (msg.type === 'castle_command' && typeof msg.requestId === 'string') {
|
|
449
|
+
void handleCastleCommand(ws, projectDir, msg.requestId, msg.command, msg.params);
|
|
450
|
+
}
|
|
410
451
|
}
|
|
411
452
|
catch {
|
|
412
453
|
// ignore malformed frames
|
package/kits/basic-2d/CLAUDE.md
CHANGED
|
@@ -65,7 +65,7 @@ A fresh `Behavior` instance is constructed per actor per frame from the actor's
|
|
|
65
65
|
"id": "paddle",
|
|
66
66
|
"components": {
|
|
67
67
|
"Layout": { "x": 210, "y": 640, "width": 80, "height": 12 },
|
|
68
|
-
"Drawing": { "file": "drawings/
|
|
68
|
+
"Drawing": { "file": "drawings/paddle.drawing" },
|
|
69
69
|
"Collider": { "kind": "solid", "width": 80, "height": 12 },
|
|
70
70
|
"Paddle": { "speed": 360 }
|
|
71
71
|
}
|
|
@@ -79,8 +79,9 @@ Rules: every actor needs a unique `id` (any string) and almost always a `Layout`
|
|
|
79
79
|
## Built-in behaviors
|
|
80
80
|
|
|
81
81
|
- **Layout** — `{ x, y, width, height, z?, rotation? }`. Every actor needs one. `z` orders draw (low first). `rotation` is degrees about the center.
|
|
82
|
-
- **Drawing** — `{ file: "drawings/foo.drawing", tint?: "#rrggbbaa" }`. Renders the pixel-art file scaled into the Layout box. `tint` multiplies; use white (`#ffffffff`) or omit for the original colors. For a solid-color rectangle, point `file` at `drawings/
|
|
83
|
-
- **
|
|
82
|
+
- **Drawing** — `{ file: "drawings/foo.drawing", tint?: "#rrggbbaa" }`. Renders the pixel-art file scaled into the Layout box. `tint` multiplies; use white (`#ffffffff`) or omit for the original colors. For a solid-color rectangle, point `file` at `drawings/default.drawing` (an 8×8 white block) and set `tint` to your color.
|
|
83
|
+
- **Missing drawings fall back to a block.** If `file` names a drawing that doesn't exist yet, it renders as `drawings/default.drawing` (a plain white block) — so you can give an actor its REAL intended drawing name (`drawings/paddle.drawing`) right away and it shows a block until that file is created. Reference real names from the start; don't wait for the art.
|
|
84
|
+
- **Don't distort pixel art.** Keep an actor's Layout aspect ratio close to its drawing's native aspect ratio, and don't scale a sprite way up — stretching a small sprite into a long/tall actor wrecks the pixels (a brick sprite stretched into a wall ruins the bricks). For long surfaces (walls, floors, platforms), make a drawing proportioned to the surface (or tile several actors) instead of stretching one. `default.drawing` is the exception — it's a flat solid fill, so any size/tint is fine.
|
|
84
85
|
- **Collider** — `{ kind: 'solid'|'pickup', width, height, offsetX?, offsetY?, debug? }`. Just a rect; the framework does NOT auto-resolve collisions for you. Use it as data:
|
|
85
86
|
|
|
86
87
|
```jsx
|
|
@@ -6,7 +6,7 @@ export class Drawing {
|
|
|
6
6
|
static behaviorName = 'Drawing';
|
|
7
7
|
|
|
8
8
|
static defaultProps = {
|
|
9
|
-
file: 'drawings/
|
|
9
|
+
file: 'drawings/default.drawing',
|
|
10
10
|
tint: '#ffffffff',
|
|
11
11
|
};
|
|
12
12
|
|
|
@@ -18,7 +18,10 @@ export class Drawing {
|
|
|
18
18
|
if (actor.runtime?.collected) return;
|
|
19
19
|
const layout = actor.components.Layout;
|
|
20
20
|
if (!layout) return;
|
|
21
|
-
|
|
21
|
+
// Fall back to default.drawing (a plain block) when the named file does not
|
|
22
|
+
// exist yet -- so a scene can reference drawings/foo.drawing before the
|
|
23
|
+
// drawing task has created it, and it shows a block until the real art lands.
|
|
24
|
+
const drawing = scene.drawings[this.props.file] ?? scene.drawings['drawings/default.drawing'];
|
|
22
25
|
if (!drawing) return;
|
|
23
26
|
drawPixelDrawing(
|
|
24
27
|
ctx,
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "castle-web-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.60",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"castle-web": "./dist/index.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc && rm -rf kits && cp -R ../kits kits",
|
|
9
|
+
"build": "tsc && cp -R src/castle-host dist/castle-host && rm -rf kits && cp -R ../kits kits",
|
|
10
10
|
"dev": "tsc --watch",
|
|
11
11
|
"check": "eslint . && jscpd && tsc --noEmit"
|
|
12
12
|
},
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"path": [
|
|
15
15
|
"src"
|
|
16
16
|
],
|
|
17
|
+
"ignore": [
|
|
18
|
+
"src/castle-host/**"
|
|
19
|
+
],
|
|
17
20
|
"threshold": 0,
|
|
18
21
|
"reporters": [
|
|
19
22
|
"consoleFull"
|