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
@@ -21,34 +21,28 @@
21
21
  *
22
22
  * UP direction derives from DOWN but colours the head area with hair instead
23
23
  * of skin (the back of the head is visible).
24
- * RIGHT direction mirrors LEFT (direction 06) template horizontally.
24
+ * The canonical side template is direction 06 (right); LEFT is derived by
25
+ * mirroring it so generated frame keys stay aligned with 04 = left and 06 = right.
25
26
  *
26
27
  * @module src/server/semantic-layer-generator-skin.js
27
28
  * @namespace SemanticLayerGeneratorSkin
28
29
  */
29
-
30
30
  import { readFileSync } from 'fs';
31
31
  import path from 'path';
32
32
  import { fileURLToPath } from 'url';
33
33
  import crypto from 'crypto';
34
-
35
34
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
35
  const TEMPLATES_DIR = path.resolve(__dirname, '../client/public/cyberia/assets/templates');
37
-
38
36
  /* ─── Grid dimension (must match GRID_DIM in main generator) ────────────── */
39
37
  const SKIN_GRID_DIM = 24;
40
-
41
38
  /* ─── Y threshold: pixels at or above this row are considered "head" ─────── */
42
39
  const HEAD_Y_MAX = 12;
43
-
44
40
  /* ─── Y threshold: pixels at or above (>=) this row are "shoe" zone ──────── */
45
41
  const SHOE_Y_MIN = 23;
46
-
47
42
  /* ═══════════════════════════════════════════════════════════════════════════
48
43
  * TEMPLATE LOADING
49
44
  * Templates are loaded once at module initialisation (synchronous I/O).
50
45
  * ═══════════════════════════════════════════════════════════════════════════ */
51
-
52
46
  /**
53
47
  * Loads a JSON template file and returns the parsed value.
54
48
  * Returns [] on any error so the generator degrades gracefully.
@@ -62,7 +56,6 @@ function loadTemplate(filename) {
62
56
  return [];
63
57
  }
64
58
  }
65
-
66
59
  /**
67
60
  * Reads a full skin template (item-skin-08.json / item-skin-06.json) and
68
61
  * extracts the black (#000000) outline pixels from its `color` grid.
@@ -90,7 +83,6 @@ function loadBorderFromTemplate(filename) {
90
83
  return [];
91
84
  }
92
85
  }
93
-
94
86
  /**
95
87
  * Raw template pixel lists keyed by direction and zone name.
96
88
  * Coordinates are [x, y] pairs where (0,0) is the top-left cell of a
@@ -98,8 +90,8 @@ function loadBorderFromTemplate(filename) {
98
90
  *
99
91
  * @type {{ down: Object, left: Object }}
100
92
  */
