castle-web-cli 0.4.94 → 0.4.96
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 +24 -66
- package/dist/init.js +43 -7
- package/dist/metering.d.ts +23 -0
- package/dist/metering.js +52 -0
- package/kits/basic-2d/castle.json +23 -4
- package/kits/physics-2d/CLAUDE.md +116 -6
- package/kits/physics-2d/behaviors/Collider.jsx +76 -10
- package/kits/physics-2d/behaviors/Sprite.jsx +6 -1
- package/kits/physics-2d/castle.json +23 -4
- package/kits/physics-2d/drawings/joint-rope.pxart +26 -0
- package/kits/physics-2d/editors/SceneEditor.jsx +58 -0
- package/kits/physics-2d/editors/SelectionOverlay.jsx +48 -19
- package/kits/physics-2d/editors/behaviorRegistry.js +8 -2
- package/kits/physics-2d/engine/blueprint.js +39 -3
- package/kits/physics-2d/engine/collider.js +13 -3
- package/kits/physics-2d/engine/files.js +26 -5
- package/kits/physics-2d/engine/scene.js +9 -1
- package/kits/physics-2d/engine/systemRegistry.js +5 -1
- package/kits/physics-2d/package-lock.json +10 -3
- package/kits/physics-2d/physics/PhysicsSystem.js +112 -0
- package/kits/physics-2d/physics/behaviors/Joints.jsx +291 -0
- package/kits/physics-2d/physics/jointArt.js +57 -0
- package/kits/physics-2d/physics/joints.js +136 -0
- package/kits/physics-2d/physics/matterBridge.js +22 -2
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -3,11 +3,30 @@
|
|
|
3
3
|
"initialPanels": [
|
|
4
4
|
{
|
|
5
5
|
"width": 400,
|
|
6
|
-
"column": [
|
|
6
|
+
"column": [
|
|
7
|
+
{
|
|
8
|
+
"type": "playtest"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"type": "files"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
7
14
|
},
|
|
8
|
-
{
|
|
15
|
+
{
|
|
16
|
+
"type": "editor",
|
|
17
|
+
"file": "scenes/main.scene"
|
|
18
|
+
}
|
|
9
19
|
],
|
|
10
20
|
"hiddenPaths": [],
|
|
11
|
-
"visiblePaths": [
|
|
12
|
-
|
|
21
|
+
"visiblePaths": [
|
|
22
|
+
"drawings/**",
|
|
23
|
+
"scenes/**",
|
|
24
|
+
"blueprints/**",
|
|
25
|
+
"behaviors/**"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"deckId": "ckRZGFW4iPrx",
|
|
29
|
+
"cardId": "_oBFbAW6DxsO",
|
|
30
|
+
"title": "physics-2d",
|
|
31
|
+
"publishedVersion": "2026-07-28T17:50:39.741Z"
|
|
13
32
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "compact",
|
|
3
|
+
"palette": {
|
|
4
|
+
"a": "#8a4836",
|
|
5
|
+
"b": "#bf6f4a",
|
|
6
|
+
"c": "#e69c69"
|
|
7
|
+
},
|
|
8
|
+
"grid": [
|
|
9
|
+
"................",
|
|
10
|
+
"................",
|
|
11
|
+
"................",
|
|
12
|
+
"..cc........cc..",
|
|
13
|
+
".b..b......b..b.",
|
|
14
|
+
"a....a....a....a",
|
|
15
|
+
"......a..a......",
|
|
16
|
+
".......aa.......",
|
|
17
|
+
".......aa.......",
|
|
18
|
+
"......a..a......",
|
|
19
|
+
"a....a....a....a",
|
|
20
|
+
".b..b......b..b.",
|
|
21
|
+
"..cc........cc..",
|
|
22
|
+
"................",
|
|
23
|
+
"................",
|
|
24
|
+
"................"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -161,6 +161,27 @@ export function SceneEditor({
|
|
|
161
161
|
const editCameraRef = useRef(reloadStash?.editCamera ?? { x: 0, y: 0, zoom: 1 });
|
|
162
162
|
const selectedActorIdsRef = useRef(selectedActorIds);
|
|
163
163
|
selectedActorIdsRef.current = selectedActorIds;
|
|
164
|
+
// Transient "click an actor to pick it" mode, triggered by an inspector field
|
|
165
|
+
// (e.g. Joint's target picker). Holds `{ onPick }`; the next edit-canvas click
|
|
166
|
+
// resolves it with the clicked actor's id instead of selecting. A ref mirror
|
|
167
|
+
// lets the pointer gesture read it live without re-creating handlers.
|
|
168
|
+
const [pickRequest, setPickRequest] = useState(null);
|
|
169
|
+
const pickRequestRef = useRef(null);
|
|
170
|
+
pickRequestRef.current = pickRequest;
|
|
171
|
+
const beginPick = useCallback((onPick) => setPickRequest(onPick ? { onPick } : null), []);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!pickRequest) return undefined;
|
|
174
|
+
const onKey = (e) => e.key === 'Escape' && setPickRequest(null);
|
|
175
|
+
window.addEventListener('keydown', onKey);
|
|
176
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
177
|
+
}, [pickRequest]);
|
|
178
|
+
// Which Collider shape the inspector has selected, so the on-canvas overlay can
|
|
179
|
+
// highlight it. The Collider inspector reports its index up via onSelectShape.
|
|
180
|
+
// A ref mirror feeds the draw loop (blueprint-lit instances highlight via the
|
|
181
|
+
// canvas draw); the SelectionOverlay reads the state directly.
|
|
182
|
+
const [colliderShapeSel, setColliderShapeSel] = useState(0);
|
|
183
|
+
const colliderShapeSelRef = useRef(colliderShapeSel);
|
|
184
|
+
colliderShapeSelRef.current = colliderShapeSel;
|
|
164
185
|
const [isPlaying, setIsPlaying] = useState(reloadStash?.isPlaying ?? false);
|
|
165
186
|
// Hotbar blueprint selection (tap a slot -> inspect/edit the blueprint in
|
|
166
187
|
// the sidebar; drag remains the placement gesture). Mutually exclusive with
|
|
@@ -239,9 +260,19 @@ export function SceneEditor({
|
|
|
239
260
|
marqueeRef,
|
|
240
261
|
selectedActorIdsRef,
|
|
241
262
|
selectedBlueprintPathRef,
|
|
263
|
+
colliderShapeSelRef,
|
|
242
264
|
});
|
|
243
265
|
const panGesture = usePanGesture({ canvasRef, editCameraRef, isPlaying });
|
|
244
266
|
useScenePlayKeys(runtimeRef);
|
|
267
|
+
// Resolve an in-progress target pick: a click on the owner itself is ignored
|
|
268
|
+
// (a self-joint is meaningless); a click on empty space cancels.
|
|
269
|
+
const onPickTarget = (actorId) => {
|
|
270
|
+
const req = pickRequestRef.current;
|
|
271
|
+
if (!req) return;
|
|
272
|
+
if (actorId && actorId === selectedActorIdsRef.current[0]) return;
|
|
273
|
+
setPickRequest(null);
|
|
274
|
+
if (actorId) req.onPick(actorId);
|
|
275
|
+
};
|
|
245
276
|
const gesture = useSelectionGesture({
|
|
246
277
|
canvasRef,
|
|
247
278
|
sceneData,
|
|
@@ -254,6 +285,8 @@ export function SceneEditor({
|
|
|
254
285
|
onSetMultiSelectMode,
|
|
255
286
|
marqueeRef,
|
|
256
287
|
editCameraRef,
|
|
288
|
+
pickRequestRef,
|
|
289
|
+
onPickTarget,
|
|
257
290
|
applyScene: (next) => onChange(serialize(next)),
|
|
258
291
|
recordSceneSnapshot: history.recordSnapshot,
|
|
259
292
|
});
|
|
@@ -549,6 +582,7 @@ export function SceneEditor({
|
|
|
549
582
|
previewSceneData={previewSceneData}
|
|
550
583
|
sprites={sprites}
|
|
551
584
|
selectedActorIds={selectedActorIds}
|
|
585
|
+
selectedColliderShape={colliderShapeSel}
|
|
552
586
|
snap={snapSettings}
|
|
553
587
|
onArrange={isBlueprintFile ? undefined : actions.arrangeSelection}
|
|
554
588
|
onClone={isBlueprintFile ? undefined : actions.duplicateSelection}
|
|
@@ -594,6 +628,7 @@ export function SceneEditor({
|
|
|
594
628
|
onSetComponent={blueprintActions.setComponent}
|
|
595
629
|
onAddBehavior={blueprintActions.addBehavior}
|
|
596
630
|
onRemoveBehavior={blueprintActions.removeBehavior}
|
|
631
|
+
onSelectShape={setColliderShapeSel}
|
|
597
632
|
/>
|
|
598
633
|
) : showMulti ? (
|
|
599
634
|
<MultiSelectInspector
|
|
@@ -622,6 +657,9 @@ export function SceneEditor({
|
|
|
622
657
|
? undefined
|
|
623
658
|
: () => onSelectBlueprint(rawSelectedActor.blueprint)
|
|
624
659
|
}
|
|
660
|
+
onBeginPick={beginPick}
|
|
661
|
+
pickActive={Boolean(pickRequest)}
|
|
662
|
+
onSelectShape={setColliderShapeSel}
|
|
625
663
|
/>
|
|
626
664
|
) : (
|
|
627
665
|
<SceneInspector
|
|
@@ -800,6 +838,13 @@ function useSelectionGesture(args) {
|
|
|
800
838
|
const stack = scene.actorsAt(point.x, point.y);
|
|
801
839
|
const stackIds = stack.map((a) => a.id);
|
|
802
840
|
const actor = stack[0] ?? null;
|
|
841
|
+
// Target-pick mode swallows this click: resolve the pick with the clicked
|
|
842
|
+
// actor (or cancel on empty space) instead of selecting/dragging.
|
|
843
|
+
if (current.pickRequestRef?.current) {
|
|
844
|
+
current.canvasRef.current.releasePointerCapture?.(event.pointerId);
|
|
845
|
+
current.onPickTarget(actor?.id ?? null);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
803
848
|
const drag = {
|
|
804
849
|
pointerId: event.pointerId,
|
|
805
850
|
startPoint: point,
|
|
@@ -1339,6 +1384,7 @@ function useScenePlayLoop({
|
|
|
1339
1384
|
marqueeRef,
|
|
1340
1385
|
selectedActorIdsRef,
|
|
1341
1386
|
selectedBlueprintPathRef,
|
|
1387
|
+
colliderShapeSelRef,
|
|
1342
1388
|
}) {
|
|
1343
1389
|
// Spin up / tear down the play-mode runtime as the user toggles play. `text`
|
|
1344
1390
|
// is the stable identity for `sceneData` (which is re-parsed every render);
|
|
@@ -1398,6 +1444,9 @@ function useScenePlayLoop({
|
|
|
1398
1444
|
useCamera: true,
|
|
1399
1445
|
editPlaceholders: !isPlaying,
|
|
1400
1446
|
dimBlueprintPath: isPlaying ? null : selectedBlueprintPathRef.current,
|
|
1447
|
+
// Highlight the collider shape the blueprint inspector has selected on
|
|
1448
|
+
// its lit instances.
|
|
1449
|
+
colliderShapeSel: isPlaying ? -1 : colliderShapeSelRef?.current ?? -1,
|
|
1401
1450
|
});
|
|
1402
1451
|
}
|
|
1403
1452
|
raf = requestAnimationFrame(frame);
|
|
@@ -1415,6 +1464,7 @@ function useScenePlayLoop({
|
|
|
1415
1464
|
marqueeRef,
|
|
1416
1465
|
selectedActorIdsRef,
|
|
1417
1466
|
selectedBlueprintPathRef,
|
|
1467
|
+
colliderShapeSelRef,
|
|
1418
1468
|
]);
|
|
1419
1469
|
}
|
|
1420
1470
|
function useScenePlayKeys(runtimeRef) {
|
|
@@ -1523,6 +1573,7 @@ function BlueprintInspector({
|
|
|
1523
1573
|
onSetComponent,
|
|
1524
1574
|
onAddBehavior,
|
|
1525
1575
|
onRemoveBehavior,
|
|
1576
|
+
onSelectShape,
|
|
1526
1577
|
}) {
|
|
1527
1578
|
const templateActor = { id: '__blueprint__', components: template.components };
|
|
1528
1579
|
const instanceCount = countBlueprintInstances(files, blueprintPath);
|
|
@@ -1552,6 +1603,7 @@ function BlueprintInspector({
|
|
|
1552
1603
|
onSetComponent={(actorId, behaviorName, nextProps) => onSetComponent(behaviorName, nextProps)}
|
|
1553
1604
|
onAddBehavior={(actorId, behaviorName) => onAddBehavior(behaviorName)}
|
|
1554
1605
|
onRemoveBehavior={(actorId, behaviorName) => onRemoveBehavior(behaviorName)}
|
|
1606
|
+
onSelectShape={onSelectShape}
|
|
1555
1607
|
/>
|
|
1556
1608
|
</>
|
|
1557
1609
|
);
|
|
@@ -1593,6 +1645,9 @@ function ActorInspector({
|
|
|
1593
1645
|
onRemoveBehavior,
|
|
1594
1646
|
onFork,
|
|
1595
1647
|
onEditBlueprint,
|
|
1648
|
+
onBeginPick,
|
|
1649
|
+
pickActive,
|
|
1650
|
+
onSelectShape,
|
|
1596
1651
|
}) {
|
|
1597
1652
|
const isInstance = Boolean(rawActor?.blueprint);
|
|
1598
1653
|
const presentNames = new Set(
|
|
@@ -1687,6 +1742,9 @@ function ActorInspector({
|
|
|
1687
1742
|
sprites={sprites}
|
|
1688
1743
|
setComponent={setComponent}
|
|
1689
1744
|
override={override}
|
|
1745
|
+
beginPick={onBeginPick}
|
|
1746
|
+
pickActive={pickActive}
|
|
1747
|
+
onSelectShape={onSelectShape}
|
|
1690
1748
|
/>
|
|
1691
1749
|
) : (
|
|
1692
1750
|
<AutoInspector
|
|
@@ -68,6 +68,7 @@ export function SelectionOverlay({
|
|
|
68
68
|
previewSceneData,
|
|
69
69
|
sprites,
|
|
70
70
|
selectedActorIds,
|
|
71
|
+
selectedColliderShape,
|
|
71
72
|
snap,
|
|
72
73
|
onArrange,
|
|
73
74
|
onClone,
|
|
@@ -198,6 +199,9 @@ export function SelectionOverlay({
|
|
|
198
199
|
|
|
199
200
|
const geometry = getOverlayGeometry(frame, box, camera);
|
|
200
201
|
const colliderFrames = getSelectedColliderFrames(previewSceneData, selectedActorIds, sprites);
|
|
202
|
+
// The collider inspector edits one shape of a single-selected actor; highlight
|
|
203
|
+
// that shape's box so you can see which one you're editing at a glance.
|
|
204
|
+
const editedColliderActor = selectedActorIds.length === 1 ? selectedActorIds[0] : null;
|
|
201
205
|
|
|
202
206
|
return (
|
|
203
207
|
<div ref={rootRef} className={styles.selOverlayRoot}>
|
|
@@ -211,24 +215,34 @@ export function SelectionOverlay({
|
|
|
211
215
|
<div
|
|
212
216
|
className={styles.selChromeWorld}
|
|
213
217
|
style={{ transform: `translate(${-geometry.camX}px, ${-geometry.camY}px)` }}>
|
|
214
|
-
{colliderFrames.map((collider, i) =>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
collider.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
218
|
+
{colliderFrames.map((collider, i) => {
|
|
219
|
+
const highlighted =
|
|
220
|
+
collider.actorId === editedColliderActor && collider.shapeIndex === selectedColliderShape;
|
|
221
|
+
return (
|
|
222
|
+
<div
|
|
223
|
+
key={`${collider.actorId}:${i}`}
|
|
224
|
+
className={cx(
|
|
225
|
+
styles.selColliderBox,
|
|
226
|
+
collider.isTrigger && styles.selColliderPickup
|
|
227
|
+
)}
|
|
228
|
+
style={{
|
|
229
|
+
left: collider.x,
|
|
230
|
+
top: collider.y,
|
|
231
|
+
width: collider.width,
|
|
232
|
+
height: collider.height,
|
|
233
|
+
borderRadius: collider.shape === 'circle' ? '50%' : undefined,
|
|
234
|
+
transformOrigin: '0 0',
|
|
235
|
+
transform: colliderTransform(collider),
|
|
236
|
+
// The shape being edited: a bright border + tint, lifted above
|
|
237
|
+
// the others so an overlapping (e.g. skateboard) shape reads clearly.
|
|
238
|
+
borderColor: highlighted ? '#ffd24a' : undefined,
|
|
239
|
+
borderWidth: highlighted ? 3 : undefined,
|
|
240
|
+
background: highlighted ? 'rgba(255, 210, 74, 0.14)' : undefined,
|
|
241
|
+
zIndex: highlighted ? 1 : 0,
|
|
242
|
+
}}
|
|
243
|
+
/>
|
|
244
|
+
);
|
|
245
|
+
})}
|
|
232
246
|
</div>
|
|
233
247
|
</div>
|
|
234
248
|
|
|
@@ -456,6 +470,19 @@ function getActionAnchor({ centerX, centerY, halfH, rotateAnchor }) {
|
|
|
456
470
|
};
|
|
457
471
|
}
|
|
458
472
|
|
|
473
|
+
// Compose the collider box's on-screen transform (origin = box top-left): rotate
|
|
474
|
+
// by the shape's own `angle` about its center, then by the actor's Layout.rotation
|
|
475
|
+
// about the actor center. Both pivots explicit since they differ.
|
|
476
|
+
function colliderTransform(c) {
|
|
477
|
+
const ax = c.originX;
|
|
478
|
+
const ay = c.originY;
|
|
479
|
+
const layout = `translate(${ax}px, ${ay}px) rotate(${c.rotation}deg) translate(${-ax}px, ${-ay}px)`;
|
|
480
|
+
if (!c.shapeAngle) return layout;
|
|
481
|
+
const cx = c.width / 2;
|
|
482
|
+
const cy = c.height / 2;
|
|
483
|
+
return `${layout} translate(${cx}px, ${cy}px) rotate(${c.shapeAngle}deg) translate(${-cx}px, ${-cy}px)`;
|
|
484
|
+
}
|
|
485
|
+
|
|
459
486
|
function getSelectedColliderFrames(sceneData, actorIds, sprites) {
|
|
460
487
|
if (!sceneData || !actorIds || actorIds.length === 0) return [];
|
|
461
488
|
const wanted = new Set(actorIds);
|
|
@@ -470,7 +497,7 @@ function getSelectedColliderFrames(sceneData, actorIds, sprites) {
|
|
|
470
497
|
// One preview box per collider shape, from the shared geometry so the
|
|
471
498
|
// preview matches the physics body. A circle renders as a radius-sized
|
|
472
499
|
// square with border-radius; a triangle/polygon as its AABB (v1).
|
|
473
|
-
return shapes.map((s) => {
|
|
500
|
+
return shapes.map((s, shapeIndex) => {
|
|
474
501
|
let kind = 'box';
|
|
475
502
|
let x;
|
|
476
503
|
let y;
|
|
@@ -505,6 +532,7 @@ function getSelectedColliderFrames(sceneData, actorIds, sprites) {
|
|
|
505
532
|
}
|
|
506
533
|
return {
|
|
507
534
|
actorId: actor.id,
|
|
535
|
+
shapeIndex,
|
|
508
536
|
shape: kind,
|
|
509
537
|
isTrigger,
|
|
510
538
|
x,
|
|
@@ -512,6 +540,7 @@ function getSelectedColliderFrames(sceneData, actorIds, sprites) {
|
|
|
512
540
|
width,
|
|
513
541
|
height,
|
|
514
542
|
rotation: layout.rotation ?? 0,
|
|
543
|
+
shapeAngle: s.type === 'box' ? s.angle ?? 0 : 0,
|
|
515
544
|
originX: layout.x + layout.width / 2 - x,
|
|
516
545
|
originY: layout.y + layout.height / 2 - y,
|
|
517
546
|
};
|
|
@@ -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';
|
|
@@ -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
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// A Collider is a LIST of shapes (`Collider.shapes`), each stored as a FRACTION
|
|
6
6
|
// of the actor's Layout box -- so a collider keeps its relative size and position
|
|
7
7
|
// when the box is resized (blueprint default OR per-instance override). Shapes:
|
|
8
|
-
// box: { type:'box', x, y, w, h }
|
|
8
|
+
// box: { type:'box', x, y, w, h, angle? } center + size (frac); angle deg
|
|
9
9
|
// circle: { type:'circle', x, y, r } center frac; r = frac of min(box side)
|
|
10
10
|
// triangle: { type:'triangle', points:[{x,y}×3] } fractions of box
|
|
11
11
|
// polygon: { type:'polygon', points:[{x,y}×N] }
|
|
@@ -140,7 +140,10 @@ function resolveShape(shape, layout) {
|
|
|
140
140
|
const h = (shape.h ?? 1) * bh;
|
|
141
141
|
const cx = wx(shape.x ?? 0.5);
|
|
142
142
|
const cy = wy(shape.y ?? 0.5);
|
|
143
|
-
|
|
143
|
+
// `angle` (deg) rotates the box about its own center, within the unrotated box
|
|
144
|
+
// frame; Layout.rotation composes on top (matter body + draw ctx). `x`/`y` are
|
|
145
|
+
// the UNROTATED top-left -- shapeAabb widens them when angled.
|
|
146
|
+
return { type: 'box', cx, cy, width: w, height: h, x: cx - w / 2, y: cy - h / 2, angle: shape.angle ?? 0 };
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
// World-space shapes for an actor's collider (list), or null if no Layout/Collider.
|
|
@@ -172,7 +175,14 @@ function shapeAabb(s) {
|
|
|
172
175
|
}
|
|
173
176
|
return [x0, y0, x1, y1];
|
|
174
177
|
}
|
|
175
|
-
|
|
178
|
+
const ang = ((s.angle ?? 0) * Math.PI) / 180;
|
|
179
|
+
if (!ang) return [s.x, s.y, s.x + s.width, s.y + s.height];
|
|
180
|
+
// AABB of a box rotated about its center: project the half-extents onto the axes.
|
|
181
|
+
const c = Math.abs(Math.cos(ang));
|
|
182
|
+
const sn = Math.abs(Math.sin(ang));
|
|
183
|
+
const ex = (s.width / 2) * c + (s.height / 2) * sn;
|
|
184
|
+
const ey = (s.width / 2) * sn + (s.height / 2) * c;
|
|
185
|
+
return [s.cx - ex, s.cy - ey, s.cx + ex, s.cy + ey];
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
// AABB of the whole collider (union of all shapes), world units -- for
|
|
@@ -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]) {
|
|
@@ -59,6 +62,11 @@ export class SceneRuntime {
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
load(sceneData) {
|
|
65
|
+
// Reset registered systems (e.g. physics) so a reload/restart/scene
|
|
66
|
+
// transition starts from the authored layout -- otherwise a system holding
|
|
67
|
+
// simulation state (body positions, joints) would carry it across the load.
|
|
68
|
+
// No-op on the constructor's first load, before any system is registered.
|
|
69
|
+
for (const system of this.systems) system.reset?.(this);
|
|
62
70
|
this.data = structuredClone(sceneData);
|
|
63
71
|
this.actors = new Map();
|
|
64
72
|
for (const actor of this.data.actors ?? []) {
|
|
@@ -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');
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "physics-2d",
|
|
3
3
|
"lockfileVersion": 3,
|
|
4
4
|
"requires": true,
|
|
5
5
|
"packages": {
|
|
6
6
|
"": {
|
|
7
|
-
"name": "
|
|
7
|
+
"name": "physics-2d",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@codemirror/commands": "^6.10.3",
|
|
10
10
|
"@codemirror/lang-javascript": "^6.2.5",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"@lezer/highlight": "^1.2.3",
|
|
16
16
|
"castle-web-sdk": "file:../../sdk",
|
|
17
17
|
"codemirror": "^6.0.2",
|
|
18
|
+
"matter-js": "^0.20.0",
|
|
18
19
|
"react": "^19.2.4",
|
|
19
20
|
"react-dom": "^19.2.4"
|
|
20
21
|
},
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
},
|
|
28
29
|
"../../sdk": {
|
|
29
30
|
"name": "castle-web-sdk",
|
|
30
|
-
"version": "0.4.
|
|
31
|
+
"version": "0.4.11",
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"eslint": "^9.0.0",
|
|
33
34
|
"jscpd": "^4.0.5",
|
|
@@ -1899,6 +1900,12 @@
|
|
|
1899
1900
|
"node": ">= 0.4"
|
|
1900
1901
|
}
|
|
1901
1902
|
},
|
|
1903
|
+
"node_modules/matter-js": {
|
|
1904
|
+
"version": "0.20.0",
|
|
1905
|
+
"resolved": "https://registry.npmjs.org/matter-js/-/matter-js-0.20.0.tgz",
|
|
1906
|
+
"integrity": "sha512-iC9fYR7zVT3HppNnsFsp9XOoQdQN2tUyfaKg4CHLH8bN+j6GT4Gw7IH2rP0tflAebrHFw730RR3DkVSZRX8hwA==",
|
|
1907
|
+
"license": "MIT"
|
|
1908
|
+
},
|
|
1902
1909
|
"node_modules/merge-stream": {
|
|
1903
1910
|
"version": "2.0.0",
|
|
1904
1911
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|