castle-web-cli 0.4.91 → 0.4.93

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 (52) hide show
  1. package/dist/agent.js +141 -22
  2. package/dist/api.d.ts +8 -0
  3. package/dist/api.js +10 -0
  4. package/dist/bundle.js +2 -2
  5. package/dist/get-deck.d.ts +1 -1
  6. package/dist/get-deck.js +93 -22
  7. package/dist/ide.js +123 -15
  8. package/dist/imports.d.ts +22 -0
  9. package/dist/imports.js +549 -0
  10. package/dist/index.js +45 -15
  11. package/dist/init.js +169 -1
  12. package/dist/install.d.ts +1 -1
  13. package/dist/install.js +14 -1
  14. package/dist/metering.d.ts +1 -0
  15. package/dist/metering.js +1 -1
  16. package/dist/native/loop.js +1 -0
  17. package/dist/native/openrouter.d.ts +1 -0
  18. package/dist/native/openrouter.js +13 -6
  19. package/dist/native/types.d.ts +1 -0
  20. package/dist/normalize.js +4 -0
  21. package/dist/openrouter-catalog.d.ts +3 -1
  22. package/dist/openrouter-catalog.js +15 -10
  23. package/dist/save-deck.d.ts +2 -0
  24. package/dist/save-deck.js +25 -19
  25. package/dist/serve.js +2 -2
  26. package/dist/shell/assets/{index-DSIr52Kl.css → index-CWNH9QiB.css} +1 -1
  27. package/dist/shell/assets/{index-BFCG4tLs.js → index-_C2BvstY.js} +21 -21
  28. package/dist/shell/index.html +2 -2
  29. package/dist/vitePlugins.d.ts +1 -0
  30. package/dist/vitePlugins.js +33 -0
  31. package/kits/basic-2d/CLAUDE.md +20 -0
  32. package/kits/basic-2d/behaviors/Sprite.jsx +6 -1
  33. package/kits/basic-2d/editors/BlueprintLibrary.jsx +14 -8
  34. package/kits/basic-2d/editors/SceneEditor.jsx +39 -6
  35. package/kits/basic-2d/editors/behaviorRegistry.js +8 -2
  36. package/kits/basic-2d/engine/behaviorExtensions.js +5 -1
  37. package/kits/basic-2d/engine/blueprint.js +39 -3
  38. package/kits/basic-2d/engine/files.js +26 -5
  39. package/kits/basic-2d/engine/scene.js +14 -3
  40. package/kits/basic-2d/engine/systemRegistry.js +5 -1
  41. package/kits/physics-2d/behaviors/Collider.jsx +190 -153
  42. package/kits/physics-2d/editors/SceneEditor.jsx +42 -6
  43. package/kits/physics-2d/editors/SelectionOverlay.jsx +57 -28
  44. package/kits/physics-2d/engine/collider.js +123 -105
  45. package/kits/physics-2d/engine/scene.js +10 -2
  46. package/kits/physics-2d/physics/behaviors/RigidBody.jsx +3 -0
  47. package/kits/physics-2d/physics/matterBridge.js +73 -24
  48. package/package.json +1 -1
  49. package/dist/pull.d.ts +0 -4
  50. package/dist/pull.js +0 -119
  51. package/kits/physics-2d/engine/behaviorExtensions.js +0 -28
  52. package/kits/physics-2d/physics/extensions/collider.js +0 -15
@@ -4,8 +4,8 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Castle Editor</title>
7
- <script type="module" crossorigin src="/__castle/ide/assets/index-BFCG4tLs.js"></script>
8
- <link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-DSIr52Kl.css">
7
+ <script type="module" crossorigin src="/__castle/ide/assets/index-_C2BvstY.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-CWNH9QiB.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -1,2 +1,3 @@
1
1
  import type { Plugin } from 'vite';
2
+ export declare function importsAliasPlugin(): Plugin;
2
3
  export declare function sceneFilesPlugin(): Plugin;
@@ -1,4 +1,11 @@
1
+ import * as path from 'path';
1
2
  import * as fs from 'fs';
