chore: establish formatting baseline and verification scripts

This commit is contained in:
2026-10-28 12:11:36 +03:30
parent 751af768ce
commit 08b6cd464b
33 changed files with 7264 additions and 5006 deletions
+55 -35
View File
@@ -1,47 +1,67 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createState, purchaseBlocker, priceAt, canAffordAnything, formatClock} from '../src/game-state.js';
import {createPayment, beginPayment, readyPayment, swipePayment, cancelPayment} from '../src/payment.js';
import {machineById, machines} from '../src/locations.js';
import {
createState,
purchaseBlocker,
priceAt,
canAffordAnything,
formatClock,
} from '../src/game-state.js';
import {
createPayment,
beginPayment,
readyPayment,
swipePayment,
cancelPayment,
} from '../src/payment.js';
import { machineById, machines } from '../src/locations.js';
const park = machineById('mellat-park'), school = machineById('school-rahil'), uni = machineById('ferdowsi-university');
const park = machineById('mellat-park'),
school = machineById('school-rahil'),
uni = machineById('ferdowsi-university');
test('Payment charges once, only after ready and an explicit swipe', () => {
const state = createState(), payment = createPayment();
assert.ok(beginPayment(state, payment, park, 'juice'));
assert.equal(state.balance, 500);
assert.equal(swipePayment(state, payment), false, 'not ready yet');
assert.ok(readyPayment(payment));
assert.ok(swipePayment(state, payment));
assert.equal(state.balance, 465);
assert.equal(swipePayment(state, payment), false, 'no double charge');
assert.equal(state.purchases.length, 1);
assert.equal(beginPayment(state, createPayment(), park, 'water'), false, 'busy until delivery ends');
const state = createState(),
payment = createPayment();
assert.ok(beginPayment(state, payment, park, 'juice'));
assert.equal(state.balance, 500);
assert.equal(swipePayment(state, payment), false, 'not ready yet');
assert.ok(readyPayment(payment));
assert.ok(swipePayment(state, payment));
assert.equal(state.balance, 465);
assert.equal(swipePayment(state, payment), false, 'no double charge');
assert.equal(state.purchases.length, 1);
assert.equal(
beginPayment(state, createPayment(), park, 'water'),
false,
'busy until delivery ends',
);
});
test('Cancel, closed machines, unknown products and low funds never charge', () => {
const state = createState(), payment = createPayment();
assert.ok(beginPayment(state, payment, park, 'cake'));
assert.ok(cancelPayment(state, payment));
assert.equal(state.balance, 500);
assert.equal(state.busy, false);
assert.equal(purchaseBlocker(state, park, 'not-a-product'), 'unknown');
assert.equal(purchaseBlocker(state, school, 'nuts'), 'unknown', 'school does not sell nuts');
state.minutes = 14 * 60;
assert.equal(purchaseBlocker(state, school, 'water'), 'closed');
state.minutes = 9 * 60;
state.balance = 10;
assert.equal(purchaseBlocker(state, park, 'water'), 'funds');
assert.equal(beginPayment(state, payment, park, 'water'), false);
const state = createState(),
payment = createPayment();
assert.ok(beginPayment(state, payment, park, 'cake'));
assert.ok(cancelPayment(state, payment));
assert.equal(state.balance, 500);
assert.equal(state.busy, false);
assert.equal(purchaseBlocker(state, park, 'not-a-product'), 'unknown');
assert.equal(purchaseBlocker(state, school, 'nuts'), 'unknown', 'school does not sell nuts');
state.minutes = 14 * 60;
assert.equal(purchaseBlocker(state, school, 'water'), 'closed');
state.minutes = 9 * 60;
state.balance = 10;
assert.equal(purchaseBlocker(state, park, 'water'), 'funds');
assert.equal(beginPayment(state, payment, park, 'water'), false);
});
test('Prices follow machine discounts and the clock is Persian', () => {
assert.equal(priceAt(uni, 'nuts'), 52);
assert.equal(priceAt(park, 'nuts'), 65);
assert.equal(formatClock(8 * 60 + 5), '۰۸:۰۵');
const state = createState();
state.balance = 5;
assert.equal(canAffordAnything(state, machines), false);
state.balance = 15;
assert.ok(canAffordAnything(state, machines));
assert.equal(priceAt(uni, 'nuts'), 52);
assert.equal(priceAt(park, 'nuts'), 65);
assert.equal(formatClock(8 * 60 + 5), '۰۸:۰۵');
const state = createState();
state.balance = 5;
assert.equal(canAffordAnything(state, machines), false);
state.balance = 15;
assert.ok(canAffordAnything(state, machines));
});
+49 -45
View File
@@ -1,59 +1,63 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {smoothFix, arrivalRadius, insideWorld, createStabilizer} from '../src/realworld.js';
import { smoothFix, arrivalRadius, insideWorld, createStabilizer } from '../src/realworld.js';
test('GPS fixes blend by accuracy and a stale estimate is replaced', () => {
const first = smoothFix(null, {x: 0, z: 0, accuracy: 10, time: 0});
assert.deepEqual([first.x, first.z], [0, 0]);
const equal = smoothFix(first, {x: 10, z: 0, accuracy: 10, time: 1000});
assert.ok(Math.abs(equal.x - 5) < 1e-9, 'equal accuracy meets halfway');
assert.ok(equal.accuracy < 10, 'two fixes are better than one');
const poor = smoothFix(first, {x: 50, z: 0, accuracy: 60, time: 1000});
assert.ok(poor.x < 3, 'a poor fix barely moves a good estimate');
const stale = smoothFix(first, {x: 99, z: 0, accuracy: 30, time: 60000});
assert.equal(stale.x, 99);
const first = smoothFix(null, { x: 0, z: 0, accuracy: 10, time: 0 });
assert.deepEqual([first.x, first.z], [0, 0]);
const equal = smoothFix(first, { x: 10, z: 0, accuracy: 10, time: 1000 });
assert.ok(Math.abs(equal.x - 5) < 1e-9, 'equal accuracy meets halfway');
assert.ok(equal.accuracy < 10, 'two fixes are better than one');
const poor = smoothFix(first, { x: 50, z: 0, accuracy: 60, time: 1000 });
assert.ok(poor.x < 3, 'a poor fix barely moves a good estimate');
const stale = smoothFix(first, { x: 99, z: 0, accuracy: 30, time: 60000 });
assert.equal(stale.x, 99);
});
test('Arrival radius widens with poor GPS and bounds check the world', () => {
assert.equal(arrivalRadius(4), 20);
assert.equal(arrivalRadius(12), 30);
assert.equal(arrivalRadius(100), 50);
const bounds = {minX: -10, minZ: -10, maxX: 10, maxZ: 10};
assert.ok(insideWorld(bounds, 0, 0));
assert.equal(insideWorld(bounds, 11, 0), false);
assert.equal(arrivalRadius(4), 20);
assert.equal(arrivalRadius(12), 30);
assert.equal(arrivalRadius(100), 50);
const bounds = { minX: -10, minZ: -10, maxX: 10, maxZ: 10 };
assert.ok(insideWorld(bounds, 0, 0));
assert.equal(insideWorld(bounds, 11, 0), false);
});
test('Stable mode holds still through jitter, drops jumps and follows real walking', () => {
const s = createStabilizer();
s.push({x: 0, z: 0, accuracy: 8, time: 0, speed: 0});
// Standing still: fixes wander by a few metres, the shown position does not move.
for (let i = 1; i <= 10; i++) {
const p = s.push({x: (i % 2 ? 3 : -3), z: (i % 3) - 1, accuracy: 8, time: i * 1000, speed: 0});
assert.deepEqual([p.x, p.z], [0, 0]);
}
// A single 80 m jump one second later is impossible on foot and is dropped.
assert.deepEqual([s.push({x: 80, z: 0, accuracy: 10, time: 11000, speed: 0}).x], [0]);
// Walking east at 1.4 m/s: the shown position follows.
let p;
for (let i = 1; i <= 20; i++) p = s.push({x: 1.4 * i, z: 0, accuracy: 6, time: 11000 + i * 1000, speed: 1.4});
assert.ok(p.x > 15, `followed the walk (${p.x})`);
// With stable mode off every smoothed fix is shown.
const raw = createStabilizer();
raw.stable = false;
raw.push({x: 0, z: 0, accuracy: 8, time: 0});
assert.ok(raw.push({x: 3, z: 0, accuracy: 8, time: 1000}).x > 1);
const s = createStabilizer();
s.push({ x: 0, z: 0, accuracy: 8, time: 0, speed: 0 });
// Standing still: fixes wander by a few metres, the shown position does not move.
for (let i = 1; i <= 10; i++) {
const p = s.push({ x: i % 2 ? 3 : -3, z: (i % 3) - 1, accuracy: 8, time: i * 1000, speed: 0 });
assert.deepEqual([p.x, p.z], [0, 0]);
}
// A single 80 m jump one second later is impossible on foot and is dropped.
assert.deepEqual([s.push({ x: 80, z: 0, accuracy: 10, time: 11000, speed: 0 }).x], [0]);
// Walking east at 1.4 m/s: the shown position follows.
let p;
for (let i = 1; i <= 20; i++)
p = s.push({ x: 1.4 * i, z: 0, accuracy: 6, time: 11000 + i * 1000, speed: 1.4 });
assert.ok(p.x > 15, `followed the walk (${p.x})`);
// With stable mode off every smoothed fix is shown.
const raw = createStabilizer();
raw.stable = false;
raw.push({ x: 0, z: 0, accuracy: 8, time: 0 });
assert.ok(raw.push({ x: 3, z: 0, accuracy: 8, time: 1000 }).x > 1);
});
test('AR camera faces north/west with the phone held upright, and the FOV fits the screen', async () => {
const {Quaternion, Vector3} = await import('three');
const {orientationQuaternion, coverFov} = await import('../src/ar.js');
const look = (alpha, beta = 90, gamma = 0, angle = 0) => new Vector3(0, 0, -1).applyQuaternion(orientationQuaternion(new Quaternion(), alpha, beta, gamma, angle));
const north = look(0);
assert.ok(north.z < -.99, 'upright phone with alpha 0 looks north (−z)');
assert.ok(look(90).x < -.99, 'alpha 90 (turned left) looks west (−x)');
assert.ok(look(0, 0).y < -.99, 'phone lying flat looks at the ground');
// Portrait 9:19.5 screen over a 3:4 portrait video: the full long-side 65° shows vertically.
assert.ok(Math.abs(coverFov(3 / 4, 9 / 19.5) - 65) < .5);
// Landscape screen wider than the 4:3 video: the width shows 65°, so the vertical view is narrower.
assert.ok(coverFov(4 / 3, 19.5 / 9) < 35);
const { Quaternion, Vector3 } = await import('three');
const { orientationQuaternion, coverFov } = await import('../src/ar.js');
const look = (alpha, beta = 90, gamma = 0, angle = 0) =>
new Vector3(0, 0, -1).applyQuaternion(
orientationQuaternion(new Quaternion(), alpha, beta, gamma, angle),
);
const north = look(0);
assert.ok(north.z < -0.99, 'upright phone with alpha 0 looks north (−z)');
assert.ok(look(90).x < -0.99, 'alpha 90 (turned left) looks west (−x)');
assert.ok(look(0, 0).y < -0.99, 'phone lying flat looks at the ground');
// Portrait 9:19.5 screen over a 3:4 portrait video: the full long-side 65° shows vertically.
assert.ok(Math.abs(coverFov(3 / 4, 9 / 19.5) - 65) < 0.5);
// Landscape screen wider than the 4:3 video: the width shows 65°, so the vertical view is narrower.
assert.ok(coverFov(4 / 3, 19.5 / 9) < 35);
});
+46 -32
View File
@@ -1,41 +1,55 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {awardsFor, rankFor, completeMissions, activeMission, POINTS, RANKS} from '../src/scoring.js';
import {createState, purchase} from '../src/game-state.js';
import {machineById} from '../src/locations.js';
import {
awardsFor,
rankFor,
completeMissions,
activeMission,
POINTS,
RANKS,
} from '../src/scoring.js';
import { createState, purchase } from '../src/game-state.js';
import { machineById } from '../src/locations.js';
const total = awards => awards.reduce((s, a) => s + a.points, 0);
const total = (awards) => awards.reduce((s, a) => s + a.points, 0);
test('Discovery, combo and all-category bonuses are paid once', () => {
const state = createState(), park = machineById('mellat-park');
const buy = (machine, product) => {
const before = state.purchases.slice();
return awardsFor(before, purchase(state, machine, product));
};
assert.equal(total(buy(park, 'water')), POINTS.purchase + POINTS.discovery);
assert.equal(total(buy(park, 'juice')), POINTS.purchase, 'same type: no combo');
assert.equal(total(buy(park, 'cake')), POINTS.purchase + POINTS.combo);
assert.equal(total(buy(park, 'water')), POINTS.purchase, 'combo only once');
buy(machineById('factory'), 'water');
buy(machineById('school-rahil'), 'water');
buy(machineById('ferdowsi-university'), 'water');
buy(machineById('azadi-metro'), 'water');
state.minutes = 11 * 60;
const last = buy(machineById('mashhad-mall'), 'water');
assert.ok(last.some(a => a.points === POINTS.allCategories));
const state = createState(),
park = machineById('mellat-park');
const buy = (machine, product) => {
const before = state.purchases.slice();
return awardsFor(before, purchase(state, machine, product));
};
assert.equal(total(buy(park, 'water')), POINTS.purchase + POINTS.discovery);
assert.equal(total(buy(park, 'juice')), POINTS.purchase, 'same type: no combo');
assert.equal(total(buy(park, 'cake')), POINTS.purchase + POINTS.combo);
assert.equal(total(buy(park, 'water')), POINTS.purchase, 'combo only once');
buy(machineById('factory'), 'water');
buy(machineById('school-rahil'), 'water');
buy(machineById('ferdowsi-university'), 'water');
buy(machineById('azadi-metro'), 'water');
state.minutes = 11 * 60;
const last = buy(machineById('mashhad-mall'), 'water');
assert.ok(last.some((a) => a.points === POINTS.allCategories));
});
test('Ranks and missions advance with progress', () => {
assert.equal(rankFor(0).name, RANKS[0].name);
assert.equal(rankFor(RANKS[1].xp).index, 1);
assert.equal(rankFor(1e6).progress, 1);
const state = createState();
assert.equal(activeMission(state).id, 'factory');
purchase(state, machineById('factory'), 'water');
assert.deepEqual(completeMissions(state).map(m => m.id), ['factory']);
assert.equal(state.balance, 500 - 8 + 20, 'factory sells at half price');
purchase(state, machineById('mellat-park'), 'water');
assert.deepEqual(completeMissions(state).map(m => m.id), ['first']);
assert.deepEqual(completeMissions(state), [], 'rewards are not repeated');
assert.equal(activeMission(state).id, 'school');
assert.equal(rankFor(0).name, RANKS[0].name);
assert.equal(rankFor(RANKS[1].xp).index, 1);
assert.equal(rankFor(1e6).progress, 1);
const state = createState();
assert.equal(activeMission(state).id, 'factory');
purchase(state, machineById('factory'), 'water');
assert.deepEqual(
completeMissions(state).map((m) => m.id),
['factory'],
);
assert.equal(state.balance, 500 - 8 + 20, 'factory sells at half price');
purchase(state, machineById('mellat-park'), 'water');
assert.deepEqual(
completeMissions(state).map((m) => m.id),
['first'],
);
assert.deepEqual(completeMissions(state), [], 'rewards are not repeated');
assert.equal(activeMission(state).id, 'school');
});
+102 -72
View File
@@ -1,97 +1,127 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {toLocal, toLatLon, bearing, ORIGIN} from '../src/geo.js';
import {createCollisionWorld, resolveCircle, blocked, clearFraction} from '../src/collision.js';
import {buildGraph, routeBetween, nearestSegment, buildSegmentIndex} from '../src/routing.js';
import {createCar, stepCar} from '../src/vehicle.js';
import {createTraffic} from '../src/traffic.js';
import {Scene} from 'three';
import {machines, machineById, FACTORY} from '../src/locations.js';
import { readFileSync } from 'node:fs';
import { toLocal, toLatLon, bearing, ORIGIN } from '../src/geo.js';
import { createCollisionWorld, resolveCircle, blocked, clearFraction } from '../src/collision.js';
import { buildGraph, routeBetween, nearestSegment, buildSegmentIndex } from '../src/routing.js';
import { createCar, stepCar } from '../src/vehicle.js';
import { createTraffic } from '../src/traffic.js';
import { Scene } from 'three';
import { machines, machineById, FACTORY } from '../src/locations.js';
const data = JSON.parse(readFileSync(new URL('../src/data/mellat.json', import.meta.url)));
test('Projection round-trips and bearings follow the compass', () => {
const p = toLocal(36.32092, 59.53744), back = toLatLon(p.x, p.z);
assert.ok(Math.abs(back.lat - 36.32092) < 1e-9 && Math.abs(back.lon - 59.53744) < 1e-9);
assert.ok(p.z < 0, 'north of the origin is -z');
assert.deepEqual(toLocal(ORIGIN.lat, ORIGIN.lon), {x: 0, z: 0});
assert.equal(bearing(0, -1), 0);
assert.equal(bearing(1, 0), 90);
const p = toLocal(36.32092, 59.53744),
back = toLatLon(p.x, p.z);
assert.ok(Math.abs(back.lat - 36.32092) < 1e-9 && Math.abs(back.lon - 59.53744) < 1e-9);
assert.ok(p.z < 0, 'north of the origin is -z');
assert.deepEqual(toLocal(ORIGIN.lat, ORIGIN.lon), { x: 0, z: 0 });
assert.equal(bearing(0, -1), 0);
assert.equal(bearing(1, 0), 90);
});
test('Circles are pushed out of footprints and segments stop at walls', () => {
const world = createCollisionWorld([[0, 0, 10, 0, 10, 10, 0, 10]]);
assert.ok(blocked(world, 5, 5));
const out = resolveCircle(world, 9.5, 5, 1);
assert.ok(out.hit && out.x >= 10.99);
assert.equal(resolveCircle(world, 20, 20, 1).hit, false);
assert.ok(Math.abs(clearFraction(world, -10, 5, 10, 5) - .5) < .06);
const world = createCollisionWorld([[0, 0, 10, 0, 10, 10, 0, 10]]);
assert.ok(blocked(world, 5, 5));
const out = resolveCircle(world, 9.5, 5, 1);
assert.ok(out.hit && out.x >= 10.99);
assert.equal(resolveCircle(world, 20, 20, 1).hit, false);
assert.ok(Math.abs(clearFraction(world, -10, 5, 10, 5) - 0.5) < 0.06);
});
test('Real map: the factory connects by car to the city, and every city machine is reachable', () => {
const walk = buildGraph(data.roads), drive = buildGraph(data.roads, {drivable: true});
const toCity = routeBetween(drive, FACTORY.center.x, FACTORY.center.z, -445.8, -275.7);
assert.ok(toCity && toCity.length > 18000 && toCity.length < 23000, `factory → Park Mellat is ~20 km (${toCity?.length})`);
const start = machineById('mellat-park').anchor;
for (const m of machines.filter(m => m.anchor)) {
const onFoot = routeBetween(walk, start.x, start.z, m.anchor.x, m.anchor.z);
assert.ok(onFoot, `walk route to ${m.id}`);
assert.ok(onFoot.length >= Math.hypot(m.anchor.x - start.x, m.anchor.z - start.z) - 1);
assert.ok(routeBetween(drive, start.x, start.z, m.anchor.x, m.anchor.z), `drive route to ${m.id}`);
}
const street = nearestSegment(buildSegmentIndex(data.roads), 494, 280, 30, s => s[5] >= 0);
assert.equal(data.names[street.name], 'بولوار ملک آباد');
const walk = buildGraph(data.roads),
drive = buildGraph(data.roads, { drivable: true });
const toCity = routeBetween(drive, FACTORY.center.x, FACTORY.center.z, -445.8, -275.7);
assert.ok(
toCity && toCity.length > 18000 && toCity.length < 23000,
`factory → Park Mellat is ~20 km (${toCity?.length})`,
);
const start = machineById('mellat-park').anchor;
for (const m of machines.filter((m) => m.anchor)) {
const onFoot = routeBetween(walk, start.x, start.z, m.anchor.x, m.anchor.z);
assert.ok(onFoot, `walk route to ${m.id}`);
assert.ok(onFoot.length >= Math.hypot(m.anchor.x - start.x, m.anchor.z - start.z) - 1);
assert.ok(
routeBetween(drive, start.x, start.z, m.anchor.x, m.anchor.z),
`drive route to ${m.id}`,
);
}
const street = nearestSegment(buildSegmentIndex(data.roads), 494, 280, 30, (s) => s[5] >= 0);
assert.equal(data.names[street.name], 'بولوار ملک آباد');
});
test('Car accelerates, steers right with positive input and brakes', () => {
const car = createCar(0, 0, 0);
for (let i = 0; i < 60; i++) stepCar(car, {throttle: 1}, 1 / 30);
assert.ok(car.speed > 10 && car.x > 10 && Math.abs(car.z) < 1e-9);
for (let i = 0; i < 30; i++) stepCar(car, {throttle: 1, steer: 1}, 1 / 30);
assert.ok(car.z > 0, 'turning right while heading east moves south (+z)');
for (let i = 0; i < 90; i++) stepCar(car, {throttle: -1}, 1 / 30);
assert.ok(car.speed <= 0);
const car = createCar(0, 0, 0);
for (let i = 0; i < 60; i++) stepCar(car, { throttle: 1 }, 1 / 30);
assert.ok(car.speed > 10 && car.x > 10 && Math.abs(car.z) < 1e-9);
for (let i = 0; i < 30; i++) stepCar(car, { throttle: 1, steer: 1 }, 1 / 30);
assert.ok(car.z > 0, 'turning right while heading east moves south (+z)');
for (let i = 0; i < 90; i++) stepCar(car, { throttle: -1 }, 1 / 30);
assert.ok(car.speed <= 0);
});
test('Traffic drives with the flow on one-way carriageways and stays inside them', () => {
const oneway = new Set(data.oneway);
assert.ok(data.roads.some((r, i) => data.names[r[1]]?.includes('چناران') && oneway.has(i)), 'Chenaran highway is one-way per carriageway');
const traffic = createTraffic(new Scene(), data, 28), around = {x: -10008, z: -15049};
for (let i = 0; i < 400; i++) traffic.update(1 / 30, [], around);
const active = traffic.cars.filter(c => c.road);
assert.ok(active.length > 10, `cars spawned near the player (${active.length})`);
for (const car of active.filter(c => c.road.oneway)) {
assert.equal(car.dir, 1);
assert.ok(Math.abs(car.lane) < car.road.width / 2, 'lane inside the carriageway');
}
const oneway = new Set(data.oneway);
assert.ok(
data.roads.some((r, i) => data.names[r[1]]?.includes('چناران') && oneway.has(i)),
'Chenaran highway is one-way per carriageway',
);
const traffic = createTraffic(new Scene(), data, 28),
around = { x: -10008, z: -15049 };
for (let i = 0; i < 400; i++) traffic.update(1 / 30, [], around);
const active = traffic.cars.filter((c) => c.road);
assert.ok(active.length > 10, `cars spawned near the player (${active.length})`);
for (const car of active.filter((c) => c.road.oneway)) {
assert.equal(car.dir, 1);
assert.ok(Math.abs(car.lane) < car.road.width / 2, 'lane inside the carriageway');
}
});
test('Microsoft buildings decode into the world, stay clear of the factory and leave machines reachable', async () => {
const {decodeBuildings} = await import('../src/buildings-data.js');
const file = readFileSync(new URL('../public/data/buildings.bin', import.meta.url));
const buildings = decodeBuildings(file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength));
assert.ok(buildings.length > 100000, `${buildings.length} buildings`);
const {minX, minZ, maxX, maxZ} = data.bounds;
for (const b of buildings) {
assert.ok(b[0] >= 3 && b[0] <= 30, `height ${b[0]}`);
assert.ok(b[1] >= minX - 150 && b[1] <= maxX + 150 && b[2] >= minZ - 150 && b[2] <= maxZ + 150, 'near the region');
}
const world = createCollisionWorld([...data.buildings, ...buildings].map(b => b.slice(1)));
assert.equal(blocked(world, FACTORY.center.x, FACTORY.center.z), false, 'factory compound is clear');
// Every city machine's path point is still outside buildings, so the family can stand there.
for (const m of machines.filter(m => m.anchor)) assert.equal(blocked(world, m.anchor.x, m.anchor.z), false, m.id);
const { decodeBuildings } = await import('../src/buildings-data.js');
const file = readFileSync(new URL('../public/data/buildings.bin', import.meta.url));
const buildings = decodeBuildings(
file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength),
);
assert.ok(buildings.length > 100000, `${buildings.length} buildings`);
const { minX, minZ, maxX, maxZ } = data.bounds;
for (const b of buildings) {
assert.ok(b[0] >= 3 && b[0] <= 30, `height ${b[0]}`);
assert.ok(
b[1] >= minX - 150 && b[1] <= maxX + 150 && b[2] >= minZ - 150 && b[2] <= maxZ + 150,
'near the region',
);
}
const world = createCollisionWorld([...data.buildings, ...buildings].map((b) => b.slice(1)));
assert.equal(
blocked(world, FACTORY.center.x, FACTORY.center.z),
false,
'factory compound is clear',
);
// Every city machine's path point is still outside buildings, so the family can stand there.
for (const m of machines.filter((m) => m.anchor))
assert.equal(blocked(world, m.anchor.x, m.anchor.z), false, m.id);
});
test('Satellite tiles land where the map is: a tile over Park Mellat covers the park', async () => {
const {tileCorners} = await import('../src/imagery.js');
const index = JSON.parse(readFileSync(new URL('../public/imagery/index.json', import.meta.url)));
assert.ok(index[17].length > 100 && index[15].length > 100);
const park = toLocal(36.32092, 59.53744), z = 17;
const x = Math.floor((59.53744 + 180) / 360 * 2 ** z), y = Math.floor((1 - Math.asinh(Math.tan(36.32092 * Math.PI / 180)) / Math.PI) / 2 * 2 ** z);
assert.ok(index[17].includes(`${x}/${y}`), 'the Park Mellat tile was downloaded');
const [nw, ne, sw, se] = tileCorners(x, y, z);
assert.ok(nw.x < ne.x && nw.z < sw.z && se.x > sw.x, 'north-west corner is up-left in world space');
assert.ok(park.x >= nw.x && park.x <= se.x && park.z >= nw.z && park.z <= se.z, 'the park centre lies inside its tile');
assert.ok(Math.abs((ne.x - nw.x) - 247) < 5, 'a zoom-17 tile is ~247 m wide at Mashhad');
const { tileCorners } = await import('../src/imagery.js');
const index = JSON.parse(readFileSync(new URL('../public/imagery/index.json', import.meta.url)));
assert.ok(index[17].length > 100 && index[15].length > 100);
const park = toLocal(36.32092, 59.53744),
z = 17;
const x = Math.floor(((59.53744 + 180) / 360) * 2 ** z),
y = Math.floor(((1 - Math.asinh(Math.tan((36.32092 * Math.PI) / 180)) / Math.PI) / 2) * 2 ** z);
assert.ok(index[17].includes(`${x}/${y}`), 'the Park Mellat tile was downloaded');
const [nw, ne, sw, se] = tileCorners(x, y, z);
assert.ok(
nw.x < ne.x && nw.z < sw.z && se.x > sw.x,
'north-west corner is up-left in world space',
);
assert.ok(
park.x >= nw.x && park.x <= se.x && park.z >= nw.z && park.z <= se.z,
'the park centre lies inside its tile',
);
assert.ok(Math.abs(ne.x - nw.x - 247) < 5, 'a zoom-17 tile is ~247 m wide at Mashhad');
});