101
- const RAW = {
102
- down: {
93
+ class RAW {
94
+ static down = {
103
95
  /** Full body silhouette – face, torso, arms, legs */
104
96
  skin: loadTemplate('item-skin-style-skin-08.json'),
105
97
  /** Shirt / breastplate zone */
@@ -110,16 +102,15 @@ const RAW = {
110
102
  hair: loadTemplate('item-skin-style-hair.json'),
111
103
  /** Black outline border from full template color grid */
112
104
  border: loadBorderFromTemplate('item-skin-08.json'),
113
- },
114
- left: {
105
+ };
106
+ static left = {
115
107
  skin: loadTemplate('item-skin-style-skin-06.json'),
116
108
  shirt: loadTemplate('item-skin-style-breastplate-06.json'),
117
109
  legs: loadTemplate('item-skin-style-legs-06.json'),
118
110
  hair: loadTemplate('item-skin-style-hair.json'),
119
111
  border: loadBorderFromTemplate('item-skin-06.json'),
120
- },
121
- };
122
-
112
+ };
113
+ }
123
114
  /**
124
115
  * Splits the legs template into pants (y < SHOE_Y_MIN) and shoes (y >= SHOE_Y_MIN).
125
116
  * @param {number[][]} legsCoords
@@ -134,7 +125,6 @@ function splitLegs(legsCoords) {
134
125
  }
135
126
  return { pants, shoes };
136
127
  }
137
-
138
128
  /**
139
129
  * Mirrors a pixel list horizontally so a left-facing sprite becomes right-facing.
140
130
  * Uses the reference width 26 (template grid): x_new = 25 − x.
@@ -147,7 +137,6 @@ function mirrorH(coords) {
147
137
  .map(([x, y]) => [25 - x, y])
148
138
  .filter(([x, y]) => x >= 0 && x < SKIN_GRID_DIM && y >= 0 && y < SKIN_GRID_DIM);
149
139
  }
150
-
151
140
  /**
152
141
  * Per-direction pixel zone definitions.
153
142
  * Shoes are separated from pants; hair and skin are provided per direction.
@@ -160,33 +149,27 @@ const ZONES = (() => {
160
149
  const { pants, shoes } = splitLegs(RAW.down.legs);
161
150
  return { skin: RAW.down.skin, shirt: RAW.down.shirt, pants, shoes, hair: RAW.down.hair, border: RAW.down.border };
162
151
  })();
163
-
164
- const left = (() => {
152
+ const right = (() => {
165
153
  const { pants, shoes } = splitLegs(RAW.left.legs);
166
154
  return { skin: RAW.left.skin, shirt: RAW.left.shirt, pants, shoes, hair: RAW.left.hair, border: RAW.left.border };
167
155
  })();
168
-
169
- const right = {
170
- skin: mirrorH(left.skin),
171
- shirt: mirrorH(left.shirt),
172
- pants: mirrorH(left.pants),
173
- shoes: mirrorH(left.shoes),
174
- hair: mirrorH(left.hair),
175
- border: mirrorH(left.border),
156
+ const left = {
157
+ skin: mirrorH(right.skin),
158
+ shirt: mirrorH(right.shirt),
159
+ pants: mirrorH(right.pants),
160
+ shoes: mirrorH(right.shoes),
161
+ hair: mirrorH(right.hair),
162
+ border: mirrorH(right.border),
176
163
  };
177
-
178
164
  // UP: same body layout as DOWN but head pixels (y <= HEAD_Y_MAX) are painted
179
165
  // with hair color (back of character's head), body/arms remain skin colored.
180
166
  // Reuse down zones; we flag this dynamically in buildDirectionMatrix.
181
167
  const up = { ...down, isUpDirection: true };
182
-
183
168
  return { down, left, right, up };
184
169
  })();
185
-
186
170
  /* ═══════════════════════════════════════════════════════════════════════════
187
171
  * COLOUR UTILITIES
188
172
  * ═══════════════════════════════════════════════════════════════════════════ */
189
-
190
173
  /**
191
174
  * Mini LCG-based RNG (32-bit seed). Avoids importing from the parent module.
192
175
  * @param {number} seed
@@ -199,7 +182,6 @@ function lcgRng(seed) {
199
182
  return s / 4294967296;
200
183
  };
201
184
  }
202
-
203
185
  /**
204
186
  * Simple string hash → 32-bit unsigned integer.
205
187
  * @param {string} str
@@ -210,7 +192,6 @@ function hashStr(str) {
210
192
  for (let i = 0; i < str.length; i++) h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
211
193
  return h >>> 0;
212
194
  }
213
-
214
195
  /**
215
196
  * HSL colour to [r, g, b] (each 0–255).
216
197
  * @param {number} h Hue in [0, 1).
@@ -230,17 +211,14 @@ function hslToRgb(h, s, l) {
230
211
  };
231
212
  return [Math.round(hue2rgb(h + 1 / 3) * 255), Math.round(hue2rgb(h) * 255), Math.round(hue2rgb(h - 1 / 3) * 255)];
232
213
  }
233
-
234
214
  /** Clamp v to [lo, hi]. */
235
215
  function clamp(v, lo, hi) {
236
216
  return v < lo ? lo : v > hi ? hi : v;
237
217
  }
238
-
239
218
  /* ═══════════════════════════════════════════════════════════════════════════
240
219
  * PALETTE DERIVATION
241
220
  * All colours are deterministic from (seed, itemId).
242
221
  * ═══════════════════════════════════════════════════════════════════════════ */
243
-
244
222
  /**
245
223
  * @typedef {Object} SkinPalette
246
224
  * @property {number[]} skinColor RGBA – face / hands / neck
@@ -250,7 +228,6 @@ function clamp(v, lo, hi) {
250
228
  * @property {number[]} shoeColor RGBA – shoes / boots
251
229
  * @property {number} hairDepth Max Y row (inclusive) covered by hair; controls hair length.
252
230
  */
253
-
254
231
  /**
255
232
  * Derives a fully deterministic 5-colour palette for a skin variant.
256
233
  *
@@ -261,7 +238,6 @@ function clamp(v, lo, hi) {
261
238
  */
262
239
  function deriveSkinPalette(seed, itemId, subtype = 'random') {
263
240
  const rng = lcgRng(hashStr(`${seed}:${itemId}:skin-palette`));
264
-
265
241
  /* ── Skin tones: dark brown → light peach ───────────────────────────── */
266
242
  const allSkinTones = [
267
243
  [38, 22, 14], // near-black
@@ -277,7 +253,6 @@ function deriveSkinPalette(seed, itemId, subtype = 'random') {
277
253
  const skinPool =
278
254
  subtype === 'dark' ? allSkinTones.slice(0, 3) : subtype === 'light' ? allSkinTones.slice(5) : allSkinTones;
279
255
  const skinRgb = skinPool[Math.floor(rng() * skinPool.length)];
280
-
281
256
  /* ── Hair: natural shades + vivid options ───────────────────────────── */
282
257
  const naturalHair = [
283
258
  [18, 12, 8], // near-black
@@ -302,21 +277,17 @@ function deriveSkinPalette(seed, itemId, subtype = 'random') {
302
277
  const allHairPresets = [...naturalHair, ...vividHair];
303
278
  const hairPool = subtype === 'vivid' ? vividHair : subtype === 'natural' ? naturalHair : allHairPresets;
304
279
  const hairRgb = hairPool[Math.floor(rng() * hairPool.length)];
305
-
306
280
  /* ── Clothing: random hue, reasonable saturation/lightness ─────────── */
307
281
  const shirtRgb = hslToRgb(rng(), 0.6 + rng() * 0.35, 0.32 + rng() * 0.28);
308
282
  const pantsRgb = hslToRgb(rng(), 0.55 + rng() * 0.4, 0.2 + rng() * 0.3);
309
-
310
283
  /* Shoes tend to be darker (lower lightness) */
311
284
  const shoeH = rng();
312
285
  const shoeRgb = hslToRgb(shoeH, 0.35 + rng() * 0.4, 0.12 + rng() * 0.2);
313
-
314
286
  /* ── Hair depth: controls how far down the hair goes on the head ───── *
315
287
  * 0 → shaved (no hair at all — bald silhouette) *
316
288
  * 5 – 11 → short crop to long flowing hair */
317
289
  const hairDepthOptions = subtype === 'shaved' ? [0] : [5, 6, 7, 8, 9, 10, 11];
318
290
  const hairDepth = hairDepthOptions[Math.floor(rng() * hairDepthOptions.length)];
319
-
320
291
  return {
321
292
  skinColor: [...skinRgb, 255],
322
293
  hairColor: [...hairRgb, 255],
@@ -326,11 +297,9 @@ function deriveSkinPalette(seed, itemId, subtype = 'random') {
326
297
  hairDepth,
327
298
  };
328
299
  }
329
-
330
300
  /* ═══════════════════════════════════════════════════════════════════════════
331
301
  * FRAME MATRIX BUILDER
332
302
  * ═══════════════════════════════════════════════════════════════════════════ */
333
-
334
303
  /**
335
304
  * Returns the index of `rgba` in `globalColors`, adding it if absent.
336
305
  * @param {number[][]} globalColors
@@ -345,7 +314,6 @@ function getOrAddColor(globalColors, rgba) {
345
314
  globalColors.push([...rgba]);
346
315
  return globalColors.length - 1;
347
316
  }
348
-
349
317
  /**
350
318
  * Paints a list of [x,y] coordinates onto a frame matrix with a color index.
351
319
  * Skips coordinates outside SKIN_GRID_DIM.
@@ -360,7 +328,6 @@ function paint(matrix, coords, colorIdx) {
360
328
  }
361
329
  }
362
330
  }
363
-
364
331
  /**
365
332
  * Builds a 24 × 24 frame matrix for DOWN, LEFT, or RIGHT directions.
366
333
  *
@@ -384,25 +351,20 @@ function paint(matrix, coords, colorIdx) {
384
351
  */
385
352
  function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLabel) {
386
353
  const matrix = Array.from({ length: SKIN_GRID_DIM }, () => Array(SKIN_GRID_DIM).fill(0));
387
-
388
354
  const skinIdx = getOrAddColor(globalColors, palette.skinColor);
389
355
  const hairIdx = getOrAddColor(globalColors, palette.hairColor);
390
356
  const shirtIdx = getOrAddColor(globalColors, palette.shirtColor);
391
357
  const pantsIdx = getOrAddColor(globalColors, palette.pantsColor);
392
358
  const shoeIdx = getOrAddColor(globalColors, palette.shoeColor);
393
-
394
359
  // Sets for fast per-pixel lookup
395
360
  const borderSet = new Set(zones.border.map(([x, y]) => `${x},${y}`));
396
361
  const skinSet = new Set(zones.skin.map(([x, y]) => `${x},${y}`));
397
-
398
362
  // 1. Full body silhouette → skin tone
399
363
  paint(matrix, zones.skin, skinIdx);
400
-
401
364
  // Compute hair pixels and per-row bounding boxes once — shared by steps 2b and 2c.
402
365
  // hairDepth === 0 → shaved head, all hair steps are skipped.
403
366
  const hairPixels =
404
367
  palette.hairDepth > 0 ? zones.skin.filter(([x, y]) => y <= palette.hairDepth && !borderSet.has(`${x},${y}`)) : [];
405
-
406
368
  const hairRowBounds = new Map();
407
369
  for (const [x, y] of hairPixels) {
408
370
  const b = hairRowBounds.get(y) || { min: x, max: x };
@@ -411,19 +373,15 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
411
373
  hairRowBounds.set(y, b);
412
374
  }
413
375
  const hairRows = [...hairRowBounds.keys()].sort((a, b) => a - b);
414
-
415
376
  if (palette.hairDepth > 0) {
416
377
  // 2. Hair fill
417
378
  paint(matrix, hairPixels, hairIdx);
418
-
419
379
  const blackIdx = getOrAddColor(globalColors, [0, 0, 0, 255]);
420
-
421
380
  // 2b. Black hairline + bang wisps at the fringe (bottom of hair zone).
422
381
  if (hairRows.length > 0) {
423
382
  const rngFringe = lcgRng(hashStr(`${seed}:${itemId}:fringe-${dirLabel}`));
424
383
  const bottomY = hairRows[hairRows.length - 1];
425
384
  const { min: fMin, max: fMax } = hairRowBounds.get(bottomY);
426
-
427
385
  // Build 2–4 random bang-wisp columns
428
386
  const fringeCols = [];
429
387
  for (let x = fMin; x <= fMax; x++) fringeCols.push(x);
@@ -449,7 +407,6 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
449
407
  matrix[tipY][wx] = blackIdx;
450
408
  }
451
409
  }
452
-
453
410
  // Black hairline for non-wisp fringe columns
454
411
  const fringeY = bottomY + 1;
455
412
  if (fringeY < SKIN_GRID_DIM) {
@@ -460,7 +417,6 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
460
417
  }
461
418
  }
462
419
  }
463
-
464
420
  // 2c. SIDE HAIR STRANDS — 1-px strands at each temple, anchored at the
465
421
  // outermost border columns of the head at HEAD_Y_MAX (ear level).
466
422
  // Fixed anchor means position is independent of hairDepth, giving a
@@ -472,36 +428,28 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
472
428
  {
473
429
  const rngWisp = lcgRng(hashStr(`${seed}:${itemId}:outer-wisp-${dirLabel}`));
474
430
  const strandRows = palette.hairDepth >= 5 ? Math.min(palette.hairDepth - 4, 3) : 0;
475
-
476
431
  if (strandRows > 0) {
477
432
  const borderAtHead = zones.border.filter(([, y]) => y === HEAD_Y_MAX);
478
433
  if (borderAtHead.length >= 2) {
479
434
  const hbL = Math.min(...borderAtHead.map(([x]) => x));
480
435
  const hbR = Math.max(...borderAtHead.map(([x]) => x));
481
-
482
436
  let lastLx = null,
483
437
  lastRx = null;
484
-
485
438
  for (let row = 0; row < strandRows; row++) {
486
439
  const y = HEAD_Y_MAX + 1 + row; // just below ear level
487
440
  if (y >= SKIN_GRID_DIM) break;
488
441
  const ext = rngWisp() < 0.3 ? 2 : 1;
489
-
490
442
  const lMin = Math.max(0, hbL - ext);
491
443
  const rMax = Math.min(SKIN_GRID_DIM - 1, hbR + ext);
492
-
493
444
  // Left strand: pixels outside the body to the left of hbL
494
445
  for (let x = lMin; x < hbL; x++) matrix[y][x] = hairIdx;
495
446
  // Right strand: pixels outside the body to the right of hbR
496
447
  for (let x = hbR + 1; x <= rMax; x++) matrix[y][x] = hairIdx;
497
-
498
448
  if (lMin > 0 && matrix[y][lMin - 1] !== hairIdx) matrix[y][lMin - 1] = blackIdx;
499
449
  if (rMax < SKIN_GRID_DIM - 1 && matrix[y][rMax + 1] !== hairIdx) matrix[y][rMax + 1] = blackIdx;
500
-
501
450
  lastLx = lMin;
502
451
  lastRx = rMax;
503
452
  }
504
-
505
453
  const tipY = HEAD_Y_MAX + 1 + strandRows;
506
454
  if (tipY < SKIN_GRID_DIM) {
507
455
  if (lastLx !== null) matrix[tipY][Math.max(0, lastLx)] = blackIdx;
@@ -510,7 +458,6 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
510
458
  }
511
459
  }
512
460
  }
513
-
514
461
  // 2d. UPPER SIDE HAIR ARCH — thick arch on each temple anchored at y=10.
515
462
  // The arch is widest at its crown (y=10, 4-5 px outward from the border)
516
463
  // and tapers toward the ear base, creating a visible sideburn arc.
@@ -520,39 +467,31 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
520
467
  const UPPER_ANCHOR_Y = HEAD_Y_MAX - 2; // y=10
521
468
  const rngWisp2 = lcgRng(hashStr(`${seed}:${itemId}:outer-wisp2-${dirLabel}`));
522
469
  const strandRows2 = palette.hairDepth >= 5 ? Math.min(palette.hairDepth - 4, 3) : 0;
523
-
524
470
  if (strandRows2 > 0) {
525
471
  const borderAtUpper = zones.border.filter(([, y]) => y === UPPER_ANCHOR_Y);
526
472
  if (borderAtUpper.length >= 2) {
527
473
  const hbL2 = Math.min(...borderAtUpper.map(([x]) => x));
528
474
  const hbR2 = Math.max(...borderAtUpper.map(([x]) => x));
529
-
530
475
  // Arch widths per row: narrow at crown (y=10), widest at ear base (y=12).
531
476
  // Follows the head silhouette arc — wider where the head is wider.
532
477
  const extBase = [2, 3, 4];
533
478
  let lastLx2 = null,
534
479
  lastRx2 = null;
535
-
536
480
  for (let row = 0; row < strandRows2; row++) {
537
481
  const y = UPPER_ANCHOR_Y + row; // y=10, y=11, y=12
538
482
  if (y >= SKIN_GRID_DIM) break;
539
483
  const ext2 = extBase[row] + (rngWisp2() < 0.4 ? 1 : 0); // 40 % +1 bonus
540
-
541
484
  const innerL2 = hbL2 + 2; // 2 px toward center
542
485
  const innerR2 = hbR2 - 2;
543
486
  const lMin2 = Math.max(0, innerL2 - ext2);
544
487
  const rMax2 = Math.min(SKIN_GRID_DIM - 1, innerR2 + ext2);
545
-
546
488
  for (let x = lMin2; x < innerL2; x++) matrix[y][x] = hairIdx;
547
489
  for (let x = innerR2 + 1; x <= rMax2; x++) matrix[y][x] = hairIdx;
548
-
549
490
  if (lMin2 > 0 && matrix[y][lMin2 - 1] !== hairIdx) matrix[y][lMin2 - 1] = blackIdx;
550
491
  if (rMax2 < SKIN_GRID_DIM - 1 && matrix[y][rMax2 + 1] !== hairIdx) matrix[y][rMax2 + 1] = blackIdx;
551
-
552
492
  lastLx2 = lMin2;
553
493
  lastRx2 = rMax2;
554
494
  }
555
-
556
495
  const tipY2 = UPPER_ANCHOR_Y + strandRows2; // first row after arch
557
496
  if (tipY2 < SKIN_GRID_DIM) {
558
497
  if (lastLx2 !== null && matrix[tipY2][Math.max(0, lastLx2)] !== hairIdx)
@@ -563,13 +502,12 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
563
502
  }
564
503
  }
565
504
  }
566
-
567
505
  // 2e. HAIR EDGE DISTORTIONS — 5–9 stray hair pixels scattered along the
568
506
  // outer border of the hair zone for an organic, hand-drawn look.
569
507
  // Each pixel sits one step outside the body silhouette and is closed
570
508
  // with a black tip pixel for pixel-art definition.
571
509
  {
572
- const rngDist = lcgRng(hashStr(`${seed}:${itemId}:hair-distort-${dirLabel}`));
510
+ const rngDist = lcgRng(hashStr(`${seed}:${itemId}:hair-distort-${dirLabel}`));
573
511
  // Collect the outermost border column per hair row.
574
512
  const borderInHair = zones.border.filter(([, y]) => y <= palette.hairDepth);
575
513
  const bhrMap = new Map();
@@ -579,14 +517,14 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
579
517
  b.max = Math.max(b.max, x);
580
518
  bhrMap.set(y, b);
581
519
  }
582
- const bhrRows = [...bhrMap.keys()];
520
+ const bhrRows = [...bhrMap.keys()];
583
521
  const numDistort = 5 + Math.floor(rngDist() * 5); // 5–9
584
522
  for (let i = 0; i < numDistort; i++) {
585
- const y = bhrRows[Math.floor(rngDist() * bhrRows.length)];
523
+ const y = bhrRows[Math.floor(rngDist() * bhrRows.length)];
586
524
  const bnd = bhrMap.get(y);
587
525
  if (!bnd) continue;
588
526
  const side = rngDist() < 0.5 ? -1 : 1;
589
- const px = side < 0 ? bnd.min - 1 : bnd.max + 1;
527
+ const px = side < 0 ? bnd.min - 1 : bnd.max + 1;
590
528
  if (px >= 0 && px < SKIN_GRID_DIM && matrix[y][px] !== hairIdx) {
591
529
  matrix[y][px] = hairIdx;
592
530
  const bx = px + side;
@@ -594,38 +532,31 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
594
532
  }
595
533
  }
596
534
  }
597
-
598
535
  // 2f. MID-CROWN SIDE HAIR ARC — 7-row arc of hair strands following the
599
536
  // head edge, centered at y=7 (crown shoulders). Right strand anchors
600
537
  // near x=8 (character right), left mirrors it at x≈15. Width peaks
601
538
  // at the center row (3 px outward) and tapers to 1 px at the extremes.
602
539
  // Per-row 40 % width bonus + 30 % extra distortion pixel per side.
603
540
  {
604
- const CROWN_Y = 7;
605
- const crownW = [3, 2, 2, 1]; // base width at |dy| = 0, 1, 2, 3
541
+ const CROWN_Y = 7;
542
+ const crownW = [3, 2, 2, 1]; // base width at |dy| = 0, 1, 2, 3
606
543
  const rngCrown = lcgRng(hashStr(`${seed}:${itemId}:crown-side-${dirLabel}`));
607
-
608
544
  for (let dy = -3; dy <= 3; dy++) {
609
545
  const y = CROWN_Y + dy;
610
546
  if (y < 0 || y >= SKIN_GRID_DIM) continue;
611
-
612
547
  const borderAtY = zones.border.filter(([, by]) => by === y);
613
548
  if (borderAtY.length < 2) continue;
614
549
  const hbL = Math.min(...borderAtY.map(([x]) => x));
615
550
  const hbR = Math.max(...borderAtY.map(([x]) => x));
616
-
617
551
  const ext = crownW[Math.abs(dy)] + (rngCrown() < 0.4 ? 1 : 0);
618
-
619
552
  // Left strand (character right, viewer left)
620
553
  const lMin = Math.max(0, hbL - ext);
621
554
  for (let x = lMin; x < hbL; x++) matrix[y][x] = hairIdx;
622
555
  if (lMin > 0 && matrix[y][lMin - 1] !== hairIdx) matrix[y][lMin - 1] = blackIdx;
623
-
624
556
  // Right strand (character left, viewer right)
625
557
  const rMax = Math.min(SKIN_GRID_DIM - 1, hbR + ext);
626
558
  for (let x = hbR + 1; x <= rMax; x++) matrix[y][x] = hairIdx;
627
559
  if (rMax < SKIN_GRID_DIM - 1 && matrix[y][rMax + 1] !== hairIdx) matrix[y][rMax + 1] = blackIdx;
628
-
629
560
  // Distortion: 30 % chance of one extra pixel per side
630
561
  if (rngCrown() < 0.3) {
631
562
  const px = lMin - 1;
@@ -644,16 +575,13 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
644
575
  }
645
576
  }
646
577
  }
647
-
648
578
  // 3. Clothes
649
579
  paint(matrix, zones.shirt, shirtIdx);
650
580
  paint(matrix, zones.pants, pantsIdx);
651
581
  paint(matrix, zones.shoes, shoeIdx);
652
-
653
582
  // 4. Black border outline — always last
654
583
  const borderIdx = getOrAddColor(globalColors, [0, 0, 0, 255]);
655
584
  paint(matrix, zones.border, borderIdx);
656
-
657
585
  // 4b. CROWN BORDER SOFTENING — for non-shaved skins, replace most outer
658
586
  // silhouette border pixels within the hair zone with hair colour so the
659
587
  // crown blends into the background rather than having a hard black outline.
@@ -668,19 +596,23 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
668
596
  for (let dx = -1; dx <= 1 && !isOuter; dx++) {
669
597
  for (let dy = -1; dy <= 1 && !isOuter; dy++) {
670
598
  if (dx === 0 && dy === 0) continue;
671
- const nx = bx + dx, ny = by + dy;
672
- if (nx < 0 || nx >= SKIN_GRID_DIM || ny < 0 || ny >= SKIN_GRID_DIM ||
673
- (!skinSet.has(`${nx},${ny}`) && !borderSet.has(`${nx},${ny}`)))
599
+ const nx = bx + dx,
600
+ ny = by + dy;
601
+ if (
602
+ nx < 0 ||
603
+ nx >= SKIN_GRID_DIM ||
604
+ ny < 0 ||
605
+ ny >= SKIN_GRID_DIM ||
606
+ (!skinSet.has(`${nx},${ny}`) && !borderSet.has(`${nx},${ny}`))
607
+ )
674
608
  isOuter = true;
675
609
  }
676
610
  }
677
611
  if (isOuter && rngBorder() < 0.75) matrix[by][bx] = hairIdx;
678
612
  }
679
613
  }
680
-
681
614
  return matrix;
682
615
  }
683
-
684
616
  /**
685
617
  * Builds a 24 × 24 frame matrix for the UP (back-facing) direction.
686
618
  *
@@ -701,27 +633,22 @@ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLab
701
633
  */
702
634
  function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
703
635
  const matrix = Array.from({ length: SKIN_GRID_DIM }, () => Array(SKIN_GRID_DIM).fill(0));
704
-
705
636
  const skinIdx = getOrAddColor(globalColors, palette.skinColor);
706
637
  const hairIdx = getOrAddColor(globalColors, palette.hairColor);
707
638
  const shirtIdx = getOrAddColor(globalColors, palette.shirtColor);
708
639
  const pantsIdx = getOrAddColor(globalColors, palette.pantsColor);
709
640
  const shoeIdx = getOrAddColor(globalColors, palette.shoeColor);
710
641
  const blackIdx = getOrAddColor(globalColors, [0, 0, 0, 255]);
711
-
712
642
  const rng = lcgRng(hashStr(`${seed}:${itemId}:up-hair`));
713
-
714
643
  // All body pixels (skin + border) used for outer-silhouette detection
715
644
  const allBodyCoords = [...zones.skin, ...zones.border];
716
645
  const bodySet = new Set(allBodyCoords.map(([x, y]) => `${x},${y}`));
717
-
718
646
  // 1. Paint full body → skin tone, then clothes, then border
719
647
  paint(matrix, zones.skin, skinIdx);
720
648
  paint(matrix, zones.shirt, shirtIdx);
721
649
  paint(matrix, zones.pants, pantsIdx);
722
650
  paint(matrix, zones.shoes, shoeIdx);
723
651
  paint(matrix, zones.border, blackIdx);
724
-
725
652
  // 1b. FACE FEATURE WIPE — the zones come from the DOWN (front-facing) template
726
653
  // which embeds eyes, pupils and mouth as black border pixels. In the UP
727
654
  // (back-of-head) view those pixels must not appear.
@@ -747,7 +674,6 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
747
674
  }
