{"version":3,"sources":["settings-controls.mjs"],"sourcesContent":["// ============ PLAYER CONTROLS: LOOK, KEYS, FIELD OF VIEW, GRAPHICS QUALITY, SOUND ============\n// The settings a player arrives expecting, because every other game has them: how fast the view turns, which way is\n// up, which key does what, how wide the view is, how hard the renderer works, and how loud it all is. Missing, they\n// read as unfinished.\n//\n// Pure and dependency-free — no DOM, no storage, no imports — so every rule that decides something (the defaults, the\n// clamps, the keymap and its conflict rule, the quality table) is exercised in node by tests/settings-controls.test.mjs\n// (and the sound settings by tests/sound.test.mjs) instead of by pressing keys. app.js does the DOM half: the Settings\n// panel, localStorage, and handing the result to the system view, which resolves its own keys and scales its own look\n// through the functions below.\n//\n// EVERY DEFAULT IS TODAY'S BEHAVIOUR: sensitivity 1x, up is up, 75 degrees, the keys the game has always had, a quality\n// preset that is exactly what the renderer already did, and silence. Nobody's game changes until they touch a setting.\n\nconst has = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);\n\n// ---- LOOK ---------------------------------------------------------------------------------------------------------\n// A multiplier on the view's own look rate (system-view.js WALK.look and OFLY_LOOK), for the mouse and for a touch drag\n// alike: on a tablet the drag IS the look, and a setting that only reached the mouse would do nothing on the device\n// most likely to want it. The step is a twentieth, so the slider lands on numbers a player can read and say back.\nexport const LOOK_SENS = Object.freeze({ min: 0.25, max: 3, step: 0.05, def: 1 });\n\nexport function clampSens(v) {\n  const n = typeof v === 'number' ? v : parseFloat(v);\n  if (!Number.isFinite(n)) return LOOK_SENS.def;\n  const per = Math.round(1 / LOOK_SENS.step);   // 20: multiply and divide by a whole number, so 1.25 stays 1.25\n  return Math.round(Math.min(LOOK_SENS.max, Math.max(LOOK_SENS.min, n)) * per) / per;\n}\n\n// The two multipliers a look handler applies to a pointer or finger delta. Inverting flips the SIGN of the vertical one\n// and nothing else, so the flight-stick convention (push away to look down) costs the handlers no branch of their own.\nexport function lookScale(sens, invertY) {\n  const s = clampSens(sens);\n  return { x: s, y: invertY ? -s : s };\n}\n\n// ---- FIELD OF VIEW ------------------------------------------------------------------------------------------------\n// Vertical degrees, on foot only: the galaxy and the orbit views frame whole systems at a fixed 75 and never read it.\n// The range is the one the slider has always had, so a width somebody already chose is never clamped away from them.\nexport const FOV_RANGE = Object.freeze({ min: 50, max: 110, def: 75 });\n\nexport function clampFov(v) {\n  const n = typeof v === 'number' ? v : parseFloat(v);\n  if (!Number.isFinite(n)) return FOV_RANGE.def;\n  return Math.max(FOV_RANGE.min, Math.min(FOV_RANGE.max, Math.round(n)));\n}\n\n// A stored width is taken only if the slider could have written it; anything else is the default. That is how the\n// setting has always been read back (app.js getFov), kept exactly so that moving it here changes nothing for anyone.\nexport function fovFromStored(raw) {\n  const v = parseFloat(raw);\n  return (v >= FOV_RANGE.min && v <= FOV_RANGE.max) ? v : FOV_RANGE.def;\n}\n\n// ---- GRAPHICS QUALITY ---------------------------------------------------------------------------------------------\n// Three presets over the costs that can change while the game runs, with no new GL context and no reload:\n//   maxDpr        render resolution of the galaxy and the orbit views, as a ceiling on devicePixelRatio\n//   worldMaxDpr   the same for the on-foot view, where its adaptive scaler starts (system-view.js animate)\n//   (the scaler's floor is SCALER_FLOOR of either ceiling: worldPixelRatio, orbitPixelRatio)\n//   The rest are the same on every preset since Oct 19 (PRESETS NEVER CHANGE WHAT IS DRAWN, below), and kept as fields\n//   so the renderer reads one table:\n//   shadowSteps   cells a terrain shadow ray may cross before it gives up: the per-pixel march in VOX_LIGHT_GLSL\n//   nearbyWorlds  whether the moons and neighbours of the world in focus are built as terrain, or stay the cheap\n//                 placeholder cubes every distant body already is\n//   tempDetail    whether a world's temperature look (PHY-09: frost, glowing cracks) draws its detail from the ice\n//                 canvases, or from the kube's own texture alone: no extra canvas baked, held or sampled\n//   seaDetail     whether a natural sea (version 2's lakes and oceans) measures the water under each piece of its\n//                 surface to shade it, or wears one middling depth everywhere: eight fewer texture reads a sea pixel\n//   fluidFx       what a liquid draws while it moves (the owner's Fluid Animations plan, Oct 6, `quality: tiered`):\n//                   rings    how many ring sources a liquid's surface lights at once (dips, edits, wading), 0 for none\n//                   streaks  light streaks running the way moving water runs\n//                   bands    slow bands of light across a sea on foot: 0 off, 1 everywhere, 2 within BANDS_NEAR kubes\n//                   ribbons  a falling ribbon where poured water goes over a ledge, and the sheet into a breach\n//                   ride     floating leaves ride the drawn surface while it moves\n//                   wetBeds  ground a liquid has just left stays dark and wet a while\n//                 Easing is on every preset. Low keeps the geometry (ribbons, riding, wet beds) and drops what is drawn\n//                 per pixel of water (rings, streaks, bands); Medium caps the rings at four and keeps the bands near\n//                 you. A player who asked for less motion (motion.mjs) gets none of the moving ones whatever the preset.\n//\n// HIGH (shown as Auto) IS THE SCREEN'S OWN RESOLUTION, up to 2x, in orbit and on foot alike (the owner, Oct 1: the walk\n// was blurry at a capped 1.5). The code never picks a resolution by device class, only by devicePixelRatio, and from Oct\n// 19 the adaptive scaler (FRAME_SCALER) holds every view, orbit included, at 60 by giving up at most a quarter of it.\n// LOW is one pixel per CSS pixel everywhere (a quarter of High's fragments on a 2x screen): the floor device is an iPad\n// Air 2 on Safari 15, with 2 GB shared between two live GL contexts. It used to draw less as well (shorter shadows,\n// terrain only for the world in focus, plainer frost and lava, no fluid rings); it draws what High draws now, as every\n// preset does, and its savings are its pixels.\nexport const FLUID_RINGS_MAX = 8;    // the ring sources a liquid's shader is compiled for; a preset lights fewer\nexport const BANDS_NEAR = 32;        // kubes: how far from the camera Medium draws a sea's light bands\nconst FX = (rings, streaks, bands) => Object.freeze({ rings, streaks, bands, ribbons: true, ride: true, wetBeds: true });\n// PRESETS NEVER CHANGE WHAT IS DRAWN (the owner's render rules, Oct 18-19: \"graphics presets automatic with optional\n// override\", \"presets never change what's drawn, only how it's computed\"). Every preset marches the same 56-cell shadows,\n// builds the same worlds, wears the same temperature detail, seas and fluid effects: a Low screen shows the same picture as\n// a High one, at fewer pixels. What a preset sets is the RESOLUTION: its ceiling, and the scaler's floor under it\n// (SCALER_FLOOR of the ceiling, never lower). High is shown as Auto, the default: the screen's own resolution up to 2x, with\n// the scaler stepping down (to 75%, no further) only while frames miss the display's refresh. Medium and Low are the\n// optional lower ceilings, for a device that runs hot or a player who would rather keep the battery.\n// (Until Oct 19, Low and Medium also marched shorter shadows (24 and 40 cells), built terrain only for the world in focus\n// (Low), and dropped the temperature detail, the seas' measured depths and the fluid's rings, streaks and bands.)\nexport const QUALITY_PRESETS = Object.freeze({\n  low:    Object.freeze({ id: 'low',    label: 'Low',    maxDpr: 1,   worldMaxDpr: 1,    shadowSteps: 56, nearbyWorlds: true, tempDetail: true, seaDetail: true, fluidFx: FX(FLUID_RINGS_MAX, true, 2) }),\n  medium: Object.freeze({ id: 'medium', label: 'Medium', maxDpr: 1.5, worldMaxDpr: 1.25, shadowSteps: 56, nearbyWorlds: true, tempDetail: true, seaDetail: true, fluidFx: FX(FLUID_RINGS_MAX, true, 2) }),\n  high:   Object.freeze({ id: 'high',   label: 'Auto',   maxDpr: 2,   worldMaxDpr: 2,    shadowSteps: 56, nearbyWorlds: true, tempDetail: true, seaDetail: true, fluidFx: FX(FLUID_RINGS_MAX, true, 2) }),   // bands near the camera only (Oct 7: everywhere, they read as a checkerboard from orbit)\n});\n// THE SCALER'S FLOOR, AS A SHARE OF THE CEILING (the owner, Oct 18: \"resolution scaler only as last resort, never below\n// 75%\"). In orbit and on foot, on every screen and every preset.\nexport const SCALER_FLOOR = 0.75;\n// WHAT A LIQUID DRAWS NOW: the preset's fluidFx, less every moving effect for a player who asked for less motion (the\n// plan's `reduced: calm`: a liquid still eases to its new shape, in half a second, with no stream, rings, streaks or\n// bands). Pure, so the renderer and the tests read one rule.\nexport function fluidFxFor(q, reduced) {\n  const fx = qualityPreset(q).fluidFx;\n  if (!reduced) return fx;\n  return Object.freeze({ ...fx, rings: 0, streaks: false, bands: 0, ribbons: false, calm: true });\n}\nexport const QUALITY_IDS = Object.freeze(['low', 'medium', 'high']);\nexport const QUALITY_DEFAULT = 'high';\n// A PHONE STARTS ON MEDIUM (speed audit D1, Oct 21): High is a DPR-2 ceiling, antialiasing and the longest shadow march,\n// and the scaler only steps down slowly. A touch screen (coarse pointer) or a small-memory device that never chose a\n// preset gets Medium; a chosen preset is kept, and the setting still offers High.\nexport function qualityDefaultFor(dev) {\n  const d = dev || {};\n  return d.coarse || (Number(d.memory) > 0 && Number(d.memory) <= 4) ? 'medium' : QUALITY_DEFAULT;\n}\n// A GLSL loop needs a CONSTANT bound, so the march is compiled at the longest reach any preset asks for and each preset\n// breaks out of it early through a uniform. Deriving the bound from the table means a longer preset cannot be added\n// without the shader being able to honour it.\nexport const SHADOW_STEPS_MAX = Math.max(...QUALITY_IDS.map((id) => QUALITY_PRESETS[id].shadowSteps));\n\n// A preset by id (or a preset passed straight back), and the default for anything else — a stale id from a later\n// version, a typo in storage, or an inherited name like \"constructor\" that a plain lookup would happily return.\nexport function qualityPreset(q) {\n  if (q && typeof q === 'object' && has(QUALITY_PRESETS, q.id) && QUALITY_PRESETS[q.id] === q) return q;\n  return has(QUALITY_PRESETS, q) ? QUALITY_PRESETS[q] : QUALITY_PRESETS[QUALITY_DEFAULT];\n}\n\n// The pixel ratio for the galaxy and orbit views on a screen of this density.\nexport function viewPixelRatio(dpr, q) {\n  const d = dpr > 0 ? dpr : 1;\n  return Math.min(d, qualityPreset(q).maxDpr);\n}\n\n// The on-foot range: the ceiling the adaptive scaler starts at, and the floor it may shed to, SCALER_FLOOR of it. (Until\n// Oct 19 the floor was native on a low-density screen and the preset's own below that, which on a 2x screen let High shed\n// to half its resolution; the owner's rule now is three quarters of the ceiling, on every screen, and only while frames\n// are late.)\nexport function worldPixelRatio(dpr, q) {\n  const p = qualityPreset(q), d = dpr > 0 ? dpr : 1, max = Math.min(d, p.worldMaxDpr);\n  return { max, min: max * SCALER_FLOOR };\n}\n// THE FIRST SECONDS OF A COLD LOAD DRAW AT ONE PIXEL PER POINT (the owner's play review, Oct 23, s2: \"progressive pixel\n// ratio and a lighter first frame\"). A phone waited 11 s for its first galaxy frame, at 375x812: the first frames are the\n// ones that build and upload everything, and on a 3x screen each also fills nine pixels per point, antialiased. So the\n// galaxy opens at 1 (or the screen's own ratio, if that is lower), holds it for holdMs while the first frames do their\n// one-off work, then climbs to the preset's ratio a step at a time, stepMs apart: each step reallocates the drawing\n// buffer once, and a climb in steps keeps that cost spread out. Pure: the ratio a view should draw at, `ms` after its\n// first frame, on its way to `target`.\nexport const BOOT_RATIO = Object.freeze({ start: 1, holdMs: 1500, stepMs: 700, step: 0.5 });\nexport function bootRatioAt(target, ms) {\n  const t = target > 0 ? target : 1, B = BOOT_RATIO;\n  if (t <= B.start) return t;\n  if (!(ms >= B.holdMs)) return B.start;\n  return Math.min(t, B.start + B.step * (1 + Math.floor((ms - B.holdMs) / B.stepMs)));\n}\n// …and the orbit and galaxy views' (the render plan, phase 7: the scaler runs in orbit too, the last resort for the\n// heaviest views): from viewPixelRatio down to SCALER_FLOOR of it.\nexport function orbitPixelRatio(dpr, q) {\n  const max = viewPixelRatio(dpr, q);\n  return { max, min: max * SCALER_FLOOR };\n}\n\n// ---- FRAME BUDGETS (STAB-05) ---------------------------------------------------------------------------------------\n// How long a frame may take on each class of device, kept beside the presets it comes from. The on-foot adaptive scaler\n// (system-view.js animate) reads its thresholds from FRAME_SCALER, and the developer readout (Settings → Display) judges\n// the device it runs on against FRAME_BUDGETS, so the game and the person measuring it hold the same line.\n//\n// THE SCALER IS WHERE THE LINE COMES FROM. On foot it averages the real frame period over 30 frames and moves the render\n// resolution: down a step of 0.15 when the average is over 21 ms (below ~48 fps: a 60 Hz display missing about one\n// refresh in four), up a step of 0.1 when there is headroom. The gap between the two is deliberate: shed and restore\n// about a single line and the scaler flaps, reallocating the drawing buffer twice a second.\n//\n// HEADROOM IS MEASURED AGAINST THE DISPLAY, not against a fixed 13.5 ms. That line (~74 fps) can never be crossed on a\n// 60 Hz screen, where vsync holds every frame at 16.7 ms however light it is, so on the commonest display the scaler\n// could only ever go DOWN: the terrain build on landing shed it to the floor and it stayed there for the visit (Oct 1,\n// the owner's \"a little blurry\"). The refresh is the window's MEDIAN frame period, held to no slower than 60 Hz so a\n// device that never makes 60 is not mistaken for a 30 Hz screen with headroom. A window whose typical frame is within 10%\n// of it has dropped next to nothing, which is headroom whatever the refresh. Not the shortest frame: rAF timestamps\n// jitter a millisecond either side of vsync, and measured on the owner's screen (shortest 15.6 ms, 75th percentile\n// 17.6 ms at a steady 60 Hz) a line drawn from the shortest frame was never met, and the scaler never climbed.\n//\n// A RESTORE THAT CANNOT HOLD WAITS LONGER. It needs restoreWindows quiet windows in a row; a shed within two windows of\n// a restore doubles that, to restoreWindowsMax (8 s), so a ceiling the GPU cannot quite hold settles one step below it.\n//\n// IT JUDGES THE TYPICAL FRAME, NOT THE MEAN. Resolution only changes how long the GPU spends filling pixels, which slows\n// nearly every frame; a CPU hitch (a chunk build, a snapshot, a collection) is one long frame that resolution does not\n// touch. Judged by the mean, the hitches after a landing (measured Oct 1: median 16.7 ms, one frame in ten at 66-100 ms)\n// shed the owner's 2x screen to the floor, flapped the restore wait up to 8 s, and a 33 ms hitch every couple of seconds\n// then kept it there. So the line is read at the 75th percentile of the window: shed when more than a quarter of frames\n// are late, count a window as quiet when three quarters are on time.\n//\n// ON FOOT IT JUDGES THE 95TH PERCENTILE, LESS THE HITCHES (the owner's play review, Oct 23, s7: \"Target 60 fps: lower the\n// render resolution step by step when the 95th percentile frame passes 20 ms, and raise it again when there is room\").\n// Measured for the owner at 1920x929 on a 2x screen: 53 fps, 95th percentile 34 ms, and the scaler never moved. At 53 fps\n// on a 60 Hz display about one frame in eight misses vsync and lands at 33 ms, so three quarters of every window are on\n// time: the 75th percentile read 16.7 ms, under the 21 ms line, and the walk stayed at full resolution, judder and all.\n// The 95th percentile sees those missed frames (walk.quantile, walk.shedAboveMs). What it must not see is the CPU hitch\n// the rule above was written for, which resolution cannot fix: a frame longer than walk.hitchMs (three refreshes) is\n// counted as the window's median instead, unless the median is itself over the line (a device that is simply slow,\n// whose long frames are its real ones). Orbit keeps the 75th percentile and its line.\nexport const FRAME_SCALER = Object.freeze({ windowFrames: 30, quantile: 0.75, shedAboveMs: 21, restoreBelowMs: 13.5, restoreBelowRefresh: 1.1,\n  restoreWindows: 2, restoreWindowsMax: 16, shedStep: 0.15, restoreStep: 0.1,\n  walk: Object.freeze({ quantile: 0.95, shedAboveMs: 20, hitchMs: 50 }) });\n// The window's typical frame (at `quantile`, FRAME_SCALER's by default), its median and its shortest, from its frame\n// periods. With `hitchMs`, a frame longer than that is read as the median (a hitch, not fill rate), unless the median is\n// over `lineMs` too.\nexport function scalerWindow(periods, quantile = FRAME_SCALER.quantile, { hitchMs = 0, lineMs = Infinity } = {}) {\n  const s = periods.slice().sort((a, b) => a - b);\n  const medMs = s[s.length >> 1];\n  const t = hitchMs > 0 && medMs <= lineMs ? s.map((v) => (v > hitchMs ? medMs : v)).sort((a, b) => a - b) : s;\n  return { typMs: t[Math.min(t.length - 1, Math.floor(t.length * quantile))], medMs, minMs: s[0] };\n}\n// …the on-foot reading of a window: its 95th percentile, hitches aside (FRAME_SCALER.walk).\nexport function walkScalerWindow(periods) {\n  const W = FRAME_SCALER.walk;\n  return scalerWindow(periods, W.quantile, { hitchMs: W.hitchMs, lineMs: W.shedAboveMs });\n}\n\n// One decision of the on-foot scaler, at the end of a window: the new pixel ratio. `st` is the scaler's own memory\n// (scalerState()), kept by the caller and changed here; typMs and medMs are the window's typical and median frame\n// period (scalerWindow).\nexport function scalerState() { return { good: 0, need: FRAME_SCALER.restoreWindows, sinceRestore: Infinity, sinceShed: 0 }; }\nexport function scalerStep(st, typMs, medMs, dpr, floor, ceil, shedAboveMs = FRAME_SCALER.shedAboveMs) {\n  const F = FRAME_SCALER;\n  st.sinceRestore++; st.sinceShed = (st.sinceShed || 0) + 1;\n  if (typMs > shedAboveMs && dpr > floor + 1e-3) {\n    if (st.sinceRestore <= 2) st.need = Math.min(F.restoreWindowsMax, st.need * 2);   // that restore could not hold\n    st.good = 0; st.sinceShed = 0;\n    return Math.max(floor, dpr - F.shedStep);\n  }\n  // …AND THE WAIT COMES BACK DOWN. Doubling it was all the memory the scaler had, so the flaps of a landing (terrain\n  // building, frames late and on time by turns) left it at 8 s for the rest of the visit, and with a spike every few\n  // seconds it never saw 8 quiet seconds in a row: measured Oct 1, held at 1x on healthy frames. Every 20 windows\n  // (10 s) without a shed, it halves.\n  if (st.sinceShed >= 20 && st.need > F.restoreWindows) { st.need = Math.max(F.restoreWindows, st.need / 2); st.sinceShed = 0; }\n  const refresh = Math.min(1000 / 60, Math.max(4, medMs));\n  if (typMs < Math.max(F.restoreBelowMs, refresh * F.restoreBelowRefresh)) {\n    if (++st.good >= st.need && dpr < ceil - 1e-3) { st.good = 0; st.sinceRestore = 0; return Math.min(ceil, dpr + F.restoreStep); }\n  } else st.good = 0;\n  return dpr;\n}\n\n// THE BUDGETS. `frameMs` is one refresh of the display the class plays on: the target. `meanMs` is the most the AVERAGE\n// frame may cost before the class is over budget, and for the two 60 Hz classes it is the scaler's shed line, so \"over\n// budget\" and \"the game has started giving up resolution to get back under\" are the same statement. `p95Ms` lets one\n// frame in twenty run a refresh late (two refreshes long): one late frame is invisible, a run of them is judder.\n// `preset` is the graphics quality the class is expected to play on.\n//\n//   desktop  A laptop or desktop at 60 Hz, on High, the default everywhere.\n//   phone    A recent phone at 60 Hz. The scaler does not know it is on a phone and holds it to the same line; Medium is\n//            the preset to suggest to one that cannot hold it (a lower ceiling on foot, shorter shadows).\n//   floor    An iPad Air 2 on Safari 15, with 2 GB shared between two live WebGL contexts. 30 fps is the honest aim for an\n//            A8X marching voxel shadows. On Low the scaler reaches its 0.75 floor within a second or two of landing and\n//            has nothing left to shed, so this budget is what that floor must hold: 33.3 ms, and one frame in twenty\n//            allowed a refresh late.\n//\n// MEMORY HAS ONE BUDGET FOR EVERY CLASS: A LONG SESSION MUST NOT GROW. After each trip out of a system the heap (once\n// collected) and the GPU objects each live renderer holds come back to where they were, within a frame's garbage\n// (heapNoiseMB). The floor is why the growth allowed is zero rather than small: two gigabytes shared with two GL\n// contexts is spent by a leak long before an hour is up.\nexport const FRAME_BUDGETS = Object.freeze({\n  desktop: Object.freeze({ id: 'desktop', label: 'Laptop or desktop', hz: 60, frameMs: 1000 / 60, meanMs: FRAME_SCALER.shedAboveMs, p95Ms: 2000 / 60, preset: 'high' }),\n  phone:   Object.freeze({ id: 'phone', label: 'Recent phone', hz: 60, frameMs: 1000 / 60, meanMs: FRAME_SCALER.shedAboveMs, p95Ms: 2000 / 60, preset: 'medium' }),\n  floor:   Object.freeze({ id: 'floor', label: 'iPad Air 2 (the floor)', hz: 30, frameMs: 1000 / 30, meanMs: 1000 / 30, p95Ms: 2000 / 30, preset: 'low' }),\n});\nexport const MEMORY_BUDGET = Object.freeze({ heapGrowMBPerHour: 0, heapNoiseMB: 8, gpuGrowPerHour: 0 });\n\n// Which budget a device is held to. The game has never picked anything by device class (only by devicePixelRatio), and\n// this does not start: a touch screen is a phone unless its player has chosen Low, which is what Low is for; anything\n// with a mouse is a laptop or desktop, whatever it is set to.\nexport function budgetFor({ coarse, quality } = {}) {\n  if (!coarse) return FRAME_BUDGETS.desktop;\n  return qualityPreset(quality).id === 'low' ? FRAME_BUDGETS.floor : FRAME_BUDGETS.phone;\n}\n\n// The mean, 95th percentile and worst of a run of frame periods (ms), and the frame rate they amount to.\nexport function frameStats(periods) {\n  const v = (Array.isArray(periods) || ArrayBuffer.isView(periods) ? Array.from(periods) : [])\n    .filter((x) => typeof x === 'number' && Number.isFinite(x) && x > 0);\n  if (!v.length) return { n: 0, meanMs: 0, p95Ms: 0, maxMs: 0, fps: 0 };\n  const sorted = v.slice().sort((a, b) => a - b);\n  const mean = v.reduce((a, b) => a + b, 0) / v.length;\n  const p95 = sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)];\n  return { n: v.length, meanMs: mean, p95Ms: p95, maxMs: sorted[sorted.length - 1], fps: 1000 / mean };\n}\n\n// 'ok' holds the display's refresh (within a tenth), 'over' breaks the budget on either count, 'warn' is in between:\n// under the line, but the scaler would already be shedding resolution on foot.\nexport function judgeFrames(stats, budget) {\n  if (!stats || !stats.n || !budget) return 'none';\n  if (stats.meanMs > budget.meanMs || stats.p95Ms > budget.p95Ms) return 'over';\n  return stats.meanMs <= budget.frameMs * 1.1 ? 'ok' : 'warn';\n}\n\n// THE DEVELOPER READOUT'S LINES (Settings → Display, \"Frame time and memory\"), worked out here so they are tested rather\n// than eyeballed on a phone. `views` is one entry per live renderer ({ name, geometries, textures, programs }); `heapMB`\n// is null where the browser does not report the heap (Safari does not; Chrome does). The heap moves with every frame's\n// garbage, so the change since the readout was turned on says more about growth than any one reading.\nexport function perfReadoutLines({ stats, budget, heapMB, heapStartMB, views, dom, res } = {}) {\n  const f1 = (x) => (Math.round(x * 10) / 10).toFixed(1);\n  const lines = [{ level: judgeFrames(stats, budget),\n    text: stats && stats.n ? `FRAME ${f1(stats.meanMs)} ms  p95 ${f1(stats.p95Ms)}  ${Math.round(stats.fps)} fps` : 'FRAME measuring…' }];\n  // THE RESOLUTION BEING DRAWN, against the screen's own: the on-foot scaler moves it, and below the screen's density is\n  // what reads as blur. Shown so a tester can see the scaler work rather than infer it.\n  if (res && Number.isFinite(res.ratio)) lines.push({ level: 'info', text: `RES ${res.ratio.toFixed(2)}x of a ${+(res.dpr || 1).toFixed(2)}x screen` });\n  if (budget) lines.push({ level: 'info', text: `BUDGET ${budget.label}: ${f1(budget.frameMs)} ms, over past ${f1(budget.meanMs)} (p95 ${f1(budget.p95Ms)})` });\n  if (heapMB == null || !Number.isFinite(heapMB)) lines.push({ level: 'info', text: 'HEAP not reported by this browser' });\n  else {\n    const d = heapStartMB == null ? null : Math.round(heapMB - heapStartMB);\n    lines.push({ level: 'info', text: `HEAP ${Math.round(heapMB)} MB` + (d == null ? '' : ` (${d >= 0 ? '+' : ''}${d} since on)`) });\n  }\n  for (const v of views || []) lines.push({ level: 'info', text: `${String(v.name).toUpperCase()} geo ${v.geometries} tex ${v.textures} prog ${v.programs}` });\n  if (dom != null) lines.push({ level: 'info', text: `DOM ${dom} elements` });\n  return lines;\n}\n\n// ---- KEYS ---------------------------------------------------------------------------------------------------------\n// A binding is a KeyboardEvent.code — the key's POSITION — never the character it types. That is what makes an AZERTY\n// keyboard walk on Z Q S D, the keys that sit where W A S D sit on the keyboard the defaults were chosen on, which is\n// what every AZERTY player expects. Reading e.key, as the game did before, scattered the four movement keys across\n// three rows there. The labels are the other half of that: codeLabel names a key by what is PRINTED on it whenever the\n// browser can say (its layout map, or a key the player has pressed), so the guide never tells an AZERTY player \"W\".\n//\n// `modes` says where an action listens: 'foot' on a world's surface, 'fly' everywhere else (the galaxy, a system, the\n// asteroid flyer). Two actions may share a key only if they never listen in the same place, which is how V was the\n// pulse on foot and scout in flight until the pulse was removed (the owner, Oct 7); V is free on foot now. `altOk` marks the held movement keys, which have never cared whether Alt\n// was down; every other action leaves Alt, like Ctrl and Cmd, to the browser and the system. `group` only orders the\n// Settings list.\nconst FOOT = Object.freeze(['foot']), FLY = Object.freeze(['fly']), BOTH = Object.freeze(['foot', 'fly']);\nconst act = (id, label, keys, modes, group, altOk) =>\n  Object.freeze({ id, label, keys: Object.freeze(keys.slice()), modes, group, altOk: !!altOk });\nexport const KEY_ACTIONS = Object.freeze([\n  act('forward',     'Move forward',                    ['KeyW'],                  BOTH, 'move', true),\n  act('back',        'Move back',                       ['KeyS'],                  BOTH, 'move', true),\n  act('left',        'Move left',                       ['KeyA'],                  BOTH, 'move', true),\n  act('right',       'Move right',                      ['KeyD'],                  BOTH, 'move', true),\n  act('jump',        'Jump',                            ['Space'],                 BOTH, 'move', true),\n  act('crouch',      'Crouch (hold)',                   ['ShiftLeft', 'ShiftRight'], BOTH, 'move', true),\n  act('zoom',        'Zoom in (hold)',                  ['KeyC'],                  BOTH, 'move', true),\n  act('use',         'Use a bench or machine',          ['KeyF'],                  FOOT, 'act'),\n  // 'pulse' (V on foot: show buried deposits) was here until the owner removed it, Oct 7. A saved keymap that still\n  // names it loads clean: keymapFrom reads only the actions listed here, so the entry is ignored, and the next save\n  // (keymapOverrides) leaves it out.\n  act('inventory',   'Backpack and hand crafting',      ['KeyE'],                  BOTH, 'act'),\n  act('wave',        'Wave',                            ['KeyH'],                  BOTH, 'act'),\n  act('leave',       'Back up to orbit',                ['KeyL'],                  FOOT, 'act'),\n  // TALKING (the owner's play review, Oct 23, f9), on T since Oct 26 (the owner: \"change the mapping of chat to keyboard key\n  // T. Remove trading altogether\"), everywhere: messages reach friends and groups from orbit too (Chat and DMs picks, C2).\n  // Trade (T) and the emoji wheel (R) are gone (C14: \"remove the separate emojis entirely\"); a saved binding for either is ignored.\n  act('chat',        'Chat and messages',               ['KeyT'],                  BOTH, 'act'),\n  // THE COMMAND BAR'S NEW KEY (the owner's pick of Oct 24, D1: \"Q = drop one of the held stack\"): Q was free on foot and in flight.\n  act('drop',        'Drop one of the held stack',      ['KeyQ'],                  FOOT, 'act'),\n  // ON FOOT ONLY since the navigation keys (the owner's Navigation Overhaul, Oct 7: \"/ search\"). In flight \"/\" opens\n  // Search; the asteroid flyer frees its pointer on the same key (system-view _oflyKey), which a panel opening needs\n  // anyway, and Esc frees it everywhere.\n  act('freePointer', 'Free the pointer',                ['Slash'],                 FOOT, 'act'),\n  act('scout',       'Scout: look without travelling',  ['KeyV'],                  FLY,  'act'),\n  // Delete AND Backspace, as it has always been: the key a Mac prints \"delete\" on sends Backspace.\n  // UP A LEVEL since the owner's Level Cards picks (Oct 8: \"back it only delete/backspace. Not escape.\"): the card's chain,\n  // a moon to its planet and on to the universe; on foot, back up to orbit. The id stays, so a saved binding keeps working.\n  act('galaxy',      'Up a level',                      ['Delete', 'Backspace'],   BOTH, 'act'),\n  act('friends',     'Friends online (hold)',           ['Tab'],                   BOTH, 'panel'),\n  // The card's Info (the owner's World Info picks, Oct 8: the info panel is gone; remove=key, I opens and closes Info).\n  act('info',        'Info about the selection',        ['KeyI'],                  BOTH, 'panel'),\n  // The world card, open or folded to its ⓘ (the owner's World Card Placement picks, Oct 8: reopenKey). J was free in\n  // flight and on foot; I opens the card's Info.\n  act('card',        'World card: show or hide',        ['KeyJ'],                  BOTH, 'panel'),\n  act('feedback',    'Send feedback',                   ['KeyB'],                  BOTH, 'panel'),\n  // Kubipedia, at the item under the pointer when there is one (the menu-panels plan, Oct 6: \"Item look-up + K key\").\n  // K was free in both places, so no player's existing choice can collide with it.\n  act('kubipedia',   'Kubipedia (look up what you point at)', ['KeyK'],          BOTH, 'panel'),\n  // THE NAVIGATION SET (the owner's Navigation Overhaul, Oct 7: keys = \"A full navigation set\"), in flight: the galaxy,\n  // a system and the universe. Every one was free there: F is Use on foot only, O, N, M, Enter, the arrows, + and -\n  // were bound to nothing, and \"/\" moved to on foot only above. Esc, which goes up a level in flight now, is reserved\n  // (reservedReason) and so is not listed: it is the one way out of every panel. Settings, which Esc opened, has a key\n  // of its own. Home and Recentre, cycling and Reset view are not here: the owner kept those as they are (home, cycle\n  // and tilt), so V is still the way home from scouting.\n  act('settings',    'Settings',                        ['KeyO'],                  BOTH, 'panel'),\n  act('search',      'Search places and people',        ['Slash'],                 FLY,  'nav'),\n  act('nearby',      'Nearby list (on the card)',       ['KeyN'],                  FLY,  'nav'),\n  // (Map, M, was here until the owner removed the map, Oct 26: \"remove the map functionality and buttons entirely\".)\n  act('frame',       'Frame the selection',             ['KeyF'],                  FLY,  'nav'),\n  act('travel',      'Travel to what you are looking at', ['Enter', 'NumpadEnter'], FLY, 'nav'),\n  act('orbitLeft',   'Turn the view left',              ['ArrowLeft'],             FLY,  'nav'),\n  act('orbitRight',  'Turn the view right',             ['ArrowRight'],            FLY,  'nav'),\n  act('orbitUp',     'Tilt the view up',                ['ArrowUp'],               FLY,  'nav'),\n  act('orbitDown',   'Tilt the view down',              ['ArrowDown'],             FLY,  'nav'),\n  act('zoomIn',      'Zoom in a step',                  ['Equal', 'NumpadAdd'],    FLY,  'nav'),\n  act('zoomOut',     'Zoom out a step',                 ['Minus', 'NumpadSubtract'], FLY, 'nav'),\n]);\nexport const KEY_GROUPS = Object.freeze([['move', 'Moving'], ['act', 'Actions'], ['nav', 'Getting around'], ['panel', 'Panels']]);\nconst ACTION_BY_ID = Object.create(null);\nfor (const a of KEY_ACTIONS) ACTION_BY_ID[a.id] = a;\nexport function keyActionById(id) { return (typeof id === 'string' && ACTION_BY_ID[id]) || null; }\n\nconst sharesMode = (a, b) => a.modes.some((m) => b.modes.includes(m));\nconst sameCodes = (a, b) => a.length === b.length && a.every((c) => b.includes(c));\n// Pressing either Shift binds both, the way the default does and the way the key is printed: nobody means \"the LEFT\n// Shift crouches and the right one does nothing\".\nconst TWIN = { ShiftLeft: 'ShiftRight', ShiftRight: 'ShiftLeft' };\n\n// Keys no action may take, and the reason a player is given. Esc is the one way out of every panel; the number row\n// picks hotbar slots (by the character, so the numeric keypad is kept too); Ctrl, Alt and the system key carry the\n// browser's and the system's own shortcuts, and a binding on one would turn every other key into a shortcut while it\n// was held (Ctrl+W closes the tab, and no page can stop it). The lock keys report a toggle, not a press and a release,\n// and the function keys are the browser's.\nexport function reservedReason(code) {\n  if (typeof code !== 'string' || !code || code === 'Unidentified') return 'That key cannot be read, so it cannot be bound.';\n  if (code === 'Escape') return 'Esc always closes a panel, the world card or Settings.';\n  if (/^(Digit|Numpad)[0-9]$/.test(code)) return 'The number keys choose hotbar slots.';\n  if (/^(Control|Alt|Meta|OS)(Left|Right)$/.test(code) || code === 'AltGraph') return 'Ctrl, Alt and the system key stay with the browser’s own shortcuts.';\n  if (/^(CapsLock|NumLock|ScrollLock|Fn|FnLock|ContextMenu)$/.test(code)) return 'That key cannot be held like a game key.';\n  if (/^F([1-9]|1[0-9]|2[0-4])$/.test(code)) return 'The function keys belong to the browser.';\n  return null;\n}\n\nconst CODE_RE = /^[A-Za-z][A-Za-z0-9]{0,31}$/;\nfunction validCodes(list) {\n  if (!Array.isArray(list) || !list.length || list.length > 2) return null;\n  const out = [];\n  for (const c of list) {\n    if (typeof c !== 'string' || !CODE_RE.test(c) || reservedReason(c) || out.includes(c)) return null;\n    out.push(c);\n  }\n  return out;\n}\n\n// The full keymap — every action's codes — from whatever the player has overridden, which may be nothing, may be\n// from an older version, and may have been edited by hand. Anything unreadable falls back to its default.\n//\n// A PLAYER'S CHOICE OUTRANKS A DEFAULT. Chosen bindings are placed first, so a default added in a later version can\n// never take a key the player already uses; the new action simply starts unbound, and Settings says \"Not bound\".\nexport function keymapFrom(overrides) {\n  const o = (overrides && typeof overrides === 'object' && !Array.isArray(overrides)) ? overrides : {};\n  const km = {};\n  const taken = (a, c) => KEY_ACTIONS.some((b) => b !== a && km[b.id] && sharesMode(a, b) && km[b.id].includes(c));\n  for (const a of KEY_ACTIONS) {\n    const mine = has(o, a.id) ? validCodes(o[a.id]) : null;\n    if (mine) km[a.id] = mine.filter((c) => !taken(a, c));\n  }\n  for (const a of KEY_ACTIONS) if (!km[a.id]) km[a.id] = a.keys.filter((c) => !taken(a, c));\n  const ordered = {};   // in the actions' own order whatever order they were placed in, so two equal keymaps read the same\n  for (const a of KEY_ACTIONS) ordered[a.id] = km[a.id];\n  return ordered;\n}\n// Frozen all the way down: it is handed out as a starting keymap, and an edit to it would move every player's defaults.\nexport const DEFAULT_KEYMAP = (() => {\n  const km = keymapFrom(null);\n  for (const id of Object.keys(km)) Object.freeze(km[id]);\n  return Object.freeze(km);\n})();\n\n// Only what differs from the defaults is stored, so a default that improves later reaches everyone who never chose.\nexport function keymapOverrides(km) {\n  const out = {};\n  for (const a of KEY_ACTIONS) {\n    const c = km && km[a.id];\n    if (Array.isArray(c) && c.length && !sameCodes(c, a.keys)) out[a.id] = c.slice();\n  }\n  return out;\n}\n\n// Bind `code` to an action, or refuse and say why. A key already held by an action that listens in the same place is\n// REFUSED, naming that action, rather than silently swapped or stolen: a swap would move a key the player was not\n// looking at, and they would find out mid-game. Rebinding an action to the key it already has is simply fine.\nexport function rebind(km, actionId, code) {\n  const a = keyActionById(actionId);\n  if (!a) return { ok: false, reason: 'unknown' };\n  const why = reservedReason(code);\n  if (why) return { ok: false, reason: 'reserved', why };\n  const codes = has(TWIN, code) ? [code, TWIN[code]] : [code];\n  for (const b of KEY_ACTIONS) {\n    if (b === a || !sharesMode(a, b)) continue;\n    const held = (km && km[b.id]) || [];\n    if (codes.some((c) => held.includes(c))) return { ok: false, reason: 'taken', other: b.id };\n  }\n  const next = {};\n  for (const b of KEY_ACTIONS) next[b.id] = ((km && km[b.id]) || []).slice();\n  next[a.id] = codes;\n  return { ok: true, keymap: next };\n}\n\n// THE SWAP, OFFERED, NEVER ASSUMED (QA F-53). rebind refuses a taken key for the reason it gives above, and the player\n// was left to go and free the key by hand: find the other action, give it something else, come back. The refusal now\n// carries a one-press way out, which this works out: `actionId` takes `code`, and every action that listens where it\n// does and held that key gives it up. Each of those keeps its other key if it had two (Up a level keeps Delete when\n// Backspace goes); otherwise it takes over the keys `actionId` had, minus any a third action already uses where it\n// listens; and if nothing is left it is Not bound, which Settings shows and says. Returns rebind's answer for anything\n// that is not a swap (a free key, a reserved one), so the caller can always call this. `moved` lists who changed, for\n// the line that says so.\nexport function swapBind(km, actionId, code) {\n  const r = rebind(km, actionId, code);\n  if (r.ok || r.reason !== 'taken') return r;\n  const a = keyActionById(actionId);\n  const codes = has(TWIN, code) ? [code, TWIN[code]] : [code];\n  const next = {};\n  for (const b of KEY_ACTIONS) next[b.id] = ((km && km[b.id]) || []).slice();\n  const old = next[a.id];\n  next[a.id] = codes;\n  const moved = [];\n  for (const b of KEY_ACTIONS) {\n    if (b === a || !sharesMode(a, b) || !next[b.id].some((c) => codes.includes(c))) continue;\n    const kept = next[b.id].filter((c) => !codes.includes(c));\n    next[b.id] = kept.length ? kept\n      : old.filter((c) => !KEY_ACTIONS.some((x) => x !== b && sharesMode(x, b) && next[x.id].includes(c)));\n    moved.push({ id: b.id, codes: next[b.id].slice() });\n  }\n  return { ok: true, keymap: next, moved };\n}\n\n// The code of a key event. Every browser the game supports fills e.code for a physical key; a virtual keyboard or a\n// synthetic event may not, and then the character stands in for the position, which is right on the layout the\n// defaults were chosen on and harmless elsewhere.\nconst KEY_TO_CODE = { ' ': 'Space', Shift: 'ShiftLeft', Tab: 'Tab', Enter: 'Enter', Delete: 'Delete', Backspace: 'Backspace',\n  Escape: 'Escape', '/': 'Slash', '.': 'Period', ',': 'Comma', ';': 'Semicolon', \"'\": 'Quote', '-': 'Minus', '=': 'Equal',\n  '[': 'BracketLeft', ']': 'BracketRight', '\\\\': 'Backslash', '`': 'Backquote',\n  ArrowUp: 'ArrowUp', ArrowDown: 'ArrowDown', ArrowLeft: 'ArrowLeft', ArrowRight: 'ArrowRight' };\nexport function eventCode(e) {\n  if (e && typeof e.code === 'string' && e.code) return e.code;\n  const k = e && e.key;\n  if (typeof k !== 'string' || !k) return '';\n  if (/^[a-z]$/i.test(k)) return 'Key' + k.toUpperCase();\n  if (/^[0-9]$/.test(k)) return 'Digit' + k;\n  return has(KEY_TO_CODE, k) ? KEY_TO_CODE[k] : '';\n}\n\n// The action a code triggers in a mode, or null. Conflicts are refused within a mode, so there is at most one.\nexport function actionFor(km, code, mode) {\n  if (!code || !km) return null;\n  for (const a of KEY_ACTIONS) if (a.modes.includes(mode) && (km[a.id] || []).includes(code)) return a.id;\n  return null;\n}\n\n// Typing in a field is never a game key: a name with a W in it must not walk the player off a ledge.\n// …BUT ONLY A FIELD THAT IS STILL ON SCREEN. A field keeps focus after its dialog closes (click into Settings' contact\n// box, then close Settings), and every key then read as typing: F, WASD and the rest were dead until a click elsewhere\n// (playtest, Oct 1). A field inside a closed dialog or a hidden panel lets go of focus and stops counting.\nexport function typingIn(t) {\n  if (!(t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable))) return false;\n  if (typeof t.closest === 'function' && t.closest('.modal-overlay:not(.open), .hidden')) {\n    try { t.blur(); } catch (e) { /* nothing to give back */ }\n    return false;\n  }\n  return true;\n}\n\n// The action a key EVENT triggers — the one question every key handler in the game asks.\n//\n// A KEYDOWN counts only outside a text field and without Ctrl or Cmd, which belong to the browser and to the game's\n// own copy-link shortcut (so Cmd+C still copies rather than zooming); Alt is refused too, except by the held movement\n// keys, which have never cared. A KEYUP always resolves, from anywhere and whatever is held, because a release that\n// goes astray leaves a key stuck down: a W held while a field takes focus must still let go when the finger lifts.\nexport function actionForEvent(km, e, mode) {\n  if (!e) return null;\n  const code = eventCode(e);\n  if (e.type === 'keyup') return actionFor(km, code, mode);\n  if (typingIn(e.target) || e.ctrlKey || e.metaKey) return null;\n  const id = actionFor(km, code, mode);\n  if (id && e.altKey && !ACTION_BY_ID[id].altOk) return null;\n  return id;\n}\n\n// Whether a code belongs to an action in ANY mode: the release test for a held key, which must let go wherever it is.\nexport function isBound(km, actionId, code) {\n  return !!(code && km && Array.isArray(km[actionId]) && km[actionId].includes(code));\n}\n\n// ---- KEY NAMES ----------------------------------------------------------------------------------------------------\nconst KEY_NAMES = { Space: 'Space', Tab: 'Tab', Enter: 'Enter', Backspace: 'Backspace', Delete: 'Delete', Insert: 'Insert',\n  Home: 'Home', End: 'End', PageUp: 'Page Up', PageDown: 'Page Down', ShiftLeft: 'Left Shift', ShiftRight: 'Right Shift',\n  ArrowUp: '↑', ArrowDown: '↓', ArrowLeft: '←', ArrowRight: '→',\n  Slash: '/', Backslash: '\\\\', IntlBackslash: '\\\\', Period: '.', Comma: ',', Semicolon: ';', Quote: \"'\", Backquote: '`',\n  BracketLeft: '[', BracketRight: ']', Minus: '-', Equal: '=',\n  NumpadEnter: 'Num Enter', NumpadAdd: 'Num +', NumpadSubtract: 'Num -', NumpadMultiply: 'Num *', NumpadDivide: 'Num /',\n  NumpadDecimal: 'Num .', NumpadEqual: 'Num =', PrintScreen: 'Print Screen', Pause: 'Pause' };\n// Two codes that together are the one key a player thinks of: both Shifts are \"Shift\", and Delete with Backspace is\n// \"Delete\", which is what a Mac prints on the key that sends Backspace and a PC on the one that sends Delete.\n// The zoom steps are the keys printed + and - (the main row's = and -, and the keypad's), and Travel is Enter on either.\nconst PAIRS = [[['ShiftLeft', 'ShiftRight'], 'Shift'], [['Delete', 'Backspace'], 'Delete'],\n               [['Equal', 'NumpadAdd'], '+'], [['Minus', 'NumpadSubtract'], '−'], [['Enter', 'NumpadEnter'], 'Enter']];\n\n// The codes whose printed character is worth learning: letters and punctuation, the keys that move between layouts.\n// Not the digits, which the hotbar reads by character and which most layouts print as digits anyway.\nconst LEARNABLE = /^(Key[A-Z]|Minus|Equal|BracketLeft|BracketRight|Backslash|Semicolon|Quote|Backquote|Comma|Period|Slash|IntlBackslash|IntlRo|IntlYen)$/;\n\n// A key's name, from what is printed on it when `layout` (code -> character) knows, else from the code itself.\nexport function codeLabel(code, layout) {\n  const printed = has(layout, code) ? layout[code] : null;\n  if (typeof printed === 'string' && printed.length === 1 && printed.trim()) {\n    const up = printed.toUpperCase();\n    return up.length === 1 ? up : printed;   // \"ß\" upper-cases to \"SS\", which is not what the key says\n  }\n  if (typeof code !== 'string' || !code) return '?';\n  if (/^Key[A-Z]$/.test(code)) return code.slice(3);\n  if (/^Digit[0-9]$/.test(code)) return code.slice(5);\n  if (/^Numpad[0-9]$/.test(code)) return 'Num ' + code.slice(6);\n  return has(KEY_NAMES, code) ? KEY_NAMES[code] : code;\n}\n\n// A binding's name as the Settings list and How to play show it.\nexport function bindingLabel(codes, layout) {\n  const rest = Array.isArray(codes) ? codes.slice() : [];\n  if (!rest.length) return 'Not bound';\n  const names = [];\n  for (const [pair, name] of PAIRS) {\n    if (!pair.every((c) => rest.includes(c))) continue;\n    names.push(name);\n    for (const c of pair) rest.splice(rest.indexOf(c), 1);\n  }\n  for (const c of rest) names.push(codeLabel(c, layout));\n  return names.filter((n, i) => names.indexOf(n) === i).join(' / ');\n}\n\n// Text that names keys by ACTION — \"Double-tap {forward} to sprint\" — filled from the live bindings. Braces around\n// anything that is not an action are left alone.\nexport function fillKeys(text, labelFor) {\n  return String(text == null ? '' : text).replace(/\\{([A-Za-z]+)\\}/g, (m, id) => (keyActionById(id) ? labelFor(id) : m));\n}\n\n// Learn what is printed on a key from a press of it, so labels are right on browsers with no layout map (Safari,\n// Firefox). Only a bare press outside a text field teaches anything: Shift prints a different character, and what a\n// player types into a field is none of this code's business. Returns the new label map, or null if nothing was learnt.\nexport function learnLabel(labels, e) {\n  if (!e || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || typingIn(e.target)) return null;\n  const code = e.code, key = e.key;\n  if (typeof code !== 'string' || !LEARNABLE.test(code) || typeof key !== 'string' || key.length !== 1 || !key.trim()) return null;\n  if (has(labels, code) && labels[code] === key) return null;\n  const next = Object.assign({}, labels);\n  next[code] = key;\n  return next;\n}\n// Only learnable codes with a single printable character each, from storage or from a browser's layout map: anything\n// else in there is dropped rather than shown as a key's name.\nexport function sanitizeLabels(raw) {\n  const out = {};\n  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;\n  for (const code of Object.keys(raw).slice(0, 64)) {\n    const k = raw[code];\n    if (LEARNABLE.test(code) && typeof k === 'string' && k.length === 1 && k.trim()) out[code] = k;\n  }\n  return out;\n}\n\n// ---- SOUND (EXP-03; the owner's Sound Bench picks, Oct 21) ----------------------------------------------------------\n// ON, WITH THE MUSIC OFF (the owner, Oct 21: \"Effects on, music off\" at launch). Nothing is downloaded until the first\n// press of a visit (app.js loads sound.mjs then, inside the gesture a browser asks for). A player who has CHOSEN keeps\n// the choice, because only an absent setting is the default (soundFrom): anyone who turned sound off is still off.\nexport const SOUND_ON_DEFAULT = true;\n// Music, effects and ambience are set apart because players want them apart (e_sliders 1). Music starts at nothing:\n// it is there to be turned up, and at zero none of it is written. Ambience (wind, water, lava, towns) sits under the\n// effects, which only play when something happens.\nexport const SOUND_VOL = Object.freeze({ min: 0, max: 1, step: 0.05, music: 0, fx: 0.8, amb: 0.6 });\n\n// A stored volume, on the slider's own steps, or the default when there is nothing readable.\nexport function clampVol(v, def) {\n  const n = typeof v === 'number' ? v : parseFloat(v);\n  if (!Number.isFinite(n)) return def;\n  const per = Math.round(1 / SOUND_VOL.step);\n  return Math.round(Math.min(SOUND_VOL.max, Math.max(SOUND_VOL.min, n)) * per) / per;\n}\n// On ('1'), off ('0'), or never chosen, which is the default. Off is STORED rather than removed for exactly that\n// reason: a removed key would read as \"never chosen\" and turn on for this player the day the default does.\nexport function soundFrom(raw, def = SOUND_ON_DEFAULT) {\n  return raw === '1' ? true : raw === '0' ? false : !!def;\n}\n\n// ---- STORAGE ------------------------------------------------------------------------------------------------------\n// Per DEVICE, in localStorage, like the FOV slider these join: a mouse, a keyboard layout and a GPU belong to the\n// machine rather than to the account, and a new field on the user document would need a rules change to be written.\n// Sound is per device for the same reason and one more: a phone in a quiet room and a desktop with speakers are the\n// same player wanting different things. 'fov' keeps the key it has always had, so a width chosen before this change is\n// still the one in use.\nexport const STORE_KEYS = Object.freeze({ sens: 'kubicor.lookSens', invertY: 'kubicor.invertY', fov: 'fov',\n  quality: 'kubicor.quality', keys: 'kubicor.keys', labels: 'kubicor.keyLabels', perf: 'kubicor.perfReadout',\n  sound: 'kubicor.sound', musicVol: 'kubicor.musicVol', fxVol: 'kubicor.fxVol', ambVol: 'kubicor.ambVol',\n  textSize: 'kubicor.textSize', reduceMotion: 'kubicor.reduceMotion', stickSide: 'kubicor.stickSide', touchSize: 'kubicor.touchSize',\n  touchMirror: 'kubicor.touchMirror' });\n\n// ---- ACCESSIBILITY AND TOUCH (the menu-panels plan, Oct 6) -----------------------------------------------------------\n// TEXT SIZE scales the root font size, and with it everything the panels and the HUD size in rem; 100% is the game as it\n// has always been. REDUCE MOTION is the game's own switch for what the system setting already does (motion.mjs): a\n// player can ask for less motion here without changing their whole device. TOUCH: the left-handed MIRROR of the on-foot\n// layout (the owner's mobile-controls pick, Oct 6, which replaced a stick-side switch: 'kubicor.stickSide' = 'right'\n// still reads as mirrored until the mirror itself is chosen), and how big the round buttons are. Per device, like\n// everything else in this file.\nexport const TEXT_SIZES = Object.freeze([100, 115, 130]);\nexport const TOUCH_SIZES = Object.freeze([['s', 'S'], ['m', 'M'], ['l', 'L']]);\nexport const clampTextSize = (v) => { const n = parseInt(v, 10); return TEXT_SIZES.includes(n) ? n : 100; };\n// '1' on, '0' off; never chosen falls back to the old stick-side key, so a player who had put the stick on the right\n// still has it there.\nexport const touchMirrorFrom = (raw, legacyStick) => raw === '1' ? true : raw === '0' ? false : legacyStick === 'right';\n\n// Everything, read back through `get` (key -> string or null). A value that is ABSENT or unreadable is the default,\n// which is the whole reason a default can be set here without resetting anybody.\nexport function readControls(get, dev) {\n  const read = (k) => { try { const v = get(k); return v == null ? null : String(v); } catch (e) { return null; } };\n  const json = (k) => { try { return JSON.parse(read(k) || 'null'); } catch (e) { return null; } };\n  const sensRaw = read(STORE_KEYS.sens);\n  const q = read(STORE_KEYS.quality);\n  const stored = json(STORE_KEYS.keys);\n  return {\n    sens: sensRaw === null ? LOOK_SENS.def : clampSens(sensRaw),\n    invertY: read(STORE_KEYS.invertY) === '1',\n    fov: fovFromStored(read(STORE_KEYS.fov)),\n    quality: has(QUALITY_PRESETS, q) ? q : qualityDefaultFor(dev),\n    keys: keymapFrom(stored && stored.v === 1 ? stored.keys : null),\n    labels: sanitizeLabels(json(STORE_KEYS.labels)),\n    perf: read(STORE_KEYS.perf) === '1',   // the developer readout (Settings → Display), off unless this device turned it on\n    sound: soundFrom(read(STORE_KEYS.sound)),                        // Settings → Sound: off until chosen (SOUND_ON_DEFAULT)\n    musicVol: clampVol(read(STORE_KEYS.musicVol), SOUND_VOL.music),\n    fxVol: clampVol(read(STORE_KEYS.fxVol), SOUND_VOL.fx),\n    ambVol: clampVol(read(STORE_KEYS.ambVol), SOUND_VOL.amb),\n    textSize: clampTextSize(read(STORE_KEYS.textSize)),\n    reduceMotion: read(STORE_KEYS.reduceMotion) === '1',\n    touchMirror: touchMirrorFrom(read(STORE_KEYS.touchMirror), read(STORE_KEYS.stickSide)),\n    touchSize: ['s', 'l'].includes(read(STORE_KEYS.touchSize)) ? read(STORE_KEYS.touchSize) : 'm',\n  };\n}\n\n// What to store for a keymap, or null when it is all defaults (the caller removes the key, and \"Reset to defaults\"\n// leaves nothing behind).\nexport function keysToStore(km) {\n  const o = keymapOverrides(km);\n  return Object.keys(o).length ? JSON.stringify({ v: 1, keys: o }) : null;\n}\n"],"mappings":"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","names":[],"file":"settings-controls.mjs"}