@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,46 +1,35 @@
1
1
  /**
2
- * Canonical server-owned defaults for the Cyberia runtime.
2
+ * Canonical simulation defaults for the Cyberia runtime: per-entity-type item
3
+ * sets, simulation / AOI / combat / economy / skill rules, equipment rules,
4
+ * status-icon numeric IDs, and the seed content (dialogues, actions, quests)
5
+ * the CLI writes to Mongo.
3
6
  *
4
- * Single source of truth for everything the **authoritative simulation
5
- * server** (cyberia-server, Go) needs to build and run a world:
7
+ * STRICT BOUNDARY: never import this from `src/client/`. The browser bundler
8
+ * resolves imports recursively, so one browser-side import ships the whole
9
+ * simulation defaults in the public JS payload.
6
10
  *
7
- * - per-entity-type live / dead / drop item configuration
8
- * - simulation, AOI, combat, economy, and skill rules
9
- * - equipment rules
10
- * - status-icon **numeric IDs** (visuals live in client defaults)
11
- * - seed content (dialogues, actions, quests) consumed by the
12
- * persistence/CLI tooling that bootstraps the world
13
- * - native-dependency pin list for chain-bridge tooling
14
- *
15
- * STRICT BOUNDARY
16
- * ---------------
17
- * NEVER imported from any file under `src/client/`. The browser bundler
18
- * resolves imports recursively, so a single browser-side import would drag
19
- * the entire simulation defaults into the public JS payload.
20
- *
21
- * Shared content **vocabulary** (item/entity type enums, the
22
- * `DefaultCyberiaItems` registry, `ENTITY_TYPE_TO_ITEM_TYPES`, the quest
23
- * step objective enum) lives in `SharedDefaultsCyberia.js`. This file
24
- * re-imports those so the browser editor never needs to reach into
25
- * server-defaults to learn the schema.
26
- *
27
- * Consumers:
28
- * - cyberia-instance-conf model + grpc-server (gameplay config)
29
- * - cyberia-world-generator / cyberia-fallback-world (world build)
30
- * - cyberia-quest / cyberia-action / Mongo seed scripts (content)
31
- * - bin/cyberia + bin/build + bin/deploy (CLI + chain bridge)
11
+ * Shared content vocabulary (item / entity type enums, `DefaultCyberiaItems`,
12
+ * `ENTITY_TYPE_TO_ITEM_TYPES`, quest step objectives) lives in
13
+ * `SharedDefaultsCyberia.js`.
32
14
  *
33
15
  * @module src/api/cyberia-server-defaults/cyberia-server-defaults.js
34
16
  */
35
17
 
36
- // The canonical client-defaults module lives under src/client/ so the
37
- // browser bundler can resolve the URL inside the client tree. Engine-side
38
- // Node imports work from any path, so we reach into it from here.
18
+ // Shared vocabulary lives under src/client/ so the browser bundler resolves it.
39
19
  import {
40
20
  ITEM_TYPES,
21
+ STAT_TYPES,
22
+ STAT_MODIFIER_MAX,
23
+ ENTITY_LEVEL_MAX,
41
24
  ENTITY_TYPES,
42
25
  SKILL_LOGIC_ID_VALUES,
43
26
  isCanonicalSkillLogicId,
27
+ AUDIO_BUSES,
28
+ AUDIO_BUS_MUSIC,
29
+ AUDIO_BUS_SFX,
30
+ AUDIO_LOGIC_ID_BUSES,
31
+ AUDIO_LOGIC_ID_VALUES,
32
+ isCanonicalAudioLogicId,
44
33
  } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
45
34
 
46
35
  export const DOCKER_COMPOSE_ID = 'cyberia';
@@ -60,10 +49,9 @@ export const DEPLOY_ID = 'dd-cyberia';
60
49
  * - `skills` (expanded metadata with name, description, summoned entity)
61
50
  *
62
51
  * Consumers:
63
- * - `CYBERIA_INSTANCE_CONF_DEFAULTS.skillConfig` (gRPC fallback defaults)
64
- * - `bin/cyberia.js seed-skills` (upsert into the cyberia-skill collection
65
- * the authoritative CyberiaSkill model keeps the full record, including the
66
- * `skills` metadata the instance-conf skillConfig schema drops)
52
+ * - `bin/cyberia.js seed-skills` (upsert into the cyberia-skill collection, which is the
53
+ * authoritative store an instance then runs the subset its own content triggers)
54
+ * - the fallback world, which has no collection to read
67
55
  */
68
56
  export const DefaultSkillConfig = [
69
57
  {
@@ -92,18 +80,6 @@ export const DefaultSkillConfig = [
92
80
  },
93
81
  ],
94
82
  },
95
- // {
96
- // triggerItemId: 'anon',
97
- // logicEventIds: ['doppelganger'],
98
- // skills: [
99
- // {
100
- // logicEventId: 'doppelganger',
101
- // name: 'Doppelganger',
102
- // description: 'Summons a passive clone of yourself that wanders nearby. Spawn chance scales with Intelligence.',
103
- // summonedEntityItemId: '$active_skin',
104
- // },
105
- // ],
106
- // },
107
83
  {
108
84
  triggerItemId: 'hatchet',
109
85
  logicEventIds: ['projectile'],
@@ -119,9 +95,7 @@ export const DefaultSkillConfig = [
119
95
  },
120
96
  ];
121
97
 
