@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,205 @@
|
|
|
1
|
+
import { createMapAuthority, restoreMapAuthority } from "../authority/authority.js";
|
|
2
|
+
import { MAP_BUILDER_CHANNEL, changedElementIds, forgetHitboxes, syncElementHitboxes, unwrap, wrap } from "../rpgjs/shared.js";
|
|
3
|
+
import { keyValueMapStore, roomStorageMapStore } from "./storage.js";
|
|
4
|
+
import { createModule, defineModule } from "@rpgjs/common";
|
|
5
|
+
//#region src/server/index.ts
|
|
6
|
+
var entries = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
/** The map each player is connected to. A player changing map does not always leave the old one through a hook. */
|
|
8
|
+
var connectedTo = /* @__PURE__ */ new Map();
|
|
9
|
+
/** The authority of a map, once it exists. For game code: `getMapAuthority(player.getCurrentMap())?.apply(…)`. */
|
|
10
|
+
var getMapAuthority = (map) => map ? entries.get(map)?.authority : void 0;
|
|
11
|
+
/** Resolves when the authority of a map exists, or to `undefined` for a map that is not modifiable. */
|
|
12
|
+
var whenMapAuthority = (map) => (map ? entries.get(map)?.ready : void 0) ?? Promise.resolve(void 0);
|
|
13
|
+
var isSource = (maps) => typeof maps === "object" && typeof maps.load === "function";
|
|
14
|
+
async function resolveDefinition(maps, mapId) {
|
|
15
|
+
if (typeof maps === "function") return maps(mapId);
|
|
16
|
+
if (isSource(maps)) try {
|
|
17
|
+
return await maps.load(mapId);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code === "not-found") return void 0;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
return Object.hasOwn(maps, mapId) ? maps[mapId] : void 0;
|
|
23
|
+
}
|
|
24
|
+
var isStore = (value) => typeof value?.load === "function";
|
|
25
|
+
/** The module of the map builder for an RPGJS server. Install it with `provideMapBuilder`, or `provideServerModules`. */
|
|
26
|
+
function createMapBuilderServer(options) {
|
|
27
|
+
const reportError = options.onError ?? ((error) => console.error(error));
|
|
28
|
+
const describeContext = {
|
|
29
|
+
assets: options.assets,
|
|
30
|
+
...options.authority?.hooks ? { hooks: options.authority.hooks } : {}
|
|
31
|
+
};
|
|
32
|
+
const eventsOptions = options.events === false ? void 0 : options.events ?? {};
|
|
33
|
+
const prefix = eventsOptions?.idPrefix ?? "builder:";
|
|
34
|
+
const physics = options.physics !== false;
|
|
35
|
+
const bound = /* @__PURE__ */ new WeakSet();
|
|
36
|
+
const hitboxHost = (map) => map;
|
|
37
|
+
/** Creates, moves and removes the RPGJS events of a map, in order. */
|
|
38
|
+
const syncEvents = (map, entry, authority, patches) => {
|
|
39
|
+
if (!eventsOptions) return;
|
|
40
|
+
const state = authority.model.state;
|
|
41
|
+
entry.queue = entry.queue.then(async () => {
|
|
42
|
+
const wanted = new Map(state.events.map((event) => [event.id, event]));
|
|
43
|
+
const touched = patches === "all" ? /* @__PURE__ */ new Set([...entry.eventIds, ...wanted.keys()]) : new Set(patches.flatMap((patch) => patch.type === "event.add" ? [patch.event.id] : patch.type === "event.remove" || patch.type === "event.move" ? [patch.id] : []));
|
|
44
|
+
for (const id of touched) {
|
|
45
|
+
const event = wanted.get(id);
|
|
46
|
+
const rpgId = `${prefix}${id}`;
|
|
47
|
+
const existing = map.getEvent(rpgId);
|
|
48
|
+
if (!event) {
|
|
49
|
+
if (entry.eventIds.delete(id) || existing) map.removeEvent(rpgId);
|
|
50
|
+
} else if (existing) await existing.teleport?.({
|
|
51
|
+
x: event.x,
|
|
52
|
+
y: event.y
|
|
53
|
+
});
|
|
54
|
+
else {
|
|
55
|
+
const defined = eventsOptions.define?.(event, map);
|
|
56
|
+
const hooks = defined?.hooks ?? {};
|
|
57
|
+
await map.createDynamicEvent({
|
|
58
|
+
id: rpgId,
|
|
59
|
+
x: event.x,
|
|
60
|
+
y: event.y,
|
|
61
|
+
...defined?.hitbox ? { hitbox: defined.hitbox } : {},
|
|
62
|
+
event: {
|
|
63
|
+
...hooks,
|
|
64
|
+
name: event.kind,
|
|
65
|
+
onInit() {
|
|
66
|
+
hooks.onInit?.call(this);
|
|
67
|
+
if (event.graphic) this.setGraphic?.(event.graphic);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
entry.eventIds.add(id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}).catch(reportError);
|
|
75
|
+
};
|
|
76
|
+
const createEntry = (map) => {
|
|
77
|
+
const entry = {
|
|
78
|
+
ready: Promise.resolve(void 0),
|
|
79
|
+
connections: /* @__PURE__ */ new Map(),
|
|
80
|
+
eventIds: /* @__PURE__ */ new Set(),
|
|
81
|
+
queue: Promise.resolve()
|
|
82
|
+
};
|
|
83
|
+
entry.ready = (async () => {
|
|
84
|
+
const definition = await resolveDefinition(options.maps, map.id);
|
|
85
|
+
if (!definition) return void 0;
|
|
86
|
+
const persistChoice = options.persist?.(map);
|
|
87
|
+
const persist = persistChoice ? isStore(persistChoice) ? {
|
|
88
|
+
store: persistChoice,
|
|
89
|
+
id: map.id
|
|
90
|
+
} : {
|
|
91
|
+
...persistChoice,
|
|
92
|
+
id: persistChoice.id ?? map.id
|
|
93
|
+
} : void 0;
|
|
94
|
+
const authorityOptions = {
|
|
95
|
+
...options.authority,
|
|
96
|
+
definition,
|
|
97
|
+
assets: options.assets,
|
|
98
|
+
...persist ? { persist } : {},
|
|
99
|
+
onError: reportError,
|
|
100
|
+
onChange: (change) => {
|
|
101
|
+
if (physics) syncElementHitboxes(hitboxHost(map), authority.model.state, changedElementIds(change.patches), describeContext);
|
|
102
|
+
syncEvents(map, entry, authority, change.patches);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const authority = persist ? await restoreMapAuthority({
|
|
106
|
+
...authorityOptions,
|
|
107
|
+
persist
|
|
108
|
+
}) : createMapAuthority(authorityOptions);
|
|
109
|
+
entry.authority = authority;
|
|
110
|
+
if (physics) syncElementHitboxes(hitboxHost(map), authority.model.state, "all", describeContext);
|
|
111
|
+
syncEvents(map, entry, authority, "all");
|
|
112
|
+
options.onReady?.(authority, map);
|
|
113
|
+
return authority;
|
|
114
|
+
})().catch((error) => {
|
|
115
|
+
reportError(error);
|
|
116
|
+
});
|
|
117
|
+
return entry;
|
|
118
|
+
};
|
|
119
|
+
const entryOf = (map) => {
|
|
120
|
+
let entry = entries.get(map);
|
|
121
|
+
if (!entry) {
|
|
122
|
+
entry = createEntry(map);
|
|
123
|
+
entries.set(map, entry);
|
|
124
|
+
}
|
|
125
|
+
return entry;
|
|
126
|
+
};
|
|
127
|
+
/** Routes what a player sends to the connection they have on the map they are on. */
|
|
128
|
+
const bindPlayer = (player) => {
|
|
129
|
+
if (bound.has(player)) return;
|
|
130
|
+
bound.add(player);
|
|
131
|
+
player.on(MAP_BUILDER_CHANNEL, (raw) => {
|
|
132
|
+
const envelope = unwrap(raw);
|
|
133
|
+
const map = player.getCurrentMap();
|
|
134
|
+
if (!envelope || !map || envelope.map !== map.id) return;
|
|
135
|
+
entries.get(map)?.connections.get(player.id)?.receive(envelope.message);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
/** Ends the connection of a player to a map (to whichever they are on, when none is given). */
|
|
139
|
+
const disconnect = (player, only) => {
|
|
140
|
+
const current = connectedTo.get(player.id);
|
|
141
|
+
if (!current || only && current.map !== only) return;
|
|
142
|
+
current.connection.disconnect();
|
|
143
|
+
entries.get(current.map)?.connections.delete(player.id);
|
|
144
|
+
connectedTo.delete(player.id);
|
|
145
|
+
};
|
|
146
|
+
return defineModule({
|
|
147
|
+
player: {
|
|
148
|
+
onConnected: (player) => bindPlayer(player),
|
|
149
|
+
onJoinMap: (player) => bindPlayer(player)
|
|
150
|
+
},
|
|
151
|
+
map: {
|
|
152
|
+
async onLoad(map) {
|
|
153
|
+
const authority = await entryOf(map).ready;
|
|
154
|
+
if (!authority) return;
|
|
155
|
+
if (physics) syncElementHitboxes(hitboxHost(map), authority.model.state, "all", describeContext);
|
|
156
|
+
syncEvents(map, entryOf(map), authority, "all");
|
|
157
|
+
},
|
|
158
|
+
onPhysicsInit(map) {
|
|
159
|
+
forgetHitboxes(map);
|
|
160
|
+
const authority = entries.get(map)?.authority;
|
|
161
|
+
if (physics && authority) syncElementHitboxes(hitboxHost(map), authority.model.state, "all", describeContext);
|
|
162
|
+
},
|
|
163
|
+
async onJoin(player, map) {
|
|
164
|
+
bindPlayer(player);
|
|
165
|
+
const entry = entryOf(map);
|
|
166
|
+
const authority = await entry.ready;
|
|
167
|
+
if (!authority) return;
|
|
168
|
+
disconnect(player);
|
|
169
|
+
const id = map.id;
|
|
170
|
+
const connection = authority.connect({
|
|
171
|
+
id: player.id,
|
|
172
|
+
actor: options.actor?.(player) ?? player,
|
|
173
|
+
send: (message) => {
|
|
174
|
+
if (player.getCurrentMap() !== map) return disconnect(player, map);
|
|
175
|
+
player.emit(MAP_BUILDER_CHANNEL, wrap(id, message));
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
entry.connections.set(player.id, connection);
|
|
179
|
+
connectedTo.set(player.id, {
|
|
180
|
+
map,
|
|
181
|
+
connection
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
onLeave(player, map) {
|
|
185
|
+
disconnect(player, map);
|
|
186
|
+
const entry = entries.get(map);
|
|
187
|
+
if (entry && entry.connections.size === 0) entry.authority?.flush().catch(reportError);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Installs the map builder on an RPGJS server:
|
|
194
|
+
*
|
|
195
|
+
* ```ts
|
|
196
|
+
* createServer({ providers: [provideMapBuilder({ assets, maps: { forest }, authority: { … } })] })
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
function provideMapBuilder(options) {
|
|
200
|
+
return createModule("MapBuilder", [{ server: createMapBuilderServer(options) }]);
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
203
|
+
export { MAP_BUILDER_CHANNEL, createMapBuilderServer, getMapAuthority, keyValueMapStore, provideMapBuilder, roomStorageMapStore, whenMapAuthority };
|
|
204
|
+
|
|
205
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/server/index.ts"],"sourcesContent":["import { createModule, defineModule, type RpgProvider } from \"@rpgjs/common\";\nimport type { RpgMap, RpgPlayer, RpgServer } from \"@rpgjs/server\";\nimport {\n createMapAuthority,\n restoreMapAuthority,\n type AuthorityConnection,\n type MapAuthority,\n type MapAuthorityOptions,\n type PersistOptions,\n} from \"../authority/authority\";\nimport type { MapStore } from \"../authority/store\";\nimport type { AssetResolver } from \"../model/assets\";\nimport type { MapDefinition, MapEvent } from \"../model/definition\";\nimport type { MapChange } from \"../model/model\";\nimport type { MapPatch } from \"../model/patches\";\nimport type { Catalog } from \"../interaction/catalog\";\nimport type { MapSource } from \"../model/source\";\nimport type { DescribeContext } from \"../renderer/scene\";\nimport {\n MAP_BUILDER_CHANNEL,\n changedElementIds,\n forgetHitboxes,\n syncElementHitboxes,\n unwrap,\n wrap,\n type StaticHitboxHost,\n} from \"../rpgjs/shared\";\n\nexport { MAP_BUILDER_CHANNEL } from \"../rpgjs/shared\";\nexport { keyValueMapStore, roomStorageMapStore, type KeyValueStorage } from \"./storage\";\n\n/** Where the modifiable maps come from, by the id of the RPGJS map. A map without a definition is left alone. */\nexport type MapBuilderMaps =\n | Readonly<Record<string, MapDefinition>>\n | ((mapId: string) => MapDefinition | undefined | Promise<MapDefinition | undefined>)\n | MapSource;\n\n/** An RPGJS event made for a map event: hooks, hitbox, and what the game wants it to do. */\nexport interface MapBuilderEventDefinition {\n readonly hooks?: Record<string, unknown>;\n readonly hitbox?: { readonly width: number; readonly height: number };\n}\n\nexport interface MapBuilderEventsOptions {\n /**\n * What an RPGJS event does for a map event: `onAction`, `onPlayerTouch`… By default it is\n * only drawn, with the `graphic` of the map event as its RPGJS graphic.\n */\n readonly define?: (event: MapEvent, map: RpgMap) => MapBuilderEventDefinition | undefined;\n /** Prefix of the ids of the RPGJS events, so they never meet the game's own. Defaults to `\"builder:\"`. */\n readonly idPrefix?: string;\n}\n\nexport interface MapBuilderServerOptions<C extends Catalog<string> = Catalog<string>> {\n /** What the maps are made of. A server has no image to measure: assets must declare a `size` or a `frame`. */\n readonly assets: AssetResolver;\n readonly maps: MapBuilderMaps;\n /** Rules, catalog, who may send patches, who is the author of a change… */\n readonly authority?: Omit<MapAuthorityOptions<C>, \"definition\" | \"revision\" | \"assets\" | \"persist\" | \"onChange\" | \"replay\" | \"onError\">;\n /** Keep a map between restarts: `(map) => ({ store: roomStorageMapStore(map) })`. Off by default. */\n readonly persist?: (map: RpgMap) => Omit<PersistOptions, \"id\"> & { readonly id?: string } | MapStore | undefined;\n /** Who is acting, as the rules of the pipelines see them. Defaults to the player. */\n readonly actor?: (player: RpgPlayer) => unknown;\n /** Keep the static colliders of the physics world in step with the elements. Defaults to true. */\n readonly physics?: boolean;\n /** Turn the events of the map into RPGJS events. Defaults to on, drawn only; `false` leaves events alone. */\n readonly events?: false | MapBuilderEventsOptions;\n /** Called once the authority of a map exists: the place to hook a lever, a quest, a timer. */\n readonly onReady?: (authority: MapAuthority, map: RpgMap) => void;\n readonly onError?: (error: unknown) => void;\n}\n\ninterface Entry {\n readonly ready: Promise<MapAuthority | undefined>;\n authority?: MapAuthority;\n /** By player id: the player object is not always the same instance from one hook to the other. */\n readonly connections: Map<string, AuthorityConnection>;\n /** RPGJS events this module created, so it can remove them. */\n readonly eventIds: Set<string>;\n /** Event changes are applied one after the other: creating an event is asynchronous. */\n queue: Promise<void>;\n}\n\nconst entries = new WeakMap<object, Entry>();\n/** The map each player is connected to. A player changing map does not always leave the old one through a hook. */\nconst connectedTo = new Map<string, { readonly map: object; readonly connection: AuthorityConnection }>();\n\n/** The authority of a map, once it exists. For game code: `getMapAuthority(player.getCurrentMap())?.apply(…)`. */\nexport const getMapAuthority = (map: object | null | undefined): MapAuthority | undefined => (map ? entries.get(map)?.authority : undefined);\n\n/** Resolves when the authority of a map exists, or to `undefined` for a map that is not modifiable. */\nexport const whenMapAuthority = (map: object | null | undefined): Promise<MapAuthority | undefined> =>\n (map ? entries.get(map)?.ready : undefined) ?? Promise.resolve(undefined);\n\nconst isSource = (maps: MapBuilderMaps): maps is MapSource => typeof maps === \"object\" && typeof (maps as MapSource).load === \"function\";\n\nasync function resolveDefinition(maps: MapBuilderMaps, mapId: string): Promise<MapDefinition | undefined> {\n if (typeof maps === \"function\") return maps(mapId);\n if (isSource(maps)) {\n try {\n return await maps.load(mapId);\n } catch (error) {\n if ((error as { code?: string }).code === \"not-found\") return undefined;\n throw error;\n }\n }\n return Object.hasOwn(maps, mapId) ? (maps as Record<string, MapDefinition>)[mapId] : undefined;\n}\n\nconst isStore = (value: unknown): value is MapStore => typeof (value as MapStore | undefined)?.load === \"function\";\n\n/** The module of the map builder for an RPGJS server. Install it with `provideMapBuilder`, or `provideServerModules`. */\nexport function createMapBuilderServer<C extends Catalog<string> = Catalog<string>>(options: MapBuilderServerOptions<C>): RpgServer {\n const reportError = options.onError ?? ((error: unknown) => console.error(error));\n const describeContext: DescribeContext = { assets: options.assets, ...(options.authority?.hooks ? { hooks: options.authority.hooks } : {}) };\n const eventsOptions = options.events === false ? undefined : (options.events ?? {});\n const prefix = eventsOptions?.idPrefix ?? \"builder:\";\n const physics = options.physics !== false;\n const bound = new WeakSet<object>();\n\n const hitboxHost = (map: RpgMap): StaticHitboxHost => map as unknown as StaticHitboxHost;\n\n /** Creates, moves and removes the RPGJS events of a map, in order. */\n const syncEvents = (map: RpgMap, entry: Entry, authority: MapAuthority, patches: readonly MapPatch[] | \"all\"): void => {\n if (!eventsOptions) return;\n const state = authority.model.state;\n entry.queue = entry.queue\n .then(async () => {\n const wanted = new Map(state.events.map((event) => [event.id, event]));\n const touched =\n patches === \"all\"\n ? new Set<string>([...entry.eventIds, ...wanted.keys()])\n : new Set(\n patches.flatMap((patch) =>\n patch.type === \"event.add\" ? [patch.event.id] : patch.type === \"event.remove\" || patch.type === \"event.move\" ? [patch.id] : [],\n ),\n );\n for (const id of touched) {\n const event = wanted.get(id);\n const rpgId = `${prefix}${id}`;\n const existing = map.getEvent(rpgId) as unknown as { teleport?(position: { x: number; y: number }): unknown } | undefined;\n if (!event) {\n if (entry.eventIds.delete(id) || existing) map.removeEvent(rpgId);\n } else if (existing) {\n await existing.teleport?.({ x: event.x, y: event.y });\n } else {\n const defined = eventsOptions.define?.(event, map);\n const hooks = defined?.hooks ?? {};\n await map.createDynamicEvent({\n id: rpgId,\n x: event.x,\n y: event.y,\n ...(defined?.hitbox ? { hitbox: defined.hitbox } : {}),\n event: {\n ...hooks,\n name: event.kind,\n onInit(this: { setGraphic?(graphic: string): void }) {\n (hooks.onInit as (() => void) | undefined)?.call(this);\n if (event.graphic) this.setGraphic?.(event.graphic);\n },\n } as never,\n });\n entry.eventIds.add(id);\n }\n }\n })\n .catch(reportError);\n };\n\n const createEntry = (map: RpgMap): Entry => {\n const entry: Entry = {\n ready: Promise.resolve(undefined),\n connections: new Map(),\n eventIds: new Set(),\n queue: Promise.resolve(),\n };\n const ready = (async (): Promise<MapAuthority | undefined> => {\n const definition = await resolveDefinition(options.maps, map.id);\n if (!definition) return undefined;\n\n const persistChoice = options.persist?.(map);\n const persist: PersistOptions | undefined = persistChoice\n ? isStore(persistChoice)\n ? { store: persistChoice, id: map.id }\n : { ...persistChoice, id: persistChoice.id ?? map.id }\n : undefined;\n\n const authorityOptions = {\n ...options.authority,\n definition,\n assets: options.assets,\n ...(persist ? { persist } : {}),\n onError: reportError,\n onChange: (change: MapChange) => {\n if (physics) syncElementHitboxes(hitboxHost(map), authority.model.state, changedElementIds(change.patches), describeContext);\n syncEvents(map, entry, authority, change.patches);\n },\n } satisfies MapAuthorityOptions<C>;\n const authority: MapAuthority = persist\n ? await restoreMapAuthority({ ...authorityOptions, persist })\n : createMapAuthority(authorityOptions);\n\n entry.authority = authority;\n if (physics) syncElementHitboxes(hitboxHost(map), authority.model.state, \"all\", describeContext);\n syncEvents(map, entry, authority, \"all\");\n options.onReady?.(authority, map);\n return authority;\n })().catch((error) => {\n reportError(error);\n return undefined;\n });\n (entry as { ready: Promise<MapAuthority | undefined> }).ready = ready;\n return entry;\n };\n\n const entryOf = (map: RpgMap): Entry => {\n let entry = entries.get(map);\n if (!entry) {\n entry = createEntry(map);\n entries.set(map, entry);\n }\n return entry;\n };\n\n /** Routes what a player sends to the connection they have on the map they are on. */\n const bindPlayer = (player: RpgPlayer): void => {\n if (bound.has(player)) return;\n bound.add(player);\n player.on(MAP_BUILDER_CHANNEL, (raw: unknown) => {\n const envelope = unwrap(raw);\n const map = player.getCurrentMap() as RpgMap | null | undefined;\n if (!envelope || !map || envelope.map !== map.id) return;\n entries.get(map)?.connections.get(player.id)?.receive(envelope.message);\n });\n };\n\n /** Ends the connection of a player to a map (to whichever they are on, when none is given). */\n const disconnect = (player: RpgPlayer, only?: object): void => {\n const current = connectedTo.get(player.id);\n if (!current || (only && current.map !== only)) return;\n current.connection.disconnect();\n entries.get(current.map)?.connections.delete(player.id);\n connectedTo.delete(player.id);\n };\n\n return defineModule<RpgServer>({\n player: {\n onConnected: (player: RpgPlayer) => bindPlayer(player),\n onJoinMap: (player: RpgPlayer) => bindPlayer(player),\n },\n map: {\n async onLoad(map: RpgMap) {\n const authority = await entryOf(map).ready;\n if (!authority) return;\n // Runs again whenever the map is updated or restored, which wipes events and colliders: put them back.\n if (physics) syncElementHitboxes(hitboxHost(map), authority.model.state, \"all\", describeContext);\n syncEvents(map, entryOf(map), authority, \"all\");\n },\n onPhysicsInit(map: RpgMap) {\n forgetHitboxes(map); // the physics world was rebuilt: nothing we applied is there any more\n const authority = entries.get(map)?.authority;\n if (physics && authority) syncElementHitboxes(hitboxHost(map), authority.model.state, \"all\", describeContext);\n },\n async onJoin(player: RpgPlayer, map: RpgMap) {\n bindPlayer(player);\n const entry = entryOf(map);\n const authority = await entry.ready;\n if (!authority) return;\n // Whatever the player was connected to before, they are on this map now.\n disconnect(player);\n const id = map.id;\n const connection = authority.connect({\n id: player.id,\n actor: options.actor?.(player) ?? player,\n send: (message) => {\n // A player who moved on, or went away, is dropped the first time something is sent to them.\n if ((player.getCurrentMap() as unknown) !== map) return disconnect(player, map);\n player.emit(MAP_BUILDER_CHANNEL, wrap(id, message));\n },\n });\n entry.connections.set(player.id, connection);\n connectedTo.set(player.id, { map, connection });\n },\n onLeave(player: RpgPlayer, map: RpgMap) {\n disconnect(player, map);\n // RPGJS has no hook for a map that is unloaded: the last player leaving is the closest moment to save what is pending.\n const entry = entries.get(map);\n if (entry && entry.connections.size === 0) void entry.authority?.flush().catch(reportError);\n },\n },\n });\n}\n\n/**\n * Installs the map builder on an RPGJS server:\n *\n * ```ts\n * createServer({ providers: [provideMapBuilder({ assets, maps: { forest }, authority: { … } })] })\n * ```\n */\nexport function provideMapBuilder<C extends Catalog<string> = Catalog<string>>(options: MapBuilderServerOptions<C>): RpgProvider[] {\n return createModule(\"MapBuilder\", [{ server: createMapBuilderServer(options) }]);\n}\n"],"mappings":";;;;;AAmFA,IAAM,0BAAU,IAAI,QAAuB;;AAE3C,IAAM,8BAAc,IAAI,IAAgF;;AAGxG,IAAa,mBAAmB,QAA8D,MAAM,QAAQ,IAAI,GAAG,CAAC,EAAE,YAAY,KAAA;;AAGlI,IAAa,oBAAoB,SAC9B,MAAM,QAAQ,IAAI,GAAG,CAAC,EAAE,QAAQ,KAAA,MAAc,QAAQ,QAAQ,KAAA,CAAS;AAE1E,IAAM,YAAY,SAA4C,OAAO,SAAS,YAAY,OAAQ,KAAmB,SAAS;AAE9H,eAAe,kBAAkB,MAAsB,OAAmD;CACxG,IAAI,OAAO,SAAS,YAAY,OAAO,KAAK,KAAK;CACjD,IAAI,SAAS,IAAI,GACf,IAAI;EACF,OAAO,MAAM,KAAK,KAAK,KAAK;CAC9B,SAAS,OAAO;EACd,IAAK,MAA4B,SAAS,aAAa,OAAO,KAAA;EAC9D,MAAM;CACR;CAEF,OAAO,OAAO,OAAO,MAAM,KAAK,IAAK,KAAuC,SAAS,KAAA;AACvF;AAEA,IAAM,WAAW,UAAsC,OAAQ,OAAgC,SAAS;;AAGxG,SAAgB,uBAAoE,SAAgD;CAClI,MAAM,cAAc,QAAQ,aAAa,UAAmB,QAAQ,MAAM,KAAK;CAC/E,MAAM,kBAAmC;EAAE,QAAQ,QAAQ;EAAQ,GAAI,QAAQ,WAAW,QAAQ,EAAE,OAAO,QAAQ,UAAU,MAAM,IAAI,CAAC;CAAG;CAC3I,MAAM,gBAAgB,QAAQ,WAAW,QAAQ,KAAA,IAAa,QAAQ,UAAU,CAAC;CACjF,MAAM,SAAS,eAAe,YAAY;CAC1C,MAAM,UAAU,QAAQ,YAAY;CACpC,MAAM,wBAAQ,IAAI,QAAgB;CAElC,MAAM,cAAc,QAAkC;;CAGtD,MAAM,cAAc,KAAa,OAAc,WAAyB,YAA+C;EACrH,IAAI,CAAC,eAAe;EACpB,MAAM,QAAQ,UAAU,MAAM;EAC9B,MAAM,QAAQ,MAAM,MACjB,KAAK,YAAY;GAChB,MAAM,SAAS,IAAI,IAAI,MAAM,OAAO,KAAK,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC;GACrE,MAAM,UACJ,YAAY,wBACR,IAAI,IAAY,CAAC,GAAG,MAAM,UAAU,GAAG,OAAO,KAAK,CAAC,CAAC,IACrD,IAAI,IACF,QAAQ,SAAS,UACf,MAAM,SAAS,cAAc,CAAC,MAAM,MAAM,EAAE,IAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,CAC/H,CACF;GACN,KAAK,MAAM,MAAM,SAAS;IACxB,MAAM,QAAQ,OAAO,IAAI,EAAE;IAC3B,MAAM,QAAQ,GAAG,SAAS;IAC1B,MAAM,WAAW,IAAI,SAAS,KAAK;IACnC,IAAI,CAAC;SACC,MAAM,SAAS,OAAO,EAAE,KAAK,UAAU,IAAI,YAAY,KAAK;IAAA,OAC3D,IAAI,UACT,MAAM,SAAS,WAAW;KAAE,GAAG,MAAM;KAAG,GAAG,MAAM;IAAE,CAAC;SAC/C;KACL,MAAM,UAAU,cAAc,SAAS,OAAO,GAAG;KACjD,MAAM,QAAQ,SAAS,SAAS,CAAC;KACjC,MAAM,IAAI,mBAAmB;MAC3B,IAAI;MACJ,GAAG,MAAM;MACT,GAAG,MAAM;MACT,GAAI,SAAS,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;MACpD,OAAO;OACL,GAAG;OACH,MAAM,MAAM;OACZ,SAAqD;QACnD,MAAO,QAAqC,KAAK,IAAI;QACrD,IAAI,MAAM,SAAS,KAAK,aAAa,MAAM,OAAO;OACpD;MACF;KACF,CAAC;KACD,MAAM,SAAS,IAAI,EAAE;IACvB;GACF;EACF,CAAC,CAAC,CACD,MAAM,WAAW;CACtB;CAEA,MAAM,eAAe,QAAuB;EAC1C,MAAM,QAAe;GACnB,OAAO,QAAQ,QAAQ,KAAA,CAAS;GAChC,6BAAa,IAAI,IAAI;GACrB,0BAAU,IAAI,IAAI;GAClB,OAAO,QAAQ,QAAQ;EACzB;EAoCA,MAAwD,SAnCzC,YAA+C;GAC5D,MAAM,aAAa,MAAM,kBAAkB,QAAQ,MAAM,IAAI,EAAE;GAC/D,IAAI,CAAC,YAAY,OAAO,KAAA;GAExB,MAAM,gBAAgB,QAAQ,UAAU,GAAG;GAC3C,MAAM,UAAsC,gBACxC,QAAQ,aAAa,IACnB;IAAE,OAAO;IAAe,IAAI,IAAI;GAAG,IACnC;IAAE,GAAG;IAAe,IAAI,cAAc,MAAM,IAAI;GAAG,IACrD,KAAA;GAEJ,MAAM,mBAAmB;IACvB,GAAG,QAAQ;IACX;IACA,QAAQ,QAAQ;IAChB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;IAC7B,SAAS;IACT,WAAW,WAAsB;KAC/B,IAAI,SAAS,oBAAoB,WAAW,GAAG,GAAG,UAAU,MAAM,OAAO,kBAAkB,OAAO,OAAO,GAAG,eAAe;KAC3H,WAAW,KAAK,OAAO,WAAW,OAAO,OAAO;IAClD;GACF;GACA,MAAM,YAA0B,UAC5B,MAAM,oBAAoB;IAAE,GAAG;IAAkB;GAAQ,CAAC,IAC1D,mBAAmB,gBAAgB;GAEvC,MAAM,YAAY;GAClB,IAAI,SAAS,oBAAoB,WAAW,GAAG,GAAG,UAAU,MAAM,OAAO,OAAO,eAAe;GAC/F,WAAW,KAAK,OAAO,WAAW,KAAK;GACvC,QAAQ,UAAU,WAAW,GAAG;GAChC,OAAO;EACT,EAAA,CAAG,CAAC,CAAC,OAAO,UAAU;GACpB,YAAY,KAAK;EAEnB,CACgE;EAChE,OAAO;CACT;CAEA,MAAM,WAAW,QAAuB;EACtC,IAAI,QAAQ,QAAQ,IAAI,GAAG;EAC3B,IAAI,CAAC,OAAO;GACV,QAAQ,YAAY,GAAG;GACvB,QAAQ,IAAI,KAAK,KAAK;EACxB;EACA,OAAO;CACT;;CAGA,MAAM,cAAc,WAA4B;EAC9C,IAAI,MAAM,IAAI,MAAM,GAAG;EACvB,MAAM,IAAI,MAAM;EAChB,OAAO,GAAG,sBAAsB,QAAiB;GAC/C,MAAM,WAAW,OAAO,GAAG;GAC3B,MAAM,MAAM,OAAO,cAAc;GACjC,IAAI,CAAC,YAAY,CAAC,OAAO,SAAS,QAAQ,IAAI,IAAI;GAClD,QAAQ,IAAI,GAAG,CAAC,EAAE,YAAY,IAAI,OAAO,EAAE,CAAC,EAAE,QAAQ,SAAS,OAAO;EACxE,CAAC;CACH;;CAGA,MAAM,cAAc,QAAmB,SAAwB;EAC7D,MAAM,UAAU,YAAY,IAAI,OAAO,EAAE;EACzC,IAAI,CAAC,WAAY,QAAQ,QAAQ,QAAQ,MAAO;EAChD,QAAQ,WAAW,WAAW;EAC9B,QAAQ,IAAI,QAAQ,GAAG,CAAC,EAAE,YAAY,OAAO,OAAO,EAAE;EACtD,YAAY,OAAO,OAAO,EAAE;CAC9B;CAEA,OAAO,aAAwB;EAC7B,QAAQ;GACN,cAAc,WAAsB,WAAW,MAAM;GACrD,YAAY,WAAsB,WAAW,MAAM;EACrD;EACA,KAAK;GACH,MAAM,OAAO,KAAa;IACxB,MAAM,YAAY,MAAM,QAAQ,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC,WAAW;IAEhB,IAAI,SAAS,oBAAoB,WAAW,GAAG,GAAG,UAAU,MAAM,OAAO,OAAO,eAAe;IAC/F,WAAW,KAAK,QAAQ,GAAG,GAAG,WAAW,KAAK;GAChD;GACA,cAAc,KAAa;IACzB,eAAe,GAAG;IAClB,MAAM,YAAY,QAAQ,IAAI,GAAG,CAAC,EAAE;IACpC,IAAI,WAAW,WAAW,oBAAoB,WAAW,GAAG,GAAG,UAAU,MAAM,OAAO,OAAO,eAAe;GAC9G;GACA,MAAM,OAAO,QAAmB,KAAa;IAC3C,WAAW,MAAM;IACjB,MAAM,QAAQ,QAAQ,GAAG;IACzB,MAAM,YAAY,MAAM,MAAM;IAC9B,IAAI,CAAC,WAAW;IAEhB,WAAW,MAAM;IACjB,MAAM,KAAK,IAAI;IACf,MAAM,aAAa,UAAU,QAAQ;KACnC,IAAI,OAAO;KACX,OAAO,QAAQ,QAAQ,MAAM,KAAK;KAClC,OAAO,YAAY;MAEjB,IAAK,OAAO,cAAc,MAAkB,KAAK,OAAO,WAAW,QAAQ,GAAG;MAC9E,OAAO,KAAK,qBAAqB,KAAK,IAAI,OAAO,CAAC;KACpD;IACF,CAAC;IACD,MAAM,YAAY,IAAI,OAAO,IAAI,UAAU;IAC3C,YAAY,IAAI,OAAO,IAAI;KAAE;KAAK;IAAW,CAAC;GAChD;GACA,QAAQ,QAAmB,KAAa;IACtC,WAAW,QAAQ,GAAG;IAEtB,MAAM,QAAQ,QAAQ,IAAI,GAAG;IAC7B,IAAI,SAAS,MAAM,YAAY,SAAS,GAAG,MAAW,WAAW,MAAM,CAAC,CAAC,MAAM,WAAW;GAC5F;EACF;CACF,CAAC;AACH;;;;;;;;AASA,SAAgB,kBAA+D,SAAoD;CACjI,OAAO,aAAa,cAAc,CAAC,EAAE,QAAQ,uBAAuB,OAAO,EAAE,CAAC,CAAC;AACjF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MapStore } from '../authority/store';
|
|
2
|
+
/** The two calls of a key-value store: a Durable Object's storage, KV, a file. */
|
|
3
|
+
export interface KeyValueStorage {
|
|
4
|
+
get(key: string): unknown | Promise<unknown>;
|
|
5
|
+
put(key: string, value: unknown): unknown | Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Keeps maps in any key-value storage: a snapshot under one key, the changes made since under
|
|
9
|
+
* another. Values must be small enough for the storage (a Durable Object accepts 128 KiB per key
|
|
10
|
+
* with the key-value API): for large maps, write snapshots often so the log stays short, or give
|
|
11
|
+
* the authority a store of your own.
|
|
12
|
+
*/
|
|
13
|
+
export declare function keyValueMapStore(storage: KeyValueStorage, prefix?: string): MapStore;
|
|
14
|
+
/** The storage of the room of a map, which survives a restart of the server. */
|
|
15
|
+
export declare function roomStorageMapStore(map: object): MapStore;
|
|
16
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/server/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAyB,MAAM,oBAAoB,CAAC;AAE1E,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAuB,GAAG,QAAQ,CAiClG;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAKzD"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/server/storage.ts
|
|
2
|
+
/**
|
|
3
|
+
* Keeps maps in any key-value storage: a snapshot under one key, the changes made since under
|
|
4
|
+
* another. Values must be small enough for the storage (a Durable Object accepts 128 KiB per key
|
|
5
|
+
* with the key-value API): for large maps, write snapshots often so the log stays short, or give
|
|
6
|
+
* the authority a store of your own.
|
|
7
|
+
*/
|
|
8
|
+
function keyValueMapStore(storage, prefix = "$room:map-builder:") {
|
|
9
|
+
const snapshotKey = (id) => `${prefix}${id}:snapshot`;
|
|
10
|
+
const logKey = (id) => `${prefix}${id}:log`;
|
|
11
|
+
/** The log as this store wrote it: the authority is the only writer, so it is read from the storage once. */
|
|
12
|
+
const logs = /* @__PURE__ */ new Map();
|
|
13
|
+
const readLog = async (id) => {
|
|
14
|
+
let log = logs.get(id);
|
|
15
|
+
if (!log) {
|
|
16
|
+
log = await storage.get(logKey(id)) ?? [];
|
|
17
|
+
logs.set(id, log);
|
|
18
|
+
}
|
|
19
|
+
return log;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
async load(id) {
|
|
23
|
+
const snapshot = await storage.get(snapshotKey(id));
|
|
24
|
+
if (!snapshot) return void 0;
|
|
25
|
+
return {
|
|
26
|
+
snapshot,
|
|
27
|
+
patches: (await readLog(id)).filter((entry) => entry.revision > snapshot.revision)
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
async save(id, snapshot) {
|
|
31
|
+
await storage.put(snapshotKey(id), snapshot);
|
|
32
|
+
const rest = (await readLog(id)).filter((entry) => entry.revision > snapshot.revision);
|
|
33
|
+
logs.set(id, rest);
|
|
34
|
+
await storage.put(logKey(id), rest);
|
|
35
|
+
},
|
|
36
|
+
async append(id, entry) {
|
|
37
|
+
const log = [...await readLog(id), entry];
|
|
38
|
+
logs.set(id, log);
|
|
39
|
+
await storage.put(logKey(id), log);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** The storage of the room of a map, which survives a restart of the server. */
|
|
44
|
+
function roomStorageMapStore(map) {
|
|
45
|
+
const storage = map.partyRoom?.storage;
|
|
46
|
+
if (!storage) throw new Error("This map has no room storage: give the authority a store of its own (`persist`)");
|
|
47
|
+
return keyValueMapStore(storage);
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { keyValueMapStore, roomStorageMapStore };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","names":[],"sources":["../../src/server/storage.ts"],"sourcesContent":["import type { MapStore, PatchEntry, StoredMap } from \"../authority/store\";\n\n/** The two calls of a key-value store: a Durable Object's storage, KV, a file. */\nexport interface KeyValueStorage {\n get(key: string): unknown | Promise<unknown>;\n put(key: string, value: unknown): unknown | Promise<unknown>;\n}\n\n/**\n * Keeps maps in any key-value storage: a snapshot under one key, the changes made since under\n * another. Values must be small enough for the storage (a Durable Object accepts 128 KiB per key\n * with the key-value API): for large maps, write snapshots often so the log stays short, or give\n * the authority a store of your own.\n */\nexport function keyValueMapStore(storage: KeyValueStorage, prefix = \"$room:map-builder:\"): MapStore {\n const snapshotKey = (id: string): string => `${prefix}${id}:snapshot`;\n const logKey = (id: string): string => `${prefix}${id}:log`;\n /** The log as this store wrote it: the authority is the only writer, so it is read from the storage once. */\n const logs = new Map<string, PatchEntry[]>();\n const readLog = async (id: string): Promise<PatchEntry[]> => {\n let log = logs.get(id);\n if (!log) {\n log = ((await storage.get(logKey(id))) as PatchEntry[] | undefined) ?? [];\n logs.set(id, log);\n }\n return log;\n };\n\n return {\n async load(id): Promise<StoredMap | undefined> {\n const snapshot = (await storage.get(snapshotKey(id))) as StoredMap[\"snapshot\"] | undefined;\n if (!snapshot) return undefined;\n return { snapshot, patches: (await readLog(id)).filter((entry) => entry.revision > snapshot.revision) };\n },\n async save(id, snapshot) {\n await storage.put(snapshotKey(id), snapshot);\n // What the snapshot covers is no longer needed.\n const rest = (await readLog(id)).filter((entry) => entry.revision > snapshot.revision);\n logs.set(id, rest);\n await storage.put(logKey(id), rest);\n },\n async append(id, entry) {\n const log = [...(await readLog(id)), entry];\n logs.set(id, log);\n await storage.put(logKey(id), log);\n },\n };\n}\n\n/** The storage of the room of a map, which survives a restart of the server. */\nexport function roomStorageMapStore(map: object): MapStore {\n // RPGJS rooms keep their storage in a field that is not part of the public types.\n const storage = (map as { partyRoom?: { storage?: KeyValueStorage } }).partyRoom?.storage;\n if (!storage) throw new Error(\"This map has no room storage: give the authority a store of its own (`persist`)\");\n return keyValueMapStore(storage);\n}\n"],"mappings":";;;;;;;AAcA,SAAgB,iBAAiB,SAA0B,SAAS,sBAAgC;CAClG,MAAM,eAAe,OAAuB,GAAG,SAAS,GAAG;CAC3D,MAAM,UAAU,OAAuB,GAAG,SAAS,GAAG;;CAEtD,MAAM,uBAAO,IAAI,IAA0B;CAC3C,MAAM,UAAU,OAAO,OAAsC;EAC3D,IAAI,MAAM,KAAK,IAAI,EAAE;EACrB,IAAI,CAAC,KAAK;GACR,MAAQ,MAAM,QAAQ,IAAI,OAAO,EAAE,CAAC,KAAmC,CAAC;GACxE,KAAK,IAAI,IAAI,GAAG;EAClB;EACA,OAAO;CACT;CAEA,OAAO;EACL,MAAM,KAAK,IAAoC;GAC7C,MAAM,WAAY,MAAM,QAAQ,IAAI,YAAY,EAAE,CAAC;GACnD,IAAI,CAAC,UAAU,OAAO,KAAA;GACtB,OAAO;IAAE;IAAU,UAAU,MAAM,QAAQ,EAAE,EAAA,CAAG,QAAQ,UAAU,MAAM,WAAW,SAAS,QAAQ;GAAE;EACxG;EACA,MAAM,KAAK,IAAI,UAAU;GACvB,MAAM,QAAQ,IAAI,YAAY,EAAE,GAAG,QAAQ;GAE3C,MAAM,QAAQ,MAAM,QAAQ,EAAE,EAAA,CAAG,QAAQ,UAAU,MAAM,WAAW,SAAS,QAAQ;GACrF,KAAK,IAAI,IAAI,IAAI;GACjB,MAAM,QAAQ,IAAI,OAAO,EAAE,GAAG,IAAI;EACpC;EACA,MAAM,OAAO,IAAI,OAAO;GACtB,MAAM,MAAM,CAAC,GAAI,MAAM,QAAQ,EAAE,GAAI,KAAK;GAC1C,KAAK,IAAI,IAAI,GAAG;GAChB,MAAM,QAAQ,IAAI,OAAO,EAAE,GAAG,GAAG;EACnC;CACF;AACF;;AAGA,SAAgB,oBAAoB,KAAuB;CAEzD,MAAM,UAAW,IAAsD,WAAW;CAClF,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,iFAAiF;CAC/G,OAAO,iBAAiB,OAAO;AACjC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Skin, SkinLayerFactory } from './types';
|
|
2
|
+
export declare const SKIN_COLORS: {
|
|
3
|
+
readonly accent: 3900150;
|
|
4
|
+
readonly valid: 2278750;
|
|
5
|
+
readonly invalid: 15680580;
|
|
6
|
+
};
|
|
7
|
+
export interface GhostLookOptions {
|
|
8
|
+
readonly valid: number;
|
|
9
|
+
readonly invalid: number;
|
|
10
|
+
/** Opacity of the sprite of the item. */
|
|
11
|
+
readonly alpha: number;
|
|
12
|
+
/** Makes the ghost pulse: opacity swings around `alpha`. */
|
|
13
|
+
readonly pulse: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** The ghost of an item: its image, translucent, and an outline that says whether it can be placed here. */
|
|
16
|
+
export declare function ghostLayer(look: GhostLookOptions): SkinLayerFactory;
|
|
17
|
+
export interface FrameLookOptions {
|
|
18
|
+
/** Colour of the frame around what is selected. */
|
|
19
|
+
readonly color: number;
|
|
20
|
+
/** `"glow"` adds a soft halo around the line. */
|
|
21
|
+
readonly outline: "line" | "glow";
|
|
22
|
+
}
|
|
23
|
+
/** The selection, hover, drag and marquee layers, in a given look. */
|
|
24
|
+
export declare function framesSkin(look: FrameLookOptions, ghost: SkinLayerFactory): Skin;
|
|
25
|
+
/** A sober skin: plain frames, a translucent ghost. Enough for a tool, meant to be replaced. */
|
|
26
|
+
export declare function defaultSkin(): Skin;
|
|
27
|
+
//# sourceMappingURL=default.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default.d.ts","sourceRoot":"","sources":["../../src/skins/default.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAA0B,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE9E,eAAO,MAAM,WAAW;;;;CAAoE,CAAC;AAE7F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,4GAA4G;AAC5G,wBAAgB,UAAU,CAAC,IAAI,EAAE,gBAAgB,GAAG,gBAAgB,CAiDnE;AAED,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAoChF;AAED,gGAAgG;AAChG,wBAAgB,WAAW,IAAI,IAAI,CAKlC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { sameTarget } from "../interaction/picking.js";
|
|
2
|
+
import { drawFrames, frameLayer, nodeOf } from "./frames.js";
|
|
3
|
+
import { Container, Graphics, Sprite, Texture } from "pixi.js";
|
|
4
|
+
//#region src/skins/default.ts
|
|
5
|
+
var SKIN_COLORS = {
|
|
6
|
+
accent: 3900150,
|
|
7
|
+
valid: 2278750,
|
|
8
|
+
invalid: 15680580
|
|
9
|
+
};
|
|
10
|
+
/** The ghost of an item: its image, translucent, and an outline that says whether it can be placed here. */
|
|
11
|
+
function ghostLayer(look) {
|
|
12
|
+
return (context) => {
|
|
13
|
+
const holder = new Container();
|
|
14
|
+
const sprite = new Sprite(Texture.EMPTY);
|
|
15
|
+
const outline = new Graphics();
|
|
16
|
+
holder.addChild(sprite, outline);
|
|
17
|
+
holder.visible = false;
|
|
18
|
+
context.layer.addChild(holder);
|
|
19
|
+
let ghost = null;
|
|
20
|
+
let elapsed = 0;
|
|
21
|
+
const draw = () => {
|
|
22
|
+
holder.visible = ghost !== null && ghost.bounds !== void 0;
|
|
23
|
+
if (!ghost?.bounds) return;
|
|
24
|
+
const color = ghost.valid ? look.valid : look.invalid;
|
|
25
|
+
const { x, y, width, height } = ghost.bounds;
|
|
26
|
+
const descriptor = context.assets.resolve(ghost.kind);
|
|
27
|
+
const texture = descriptor ? context.textures.get(descriptor.src, descriptor.frame) : void 0;
|
|
28
|
+
if (descriptor && !texture) context.textures.ready(descriptor.src, descriptor.frame).then(context.requestUpdate, () => void 0);
|
|
29
|
+
sprite.tint = ghost.valid ? 16777215 : look.invalid;
|
|
30
|
+
sprite.alpha = look.pulse ? Math.max(.15, look.alpha + Math.sin(elapsed / 180) * .2) : look.alpha;
|
|
31
|
+
sprite.visible = texture !== void 0;
|
|
32
|
+
if (texture) {
|
|
33
|
+
sprite.texture = texture;
|
|
34
|
+
sprite.position.set(x, y);
|
|
35
|
+
sprite.width = width;
|
|
36
|
+
sprite.height = height;
|
|
37
|
+
}
|
|
38
|
+
const frames = [{
|
|
39
|
+
rect: ghost.bounds,
|
|
40
|
+
color,
|
|
41
|
+
width: 1,
|
|
42
|
+
alpha: .7
|
|
43
|
+
}];
|
|
44
|
+
if (ghost.hitbox) frames.push({
|
|
45
|
+
rect: ghost.hitbox,
|
|
46
|
+
color,
|
|
47
|
+
fill: .25,
|
|
48
|
+
width: 1
|
|
49
|
+
});
|
|
50
|
+
drawFrames(outline, frames);
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
update(snapshot) {
|
|
54
|
+
ghost = snapshot.ghost;
|
|
55
|
+
draw();
|
|
56
|
+
},
|
|
57
|
+
tick(deltaMs) {
|
|
58
|
+
elapsed += deltaMs;
|
|
59
|
+
if (look.pulse && ghost) draw();
|
|
60
|
+
},
|
|
61
|
+
destroy: () => holder.destroy({ children: true })
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** The selection, hover, drag and marquee layers, in a given look. */
|
|
66
|
+
function framesSkin(look, ghost) {
|
|
67
|
+
const framed = (rect, color, width) => look.outline === "glow" ? [{
|
|
68
|
+
rect,
|
|
69
|
+
color,
|
|
70
|
+
width: width + 4,
|
|
71
|
+
alpha: .22
|
|
72
|
+
}, {
|
|
73
|
+
rect,
|
|
74
|
+
color,
|
|
75
|
+
width,
|
|
76
|
+
alpha: 1
|
|
77
|
+
}] : [{
|
|
78
|
+
rect,
|
|
79
|
+
color,
|
|
80
|
+
width,
|
|
81
|
+
alpha: 1
|
|
82
|
+
}];
|
|
83
|
+
return {
|
|
84
|
+
selection: frameLayer((snapshot, context) => snapshot.selection.flatMap((target) => {
|
|
85
|
+
const node = nodeOf(context.nodes(), target);
|
|
86
|
+
return node ? framed(node.bounds, look.color, 2) : [];
|
|
87
|
+
})),
|
|
88
|
+
hover: frameLayer((snapshot, context) => {
|
|
89
|
+
const hovered = snapshot.hovered;
|
|
90
|
+
if (!hovered || snapshot.selection.some((target) => sameTarget(target, hovered))) return [];
|
|
91
|
+
const node = nodeOf(context.nodes(), hovered);
|
|
92
|
+
return node ? [{
|
|
93
|
+
rect: node.bounds,
|
|
94
|
+
color: look.color,
|
|
95
|
+
width: 1,
|
|
96
|
+
alpha: .6
|
|
97
|
+
}] : [];
|
|
98
|
+
}),
|
|
99
|
+
drag: frameLayer((snapshot, context) => {
|
|
100
|
+
const drag = snapshot.drag;
|
|
101
|
+
if (!drag) return [];
|
|
102
|
+
const color = drag.valid ? SKIN_COLORS.valid : SKIN_COLORS.invalid;
|
|
103
|
+
return drag.targets.flatMap((target) => {
|
|
104
|
+
const node = nodeOf(context.nodes(), target);
|
|
105
|
+
if (!node) return [];
|
|
106
|
+
return [{
|
|
107
|
+
rect: drag.live ? node.bounds : {
|
|
108
|
+
...node.bounds,
|
|
109
|
+
x: node.bounds.x + drag.delta.x,
|
|
110
|
+
y: node.bounds.y + drag.delta.y
|
|
111
|
+
},
|
|
112
|
+
color,
|
|
113
|
+
width: 1,
|
|
114
|
+
alpha: .9,
|
|
115
|
+
fill: .15
|
|
116
|
+
}];
|
|
117
|
+
});
|
|
118
|
+
}),
|
|
119
|
+
marquee: frameLayer((snapshot) => snapshot.marquee ? [{
|
|
120
|
+
rect: snapshot.marquee,
|
|
121
|
+
color: look.color,
|
|
122
|
+
width: 1,
|
|
123
|
+
alpha: .9,
|
|
124
|
+
fill: .1
|
|
125
|
+
}] : []),
|
|
126
|
+
ghost
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** A sober skin: plain frames, a translucent ghost. Enough for a tool, meant to be replaced. */
|
|
130
|
+
function defaultSkin() {
|
|
131
|
+
return framesSkin({
|
|
132
|
+
color: SKIN_COLORS.accent,
|
|
133
|
+
outline: "line"
|
|
134
|
+
}, ghostLayer({
|
|
135
|
+
valid: SKIN_COLORS.valid,
|
|
136
|
+
invalid: SKIN_COLORS.invalid,
|
|
137
|
+
alpha: .65,
|
|
138
|
+
pulse: false
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
export { SKIN_COLORS, defaultSkin, framesSkin, ghostLayer };
|
|
143
|
+
|
|
144
|
+
//# sourceMappingURL=default.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default.js","names":[],"sources":["../../src/skins/default.ts"],"sourcesContent":["import { Container, Graphics, Sprite, Texture } from \"pixi.js\";\nimport { sameTarget } from \"../interaction/picking\";\nimport type { GhostState } from \"../interaction/controller\";\nimport { drawFrames, frameLayer, nodeOf, type DrawnFrame } from \"./frames\";\nimport type { Skin, SkinContext, SkinLayer, SkinLayerFactory } from \"./types\";\n\nexport const SKIN_COLORS = { accent: 0x3b82f6, valid: 0x22c55e, invalid: 0xef4444 } as const;\n\nexport interface GhostLookOptions {\n readonly valid: number;\n readonly invalid: number;\n /** Opacity of the sprite of the item. */\n readonly alpha: number;\n /** Makes the ghost pulse: opacity swings around `alpha`. */\n readonly pulse: boolean;\n}\n\n/** The ghost of an item: its image, translucent, and an outline that says whether it can be placed here. */\nexport function ghostLayer(look: GhostLookOptions): SkinLayerFactory {\n return (context: SkinContext): SkinLayer => {\n const holder = new Container();\n const sprite = new Sprite(Texture.EMPTY);\n const outline = new Graphics();\n holder.addChild(sprite, outline);\n holder.visible = false;\n context.layer.addChild(holder);\n\n let ghost: GhostState | null = null;\n let elapsed = 0;\n\n const draw = (): void => {\n holder.visible = ghost !== null && ghost.bounds !== undefined;\n if (!ghost?.bounds) return;\n const color = ghost.valid ? look.valid : look.invalid;\n const { x, y, width, height } = ghost.bounds;\n\n const descriptor = context.assets.resolve(ghost.kind);\n const texture = descriptor ? context.textures.get(descriptor.src, descriptor.frame) : undefined;\n if (descriptor && !texture) void context.textures.ready(descriptor.src, descriptor.frame).then(context.requestUpdate, () => undefined);\n // Opacity and tint do not depend on the image: they are right from the first frame.\n sprite.tint = ghost.valid ? 0xffffff : look.invalid;\n sprite.alpha = look.pulse ? Math.max(0.15, look.alpha + Math.sin(elapsed / 180) * 0.2) : look.alpha;\n sprite.visible = texture !== undefined;\n if (texture) {\n sprite.texture = texture;\n sprite.position.set(x, y);\n sprite.width = width;\n sprite.height = height;\n }\n\n const frames: DrawnFrame[] = [{ rect: ghost.bounds, color, width: 1, alpha: 0.7 }];\n if (ghost.hitbox) frames.push({ rect: ghost.hitbox, color, fill: 0.25, width: 1 });\n drawFrames(outline, frames);\n };\n\n return {\n update(snapshot) {\n ghost = snapshot.ghost;\n draw();\n },\n tick(deltaMs) {\n elapsed += deltaMs;\n if (look.pulse && ghost) draw();\n },\n destroy: () => holder.destroy({ children: true }),\n };\n };\n}\n\nexport interface FrameLookOptions {\n /** Colour of the frame around what is selected. */\n readonly color: number;\n /** `\"glow\"` adds a soft halo around the line. */\n readonly outline: \"line\" | \"glow\";\n}\n\n/** The selection, hover, drag and marquee layers, in a given look. */\nexport function framesSkin(look: FrameLookOptions, ghost: SkinLayerFactory): Skin {\n const framed = (rect: DrawnFrame[\"rect\"], color: number, width: number): DrawnFrame[] =>\n look.outline === \"glow\"\n ? [{ rect, color, width: width + 4, alpha: 0.22 }, { rect, color, width, alpha: 1 }]\n : [{ rect, color, width, alpha: 1 }];\n\n return {\n selection: frameLayer((snapshot, context) =>\n snapshot.selection.flatMap((target) => {\n const node = nodeOf(context.nodes(), target);\n return node ? framed(node.bounds, look.color, 2) : [];\n }),\n ),\n hover: frameLayer((snapshot, context) => {\n const hovered = snapshot.hovered;\n if (!hovered || snapshot.selection.some((target) => sameTarget(target, hovered))) return [];\n const node = nodeOf(context.nodes(), hovered);\n return node ? [{ rect: node.bounds, color: look.color, width: 1, alpha: 0.6 }] : [];\n }),\n drag: frameLayer((snapshot, context) => {\n const drag = snapshot.drag;\n if (!drag) return [];\n const color = drag.valid ? SKIN_COLORS.valid : SKIN_COLORS.invalid;\n return drag.targets.flatMap((target) => {\n const node = nodeOf(context.nodes(), target);\n if (!node) return [];\n // In live mode the element has already moved: the frame is just where it is.\n const rect = drag.live ? node.bounds : { ...node.bounds, x: node.bounds.x + drag.delta.x, y: node.bounds.y + drag.delta.y };\n return [{ rect, color, width: 1, alpha: 0.9, fill: 0.15 }];\n });\n }),\n marquee: frameLayer((snapshot) =>\n snapshot.marquee ? [{ rect: snapshot.marquee, color: look.color, width: 1, alpha: 0.9, fill: 0.1 }] : [],\n ),\n ghost,\n };\n}\n\n/** A sober skin: plain frames, a translucent ghost. Enough for a tool, meant to be replaced. */\nexport function defaultSkin(): Skin {\n return framesSkin(\n { color: SKIN_COLORS.accent, outline: \"line\" },\n ghostLayer({ valid: SKIN_COLORS.valid, invalid: SKIN_COLORS.invalid, alpha: 0.65, pulse: false }),\n );\n}\n\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Graphics } from 'pixi.js';
|
|
2
|
+
import { InteractionSnapshot } from '../interaction/controller';
|
|
3
|
+
import { TargetRef } from '../interaction/picking';
|
|
4
|
+
import { Rect } from '../renderer/geometry';
|
|
5
|
+
import { SceneNode } from '../renderer/scene';
|
|
6
|
+
import { SkinContext, SkinLayerFactory } from './types';
|
|
7
|
+
/** A rectangle to draw: filled, stroked, or both. */
|
|
8
|
+
export interface DrawnFrame {
|
|
9
|
+
readonly rect: Rect;
|
|
10
|
+
readonly color: number;
|
|
11
|
+
readonly width?: number;
|
|
12
|
+
readonly alpha?: number;
|
|
13
|
+
readonly fill?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function drawFrames(graphics: Graphics, frames: readonly DrawnFrame[]): void;
|
|
16
|
+
export declare const nodeOf: (nodes: readonly SceneNode[], target: TargetRef) => SceneNode | undefined;
|
|
17
|
+
/** A layer that draws rectangles computed from the state. The base of the default and game skins. */
|
|
18
|
+
export declare function frameLayer(frames: (snapshot: InteractionSnapshot, context: SkinContext, elapsedMs: number) => readonly DrawnFrame[]): SkinLayerFactory;
|
|
19
|
+
//# sourceMappingURL=frames.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frames.d.ts","sourceRoot":"","sources":["../../src/skins/frames.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAa,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAExE,qDAAqD;AACrD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,CASlF;AAED,eAAO,MAAM,MAAM,GAAI,OAAO,SAAS,SAAS,EAAE,EAAE,QAAQ,SAAS,KAAG,SAAS,GAAG,SAGnF,CAAC;AAEF,qGAAqG;AACrG,wBAAgB,UAAU,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,mBAAmB,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,KAAK,SAAS,UAAU,EAAE,GAAG,gBAAgB,CAqBtJ"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { targetKey } from "../interaction/picking.js";
|
|
2
|
+
import { Graphics } from "pixi.js";
|
|
3
|
+
//#region src/skins/frames.ts
|
|
4
|
+
function drawFrames(graphics, frames) {
|
|
5
|
+
graphics.clear();
|
|
6
|
+
for (const frame of frames) {
|
|
7
|
+
const { x, y, width, height } = frame.rect;
|
|
8
|
+
if (frame.fill !== void 0) graphics.rect(x, y, width, height).fill({
|
|
9
|
+
color: frame.color,
|
|
10
|
+
alpha: frame.fill
|
|
11
|
+
});
|
|
12
|
+
if (frame.width !== void 0 && frame.width > 0) graphics.rect(x, y, width, height).stroke({
|
|
13
|
+
width: frame.width,
|
|
14
|
+
color: frame.color,
|
|
15
|
+
alpha: frame.alpha ?? 1
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
var nodeOf = (nodes, target) => {
|
|
20
|
+
const key = targetKey(target);
|
|
21
|
+
return nodes.find((node) => node.key === key);
|
|
22
|
+
};
|
|
23
|
+
/** A layer that draws rectangles computed from the state. The base of the default and game skins. */
|
|
24
|
+
function frameLayer(frames) {
|
|
25
|
+
return (context) => {
|
|
26
|
+
const graphics = new Graphics();
|
|
27
|
+
context.layer.addChild(graphics);
|
|
28
|
+
let last;
|
|
29
|
+
let elapsed = 0;
|
|
30
|
+
const draw = () => {
|
|
31
|
+
if (last) drawFrames(graphics, frames(last, context, elapsed));
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
update(snapshot) {
|
|
35
|
+
last = snapshot;
|
|
36
|
+
draw();
|
|
37
|
+
},
|
|
38
|
+
tick(deltaMs) {
|
|
39
|
+
elapsed += deltaMs;
|
|
40
|
+
if (last) draw();
|
|
41
|
+
},
|
|
42
|
+
destroy: () => graphics.destroy()
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { drawFrames, frameLayer, nodeOf };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=frames.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frames.js","names":[],"sources":["../../src/skins/frames.ts"],"sourcesContent":["import { Graphics } from \"pixi.js\";\nimport type { InteractionSnapshot } from \"../interaction/controller\";\nimport { targetKey, type TargetRef } from \"../interaction/picking\";\nimport type { Rect } from \"../renderer/geometry\";\nimport type { SceneNode } from \"../renderer/scene\";\nimport type { SkinContext, SkinLayer, SkinLayerFactory } from \"./types\";\n\n/** A rectangle to draw: filled, stroked, or both. */\nexport interface DrawnFrame {\n readonly rect: Rect;\n readonly color: number;\n readonly width?: number;\n readonly alpha?: number;\n readonly fill?: number;\n}\n\nexport function drawFrames(graphics: Graphics, frames: readonly DrawnFrame[]): void {\n graphics.clear();\n for (const frame of frames) {\n const { x, y, width, height } = frame.rect;\n if (frame.fill !== undefined) graphics.rect(x, y, width, height).fill({ color: frame.color, alpha: frame.fill });\n if (frame.width !== undefined && frame.width > 0) {\n graphics.rect(x, y, width, height).stroke({ width: frame.width, color: frame.color, alpha: frame.alpha ?? 1 });\n }\n }\n}\n\nexport const nodeOf = (nodes: readonly SceneNode[], target: TargetRef): SceneNode | undefined => {\n const key = targetKey(target);\n return nodes.find((node) => node.key === key);\n};\n\n/** A layer that draws rectangles computed from the state. The base of the default and game skins. */\nexport function frameLayer(frames: (snapshot: InteractionSnapshot, context: SkinContext, elapsedMs: number) => readonly DrawnFrame[]): SkinLayerFactory {\n return (context): SkinLayer => {\n const graphics = new Graphics();\n context.layer.addChild(graphics);\n let last: InteractionSnapshot | undefined;\n let elapsed = 0;\n const draw = (): void => {\n if (last) drawFrames(graphics, frames(last, context, elapsed));\n };\n return {\n update(snapshot) {\n last = snapshot;\n draw();\n },\n tick(deltaMs) {\n elapsed += deltaMs;\n if (last) draw();\n },\n destroy: () => graphics.destroy(),\n };\n };\n}\n"],"mappings":";;;AAgBA,SAAgB,WAAW,UAAoB,QAAqC;CAClF,SAAS,MAAM;CACf,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,EAAE,GAAG,GAAG,OAAO,WAAW,MAAM;EACtC,IAAI,MAAM,SAAS,KAAA,GAAW,SAAS,KAAK,GAAG,GAAG,OAAO,MAAM,CAAC,CAAC,KAAK;GAAE,OAAO,MAAM;GAAO,OAAO,MAAM;EAAK,CAAC;EAC/G,IAAI,MAAM,UAAU,KAAA,KAAa,MAAM,QAAQ,GAC7C,SAAS,KAAK,GAAG,GAAG,OAAO,MAAM,CAAC,CAAC,OAAO;GAAE,OAAO,MAAM;GAAO,OAAO,MAAM;GAAO,OAAO,MAAM,SAAS;EAAE,CAAC;CAEjH;AACF;AAEA,IAAa,UAAU,OAA6B,WAA6C;CAC/F,MAAM,MAAM,UAAU,MAAM;CAC5B,OAAO,MAAM,MAAM,SAAS,KAAK,QAAQ,GAAG;AAC9C;;AAGA,SAAgB,WAAW,QAA6H;CACtJ,QAAQ,YAAuB;EAC7B,MAAM,WAAW,IAAI,SAAS;EAC9B,QAAQ,MAAM,SAAS,QAAQ;EAC/B,IAAI;EACJ,IAAI,UAAU;EACd,MAAM,aAAmB;GACvB,IAAI,MAAM,WAAW,UAAU,OAAO,MAAM,SAAS,OAAO,CAAC;EAC/D;EACA,OAAO;GACL,OAAO,UAAU;IACf,OAAO;IACP,KAAK;GACP;GACA,KAAK,SAAS;IACZ,WAAW;IACX,IAAI,MAAM,KAAK;GACjB;GACA,eAAe,SAAS,QAAQ;EAClC;CACF;AACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Skin } from './types';
|
|
2
|
+
export interface GameSkinOptions {
|
|
3
|
+
/** Colours of the ghost of the item being placed. */
|
|
4
|
+
readonly ghost?: {
|
|
5
|
+
readonly valid?: number;
|
|
6
|
+
readonly invalid?: number;
|
|
7
|
+
readonly alpha?: number;
|
|
8
|
+
};
|
|
9
|
+
/** `"glow"` (default) gives selections a soft halo, `"line"` a plain frame. */
|
|
10
|
+
readonly outline?: "line" | "glow";
|
|
11
|
+
/** Colour of selections and hovers. */
|
|
12
|
+
readonly color?: number;
|
|
13
|
+
/** Makes the ghost pulse. */
|
|
14
|
+
readonly pulse?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A skin for players rather than editors: a glowing outline instead of a
|
|
18
|
+
* technical frame, and a ghost that can pulse and take the colours of the game.
|
|
19
|
+
*/
|
|
20
|
+
export declare function gameSkin(options?: GameSkinOptions): Skin;
|
|
21
|
+
//# sourceMappingURL=game.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game.d.ts","sourceRoot":"","sources":["../../src/skins/game.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjG,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,uCAAuC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,6BAA6B;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,IAAI,CAU5D"}
|