cyberia 3.1.3 → 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 (377) hide show
  1. package/.env.example +0 -2
  2. package/.github/workflows/engine-cyberia.cd.yml +10 -8
  3. package/.github/workflows/engine-cyberia.ci.yml +12 -29
  4. package/.github/workflows/ghpkg.ci.yml +4 -4
  5. package/.github/workflows/npmpkg.ci.yml +28 -11
  6. package/.github/workflows/publish.ci.yml +21 -2
  7. package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
  8. package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
  9. package/.github/workflows/release.cd.yml +14 -10
  10. package/CHANGELOG.md +783 -1
  11. package/CLI-HELP.md +95 -18
  12. package/Dockerfile +0 -2
  13. package/README.md +290 -220
  14. package/bin/build.js +24 -7
  15. package/bin/cyberia.js +2838 -252
  16. package/bin/deploy.js +747 -125
  17. package/bin/file.js +9 -0
  18. package/bin/index.js +2838 -252
  19. package/bin/vs.js +1 -1
  20. package/conf.js +99 -65
  21. package/deployment.yaml +18 -164
  22. package/hardhat/hardhat.config.js +13 -13
  23. package/hardhat/ignition/modules/ObjectLayerToken.js +1 -1
  24. package/hardhat/package-lock.json +2559 -5864
  25. package/hardhat/package.json +14 -23
  26. package/hardhat/scripts/deployObjectLayerToken.js +1 -1
  27. package/hardhat/test/ObjectLayerToken.js +4 -2
  28. package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
  29. package/hardhat/types/ethers-contracts/common.ts +92 -0
  30. package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
  31. package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
  32. package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
  33. package/hardhat/types/ethers-contracts/index.ts +6 -0
  34. package/jsconfig.json +1 -1
  35. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +6 -5
  36. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +6 -5
  37. package/manifests/deployment/dd-cyberia-development/deployment.yaml +18 -164
  38. package/manifests/deployment/dd-cyberia-development/proxy.yaml +7 -79
  39. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  40. package/manifests/deployment/dd-test-development/deployment.yaml +112 -28
  41. package/manifests/deployment/dd-test-development/proxy.yaml +46 -1
  42. package/manifests/deployment/playwright/deployment.yaml +1 -1
  43. package/nodemon.json +1 -1
  44. package/package.json +39 -24
  45. package/proxy.yaml +7 -79
  46. package/scripts/k3s-node-setup.sh +2 -2
  47. package/scripts/nat-iptables.sh +103 -18
  48. package/scripts/rhel-grpc-setup.sh +56 -0
  49. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +58 -14
  50. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +23 -14
  51. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  52. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +148 -20
  53. package/src/api/core/core.controller.js +10 -10
  54. package/src/api/core/core.service.js +10 -10
  55. package/src/api/crypto/crypto.controller.js +8 -8
  56. package/src/api/crypto/crypto.service.js +8 -8
  57. package/src/api/cyberia-action/cyberia-action.controller.js +74 -0
  58. package/src/api/cyberia-action/cyberia-action.model.js +87 -0
  59. package/src/api/cyberia-action/cyberia-action.router.js +27 -0
  60. package/src/api/cyberia-action/cyberia-action.service.js +42 -0
  61. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +93 -0
  62. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
  63. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +29 -0
  64. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +51 -0
  65. package/src/api/cyberia-entity/cyberia-entity.controller.js +74 -0
  66. package/src/api/cyberia-entity/cyberia-entity.model.js +24 -0
  67. package/src/api/cyberia-entity/cyberia-entity.router.js +27 -0
  68. package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
  69. package/src/api/cyberia-instance/cyberia-fallback-world.js +178 -0
  70. package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
  71. package/src/api/cyberia-instance/cyberia-instance.model.js +87 -0
  72. package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
  73. package/src/api/cyberia-instance/cyberia-instance.service.js +156 -0
  74. package/src/api/cyberia-instance/cyberia-portal-connector.js +260 -0
  75. package/src/api/cyberia-instance/cyberia-world-generator.js +505 -0
  76. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +74 -0
  77. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +574 -0
  78. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +231 -0
  79. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +27 -0
  80. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +46 -0
  81. package/src/api/cyberia-map/cyberia-map.controller.js +79 -0
  82. package/src/api/cyberia-map/cyberia-map.model.js +30 -0
  83. package/src/api/cyberia-map/cyberia-map.router.js +40 -0
  84. package/src/api/cyberia-map/cyberia-map.service.js +74 -0
  85. package/src/api/cyberia-quest/cyberia-quest.controller.js +74 -0
  86. package/src/api/cyberia-quest/cyberia-quest.model.js +67 -0
  87. package/src/api/cyberia-quest/cyberia-quest.router.js +27 -0
  88. package/src/api/cyberia-quest/cyberia-quest.service.js +42 -0
  89. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +74 -0
  90. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +49 -0
  91. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +27 -0
  92. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +42 -0
  93. package/src/api/default/default.controller.js +10 -10
  94. package/src/api/default/default.service.js +10 -10
  95. package/src/api/document/document.controller.js +12 -12
  96. package/src/api/document/document.model.js +10 -16
  97. package/src/api/file/file.controller.js +8 -8
  98. package/src/api/file/file.model.js +10 -10
  99. package/src/api/file/file.ref.json +18 -0
  100. package/src/api/file/file.service.js +36 -36
  101. package/src/api/instance/instance.controller.js +10 -10
  102. package/src/api/instance/instance.model.js +4 -10
  103. package/src/api/instance/instance.service.js +10 -10
  104. package/src/api/ipfs/ipfs.controller.js +15 -36
  105. package/src/api/ipfs/ipfs.model.js +47 -47
  106. package/src/api/ipfs/ipfs.router.js +8 -13
  107. package/src/api/ipfs/ipfs.service.js +67 -129
  108. package/src/api/object-layer/object-layer.controller.js +12 -12
  109. package/src/api/object-layer/object-layer.model.js +4 -17
  110. package/src/api/object-layer/object-layer.router.js +30 -0
  111. package/src/api/object-layer/object-layer.service.js +126 -43
  112. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +10 -10
  113. package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +6 -16
  114. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +18 -14
  115. package/src/api/test/test.controller.js +8 -8
  116. package/src/api/test/test.service.js +8 -8
  117. package/src/api/user/guest.service.js +99 -0
  118. package/src/api/user/user.controller.js +6 -6
  119. package/src/api/user/user.model.js +8 -13
  120. package/src/api/user/user.service.js +11 -27
  121. package/src/cli/cluster.js +68 -21
  122. package/src/cli/db.js +753 -825
  123. package/src/cli/deploy.js +215 -125
  124. package/src/cli/env.js +29 -0
  125. package/src/cli/fs.js +82 -8
  126. package/src/cli/image.js +43 -1
  127. package/src/cli/index.js +74 -3
  128. package/src/cli/kubectl.js +211 -0
  129. package/src/cli/release.js +340 -0
  130. package/src/cli/repository.js +475 -74
  131. package/src/cli/run.js +582 -43
  132. package/src/cli/secrets.js +73 -0
  133. package/src/cli/ssh.js +1 -1
  134. package/src/cli/static.js +43 -115
  135. package/src/cli/test.js +3 -3
  136. package/src/client/Cryptokoyn.index.js +18 -22
  137. package/src/client/CyberiaPortal.index.js +19 -24
  138. package/src/client/Default.index.js +21 -34
  139. package/src/client/Itemledger.index.js +20 -27
  140. package/src/client/Underpost.index.js +19 -24
  141. package/src/client/components/core/404.js +4 -4
  142. package/src/client/components/core/500.js +4 -4
  143. package/src/client/components/core/Account.js +73 -60
  144. package/src/client/components/core/AgGrid.js +23 -33
  145. package/src/client/components/core/Alert.js +12 -13
  146. package/src/client/components/core/AppStore.js +69 -0
  147. package/src/client/components/core/Auth.js +35 -37
  148. package/src/client/components/core/Badge.js +7 -13
  149. package/src/client/components/core/BtnIcon.js +15 -17
  150. package/src/client/components/core/CalendarCore.js +43 -64
  151. package/src/client/components/core/Chat.js +13 -15
  152. package/src/client/components/core/ClientEvents.js +87 -0
  153. package/src/client/components/core/ColorPaletteElement.js +309 -0
  154. package/src/client/components/core/Content.js +17 -14
  155. package/src/client/components/core/Css.js +15 -71
  156. package/src/client/components/core/CssCore.js +12 -16
  157. package/src/client/components/core/D3Chart.js +4 -4
  158. package/src/client/components/core/Docs.js +64 -91
  159. package/src/client/components/core/DropDown.js +194 -96
  160. package/src/client/components/core/EventBus.js +92 -0
  161. package/src/client/components/core/EventsUI.js +14 -17
  162. package/src/client/components/core/FileExplorer.js +96 -228
  163. package/src/client/components/core/FullScreen.js +47 -75
  164. package/src/client/components/core/Input.js +24 -69
  165. package/src/client/components/core/Keyboard.js +26 -19
  166. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  167. package/src/client/components/core/LoadingAnimation.js +25 -31
  168. package/src/client/components/core/LogIn.js +43 -43
  169. package/src/client/components/core/LogOut.js +25 -16
  170. package/src/client/components/core/Modal.js +462 -179
  171. package/src/client/components/core/NotificationManager.js +14 -18
  172. package/src/client/components/core/Panel.js +54 -51
  173. package/src/client/components/core/PanelForm.js +44 -144
  174. package/src/client/components/core/Polyhedron.js +110 -214
  175. package/src/client/components/core/PublicProfile.js +39 -32
  176. package/src/client/components/core/Recover.js +48 -44
  177. package/src/client/components/core/Responsive.js +88 -32
  178. package/src/client/components/core/RichText.js +9 -18
  179. package/src/client/components/core/Router.js +24 -3
  180. package/src/client/components/core/SearchBox.js +37 -37
  181. package/src/client/components/core/SignUp.js +39 -30
  182. package/src/client/components/core/SocketIo.js +112 -30
  183. package/src/client/components/core/SocketIoHandler.js +75 -0
  184. package/src/client/components/core/Stream.js +143 -95
  185. package/src/client/components/core/ToggleSwitch.js +8 -20
  186. package/src/client/components/core/ToolTip.js +5 -17
  187. package/src/client/components/core/Translate.js +56 -59
  188. package/src/client/components/core/Validator.js +26 -16
  189. package/src/client/components/core/Wallet.js +15 -26
  190. package/src/client/components/core/Webhook.js +40 -7
  191. package/src/client/components/core/Worker.js +163 -27
  192. package/src/client/components/core/windowGetDimensions.js +7 -7
  193. package/src/client/components/cryptokoyn/{MenuCryptokoyn.js → AppShellCryptokoyn.js} +59 -59
  194. package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
  195. package/src/client/components/cryptokoyn/CssCryptokoyn.js +15 -15
  196. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +9 -7
  197. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +8 -6
  198. package/src/client/components/cryptokoyn/RouterCryptokoyn.js +37 -0
  199. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +4 -4
  200. package/src/client/components/cryptokoyn/SignUpCryptokoyn.js +6 -4
  201. package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
  202. package/src/client/components/cyberia/InstanceEngineCyberia.js +781 -0
  203. package/src/client/components/cyberia/MapEngineCyberia.js +1836 -2
  204. package/src/client/components/cyberia/ObjectLayerEngine.js +19 -0
  205. package/src/client/components/cyberia/ObjectLayerEngineModal.js +1220 -99
  206. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +252 -316
  207. package/src/client/components/cyberia-portal/{MenuCyberiaPortal.js → AppShellCyberiaPortal.js} +136 -103
  208. package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
  209. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +462 -32
  210. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +15 -15
  211. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +9 -7
  212. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +8 -6
  213. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +4 -4
  214. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +60 -0
  215. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +4 -4
  216. package/src/client/components/cyberia-portal/SignUpCyberiaPortal.js +6 -4
  217. package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
  218. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -4
  219. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +91 -91
  220. package/src/client/components/default/AppStoreDefault.js +5 -0
  221. package/src/client/components/default/CssDefault.js +12 -12
  222. package/src/client/components/default/LogInDefault.js +9 -7
  223. package/src/client/components/default/LogOutDefault.js +8 -6
  224. package/src/client/components/default/RouterDefault.js +47 -0
  225. package/src/client/components/default/SettingsDefault.js +4 -4
  226. package/src/client/components/default/SignUpDefault.js +6 -4
  227. package/src/client/components/default/SocketIoDefault.js +3 -51
  228. package/src/client/components/default/TranslateDefault.js +3 -3
  229. package/src/client/components/itemledger/{MenuItemledger.js → AppShellItemledger.js} +59 -59
  230. package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
  231. package/src/client/components/itemledger/CssItemledger.js +15 -15
  232. package/src/client/components/itemledger/LogInItemledger.js +9 -7
  233. package/src/client/components/itemledger/LogOutItemledger.js +8 -6
  234. package/src/client/components/itemledger/RouterItemledger.js +38 -0
  235. package/src/client/components/itemledger/SettingsItemledger.js +4 -4
  236. package/src/client/components/itemledger/SignUpItemledger.js +6 -4
  237. package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
  238. package/src/client/components/itemledger/TranslateItemledger.js +3 -3
  239. package/src/client/components/underpost/{MenuUnderpost.js → AppShellUnderpost.js} +92 -92
  240. package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
  241. package/src/client/components/underpost/CssUnderpost.js +14 -14
  242. package/src/client/components/underpost/CyberpunkBloggerUnderpost.js +4 -4
  243. package/src/client/components/underpost/DocumentSearchProvider.js +1 -1
  244. package/src/client/components/underpost/LabGalleryUnderpost.js +12 -15
  245. package/src/client/components/underpost/LogInUnderpost.js +9 -7
  246. package/src/client/components/underpost/LogOutUnderpost.js +8 -6
  247. package/src/client/components/underpost/RouterUnderpost.js +45 -0
  248. package/src/client/components/underpost/SettingsUnderpost.js +4 -4
  249. package/src/client/components/underpost/SignUpUnderpost.js +6 -4
  250. package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
  251. package/src/client/components/underpost/TranslateUnderpost.js +4 -4
  252. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +235 -0
  253. package/src/client/public/cyberia-docs/ARCHITECTURE.md +443 -0
  254. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +417 -0
  255. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +313 -0
  256. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +260 -0
  257. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +241 -0
  258. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +300 -0
  259. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +279 -0
  260. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +206 -0
  261. package/src/client/public/cyberia-docs/ROADMAP.md +240 -0
  262. package/src/client/public/cyberia-docs/WHITE-PAPER.md +732 -0
  263. package/src/client/services/atlas-sprite-sheet/atlas-sprite-sheet.service.js +14 -20
  264. package/src/client/services/core/core.service.js +35 -55
  265. package/src/client/services/crypto/crypto.service.js +8 -13
  266. package/src/client/services/cyberia-action/cyberia-action.service.js +99 -0
  267. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +99 -0
  268. package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
  269. package/src/client/services/cyberia-entity/cyberia-entity.service.js +99 -0
  270. package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
  271. package/src/client/services/cyberia-instance/cyberia-instance.service.js +116 -0
  272. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +99 -0
  273. package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
  274. package/src/client/services/cyberia-map/cyberia-map.service.js +120 -0
  275. package/src/client/services/cyberia-quest/cyberia-quest.service.js +99 -0
  276. package/src/client/services/cyberia-quest-progress/cyberia-quest-progress.service.js +99 -0
  277. package/src/client/services/default/default.management.js +159 -267
  278. package/src/client/services/default/default.service.js +10 -16
  279. package/src/client/services/document/document.service.js +14 -19
  280. package/src/client/services/file/file.service.js +8 -13
  281. package/src/client/services/instance/instance.management.js +6 -6
  282. package/src/client/services/instance/instance.service.js +10 -15
  283. package/src/client/services/ipfs/ipfs.service.js +14 -40
  284. package/src/client/services/object-layer/object-layer.management.js +14 -14
  285. package/src/client/services/object-layer/object-layer.service.js +39 -24
  286. package/src/client/services/object-layer-render-frames/object-layer-render-frames.service.js +10 -16
  287. package/src/client/services/test/test.service.js +8 -13
  288. package/src/client/services/user/guest.service.js +86 -0
  289. package/src/client/services/user/user.management.js +6 -6
  290. package/src/client/services/user/user.service.js +14 -20
  291. package/src/client/ssr/body/404.js +3 -3
  292. package/src/client/ssr/body/500.js +3 -3
  293. package/src/client/ssr/body/CacheControl.js +5 -2
  294. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  295. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +13 -6
  296. package/src/client/ssr/head/PwaItemledger.js +197 -60
  297. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  298. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  299. package/src/client/ssr/offline/Maintenance.js +12 -11
  300. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  301. package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
  302. package/src/client/ssr/pages/Test.js +2 -2
  303. package/src/client/sw/core.sw.js +212 -0
  304. package/src/grpc/cyberia/grpc-server.js +642 -0
  305. package/src/index.js +24 -1
  306. package/src/runtime/cyberia-client/Dockerfile +80 -0
  307. package/src/runtime/cyberia-server/Dockerfile +37 -0
  308. package/src/runtime/express/Dockerfile +5 -1
  309. package/src/runtime/express/Express.js +18 -1
  310. package/src/runtime/lampp/Dockerfile +17 -5
  311. package/src/runtime/lampp/Lampp.js +27 -4
  312. package/src/runtime/wp/Dockerfile +62 -0
  313. package/src/runtime/wp/Wp.js +639 -0
  314. package/src/server/atlas-sprite-sheet-generator.js +4 -2
  315. package/src/server/auth.js +24 -1
  316. package/src/server/backup.js +37 -9
  317. package/src/server/client-build-docs.js +52 -46
  318. package/src/server/client-build.js +356 -82
  319. package/src/server/client-formatted.js +140 -57
  320. package/src/server/conf.js +29 -13
  321. package/src/server/cron.js +25 -23
  322. package/src/server/data-query.js +32 -20
  323. package/src/server/dns.js +24 -1
  324. package/src/server/ipfs-client.js +253 -89
  325. package/src/server/object-layer.js +150 -114
  326. package/src/server/peer.js +8 -0
  327. package/src/server/process.js +13 -27
  328. package/src/server/runtime.js +25 -1
  329. package/src/server/semantic-layer-generator-floor.js +319 -0
  330. package/src/server/semantic-layer-generator-resource.js +259 -0
  331. package/src/server/semantic-layer-generator-skin.js +1164 -0
  332. package/src/server/semantic-layer-generator.js +211 -542
  333. package/src/server/shape-generator.js +108 -0
  334. package/src/server/start.js +19 -5
  335. package/src/server/valkey.js +141 -235
  336. package/src/ws/IoInterface.js +1 -10
  337. package/src/ws/IoServer.js +14 -33
  338. package/src/ws/core/channels/core.ws.chat.js +65 -20
  339. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  340. package/src/ws/core/channels/core.ws.stream.js +90 -31
  341. package/src/ws/core/core.ws.connection.js +12 -33
  342. package/src/ws/core/core.ws.emit.js +10 -26
  343. package/src/ws/core/core.ws.server.js +25 -58
  344. package/src/ws/default/channels/default.ws.main.js +53 -12
  345. package/src/ws/default/default.ws.connection.js +26 -13
  346. package/src/ws/default/default.ws.server.js +30 -12
  347. package/tsconfig.docs.json +15 -0
  348. package/typedoc.dd-cyberia.json +29 -0
  349. package/typedoc.json +29 -0
  350. package/WHITE-PAPER.md +0 -1540
  351. package/hardhat/README.md +0 -531
  352. package/hardhat/WHITE-PAPER.md +0 -1540
  353. package/jsdoc.dd-cyberia.json +0 -59
  354. package/jsdoc.json +0 -59
  355. package/src/api/object-layer/README.md +0 -347
  356. package/src/client/components/core/ColorPalette.js +0 -5267
  357. package/src/client/components/core/JoyStick.js +0 -80
  358. package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
  359. package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
  360. package/src/client/components/cryptokoyn/RoutesCryptokoyn.js +0 -39
  361. package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
  362. package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +0 -58
  363. package/src/client/components/cyberia-portal/ServerCyberiaPortal.js +0 -136
  364. package/src/client/components/default/ElementsDefault.js +0 -38
  365. package/src/client/components/default/RoutesDefault.js +0 -49
  366. package/src/client/components/itemledger/CommonItemledger.js +0 -29
  367. package/src/client/components/itemledger/ElementsItemledger.js +0 -38
  368. package/src/client/components/itemledger/RoutesItemledger.js +0 -40
  369. package/src/client/components/underpost/CommonUnderpost.js +0 -29
  370. package/src/client/components/underpost/ElementsUnderpost.js +0 -38
  371. package/src/client/components/underpost/RoutesUnderpost.js +0 -47
  372. package/src/client/sw/default.sw.js +0 -127
  373. package/src/client/sw/template.sw.js +0 -84
  374. package/src/ws/core/management/core.ws.chat.js +0 -8
  375. package/src/ws/core/management/core.ws.mailer.js +0 -16
  376. package/src/ws/core/management/core.ws.stream.js +0 -8
  377. package/src/ws/default/management/default.ws.main.js +0 -8
