@underpostnet/cyberia 3.3.73 → 3.3.77

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 (269) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +10 -0
  3. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  4. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  5. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  8. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  9. package/.github/workflows/engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  11. package/AGENTS.md +61 -12
  12. package/CHANGELOG.md +159 -161
  13. package/CLI-HELP.md +20 -13
  14. package/Dockerfile +1 -1
  15. package/Dockerfile.dev +1 -1
  16. package/Dockerfile.test +2 -2
  17. package/README.md +28 -28
  18. package/bin/build.js +96 -21
  19. package/bin/cyberia.js +1247 -1373
  20. package/bin/index.js +1247 -1373
  21. package/compose.env +17 -5
  22. package/conf.js +7 -4
  23. package/deploy/cyberia-client/deploy.sh +10 -11
  24. package/deploy/cyberia-client/package.sh +0 -1
  25. package/deploy/cyberia-client/state.sh +3 -2
  26. package/deploy/cyberia-server/deploy.sh +10 -11
  27. package/deploy/cyberia-server/package.sh +0 -1
  28. package/deploy/cyberia-server/state.sh +3 -2
  29. package/deploy/dd-cyberia/deploy.sh +5 -5
  30. package/deploy/dd-cyberia/init.sh +15 -14
  31. package/deploy/dd-cyberia/package.sh +0 -1
  32. package/deploy/dd-cyberia/state.sh +2 -2
  33. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  34. package/deploy/lib/config.sh +29 -0
  35. package/deploy/lib/github-actions-logging.sh +92 -76
  36. package/deploy/lib/host.sh +28 -19
  37. package/deploy/lib/state.sh +4 -1
  38. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  39. package/deploy/release/deploy.sh +0 -2
  40. package/deployment.yaml +1 -1
  41. package/docker-compose.yml +23 -17
  42. package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
  43. package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
  44. package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
  45. package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
  46. package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
  47. package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
  48. package/docs/coverage/cyberia/base.css +362 -0
  49. package/docs/coverage/cyberia/block-navigation.js +82 -0
  50. package/docs/coverage/cyberia/favicon.png +0 -0
  51. package/docs/coverage/cyberia/index.html +161 -0
  52. package/docs/coverage/cyberia/prettify.css +101 -0
  53. package/docs/coverage/cyberia/prettify.js +937 -0
  54. package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
  55. package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
  56. package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
  57. package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
  58. package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
  59. package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
  60. package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
  61. package/docs/coverage/cyberia/sorter.js +205 -0
  62. package/hardhat/package-lock.json +2 -2
  63. package/hardhat/package.json +1 -1
  64. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  65. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  66. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  67. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  68. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  69. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  70. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  71. package/manifests/deployment/mongo-express/service.yaml +16 -0
  72. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  73. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  74. package/package.json +19 -17
  75. package/scripts/test-monitor.sh +3 -3
  76. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  77. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  80. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  81. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  82. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  83. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  84. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  85. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  86. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  87. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  88. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  90. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  91. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  92. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  93. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  94. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  95. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  96. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  97. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  98. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  99. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  100. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  101. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  102. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  103. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  104. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  107. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  110. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  111. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  112. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  113. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  114. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  115. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  116. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  118. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  119. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  120. package/src/api/file/file.ref.js +153 -0
  121. package/src/api/file/file.ref.json +11 -2
  122. package/src/api/object-layer/object-layer.model.js +47 -12
  123. package/src/api/object-layer/object-layer.router.js +10 -3
  124. package/src/api/object-layer/object-layer.service.js +113 -120
  125. package/src/cli/app.js +12 -1
  126. package/src/cli/client.js +2 -2
  127. package/src/cli/cluster.js +80 -18
  128. package/src/cli/deploy.js +215 -34
  129. package/src/cli/docker-compose.js +1 -1
  130. package/src/cli/fs-selection.js +115 -0
  131. package/src/cli/fs.js +158 -342
  132. package/src/cli/index.js +22 -10
  133. package/src/cli/release.js +3 -1
  134. package/src/cli/repository.js +35 -9
  135. package/src/cli/run.js +80 -199
  136. package/src/cli/test.js +4 -0
  137. package/src/cli/wireguard.js +32 -31
  138. package/src/client/components/core/Badge.js +0 -1
  139. package/src/client/components/core/CssCore.js +8 -1
  140. package/src/client/components/core/Docs.js +77 -100
  141. package/src/client/components/core/Modal.js +77 -23
  142. package/src/client/components/core/ToolTip.js +15 -0
  143. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
  144. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  145. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  146. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  147. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  148. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  149. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  150. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  151. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  152. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  153. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  154. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  155. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  156. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
  157. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  158. package/src/client/components/default/AppShellDefault.js +1 -4
  159. package/src/client/components/itemledger/AppShellItemledger.js +0 -2
  160. package/src/client/components/underpost/AppShellUnderpost.js +1 -1
  161. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  162. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  163. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  164. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  165. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  166. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  167. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  168. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  169. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  170. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  171. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  172. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  173. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  174. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  175. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  176. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  177. package/src/client/services/object-layer/object-layer.management.js +6 -10
  178. package/src/client/services/object-layer/object-layer.service.js +3 -1
  179. package/src/client/ssr/views/Cyberia404.js +3 -3
  180. package/src/client-builder/client-build-docs.js +52 -52
  181. package/src/client-builder/client-build.js +67 -44
  182. package/src/client-builder/client-bundle.js +347 -0
  183. package/src/db/mongo/MongoExpress.js +187 -0
  184. package/src/grpc/cyberia/grpc-server.js +5 -7
  185. package/src/index.js +1 -1
  186. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  187. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  188. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  189. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  190. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  191. package/src/projects/cyberia/gemini-client.js +3 -3
  192. package/src/projects/cyberia/generate-saga.js +4 -11
  193. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  194. package/src/projects/cyberia/instance-backup.js +210 -0
  195. package/src/projects/cyberia/instance-data.js +177 -257
  196. package/src/projects/cyberia/map-preview-generator.js +32 -68
  197. package/src/projects/cyberia/object-layer.js +152 -319
  198. package/src/projects/cyberia/seed-audio.js +139 -0
  199. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  200. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  201. package/src/projects/cyberia/stat-balance.js +81 -0
  202. package/src/projects/cyberia/stat-commands.js +23 -0
  203. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  204. package/src/runtime/cyberia-client/Dockerfile +8 -4
  205. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  206. package/src/runtime/cyberia-server/Dockerfile +9 -2
  207. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  208. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  209. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  210. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  211. package/src/runtime/engine-cyberia/compose.env +17 -5
  212. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  213. package/src/server/build/coverage.js +113 -42
  214. package/src/server/build/package.js +55 -12
  215. package/src/server/build/testing.js +59 -9
  216. package/src/server/network/middlewares.js +8 -2
  217. package/src/server/network/underpost-gateway.js +113 -14
  218. package/src/server/ops/logger.js +70 -35
  219. package/src/server/runtime/conf.js +40 -9
  220. package/src/server/runtime/start.js +11 -2
  221. package/src/server/storage/data-query.js +16 -0
  222. package/src/server/storage/downloader.js +62 -14
  223. package/src/server/storage/zip.js +153 -0
  224. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  225. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  226. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  227. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  228. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  229. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  230. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  231. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  232. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  233. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  234. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  235. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  236. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  237. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  238. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  239. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  240. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  241. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  242. package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
  243. package/test/unit/catalog.test.js +13 -2
  244. package/test/unit/client-build-docs.test.js +97 -70
  245. package/test/unit/client-bundle.test.js +313 -0
  246. package/test/unit/conf-resolution.test.js +58 -7
  247. package/test/unit/coverage-artifact.test.js +120 -29
  248. package/test/unit/cyberia/instance-backup.test.js +183 -0
  249. package/test/unit/cyberia/instance-data.test.js +314 -0
  250. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  251. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  252. package/test/unit/cyberia/stat-balance.test.js +69 -0
  253. package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
  254. package/test/unit/deploy-log-table.test.js +76 -5
  255. package/test/unit/downloader-integrity.test.js +98 -0
  256. package/test/unit/file-reference-registry.test.js +141 -0
  257. package/test/unit/fs-storage-paths.test.js +596 -0
  258. package/test/unit/gateway-pod-fetch.test.js +95 -0
  259. package/test/unit/letsencrypt-issuer.test.js +101 -0
  260. package/test/unit/logger-redaction.test.js +29 -0
  261. package/test/unit/package.test.js +70 -16
  262. package/test/unit/prepare-host.test.js +84 -1
  263. package/test/unit/release-bump.test.js +3 -1
  264. package/test/unit/start-options.test.js +12 -0
  265. package/test/unit/test-tiers.test.js +27 -0
  266. package/test/unit/zip-archive.test.js +142 -0
  267. package/vitest.config.js +11 -3
  268. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  269. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
