cyberia 3.2.5 → 3.2.9

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 (301) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +2 -2
  2. package/.github/workflows/release.cd.yml +1 -2
  3. package/CHANGELOG.md +351 -1
  4. package/CLI-HELP.md +40 -13
  5. package/Dockerfile +0 -4
  6. package/README.md +242 -497
  7. package/bin/build.js +19 -5
  8. package/bin/cyberia.js +1149 -240
  9. package/bin/deploy.js +570 -1
  10. package/bin/file.js +6 -0
  11. package/bin/index.js +1149 -240
  12. package/bin/vs.js +1 -1
  13. package/conf.js +67 -89
  14. package/deployment.yaml +4 -222
  15. package/hardhat/package-lock.json +32 -32
  16. package/hardhat/package.json +3 -3
  17. package/jsconfig.json +1 -1
  18. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  19. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  20. package/manifests/deployment/dd-cyberia-development/deployment.yaml +4 -222
  21. package/manifests/deployment/dd-cyberia-development/proxy.yaml +10 -118
  22. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  23. package/manifests/deployment/dd-test-development/deployment.yaml +136 -66
  24. package/manifests/deployment/dd-test-development/proxy.yaml +41 -5
  25. package/package.json +23 -14
  26. package/proxy.yaml +10 -118
  27. package/scripts/k3s-node-setup.sh +2 -2
  28. package/scripts/nat-iptables.sh +103 -18
  29. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +18 -18
  30. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -14
  31. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +76 -21
  32. package/src/api/core/core.controller.js +10 -10
  33. package/src/api/core/core.service.js +10 -10
  34. package/src/api/crypto/crypto.controller.js +8 -8
  35. package/src/api/crypto/crypto.service.js +8 -8
  36. package/src/api/cyberia-action/cyberia-action.controller.js +74 -0
  37. package/src/api/cyberia-action/cyberia-action.model.js +87 -0
  38. package/src/api/cyberia-action/cyberia-action.router.js +27 -0
  39. package/src/api/cyberia-action/cyberia-action.service.js +42 -0
  40. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +13 -13
  41. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +11 -11
  42. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +2 -2
  43. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +16 -16
  44. package/src/api/cyberia-entity/cyberia-entity.controller.js +10 -10
  45. package/src/api/cyberia-entity/cyberia-entity.service.js +10 -10
  46. package/src/api/cyberia-instance/cyberia-fallback-world.js +19 -209
  47. package/src/api/cyberia-instance/cyberia-instance.controller.js +14 -14
  48. package/src/api/cyberia-instance/cyberia-instance.model.js +3 -0
  49. package/src/api/cyberia-instance/cyberia-instance.service.js +22 -57
  50. package/src/api/cyberia-instance/cyberia-portal-connector.js +20 -246
  51. package/src/api/cyberia-instance/cyberia-world-generator.js +505 -0
  52. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +10 -10
  53. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +216 -55
  54. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +4 -1
  55. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +18 -14
  56. package/src/api/cyberia-map/cyberia-map.controller.js +10 -10
  57. package/src/api/cyberia-map/cyberia-map.service.js +10 -10
  58. package/src/api/cyberia-quest/cyberia-quest.controller.js +74 -0
  59. package/src/api/cyberia-quest/cyberia-quest.model.js +67 -0
  60. package/src/api/cyberia-quest/cyberia-quest.router.js +27 -0
  61. package/src/api/cyberia-quest/cyberia-quest.service.js +42 -0
  62. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +74 -0
  63. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +49 -0
  64. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +27 -0
  65. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +42 -0
  66. package/src/api/default/default.controller.js +10 -10
  67. package/src/api/default/default.service.js +10 -10
  68. package/src/api/document/document.controller.js +12 -12
  69. package/src/api/document/document.model.js +10 -16
  70. package/src/api/file/file.controller.js +8 -8
  71. package/src/api/file/file.model.js +10 -10
  72. package/src/api/file/file.service.js +36 -36
  73. package/src/api/instance/instance.controller.js +10 -10
  74. package/src/api/instance/instance.model.js +4 -10
  75. package/src/api/instance/instance.service.js +10 -10
  76. package/src/api/ipfs/ipfs.controller.js +12 -12
  77. package/src/api/ipfs/ipfs.model.js +4 -13
  78. package/src/api/ipfs/ipfs.service.js +14 -28
  79. package/src/api/object-layer/object-layer.controller.js +12 -12
  80. package/src/api/object-layer/object-layer.model.js +4 -17
  81. package/src/api/object-layer/object-layer.service.js +12 -12
  82. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +10 -10
  83. package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +6 -16
  84. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +18 -14
  85. package/src/api/test/test.controller.js +8 -8
  86. package/src/api/test/test.service.js +8 -8
  87. package/src/api/user/guest.service.js +99 -0
  88. package/src/api/user/user.controller.js +6 -6
  89. package/src/api/user/user.model.js +8 -13
  90. package/src/api/user/user.service.js +3 -20
  91. package/src/cli/cluster.js +61 -14
  92. package/src/cli/db.js +47 -2
  93. package/src/cli/deploy.js +67 -35
  94. package/src/cli/fs.js +79 -8
  95. package/src/cli/image.js +43 -1
  96. package/src/cli/index.js +26 -1
  97. package/src/cli/release.js +57 -1
  98. package/src/cli/repository.js +69 -31
  99. package/src/cli/run.js +415 -36
  100. package/src/cli/ssh.js +1 -1
  101. package/src/cli/static.js +43 -115
  102. package/src/client/Cryptokoyn.index.js +18 -21
  103. package/src/client/CyberiaPortal.index.js +19 -23
  104. package/src/client/Default.index.js +21 -33
  105. package/src/client/Itemledger.index.js +20 -26
  106. package/src/client/Underpost.index.js +19 -23
  107. package/src/client/components/core/404.js +4 -4
  108. package/src/client/components/core/500.js +4 -4
  109. package/src/client/components/core/Account.js +73 -60
  110. package/src/client/components/core/AgGrid.js +23 -33
  111. package/src/client/components/core/Alert.js +12 -13
  112. package/src/client/components/core/AppStore.js +1 -1
  113. package/src/client/components/core/Auth.js +35 -37
  114. package/src/client/components/core/Badge.js +7 -13
  115. package/src/client/components/core/BtnIcon.js +15 -17
  116. package/src/client/components/core/CalendarCore.js +42 -63
  117. package/src/client/components/core/Chat.js +13 -15
  118. package/src/client/components/core/ClientEvents.js +87 -0
  119. package/src/client/components/core/ColorPaletteElement.js +309 -0
  120. package/src/client/components/core/Content.js +17 -14
  121. package/src/client/components/core/Css.js +15 -71
  122. package/src/client/components/core/CssCore.js +12 -16
  123. package/src/client/components/core/D3Chart.js +4 -4
  124. package/src/client/components/core/Docs.js +64 -91
  125. package/src/client/components/core/DropDown.js +69 -91
  126. package/src/client/components/core/EventBus.js +92 -0
  127. package/src/client/components/core/EventsUI.js +14 -17
  128. package/src/client/components/core/FileExplorer.js +96 -228
  129. package/src/client/components/core/FullScreen.js +47 -75
  130. package/src/client/components/core/Input.js +24 -69
  131. package/src/client/components/core/Keyboard.js +25 -18
  132. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  133. package/src/client/components/core/LoadingAnimation.js +25 -31
  134. package/src/client/components/core/LogIn.js +41 -41
  135. package/src/client/components/core/LogOut.js +23 -14
  136. package/src/client/components/core/Modal.js +462 -178
  137. package/src/client/components/core/NotificationManager.js +14 -18
  138. package/src/client/components/core/Panel.js +54 -50
  139. package/src/client/components/core/PanelForm.js +25 -125
  140. package/src/client/components/core/Polyhedron.js +110 -214
  141. package/src/client/components/core/PublicProfile.js +39 -32
  142. package/src/client/components/core/Recover.js +48 -44
  143. package/src/client/components/core/Responsive.js +88 -32
  144. package/src/client/components/core/RichText.js +9 -18
  145. package/src/client/components/core/Router.js +24 -3
  146. package/src/client/components/core/SearchBox.js +37 -37
  147. package/src/client/components/core/SignUp.js +39 -30
  148. package/src/client/components/core/SocketIo.js +31 -2
  149. package/src/client/components/core/SocketIoHandler.js +6 -6
  150. package/src/client/components/core/ToggleSwitch.js +8 -20
  151. package/src/client/components/core/ToolTip.js +5 -17
  152. package/src/client/components/core/Translate.js +56 -59
  153. package/src/client/components/core/Validator.js +26 -16
  154. package/src/client/components/core/Wallet.js +15 -26
  155. package/src/client/components/core/Worker.js +163 -27
  156. package/src/client/components/core/windowGetDimensions.js +7 -7
  157. package/src/client/components/cryptokoyn/{MenuCryptokoyn.js → AppShellCryptokoyn.js} +57 -57
  158. package/src/client/components/cryptokoyn/CssCryptokoyn.js +15 -15
  159. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +6 -4
  160. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +6 -4
  161. package/src/client/components/cryptokoyn/RouterCryptokoyn.js +37 -0
  162. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +4 -4
  163. package/src/client/components/cryptokoyn/SignUpCryptokoyn.js +6 -4
  164. package/src/client/components/cyberia/InstanceEngineCyberia.js +141 -60
  165. package/src/client/components/cyberia/MapEngineCyberia.js +691 -214
  166. package/src/client/components/cyberia/ObjectLayerEngine.js +19 -0
  167. package/src/client/components/cyberia/ObjectLayerEngineModal.js +1204 -94
  168. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +196 -298
  169. package/src/client/components/cyberia-portal/{MenuCyberiaPortal.js → AppShellCyberiaPortal.js} +102 -102
  170. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +305 -61
  171. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +15 -15
  172. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +6 -4
  173. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +6 -4
  174. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +4 -4
  175. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +60 -0
  176. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +4 -4
  177. package/src/client/components/cyberia-portal/SignUpCyberiaPortal.js +6 -4
  178. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -4
  179. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +87 -87
  180. package/src/client/components/default/CssDefault.js +12 -12
  181. package/src/client/components/default/LogInDefault.js +6 -4
  182. package/src/client/components/default/LogOutDefault.js +6 -4
  183. package/src/client/components/default/RouterDefault.js +47 -0
  184. package/src/client/components/default/SettingsDefault.js +4 -4
  185. package/src/client/components/default/SignUpDefault.js +6 -4
  186. package/src/client/components/default/TranslateDefault.js +3 -3
  187. package/src/client/components/itemledger/{MenuItemledger.js → AppShellItemledger.js} +57 -57
  188. package/src/client/components/itemledger/CssItemledger.js +15 -15
  189. package/src/client/components/itemledger/LogInItemledger.js +6 -4
  190. package/src/client/components/itemledger/LogOutItemledger.js +6 -4
  191. package/src/client/components/itemledger/RouterItemledger.js +38 -0
  192. package/src/client/components/itemledger/SettingsItemledger.js +4 -4
  193. package/src/client/components/itemledger/SignUpItemledger.js +6 -4
  194. package/src/client/components/itemledger/TranslateItemledger.js +3 -3
  195. package/src/client/components/underpost/{MenuUnderpost.js → AppShellUnderpost.js} +88 -88
  196. package/src/client/components/underpost/CssUnderpost.js +14 -14
  197. package/src/client/components/underpost/CyberpunkBloggerUnderpost.js +4 -4
  198. package/src/client/components/underpost/DocumentSearchProvider.js +1 -1
  199. package/src/client/components/underpost/LabGalleryUnderpost.js +12 -15
  200. package/src/client/components/underpost/LogInUnderpost.js +6 -4
  201. package/src/client/components/underpost/LogOutUnderpost.js +6 -4
  202. package/src/client/components/underpost/RouterUnderpost.js +45 -0
  203. package/src/client/components/underpost/SettingsUnderpost.js +4 -4
  204. package/src/client/components/underpost/SignUpUnderpost.js +6 -4
  205. package/src/client/components/underpost/TranslateUnderpost.js +4 -4
  206. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +235 -0
  207. package/src/client/public/cyberia-docs/ARCHITECTURE.md +443 -0
  208. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +417 -0
  209. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +313 -0
  210. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +260 -0
  211. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +241 -0
  212. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +300 -0
  213. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +279 -0
  214. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +206 -0
  215. package/src/client/public/cyberia-docs/ROADMAP.md +240 -0
  216. package/src/client/public/cyberia-docs/WHITE-PAPER.md +732 -0
  217. package/src/client/services/atlas-sprite-sheet/atlas-sprite-sheet.service.js +14 -20
  218. package/src/client/services/core/core.service.js +17 -49
  219. package/src/client/services/crypto/crypto.service.js +8 -13
  220. package/src/client/services/cyberia-action/cyberia-action.service.js +99 -0
  221. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +10 -16
  222. package/src/client/services/cyberia-entity/cyberia-entity.management.js +5 -5
  223. package/src/client/services/cyberia-entity/cyberia-entity.service.js +10 -16
  224. package/src/client/services/cyberia-instance/cyberia-instance.management.js +6 -6
  225. package/src/client/services/cyberia-instance/cyberia-instance.service.js +12 -18
  226. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +10 -16
  227. package/src/client/services/cyberia-map/cyberia-map.management.js +6 -6
  228. package/src/client/services/cyberia-map/cyberia-map.service.js +12 -18
  229. package/src/client/services/cyberia-quest/cyberia-quest.service.js +99 -0
  230. package/src/client/services/cyberia-quest-progress/cyberia-quest-progress.service.js +99 -0
  231. package/src/client/services/default/default.management.js +159 -267
  232. package/src/client/services/default/default.service.js +10 -16
  233. package/src/client/services/document/document.service.js +14 -19
  234. package/src/client/services/file/file.service.js +8 -13
  235. package/src/client/services/instance/instance.management.js +5 -5
  236. package/src/client/services/instance/instance.service.js +10 -15
  237. package/src/client/services/ipfs/ipfs.service.js +12 -18
  238. package/src/client/services/object-layer/object-layer.management.js +12 -12
  239. package/src/client/services/object-layer/object-layer.service.js +20 -26
  240. package/src/client/services/object-layer-render-frames/object-layer-render-frames.service.js +10 -16
  241. package/src/client/services/test/test.service.js +8 -13
  242. package/src/client/services/user/guest.service.js +86 -0
  243. package/src/client/services/user/user.management.js +5 -5
  244. package/src/client/services/user/user.service.js +14 -20
  245. package/src/client/ssr/body/404.js +3 -3
  246. package/src/client/ssr/body/500.js +3 -3
  247. package/src/client/ssr/body/CacheControl.js +5 -2
  248. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  249. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +13 -6
  250. package/src/client/ssr/head/PwaItemledger.js +197 -60
  251. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  252. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  253. package/src/client/ssr/offline/Maintenance.js +12 -11
  254. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  255. package/src/client/ssr/pages/Test.js +2 -2
  256. package/src/client/sw/core.sw.js +212 -0
  257. package/src/grpc/cyberia/grpc-server.js +179 -67
  258. package/src/index.js +1 -1
  259. package/src/runtime/cyberia-client/Dockerfile +80 -0
  260. package/src/runtime/cyberia-server/Dockerfile +37 -0
  261. package/src/runtime/express/Dockerfile +4 -4
  262. package/src/runtime/lampp/Dockerfile +8 -7
  263. package/src/runtime/wp/Dockerfile +11 -17
  264. package/src/server/atlas-sprite-sheet-generator.js +4 -2
  265. package/src/server/client-build-docs.js +45 -46
  266. package/src/server/client-build.js +334 -60
  267. package/src/server/client-formatted.js +47 -16
  268. package/src/server/conf.js +5 -4
  269. package/src/server/data-query.js +32 -20
  270. package/src/server/dns.js +22 -0
  271. package/src/server/ipfs-client.js +232 -91
  272. package/src/server/object-layer.js +1 -6
  273. package/src/server/process.js +13 -27
  274. package/src/server/semantic-layer-generator-floor.js +11 -51
  275. package/src/server/semantic-layer-generator-resource.js +259 -0
  276. package/src/server/semantic-layer-generator-skin.js +41 -171
  277. package/src/server/semantic-layer-generator.js +122 -14
  278. package/src/server/shape-generator.js +108 -0
  279. package/src/server/start.js +17 -3
  280. package/src/server/valkey.js +141 -235
  281. package/tsconfig.docs.json +15 -0
  282. package/typedoc.dd-cyberia.json +29 -0
  283. package/typedoc.json +29 -0
  284. package/WHITE-PAPER.md +0 -1540
  285. package/hardhat/README.md +0 -531
  286. package/hardhat/WHITE-PAPER.md +0 -1540
  287. package/jsdoc.dd-cyberia.json +0 -68
  288. package/jsdoc.json +0 -68
  289. package/src/api/object-layer/README.md +0 -672
  290. package/src/client/components/core/ColorPalette.js +0 -5267
  291. package/src/client/components/core/JoyStick.js +0 -80
  292. package/src/client/components/cryptokoyn/RoutesCryptokoyn.js +0 -39
  293. package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +0 -62
  294. package/src/client/components/cyberia-portal/ServerCyberiaPortal.js +0 -136
  295. package/src/client/components/default/RoutesDefault.js +0 -49
  296. package/src/client/components/itemledger/RoutesItemledger.js +0 -40
  297. package/src/client/components/underpost/RoutesUnderpost.js +0 -47
  298. package/src/client/sw/default.sw.js +0 -127
  299. package/src/client/sw/template.sw.js +0 -84
  300. package/src/grpc/cyberia/OFF_CHAIN_ECONOMY.md +0 -305
  301. package/src/grpc/cyberia/README.md +0 -326
