cyberia 3.2.22 → 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 (185) 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 +33 -24
  11. package/.github/workflows/engine-cyberia.ci.yml +13 -3
  12. package/.github/workflows/ghpkg.ci.yml +88 -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 +10 -7
  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 +301 -1
  21. package/CLI-HELP.md +71 -6
  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 +31 -0
  28. package/bin/cyberia.js +1080 -184
  29. package/bin/deploy.js +2 -2
  30. package/bin/index.js +1080 -184
  31. package/bump.config.js +1 -0
  32. package/compose.env +131 -0
  33. package/conf.js +18 -1
  34. package/deployment.yaml +2 -2
  35. package/docker-compose.yml +316 -0
  36. package/hardhat/hardhat.config.js +2 -2
  37. package/hardhat/package-lock.json +620 -2041
  38. package/hardhat/package.json +7 -5
  39. package/hardhat/scripts/deployObjectLayerToken.js +18 -18
  40. package/hardhat/test/ObjectLayerToken.js +378 -274
  41. package/ipfs/configure-ipfs.sh +13 -0
  42. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  43. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  44. package/manifests/deployment/dd-cyberia-development/deployment.yaml +2 -2
  45. package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
  46. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
  47. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  48. package/mongodb/entrypoint.sh +76 -0
  49. package/nginx.conf +87 -0
  50. package/package.json +31 -19
  51. package/pv-pvc.yaml +32 -0
  52. package/scripts/disk-clean.sh +85 -60
  53. package/scripts/kubeadm-node-setup.sh +317 -0
  54. package/scripts/rocky-kickstart.sh +877 -185
  55. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  56. package/scripts/test-monitor.sh +3 -5
  57. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
  58. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
  59. package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
  60. package/src/api/cyberia-action/cyberia-action.model.js +21 -29
  61. package/src/api/cyberia-action/cyberia-action.router.js +42 -7
  62. package/src/api/cyberia-action/cyberia-action.service.js +20 -4
  63. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
  64. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
  65. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
  66. package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
  67. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
  68. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
  69. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
  70. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
  71. package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
  72. package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
  73. package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
  74. package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
  75. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
  76. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
  77. package/src/api/cyberia-map/cyberia-map.router.js +21 -6
  78. package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
  79. package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
  80. package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
  81. package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
  82. package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
  83. package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
  84. package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
  85. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -129
  86. package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
  87. package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
  88. package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
  89. package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
  90. package/src/api/ipfs/ipfs.service.js +28 -15
  91. package/src/api/object-layer/object-layer.router.js +25 -15
  92. package/src/api/object-layer/object-layer.service.js +15 -20
  93. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
  94. package/src/cli/baremetal.js +717 -16
  95. package/src/cli/cluster.js +80 -5
  96. package/src/cli/deploy.js +127 -11
  97. package/src/cli/docker-compose.js +648 -0
  98. package/src/cli/env.js +11 -2
  99. package/src/cli/fs.js +75 -30
  100. package/src/cli/image.js +129 -51
  101. package/src/cli/index.js +110 -6
  102. package/src/cli/kickstart.js +142 -20
  103. package/src/cli/release.js +46 -4
  104. package/src/cli/repository.js +238 -33
  105. package/src/cli/run.js +520 -114
  106. package/src/cli/secrets.js +82 -48
  107. package/src/cli/ssh.js +234 -0
  108. package/src/cli/static.js +2 -2
  109. package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
  110. package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
  111. package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
  112. package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
  113. package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
  114. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
  115. package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
  116. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
  117. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
  118. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
  119. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +45 -27
  120. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
  121. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
  122. package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
  123. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
  124. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +8 -1
  125. package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
  126. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +5 -5
  127. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  128. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  129. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
  130. package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
  131. package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
  132. package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
  133. package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
  134. package/src/client/services/object-layer/object-layer.management.js +6 -6
  135. package/src/{server → client-builder}/client-build-docs.js +15 -5
  136. package/src/{server → client-builder}/client-build-live.js +3 -3
  137. package/src/{server → client-builder}/client-build.js +25 -22
  138. package/src/{server → client-builder}/client-dev-server.js +3 -3
  139. package/src/{server → client-builder}/client-icons.js +2 -2
  140. package/src/{server → client-builder}/ssr.js +5 -5
  141. package/src/client.build.js +1 -3
  142. package/src/client.dev.js +1 -1
  143. package/src/db/mongo/MongoBootstrap.js +12 -12
  144. package/src/grpc/cyberia/grpc-server.js +255 -70
  145. package/src/index.js +12 -1
  146. package/src/mailer/EmailRender.js +1 -1
  147. package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
  148. package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
  149. package/src/projects/cyberia/catalog-cyberia.js +81 -0
  150. package/src/projects/cyberia/gemini-client.js +175 -0
  151. package/src/projects/cyberia/generate-saga.js +2107 -0
  152. package/src/{server → projects/cyberia}/ipfs-client.js +2 -2
  153. package/src/{server → projects/cyberia}/object-layer.js +12 -108
  154. package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
  155. package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
  156. package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
  157. package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
  158. package/src/{server → projects/cyberia}/shape-generator.js +2 -2
  159. package/src/{server → projects/underpost}/catalog-underpost.js +1 -2
  160. package/src/runtime/cyberia-client/Dockerfile +27 -61
  161. package/src/runtime/cyberia-client/Dockerfile.dev +20 -12
  162. package/src/runtime/cyberia-server/Dockerfile +21 -20
  163. package/src/runtime/cyberia-server/Dockerfile.dev +17 -8
  164. package/src/runtime/engine-cyberia/Dockerfile +143 -0
  165. package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
  166. package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
  167. package/src/runtime/engine-cyberia/compose.env +131 -0
  168. package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
  169. package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
  170. package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
  171. package/src/runtime/engine-cyberia/nginx.conf +87 -0
  172. package/src/runtime/express/Express.js +2 -2
  173. package/src/runtime/nginx/Nginx.js +250 -0
  174. package/src/server/catalog.js +9 -14
  175. package/src/server/conf.js +3 -6
  176. package/src/server/runtime-status.js +18 -1
  177. package/src/server/start.js +12 -2
  178. package/src/server.js +6 -2
  179. package/test/cyberia-instance-conf-defaults.test.js +140 -0
  180. package/test/deploy-monitor.test.js +26 -10
  181. package/test/shape-generator.test.js +7 -1
  182. package/typedoc.dd-cyberia.json +3 -1
  183. package/typedoc.json +3 -1
  184. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  185. /package/src/{server → client-builder}/client-formatted.js +0 -0
