// 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); }