748
675
  if (!outer) matrix[by][bx] = skinIdx;
749
676
  }
750
-
751
677
  // 2. HEAD HAIR — two paths: shaved (hairDepth=0) or normal.
752
678
  //
753
679
  // SHAVED: scanline-fill the head with *skin tone* (covers inner face-feature
@@ -764,7 +690,6 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
764
690
  b.max = Math.max(b.max, x);
765
691
  headBounds.set(y, b);
766
692
  }
767
-
768
693
  // Shared helper: repaint outer head silhouette black; inner pixels stay as-is.
769
694
  // A border pixel is "outer" if any 8-connected neighbour is outside bodySet.
770
695
  const repaintOuterHeadBorder = () => {
@@ -783,7 +708,6 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
783
708
  if (outer) matrix[by][bx] = blackIdx;
784
709
  }
785
710
  };
786
-
787
711
  if (palette.hairDepth === 0) {
788
712
  // ── SHAVED HEAD ─────────────────────────────────────────────────────────
789
713
  // Fill head scanlines with skin tone (eliminates any face-feature artefacts)
@@ -798,10 +722,8 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
798
722
  for (const [y, { min, max }] of headBounds) {
799
723
  for (let x = min; x <= max; x++) matrix[y][x] = hairIdx;
800
724
  }
801
-
802
725
  // Repaint outer silhouette black; inner face-feature pixels stay as hair
803
726
  repaintOuterHeadBorder();
804
-
805
727
  // Crown border softening (UP) — same intent as 4b in buildDirectionMatrix.
806
728
  // repaintOuterHeadBorder() just set outer head border pixels to black;
807
729
  // randomly convert 75 % of them back to hair colour for a softer crown edge.
@@ -814,15 +736,15 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
814
736
  for (let dx = -1; dx <= 1 && !isOuter; dx++) {
815
737
  for (let dy = -1; dy <= 1 && !isOuter; dy++) {
816
738
  if (dx === 0 && dy === 0) continue;
817
- const nx = bx + dx, ny = by + dy;
818
- if (nx < 0 || nx >= SKIN_GRID_DIM || ny < 0 || ny >= SKIN_GRID_DIM ||
819
- !bodySet.has(`${nx},${ny}`)) isOuter = true;
739
+ const nx = bx + dx,
740
+ ny = by + dy;
741
+ if (nx < 0 || nx >= SKIN_GRID_DIM || ny < 0 || ny >= SKIN_GRID_DIM || !bodySet.has(`${nx},${ny}`))
742
+ isOuter = true;
820
743
  }
821
744
  }
822
745
  if (isOuter && rngBorder() < 0.75) matrix[by][bx] = hairIdx;
823
746
  }
