Files
UnrealPrototyping/Tools/Orogen/js/planet-code.js
T
2026-09-25 17:02:24 +03:00

276 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Planet code encode/decode — packs seed + slider values into a compact base36 string.
// Pure functions, no DOM access.
// Slider quantization tables
const SLIDERS = [
{ min: 5000, step: 1000, count: 2556 }, // Detail (N)
{ min: 0, step: 0.05, count: 21 }, // Irregularity (jitter)
{ min: 4, step: 1, count: 117 }, // Plates (P)
{ min: 1, step: 1, count: 10 }, // Continents
{ min: 0, step: 0.01, count: 51 }, // Roughness
{ min: 0, step: 0.05, count: 21 }, // Smoothing
{ min: 0, step: 0.05, count: 21 }, // Glacial Erosion
{ min: 0, step: 0.05, count: 21 }, // Hydraulic Erosion
{ min: 0, step: 0.05, count: 21 }, // Thermal Erosion
{ min: 0, step: 0.05, count: 21 }, // Ridge Sharpening
{ min: 0, step: 0.05, count: 21 }, // Soil Creep
{ min: 0, step: 0.05, count: 21 }, // Terrain Warp
{ min: 0, step: 0.05, count: 21 }, // 12: Continent Size Variety
{ min: -15, step: 1, count: 31 }, // 13: Temperature
{ min: -1, step: 0.1, count: 21 }, // 14: Precipitation
{ min: 0, step: 0.01, count: 101 }, // 15: Land Coverage
];
// Mixed-radix bases (right-to-left): lcIdx, prcIdx, tmpIdx, csvIdx, twIdx, scIdx, rsIdx, teIdx, heIdx, glIdx, smIdx, nsIdx, cnIdx, pIdx, jIdx, nIdx, seed
const RADICES = [101, 21, 31, 21, 21, 21, 21, 21, 21, 21, 21, 51, 10, 117, 21, 2556];
const SEED_MAX = 16777216; // 2^24
const BASE_LEN = 22; // base code length (no toggles)
const PREV5_LEN = 21; // previous 21-char codes (before land coverage)
const PREV4_LEN = 18; // previous 18-char codes (before continent variety/temp/precip)
const PREV3_LEN = 17; // previous 17-char codes (before terrain warp)
const PREV2_LEN = 16; // previous 16-char codes (before glacial erosion)
const PREV_LEN = 14; // previous 14-char codes (before ridge/creep)
const LEGACY_LEN = 13; // legacy 13-char codes (single erosion slider)
const IDX_CHARS = 2; // base36 chars per plate index (max index 119 = "3b")
// Legacy radices for decoding old 13-char codes (single erosion slider)
const LEGACY_RADICES = [21, 21, 51, 10, 117, 21, 2559];
// Previous-gen radices for decoding 14-char codes (two erosion sliders, no ridge/creep)
const PREV_RADICES = [21, 21, 21, 51, 10, 117, 21, 2559];
// Previous2-gen radices for decoding 16-char codes (no glacial erosion)
const PREV2_RADICES = [21, 21, 21, 21, 21, 51, 10, 117, 21, 2559];
// Previous3-gen radices for decoding 17-char codes (no terrain warp)
const PREV3_RADICES = [21, 21, 21, 21, 21, 21, 51, 10, 117, 21, 2559];
// Previous5-gen radices for decoding 21-char codes (before land coverage)
const PREV5_RADICES = [21, 31, 21, 21, 21, 21, 21, 21, 21, 21, 51, 10, 117, 21, 2556];
// Previous4-gen radices for decoding 18-char codes (before continent variety/temp/precip)
const PREV4_RADICES = [21, 21, 21, 21, 21, 21, 21, 51, 10, 117, 21, 2556];
function toIndex(value, slider) {
return Math.round((value - slider.min) / slider.step);
}
function fromIndex(idx, slider) {
// Round to step precision to avoid floating-point drift
const raw = slider.min + idx * slider.step;
const decimals = slider.step < 1 ? String(slider.step).split('.')[1].length : 0;
return decimals > 0 ? parseFloat(raw.toFixed(decimals)) : raw;
}
/** Parse a base36 string into a BigInt (char-by-char for full precision). */
function parseBase36(str) {
return [...str].reduce((acc, ch) => {
const d = parseInt(ch, 36);
if (isNaN(d)) throw new Error('bad char');
return acc * 36n + BigInt(d);
}, 0n);
}
// Decode format configs: one entry per code length.
// fields: [fieldName, SLIDERS_index] in LSB-first extraction order.
// defaults: field values not encoded in this format.
const DECODE_FORMATS = {
[LEGACY_LEN]: {
radices: LEGACY_RADICES,
fields: [
['hydraulicErosion', 7], ['smoothing', 5], ['roughness', 4],
['numContinents', 3], ['P', 2], ['jitter', 1], ['N', 0],
],
defaults: { terrainWarp: 0.5, glacialErosion: 0, thermalErosion: 0.1,
ridgeSharpening: 0.35, soilCreep: 0.05, continentSizeVariety: 0,
temperatureOffset: 0, precipitationOffset: 0, landCoverage: 0.3 }
},
[PREV_LEN]: {
radices: PREV_RADICES,
fields: [
['thermalErosion', 8], ['hydraulicErosion', 7], ['smoothing', 5], ['roughness', 4],
['numContinents', 3], ['P', 2], ['jitter', 1], ['N', 0],
],
defaults: { terrainWarp: 0.5, glacialErosion: 0, ridgeSharpening: 0.35,
soilCreep: 0.05, continentSizeVariety: 0,
temperatureOffset: 0, precipitationOffset: 0, landCoverage: 0.3 }
},
[PREV2_LEN]: {
radices: PREV2_RADICES,
fields: [
['soilCreep', 10], ['ridgeSharpening', 9], ['thermalErosion', 8], ['hydraulicErosion', 7],
['smoothing', 5], ['roughness', 4], ['numContinents', 3], ['P', 2], ['jitter', 1], ['N', 0],
],
defaults: { terrainWarp: 0.5, glacialErosion: 0, continentSizeVariety: 0,
temperatureOffset: 0, precipitationOffset: 0, landCoverage: 0.3 }
},
[PREV3_LEN]: {
radices: PREV3_RADICES,
fields: [
['soilCreep', 10], ['ridgeSharpening', 9], ['thermalErosion', 8], ['hydraulicErosion', 7],
['glacialErosion', 6], ['smoothing', 5], ['roughness', 4],
['numContinents', 3], ['P', 2], ['jitter', 1], ['N', 0],
],
defaults: { terrainWarp: 0.5, continentSizeVariety: 0,
temperatureOffset: 0, precipitationOffset: 0, landCoverage: 0.3 }
},
[PREV4_LEN]: {
radices: PREV4_RADICES,
fields: [
['terrainWarp', 11], ['soilCreep', 10], ['ridgeSharpening', 9],
['thermalErosion', 8], ['hydraulicErosion', 7], ['glacialErosion', 6],
['smoothing', 5], ['roughness', 4], ['numContinents', 3], ['P', 2], ['jitter', 1], ['N', 0],
],
defaults: { continentSizeVariety: 0, temperatureOffset: 0, precipitationOffset: 0, landCoverage: 0.3 }
},
[PREV5_LEN]: {
radices: PREV5_RADICES,
fields: [
['precipitationOffset', 14], ['temperatureOffset', 13], ['continentSizeVariety', 12],
['terrainWarp', 11], ['soilCreep', 10], ['ridgeSharpening', 9],
['thermalErosion', 8], ['hydraulicErosion', 7], ['glacialErosion', 6],
['smoothing', 5], ['roughness', 4], ['numContinents', 3], ['P', 2], ['jitter', 1], ['N', 0],
],
defaults: { landCoverage: 0.3 }
},
[BASE_LEN]: {
radices: RADICES,
fields: [
['landCoverage', 15], ['precipitationOffset', 14], ['temperatureOffset', 13],
['continentSizeVariety', 12], ['terrainWarp', 11], ['soilCreep', 10], ['ridgeSharpening', 9],
['thermalErosion', 8], ['hydraulicErosion', 7], ['glacialErosion', 6],
['smoothing', 5], ['roughness', 4], ['numContinents', 3], ['P', 2], ['jitter', 1], ['N', 0],
],
defaults: {}
},
};
/** Generic mixed-radix decode: extract fields LSB-first, validate, convert, apply defaults. */
function decodeFormat(packed, config, toggleStr) {
const { radices, fields, defaults } = config;
const result = {};
for (let i = 0; i < radices.length; i++) {
const [name, si] = fields[i];
const idx = Number(packed % BigInt(radices[i]));
packed = packed / BigInt(radices[i]);
if (idx >= SLIDERS[si].count) return null;
result[name] = fromIndex(idx, SLIDERS[si]);
}
result.seed = Number(packed);
if (result.seed < 0 || result.seed >= SEED_MAX) return null;
Object.assign(result, defaults);
const toggledIndices = [];
if (toggleStr) {
for (let i = 0; i < toggleStr.length; i += IDX_CHARS) {
const idx = parseInt(toggleStr.slice(i, i + IDX_CHARS), 36);
if (isNaN(idx) || idx >= result.P) return null;
toggledIndices.push(idx);
}
}
result.toggledIndices = toggledIndices;
return result;
}
/**
* Encode planet parameters into a base36 planet code.
* @param {number} seed - Integer seed 0–16777215
* @param {number} N - Detail (5000–2560000, step 1000)
* @param {number} jitter - Irregularity (0–1, step 0.05)
* @param {number} P - Plates (4–120, step 1)
* @param {number} numContinents - Continents (1–10, step 1)
* @param {number} roughness - Roughness (0–0.5, step 0.01)
* @param {number} terrainWarp - Terrain Warp (0–1, step 0.05)
* @param {number} smoothing - Smoothing (0–1, step 0.05)
* @param {number} glacialErosion - Glacial Erosion (0–1, step 0.05)
* @param {number} hydraulicErosion - Hydraulic Erosion (0–1, step 0.05)
* @param {number} thermalErosion - Thermal Erosion (0–1, step 0.05)
* @param {number} ridgeSharpening - Ridge Sharpening (0–1, step 0.05)
* @param {number} soilCreep - Soil Creep (0–1, step 0.05)
* @param {number} continentSizeVariety - Continent Size Variety (0–1, step 0.05)
* @param {number} temperatureOffset - Temperature offset (-15–15, step 1)
* @param {number} precipitationOffset - Precipitation offset (-1–1, step 0.1)
* @param {number} landCoverage - Land Coverage (0–1, step 0.05)
* @param {number[]} [toggledIndices=[]] - Sorted array of toggled plate indices
* @returns {string} base36 code (22 chars without edits, 22 + '-' + 2*k with k edits)
*/
export function encodePlanetCode(seed, N, jitter, P, numContinents, roughness, terrainWarp, smoothing, glacialErosion, hydraulicErosion, thermalErosion, ridgeSharpening, soilCreep, continentSizeVariety, temperatureOffset, precipitationOffset, landCoverage, toggledIndices = []) {
const nIdx = toIndex(N, SLIDERS[0]);
const jIdx = toIndex(jitter, SLIDERS[1]);
const pIdx = toIndex(P, SLIDERS[2]);
const cnIdx = toIndex(numContinents, SLIDERS[3]);
const nsIdx = toIndex(roughness, SLIDERS[4]);
const smIdx = toIndex(smoothing, SLIDERS[5]);
const glIdx = toIndex(glacialErosion, SLIDERS[6]);
const heIdx = toIndex(hydraulicErosion, SLIDERS[7]);
const teIdx = toIndex(thermalErosion, SLIDERS[8]);
const rsIdx = toIndex(ridgeSharpening, SLIDERS[9]);
const scIdx = toIndex(soilCreep, SLIDERS[10]);
const twIdx = toIndex(terrainWarp, SLIDERS[11]);
const csvIdx = toIndex(continentSizeVariety, SLIDERS[12]);
const tmpIdx = toIndex(temperatureOffset, SLIDERS[13]);
const prcIdx = toIndex(precipitationOffset, SLIDERS[14]);
const lcIdx = toIndex(landCoverage, SLIDERS[15]);
// Mixed-radix packing (least-significant first: lcIdx, prcIdx, tmpIdx, csvIdx, twIdx, ...)
let packed = BigInt(seed);
packed = packed * BigInt(RADICES[15]) + BigInt(nIdx); // * 2556
packed = packed * BigInt(RADICES[14]) + BigInt(jIdx); // * 21
packed = packed * BigInt(RADICES[13]) + BigInt(pIdx); // * 117
packed = packed * BigInt(RADICES[12]) + BigInt(cnIdx); // * 10
packed = packed * BigInt(RADICES[11]) + BigInt(nsIdx); // * 51
packed = packed * BigInt(RADICES[10]) + BigInt(smIdx); // * 21
packed = packed * BigInt(RADICES[9]) + BigInt(glIdx); // * 21
packed = packed * BigInt(RADICES[8]) + BigInt(heIdx); // * 21
packed = packed * BigInt(RADICES[7]) + BigInt(teIdx); // * 21
packed = packed * BigInt(RADICES[6]) + BigInt(rsIdx); // * 21
packed = packed * BigInt(RADICES[5]) + BigInt(scIdx); // * 21
packed = packed * BigInt(RADICES[4]) + BigInt(twIdx); // * 21
packed = packed * BigInt(RADICES[3]) + BigInt(csvIdx); // * 21
packed = packed * BigInt(RADICES[2]) + BigInt(tmpIdx); // * 31
packed = packed * BigInt(RADICES[1]) + BigInt(prcIdx); // * 21
packed = packed * BigInt(RADICES[0]) + BigInt(lcIdx); // * 21
let code = packed.toString(36).padStart(BASE_LEN, '0');
// Append toggled plate indices: "-" + 2-char base36 per index
if (toggledIndices.length > 0) {
code += '-' + toggledIndices
.map(i => i.toString(36).padStart(IDX_CHARS, '0'))
.join('');
}
return code;
}
/**
* Decode a base36 planet code back into planet parameters.
* Supports 22-char (current), 21-char (prev5), 18-char (prev4), 17-char (prev3), 16-char (prev2), 14-char (previous-gen), and 13-char (legacy) codes.
* @param {string} code - base36 code (13, 14, 16, 17, 18, 21, or 22 chars, optionally followed by "-" + toggle indices)
* @returns {{ seed: number, N: number, jitter: number, P: number, numContinents: number, roughness: number, terrainWarp: number, smoothing: number, glacialErosion: number, hydraulicErosion: number, thermalErosion: number, ridgeSharpening: number, soilCreep: number, continentSizeVariety: number, temperatureOffset: number, precipitationOffset: number, landCoverage: number, toggledIndices: number[] } | null}
*/
export function decodePlanetCode(code) {
if (typeof code !== 'string') return null;
code = code.trim().toLowerCase();
// Split base code from optional toggle suffix
const dashIdx = code.indexOf('-');
const base = dashIdx === -1 ? code : code.slice(0, dashIdx);
const toggleStr = dashIdx === -1 ? '' : code.slice(dashIdx + 1);
const config = DECODE_FORMATS[base.length];
if (!config) return null;
if (!/^[0-9a-z]+$/.test(base)) return null;
if (toggleStr && !/^[0-9a-z]+$/.test(toggleStr)) return null;
if (toggleStr && toggleStr.length % IDX_CHARS !== 0) return null;
let packed;
try {
packed = parseBase36(base);
} catch {
return null;
}
return decodeFormat(packed, config, toggleStr);
}