@jgengine/editor 0.10.0
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/CHANGELOG.md +325 -0
- package/dist/AssetBrowser.d.ts +20 -0
- package/dist/AssetBrowser.js +49 -0
- package/dist/CatalogsPanel.d.ts +10 -0
- package/dist/CatalogsPanel.js +41 -0
- package/dist/CollectionsPanel.d.ts +10 -0
- package/dist/CollectionsPanel.js +35 -0
- package/dist/DebugDraw.d.ts +17 -0
- package/dist/DebugDraw.js +233 -0
- package/dist/EditorApp.d.ts +26 -0
- package/dist/EditorApp.js +333 -0
- package/dist/EditorCameraDriver.d.ts +5 -0
- package/dist/EditorCameraDriver.js +18 -0
- package/dist/EditorChrome.d.ts +22 -0
- package/dist/EditorChrome.js +559 -0
- package/dist/EditorContextMenu.d.ts +11 -0
- package/dist/EditorContextMenu.js +12 -0
- package/dist/InspectorPanel.d.ts +14 -0
- package/dist/InspectorPanel.js +110 -0
- package/dist/MaterialDropZone.d.ts +10 -0
- package/dist/MaterialDropZone.js +77 -0
- package/dist/OutlinerPanel.d.ts +14 -0
- package/dist/OutlinerPanel.js +84 -0
- package/dist/PerfProbe.d.ts +5 -0
- package/dist/PerfProbe.js +34 -0
- package/dist/PrefabsPanel.d.ts +13 -0
- package/dist/PrefabsPanel.js +42 -0
- package/dist/RuntimePlayBridge.d.ts +18 -0
- package/dist/RuntimePlayBridge.js +140 -0
- package/dist/ScatterPreview.d.ts +11 -0
- package/dist/ScatterPreview.js +30 -0
- package/dist/SchemaInspector.d.ts +19 -0
- package/dist/SchemaInspector.js +78 -0
- package/dist/SelectionGizmo.d.ts +25 -0
- package/dist/SelectionGizmo.js +455 -0
- package/dist/StandaloneEditor.d.ts +44 -0
- package/dist/StandaloneEditor.js +92 -0
- package/dist/TerrainPanel.d.ts +7 -0
- package/dist/TerrainPanel.js +59 -0
- package/dist/TerrainSculpt.d.ts +14 -0
- package/dist/TerrainSculpt.js +415 -0
- package/dist/TriggerInspector.d.ts +13 -0
- package/dist/TriggerInspector.js +60 -0
- package/dist/agent/AgentPanel.d.ts +13 -0
- package/dist/agent/AgentPanel.js +156 -0
- package/dist/agent/context.d.ts +27 -0
- package/dist/agent/context.js +39 -0
- package/dist/agent/endpoint.d.ts +68 -0
- package/dist/agent/endpoint.js +186 -0
- package/dist/agent/toolBridge.d.ts +35 -0
- package/dist/agent/toolBridge.js +93 -0
- package/dist/agent/turn.d.ts +71 -0
- package/dist/agent/turn.js +113 -0
- package/dist/chromeFields.d.ts +15 -0
- package/dist/chromeFields.js +13 -0
- package/dist/chromeStyles.d.ts +3 -0
- package/dist/chromeStyles.js +3 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +19 -0
- package/dist/mcp/bridgeServer.d.ts +13 -0
- package/dist/mcp/bridgeServer.js +1 -0
- package/dist/mcp/loadGameCatalogs.d.ts +24 -0
- package/dist/mcp/loadGameCatalogs.js +87 -0
- package/dist/mcp/rpcRequest.d.ts +22 -0
- package/dist/mcp/rpcRequest.js +97 -0
- package/dist/mcp/tools.d.ts +8 -0
- package/dist/mcp/tools.js +764 -0
- package/dist/outlinerModel.d.ts +76 -0
- package/dist/outlinerModel.js +95 -0
- package/dist/perfMarks.d.ts +25 -0
- package/dist/perfMarks.js +36 -0
- package/dist/session.d.ts +400 -0
- package/dist/session.js +1032 -0
- package/dist/uiStore.d.ts +108 -0
- package/dist/uiStore.js +128 -0
- package/dist/useF2Chord.d.ts +2 -0
- package/dist/useF2Chord.js +32 -0
- package/dist/useStoreSelector.d.ts +31 -0
- package/dist/useStoreSelector.js +51 -0
- package/dist/viewportContextMenu.d.ts +24 -0
- package/dist/viewportContextMenu.js +32 -0
- package/package.json +58 -0
- package/skills/jgengine/SKILL.md +823 -0
- package/skills/jgengine/api.md +416 -0
- package/skills/jgengine/capabilities.md +21 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine/reference-orphans.md +9 -0
- package/skills/jgengine/reference-systems.md +60 -0
- package/skills/jgengine-editor/SKILL.md +382 -0
- package/skills/jgengine-editor/api.md +416 -0
- package/skills/jgengine-editor/capabilities.md +29 -0
- package/skills/jgengine-editor/reference.md +64 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
|
|
4
|
+
const INPUT = "rounded-md border border-white/10 bg-black/40 px-2 py-1 outline-none transition-colors placeholder:text-neutral-600 focus:border-cyan-400/60 focus:bg-black/60";
|
|
5
|
+
const BTN = "rounded-md bg-white/[0.04] px-2 py-1 text-neutral-300 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/10 hover:text-neutral-100";
|
|
6
|
+
const CHIP_BTN = "rounded-md px-1.5 py-0.5 text-[10px] ring-1 ring-inset transition-colors";
|
|
7
|
+
function newCollectionId() {
|
|
8
|
+
return `col_${Date.now().toString(36)}_${Math.floor(Math.random() * 1296).toString(36)}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Named selection sets / production groups: bookmark the current selection, restore it later,
|
|
12
|
+
* add-to / remove-from membership, and toggle a collection's color/lock/visibility. Locking a
|
|
13
|
+
* collection blocks move and delete on its members at the session level (see `isEditorObjectLocked`).
|
|
14
|
+
* @internal — mounted by `EditorChrome` as a left-aside tab.
|
|
15
|
+
*/
|
|
16
|
+
export function CollectionsPanel({ session }) {
|
|
17
|
+
const document = useStoreSelector(session, (state) => state.document);
|
|
18
|
+
const selection = useStoreSelector(session, (state) => state.selection, shallowArrayEqual);
|
|
19
|
+
const [name, setName] = useState("");
|
|
20
|
+
const collections = document.collections;
|
|
21
|
+
const createFromSelection = () => {
|
|
22
|
+
const trimmed = name.trim();
|
|
23
|
+
if (trimmed.length === 0 || selection.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
session.dispatch({ type: "createCollection", id: newCollectionId(), name: trimmed, memberIds: selection });
|
|
26
|
+
setName("");
|
|
27
|
+
};
|
|
28
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 p-2", children: [_jsxs("div", { className: "flex gap-1", children: [_jsx("input", { className: `min-w-0 flex-1 ${INPUT}`, placeholder: "New collection name\u2026", value: name, onChange: (event) => setName(event.target.value), onKeyDown: (event) => {
|
|
29
|
+
if (event.key === "Enter")
|
|
30
|
+
createFromSelection();
|
|
31
|
+
} }), _jsx("button", { type: "button", className: BTN, disabled: selection.length === 0 || name.trim().length === 0, onClick: createFromSelection, children: "From selection" })] }), selection.length === 0 ? (_jsx("div", { className: "text-[10px] text-neutral-500", children: "Select objects first, then name and save a set." })) : null, _jsx("div", { className: "min-h-0 flex-1 space-y-1 overflow-auto pr-1", children: collections.length === 0 ? (_jsx("div", { className: "text-neutral-600", children: "No collections yet." })) : (collections.map((collection) => (_jsx(CollectionRow, { session: session, collection: collection, selection: selection }, collection.id)))) })] }));
|
|
32
|
+
}
|
|
33
|
+
function CollectionRow({ session, collection, selection, }) {
|
|
34
|
+
return (_jsxs("div", { className: "space-y-1 rounded-md bg-white/[0.03] p-1.5 ring-1 ring-inset ring-white/[0.06]", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full ring-1 ring-inset ring-white/20", style: { backgroundColor: collection.color ?? "#64748b" } }), _jsx("button", { type: "button", className: "min-w-0 flex-1 truncate text-left text-neutral-200 hover:text-cyan-200", title: "Restore selection", onClick: () => session.dispatch({ type: "selectCollection", id: collection.id }), children: collection.name }), _jsx("span", { className: "text-neutral-500", children: collection.memberIds.length })] }), _jsxs("div", { className: "flex flex-wrap gap-1", children: [_jsx("button", { type: "button", className: `${CHIP_BTN} ${collection.locked === true ? "bg-amber-500/20 text-amber-200 ring-amber-400/30" : "bg-black/30 text-neutral-400 ring-white/[0.06] hover:text-neutral-200"}`, onClick: () => session.dispatch({ type: "setCollectionFlags", id: collection.id, patch: { locked: collection.locked !== true } }), children: collection.locked === true ? "🔒 locked" : "🔓 lock" }), _jsx("button", { type: "button", className: `${CHIP_BTN} ${collection.visible === false ? "bg-black/30 text-neutral-500 ring-white/[0.06]" : "bg-cyan-500/15 text-cyan-200 ring-cyan-400/25"}`, onClick: () => session.dispatch({ type: "setCollectionFlags", id: collection.id, patch: { visible: collection.visible === false } }), children: collection.visible === false ? "hidden" : "visible" }), _jsx("label", { className: "flex items-center gap-1 rounded-md bg-black/30 px-1.5 py-0.5 ring-1 ring-inset ring-white/[0.06]", children: _jsx("input", { type: "color", className: "h-3 w-4 cursor-pointer border-none bg-transparent p-0", value: collection.color ?? "#64748b", onChange: (event) => session.dispatch({ type: "setCollectionFlags", id: collection.id, patch: { color: event.target.value } }) }) }), _jsx("button", { type: "button", className: `${CHIP_BTN} bg-black/30 text-neutral-400 ring-white/[0.06] hover:text-neutral-200 disabled:opacity-30`, disabled: selection.length === 0, onClick: () => session.dispatch({ type: "addToCollection", id: collection.id, ids: selection }), children: "+ add selected" }), _jsx("button", { type: "button", className: `${CHIP_BTN} bg-black/30 text-neutral-400 ring-white/[0.06] hover:text-neutral-200 disabled:opacity-30`, disabled: selection.length === 0, onClick: () => session.dispatch({ type: "removeFromCollection", id: collection.id, ids: selection }), children: "\u2212 remove selected" }), _jsx("button", { type: "button", className: `${CHIP_BTN} ml-auto bg-rose-500/15 text-rose-200 ring-rose-400/25 hover:bg-rose-500/25`, onClick: () => session.dispatch({ type: "deleteCollection", id: collection.id }), children: "Delete" })] })] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EditorDocument, EditorKindVisibility, EditorVec3 } from "@jgengine/core/editor/index";
|
|
2
|
+
/** Live preview of an in-progress path drawing: placed points and the connecting line. */
|
|
3
|
+
export declare function PathDraftPreview({ points }: {
|
|
4
|
+
points: readonly EditorVec3[];
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
/** Renders every visible marker, volume, path, and note from a document as in-scene 3D gizmos. */
|
|
7
|
+
export declare function EditorLayerOverlays({ document, visibility, selection, onSelect, activePathPoint, groundHeightAt, }: {
|
|
8
|
+
document: EditorDocument;
|
|
9
|
+
visibility: EditorKindVisibility;
|
|
10
|
+
selection: readonly string[];
|
|
11
|
+
onSelect: (id: string) => void;
|
|
12
|
+
activePathPoint?: {
|
|
13
|
+
pathId: string;
|
|
14
|
+
index: number;
|
|
15
|
+
} | null;
|
|
16
|
+
groundHeightAt?: (x: number, z: number) => number;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Html } from "@react-three/drei";
|
|
3
|
+
import { memo, useEffect, useMemo } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
const DEFAULT_COLORS = {
|
|
6
|
+
player_spawn: "#22d3ee",
|
|
7
|
+
boss: "#f43f5e",
|
|
8
|
+
mob: "#f97316",
|
|
9
|
+
vendor: "#a78bfa",
|
|
10
|
+
chest: "#fbbf24",
|
|
11
|
+
travel: "#34d399",
|
|
12
|
+
npc: "#60a5fa",
|
|
13
|
+
poi: "#e879f9",
|
|
14
|
+
goal: "#f472b6",
|
|
15
|
+
branch: "#facc15",
|
|
16
|
+
zone: "#38bdf8",
|
|
17
|
+
flatten: "#94a3b8",
|
|
18
|
+
cluster: "#fb923c",
|
|
19
|
+
aggro: "#ef4444",
|
|
20
|
+
leash: "#f59e0b",
|
|
21
|
+
discover: "#4ade80",
|
|
22
|
+
capture: "#f472b6",
|
|
23
|
+
prompt: "#c084fc",
|
|
24
|
+
respawn_skip: "#64748b",
|
|
25
|
+
road: "#cbd5e1",
|
|
26
|
+
corridor: "#38bdf8",
|
|
27
|
+
route: "#e2e8f0",
|
|
28
|
+
note: "#fde68a",
|
|
29
|
+
};
|
|
30
|
+
const RING_SEGMENTS = 40;
|
|
31
|
+
const MARKER_SEGMENTS = 8;
|
|
32
|
+
function colorFor(kind, override) {
|
|
33
|
+
return override ?? DEFAULT_COLORS[kind] ?? "#ffffff";
|
|
34
|
+
}
|
|
35
|
+
function isVisible(visibility, kind) {
|
|
36
|
+
return visibility[kind] !== false;
|
|
37
|
+
}
|
|
38
|
+
function pushRing(out, radius, y, axis, segments = RING_SEGMENTS) {
|
|
39
|
+
for (let i = 0; i < segments; i += 1) {
|
|
40
|
+
const a = (i / segments) * Math.PI * 2;
|
|
41
|
+
const b = ((i + 1) / segments) * Math.PI * 2;
|
|
42
|
+
const pa = ringPoint(radius, y, axis, a);
|
|
43
|
+
const pb = ringPoint(radius, y, axis, b);
|
|
44
|
+
out.push(pa[0], pa[1], pa[2], pb[0], pb[1], pb[2]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function ringPoint(radius, offset, axis, t) {
|
|
48
|
+
const c = Math.cos(t) * radius;
|
|
49
|
+
const s = Math.sin(t) * radius;
|
|
50
|
+
if (axis === "y")
|
|
51
|
+
return [c, offset, s];
|
|
52
|
+
if (axis === "x")
|
|
53
|
+
return [offset, c, s];
|
|
54
|
+
return [c, s, offset];
|
|
55
|
+
}
|
|
56
|
+
function volumeWireframe(volume) {
|
|
57
|
+
const out = [];
|
|
58
|
+
if (volume.shape === "box") {
|
|
59
|
+
const he = volume.halfExtents ?? { x: volume.radius ?? 5, y: volume.height ?? 4, z: volume.radius ?? 5 };
|
|
60
|
+
const xs = [-he.x, he.x];
|
|
61
|
+
const ys = [0, he.y * 2];
|
|
62
|
+
const zs = [-he.z, he.z];
|
|
63
|
+
for (const y of ys) {
|
|
64
|
+
out.push(xs[0], y, zs[0], xs[1], y, zs[0]);
|
|
65
|
+
out.push(xs[1], y, zs[0], xs[1], y, zs[1]);
|
|
66
|
+
out.push(xs[1], y, zs[1], xs[0], y, zs[1]);
|
|
67
|
+
out.push(xs[0], y, zs[1], xs[0], y, zs[0]);
|
|
68
|
+
}
|
|
69
|
+
for (const x of xs)
|
|
70
|
+
for (const z of zs)
|
|
71
|
+
out.push(x, ys[0], z, x, ys[1], z);
|
|
72
|
+
return new Float32Array(out);
|
|
73
|
+
}
|
|
74
|
+
const radius = Math.max(0.5, volume.radius ?? 5);
|
|
75
|
+
if (volume.shape === "cylinder") {
|
|
76
|
+
const height = Math.max(0.5, volume.height ?? 4);
|
|
77
|
+
pushRing(out, radius, 0.1, "y");
|
|
78
|
+
pushRing(out, radius, height, "y");
|
|
79
|
+
for (let i = 0; i < 4; i += 1) {
|
|
80
|
+
const t = (i / 4) * Math.PI * 2;
|
|
81
|
+
out.push(Math.cos(t) * radius, 0.1, Math.sin(t) * radius, Math.cos(t) * radius, height, Math.sin(t) * radius);
|
|
82
|
+
}
|
|
83
|
+
return new Float32Array(out);
|
|
84
|
+
}
|
|
85
|
+
pushRing(out, radius, 0.1, "y");
|
|
86
|
+
pushRing(out, radius, 0, "x");
|
|
87
|
+
pushRing(out, radius, 0, "z");
|
|
88
|
+
return new Float32Array(out);
|
|
89
|
+
}
|
|
90
|
+
const MarkerMesh = memo(function MarkerMesh({ marker, selected, onSelect, sharedSphere, sharedCone, groundHeightAt, }) {
|
|
91
|
+
const color = colorFor(marker.kind, marker.color);
|
|
92
|
+
const scale = selected ? 1.25 : 1;
|
|
93
|
+
const baseY = groundHeightAt !== undefined ? groundHeightAt(marker.position.x, marker.position.z) : marker.position.y;
|
|
94
|
+
return (_jsxs("group", { position: [marker.position.x, baseY + 1.2, marker.position.z], "rotation-y": marker.rotationY ?? 0, scale: scale, userData: { jgEditorId: marker.id }, onClick: (event) => {
|
|
95
|
+
event.stopPropagation();
|
|
96
|
+
onSelect(marker.id);
|
|
97
|
+
}, children: [_jsx("mesh", { geometry: sharedSphere, children: _jsx("meshBasicMaterial", { color: selected ? "#ffffff" : color }) }), _jsx("mesh", { position: [0, 1.35, 0], geometry: sharedCone, children: _jsx("meshBasicMaterial", { color: color }) })] }));
|
|
98
|
+
});
|
|
99
|
+
/** Shape-true wireframe — ground ring plus vertical profile for spheres, cylinders, and boxes. */
|
|
100
|
+
const VolumeShapeLines = memo(function VolumeShapeLines({ volume, selected, onSelect, }) {
|
|
101
|
+
const color = colorFor(volume.kind, volume.color);
|
|
102
|
+
const object = useMemo(() => {
|
|
103
|
+
const geometry = new THREE.BufferGeometry();
|
|
104
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(volumeWireframe(volume), 3));
|
|
105
|
+
const material = new THREE.LineBasicMaterial({
|
|
106
|
+
color: selected ? "#ffffff" : color,
|
|
107
|
+
transparent: true,
|
|
108
|
+
opacity: selected ? 1 : 0.75,
|
|
109
|
+
});
|
|
110
|
+
return new THREE.LineSegments(geometry, material);
|
|
111
|
+
}, [
|
|
112
|
+
volume.shape,
|
|
113
|
+
volume.radius,
|
|
114
|
+
volume.height,
|
|
115
|
+
volume.halfExtents?.x,
|
|
116
|
+
volume.halfExtents?.y,
|
|
117
|
+
volume.halfExtents?.z,
|
|
118
|
+
color,
|
|
119
|
+
selected,
|
|
120
|
+
]);
|
|
121
|
+
useEffect(() => () => {
|
|
122
|
+
object.geometry.dispose();
|
|
123
|
+
object.material.dispose();
|
|
124
|
+
}, [object]);
|
|
125
|
+
return (_jsx("group", { position: [volume.center.x, volume.center.y, volume.center.z], userData: { jgEditorId: volume.id }, onClick: (event) => {
|
|
126
|
+
event.stopPropagation();
|
|
127
|
+
onSelect(volume.id);
|
|
128
|
+
}, children: _jsx("primitive", { object: object }) }));
|
|
129
|
+
});
|
|
130
|
+
const PathRibbon = memo(function PathRibbon({ path, selected, activePointIndex, onSelect, groundHeightAt, }) {
|
|
131
|
+
const color = colorFor(path.kind, path.color);
|
|
132
|
+
const yAt = (point) => groundHeightAt !== undefined ? groundHeightAt(point.x, point.z) : point.y;
|
|
133
|
+
const object = useMemo(() => {
|
|
134
|
+
if (path.points.length < 2)
|
|
135
|
+
return null;
|
|
136
|
+
const stride = !selected && path.points.length > 80 ? 2 : 1;
|
|
137
|
+
const picked = [];
|
|
138
|
+
for (let i = 0; i < path.points.length; i += stride) {
|
|
139
|
+
picked.push(path.points[i]);
|
|
140
|
+
}
|
|
141
|
+
const last = path.points[path.points.length - 1];
|
|
142
|
+
const tail = picked[picked.length - 1];
|
|
143
|
+
if (tail === undefined || tail.x !== last.x || tail.z !== last.z)
|
|
144
|
+
picked.push(last);
|
|
145
|
+
const positions = new Float32Array(picked.length * 3);
|
|
146
|
+
picked.forEach((point, index) => {
|
|
147
|
+
positions[index * 3] = point.x;
|
|
148
|
+
positions[index * 3 + 1] = yAt(point) + 0.8;
|
|
149
|
+
positions[index * 3 + 2] = point.z;
|
|
150
|
+
});
|
|
151
|
+
const geometry = new THREE.BufferGeometry();
|
|
152
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
153
|
+
const material = new THREE.LineBasicMaterial({
|
|
154
|
+
color: selected ? "#ffffff" : color,
|
|
155
|
+
transparent: true,
|
|
156
|
+
opacity: selected ? 1 : 0.85,
|
|
157
|
+
});
|
|
158
|
+
return new THREE.Line(geometry, material);
|
|
159
|
+
}, [path.points, color, selected, groundHeightAt]);
|
|
160
|
+
useEffect(() => () => {
|
|
161
|
+
if (object === null)
|
|
162
|
+
return;
|
|
163
|
+
object.geometry.dispose();
|
|
164
|
+
object.material.dispose();
|
|
165
|
+
}, [object]);
|
|
166
|
+
if (object === null)
|
|
167
|
+
return null;
|
|
168
|
+
return (_jsxs("group", { userData: { jgEditorId: path.id }, onClick: (event) => {
|
|
169
|
+
event.stopPropagation();
|
|
170
|
+
onSelect(path.id);
|
|
171
|
+
}, children: [_jsx("primitive", { object: object }), selected
|
|
172
|
+
? path.points.map((point, index) => (_jsxs("mesh", { position: [point.x, yAt(point) + 0.8, point.z], children: [_jsx("sphereGeometry", { args: [index === activePointIndex ? 0.7 : 0.45, 8, 8] }), _jsx("meshBasicMaterial", { color: index === activePointIndex ? "#22d3ee" : "#ffffff" })] }, index)))
|
|
173
|
+
: null] }));
|
|
174
|
+
});
|
|
175
|
+
const NotePin = memo(function NotePin({ note, selected, onSelect, groundHeightAt, }) {
|
|
176
|
+
const color = colorFor("note", note.color);
|
|
177
|
+
const baseY = groundHeightAt !== undefined ? groundHeightAt(note.position.x, note.position.z) : note.position.y;
|
|
178
|
+
return (_jsxs("group", { position: [note.position.x, baseY + 1, note.position.z], userData: { jgEditorId: note.id }, onClick: (event) => {
|
|
179
|
+
event.stopPropagation();
|
|
180
|
+
onSelect(note.id);
|
|
181
|
+
}, children: [_jsxs("mesh", { scale: selected ? 1.3 : 1, children: [_jsx("octahedronGeometry", { args: [0.6] }), _jsx("meshBasicMaterial", { color: selected ? "#ffffff" : color })] }), _jsx(Html, { center: true, distanceFactor: 40, style: { pointerEvents: "none" }, children: _jsx("div", { style: {
|
|
182
|
+
transform: "translateY(-24px)",
|
|
183
|
+
whiteSpace: "nowrap",
|
|
184
|
+
maxWidth: "220px",
|
|
185
|
+
overflow: "hidden",
|
|
186
|
+
textOverflow: "ellipsis",
|
|
187
|
+
fontSize: "11px",
|
|
188
|
+
color: "#fde68a",
|
|
189
|
+
background: "rgba(0,0,0,0.6)",
|
|
190
|
+
borderRadius: "4px",
|
|
191
|
+
padding: "1px 6px",
|
|
192
|
+
}, children: note.text }) })] }));
|
|
193
|
+
});
|
|
194
|
+
/** Live preview of an in-progress path drawing: placed points and the connecting line. */
|
|
195
|
+
export function PathDraftPreview({ points }) {
|
|
196
|
+
const object = useMemo(() => {
|
|
197
|
+
if (points.length < 2)
|
|
198
|
+
return null;
|
|
199
|
+
const positions = new Float32Array(points.length * 3);
|
|
200
|
+
points.forEach((point, index) => {
|
|
201
|
+
positions[index * 3] = point.x;
|
|
202
|
+
positions[index * 3 + 1] = point.y + 0.8;
|
|
203
|
+
positions[index * 3 + 2] = point.z;
|
|
204
|
+
});
|
|
205
|
+
const geometry = new THREE.BufferGeometry();
|
|
206
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
207
|
+
const material = new THREE.LineBasicMaterial({ color: "#22d3ee" });
|
|
208
|
+
return new THREE.Line(geometry, material);
|
|
209
|
+
}, [points]);
|
|
210
|
+
useEffect(() => () => {
|
|
211
|
+
if (object === null)
|
|
212
|
+
return;
|
|
213
|
+
object.geometry.dispose();
|
|
214
|
+
object.material.dispose();
|
|
215
|
+
}, [object]);
|
|
216
|
+
return (_jsxs("group", { children: [object !== null ? _jsx("primitive", { object: object }) : null, points.map((point, index) => (_jsxs("mesh", { position: [point.x, point.y + 0.8, point.z], children: [_jsx("sphereGeometry", { args: [0.5, 8, 8] }), _jsx("meshBasicMaterial", { color: "#22d3ee" })] }, index)))] }));
|
|
217
|
+
}
|
|
218
|
+
/** Renders every visible marker, volume, path, and note from a document as in-scene 3D gizmos. */
|
|
219
|
+
export function EditorLayerOverlays({ document, visibility, selection, onSelect, activePathPoint, groundHeightAt, }) {
|
|
220
|
+
const selected = useMemo(() => new Set(selection), [selection]);
|
|
221
|
+
const sharedSphere = useMemo(() => new THREE.SphereGeometry(0.85, MARKER_SEGMENTS, MARKER_SEGMENTS), []);
|
|
222
|
+
const sharedCone = useMemo(() => new THREE.ConeGeometry(0.35, 0.9, 6), []);
|
|
223
|
+
useEffect(() => () => {
|
|
224
|
+
sharedSphere.dispose();
|
|
225
|
+
sharedCone.dispose();
|
|
226
|
+
}, [sharedSphere, sharedCone]);
|
|
227
|
+
const visibilityKey = useMemo(() => JSON.stringify(visibility), [visibility]);
|
|
228
|
+
const volumes = useMemo(() => document.volumes.filter((volume) => isVisible(visibility, volume.kind)), [document.volumes, visibilityKey, visibility]);
|
|
229
|
+
const paths = useMemo(() => document.paths.filter((path) => isVisible(visibility, path.kind)), [document.paths, visibilityKey, visibility]);
|
|
230
|
+
const markers = useMemo(() => document.markers.filter((marker) => isVisible(visibility, marker.kind)), [document.markers, visibilityKey, visibility]);
|
|
231
|
+
const notes = useMemo(() => (isVisible(visibility, "note") ? document.annotations : []), [document.annotations, visibilityKey, visibility]);
|
|
232
|
+
return (_jsxs("group", { children: [volumes.map((volume) => (_jsx(VolumeShapeLines, { volume: volume, selected: selected.has(volume.id), onSelect: onSelect }, volume.id))), paths.map((path) => (_jsx(PathRibbon, { path: path, selected: selected.has(path.id), activePointIndex: activePathPoint != null && activePathPoint.pathId === path.id ? activePathPoint.index : null, onSelect: onSelect, groundHeightAt: groundHeightAt }, path.id))), markers.map((marker) => (_jsx(MarkerMesh, { marker: marker, selected: selected.has(marker.id), onSelect: onSelect, sharedSphere: sharedSphere, sharedCone: sharedCone, groundHeightAt: groundHeightAt }, marker.id))), notes.map((note) => (_jsx(NotePin, { note: note, selected: selected.has(note.id), onSelect: onSelect, groundHeightAt: groundHeightAt }, note.id)))] }));
|
|
233
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ComponentType } from "react";
|
|
2
|
+
import type { EditorCatalogDefinition, EditorLayersInput } from "@jgengine/core/editor/index";
|
|
3
|
+
import type { PlayableGame } from "@jgengine/shell/registry";
|
|
4
|
+
import { type EditorRunMode } from "./session.js";
|
|
5
|
+
/** Props for mounting the scene editor over a playable game. */
|
|
6
|
+
export interface EditorAppProps {
|
|
7
|
+
gameId: string;
|
|
8
|
+
playable: PlayableGame;
|
|
9
|
+
layers?: EditorLayersInput;
|
|
10
|
+
/** Game-exported gameplay catalog definitions (schemas + defaults) for the Data panel / catalog RPC. */
|
|
11
|
+
catalogs?: readonly EditorCatalogDefinition[];
|
|
12
|
+
save?: EditorSaveFn;
|
|
13
|
+
/** Skin for the play/walk escape chip. Omit for the default pill; pass `null` to hide it (F2+E still exits); pass a component to place the game's own. */
|
|
14
|
+
modeChip?: ComponentType<{
|
|
15
|
+
mode: EditorRunMode;
|
|
16
|
+
onExit: () => void;
|
|
17
|
+
}> | null;
|
|
18
|
+
}
|
|
19
|
+
/** Persists an exported document JSON; resolves with where it landed or why it failed. */
|
|
20
|
+
export type EditorSaveFn = (json: string) => Promise<{
|
|
21
|
+
ok: boolean;
|
|
22
|
+
path?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
|
|
26
|
+
export declare function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }: EditorAppProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { editorDocumentBounds, findEditorMarker } from "@jgengine/core/editor/index";
|
|
4
|
+
import { getSaveEndpoint } from "@jgengine/core/devtools/saveEndpoint";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { GamePlayerShell } from "@jgengine/shell/GamePlayerShell";
|
|
7
|
+
import { assetsFromCatalog } from "./AssetBrowser.js";
|
|
8
|
+
import { EditorCameraDriver } from "./EditorCameraDriver.js";
|
|
9
|
+
import { EditorChrome } from "./EditorChrome.js";
|
|
10
|
+
import { EditorLayerOverlays, PathDraftPreview } from "./DebugDraw.js";
|
|
11
|
+
import { MaterialDropZone } from "./MaterialDropZone.js";
|
|
12
|
+
import { PerfProbe } from "./PerfProbe.js";
|
|
13
|
+
import { ScatterPreview } from "./ScatterPreview.js";
|
|
14
|
+
import { SelectionGizmo, ViewportSelect } from "./SelectionGizmo.js";
|
|
15
|
+
import { TerrainSculpt } from "./TerrainSculpt.js";
|
|
16
|
+
import { RuntimePlayInspectorChrome, RuntimePlayPublisher } from "./RuntimePlayBridge.js";
|
|
17
|
+
import { createEditorHost } from "./session.js";
|
|
18
|
+
import { createEditorUiStore } from "./uiStore.js";
|
|
19
|
+
import { useF2Chord } from "./useF2Chord.js";
|
|
20
|
+
import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
|
|
21
|
+
function endpointSaver(gameId) {
|
|
22
|
+
const endpoint = getSaveEndpoint();
|
|
23
|
+
if (endpoint === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
return async (json) => {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(endpoint.url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "content-type": "application/json" },
|
|
30
|
+
body: JSON.stringify({ kind: "editor-document", gameId, json }),
|
|
31
|
+
});
|
|
32
|
+
return (await response.json());
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function prefsKey(gameId) {
|
|
40
|
+
return `jgeditor:prefs:${gameId}`;
|
|
41
|
+
}
|
|
42
|
+
function draftKey(gameId) {
|
|
43
|
+
return `jgeditor:draft:${gameId}`;
|
|
44
|
+
}
|
|
45
|
+
function readDraft(gameId) {
|
|
46
|
+
try {
|
|
47
|
+
return localStorage.getItem(draftKey(gameId));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function writeDraft(gameId, json) {
|
|
54
|
+
try {
|
|
55
|
+
if (json === null)
|
|
56
|
+
localStorage.removeItem(draftKey(gameId));
|
|
57
|
+
else
|
|
58
|
+
localStorage.setItem(draftKey(gameId), json);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const DRAFT_DEBOUNCE_MS = 800;
|
|
65
|
+
function loadPrefs(gameId) {
|
|
66
|
+
try {
|
|
67
|
+
const raw = localStorage.getItem(prefsKey(gameId));
|
|
68
|
+
if (raw === null)
|
|
69
|
+
return {};
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function savePrefs(gameId, prefs) {
|
|
78
|
+
try {
|
|
79
|
+
localStorage.setItem(prefsKey(gameId), JSON.stringify(prefs));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function EditorWorldOverlay({ api, ui, world, }) {
|
|
86
|
+
const session = api.getSession();
|
|
87
|
+
const ctx = useGameContext();
|
|
88
|
+
const visibilityStore = useMemo(() => ({ getState: api.getVisibility, subscribe: api.subscribeVisibility }), [api]);
|
|
89
|
+
const document = useStoreSelector(session, (s) => s.document);
|
|
90
|
+
const selection = useStoreSelector(session, (s) => s.selection, shallowArrayEqual);
|
|
91
|
+
const visibility = useStoreSelector(visibilityStore, (v) => v);
|
|
92
|
+
const showGrid = useStoreSelector(ui, (s) => s.showGrid);
|
|
93
|
+
const pathPoint = useStoreSelector(ui, (s) => s.pathPoint);
|
|
94
|
+
const pathDraft = useStoreSelector(ui, (s) => s.pathDraft);
|
|
95
|
+
const groundHeightAt = useCallback((x, z) => ctx.world.groundHeightAt(x, z), [ctx.world]);
|
|
96
|
+
return (_jsxs(_Fragment, { children: [_jsx(PerfProbe, { api: api }), _jsx(EditorCameraDriver, { api: api }), _jsx(ViewportSelect, { api: api, ui: ui }), _jsx(MaterialDropZone, { api: api }), _jsx(TerrainSculpt, { api: api, ui: ui, world: world }), _jsx(ScatterPreview, { api: api }), showGrid ? _jsx("gridHelper", { args: [400, 80, "#3b4252", "#20242e"], position: [0, 0.05, 0] }) : null, _jsx(EditorLayerOverlays, { document: document, visibility: visibility, selection: selection, onSelect: (id) => session.dispatch({ type: "select", ids: [id] }), activePathPoint: pathPoint, groundHeightAt: groundHeightAt }), pathDraft.length > 0 ? _jsx(PathDraftPreview, { points: pathDraft }) : null, _jsx(SelectionGizmo, { session: session, ui: ui, groundSnap: groundHeightAt })] }));
|
|
97
|
+
}
|
|
98
|
+
/** Default skin for the play/walk escape chip; swap via `EditorAppProps.modeChip`, or pass `null` to hide it (F2+E still exits). */
|
|
99
|
+
function EditorModeChip({ mode, onExit }) {
|
|
100
|
+
return (_jsx("div", { className: "pointer-events-none absolute inset-x-0 top-2 z-50 flex justify-center", children: _jsxs("button", { type: "button", className: "pointer-events-auto rounded-full border border-white/10 bg-black/70 px-4 py-1.5 text-[11px] text-neutral-100 shadow-lg shadow-black/40 backdrop-blur-md transition-colors hover:bg-black/90", onClick: onExit, children: [mode === "play" ? "▶ Playing" : "🚶 Walking", " \u2014 back to editor (F2+E)"] }) }));
|
|
101
|
+
}
|
|
102
|
+
/** Headless placement: wires the F2+E exit chord and renders `chip` (the default skin, a
|
|
103
|
+
* game-supplied override, or nothing when `chip` is `null`) — the chord always works either way. */
|
|
104
|
+
function EditorModeChipHost({ api, mode, chip, }) {
|
|
105
|
+
const onExit = useCallback(() => api.setMode("edit"), [api]);
|
|
106
|
+
useF2Chord("KeyE", onExit);
|
|
107
|
+
if (chip === null)
|
|
108
|
+
return null;
|
|
109
|
+
const Chip = chip;
|
|
110
|
+
return _jsx(Chip, { mode: mode, onExit: onExit });
|
|
111
|
+
}
|
|
112
|
+
/** Prefer a grounded spawn for the first shot — full-world framing clips past default far=300. */
|
|
113
|
+
function resolveEditorCamera(document) {
|
|
114
|
+
const spawn = findEditorMarker(document, "player_spawn") ??
|
|
115
|
+
findEditorMarker(document, "car_start") ??
|
|
116
|
+
document.markers[0];
|
|
117
|
+
const bounds = editorDocumentBounds(document);
|
|
118
|
+
const worldSpan = bounds === null
|
|
119
|
+
? 200
|
|
120
|
+
: Math.max(80, Math.hypot(bounds.max.x - bounds.min.x, bounds.max.z - bounds.min.z) * 0.55);
|
|
121
|
+
if (spawn !== undefined) {
|
|
122
|
+
return {
|
|
123
|
+
target: { x: spawn.position.x, y: spawn.position.y + 8, z: spawn.position.z },
|
|
124
|
+
span: Math.min(180, Math.max(60, worldSpan * 0.12)),
|
|
125
|
+
far: Math.max(4000, worldSpan * 4),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (bounds === null) {
|
|
129
|
+
return { target: { x: 0, y: 12, z: 0 }, span: 80, far: 4000 };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
target: {
|
|
133
|
+
x: (bounds.min.x + bounds.max.x) / 2,
|
|
134
|
+
y: (bounds.min.y + bounds.max.y) / 2 + 12,
|
|
135
|
+
z: (bounds.min.z + bounds.max.z) / 2,
|
|
136
|
+
},
|
|
137
|
+
span: Math.min(220, worldSpan),
|
|
138
|
+
far: Math.max(4000, worldSpan * 4),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
|
|
142
|
+
export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }) {
|
|
143
|
+
const resolvedModeChip = modeChip === undefined ? EditorModeChip : modeChip;
|
|
144
|
+
const saveFn = useMemo(() => save ?? endpointSaver(gameId), [save, gameId]);
|
|
145
|
+
const uiStoreRef = useRef(null);
|
|
146
|
+
if (uiStoreRef.current === null)
|
|
147
|
+
uiStoreRef.current = createEditorUiStore();
|
|
148
|
+
const ui = uiStoreRef.current;
|
|
149
|
+
const [mode, setModeState] = useState("edit");
|
|
150
|
+
const catalogAssets = useMemo(() => {
|
|
151
|
+
try {
|
|
152
|
+
const ids = playable.game.assets.ids();
|
|
153
|
+
return assetsFromCatalog(ids, (id) => playable.game.assets.resolve(id));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}, [playable]);
|
|
159
|
+
const host = useMemo(() => {
|
|
160
|
+
const created = createEditorHost({
|
|
161
|
+
gameId,
|
|
162
|
+
layers,
|
|
163
|
+
catalogs,
|
|
164
|
+
assets: catalogAssets,
|
|
165
|
+
});
|
|
166
|
+
return { ...created, baselineJson: created.session.exportJson(true) };
|
|
167
|
+
}, [gameId, layers, catalogs, catalogAssets]);
|
|
168
|
+
useEffect(() => host.dispose, [host]);
|
|
169
|
+
useEffect(() => host.api.subscribeMode(setModeState), [host]);
|
|
170
|
+
const [pendingDraft, setPendingDraft] = useState(null);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const draft = readDraft(gameId);
|
|
173
|
+
if (draft !== null && draft !== host.baselineJson)
|
|
174
|
+
setPendingDraft(draft);
|
|
175
|
+
let timer = null;
|
|
176
|
+
const unsubscribe = host.session.subscribe(() => {
|
|
177
|
+
if (timer !== null)
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
timer = setTimeout(() => {
|
|
180
|
+
const json = host.session.exportJson(true);
|
|
181
|
+
writeDraft(gameId, json === host.baselineJson ? null : json);
|
|
182
|
+
}, DRAFT_DEBOUNCE_MS);
|
|
183
|
+
});
|
|
184
|
+
return () => {
|
|
185
|
+
if (timer !== null)
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
unsubscribe();
|
|
188
|
+
};
|
|
189
|
+
}, [gameId, host]);
|
|
190
|
+
const restoreDraft = () => {
|
|
191
|
+
if (pendingDraft === null)
|
|
192
|
+
return;
|
|
193
|
+
try {
|
|
194
|
+
host.session.dispatch({ type: "importJson", json: pendingDraft });
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
writeDraft(gameId, null);
|
|
198
|
+
}
|
|
199
|
+
setPendingDraft(null);
|
|
200
|
+
};
|
|
201
|
+
const discardDraft = () => {
|
|
202
|
+
writeDraft(gameId, null);
|
|
203
|
+
setPendingDraft(null);
|
|
204
|
+
};
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const prefs = loadPrefs(gameId);
|
|
207
|
+
if (prefs.visibility !== undefined) {
|
|
208
|
+
host.api.setVisibility({ ...host.api.getVisibility(), ...prefs.visibility });
|
|
209
|
+
}
|
|
210
|
+
ui.patch({
|
|
211
|
+
...(prefs.snapMode === undefined ? {} : { snapMode: prefs.snapMode }),
|
|
212
|
+
...(prefs.gridSize === undefined ? {} : { gridSize: prefs.gridSize }),
|
|
213
|
+
...(prefs.showGrid === undefined ? {} : { showGrid: prefs.showGrid }),
|
|
214
|
+
});
|
|
215
|
+
const persist = () => {
|
|
216
|
+
const state = ui.getState();
|
|
217
|
+
savePrefs(gameId, {
|
|
218
|
+
visibility: host.api.getVisibility(),
|
|
219
|
+
snapMode: state.snapMode,
|
|
220
|
+
gridSize: state.gridSize,
|
|
221
|
+
showGrid: state.showGrid,
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
const unsubscribeVisibility = host.api.subscribeVisibility(persist);
|
|
225
|
+
const unsubscribeUi = ui.subscribe(persist);
|
|
226
|
+
return () => {
|
|
227
|
+
unsubscribeVisibility();
|
|
228
|
+
unsubscribeUi();
|
|
229
|
+
};
|
|
230
|
+
}, [gameId, host, ui]);
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (typeof document === "undefined")
|
|
233
|
+
return;
|
|
234
|
+
document.documentElement.dataset.jgEditor = "1";
|
|
235
|
+
document.documentElement.dataset.jgEditorGame = gameId;
|
|
236
|
+
const frame = requestAnimationFrame(() => {
|
|
237
|
+
requestAnimationFrame(() => {
|
|
238
|
+
document.documentElement.dataset.jgFrameReady = "1";
|
|
239
|
+
if (document.documentElement.dataset.jgCapture === "preparing") {
|
|
240
|
+
document.documentElement.dataset.jgCapture = "ready";
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
return () => {
|
|
245
|
+
cancelAnimationFrame(frame);
|
|
246
|
+
delete document.documentElement.dataset.jgEditor;
|
|
247
|
+
delete document.documentElement.dataset.jgEditorGame;
|
|
248
|
+
};
|
|
249
|
+
}, [gameId]);
|
|
250
|
+
const initialCamera = useMemo(() => resolveEditorCamera(host.session.getState().document), [host]);
|
|
251
|
+
const editorPlayable = useMemo(() => {
|
|
252
|
+
const frozenLoop = {
|
|
253
|
+
onInit: playable.loop.onInit,
|
|
254
|
+
onNewPlayer: playable.loop.onNewPlayer,
|
|
255
|
+
onTick: () => {
|
|
256
|
+
// Placement/walk views freeze combat/AI so the frame isn't burned on sim.
|
|
257
|
+
},
|
|
258
|
+
onPlayerLeave: playable.loop.onPlayerLeave,
|
|
259
|
+
onReset: playable.loop.onReset,
|
|
260
|
+
onDispose: playable.loop.onDispose,
|
|
261
|
+
};
|
|
262
|
+
if (mode === "play") {
|
|
263
|
+
const BaseUI = playable.GameUI;
|
|
264
|
+
const BaseOverlay = playable.WorldOverlay;
|
|
265
|
+
const useBuiltinPlayChrome = resolvedModeChip === EditorModeChip;
|
|
266
|
+
return {
|
|
267
|
+
...playable,
|
|
268
|
+
GameUI: function EditorPlayUi() {
|
|
269
|
+
const onExit = useCallback(() => host.api.setMode("edit"), []);
|
|
270
|
+
useF2Chord("KeyE", onExit);
|
|
271
|
+
return (_jsxs(_Fragment, { children: [BaseUI !== undefined ? _jsx(BaseUI, {}) : null, useBuiltinPlayChrome ? (_jsx(RuntimePlayInspectorChrome, { api: host.api, onExit: onExit })) : (_jsx(EditorModeChipHost, { api: host.api, mode: "play", chip: resolvedModeChip }))] }));
|
|
272
|
+
},
|
|
273
|
+
WorldOverlay: function EditorPlayOverlay({ ctx }) {
|
|
274
|
+
return (_jsxs(_Fragment, { children: [BaseOverlay !== undefined ? _jsx(BaseOverlay, { ctx: ctx }) : null, _jsx(PerfProbe, { api: host.api }), _jsx(RuntimePlayPublisher, { api: host.api })] }));
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (mode === "walk") {
|
|
279
|
+
return {
|
|
280
|
+
...playable,
|
|
281
|
+
GameUI: function EditorWalkUi() {
|
|
282
|
+
return _jsx(EditorModeChipHost, { api: host.api, mode: "walk", chip: resolvedModeChip });
|
|
283
|
+
},
|
|
284
|
+
WorldOverlay: function EditorWalkOverlay() {
|
|
285
|
+
const ctx = useGameContext();
|
|
286
|
+
return (_jsxs(_Fragment, { children: [_jsx(PerfProbe, { api: host.api }), _jsx(EditorLayerOverlays, { document: host.api.getSession().getState().document, visibility: host.api.getVisibility(), selection: host.api.getSession().getState().selection, onSelect: () => { }, groundHeightAt: (x, z) => ctx.world.groundHeightAt(x, z) })] }));
|
|
287
|
+
},
|
|
288
|
+
loop: frozenLoop,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const WorldOverlay = function EditorOverlay() {
|
|
292
|
+
return _jsx(EditorWorldOverlay, { api: host.api, ui: ui, world: playable.game.world });
|
|
293
|
+
};
|
|
294
|
+
const GameUI = function EditorUi() {
|
|
295
|
+
return _jsx(EditorChrome, { gameId: gameId, session: host.api.getSession(), api: host.api, assets: catalogAssets, ui: ui, baselineJson: host.baselineJson, save: saveFn });
|
|
296
|
+
};
|
|
297
|
+
const { target, span, far } = initialCamera;
|
|
298
|
+
return {
|
|
299
|
+
...playable,
|
|
300
|
+
GameUI,
|
|
301
|
+
WorldOverlay,
|
|
302
|
+
shadows: false,
|
|
303
|
+
loop: frozenLoop,
|
|
304
|
+
// Drop FPS gun chrome; keep world only.
|
|
305
|
+
// WorldOverlay above is the editor layers, not PandoraViewmodel.
|
|
306
|
+
camera: {
|
|
307
|
+
rig: "inspection",
|
|
308
|
+
frustum: {
|
|
309
|
+
fov: 55,
|
|
310
|
+
near: 0.5,
|
|
311
|
+
far,
|
|
312
|
+
},
|
|
313
|
+
inspection: {
|
|
314
|
+
target,
|
|
315
|
+
initialDistance: span,
|
|
316
|
+
initialPosition: {
|
|
317
|
+
x: target.x + span * 0.35,
|
|
318
|
+
y: target.y + span * 0.55,
|
|
319
|
+
z: target.z + span * 0.75,
|
|
320
|
+
},
|
|
321
|
+
minDistance: 4,
|
|
322
|
+
maxDistance: Math.max(far * 0.9, span * 8),
|
|
323
|
+
pan: true,
|
|
324
|
+
rotateSpeed: 0.7,
|
|
325
|
+
zoomSpeed: 1.1,
|
|
326
|
+
minPolarAngle: 0.15,
|
|
327
|
+
maxPolarAngle: Math.PI * 0.48,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}, [playable, host, gameId, initialCamera, catalogAssets, ui, mode, saveFn, resolvedModeChip]);
|
|
332
|
+
return (_jsxs("div", { className: "relative h-full w-full bg-neutral-950", "data-jg-editor": "1", "data-jg-editor-game": gameId, children: [_jsx(GamePlayerShell, { playable: editorPlayable }), pendingDraft !== null && mode === "edit" ? (_jsxs("div", { className: "absolute left-1/2 top-14 z-[60] flex -translate-x-1/2 items-center gap-3 rounded-xl border border-amber-400/30 bg-amber-950/90 px-4 py-2 text-xs text-amber-100 shadow-2xl shadow-black/50 backdrop-blur-md", children: [_jsx("span", { children: "Unsaved edits from a previous session found." }), _jsx("button", { type: "button", className: "rounded-md bg-gradient-to-b from-amber-500 to-amber-600 px-2.5 py-1 font-semibold text-white shadow-md shadow-amber-950/50 transition-colors hover:from-amber-400 hover:to-amber-500", onClick: restoreDraft, children: "Restore" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2.5 py-1 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/15", onClick: discardDraft, children: "Discard" })] })) : null] }));
|
|
333
|
+
}
|