underpost 2.85.7 → 2.89.0

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 (36) hide show
  1. package/.github/workflows/release.cd.yml +1 -1
  2. package/README.md +2 -2
  3. package/bin/build.js +8 -10
  4. package/cli.md +3 -2
  5. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  6. package/manifests/deployment/dd-test-development/deployment.yaml +50 -50
  7. package/manifests/deployment/dd-test-development/proxy.yaml +4 -4
  8. package/package.json +1 -1
  9. package/src/api/file/file.service.js +29 -3
  10. package/src/cli/baremetal.js +1 -2
  11. package/src/cli/index.js +1 -0
  12. package/src/cli/repository.js +8 -1
  13. package/src/cli/run.js +97 -36
  14. package/src/client/components/core/AgGrid.js +42 -3
  15. package/src/client/components/core/CommonJs.js +4 -0
  16. package/src/client/components/core/Css.js +95 -48
  17. package/src/client/components/core/CssCore.js +0 -1
  18. package/src/client/components/core/LoadingAnimation.js +2 -2
  19. package/src/client/components/core/Logger.js +2 -9
  20. package/src/client/components/core/Modal.js +22 -14
  21. package/src/client/components/core/ObjectLayerEngine.js +300 -9
  22. package/src/client/components/core/ObjectLayerEngineModal.js +686 -148
  23. package/src/client/components/core/ObjectLayerEngineViewer.js +1061 -0
  24. package/src/client/components/core/Pagination.js +15 -5
  25. package/src/client/components/core/Router.js +5 -1
  26. package/src/client/components/core/Translate.js +4 -0
  27. package/src/client/components/core/Worker.js +8 -1
  28. package/src/client/services/default/default.management.js +86 -16
  29. package/src/db/mariadb/MariaDB.js +2 -2
  30. package/src/index.js +1 -1
  31. package/src/server/client-build.js +57 -2
  32. package/src/server/object-layer.js +44 -0
  33. package/src/server/start.js +12 -0
  34. package/src/ws/IoInterface.js +2 -3
  35. package/AUTHORS.md +0 -21
  36. package/src/server/network.js +0 -72
@@ -5,11 +5,19 @@ import { DropDown } from './DropDown.js';
5
5
  import { EventsUI } from './EventsUI.js';
6
6
  import { Translate } from './Translate.js';
7
7
  import { s, append, hexToRgbA } from './VanillaJs.js';
8
- import { getProxyPath } from './Router.js';
8
+ import { getProxyPath, getQueryParams, setPath, setQueryParams } from './Router.js';
9
9
  import { s4 } from './CommonJs.js';
10
10
  import { Input } from './Input.js';
11
11
  import { ToggleSwitch } from './ToggleSwitch.js';
12
12
  import { ObjectLayerService } from '../../services/object-layer/object-layer.service.js';
13
+ import { NotificationManager } from './NotificationManager.js';
14
+ import { AgGrid } from './AgGrid.js';
15
+ import { Modal } from './Modal.js';
16
+ import { loggerFactory } from './Logger.js';
17
+ import { LoadingAnimation } from './LoadingAnimation.js';
18
+ import { DefaultManagement } from '../../services/default/default.management.js';
19
+
20
+ const logger = loggerFactory(import.meta, { trace: true });
13
21
 
