cyberia 3.0.1 → 3.0.2

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 (49) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +1 -0
  2. package/CHANGELOG.md +56 -1
  3. package/CLI-HELP.md +2 -4
  4. package/README.md +139 -0
  5. package/bin/build.js +5 -0
  6. package/bin/cyberia.js +385 -71
  7. package/bin/deploy.js +18 -26
  8. package/bin/file.js +3 -0
  9. package/bin/index.js +385 -71
  10. package/conf.js +32 -3
  11. package/deployment.yaml +2 -2
  12. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  13. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  14. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  15. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  16. package/manifests/ipfs/configmap.yaml +7 -0
  17. package/package.json +8 -8
  18. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +2 -0
  19. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -0
  20. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +93 -2
  21. package/src/api/file/file.controller.js +3 -13
  22. package/src/api/file/file.ref.json +0 -21
  23. package/src/api/ipfs/ipfs.controller.js +104 -0
  24. package/src/api/ipfs/ipfs.model.js +71 -0
  25. package/src/api/ipfs/ipfs.router.js +31 -0
  26. package/src/api/ipfs/ipfs.service.js +193 -0
  27. package/src/api/object-layer/README.md +139 -0
  28. package/src/api/object-layer/object-layer.controller.js +3 -0
  29. package/src/api/object-layer/object-layer.model.js +15 -1
  30. package/src/api/object-layer/object-layer.router.js +6 -10
  31. package/src/api/object-layer/object-layer.service.js +311 -182
  32. package/src/cli/cluster.js +30 -38
  33. package/src/cli/index.js +0 -1
  34. package/src/cli/run.js +14 -0
  35. package/src/client/components/core/LoadingAnimation.js +2 -3
  36. package/src/client/components/core/Modal.js +1 -1
  37. package/src/client/components/cyberia/ObjectLayerEngineModal.js +4 -5
  38. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +280 -29
  39. package/src/client/services/ipfs/ipfs.service.js +144 -0
  40. package/src/client/services/object-layer/object-layer.management.js +161 -8
  41. package/src/index.js +1 -1
  42. package/src/runtime/express/Express.js +1 -1
  43. package/src/server/auth.js +18 -18
  44. package/src/server/ipfs-client.js +433 -0
  45. package/src/server/object-layer.js +649 -18
  46. package/src/server/semantic-layer-generator.js +1083 -0
  47. package/src/server/shape-generator.js +952 -0
  48. package/test/shape-generator.test.js +457 -0
  49. package/bin/ssl.js +0 -63