824
747
  }
825
-
826
748
  // 2b side. SIDE HAIR STRANDS (UP) — identical geometry to the other three
827
749
  // directions: 1-px strands just below HEAD_Y_MAX, anchored at the
828
750
  // head border bounds at that row.
@@ -833,28 +755,21 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
833
755
  const strandRows = Math.min(palette.hairDepth - 4, 3);
834
756
  const hbL = headAtMax.min;
835
757
  const hbR = headAtMax.max;
836
-
837
758
  let lastLx = null,
838
759
  lastRx = null;
839
-
840
760
  for (let row = 0; row < strandRows; row++) {
841
761
  const y = HEAD_Y_MAX + 1 + row;
842
762
  if (y >= SKIN_GRID_DIM) break;
843
763
  const ext = rngWisp() < 0.3 ? 2 : 1;
844
-
845
764
  const lMin = Math.max(0, hbL - ext);
846
765
  const rMax = Math.min(SKIN_GRID_DIM - 1, hbR + ext);
847
-
848
766
  for (let x = lMin; x < hbL; x++) matrix[y][x] = hairIdx;
849
767
  for (let x = hbR + 1; x <= rMax; x++) matrix[y][x] = hairIdx;
850
-
851
768
  if (lMin > 0 && matrix[y][lMin - 1] !== hairIdx) matrix[y][lMin - 1] = blackIdx;
852
769
  if (rMax < SKIN_GRID_DIM - 1 && matrix[y][rMax + 1] !== hairIdx) matrix[y][rMax + 1] = blackIdx;
853
-
854
770
  lastLx = lMin;
855
771
  lastRx = rMax;
856
772
  }
