{"version":3,"sources":["travel.mjs"],"sourcesContent":["// WHAT A TRIP COSTS, DECIDED WHERE IT CAN BE TRUSTED.\n//\n// spendEnergy is the server's authority over whereabouts — it debits the fare and relocates the player in one\n// transaction — but it took the FARE from the client and only clamped it:\n//\n//     const amount = Math.min(TRAVEL_MAX, Math.max(0, Number(request.data.amount) || 0));\n//\n// and whereaboutsAfter(from, to) normalises any destination without asking whether the move was legal. So a client\n// that sent { amount: 0, to: <anywhere> } was moved there for nothing. That voids the energy economy, the TRAVEL_MAX\n// range, and every gate that depends on where a player is — mining, building, looting, the lot — which matters a\n// great deal more once tokens can be bought with money.\n//\n// The arithmetic was never the hard part: it is a handful of pure functions over galaxy-shape, which is already a\n// byte-identical twin across the client and the Functions precisely so that both sides agree about where things\n// are. What was missing was the server USING it.\n//\n// WHY THE SHAPE MODULE ARRIVES AS AN ARGUMENT. The twins that back this file — energy.mjs and whereabouts.mjs —\n// have the same name on both sides, so this file can import them and stay byte-identical. galaxy-shape does not:\n// it is public/galaxy-shape.js on one side and functions/lib/galaxy-shape.mjs on the other, and an import of either\n// name would make the two copies differ. So it is passed in as `G`, which is the convention share.js already uses\n// for the same reason (renderGalaxy takes `G: GX`).\nimport { travelCost } from './energy.mjs';\nimport { moveKind, normAddr, galaxyOf } from './whereabouts.mjs';\nimport { fareFactor } from './cosmos-kind.mjs';   // a black hole's slingshot, a nebula's drag (Oct 27)\n\n// The twin of app.js's getGalaxyDiameter, ROUNDING INCLUDED. That one returns a fixed(2) STRING which the client\n// parseFloats, so the price a player is quoted is computed from a 2-decimal diameter. Rounding differently here\n// would make the server disagree with the quote by a hair on every trip — and a fare that is a hair higher than\n// the quote is a refusal the player cannot explain.\nexport function galaxyDiameterPc(starCount) {\n  const d = 10 * Math.sqrt(Number(starCount || 0) / 325);\n  return Number(Math.min(100, Math.max(10, d)).toFixed(2));\n}\n\n// WHERE ONE STAR IS, in galaxy-radius units. The same three lines buildSilhouette uses per sample — same seed\n// string, same call order, same vertical spread — because the star a player clicked IS one of those samples and\n// the two have to land in the same place.\nexport function starPoint(G, galaxy, index) {\n  const shape = G.shapeForGalaxy(galaxy);\n  const s = G.shapeSeed(galaxy, shape);\n  const N = G.starsIn(galaxy);\n  const rnd = G.seeded(galaxy + '.' + index + '.place');\n  const p = G.place(shape, index, N, rnd, s);\n  return [p[0], (rnd() * 2 - 1) * 0.033, p[1]];\n}\n\n// PARSECS BETWEEN TWO STARS. The client measures in the galaxy's local units and multiplies by PC_PER_UNIT, which\n// is diameterPc / 2 / totalRadius; place() already returns those units divided by totalRadius, so the radius\n// cancels and what is left is the normalised distance times the half-diameter.\n// WHERE AN ADDRESS SITS INSIDE ITS GALAXY. A null index is not a missing argument, it is a real place: a bare\n// galaxy is where a crossing LEAVES you, standing between the stars, and its point is the centre.\n//\n// This is not a choice made here — it is the client's, copied. hopMeasure already says it out loud: \"Arriving\n// from another galaxy, there is no system here to leave from — so the trip is measured from the galaxy's centre,\n// which is where a crossing drops you\", and sets _hopA to (0,0,0). The server refused the same trip as\n// unpriceable, so the player was quoted a fare, charged it optimistically, and then snapped back when\n// spendEnergy declined a move the client had already shown them the price of.\nconst pointIn = (G, galaxy, index) => (index == null ? [0, 0, 0] : starPoint(G, galaxy, index));\n\n// THE WAY A TRIP COMES IN to a system: from where it left (a null index: the galaxy's centre) less where it arrives, which is\n// the side of a nebula it pushes in through (cosmos-kind.mjs nebulaDrag).\nexport function bearingTo(G, galaxy, fromIndex, toIndex) {\n  const a = pointIn(G, galaxy, fromIndex), b = pointIn(G, galaxy, toIndex);\n  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];\n}\n\nexport function hopPc(G, galaxy, fromIndex, toIndex) {\n  const a = pointIn(G, galaxy, fromIndex), b = pointIn(G, galaxy, toIndex);\n  const d = Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);\n  return d * galaxyDiameterPc(G.starsIn(galaxy)) / 2;\n}\n\n// LATTICE CELLS BETWEEN TWO GALAXIES. neighbourOffset multiplies exactly this by homeRadius * CELL_SPACING, and\n// the client divides that back out again with CELLS_PER_UNIT — so this is the same number with the scaling taken\n// off both ends. The web offset is part of it: it is what makes the field a web rather than a grid, and leaving it\n// out would under-price every crossing by however far the two cells are jittered.\nexport function crossCells(G, fromGalaxy, toGalaxy) {\n  const a = G.galaxyCell(Number(fromGalaxy)), b = G.galaxyCell(Number(toGalaxy));\n  const wa = G.webOffset(a[0], a[1], a[2]), wb = G.webOffset(b[0], b[1], b[2]);\n  return Math.hypot(b[0] - a[0] + wb[0] - wa[0],\n                    b[1] - a[1] + wb[1] - wa[1],\n                    b[2] - a[2] + wb[2] - wa[2]);\n}\n\nconst sysIndexOf = (addr) => {\n  const p = String(normAddr(addr) || '').split('.');\n  if (p.length < 2) return null;\n  const n = parseInt(p[1], 10);\n  return Number.isFinite(n) ? n : null;\n};\n\n// THE ONE THE SERVER CALLS. Returns what this move costs from where the player actually is — never what they said\n// it costs. null means \"not a move that can be priced\", which the caller must treat as a refusal rather than as\n// free: an unpriceable move is one this function does not understand, and charging nothing for it is the bug.\nexport function fareFor(G, from, to) {\n  if (!normAddr(to)) return null;\n  // NOBODY IS ANYWHERE YET. A player with no whereabouts is arriving for the first time, and a first arrival is not\n  // a trip — chooseSpawn owns that decision and its own rules about where you may start.\n  if (!normAddr(from)) return 0;\n  const kind = moveKind(from, to);\n  // Already there, or moving WITHIN the system you are in: landing on a world and lifting back to orbit are the\n  // same journey's two halves and neither has ever been charged for.\n  if (kind === null || kind === 'land' || kind === 'leave') return 0;\n  if (kind === 'galaxy') {\n    const gf = galaxyOf(from), gt = galaxyOf(to);\n    if (!gf || !gt) return null;\n    // a black hole's slingshot, a nebula's drag (cosmos-kind.mjs); a crossing arrives at the galaxy's centre, so it comes in from there\n    const tiG = sysIndexOf(to), x = fareFactor(gf, sysIndexOf(from), gt, tiG, tiG == null ? null : bearingTo(G, gt, null, tiG));\n    if (String(gf) === String(gt)) return travelCost('galaxy', 0) * x;   // same galaxy, no system named: the base fare\n    return travelCost('galaxy', crossCells(G, gf, gt)) * x;\n  }\n  if (kind === 'system') {\n    const g = galaxyOf(to);\n    const fi = sysIndexOf(from), ti = sysIndexOf(to);\n    // ti IS required — a 'system' move names a system to arrive at, and one without an index is not a destination\n    // this function understands. fi is NOT: a player between the stars has no system to leave from, and that is a\n    // legal, reachable, extremely ordinary state, because it is the one every galaxy crossing ends in.\n    if (!g || ti == null) return null;\n    // …A BLACK HOLE'S SLINGSHOT HALVES IT, A NEBULA'S GAS ADDS HALF (cosmos-kind.mjs fareFactor, the owner's picks, Oct 27)\n    // …BY THE GAS THE ROUTE CROSSES (ND5, Oct 29), on the bearing it comes in on\n    return travelCost('system', hopPc(G, g, fi, ti)) * fareFactor(galaxyOf(from), fi, g, ti, bearingTo(G, g, fi, ti));\n  }\n  return null;\n}\n"],"mappings":"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","names":[],"file":"travel.mjs"}