@@ -0,0 +1,1083 @@
1
+ /**
2
+ * Semantic Layer Generator for Cyberia Online.
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.
10
+ *
11
+ * @module src/server/semantic-layer-generator.js
12
+ * @namespace SemanticLayerGenerator
13
+ */
14
+
15
+ import crypto from 'crypto';
16
+
17
+ import { createRng, seedToInt, createNoise2D, generateShape, listShapes } from './shape-generator.js';
18
+ import { loggerFactory } from './logger.js';
19
+
20
+ const logger = loggerFactory(import.meta);
21
+
22
+ /* ═══════════════════════════════════════════════════════════════════════════
23
+ * DETERMINISTIC HASHING
24
+ * ═══════════════════════════════════════════════════════════════════════════ */
25
+
26
+ /**
27
+ * Produces a deterministic 32-bit integer hash from an arbitrary string.
28
+ * Used to derive per-layer and per-frame seeds.
29
+ * @param {string} str
30
+ * @returns {number}
31
+ * @memberof SemanticLayerGenerator
32
+ */
33
+ /**
34
+ * 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
+ * @param {string} seed
38
+ * @returns {string} A valid UUID v4 string.
39
+ * @memberof SemanticLayerGenerator
40
+ */
41
+ function seedToUUIDv4(seed) {
42
+ const hash = crypto.createHash('sha256').update(seed).digest();
43
+ // Set version nibble (byte 6, high nibble) to 0100 (version 4)
44
+ hash[6] = (hash[6] & 0x0f) | 0x40;
45
+ // Set variant bits (byte 8, high 2 bits) to 10xx
46
+ hash[8] = (hash[8] & 0x3f) | 0x80;
47
+ const hex = hash.toString('hex');
48
+ return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20, 32)].join('-');
49
+ }
50
+
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
+ /**
60
+ * Derives a per-layer seed: layerSeed = hash(seed + ':' + itemId + ':' + layerKey)
61
+ * @param {string} seed
62
+ * @param {string} itemId
63
+ * @param {string} layerKey
64
+ * @returns {number}
65
+ * @memberof SemanticLayerGenerator
66
+ */
67
+ function deriveLayerSeed(seed, itemId, layerKey) {
68
+ return hashString(`${seed}:${itemId}:${layerKey}`);
69
+ }
70
+
71
+ /**
72
+ * Derives a per-frame seed: frameSeed = hash(layerSeed + ':' + frameIndex)
73
+ * @param {number} layerSeed
74
+ * @param {number} frameIndex
75
+ * @returns {number}
76
+ * @memberof SemanticLayerGenerator
77
+ */
78
+ function deriveFrameSeed(layerSeed, frameIndex) {
79
+ return hashString(`${layerSeed}:${frameIndex}`);
80
+ }
81
+
82
+ /* ═══════════════════════════════════════════════════════════════════════════
83
+ * SEMANTIC REGISTRY
84
+ * Maps item-id prefixes to semantic descriptors that drive generation.
85
+ * ═══════════════════════════════════════════════════════════════════════════ */
86
+
87
+ /**
88
+ * @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…).
94
+ * @memberof SemanticLayerGenerator
95
+ */
96
+
97
+ /**
98
+ * @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.
112
+ * @memberof SemanticLayerGenerator
113
+ */
114
+
115
+ /**
116
+ * Global semantic registry keyed by item-id prefix.
117
+ * @type {Object<string, SemanticDescriptor>}
118
+ */
119
+ const semanticRegistry = {};
120
+
121
+ /**
122
+ * Registers a semantic descriptor for an item-id prefix.
123
+ * @param {string} prefix
124
+ * @param {SemanticDescriptor} descriptor
125
+ * @memberof SemanticLayerGenerator
126
+ */
127
+ export function registerSemantic(prefix, descriptor) {
128
+ semanticRegistry[prefix] = descriptor;
129
+ }
130
+
131
+ /**
132
+ * Looks up the best-matching semantic descriptor for an item-id.
133
+ * Tries exact match first, then longest prefix match.
134
+ * @param {string} itemId
135
+ * @returns {SemanticDescriptor|null}
136
+ * @memberof SemanticLayerGenerator
137
+ */
138
+ export function lookupSemantic(itemId) {
139
+ if (semanticRegistry[itemId]) return semanticRegistry[itemId];
140
+ // Longest prefix match
141
+ let best = null;
142
+ let bestLen = 0;
143
+ for (const prefix of Object.keys(semanticRegistry)) {
144
+ if (itemId.startsWith(prefix) && prefix.length > bestLen) {
145
+ best = semanticRegistry[prefix];
146
+ bestLen = prefix.length;
147
+ }
148
+ }
149
+ return best;
150
+ }
151
+
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
+ });
531
+
532
+ /* ═══════════════════════════════════════════════════════════════════════════
533
+ * COLOR UTILITIES
534
+ * ═══════════════════════════════════════════════════════════════════════════ */
535
+
536
+ /**
537
+ * Picks a palette color deterministically from the hint palette.
538
+ * @param {number[][]} palette
539
+ * @param {function():number} rng
540
+ * @param {number} colorShift - Max channel deviation.
541
+ * @returns {number[]} RGBA array.
542
+ * @memberof SemanticLayerGenerator
543
+ */
544
+ function pickColor(palette, rng, colorShift = 0) {
545
+ const idx = Math.floor(rng() * palette.length);
546
+ const base = [...palette[idx]];
547
+ if (colorShift > 0) {
548
+ for (let i = 0; i < 3; i++) {
549
+ base[i] = Math.max(0, Math.min(255, base[i] + Math.round((rng() * 2 - 1) * colorShift)));
550
+ }
551
+ }
552
+ return base;
553
+ }
554
+
555
+ /**
556
+ * Clamp an integer value between min and max inclusive.
557
+ * @param {number} v
558
+ * @param {number} min
559
+ * @param {number} max
560
+ * @returns {number}
561
+ */
562
+ function clamp(v, min, max) {
563
+ return v < min ? min : v > max ? max : v;
564
+ }
565
+
566
+ /* ═══════════════════════════════════════════════════════════════════════════
567
+ * WEIGHTED SHAPE SELECTION
568
+ * ═══════════════════════════════════════════════════════════════════════════ */
569
+
570
+ /**
571
+ * Picks a shape key from candidate list, weighted by the descriptor's preferredShapes.
572
+ * Falls back to uniform if no weights match.
573
+ * @param {string[]} candidates
574
+ * @param {Object<string,number>} weights
575
+ * @param {function():number} rng
576
+ * @returns {string}
577
+ * @memberof SemanticLayerGenerator
578
+ */
579
+ function pickShape(candidates, weights, rng) {
580
+ // Filter to only shapes that actually exist in the shape registry
581
+ const available = listShapes();
582
+ 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
+ }
587
+ const w = valid.map((k) => weights[k] ?? 1);
588
+ const total = w.reduce((s, v) => s + v, 0);
589
+ let r = rng() * total;
590
+ for (let i = 0; i < valid.length; i++) {
591
+ r -= w[i];
592
+ if (r <= 0) return valid[i];
593
+ }
594
+ return valid[valid.length - 1];
595
+ }
596
+
597
+ /* ═══════════════════════════════════════════════════════════════════════════
598
+ * FRAME MATRIX GENERATION
599
+ * Converts generated shape layers into a 24×24 frame_matrix compatible
600
+ * with the ObjectLayerEngine frame format.
601
+ * ═══════════════════════════════════════════════════════════════════════════ */
602
+
603
+ const GRID_DIM = 24; // standard object layer grid (24 rows × 24 cols)
604
+
605
+ /**
606
+ * 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
+ * @memberof SemanticLayerGenerator
615
+ */
616
+ function generateNoiseFieldLayer(palette, layerSeedInt, frameIndex, spec, density) {
617
+ const frameSeedInt = deriveFrameSeed(layerSeedInt, frameIndex);
618
+ const rng = createRng(frameSeedInt);
619
+ const noise = createNoise2D(createRng(layerSeedInt)); // topology from layerSeed, not frameSeed
620
+
621
+ const colors = [];
622
+ const frameMatrix = [];
623
+
624
+ // Pre-pick a small set of colors for the base
625
+ const baseColors = [];
626
+ const colorRng = createRng(layerSeedInt + 7); // stable color selection
627
+ for (let i = 0; i < Math.min(palette.length, 4); i++) {
628
+ baseColors.push(pickColor(palette, colorRng, spec.colorShift));
629
+ }
630
+
631
+ // Register base colors
632
+ const colorIndices = baseColors.map((c) => {
633
+ colors.push(c);
634
+ return colors.length - 1;
635
+ });
636
+
637
+ const noiseFreq = 0.15 + spec.noiseLevel * 0.3;
638
+ // Frame-level noise offset for smooth temporal variation
639
+ const frameOffsetX = frameIndex * 0.05;
640
+ const frameOffsetY = frameIndex * 0.03;
641
+
642
+ for (let y = 0; y < GRID_DIM; y++) {
643
+ const row = [];
644
+ for (let x = 0; x < GRID_DIM; x++) {
645
+ const n = noise((x + frameOffsetX) * noiseFreq, (y + frameOffsetY) * noiseFreq);
646
+ // Map noise [-1,1] → color index
647
+ const normalized = (n + 1) / 2; // 0..1
648
+ const idx = Math.min(colorIndices.length - 1, Math.floor(normalized * colorIndices.length));
649
+ row.push(colorIndices[idx]);
650
+ }
651
+ frameMatrix.push(row);
652
+ }
653
+
654
+ return { frameMatrix, colors };
655
+ }
656
+
657
+ /**
658
+ * 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
+ * @memberof SemanticLayerGenerator
668
+ */
669
+ function stampShape(frameMatrix, colors, shapeKey, transform, color, noiseLevel, detailLevel, seed) {
670
+ // Generate shape using intCoords for pixel-level placement
671
+ const gridSize = Math.max(4, Math.round(GRID_DIM * transform.scale));
672
+ const result = generateShape(shapeKey, {
673
+ intCoords: [gridSize, gridSize],
674
+ noise: noiseLevel,
675
+ rotation: transform.rotation,
676
+ seed,
677
+ count: Math.round(80 * detailLevel),
678
+ });
679
+
680
+ // Register color
681
+ const existingIdx = colors.findIndex(
682
+ (c) => c[0] === color[0] && c[1] === color[1] && c[2] === color[2] && c[3] === color[3],
683
+ );
684
+ let colorIdx;
685
+ if (existingIdx >= 0) {
686
+ colorIdx = existingIdx;
687
+ } else {
688
+ colors.push([...color]);
689
+ colorIdx = colors.length - 1;
690
+ }
691
+
692
+ // Compute offset to center the shape at (transform.x, transform.y)
693
+ const offsetX = Math.round(transform.x * GRID_DIM - gridSize / 2);
694
+ const offsetY = Math.round(transform.y * GRID_DIM - gridSize / 2);
695
+
696
+ for (const pt of result.points) {
697
+ const gx = pt.x + offsetX;
698
+ const gy = pt.y + offsetY;
699
+ if (gx >= 0 && gx < GRID_DIM && gy >= 0 && gy < GRID_DIM) {
700
+ frameMatrix[gy][gx] = colorIdx;
701
+ }
702
+ }
703
+ }
704
+
705
+ /* ═══════════════════════════════════════════════════════════════════════════
706
+ * CORE GENERATION PIPELINE
707
+ * ═══════════════════════════════════════════════════════════════════════════ */
708
+
709
+ /**
710
+ * @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).
716
+ * @memberof SemanticLayerGenerator
717
+ */
718
+
719
+ /**
720
+ * @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.
726
+ * @memberof SemanticLayerGenerator
727
+ */
728
+
729
+ /**
730
+ * @typedef {Object} GenerationResult
731
+ * @property {string} itemId
732
+ * @property {string} seed
733
+ * @property {number} frameIndex
734
+ * @property {GeneratedLayer[]} layers
735
+ * @property {number[][]} compositeFrameMatrix - Final composited 24×24 matrix.
736
+ * @property {number[][]} compositeColors - Final color palette.
737
+ * @memberof SemanticLayerGenerator
738
+ */
739
+
740
+ /**
741
+ * Generates all semantic layers for a single frame of an item.
742
+ *
743
+ * Shape topology stays fixed across adjacent frames (determined by layerSeed);
744
+ * only smooth per-frame transforms vary (derived from frameSeed + low-frequency noise).
745
+ *
746
+ * @param {GenerateLayerOptions} options
747
+ * @returns {GenerationResult}
748
+ * @memberof SemanticLayerGenerator
749
+ */
750
+ export function generateFrame(options) {
751
+ const { itemId, seed, frameIndex = 0, count = 3, density = 0.5 } = options;
752
+
753
+ const descriptor = lookupSemantic(itemId);
754
+ if (!descriptor) {
755
+ throw new Error(
756
+ `No semantic descriptor found for item-id "${itemId}". ` +
757
+ `Available prefixes: ${Object.keys(semanticRegistry).join(', ')}`,
758
+ );
759
+ }
760
+
761
+ const { paletteHints, preferredShapes, layers: layerSpecs } = descriptor;
762
+ const layerKeys = Object.keys(layerSpecs);
763
+
764
+ // Composite frame matrix (start transparent — color index 0 will be transparent)
765
+ const compositeColors = [[0, 0, 0, 0]]; // index 0 = transparent
766
+ const compositeMatrix = Array.from({ length: GRID_DIM }, () => Array(GRID_DIM).fill(0));
767
+
768
+ const generatedLayers = [];
769
+
770
+ for (const layerKey of layerKeys) {
771
+ const spec = layerSpecs[layerKey];
772
+ const layerSeedInt = deriveLayerSeed(seed, itemId, layerKey);
773
+ const layerId = `${itemId}-${layerKey}`;
774
+
775
+ if (spec.generator === 'noise-field') {
776
+ // ── Noise-field base layer ──
777
+ const { frameMatrix, colors: layerColors } = generateNoiseFieldLayer(
778
+ paletteHints,
779
+ layerSeedInt,
780
+ frameIndex,
781
+ spec,
782
+ density,
783
+ );
784
+
785
+ // Merge colors into composite palette
786
+ const colorMap = {};
787
+ for (let ci = 0; ci < layerColors.length; ci++) {
788
+ const c = layerColors[ci];
789
+ let found = compositeColors.findIndex(
790
+ (cc) => cc[0] === c[0] && cc[1] === c[1] && cc[2] === c[2] && cc[3] === c[3],
791
+ );
792
+ if (found < 0) {
793
+ compositeColors.push([...c]);
794
+ found = compositeColors.length - 1;
795
+ }
796
+ colorMap[ci] = found;
797
+ }
798
+
799
+ // Stamp onto composite
800
+ for (let y = 0; y < GRID_DIM; y++) {
801
+ for (let x = 0; x < GRID_DIM; x++) {
802
+ const mapped = colorMap[frameMatrix[y][x]];
803
+ if (mapped !== undefined && mapped !== 0) {
804
+ compositeMatrix[y][x] = mapped;
805
+ }
806
+ }
807
+ }
808
+
809
+ generatedLayers.push({
810
+ layerId,
811
+ layerKey,
812
+ keys: [{ type: 'noise-field', noiseLevel: spec.noiseLevel }],
813
+ frameMatrix,
814
+ colors: layerColors,
815
+ });
816
+ } else if (spec.generator === 'shape') {
817
+ // ── Shape element layer ──
818
+ const layerRng = createRng(layerSeedInt);
819
+ const frameSeedInt = deriveFrameSeed(layerSeedInt, frameIndex);
820
+ const frameRng = createRng(frameSeedInt);
821
+ const frameNoise = createNoise2D(createRng(frameSeedInt + 3));
822
+
823
+ const effectiveCount = Math.max(1, Math.round(spec.count * count * density));
824
+ const layerEntries = [];
825
+
826
+ // Frame matrix for this layer alone (for metadata)
827
+ const layerMatrix = Array.from({ length: GRID_DIM }, () => Array(GRID_DIM).fill(-1));
828
+ const layerColors = [];
829
+
830
+ for (let ei = 0; ei < effectiveCount; ei++) {
831
+ // Sparsity check — deterministic per element
832
+ if (layerRng() < spec.sparsity) continue;
833
+
834
+ // ── Shape selection (stable across frames) ──
835
+ const shapeKey = pickShape(spec.shapes, preferredShapes, createRng(layerSeedInt + ei * 97));
836
+
837
+ // ── Base transform (stable across frames — from layerSeed) ──
838
+ const baseRng = createRng(layerSeedInt + ei * 31 + 17);
839
+ const baseX = baseRng();
840
+ const baseY = baseRng();
841
+ const baseScale = 0.15 + baseRng() * 0.25 + (baseRng() * 2 - 1) * spec.scaleVariance * 0.15;
842
+ const baseRotation = (baseRng() * 2 - 1) * spec.rotationVariance;
843
+
844
+ // ── Per-frame smooth perturbation (temporal coherence) ──
845
+ const fj = spec.frameJitter || 0;
846
+ const fr = spec.frameRotation || 0;
847
+ const fs = spec.frameScale || 0;
848
+
849
+ // Low-frequency noise indexed by element position for smooth frame-to-frame change
850
+ const noiseX = frameNoise(ei * 2.5, frameIndex * 0.1) * fj;
851
+ const noiseY = frameNoise(ei * 2.5 + 100, frameIndex * 0.1) * fj;
852
+ const noiseRot = frameNoise(ei * 2.5 + 200, frameIndex * 0.1) * fr;
853
+ const noiseScale = frameNoise(ei * 2.5 + 300, frameIndex * 0.1) * fs;
854
+
855
+ const transform = {
856
+ x: clamp(baseX + noiseX, 0.05, 0.95),
857
+ y: clamp(baseY + noiseY, 0.05, 0.95),
858
+ scale: Math.max(0.05, baseScale + noiseScale),
859
+ rotation: baseRotation + noiseRot,
860
+ };
861
+
862
+ // ── Color (stable per element, small per-frame shift) ──
863
+ const colorRng = createRng(layerSeedInt + ei * 53 + 7);
864
+ const baseColor = pickColor(paletteHints, colorRng, spec.colorShift);
865
+
866
+ // Small per-frame color shift for shimmer
867
+ const frameColorShift = Math.round(frameNoise(ei * 3.7 + 400, frameIndex * 0.08) * 3);
868
+ const color = baseColor.map((ch, ci) => (ci < 3 ? clamp(ch + frameColorShift, 0, 255) : ch));
869
+
870
+ // ── Shape seed (stable topology) ──
871
+ const shapeSeed = `${seed}:${itemId}:${layerKey}:${ei}`;
872
+
873
+ // Stamp onto composite matrix
874
+ stampShape(
875
+ compositeMatrix,
876
+ compositeColors,
877
+ shapeKey,
878
+ transform,
879
+ color,
880
+ spec.noiseLevel * (spec.jitter + 0.5),
881
+ spec.detailLevel,
882
+ shapeSeed,
883
+ );
884
+
885
+ // Also stamp onto layer-local matrix for metadata
886
+ stampShape(
887
+ layerMatrix,
888
+ layerColors,
889
+ shapeKey,
890
+ transform,
891
+ color,
892
+ spec.noiseLevel * 0.5,
893
+ spec.detailLevel,
894
+ shapeSeed,
895
+ );
896
+
897
+ layerEntries.push({
898
+ type: 'shape',
899
+ shapeKey,
900
+ transform,
901
+ color,
902
+ shapeSeed,
903
+ });
904
+ }
905
+
906
+ generatedLayers.push({
907
+ layerId,
908
+ layerKey,
909
+ keys: layerEntries,
910
+ frameMatrix: layerMatrix,
911
+ colors: layerColors,
912
+ });
913
+ }
914
+ }
915
+
916
+ return {
917
+ itemId,
918
+ seed,
919
+ frameIndex,
920
+ layers: generatedLayers,
921
+ compositeFrameMatrix: compositeMatrix,
922
+ compositeColors,
923
+ };
924
+ }
925
+
926
+ /* ═══════════════════════════════════════════════════════════════════════════
927
+ * MULTI-FRAME GENERATION
928
+ * Generates multiple consecutive frames with temporal coherence.
929
+ * ═══════════════════════════════════════════════════════════════════════════ */
930
+
931
+ /**
932
+ * @typedef {Object} MultiFrameResult
933
+ * @property {string} itemId
934
+ * @property {string} seed
935
+ * @property {number} frameCount
936
+ * @property {GenerationResult[]} frames
937
+ * @property {Object} objectLayerRenderFramesData - Ready for ObjectLayerEngine.createObjectLayerDocuments.
938
+ * @property {Object} objectLayerData - Ready for ObjectLayerEngine.createObjectLayerDocuments.
939
+ * @memberof SemanticLayerGenerator
940
+ */
941
+
942
+ /**
943
+ * Generates N consecutive frames for an item-id, producing data structures
944
+ * compatible with ObjectLayerEngine document creation.
945
+ *
946
+ * @param {Object} options
947
+ * @param {string} options.itemId
948
+ * @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.
954
+ * @returns {MultiFrameResult}
955
+ * @memberof SemanticLayerGenerator
956
+ */
957
+ export function generateMultiFrame(options) {
958
+ const { itemId, seed, frameCount = 1, startFrame = 0, count = 3, density = 0.5, frameDuration = 250 } = options;
959
+
960
+ const descriptor = lookupSemantic(itemId);
961
+ if (!descriptor) {
962
+ throw new Error(
963
+ `No semantic descriptor found for item-id "${itemId}". ` +
964
+ `Available prefixes: ${Object.keys(semanticRegistry).join(', ')}`,
965
+ );
966
+ }
967
+
968
+ const frames = [];
969
+ let mergedColors = [[0, 0, 0, 0]]; // index 0 = transparent
970
+
971
+ // First pass: generate all frames
972
+ 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);
981
+ }
982
+
983
+ // Second pass: unify color palettes across all frames
984
+ const globalColors = [[0, 0, 0, 0]];
985
+ const frameMappings = [];
986
+
987
+ for (const frame of frames) {
988
+ const mapping = {};
989
+ for (let ci = 0; ci < frame.compositeColors.length; ci++) {
990
+ 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]);
992
+ if (found < 0) {
993
+ globalColors.push([...c]);
994
+ found = globalColors.length - 1;
995
+ }
996
+ mapping[ci] = found;
997
+ }
998
+ frameMappings.push(mapping);
999
+ }
1000
+
1001
+ // Third pass: remap frame matrices to global palette
1002
+ const remappedFrames = frames.map((frame, fi) => {
1003
+ const mapping = frameMappings[fi];
1004
+ return frame.compositeFrameMatrix.map((row) => row.map((ci) => (mapping[ci] !== undefined ? mapping[ci] : 0)));
1005
+ });
1006
+
1007
+ // Build objectLayerRenderFramesData structure
1008
+ // For floor types: use direction '08' (down_idle / default_idle / none_idle)
1009
+ const objectLayerRenderFramesData = {
1010
+ frame_duration: frameDuration,
1011
+ is_stateless: descriptor.itemType === 'floor',
1012
+ frames: {},
1013
+ colors: globalColors,
1014
+ };
1015
+
1016
+ // Assign frames to directions
1017
+ const directionMappings = {
1018
+ floor: [['down_idle', 'none_idle', 'default_idle']],
1019
+ skin: [
1020
+ ['down_idle', 'none_idle', 'default_idle'],
1021
+ ['up_idle'],
1022
+ ['left_idle', 'up_left_idle', 'down_left_idle'],
1023
+ ['right_idle', 'up_right_idle', 'down_right_idle'],
1024
+ ],
1025
+ weapon: [['down_idle', 'none_idle', 'default_idle']],
1026
+ skill: [['down_idle', 'none_idle', 'default_idle']],
1027
+ coin: [['down_idle', 'none_idle', 'default_idle']],
1028
+ };
1029
+
1030
+ const dirGroups = directionMappings[descriptor.itemType] || directionMappings.floor;
1031
+
1032
+ for (const dirGroup of dirGroups) {
1033
+ for (const dirName of dirGroup) {
1034
+ objectLayerRenderFramesData.frames[dirName] = [...remappedFrames];
1035
+ }
1036
+ }
1037
+
1038
+ // Build objectLayerData
1039
+ const objectLayerData = {
1040
+ data: {
1041
+ item: {
1042
+ id: itemId,
1043
+ type: descriptor.itemType,
1044
+ description: `Procedurally generated ${descriptor.semanticTags.join(', ')} (seed: ${seed})`,
1045
+ activable: true,
1046
+ },
1047
+ stats: {
1048
+ effect: hashMod(seed + ':effect', 11),
1049
+ resistance: hashMod(seed + ':resistance', 11),
1050
+ agility: hashMod(seed + ':agility', 11),
1051
+ range: hashMod(seed + ':range', 11),
1052
+ intelligence: hashMod(seed + ':intelligence', 11),
1053
+ utility: hashMod(seed + ':utility', 11),
1054
+ },
1055
+ seed: seedToUUIDv4(seed + ':' + itemId),
1056
+ },
1057
+ };
1058
+
1059
+ return {
1060
+ itemId,
1061
+ seed,
1062
+ frameCount,
1063
+ frames,
1064
+ objectLayerRenderFramesData,
1065
+ objectLayerData,
1066
+ };
1067
+ }
1068
+
1069
+ /**
1070
+ * Deterministic modulo hash for stat generation.
1071
+ * @param {string} str
1072
+ * @param {number} mod
1073
+ * @returns {number}
1074
+ */
1075
+ function hashMod(str, mod) {
1076
+ return ((hashString(str) % mod) + mod) % mod;
1077
+ }
1078
+
1079
+ /* ═══════════════════════════════════════════════════════════════════════════
1080
+ * EXPORTS
1081
+ * ═══════════════════════════════════════════════════════════════════════════ */
1082
+
1083
+ export { hashString, deriveLayerSeed, deriveFrameSeed, pickColor, pickShape, seedToUUIDv4, GRID_DIM, semanticRegistry };