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
@@ -1,12 +1,14 @@
1
1
  import { BtnIcon } from '../core/BtnIcon.js';
2
2
  import { Input, InputFile, getFileFromBlobEndpoint } from '../core/Input.js';
3
3
  import { htmls, s } from '../core/VanillaJs.js';
4
+ import { commonModeratorGuard } from '../core/CommonJs.js';
4
5
  import { NotificationManager } from '../core/NotificationManager.js';
5
6
  import { Translate } from '../core/Translate.js';
6
7
  import { dynamicCol, darkTheme, ThemeEvents } from '../core/Css.js';
7
8
  import { DropDown } from '../core/DropDown.js';
8
9
  import { CyberiaInstanceManagement } from '../../services/cyberia-instance/cyberia-instance.management.js';
9
10
  import { CyberiaInstanceService } from '../../services/cyberia-instance/cyberia-instance.service.js';
11
+ import { CyberiaInstanceConfService } from '../../services/cyberia-instance-conf/cyberia-instance-conf.service.js';
10
12
  import { CyberiaMapService } from '../../services/cyberia-map/cyberia-map.service.js';
11
13
  import { FileService } from '../../services/file/file.service.js';
12
14
  import { DefaultManagement } from '../../services/default/default.management.js';
@@ -27,28 +29,102 @@ class InstanceEngineCyberia {
27
29
  static thumbnailDirty = false;
28
30
  static portals = [];
29
31
  static itemIdsDropdownId = 'instance-engine-item-ids-dropdown';
32
+ static itemInventoryListId = 'instance-engine-item-inventory-list';
33
+ // itemId → defaultPlayerInventory flag for the currently selected item ids.
34
+ static itemInventoryFlags = {};
35
+
36
+ // Current selection, read from the dropdown's `oncheckvalues` (always live —
37
+ // it is updated before `_renderSelectedBadges` runs, unlike `.value` which
38
+ // lags by one in the option-click path).
39
+ static getSelectedItemIds() {
40
+ const token = DropDown.Tokens[InstanceEngineCyberia.itemIdsDropdownId];
41
+ if (!token) return [];
42
+ const fromChecks = Object.values(token.oncheckvalues || {})
43
+ .map((v) => v.data)
44
+ .filter(Boolean);
45
+ if (fromChecks.length > 0) return fromChecks;
46
+ return Array.isArray(token.value) ? token.value.filter(Boolean) : [];
47
+ }
48
+
49
+ // Render the per-item "Default Player Inventory" toggles for every selected
50
+ // item id. Kept in sync with the dropdown via a MutationObserver on its badge
51
+ // container (see render()).
52
+ static renderItemInventoryList(containerId = InstanceEngineCyberia.itemInventoryListId) {
53
+ const container = s(`.${containerId}`);
54
+ if (!container) return;
55
+
56
+ const selected = InstanceEngineCyberia.getSelectedItemIds();
57
+
58
+ // Drop flags for items no longer selected so the payload never carries them.
59
+ for (const key of Object.keys(InstanceEngineCyberia.itemInventoryFlags)) {
60
+ if (!selected.includes(key)) delete InstanceEngineCyberia.itemInventoryFlags[key];
61
+ }
62
+
63
+ if (selected.length === 0) {
64
+ htmls(`.${containerId}`, html`<div style="color:#888;font-size:12px;">No item IDs selected.</div>`);
65
+ return;
66
+ }
67
+
68
+ let listHtml = '';
69
+ for (const itemId of selected) {
70
+ const checked = InstanceEngineCyberia.itemInventoryFlags[itemId] ? 'checked' : '';
71
+ listHtml += html`<div class="fl" style="border-bottom:1px solid #444;padding:4px 0;align-items:center;">
72
+ <div class="in fll" style="flex:1;font-size:12px;font-family:monospace;">${itemId}</div>
73
+ <div class="in fll" style="display:flex;align-items:center;justify-content:flex-end;">
74
+ <label style="font-size:11px;cursor:pointer;display:flex;align-items:center;gap:5px;">
75
+ <input
76
+ type="checkbox"
77
+ class="instance-engine-item-inv-checkbox"
78
+ data-item-id="${itemId}"
79
+ ${checked}
80
+ style="cursor:pointer;"
81
+ />
82
+ Default Player Inventory
83
+ </label>
84
+ </div>
85
+ </div>`;
86
+ }
87
+ htmls(`.${containerId}`, listHtml);
88
+
89
+ container.querySelectorAll('.instance-engine-item-inv-checkbox').forEach((cb) => {
90
+ cb.onchange = () => {
91
+ InstanceEngineCyberia.itemInventoryFlags[cb.dataset.itemId] = cb.checked;
92
+ };
93
+ });
94
+ }
30
95
 
31
96
  static syncItemIdsDropdownSelection(itemIds = []) {
32
97
  const dropdownId = InstanceEngineCyberia.itemIdsDropdownId;
33
98
  if (!DropDown.Tokens[dropdownId]) return;
34
99
 
100
+ // Accept both the new [{ id, defaultPlayerInventory }] shape and the legacy
101
+ // string[] shape so older instance docs keep loading.
102
+ const normalized = (itemIds || [])
103
+ .map((entry) => (typeof entry === 'string' ? { id: entry, defaultPlayerInventory: false } : entry))
104
+ .filter((entry) => entry && entry.id);
105
+
35
106
  DropDown.Tokens[dropdownId].value = [];
36
107
  if (s(`.${dropdownId}`)) s(`.${dropdownId}`).value = [];
37
108
  DropDown.Tokens[dropdownId].oncheckvalues = {};
38
109
  htmls(`.dropdown-current-${dropdownId}`, '');
39
110
  htmls(`.${dropdownId}-render-container`, '');
40
111
 
41
- for (const itemId of itemIds) {
42
- const key = dropdownValueKey(itemId);
112
+ InstanceEngineCyberia.itemInventoryFlags = {};
113
+ const ids = [];
114
+ for (const entry of normalized) {
115
+ const key = dropdownValueKey(entry.id);
43
116
  DropDown.Tokens[dropdownId].oncheckvalues[key] = {
44
- data: itemId,
45
- display: itemId,
46
- value: itemId,
117
+ data: entry.id,
118
+ display: entry.id,
119
+ value: entry.id,
47
120
  };
121
+ InstanceEngineCyberia.itemInventoryFlags[entry.id] = !!entry.defaultPlayerInventory;
122
+ ids.push(entry.id);
48
123
  }
49
- DropDown.Tokens[dropdownId].value = [...itemIds];
50
- if (s(`.${dropdownId}`)) s(`.${dropdownId}`).value = [...itemIds];
124
+ DropDown.Tokens[dropdownId].value = [...ids];
125
+ if (s(`.${dropdownId}`)) s(`.${dropdownId}`).value = [...ids];
51
126
  DropDown.Tokens[dropdownId]._renderSelectedBadges?.();
127
+ InstanceEngineCyberia.renderItemInventoryList();
52
128
  }
53
129
 
54
130
  static async buildItemIdsDropdown() {
@@ -151,6 +227,8 @@ class InstanceEngineCyberia {
151
227
 
152
228
  static async render(options = {}) {
153
229
  const { appStore } = options;
230
+ const role = appStore?.Data?.user?.main?.model?.user?.role || 'guest';
231
+ const canMutate = commonModeratorGuard(role);
154
232
  const idCode = 'instance-engine-input-code';
155
233
  const idName = 'instance-engine-input-name';
156
234
  const idDescription = 'instance-engine-input-description';
@@ -169,6 +247,11 @@ class InstanceEngineCyberia {
169
247
  const idTargetCellY = 'instance-engine-target-cell-y';
170
248
  const idFilterSource = 'instance-engine-filter-source';
171
249
  const idFilterTarget = 'instance-engine-filter-target';
250
+ const idSpawnMapCode = 'instance-engine-spawn-map-code';
251
+ const idSpawnCellX = 'instance-engine-spawn-cell-x';
252
+ const idSpawnCellY = 'instance-engine-spawn-cell-y';
253
+ const idSpawnRandom = 'instance-engine-spawn-random';
254
+ const idAoiRadius = 'instance-engine-aoi-radius';
172
255
 
173
256
  InstanceEngineCyberia.currentInstanceId = null;
174
257
  InstanceEngineCyberia.currentThumbnailId = null;
@@ -184,7 +267,10 @@ class InstanceEngineCyberia {
184
267
  const cyberiaMapCodes = DropDown.Tokens[idMapCodesDropdown]?.value
185
268
  ? [...DropDown.Tokens[idMapCodesDropdown].value]
186
269
  : [];
187
- const itemIds = DropDown.Tokens[idItemIdsDropdown]?.value ? [...DropDown.Tokens[idItemIdsDropdown].value] : [];
270
+ const itemIds = InstanceEngineCyberia.getSelectedItemIds().map((id) => ({
271
+ id,
272
+ defaultPlayerInventory: !!InstanceEngineCyberia.itemInventoryFlags[id],
273
+ }));
188
274
  const payload = {
189
275
  code: s(`.${idCode}`)?.value || '',
190
276
  name: s(`.${idName}`)?.value || '',
@@ -194,11 +280,37 @@ class InstanceEngineCyberia {
194
280
  cyberiaMapCodes,
195
281
  itemIds,
196
282
  portals: InstanceEngineCyberia.portals,
283
+ playerSpawn: {
284
+ sourceMapCode: s(`.${idSpawnMapCode}`)?.value?.trim() || '',
285
+ sourceCellX: parseInt(s(`.${idSpawnCellX}`)?.value) || 0,
286
+ sourceCellY: parseInt(s(`.${idSpawnCellY}`)?.value) || 0,
287
+ random: !!s(`.${idSpawnRandom}`)?.checked,
288
+ },
197
289
  };
198
290
  if (InstanceEngineCyberia.currentThumbnailId) payload.thumbnail = InstanceEngineCyberia.currentThumbnailId;
199
291
  return payload;
200
292
  };
201
293
 
294
+ // AOI radius lives on the linked CyberiaInstanceConf (auto-upserted on
295
+ // instance save). Fetch / update it by instanceCode so the engine can edit
296
+ // it alongside instance identity without touching the rest of the conf.
297
+ const fetchConfByCode = async (instanceCode) => {
298
+ if (!instanceCode) return null;
299
+ const res = await CyberiaInstanceConfService.get({
300
+ filterModel: { instanceCode: { filterType: 'text', type: 'equals', filter: instanceCode } },
301
+ });
302
+ return res?.data?.data?.[0] || null;
303
+ };
304
+
305
+ const persistAoiRadius = async (instanceCode) => {
306
+ const raw = s(`.${idAoiRadius}`)?.value;
307
+ if (raw === undefined || raw === '') return;
308
+ const aoiRadius = parseFloat(raw);
309
+ if (!Number.isFinite(aoiRadius)) return;
310
+ const conf = await fetchConfByCode(instanceCode);
311
+ if (conf?._id) await CyberiaInstanceConfService.put({ id: conf._id, body: { aoiRadius } });
312
+ };
313
+
202
314
  const persistInstance = async ({ notify = true } = {}) => {
203
315
  // Upload thumbnail file only if user selected a new one
204
316
  const thumbnailInput = s(`.${idThumbnail}`);
@@ -245,6 +357,9 @@ class InstanceEngineCyberia {
245
357
  }
246
358
  if (result.status === 'success') {
247
359
  if (result.data?._id) InstanceEngineCyberia.currentInstanceId = result.data._id;
360
+ // The conf is auto-upserted on instance save; persist the AOI radius onto
361
+ // it now that we know the instance code resolves to a conf document.
362
+ await persistAoiRadius(body.code);
248
363
  await DefaultManagement.loadTable(managementId, { force: true, reload: true });
249
364
  }
250
365
  return result;
@@ -347,6 +462,17 @@ class InstanceEngineCyberia {
347
462
  portalMode: p.portalMode || 'inter-portal',
348
463
  }));
349
464
  InstanceEngineCyberia.renderPortalList(portalListId);
465
+
466
+ // Player spawn (instance-level).
467
+ const spawn = instanceData.playerSpawn || {};
468
+ if (s(`.${idSpawnMapCode}`)) s(`.${idSpawnMapCode}`).value = spawn.sourceMapCode || '';
469
+ if (s(`.${idSpawnCellX}`)) s(`.${idSpawnCellX}`).value = spawn.sourceCellX ?? 0;
470
+ if (s(`.${idSpawnCellY}`)) s(`.${idSpawnCellY}`).value = spawn.sourceCellY ?? 0;
471
+ if (s(`.${idSpawnRandom}`)) s(`.${idSpawnRandom}`).checked = !!spawn.random;
472
+
473
+ // AOI radius (lives on the linked conf, fetched by instance code).
474
+ const conf = await fetchConfByCode(instanceData.code);
475
+ if (s(`.${idAoiRadius}`)) s(`.${idAoiRadius}`).value = conf?.aoiRadius ?? '';
350
476
  };
351
477
 
352
478
  const resetForm = () => {
@@ -381,8 +507,15 @@ class InstanceEngineCyberia {
381
507
  htmls(`.dropdown-current-${idItemIdsDropdown}`, '');
382
508
  htmls(`.${idItemIdsDropdown}-render-container`, '');
383
509
  }
510
+ InstanceEngineCyberia.itemInventoryFlags = {};
511
+ InstanceEngineCyberia.renderItemInventoryList();
384
512
  InstanceEngineCyberia.portals = [];
385
513
  InstanceEngineCyberia.renderPortalList(portalListId);
514
+ if (s(`.${idSpawnMapCode}`)) s(`.${idSpawnMapCode}`).value = '';
515
+ if (s(`.${idSpawnCellX}`)) s(`.${idSpawnCellX}`).value = 0;
516
+ if (s(`.${idSpawnCellY}`)) s(`.${idSpawnCellY}`).value = 0;
517
+ if (s(`.${idSpawnRandom}`)) s(`.${idSpawnRandom}`).checked = false;
518
+ if (s(`.${idAoiRadius}`)) s(`.${idAoiRadius}`).value = '';
386
519
  };
387
520
 
388
521
  setTimeout(() => {
@@ -495,6 +628,19 @@ class InstanceEngineCyberia {
495
628
  InstanceEngineCyberia.renderPortalList(portalListId);
496
629
  };
497
630
 
631
+ // Keep the per-item "Default Player Inventory" toggles in sync with the
632
+ // item-ids dropdown. Every add/remove/clear re-renders the dropdown badge
633
+ // container, so observing it is a reliable change hook (the dropdown
634
+ // exposes no onChange callback).
635
+ const itemBadgeContainer = s(`.dropdown-current-${idItemIdsDropdown}`);
636
+ if (itemBadgeContainer) {
637
+ const itemInventoryObserver = new MutationObserver(() => {
638
+ InstanceEngineCyberia.renderItemInventoryList();
639
+ });
640
+ itemInventoryObserver.observe(itemBadgeContainer, { childList: true, subtree: true });
641
+ }
642
+ InstanceEngineCyberia.renderItemInventoryList();
643
+
498
644
  ThemeEvents['instance-engine-theme'] = () => {
499
645
  InstanceEngineCyberia.renderPortalList(portalListId);
500
646
  };
@@ -527,6 +673,7 @@ class InstanceEngineCyberia {
527
673
  const dcPortalSource = 'instance-engine-dc-portal-source';
528
674
  const dcPortalTarget = 'instance-engine-dc-portal-target';
529
675
  const dcPortalFilter = 'instance-engine-dc-portal-filter';
676
+ const dcSpawn = 'instance-engine-dc-spawn';
530
677
 
531
678
  return html`<div class="in section-mp instance-engine-container">
532
679
  ${dynamicCol({ containerSelector: 'instance-engine-container', id: dcFields, type: 'search-inputs' })}
@@ -645,6 +792,10 @@ class InstanceEngineCyberia {
645
792
  })}
646
793
  </div>
647
794
  <div class="in section-mp" style="margin-top: 10px;">${await InstanceEngineCyberia.buildItemIdsDropdown()}</div>
795
+ <div class="in section-mp" style="margin-top: 5px;">
796
+ <div class="in input-label" style="font-size:13px;margin-bottom:5px;">Default Player Inventory</div>
797
+ <div class="in ${InstanceEngineCyberia.itemInventoryListId}" style="max-height:200px;overflow-y:auto;"></div>
798
+ </div>
648
799
  <div class="in section-mp" style="margin-top: 10px;">
649
800
  <div class="in input-label" style="font-size:14px;margin-bottom:5px;">Portals</div>
650
801
  ${dynamicCol({ containerSelector: 'instance-engine-container', id: dcPortalSource, type: 'search-inputs' })}
@@ -757,14 +908,69 @@ class InstanceEngineCyberia {
757
908
  <div class="in ${portalListId}" style="margin-top: 10px; max-height: 200px; overflow-y: auto;"></div>
758
909
  </div>
759
910
  <div class="in section-mp" style="margin-top: 10px;">
760
- ${dynamicCol({ containerSelector: 'instance-engine-container', id: dcSaveNew, type: 'a-50-b-50' })}
911
+ <div class="in input-label" style="font-size:14px;margin-bottom:5px;">Player Spawn &amp; World</div>
912
+ <div class="in" style="font-size:12px;color:#888;margin-bottom:6px;">
913
+ Fixed spawn places every new player at the cell below. Enable Random (or leave the map code blank) to spawn at
914
+ a random walkable cell on a random map.
915
+ </div>
916
+ ${dynamicCol({ containerSelector: 'instance-engine-container', id: dcSpawn, type: 'search-inputs' })}
761
917
  <div class="fl">
762
- <div class="in fll ${dcSaveNew}-col-a" style="padding: 5px;">
763
- ${await BtnIcon.instance({
764
- class: 'wfa btn-instance-engine-save',
765
- label: html`<i class="fa-solid fa-floppy-disk"></i> Save Instance`,
918
+ <div class="in fll ${dcSpawn}-col-a">
919
+ ${await Input.instance({
920
+ id: idSpawnMapCode,
921
+ label: html`Spawn Map Code`,
922
+ containerClass: 'inl',
923
+ type: 'text',
924
+ })}
925
+ </div>
926
+ <div class="in fll ${dcSpawn}-col-b">
927
+ ${await Input.instance({
928
+ id: idSpawnCellX,
929
+ label: html`Spawn Cell X`,
930
+ containerClass: 'inl',
931
+ type: 'number',
932
+ min: 0,
933
+ value: 0,
934
+ })}
935
+ </div>
936
+ <div class="in fll ${dcSpawn}-col-c">
937
+ ${await Input.instance({
938
+ id: idSpawnCellY,
939
+ label: html`Spawn Cell Y`,
940
+ containerClass: 'inl',
941
+ type: 'number',
942
+ min: 0,
943
+ value: 0,
766
944
  })}
767
945
  </div>
946
+ </div>
947
+ <div class="in" style="margin-top:6px;">
948
+ <label style="font-size:13px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;">
949
+ <input class="${idSpawnRandom}" type="checkbox" style="cursor:pointer;" /> Random spawn (ignore the fixed
950
+ cell)
951
+ </label>
952
+ </div>
953
+ <div class="in" style="margin-top:10px;">
954
+ ${await Input.instance({
955
+ id: idAoiRadius,
956
+ label: html`AOI Radius (cells)`,
957
+ containerClass: 'inl',
958
+ type: 'number',
959
+ min: 1,
960
+ })}
961
+ </div>
962
+ </div>
963
+ <div class="in section-mp" style="margin-top: 10px;">
964
+ ${dynamicCol({ containerSelector: 'instance-engine-container', id: dcSaveNew, type: 'a-50-b-50' })}
965
+ <div class="fl">
966
+ ${canMutate
967
+ ? html`<div class="in fll ${dcSaveNew}-col-a" style="padding: 5px;">
968
+ ${await BtnIcon.instance({
969
+ class: 'wfa btn-instance-engine-save',
970
+ label: html`<i class="fa-solid fa-floppy-disk"></i> Save Instance`,
971
+ })}
972
+ </div>`
973
+ : ''}
768
974
  <div class="in fll ${dcSaveNew}-col-b" style="padding: 5px;">
769
975
  ${await BtnIcon.instance({
770
976
  class: 'wfa btn-instance-engine-new',
@@ -1,6 +1,7 @@
1
1
  import { BtnIcon } from '../core/BtnIcon.js';
2
2
  import { Input, InputFile, getFileFromBlobEndpoint } from '../core/Input.js';
3
3
  import { htmls, s } from '../core/VanillaJs.js';
4
+ import { commonModeratorGuard } from '../core/CommonJs.js';
4
5
  import { NotificationManager } from '../core/NotificationManager.js';
5
6
  import { Translate } from '../core/Translate.js';
6
7
  import { darkTheme, dynamicCol, ThemeEvents } from '../core/Css.js';
@@ -33,6 +34,7 @@ class MapEngineCyberia {
33
34
  static loadMap = null;
34
35
  static showGridBorders = true;
35
36
  static addOnClick = true;
37
+ static removeOnClick = false;
36
38
  static showObjectLayers = false;
37
39
  static enableRandomFactors = false;
38
40
  static captureObjLayerThumbnail = true;
@@ -151,13 +153,43 @@ class MapEngineCyberia {
151
153
  return false;
152
154
  }
153
155
 
156
+ // Collect indices of filtered entities that have the source itemId
157
+ const eligible = [];
158
+ for (const { i } of filtered) {
159
+ const entity = MapEngineCyberia.entities[i];
160
+ if (Array.isArray(entity?.objectLayerItemIds) && entity.objectLayerItemIds.includes(source)) {
161
+ eligible.push(i);
162
+ }
163
+ }
164
+
165
+ if (!eligible.length) {
166
+ NotificationManager.Push({
167
+ html: `No exact ItemId matches for "${source}" were found in the current filtered entities.`,
168
+ status: 'error',
169
+ });
170
+ return false;
171
+ }
172
+
173
+ // When enableRandomFactors is active, each matching entity independently rolls
174
+ // a random probability check in [fMin, fMax]. If the roll passes, it gets renamed;
175
+ // otherwise it stays unchanged. This produces an intermediate intensity between
176
+ // "almost everyone" (factors near/above 1.0) and "almost no one" (factors near 0).
154
177
  let matchedEntities = 0;
155
178
  let renamedReferences = 0;
156
179
  const changed = MapEngineCyberia.commitEntityMutation(() => {
157
- for (const { i } of filtered) {
180
+ for (const i of eligible) {
181
+ if (MapEngineCyberia.enableRandomFactors) {
182
+ const rawA = parseFloat(s('.map-engine-factor-a')?.value);
183
+ const rawB = parseFloat(s('.map-engine-factor-b')?.value);
184
+ const minF = Number.isFinite(rawA) ? rawA : 0.5;
185
+ const maxF = Number.isFinite(rawB) ? rawB : 1.5;
186
+ const fMin = Math.min(minF, maxF);
187
+ const fMax = Math.max(minF, maxF);
188
+ const roll = fMin + Math.random() * (fMax - fMin);
189
+ // If the roll is >= 1.0, always rename; otherwise compare against Math.random()
190
+ if (roll < 1.0 && Math.random() > roll) continue;
191
+ }
158
192
  const entity = MapEngineCyberia.entities[i];
159
- if (!Array.isArray(entity?.objectLayerItemIds) || entity.objectLayerItemIds.length === 0) continue;
160
-
161
193
  let entityChanged = false;
162
194
  entity.objectLayerItemIds = entity.objectLayerItemIds.map((itemId) => {
163
195
  if (itemId !== source) return itemId;
@@ -165,7 +197,6 @@ class MapEngineCyberia {
165
197
  renamedReferences += 1;
166
198
  return target;
167
199
  });
168
-
169
200
  if (entityChanged) matchedEntities += 1;
170
201
  }
171
202
  });
@@ -570,6 +601,8 @@ class MapEngineCyberia {
570
601
 
571
602
  static async render(options = {}) {
572
603
  const { appStore } = options;
604
+ const role = appStore?.Data?.user?.main?.model?.user?.role || 'guest';
605
+ const canMutate = commonModeratorGuard(role);
573
606
  const idCode = 'map-engine-input-code';
574
607
  const idName = 'map-engine-input-name';
575
608
  const idDescription = 'map-engine-input-description';
@@ -1143,7 +1176,15 @@ class MapEngineCyberia {
1143
1176
  if (s(`.${idInitCellX}`)) s(`.${idInitCellX}`).value = col;
1144
1177
  if (s(`.${idInitCellY}`)) s(`.${idInitCellY}`).value = row;
1145
1178
 
1146
- if (MapEngineCyberia.addOnClick) addEntityLocally();
1179
+ if (MapEngineCyberia.addOnClick) {
1180
+ addEntityLocally();
1181
+ } else if (MapEngineCyberia.removeOnClick) {
1182
+ MapEngineCyberia.commitEntityMutation(() => {
1183
+ MapEngineCyberia.entities = MapEngineCyberia.entities.filter(
1184
+ (entity) => !(entity.initCellX === col && entity.initCellY === row),
1185
+ );
1186
+ });
1187
+ }
1147
1188
  };
1148
1189
 
1149
1190
  if (s(`.btn-map-engine-add-entity`)) s(`.btn-map-engine-add-entity`).onclick = () => addEntityLocally();
@@ -1461,67 +1502,107 @@ class MapEngineCyberia {
1461
1502
  ${dynamicCol({ containerSelector: 'map-engine-container', id: dcCanvasOpts, type: 'search-inputs' })}
1462
1503
  <div class="fl" style="margin-bottom: 5px;">
1463
1504
  <div class="in fll ${dcCanvasOpts}-col-a">
1464
- <div class="fl" style="align-items: center; gap: 8px; font-size: 20px; text-align: left;">
1465
- ${await ToggleSwitch.instance({
1466
- id: 'map-engine-show-grid',
1467
- type: 'checkbox',
1468
- displayMode: 'checkbox',
1469
- containerClass: 'in fll',
1470
- checked: true,
1471
- on: {
1472
- checked: () => {
1473
- MapEngineCyberia.showGridBorders = true;
1474
- rerenderCanvas();
1505
+ <div class="in section-mp-border" style="padding: 10px;">
1506
+ <div class="in input-label" style="margin-bottom: 6px; font-size: 14px;">Click Action</div>
1507
+ <div class="fl" style="align-items: center; gap: 8px; font-size: 20px; text-align: left;">
1508
+ ${await ToggleSwitch.instance({
1509
+ id: 'map-engine-add-on-click',
1510
+ type: 'checkbox',
1511
+ displayMode: 'checkbox',
1512
+ containerClass: 'in fll',
1513
+ checked: true,
1514
+ on: {
1515
+ checked: () => {
1516
+ MapEngineCyberia.addOnClick = true;
1517
+ MapEngineCyberia.removeOnClick = false;
1518
+ const removeToken = ToggleSwitch.Tokens['map-engine-remove-on-click'];
1519
+ if (removeToken) {
1520
+ const removeCheckbox = s('.map-engine-remove-on-click-checkbox');
1521
+ if (removeCheckbox && removeCheckbox.checked) removeToken.click();
1522
+ }
1523
+ },
1524
+ unchecked: () => {
1525
+ MapEngineCyberia.addOnClick = false;
1526
+ },
1475
1527
  },
1476
- unchecked: () => {
1477
- MapEngineCyberia.showGridBorders = false;
1478
- rerenderCanvas();
1528
+ })}
1529
+ <div class="section-mp">&nbsp &nbsp Add on Click</div>
1530
+ </div>
1531
+ <div
1532
+ class="fl"
1533
+ style="align-items: center; gap: 8px; font-size: 20px; text-align: left; margin-top: 4px;"
1534
+ >
1535
+ ${await ToggleSwitch.instance({
1536
+ id: 'map-engine-remove-on-click',
1537
+ type: 'checkbox',
1538
+ displayMode: 'checkbox',
1539
+ containerClass: 'in fll',
1540
+ checked: false,
1541
+ on: {
1542
+ checked: () => {
1543
+ MapEngineCyberia.removeOnClick = true;
1544
+ MapEngineCyberia.addOnClick = false;
1545
+ const addToken = ToggleSwitch.Tokens['map-engine-add-on-click'];
1546
+ if (addToken) {
1547
+ const addCheckbox = s('.map-engine-add-on-click-checkbox');
1548
+ if (addCheckbox && addCheckbox.checked) addToken.click();
1549
+ }
1550
+ },
1551
+ unchecked: () => {
1552
+ MapEngineCyberia.removeOnClick = false;
1553
+ },
1479
1554
  },
1480
- },
1481
- })}
1482
- <div class="section-mp">&nbsp &nbsp Show Grid</div>
1555
+ })}
1556
+ <div class="section-mp">&nbsp &nbsp Remove on Click</div>
1557
+ </div>
1483
1558
  </div>
1484
1559
  </div>
1485
1560
  <div class="in fll ${dcCanvasOpts}-col-b">
1486
- <div class="fl" style="align-items: center; gap: 8px; font-size: 20px; text-align: left;">
1487
- ${await ToggleSwitch.instance({
1488
- id: 'map-engine-add-on-click',
1489
- type: 'checkbox',
1490
- displayMode: 'checkbox',
1491
- containerClass: 'in fll',
1492
- checked: true,
1493
- on: {
1494
- checked: () => {
1495
- MapEngineCyberia.addOnClick = true;
1496
- },
1497
- unchecked: () => {
1498
- MapEngineCyberia.addOnClick = false;
1561
+ <div class="in section-mp-border" style="padding: 10px;">
1562
+ <div class="in input-label" style="margin-bottom: 6px; font-size: 14px;">View Options</div>
1563
+ <div class="fl" style="align-items: center; gap: 8px; font-size: 20px; text-align: left;">
1564
+ ${await ToggleSwitch.instance({
1565
+ id: 'map-engine-show-grid',
1566
+ type: 'checkbox',
1567
+ displayMode: 'checkbox',
1568
+ containerClass: 'in fll',
1569
+ checked: true,
1570
+ on: {
1571
+ checked: () => {
1572
+ MapEngineCyberia.showGridBorders = true;
1573
+ rerenderCanvas();
1574
+ },
1575
+ unchecked: () => {
1576
+ MapEngineCyberia.showGridBorders = false;
1577
+ rerenderCanvas();
1578
+ },
1499
1579
  },
1500
- },
1501
- })}
1502
- <div class="section-mp">&nbsp &nbsp Add on Click</div>
1503
- </div>
1504
- </div>
1505
- <div class="in fll ${dcCanvasOpts}-col-c">
1506
- <div class="fl" style="align-items: center; gap: 8px; font-size: 20px; text-align: left;">
1507
- ${await ToggleSwitch.instance({
1508
- id: 'map-engine-show-object-layers',
1509
- type: 'checkbox',
1510
- displayMode: 'checkbox',
1511
- containerClass: 'in fll',
1512
- checked: false,
1513
- on: {
1514
- checked: () => {
1515
- MapEngineCyberia.showObjectLayers = true;
1516
- rerenderCanvas();
1517
- },
1518
- unchecked: () => {
1519
- MapEngineCyberia.showObjectLayers = false;
1520
- rerenderCanvas();
1580
+ })}
1581
+ <div class="section-mp">&nbsp &nbsp Show Grid</div>
1582
+ </div>
1583
+ <div
1584
+ class="fl"
1585
+ style="align-items: center; gap: 8px; font-size: 20px; text-align: left; margin-top: 4px;"
1586
+ >
1587
+ ${await ToggleSwitch.instance({
1588
+ id: 'map-engine-show-object-layers',
1589
+ type: 'checkbox',
1590
+ displayMode: 'checkbox',
1591
+ containerClass: 'in fll',
1592
+ checked: false,
1593
+ on: {
1594
+ checked: () => {
1595
+ MapEngineCyberia.showObjectLayers = true;
1596
+ rerenderCanvas();
1597
+ },
1598
+ unchecked: () => {
1599
+ MapEngineCyberia.showObjectLayers = false;
1600
+ rerenderCanvas();
1601
+ },
1521
1602
  },
1522
- },
1523
- })}
1524
- <div class="section-mp">&nbsp &nbsp Object Layers</div>
1603
+ })}
1604
+ <div class="section-mp">&nbsp &nbsp Object Layers</div>
1605
+ </div>
1525
1606
  </div>
1526
1607
  </div>
1527
1608
  </div>
@@ -1813,18 +1894,20 @@ class MapEngineCyberia {
1813
1894
  <div class="in ${entityListId}" style="margin-top: 10px; max-height: 200px; overflow-y: auto;"></div>
1814
1895
  ${dynamicCol({ containerSelector: 'map-engine-container', id: dcSaveNew, type: 'search-inputs' })}
1815
1896
  <div class="fl" style="margin-top: 10px;">
1816
- <div class="in fll ${dcSaveNew}-col-a" style="padding: 5px;">
1817
- ${await BtnIcon.instance({
1818
- class: 'wfa btn-map-engine-save-map',
1819
- label: html`<i class="fa-solid fa-floppy-disk"></i> Save Map`,
1820
- })}
1821
- </div>
1822
- <div class="in fll ${dcSaveNew}-col-b" style="padding: 5px;">
1823
- ${await BtnIcon.instance({
1824
- class: 'wfa btn-map-engine-clone-map',
1825
- label: html`<i class="fa-solid fa-clone"></i> Clone Map`,
1826
- })}
1827
- </div>
1897
+ ${canMutate
1898
+ ? html`<div class="in fll ${dcSaveNew}-col-a" style="padding: 5px;">
1899
+ ${await BtnIcon.instance({
1900
+ class: 'wfa btn-map-engine-save-map',
1901
+ label: html`<i class="fa-solid fa-floppy-disk"></i> Save Map`,
1902
+ })}
1903
+ </div>
1904
+ <div class="in fll ${dcSaveNew}-col-b" style="padding: 5px;">
1905
+ ${await BtnIcon.instance({
1906
+ class: 'wfa btn-map-engine-clone-map',
1907
+ label: html`<i class="fa-solid fa-clone"></i> Clone Map`,
1908
+ })}
1909
+ </div>`
1910
+ : ''}
1828
1911
  <div class="in fll ${dcSaveNew}-col-c" style="padding: 5px;">
1829
1912
  ${await BtnIcon.instance({
1830
1913
  class: 'wfa btn-map-engine-new-map',