cyberia 3.0.3 → 3.2.5

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 (296) hide show
  1. package/{.env.production → .env.example} +20 -4
  2. package/.github/workflows/engine-cyberia.cd.yml +43 -10
  3. package/.github/workflows/engine-cyberia.ci.yml +48 -26
  4. package/.github/workflows/ghpkg.ci.yml +5 -5
  5. package/.github/workflows/gitlab.ci.yml +1 -1
  6. package/.github/workflows/hardhat.ci.yml +82 -0
  7. package/.github/workflows/npmpkg.ci.yml +60 -14
  8. package/.github/workflows/publish.ci.yml +26 -7
  9. package/.github/workflows/publish.cyberia.ci.yml +5 -5
  10. package/.github/workflows/pwa-microservices-template-page.cd.yml +6 -7
  11. package/.github/workflows/pwa-microservices-template-test.ci.yml +4 -4
  12. package/.github/workflows/release.cd.yml +14 -8
  13. package/.vscode/extensions.json +9 -8
  14. package/.vscode/settings.json +3 -2
  15. package/CHANGELOG.md +643 -1
  16. package/CLI-HELP.md +132 -57
  17. package/Dockerfile +4 -2
  18. package/README.md +347 -22
  19. package/WHITE-PAPER.md +1540 -0
  20. package/bin/build.js +21 -12
  21. package/bin/cyberia.js +2640 -106
  22. package/bin/deploy.js +258 -372
  23. package/bin/file.js +5 -1
  24. package/bin/index.js +2640 -106
  25. package/bin/vs.js +3 -3
  26. package/conf.js +169 -105
  27. package/deployment.yaml +236 -20
  28. package/hardhat/.env.example +31 -0
  29. package/hardhat/README.md +531 -0
  30. package/hardhat/WHITE-PAPER.md +1540 -0
  31. package/hardhat/contracts/ObjectLayerToken.sol +391 -0
  32. package/hardhat/deployments/.gitkeep +0 -0
  33. package/hardhat/deployments/hardhat-ObjectLayerToken.json +11 -0
  34. package/hardhat/hardhat.config.js +136 -0
  35. package/hardhat/ignition/modules/ObjectLayerToken.js +21 -0
  36. package/hardhat/networks/besu-object-layer.network.json +138 -0
  37. package/hardhat/package-lock.json +4323 -0
  38. package/hardhat/package.json +36 -0
  39. package/hardhat/scripts/deployObjectLayerToken.js +98 -0
  40. package/hardhat/test/ObjectLayerToken.js +592 -0
  41. package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
  42. package/hardhat/types/ethers-contracts/common.ts +92 -0
  43. package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
  44. package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
  45. package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
  46. package/hardhat/types/ethers-contracts/index.ts +6 -0
  47. package/jsdoc.dd-cyberia.json +68 -0
  48. package/jsdoc.json +65 -49
  49. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -4
  50. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -4
  51. package/manifests/deployment/dd-cyberia-development/deployment.yaml +562 -0
  52. package/manifests/deployment/dd-cyberia-development/proxy.yaml +297 -0
  53. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +132 -0
  54. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  55. package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
  56. package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
  57. package/manifests/deployment/playwright/deployment.yaml +1 -1
  58. package/manifests/pv-pvc-dd.yaml +1 -1
  59. package/nodemon.json +1 -1
  60. package/package.json +60 -48
  61. package/proxy.yaml +118 -10
  62. package/pv-pvc.yaml +132 -0
  63. package/scripts/k3s-node-setup.sh +1 -1
  64. package/scripts/ports-ls.sh +2 -0
  65. package/scripts/rhel-grpc-setup.sh +56 -0
  66. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +47 -1
  67. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +17 -2
  68. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  69. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +80 -7
  70. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +93 -0
  71. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
  72. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +29 -0
  73. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +51 -0
  74. package/src/api/cyberia-entity/cyberia-entity.controller.js +74 -0
  75. package/src/api/cyberia-entity/cyberia-entity.model.js +24 -0
  76. package/src/api/cyberia-entity/cyberia-entity.router.js +27 -0
  77. package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
  78. package/src/api/cyberia-instance/cyberia-fallback-world.js +368 -0
  79. package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
  80. package/src/api/cyberia-instance/cyberia-instance.model.js +84 -0
  81. package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
  82. package/src/api/cyberia-instance/cyberia-instance.service.js +191 -0
  83. package/src/api/cyberia-instance/cyberia-portal-connector.js +486 -0
  84. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +74 -0
  85. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +413 -0
  86. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +228 -0
  87. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +27 -0
  88. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +42 -0
  89. package/src/api/cyberia-map/cyberia-map.controller.js +79 -0
  90. package/src/api/cyberia-map/cyberia-map.model.js +30 -0
  91. package/src/api/cyberia-map/cyberia-map.router.js +40 -0
  92. package/src/api/cyberia-map/cyberia-map.service.js +74 -0
  93. package/src/api/document/document.service.js +1 -1
  94. package/src/api/file/file.controller.js +3 -1
  95. package/src/api/file/file.ref.json +18 -0
  96. package/src/api/file/file.service.js +28 -5
  97. package/src/api/ipfs/ipfs.controller.js +4 -25
  98. package/src/api/ipfs/ipfs.model.js +43 -34
  99. package/src/api/ipfs/ipfs.router.js +8 -13
  100. package/src/api/ipfs/ipfs.service.js +56 -104
  101. package/src/api/object-layer/README.md +347 -22
  102. package/src/api/object-layer/object-layer.controller.js +6 -2
  103. package/src/api/object-layer/object-layer.model.js +12 -8
  104. package/src/api/object-layer/object-layer.router.js +698 -42
  105. package/src/api/object-layer/object-layer.service.js +119 -37
  106. package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +1 -2
  107. package/src/api/user/user.router.js +10 -5
  108. package/src/api/user/user.service.js +15 -14
  109. package/src/cli/baremetal.js +6 -10
  110. package/src/cli/cloud-init.js +0 -3
  111. package/src/cli/cluster.js +7 -7
  112. package/src/cli/db.js +723 -857
  113. package/src/cli/deploy.js +215 -105
  114. package/src/cli/env.js +34 -5
  115. package/src/cli/fs.js +5 -4
  116. package/src/cli/image.js +0 -3
  117. package/src/cli/index.js +83 -15
  118. package/src/cli/kubectl.js +211 -0
  119. package/src/cli/monitor.js +5 -6
  120. package/src/cli/release.js +284 -0
  121. package/src/cli/repository.js +708 -62
  122. package/src/cli/run.js +371 -151
  123. package/src/cli/secrets.js +73 -2
  124. package/src/cli/ssh.js +1 -1
  125. package/src/cli/test.js +3 -3
  126. package/src/client/Cryptokoyn.index.js +3 -4
  127. package/src/client/CyberiaPortal.index.js +3 -4
  128. package/src/client/Default.index.js +3 -4
  129. package/src/client/Itemledger.index.js +4 -963
  130. package/src/client/Underpost.index.js +3 -4
  131. package/src/client/components/core/AgGrid.js +20 -5
  132. package/src/client/components/core/Alert.js +2 -2
  133. package/src/client/components/core/AppStore.js +69 -0
  134. package/src/client/components/core/CalendarCore.js +2 -2
  135. package/src/client/components/core/Content.js +22 -3
  136. package/src/client/components/core/Docs.js +30 -6
  137. package/src/client/components/core/DropDown.js +137 -17
  138. package/src/client/components/core/FileExplorer.js +71 -4
  139. package/src/client/components/core/Input.js +1 -1
  140. package/src/client/components/core/Keyboard.js +2 -2
  141. package/src/client/components/core/LogIn.js +2 -2
  142. package/src/client/components/core/LogOut.js +2 -2
  143. package/src/client/components/core/Modal.js +20 -7
  144. package/src/client/components/core/Panel.js +0 -1
  145. package/src/client/components/core/PanelForm.js +19 -19
  146. package/src/client/components/core/RichText.js +1 -2
  147. package/src/client/components/core/SocketIo.js +82 -29
  148. package/src/client/components/core/SocketIoHandler.js +75 -0
  149. package/src/client/components/core/Stream.js +143 -95
  150. package/src/client/components/core/Webhook.js +40 -7
  151. package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
  152. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +3 -3
  153. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +2 -2
  154. package/src/client/components/cryptokoyn/MenuCryptokoyn.js +3 -3
  155. package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
  156. package/src/client/components/cyberia/InstanceEngineCyberia.js +700 -0
  157. package/src/client/components/cyberia/MapEngineCyberia.js +1359 -2
  158. package/src/client/components/cyberia/ObjectLayerEngineModal.js +17 -6
  159. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +92 -54
  160. package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
  161. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +217 -30
  162. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +44 -2
  163. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +3 -4
  164. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +2 -2
  165. package/src/client/components/cyberia-portal/MenuCyberiaPortal.js +104 -9
  166. package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +5 -0
  167. package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
  168. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  169. package/src/client/components/default/AppStoreDefault.js +5 -0
  170. package/src/client/components/default/LogInDefault.js +3 -3
  171. package/src/client/components/default/LogOutDefault.js +2 -2
  172. package/src/client/components/default/MenuDefault.js +5 -5
  173. package/src/client/components/default/SocketIoDefault.js +3 -51
  174. package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
  175. package/src/client/components/itemledger/LogInItemledger.js +3 -3
  176. package/src/client/components/itemledger/LogOutItemledger.js +2 -2
  177. package/src/client/components/itemledger/MenuItemledger.js +3 -3
  178. package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
  179. package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
  180. package/src/client/components/underpost/CssUnderpost.js +59 -0
  181. package/src/client/components/underpost/LogInUnderpost.js +6 -3
  182. package/src/client/components/underpost/LogOutUnderpost.js +4 -2
  183. package/src/client/components/underpost/MenuUnderpost.js +104 -18
  184. package/src/client/components/underpost/RoutesUnderpost.js +2 -0
  185. package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
  186. package/src/client/public/cryptokoyn/assets/logo/base-icon.png +0 -0
  187. package/src/client/public/cryptokoyn/browserconfig.xml +12 -0
  188. package/src/client/public/cryptokoyn/microdata.json +85 -0
  189. package/src/client/public/cryptokoyn/site.webmanifest +57 -0
  190. package/src/client/public/cryptokoyn/sitemap +3 -3
  191. package/src/client/public/default/sitemap +3 -3
  192. package/src/client/public/itemledger/browserconfig.xml +2 -2
  193. package/src/client/public/itemledger/manifest.webmanifest +4 -4
  194. package/src/client/public/itemledger/microdata.json +71 -0
  195. package/src/client/public/itemledger/sitemap +3 -3
  196. package/src/client/public/itemledger/yandex-browser-manifest.json +2 -2
  197. package/src/client/public/test/sitemap +3 -3
  198. package/src/client/services/core/core.service.js +20 -8
  199. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +105 -0
  200. package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
  201. package/src/client/services/cyberia-entity/cyberia-entity.service.js +105 -0
  202. package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
  203. package/src/client/services/cyberia-instance/cyberia-instance.service.js +122 -0
  204. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +105 -0
  205. package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
  206. package/src/client/services/cyberia-map/cyberia-map.service.js +126 -0
  207. package/src/client/services/instance/instance.management.js +2 -2
  208. package/src/client/services/ipfs/ipfs.service.js +3 -23
  209. package/src/client/services/object-layer/object-layer.management.js +3 -3
  210. package/src/client/services/object-layer/object-layer.service.js +21 -0
  211. package/src/client/services/user/user.management.js +2 -2
  212. package/src/client/ssr/body/404.js +15 -11
  213. package/src/client/ssr/body/500.js +15 -11
  214. package/src/client/ssr/body/SwaggerDarkMode.js +285 -0
  215. package/src/client/ssr/head/PwaItemledger.js +60 -0
  216. package/src/client/ssr/offline/NoNetworkConnection.js +11 -10
  217. package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
  218. package/src/client/ssr/pages/Test.js +11 -10
  219. package/src/client.build.js +0 -3
  220. package/src/client.dev.js +0 -3
  221. package/src/db/DataBaseProvider.js +17 -2
  222. package/src/db/mariadb/MariaDB.js +14 -9
  223. package/src/db/mongo/MongooseDB.js +17 -1
  224. package/src/grpc/cyberia/OFF_CHAIN_ECONOMY.md +305 -0
  225. package/src/grpc/cyberia/README.md +326 -0
  226. package/src/grpc/cyberia/grpc-server.js +530 -0
  227. package/src/index.js +24 -1
  228. package/src/proxy.js +0 -3
  229. package/src/runtime/express/Dockerfile +4 -0
  230. package/src/runtime/express/Express.js +33 -10
  231. package/src/runtime/lampp/Dockerfile +13 -2
  232. package/src/runtime/lampp/Lampp.js +33 -17
  233. package/src/runtime/wp/Dockerfile +68 -0
  234. package/src/runtime/wp/Wp.js +639 -0
  235. package/src/server/auth.js +36 -15
  236. package/src/server/backup.js +39 -12
  237. package/src/server/besu-genesis-generator.js +1630 -0
  238. package/src/server/client-build-docs.js +133 -17
  239. package/src/server/client-build-live.js +9 -18
  240. package/src/server/client-build.js +229 -101
  241. package/src/server/client-dev-server.js +14 -13
  242. package/src/server/client-formatted.js +109 -57
  243. package/src/server/conf.js +391 -164
  244. package/src/server/cron.js +27 -24
  245. package/src/server/dns.js +29 -12
  246. package/src/server/downloader.js +0 -2
  247. package/src/server/ipfs-client.js +24 -1
  248. package/src/server/logger.js +27 -9
  249. package/src/server/object-layer.js +217 -103
  250. package/src/server/peer.js +8 -2
  251. package/src/server/process.js +1 -50
  252. package/src/server/proxy.js +4 -8
  253. package/src/server/runtime.js +30 -9
  254. package/src/server/semantic-layer-generator-floor.js +359 -0
  255. package/src/server/semantic-layer-generator-skin.js +1294 -0
  256. package/src/server/semantic-layer-generator.js +116 -555
  257. package/src/server/ssr.js +0 -3
  258. package/src/server/start.js +19 -12
  259. package/src/server/tls.js +0 -2
  260. package/src/server.js +0 -4
  261. package/src/ws/IoInterface.js +1 -10
  262. package/src/ws/IoServer.js +14 -33
  263. package/src/ws/core/channels/core.ws.chat.js +65 -20
  264. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  265. package/src/ws/core/channels/core.ws.stream.js +90 -31
  266. package/src/ws/core/core.ws.connection.js +12 -33
  267. package/src/ws/core/core.ws.emit.js +10 -26
  268. package/src/ws/core/core.ws.server.js +25 -58
  269. package/src/ws/default/channels/default.ws.main.js +53 -12
  270. package/src/ws/default/default.ws.connection.js +26 -13
  271. package/src/ws/default/default.ws.server.js +30 -12
  272. package/.env.development +0 -43
  273. package/.env.test +0 -43
  274. package/hardhat/contracts/CryptoKoyn.sol +0 -59
  275. package/hardhat/contracts/ItemLedger.sol +0 -73
  276. package/hardhat/contracts/Lock.sol +0 -34
  277. package/hardhat/hardhat.config.cjs +0 -45
  278. package/hardhat/ignition/modules/Lock.js +0 -18
  279. package/hardhat/networks/cryptokoyn-itemledger.network.json +0 -29
  280. package/hardhat/scripts/deployCryptokoyn.cjs +0 -25
  281. package/hardhat/scripts/deployItemledger.cjs +0 -25
  282. package/hardhat/test/Lock.js +0 -126
  283. package/hardhat/white-paper.md +0 -581
  284. package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
  285. package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
  286. package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
  287. package/src/client/components/default/ElementsDefault.js +0 -38
  288. package/src/client/components/itemledger/CommonItemledger.js +0 -29
  289. package/src/client/components/itemledger/ElementsItemledger.js +0 -38
  290. package/src/client/components/underpost/CommonUnderpost.js +0 -29
  291. package/src/client/components/underpost/ElementsUnderpost.js +0 -38
  292. package/src/ws/core/management/core.ws.chat.js +0 -8
  293. package/src/ws/core/management/core.ws.mailer.js +0 -16
  294. package/src/ws/core/management/core.ws.stream.js +0 -8
  295. package/src/ws/default/management/default.ws.main.js +0 -8
  296. package/white-paper.md +0 -581
