@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,14 +1,15 @@
1
1
  import { BtnIcon } from '../core/BtnIcon.js';
2
- import { Input } from '../core/Input.js';
3
2
  import { commonModeratorGuard } from '../core/CommonJs.js';
4
3
  import { htmls, s } from '../core/VanillaJs.js';
5
4
  import { NotificationManager } from '../core/NotificationManager.js';
6
5
  import { Translate } from '../core/Translate.js';
7
- import { darkTheme, dynamicCol, ThemeEvents } from '../core/Css.js';
6
+ import { darkTheme, ThemeEvents } from '../core/Css.js';
8
7
  import { DropDown } from '../core/DropDown.js';
9
8
  import { AgGrid } from '../core/AgGrid.js';
9
+ import { getProxyPath } from '../core/Router.js';
10
10
  import { ObjectLayerService } from '../../services/object-layer/object-layer.service.js';
11
11
  import { CyberiaEntityTypeDefaultService } from '../../services/cyberia-entity-type-default/cyberia-entity-type-default.service.js';
12
+ import { CyberiaInstanceConfService } from '../../services/cyberia-instance-conf/cyberia-instance-conf.service.js';
12
13
  import { ENTITY_TYPES, SELECTABLE_ENTITY_BEHAVIORS } from './SharedDefaultsCyberia.js';
13
14
 
14
15
  // DropDown invokes optionData.onClick on selection, so every option must carry one.
@@ -19,8 +20,8 @@ const subtleBorder = () => (darkTheme ? '#444' : '#e0e0e0');
19
20
  // EntityEngineCyberia — CRUD surface for the cyberia-entity-type-default
20
21
  // collection (the DB-backed mirror of ENTITY_TYPE_DEFAULTS). An ag-grid lists
21
22
  // every default; clicking a row loads it into the form. The form binds an
22
- // entityType to its lifecycle item-id sets (live / dead / drop) plus the seed
23
- // defaultObjectLayers, with server-backed item-id autocomplete pickers so the
23
+ // entityType to its lifecycle item-id sets (live / dead / drop) plus the
24
+ // inventory-only extras, with server-backed item-id autocomplete pickers so the
24
25
  // high-frequency itemId fields stay fast and typo-free. The runtime linking
25
26
  // rule (entity matches entityType AND carries an active itemId from liveItemIds)
26
27
  // is content-authority only; this engine never mutates entities or the
@@ -33,7 +34,17 @@ class EntityEngineCyberia {
33
34
  static liveItemIds = [];
34
35
  static deadItemIds = [];
35
36
  static dropItemIds = [];
36
- static defaultObjectLayers = [];
37
+ static inventoryItemsIds = [];
38
+ // Per-id overrides of what the lists derive: { itemId, active, quantity }.
39
+ static overrideItemsIdsState = [];
40
+ // Instance codes whose conf references the loaded default, and every code that could.
41
+ static instanceCodes = [];
42
+ static availableInstanceCodes = [];
43
+ // entityDefault _id → instance codes, for the grid column. Built with the list.
44
+ static instanceLinks = new Map();
45
+ // itemId → item type. A sprite lives under its type, so an item id alone cannot be previewed.
46
+ // Types never change, so this is a cache that only ever grows: ids reach it from the list and
47
+ // from every id added in the form, which is what makes a just-picked item preview immediately.
37
48
 
38
49
  static gridId = 'entity-engine-grid';
39
50
 
@@ -43,7 +54,9 @@ class EntityEngineCyberia {
43
54
  liveItemPicker: 'entity-engine-live-item-picker',
44
55
  deadItemPicker: 'entity-engine-dead-item-picker',
45
56
  dropItemPicker: 'entity-engine-drop-item-picker',
46
- layerItemPicker: 'entity-engine-layer-item-picker',
57
+ inventoryItemPicker: 'entity-engine-inventory-item-picker',
58
+ instancePicker: 'entity-engine-instance-picker',
59
+ overrideItemPicker: 'entity-engine-override-item-picker',
47
60
  };
48
61
 
49
62
  // Behavior options: a leading "auto" (empty → runtime derives armed→hostile,
@@ -84,20 +97,51 @@ class EntityEngineCyberia {
84
97
  containerClass: 'inl',
85
98
  serviceProvider: async (q) => {
86
99
  const result = await ObjectLayerService.searchItemIds({ q });
87
- if (result.status === 'success' && result.data?.itemIds) {
88
- return result.data.itemIds.map((itemId) => dropdownOption(itemId));
100
+ if (result.status === 'success' && result.data?.items) {
101
+ return result.data.items.map(({ id }) => dropdownOption(id));
89
102
  }
90
103
  return [];
91
104
  },
92
105
  });
93
106
  }
94
107
 
95
- // ── Item-id list editors (live / dead / drop) ────────────────────────────
108
+ // Instance codes are a small, already-loaded set, so the picker filters them in place rather
109
+ // than round-tripping per keystroke the way the item-id pickers do.
110
+ static async buildInstanceDropdown(id) {
111
+ return await DropDown.instance({
112
+ id,
113
+ label: html`Search instance code`,
114
+ data: [],
115
+ containerClass: 'inl',
116
+ serviceProvider: async (q) => {
117
+ const query = String(q || '').toLowerCase();
118
+ return EntityEngineCyberia.availableInstanceCodes
119
+ .filter((code) => !query || code.toLowerCase().includes(query))
120
+ .map((code) => dropdownOption(code));
121
+ },
122
+ });
123
+ }
124
+
125
+ // ── Item-id list editors ─────────────────────────────────────────────────
126
+ // The lifecycle discriminators plus the inventory-only extras. All four are plain id lists:
127
+ // what an entity carries is their union, and what is active follows from the list, never from
128
+ // a flag stored per row. Form field → stored key, so nothing that holds item ids can be left
129
+ // out of the type resolution or the grid.
130
+ static ITEM_FIELD_KEYS = Object.freeze({
131
+ live: 'liveItemIds',
132
+ dead: 'deadItemIds',
133
+ drop: 'dropItemIds',
134
+ inventory: 'inventoryItemsIds',
135
+ });
136
+
137
+ static ITEM_FIELDS = Object.keys(EntityEngineCyberia.ITEM_FIELD_KEYS);
138
+
96
139
  static listFieldOf(field) {
97
140
  return {
98
141
  live: EntityEngineCyberia.liveItemIds,
99
142
  dead: EntityEngineCyberia.deadItemIds,
100
143
  drop: EntityEngineCyberia.dropItemIds,
144
+ inventory: EntityEngineCyberia.inventoryItemsIds,
101
145
  }[field];
102
146
  }
