{"version":3,"sources":["climate.mjs"],"sourcesContent":["// A WORLD'S CLIMATE (WG-09, tools/terrain-evolve/BIOMES.md): how it turns, and how warm each part of it is. Pure and\n// twin-safe: no imports, no DOM, no clock, and only exactly specified arithmetic (+ − × ÷ and sqrt, Math.imul), so the\n// servers' copies (functions/lib, zone-server/lib, byte-identical) answer as the client does when biomes are mined.\n//\n// THE ROTATION REGIME, from the orbit and the star. A planet close to a dim star is tidally locked to it: one face\n// always to the star (the day face), the opposite one away (the night face), the four between on the terminator. How\n// close is \"close\" grows as the star dims: an M dwarf's first three planets, a K star's first two, a G star's first; an\n// F, A, B or O star locks none (its habitable distances are far out). A moon is locked to its planet, not to its star,\n// so the star's light sweeps round it: for its climate it turns. So does each of a pair of planets orbiting each other\n// (addresses ending a and b), locked to its partner.\n//\n// A WORLD THAT TURNS spins as the renderer spins it: the cube stands corner-up and turns about its (1, 1, 1) diagonal,\n// so its poles are the two corners on that diagonal (the owner's choice, Sep 29). Its cold caps are round those two\n// corners, and its warm equator is the zig-zag belt where the plane x + y + z = 0 crosses all six faces: every face has\n// a cold corner and a warm side.\n//\n// THE CLIMATE FIELD is a function of the direction from the world's centre only, so it is continuous over every edge and\n// corner of the cube by construction. Its contrast shrinks with thicker air (heat carried round) and is greatest on an\n// airless world.\n\n// FNV-1a over a string: a number in [0, 1).\nfunction hash01(s) {\n  let h = 2166136261;\n  for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);\n  h ^= h >>> 15; h = Math.imul(h, 0x2c1b3c6d); h ^= h >>> 12;\n  return (h >>> 0) / 4294967296;\n}\nconst smooth = (t) => (t <= 0 ? 0 : t >= 1 ? 1 : t * t * (3 - 2 * t));\n\n// The six faces, as the renderer's grid has them: face index fi = axis * 2 + (negative ? 1 : 0).\nexport const FACES = Object.freeze([[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]]);\nexport const ZONES = Object.freeze(['day', 'night', 'terminator', 'pole', 'equator']);\nconst LOCK_LANES = { M: 3, K: 2, G: 1 };\n// The spin axis of every world that turns, in the voxel grid's frame: the corner the renderer stands up (system-view.js\n// VOXEL_CORNER_UP turns it to +Y, the axis setSpin turns the world about), as a unit vector.\nconst S3 = 1 / Math.sqrt(3);\nexport const SPIN_AXIS = Object.freeze([S3, S3, S3]);\n\n// The spectral class of a star from its name (\"M-Class Giant\" → \"M\"); null when there is none (a rogue world).\nexport function starClassOf(name) {\n  const m = /\\b([OBAFGKM])-Class\\b/.exec(String(name || ''));\n  return m ? m[1] : null;\n}\n// A world's place, from its address: galaxy.system.star.planet[a|b][.moon]. Returns { planet (1-based lane order), moon,\n// binary (one of a pair of planets) }.\nexport function placeOf(addr) {\n  const p = String(addr || '').split('.');\n  return { planet: parseInt(p[3], 10) || 0, moon: p.length >= 5, binary: /[ab]$/.test(p[3] || '') };\n}\n\n// { regime: 'locked' | 'rotating', face, dir }: a locked world's day face (a face index, drawn for the world) and its\n// normal; a turning world's face is null and dir its spin axis, SPIN_AXIS. `world` is { addr, star } (the star's name;\n// a moon is told by its address).\nexport function rotationOf(world) {\n  const { planet, moon, binary } = placeOf(world.addr), cls = starClassOf(world.star);\n  const locked = !moon && !binary && planet > 0 && !!cls && planet <= (LOCK_LANES[cls] || 0);\n  if (!locked) return { regime: 'rotating', face: null, dir: SPIN_AXIS };\n  const face = Math.floor(hash01((world.addr || '') + '#climate-axis') * 6);\n  return { regime: 'locked', face, dir: FACES[face] };\n}\n\n// How far the field departs from the mean, by the air: 1 airless, a quarter at a bar, near nothing at ten.\nconst contrastOf = (world) => (world.airless ? 1 : 1 / (1 + (world.pressureMbar || 0) / 300));\n\n// A world's climate: its mean temperature (the temperature model's, as the game reads it), its regime, its day face\n// (locked) and axis, the field's coldest and warmest (minC, maxC), and each face's zone and temperature at its middle.\n// (A turning world's six middles all stand at the mean, a third of the way from the equator to a pole: its caps and\n// its belt cross every face.) `world` is { addr, star, tempC, pressureMbar, airless }.\n// NO LATITUDE ON A TURNING WORLD (world version 12, the owner, Oct 11: \"remove latitude from climate\", and nothing in its\n// place: \"flat\"). With `opts.flat`, a world that turns has ONE temperature, its mean, from pole to pole and at every\n// height: no cold caps, no warm belt, and the `pole` zone weighs nothing. A biome that needs cold or warm ground grows\n// only on a world whose average suits it; region noise and the ground's roles still lay the biomes out. A tidally\n// locked world keeps its day-face and night-face field even so: that follows its star, not its poles (Night-side Glow,\n// a day face's Volcanic Fields). The spin axis is unchanged (the corner diagonal), and so are day and night.\n// ONE TEMPERATURE ON EVERY WORLD (world version 13, the owner, Oct 12: \"change world gen so the entire worlds are one\n// temperature — seasons only affects daylight/geometry\", and of a locked world: \"LOCKED WORLDS ARE ONE TEMPERATURE TOO\").\n// With `opts.oneTemp` a world locked to its star is flat as well: no hot day face, no frozen night face, its one\n// temperature its mean from the substellar point to the antistellar one, and the same as a turning world's would be\n// (no special average of two faces: the mean is temperature.mjs's planet-wide number either way). What it keeps is its\n// LIGHT: it is still locked (regime, face and dir), so its night face is still dark (biomes.mjs dimAt, the nightside\n// biomes) and its growths still lean to its star; only the temperature, and with it the day face's let-in for Volcanic\n// Fields (lockedDayC), is gone. Without it every world is what version 12 (or older) made it, to the bit.\nexport function climateOf(world, opts) {\n  const rot = rotationOf(world), k = contrastOf(world), mean = +world.tempC || 0;\n  const flat = (!!(opts && opts.flat) && rot.regime !== 'locked') || !!(opts && opts.oneTemp);\n  const c = { addr: world.addr, meanC: mean, pressureMbar: world.airless ? 0 : (world.pressureMbar || 0), airless: !!world.airless,\n              regime: rot.regime, face: rot.face, dir: rot.dir, contrast: k, faces: [],\n              minC: Math.max(-273, flat ? mean : rot.regime === 'locked' ? mean - 180 * k : mean - 60 * k),\n              maxC: Math.max(-273, flat ? mean : rot.regime === 'locked' ? mean + 120 * k : mean + 30 * k) };\n  if (flat) c.flat = true;\n  for (let fi = 0; fi < 6; fi++) { const f = fieldAt(c, FACES[fi][0], FACES[fi][1], FACES[fi][2]); c.faces.push({ face: fi, zone: f.zone, tempC: f.tempC }); }\n  return c;\n}\n\n// The field at a direction (x, y, z) (any length but zero): the temperature there, and how much of each zone it is\n// (weights summing to 1, the zone the largest). Locked: warmest at the substellar point (up to 120 °C × contrast over\n// the mean), coldest at the antistellar one (down to 180 °C × contrast under it). Turning: the equator up to 30 °C ×\n// contrast over the mean and the poles 60 °C × contrast under it (a sphere's mean of the square of the latitude's sine\n// is a third, so the field averages to the mean); a pole's cap reaches about 39° from its corner.\nexport function fieldAt(c, x, y, z) {\n  const l = Math.sqrt(x * x + y * y + z * z) || 1, a = c.dir;\n  const mu = (x * a[0] + y * a[1] + z * a[2]) / l, k = c.contrast;\n  if (c.regime === 'locked') {\n    // (version 13: a locked world that is flat, climateOf opts.oneTemp, reads its mean everywhere; its zones stay its light's)\n    const t = c.flat ? c.meanC : c.meanC + (mu > 0 ? 120 * k * mu : 180 * k * mu);\n    const day = smooth((mu - 0.3) / 0.4), night = smooth((-mu - 0.3) / 0.4);\n    const w = { day, night, terminator: 1 - day - night, pole: 0, equator: 0 };\n    return { tempC: t < -273 ? -273 : t, zone: day >= 0.5 ? 'day' : night >= 0.5 ? 'night' : 'terminator', w };\n  }\n  if (c.flat) {                                              // version 12: one temperature, the world's mean (climateOf opts.flat)\n    const t = c.meanC;\n    return { tempC: t < -273 ? -273 : t, zone: 'equator', w: { day: 0, night: 0, terminator: 0, pole: 0, equator: 1 } };\n  }\n  const s2 = mu * mu, t = c.meanC + 90 * k * (1 / 3 - s2);\n  const pole = smooth((s2 - 0.45) / 0.3);\n  return { tempC: t < -273 ? -273 : t, zone: pole >= 0.5 ? 'pole' : 'equator', w: { day: 0, night: 0, terminator: 0, pole, equator: 1 - pole } };\n}\n"],"mappings":"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","names":["t"],"file":"climate.mjs"}