@@ -0,0 +1,191 @@
1
+ import { DataBaseProvider } from '../../db/DataBaseProvider.js';
2
+ import { loggerFactory } from '../../server/logger.js';
3
+ import { DataQuery } from '../../server/data-query.js';
4
+ import { connectPortals, generateProceduralEntities } from './cyberia-portal-connector.js';
5
+ import { generateFallbackWorld } from './cyberia-fallback-world.js';
6
+ import { CYBERIA_INSTANCE_CONF_DEFAULTS } from '../cyberia-instance-conf/cyberia-instance-conf.defaults.js';
7
+
8
+ const logger = loggerFactory(import.meta);
9
+
10
+ const CyberiaInstanceService = {
11
+ post: async (req, res, options) => {
12
+ /** @type {import('./cyberia-instance.model.js').CyberiaInstanceModel} */
13
+ const CyberiaInstance = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstance;
14
+ const CyberiaInstanceConf =
15
+ DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstanceConf;
16
+ if (req.auth && req.auth.user) req.body.creator = req.auth.user._id;
17
+ const instance = await new CyberiaInstance(req.body).save();
18
+
19
+ // Auto-upsert a CyberiaInstanceConf for this instance using schema defaults.
20
+ // $setOnInsert ensures existing conf documents are never overwritten.
21
+ if (instance.code && CyberiaInstanceConf) {
22
+ try {
23
+ const conf = await CyberiaInstanceConf.findOneAndUpdate(
24
+ { instanceCode: instance.code },
25
+ { $setOnInsert: { instanceCode: instance.code } },
26
+ { upsert: true, new: true },
27
+ );
28
+ if (conf && !instance.conf) {
29
+ await CyberiaInstance.findByIdAndUpdate(instance._id, { conf: conf._id });
30
+ instance.conf = conf._id;
31
+ }
32
+ } catch (e) {
33
+ logger.error('auto-upsert CyberiaInstanceConf failed:', e);
34
+ }
35
+ }
36
+
37
+ return instance;
38
+ },
39
+ get: async (req, res, options) => {
40
+ /** @type {import('./cyberia-instance.model.js').CyberiaInstanceModel} */
41
+ const CyberiaInstance = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstance;
42
+ const populateCreator = { path: 'creator', model: 'User', select: '_id username' };
43
+ if (req.params.id) return await CyberiaInstance.findById(req.params.id).populate(populateCreator);
44
+
45
+ // Parse query parameters using DataQuery helper
46
+ const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
47
+
48
+ const [data, total] = await Promise.all([
49
+ CyberiaInstance.find(query).sort(sort).limit(limit).skip(skip).populate(populateCreator),
50
+ CyberiaInstance.countDocuments(query),
51
+ ]);
52
+
53
+ const totalPages = Math.ceil(total / limit);
54
+ return { data, total, page, totalPages };
55
+ },
56
+ put: async (req, res, options) => {
57
+ /** @type {import('./cyberia-instance.model.js').CyberiaInstanceModel} */
58
+ const CyberiaInstance = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstance;
59
+ const instance = await CyberiaInstance.findById(req.params.id);
60
+ if (!instance) throw new Error('instance not found');
61
+ if (req.auth.user.role !== 'admin' && String(instance.creator) !== String(req.auth.user._id))
62
+ throw new Error('insufficient permission');
63
+ if (req.body.thumbnail && instance.thumbnail && String(req.body.thumbnail) !== String(instance.thumbnail)) {
64
+ const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
65
+ await File.findByIdAndDelete(instance.thumbnail);
66
+ }
67
+ return await CyberiaInstance.findByIdAndUpdate(req.params.id, req.body, { returnDocument: 'after' });
68
+ },
69
+ /**
70
+ * Central portal connector endpoint.
71
+ *
72
+ * Delegates topology computation to the pure-function `connectPortals()`
73
+ * from cyberia-portal-connector.js so the same logic can be used by the
74
+ * GUI without a DB dependency.
75
+ *
76
+ * Optionally generates procedural fallback obstacle/foreground entities
77
+ * for maps that have none, controlled by query flags:
78
+ * ?generateEntities=true — append procedural obstacles & foreground
79
+ * ?obstacleCount=N — obstacles per map (default 5)
80
+ * ?foregroundCount=N — foreground per map (default 3)
81
+ * ?persist=true — save generated portals & entities to DB
82
+ */
83
+ portalConnect: async (req, res, options) => {
84
+ const CyberiaInstance = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstance;
85
+ const CyberiaMap = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaMap;
86
+
87
+ const instance = await CyberiaInstance.findById(req.params.id).lean();
88
+ if (!instance) throw new Error('instance not found');
89
+
90
+ const mapCodes = instance.cyberiaMapCodes || [];
91
+
92
+ // Load maps with the fields needed by the connector.
93
+ const mapDocs = await CyberiaMap.find(
94
+ { code: { $in: mapCodes } },
95
+ {
96
+ code: 1,
97
+ gridX: 1,
98
+ gridY: 1,
99
+ entities: 1,
100
+ },
101
+ ).lean();
102
+
103
+ // ── Portal topology (pure function) ──────────────────────────────────
104
+ const result = connectPortals(mapCodes, mapDocs);
105
+
106
+ // ── Procedural entity generation (optional) ──────────────────────────
107
+ const colors = CYBERIA_INSTANCE_CONF_DEFAULTS.colors;
108
+ const wantEntities = req.query?.generateEntities === 'true';
109
+ const obstacleCount = req.query?.obstacleCount ? parseInt(req.query.obstacleCount, 10) : undefined;
110
+ const foregroundCount = req.query?.foregroundCount ? parseInt(req.query.foregroundCount, 10) : undefined;
111
+ const seed = instance.seed || '';
112
+
113
+ const generatedEntities = {};
114
+ if (wantEntities) {
115
+ for (const doc of mapDocs) {
116
+ const hasObstacles = (doc.entities || []).some((e) => e.entityType === 'obstacle');
117
+ const hasForeground = (doc.entities || []).some((e) => e.entityType === 'foreground');
118
+ if (!hasObstacles || !hasForeground) {
119
+ const mapSeed = seed ? `${seed}:${doc.code}` : doc.code;
120
+ const generated = generateProceduralEntities({ gridX: doc.gridX || 16, gridY: doc.gridY || 16 }, colors, {
121
+ obstacleCount,
122
+ foregroundCount,
123
+ seed: mapSeed,
124
+ });
125
+ generatedEntities[doc.code] = {
126
+ obstacles: hasObstacles ? [] : generated.obstacles,
127
+ foreground: hasForeground ? [] : generated.foreground,
128
+ };
129
+ }
130
+ }
131
+ }
132
+
133
+ // ── Persist to DB when requested ─────────────────────────────────────
134
+ const persist = req.query?.persist === 'true';
135
+ if (persist) {
136
+ await CyberiaInstance.findByIdAndUpdate(req.params.id, { portals: result.portals });
137
+ for (const [mapCode, ents] of Object.entries(generatedEntities)) {
138
+ const toAdd = [...ents.obstacles, ...ents.foreground];
139
+ if (toAdd.length > 0) {
140
+ await CyberiaMap.findOneAndUpdate({ code: mapCode }, { $push: { entities: { $each: toAdd } } });
141
+ }
142
+ }
143
+ }
144
+
145
+ return {
146
+ ...result,
147
+ ...(wantEntities ? { generatedEntities } : {}),
148
+ persisted: persist,
149
+ };
150
+ },
151
+
152
+ delete: async (req, res, options) => {
153
+ /** @type {import('./cyberia-instance.model.js').CyberiaInstanceModel} */
154
+ const CyberiaInstance = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstance;
155
+ if (req.params.id) {
156
+ const instance = await CyberiaInstance.findById(req.params.id);
157
+ if (!instance) throw new Error('instance not found');
158
+ if (req.auth.user.role !== 'admin' && String(instance.creator) !== String(req.auth.user._id))
159
+ throw new Error('insufficient permission');
160
+ if (instance.thumbnail) {
161
+ const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
162
+ await File.findByIdAndDelete(instance.thumbnail);
163
+ }
164
+ return await CyberiaInstance.findByIdAndDelete(req.params.id);
165
+ } else return await CyberiaInstance.deleteMany();
166
+ },
167
+
168
+ /**
169
+ * Return an in-memory procedural fallback world.
170
+ *
171
+ * Nothing is persisted to MongoDB. The world is regenerated on every
172
+ * call but stays deterministic for a given seed.
173
+ *
174
+ * Query params:
175
+ * ?mapCount=<number> — maps to generate (default: 4)
176
+ * ?botCount=<number> — bots per map (random 8–16 if omitted)
177
+ * ?obstacleCount=<number> — obstacles per map (random 12–20 if omitted)
178
+ * ?foregroundCount=<number>— foreground per map (random 6–12 if omitted)
179
+ */
180
+ fallbackWorld: async (req) => {
181
+ const q = req.query || {};
182
+ return generateFallbackWorld({
183
+ mapCount: q.mapCount ? parseInt(q.mapCount, 10) : undefined,
184
+ botCount: q.botCount ? parseInt(q.botCount, 10) : undefined,
185
+ obstacleCount: q.obstacleCount ? parseInt(q.obstacleCount, 10) : undefined,
186
+ foregroundCount: q.foregroundCount ? parseInt(q.foregroundCount, 10) : undefined,
187
+ });
188
+ },
189
+ };
190
+
191
+ export { CyberiaInstanceService };
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Central Portal Connector — pure-function module.
3
+ *
4
+ * Shared by the backend (CyberiaInstanceService) and the GUI (map editor)
5
+ * to build, validate, and procedurally generate portal topology and world
6
+ * entities for a CyberiaInstance.
7
+ *
8
+ * All exported functions are stateless and synchronous — they operate on
9
+ * plain JS objects (lean Mongoose docs or JSON from the API) so the GUI
10
+ * can call them directly without a DB dependency.
11
+ *
12
+ * @module src/api/cyberia-instance/cyberia-portal-connector
13
+ */
14
+
15
+ // ── Color helpers ────────────────────────────────────────────────────────────
16
+
17
+ /**
18
+ * Convert a { r, g, b, a } palette entry to an `rgba(…)` CSS string.
19
+ * @param {{ r: number, g: number, b: number, a: number }} c
20
+ * @returns {string}
21
+ */
22
+ const colorToRgba = (c) => `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a / 255})`;
23
+
24
+ /**
25
+ * Look up a palette entry by key from a colours array.
26
+ * @param {Array<{ key: string, r: number, g: number, b: number, a: number }>} colors
27
+ * @param {string} key
28
+ * @returns {{ r: number, g: number, b: number, a: number } | undefined}
29
+ */
30
+ const findColor = (colors, key) => colors.find((c) => c.key === key);
31
+
32
+ // ── Random helpers ───────────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Return a random integer in [min, max] (inclusive).
36
+ * @param {number} min
37
+ * @param {number} max
38
+ * @returns {number}
39
+ */
40
+ const randInt = (min, max) => min + Math.floor(Math.random() * (max - min + 1));
41
+
42
+ // ── Occupancy grid ───────────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * 2D boolean grid that tracks which cells are blocked (obstacle / placed entity).
46
+ * Used to find valid walkable positions when placing portals and bots.
47
+ */
48
+ class OccupancyGrid {
49
+ /**
50
+ * @param {number} width Grid columns.
51
+ * @param {number} height Grid rows.
52
+ */
53
+ constructor(width, height) {
54
+ this.width = width;
55
+ this.height = height;
56
+ // false = walkable, true = blocked
57
+ this.cells = Array.from({ length: height }, () => new Array(width).fill(false));
58
+ }
59
+
60
+ /**
61
+ * Mark a rectangular region as blocked.
62
+ * @param {number} x
63
+ * @param {number} y
64
+ * @param {number} w
65
+ * @param {number} h
66
+ */
67
+ block(x, y, w, h) {
68
+ for (let row = y; row < y + h && row < this.height; row++) {
69
+ for (let col = x; col < x + w && col < this.width; col++) {
70
+ if (row >= 0 && col >= 0) this.cells[row][col] = true;
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Check whether a rectangle fits entirely within walkable (unblocked) cells.
77
+ * @param {number} x
78
+ * @param {number} y
79
+ * @param {number} w
80
+ * @param {number} h
81
+ * @returns {boolean}
82
+ */
83
+ fits(x, y, w, h) {
84
+ if (x < 0 || y < 0 || x + w > this.width || y + h > this.height) return false;
85
+ for (let row = y; row < y + h; row++) {
86
+ for (let col = x; col < x + w; col++) {
87
+ if (this.cells[row][col]) return false;
88
+ }
89
+ }
90
+ return true;
91
+ }
92
+
93
+ /**
94
+ * Find a random walkable position for a rectangle of given dimensions.
95
+ * Tries up to `maxAttempts` random positions before giving up.
96
+ * @param {number} w
97
+ * @param {number} h
98
+ * @param {number} [maxAttempts=200]
99
+ * @returns {{ x: number, y: number } | null} Position or null if no fit found.
100
+ */
101
+ findPosition(w, h, maxAttempts = 200) {
102
+ const maxX = Math.max(0, this.width - w);
103
+ const maxY = Math.max(0, this.height - h);
104
+ for (let i = 0; i < maxAttempts; i++) {
105
+ const x = randInt(0, maxX);
106
+ const y = randInt(0, maxY);
107
+ if (this.fits(x, y, w, h)) return { x, y };
108
+ }
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * Populate the grid from an array of obstacle entities.
114
+ * @param {Array<{ initCellX: number, initCellY: number, dimX: number, dimY: number }>} obstacles
115
+ */
116
+ addObstacles(obstacles) {
117
+ for (const o of obstacles) {
118
+ this.block(o.initCellX, o.initCellY, o.dimX, o.dimY);
119
+ }
120
+ }
121
+ }
122
+
123
+ // ── Portal topology builders ─────────────────────────────────────────────────
124
+
125
+ /**
126
+ * Canonical portal mode strings.
127
+ * @enum {string}
128
+ */
129
+ const PORTAL_MODES = Object.freeze({
130
+ INTER_PORTAL: 'inter-portal', // teleport to a portal on another map
131
+ INTER_RANDOM: 'inter-random', // teleport to a random spot on another map
132
+ INTRA_RANDOM: 'intra-random', // teleport to a random spot on the same map
133
+ INTRA_PORTAL: 'intra-portal', // teleport to a portal on the same map
134
+ });
135
+
136
+ /**
137
+ * All portal mode values as an array (for random selection).
138
+ * @type {string[]}
139
+ */
140
+ const PORTAL_MODE_LIST = Object.values(PORTAL_MODES);
141
+
142
+ /**
143
+ * Map from portal mode to its palette colour key.
144
+ * @type {Record<string, string>}
145
+ */
146
+ const PORTAL_MODE_COLOR_KEY = Object.freeze({
147
+ [PORTAL_MODES.INTER_PORTAL]: 'PORTAL_INTER_PORTAL',
148
+ [PORTAL_MODES.INTER_RANDOM]: 'PORTAL_INTER_RANDOM',
149
+ [PORTAL_MODES.INTRA_RANDOM]: 'PORTAL_INTRA_RANDOM',
150
+ [PORTAL_MODES.INTRA_PORTAL]: 'PORTAL_INTRA_PORTAL',
151
+ });
152
+
153
+ /**
154
+ * Portal modes available for extra (non-ring) portals.
155
+ * The ring always uses INTER_PORTAL; extras are randomly chosen from these.
156
+ * @type {string[]}
157
+ */
158
+ const EXTRA_PORTAL_MODES = [PORTAL_MODES.INTRA_PORTAL, PORTAL_MODES.INTRA_RANDOM, PORTAL_MODES.INTER_RANDOM];
159
+
160
+ /**
161
+ * Extract all portal-type entities from each map document and build
162
+ * a lookup: `{ [mapCode]: portalEntity[] }`.
163
+ *
164
+ * @param {Array<{ code: string, entities: Array<{ entityType: string, portalSubtype?: string, initCellX: number, initCellY: number }> }>} maps
165
+ * @returns {Record<string, object[]>}
166
+ */
167
+ function indexPortalEntities(maps) {
168
+ const idx = {};
169
+ for (const map of maps) {
170
+ idx[map.code] = (map.entities || []).filter((e) => e.entityType === 'portal');
171
+ }
172
+ return idx;
173
+ }
174
+
175
+ /**
176
+ * Build portal edges from a set of maps with portal entities.
177
+ *
178
+ * Phase 1 — **Ring guarantee**: creates an inter-portal ring that
179
+ * connects every map in a circle (0→1→2→…→n-1→0) so that every map
180
+ * is reachable from every other map. One portal entity per map is
181
+ * consumed for the ring.
182
+ *
183
+ * Phase 2 — **Extra edges**: remaining portal entities (those not used
184
+ * in the ring) produce edges according to their `portalSubtype`:
185
+ * inter-portal → portal on a DIFFERENT map
186
+ * inter-random → random pos on a DIFFERENT map
187
+ * intra-random → random pos on the SAME map
188
+ * intra-portal → portal on the SAME map
189
+ *
190
+ * @param {string[]} orderedCodes Map codes in instance order.
191
+ * @param {Record<string, object[]>} portalIndex From `indexPortalEntities`.
192
+ * @returns {{ portals: object[], topology: string }}
193
+ */
194
+ function buildTopologyFromSubtypes(orderedCodes, portalIndex) {
195
+ const n = orderedCodes.length;
196
+ if (n < 1) return { portals: [], topology: 'none' };
197
+
198
+ const portals = [];
199
+ const usedInRing = new Set();
200
+
201
+ // ── Phase 1: Guaranteed inter-portal ring ───────────────────────────
202
+ // Each map links to the next in a circle: 0→1→2→…→(n-1)→0.
203
+ if (n >= 2) {
204
+ for (let i = 0; i < n; i++) {
205
+ const srcCode = orderedCodes[i];
206
+ const tgtCode = orderedCodes[(i + 1) % n];
207
+
208
+ // Prefer an unused inter-portal entity as source; fall back to any unused, then any
209
+ const srcAll = portalIndex[srcCode] || [];
210
+ const srcInterUnused = srcAll.filter(
211
+ (e) => (e.portalSubtype || PORTAL_MODES.INTER_PORTAL) === PORTAL_MODES.INTER_PORTAL && !usedInRing.has(e),
212
+ );
213
+ const srcAnyUnused = srcAll.filter((e) => !usedInRing.has(e));
214
+ const srcEnt = srcInterUnused[0] || srcAnyUnused[0] || srcAll[0];
215
+
216
+ // Target: pick any portal on the target map for landing coordinates
217
+ const tgtAll = portalIndex[tgtCode] || [];
218
+ const tgtEnt = tgtAll.length > 0 ? tgtAll[Math.floor(Math.random() * tgtAll.length)] : null;
219
+
220
+ if (srcEnt) {
221
+ usedInRing.add(srcEnt);
222
+ portals.push({
223
+ sourceMapCode: srcCode,
224
+ sourceCellX: srcEnt.initCellX ?? 0,
225
+ sourceCellY: srcEnt.initCellY ?? 0,
226
+ targetMapCode: tgtCode,
227
+ targetCellX: tgtEnt?.initCellX ?? 0,
228
+ targetCellY: tgtEnt?.initCellY ?? 0,
229
+ portalMode: PORTAL_MODES.INTER_PORTAL,
230
+ });
231
+ }
232
+ }
233
+ }
234
+
235
+ // ── Phase 2: Extra edges from remaining portals ─────────────────────
236
+ const otherMap = (srcCode) => {
237
+ if (n < 2) return srcCode;
238
+ let code;
239
+ do {
240
+ code = orderedCodes[Math.floor(Math.random() * n)];
241
+ } while (code === srcCode && n > 1);
242
+ return code;
243
+ };
244
+
245
+ for (const srcCode of orderedCodes) {
246
+ const allOnMap = portalIndex[srcCode] || [];
247
+ const remaining = allOnMap.filter((e) => !usedInRing.has(e));
248
+
249
+ for (const srcEnt of remaining) {
250
+ const sub = srcEnt.portalSubtype || PORTAL_MODES.INTER_PORTAL;
251
+
252
+ switch (sub) {
253
+ case PORTAL_MODES.INTER_PORTAL: {
254
+ const tgtCode = otherMap(srcCode);
255
+ const candidates = portalIndex[tgtCode] || [];
256
+ const tgtEnt = candidates.length > 0 ? candidates[Math.floor(Math.random() * candidates.length)] : null;
257
+ portals.push({
258
+ sourceMapCode: srcCode,
259
+ sourceCellX: srcEnt.initCellX ?? 0,
260
+ sourceCellY: srcEnt.initCellY ?? 0,
261
+ targetMapCode: tgtCode,
262
+ targetCellX: tgtEnt?.initCellX ?? 0,
263
+ targetCellY: tgtEnt?.initCellY ?? 0,
264
+ portalMode: PORTAL_MODES.INTER_PORTAL,
265
+ });
266
+ break;
267
+ }
268
+ case PORTAL_MODES.INTER_RANDOM: {
269
+ const tgtCode = otherMap(srcCode);
270
+ portals.push({
271
+ sourceMapCode: srcCode,
272
+ sourceCellX: srcEnt.initCellX ?? 0,
273
+ sourceCellY: srcEnt.initCellY ?? 0,
274
+ targetMapCode: tgtCode,
275
+ targetCellX: -1,
276
+ targetCellY: -1,
277
+ portalMode: PORTAL_MODES.INTER_RANDOM,
278
+ });
279
+ break;
280
+ }
281
+ case PORTAL_MODES.INTRA_RANDOM: {
282
+ portals.push({
283
+ sourceMapCode: srcCode,
284
+ sourceCellX: srcEnt.initCellX ?? 0,
285
+ sourceCellY: srcEnt.initCellY ?? 0,
286
+ targetMapCode: srcCode,
287
+ targetCellX: -1,
288
+ targetCellY: -1,
289
+ portalMode: PORTAL_MODES.INTRA_RANDOM,
290
+ });
291
+ break;
292
+ }
293
+ case PORTAL_MODES.INTRA_PORTAL: {
294
+ const candidates = allOnMap.filter(
295
+ (e) => e !== srcEnt && (e.initCellX !== srcEnt.initCellX || e.initCellY !== srcEnt.initCellY),
296
+ );
297
+ const tgtEnt = candidates.length > 0 ? candidates[Math.floor(Math.random() * candidates.length)] : null;
298
+ portals.push({
299
+ sourceMapCode: srcCode,
300
+ sourceCellX: srcEnt.initCellX ?? 0,
301
+ sourceCellY: srcEnt.initCellY ?? 0,
302
+ targetMapCode: srcCode,
303
+ targetCellX: tgtEnt?.initCellX ?? 0,
304
+ targetCellY: tgtEnt?.initCellY ?? 0,
305
+ portalMode: PORTAL_MODES.INTRA_PORTAL,
306
+ });
307
+ break;
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ return { portals, topology: n === 1 ? 'intra-only' : 'ring+mixed' };
314
+ }
315
+
316
+ /**
317
+ * Central portal-connect pipeline.
318
+ *
319
+ * Given an instance's ordered map codes and the full map documents (with
320
+ * entities), returns the auto-generated portal edge list.
321
+ *
322
+ * @param {string[]} mapCodes Instance's `cyberiaMapCodes` array.
323
+ * @param {Array<{ code: string, entities: object[] }>} maps Map documents (lean or JSON).
324
+ * @returns {{ portals: object[], topology: string, mapCount: number }}
325
+ */
326
+ function connectPortals(mapCodes, maps) {
327
+ if (!mapCodes || mapCodes.length < 1) {
328
+ return { portals: [], topology: 'none', mapCount: mapCodes?.length ?? 0, message: 'Need at least 1 map.' };
329
+ }
330
+
331
+ const portalIndex = indexPortalEntities(maps);
332
+
333
+ // Filter to codes that actually exist in the fetched maps.
334
+ const knownCodes = new Set(maps.map((m) => m.code));
335
+ const ordered = mapCodes.filter((c) => knownCodes.has(c));
336
+ if (ordered.length < 1) {
337
+ return { portals: [], topology: 'none', mapCount: ordered.length, message: 'Need at least 1 map.' };
338
+ }
339
+
340
+ const { portals, topology } = buildTopologyFromSubtypes(ordered, portalIndex);
341
+ return { portals, topology, mapCount: ordered.length };
342
+ }
343
+
344
+ // ── Procedural entity generators ─────────────────────────────────────────────
345
+
346
+ /**
347
+ * Generate procedural obstacle entities for a map.
348
+ *
349
+ * Obstacles use empty `objectLayerItemIds` so they render as a solid colour
350
+ * from the OBSTACLE palette entry. Count, dimensions, and positions are
351
+ * all fully random within the declared ranges.
352
+ *
353
+ * @param {{ gridX: number, gridY: number }} mapDims Map grid dimensions.
354
+ * @param {Array<{ key: string, r: number, g: number, b: number, a: number }>} colors Palette.
355
+ * @param {object} [opts]
356
+ * @param {number} [opts.count] Override count (ignores range).
357
+ * @param {number} [opts.minDim=1] Minimum obstacle width/height (cells).
358
+ * @param {number} [opts.maxDim=4] Maximum obstacle width/height (cells).
359
+ * @returns {object[]} Array of CyberiaEntity plain objects.
360
+ */
361
+ function generateObstacles(mapDims, colors, opts = {}) {
362
+ const { minDim = 1, maxDim = 4 } = opts;
363
+ const count = opts.count ?? randInt(OBSTACLE_RANGE[0], OBSTACLE_RANGE[1]);
364
+ const { gridX, gridY } = mapDims;
365
+
366
+ const obstacleColor = findColor(colors, 'OBSTACLE');
367
+ const rgba = obstacleColor ? colorToRgba(obstacleColor) : 'rgba(80, 80, 80, 1)';
368
+
369
+ const entities = [];
370
+ for (let i = 0; i < count; i++) {
371
+ const dimX = randInt(minDim, maxDim);
372
+ const dimY = randInt(minDim, maxDim);
373
+ const maxX = Math.max(0, gridX - dimX);
374
+ const maxY = Math.max(0, gridY - dimY);
375
+ entities.push({
376
+ entityType: 'obstacle',
377
+ initCellX: randInt(0, maxX),
378
+ initCellY: randInt(0, maxY),
379
+ dimX,
380
+ dimY,
381
+ color: rgba,
382
+ objectLayerItemIds: [],
383
+ });
384
+ }
385
+ return entities;
386
+ }
387
+
388
+ /**
389
+ * Generate procedural foreground entities for a map.
390
+ *
391
+ * Foregrounds use empty `objectLayerItemIds` and a semi-transparent colour
392
+ * from the FOREGROUND palette entry. Count, dimensions, and positions are
393
+ * all fully random within the declared ranges.
394
+ *
395
+ * @param {{ gridX: number, gridY: number }} mapDims Map grid dimensions.
396
+ * @param {Array<{ key: string, r: number, g: number, b: number, a: number }>} colors Palette.
397
+ * @param {object} [opts]
398
+ * @param {number} [opts.count] Override count (ignores range).
399
+ * @param {number} [opts.minDim=2] Minimum foreground width/height (cells).
400
+ * @param {number} [opts.maxDim=6] Maximum foreground width/height (cells).
401
+ * @returns {object[]} Array of CyberiaEntity plain objects.
402
+ */
403
+ function generateForeground(mapDims, colors, opts = {}) {
404
+ const { minDim = 2, maxDim = 6 } = opts;
405
+ const count = opts.count ?? randInt(FOREGROUND_RANGE[0], FOREGROUND_RANGE[1]);
406
+ const { gridX, gridY } = mapDims;
407
+
408
+ const fgColor = findColor(colors, 'FOREGROUND');
409
+ const rgba = fgColor ? colorToRgba(fgColor) : 'rgba(200, 200, 200, 0.31)';
410
+
411
+ const entities = [];
412
+ for (let i = 0; i < count; i++) {
413
+ const dimX = randInt(minDim, maxDim);
414
+ const dimY = randInt(minDim, maxDim);
415
+ const maxX = Math.max(0, gridX - dimX);
416
+ const maxY = Math.max(0, gridY - dimY);
417
+ entities.push({
418
+ entityType: 'foreground',
419
+ initCellX: randInt(0, maxX),
420
+ initCellY: randInt(0, maxY),
421
+ dimX,
422
+ dimY,
423
+ color: rgba,
424
+ objectLayerItemIds: [],
425
+ });
426
+ }
427
+ return entities;
428
+ }
429
+
430
+ /**
431
+ * Generate all procedural fallback entities (obstacles + foreground) for a map.
432
+ *
433
+ * @param {{ gridX: number, gridY: number }} mapDims
434
+ * @param {Array<{ key: string, r: number, g: number, b: number, a: number }>} colors
435
+ * @param {object} [opts]
436
+ * @param {number} [opts.obstacleCount]
437
+ * @param {number} [opts.foregroundCount]
438
+ * @returns {{ obstacles: object[], foreground: object[] }}
439
+ */
440
+ function generateProceduralEntities(mapDims, colors, opts = {}) {
441
+ return {
442
+ obstacles: generateObstacles(mapDims, colors, { count: opts.obstacleCount }),
443
+ foreground: generateForeground(mapDims, colors, { count: opts.foregroundCount }),
444
+ };
445
+ }
446
+
447
+ // ── Entity count ranges ──────────────────────────────────────────────────────
448
+ // [min, max] — actual count is random within range on each generation call.
449
+
450
+ const OBSTACLE_RANGE = [20, 35];
451
+ const FOREGROUND_RANGE = [10, 20];
452
+ const BOT_RANGE = [8, 16];
453
+ const BOT_WEAPON_CHANCE = 0.6;
454
+ const PORTAL_DIM_RANGE = [2, 3];
455
+ const PORTAL_COUNT_RANGE = [2, 4];
456
+
457
+ // ── Public API ───────────────────────────────────────────────────────────────
458
+
459
+ export {
460
+ // Portal topology
461
+ connectPortals,
462
+ buildTopologyFromSubtypes,
463
+ indexPortalEntities,
464
+ // Portal modes
465
+ PORTAL_MODES,
466
+ PORTAL_MODE_LIST,
467
+ PORTAL_MODE_COLOR_KEY,
468
+ EXTRA_PORTAL_MODES,
469
+ // Procedural entities
470
+ generateObstacles,
471
+ generateForeground,
472
+ generateProceduralEntities,
473
+ // Placement
474
+ OccupancyGrid,
475
+ // Helpers
476
+ colorToRgba,
477
+ findColor,
478
+ randInt,
479
+ // Ranges
480
+ OBSTACLE_RANGE,
481
+ FOREGROUND_RANGE,
482
+ BOT_RANGE,
483
+ BOT_WEAPON_CHANCE,
484
+ PORTAL_DIM_RANGE,
485
+ PORTAL_COUNT_RANGE,
486
+ };