@underpostnet/cyberia 3.3.73 → 3.3.77

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 (269) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +10 -0
  3. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  4. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  5. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  8. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  9. package/.github/workflows/engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  11. package/AGENTS.md +61 -12
  12. package/CHANGELOG.md +159 -161
  13. package/CLI-HELP.md +20 -13
  14. package/Dockerfile +1 -1
  15. package/Dockerfile.dev +1 -1
  16. package/Dockerfile.test +2 -2
  17. package/README.md +28 -28
  18. package/bin/build.js +96 -21
  19. package/bin/cyberia.js +1247 -1373
  20. package/bin/index.js +1247 -1373
  21. package/compose.env +17 -5
  22. package/conf.js +7 -4
  23. package/deploy/cyberia-client/deploy.sh +10 -11
  24. package/deploy/cyberia-client/package.sh +0 -1
  25. package/deploy/cyberia-client/state.sh +3 -2
  26. package/deploy/cyberia-server/deploy.sh +10 -11
  27. package/deploy/cyberia-server/package.sh +0 -1
  28. package/deploy/cyberia-server/state.sh +3 -2
  29. package/deploy/dd-cyberia/deploy.sh +5 -5
  30. package/deploy/dd-cyberia/init.sh +15 -14
  31. package/deploy/dd-cyberia/package.sh +0 -1
  32. package/deploy/dd-cyberia/state.sh +2 -2
  33. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  34. package/deploy/lib/config.sh +29 -0
  35. package/deploy/lib/github-actions-logging.sh +92 -76
  36. package/deploy/lib/host.sh +28 -19
  37. package/deploy/lib/state.sh +4 -1
  38. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  39. package/deploy/release/deploy.sh +0 -2
  40. package/deployment.yaml +1 -1
  41. package/docker-compose.yml +23 -17
  42. package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
  43. package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
  44. package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
  45. package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
  46. package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
  47. package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
  48. package/docs/coverage/cyberia/base.css +362 -0
  49. package/docs/coverage/cyberia/block-navigation.js +82 -0
  50. package/docs/coverage/cyberia/favicon.png +0 -0
  51. package/docs/coverage/cyberia/index.html +161 -0
  52. package/docs/coverage/cyberia/prettify.css +101 -0
  53. package/docs/coverage/cyberia/prettify.js +937 -0
  54. package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
  55. package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
  56. package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
  57. package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
  58. package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
  59. package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
  60. package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
  61. package/docs/coverage/cyberia/sorter.js +205 -0
  62. package/hardhat/package-lock.json +2 -2
  63. package/hardhat/package.json +1 -1
  64. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  65. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  66. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  67. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  68. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  69. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  70. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  71. package/manifests/deployment/mongo-express/service.yaml +16 -0
  72. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  73. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  74. package/package.json +19 -17
  75. package/scripts/test-monitor.sh +3 -3
  76. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  77. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  80. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  81. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  82. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  83. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  84. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  85. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  86. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  87. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  88. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  90. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  91. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  92. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  93. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  94. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  95. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  96. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  97. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  98. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  99. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  100. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  101. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  102. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  103. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  104. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  107. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  110. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  111. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  112. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  113. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  114. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  115. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  116. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  118. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  119. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  120. package/src/api/file/file.ref.js +153 -0
  121. package/src/api/file/file.ref.json +11 -2
  122. package/src/api/object-layer/object-layer.model.js +47 -12
  123. package/src/api/object-layer/object-layer.router.js +10 -3
  124. package/src/api/object-layer/object-layer.service.js +113 -120
  125. package/src/cli/app.js +12 -1
  126. package/src/cli/client.js +2 -2
  127. package/src/cli/cluster.js +80 -18
  128. package/src/cli/deploy.js +215 -34
  129. package/src/cli/docker-compose.js +1 -1
  130. package/src/cli/fs-selection.js +115 -0
  131. package/src/cli/fs.js +158 -342
  132. package/src/cli/index.js +22 -10
  133. package/src/cli/release.js +3 -1
  134. package/src/cli/repository.js +35 -9
  135. package/src/cli/run.js +80 -199
  136. package/src/cli/test.js +4 -0
  137. package/src/cli/wireguard.js +32 -31
  138. package/src/client/components/core/Badge.js +0 -1
  139. package/src/client/components/core/CssCore.js +8 -1
  140. package/src/client/components/core/Docs.js +77 -100
  141. package/src/client/components/core/Modal.js +77 -23
  142. package/src/client/components/core/ToolTip.js +15 -0
  143. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
  144. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  145. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  146. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  147. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  148. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  149. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  150. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  151. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  152. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  153. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  154. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  155. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  156. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
  157. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  158. package/src/client/components/default/AppShellDefault.js +1 -4
  159. package/src/client/components/itemledger/AppShellItemledger.js +0 -2
  160. package/src/client/components/underpost/AppShellUnderpost.js +1 -1
  161. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  162. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  163. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  164. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  165. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  166. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  167. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  168. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  169. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  170. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  171. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  172. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  173. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  174. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  175. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  176. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  177. package/src/client/services/object-layer/object-layer.management.js +6 -10
  178. package/src/client/services/object-layer/object-layer.service.js +3 -1
  179. package/src/client/ssr/views/Cyberia404.js +3 -3
  180. package/src/client-builder/client-build-docs.js +52 -52
  181. package/src/client-builder/client-build.js +67 -44
  182. package/src/client-builder/client-bundle.js +347 -0
  183. package/src/db/mongo/MongoExpress.js +187 -0
  184. package/src/grpc/cyberia/grpc-server.js +5 -7
  185. package/src/index.js +1 -1
  186. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  187. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  188. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  189. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  190. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  191. package/src/projects/cyberia/gemini-client.js +3 -3
  192. package/src/projects/cyberia/generate-saga.js +4 -11
  193. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  194. package/src/projects/cyberia/instance-backup.js +210 -0
  195. package/src/projects/cyberia/instance-data.js +177 -257
  196. package/src/projects/cyberia/map-preview-generator.js +32 -68
  197. package/src/projects/cyberia/object-layer.js +152 -319
  198. package/src/projects/cyberia/seed-audio.js +139 -0
  199. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  200. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  201. package/src/projects/cyberia/stat-balance.js +81 -0
  202. package/src/projects/cyberia/stat-commands.js +23 -0
  203. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  204. package/src/runtime/cyberia-client/Dockerfile +8 -4
  205. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  206. package/src/runtime/cyberia-server/Dockerfile +9 -2
  207. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  208. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  209. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  210. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  211. package/src/runtime/engine-cyberia/compose.env +17 -5
  212. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  213. package/src/server/build/coverage.js +113 -42
  214. package/src/server/build/package.js +55 -12
  215. package/src/server/build/testing.js +59 -9
  216. package/src/server/network/middlewares.js +8 -2
  217. package/src/server/network/underpost-gateway.js +113 -14
  218. package/src/server/ops/logger.js +70 -35
  219. package/src/server/runtime/conf.js +40 -9
  220. package/src/server/runtime/start.js +11 -2
  221. package/src/server/storage/data-query.js +16 -0
  222. package/src/server/storage/downloader.js +62 -14
  223. package/src/server/storage/zip.js +153 -0
  224. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  225. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  226. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  227. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  228. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  229. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  230. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  231. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  232. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  233. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  234. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  235. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  236. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  237. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  238. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  239. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  240. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  241. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  242. package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
  243. package/test/unit/catalog.test.js +13 -2
  244. package/test/unit/client-build-docs.test.js +97 -70
  245. package/test/unit/client-bundle.test.js +313 -0
  246. package/test/unit/conf-resolution.test.js +58 -7
  247. package/test/unit/coverage-artifact.test.js +120 -29
  248. package/test/unit/cyberia/instance-backup.test.js +183 -0
  249. package/test/unit/cyberia/instance-data.test.js +314 -0
  250. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  251. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  252. package/test/unit/cyberia/stat-balance.test.js +69 -0
  253. package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
  254. package/test/unit/deploy-log-table.test.js +76 -5
  255. package/test/unit/downloader-integrity.test.js +98 -0
  256. package/test/unit/file-reference-registry.test.js +141 -0
  257. package/test/unit/fs-storage-paths.test.js +596 -0
  258. package/test/unit/gateway-pod-fetch.test.js +95 -0
  259. package/test/unit/letsencrypt-issuer.test.js +101 -0
  260. package/test/unit/logger-redaction.test.js +29 -0
  261. package/test/unit/package.test.js +70 -16
  262. package/test/unit/prepare-host.test.js +84 -1
  263. package/test/unit/release-bump.test.js +3 -1
  264. package/test/unit/start-options.test.js +12 -0
  265. package/test/unit/test-tiers.test.js +27 -0
  266. package/test/unit/zip-archive.test.js +142 -0
  267. package/vitest.config.js +11 -3
  268. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  269. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
