cyberia 3.1.3 → 3.2.5

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 (208) 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 +13 -8
  10. package/CHANGELOG.md +433 -1
  11. package/CLI-HELP.md +57 -7
  12. package/Dockerfile +4 -2
  13. package/README.md +347 -22
  14. package/bin/build.js +5 -2
  15. package/bin/cyberia.js +1789 -112
  16. package/bin/deploy.js +177 -124
  17. package/bin/file.js +3 -0
  18. package/bin/index.js +1789 -112
  19. package/conf.js +64 -8
  20. package/deployment.yaml +92 -20
  21. package/hardhat/hardhat.config.js +13 -13
  22. package/hardhat/ignition/modules/ObjectLayerToken.js +1 -1
  23. package/hardhat/package-lock.json +2554 -5859
  24. package/hardhat/package.json +13 -22
  25. package/hardhat/scripts/deployObjectLayerToken.js +1 -1
  26. package/hardhat/test/ObjectLayerToken.js +4 -2
  27. package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
  28. package/hardhat/types/ethers-contracts/common.ts +92 -0
  29. package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
  30. package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
  31. package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
  32. package/hardhat/types/ethers-contracts/index.ts +6 -0
  33. package/jsdoc.dd-cyberia.json +64 -55
  34. package/jsdoc.json +64 -55
  35. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -4
  36. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -4
  37. package/manifests/deployment/dd-cyberia-development/deployment.yaml +92 -20
  38. package/manifests/deployment/dd-cyberia-development/proxy.yaml +54 -18
  39. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  40. package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
  41. package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
  42. package/manifests/deployment/playwright/deployment.yaml +1 -1
  43. package/nodemon.json +1 -1
  44. package/package.json +22 -16
  45. package/proxy.yaml +54 -18
  46. package/scripts/rhel-grpc-setup.sh +56 -0
  47. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +44 -0
  48. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +16 -0
  49. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  50. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +80 -7
  51. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +93 -0
  52. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
  53. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +29 -0
  54. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +51 -0
  55. package/src/api/cyberia-entity/cyberia-entity.controller.js +74 -0
  56. package/src/api/cyberia-entity/cyberia-entity.model.js +24 -0
  57. package/src/api/cyberia-entity/cyberia-entity.router.js +27 -0
  58. package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
  59. package/src/api/cyberia-instance/cyberia-fallback-world.js +368 -0
  60. package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
  61. package/src/api/cyberia-instance/cyberia-instance.model.js +84 -0
  62. package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
  63. package/src/api/cyberia-instance/cyberia-instance.service.js +191 -0
  64. package/src/api/cyberia-instance/cyberia-portal-connector.js +486 -0
  65. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +74 -0
  66. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +413 -0
  67. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +228 -0
  68. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +27 -0
  69. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +42 -0
  70. package/src/api/cyberia-map/cyberia-map.controller.js +79 -0
  71. package/src/api/cyberia-map/cyberia-map.model.js +30 -0
  72. package/src/api/cyberia-map/cyberia-map.router.js +40 -0
  73. package/src/api/cyberia-map/cyberia-map.service.js +74 -0
  74. package/src/api/file/file.ref.json +18 -0
  75. package/src/api/ipfs/ipfs.controller.js +4 -25
  76. package/src/api/ipfs/ipfs.model.js +43 -34
  77. package/src/api/ipfs/ipfs.router.js +8 -13
  78. package/src/api/ipfs/ipfs.service.js +54 -102
  79. package/src/api/object-layer/README.md +347 -22
  80. package/src/api/object-layer/object-layer.router.js +30 -0
  81. package/src/api/object-layer/object-layer.service.js +114 -31
  82. package/src/api/user/user.service.js +8 -7
  83. package/src/cli/cluster.js +7 -7
  84. package/src/cli/db.js +710 -827
  85. package/src/cli/deploy.js +151 -93
  86. package/src/cli/env.js +29 -0
  87. package/src/cli/fs.js +5 -2
  88. package/src/cli/index.js +48 -2
  89. package/src/cli/kubectl.js +211 -0
  90. package/src/cli/release.js +284 -0
  91. package/src/cli/repository.js +438 -75
  92. package/src/cli/run.js +195 -35
  93. package/src/cli/secrets.js +73 -0
  94. package/src/cli/test.js +3 -3
  95. package/src/client/Cryptokoyn.index.js +3 -4
  96. package/src/client/CyberiaPortal.index.js +3 -4
  97. package/src/client/Default.index.js +3 -4
  98. package/src/client/Itemledger.index.js +3 -4
  99. package/src/client/Underpost.index.js +3 -4
  100. package/src/client/components/core/AppStore.js +69 -0
  101. package/src/client/components/core/CalendarCore.js +2 -2
  102. package/src/client/components/core/DropDown.js +137 -17
  103. package/src/client/components/core/Keyboard.js +2 -2
  104. package/src/client/components/core/LogIn.js +2 -2
  105. package/src/client/components/core/LogOut.js +2 -2
  106. package/src/client/components/core/Modal.js +0 -1
  107. package/src/client/components/core/Panel.js +0 -1
  108. package/src/client/components/core/PanelForm.js +19 -19
  109. package/src/client/components/core/SocketIo.js +82 -29
  110. package/src/client/components/core/SocketIoHandler.js +75 -0
  111. package/src/client/components/core/Stream.js +143 -95
  112. package/src/client/components/core/Webhook.js +40 -7
  113. package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
  114. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +3 -3
  115. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +2 -2
  116. package/src/client/components/cryptokoyn/MenuCryptokoyn.js +3 -3
  117. package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
  118. package/src/client/components/cyberia/InstanceEngineCyberia.js +700 -0
  119. package/src/client/components/cyberia/MapEngineCyberia.js +1359 -2
  120. package/src/client/components/cyberia/ObjectLayerEngineModal.js +17 -6
  121. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +92 -54
  122. package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
  123. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +216 -30
  124. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +3 -3
  125. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +2 -2
  126. package/src/client/components/cyberia-portal/MenuCyberiaPortal.js +40 -7
  127. package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +4 -0
  128. package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
  129. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  130. package/src/client/components/default/AppStoreDefault.js +5 -0
  131. package/src/client/components/default/LogInDefault.js +3 -3
  132. package/src/client/components/default/LogOutDefault.js +2 -2
  133. package/src/client/components/default/MenuDefault.js +5 -5
  134. package/src/client/components/default/SocketIoDefault.js +3 -51
  135. package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
  136. package/src/client/components/itemledger/LogInItemledger.js +3 -3
  137. package/src/client/components/itemledger/LogOutItemledger.js +2 -2
  138. package/src/client/components/itemledger/MenuItemledger.js +3 -3
  139. package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
  140. package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
  141. package/src/client/components/underpost/LogInUnderpost.js +3 -3
  142. package/src/client/components/underpost/LogOutUnderpost.js +2 -2
  143. package/src/client/components/underpost/MenuUnderpost.js +5 -5
  144. package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
  145. package/src/client/services/core/core.service.js +20 -8
  146. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +105 -0
  147. package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
  148. package/src/client/services/cyberia-entity/cyberia-entity.service.js +105 -0
  149. package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
  150. package/src/client/services/cyberia-instance/cyberia-instance.service.js +122 -0
  151. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +105 -0
  152. package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
  153. package/src/client/services/cyberia-map/cyberia-map.service.js +126 -0
  154. package/src/client/services/instance/instance.management.js +2 -2
  155. package/src/client/services/ipfs/ipfs.service.js +3 -23
  156. package/src/client/services/object-layer/object-layer.management.js +3 -3
  157. package/src/client/services/object-layer/object-layer.service.js +21 -0
  158. package/src/client/services/user/user.management.js +2 -2
  159. package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
  160. package/src/grpc/cyberia/OFF_CHAIN_ECONOMY.md +305 -0
  161. package/src/grpc/cyberia/README.md +326 -0
  162. package/src/grpc/cyberia/grpc-server.js +530 -0
  163. package/src/index.js +24 -1
  164. package/src/runtime/express/Dockerfile +4 -0
  165. package/src/runtime/express/Express.js +18 -1
  166. package/src/runtime/lampp/Dockerfile +13 -2
  167. package/src/runtime/lampp/Lampp.js +27 -4
  168. package/src/runtime/wp/Dockerfile +68 -0
  169. package/src/runtime/wp/Wp.js +639 -0
  170. package/src/server/auth.js +24 -1
  171. package/src/server/backup.js +37 -9
  172. package/src/server/client-build-docs.js +9 -2
  173. package/src/server/client-build.js +31 -31
  174. package/src/server/client-formatted.js +109 -57
  175. package/src/server/conf.js +24 -9
  176. package/src/server/cron.js +25 -23
  177. package/src/server/dns.js +2 -1
  178. package/src/server/ipfs-client.js +24 -1
  179. package/src/server/object-layer.js +149 -108
  180. package/src/server/peer.js +8 -0
  181. package/src/server/runtime.js +25 -1
  182. package/src/server/semantic-layer-generator-floor.js +359 -0
  183. package/src/server/semantic-layer-generator-skin.js +1294 -0
  184. package/src/server/semantic-layer-generator.js +116 -555
  185. package/src/server/start.js +2 -2
  186. package/src/ws/IoInterface.js +1 -10
  187. package/src/ws/IoServer.js +14 -33
  188. package/src/ws/core/channels/core.ws.chat.js +65 -20
  189. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  190. package/src/ws/core/channels/core.ws.stream.js +90 -31
  191. package/src/ws/core/core.ws.connection.js +12 -33
  192. package/src/ws/core/core.ws.emit.js +10 -26
  193. package/src/ws/core/core.ws.server.js +25 -58
  194. package/src/ws/default/channels/default.ws.main.js +53 -12
  195. package/src/ws/default/default.ws.connection.js +26 -13
  196. package/src/ws/default/default.ws.server.js +30 -12
  197. package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
  198. package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
  199. package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
  200. package/src/client/components/default/ElementsDefault.js +0 -38
  201. package/src/client/components/itemledger/CommonItemledger.js +0 -29
  202. package/src/client/components/itemledger/ElementsItemledger.js +0 -38
  203. package/src/client/components/underpost/CommonUnderpost.js +0 -29
  204. package/src/client/components/underpost/ElementsUnderpost.js +0 -38
  205. package/src/ws/core/management/core.ws.chat.js +0 -8
  206. package/src/ws/core/management/core.ws.mailer.js +0 -16
  207. package/src/ws/core/management/core.ws.stream.js +0 -8
  208. package/src/ws/default/management/default.ws.main.js +0 -8
