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.
- package/dist/agent.js +141 -22
- package/dist/api.d.ts +8 -0
- package/dist/api.js +10 -0
- package/dist/bundle.js +2 -2
- package/dist/get-deck.d.ts +1 -1
- package/dist/get-deck.js +93 -22
- package/dist/ide.js +123 -15
- package/dist/imports.d.ts +22 -0
- package/dist/imports.js +549 -0
- package/dist/index.js +45 -15
- package/dist/init.js +169 -1
- package/dist/install.d.ts +1 -1
- package/dist/install.js +14 -1
- package/dist/metering.d.ts +1 -0
- package/dist/metering.js +1 -1
- package/dist/native/loop.js +1 -0
- package/dist/native/openrouter.d.ts +1 -0
- package/dist/native/openrouter.js +13 -6
- package/dist/native/types.d.ts +1 -0
- package/dist/normalize.js +4 -0
- package/dist/openrouter-catalog.d.ts +3 -1
- package/dist/openrouter-catalog.js +15 -10
- package/dist/save-deck.d.ts +2 -0
- package/dist/save-deck.js +25 -19
- package/dist/serve.js +2 -2
- package/dist/shell/assets/{index-DSIr52Kl.css → index-CWNH9QiB.css} +1 -1
- package/dist/shell/assets/{index-BFCG4tLs.js → index-_C2BvstY.js} +21 -21
- package/dist/shell/index.html +2 -2
- package/dist/vitePlugins.d.ts +1 -0
- package/dist/vitePlugins.js +33 -0
- package/kits/basic-2d/CLAUDE.md +20 -0
- package/kits/basic-2d/behaviors/Sprite.jsx +6 -1
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +14 -8
- package/kits/basic-2d/editors/SceneEditor.jsx +39 -6
- package/kits/basic-2d/editors/behaviorRegistry.js +8 -2
- package/kits/basic-2d/engine/behaviorExtensions.js +5 -1
- package/kits/basic-2d/engine/blueprint.js +39 -3
- package/kits/basic-2d/engine/files.js +26 -5
- package/kits/basic-2d/engine/scene.js +14 -3
- package/kits/basic-2d/engine/systemRegistry.js +5 -1
- package/kits/physics-2d/behaviors/Collider.jsx +190 -153
- package/kits/physics-2d/editors/SceneEditor.jsx +42 -6
- package/kits/physics-2d/editors/SelectionOverlay.jsx +57 -28
- package/kits/physics-2d/engine/collider.js +123 -105
- package/kits/physics-2d/engine/scene.js +10 -2
- package/kits/physics-2d/physics/behaviors/RigidBody.jsx +3 -0
- package/kits/physics-2d/physics/matterBridge.js +73 -24
- package/package.json +1 -1
- package/dist/pull.d.ts +0 -4
- package/dist/pull.js +0 -119
- package/kits/physics-2d/engine/behaviorExtensions.js +0 -28
- package/kits/physics-2d/physics/extensions/collider.js +0 -15
package/dist/shell/index.html
CHANGED
|
@@ -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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-
|
|
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>
|
package/dist/vitePlugins.d.ts
CHANGED
package/dist/vitePlugins.js
CHANGED
|
@@ -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',
|
package/kits/basic-2d/CLAUDE.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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
|
-
|
|
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('
|
|
8
|
-
...import.meta.glob('
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
305
|
+
hits.push(actor);
|
|
295
306
|
}
|
|
296
307
|
}
|
|
297
|
-
return
|
|
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
|
-
|
|
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');
|