cyberia 3.2.12 → 3.2.70

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 (194) hide show
  1. package/.env.example +127 -68
  2. package/.github/workflows/cyberia-client.cd.yml +40 -0
  3. package/.github/workflows/cyberia-server.cd.yml +40 -0
  4. package/.github/workflows/docker-image.cyberia-client.ci.yml +49 -0
  5. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +48 -0
  6. package/.github/workflows/docker-image.cyberia-server.ci.yml +69 -0
  7. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +69 -0
  8. package/.github/workflows/docker-image.engine-cyberia.ci.yml +52 -0
  9. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +52 -0
  10. package/.github/workflows/engine-cyberia.cd.yml +34 -24
  11. package/.github/workflows/engine-cyberia.ci.yml +27 -5
  12. package/.github/workflows/ghpkg.ci.yml +89 -1
  13. package/.github/workflows/gitlab.ci.yml +1 -1
  14. package/.github/workflows/hardhat.ci.yml +1 -1
  15. package/.github/workflows/npmpkg.ci.yml +19 -12
  16. package/.github/workflows/publish.ci.yml +2 -2
  17. package/.github/workflows/publish.cyberia.ci.yml +5 -16
  18. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -8
  19. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  20. package/CHANGELOG.md +451 -1
  21. package/CLI-HELP.md +1040 -1130
  22. package/Dockerfile +141 -43
  23. package/Dockerfile.dev +143 -0
  24. package/Dockerfile.test +165 -0
  25. package/README.md +1 -1
  26. package/baremetal/commission-workflows.json +1 -0
  27. package/bin/build.js +128 -136
  28. package/bin/build.template.js +25 -179
  29. package/bin/cyberia.js +1089 -188
  30. package/bin/deploy.js +6 -3
  31. package/bin/index.js +1089 -188
  32. package/bump.config.js +1 -0
  33. package/compose.env +131 -0
  34. package/conf.js +19 -1
  35. package/deployment.yaml +74 -2
  36. package/docker-compose.yml +316 -0
  37. package/hardhat/hardhat.config.js +2 -2
  38. package/hardhat/package-lock.json +620 -2041
  39. package/hardhat/package.json +7 -5
  40. package/hardhat/scripts/deployObjectLayerToken.js +18 -18
  41. package/hardhat/test/ObjectLayerToken.js +378 -274
  42. package/ipfs/configure-ipfs.sh +13 -0
  43. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  44. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  45. package/manifests/deployment/dd-cyberia-development/deployment.yaml +74 -2
  46. package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
  47. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
  48. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  49. package/mongodb/entrypoint.sh +76 -0
  50. package/nginx.conf +87 -0
  51. package/package.json +34 -22
  52. package/pv-pvc.yaml +32 -0
  53. package/scripts/disk-clean.sh +85 -60
  54. package/scripts/kubeadm-node-setup.sh +317 -0
  55. package/scripts/link-local-underpost-cli.sh +6 -0
  56. package/scripts/rocky-kickstart.sh +877 -185
  57. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  58. package/scripts/test-monitor.sh +248 -0
  59. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
  60. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
  61. package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
  62. package/src/api/cyberia-action/cyberia-action.model.js +21 -29
  63. package/src/api/cyberia-action/cyberia-action.router.js +42 -7
  64. package/src/api/cyberia-action/cyberia-action.service.js +20 -4
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
  66. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
  67. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
  68. package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
  69. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
  70. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
  71. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
  72. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
  73. package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
  74. package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
  75. package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
  76. package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
  77. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
  78. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
  79. package/src/api/cyberia-map/cyberia-map.router.js +21 -6
  80. package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
  81. package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
  82. package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
  83. package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
  84. package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
  85. package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
  86. package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
  87. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -122
  88. package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
  89. package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
  90. package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
  91. package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
  92. package/src/api/ipfs/ipfs.service.js +28 -15
  93. package/src/api/object-layer/object-layer.router.js +25 -15
  94. package/src/api/object-layer/object-layer.service.js +15 -20
  95. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
  96. package/src/cli/baremetal.js +717 -16
  97. package/src/cli/cluster.js +80 -5
  98. package/src/cli/deploy.js +326 -292
  99. package/src/cli/docker-compose.js +648 -0
  100. package/src/cli/env.js +12 -6
  101. package/src/cli/fs.js +75 -30
  102. package/src/cli/image.js +187 -55
  103. package/src/cli/index.js +157 -6
  104. package/src/cli/kickstart.js +142 -20
  105. package/src/cli/monitor.js +387 -6
  106. package/src/cli/release.js +72 -15
  107. package/src/cli/repository.js +339 -40
  108. package/src/cli/run.js +678 -186
  109. package/src/cli/secrets.js +82 -48
  110. package/src/cli/ssh.js +234 -0
  111. package/src/cli/static.js +2 -2
  112. package/src/client/components/core/PanelForm.js +44 -44
  113. package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
  114. package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
  115. package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
  116. package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
  117. package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
  118. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
  119. package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
  120. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
  121. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
  122. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
  123. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +92 -20
  124. package/src/client/public/cyberia-docs/ARCHITECTURE.md +272 -50
  125. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
  126. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
  127. package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
  128. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
  129. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +28 -12
  130. package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
  131. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +27 -5
  132. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  133. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  134. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
  135. package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
  136. package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
  137. package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
  138. package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
  139. package/src/client/services/object-layer/object-layer.management.js +6 -6
  140. package/src/{server → client-builder}/client-build-docs.js +15 -5
  141. package/src/{server → client-builder}/client-build-live.js +3 -3
  142. package/src/{server → client-builder}/client-build.js +25 -22
  143. package/src/{server → client-builder}/client-dev-server.js +3 -3
  144. package/src/{server → client-builder}/client-icons.js +2 -2
  145. package/src/{server → client-builder}/ssr.js +5 -5
  146. package/src/client.build.js +1 -3
  147. package/src/client.dev.js +1 -1
  148. package/src/db/mongo/MongoBootstrap.js +12 -12
  149. package/src/db/mongo/MongooseDB.js +2 -1
  150. package/src/grpc/cyberia/grpc-server.js +255 -70
  151. package/src/index.js +12 -1
  152. package/src/mailer/EmailRender.js +1 -1
  153. package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
  154. package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
  155. package/src/projects/cyberia/catalog-cyberia.js +81 -0
  156. package/src/projects/cyberia/gemini-client.js +175 -0
  157. package/src/projects/cyberia/generate-saga.js +2107 -0
  158. package/src/{server → projects/cyberia}/ipfs-client.js +6 -4
  159. package/src/{server → projects/cyberia}/object-layer.js +12 -108
  160. package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
  161. package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
  162. package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
  163. package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
  164. package/src/{server → projects/cyberia}/shape-generator.js +2 -2
  165. package/src/projects/underpost/catalog-underpost.js +60 -0
  166. package/src/runtime/cyberia-client/Dockerfile +31 -83
  167. package/src/runtime/cyberia-client/Dockerfile.dev +23 -30
  168. package/src/runtime/cyberia-server/Dockerfile +23 -42
  169. package/src/runtime/cyberia-server/Dockerfile.dev +20 -35
  170. package/src/runtime/engine-cyberia/Dockerfile +143 -0
  171. package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
  172. package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
  173. package/src/runtime/engine-cyberia/compose.env +131 -0
  174. package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
  175. package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
  176. package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
  177. package/src/runtime/engine-cyberia/nginx.conf +87 -0
  178. package/src/runtime/express/Express.js +2 -2
  179. package/src/runtime/nginx/Nginx.js +250 -0
  180. package/src/runtime/wp/Dockerfile +3 -3
  181. package/src/server/catalog.js +72 -0
  182. package/src/server/conf.js +415 -60
  183. package/src/server/runtime-status.js +252 -0
  184. package/src/server/start.js +42 -11
  185. package/src/server.js +6 -2
  186. package/test/cyberia-instance-conf-defaults.test.js +140 -0
  187. package/test/deploy-monitor.test.js +267 -0
  188. package/test/shape-generator.test.js +7 -1
  189. package/typedoc.dd-cyberia.json +3 -1
  190. package/typedoc.json +3 -1
  191. package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
  192. package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
  193. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  194. /package/src/{server → client-builder}/client-formatted.js +0 -0