@@ -1,12 +1,16 @@
1
1
  /**
2
- * Semantic Layer Generator for Cyberia Online.
2
+ * Semantic Layer Generator for Cyberia Online — Router / Dispatcher.
3
3
  *
4
- * Produces semantically consistent object layers with controlled, reproducible
5
- * variation and short-term temporal coherence (consecutive frames stay consistent).
6
- *
7
- * Uses the shape-generator primitives (createRng, seedToInt, createNoise2D, generateShape)
8
- * and object-layer engine (createObjectLayerDocuments, buildImgFromTile) to produce
9
- * frame matrices that can be persisted to MongoDB / IPFS / static assets.
4
+ * This module is the public API surface. It:
5
+ * 1. Owns the semantic registry (registerSemantic / lookupSemantic).
6
+ * 2. Provides the shared utility functions used by category submodules.
7
+ * 3. Implements the default shape/noise-field generation pipeline
8
+ * (generateFrame / generateMultiFrame).
9
+ * 4. Delegates to descriptor-attached custom generators when present
10
+ * (e.g. skin uses template-based pixel painting via customMultiFrameGenerator).
11
+ * 5. Loads category submodules at initialisation:
12
+ * • semantic-layer-generator-floor.js — all floor-* descriptors
13
+ * • semantic-layer-generator-skin.js — skin-* descriptors
10
14
  *
11
15
  * @module src/server/semantic-layer-generator.js
12
16
  * @namespace SemanticLayerGenerator
@@ -17,6 +21,9 @@ import crypto from 'crypto';
17
21
  import { createRng, seedToInt, createNoise2D, generateShape, listShapes } from './shape-generator.js';
18
22
  import { loggerFactory } from './logger.js';
19
23
 
24
+ import { registerFloorSemantics } from './semantic-layer-generator-floor.js';
25
+ import { registerSkinSemantics } from './semantic-layer-generator-skin.js';
26
+
20
27
  const logger = loggerFactory(import.meta);
21
28
 
22
29
  /* ═══════════════════════════════════════════════════════════════════════════
@@ -25,39 +32,34 @@ const logger = loggerFactory(import.meta);
25
32
 
26
33
  /**
27
34
  * Produces a deterministic 32-bit integer hash from an arbitrary string.
28
- * Used to derive per-layer and per-frame seeds.
29
35
  * @param {string} str
30
36
  * @returns {number}
31
37
  * @memberof SemanticLayerGenerator
32
38
  */
39
+ function hashString(str) {
40
+ let h = 0;
41
+ for (let i = 0; i < str.length; i++) {
42
+ h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
43
+ }
44
+ return h;
45
+ }
46
+
33
47
  /**
34
48
  * Derives a deterministic UUID v4 from an arbitrary seed string.
35
- * Uses SHA-256 to hash the seed, then formats 16 bytes as a valid UUID v4
36
- * (version nibble = 4, variant bits = 10xx).
37
49
  * @param {string} seed
38
- * @returns {string} A valid UUID v4 string.
50
+ * @returns {string}
39
51
  * @memberof SemanticLayerGenerator
40
52
  */