@@ -0,0 +1,1867 @@
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 { getProxyPath } from '../core/Router.js';
10
+ import { ObjectLayerService } from '../../services/object-layer/object-layer.service.js';
11
+ import { CyberiaMapService } from '../../services/cyberia-map/cyberia-map.service.js';
12
+ import { CyberiaQuestService } from '../../services/cyberia-quest/cyberia-quest.service.js';
13
+ import { CyberiaActionService } from '../../services/cyberia-action/cyberia-action.service.js';
14
+ import { CyberiaDialogueService } from '../../services/cyberia-dialogue/cyberia-dialogue.service.js';
15
+ import { CyberiaSkillService } from '../../services/cyberia-skill/cyberia-skill.service.js';
16
+ import { QUEST_STEPS_TYPES, getDefaultCyberiaItemById, SKILL_LOGIC_IDS } from './SharedDefaultsCyberia.js';
17
+
18
+ const textFilter = (filter) => ({ filterType: 'text', type: 'equals', filter });
19
+ // DropDown invokes optionData.onClick on selection, so every option (including
20
+ // serviceProvider results) must carry one.
21
+ const dropdownOption = (value) => ({ value, display: value, data: value, onClick: () => {} });
22
+ const groupBorder = () => (darkTheme ? '#3a3a3a' : '#d4d4d4');
23
+ const subtleBorder = () => (darkTheme ? '#444' : '#e0e0e0');
24
+
25
+ // ActionEngineCyberia — a map-driven engine that unifies quest, action and
26
+ // dialogue authoring around a shared, visual map surface. It is intentionally
27
+ // decoupled from MapEngineCyberia: the only integration points are the public
28
+ // data services (maps, quests, actions, dialogues, object-layer item ids) and
29
+ // the same object-layer texture-loading convention. The engine reads a map
30
+ // purely as a placement backdrop and never mutates map documents. CRUD is
31
+ // surfaced as per-entity card lists (mirroring the map-engine entity list),
32
+ // not management grids.
33
+ class ActionEngineCyberia {
34
+ static mode = 'quest';
35
+ static mapCode = '';
36
+ static mapDoc = null;
37
+ static imageCache = {};
38
+ static assignments = { quests: [], actions: [] };
39
+
40
+ static questListCache = [];
41
+ static actionListCache = [];
42
+ static dialogueListCache = [];
43
+ static skillListCache = [];
44
+
45
+ static currentQuestId = null;
46
+ static currentActionId = null;
47
+ static currentDialogueId = null;
48
+ static currentSkillId = null;
49
+
50
+ static questSteps = [];
51
+ static questRewards = [];
52
+ static actionQuestDialogues = [];
53
+ static loadedActionPayloadExtras = {};
54
+ // Editable `skills[]` for the loaded CyberiaSkill (logicEventIds are derived
55
+ // from these on save, mirroring DefaultSkillConfig).
56
+ static skillDefs = [];
57
+
58
+ static ids = {
59
+ mapDropdown: 'action-engine-map-dropdown',
60
+ objectiveItemPicker: 'action-engine-objective-item-picker',
61
+ rewardItemPicker: 'action-engine-reward-item-picker',
62
+ objectiveType: 'action-engine-objective-type',
63
+ actionDialogPicker: 'action-engine-action-dialog-picker',
64
+ actionQuestDialogPicker: 'action-engine-action-quest-dialog-picker',
65
+ skillTriggerItemPicker: 'action-engine-skill-trigger-item-picker',
66
+ skillSummonedItemPicker: 'action-engine-skill-summoned-item-picker',
67
+ };
68
+
69
+ // ── Searchable dropdown helpers ─────────────────────────────────────────
70
+ static getSingleDropdownValue(id) {
71
+ const value = DropDown.Tokens[id]?.value;
72
+ return typeof value === 'string' ? value.trim() : '';
73
+ }
74
+
75
+ static setSingleDropdownValue(id, value) {
76
+ if (!DropDown.Tokens[id]) return;
77
+ const v = value || '';
78
+ DropDown.Tokens[id].value = v;
79
+ if (s(`.${id}`)) s(`.${id}`).value = v;
80
+ htmls(`.dropdown-current-${id}`, v || '');
81
+ }
82
+
83
+ // itemId references the item/object-layer collection, so the picker is a fast
84
+ // server-backed autocomplete instead of a free-text input.
85
+ static async buildItemIdDropdown(id, label) {
86
+ return await DropDown.instance({
87
+ id,
88
+ label,
89
+ data: [],
90
+ containerClass: 'inl',
91
+ serviceProvider: async (q) => {
92
+ const result = await ObjectLayerService.searchItemIds({ q });
93
+ if (result.status === 'success' && result.data?.itemIds) {
94
+ return result.data.itemIds.map((itemId) => dropdownOption(itemId));
95
+ }
96
+ return [];
97
+ },
98
+ });
99
+ }
100
+
101
+ static async buildMapDropdown(id) {
102
+ return await DropDown.instance({
103
+ id,
104
+ label: html`Map`,
105
+ data: [],
106
+ containerClass: 'inl',
107
+ serviceProvider: async (q) => {
108
+ const result = await CyberiaMapService.searchCodes({ q });
109
+ if (result.status === 'success' && result.data?.codes) {
110
+ return result.data.codes.map((code) => dropdownOption(code));
111
+ }
112
+ return [];
113
+ },
114
+ });
115
+ }
116
+
117
+ // Dialogue groups are keyed by `code`; the picker derives distinct codes from
118
+ // the dialogue collection so authors can reuse existing dialogue groups.
119
+ static async buildDialogCodeDropdown(id, label) {
120
+ return await DropDown.instance({
121
+ id,
122
+ label,
123
+ data: [],
124
+ containerClass: 'inl',
125
+ serviceProvider: async (q) => {
126
+ const result = await CyberiaDialogueService.get({
127
+ limit: 100,
128
+ filterModel: { code: { filterType: 'text', type: 'contains', filter: q } },
129
+ });
130
+ const rows = result?.data?.data || [];
131
+ const codes = [...new Set(rows.map((r) => r.code).filter(Boolean))];
132
+ return codes.map((code) => dropdownOption(code));
133
+ },
134
+ });
135
+ }
136
+
137
+ // ── Object-layer textures (same loading convention as MapEngineCyberia) ──
138
+ static loadObjectLayerImage(itemId, onLoad) {
139
+ if (ActionEngineCyberia.imageCache[itemId]) return;
140
+ ActionEngineCyberia.imageCache[itemId] = { img: null, loaded: false, error: false };
141
+
142
+ const loadImage = (type, id) => {
143
+ const img = new Image();
144
+ img.onload = () => {
145
+ ActionEngineCyberia.imageCache[itemId].img = img;
146
+ ActionEngineCyberia.imageCache[itemId].loaded = true;
147
+ if (onLoad) onLoad();
148
+ };
149
+ img.onerror = () => {
150
+ ActionEngineCyberia.imageCache[itemId].error = true;
151
+ };
152
+ img.src = `${getProxyPath()}assets/${type}/${id}/08/0.png`;
153
+ };
154
+
155
+ const sharedItem = getDefaultCyberiaItemById(itemId)?.item;
156
+ if (sharedItem?.type && sharedItem?.id) {
157
+ loadImage(sharedItem.type, sharedItem.id);
158
+ return;
159
+ }
160
+
161
+ ObjectLayerService.get({
162
+ limit: 1,
163
+ filterModel: { 'data.item.id': { filterType: 'text', type: 'equals', filter: itemId } },
164
+ })
165
+ .then((res) => {
166
+ const doc = res?.data?.data?.[0];
167
+ if (!doc || !doc.data?.item?.type || !doc.data?.item?.id) {
168
+ ActionEngineCyberia.imageCache[itemId].error = true;
169
+ return;
170
+ }
171
+ loadImage(doc.data.item.type, doc.data.item.id);
172
+ })
173
+ .catch(() => {
174
+ ActionEngineCyberia.imageCache[itemId].error = true;
175
+ });
176
+ }
177
+
178
+ static preloadMapObjectLayers(onLoad) {
179
+ for (const entity of ActionEngineCyberia.mapDoc?.entities || [])
180
+ for (const itemId of entity.objectLayerItemIds || []) ActionEngineCyberia.loadObjectLayerImage(itemId, onLoad);
181
+ }
182
+
183
+ // ── Map canvas ──────────────────────────────────────────────────────────
184
+ static getMapGeometry() {
185
+ const map = ActionEngineCyberia.mapDoc;
186
+ return {
187
+ cols: map?.gridX || 16,
188
+ rows: map?.gridY || 16,
189
+ cellW: Math.max(8, Math.min(28, map?.cellWidth || 18)),
190
+ cellH: Math.max(8, Math.min(28, map?.cellHeight || 18)),
191
+ };
192
+ }
193
+
194
+ static getSelectedCell() {
195
+ return {
196
+ x: parseInt(s('.action-engine-cell-x')?.value),
197
+ y: parseInt(s('.action-engine-cell-y')?.value),
198
+ };
199
+ }
200
+
201
+ static renderMapCanvas() {
202
+ const canvas = s('.action-engine-canvas');
203
+ if (!canvas) return;
204
+ const { cols, rows, cellW, cellH } = ActionEngineCyberia.getMapGeometry();
205
+ canvas.width = cols * cellW;
206
+ canvas.height = rows * cellH;
207
+ const ctx = canvas.getContext('2d');
208
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
209
+
210
+ for (const entity of ActionEngineCyberia.mapDoc?.entities || []) {
211
+ const x = entity.initCellX * cellW;
212
+ const y = entity.initCellY * cellH;
213
+ const w = entity.dimX * cellW;
214
+ const h = entity.dimY * cellH;
215
+ let drew = false;
216
+ if (entity.objectLayerItemIds?.length) {
217
+ for (const itemId of entity.objectLayerItemIds) {
218
+ const cached = ActionEngineCyberia.imageCache[itemId];
219
+ if (cached?.loaded && cached.img) {
220
+ ctx.drawImage(cached.img, x, y, w, h);
221
+ drew = true;
222
+ }
223
+ }
224
+ }
225
+ if (!drew) {
226
+ ctx.fillStyle = entity.color || 'rgba(80,80,80,0.6)';
227
+ ctx.fillRect(x, y, w, h);
228
+ }
229
+ }
230
+
231
+ ctx.strokeStyle = darkTheme ? '#3a3a3a' : '#cccccc';
232
+ ctx.lineWidth = 1;
233
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) ctx.strokeRect(c * cellW, r * cellH, cellW, cellH);
234
+
235
+ const drawMarker = (x, y, color, glyph) => {
236
+ const cx = x * cellW + cellW / 2;
237
+ const cy = y * cellH + cellH / 2;
238
+ ctx.beginPath();
239
+ ctx.arc(cx, cy, Math.min(cellW, cellH) / 2.4, 0, Math.PI * 2);
240
+ ctx.fillStyle = color;
241
+ ctx.fill();
242
+ ctx.fillStyle = '#fff';
243
+ ctx.font = `bold ${Math.floor(Math.min(cellW, cellH) * 0.7)}px monospace`;
244
+ ctx.textAlign = 'center';
245
+ ctx.textBaseline = 'middle';
246
+ ctx.fillText(glyph, cx, cy);
247
+ };
248
+ for (const q of ActionEngineCyberia.assignments.quests)
249
+ if (q.sourceCellX != null) drawMarker(q.sourceCellX, q.sourceCellY, 'rgba(40,120,220,0.92)', 'Q');
250
+ for (const a of ActionEngineCyberia.assignments.actions)
251
+ if (a.sourceCellX != null) drawMarker(a.sourceCellX, a.sourceCellY, 'rgba(220,120,40,0.92)', 'A');
252
+
253
+ const sel = ActionEngineCyberia.getSelectedCell();
254
+ if (Number.isFinite(sel.x) && Number.isFinite(sel.y)) {
255
+ ctx.strokeStyle = '#1fd11f';
256
+ ctx.lineWidth = 3;
257
+ ctx.strokeRect(sel.x * cellW + 1, sel.y * cellH + 1, cellW - 2, cellH - 2);
258
+ }
259
+ }
260
+
261
+ static async loadMap(code) {
262
+ if (!code) {
263
+ NotificationManager.Push({ html: 'Select a map first.', status: 'warning' });
264
+ return;
265
+ }
266
+ const result = await CyberiaMapService.get({ limit: 1, filterModel: { code: textFilter(code) } });
267
+ const doc = result?.data?.data?.[0];
268
+ if (!doc) {
269
+ NotificationManager.Push({ html: `Map "${code}" not found`, status: 'error' });
270
+ return;
271
+ }
272
+ ActionEngineCyberia.mapCode = code;
273
+ ActionEngineCyberia.mapDoc = doc;
274
+ htmls('.action-engine-map-meta', `${doc.code} · ${doc.gridX || 16}×${doc.gridY || 16} cells`);
275
+ ActionEngineCyberia.preloadMapObjectLayers(() => ActionEngineCyberia.renderMapCanvas());
276
+ await ActionEngineCyberia.refreshAssignments();
277
+ ActionEngineCyberia.renderMapCanvas();
278
+ }
279
+
280
+ static async refreshAssignments() {
281
+ const code = ActionEngineCyberia.mapCode;
282
+ if (!code) {
283
+ ActionEngineCyberia.assignments = { quests: [], actions: [] };
284
+ ActionEngineCyberia.renderAssignmentReview();
285
+ return;
286
+ }
287
+ const [questRes, actionRes] = await Promise.all([
288
+ CyberiaQuestService.get({ limit: 500, filterModel: { sourceMapCode: textFilter(code) } }),
289
+ CyberiaActionService.get({ limit: 500, filterModel: { sourceMapCode: textFilter(code) } }),
290
+ ]);
291
+ ActionEngineCyberia.assignments = {
292
+ quests: questRes?.data?.data || [],
293
+ actions: actionRes?.data?.data || [],
294
+ };
295
+ ActionEngineCyberia.renderMapCanvas();
296
+ ActionEngineCyberia.renderAssignmentReview();
297
+ }
298
+
299
+ static renderAssignmentReview() {
300
+ const container = s('.action-engine-assignment-review');
301
+ if (!container) return;
302
+ const { quests, actions } = ActionEngineCyberia.assignments;
303
+ const row = (kind, color, doc) =>
304
+ html`<div
305
+ class="fl action-engine-assignment-row"
306
+ style="border-bottom:1px solid ${subtleBorder()};padding:4px 0;align-items:center;cursor:pointer;"
307
+ data-kind="${kind}"
308
+ data-id="${doc._id}"
309
+ >
310
+ <div
311
+ class="in fll"
312
+ style="width:18px;height:18px;border-radius:50%;background:${color};color:#fff;text-align:center;font-size:11px;line-height:18px;margin-right:6px;"
313
+ >
314
+ ${kind === 'quest' ? 'Q' : 'A'}
315
+ </div>
316
+ <div class="in fll" style="flex:1;font-size:12px;font-family:monospace;">
317
+ ${doc.code}
318
+ ${doc.sourceCellX != null
319
+ ? `(${doc.sourceCellX}, ${doc.sourceCellY})`
320
+ : '<span style="color:#999;">unplaced</span>'}
321
+ </div>
322
+ </div>`;
323
+ let out = '';
324
+ for (const q of quests) out += row('quest', 'rgba(40,120,220,0.92)', q);
325
+ for (const a of actions) out += row('action', 'rgba(220,120,40,0.92)', a);
326
+ if (!out)
327
+ out = `<div style="color:#888;font-size:13px;">${ActionEngineCyberia.mapCode ? 'No assignments on this map yet.' : 'Load a map to review assignments.'}</div>`;
328
+ htmls('.action-engine-assignment-review', out);
329
+
330
+ container.querySelectorAll('.action-engine-assignment-row').forEach((rowEl) => {
331
+ rowEl.onclick = async () => {
332
+ const { kind, id } = rowEl.dataset;
333
+ if (kind === 'quest') {
334
+ ActionEngineCyberia.setMode('quest');
335
+ const res = await CyberiaQuestService.get({ id });
336
+ if (res.status === 'success' && res.data) ActionEngineCyberia.loadQuest(res.data);
337
+ } else {
338
+ ActionEngineCyberia.setMode('action');
339
+ const res = await CyberiaActionService.get({ id });
340
+ if (res.status === 'success' && res.data) ActionEngineCyberia.loadAction(res.data);
341
+ }
342
+ };
343
+ });
344
+ }
345
+
346
+ // ── Mode switching ──────────────────────────────────────────────────────
347
+ static setMode(mode) {
348
+ ActionEngineCyberia.mode = mode;
349
+ for (const m of ['quest', 'action', 'dialogue', 'skill']) {
350
+ const panel = s(`.action-engine-panel-${m}`);
351
+ if (panel) panel.classList[m === mode ? 'remove' : 'add']('hide');
352
+ const tab = s(`.action-engine-tab-${m}`);
353
+ if (tab) {
354
+ tab.style.opacity = m === mode ? '1' : '0.5';
355
+ tab.style.fontWeight = m === mode ? 'bold' : 'normal';
356
+ tab.style.borderBottomColor = m === mode ? '#1fd11f' : 'transparent';
357
+ }
358
+ }
359
+ }
360
+
361
+ // Keeps the shared map surface in sync with the loaded document: reflects the
362
+ // map dropdown, loads the matching map (with object-layer textures) and
363
+ // highlights the assigned cell so the editor is reactive on load.
364
+ static async syncMapForLoadedDoc(doc) {
365
+ if (doc.sourceMapCode) {
366
+ ActionEngineCyberia.setSingleDropdownValue(ActionEngineCyberia.ids.mapDropdown, doc.sourceMapCode);
367
+ if (doc.sourceMapCode !== ActionEngineCyberia.mapCode) {
368
+ await ActionEngineCyberia.loadMap(doc.sourceMapCode);
369
+ return;
370
+ }
371
+ }
372
+ ActionEngineCyberia.renderMapCanvas();
373
+ }
374
+
375
+ // ── Multi-step quest editor ─────────────────────────────────────────────
376
+ static syncStepDescriptions() {
377
+ ActionEngineCyberia.questSteps.forEach((step, i) => {
378
+ const el = s(`.action-engine-step-desc-${i}`);
379
+ if (el) step.description = el.value;
380
+ });
381
+ }
382
+
383
+ static async renderStepList() {
384
+ const container = s('.action-engine-step-list');
385
+ if (!container) return;
386
+ const steps = ActionEngineCyberia.questSteps;
387
+ let out = '';
388
+ for (let i = 0; i < steps.length; i++) {
389
+ const step = steps[i];
390
+ let objOut = '';
391
+ for (let j = 0; j < step.objectives.length; j++) {
392
+ const o = step.objectives[j];
393
+ objOut += html`<div
394
+ class="fl"
395
+ style="border-bottom:1px solid ${subtleBorder()};padding:3px 0;align-items:center;"
396
+ >
397
+ <div class="in fll" style="flex:1;font-family:monospace;font-size:12px;">
398
+ <span style="color:${darkTheme ? '#8cf' : '#246'};">${o.type}</span> · ${o.itemId} ×${o.quantity}
399
+ </div>
400
+ ${await BtnIcon.instance({
401
+ class: `btn-aeq-rmobj-${i}-${j}`,
402
+ label: html`<i class="fa-solid fa-xmark"></i>`,
403
+ style: 'min-width:30px;padding:2px 8px;',
404
+ })}
405
+ </div>`;
406
+ }
407
+ if (!objOut) objOut = '<div style="color:#888;font-size:11px;padding:2px 0;">No objectives in this step.</div>';
408
+ out += html`<div
409
+ class="in"
410
+ style="border:1px solid ${subtleBorder()};border-radius:6px;padding:8px;margin-bottom:8px;"
411
+ >
412
+ <div class="fl" style="align-items:center;margin-bottom:6px;">
413
+ <div class="in fll" style="flex:1;font-weight:bold;font-size:12px;">
414
+ <i class="fa-solid fa-layer-group"></i> Step ${i + 1}
415
+ </div>
416
+ ${await BtnIcon.instance({
417
+ class: `btn-aeq-rmstep-${i}`,
418
+ label: html`<i class="fa-solid fa-trash"></i>`,
419
+ style: 'min-width:30px;padding:2px 8px;',
420
+ })}
421
+ </div>
422
+ ${await Input.instance({
423
+ id: `action-engine-step-desc-${i}`,
424
+ label: html`Step description`,
425
+ containerClass: 'inl',
426
+ type: 'text',
427
+ value: step.description || '',
428
+ })}
429
+ <div class="in" style="margin-top:6px;">${objOut}</div>
430
+ </div>`;
431
+ }
432
+ if (!out) out = '<div style="color:#888;font-size:12px;">No steps yet. Add a step to begin.</div>';
433
+ htmls('.action-engine-step-list', out);
434
+
435
+ for (let i = 0; i < steps.length; i++) {
436
+ if (s(`.btn-aeq-rmstep-${i}`))
437
+ s(`.btn-aeq-rmstep-${i}`).onclick = () => {
438
+ ActionEngineCyberia.syncStepDescriptions();
439
+ ActionEngineCyberia.questSteps.splice(i, 1);
440
+ ActionEngineCyberia.renderStepList();
441
+ };
442
+ for (let j = 0; j < steps[i].objectives.length; j++) {
443
+ if (s(`.btn-aeq-rmobj-${i}-${j}`))
444
+ s(`.btn-aeq-rmobj-${i}-${j}`).onclick = () => {
445
+ ActionEngineCyberia.syncStepDescriptions();
446
+ ActionEngineCyberia.questSteps[i].objectives.splice(j, 1);
447
+ ActionEngineCyberia.renderStepList();
448
+ };
449
+ }
450
+ }
451
+ }
452
+
453
+ static addStep() {
454
+ ActionEngineCyberia.syncStepDescriptions();
455
+ const n = ActionEngineCyberia.questSteps.length + 1;
456
+ ActionEngineCyberia.questSteps.push({ id: `step-${n}`, description: '', objectives: [] });
457
+ ActionEngineCyberia.renderStepList();
458
+ if (s('.action-engine-objective-step')) s('.action-engine-objective-step').value = n;
459
+ }
460
+
461
+ static addObjective() {
462
+ const itemId = ActionEngineCyberia.getSingleDropdownValue(ActionEngineCyberia.ids.objectiveItemPicker);
463
+ const type = DropDown.Tokens[ActionEngineCyberia.ids.objectiveType]?.value || QUEST_STEPS_TYPES[0];
464
+ const quantity = Math.max(1, parseInt(s('.action-engine-objective-qty')?.value) || 1);
465
+ if (!itemId) {
466
+ NotificationManager.Push({ html: 'Select an itemId for the objective.', status: 'error' });
467
+ return;
468
+ }
469
+ ActionEngineCyberia.syncStepDescriptions();
470
+ let stepIdx = (parseInt(s('.action-engine-objective-step')?.value) || 1) - 1;
471
+ if (ActionEngineCyberia.questSteps.length === 0) {
472
+ ActionEngineCyberia.questSteps.push({ id: 'step-1', description: '', objectives: [] });
473
+ stepIdx = 0;
474
+ if (s('.action-engine-objective-step')) s('.action-engine-objective-step').value = 1;
475
+ }
476
+ if (stepIdx < 0 || stepIdx >= ActionEngineCyberia.questSteps.length) {
477
+ NotificationManager.Push({ html: `Step #${stepIdx + 1} does not exist.`, status: 'error' });
478
+ return;
479
+ }
480
+ const duplicate = ActionEngineCyberia.questSteps[stepIdx].objectives.some(
481
+ (o) => o.type === type && o.itemId === itemId,
482
+ );
483
+ if (duplicate) {
484
+ NotificationManager.Push({
485
+ html: `Step ${stepIdx + 1} already has a "${type}" objective for "${itemId}".`,
486
+ status: 'error',
487
+ });
488
+ return;
489
+ }
490
+ ActionEngineCyberia.questSteps[stepIdx].objectives.push({ type, itemId, quantity });
491
+ ActionEngineCyberia.renderStepList();
492
+ }
493
+
494
+ static async renderRewardList() {
495
+ const container = s('.action-engine-reward-list');
496
+ if (!container) return;
497
+ let out = '';
498
+ for (let i = 0; i < ActionEngineCyberia.questRewards.length; i++) {
499
+ const r = ActionEngineCyberia.questRewards[i];
500
+ out += html`<div
501
+ class="fl"
502
+ style="border-bottom:1px solid ${subtleBorder()};padding:3px 0;align-items:center;font-size:12px;font-family:monospace;"
503
+ >
504
+ <div class="in fll" style="flex:1;">${r.itemId} ×${r.quantity}</div>
505
+ ${await BtnIcon.instance({
506
+ class: `btn-aeq-rmreward-${i}`,
507
+ label: html`<i class="fa-solid fa-xmark"></i>`,
508
+ style: 'min-width:30px;padding:2px 8px;',
509
+ })}
510
+ </div>`;
511
+ }
512
+ if (!out) out = '<div style="color:#888;font-size:12px;">No rewards.</div>';
513
+ htmls('.action-engine-reward-list', out);
514
+ for (let i = 0; i < ActionEngineCyberia.questRewards.length; i++) {
515
+ if (s(`.btn-aeq-rmreward-${i}`))
516
+ s(`.btn-aeq-rmreward-${i}`).onclick = () => {
517
+ ActionEngineCyberia.questRewards.splice(i, 1);
518
+ ActionEngineCyberia.renderRewardList();
519
+ };
520
+ }
521
+ }
522
+
523
+ static addReward() {
524
+ const itemId = ActionEngineCyberia.getSingleDropdownValue(ActionEngineCyberia.ids.rewardItemPicker);
525
+ const quantity = Math.max(1, parseInt(s('.action-engine-reward-qty')?.value) || 1);
526
+ if (!itemId) {
527
+ NotificationManager.Push({ html: 'Select an itemId for the reward.', status: 'error' });
528
+ return;
529
+ }
530
+ ActionEngineCyberia.questRewards.push({ itemId, quantity });
531
+ ActionEngineCyberia.renderRewardList();
532
+ }
533
+
534
+ static async renderQuestDialogueList() {
535
+ const container = s('.action-engine-quest-dialogue-list');
536
+ if (!container) return;
537
+ let out = '';
538
+ for (let i = 0; i < ActionEngineCyberia.actionQuestDialogues.length; i++) {
539
+ const d = ActionEngineCyberia.actionQuestDialogues[i];
540
+ out += html`<div
541
+ class="fl"
542
+ style="border-bottom:1px solid ${subtleBorder()};padding:3px 0;align-items:center;font-size:12px;font-family:monospace;"
543
+ >
544
+ <div class="in fll" style="flex:1;">
545
+ <span style="color:${darkTheme ? '#8cf' : '#246'};">${d.questCode}</span>
546
+ <span style="margin:0 4px;">&rarr;</span>${d.dialogCode}
547
+ </div>
548
+ ${await BtnIcon.instance({
549
+ class: `btn-aea-rmqd-${i}`,
550
+ label: html`<i class="fa-solid fa-xmark"></i>`,
551
+ style: 'min-width:30px;padding:2px 8px;',
552
+ })}
553
+ </div>`;
554
+ }
555
+ if (!out) out = '<div style="color:#888;font-size:12px;">No quest dialogues mapped.</div>';
556
+ htmls('.action-engine-quest-dialogue-list', out);
557
+ for (let i = 0; i < ActionEngineCyberia.actionQuestDialogues.length; i++) {
558
+ if (s(`.btn-aea-rmqd-${i}`))
559
+ s(`.btn-aea-rmqd-${i}`).onclick = () => {
560
+ ActionEngineCyberia.actionQuestDialogues.splice(i, 1);
561
+ ActionEngineCyberia.renderQuestDialogueList();
562
+ };
563
+ }
564
+ }
565
+
566
+ // ── Card lists (CRUD surface, mirrors the map-engine entity list) ────────
567
+ static cardActionsHtml(loadCls, delCls) {
568
+ return Promise.all([
569
+ BtnIcon.instance({
570
+ class: loadCls,
571
+ label: html`<i class="fa-solid fa-upload"></i>`,
572
+ style: 'min-width:34px;padding:3px 9px;',
573
+ }),
574
+ ActionEngineCyberia.canMutate
575
+ ? BtnIcon.instance({
576
+ class: delCls,
577
+ label: html`<i class="fa-solid fa-trash"></i>`,
578
+ style: 'min-width:34px;padding:3px 9px;',
579
+ })
580
+ : '',
581
+ ]);
582
+ }
583
+
584
+ static cardWrap(inner) {
585
+ return html`<div
586
+ class="in"
587
+ style="border:1px solid ${subtleBorder()};border-radius:6px;padding:8px;margin-bottom:6px;"
588
+ >
589
+ ${inner}
590
+ </div>`;
591
+ }
592
+
593
+ static async renderQuestCards() {
594
+ const container = s('.action-engine-quest-cards');
595
+ if (!container) return;
596
+ const filter = (s('.action-engine-quest-search')?.value || '').toLowerCase();
597
+ const items = ActionEngineCyberia.questListCache.filter(
598
+ (d) => !filter || (d.code || '').toLowerCase().includes(filter) || (d.title || '').toLowerCase().includes(filter),
599
+ );
600
+ let out = '';
601
+ for (const doc of items) {
602
+ const stepCount = (doc.steps || []).length;
603
+ const objCount = (doc.steps || []).reduce((a, st) => a + (st.objectives || []).length, 0);
604
+ const [loadBtn, delBtn] = await ActionEngineCyberia.cardActionsHtml(
605
+ `btn-aeq-load-${doc._id}`,
606
+ `btn-aeq-del-${doc._id}`,
607
+ );
608
+ out += ActionEngineCyberia.cardWrap(
609
+ html`<div class="fl" style="align-items:center;">
610
+ <div class="in fll" style="flex:1;">
611
+ <div style="font-weight:bold;font-family:monospace;font-size:12px;">${doc.code}</div>
612
+ <div style="font-size:11px;color:#888;">${doc.title || ''}</div>
613
+ <div style="font-size:11px;color:#888;">
614
+ ${doc.sourceMapCode ? `${doc.sourceMapCode} (${doc.sourceCellX}, ${doc.sourceCellY})` : 'unplaced'} ·
615
+ ${stepCount} step(s) · ${objCount} objective(s)
616
+ </div>
617
+ </div>
618
+ <div class="in fll" style="display:flex;gap:4px;">${loadBtn}${delBtn}</div>
619
+ </div>`,
620
+ );
621
+ }
622
+ if (!out) out = '<div style="color:#888;font-size:12px;">No quests found.</div>';
623
+ htmls('.action-engine-quest-cards', out);
624
+ for (const doc of items) {
625
+ if (s(`.btn-aeq-load-${doc._id}`))
626
+ s(`.btn-aeq-load-${doc._id}`).onclick = async () => {
627
+ const res = await CyberiaQuestService.get({ id: doc._id });
628
+ if (res.status === 'success' && res.data) ActionEngineCyberia.loadQuest(res.data);
629
+ };
630
+ if (s(`.btn-aeq-del-${doc._id}`))
631
+ s(`.btn-aeq-del-${doc._id}`).onclick = () => ActionEngineCyberia.deleteQuest(doc._id);
632
+ }
633
+ }
634
+
635
+ static async renderActionCards() {
636
+ const container = s('.action-engine-action-cards');
637
+ if (!container) return;
638
+ const filter = (s('.action-engine-action-search')?.value || '').toLowerCase();
639
+ const items = ActionEngineCyberia.actionListCache.filter(
640
+ (d) => !filter || (d.code || '').toLowerCase().includes(filter) || (d.label || '').toLowerCase().includes(filter),
641
+ );
642
+ let out = '';
643
+ for (const doc of items) {
644
+ const [loadBtn, delBtn] = await ActionEngineCyberia.cardActionsHtml(
645
+ `btn-aea-load-${doc._id}`,
646
+ `btn-aea-del-${doc._id}`,
647
+ );
648
+ out += ActionEngineCyberia.cardWrap(
649
+ html`<div class="fl" style="align-items:center;">
650
+ <div class="in fll" style="flex:1;">
651
+ <div style="font-weight:bold;font-family:monospace;font-size:12px;">${doc.code}</div>
652
+ <div style="font-size:11px;color:#888;">${doc.label || ''}</div>
653
+ <div style="font-size:11px;color:#888;">
654
+ ${doc.sourceMapCode ? `${doc.sourceMapCode} (${doc.sourceCellX}, ${doc.sourceCellY})` : 'unplaced'} ·
655
+ dialog: ${doc.dialogCode || '—'} · ${(doc.questDialogueCodes || []).length} quest-dialog(s)
656
+ </div>
657
+ </div>
658
+ <div class="in fll" style="display:flex;gap:4px;">${loadBtn}${delBtn}</div>
659
+ </div>`,
660
+ );
661
+ }
662
+ if (!out) out = '<div style="color:#888;font-size:12px;">No actions found.</div>';
663
+ htmls('.action-engine-action-cards', out);
664
+ for (const doc of items) {
665
+ if (s(`.btn-aea-load-${doc._id}`))
666
+ s(`.btn-aea-load-${doc._id}`).onclick = async () => {
667
+ const res = await CyberiaActionService.get({ id: doc._id });
668
+ if (res.status === 'success' && res.data) ActionEngineCyberia.loadAction(res.data);
669
+ };
670
+ if (s(`.btn-aea-del-${doc._id}`))
671
+ s(`.btn-aea-del-${doc._id}`).onclick = () => ActionEngineCyberia.deleteAction(doc._id);
672
+ }
673
+ }
674
+
675
+ static async renderDialogueCards() {
676
+ const container = s('.action-engine-dialogue-cards');
677
+ if (!container) return;
678
+ const filter = (s('.action-engine-dialogue-search')?.value || '').toLowerCase();
679
+ const items = ActionEngineCyberia.dialogueListCache.filter(
680
+ (d) =>
681
+ !filter ||
682
+ (d.code || '').toLowerCase().includes(filter) ||
683
+ (d.speaker || '').toLowerCase().includes(filter) ||
684
+ (d.text || '').toLowerCase().includes(filter),
685
+ );
686
+ let out = '';
687
+ for (const doc of items) {
688
+ const [loadBtn, delBtn] = await ActionEngineCyberia.cardActionsHtml(
689
+ `btn-aed-load-${doc._id}`,
690
+ `btn-aed-del-${doc._id}`,
691
+ );
692
+ const snippet = (doc.text || '').length > 60 ? `${doc.text.slice(0, 60)}…` : doc.text || '';
693
+ out += ActionEngineCyberia.cardWrap(
694
+ html`<div class="fl" style="align-items:center;">
695
+ <div class="in fll" style="flex:1;">
696
+ <div style="font-weight:bold;font-family:monospace;font-size:12px;">${doc.code} #${doc.order ?? 0}</div>
697
+ <div style="font-size:11px;color:#888;">${doc.speaker || '—'} · ${doc.mood || 'neutral'}</div>
698
+ <div style="font-size:11px;color:#888;">${snippet}</div>
699
+ </div>
700
+ <div class="in fll" style="display:flex;gap:4px;">${loadBtn}${delBtn}</div>
701
+ </div>`,
702
+ );
703
+ }
704
+ if (!out) out = '<div style="color:#888;font-size:12px;">No dialogue lines found.</div>';
705
+ htmls('.action-engine-dialogue-cards', out);
706
+ for (const doc of items) {
707
+ if (s(`.btn-aed-load-${doc._id}`))
708
+ s(`.btn-aed-load-${doc._id}`).onclick = async () => {
709
+ const res = await CyberiaDialogueService.get({ id: doc._id });
710
+ if (res.status === 'success' && res.data) ActionEngineCyberia.loadDialogue(res.data);
711
+ };
712
+ if (s(`.btn-aed-del-${doc._id}`))
713
+ s(`.btn-aed-del-${doc._id}`).onclick = () => ActionEngineCyberia.deleteDialogue(doc._id);
714
+ }
715
+ }
716
+
717
+ static async refreshQuestList() {
718
+ const res = await CyberiaQuestService.get({ limit: 500 });
719
+ ActionEngineCyberia.questListCache = res?.data?.data || [];
720
+ await ActionEngineCyberia.renderQuestCards();
721
+ }
722
+
723
+ static async refreshActionList() {
724
+ const res = await CyberiaActionService.get({ limit: 500 });
725
+ ActionEngineCyberia.actionListCache = res?.data?.data || [];
726
+ await ActionEngineCyberia.renderActionCards();
727
+ }
728
+
729
+ static async refreshDialogueList() {
730
+ const res = await CyberiaDialogueService.get({ limit: 500 });
731
+ ActionEngineCyberia.dialogueListCache = res?.data?.data || [];
732
+ await ActionEngineCyberia.renderDialogueCards();
733
+ }
734
+
735
+ static notifyResult(result, isUpdate) {
736
+ NotificationManager.Push({
737
+ html:
738
+ result.status === 'error'
739
+ ? result.message
740
+ : isUpdate
741
+ ? Translate.instance('success-update-item')
742
+ : Translate.instance('success-create-item'),
743
+ status: result.status,
744
+ });
745
+ }
746
+
747
+ // ── Quest payload / load / persistence ──────────────────────────────────
748
+ static getQuestPayload() {
749
+ ActionEngineCyberia.syncStepDescriptions();
750
+ const csv = (v) =>
751
+ (v || '')
752
+ .split(',')
753
+ .map((t) => t.trim())
754
+ .filter(Boolean);
755
+ const payload = {
756
+ code: s('.action-engine-quest-code')?.value?.trim() || '',
757
+ title: s('.action-engine-quest-title')?.value?.trim() || '',
758
+ description: s('.action-engine-quest-description')?.value || '',
759
+ prerequisiteCodes: csv(s('.action-engine-quest-prerequisites')?.value),
760
+ unlocksQuestCodes: csv(s('.action-engine-quest-unlocks')?.value),
761
+ steps: ActionEngineCyberia.questSteps.map((st, i) => ({
762
+ id: st.id || `step-${i + 1}`,
763
+ description: st.description || '',
764
+ objectives: st.objectives.map((o) => ({ ...o })),
765
+ })),
766
+ rewards: ActionEngineCyberia.questRewards.map((r) => ({ ...r })),
767
+ };
768
+ const mapCode = s('.action-engine-quest-map-code')?.value?.trim();
769
+ const cellX = parseInt(s('.action-engine-cell-x')?.value);
770
+ const cellY = parseInt(s('.action-engine-cell-y')?.value);
771
+ if (mapCode) payload.sourceMapCode = mapCode;
772
+ if (Number.isFinite(cellX)) payload.sourceCellX = cellX;
773
+ if (Number.isFinite(cellY)) payload.sourceCellY = cellY;
774
+ return payload;
775
+ }
776
+
777
+ static async loadQuest(doc) {
778
+ ActionEngineCyberia.setMode('quest');
779
+ ActionEngineCyberia.currentQuestId = doc._id || null;
780
+ if (s('.action-engine-quest-code')) s('.action-engine-quest-code').value = doc.code || '';
781
+ if (s('.action-engine-quest-title')) s('.action-engine-quest-title').value = doc.title || '';
782
+ if (s('.action-engine-quest-description')) s('.action-engine-quest-description').value = doc.description || '';
783
+ if (s('.action-engine-quest-prerequisites'))
784
+ s('.action-engine-quest-prerequisites').value = (doc.prerequisiteCodes || []).join(', ');
785
+ if (s('.action-engine-quest-unlocks'))
786
+ s('.action-engine-quest-unlocks').value = (doc.unlocksQuestCodes || []).join(', ');
787
+ if (s('.action-engine-quest-map-code')) s('.action-engine-quest-map-code').value = doc.sourceMapCode || '';
788
+ if (s('.action-engine-cell-x')) s('.action-engine-cell-x').value = doc.sourceCellX ?? '';
789
+ if (s('.action-engine-cell-y')) s('.action-engine-cell-y').value = doc.sourceCellY ?? '';
790
+ ActionEngineCyberia.questSteps = (doc.steps || []).map((st, i) => ({
791
+ id: st.id || `step-${i + 1}`,
792
+ description: st.description || '',
793
+ objectives: (st.objectives || []).map((o) => ({ type: o.type, itemId: o.itemId, quantity: o.quantity || 1 })),
794
+ }));
795
+ ActionEngineCyberia.questRewards = (doc.rewards || []).map((r) => ({
796
+ itemId: r.itemId,
797
+ quantity: r.quantity || 1,
798
+ }));
799
+ ActionEngineCyberia.renderStepList();
800
+ ActionEngineCyberia.renderRewardList();
801
+ await ActionEngineCyberia.syncMapForLoadedDoc(doc);
802
+ NotificationManager.Push({ html: `Quest "${doc.code}" loaded`, status: 'success' });
803
+ }
804
+
805
+ static resetQuest() {
806
+ ActionEngineCyberia.currentQuestId = null;
807
+ ActionEngineCyberia.questSteps = [];
808
+ ActionEngineCyberia.questRewards = [];
809
+ for (const cls of [
810
+ 'action-engine-quest-code',
811
+ 'action-engine-quest-title',
812
+ 'action-engine-quest-description',
813
+ 'action-engine-quest-prerequisites',
814
+ 'action-engine-quest-unlocks',
815
+ ])
816
+ if (s(`.${cls}`)) s(`.${cls}`).value = '';
817
+ ActionEngineCyberia.renderStepList();
818
+ ActionEngineCyberia.renderRewardList();
819
+ }
820
+
821
+ static async saveQuest() {
822
+ const body = ActionEngineCyberia.getQuestPayload();
823
+ if (!body.code || !body.title) {
824
+ NotificationManager.Push({ html: 'Quest code and title are required.', status: 'error' });
825
+ return;
826
+ }
827
+ const isUpdate = !!ActionEngineCyberia.currentQuestId;
828
+ const result = isUpdate
829
+ ? await CyberiaQuestService.put({ id: ActionEngineCyberia.currentQuestId, body })
830
+ : await CyberiaQuestService.post({ body });
831
+ ActionEngineCyberia.notifyResult(result, isUpdate);
832
+ if (result.status === 'success') {
833
+ if (result.data?._id) ActionEngineCyberia.currentQuestId = result.data._id;
834
+ await ActionEngineCyberia.refreshQuestList();
835
+ await ActionEngineCyberia.refreshAssignments();
836
+ }
837
+ }
838
+
839
+ static async cloneQuest() {
840
+ if (!ActionEngineCyberia.currentQuestId) {
841
+ NotificationManager.Push({ html: 'Load a quest to clone first.', status: 'warning' });
842
+ return;
843
+ }
844
+ const body = ActionEngineCyberia.getQuestPayload();
845
+ body.code = `${body.code}-clone`;
846
+ const result = await CyberiaQuestService.post({ body });
847
+ ActionEngineCyberia.notifyResult(result, false);
848
+ if (result.status === 'success') {
849
+ if (result.data?._id) ActionEngineCyberia.currentQuestId = result.data._id;
850
+ if (s('.action-engine-quest-code')) s('.action-engine-quest-code').value = body.code;
851
+ await ActionEngineCyberia.refreshQuestList();
852
+ await ActionEngineCyberia.refreshAssignments();
853
+ }
854
+ }
855
+
856
+ static async deleteQuest(id) {
857
+ const targetId = id || ActionEngineCyberia.currentQuestId;
858
+ if (!targetId) return;
859
+ const result = await CyberiaQuestService.delete({ id: targetId });
860
+ NotificationManager.Push({
861
+ html: result.status === 'error' ? result.message : Translate.instance('item-success-delete'),
862
+ status: result.status,
863
+ });
864
+ if (result.status === 'success') {
865
+ if (targetId === ActionEngineCyberia.currentQuestId) ActionEngineCyberia.resetQuest();
866
+ await ActionEngineCyberia.refreshQuestList();
867
+ await ActionEngineCyberia.refreshAssignments();
868
+ }
869
+ }
870
+
871
+ // ── Action payload / load / persistence ─────────────────────────────────
872
+ static getActionPayload() {
873
+ const payload = {
874
+ ...ActionEngineCyberia.loadedActionPayloadExtras,
875
+ code: s('.action-engine-action-code')?.value?.trim() || '',
876
+ label: s('.action-engine-action-label')?.value?.trim() || '',
877
+ dialogCode: ActionEngineCyberia.getSingleDropdownValue(ActionEngineCyberia.ids.actionDialogPicker),
878
+ questDialogueCodes: ActionEngineCyberia.actionQuestDialogues.map((d) => ({ ...d })),
879
+ };
880
+ const mapCode = s('.action-engine-action-map-code')?.value?.trim();
881
+ const cellX = parseInt(s('.action-engine-cell-x')?.value);
882
+ const cellY = parseInt(s('.action-engine-cell-y')?.value);
883
+ if (mapCode) payload.sourceMapCode = mapCode;
884
+ if (Number.isFinite(cellX)) payload.sourceCellX = cellX;
885
+ if (Number.isFinite(cellY)) payload.sourceCellY = cellY;
886
+ return payload;
887
+ }
888
+
889
+ static async loadAction(doc) {
890
+ ActionEngineCyberia.setMode('action');
891
+ ActionEngineCyberia.currentActionId = doc._id || null;
892
+ // Out-of-scope payloads (shop/craft/storage) are preserved verbatim so
893
+ // editing an action here never drops data the engine doesn't manage yet.
894
+ const {
895
+ _id,
896
+ code,
897
+ label,
898
+ dialogCode,
899
+ questDialogueCodes,
900
+ sourceMapCode,
901
+ sourceCellX,
902
+ sourceCellY,
903
+ createdAt,
904
+ updatedAt,
905
+ __v,
906
+ ...extras
907
+ } = doc;
908
+ ActionEngineCyberia.loadedActionPayloadExtras = extras;
909
+ if (s('.action-engine-action-code')) s('.action-engine-action-code').value = doc.code || '';
910
+ if (s('.action-engine-action-label')) s('.action-engine-action-label').value = doc.label || '';
911
+ if (s('.action-engine-action-map-code')) s('.action-engine-action-map-code').value = doc.sourceMapCode || '';
912
+ if (s('.action-engine-cell-x')) s('.action-engine-cell-x').value = doc.sourceCellX ?? '';
913
+ if (s('.action-engine-cell-y')) s('.action-engine-cell-y').value = doc.sourceCellY ?? '';
914
+ ActionEngineCyberia.setSingleDropdownValue(ActionEngineCyberia.ids.actionDialogPicker, doc.dialogCode || '');
915
+ ActionEngineCyberia.actionQuestDialogues = (doc.questDialogueCodes || []).map((d) => ({
916
+ questCode: d.questCode,
917
+ dialogCode: d.dialogCode,
918
+ }));
919
+ ActionEngineCyberia.renderQuestDialogueList();
920
+ await ActionEngineCyberia.syncMapForLoadedDoc(doc);
921
+ NotificationManager.Push({ html: `Action "${doc.code}" loaded`, status: 'success' });
922
+ }
923
+
924
+ static resetAction() {
925
+ ActionEngineCyberia.currentActionId = null;
926
+ ActionEngineCyberia.actionQuestDialogues = [];
927
+ ActionEngineCyberia.loadedActionPayloadExtras = {};
928
+ for (const cls of ['action-engine-action-code', 'action-engine-action-label'])
929
+ if (s(`.${cls}`)) s(`.${cls}`).value = '';
930
+ ActionEngineCyberia.setSingleDropdownValue(ActionEngineCyberia.ids.actionDialogPicker, '');
931
+ ActionEngineCyberia.renderQuestDialogueList();
932
+ }
933
+
934
+ static async saveAction() {
935
+ const body = ActionEngineCyberia.getActionPayload();
936
+ if (!body.code) {
937
+ NotificationManager.Push({ html: 'Action code is required.', status: 'error' });
938
+ return;
939
+ }
940
+ const isUpdate = !!ActionEngineCyberia.currentActionId;
941
+ const result = isUpdate
942
+ ? await CyberiaActionService.put({ id: ActionEngineCyberia.currentActionId, body })
943
+ : await CyberiaActionService.post({ body });
944
+ ActionEngineCyberia.notifyResult(result, isUpdate);
945
+ if (result.status === 'success') {
946
+ if (result.data?._id) ActionEngineCyberia.currentActionId = result.data._id;
947
+ await ActionEngineCyberia.refreshActionList();
948
+ await ActionEngineCyberia.refreshAssignments();
949
+ }
950
+ }
951
+
952
+ static async cloneAction() {
953
+ if (!ActionEngineCyberia.currentActionId) {
954
+ NotificationManager.Push({ html: 'Load an action to clone first.', status: 'warning' });
955
+ return;
956
+ }
957
+ const body = ActionEngineCyberia.getActionPayload();
958
+ body.code = `${body.code}-clone`;
959
+ const result = await CyberiaActionService.post({ body });
960
+ ActionEngineCyberia.notifyResult(result, false);
961
+ if (result.status === 'success') {
962
+ if (result.data?._id) ActionEngineCyberia.currentActionId = result.data._id;
963
+ if (s('.action-engine-action-code')) s('.action-engine-action-code').value = body.code;
964
+ await ActionEngineCyberia.refreshActionList();
965
+ await ActionEngineCyberia.refreshAssignments();
966
+ }
967
+ }
968
+
969
+ static async deleteAction(id) {
970
+ const targetId = id || ActionEngineCyberia.currentActionId;
971
+ if (!targetId) return;
972
+ const result = await CyberiaActionService.delete({ id: targetId });
973
+ NotificationManager.Push({
974
+ html: result.status === 'error' ? result.message : Translate.instance('item-success-delete'),
975
+ status: result.status,
976
+ });
977
+ if (result.status === 'success') {
978
+ if (targetId === ActionEngineCyberia.currentActionId) ActionEngineCyberia.resetAction();
979
+ await ActionEngineCyberia.refreshActionList();
980
+ await ActionEngineCyberia.refreshAssignments();
981
+ }
982
+ }
983
+
984
+ // ── Dialogue line persistence ───────────────────────────────────────────
985
+ static getDialoguePayload() {
986
+ return {
987
+ code: s('.action-engine-dialogue-code')?.value?.trim() || '',
988
+ order: parseInt(s('.action-engine-dialogue-order')?.value) || 0,
989
+ speaker: s('.action-engine-dialogue-speaker')?.value || '',
990
+ text: s('.action-engine-dialogue-text')?.value || '',
991
+ mood: s('.action-engine-dialogue-mood')?.value?.trim() || 'neutral',
992
+ };
993
+ }
994
+
995
+ static loadDialogue(doc) {
996
+ ActionEngineCyberia.setMode('dialogue');
997
+ ActionEngineCyberia.currentDialogueId = doc._id || null;
998
+ if (s('.action-engine-dialogue-code')) s('.action-engine-dialogue-code').value = doc.code || '';
999
+ if (s('.action-engine-dialogue-order')) s('.action-engine-dialogue-order').value = doc.order ?? 0;
1000
+ if (s('.action-engine-dialogue-speaker')) s('.action-engine-dialogue-speaker').value = doc.speaker || '';
1001
+ if (s('.action-engine-dialogue-text')) s('.action-engine-dialogue-text').value = doc.text || '';
1002
+ if (s('.action-engine-dialogue-mood')) s('.action-engine-dialogue-mood').value = doc.mood || 'neutral';
1003
+ NotificationManager.Push({ html: `Dialogue line "${doc.code}" loaded`, status: 'success' });
1004
+ }
1005
+
1006
+ static resetDialogue() {
1007
+ ActionEngineCyberia.currentDialogueId = null;
1008
+ if (s('.action-engine-dialogue-code')) s('.action-engine-dialogue-code').value = '';
1009
+ if (s('.action-engine-dialogue-order')) s('.action-engine-dialogue-order').value = 0;
1010
+ if (s('.action-engine-dialogue-speaker')) s('.action-engine-dialogue-speaker').value = '';
1011
+ if (s('.action-engine-dialogue-text')) s('.action-engine-dialogue-text').value = '';
1012
+ if (s('.action-engine-dialogue-mood')) s('.action-engine-dialogue-mood').value = 'neutral';
1013
+ }
1014
+
1015
+ static async saveDialogue() {
1016
+ const body = ActionEngineCyberia.getDialoguePayload();
1017
+ if (!body.code || !body.text) {
1018
+ NotificationManager.Push({ html: 'Dialogue code and text are required.', status: 'error' });
1019
+ return;
1020
+ }
1021
+ const isUpdate = !!ActionEngineCyberia.currentDialogueId;
1022
+ const result = isUpdate
1023
+ ? await CyberiaDialogueService.put({ id: ActionEngineCyberia.currentDialogueId, body })
1024
+ : await CyberiaDialogueService.post({ body });
1025
+ ActionEngineCyberia.notifyResult(result, isUpdate);
1026
+ if (result.status === 'success') {
1027
+ if (result.data?._id) ActionEngineCyberia.currentDialogueId = result.data._id;
1028
+ await ActionEngineCyberia.refreshDialogueList();
1029
+ }
1030
+ }
1031
+
1032
+ static async deleteDialogue(id) {
1033
+ const targetId = id || ActionEngineCyberia.currentDialogueId;
1034
+ if (!targetId) return;
1035
+ const result = await CyberiaDialogueService.delete({ id: targetId });
1036
+ NotificationManager.Push({
1037
+ html: result.status === 'error' ? result.message : Translate.instance('item-success-delete'),
1038
+ status: result.status,
1039
+ });
1040
+ if (result.status === 'success') {
1041
+ if (targetId === ActionEngineCyberia.currentDialogueId) ActionEngineCyberia.resetDialogue();
1042
+ await ActionEngineCyberia.refreshDialogueList();
1043
+ }
1044
+ }
1045
+
1046
+ // ── Skill list editor + persistence (own model: CyberiaSkill) ────────────
1047
+ static async renderSkillDefList() {
1048
+ const container = s('.action-engine-skill-def-list');
1049
+ if (!container) return;
1050
+ let out = '';
1051
+ for (let i = 0; i < ActionEngineCyberia.skillDefs.length; i++) {
1052
+ const sk = ActionEngineCyberia.skillDefs[i];
1053
+ out += html`<div
1054
+ class="fl"
1055
+ style="border-bottom:1px solid ${subtleBorder()};padding:3px 0;align-items:center;font-size:12px;font-family:monospace;"
1056
+ >
1057
+ <div class="in fll" style="flex:1;">
1058
+ <span style="color:${darkTheme ? '#8cf' : '#246'};">${sk.logicEventId}</span>${sk.name
1059
+ ? ` · ${sk.name}`
1060
+ : ''}${sk.summonedEntityItemId
1061
+ ? html`<span style="margin:0 4px;">&rarr;</span>${sk.summonedEntityItemId}`
1062
+ : ''}
1063
+ </div>
1064
+ ${await BtnIcon.instance({
1065
+ class: `btn-aes-rmskill-${i}`,
1066
+ label: html`<i class="fa-solid fa-xmark"></i>`,
1067
+ style: 'min-width:30px;padding:2px 8px;',
1068
+ })}
1069
+ </div>`;
1070
+ }
1071
+ if (!out) out = '<div style="color:#888;font-size:12px;">No skills. Add at least one logic event.</div>';
1072
+ htmls('.action-engine-skill-def-list', out);
1073
+ for (let i = 0; i < ActionEngineCyberia.skillDefs.length; i++) {
1074
+ if (s(`.btn-aes-rmskill-${i}`))
1075
+ s(`.btn-aes-rmskill-${i}`).onclick = () => {
1076
+ ActionEngineCyberia.skillDefs.splice(i, 1);
1077
+ ActionEngineCyberia.renderSkillDefList();
1078
+ };
1079
+ }
1080
+ }
1081
+
1082
+ static addSkillDef() {
1083
+ const logicEventId = ActionEngineCyberia.getSingleDropdownValue('action-engine-skill-logic-event');
1084
+ const name = s('.action-engine-skill-name')?.value?.trim() || '';
1085
+ const description = s('.action-engine-skill-description')?.value || '';
1086
+ const summonedEntityItemId = ActionEngineCyberia.getSingleDropdownValue(
1087
+ ActionEngineCyberia.ids.skillSummonedItemPicker,
1088
+ );
1089
+ if (!logicEventId) {
1090
+ NotificationManager.Push({ html: 'Select a canonical logic event id first.', status: 'error' });
1091
+ return;
1092
+ }
1093
+ if (ActionEngineCyberia.skillDefs.some((sk) => sk.logicEventId === logicEventId)) {
1094
+ NotificationManager.Push({ html: `Logic event "${logicEventId}" already added.`, status: 'error' });
1095
+ return;
1096
+ }
1097
+ ActionEngineCyberia.skillDefs.push({ logicEventId, name, description, summonedEntityItemId });
1098
+ ActionEngineCyberia.setSingleDropdownValue('action-engine-skill-logic-event', '');
1099
+ for (const cls of ['action-engine-skill-name', 'action-engine-skill-description'])
1100
+ if (s(`.${cls}`)) s(`.${cls}`).value = '';
1101
+ ActionEngineCyberia.setSingleDropdownValue(ActionEngineCyberia.ids.skillSummonedItemPicker, '');
1102
+ ActionEngineCyberia.renderSkillDefList();
1103
+ }
1104
+
1105
+ static async renderSkillCards() {
1106
+ const container = s('.action-engine-skill-cards');
1107
+ if (!container) return;
1108
+ const filter = (s('.action-engine-skill-search')?.value || '').toLowerCase();
1109
+ const items = ActionEngineCyberia.skillListCache.filter(
1110
+ (d) =>
1111
+ !filter ||
1112
+ (d.triggerItemId || '').toLowerCase().includes(filter) ||
1113
+ (d.logicEventIds || []).some((l) => (l || '').toLowerCase().includes(filter)),
1114
+ );
1115
+ let out = '';
1116
+ for (const doc of items) {
1117
+ const [loadBtn, delBtn] = await ActionEngineCyberia.cardActionsHtml(
1118
+ `btn-aes-load-${doc._id}`,
1119
+ `btn-aes-del-${doc._id}`,
1120
+ );
1121
+ out += ActionEngineCyberia.cardWrap(
1122
+ html`<div class="fl" style="align-items:center;">
1123
+ <div class="in fll" style="flex:1;">
1124
+ <div style="font-weight:bold;font-family:monospace;font-size:12px;">${doc.triggerItemId}</div>
1125
+ <div style="font-size:11px;color:#888;">${(doc.logicEventIds || []).join(', ') || '—'}</div>
1126
+ <div style="font-size:11px;color:#888;">${(doc.skills || []).length} skill(s)</div>
1127
+ </div>
1128
+ <div class="in fll" style="display:flex;gap:4px;">${loadBtn}${delBtn}</div>
1129
+ </div>`,
1130
+ );
1131
+ }
1132
+ if (!out) out = '<div style="color:#888;font-size:12px;">No skills found.</div>';
1133
+ htmls('.action-engine-skill-cards', out);
1134
+ for (const doc of items) {
1135
+ if (s(`.btn-aes-load-${doc._id}`))
1136
+ s(`.btn-aes-load-${doc._id}`).onclick = async () => {
1137
+ const res = await CyberiaSkillService.get({ id: doc._id });
1138
+ if (res.status === 'success' && res.data) ActionEngineCyberia.loadSkill(res.data);
1139
+ };
1140
+ if (s(`.btn-aes-del-${doc._id}`))
1141
+ s(`.btn-aes-del-${doc._id}`).onclick = () => ActionEngineCyberia.deleteSkill(doc._id);
1142
+ }
1143
+ }
1144
+
1145
+ static async refreshSkillList() {
1146
+ const res = await CyberiaSkillService.get({ limit: 500 });
1147
+ ActionEngineCyberia.skillListCache = res?.data?.data || [];
1148
+ await ActionEngineCyberia.renderSkillCards();
1149
+ }
1150
+
1151
+ static getSkillPayload() {
1152
+ const triggerItemId = ActionEngineCyberia.getSingleDropdownValue(ActionEngineCyberia.ids.skillTriggerItemPicker);
1153
+ const skills = ActionEngineCyberia.skillDefs.map((sk) => ({
1154
+ logicEventId: sk.logicEventId,
1155
+ name: sk.name || '',
1156
+ description: sk.description || '',
1157
+ summonedEntityItemId: sk.summonedEntityItemId || '',
1158
+ }));
1159
+ // logicEventIds is the compact discriminator list — derived from the skills
1160
+ // so the two can never drift (matches DefaultSkillConfig).
1161
+ const logicEventIds = [...new Set(skills.map((sk) => sk.logicEventId).filter(Boolean))];
1162
+ return { triggerItemId, logicEventIds, skills };
1163
+ }
1164
+
1165
+ static loadSkill(doc) {
1166
+ ActionEngineCyberia.setMode('skill');
1167
+ ActionEngineCyberia.currentSkillId = doc._id || null;
1168
+ ActionEngineCyberia.setSingleDropdownValue(ActionEngineCyberia.ids.skillTriggerItemPicker, doc.triggerItemId || '');
1169
+ ActionEngineCyberia.skillDefs = (doc.skills || []).map((sk) => ({
1170
+ logicEventId: sk.logicEventId,
1171
+ name: sk.name || '',
1172
+ description: sk.description || '',
1173
+ summonedEntityItemId: sk.summonedEntityItemId || '',
1174
+ }));
1175
+ ActionEngineCyberia.renderSkillDefList();
1176
+ NotificationManager.Push({ html: `Skill "${doc.triggerItemId}" loaded`, status: 'success' });
1177
+ }
1178
+
1179
+ static resetSkill() {
1180
+ ActionEngineCyberia.currentSkillId = null;
1181
+ ActionEngineCyberia.skillDefs = [];
1182
+ ActionEngineCyberia.setSingleDropdownValue(ActionEngineCyberia.ids.skillTriggerItemPicker, '');
1183
+ ActionEngineCyberia.setSingleDropdownValue(ActionEngineCyberia.ids.skillSummonedItemPicker, '');
1184
+ ActionEngineCyberia.setSingleDropdownValue('action-engine-skill-logic-event', '');
1185
+ for (const cls of ['action-engine-skill-name', 'action-engine-skill-description'])
1186
+ if (s(`.${cls}`)) s(`.${cls}`).value = '';
1187
+ ActionEngineCyberia.renderSkillDefList();
1188
+ }
1189
+
1190
+ static async saveSkill() {
1191
+ const body = ActionEngineCyberia.getSkillPayload();
1192
+ if (!body.triggerItemId) {
1193
+ NotificationManager.Push({ html: 'Trigger item id is required.', status: 'error' });
1194
+ return;
1195
+ }
1196
+ if (body.skills.length === 0) {
1197
+ NotificationManager.Push({ html: 'Add at least one skill (logic event).', status: 'error' });
1198
+ return;
1199
+ }
1200
+ const isUpdate = !!ActionEngineCyberia.currentSkillId;
1201
+ const result = isUpdate
1202
+ ? await CyberiaSkillService.put({ id: ActionEngineCyberia.currentSkillId, body })
1203
+ : await CyberiaSkillService.post({ body });
1204
+ ActionEngineCyberia.notifyResult(result, isUpdate);
1205
+ if (result.status === 'success') {
1206
+ if (result.data?._id) ActionEngineCyberia.currentSkillId = result.data._id;
1207
+ await ActionEngineCyberia.refreshSkillList();
1208
+ }
1209
+ }
1210
+
1211
+ static async deleteSkill(id) {
1212
+ const targetId = id || ActionEngineCyberia.currentSkillId;
1213
+ if (!targetId) return;
1214
+ const result = await CyberiaSkillService.delete({ id: targetId });
1215
+ NotificationManager.Push({
1216
+ html: result.status === 'error' ? result.message : Translate.instance('item-success-delete'),
1217
+ status: result.status,
1218
+ });
1219
+ if (result.status === 'success') {
1220
+ if (targetId === ActionEngineCyberia.currentSkillId) ActionEngineCyberia.resetSkill();
1221
+ await ActionEngineCyberia.refreshSkillList();
1222
+ }
1223
+ }
1224
+
1225
+ // ── Render ──────────────────────────────────────────────────────────────
1226
+ static async render(options = {}) {
1227
+ const { appStore } = options;
1228
+ const role = appStore?.Data?.user?.main?.model?.user?.role || 'guest';
1229
+ ActionEngineCyberia.canMutate = commonModeratorGuard(role);
1230
+ ActionEngineCyberia.mode = 'quest';
1231
+ ActionEngineCyberia.mapCode = '';
1232
+ ActionEngineCyberia.mapDoc = null;
1233
+ ActionEngineCyberia.imageCache = {};
1234
+ ActionEngineCyberia.assignments = { quests: [], actions: [] };
1235
+ ActionEngineCyberia.questListCache = [];
1236
+ ActionEngineCyberia.actionListCache = [];
1237
+ ActionEngineCyberia.dialogueListCache = [];
1238
+ ActionEngineCyberia.skillListCache = [];
1239
+ ActionEngineCyberia.currentQuestId = null;
1240
+ ActionEngineCyberia.currentActionId = null;
1241
+ ActionEngineCyberia.currentDialogueId = null;
1242
+ ActionEngineCyberia.currentSkillId = null;
1243
+ ActionEngineCyberia.questSteps = [];
1244
+ ActionEngineCyberia.questRewards = [];
1245
+ ActionEngineCyberia.actionQuestDialogues = [];
1246
+ ActionEngineCyberia.loadedActionPayloadExtras = {};
1247
+ ActionEngineCyberia.skillDefs = [];
1248
+
1249
+ const ids = ActionEngineCyberia.ids;
1250
+ const objectiveTypeOptions = QUEST_STEPS_TYPES.map((t) => ({ value: t, display: t, data: t, onClick: () => {} }));
1251
+
1252
+ // Responsive column groups: dynamicCol collapses these to full width on
1253
+ // narrow / mobile widths (observing .action-engine-container).
1254
+ const cont = 'action-engine-container';
1255
+ const dc = {
1256
+ cell: 'action-engine-dc-cell',
1257
+ questCt: 'action-engine-dc-quest-ct',
1258
+ questChain: 'action-engine-dc-quest-chain',
1259
+ objective: 'action-engine-dc-objective',
1260
+ reward: 'action-engine-dc-reward',
1261
+ actionCl: 'action-engine-dc-action-cl',
1262
+ actionQd: 'action-engine-dc-action-qd',
1263
+ dialogue: 'action-engine-dc-dialogue',
1264
+ skillDef: 'action-engine-dc-skill-def',
1265
+ };
1266
+
1267
+ const group = (title, icon, inner) =>
1268
+ html`<div class="in" style="border:1px solid ${groupBorder()};border-radius:8px;padding:12px;margin-bottom:14px;">
1269
+ <div
1270
+ class="in"
1271
+ style="font-size:12px;font-weight:bold;text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px;opacity:.85;"
1272
+ >
1273
+ <i class="${icon}"></i> ${title}
1274
+ </div>
1275
+ ${inner}
1276
+ </div>`;
1277
+
1278
+ // Buttons grow equally and wrap to their own line once they drop below the
1279
+ // flex-basis, which keeps them tappable on narrow / mobile widths.
1280
+ const crudButtons = async (prefix, includeClone) =>
1281
+ html`<div class="fl" style="margin-top:4px;flex-wrap:wrap;">
1282
+ ${ActionEngineCyberia.canMutate
1283
+ ? html`<div class="in fll" style="flex:1 1 120px;padding:3px;">
1284
+ ${await BtnIcon.instance({
1285
+ class: `wfa btn-action-engine-${prefix}-save`,
1286
+ label: html`<i class="fa-solid fa-floppy-disk"></i> Save`,
1287
+ })}
1288
+ </div>
1289
+ ${includeClone
1290
+ ? html`<div class="in fll" style="flex:1 1 120px;padding:3px;">
1291
+ ${await BtnIcon.instance({
1292
+ class: `wfa btn-action-engine-${prefix}-clone`,
1293
+ label: html`<i class="fa-solid fa-clone"></i> Clone`,
1294
+ })}
1295
+ </div>`
1296
+ : ''}
1297
+ <div class="in fll" style="flex:1 1 120px;padding:3px;">
1298
+ ${await BtnIcon.instance({
1299
+ class: `wfa btn-action-engine-${prefix}-delete`,
1300
+ label: html`<i class="fa-solid fa-trash"></i> Delete`,
1301
+ })}
1302
+ </div>`
1303
+ : ''}
1304
+ <div class="in fll" style="flex:1 1 120px;padding:3px;">
1305
+ ${await BtnIcon.instance({
1306
+ class: `wfa btn-action-engine-${prefix}-new`,
1307
+ label: html`<i class="fa-solid fa-file"></i> New`,
1308
+ })}
1309
+ </div>
1310
+ </div>`;
1311
+
1312
+ const listToolbar = async (prefix) =>
1313
+ html`<div class="fl" style="align-items:flex-end;margin-bottom:8px;flex-wrap:wrap;">
1314
+ <div class="in fll" style="flex:1 1 160px;">
1315
+ ${await Input.instance({
1316
+ id: `action-engine-${prefix}-search`,
1317
+ label: html`Search`,
1318
+ containerClass: 'inl',
1319
+ type: 'text',
1320
+ placeholder: true,
1321
+ })}
1322
+ </div>
1323
+ <div class="in fll" style="padding-left:6px;">
1324
+ ${await BtnIcon.instance({
1325
+ class: `wfa btn-action-engine-${prefix}-refresh`,
1326
+ label: html`<i class="fa-solid fa-rotate"></i> Refresh`,
1327
+ })}
1328
+ </div>
1329
+ </div>`;
1330
+
1331
+ setTimeout(async () => {
1332
+ ThemeEvents['action-engine-theme'] = () => {
1333
+ ActionEngineCyberia.renderMapCanvas();
1334
+ ActionEngineCyberia.renderAssignmentReview();
1335
+ };
1336
+
1337
+ for (const m of ['quest', 'action', 'dialogue', 'skill'])
1338
+ if (s(`.action-engine-tab-${m}`)) s(`.action-engine-tab-${m}`).onclick = () => ActionEngineCyberia.setMode(m);
1339
+ ActionEngineCyberia.setMode('quest');
1340
+
1341
+ if (s('.btn-action-engine-load-map'))
1342
+ s('.btn-action-engine-load-map').onclick = () =>
1343
+ ActionEngineCyberia.loadMap(ActionEngineCyberia.getSingleDropdownValue(ids.mapDropdown));
1344
+
1345
+ const canvas = s('.action-engine-canvas');
1346
+ if (canvas)
1347
+ canvas.onclick = (e) => {
1348
+ if (!ActionEngineCyberia.mapDoc) return;
1349
+ const rect = canvas.getBoundingClientRect();
1350
+ const { cellW, cellH } = ActionEngineCyberia.getMapGeometry();
1351
+ const col = Math.floor(((e.clientX - rect.left) * (canvas.width / rect.width)) / cellW);
1352
+ const row = Math.floor(((e.clientY - rect.top) * (canvas.height / rect.height)) / cellH);
1353
+ if (s('.action-engine-cell-x')) s('.action-engine-cell-x').value = col;
1354
+ if (s('.action-engine-cell-y')) s('.action-engine-cell-y').value = row;
1355
+ const mapField =
1356
+ ActionEngineCyberia.mode === 'action' ? '.action-engine-action-map-code' : '.action-engine-quest-map-code';
1357
+ if (s(mapField)) s(mapField).value = ActionEngineCyberia.mapCode;
1358
+ htmls('.action-engine-cell-coords', `Cell: (${col}, ${row})`);
1359
+ ActionEngineCyberia.renderMapCanvas();
1360
+ };
1361
+
1362
+ // Quest wiring
1363
+ if (s('.btn-action-engine-add-step'))
1364
+ s('.btn-action-engine-add-step').onclick = () => ActionEngineCyberia.addStep();
1365
+ if (s('.btn-action-engine-add-objective'))
1366
+ s('.btn-action-engine-add-objective').onclick = () => ActionEngineCyberia.addObjective();
1367
+ if (s('.btn-action-engine-add-reward'))
1368
+ s('.btn-action-engine-add-reward').onclick = () => ActionEngineCyberia.addReward();
1369
+ if (s('.btn-action-engine-quest-save'))
1370
+ s('.btn-action-engine-quest-save').onclick = () => ActionEngineCyberia.saveQuest();
1371
+ if (s('.btn-action-engine-quest-clone'))
1372
+ s('.btn-action-engine-quest-clone').onclick = () => ActionEngineCyberia.cloneQuest();
1373
+ if (s('.btn-action-engine-quest-delete'))
1374
+ s('.btn-action-engine-quest-delete').onclick = () => ActionEngineCyberia.deleteQuest();
1375
+ if (s('.btn-action-engine-quest-new'))
1376
+ s('.btn-action-engine-quest-new').onclick = () => ActionEngineCyberia.resetQuest();
1377
+ if (s('.btn-action-engine-quest-refresh'))
1378
+ s('.btn-action-engine-quest-refresh').onclick = () => ActionEngineCyberia.refreshQuestList();
1379
+ if (s('.action-engine-quest-search'))
1380
+ s('.action-engine-quest-search').addEventListener('input', () => ActionEngineCyberia.renderQuestCards());
1381
+
1382
+ // Action wiring
1383
+ if (s('.btn-action-engine-add-quest-dialogue'))
1384
+ s('.btn-action-engine-add-quest-dialogue').onclick = () => {
1385
+ const questCode = s('.action-engine-action-quest-code')?.value?.trim();
1386
+ const dialogCode = ActionEngineCyberia.getSingleDropdownValue(ids.actionQuestDialogPicker);
1387
+ if (!questCode || !dialogCode) {
1388
+ NotificationManager.Push({ html: 'Quest code and dialog code are required.', status: 'error' });
1389
+ return;
1390
+ }
1391
+ ActionEngineCyberia.actionQuestDialogues.push({ questCode, dialogCode });
1392
+ ActionEngineCyberia.renderQuestDialogueList();
1393
+ };
1394
+ if (s('.btn-action-engine-action-save'))
1395
+ s('.btn-action-engine-action-save').onclick = () => ActionEngineCyberia.saveAction();
1396
+ if (s('.btn-action-engine-action-clone'))
1397
+ s('.btn-action-engine-action-clone').onclick = () => ActionEngineCyberia.cloneAction();
1398
+ if (s('.btn-action-engine-action-delete'))
1399
+ s('.btn-action-engine-action-delete').onclick = () => ActionEngineCyberia.deleteAction();
1400
+ if (s('.btn-action-engine-action-new'))
1401
+ s('.btn-action-engine-action-new').onclick = () => ActionEngineCyberia.resetAction();
1402
+ if (s('.btn-action-engine-action-refresh'))
1403
+ s('.btn-action-engine-action-refresh').onclick = () => ActionEngineCyberia.refreshActionList();
1404
+ if (s('.action-engine-action-search'))
1405
+ s('.action-engine-action-search').addEventListener('input', () => ActionEngineCyberia.renderActionCards());
1406
+
1407
+ // Dialogue wiring
1408
+ if (s('.btn-action-engine-dialogue-save'))
1409
+ s('.btn-action-engine-dialogue-save').onclick = () => ActionEngineCyberia.saveDialogue();
1410
+ if (s('.btn-action-engine-dialogue-delete'))
1411
+ s('.btn-action-engine-dialogue-delete').onclick = () => ActionEngineCyberia.deleteDialogue();
1412
+ if (s('.btn-action-engine-dialogue-new'))
1413
+ s('.btn-action-engine-dialogue-new').onclick = () => ActionEngineCyberia.resetDialogue();
1414
+ if (s('.btn-action-engine-dialogue-refresh'))
1415
+ s('.btn-action-engine-dialogue-refresh').onclick = () => ActionEngineCyberia.refreshDialogueList();
1416
+ if (s('.action-engine-dialogue-search'))
1417
+ s('.action-engine-dialogue-search').addEventListener('input', () => ActionEngineCyberia.renderDialogueCards());
1418
+
1419
+ // Skill wiring
1420
+ if (s('.btn-action-engine-add-skill-def'))
1421
+ s('.btn-action-engine-add-skill-def').onclick = () => ActionEngineCyberia.addSkillDef();
1422
+ if (s('.btn-action-engine-skill-save'))
1423
+ s('.btn-action-engine-skill-save').onclick = () => ActionEngineCyberia.saveSkill();
1424
+ if (s('.btn-action-engine-skill-delete'))
1425
+ s('.btn-action-engine-skill-delete').onclick = () => ActionEngineCyberia.deleteSkill();
1426
+ if (s('.btn-action-engine-skill-new'))
1427
+ s('.btn-action-engine-skill-new').onclick = () => ActionEngineCyberia.resetSkill();
1428
+ if (s('.btn-action-engine-skill-refresh'))
1429
+ s('.btn-action-engine-skill-refresh').onclick = () => ActionEngineCyberia.refreshSkillList();
1430
+ if (s('.action-engine-skill-search'))
1431
+ s('.action-engine-skill-search').addEventListener('input', () => ActionEngineCyberia.renderSkillCards());
1432
+
1433
+ ActionEngineCyberia.renderStepList();
1434
+ ActionEngineCyberia.renderRewardList();
1435
+ ActionEngineCyberia.renderQuestDialogueList();
1436
+ ActionEngineCyberia.renderSkillDefList();
1437
+ ActionEngineCyberia.renderAssignmentReview();
1438
+ await ActionEngineCyberia.refreshQuestList();
1439
+ await ActionEngineCyberia.refreshActionList();
1440
+ await ActionEngineCyberia.refreshDialogueList();
1441
+ await ActionEngineCyberia.refreshSkillList();
1442
+ });
1443
+
1444
+ const tabBtn = (mode, label) =>
1445
+ html`<div
1446
+ class="in fll action-engine-tab-${mode}"
1447
+ style="cursor:pointer;padding:9px 16px;font-size:13px;border-bottom:3px solid transparent;text-align:center;"
1448
+ >
1449
+ ${label}
1450
+ </div>`;
1451
+
1452
+ return html`<div class="in section-mp action-engine-container">
1453
+ ${group(
1454
+ 'Map Placement',
1455
+ 'fa-solid fa-map-location-dot',
1456
+ html`<div class="fl" style="align-items:flex-end;">
1457
+ <div class="in fll" style="flex:1;">${await ActionEngineCyberia.buildMapDropdown(ids.mapDropdown)}</div>
1458
+ <div class="in fll" style="padding-left:6px;">
1459
+ ${await BtnIcon.instance({
1460
+ class: 'wfa btn-action-engine-load-map',
1461
+ label: html`<i class="fa-solid fa-download"></i> Load Map`,
1462
+ })}
1463
+ </div>
1464
+ </div>
1465
+ <div
1466
+ class="in action-engine-map-meta"
1467
+ style="font-size:12px;color:#888;font-family:monospace;margin:6px 0 2px;"
1468
+ ></div>
1469
+ <div class="in action-engine-cell-coords" style="font-size:12px;font-family:monospace;margin-bottom:6px;">
1470
+ Cell: (—, —)
1471
+ </div>
1472
+ <div class="in" style="overflow:auto;max-width:100%;border:1px solid ${subtleBorder()};border-radius:4px;">
1473
+ <canvas class="action-engine-canvas" style="display:block;cursor:crosshair;"></canvas>
1474
+ </div>
1475
+ ${dynamicCol({ containerSelector: cont, id: dc.cell, type: 'a-50-b-50' })}
1476
+ <div class="fl" style="margin-top:8px;">
1477
+ <div class="in fll ${dc.cell}-col-a" style="padding-right:4px;">
1478
+ ${await Input.instance({
1479
+ id: 'action-engine-cell-x',
1480
+ label: html`Cell X`,
1481
+ containerClass: 'inl',
1482
+ type: 'number',
1483
+ min: 0,
1484
+ })}
1485
+ </div>
1486
+ <div class="in fll ${dc.cell}-col-b" style="padding-left:4px;">
1487
+ ${await Input.instance({
1488
+ id: 'action-engine-cell-y',
1489
+ label: html`Cell Y`,
1490
+ containerClass: 'inl',
1491
+ type: 'number',
1492
+ min: 0,
1493
+ })}
1494
+ </div>
1495
+ </div>
1496
+ <div class="in input-label" style="font-size:12px;margin:10px 0 4px;opacity:.8;">
1497
+ <i class="fa-solid fa-list-check"></i> Assignments on map
1498
+ </div>
1499
+ <div class="in action-engine-assignment-review" style="max-height:140px;overflow-y:auto;"></div>`,
1500
+ )}
1501
+
1502
+ <div class="fl" style="border-bottom:1px solid ${groupBorder()};margin-bottom:14px;">
1503
+ ${tabBtn('quest', html`<i class="fa-solid fa-scroll"></i> Quests`)}
1504
+ ${tabBtn('action', html`<i class="fa-solid fa-handshake"></i> Actions`)}
1505
+ ${tabBtn('dialogue', html`<i class="fa-solid fa-comments"></i> Dialogues`)}
1506
+ ${tabBtn('skill', html`<i class="fa-solid fa-wand-sparkles"></i> Skills`)}
1507
+ </div>
1508
+
1509
+ <!-- Quest panel -->
1510
+ <div class="in action-engine-panel-quest">
1511
+ ${group(
1512
+ 'Quest Details',
1513
+ 'fa-solid fa-circle-info',
1514
+ html`${dynamicCol({ containerSelector: cont, id: dc.questCt, type: 'a-50-b-50' })}
1515
+ <div class="fl">
1516
+ <div class="in fll ${dc.questCt}-col-a" style="padding-right:4px;">
1517
+ ${await Input.instance({
1518
+ id: 'action-engine-quest-code',
1519
+ label: html`Quest Code`,
1520
+ containerClass: 'inl',
1521
+ type: 'text',
1522
+ })}
1523
+ </div>
1524
+ <div class="in fll ${dc.questCt}-col-b" style="padding-left:4px;">
1525
+ ${await Input.instance({
1526
+ id: 'action-engine-quest-title',
1527
+ label: html`Title`,
1528
+ containerClass: 'inl',
1529
+ type: 'text',
1530
+ })}
1531
+ </div>
1532
+ </div>
1533
+ <div class="in" style="margin-top:6px;">
1534
+ ${await Input.instance({
1535
+ id: 'action-engine-quest-description',
1536
+ label: html`Description`,
1537
+ containerClass: 'inl',
1538
+ type: 'text',
1539
+ })}
1540
+ </div>
1541
+ <div class="in" style="margin-top:6px;">
1542
+ ${await Input.instance({
1543
+ id: 'action-engine-quest-map-code',
1544
+ label: html`Source Map Code (click a cell to assign)`,
1545
+ containerClass: 'inl',
1546
+ type: 'text',
1547
+ })}
1548
+ </div>`,
1549
+ )}
1550
+ ${group(
1551
+ 'Quest Chain',
1552
+ 'fa-solid fa-diagram-project',
1553
+ html`${dynamicCol({ containerSelector: cont, id: dc.questChain, type: 'a-50-b-50' })}
1554
+ <div class="fl">
1555
+ <div class="in fll ${dc.questChain}-col-a" style="padding-right:4px;">
1556
+ ${await Input.instance({
1557
+ id: 'action-engine-quest-prerequisites',
1558
+ label: html`Prerequisite Codes (csv)`,
1559
+ containerClass: 'inl',
1560
+ type: 'text',
1561
+ })}
1562
+ </div>
1563
+ <div class="in fll ${dc.questChain}-col-b" style="padding-left:4px;">
1564
+ ${await Input.instance({
1565
+ id: 'action-engine-quest-unlocks',
1566
+ label: html`Unlocks Codes (csv)`,
1567
+ containerClass: 'inl',
1568
+ type: 'text',
1569
+ })}
1570
+ </div>
1571
+ </div>`,
1572
+ )}
1573
+ ${group(
1574
+ 'Steps & Objectives',
1575
+ 'fa-solid fa-list-ol',
1576
+ html`<div class="in" style="margin-bottom:8px;">
1577
+ ${await BtnIcon.instance({
1578
+ class: 'wfa btn-action-engine-add-step',
1579
+ label: html`<i class="fa-solid fa-plus"></i> Add Step`,
1580
+ })}
1581
+ </div>
1582
+ <div class="in action-engine-step-list" style="margin-bottom:10px;"></div>
1583
+ <div class="in input-label" style="font-size:12px;margin-bottom:6px;opacity:.8;">Add objective to step</div>
1584
+ ${dynamicCol({ containerSelector: cont, id: dc.objective, type: 'search-inputs' })}
1585
+ <div class="fl" style="align-items:flex-end;">
1586
+ <div class="in fll ${dc.objective}-col-a" style="padding-right:4px;">
1587
+ ${await Input.instance({
1588
+ id: 'action-engine-objective-step',
1589
+ label: html`Step #`,
1590
+ containerClass: 'inl',
1591
+ type: 'number',
1592
+ min: 1,
1593
+ value: 1,
1594
+ })}
1595
+ </div>
1596
+ <div class="in fll ${dc.objective}-col-b" style="padding-right:4px;">
1597
+ ${await DropDown.instance({
1598
+ id: ids.objectiveType,
1599
+ label: html`Type`,
1600
+ data: objectiveTypeOptions,
1601
+ value: QUEST_STEPS_TYPES[0],
1602
+ containerClass: 'inl',
1603
+ })}
1604
+ </div>
1605
+ <div class="in fll ${dc.objective}-col-c" style="padding-right:4px;">
1606
+ ${await Input.instance({
1607
+ id: 'action-engine-objective-qty',
1608
+ label: html`Qty`,
1609
+ containerClass: 'inl',
1610
+ type: 'number',
1611
+ min: 1,
1612
+ value: 1,
1613
+ })}
1614
+ </div>
1615
+ </div>
1616
+ <div class="in" style="margin-top:6px;">
1617
+ ${await ActionEngineCyberia.buildItemIdDropdown(ids.objectiveItemPicker, html`Search itemId`)}
1618
+ </div>
1619
+ <div class="in" style="margin-top:6px;">
1620
+ ${await BtnIcon.instance({
1621
+ class: 'wfa btn-action-engine-add-objective',
1622
+ label: html`<i class="fa-solid fa-plus"></i> Add Objective`,
1623
+ })}
1624
+ </div>`,
1625
+ )}
1626
+ ${group(
1627
+ 'Rewards',
1628
+ 'fa-solid fa-gift',
1629
+ html`${dynamicCol({ containerSelector: cont, id: dc.reward, type: 'default' })}
1630
+ <div class="fl" style="align-items:flex-end;">
1631
+ <div class="in fll ${dc.reward}-col-a" style="padding-right:4px;">
1632
+ ${await Input.instance({
1633
+ id: 'action-engine-reward-qty',
1634
+ label: html`Qty`,
1635
+ containerClass: 'inl',
1636
+ type: 'number',
1637
+ min: 1,
1638
+ value: 1,
1639
+ })}
1640
+ </div>
1641
+ <div class="in fll ${dc.reward}-col-b">
1642
+ ${await ActionEngineCyberia.buildItemIdDropdown(ids.rewardItemPicker, html`Search itemId`)}
1643
+ </div>
1644
+ </div>
1645
+ <div class="in" style="margin-top:6px;">
1646
+ ${await BtnIcon.instance({
1647
+ class: 'wfa btn-action-engine-add-reward',
1648
+ label: html`<i class="fa-solid fa-plus"></i> Add Reward`,
1649
+ })}
1650
+ </div>
1651
+ <div class="in action-engine-reward-list" style="margin-top:8px;"></div>`,
1652
+ )}
1653
+ ${group('Save Quest', 'fa-solid fa-floppy-disk', await crudButtons('quest', true))}
1654
+ ${group(
1655
+ 'Existing Quests',
1656
+ 'fa-solid fa-scroll',
1657
+ html`${await listToolbar('quest')}
1658
+ <div class="in action-engine-quest-cards" style="max-height:300px;overflow-y:auto;"></div>`,
1659
+ )}
1660
+ </div>
1661
+
1662
+ <!-- Action panel -->
1663
+ <div class="in action-engine-panel-action hide">
1664
+ ${group(
1665
+ 'Action Details',
1666
+ 'fa-solid fa-circle-info',
1667
+ html`${dynamicCol({ containerSelector: cont, id: dc.actionCl, type: 'a-50-b-50' })}
1668
+ <div class="fl">
1669
+ <div class="in fll ${dc.actionCl}-col-a" style="padding-right:4px;">
1670
+ ${await Input.instance({
1671
+ id: 'action-engine-action-code',
1672
+ label: html`Action Code`,
1673
+ containerClass: 'inl',
1674
+ type: 'text',
1675
+ })}
1676
+ </div>
1677
+ <div class="in fll ${dc.actionCl}-col-b" style="padding-left:4px;">
1678
+ ${await Input.instance({
1679
+ id: 'action-engine-action-label',
1680
+ label: html`Label (nameplate)`,
1681
+ containerClass: 'inl',
1682
+ type: 'text',
1683
+ })}
1684
+ </div>
1685
+ </div>
1686
+ <div class="in" style="margin-top:6px;">
1687
+ ${await Input.instance({
1688
+ id: 'action-engine-action-map-code',
1689
+ label: html`Source Map Code (click a cell to assign)`,
1690
+ containerClass: 'inl',
1691
+ type: 'text',
1692
+ })}
1693
+ </div>
1694
+ <div class="in" style="margin-top:8px;">
1695
+ ${await ActionEngineCyberia.buildDialogCodeDropdown(
1696
+ ids.actionDialogPicker,
1697
+ html`Default greeting dialog code`,
1698
+ )}
1699
+ </div>`,
1700
+ )}
1701
+ ${group(
1702
+ 'Per-quest Dialogue Mapping',
1703
+ 'fa-solid fa-comments',
1704
+ html`${dynamicCol({ containerSelector: cont, id: dc.actionQd, type: 'a-50-b-50' })}
1705
+ <div class="fl" style="align-items:flex-end;">
1706
+ <div class="in fll ${dc.actionQd}-col-a" style="padding-right:4px;">
1707
+ ${await Input.instance({
1708
+ id: 'action-engine-action-quest-code',
1709
+ label: html`Quest Code`,
1710
+ containerClass: 'inl',
1711
+ type: 'text',
1712
+ })}
1713
+ </div>
1714
+ <div class="in fll ${dc.actionQd}-col-b">
1715
+ ${await ActionEngineCyberia.buildDialogCodeDropdown(ids.actionQuestDialogPicker, html`Dialog Code`)}
1716
+ </div>
1717
+ </div>
1718
+ <div class="in" style="margin-top:6px;">
1719
+ ${await BtnIcon.instance({
1720
+ class: 'wfa btn-action-engine-add-quest-dialogue',
1721
+ label: html`<i class="fa-solid fa-plus"></i> Map Dialogue`,
1722
+ })}
1723
+ </div>
1724
+ <div class="in action-engine-quest-dialogue-list" style="margin-top:8px;"></div>`,
1725
+ )}
1726
+ ${group('Save Action', 'fa-solid fa-floppy-disk', await crudButtons('action', true))}
1727
+ ${group(
1728
+ 'Existing Actions',
1729
+ 'fa-solid fa-handshake',
1730
+ html`${await listToolbar('action')}
1731
+ <div class="in action-engine-action-cards" style="max-height:300px;overflow-y:auto;"></div>`,
1732
+ )}
1733
+ </div>
1734
+
1735
+ <!-- Dialogue panel -->
1736
+ <div class="in action-engine-panel-dialogue hide">
1737
+ ${group(
1738
+ 'Dialogue Line',
1739
+ 'fa-solid fa-comment-dots',
1740
+ html`${dynamicCol({ containerSelector: cont, id: dc.dialogue, type: 'search-inputs' })}
1741
+ <div class="fl">
1742
+ <div class="in fll ${dc.dialogue}-col-a" style="padding-right:4px;">
1743
+ ${await Input.instance({
1744
+ id: 'action-engine-dialogue-code',
1745
+ label: html`Dialogue Code`,
1746
+ containerClass: 'inl',
1747
+ type: 'text',
1748
+ })}
1749
+ </div>
1750
+ <div class="in fll ${dc.dialogue}-col-b" style="padding-right:4px;">
1751
+ ${await Input.instance({
1752
+ id: 'action-engine-dialogue-order',
1753
+ label: html`Order`,
1754
+ containerClass: 'inl',
1755
+ type: 'number',
1756
+ min: 0,
1757
+ value: 0,
1758
+ })}
1759
+ </div>
1760
+ <div class="in fll ${dc.dialogue}-col-c">
1761
+ ${await Input.instance({
1762
+ id: 'action-engine-dialogue-mood',
1763
+ label: html`Mood`,
1764
+ containerClass: 'inl',
1765
+ type: 'text',
1766
+ })}
1767
+ </div>
1768
+ </div>
1769
+ <div class="in" style="margin-top:6px;">
1770
+ ${await Input.instance({
1771
+ id: 'action-engine-dialogue-speaker',
1772
+ label: html`Speaker`,
1773
+ containerClass: 'inl',
1774
+ type: 'text',
1775
+ })}
1776
+ </div>
1777
+ <div class="in" style="margin-top:6px;">
1778
+ ${await Input.instance({
1779
+ id: 'action-engine-dialogue-text',
1780
+ label: html`Text`,
1781
+ containerClass: 'inl',
1782
+ type: 'text',
1783
+ })}
1784
+ </div>`,
1785
+ )}
1786
+ ${group('Save Dialogue Line', 'fa-solid fa-floppy-disk', await crudButtons('dialogue', false))}
1787
+ ${group(
1788
+ 'Existing Dialogue Lines',
1789
+ 'fa-solid fa-comments',
1790
+ html`${await listToolbar('dialogue')}
1791
+ <div class="in action-engine-dialogue-cards" style="max-height:300px;overflow-y:auto;"></div>`,
1792
+ )}
1793
+ </div>
1794
+
1795
+ <!-- Skill panel -->
1796
+ <div class="in action-engine-panel-skill hide">
1797
+ ${group(
1798
+ 'Skill Definition',
1799
+ 'fa-solid fa-circle-info',
1800
+ html`<div class="in input-label" style="font-size:12px;margin-bottom:6px;opacity:.8;">
1801
+ Trigger item — the ObjectLayer item whose active layer fires these skills.
1802
+ </div>
1803
+ ${await ActionEngineCyberia.buildItemIdDropdown(ids.skillTriggerItemPicker, html`Search trigger itemId`)}`,
1804
+ )}
1805
+ ${group(
1806
+ 'Logic Skills',
1807
+ 'fa-solid fa-bolt',
1808
+ html`<div class="in input-label" style="font-size:12px;margin-bottom:6px;opacity:.8;">Add logic event</div>
1809
+ ${dynamicCol({ containerSelector: cont, id: dc.skillDef, type: 'a-50-b-50' })}
1810
+ <div class="fl">
1811
+ <div class="in fll ${dc.skillDef}-col-a" style="padding-right:4px;">
1812
+ ${await DropDown.instance({
1813
+ id: 'action-engine-skill-logic-event',
1814
+ label: html`Logic Event Id`,
1815
+ containerClass: 'inl',
1816
+ data: SKILL_LOGIC_IDS.map((l) => ({
1817
+ value: l.id,
1818
+ display: l.name ? `${l.id} — ${l.name}` : l.id,
1819
+ data: l.id,
1820
+ onClick: () => {},
1821
+ })),
1822
+ })}
1823
+ </div>
1824
+ <div class="in fll ${dc.skillDef}-col-b" style="padding-left:4px;">
1825
+ ${await Input.instance({
1826
+ id: 'action-engine-skill-name',
1827
+ label: html`Name`,
1828
+ containerClass: 'inl',
1829
+ type: 'text',
1830
+ })}
1831
+ </div>
1832
+ </div>
1833
+ <div class="in" style="margin-top:6px;">
1834
+ ${await Input.instance({
1835
+ id: 'action-engine-skill-description',
1836
+ label: html`Description`,
1837
+ containerClass: 'inl',
1838
+ type: 'text',
1839
+ })}
1840
+ </div>
1841
+ <div class="in" style="margin-top:6px;">
1842
+ ${await ActionEngineCyberia.buildItemIdDropdown(
1843
+ ids.skillSummonedItemPicker,
1844
+ html`Summoned entity itemId`,
1845
+ )}
1846
+ </div>
1847
+ <div class="in" style="margin-top:6px;">
1848
+ ${await BtnIcon.instance({
1849
+ class: 'wfa btn-action-engine-add-skill-def',
1850
+ label: html`<i class="fa-solid fa-plus"></i> Add Skill`,
1851
+ })}
1852
+ </div>
1853
+ <div class="in action-engine-skill-def-list" style="margin-top:8px;"></div>`,
1854
+ )}
1855
+ ${group('Save Skill', 'fa-solid fa-floppy-disk', await crudButtons('skill', false))}
1856
+ ${group(
1857
+ 'Existing Skills',
1858
+ 'fa-solid fa-wand-sparkles',
1859
+ html`${await listToolbar('skill')}
1860
+ <div class="in action-engine-skill-cards" style="max-height:300px;overflow-y:auto;"></div>`,
1861
+ )}
1862
+ </div>
1863
+ </div>`;
1864
+ }
1865
+ }
1866
+
1867
+ export { ActionEngineCyberia };