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.
- package/.github/workflows/engine-cyberia.cd.yml +1 -0
- package/CHANGELOG.md +56 -1
- package/CLI-HELP.md +2 -4
- package/README.md +139 -0
- package/bin/build.js +5 -0
- package/bin/cyberia.js +385 -71
- package/bin/deploy.js +18 -26
- package/bin/file.js +3 -0
- package/bin/index.js +385 -71
- package/conf.js +32 -3
- package/deployment.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/manifests/ipfs/configmap.yaml +7 -0
- package/package.json +8 -8
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +2 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +93 -2
- package/src/api/file/file.controller.js +3 -13
- package/src/api/file/file.ref.json +0 -21
- package/src/api/ipfs/ipfs.controller.js +104 -0
- package/src/api/ipfs/ipfs.model.js +71 -0
- package/src/api/ipfs/ipfs.router.js +31 -0
- package/src/api/ipfs/ipfs.service.js +193 -0
- package/src/api/object-layer/README.md +139 -0
- package/src/api/object-layer/object-layer.controller.js +3 -0
- package/src/api/object-layer/object-layer.model.js +15 -1
- package/src/api/object-layer/object-layer.router.js +6 -10
- package/src/api/object-layer/object-layer.service.js +311 -182
- package/src/cli/cluster.js +30 -38
- package/src/cli/index.js +0 -1
- package/src/cli/run.js +14 -0
- package/src/client/components/core/LoadingAnimation.js +2 -3
- package/src/client/components/core/Modal.js +1 -1
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +4 -5
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +280 -29
- package/src/client/services/ipfs/ipfs.service.js +144 -0
- package/src/client/services/object-layer/object-layer.management.js +161 -8
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +1 -1
- package/src/server/auth.js +18 -18
- package/src/server/ipfs-client.js +433 -0
- package/src/server/object-layer.js +649 -18
- package/src/server/semantic-layer-generator.js +1083 -0
- package/src/server/shape-generator.js +952 -0
- package/test/shape-generator.test.js +457 -0
- 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 };
|