@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.
Files changed (287) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +341 -2
  3. package/dist/authority/authority.d.ts +118 -0
  4. package/dist/authority/authority.d.ts.map +1 -0
  5. package/dist/authority/authority.js +289 -0
  6. package/dist/authority/authority.js.map +1 -0
  7. package/dist/authority/index.d.ts +3 -0
  8. package/dist/authority/index.d.ts.map +1 -0
  9. package/dist/authority/index.js +3 -0
  10. package/dist/authority/store.d.ts +36 -0
  11. package/dist/authority/store.d.ts.map +1 -0
  12. package/dist/authority/store.js +35 -0
  13. package/dist/authority/store.js.map +1 -0
  14. package/dist/client/index.d.ts +72 -0
  15. package/dist/client/index.d.ts.map +1 -0
  16. package/dist/client/index.js +200 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/embed/client.d.ts +85 -0
  19. package/dist/embed/client.d.ts.map +1 -0
  20. package/dist/embed/client.js +255 -0
  21. package/dist/embed/client.js.map +1 -0
  22. package/dist/embed/host.d.ts +112 -0
  23. package/dist/embed/host.d.ts.map +1 -0
  24. package/dist/embed/host.js +278 -0
  25. package/dist/embed/host.js.map +1 -0
  26. package/dist/embed/index.d.ts +4 -0
  27. package/dist/embed/index.d.ts.map +1 -0
  28. package/dist/embed/index.js +4 -0
  29. package/dist/embed/protocol.d.ts +137 -0
  30. package/dist/embed/protocol.d.ts.map +1 -0
  31. package/dist/embed/protocol.js +161 -0
  32. package/dist/embed/protocol.js.map +1 -0
  33. package/dist/features/index.d.ts +66 -0
  34. package/dist/features/index.d.ts.map +1 -0
  35. package/dist/features/index.js +157 -0
  36. package/dist/features/index.js.map +1 -0
  37. package/dist/features/skin.d.ts +9 -0
  38. package/dist/features/skin.d.ts.map +1 -0
  39. package/dist/features/skin.js +43 -0
  40. package/dist/features/skin.js.map +1 -0
  41. package/dist/features/terrain.d.ts +66 -0
  42. package/dist/features/terrain.d.ts.map +1 -0
  43. package/dist/features/terrain.js +291 -0
  44. package/dist/features/terrain.js.map +1 -0
  45. package/dist/index.d.ts +9 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +51 -0
  48. package/dist/interaction/builtin-intents.d.ts +9 -0
  49. package/dist/interaction/builtin-intents.d.ts.map +1 -0
  50. package/dist/interaction/builtin-intents.js +61 -0
  51. package/dist/interaction/builtin-intents.js.map +1 -0
  52. package/dist/interaction/catalog.d.ts +24 -0
  53. package/dist/interaction/catalog.d.ts.map +1 -0
  54. package/dist/interaction/catalog.js +20 -0
  55. package/dist/interaction/catalog.js.map +1 -0
  56. package/dist/interaction/controller.d.ts +137 -0
  57. package/dist/interaction/controller.d.ts.map +1 -0
  58. package/dist/interaction/controller.js +215 -0
  59. package/dist/interaction/controller.js.map +1 -0
  60. package/dist/interaction/index.d.ts +14 -0
  61. package/dist/interaction/index.d.ts.map +1 -0
  62. package/dist/interaction/index.js +13 -0
  63. package/dist/interaction/input.d.ts +26 -0
  64. package/dist/interaction/input.d.ts.map +1 -0
  65. package/dist/interaction/input.js +33 -0
  66. package/dist/interaction/input.js.map +1 -0
  67. package/dist/interaction/intents.d.ts +80 -0
  68. package/dist/interaction/intents.d.ts.map +1 -0
  69. package/dist/interaction/keyboard.d.ts +26 -0
  70. package/dist/interaction/keyboard.d.ts.map +1 -0
  71. package/dist/interaction/keyboard.js +32 -0
  72. package/dist/interaction/keyboard.js.map +1 -0
  73. package/dist/interaction/picking.d.ts +33 -0
  74. package/dist/interaction/picking.d.ts.map +1 -0
  75. package/dist/interaction/picking.js +35 -0
  76. package/dist/interaction/picking.js.map +1 -0
  77. package/dist/interaction/pipeline.d.ts +45 -0
  78. package/dist/interaction/pipeline.d.ts.map +1 -0
  79. package/dist/interaction/pipeline.js +26 -0
  80. package/dist/interaction/pipeline.js.map +1 -0
  81. package/dist/interaction/placement.d.ts +76 -0
  82. package/dist/interaction/placement.d.ts.map +1 -0
  83. package/dist/interaction/placement.js +238 -0
  84. package/dist/interaction/placement.js.map +1 -0
  85. package/dist/interaction/selection-tool.d.ts +29 -0
  86. package/dist/interaction/selection-tool.d.ts.map +1 -0
  87. package/dist/interaction/selection-tool.js +190 -0
  88. package/dist/interaction/selection-tool.js.map +1 -0
  89. package/dist/interaction/steps.d.ts +62 -0
  90. package/dist/interaction/steps.d.ts.map +1 -0
  91. package/dist/interaction/steps.js +126 -0
  92. package/dist/interaction/steps.js.map +1 -0
  93. package/dist/interaction/store.d.ts +10 -0
  94. package/dist/interaction/store.d.ts.map +1 -0
  95. package/dist/interaction/store.js +28 -0
  96. package/dist/interaction/store.js.map +1 -0
  97. package/dist/interaction/tools.d.ts +26 -0
  98. package/dist/interaction/tools.d.ts.map +1 -0
  99. package/dist/interaction/tools.js +41 -0
  100. package/dist/interaction/tools.js.map +1 -0
  101. package/dist/model/assets.d.ts +36 -0
  102. package/dist/model/assets.d.ts.map +1 -0
  103. package/dist/model/assets.js +18 -0
  104. package/dist/model/assets.js.map +1 -0
  105. package/dist/model/committer.d.ts +66 -0
  106. package/dist/model/committer.d.ts.map +1 -0
  107. package/dist/model/committer.js +38 -0
  108. package/dist/model/committer.js.map +1 -0
  109. package/dist/model/core-patches.d.ts +2 -0
  110. package/dist/model/core-patches.d.ts.map +1 -0
  111. package/dist/model/core-patches.js +193 -0
  112. package/dist/model/core-patches.js.map +1 -0
  113. package/dist/model/define-map.d.ts +52 -0
  114. package/dist/model/define-map.d.ts.map +1 -0
  115. package/dist/model/define-map.js +66 -0
  116. package/dist/model/define-map.js.map +1 -0
  117. package/dist/model/definition.d.ts +57 -0
  118. package/dist/model/definition.d.ts.map +1 -0
  119. package/dist/model/definition.js +6 -0
  120. package/dist/model/definition.js.map +1 -0
  121. package/dist/model/errors.d.ts +21 -0
  122. package/dist/model/errors.d.ts.map +1 -0
  123. package/dist/model/errors.js +14 -0
  124. package/dist/model/errors.js.map +1 -0
  125. package/dist/model/history.d.ts +43 -0
  126. package/dist/model/history.d.ts.map +1 -0
  127. package/dist/model/history.js +141 -0
  128. package/dist/model/history.js.map +1 -0
  129. package/dist/model/index.d.ts +14 -0
  130. package/dist/model/index.d.ts.map +1 -0
  131. package/dist/model/index.js +14 -0
  132. package/dist/model/json.d.ts +15 -0
  133. package/dist/model/json.d.ts.map +1 -0
  134. package/dist/model/json.js +35 -0
  135. package/dist/model/json.js.map +1 -0
  136. package/dist/model/model.d.ts +68 -0
  137. package/dist/model/model.d.ts.map +1 -0
  138. package/dist/model/model.js +170 -0
  139. package/dist/model/model.js.map +1 -0
  140. package/dist/model/parse.d.ts +24 -0
  141. package/dist/model/parse.d.ts.map +1 -0
  142. package/dist/model/parse.js +165 -0
  143. package/dist/model/parse.js.map +1 -0
  144. package/dist/model/patches.d.ts +100 -0
  145. package/dist/model/patches.d.ts.map +1 -0
  146. package/dist/model/patches.js +21 -0
  147. package/dist/model/patches.js.map +1 -0
  148. package/dist/model/result.d.ts +22 -0
  149. package/dist/model/result.d.ts.map +1 -0
  150. package/dist/model/result.js +10 -0
  151. package/dist/model/result.js.map +1 -0
  152. package/dist/model/source.d.ts +52 -0
  153. package/dist/model/source.d.ts.map +1 -0
  154. package/dist/model/source.js +75 -0
  155. package/dist/model/source.js.map +1 -0
  156. package/dist/protocol/index.d.ts +92 -0
  157. package/dist/protocol/index.d.ts.map +1 -0
  158. package/dist/protocol/index.js +106 -0
  159. package/dist/protocol/index.js.map +1 -0
  160. package/dist/remote/committer.d.ts +43 -0
  161. package/dist/remote/committer.d.ts.map +1 -0
  162. package/dist/remote/committer.js +206 -0
  163. package/dist/remote/committer.js.map +1 -0
  164. package/dist/remote/index.d.ts +4 -0
  165. package/dist/remote/index.d.ts.map +1 -0
  166. package/dist/remote/index.js +4 -0
  167. package/dist/remote/join.d.ts +30 -0
  168. package/dist/remote/join.d.ts.map +1 -0
  169. package/dist/remote/join.js +36 -0
  170. package/dist/remote/join.js.map +1 -0
  171. package/dist/remote/memory-transport.d.ts +20 -0
  172. package/dist/remote/memory-transport.d.ts.map +1 -0
  173. package/dist/remote/memory-transport.js +48 -0
  174. package/dist/remote/memory-transport.js.map +1 -0
  175. package/dist/renderer/effects.d.ts +52 -0
  176. package/dist/renderer/effects.d.ts.map +1 -0
  177. package/dist/renderer/effects.js +111 -0
  178. package/dist/renderer/effects.js.map +1 -0
  179. package/dist/renderer/feature.d.ts +79 -0
  180. package/dist/renderer/feature.d.ts.map +1 -0
  181. package/dist/renderer/feature.js +50 -0
  182. package/dist/renderer/feature.js.map +1 -0
  183. package/dist/renderer/geometry.d.ts +59 -0
  184. package/dist/renderer/geometry.d.ts.map +1 -0
  185. package/dist/renderer/geometry.js +114 -0
  186. package/dist/renderer/geometry.js.map +1 -0
  187. package/dist/renderer/index.d.ts +7 -0
  188. package/dist/renderer/index.d.ts.map +1 -0
  189. package/dist/renderer/index.js +7 -0
  190. package/dist/renderer/scene.d.ts +77 -0
  191. package/dist/renderer/scene.d.ts.map +1 -0
  192. package/dist/renderer/scene.js +125 -0
  193. package/dist/renderer/scene.js.map +1 -0
  194. package/dist/renderer/textures.d.ts +20 -0
  195. package/dist/renderer/textures.d.ts.map +1 -0
  196. package/dist/renderer/textures.js +61 -0
  197. package/dist/renderer/textures.js.map +1 -0
  198. package/dist/renderer/view.d.ts +139 -0
  199. package/dist/renderer/view.d.ts.map +1 -0
  200. package/dist/renderer/view.js +349 -0
  201. package/dist/renderer/view.js.map +1 -0
  202. package/dist/rpgjs/shared.d.ts +41 -0
  203. package/dist/rpgjs/shared.d.ts.map +1 -0
  204. package/dist/rpgjs/shared.js +77 -0
  205. package/dist/rpgjs/shared.js.map +1 -0
  206. package/dist/server/index.d.ts +64 -0
  207. package/dist/server/index.d.ts.map +1 -0
  208. package/dist/server/index.js +205 -0
  209. package/dist/server/index.js.map +1 -0
  210. package/dist/server/storage.d.ts +16 -0
  211. package/dist/server/storage.d.ts.map +1 -0
  212. package/dist/server/storage.js +52 -0
  213. package/dist/server/storage.js.map +1 -0
  214. package/dist/skins/default.d.ts +27 -0
  215. package/dist/skins/default.d.ts.map +1 -0
  216. package/dist/skins/default.js +144 -0
  217. package/dist/skins/default.js.map +1 -0
  218. package/dist/skins/frames.d.ts +19 -0
  219. package/dist/skins/frames.d.ts.map +1 -0
  220. package/dist/skins/frames.js +49 -0
  221. package/dist/skins/frames.js.map +1 -0
  222. package/dist/skins/game.d.ts +21 -0
  223. package/dist/skins/game.d.ts.map +1 -0
  224. package/dist/skins/game.js +21 -0
  225. package/dist/skins/game.js.map +1 -0
  226. package/dist/skins/index.d.ts +5 -0
  227. package/dist/skins/index.d.ts.map +1 -0
  228. package/dist/skins/index.js +5 -0
  229. package/dist/skins/types.d.ts +46 -0
  230. package/dist/skins/types.d.ts.map +1 -0
  231. package/dist/skins/types.js +12 -0
  232. package/dist/skins/types.js.map +1 -0
  233. package/dist/terrain/data.d.ts +10 -0
  234. package/dist/terrain/data.d.ts.map +1 -0
  235. package/dist/terrain/data.js +83 -0
  236. package/dist/terrain/data.js.map +1 -0
  237. package/dist/terrain/define.d.ts +72 -0
  238. package/dist/terrain/define.d.ts.map +1 -0
  239. package/dist/terrain/define.js +91 -0
  240. package/dist/terrain/define.js.map +1 -0
  241. package/dist/terrain/editing.d.ts +54 -0
  242. package/dist/terrain/editing.d.ts.map +1 -0
  243. package/dist/terrain/editing.js +169 -0
  244. package/dist/terrain/editing.js.map +1 -0
  245. package/dist/terrain/index.d.ts +10 -0
  246. package/dist/terrain/index.d.ts.map +1 -0
  247. package/dist/terrain/index.js +10 -0
  248. package/dist/terrain/kernel.d.ts +57 -0
  249. package/dist/terrain/kernel.d.ts.map +1 -0
  250. package/dist/terrain/kernel.js +198 -0
  251. package/dist/terrain/kernel.js.map +1 -0
  252. package/dist/terrain/mask.d.ts +37 -0
  253. package/dist/terrain/mask.d.ts.map +1 -0
  254. package/dist/terrain/mask.js +244 -0
  255. package/dist/terrain/mask.js.map +1 -0
  256. package/dist/terrain/patches.d.ts +32 -0
  257. package/dist/terrain/patches.d.ts.map +1 -0
  258. package/dist/terrain/patches.js +135 -0
  259. package/dist/terrain/patches.js.map +1 -0
  260. package/dist/terrain/rle.d.ts +4 -0
  261. package/dist/terrain/rle.d.ts.map +1 -0
  262. package/dist/terrain/rle.js +68 -0
  263. package/dist/terrain/rle.js.map +1 -0
  264. package/dist/terrain/scheduler.d.ts +46 -0
  265. package/dist/terrain/scheduler.d.ts.map +1 -0
  266. package/dist/terrain/scheduler.js +148 -0
  267. package/dist/terrain/scheduler.js.map +1 -0
  268. package/dist/terrain/types.d.ts +110 -0
  269. package/dist/terrain/types.d.ts.map +1 -0
  270. package/dist/terrain/types.js +11 -0
  271. package/dist/terrain/types.js.map +1 -0
  272. package/dist/terrain-render/backend.d.ts +66 -0
  273. package/dist/terrain-render/backend.d.ts.map +1 -0
  274. package/dist/terrain-render/backend.js +96 -0
  275. package/dist/terrain-render/backend.js.map +1 -0
  276. package/dist/terrain-render/host.d.ts +21 -0
  277. package/dist/terrain-render/host.d.ts.map +1 -0
  278. package/dist/terrain-render/host.js +62 -0
  279. package/dist/terrain-render/host.js.map +1 -0
  280. package/dist/terrain-render/index.d.ts +3 -0
  281. package/dist/terrain-render/index.d.ts.map +1 -0
  282. package/dist/terrain-render/index.js +3 -0
  283. package/dist/terrain-render/worker.d.ts +2 -0
  284. package/dist/terrain-render/worker.d.ts.map +1 -0
  285. package/dist/terrain-render/worker.js +34 -0
  286. package/dist/terrain-render/worker.js.map +1 -0
  287. package/package.json +124 -3