@@ -1,6 +1,6 @@
1
1
  import { CoreService } from '../../services/core/core.service.js';
2
2
  import { BtnIcon } from '../core/BtnIcon.js';
3
- import { borderChar, dynamicCol } from '../core/Css.js';
3
+ import { borderChar, Css, dynamicCol, Themes } from '../core/Css.js';
4
4
  import { DropDown } from '../core/DropDown.js';
5
5
  import { EventsUI } from '../core/EventsUI.js';
6
6
  import { Translate } from '../core/Translate.js';
@@ -16,22 +16,525 @@ import { Modal } from '../core/Modal.js';
16
16
  import { LoadingAnimation } from '../core/LoadingAnimation.js';
17
17
  import { DefaultManagement } from '../../services/default/default.management.js';
18
18
  import * as _ from '../cyberia/ObjectLayerEngine.js';
19
+ import '../core/ColorPaletteElement.js';
19
20
 
20
- const ObjectLayerEngineModal = {
21
- selectItemType: 'skin',
22
- itemActivable: false,
23
- renderIsStateless: false,
24
- renderFrameDuration: 100,
25
- existingObjectLayerId: null,
26
- originalDirectionCodes: [],
27
- templates: [
21
+ const CANVAS_BEHAVIOR_ICON = 'fa-solid fa-shapes';
22
+
23
+ const DISTORTION_TYPES = Object.freeze([
24
+ {
25
+ value: 'position-jitter',
26
+ label: 'Position Jitter',
27
+ group: 'distortion',
28
+ icon: CANVAS_BEHAVIOR_ICON,
29
+ },
30
+ {
31
+ value: 'rotation-drift',
32
+ label: 'Rotation Drift',
33
+ group: 'distortion',
34
+ icon: CANVAS_BEHAVIOR_ICON,
35
+ },
36
+ {
37
+ value: 'scale-wobble',
38
+ label: 'Scale Wobble',
39
+ group: 'distortion',
40
+ icon: CANVAS_BEHAVIOR_ICON,
41
+ },
42
+ {
43
+ value: 'particle-drift',
44
+ label: 'Particle Drift',
45
+ group: 'distortion',
46
+ icon: CANVAS_BEHAVIOR_ICON,
47
+ },
48
+ ]);
49
+
50
+ const MOSAIC_TYPES = Object.freeze([
51
+ {
52
+ value: 'mosaic-diamond-checker',
53
+ label: 'Diamond Checker',
54
+ group: 'mosaic',
55
+ icon: CANVAS_BEHAVIOR_ICON,
56
+ },
57
+ {
58
+ value: 'mosaic-rhombus-lattice',
59
+ label: 'Rhombus Lattice',
60
+ group: 'mosaic',
61
+ icon: CANVAS_BEHAVIOR_ICON,
62
+ },
63
+ {
64
+ value: 'mosaic-zigzag-rows',
65
+ label: 'Zigzag Rows',
66
+ group: 'mosaic',
67
+ icon: CANVAS_BEHAVIOR_ICON,
68
+ },
69
+ {
70
+ value: 'mosaic-staggered-tiles',
71
+ label: 'Staggered Tiles',
72
+ group: 'mosaic',
73
+ icon: CANVAS_BEHAVIOR_ICON,
74
+ },
75
+ {
76
+ value: 'mosaic-brick-offset',
77
+ label: 'Brick Offset',
78
+ group: 'mosaic',
79
+ icon: CANVAS_BEHAVIOR_ICON,
80
+ },
81
+ ]);
82
+
83
+ const CANVAS_BEHAVIORS = Object.freeze([...DISTORTION_TYPES, ...MOSAIC_TYPES]);
84
+
85
+ const DEFAULT_DISTORTION_TYPE = DISTORTION_TYPES[0].value;
86
+ const DEFAULT_DISTORTION_STATUS =
87
+ 'Applies the selected canvas behavior directly to the current editor frame. factorA controls distortion density or mosaic tile scale.';
88
+ const DEFAULT_DISTORTION_FACTOR_A = 0.12;
89
+ const DEFAULT_STAT_RANDOM_MIN = 0;
90
+ const DEFAULT_STAT_RANDOM_MAX = 10;
91
+ const UNIFORM_OPACITY_TOGGLE_ID = 'ol-uniform-opacity-lock';
92
+ const DIRECTION_PREVIEW_MODAL_ID = 'modal-object-layer-direction-preview';
93
+ const CANVAS_BEHAVIOR_BY_VALUE = Object.freeze(
94
+ Object.fromEntries(CANVAS_BEHAVIORS.map((entry) => [entry.value, entry])),
95
+ );
96
+ const isMosaicBehavior = (value = '') => CANVAS_BEHAVIOR_BY_VALUE[value]?.group === 'mosaic';
97
+ const getCanvasBehaviorDisplay = (value = DEFAULT_DISTORTION_TYPE) => {
98
+ const behavior = CANVAS_BEHAVIOR_BY_VALUE[value] || CANVAS_BEHAVIOR_BY_VALUE[DEFAULT_DISTORTION_TYPE];
99
+ return `<i class="${CANVAS_BEHAVIOR_ICON}"></i> ${behavior.label}`;
100
+ };
101
+
102
+ const hashString = (str = '') => {
103
+ let hash = 0;
104
+ for (let i = 0; i < str.length; i++) {
105
+ hash = (Math.imul(31, hash) + str.charCodeAt(i)) | 0;
106
+ }
107
+ return hash;
108
+ };
109
+
110
+ const clampNumber = (value, min, max) => (value < min ? min : value > max ? max : value);
111
+ const lerp = (start, end, factor) => start + (end - start) * factor;
112
+ const smoothstep = (value) => value * value * (3 - 2 * value);
113
+ const normalizedHash = (seed, x, y) => ((hashString(`${seed}:${x}:${y}`) >>> 0) / 4294967295) * 2 - 1;
114
+
115
+ const sampleSmoothNoise = (seed, x, y) => {
116
+ const x0 = Math.floor(x);
117
+ const y0 = Math.floor(y);
118
+ const x1 = x0 + 1;
119
+ const y1 = y0 + 1;
120
+
121
+ const sx = smoothstep(x - x0);
122
+ const sy = smoothstep(y - y0);
123
+ const n00 = normalizedHash(seed, x0, y0);
124
+ const n10 = normalizedHash(seed, x1, y0);
125
+ const n01 = normalizedHash(seed, x0, y1);
126
+ const n11 = normalizedHash(seed, x1, y1);
127
+
128
+ return lerp(lerp(n00, n10, sx), lerp(n01, n11, sx), sy);
129
+ };
130
+
131
+ const isVisibleCell = (cell) => Array.isArray(cell) && (cell[3] || 0) > 0;
132
+ const cloneMatrix = (matrix = []) => matrix.map((row) => row.map((cell) => cell.slice()));
133
+ const modulo = (value, divisor) => {
134
+ if (!divisor) return 0;
135
+ return ((value % divisor) + divisor) % divisor;
136
+ };
137
+ const normalizeColorCell = (cell = [255, 0, 0, 255]) => [
138
+ clampNumber(Math.round(cell[0] || 0), 0, 255),
139
+ clampNumber(Math.round(cell[1] || 0), 0, 255),
140
+ clampNumber(Math.round(cell[2] || 0), 0, 255),
141
+ clampNumber(Math.round(cell[3] ?? 255), 0, 255),
142
+ ];
143
+ const getRenderedInputNode = (id = '') => s(`.${id}`) || s(`#${id}-name`) || s(`#${id}`);
144
+
145
+ const countChangedCells = (baseMatrix = [], nextMatrix = []) => {
146
+ const height = Math.max(baseMatrix.length, nextMatrix.length);
147
+ const width = Math.max(baseMatrix[0]?.length || 0, nextMatrix[0]?.length || 0);
148
+ let changedCells = 0;
149
+
150
+ for (let y = 0; y < height; y++) {
151
+ for (let x = 0; x < width; x++) {
152
+ const baseCell = baseMatrix[y]?.[x] || [0, 0, 0, 0];
153
+ const nextCell = nextMatrix[y]?.[x] || [0, 0, 0, 0];
154
+ if (
155
+ baseCell[0] !== nextCell[0] ||
156
+ baseCell[1] !== nextCell[1] ||
157
+ baseCell[2] !== nextCell[2] ||
158
+ baseCell[3] !== nextCell[3]
159
+ ) {
160
+ changedCells++;
161
+ }
162
+ }
163
+ }
164
+
165
+ return changedCells;
166
+ };
167
+
168
+ const deriveMatrixLayerSeed = (matrix = []) => {
169
+ const signature = [];
170
+ for (let y = 0; y < matrix.length; y++) {
171
+ for (let x = 0; x < (matrix[y]?.length || 0); x++) {
172
+ const cell = matrix[y][x];
173
+ if (!isVisibleCell(cell)) continue;
174
+ signature.push(`${x}:${y}:${cell.join(',')}`);
175
+ }
176
+ }
177
+ return hashString(`${matrix[0]?.length || 0}x${matrix.length}:${signature.join('|')}`);
178
+ };
179
+
180
+ const colorDistance = (left = [0, 0, 0, 0], right = [0, 0, 0, 0]) => {
181
+ const deltaRed = (left[0] - right[0]) / 255;
182
+ const deltaGreen = (left[1] - right[1]) / 255;
183
+ const deltaBlue = (left[2] - right[2]) / 255;
184
+ const deltaAlpha = ((left[3] || 0) - (right[3] || 0)) / 255;
185
+ return (
186
+ Math.sqrt(deltaRed * deltaRed + deltaGreen * deltaGreen + deltaBlue * deltaBlue + deltaAlpha * deltaAlpha * 0.35) /
187
+ 1.83
188
+ );
189
+ };
190
+
191
+ const getNeighborEntries = (matrix, x, y) => {
192
+ const neighbors = [];
193
+ const sourceCell = matrix[y]?.[x] || [0, 0, 0, 0];
194
+
195
+ for (let offsetY = -1; offsetY <= 1; offsetY++) {
196
+ for (let offsetX = -1; offsetX <= 1; offsetX++) {
197
+ if (offsetX === 0 && offsetY === 0) continue;
198
+
199
+ const neighborX = x + offsetX;
200
+ const neighborY = y + offsetY;
201
+ if (
202
+ neighborY < 0 ||
203
+ neighborX < 0 ||
204
+ neighborY >= matrix.length ||
205
+ neighborX >= (matrix[neighborY]?.length || 0)
206
+ ) {
207
+ continue;
208
+ }
209
+
210
+ const neighborCell = matrix[neighborY][neighborX];
211
+ const visible = isVisibleCell(neighborCell);
212
+ neighbors.push({
213
+ x: neighborX,
214
+ y: neighborY,
215
+ dx: offsetX,
216
+ dy: offsetY,
217
+ cell: neighborCell.slice(),
218
+ visible,
219
+ distance: visible ? colorDistance(sourceCell, neighborCell) : 1,
220
+ });
221
+ }
222
+ }
223
+
224
+ return neighbors;
225
+ };
226
+
227
+ const getDistortionTime = (distortionSeed = 0) => (Math.abs(Number(distortionSeed) || 0) % 4096) / 128 + 1;
228
+
229
+ const getDirectionalVector = ({ x, y, width, height, distortionType, frameSeed, distortionSeed }) => {
230
+ const distortionTime = getDistortionTime(distortionSeed);
231
+ const noiseX = sampleSmoothNoise(frameSeed + 211, x * 0.26 + distortionTime * 0.14, y * 0.26 + 1.9);
232
+ const noiseY = sampleSmoothNoise(frameSeed + 263, x * 0.26 + 7.1, y * 0.26 + distortionTime * 0.14);
233
+ const centerX = (width - 1) / 2;
234
+ const centerY = (height - 1) / 2;
235
+ const deltaX = x - centerX;
236
+ const deltaY = y - centerY;
237
+ const radialX = deltaX === 0 ? 0 : deltaX / Math.max(1, Math.abs(deltaX) + Math.abs(deltaY));
238
+ const radialY = deltaY === 0 ? 0 : deltaY / Math.max(1, Math.abs(deltaX) + Math.abs(deltaY));
239
+
240
+ if (distortionType === 'position-jitter') {
241
+ return { dx: noiseX, dy: noiseY };
242
+ }
243
+
244
+ if (distortionType === 'rotation-drift') {
245
+ const tangentDirection = sampleSmoothNoise(frameSeed + 307, distortionTime * 0.24, 3.7) >= 0 ? 1 : -1;
246
+ return {
247
+ dx: tangentDirection * -radialY + noiseX * 0.3,
248
+ dy: tangentDirection * radialX + noiseY * 0.3,
249
+ };
250
+ }
251
+
252
+ if (distortionType === 'scale-wobble') {
253
+ const radialDirection = sampleSmoothNoise(frameSeed + 359, distortionTime * 0.18, 4.6) >= 0 ? 1 : -1;
254
+ return {
255
+ dx: radialX * radialDirection + noiseX * 0.2,
256
+ dy: radialY * radialDirection + noiseY * 0.2,
257
+ };
258
+ }
259
+
260
+ return {
261
+ dx: (noiseX >= 0 ? 0.6 : -0.6) + noiseX * 0.6,
262
+ dy: (noiseY >= 0 ? 0.6 : -0.6) + noiseY * 0.6,
263
+ };
264
+ };
265
+
266
+ const collectVisibleCells = (matrix = [], layerSeed, frameSeed, distortionType, distortionSeed) => {
267
+ const distortionTime = getDistortionTime(distortionSeed);
268
+ const visibleCells = [];
269
+
270
+ for (let y = 0; y < matrix.length; y++) {
271
+ for (let x = 0; x < (matrix[y]?.length || 0); x++) {
272
+ const cell = matrix[y][x];
273
+ if (!isVisibleCell(cell)) continue;
274
+
275
+ const neighbors = getNeighborEntries(matrix, x, y);
276
+ const visibleNeighbors = neighbors.filter((neighbor) => neighbor.visible);
277
+ const edgeScore = neighbors.some((neighbor) => !neighbor.visible) ? 1 : 0;
278
+ const variationScore = visibleNeighbors.length
279
+ ? visibleNeighbors.reduce((sum, neighbor) => {
280
+ if (neighbor.distance < 0.01) return sum + 0.04;
281
+ if (neighbor.distance <= 0.35) return sum + (1 - Math.abs(neighbor.distance - 0.14) / 0.21);
282
+ if (neighbor.distance <= 0.55) return sum + 0.18;
283
+ return sum + 0.04;
284
+ }, 0) / visibleNeighbors.length
285
+ : 0;
286
+
287
+ const activity = (sampleSmoothNoise(frameSeed + 401, x * 0.24 + distortionTime * 0.1, y * 0.24 + 2.7) + 1) / 2;
288
+ const stabilityBias = 1 - (hashString(`${layerSeed}:${distortionType}:${x}:${y}`) >>> 0) / 4294967295;
289
+
290
+ visibleCells.push({
291
+ x,
292
+ y,
293
+ cell: cell.slice(),
294
+ neighbors,
295
+ edgeScore,
296
+ variationScore,
297
+ score: variationScore * 0.58 + edgeScore * 0.2 + activity * 0.18 + stabilityBias * 0.04,
298
+ });
299
+ }
300
+ }
301
+
302
+ return visibleCells.sort((left, right) => right.score - left.score);
303
+ };
304
+
305
+ const chooseDistortionTarget = ({ entry, matrix, distortionType, frameSeed, distortionSeed, reservedCells }) => {
306
+ const width = matrix[0]?.length || 0;
307
+ const height = matrix.length;
308
+ const vector = getDirectionalVector({
309
+ x: entry.x,
310
+ y: entry.y,
311
+ width,
312
+ height,
313
+ distortionType,
314
+ frameSeed,
315
+ distortionSeed,
316
+ });
317
+ const vectorLength = Math.hypot(vector.dx, vector.dy) || 1;
318
+
319
+ return [...entry.neighbors]
320
+ .map((neighbor) => {
321
+ const alignment =
322
+ (neighbor.dx * vector.dx + neighbor.dy * vector.dy) /
323
+ ((Math.hypot(neighbor.dx, neighbor.dy) || 1) * vectorLength);
324
+ let score = alignment * 0.55;
325
+
326
+ if (!neighbor.visible) {
327
+ score += 0.22 + entry.edgeScore * 0.16;
328
+ } else if (neighbor.distance >= 0.01 && neighbor.distance <= 0.38) {
329
+ score += 0.34 + (0.38 - neighbor.distance) * 0.45;
330
+ } else {
331
+ score -= 0.2;
332
+ }
333
+
334
+ if (distortionType === 'particle-drift' && Math.abs(neighbor.dx) === 1 && Math.abs(neighbor.dy) === 1) {
335
+ score += 0.16;
336
+ }
337
+ if (distortionType === 'rotation-drift' && Math.abs(neighbor.dx) + Math.abs(neighbor.dy) === 1) {
338
+ score += 0.06;
339
+ }
340
+ if (distortionType === 'scale-wobble' && !neighbor.visible) {
341
+ score += 0.08;
342
+ }
343
+ if (reservedCells.has(`${neighbor.x}:${neighbor.y}`)) {
344
+ score = -Infinity;
345
+ }
346
+
347
+ return { ...neighbor, score };
348
+ })
349
+ .sort((left, right) => right.score - left.score)
350
+ .find((candidate) => candidate.score > 0.02);
351
+ };
352
+
353
+ const buildDistortedMatrix = (
354
+ baseMatrix,
355
+ distortionType,
356
+ factorA = DEFAULT_DISTORTION_FACTOR_A,
357
+ distortionSeed = Date.now(),
358
+ ) => {
359
+ const layerSeed = deriveMatrixLayerSeed(baseMatrix);
360
+ const normalizedSeed = Number.isFinite(distortionSeed) ? distortionSeed : hashString(String(distortionSeed));
361
+ const frameSeed = hashString(`${layerSeed}:${distortionType}:${normalizedSeed}`);
362
+ const visibleCells = collectVisibleCells(baseMatrix, layerSeed, frameSeed, distortionType, normalizedSeed);
363
+ const result = cloneMatrix(baseMatrix);
364
+
365
+ if (!visibleCells.length) {
366
+ return { matrix: result, changedCells: 0, appliedDistortions: 0, layerSeed, frameSeed };
367
+ }
368
+
369
+ const densityFactor = clampNumber(Number.isFinite(factorA) ? factorA : DEFAULT_DISTORTION_FACTOR_A, 0.01, 1);
370
+ const maxBudget = Math.max(1, Math.round(visibleCells.length * 0.35));
371
+ const distortionBudget = clampNumber(Math.round(visibleCells.length * densityFactor), 1, maxBudget);
372
+ const reservedCells = new Set();
373
+ let appliedDistortions = 0;
374
+
375
+ for (const entry of visibleCells) {
376
+ if (appliedDistortions >= distortionBudget) break;
377
+
378
+ const sourceKey = `${entry.x}:${entry.y}`;
379
+ if (reservedCells.has(sourceKey)) continue;
380
+
381
+ const target = chooseDistortionTarget({
382
+ entry,
383
+ matrix: baseMatrix,
384
+ distortionType,
385
+ frameSeed,
386
+ distortionSeed: normalizedSeed,
387
+ reservedCells,
388
+ });
389
+
390
+ if (!target) continue;
391
+
392
+ if (target.visible) {
393
+ const nextCell = result[target.y][target.x].slice();
394
+ result[target.y][target.x] = entry.cell.slice();
395
+ result[entry.y][entry.x] = nextCell;
396
+ } else {
397
+ result[target.y][target.x] = entry.cell.slice();
398
+ result[entry.y][entry.x] = [0, 0, 0, 0];
399
+ }
400
+
401
+ reservedCells.add(sourceKey);
402
+ reservedCells.add(`${target.x}:${target.y}`);
403
+ appliedDistortions++;
404
+ }
405
+
406
+ let changedCells = countChangedCells(baseMatrix, result);
407
+
408
+ if (changedCells === 0) {
409
+ for (const entry of visibleCells) {
410
+ const transparentTarget = entry.neighbors.find((neighbor) => !neighbor.visible);
411
+ if (!transparentTarget) continue;
412
+ result[transparentTarget.y][transparentTarget.x] = entry.cell.slice();
413
+ result[entry.y][entry.x] = [0, 0, 0, 0];
414
+ appliedDistortions = 1;
415
+ changedCells = countChangedCells(baseMatrix, result);
416
+ break;
417
+ }
418
+ }
419
+
420
+ return {
421
+ matrix: result,
422
+ changedCells,
423
+ appliedDistortions,
424
+ densityFactor,
425
+ layerSeed,
426
+ frameSeed,
427
+ };
428
+ };
429
+
430
+ const getMosaicTileSize = (width, height, factorA = DEFAULT_DISTORTION_FACTOR_A) => {
431
+ const minDimension = Math.max(1, Math.min(width, height));
432
+ const normalizedFactor = clampNumber(Number.isFinite(factorA) ? factorA : DEFAULT_DISTORTION_FACTOR_A, 0.01, 1);
433
+ return clampNumber(
434
+ Math.round(1 + normalizedFactor * Math.max(2, Math.min(10, Math.floor(minDimension / 2)))),
435
+ 1,
436
+ minDimension,
437
+ );
438
+ };
439
+
440
+ const buildMosaicMatrix = (
441
+ baseMatrix,
442
+ mosaicType,
443
+ factorA = DEFAULT_DISTORTION_FACTOR_A,
444
+ brushCell = [255, 0, 0, 255],
445
+ ) => {
446
+ const height = baseMatrix.length;
447
+ const width = baseMatrix[0]?.length || 0;
448
+ const result = cloneMatrix(baseMatrix);
449
+
450
+ if (!height || !width) {
451
+ return { matrix: result, changedCells: 0, paintedCells: 0, tileSize: 1 };
452
+ }
453
+
454
+ const tileSize = getMosaicTileSize(width, height, factorA);
455
+ const gap = Math.max(1, Math.floor(tileSize / 2));
456
+ const thickness = Math.max(1, Math.ceil(tileSize / 3));
457
+ const paintCell = normalizeColorCell(brushCell);
458
+ let paintedCells = 0;
459
+
460
+ const shouldPaint = (x, y) => {
461
+ switch (mosaicType) {
462
+ case 'mosaic-diamond-checker': {
463
+ const span = tileSize * 2 + gap;
464
+ const tileX = Math.floor(x / span);
465
+ const tileY = Math.floor(y / span);
466
+ if ((tileX + tileY) % 2 !== 0) return false;
467
+ const localX = modulo(x, span) - tileSize;
468
+ const localY = modulo(y, span) - tileSize;
469
+ return Math.abs(localX) + Math.abs(localY) <= tileSize - 1;
470
+ }
471
+ case 'mosaic-rhombus-lattice': {
472
+ const period = tileSize * 2 + gap;
473
+ const diagonalA = modulo(x + y, period);
474
+ const diagonalB = modulo(x - y, period);
475
+ return diagonalA < thickness || diagonalB < thickness;
476
+ }
477
+ case 'mosaic-zigzag-rows': {
478
+ const zigzagWidth = Math.max(2, tileSize * 2 + gap);
479
+ const bandHeight = Math.max(1, tileSize);
480
+ const rowIndex = Math.floor(y / bandHeight);
481
+ const localX = modulo(x + (rowIndex % 2) * tileSize, zigzagWidth);
482
+ const ridge = modulo(y, bandHeight);
483
+ return Math.abs(localX - ridge) < thickness || Math.abs(localX - (zigzagWidth - ridge - 1)) < thickness;
484
+ }
485
+ case 'mosaic-staggered-tiles': {
486
+ const step = tileSize + gap;
487
+ const rowIndex = Math.floor(y / step);
488
+ const localX = modulo(x + (rowIndex % 2) * Math.floor(step / 2), step);
489
+ const localY = modulo(y, step);
490
+ return localX < tileSize && localY < tileSize;
491
+ }
492
+ case 'mosaic-brick-offset': {
493
+ const brickWidth = tileSize * 2 + gap;
494
+ const brickHeight = tileSize + gap;
495
+ const rowIndex = Math.floor(y / brickHeight);
496
+ const localX = modulo(x + (rowIndex % 2) * Math.floor(brickWidth / 2), brickWidth);
497
+ const localY = modulo(y, brickHeight);
498
+ return localX < brickWidth - gap && localY < tileSize;
499
+ }
500
+ default:
501
+ return false;
502
+ }
503
+ };
504
+
505
+ for (let y = 0; y < height; y++) {
506
+ for (let x = 0; x < width; x++) {
507
+ if (!shouldPaint(x, y)) continue;
508
+ paintedCells++;
509
+ result[y][x] = paintCell.slice();
510
+ }
511
+ }
512
+
513
+ return {
514
+ matrix: result,
515
+ changedCells: countChangedCells(baseMatrix, result),
516
+ paintedCells,
517
+ tileSize,
518
+ };
519
+ };
520
+
521
+ class ObjectLayerEngineModal {
522
+ static selectItemType = 'skin';
523
+ static itemActivable = false;
524
+ static renderFrameDuration = 100;
525
+ static existingObjectLayerId = null;
526
+ static originalDirectionCodes = [];
527
+ static selectedDistortionType = DEFAULT_DISTORTION_TYPE;
528
+ static distortionFactorA = DEFAULT_DISTORTION_FACTOR_A;
529
+ static uniformOpacityEnabled = false;
530
+ static templates = [
28
531
  {
29
532
  label: 'empty',
30
533
  id: 'empty',
31
534
  data: [],
32
535
  },
33
- ],
34
- statDescriptions: {
536
+ ];
537
+ static statDescriptions = {
35
538
  effect: {
36
539
  title: 'Effect',
37
540
  icon: 'fa-solid fa-burst',
@@ -69,9 +572,9 @@ const ObjectLayerEngineModal = {
69
572
  description: 'Reduces the cooldown time between actions, allowing for more frequent actions.',
70
573
  detail: 'It also increases the chance to trigger life-regeneration events.',
71
574
  },
72
- },
575
+ };
73
576
 
74
- RenderTemplate: (colorTemplate) => {
577
+ static RenderTemplate = (colorTemplate) => {
75
578
  const ole = s('object-layer-engine');
76
579
  if (!ole) {
77
580
  return;
@@ -84,17 +587,20 @@ const ObjectLayerEngineModal = {
84
587
 
85
588
  const matrix = colorTemplate.map((row) => row.map((hex) => [...hexToRgbA(hex), 255]));
86
589
  ole.loadMatrix(matrix);
87
- },
88
- ObjectLayerData: {},
89
- clearData: function () {
90
- // Clear all cached object layer data to prevent contamination between sessions
590
+ };
591
+
592
+ static ObjectLayerData = {};
593
+
594
+ static clearData() {
91
595
  this.ObjectLayerData = {};
92
596
  this.selectItemType = 'skin';
93
597
  this.itemActivable = false;
94
- this.renderIsStateless = false;
95
598
  this.renderFrameDuration = 100;
96
599
  this.existingObjectLayerId = null;
97
600
  this.originalDirectionCodes = [];
601
+ this.selectedDistortionType = DEFAULT_DISTORTION_TYPE;
602
+ this.distortionFactorA = DEFAULT_DISTORTION_FACTOR_A;
603
+ this.uniformOpacityEnabled = false;
98
604
  this.templates = [
99
605
  {
100
606
  label: 'empty',
@@ -103,13 +609,11 @@ const ObjectLayerEngineModal = {
103
609
  },
104
610
  ];
105
611
 
106
- // Clear the canvas if it exists
107
612
  const ole = s('object-layer-engine');
108
613
  if (ole && typeof ole.clear === 'function') {
109
614
  ole.clear();
110
615
  }
111
616
 
112
- // Clear all frame previews from DOM for all direction codes
113
617
  const directionCodes = ['08', '18', '02', '12', '04', '14', '06', '16'];
114
618
  for (const directionCode of directionCodes) {
115
619
  const framesContainer = s(`.frames-${directionCode}`);
@@ -132,24 +636,42 @@ const ObjectLayerEngineModal = {
132
636
  const activableCheckbox = s('#ol-toggle-item-activable');
133
637
  if (activableCheckbox) activableCheckbox.checked = false;
134
638
 
135
- const statelessCheckbox = s('#ol-toggle-render-is-stateless');
136
- if (statelessCheckbox) statelessCheckbox.checked = false;
137
-
138
639
  // Clear stat inputs with correct IDs
139
640
  const statTypes = Object.keys(ObjectLayerEngineModal.statDescriptions);
140
641
  for (const stat of statTypes) {
141
- const statInput = s(`#ol-input-item-stats-${stat}`);
642
+ const statInput = getRenderedInputNode(`ol-input-item-stats-${stat}`);
142
643
  if (statInput) statInput.value = '0';
143
644
  }
144
645
 
646
+ const statRandomMinInput = getRenderedInputNode('ol-input-stats-random-min');
647
+ if (statRandomMinInput) statRandomMinInput.value = String(DEFAULT_STAT_RANDOM_MIN);
648
+
649
+ const statRandomMaxInput = getRenderedInputNode('ol-input-stats-random-max');
650
+ if (statRandomMaxInput) statRandomMaxInput.value = String(DEFAULT_STAT_RANDOM_MAX);
651
+
145
652
  // Clear DropDown displays
146
653
  const templateDropdownCurrent = s(`.dropdown-current-ol-dropdown-template`);
147
654
  if (templateDropdownCurrent) templateDropdownCurrent.innerHTML = '';
148
655
 
149
656
  const itemTypeDropdownCurrent = s(`.dropdown-current-ol-dropdown-item-type`);
150
657
  if (itemTypeDropdownCurrent) itemTypeDropdownCurrent.innerHTML = 'skin';
151
- },
152
- loadFromDatabase: async (objectLayerId) => {
658
+
659
+ const distortionDropdownCurrent = s(`.dropdown-current-ol-dropdown-distortion-type`);
660
+ if (distortionDropdownCurrent) {
661
+ distortionDropdownCurrent.innerHTML = getCanvasBehaviorDisplay(DEFAULT_DISTORTION_TYPE);
662
+ }
663
+
664
+ const distortionFactorInput = s('#ol-input-distortion-factor-a') || s('.ol-input-distortion-factor-a');
665
+ if (distortionFactorInput) distortionFactorInput.value = String(DEFAULT_DISTORTION_FACTOR_A);
666
+
667
+ const distortionStatusNode = s(`.ol-distortion-status`);
668
+ if (distortionStatusNode) {
669
+ distortionStatusNode.style.color = '#888';
670
+ distortionStatusNode.innerHTML = DEFAULT_DISTORTION_STATUS;
671
+ }
672
+ }
673
+
674
+ static loadFromDatabase = async (objectLayerId) => {
153
675
  try {
154
676
  // Load metadata first (lightweight)
155
677
  const { status: metaStatus, data: metadata } = await ObjectLayerService.getMetadata({ id: objectLayerId });
@@ -184,8 +706,9 @@ const ObjectLayerEngineModal = {
184
706
  });
185
707
  return null;
186
708
  }
187
- },
188
- Render: async (options = { idModal: '', appStore: {} }) => {
709
+ };
710
+
711
+ static instance = async (options = { idModal: '', appStore: {} }) => {
189
712
  // Clear all cached data at the start of each render to prevent contamination
190
713
  ObjectLayerEngineModal.clearData();
191
714
 
@@ -202,8 +725,14 @@ const ObjectLayerEngineModal = {
202
725
  '06': 'Right Idle',
203
726
  16: 'Right Walk',
204
727
  };
205
- const itemTypes = ['skin', 'weapon', 'armor', 'artifact', 'floor'];
728
+ const itemTypes = ['skin', 'weapon', 'armor', 'artifact', 'floor', 'resource', 'obstacle', 'foreground', 'portal'];
206
729
  const statTypes = ['effect', 'resistance', 'agility', 'range', 'intelligence', 'utility'];
730
+ const distortionDropdownId = 'ol-dropdown-distortion-type';
731
+ const distortionApplyBtnClass = 'ol-btn-apply-distortion';
732
+ const distortionStatusClass = 'ol-distortion-status';
733
+ const statsRandomizeBtnClass = 'ol-btn-randomize-stats';
734
+ const statsRandomMinInputId = 'ol-input-stats-random-min';
735
+ const statsRandomMaxInputId = 'ol-input-stats-random-max';
207
736
 
208
737
  // Check if we have an 'id' query parameter to load existing object layer
209
738
  const queryParams = getQueryParams();
@@ -213,6 +742,130 @@ const ObjectLayerEngineModal = {
213
742
  let editingFrameId = null;
214
743
  let editingDirectionCode = null;
215
744
 
745
+ const readDistortionFactorA = () => {
746
+ const factorInput = s('.ol-input-distortion-factor-a') || s('#ol-input-distortion-factor-a');
747
+ const parsedFactor = Number.parseFloat(factorInput?.value);
748
+ const normalizedFactor = clampNumber(
749
+ Number.isFinite(parsedFactor)
750
+ ? parsedFactor
751
+ : ObjectLayerEngineModal.distortionFactorA || DEFAULT_DISTORTION_FACTOR_A,
752
+ 0.01,
753
+ 1,
754
+ );
755
+
756
+ ObjectLayerEngineModal.distortionFactorA = normalizedFactor;
757
+ if (factorInput) {
758
+ factorInput.value = String(Math.round(normalizedFactor * 100) / 100);
759
+ }
760
+
761
+ return normalizedFactor;
762
+ };
763
+
764
+ const readRandomStatBounds = () => {
765
+ const minInput = getRenderedInputNode(statsRandomMinInputId);
766
+ const maxInput = getRenderedInputNode(statsRandomMaxInputId);
767
+ let minValue = Number.parseInt(minInput?.value, 10);
768
+ let maxValue = Number.parseInt(maxInput?.value, 10);
769
+
770
+ minValue = clampNumber(Number.isFinite(minValue) ? minValue : DEFAULT_STAT_RANDOM_MIN, 0, 10);
771
+ maxValue = clampNumber(Number.isFinite(maxValue) ? maxValue : DEFAULT_STAT_RANDOM_MAX, 0, 10);
772
+
773
+ if (minValue > maxValue) {
774
+ const nextMin = maxValue;
775
+ maxValue = minValue;
776
+ minValue = nextMin;
777
+ }
778
+
779
+ if (minInput) minInput.value = String(minValue);
780
+ if (maxInput) maxInput.value = String(maxValue);
781
+
782
+ return { minValue, maxValue };
783
+ };
784
+
785
+ const randomizeStatInputs = () => {
786
+ const { minValue, maxValue } = readRandomStatBounds();
787
+
788
+ for (const statType of statTypes) {
789
+ const statInput = getRenderedInputNode(`ol-input-item-stats-${statType}`);
790
+ if (!statInput) continue;
791
+
792
+ const randomValue = Math.floor(Math.random() * (maxValue - minValue + 1)) + minValue;
793
+ statInput.value = String(randomValue);
794
+ }
795
+
796
+ NotificationManager.Push({
797
+ html: `Stats randomized between ${minValue} and ${maxValue}.`,
798
+ status: 'success',
799
+ });
800
+ };
801
+
802
+ let uniformOpacitySyncInProgress = false;
803
+
804
+ const buildUniformOpacityMatrix = (matrix = [], targetAlpha = 255) => {
805
+ const clampedAlpha = clampNumber(Number(targetAlpha) || 0, 0, 255);
806
+ let changedCells = 0;
807
+ const nextMatrix = matrix.map((row) =>
808
+ row.map((cell) => {
809
+ const nextCell = cell.slice();
810
+ if (isVisibleCell(nextCell) && nextCell[3] !== clampedAlpha) {
811
+ nextCell[3] = clampedAlpha;
812
+ changedCells++;
813
+ }
814
+ return nextCell;
815
+ }),
816
+ );
817
+
818
+ return { matrix: nextMatrix, changedCells, alpha: clampedAlpha };
819
+ };
820
+
821
+ const applyUniformOpacityToEditor = ({ captureUndo = false } = {}) => {
822
+ const ole = s('object-layer-engine');
823
+ if (!ole || !ObjectLayerEngineModal.uniformOpacityEnabled || uniformOpacitySyncInProgress) return false;
824
+
825
+ let currentFrame = null;
826
+ try {
827
+ const exportedMatrix = ole.exportMatrixJSON();
828
+ currentFrame = typeof exportedMatrix === 'string' ? JSON.parse(exportedMatrix) : exportedMatrix;
829
+ } catch (error) {
830
+ return false;
831
+ }
832
+
833
+ const currentMatrix = currentFrame?.matrix;
834
+ if (!Array.isArray(currentMatrix) || !currentMatrix.length || !currentMatrix[0]?.length) {
835
+ return false;
836
+ }
837
+
838
+ const targetAlpha = clampNumber(ole.getBrushAlpha?.() ?? ole.getBrushColor?.()?.[3] ?? 255, 0, 255);
839
+ const nextFrame = buildUniformOpacityMatrix(currentMatrix, targetAlpha);
840
+ if (!nextFrame.changedCells) return false;
841
+
842
+ const nextSnapshot = {
843
+ width: currentFrame.width || currentMatrix[0].length,
844
+ height: currentFrame.height || currentMatrix.length,
845
+ matrix: nextFrame.matrix,
846
+ };
847
+ const beforeSnapshot = captureUndo && typeof ole._snapshot === 'function' ? ole._snapshot() : null;
848
+
849
+ if (
850
+ captureUndo &&
851
+ beforeSnapshot &&
852
+ typeof ole._pushUndo === 'function' &&
853
+ typeof ole._matricesEqual === 'function' &&
854
+ !ole._matricesEqual(beforeSnapshot, nextSnapshot)
855
+ ) {
856
+ ole._pushUndo(beforeSnapshot);
857
+ }
858
+
859
+ uniformOpacitySyncInProgress = true;
860
+ try {
861
+ ole.loadMatrix(nextFrame.matrix);
862
+ } finally {
863
+ uniformOpacitySyncInProgress = false;
864
+ }
865
+
866
+ return true;
867
+ };
868
+
216
869
  // Helper function to update UI when entering edit mode
217
870
  const enterEditMode = (frameId, directionCode) => {
218
871
  editingFrameId = frameId;
@@ -331,7 +984,6 @@ const ObjectLayerEngineModal = {
331
984
  }
332
985
  }
333
986
  if (objectLayerRenderFramesId) {
334
- ObjectLayerEngineModal.renderIsStateless = objectLayerRenderFramesId.is_stateless || false;
335
987
  ObjectLayerEngineModal.renderFrameDuration = objectLayerRenderFramesId.frame_duration || 100;
336
988
  }
337
989
  }
@@ -367,6 +1019,154 @@ const ObjectLayerEngineModal = {
367
1019
  const pixelSize = parseInt(320 / Math.max(cellsW, cellsH));
368
1020
  const idSectionA = 'template-section-a';
369
1021
  const idSectionB = 'template-section-b';
1022
+ const colorPaletteClass = 'ol-color-palette';
1023
+ let directionPreviewRuntime = null;
1024
+
1025
+ const cleanupDirectionPreviewRuntime = () => {
1026
+ if (!directionPreviewRuntime) return;
1027
+ if (directionPreviewRuntime.intervalId) {
1028
+ clearInterval(directionPreviewRuntime.intervalId);
1029
+ }
1030
+ for (const frameUrl of directionPreviewRuntime.frameUrls || []) {
1031
+ URL.revokeObjectURL(frameUrl);
1032
+ }
1033
+ directionPreviewRuntime = null;
1034
+ };
1035
+
1036
+ const cleanupDirectionPreviewModal = () => {
1037
+ cleanupDirectionPreviewRuntime();
1038
+ if (s(`.${DIRECTION_PREVIEW_MODAL_ID}`)) {
1039
+ Modal.removeModal(DIRECTION_PREVIEW_MODAL_ID);
1040
+ }
1041
+ };
1042
+
1043
+ if (Modal.Data[options.idModal]) {
1044
+ Modal.Data[options.idModal].onCloseListener[`${options.idModal}-direction-preview-cleanup`] = () => {
1045
+ cleanupDirectionPreviewModal();
1046
+ delete Modal.Data[options.idModal]?.onCloseListener?.[`${options.idModal}-direction-preview-cleanup`];
1047
+ };
1048
+ }
1049
+
1050
+ const getCurrentFrameDuration = () => {
1051
+ const durationInput = s('.ol-input-render-frame-duration') || s('#ol-input-render-frame-duration');
1052
+ const parsedDuration = Number.parseInt(durationInput?.value, 10);
1053
+ return Math.max(
1054
+ 100,
1055
+ Number.isFinite(parsedDuration) ? parsedDuration : ObjectLayerEngineModal.renderFrameDuration || 100,
1056
+ );
1057
+ };
1058
+
1059
+ const openDirectionPreviewModal = async (directionCode) => {
1060
+ const frames = ObjectLayerEngineModal.ObjectLayerData[directionCode] || [];
1061
+ if (!frames.length) {
1062
+ NotificationManager.Push({
1063
+ html: `No frames available yet for ${directionCodeLabels[directionCode] || directionCode}.`,
1064
+ status: 'warning',
1065
+ });
1066
+ return;
1067
+ }
1068
+
1069
+ const frameUrls = frames
1070
+ .map((frame) => {
1071
+ if (!frame?.image) return null;
1072
+ return URL.createObjectURL(frame.image);
1073
+ })
1074
+ .filter(Boolean);
1075
+
1076
+ if (!frameUrls.length) {
1077
+ NotificationManager.Push({
1078
+ html: `Could not build a local preview for ${directionCodeLabels[directionCode] || directionCode}.`,
1079
+ status: 'error',
1080
+ });
1081
+ return;
1082
+ }
1083
+
1084
+ cleanupDirectionPreviewModal();
1085
+
1086
+ const { barConfig } = await Themes[Css.currentTheme]();
1087
+ const frameDuration = getCurrentFrameDuration();
1088
+ const previewWidth = Math.max(180, cellsW * pixelSize);
1089
+ const previewHeight = Math.max(180, cellsH * pixelSize);
1090
+
1091
+ await Modal.instance({
1092
+ id: DIRECTION_PREVIEW_MODAL_ID,
1093
+ barConfig,
1094
+ title: `${directionCodeLabels[directionCode] || directionCode} Preview`,
1095
+ html: () => html`
1096
+ <div class="in section-mp" style="text-align: center; min-width: ${previewWidth}px;">
1097
+ <div class="in" style="font-size: 12px; color: #999; margin-bottom: 8px;">
1098
+ ${frames.length} frame${frames.length === 1 ? '' : 's'} at ${frameDuration}ms
1099
+ </div>
1100
+ <div
1101
+ class="in direction-preview-stage"
1102
+ style="display: inline-flex; align-items: center; justify-content: center; min-height: ${previewHeight}px; min-width: ${previewWidth}px; background-image: linear-gradient(45deg, rgba(255,255,255,0.08) 25%, transparent 25%), linear-gradient(-45deg, rgba(255,255,255,0.08) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.08) 75%), linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.08) 75%); background-size: 24px 24px; background-position: 0 0, 0 12px, 12px -12px, -12px 0; border: 1px solid rgba(255,255,255,0.15);"
1103
+ >
1104
+ <img
1105
+ class="direction-preview-image"
1106
+ src="${frameUrls[0]}"
1107
+ style="image-rendering: pixelated; width: ${previewWidth}px; height: ${previewHeight}px; object-fit: contain;"
1108
+ />
1109
+ </div>
1110
+ <div class="fl" style="justify-content: center; gap: 6px; flex-wrap: wrap; margin-top: 10px;">
1111
+ ${frameUrls.map(
1112
+ (frameUrl, frameIndex) => html`
1113
+ <img
1114
+ src="${frameUrl}"
1115
+ style="width: 48px; height: 48px; object-fit: contain; image-rendering: pixelated; border: 1px solid rgba(255,255,255,0.15); background: rgba(0,0,0,0.25);"
1116
+ title="Frame ${frameIndex + 1}"
1117
+ />
1118
+ `,
1119
+ )}
1120
+ </div>
1121
+ </div>
1122
+ `,
1123
+ style: {
1124
+ width: `${Math.max(300, previewWidth + 40)}px`,
1125
+ height: `${Math.max(260, previewHeight + 150)}px`,
1126
+ border: '1px solid rgba(255,255,255,0.15)',
1127
+ 'z-index': 10,
1128
+ },
1129
+ slideMenu: 'modal-menu',
1130
+ });
1131
+
1132
+ const previewImage = s(`.${DIRECTION_PREVIEW_MODAL_ID} .direction-preview-image`);
1133
+ if (!previewImage) {
1134
+ cleanupDirectionPreviewRuntime();
1135
+ return;
1136
+ }
1137
+
1138
+ let currentFrameIndex = 0;
1139
+ const advancePreviewFrame = () => {
1140
+ if (!previewImage) return;
1141
+ currentFrameIndex = (currentFrameIndex + 1) % frameUrls.length;
1142
+ previewImage.src = frameUrls[currentFrameIndex];
1143
+ };
1144
+
1145
+ directionPreviewRuntime = {
1146
+ frameUrls,
1147
+ intervalId: frameUrls.length > 1 ? setInterval(advancePreviewFrame, frameDuration) : null,
1148
+ };
1149
+
1150
+ if (Modal.Data[DIRECTION_PREVIEW_MODAL_ID]) {
1151
+ Modal.Data[DIRECTION_PREVIEW_MODAL_ID].onCloseListener[DIRECTION_PREVIEW_MODAL_ID] = () => {
1152
+ cleanupDirectionPreviewRuntime();
1153
+ delete Modal.Data[DIRECTION_PREVIEW_MODAL_ID]?.onCloseListener?.[DIRECTION_PREVIEW_MODAL_ID];
1154
+ };
1155
+ }
1156
+ };
1157
+
1158
+ const rgbaToHex = (rgba = [0, 0, 0]) => {
1159
+ const red = Math.max(0, Math.min(255, rgba[0] || 0))
1160
+ .toString(16)
1161
+ .padStart(2, '0');
1162
+ const green = Math.max(0, Math.min(255, rgba[1] || 0))
1163
+ .toString(16)
1164
+ .padStart(2, '0');
1165
+ const blue = Math.max(0, Math.min(255, rgba[2] || 0))
1166
+ .toString(16)
1167
+ .padStart(2, '0');
1168
+ return `#${red}${green}${blue}`.toUpperCase();
1169
+ };
370
1170
 
371
1171
  let directionsCodeBarRender = '';
372
1172
 
@@ -389,11 +1189,11 @@ const ObjectLayerEngineModal = {
389
1189
  src="${URL.createObjectURL(image)}"
390
1190
  data-direction-code="${capturedDirectionCode}"
391
1191
  />
392
- ${await BtnIcon.Render({
1192
+ ${await BtnIcon.instance({
393
1193
  label: html`<i class="fa-solid fa-edit"></i>`,
394
1194
  class: `abs direction-code-bar-edit-btn direction-code-bar-edit-btn-${id}`,
395
1195
  })}
396
- ${await BtnIcon.Render({
1196
+ ${await BtnIcon.instance({
397
1197
  label: html`<i class="fa-solid fa-trash"></i>`,
398
1198
  class: `abs direction-code-bar-trash-btn direction-code-bar-trash-btn-${id}`,
399
1199
  })}
@@ -538,14 +1338,18 @@ const ObjectLayerEngineModal = {
538
1338
  <div class="fl">
539
1339
  <div class="in fll">
540
1340
  <div class="in direction-code-bar-frames-title">${directionCodeLabels[directionCode]}</div>
541
- <div class="in direction-code-bar-frames-btn">
542
- ${await BtnIcon.Render({
1341
+ <div class="fl direction-code-bar-btn-row" style="gap: 6px;">
1342
+ ${await BtnIcon.instance({
543
1343
  label: html`
544
1344
  <i class="fa-solid fa-plus direction-code-bar-frames-btn-icon-add-${directionCode}"></i>
545
1345
  <i class="fa-solid fa-edit direction-code-bar-frames-btn-icon-edit-${directionCode} hide"></i>
546
1346
  `,
547
1347
  class: `direction-code-bar-frames-btn-add direction-code-bar-frames-btn-${directionCode}`,
548
1348
  })}
1349
+ ${await BtnIcon.instance({
1350
+ label: html`<i class="fa-solid fa-play"></i>`,
1351
+ class: `direction-code-bar-preview-btn direction-code-bar-preview-btn-${directionCode}`,
1352
+ })}
549
1353
  </div>
550
1354
  </div>
551
1355
  <div class="frames-${directionCode}"></div>
@@ -560,7 +1364,7 @@ const ObjectLayerEngineModal = {
560
1364
  const statValue = loadedData?.metadata?.data?.stats?.[statType] || 0;
561
1365
  statsInputsRender += html`
562
1366
  <div class="inl" style="margin-bottom: 10px; position: relative;">
563
- ${await Input.Render({
1367
+ ${await Input.instance({
564
1368
  id: `ol-input-item-stats-${statType}`,
565
1369
  label: html`<div
566
1370
  title="${statInfo.description} ${statInfo.detail}"
@@ -651,8 +1455,13 @@ const ObjectLayerEngineModal = {
651
1455
  }
652
1456
 
653
1457
  const buttonSelector = `.direction-code-bar-frames-btn-${currentDirectionCode}`;
1458
+ const previewButtonSelector = `.direction-code-bar-preview-btn-${currentDirectionCode}`;
654
1459
  console.log(`Registering click handler for: ${buttonSelector}`);
655
1460
 
1461
+ EventsUI.onClick(previewButtonSelector, async () => {
1462
+ await openDirectionPreviewModal(currentDirectionCode);
1463
+ });
1464
+
656
1465
  EventsUI.onClick(buttonSelector, async () => {
657
1466
  console.log(`Add frame button clicked for direction: ${currentDirectionCode}`);
658
1467
  const ole = s('object-layer-engine');
@@ -738,7 +1547,165 @@ const ObjectLayerEngineModal = {
738
1547
  await loadFrames();
739
1548
  s('object-layer-engine').clear();
740
1549
 
741
- EventsUI.onClick(`.ol-btn-save`, async () => {
1550
+ const editor = s('object-layer-engine');
1551
+ const colorPalette = s(`.${colorPaletteClass}`);
1552
+
1553
+ const syncPaletteFromEditor = (event = null) => {
1554
+ if (!colorPalette || !editor) {
1555
+ return;
1556
+ }
1557
+ const nextHex = event?.detail?.hex || rgbaToHex(editor.getBrushColor?.());
1558
+ if (nextHex && colorPalette.value !== nextHex) {
1559
+ colorPalette.value = nextHex;
1560
+ }
1561
+ };
1562
+
1563
+ const syncEditorFromPalette = (event) => {
1564
+ if (!editor) {
1565
+ return;
1566
+ }
1567
+ const nextHex = event.detail?.value || event.detail?.hex;
1568
+ if (!nextHex) {
1569
+ return;
1570
+ }
1571
+ const [red, green, blue] = hexToRgbA(nextHex);
1572
+ const alpha = editor.getBrushAlpha?.() ?? editor.getBrushColor?.()?.[3] ?? 255;
1573
+ editor.setBrushColor([red, green, blue, alpha]);
1574
+ };
1575
+
1576
+ if (colorPalette) {
1577
+ colorPalette.addEventListener('colorchange', syncEditorFromPalette);
1578
+ }
1579
+ if (editor) {
1580
+ editor.addEventListener('brushcolorchange', syncPaletteFromEditor);
1581
+ }
1582
+
1583
+ const syncUniformOpacityFromEditor = () => {
1584
+ applyUniformOpacityToEditor();
1585
+ };
1586
+
1587
+ if (editor) {
1588
+ editor.addEventListener('brushcolorchange', syncUniformOpacityFromEditor);
1589
+ editor.addEventListener('matrixload', syncUniformOpacityFromEditor);
1590
+ }
1591
+ syncPaletteFromEditor();
1592
+
1593
+ const setDistortionStatus = (message, tone = 'muted') => {
1594
+ const statusNode = s(`.${distortionStatusClass}`);
1595
+ if (!statusNode) return;
1596
+ statusNode.style.color = tone === 'success' ? '#8fd18c' : tone === 'error' ? '#ff8a8a' : '#888';
1597
+ statusNode.innerHTML = message;
1598
+ };
1599
+
1600
+ const applyDistortionToCurrentFrame = async () => {
1601
+ const ole = s('object-layer-engine');
1602
+ if (!ole || typeof ole.exportMatrixJSON !== 'function' || typeof ole.loadMatrix !== 'function') return;
1603
+
1604
+ let currentJson = null;
1605
+ let currentFrame = null;
1606
+
1607
+ try {
1608
+ currentJson = ole.exportMatrixJSON();
1609
+ currentFrame = typeof currentJson === 'string' ? JSON.parse(currentJson) : currentJson;
1610
+ } catch (error) {
1611
+ setDistortionStatus('Could not read the current frame from the editor.', 'error');
1612
+ return;
1613
+ }
1614
+
1615
+ const currentMatrix = currentFrame?.matrix;
1616
+ if (!Array.isArray(currentMatrix) || !currentMatrix.length || !currentMatrix[0]?.length) {
1617
+ setDistortionStatus('The current frame has no editable matrix data.', 'error');
1618
+ return;
1619
+ }
1620
+ const selectedDistortionType = ObjectLayerEngineModal.selectedDistortionType || DEFAULT_DISTORTION_TYPE;
1621
+ const selectedBehavior =
1622
+ CANVAS_BEHAVIOR_BY_VALUE[selectedDistortionType] || CANVAS_BEHAVIOR_BY_VALUE[DEFAULT_DISTORTION_TYPE];
1623
+ const distortionFactorA = readDistortionFactorA();
1624
+ const isMosaicMode = isMosaicBehavior(selectedDistortionType);
1625
+
1626
+ if (!isMosaicMode && !currentMatrix.some((row) => row.some((cell) => isVisibleCell(cell)))) {
1627
+ setDistortionStatus('Paint something on the current frame before applying a distortion.', 'error');
1628
+ return;
1629
+ }
1630
+
1631
+ const baseFrame = currentFrame;
1632
+ let transformationResult = null;
1633
+
1634
+ if (isMosaicMode) {
1635
+ const activeBrushColor = normalizeColorCell(ole.getBrushColor?.() || [255, 0, 0, 255]);
1636
+ transformationResult = buildMosaicMatrix(
1637
+ baseFrame.matrix,
1638
+ selectedDistortionType,
1639
+ distortionFactorA,
1640
+ activeBrushColor,
1641
+ );
1642
+ } else {
1643
+ const seedBase = Date.now() + hashString(currentJson);
1644
+ for (let attempt = 0; attempt < 4; attempt++) {
1645
+ const distortionSeed = seedBase + attempt * 977;
1646
+ transformationResult = buildDistortedMatrix(
1647
+ baseFrame.matrix,
1648
+ selectedDistortionType,
1649
+ distortionFactorA,
1650
+ distortionSeed,
1651
+ );
1652
+ if (transformationResult.changedCells > 0) break;
1653
+ }
1654
+ }
1655
+
1656
+ if (!transformationResult || transformationResult.changedCells === 0) {
1657
+ setDistortionStatus(
1658
+ isMosaicMode
1659
+ ? 'No visible mosaic was painted on the current frame. Try another factorA value or a different active color.'
1660
+ : 'No visible distortion was produced for the current frame. Try applying again or edit the frame shape first.',
1661
+ 'error',
1662
+ );
1663
+ return;
1664
+ }
1665
+
1666
+ const nextSnapshot = {
1667
+ width: baseFrame.width,
1668
+ height: baseFrame.height,
1669
+ matrix: transformationResult.matrix,
1670
+ };
1671
+ const beforeSnapshot = typeof ole._snapshot === 'function' ? ole._snapshot() : null;
1672
+
1673
+ if (
1674
+ beforeSnapshot &&
1675
+ typeof ole._pushUndo === 'function' &&
1676
+ typeof ole._matricesEqual === 'function' &&
1677
+ !ole._matricesEqual(beforeSnapshot, nextSnapshot)
1678
+ ) {
1679
+ ole._pushUndo(beforeSnapshot);
1680
+ }
1681
+
1682
+ ole.loadMatrix(transformationResult.matrix);
1683
+
1684
+ if (isMosaicMode) {
1685
+ setDistortionStatus(
1686
+ `${selectedBehavior.label} painted on the current frame with the active color. tileSize ${transformationResult.tileSize}, ${transformationResult.paintedCells || 0} pattern cells, ${transformationResult.changedCells} cells changed, factorA=${distortionFactorA.toFixed(2)}.`,
1687
+ 'success',
1688
+ );
1689
+ } else {
1690
+ setDistortionStatus(
1691
+ `${selectedBehavior.label} applied to the current frame. ${transformationResult.appliedDistortions || 0} local shifts, ${transformationResult.changedCells} cells changed, factorA=${distortionFactorA.toFixed(2)}.`,
1692
+ 'success',
1693
+ );
1694
+ }
1695
+ };
1696
+
1697
+ EventsUI.onClick(`.${distortionApplyBtnClass}`, async () => {
1698
+ await applyDistortionToCurrentFrame();
1699
+ });
1700
+ setDistortionStatus(DEFAULT_DISTORTION_STATUS);
1701
+
1702
+ EventsUI.onClick(`.${statsRandomizeBtnClass}`, async () => {
1703
+ randomizeStatInputs();
1704
+ });
1705
+
1706
+ const persistObjectLayer = async ({ clone = false } = {}) => {
1707
+ const isUpdateMode = Boolean(ObjectLayerEngineModal.existingObjectLayerId) && !clone;
1708
+
742
1709
  // Validate minimum frame_duration 100ms
743
1710
  const frameDuration = parseInt(s(`.ol-input-render-frame-duration`).value);
744
1711
  if (!frameDuration || frameDuration < 100) {
@@ -776,7 +1743,6 @@ const ObjectLayerEngineModal = {
776
1743
  frames: {},
777
1744
  colors: [],
778
1745
  frame_duration: ObjectLayerEngineModal.renderFrameDuration,
779
- is_stateless: ObjectLayerEngineModal.renderIsStateless,
780
1746
  };
781
1747
 
782
1748
  const objectLayer = {
@@ -822,7 +1788,6 @@ const ObjectLayerEngineModal = {
822
1788
  }
823
1789
  }
824
1790
  objectLayerRenderFramesData.frame_duration = parseInt(s(`.ol-input-render-frame-duration`).value);
825
- objectLayerRenderFramesData.is_stateless = ObjectLayerEngineModal.renderIsStateless;
826
1791
  objectLayer.data.stats = {
827
1792
  effect: parseInt(s(`.ol-input-item-stats-effect`).value),
828
1793
  resistance: parseInt(s(`.ol-input-item-stats-resistance`).value),
@@ -838,15 +1803,15 @@ const ObjectLayerEngineModal = {
838
1803
  description: s(`.ol-input-item-description`).value,
839
1804
  };
840
1805
 
841
- // Add _id if we're updating an existing object layer
842
- if (ObjectLayerEngineModal.existingObjectLayerId) {
1806
+ // Add _id only when updating the existing object layer.
1807
+ if (isUpdateMode) {
843
1808
  objectLayer._id = ObjectLayerEngineModal.existingObjectLayerId;
844
1809
  }
845
1810
 
846
1811
  console.warn(
847
1812
  'objectLayer',
848
1813
  objectLayer,
849
- ObjectLayerEngineModal.existingObjectLayerId ? '(UPDATE MODE)' : '(CREATE MODE)',
1814
+ clone ? '(CLONE MODE)' : isUpdateMode ? '(UPDATE MODE)' : '(CREATE MODE)',
850
1815
  );
851
1816
 
852
1817
  if (appStore.Data.user.main.model.user.role === 'guest') {
@@ -863,14 +1828,14 @@ const ObjectLayerEngineModal = {
863
1828
  const directionCodesToUpload = Object.keys(ObjectLayerEngineModal.ObjectLayerData);
864
1829
 
865
1830
  // In UPDATE mode, also include original direction codes that may have been cleared
866
- const allDirectionCodes = ObjectLayerEngineModal.existingObjectLayerId
1831
+ const allDirectionCodes = isUpdateMode
867
1832
  ? [...new Set([...directionCodesToUpload, ...ObjectLayerEngineModal.originalDirectionCodes])]
868
1833
  : directionCodesToUpload;
869
1834
 
870
1835
  console.warn(
871
1836
  `Uploading frames for ${allDirectionCodes.length} directions:`,
872
1837
  allDirectionCodes,
873
- ObjectLayerEngineModal.existingObjectLayerId ? '(UPDATE MODE)' : '(CREATE MODE)',
1838
+ clone ? '(CLONE MODE)' : isUpdateMode ? '(UPDATE MODE)' : '(CREATE MODE)',
874
1839
  );
875
1840
 
876
1841
  for (const directionCode of allDirectionCodes) {
@@ -895,7 +1860,7 @@ const ObjectLayerEngineModal = {
895
1860
 
896
1861
  // Send all frames for this direction in one request (even if empty, to remove frames)
897
1862
  try {
898
- if (ObjectLayerEngineModal.existingObjectLayerId) {
1863
+ if (isUpdateMode) {
899
1864
  // UPDATE: use PUT endpoint with object layer ID
900
1865
  const { status, data } = await ObjectLayerService.put({
901
1866
  id: `${ObjectLayerEngineModal.existingObjectLayerId}/frame-image/${objectLayer.data.item.type}/${objectLayer.data.item.id}/${directionCode}`,
@@ -936,7 +1901,7 @@ const ObjectLayerEngineModal = {
936
1901
  };
937
1902
 
938
1903
  let response;
939
- if (ObjectLayerEngineModal.existingObjectLayerId) {
1904
+ if (isUpdateMode) {
940
1905
  // UPDATE existing object layer
941
1906
  console.warn(
942
1907
  'PUT path:',
@@ -957,22 +1922,28 @@ const ObjectLayerEngineModal = {
957
1922
  const { status, data, message } = response;
958
1923
 
959
1924
  if (status === 'success') {
1925
+ const successAction = clone ? 'cloned' : isUpdateMode ? 'updated' : 'created';
960
1926
  NotificationManager.Push({
961
- html: `Object layer "${objectLayer.data.item.id}" ${
962
- ObjectLayerEngineModal.existingObjectLayerId ? 'updated' : 'created'
963
- } successfully!`,
1927
+ html: `Object layer "${objectLayer.data.item.id}" ${successAction} successfully!`,
964
1928
  status: 'success',
965
1929
  });
966
1930
  ObjectLayerEngineModal.toManagement(data?._id || ObjectLayerEngineModal.existingObjectLayerId);
967
1931
  } else {
1932
+ const errorAction = clone ? 'cloning' : isUpdateMode ? 'updating' : 'creating';
968
1933
  NotificationManager.Push({
969
- html: `Error ${
970
- ObjectLayerEngineModal.existingObjectLayerId ? 'updating' : 'creating'
971
- } object layer: ${message}`,
1934
+ html: `Error ${errorAction} object layer: ${message}`,
972
1935
  status: 'error',
973
1936
  });
974
1937
  }
975
1938
  }
1939
+ };
1940
+
1941
+ EventsUI.onClick(`.ol-btn-save`, async () => {
1942
+ await persistObjectLayer();
1943
+ });
1944
+
1945
+ EventsUI.onClick(`.ol-btn-clone`, async () => {
1946
+ await persistObjectLayer({ clone: true });
976
1947
  });
977
1948
 
978
1949
  // Add reset button event listener
@@ -1040,6 +2011,10 @@ const ObjectLayerEngineModal = {
1040
2011
  color: white;
1041
2012
  border: none !important;
1042
2013
  }
2014
+ .direction-code-bar-preview-btn {
2015
+ color: white;
2016
+ border: none !important;
2017
+ }
1043
2018
  .direction-code-bar-trash-btn:hover {
1044
2019
  background: none !important;
1045
2020
  color: red;
@@ -1052,6 +2027,10 @@ const ObjectLayerEngineModal = {
1052
2027
  background: none !important;
1053
2028
  color: #c7ff58;
1054
2029
  }
2030
+ .direction-code-bar-preview-btn:hover {
2031
+ background: none !important;
2032
+ color: #5ee6ff;
2033
+ }
1055
2034
  .ol-btn-save {
1056
2035
  width: 120px;
1057
2036
  padding: 0.5rem;
@@ -1064,6 +2043,16 @@ const ObjectLayerEngineModal = {
1064
2043
  font-size: 20px;
1065
2044
  min-height: 50px;
1066
2045
  }
2046
+ .ol-btn-clone {
2047
+ width: 120px;
2048
+ padding: 0.5rem;
2049
+ font-size: 20px;
2050
+ min-height: 50px;
2051
+ }
2052
+ .ol-btn-randomize-stats {
2053
+ min-height: 50px;
2054
+ padding: 0.5rem 0.75rem;
2055
+ }
1067
2056
  .ol-number-label {
1068
2057
  width: 120px;
1069
2058
  font-size: 16px;
@@ -1102,6 +2091,7 @@ const ObjectLayerEngineModal = {
1102
2091
  '.direction-code-bar-edit-btn',
1103
2092
  '.direction-code-bar-trash-btn',
1104
2093
  '.direction-code-bar-frames-btn-add',
2094
+ '.direction-code-bar-preview-btn',
1105
2095
  ])}
1106
2096
  <div class="in frame-editor-container-loading">
1107
2097
  <div class="abs center frame-editor-container-loading-center"></div>
@@ -1111,20 +2101,117 @@ const ObjectLayerEngineModal = {
1111
2101
 
1112
2102
  <object-layer-engine id="ole" width="${cellsW}" height="${cellsH}" pixel-size="${pixelSize}">
1113
2103
  </object-layer-engine>
2104
+ <div class="in section-mp-border" style="margin-top: 10px;">
2105
+ <div class="in sub-title-modal"><i class="fa-solid fa-palette"></i> Brush palette</div>
2106
+ <color-palette class="${colorPaletteClass}" value="#FF0000"></color-palette>
2107
+ <div class="fl" style="align-items: center; gap: 8px; margin-top: 8px;">
2108
+ ${await ToggleSwitch.instance({
2109
+ id: UNIFORM_OPACITY_TOGGLE_ID,
2110
+ type: 'checkbox',
2111
+ displayMode: 'checkbox',
2112
+ containerClass: 'in fll',
2113
+ checked: ObjectLayerEngineModal.uniformOpacityEnabled,
2114
+ on: {
2115
+ checked: () => {
2116
+ ObjectLayerEngineModal.uniformOpacityEnabled = true;
2117
+ applyUniformOpacityToEditor({ captureUndo: true });
2118
+ },
2119
+ unchecked: () => {
2120
+ ObjectLayerEngineModal.uniformOpacityEnabled = false;
2121
+ },
2122
+ },
2123
+ })}
2124
+ <div class="section-mp" style="font-size: 14px;">
2125
+ Keep all visible cells at the current opacity bar value
2126
+ </div>
2127
+ </div>
2128
+ </div>
2129
+ <div class="in section-mp-border" style="margin-top: 10px;">
2130
+ <div class="in sub-title-modal"><i class="fa-solid fa-wand-magic-sparkles"></i> Canvas macro</div>
2131
+ <div class="fl" style="align-items: flex-start; gap: 8px; flex-wrap: wrap;">
2132
+ <div class="in fll" style="min-width: 240px;">
2133
+ ${await DropDown.instance({
2134
+ id: distortionDropdownId,
2135
+ value: ObjectLayerEngineModal.selectedDistortionType,
2136
+ label: html`Select behavior`,
2137
+ disableSearchBox: true,
2138
+ data: [
2139
+ {
2140
+ kind: 'group',
2141
+ value: 'group-distortion-behaviors',
2142
+ display: html`<div style="padding: 0 6px; color: #9d9d9d;">Distortion behaviors</div>`,
2143
+ },
2144
+ ...DISTORTION_TYPES.map((distortion) => ({
2145
+ value: distortion.value,
2146
+ display: html`<i class="${CANVAS_BEHAVIOR_ICON}"></i> ${distortion.label}`,
2147
+ onClick: async () => {
2148
+ ObjectLayerEngineModal.selectedDistortionType = distortion.value;
2149
+ readDistortionFactorA();
2150
+ const statusNode = s(`.${distortionStatusClass}`);
2151
+ if (statusNode) {
2152
+ statusNode.style.color = '#888';
2153
+ statusNode.innerHTML = `${distortion.label} ready for direct canvas apply. factorA controls local distortion density.`;
2154
+ }
2155
+ },
2156
+ })),
2157
+ {
2158
+ kind: 'group',
2159
+ value: 'group-mosaic-behaviors',
2160
+ display: html`<div style="padding: 0 6px; color: #9d9d9d;">Mosaic drawing behaviors</div>`,
2161
+ },
2162
+ ...MOSAIC_TYPES.map((mosaic) => ({
2163
+ value: mosaic.value,
2164
+ display: html`<i class="${CANVAS_BEHAVIOR_ICON}"></i> ${mosaic.label}`,
2165
+ onClick: async () => {
2166
+ ObjectLayerEngineModal.selectedDistortionType = mosaic.value;
2167
+ readDistortionFactorA();
2168
+ const statusNode = s(`.${distortionStatusClass}`);
2169
+ if (statusNode) {
2170
+ statusNode.style.color = '#888';
2171
+ statusNode.innerHTML = `${mosaic.label} ready for direct canvas apply. factorA controls tile size and density.`;
2172
+ }
2173
+ },
2174
+ })),
2175
+ ],
2176
+ })}
2177
+ </div>
2178
+ <div class="in fll" style="width: 120px;">
2179
+ ${await Input.instance({
2180
+ id: `ol-input-distortion-factor-a`,
2181
+ label: html`factorA`,
2182
+ containerClass: 'inl',
2183
+ type: 'number',
2184
+ min: 0.01,
2185
+ max: 1,
2186
+ step: 0.01,
2187
+ value: ObjectLayerEngineModal.distortionFactorA,
2188
+ })}
2189
+ </div>
2190
+ <div class="in fll">
2191
+ ${await BtnIcon.instance({
2192
+ class: distortionApplyBtnClass,
2193
+ label: html`<i class="fa-solid fa-bolt"></i> Apply To Frame`,
2194
+ })}
2195
+ </div>
2196
+ </div>
2197
+ <div class="in ${distortionStatusClass}" style="margin-top: 6px; font-size: 12px; color: #888;">
2198
+ ${DEFAULT_DISTORTION_STATUS}
2199
+ </div>
2200
+ </div>
1114
2201
  <object-layer-png-loader id="loader" editor-selector="#ole"></object-layer-png-loader>
1115
2202
  </div>
1116
2203
 
1117
2204
  <div class="in section-mp section-mp-border">
1118
- <div class="in sub-title-modal"><i class="fa-solid fa-database"></i> Render data</div>
2205
+ <div class="in sub-title-modal"><i class="fa-solid fa-database"></i> instance data</div>
1119
2206
  ${dynamicCol({ containerSelector: options.idModal, id: idSectionA })}
1120
2207
 
1121
2208
  <div class="fl">
1122
2209
  <div class="in fll ${idSectionA}-col-a">
1123
2210
  <div class="in section-mp">
1124
- ${await DropDown.Render({
2211
+ ${await DropDown.instance({
1125
2212
  id: 'ol-dropdown-template',
1126
2213
  value: ObjectLayerEngineModal.templates[0].id,
1127
- label: html`${Translate.Render('select-template')}`,
2214
+ label: html`${Translate.instance('select-template')}`,
1128
2215
  data: ObjectLayerEngineModal.templates.map((template) => {
1129
2216
  return {
1130
2217
  value: template.id,
@@ -1139,7 +2226,7 @@ const ObjectLayerEngineModal = {
1139
2226
  </div>
1140
2227
  <div class="in fll ${idSectionA}-col-b">
1141
2228
  <div class="in section-mp-border" style="width: 135px;">
1142
- ${await Input.Render({
2229
+ ${await Input.instance({
1143
2230
  id: `ol-input-render-frame-duration`,
1144
2231
  label: html`<div class="inl ol-number-label">
1145
2232
  <i class="fa-solid fa-chart-simple"></i> Frame duration
@@ -1152,25 +2239,6 @@ const ObjectLayerEngineModal = {
1152
2239
  value: ObjectLayerEngineModal.renderFrameDuration,
1153
2240
  })}
1154
2241
  </div>
1155
- <div class="in section-mp">
1156
- ${await ToggleSwitch.Render({
1157
- id: 'ol-toggle-render-is-stateless',
1158
- wrapper: true,
1159
- wrapperLabel: html`${Translate.Render('is-stateless')}`,
1160
- disabledOnClick: true,
1161
- checked: ObjectLayerEngineModal.renderIsStateless,
1162
- on: {
1163
- unchecked: () => {
1164
- ObjectLayerEngineModal.renderIsStateless = false;
1165
- console.warn('renderIsStateless', ObjectLayerEngineModal.renderIsStateless);
1166
- },
1167
- checked: () => {
1168
- ObjectLayerEngineModal.renderIsStateless = true;
1169
- console.warn('renderIsStateless', ObjectLayerEngineModal.renderIsStateless);
1170
- },
1171
- },
1172
- })}
1173
- </div>
1174
2242
  </div>
1175
2243
  </div>
1176
2244
  ${directionsCodeBarRender}
@@ -1181,25 +2249,25 @@ const ObjectLayerEngineModal = {
1181
2249
  <div class="in fll ${idSectionB}-col-a">
1182
2250
  <div class="in section-mp section-mp-border">
1183
2251
  <div class="in sub-title-modal"><i class="fa-solid fa-database"></i> Item data</div>
1184
- ${await Input.Render({
2252
+ ${await Input.instance({
1185
2253
  id: `ol-input-item-id`,
1186
- label: html`<i class="fa-solid fa-pen-to-square"></i> ${Translate.Render('item-id')}`,
2254
+ label: html`<i class="fa-solid fa-pen-to-square"></i> ${Translate.instance('item-id')}`,
1187
2255
  containerClass: '',
1188
2256
  placeholder: true,
1189
2257
  value: loadedData?.metadata?.data?.item?.id || '',
1190
2258
  })}
1191
- ${await Input.Render({
2259
+ ${await Input.instance({
1192
2260
  id: `ol-input-item-description`,
1193
- label: html`<i class="fa-solid fa-pen-to-square"></i> ${Translate.Render('item-description')}`,
2261
+ label: html`<i class="fa-solid fa-pen-to-square"></i> ${Translate.instance('item-description')}`,
1194
2262
  containerClass: '',
1195
2263
  placeholder: true,
1196
2264
  value: loadedData?.metadata?.data?.item?.description || '',
1197
2265
  })}
1198
2266
  <div class="in section-mp">
1199
- ${await DropDown.Render({
2267
+ ${await DropDown.instance({
1200
2268
  id: 'ol-dropdown-item-type',
1201
2269
  value: ObjectLayerEngineModal.selectItemType,
1202
- label: html`${Translate.Render('select-item-type')}`,
2270
+ label: html`${Translate.instance('select-item-type')}`,
1203
2271
  data: itemTypes.map((itemType) => {
1204
2272
  return {
1205
2273
  value: itemType,
@@ -1213,10 +2281,10 @@ const ObjectLayerEngineModal = {
1213
2281
  })}
1214
2282
  </div>
1215
2283
  <div class="in section-mp">
1216
- ${await ToggleSwitch.Render({
2284
+ ${await ToggleSwitch.instance({
1217
2285
  id: 'ol-toggle-item-activable',
1218
2286
  wrapper: true,
1219
- wrapperLabel: html`${Translate.Render('item-activable')}`,
2287
+ wrapperLabel: html`${Translate.instance('item-activable')}`,
1220
2288
  disabledOnClick: true,
1221
2289
  checked: ObjectLayerEngineModal.itemActivable,
1222
2290
  on: {
@@ -1236,25 +2304,65 @@ const ObjectLayerEngineModal = {
1236
2304
  <div class="in fll ${idSectionB}-col-b">
1237
2305
  <div class="in section-mp section-mp-border">
1238
2306
  <div class="in sub-title-modal"><i class="fa-solid fa-database"></i> Stats data</div>
2307
+ <div class="fl" style="align-items: flex-end; gap: 8px; flex-wrap: wrap; margin-bottom: 10px;">
2308
+ <div class="in fll" style="width: 110px;">
2309
+ ${await Input.instance({
2310
+ id: statsRandomMinInputId,
2311
+ label: html`Random min`,
2312
+ containerClass: 'inl',
2313
+ type: 'number',
2314
+ min: 0,
2315
+ max: 10,
2316
+ placeholder: true,
2317
+ value: DEFAULT_STAT_RANDOM_MIN,
2318
+ })}
2319
+ </div>
2320
+ <div class="in fll" style="width: 110px;">
2321
+ ${await Input.instance({
2322
+ id: statsRandomMaxInputId,
2323
+ label: html`Random max`,
2324
+ containerClass: 'inl',
2325
+ type: 'number',
2326
+ min: 0,
2327
+ max: 10,
2328
+ placeholder: true,
2329
+ value: DEFAULT_STAT_RANDOM_MAX,
2330
+ })}
2331
+ </div>
2332
+ <div class="in fll">
2333
+ ${await BtnIcon.instance({
2334
+ label: html`<i class="fa-solid fa-dice"></i> Randomize`,
2335
+ class: statsRandomizeBtnClass,
2336
+ })}
2337
+ </div>
2338
+ </div>
1239
2339
  ${statsInputsRender}
1240
2340
  </div>
1241
2341
  </div>
1242
2342
  </div>
1243
2343
 
1244
2344
  <div class="fl section-mp">
1245
- ${await BtnIcon.Render({
1246
- label: html`<i class="submit-btn-icon fa-solid fa-folder-open"></i> ${Translate.Render('save')}`,
2345
+ ${await BtnIcon.instance({
2346
+ label: html`<i class="submit-btn-icon fa-solid fa-folder-open"></i>
2347
+ ${ObjectLayerEngineModal.existingObjectLayerId ? 'Update' : Translate.instance('save')}`,
1247
2348
  class: `in flr ol-btn-save`,
1248
2349
  })}
1249
- ${await BtnIcon.Render({
1250
- label: html`<i class="submit-btn-icon fa-solid fa-broom"></i> ${Translate.Render('reset')}`,
2350
+ ${ObjectLayerEngineModal.existingObjectLayerId
2351
+ ? await BtnIcon.instance({
2352
+ label: html`<i class="submit-btn-icon fa-solid fa-clone"></i> Clone`,
2353
+ class: `in flr ol-btn-clone`,
2354
+ })
2355
+ : ''}
2356
+ ${await BtnIcon.instance({
2357
+ label: html`<i class="submit-btn-icon fa-solid fa-broom"></i> ${Translate.instance('reset')}`,
1251
2358
  class: `in flr ol-btn-reset`,
1252
2359
  })}
1253
2360
  </div>
1254
2361
  <div class="in section-mp"></div>
1255
2362
  `;
1256
- },
1257
- getDirectionsFromDirectionCode(directionCode = '08') {
2363
+ };
2364
+
2365
+ static getDirectionsFromDirectionCode(directionCode = '08') {
1258
2366
  let objectLayerFrameDirections = [];
1259
2367
 
1260
2368
  switch (directionCode) {
@@ -1285,8 +2393,9 @@ const ObjectLayerEngineModal = {
1285
2393
  }
1286
2394
 
1287
2395
  return objectLayerFrameDirections;
1288
- },
1289
- toManagement: async (id = null) => {
2396
+ }
2397
+
2398
+ static toManagement = async (id = null) => {
1290
2399
  await ObjectLayerEngineModal.clearData();
1291
2400
  const subModalId = 'management';
1292
2401
  const modalId = `modal-object-layer-engine-${subModalId}`;
@@ -1313,8 +2422,9 @@ const ObjectLayerEngineModal = {
1313
2422
  await DefaultManagement.waitGridReady(modalId);
1314
2423
  await DefaultManagement.loadTable(modalId, { force: true, reload: true });
1315
2424
  });
1316
- },
1317
- Reload: async function () {
2425
+ };
2426
+
2427
+ static async Reload() {
1318
2428
  // Clear data before reload to prevent contamination
1319
2429
  ObjectLayerEngineModal.clearData();
1320
2430
  const idModal = 'modal-object-layer-engine';
@@ -1323,7 +2433,7 @@ const ObjectLayerEngineModal = {
1323
2433
  idModal,
1324
2434
  html: await Modal.Data[idModal].options.html(),
1325
2435
  });
1326
- },
1327
- };
2436
+ }
2437
+ }
1328
2438
 
1329
2439
  export { ObjectLayerEngineModal };