@@ -13,8 +13,8 @@ import { FileService } from '../../services/file/file.service.js';
13
13
  import { DefaultManagement } from '../../services/default/default.management.js';
14
14
  import { getApiBaseUrl } from '../../services/core/core.service.js';
15
15
  import { ObjectLayerService } from '../../services/object-layer/object-layer.service.js';
16
- import { getProxyPath } from '../core/Router.js';
17
- import { ENTITY_TYPES, getDefaultCyberiaItemById } from './SharedDefaultsCyberia.js';
16
+ import { getProxyPath, getQueryParams, listenQueryParamsChange, setQueryParams } from '../core/Router.js';
17
+ import { ENTITY_TYPES, ENTITY_LEVEL_MIN, ENTITY_LEVEL_MAX, validateEntityLevel } from './SharedDefaultsCyberia.js';
18
18
  import '../core/ColorPaletteElement.js';
19
19
 
20
20
  const DEFAULT_ENTITY_TYPE = ENTITY_TYPES.floor;
@@ -29,6 +29,7 @@ const createDropdownOption = (value, onClick = () => {}, display = value, data =
29
29
  class MapEngineCyberia {
30
30
  static entities = [];
31
31
  static currentMapId = null;
32
+ static currentMapCode = null;
32
33
  static currentThumbnailId = null;
33
34
  static thumbnailDirty = false;
34
35
  static currentPreviewId = null;
@@ -65,6 +66,14 @@ class MapEngineCyberia {
65
66
  : [];
66
67
  }
67
68
 
69
+ /** Comma-separated ItemIds as typed, in order, blanks removed. Duplicates are the caller's to judge. */
70
+ static parseItemIdList(value = '') {
71
+ return String(value)
72
+ .split(',')
73
+ .map((itemId) => itemId.trim())
74
+ .filter((itemId) => itemId);
75
+ }
76
+
68
77
  static getRenameObjectLayerItemIds() {
69
78
  return {
70
79
  source: s(`.${MapEngineCyberia.renameSourceObjectLayerInputId}`)?.value?.trim() || '',
@@ -72,6 +81,47 @@ class MapEngineCyberia {
72
81
  };
73
82
  }
74
83
 
84
+ /**
85
+ * source ItemId → the ItemIds that replace it, or the reason the lists cannot be paired.
86
+ *
87
+ * Three shapes, each unambiguous on its own:
88
+ * - one source, many targets — the source expands into all of them, so a layer can be split
89
+ * into a skin plus a weapon in one pass;
90
+ * - one target, many sources — every source collapses onto it;
91
+ * - equal counts — paired off in the order they were typed.
92
+ * Any other combination is a typo worth refusing: pairing a short list by position would
93
+ * rewrite some entities and silently leave the rest.
94
+ *
95
+ * @returns {{map?: Map<string,string[]>, error?: string}}
96
+ */
97
+ static buildRenameItemIdMap() {
98
+ const { source, target } = MapEngineCyberia.getRenameObjectLayerItemIds();
99
+ const sources = MapEngineCyberia.parseItemIdList(source);
100
+ const targets = MapEngineCyberia.parseItemIdList(target);
101
+ if (!sources.length || !targets.length) return { error: 'Source and target ItemId are required.' };
102
+
103
+ const repeated = sources.filter((itemId, index) => sources.indexOf(itemId) !== index);
104
+ if (repeated.length) {
105
+ return { error: `Each source ItemId may appear once: "${[...new Set(repeated)].join('", "')}" is repeated.` };
106
+ }
107
+ if (sources.length > 1 && targets.length > 1 && targets.length !== sources.length) {
108
+ return {
109
+ error: `${sources.length} sources take one target for all of them, or ${sources.length} paired in order — got ${targets.length}. A list of targets replaces a single source.`,
110
+ };
111
+ }
112
+
113
+ const replacement = (index) =>
114
+ sources.length === 1 || targets.length === 1 ? [...new Set(targets)] : [targets[index]];
115
+ const map = new Map(sources.map((itemId, index) => [itemId, replacement(index)]));
116
+ // A source that expands into a list containing itself is keeping its layer and gaining
117
+ // others, which is the point. Only a replacement that is the source alone does nothing.
118
+ const unchanged = [...map].filter(([from, to]) => to.length === 1 && to[0] === from).map(([from]) => from);
119
+ if (unchanged.length) {
120
+ return { error: `Source and target ItemId must differ: "${unchanged.join('", "')}".` };
121
+ }
122
+ return { map };
123
+ }
124
+
75
125
  static setDropdownValue(dropdownId, value) {
76
126
  if (!value || !DropDown.Tokens[dropdownId]) return;
77
127
  DropDown.Tokens[dropdownId].value = value;
@@ -111,8 +161,8 @@ class MapEngineCyberia {
111
161
  excludeSelected: true,
112
162
  serviceProvider: async (q) => {
113
163
  const result = await ObjectLayerService.searchItemIds({ q });
114
- if (result.status === 'success' && result.data?.itemIds) {
115
- return result.data.itemIds.map((itemId) => createDropdownOption(itemId));
164
+ if (result.status === 'success' && result.data?.items) {
165
+ return result.data.items.map(({ id }) => createDropdownOption(id));
116
166
  }
117
167
  return [];
118
168
  },
@@ -128,22 +178,12 @@ class MapEngineCyberia {
128
178
  }
129
179
 
130
180
  static renameFilteredObjectLayerItemId() {
131
- const { source, target } = MapEngineCyberia.getRenameObjectLayerItemIds();
132
- if (!source || !target) {
133
- NotificationManager.Push({
134
- html: 'Source and target ItemId are required.',
135
- status: 'error',
136
- });
137
- return false;
138
- }
139
-
140
- if (source === target) {
141
- NotificationManager.Push({
142
- html: 'Source and target ItemId must be different.',
143
- status: 'error',
144
- });
181
+ const { map: renames, error } = MapEngineCyberia.buildRenameItemIdMap();
182
+ if (error) {
183
+ NotificationManager.Push({ html: error, status: 'error' });
145
184
  return false;
146
185
  }
186
+ const sourceList = [...renames.keys()].join('", "');
147
187
 
148
188
  const filtered = MapEngineCyberia.getFilteredEntities();
149
189
  if (!filtered.length) {
@@ -154,18 +194,19 @@ class MapEngineCyberia {
154
194
  return false;
155
195
  }
156
196
 
157
- // Collect indices of filtered entities that have the source itemId
197
+ // Exact ItemId equality, never a prefix or a case fold: this rewrites map documents, and a
198
+ // near-miss would rename an entity nobody asked about.
158
199
  const eligible = [];
159
200
  for (const { i } of filtered) {
160
201
  const entity = MapEngineCyberia.entities[i];
161
- if (Array.isArray(entity?.objectLayerItemIds) && entity.objectLayerItemIds.includes(source)) {
202
+ if (Array.isArray(entity?.objectLayerItemIds) && entity.objectLayerItemIds.some((id) => renames.has(id))) {
162
203
  eligible.push(i);
163
204
  }
164
205
  }
165
206
 
166
207
  if (!eligible.length) {
167
208
  NotificationManager.Push({
168
- html: `No exact ItemId matches for "${source}" were found in the current filtered entities.`,
209
+ html: `No exact ItemId matches for "${sourceList}" were found in the current filtered entities.`,
169
210
  status: 'error',
170
211
  });
171
212
  return false;
@@ -175,6 +216,8 @@ class MapEngineCyberia {
175
216
  // a random probability check in [fMin, fMax]. If the roll passes, it gets renamed;
176
217
  // otherwise it stays unchanged. This produces an intermediate intensity between
177
218
  // "almost everyone" (factors near/above 1.0) and "almost no one" (factors near 0).
219
+ // The roll is per entity, not per ItemId: an entity carrying several sources takes all of
220
+ // its renames or none, so a partly-renamed loadout is never produced.
178
221
  let matchedEntities = 0;
179
222
  let renamedReferences = 0;
180
223
  const changed = MapEngineCyberia.commitEntityMutation(() => {
@@ -191,20 +234,28 @@ class MapEngineCyberia {
191
234
  if (roll < 1.0 && Math.random() > roll) continue;
192
235
  }
193
236
  const entity = MapEngineCyberia.entities[i];
194
- let entityChanged = false;
195
- entity.objectLayerItemIds = entity.objectLayerItemIds.map((itemId) => {
196
- if (itemId !== source) return itemId;
197
- entityChanged = true;
198
- renamedReferences += 1;
199
- return target;
237
+ const previous = entity.objectLayerItemIds;
238
+ let replaced = 0;
239
+ const expanded = previous.flatMap((itemId) => {
240
+ if (!renames.has(itemId)) return [itemId];
241
+ replaced += 1;
242
+ return renames.get(itemId);
200
243
  });
201
- if (entityChanged) matchedEntities += 1;
244
+ // A replacement the entity already carries must not be added twice: an ItemId appearing
245
+ // once is what every reader of a loadout assumes.
246
+ const next = [...new Set(expanded)];
247
+ if (next.length === previous.length && next.every((itemId, index) => itemId === previous[index])) continue;
248
+ entity.objectLayerItemIds = next;
249
+ renamedReferences += replaced;
250
+ matchedEntities += 1;
202
251
  }
203
252
  });
204
253
 
205
254
  if (!changed) {
206
255
  NotificationManager.Push({
207
- html: `No exact ItemId matches for "${source}" were found in the current filtered entities.`,
256
+ html: MapEngineCyberia.enableRandomFactors
257
+ ? `Random factors skipped all ${eligible.length} matching entit${eligible.length === 1 ? 'y' : 'ies'} — raise the factors or turn them off.`
258
+ : `All ${eligible.length} entit${eligible.length === 1 ? 'y' : 'ies'} matching "${sourceList}" already carry the target ItemIds.`,
208
259
  status: 'error',
209
260
  });
210
261
  return false;
@@ -374,16 +425,24 @@ class MapEngineCyberia {
374
425
  filterType: s('.map-engine-filter-entity-type')?.value?.trim().toLowerCase() || '',
375
426
  filterX: s('.map-engine-filter-init-x')?.value?.trim() || '',
376
427
  filterY: s('.map-engine-filter-init-y')?.value?.trim() || '',
428
+ filterItemIds: MapEngineCyberia.parseItemIdList(s('.map-engine-filter-object-layer-item-ids')?.value || ''),
377
429
  };
378
430
  }
379
431
 
380
432
  static getFilteredEntities() {
381
- const { filterType, filterX, filterY } = MapEngineCyberia.getEntityFilters();
433
+ const { filterType, filterX, filterY, filterItemIds } = MapEngineCyberia.getEntityFilters();
382
434
 
383
435
  return MapEngineCyberia.entities.reduce((acc, entity, i) => {
384
436
  if (filterType && !(entity.entityType || '').toLowerCase().includes(filterType)) return acc;
385
437
  if (filterX !== '' && String(entity.initCellX) !== filterX) return acc;
386
438
  if (filterY !== '' && String(entity.initCellY) !== filterY) return acc;
439
+ // Every listed id, matched exactly: the list narrows the selection like the other filters,
440
+ // so naming a second id picks out the entities carrying that combination rather than
441
+ // widening the set. What this selects is exactly what the rename will act on.
442
+ if (filterItemIds.length) {
443
+ const carried = entity.objectLayerItemIds || [];
444
+ if (!filterItemIds.every((itemId) => carried.includes(itemId))) return acc;
445
+ }
387
446
  acc.push({ entity, i });
388
447
  return acc;
389
448
  }, []);
@@ -393,41 +452,16 @@ class MapEngineCyberia {
393
452
  if (MapEngineCyberia.imageCache[itemId]) return;
394
453
  MapEngineCyberia.imageCache[itemId] = { img: null, loaded: false, error: false };
395
454
 
396
- const loadImage = (type, id) => {
397
- const img = new Image();
398
- img.onload = () => {
399
- MapEngineCyberia.imageCache[itemId].img = img;
400
- MapEngineCyberia.imageCache[itemId].loaded = true;
401
- if (onLoad) onLoad();
402
- };
403
- img.onerror = () => {
404
- MapEngineCyberia.imageCache[itemId].error = true;
405
- };
406
- img.src = `${getProxyPath()}assets/${type}/${id}/08/0.png`;
455
+ const img = new Image();
456
+ img.onload = () => {
457
+ MapEngineCyberia.imageCache[itemId].img = img;
458
+ MapEngineCyberia.imageCache[itemId].loaded = true;
459
+ if (onLoad) onLoad();
407
460
  };
408
-
409
- const sharedItem = getDefaultCyberiaItemById(itemId)?.item;
410
- if (sharedItem?.type && sharedItem?.id) {
411
- loadImage(sharedItem.type, sharedItem.id);
412
- return;
413
- }
414
-
415
- ObjectLayerService.get({
416
- limit: 1,
417
- filterModel: { 'data.item.id': { filterType: 'text', type: 'equals', filter: itemId } },
418
- })
419
- .then((res) => {
420
- const doc = res?.data?.data?.[0];
421
- if (!doc || !doc.data?.item?.type || !doc.data?.item?.id) {
422
- MapEngineCyberia.imageCache[itemId].error = true;
423
- return;
424
- }
425
- const { type, id } = doc.data.item;
426
- loadImage(type, id);
427
- })
428
- .catch(() => {
429
- MapEngineCyberia.imageCache[itemId].error = true;
430
- });
461
+ img.onerror = () => {
462
+ MapEngineCyberia.imageCache[itemId].error = true;
463
+ };
464
+ img.src = `${getProxyPath()}api/atlas-sprite-sheet/idle-preview/${itemId}`;
431
465
  }
432
466
 
433
467
  static renderGrid(canvas, cols, rows, cellW, cellH, showGrid = true) {
@@ -578,6 +612,7 @@ class MapEngineCyberia {
578
612
  if (s('.map-engine-init-cell-x')) s('.map-engine-init-cell-x').value = entity.initCellX || 0;
579
613
  if (s('.map-engine-init-cell-y')) s('.map-engine-init-cell-y').value = entity.initCellY || 0;
580
614
  if (s('.map-engine-dim-x')) s('.map-engine-dim-x').value = entity.dimX || 1;
615
+ if (s('.map-engine-level')) s('.map-engine-level').value = entity.level ?? '';
581
616
  if (s('.map-engine-dim-y')) s('.map-engine-dim-y').value = entity.dimY || 1;
582
617
 
583
618
  // Parse rgba color back to hex + alpha
@@ -619,6 +654,7 @@ class MapEngineCyberia {
619
654
  const canvasId = 'map-engine-canvas';
620
655
 
621
656
  const idEntityType = MapEngineCyberia.entityTypeDropdownId;
657
+ const idLevel = 'map-engine-level';
622
658
  const idInitCellX = 'map-engine-init-cell-x';
623
659
  const idInitCellY = 'map-engine-init-cell-y';
624
660
  const idDimX = 'map-engine-dim-x';
@@ -639,6 +675,7 @@ class MapEngineCyberia {
639
675
 
640
676
  MapEngineCyberia.setEntities([], { clearHistory: true });
641
677
  MapEngineCyberia.currentMapId = null;
678
+ MapEngineCyberia.currentMapCode = null;
642
679
  MapEngineCyberia.currentThumbnailId = null;
643
680
  MapEngineCyberia.currentPreviewId = null;
644
681
 
@@ -704,7 +741,16 @@ class MapEngineCyberia {
704
741
  const getEntityParams = () => {
705
742
  const hex = s(`.${idColor}`)?.value || '#ff0000';
706
743
  const alpha = parseFloat(s(`.${idAlpha}`)?.value);
744
+ const rawLevel = s(`.${idLevel}`)?.value.trim();
745
+ let level;
746
+ try {
747
+ if (rawLevel) level = validateEntityLevel(Number(rawLevel));
748
+ } catch (error) {
749
+ NotificationManager.Push({ html: error.message, status: 'error' });
750
+ return null;
751
+ }
707
752
  return {
753
+ level,
708
754
  entityType: MapEngineCyberia.getSelectedEntityType(),
709
755
  initCellX: parseInt(s(`.${idInitCellX}`)?.value) || 0,
710
756
  initCellY: parseInt(s(`.${idInitCellY}`)?.value) || 0,
@@ -724,6 +770,7 @@ class MapEngineCyberia {
724
770
 
725
771
  const addEntityLocally = () => {
726
772
  const ep = getEntityParams();
773
+ if (!ep) return;
727
774
  ep.objectLayerItemIds = DropDown.Tokens[idObjLayerDropdown]?.value
728
775
  ? [...DropDown.Tokens[idObjLayerDropdown].value]
729
776
  : [];
@@ -735,6 +782,7 @@ class MapEngineCyberia {
735
782
 
736
783
  const fillMapWithEntity = () => {
737
784
  const ep = getEntityParams();
785
+ if (!ep) return;
738
786
  ep.objectLayerItemIds = DropDown.Tokens[idObjLayerDropdown]?.value
739
787
  ? [...DropDown.Tokens[idObjLayerDropdown].value]
740
788
  : [];
@@ -813,6 +861,7 @@ class MapEngineCyberia {
813
861
  const selectedCount = getAffectedEntityCount(preserveIndices.length);
814
862
  const selected = [...preserveIndices].sort(() => Math.random() - 0.5).slice(0, selectedCount);
815
863
  const template = getEntityParams();
864
+ if (!template) return;
816
865
  template.objectLayerItemIds = DropDown.Tokens[idObjLayerDropdown]?.value
817
866
  ? [...DropDown.Tokens[idObjLayerDropdown].value]
818
867
  : [];
@@ -823,6 +872,7 @@ class MapEngineCyberia {
823
872
  const replacement = {
824
873
  ...target,
825
874
  entityType: template.entityType,
875
+ level: template.level,
826
876
  dimX: template.dimX,
827
877
  dimY: template.dimY,
828
878
  color: template.color,
@@ -964,6 +1014,9 @@ class MapEngineCyberia {
964
1014
  });
965
1015
  if (result.status === 'success') {
966
1016
  if (result.data?._id) MapEngineCyberia.currentMapId = result.data._id;
1017
+ // A saved map becomes the one the URL points at.
1018
+ MapEngineCyberia.currentMapCode = body.code || null;
1019
+ setQueryParams({ mapCode: body.code || null }, { replace: true });
967
1020
  renderPreviewImage();
968
1021
  await DefaultManagement.loadTable(managementId, { force: true, reload: true });
969
1022
  }
@@ -1008,6 +1061,8 @@ class MapEngineCyberia {
1008
1061
  });
1009
1062
  if (result.status === 'success') {
1010
1063
  if (result.data?._id) MapEngineCyberia.currentMapId = result.data._id;
1064
+ MapEngineCyberia.currentMapCode = body.code || null;
1065
+ setQueryParams({ mapCode: body.code || null }, { replace: true });
1011
1066
  if (cloneThumbnailId) MapEngineCyberia.currentThumbnailId = cloneThumbnailId;
1012
1067
  if (clonePreviewId) MapEngineCyberia.currentPreviewId = clonePreviewId;
1013
1068
  renderPreviewImage();
@@ -1017,6 +1072,8 @@ class MapEngineCyberia {
1017
1072
 
1018
1073
  const loadMap = async (mapData) => {
1019
1074
  MapEngineCyberia.currentMapId = mapData._id || null;
1075
+ MapEngineCyberia.currentMapCode = mapData.code || null;
1076
+ setQueryParams({ mapCode: mapData.code || null }, { replace: true });
1020
1077
  if (s(`.${idCode}`)) s(`.${idCode}`).value = mapData.code || '';
1021
1078
  if (s(`.${idName}`)) s(`.${idName}`).value = mapData.name || '';
1022
1079
  if (s(`.${idDescription}`)) s(`.${idDescription}`).value = mapData.description || '';
@@ -1089,6 +1146,7 @@ class MapEngineCyberia {
1089
1146
 
1090
1147
  const nextEntities = (mapData.entities || []).map((e) => ({
1091
1148
  entityType: e.entityType,
1149
+ level: e.level,
1092
1150
  initCellX: e.initCellX,
1093
1151
  initCellY: e.initCellY,
1094
1152
  dimX: e.dimX,
@@ -1103,6 +1161,8 @@ class MapEngineCyberia {
1103
1161
 
1104
1162
  const resetForm = () => {
1105
1163
  MapEngineCyberia.currentMapId = null;
1164
+ MapEngineCyberia.currentMapCode = null;
1165
+ setQueryParams({ mapCode: null }, { replace: true });
1106
1166
  MapEngineCyberia.currentThumbnailId = null;
1107
1167
  MapEngineCyberia.currentPreviewId = null;
1108
1168
  MapEngineCyberia.thumbnailDirty = false;
@@ -1127,6 +1187,7 @@ class MapEngineCyberia {
1127
1187
  if (s(`.${idY}`)) s(`.${idY}`).value = 16;
1128
1188
  if (s(`.${idCellW}`)) s(`.${idCellW}`).value = 32;
1129
1189
  if (s(`.${idCellH}`)) s(`.${idCellH}`).value = 32;
1190
+ if (s(`.${idLevel}`)) s(`.${idLevel}`).value = '';
1130
1191
  if (s(`.${idVariationPreserve}`)) s(`.${idVariationPreserve}`).value = '';
1131
1192
  if (s(`.${idRenameSourceObjectLayer}`)) s(`.${idRenameSourceObjectLayer}`).value = '';
1132
1193
  if (s(`.${idRenameTargetObjectLayer}`)) s(`.${idRenameTargetObjectLayer}`).value = '';
@@ -1140,6 +1201,28 @@ class MapEngineCyberia {
1140
1201
  }
1141
1202
  };
1142
1203
 
1204
+ // ?mapCode=<code> loads that map into the form on arrival.
1205
+ const loadMapByCode = async (code) => {
1206
+ if (!code || code === MapEngineCyberia.currentMapCode) return;
1207
+ const { status, data } = await CyberiaMapService.get({ id: code });
1208
+ if (status !== 'success' || !data?._id) {
1209
+ NotificationManager.Push({ html: `No map answers to the code "${code}"`, status: 'error' });
1210
+ return;
1211
+ }
1212
+ await loadMap(data);
1213
+ NotificationManager.Push({ html: `Map "${data.name || data.code}" loaded`, status: 'success' });
1214
+ };
1215
+
1216
+ listenQueryParamsChange({
1217
+ id: 'cyberia-map-engine-query-listener',
1218
+ event: async (queryParams) => {
1219
+ if (!s(`.${idCode}`)) return;
1220
+ const code = queryParams.mapCode || null;
1221
+ if (code) await loadMapByCode(code);
1222
+ else if (MapEngineCyberia.currentMapCode) resetForm();
1223
+ },
1224
+ });
1225
+
1143
1226
  setTimeout(() => {
1144
1227
  const canvas = s(`.${canvasId}`);
1145
1228
  if (!canvas) return;
@@ -1311,13 +1394,13 @@ class MapEngineCyberia {
1311
1394
  MapEngineCyberia.renderEntityList(entityListId);
1312
1395
  }, 300);
1313
1396
  };
1314
- [idFilterEntityType, idFilterInitX, idFilterInitY].forEach((cls) => {
1397
+ [idFilterEntityType, idFilterInitX, idFilterInitY, idFilterObjectLayerItemIds].forEach((cls) => {
1315
1398
  if (s(`.${cls}`)) s(`.${cls}`).addEventListener('input', applyEntityFilter);
1316
1399
  });
1317
1400
 
1318
1401
  if (s('.btn-map-engine-clear-entity-filter'))
1319
1402
  s('.btn-map-engine-clear-entity-filter').onclick = () => {
1320
- [idFilterEntityType, idFilterInitX, idFilterInitY].forEach((cls) => {
1403
+ [idFilterEntityType, idFilterInitX, idFilterInitY, idFilterObjectLayerItemIds].forEach((cls) => {
1321
1404
  if (s(`.${cls}`)) s(`.${cls}`).value = '';
1322
1405
  });
1323
1406
  MapEngineCyberia.renderEntityList(entityListId);
@@ -1330,6 +1413,9 @@ class MapEngineCyberia {
1330
1413
  s('.btn-map-engine-rename-filtered-object-layer-item-id').onclick = () => {
1331
1414
  MapEngineCyberia.renameFilteredObjectLayerItemId();
1332
1415
  };
1416
+
1417
+ // Loaded last: loadMap repaints the canvas through the history sync above.
1418
+ loadMapByCode(getQueryParams().mapCode);
1333
1419
  });
1334
1420
 
1335
1421
  const statusOptions = [
@@ -1368,6 +1454,7 @@ class MapEngineCyberia {
1368
1454
  const idFilterEntityType = 'map-engine-filter-entity-type';
1369
1455
  const idFilterInitX = 'map-engine-filter-init-x';
1370
1456
  const idFilterInitY = 'map-engine-filter-init-y';
1457
+ const idFilterObjectLayerItemIds = 'map-engine-filter-object-layer-item-ids';
1371
1458
 
1372
1459
  return html`<div class="in section-mp map-engine-container">
1373
1460
  ${dynamicCol({ containerSelector: 'map-engine-container', id: dcMapFields, type: 'search-inputs' })}
@@ -1674,6 +1761,11 @@ class MapEngineCyberia {
1674
1761
  value: DEFAULT_ENTITY_TYPE,
1675
1762
  containerClass: 'inl',
1676
1763
  })}
1764
+ ${await Input.instance({
1765
+ id: idLevel,
1766
+ label: html`Level (empty uses instance default)`,
1767
+ containerClass: 'inl', type: 'number', min: ENTITY_LEVEL_MIN, max: ENTITY_LEVEL_MAX, value: '',
1768
+ })}
1677
1769
  </div>
1678
1770
  <div class="in fll ${dcEntityType}-col-b">
1679
1771
  ${await Input.instance({
@@ -1816,13 +1908,15 @@ class MapEngineCyberia {
1816
1908
  <div class="in section-mp-border" style="margin: 10px; padding: 10px;">
1817
1909
  <div class="in input-label">Rename Object Layer ItemId on Filtered Entities</div>
1818
1910
  <div class="in" style="font-size:12px;color:#888;margin-bottom:8px;">
1819
- Replaces only exact source ItemId matches inside entities currently visible through the filters.
1911
+ Replaces only exact source ItemId matches inside entities currently visible through the filters. Both
1912
+ fields take comma-separated lists: several targets expand one source into all of them, one target
1913
+ renames every source onto it, and equal counts pair off in the order typed.
1820
1914
  </div>
1821
1915
  <div class="fl">
1822
1916
  <div class="in fll" style="flex:1;padding-right:5px;">
1823
1917
  ${await Input.instance({
1824
1918
  id: idRenameSourceObjectLayer,
1825
- label: html`Source ItemId`,
1919
+ label: html`Source ItemIds`,
1826
1920
  containerClass: 'inl',
1827
1921
  type: 'text',
1828
1922
  placeholder: true,
@@ -1831,7 +1925,7 @@ class MapEngineCyberia {
1831
1925
  <div class="in fll" style="flex:1;padding-left:5px;">
1832
1926
  ${await Input.instance({
1833
1927
  id: idRenameTargetObjectLayer,
1834
- label: html`Target ItemId`,
1928
+ label: html`Target ItemIds`,
1835
1929
  containerClass: 'inl',
1836
1930
  type: 'text',
1837
1931
  placeholder: true,
@@ -1923,6 +2017,18 @@ class MapEngineCyberia {
1923
2017
  })}
1924
2018
  </div>
1925
2019
  </div>
2020
+ <div class="in" style="margin-top:5px;">
2021
+ ${await Input.instance({
2022
+ id: idFilterObjectLayerItemIds,
2023
+ label: html`Object Layer ItemIds`,
2024
+ containerClass: 'inl',
2025
+ type: 'text',
2026
+ placeholder: true,
2027
+ })}
2028
+ <div class="in" style="font-size:12px;color:#888;margin-top:3px;">
2029
+ Comma separated, matched exactly. Keeps only entities carrying all of them.
2030
+ </div>
2031
+ </div>
1926
2032
  <div
1927
2033
  class="in map-engine-entity-filter-count"
1928
2034
  style="margin-top:5px;font-size:12px;color:#888;font-family:monospace;"