41
53
  function seedToUUIDv4(seed) {
42
54
  const hash = crypto.createHash('sha256').update(seed).digest();
43
- // Set version nibble (byte 6, high nibble) to 0100 (version 4)
44
55
  hash[6] = (hash[6] & 0x0f) | 0x40;
45
- // Set variant bits (byte 8, high 2 bits) to 10xx
46
56
  hash[8] = (hash[8] & 0x3f) | 0x80;
47
57
  const hex = hash.toString('hex');
48
58
  return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20, 32)].join('-');
49
59
  }
50
60
 
51
- function hashString(str) {
52
- let h = 0;
53
- for (let i = 0; i < str.length; i++) {
54
- h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
55
- }
56
- return h;
57
- }
58
-
59
61
  /**
60
- * Derives a per-layer seed: layerSeed = hash(seed + ':' + itemId + ':' + layerKey)
62
+ * Derives a per-layer seed.
61
63
  * @param {string} seed
62
64
  * @param {string} itemId
63
65
  * @param {string} layerKey
@@ -69,7 +71,7 @@ function deriveLayerSeed(seed, itemId, layerKey) {
69
71
  }
70
72
 
71
73
  /**
72
- * Derives a per-frame seed: frameSeed = hash(layerSeed + ':' + frameIndex)
74
+ * Derives a per-frame seed.
73
75
  * @param {number} layerSeed
74
76
  * @param {number} frameIndex
75
77
  * @returns {number}
@@ -81,34 +83,34 @@ function deriveFrameSeed(layerSeed, frameIndex) {
81
83
 
82
84
  /* ═══════════════════════════════════════════════════════════════════════════
83
85
  * SEMANTIC REGISTRY
84
- * Maps item-id prefixes to semantic descriptors that drive generation.
85
86
  * ═══════════════════════════════════════════════════════════════════════════ */
86
87
 
87
88
  /**
88
89
  * @typedef {Object} SemanticDescriptor
89
- * @property {string[]} semanticTags - Conceptual tags (e.g. ['sand','dune']).
90
- * @property {number[][]} paletteHints - Array of RGBA palette colors.
91
- * @property {Object<string,number>} preferredShapes - Shape key → relative weight.
92
- * @property {Object<string,LayerSpec>} layers - Named layer specifications.
93
- * @property {string} itemType - Object layer type (floor, skin, weapon…).
90
+ * @property {string[]} semanticTags Conceptual tags.
91
+ * @property {number[][]} paletteHints RGBA palette colours.
92
+ * @property {Object<string,number>} preferredShapes Shape key → weight.
93
+ * @property {Object<string,LayerSpec>} layers Named layer specs.
94
+ * @property {string} itemType floor | skin | weapon | skill | coin
95
+ * @property {Function} [customMultiFrameGenerator] Overrides the default multi-frame pipeline.
94
96
  * @memberof SemanticLayerGenerator
95
97
  */
96
98
 
97
99
  /**
98
100
  * @typedef {Object} LayerSpec
99
- * @property {string} generator - 'shape' | 'noise-field' | 'sprite'
100
- * @property {string[]} shapes - Candidate shape keys for this layer.
101
- * @property {number} count - Number of elements to place.
102
- * @property {number} scaleVariance - Max ± scale deviation (0..1).
103
- * @property {number} rotationVariance- Max ± rotation deviation in degrees.
104
- * @property {number} colorShift - Max RGBA channel shift per element.
105
- * @property {number} jitter - Positional jitter amount (0..1).
106
- * @property {number} noiseLevel - Noise displacement applied to shapes.
107
- * @property {number} detailLevel - Point count multiplier for shapes.
108
- * @property {number} sparsity - Fraction of grid cells left empty (0..1).
109
- * @property {number} [frameJitter] - Per-frame positional wobble for temporal coherence.
110
- * @property {number} [frameRotation] - Per-frame rotation wobble in degrees.
111
- * @property {number} [frameScale] - Per-frame scale wobble.
101
+ * @property {string} generator 'shape' | 'noise-field' | 'template-zone'
102
+ * @property {string[]} [shapes]
103
+ * @property {number} [count]
104
+ * @property {number} [scaleVariance]
105
+ * @property {number} [rotationVariance]
106
+ * @property {number} [colorShift]
107
+ * @property {number} [jitter]
108
+ * @property {number} [noiseLevel]
109
+ * @property {number} [detailLevel]
110
+ * @property {number} [sparsity]
111
+ * @property {number} [frameJitter]
112
+ * @property {number} [frameRotation]
113
+ * @property {number} [frameScale]
112
114
  * @memberof SemanticLayerGenerator
113
115
  */
114
116
 