122
- // Fail fast on a non-canonical logicEventId: the canonical LogicId registry in
123
- // SharedDefaultsCyberia.js is the single source of truth, so a typo or a handler
124
- // the dispatcher does not know must surface at boot, not as a silent no-op skill.
98
+ // A logicEventId absent from the SharedDefaultsCyberia registry fails at boot.
125
99
  for (const cfg of DefaultSkillConfig) {
126
100
  for (const logicEventId of [...(cfg.logicEventIds || []), ...(cfg.skills || []).map((sk) => sk.logicEventId)]) {
127
101
  if (!isCanonicalSkillLogicId(logicEventId)) {
@@ -137,18 +111,11 @@ for (const cfg of DefaultSkillConfig) {
137
111
  // Lain demo quests (testing only) — single source of truth
138
112
  // ─────────────────────────────────────────────────────────────────────────────
139
113
  //
140
- // Offered by Lain (15,22): a batch of simple no-prerequisite quests so the
141
- // Quest Journal can exercise pagination with 4+ concurrently active quests.
142
- //
143
- // These specs drive THREE derived structures so they can never drift apart:
144
- // - DefaultCyberiaQuests — the quest definitions
145
- // - DefaultCyberiaDialogues — one quest-talk dialogue per talk objective
146
- // - DefaultCyberiaActions — Lain's questDialogueCodes mapping
114
+ // No-prerequisite quests offered by Lain (15,22). These specs derive the quest
115
+ // definitions, their quest-talk dialogues, and Lain's questDialogueCodes map.
147
116
  //
148
- // Every `talk` objective MUST have its own quest-talk dialogue mapped on the
149
- // provider's action: the server validates a talk objective only when the
150
- // completed dialogue matches that mapping, so a quest without one could be
151
- // satisfied by reading the NPC's default greeting.
117
+ // Each `talk` objective needs its own quest-talk dialogue on the provider's
118
+ // action. The server validates a talk only against that mapping.
152
119
 
153
120
  const LAIN_DEMO_QUEST_SPECS = [
154
121
  {
@@ -262,12 +229,7 @@ const LAIN_DEMO_TALK_SPECS = LAIN_DEMO_QUEST_SPECS.filter((q) =>
262
229
  (q.steps || []).some((s) => (s.objectives || []).some((o) => o.type === 'talk' && o.itemId === 'lain')),
263
230
  );
264
231
 
265
- /**
266
- * Default dialogue seeds. Mirrors `CyberiaDialogue` model schema:
267
- * { code, order, speaker, text, mood }
268
- *
269
- * Used by the seed/migration script to populate Mongo on first boot.
270
- */
232
+ /** Dialogue seeds ({ code, order, speaker, text, mood }) the CLI writes to Mongo. */
271
233
  export const DefaultCyberiaDialogues = [
272
234
  {
273
235
  code: 'default-coin',
@@ -527,12 +489,10 @@ export const DefaultCyberiaDialogues = [
527
489
  * Each entry follows the `CyberiaAction` model schema.
528
490
  */
529
491
  export const DefaultCyberiaActions = [
530
- // An action has no `type`: it declares the capabilities available at a cell.
531
- // `code` is a generic location slug; `label` is the bot's overhead name (the
532
- // client fetches it by code via REST). The NPC skin is derived from
533
- // `dialogCode` (default-<skin>). `questDialogueCodes` maps each quest the NPC
534
- // handles to the dialogue shown for it (offer + talk-objective validation).
535
- // The quests an NPC OFFERS are those whose source cell matches the action's.
492
+ // An action declares the capabilities available at a cell. `code` is a
493
+ // location slug, `label` the bot's overhead name, and the NPC skin comes from
494
+ // `dialogCode` (default-<skin>). `questDialogueCodes` maps a quest to its
495
+ // dialogue. An NPC offers the quests whose source cell matches the action's.
536
496
  {
537
497
  code: 'loc-fallback-map-0-12-10',
538
498
  label: 'Wason',
@@ -566,9 +526,8 @@ export const DefaultCyberiaActions = [
566
526
  questDialogueCodes: [{ questCode: 'bounty-quest-alpha', dialogCode: 'quest-talk-agent' }],
567
527
  },
568
528
  {
569
- // Shop capability: an action carrying shopItems is a vendor. The client
570
- // surfaces a Shop tab for it and the simulation validates every purchase
571
- // against this catalog (price item + quantity).
529
+ // Shop capability: an action with shopItems is a vendor. The simulation
530
+ // validates every purchase against this catalog (price item + quantity).
572
531
  code: 'loc-fallback-map-0-18-16',
573
532
  label: 'Punk',
574
533
  sourceMapCode: 'fallback-map-0',
@@ -578,12 +537,9 @@ export const DefaultCyberiaActions = [
578
537
  shopItems: [{ itemId: 'tim-knife', priceItemId: 'coin', priceQty: 10 }],
579
538
  },
580
539
  {
581
- // Assembler capability: an action carrying craftRecipes is a fabrication
582
- // terminal. The client surfaces an Assembly tab for it and the simulation
583
- // validates every synthesis against these recipes (ingredients outputs).
584
- // Every id here is obtainable inside the fallback world — wood from the
585
- // resource nodes, coin from field drops — so the loop is playable on a
586
- // fresh spawn with no seeding.
540
+ // Assembler capability: an action with craftRecipes is a fabrication
541
+ // terminal. The simulation validates every synthesis against these recipes.
542
+ // Every id here is obtainable in the fallback world with no seeding.
587
543
  code: 'loc-fallback-map-0-15-16',
588
544
  label: 'Eiri',
589
545
  sourceMapCode: 'fallback-map-0',
@@ -609,10 +565,7 @@ export const DefaultCyberiaActions = [
609
565
  craftTimeMs: 5000,
610
566
  },
611
567
  {
612
- // Multi-output: one blade breaks down into three component stacks, so
613
- // the assembly UI and the arrival flights are exercised with more than
614
- // a single result slot. Closes the loop with Punk's shop — buy a knife,
615
- // salvage it back into parts.
568
+ // Multi-output salvage recipe: one blade breaks down into three stacks.
616
569
  outputItems: [
617
570
  { itemId: 'wood-drop-1', qty: 2 },
618
571
  { itemId: 'wood-drop-2', qty: 1 },
@@ -624,9 +577,8 @@ export const DefaultCyberiaActions = [
624
577
  ],
625
578
  },
626
579
  {
627
- // Storage capability: an action carrying storageSlots is a personal vault.
628
- // The client surfaces a Storage tab whose square grid is sized from the
629
- // capacity (25 → 5x5) and the simulation owns the contents.
580
+ // Storage capability: an action with storageSlots is a personal vault. The
581
+ // simulation owns the contents; the slot count sizes the grid (25 5x5).
630
582
  code: 'loc-fallback-map-0-12-22',
631
583
  label: 'Kaneki',
632
584
  sourceMapCode: 'fallback-map-0',
@@ -642,9 +594,7 @@ export const DefaultCyberiaActions = [
642
594
  sourceCellX: 15,
643
595
  sourceCellY: 22,
644
596
  dialogCode: 'default-lain',
645
- // Every demo quest with a `talk` objective gets its own quest-talk
646
- // dialogue here — without the mapping the server would accept the
647
- // default greeting as the talk, and no quest-talk button would show.
597
+ // One quest-talk dialogue per demo quest that carries a `talk` objective.
648
598
  questDialogueCodes: LAIN_DEMO_TALK_SPECS.map((q) => ({
649
599
  questCode: lainDemoQuestCode(q.suffix),
650
600
  dialogCode: lainDemoTalkDialogCode(q.suffix),
@@ -687,8 +637,7 @@ export const DefaultCyberiaQuests = [
687
637
  rewards: [{ itemId: 'coin', quantity: 50 }],
688
638
  },
689
639
  {
690
- // Parallel initial mission same source cell as Wason (12,10), no
691
- // prerequisites, so the player can accept it alongside the intro quest.
640
+ // Parallel initial mission: same source cell as the intro quest, no prerequisites.
692
641
  code: 'wason-errand',
693
642
  title: "Wason's Errand",
694
643
  description: 'Gather coins from the field and bring them back to Wason.',
@@ -741,8 +690,7 @@ export const DefaultCyberiaQuests = [
741
690
  rewards: [{ itemId: 'hatchet', quantity: 1 }],
742
691
  },
743
692
  // ── Demo quests (testing only) ─────────────────────────────────────────────
744
- // Built from LAIN_DEMO_QUEST_SPECS so the quest definitions, their
745
- // quest-talk dialogues, and Lain's questDialogueCodes mapping stay in sync.
693
+ // Derived from LAIN_DEMO_QUEST_SPECS.
746
694
  ...LAIN_DEMO_QUEST_SPECS.map((q) => ({
747
695
  code: lainDemoQuestCode(q.suffix),
748
696
  title: q.title,
@@ -762,11 +710,9 @@ export const DefaultCyberiaQuests = [
762
710
  // ─────────────────────────────────────────────────────────────────────────────
763
711
 
764
712
  /**
765
- * Governs which ObjectLayer item types may be simultaneously active on an
766
- * entity and enforces the one-per-type constraint. The server validates
767
- * every item_activation request against these rules, scoped to the item set
768
- * of the entity's current state: liveItemIds while alive, deadItemIds while
769
- * in the Fragmented State (see DEFAULT_DEAD_ITEM_ID).
713
+ * Which ObjectLayer item types may be active together on an entity. The server
714
+ * validates every item_activation against these rules, scoped to the item set
715
+ * of the entity state: liveItemIds while alive, deadItemIds while dead.
770
716
  */
771
717
  export const EQUIPMENT_RULES_DEFAULTS = Object.freeze({
772
718
  activeItemTypes: [ITEM_TYPES.skin, ITEM_TYPES.breastplate, ITEM_TYPES.weapon],
@@ -779,13 +725,10 @@ export const EQUIPMENT_RULES_DEFAULTS = Object.freeze({
779
725
  // ─────────────────────────────────────────────────────────────────────────────
780
726
 
781
727
  /**
782
- * Numeric Entity Status Indicator (ESI) IDs. The Go server stamps one of
783
- * these u8 IDs on every entity in the AOI binary wire format; the client
784
- * resolves the icon stem + border colour from its own presentation
785
- * defaults table (see `SharedDefaultsCyberia.js#STATUS_ICONS_PRESENTATION`).
786
- *
787
- * IDs MUST stay in sync with:
788
- * cyberia-server/src/entity_status.go (StatusNone … StatusResourceExtracted)
728
+ * Numeric Entity Status Indicator (ESI) IDs. The server stamps one u8 ID on
729
+ * every entity in the AOI wire format; the client resolves the icon from
730
+ * `SharedDefaultsCyberia.js#STATUS_ICONS_PRESENTATION`. These IDs are wire
731
+ * contract: append only, never renumber.
789
732
  */
790
733
  export const STATUS_ICONS = Object.freeze([
791
734
  { id: 0, name: 'none', description: 'No icon (skill/coin bots, world objects)' },
@@ -836,14 +779,14 @@ export const RESOURCE_ENTITY_TYPE_DEFAULTS = Object.freeze([
836
779
  liveItemIds: ['wood-1'],
837
780
  deadItemIds: ['wood-extracted-1'],
838
781
  dropItemIds: ['wood-drop-1'],
839
- defaultObjectLayers: [],
782
+ inventoryItemsIds: [],
840
783
  }),
841
784
  Object.freeze({
842
785
  entityType: ENTITY_TYPES.resource,
843
786
  liveItemIds: ['wood-2'],
844
787
  deadItemIds: ['wood-extracted-2'],
845
788
  dropItemIds: ['wood-drop-2'],
846
- defaultObjectLayers: [],
789
+ inventoryItemsIds: [],
847
790
  }),
848
791
  ]);
849
792
 
@@ -851,63 +794,51 @@ export const RESOURCE_ENTITY_TYPE_DEFAULTS = Object.freeze([
851
794
  export const RESOURCE_ENTITY_TYPE_DEFAULT = RESOURCE_ENTITY_TYPE_DEFAULTS[0];
852
795
 
853
796
  /**
854
- * Canonical dead-state visual (Fragmentation): applied by cyberia-server when
855
- * an entity-type default declares no deadItemIds. Dead-state items this
856
- * default and any deadItemIds configured via MongoDB form the Fragmented
857
- * State's aesthetic loadout: equippable only while the entity is dead, under
858
- * EQUIPMENT_RULES_DEFAULTS scoped to the dead-item set, and remembered across
859
- * deaths. Only this default id is excluded from the player inventory wire;
860
- * the rest appear (lock-badged while alive). No defaultObjectLayers row is
861
- * needed — the death handler appends missing dead items on demand.
862
- * Mirrored by `defaultDeadItemID` in cyberia-server/game/dead_items.go.
797
+ * Canonical dead-state visual (Fragmentation). The server applies it when an
798
+ * entity-type default declares no deadItemIds. Dead items are equippable only
799
+ * while the entity is dead and are kept across deaths. This id stays out of
800
+ * the player inventory wire; other dead items appear in it.
863
801
  */
864
802
  export const DEFAULT_DEAD_ITEM_ID = 'fragmentation';
865
803
 
866
804
  /**
867
- * Per-entity-type defaults consumed by the Go server (live / dead / drop
868
- * item IDs and the seed inventory for newly spawned entities).
805
+ * Per-entity-type defaults the simulation consumes: live / dead / drop item IDs
806
+ * and the seed inventory of a new entity.
869
807
  *
870
808
  * Field reference:
871
- * entityType — server-side category string.
872
- * liveItemIds — ObjectLayer item IDs while the entity is alive.
873
- * deadItemIds — IDs swapped in on death / ghost state.
874
- * dropItemIds — IDs granted to the killer on resource depletion.
875
- * defaultObjectLayers initial inventory rows ({itemId, active, qty}).
809
+ * entityType — server-side category string.
810
+ * liveItemIds — ObjectLayer item IDs while the entity is alive.
811
+ * deadItemIDs — IDs swapped in on death / ghost state.
812
+ * dropItemIds — IDs granted to the killer on resource depletion.
813
+ * inventoryItemsIds IDs the entity carries but never activates by lifecycle.
814
+ * overrideItemsIdsState — per-id overrides of what those lists derive.
815
+ *
816
+ * The three lifecycle lists are discriminators, not separate inventories: an entity holds one
817
+ * inventory and the runtime activates the slots the context calls for. `inventoryItemsIds` is only
818
+ * for what no lifecycle state ever activates — a coin balance, say. See
819
+ * {@link resolveEntityInventory}, which is where that single inventory is derived.
876
820
  */
877
821
  export const ENTITY_TYPE_DEFAULTS = Object.freeze([
878
822
  {
879
823
  entityType: ENTITY_TYPES.player,
880
824
  liveItemIds: ['anon', 'atlas_pistol_mk2'],
881
825
  deadItemIds: [DEFAULT_DEAD_ITEM_ID],
882
- defaultObjectLayers: [
883
- { itemId: 'anon', active: true, quantity: 1 },
884
- { itemId: 'atlas_pistol_mk2', active: true, quantity: 1 },
885
- { itemId: 'coin', active: false, quantity: 0 },
886
- ],
826
+ inventoryItemsIds: ['coin'],
887
827
  },
888
828
  {
889
829
  entityType: ENTITY_TYPES.other_player,
890
830
  liveItemIds: ['anon', 'atlas_pistol_mk2'],
891
831
  deadItemIds: [DEFAULT_DEAD_ITEM_ID],
892
- defaultObjectLayers: [
893
- { itemId: 'anon', active: true, quantity: 1 },
894
- { itemId: 'atlas_pistol_mk2', active: true, quantity: 1 },
895
- { itemId: 'coin', active: false, quantity: 0 },
896
- ],
832
+ inventoryItemsIds: ['coin'],
897
833
  },
898
834
  {
899
835
  entityType: ENTITY_TYPES.bot,
900
836
  liveItemIds: ['purple'],
901
837
  deadItemIds: [DEFAULT_DEAD_ITEM_ID],
902
- defaultObjectLayers: [
903
- { itemId: 'purple', active: true, quantity: 1 },
904
- { itemId: 'coin', active: false, quantity: 0 },
905
- ],
838
+ inventoryItemsIds: ['coin'],
906
839
  },
907
- // Fallback-world mission/action givers. Resolved by their active skin (the
908
- // liveItemIds key), these bots take the canonical `provider` behavior: they
909
- // barely move from their spawn and are immortal. Lain only talks in place, so
910
- // she is fully static. Authors can retarget any of these via EntityEngineCyberia.
840
+ // Fallback-world mission givers, keyed by their active skin. `provider` bots
841
+ // stay near their spawn and are immortal; `provider-static` bots do not move.
911
842
  { entityType: ENTITY_TYPES.bot, liveItemIds: ['wason'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
912
843
  { entityType: ENTITY_TYPES.bot, liveItemIds: ['alex'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
913
844
  { entityType: ENTITY_TYPES.bot, liveItemIds: ['agent'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
@@ -934,37 +865,277 @@ export const ENTITY_TYPE_DEFAULTS = Object.freeze([
934
865
  entityType: ENTITY_TYPES.skill,
935
866
  liveItemIds: ['atlas_pistol_mk2_bullet'],
936
867
  deadItemIds: [],
937
- defaultObjectLayers: [{ itemId: 'atlas_pistol_mk2_bullet', active: true, quantity: 1 }],
868
+ inventoryItemsIds: [],
938
869
  },
939
870
  {
940
871
  entityType: ENTITY_TYPES.coin,
941
872
  liveItemIds: ['coin'],
942
873
  deadItemIds: [],
943
- defaultObjectLayers: [{ itemId: 'coin', active: true, quantity: 1 }],
874
+ inventoryItemsIds: [],
944
875
  },
945
- { entityType: ENTITY_TYPES.floor, liveItemIds: ['grass'], deadItemIds: [], dropItemIds: [], defaultObjectLayers: [] },
946
- { entityType: ENTITY_TYPES.obstacle, liveItemIds: [], deadItemIds: [], dropItemIds: [], defaultObjectLayers: [] },
947
- { entityType: ENTITY_TYPES.portal, liveItemIds: [], deadItemIds: [], dropItemIds: [], defaultObjectLayers: [] },
948
- { entityType: ENTITY_TYPES.foreground, liveItemIds: [], deadItemIds: [], dropItemIds: [], defaultObjectLayers: [] },
876
+ { entityType: ENTITY_TYPES.floor, liveItemIds: ['grass'], deadItemIds: [], dropItemIds: [], inventoryItemsIds: [] },
877
+ { entityType: ENTITY_TYPES.obstacle, liveItemIds: [], deadItemIds: [], dropItemIds: [], inventoryItemsIds: [] },
878
+ { entityType: ENTITY_TYPES.portal, liveItemIds: [], deadItemIds: [], dropItemIds: [], inventoryItemsIds: [] },
879
+ { entityType: ENTITY_TYPES.foreground, liveItemIds: [], deadItemIds: [], dropItemIds: [], inventoryItemsIds: [] },
949
880
  // Static decorator — non-moving, passable, depth-sorted. Visuals come from the
950
881
  // map definition / world generator; no live/dead/drop rotation.
951
- { entityType: ENTITY_TYPES.static, liveItemIds: [], deadItemIds: [], dropItemIds: [], defaultObjectLayers: [] },
882
+ { entityType: ENTITY_TYPES.static, liveItemIds: [], deadItemIds: [], dropItemIds: [], inventoryItemsIds: [] },
952
883
  ...RESOURCE_ENTITY_TYPE_DEFAULTS,
953
884
  ]);
954
885
 
886
+ /**
887
+ * The default a placed entity resolves to, by the item ids it already carries.
888
+ *
889
+ * Mirrors `resolveEntityDefaultBuild` in cyberia-server/game/entity_defaults.go, which is the rule
890
+ * the simulation applies at spawn: a default matches only when the entity carries ALL of its live
891
+ * ids, and the most specific match wins — the one requiring the largest set, so `{purple, pistol}`
892
+ * beats `{purple}`. Ties keep the earlier default, which makes the order a world declares them in
893
+ * significant. With no containing match, the first default of that type answers.
894
+ *
895
+ * @param {{entityType: string, itemIds?: string[]}} entity - Placed entity type and its item ids.
896
+ * @param {Array<{entityType: string, liveItemIds?: string[]}>} defaults - Candidate defaults, in order.
897
+ * @returns {object|undefined} The matched default, or undefined when the type has none.
898
+ */
899
+ export function resolveEntityDefaultBuild({ entityType, itemIds = [] }, defaults = []) {
900
+ const carried = new Set(itemIds.filter(Boolean));
901
+ let firstOfType;
902
+ let matched;
903
+ let matchedSize = 0;
904
+ for (const candidate of defaults) {
905
+ if (candidate?.entityType !== entityType) continue;
906
+ firstOfType ??= candidate;
907
+ const liveItemIds = candidate.liveItemIds || [];
908
+ if (carried.size === 0 || liveItemIds.length === 0) continue;
909
+ if (!liveItemIds.every((itemId) => carried.has(itemId))) continue;
910
+ if (liveItemIds.length > matchedSize) {
911
+ matched = candidate;
912
+ matchedSize = liveItemIds.length;
913
+ }
914
+ }
915
+ return matched ?? firstOfType;
916
+ }
917
+
918
+ /**
919
+ * The canonical inventory contract: one entity, one inventory, derived.
920
+ *
921
+ * An entity carries the union of every item id its default names — the lifecycle discriminators
922
+ * (live / dead / drop) and the inventory-only extras — deduplicated, in that order. Which slots
923
+ * are *active* is not stored anywhere: it follows from the discriminator the id belongs to, and
924
+ * the runtime flips them as context changes (`activateOrAppendLayer` in
925
+ * cyberia-server/game/dead_items.go activates the slot that is already there). Seeding the whole
926
+ * union is what lets it activate rather than append.
927
+ *
928
+ * Spawn state is alive, so the live ids are the active ones. A worn item and an inventory-only
929
+ * item are stock the entity holds, so each starts as one unit. A dead or drop id is a lifecycle
930
+ * slot the runtime activates or scatters later, so it starts empty. The server owns the coin
931
+ * quantity from spawn on, whatever the seed says.
932
+ *
933
+ * `overrideItemsIdsState` adjusts that derivation for one carried id: `active` forces the spawn
934
+ * state — a skin the equipment rules would otherwise leave inactive — and `quantity` sizes a
935
+ * stack, which is how a drop bundle declares how many it scatters. It never adds an id: the union
936
+ * decides membership, an override only what a member starts as, so a rule naming an id no list
937
+ * carries does nothing at all.
938
+ *
939
+ * Activating one item can deactivate another. `EQUIPMENT_RULES_DEFAULTS` allows one active item
940
+ * per type, so an override that activates a skin is a decision about which skin the entity wears:
941
+ * the one the lists derived gives way to the one the author named. Only the governed types are
942
+ * constrained, and only where the item's type is known — a resource visual, a coin, anything the
943
+ * rules do not name, passes through untouched.
944
+ *
945
+ * @param {{liveItemIds?:string[],deadItemIds?:string[],dropItemIds?:string[],inventoryItemsIds?:string[],
946
+ * overrideItemsIdsState?:Array<{itemId:string,active?:boolean,quantity?:number}>}} entityDefault
947
+ * @param {{itemTypes?:Object<string,string>|Map<string,string>}} [context] - itemId → item type, where known.
948
+ * @returns {Array<{itemId:string,active:boolean,quantity:number}>} Inventory rows, as the wire carries them.
949
+ */
950
+ export function resolveEntityInventory(entityDefault = {}, { itemTypes } = {}) {
951
+ const live = new Set(entityDefault.liveItemIds || []);
952
+ const overrides = new Map(
953
+ (entityDefault.overrideItemsIdsState || []).filter((rule) => rule?.itemId).map((rule) => [rule.itemId, rule]),
954
+ );
955
+ const itemIds = [
956
+ ...new Set(
957
+ [
958
+ ...(entityDefault.liveItemIds || []),
959
+ ...(entityDefault.deadItemIds || []),
960
+ ...(entityDefault.dropItemIds || []),
961
+ ...(entityDefault.inventoryItemsIds || []),
962
+ ].filter(Boolean),
963
+ ),
964
+ ];
965
+ const drops = new Set(entityDefault.dropItemIds || []);
966
+ const stock = new Set(entityDefault.inventoryItemsIds || []);
967
+ const rows = itemIds.map((itemId) => {
968
+ const override = overrides.get(itemId);
969
+ const active = 'boolean' === typeof override?.active ? override.active : live.has(itemId);
970
+ const held = active || stock.has(itemId);
971
+ const quantity = Number.isFinite(override?.quantity) ? override.quantity : held ? 1 : 0;
972
+ // A drop id scatters on death unless an override says how often. Rows that are not drops
973
+ // carry the same 1, so every consumer reads one field and never a missing one.
974
+ const dropChance =
975
+ drops.has(itemId) && Number.isFinite(override?.dropChance)
976
+ ? Math.min(1, Math.max(0, override.dropChance))
977
+ : 1;
978
+ return { itemId, active, quantity, dropChance };
979
+ });
980
+ return applyEquipmentRules(rows, { itemTypes, overrides });
981
+ }
982
+
983
+ /**
984
+ * One active item per governed type, with the authored choice ahead of the derived one.
985
+ *
986
+ * An override that activates an item states which item of its type the entity wears; a row the
987
+ * lists merely derived yields to it. Two overrides of the same type resolve in list order, first
988
+ * kept — the same first-wins the runtime applies when it normalizes a loadout.
989
+ *
990
+ * @param {Array<{itemId:string,active:boolean,quantity:number}>} rows - Resolved inventory rows.
991
+ * @param {{itemTypes?:Object<string,string>|Map<string,string>, overrides?:Map<string,object>}} context
992
+ * @returns {Array<{itemId:string,active:boolean,quantity:number}>} The same rows, contested types settled.
993
+ */
994
+ function applyEquipmentRules(rows, { itemTypes, overrides = new Map() } = {}) {
995
+ if (!itemTypes || !EQUIPMENT_RULES_DEFAULTS.onePerType) return rows;
996
+ const typeOf = (itemId) => (itemTypes instanceof Map ? itemTypes.get(itemId) : itemTypes[itemId]) || '';
997
+ const governed = new Set(EQUIPMENT_RULES_DEFAULTS.activeItemTypes);
998
+ const claimed = new Set();
999
+ // Authored first, so an override wins the slot it asks for; the derived rows fill what is left.
1000
+ const contenders = [
1001
+ ...rows.filter((row) => row.active && overrides.get(row.itemId)?.active === true),
1002
+ ...rows.filter((row) => row.active && overrides.get(row.itemId)?.active !== true),
1003
+ ];
1004
+ const deactivated = new Set();
1005
+ for (const row of contenders) {
1006
+ const type = typeOf(row.itemId);
1007
+ if (!governed.has(type)) continue;
1008
+ if (claimed.has(type)) deactivated.add(row.itemId);
1009
+ else claimed.add(type);
1010
+ }
1011
+ // Taking something off does not take it away: the entity still carries the stack, so only the
1012
+ // worn flag changes. A row emptied here would leave the inventory holding nothing.
1013
+ return rows.map((row) => (deactivated.has(row.itemId) ? { ...row, active: false } : row));
1014
+ }
1015
+
955
1016
  // ─────────────────────────────────────────────────────────────────────────────
956
- // Player spawn
1017
+ // Audio seed content
957
1018
  // ─────────────────────────────────────────────────────────────────────────────
958
1019
 
959
1020
  /**
960
- * Canonical default player spawn policy. Mirrors PlayerSpawnSchema in
961
- * cyberia-instance.model.js and the Go PlayerSpawnConfig: when `random` is false
962
- * and `sourceMapCode` names a loaded map, new players spawn at
963
- * (sourceCellX, sourceCellY) on it; otherwise (random, or no/unknown map) they
964
- * spawn at a random walkable cell on a random map.
1021
+ * The audio bank the seeded world needs: one `cyberia-audio` module per asset code.
1022
+ *
1023
+ * `bus` names the module directory (`cyberia-audio/src/audio-module/<bus-id>/<code>`) and is the
1024
+ * asset's natural route; `options` are that module's render parameters. The code is also the
1025
+ * identity the client fetches by, so a code here must exist as a module there.
1026
+ *
1027
+ * @type {ReadonlyArray<{code:string,bus:'music'|'sfx',options?:object}>}
1028
+ */
1029
+ export const DEFAULT_AUDIO_BANK = Object.freeze([
1030
+ ...['shoot', 'coin', 'drop', 'item-pickup', 'victory', 'level-up', 'death', 'heal', 'hit', 'portal', 'ui-click', 'footsteps'].map(
1031
+ (code) => Object.freeze({ code, bus: AUDIO_BUS_SFX }),
1032
+ ),
1033
+ Object.freeze({ code: 'exploration', bus: AUDIO_BUS_MUSIC, options: { cycles: 1 } }),
1034
+ Object.freeze({ code: 'combat', bus: AUDIO_BUS_MUSIC, options: { rounds: 1 } }),
1035
+ Object.freeze({ code: 'boss', bus: AUDIO_BUS_MUSIC }),
1036
+ Object.freeze({ code: 'portal-cooldown', bus: AUDIO_BUS_MUSIC }),
1037
+ Object.freeze({ code: 'craft', bus: AUDIO_BUS_MUSIC }),
1038
+ ]);
1039
+
1040
+ /** Map-level audio settings the seed writes onto every seeded map. */
1041
+ export const DEFAULT_AUDIO_SETTINGS = Object.freeze({ volume: 0.8, crossfadeMs: 800 });
1042
+
1043
+ /**
1044
+ * The music bed a seeded map falls back to when no event is holding the bus.
1045
+ *
1046
+ * One bed for every map of a world: what a map sounds like when nothing is happening is the
1047
+ * world's ambient identity, and the events are what make a place sound different — a combat or a
1048
+ * boss bed is bound to the moment it belongs to, not to a map index.
1049
+ */
1050
+ export const DEFAULT_MAP_MUSIC = 'exploration';
1051
+
1052
+ /**
1053
+ * Canonical runtime-event → asset bindings.
1054
+ *
1055
+ * `logicEventId` is the semantic event the client emits (an audio LogicId, or a skill LogicId the
1056
+ * dispatcher runs); `audioCode` is the bank entry that answers it.
965
1057
  *
966
- * The canonical default is a random spawn so a fresh or procedural world never
967
- * piles every new player onto a single cell.
1058
+ * @type {ReadonlyArray<{logicEventId:string,audioCode:string}>}
1059
+ */
1060
+ export const DEFAULT_AUDIO_BINDINGS = Object.freeze([
1061
+ Object.freeze({ logicEventId: 'projectile', audioCode: 'shoot' }),
1062
+ Object.freeze({ logicEventId: 'coin_drop_or_transaction', audioCode: 'coin' }),
1063
+ Object.freeze({ logicEventId: 'drop', audioCode: 'drop' }),
1064
+ Object.freeze({ logicEventId: 'item-pickup', audioCode: 'item-pickup' }),
1065
+ Object.freeze({ logicEventId: 'craft', audioCode: 'craft' }),
1066
+ Object.freeze({ logicEventId: 'heal', audioCode: 'heal' }),
1067
+ Object.freeze({ logicEventId: 'hit', audioCode: 'hit' }),
1068
+ Object.freeze({ logicEventId: 'portal', audioCode: 'portal' }),
1069
+ Object.freeze({ logicEventId: 'ui-click', audioCode: 'ui-click' }),
1070
+ Object.freeze({ logicEventId: 'footsteps', audioCode: 'footsteps' }),
1071
+ Object.freeze({ logicEventId: 'combat', audioCode: 'combat' }),
1072
+ Object.freeze({ logicEventId: 'boss', audioCode: 'boss' }),
1073
+ Object.freeze({ logicEventId: 'victory', audioCode: 'victory' }),
1074
+ Object.freeze({ logicEventId: 'level-up', audioCode: 'level-up' }),
1075
+ Object.freeze({ logicEventId: 'death', audioCode: 'death' }),
1076
+ Object.freeze({ logicEventId: 'portal-cooldown', audioCode: 'portal-cooldown' }),
1077
+ ]);
1078
+
1079
+ const AUDIO_BANK_CODES = DEFAULT_AUDIO_BANK.map(({ code }) => code);
1080
+
1081
+ for (const { code, bus } of DEFAULT_AUDIO_BANK) {
1082
+ if (!AUDIO_BUSES.includes(bus)) {
1083
+ throw new Error(`DEFAULT_AUDIO_BANK: "${code}" names bus "${bus}". Allowed: ${AUDIO_BUSES.join(', ')}`);
1084
+ }
1085
+ }
1086
+
1087
+ // Fail fast on an audio binding the runtime cannot honour: an unknown event would never fire, and
1088
+ // an unbanked code would leave the client requesting an asset the seed never imported. Both read
1089
+ // as silence at play time, so they must surface at boot instead.
1090
+ for (const { logicEventId, audioCode } of DEFAULT_AUDIO_BINDINGS) {
1091
+ if (!isCanonicalAudioLogicId(logicEventId)) {
1092
+ throw new Error(
1093
+ `DEFAULT_AUDIO_BINDINGS: unknown logicEventId "${logicEventId}". ` +
1094
+ `Allowed (SharedDefaultsCyberia): ${[...AUDIO_LOGIC_ID_VALUES, ...SKILL_LOGIC_ID_VALUES].join(', ')}`,
1095
+ );
1096
+ }
1097
+ if (!AUDIO_BANK_CODES.includes(audioCode)) {
1098
+ throw new Error(
1099
+ `DEFAULT_AUDIO_BINDINGS: "${logicEventId}" binds audioCode "${audioCode}", ` +
1100
+ `absent from DEFAULT_AUDIO_BANK: ${AUDIO_BANK_CODES.join(', ')}`,
1101
+ );
1102
+ }
1103
+ }
1104
+ if (!AUDIO_BANK_CODES.includes(DEFAULT_MAP_MUSIC)) {
1105
+ throw new Error(`Default map music "${DEFAULT_MAP_MUSIC}" is absent from DEFAULT_AUDIO_BANK`);
1106
+ }
1107
+
1108
+ /**
1109
+ * Expands {@link DEFAULT_AUDIO_BINDINGS} into `cyberia-map-audio-conf` event records.
1110
+ *
1111
+ * Routing is derived, not restated: an event's bus comes from the canonical audio registry
1112
+ * (a skill LogicId is a one-shot, so it falls back to sfx), music holds the bed in a loop unless
1113
+ * the binding is a one-shot cue, and only music crossfades.
1114
+ *
1115
+ * @param {{volume?:number,crossfadeMs?:number}} [settings=DEFAULT_AUDIO_SETTINGS]
1116
+ * @returns {Array<{logicEventId:string,audioCode:string,settings:object}>}
1117
+ */
1118
+ export function buildAudioEventBindings(settings = DEFAULT_AUDIO_SETTINGS) {
1119
+ const { volume, crossfadeMs } = { ...DEFAULT_AUDIO_SETTINGS, ...settings };
1120
+ return DEFAULT_AUDIO_BINDINGS.map(({ logicEventId, audioCode }) => {
1121
+ const bus = AUDIO_LOGIC_ID_BUSES[logicEventId] ?? AUDIO_BUS_SFX;
1122
+ const music = AUDIO_BUS_MUSIC === bus;
1123
+ return {
1124
+ logicEventId,
1125
+ audioCode,
1126
+ settings: { bus, volume, loop: music, crossfadeMs: music ? crossfadeMs : 0 },
1127
+ };
1128
+ });
1129
+ }
1130
+
1131
+ // ─────────────────────────────────────────────────────────────────────────────
1132
+ // Player spawn
1133
+ // ─────────────────────────────────────────────────────────────────────────────
1134
+
1135
+ /**
1136
+ * Default player spawn policy, the shape of PlayerSpawnSchema. With `random`
1137
+ * false and `sourceMapCode` naming a loaded map, players spawn at
1138
+ * (sourceCellX, sourceCellY). Otherwise they spawn on a random walkable cell.
968
1139
  */
969
1140
  export const DEFAULT_PLAYER_SPAWN = Object.freeze({
970
1141
  sourceMapCode: 'fallback-map-0',
@@ -986,6 +1157,58 @@ export const DEFAULT_PLAYER_SPAWN = Object.freeze({
986
1157
  * forbidden — see `SharedDefaultsCyberia.js` and the
987
1158
  * `/api/cyberia-client-hints` REST endpoint for presentation overrides.
988
1159
  */
1160
+ export const PROGRESSION_RULES_DEFAULTS = Object.freeze({
1161
+ maxLevel: 100,
1162
+ xpPerLevel: 100,
1163
+ baseStats: Object.freeze({ effect: 5, resistance: 10, agility: 0, range: 0, intelligence: 0, utility: 0 }),
1164
+ perLevelStats: Object.freeze({ effect: 2, resistance: 5, agility: 1, range: 10, intelligence: 1, utility: 1 }),
1165
+ killXp: 25,
1166
+ questXp: 100,
1167
+ objectiveXp: 10,
1168
+ minAwardIntervalMs: 500,
1169
+ repeatWindowMs: 60000,
1170
+ maxRepeatAwards: 4,
1171
+ maxAwardsPerWindow: 30,
1172
+ defaultBotLevel: 1,
1173
+ });
1174
+
1175
+ export const PROGRESSION_RULE_LIMITS = Object.freeze(Object.fromEntries(Object.entries({
1176
+ maxLevel: [1, ENTITY_LEVEL_MAX], xpPerLevel: [1, 1000000],
1177
+ killXp: [0, 1000000], questXp: [0, 1000000], objectiveXp: [0, 1000000],
1178
+ minAwardIntervalMs: [1, 60000], repeatWindowMs: [1000, 3600000],
1179
+ maxRepeatAwards: [1, 100], maxAwardsPerWindow: [1, 1000], defaultBotLevel: [1, ENTITY_LEVEL_MAX],
1180
+ }).map(([key, bounds]) => [key, Object.freeze(bounds)])));
1181
+
1182
+ export function resolveProgressionRules(input = {}) {
1183
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('Progression rules must be an object.');
1184
+ const defaults = PROGRESSION_RULES_DEFAULTS;
1185
+ const rules = {
1186
+ ...defaults, ...input,
1187
+ baseStats: { ...defaults.baseStats, ...input.baseStats },
1188
+ perLevelStats: { ...defaults.perLevelStats, ...input.perLevelStats },
1189
+ };
1190
+ for (const key of ['baseStats', 'perLevelStats']) {
1191
+ if (input[key] !== undefined && (!input[key] || typeof input[key] !== 'object' || Array.isArray(input[key]))) {
1192
+ throw new TypeError('Base stat curve must be an object: ' + key);
1193
+ }
1194
+ }
1195
+ if (rules.defaultBotLevel > rules.maxLevel) throw new RangeError('Default bot level exceeds maximum level.');
1196
+ for (const [key, [min, max]] of Object.entries(PROGRESSION_RULE_LIMITS)) {
1197
+ if (!Number.isInteger(rules[key]) || rules[key] < min || rules[key] > max) throw new RangeError('Invalid progression rule: ' + key);
1198
+ }
1199
+ for (const block of [rules.baseStats, rules.perLevelStats]) {
1200
+ for (const [key, value] of Object.entries(block)) {
1201
+ if (!STAT_TYPES.includes(key) || !Number.isInteger(value) || value < 0 || value > STAT_MODIFIER_MAX) {
1202
+ throw new RangeError('Invalid base stat curve: ' + key);
1203
+ }
1204
+ }
1205
+ }
1206
+ for (const key of Object.keys(rules)) {
1207
+ if (!(key in defaults)) throw new TypeError('Unknown progression rule: ' + key);
1208
+ }
1209
+ return rules;
1210
+ }
1211
+
989
1212
  export const CYBERIA_INSTANCE_CONF_DEFAULTS = {
990
1213
  // ── Tick model ─────────────────────────────────────────────────────
991
1214
  tickRate: 60,
@@ -1014,7 +1237,7 @@ export const CYBERIA_INSTANCE_CONF_DEFAULTS = {
1014
1237
  // projectiles and every other entity keep entityBaseSpeed. 0 falls back to
1015
1238
  // entityBaseSpeed.
1016
1239
  playerBaseSpeed: 8,
1017
- sumStatsLimit: 500,
1240
+ progressionRules: PROGRESSION_RULES_DEFAULTS,
1018
1241
  maxActiveLayers: 4,
1019
1242
  initialLifeFraction: 1.0,
1020
1243
 
@@ -1034,29 +1257,33 @@ export const CYBERIA_INSTANCE_CONF_DEFAULTS = {
1034
1257
  craftingFeePercent: 0.0,
1035
1258
  },
1036
1259
 
1037
- // ── Regen ──────────────────────────────────────────────────────────
1038
- lifeRegenChance: 300,
1039
- maxChance: 10000,
1260
+ // ── Chances, as fractions of 1 ───────────────────────────────────
1261
+ // A tap has lifeRegenChance to regenerate life; utility raises it. No chance the
1262
+ // stats raise passes maxChance, so nothing becomes a certainty.
1263
+ lifeRegenChance: 0.15,
1264
+ maxChance: 0.95,
1040
1265
 
1041
1266
  // ── Per-entity-type defaults ───────────────────────────────────────
1042
- entityDefaults: ENTITY_TYPE_DEFAULTS.map((e) => ({ ...e })),
1267
+ // References into the CyberiaEntityTypeDefault collection — see the schema.
1268
+ // Empty by default: a conf that names no document resolves against the
1269
+ // canonical ENTITY_TYPE_DEFAULTS above, so a fresh world is complete without
1270
+ // owning a single row.
1271
+ entityDefaults: [],
1043
1272
 
1044
1273
  // ── Status icons (numeric IDs only — visuals live in client defaults) ──
1045
1274
  statusIcons: STATUS_ICONS.map((s) => ({ ...s })),
1046
1275
 
1047
1276
  // ── Skill system ───────────────────────────────────────────────────
1048
- // Canonical skill definitions live in DefaultSkillConfig above.
1049
- // This reference is the single point of consumption for the gRPC
1050
- // fallback defaults any change goes through DefaultSkillConfig.
1051
- skillConfig: DefaultSkillConfig,
1052
-
1277
+ // No skillConfig: the skills an instance runs are derived from its own content, never
1278
+ // stored on the conf. See DefaultSkillConfig above for the definitions and
1279
+ // cyberia-instance-items.js for the membership rule.
1053
1280
  skillRules: {
1054
- projectileSpawnChance: 0.5,
1281
+ projectileSpawnChance: 0.75,
1055
1282
  projectileLifetimeMs: 2000,
1056
1283
  projectileWidth: 1,
1057
1284
  projectileHeight: 1,
1058
1285
  projectileSpeedMultiplier: 3,
1059
- doppelgangerSpawnChance: 0.5,
1286
+ doppelgangerSpawnChance: 0.6,
1060
1287
  doppelgangerLifetimeMs: 5000,
1061
1288
  doppelgangerSpawnRadius: 3,
1062
1289
  doppelgangerInitialLifeFraction: 1.0,
@@ -1078,7 +1305,7 @@ const _cloneDefault = (v) => JSON.parse(JSON.stringify(v));
1078
1305
  * Recursively backfill one value against its canonical default:
1079
1306
  * - null / undefined → deep clone of the default
1080
1307
  * - empty array → deep clone of the default (config lists must never
1081
- * export empty — e.g. skillConfig, entityDefaults)
1308
+ * export empty — e.g. entityDefaults, statusIcons)
1082
1309
  * - non-empty array → kept verbatim (author content)
1083
1310
  * - plain object → keep author-set keys, recurse to fill missing default keys
1084
1311
  * - scalar → kept verbatim when present (0, '', false count as present)
@@ -1098,26 +1325,12 @@ function _deepFillDefaults(value, defaultValue) {
1098
1325
  return value;
1099
1326
  }
1100
1327
 
1101
- /**
1102
- * Normalise a skillConfig array to the shape CyberiaInstanceConfSchema stores
1103
- * (`SkillConfigEntrySchema` = { triggerItemId, logicEventIds }). The canonical
1104
- * DefaultSkillConfig carries extra `skills` metadata the schema drops on write;
1105
- * stripping it here keeps export ⇄ DB round-trips churn-free.
1106
- */
1107
- function _normaliseSkillConfig(skillConfig) {
1108
- if (!Array.isArray(skillConfig)) return [];
1109
- return skillConfig.map((entry) => ({
1110
- triggerItemId: entry.triggerItemId,
1111
- logicEventIds: [...(entry.logicEventIds || [])],
1112
- }));
1113
- }
1114
-
1115
1328
  /**
1116
1329
  * Return a copy of a CyberiaInstanceConf document with every field defined by
1117
1330
  * CyberiaInstanceConfSchema present. Missing, null/undefined, or empty-array
1118
1331
  * fields — common when a doc is read with `.lean()` (which skips Mongoose schema
1119
1332
  * defaults), was created before a schema field existed, or was seeded with the
1120
- * schema's empty-array default (e.g. `skillConfig`) — are filled from
1333
+ * schema's empty-array default (e.g. `entityDefaults`) — are filled from
1121
1334
  * CYBERIA_INSTANCE_CONF_DEFAULTS, the same canonical values the fallback world
1122
1335
  * uses. Author-set scalars (including 0, '', false), non-empty arrays, and DB
1123
1336
  * metadata (_id, instanceCode, timestamps) are preserved untouched.
@@ -1131,6 +1344,8 @@ export function fillInstanceConfDefaults(conf = {}) {
1131
1344
  for (const key of Object.keys(CYBERIA_INSTANCE_CONF_DEFAULTS)) {
1132
1345
  out[key] = _deepFillDefaults(source[key], CYBERIA_INSTANCE_CONF_DEFAULTS[key]);
1133
1346
  }
1134
- out.skillConfig = _normaliseSkillConfig(out.skillConfig);
1347
+ // A conf from before skills were derived still carries the field; it is not part of the
1348
+ // schema any more, and leaving it would put a stale second answer back on the wire.
1349
+ delete out.skillConfig;
1135
1350
  return out;
1136
1351
  }