@jgengine/core 0.9.0 → 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 +21 -1
- package/dist/ai/spawnDirector.d.ts +13 -0
- package/dist/ai/spawnDirector.js +40 -16
- package/dist/ai/spawnPoint.d.ts +5 -0
- package/dist/ai/spawnPoint.js +5 -0
- package/dist/audio/audioFalloff.d.ts +5 -2
- package/dist/audio/music.d.ts +53 -0
- package/dist/audio/music.js +34 -0
- package/dist/audio/synth.d.ts +60 -0
- package/dist/audio/synth.js +8 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +14 -0
- package/dist/cartridge/runtime.js +2 -2
- package/dist/combat/dotField.d.ts +43 -0
- package/dist/combat/dotField.js +48 -0
- package/dist/combat/projectiles.d.ts +2 -2
- package/dist/combat/projectiles.js +18 -5
- package/dist/combat/regenShield.d.ts +35 -0
- package/dist/combat/regenShield.js +45 -0
- package/dist/combat/shotOrigin.d.ts +13 -1
- package/dist/combat/shotOrigin.js +31 -2
- package/dist/editor/commands.d.ts +65 -0
- package/dist/editor/commands.js +193 -0
- package/dist/editor/document.d.ts +40 -0
- package/dist/editor/document.js +153 -0
- package/dist/editor/index.d.ts +30 -0
- package/dist/editor/index.js +6 -0
- package/dist/editor/types.d.ts +69 -0
- package/dist/editor/types.js +29 -0
- package/dist/game/connectedPlayers.d.ts +28 -0
- package/dist/game/connectedPlayers.js +21 -0
- package/dist/game/defineGame.d.ts +42 -1
- package/dist/game/defineGame.js +1 -0
- package/dist/game/events.d.ts +15 -0
- package/dist/game/keyValueStore.d.ts +25 -0
- package/dist/game/keyValueStore.js +61 -0
- package/dist/game/levelSequence.d.ts +24 -2
- package/dist/game/levelSequence.js +51 -2
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +1 -0
- package/dist/game/modelAnimation.d.ts +8 -0
- package/dist/game/modelAnimation.js +18 -0
- package/dist/game/playableGame.d.ts +68 -5
- package/dist/game/recordBook.d.ts +3 -5
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/input/actionBindings.d.ts +2 -0
- package/dist/input/actionBindings.js +1 -0
- package/dist/input/axisInput.d.ts +7 -0
- package/dist/input/axisInput.js +16 -0
- package/dist/interaction/contextMenu.d.ts +1 -0
- package/dist/interaction/contextMenu.js +1 -0
- package/dist/interaction/proximityPrompt.d.ts +3 -1
- package/dist/interaction/proximityPrompt.js +3 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +22 -1
- package/dist/movement/movementModel.d.ts +4 -0
- package/dist/movement/movementModel.js +13 -0
- package/dist/movement/playerMovement.d.ts +41 -0
- package/dist/movement/playerMovement.js +181 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +2 -0
- package/dist/render/postProcessing.d.ts +56 -0
- package/dist/render/postProcessing.js +1 -0
- package/dist/runtime/adapter.d.ts +19 -0
- package/dist/runtime/adapter.js +11 -5
- package/dist/runtime/gameContext.d.ts +53 -10
- package/dist/runtime/gameContext.js +192 -54
- package/dist/runtime/hostedGameRunner.d.ts +49 -0
- package/dist/runtime/hostedGameRunner.js +62 -0
- package/dist/runtime/hostedWorldSession.d.ts +65 -0
- package/dist/runtime/hostedWorldSession.js +57 -0
- package/dist/runtime/inputSnapshot.d.ts +13 -0
- package/dist/runtime/inputSnapshot.js +2 -0
- package/dist/runtime/motionIntents.d.ts +2 -0
- package/dist/runtime/motionIntents.js +11 -0
- package/dist/runtime/worldChannel.d.ts +58 -0
- package/dist/runtime/worldChannel.js +69 -0
- package/dist/runtime/worldMirror.d.ts +24 -0
- package/dist/runtime/worldMirror.js +31 -0
- package/dist/runtime/worldReplication.d.ts +46 -0
- package/dist/runtime/worldReplication.js +185 -0
- package/dist/runtime/worldSnapshot.d.ts +17 -0
- package/dist/runtime/worldSnapshot.js +15 -0
- package/dist/scene/assetCatalog.d.ts +1 -0
- package/dist/scene/colliders.d.ts +6 -0
- package/dist/scene/colliders.js +25 -1
- package/dist/scene/entityStats.d.ts +4 -0
- package/dist/scene/entityStats.js +21 -0
- package/dist/scene/entityStore.d.ts +29 -0
- package/dist/scene/entityStore.js +60 -0
- package/dist/store/observableKeyedStore.d.ts +4 -0
- package/dist/store/observableKeyedStore.js +9 -0
- package/dist/tactics/fallingGrid.d.ts +2 -2
- package/dist/tactics/fallingGrid.js +2 -2
- package/dist/ui/gameLayout.d.ts +125 -0
- package/dist/ui/gameLayout.js +129 -0
- package/dist/ui/orientation.d.ts +39 -0
- package/dist/ui/orientation.js +49 -0
- package/dist/ui/swingTimer.d.ts +42 -0
- package/dist/ui/swingTimer.js +38 -0
- package/dist/visibility/spatialIndex.js +34 -20
- package/dist/world/environmentSummary.d.ts +11 -2
- package/dist/world/environmentSummary.js +11 -1
- package/dist/world/features.d.ts +134 -5
- package/dist/world/features.js +40 -3
- package/dist/world/roads.d.ts +47 -0
- package/dist/world/roads.js +146 -0
- package/dist/world/streets.d.ts +80 -0
- package/dist/world/streets.js +144 -0
- package/dist/world/terrain.d.ts +32 -5
- package/dist/world/terrain.js +124 -12
- package/llms.txt +485 -79
- package/package.json +1 -1
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
import { type EntityColliderSet } from "../scene/colliders.js";
|
|
2
3
|
import type { Aim } from "../scene/spatial.js";
|
|
3
4
|
/**
|
|
4
5
|
* How a shot's world-space origin (and optional direction) is resolved before prediction/settlement.
|
|
5
|
-
* - `
|
|
6
|
+
* - `eye` — `aim.origin` when present, else the shooter's entity position raised to eye height; the
|
|
7
|
+
* shot traces the shooter's sightline, so what the crosshair covers is what gets hit (the default).
|
|
8
|
+
* - `legacy` — `aim.origin` when present, else the shooter's raw entity position (feet).
|
|
6
9
|
* - `entity` — always the shooter's entity position.
|
|
7
10
|
* - `entityOffset` / `muzzle` — entity-local offset rotated by the shooter's yaw (muzzle on a weapon model).
|
|
8
11
|
* - `camera` — explicit camera/reticle world origin (and optional direction override).
|
|
9
12
|
* - `world` — absolute world origin.
|
|
10
13
|
*/
|
|
11
14
|
export type ShotOriginPolicy = {
|
|
15
|
+
kind: "eye";
|
|
16
|
+
height?: number;
|
|
17
|
+
} | {
|
|
12
18
|
kind: "legacy";
|
|
13
19
|
} | {
|
|
14
20
|
kind: "entity";
|
|
@@ -34,7 +40,13 @@ export interface ResolvedShot {
|
|
|
34
40
|
export interface ShotOriginDeps {
|
|
35
41
|
positionOf(instanceId: string): EntityPosition | undefined;
|
|
36
42
|
rotationYOf?(instanceId: string): number | undefined;
|
|
43
|
+
/** When provided, the `eye` policy sizes its height from the shooter's own hitbox instead of the humanoid default. */
|
|
44
|
+
collidersOf?(instanceId: string): EntityColliderSet | null | undefined;
|
|
37
45
|
}
|
|
46
|
+
/** Shot-origin and first-person camera eye height above an entity's position: 90% of the default 1.8m hitbox top. */
|
|
47
|
+
export declare const DEFAULT_EYE_HEIGHT: number;
|
|
48
|
+
/** Eye height derived from a collider set: 90% of the tallest hitbox top, or the humanoid default when unknown. */
|
|
49
|
+
export declare function eyeHeightFromColliders(set: EntityColliderSet | null | undefined): number;
|
|
38
50
|
export declare function aimDirection(aim: Aim): EntityPosition | null;
|
|
39
51
|
export declare function aimSpreadDeg(aim: Aim): number;
|
|
40
52
|
export declare function resolveShot(deps: ShotOriginDeps, from: string, aim: Aim, policy?: ShotOriginPolicy): ResolvedShot | null;
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import { worldOffset } from "../scene/colliders.js";
|
|
1
|
+
import { resolveColliders, worldOffset } from "../scene/colliders.js";
|
|
2
2
|
const DEFAULT_MUZZLE_OFFSET = [0, 1.4, 0.35];
|
|
3
|
+
const EYE_HEIGHT_RATIO = 0.9;
|
|
4
|
+
/** Shot-origin and first-person camera eye height above an entity's position: 90% of the default 1.8m hitbox top. */
|
|
5
|
+
export const DEFAULT_EYE_HEIGHT = EYE_HEIGHT_RATIO * 1.8;
|
|
6
|
+
/** Eye height derived from a collider set: 90% of the tallest hitbox top, or the humanoid default when unknown. */
|
|
7
|
+
export function eyeHeightFromColliders(set) {
|
|
8
|
+
const colliders = resolveColliders(set);
|
|
9
|
+
let top = 0;
|
|
10
|
+
for (const collider of colliders) {
|
|
11
|
+
const offsetY = collider.shape.offset?.[1] ?? 0;
|
|
12
|
+
const halfHeight = collider.shape.kind === "sphere" ? collider.shape.radius : collider.shape.halfExtents[1];
|
|
13
|
+
top = Math.max(top, offsetY + halfHeight);
|
|
14
|
+
}
|
|
15
|
+
return top > 0 ? top * EYE_HEIGHT_RATIO : DEFAULT_EYE_HEIGHT;
|
|
16
|
+
}
|
|
3
17
|
function normalize(vector) {
|
|
4
18
|
const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
|
|
5
19
|
if (length === 0)
|
|
@@ -15,11 +29,26 @@ export function aimDirection(aim) {
|
|
|
15
29
|
export function aimSpreadDeg(aim) {
|
|
16
30
|
return "origin" in aim ? 0 : aim.spread ?? 0;
|
|
17
31
|
}
|
|
18
|
-
export function resolveShot(deps, from, aim, policy = { kind: "
|
|
32
|
+
export function resolveShot(deps, from, aim, policy = { kind: "eye" }) {
|
|
19
33
|
const aimDir = aimDirection(aim);
|
|
20
34
|
if (aimDir === null && policy.kind !== "camera" && policy.kind !== "world")
|
|
21
35
|
return null;
|
|
22
36
|
switch (policy.kind) {
|
|
37
|
+
case "eye": {
|
|
38
|
+
if ("origin" in aim) {
|
|
39
|
+
if (aimDir === null)
|
|
40
|
+
return null;
|
|
41
|
+
return { origin: aim.origin, direction: aimDir };
|
|
42
|
+
}
|
|
43
|
+
const position = deps.positionOf(from);
|
|
44
|
+
if (position === undefined || aimDir === null)
|
|
45
|
+
return null;
|
|
46
|
+
const height = policy.height ??
|
|
47
|
+
(deps.collidersOf !== undefined
|
|
48
|
+
? eyeHeightFromColliders(deps.collidersOf(from))
|
|
49
|
+
: DEFAULT_EYE_HEIGHT);
|
|
50
|
+
return { origin: [position[0], position[1] + height, position[2]], direction: aimDir };
|
|
51
|
+
}
|
|
23
52
|
case "legacy": {
|
|
24
53
|
const origin = "origin" in aim ? aim.origin : deps.positionOf(from);
|
|
25
54
|
if (origin === undefined || aimDir === null)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { EditorDocument, EditorMarker, EditorVec3, EditorVolume } from "./types.js";
|
|
2
|
+
/** A single editor mutation — select, move, add, remove, undo/redo — dispatched to a session. */
|
|
3
|
+
export type EditorCommand = {
|
|
4
|
+
type: "select";
|
|
5
|
+
ids: readonly string[];
|
|
6
|
+
} | {
|
|
7
|
+
type: "clearSelection";
|
|
8
|
+
} | {
|
|
9
|
+
type: "setTransform";
|
|
10
|
+
id: string;
|
|
11
|
+
position?: EditorVec3;
|
|
12
|
+
rotationY?: number;
|
|
13
|
+
} | {
|
|
14
|
+
type: "addMarker";
|
|
15
|
+
marker: EditorMarker;
|
|
16
|
+
} | {
|
|
17
|
+
type: "remove";
|
|
18
|
+
id: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: "setVolume";
|
|
21
|
+
id: string;
|
|
22
|
+
patch: Partial<Omit<EditorVolume, "id">>;
|
|
23
|
+
} | {
|
|
24
|
+
type: "addVolume";
|
|
25
|
+
volume: EditorVolume;
|
|
26
|
+
} | {
|
|
27
|
+
type: "importDocument";
|
|
28
|
+
document: EditorDocument;
|
|
29
|
+
} | {
|
|
30
|
+
type: "importJson";
|
|
31
|
+
json: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: "replaceDocument";
|
|
34
|
+
document: EditorDocument;
|
|
35
|
+
} | {
|
|
36
|
+
type: "undo";
|
|
37
|
+
} | {
|
|
38
|
+
type: "redo";
|
|
39
|
+
};
|
|
40
|
+
/** The document plus current selection at a point in editor history. */
|
|
41
|
+
export interface EditorSessionState {
|
|
42
|
+
document: EditorDocument;
|
|
43
|
+
selection: string[];
|
|
44
|
+
}
|
|
45
|
+
/** Stateful, undoable handle for driving scene edits from UI or an MCP agent. */
|
|
46
|
+
export interface EditorSession {
|
|
47
|
+
getState(): EditorSessionState;
|
|
48
|
+
subscribe(listener: (state: EditorSessionState) => void): () => void;
|
|
49
|
+
dispatch(command: EditorCommand): EditorSessionState;
|
|
50
|
+
exportJson(pretty?: boolean): string;
|
|
51
|
+
canUndo(): boolean;
|
|
52
|
+
canRedo(): boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Creates an editor session with undo/redo history seeded from an initial document. */
|
|
55
|
+
export declare function createEditorSession(initial: EditorDocument, historyLimit?: number): EditorSession;
|
|
56
|
+
/** Compact snapshot of a session state — counts, selection, and the selected object. */
|
|
57
|
+
export declare function summarizeEditorSession(state: EditorSessionState): {
|
|
58
|
+
markers: number;
|
|
59
|
+
volumes: number;
|
|
60
|
+
paths: number;
|
|
61
|
+
annotations: number;
|
|
62
|
+
selection: string[];
|
|
63
|
+
selectedMarker?: EditorMarker;
|
|
64
|
+
selectedVolume?: EditorVolume;
|
|
65
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { cloneEditorDocument, findEditorMarker, findEditorVolume, importEditorDocumentJson, } from "./document.js";
|
|
2
|
+
function snapshotState(state) {
|
|
3
|
+
return {
|
|
4
|
+
document: cloneEditorDocument(state.document),
|
|
5
|
+
selection: [...state.selection],
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function removeById(doc, id) {
|
|
9
|
+
return {
|
|
10
|
+
version: 1,
|
|
11
|
+
markers: doc.markers.filter((marker) => marker.id !== id),
|
|
12
|
+
volumes: doc.volumes.filter((volume) => volume.id !== id),
|
|
13
|
+
paths: doc.paths.filter((path) => path.id !== id),
|
|
14
|
+
annotations: doc.annotations.filter((note) => note.id !== id),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function applyMutating(state, command) {
|
|
18
|
+
switch (command.type) {
|
|
19
|
+
case "select":
|
|
20
|
+
return { ...state, selection: [...command.ids] };
|
|
21
|
+
case "clearSelection":
|
|
22
|
+
return { ...state, selection: [] };
|
|
23
|
+
case "setTransform": {
|
|
24
|
+
const markers = state.document.markers.map((marker) => {
|
|
25
|
+
if (marker.id !== command.id)
|
|
26
|
+
return marker;
|
|
27
|
+
return {
|
|
28
|
+
...marker,
|
|
29
|
+
position: command.position === undefined ? marker.position : { ...command.position },
|
|
30
|
+
...(command.rotationY === undefined ? {} : { rotationY: command.rotationY }),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
const volumes = state.document.volumes.map((volume) => {
|
|
34
|
+
if (volume.id !== command.id)
|
|
35
|
+
return volume;
|
|
36
|
+
if (command.position === undefined)
|
|
37
|
+
return volume;
|
|
38
|
+
return { ...volume, center: { ...command.position } };
|
|
39
|
+
});
|
|
40
|
+
const annotations = state.document.annotations.map((note) => {
|
|
41
|
+
if (note.id !== command.id)
|
|
42
|
+
return note;
|
|
43
|
+
if (command.position === undefined)
|
|
44
|
+
return note;
|
|
45
|
+
return { ...note, position: { ...command.position } };
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
...state,
|
|
49
|
+
document: { ...state.document, markers, volumes, annotations },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
case "addMarker":
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
document: {
|
|
56
|
+
...state.document,
|
|
57
|
+
markers: [...state.document.markers.filter((m) => m.id !== command.marker.id), command.marker],
|
|
58
|
+
},
|
|
59
|
+
selection: [command.marker.id],
|
|
60
|
+
};
|
|
61
|
+
case "addVolume":
|
|
62
|
+
return {
|
|
63
|
+
...state,
|
|
64
|
+
document: {
|
|
65
|
+
...state.document,
|
|
66
|
+
volumes: [...state.document.volumes.filter((v) => v.id !== command.volume.id), command.volume],
|
|
67
|
+
},
|
|
68
|
+
selection: [command.volume.id],
|
|
69
|
+
};
|
|
70
|
+
case "setVolume": {
|
|
71
|
+
const volumes = state.document.volumes.map((volume) => {
|
|
72
|
+
if (volume.id !== command.id)
|
|
73
|
+
return volume;
|
|
74
|
+
return {
|
|
75
|
+
...volume,
|
|
76
|
+
...command.patch,
|
|
77
|
+
center: command.patch.center === undefined ? volume.center : { ...command.patch.center },
|
|
78
|
+
halfExtents: command.patch.halfExtents === undefined
|
|
79
|
+
? volume.halfExtents
|
|
80
|
+
: { ...command.patch.halfExtents },
|
|
81
|
+
meta: command.patch.meta === undefined ? volume.meta : { ...command.patch.meta },
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
return { ...state, document: { ...state.document, volumes } };
|
|
85
|
+
}
|
|
86
|
+
case "remove":
|
|
87
|
+
return {
|
|
88
|
+
document: removeById(state.document, command.id),
|
|
89
|
+
selection: state.selection.filter((id) => id !== command.id),
|
|
90
|
+
};
|
|
91
|
+
case "importDocument":
|
|
92
|
+
case "replaceDocument":
|
|
93
|
+
return {
|
|
94
|
+
document: cloneEditorDocument(command.document),
|
|
95
|
+
selection: [],
|
|
96
|
+
};
|
|
97
|
+
case "importJson":
|
|
98
|
+
return {
|
|
99
|
+
document: importEditorDocumentJson(command.json),
|
|
100
|
+
selection: [],
|
|
101
|
+
};
|
|
102
|
+
case "undo":
|
|
103
|
+
case "redo":
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function isStructural(command) {
|
|
108
|
+
return (command.type === "setTransform" ||
|
|
109
|
+
command.type === "addMarker" ||
|
|
110
|
+
command.type === "addVolume" ||
|
|
111
|
+
command.type === "setVolume" ||
|
|
112
|
+
command.type === "remove" ||
|
|
113
|
+
command.type === "importDocument" ||
|
|
114
|
+
command.type === "importJson" ||
|
|
115
|
+
command.type === "replaceDocument");
|
|
116
|
+
}
|
|
117
|
+
/** Creates an editor session with undo/redo history seeded from an initial document. */
|
|
118
|
+
export function createEditorSession(initial, historyLimit = 100) {
|
|
119
|
+
let state = {
|
|
120
|
+
document: cloneEditorDocument(initial),
|
|
121
|
+
selection: [],
|
|
122
|
+
};
|
|
123
|
+
const past = [];
|
|
124
|
+
const future = [];
|
|
125
|
+
const listeners = new Set();
|
|
126
|
+
const emit = () => {
|
|
127
|
+
for (const listener of listeners)
|
|
128
|
+
listener(state);
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
getState: () => state,
|
|
132
|
+
subscribe(listener) {
|
|
133
|
+
listeners.add(listener);
|
|
134
|
+
return () => {
|
|
135
|
+
listeners.delete(listener);
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
canUndo: () => past.length > 0,
|
|
139
|
+
canRedo: () => future.length > 0,
|
|
140
|
+
dispatch(command) {
|
|
141
|
+
if (command.type === "undo") {
|
|
142
|
+
const previous = past.pop();
|
|
143
|
+
if (previous === undefined)
|
|
144
|
+
return state;
|
|
145
|
+
future.push(snapshotState(state));
|
|
146
|
+
state = previous;
|
|
147
|
+
emit();
|
|
148
|
+
return state;
|
|
149
|
+
}
|
|
150
|
+
if (command.type === "redo") {
|
|
151
|
+
const next = future.pop();
|
|
152
|
+
if (next === undefined)
|
|
153
|
+
return state;
|
|
154
|
+
past.push(snapshotState(state));
|
|
155
|
+
state = next;
|
|
156
|
+
emit();
|
|
157
|
+
return state;
|
|
158
|
+
}
|
|
159
|
+
const next = applyMutating(state, command);
|
|
160
|
+
if (next === null)
|
|
161
|
+
return state;
|
|
162
|
+
if (isStructural(command)) {
|
|
163
|
+
past.push(snapshotState(state));
|
|
164
|
+
if (past.length > historyLimit)
|
|
165
|
+
past.shift();
|
|
166
|
+
future.length = 0;
|
|
167
|
+
}
|
|
168
|
+
state = next;
|
|
169
|
+
emit();
|
|
170
|
+
return state;
|
|
171
|
+
},
|
|
172
|
+
exportJson(pretty = true) {
|
|
173
|
+
return JSON.stringify(state.document, null, pretty ? 2 : undefined);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** Compact snapshot of a session state — counts, selection, and the selected object. */
|
|
178
|
+
export function summarizeEditorSession(state) {
|
|
179
|
+
const selectedId = state.selection[0];
|
|
180
|
+
return {
|
|
181
|
+
markers: state.document.markers.length,
|
|
182
|
+
volumes: state.document.volumes.length,
|
|
183
|
+
paths: state.document.paths.length,
|
|
184
|
+
annotations: state.document.annotations.length,
|
|
185
|
+
selection: [...state.selection],
|
|
186
|
+
...(selectedId === undefined
|
|
187
|
+
? {}
|
|
188
|
+
: {
|
|
189
|
+
selectedMarker: findEditorMarker(state.document, selectedId),
|
|
190
|
+
selectedVolume: findEditorVolume(state.document, selectedId),
|
|
191
|
+
}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EditorDocument, EditorLayersInput, EditorMarker, EditorNote, EditorPath, EditorVolume } from "./types.js";
|
|
2
|
+
/** Builds a fresh, empty editor document to start authoring a scene from scratch. */
|
|
3
|
+
export declare function createEmptyEditorDocument(): EditorDocument;
|
|
4
|
+
/** Deep-copies an editor document so edits never mutate the source. */
|
|
5
|
+
export declare function cloneEditorDocument(doc: EditorDocument): EditorDocument;
|
|
6
|
+
/** Resolves a game's `editorLayers` export — document, partial data, or factory — into a full document. */
|
|
7
|
+
export declare function normalizeEditorLayers(input: EditorLayersInput | undefined | null): EditorDocument;
|
|
8
|
+
/** Combines multiple editor documents' markers, volumes, paths, and notes into one. */
|
|
9
|
+
export declare function mergeEditorDocuments(...docs: readonly EditorDocument[]): EditorDocument;
|
|
10
|
+
/** Looks up a marker by id in an editor document. */
|
|
11
|
+
export declare function findEditorMarker(doc: EditorDocument, id: string): EditorMarker | undefined;
|
|
12
|
+
/** Looks up a volume by id in an editor document. */
|
|
13
|
+
export declare function findEditorVolume(doc: EditorDocument, id: string): EditorVolume | undefined;
|
|
14
|
+
/** Looks up a path by id in an editor document. */
|
|
15
|
+
export declare function findEditorPath(doc: EditorDocument, id: string): EditorPath | undefined;
|
|
16
|
+
/** Looks up an annotation note by id in an editor document. */
|
|
17
|
+
export declare function findEditorNote(doc: EditorDocument, id: string): EditorNote | undefined;
|
|
18
|
+
/** Lists the distinct marker, volume, and path kinds authored in a document. */
|
|
19
|
+
export declare function listEditorKinds(doc: EditorDocument): {
|
|
20
|
+
markers: string[];
|
|
21
|
+
volumes: string[];
|
|
22
|
+
paths: string[];
|
|
23
|
+
};
|
|
24
|
+
/** Serializes an editor document to JSON text for saving or export. */
|
|
25
|
+
export declare function exportEditorDocumentJson(doc: EditorDocument, pretty?: boolean): string;
|
|
26
|
+
/** Parses JSON text back into a normalized editor document. */
|
|
27
|
+
export declare function importEditorDocumentJson(raw: string): EditorDocument;
|
|
28
|
+
/** Computes the world-space min/max bounds spanning every object in a document, or null if empty. */
|
|
29
|
+
export declare function editorDocumentBounds(doc: EditorDocument): {
|
|
30
|
+
min: {
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
z: number;
|
|
34
|
+
};
|
|
35
|
+
max: {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
z: number;
|
|
39
|
+
};
|
|
40
|
+
} | null;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Builds a fresh, empty editor document to start authoring a scene from scratch. */
|
|
2
|
+
export function createEmptyEditorDocument() {
|
|
3
|
+
return {
|
|
4
|
+
version: 1,
|
|
5
|
+
markers: [],
|
|
6
|
+
volumes: [],
|
|
7
|
+
paths: [],
|
|
8
|
+
annotations: [],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Deep-copies an editor document so edits never mutate the source. */
|
|
12
|
+
export function cloneEditorDocument(doc) {
|
|
13
|
+
return {
|
|
14
|
+
version: 1,
|
|
15
|
+
markers: doc.markers.map((marker) => ({
|
|
16
|
+
...marker,
|
|
17
|
+
position: { ...marker.position },
|
|
18
|
+
...(marker.meta === undefined ? {} : { meta: { ...marker.meta } }),
|
|
19
|
+
})),
|
|
20
|
+
volumes: doc.volumes.map((volume) => ({
|
|
21
|
+
...volume,
|
|
22
|
+
center: { ...volume.center },
|
|
23
|
+
...(volume.halfExtents === undefined ? {} : { halfExtents: { ...volume.halfExtents } }),
|
|
24
|
+
...(volume.meta === undefined ? {} : { meta: { ...volume.meta } }),
|
|
25
|
+
})),
|
|
26
|
+
paths: doc.paths.map((path) => ({
|
|
27
|
+
...path,
|
|
28
|
+
points: path.points.map((point) => ({ ...point })),
|
|
29
|
+
...(path.meta === undefined ? {} : { meta: { ...path.meta } }),
|
|
30
|
+
})),
|
|
31
|
+
annotations: doc.annotations.map((note) => ({
|
|
32
|
+
...note,
|
|
33
|
+
position: { ...note.position },
|
|
34
|
+
...(note.meta === undefined ? {} : { meta: { ...note.meta } }),
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function asArray(value) {
|
|
39
|
+
return value === undefined ? [] : [...value];
|
|
40
|
+
}
|
|
41
|
+
/** Resolves a game's `editorLayers` export — document, partial data, or factory — into a full document. */
|
|
42
|
+
export function normalizeEditorLayers(input) {
|
|
43
|
+
if (input === undefined || input === null)
|
|
44
|
+
return createEmptyEditorDocument();
|
|
45
|
+
const resolved = typeof input === "function" ? input() : input;
|
|
46
|
+
return {
|
|
47
|
+
version: 1,
|
|
48
|
+
markers: asArray(resolved.markers),
|
|
49
|
+
volumes: asArray(resolved.volumes),
|
|
50
|
+
paths: asArray(resolved.paths),
|
|
51
|
+
annotations: asArray(resolved.annotations),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Combines multiple editor documents' markers, volumes, paths, and notes into one. */
|
|
55
|
+
export function mergeEditorDocuments(...docs) {
|
|
56
|
+
const out = createEmptyEditorDocument();
|
|
57
|
+
for (const doc of docs) {
|
|
58
|
+
out.markers.push(...doc.markers);
|
|
59
|
+
out.volumes.push(...doc.volumes);
|
|
60
|
+
out.paths.push(...doc.paths);
|
|
61
|
+
out.annotations.push(...doc.annotations);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/** Looks up a marker by id in an editor document. */
|
|
66
|
+
export function findEditorMarker(doc, id) {
|
|
67
|
+
return doc.markers.find((marker) => marker.id === id);
|
|
68
|
+
}
|
|
69
|
+
/** Looks up a volume by id in an editor document. */
|
|
70
|
+
export function findEditorVolume(doc, id) {
|
|
71
|
+
return doc.volumes.find((volume) => volume.id === id);
|
|
72
|
+
}
|
|
73
|
+
/** Looks up a path by id in an editor document. */
|
|
74
|
+
export function findEditorPath(doc, id) {
|
|
75
|
+
return doc.paths.find((path) => path.id === id);
|
|
76
|
+
}
|
|
77
|
+
/** Looks up an annotation note by id in an editor document. */
|
|
78
|
+
export function findEditorNote(doc, id) {
|
|
79
|
+
return doc.annotations.find((note) => note.id === id);
|
|
80
|
+
}
|
|
81
|
+
/** Lists the distinct marker, volume, and path kinds authored in a document. */
|
|
82
|
+
export function listEditorKinds(doc) {
|
|
83
|
+
const markers = new Set();
|
|
84
|
+
const volumes = new Set();
|
|
85
|
+
const paths = new Set();
|
|
86
|
+
for (const marker of doc.markers)
|
|
87
|
+
markers.add(marker.kind);
|
|
88
|
+
for (const volume of doc.volumes)
|
|
89
|
+
volumes.add(volume.kind);
|
|
90
|
+
for (const path of doc.paths)
|
|
91
|
+
paths.add(path.kind);
|
|
92
|
+
return {
|
|
93
|
+
markers: [...markers].sort(),
|
|
94
|
+
volumes: [...volumes].sort(),
|
|
95
|
+
paths: [...paths].sort(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Serializes an editor document to JSON text for saving or export. */
|
|
99
|
+
export function exportEditorDocumentJson(doc, pretty = true) {
|
|
100
|
+
return JSON.stringify(doc, null, pretty ? 2 : undefined);
|
|
101
|
+
}
|
|
102
|
+
/** Parses JSON text back into a normalized editor document. */
|
|
103
|
+
export function importEditorDocumentJson(raw) {
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
106
|
+
throw new Error("editor document must be an object");
|
|
107
|
+
}
|
|
108
|
+
return normalizeEditorLayers(parsed);
|
|
109
|
+
}
|
|
110
|
+
/** Computes the world-space min/max bounds spanning every object in a document, or null if empty. */
|
|
111
|
+
export function editorDocumentBounds(doc) {
|
|
112
|
+
let minX = Infinity;
|
|
113
|
+
let minY = Infinity;
|
|
114
|
+
let minZ = Infinity;
|
|
115
|
+
let maxX = -Infinity;
|
|
116
|
+
let maxY = -Infinity;
|
|
117
|
+
let maxZ = -Infinity;
|
|
118
|
+
let any = false;
|
|
119
|
+
const include = (x, y, z) => {
|
|
120
|
+
any = true;
|
|
121
|
+
if (x < minX)
|
|
122
|
+
minX = x;
|
|
123
|
+
if (y < minY)
|
|
124
|
+
minY = y;
|
|
125
|
+
if (z < minZ)
|
|
126
|
+
minZ = z;
|
|
127
|
+
if (x > maxX)
|
|
128
|
+
maxX = x;
|
|
129
|
+
if (y > maxY)
|
|
130
|
+
maxY = y;
|
|
131
|
+
if (z > maxZ)
|
|
132
|
+
maxZ = z;
|
|
133
|
+
};
|
|
134
|
+
for (const marker of doc.markers) {
|
|
135
|
+
include(marker.position.x, marker.position.y, marker.position.z);
|
|
136
|
+
}
|
|
137
|
+
for (const volume of doc.volumes) {
|
|
138
|
+
include(volume.center.x, volume.center.y, volume.center.z);
|
|
139
|
+
}
|
|
140
|
+
for (const path of doc.paths) {
|
|
141
|
+
for (const point of path.points)
|
|
142
|
+
include(point.x, point.y, point.z);
|
|
143
|
+
}
|
|
144
|
+
for (const note of doc.annotations) {
|
|
145
|
+
include(note.position.x, note.position.y, note.position.z);
|
|
146
|
+
}
|
|
147
|
+
if (!any)
|
|
148
|
+
return null;
|
|
149
|
+
return {
|
|
150
|
+
min: { x: minX, y: minY, z: minZ },
|
|
151
|
+
max: { x: maxX, y: maxY, z: maxZ },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** The full authored scene: every marker, volume, path, and note for a game. */
|
|
2
|
+
export type { EditorDocument } from "./types.js";
|
|
3
|
+
/** Per-kind show/hide flags for the editor's layer panel. */
|
|
4
|
+
export type { EditorKindVisibility } from "./types.js";
|
|
5
|
+
/** Accepted shape for a game's `editorLayers` export: a document, partial data, or a factory. */
|
|
6
|
+
export type { EditorLayersInput } from "./types.js";
|
|
7
|
+
/** A placeable point object in the scene — spawn, mob, chest, POI, etc. */
|
|
8
|
+
export type { EditorMarker } from "./types.js";
|
|
9
|
+
/** A free-text annotation pinned to a world position for designers. */
|
|
10
|
+
export type { EditorNote } from "./types.js";
|
|
11
|
+
/** A polyline of points — road, corridor, patrol route — placed in the scene. */
|
|
12
|
+
export type { EditorPath } from "./types.js";
|
|
13
|
+
/** A world-space point used across editor markers, volumes, and paths. */
|
|
14
|
+
export type { EditorVec3 } from "./types.js";
|
|
15
|
+
/** A spatial region — zone, aggro range, capture area — placed in the scene. */
|
|
16
|
+
export type { EditorVolume } from "./types.js";
|
|
17
|
+
/** Collision shape a volume is rendered and tested as. */
|
|
18
|
+
export type { EditorVolumeShape } from "./types.js";
|
|
19
|
+
export { WELL_KNOWN_MARKER_KINDS, WELL_KNOWN_PATH_KINDS, WELL_KNOWN_VOLUME_KINDS, } from "./types.js";
|
|
20
|
+
export { cloneEditorDocument, createEmptyEditorDocument, editorDocumentBounds, exportEditorDocumentJson, findEditorMarker, findEditorNote, findEditorPath, findEditorVolume, importEditorDocumentJson, listEditorKinds, mergeEditorDocuments, normalizeEditorLayers, } from "./document.js";
|
|
21
|
+
/** Creates an editor session with undo/redo history seeded from an initial document. */
|
|
22
|
+
export { createEditorSession } from "./commands.js";
|
|
23
|
+
/** Compact snapshot of a session state — counts, selection, and the selected object. */
|
|
24
|
+
export { summarizeEditorSession } from "./commands.js";
|
|
25
|
+
/** A single editor mutation — select, move, add, remove, undo/redo — dispatched to a session. */
|
|
26
|
+
export type { EditorCommand } from "./commands.js";
|
|
27
|
+
/** Stateful, undoable handle for driving scene edits from UI or an MCP agent. */
|
|
28
|
+
export type { EditorSession } from "./commands.js";
|
|
29
|
+
/** The document plus current selection at a point in editor history. */
|
|
30
|
+
export type { EditorSessionState } from "./commands.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { WELL_KNOWN_MARKER_KINDS, WELL_KNOWN_PATH_KINDS, WELL_KNOWN_VOLUME_KINDS, } from "./types.js";
|
|
2
|
+
export { cloneEditorDocument, createEmptyEditorDocument, editorDocumentBounds, exportEditorDocumentJson, findEditorMarker, findEditorNote, findEditorPath, findEditorVolume, importEditorDocumentJson, listEditorKinds, mergeEditorDocuments, normalizeEditorLayers, } from "./document.js";
|
|
3
|
+
/** Creates an editor session with undo/redo history seeded from an initial document. */
|
|
4
|
+
export { createEditorSession } from "./commands.js";
|
|
5
|
+
/** Compact snapshot of a session state — counts, selection, and the selected object. */
|
|
6
|
+
export { summarizeEditorSession } from "./commands.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** A world-space point used across editor markers, volumes, and paths. */
|
|
2
|
+
export type EditorVec3 = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
z: number;
|
|
6
|
+
};
|
|
7
|
+
/** Collision shape a volume is rendered and tested as. */
|
|
8
|
+
export type EditorVolumeShape = "sphere" | "cylinder" | "box";
|
|
9
|
+
/** A placeable point object in the scene — spawn, mob, chest, POI, etc. */
|
|
10
|
+
export interface EditorMarker {
|
|
11
|
+
id: string;
|
|
12
|
+
kind: string;
|
|
13
|
+
position: EditorVec3;
|
|
14
|
+
rotationY?: number;
|
|
15
|
+
color?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
meta?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
/** A spatial region — zone, aggro range, capture area — placed in the scene. */
|
|
20
|
+
export interface EditorVolume {
|
|
21
|
+
id: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
shape: EditorVolumeShape;
|
|
24
|
+
center: EditorVec3;
|
|
25
|
+
radius?: number;
|
|
26
|
+
height?: number;
|
|
27
|
+
halfExtents?: EditorVec3;
|
|
28
|
+
color?: string;
|
|
29
|
+
label?: string;
|
|
30
|
+
meta?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
/** A polyline of points — road, corridor, patrol route — placed in the scene. */
|
|
33
|
+
export interface EditorPath {
|
|
34
|
+
id: string;
|
|
35
|
+
kind: string;
|
|
36
|
+
points: readonly EditorVec3[];
|
|
37
|
+
width?: number;
|
|
38
|
+
color?: string;
|
|
39
|
+
label?: string;
|
|
40
|
+
meta?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
/** A free-text annotation pinned to a world position for designers. */
|
|
43
|
+
export interface EditorNote {
|
|
44
|
+
id: string;
|
|
45
|
+
text: string;
|
|
46
|
+
position: EditorVec3;
|
|
47
|
+
color?: string;
|
|
48
|
+
meta?: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
/** The full authored scene: every marker, volume, path, and note for a game. */
|
|
51
|
+
export interface EditorDocument {
|
|
52
|
+
version: 1;
|
|
53
|
+
markers: EditorMarker[];
|
|
54
|
+
volumes: EditorVolume[];
|
|
55
|
+
paths: EditorPath[];
|
|
56
|
+
annotations: EditorNote[];
|
|
57
|
+
}
|
|
58
|
+
/** Accepted shape for a game's `editorLayers` export: a document, partial data, or a factory. */
|
|
59
|
+
export type EditorLayersInput = EditorDocument | Partial<Omit<EditorDocument, "version">> | (() => EditorDocument | Partial<Omit<EditorDocument, "version">>);
|
|
60
|
+
/** Per-kind show/hide flags for the editor's layer panel. */
|
|
61
|
+
export interface EditorKindVisibility {
|
|
62
|
+
[kind: string]: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Standard marker kinds recognized with default colors and behavior. */
|
|
65
|
+
export declare const WELL_KNOWN_MARKER_KINDS: readonly ["player_spawn", "mob", "boss", "vendor", "chest", "travel", "npc", "poi", "prop", "goal", "branch"];
|
|
66
|
+
/** Standard volume kinds recognized with default colors and behavior. */
|
|
67
|
+
export declare const WELL_KNOWN_VOLUME_KINDS: readonly ["zone", "flatten", "cluster", "aggro", "leash", "discover", "capture", "prompt", "poi", "respawn_skip"];
|
|
68
|
+
/** Standard path kinds recognized with default colors and behavior. */
|
|
69
|
+
export declare const WELL_KNOWN_PATH_KINDS: readonly ["road", "corridor", "branch", "route"];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Standard marker kinds recognized with default colors and behavior. */
|
|
2
|
+
export const WELL_KNOWN_MARKER_KINDS = [
|
|
3
|
+
"player_spawn",
|
|
4
|
+
"mob",
|
|
5
|
+
"boss",
|
|
6
|
+
"vendor",
|
|
7
|
+
"chest",
|
|
8
|
+
"travel",
|
|
9
|
+
"npc",
|
|
10
|
+
"poi",
|
|
11
|
+
"prop",
|
|
12
|
+
"goal",
|
|
13
|
+
"branch",
|
|
14
|
+
];
|
|
15
|
+
/** Standard volume kinds recognized with default colors and behavior. */
|
|
16
|
+
export const WELL_KNOWN_VOLUME_KINDS = [
|
|
17
|
+
"zone",
|
|
18
|
+
"flatten",
|
|
19
|
+
"cluster",
|
|
20
|
+
"aggro",
|
|
21
|
+
"leash",
|
|
22
|
+
"discover",
|
|
23
|
+
"capture",
|
|
24
|
+
"prompt",
|
|
25
|
+
"poi",
|
|
26
|
+
"respawn_skip",
|
|
27
|
+
];
|
|
28
|
+
/** Standard path kinds recognized with default colors and behavior. */
|
|
29
|
+
export const WELL_KNOWN_PATH_KINDS = ["road", "corridor", "branch", "route"];
|