castle-web-cli 0.4.82 → 0.4.84
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-failures.d.ts +17 -0
- package/dist/agent-failures.js +151 -0
- package/dist/agent.d.ts +27 -0
- package/dist/agent.js +614 -57
- package/dist/ide.js +150 -1
- package/dist/native/loop.js +40 -1
- package/dist/native/openrouter.d.ts +12 -1
- package/dist/native/openrouter.js +45 -2
- package/dist/native/types.d.ts +6 -0
- package/dist/native/types.js +0 -38
- package/dist/openrouter-catalog.d.ts +28 -0
- package/dist/openrouter-catalog.js +299 -0
- package/dist/shell/assets/index-BOgm5T3W.js +144 -0
- package/dist/shell/assets/index-DonnH--m.css +1 -0
- package/dist/shell/index.html +2 -2
- package/dist/shell/operator.png +0 -0
- package/kits/basic-2d/CLAUDE.md +27 -23
- package/kits/basic-2d/behaviors/Collider.jsx +24 -30
- package/kits/basic-2d/behaviors/Layout.jsx +9 -6
- package/kits/basic-2d/behaviors/Sprite.jsx +137 -7
- package/kits/basic-2d/blueprints/cauldron.scene +3 -5
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +11 -11
- package/kits/basic-2d/editors/SceneEditor.jsx +212 -50
- package/kits/basic-2d/editors/SelectionOverlay.jsx +73 -54
- package/kits/basic-2d/editors/inspectorSheet.js +5 -1
- package/kits/basic-2d/engine/ScenePlayer.jsx +98 -7
- package/kits/basic-2d/engine/autoInspector.jsx +26 -7
- package/kits/basic-2d/engine/blueprint.js +35 -8
- package/kits/basic-2d/engine/collider.js +146 -0
- package/kits/basic-2d/engine/scene.js +53 -30
- package/kits/basic-2d/engine/spriteGeometry.js +32 -0
- package/kits/basic-2d/engine/ui.jsx +89 -30
- package/kits/basic-2d/engine/ui.module.css +157 -53
- package/kits/basic-2d/scenes/main.scene +3 -3
- package/package.json +2 -1
- package/dist/shell/assets/index-ByhgiJoP.js +0 -141
- package/dist/shell/assets/index-D6hM_VlW.css +0 -1
|
@@ -4,17 +4,15 @@
|
|
|
4
4
|
{
|
|
5
5
|
"components": {
|
|
6
6
|
"Layout": {
|
|
7
|
-
"width":
|
|
8
|
-
"height":
|
|
7
|
+
"width": 100,
|
|
8
|
+
"height": 100,
|
|
9
9
|
"z": 1
|
|
10
10
|
},
|
|
11
11
|
"Sprite": {
|
|
12
12
|
"file": "drawings/cauldron.pxart"
|
|
13
13
|
},
|
|
14
14
|
"Collider": {
|
|
15
|
-
"kind": "solid"
|
|
16
|
-
"width": 70,
|
|
17
|
-
"height": 70
|
|
15
|
+
"kind": "solid"
|
|
18
16
|
}
|
|
19
17
|
}
|
|
20
18
|
}
|
|
@@ -17,6 +17,7 @@ export function BlueprintLibrary({
|
|
|
17
17
|
editCameraRef,
|
|
18
18
|
isPlaying,
|
|
19
19
|
selectedBlueprintPath,
|
|
20
|
+
instanceBlueprintPath,
|
|
20
21
|
onSelectBlueprint,
|
|
21
22
|
onAddActor,
|
|
22
23
|
dragPlace,
|
|
@@ -103,16 +104,22 @@ export function BlueprintLibrary({
|
|
|
103
104
|
type="button"
|
|
104
105
|
className={styles.bpActionSlot}
|
|
105
106
|
aria-label="Add actor"
|
|
106
|
-
title="
|
|
107
|
+
title="New blueprint"
|
|
107
108
|
onClick={onAddActor}
|
|
108
109
|
disabled={isPlaying}>
|
|
109
|
-
<
|
|
110
|
+
<Icon name="plus" />
|
|
110
111
|
</button>
|
|
111
112
|
{blueprints.map((blueprint) => (
|
|
112
113
|
<button
|
|
113
114
|
key={blueprint.path}
|
|
114
115
|
type="button"
|
|
115
|
-
className={cx(
|
|
116
|
+
className={cx(
|
|
117
|
+
styles.bpSlot,
|
|
118
|
+
selectedBlueprintPath === blueprint.path && styles.bpSlotSelected,
|
|
119
|
+
selectedBlueprintPath !== blueprint.path &&
|
|
120
|
+
instanceBlueprintPath === blueprint.path &&
|
|
121
|
+
styles.bpSlotInstance
|
|
122
|
+
)}
|
|
116
123
|
title={`${blueprint.name} -- click to edit, drag to place`}
|
|
117
124
|
disabled={isPlaying}
|
|
118
125
|
onPointerDown={(event) => onSlotPointerDown(event, blueprint)}
|
|
@@ -125,7 +132,6 @@ export function BlueprintLibrary({
|
|
|
125
132
|
onSelectBlueprint(blueprint.path);
|
|
126
133
|
}}>
|
|
127
134
|
<BlueprintThumbnail blueprint={blueprint} sprites={sprites} />
|
|
128
|
-
<InstanceCountBadge files={files} blueprintPath={blueprint.path} />
|
|
129
135
|
</button>
|
|
130
136
|
))}
|
|
131
137
|
{dragPreview ? <BlueprintDragPreview preview={dragPreview} sprites={sprites} /> : null}
|
|
@@ -176,12 +182,6 @@ function DeleteBlueprintConfirm({ files, blueprint, onCancel, onConfirm }) {
|
|
|
176
182
|
);
|
|
177
183
|
}
|
|
178
184
|
|
|
179
|
-
function InstanceCountBadge({ files, blueprintPath }) {
|
|
180
|
-
const count = countBlueprintInstances(files, blueprintPath);
|
|
181
|
-
if (count === 0) return null;
|
|
182
|
-
return <span className={styles.bpSlotBadge}>{count > 99 ? '99+' : count}</span>;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
185
|
function BlueprintDragPreview({ preview, sprites }) {
|
|
186
186
|
const layout = preview.blueprint.components?.Layout;
|
|
187
187
|
const width = Math.max(24, Math.min(80, layout?.width ?? 48));
|
|
@@ -201,7 +201,7 @@ function BlueprintDragPreview({ preview, sprites }) {
|
|
|
201
201
|
);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
function BlueprintThumbnail({ blueprint, sprites, preview = false }) {
|
|
204
|
+
export function BlueprintThumbnail({ blueprint, sprites, preview = false }) {
|
|
205
205
|
const canvasRef = useRef(null);
|
|
206
206
|
const spritePath = blueprint.components?.Sprite?.file;
|
|
207
207
|
const sprite = spritePath ? sprites?.[spritePath] : null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { writeFile } from 'castle-web-sdk';
|
|
2
|
+
import { onSaveReloadState, takeReloadState, writeFile } from 'castle-web-sdk';
|
|
3
3
|
import { basename, formatJson, parseJsonFile } from '../engine/files';
|
|
4
4
|
import {
|
|
5
5
|
arrangeActors,
|
|
@@ -17,9 +17,11 @@ import {
|
|
|
17
17
|
addActorWithBlueprint,
|
|
18
18
|
blueprintDropXY,
|
|
19
19
|
cascadeDeleteBlueprint,
|
|
20
|
+
countBlueprintInstances,
|
|
20
21
|
forkActorToBlueprint,
|
|
21
22
|
getBlueprintTemplate,
|
|
22
23
|
isBlueprintPath,
|
|
24
|
+
isInheritedProp,
|
|
23
25
|
migrateOrphanActors,
|
|
24
26
|
placeBlueprintInstance,
|
|
25
27
|
setBlueprintName,
|
|
@@ -45,7 +47,7 @@ import {
|
|
|
45
47
|
import { AutoInspector } from '../engine/autoInspector';
|
|
46
48
|
import { SceneUI } from '../engine/SceneUI';
|
|
47
49
|
import { SelectionOverlay } from './SelectionOverlay';
|
|
48
|
-
import { BlueprintLibrary } from './BlueprintLibrary';
|
|
50
|
+
import { BlueprintLibrary, BlueprintThumbnail } from './BlueprintLibrary';
|
|
49
51
|
import { behaviorClasses, findBehaviorClass } from './behaviorRegistry';
|
|
50
52
|
import { useEditHistory, useUndoRedoShortcuts } from './editorHistory';
|
|
51
53
|
// A blueprint file's `actors[0]` has no `id` (it's the template, not an
|
|
@@ -137,16 +139,32 @@ export function SceneEditor({
|
|
|
137
139
|
const canvasRef = useRef(null);
|
|
138
140
|
const runtimeRef = useRef(null);
|
|
139
141
|
const marqueeRef = useRef(null);
|
|
140
|
-
|
|
142
|
+
// Ephemeral editor state (camera, play mode, inspected blueprint) restored
|
|
143
|
+
// across a reload: an agent's CODE edit triggers requestReload, which would
|
|
144
|
+
// otherwise reset the viewport/play state on every change. Data edits fold in
|
|
145
|
+
// live without a reload, so they never need this. `takeReloadState` is
|
|
146
|
+
// one-shot (clears on read), so read it once here. Selection is stashed
|
|
147
|
+
// separately by SingleEditor.
|
|
148
|
+
const reloadStashKey = `scene-editor:${path}`;
|
|
149
|
+
const [reloadStash] = useState(() => takeReloadState(reloadStashKey));
|
|
150
|
+
const editCameraRef = useRef(reloadStash?.editCamera ?? { x: 0, y: 0, zoom: 1 });
|
|
141
151
|
const selectedActorIdsRef = useRef(selectedActorIds);
|
|
142
152
|
selectedActorIdsRef.current = selectedActorIds;
|
|
143
|
-
const [isPlaying, setIsPlaying] = useState(false);
|
|
153
|
+
const [isPlaying, setIsPlaying] = useState(reloadStash?.isPlaying ?? false);
|
|
144
154
|
// Hotbar blueprint selection (tap a slot -> inspect/edit the blueprint in
|
|
145
155
|
// the sidebar; drag remains the placement gesture). Mutually exclusive with
|
|
146
156
|
// actor selection: ANY actor-selection change -- including clearing it via
|
|
147
157
|
// empty-canvas click or Escape -- also drops the blueprint selection, which
|
|
148
158
|
// is why every internal caller goes through the wrapper below.
|
|
149
|
-
const [selectedBlueprintPath, setSelectedBlueprintPath] = useState(
|
|
159
|
+
const [selectedBlueprintPath, setSelectedBlueprintPath] = useState(
|
|
160
|
+
reloadStash?.selectedBlueprintPath ?? null
|
|
161
|
+
);
|
|
162
|
+
// Read live in the render loop (a ref so changing it doesn't restart the
|
|
163
|
+
// loop): while a blueprint is selected, its instances stay lit and every
|
|
164
|
+
// other actor dims. Assigned below once `hotbarBlueprint` resolves, so a
|
|
165
|
+
// stale/deleted selection (file vanished) doesn't dim the scene with nothing
|
|
166
|
+
// to highlight.
|
|
167
|
+
const selectedBlueprintPathRef = useRef(selectedBlueprintPath);
|
|
150
168
|
const onSelectActorIds = useCallback(
|
|
151
169
|
(ids) => {
|
|
152
170
|
setSelectedBlueprintPath(null);
|
|
@@ -171,6 +189,19 @@ export function SceneEditor({
|
|
|
171
189
|
// then, but without this the keyboard shortcut could still rewrite the
|
|
172
190
|
// scene file out from under the running play-mode runtime.
|
|
173
191
|
useUndoRedoShortcuts(history, !isPlaying);
|
|
192
|
+
// Stash ephemeral editor state right before a reload so it survives an agent
|
|
193
|
+
// code edit. `editCamera` is read live from the ref at save time (a reload is
|
|
194
|
+
// imminent, so the latest value is what matters); isPlaying / blueprint
|
|
195
|
+
// selection re-register whenever they change.
|
|
196
|
+
useEffect(
|
|
197
|
+
() =>
|
|
198
|
+
onSaveReloadState(reloadStashKey, () => ({
|
|
199
|
+
isPlaying,
|
|
200
|
+
selectedBlueprintPath,
|
|
201
|
+
editCamera: editCameraRef.current,
|
|
202
|
+
})),
|
|
203
|
+
[reloadStashKey, isPlaying, selectedBlueprintPath]
|
|
204
|
+
);
|
|
174
205
|
const showMulti = !isBlueprintFile && (selectedActorIds.length > 1 || multiSelectMode);
|
|
175
206
|
const inspectorSheet = useSelectionInspectorSheet(true);
|
|
176
207
|
const { value: parsedSceneData, error } = parseJsonFile(path, text);
|
|
@@ -196,6 +227,7 @@ export function SceneEditor({
|
|
|
196
227
|
editCameraRef,
|
|
197
228
|
marqueeRef,
|
|
198
229
|
selectedActorIdsRef,
|
|
230
|
+
selectedBlueprintPathRef,
|
|
199
231
|
});
|
|
200
232
|
const panGesture = usePanGesture({ canvasRef, editCameraRef, isPlaying });
|
|
201
233
|
useScenePlayKeys(runtimeRef);
|
|
@@ -249,8 +281,11 @@ export function SceneEditor({
|
|
|
249
281
|
selectedActorIds.length === 1
|
|
250
282
|
? previewSceneData.actors.find((actor) => actor.id === selectedActorIds[0])
|
|
251
283
|
: undefined;
|
|
284
|
+
const selectedBlueprintTemplate = rawSelectedActor?.blueprint
|
|
285
|
+
? getBlueprintTemplate(files, rawSelectedActor.blueprint)
|
|
286
|
+
: undefined;
|
|
252
287
|
const selectedBlueprintName = rawSelectedActor?.blueprint
|
|
253
|
-
?
|
|
288
|
+
? selectedBlueprintTemplate?.name ?? basename(rawSelectedActor.blueprint)
|
|
254
289
|
: undefined;
|
|
255
290
|
// Hotbar-selected blueprint, resolved fresh from live files each render. A
|
|
256
291
|
// selection whose file vanished (cascade delete, external edit) degrades to
|
|
@@ -259,6 +294,9 @@ export function SceneEditor({
|
|
|
259
294
|
!isBlueprintFile && selectedBlueprintPath
|
|
260
295
|
? getBlueprintTemplate(files, selectedBlueprintPath)
|
|
261
296
|
: null;
|
|
297
|
+
// Only dim for a blueprint that actually resolves to an inspectable template
|
|
298
|
+
// (mirrors the sidebar): a vanished/stale selection highlights nothing.
|
|
299
|
+
selectedBlueprintPathRef.current = hotbarBlueprint ? selectedBlueprintPath : null;
|
|
262
300
|
// Template edits from the sidebar write the BLUEPRINT file, not this scene:
|
|
263
301
|
// optimistic fold into live files state (instant merged-instance updates
|
|
264
302
|
// here) + debounced writeFile via SingleEditor's saver, which also makes
|
|
@@ -391,24 +429,6 @@ export function SceneEditor({
|
|
|
391
429
|
void writeFile(result.blueprintFile.path, result.blueprintFile.text);
|
|
392
430
|
history.commit(serialize(result.sceneData));
|
|
393
431
|
};
|
|
394
|
-
const inspectorLabel = isBlueprintFile
|
|
395
|
-
? 'Blueprint'
|
|
396
|
-
: hotbarBlueprint
|
|
397
|
-
? 'Blueprint'
|
|
398
|
-
: showMulti
|
|
399
|
-
? 'Multi-select'
|
|
400
|
-
: selectedActor
|
|
401
|
-
? 'Inspector'
|
|
402
|
-
: 'Scene';
|
|
403
|
-
const inspectorHint = isBlueprintFile
|
|
404
|
-
? sceneData.name ?? basename(path)
|
|
405
|
-
: hotbarBlueprint
|
|
406
|
-
? hotbarBlueprint.name
|
|
407
|
-
: showMulti
|
|
408
|
-
? `${selectedActorIds.length} actor${selectedActorIds.length === 1 ? '' : 's'} selected`
|
|
409
|
-
: selectedActor
|
|
410
|
-
? `actor ${selectedActor.id}`
|
|
411
|
-
: 'scene settings';
|
|
412
432
|
return (
|
|
413
433
|
<>
|
|
414
434
|
<EditorHeader
|
|
@@ -451,6 +471,7 @@ export function SceneEditor({
|
|
|
451
471
|
editCameraRef={editCameraRef}
|
|
452
472
|
isPlaying={isPlaying}
|
|
453
473
|
selectedBlueprintPath={selectedBlueprintPath}
|
|
474
|
+
instanceBlueprintPath={rawSelectedActor?.blueprint ?? null}
|
|
454
475
|
onSelectBlueprint={onSelectBlueprint}
|
|
455
476
|
onAddActor={onAddActor}
|
|
456
477
|
dragPlace={dragPlace}
|
|
@@ -506,6 +527,7 @@ export function SceneEditor({
|
|
|
506
527
|
editCameraRef={editCameraRef}
|
|
507
528
|
sceneData={sceneData}
|
|
508
529
|
previewSceneData={previewSceneData}
|
|
530
|
+
sprites={sprites}
|
|
509
531
|
selectedActorIds={selectedActorIds}
|
|
510
532
|
snap={snapSettings}
|
|
511
533
|
onArrange={isBlueprintFile ? undefined : actions.arrangeSelection}
|
|
@@ -519,13 +541,15 @@ export function SceneEditor({
|
|
|
519
541
|
</div>
|
|
520
542
|
<aside {...inspectorSheet.rootProps}>
|
|
521
543
|
<div {...inspectorSheet.grabProps}>
|
|
522
|
-
<SheetGrabHandle
|
|
544
|
+
<SheetGrabHandle />
|
|
523
545
|
</div>
|
|
524
546
|
<div className={cx(styles.sheetBody, styles.inspectorBody)}>
|
|
525
547
|
{hotbarBlueprint ? (
|
|
526
548
|
<BlueprintInspector
|
|
527
549
|
name={hotbarBlueprint.name}
|
|
528
550
|
template={hotbarBlueprint}
|
|
551
|
+
sprites={sprites}
|
|
552
|
+
blueprintPath={selectedBlueprintPath}
|
|
529
553
|
files={files}
|
|
530
554
|
onRename={blueprintActions.rename}
|
|
531
555
|
onSetComponent={blueprintActions.setComponent}
|
|
@@ -542,14 +566,23 @@ export function SceneEditor({
|
|
|
542
566
|
/>
|
|
543
567
|
) : selectedActor ? (
|
|
544
568
|
<ActorInspector
|
|
569
|
+
key={selectedActor.id}
|
|
545
570
|
selectedActor={selectedActor}
|
|
546
571
|
rawActor={rawSelectedActor}
|
|
547
572
|
blueprintName={selectedBlueprintName}
|
|
573
|
+
blueprintTemplate={selectedBlueprintTemplate}
|
|
574
|
+
sprites={sprites}
|
|
548
575
|
files={files}
|
|
576
|
+
showAddBehavior={isBlueprintFile}
|
|
549
577
|
onSetComponent={actions.updateComponent}
|
|
550
578
|
onAddBehavior={actions.addBehavior}
|
|
551
579
|
onRemoveBehavior={actions.removeBehavior}
|
|
552
580
|
onFork={isBlueprintFile ? undefined : onForkSelection}
|
|
581
|
+
onEditBlueprint={
|
|
582
|
+
isBlueprintFile || !rawSelectedActor?.blueprint
|
|
583
|
+
? undefined
|
|
584
|
+
: () => onSelectBlueprint(rawSelectedActor.blueprint)
|
|
585
|
+
}
|
|
553
586
|
/>
|
|
554
587
|
) : (
|
|
555
588
|
<SceneInspector
|
|
@@ -692,8 +725,9 @@ function parseHexColor(value) {
|
|
|
692
725
|
function useSelectionInspectorSheet(open) {
|
|
693
726
|
// Inspector visibility is driven by selection / multi-mode; the sheet hook
|
|
694
727
|
// owns the drag-resize height and remembers it across selections while
|
|
695
|
-
// mounted (and restores it when the sheet reopens)
|
|
696
|
-
|
|
728
|
+
// mounted (and restores it when the sheet reopens), plus persists it across
|
|
729
|
+
// reloads via `storageKey`.
|
|
730
|
+
return useMobileSheet({ open, baseClassName: styles.inspector, storageKey: 'scene-inspector' });
|
|
697
731
|
}
|
|
698
732
|
function useSelectionGesture(args) {
|
|
699
733
|
const dragRef = useRef(null);
|
|
@@ -724,6 +758,7 @@ function useSelectionGesture(args) {
|
|
|
724
758
|
movingActorIds: [],
|
|
725
759
|
moveStarts: {},
|
|
726
760
|
recordedMoveSnapshot: false,
|
|
761
|
+
lastMoveKey: '0,0',
|
|
727
762
|
selectionBeforeMarquee: [...current.selectedActorIds],
|
|
728
763
|
};
|
|
729
764
|
if (event.shiftKey) {
|
|
@@ -759,6 +794,15 @@ function useSelectionGesture(args) {
|
|
|
759
794
|
const dx = snapDelta(dxTotal, snap.gridSize, snap.enabled);
|
|
760
795
|
const dy = snapDelta(dyTotal, snap.gridSize, snap.enabled);
|
|
761
796
|
drag.lastPoint = point;
|
|
797
|
+
// Key the commit on the snapped delta, not on `nextScene === sceneData`.
|
|
798
|
+
// `moveActorsFromStarts` returns the pointer-down scene unchanged when the
|
|
799
|
+
// delta lands back on the start, and an identity guard would skip applying
|
|
800
|
+
// it -- stranding the actors on the last grid step instead of letting them
|
|
801
|
+
// return to origin. Re-apply whenever the snapped delta changes; the
|
|
802
|
+
// identity check now only gates recording the undo snapshot.
|
|
803
|
+
const moveKey = `${dx},${dy}`;
|
|
804
|
+
if (moveKey === drag.lastMoveKey) return;
|
|
805
|
+
drag.lastMoveKey = moveKey;
|
|
762
806
|
const nextScene = moveActorsFromStarts(
|
|
763
807
|
current.sceneData,
|
|
764
808
|
drag.movingActorIds,
|
|
@@ -766,8 +810,7 @@ function useSelectionGesture(args) {
|
|
|
766
810
|
dx,
|
|
767
811
|
dy
|
|
768
812
|
);
|
|
769
|
-
if (nextScene
|
|
770
|
-
if (!drag.recordedMoveSnapshot) {
|
|
813
|
+
if (nextScene !== current.sceneData && !drag.recordedMoveSnapshot) {
|
|
771
814
|
current.recordSceneSnapshot();
|
|
772
815
|
drag.recordedMoveSnapshot = true;
|
|
773
816
|
}
|
|
@@ -1179,6 +1222,7 @@ function useScenePlayLoop({
|
|
|
1179
1222
|
editCameraRef,
|
|
1180
1223
|
marqueeRef,
|
|
1181
1224
|
selectedActorIdsRef,
|
|
1225
|
+
selectedBlueprintPathRef,
|
|
1182
1226
|
}) {
|
|
1183
1227
|
// Spin up / tear down the play-mode runtime as the user toggles play. `text`
|
|
1184
1228
|
// is the stable identity for `sceneData` (which is re-parsed every render);
|
|
@@ -1234,7 +1278,7 @@ function useScenePlayLoop({
|
|
|
1234
1278
|
viewport: frameViewport,
|
|
1235
1279
|
useCamera: true,
|
|
1236
1280
|
editPlaceholders: !isPlaying,
|
|
1237
|
-
|
|
1281
|
+
dimBlueprintPath: isPlaying ? null : selectedBlueprintPathRef.current,
|
|
1238
1282
|
});
|
|
1239
1283
|
}
|
|
1240
1284
|
raf = requestAnimationFrame(frame);
|
|
@@ -1242,7 +1286,17 @@ function useScenePlayLoop({
|
|
|
1242
1286
|
raf = requestAnimationFrame(frame);
|
|
1243
1287
|
return () => cancelAnimationFrame(raf);
|
|
1244
1288
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- `text` proxies `sceneData` identity; resetting on every parse churns the loop and wipes runtime state.
|
|
1245
|
-
}, [
|
|
1289
|
+
}, [
|
|
1290
|
+
sprites,
|
|
1291
|
+
files,
|
|
1292
|
+
isPlaying,
|
|
1293
|
+
text,
|
|
1294
|
+
canvasRef,
|
|
1295
|
+
runtimeRef,
|
|
1296
|
+
marqueeRef,
|
|
1297
|
+
selectedActorIdsRef,
|
|
1298
|
+
selectedBlueprintPathRef,
|
|
1299
|
+
]);
|
|
1246
1300
|
}
|
|
1247
1301
|
function useScenePlayKeys(runtimeRef) {
|
|
1248
1302
|
useEffect(() => {
|
|
@@ -1343,6 +1397,8 @@ function SceneInspector({ sceneData, snapSettings, onChangeScene, onChangeEditor
|
|
|
1343
1397
|
function BlueprintInspector({
|
|
1344
1398
|
name,
|
|
1345
1399
|
template,
|
|
1400
|
+
sprites,
|
|
1401
|
+
blueprintPath,
|
|
1346
1402
|
files,
|
|
1347
1403
|
onRename,
|
|
1348
1404
|
onSetComponent,
|
|
@@ -1350,16 +1406,25 @@ function BlueprintInspector({
|
|
|
1350
1406
|
onRemoveBehavior,
|
|
1351
1407
|
}) {
|
|
1352
1408
|
const templateActor = { id: '__blueprint__', components: template.components };
|
|
1409
|
+
const instanceCount = countBlueprintInstances(files, blueprintPath);
|
|
1353
1410
|
return (
|
|
1354
1411
|
<>
|
|
1355
|
-
<div className={styles.
|
|
1356
|
-
<
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1412
|
+
<div className={styles.instanceHeader}>
|
|
1413
|
+
<div className={styles.instanceHeaderName}>
|
|
1414
|
+
<BlueprintThumbnail blueprint={template} sprites={sprites} />
|
|
1415
|
+
<div className={styles.instanceHeaderNameCol}>
|
|
1416
|
+
<input
|
|
1417
|
+
className={styles.blueprintNameInput}
|
|
1418
|
+
value={name}
|
|
1419
|
+
onChange={(event) => onRename?.(event.target.value)}
|
|
1420
|
+
aria-label="Blueprint name"
|
|
1421
|
+
spellCheck={false}
|
|
1422
|
+
/>
|
|
1423
|
+
<span className={styles.instanceHeaderId}>
|
|
1424
|
+
{instanceCount} {instanceCount === 1 ? 'actor' : 'actors'}
|
|
1425
|
+
</span>
|
|
1426
|
+
</div>
|
|
1427
|
+
</div>
|
|
1363
1428
|
</div>
|
|
1364
1429
|
<ActorInspector
|
|
1365
1430
|
selectedActor={templateActor}
|
|
@@ -1372,15 +1437,43 @@ function BlueprintInspector({
|
|
|
1372
1437
|
</>
|
|
1373
1438
|
);
|
|
1374
1439
|
}
|
|
1440
|
+
// Per-behavior helper the inspector fields use to render instance overrides.
|
|
1441
|
+
// `isOverridden` is true only for inherited props the instance has its own
|
|
1442
|
+
// value for (position/rotation are `inherit: false` and always instance-local,
|
|
1443
|
+
// so they never read as "overriding" a blueprint default). `baseline` is what
|
|
1444
|
+
// the prop falls back to when reset -- the blueprint template value if present,
|
|
1445
|
+
// else the behavior's default. `reset` writes that baseline; for inherited
|
|
1446
|
+
// props `applyComponentPatch` then drops the now-redundant override.
|
|
1447
|
+
function makeOverrideContext({ Behavior, rawComponent, templateProps, setComponent }) {
|
|
1448
|
+
const baselineOf = (prop) =>
|
|
1449
|
+
templateProps && prop in templateProps ? templateProps[prop] : Behavior?.defaultProps?.[prop];
|
|
1450
|
+
return {
|
|
1451
|
+
isOverridden: (prop) =>
|
|
1452
|
+
isInheritedProp(Behavior, prop) && !!rawComponent && prop in rawComponent,
|
|
1453
|
+
// True when the behavior overrides at least one inherited prop -- i.e. it
|
|
1454
|
+
// has a field that renders with the purple override tint. Drives the
|
|
1455
|
+
// matching tint on the panel's title. Position/rotation are inherit:false,
|
|
1456
|
+
// so a position-only instance override doesn't count (nor does it tint any
|
|
1457
|
+
// field), matching isOverridden's semantics.
|
|
1458
|
+
anyOverridden: () =>
|
|
1459
|
+
!!rawComponent && Object.keys(rawComponent).some((prop) => isInheritedProp(Behavior, prop)),
|
|
1460
|
+
baseline: baselineOf,
|
|
1461
|
+
reset: (prop) => setComponent({ [prop]: baselineOf(prop) }),
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1375
1464
|
function ActorInspector({
|
|
1376
1465
|
selectedActor,
|
|
1377
1466
|
rawActor,
|
|
1378
1467
|
blueprintName,
|
|
1468
|
+
blueprintTemplate,
|
|
1469
|
+
sprites,
|
|
1470
|
+
showAddBehavior = true,
|
|
1379
1471
|
files,
|
|
1380
1472
|
onSetComponent,
|
|
1381
1473
|
onAddBehavior,
|
|
1382
1474
|
onRemoveBehavior,
|
|
1383
1475
|
onFork,
|
|
1476
|
+
onEditBlueprint,
|
|
1384
1477
|
}) {
|
|
1385
1478
|
const isInstance = Boolean(rawActor?.blueprint);
|
|
1386
1479
|
const presentNames = new Set(
|
|
@@ -1392,38 +1485,96 @@ function ActorInspector({
|
|
|
1392
1485
|
(candidate) => !presentNames.has(candidate.behaviorName)
|
|
1393
1486
|
);
|
|
1394
1487
|
const behaviorEntries = Object.entries(selectedActor.components).filter((entry) => !!entry[1]);
|
|
1488
|
+
// For an instance, behaviors it doesn't already override are collapsed behind
|
|
1489
|
+
// a reveal button; Layout and any behavior with an instance override stay
|
|
1490
|
+
// visible below the header. Revealing is a pure view toggle -- it never
|
|
1491
|
+
// touches the actor, it just exposes the inherited behaviors' fields so they
|
|
1492
|
+
// can be overridden. Keyed by actor id at the call site, so the collapsed
|
|
1493
|
+
// state resets when a different actor is selected.
|
|
1494
|
+
const [showAllBehaviors, setShowAllBehaviors] = useState(false);
|
|
1495
|
+
const isPinnedBehavior = (behaviorName) =>
|
|
1496
|
+
behaviorName === 'Layout' || Boolean(rawActor?.components?.[behaviorName]);
|
|
1497
|
+
const collapsed = isInstance && !showAllBehaviors;
|
|
1498
|
+
const visibleEntries = collapsed
|
|
1499
|
+
? behaviorEntries.filter(([behaviorName]) => isPinnedBehavior(behaviorName))
|
|
1500
|
+
: behaviorEntries;
|
|
1501
|
+
const showReveal = collapsed && visibleEntries.length < behaviorEntries.length;
|
|
1395
1502
|
return (
|
|
1396
1503
|
<>
|
|
1397
1504
|
{isInstance ? (
|
|
1398
|
-
<div className={styles.
|
|
1399
|
-
<
|
|
1505
|
+
<div className={styles.instanceHeader}>
|
|
1506
|
+
<div className={styles.instanceHeaderName}>
|
|
1507
|
+
<BlueprintThumbnail
|
|
1508
|
+
blueprint={blueprintTemplate ?? { components: selectedActor.components }}
|
|
1509
|
+
sprites={sprites}
|
|
1510
|
+
/>
|
|
1511
|
+
<div className={styles.instanceHeaderNameCol}>
|
|
1512
|
+
<span className={styles.instanceHeaderNameText}>
|
|
1513
|
+
<strong className={styles.instanceHeaderNameStrong}>{blueprintName}</strong> Actor
|
|
1514
|
+
</span>
|
|
1515
|
+
<span className={styles.instanceHeaderId}>id: {selectedActor.id}</span>
|
|
1516
|
+
</div>
|
|
1517
|
+
</div>
|
|
1518
|
+
{onEditBlueprint ? (
|
|
1519
|
+
<button
|
|
1520
|
+
type="button"
|
|
1521
|
+
className={styles.instanceForkButton}
|
|
1522
|
+
onClick={onEditBlueprint}
|
|
1523
|
+
title="Select this blueprint in the hotbar">
|
|
1524
|
+
<Icon name="pencil" />
|
|
1525
|
+
<span>Edit blueprint</span>
|
|
1526
|
+
</button>
|
|
1527
|
+
) : null}
|
|
1400
1528
|
{onFork ? (
|
|
1401
|
-
<
|
|
1529
|
+
<button
|
|
1530
|
+
type="button"
|
|
1531
|
+
className={styles.instanceForkButton}
|
|
1532
|
+
onClick={onFork}
|
|
1533
|
+
title="New blueprint from this actor">
|
|
1534
|
+
<Icon name="code-fork" />
|
|
1535
|
+
<span>Fork new blueprint</span>
|
|
1536
|
+
</button>
|
|
1402
1537
|
) : null}
|
|
1403
1538
|
</div>
|
|
1404
1539
|
) : null}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1540
|
+
{showAddBehavior ? (
|
|
1541
|
+
<AddBehaviorPicker
|
|
1542
|
+
available={availableBehaviors}
|
|
1543
|
+
onAdd={(behaviorName) => onAddBehavior(selectedActor.id, behaviorName)}
|
|
1544
|
+
/>
|
|
1545
|
+
) : null}
|
|
1546
|
+
{visibleEntries.map(([behaviorName, component], index) => {
|
|
1410
1547
|
const Behavior = behaviorClasses.find(
|
|
1411
1548
|
(candidate) => candidate.behaviorName === behaviorName
|
|
1412
1549
|
);
|
|
1413
1550
|
const Inspector = Behavior?.Inspector;
|
|
1551
|
+
const setComponent = (nextProps) =>
|
|
1552
|
+
onSetComponent(selectedActor.id, behaviorName, nextProps);
|
|
1553
|
+
// Instance overrides get a purple tint + "Default: X [Reset]" sub-line.
|
|
1554
|
+
// Null when editing a blueprint template (nothing to override against).
|
|
1555
|
+
const override = isInstance
|
|
1556
|
+
? makeOverrideContext({
|
|
1557
|
+
Behavior,
|
|
1558
|
+
rawComponent: rawActor?.components?.[behaviorName],
|
|
1559
|
+
templateProps: blueprintTemplate?.components?.[behaviorName],
|
|
1560
|
+
setComponent,
|
|
1561
|
+
})
|
|
1562
|
+
: null;
|
|
1414
1563
|
const body = Inspector ? (
|
|
1415
1564
|
<Inspector
|
|
1416
1565
|
actor={selectedActor}
|
|
1417
1566
|
component={component}
|
|
1418
1567
|
files={files}
|
|
1419
|
-
setComponent={
|
|
1568
|
+
setComponent={setComponent}
|
|
1569
|
+
override={override}
|
|
1420
1570
|
/>
|
|
1421
1571
|
) : (
|
|
1422
1572
|
<AutoInspector
|
|
1423
1573
|
behaviorName={behaviorName}
|
|
1424
1574
|
defaultProps={Behavior?.defaultProps ?? component}
|
|
1425
1575
|
component={component}
|
|
1426
|
-
setComponent={
|
|
1576
|
+
setComponent={setComponent}
|
|
1577
|
+
override={override}
|
|
1427
1578
|
/>
|
|
1428
1579
|
);
|
|
1429
1580
|
// Layout is core to every actor -- it cannot be removed. A component
|
|
@@ -1432,7 +1583,9 @@ function ActorInspector({
|
|
|
1432
1583
|
// inherited component" concept (see propertyMeta punt list).
|
|
1433
1584
|
const removable =
|
|
1434
1585
|
behaviorName !== 'Layout' && (!isInstance || Boolean(rawActor.components?.[behaviorName]));
|
|
1435
|
-
|
|
1586
|
+
// When the reveal button follows, keep the last visible panel's divider
|
|
1587
|
+
// so it doesn't merge into the button row.
|
|
1588
|
+
const isLast = index === visibleEntries.length - 1 && !showReveal;
|
|
1436
1589
|
// Trash renders before the panel so the panel stays the wrapper's
|
|
1437
1590
|
// last child -- the panel's own `:last-child` border zeroes out, and
|
|
1438
1591
|
// this wrapper owns the divider between panels deterministically.
|
|
@@ -1469,6 +1622,15 @@ function ActorInspector({
|
|
|
1469
1622
|
</div>
|
|
1470
1623
|
);
|
|
1471
1624
|
})}
|
|
1625
|
+
{showReveal ? (
|
|
1626
|
+
<button
|
|
1627
|
+
type="button"
|
|
1628
|
+
className={styles.revealOverridesButton}
|
|
1629
|
+
onClick={() => setShowAllBehaviors(true)}>
|
|
1630
|
+
<Icon name="chevron-down" />
|
|
1631
|
+
<span>Override behavior properties</span>
|
|
1632
|
+
</button>
|
|
1633
|
+
) : null}
|
|
1472
1634
|
</>
|
|
1473
1635
|
);
|
|
1474
1636
|
}
|