14
22
  const ObjectLayerEngineModal = {
15
23
  templates: [
@@ -19,6 +27,46 @@ const ObjectLayerEngineModal = {
19
27
  data: [],
20
28
  },
21
29
  ],
30
+ statDescriptions: {
31
+ effect: {
32
+ title: 'Effect',
33
+ icon: 'fa-solid fa-burst',
34
+ description: 'Amount of life removed when an entity collides or deals an impact.',
35
+ detail: 'Measured in life points.',
36
+ },
37
+ resistance: {
38
+ title: 'Resistance',
39
+ icon: 'fa-solid fa-shield',
40
+ description: "Adds to the owner's maximum life (survivability cap).",
41
+ detail:
42
+ "This value is summed with the entity's base max life. It also increases the amount of life restored when a regeneration event occurs (adds directly to current life).",
43
+ },
44
+ agility: {
45
+ title: 'Agility',
46
+ icon: 'fa-solid fa-person-running',
47
+ description: 'Increases the movement speed of entities.',
48
+ detail: 'Higher values result in faster movement.',
49
+ },
50
+ range: {
51
+ title: 'Range',
52
+ icon: 'fa-solid fa-bullseye',
53
+ description: 'Increases the lifetime of a cast/summoned entity.',
54
+ detail: 'Measured in milliseconds.',
55
+ },
56
+ intelligence: {
57
+ title: 'Intelligence',
58
+ icon: 'fa-solid fa-brain',
59
+ description: 'Probability-based stat that increases the chance to spawn/trigger a summoned entity.',
60
+ detail: 'Higher values increase summoning success rate.',
61
+ },
62
+ utility: {
63
+ title: 'Utility',
64
+ icon: 'fa-solid fa-wrench',
65
+ description: 'Reduces the cooldown time between actions, allowing for more frequent actions.',
66
+ detail: 'It also increases the chance to trigger life-regeneration events.',
67
+ },
68
+ },
69
+
22
70
  RenderTemplate: (colorTemplate) => {
23
71
  const ole = s('object-layer-engine');
24
72
  if (!ole) {
@@ -34,7 +82,88 @@ const ObjectLayerEngineModal = {
34
82
  ole.loadMatrix(matrix);
35
83
  },
36
84
  ObjectLayerData: {},
37
- Render: async (options = { idModal: '' }) => {
85
+ clearData: function () {
86
+ // Clear all cached object layer data to prevent contamination between sessions
87
+ this.ObjectLayerData = {};
88
+
89
+ // Clear the canvas if it exists
90
+ const ole = s('object-layer-engine');
91
+ if (ole && typeof ole.clear === 'function') {
92
+ ole.clear();
93
+ }
94
+
95
+ // Clear all frame previews from DOM for all direction codes
96
+ const directionCodes = ['08', '18', '02', '12', '04', '14', '06', '16'];
97
+ for (const directionCode of directionCodes) {
98
+ const framesContainer = s(`.frames-${directionCode}`);
99
+ if (framesContainer) {
100
+ framesContainer.innerHTML = '';
101
+ }
102
+ }
103
+
104
+ // Clear form inputs with correct IDs
105
+ const itemIdInput = s('#ol-input-item-id');
106
+ if (itemIdInput) itemIdInput.value = '';
107
+
108
+ const itemDescInput = s('#ol-input-item-description');
109
+ if (itemDescInput) itemDescInput.value = '';
110
+
111
+ const frameDurationInput = s('#ol-input-render-frame-duration');
112
+ if (frameDurationInput) frameDurationInput.value = '100';
113
+
114
+ // Reset toggle switches with correct IDs
115
+ const activableCheckbox = s('#ol-toggle-item-activable');
116
+ if (activableCheckbox) activableCheckbox.checked = false;
117
+
118
+ const statelessCheckbox = s('#ol-toggle-render-is-stateless');
119
+ if (statelessCheckbox) statelessCheckbox.checked = false;
120
+
121
+ // Clear stat inputs with correct IDs
122
+ const statTypes = Object.keys(ObjectLayerEngineModal.statDescriptions);
123
+ for (const stat of statTypes) {
124
+ const statInput = s(`#ol-input-item-stats-${stat}`);
125
+ if (statInput) statInput.value = '0';
126
+ }
127
+ },
128
+ loadFromDatabase: async (objectLayerId) => {
129
+ try {
130
+ // Load metadata first (lightweight)
131
+ const { status: metaStatus, data: metadata } = await ObjectLayerService.getMetadata({ id: objectLayerId });
132
+
133
+ if (metaStatus !== 'success' || !metadata) {
134
+ NotificationManager.Push({
135
+ html: `Failed to load object layer metadata`,
136
+ status: 'error',
137
+ });
138
+ return null;
139
+ }
140
+
141
+ // Load render data separately (heavy)
142
+ const { status: renderStatus, data: renderData } = await ObjectLayerService.getRender({ id: objectLayerId });
143
+
144
+ if (renderStatus !== 'success' || !renderData) {
145
+ NotificationManager.Push({
146
+ html: `Failed to load object layer render data`,
147
+ status: 'error',
148
+ });
149
+ return null;
150
+ }
151
+
152
+ return { metadata, renderData };
153
+ } catch (error) {
154
+ console.error('Error loading object layer from database:', error);
155
+ NotificationManager.Push({
156
+ html: `Error loading object layer: ${error.message}`,
157
+ status: 'error',
158
+ });
159
+ return null;
160
+ }
161
+ },
162
+ Render: async (options = { idModal: '', Elements: {} }) => {
163
+ // Clear all cached data at the start of each render to prevent contamination
164
+ ObjectLayerEngineModal.clearData();
165
+
166
+ const { Elements } = options;
38
167
  await import(`${getProxyPath()}components/core/ObjectLayerEngine.js`);
39
168
  // await import(`${getProxyPath()}components/core/WebComponent.js`);
40
169
  const directionCodes = ['08', '18', '02', '12', '04', '14', '06', '16'];
@@ -45,6 +174,37 @@ const ObjectLayerEngineModal = {
45
174
  let renderIsStateless = false;
46
175
  let renderFrameDuration = 100;
47
176
 
177
+ // Check if we have a 'cid' query parameter to load existing object layer
178
+ const queryParams = getQueryParams();
179
+ let loadedData = null;
180
+ let existingObjectLayerId = null; // Track the _id for updates
181
+ let originalDirectionCodes = []; // Track original direction codes for update mode
182
+ if (queryParams.cid) {
183
+ existingObjectLayerId = queryParams.cid;
184
+ loadedData = await ObjectLayerEngineModal.loadFromDatabase(queryParams.cid);
185
+
186
+ if (loadedData) {
187
+ const { metadata, renderData } = loadedData;
188
+
189
+ // Set form values from metadata
190
+ if (metadata.data) {
191
+ if (metadata.data.item) {
192
+ selectItemType = metadata.data.item.type || itemTypes[0];
193
+ itemActivable = metadata.data.item.activable || false;
194
+
195
+ // Add loaded item type to itemTypes array if it doesn't exist
196
+ if (selectItemType && !itemTypes.includes(selectItemType)) {
197
+ itemTypes.push(selectItemType);
198
+ }
199
+ }
200
+ if (metadata.data.render) {
201
+ renderIsStateless = metadata.data.render.is_stateless || false;
202
+ renderFrameDuration = metadata.data.render.frame_duration || 100;
203
+ }
204
+ }
205
+ }
206
+ }
207
+
48
208
  for (const url of [
49
209
  `${getProxyPath()}assets/templates/item-skin-08.json`,
50
210
  `${getProxyPath()}assets/templates/item-skin-06.json`,
@@ -64,173 +224,471 @@ const ObjectLayerEngineModal = {
64
224
 
65
225
  let directionsCodeBarRender = '';
66
226
 
227
+ // Helper function to add a frame to the direction bar
228
+ const addFrameToBar = async (directionCode, id, image, json) => {
229
+ // Capture directionCode in a local variable to ensure proper closure
230
+ const capturedDirectionCode = directionCode;
231
+
232
+ append(
233
+ `.frames-${capturedDirectionCode}`,
234
+ html`
235
+ <div class="in fll ${id}">
236
+ <img
237
+ class="in fll direction-code-bar-frames-img direction-code-bar-frames-img-${id}"
238
+ src="${URL.createObjectURL(image)}"
239
+ data-direction-code="${capturedDirectionCode}"
240
+ />
241
+ ${await BtnIcon.Render({
242
+ label: html`<i class="fa-solid fa-trash"></i>`,
243
+ class: `abs direction-code-bar-trash-btn direction-code-bar-trash-btn-${id}`,
244
+ })}
245
+ </div>
246
+ `,
247
+ );
248
+
249
+ EventsUI.onClick(`.direction-code-bar-frames-img-${id}`, async (e) => {
250
+ // Get direction code from data attribute to ensure we're using the correct one
251
+ const clickedDirectionCode = e.target.getAttribute('data-direction-code') || capturedDirectionCode;
252
+ console.log(`Clicked frame ${id} from direction code: ${clickedDirectionCode}`);
253
+ const frameData = ObjectLayerEngineModal.ObjectLayerData[clickedDirectionCode]?.find(
254
+ (frame) => frame.id === id,
255
+ );
256
+ if (frameData && frameData.json) {
257
+ console.log(`Loading frame data for direction code ${clickedDirectionCode}:`, frameData.json);
258
+ s('object-layer-engine').importMatrixJSON(frameData.json);
259
+ } else {
260
+ console.error(`Frame data not found for id ${id} in direction code ${clickedDirectionCode}`);
261
+ }
262
+ });
263
+
264
+ EventsUI.onClick(`.direction-code-bar-trash-btn-${id}`, async () => {
265
+ s(`.${id}`).remove();
266
+ ObjectLayerEngineModal.ObjectLayerData[capturedDirectionCode] = ObjectLayerEngineModal.ObjectLayerData[
267
+ capturedDirectionCode
268
+ ].filter((frame) => frame.id !== id);
269
+ });
270
+ };
271
+
272
+ // Helper function to show loading animation
273
+ const showFrameLoading = () => {
274
+ if (!s(`.frame-editor-container`) || s(`.frame-editor-container`).classList.contains('hide')) return;
275
+ LoadingAnimation.spinner.play(`.frame-editor-container-loading`, 'dual-ring-mini', {
276
+ prepend: html`<span class="inl loading-text">Loading </span><br /><br /> ` + '<div style="color: gray;">',
277
+ append: '</div>',
278
+ });
279
+ s(`.frame-editor-container`).classList.add('hide');
280
+ s(`.frame-editor-container-loading`).classList.remove('hide');
281
+ };
282
+
283
+ // Helper function to hide loading animation
284
+ const hideFrameLoading = () => {
285
+ if (!s(`.frame-editor-container-loading`) || s(`.frame-editor-container-loading`).classList.contains('hide'))
286
+ return;
287
+ LoadingAnimation.spinner.stop(`.frame-editor-container-loading`);
288
+ s(`.frame-editor-container-loading`).classList.add('hide');
289
+ s(`.frame-editor-container`).classList.remove('hide');
290
+ };
291
+
292
+ // Helper function to process and add frame from PNG URL using ObjectLayerPngLoader
293
+ const processAndAddFrameFromPngUrl = async (directionCode, pngUrl) => {
294
+ // Wait for components to be available with retry logic
295
+ let ole = s('object-layer-engine');
296
+ let loader = s('object-layer-png-loader');
297
+
298
+ if (!ole || !loader) {
299
+ console.error('object-layer-engine or object-layer-png-loader component not found after retries');
300
+ return;
301
+ }
302
+
303
+ try {
304
+ // Load PNG using the loader component - it will automatically load into the editor
305
+ await loader.loadPngUrl(pngUrl);
306
+
307
+ // Export as blob and JSON from component after loading
308
+ const image = await ole.toBlob();
309
+ const json = ole.exportMatrixJSON();
310
+ const id = `frame-loaded-${s4()}-${s4()}`;
311
+
312
+ // Add to ObjectLayerData
313
+ if (!ObjectLayerEngineModal.ObjectLayerData[directionCode]) {
314
+ ObjectLayerEngineModal.ObjectLayerData[directionCode] = [];
315
+ }
316
+ ObjectLayerEngineModal.ObjectLayerData[directionCode].push({ id, image, json });
317
+ console.log(
318
+ `Stored frame ${id} in direction code ${directionCode}. Total frames:`,
319
+ ObjectLayerEngineModal.ObjectLayerData[directionCode].length,
320
+ );
321
+
322
+ // Add to UI
323
+ await addFrameToBar(directionCode, id, image, json);
324
+ } catch (error) {
325
+ console.error('Error loading frame from PNG URL:', error);
326
+ }
327
+ };
328
+
67
329
  for (const directionCode of directionCodes) {
68
- setTimeout(() => {
69
- EventsUI.onClick(`.direction-code-bar-frames-btn-${directionCode}`, async () => {
70
- const image = await s('object-layer-engine').toBlob();
71
- const json = s('object-layer-engine').exportMatrixJSON();
72
- const id = `frame-capture-${s4()}-${s4()}`;
73
-
74
- if (!ObjectLayerEngineModal.ObjectLayerData[directionCode])
75
- ObjectLayerEngineModal.ObjectLayerData[directionCode] = [];
76
- ObjectLayerEngineModal.ObjectLayerData[directionCode].push({ id, image, json });
77
-
78
- append(
79
- `.frames-${directionCode}`,
80
- html`
81
- <div class="in fll ${id}">
82
- <img class="in fll direction-code-bar-frames-img" src="${URL.createObjectURL(image)}" />
330
+ directionsCodeBarRender += html`
331
+ <div class="in section-mp-border">
332
+ <div class="fl">
333
+ <div class="in fll">
334
+ <div class="in direction-code-bar-frames-title">${directionCode}</div>
335
+ <div class="in direction-code-bar-frames-btn">
83
336
  ${await BtnIcon.Render({
84
- label: html`<i class="fa-solid fa-trash"></i>`,
85
- class: `abs direction-code-bar-trash-btn direction-code-bar-trash-btn-${id}`,
337
+ label: html`<i class="fa-solid fa-plus"></i>`,
338
+ class: `direction-code-bar-frames-btn-${directionCode}`,
86
339
  })}
87
340
  </div>
88
- `,
89
- );
341
+ </div>
342
+ <div class="frames-${directionCode}"></div>
343
+ </div>
344
+ </div>
345
+ `;
346
+ }
347
+
348
+ let statsInputsRender = '';
349
+ for (const statType of statTypes) {
350
+ const statInfo = ObjectLayerEngineModal.statDescriptions[statType];
351
+ const statValue = loadedData?.metadata?.data?.stats?.[statType] || 0;
352
+ statsInputsRender += html`
353
+ <div class="inl" style="margin-bottom: 10px; position: relative;">
354
+ ${await Input.Render({
355
+ id: `ol-input-item-stats-${statType}`,
356
+ label: html`<div
357
+ title="${statInfo.description} ${statInfo.detail}"
358
+ class="inl stat-label-container stat-info-icon"
359
+ style="width: 120px; font-size: 16px; overflow: visible; position: relative;"
360
+ >
361
+ <i class="${statInfo.icon}" style="margin-right: 5px;"></i> ${statInfo.title}
362
+ </div>`,
363
+ containerClass: 'inl',
364
+ type: 'number',
365
+ min: 0,
366
+ max: 10,
367
+ placeholder: true,
368
+ value: statValue,
369
+ })}
370
+ <div class="in stat-description">
371
+ ${statInfo.description}<br />
372
+ <span style="color: #888; font-style: italic;">${statInfo.detail}</span>
373
+ </div>
374
+ </div>
375
+ `;
376
+ }
377
+
378
+ setTimeout(async () => {
379
+ showFrameLoading();
380
+ for (const directionCode of directionCodes) {
381
+ // Use IIFE to properly capture directionCode and handle async operations
382
+ await (async (currentDirectionCode) => {
383
+ // Register frame add button handler after DOM is ready
384
+ // Wait longer to ensure all direction bars are rendered
385
+
386
+ if (loadedData && loadedData.metadata && loadedData.metadata.data && currentDirectionCode) {
387
+ // Show loading animation only once on first direction that has frames
388
+
389
+ const { type, id } = loadedData.metadata.data.item;
390
+ const directions = ObjectLayerEngineModal.getDirectionsFromDirectionCode(currentDirectionCode);
391
+
392
+ console.log(`Loading frames for direction code: ${currentDirectionCode}, directions:`, directions);
90
393
 
91
- EventsUI.onClick(`.direction-code-bar-trash-btn-${id}`, async () => {
92
- s(`.${id}`).remove();
93
- ObjectLayerEngineModal.ObjectLayerData[directionCode] = ObjectLayerEngineModal.ObjectLayerData[
94
- directionCode
95
- ].filter((frame) => frame.id !== id);
394
+ // Check if frames exist for any direction mapped to this direction code
395
+ const { frames } = loadedData.renderData.data.render;
396
+ for (const direction of directions) {
397
+ if (frames[direction] && frames[direction].length > 0) {
398
+ // Track this direction code as having original data
399
+ if (!originalDirectionCodes.includes(currentDirectionCode)) {
400
+ originalDirectionCodes.push(currentDirectionCode);
401
+ }
402
+ // Load frames from static PNG URLs sequentially to avoid race conditions
403
+ const frameCount = frames[direction].length;
404
+ console.log(`Found ${frameCount} frames for direction: ${direction} (code: ${currentDirectionCode})`);
405
+ for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) {
406
+ const pngUrl = `${getProxyPath()}assets/${type}/${id}/${currentDirectionCode}/${frameIndex}.png`;
407
+ console.log(`Loading frame ${frameIndex} for direction code ${currentDirectionCode} from: ${pngUrl}`);
408
+ await processAndAddFrameFromPngUrl(currentDirectionCode, pngUrl);
409
+ }
410
+ console.log(`Completed loading ${frameCount} frames for direction code: ${currentDirectionCode}`);
411
+ // Once we found frames for this direction code, we can break to avoid duplicates
412
+ break;
413
+ }
414
+ }
415
+ }
416
+
417
+ const buttonSelector = `.direction-code-bar-frames-btn-${currentDirectionCode}`;
418
+ console.log(`Registering click handler for: ${buttonSelector}`);
419
+
420
+ EventsUI.onClick(buttonSelector, async () => {
421
+ console.log(`Add frame button clicked for direction: ${currentDirectionCode}`);
422
+ const ole = s('object-layer-engine');
423
+ if (!ole) {
424
+ console.error('object-layer-engine not found');
425
+ return;
426
+ }
427
+ const image = await ole.toBlob();
428
+ const json = ole.exportMatrixJSON();
429
+ const id = `frame-capture-${s4()}-${s4()}`;
430
+ console.log(`Creating new frame ${id} for direction ${currentDirectionCode}`);
431
+
432
+ if (!ObjectLayerEngineModal.ObjectLayerData[currentDirectionCode])
433
+ ObjectLayerEngineModal.ObjectLayerData[currentDirectionCode] = [];
434
+ ObjectLayerEngineModal.ObjectLayerData[currentDirectionCode].push({ id, image, json });
435
+ console.log(
436
+ `Stored frame ${id} in direction code ${currentDirectionCode}. Total frames:`,
437
+ ObjectLayerEngineModal.ObjectLayerData[currentDirectionCode].length,
438
+ );
439
+
440
+ await addFrameToBar(currentDirectionCode, id, image, json);
96
441
  });
97
- });
442
+ })(directionCode);
443
+ }
444
+ hideFrameLoading();
445
+ s('object-layer-engine').clear();
98
446
 
99
- EventsUI.onClick(`.ol-btn-save`, async () => {
100
- const objectLayer = {
101
- data: {
102
- render: {
103
- frames: {},
104
- color: [],
105
- frame_duration: 0,
106
- is_stateless: false,
107
- },
108
- stats: {},
109
- item: {},
447
+ EventsUI.onClick(`.ol-btn-save`, async () => {
448
+ // Validate minimum frame_duration 100ms
449
+ const frameDuration = parseInt(s(`.ol-input-render-frame-duration`).value);
450
+ if (!frameDuration || frameDuration < 100) {
451
+ NotificationManager.Push({
452
+ html: 'Frame duration must be at least 100ms',
453
+ status: 'error',
454
+ });
455
+ return;
456
+ }
457
+
458
+ // Validate that item.id is not empty
459
+ const itemId = s(`.ol-input-item-id`).value;
460
+ if (!itemId || itemId.trim() === '') {
461
+ NotificationManager.Push({
462
+ html: 'Item ID is required',
463
+ status: 'error',
464
+ });
465
+ return;
466
+ }
467
+
468
+ const objectLayer = {
469
+ data: {
470
+ render: {
471
+ frames: {},
472
+ color: [],
473
+ frame_duration: 0,
474
+ is_stateless: false,
110
475
  },
111
- };
112
- for (const directionCode of directionCodes) {
113
- const directions = ObjectLayerEngineModal.getDirectionsFromDirectionCode(directionCode);
114
- for (const direction of directions) {
115
- if (!objectLayer.data.render.frames[direction]) objectLayer.data.render.frames[direction] = [];
476
+ stats: {},
477
+ item: {},
478
+ },
479
+ };
480
+ for (const directionCode of directionCodes) {
481
+ const directions = ObjectLayerEngineModal.getDirectionsFromDirectionCode(directionCode);
482
+ for (const direction of directions) {
483
+ if (!objectLayer.data.render.frames[direction]) objectLayer.data.render.frames[direction] = [];
116
484
 
117
- if (!(directionCode in ObjectLayerEngineModal.ObjectLayerData)) {
118
- console.warn('No set directionCodeBarFrameData for directionCode', directionCode);
119
- continue;
120
- }
485
+ if (!(directionCode in ObjectLayerEngineModal.ObjectLayerData)) {
486
+ console.warn('No set directionCodeBarFrameData for directionCode', directionCode);
487
+ continue;
488
+ }
121
489
 
122
- for (const frameData of ObjectLayerEngineModal.ObjectLayerData[directionCode]) {
123
- const { matrix } = JSON.parse(frameData.json);
124
- const frameIndexColorMatrix = [];
125
- let indexRow = -1;
126
- for (const row of matrix) {
127
- indexRow++;
128
- frameIndexColorMatrix[indexRow] = [];
129
- let indexCol = -1;
130
- for (const value of row) {
131
- indexCol++;
132
- let colorIndex = objectLayer.data.render.color.findIndex(
133
- (color) =>
134
- color[0] === value[0] &&
135
- color[1] === value[1] &&
136
- color[2] === value[2] &&
137
- color[3] === value[3],
138
- );
139
- if (colorIndex === -1) {
140
- objectLayer.data.render.color.push(value);
141
- colorIndex = objectLayer.data.render.color.length - 1;
142
- }
143
- frameIndexColorMatrix[indexRow][indexCol] = colorIndex;
490
+ for (const frameData of ObjectLayerEngineModal.ObjectLayerData[directionCode]) {
491
+ const { matrix } = JSON.parse(frameData.json);
492
+ const frameIndexColorMatrix = [];
493
+ let indexRow = -1;
494
+ for (const row of matrix) {
495
+ indexRow++;
496
+ frameIndexColorMatrix[indexRow] = [];
497
+ let indexCol = -1;
498
+ for (const value of row) {
499
+ indexCol++;
500
+ let colorIndex = objectLayer.data.render.color.findIndex(
501
+ (color) =>
502
+ color[0] === value[0] && color[1] === value[1] && color[2] === value[2] && color[3] === value[3],
503
+ );
504
+ if (colorIndex === -1) {
505
+ objectLayer.data.render.color.push(value);
506
+ colorIndex = objectLayer.data.render.color.length - 1;
144
507
  }
508
+ frameIndexColorMatrix[indexRow][indexCol] = colorIndex;
145
509
  }
146
- objectLayer.data.render.frames[direction].push(frameIndexColorMatrix);
147
510
  }
511
+ objectLayer.data.render.frames[direction].push(frameIndexColorMatrix);
148
512
  }
149
513
  }
150
- objectLayer.data.render.frame_duration = parseInt(s(`.ol-input-render-frame-duration`).value);
151
- objectLayer.data.render.is_stateless = renderIsStateless;
152
- objectLayer.data.stats = {
153
- effect: parseInt(s(`.ol-input-item-stats-effect`).value),
154
- resistance: parseInt(s(`.ol-input-item-stats-resistance`).value),
155
- agility: parseInt(s(`.ol-input-item-stats-agility`).value),
156
- range: parseInt(s(`.ol-input-item-stats-range`).value),
157
- intelligence: parseInt(s(`.ol-input-item-stats-intelligence`).value),
158
- utility: parseInt(s(`.ol-input-item-stats-utility`).value),
159
- };
160
- objectLayer.data.item = {
161
- type: selectItemType,
162
- activable: itemActivable,
163
- id: s(`.ol-input-item-id`).value,
164
- description: s(`.ol-input-item-description`).value,
165
- };
166
- console.warn('objectLayer', objectLayer);
167
-
168
- // Upload images
169
- {
170
- for (const directionCode of Object.keys(ObjectLayerEngineModal.ObjectLayerData)) {
171
- let frameIndex = -1;
172
- for (const frame of ObjectLayerEngineModal.ObjectLayerData[directionCode]) {
173
- frameIndex++;
174
- const pngBlob = frame.image;
175
-
176
- const form = new FormData();
177
- form.append(directionCode, pngBlob, `${frameIndex}.png`);
178
-
179
- const { status, data } = await ObjectLayerService.post({
180
- id: `frame-image/${objectLayer.data.item.type}/${objectLayer.data.item.id}/${directionCode}`,
514
+ }
515
+ objectLayer.data.render.frame_duration = parseInt(s(`.ol-input-render-frame-duration`).value);
516
+ objectLayer.data.render.is_stateless = renderIsStateless;
517
+ objectLayer.data.stats = {
518
+ effect: parseInt(s(`.ol-input-item-stats-effect`).value),
519
+ resistance: parseInt(s(`.ol-input-item-stats-resistance`).value),
520
+ agility: parseInt(s(`.ol-input-item-stats-agility`).value),
521
+ range: parseInt(s(`.ol-input-item-stats-range`).value),
522
+ intelligence: parseInt(s(`.ol-input-item-stats-intelligence`).value),
523
+ utility: parseInt(s(`.ol-input-item-stats-utility`).value),
524
+ };
525
+ objectLayer.data.item = {
526
+ type: selectItemType,
527
+ activable: itemActivable,
528
+ id: s(`.ol-input-item-id`).value,
529
+ description: s(`.ol-input-item-description`).value,
530
+ };
531
+
532
+ // Add _id if we're updating an existing object layer
533
+ if (existingObjectLayerId) {
534
+ objectLayer._id = existingObjectLayerId;
535
+ }
536
+
537
+ console.warn('objectLayer', objectLayer, existingObjectLayerId ? '(UPDATE MODE)' : '(CREATE MODE)');
538
+
539
+ if (Elements.Data.user.main.model.user.role === 'guest') {
540
+ NotificationManager.Push({
541
+ html: 'Guests cannot save object layers. Please log in.',
542
+ status: 'warning',
543
+ });
544
+ return;
545
+ }
546
+
547
+ // Upload images
548
+ {
549
+ // Get all direction codes that currently have frames
550
+ const directionCodesToUpload = Object.keys(ObjectLayerEngineModal.ObjectLayerData);
551
+
552
+ // In UPDATE mode, also include original direction codes that may have been cleared
553
+ const allDirectionCodes = existingObjectLayerId
554
+ ? [...new Set([...directionCodesToUpload, ...originalDirectionCodes])]
555
+ : directionCodesToUpload;
556
+
557
+ console.warn(
558
+ `Uploading frames for ${allDirectionCodes.length} directions:`,
559
+ allDirectionCodes,
560
+ existingObjectLayerId ? '(UPDATE MODE)' : '(CREATE MODE)',
561
+ );
562
+
563
+ for (const directionCode of allDirectionCodes) {
564
+ const frames = ObjectLayerEngineModal.ObjectLayerData[directionCode] || [];
565
+ console.warn(`Direction ${directionCode}: ${frames.length} frames`);
566
+
567
+ // Create FormData with ALL frames for this direction
568
+ const form = new FormData();
569
+ let frameIndex = -1;
570
+ for (const frame of frames) {
571
+ frameIndex++;
572
+ const pngBlob = frame.image;
573
+
574
+ if (!pngBlob) {
575
+ console.error(`Frame ${frameIndex} in direction ${directionCode} has no image blob!`);
576
+ continue;
577
+ }
578
+
579
+ // Append all frames to the same FormData
580
+ form.append(directionCode, pngBlob, `${frameIndex}.png`);
581
+ }
582
+
583
+ // Send all frames for this direction in one request (even if empty, to remove frames)
584
+ try {
585
+ if (existingObjectLayerId) {
586
+ // UPDATE: use PUT endpoint with object layer ID
587
+ const { status, data } = await ObjectLayerService.put({
588
+ id: `${existingObjectLayerId}/frame-image/${objectLayer.data.item.type}/${objectLayer.data.item.id}/${directionCode}`,
181
589
  body: form,
182
590
  headerId: 'file',
183
591
  });
592
+ console.warn(`Updated ${frames.length} frames for direction ${directionCode}`);
593
+ } else {
594
+ // CREATE: use POST endpoint (only if frames exist)
595
+ if (frames.length > 0) {
596
+ const { status, data } = await ObjectLayerService.post({
597
+ id: `frame-image/${objectLayer.data.item.type}/${objectLayer.data.item.id}/${directionCode}`,
598
+ body: form,
599
+ headerId: 'file',
600
+ });
601
+ console.warn(`Created ${frames.length} frames for direction ${directionCode}`);
602
+ }
184
603
  }
604
+ } catch (error) {
605
+ console.error(`Error uploading frames for direction ${directionCode}:`, error);
606
+ NotificationManager.Push({
607
+ html: `Error uploading frames for direction ${directionCode}: ${error.message}`,
608
+ status: 'error',
609
+ });
610
+ return;
185
611
  }
186
612
  }
187
613
 
188
- // Upload metadata
189
- {
190
- delete objectLayer.data.render.frames;
191
- delete objectLayer.data.render.color;
192
- const { status, data } = await ObjectLayerService.post({
614
+ console.warn('All frames uploaded successfully');
615
+ }
616
+
617
+ // Upload metadata
618
+ {
619
+ delete objectLayer.data.render.frames;
620
+ delete objectLayer.data.render.color;
621
+
622
+ let response;
623
+ if (existingObjectLayerId) {
624
+ // UPDATE existing object layer
625
+ console.warn(
626
+ 'PUT path:',
627
+ `${existingObjectLayerId}/metadata/${objectLayer.data.item.type}/${objectLayer.data.item.id}`,
628
+ );
629
+ response = await ObjectLayerService.put({
630
+ id: `${existingObjectLayerId}/metadata/${objectLayer.data.item.type}/${objectLayer.data.item.id}`,
631
+ body: objectLayer,
632
+ });
633
+ } else {
634
+ // CREATE new object layer
635
+ response = await ObjectLayerService.post({
193
636
  id: `metadata/${objectLayer.data.item.type}/${objectLayer.data.item.id}`,
194
637
  body: objectLayer,
195
638
  });
196
639
  }
197
- });
640
+
641
+ const { status, data, message } = response;
642
+
643
+ if (status === 'success') {
644
+ NotificationManager.Push({
645
+ html: `Object layer "${objectLayer.data.item.id}" ${existingObjectLayerId ? 'updated' : 'created'} successfully!`,
646
+ status: 'success',
647
+ });
648
+ ObjectLayerEngineModal.toManagement();
649
+ } else {
650
+ NotificationManager.Push({
651
+ html: `Error ${existingObjectLayerId ? 'updating' : 'creating'} object layer: ${message}`,
652
+ status: 'error',
653
+ });
654
+ }
655
+ }
198
656
  });
199
- directionsCodeBarRender += html`
200
- <div class="in section-mp-border">
201
- <div class="fl">
202
- <div class="in fll">
203
- <div class="in direction-code-bar-frames-title">${directionCode}</div>
204
- <div class="in direction-code-bar-frames-btn">
205
- ${await BtnIcon.Render({
206
- label: html`<i class="fa-solid fa-plus"></i>`,
207
- class: `direction-code-bar-frames-btn-${directionCode}`,
208
- })}
657
+
658
+ // Add reset button event listener
659
+ EventsUI.onClick(`.ol-btn-reset`, async () => {
660
+ const confirmResult = await Modal.RenderConfirm({
661
+ html: async () => {
662
+ return html`
663
+ <div class="in section-mp" style="text-align: center">
664
+ Are you sure you want to reset the form? All unsaved data will be lost.
209
665
  </div>
210
- </div>
211
- <div class="frames-${directionCode}"></div>
212
- </div>
213
- </div>
214
- `;
215
- }
666
+ `;
667
+ },
668
+ id: `reset-ol-modal-confirm`,
669
+ });
216
670
 
217
- let statsInputsRender = '';
218
- for (const statType of statTypes) {
219
- statsInputsRender += html`
220
- ${await Input.Render({
221
- id: `ol-input-item-stats-${statType}`,
222
- label: html`<div class="inl" style="width: 120px; font-size: 16px; overflow: hidden">
223
- <i class="fa-solid fa-chart-simple"></i> ${statType}
224
- </div>`,
225
- containerClass: 'inl',
226
- type: 'number',
227
- min: 0,
228
- max: 10,
229
- placeholder: true,
230
- value: 0,
231
- })}
232
- `;
233
- }
671
+ if (confirmResult.status === 'confirm') {
672
+ NotificationManager.Push({
673
+ html: 'Resetting form to create new object layer...',
674
+ status: 'info',
675
+ });
676
+
677
+ // Clear all data
678
+ ObjectLayerEngineModal.clearData();
679
+
680
+ setPath(`${getProxyPath()}object-layer-engine`);
681
+
682
+ // Reload the modal
683
+ await ObjectLayerEngineModal.Reload();
684
+
685
+ NotificationManager.Push({
686
+ html: 'Form reset! Ready to create new object layer.',
687
+ status: 'success',
688
+ });
689
+ }
690
+ });
691
+ });
234
692
 
235
693
  return html`
236
694
  <style>
@@ -243,6 +701,7 @@ const ObjectLayerEngineModal = {
243
701
  width: 100px;
244
702
  height: auto;
245
703
  margin: 3px;
704
+ cursor: pointer;
246
705
  }
247
706
  .direction-code-bar-trash-btn {
248
707
  top: 3px;
@@ -251,9 +710,16 @@ const ObjectLayerEngineModal = {
251
710
  color: white;
252
711
  }
253
712
  .ol-btn-save {
713
+ width: 120px;
254
714
  padding: 0.5rem;
255
- font-size: 30px;
256
- font-weight: bold;
715
+ font-size: 20px;
716
+ min-height: 50px;
717
+ }
718
+ .ol-btn-reset {
719
+ width: 120px;
720
+ padding: 0.5rem;
721
+ font-size: 20px;
722
+ min-height: 50px;
257
723
  }
258
724
  .ol-number-label {
259
725
  width: 120px;
@@ -264,10 +730,34 @@ const ObjectLayerEngineModal = {
264
730
  .sub-title-modal {
265
731
  color: #ffcc00;
266
732
  }
733
+ .stat-label-container {
734
+ display: flex;
735
+ align-items: center;
736
+ }
737
+ .stat-info-icon {
738
+ cursor: default;
739
+ }
740
+ .stat-description {
741
+ padding: 2px 5px;
742
+ border-left: 2px solid #444;
743
+ margin-bottom: 5px;
744
+ max-width: 200px;
745
+ }
746
+ .frame-editor-container-loading {
747
+ width: 100%;
748
+ height: 150px;
749
+ color: #ffcc00;
750
+ }
751
+ .loading-text {
752
+ font-family: 'retro-font';
753
+ font-size: 26px;
754
+ }
267
755
  </style>
268
- ${borderChar(2, 'black', ['.sub-title-modal'])}
269
-
270
- <div class="in section-mp section-mp-border">
756
+ ${borderChar(2, 'black', ['.sub-title-modal', '.frame-editor-container-loading'])}
757
+ <div class="in frame-editor-container-loading">
758
+ <div class="abs center frame-editor-container-loading-center"></div>
759
+ </div>
760
+ <div class="in section-mp section-mp-border frame-editor-container">
271
761
  <div class="in sub-title-modal"><i class="fa-solid fa-table-cells-large"></i> Frame editor</div>
272
762
 
273
763
  <object-layer-engine id="ole" width="${cells}" height="${cells}" pixel-size="${pixelSize}">
@@ -346,16 +836,18 @@ const ObjectLayerEngineModal = {
346
836
  label: html`<i class="fa-solid fa-pen-to-square"></i> ${Translate.Render('item-id')}`,
347
837
  containerClass: '',
348
838
  placeholder: true,
839
+ value: loadedData?.metadata?.data?.item?.id || '',
349
840
  })}
350
841
  ${await Input.Render({
351
842
  id: `ol-input-item-description`,
352
843
  label: html`<i class="fa-solid fa-pen-to-square"></i> ${Translate.Render('item-description')}`,
353
844
  containerClass: '',
354
845
  placeholder: true,
846
+ value: loadedData?.metadata?.data?.item?.description || '',
355
847
  })}
356
848
  <div class="in section-mp">
357
849
  ${await DropDown.Render({
358
- value: itemTypes[0],
850
+ value: selectItemType,
359
851
  label: html`${Translate.Render('select-item-type')}`,
360
852
  data: itemTypes.map((itemType) => {
361
853
  return {
@@ -398,12 +890,17 @@ const ObjectLayerEngineModal = {
398
890
  </div>
399
891
  </div>
400
892
 
401
- <div class="in section-mp">
893
+ <div class="fl section-mp">
402
894
  ${await BtnIcon.Render({
403
- label: html`<i class="fa-solid fa-save"></i> ${Translate.Render('save')}`,
895
+ label: html`<i class="submit-btn-icon fa-solid fa-folder-open"></i> ${Translate.Render('save')}`,
404
896
  class: `in flr ol-btn-save`,
405
897
  })}
898
+ ${await BtnIcon.Render({
899
+ label: html`<i class="submit-btn-icon fa-solid fa-broom"></i> ${Translate.Render('reset')}`,
900
+ class: `in flr ol-btn-reset`,
901
+ })}
406
902
  </div>
903
+ <div class="in section-mp"></div>
407
904
  `;
408
905
  },
409
906
  getDirectionsFromDirectionCode(directionCode = '08') {
@@ -438,6 +935,47 @@ const ObjectLayerEngineModal = {
438
935
 
439
936
  return objectLayerFrameDirections;
440
937
  },
938
+ toManagement: async () => {
939
+ await ObjectLayerEngineModal.clearData();
940
+ const queryParams = getQueryParams();
941
+ queryParams.page = 1;
942
+ setQueryParams(queryParams);
943
+ const managerComponent = DefaultManagement.Tokens['modal-object-layer-engine-management'];
944
+ if (managerComponent) {
945
+ managerComponent.page = 1;
946
+ if (!managerComponent.readyRowDataEvent) managerComponent.readyRowDataEvent = {};
947
+ let readyLoad = false;
948
+ const gridId = 'object-layer-engine-management-grid-modal-object-layer-engine-management';
949
+ managerComponent.readyRowDataEvent['object-layer-engine-management'] = async () => {
950
+ if (readyLoad) {
951
+ AgGrid.grids[gridId].setGridOption('getRowClass', null);
952
+ return delete managerComponent.readyRowDataEvent['object-layer-engine-management'];
953
+ }
954
+
955
+ AgGrid.grids[gridId].setGridOption('getRowClass', (params) => {
956
+ if (params.node.rowIndex === 0) {
957
+ return 'row-new-highlight';
958
+ }
959
+ });
960
+ readyLoad = true;
961
+ };
962
+ }
963
+
964
+ const _s = s(`.management-table-btn-reload-modal-object-layer-engine-management`);
965
+ if (_s) _s.click();
966
+
967
+ s(`.main-btn-object-layer-engine-management`).click();
968
+ },
969
+ Reload: async function () {
970
+ // Clear data before reload to prevent contamination
971
+ ObjectLayerEngineModal.clearData();
972
+ const idModal = 'modal-object-layer-engine';
973
+ if (s(`.modal-object-layer-engine`))
974
+ Modal.writeHTML({
975
+ idModal,
976
+ html: await Modal.Data[idModal].options.html(),
977
+ });
978
+ },
441
979
  };
442
980
 
443
981
  export { ObjectLayerEngineModal };