@@ -0,0 +1,1164 @@
1
+ /**
2
+ * Skin semantic descriptor and template-based procedural generator.
3
+ *
4
+ * Instead of the shape/noise-field approach used by floor tiles, character
5
+ * skins are produced by painting pixel zones read from the canonical
6
+ * artist-authored templates in:
7
+ *
8
+ * src/client/public/cyberia/assets/templates/
9
+ *
10
+ * Templates define which pixels belong to each body part (hair, face/hands,
11
+ * shirt/breastplate, pants, shoes). A procedural per-seed color palette is
12
+ * applied so every generated skin has a distinct look while remaining
13
+ * technically valid for all four cardinal directions.
14
+ *
15
+ * Zone painting order (later zones override earlier ones):
16
+ * 1. skin – full body silhouette in skin-tone color
17
+ * 2. shirt – breastplate/shirt overpaints the torso
18
+ * 3. pants – legs overpaints the legs area
19
+ * 4. shoes – bottom rows of legs in shoe color
20
+ * 5. hair – hair overpaints the head crown
21
+ *
22
+ * UP direction derives from DOWN but colours the head area with hair instead
23
+ * of skin (the back of the head is visible).
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.
26
+ *
27
+ * @module src/server/semantic-layer-generator-skin.js
28
+ * @namespace SemanticLayerGeneratorSkin
29
+ */
30
+ import { readFileSync } from 'fs';
31
+ import path from 'path';
32
+ import { fileURLToPath } from 'url';
33
+ import crypto from 'crypto';
34
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
35
+ const TEMPLATES_DIR = path.resolve(__dirname, '../client/public/cyberia/assets/templates');
36
+ /* ─── Grid dimension (must match GRID_DIM in main generator) ────────────── */
37
+ const SKIN_GRID_DIM = 24;
38
+ /* ─── Y threshold: pixels at or above this row are considered "head" ─────── */
39
+ const HEAD_Y_MAX = 12;
40
+ /* ─── Y threshold: pixels at or above (>=) this row are "shoe" zone ──────── */
41
+ const SHOE_Y_MIN = 23;
42
+ /* ═══════════════════════════════════════════════════════════════════════════
43
+ * TEMPLATE LOADING
44
+ * Templates are loaded once at module initialisation (synchronous I/O).
45
+ * ═══════════════════════════════════════════════════════════════════════════ */
46
+ /**
47
+ * Loads a JSON template file and returns the parsed value.
48
+ * Returns [] on any error so the generator degrades gracefully.
49
+ * @param {string} filename
50
+ * @returns {Array}
51
+ */
52
+ function loadTemplate(filename) {
53
+ try {
54
+ return JSON.parse(readFileSync(path.join(TEMPLATES_DIR, filename), 'utf8'));
55
+ } catch {
56
+ return [];
57
+ }
58
+ }
59
+ /**
60
+ * Reads a full skin template (item-skin-08.json / item-skin-06.json) and
61
+ * extracts the black (#000000) outline pixels from its `color` grid.
62
+ * Pixels outside the 24 × 24 play area (x or y >= SKIN_GRID_DIM) are ignored.
63
+ * @param {string} filename Template filename (full template, not style overlay).
64
+ * @returns {number[][]} Array of [x, y] border pixel coordinates.
65
+ */
66
+ function loadBorderFromTemplate(filename) {
67
+ try {
68
+ const template = JSON.parse(readFileSync(path.join(TEMPLATES_DIR, filename), 'utf8'));
69
+ const colorGrid = template.color;
70
+ if (!Array.isArray(colorGrid)) return [];
71
+ const border = [];
72
+ for (let y = 0; y < colorGrid.length; y++) {
73
+ const row = colorGrid[y];
74
+ if (!Array.isArray(row)) continue;
75
+ for (let x = 0; x < row.length; x++) {
76
+ if (row[x] === '#000000' && x < SKIN_GRID_DIM && y < SKIN_GRID_DIM) {
77
+ border.push([x, y]);
78
+ }
79
+ }
80
+ }
81
+ return border;
82
+ } catch {
83
+ return [];
84
+ }
85
+ }
86
+ /**
87
+ * Raw template pixel lists keyed by direction and zone name.
88
+ * Coordinates are [x, y] pairs where (0,0) is the top-left cell of a
89
+ * SKIN_GRID_DIM × SKIN_GRID_DIM (24 × 24) grid.
90
+ *
91
+ * @type {{ down: Object, left: Object }}
92
+ */
93
+ class RAW {
94
+ static down = {
95
+ /** Full body silhouette – face, torso, arms, legs */
96
+ skin: loadTemplate('item-skin-style-skin-08.json'),
97
+ /** Shirt / breastplate zone */
98
+ shirt: loadTemplate('item-skin-style-breastplate-08.json'),
99
+ /** Legs / pants zone (includes shoe row) */
100
+ legs: loadTemplate('item-skin-style-legs-08.json'),
101
+ /** Hair – direction-agnostic crown */
102
+ hair: loadTemplate('item-skin-style-hair.json'),
103
+ /** Black outline border from full template color grid */
104
+ border: loadBorderFromTemplate('item-skin-08.json'),
105
+ };
106
+ static left = {
107
+ skin: loadTemplate('item-skin-style-skin-06.json'),
108
+ shirt: loadTemplate('item-skin-style-breastplate-06.json'),
109
+ legs: loadTemplate('item-skin-style-legs-06.json'),
110
+ hair: loadTemplate('item-skin-style-hair.json'),
111
+ border: loadBorderFromTemplate('item-skin-06.json'),
112
+ };
113
+ }
114
+ /**
115
+ * Splits the legs template into pants (y < SHOE_Y_MIN) and shoes (y >= SHOE_Y_MIN).
116
+ * @param {number[][]} legsCoords
117
+ * @returns {{ pants: number[][], shoes: number[][] }}
118
+ */
119
+ function splitLegs(legsCoords) {
120
+ const pants = [];
121
+ const shoes = [];
122
+ for (const [x, y] of legsCoords) {
123
+ if (y >= SHOE_Y_MIN) shoes.push([x, y]);
124
+ else pants.push([x, y]);
125
+ }
126
+ return { pants, shoes };
127
+ }
128
+ /**
129
+ * Mirrors a pixel list horizontally so a left-facing sprite becomes right-facing.
130
+ * Uses the reference width 26 (template grid): x_new = 25 − x.
131
+ * All resulting coordinates remain within 0..23.
132
+ * @param {number[][]} coords
133
+ * @returns {number[][]}
134
+ */
135
+ function mirrorH(coords) {
136
+ return coords
137
+ .map(([x, y]) => [25 - x, y])
138
+ .filter(([x, y]) => x >= 0 && x < SKIN_GRID_DIM && y >= 0 && y < SKIN_GRID_DIM);
139
+ }
140
+ /**
141
+ * Per-direction pixel zone definitions.
142
+ * Shoes are separated from pants; hair and skin are provided per direction.
143
+ *
144
+ * @typedef {{ skin: number[][], shirt: number[][], pants: number[][], shoes: number[][], hair: number[][] }} DirectionZones
145
+ * @type {{ down: DirectionZones, left: DirectionZones, right: DirectionZones, up: Object }}
146
+ */
147
+ const ZONES = (() => {
148
+ const down = (() => {
149
+ const { pants, shoes } = splitLegs(RAW.down.legs);
150
+ return { skin: RAW.down.skin, shirt: RAW.down.shirt, pants, shoes, hair: RAW.down.hair, border: RAW.down.border };
151
+ })();
152
+ const right = (() => {
153
+ const { pants, shoes } = splitLegs(RAW.left.legs);
154
+ return { skin: RAW.left.skin, shirt: RAW.left.shirt, pants, shoes, hair: RAW.left.hair, border: RAW.left.border };
155
+ })();
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),
163
+ };
164
+ // UP: same body layout as DOWN but head pixels (y <= HEAD_Y_MAX) are painted
165
+ // with hair color (back of character's head), body/arms remain skin colored.
166
+ // Reuse down zones; we flag this dynamically in buildDirectionMatrix.
167
+ const up = { ...down, isUpDirection: true };
168
+ return { down, left, right, up };
169
+ })();
170
+ /* ═══════════════════════════════════════════════════════════════════════════
171
+ * COLOUR UTILITIES
172
+ * ═══════════════════════════════════════════════════════════════════════════ */
173
+ /**
174
+ * Mini LCG-based RNG (32-bit seed). Avoids importing from the parent module.
175
+ * @param {number} seed
176
+ * @returns {function(): number} RNG returning floats in [0, 1).
177
+ */
178
+ function lcgRng(seed) {
179
+ let s = seed >>> 0;
180
+ return () => {
181
+ s = (Math.imul(1664525, s) + 1013904223) >>> 0;
182
+ return s / 4294967296;
183
+ };
184
+ }
185
+ /**
186
+ * Simple string hash → 32-bit unsigned integer.
187
+ * @param {string} str
188
+ * @returns {number}
189
+ */
190
+ function hashStr(str) {
191
+ let h = 0;
192
+ for (let i = 0; i < str.length; i++) h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
193
+ return h >>> 0;
194
+ }
195
+ /**
196
+ * HSL colour to [r, g, b] (each 0–255).
197
+ * @param {number} h Hue in [0, 1).
198
+ * @param {number} s Saturation in [0, 1].
199
+ * @param {number} l Lightness in [0, 1].
200
+ * @returns {number[]}
201
+ */
202
+ function hslToRgb(h, s, l) {
203
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
204
+ const p = 2 * l - q;
205
+ const hue2rgb = (x) => {
206
+ const t = ((x % 1) + 1) % 1;
207
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
208
+ if (t < 1 / 2) return q;
209
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
210
+ return p;
211
+ };
212
+ return [Math.round(hue2rgb(h + 1 / 3) * 255), Math.round(hue2rgb(h) * 255), Math.round(hue2rgb(h - 1 / 3) * 255)];
213
+ }
214
+ /** Clamp v to [lo, hi]. */
215
+ function clamp(v, lo, hi) {
216
+ return v < lo ? lo : v > hi ? hi : v;
217
+ }
218
+ /* ═══════════════════════════════════════════════════════════════════════════
219
+ * PALETTE DERIVATION
220
+ * All colours are deterministic from (seed, itemId).
221
+ * ═══════════════════════════════════════════════════════════════════════════ */
222
+ /**
223
+ * @typedef {Object} SkinPalette
224
+ * @property {number[]} skinColor RGBA – face / hands / neck
225
+ * @property {number[]} hairColor RGBA – hair
226
+ * @property {number[]} shirtColor RGBA – shirt / breastplate
227
+ * @property {number[]} pantsColor RGBA – pants / legs
228
+ * @property {number[]} shoeColor RGBA – shoes / boots
229
+ * @property {number} hairDepth Max Y row (inclusive) covered by hair; controls hair length.
230
+ */
231
+ /**
232
+ * Derives a fully deterministic 5-colour palette for a skin variant.
233
+ *
234
+ * @param {string} seed
235
+ * @param {string} itemId
236
+ * @param {'random'|'dark'|'light'|'vivid'|'natural'|'shaved'} [subtype='random']
237
+ * @returns {SkinPalette}
238
+ */
239
+ function deriveSkinPalette(seed, itemId, subtype = 'random') {
240
+ const rng = lcgRng(hashStr(`${seed}:${itemId}:skin-palette`));
241
+ /* ── Skin tones: dark brown → light peach ───────────────────────────── */
242
+ const allSkinTones = [
243
+ [38, 22, 14], // near-black
244
+ [72, 44, 28], // very dark
245
+ [110, 68, 44], // dark
246
+ [152, 100, 66], // medium
247
+ [186, 134, 94], // warm mid
248
+ [214, 170, 130], // light
249
+ [232, 196, 164], // very light
250
+ [248, 220, 196], // pale
251
+ ];
252
+ // Constrain tone pool by subtype
253
+ const skinPool =
254
+ subtype === 'dark' ? allSkinTones.slice(0, 3) : subtype === 'light' ? allSkinTones.slice(5) : allSkinTones;
255
+ const skinRgb = skinPool[Math.floor(rng() * skinPool.length)];
256
+ /* ── Hair: natural shades + vivid options ───────────────────────────── */
257
+ const naturalHair = [
258
+ [18, 12, 8], // near-black
259
+ [55, 32, 14], // very dark brown
260
+ [95, 56, 22], // dark brown
261
+ [158, 108, 42], // medium brown
262
+ [194, 156, 60], // blond
263
+ [210, 90, 36], // auburn / ginger
264
+ [36, 36, 36], // dark grey
265
+ [140, 140, 140], // grey
266
+ [230, 230, 230], // white
267
+ ];
268
+ const vividHair = [
269
+ [168, 22, 22], // vivid red
270
+ [22, 72, 210], // vivid blue
271
+ [20, 180, 76], // vivid green
272
+ [160, 22, 178], // vivid purple
273
+ [210, 168, 22], // golden
274
+ [22, 200, 200], // vivid cyan
275
+ [220, 60, 160], // hot pink
276
+ ];
277
+ const allHairPresets = [...naturalHair, ...vividHair];
278
+ const hairPool = subtype === 'vivid' ? vividHair : subtype === 'natural' ? naturalHair : allHairPresets;
279
+ const hairRgb = hairPool[Math.floor(rng() * hairPool.length)];
280
+ /* ── Clothing: random hue, reasonable saturation/lightness ─────────── */
281
+ const shirtRgb = hslToRgb(rng(), 0.6 + rng() * 0.35, 0.32 + rng() * 0.28);
282
+ const pantsRgb = hslToRgb(rng(), 0.55 + rng() * 0.4, 0.2 + rng() * 0.3);
283
+ /* Shoes tend to be darker (lower lightness) */
284
+ const shoeH = rng();
285
+ const shoeRgb = hslToRgb(shoeH, 0.35 + rng() * 0.4, 0.12 + rng() * 0.2);
286
+ /* ── Hair depth: controls how far down the hair goes on the head ───── *
287
+ * 0 → shaved (no hair at all — bald silhouette) *
288
+ * 5 – 11 → short crop to long flowing hair */
289
+ const hairDepthOptions = subtype === 'shaved' ? [0] : [5, 6, 7, 8, 9, 10, 11];
290
+ const hairDepth = hairDepthOptions[Math.floor(rng() * hairDepthOptions.length)];
291
+ return {
292
+ skinColor: [...skinRgb, 255],
293
+ hairColor: [...hairRgb, 255],
294
+ shirtColor: [...shirtRgb, 255],
295
+ pantsColor: [...pantsRgb, 255],
296
+ shoeColor: [...shoeRgb, 255],
297
+ hairDepth,
298
+ };
299
+ }
300
+ /* ═══════════════════════════════════════════════════════════════════════════
301
+ * FRAME MATRIX BUILDER
302
+ * ═══════════════════════════════════════════════════════════════════════════ */
303
+ /**
304
+ * Returns the index of `rgba` in `globalColors`, adding it if absent.
305
+ * @param {number[][]} globalColors
306
+ * @param {number[]} rgba
307
+ * @returns {number}
308
+ */
309
+ function getOrAddColor(globalColors, rgba) {
310
+ const idx = globalColors.findIndex(
311
+ (c) => c[0] === rgba[0] && c[1] === rgba[1] && c[2] === rgba[2] && c[3] === rgba[3],
312
+ );
313
+ if (idx >= 0) return idx;
314
+ globalColors.push([...rgba]);
315
+ return globalColors.length - 1;
316
+ }
317
+ /**
318
+ * Paints a list of [x,y] coordinates onto a frame matrix with a color index.
319
+ * Skips coordinates outside SKIN_GRID_DIM.
320
+ * @param {number[][]} matrix
321
+ * @param {number[][]} coords
322
+ * @param {number} colorIdx
323
+ */
324
+ function paint(matrix, coords, colorIdx) {
325
+ for (const [x, y] of coords) {
326
+ if (x >= 0 && x < SKIN_GRID_DIM && y >= 0 && y < SKIN_GRID_DIM) {
327
+ matrix[y][x] = colorIdx;
328
+ }
329
+ }
330
+ }
331
+ /**
332
+ * Builds a 24 × 24 frame matrix for DOWN, LEFT, or RIGHT directions.
333
+ *
334
+ * Hair is derived from interior skin pixels at y <= palette.hairDepth.
335
+ * hairDepth === 0 means shaved head — all hair steps are skipped.
336
+ *
337
+ * Non-shaved heads get:
338
+ * 2b. Black hairline at y = hairDepth+1 with 2–4 bang wisps punching through.
339
+ * 2c. Side hair extension: narrow strips hang from the crown's left/right
340
+ * edges for hairExtend = hairDepth − 3 rows (same formula as UP's
341
+ * back-extension), ensuring visual hair-length consistency across all 4
342
+ * directions.
343
+ *
344
+ * @param {DirectionZones} zones
345
+ * @param {SkinPalette} palette
346
+ * @param {number[][]} globalColors Mutated in place to accumulate unique colours.
347
+ * @param {string} seed
348
+ * @param {string} itemId
349
+ * @param {string} dirLabel 'down' | 'left' | 'right' — differentiates RNG streams.
350
+ * @returns {number[][]}
351
+ */
352
+ function buildDirectionMatrix(zones, palette, globalColors, seed, itemId, dirLabel) {
353
+ const matrix = Array.from({ length: SKIN_GRID_DIM }, () => Array(SKIN_GRID_DIM).fill(0));
354
+ const skinIdx = getOrAddColor(globalColors, palette.skinColor);
355
+ const hairIdx = getOrAddColor(globalColors, palette.hairColor);
356
+ const shirtIdx = getOrAddColor(globalColors, palette.shirtColor);
357
+ const pantsIdx = getOrAddColor(globalColors, palette.pantsColor);
358
+ const shoeIdx = getOrAddColor(globalColors, palette.shoeColor);
359
+ // Sets for fast per-pixel lookup
360
+ const borderSet = new Set(zones.border.map(([x, y]) => `${x},${y}`));
361
+ const skinSet = new Set(zones.skin.map(([x, y]) => `${x},${y}`));
362
+ // 1. Full body silhouette → skin tone
363
+ paint(matrix, zones.skin, skinIdx);
364
+ // Compute hair pixels and per-row bounding boxes once — shared by steps 2b and 2c.
365
+ // hairDepth === 0 → shaved head, all hair steps are skipped.
366
+ const hairPixels =
367
+ palette.hairDepth > 0 ? zones.skin.filter(([x, y]) => y <= palette.hairDepth && !borderSet.has(`${x},${y}`)) : [];
368
+ const hairRowBounds = new Map();
369
+ for (const [x, y] of hairPixels) {
370
+ const b = hairRowBounds.get(y) || { min: x, max: x };
371
+ b.min = Math.min(b.min, x);
372
+ b.max = Math.max(b.max, x);
373
+ hairRowBounds.set(y, b);
374
+ }
375
+ const hairRows = [...hairRowBounds.keys()].sort((a, b) => a - b);
376
+ if (palette.hairDepth > 0) {
377
+ // 2. Hair fill
378
+ paint(matrix, hairPixels, hairIdx);
379
+ const blackIdx = getOrAddColor(globalColors, [0, 0, 0, 255]);
380
+ // 2b. Black hairline + bang wisps at the fringe (bottom of hair zone).
381
+ if (hairRows.length > 0) {
382
+ const rngFringe = lcgRng(hashStr(`${seed}:${itemId}:fringe-${dirLabel}`));
383
+ const bottomY = hairRows[hairRows.length - 1];
384
+ const { min: fMin, max: fMax } = hairRowBounds.get(bottomY);
385
+ // Build 2–4 random bang-wisp columns
386
+ const fringeCols = [];
387
+ for (let x = fMin; x <= fMax; x++) fringeCols.push(x);
388
+ const numWisps = 2 + Math.floor(rngFringe() * 3);
389
+ const wispCols = new Set();
390
+ for (let i = 0; i < numWisps; i++) {
391
+ const wx = fringeCols[Math.floor(rngFringe() * fringeCols.length)];
392
+ wispCols.add(wx);
393
+ const wLen = 1 + Math.floor(rngFringe() * 2);
394
+ for (let dy = 1; dy <= wLen; dy++) {
395
+ const wy = bottomY + dy;
396
+ if (wy < SKIN_GRID_DIM && wy <= HEAD_Y_MAX && skinSet.has(`${wx},${wy}`) && !borderSet.has(`${wx},${wy}`)) {
397
+ matrix[wy][wx] = hairIdx;
398
+ }
399
+ }
400
+ const tipY = bottomY + wLen + 1;
401
+ if (
402
+ tipY < SKIN_GRID_DIM &&
403
+ tipY <= HEAD_Y_MAX &&
404
+ skinSet.has(`${wx},${tipY}`) &&
405
+ !borderSet.has(`${wx},${tipY}`)
406
+ ) {
407
+ matrix[tipY][wx] = blackIdx;
408
+ }
409
+ }
410
+ // Black hairline for non-wisp fringe columns
411
+ const fringeY = bottomY + 1;
412
+ if (fringeY < SKIN_GRID_DIM) {
413
+ for (let x = fMin; x <= fMax; x++) {
414
+ if (!wispCols.has(x) && skinSet.has(`${x},${fringeY}`) && !borderSet.has(`${x},${fringeY}`)) {
415
+ matrix[fringeY][x] = blackIdx;
416
+ }
417
+ }
418
+ }
419
+ }
420
+ // 2c. SIDE HAIR STRANDS — 1-px strands at each temple, anchored at the
421
+ // outermost border columns of the head at HEAD_Y_MAX (ear level).
422
+ // Fixed anchor means position is independent of hairDepth, giving a
423
+ // consistent result across all seeds and templates.
424
+ //
425
+ // Length: 1–3 rows (hairDepth 5→1, 6→2, 7+→3).
426
+ // Width: 1 px by default; 30 % chance of 2 px per row for subtle
427
+ // seed-based variation. No inward drift.
428
+ {
429
+ const rngWisp = lcgRng(hashStr(`${seed}:${itemId}:outer-wisp-${dirLabel}`));
430
+ const strandRows = palette.hairDepth >= 5 ? Math.min(palette.hairDepth - 4, 3) : 0;
431
+ if (strandRows > 0) {
432
+ const borderAtHead = zones.border.filter(([, y]) => y === HEAD_Y_MAX);
433
+ if (borderAtHead.length >= 2) {
434
+ const hbL = Math.min(...borderAtHead.map(([x]) => x));
435
+ const hbR = Math.max(...borderAtHead.map(([x]) => x));
436
+ let lastLx = null,
437
+ lastRx = null;
438
+ for (let row = 0; row < strandRows; row++) {
439
+ const y = HEAD_Y_MAX + 1 + row; // just below ear level
440
+ if (y >= SKIN_GRID_DIM) break;
441
+ const ext = rngWisp() < 0.3 ? 2 : 1;
442
+ const lMin = Math.max(0, hbL - ext);
443
+ const rMax = Math.min(SKIN_GRID_DIM - 1, hbR + ext);
444
+ // Left strand: pixels outside the body to the left of hbL
445
+ for (let x = lMin; x < hbL; x++) matrix[y][x] = hairIdx;
446
+ // Right strand: pixels outside the body to the right of hbR
447
+ for (let x = hbR + 1; x <= rMax; x++) matrix[y][x] = hairIdx;
448
+ if (lMin > 0 && matrix[y][lMin - 1] !== hairIdx) matrix[y][lMin - 1] = blackIdx;
449
+ if (rMax < SKIN_GRID_DIM - 1 && matrix[y][rMax + 1] !== hairIdx) matrix[y][rMax + 1] = blackIdx;
450
+ lastLx = lMin;
451
+ lastRx = rMax;
452
+ }
453
+ const tipY = HEAD_Y_MAX + 1 + strandRows;
454
+ if (tipY < SKIN_GRID_DIM) {
455
+ if (lastLx !== null) matrix[tipY][Math.max(0, lastLx)] = blackIdx;
456
+ if (lastRx !== null) matrix[tipY][Math.min(SKIN_GRID_DIM - 1, lastRx)] = blackIdx;
457
+ }
458
+ }
459
+ }
460
+ }
461
+ // 2d. UPPER SIDE HAIR ARCH — thick arch on each temple anchored at y=10.
462
+ // The arch is widest at its crown (y=10, 4-5 px outward from the border)
463
+ // and tapers toward the ear base, creating a visible sideburn arc.
464
+ // 3 rows max (hairDepth 5→1 row, 6→2 rows, 7+→3 rows).
465
+ // Together with the lower 2c strands the pair forms a broken arc shape.
466
+ {
467
+ const UPPER_ANCHOR_Y = HEAD_Y_MAX - 2; // y=10
468
+ const rngWisp2 = lcgRng(hashStr(`${seed}:${itemId}:outer-wisp2-${dirLabel}`));
469
+ const strandRows2 = palette.hairDepth >= 5 ? Math.min(palette.hairDepth - 4, 3) : 0;
470
+ if (strandRows2 > 0) {
471
+ const borderAtUpper = zones.border.filter(([, y]) => y === UPPER_ANCHOR_Y);
472
+ if (borderAtUpper.length >= 2) {
473
+ const hbL2 = Math.min(...borderAtUpper.map(([x]) => x));
474
+ const hbR2 = Math.max(...borderAtUpper.map(([x]) => x));
475
+ // Arch widths per row: narrow at crown (y=10), widest at ear base (y=12).
476
+ // Follows the head silhouette arc — wider where the head is wider.
477
+ const extBase = [2, 3, 4];
478
+ let lastLx2 = null,
479
+ lastRx2 = null;
480
+ for (let row = 0; row < strandRows2; row++) {
481
+ const y = UPPER_ANCHOR_Y + row; // y=10, y=11, y=12
482
+ if (y >= SKIN_GRID_DIM) break;
483
+ const ext2 = extBase[row] + (rngWisp2() < 0.4 ? 1 : 0); // 40 % +1 bonus
484
+ const innerL2 = hbL2 + 2; // 2 px toward center
485
+ const innerR2 = hbR2 - 2;
486
+ const lMin2 = Math.max(0, innerL2 - ext2);
487
+ const rMax2 = Math.min(SKIN_GRID_DIM - 1, innerR2 + ext2);
488
+ for (let x = lMin2; x < innerL2; x++) matrix[y][x] = hairIdx;
489
+ for (let x = innerR2 + 1; x <= rMax2; x++) matrix[y][x] = hairIdx;
490
+ if (lMin2 > 0 && matrix[y][lMin2 - 1] !== hairIdx) matrix[y][lMin2 - 1] = blackIdx;
491
+ if (rMax2 < SKIN_GRID_DIM - 1 && matrix[y][rMax2 + 1] !== hairIdx) matrix[y][rMax2 + 1] = blackIdx;
492
+ lastLx2 = lMin2;
493
+ lastRx2 = rMax2;
494
+ }
495
+ const tipY2 = UPPER_ANCHOR_Y + strandRows2; // first row after arch
496
+ if (tipY2 < SKIN_GRID_DIM) {
497
+ if (lastLx2 !== null && matrix[tipY2][Math.max(0, lastLx2)] !== hairIdx)
498
+ matrix[tipY2][Math.max(0, lastLx2)] = blackIdx;
499
+ if (lastRx2 !== null && matrix[tipY2][Math.min(SKIN_GRID_DIM - 1, lastRx2)] !== hairIdx)
500
+ matrix[tipY2][Math.min(SKIN_GRID_DIM - 1, lastRx2)] = blackIdx;
501
+ }
502
+ }
503
+ }
504
+ }
505
+ // 2e. HAIR EDGE DISTORTIONS — 5–9 stray hair pixels scattered along the
506
+ // outer border of the hair zone for an organic, hand-drawn look.
507
+ // Each pixel sits one step outside the body silhouette and is closed
508
+ // with a black tip pixel for pixel-art definition.
509
+ {
510
+ const rngDist = lcgRng(hashStr(`${seed}:${itemId}:hair-distort-${dirLabel}`));
511
+ // Collect the outermost border column per hair row.
512
+ const borderInHair = zones.border.filter(([, y]) => y <= palette.hairDepth);
513
+ const bhrMap = new Map();
514
+ for (const [x, y] of borderInHair) {
515
+ const b = bhrMap.get(y) || { min: x, max: x };
516
+ b.min = Math.min(b.min, x);
517
+ b.max = Math.max(b.max, x);
518
+ bhrMap.set(y, b);
519
+ }
520
+ const bhrRows = [...bhrMap.keys()];
521
+ const numDistort = 5 + Math.floor(rngDist() * 5); // 5–9
522
+ for (let i = 0; i < numDistort; i++) {
523
+ const y = bhrRows[Math.floor(rngDist() * bhrRows.length)];
524
+ const bnd = bhrMap.get(y);
525
+ if (!bnd) continue;
526
+ const side = rngDist() < 0.5 ? -1 : 1;
527
+ const px = side < 0 ? bnd.min - 1 : bnd.max + 1;
528
+ if (px >= 0 && px < SKIN_GRID_DIM && matrix[y][px] !== hairIdx) {
529
+ matrix[y][px] = hairIdx;
530
+ const bx = px + side;
531
+ if (bx >= 0 && bx < SKIN_GRID_DIM && matrix[y][bx] !== hairIdx) matrix[y][bx] = blackIdx;
532
+ }
533
+ }
534
+ }
535
+ // 2f. MID-CROWN SIDE HAIR ARC — 7-row arc of hair strands following the
536
+ // head edge, centered at y=7 (crown shoulders). Right strand anchors
537
+ // near x=8 (character right), left mirrors it at x≈15. Width peaks
538
+ // at the center row (3 px outward) and tapers to 1 px at the extremes.
539
+ // Per-row 40 % width bonus + 30 % extra distortion pixel per side.
540
+ {
541
+ const CROWN_Y = 7;
542
+ const crownW = [3, 2, 2, 1]; // base width at |dy| = 0, 1, 2, 3
543
+ const rngCrown = lcgRng(hashStr(`${seed}:${itemId}:crown-side-${dirLabel}`));
544
+ for (let dy = -3; dy <= 3; dy++) {
545
+ const y = CROWN_Y + dy;
546
+ if (y < 0 || y >= SKIN_GRID_DIM) continue;
547
+ const borderAtY = zones.border.filter(([, by]) => by === y);
548
+ if (borderAtY.length < 2) continue;
549
+ const hbL = Math.min(...borderAtY.map(([x]) => x));
550
+ const hbR = Math.max(...borderAtY.map(([x]) => x));
551
+ const ext = crownW[Math.abs(dy)] + (rngCrown() < 0.4 ? 1 : 0);
552
+ // Left strand (character right, viewer left)
553
+ const lMin = Math.max(0, hbL - ext);
554
+ for (let x = lMin; x < hbL; x++) matrix[y][x] = hairIdx;
555
+ if (lMin > 0 && matrix[y][lMin - 1] !== hairIdx) matrix[y][lMin - 1] = blackIdx;
556
+ // Right strand (character left, viewer right)
557
+ const rMax = Math.min(SKIN_GRID_DIM - 1, hbR + ext);
558
+ for (let x = hbR + 1; x <= rMax; x++) matrix[y][x] = hairIdx;
559
+ if (rMax < SKIN_GRID_DIM - 1 && matrix[y][rMax + 1] !== hairIdx) matrix[y][rMax + 1] = blackIdx;
560
+ // Distortion: 30 % chance of one extra pixel per side
561
+ if (rngCrown() < 0.3) {
562
+ const px = lMin - 1;
563
+ if (px >= 0 && matrix[y][px] !== hairIdx) {
564
+ matrix[y][px] = hairIdx;
565
+ if (px > 0 && matrix[y][px - 1] !== hairIdx) matrix[y][px - 1] = blackIdx;
566
+ }
567
+ }
568
+ if (rngCrown() < 0.3) {
569
+ const px = rMax + 1;
570
+ if (px < SKIN_GRID_DIM && matrix[y][px] !== hairIdx) {
571
+ matrix[y][px] = hairIdx;
572
+ if (px < SKIN_GRID_DIM - 1 && matrix[y][px + 1] !== hairIdx) matrix[y][px + 1] = blackIdx;
573
+ }
574
+ }
575
+ }
576
+ }
577
+ }
578
+ // 3. Clothes
579
+ paint(matrix, zones.shirt, shirtIdx);
580
+ paint(matrix, zones.pants, pantsIdx);
581
+ paint(matrix, zones.shoes, shoeIdx);
582
+ // 4. Black border outline — always last
583
+ const borderIdx = getOrAddColor(globalColors, [0, 0, 0, 255]);
584
+ paint(matrix, zones.border, borderIdx);
585
+ // 4b. CROWN BORDER SOFTENING — for non-shaved skins, replace most outer
586
+ // silhouette border pixels within the hair zone with hair colour so the
587
+ // crown blends into the background rather than having a hard black outline.
588
+ // Interior features (eyes, pupils) are never touched — only outer-silhouette
589
+ // pixels (those with at least one empty 8-neighbour) are candidates.
590
+ // ~25 % of candidates are kept black for pixel-art definition.
591
+ if (palette.hairDepth > 0) {
592
+ const rngBorder = lcgRng(hashStr(`${seed}:${itemId}:soften-border-${dirLabel}`));
593
+ for (const [bx, by] of zones.border) {
594
+ if (by > palette.hairDepth) continue;
595
+ let isOuter = false;
596
+ for (let dx = -1; dx <= 1 && !isOuter; dx++) {
597
+ for (let dy = -1; dy <= 1 && !isOuter; dy++) {
598
+ if (dx === 0 && dy === 0) continue;
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
+ )
608
+ isOuter = true;
609
+ }
610
+ }
611
+ if (isOuter && rngBorder() < 0.75) matrix[by][bx] = hairIdx;
612
+ }
613
+ }
614
+ return matrix;
615
+ }
616
+ /**
617
+ * Builds a 24 × 24 frame matrix for the UP (back-facing) direction.
618
+ *
619
+ * The entire head is covered with hair, eliminating face features (eyes/mouth).
620
+ * Only the outer silhouette border is kept black; inner face-feature pixels
621
+ * (pupils, mouth line) are overwritten with hair.
622
+ *
623
+ * Hair flows below the head onto the upper back with a tapering strip.
624
+ * Black borders frame the sides and bottom of the flowing hair.
625
+ * Random wisps extend 1–2 px beyond the body silhouette for dynamism.
626
+ *
627
+ * @param {DirectionZones} zones Reuses ZONES.down pixel data.
628
+ * @param {SkinPalette} palette
629
+ * @param {number[][]} globalColors Mutated in place.
630
+ * @param {string} seed
631
+ * @param {string} itemId
632
+ * @returns {number[][]}
633
+ */
634
+ function buildUpDirectionMatrix(zones, palette, globalColors, seed, itemId) {
635
+ const matrix = Array.from({ length: SKIN_GRID_DIM }, () => Array(SKIN_GRID_DIM).fill(0));
636
+ const skinIdx = getOrAddColor(globalColors, palette.skinColor);
637
+ const hairIdx = getOrAddColor(globalColors, palette.hairColor);
638
+ const shirtIdx = getOrAddColor(globalColors, palette.shirtColor);
639
+ const pantsIdx = getOrAddColor(globalColors, palette.pantsColor);
640
+ const shoeIdx = getOrAddColor(globalColors, palette.shoeColor);
641
+ const blackIdx = getOrAddColor(globalColors, [0, 0, 0, 255]);
642
+ const rng = lcgRng(hashStr(`${seed}:${itemId}:up-hair`));
643
+ // All body pixels (skin + border) used for outer-silhouette detection
644
+ const allBodyCoords = [...zones.skin, ...zones.border];
645
+ const bodySet = new Set(allBodyCoords.map(([x, y]) => `${x},${y}`));
646
+ // 1. Paint full body → skin tone, then clothes, then border
647
+ paint(matrix, zones.skin, skinIdx);
648
+ paint(matrix, zones.shirt, shirtIdx);
649
+ paint(matrix, zones.pants, pantsIdx);
650
+ paint(matrix, zones.shoes, shoeIdx);
651
+ paint(matrix, zones.border, blackIdx);
652
+ // 1b. FACE FEATURE WIPE — the zones come from the DOWN (front-facing) template
653
+ // which embeds eyes, pupils and mouth as black border pixels. In the UP
654
+ // (back-of-head) view those pixels must not appear.
655
+ // Walk every border pixel in the face+neck band (y ≤ FACE_FEATURE_Y_MAX).
656
+ // If a pixel has ALL 8 neighbours inside bodySet it is an *interior* feature
657
+ // (eye outline, pupil, mouth corners) — paint it with skinIdx so it is
658
+ // invisible. Outer-silhouette pixels keep their black value.
659
+ //
660
+ // The mouth is at y=15 in the 26×26 template, which is ABOVE HEAD_Y_MAX=12
661
+ // so the subsequent scanline fill does NOT reach it — this wipe is the only
662
+ // place that erases it.
663
+ const FACE_FEATURE_Y_MAX = 17; // below collar; safe upper bound for face features
664
+ for (const [bx, by] of zones.border) {
665
+ if (by > FACE_FEATURE_Y_MAX) continue;
666
+ let outer = false;
667
+ for (let dx = -1; dx <= 1 && !outer; dx++) {
668
+ for (let dy = -1; dy <= 1 && !outer; dy++) {
669
+ if (dx === 0 && dy === 0) continue;
670
+ const nx = bx + dx,
671
+ ny = by + dy;
672
+ if (nx < 0 || nx >= SKIN_GRID_DIM || ny < 0 || ny >= SKIN_GRID_DIM || !bodySet.has(`${nx},${ny}`)) outer = true;
673
+ }
674
+ }
675
+ if (!outer) matrix[by][bx] = skinIdx;
676
+ }
677
+ // 2. HEAD HAIR — two paths: shaved (hairDepth=0) or normal.
678
+ //
679
+ // SHAVED: scanline-fill the head with *skin tone* (covers inner face-feature
680
+ // border pixels so no eyes/mouth bleed through on the bald back-head).
681
+ // Repaint only the outer silhouette black. No extensions or wisps.
682
+ //
683
+ // NORMAL: scanline-fill with hair colour, keep inner pixels as hair, repaint
684
+ // outer silhouette black, then extend hair strip + add wisps.
685
+ const headBounds = new Map();
686
+ for (const [x, y] of allBodyCoords) {
687
+ if (y > HEAD_Y_MAX) continue;
688
+ const b = headBounds.get(y) || { min: x, max: x };
689
+ b.min = Math.min(b.min, x);
690
+ b.max = Math.max(b.max, x);
691
+ headBounds.set(y, b);
692
+ }
693
+ // Shared helper: repaint outer head silhouette black; inner pixels stay as-is.
694
+ // A border pixel is "outer" if any 8-connected neighbour is outside bodySet.
695
+ const repaintOuterHeadBorder = () => {
696
+ for (const [bx, by] of zones.border) {
697
+ if (by > HEAD_Y_MAX) continue;
698
+ let outer = false;
699
+ for (let dx = -1; dx <= 1 && !outer; dx++) {
700
+ for (let dy = -1; dy <= 1 && !outer; dy++) {
701
+ if (dx === 0 && dy === 0) continue;
702
+ const nx = bx + dx,
703
+ ny = by + dy;
704
+ if (nx < 0 || nx >= SKIN_GRID_DIM || ny < 0 || ny >= SKIN_GRID_DIM || !bodySet.has(`${nx},${ny}`))
705
+ outer = true;
706
+ }
707
+ }
708
+ if (outer) matrix[by][bx] = blackIdx;
709
+ }
710
+ };
711
+ if (palette.hairDepth === 0) {
712
+ // ── SHAVED HEAD ─────────────────────────────────────────────────────────
713
+ // Fill head scanlines with skin tone (eliminates any face-feature artefacts)
714
+ for (const [y, { min, max }] of headBounds) {
715
+ for (let x = min; x <= max; x++) matrix[y][x] = skinIdx;
716
+ }
717
+ repaintOuterHeadBorder();
718
+ // No extended hair, no wisps.
719
+ } else {
720
+ // ── NORMAL HAIR HEAD ────────────────────────────────────────────────────
721
+ // Scanline-fill y <= HEAD_Y_MAX with hair colour
722
+ for (const [y, { min, max }] of headBounds) {
723
+ for (let x = min; x <= max; x++) matrix[y][x] = hairIdx;
724
+ }
725
+ // Repaint outer silhouette black; inner face-feature pixels stay as hair
726
+ repaintOuterHeadBorder();
727
+ // Crown border softening (UP) — same intent as 4b in buildDirectionMatrix.
728
+ // repaintOuterHeadBorder() just set outer head border pixels to black;
729
+ // randomly convert 75 % of them back to hair colour for a softer crown edge.
730
+ {
731
+ const rngBorder = lcgRng(hashStr(`${seed}:${itemId}:soften-border-up`));
732
+ for (const [bx, by] of zones.border) {
733
+ if (by > HEAD_Y_MAX) continue;
734
+ if (matrix[by][bx] !== blackIdx) continue; // already hair / not yet set
735
+ let isOuter = false;
736
+ for (let dx = -1; dx <= 1 && !isOuter; dx++) {
737
+ for (let dy = -1; dy <= 1 && !isOuter; dy++) {
738
+ if (dx === 0 && dy === 0) continue;
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;
743
+ }
744
+ }
745
+ if (isOuter && rngBorder() < 0.75) matrix[by][bx] = hairIdx;
746
+ }
747
+ }
748
+ // 2b side. SIDE HAIR STRANDS (UP) — identical geometry to the other three
749
+ // directions: 1-px strands just below HEAD_Y_MAX, anchored at the
750
+ // head border bounds at that row.
751
+ {
752
+ const headAtMax = headBounds.get(HEAD_Y_MAX);
753
+ if (headAtMax && palette.hairDepth >= 5) {
754
+ const rngWisp = lcgRng(hashStr(`${seed}:${itemId}:outer-wisp-up`));
755
+ const strandRows = Math.min(palette.hairDepth - 4, 3);
756
+ const hbL = headAtMax.min;
757
+ const hbR = headAtMax.max;
758
+ let lastLx = null,
759
+ lastRx = null;
760
+ for (let row = 0; row < strandRows; row++) {
761
+ const y = HEAD_Y_MAX + 1 + row;
762
+ if (y >= SKIN_GRID_DIM) break;
763
+ const ext = rngWisp() < 0.3 ? 2 : 1;
764
+ const lMin = Math.max(0, hbL - ext);
765
+ const rMax = Math.min(SKIN_GRID_DIM - 1, hbR + ext);
766
+ for (let x = lMin; x < hbL; x++) matrix[y][x] = hairIdx;
767
+ for (let x = hbR + 1; x <= rMax; x++) matrix[y][x] = hairIdx;
768
+ if (lMin > 0 && matrix[y][lMin - 1] !== hairIdx) matrix[y][lMin - 1] = blackIdx;
769
+ if (rMax < SKIN_GRID_DIM - 1 && matrix[y][rMax + 1] !== hairIdx) matrix[y][rMax + 1] = blackIdx;
770
+ lastLx = lMin;
771
+ lastRx = rMax;
772
+ }
773
+ const tipY = HEAD_Y_MAX + 1 + strandRows;
774
+ if (tipY < SKIN_GRID_DIM) {
775
+ if (lastLx !== null) matrix[tipY][Math.max(0, lastLx)] = blackIdx;
776
+ if (lastRx !== null) matrix[tipY][Math.min(SKIN_GRID_DIM - 1, lastRx)] = blackIdx;
777
+ }
778
+ }
779
+ }
780
+ // 2c side. UPPER SIDE HAIR ARCH (UP) — mirrors 2d in buildDirectionMatrix;
781
+ // thick arch starting at y=10, widest at the crown.
782
+ {
783
+ const UPPER_ANCHOR_Y = HEAD_Y_MAX - 2; // y=10
784
+ const headAtUpper = headBounds.get(UPPER_ANCHOR_Y);
785
+ if (headAtUpper && palette.hairDepth >= 5) {
786
+ const rngWisp2 = lcgRng(hashStr(`${seed}:${itemId}:outer-wisp2-up`));
787
+ const strandRows2 = Math.min(palette.hairDepth - 4, 3);
788
+ const hbL2 = headAtUpper.min;
789
+ const hbR2 = headAtUpper.max;
790
+ const extBase = [2, 3, 4];
791
+ let lastLx2 = null,
792
+ lastRx2 = null;
793
+ for (let row = 0; row < strandRows2; row++) {
794
+ const y = UPPER_ANCHOR_Y + row; // y=10, y=11, y=12
795
+ if (y >= SKIN_GRID_DIM) break;
796
+ const ext2 = extBase[row] + (rngWisp2() < 0.4 ? 1 : 0);
797
+ const innerL2 = hbL2 + 2; // 2 px toward center
798
+ const innerR2 = hbR2 - 2;
799
+ const lMin2 = Math.max(0, innerL2 - ext2);
800
+ const rMax2 = Math.min(SKIN_GRID_DIM - 1, innerR2 + ext2);
801
+ for (let x = lMin2; x < innerL2; x++) matrix[y][x] = hairIdx;
802
+ for (let x = innerR2 + 1; x <= rMax2; x++) matrix[y][x] = hairIdx;
803
+ if (lMin2 > 0 && matrix[y][lMin2 - 1] !== hairIdx) matrix[y][lMin2 - 1] = blackIdx;
804
+ if (rMax2 < SKIN_GRID_DIM - 1 && matrix[y][rMax2 + 1] !== hairIdx) matrix[y][rMax2 + 1] = blackIdx;
805
+ lastLx2 = lMin2;
806
+ lastRx2 = rMax2;
807
+ }
808
+ const tipY2 = UPPER_ANCHOR_Y + strandRows2;
809
+ if (tipY2 < SKIN_GRID_DIM) {
810
+ if (lastLx2 !== null && matrix[tipY2][Math.max(0, lastLx2)] !== hairIdx)
811
+ matrix[tipY2][Math.max(0, lastLx2)] = blackIdx;
812
+ if (lastRx2 !== null && matrix[tipY2][Math.min(SKIN_GRID_DIM - 1, lastRx2)] !== hairIdx)
813
+ matrix[tipY2][Math.min(SKIN_GRID_DIM - 1, lastRx2)] = blackIdx;
814
+ }
815
+ }
816
+ }
817
+ // 2d. HEAD HAIR EDGE DISTORTIONS (UP) — 5–9 stray pixels along the outer
818
+ // head silhouette for organic texture. Identical approach to 2e in
819
+ // buildDirectionMatrix, using headBounds for column references.
820
+ {
821
+ const rngDist = lcgRng(hashStr(`${seed}:${itemId}:hair-distort-up`));
822
+ const hbKeys = [...headBounds.keys()];
823
+ const numDistort = 5 + Math.floor(rngDist() * 5);
824
+ for (let i = 0; i < numDistort; i++) {
825
+ const y = hbKeys[Math.floor(rngDist() * hbKeys.length)];
826
+ const bnd = headBounds.get(y);
827
+ if (!bnd) continue;
828
+ const side = rngDist() < 0.5 ? -1 : 1;
829
+ const px = side < 0 ? bnd.min - 1 : bnd.max + 1;
830
+ if (px >= 0 && px < SKIN_GRID_DIM && matrix[y][px] !== hairIdx) {
831
+ matrix[y][px] = hairIdx;
832
+ const bx = px + side;
833
+ if (bx >= 0 && bx < SKIN_GRID_DIM && matrix[y][bx] !== hairIdx) matrix[y][bx] = blackIdx;
834
+ }
835
+ }
836
+ }
837
+ // 2e. MID-CROWN SIDE HAIR ARC (UP) — mirrors 2f in buildDirectionMatrix;
838
+ // same 7-row arc centered at y=7, using headBounds for the silhouette
839
+ // edge reference instead of zones.border.
840
+ {
841
+ const CROWN_Y = 7;
842
+ const crownW = [3, 2, 2, 1];
843
+ const rngCrown = lcgRng(hashStr(`${seed}:${itemId}:crown-side-up`));
844
+ for (let dy = -3; dy <= 3; dy++) {
845
+ const y = CROWN_Y + dy;
846
+ if (y < 0 || y >= SKIN_GRID_DIM) continue;
847
+ const bnd = headBounds.get(y);
848
+ if (!bnd) continue;
849
+ const hbL = bnd.min;
850
+ const hbR = bnd.max;
851
+ const ext = crownW[Math.abs(dy)] + (rngCrown() < 0.4 ? 1 : 0);
852
+ const lMin = Math.max(0, hbL - ext);
853
+ for (let x = lMin; x < hbL; x++) matrix[y][x] = hairIdx;
854
+ if (lMin > 0 && matrix[y][lMin - 1] !== hairIdx) matrix[y][lMin - 1] = blackIdx;
855
+ const rMax = Math.min(SKIN_GRID_DIM - 1, hbR + ext);
856
+ for (let x = hbR + 1; x <= rMax; x++) matrix[y][x] = hairIdx;
857
+ if (rMax < SKIN_GRID_DIM - 1 && matrix[y][rMax + 1] !== hairIdx) matrix[y][rMax + 1] = blackIdx;
858
+ if (rngCrown() < 0.3) {
859
+ const px = lMin - 1;
860
+ if (px >= 0 && matrix[y][px] !== hairIdx) {
861
+ matrix[y][px] = hairIdx;
862
+ if (px > 0 && matrix[y][px - 1] !== hairIdx) matrix[y][px - 1] = blackIdx;
863
+ }
864
+ }
865
+ if (rngCrown() < 0.3) {
866
+ const px = rMax + 1;
867
+ if (px < SKIN_GRID_DIM && matrix[y][px] !== hairIdx) {
868
+ matrix[y][px] = hairIdx;
869
+ if (px < SKIN_GRID_DIM - 1 && matrix[y][px + 1] !== hairIdx) matrix[y][px + 1] = blackIdx;
870
+ }
871
+ }
872
+ }
873
+ }
874
+ // 4. EXTENDED HAIR — flows below the head onto the upper back.
875
+ // hairDepth (5–11) maps to hairExtend (2–8 rows below HEAD_Y_MAX).
876
+ const hairExtend = Math.min(palette.hairDepth - 3, 6); // 5→2 … 9→6, capped at 6
877
+ const extMaxY = Math.min(HEAD_Y_MAX + hairExtend, SKIN_GRID_DIM - 2);
878
+ // Compute body-bounds for each extension row (centering reference)
879
+ const extBounds = new Map();
880
+ for (const [x, y] of allBodyCoords) {
881
+ if (y <= HEAD_Y_MAX || y > extMaxY) continue;
882
+ const b = extBounds.get(y) || { min: x, max: x };
883
+ b.min = Math.min(b.min, x);
884
+ b.max = Math.max(b.max, x);
885
+ extBounds.set(y, b);
886
+ }
887
+ const extYs = [...extBounds.keys()].sort((a, b) => a - b);
888
+ // Track actual hair strip bounds per row (for border painting)
889
+ const hairStripBounds = new Map();
890
+ for (const y of extYs) {
891
+ const { min, max } = extBounds.get(y);
892
+ const cx = (min + max) / 2;
893
+ const bodyHalfW = (max - min) / 2;
894
+ const yDist = y - HEAD_Y_MAX;
895
+ // Taper: narrower further from the head
896
+ const taper = Math.max(0.35, 1 - yDist * 0.08);
897
+ const halfW = Math.max(2, Math.round(bodyHalfW * taper));
898
+ // Per-row random outside extension (0–2 px each side = wisps)
899
+ const extL = Math.floor(rng() * 2);
900
+ const extR = Math.floor(rng() * 2);
901
+ const hMin = Math.max(0, Math.floor(cx) - halfW - extL);
902
+ const hMax = Math.min(SKIN_GRID_DIM - 1, Math.ceil(cx) + halfW + extR);
903
+ for (let x = hMin; x <= hMax; x++) matrix[y][x] = hairIdx;
904
+ // Store core bounds (without wisp extension) for border
905
+ hairStripBounds.set(y, {
906
+ min: Math.max(0, Math.floor(cx) - halfW),
907
+ max: Math.min(SKIN_GRID_DIM - 1, Math.ceil(cx) + halfW),
908
+ });
909
+ }
910
+ // 5. BLACK BORDERS on sides and bottom of the hair extension.
911
+ for (const y of extYs) {
912
+ const { min, max } = hairStripBounds.get(y);
913
+ if (min > 0 && matrix[y][min - 1] !== hairIdx) matrix[y][min - 1] = blackIdx;
914
+ if (max < SKIN_GRID_DIM - 1 && matrix[y][max + 1] !== hairIdx) matrix[y][max + 1] = blackIdx;
915
+ }
916
+ if (extYs.length > 0) {
917
+ const bottomY = extYs[extYs.length - 1];
918
+ const { min, max } = hairStripBounds.get(bottomY);
919
+ const nextY = bottomY + 1;
920
+ if (nextY < SKIN_GRID_DIM) {
921
+ for (let x = min - 1; x <= max + 1; x++) {
922
+ if (x >= 0 && x < SKIN_GRID_DIM && matrix[nextY][x] !== hairIdx) matrix[nextY][x] = blackIdx;
923
+ }
924
+ }
925
+ }
926
+ // 6. RANDOM WISPS — 2–4 stray hair pixels at edges for visual dynamism.
927
+ // Each wisp extends 1–2 px beyond the current hair boundary,
928
+ // with a black tip pixel closing it off.
929
+ const allHairRows = [...headBounds.keys(), ...extYs];
930
+ const numWisps = 2 + Math.floor(rng() * 3);
931
+ for (let i = 0; i < numWisps; i++) {
932
+ const wy = allHairRows[Math.floor(rng() * allHairRows.length)];
933
+ const hb = hairStripBounds.get(wy) || headBounds.get(wy);
934
+ if (!hb) continue;
935
+ const side = rng() < 0.5 ? -1 : 1;
936
+ const baseX = side < 0 ? hb.min - 1 : hb.max + 1;
937
+ if (baseX < 0 || baseX >= SKIN_GRID_DIM) continue;
938
+ matrix[wy][baseX] = hairIdx;
939
+ // Optional second wisp pixel
940
+ if (rng() < 0.45) {
941
+ const px2 = baseX + side;
942
+ if (px2 >= 0 && px2 < SKIN_GRID_DIM) matrix[wy][px2] = hairIdx;
943
+ }
944
+ // Black closing tip
945
+ const tipX =
946
+ side < 0
947
+ ? Math.max(0, baseX - (rng() < 0.45 ? 2 : 1))
948
+ : Math.min(SKIN_GRID_DIM - 1, baseX + (rng() < 0.45 ? 2 : 1));
949
+ if (matrix[wy][tipX] !== hairIdx) matrix[wy][tipX] = blackIdx;
950
+ }
951
+ } // end else (normal hair)
952
+ return matrix;
953
+ }
954
+ /* ═══════════════════════════════════════════════════════════════════════════
955
+ * WALK FRAME BUILDER
956
+ * Two-frame walk cycle: frame 0 = idle pose, frame 1 = body shifted up 1px.
957
+ * The vertical bob creates a natural walking bounce effect.
958
+ * ═══════════════════════════════════════════════════════════════════════════ */
959
+ /**
960
+ * Derives a two-frame walk cycle from a pre-built idle matrix.
961
+ * Frame 0: deep copy of idleMatrix.
962
+ * Frame 1: all rows shifted up by 1 pixel — the walk-bounce step.
963
+ * The bottom row becomes fully transparent (index 0).
964
+ *
965
+ * @param {number[][]} idleMatrix Pre-built idle frame (not mutated).
966
+ * @returns {number[][][]} [frame0, frame1]
967
+ */
968
+ function buildWalkFrames(idleMatrix) {
969
+ const FOOT_TOP = SHOE_Y_MIN - 1; // y=22 — bottom of leg zone, just above shoes
970
+ const midX = Math.floor(SKIN_GRID_DIM / 2); // x=12 — column boundary between left/right foot
971
+ // Build one walk frame: copy idle, then raise shoe row up 1 px for the chosen side,
972
+ // leaving y=SHOE_Y_MIN transparent for that foot.
973
+ const makeFrame = (liftLeft) => {
974
+ const frame = idleMatrix.map((row) => [...row]);
975
+ const xFrom = liftLeft ? 0 : midX;
976
+ const xTo = liftLeft ? midX : SKIN_GRID_DIM;
977
+ for (let x = xFrom; x < xTo; x++) {
978
+ frame[FOOT_TOP][x] = idleMatrix[SHOE_Y_MIN][x]; // raise shoe to y=22
979
+ frame[SHOE_Y_MIN][x] = 0; // clear y=23 → transparent gap
980
+ }
981
+ return frame;
982
+ };
983
+ // frame0: right foot raised; frame1: left foot raised → alternating step cycle
984
+ return [makeFrame(false), makeFrame(true)];
985
+ }
986
+ /* ═══════════════════════════════════════════════════════════════════════════
987
+ * UUID HELPER (self-contained, no import from parent)
988
+ * ═══════════════════════════════════════════════════════════════════════════ */
989
+ /**
990
+ * Derives a deterministic UUID v4 from an arbitrary seed string.
991
+ * @param {string} seed
992
+ * @returns {string}
993
+ */
994
+ function localSeedToUUIDv4(seed) {
995
+ const hash = crypto.createHash('sha256').update(seed).digest();
996
+ hash[6] = (hash[6] & 0x0f) | 0x40;
997
+ hash[8] = (hash[8] & 0x3f) | 0x80;
998
+ const hex = hash.toString('hex');
999
+ return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20, 32)].join('-');
1000
+ }
1001
+ /* ═══════════════════════════════════════════════════════════════════════════
1002
+ * CUSTOM MULTI-FRAME GENERATOR
1003
+ * Called by generateMultiFrame() when descriptor.customMultiFrameGenerator exists.
1004
+ * ═══════════════════════════════════════════════════════════════════════════ */
1005
+ /**
1006
+ * Generates a complete MultiFrameResult for a skin item using template-based
1007
+ * pixel painting. Each of the four cardinal directions gets a properly oriented
1008
+ * frame matrix; all share the same deterministic colour palette.
1009
+ *
1010
+ * @param {import('./semantic-layer-generator.js').GenerateLayerOptions & { frameCount?: number, startFrame?: number, frameDuration?: number }} options
1011
+ * @param {import('./semantic-layer-generator.js').SemanticDescriptor} _descriptor
1012
+ * @returns {import('./semantic-layer-generator.js').MultiFrameResult}
1013
+ */
1014
+ function generateSkinMultiFrame(options, _descriptor) {
1015
+ const { itemId, seed, frameCount = 1, startFrame = 0, frameDuration = 250 } = options;
1016
+ // Shared mutable palette; starts with index 0 = transparent
1017
+ const globalColors = [[0, 0, 0, 0]];
1018
+ // Skin subtype is injected via the descriptor (set during registration).
1019
+ // Falls back to 'random' for the legacy skin- prefix or any unknown descriptor.
1020
+ const subtype = _descriptor?.skinSubtype ?? 'random';
1021
+ const palette = deriveSkinPalette(seed, itemId, subtype);
1022
+ // Build idle direction matrices
1023
+ const matrices = {
1024
+ down: buildDirectionMatrix(ZONES.down, palette, globalColors, seed, itemId, 'down'),
1025
+ up: buildUpDirectionMatrix(ZONES.up, palette, globalColors, seed, itemId),
1026
+ left: buildDirectionMatrix(ZONES.left, palette, globalColors, seed, itemId, 'left'),
1027
+ right: buildDirectionMatrix(ZONES.right, palette, globalColors, seed, itemId, 'right'),
1028
+ };
1029
+ // Build 2-frame walk cycles from idle matrices (shared color palette, no extra allocations)
1030
+ const walkFrames = {
1031
+ down: buildWalkFrames(matrices.down),
1032
+ up: buildWalkFrames(matrices.up),
1033
+ left: buildWalkFrames(matrices.left),
1034
+ right: buildWalkFrames(matrices.right),
1035
+ };
1036
+ // Idle: frameCount identical copies of the direction matrix.
1037
+ const makeIdleArray = (matrix) => Array.from({ length: frameCount }, () => matrix.map((row) => [...row]));
1038
+ // Walking: always exactly 2 frames (walk cycle is independent of frameCount).
1039
+ const makeWalkArray = (frames2) => frames2.map((m) => m.map((row) => [...row]));
1040
+ const objectLayerRenderFramesData = {
1041
+ frame_duration: frameDuration,
1042
+ frames: {
1043
+ // DOWN (08) idle
1044
+ down_idle: makeIdleArray(matrices.down),
1045
+ none_idle: makeIdleArray(matrices.down),
1046
+ default_idle: makeIdleArray(matrices.down),
1047
+ // UP (02) idle
1048
+ up_idle: makeIdleArray(matrices.up),
1049
+ // LEFT (06) idle
1050
+ left_idle: makeIdleArray(matrices.left),
1051
+ up_left_idle: makeIdleArray(matrices.left),
1052
+ down_left_idle: makeIdleArray(matrices.left),
1053
+ // RIGHT (04) idle
1054
+ right_idle: makeIdleArray(matrices.right),
1055
+ up_right_idle: makeIdleArray(matrices.right),
1056
+ down_right_idle: makeIdleArray(matrices.right),
1057
+ // Walking animations – 2-frame bounce cycle
1058
+ down_walking: makeWalkArray(walkFrames.down),
1059
+ up_walking: makeWalkArray(walkFrames.up),
1060
+ left_walking: makeWalkArray(walkFrames.left),
1061
+ right_walking: makeWalkArray(walkFrames.right),
1062
+ },
1063
+ colors: globalColors,
1064
+ };
1065
+ const objectLayerData = {
1066
+ data: {
1067
+ item: {
1068
+ id: itemId,
1069
+ type: 'skin',
1070
+ description: `Procedurally generated character skin (seed: ${seed})`,
1071
+ activable: true,
1072
+ },
1073
+ stats: {
1074
+ effect: 0,
1075
+ resistance: 0,
1076
+ agility: 0,
1077
+ range: 0,
1078
+ intelligence: 0,
1079
+ utility: 0,
1080
+ },
1081
+ ledger: { type: 'OFF_CHAIN' },
1082
+ seed: localSeedToUUIDv4(`${seed}:${itemId}`),
1083
+ },
1084
+ };
1085
+ // Build synthetic frames array for layer-summary logging in cyberia.js
1086
+ const layerSummary = [
1087
+ { layerKey: 'skin', layerId: `${itemId}-skin`, keys: ZONES.down.skin.map(() => ({ type: 'template' })) },
1088
+ { layerKey: 'hair', layerId: `${itemId}-hair`, keys: ZONES.down.hair.map(() => ({ type: 'template' })) },
1089
+ { layerKey: 'shirt', layerId: `${itemId}-shirt`, keys: ZONES.down.shirt.map(() => ({ type: 'template' })) },
1090
+ { layerKey: 'pants', layerId: `${itemId}-pants`, keys: ZONES.down.pants.map(() => ({ type: 'template' })) },
1091
+ { layerKey: 'shoes', layerId: `${itemId}-shoes`, keys: ZONES.down.shoes.map(() => ({ type: 'template' })) },
1092
+ { layerKey: 'border', layerId: `${itemId}-border`, keys: ZONES.down.border.map(() => ({ type: 'template' })) },
1093
+ ];
1094
+ const frames = Array.from({ length: frameCount }, (_, fi) => ({
1095
+ itemId,
1096
+ seed,
1097
+ frameIndex: startFrame + fi,
1098
+ layers: layerSummary,
1099
+ compositeFrameMatrix: matrices.down,
1100
+ compositeColors: globalColors,
1101
+ }));
1102
+ return {
1103
+ itemId,
1104
+ seed,
1105
+ frameCount,
1106
+ frames,
1107
+ objectLayerRenderFramesData,
1108
+ objectLayerData,
1109
+ };
1110
+ }
1111
+ /* ═══════════════════════════════════════════════════════════════════════════
1112
+ * REGISTRATION
1113
+ * ═══════════════════════════════════════════════════════════════════════════ */
1114
+ /**
1115
+ * Registers all skin semantic descriptors.
1116
+ * Uses dependency injection — no import from the parent module.
1117
+ *
1118
+ * @param {function(string, import('./semantic-layer-generator.js').SemanticDescriptor): void} registerFn
1119
+ * @memberof SemanticLayerGeneratorSkin
1120
+ */
1121
+ export function registerSkinSemantics(registerFn) {
1122
+ /**
1123
+ * Skin subtypes — each maps a prefix to palette constraints.
1124
+ *
1125
+ * | Prefix | Skin tone | Hair pool | hairDepth |
1126
+ * |-----------------|---------------|--------------------|----------|
1127
+ * | skin-random | any | any | 5–11 |
1128
+ * | skin-dark | dark (1–3) | any | 5–11 |
1129
+ * | skin-light | light (6–8) | any | 5–11 |
1130
+ * | skin-vivid | any | vivid only | 5–11 |
1131
+ * | skin-natural | any | natural only | 5–11 |
1132
+ * | skin-shaved | any | any (unused) | 0 only |
1133
+ */
1134
+ const SUBTYPES = [
1135
+ { prefix: 'skin-random', subtype: 'random', desc: 'Fully random skin tone and hair' },
1136
+ { prefix: 'skin-dark', subtype: 'dark', desc: 'Dark skin tones' },
1137
+ { prefix: 'skin-light', subtype: 'light', desc: 'Light / pale skin tones' },
1138
+ { prefix: 'skin-vivid', subtype: 'vivid', desc: 'Vivid / exotic hair colours (blue, red, green…)' },
1139
+ { prefix: 'skin-natural', subtype: 'natural', desc: 'Natural hair colours (brown, blond, grey…)' },
1140
+ { prefix: 'skin-shaved', subtype: 'shaved', desc: 'Shaved / bald head — no hair' },
1141
+ ];
1142
+ const sharedLayers = {
1143
+ skin: { generator: 'template-zone' },
1144
+ hair: { generator: 'template-zone' },
1145
+ shirt: { generator: 'template-zone' },
1146
+ pants: { generator: 'template-zone' },
1147
+ shoes: { generator: 'template-zone' },
1148
+ border: { generator: 'template-zone' },
1149
+ };
1150
+ for (const { prefix, subtype, desc } of SUBTYPES) {
1151
+ registerFn(prefix, {
1152
+ semanticTags: ['character', 'body', 'humanoid'],
1153
+ paletteHints: [],
1154
+ preferredShapes: {},
1155
+ itemType: 'skin',
1156
+ skinSubtype: subtype,
1157
+ description: desc,
1158
+ layers: sharedLayers,
1159
+ // Custom generator bypasses the default shape/noise pipeline;
1160
+ // receives this descriptor so it can read skinSubtype.
1161
+ customMultiFrameGenerator: generateSkinMultiFrame,
1162
+ });
1163
+ }
1164
+ }