857
-
858
773
  const tipY = HEAD_Y_MAX + 1 + strandRows;
859
774
  if (tipY < SKIN_GRID_DIM) {
860
775
  if (lastLx !== null) matrix[tipY][Math.max(0, lastLx)] = blackIdx;
@@ -862,7 +777,6 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
862
777
  }
863
778
  }
864
779
  }
865
-
866
780
  // 2c side. UPPER SIDE HAIR ARCH (UP) — mirrors 2d in buildDirectionMatrix;
867
781
  // thick arch starting at y=10, widest at the crown.
868
782
  {
@@ -873,31 +787,24 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
873
787
  const strandRows2 = Math.min(palette.hairDepth - 4, 3);
874
788
  const hbL2 = headAtUpper.min;
875
789
  const hbR2 = headAtUpper.max;
876
-
877
790
  const extBase = [2, 3, 4];
878
791
  let lastLx2 = null,
879
792
  lastRx2 = null;
880
-
881
793
  for (let row = 0; row < strandRows2; row++) {
882
794
  const y = UPPER_ANCHOR_Y + row; // y=10, y=11, y=12
883
795
  if (y >= SKIN_GRID_DIM) break;
884
796
  const ext2 = extBase[row] + (rngWisp2() < 0.4 ? 1 : 0);
885
-
886
797
  const innerL2 = hbL2 + 2; // 2 px toward center
887
798
  const innerR2 = hbR2 - 2;
888
799
  const lMin2 = Math.max(0, innerL2 - ext2);
889
800
  const rMax2 = Math.min(SKIN_GRID_DIM - 1, innerR2 + ext2);
890
-
891
801
  for (let x = lMin2; x < innerL2; x++) matrix[y][x] = hairIdx;
892
802
  for (let x = innerR2 + 1; x <= rMax2; x++) matrix[y][x] = hairIdx;
893
-
894
803
  if (lMin2 > 0 && matrix[y][lMin2 - 1] !== hairIdx) matrix[y][lMin2 - 1] = blackIdx;
895
804
  if (rMax2 < SKIN_GRID_DIM - 1 && matrix[y][rMax2 + 1] !== hairIdx) matrix[y][rMax2 + 1] = blackIdx;
896
-
897
805
  lastLx2 = lMin2;
898
806
  lastRx2 = rMax2;
899
807
  }
900
-
901
808
  const tipY2 = UPPER_ANCHOR_Y + strandRows2;
902
809
  if (tipY2 < SKIN_GRID_DIM) {
903
810
  if (lastLx2 !== null && matrix[tipY2][Math.max(0, lastLx2)] !== hairIdx)
@@ -907,20 +814,19 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
907
814
  }
908
815
  }
