castle-web-cli 0.4.129 → 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/dist/agent-prompts.js +4 -4
- package/dist/filesChanged.d.ts +2 -0
- package/dist/filesChanged.js +6 -2
- package/dist/ide.js +15 -4
- package/dist/importBrowse.js +1 -0
- package/dist/index.js +10 -4
- package/dist/init.js +63 -15
- package/dist/native/loop.js +1 -1
- package/dist/native/tools.js +1 -1
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +20 -1
- package/dist/shell/assets/index-BpOLUyVO.js +441 -0
- package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/CLAUDE.md +9 -10
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/editors/PlayOnly.jsx +16 -6
- package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
- package/kits/physics-2d/editors/SceneEditor.jsx +10 -10
- package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
- package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
- package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
- package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
- package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
- package/kits/physics-2d/engine/ScenePlayer.jsx +2 -2
- package/kits/physics-2d/engine/blueprint.js +23 -5
- package/kits/physics-2d/engine/liveReload.js +33 -21
- package/kits/physics-2d/engine/scene.js +17 -5
- package/kits/physics-2d/engine/systemRegistry.js +1 -1
- package/kits/physics-2d/engine/ui.jsx +2 -2
- package/kits/physics-2d/engine/ui.module.css +42 -0
- package/kits/physics-3d/CLAUDE.md +108 -0
- package/kits/physics-3d/behaviors/Body.jsx +51 -0
- package/kits/physics-3d/behaviors/Door.jsx +31 -0
- package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
- package/kits/physics-3d/behaviors/Model.jsx +14 -0
- package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
- package/kits/physics-3d/behaviors/Player.jsx +172 -0
- package/kits/physics-3d/behaviors/Shape.jsx +75 -0
- package/kits/physics-3d/behaviors/Solid.jsx +11 -0
- package/kits/physics-3d/behaviors/Transform.jsx +24 -0
- package/kits/physics-3d/blueprints/barrel.scene +15 -0
- package/kits/physics-3d/blueprints/crate.scene +29 -0
- package/kits/physics-3d/blueprints/door.scene +35 -0
- package/kits/physics-3d/blueprints/gem.scene +26 -0
- package/kits/physics-3d/blueprints/pillar.scene +25 -0
- package/kits/physics-3d/blueprints/platform.scene +23 -0
- package/kits/physics-3d/blueprints/player.scene +29 -0
- package/kits/physics-3d/blueprints/rock.scene +14 -0
- package/kits/physics-3d/blueprints/statue.scene +26 -0
- package/kits/physics-3d/blueprints/tree.scene +14 -0
- package/kits/physics-3d/blueprints/wall.scene +26 -0
- package/kits/{basic-2d → physics-3d}/castle.json +23 -5
- package/kits/physics-3d/docs/pxmodel-format.md +111 -0
- package/kits/physics-3d/drawings/crate.pxart +26 -0
- package/kits/physics-3d/drawings/door-edge.pxart +26 -0
- package/kits/physics-3d/drawings/door.pxart +27 -0
- package/kits/physics-3d/drawings/face.pxart +26 -0
- package/kits/physics-3d/drawings/floor.pxart +27 -0
- package/kits/physics-3d/drawings/platform.pxart +27 -0
- package/kits/physics-3d/drawings/statue.pxart +27 -0
- package/kits/physics-3d/drawings/wall.pxart +27 -0
- package/kits/physics-3d/engine3d/PlayOnly3D.jsx +36 -0
- package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
- package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
- package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
- package/kits/physics-3d/engine3d/editor3dData.js +114 -0
- package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
- package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
- package/kits/physics-3d/engine3d/editorWorld.js +249 -0
- package/kits/physics-3d/engine3d/materials.js +174 -0
- package/kits/physics-3d/engine3d/meshops.js +123 -0
- package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
- package/kits/physics-3d/engine3d/modelFiles.js +136 -0
- package/kits/physics-3d/engine3d/pxmodel.js +265 -0
- package/kits/physics-3d/engine3d/thumbnails.js +137 -0
- package/kits/physics-3d/engine3d/world3d.js +216 -0
- package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
- package/kits/physics-3d/index.html +20 -0
- package/kits/physics-3d/main.jsx +30 -0
- package/kits/physics-3d/models/barrel.pxmodel +10 -0
- package/kits/physics-3d/models/player.pxmodel +222 -0
- package/kits/physics-3d/models/rock.pxmodel +30 -0
- package/kits/physics-3d/models/tree.pxmodel +10 -0
- package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
- package/kits/{basic-2d → physics-3d}/package.json +11 -9
- package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
- package/kits/physics-3d/scenes/main.scene +398 -0
- package/kits/physics-3d/scenes/model-lab.scene +111 -0
- package/kits/physics-3d/systems/physics3d.js +356 -0
- package/package.json +5 -2
- package/dist/shell/assets/index-DKu9ejyh.js +0 -436
- package/kits/basic-2d/CLAUDE.md +0 -221
- package/kits/basic-2d/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d/behaviors/Collider.jsx +0 -213
- package/kits/basic-2d/behaviors/Layout.jsx +0 -53
- package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
- package/kits/basic-2d/behaviors/tint.js +0 -47
- package/kits/basic-2d/blueprints/cauldron.scene +0 -20
- package/kits/basic-2d/docs/pxart-format.md +0 -377
- package/kits/basic-2d/drawings/cauldron.pxart +0 -113
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
- package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
- package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
- package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
- package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
- package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
- package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
- package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
- package/kits/basic-2d/editors/editorHistory.js +0 -157
- package/kits/basic-2d/editors/inspectorSheet.js +0 -13
- package/kits/basic-2d/editors/pixelCanvas.js +0 -11
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
- package/kits/basic-2d/editors/pixelGeometry.js +0 -140
- package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
- package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
- package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
- package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
- package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
- package/kits/basic-2d/editors/pxArtTools.js +0 -232
- package/kits/basic-2d/editors/useArtboardFit.js +0 -105
- package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
- package/kits/basic-2d/engine/SceneUI.jsx +0 -59
- package/kits/basic-2d/engine/assets.js +0 -15
- package/kits/basic-2d/engine/autoInspector.jsx +0 -70
- package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
- package/kits/basic-2d/engine/blueprint.js +0 -557
- package/kits/basic-2d/engine/collider.js +0 -200
- package/kits/basic-2d/engine/files.js +0 -141
- package/kits/basic-2d/engine/liveReload.js +0 -88
- package/kits/basic-2d/engine/pxart.js +0 -1032
- package/kits/basic-2d/engine/pxartSmooth.js +0 -222
- package/kits/basic-2d/engine/scene.js +0 -696
- package/kits/basic-2d/engine/spriteGeometry.js +0 -32
- package/kits/basic-2d/engine/systemRegistry.js +0 -16
- package/kits/basic-2d/engine/ui.jsx +0 -695
- package/kits/basic-2d/engine/ui.module.css +0 -2287
- package/kits/basic-2d/index.html +0 -24
- package/kits/basic-2d/main.jsx +0 -24
- package/kits/basic-2d/scenes/main.scene +0 -16
- package/kits/basic-2d/scripts/draw.mjs +0 -121
- /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
- /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// 3d scene player: mounts a SceneRuntime (from the imported physics-2d kit's
|
|
2
|
+
// engine -- the actor/behavior model is shared) against a three.js world.
|
|
3
|
+
// Behaviors update, the physics3d system steps rapier, then Transforms sync to
|
|
4
|
+
// three nodes and the composer renders. No game logic here.
|
|
5
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { makeScene } from '@imports/castle.physics-2d/engine/scene';
|
|
7
|
+
import {
|
|
8
|
+
makeWorld3D,
|
|
9
|
+
resizeWorld,
|
|
10
|
+
syncActors,
|
|
11
|
+
renderWorld,
|
|
12
|
+
disposeWorld,
|
|
13
|
+
updatePhysicsDebug,
|
|
14
|
+
} from './world3d';
|
|
15
|
+
|
|
16
|
+
export function Scene3DPlayer({
|
|
17
|
+
sceneData,
|
|
18
|
+
sprites,
|
|
19
|
+
files,
|
|
20
|
+
behaviorClasses,
|
|
21
|
+
scenePath,
|
|
22
|
+
onFirstFrame,
|
|
23
|
+
}) {
|
|
24
|
+
const canvasRef = useRef(null);
|
|
25
|
+
const scoreRef = useRef(null);
|
|
26
|
+
const lookRef = useRef(null);
|
|
27
|
+
const [error, setError] = useState(null);
|
|
28
|
+
const [runId, setRunId] = useState(0);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const canvas = canvasRef.current;
|
|
32
|
+
if (!canvas) return undefined;
|
|
33
|
+
const runtime = makeScene(sceneData, behaviorClasses, sprites, files, scenePath);
|
|
34
|
+
const world = makeWorld3D(canvas);
|
|
35
|
+
runtime.three = world;
|
|
36
|
+
runtime.hud = {};
|
|
37
|
+
// Scene-independent state: survives `loadFromFile` (the scene data is
|
|
38
|
+
// replaced, the runtime instance is not). Player score/inventory and the
|
|
39
|
+
// camera angle live here so they cross scene switches.
|
|
40
|
+
runtime.persistent = {};
|
|
41
|
+
runtime.currentScenePath = scenePath ?? 'scenes/main.scene';
|
|
42
|
+
|
|
43
|
+
const detachInput = attachInput(canvas, runtime, world);
|
|
44
|
+
const stopResize = attachResize(canvas, world);
|
|
45
|
+
const onLoopError = (err) => {
|
|
46
|
+
console.error(err);
|
|
47
|
+
setError(err);
|
|
48
|
+
};
|
|
49
|
+
const hudRefs = { score: scoreRef, look: lookRef };
|
|
50
|
+
const stopLoop = startLoop(runtime, world, sprites, hudRefs, onFirstFrame, onLoopError);
|
|
51
|
+
return () => {
|
|
52
|
+
stopLoop();
|
|
53
|
+
stopResize();
|
|
54
|
+
detachInput();
|
|
55
|
+
runtime.dispose();
|
|
56
|
+
disposeWorld(world);
|
|
57
|
+
};
|
|
58
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
|
+
}, [runId]);
|
|
60
|
+
|
|
61
|
+
const onRestart = useCallback(() => {
|
|
62
|
+
setError(null);
|
|
63
|
+
setRunId((n) => n + 1);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div style={{ position: 'fixed', inset: 0, background: '#000' }}>
|
|
68
|
+
<canvas
|
|
69
|
+
ref={canvasRef}
|
|
70
|
+
tabIndex={0}
|
|
71
|
+
style={{
|
|
72
|
+
width: '100%',
|
|
73
|
+
height: '100%',
|
|
74
|
+
display: 'block',
|
|
75
|
+
outline: 'none',
|
|
76
|
+
touchAction: 'none',
|
|
77
|
+
userSelect: 'none',
|
|
78
|
+
WebkitUserSelect: 'none',
|
|
79
|
+
WebkitTouchCallout: 'none',
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
<div ref={scoreRef} style={hudStyles.score} />
|
|
83
|
+
<div ref={lookRef} style={hudStyles.look} />
|
|
84
|
+
{error ? <LoopErrorOverlay error={error} onRestart={onRestart} /> : null}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function attachInput(canvas, runtime, world) {
|
|
90
|
+
const onKeyDown = (event) => {
|
|
91
|
+
// Space is gameplay (jump); keep it from scrolling the page.
|
|
92
|
+
if (event.code === 'Space' && event.target === canvas) event.preventDefault();
|
|
93
|
+
if (event.code === 'KeyP' && !event.repeat) {
|
|
94
|
+
world.debugLines.visible = !world.debugLines.visible;
|
|
95
|
+
}
|
|
96
|
+
runtime.keys.add(event.code);
|
|
97
|
+
runtime.keys.add(event.key);
|
|
98
|
+
};
|
|
99
|
+
const onKeyUp = (event) => {
|
|
100
|
+
runtime.keys.delete(event.code);
|
|
101
|
+
runtime.keys.delete(event.key);
|
|
102
|
+
};
|
|
103
|
+
// Pointer (mouse or touch) input is multitouch, tracked per pointerId:
|
|
104
|
+
// - a DRAG is the virtual stick -- the offset from where the drag started
|
|
105
|
+
// maps to a walk direction (drag up = forward). The first pointer to
|
|
106
|
+
// travel past the tap slop owns the stick until it lifts.
|
|
107
|
+
// - a TAP (down and up quickly without moving much) is a jump, even while
|
|
108
|
+
// another finger is dragging. `moveInput.consumeJump()` hands the
|
|
109
|
+
// pending tap to the Player behavior exactly once.
|
|
110
|
+
// `runtime.moveInput` is what the Player behavior reads.
|
|
111
|
+
const STICK_RADIUS_PX = 56;
|
|
112
|
+
const TAP_SLOP_PX = 12;
|
|
113
|
+
const TAP_MS = 300;
|
|
114
|
+
const pointers = new Map();
|
|
115
|
+
let stickId = null;
|
|
116
|
+
let jumpPending = false;
|
|
117
|
+
const move = {
|
|
118
|
+
x: 0,
|
|
119
|
+
y: 0,
|
|
120
|
+
active: false,
|
|
121
|
+
consumeJump() {
|
|
122
|
+
const had = jumpPending;
|
|
123
|
+
jumpPending = false;
|
|
124
|
+
return had;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
runtime.moveInput = move;
|
|
128
|
+
const clearStick = () => {
|
|
129
|
+
stickId = null;
|
|
130
|
+
move.active = false;
|
|
131
|
+
move.x = 0;
|
|
132
|
+
move.y = 0;
|
|
133
|
+
};
|
|
134
|
+
const onPointerDown = (event) => {
|
|
135
|
+
canvas.focus();
|
|
136
|
+
pointers.set(event.pointerId, {
|
|
137
|
+
x: event.clientX,
|
|
138
|
+
y: event.clientY,
|
|
139
|
+
time: performance.now(),
|
|
140
|
+
moved: false,
|
|
141
|
+
});
|
|
142
|
+
canvas.setPointerCapture(event.pointerId);
|
|
143
|
+
};
|
|
144
|
+
const onPointerMove = (event) => {
|
|
145
|
+
const p = pointers.get(event.pointerId);
|
|
146
|
+
if (!p) return;
|
|
147
|
+
const dx = event.clientX - p.x;
|
|
148
|
+
const dy = event.clientY - p.y;
|
|
149
|
+
if (!p.moved && Math.hypot(dx, dy) > TAP_SLOP_PX) p.moved = true;
|
|
150
|
+
// A pointer claims the stick once it is clearly a drag (and nothing else
|
|
151
|
+
// holds it) -- so a tapping finger never yanks the walk direction.
|
|
152
|
+
if (p.moved && stickId === null) stickId = event.pointerId;
|
|
153
|
+
if (stickId !== event.pointerId) return;
|
|
154
|
+
const nx = dx / STICK_RADIUS_PX;
|
|
155
|
+
const ny = dy / STICK_RADIUS_PX;
|
|
156
|
+
const len = Math.hypot(nx, ny);
|
|
157
|
+
const scale = len > 1 ? 1 / len : 1;
|
|
158
|
+
move.active = true;
|
|
159
|
+
move.x = nx * scale;
|
|
160
|
+
move.y = ny * scale;
|
|
161
|
+
};
|
|
162
|
+
const onPointerUp = (event) => {
|
|
163
|
+
const p = pointers.get(event.pointerId);
|
|
164
|
+
pointers.delete(event.pointerId);
|
|
165
|
+
if (stickId === event.pointerId) clearStick();
|
|
166
|
+
if (event.type === 'pointercancel' || !p) return;
|
|
167
|
+
if (!p.moved && performance.now() - p.time < TAP_MS) jumpPending = true;
|
|
168
|
+
};
|
|
169
|
+
window.addEventListener('keydown', onKeyDown);
|
|
170
|
+
window.addEventListener('keyup', onKeyUp);
|
|
171
|
+
canvas.addEventListener('pointerdown', onPointerDown);
|
|
172
|
+
canvas.addEventListener('pointermove', onPointerMove);
|
|
173
|
+
canvas.addEventListener('pointerup', onPointerUp);
|
|
174
|
+
canvas.addEventListener('pointercancel', onPointerUp);
|
|
175
|
+
if (document.hasFocus() || window.parent === window) canvas.focus();
|
|
176
|
+
return () => {
|
|
177
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
178
|
+
window.removeEventListener('keyup', onKeyUp);
|
|
179
|
+
canvas.removeEventListener('pointerdown', onPointerDown);
|
|
180
|
+
canvas.removeEventListener('pointermove', onPointerMove);
|
|
181
|
+
canvas.removeEventListener('pointerup', onPointerUp);
|
|
182
|
+
canvas.removeEventListener('pointercancel', onPointerUp);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function attachResize(canvas, world) {
|
|
187
|
+
const fit = () => resizeWorld(world, canvas.clientWidth, canvas.clientHeight);
|
|
188
|
+
const observer = new ResizeObserver(fit);
|
|
189
|
+
observer.observe(canvas);
|
|
190
|
+
fit();
|
|
191
|
+
return () => observer.disconnect();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function startLoop(runtime, world, sprites, hudRefs, onFirstFrame, onError) {
|
|
195
|
+
let raf = 0;
|
|
196
|
+
let previousTime = performance.now();
|
|
197
|
+
let firstFrameSignaled = false;
|
|
198
|
+
const tick = (now) => {
|
|
199
|
+
const dt = Math.min(0.033, (now - previousTime) / 1000);
|
|
200
|
+
previousTime = now;
|
|
201
|
+
try {
|
|
202
|
+
runtime.update(dt);
|
|
203
|
+
// A behavior may have requested a scene switch; apply it here, between
|
|
204
|
+
// frames, never mid physics dispatch. The load resets
|
|
205
|
+
// registered systems -- the rapier world frees and lazily rebuilds --
|
|
206
|
+
// while `runtime.persistent` carries the player's cross-scene state.
|
|
207
|
+
if (runtime.requestedScene) {
|
|
208
|
+
const next = runtime.requestedScene;
|
|
209
|
+
runtime.requestedScene = null;
|
|
210
|
+
runtime.loadFromFile(next);
|
|
211
|
+
runtime.currentScenePath = next;
|
|
212
|
+
runtime.persistent.sceneChangeGraceUntil = runtime.time + 1;
|
|
213
|
+
}
|
|
214
|
+
syncActors(world, runtime, sprites, dt);
|
|
215
|
+
if (world.debugLines.visible) updatePhysicsDebug(world, runtime.physics3d?.debugRender());
|
|
216
|
+
renderWorld(world);
|
|
217
|
+
updateHud(runtime, hudRefs);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
onError?.(err);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!firstFrameSignaled) {
|
|
223
|
+
firstFrameSignaled = true;
|
|
224
|
+
requestAnimationFrame(() => onFirstFrame?.());
|
|
225
|
+
}
|
|
226
|
+
raf = requestAnimationFrame(tick);
|
|
227
|
+
};
|
|
228
|
+
raf = requestAnimationFrame(tick);
|
|
229
|
+
return () => cancelAnimationFrame(raf);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Behaviors write `scene.hud = { score, look }`; the loop pushes it into the
|
|
233
|
+
// overlay divs imperatively (no react re-render at frame rate).
|
|
234
|
+
function updateHud(runtime, hudRefs) {
|
|
235
|
+
const hud = runtime.hud ?? {};
|
|
236
|
+
const scoreText = hud.score ? String(hud.score) : '';
|
|
237
|
+
const lookText = hud.look ?? '';
|
|
238
|
+
if (hudRefs.score.current && hudRefs.score.current.textContent !== scoreText) {
|
|
239
|
+
hudRefs.score.current.textContent = scoreText;
|
|
240
|
+
}
|
|
241
|
+
if (hudRefs.look.current && hudRefs.look.current.textContent !== lookText) {
|
|
242
|
+
hudRefs.look.current.textContent = lookText;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const hudStyles = {
|
|
247
|
+
score: {
|
|
248
|
+
position: 'absolute',
|
|
249
|
+
top: 14,
|
|
250
|
+
left: 16,
|
|
251
|
+
color: '#7bd9ed',
|
|
252
|
+
font: "16px/1.4 'SFMono-Regular', Consolas, monospace",
|
|
253
|
+
textShadow: '0 1px 2px rgba(0,0,0,0.8)',
|
|
254
|
+
pointerEvents: 'none',
|
|
255
|
+
},
|
|
256
|
+
look: {
|
|
257
|
+
position: 'absolute',
|
|
258
|
+
bottom: 22,
|
|
259
|
+
left: 0,
|
|
260
|
+
right: 0,
|
|
261
|
+
textAlign: 'center',
|
|
262
|
+
color: '#e6e6e2',
|
|
263
|
+
fontStyle: 'italic',
|
|
264
|
+
font: 'italic 15px/1.4 Georgia, serif',
|
|
265
|
+
textShadow: '0 1px 3px rgba(0,0,0,0.9)',
|
|
266
|
+
pointerEvents: 'none',
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
function LoopErrorOverlay({ error, onRestart }) {
|
|
271
|
+
return (
|
|
272
|
+
<div
|
|
273
|
+
style={{
|
|
274
|
+
position: 'absolute',
|
|
275
|
+
inset: 0,
|
|
276
|
+
zIndex: 2147483000,
|
|
277
|
+
padding: '20px 22px',
|
|
278
|
+
background: 'rgba(20, 8, 8, 0.94)',
|
|
279
|
+
color: '#ffd9dc',
|
|
280
|
+
font: "13px/1.5 'SFMono-Regular', Consolas, monospace",
|
|
281
|
+
overflow: 'auto',
|
|
282
|
+
}}>
|
|
283
|
+
<div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 12 }}>
|
|
284
|
+
<span style={{ fontSize: 15, fontWeight: 700, color: '#ff6b74' }}>Play error</span>
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
onClick={onRestart}
|
|
288
|
+
style={{
|
|
289
|
+
marginLeft: 'auto',
|
|
290
|
+
padding: '4px 12px',
|
|
291
|
+
border: '1px solid #a02828',
|
|
292
|
+
borderRadius: 4,
|
|
293
|
+
background: '#a02828',
|
|
294
|
+
color: '#fff',
|
|
295
|
+
font: 'inherit',
|
|
296
|
+
cursor: 'pointer',
|
|
297
|
+
}}>
|
|
298
|
+
Restart
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
<div style={{ color: '#fff', fontWeight: 600 }}>
|
|
302
|
+
{error?.name ?? 'Error'}: {error?.message ?? String(error)}
|
|
303
|
+
</div>
|
|
304
|
+
{typeof error?.stack === 'string' ? (
|
|
305
|
+
<pre style={{ whiteSpace: 'pre-wrap', color: '#e79aa0', fontSize: 12 }}>{error.stack}</pre>
|
|
306
|
+
) : null}
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Data-side helpers for the 3d scene editor: the 3d counterparts of the
|
|
2
|
+
// imported engine's placement / duplication helpers (which write 2d Layout),
|
|
3
|
+
// plus the blueprint-file template-id shim. Everything here works on parsed
|
|
4
|
+
// scene JSON and returns fresh copies -- commits go through the editor's
|
|
5
|
+
// history.
|
|
6
|
+
import {
|
|
7
|
+
getBlueprintTemplate,
|
|
8
|
+
mintActorIdFromName,
|
|
9
|
+
mintBlueprintFile,
|
|
10
|
+
} from '@imports/castle.physics-2d/engine/blueprint';
|
|
11
|
+
|
|
12
|
+
export const TEMPLATE_ACTOR_ID = '__template__';
|
|
13
|
+
|
|
14
|
+
export const round2 = (v) => Math.round(v * 100) / 100;
|
|
15
|
+
|
|
16
|
+
// Grid snap for placements; matches the gizmo's 0.1m translation snap.
|
|
17
|
+
const snap = (v) => Math.round(v * 10) / 10;
|
|
18
|
+
|
|
19
|
+
// A blueprint file's template actor has no id on disk; inject a synthetic one
|
|
20
|
+
// so selection / inspector / gizmo code sees a normal actor, and strip it
|
|
21
|
+
// before every write.
|
|
22
|
+
export function withTemplateId(sceneData, isBlueprintFile) {
|
|
23
|
+
if (!sceneData || !isBlueprintFile) return sceneData;
|
|
24
|
+
const next = structuredClone(sceneData);
|
|
25
|
+
next.actors = next.actors ?? [];
|
|
26
|
+
if (next.actors[0]) next.actors[0].id = TEMPLATE_ACTOR_ID;
|
|
27
|
+
return next;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function stripTemplateId(sceneData) {
|
|
31
|
+
const next = structuredClone(sceneData);
|
|
32
|
+
if (next.actors?.[0]?.id === TEMPLATE_ACTOR_ID) delete next.actors[0].id;
|
|
33
|
+
return next;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Place a new instance of a blueprint at a ground point. Only Transform x/z
|
|
37
|
+
// are written on the instance -- height and everything else inherit.
|
|
38
|
+
export function placeBlueprintInstance3D(sceneData, files, blueprintPath, point) {
|
|
39
|
+
const next = structuredClone(sceneData);
|
|
40
|
+
next.actors = next.actors ?? [];
|
|
41
|
+
const existingIds = new Set(next.actors.map((actor) => actor.id));
|
|
42
|
+
const template = getBlueprintTemplate(files, blueprintPath);
|
|
43
|
+
const newId = mintActorIdFromName(existingIds, template?.name);
|
|
44
|
+
next.actors.push({
|
|
45
|
+
id: newId,
|
|
46
|
+
blueprint: blueprintPath,
|
|
47
|
+
components: { Transform: { x: snap(point.x), z: snap(point.z) } },
|
|
48
|
+
});
|
|
49
|
+
return { sceneData: next, newId };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Duplicate selected actors with a 1m diagonal offset (the 3d version of the
|
|
53
|
+
// imported engine's duplicateActors, which offsets 2d Layout instead).
|
|
54
|
+
// `resolvedActors` supplies merged Transforms so an instance that inherits its
|
|
55
|
+
// position still lands next to the original.
|
|
56
|
+
export function duplicateActors3D(sceneData, actorIds, resolvedActors) {
|
|
57
|
+
const next = structuredClone(sceneData);
|
|
58
|
+
const existingIds = new Set(next.actors.map((actor) => actor.id));
|
|
59
|
+
const newIds = [];
|
|
60
|
+
for (const id of actorIds) {
|
|
61
|
+
const source = next.actors.find((actor) => actor.id === id);
|
|
62
|
+
if (!source) continue;
|
|
63
|
+
const copy = structuredClone(source);
|
|
64
|
+
copy.id = mintActorIdFromName(existingIds, id.replace(/-\d+$/, ''));
|
|
65
|
+
existingIds.add(copy.id);
|
|
66
|
+
const merged = resolvedActors?.find((actor) => actor.id === id)?.components?.Transform ?? {};
|
|
67
|
+
copy.components = {
|
|
68
|
+
...(copy.components ?? {}),
|
|
69
|
+
Transform: {
|
|
70
|
+
...(copy.components?.Transform ?? {}),
|
|
71
|
+
x: round2((merged.x ?? 0) + 1),
|
|
72
|
+
z: round2((merged.z ?? 0) + 1),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
next.actors.push(copy);
|
|
76
|
+
newIds.push(copy.id);
|
|
77
|
+
}
|
|
78
|
+
return { sceneData: next, newIds };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The hotbar's "+" slot: mint a fresh 3d blueprint file (a solid box) and
|
|
82
|
+
// place one instance of it at the origin.
|
|
83
|
+
export function addActorWithBlueprint3D(sceneData, files) {
|
|
84
|
+
const blueprintFile = mintBlueprintFile(files, {
|
|
85
|
+
Transform: { y: 0.5 },
|
|
86
|
+
Shape: { kind: 'box' },
|
|
87
|
+
Solid: {},
|
|
88
|
+
});
|
|
89
|
+
const filesWithNew = { ...files, [blueprintFile.path]: blueprintFile.text };
|
|
90
|
+
const { sceneData: next, newId } = placeBlueprintInstance3D(
|
|
91
|
+
sceneData,
|
|
92
|
+
filesWithNew,
|
|
93
|
+
blueprintFile.path,
|
|
94
|
+
{ x: 0, z: 0 }
|
|
95
|
+
);
|
|
96
|
+
return { sceneData: next, newId, blueprintFile };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A gizmo gesture's end state -> sparse Transform override patch. Position is
|
|
100
|
+
// always written (instance-local); rotation only when meaningful or already
|
|
101
|
+
// present.
|
|
102
|
+
export function transformPatch(existing, change) {
|
|
103
|
+
const next = {
|
|
104
|
+
...(existing ?? {}),
|
|
105
|
+
x: round2(change.x),
|
|
106
|
+
y: round2(change.y),
|
|
107
|
+
z: round2(change.z),
|
|
108
|
+
};
|
|
109
|
+
for (const key of ['rotationX', 'rotationY', 'rotationZ']) {
|
|
110
|
+
const value = Math.round((change[key] ?? 0) * 10) / 10;
|
|
111
|
+
if (value !== 0 || (existing && existing[key] !== undefined)) next[key] = value;
|
|
112
|
+
}
|
|
113
|
+
return next;
|
|
114
|
+
}
|