@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,165 @@
1
+ import { cloneJson, isJsonObject, isJsonValue, isPlainObject } from "./json.js";
2
+ import "./definition.js";
3
+ import { MapDefinitionError } from "./errors.js";
4
+ //#region src/model/parse.ts
5
+ /** Migrations keyed by the version they upgrade *from*. Empty until version 2 exists. */
6
+ var defaultMapMigrations = {};
7
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
8
+ var isPositive = (value) => isFiniteNumber(value) && value > 0;
9
+ /** A positive number, or `[x, y]` of positive numbers. */
10
+ var isScale = (value) => isPositive(value) || Array.isArray(value) && value.length === 2 && value.every(isPositive);
11
+ var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
12
+ /** Problems found in one element or event, with paths relative to `path`. */
13
+ function checkPlaced(value, path, kind) {
14
+ if (!isPlainObject(value)) return [{
15
+ path,
16
+ message: `${kind} must be an object`
17
+ }];
18
+ const issues = [];
19
+ if (!isNonEmptyString(value.id)) issues.push({
20
+ path: `${path}.id`,
21
+ message: "must be a non-empty string"
22
+ });
23
+ if (!isNonEmptyString(value.kind)) issues.push({
24
+ path: `${path}.kind`,
25
+ message: "must be a non-empty string"
26
+ });
27
+ if (!isFiniteNumber(value.x)) issues.push({
28
+ path: `${path}.x`,
29
+ message: "must be a finite number"
30
+ });
31
+ if (!isFiniteNumber(value.y)) issues.push({
32
+ path: `${path}.y`,
33
+ message: "must be a finite number"
34
+ });
35
+ if (value.data !== void 0 && !isJsonObject(value.data)) issues.push({
36
+ path: `${path}.data`,
37
+ message: "must be a JSON object"
38
+ });
39
+ if (kind === "event" && value.graphic !== void 0 && !isNonEmptyString(value.graphic)) issues.push({
40
+ path: `${path}.graphic`,
41
+ message: "must be a non-empty string"
42
+ });
43
+ if (kind === "element" && value.scale !== void 0 && !isScale(value.scale)) issues.push({
44
+ path: `${path}.scale`,
45
+ message: "must be a positive number or [x, y] of positive numbers"
46
+ });
47
+ if (kind === "element" && value.depth !== void 0) if (!isPlainObject(value.depth)) issues.push({
48
+ path: `${path}.depth`,
49
+ message: "must be an object"
50
+ });
51
+ else for (const key of [
52
+ "z",
53
+ "anchorY",
54
+ "height"
55
+ ]) {
56
+ const field = value.depth[key];
57
+ if (field !== void 0 && !isFiniteNumber(field)) issues.push({
58
+ path: `${path}.depth.${key}`,
59
+ message: "must be a finite number"
60
+ });
61
+ }
62
+ return issues;
63
+ }
64
+ function checkList(value, path, kind, issues) {
65
+ if (!Array.isArray(value)) {
66
+ issues.push({
67
+ path,
68
+ message: "must be an array"
69
+ });
70
+ return;
71
+ }
72
+ const seen = /* @__PURE__ */ new Set();
73
+ value.forEach((item, index) => {
74
+ const itemPath = `${path}[${index}]`;
75
+ issues.push(...checkPlaced(item, itemPath, kind));
76
+ const id = isPlainObject(item) ? item.id : void 0;
77
+ if (isNonEmptyString(id)) {
78
+ if (seen.has(id)) issues.push({
79
+ path: `${itemPath}.id`,
80
+ message: `duplicate ${kind} id "${id}"`
81
+ });
82
+ seen.add(id);
83
+ }
84
+ });
85
+ }
86
+ function migrate(raw, options) {
87
+ const current = options.currentVersion ?? 1;
88
+ const migrations = options.migrations ?? defaultMapMigrations;
89
+ let version = raw.version;
90
+ if (!Number.isInteger(version) || version < 1) throw new MapDefinitionError([{
91
+ path: "version",
92
+ message: "must be a positive integer"
93
+ }]);
94
+ if (version > current) throw new MapDefinitionError([{
95
+ path: "version",
96
+ message: `version ${version} is newer than the supported version ${current}; upgrade @rpgjs/map-builder`
97
+ }]);
98
+ let out = raw;
99
+ while (version < current) {
100
+ const step = migrations[version];
101
+ if (!step) throw new MapDefinitionError([{
102
+ path: "version",
103
+ message: `no migration from version ${version} to ${version + 1}`
104
+ }]);
105
+ out = {
106
+ ...step(out),
107
+ version: version + 1
108
+ };
109
+ version = out.version;
110
+ }
111
+ return out;
112
+ }
113
+ /** Canonical form: an empty `data` object is omitted. */
114
+ function canonicalPlaced(item) {
115
+ return Object.fromEntries(Object.entries(item).filter(([key, value]) => value !== void 0 && !(key === "data" && isPlainObject(value) && Object.keys(value).length === 0)).map(([key, value]) => [key, isJsonValue(value) ? cloneJson(value) : value]));
116
+ }
117
+ /**
118
+ * Validates, migrates and copies a raw definition (for instance parsed JSON).
119
+ *
120
+ * - The result shares no structure with the input.
121
+ * - Fields unknown to this version are preserved, so an older tool never destroys
122
+ * data written by a newer one.
123
+ * - Throws {@link MapDefinitionError} listing every problem found.
124
+ */
125
+ function parseMapDefinition(raw, options = {}) {
126
+ if (!isPlainObject(raw)) throw new MapDefinitionError([{
127
+ path: "",
128
+ message: "must be an object"
129
+ }]);
130
+ const source = migrate(raw, options);
131
+ const issues = [];
132
+ if (!isNonEmptyString(source.id)) issues.push({
133
+ path: "id",
134
+ message: "must be a non-empty string"
135
+ });
136
+ const size = source.size;
137
+ if (!Array.isArray(size) || size.length !== 2 || !size.every((n) => isFiniteNumber(n) && n > 0)) issues.push({
138
+ path: "size",
139
+ message: "must be [width, height] in pixels, both positive"
140
+ });
141
+ if (source.tileSize !== void 0 && !(isFiniteNumber(source.tileSize) && source.tileSize > 0)) issues.push({
142
+ path: "tileSize",
143
+ message: "must be a positive number"
144
+ });
145
+ checkList(source.elements ?? [], "elements", "element", issues);
146
+ checkList(source.events ?? [], "events", "event", issues);
147
+ if (source.extensions !== void 0) {
148
+ if (!isPlainObject(source.extensions) || !isJsonValue(source.extensions)) issues.push({
149
+ path: "extensions",
150
+ message: "must be a JSON object keyed by feature id"
151
+ });
152
+ }
153
+ if (issues.length > 0) throw new MapDefinitionError(issues);
154
+ const { elements = [], events = [], extensions, ...rest } = source;
155
+ return Object.fromEntries([
156
+ ...Object.entries(rest).filter(([, value]) => value !== void 0).map(([key, value]) => [key, isJsonValue(value) ? cloneJson(value) : value]),
157
+ ["elements", elements.map(canonicalPlaced)],
158
+ ["events", events.map(canonicalPlaced)],
159
+ ...extensions !== void 0 ? [["extensions", cloneJson(extensions)]] : []
160
+ ]);
161
+ }
162
+ //#endregion
163
+ export { checkPlaced, defaultMapMigrations, isScale, parseMapDefinition };
164
+
165
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","names":[],"sources":["../../src/model/parse.ts"],"sourcesContent":["import { MAP_DEFINITION_VERSION, type MapDefinition } from \"./definition\";\nimport { MapDefinitionError, type MapIssue } from \"./errors\";\nimport { cloneJson, isJsonObject, isJsonValue, isPlainObject, type JsonValue } from \"./json\";\n\n/** Upgrades a raw definition from version `n` to version `n + 1`. */\nexport type MapMigration = (raw: Record<string, unknown>) => Record<string, unknown>;\n\n/** Migrations keyed by the version they upgrade *from*. Empty until version 2 exists. */\nexport const defaultMapMigrations: Readonly<Record<number, MapMigration>> = {};\n\nexport interface ParseMapOptions {\n readonly migrations?: Readonly<Record<number, MapMigration>>;\n readonly currentVersion?: number;\n}\n\nconst isFiniteNumber = (value: unknown): value is number => typeof value === \"number\" && Number.isFinite(value);\nconst isPositive = (value: unknown): value is number => isFiniteNumber(value) && value > 0;\n/** A positive number, or `[x, y]` of positive numbers. */\nexport const isScale = (value: unknown): boolean =>\n isPositive(value) || (Array.isArray(value) && value.length === 2 && value.every(isPositive));\nconst isNonEmptyString = (value: unknown): value is string => typeof value === \"string\" && value.length > 0;\n\n/** Problems found in one element or event, with paths relative to `path`. */\nexport function checkPlaced(value: unknown, path: string, kind: \"element\" | \"event\"): MapIssue[] {\n if (!isPlainObject(value)) return [{ path, message: `${kind} must be an object` }];\n const issues: MapIssue[] = [];\n if (!isNonEmptyString(value.id)) issues.push({ path: `${path}.id`, message: \"must be a non-empty string\" });\n if (!isNonEmptyString(value.kind)) issues.push({ path: `${path}.kind`, message: \"must be a non-empty string\" });\n if (!isFiniteNumber(value.x)) issues.push({ path: `${path}.x`, message: \"must be a finite number\" });\n if (!isFiniteNumber(value.y)) issues.push({ path: `${path}.y`, message: \"must be a finite number\" });\n if (value.data !== undefined && !isJsonObject(value.data)) {\n issues.push({ path: `${path}.data`, message: \"must be a JSON object\" });\n }\n if (kind === \"event\" && value.graphic !== undefined && !isNonEmptyString(value.graphic)) {\n issues.push({ path: `${path}.graphic`, message: \"must be a non-empty string\" });\n }\n if (kind === \"element\" && value.scale !== undefined && !isScale(value.scale)) {\n issues.push({ path: `${path}.scale`, message: \"must be a positive number or [x, y] of positive numbers\" });\n }\n if (kind === \"element\" && value.depth !== undefined) {\n if (!isPlainObject(value.depth)) {\n issues.push({ path: `${path}.depth`, message: \"must be an object\" });\n } else {\n for (const key of [\"z\", \"anchorY\", \"height\"] as const) {\n const field = value.depth[key];\n if (field !== undefined && !isFiniteNumber(field)) {\n issues.push({ path: `${path}.depth.${key}`, message: \"must be a finite number\" });\n }\n }\n }\n }\n return issues;\n}\n\nfunction checkList(value: unknown, path: string, kind: \"element\" | \"event\", issues: MapIssue[]): void {\n if (!Array.isArray(value)) {\n issues.push({ path, message: \"must be an array\" });\n return;\n }\n const seen = new Set<string>();\n value.forEach((item, index) => {\n const itemPath = `${path}[${index}]`;\n issues.push(...checkPlaced(item, itemPath, kind));\n const id = isPlainObject(item) ? item.id : undefined;\n if (isNonEmptyString(id)) {\n if (seen.has(id)) issues.push({ path: `${itemPath}.id`, message: `duplicate ${kind} id \"${id}\"` });\n seen.add(id);\n }\n });\n}\n\nfunction migrate(raw: Record<string, unknown>, options: ParseMapOptions): Record<string, unknown> {\n const current = options.currentVersion ?? MAP_DEFINITION_VERSION;\n const migrations = options.migrations ?? defaultMapMigrations;\n let version = raw.version;\n if (!Number.isInteger(version) || (version as number) < 1) {\n throw new MapDefinitionError([{ path: \"version\", message: \"must be a positive integer\" }]);\n }\n if ((version as number) > current) {\n throw new MapDefinitionError([\n {\n path: \"version\",\n message: `version ${version} is newer than the supported version ${current}; upgrade @rpgjs/map-builder`,\n },\n ]);\n }\n let out = raw;\n while ((version as number) < current) {\n const step = migrations[version as number];\n if (!step) {\n throw new MapDefinitionError([\n { path: \"version\", message: `no migration from version ${version} to ${(version as number) + 1}` },\n ]);\n }\n out = { ...step(out), version: (version as number) + 1 };\n version = out.version;\n }\n return out;\n}\n\n/** Canonical form: an empty `data` object is omitted. */\nfunction canonicalPlaced<T extends Record<string, unknown>>(item: T): T {\n return Object.fromEntries(\n Object.entries(item)\n .filter(([key, value]) => value !== undefined && !(key === \"data\" && isPlainObject(value) && Object.keys(value).length === 0))\n .map(([key, value]) => [key, isJsonValue(value) ? cloneJson(value) : value]),\n ) as T;\n}\n\n/**\n * Validates, migrates and copies a raw definition (for instance parsed JSON).\n *\n * - The result shares no structure with the input.\n * - Fields unknown to this version are preserved, so an older tool never destroys\n * data written by a newer one.\n * - Throws {@link MapDefinitionError} listing every problem found.\n */\nexport function parseMapDefinition(raw: unknown, options: ParseMapOptions = {}): MapDefinition {\n if (!isPlainObject(raw)) throw new MapDefinitionError([{ path: \"\", message: \"must be an object\" }]);\n const source = migrate(raw, options);\n\n const issues: MapIssue[] = [];\n if (!isNonEmptyString(source.id)) issues.push({ path: \"id\", message: \"must be a non-empty string\" });\n\n const size = source.size;\n if (!Array.isArray(size) || size.length !== 2 || !size.every((n) => isFiniteNumber(n) && n > 0)) {\n issues.push({ path: \"size\", message: \"must be [width, height] in pixels, both positive\" });\n }\n if (source.tileSize !== undefined && !(isFiniteNumber(source.tileSize) && source.tileSize > 0)) {\n issues.push({ path: \"tileSize\", message: \"must be a positive number\" });\n }\n checkList(source.elements ?? [], \"elements\", \"element\", issues);\n checkList(source.events ?? [], \"events\", \"event\", issues);\n if (source.extensions !== undefined) {\n if (!isPlainObject(source.extensions) || !isJsonValue(source.extensions)) {\n issues.push({ path: \"extensions\", message: \"must be a JSON object keyed by feature id\" });\n }\n }\n if (issues.length > 0) throw new MapDefinitionError(issues);\n\n const { elements = [], events = [], extensions, ...rest } = source as Record<string, unknown> & {\n elements?: Record<string, unknown>[];\n events?: Record<string, unknown>[];\n extensions?: Record<string, JsonValue>;\n };\n const definition = Object.fromEntries([\n ...Object.entries(rest)\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) => [key, isJsonValue(value) ? cloneJson(value) : value]),\n [\"elements\", elements.map(canonicalPlaced)],\n [\"events\", events.map(canonicalPlaced)],\n ...(extensions !== undefined ? [[\"extensions\", cloneJson(extensions)]] : []),\n ]);\n return definition as unknown as MapDefinition;\n}\n"],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ import { MapDepth, MapElement, MapEvent, MapScale, MapState } from './definition';
2
+ import { MapRejection, RejectionCode } from './errors';
3
+ import { JsonObject, JsonValue } from './json';
4
+ /**
5
+ * Every patch type the model understands, mapped to its payload.
6
+ *
7
+ * The registry is open: a feature adds its own patch types by merging into this
8
+ * interface, and gets them checked everywhere `MapPatch` is used.
9
+ *
10
+ * ```ts
11
+ * declare module "@rpgjs/map-builder/model" {
12
+ * interface PatchRegistry {
13
+ * "tilling.till": { x: number; y: number };
14
+ * }
15
+ * }
16
+ * ```
17
+ *
18
+ * Payloads must be JSON-serializable: patches are sent over the network, stored
19
+ * and replayed.
20
+ */
21
+ export interface PatchRegistry {
22
+ "element.add": {
23
+ readonly element: MapElement;
24
+ readonly index?: number;
25
+ };
26
+ "element.remove": {
27
+ readonly id: string;
28
+ };
29
+ "element.move": {
30
+ readonly id: string;
31
+ readonly x: number;
32
+ readonly y: number;
33
+ };
34
+ /** `kind` replaces the asset reference. `depth: null` and `scale: null` clear them. */
35
+ "element.update": {
36
+ readonly id: string;
37
+ readonly set: {
38
+ readonly kind?: string;
39
+ readonly depth?: MapDepth | null;
40
+ readonly scale?: MapScale | null;
41
+ };
42
+ };
43
+ /** Merges `set` into the element data, then removes the `unset` keys. */
44
+ "element.data": {
45
+ readonly id: string;
46
+ readonly set?: JsonObject;
47
+ readonly unset?: readonly string[];
48
+ };
49
+ "event.add": {
50
+ readonly event: MapEvent;
51
+ readonly index?: number;
52
+ };
53
+ "event.remove": {
54
+ readonly id: string;
55
+ };
56
+ "event.move": {
57
+ readonly id: string;
58
+ readonly x: number;
59
+ readonly y: number;
60
+ };
61
+ /** Sets the data a feature owns. An absent `value` removes it. */
62
+ "extension.set": {
63
+ readonly id: string;
64
+ readonly value?: JsonValue;
65
+ };
66
+ }
67
+ export type PatchType = keyof PatchRegistry & string;
68
+ export type MapPatchOf<K extends PatchType> = {
69
+ readonly type: K;
70
+ /**
71
+ * Revision the patch was computed against. The authority rejects (or rebases)
72
+ * a patch built on a stale revision.
73
+ */
74
+ readonly baseRevision?: number;
75
+ } & PatchRegistry[K];
76
+ /** Discriminated union on `type`, derived from the registry. */
77
+ export type MapPatch = {
78
+ [K in PatchType]: MapPatchOf<K>;
79
+ }[PatchType];
80
+ export type PatchOutcome = {
81
+ readonly ok: true;
82
+ readonly state: MapState;
83
+ /** Patches that undo this one, in the order they must be applied. */
84
+ readonly inverse: readonly MapPatch[];
85
+ } | {
86
+ readonly ok: false;
87
+ readonly rejection: MapRejection;
88
+ };
89
+ /** How a patch type changes the state, and how it is undone. Pure: never mutate `state`. */
90
+ export interface PatchHandler<K extends PatchType> {
91
+ apply(state: MapState, patch: MapPatchOf<K>): PatchOutcome;
92
+ }
93
+ export type PatchHandlers = {
94
+ readonly [K in PatchType]?: PatchHandler<K>;
95
+ };
96
+ export declare const patchOk: (state: MapState, inverse: readonly MapPatch[]) => PatchOutcome;
97
+ export declare const patchFail: (code: RejectionCode, message: string) => PatchOutcome;
98
+ /** Declares handlers with their types checked against the registry. */
99
+ export declare function definePatchHandlers(handlers: PatchHandlers): PatchHandlers;
100
+ //# sourceMappingURL=patches.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patches.d.ts","sourceRoot":"","sources":["../../src/model/patches.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ //#region src/model/patches.ts
2
+ var patchOk = (state, inverse) => ({
3
+ ok: true,
4
+ state,
5
+ inverse
6
+ });
7
+ var patchFail = (code, message) => ({
8
+ ok: false,
9
+ rejection: {
10
+ code,
11
+ message
12
+ }
13
+ });
14
+ /** Declares handlers with their types checked against the registry. */
15
+ function definePatchHandlers(handlers) {
16
+ return handlers;
17
+ }
18
+ //#endregion
19
+ export { definePatchHandlers, patchFail, patchOk };
20
+
21
+ //# sourceMappingURL=patches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patches.js","names":[],"sources":["../../src/model/patches.ts"],"sourcesContent":["import type { MapDepth, MapElement, MapEvent, MapScale, MapState } from \"./definition\";\nimport type { MapRejection, RejectionCode } from \"./errors\";\nimport type { JsonObject, JsonValue } from \"./json\";\n\n/**\n * Every patch type the model understands, mapped to its payload.\n *\n * The registry is open: a feature adds its own patch types by merging into this\n * interface, and gets them checked everywhere `MapPatch` is used.\n *\n * ```ts\n * declare module \"@rpgjs/map-builder/model\" {\n * interface PatchRegistry {\n * \"tilling.till\": { x: number; y: number };\n * }\n * }\n * ```\n *\n * Payloads must be JSON-serializable: patches are sent over the network, stored\n * and replayed.\n */\nexport interface PatchRegistry {\n \"element.add\": { readonly element: MapElement; readonly index?: number };\n \"element.remove\": { readonly id: string };\n \"element.move\": { readonly id: string; readonly x: number; readonly y: number };\n /** `kind` replaces the asset reference. `depth: null` and `scale: null` clear them. */\n \"element.update\": {\n readonly id: string;\n readonly set: {\n readonly kind?: string;\n readonly depth?: MapDepth | null;\n readonly scale?: MapScale | null;\n };\n };\n /** Merges `set` into the element data, then removes the `unset` keys. */\n \"element.data\": { readonly id: string; readonly set?: JsonObject; readonly unset?: readonly string[] };\n \"event.add\": { readonly event: MapEvent; readonly index?: number };\n \"event.remove\": { readonly id: string };\n \"event.move\": { readonly id: string; readonly x: number; readonly y: number };\n /** Sets the data a feature owns. An absent `value` removes it. */\n \"extension.set\": { readonly id: string; readonly value?: JsonValue };\n}\n\nexport type PatchType = keyof PatchRegistry & string;\n\nexport type MapPatchOf<K extends PatchType> = {\n readonly type: K;\n /**\n * Revision the patch was computed against. The authority rejects (or rebases)\n * a patch built on a stale revision.\n */\n readonly baseRevision?: number;\n} & PatchRegistry[K];\n\n/** Discriminated union on `type`, derived from the registry. */\nexport type MapPatch = { [K in PatchType]: MapPatchOf<K> }[PatchType];\n\nexport type PatchOutcome =\n | {\n readonly ok: true;\n readonly state: MapState;\n /** Patches that undo this one, in the order they must be applied. */\n readonly inverse: readonly MapPatch[];\n }\n | { readonly ok: false; readonly rejection: MapRejection };\n\n/** How a patch type changes the state, and how it is undone. Pure: never mutate `state`. */\nexport interface PatchHandler<K extends PatchType> {\n apply(state: MapState, patch: MapPatchOf<K>): PatchOutcome;\n}\n\nexport type PatchHandlers = { readonly [K in PatchType]?: PatchHandler<K> };\n\nexport const patchOk = (state: MapState, inverse: readonly MapPatch[]): PatchOutcome => ({ ok: true, state, inverse });\n\nexport const patchFail = (code: RejectionCode, message: string): PatchOutcome => ({\n ok: false,\n rejection: { code, message },\n});\n\n/** Declares handlers with their types checked against the registry. */\nexport function definePatchHandlers(handlers: PatchHandlers): PatchHandlers {\n return handlers;\n}\n"],"mappings":";AAyEA,IAAa,WAAW,OAAiB,aAAgD;CAAE,IAAI;CAAM;CAAO;AAAQ;AAEpH,IAAa,aAAa,MAAqB,aAAmC;CAChF,IAAI;CACJ,WAAW;EAAE;EAAM;CAAQ;AAC7B;;AAGA,SAAgB,oBAAoB,UAAwC;CAC1E,OAAO;AACT"}
@@ -0,0 +1,22 @@
1
+ import { MapRejection } from './errors';
2
+ import { MapPatch } from './patches';
3
+ export type MaybePromise<T> = T | Promise<T>;
4
+ export type Unsubscribe = () => void;
5
+ export interface ApplySuccess {
6
+ readonly ok: true;
7
+ /** Revision of the map after the change. A batch gets a single revision. */
8
+ readonly revision: number;
9
+ /** Patches that were applied. */
10
+ readonly patches: readonly MapPatch[];
11
+ /** Patches that undo the change, in the order they must be applied. */
12
+ readonly inverse: readonly MapPatch[];
13
+ }
14
+ export interface ApplyFailure {
15
+ readonly ok: false;
16
+ readonly rejection: MapRejection;
17
+ }
18
+ export type ApplyResult = ApplySuccess | ApplyFailure;
19
+ export declare const isThenable: <T>(value: MaybePromise<T>) => value is Promise<T>;
20
+ /** Runs `next` on a value that may or may not be a promise, keeping sync values sync. */
21
+ export declare function chain<T, U>(value: MaybePromise<T>, next: (value: T) => U): MaybePromise<U>;
22
+ //# sourceMappingURL=result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/model/result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAC7C,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;IACtC,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;CAClC;AAED,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;AAEtD,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,OAAO,YAAY,CAAC,CAAC,CAAC,KAAG,KAAK,IAAI,OAAO,CAAC,CAAC,CACgC,CAAC;AAE1G,yFAAyF;AACzF,wBAAgB,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAE1F"}
@@ -0,0 +1,10 @@
1
+ //#region src/model/result.ts
2
+ var isThenable = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
3
+ /** Runs `next` on a value that may or may not be a promise, keeping sync values sync. */
4
+ function chain(value, next) {
5
+ return isThenable(value) ? value.then(next) : next(value);
6
+ }
7
+ //#endregion
8
+ export { chain, isThenable };
9
+
10
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.js","names":[],"sources":["../../src/model/result.ts"],"sourcesContent":["import type { MapRejection } from \"./errors\";\nimport type { MapPatch } from \"./patches\";\n\nexport type MaybePromise<T> = T | Promise<T>;\nexport type Unsubscribe = () => void;\n\nexport interface ApplySuccess {\n readonly ok: true;\n /** Revision of the map after the change. A batch gets a single revision. */\n readonly revision: number;\n /** Patches that were applied. */\n readonly patches: readonly MapPatch[];\n /** Patches that undo the change, in the order they must be applied. */\n readonly inverse: readonly MapPatch[];\n}\n\nexport interface ApplyFailure {\n readonly ok: false;\n readonly rejection: MapRejection;\n}\n\nexport type ApplyResult = ApplySuccess | ApplyFailure;\n\nexport const isThenable = <T>(value: MaybePromise<T>): value is Promise<T> =>\n typeof value === \"object\" && value !== null && typeof (value as { then?: unknown }).then === \"function\";\n\n/** Runs `next` on a value that may or may not be a promise, keeping sync values sync. */\nexport function chain<T, U>(value: MaybePromise<T>, next: (value: T) => U): MaybePromise<U> {\n return isThenable(value) ? value.then(next) : next(value);\n}\n"],"mappings":";AAuBA,IAAa,cAAiB,UAC5B,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAQ,MAA6B,SAAS;;AAG/F,SAAgB,MAAY,OAAwB,MAAwC;CAC1F,OAAO,WAAW,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,KAAK;AAC1D"}
@@ -0,0 +1,52 @@
1
+ import { MapDefinition, MapElement, MapEvent } from './definition';
2
+ export interface MapRect {
3
+ readonly x: number;
4
+ readonly y: number;
5
+ readonly width: number;
6
+ readonly height: number;
7
+ }
8
+ /** The part of a map that falls inside a rectangle, for streaming large maps. */
9
+ export interface MapRegion {
10
+ readonly id: string;
11
+ readonly rect: MapRect;
12
+ readonly elements: readonly MapElement[];
13
+ readonly events: readonly MapEvent[];
14
+ }
15
+ /** Where a map comes from: code, a JSON file, Tiled, a database, Studio… */
16
+ export interface MapSource {
17
+ load(id: string): Promise<MapDefinition>;
18
+ /** Natively loads one region. When absent, {@link loadMapRegion} derives it from `load`. */
19
+ loadRegion?(id: string, rect: MapRect): Promise<MapRegion>;
20
+ list?(): Promise<readonly string[]>;
21
+ }
22
+ export type MapSourceErrorCode = "not-found" | "load-failed";
23
+ export declare class MapSourceError extends Error {
24
+ readonly code: MapSourceErrorCode;
25
+ readonly mapId: string;
26
+ constructor(code: MapSourceErrorCode, mapId: string, message: string);
27
+ }
28
+ /**
29
+ * Elements and events whose origin falls in `rect`, grown by `margin` pixels so
30
+ * that objects larger than their origin are not cut off at the border.
31
+ */
32
+ export declare function regionOf(definition: MapDefinition, rect: MapRect, margin?: number): MapRegion;
33
+ export declare function loadMapRegion(source: MapSource, id: string, rect: MapRect, margin?: number): Promise<MapRegion>;
34
+ /** Maps written in code (`defineMap`). Every `load` returns a fresh copy. */
35
+ export declare function codeMapSource(maps: readonly MapDefinition[]): MapSource;
36
+ export type JsonMapSourceOptions = {
37
+ readonly maps: Readonly<Record<string, unknown>>;
38
+ } | {
39
+ readonly read: (id: string) => unknown | Promise<unknown>;
40
+ };
41
+ /** Maps stored as JSON (files, a database). Content is validated and migrated on load. */
42
+ export declare function jsonMapSource(options: JsonMapSourceOptions): MapSource;
43
+ export interface HttpMapSourceOptions {
44
+ /** Maps are fetched from `${baseUrl}/${id}`, unless `url` is given. */
45
+ readonly baseUrl: string;
46
+ readonly url?: (id: string) => string;
47
+ readonly fetch?: typeof fetch;
48
+ readonly headers?: Readonly<Record<string, string>>;
49
+ }
50
+ /** Maps served over HTTP as JSON. The `fetch` implementation can be injected. */
51
+ export declare function httpMapSource(options: HttpMapSourceOptions): MapSource;
52
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/model/source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAIxE,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;CACtC;AAED,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACzC,4FAA4F;IAC5F,UAAU,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAC3D,IAAI,CAAC,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;CACrC;AAED,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,aAAa,CAAC;AAE7D,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAMrE;AAKD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,UAAU,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,SAAI,GAAG,SAAS,CAOxF;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,SAAI,GAAG,OAAO,CAAC,SAAS,CAAC,CAGhH;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,CAYvE;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC;AAElE,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAgBtE;AAED,MAAM,WAAW,oBAAoB;IACnC,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAatE"}
@@ -0,0 +1,75 @@
1
+ import { cloneJson } from "./json.js";
2
+ import { parseMapDefinition } from "./parse.js";
3
+ //#region src/model/source.ts
4
+ var MapSourceError = class extends Error {
5
+ code;
6
+ mapId;
7
+ constructor(code, mapId, message) {
8
+ super(message);
9
+ this.name = "MapSourceError";
10
+ this.code = code;
11
+ this.mapId = mapId;
12
+ }
13
+ };
14
+ var inside = (x, y, rect, margin) => x >= rect.x - margin && x <= rect.x + rect.width + margin && y >= rect.y - margin && y <= rect.y + rect.height + margin;
15
+ /**
16
+ * Elements and events whose origin falls in `rect`, grown by `margin` pixels so
17
+ * that objects larger than their origin are not cut off at the border.
18
+ */
19
+ function regionOf(definition, rect, margin = 0) {
20
+ return {
21
+ id: definition.id,
22
+ rect,
23
+ elements: definition.elements.filter((element) => inside(element.x, element.y, rect, margin)),
24
+ events: definition.events.filter((entry) => inside(entry.x, entry.y, rect, margin))
25
+ };
26
+ }
27
+ async function loadMapRegion(source, id, rect, margin = 0) {
28
+ if (source.loadRegion) return source.loadRegion(id, rect);
29
+ return regionOf(await source.load(id), rect, margin);
30
+ }
31
+ /** Maps written in code (`defineMap`). Every `load` returns a fresh copy. */
32
+ function codeMapSource(maps) {
33
+ const table = new Map(maps.map((map) => [map.id, map]));
34
+ return {
35
+ async load(id) {
36
+ const map = table.get(id);
37
+ if (!map) throw new MapSourceError("not-found", id, `Unknown map "${id}"`);
38
+ return cloneJson(map);
39
+ },
40
+ async list() {
41
+ return [...table.keys()];
42
+ }
43
+ };
44
+ }
45
+ /** Maps stored as JSON (files, a database). Content is validated and migrated on load. */
46
+ function jsonMapSource(options) {
47
+ return {
48
+ async load(id) {
49
+ let raw;
50
+ if ("maps" in options) {
51
+ if (!Object.hasOwn(options.maps, id)) throw new MapSourceError("not-found", id, `Unknown map "${id}"`);
52
+ raw = options.maps[id];
53
+ } else raw = await options.read(id);
54
+ return parseMapDefinition(raw);
55
+ },
56
+ async list() {
57
+ return "maps" in options ? Object.keys(options.maps) : [];
58
+ }
59
+ };
60
+ }
61
+ /** Maps served over HTTP as JSON. The `fetch` implementation can be injected. */
62
+ function httpMapSource(options) {
63
+ const doFetch = options.fetch ?? ((...args) => fetch(...args));
64
+ const urlOf = options.url ?? ((id) => `${options.baseUrl.replace(/\/$/, "")}/${encodeURIComponent(id)}`);
65
+ return jsonMapSource({ async read(id) {
66
+ const response = await doFetch(urlOf(id), options.headers ? { headers: { ...options.headers } } : void 0);
67
+ if (response.status === 404) throw new MapSourceError("not-found", id, `Unknown map "${id}"`);
68
+ if (!response.ok) throw new MapSourceError("load-failed", id, `Loading map "${id}" failed with status ${response.status}`);
69
+ return response.json();
70
+ } });
71
+ }
72
+ //#endregion
73
+ export { MapSourceError, codeMapSource, httpMapSource, jsonMapSource, loadMapRegion, regionOf };
74
+
75
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.js","names":[],"sources":["../../src/model/source.ts"],"sourcesContent":["import type { MapDefinition, MapElement, MapEvent } from \"./definition\";\nimport { cloneJson, type JsonValue } from \"./json\";\nimport { parseMapDefinition } from \"./parse\";\n\nexport interface MapRect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/** The part of a map that falls inside a rectangle, for streaming large maps. */\nexport interface MapRegion {\n readonly id: string;\n readonly rect: MapRect;\n readonly elements: readonly MapElement[];\n readonly events: readonly MapEvent[];\n}\n\n/** Where a map comes from: code, a JSON file, Tiled, a database, Studio… */\nexport interface MapSource {\n load(id: string): Promise<MapDefinition>;\n /** Natively loads one region. When absent, {@link loadMapRegion} derives it from `load`. */\n loadRegion?(id: string, rect: MapRect): Promise<MapRegion>;\n list?(): Promise<readonly string[]>;\n}\n\nexport type MapSourceErrorCode = \"not-found\" | \"load-failed\";\n\nexport class MapSourceError extends Error {\n readonly code: MapSourceErrorCode;\n readonly mapId: string;\n\n constructor(code: MapSourceErrorCode, mapId: string, message: string) {\n super(message);\n this.name = \"MapSourceError\";\n this.code = code;\n this.mapId = mapId;\n }\n}\n\nconst inside = (x: number, y: number, rect: MapRect, margin: number): boolean =>\n x >= rect.x - margin && x <= rect.x + rect.width + margin && y >= rect.y - margin && y <= rect.y + rect.height + margin;\n\n/**\n * Elements and events whose origin falls in `rect`, grown by `margin` pixels so\n * that objects larger than their origin are not cut off at the border.\n */\nexport function regionOf(definition: MapDefinition, rect: MapRect, margin = 0): MapRegion {\n return {\n id: definition.id,\n rect,\n elements: definition.elements.filter((element) => inside(element.x, element.y, rect, margin)),\n events: definition.events.filter((entry) => inside(entry.x, entry.y, rect, margin)),\n };\n}\n\nexport async function loadMapRegion(source: MapSource, id: string, rect: MapRect, margin = 0): Promise<MapRegion> {\n if (source.loadRegion) return source.loadRegion(id, rect);\n return regionOf(await source.load(id), rect, margin);\n}\n\n/** Maps written in code (`defineMap`). Every `load` returns a fresh copy. */\nexport function codeMapSource(maps: readonly MapDefinition[]): MapSource {\n const table = new Map(maps.map((map) => [map.id, map]));\n return {\n async load(id) {\n const map = table.get(id);\n if (!map) throw new MapSourceError(\"not-found\", id, `Unknown map \"${id}\"`);\n return cloneJson(map as unknown as JsonValue) as unknown as MapDefinition;\n },\n async list() {\n return [...table.keys()];\n },\n };\n}\n\nexport type JsonMapSourceOptions =\n | { readonly maps: Readonly<Record<string, unknown>> }\n | { readonly read: (id: string) => unknown | Promise<unknown> };\n\n/** Maps stored as JSON (files, a database). Content is validated and migrated on load. */\nexport function jsonMapSource(options: JsonMapSourceOptions): MapSource {\n return {\n async load(id) {\n let raw: unknown;\n if (\"maps\" in options) {\n if (!Object.hasOwn(options.maps, id)) throw new MapSourceError(\"not-found\", id, `Unknown map \"${id}\"`);\n raw = options.maps[id];\n } else {\n raw = await options.read(id);\n }\n return parseMapDefinition(raw);\n },\n async list() {\n return \"maps\" in options ? Object.keys(options.maps) : [];\n },\n };\n}\n\nexport interface HttpMapSourceOptions {\n /** Maps are fetched from `${baseUrl}/${id}`, unless `url` is given. */\n readonly baseUrl: string;\n readonly url?: (id: string) => string;\n readonly fetch?: typeof fetch;\n readonly headers?: Readonly<Record<string, string>>;\n}\n\n/** Maps served over HTTP as JSON. The `fetch` implementation can be injected. */\nexport function httpMapSource(options: HttpMapSourceOptions): MapSource {\n const doFetch = options.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args));\n const urlOf = options.url ?? ((id: string) => `${options.baseUrl.replace(/\\/$/, \"\")}/${encodeURIComponent(id)}`);\n return jsonMapSource({\n async read(id) {\n const response = await doFetch(urlOf(id), options.headers ? { headers: { ...options.headers } } : undefined);\n if (response.status === 404) throw new MapSourceError(\"not-found\", id, `Unknown map \"${id}\"`);\n if (!response.ok) {\n throw new MapSourceError(\"load-failed\", id, `Loading map \"${id}\" failed with status ${response.status}`);\n }\n return response.json();\n },\n });\n}\n"],"mappings":";;;AA6BA,IAAa,iBAAb,cAAoC,MAAM;CACxC;CACA;CAEA,YAAY,MAA0B,OAAe,SAAiB;EACpE,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;CACf;AACF;AAEA,IAAM,UAAU,GAAW,GAAW,MAAe,WACnD,KAAK,KAAK,IAAI,UAAU,KAAK,KAAK,IAAI,KAAK,QAAQ,UAAU,KAAK,KAAK,IAAI,UAAU,KAAK,KAAK,IAAI,KAAK,SAAS;;;;;AAMnH,SAAgB,SAAS,YAA2B,MAAe,SAAS,GAAc;CACxF,OAAO;EACL,IAAI,WAAW;EACf;EACA,UAAU,WAAW,SAAS,QAAQ,YAAY,OAAO,QAAQ,GAAG,QAAQ,GAAG,MAAM,MAAM,CAAC;EAC5F,QAAQ,WAAW,OAAO,QAAQ,UAAU,OAAO,MAAM,GAAG,MAAM,GAAG,MAAM,MAAM,CAAC;CACpF;AACF;AAEA,eAAsB,cAAc,QAAmB,IAAY,MAAe,SAAS,GAAuB;CAChH,IAAI,OAAO,YAAY,OAAO,OAAO,WAAW,IAAI,IAAI;CACxD,OAAO,SAAS,MAAM,OAAO,KAAK,EAAE,GAAG,MAAM,MAAM;AACrD;;AAGA,SAAgB,cAAc,MAA2C;CACvE,MAAM,QAAQ,IAAI,IAAI,KAAK,KAAK,QAAQ,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC;CACtD,OAAO;EACL,MAAM,KAAK,IAAI;GACb,MAAM,MAAM,MAAM,IAAI,EAAE;GACxB,IAAI,CAAC,KAAK,MAAM,IAAI,eAAe,aAAa,IAAI,gBAAgB,GAAG,EAAE;GACzE,OAAO,UAAU,GAA2B;EAC9C;EACA,MAAM,OAAO;GACX,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC;EACzB;CACF;AACF;;AAOA,SAAgB,cAAc,SAA0C;CACtE,OAAO;EACL,MAAM,KAAK,IAAI;GACb,IAAI;GACJ,IAAI,UAAU,SAAS;IACrB,IAAI,CAAC,OAAO,OAAO,QAAQ,MAAM,EAAE,GAAG,MAAM,IAAI,eAAe,aAAa,IAAI,gBAAgB,GAAG,EAAE;IACrG,MAAM,QAAQ,KAAK;GACrB,OACE,MAAM,MAAM,QAAQ,KAAK,EAAE;GAE7B,OAAO,mBAAmB,GAAG;EAC/B;EACA,MAAM,OAAO;GACX,OAAO,UAAU,UAAU,OAAO,KAAK,QAAQ,IAAI,IAAI,CAAC;EAC1D;CACF;AACF;;AAWA,SAAgB,cAAc,SAA0C;CACtE,MAAM,UAAU,QAAQ,WAAW,GAAG,SAAmC,MAAM,GAAG,IAAI;CACtF,MAAM,QAAQ,QAAQ,SAAS,OAAe,GAAG,QAAQ,QAAQ,QAAQ,OAAO,EAAE,EAAE,GAAG,mBAAmB,EAAE;CAC5G,OAAO,cAAc,EACnB,MAAM,KAAK,IAAI;EACb,MAAM,WAAW,MAAM,QAAQ,MAAM,EAAE,GAAG,QAAQ,UAAU,EAAE,SAAS,EAAE,GAAG,QAAQ,QAAQ,EAAE,IAAI,KAAA,CAAS;EAC3G,IAAI,SAAS,WAAW,KAAK,MAAM,IAAI,eAAe,aAAa,IAAI,gBAAgB,GAAG,EAAE;EAC5F,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,eAAe,eAAe,IAAI,gBAAgB,GAAG,uBAAuB,SAAS,QAAQ;EAEzG,OAAO,SAAS,KAAK;CACvB,EACF,CAAC;AACH"}
@@ -0,0 +1,92 @@
1
+ import { MapDefinition } from '../model/definition';
2
+ import { MapPatch } from '../model/patches';
3
+ import { MapIntent } from '../interaction/intents';
4
+ /**
5
+ * Messages between a map client and the authority of that map. They are plain JSON, so
6
+ * any transport carries them: a WebSocket, an RPGJS socket event, a Worker message.
7
+ *
8
+ * The client asks (`intent`), the authority decides and tells everybody (`patches`).
9
+ * A client never sends patches unless the authority trusts it.
10
+ */
11
+ export declare const MAP_BUILDER_EVENT = "map-builder";
12
+ export type ClientMessage =
13
+ /** Join, or catch up. `since` is the last revision the client has. */
14
+ {
15
+ readonly type: "sync";
16
+ readonly since?: number;
17
+ }
18
+ /** Ask for something to happen. `id` comes back in the answer. */
19
+ | {
20
+ readonly type: "intent";
21
+ readonly id: string;
22
+ readonly intents: readonly MapIntent[];
23
+ }
24
+ /** Ask for patches to be applied as they are. Only for clients the authority trusts (an editor, an undo). */
25
+ | {
26
+ readonly type: "patches";
27
+ readonly id: string;
28
+ readonly patches: readonly MapPatch[];
29
+ };
30
+ export type ServerMessage =
31
+ /** The whole map, at a revision: to join, or when the client is too far behind. */
32
+ {
33
+ readonly type: "snapshot";
34
+ readonly revision: number;
35
+ readonly definition: MapDefinition;
36
+ }
37
+ /** A change that became true. `requestId` is set for the client that asked for it. */
38
+ | {
39
+ readonly type: "patches";
40
+ readonly revision: number;
41
+ readonly patches: readonly MapPatch[];
42
+ readonly requestId?: string;
43
+ /** Who made the change, when the authority knows and wants to say. */
44
+ readonly author?: string;
45
+ }
46
+ /** The authority said no, and why. */
47
+ | {
48
+ readonly type: "rejected";
49
+ readonly id: string;
50
+ readonly reason: string;
51
+ readonly message: string;
52
+ readonly patchIndex?: number;
53
+ }
54
+ /** The client is up to date: nothing to send. */
55
+ | {
56
+ readonly type: "synced";
57
+ readonly revision: number;
58
+ }
59
+ /** A short visual effect: it is shown, not kept. */
60
+ | {
61
+ readonly type: "effect";
62
+ readonly target: EffectTargetMessage;
63
+ readonly name: string;
64
+ readonly params?: Readonly<Record<string, unknown>>;
65
+ };
66
+ export type EffectTargetMessage = string | {
67
+ readonly type: "element" | "event";
68
+ readonly id: string;
69
+ };
70
+ /** What an authority refuses to read, to keep a client from making it work too hard. */
71
+ export declare const PROTOCOL_LIMITS: {
72
+ /** Intents in one request. */
73
+ readonly maxIntents: 64;
74
+ /** Patches in one trusted request. */
75
+ readonly maxPatches: 256;
76
+ /** Length of a request id. */
77
+ readonly maxIdLength: 64;
78
+ };
79
+ /**
80
+ * Reads a message that came from the network. Anything that does not have exactly
81
+ * the expected shape is `undefined`: never trust a client, nor assume a message is one.
82
+ */
83
+ export declare function parseClientMessage(raw: unknown): ClientMessage | undefined;
84
+ /** Reads a message the authority sent. A client must not crash on garbage either. */
85
+ export declare function parseServerMessage(raw: unknown): ServerMessage | undefined;
86
+ /** A way to carry messages, whatever the framework: both ends of a connection implement it. */
87
+ export interface MapTransport {
88
+ send(message: ClientMessage): void;
89
+ /** Called for every message from the authority. Returns the function that stops listening. */
90
+ onMessage(listener: (message: ServerMessage) => void): () => void;
91
+ }
92
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/protocol/index.ts"],"names":[],"mappings":"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"}