909
816
  }
910
-
911
817
  // 2d. HEAD HAIR EDGE DISTORTIONS (UP) — 5–9 stray pixels along the outer
912
818
  // head silhouette for organic texture. Identical approach to 2e in
913
819
  // buildDirectionMatrix, using headBounds for column references.
914
820
  {
915
- const rngDist = lcgRng(hashStr(`${seed}:${itemId}:hair-distort-up`));
916
- const hbKeys = [...headBounds.keys()];
821
+ const rngDist = lcgRng(hashStr(`${seed}:${itemId}:hair-distort-up`));
822
+ const hbKeys = [...headBounds.keys()];
917
823
  const numDistort = 5 + Math.floor(rngDist() * 5);
918
824
  for (let i = 0; i < numDistort; i++) {
919
- const y = hbKeys[Math.floor(rngDist() * hbKeys.length)];
825
+ const y = hbKeys[Math.floor(rngDist() * hbKeys.length)];
920
826
  const bnd = headBounds.get(y);
921
827
  if (!bnd) continue;
922
828
  const side = rngDist() < 0.5 ? -1 : 1;
923
- const px = side < 0 ? bnd.min - 1 : bnd.max + 1;
829
+ const px = side < 0 ? bnd.min - 1 : bnd.max + 1;
924
830
  if (px >= 0 && px < SKIN_GRID_DIM && matrix[y][px] !== hairIdx) {
925
831
  matrix[y][px] = hairIdx;
926
832
  const bx = px + side;
@@ -928,15 +834,13 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
928
834
  }
929
835
  }
930
836
  }
