castle-web-cli 0.4.130 → 0.4.131
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/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-3d/CLAUDE.md +2 -3
- 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
|
@@ -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.
|
|
@@ -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
|
|
@@ -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
|
-
}
|