@@ -0,0 +1,190 @@
1
+ import { nodesIn, sameTarget, targetOf } from "./picking.js";
2
+ //#region src/interaction/selection-tool.ts
3
+ var rectOf = (a, b) => ({
4
+ x: Math.min(a.x, b.x),
5
+ y: Math.min(a.y, b.y),
6
+ width: Math.abs(a.x - b.x),
7
+ height: Math.abs(a.y - b.y)
8
+ });
9
+ var withTarget = (selection, target) => selection.some((other) => sameTarget(other, target)) ? [...selection] : [...selection, target];
10
+ /**
11
+ * Hover, click and rubber-band selection, dragging and deleting. Headless: it
12
+ * only reads and writes the interaction state and submits intents.
13
+ */
14
+ function selectionTool(options = {}) {
15
+ const types = options.types ?? ["element", "event"];
16
+ const pickOptions = {
17
+ area: options.area ?? "bounds",
18
+ types
19
+ };
20
+ const dragOptions = options.drag === false ? null : options.drag ?? {};
21
+ const threshold = dragOptions?.threshold ?? 3;
22
+ const live = dragOptions?.live ?? false;
23
+ const marquee = options.marquee ?? true;
24
+ let pending = null;
25
+ let band = null;
26
+ let dragCount = 0;
27
+ let dragKey = "";
28
+ const moves = (drag, dx, dy) => drag.targets.flatMap((target) => {
29
+ const start = drag.starts.get(`${target.type}:${target.id}`);
30
+ return target.type === "element" && start ? [{
31
+ type: "element.move",
32
+ id: target.id,
33
+ x: start.x + dx,
34
+ y: start.y + dy
35
+ }] : [];
36
+ });
37
+ const endDrag = (controller) => {
38
+ pending = null;
39
+ controller.state.update({ drag: null });
40
+ };
41
+ const abort = (controller) => {
42
+ if (pending?.started && pending.applied) controller.model.history?.undo();
43
+ if (pending) endDrag(controller);
44
+ if (band) {
45
+ band = null;
46
+ controller.state.update({ marquee: null });
47
+ }
48
+ };
49
+ return {
50
+ id: "selection",
51
+ modes: options.modes ?? ["edit"],
52
+ ...options.priority !== void 0 ? { priority: options.priority } : {},
53
+ ...options.when ? { when: options.when } : {},
54
+ pointerDown(input, controller) {
55
+ if (input.button !== 0) return false;
56
+ const node = controller.pick(input, pickOptions);
57
+ const selection = controller.state.get().selection;
58
+ if (!node) {
59
+ if (!input.modifiers.shift) controller.state.update({ selection: [] });
60
+ if (!marquee) return false;
61
+ band = {
62
+ origin: input,
63
+ base: input.modifiers.shift ? selection : []
64
+ };
65
+ return true;
66
+ }
67
+ const target = targetOf(node);
68
+ const selected = selection.some((other) => sameTarget(other, target));
69
+ if (input.modifiers.shift) {
70
+ controller.state.update({ selection: selected ? selection.filter((other) => !sameTarget(other, target)) : [...selection, target] });
71
+ return true;
72
+ }
73
+ const next = selected ? selection : [target];
74
+ controller.state.update({ selection: next });
75
+ if (dragOptions) {
76
+ const state = controller.model.state;
77
+ const starts = /* @__PURE__ */ new Map();
78
+ for (const item of next) {
79
+ const element = item.type === "element" ? state.elements.find((entry) => entry.id === item.id) : void 0;
80
+ if (element) starts.set(`${item.type}:${item.id}`, {
81
+ x: element.x,
82
+ y: element.y
83
+ });
84
+ }
85
+ pending = {
86
+ origin: input,
87
+ targets: next,
88
+ starts,
89
+ ...selected && next.length > 1 ? { collapseTo: target } : {},
90
+ started: false,
91
+ applied: false
92
+ };
93
+ }
94
+ return true;
95
+ },
96
+ pointerMove(input, controller) {
97
+ if (band) {
98
+ const rect = rectOf(band.origin, input);
99
+ const selection = nodesIn(controller.nodes(), rect, types).map(targetOf).reduce((all, target) => withTarget(all, target), [...band.base]);
100
+ controller.state.update({
101
+ marquee: rect,
102
+ selection
103
+ });
104
+ return true;
105
+ }
106
+ if (pending) {
107
+ const dx = input.x - pending.origin.x;
108
+ const dy = input.y - pending.origin.y;
109
+ if (!pending.started && Math.hypot(dx, dy) < threshold) return true;
110
+ if (!pending.started) {
111
+ pending.started = true;
112
+ dragCount += 1;
113
+ dragKey = `drag:${dragCount}`;
114
+ }
115
+ const intents = moves(pending, dx, dy);
116
+ if (intents.length === 0) return true;
117
+ const refused = intents.map((intent) => controller.preview(intent, { ignore: pending.targets })).find((result) => !result.ok);
118
+ const reason = refused && !refused.ok ? refused.reason : void 0;
119
+ controller.state.update({ drag: {
120
+ targets: pending.targets,
121
+ origin: pending.origin,
122
+ delta: {
123
+ x: dx,
124
+ y: dy
125
+ },
126
+ valid: !refused,
127
+ ...reason !== void 0 ? { reason } : {},
128
+ live
129
+ } });
130
+ if (live && !refused) {
131
+ const result = controller.submit(intents, { coalesce: dragKey });
132
+ if (!("then" in result) && result.ok) pending.applied = true;
133
+ }
134
+ return true;
135
+ }
136
+ const node = controller.pick(input, pickOptions);
137
+ controller.state.update({ hovered: node ? targetOf(node) : null });
138
+ return false;
139
+ },
140
+ pointerUp(input, controller) {
141
+ if (band) {
142
+ band = null;
143
+ controller.state.update({ marquee: null });
144
+ return;
145
+ }
146
+ if (!pending) return;
147
+ const drag = pending;
148
+ if (!drag.started) {
149
+ if (drag.collapseTo) controller.state.update({ selection: [drag.collapseTo] });
150
+ pending = null;
151
+ return;
152
+ }
153
+ if (!live) {
154
+ const state = controller.state.get().drag;
155
+ if (state?.valid) controller.submit(moves(drag, state.delta.x, state.delta.y));
156
+ }
157
+ endDrag(controller);
158
+ },
159
+ key(input, controller) {
160
+ if (input.key === "Escape") {
161
+ if (pending?.started || band) {
162
+ abort(controller);
163
+ return true;
164
+ }
165
+ if (controller.state.get().selection.length > 0) {
166
+ controller.state.update({ selection: [] });
167
+ return true;
168
+ }
169
+ return false;
170
+ }
171
+ if (options.remove && (input.key === "Delete" || input.key === "Backspace")) {
172
+ const ids = controller.state.get().selection.filter((target) => target.type === "element");
173
+ if (ids.length === 0) return false;
174
+ controller.submit(ids.map((target) => ({
175
+ type: "element.remove",
176
+ id: target.id
177
+ })));
178
+ return true;
179
+ }
180
+ return false;
181
+ },
182
+ cancel(controller) {
183
+ abort(controller);
184
+ }
185
+ };
186
+ }
187
+ //#endregion
188
+ export { selectionTool };
189
+
190
+ //# sourceMappingURL=selection-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection-tool.js","names":[],"sources":["../../src/interaction/selection-tool.ts"],"sourcesContent":["import type { Rect } from \"../renderer/geometry\";\nimport type { InteractionController, Tool } from \"./controller\";\nimport type { KeyInput, PointerInput } from \"./input\";\nimport type { MapIntent } from \"./intents\";\nimport { nodesIn, sameTarget, targetOf, type PickOptions, type Point, type TargetRef } from \"./picking\";\n\nexport interface SelectionToolOptions {\n readonly modes?: readonly string[];\n /** What can be selected. Defaults to elements and events. */\n readonly types?: readonly TargetRef[\"type\"][];\n readonly area?: PickOptions[\"area\"];\n /**\n * Dragging selected elements. `false` turns it off. With `live`, the model\n * follows the pointer (one undo step); without, it changes on release.\n */\n readonly drag?: false | { readonly live?: boolean; readonly threshold?: number };\n /** Select with a rubber band on empty space. Defaults to true. */\n readonly marquee?: boolean;\n /** `Delete` and `Backspace` remove the selected elements. Defaults to false. */\n readonly remove?: boolean;\n readonly when?: (input: PointerInput | KeyInput) => boolean;\n readonly priority?: number;\n}\n\ninterface Pending {\n readonly origin: Point;\n readonly targets: readonly TargetRef[];\n readonly starts: ReadonlyMap<string, Point>;\n /** Clicking a member of a multiple selection, without a drag, narrows the selection to it. */\n readonly collapseTo?: TargetRef;\n started: boolean;\n applied: boolean;\n}\n\nconst rectOf = (a: Point, b: Point): Rect => ({\n x: Math.min(a.x, b.x),\n y: Math.min(a.y, b.y),\n width: Math.abs(a.x - b.x),\n height: Math.abs(a.y - b.y),\n});\n\nconst withTarget = (selection: readonly TargetRef[], target: TargetRef): TargetRef[] =>\n selection.some((other) => sameTarget(other, target)) ? [...selection] : [...selection, target];\n\n/**\n * Hover, click and rubber-band selection, dragging and deleting. Headless: it\n * only reads and writes the interaction state and submits intents.\n */\nexport function selectionTool(options: SelectionToolOptions = {}): Tool {\n const types = options.types ?? ([\"element\", \"event\"] as const);\n const pickOptions: PickOptions = { area: options.area ?? \"bounds\", types };\n const dragOptions = options.drag === false ? null : (options.drag ?? {});\n const threshold = dragOptions?.threshold ?? 3;\n const live = dragOptions?.live ?? false;\n const marquee = options.marquee ?? true;\n\n let pending: Pending | null = null;\n let band: { origin: Point; base: readonly TargetRef[] } | null = null;\n let dragCount = 0;\n let dragKey = \"\";\n\n const moves = (drag: Pending, dx: number, dy: number): MapIntent[] =>\n drag.targets.flatMap((target): MapIntent[] => {\n const start = drag.starts.get(`${target.type}:${target.id}`);\n return target.type === \"element\" && start ? [{ type: \"element.move\", id: target.id, x: start.x + dx, y: start.y + dy }] : [];\n });\n\n const endDrag = (controller: InteractionController): void => {\n pending = null;\n controller.state.update({ drag: null });\n };\n\n const abort = (controller: InteractionController): void => {\n if (pending?.started && pending.applied) controller.model.history?.undo();\n if (pending) endDrag(controller);\n if (band) {\n band = null;\n controller.state.update({ marquee: null });\n }\n };\n\n return {\n id: \"selection\",\n modes: options.modes ?? [\"edit\"],\n ...(options.priority !== undefined ? { priority: options.priority } : {}),\n ...(options.when ? { when: options.when } : {}),\n\n pointerDown(input: PointerInput, controller) {\n if (input.button !== 0) return false;\n const node = controller.pick(input, pickOptions);\n const selection = controller.state.get().selection;\n\n if (!node) {\n if (!input.modifiers.shift) controller.state.update({ selection: [] });\n if (!marquee) return false;\n band = { origin: input, base: input.modifiers.shift ? selection : [] };\n return true;\n }\n\n const target = targetOf(node);\n const selected = selection.some((other) => sameTarget(other, target));\n if (input.modifiers.shift) {\n controller.state.update({ selection: selected ? selection.filter((other) => !sameTarget(other, target)) : [...selection, target] });\n return true;\n }\n\n const next = selected ? selection : [target];\n controller.state.update({ selection: next });\n if (dragOptions) {\n const state = controller.model.state;\n const starts = new Map<string, Point>();\n for (const item of next) {\n const element = item.type === \"element\" ? state.elements.find((entry) => entry.id === item.id) : undefined;\n if (element) starts.set(`${item.type}:${item.id}`, { x: element.x, y: element.y });\n }\n pending = {\n origin: input,\n targets: next,\n starts,\n ...(selected && next.length > 1 ? { collapseTo: target } : {}),\n started: false,\n applied: false,\n };\n }\n return true;\n },\n\n pointerMove(input: PointerInput, controller) {\n if (band) {\n const rect = rectOf(band.origin, input);\n const inside = nodesIn(controller.nodes(), rect, types).map(targetOf);\n const selection = inside.reduce<TargetRef[]>((all, target) => withTarget(all, target), [...band.base]);\n controller.state.update({ marquee: rect, selection });\n return true;\n }\n\n if (pending) {\n const dx = input.x - pending.origin.x;\n const dy = input.y - pending.origin.y;\n if (!pending.started && Math.hypot(dx, dy) < threshold) return true;\n if (!pending.started) {\n pending.started = true;\n dragCount += 1;\n dragKey = `drag:${dragCount}`;\n }\n\n const intents = moves(pending, dx, dy);\n if (intents.length === 0) return true;\n const refused = intents\n .map((intent) => controller.preview(intent as never, { ignore: pending!.targets }))\n .find((result) => !result.ok);\n const reason = refused && !refused.ok ? refused.reason : undefined;\n controller.state.update({\n drag: {\n targets: pending.targets,\n origin: pending.origin,\n delta: { x: dx, y: dy },\n valid: !refused,\n ...(reason !== undefined ? { reason } : {}),\n live,\n },\n });\n if (live && !refused) {\n const result = controller.submit(intents, { coalesce: dragKey });\n if (!(\"then\" in (result as object)) && (result as { ok: boolean }).ok) pending.applied = true;\n }\n return true;\n }\n\n const node = controller.pick(input, pickOptions);\n controller.state.update({ hovered: node ? targetOf(node) : null });\n return false;\n },\n\n pointerUp(input: PointerInput, controller) {\n if (band) {\n band = null;\n controller.state.update({ marquee: null });\n return;\n }\n if (!pending) return;\n const drag = pending;\n if (!drag.started) {\n if (drag.collapseTo) controller.state.update({ selection: [drag.collapseTo] });\n pending = null;\n return;\n }\n if (!live) {\n const state = controller.state.get().drag;\n if (state?.valid) controller.submit(moves(drag, state.delta.x, state.delta.y));\n }\n endDrag(controller);\n },\n\n key(input: KeyInput, controller) {\n if (input.key === \"Escape\") {\n if (pending?.started || band) {\n abort(controller);\n return true;\n }\n if (controller.state.get().selection.length > 0) {\n controller.state.update({ selection: [] });\n return true;\n }\n return false;\n }\n if (options.remove && (input.key === \"Delete\" || input.key === \"Backspace\")) {\n const ids = controller.state.get().selection.filter((target) => target.type === \"element\");\n if (ids.length === 0) return false;\n controller.submit(ids.map((target): MapIntent => ({ type: \"element.remove\", id: target.id })));\n return true;\n }\n return false;\n },\n\n cancel(controller) {\n abort(controller);\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import { Rect } from '../renderer/geometry';
2
+ import { MapElement } from '../model/definition';
3
+ import { IntentType, PositionedIntentType } from './intents';
4
+ import { PipelineStep } from './pipeline';
5
+ /** Rounds the position of an intent to a grid. */
6
+ export declare function snapToGrid<K extends PositionedIntentType>(size: number, origin?: {
7
+ x?: number;
8
+ y?: number;
9
+ }): PipelineStep<K>;
10
+ /** Refuses a position that would put the thing partly outside the map (`out-of-bounds`). */
11
+ export declare function withinMap<K extends PositionedIntentType>(): PipelineStep<K>;
12
+ export interface RequireFreeOptions {
13
+ /** Whose collision boxes count as obstacles. Defaults to `"elements"`. */
14
+ readonly against?: "elements" | "all";
15
+ }
16
+ /**
17
+ * Refuses a position where the thing would overlap another one (`occupied`).
18
+ * Collision boxes are used when there are, drawn bounds otherwise.
19
+ */
20
+ export declare function requireFree<K extends PositionedIntentType>(options?: RequireFreeOptions): PipelineStep<K>;
21
+ /** At most `limit` elements of the asset `kind` on the map (`max-count`). */
22
+ export declare function maxCount<K extends PositionedIntentType>(kind: string, limit: number): PipelineStep<K>;
23
+ /** Keeps at least `distance` pixels between the thing and the elements of asset `kind` (`too-close`). */
24
+ export declare function minDistance<K extends PositionedIntentType>(kind: string, distance: number): PipelineStep<K>;
25
+ /** What the multiplayer steps know about whoever is acting. The game decides what an actor is. */
26
+ export type ActorId = (actor: unknown) => string | undefined;
27
+ export interface OwnerOnlyOptions {
28
+ /** Identifies the actor. Without an id, nothing owned can be touched. */
29
+ readonly actor: ActorId;
30
+ /** Who owns an element. Defaults to the `owner` string of its `data`. */
31
+ readonly owner?: (element: MapElement) => string | undefined;
32
+ /** Actors who may touch anything, such as game masters. */
33
+ readonly bypass?: (actor: unknown) => boolean;
34
+ }
35
+ /**
36
+ * Only the owner of an element may move or remove it (`not-owner`). Elements nobody
37
+ * owns are free for all; stamp `data.owner` when placing to make them private.
38
+ */
39
+ export declare function ownerOnly<K extends "element.move" | "element.remove">(options: OwnerOnlyOptions): PipelineStep<K>;
40
+ export interface RateLimitOptions {
41
+ /** How many intents are allowed in each window. */
42
+ readonly max: number;
43
+ readonly windowMs: number;
44
+ /** What the limit is counted per. Defaults to the actor itself. */
45
+ readonly key?: (actor: unknown) => unknown;
46
+ readonly now?: () => number;
47
+ }
48
+ /**
49
+ * At most `max` intents per `windowMs` for each actor (`rate-limit`). A preview does
50
+ * not count: only what is really submitted does.
51
+ */
52
+ export declare function rateLimit<K extends IntentType>(options: RateLimitOptions): PipelineStep<K>;
53
+ export interface ProtectedZoneOptions {
54
+ /** Actors allowed inside the zones. */
55
+ readonly except?: (actor: unknown) => boolean;
56
+ }
57
+ /**
58
+ * Nothing may be placed in, moved into, moved out of or removed from these rectangles
59
+ * (`protected-zone`): a village square, a spawn point.
60
+ */
61
+ export declare function protectedZone<K extends "element.move" | "element.remove" | "element.place">(zones: readonly Rect[], options?: ProtectedZoneOptions): PipelineStep<K>;
62
+ //# sourceMappingURL=steps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steps.d.ts","sourceRoot":"","sources":["../../src/interaction/steps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAY,UAAU,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,kDAAkD;AAClD,wBAAgB,UAAU,CAAC,CAAC,SAAS,oBAAoB,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE;IAAE,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,YAAY,CAAC,CAAC,CAAC,CAUjI;AAWD,4FAA4F;AAC5F,wBAAgB,SAAS,CAAC,CAAC,SAAS,oBAAoB,KAAK,YAAY,CAAC,CAAC,CAAC,CAS3E;AAED,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC;CACvC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,oBAAoB,EAAE,OAAO,GAAE,kBAAuB,GAAG,YAAY,CAAC,CAAC,CAAC,CAc7G;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,CAAC,SAAS,oBAAoB,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAOrG;AAED,yGAAyG;AACzG,wBAAgB,WAAW,CAAC,CAAC,SAAS,oBAAoB,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAc3G;AAED,kGAAkG;AAClG,MAAM,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,MAAM,GAAG,SAAS,CAAC;IAC7D,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;CAC/C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,cAAc,GAAG,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,GAAG,YAAY,CAAC,CAAC,CAAC,CASjH;AAED,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAC3C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EAAE,gBAAgB,GAAG,YAAY,CAAC,CAAC,CAAC,CAwB1F;AAED,MAAM,WAAW,oBAAoB;IACnC,uCAAuC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;CAC/C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,cAAc,GAAG,gBAAgB,GAAG,eAAe,EACzF,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,OAAO,GAAE,oBAAyB,GACjC,YAAY,CAAC,CAAC,CAAC,CAcjB"}
@@ -0,0 +1,126 @@
1
+ import { sameTarget } from "./picking.js";
2
+ //#region src/interaction/steps.ts
3
+ /** Rounds the position of an intent to a grid. */
4
+ function snapToGrid(size, origin = {}) {
5
+ if (!(size > 0)) throw new Error("snapToGrid needs a size greater than 0");
6
+ const ox = origin.x ?? 0;
7
+ const oy = origin.y ?? 0;
8
+ return (intent, _context, next) => next({
9
+ ...intent,
10
+ x: Math.round((intent.x - ox) / size) * size + ox,
11
+ y: Math.round((intent.y - oy) / size) * size + oy
12
+ });
13
+ }
14
+ var overlaps = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
15
+ var gap = (a, b) => {
16
+ const dx = Math.max(0, a.x - (b.x + b.width), b.x - (a.x + a.width));
17
+ const dy = Math.max(0, a.y - (b.y + b.height), b.y - (a.y + a.height));
18
+ return Math.hypot(dx, dy);
19
+ };
20
+ /** Refuses a position that would put the thing partly outside the map (`out-of-bounds`). */
21
+ function withinMap() {
22
+ return (intent, context, next) => {
23
+ const space = context.footprint(intent)?.bounds;
24
+ if (!space) return next();
25
+ const [width, height] = context.state.size;
26
+ return space.x < 0 || space.y < 0 || space.x + space.width > width || space.y + space.height > height ? context.reject("out-of-bounds", "The position is outside the map") : next();
27
+ };
28
+ }
29
+ /**
30
+ * Refuses a position where the thing would overlap another one (`occupied`).
31
+ * Collision boxes are used when there are, drawn bounds otherwise.
32
+ */
33
+ function requireFree(options = {}) {
34
+ return (intent, context, next) => {
35
+ const footprint = context.footprint(intent);
36
+ if (!footprint) return next();
37
+ const space = footprint.hitbox ?? footprint.bounds;
38
+ const ignored = (target) => (footprint.ignore ?? []).some((other) => sameTarget(other, target));
39
+ return context.nodes.some((node) => (options.against === "all" || node.type === "element") && !ignored({
40
+ type: node.type,
41
+ id: node.id
42
+ }) && overlaps(space, node.hitbox ?? node.bounds)) ? context.reject("occupied", "Something is already there") : next();
43
+ };
44
+ }
45
+ /** At most `limit` elements of the asset `kind` on the map (`max-count`). */
46
+ function maxCount(kind, limit) {
47
+ return (intent, context, next) => {
48
+ if (intent.type !== "element.place") return next();
49
+ return context.state.elements.filter((element) => element.kind === kind).length >= limit ? context.reject("max-count", `At most ${limit} of "${kind}" can be placed`) : next();
50
+ };
51
+ }
52
+ /** Keeps at least `distance` pixels between the thing and the elements of asset `kind` (`too-close`). */
53
+ function minDistance(kind, distance) {
54
+ return (intent, context, next) => {
55
+ const footprint = context.footprint(intent);
56
+ if (!footprint) return next();
57
+ const space = footprint.hitbox ?? footprint.bounds;
58
+ return context.nodes.some((node) => node.type === "element" && node.kind === kind && !(footprint.ignore ?? []).some((other) => sameTarget(other, {
59
+ type: node.type,
60
+ id: node.id
61
+ })) && gap(space, node.hitbox ?? node.bounds) < distance) ? context.reject("too-close", `Keep ${distance}px away from "${kind}"`) : next();
62
+ };
63
+ }
64
+ /**
65
+ * Only the owner of an element may move or remove it (`not-owner`). Elements nobody
66
+ * owns are free for all; stamp `data.owner` when placing to make them private.
67
+ */
68
+ function ownerOnly(options) {
69
+ const ownerOf = options.owner ?? ((element) => typeof element.data?.owner === "string" ? element.data.owner : void 0);
70
+ return (intent, context, next) => {
71
+ if (options.bypass?.(context.actor)) return next();
72
+ const element = context.state.elements.find((entry) => entry.id === intent.id);
73
+ const owner = element ? ownerOf(element) : void 0;
74
+ if (owner === void 0) return next();
75
+ return options.actor(context.actor) === owner ? next() : context.reject("not-owner", "This belongs to someone else");
76
+ };
77
+ }
78
+ /**
79
+ * At most `max` intents per `windowMs` for each actor (`rate-limit`). A preview does
80
+ * not count: only what is really submitted does.
81
+ */
82
+ function rateLimit(options) {
83
+ if (!(options.max >= 1) || !(options.windowMs > 0)) throw new Error("rateLimit needs max >= 1 and windowMs > 0");
84
+ const now = options.now ?? (() => Date.now());
85
+ const byObject = /* @__PURE__ */ new WeakMap();
86
+ const byValue = /* @__PURE__ */ new Map();
87
+ const history = (key) => {
88
+ const table = typeof key === "object" && key !== null ? byObject : byValue;
89
+ let list = table.get(key);
90
+ if (!list) {
91
+ list = [];
92
+ table.set(key, list);
93
+ }
94
+ return list;
95
+ };
96
+ return (intent, context, next) => {
97
+ if (context.preview) return next();
98
+ const times = history(options.key ? options.key(context.actor) : context.actor);
99
+ const cutoff = now() - options.windowMs;
100
+ while (times.length > 0 && times[0] <= cutoff) times.shift();
101
+ if (times.length >= options.max) return context.reject("rate-limit", "Too many actions, slow down");
102
+ times.push(now());
103
+ return next();
104
+ };
105
+ }
106
+ /**
107
+ * Nothing may be placed in, moved into, moved out of or removed from these rectangles
108
+ * (`protected-zone`): a village square, a spawn point.
109
+ */
110
+ function protectedZone(zones, options = {}) {
111
+ return (intent, context, next) => {
112
+ if (options.except?.(context.actor)) return next();
113
+ const spaces = [];
114
+ const destination = context.footprint(intent)?.bounds;
115
+ if (destination) spaces.push(destination);
116
+ if ("id" in intent && typeof intent.id === "string") {
117
+ const current = context.scene.find((node) => node.type === "element" && node.id === intent.id);
118
+ if (current) spaces.push(current.bounds);
119
+ }
120
+ return spaces.some((space) => zones.some((zone) => overlaps(space, zone))) ? context.reject("protected-zone", "This area is protected") : next();
121
+ };
122
+ }
123
+ //#endregion
124
+ export { maxCount, minDistance, ownerOnly, protectedZone, rateLimit, requireFree, snapToGrid, withinMap };
125
+
126
+ //# sourceMappingURL=steps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steps.js","names":[],"sources":["../../src/interaction/steps.ts"],"sourcesContent":["import type { Rect } from \"../renderer/geometry\";\nimport type { MapElement } from \"../model/definition\";\nimport type { IntentOf, IntentType, PositionedIntentType } from \"./intents\";\nimport type { PipelineStep } from \"./pipeline\";\nimport { sameTarget, type TargetRef } from \"./picking\";\n\n/** Rounds the position of an intent to a grid. */\nexport function snapToGrid<K extends PositionedIntentType>(size: number, origin: { x?: number; y?: number } = {}): PipelineStep<K> {\n if (!(size > 0)) throw new Error(\"snapToGrid needs a size greater than 0\");\n const ox = origin.x ?? 0;\n const oy = origin.y ?? 0;\n return (intent, _context, next) =>\n next({\n ...intent,\n x: Math.round((intent.x - ox) / size) * size + ox,\n y: Math.round((intent.y - oy) / size) * size + oy,\n } as IntentOf<K>);\n}\n\nconst overlaps = (a: Rect, b: Rect): boolean =>\n a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;\n\nconst gap = (a: Rect, b: Rect): number => {\n const dx = Math.max(0, a.x - (b.x + b.width), b.x - (a.x + a.width));\n const dy = Math.max(0, a.y - (b.y + b.height), b.y - (a.y + a.height));\n return Math.hypot(dx, dy);\n};\n\n/** Refuses a position that would put the thing partly outside the map (`out-of-bounds`). */\nexport function withinMap<K extends PositionedIntentType>(): PipelineStep<K> {\n return (intent, context, next) => {\n const space = context.footprint(intent)?.bounds;\n if (!space) return next();\n const [width, height] = context.state.size;\n return space.x < 0 || space.y < 0 || space.x + space.width > width || space.y + space.height > height\n ? context.reject(\"out-of-bounds\", \"The position is outside the map\")\n : next();\n };\n}\n\nexport interface RequireFreeOptions {\n /** Whose collision boxes count as obstacles. Defaults to `\"elements\"`. */\n readonly against?: \"elements\" | \"all\";\n}\n\n/**\n * Refuses a position where the thing would overlap another one (`occupied`).\n * Collision boxes are used when there are, drawn bounds otherwise.\n */\nexport function requireFree<K extends PositionedIntentType>(options: RequireFreeOptions = {}): PipelineStep<K> {\n return (intent, context, next) => {\n const footprint = context.footprint(intent);\n if (!footprint) return next();\n const space = footprint.hitbox ?? footprint.bounds;\n const ignored = (target: TargetRef): boolean => (footprint.ignore ?? []).some((other) => sameTarget(other, target));\n const blocked = context.nodes.some(\n (node) =>\n (options.against === \"all\" || node.type === \"element\") &&\n !ignored({ type: node.type, id: node.id }) &&\n overlaps(space, node.hitbox ?? node.bounds),\n );\n return blocked ? context.reject(\"occupied\", \"Something is already there\") : next();\n };\n}\n\n/** At most `limit` elements of the asset `kind` on the map (`max-count`). */\nexport function maxCount<K extends PositionedIntentType>(kind: string, limit: number): PipelineStep<K> {\n return (intent, context, next) => {\n // Only a placement adds one: moving an element keeps the count where it is.\n if (intent.type !== \"element.place\") return next();\n const placed = context.state.elements.filter((element) => element.kind === kind).length;\n return placed >= limit ? context.reject(\"max-count\", `At most ${limit} of \"${kind}\" can be placed`) : next();\n };\n}\n\n/** Keeps at least `distance` pixels between the thing and the elements of asset `kind` (`too-close`). */\nexport function minDistance<K extends PositionedIntentType>(kind: string, distance: number): PipelineStep<K> {\n return (intent, context, next) => {\n const footprint = context.footprint(intent);\n if (!footprint) return next();\n const space = footprint.hitbox ?? footprint.bounds;\n const tooClose = context.nodes.some(\n (node) =>\n node.type === \"element\" &&\n node.kind === kind &&\n !(footprint.ignore ?? []).some((other) => sameTarget(other, { type: node.type, id: node.id })) &&\n gap(space, node.hitbox ?? node.bounds) < distance,\n );\n return tooClose ? context.reject(\"too-close\", `Keep ${distance}px away from \"${kind}\"`) : next();\n };\n}\n\n/** What the multiplayer steps know about whoever is acting. The game decides what an actor is. */\nexport type ActorId = (actor: unknown) => string | undefined;\n\nexport interface OwnerOnlyOptions {\n /** Identifies the actor. Without an id, nothing owned can be touched. */\n readonly actor: ActorId;\n /** Who owns an element. Defaults to the `owner` string of its `data`. */\n readonly owner?: (element: MapElement) => string | undefined;\n /** Actors who may touch anything, such as game masters. */\n readonly bypass?: (actor: unknown) => boolean;\n}\n\n/**\n * Only the owner of an element may move or remove it (`not-owner`). Elements nobody\n * owns are free for all; stamp `data.owner` when placing to make them private.\n */\nexport function ownerOnly<K extends \"element.move\" | \"element.remove\">(options: OwnerOnlyOptions): PipelineStep<K> {\n const ownerOf = options.owner ?? ((element: MapElement) => (typeof element.data?.owner === \"string\" ? element.data.owner : undefined));\n return (intent, context, next) => {\n if (options.bypass?.(context.actor)) return next();\n const element = context.state.elements.find((entry) => entry.id === intent.id);\n const owner = element ? ownerOf(element) : undefined;\n if (owner === undefined) return next();\n return options.actor(context.actor) === owner ? next() : context.reject(\"not-owner\", \"This belongs to someone else\");\n };\n}\n\nexport interface RateLimitOptions {\n /** How many intents are allowed in each window. */\n readonly max: number;\n readonly windowMs: number;\n /** What the limit is counted per. Defaults to the actor itself. */\n readonly key?: (actor: unknown) => unknown;\n readonly now?: () => number;\n}\n\n/**\n * At most `max` intents per `windowMs` for each actor (`rate-limit`). A preview does\n * not count: only what is really submitted does.\n */\nexport function rateLimit<K extends IntentType>(options: RateLimitOptions): PipelineStep<K> {\n if (!(options.max >= 1) || !(options.windowMs > 0)) throw new Error(\"rateLimit needs max >= 1 and windowMs > 0\");\n const now = options.now ?? (() => Date.now());\n // Objects are held weakly, so a player who leaves does not stay in memory.\n const byObject = new WeakMap<object, number[]>();\n const byValue = new Map<unknown, number[]>();\n const history = (key: unknown): number[] => {\n const table = typeof key === \"object\" && key !== null ? byObject : byValue;\n let list = table.get(key as never);\n if (!list) {\n list = [];\n table.set(key as never, list);\n }\n return list;\n };\n return (intent, context, next) => {\n if (context.preview) return next();\n const times = history(options.key ? options.key(context.actor) : context.actor);\n const cutoff = now() - options.windowMs;\n while (times.length > 0 && times[0]! <= cutoff) times.shift();\n if (times.length >= options.max) return context.reject(\"rate-limit\", \"Too many actions, slow down\");\n times.push(now());\n return next();\n };\n}\n\nexport interface ProtectedZoneOptions {\n /** Actors allowed inside the zones. */\n readonly except?: (actor: unknown) => boolean;\n}\n\n/**\n * Nothing may be placed in, moved into, moved out of or removed from these rectangles\n * (`protected-zone`): a village square, a spawn point.\n */\nexport function protectedZone<K extends \"element.move\" | \"element.remove\" | \"element.place\">(\n zones: readonly Rect[],\n options: ProtectedZoneOptions = {},\n): PipelineStep<K> {\n return (intent, context, next) => {\n if (options.except?.(context.actor)) return next();\n const spaces: Rect[] = [];\n const destination = context.footprint(intent)?.bounds;\n if (destination) spaces.push(destination);\n if (\"id\" in intent && typeof intent.id === \"string\") {\n const current = context.scene.find((node) => node.type === \"element\" && node.id === intent.id);\n if (current) spaces.push(current.bounds);\n }\n return spaces.some((space) => zones.some((zone) => overlaps(space, zone)))\n ? context.reject(\"protected-zone\", \"This area is protected\")\n : next();\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { Unsubscribe } from '../model/result';
2
+ /** A small observable store, framework-neutral: `subscribe(cb)` plugs into signals, Vue, React… */
3
+ export interface Store<T> {
4
+ get(): T;
5
+ /** Merges `patch` into the state. Listeners run only if a value actually changed. */
6
+ update(patch: Partial<T>): void;
7
+ subscribe(listener: (state: T) => void): Unsubscribe;
8
+ }
9
+ export declare function createStore<T extends object>(initial: T, onListenerError?: (error: unknown) => void): Store<T>;
10
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/interaction/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,mGAAmG;AACnG,MAAM,WAAW,KAAK,CAAC,CAAC;IACtB,GAAG,IAAI,CAAC,CAAC;IACT,qFAAqF;IACrF,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChC,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,WAAW,CAAC;CACtD;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,eAAe,GAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAoB,GAAG,KAAK,CAAC,CAAC,CAAC,CAsB7H"}
@@ -0,0 +1,28 @@
1
+ //#region src/interaction/store.ts
2
+ function createStore(initial, onListenerError = console.error) {
3
+ let state = initial;
4
+ const listeners = /* @__PURE__ */ new Set();
5
+ return {
6
+ get: () => state,
7
+ update(patch) {
8
+ if (Object.keys(patch).every((key) => Object.is(state[key], patch[key]))) return;
9
+ state = {
10
+ ...state,
11
+ ...patch
12
+ };
13
+ for (const listener of [...listeners]) try {
14
+ listener(state);
15
+ } catch (error) {
16
+ onListenerError(error);
17
+ }
18
+ },
19
+ subscribe(listener) {
20
+ listeners.add(listener);
21
+ return () => listeners.delete(listener);
22
+ }
23
+ };
24
+ }
25
+ //#endregion
26
+ export { createStore };
27
+
28
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","names":[],"sources":["../../src/interaction/store.ts"],"sourcesContent":["import type { Unsubscribe } from \"../model/result\";\n\n/** A small observable store, framework-neutral: `subscribe(cb)` plugs into signals, Vue, React… */\nexport interface Store<T> {\n get(): T;\n /** Merges `patch` into the state. Listeners run only if a value actually changed. */\n update(patch: Partial<T>): void;\n subscribe(listener: (state: T) => void): Unsubscribe;\n}\n\nexport function createStore<T extends object>(initial: T, onListenerError: (error: unknown) => void = console.error): Store<T> {\n let state = initial;\n const listeners = new Set<(state: T) => void>();\n return {\n get: () => state,\n update(patch) {\n const keys = Object.keys(patch) as Array<keyof T>;\n if (keys.every((key) => Object.is(state[key], patch[key]))) return;\n state = { ...state, ...patch };\n for (const listener of [...listeners]) {\n try {\n listener(state);\n } catch (error) {\n onListenerError(error);\n }\n }\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n"],"mappings":";AAUA,SAAgB,YAA8B,SAAY,kBAA4C,QAAQ,OAAiB;CAC7H,IAAI,QAAQ;CACZ,MAAM,4BAAY,IAAI,IAAwB;CAC9C,OAAO;EACL,WAAW;EACX,OAAO,OAAO;GAEZ,IADa,OAAO,KAAK,KACrB,CAAA,CAAK,OAAO,QAAQ,OAAO,GAAG,MAAM,MAAM,MAAM,IAAI,CAAC,GAAG;GAC5D,QAAQ;IAAE,GAAG;IAAO,GAAG;GAAM;GAC7B,KAAK,MAAM,YAAY,CAAC,GAAG,SAAS,GAClC,IAAI;IACF,SAAS,KAAK;GAChB,SAAS,OAAO;IACd,gBAAgB,KAAK;GACvB;EAEJ;EACA,UAAU,UAAU;GAClB,UAAU,IAAI,QAAQ;GACtB,aAAa,UAAU,OAAO,QAAQ;EACxC;CACF;AACF"}
@@ -0,0 +1,26 @@
1
+ import { SceneNode } from '../renderer/scene';
2
+ import { InteractionController, Tool } from './controller';
3
+ import { KeyInput, PointerInput } from './input';
4
+ import { sameTarget, PickOptions, TargetRef } from './picking';
5
+ type Gate = (input: PointerInput | KeyInput) => boolean;
6
+ export interface PlayToolOptions {
7
+ /** Called when the player clicks something. The game decides what that means. */
8
+ onInteract(target: TargetRef, node: SceneNode, input: PointerInput, controller: InteractionController): void;
9
+ /** What can be interacted with. Defaults to events only. */
10
+ readonly types?: readonly TargetRef["type"][];
11
+ readonly area?: PickOptions["area"];
12
+ readonly modes?: readonly string[];
13
+ readonly when?: Gate;
14
+ readonly priority?: number;
15
+ }
16
+ /** Clicking in the world, as a player: the game decides what a click on something does. */
17
+ export declare function playTool(options: PlayToolOptions): Tool;
18
+ export interface HoverToolOptions {
19
+ readonly types?: readonly TargetRef["type"][];
20
+ readonly area?: PickOptions["area"];
21
+ readonly modes?: readonly string[];
22
+ }
23
+ /** Only tracks what is under the pointer. This is what the `inspect` mode is made of. */
24
+ export declare function hoverTool(options?: HoverToolOptions): Tool;
25
+ export { sameTarget };
26
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/interaction/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,qBAAqB,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,EAAE,UAAU,EAAY,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAEnF,KAAK,IAAI,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,QAAQ,KAAK,OAAO,CAAC;AAExD,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC7G,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,2FAA2F;AAC3F,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAcvD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,yFAAyF;AACzF,wBAAgB,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,IAAI,CAW9D;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { targetOf } from "./picking.js";
2
+ //#region src/interaction/tools.ts
3
+ /** Clicking in the world, as a player: the game decides what a click on something does. */
4
+ function playTool(options) {
5
+ return {
6
+ id: "play",
7
+ modes: options.modes ?? ["play"],
8
+ ...options.priority !== void 0 ? { priority: options.priority } : {},
9
+ ...options.when ? { when: options.when } : {},
10
+ pointerDown(input, controller) {
11
+ if (input.button !== 0) return false;
12
+ const node = controller.pick(input, {
13
+ area: options.area ?? "bounds",
14
+ types: options.types ?? ["event"]
15
+ });
16
+ if (!node) return false;
17
+ options.onInteract(targetOf(node), node, input, controller);
18
+ return true;
19
+ }
20
+ };
21
+ }
22
+ /** Only tracks what is under the pointer. This is what the `inspect` mode is made of. */
23
+ function hoverTool(options = {}) {
24
+ return {
25
+ id: "hover",
26
+ modes: options.modes ?? ["inspect"],
27
+ pointerMove(input, controller) {
28
+ const node = controller.pick(input, {
29
+ ...options.area ? { area: options.area } : {},
30
+ ...options.types ? { types: options.types } : {}
31
+ });
32
+ controller.state.update({ hovered: node ? targetOf(node) : null });
33
+ return false;
34
+ },
35
+ cancel: (controller) => controller.state.update({ hovered: null })
36
+ };
37
+ }
38
+ //#endregion
39
+ export { hoverTool, playTool };
40
+
41
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","names":[],"sources":["../../src/interaction/tools.ts"],"sourcesContent":["import type { SceneNode } from \"../renderer/scene\";\nimport type { InteractionController, Tool } from \"./controller\";\nimport type { KeyInput, PointerInput } from \"./input\";\nimport { sameTarget, targetOf, type PickOptions, type TargetRef } from \"./picking\";\n\ntype Gate = (input: PointerInput | KeyInput) => boolean;\n\nexport interface PlayToolOptions {\n /** Called when the player clicks something. The game decides what that means. */\n onInteract(target: TargetRef, node: SceneNode, input: PointerInput, controller: InteractionController): void;\n /** What can be interacted with. Defaults to events only. */\n readonly types?: readonly TargetRef[\"type\"][];\n readonly area?: PickOptions[\"area\"];\n readonly modes?: readonly string[];\n readonly when?: Gate;\n readonly priority?: number;\n}\n\n/** Clicking in the world, as a player: the game decides what a click on something does. */\nexport function playTool(options: PlayToolOptions): Tool {\n return {\n id: \"play\",\n modes: options.modes ?? [\"play\"],\n ...(options.priority !== undefined ? { priority: options.priority } : {}),\n ...(options.when ? { when: options.when } : {}),\n pointerDown(input, controller) {\n if (input.button !== 0) return false;\n const node = controller.pick(input, { area: options.area ?? \"bounds\", types: options.types ?? [\"event\"] });\n if (!node) return false;\n options.onInteract(targetOf(node), node, input, controller);\n return true;\n },\n };\n}\n\nexport interface HoverToolOptions {\n readonly types?: readonly TargetRef[\"type\"][];\n readonly area?: PickOptions[\"area\"];\n readonly modes?: readonly string[];\n}\n\n/** Only tracks what is under the pointer. This is what the `inspect` mode is made of. */\nexport function hoverTool(options: HoverToolOptions = {}): Tool {\n return {\n id: \"hover\",\n modes: options.modes ?? [\"inspect\"],\n pointerMove(input, controller) {\n const node = controller.pick(input, { ...(options.area ? { area: options.area } : {}), ...(options.types ? { types: options.types } : {}) });\n controller.state.update({ hovered: node ? targetOf(node) : null });\n return false;\n },\n cancel: (controller) => controller.state.update({ hovered: null }),\n };\n}\n\nexport { sameTarget };\n"],"mappings":";;;AAmBA,SAAgB,SAAS,SAAgC;CACvD,OAAO;EACL,IAAI;EACJ,OAAO,QAAQ,SAAS,CAAC,MAAM;EAC/B,GAAI,QAAQ,aAAa,KAAA,IAAY,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;EACvE,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;EAC7C,YAAY,OAAO,YAAY;GAC7B,IAAI,MAAM,WAAW,GAAG,OAAO;GAC/B,MAAM,OAAO,WAAW,KAAK,OAAO;IAAE,MAAM,QAAQ,QAAQ;IAAU,OAAO,QAAQ,SAAS,CAAC,OAAO;GAAE,CAAC;GACzG,IAAI,CAAC,MAAM,OAAO;GAClB,QAAQ,WAAW,SAAS,IAAI,GAAG,MAAM,OAAO,UAAU;GAC1D,OAAO;EACT;CACF;AACF;;AASA,SAAgB,UAAU,UAA4B,CAAC,GAAS;CAC9D,OAAO;EACL,IAAI;EACJ,OAAO,QAAQ,SAAS,CAAC,SAAS;EAClC,YAAY,OAAO,YAAY;GAC7B,MAAM,OAAO,WAAW,KAAK,OAAO;IAAE,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;IAAI,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;GAAG,CAAC;GAC3I,WAAW,MAAM,OAAO,EAAE,SAAS,OAAO,SAAS,IAAI,IAAI,KAAK,CAAC;GACjE,OAAO;EACT;EACA,SAAS,eAAe,WAAW,MAAM,OAAO,EAAE,SAAS,KAAK,CAAC;CACnE;AACF"}
@@ -0,0 +1,36 @@
1
+ /** What the renderer needs to know about an asset, independently of how it is stored. */
2
+ export interface AssetDescriptor {
3
+ /** Where the image lives. How it is fetched and decoded is up to the renderer. */
4
+ readonly src: string;
5
+ /**
6
+ * `[x, y, width, height]` of the part of the image to show, for assets cut out
7
+ * of a larger image (a tileset). Defaults to the whole image.
8
+ */
9
+ readonly frame?: readonly [x: number, y: number, width: number, height: number];
10
+ /**
11
+ * Size in pixels at which the asset is drawn. Defaults to the size of `frame`.
12
+ * Without a `frame`, it is also taken as the size of the image. An asset that
13
+ * declares neither is measured once its image has loaded.
14
+ */
15
+ readonly size?: readonly [width: number, height: number];
16
+ /**
17
+ * `[x, y, width, height]` of the collision box, in pixels of the *source* image
18
+ * (`frame`), relative to its top-left corner. It is scaled with the asset.
19
+ */
20
+ readonly hitbox?: readonly [x: number, y: number, width: number, height: number];
21
+ }
22
+ /**
23
+ * Turns an asset reference (`"tree-oak"`) into a descriptor. Studio, a local
24
+ * folder or a game each provide their own.
25
+ */
26
+ export interface AssetResolver<K extends string = string> {
27
+ /** Every reference this resolver knows. */
28
+ readonly kinds: readonly K[];
29
+ has(kind: string): kind is K;
30
+ resolve(kind: string): AssetDescriptor | undefined;
31
+ }
32
+ /** Union of the references a resolver knows. */
33
+ export type AssetKind<A> = A extends AssetResolver<infer K> ? K : never;
34
+ /** Declares assets once, so that references are checked by the compiler. */
35
+ export declare function defineAssets<const T extends Record<string, AssetDescriptor>>(assets: T): AssetResolver<Extract<keyof T, string>>;
36
+ //# sourceMappingURL=assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/model/assets.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,WAAW,eAAe;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAChF;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACzD;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;CAClF;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IACtD,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IAC7B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,CAAC,CAAC;IAC7B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;CACpD;AAED,gDAAgD;AAChD,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAExE,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAC1E,MAAM,EAAE,CAAC,GACR,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAYzC"}
@@ -0,0 +1,18 @@
1
+ //#region src/model/assets.ts
2
+ /** Declares assets once, so that references are checked by the compiler. */
3
+ function defineAssets(assets) {
4
+ const table = new Map(Object.entries(assets));
5
+ return {
6
+ kinds: [...table.keys()],
7
+ has(kind) {
8
+ return table.has(kind);
9
+ },
10
+ resolve(kind) {
11
+ return table.get(kind);
12
+ }
13
+ };
14
+ }
15
+ //#endregion
16
+ export { defineAssets };
17
+
18
+ //# sourceMappingURL=assets.js.map