@@ -5,14 +5,28 @@
5
5
  * @namespace CyberiaAtlasSpriteSheetGenerator
6
6
  */
7
7
 
8
- import fs from 'fs-extra';
9
- import path from 'path';
10
- import sharp from 'sharp';
11
8
  import { Jimp, rgbaToInt } from 'jimp';
12
9
  import { loggerFactory } from '../../server/ops/logger.js';
10
+ import { getKeyframeDirectionsByCode } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
13
11
 
14
12
  const logger = loggerFactory(import.meta);
15
13
 
14
+ /** Direction code of the still an item is previewed by: down idle. */
15
+ export const IDLE_PREVIEW_DIRECTION_CODE = '08';
16
+
17
+ /**
18
+ * Pixels per cell of the human-resolution atlas, when no factor is given.
19
+ * @memberof CyberiaAtlasSpriteSheetGenerator
20
+ */
21
+ export const DEFAULT_ATLAS_UPSCALE_FACTOR = 20;
22
+
23
+ /**
24
+ * Pixels per cell of the minified atlas. The client runtime downloads this one,
25
+ * so it stays at one pixel per cell.
26
+ * @memberof CyberiaAtlasSpriteSheetGenerator
27
+ */
28
+ const MINIFY_CELL_PIXEL_DIM = 1;
29
+
16
30
  /**
17
31
  * @typedef {Object} AtlasFrame
18
32
  * @property {number} x - X position in atlas
@@ -38,7 +52,7 @@ export class AtlasSpriteSheetGenerator {
38
52
  * @returns {Promise<Jimp>} The generated image
39
53
  * @memberof CyberiaAtlasSpriteSheetGenerator
40
54
  */
