castle-web-cli 0.4.71 → 0.4.73

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.
Files changed (57) hide show
  1. package/dist/agent-prompts.js +22 -3
  2. package/dist/agent.js +731 -313
  3. package/dist/init.js +1 -1
  4. package/dist/shell/assets/index-Dfn29Bkt.js +108 -0
  5. package/dist/shell/assets/{index-CVEnWuGV.css → index-WNbOHPBj.css} +1 -1
  6. package/dist/shell/index.html +2 -2
  7. package/dist/vitePlugins.js +3 -2
  8. package/kits/basic-2d/CLAUDE.md +29 -8
  9. package/kits/basic-2d/behaviors/Collider.jsx +6 -4
  10. package/kits/basic-2d/behaviors/Layout.jsx +2 -2
  11. package/kits/basic-2d/behaviors/Sprite.jsx +210 -0
  12. package/kits/basic-2d/behaviors/tint.js +47 -0
  13. package/kits/basic-2d/docs/pxart-format.md +298 -0
  14. package/kits/basic-2d/drawings/pig.pxart +59 -0
  15. package/kits/basic-2d/editors/App.jsx +125 -76
  16. package/kits/basic-2d/editors/CodeEditor.jsx +9 -45
  17. package/kits/basic-2d/editors/FileBrowser.jsx +234 -47
  18. package/kits/basic-2d/editors/PlayOnly.jsx +9 -7
  19. package/kits/basic-2d/editors/PxArtEditor.jsx +662 -0
  20. package/kits/basic-2d/editors/SceneEditor.jsx +587 -221
  21. package/kits/basic-2d/editors/SelectionOverlay.jsx +808 -0
  22. package/kits/basic-2d/editors/SingleEditor.jsx +38 -20
  23. package/kits/basic-2d/editors/codeTheme.js +135 -0
  24. package/kits/basic-2d/editors/editorHistory.js +44 -17
  25. package/kits/basic-2d/editors/inspectorSheet.js +23 -0
  26. package/kits/basic-2d/editors/pixelCanvas.js +11 -0
  27. package/kits/basic-2d/editors/pixelEditorChrome.jsx +55 -0
  28. package/kits/basic-2d/editors/pixelGeometry.js +45 -0
  29. package/kits/basic-2d/editors/pixelInspector.jsx +416 -0
  30. package/kits/basic-2d/editors/pxArtEditorModel.js +718 -0
  31. package/kits/basic-2d/editors/pxArtPlayback.js +92 -0
  32. package/kits/basic-2d/editors/pxArtTimeline.jsx +752 -0
  33. package/kits/basic-2d/editors/pxArtTimeline.module.css +506 -0
  34. package/kits/basic-2d/editors/pxArtTools.js +124 -0
  35. package/kits/basic-2d/editors/useArtboardFit.js +102 -0
  36. package/kits/basic-2d/engine/ScenePlayer.jsx +10 -4
  37. package/kits/basic-2d/engine/SceneUI.jsx +3 -11
  38. package/kits/basic-2d/engine/assets.js +15 -0
  39. package/kits/basic-2d/engine/files.js +57 -2
  40. package/kits/basic-2d/engine/pxart.js +985 -0
  41. package/kits/basic-2d/engine/scene.js +222 -41
  42. package/kits/basic-2d/engine/ui.jsx +155 -26
  43. package/kits/basic-2d/engine/ui.module.css +1280 -344
  44. package/kits/basic-2d/eslint.config.js +21 -0
  45. package/kits/basic-2d/index.html +13 -0
  46. package/kits/basic-2d/package.json +1 -0
  47. package/kits/basic-2d/pnpm-lock.yaml +5 -5
  48. package/kits/basic-2d/scenes/main.scene +19 -26
  49. package/kits/basic-2d/scripts/draw.mjs +121 -0
  50. package/kits/basic-3d/editors/PlayOnly.jsx +9 -1
  51. package/kits/basic-3d/engine/ScenePlayer.jsx +7 -1
  52. package/package.json +1 -1
  53. package/dist/shell/assets/index-BY21Og40.js +0 -106
  54. package/kits/basic-2d/behaviors/Drawing.jsx +0 -142
  55. package/kits/basic-2d/drawings/block.drawing +0 -70
  56. package/kits/basic-2d/drawings/default.drawing +0 -70
  57. package/kits/basic-2d/editors/DrawingEditor.jsx +0 -224