103
147
 
@@ -111,7 +155,9 @@ class EntityEngineCyberia {
111
155
  class="fl"
112
156
  style="border-bottom:1px solid ${subtleBorder()};padding:3px 0;align-items:center;font-size:12px;font-family:monospace;"
113
157
  >
114
- <div class="in fll" style="flex:1;">${items[i]}</div>
158
+ <div class="in fll" style="flex:1;display:flex;align-items:center;gap:6px;">
159
+ ${EntityEngineCyberia.itemPreview(items[i])}${items[i]}
160
+ </div>
115
161
  ${await BtnIcon.instance({
116
162
  class: `btn-entity-engine-rm-${field}-${i}`,
117
163
  label: html`<i class="fa-solid fa-xmark"></i>`,
@@ -130,7 +176,7 @@ class EntityEngineCyberia {
130
176
  }
131
177
  }
132
178
 
133
- static addItem(field, pickerId) {
179
+ static async addItem(field, pickerId) {
134
180
  const itemId = EntityEngineCyberia.getSingleDropdownValue(pickerId);
135
181
  if (!itemId) {
136
182
  NotificationManager.Push({ html: 'Select an itemId first.', status: 'error' });
@@ -143,83 +189,342 @@ class EntityEngineCyberia {
143
189
  }
144
190
  list.push(itemId);
145
191
  EntityEngineCyberia.setSingleDropdownValue(pickerId, '');
146
- EntityEngineCyberia.renderItemList(field);
192
+ await EntityEngineCyberia.renderItemList(field);
193
+ }
194
+
195
+ // ── Override rules ({ itemId, active, quantity }) ─────────────────────────
196
+ // Every id the four lists carry, which is exactly what an override may adjust.
197
+ static carriedItemIds() {
198
+ return [
199
+ ...new Set(
200
+ EntityEngineCyberia.ITEM_FIELDS.flatMap((field) => EntityEngineCyberia.listFieldOf(field)).filter(Boolean),
201
+ ),
202
+ ];
147
203
  }
148
204
 
149
- // ── Default object-layer rows ({ itemId, active, quantity }) ──────────────
150
- static async renderLayerList() {
151
- const container = s('.entity-engine-layer-list');
205
+ static async renderOverrideList() {
206
+ const container = s('.entity-engine-override-list');
152
207
  if (!container) return;
208
+ const carried = new Set(EntityEngineCyberia.carriedItemIds());
153
209
  let out = '';
154
- for (let i = 0; i < EntityEngineCyberia.defaultObjectLayers.length; i++) {
155
- const l = EntityEngineCyberia.defaultObjectLayers[i];
210
+ for (let i = 0; i < EntityEngineCyberia.overrideItemsIdsState.length; i++) {
211
+ const rule = EntityEngineCyberia.overrideItemsIdsState[i];
212
+ // An override for an id no list carries is inert — the union decides membership.
213
+ const orphan = !carried.has(rule.itemId);
214
+ // Drop chance only reaches the simulation for an id the drop list carries, so the input is
215
+ // offered where it can do something and the reason is stated where it cannot.
216
+ const isDrop = EntityEngineCyberia.listFieldOf('drop').includes(rule.itemId);
156
217
  out += html`<div
157
218
  class="fl"
158
219
  style="border-bottom:1px solid ${subtleBorder()};padding:3px 0;align-items:center;font-size:12px;font-family:monospace;"
159
220
  >
160
- <div class="in fll" style="flex:1;">
161
- ${l.itemId}
162
- <span style="color:${l.active ? (darkTheme ? '#7ec77e' : '#2a7d2a') : '#888'};">
163
- · ${l.active ? 'active' : 'inactive'}</span
164
- >
165
- · ×${l.quantity}
221
+ <div class="in fll" style="flex:1;display:flex;align-items:center;gap:6px;">
222
+ ${EntityEngineCyberia.itemPreview(rule.itemId)}
223
+ <span style="${orphan ? 'color:#c90;' : ''}">${rule.itemId}</span>
224
+ <label style="cursor:pointer;display:inline-flex;align-items:center;gap:4px;">
225
+ <input class="entity-engine-override-active-${i}" type="checkbox" ${rule.active ? 'checked' : ''} />
226
+ active
227
+ </label>
228
+ <span>×</span>
229
+ <input
230
+ class="entity-engine-override-qty-${i}"
231
+ type="number"
232
+ min="1"
233
+ value="${rule.quantity}"
234
+ style="width:60px;"
235
+ />
236
+ ${isDrop
237
+ ? html`<label
238
+ style="cursor:pointer;display:inline-flex;align-items:center;gap:4px;"
239
+ title="Probability this id scatters on death. 1 always drops, 0 never does."
240
+ >
241
+ drop
242
+ <input
243
+ class="entity-engine-override-drop-chance-${i}"
244
+ type="number"
245
+ min="0"
246
+ max="1"
247
+ step="0.05"
248
+ value="${Number.isFinite(rule.dropChance) ? rule.dropChance : 1}"
249
+ style="width:60px;"
250
+ />
251
+ </label>`
252
+ : ''}
253
+ ${orphan ? html`<span style="color:#c90;font-size:11px;">no list carries this id</span>` : ''}
166
254
  </div>
167
255
  ${await BtnIcon.instance({
168
- class: `btn-entity-engine-rm-layer-${i}`,
256
+ class: `btn-entity-engine-rm-override-${i}`,
169
257
  label: html`<i class="fa-solid fa-xmark"></i>`,
170
258
  style: 'min-width:30px;padding:2px 8px;',
171
259
  })}
172
260
  </div>`;
173
261
  }
174
- if (!out) out = '<div style="color:#888;font-size:12px;">No default object layers.</div>';
175
- htmls('.entity-engine-layer-list', out);
176
- for (let i = 0; i < EntityEngineCyberia.defaultObjectLayers.length; i++) {
177
- if (s(`.btn-entity-engine-rm-layer-${i}`))
178
- s(`.btn-entity-engine-rm-layer-${i}`).onclick = () => {
179
- EntityEngineCyberia.defaultObjectLayers.splice(i, 1);
180
- EntityEngineCyberia.renderLayerList();
262
+ if (!out) out = '<div style="color:#888;font-size:12px;">No overrides every id starts as its list derives.</div>';
263
+ htmls('.entity-engine-override-list', out);
264
+ for (let i = 0; i < EntityEngineCyberia.overrideItemsIdsState.length; i++) {
265
+ const active = s(`.entity-engine-override-active-${i}`);
266
+ if (active) active.onchange = () => (EntityEngineCyberia.overrideItemsIdsState[i].active = active.checked);
267
+ const qty = s(`.entity-engine-override-qty-${i}`);
268
+ if (qty)
269
+ qty.onchange = () =>
270
+ (EntityEngineCyberia.overrideItemsIdsState[i].quantity = Math.max(1, parseInt(qty.value) || 1));
271
+ const dropChance = s(`.entity-engine-override-drop-chance-${i}`);
272
+ if (dropChance)
273
+ dropChance.onchange = () => {
274
+ const parsed = parseFloat(dropChance.value);
275
+ const rule = EntityEngineCyberia.overrideItemsIdsState[i];
276
+ // An empty or unreadable box means "say nothing", which the whole pipeline reads as
277
+ // always drops — the same as a world that never set one.
278
+ if (!Number.isFinite(parsed)) delete rule.dropChance;
279
+ else rule.dropChance = Math.min(1, Math.max(0, parsed));
280
+ dropChance.value = Number.isFinite(rule.dropChance) ? rule.dropChance : 1;
281
+ };
282
+ if (s(`.btn-entity-engine-rm-override-${i}`))
283
+ s(`.btn-entity-engine-rm-override-${i}`).onclick = () => {
284
+ EntityEngineCyberia.overrideItemsIdsState.splice(i, 1);
285
+ EntityEngineCyberia.renderOverrideList();
181
286
  };
182
287
  }
183
288
  }
184
289
 
185
- static addLayer() {
186
- const itemId = EntityEngineCyberia.getSingleDropdownValue(EntityEngineCyberia.ids.layerItemPicker);
290
+ static async addOverride() {
291
+ const itemId = EntityEngineCyberia.getSingleDropdownValue(EntityEngineCyberia.ids.overrideItemPicker);
187
292
  if (!itemId) {
188
- NotificationManager.Push({ html: 'Select an itemId for the object layer.', status: 'error' });
293
+ NotificationManager.Push({ html: 'Select an itemId to override.', status: 'error' });
294
+ return;
295
+ }
296
+ if (EntityEngineCyberia.overrideItemsIdsState.some((rule) => rule.itemId === itemId)) {
297
+ NotificationManager.Push({ html: `"${itemId}" already has an override.`, status: 'warning' });
298
+ return;
299
+ }
300
+ EntityEngineCyberia.overrideItemsIdsState.push({
301
+ itemId,
302
+ active: EntityEngineCyberia.listFieldOf('live').includes(itemId),
303
+ quantity: 1,
304
+ });
305
+ EntityEngineCyberia.setSingleDropdownValue(EntityEngineCyberia.ids.overrideItemPicker, '');
306
+ await EntityEngineCyberia.renderOverrideList();
307
+ }
308
+
309
+ // ── Instance links (CyberiaInstanceConf.entityDefaults) ───────────────────
310
+ static async renderInstanceList() {
311
+ const container = s('.entity-engine-instance-list');
312
+ if (!container) return;
313
+ let out = '';
314
+ for (let i = 0; i < EntityEngineCyberia.instanceCodes.length; i++) {
315
+ out += html`<div
316
+ class="fl"
317
+ style="border-bottom:1px solid ${subtleBorder()};padding:3px 0;align-items:center;font-size:12px;font-family:monospace;"
318
+ >
319
+ <div class="in fll" style="flex:1;">${EntityEngineCyberia.instanceCodes[i]}</div>
320
+ ${await BtnIcon.instance({
321
+ class: `btn-entity-engine-rm-instance-${i}`,
322
+ label: html`<i class="fa-solid fa-xmark"></i>`,
323
+ style: 'min-width:30px;padding:2px 8px;',
324
+ })}
325
+ </div>`;
326
+ }
327
+ if (!out) {
328
+ out = html`<div style="color:#888;font-size:12px;">
329
+ No instance runs on this default — it reaches no world until one references it.
330
+ </div>`;
331
+ }
332
+ htmls('.entity-engine-instance-list', out);
333
+ for (let i = 0; i < EntityEngineCyberia.instanceCodes.length; i++) {
334
+ if (s(`.btn-entity-engine-rm-instance-${i}`))
335
+ s(`.btn-entity-engine-rm-instance-${i}`).onclick = () => {
336
+ EntityEngineCyberia.instanceCodes.splice(i, 1);
337
+ EntityEngineCyberia.renderInstanceList();
338
+ };
339
+ }
340
+ }
341
+
342
+ static addInstance() {
343
+ const code = EntityEngineCyberia.getSingleDropdownValue(EntityEngineCyberia.ids.instancePicker);
344
+ if (!code) {
345
+ NotificationManager.Push({ html: 'Select an instance code first.', status: 'error' });
189
346
  return;
190
347
  }
191
- if (EntityEngineCyberia.defaultObjectLayers.some((l) => l.itemId === itemId)) {
192
- NotificationManager.Push({ html: `Object layer "${itemId}" already added.`, status: 'warning' });
348
+ if (EntityEngineCyberia.instanceCodes.includes(code)) {
349
+ NotificationManager.Push({ html: `"${code}" already linked.`, status: 'warning' });
193
350
  return;
194
351
  }
195
- const active = !!s('.entity-engine-layer-active')?.checked;
196
- const quantity = Math.max(0, parseInt(s('.entity-engine-layer-qty')?.value) || 0);
197
- EntityEngineCyberia.defaultObjectLayers.push({ itemId, active, quantity });
198
- EntityEngineCyberia.setSingleDropdownValue(EntityEngineCyberia.ids.layerItemPicker, '');
199
- if (s('.entity-engine-layer-active')) s('.entity-engine-layer-active').checked = false;
200
- if (s('.entity-engine-layer-qty')) s('.entity-engine-layer-qty').value = 0;
201
- EntityEngineCyberia.renderLayerList();
352
+ EntityEngineCyberia.instanceCodes.push(code);
353
+ EntityEngineCyberia.setSingleDropdownValue(EntityEngineCyberia.ids.instancePicker, '');
354
+ EntityEngineCyberia.renderInstanceList();
355
+ }
356
+
357
+ // Persisted after the default itself: an instance can only reference a document that exists.
358
+ static async persistInstanceLinks() {
359
+ if (!EntityEngineCyberia.currentId) return;
360
+ const result = await CyberiaEntityTypeDefaultService.setInstances({
361
+ id: EntityEngineCyberia.currentId,
362
+ body: { instanceCodes: EntityEngineCyberia.instanceCodes },
363
+ });
364
+ if (result.status === 'error') {
365
+ NotificationManager.Push({ html: result.message, status: 'error' });
366
+ return;
367
+ }
368
+ const { linked = [], unlinked = [] } = result.data || {};
369
+ if (linked.length || unlinked.length) {
370
+ NotificationManager.Push({
371
+ html: `Instances updated${linked.length ? ` · linked ${linked.join(', ')}` : ''}${
372
+ unlinked.length ? ` · unlinked ${unlinked.join(', ')}` : ''
373
+ }`,
374
+ status: 'success',
375
+ });
376
+ }
202
377
  }
203
378
 
204
379
  // ── Grid (CRUD list surface) ──────────────────────────────────────────────
380
+ static ITEM_PREVIEW_PX = 60;
381
+
382
+ // The item's first idle frame, or the placeholder when its type is unresolved or the art is
383
+ // missing. Both absences look the same on purpose: either way the runtime cannot draw it.
384
+ static itemPreview(itemId) {
385
+ const size = EntityEngineCyberia.ITEM_PREVIEW_PX;
386
+ const placeholder = (display) =>
387
+ html`<div style="width:${size}px;height:${size}px;display:${display};align-items:center;justify-content:center;">
388
+ <i class="fas fa-image" style="font-size:${Math.round(size / 2)}px;color:#999;"></i>
389
+ </div>`;
390
+ return html`<img
391
+ src="${getProxyPath()}api/atlas-sprite-sheet/idle-preview/${itemId}"
392
+ style="width:${size}px;height:${size}px;display:block;image-rendering:pixelated;"
393
+ alt="${itemId}"
394
+ onerror="this.style.display='none';this.nextElementSibling.style.display='flex';"
395
+ />
396
+ ${placeholder('none')}`;
397
+ }
398
+
399
+ // Item ids read as a strip of sprites, each labelled with its id: a default is recognised by
400
+ // what it looks like long before anyone reads the ids. Arrays stay arrays on the row so the
401
+ // renderer can lay them out; ag-grid's text filter still sees the comma-joined form.
402
+ static itemIdsRenderer(field) {
403
+ return class {
404
+ eGui;
405
+
406
+ init(params) {
407
+ this.eGui = document.createElement('div');
408
+ const itemIds = params.data?.[field] || [];
409
+ if (itemIds.length === 0) {
410
+ this.eGui.innerHTML = html`<span style="color:#666;font-style:italic;">—</span>`;
411
+ return;
412
+ }
413
+ this.eGui.style.cssText = 'display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:4px 0;';
414
+ this.eGui.innerHTML = itemIds
415
+ .map(
416
+ (itemId, index) =>
417
+ html`<span
418
+ style="display:inline-flex;align-items:center;gap:4px;font-size:11px;font-family:monospace;"
419
+ title="${itemId}"
420
+ >${EntityEngineCyberia.itemPreview(itemId)}${itemId}${index < itemIds.length - 1 ? ',' : ''}</span
421
+ >`,
422
+ )
423
+ .join('');
424
+ }
425
+
426
+ getGui() {
427
+ return this.eGui;
428
+ }
429
+
430
+ refresh() {
431
+ return true;
432
+ }
433
+ };
434
+ }
435
+
436
+ // The overrides a default carries, one rule per line. No sprite here: a rule is read as a rule —
437
+ // whether the id is active, how many of it, and which id it adjusts — and the id's own strip is
438
+ // already three columns to the left.
439
+ static overridesRenderer() {
440
+ return class {
441
+ eGui;
442
+
443
+ init(params) {
444
+ this.eGui = document.createElement('div');
445
+ const rules = params.data?.overrideItemsIdsState || [];
446
+ if (rules.length === 0) {
447
+ this.eGui.innerHTML = html`<span style="color:#666;font-style:italic;">—</span>`;
448
+ return;
449
+ }
450
+ this.eGui.style.cssText = 'display:flex;flex-direction:column;gap:2px;padding:4px 0;';
451
+ this.eGui.innerHTML = rules
452
+ .map(
453
+ ({ itemId, active, quantity, dropChance }) =>
454
+ html`<span
455
+ style="font-size:11px;font-family:monospace;white-space:nowrap;${active ? '' : 'color:#888;'}"
456
+ title="${itemId}"
457
+ >${active ? 'active' : 'inactive'} ×${Math.max(1, quantity || 1)}${Number.isFinite(dropChance) &&
458
+ 1 !== dropChance
459
+ ? ` drop ${dropChance}`
460
+ : ''} ${itemId}</span
461
+ >`,
462
+ )
463
+ .join('');
464
+ }
465
+
466
+ getGui() {
467
+ return this.eGui;
468
+ }
469
+
470
+ refresh() {
471
+ return true;
472
+ }
473
+ };
474
+ }
475
+
476
+ // What the grid's text filter reads for a column of rules, since an array of objects has no
477
+ // useful string form of its own.
478
+ static overridesFilterText = (rules = []) =>
479
+ rules
480
+ .map(
481
+ ({ itemId, active, quantity, dropChance }) =>
482
+ `${itemId} ${active ? 'active' : 'inactive'} x${quantity || 1}` +
483
+ (Number.isFinite(dropChance) && 1 !== dropChance ? ` drop${dropChance}` : ''),
484
+ )
485
+ .join(', ');
486
+
205
487
  static toRow(doc) {
206
488
  return {
207
489
  _id: doc._id,
208
490
  entityType: doc.entityType || '',
209
491
  behavior: doc.behavior || '',
210
- liveItemIds: (doc.liveItemIds || []).join(', '),
211
- deadItemIds: (doc.deadItemIds || []).join(', '),
212
- dropItemIds: (doc.dropItemIds || []).join(', '),
213
- objectLayers: (doc.defaultObjectLayers || []).length,
492
+ instances: (EntityEngineCyberia.instanceLinks.get(String(doc._id)) || []).join(', '),
493
+ liveItemIds: [...(doc.liveItemIds || [])],
494
+ deadItemIds: [...(doc.deadItemIds || [])],
495
+ dropItemIds: [...(doc.dropItemIds || [])],
496
+ inventoryItemsIds: [...(doc.inventoryItemsIds || [])],
497
+ overrideItemsIdsState: [...(doc.overrideItemsIdsState || [])],
214
498
  };
215
499
  }
216
500
 
501
+ // The link lives on CyberiaInstanceConf.entityDefaults, so one conf read answers it for every
502
+ // row at once — there is no second place to ask, and nothing to keep in sync.
503
+ static async refreshInstanceLinks() {
504
+ const res = await CyberiaInstanceConfService.get({ limit: 500 });
505
+ const confs = res?.data?.data || [];
506
+ EntityEngineCyberia.availableInstanceCodes = confs.map((conf) => conf.instanceCode).sort();
507
+ EntityEngineCyberia.instanceLinks = new Map();
508
+ for (const conf of confs) {
509
+ for (const ref of conf.entityDefaults || []) {
510
+ const key = String(ref);
511
+ if (!EntityEngineCyberia.instanceLinks.has(key)) EntityEngineCyberia.instanceLinks.set(key, []);
512
+ EntityEngineCyberia.instanceLinks.get(key).push(conf.instanceCode);
513
+ }
514
+ }
515
+ for (const codes of EntityEngineCyberia.instanceLinks.values()) codes.sort();
516
+ }
517
+
217
518
  static async refreshList() {
218
- const res = await CyberiaEntityTypeDefaultService.get({ limit: 500 });
519
+ const [res] = await Promise.all([
520
+ CyberiaEntityTypeDefaultService.get({ limit: 500 }),
521
+ EntityEngineCyberia.refreshInstanceLinks(),
522
+ ]);
219
523
  EntityEngineCyberia.listCache = res?.data?.data || [];
220
524
  const rows = EntityEngineCyberia.listCache.map((d) => EntityEngineCyberia.toRow(d));
221
525
  if (AgGrid.grids[EntityEngineCyberia.gridId])
222
526
  AgGrid.grids[EntityEngineCyberia.gridId].setGridOption('rowData', rows);
527
+ EntityEngineCyberia.renderInstanceList();
223
528
  }
224
529
 
225
530
  static notifyResult(result, isUpdate) {
@@ -242,10 +547,14 @@ class EntityEngineCyberia {
242
547
  liveItemIds: [...EntityEngineCyberia.liveItemIds],
243
548
  deadItemIds: [...EntityEngineCyberia.deadItemIds],
244
549
  dropItemIds: [...EntityEngineCyberia.dropItemIds],
245
- defaultObjectLayers: EntityEngineCyberia.defaultObjectLayers.map((l) => ({
246
- itemId: l.itemId,
247
- active: !!l.active,
248
- quantity: l.quantity || 0,
550
+ inventoryItemsIds: [...EntityEngineCyberia.inventoryItemsIds],
551
+ overrideItemsIdsState: EntityEngineCyberia.overrideItemsIdsState.map((rule) => ({
552
+ itemId: rule.itemId,
553
+ active: !!rule.active,
554
+ quantity: Math.max(1, rule.quantity || 1),
555
+ // Sent only when authored. Omitting it is what makes the id drop every time, so a rule
556
+ // that never touched the input reads the same as every rule written before it existed.
557
+ ...(Number.isFinite(rule.dropChance) ? { dropChance: rule.dropChance } : {}),
249
558
  })),
250
559
  };
251
560
  }
@@ -257,15 +566,17 @@ class EntityEngineCyberia {
257
566
  EntityEngineCyberia.liveItemIds = [...(doc.liveItemIds || [])];
258
567
  EntityEngineCyberia.deadItemIds = [...(doc.deadItemIds || [])];
259
568
  EntityEngineCyberia.dropItemIds = [...(doc.dropItemIds || [])];
260
- EntityEngineCyberia.defaultObjectLayers = (doc.defaultObjectLayers || []).map((l) => ({
261
- itemId: l.itemId,
262
- active: !!l.active,
263
- quantity: l.quantity || 0,
569
+ EntityEngineCyberia.inventoryItemsIds = [...(doc.inventoryItemsIds || [])];
570
+ EntityEngineCyberia.overrideItemsIdsState = (doc.overrideItemsIdsState || []).map((rule) => ({
571
+ itemId: rule.itemId,
572
+ active: !!rule.active,
573
+ quantity: Math.max(1, rule.quantity || 1),
574
+ ...(Number.isFinite(rule.dropChance) ? { dropChance: rule.dropChance } : {}),
264
575
  }));
265
- EntityEngineCyberia.renderItemList('live');
266
- EntityEngineCyberia.renderItemList('dead');
267
- EntityEngineCyberia.renderItemList('drop');
268
- EntityEngineCyberia.renderLayerList();
576
+ EntityEngineCyberia.instanceCodes = [...(EntityEngineCyberia.instanceLinks.get(String(doc._id)) || [])];
577
+ for (const field of EntityEngineCyberia.ITEM_FIELDS) EntityEngineCyberia.renderItemList(field);
578
+ EntityEngineCyberia.renderOverrideList();
579
+ EntityEngineCyberia.renderInstanceList();
269
580
  NotificationManager.Push({ html: `Entity default "${doc.entityType}" loaded`, status: 'success' });
270
581
  }
271
582
 
@@ -274,13 +585,14 @@ class EntityEngineCyberia {
274
585
  EntityEngineCyberia.liveItemIds = [];
275
586
  EntityEngineCyberia.deadItemIds = [];
276
587
  EntityEngineCyberia.dropItemIds = [];
277
- EntityEngineCyberia.defaultObjectLayers = [];
588
+ EntityEngineCyberia.inventoryItemsIds = [];
589
+ EntityEngineCyberia.overrideItemsIdsState = [];
590
+ EntityEngineCyberia.instanceCodes = [];
278
591
  EntityEngineCyberia.setSingleDropdownValue(EntityEngineCyberia.ids.entityType, '');
279
592
  EntityEngineCyberia.setSingleDropdownValue(EntityEngineCyberia.ids.behavior, '');
280
- EntityEngineCyberia.renderItemList('live');
281
- EntityEngineCyberia.renderItemList('dead');
282
- EntityEngineCyberia.renderItemList('drop');
283
- EntityEngineCyberia.renderLayerList();
593
+ for (const field of EntityEngineCyberia.ITEM_FIELDS) EntityEngineCyberia.renderItemList(field);
594
+ EntityEngineCyberia.renderOverrideList();
595
+ EntityEngineCyberia.renderInstanceList();
284
596
  }
285
597
 
286
598
  static validate(body) {
@@ -310,6 +622,7 @@ class EntityEngineCyberia {
310
622
  EntityEngineCyberia.notifyResult(result, isUpdate);
311
623
  if (result.status === 'success') {
312
624
  if (result.data?._id) EntityEngineCyberia.currentId = result.data._id;
625
+ await EntityEngineCyberia.persistInstanceLinks();
313
626
  await EntityEngineCyberia.refreshList();
314
627
  }
315
628
  }
@@ -324,7 +637,10 @@ class EntityEngineCyberia {
324
637
  if (!EntityEngineCyberia.validate(body)) return;
325
638
  const result = await CyberiaEntityTypeDefaultService.put({ id: EntityEngineCyberia.currentId, body });
326
639
  EntityEngineCyberia.notifyResult(result, true);
327
- if (result.status === 'success') await EntityEngineCyberia.refreshList();
640
+ if (result.status === 'success') {
641
+ await EntityEngineCyberia.persistInstanceLinks();
642
+ await EntityEngineCyberia.refreshList();
643
+ }
328
644
  }
329
645
 
330
646
  // Clone: create a NEW document from the current form values (POST), then load
@@ -338,6 +654,7 @@ class EntityEngineCyberia {
338
654
  EntityEngineCyberia.notifyResult(result, false);
339
655
  if (result.status === 'success') {
340
656
  if (result.data?._id) EntityEngineCyberia.currentId = result.data._id;
657
+ await EntityEngineCyberia.persistInstanceLinks();
341
658
  await EntityEngineCyberia.refreshList();
342
659
  NotificationManager.Push({ html: 'Cloned into a new entity default.', status: 'success' });
343
660
  }
@@ -370,12 +687,12 @@ class EntityEngineCyberia {
370
687
  EntityEngineCyberia.liveItemIds = [];
371
688
  EntityEngineCyberia.deadItemIds = [];
372
689
  EntityEngineCyberia.dropItemIds = [];
373
- EntityEngineCyberia.defaultObjectLayers = [];
690
+ EntityEngineCyberia.inventoryItemsIds = [];
691
+ EntityEngineCyberia.overrideItemsIdsState = [];
692
+ EntityEngineCyberia.instanceCodes = [];
374
693
 
375
694
  const ids = EntityEngineCyberia.ids;
376
695
  const entityTypeOptions = Object.values(ENTITY_TYPES).map((t) => dropdownOption(t));
377
- const cont = 'entity-engine-container';
378
- const dc = { layer: 'entity-engine-dc-layer' };
379
696
 
380
697
  const group = (title, icon, inner) =>
381
698
  html`<div class="in" style="border:1px solid ${groupBorder()};border-radius:8px;padding:12px;margin-bottom:14px;">
@@ -405,19 +722,19 @@ class EntityEngineCyberia {
405
722
 
406
723
  setTimeout(async () => {
407
724
  ThemeEvents['entity-engine-theme'] = () => {
408
- EntityEngineCyberia.renderItemList('live');
409
- EntityEngineCyberia.renderItemList('dead');
410
- EntityEngineCyberia.renderItemList('drop');
411
- EntityEngineCyberia.renderLayerList();
725
+ for (const field of EntityEngineCyberia.ITEM_FIELDS) EntityEngineCyberia.renderItemList(field);
726
+ EntityEngineCyberia.renderOverrideList();
412
727
  };
413
728
 
414
- for (const field of ['live', 'dead', 'drop'])
729
+ for (const field of EntityEngineCyberia.ITEM_FIELDS)
415
730
  if (s(`.btn-entity-engine-add-${field}`))
416
731
  s(`.btn-entity-engine-add-${field}`).onclick = () =>
417
732
  EntityEngineCyberia.addItem(field, ids[`${field}ItemPicker`]);
418
733
 
419
- if (s('.btn-entity-engine-add-layer'))
420
- s('.btn-entity-engine-add-layer').onclick = () => EntityEngineCyberia.addLayer();
734
+ if (s('.btn-entity-engine-add-override'))
735
+ s('.btn-entity-engine-add-override').onclick = () => EntityEngineCyberia.addOverride();
736
+ if (s('.btn-entity-engine-add-instance'))
737
+ s('.btn-entity-engine-add-instance').onclick = () => EntityEngineCyberia.addInstance();
421
738
  if (s('.btn-entity-engine-save')) s('.btn-entity-engine-save').onclick = () => EntityEngineCyberia.save();
422
739
  if (s('.btn-entity-engine-update')) s('.btn-entity-engine-update').onclick = () => EntityEngineCyberia.update();
423
740
  if (s('.btn-entity-engine-clone')) s('.btn-entity-engine-clone').onclick = () => EntityEngineCyberia.clone();
@@ -426,10 +743,8 @@ class EntityEngineCyberia {
426
743
  if (s('.btn-entity-engine-refresh'))
427
744
  s('.btn-entity-engine-refresh').onclick = () => EntityEngineCyberia.refreshList();
428
745
 
429
- EntityEngineCyberia.renderItemList('live');
430
- EntityEngineCyberia.renderItemList('dead');
431
- EntityEngineCyberia.renderItemList('drop');
432
- EntityEngineCyberia.renderLayerList();
746
+ for (const field of EntityEngineCyberia.ITEM_FIELDS) EntityEngineCyberia.renderItemList(field);
747
+ EntityEngineCyberia.renderOverrideList();
433
748
  await EntityEngineCyberia.refreshList();
434
749
  });
435
750
 
@@ -457,10 +772,44 @@ class EntityEngineCyberia {
457
772
  columnDefs: [
458
773
  { field: 'entityType', headerName: 'Entity Type', minWidth: 120 },
459
774
  { field: 'behavior', headerName: 'Behavior', minWidth: 110 },
460
- { field: 'liveItemIds', headerName: 'Live Item Ids' },
461
- { field: 'deadItemIds', headerName: 'Dead Item Ids' },
462
- { field: 'dropItemIds', headerName: 'Drop Item Ids' },
463
- { field: 'objectLayers', headerName: 'OL', minWidth: 70 },
775
+ { field: 'instances', headerName: 'Instances', minWidth: 160 },
776
+ {
777
+ field: 'liveItemIds',
778
+ headerName: 'Live Item Ids',
779
+ minWidth: 220,
780
+ autoHeight: true,
781
+ cellRenderer: EntityEngineCyberia.itemIdsRenderer('liveItemIds'),
782
+ },
783
+ {
784
+ field: 'deadItemIds',
785
+ headerName: 'Dead Item Ids',
786
+ minWidth: 220,
787
+ autoHeight: true,
788
+ cellRenderer: EntityEngineCyberia.itemIdsRenderer('deadItemIds'),
789
+ },
790
+ {
791
+ field: 'dropItemIds',
792
+ headerName: 'Drop Item Ids',
793
+ minWidth: 220,
794
+ autoHeight: true,
795
+ cellRenderer: EntityEngineCyberia.itemIdsRenderer('dropItemIds'),
796
+ },
797
+ {
798
+ field: 'inventoryItemsIds',
799
+ headerName: 'Inventory Items Id',
800
+ minWidth: 220,
801
+ autoHeight: true,
802
+ cellRenderer: EntityEngineCyberia.itemIdsRenderer('inventoryItemsIds'),
803
+ },
804
+ {
805
+ field: 'overrideItemsIdsState',
806
+ headerName: 'Resume Override',
807
+ minWidth: 200,
808
+ autoHeight: true,
809
+ cellRenderer: EntityEngineCyberia.overridesRenderer(),
810
+ filterValueGetter: (params) =>
811
+ EntityEngineCyberia.overridesFilterText(params.data?.overrideItemsIdsState),
812
+ },
464
813
  ],
465
814
  rowSelection: 'single',
466
815
  onRowClicked: (event) => {
@@ -509,67 +858,89 @@ class EntityEngineCyberia {
509
858
  ${group('Dead Item Ids', 'fa-solid fa-skull', await itemListEditor('dead', html`Search dead itemId`))}
510
859
  ${group('Drop Item Ids', 'fa-solid fa-gift', await itemListEditor('drop', html`Search drop itemId`))}
511
860
  ${group(
512
- 'Default Object Layers',
513
- 'fa-solid fa-layer-group',
514
- html`${dynamicCol({ containerSelector: cont, id: dc.layer, type: 'a-50-b-50' })}
861
+ 'Inventory Items Id',
862
+ 'fa-solid fa-box-open',
863
+ html`<div class="in" style="font-size:12px;color:#888;margin-bottom:8px;">
864
+ <i class="fa-solid fa-circle-info"></i> The entity carries the union of all four lists. These are the extras
865
+ no lifecycle state activates — a coin balance, say; live, dead and drop ids are already carried and are
866
+ activated by context.
867
+ </div>
868
+ ${await itemListEditor('inventory', html`Search inventory itemId`)}`,
869
+ )}
870
+ ${group(
871
+ 'Override Item State',
872
+ 'fa-solid fa-sliders',
873
+ html`<div class="in" style="font-size:12px;color:#888;margin-bottom:8px;">
874
+ <i class="fa-solid fa-circle-info"></i> Adjusts what the lists above derive for one id they already carry:
875
+ <b>active</b> forces the spawn state — a skin the equipment rules would otherwise leave inactive — and the
876
+ quantity sizes a stack, which is how a drop bundle declares how many it scatters. An override never adds an
877
+ id.
878
+ </div>
515
879
  <div class="fl" style="align-items:flex-end;">
516
- <div class="in fll ${dc.layer}-col-a" style="padding-right:4px;">
517
- ${await EntityEngineCyberia.buildItemIdDropdown(ids.layerItemPicker, html`Search itemId`)}
880
+ <div class="in fll" style="flex:1;">
881
+ ${await EntityEngineCyberia.buildItemIdDropdown(ids.overrideItemPicker, html`Search carried itemId`)}
518
882
  </div>
519
- <div class="in fll ${dc.layer}-col-b" style="padding-left:4px;">
520
- ${await Input.instance({
521
- id: 'entity-engine-layer-qty',
522
- label: html`Quantity`,
523
- containerClass: 'inl',
524
- type: 'number',
525
- min: 0,
526
- value: 0,
883
+ <div class="in fll" style="padding-left:6px;">
884
+ ${await BtnIcon.instance({
885
+ class: 'wfa btn-entity-engine-add-override',
886
+ label: html`<i class="fa-solid fa-plus"></i> Override`,
527
887
  })}
528
888
  </div>
529
889
  </div>
530
- <div class="in" style="margin-top:8px;">
531
- <label style="font-size:13px;cursor:pointer;">
532
- <input class="entity-engine-layer-active" type="checkbox" /> Active on spawn
533
- </label>
890
+ <div class="in entity-engine-override-list" style="margin-top:8px;"></div>`,
891
+ )}
892
+ ${group(
893
+ 'Instances',
894
+ 'fa-solid fa-earth-americas',
895
+ html`<div class="in" style="font-size:12px;opacity:.8;margin-bottom:8px;">
896
+ A world runs on this default only while its conf references it, and the reference is what scopes it — an
897
+ instance never picks up another's wiring by sharing item ids.
534
898
  </div>
535
- <div class="in" style="margin-top:8px;">
536
- ${await BtnIcon.instance({
537
- class: 'wfa btn-entity-engine-add-layer',
538
- label: html`<i class="fa-solid fa-plus"></i> Add Object Layer`,
539
- })}
899
+ <div class="fl" style="align-items:flex-end;">
900
+ <div class="in fll" style="flex:1;">
901
+ ${await EntityEngineCyberia.buildInstanceDropdown(ids.instancePicker)}
902
+ </div>
903
+ <div class="in fll" style="padding-left:6px;">
904
+ ${await BtnIcon.instance({
905
+ class: 'wfa btn-entity-engine-add-instance',
906
+ label: html`<i class="fa-solid fa-plus"></i> Link`,
907
+ })}
908
+ </div>
540
909
  </div>
541
- <div class="in entity-engine-layer-list" style="margin-top:8px;"></div>`,
910
+ <div class="in entity-engine-instance-list" style="margin-top:8px;"></div>`,
542
911
  )}
543
912
  ${group(
544
913
  'Save Entity Default',
545
914
  'fa-solid fa-floppy-disk',
546
915
  html`<div class="fl" style="margin-top:4px;flex-wrap:wrap;">
547
- ${EntityEngineCyberia.canMutate
548
- ? html`<div class="in fll" style="flex:1 1 120px;padding:3px;">
549
- ${await BtnIcon.instance({
916
+ ${
917
+ EntityEngineCyberia.canMutate
918
+ ? html`<div class="in fll" style="flex:1 1 120px;padding:3px;">
919
+ ${await BtnIcon.instance({
550
920
  class: 'wfa btn-entity-engine-save',
551
921
  label: html`<i class="fa-solid fa-floppy-disk"></i> Save`,
552
922
  })}
553
- </div>
554
- <div class="in fll" style="flex:1 1 120px;padding:3px;">
555
- ${await BtnIcon.instance({
923
+ </div>
924
+ <div class="in fll" style="flex:1 1 120px;padding:3px;">
925
+ ${await BtnIcon.instance({
556
926
  class: 'wfa btn-entity-engine-update',
557
927
  label: html`<i class="fa-solid fa-pen-to-square"></i> Update`,
558
928
  })}
559
- </div>
560
- <div class="in fll" style="flex:1 1 120px;padding:3px;">
561
- ${await BtnIcon.instance({
929
+ </div>
930
+ <div class="in fll" style="flex:1 1 120px;padding:3px;">
931
+ ${await BtnIcon.instance({
562
932
  class: 'wfa btn-entity-engine-clone',
563
933
  label: html`<i class="fa-solid fa-clone"></i> Clone`,
564
934
  })}
565
- </div>
566
- <div class="in fll" style="flex:1 1 120px;padding:3px;">
567
- ${await BtnIcon.instance({
935
+ </div>
936
+ <div class="in fll" style="flex:1 1 120px;padding:3px;">
937
+ ${await BtnIcon.instance({
568
938
  class: 'wfa btn-entity-engine-delete',
569
939
  label: html`<i class="fa-solid fa-trash"></i> Delete`,
570
940
  })}
571
- </div>`
572
- : ''}
941
+ </div>`
942
+ : ''
943
+ }
573
944
  <div class="in fll" style="flex:1 1 120px;padding:3px;">
574
945
  ${await BtnIcon.instance({
575
946
  class: 'wfa btn-entity-engine-new',