castle-web-cli 0.4.130 → 0.4.132
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-prompts.js +2 -2
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/editors/PlayOnly.jsx +1 -0
- package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
- package/kits/physics-2d/engine/ScenePlayer.jsx +2 -2
- package/kits/physics-2d/engine/blueprint.js +8 -2
- package/kits/physics-2d/engine/scene.js +17 -5
- package/kits/physics-2d/scripts/svg-emission-guide.md +3 -2
- package/kits/physics-3d/CLAUDE.md +7 -5
- package/kits/physics-3d/behaviors/Pickup.jsx +1 -1
- package/kits/physics-3d/castle.json +1 -1
- package/kits/physics-3d/engine3d/PlayOnly3D.jsx +9 -5
- package/kits/physics-3d/engine3d/Scene3DEditor.jsx +2 -2
- package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +4 -4
- package/kits/physics-3d/engine3d/editorWorld.js +2 -2
- package/kits/physics-3d/engine3d/modelFiles.js +9 -12
- package/kits/physics-3d/scenes/main.scene +0 -16
- package/kits/physics-3d/scenes/model-lab.scene +0 -16
- package/package.json +1 -1
- package/kits/physics-3d/behaviors/Portal.jsx +0 -19
- package/kits/physics-3d/blueprints/portal.scene +0 -21
package/dist/agent-prompts.js
CHANGED
|
@@ -889,7 +889,7 @@ export function buildTaskPrompt(opts) {
|
|
|
889
889
|
? `\n- Before you finish, write these lines to ${opts.handoffPath} -- they are the ONLY record of this task once the task itself is forgotten:\n - \`what: <what now exists, and where>\` -- an existence fact, not a report of activity: "a shop panel in ui/shop.js the bag button opens", never "updated the shop logic". This is what the plan shows as already built, so a later agent extends it instead of rebuilding it.\n - \`files: <the files you created or changed, comma-separated>\`\n - \`knobs: <the values worth tuning, comma-separated>\` -- names only, and only if there are real ones.\n - \`durable: <fact>\` -- the single thing worth remembering that is NOT covered by the three above (a preference the user expressed, a constraint you ran into, an approach that did not work). At most 200 characters. Leave it out only if nothing genuinely qualifies.`
|
|
890
890
|
: "",
|
|
891
891
|
].join("");
|
|
892
|
-
// Claude-only: collapsing the wrap-up (progress 90 +
|
|
892
|
+
// Claude-only: collapsing the wrap-up (progress 90 + notes) into
|
|
893
893
|
// one shell call reliably saves 1-2 serial ~7s turns there. Cursor's
|
|
894
894
|
// composer sometimes reacts to the same rule with MORE calls, so it stays
|
|
895
895
|
// on the plain instructions.
|
|
@@ -900,7 +900,7 @@ export function buildTaskPrompt(opts) {
|
|
|
900
900
|
? "the 90-progress write, the `castle-web save-version` for your paths, writing the notes file, and the handoff lines"
|
|
901
901
|
: "the 90-progress write, the `castle-web save-version` for your paths, and writing the notes file";
|
|
902
902
|
const wrapUp = opts.backend === "claude" || opts.backend === "smith"
|
|
903
|
-
? `\n- Wrap up in ONE tool call, not several: once your last file edit is done, combine ${wrapUpList} into a single shell command (\`;\`-separated so the notes land even if
|
|
903
|
+
? `\n- Wrap up in ONE tool call, not several: once your last file edit is done, combine ${wrapUpList} into a single shell command (\`;\`-separated so the notes land even if an earlier part hiccups). Then stop -- no extra turns after it.`
|
|
904
904
|
: "";
|
|
905
905
|
return `You are a background build agent for the Castle deck "${opts.deckLabel}" (current directory). A separate conversation agent dispatched you with one task. Follow the deck's CLAUDE.md / AGENTS.md conventions. Do not reload the served deck -- the person applies your changes when they are ready. \`npm run restart\` exists, but it is HIGHLY flow breaking: it reloads every panel they have open, including the one they are working in. Use it only when a change clearly requires restarting their whole experience.${quickReference}${layout}${deckSource}
|
|
906
906
|
|
|
@@ -254,7 +254,7 @@ export function SceneEditor({
|
|
|
254
254
|
// Sparse per-instance overrides merged with each actor's blueprint template
|
|
255
255
|
// -- what hit-testing, drag-start snapshots, and the inspector should read.
|
|
256
256
|
// Never the basis for a write (see engine/blueprint.js).
|
|
257
|
-
const previewSceneData = sceneData ? makeScene(sceneData, behaviorClasses, sprites, files).data : null;
|
|
257
|
+
const previewSceneData = sceneData ? makeScene(sceneData, behaviorClasses, sprites, files, path).data : null;
|
|
258
258
|
const serialize = useCallback(
|
|
259
259
|
(next) => formatJson(isBlueprintFile ? stripTemplateId(next) : next),
|
|
260
260
|
[isBlueprintFile]
|
|
@@ -928,7 +928,7 @@ function useSelectionGesture(args) {
|
|
|
928
928
|
const cam = current.editCameraRef.current;
|
|
929
929
|
const point = { x: raw.x + cam.x, y: raw.y + cam.y };
|
|
930
930
|
current.canvasRef.current.setPointerCapture(event.pointerId);
|
|
931
|
-
const scene = makeScene(current.sceneData, behaviorClasses, current.sprites, current.files);
|
|
931
|
+
const scene = makeScene(current.sceneData, behaviorClasses, current.sprites, current.files, current.path);
|
|
932
932
|
const stack = scene.actorsAt(point.x, point.y);
|
|
933
933
|
const stackIds = stack.map((a) => a.id);
|
|
934
934
|
const actor = stack[0] ?? null;
|
|
@@ -1408,7 +1408,7 @@ function finalizeMarquee(drag, current) {
|
|
|
1408
1408
|
const m = current.marqueeRef.current;
|
|
1409
1409
|
if (!m || !current.sceneData) return;
|
|
1410
1410
|
if (m.width === 0 && m.height === 0) return;
|
|
1411
|
-
const scene = makeScene(current.sceneData, behaviorClasses, current.sprites, current.files);
|
|
1411
|
+
const scene = makeScene(current.sceneData, behaviorClasses, current.sprites, current.files, current.path);
|
|
1412
1412
|
const hits = scene.actorIdsInRect(m);
|
|
1413
1413
|
const merged = new Set(drag.selectionBeforeMarquee);
|
|
1414
1414
|
for (const id of hits) merged.add(id);
|
|
@@ -1511,7 +1511,7 @@ function useScenePlayLoop({
|
|
|
1511
1511
|
return undefined;
|
|
1512
1512
|
}
|
|
1513
1513
|
if (!sceneData) return undefined;
|
|
1514
|
-
const runtime = makeScene(sceneData, behaviorClasses, sprites, files).clone();
|
|
1514
|
+
const runtime = makeScene(sceneData, behaviorClasses, sprites, files, path).clone();
|
|
1515
1515
|
runtimeRef.current = runtime;
|
|
1516
1516
|
// Pressing Stop has to stop what the runtime started -- a sound keeps
|
|
1517
1517
|
// playing on its own otherwise, with no scene left to stop it.
|
|
@@ -1540,7 +1540,7 @@ function useScenePlayLoop({
|
|
|
1540
1540
|
: editViewportForZoom(editCameraRef.current.zoom);
|
|
1541
1541
|
const scene = isPlaying
|
|
1542
1542
|
? runtimeRef.current
|
|
1543
|
-
: makeScene(sceneData, behaviorClasses, sprites, files);
|
|
1543
|
+
: makeScene(sceneData, behaviorClasses, sprites, files, path);
|
|
1544
1544
|
if (scene) {
|
|
1545
1545
|
const snap = getSnapSettings(sceneData);
|
|
1546
1546
|
if (isPlaying) scene.update(dt);
|
|
@@ -6,7 +6,7 @@ import { SceneUI } from './SceneUI';
|
|
|
6
6
|
// behavior-driven UI overlay. No game logic lives here -- behaviors and
|
|
7
7
|
// scenes are the place for that. The dev logs drawer is a builtin shell panel
|
|
8
8
|
// now (cli/src/shell), not rendered here.
|
|
9
|
-
export function ScenePlayer({ sceneData, sprites, files, behaviorClasses, onFirstFrame }) {
|
|
9
|
+
export function ScenePlayer({ sceneData, sprites, files, behaviorClasses, scenePath, onFirstFrame }) {
|
|
10
10
|
const canvasRef = useRef(null);
|
|
11
11
|
const runtimeRef = useRef(null);
|
|
12
12
|
// An error thrown from a behavior's update/draw kills the raf loop (it can't
|
|
@@ -22,7 +22,7 @@ export function ScenePlayer({ sceneData, sprites, files, behaviorClasses, onFirs
|
|
|
22
22
|
const ctx = canvas.getContext('2d');
|
|
23
23
|
if (!ctx) return undefined;
|
|
24
24
|
configureSceneCanvas(canvas, ctx);
|
|
25
|
-
const runtime = makeScene(sceneData, behaviorClasses, sprites, files).clone();
|
|
25
|
+
const runtime = makeScene(sceneData, behaviorClasses, sprites, files, scenePath).clone();
|
|
26
26
|
runtimeRef.current = runtime;
|
|
27
27
|
// Store BOTH the physical code ('KeyX', 'ArrowLeft', 'Space') and the
|
|
28
28
|
// logical key ('x', 'ArrowLeft', ' ') so behaviors can match either. Codes
|
|
@@ -94,10 +94,16 @@ function resolveRefsIn(props, files, fromPath) {
|
|
|
94
94
|
// call for live propagation. Returns null when the file is missing, deleted,
|
|
95
95
|
// or malformed (a dangling `blueprint` ref degrades to "no template" rather
|
|
96
96
|
// than throwing).
|
|
97
|
-
export function getBlueprintTemplate(files, blueprintPath) {
|
|
97
|
+
export function getBlueprintTemplate(files, blueprintPath, fromPath) {
|
|
98
98
|
// The ref may name this deck's blueprint or an import's; the template's own
|
|
99
99
|
// refs then resolve against whichever deck the blueprint turned out to be in.
|
|
100
|
-
|
|
100
|
+
//
|
|
101
|
+
// `fromPath` is the file the ref was WRITTEN in, and it matters as soon as a
|
|
102
|
+
// kit is opened as an import: a scene at `imports/<alias>/scenes/main.scene`
|
|
103
|
+
// says `blueprints/wall.scene` meaning its own deck's, and resolving that
|
|
104
|
+
// against the deck root finds nothing -- every actor silently loses its
|
|
105
|
+
// template and renders as an untextured default.
|
|
106
|
+
const key = resolveDeckFile(blueprintPath, fromPath);
|
|
101
107
|
const template = parseBlueprintText(files?.[key]);
|
|
102
108
|
return resolveTemplateRefs(template, files, key);
|
|
103
109
|
}
|
|
@@ -35,7 +35,12 @@ export class SceneRuntime {
|
|
|
35
35
|
// actor's `blueprint` ref against the CURRENT blueprint file content -- not
|
|
36
36
|
// a load-time snapshot -- so a blueprint edit propagates to every scene that
|
|
37
37
|
// places it, in both the editor and play mode / `PlayOnly`.
|
|
38
|
-
constructor(sceneData, behaviors, sprites, files) {
|
|
38
|
+
constructor(sceneData, behaviors, sprites, files, scenePath) {
|
|
39
|
+
// The file this scene was read from, so blueprint refs in it resolve the
|
|
40
|
+
// way its own deck writes them. Undefined when a caller hands over scene
|
|
41
|
+
// data with no file behind it; refs then resolve against the deck root,
|
|
42
|
+
// which is what they meant before this existed.
|
|
43
|
+
this.scenePath = scenePath;
|
|
39
44
|
this.behaviors = new Map(behaviors.map((Behavior) => [Behavior.behaviorName, Behavior]));
|
|
40
45
|
this.sprites = sprites ?? {};
|
|
41
46
|
this.files = files ?? {};
|
|
@@ -87,7 +92,7 @@ export class SceneRuntime {
|
|
|
87
92
|
for (const actor of this.data.actors ?? []) {
|
|
88
93
|
actor.runtime = {};
|
|
89
94
|
if (actor.blueprint) {
|
|
90
|
-
const template = getBlueprintTemplate(this.files, actor.blueprint);
|
|
95
|
+
const template = getBlueprintTemplate(this.files, actor.blueprint, this.scenePath);
|
|
91
96
|
actor.components = mergeComponents(template?.components, actor.components);
|
|
92
97
|
}
|
|
93
98
|
this.actors.set(actor.id, actor);
|
|
@@ -120,13 +125,20 @@ export class SceneRuntime {
|
|
|
120
125
|
// Transition the running scene to the scene stored in the named file (read it
|
|
121
126
|
// fresh, then load it). The building block for "go to scene" / restart.
|
|
122
127
|
loadFromFile(name) {
|
|
128
|
+
this.scenePath = resolveSceneFileKey(name);
|
|
123
129
|
this.load(this.readFromFile(name));
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
clone() {
|
|
127
133
|
// Route through makeScene so the clone gets the same registered systems as a
|
|
128
134
|
// freshly-made runtime.
|
|
129
|
-
return makeScene(
|
|
135
|
+
return makeScene(
|
|
136
|
+
this.serialize(),
|
|
137
|
+
[...this.behaviors.values()],
|
|
138
|
+
this.sprites,
|
|
139
|
+
this.files,
|
|
140
|
+
this.scenePath
|
|
141
|
+
);
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
serialize() {
|
|
@@ -426,8 +438,8 @@ export class SceneRuntime {
|
|
|
426
438
|
}
|
|
427
439
|
}
|
|
428
440
|
|
|
429
|
-
export function makeScene(sceneData, behaviors, sprites, files) {
|
|
430
|
-
const runtime = new SceneRuntime(sceneData, behaviors, sprites, files);
|
|
441
|
+
export function makeScene(sceneData, behaviors, sprites, files, scenePath) {
|
|
442
|
+
const runtime = new SceneRuntime(sceneData, behaviors, sprites, files, scenePath);
|
|
431
443
|
// Install any runtime systems the kit provides (systems/*.js) -- no-op in a kit
|
|
432
444
|
// with none. A system's engine is created lazily on first use, so draw-only
|
|
433
445
|
// editor previews (which never call update) stay free.
|
|
@@ -90,5 +90,6 @@ A curvy silhouette with an overlay accent (later shape covers earlier stroke):
|
|
|
90
90
|
</svg>
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
A newly written sprite is not in the deck's file glob until the panel reloads,
|
|
94
|
+
so an actor pointing at one renders the placeholder until then. That is fine --
|
|
95
|
+
the person reloads when they are ready; do not force it.
|
|
@@ -43,9 +43,8 @@ bottom }`; any ref can pick a frame of a multi-frame pxart with
|
|
|
43
43
|
near), `Lookable { text }` (hud caption when the player faces it nearby).
|
|
44
44
|
- Hud: behaviors write `scene.hud = { score, look }`; the player behavior
|
|
45
45
|
already does.
|
|
46
|
-
- Scene switching:
|
|
47
|
-
|
|
48
|
-
`scene.requestedScene = 'scenes/other.scene'` from any behavior (applied
|
|
46
|
+
- Scene switching: set `scene.requestedScene = 'scenes/other.scene'` from any
|
|
47
|
+
behavior (applied
|
|
49
48
|
between frames; safer than calling `scene.loadFromFile` mid-callback). A
|
|
50
49
|
load rebuilds the physics world and resets the scene to its authored state;
|
|
51
50
|
`scene.persistent` survives loads -- the player's score, collected-pickup
|
|
@@ -75,8 +74,11 @@ bottom }`; any ref can pick a frame of a multi-frame pxart with
|
|
|
75
74
|
- Controls are tank-style: W/S (or up/down) walk forward/back along facing,
|
|
76
75
|
A/D (or left/right) turn; a pointer/touch drag is a virtual stick where the
|
|
77
76
|
drag angle blends walking and turning. Space jumps.
|
|
78
|
-
-
|
|
79
|
-
|
|
77
|
+
- Do not reload. Data edits (scenes, drawings, models) appear in the open
|
|
78
|
+
editors on their own. Code edits wait for the person to apply them --
|
|
79
|
+
reloading a panel someone is working in loses their place.
|
|
80
|
+
- After a coherent change: `npm run check` (lint + duplicate gate +
|
|
81
|
+
single-file bundle) must pass.
|
|
80
82
|
|
|
81
83
|
## What lives where
|
|
82
84
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// instead of blocking, which is what fires onCollisionEnter here.
|
|
4
4
|
//
|
|
5
5
|
// Collection is remembered in the persistent store keyed by scene + actor id,
|
|
6
|
-
// so
|
|
6
|
+
// so re-entering a scene doesn't respawn (and double-count) shards
|
|
7
7
|
// -- scene loads otherwise reset the world to its authored state.
|
|
8
8
|
function pickupKey(scene, actor) {
|
|
9
9
|
return `${scene.currentScenePath ?? ''}:${actor.id}`;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// Play-mode entry point, the 3d counterpart of the imported kit's PlayOnly.
|
|
2
|
-
// Files, assets
|
|
3
|
-
//
|
|
2
|
+
// Files, assets and the behavior registry all come from the physics-2d import
|
|
3
|
+
// (they're renderer-agnostic); only the player is 3d.
|
|
4
|
+
//
|
|
5
|
+
// Nothing is folded into a run in progress: a file changing on disk can always
|
|
6
|
+
// have altered how the run would have gone up to this point. Getting current is
|
|
7
|
+
// a whole-panel reload, and the shell decides when.
|
|
4
8
|
import React from 'react';
|
|
5
9
|
import { Lifecycle } from 'castle-web-sdk';
|
|
6
10
|
import { parseJsonFile } from '@imports/castle.physics-2d/engine/files';
|
|
7
|
-
import {
|
|
11
|
+
import { initialFiles3D } from './modelFiles';
|
|
8
12
|
import { collectAssets } from '@imports/castle.physics-2d/engine/assets';
|
|
9
13
|
import { behaviorClasses } from '@imports/castle.physics-2d/editors/behaviorRegistry';
|
|
10
14
|
import { Scene3DPlayer } from './Scene3DPlayer';
|
|
@@ -12,7 +16,7 @@ import { Scene3DPlayer } from './Scene3DPlayer';
|
|
|
12
16
|
const DEFAULT_SCENE = 'scenes/main.scene';
|
|
13
17
|
|
|
14
18
|
export function PlayOnly3D({ initialScene } = {}) {
|
|
15
|
-
const
|
|
19
|
+
const files = initialFiles3D;
|
|
16
20
|
const scenePath =
|
|
17
21
|
initialScene && files[initialScene] !== undefined ? initialScene : DEFAULT_SCENE;
|
|
18
22
|
const { value: sceneData } = parseJsonFile(scenePath, files[scenePath] ?? '');
|
|
@@ -20,7 +24,7 @@ export function PlayOnly3D({ initialScene } = {}) {
|
|
|
20
24
|
const { sprites } = collectAssets(files);
|
|
21
25
|
return (
|
|
22
26
|
<Scene3DPlayer
|
|
23
|
-
key={
|
|
27
|
+
key={scenePath}
|
|
24
28
|
sceneData={sceneData}
|
|
25
29
|
sprites={sprites}
|
|
26
30
|
files={files}
|
|
@@ -112,7 +112,7 @@ export function Scene3DEditor({ path }) {
|
|
|
112
112
|
const sceneData = withTemplateId(parsed, isBlueprintFile);
|
|
113
113
|
const sprites = useStableSprites(files);
|
|
114
114
|
const previewSceneData = sceneData
|
|
115
|
-
? makeScene(sceneData, behaviorClasses, sprites, files).data
|
|
115
|
+
? makeScene(sceneData, behaviorClasses, sprites, files, path).data
|
|
116
116
|
: null;
|
|
117
117
|
const serialize = (next) => formatJson(isBlueprintFile ? stripTemplateId(next) : next);
|
|
118
118
|
const commitScene = (next, options) => history.commit(serialize(next), options);
|
|
@@ -168,7 +168,7 @@ export function Scene3DEditor({ path }) {
|
|
|
168
168
|
if (ed.lastAppliedText === text && ed.lastDataVersion === dataVersion) return;
|
|
169
169
|
ed.lastAppliedText = text;
|
|
170
170
|
ed.lastDataVersion = dataVersion;
|
|
171
|
-
setEditorScene(ed, sceneData, behaviorClasses, sprites, files);
|
|
171
|
+
setEditorScene(ed, sceneData, behaviorClasses, sprites, files, path);
|
|
172
172
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
173
173
|
}, [text, dataVersion]);
|
|
174
174
|
useEffect(() => {
|
|
@@ -30,7 +30,7 @@ export function Scene3DPlayer({
|
|
|
30
30
|
useEffect(() => {
|
|
31
31
|
const canvas = canvasRef.current;
|
|
32
32
|
if (!canvas) return undefined;
|
|
33
|
-
const runtime = makeScene(sceneData, behaviorClasses, sprites, files);
|
|
33
|
+
const runtime = makeScene(sceneData, behaviorClasses, sprites, files, scenePath);
|
|
34
34
|
const world = makeWorld3D(canvas);
|
|
35
35
|
runtime.three = world;
|
|
36
36
|
runtime.hud = {};
|
|
@@ -200,8 +200,8 @@ function startLoop(runtime, world, sprites, hudRefs, onFirstFrame, onError) {
|
|
|
200
200
|
previousTime = now;
|
|
201
201
|
try {
|
|
202
202
|
runtime.update(dt);
|
|
203
|
-
// A
|
|
204
|
-
//
|
|
203
|
+
// A behavior may have requested a scene switch; apply it here, between
|
|
204
|
+
// frames, never mid physics dispatch. The load resets
|
|
205
205
|
// registered systems -- the rapier world frees and lazily rebuilds --
|
|
206
206
|
// while `runtime.persistent` carries the player's cross-scene state.
|
|
207
207
|
if (runtime.requestedScene) {
|
|
@@ -209,7 +209,7 @@ function startLoop(runtime, world, sprites, hudRefs, onFirstFrame, onError) {
|
|
|
209
209
|
runtime.requestedScene = null;
|
|
210
210
|
runtime.loadFromFile(next);
|
|
211
211
|
runtime.currentScenePath = next;
|
|
212
|
-
runtime.persistent.
|
|
212
|
+
runtime.persistent.sceneChangeGraceUntil = runtime.time + 1;
|
|
213
213
|
}
|
|
214
214
|
syncActors(world, runtime, sprites, dt);
|
|
215
215
|
if (world.debugLines.visible) updatePhysicsDebug(world, runtime.physics3d?.debugRender());
|
|
@@ -50,9 +50,9 @@ export function createEditorWorld(canvas, callbacks) {
|
|
|
50
50
|
// Swap in fresh scene data (parsed file JSON). The display runtime is never
|
|
51
51
|
// stepped -- actors sit at their authored Transforms; blueprints and default
|
|
52
52
|
// props merge exactly as in play.
|
|
53
|
-
export function setEditorScene(ed, sceneData, behaviors, sprites, files) {
|
|
53
|
+
export function setEditorScene(ed, sceneData, behaviors, sprites, files, scenePath) {
|
|
54
54
|
ed.runtime?.dispose();
|
|
55
|
-
ed.runtime = makeScene(sceneData, behaviors, sprites, files);
|
|
55
|
+
ed.runtime = makeScene(sceneData, behaviors, sprites, files, scenePath);
|
|
56
56
|
ed.sprites = sprites;
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// the files API instead of forcing a context reload (the glob puts models in
|
|
7
7
|
// vite's module graph, which the imported hook would read as "code changed").
|
|
8
8
|
import { useEffect, useRef, useState } from 'react';
|
|
9
|
-
import { onFilesChanged
|
|
9
|
+
import { onFilesChanged } from 'castle-web-sdk';
|
|
10
10
|
import { initialFiles, initialMedia, isImageFile } from '@imports/castle.physics-2d/engine/files';
|
|
11
11
|
import { parseFull } from '@imports/castle.physics-2d/engine/pxart';
|
|
12
12
|
import { imageArt } from '@imports/castle.physics-2d/engine/art';
|
|
@@ -50,7 +50,7 @@ async function readDeckFile(path) {
|
|
|
50
50
|
return typeof data.contents === 'string' ? data.contents : '';
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async function applyDataChanges(changes, setFiles
|
|
53
|
+
async function applyDataChanges(changes, setFiles) {
|
|
54
54
|
const updates = await Promise.all(
|
|
55
55
|
changes.map(async (change) => {
|
|
56
56
|
if (change.event === 'delete') return { path: change.path, text: null };
|
|
@@ -71,36 +71,33 @@ async function applyDataChanges(changes, setFiles, setDataVersion) {
|
|
|
71
71
|
}
|
|
72
72
|
return next;
|
|
73
73
|
});
|
|
74
|
-
setDataVersion((version) => version + 1);
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
// Live deck files including models. Same contract as the imported
|
|
78
77
|
// useLiveDeckFiles: `shouldSkipPath` keeps an editor's in-flight writes from
|
|
79
|
-
// being clobbered by their own fs echo
|
|
80
|
-
//
|
|
78
|
+
// being clobbered by their own fs echo.
|
|
79
|
+
//
|
|
80
|
+
// A code change does nothing here, matching the imported hook: only a page
|
|
81
|
+
// reload picks up new code, and throwing one at a panel someone is working in
|
|
82
|
+
// loses their place, so the panel header offers it instead.
|
|
81
83
|
export function useLiveDeckFiles3D({ shouldSkipPath } = {}) {
|
|
82
84
|
const [files, setFiles] = useState(initialFiles3D);
|
|
83
|
-
const [dataVersion, setDataVersion] = useState(0);
|
|
84
85
|
const skipRef = useRef(shouldSkipPath);
|
|
85
86
|
skipRef.current = shouldSkipPath;
|
|
86
87
|
|
|
87
88
|
useEffect(
|
|
88
89
|
() =>
|
|
89
90
|
onFilesChanged((event) => {
|
|
90
|
-
if (event.changes.some(needsContextReload)) {
|
|
91
|
-
requestReload();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
91
|
const dataChanges = event.changes.filter(
|
|
95
92
|
(change) => isDataFile3D(change.path) && !skipRef.current?.(change.path)
|
|
96
93
|
);
|
|
97
94
|
if (dataChanges.length === 0) return;
|
|
98
|
-
void applyDataChanges(dataChanges, setFiles
|
|
95
|
+
void applyDataChanges(dataChanges, setFiles);
|
|
99
96
|
}),
|
|
100
97
|
[]
|
|
101
98
|
);
|
|
102
99
|
|
|
103
|
-
return { files, setFiles
|
|
100
|
+
return { files, setFiles };
|
|
104
101
|
}
|
|
105
102
|
|
|
106
103
|
// The sprites map with parsed pxarts CACHED BY TEXT: re-parsing every file on
|
|
@@ -372,22 +372,6 @@
|
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
374
|
},
|
|
375
|
-
{
|
|
376
|
-
"id": "portal-lab",
|
|
377
|
-
"blueprint": "blueprints/portal.scene",
|
|
378
|
-
"components": {
|
|
379
|
-
"Transform": {
|
|
380
|
-
"x": -8,
|
|
381
|
-
"z": 2
|
|
382
|
-
},
|
|
383
|
-
"Portal": {
|
|
384
|
-
"scene": "scenes/model-lab.scene"
|
|
385
|
-
},
|
|
386
|
-
"Lookable": {
|
|
387
|
-
"text": "a portal to the model lab."
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
},
|
|
391
375
|
{
|
|
392
376
|
"id": "crate",
|
|
393
377
|
"blueprint": "blueprints/crate.scene",
|
|
@@ -106,22 +106,6 @@
|
|
|
106
106
|
"rotationY": 140
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
"id": "portal-courtyard",
|
|
112
|
-
"blueprint": "blueprints/portal.scene",
|
|
113
|
-
"components": {
|
|
114
|
-
"Transform": {
|
|
115
|
-
"x": -2,
|
|
116
|
-
"z": 8.6
|
|
117
|
-
},
|
|
118
|
-
"Portal": {
|
|
119
|
-
"scene": "scenes/main.scene"
|
|
120
|
-
},
|
|
121
|
-
"Lookable": {
|
|
122
|
-
"text": "a portal back to the courtyard."
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
109
|
}
|
|
126
110
|
]
|
|
127
111
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// Walk-through scene switching: a sensor actor (a glowing ring works well)
|
|
2
|
-
// that asks the runtime to load another scene when the player touches it.
|
|
3
|
-
// The request is applied by the player loop BETWEEN frames -- never mid
|
|
4
|
-
// physics dispatch -- and a short grace after each switch keeps a portal from
|
|
5
|
-
// instantly bouncing the player back where they came from.
|
|
6
|
-
export class Portal {
|
|
7
|
-
static behaviorName = 'Portal';
|
|
8
|
-
static defaultProps = { scene: '' };
|
|
9
|
-
|
|
10
|
-
constructor(props) {
|
|
11
|
-
this.props = props;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
onCollisionEnter(actor, scene, other) {
|
|
15
|
-
if (!other.components.Player || !this.props.scene) return;
|
|
16
|
-
if (scene.time < (scene.persistent?.portalGraceUntil ?? 0)) return;
|
|
17
|
-
scene.requestedScene = this.props.scene;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Portal",
|
|
3
|
-
"actors": [
|
|
4
|
-
{
|
|
5
|
-
"components": {
|
|
6
|
-
"Transform": { "y": 1.05, "rotationX": 90 },
|
|
7
|
-
"Shape": {
|
|
8
|
-
"kind": "cylinder",
|
|
9
|
-
"radius": 0.9,
|
|
10
|
-
"height": 0.16,
|
|
11
|
-
"color": "#0cf1ff",
|
|
12
|
-
"emissive": "#0cf1ff",
|
|
13
|
-
"emissiveIntensity": 0.65,
|
|
14
|
-
"roughness": 0.3
|
|
15
|
-
},
|
|
16
|
-
"Portal": {},
|
|
17
|
-
"Lookable": { "text": "a portal.", "range": 4.5 }
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
}
|