931
-
932
837
  // 2e. MID-CROWN SIDE HAIR ARC (UP) — mirrors 2f in buildDirectionMatrix;
933
838
  // same 7-row arc centered at y=7, using headBounds for the silhouette
934
839
  // edge reference instead of zones.border.
935
840
  {
936
- const CROWN_Y = 7;
937
- const crownW = [3, 2, 2, 1];
841
+ const CROWN_Y = 7;
842
+ const crownW = [3, 2, 2, 1];
938
843
  const rngCrown = lcgRng(hashStr(`${seed}:${itemId}:crown-side-up`));
939
-
940
844
  for (let dy = -3; dy <= 3; dy++) {
941
845
  const y = CROWN_Y + dy;
942
846
  if (y < 0 || y >= SKIN_GRID_DIM) continue;
@@ -944,17 +848,13 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
944
848
  if (!bnd) continue;
945
849
  const hbL = bnd.min;
946
850
  const hbR = bnd.max;
947
-
948
851
  const ext = crownW[Math.abs(dy)] + (rngCrown() < 0.4 ? 1 : 0);
949
-
950
852
  const lMin = Math.max(0, hbL - ext);
951
853
  for (let x = lMin; x < hbL; x++) matrix[y][x] = hairIdx;
952
854
  if (lMin > 0 && matrix[y][lMin - 1] !== hairIdx) matrix[y][lMin - 1] = blackIdx;
953
-
954
855
  const rMax = Math.min(SKIN_GRID_DIM - 1, hbR + ext);
955
856
  for (let x = hbR + 1; x <= rMax; x++) matrix[y][x] = hairIdx;
956
857
  if (rMax < SKIN_GRID_DIM - 1 && matrix[y][rMax + 1] !== hairIdx) matrix[y][rMax + 1] = blackIdx;
957
-
958
858
  if (rngCrown() < 0.3) {
959
859
  const px = lMin - 1;
960
860
  if (px >= 0 && matrix[y][px] !== hairIdx) {
@@ -971,12 +871,10 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
971
871
  }
972
872
  }
973
873
  }
974
-
975
874
  // 4. EXTENDED HAIR — flows below the head onto the upper back.
976
875
  // hairDepth (5–11) maps to hairExtend (2–8 rows below HEAD_Y_MAX).
977
876
  const hairExtend = Math.min(palette.hairDepth - 3, 6); // 5→2 … 9→6, capped at 6
978
877
  const extMaxY = Math.min(HEAD_Y_MAX + hairExtend, SKIN_GRID_DIM - 2);
979
-
980
878
  // Compute body-bounds for each extension row (centering reference)
981
879
  const extBounds = new Map();
982
880
  for (const [x, y] of allBodyCoords) {
@@ -987,7 +885,6 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
987
885
  extBounds.set(y, b);
988
886
  }
989
887
  const extYs = [...extBounds.keys()].sort((a, b) => a - b);
990
-
991
888
  // Track actual hair strip bounds per row (for border painting)
992
889
  const hairStripBounds = new Map();
993
890
  for (const y of extYs) {
@@ -1010,7 +907,6 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
1010
907
  max: Math.min(SKIN_GRID_DIM - 1, Math.ceil(cx) + halfW),
1011
908
  });
1012
909
  }
1013
-
1014
910
  // 5. BLACK BORDERS on sides and bottom of the hair extension.
1015
911
  for (const y of extYs) {
1016
912
  const { min, max } = hairStripBounds.get(y);
@@ -1027,7 +923,6 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
1027
923
  }
1028
924
  }
1029
925
  }
1030
-
1031
926
  // 6. RANDOM WISPS — 2–4 stray hair pixels at edges for visual dynamism.
1032
927
  // Each wisp extends 1–2 px beyond the current hair boundary,
1033
928
  // with a black tip pixel closing it off.
@@ -1054,16 +949,13 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
1054
949
  if (matrix[wy][tipX] !== hairIdx) matrix[wy][tipX] = blackIdx;
1055
950
  }
1056
951
  } // end else (normal hair)
1057
-
1058
952
  return matrix;
1059
953
  }
1060
-
1061
954
  /* ═══════════════════════════════════════════════════════════════════════════
1062
955
  * WALK FRAME BUILDER
1063
956
  * Two-frame walk cycle: frame 0 = idle pose, frame 1 = body shifted up 1px.
1064
957
  * The vertical bob creates a natural walking bounce effect.
1065
958
  * ═══════════════════════════════════════════════════════════════════════════ */
1066
-
1067
959
  /**
1068
960
  * Derives a two-frame walk cycle from a pre-built idle matrix.
1069
961
  * Frame 0: deep copy of idleMatrix.
@@ -1076,7 +968,6 @@ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
1076
968
  function buildWalkFrames(idleMatrix) {
1077
969
  const FOOT_TOP = SHOE_Y_MIN - 1; // y=22 — bottom of leg zone, just above shoes
1078
970
  const midX = Math.floor(SKIN_GRID_DIM / 2); // x=12 — column boundary between left/right foot
1079
-
1080
971
  // Build one walk frame: copy idle, then raise shoe row up 1 px for the chosen side,
1081
972
  // leaving y=SHOE_Y_MIN transparent for that foot.
1082
973
  const makeFrame = (liftLeft) => {
@@ -1089,15 +980,12 @@ function buildWalkFrames(idleMatrix) {
1089
980
  }
1090
981
  return frame;
1091
982
  };
1092
-
1093
983
  // frame0: right foot raised; frame1: left foot raised → alternating step cycle
1094
984
  return [makeFrame(false), makeFrame(true)];
1095
985
  }
1096
-
1097
986
  /* ═══════════════════════════════════════════════════════════════════════════
1098
987
  * UUID HELPER (self-contained, no import from parent)
1099
988
  * ═══════════════════════════════════════════════════════════════════════════ */
1100
-
1101
989
  /**
1102
990
  * Derives a deterministic UUID v4 from an arbitrary seed string.
1103
991
  * @param {string} seed
@@ -1110,12 +998,10 @@ function localSeedToUUIDv4(seed) {
1110
998
  const hex = hash.toString('hex');
1111
999
  return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20, 32)].join('-');
1112
1000
  }
1113
-
1114
1001
  /* ═══════════════════════════════════════════════════════════════════════════
1115
1002
  * CUSTOM MULTI-FRAME GENERATOR
1116
1003
  * Called by generateMultiFrame() when descriptor.customMultiFrameGenerator exists.
1117
1004
  * ═══════════════════════════════════════════════════════════════════════════ */
