cyberia 3.1.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 (208) hide show
  1. package/.env.example +0 -2
  2. package/.github/workflows/engine-cyberia.cd.yml +10 -8
  3. package/.github/workflows/engine-cyberia.ci.yml +12 -29
  4. package/.github/workflows/ghpkg.ci.yml +4 -4
  5. package/.github/workflows/npmpkg.ci.yml +28 -11
  6. package/.github/workflows/publish.ci.yml +21 -2
  7. package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
  8. package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
  9. package/.github/workflows/release.cd.yml +13 -8
  10. package/CHANGELOG.md +433 -1
  11. package/CLI-HELP.md +57 -7
  12. package/Dockerfile +4 -2
  13. package/README.md +347 -22
  14. package/bin/build.js +5 -2
  15. package/bin/cyberia.js +1789 -112
  16. package/bin/deploy.js +177 -124
  17. package/bin/file.js +3 -0
  18. package/bin/index.js +1789 -112
  19. package/conf.js +64 -8
  20. package/deployment.yaml +92 -20
  21. package/hardhat/hardhat.config.js +13 -13
  22. package/hardhat/ignition/modules/ObjectLayerToken.js +1 -1
  23. package/hardhat/package-lock.json +2554 -5859
  24. package/hardhat/package.json +13 -22
  25. package/hardhat/scripts/deployObjectLayerToken.js +1 -1
  26. package/hardhat/test/ObjectLayerToken.js +4 -2
  27. package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
  28. package/hardhat/types/ethers-contracts/common.ts +92 -0
  29. package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
  30. package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
  31. package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
  32. package/hardhat/types/ethers-contracts/index.ts +6 -0
  33. package/jsdoc.dd-cyberia.json +64 -55
  34. package/jsdoc.json +64 -55
  35. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -4
  36. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -4
  37. package/manifests/deployment/dd-cyberia-development/deployment.yaml +92 -20
  38. package/manifests/deployment/dd-cyberia-development/proxy.yaml +54 -18
  39. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  40. package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
  41. package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
  42. package/manifests/deployment/playwright/deployment.yaml +1 -1
  43. package/nodemon.json +1 -1
  44. package/package.json +22 -16
  45. package/proxy.yaml +54 -18
  46. package/scripts/rhel-grpc-setup.sh +56 -0
  47. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +44 -0
  48. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +16 -0
  49. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  50. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +80 -7
  51. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +93 -0
  52. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
  53. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +29 -0
  54. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +51 -0
  55. package/src/api/cyberia-entity/cyberia-entity.controller.js +74 -0
  56. package/src/api/cyberia-entity/cyberia-entity.model.js +24 -0
  57. package/src/api/cyberia-entity/cyberia-entity.router.js +27 -0
  58. package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
  59. package/src/api/cyberia-instance/cyberia-fallback-world.js +368 -0
  60. package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
  61. package/src/api/cyberia-instance/cyberia-instance.model.js +84 -0
  62. package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
  63. package/src/api/cyberia-instance/cyberia-instance.service.js +191 -0
  64. package/src/api/cyberia-instance/cyberia-portal-connector.js +486 -0
  65. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +74 -0
  66. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +413 -0
  67. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +228 -0
  68. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +27 -0
  69. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +42 -0
  70. package/src/api/cyberia-map/cyberia-map.controller.js +79 -0
  71. package/src/api/cyberia-map/cyberia-map.model.js +30 -0
  72. package/src/api/cyberia-map/cyberia-map.router.js +40 -0
  73. package/src/api/cyberia-map/cyberia-map.service.js +74 -0
  74. package/src/api/file/file.ref.json +18 -0
  75. package/src/api/ipfs/ipfs.controller.js +4 -25
  76. package/src/api/ipfs/ipfs.model.js +43 -34
  77. package/src/api/ipfs/ipfs.router.js +8 -13
  78. package/src/api/ipfs/ipfs.service.js +54 -102
  79. package/src/api/object-layer/README.md +347 -22
  80. package/src/api/object-layer/object-layer.router.js +30 -0
  81. package/src/api/object-layer/object-layer.service.js +114 -31
  82. package/src/api/user/user.service.js +8 -7
  83. package/src/cli/cluster.js +7 -7
  84. package/src/cli/db.js +710 -827
  85. package/src/cli/deploy.js +151 -93
  86. package/src/cli/env.js +29 -0
  87. package/src/cli/fs.js +5 -2
  88. package/src/cli/index.js +48 -2
  89. package/src/cli/kubectl.js +211 -0
  90. package/src/cli/release.js +284 -0
  91. package/src/cli/repository.js +438 -75
  92. package/src/cli/run.js +195 -35
  93. package/src/cli/secrets.js +73 -0
  94. package/src/cli/test.js +3 -3
  95. package/src/client/Cryptokoyn.index.js +3 -4
  96. package/src/client/CyberiaPortal.index.js +3 -4
  97. package/src/client/Default.index.js +3 -4
  98. package/src/client/Itemledger.index.js +3 -4
  99. package/src/client/Underpost.index.js +3 -4
  100. package/src/client/components/core/AppStore.js +69 -0
  101. package/src/client/components/core/CalendarCore.js +2 -2
  102. package/src/client/components/core/DropDown.js +137 -17
  103. package/src/client/components/core/Keyboard.js +2 -2
  104. package/src/client/components/core/LogIn.js +2 -2
  105. package/src/client/components/core/LogOut.js +2 -2
  106. package/src/client/components/core/Modal.js +0 -1
  107. package/src/client/components/core/Panel.js +0 -1
  108. package/src/client/components/core/PanelForm.js +19 -19
  109. package/src/client/components/core/SocketIo.js +82 -29
  110. package/src/client/components/core/SocketIoHandler.js +75 -0
  111. package/src/client/components/core/Stream.js +143 -95
  112. package/src/client/components/core/Webhook.js +40 -7
  113. package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
  114. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +3 -3
  115. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +2 -2
  116. package/src/client/components/cryptokoyn/MenuCryptokoyn.js +3 -3
  117. package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
  118. package/src/client/components/cyberia/InstanceEngineCyberia.js +700 -0
  119. package/src/client/components/cyberia/MapEngineCyberia.js +1359 -2
  120. package/src/client/components/cyberia/ObjectLayerEngineModal.js +17 -6
  121. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +92 -54
  122. package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
  123. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +216 -30
  124. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +3 -3
  125. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +2 -2
  126. package/src/client/components/cyberia-portal/MenuCyberiaPortal.js +40 -7
  127. package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +4 -0
  128. package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
  129. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  130. package/src/client/components/default/AppStoreDefault.js +5 -0
  131. package/src/client/components/default/LogInDefault.js +3 -3
  132. package/src/client/components/default/LogOutDefault.js +2 -2
  133. package/src/client/components/default/MenuDefault.js +5 -5
  134. package/src/client/components/default/SocketIoDefault.js +3 -51
  135. package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
  136. package/src/client/components/itemledger/LogInItemledger.js +3 -3
  137. package/src/client/components/itemledger/LogOutItemledger.js +2 -2
  138. package/src/client/components/itemledger/MenuItemledger.js +3 -3
  139. package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
  140. package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
  141. package/src/client/components/underpost/LogInUnderpost.js +3 -3
  142. package/src/client/components/underpost/LogOutUnderpost.js +2 -2
  143. package/src/client/components/underpost/MenuUnderpost.js +5 -5
  144. package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
  145. package/src/client/services/core/core.service.js +20 -8
  146. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +105 -0
  147. package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
  148. package/src/client/services/cyberia-entity/cyberia-entity.service.js +105 -0
  149. package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
  150. package/src/client/services/cyberia-instance/cyberia-instance.service.js +122 -0
  151. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +105 -0
  152. package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
  153. package/src/client/services/cyberia-map/cyberia-map.service.js +126 -0
  154. package/src/client/services/instance/instance.management.js +2 -2
  155. package/src/client/services/ipfs/ipfs.service.js +3 -23
  156. package/src/client/services/object-layer/object-layer.management.js +3 -3
  157. package/src/client/services/object-layer/object-layer.service.js +21 -0
  158. package/src/client/services/user/user.management.js +2 -2
  159. package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
  160. package/src/grpc/cyberia/OFF_CHAIN_ECONOMY.md +305 -0
  161. package/src/grpc/cyberia/README.md +326 -0
  162. package/src/grpc/cyberia/grpc-server.js +530 -0
  163. package/src/index.js +24 -1
  164. package/src/runtime/express/Dockerfile +4 -0
  165. package/src/runtime/express/Express.js +18 -1
  166. package/src/runtime/lampp/Dockerfile +13 -2
  167. package/src/runtime/lampp/Lampp.js +27 -4
  168. package/src/runtime/wp/Dockerfile +68 -0
  169. package/src/runtime/wp/Wp.js +639 -0
  170. package/src/server/auth.js +24 -1
  171. package/src/server/backup.js +37 -9
  172. package/src/server/client-build-docs.js +9 -2
  173. package/src/server/client-build.js +31 -31
  174. package/src/server/client-formatted.js +109 -57
  175. package/src/server/conf.js +24 -9
  176. package/src/server/cron.js +25 -23
  177. package/src/server/dns.js +2 -1
  178. package/src/server/ipfs-client.js +24 -1
  179. package/src/server/object-layer.js +149 -108
  180. package/src/server/peer.js +8 -0
  181. package/src/server/runtime.js +25 -1
  182. package/src/server/semantic-layer-generator-floor.js +359 -0
  183. package/src/server/semantic-layer-generator-skin.js +1294 -0
  184. package/src/server/semantic-layer-generator.js +116 -555
  185. package/src/server/start.js +2 -2
  186. package/src/ws/IoInterface.js +1 -10
  187. package/src/ws/IoServer.js +14 -33
  188. package/src/ws/core/channels/core.ws.chat.js +65 -20
  189. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  190. package/src/ws/core/channels/core.ws.stream.js +90 -31
  191. package/src/ws/core/core.ws.connection.js +12 -33
  192. package/src/ws/core/core.ws.emit.js +10 -26
  193. package/src/ws/core/core.ws.server.js +25 -58
  194. package/src/ws/default/channels/default.ws.main.js +53 -12
  195. package/src/ws/default/default.ws.connection.js +26 -13
  196. package/src/ws/default/default.ws.server.js +30 -12
  197. package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
  198. package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
  199. package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
  200. package/src/client/components/default/ElementsDefault.js +0 -38
  201. package/src/client/components/itemledger/CommonItemledger.js +0 -29
  202. package/src/client/components/itemledger/ElementsItemledger.js +0 -38
  203. package/src/client/components/underpost/CommonUnderpost.js +0 -29
  204. package/src/client/components/underpost/ElementsUnderpost.js +0 -38
  205. package/src/ws/core/management/core.ws.chat.js +0 -8
  206. package/src/ws/core/management/core.ws.mailer.js +0 -16
  207. package/src/ws/core/management/core.ws.stream.js +0 -8
  208. package/src/ws/default/management/default.ws.main.js +0 -8