@@ -137,7 +139,6 @@ export function registerSemantic(prefix, descriptor) {
137
139
  */
138
140
  export function lookupSemantic(itemId) {
139
141
  if (semanticRegistry[itemId]) return semanticRegistry[itemId];
140
- // Longest prefix match
141
142
  let best = null;
142
143
  let bestLen = 0;
143
144
  for (const prefix of Object.keys(semanticRegistry)) {
@@ -149,385 +150,9 @@ export function lookupSemantic(itemId) {
149
150
  return best;
150
151
  }
151
152
 
152
- /* ── Built-in semantic descriptors ─────────────────────────────────────── */
153
-
154
- registerSemantic('floor-desert', {
155
- semanticTags: ['sand', 'dune', 'arid', 'dry'],
156
- paletteHints: [
157
- [210, 180, 120, 255], // warm sand
158
- [194, 160, 100, 255], // darker sand
159
- [230, 205, 150, 255], // light sand
160
- [180, 140, 80, 255], // ochre
161
- [160, 120, 70, 255], // deep ochre
162
- [120, 90, 55, 255], // rock brown
163
- [100, 80, 50, 255], // shadow
164
- [240, 220, 175, 255], // highlight
165
- ],
166
- preferredShapes: { ellipse: 3, circle: 2, cactus: 1, star: 0.5 },
167
- itemType: 'floor',
168
- layers: {
169
- base: {
170
- generator: 'noise-field',
171
- shapes: ['ellipse'],
172
- count: 0,
173
- scaleVariance: 0,
174
- rotationVariance: 0,
175
- colorShift: 8,
176
- jitter: 0,
177
- noiseLevel: 0.3,
178
- detailLevel: 1,
179
- sparsity: 0,
180
- frameJitter: 0,
181
- frameRotation: 0,
182
- frameScale: 0,
183
- },
184
- dunes: {
185
- generator: 'shape',
186
- shapes: ['ellipse', 'circle'],
187
- count: 5,
188
- scaleVariance: 0.3,
189
- rotationVariance: 15,
190
- colorShift: 12,
191
- jitter: 0.08,
192
- noiseLevel: 0.15,
193
- detailLevel: 1.2,
194
- sparsity: 0.3,
195
- frameJitter: 0.005,
196
- frameRotation: 0.5,
197
- frameScale: 0.005,
198
- },
199
- rocks: {
200
- generator: 'shape',
201
- shapes: ['star', 'circle'],
202
- count: 3,
203
- scaleVariance: 0.4,
204
- rotationVariance: 45,
205
- colorShift: 15,
206
- jitter: 0.12,
207
- noiseLevel: 0.2,
208
- detailLevel: 1,
209
- sparsity: 0.5,
210
- frameJitter: 0.002,
211
- frameRotation: 0.3,
212
- frameScale: 0.003,
213
- },
214
- tufts: {
215
- generator: 'shape',
216
- shapes: ['cactus', 'star'],
217
- count: 2,
218
- scaleVariance: 0.25,
219
- rotationVariance: 20,
220
- colorShift: 10,
221
- jitter: 0.1,
222
- noiseLevel: 0.1,
223
- detailLevel: 0.8,
224
- sparsity: 0.6,
225
- frameJitter: 0.008,
226
- frameRotation: 1.0,
227
- frameScale: 0.006,
228
- },
229
- },
230
- });
231
-
232
- registerSemantic('floor-grass', {
233
- semanticTags: ['grass', 'meadow', 'green', 'earth'],
234
- paletteHints: [
235
- [80, 140, 60, 255], // base green
236
- [60, 120, 45, 255], // dark green
237
- [110, 170, 80, 255], // light green
238
- [90, 130, 55, 255], // mid green
239
- [130, 100, 65, 255], // earth
240
- [70, 110, 40, 255], // shadow green
241
- [150, 190, 100, 255], // highlight
242
- [100, 85, 50, 255], // dark earth
243
- ],
244
- preferredShapes: { ellipse: 3, circle: 2, heart: 0.5 },
245
- itemType: 'floor',
246
- layers: {
247
- base: {
248
- generator: 'noise-field',
249
- shapes: ['circle'],
250
- count: 0,
251
- scaleVariance: 0,
252
- rotationVariance: 0,
253
- colorShift: 10,
254
- jitter: 0,
255
- noiseLevel: 0.25,
256
- detailLevel: 1,
257
- sparsity: 0,
258
- frameJitter: 0,
259
- frameRotation: 0,
260
- frameScale: 0,
261
- },
262
- blades: {
263
- generator: 'shape',
264
- shapes: ['ellipse', 'heart'],
265
- count: 6,
266
- scaleVariance: 0.35,
267
- rotationVariance: 40,
268
- colorShift: 15,
269
- jitter: 0.1,
270
- noiseLevel: 0.12,
271
- detailLevel: 1.0,
272
- sparsity: 0.25,
273
- frameJitter: 0.01,
274
- frameRotation: 2.0,
275
- frameScale: 0.005,
276
- },
277
- patches: {
278
- generator: 'shape',
279
- shapes: ['circle', 'ellipse'],
280
- count: 3,
281
- scaleVariance: 0.3,
282
- rotationVariance: 30,
283
- colorShift: 12,
284
- jitter: 0.08,
285
- noiseLevel: 0.15,
286
- detailLevel: 0.9,
287
- sparsity: 0.4,
288
- frameJitter: 0.003,
289
- frameRotation: 0.5,
290
- frameScale: 0.003,
291
- },
292
- },
293
- });
294
-
295
- registerSemantic('floor-water', {
296
- semanticTags: ['water', 'ocean', 'wave', 'liquid'],
297
- paletteHints: [
298
- [40, 100, 180, 255], // deep blue
299
- [60, 130, 200, 255], // mid blue
300
- [90, 160, 220, 255], // light blue
301
- [120, 190, 230, 255], // highlight
302
- [30, 80, 150, 255], // dark blue
303
- [70, 140, 210, 255], // wave crest
304
- [150, 210, 240, 255], // foam
305
- [20, 60, 120, 255], // abyss
306
- ],
307
- preferredShapes: { ellipse: 3, circle: 2 },
308
- itemType: 'floor',
309
- layers: {
310
- base: {
311
- generator: 'noise-field',
312
- shapes: ['ellipse'],
313
- count: 0,
314
- scaleVariance: 0,
315
- rotationVariance: 0,
316
- colorShift: 12,
317
- jitter: 0,
318
- noiseLevel: 0.35,
319
- detailLevel: 1,
320
- sparsity: 0,
321
- frameJitter: 0,
322
- frameRotation: 0,
323
- frameScale: 0,
324
- },
325
- waves: {
326
- generator: 'shape',
327
- shapes: ['ellipse'],
328
- count: 4,
329
- scaleVariance: 0.3,
330
- rotationVariance: 10,
331
- colorShift: 18,
332
- jitter: 0.06,
333
- noiseLevel: 0.2,
334
- detailLevel: 1.2,
335
- sparsity: 0.2,
336
- frameJitter: 0.015,
337
- frameRotation: 1.5,
338
- frameScale: 0.008,
339
- },
340
- foam: {
341
- generator: 'shape',
342
- shapes: ['circle'],
343
- count: 3,
344
- scaleVariance: 0.5,
345
- rotationVariance: 0,
346
- colorShift: 10,
347
- jitter: 0.15,
348
- noiseLevel: 0.1,
349
- detailLevel: 0.7,
350
- sparsity: 0.5,
351
- frameJitter: 0.012,
352
- frameRotation: 0.5,
353
- frameScale: 0.01,
354
- },
355
- },
356
- });
357
-
358
- registerSemantic('floor-stone', {
359
- semanticTags: ['stone', 'rock', 'cobble', 'grey'],
360
- paletteHints: [
361
- [140, 140, 145, 255], // base grey
362
- [120, 118, 125, 255], // dark grey
363
- [165, 165, 170, 255], // light grey
364
- [100, 98, 105, 255], // shadow
365
- [180, 180, 185, 255], // highlight
366
- [90, 85, 80, 255], // dark rock
367
- [155, 150, 148, 255], // warm grey
368
- [110, 108, 115, 255], // cool grey
369
- ],
370
- preferredShapes: { circle: 3, ellipse: 2, star: 1, 'pixel-art': 0.5 },
371
- itemType: 'floor',
372
- layers: {
373
- base: {
374
- generator: 'noise-field',
375
- shapes: ['circle'],
376
- count: 0,
377
- scaleVariance: 0,
378
- rotationVariance: 0,
379
- colorShift: 6,
380
- jitter: 0,
381
- noiseLevel: 0.2,
382
- detailLevel: 1,
383
- sparsity: 0,
384
- frameJitter: 0,
385
- frameRotation: 0,
386
- frameScale: 0,
387
- },
388
- cobbles: {
389
- generator: 'shape',
390
- shapes: ['circle', 'ellipse', 'pixel-art'],
391
- count: 6,
392
- scaleVariance: 0.35,
393
- rotationVariance: 90,
394
- colorShift: 10,
395
- jitter: 0.06,
396
- noiseLevel: 0.15,
397
- detailLevel: 1.0,
398
- sparsity: 0.2,
399
- frameJitter: 0.001,
400
- frameRotation: 0.2,
401
- frameScale: 0.001,
402
- },
403
- cracks: {
404
- generator: 'shape',
405
- shapes: ['star'],
406
- count: 2,
407
- scaleVariance: 0.5,
408
- rotationVariance: 180,
409
- colorShift: 20,
410
- jitter: 0.1,
411
- noiseLevel: 0.25,
412
- detailLevel: 0.8,
413
- sparsity: 0.6,
414
- frameJitter: 0.0,
415
- frameRotation: 0.0,
416
- frameScale: 0.0,
417
- },
418
- },
419
- });
420
-
421
- registerSemantic('floor-lava', {
422
- semanticTags: ['lava', 'magma', 'fire', 'hot'],
423
- paletteHints: [
424
- [200, 50, 20, 255], // hot red
425
- [230, 100, 30, 255], // orange
426
- [255, 180, 50, 255], // bright yellow
427
- [160, 30, 10, 255], // dark red
428
- [80, 20, 10, 255], // cooled rock
429
- [50, 15, 8, 255], // dark crust
430
- [240, 140, 40, 255], // glow
431
- [120, 25, 12, 255], // mid lava
432
- ],
433
- preferredShapes: { circle: 3, ellipse: 2, cactus: 1 },
434
- itemType: 'floor',
435
- layers: {
436
- base: {
437
- generator: 'noise-field',
438
- shapes: ['circle'],
439
- count: 0,
440
- scaleVariance: 0,
441
- rotationVariance: 0,
442
- colorShift: 15,
443
- jitter: 0,
444
- noiseLevel: 0.4,
445
- detailLevel: 1,
446
- sparsity: 0,
447
- frameJitter: 0,
448
- frameRotation: 0,
449
- frameScale: 0,
450
- },
451
- flow: {
452
- generator: 'shape',
453
- shapes: ['ellipse', 'circle'],
454
- count: 5,
455
- scaleVariance: 0.35,
456
- rotationVariance: 25,
457
- colorShift: 20,
458
- jitter: 0.1,
459
- noiseLevel: 0.2,
460
- detailLevel: 1.1,
461
- sparsity: 0.2,
462
- frameJitter: 0.02,
463
- frameRotation: 2.0,
464
- frameScale: 0.01,
465
- },
466
- crust: {
467
- generator: 'shape',
468
- shapes: ['star', 'cactus'],
469
- count: 3,
470
- scaleVariance: 0.4,
471
- rotationVariance: 60,
472
- colorShift: 10,
473
- jitter: 0.08,
474
- noiseLevel: 0.15,
475
- detailLevel: 0.9,
476
- sparsity: 0.4,
477
- frameJitter: 0.005,
478
- frameRotation: 0.5,
479
- frameScale: 0.004,
480
- },
481
- },
482
- });
483
-
484
- registerSemantic('skin-', {
485
- semanticTags: ['character', 'body', 'humanoid'],
486
- paletteHints: [
487
- [180, 140, 110, 255],
488
- [160, 120, 90, 255],
489
- [200, 160, 130, 255],
490
- [140, 100, 70, 255],
491
- [100, 70, 50, 255],
492
- [60, 60, 80, 255],
493
- [80, 80, 100, 255],
494
- [220, 180, 150, 255],
495
- ],
496
- preferredShapes: { 'skull-bone': 2, circle: 2, ellipse: 1 },
497
- itemType: 'skin',
498
- layers: {
499
- body: {
500
- generator: 'shape',
501
- shapes: ['ellipse', 'circle'],
502
- count: 4,
503
- scaleVariance: 0.3,
504
- rotationVariance: 10,
505
- colorShift: 12,
506
- jitter: 0.05,
507
- noiseLevel: 0.1,
508
- detailLevel: 1.2,
509
- sparsity: 0.2,
510
- frameJitter: 0.003,
511
- frameRotation: 0.5,
512
- frameScale: 0.003,
513
- },
514
- detail: {
515
- generator: 'shape',
516
- shapes: ['skull-bone', 'star'],
517
- count: 2,
518
- scaleVariance: 0.2,
519
- rotationVariance: 5,
520
- colorShift: 8,
521
- jitter: 0.03,
522
- noiseLevel: 0.08,
523
- detailLevel: 1.5,
524
- sparsity: 0.5,
525
- frameJitter: 0.002,
526
- frameRotation: 0.3,
527
- frameScale: 0.002,
528
- },
529
- },
530
- });
153
+ /* ── Load category submodules at startup ────────────────────────────────── */
154
+ registerFloorSemantics(registerSemantic);
155
+ registerSkinSemantics(registerSemantic);
531
156
 
532
157
  /* ═══════════════════════════════════════════════════════════════════════════
533
158
  * COLOR UTILITIES
@@ -537,7 +162,7 @@ registerSemantic('skin-', {
537
162
  * Picks a palette color deterministically from the hint palette.
538
163
  * @param {number[][]} palette
539
164
  * @param {function():number} rng
540
- * @param {number} colorShift - Max channel deviation.
165
+ * @param {number} colorShift
541
166
  * @returns {number[]} RGBA array.
542
167
  * @memberof SemanticLayerGenerator
543
168
  */
@@ -569,7 +194,6 @@ function clamp(v, min, max) {
569
194
 
570
195
  /**
571
196
  * Picks a shape key from candidate list, weighted by the descriptor's preferredShapes.
572
- * Falls back to uniform if no weights match.
573
197
  * @param {string[]} candidates
574
198
  * @param {Object<string,number>} weights
575
199
  * @param {function():number} rng
@@ -577,13 +201,9 @@ function clamp(v, min, max) {
577
201
  * @memberof SemanticLayerGenerator
578
202
  */
579
203
  function pickShape(candidates, weights, rng) {
580
- // Filter to only shapes that actually exist in the shape registry
581
204
  const available = listShapes();
582
205
  const valid = candidates.filter((c) => available.includes(c));
583
- if (valid.length === 0) {
584
- // fallback to any available shape
585
- return available[Math.floor(rng() * available.length)];
586
- }
206
+ if (valid.length === 0) return available[Math.floor(rng() * available.length)];
587
207
  const w = valid.map((k) => weights[k] ?? 1);
588
208
  const total = w.reduce((s, v) => s + v, 0);
589
209
  let r = rng() * total;
@@ -596,46 +216,33 @@ function pickShape(candidates, weights, rng) {
596
216
 
597
217
  /* ═══════════════════════════════════════════════════════════════════════════
598
218
  * FRAME MATRIX GENERATION
599
- * Converts generated shape layers into a 24×24 frame_matrix compatible
600
- * with the ObjectLayerEngine frame format.
601
219
  * ═══════════════════════════════════════════════════════════════════════════ */
602
220
 
603
- const GRID_DIM = 24; // standard object layer grid (24 rows × 24 cols)
221
+ const GRID_DIM = 24;
604
222
 
605
223
  /**
606
224
  * Generates a noise-field base layer as a 24×24 frame matrix.
607
- * Uses low-frequency 2D noise seeded per-frame for temporal coherence.
608
- * @param {number[][]} palette
609
- * @param {number} layerSeedInt
610
- * @param {number} frameIndex
611
- * @param {LayerSpec} spec
612
- * @param {number} density
613
- * @returns {{frameMatrix: number[][], colors: number[][]}}
614
225
  * @memberof SemanticLayerGenerator
615
226
  */
616
227
  function generateNoiseFieldLayer(palette, layerSeedInt, frameIndex, spec, density) {
617
228
  const frameSeedInt = deriveFrameSeed(layerSeedInt, frameIndex);
618
229
  const rng = createRng(frameSeedInt);
619
- const noise = createNoise2D(createRng(layerSeedInt)); // topology from layerSeed, not frameSeed
230
+ const noise = createNoise2D(createRng(layerSeedInt));
620
231
 
621
232
  const colors = [];
622
233
  const frameMatrix = [];
623
234
 
624
- // Pre-pick a small set of colors for the base
625
235
  const baseColors = [];
626
- const colorRng = createRng(layerSeedInt + 7); // stable color selection
236
+ const colorRng = createRng(layerSeedInt + 7);
627
237
  for (let i = 0; i < Math.min(palette.length, 4); i++) {
628
238
  baseColors.push(pickColor(palette, colorRng, spec.colorShift));
629
239
  }
630
-
631
- // Register base colors
632
240
  const colorIndices = baseColors.map((c) => {
633
241
  colors.push(c);
634
242
  return colors.length - 1;
635
243
  });
636
244
 
637
245
  const noiseFreq = 0.15 + spec.noiseLevel * 0.3;
638
- // Frame-level noise offset for smooth temporal variation
639
246
  const frameOffsetX = frameIndex * 0.05;
640
247
  const frameOffsetY = frameIndex * 0.03;
641
248
 
@@ -643,8 +250,7 @@ function generateNoiseFieldLayer(palette, layerSeedInt, frameIndex, spec, densit
643
250
  const row = [];
644
251
  for (let x = 0; x < GRID_DIM; x++) {
645
252
  const n = noise((x + frameOffsetX) * noiseFreq, (y + frameOffsetY) * noiseFreq);
646
- // Map noise [-1,1] color index
647
- const normalized = (n + 1) / 2; // 0..1
253
+ const normalized = (n + 1) / 2;
648
254
  const idx = Math.min(colorIndices.length - 1, Math.floor(normalized * colorIndices.length));
649
255
  row.push(colorIndices[idx]);
650
256
  }
@@ -656,18 +262,9 @@ function generateNoiseFieldLayer(palette, layerSeedInt, frameIndex, spec, densit
656
262
 
657
263
  /**
658
264
  * Stamps a parametric shape onto a frame matrix at a given position/scale.
659
- * @param {number[][]} frameMatrix - Mutable 24×24 matrix.
660
- * @param {number[][]} colors - Mutable color palette.
661
- * @param {string} shapeKey
662
- * @param {Object} transform - { x, y, scale, rotation }
663
- * @param {number[]} color - RGBA color for this stamp.
664
- * @param {number} noiseLevel
665
- * @param {number} detailLevel
666
- * @param {string} seed - String seed for shape generation.
667
265
  * @memberof SemanticLayerGenerator
668
266
  */
669
267
  function stampShape(frameMatrix, colors, shapeKey, transform, color, noiseLevel, detailLevel, seed) {
670
- // Generate shape using intCoords for pixel-level placement
671
268
  const gridSize = Math.max(4, Math.round(GRID_DIM * transform.scale));
672
269
  const result = generateShape(shapeKey, {
673
270
  intCoords: [gridSize, gridSize],
@@ -677,7 +274,6 @@ function stampShape(frameMatrix, colors, shapeKey, transform, color, noiseLevel,
677
274
  count: Math.round(80 * detailLevel),
678
275
  });
679
276
 
680
- // Register color
681
277
  const existingIdx = colors.findIndex(
682
278
  (c) => c[0] === color[0] && c[1] === color[1] && c[2] === color[2] && c[3] === color[3],
683
279
  );
@@ -689,7 +285,6 @@ function stampShape(frameMatrix, colors, shapeKey, transform, color, noiseLevel,
689
285
  colorIdx = colors.length - 1;
690
286
  }
691
287
 
692
- // Compute offset to center the shape at (transform.x, transform.y)
693
288
  const offsetX = Math.round(transform.x * GRID_DIM - gridSize / 2);
694
289
  const offsetY = Math.round(transform.y * GRID_DIM - gridSize / 2);
695
290
 
@@ -708,40 +303,40 @@ function stampShape(frameMatrix, colors, shapeKey, transform, color, noiseLevel,
708
303
 
709
304
  /**
710
305
  * @typedef {Object} GenerateLayerOptions
711
- * @property {string} itemId - The item identifier (e.g. 'floor-desert').
712
- * @property {string} seed - Master seed string (e.g. 'fx-42').
713
- * @property {number} frameIndex - Current frame index (0-based).
714
- * @property {number} [count=3] - Number of shape elements per layer (multiplier).
715
- * @property {number} [density=0.5]- Overall density factor (0..1).
306
+ * @property {string} itemId
307
+ * @property {string} seed
308
+ * @property {number} [frameIndex=0]
309
+ * @property {number} [count=3]
310
+ * @property {number} [density=0.5]
716
311
  * @memberof SemanticLayerGenerator
717
312
  */
718
313
 
719
314
  /**
720
315
  * @typedef {Object} GeneratedLayer
721
- * @property {string} layerId - Composite id: <itemId>-<layerKey>.
722
- * @property {string} layerKey - The layer name (e.g. 'dunes', 'rocks').
723
- * @property {Object[]} keys - Content entries placed in this layer.
724
- * @property {number[][]} frameMatrix - 24×24 color-index matrix.
725
- * @property {number[][]} colors - Shared color palette.
316
+ * @property {string} layerId
317
+ * @property {string} layerKey
318
+ * @property {Object[]} keys
319
+ * @property {number[][]} frameMatrix
320
+ * @property {number[][]} colors
726
321
  * @memberof SemanticLayerGenerator
727
322
  */
728
323
 
729
324
  /**
730
325
  * @typedef {Object} GenerationResult
731
- * @property {string} itemId
732
- * @property {string} seed
733
- * @property {number} frameIndex
326
+ * @property {string} itemId
327
+ * @property {string} seed
328
+ * @property {number} frameIndex
734
329
  * @property {GeneratedLayer[]} layers
735
- * @property {number[][]} compositeFrameMatrix - Final composited 24×24 matrix.
736
- * @property {number[][]} compositeColors - Final color palette.
330
+ * @property {number[][]} compositeFrameMatrix
331
+ * @property {number[][]} compositeColors
737
332
  * @memberof SemanticLayerGenerator
738
333
  */
739
334
 
740
335
  /**
741
336
  * Generates all semantic layers for a single frame of an item.
742
337
  *
743
- * Shape topology stays fixed across adjacent frames (determined by layerSeed);
744
- * only smooth per-frame transforms vary (derived from frameSeed + low-frequency noise).
338
+ * If the descriptor provides `customMultiFrameGenerator`, a stub result is
339
+ * returned use `generateMultiFrame` for those descriptors.
745
340
  *
746
341
  * @param {GenerateLayerOptions} options
747
342
  * @returns {GenerationResult}
@@ -758,13 +353,23 @@ export function generateFrame(options) {
758
353
  );
759
354
  }
760
355
 
356
+ // Descriptors with a custom multi-frame generator: return a transparent stub.
357
+ if (typeof descriptor.customMultiFrameGenerator === 'function') {
358
+ return {
359
+ itemId,
360
+ seed,
361
+ frameIndex,
362
+ layers: [],
363
+ compositeFrameMatrix: Array.from({ length: GRID_DIM }, () => Array(GRID_DIM).fill(0)),
364
+ compositeColors: [[0, 0, 0, 0]],
365
+ };
366
+ }
367
+
761
368
  const { paletteHints, preferredShapes, layers: layerSpecs } = descriptor;
762
369
  const layerKeys = Object.keys(layerSpecs);
763
370
 
764
- // Composite frame matrix (start transparent — color index 0 will be transparent)
765
- const compositeColors = [[0, 0, 0, 0]]; // index 0 = transparent
371
+ const compositeColors = [[0, 0, 0, 0]];
766
372
  const compositeMatrix = Array.from({ length: GRID_DIM }, () => Array(GRID_DIM).fill(0));
767
-
768
373
  const generatedLayers = [];
769
374
 
770
375
  for (const layerKey of layerKeys) {
@@ -773,7 +378,6 @@ export function generateFrame(options) {
773
378
  const layerId = `${itemId}-${layerKey}`;
774
379
 
775
380
  if (spec.generator === 'noise-field') {
776
- // ── Noise-field base layer ──
777
381
  const { frameMatrix, colors: layerColors } = generateNoiseFieldLayer(
778
382
  paletteHints,
779
383
  layerSeedInt,
@@ -782,7 +386,6 @@ export function generateFrame(options) {
782
386
  density,
783
387
  );
784
388
 
785
- // Merge colors into composite palette
786
389
  const colorMap = {};
787
390
  for (let ci = 0; ci < layerColors.length; ci++) {
788
391
  const c = layerColors[ci];
@@ -796,13 +399,10 @@ export function generateFrame(options) {
796
399
  colorMap[ci] = found;
797
400
  }
798
401
 
799
- // Stamp onto composite
800
402
  for (let y = 0; y < GRID_DIM; y++) {
801
403
  for (let x = 0; x < GRID_DIM; x++) {
802
404
  const mapped = colorMap[frameMatrix[y][x]];
803
- if (mapped !== undefined && mapped !== 0) {
804
- compositeMatrix[y][x] = mapped;
805
- }
405
+ if (mapped !== undefined && mapped !== 0) compositeMatrix[y][x] = mapped;
806
406
  }
807
407
  }
808
408
 
@@ -814,7 +414,6 @@ export function generateFrame(options) {
814
414
  colors: layerColors,
815
415
  });
816
416
  } else if (spec.generator === 'shape') {
817
- // ── Shape element layer ──
818
417
  const layerRng = createRng(layerSeedInt);
819
418
  const frameSeedInt = deriveFrameSeed(layerSeedInt, frameIndex);
820
419
  const frameRng = createRng(frameSeedInt);
@@ -822,31 +421,24 @@ export function generateFrame(options) {
822
421
 
823
422
  const effectiveCount = Math.max(1, Math.round(spec.count * count * density));
824
423
  const layerEntries = [];
825
-
826
- // Frame matrix for this layer alone (for metadata)
827
424
  const layerMatrix = Array.from({ length: GRID_DIM }, () => Array(GRID_DIM).fill(-1));
828
425
  const layerColors = [];
829
426
 
830
427
  for (let ei = 0; ei < effectiveCount; ei++) {
831
- // Sparsity check — deterministic per element
832
428
  if (layerRng() < spec.sparsity) continue;
833
429
 
834
- // ── Shape selection (stable across frames) ──
835
430
  const shapeKey = pickShape(spec.shapes, preferredShapes, createRng(layerSeedInt + ei * 97));
836
431
 
837
- // ── Base transform (stable across frames — from layerSeed) ──
838
432
  const baseRng = createRng(layerSeedInt + ei * 31 + 17);
839
433
  const baseX = baseRng();
840
434
  const baseY = baseRng();
841
435
  const baseScale = 0.15 + baseRng() * 0.25 + (baseRng() * 2 - 1) * spec.scaleVariance * 0.15;
842
436
  const baseRotation = (baseRng() * 2 - 1) * spec.rotationVariance;
843
437
 
844
- // ── Per-frame smooth perturbation (temporal coherence) ──
845
438
  const fj = spec.frameJitter || 0;
846
439
  const fr = spec.frameRotation || 0;
847
440
  const fs = spec.frameScale || 0;
848
441
 
849
- // Low-frequency noise indexed by element position for smooth frame-to-frame change
850
442
  const noiseX = frameNoise(ei * 2.5, frameIndex * 0.1) * fj;
851
443
  const noiseY = frameNoise(ei * 2.5 + 100, frameIndex * 0.1) * fj;
852
444
  const noiseRot = frameNoise(ei * 2.5 + 200, frameIndex * 0.1) * fr;
@@ -859,48 +451,23 @@ export function generateFrame(options) {
859
451
  rotation: baseRotation + noiseRot,
860
452
  };
861
453
 
862
- // ── Color (stable per element, small per-frame shift) ──
863
454
  const colorRng = createRng(layerSeedInt + ei * 53 + 7);
864
455
  const baseColor = pickColor(paletteHints, colorRng, spec.colorShift);
865
-
866
- // Small per-frame color shift for shimmer
867
456
  const frameColorShift = Math.round(frameNoise(ei * 3.7 + 400, frameIndex * 0.08) * 3);
868
457
  const color = baseColor.map((ch, ci) => (ci < 3 ? clamp(ch + frameColorShift, 0, 255) : ch));
869
458
 
870
- // ── Shape seed (stable topology) ──
871
459
  const shapeSeed = `${seed}:${itemId}:${layerKey}:${ei}`;
872
460
 
873
- // Stamp onto composite matrix
874
461
  stampShape(
875
- compositeMatrix,
876
- compositeColors,
877
- shapeKey,
878
- transform,
879
- color,
880
- spec.noiseLevel * (spec.jitter + 0.5),
881
- spec.detailLevel,
882
- shapeSeed,
462
+ compositeMatrix, compositeColors, shapeKey, transform, color,
463
+ spec.noiseLevel * (spec.jitter + 0.5), spec.detailLevel, shapeSeed,
883
464
  );
884
-
885
- // Also stamp onto layer-local matrix for metadata
886
465
  stampShape(
887
- layerMatrix,
888
- layerColors,
889
- shapeKey,
890
- transform,
891
- color,
892
- spec.noiseLevel * 0.5,
893
- spec.detailLevel,
894
- shapeSeed,
466
+ layerMatrix, layerColors, shapeKey, transform, color,
467
+ spec.noiseLevel * 0.5, spec.detailLevel, shapeSeed,
895
468
  );
896
469
 
897
- layerEntries.push({
898
- type: 'shape',
899
- shapeKey,
900
- transform,
901
- color,
902
- shapeSeed,
903
- });
470
+ layerEntries.push({ type: 'shape', shapeKey, transform, color, shapeSeed });
904
471
  }
905
472
 
906
473
  generatedLayers.push({
@@ -911,6 +478,7 @@ export function generateFrame(options) {
911
478
  colors: layerColors,
912
479
  });
913
480
  }
481
+ // 'template-zone' handled by customMultiFrameGenerator — skip here
914
482
  }
915
483
 
916
484
  return {
@@ -925,32 +493,34 @@ export function generateFrame(options) {
925
493
 
926
494
  /* ═══════════════════════════════════════════════════════════════════════════
927
495
  * MULTI-FRAME GENERATION
928
- * Generates multiple consecutive frames with temporal coherence.
929
496
  * ═══════════════════════════════════════════════════════════════════════════ */
930
497
 
931
498
  /**
932
499
  * @typedef {Object} MultiFrameResult
933
- * @property {string} itemId
934
- * @property {string} seed
935
- * @property {number} frameCount
500
+ * @property {string} itemId
501
+ * @property {string} seed
502
+ * @property {number} frameCount
936
503
  * @property {GenerationResult[]} frames
937
- * @property {Object} objectLayerRenderFramesData - Ready for ObjectLayerEngine.createObjectLayerDocuments.
938
- * @property {Object} objectLayerData - Ready for ObjectLayerEngine.createObjectLayerDocuments.
504
+ * @property {Object} objectLayerRenderFramesData
505
+ * @property {Object} objectLayerData
939
506
  * @memberof SemanticLayerGenerator
940
507
  */
941
508
 
942
509
  /**
943
- * Generates N consecutive frames for an item-id, producing data structures
944
- * compatible with ObjectLayerEngine document creation.
510
+ * Generates N consecutive frames for an item-id.
511
+ *
512
+ * If the matched descriptor provides `customMultiFrameGenerator`, it is invoked
513
+ * directly and its result is returned unchanged — allowing category submodules
514
+ * (e.g. skin) to fully control frame layout and direction assignment.
945
515
  *
946
- * @param {Object} options
516
+ * @param {Object} options
947
517
  * @param {string} options.itemId
948
518
  * @param {string} options.seed
949
- * @param {number} [options.frameCount=1] - Number of frames to generate.
950
- * @param {number} [options.startFrame=0] - Starting frame index.
951
- * @param {number} [options.count=3] - Shape element count multiplier.
952
- * @param {number} [options.density=0.5] - Density factor.
953
- * @param {number} [options.frameDuration=250] - ms per frame.
519
+ * @param {number} [options.frameCount=1]
520
+ * @param {number} [options.startFrame=0]
521
+ * @param {number} [options.count=3]
522
+ * @param {number} [options.density=0.5]
523
+ * @param {number} [options.frameDuration=250]
954
524
  * @returns {MultiFrameResult}
955
525
  * @memberof SemanticLayerGenerator
956
526
  */
@@ -965,22 +535,17 @@ export function generateMultiFrame(options) {
965
535
  );
966
536
  }
967
537
 
968
- const frames = [];
969
- let mergedColors = [[0, 0, 0, 0]]; // index 0 = transparent
538
+ // ── Dispatch to custom generator ─────────────────────────────────────────
539
+ if (typeof descriptor.customMultiFrameGenerator === 'function') {
540
+ return descriptor.customMultiFrameGenerator(options, descriptor);
541
+ }
970
542
 
971
- // First pass: generate all frames
543
+ // ── Default shape / noise-field pipeline ─────────────────────────────────
544
+ const frames = [];
972
545
  for (let fi = 0; fi < frameCount; fi++) {
973
- const result = generateFrame({
974
- itemId,
975
- seed,
976
- frameIndex: startFrame + fi,
977
- count,
978
- density,
979
- });
980
- frames.push(result);
546
+ frames.push(generateFrame({ itemId, seed, frameIndex: startFrame + fi, count, density }));
981
547
  }
982
548
 
983
- // Second pass: unify color palettes across all frames
984
549
  const globalColors = [[0, 0, 0, 0]];
985
550
  const frameMappings = [];
986
551
 
@@ -988,7 +553,9 @@ export function generateMultiFrame(options) {
988
553
  const mapping = {};
989
554
  for (let ci = 0; ci < frame.compositeColors.length; ci++) {
990
555
  const c = frame.compositeColors[ci];
991
- let found = globalColors.findIndex((gc) => gc[0] === c[0] && gc[1] === c[1] && gc[2] === c[2] && gc[3] === c[3]);
556
+ let found = globalColors.findIndex(
557
+ (gc) => gc[0] === c[0] && gc[1] === c[1] && gc[2] === c[2] && gc[3] === c[3],
558
+ );
992
559
  if (found < 0) {
993
560
  globalColors.push([...c]);
994
561
  found = globalColors.length - 1;
@@ -998,14 +565,11 @@ export function generateMultiFrame(options) {
998
565
  frameMappings.push(mapping);
999
566
  }
1000
567
 
1001
- // Third pass: remap frame matrices to global palette
1002
568
  const remappedFrames = frames.map((frame, fi) => {
1003
569
  const mapping = frameMappings[fi];
1004
570
  return frame.compositeFrameMatrix.map((row) => row.map((ci) => (mapping[ci] !== undefined ? mapping[ci] : 0)));
1005
571
  });
1006
572
 
1007
- // Build objectLayerRenderFramesData structure
1008
- // For floor types: use direction '08' (down_idle / default_idle / none_idle)
1009
573
  const objectLayerRenderFramesData = {
1010
574
  frame_duration: frameDuration,
1011
575
  is_stateless: descriptor.itemType === 'floor',
@@ -1013,7 +577,6 @@ export function generateMultiFrame(options) {
1013
577
  colors: globalColors,
1014
578
  };
1015
579
 
1016
- // Assign frames to directions
1017
580
  const directionMappings = {
1018
581
  floor: [['down_idle', 'none_idle', 'default_idle']],
1019
582
  skin: [
@@ -1028,14 +591,12 @@ export function generateMultiFrame(options) {
1028
591
  };
1029
592
 
1030
593
  const dirGroups = directionMappings[descriptor.itemType] || directionMappings.floor;
1031
-
1032
594
  for (const dirGroup of dirGroups) {
1033
595
  for (const dirName of dirGroup) {
1034
596
  objectLayerRenderFramesData.frames[dirName] = [...remappedFrames];
1035
597
  }
1036
598
  }
1037
599
 
1038
- // Build objectLayerData
1039
600
  const objectLayerData = {
1040
601
  data: {
1041
602
  item: {