3
+ import { IMPORTS_DIR } from './imports.js';
4
+ // Mirrors castle-web-sdk's IMPORTS_PREFIX, which is the definition deck code
5
+ // uses. Duplicated rather than imported: the CLI compiles under node16 module
6
+ // resolution and the SDK's published types don't, and one short string is not
7
+ // worth reconciling that.
8
+ const IMPORTS_PREFIX = '@imports/';
2
9
  // Safety net for `.scene` / `.drawing` / `.pxart` files imported as JS modules.
3
10
  //
4
11
  // These files are DATA (JSON), not modules. The blessed way to read them is the
@@ -11,6 +18,32 @@ import * as fs from 'fs';
11
18
  // `?raw` (and any other query suffix) is left untouched so the engine's
12
19
  // `import.meta.glob(..., { query: '?raw' })` still gets raw text.
13
20
  const DATA_FILE_EXTS = ['.scene', '.drawing', '.pxart'];
21
+ // `@imports/<alias>/...` is how a deck names a file of another deck it imports,
22
+ // in JS as in everything else (see castle-web-sdk's resolveDeckFile). Imports are
23
+ // flattened into one `imports/` at the deck root, so the alias resolves there --
24
+ // and means the same thing from any file at any depth, which a relative path
25
+ // could not: a dependency's `./imports/x` is right while that dependency is the
26
+ // deck being served and one level too deep once it is itself an import.
27
+ //
28
+ // Registered for the dev server AND the bundler. A specifier that resolves in
29
+ // one and not the other is a deck that plays in the editor and breaks when it
30
+ // ships.
31
+ export function importsAliasPlugin() {
32
+ return {
33
+ name: 'castle-imports-alias',
34
+ enforce: 'pre',
35
+ async resolveId(source, importer, options) {
36
+ if (!source.startsWith(IMPORTS_PREFIX))
37
+ return null;
38
+ const root = this.environment?.config?.root ?? process.cwd();
39
+ const target = path.join(root, IMPORTS_DIR, source.slice(IMPORTS_PREFIX.length));
40
+ // Hand the rewritten path back to vite rather than returning it resolved:
41
+ // extensions, index files and the rest of normal resolution still have to
42
+ // happen, and duplicating that here would only get it subtly wrong.
43
+ return (await this.resolve(target, importer, { ...options, skipSelf: true })) ?? target;
44
+ },
45
+ };
46
+ }
14
47
  export function sceneFilesPlugin() {
15
48
  return {
16
49
  name: 'castle-scene-files',
@@ -19,8 +19,28 @@ Do you already know what you want to make, or do you want to figure it out toget
19
19
  - After any code, scene, or drawing edit, run `npm run restart`.
20
20
  - Space is reserved by the editor for play/stop; do not bind Space to gameplay.
21
21
  - Do not read `engine/`, `editors/`, or built-in behaviors (`Layout.jsx`, `Sprite.jsx`, `Collider.jsx`, `Camera.jsx`) to build a game. Their public API is documented below.
22
+ - Naming files: a plain path (`drawings/ship.pxart`) is a file of THIS deck; `@imports/<alias>/...` is a file of a deck this one imports. Same in JS (`import x from '@imports/someone.pack/thing.js'`), in scene/blueprint refs, and in `Sprite.file`. See `## Files and imports`.
22
23
  - Details below: `## Behavior shape`, `## Scene file`, `## Blueprints`, `## Built-in behaviors`, `## Creating pixel art`, `## SceneRuntime API`, and `## Input shortcuts`.
23
24
 
25
+ ## Files and imports
26
+
27
+ One rule wherever a file is named -- JS imports, `"blueprint"` refs, `Sprite.file`,
28
+ anything a behavior invents:
29
+
30
+ - `drawings/ship.pxart` -- a file of the deck the reference is WRITTEN IN. In this
31
+ deck's own files that means this deck; in a file belonging to an import, that
32
+ import. So a kit's blueprint saying `drawings/cauldron.pxart` keeps meaning the
33
+ kit's drawing once the kit is imported by someone else.
34
+ - `@imports/<alias>/drawings/ship.pxart` -- a file of the deck imported under
35
+ `<alias>`. This is the only way to name another deck's file, so cross-deck
36
+ references are visible as such, and it means the same thing from any file at
37
+ any depth.
38
+
39
+ Imports are read-only: their files belong to the deck they came from. Use them,
40
+ don't edit them. `castle-web add-import <deckId>` adds one, `update-import`
41
+ re-fetches it. `resolveDeckFile` from `castle-web-sdk` is the rule itself, if a
42
+ behavior needs to resolve a path it was handed.
43
+
24
44
  ## Scope
25
45
 
26
46
  Write the smallest game that satisfies what the user asked for. No sound, particles, menus, multi-level progression, or visual polish unless they specifically asked for it. A typical behavior is 30–80 lines — if yours is hitting 200, you're over-engineering: cut feel-good extras, fewer fields on props, fewer edge cases, fewer comments. Ship the core loop first; the user can ask for more.
@@ -1,3 +1,4 @@
1
+ import { resolveDeckFile } from 'castle-web-sdk';
1
2
  import React from 'react';
2
3
  import { frameCount, renderSpriteFrame } from '../engine/pxart';
3
4
  import { renderSmoothSpriteFrame } from '../engine/pxartSmooth';
@@ -37,7 +38,11 @@ export class Sprite {
37
38
  }
38
39
 
39
40
  resolveSprite(scene) {
40
- return scene.sprites?.[this.props.file] ?? scene.sprites?.[FALLBACK_FILE] ?? null;
41
+ // `drawings/x.pxart` is this deck's; `@imports/<alias>/drawings/x.pxart` is
42
+ // an import's. (A value inherited from an imported blueprint arrives already
43
+ // resolved against that blueprint's deck -- see engine/blueprint.js.)
44
+ const file = resolveDeckFile(this.props.file);
45
+ return scene.sprites?.[file] ?? scene.sprites?.[FALLBACK_FILE] ?? null;
41
46
  }
42
47
 
43
48
  update(actor, scene, dt) {
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
2
  import { renderSpriteFrame } from '../engine/pxart';
3
3
  import { screenToCard } from '../engine/scene';
4
- import { countBlueprintInstances, listBlueprints } from '../engine/blueprint';
4
+ import { countBlueprintInstances, isImportedPath, listBlueprints } from '../engine/blueprint';
5
5
  import { ConfirmDialog, ContextMenu, cx, Icon, styles } from '../engine/ui';
6
6
 
7
7
  const DRAG_THRESHOLD = 4;
@@ -157,13 +157,19 @@ export function BlueprintLibrary({
157
157
  x={contextMenu.x}
158
158
  y={contextMenu.y}
159
159
  onClose={() => setContextMenu(null)}
160
- items={[
161
- {
162
- key: 'delete',
163
- label: `Delete ${contextMenu.blueprint.name}...`,
164
- onClick: () => setConfirmDelete(contextMenu.blueprint),
165
- },
166
- ]}
160
+ items={
161
+ // An imported blueprint belongs to the deck it came from: placeable
162
+ // here, but there is nothing this deck can delete.
163
+ isImportedPath(contextMenu.blueprint.path)
164
+ ? [{ key: 'imported', label: 'From an import (read-only)', disabled: true }]
165
+ : [
166
+ {
167
+ key: 'delete',
168
+ label: `Delete ${contextMenu.blueprint.name}...`,
169
+ onClick: () => setConfirmDelete(contextMenu.blueprint),
170
+ },
171
+ ]
172
+ }
167
173
  />
168
174
  ) : null}
169
175
  {confirmDelete ? (
@@ -797,7 +797,9 @@ function useSelectionGesture(args) {
797
797
  const point = { x: raw.x + cam.x, y: raw.y + cam.y };
798
798
  current.canvasRef.current.setPointerCapture(event.pointerId);
799
799
  const scene = makeScene(current.sceneData, behaviorClasses, current.sprites, current.files);
800
- const actor = scene.actorAt(point.x, point.y);
800
+ const stack = scene.actorsAt(point.x, point.y);
801
+ const stackIds = stack.map((a) => a.id);
802
+ const actor = stack[0] ?? null;
801
803
  const drag = {
802
804
  pointerId: event.pointerId,
803
805
  startPoint: point,
@@ -820,7 +822,7 @@ function useSelectionGesture(args) {
820
822
  } else if (current.multiSelectMode) {
821
823
  handleModePointerDown(drag, actor, current);
822
824
  } else {
823
- handleDefaultPointerDown(drag, actor, point, current);
825
+ handleDefaultPointerDown(drag, actor, point, current, stackIds);
824
826
  }
825
827
  drag.moveStarts = collectMoveStarts(current.sceneData, drag.movingActorIds);
826
828
  dragRef.current = drag;
@@ -891,6 +893,13 @@ function useSelectionGesture(args) {
891
893
  finalizeMarquee(drag, current);
892
894
  } else if (drag.kind === 'idle' && !drag.movedFar && !drag.longPressFired) {
893
895
  handleTap(drag, current);
896
+ } else if (
897
+ drag.kind === 'move' &&
898
+ !drag.movedFar &&
899
+ !drag.longPressFired &&
900
+ drag.cycleStack
901
+ ) {
902
+ cycleSelection(drag, current);
894
903
  }
895
904
  current.marqueeRef.current = null;
896
905
  dragRef.current = null;
@@ -1198,13 +1207,23 @@ function handleModePointerDown(drag, actor, current) {
1198
1207
  drag.pendingMarquee = true;
1199
1208
  }
1200
1209
  }
1201
- function handleDefaultPointerDown(drag, actor, point, current) {
1210
+ function handleDefaultPointerDown(drag, actor, point, current, stackIds) {
1202
1211
  if (actor) {
1203
- if (!current.selectedActorIds.includes(actor.id)) {
1212
+ const sel = current.selectedActorIds;
1213
+ if (sel.length === 1 && stackIds.length > 1 && stackIds.includes(sel[0])) {
1214
+ // Overlapping pile with a single selected actor under the cursor: keep it
1215
+ // selected so it stays draggable, and arm click-to-cycle so a stationary
1216
+ // click descends to the next actor beneath it (see cycleSelection).
1217
+ drag.movingActorIds = [...sel];
1218
+ drag.cycleStack = stackIds;
1219
+ } else if (sel.includes(actor.id)) {
1220
+ // Pressed an actor that's part of the current selection: keep the
1221
+ // selection so the whole group stays draggable.
1222
+ drag.movingActorIds = [...sel];
1223
+ } else {
1224
+ // Fresh pick: select the topmost actor under the cursor.
1204
1225
  current.onSelectActorIds([actor.id]);
1205
1226
  drag.movingActorIds = [actor.id];
1206
- } else {
1207
- drag.movingActorIds = [...current.selectedActorIds];
1208
1227
  }
1209
1228
  drag.kind = 'move';
1210
1229
  drag.longPressTimer = window.setTimeout(() => {
@@ -1235,6 +1254,20 @@ function finalizeMarquee(drag, current) {
1235
1254
  for (const id of hits) merged.add(id);
1236
1255
  current.onSelectActorIds([...merged]);
1237
1256
  }
1257
+ // A stationary click on a pile of overlapping actors advances the selection to
1258
+ // the next actor below the currently selected one, wrapping around at the
1259
+ // bottom. This lets repeated clicks in the same spot reach an actor buried under
1260
+ // others that would otherwise always win the topmost hit-test.
1261
+ function cycleSelection(drag, current) {
1262
+ const stack = drag.cycleStack;
1263
+ if (!stack || stack.length < 2) return;
1264
+ const sel = current.selectedActorIds;
1265
+ if (sel.length !== 1) return;
1266
+ const idx = stack.indexOf(sel[0]);
1267
+ if (idx === -1) return;
1268
+ const nextId = stack[(idx + 1) % stack.length];
1269
+ if (nextId !== sel[0]) current.onSelectActorIds([nextId]);
1270
+ }
1238
1271
  function handleTap(drag, current) {
1239
1272
  if (!drag.modeAtStart) return;
1240
1273
  if (drag.startedOnActorId !== null) {
@@ -3,9 +3,15 @@
3
3
  // picked up on the next reload/restart. The physics glob is what lets the
4
4
  // self-contained physics module ship its behaviors without polluting the core
5
5
  // `behaviors/` folder — a kit adopts physics by copying `physics/` in.
6
+ // Root-anchored so the kit finds the DECK's behaviors even when the kit is
7
+ // itself an import (see engine/files.js). Imports are swept first and the deck's
8
+ // own behaviors last: collectBehaviors keys by behaviorName, so a behavior the
9
+ // deck defines wins over one of the same name from a dependency.
6
10
  const modules = {
7
- ...import.meta.glob('../behaviors/*.jsx', { eager: true }),
8
- ...import.meta.glob('../physics/behaviors/*.jsx', { eager: true }),
11
+ ...import.meta.glob('/imports/*/behaviors/*.jsx', { eager: true }),
12
+ ...import.meta.glob('/imports/*/physics/behaviors/*.jsx', { eager: true }),
13
+ ...import.meta.glob('/behaviors/*.jsx', { eager: true }),
14
+ ...import.meta.glob('/physics/behaviors/*.jsx', { eager: true }),
9
15
  };
10
16
  function isBehaviorClass(value) {
11
17
  return typeof value === 'function' && typeof value.behaviorName === 'string';
@@ -14,7 +14,11 @@
14
14
  // already renders leftover defaultProps generically, so registered fields show
15
15
  // up with no inspector change. Empty in a kit with no `*/extensions/` dir (e.g.
16
16
  // basic-2d). Symmetric with engine/systemRegistry.js and editors/behaviorRegistry.js.
17
- const modules = import.meta.glob('../*/extensions/*.js', { eager: true });
17
+ // Root-anchored, deck + imports (see engine/files.js).
18
+ const modules = {
19
+ ...import.meta.glob('/imports/*/*/extensions/*.js', { eager: true }),
20
+ ...import.meta.glob('/*/extensions/*.js', { eager: true }),
21
+ };
18
22
  const extensions = Object.values(modules)
19
23
  .map((mod) => mod.behaviorExtension)
20
24
  .filter(Boolean);
@@ -6,6 +6,7 @@
6
6
  // panel propagates to every scene that places it. Shared by the runtime
7
7
  // (`scene.js`, so play mode and `PlayOnly` resolve blueprints too) and the
8
8
  // editors (which also mint/fork/migrate/cascade-delete blueprint files).
9
+ import { resolveDeckFile } from 'castle-web-sdk';
9
10
  import { formatJson } from './files';
10
11
  import { DEFAULT_RESOLUTION, TRANSPARENT, serializeCompact } from './pxart';
11
12
  import { cardSize, dedupeActorId } from './scene';
@@ -13,8 +14,19 @@ import { cardSize, dedupeActorId } from './scene';
13
14
  export const BLUEPRINTS_DIR = 'blueprints';
14
15
  export const DRAWINGS_DIR = 'drawings';
15
16
 
17
+ // A blueprint of this deck (`blueprints/x.scene`) OR of one it imports
18
+ // (`imports/<alias>/blueprints/x.scene`) -- an imported deck's blueprints are
19
+ // placeable like any other, which is most of the point of importing one.
20
+ const BLUEPRINT_PATH_RE = new RegExp(`^(imports/[^/]+/)?${BLUEPRINTS_DIR}/`);
21
+
16
22
  export function isBlueprintPath(path) {
17
- return typeof path === 'string' && path.startsWith(`${BLUEPRINTS_DIR}/`) && path.endsWith('.scene');
23
+ return typeof path === 'string' && path.endsWith('.scene') && BLUEPRINT_PATH_RE.test(path);
24
+ }
25
+
26
+ // Blueprints an import owns: placeable, but not editable or deletable from here
27
+ // (they belong to the deck they came from).
28
+ export function isImportedPath(path) {
29
+ return typeof path === 'string' && path.startsWith('imports/');
18
30
  }
19
31
 
20
32
  // Per-property inherit metadata for a behavior prop. Behaviors opt a prop out
@@ -49,12 +61,33 @@ function parseBlueprintText(text) {
49
61
  return { name, components: actor.components };
50
62
  }
51
63
 
64
+ // A reference names a file the way its own deck sees it -- `drawings/rock.pxart`
65
+ // for one of its own, `@imports/<alias>/...` for one of a deck it imports (the
66
+ // SDK's resolveDeckFile is the single definition of that rule). Templates are
67
+ // resolved against the file they were written in, so a kit's blueprint keeps
68
+ // meaning the kit's drawing after the kit is imported by somebody else.
69
+ function resolveTemplateRefs(template, files, fromPath) {
70
+ if (!template) return template;
71
+ for (const props of Object.values(template.components ?? {})) {
72
+ for (const [key, value] of Object.entries(props ?? {})) {
73
+ if (typeof value !== 'string' || !value) continue;
74
+ const resolved = resolveDeckFile(value, fromPath);
75
+ if (resolved !== value && files?.[resolved] !== undefined) props[key] = resolved;
76
+ }
77
+ }
78
+ return template;
79
+ }
80
+
52
81
  // Resolve a blueprint's template from the LIVE files map -- the load-bearing
53
82
  // call for live propagation. Returns null when the file is missing, deleted,
54
83
  // or malformed (a dangling `blueprint` ref degrades to "no template" rather
55
84
  // than throwing).
56
85
  export function getBlueprintTemplate(files, blueprintPath) {
57
- return parseBlueprintText(files?.[blueprintPath]);
86
+ // The ref may name this deck's blueprint or an import's; the template's own
87
+ // refs then resolve against whichever deck the blueprint turned out to be in.
88
+ const key = resolveDeckFile(blueprintPath);
89
+ const template = parseBlueprintText(files?.[key]);
90
+ return resolveTemplateRefs(template, files, key);
58
91
  }
59
92
 
60
93
  // Every blueprint currently in the deck, for the library/hotbar. Skips
@@ -63,7 +96,10 @@ export function listBlueprints(files) {
63
96
  const out = [];
64
97
  for (const path of Object.keys(files ?? {})) {
65
98
  if (!isBlueprintPath(path)) continue;
66
- const template = parseBlueprintText(files[path]);
99
+ // Same origin resolution getBlueprintTemplate does: an imported blueprint's
100
+ // `drawings/x.pxart` means the import's drawing, and the library previews
101
+ // these templates -- unresolved, the slot renders with no art.
102
+ const template = resolveTemplateRefs(parseBlueprintText(files[path]), files, path);
67
103
  if (!template) continue;
68
104
  out.push({ path, name: template.name, components: template.components });
69
105
  }
@@ -1,12 +1,33 @@
1
1
  // Seed the file map by scanning the deck dir. Vite module-cache is invalidated
2
2
  // on restart, so a newly-created file just shows up on the next reload.
3
- const rawModules = import.meta.glob(
4
- ['../scenes/*.scene', '../blueprints/*.scene', '../drawings/*.pxart', '../behaviors/*.jsx'],
5
- { query: '?raw', import: 'default', eager: true }
6
- );
3
+ // Anchored at the DECK ROOT (`/scenes/...`) rather than relative to this module,
4
+ // because the kit itself may be an import: living at `imports/<alias>/`, a
5
+ // `../scenes/*.scene` would find the KIT's scenes instead of the deck's. Anchored
6
+ // at the root, one pattern works whether the kit is the deck or a dependency.
7
+ //
8
+ // The first glob adds every import's content, keyed by its full path
9
+ // (`imports/<alias>/drawings/rock.pxart`) -- which is exactly how a scene or
10
+ // blueprint refers to a dependency's file, so no lookup elsewhere changes.
11
+ // (Patterns and options must be literals: import.meta.glob is a compile-time
12
+ // transform, so these can't be hoisted into shared constants.)
13
+ const rawModules = {
14
+ ...import.meta.glob(
15
+ [
16
+ '/imports/*/scenes/*.scene',
17
+ '/imports/*/blueprints/*.scene',
18
+ '/imports/*/drawings/*.pxart',
19
+ '/imports/*/behaviors/*.jsx',
20
+ ],
21
+ { query: '?raw', import: 'default', eager: true }
22
+ ),
23
+ ...import.meta.glob(
24
+ ['/scenes/*.scene', '/blueprints/*.scene', '/drawings/*.pxart', '/behaviors/*.jsx'],
25
+ { query: '?raw', import: 'default', eager: true }
26
+ ),
27
+ };
7
28
  export const initialFiles = Object.fromEntries(
8
29
  Object.entries(rawModules)
9
- .map(([globPath, text]) => [globPath.replace(/^\.\.\//, ''), text])
30
+ .map(([globPath, text]) => [globPath.replace(/^\//, ''), text])
10
31
  .sort(([a], [b]) => a.localeCompare(b))
11
32
  );
12
33
  export function getFileKind(path) {
@@ -1,3 +1,4 @@
1
+ import { resolveDeckFile } from 'castle-web-sdk';
1
2
  import { initialFiles, parseJsonFile } from './files';
2
3
  import { getBlueprintTemplate, mergeComponents } from './blueprint';
3
4
  import { getColliderRect, intersects, spriteIsEmpty } from './collider';
@@ -18,7 +19,9 @@ export const cardSize = { width: CARD_WIDTH, height: CARD_HEIGHT };
18
19
  // 'main.scene', 'scenes/main.scene', './scenes/main.scene', or 'main' -- the
19
20
  // `.scene` extension and a leading `scenes/` are both optional.
20
21
  function resolveSceneFileKey(name) {
21
- const key = String(name).replace(/^\.?\//, '');
22
+ // `@imports/<alias>/scenes/x.scene` names an import's scene; anything else is
23
+ // this deck's, with the `scenes/` folder and `.scene` suffix both optional.
24
+ const key = resolveDeckFile(String(name));
22
25
  const candidates = [key];
23
26
  if (!key.endsWith('.scene')) candidates.push(`${key}.scene`);
24
27
  for (const candidate of [...candidates]) {
@@ -281,7 +284,15 @@ export class SceneRuntime {
281
284
  }
282
285
 
283
286
  actorAt(x, y) {
287
+ return this.actorsAt(x, y)[0] ?? null;
288
+ }
289
+
290
+ // All actors whose Layout box contains the point, ordered topmost-first (high
291
+ // z -> low z). Used by the editor's click-to-cycle so repeated clicks in the
292
+ // same spot can walk down through overlapping actors.
293
+ actorsAt(x, y) {
284
294
  const actors = this.getActors().slice().reverse();
295
+ const hits = [];
285
296
  for (const actor of actors) {
286
297
  const layout = getLayout(actor);
287
298
  if (!layout) continue;
@@ -291,10 +302,10 @@ export class SceneRuntime {
291
302
  y >= layout.y &&
292
303
  y <= layout.y + layout.height
293
304
  ) {
294
- return actor;
305
+ hits.push(actor);
295
306
  }
296
307
  }
297
- return null;
308
+ return hits;
298
309
  }
299
310
 
300
311
  actorIdsInRect(rect) {
@@ -6,7 +6,11 @@
6
6
  // updates. Empty in a kit with no `systems/` dir (e.g. basic-2d); a kit adds a
7
7
  // system by dropping a file here -- no edits to the engine required. Symmetric
8
8
  // with editors/behaviorRegistry.js.
9
- const modules = import.meta.glob('../systems/*.js', { eager: true });
9
+ // Root-anchored, deck + imports (see engine/files.js).
10
+ const modules = {
11
+ ...import.meta.glob('/imports/*/systems/*.js', { eager: true }),
12
+ ...import.meta.glob('/systems/*.js', { eager: true }),
13
+ };
10
14
  export const systemInstallers = Object.values(modules)
11
15
  .map((mod) => mod.installSystem)
12
16
  .filter((fn) => typeof fn === 'function');