@@ -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
+ };
@@ -0,0 +1,74 @@
1
+ import { loggerFactory } from '../../server/logger.js';
2
+ import { CyberiaInstanceConfService } from './cyberia-instance-conf.service.js';
3
+
4
+ const logger = loggerFactory(import.meta);
5
+
6
+ const CyberiaInstanceConfController = {
7
+ post: async (req, res, options) => {
8
+ try {
9
+ const result = await CyberiaInstanceConfService.post(req, res, options);
10
+ return res.status(200).json({
11
+ status: 'success',
12
+ data: result,
13
+ });
14
+ } catch (error) {
15
+ logger.error(error, error.stack);
16
+ return res.status(400).json({
17
+ status: 'error',
18
+ message: error.message,
19
+ });
20
+ }
21
+ },
22
+ get: async (req, res, options) => {
23
+ try {
24
+ const { page, limit } = req.query;
25
+ const result = await CyberiaInstanceConfService.get(
26
+ { ...req, query: { ...req.query, page: parseInt(page), limit: parseInt(limit) } },
27
+ res,
28
+ options,
29
+ );
30
+ return res.status(200).json({
31
+ status: 'success',
32
+ data: result,
33
+ });
34
+ } catch (error) {
35
+ logger.error(error, error.stack);
36
+ return res.status(400).json({
37
+ status: 'error',
38
+ message: error.message,
39
+ });
40
+ }
41
+ },
42
+ put: async (req, res, options) => {
43
+ try {
44
+ const result = await CyberiaInstanceConfService.put(req, res, options);
45
+ return res.status(200).json({
46
+ status: 'success',
47
+ data: result,
48
+ });
49
+ } catch (error) {
50
+ logger.error(error, error.stack);
51
+ return res.status(400).json({
52
+ status: 'error',
53
+ message: error.message,
54
+ });
55
+ }
56
+ },
57
+ delete: async (req, res, options) => {
58
+ try {
59
+ const result = await CyberiaInstanceConfService.delete(req, res, options);
60
+ return res.status(200).json({
61
+ status: 'success',
62
+ data: result,
63
+ });
64
+ } catch (error) {
65
+ logger.error(error, error.stack);
66
+ return res.status(400).json({
67
+ status: 'error',
68
+ message: error.message,
69
+ });
70
+ }
71
+ },
72
+ };
73
+
74
+ export { CyberiaInstanceConfController };