// ── How a world position reaches a pixel ────────────────────────────────── // // PLAN.md §30.3, in the one place the page computes it. Rust's world is centred // on the origin, x east and z north; the picture is the world at a scale, with // an ocean margin around it that is measured in PIXELS and is not scaled (the // rig's 3000 map is 3000 × 0.5 + 2 × 500 = 2500 pixels). So: // // s = (width − 2 × margin) / worldSize // px = (x + worldSize / 2) × s + margin // py = (z + worldSize / 2) × s + margin measured UP from the bottom edge // // Up from the bottom because Leaflet's `CRS.Simple` has y growing north, which // is Rust's z — so the picture's bounds are `[[0, 0], [height, width]]` and a // position is `[py, px]` with no flip anywhere. // // The grid is the GAME's (D119): `gridCells` cells of `gridCellSize` metres per // side, lettered from the west and numbered from the north, `A0` at the // north-west corner. Both numbers come from the plugin, which asks the game's own // `MapHelper`; nothing here assumes a cell size. // // Pure, and free of Leaflet, so it is tested in Node. /** Pixels per metre, or 0 for a geometry that cannot place anything. */ export function scaleOf(g) { if (!g || !(g.worldSize > 0) || !(g.width > 0)) return 0 return (g.width - 2 * (g.oceanMargin || 0)) / g.worldSize } /** A world position as `[lat, lng]` in the map's pixel space. */ export function toLatLng(g, x, z) { const s = scaleOf(g) const half = g.worldSize / 2 const m = g.oceanMargin || 0 return [(Number(z) + half) * s + m, (Number(x) + half) * s + m] } /** The inverse of `toLatLng`: a point on the picture as world `{ x, z }` in metres (a click on the map). */ export function fromLatLng(g, lat, lng) { const s = scaleOf(g) if (!(s > 0)) return null const half = g.worldSize / 2 const m = g.oceanMargin || 0 return { x: (Number(lng) - m) / s - half, z: (Number(lat) - m) / s - half } } /** The picture's bounds in the same space. */ export function boundsOf(g) { return [[0, 0], [g.height, g.width]] } /** A column number as Rust spells it: 0 is A, 25 is Z, 26 is AA. */ export function column(index) { let name = '' let n = index + 1 while (n > 0) { const r = (n - 1) % 26 name = String.fromCharCode(65 + r) + name n = Math.floor((n - 1) / 26) } return name } /** The grid label for a world position, the way the in-game map writes it. */ export function gridLabel(g, x, z) { if (!g || !(g.gridCells > 0) || !(g.gridCellSize > 0)) return null const half = g.worldSize / 2 const clamp = (v) => Math.max(0, Math.min(g.gridCells - 1, v)) const col = clamp(Math.floor((Number(x) + half) / g.gridCellSize)) const row = clamp(Math.floor((half - Number(z)) / g.gridCellSize)) return `${column(col)}${row}` } /** * The grid as lines and labels in world metres: `lines` are `[[x1, z1], [x2, * z2]]` pairs, `labels` sit at each cell's north-west corner. */ export function grid(g) { if (!g || !(g.gridCells > 0) || !(g.gridCellSize > 0)) return { lines: [], labels: [] } const half = g.worldSize / 2 const n = g.gridCells const c = g.gridCellSize const lines = [] for (let i = 0; i <= n; i += 1) { const at = -half + i * c lines.push([[at, half], [at, half - n * c]]) lines.push([[-half, half - i * c], [-half + n * c, half - i * c]]) } const labels = [] for (let col = 0; col < n; col += 1) { for (let row = 0; row < n; row += 1) { labels.push({ text: `${column(col)}${row}`, x: -half + col * c, z: half - row * c }) } } return { lines, labels } } /** Seconds as `m:ss`, for a locked crate's hack. */ export function countdown(seconds) { const s = Math.max(0, Math.round(Number(seconds) || 0)) return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}` }