@@ -0,0 +1,585 @@
1
+ import { BtnIcon } from '../core/BtnIcon.js';
2
+ import { Input } from '../core/Input.js';
3
+ import { commonModeratorGuard } from '../core/CommonJs.js';
4
+ import { htmls, s } from '../core/VanillaJs.js';
5
+ import { NotificationManager } from '../core/NotificationManager.js';
6
+ import { Translate } from '../core/Translate.js';
7
+ import { darkTheme, dynamicCol, ThemeEvents } from '../core/Css.js';
8
+ import { DropDown } from '../core/DropDown.js';
9
+ import { AgGrid } from '../core/AgGrid.js';
10
+ import { ObjectLayerService } from '../../services/object-layer/object-layer.service.js';
11
+ import { CyberiaEntityTypeDefaultService } from '../../services/cyberia-entity-type-default/cyberia-entity-type-default.service.js';
12
+ import { ENTITY_TYPES, SELECTABLE_ENTITY_BEHAVIORS } from './SharedDefaultsCyberia.js';
13
+
14
+ // DropDown invokes optionData.onClick on selection, so every option must carry one.
15
+ const dropdownOption = (value) => ({ value, display: value, data: value, onClick: () => {} });
16
+ const groupBorder = () => (darkTheme ? '#3a3a3a' : '#d4d4d4');
17
+ const subtleBorder = () => (darkTheme ? '#444' : '#e0e0e0');
18
+
19
+ // EntityEngineCyberia — CRUD surface for the cyberia-entity-type-default
20
+ // collection (the DB-backed mirror of ENTITY_TYPE_DEFAULTS). An ag-grid lists
21
+ // 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
24
+ // high-frequency itemId fields stay fast and typo-free. The runtime linking
25
+ // rule (entity matches entityType AND carries an active itemId from liveItemIds)
26
+ // is content-authority only; this engine never mutates entities or the
27
+ // simulation — it owns the default content the seed/instance pipelines consume.
28
+ class EntityEngineCyberia {
29
+ static listCache = [];
30
+ static currentId = null;
31
+
32
+ // Editable working copies of the array fields for the loaded document.
33
+ static liveItemIds = [];
34
+ static deadItemIds = [];
35
+ static dropItemIds = [];
36
+ static defaultObjectLayers = [];
37
+
38
+ static gridId = 'entity-engine-grid';
39
+
40
+ static ids = {
41
+ entityType: 'entity-engine-entity-type',
42
+ behavior: 'entity-engine-behavior',
43
+ liveItemPicker: 'entity-engine-live-item-picker',
44
+ deadItemPicker: 'entity-engine-dead-item-picker',
45
+ dropItemPicker: 'entity-engine-drop-item-picker',
46
+ layerItemPicker: 'entity-engine-layer-item-picker',
47
+ };
48
+
49
+ // Behavior options: a leading "auto" (empty → runtime derives armed→hostile,
50
+ // else passive) plus the author-assignable canonical behaviors.
51
+ static behaviorOptions() {
52
+ return [
53
+ { value: '', display: '— auto (derive) —', data: '', onClick: () => {} },
54
+ ...SELECTABLE_ENTITY_BEHAVIORS.map((b) => ({
55
+ value: b.id,
56
+ display: b.label,
57
+ data: b.id,
58
+ onClick: () => {},
59
+ })),
60
+ ];
61
+ }
62
+
63
+ // ── Searchable dropdown helpers (mirror ActionEngineCyberia) ──────────────
64
+ static getSingleDropdownValue(id) {
65
+ const value = DropDown.Tokens[id]?.value;
66
+ return typeof value === 'string' ? value.trim() : '';
67
+ }
68
+
69
+ static setSingleDropdownValue(id, value) {
70
+ if (!DropDown.Tokens[id]) return;
71
+ const v = value || '';
72
+ DropDown.Tokens[id].value = v;
73
+ if (s(`.${id}`)) s(`.${id}`).value = v;
74
+ htmls(`.dropdown-current-${id}`, v || '');
75
+ }
76
+
77
+ // itemId references the object-layer collection, so each picker is a fast
78
+ // server-backed autocomplete instead of a free-text input.
79
+ static async buildItemIdDropdown(id, label) {
80
+ return await DropDown.instance({
81
+ id,
82
+ label,
83
+ data: [],
84
+ containerClass: 'inl',
85
+ serviceProvider: async (q) => {
86
+ const result = await ObjectLayerService.searchItemIds({ q });
87
+ if (result.status === 'success' && result.data?.itemIds) {
88
+ return result.data.itemIds.map((itemId) => dropdownOption(itemId));
89
+ }
90
+ return [];
91
+ },
92
+ });
93
+ }
94
+
95
+ // ── Item-id list editors (live / dead / drop) ────────────────────────────
96
+ static listFieldOf(field) {
97
+ return {
98
+ live: EntityEngineCyberia.liveItemIds,
99
+ dead: EntityEngineCyberia.deadItemIds,
100
+ drop: EntityEngineCyberia.dropItemIds,
101
+ }[field];
102
+ }
103
+
104
+ static async renderItemList(field) {
105
+ const container = s(`.entity-engine-${field}-list`);
106
+ if (!container) return;
107
+ const items = EntityEngineCyberia.listFieldOf(field);
108
+ let out = '';
109
+ for (let i = 0; i < items.length; i++) {
110
+ out += html`<div
111
+ class="fl"
112
+ style="border-bottom:1px solid ${subtleBorder()};padding:3px 0;align-items:center;font-size:12px;font-family:monospace;"
113
+ >
114
+ <div class="in fll" style="flex:1;">${items[i]}</div>
115
+ ${await BtnIcon.instance({
116
+ class: `btn-entity-engine-rm-${field}-${i}`,
117
+ label: html`<i class="fa-solid fa-xmark"></i>`,
118
+ style: 'min-width:30px;padding:2px 8px;',
119
+ })}
120
+ </div>`;
121
+ }
122
+ if (!out) out = '<div style="color:#888;font-size:12px;">No item ids.</div>';
123
+ htmls(`.entity-engine-${field}-list`, out);
124
+ for (let i = 0; i < items.length; i++) {
125
+ if (s(`.btn-entity-engine-rm-${field}-${i}`))
126
+ s(`.btn-entity-engine-rm-${field}-${i}`).onclick = () => {
127
+ EntityEngineCyberia.listFieldOf(field).splice(i, 1);
128
+ EntityEngineCyberia.renderItemList(field);
129
+ };
130
+ }
131
+ }
132
+
133
+ static addItem(field, pickerId) {
134
+ const itemId = EntityEngineCyberia.getSingleDropdownValue(pickerId);
135
+ if (!itemId) {
136
+ NotificationManager.Push({ html: 'Select an itemId first.', status: 'error' });
137
+ return;
138
+ }
139
+ const list = EntityEngineCyberia.listFieldOf(field);
140
+ if (list.includes(itemId)) {
141
+ NotificationManager.Push({ html: `"${itemId}" already in ${field} item ids.`, status: 'warning' });
142
+ return;
143
+ }
144
+ list.push(itemId);
145
+ EntityEngineCyberia.setSingleDropdownValue(pickerId, '');
146
+ EntityEngineCyberia.renderItemList(field);
147
+ }
148
+
149
+ // ── Default object-layer rows ({ itemId, active, quantity }) ──────────────
150
+ static async renderLayerList() {
151
+ const container = s('.entity-engine-layer-list');
152
+ if (!container) return;
153
+ let out = '';
154
+ for (let i = 0; i < EntityEngineCyberia.defaultObjectLayers.length; i++) {
155
+ const l = EntityEngineCyberia.defaultObjectLayers[i];
156
+ out += html`<div
157
+ class="fl"
158
+ style="border-bottom:1px solid ${subtleBorder()};padding:3px 0;align-items:center;font-size:12px;font-family:monospace;"
159
+ >
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}
166
+ </div>
167
+ ${await BtnIcon.instance({
168
+ class: `btn-entity-engine-rm-layer-${i}`,
169
+ label: html`<i class="fa-solid fa-xmark"></i>`,
170
+ style: 'min-width:30px;padding:2px 8px;',
171
+ })}
172
+ </div>`;
173
+ }
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();
181
+ };
182
+ }
183
+ }
184
+
185
+ static addLayer() {
186
+ const itemId = EntityEngineCyberia.getSingleDropdownValue(EntityEngineCyberia.ids.layerItemPicker);
187
+ if (!itemId) {
188
+ NotificationManager.Push({ html: 'Select an itemId for the object layer.', status: 'error' });
189
+ return;
190
+ }
191
+ if (EntityEngineCyberia.defaultObjectLayers.some((l) => l.itemId === itemId)) {
192
+ NotificationManager.Push({ html: `Object layer "${itemId}" already added.`, status: 'warning' });
193
+ return;
194
+ }
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();
202
+ }
203
+
204
+ // ── Grid (CRUD list surface) ──────────────────────────────────────────────
205
+ static toRow(doc) {
206
+ return {
207
+ _id: doc._id,
208
+ entityType: doc.entityType || '',
209
+ behavior: doc.behavior || '',
210
+ liveItemIds: (doc.liveItemIds || []).join(', '),
211
+ deadItemIds: (doc.deadItemIds || []).join(', '),
212
+ dropItemIds: (doc.dropItemIds || []).join(', '),
213
+ objectLayers: (doc.defaultObjectLayers || []).length,
214
+ };
215
+ }
216
+
217
+ static async refreshList() {
218
+ const res = await CyberiaEntityTypeDefaultService.get({ limit: 500 });
219
+ EntityEngineCyberia.listCache = res?.data?.data || [];
220
+ const rows = EntityEngineCyberia.listCache.map((d) => EntityEngineCyberia.toRow(d));
221
+ if (AgGrid.grids[EntityEngineCyberia.gridId])
222
+ AgGrid.grids[EntityEngineCyberia.gridId].setGridOption('rowData', rows);
223
+ }
224
+
225
+ static notifyResult(result, isUpdate) {
226
+ NotificationManager.Push({
227
+ html:
228
+ result.status === 'error'
229
+ ? result.message
230
+ : isUpdate
231
+ ? Translate.instance('success-update-item')
232
+ : Translate.instance('success-create-item'),
233
+ status: result.status,
234
+ });
235
+ }
236
+
237
+ // ── Payload / load / persistence ──────────────────────────────────────────
238
+ static getPayload() {
239
+ return {
240
+ entityType: EntityEngineCyberia.getSingleDropdownValue(EntityEngineCyberia.ids.entityType),
241
+ behavior: EntityEngineCyberia.getSingleDropdownValue(EntityEngineCyberia.ids.behavior),
242
+ liveItemIds: [...EntityEngineCyberia.liveItemIds],
243
+ deadItemIds: [...EntityEngineCyberia.deadItemIds],
244
+ dropItemIds: [...EntityEngineCyberia.dropItemIds],
245
+ defaultObjectLayers: EntityEngineCyberia.defaultObjectLayers.map((l) => ({
246
+ itemId: l.itemId,
247
+ active: !!l.active,
248
+ quantity: l.quantity || 0,
249
+ })),
250
+ };
251
+ }
252
+
253
+ static load(doc) {
254
+ EntityEngineCyberia.currentId = doc._id || null;
255
+ EntityEngineCyberia.setSingleDropdownValue(EntityEngineCyberia.ids.entityType, doc.entityType || '');
256
+ EntityEngineCyberia.setSingleDropdownValue(EntityEngineCyberia.ids.behavior, doc.behavior || '');
257
+ EntityEngineCyberia.liveItemIds = [...(doc.liveItemIds || [])];
258
+ EntityEngineCyberia.deadItemIds = [...(doc.deadItemIds || [])];
259
+ EntityEngineCyberia.dropItemIds = [...(doc.dropItemIds || [])];
260
+ EntityEngineCyberia.defaultObjectLayers = (doc.defaultObjectLayers || []).map((l) => ({
261
+ itemId: l.itemId,
262
+ active: !!l.active,
263
+ quantity: l.quantity || 0,
264
+ }));
265
+ EntityEngineCyberia.renderItemList('live');
266
+ EntityEngineCyberia.renderItemList('dead');
267
+ EntityEngineCyberia.renderItemList('drop');
268
+ EntityEngineCyberia.renderLayerList();
269
+ NotificationManager.Push({ html: `Entity default "${doc.entityType}" loaded`, status: 'success' });
270
+ }
271
+
272
+ static reset() {
273
+ EntityEngineCyberia.currentId = null;
274
+ EntityEngineCyberia.liveItemIds = [];
275
+ EntityEngineCyberia.deadItemIds = [];
276
+ EntityEngineCyberia.dropItemIds = [];
277
+ EntityEngineCyberia.defaultObjectLayers = [];
278
+ EntityEngineCyberia.setSingleDropdownValue(EntityEngineCyberia.ids.entityType, '');
279
+ EntityEngineCyberia.setSingleDropdownValue(EntityEngineCyberia.ids.behavior, '');
280
+ EntityEngineCyberia.renderItemList('live');
281
+ EntityEngineCyberia.renderItemList('dead');
282
+ EntityEngineCyberia.renderItemList('drop');
283
+ EntityEngineCyberia.renderLayerList();
284
+ }
285
+
286
+ static validate(body) {
287
+ if (!body.entityType) {
288
+ NotificationManager.Push({ html: 'Entity type is required.', status: 'error' });
289
+ return false;
290
+ }
291
+ if (body.liveItemIds.length === 0) {
292
+ NotificationManager.Push({
293
+ html: 'At least one live item id is required (it is the link to matching entities).',
294
+ status: 'error',
295
+ });
296
+ return false;
297
+ }
298
+ return true;
299
+ }
300
+
301
+ // Save: create-or-update by whether a row is loaded. Use Update for an explicit
302
+ // update of the loaded row, or Clone to force a new copy.
303
+ static async save() {
304
+ const body = EntityEngineCyberia.getPayload();
305
+ if (!EntityEngineCyberia.validate(body)) return;
306
+ const isUpdate = !!EntityEngineCyberia.currentId;
307
+ const result = isUpdate
308
+ ? await CyberiaEntityTypeDefaultService.put({ id: EntityEngineCyberia.currentId, body })
309
+ : await CyberiaEntityTypeDefaultService.post({ body });
310
+ EntityEngineCyberia.notifyResult(result, isUpdate);
311
+ if (result.status === 'success') {
312
+ if (result.data?._id) EntityEngineCyberia.currentId = result.data._id;
313
+ await EntityEngineCyberia.refreshList();
314
+ }
315
+ }
316
+
317
+ // Update: explicitly persist the loaded row (PUT). Requires a loaded document.
318
+ static async update() {
319
+ if (!EntityEngineCyberia.currentId) {
320
+ NotificationManager.Push({ html: 'Load an entity default to update first.', status: 'warning' });
321
+ return;
322
+ }
323
+ const body = EntityEngineCyberia.getPayload();
324
+ if (!EntityEngineCyberia.validate(body)) return;
325
+ const result = await CyberiaEntityTypeDefaultService.put({ id: EntityEngineCyberia.currentId, body });
326
+ EntityEngineCyberia.notifyResult(result, true);
327
+ if (result.status === 'success') await EntityEngineCyberia.refreshList();
328
+ }
329
+
330
+ // Clone: create a NEW document from the current form values (POST), then load
331
+ // the created copy. Subset matching allows shared itemIds, so a near-duplicate
332
+ // default is valid — typically the author then tweaks behavior or live ids.
333
+ static async clone() {
334
+ const body = EntityEngineCyberia.getPayload();
335
+ if (!EntityEngineCyberia.validate(body)) return;
336
+ EntityEngineCyberia.currentId = null;
337
+ const result = await CyberiaEntityTypeDefaultService.post({ body });
338
+ EntityEngineCyberia.notifyResult(result, false);
339
+ if (result.status === 'success') {
340
+ if (result.data?._id) EntityEngineCyberia.currentId = result.data._id;
341
+ await EntityEngineCyberia.refreshList();
342
+ NotificationManager.Push({ html: 'Cloned into a new entity default.', status: 'success' });
343
+ }
344
+ }
345
+
346
+ static async delete() {
347
+ const targetId = EntityEngineCyberia.currentId;
348
+ if (!targetId) {
349
+ NotificationManager.Push({ html: 'Load an entity default to delete first.', status: 'warning' });
350
+ return;
351
+ }
352
+ const result = await CyberiaEntityTypeDefaultService.delete({ id: targetId });
353
+ NotificationManager.Push({
354
+ html: result.status === 'error' ? result.message : Translate.instance('item-success-delete'),
355
+ status: result.status,
356
+ });
357
+ if (result.status === 'success') {
358
+ EntityEngineCyberia.reset();
359
+ await EntityEngineCyberia.refreshList();
360
+ }
361
+ }
362
+
363
+ // ── Render ──────────────────────────────────────────────────────────────
364
+ static async render(options = {}) {
365
+ const { appStore } = options;
366
+ const role = appStore?.Data?.user?.main?.model?.user?.role || 'guest';
367
+ EntityEngineCyberia.canMutate = commonModeratorGuard(role);
368
+ EntityEngineCyberia.listCache = [];
369
+ EntityEngineCyberia.currentId = null;
370
+ EntityEngineCyberia.liveItemIds = [];
371
+ EntityEngineCyberia.deadItemIds = [];
372
+ EntityEngineCyberia.dropItemIds = [];
373
+ EntityEngineCyberia.defaultObjectLayers = [];
374
+
375
+ const ids = EntityEngineCyberia.ids;
376
+ 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
+
380
+ const group = (title, icon, inner) =>
381
+ html`<div class="in" style="border:1px solid ${groupBorder()};border-radius:8px;padding:12px;margin-bottom:14px;">
382
+ <div
383
+ class="in"
384
+ style="font-size:12px;font-weight:bold;text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px;opacity:.85;"
385
+ >
386
+ <i class="${icon}"></i> ${title}
387
+ </div>
388
+ ${inner}
389
+ </div>`;
390
+
391
+ // A reusable item-id picker + Add button feeding one of the list editors.
392
+ const itemListEditor = async (field, pickerLabel) =>
393
+ html`<div class="fl" style="align-items:flex-end;">
394
+ <div class="in fll" style="flex:1;">
395
+ ${await EntityEngineCyberia.buildItemIdDropdown(ids[`${field}ItemPicker`], pickerLabel)}
396
+ </div>
397
+ <div class="in fll" style="padding-left:6px;">
398
+ ${await BtnIcon.instance({
399
+ class: `wfa btn-entity-engine-add-${field}`,
400
+ label: html`<i class="fa-solid fa-plus"></i> Add`,
401
+ })}
402
+ </div>
403
+ </div>
404
+ <div class="in entity-engine-${field}-list" style="margin-top:8px;"></div>`;
405
+
406
+ setTimeout(async () => {
407
+ ThemeEvents['entity-engine-theme'] = () => {
408
+ EntityEngineCyberia.renderItemList('live');
409
+ EntityEngineCyberia.renderItemList('dead');
410
+ EntityEngineCyberia.renderItemList('drop');
411
+ EntityEngineCyberia.renderLayerList();
412
+ };
413
+
414
+ for (const field of ['live', 'dead', 'drop'])
415
+ if (s(`.btn-entity-engine-add-${field}`))
416
+ s(`.btn-entity-engine-add-${field}`).onclick = () =>
417
+ EntityEngineCyberia.addItem(field, ids[`${field}ItemPicker`]);
418
+
419
+ if (s('.btn-entity-engine-add-layer'))
420
+ s('.btn-entity-engine-add-layer').onclick = () => EntityEngineCyberia.addLayer();
421
+ if (s('.btn-entity-engine-save')) s('.btn-entity-engine-save').onclick = () => EntityEngineCyberia.save();
422
+ if (s('.btn-entity-engine-update')) s('.btn-entity-engine-update').onclick = () => EntityEngineCyberia.update();
423
+ if (s('.btn-entity-engine-clone')) s('.btn-entity-engine-clone').onclick = () => EntityEngineCyberia.clone();
424
+ if (s('.btn-entity-engine-delete')) s('.btn-entity-engine-delete').onclick = () => EntityEngineCyberia.delete();
425
+ if (s('.btn-entity-engine-new')) s('.btn-entity-engine-new').onclick = () => EntityEngineCyberia.reset();
426
+ if (s('.btn-entity-engine-refresh'))
427
+ s('.btn-entity-engine-refresh').onclick = () => EntityEngineCyberia.refreshList();
428
+
429
+ EntityEngineCyberia.renderItemList('live');
430
+ EntityEngineCyberia.renderItemList('dead');
431
+ EntityEngineCyberia.renderItemList('drop');
432
+ EntityEngineCyberia.renderLayerList();
433
+ await EntityEngineCyberia.refreshList();
434
+ });
435
+
436
+ return html`<div class="in section-mp entity-engine-container">
437
+ ${group(
438
+ 'Existing Entity Defaults',
439
+ 'fa-solid fa-table-list',
440
+ html`<div class="fl" style="margin-bottom:8px;">
441
+ <div class="in fll">
442
+ ${await BtnIcon.instance({
443
+ class: 'wfa btn-entity-engine-refresh',
444
+ label: html`<i class="fa-solid fa-rotate"></i> Refresh`,
445
+ })}
446
+ </div>
447
+ </div>
448
+ <div class="in" style="font-size:12px;color:#888;margin-bottom:6px;">
449
+ Click a row to load it into the form.
450
+ </div>
451
+ ${await AgGrid.instance({
452
+ id: EntityEngineCyberia.gridId,
453
+ darkTheme,
454
+ style: { height: '300px' },
455
+ gridOptions: {
456
+ rowData: [],
457
+ columnDefs: [
458
+ { field: 'entityType', headerName: 'Entity Type', minWidth: 120 },
459
+ { 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 },
464
+ ],
465
+ rowSelection: 'single',
466
+ onRowClicked: (event) => {
467
+ const doc = EntityEngineCyberia.listCache.find((d) => d._id === event.data?._id);
468
+ if (doc) EntityEngineCyberia.load(doc);
469
+ },
470
+ },
471
+ })}`,
472
+ )}
473
+ ${group(
474
+ 'Entity Default',
475
+ 'fa-solid fa-circle-info',
476
+ html`<div class="in" style="margin-bottom:8px;">
477
+ ${await DropDown.instance({
478
+ id: ids.entityType,
479
+ label: html`Entity Type`,
480
+ data: entityTypeOptions,
481
+ containerClass: 'inl',
482
+ })}
483
+ </div>
484
+ <div class="in" style="margin-bottom:8px;">
485
+ ${await DropDown.instance({
486
+ id: ids.behavior,
487
+ label: html`Behavior`,
488
+ data: EntityEngineCyberia.behaviorOptions(),
489
+ containerClass: 'inl',
490
+ })}
491
+ </div>
492
+ <div class="in" style="font-size:12px;color:#888;">
493
+ <i class="fa-solid fa-circle-info"></i> Defaults are resolved by the entity's active itemId (usually the
494
+ skin): the system finds the document whose <b>Live Item Ids</b> contains it, then applies its dead, drop and
495
+ default object-layer ids — and its <b>Behavior</b> when set. Entity Type is a label only — it is not the
496
+ lookup key and may repeat across documents. Leave Behavior on <b>auto</b> to derive it (armed → hostile,
497
+ else passive).
498
+ </div>`,
499
+ )}
500
+ ${group(
501
+ 'Live Item Ids',
502
+ 'fa-solid fa-heart',
503
+ html`<div class="in" style="font-size:12px;color:#888;margin-bottom:8px;">
504
+ <i class="fa-solid fa-key"></i> Lookup key — each itemId may belong to only one entity default across the
505
+ whole collection (empty is allowed for non-lookup categories).
506
+ </div>
507
+ ${await itemListEditor('live', html`Search live itemId`)}`,
508
+ )}
509
+ ${group('Dead Item Ids', 'fa-solid fa-skull', await itemListEditor('dead', html`Search dead itemId`))}
510
+ ${group('Drop Item Ids', 'fa-solid fa-gift', await itemListEditor('drop', html`Search drop itemId`))}
511
+ ${group(
512
+ 'Default Object Layers',
513
+ 'fa-solid fa-layer-group',
514
+ html`${dynamicCol({ containerSelector: cont, id: dc.layer, type: 'a-50-b-50' })}
515
+ <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`)}
518
+ </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,
527
+ })}
528
+ </div>
529
+ </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>
534
+ </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
+ })}
540
+ </div>
541
+ <div class="in entity-engine-layer-list" style="margin-top:8px;"></div>`,
542
+ )}
543
+ ${group(
544
+ 'Save Entity Default',
545
+ 'fa-solid fa-floppy-disk',
546
+ 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({
550
+ class: 'wfa btn-entity-engine-save',
551
+ label: html`<i class="fa-solid fa-floppy-disk"></i> Save`,
552
+ })}
553
+ </div>
554
+ <div class="in fll" style="flex:1 1 120px;padding:3px;">
555
+ ${await BtnIcon.instance({
556
+ class: 'wfa btn-entity-engine-update',
557
+ label: html`<i class="fa-solid fa-pen-to-square"></i> Update`,
558
+ })}
559
+ </div>
560
+ <div class="in fll" style="flex:1 1 120px;padding:3px;">
561
+ ${await BtnIcon.instance({
562
+ class: 'wfa btn-entity-engine-clone',
563
+ label: html`<i class="fa-solid fa-clone"></i> Clone`,
564
+ })}
565
+ </div>
566
+ <div class="in fll" style="flex:1 1 120px;padding:3px;">
567
+ ${await BtnIcon.instance({
568
+ class: 'wfa btn-entity-engine-delete',
569
+ label: html`<i class="fa-solid fa-trash"></i> Delete`,
570
+ })}
571
+ </div>`
572
+ : ''}
573
+ <div class="in fll" style="flex:1 1 120px;padding:3px;">
574
+ ${await BtnIcon.instance({
575
+ class: 'wfa btn-entity-engine-new',
576
+ label: html`<i class="fa-solid fa-file"></i> New`,
577
+ })}
578
+ </div>
579
+ </div>`,
580
+ )}
581
+ </div>`;
582
+ }
583
+ }
584
+
585
+ export { EntityEngineCyberia };