@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
@@ -1,22 +1,6 @@
1
- /**
2
- * Instance Map service — strategic-graph REST payloads for the client's
3
- * expanded Instance Map modal.
4
- *
5
- * Serves two lightweight views of an instance, decoupled from the gameplay
6
- * AOI stream:
7
- * static — graph topology plus authored map-cell presence, capability
8
- * membership, and baseline stats for living presence (bots and
9
- * resources; portals carry none). Fetched once when the modal
10
- * opens.
11
- * dynamic — per-player capability activity. Polled while the modal is open.
12
- *
13
- * Live player position is intentionally absent: engine-cyberia holds no
14
- * real-time simulation state (see cyberia-docs/ARCHITECTURE.md). The client
15
- * overlays its own predicted position on the graph.
16
- *
17
- * @module src/api/cyberia-instance/cyberia-instance-map.service.js
18
- */
1
+ // The instance map serves authored topology and capabilities. AOI owns live stats.
19
2
 
3
+ import { CyberiaEntityTypeDefaultService } from '../cyberia-entity-type-default/cyberia-entity-type-default.service.js';
20
4
  import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
21
5
  import { generateFallbackWorld } from './cyberia-fallback-world.js';
22
6
  import {
@@ -27,10 +11,10 @@ import {
27
11
  import { FileFactory } from '../file/file.service.js';
28
12
  import { loggerFactory } from '../../server/ops/logger.js';
29
13
  import {
30
- CYBERIA_INSTANCE_CONF_DEFAULTS,
31
14
  DefaultCyberiaActions,
32
15
  DefaultCyberiaQuests,
33
16
  ENTITY_TYPE_DEFAULTS,
17
+ resolveEntityDefaultBuild,
34
18
  } from '../cyberia-server-defaults/cyberia-server-defaults.js';
35
19
  import { DefaultCyberiaItems } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
36
20
 
@@ -78,9 +62,6 @@ const entityPresenceStatus = (entity, objectLayerMetadata, behavior) => {
78
62
  }
79
63
  };
80
64
 
81
- // Behaviors whose entities never carry the sum-stats readout (mission/action
82
- // givers and fully static NPCs — mirrors the Go server's behavior semantics).
83
- const STATLESS_BEHAVIORS = new Set(['provider', 'provider-static', 'static']);
84
65
 
85
66
  const mergeEntityDefaults = (entityDefaults = []) => {
86
67
  const merged = entityDefaults.map((entityDefault) => ({
@@ -101,33 +82,10 @@ const mergeEntityDefaults = (entityDefaults = []) => {
101
82
  return merged;
102
83
  };
103
84
 
104
- // Mirrors game/entity_defaults.go: the most-specific live-item set wins, then
105
- // the first build for the entity type supplies the runtime fallback behavior.
106
- const entityBehavior = (entity, entityDefaults) => {
107
- const itemIds = new Set(entity.objectLayerItemIds || []);
108
- let firstTypeDefault;
109
- let matchedDefault;
110
- let matchedSize = 0;
111
-
112
- for (const entityDefault of entityDefaults) {
113
- if (entityDefault.entityType !== entity.entityType) continue;
114
- if (!firstTypeDefault) firstTypeDefault = entityDefault;
115
- const liveItemIds = entityDefault.liveItemIds || [];
116
- if (0 === itemIds.size || 0 === liveItemIds.length) continue;
117
- if (!liveItemIds.every((itemId) => itemIds.has(itemId))) continue;
118
- if (liveItemIds.length > matchedSize) {
119
- matchedDefault = entityDefault;
120
- matchedSize = liveItemIds.length;
121
- }
122
- }
123
- return (matchedDefault || firstTypeDefault)?.behavior || '';
124
- };
125
-
126
- const objectLayerStatsSum = (itemIds, objectLayerMetadata, sumStatsLimit) =>
127
- Math.min(
128
- (itemIds || []).reduce((sum, itemId) => sum + (objectLayerMetadata[itemId]?.statsSum || 0), 0),
129
- sumStatsLimit,
130
- );
85
+ // One resolution rule for the whole platform see resolveEntityDefaultBuild.
86
+ const entityBehavior = (entity, entityDefaults) =>
87
+ resolveEntityDefaultBuild({ entityType: entity.entityType, itemIds: entity.objectLayerItemIds }, entityDefaults)
88
+ ?.behavior || '';
131
89
 
132
90
  const resolveObjectLayerMetadata = async (itemIds, options) => {
133
91
  if (!itemIds.length) return {};
@@ -139,49 +97,28 @@ const resolveObjectLayerMetadata = async (itemIds, options) => {
139
97
  return {};
140
98
  }
141
99
  const objectLayers = await ObjectLayer.find({ 'data.item.id': { $in: itemIds } })
142
- .select('data.item.id data.item.type data.stats')
100
+ .select('data.item.id data.item.type')
143
101
  .lean();
144
102
  return Object.fromEntries(
145
103
  objectLayers.map((layer) => [
146
104
  layer.data.item.id,
147
105
  {
148
- statsSum: Object.values(layer.data.stats || {}).reduce((sum, value) => sum + (Number(value) || 0), 0),
149
106
  type: layer.data.item.type,
150
107
  },
151
108
  ]),
152
109
  );
153
110
  };
154
111
 
155
- const resolveSumStatsLimit = async (instance, options) => {
156
- const fallback = CYBERIA_INSTANCE_CONF_DEFAULTS.sumStatsLimit;
157
- let CyberiaInstanceConf;
158
- try {
159
- CyberiaInstanceConf = DataBaseProviderService.getModel('CyberiaInstanceConf', options);
160
- } catch {
161
- return fallback;
162
- }
163
- const conf = await CyberiaInstanceConf.findOne({ instanceCode: instance.code }).select('sumStatsLimit').lean();
164
- return Number.isFinite(conf?.sumStatsLimit) && conf.sumStatsLimit > 0 ? conf.sumStatsLimit : fallback;
165
- };
166
-
112
+ // Only what this instance references: the conf names its entity-type defaults by _id, so a POI
113
+ // never reads behavior out of another world's wiring.
167
114
  const resolveEntityDefaults = async (instance, options) => {
168
- let configDefaults = [];
169
115
  try {
170
116
  const CyberiaInstanceConf = DataBaseProviderService.getModel('CyberiaInstanceConf', options);
171
117
  const conf = await CyberiaInstanceConf.findOne({ instanceCode: instance.code }).select('entityDefaults').lean();
172
- configDefaults = conf?.entityDefaults || [];
118
+ return mergeEntityDefaults(await CyberiaEntityTypeDefaultService.resolve(conf?.entityDefaults, options));
173
119
  } catch {
174
- configDefaults = [];
120
+ return mergeEntityDefaults();
175
121
  }
176
-
177
- try {
178
- const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('CyberiaEntityTypeDefault', options);
179
- const defaults = await CyberiaEntityTypeDefault.find({}).lean();
180
- if (defaults.length > 0) return mergeEntityDefaults(defaults);
181
- } catch {
182
- // The editable own-model collection is optional for legacy deployments.
183
- }
184
- return mergeEntityDefaults(configDefaults);
185
122
  };
186
123
 
187
124
  const buildPresencePois = ({
@@ -191,11 +128,10 @@ const buildPresencePois = ({
191
128
  actions,
192
129
  objectLayerMetadata = {},
193
130
  entityDefaults = mergeEntityDefaults(),
194
- sumStatsLimit = CYBERIA_INSTANCE_CONF_DEFAULTS.sumStatsLimit,
195
131
  }) => {
196
132
  const pois = new Map();
197
133
 
198
- const getPoi = (mapCode, cellX, cellY, presenceStatus, sourcePriority = 0, statsSum = 0, showStatsValue = false) => {
134
+ const getPoi = (mapCode, cellX, cellY, presenceStatus, sourcePriority = 0) => {
199
135
  if (!mapCode || !Number.isFinite(cellX) || !Number.isFinite(cellY) || cellX < 0 || cellY < 0 || !presenceStatus)
200
136
  return null;
201
137
  const key = `${mapCode}:${cellX}:${cellY}`;
@@ -207,19 +143,7 @@ const buildPresencePois = ({
207
143
  presenceStatus,
208
144
  capabilities: [],
209
145
  sourcePriority,
210
- statsSum: 0,
211
- hasStatfulPresence: false,
212
- hasStatlessPresence: false,
213
146
  };
214
- if (sourcePriority >= 100) {
215
- if (showStatsValue) {
216
- poi.hasStatfulPresence = true;
217
- poi.statsSum += statsSum;
218
- } else {
219
- poi.hasStatlessPresence = true;
220
- poi.statsSum = 0;
221
- }
222
- }
223
147
  const priority = sourcePriority + (PRESENCE_PRIORITY[presenceStatus] || 0);
224
148
  const currentPriority = poi.sourcePriority + (PRESENCE_PRIORITY[poi.presenceStatus] || 0);
225
149
  if (priority > currentPriority) {
@@ -239,29 +163,14 @@ const buildPresencePois = ({
239
163
  const behavior = entityBehavior(entity, entityDefaults);
240
164
  const presenceStatus = entityPresenceStatus(entity, objectLayerMetadata, behavior);
241
165
  if (!presenceStatus) continue;
242
- // Stats travel only with living presence (bots and resources); portal
243
- // cells and provider/static-behavior entities never carry the readout.
244
- const statless =
245
- presenceStatus === PRESENCE_STATUS.portal ||
246
- presenceStatus === PRESENCE_STATUS.portalRandom ||
247
- STATLESS_BEHAVIORS.has(behavior);
248
- getPoi(
249
- map.code,
250
- entity.initCellX ?? 0,
251
- entity.initCellY ?? 0,
252
- presenceStatus,
253
- 100,
254
- statless ? 0 : objectLayerStatsSum(entity.objectLayerItemIds, objectLayerMetadata, sumStatsLimit),
255
- !statless,
256
- );
166
+ getPoi(map.code, entity.initCellX ?? 0, entity.initCellY ?? 0, presenceStatus, 100);
257
167
  }
258
168
  }
259
169
 
260
- // Portal-cell presence mirrors the AOI: the portal's OWN outgoing edge
261
- // decides random vs fixed (priority 200 overrides the authored entity
262
- // classification, which may lack the runtime-assigned mode). Fixed-cell
263
- // TARGET endpoints only guarantee a plain portal POI — their real mode, if
264
- // any, is stamped by their own outgoing edge.
170
+ // Portal-cell presence follows the AOI: the portal's own outgoing edge
171
+ // decides random against fixed, and priority 200 beats the authored entity
172
+ // class. A fixed-cell target endpoint is only a plain portal POI; its own
173
+ // outgoing edge stamps the mode.
265
174
  for (const portal of instance.portals || []) {
266
175
  getPoi(
267
176
  portal.sourceMapCode,
@@ -283,14 +192,8 @@ const buildPresencePois = ({
283
192
  addCapability(poi, CAPABILITY_ACTION);
284
193
  }
285
194
 
286
- // Display permission is explicit: a statless entity must never become a
287
- // visible `stats 0` tab due to a numeric fallback or merged POI source.
288
195
  return [...pois.values()]
289
- .map(({ sourcePriority, hasStatfulPresence, hasStatlessPresence, ...poi }) => ({
290
- ...poi,
291
- statsSum: hasStatfulPresence && !hasStatlessPresence && poi.capabilities.length === 0 ? poi.statsSum : 0,
292
- showStatsValue: hasStatfulPresence && !hasStatlessPresence && poi.capabilities.length === 0,
293
- }))
196
+ .map(({ sourcePriority, ...poi }) => poi)
294
197
  .sort(
295
198
  (left, right) =>
296
199
  left.mapCode.localeCompare(right.mapCode) || left.cellY - right.cellY || left.cellX - right.cellX,
@@ -310,7 +213,6 @@ const buildStaticPayload = ({
310
213
  fallback,
311
214
  objectLayerMetadata = {},
312
215
  entityDefaults = mergeEntityDefaults(),
313
- sumStatsLimit = CYBERIA_INSTANCE_CONF_DEFAULTS.sumStatsLimit,
314
216
  previewCachedMapCodes = new Set(),
315
217
  }) => {
316
218
  const previewRoute = (mapCode) =>
@@ -323,11 +225,10 @@ const buildStaticPayload = ({
323
225
  gridY: m.gridY,
324
226
  // File id of the map's auto-captured Object Layer render (persisted maps).
325
227
  preview: m.preview ? String(m.preview) : '',
326
- // Ready-to-fetch path of the node background. A captured File is served
327
- // directly; otherwise the client hits the preview route, which renders on
328
- // demand — and for a persisted map also stores the result as its `preview`
329
- // File, so the next payload serves it through the default blob path.
330
- // Fallback-world maps only advertise the route once a render succeeded.
228
+ // Path of the node background. A captured File is served directly. Without
229
+ // one the client hits the preview route, which renders on demand and, for a
230
+ // persisted map, stores the result as its `preview` File. A fallback-world
231
+ // map advertises the route only after a render succeeds.
331
232
  previewUrl: m.preview
332
233
  ? `/api/file/blob/${String(m.preview)}`
333
234
  : fallback
@@ -362,7 +263,6 @@ const buildStaticPayload = ({
362
263
  actions,
363
264
  objectLayerMetadata,
364
265
  entityDefaults,
365
- sumStatsLimit,
366
266
  }),
367
267
  };
368
268
  };
@@ -416,10 +316,9 @@ const classifyActionProviders = (actions, questProviders) => {
416
316
 
417
317
  /**
418
318
  * Resolve the content-authority view of an instance: its map nodes plus the
419
- * quests/actions bound to those maps. Falls back to the in-memory procedural
420
- * world (and canonical default quests/actions) when the instance is not
421
- * persisted mirroring the gRPC getFullInstance fallback that feeds
422
- * cyberia-server.
319
+ * quests and actions bound to those maps. An instance that is not persisted
320
+ * falls back to the procedural world and the default quests/actions, the same
321
+ * fallback getFullInstance serves to the simulation.
423
322
  */
424
323
  const resolveInstanceWorld = async (instanceCode, options) => {
425
324
  const CyberiaInstance = DataBaseProviderService.getModel('CyberiaInstance', options);
@@ -440,7 +339,7 @@ const resolveInstanceWorld = async (instanceCode, options) => {
440
339
  const resolved = await resolveObjectLayerMetadata(fallbackItemIds, options);
441
340
  const objectLayerMetadata = Object.fromEntries(
442
341
  Object.entries(resolved).map(([itemId, meta]) => {
443
- return [itemId, { statsSum: meta.statsSum, type: meta.type }];
342
+ return [itemId, { type: meta.type }];
444
343
  }),
445
344
  );
446
345
 
@@ -457,11 +356,9 @@ const resolveInstanceWorld = async (instanceCode, options) => {
457
356
  actions: DefaultCyberiaActions.filter((a) => mapCodes.has(a.sourceMapCode)),
458
357
  objectLayerMetadata,
459
358
  entityDefaults: mergeEntityDefaults(),
460
- sumStatsLimit: CYBERIA_INSTANCE_CONF_DEFAULTS.sumStatsLimit,
461
- // The procedural world never passes through the browser editor, so its
462
- // node backgrounds are rendered here and served from the in-memory
463
- // preview cache (see instanceMapPreview).
464
- previewCachedMapCodes: new Set(await cacheWorldMapPreviews(instanceCode, world.maps)),
359
+ // The procedural world has no editor pass, so its node backgrounds render
360
+ // here and are served from the in-memory preview cache.
361
+ previewCachedMapCodes: new Set(await cacheWorldMapPreviews(instanceCode, world.maps, { options })),
465
362
  fallback: true,
466
363
  };
467
364
  }
@@ -483,20 +380,17 @@ const resolveInstanceWorld = async (instanceCode, options) => {
483
380
  ...new Set(maps.flatMap((map) => (map.entities || []).flatMap((entity) => entity.objectLayerItemIds || []))),
484
381
  ];
485
382
  const objectLayerMetadata = await resolveObjectLayerMetadata(itemIds, options);
486
- const sumStatsLimit = await resolveSumStatsLimit(instance, options);
487
383
  const entityDefaults = await resolveEntityDefaults(instance, options);
488
384
 
489
385
  const codeSet = new Set(mapCodes);
490
386
  return {
491
387
  instance,
492
388
  maps,
493
- // Empty collections fall back to canonical defaults, matching the quest
494
- // getByCode behaviour for unseeded databases.
389
+ // Empty collections fall back to the canonical defaults, as getByCode does.
495
390
  quests: dbQuests.length > 0 ? dbQuests : DefaultCyberiaQuests.filter((q) => codeSet.has(q.sourceMapCode)),
496
391
  actions: dbActions.length > 0 ? dbActions : DefaultCyberiaActions.filter((a) => codeSet.has(a.sourceMapCode)),
497
392
  objectLayerMetadata,
498
393
  entityDefaults,
499
- sumStatsLimit,
500
394
  fallback: false,
501
395
  };
502
396
  };
@@ -532,7 +426,7 @@ class CyberiaInstanceMapService {
532
426
  if (stored?.data) return stored.data;
533
427
  }
534
428
 
535
- const rendered = await renderMapPreviewPng(map);
429
+ const rendered = await renderMapPreviewPng(map, { options });
536
430
  if (rendered) {
537
431
  const file = await new File(FileFactory.create(rendered, `${mapCode}-preview.png`)).save();
538
432
  await CyberiaMap.updateOne({ _id: map._id }, { $set: { preview: file._id } });
@@ -543,10 +437,9 @@ class CyberiaInstanceMapService {
543
437
 
544
438
  let png = getCachedMapPreview(instanceCode, mapCode);
545
439
  if (!png) {
546
- // Cold cache (first hit, or a restart since the modal last opened):
547
- // regenerate the world so the preview exists, then serve it.
440
+ // Cold cache: regenerate the world so the preview exists, then serve it.
548
441
  const { maps } = await resolveInstanceWorld(instanceCode, options);
549
- await cacheWorldMapPreviews(instanceCode, maps);
442
+ await cacheWorldMapPreviews(instanceCode, maps, { options });
550
443
  png = getCachedMapPreview(instanceCode, mapCode);
551
444
  }
552
445
  if (!png) throw new Error(`No cached preview for map "${mapCode}"`);
@@ -10,7 +10,7 @@ import { CyberiaInstanceMapService } from './cyberia-instance-map.service.js';
10
10
  import { CyberiaInstanceBootService } from './cyberia-instance-boot.service.js';
11
11
 
12
12
  const CyberiaInstanceController = buildCrudController(CyberiaInstanceService, {
13
- // The C client fetches the instance map cross-origin (same as quest metadata).
13
+ // The game client fetches the instance map cross-origin, as it does quests.
14
14
  instanceMapStatic: serviceHandler(CyberiaInstanceMapService.getStatic, { crossOrigin: true, errorStatus: 404 }),
15
15
  instanceMapDynamic: serviceHandler(CyberiaInstanceMapService.getDynamic, { crossOrigin: true, errorStatus: 404 }),
16
16
  // Node background of a fallback-world map, rendered + cached server-side.
@@ -30,9 +30,6 @@ const CyberiaInstanceController = buildCrudController(CyberiaInstanceService, {
30
30
  portalConnect: serviceHandler(CyberiaInstanceService.portalConnect),
31
31
  // Moderator/admin-triggered world reload on a running cyberia-server.
32
32
  hotReload: serviceHandler(CyberiaInstanceService.hotReload, { errorStatus: 502 }),
33
- // Fallback-world default items (volatile) + its own reload trigger.
34
- fallbackDefaultItems: serviceHandler(CyberiaInstanceService.fallbackDefaultItems),
35
- fallbackHotReload: serviceHandler(CyberiaInstanceService.fallbackHotReload, { errorStatus: 502 }),
36
33
  // Boot transport (REST fallback of the gRPC CyberiaDataService).
37
34
  bootPing: serviceHandler(CyberiaInstanceBootService.ping),
38
35
  bootObjectLayerBatch: serviceHandler(CyberiaInstanceBootService.objectLayerBatch, { errorStatus: 500 }),
@@ -68,20 +68,6 @@ const CyberiaInstanceSchema = new Schema(
68
68
  // Vertex set of the graph
69
69
  cyberiaMapCodes: { type: [String], default: [] },
70
70
 
71
- // Instance-level object layer item IDs. Each entry binds an item id to the
72
- // instance and flags whether it seeds every player's starting inventory
73
- // (defaultPlayerInventory → merged into the player entityDefault's
74
- // defaultObjectLayers when the world is served to cyberia-server).
75
- itemIds: {
76
- type: [
77
- {
78
- id: { type: String, required: true },
79
- defaultPlayerInventory: { type: Boolean, default: false },
80
- },
81
- ],
82
- default: [],
83
- },
84
-
85
71
  // Directed edge set of the graph
86
72
  portals: { type: [PortalEdgeSchema], default: [] },
87
73
 
@@ -12,9 +12,9 @@ class CyberiaInstanceRouter {
12
12
  const router = express.Router();
13
13
  // ── Custom actions (must come before generic /:id routes) ──────────────
14
14
  // Boot transport — REST fallback of the gRPC CyberiaDataService, consumed
15
- // server-to-server by cyberia-server (ENGINE_API_BASE_URL) when the engine
16
- // gRPC server (ENGINE_GRPC_ADDRESS) is not enabled. Unauthenticated for
17
- // parity with the internal-network insecure gRPC channel.
15
+ // server-to-server by cyberia-server (its --data-server-url) when the engine
16
+ // gRPC server is not enabled for the deploy. Unauthenticated for parity with
17
+ // the internal-network insecure gRPC channel.
18
18
  router.get(`/boot/ping`, async (req, res) => await CyberiaInstanceController.bootPing(req, res, options));
19
19
  router.get(
20
20
  `/boot/object-layers`,
@@ -38,19 +38,6 @@ class CyberiaInstanceRouter {
38
38
  async (req, res) => await CyberiaInstanceController.bootFullInstance(req, res, options),
39
39
  );
40
40
  router.get(`/fallback-world`, async (req, res) => await CyberiaInstanceController.fallbackWorld(req, res, options));
41
- // Fallback-world default items — volatile, process-local (never persisted).
42
- // The read is open (same as /fallback-world); staging them and triggering a
43
- // rebuild is elevated privilege, like every other hot-reload path.
44
- router.get(
45
- `/fallback-world/default-items`,
46
- async (req, res) => await CyberiaInstanceController.fallbackDefaultItems(req, res, options),
47
- );
48
- router.post(
49
- `/fallback-world/hot-reload`,
50
- options.authMiddleware,
51
- moderatorGuard,
52
- async (req, res) => await CyberiaInstanceController.fallbackHotReload(req, res, options),
53
- );
54
41
  // Instance Map — static topology/presence plus dynamic player capability activity.
55
42
  router.get(
56
43
  `/instance-map/:instanceCode/static`,
@@ -7,8 +7,6 @@ import {
7
7
  fallbackListInstance,
8
8
  DEFAULT_FALLBACK_INSTANCE_CODE,
9
9
  } from './cyberia-fallback-world.js';
10
- import { getFallbackDefaultItems, setFallbackDefaultItems } from './cyberia-fallback-default-items.js';
11
- import { CYBERIA_INSTANCE_CONF_DEFAULTS } from '../cyberia-server-defaults/cyberia-server-defaults.js';
12
10
  import { triggerHotReload } from '../../projects/cyberia/hot-reload-trigger.js';
13
11
 
14
12
  const logger = loggerFactory(import.meta);
@@ -45,7 +43,9 @@ class CyberiaInstanceService {
45
43
  /** @type {import('./cyberia-instance.model.js').CyberiaInstanceModel} */
46
44
  const CyberiaInstance = DataBaseProviderService.getModel('CyberiaInstance', options);
47
45
  const populateCreator = { path: 'creator', model: 'User', select: '_id username' };
48
- if (req.params.id) return await CyberiaInstance.findById(req.params.id).populate(populateCreator);
46
+ // Instances are addressed by code, the key the engine navigates by.
47
+ if (req.params.id)
48
+ return await CyberiaInstance.findOne(DataQuery.naturalKeyFilter('code', req.params.id)).populate(populateCreator);
49
49
 
50
50
  // Parse query parameters using DataQuery helper
51
51
  const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
@@ -185,49 +185,9 @@ class CyberiaInstanceService {
185
185
  botCount: q.botCount ? parseInt(q.botCount, 10) : undefined,
186
186
  obstacleCount: q.obstacleCount ? parseInt(q.obstacleCount, 10) : undefined,
187
187
  foregroundCount: q.foregroundCount ? parseInt(q.foregroundCount, 10) : undefined,
188
- itemIds: getFallbackDefaultItems(),
189
188
  });
190
189
  };
191
190
 
192
- /**
193
- * Current fallback-world default items — the volatile set the
194
- * FallbackWorldEngine view last pushed, or `[]` when the engine is running on
195
- * pure code defaults. Lets the view reopen showing what this process serves.
196
- *
197
- * `entityDefaultItemIds` are the ids the code defaults already ship in an
198
- * entity's `defaultObjectLayers`. Selecting one of them and leaving
199
- * `defaultPlayerInventory` unchecked REMOVES it (see
200
- * applyInstanceDefaultPlayerInventory), so the editor needs to be able to warn
201
- * before an operator silently strips the player's starting skin or weapon.
202
- */
203
- static fallbackDefaultItems = async () => {
204
- const entityDefaultItemIds = new Set();
205
- for (const entityDefault of CYBERIA_INSTANCE_CONF_DEFAULTS.entityDefaults || []) {
206
- for (const layer of entityDefault.defaultObjectLayers || []) {
207
- if (layer.itemId) entityDefaultItemIds.add(layer.itemId);
208
- }
209
- }
210
- return { itemIds: getFallbackDefaultItems(), entityDefaultItemIds: [...entityDefaultItemIds].sort() };
211
- };
212
-
213
- /**
214
- * Stage the fallback world's default items, then ask a running cyberia-server
215
- * to rebuild. The items are not persisted (see cyberia-fallback-default-items):
216
- * they are staged here first so the rebuild's `getFullInstance` re-fetch picks
217
- * them up, which is what makes them reach the server at all.
218
- *
219
- * `instanceCode` is intentionally left empty — the target world is addressed by
220
- * `serverUrl` (origin, or origin + variant sub-path), and naming the synthetic
221
- * `fallback` code would be rejected by any server serving a real instance code.
222
- */
223
- static fallbackHotReload = async (req) => {
224
- const { serverUrl, mode, itemIds } = req.body || {};
225
- if (!serverUrl) throw new Error('serverUrl is required');
226
-
227
- const staged = setFallbackDefaultItems(itemIds || []);
228
- const { transport, result, grpcError } = await triggerHotReload({ serverUrl, instanceCode: '', mode });
229
- return { transport, itemIds: staged, grpcError, ...result };
230
- };
231
191
  }
232
192
 
233
193
  export { CyberiaInstanceService };
@@ -483,7 +483,6 @@ function generateBots(mapDims, colors, opts = {}) {
483
483
  cellY = randInt(0, maxY);
484
484
  }
485
485
 
486
- // const skin = BOT_SKIN_POOL[Math.floor(Math.random() * BOT_SKIN_POOL.length)];
487
486
  const skin = 'kishins';
488
487
  const hasWeapon = nextRandom() < BOT_WEAPON_CHANCE;
489
488
  const itemIds = hasWeapon ? [skin, 'atlas_pistol_mk2'] : [skin];
@@ -506,16 +505,13 @@ function generateBots(mapDims, colors, opts = {}) {
506
505
  }
507
506
 
508
507
  /**
509
- * Action-provider NPC bots the fixed-position entities that back the
510
- * default mission system. One passive bot is placed per `CyberiaAction`
511
- * whose `sourceMapCode` matches `mapCode`, at the action's
512
- * (`sourceCellX`, `sourceCellY`). The Go server binds each bot back to its
513
- * action by these coordinates, so the positions here are the single source
514
- * of truth shared with `DefaultCyberiaActions`.
508
+ * Action-provider NPC bots, the fixed-position entities behind the mission
509
+ * system. One passive bot per `CyberiaAction` whose `sourceMapCode` matches
510
+ * `mapCode`, at the action's (`sourceCellX`, `sourceCellY`). The server binds
511
+ * a bot to its action by these coordinates.
515
512
  *
516
- * The NPC skin is derived from the action's first `default-<skin>` dialogue
517
- * code (e.g. `default-wason` → skin `wason`) there is no separate
518
- * provideItemId; the bot's own active skin is what 'talk' objectives match.
513
+ * The NPC skin comes from the action's first `default-<skin>` dialogue code
514
+ * (`default-wason` → skin `wason`). A 'talk' objective matches that skin.
519
515
  *
520
516
  * @param {string} mapCode
521
517
  * @param {Array<{ key: string, r: number, g: number, b: number, a: number }>} colors
@@ -546,9 +542,8 @@ function generateActionProviderBots(mapCode, colors, opts = {}) {
546
542
 
547
543
  if (opts.grid) opts.grid.block(cellX, cellY, dim, dim);
548
544
 
549
- // Passive quest-giver NPC: no aggro, and a small wander radius so it
550
- // drifts near its source cell but stays easy to find. The Go server binds
551
- // it by its spawn-centre (the initial cell), so the bind survives movement.
545
+ // Passive quest-giver NPC: no aggro and a small wander radius. The server
546
+ // binds it by its spawn cell, so the bind survives the drift.
552
547
  entities.push({
553
548
  entityType: 'bot',
554
549
  initCellX: cellX,