1118
-
1119
1005
  /**
1120
1006
  * Generates a complete MultiFrameResult for a skin item using template-based
1121
1007
  * pixel painting. Each of the four cardinal directions gets a properly oriented
@@ -1127,15 +1013,12 @@ function localSeedToUUIDv4(seed) {
1127
1013
  */
1128
1014
  function generateSkinMultiFrame(options, _descriptor) {
1129
1015
  const { itemId, seed, frameCount = 1, startFrame = 0, frameDuration = 250 } = options;
1130
-
1131
1016
  // Shared mutable palette; starts with index 0 = transparent
1132
1017
  const globalColors = [[0, 0, 0, 0]];
1133
-
1134
1018
  // Skin subtype is injected via the descriptor (set during registration).
1135
1019
  // Falls back to 'random' for the legacy skin- prefix or any unknown descriptor.
1136
1020
  const subtype = _descriptor?.skinSubtype ?? 'random';
1137
1021
  const palette = deriveSkinPalette(seed, itemId, subtype);
1138
-
1139
1022
  // Build idle direction matrices
1140
1023
  const matrices = {
1141
1024
  down: buildDirectionMatrix(ZONES.down, palette, globalColors, seed, itemId, 'down'),
@@ -1143,7 +1026,6 @@ function generateSkinMultiFrame(options, _descriptor) {
1143
1026
  left: buildDirectionMatrix(ZONES.left, palette, globalColors, seed, itemId, 'left'),
1144
1027
  right: buildDirectionMatrix(ZONES.right, palette, globalColors, seed, itemId, 'right'),
1145
1028
  };
1146
-
1147
1029
  // Build 2-frame walk cycles from idle matrices (shared color palette, no extra allocations)
1148
1030
  const walkFrames = {
1149
1031
  down: buildWalkFrames(matrices.down),
@@ -1151,16 +1033,12 @@ function generateSkinMultiFrame(options, _descriptor) {
1151
1033
  left: buildWalkFrames(matrices.left),
1152
1034
  right: buildWalkFrames(matrices.right),
1153
1035
  };
1154
-
1155
1036
  // Idle: frameCount identical copies of the direction matrix.
1156
1037
  const makeIdleArray = (matrix) => Array.from({ length: frameCount }, () => matrix.map((row) => [...row]));
1157
-
1158
1038
  // Walking: always exactly 2 frames (walk cycle is independent of frameCount).
1159
1039
  const makeWalkArray = (frames2) => frames2.map((m) => m.map((row) => [...row]));
1160
-
1161
1040
  const objectLayerRenderFramesData = {
1162
1041
  frame_duration: frameDuration,
1163
- is_stateless: false,
1164
1042
  frames: {
1165
1043
  // DOWN (08) idle
1166
1044
  down_idle: makeIdleArray(matrices.down),
@@ -1184,7 +1062,6 @@ function generateSkinMultiFrame(options, _descriptor) {
1184
1062
  },
1185
1063
  colors: globalColors,
1186
1064
  };
1187
-
1188
1065
  const objectLayerData = {
1189
1066
  data: {
1190
1067
  item: {
@@ -1205,7 +1082,6 @@ function generateSkinMultiFrame(options, _descriptor) {
1205
1082
  seed: localSeedToUUIDv4(`${seed}:${itemId}`),
1206
1083
  },
1207
1084
  };
1208
-
1209
1085
  // Build synthetic frames array for layer-summary logging in cyberia.js
1210
1086
  const layerSummary = [
1211
1087
  { layerKey: 'skin', layerId: `${itemId}-skin`, keys: ZONES.down.skin.map(() => ({ type: 'template' })) },
@@ -1215,7 +1091,6 @@ function generateSkinMultiFrame(options, _descriptor) {
1215
1091
  { layerKey: 'shoes', layerId: `${itemId}-shoes`, keys: ZONES.down.shoes.map(() => ({ type: 'template' })) },
1216
1092
  { layerKey: 'border', layerId: `${itemId}-border`, keys: ZONES.down.border.map(() => ({ type: 'template' })) },
1217
1093
  ];
1218
-
1219
1094
  const frames = Array.from({ length: frameCount }, (_, fi) => ({
1220
1095
  itemId,
1221
1096
  seed,
@@ -1224,7 +1099,6 @@ function generateSkinMultiFrame(options, _descriptor) {
1224
1099
  compositeFrameMatrix: matrices.down,
1225
1100
  compositeColors: globalColors,
1226
1101
  }));
1227
-
1228
1102
  return {
1229
1103
  itemId,
1230
1104
  seed,
@@ -1234,11 +1108,9 @@ function generateSkinMultiFrame(options, _descriptor) {
1234
1108
  objectLayerData,
1235
1109
  };
1236
1110
  }
1237
-
1238
1111
  /* ═══════════════════════════════════════════════════════════════════════════
1239
1112
  * REGISTRATION
1240
1113
  * ═══════════════════════════════════════════════════════════════════════════ */
1241
-
1242
1114
  /**
1243
1115
  * Registers all skin semantic descriptors.
1244
1116
  * Uses dependency injection — no import from the parent module.
@@ -1267,7 +1139,6 @@ export function registerSkinSemantics(registerFn) {
1267
1139
  { prefix: 'skin-natural', subtype: 'natural', desc: 'Natural hair colours (brown, blond, grey…)' },
1268
1140
  { prefix: 'skin-shaved', subtype: 'shaved', desc: 'Shaved / bald head — no hair' },
1269
1141
  ];
1270
-
1271
1142
  const sharedLayers = {
1272
1143
  skin: { generator: 'template-zone' },
1273
1144
  hair: { generator: 'template-zone' },
@@ -1276,7 +1147,6 @@ export function registerSkinSemantics(registerFn) {
1276
1147
  shoes: { generator: 'template-zone' },
1277
1148
  border: { generator: 'template-zone' },
1278
1149
  };
1279
-
1280
1150
  for (const { prefix, subtype, desc } of SUBTYPES) {
1281
1151
  registerFn(prefix, {
1282
1152
  semanticTags: ['character', 'body', 'humanoid'],