@@ -6,6 +6,27 @@ export default [
6
6
  ignores: ['node_modules/**', 'dist/**', '.castle/**', '*.config.js'],
7
7
  },
8
8
  js.configs.recommended,
9
+ {
10
+ // Node-side kit tooling (e.g. the `npm run draw` art generator). Runs under
11
+ // Node, not the browser, so it gets Node globals.
12
+ files: ['scripts/**/*.{js,mjs}'],
13
+ languageOptions: {
14
+ ecmaVersion: 'latest',
15
+ sourceType: 'module',
16
+ globals: {
17
+ process: 'readonly',
18
+ Buffer: 'readonly',
19
+ console: 'readonly',
20
+ URL: 'readonly',
21
+ setTimeout: 'readonly',
22
+ clearTimeout: 'readonly',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-undef': 'off',
27
+ 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
28
+ },
29
+ },
9
30
  {
10
31
  files: ['engine/**/*.{js,jsx}', 'editors/**/*.{js,jsx}', 'behaviors/**/*.{js,jsx}', '*.js', '*.jsx'],
11
32
  languageOptions: {
@@ -2,7 +2,20 @@
2
2
  <html>
3
3
  <head>
4
4
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
5
+ <meta name="color-scheme" content="dark" />
5
6
  <title>Castle Basic 2D JS Kit</title>
7
+ <!-- Declare the dark scheme + opaque dark background statically, before the
8
+ JS-imported CSS module loads. Otherwise this frame paints with the
9
+ default (light) scheme first and only switches to dark once the module
10
+ applies -- and Safari fails to repaint a composited iframe on a
11
+ color-scheme change (WebKit bug 309097), leaving static panels like the
12
+ file list blank until a forced repaint (e.g. opening devtools). -->
13
+ <style>
14
+ html {
15
+ color-scheme: dark;
16
+ background: #0a0a0a;
17
+ }
18
+ </style>
6
19
  </head>
7
20
  <body>
8
21
  <div id="root"></div>
@@ -3,6 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "scripts": {
6
+ "draw": "node scripts/draw.mjs",
6
7
  "restart": "node ../../cli/dist/index.js restart .",
7
8
  "screenshot": "node ../../cli/dist/index.js screenshot .",
8
9
  "check": "eslint . && jscpd && node --input-type=module -e \"const { bundleProject } = await import('../../cli/dist/bundle.js'); await bundleProject('.');\""
@@ -30,8 +30,8 @@ importers:
30
30
  specifier: ^1.2.3
31
31
  version: 1.2.3
32
32
  castle-web-sdk:
33
- specifier: ^0.4.6
34
- version: 0.4.6
33
+ specifier: file:../../sdk
34
+ version: file:../../sdk
35
35
  codemirror:
36
36
  specifier: ^6.0.2
37
37
  version: 6.0.2
@@ -291,8 +291,8 @@ packages:
291
291
  resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
292
292
  engines: {node: '>=6'}
293
293
 
294
- castle-web-sdk@0.4.6:
295
- resolution: {integrity: sha512-b0su+RXWYh1/A4TTbKDt3PeZ4Is5WG/vSbQ0eTEm2UpNWffmJu6k4xQT/WUCBXju3L8azaRRAG1sQsFJ7f74jA==}
294
+ castle-web-sdk@file:../../sdk:
295
+ resolution: {directory: ../../sdk, type: directory}
296
296
 
297
297
  chalk@4.1.2:
298
298
  resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -1149,7 +1149,7 @@ snapshots:
1149
1149
 
1150
1150
  callsites@3.1.0: {}
1151
1151
 
1152
- castle-web-sdk@0.4.6: {}
1152
+ castle-web-sdk@file:../../sdk: {}
1153
1153
 
1154
1154
  chalk@4.1.2:
1155
1155
  dependencies:
@@ -1,33 +1,26 @@
1
1
  {
2
- "background": "#1b2030",
2
+ "background": "#00396D",
3
3
  "actors": [
4
4
  {
5
- "id": "block_tl",
5
+ "id": "pig",
6
6
  "components": {
7
- "Layout": { "x": 150, "y": 250, "width": 80, "height": 80 },
8
- "Drawing": { "file": "drawings/block.drawing", "tint": "#ff6b6bff" }
9
- }
10
- },
11
- {
12
- "id": "block_tr",
13
- "components": {
14
- "Layout": { "x": 270, "y": 250, "width": 80, "height": 80 },
15
- "Drawing": { "file": "drawings/block.drawing", "tint": "#ffd166ff" }
16
- }
17
- },
18
- {
19
- "id": "block_bl",
20
- "components": {
21
- "Layout": { "x": 150, "y": 370, "width": 80, "height": 80 },
22
- "Drawing": { "file": "drawings/block.drawing", "tint": "#06d6a0ff" }
23
- }
24
- },
25
- {
26
- "id": "block_br",
27
- "components": {
28
- "Layout": { "x": 270, "y": 370, "width": 80, "height": 80 },
29
- "Drawing": { "file": "drawings/block.drawing", "tint": "#4dabf7ff" }
7
+ "Layout": {
8
+ "x": 210,
9
+ "y": 310,
10
+ "width": 80,
11
+ "height": 80,
12
+ "z": 1
13
+ },
14
+ "Sprite": {
15
+ "file": "drawings/pig.pxart"
16
+ },
17
+ "Collider": {
18
+ "kind": "solid",
19
+ "width": 70,
20
+ "height": 70
21
+ }
30
22
  }
31
23
  }
32
- ]
24
+ ],
25
+ "name": "Main"
33
26
  }
@@ -0,0 +1,121 @@
1
+ // `npm run draw -- <name>` — turn a terse svg-rect emission into a sprite.
2
+ //
3
+ // Reads an <svg> with one <rect> per pixel from stdin (or `--from <file>`),
4
+ // decodes + palette-quantizes it onto the agent 16-color subset, and writes a
5
+ // single-frame `drawings/<name>.pxart` in the deck (the script's cwd).
6
+ //
7
+ // This is a kit-layer concern: the `.pxart` format lives entirely in the kit
8
+ // (engine/pxart.js); the harness has no knowledge of it.
9
+
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import {
13
+ svgRectToPxArt,
14
+ serializeCompact,
15
+ AGENT_PALETTE_16,
16
+ DEFAULT_RESOLUTION,
17
+ } from '../engine/pxart.js';
18
+
19
+ // Read all of stdin as a UTF-8 string. Resolves '' if stdin is a TTY with no
20
+ // piped input so we can give a clear "no input" error instead of hanging.
21
+ function readStdin() {
22
+ return new Promise((resolve, reject) => {
23
+ if (process.stdin.isTTY) {
24
+ resolve('');
25
+ return;
26
+ }
27
+ const chunks = [];
28
+ process.stdin.on('data', (c) => chunks.push(c));
29
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
30
+ process.stdin.on('error', reject);
31
+ });
32
+ }
33
+
34
+ // Normalize a requested sprite name into a relative `drawings/<...>.pxart`
35
+ // path. Tolerates a leading `drawings/` and/or a trailing `.pxart`/`.px.json`
36
+ // so `draw ship`, `draw ship.pxart`, and `draw drawings/ship.pxart` all land in
37
+ // the same place.
38
+ function resolveSpritePath(dir, name) {
39
+ let rel = name.trim().replace(/\\/g, '/');
40
+ rel = rel.replace(/^\.?\/*/, '');
41
+ if (rel.startsWith('drawings/')) rel = rel.slice('drawings/'.length);
42
+ rel = rel.replace(/\.px\.json$/i, '').replace(/\.pxart$/i, '');
43
+ if (!rel) return null;
44
+
45
+ const drawingsDir = path.resolve(dir, 'drawings');
46
+ const filepath = path.resolve(drawingsDir, `${rel}.pxart`);
47
+ const within = path.relative(drawingsDir, filepath);
48
+ if (within.startsWith('..') || path.isAbsolute(within)) return null;
49
+ return filepath;
50
+ }
51
+
52
+ function getFlagValue(flag) {
53
+ const idx = process.argv.indexOf(flag);
54
+ return idx >= 0 ? process.argv[idx + 1] : undefined;
55
+ }
56
+
57
+ // All positional (non-flag) args after the script, in order.
58
+ function positionals() {
59
+ const args = process.argv.slice(2);
60
+ const withValues = new Set(['--from']);
61
+ const out = [];
62
+ for (let i = 0; i < args.length; i++) {
63
+ if (args[i].startsWith('--')) {
64
+ if (withValues.has(args[i])) i++;
65
+ continue;
66
+ }
67
+ out.push(args[i]);
68
+ }
69
+ return out;
70
+ }
71
+
72
+ async function main() {
73
+ const name = positionals()[0];
74
+ if (!name) {
75
+ console.error('Usage: npm run draw -- <name> [--from FILE] (svg-rect on stdin -> drawings/<name>.pxart)');
76
+ process.exit(1);
77
+ }
78
+
79
+ // The script's cwd is the deck; sprites land under <deck>/drawings/.
80
+ const dir = process.cwd();
81
+ const filepath = resolveSpritePath(dir, name);
82
+ if (!filepath) {
83
+ console.error(`Invalid sprite name: ${name}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ const fromFile = getFlagValue('--from');
88
+ let svg;
89
+ if (fromFile) {
90
+ try {
91
+ svg = fs.readFileSync(path.resolve(fromFile), 'utf-8');
92
+ } catch (e) {
93
+ console.error(`Could not read --from file ${fromFile}: ${e instanceof Error ? e.message : String(e)}`);
94
+ process.exit(1);
95
+ }
96
+ } else {
97
+ svg = await readStdin();
98
+ }
99
+
100
+ if (!svg.trim()) {
101
+ console.error('No svg-rect input. Pipe an <svg> with <rect> cells via stdin or pass --from <file>.');
102
+ process.exit(1);
103
+ }
104
+
105
+ const art = svgRectToPxArt(svg, { resolution: DEFAULT_RESOLUTION, palette: AGENT_PALETTE_16 });
106
+ if (!art) {
107
+ console.error('Could not decode any pixels from the svg-rect input (no usable <rect fill="...">). Emit one <rect> per filled pixel on a 16x16 viewBox.');
108
+ process.exit(1);
109
+ }
110
+
111
+ fs.mkdirSync(path.dirname(filepath), { recursive: true });
112
+ fs.writeFileSync(filepath, serializeCompact(art), 'utf-8');
113
+
114
+ const rel = path.relative(path.resolve(dir), filepath);
115
+ console.log(`Wrote ${rel}`);
116
+ }
117
+
118
+ main().catch((e) => {
119
+ console.error(e instanceof Error ? e.message : String(e));
120
+ process.exit(1);
121
+ });
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { Lifecycle } from 'castle-web-sdk';
2
3
  import { initialFiles, parseJsonFile, parseModels } from '../engine/files';
3
4
  import { ScenePlayer } from '../engine/ScenePlayer';
4
5
  import { behaviorClasses } from './behaviorRegistry';
@@ -10,5 +11,12 @@ export function PlayOnly() {
10
11
  const { value: sceneData } = parseJsonFile('scenes/main.scene', sceneText);
11
12
  if (!sceneData) return null;
12
13
  const models = parseModels(initialFiles);
13
- return <ScenePlayer sceneData={sceneData} models={models} behaviorClasses={behaviorClasses} />;
14
+ return (
15
+ <ScenePlayer
16
+ sceneData={sceneData}
17
+ models={models}
18
+ behaviorClasses={behaviorClasses}
19
+ onFirstFrame={Lifecycle.ready}
20
+ />
21
+ );
14
22
  }
@@ -9,9 +9,10 @@ import { TouchControls } from './TouchControls';
9
9
  // wire keyboard / pointer input, run the update+sync+render loop, and render
10
10
  // the behavior-driven UI overlay. No game logic lives here -- behaviors and
11
11
  // scenes are the place for that.
12
- export function ScenePlayer({ sceneData, models, behaviorClasses }) {
12
+ export function ScenePlayer({ sceneData, models, behaviorClasses, onFirstFrame }) {
13
13
  const runtimeRef = useRef(null);
14
14
  const cameraRef = useRef(null);
15
+ const firstFrameRef = useRef(false);
15
16
  const getKeys = useCallback(() => runtimeRef.current?.keys ?? null, []);
16
17
  const getRuntime = useCallback(() => runtimeRef.current, []);
17
18
  const pointerHandlers = useMemo(() => makePlayPointerHandlers(getRuntime), [getRuntime]);
@@ -39,6 +40,11 @@ export function ScenePlayer({ sceneData, models, behaviorClasses }) {
39
40
  applyCameraSpec(camera, runtime.camera ?? defaultCameraSpec);
40
41
  runtime.activeCamera = camera;
41
42
  renderer.render(runtime.three, camera);
43
+ if (!firstFrameRef.current) {
44
+ firstFrameRef.current = true;
45
+ // Wait one frame so the draw composites before reveal, avoiding a blank flash.
46
+ requestAnimationFrame(() => onFirstFrame?.());
47
+ }
42
48
  };
43
49
  return (
44
50
  <div style={{ position: 'fixed', inset: 0, background: '#000' }}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.71",
3
+ "version": "0.4.73",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"