{"version":3,"sources":["whereabouts.mjs"],"sourcesContent":["// WHEREABOUTS — where a player IS, as distinct from what their camera is looking at.\n//\n// NOT TO BE CONFUSED WITH presence/{uid}, which already exists and is a different thing wearing a similar word.\n// That is a CLIENT-WRITTEN HEARTBEAT of \"the world the player is on or focused on\", refreshed every few seconds so\n// peers can be drawn and WebRTC can find them; its own comment in lootStash says plainly that it \"is not a proof of\n// location and is not claimed to be one\". This is the opposite in every respect: SERVER-WRITTEN, exact, changed\n// only by a recorded move, and never refreshed by a heartbeat. One is where to draw somebody; this is where they\n// are entitled to act.\n//\n// Those were one thing until now. `currentPosition` was rewritten every time the camera settled on something:\n// selecting a star wrote it, focusing a planet inside a system wrote it, pulling back to the galaxy wrote it. So\n// \"where I am\" meant \"what is on screen\", and every question that depends on being somewhere — may I build here,\n// may I land, may I open that container — had nothing to ask.\n//\n// WHEREABOUTS ARE WHAT TRAVEL MOVES. It changes on exactly the acts that cost something or commit you: diving into a\n// system, crossing to another galaxy, landing on a world, and leaving one. Looking is free and moves nothing.\n//\n// GRANULARITY: A SYSTEM, AND A WORLD ONCE YOU LAND. That is not a shrug at precision, it is the line the energy\n// model already draws — the two priced moves (COST_SYSTEM_HOP, COST_GALAXY_CROSSING) are exactly the two that\n// relocate you between systems, and landing is already a deliberate act with a control of its own. Making every\n// body its own location would need a price for moving between planets that does not exist.\n//\n// THIS IS A GAMEPLAY GATE, NOT AN ANTI-CHEAT BOUNDARY, and the distinction is worth stating plainly because the\n// server enforces it anyway. A crafted client can claim to be anywhere, exactly as it can under-report a travel\n// distance (see spendEnergy). What that buys is building on a world it ALREADY OWNS without flying there —\n// canBuildWorld still gates ownership, submitEdits still re-derives every cell from the world seed, and the energy\n// and burst limits are untouched. Nothing is minted. The check is here because the game reads better when being\n// somewhere means something, and it belongs on the server so the rule is the same for everyone.\n\n// The grammar submitEdits validates a worldAddr with — dotted integers, each optionally carrying the a/b\n// binary-companion suffix — WIDENED BY ONE CASE: a transient's address ends in a `c<N>` segment\n// (parseTransientAddr: `galaxy.system.cK[ab]`). Whereabouts have to read those too, because mining an asteroid asks\n// which system it is in, and a grammar that rejected them answered null for every rock in the game.\n//\n// …AND BY A SECOND: A VESSEL (the owner's spec, Oct 12: player-built stations, and ships in phase 3). A vessel is a body a\n// player made, so it has no place in the generator's ladder of stars, planets and moons, and its address says so:\n// `galaxy.system.v<id>`, the system it orbits in and the number of its record (vessels/{id}). One shape only, three\n// segments, a `v` and a whole number with no leading zero, so it can never be mistaken for a planet or a framing.\n// The host body it circles is on its record, not in its address: a ship that travels (phase 3) changes system, and\n// the id is what stays the same.\nexport const VESSEL_ADDR_RE = /^[0-9]+\\.[0-9]+\\.v[1-9][0-9]{0,11}$/;\nexport const ADDR_RE = /^(?:[0-9]+[ab]?(\\.c?[0-9]+[ab]?)*|[0-9]+\\.[0-9]+\\.v[1-9][0-9]{0,11})$/;\n\n// Is this a vessel's address? Strings only, as normAddr is below.\nexport function isVesselAddr(a) { return typeof a === 'string' && a.length <= 60 && VESSEL_ADDR_RE.test(a.trim()); }\n// The record id a vessel's address names ('875150.4240.v123' -> '123'), or null.\nexport function vesselIdOf(a) { return isVesselAddr(a) ? a.trim().slice(a.trim().lastIndexOf('.v') + 2) : null; }\n\n// STRINGS ONLY. An address arriving as a number coerces to a perfectly valid galaxy id — `3` becomes '3' — which\n// means a caller that passed the wrong variable gets a plausible answer instead of a null, and the mistake surfaces\n// somewhere else entirely. This is a gate; it should be hard to feed by accident.\nexport function normAddr(a) {\n  if (typeof a !== 'string') return null;\n  const s = a.trim();\n  return (s && s.length <= 60 && ADDR_RE.test(s)) ? s : null;\n}\n\n// The system an address belongs to: galaxy and star, and nothing below them. A galaxy on its own has no system,\n// which is a real state — you are between stars, in the universe view — and is why this can answer null.\nexport function systemOf(addr) {\n  const a = normAddr(addr);\n  if (!a) return null;\n  const p = a.split('.');\n  return p.length >= 2 ? p[0] + '.' + p[1] : null;\n}\n\nexport function galaxyOf(addr) {\n  const a = normAddr(addr);\n  return a ? a.split('.')[0] : null;\n}\n\n// A TRAILING .0 IS A FRAMING, NOT A PLACE. The renderer uses `X.0` for \"pulled back to show X's moons\", so\n// `664289.14.0` is the system seen whole and `664289.14.3.0` is a planet seen with its satellites. Neither is\n// somewhere you can stand, and canEnterStar already refuses to dive into one — so presence strips it.\nexport function bodyOf(addr) {\n  let a = normAddr(addr);\n  if (!a) return null;\n  while (a.endsWith('.0')) a = a.slice(0, -2);\n  const p = a.split('.');\n  if (p.length < 3) return null;            // galaxy, or galaxy.star, is not a body\n  // …and neither is a TRANSIENT. An asteroid is mined from a flyer and despawns at the end of its single pass, so\n  // it is never somewhere a player is; treating one as a body would let presence be stored on an object that stops\n  // existing a few minutes later.\n  if (p.some((seg) => seg.charCodeAt(0) === 99)) return null;   // 'c'\n  // A VESSEL IS one (Oct 12), though since Oct 13 nobody stands on it: a player's whereabouts are a vessel only while they\n  // are DOCKED at one of its Suit Docks (users/{uid}.docked), riding it. Its record outlives any one visit, so presence and\n  // whereabouts may be stored on it. It has no framing (`.0` never follows a `v<id>`).\n  return a;\n}\n\n// Is this address somewhere a player could stand — i.e. would landing there be meaningful?\nexport function isLandable(addr) { return bodyOf(addr) != null; }\n\nexport function sameSystem(a, b) {\n  const x = systemOf(a), y = systemOf(b);\n  return !!x && x === y;\n}\n\n// STANDING ON THE SAME THING. Framings collapse, so a player landed on `g.s.3` may act on `g.s.3` and on the\n// framing `g.s.3.0` that shows it with its moons — they are the same rock seen two ways.\nexport function atSameBody(a, b) {\n  const x = bodyOf(a), y = bodyOf(b);\n  return !!x && x === y;\n}\n\n// WHERE THIS USER IS, from a user doc — and the reason it has a fallback.\n//\n// `whereabouts` is a new field, so every account that existed before this shipped has none. Reading that as \"nowhere\"\n// would lock every one of them out of building, mining and looting the moment it deployed, on worlds they own, with\n// no way to fix it except travelling somewhere. So an account without a presence falls back to `currentPosition`,\n// which is the best evidence available of where they were — it followed the camera, so it is at worst the last\n// thing they looked at, and it is corrected by the very first move they make.\n//\n// The fallback is deliberately NOT a permanent second source of truth: whereaboutsAfter writes the real field on every\n// move, so an account uses it exactly once.\nexport function whereaboutsOf(user) {\n  const u = user || {};\n  // Collapsed on the way OUT as well as on the way in. presenceAfter can never write a framing, but a hand-edited\n  // doc or an older write could hold one, and a gate that compares raw strings would then refuse a player standing\n  // exactly where the field says they are.\n  const p = normAddr(u.whereabouts);\n  if (p) return bodyOf(p) || systemOf(p) || p;\n  const c = normAddr(u.currentPosition);\n  return c ? (bodyOf(c) || systemOf(c) || c) : null;\n}\n\n// ---- WHAT WHEREABOUTS GATE ---------------------------------------------------------------------------------------\n// Each of these answers one question, and each is asked in exactly one place on the client (for the affordance)\n// and one on the server (for the rule), so the two cannot drift into disagreeing about what \"being there\" means.\n\n// Editing a world — placing and breaking — requires standing on that world. Building already happens on foot, so\n// this mostly formalises what the walk state implies; what it actually forbids is the case that WAS reachable,\n// where a player flew the camera to a neighbouring planet and edited it without ever going.\n//\n// …EXCEPT A VESSEL, WHICH IS WORKED FROM OUTSIDE (the owner, Oct 13: \"eliminate the concept of insides\"). Nobody stands\n// on or in a vessel: it is built from the BUILD FLYER round its hull, as an asteroid is mined from the mining flyer, and\n// its docks, crates and benches are used from there too. So the gate is the transient's, its SYSTEM: a player anywhere in\n// it (in orbit, on a world of it, docked at this vessel or another) may build on a vessel they build on, and use its\n// parts within reach. Who may build is still the vessel's own list (vessels.mjs canBuildVessel).\nexport function canEditAt(at, worldAddr) { return isVesselAddr(worldAddr) ? sameSystem(at, worldAddr) : atSameBody(at, worldAddr); }\n\n// Looting a death container is the same question: you have to be on the rock the crate is on.\nexport function canLootAt(at, worldAddr) { return atSameBody(at, worldAddr); }\n\n// An asteroid or a comet is mined from a flyer, not from its surface — and A PASSING ROCK IS A SHIP DESTINATION (the owner's\n// Travel Routes picks, Oct 28, A2 \"Always a ship\": \"Rocks are ship destinations like any other body\"). So the requirement\n// is being DOCKED at a vessel parked round that very rock: `at` a vessel's address, `hostOfAt` the body that vessel circles\n// (its record's `host`, which the caller reads). Until Oct 28 it was the system: anyone anywhere in it could mine every rock.\nexport function canMineTransientAt(at, transientAddr, hostOfAt) {\n  return isVesselAddr(normAddr(at) || '') && sameSystem(at, transientAddr) && typeof hostOfAt === 'string' && hostOfAt === normAddr(transientAddr);\n}\n\n// Landing is the move that puts you on a body, so it needs you already in that system — and it needs the target to\n// be somewhere you can stand, which a framing is not.\nexport function canLandAt(at, worldAddr) {\n  return isLandable(worldAddr) && sameSystem(at, worldAddr);\n}\n\n// IS THE CAMERA LOOKING AT WHERE YOU ARE? The one question the VIEWING badge asks, and the one the dock's\n// build glow asks, so they cannot disagree about whether you are here. Both sides of the comparison are read at the\n// granularity whereabouts use, which is what makes \"at the star, looking at a planet\" come out as NOT here — the\n// distinction the whole feature exists to draw.\nexport function isHere(at, addr) {\n  const a = normAddr(at), b = normAddr(addr);\n  if (!a || !b) return false;\n  return (bodyOf(b) || systemOf(b) || b) === a;\n}\n\n// HOW PRESENT ARE YOU AT THIS ADDRESS? Three answers, because the gates above already have three tiers and the\n// UI only ever had two — so the one state in the middle had to borrow a costume from one of its neighbours, and it\n// borrowed the wrong one. Framing a planet from the star you are at came out as \"not here\", which put up the\n// VIEWING badge and packed the hotbar away, in a state where you can still mine the system's transients and land\n// on that very planet for nothing.\n//\n//   'here'    the camera is on exactly where you are. If that is a body you are standing on, you may build and loot.\n//   'orbit'   the same system, framing something else in it. Free to reach: landing costs nothing and you are\n//             already close enough to mine its asteroids and comets. Not a place you need warning about.\n//   'viewing' another system, or another galaxy. Nothing is permitted, and getting there has a price.\n//\n// Derived, never stored. Orbit grants nothing that being in the system does not already grant, so there is no third\n// thing for the server to remember and no new field for a rule to have to allow.\nexport function presenceAt(at, addr) {\n  const a = normAddr(at), b = normAddr(addr);\n  if (!a || !b) return 'viewing';          // unknown whereabouts permits nothing, which is the safe way to be wrong\n  if (isHere(a, b)) return 'here';\n  // PULLING BACK IS NOT LEAVING. If the camera's subject CONTAINS where you are — the galaxy you are standing in,\n  // the system you are orbiting — then you are inside the thing being framed and there is nowhere to travel to.\n  // isHere compares at one granularity, so zooming out from a star to its galaxy came out as 'viewing': measured\n  // live, standing on 930718.1 and framing 930718 shrank the avatar to its away size, put up the VIEWING badge and\n  // slid the hotbar off screen, for a player who had not moved an inch.\n  if (a === b || a.startsWith(b + '.')) return 'here';\n  return sameSystem(a, b) ? 'orbit' : 'viewing';\n}\n// The one question the badge and the hotbar ask: is this somewhere you would have to TRAVEL to reach?\nexport function mustTravelTo(at, addr) { return presenceAt(at, addr) === 'viewing'; }\n\n// ---- WHAT KIND OF MOVE IS THIS ----------------------------------------------------------------------------------\n// One reading of a pair of addresses, so the price, the avatar and the label cannot disagree about what is about to\n// happen. Returns null when there is nothing to move.\n//\n//   'galaxy'  another galaxy entirely       COST_GALAXY_CROSSING, priced by lattice cells\n//   'system'  another star in this galaxy   COST_SYSTEM_HOP, priced by parsecs\n//   'land'    onto a body in this system    free: you are already here\n//   'leave'   back off a body to its system  free, for the same reason\n//   null      you are already there\nexport function moveKind(from, to) {\n  const f = normAddr(from), t = normAddr(to);\n  if (!t) return null;\n  if (!f) return systemOf(t) ? 'system' : 'galaxy';        // arriving from nowhere (first load) is a system entry\n  if (galaxyOf(f) !== galaxyOf(t)) return 'galaxy';\n  if (!sameSystem(f, t)) return systemOf(t) ? 'system' : 'galaxy';\n  const fb = bodyOf(f), tb = bodyOf(t);\n  if (fb === tb) return null;                               // same body, or both at system level\n  return tb ? 'land' : 'leave';\n}\n\n// Where a player IS after a move of that kind — the one place that decides what gets stored, so a 'land' cannot\n// accidentally write a framing and a 'leave' cannot accidentally keep a body.\nexport function whereaboutsAfter(from, to) {\n  const kind = moveKind(from, to);\n  if (kind === null) return normAddr(from) ? (bodyOf(from) || systemOf(from) || normAddr(from)) : null;\n  if (kind === 'leave') return systemOf(to) || normAddr(to);\n  return bodyOf(to) || systemOf(to) || normAddr(to);\n}\n"],"mappings":"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","names":[],"file":"whereabouts.mjs"}