Files
Module-Rust/client/test/mapGeometry.test.js
wtclaude 0cb9bdd1f0
All checks were successful
PR Checks / server-tests (pull_request) Successful in 28s
PR Checks / client-build (pull_request) Successful in 27s
PR Checks / frozen-manifest (pull_request) Successful in -1m9s
feat(rust): the live map (phase 14, protocol 11)
PLAN.md §30 as approved, plus D119/D120 from the build.

Server:
- rust_map_images (one row per server: picture as MEDIUMBLOB, geometry,
  monuments, DERIVATION_VERSION) and rust_map_overrides; purge.sql pair.
- mapImages.js: D110. The board poll notices a new boot/wipe/seed/size and
  asks map.info; a new key or hash from the free Rust+ cache (or a render
  kept on disk) is fetched in slices, checked against its SHA-256 and stored
  in one statement. One fetch per server, a backoff on failure, `stale`
  abandons a fetch that straddles a map change. Render now (D109) is
  admin-only and watched to completion.
- mapLive.js: D111. One map.live per server per 5 s whoever asks; positions
  are held in memory only.
- model/map: four layers (world, events public; players, bases staff), a
  fleet default plus per-server override (D114), the players layer capped by
  presence (D113), own dot and online first-party clan mates for a linked
  viewer (D115, D117, D118). A layer the viewer may not see is absent from
  the answer, never sent and hidden.
- Routes: public /servers/:id/map, /map/image (immutable under its hash),
  /map/live; admin /servers/:id/map/fetch and /render; the Map card on the
  visibility PUT. Swagger fragment and frozen manifest regenerated.

Client:
- A Map tab: Leaflet over the picture in CRS.Simple, the game's own grid
  (labels only when a cell is wide enough to hold one), a legend that lists
  hidden layers with who can see them, polled every 10 s while visible.
- D120: Leaflet is a lazy split chunk beside entry.js, not in it. release.yml
  copies every dist/*.js; checkExternals and build.test.js hold both ends.
- The Map card on Admin -> Rust visibility, with Fetch again and Render now.

Capability `map` declared for the Android app (phase 15).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-25 01:06:10 -05:00

93 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── How a world position reaches a pixel (PLAN.md §30.3) ─────────────────
//
// The grid cases are the GAME's answers, not this file's: on 2026-09-25 a probe
// on the Oxide rig (a 3000 map, seed 1234) asked `MapHelper.PositionToString`
// for these positions and wrote down what it said. A label the page draws that
// disagrees with the in-game map is a player walking to the wrong square — and
// phase 3's plugin did exactly that for three weeks with a 146.3 m cell (D119).
import test from 'node:test'
import assert from 'node:assert/strict'
import { boundsOf, column, countdown, grid, gridLabel, scaleOf, toLatLng } from '../src/lib/mapGeometry.js'
// The rig's map as `GET /map` describes it.
const RIG = { worldSize: 3000, oceanMargin: 500, width: 2500, height: 2500, gridCells: 20, gridCellSize: 150 }
test('the grid label is the game’s, at every probed position', () => {
const probed = [
['ue_jungle_swamp_a', 764.7, 167.4, 'P8'],
['ue_jungle_swamp_a', 733.7, -556.0, 'O13'],
['harbor_2', 1122.7, 204.6, 'R8'],
['harbor_1', 678.1, 1005.7, 'O3'],
['ferry_terminal_1', 645.4, -1004.1, 'O16'],
['fishing_village_a', -787.8, 224.1, 'E8'],
['fishing_village_c', -203.0, -911.1, 'I16'],
['fishing_village_b', 1142.1, -566.5, 'R13'],
['desert_military_base_c', 94.0, -729.3, 'K14'],
['arctic_research_base_a', -556.9, 840.0, 'G4'],
['powerplant_1', -608.6, -346.4, 'F12'],
['water_treatment_plant_1', 497.8, 84.9, 'N9'],
['nw-corner', -1499, 1499, 'A0'],
['se-corner', 1499, -1499, 'T19'],
['origin', 0, 0, 'K10'],
]
for (const [name, x, z, game] of probed) assert.equal(gridLabel(RIG, x, z), game, name)
})
test('a 146.3 m cell — phase 3’s constant — would have disagreed with the game', () => {
// Kept as a test so the constant cannot come back in a refactor that "fixes"
// the cell size to the number community tools quote.
assert.notEqual(gridLabel({ ...RIG, gridCells: 21, gridCellSize: 146.3 }, 645.4, -1004.1), 'O16')
})
test('columns past Z are spelled the way Rust spells them', () => {
assert.equal(column(0), 'A')
assert.equal(column(25), 'Z')
assert.equal(column(26), 'AA')
assert.equal(column(27), 'AB')
})
test('the ocean margin is in pixels and is not scaled', () => {
assert.equal(scaleOf(RIG), 0.5)
// The world's corners sit exactly one margin inside the picture's.
assert.deepEqual(toLatLng(RIG, -1500, -1500), [500, 500])
assert.deepEqual(toLatLng(RIG, 1500, 1500), [2000, 2000])
assert.deepEqual(toLatLng(RIG, 0, 0), [1250, 1250])
})
test('north is up: a larger z is a larger latitude, and x is longitude', () => {
const [lat1, lng1] = toLatLng(RIG, 100, 100)
const [lat2, lng2] = toLatLng(RIG, 100, 400)
assert.ok(lat2 > lat1)
assert.equal(lng1, lng2)
assert.deepEqual(boundsOf(RIG), [[0, 0], [2500, 2500]])
})
test('something off the edge of the world is still placed, outside the picture', () => {
// The rig's cargo ship, as the probe found it: past the world AND the margin.
const [lat, lng] = toLatLng(RIG, 2691.6, -1453.1)
assert.ok(lng > RIG.width)
assert.ok(lat > 0 && lat < RIG.height)
})
test('the grid has a line per edge and a label per cell, A0 at the north-west corner', () => {
const g = grid(RIG)
assert.equal(g.lines.length, 2 * (RIG.gridCells + 1))
assert.equal(g.labels.length, RIG.gridCells * RIG.gridCells)
const a0 = g.labels.find((l) => l.text === 'A0')
assert.deepEqual([a0.x, a0.z], [-1500, 1500])
assert.deepEqual(grid(null), { lines: [], labels: [] })
})
test('a geometry that cannot place anything places nothing rather than NaN everywhere', () => {
assert.equal(scaleOf({ worldSize: 0, width: 2500 }), 0)
assert.equal(gridLabel({ worldSize: 3000 }, 0, 0), null)
})
test('a hack timer reads as minutes and seconds', () => {
assert.equal(countdown(540), '9:00')
assert.equal(countdown(61.4), '1:01')
assert.equal(countdown(-3), '0:00')
})