@rpgjs/map-builder 0.0.0-stage → 5.0.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/LICENSE +19 -0
- package/README.md +341 -2
- package/dist/authority/authority.d.ts +118 -0
- package/dist/authority/authority.d.ts.map +1 -0
- package/dist/authority/authority.js +289 -0
- package/dist/authority/authority.js.map +1 -0
- package/dist/authority/index.d.ts +3 -0
- package/dist/authority/index.d.ts.map +1 -0
- package/dist/authority/index.js +3 -0
- package/dist/authority/store.d.ts +36 -0
- package/dist/authority/store.d.ts.map +1 -0
- package/dist/authority/store.js +35 -0
- package/dist/authority/store.js.map +1 -0
- package/dist/client/index.d.ts +72 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +200 -0
- package/dist/client/index.js.map +1 -0
- package/dist/embed/client.d.ts +85 -0
- package/dist/embed/client.d.ts.map +1 -0
- package/dist/embed/client.js +255 -0
- package/dist/embed/client.js.map +1 -0
- package/dist/embed/host.d.ts +112 -0
- package/dist/embed/host.d.ts.map +1 -0
- package/dist/embed/host.js +278 -0
- package/dist/embed/host.js.map +1 -0
- package/dist/embed/index.d.ts +4 -0
- package/dist/embed/index.d.ts.map +1 -0
- package/dist/embed/index.js +4 -0
- package/dist/embed/protocol.d.ts +137 -0
- package/dist/embed/protocol.d.ts.map +1 -0
- package/dist/embed/protocol.js +161 -0
- package/dist/embed/protocol.js.map +1 -0
- package/dist/features/index.d.ts +66 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +157 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/skin.d.ts +9 -0
- package/dist/features/skin.d.ts.map +1 -0
- package/dist/features/skin.js +43 -0
- package/dist/features/skin.js.map +1 -0
- package/dist/features/terrain.d.ts +66 -0
- package/dist/features/terrain.d.ts.map +1 -0
- package/dist/features/terrain.js +291 -0
- package/dist/features/terrain.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/interaction/builtin-intents.d.ts +9 -0
- package/dist/interaction/builtin-intents.d.ts.map +1 -0
- package/dist/interaction/builtin-intents.js +61 -0
- package/dist/interaction/builtin-intents.js.map +1 -0
- package/dist/interaction/catalog.d.ts +24 -0
- package/dist/interaction/catalog.d.ts.map +1 -0
- package/dist/interaction/catalog.js +20 -0
- package/dist/interaction/catalog.js.map +1 -0
- package/dist/interaction/controller.d.ts +137 -0
- package/dist/interaction/controller.d.ts.map +1 -0
- package/dist/interaction/controller.js +215 -0
- package/dist/interaction/controller.js.map +1 -0
- package/dist/interaction/index.d.ts +14 -0
- package/dist/interaction/index.d.ts.map +1 -0
- package/dist/interaction/index.js +13 -0
- package/dist/interaction/input.d.ts +26 -0
- package/dist/interaction/input.d.ts.map +1 -0
- package/dist/interaction/input.js +33 -0
- package/dist/interaction/input.js.map +1 -0
- package/dist/interaction/intents.d.ts +80 -0
- package/dist/interaction/intents.d.ts.map +1 -0
- package/dist/interaction/keyboard.d.ts +26 -0
- package/dist/interaction/keyboard.d.ts.map +1 -0
- package/dist/interaction/keyboard.js +32 -0
- package/dist/interaction/keyboard.js.map +1 -0
- package/dist/interaction/picking.d.ts +33 -0
- package/dist/interaction/picking.d.ts.map +1 -0
- package/dist/interaction/picking.js +35 -0
- package/dist/interaction/picking.js.map +1 -0
- package/dist/interaction/pipeline.d.ts +45 -0
- package/dist/interaction/pipeline.d.ts.map +1 -0
- package/dist/interaction/pipeline.js +26 -0
- package/dist/interaction/pipeline.js.map +1 -0
- package/dist/interaction/placement.d.ts +76 -0
- package/dist/interaction/placement.d.ts.map +1 -0
- package/dist/interaction/placement.js +238 -0
- package/dist/interaction/placement.js.map +1 -0
- package/dist/interaction/selection-tool.d.ts +29 -0
- package/dist/interaction/selection-tool.d.ts.map +1 -0
- package/dist/interaction/selection-tool.js +190 -0
- package/dist/interaction/selection-tool.js.map +1 -0
- package/dist/interaction/steps.d.ts +62 -0
- package/dist/interaction/steps.d.ts.map +1 -0
- package/dist/interaction/steps.js +126 -0
- package/dist/interaction/steps.js.map +1 -0
- package/dist/interaction/store.d.ts +10 -0
- package/dist/interaction/store.d.ts.map +1 -0
- package/dist/interaction/store.js +28 -0
- package/dist/interaction/store.js.map +1 -0
- package/dist/interaction/tools.d.ts +26 -0
- package/dist/interaction/tools.d.ts.map +1 -0
- package/dist/interaction/tools.js +41 -0
- package/dist/interaction/tools.js.map +1 -0
- package/dist/model/assets.d.ts +36 -0
- package/dist/model/assets.d.ts.map +1 -0
- package/dist/model/assets.js +18 -0
- package/dist/model/assets.js.map +1 -0
- package/dist/model/committer.d.ts +66 -0
- package/dist/model/committer.d.ts.map +1 -0
- package/dist/model/committer.js +38 -0
- package/dist/model/committer.js.map +1 -0
- package/dist/model/core-patches.d.ts +2 -0
- package/dist/model/core-patches.d.ts.map +1 -0
- package/dist/model/core-patches.js +193 -0
- package/dist/model/core-patches.js.map +1 -0
- package/dist/model/define-map.d.ts +52 -0
- package/dist/model/define-map.d.ts.map +1 -0
- package/dist/model/define-map.js +66 -0
- package/dist/model/define-map.js.map +1 -0
- package/dist/model/definition.d.ts +57 -0
- package/dist/model/definition.d.ts.map +1 -0
- package/dist/model/definition.js +6 -0
- package/dist/model/definition.js.map +1 -0
- package/dist/model/errors.d.ts +21 -0
- package/dist/model/errors.d.ts.map +1 -0
- package/dist/model/errors.js +14 -0
- package/dist/model/errors.js.map +1 -0
- package/dist/model/history.d.ts +43 -0
- package/dist/model/history.d.ts.map +1 -0
- package/dist/model/history.js +141 -0
- package/dist/model/history.js.map +1 -0
- package/dist/model/index.d.ts +14 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/index.js +14 -0
- package/dist/model/json.d.ts +15 -0
- package/dist/model/json.d.ts.map +1 -0
- package/dist/model/json.js +35 -0
- package/dist/model/json.js.map +1 -0
- package/dist/model/model.d.ts +68 -0
- package/dist/model/model.d.ts.map +1 -0
- package/dist/model/model.js +170 -0
- package/dist/model/model.js.map +1 -0
- package/dist/model/parse.d.ts +24 -0
- package/dist/model/parse.d.ts.map +1 -0
- package/dist/model/parse.js +165 -0
- package/dist/model/parse.js.map +1 -0
- package/dist/model/patches.d.ts +100 -0
- package/dist/model/patches.d.ts.map +1 -0
- package/dist/model/patches.js +21 -0
- package/dist/model/patches.js.map +1 -0
- package/dist/model/result.d.ts +22 -0
- package/dist/model/result.d.ts.map +1 -0
- package/dist/model/result.js +10 -0
- package/dist/model/result.js.map +1 -0
- package/dist/model/source.d.ts +52 -0
- package/dist/model/source.d.ts.map +1 -0
- package/dist/model/source.js +75 -0
- package/dist/model/source.js.map +1 -0
- package/dist/protocol/index.d.ts +92 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +106 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/remote/committer.d.ts +43 -0
- package/dist/remote/committer.d.ts.map +1 -0
- package/dist/remote/committer.js +206 -0
- package/dist/remote/committer.js.map +1 -0
- package/dist/remote/index.d.ts +4 -0
- package/dist/remote/index.d.ts.map +1 -0
- package/dist/remote/index.js +4 -0
- package/dist/remote/join.d.ts +30 -0
- package/dist/remote/join.d.ts.map +1 -0
- package/dist/remote/join.js +36 -0
- package/dist/remote/join.js.map +1 -0
- package/dist/remote/memory-transport.d.ts +20 -0
- package/dist/remote/memory-transport.d.ts.map +1 -0
- package/dist/remote/memory-transport.js +48 -0
- package/dist/remote/memory-transport.js.map +1 -0
- package/dist/renderer/effects.d.ts +52 -0
- package/dist/renderer/effects.d.ts.map +1 -0
- package/dist/renderer/effects.js +111 -0
- package/dist/renderer/effects.js.map +1 -0
- package/dist/renderer/feature.d.ts +79 -0
- package/dist/renderer/feature.d.ts.map +1 -0
- package/dist/renderer/feature.js +50 -0
- package/dist/renderer/feature.js.map +1 -0
- package/dist/renderer/geometry.d.ts +59 -0
- package/dist/renderer/geometry.d.ts.map +1 -0
- package/dist/renderer/geometry.js +114 -0
- package/dist/renderer/geometry.js.map +1 -0
- package/dist/renderer/index.d.ts +7 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +7 -0
- package/dist/renderer/scene.d.ts +77 -0
- package/dist/renderer/scene.d.ts.map +1 -0
- package/dist/renderer/scene.js +125 -0
- package/dist/renderer/scene.js.map +1 -0
- package/dist/renderer/textures.d.ts +20 -0
- package/dist/renderer/textures.d.ts.map +1 -0
- package/dist/renderer/textures.js +61 -0
- package/dist/renderer/textures.js.map +1 -0
- package/dist/renderer/view.d.ts +139 -0
- package/dist/renderer/view.d.ts.map +1 -0
- package/dist/renderer/view.js +349 -0
- package/dist/renderer/view.js.map +1 -0
- package/dist/rpgjs/shared.d.ts +41 -0
- package/dist/rpgjs/shared.d.ts.map +1 -0
- package/dist/rpgjs/shared.js +77 -0
- package/dist/rpgjs/shared.js.map +1 -0
- package/dist/server/index.d.ts +64 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +205 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/storage.d.ts +16 -0
- package/dist/server/storage.d.ts.map +1 -0
- package/dist/server/storage.js +52 -0
- package/dist/server/storage.js.map +1 -0
- package/dist/skins/default.d.ts +27 -0
- package/dist/skins/default.d.ts.map +1 -0
- package/dist/skins/default.js +144 -0
- package/dist/skins/default.js.map +1 -0
- package/dist/skins/frames.d.ts +19 -0
- package/dist/skins/frames.d.ts.map +1 -0
- package/dist/skins/frames.js +49 -0
- package/dist/skins/frames.js.map +1 -0
- package/dist/skins/game.d.ts +21 -0
- package/dist/skins/game.d.ts.map +1 -0
- package/dist/skins/game.js +21 -0
- package/dist/skins/game.js.map +1 -0
- package/dist/skins/index.d.ts +5 -0
- package/dist/skins/index.d.ts.map +1 -0
- package/dist/skins/index.js +5 -0
- package/dist/skins/types.d.ts +46 -0
- package/dist/skins/types.d.ts.map +1 -0
- package/dist/skins/types.js +12 -0
- package/dist/skins/types.js.map +1 -0
- package/dist/terrain/data.d.ts +10 -0
- package/dist/terrain/data.d.ts.map +1 -0
- package/dist/terrain/data.js +83 -0
- package/dist/terrain/data.js.map +1 -0
- package/dist/terrain/define.d.ts +72 -0
- package/dist/terrain/define.d.ts.map +1 -0
- package/dist/terrain/define.js +91 -0
- package/dist/terrain/define.js.map +1 -0
- package/dist/terrain/editing.d.ts +54 -0
- package/dist/terrain/editing.d.ts.map +1 -0
- package/dist/terrain/editing.js +169 -0
- package/dist/terrain/editing.js.map +1 -0
- package/dist/terrain/index.d.ts +10 -0
- package/dist/terrain/index.d.ts.map +1 -0
- package/dist/terrain/index.js +10 -0
- package/dist/terrain/kernel.d.ts +57 -0
- package/dist/terrain/kernel.d.ts.map +1 -0
- package/dist/terrain/kernel.js +198 -0
- package/dist/terrain/kernel.js.map +1 -0
- package/dist/terrain/mask.d.ts +37 -0
- package/dist/terrain/mask.d.ts.map +1 -0
- package/dist/terrain/mask.js +244 -0
- package/dist/terrain/mask.js.map +1 -0
- package/dist/terrain/patches.d.ts +32 -0
- package/dist/terrain/patches.d.ts.map +1 -0
- package/dist/terrain/patches.js +135 -0
- package/dist/terrain/patches.js.map +1 -0
- package/dist/terrain/rle.d.ts +4 -0
- package/dist/terrain/rle.d.ts.map +1 -0
- package/dist/terrain/rle.js +68 -0
- package/dist/terrain/rle.js.map +1 -0
- package/dist/terrain/scheduler.d.ts +46 -0
- package/dist/terrain/scheduler.d.ts.map +1 -0
- package/dist/terrain/scheduler.js +148 -0
- package/dist/terrain/scheduler.js.map +1 -0
- package/dist/terrain/types.d.ts +110 -0
- package/dist/terrain/types.d.ts.map +1 -0
- package/dist/terrain/types.js +11 -0
- package/dist/terrain/types.js.map +1 -0
- package/dist/terrain-render/backend.d.ts +66 -0
- package/dist/terrain-render/backend.d.ts.map +1 -0
- package/dist/terrain-render/backend.js +96 -0
- package/dist/terrain-render/backend.js.map +1 -0
- package/dist/terrain-render/host.d.ts +21 -0
- package/dist/terrain-render/host.d.ts.map +1 -0
- package/dist/terrain-render/host.js +62 -0
- package/dist/terrain-render/host.js.map +1 -0
- package/dist/terrain-render/index.d.ts +3 -0
- package/dist/terrain-render/index.d.ts.map +1 -0
- package/dist/terrain-render/index.js +3 -0
- package/dist/terrain-render/worker.d.ts +2 -0
- package/dist/terrain-render/worker.d.ts.map +1 -0
- package/dist/terrain-render/worker.js +34 -0
- package/dist/terrain-render/worker.js.map +1 -0
- package/package.json +124 -3
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { createMapView } from "../renderer/view.js";
|
|
2
|
+
import { elements } from "../features/index.js";
|
|
3
|
+
import { bindRemoteEffects, joinRemoteMap } from "../remote/join.js";
|
|
4
|
+
import "../remote/index.js";
|
|
5
|
+
import { MAP_BUILDER_CHANNEL, changedElementIds, clearHitboxes, forgetHitboxes, syncElementHitboxes, unwrap, wrap } from "../rpgjs/shared.js";
|
|
6
|
+
import { createModule, defineModule } from "@rpgjs/common";
|
|
7
|
+
import { RpgClientEngine, WebSocketToken, inject } from "@rpgjs/client";
|
|
8
|
+
import { signal } from "canvasengine";
|
|
9
|
+
//#region src/client/index.ts
|
|
10
|
+
var states = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
var MAX_BUFFERED = 64;
|
|
12
|
+
var stateOf = (engine) => {
|
|
13
|
+
let state = states.get(engine);
|
|
14
|
+
if (!state) {
|
|
15
|
+
state = {
|
|
16
|
+
pixiChildren: signal([]),
|
|
17
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
18
|
+
waiting: [],
|
|
19
|
+
buffered: /* @__PURE__ */ new Map()
|
|
20
|
+
};
|
|
21
|
+
states.set(engine, state);
|
|
22
|
+
}
|
|
23
|
+
return state;
|
|
24
|
+
};
|
|
25
|
+
/** What the map builder gives a game on the client. Pass the type of your features to get `view.placement` and the like typed. */
|
|
26
|
+
function getMapBuilder(engine = inject(RpgClientEngine)) {
|
|
27
|
+
const state = stateOf(engine);
|
|
28
|
+
return {
|
|
29
|
+
current: () => state.current,
|
|
30
|
+
whenReady: () => state.current ? Promise.resolve(state.current) : new Promise((resolve) => state.waiting.push(resolve)),
|
|
31
|
+
pixiChildren: state.pixiChildren,
|
|
32
|
+
onChange(listener) {
|
|
33
|
+
state.listeners.add(listener);
|
|
34
|
+
return () => state.listeners.delete(listener);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** The module of the map builder for an RPGJS client. Install it with `provideMapBuilder`, or `provideClientModules`. */
|
|
39
|
+
function createMapBuilderClient(options) {
|
|
40
|
+
const reportError = options.onError ?? ((error) => console.error(error));
|
|
41
|
+
const features = options.features ?? [elements()];
|
|
42
|
+
const physics = options.physics !== false;
|
|
43
|
+
const describeContext = {
|
|
44
|
+
assets: options.assets,
|
|
45
|
+
...options.view?.hooks ? { hooks: options.view.hooks } : {}
|
|
46
|
+
};
|
|
47
|
+
const sceneOf = (engine) => engine.scene;
|
|
48
|
+
const leave = (engine) => {
|
|
49
|
+
const state = stateOf(engine);
|
|
50
|
+
const handle = state.current;
|
|
51
|
+
if (!handle) return;
|
|
52
|
+
state.current = void 0;
|
|
53
|
+
state.pixiChildren.set([]);
|
|
54
|
+
if (physics) clearHitboxes(sceneOf(engine));
|
|
55
|
+
try {
|
|
56
|
+
options.onLeave?.(handle);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
reportError(error);
|
|
59
|
+
}
|
|
60
|
+
handle.dispose();
|
|
61
|
+
for (const listener of [...state.listeners]) listener(void 0);
|
|
62
|
+
};
|
|
63
|
+
/** Joins the modifiable map the player is on, if it is not already joined. */
|
|
64
|
+
const join = async (engine, mapId) => {
|
|
65
|
+
const state = stateOf(engine);
|
|
66
|
+
if (state.current?.mapId === mapId || state.joining === mapId) return;
|
|
67
|
+
leave(engine);
|
|
68
|
+
const socket = inject(WebSocketToken);
|
|
69
|
+
state.joining = mapId;
|
|
70
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
71
|
+
const transport = {
|
|
72
|
+
send: (message) => socket.emit(MAP_BUILDER_CHANNEL, wrap(mapId, message)),
|
|
73
|
+
onMessage(listener) {
|
|
74
|
+
listeners.add(listener);
|
|
75
|
+
for (const message of state.buffered.get(mapId)?.splice(0) ?? []) listener(message);
|
|
76
|
+
return () => listeners.delete(listener);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
const remote = await joinRemoteMap({
|
|
81
|
+
transport,
|
|
82
|
+
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
83
|
+
...options.patchHandlers ? { model: { patchHandlers: options.patchHandlers } } : {}
|
|
84
|
+
});
|
|
85
|
+
if (state.joining !== mapId) return remote.disconnect();
|
|
86
|
+
const view = createMapView({
|
|
87
|
+
...options.view,
|
|
88
|
+
model: remote.model,
|
|
89
|
+
assets: options.assets,
|
|
90
|
+
features,
|
|
91
|
+
onError: options.view?.onError ?? reportError
|
|
92
|
+
});
|
|
93
|
+
const stopEffects = bindRemoteEffects(remote, view.effects);
|
|
94
|
+
const host = sceneOf(engine);
|
|
95
|
+
const stopPhysics = physics ? (() => {
|
|
96
|
+
syncElementHitboxes(host, remote.model.state, "all", describeContext);
|
|
97
|
+
return remote.model.subscribe((change) => syncElementHitboxes(host, change.state, change.reset ? "all" : changedElementIds(change.patches), describeContext));
|
|
98
|
+
})() : () => void 0;
|
|
99
|
+
const handle = {
|
|
100
|
+
mapId,
|
|
101
|
+
remote,
|
|
102
|
+
view,
|
|
103
|
+
deliver: (message) => listeners.forEach((listener) => listener(message)),
|
|
104
|
+
attachTo: (container) => {
|
|
105
|
+
container.addChild(view.root);
|
|
106
|
+
return () => void (view.root.parent === container && container.removeChild(view.root));
|
|
107
|
+
},
|
|
108
|
+
dispose() {
|
|
109
|
+
stopEffects();
|
|
110
|
+
stopPhysics();
|
|
111
|
+
view.destroy();
|
|
112
|
+
remote.disconnect();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
state.current = handle;
|
|
116
|
+
state.joining = void 0;
|
|
117
|
+
state.pixiChildren.set([view.root]);
|
|
118
|
+
for (const waiter of state.waiting.splice(0)) waiter(handle);
|
|
119
|
+
for (const listener of [...state.listeners]) listener(handle);
|
|
120
|
+
options.onJoin?.(handle);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (state.joining === mapId) state.joining = void 0;
|
|
123
|
+
reportError(error);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
/** Gives what the server sends to the join of the map it is about, or keeps it until the player is there. */
|
|
127
|
+
const bindSocket = (engine) => {
|
|
128
|
+
const state = stateOf(engine);
|
|
129
|
+
const socket = inject(WebSocketToken);
|
|
130
|
+
if (state.socketBound === socket) return;
|
|
131
|
+
state.socketBound = socket;
|
|
132
|
+
socket.on(MAP_BUILDER_CHANNEL, (raw) => {
|
|
133
|
+
const envelope = unwrap(raw);
|
|
134
|
+
if (!envelope) return;
|
|
135
|
+
const message = envelope.message;
|
|
136
|
+
if (state.current?.mapId === envelope.map) return state.current.deliver(message);
|
|
137
|
+
const queue = state.buffered.get(envelope.map) ?? [];
|
|
138
|
+
queue.push(message);
|
|
139
|
+
if (queue.length > MAX_BUFFERED) queue.splice(0, queue.length - MAX_BUFFERED);
|
|
140
|
+
state.buffered.set(envelope.map, queue);
|
|
141
|
+
if (sceneOf(engine).id === envelope.map) join(engine, envelope.map);
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
const onMapLoaded = (engine) => {
|
|
145
|
+
const mapId = sceneOf(engine).id;
|
|
146
|
+
if (mapId && stateOf(engine).buffered.has(mapId)) join(engine, mapId);
|
|
147
|
+
};
|
|
148
|
+
return defineModule({
|
|
149
|
+
engine: {
|
|
150
|
+
onStart(engine) {
|
|
151
|
+
bindSocket(engine);
|
|
152
|
+
},
|
|
153
|
+
onConnected(engine) {
|
|
154
|
+
bindSocket(engine);
|
|
155
|
+
},
|
|
156
|
+
onStep(engine, _time, deltaMs) {
|
|
157
|
+
stateOf(engine).current?.view.tick(deltaMs ?? 16);
|
|
158
|
+
},
|
|
159
|
+
onDisconnect(engine) {
|
|
160
|
+
leave(engine);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
sceneMap: {
|
|
164
|
+
onAfterLoading(scene) {
|
|
165
|
+
const engine = inject(RpgClientEngine);
|
|
166
|
+
bindSocket(engine);
|
|
167
|
+
const state = stateOf(engine);
|
|
168
|
+
const sceneId = scene.id;
|
|
169
|
+
if (state.joining !== void 0 && state.joining !== sceneId) state.joining = void 0;
|
|
170
|
+
if (state.current && state.current.mapId !== sceneId) leave(engine);
|
|
171
|
+
onMapLoaded(engine);
|
|
172
|
+
},
|
|
173
|
+
onChanges() {
|
|
174
|
+
onMapLoaded(inject(RpgClientEngine));
|
|
175
|
+
},
|
|
176
|
+
onPhysicsInit(scene) {
|
|
177
|
+
const engine = inject(RpgClientEngine);
|
|
178
|
+
onMapLoaded(engine);
|
|
179
|
+
const handle = stateOf(engine).current;
|
|
180
|
+
if (!physics || !handle) return;
|
|
181
|
+
forgetHitboxes(scene);
|
|
182
|
+
syncElementHitboxes(scene, handle.remote.model.state, "all", describeContext);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Installs the map builder on an RPGJS client:
|
|
189
|
+
*
|
|
190
|
+
* ```ts
|
|
191
|
+
* startGame({ providers: [provideMapBuilder({ assets, features: [elements(), selection(), placement({ catalog })] })] })
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
function provideMapBuilder(options) {
|
|
195
|
+
return createModule("MapBuilder", [{ client: createMapBuilderClient(options) }]);
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
export { MAP_BUILDER_CHANNEL, createMapBuilderClient, getMapBuilder, provideMapBuilder };
|
|
199
|
+
|
|
200
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/client/index.ts"],"sourcesContent":["import { createModule, defineModule, type RpgProvider } from \"@rpgjs/common\";\nimport { inject, RpgClientEngine, WebSocketToken, type AbstractWebsocket, type RpgClient } from \"@rpgjs/client\";\nimport { signal } from \"canvasengine\";\nimport type { Container } from \"pixi.js\";\nimport type { AssetResolver } from \"../model/assets\";\nimport type { PatchHandlers } from \"../model/patches\";\nimport type { ApplyResult, Unsubscribe } from \"../model/result\";\nimport type { ClientMessage, MapTransport, ServerMessage } from \"../protocol\";\nimport { bindRemoteEffects, joinRemoteMap, type RemoteMap } from \"../remote\";\nimport type { FeatureApis, MapFeature } from \"../renderer/feature\";\nimport type { DescribeContext } from \"../renderer/scene\";\nimport { createMapView, type MapView, type MapViewOptions } from \"../renderer/view\";\nimport { elements } from \"../features\";\nimport {\n MAP_BUILDER_CHANNEL,\n changedElementIds,\n clearHitboxes,\n forgetHitboxes,\n syncElementHitboxes,\n unwrap,\n wrap,\n type StaticHitboxHost,\n} from \"../rpgjs/shared\";\n\nexport { MAP_BUILDER_CHANNEL } from \"../rpgjs/shared\";\n\ntype Features = readonly MapFeature<string, object>[];\n\nexport interface MapBuilderClientOptions<F extends Features = Features> {\n /** What the maps are made of: the same assets the server has. */\n readonly assets: AssetResolver;\n /**\n * What the view draws and lets the player do. Defaults to `[elements()]`: events are RPGJS\n * events, drawn by RPGJS, so `events()` would draw them twice.\n */\n readonly features?: F;\n /** Options of the view: textures, hooks, modes, actor, culling… */\n readonly view?: Omit<MapViewOptions<F, Promise<ApplyResult>>, \"model\" | \"assets\" | \"features\">;\n /** Keep the static colliders of the client's physics in step, so the player does not walk through what was built. Defaults to true. */\n readonly physics?: boolean;\n /** Handlers of the patch types the features add (`terrainPatchHandlers`): the model must know them to apply what the server tells. */\n readonly patchHandlers?: readonly PatchHandlers[];\n /** How long to wait for the server before giving up. Defaults to 10000. */\n readonly timeoutMs?: number;\n /** The player is on a modifiable map: the place to wire controls (`handle.view.placement.start(\"oak\")`). */\n readonly onJoin?: (handle: MapBuilderHandle<F>) => void;\n readonly onLeave?: (handle: MapBuilderHandle<F>) => void;\n readonly onError?: (error: unknown) => void;\n}\n\nexport interface MapBuilderHandle<F extends Features = Features> {\n readonly mapId: string;\n readonly remote: RemoteMap;\n readonly view: MapView<Promise<ApplyResult>> & FeatureApis<F>;\n /** Adds the view to a PixiJS container, e.g. the `componentInstance` of a CanvasEngine component. */\n attachTo(container: Container): () => void;\n /** Leaves the map: stops listening, removes the colliders, destroys the view. */\n dispose(): void;\n}\n\nexport interface MapBuilderClient<F extends Features = Features> {\n /** The map the player is on, once joined. */\n current(): MapBuilderHandle<F> | undefined;\n /** Resolves once the player has joined a modifiable map. */\n whenReady(): Promise<MapBuilderHandle<F>>;\n /**\n * The view as PixiJS children, for the `pixiChildren` prop of RPGJS's `EventLayerComponent`: that\n * puts the elements in the same layer as the characters, so they are sorted in depth together.\n * Empty when the player is not on a modifiable map.\n */\n readonly pixiChildren: { (): Container[]; set(value: Container[]): void };\n /** Called when the player joins or leaves a modifiable map. */\n onChange(listener: (handle: MapBuilderHandle<F> | undefined) => void): Unsubscribe;\n}\n\ninterface State {\n readonly pixiChildren: { (): Container[]; set(value: Container[]): void };\n readonly listeners: Set<(handle: MapBuilderHandle<never> | undefined) => void>;\n readonly waiting: Array<(handle: MapBuilderHandle<never>) => void>;\n /** Messages that arrived for a map the client has not loaded yet, by map. */\n readonly buffered: Map<string, ServerMessage[]>;\n current?: MapBuilderHandle<never> & { readonly deliver: (message: ServerMessage) => void };\n joining?: string;\n socketBound?: AbstractWebsocket;\n}\n\nconst states = new WeakMap<object, State>();\nconst MAX_BUFFERED = 64;\n\nconst stateOf = (engine: object): State => {\n let state = states.get(engine);\n if (!state) {\n state = { pixiChildren: signal<Container[]>([]) as State[\"pixiChildren\"], listeners: new Set(), waiting: [], buffered: new Map() };\n states.set(engine, state);\n }\n return state;\n};\n\n/** What the map builder gives a game on the client. Pass the type of your features to get `view.placement` and the like typed. */\nexport function getMapBuilder<F extends Features = Features>(engine: object = inject(RpgClientEngine)): MapBuilderClient<F> {\n const state = stateOf(engine);\n return {\n current: () => state.current as unknown as MapBuilderHandle<F> | undefined,\n whenReady: () =>\n state.current ? Promise.resolve(state.current as unknown as MapBuilderHandle<F>) : new Promise((resolve) => state.waiting.push(resolve as never)),\n pixiChildren: state.pixiChildren,\n onChange(listener) {\n state.listeners.add(listener as never);\n return () => state.listeners.delete(listener as never);\n },\n };\n}\n\n/** The module of the map builder for an RPGJS client. Install it with `provideMapBuilder`, or `provideClientModules`. */\nexport function createMapBuilderClient<const F extends Features = readonly [ReturnType<typeof elements>]>(\n options: MapBuilderClientOptions<F>,\n): RpgClient {\n const reportError = options.onError ?? ((error: unknown) => console.error(error));\n const features = (options.features ?? [elements()]) as unknown as F;\n const physics = options.physics !== false;\n const describeContext: DescribeContext = { assets: options.assets, ...(options.view?.hooks ? { hooks: options.view.hooks } : {}) };\n\n const sceneOf = (engine: RpgClientEngine): (StaticHitboxHost & { id: string }) => engine.scene as unknown as StaticHitboxHost & { id: string };\n\n const leave = (engine: RpgClientEngine): void => {\n const state = stateOf(engine);\n const handle = state.current;\n if (!handle) return;\n state.current = undefined;\n state.pixiChildren.set([]);\n if (physics) clearHitboxes(sceneOf(engine));\n try {\n options.onLeave?.(handle as never);\n } catch (error) {\n reportError(error);\n }\n handle.dispose();\n for (const listener of [...state.listeners]) listener(undefined);\n };\n\n /** Joins the modifiable map the player is on, if it is not already joined. */\n const join = async (engine: RpgClientEngine, mapId: string): Promise<void> => {\n const state = stateOf(engine);\n if (state.current?.mapId === mapId || state.joining === mapId) return;\n leave(engine);\n const socket = inject<AbstractWebsocket>(WebSocketToken);\n state.joining = mapId;\n\n const listeners = new Set<(message: ServerMessage) => void>();\n const transport: MapTransport = {\n send: (message: ClientMessage) => socket.emit(MAP_BUILDER_CHANNEL, wrap(mapId, message)),\n onMessage(listener) {\n listeners.add(listener);\n // What the server sent before the scene was loaded.\n for (const message of state.buffered.get(mapId)?.splice(0) ?? []) listener(message);\n return () => listeners.delete(listener);\n },\n };\n\n try {\n const remote = await joinRemoteMap({\n transport,\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n ...(options.patchHandlers ? { model: { patchHandlers: options.patchHandlers } } : {}),\n });\n if (state.joining !== mapId) return remote.disconnect(); // the player moved on while we waited\n\n const view = createMapView({\n ...options.view,\n model: remote.model,\n assets: options.assets,\n features,\n onError: options.view?.onError ?? reportError,\n } as never) as unknown as MapBuilderHandle<F>[\"view\"];\n const stopEffects = bindRemoteEffects(remote, view.effects);\n\n const host = sceneOf(engine);\n const stopPhysics = physics\n ? (() => {\n syncElementHitboxes(host, remote.model.state, \"all\", describeContext);\n return remote.model.subscribe((change) =>\n syncElementHitboxes(host, change.state, change.reset ? \"all\" : changedElementIds(change.patches), describeContext),\n );\n })()\n : () => undefined;\n\n const handle = {\n mapId,\n remote,\n view,\n deliver: (message: ServerMessage) => listeners.forEach((listener) => listener(message)),\n attachTo: (container: Container) => {\n container.addChild(view.root);\n return () => void (view.root.parent === container && container.removeChild(view.root));\n },\n dispose() {\n stopEffects();\n stopPhysics();\n view.destroy();\n remote.disconnect();\n },\n };\n state.current = handle as never;\n state.joining = undefined;\n state.pixiChildren.set([view.root]);\n for (const waiter of state.waiting.splice(0)) waiter(handle as never);\n for (const listener of [...state.listeners]) listener(handle as never);\n options.onJoin?.(handle as never);\n } catch (error) {\n if (state.joining === mapId) state.joining = undefined;\n reportError(error);\n }\n };\n\n /** Gives what the server sends to the join of the map it is about, or keeps it until the player is there. */\n const bindSocket = (engine: RpgClientEngine): void => {\n const state = stateOf(engine);\n const socket = inject<AbstractWebsocket>(WebSocketToken);\n if (state.socketBound === socket) return;\n state.socketBound = socket;\n socket.on(MAP_BUILDER_CHANNEL, (raw: unknown) => {\n const envelope = unwrap(raw);\n if (!envelope) return;\n const message = envelope.message as ServerMessage;\n if (state.current?.mapId === envelope.map) return state.current.deliver(message);\n const queue = state.buffered.get(envelope.map) ?? [];\n queue.push(message);\n if (queue.length > MAX_BUFFERED) queue.splice(0, queue.length - MAX_BUFFERED);\n state.buffered.set(envelope.map, queue);\n // The player may already be on that map, with nothing to tell us it loaded.\n if (sceneOf(engine).id === envelope.map) void join(engine, envelope.map);\n });\n };\n\n const onMapLoaded = (engine: RpgClientEngine): void => {\n const mapId = sceneOf(engine).id;\n if (mapId && stateOf(engine).buffered.has(mapId)) void join(engine, mapId);\n };\n\n return defineModule<RpgClient>({\n engine: {\n onStart(engine: RpgClientEngine) {\n bindSocket(engine);\n },\n onConnected(engine: RpgClientEngine) {\n bindSocket(engine);\n },\n // Time moves with the game: effects (a shake, a blink) are driven by the engine, not by a clock of their own.\n onStep(engine: RpgClientEngine, _time?: number, deltaMs?: number) {\n stateOf(engine).current?.view.tick(deltaMs ?? 16);\n },\n onDisconnect(engine: RpgClientEngine) {\n leave(engine);\n },\n },\n sceneMap: {\n onAfterLoading(scene: unknown) {\n const engine = inject(RpgClientEngine);\n bindSocket(engine);\n // A different map from the one we joined: leave it, and join this one once the server has sent it.\n const state = stateOf(engine);\n const sceneId = (scene as { id?: string }).id;\n // A join still waiting for another map is given up too: its result must not be installed on this one.\n if (state.joining !== undefined && state.joining !== sceneId) state.joining = undefined;\n if (state.current && state.current.mapId !== sceneId) leave(engine);\n onMapLoaded(engine);\n },\n // Not every RPGJS setup calls `onAfterLoading`: the map being loaded, or its physics being built, also tell us the scene is ready.\n onChanges() {\n onMapLoaded(inject(RpgClientEngine));\n },\n onPhysicsInit(scene: unknown) {\n const engine = inject(RpgClientEngine);\n onMapLoaded(engine);\n const handle = stateOf(engine).current;\n if (!physics || !handle) return;\n // The physics world was rebuilt: put the colliders back.\n forgetHitboxes(scene as object);\n syncElementHitboxes(scene as unknown as StaticHitboxHost, handle.remote.model.state, \"all\", describeContext);\n },\n },\n });\n}\n\n/**\n * Installs the map builder on an RPGJS client:\n *\n * ```ts\n * startGame({ providers: [provideMapBuilder({ assets, features: [elements(), selection(), placement({ catalog })] })] })\n * ```\n */\nexport function provideMapBuilder<const F extends Features = readonly [ReturnType<typeof elements>]>(options: MapBuilderClientOptions<F>): RpgProvider[] {\n return createModule(\"MapBuilder\", [{ client: createMapBuilderClient(options) }]);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { TargetRef } from '../interaction/picking';
|
|
2
|
+
import { JsonValue } from '../model/json';
|
|
3
|
+
import { MapModel } from '../model/model';
|
|
4
|
+
import { PatchHandlers } from '../model/patches';
|
|
5
|
+
import { ApplyResult, Unsubscribe } from '../model/result';
|
|
6
|
+
import { EditorTool, PortLike, TargetLike, WindowLike } from './protocol';
|
|
7
|
+
export interface EditorMap {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
/** The map as the game's authority has it. Trusted: raw patches, undo and redo are accepted. */
|
|
10
|
+
readonly model: MapModel<Promise<ApplyResult>>;
|
|
11
|
+
}
|
|
12
|
+
export interface EditorInteraction {
|
|
13
|
+
readonly modes: readonly string[];
|
|
14
|
+
readonly tool: EditorTool;
|
|
15
|
+
readonly rejection: {
|
|
16
|
+
readonly reason: string;
|
|
17
|
+
readonly message?: string;
|
|
18
|
+
} | null;
|
|
19
|
+
}
|
|
20
|
+
export interface ConnectEditorFrameOptions {
|
|
21
|
+
/** The iframe that shows the game, or its window. */
|
|
22
|
+
readonly frame: {
|
|
23
|
+
readonly contentWindow: TargetLike | null;
|
|
24
|
+
} | TargetLike;
|
|
25
|
+
/** Origin of the game, e.g. `"https://game.example.com"`. Nothing from another origin is believed. */
|
|
26
|
+
readonly origin: string;
|
|
27
|
+
/**
|
|
28
|
+
* Where the game really runs, when it is not the window of the frame itself: a page that wraps the game in
|
|
29
|
+
* another frame (a sandbox) gives the inner window, e.g. `(outer) => outer.frames[0]`. Messages are sent to it, and
|
|
30
|
+
* only messages that come from it are believed.
|
|
31
|
+
*/
|
|
32
|
+
readonly resolveWindow?: (frameWindow: TargetLike) => TargetLike | null | undefined;
|
|
33
|
+
/** How long to wait for the game to answer. Defaults to 15000. */
|
|
34
|
+
readonly timeoutMs?: number;
|
|
35
|
+
readonly window?: WindowLike;
|
|
36
|
+
/** Creates the channel. Defaults to `new MessageChannel()`. */
|
|
37
|
+
readonly createChannel?: () => {
|
|
38
|
+
port1: PortLike;
|
|
39
|
+
port2: PortLike;
|
|
40
|
+
};
|
|
41
|
+
/** Handlers of the patch types the game's features add (`terrainPatchHandlers`): the Studio's model must know them to apply what it is told. */
|
|
42
|
+
readonly patchHandlers?: readonly PatchHandlers[];
|
|
43
|
+
/** How long to wait for the first map, once connected. Passed to `joinRemoteMap`. */
|
|
44
|
+
readonly mapTimeoutMs?: number;
|
|
45
|
+
readonly onError?: (error: unknown) => void;
|
|
46
|
+
}
|
|
47
|
+
export interface EditorFrame {
|
|
48
|
+
/** The map the game shows, once joined. */
|
|
49
|
+
readonly map: EditorMap | undefined;
|
|
50
|
+
/** Called with every map the game shows, now included if there is one. The model of a previous map stops being fed. */
|
|
51
|
+
onMap(listener: (map: EditorMap) => void): Unsubscribe;
|
|
52
|
+
readonly interaction: EditorInteraction | undefined;
|
|
53
|
+
onInteraction(listener: (interaction: EditorInteraction) => void): Unsubscribe;
|
|
54
|
+
readonly selection: readonly TargetRef[];
|
|
55
|
+
onSelection(listener: (selection: readonly TargetRef[]) => void): Unsubscribe;
|
|
56
|
+
readonly paused: boolean;
|
|
57
|
+
onPaused(listener: (paused: boolean) => void): Unsubscribe;
|
|
58
|
+
/** World position of the pointer in the game, for a status bar. */
|
|
59
|
+
onPointer(listener: (point: {
|
|
60
|
+
x: number;
|
|
61
|
+
y: number;
|
|
62
|
+
}) => void): Unsubscribe;
|
|
63
|
+
/** Problems the game reports (a tool that cannot start, a message it did not understand). */
|
|
64
|
+
onGameError(listener: (message: string) => void): Unsubscribe;
|
|
65
|
+
/** The last value of what the game keeps for the Studio (`tool`, `canUndo`…), by key. */
|
|
66
|
+
readonly state: Readonly<Record<string, JsonValue>>;
|
|
67
|
+
/** Called when the game changes a value, and with the ones it already had when a listener is added. */
|
|
68
|
+
onState(key: string, listener: (value: JsonValue) => void): Unsubscribe;
|
|
69
|
+
/** Asks the game to do something it registered: `frame.command("copy")`, `frame.command("place", { … })`. */
|
|
70
|
+
command(name: string, args?: JsonValue): void;
|
|
71
|
+
setModes(modes: readonly string[]): void;
|
|
72
|
+
setTool(tool: EditorTool): void;
|
|
73
|
+
select(targets: readonly TargetRef[]): void;
|
|
74
|
+
pause(): void;
|
|
75
|
+
resume(): void;
|
|
76
|
+
camera(x: number, y: number): void;
|
|
77
|
+
dispose(): void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The Studio's end of the editor channels: finds the game in the frame, agrees on a private channel with it,
|
|
81
|
+
* and gives back the map (a model that works like any other), the state of the tools, and commands.
|
|
82
|
+
* Framework-neutral: wrap `onMap` and friends in Angular signals, or whatever the Studio uses.
|
|
83
|
+
*/
|
|
84
|
+
export declare function connectEditorFrame(options: ConnectEditorFrameOptions): Promise<EditorFrame>;
|
|
85
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/embed/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { joinRemoteMap } from "../remote/join.js";
|
|
2
|
+
import "../remote/index.js";
|
|
3
|
+
import { EDITOR_SOURCE, parseEnvelope, parseEvent, parseHandshake } from "./protocol.js";
|
|
4
|
+
//#region src/embed/client.ts
|
|
5
|
+
var listeners = () => {
|
|
6
|
+
const set = /* @__PURE__ */ new Set();
|
|
7
|
+
return {
|
|
8
|
+
add(listener) {
|
|
9
|
+
set.add(listener);
|
|
10
|
+
return () => set.delete(listener);
|
|
11
|
+
},
|
|
12
|
+
emit(value, onError) {
|
|
13
|
+
for (const listener of [...set]) try {
|
|
14
|
+
listener(value);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
onError(error);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The Studio's end of the editor channels: finds the game in the frame, agrees on a private channel with it,
|
|
23
|
+
* and gives back the map (a model that works like any other), the state of the tools, and commands.
|
|
24
|
+
* Framework-neutral: wrap `onMap` and friends in Angular signals, or whatever the Studio uses.
|
|
25
|
+
*/
|
|
26
|
+
async function connectEditorFrame(options) {
|
|
27
|
+
const win = options.window ?? globalThis.window;
|
|
28
|
+
if (!win) throw new Error("connectEditorFrame needs a window");
|
|
29
|
+
const report = options.onError ?? ((error) => console.error("[map-builder editor]", error));
|
|
30
|
+
const target = () => {
|
|
31
|
+
const frameWindow = "contentWindow" in options.frame ? options.frame.contentWindow : options.frame;
|
|
32
|
+
if (!frameWindow || !options.resolveWindow) return frameWindow;
|
|
33
|
+
try {
|
|
34
|
+
return options.resolveWindow(frameWindow) ?? null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const makeChannel = options.createChannel ?? (() => new MessageChannel());
|
|
40
|
+
let port;
|
|
41
|
+
let current;
|
|
42
|
+
let interaction;
|
|
43
|
+
let selection = [];
|
|
44
|
+
let paused = false;
|
|
45
|
+
let disposed = false;
|
|
46
|
+
let generation = 0;
|
|
47
|
+
let mapListeners;
|
|
48
|
+
let activeId;
|
|
49
|
+
const onMap = listeners();
|
|
50
|
+
const onInteraction = listeners();
|
|
51
|
+
const onSelection = listeners();
|
|
52
|
+
const onPaused = listeners();
|
|
53
|
+
const onPointer = listeners();
|
|
54
|
+
const onGameError = listeners();
|
|
55
|
+
const values = /* @__PURE__ */ new Map();
|
|
56
|
+
const stateListeners = /* @__PURE__ */ new Map();
|
|
57
|
+
const send = (envelope) => {
|
|
58
|
+
try {
|
|
59
|
+
port?.postMessage(envelope);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
report(error);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const control = (message) => send({
|
|
65
|
+
ch: "control",
|
|
66
|
+
message
|
|
67
|
+
});
|
|
68
|
+
let remote;
|
|
69
|
+
const joinMap = (id) => {
|
|
70
|
+
remote?.disconnect();
|
|
71
|
+
remote = void 0;
|
|
72
|
+
current = void 0;
|
|
73
|
+
const token = ++generation;
|
|
74
|
+
activeId = id;
|
|
75
|
+
const mine = /* @__PURE__ */ new Set();
|
|
76
|
+
mapListeners = mine;
|
|
77
|
+
joinRemoteMap({
|
|
78
|
+
transport: {
|
|
79
|
+
send: (message) => send({
|
|
80
|
+
ch: "map",
|
|
81
|
+
map: id,
|
|
82
|
+
message
|
|
83
|
+
}),
|
|
84
|
+
onMessage(listener) {
|
|
85
|
+
mine.add(listener);
|
|
86
|
+
return () => mine.delete(listener);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
...options.mapTimeoutMs !== void 0 ? { timeoutMs: options.mapTimeoutMs } : {},
|
|
90
|
+
...options.patchHandlers ? { model: { patchHandlers: options.patchHandlers } } : {}
|
|
91
|
+
}).then((joined) => {
|
|
92
|
+
if (token !== generation || disposed) return joined.disconnect();
|
|
93
|
+
remote = joined;
|
|
94
|
+
current = {
|
|
95
|
+
id,
|
|
96
|
+
model: joined.model
|
|
97
|
+
};
|
|
98
|
+
onMap.emit(current, report);
|
|
99
|
+
}, (error) => {
|
|
100
|
+
if (token === generation) report(error);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
const handleEvent = (event) => {
|
|
104
|
+
switch (event.type) {
|
|
105
|
+
case "map": return joinMap(event.id);
|
|
106
|
+
case "selection":
|
|
107
|
+
selection = event.targets;
|
|
108
|
+
return onSelection.emit(selection, report);
|
|
109
|
+
case "interaction":
|
|
110
|
+
interaction = {
|
|
111
|
+
modes: event.modes,
|
|
112
|
+
tool: event.tool,
|
|
113
|
+
rejection: event.rejection
|
|
114
|
+
};
|
|
115
|
+
return onInteraction.emit(interaction, report);
|
|
116
|
+
case "paused":
|
|
117
|
+
paused = event.paused;
|
|
118
|
+
return onPaused.emit(paused, report);
|
|
119
|
+
case "pointer": return onPointer.emit({
|
|
120
|
+
x: event.x,
|
|
121
|
+
y: event.y
|
|
122
|
+
}, report);
|
|
123
|
+
case "state":
|
|
124
|
+
values.set(event.key, event.value);
|
|
125
|
+
for (const listener of [...stateListeners.get(event.key) ?? []]) try {
|
|
126
|
+
listener(event.value);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
report(error);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
case "error": return onGameError.emit(event.message, report);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const onPort = (event) => {
|
|
135
|
+
const envelope = parseEnvelope(event.data);
|
|
136
|
+
if (!envelope) return;
|
|
137
|
+
if (envelope.ch === "map") {
|
|
138
|
+
if (envelope.map === activeId) for (const listener of [...mapListeners ?? []]) listener(envelope.message);
|
|
139
|
+
} else if (envelope.ch === "event") {
|
|
140
|
+
const parsed = parseEvent(envelope.message);
|
|
141
|
+
if (parsed) handleEvent(parsed);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
await new Promise((resolve, reject) => {
|
|
145
|
+
let timer;
|
|
146
|
+
const done = (error) => {
|
|
147
|
+
if (timer) clearInterval(timer);
|
|
148
|
+
clearTimeout(deadline);
|
|
149
|
+
win.removeEventListener("message", onWindow);
|
|
150
|
+
if (error) reject(error);
|
|
151
|
+
else resolve();
|
|
152
|
+
};
|
|
153
|
+
const onWindow = (event) => {
|
|
154
|
+
const frame = target();
|
|
155
|
+
if (!frame || event.source !== frame || event.origin !== options.origin) return;
|
|
156
|
+
const handshake = parseHandshake(event.data);
|
|
157
|
+
if (handshake?.type !== "ready") return;
|
|
158
|
+
if (handshake.version !== 1) return done(/* @__PURE__ */ new Error(`This game speaks editor protocol ${handshake.version}, the Studio speaks 1: it was built with another version of the map builder`));
|
|
159
|
+
const { port1, port2 } = makeChannel();
|
|
160
|
+
port = port1;
|
|
161
|
+
port1.addEventListener("message", onPort);
|
|
162
|
+
port1.start?.();
|
|
163
|
+
frame.postMessage({
|
|
164
|
+
source: EDITOR_SOURCE,
|
|
165
|
+
type: "connect",
|
|
166
|
+
version: 1
|
|
167
|
+
}, options.origin, [port2]);
|
|
168
|
+
done();
|
|
169
|
+
};
|
|
170
|
+
const deadline = setTimeout(() => done(/* @__PURE__ */ new Error("The game did not answer: is it running as an editor (createEditorHost), and is this its origin?")), options.timeoutMs ?? 15e3);
|
|
171
|
+
win.addEventListener("message", onWindow);
|
|
172
|
+
const hello = () => target()?.postMessage({
|
|
173
|
+
source: EDITOR_SOURCE,
|
|
174
|
+
type: "hello",
|
|
175
|
+
version: 1
|
|
176
|
+
}, options.origin);
|
|
177
|
+
hello();
|
|
178
|
+
timer = setInterval(hello, 200);
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
get map() {
|
|
182
|
+
return current;
|
|
183
|
+
},
|
|
184
|
+
onMap(listener) {
|
|
185
|
+
const stop = onMap.add(listener);
|
|
186
|
+
if (current) listener(current);
|
|
187
|
+
return stop;
|
|
188
|
+
},
|
|
189
|
+
get interaction() {
|
|
190
|
+
return interaction;
|
|
191
|
+
},
|
|
192
|
+
onInteraction: onInteraction.add,
|
|
193
|
+
get selection() {
|
|
194
|
+
return selection;
|
|
195
|
+
},
|
|
196
|
+
onSelection: onSelection.add,
|
|
197
|
+
get paused() {
|
|
198
|
+
return paused;
|
|
199
|
+
},
|
|
200
|
+
onPaused: onPaused.add,
|
|
201
|
+
onPointer: onPointer.add,
|
|
202
|
+
onGameError: onGameError.add,
|
|
203
|
+
get state() {
|
|
204
|
+
return Object.fromEntries(values);
|
|
205
|
+
},
|
|
206
|
+
onState(key, listener) {
|
|
207
|
+
let set = stateListeners.get(key);
|
|
208
|
+
if (!set) stateListeners.set(key, set = /* @__PURE__ */ new Set());
|
|
209
|
+
set.add(listener);
|
|
210
|
+
if (values.has(key)) listener(values.get(key));
|
|
211
|
+
return () => void set.delete(listener);
|
|
212
|
+
},
|
|
213
|
+
command: (name, args) => control({
|
|
214
|
+
type: "command",
|
|
215
|
+
name,
|
|
216
|
+
...args !== void 0 ? { args } : {}
|
|
217
|
+
}),
|
|
218
|
+
setModes: (modes) => control({
|
|
219
|
+
type: "modes",
|
|
220
|
+
modes
|
|
221
|
+
}),
|
|
222
|
+
setTool: (tool) => control({
|
|
223
|
+
type: "tool",
|
|
224
|
+
...tool
|
|
225
|
+
}),
|
|
226
|
+
select: (targets) => control({
|
|
227
|
+
type: "select",
|
|
228
|
+
targets
|
|
229
|
+
}),
|
|
230
|
+
pause: () => control({
|
|
231
|
+
type: "pause",
|
|
232
|
+
paused: true
|
|
233
|
+
}),
|
|
234
|
+
resume: () => control({
|
|
235
|
+
type: "pause",
|
|
236
|
+
paused: false
|
|
237
|
+
}),
|
|
238
|
+
camera: (x, y) => control({
|
|
239
|
+
type: "camera",
|
|
240
|
+
x,
|
|
241
|
+
y
|
|
242
|
+
}),
|
|
243
|
+
dispose() {
|
|
244
|
+
disposed = true;
|
|
245
|
+
generation += 1;
|
|
246
|
+
remote?.disconnect();
|
|
247
|
+
port?.close();
|
|
248
|
+
port = void 0;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
export { connectEditorFrame };
|
|
254
|
+
|
|
255
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../src/embed/client.ts"],"sourcesContent":["import type { TargetRef } from \"../interaction/picking\";\nimport type { JsonValue } from \"../model/json\";\nimport type { MapModel } from \"../model/model\";\nimport type { PatchHandlers } from \"../model/patches\";\nimport type { ApplyResult, Unsubscribe } from \"../model/result\";\nimport type { ClientMessage, MapTransport, ServerMessage } from \"../protocol\";\nimport { joinRemoteMap, type RemoteMap } from \"../remote\";\nimport {\n EDITOR_PROTOCOL_VERSION,\n EDITOR_SOURCE,\n parseEnvelope,\n parseEvent,\n parseHandshake,\n type ControlMessage,\n type EditorEvent,\n type EditorTool,\n type MessageEventLike,\n type PortLike,\n type TargetLike,\n type WindowLike,\n} from \"./protocol\";\n\nexport interface EditorMap {\n readonly id: string;\n /** The map as the game's authority has it. Trusted: raw patches, undo and redo are accepted. */\n readonly model: MapModel<Promise<ApplyResult>>;\n}\n\nexport interface EditorInteraction {\n readonly modes: readonly string[];\n readonly tool: EditorTool;\n readonly rejection: { readonly reason: string; readonly message?: string } | null;\n}\n\nexport interface ConnectEditorFrameOptions {\n /** The iframe that shows the game, or its window. */\n readonly frame: { readonly contentWindow: TargetLike | null } | TargetLike;\n /** Origin of the game, e.g. `\"https://game.example.com\"`. Nothing from another origin is believed. */\n readonly origin: string;\n /**\n * Where the game really runs, when it is not the window of the frame itself: a page that wraps the game in\n * another frame (a sandbox) gives the inner window, e.g. `(outer) => outer.frames[0]`. Messages are sent to it, and\n * only messages that come from it are believed.\n */\n readonly resolveWindow?: (frameWindow: TargetLike) => TargetLike | null | undefined;\n /** How long to wait for the game to answer. Defaults to 15000. */\n readonly timeoutMs?: number;\n readonly window?: WindowLike;\n /** Creates the channel. Defaults to `new MessageChannel()`. */\n readonly createChannel?: () => { port1: PortLike; port2: PortLike };\n /** Handlers of the patch types the game's features add (`terrainPatchHandlers`): the Studio's model must know them to apply what it is told. */\n readonly patchHandlers?: readonly PatchHandlers[];\n /** How long to wait for the first map, once connected. Passed to `joinRemoteMap`. */\n readonly mapTimeoutMs?: number;\n readonly onError?: (error: unknown) => void;\n}\n\nexport interface EditorFrame {\n /** The map the game shows, once joined. */\n readonly map: EditorMap | undefined;\n /** Called with every map the game shows, now included if there is one. The model of a previous map stops being fed. */\n onMap(listener: (map: EditorMap) => void): Unsubscribe;\n readonly interaction: EditorInteraction | undefined;\n onInteraction(listener: (interaction: EditorInteraction) => void): Unsubscribe;\n readonly selection: readonly TargetRef[];\n onSelection(listener: (selection: readonly TargetRef[]) => void): Unsubscribe;\n readonly paused: boolean;\n onPaused(listener: (paused: boolean) => void): Unsubscribe;\n /** World position of the pointer in the game, for a status bar. */\n onPointer(listener: (point: { x: number; y: number }) => void): Unsubscribe;\n /** Problems the game reports (a tool that cannot start, a message it did not understand). */\n onGameError(listener: (message: string) => void): Unsubscribe;\n /** The last value of what the game keeps for the Studio (`tool`, `canUndo`…), by key. */\n readonly state: Readonly<Record<string, JsonValue>>;\n /** Called when the game changes a value, and with the ones it already had when a listener is added. */\n onState(key: string, listener: (value: JsonValue) => void): Unsubscribe;\n /** Asks the game to do something it registered: `frame.command(\"copy\")`, `frame.command(\"place\", { … })`. */\n command(name: string, args?: JsonValue): void;\n setModes(modes: readonly string[]): void;\n setTool(tool: EditorTool): void;\n select(targets: readonly TargetRef[]): void;\n pause(): void;\n resume(): void;\n camera(x: number, y: number): void;\n dispose(): void;\n}\n\nconst listeners = <T>() => {\n const set = new Set<(value: T) => void>();\n return {\n add(listener: (value: T) => void): Unsubscribe {\n set.add(listener);\n return () => set.delete(listener);\n },\n emit(value: T, onError: (error: unknown) => void): void {\n for (const listener of [...set]) {\n try {\n listener(value);\n } catch (error) {\n onError(error);\n }\n }\n },\n };\n};\n\n/**\n * The Studio's end of the editor channels: finds the game in the frame, agrees on a private channel with it,\n * and gives back the map (a model that works like any other), the state of the tools, and commands.\n * Framework-neutral: wrap `onMap` and friends in Angular signals, or whatever the Studio uses.\n */\nexport async function connectEditorFrame(options: ConnectEditorFrameOptions): Promise<EditorFrame> {\n const win = options.window ?? (globalThis as unknown as { window?: WindowLike }).window;\n if (!win) throw new Error(\"connectEditorFrame needs a window\");\n const report = options.onError ?? ((error: unknown) => console.error(\"[map-builder editor]\", error));\n const target = (): TargetLike | null => {\n const frameWindow = \"contentWindow\" in options.frame ? options.frame.contentWindow : options.frame;\n if (!frameWindow || !options.resolveWindow) return frameWindow;\n try {\n return options.resolveWindow(frameWindow) ?? null;\n } catch {\n return null; // the inner frame is not there (yet)\n }\n };\n const makeChannel = options.createChannel ?? (() => new MessageChannel() as unknown as { port1: PortLike; port2: PortLike });\n\n let port: PortLike | undefined;\n let current: EditorMap | undefined;\n let interaction: EditorInteraction | undefined;\n let selection: readonly TargetRef[] = [];\n let paused = false;\n let disposed = false;\n let generation = 0;\n let mapListeners: Set<(message: ServerMessage) => void> | undefined;\n let activeId: string | undefined;\n\n const onMap = listeners<EditorMap>();\n const onInteraction = listeners<EditorInteraction>();\n const onSelection = listeners<readonly TargetRef[]>();\n const onPaused = listeners<boolean>();\n const onPointer = listeners<{ x: number; y: number }>();\n const onGameError = listeners<string>();\n const values = new Map<string, JsonValue>();\n const stateListeners = new Map<string, Set<(value: JsonValue) => void>>();\n\n const send = (envelope: unknown): void => {\n try {\n port?.postMessage(envelope);\n } catch (error) {\n report(error);\n }\n };\n const control = (message: ControlMessage): void => send({ ch: \"control\", message });\n\n let remote: RemoteMap | undefined;\n const joinMap = (id: string): void => {\n remote?.disconnect();\n remote = undefined;\n current = undefined;\n const token = ++generation;\n activeId = id;\n const mine = new Set<(message: ServerMessage) => void>();\n mapListeners = mine;\n const transport: MapTransport = {\n send: (message: ClientMessage) => send({ ch: \"map\", map: id, message }),\n onMessage(listener) {\n mine.add(listener);\n return () => mine.delete(listener);\n },\n };\n joinRemoteMap({\n transport,\n ...(options.mapTimeoutMs !== undefined ? { timeoutMs: options.mapTimeoutMs } : {}),\n ...(options.patchHandlers ? { model: { patchHandlers: options.patchHandlers } } : {}),\n }).then(\n (joined) => {\n if (token !== generation || disposed) return joined.disconnect();\n remote = joined;\n current = { id, model: joined.model };\n onMap.emit(current, report);\n },\n (error: unknown) => {\n if (token === generation) report(error);\n },\n );\n };\n\n const handleEvent = (event: EditorEvent): void => {\n switch (event.type) {\n case \"map\":\n return joinMap(event.id);\n case \"selection\":\n selection = event.targets;\n return onSelection.emit(selection, report);\n case \"interaction\":\n interaction = { modes: event.modes, tool: event.tool, rejection: event.rejection };\n return onInteraction.emit(interaction, report);\n case \"paused\":\n paused = event.paused;\n return onPaused.emit(paused, report);\n case \"pointer\":\n return onPointer.emit({ x: event.x, y: event.y }, report);\n case \"state\": {\n values.set(event.key, event.value);\n for (const listener of [...(stateListeners.get(event.key) ?? [])]) {\n try {\n listener(event.value);\n } catch (error) {\n report(error);\n }\n }\n return;\n }\n case \"error\":\n return onGameError.emit(event.message, report);\n }\n };\n\n const onPort = (event: { data: unknown }): void => {\n const envelope = parseEnvelope(event.data);\n if (!envelope) return;\n if (envelope.ch === \"map\") {\n // Only what belongs to the map we joined last: a late message of the previous one is dropped.\n if (envelope.map === activeId) {\n for (const listener of [...(mapListeners ?? [])]) listener(envelope.message as ServerMessage);\n }\n } else if (envelope.ch === \"event\") {\n const parsed = parseEvent(envelope.message);\n if (parsed) handleEvent(parsed);\n }\n };\n\n // Meet the game: say hello until it answers `ready`, then give it the port.\n await new Promise<void>((resolve, reject) => {\n let timer: ReturnType<typeof setInterval> | undefined;\n const done = (error?: Error): void => {\n if (timer) clearInterval(timer);\n clearTimeout(deadline);\n win.removeEventListener(\"message\", onWindow);\n if (error) reject(error);\n else resolve();\n };\n const onWindow = (event: MessageEventLike): void => {\n const frame = target();\n if (!frame || event.source !== frame || event.origin !== options.origin) return;\n const handshake = parseHandshake(event.data);\n if (handshake?.type !== \"ready\") return;\n if (handshake.version !== EDITOR_PROTOCOL_VERSION) {\n return done(new Error(`This game speaks editor protocol ${handshake.version}, the Studio speaks ${EDITOR_PROTOCOL_VERSION}: it was built with another version of the map builder`));\n }\n const { port1, port2 } = makeChannel();\n port = port1;\n port1.addEventListener(\"message\", onPort);\n port1.start?.();\n frame.postMessage({ source: EDITOR_SOURCE, type: \"connect\", version: EDITOR_PROTOCOL_VERSION }, options.origin, [port2 as unknown as Transferable]);\n done();\n };\n const deadline = setTimeout(() => done(new Error(\"The game did not answer: is it running as an editor (createEditorHost), and is this its origin?\")), options.timeoutMs ?? 15_000);\n win.addEventListener(\"message\", onWindow);\n const hello = (): void => target()?.postMessage({ source: EDITOR_SOURCE, type: \"hello\", version: EDITOR_PROTOCOL_VERSION }, options.origin);\n hello();\n timer = setInterval(hello, 200);\n });\n\n return {\n get map() {\n return current;\n },\n onMap(listener) {\n const stop = onMap.add(listener);\n if (current) listener(current);\n return stop;\n },\n get interaction() {\n return interaction;\n },\n onInteraction: onInteraction.add,\n get selection() {\n return selection;\n },\n onSelection: onSelection.add,\n get paused() {\n return paused;\n },\n onPaused: onPaused.add,\n onPointer: onPointer.add,\n onGameError: onGameError.add,\n get state() {\n return Object.fromEntries(values);\n },\n onState(key, listener) {\n let set = stateListeners.get(key);\n if (!set) stateListeners.set(key, (set = new Set()));\n set.add(listener);\n if (values.has(key)) listener(values.get(key)!);\n return () => void set!.delete(listener);\n },\n command: (name, args) => control({ type: \"command\", name, ...(args !== undefined ? { args } : {}) }),\n setModes: (modes) => control({ type: \"modes\", modes }),\n setTool: (tool) => control({ type: \"tool\", ...tool }),\n select: (targets) => control({ type: \"select\", targets }),\n pause: () => control({ type: \"pause\", paused: true }),\n resume: () => control({ type: \"pause\", paused: false }),\n camera: (x, y) => control({ type: \"camera\", x, y }),\n dispose() {\n disposed = true;\n generation += 1;\n remote?.disconnect();\n port?.close();\n port = undefined;\n },\n };\n}\n"],"mappings":"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"}
|