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.
@@ -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 + restart + notes) into
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 the restart hiccups). Then stop -- no extra turns after it.`
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
 
@@ -131,5 +131,5 @@
131
131
  "main": "main.jsx",
132
132
  "autoUpdateWhenImported": true,
133
133
  "title": "physics-2d",
134
- "publishedVersion": "2026-08-21T22:11:52.086Z"
134
+ "publishedVersion": "2026-08-21T22:39:24.016Z"
135
135
  }
@@ -34,6 +34,7 @@ export function PlayOnly({ initialScene } = {}) {
34
34
  sprites={sprites}
35
35
  files={files}
36
36
  behaviorClasses={behaviorClasses}
37
+ scenePath={scenePath}
37
38
  onFirstFrame={Lifecycle.ready}
38
39
  />
39
40
  );
@@ -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
- const key = resolveDeckFile(blueprintPath);
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(this.serialize(), [...this.behaviors.values()], this.sprites, this.files);
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
- **After writing new sprites referenced by the scene, `npm run restart`** so the
94
- deck's file glob picks them up.
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: place a `Portal { scene: 'scenes/other.scene' }` actor (a
47
- sensor -- the portal blueprint is a glowing ring), or set
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
- - After any edit: `npm run restart`. After a coherent change: `npm run check`
79
- (lint + duplicate gate + single-file bundle) must pass.
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 walking back through a portal doesn't respawn (and double-count) shards
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}`;
@@ -71,5 +71,5 @@
71
71
  "autoUpdateWhenImported": true,
72
72
  "deckId": "JH0SclbPVP0y",
73
73
  "cardId": "eXfAaUdbSU5A",
74
- "publishedVersion": "2026-08-21T22:11:54.424Z"
74
+ "publishedVersion": "2026-08-21T22:39:26.451Z"
75
75
  }
@@ -1,10 +1,14 @@
1
1
  // Play-mode entry point, the 3d counterpart of the imported kit's PlayOnly.
2
- // Files, assets, live reload, and the behavior registry all come from the
3
- // physics-2d import (they're renderer-agnostic); only the player is 3d.
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 { useLiveDeckFiles3D } from './modelFiles';
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 { files, dataVersion } = useLiveDeckFiles3D();
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={`${scenePath}:${dataVersion}`}
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 Portal (or any behavior) may have requested a scene switch; apply
204
- // it here, between frames, never mid physics dispatch. The load resets
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.portalGraceUntil = runtime.time + 1;
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, requestReload } from 'castle-web-sdk';
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, setDataVersion) {
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; `dataVersion` bumps on applied data
80
- // changes.
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, setDataVersion);
95
+ void applyDataChanges(dataChanges, setFiles);
99
96
  }),
100
97
  []
101
98
  );
102
99
 
103
- return { files, setFiles, dataVersion };
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,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.130",
3
+ "version": "0.4.132",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"
@@ -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
- }