41
- static async frameMatrixToImage(frameMatrix, colors, cellPixelDim = 20) {
55
+ static async frameMatrixToImage(frameMatrix, colors, cellPixelDim = 1) {
42
56
  if (!frameMatrix || frameMatrix.length === 0 || frameMatrix[0].length === 0) {
43
57
  throw new Error('Invalid frame matrix');
44
58
  }
@@ -79,59 +93,47 @@ export class AtlasSpriteSheetGenerator {
79
93
  * @memberof CyberiaAtlasSpriteSheetGenerator
80
94
  */
81
95
  static calculateOptimalDimension(frameImages) {
82
- if (!frameImages || frameImages.length === 0) {
83
- logger.warn('No frames provided for dimension calculation, using default 2048');
84
- return 2048;
85
- }
86
-
87
- // Get max frame dimensions with defensive checks
88
- const widths = frameImages.map((f) => f.width).filter((w) => !isNaN(w) && w > 0);
89
- const heights = frameImages.map((f) => f.height).filter((h) => !isNaN(h) && h > 0);
90
-
91
- if (widths.length === 0 || heights.length === 0) {
92
- logger.warn('Invalid frame dimensions found, using default 2048');
93
- return 2048;
96
+ if (
97
+ !frameImages?.length ||
98
+ frameImages.some(
99
+ ({ width, height }) => !Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0,
100
+ )
101
+ ) {
102
+ throw new Error('Invalid frame dimensions');
94
103
  }
95
-
96
- const maxFrameWidth = Math.max(...widths);
97
- const maxFrameHeight = Math.max(...heights);
98
-
99
- logger.info(`Frame dimensions: max ${maxFrameWidth}x${maxFrameHeight}, count: ${frameImages.length}`);
100
-
101
- // Calculate grid layout (try to make it roughly square)
102
- const frameCount = frameImages.length;
103
- const cols = Math.ceil(Math.sqrt(frameCount));
104
- const rows = Math.ceil(frameCount / cols);
105
-
106
- // Calculate required dimensions
107
- const requiredWidth = cols * maxFrameWidth;
108
- const requiredHeight = rows * maxFrameHeight;
109
- const requiredDim = Math.max(requiredWidth, requiredHeight);
110
-
111
- logger.info(`Grid layout: ${cols}x${rows}, required: ${requiredWidth}x${requiredHeight}`);
112
-
113
- // Round up to next power of 2
114
- const optimalDim = AtlasSpriteSheetGenerator.nextPowerOf2(requiredDim);
115
-
116
- // Ensure minimum 1024, maximum 8192
117
- const finalDim = Math.max(1024, Math.min(8192, optimalDim));
118
-
119
- logger.info(`Calculated optimal dimension: ${finalDim}x${finalDim}`);
120
-
121
- return finalDim;
104
+ const cols = Math.ceil(Math.sqrt(frameImages.length));
105
+ const rows = Math.ceil(frameImages.length / cols);
106
+ return AtlasSpriteSheetGenerator.nextPowerOf2(
107
+ Math.max(
108
+ cols * Math.max(...frameImages.map((frame) => frame.width)),
109
+ rows * Math.max(...frameImages.map((frame) => frame.height)),
110
+ ),
111
+ );
122
112
  }
123
113
 
124
114
  /**
125
- * Consolidates all frames from an ObjectLayerRenderFrames into a single atlas sprite sheet
115
+ * Consolidates all frames of an ObjectLayerRenderFrames into one atlas sprite sheet.
116
+ *
117
+ * One packing produces two renders of the same layout: `minifyBuffer` at one
118
+ * pixel per cell, which the client runtime downloads, and `buffer` at
119
+ * `upscaleFactor` pixels per cell for human viewing. `metadata` describes the
120
+ * minified render, so atlas metadata and the served blob always agree.
121
+ *
126
122
  * @static
127
123
  * @param {Object} objectLayerRenderFrames - The ObjectLayerRenderFrames document
128
124
  * @param {string} itemKey - The item key for the atlas
129
- * @param {number} [cellPixelDim=20] - Pixel dimension per cell
125
+ * @param {number} [upscaleFactor=DEFAULT_ATLAS_UPSCALE_FACTOR] - Pixels per cell of the human-resolution render
130
126
  * @param {number} [maxAtlasDim=null] - Maximum atlas dimension (auto-calculated if null)
131
- * @returns {Promise<{buffer: Buffer, metadata: Object}>} Atlas buffer and metadata
127
+ * @returns {Promise<{buffer: Buffer, minifyBuffer: Buffer, metadata: Object}>} Both renders and the shared metadata
132
128
  * @memberof CyberiaAtlasSpriteSheetGenerator
133
129
  */
134
- static async generateAtlas(objectLayerRenderFrames, itemKey, cellPixelDim = 20, maxAtlasDim = null) {
130
+ static async generateAtlas(
131
+ objectLayerRenderFrames,
132
+ itemKey,
133
+ upscaleFactor = DEFAULT_ATLAS_UPSCALE_FACTOR,
134
+ maxAtlasDim = null,
135
+ ) {
136
+ if (!Number.isInteger(upscaleFactor) || upscaleFactor < 1) throw new Error('Invalid pixel scale');
135
137
  const { frames, colors } = objectLayerRenderFrames;
136
138
  const frameDuration = Number(objectLayerRenderFrames?.frame_duration);
137
139
 
@@ -157,7 +159,7 @@ export class AtlasSpriteSheetGenerator {
157
159
  'none_idle',
158
160
  ];
159
161
 
160
- // Generate all frame images
162
+ // The packing works on the minified render; the upscaled render reuses its layout.
161
163
  const frameImages = [];
162
164
 
163
165
  for (const direction of directionOrder) {
@@ -167,10 +169,15 @@ export class AtlasSpriteSheetGenerator {
167
169
  for (let frameIndex = 0; frameIndex < directionFrames.length; frameIndex++) {
168
170
  const frameMatrix = directionFrames[frameIndex];
169
171
  try {
170
- const frameImage = await AtlasSpriteSheetGenerator.frameMatrixToImage(frameMatrix, colors, cellPixelDim);
172
+ const frameImage = await AtlasSpriteSheetGenerator.frameMatrixToImage(
173
+ frameMatrix,
174
+ colors,
175
+ MINIFY_CELL_PIXEL_DIM,
176
+ );
171
177
 
172
178
  frameImages.push({
173
179
  image: frameImage,
180
+ frameMatrix,
174
181
  direction,
175
182
  frameIndex,
176
183
  width: frameImage.bitmap.width,
@@ -194,10 +201,8 @@ export class AtlasSpriteSheetGenerator {
194
201
  );
195
202
  }
196
203
 
197
- // Validate maxAtlasDim
198
- if (isNaN(maxAtlasDim) || maxAtlasDim <= 0) {
199
- logger.error(`Invalid maxAtlasDim: ${maxAtlasDim}, using default 2048`);
200
- maxAtlasDim = 2048;
204
+ if (!Number.isInteger(maxAtlasDim) || maxAtlasDim <= 0 || maxAtlasDim > 4096) {
205
+ throw new Error('Atlas dimension must be between 1 and 4096');
201
206
  }
202
207
 
203
208
  // Simple grid packing algorithm
@@ -215,43 +220,29 @@ export class AtlasSpriteSheetGenerator {
215
220
  );
216
221
  }
217
222
 
218
- // Create atlas canvas
219
- const atlasImage = new Jimp({ width: atlasWidth, height: atlasHeight, color: 0x00000000 });
223
+ const minifyImage = new Jimp({ width: atlasWidth, height: atlasHeight, color: 0x00000000 });
224
+ const upscaledImage = new Jimp({
225
+ width: atlasWidth * upscaleFactor,
226
+ height: atlasHeight * upscaleFactor,
227
+ color: 0x00000000,
228
+ });
220
229
 
221
- // Build metadata structure first
222
230
  const frameMetadata = {};
223
231
 
224
- // Composite frames onto atlas
225
- let loggedCount = 0;
226
232
  for (const packedFrame of packedFrames) {
227
- const { image, x, y, direction, frameIndex } = packedFrame;
228
-
229
- // Log first few frames for debugging
230
- if (loggedCount < 3) {
231
- logger.info(
232
- `Frame ${loggedCount + 1}: ${direction}[${frameIndex}] → (${x},${y}) ${image.bitmap.width}x${image.bitmap.height}`,
233
- );
234
- loggedCount++;
235
- }
236
-
237
- // Manually copy pixels to ensure correct positioning
238
- for (let srcY = 0; srcY < image.bitmap.height; srcY++) {
239
- for (let srcX = 0; srcX < image.bitmap.width; srcX++) {
240
- const destX = x + srcX;
241
- const destY = y + srcY;
233
+ const { image, frameMatrix, x, y, direction, frameIndex } = packedFrame;
242
234
 
243
- // Skip if out of bounds
244
- if (destX >= atlasWidth || destY >= atlasHeight) continue;
235
+ AtlasSpriteSheetGenerator.blitFrame(minifyImage, image, x, y);
245
236
 
246
- // Get pixel color from source image
247
- const color = image.getPixelColor(srcX, srcY);
248
-
249
- // Set pixel in atlas at destination position
250
- atlasImage.setPixelColor(color, destX, destY);
251
- }
237
+ // The same cell grid at a larger scale, so the upscaled atlas is an exact
238
+ // multiple of the layout the metadata describes.
239
+ if (upscaleFactor > MINIFY_CELL_PIXEL_DIM) {
240
+ const upscaledFrame = await AtlasSpriteSheetGenerator.frameMatrixToImage(frameMatrix, colors, upscaleFactor);
241
+ AtlasSpriteSheetGenerator.blitFrame(upscaledImage, upscaledFrame, x * upscaleFactor, y * upscaleFactor);
242
+ } else {
243
+ AtlasSpriteSheetGenerator.blitFrame(upscaledImage, image, x, y);
252
244
  }
253
245
 
254
- // Store metadata in correct order
255
246
  if (!frameMetadata[direction]) {
256
247
  frameMetadata[direction] = [];
257
248
  }
@@ -265,19 +256,80 @@ export class AtlasSpriteSheetGenerator {
265
256
  };
266
257
  }
267
258
 
268
- // Convert to PNG buffer
269
- const buffer = await atlasImage.getBuffer('image/png');
270
-
271
259
  const metadata = {
272
260
  itemKey,
273
261
  atlasWidth,
274
262
  atlasHeight,
275
- cellPixelDim,
263
+ cellPixelDim: MINIFY_CELL_PIXEL_DIM,
264
+ upscaleFactor,
276
265
  frame_duration: Number.isFinite(frameDuration) ? frameDuration : 100,
277
266
  frames: frameMetadata,
278
267
  };
279
268
 
280
- return { buffer, metadata };
269
+ return {
270
+ buffer: await upscaledImage.getBuffer('image/png'),
271
+ minifyBuffer: await minifyImage.getBuffer('image/png'),
272
+ metadata,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * The frame an item is previewed by: the first frame of the first down-idle keyframe the
278
+ * atlas carries, or the first frame of any direction when it has none.
279
+ *
280
+ * @static
281
+ * @param {Object} metadata - Atlas metadata as {@link generateAtlas} returns it.
282
+ * @returns {{x:number,y:number,width:number,height:number}|null} Frame box in minified cells.
283
+ * @memberof CyberiaAtlasSpriteSheetGenerator
284
+ */
285
+ static idlePreviewFrame(metadata) {
286
+ const frames = metadata?.frames ?? {};
287
+ const preferred = getKeyframeDirectionsByCode(IDLE_PREVIEW_DIRECTION_CODE);
288
+ for (const direction of [...preferred, ...Object.keys(frames)]) {
289
+ const frame = frames[direction]?.[0];
290
+ if (frame) return frame;
291
+ }
292
+ return null;
293
+ }
294
+
295
+ /**
296
+ * Cuts the idle-preview still out of the human-resolution render, using the same layout
297
+ * `metadata.frames` describes for the minified one scaled by `metadata.upscaleFactor`.
298
+ *
299
+ * @static
300
+ * @param {Buffer} buffer - PNG bytes of the human-resolution atlas render.
301
+ * @param {Object} metadata - Atlas metadata as {@link generateAtlas} returns it.
302
+ * @returns {Promise<Buffer|null>} PNG bytes of the still, or null when the atlas has no frame.
303
+ * @memberof CyberiaAtlasSpriteSheetGenerator
304
+ */
305
+ static async idlePreviewFromAtlas(buffer, metadata) {
306
+ const frame = AtlasSpriteSheetGenerator.idlePreviewFrame(metadata);
307
+ if (!frame) return null;
308
+ const scale = Number(metadata.upscaleFactor) || 1;
309
+ const image = await Jimp.read(buffer);
310
+ image.crop({ x: frame.x * scale, y: frame.y * scale, w: frame.width * scale, h: frame.height * scale });
311
+ return await image.getBuffer('image/png');
312
+ }
313
+
314
+ /**
315
+ * Copies a frame image onto an atlas canvas at the given origin.
316
+ * @static
317
+ * @param {Jimp} atlasImage - Destination atlas canvas.
318
+ * @param {Jimp} frameImage - Source frame image.
319
+ * @param {number} originX - Destination X origin.
320
+ * @param {number} originY - Destination Y origin.
321
+ * @returns {void}
322
+ * @memberof CyberiaAtlasSpriteSheetGenerator
323
+ */
324
+ static blitFrame(atlasImage, frameImage, originX, originY) {
325
+ for (let srcY = 0; srcY < frameImage.bitmap.height; srcY++) {
326
+ for (let srcX = 0; srcX < frameImage.bitmap.width; srcX++) {
327
+ const destX = originX + srcX;
328
+ const destY = originY + srcY;
329
+ if (destX >= atlasImage.bitmap.width || destY >= atlasImage.bitmap.height) continue;
330
+ atlasImage.setPixelColor(frameImage.getPixelColor(srcX, srcY), destX, destY);
331
+ }
332
+ }
281
333
  }
282
334
 
283
335
  /**
@@ -302,7 +354,6 @@ export class AtlasSpriteSheetGenerator {
302
354
  let rowHeight = 0;
303
355
  let maxWidth = 0;
304
356
  let totalHeight = 0;
305
- let exceedsCount = 0;
306
357
 
307
358
  for (const frame of sortedFrames) {
308
359
  // Check if frame fits in current row
@@ -313,9 +364,8 @@ export class AtlasSpriteSheetGenerator {
313
364
  rowHeight = 0;
314
365
  }
315
366
 
316
- // Check if we exceed max height
317
- if (currentY + frame.height > maxDim) {
318
- exceedsCount++;
367
+ if (frame.width > maxDim || currentY + frame.height > maxDim) {
368
+ throw new Error(`Frames exceed atlas dimension ${maxDim}`);
319
369
  }
320
370
 
321
371
  packedFrames.push({
@@ -330,20 +380,8 @@ export class AtlasSpriteSheetGenerator {
330
380
  totalHeight = Math.max(totalHeight, currentY + frame.height);
331
381
  }
332
382
 
333
- // Log once if frames exceeded dimensions
334
- if (exceedsCount > 0) {
335
- const recommendedSize = AtlasSpriteSheetGenerator.nextPowerOf2(Math.max(maxWidth, totalHeight));
336
- logger.warn(
337
- `${exceedsCount} frames exceed atlas dimension (${maxDim}x${maxDim}). ` +
338
- `Actual required: ${maxWidth}x${totalHeight}. ` +
339
- `Recommended: --to-atlas-sprite-sheet ${recommendedSize}`,
340
- );
341
- }
342
-
343
- // Use maxDim as the atlas dimensions (already power-of-2 and optimal from auto-calculation)
344
- // If all frames fit within a smaller power-of-2, use that instead
345
- const atlasWidth = maxDim;
346
- const atlasHeight = maxDim;
383
+ const atlasWidth = maxWidth;
384
+ const atlasHeight = totalHeight;
347
385
 
348
386
  return {
349
387
  packedFrames,
@@ -369,45 +407,4 @@ export class AtlasSpriteSheetGenerator {
369
407
  n |= n >> 16;
370
408
  return n + 1;
371
409
  }
372
-
373
- /**
374
- * Generates atlas and saves to file system
375
- * @static
376
- * @param {Object} objectLayerRenderFrames - The ObjectLayerRenderFrames document
377
- * @param {string} itemKey - The item key for the atlas
378
- * @param {string} outputPath - Output file path
379
- * @param {number} [cellPixelDim=20] - Pixel dimension per cell
380
- * @param {number} [maxAtlasDim=null] - Maximum atlas dimension (auto-calculated if null)
381
- * @returns {Promise<{buffer: Buffer, metadata: Object, outputPath: string}>}
382
- * @memberof CyberiaAtlasSpriteSheetGenerator
383
- */
384
- static async generateAtlasToFile(
385
- objectLayerRenderFrames,
386
- itemKey,
387
- outputPath,
388
- cellPixelDim = 20,
389
- maxAtlasDim = null,
390
- ) {
391
- const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
392
- objectLayerRenderFrames,
393
- itemKey,
394
- cellPixelDim,
395
- maxAtlasDim,
396
- );
397
-
398
- await fs.ensureDir(path.dirname(outputPath));
399
- await fs.writeFile(outputPath, buffer);
400
-
401
- logger.info(`Atlas sprite sheet generated: ${outputPath}`);
402
-
403
- return { buffer, metadata, outputPath };
404
- }
405
410
  }
406
-
407
- // Export convenience functions
408
- export const generateAtlas = AtlasSpriteSheetGenerator.generateAtlas;
409
- export const generateAtlasToFile = AtlasSpriteSheetGenerator.generateAtlasToFile;
410
- export const frameMatrixToImage = AtlasSpriteSheetGenerator.frameMatrixToImage;
411
- export const packFramesGrid = AtlasSpriteSheetGenerator.packFramesGrid;
412
- export const nextPowerOf2 = AtlasSpriteSheetGenerator.nextPowerOf2;
413
- export const calculateOptimalDimension = AtlasSpriteSheetGenerator.calculateOptimalDimension;