127 lines
5.0 KiB
JavaScript
127 lines
5.0 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { smallState } from "./framework/helpers.js";
|
|
import { mapToLocal, flightLine, flightDistanceTiles, key } from "../shared/hex.js";
|
|
import { AIR_DEFENSE } from "../shared/data/combat.js";
|
|
|
|
const BIG_BUDGET = 1.0e15;
|
|
|
|
function buildingIndex(state, id) {
|
|
return state.protoBuildings.findIndex((building) => building.id === id);
|
|
}
|
|
|
|
function protoOf(state, id) {
|
|
return state.protoUnits.find((unit) => unit.id === id);
|
|
}
|
|
|
|
function giveBuilding(state, city, id, level = 1) {
|
|
city.buildings[buildingIndex(state, id)] = level;
|
|
state._gdpPerCapitaCache.clear();
|
|
}
|
|
|
|
// Any tile exactly `distance` hex steps from `center`.
|
|
function tileAtDistance(state, center, distance) {
|
|
const seen = new Set([key(center.x, center.y)]);
|
|
let frontier = [{ x: center.x, y: center.y }];
|
|
for (let step = 0; step < distance; step++) {
|
|
const next = [];
|
|
for (const cell of frontier) {
|
|
for (const neighbour of state.topology.neighbours(cell.x, cell.y)) {
|
|
const k = key(neighbour.x, neighbour.y);
|
|
if (seen.has(k) || !state.tiles[k]) continue;
|
|
seen.add(k);
|
|
next.push(neighbour);
|
|
}
|
|
}
|
|
frontier = next;
|
|
}
|
|
return frontier[0] || null;
|
|
}
|
|
|
|
export class AirMovementTest extends TestCase {
|
|
test_a_flight_is_a_straight_two_point_line() {
|
|
const state = smallState();
|
|
const airport = buildingIndex(state, "airport");
|
|
const cities = state.cities.filter((city) => city.civ === 0);
|
|
for (const city of cities) city.buildings[airport] = 1;
|
|
const [home, away] = cities;
|
|
const jet = protoOf(state, "jet_fighter");
|
|
const unit = state._spawnUnit(home.coords, 0, jet, home.id);
|
|
|
|
const path = state.findPath(unit, home.coords, away.coords);
|
|
this.assertEqual(path.length, 2, "a flight is two world-pixel points");
|
|
const expected = flightLine(
|
|
mapToLocal(home.coords.x, home.coords.y),
|
|
away.coords,
|
|
state.topology
|
|
);
|
|
this.assertApprox(path[0].x, expected[0].x, 1e-9, "the flight starts at the tile centre");
|
|
this.assertApprox(path[1].x, expected[1].x, 1e-9);
|
|
this.assertApprox(path[1].y, expected[1].y, 1e-9);
|
|
|
|
// The one segment's hours are the straight-line distance over speed, so
|
|
// fuel and flight time can never disagree.
|
|
const hours = state._pathHours(unit, path);
|
|
const expectedHours =
|
|
flightDistanceTiles(home.coords, away.coords, state.topology, state._maxStepLength) /
|
|
jet.speed;
|
|
this.assertApprox(hours, expectedHours, 1e-9, "time follows the straight line");
|
|
const segmentHours = state._segmentHours(unit, path);
|
|
this.assertEqual(segmentHours.length, 1, "one leg, one segment");
|
|
this.assertApprox(segmentHours[0], expectedHours, 1e-9);
|
|
}
|
|
|
|
test_a_ferry_flight_only_needs_a_runway_at_the_destination() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const airport = buildingIndex(state, "airport");
|
|
const cities = state.cities.filter((city) => city.civ === 0);
|
|
for (const city of cities) city.buildings[airport] = 1;
|
|
const [home, away] = cities;
|
|
const jet = protoOf(state, "jet_fighter");
|
|
const unit = state._spawnUnit(home.coords, 0, jet, home.id);
|
|
|
|
this.assertTrue(
|
|
state.requestGroupMove([unit.id], away.coords),
|
|
"a ferry to a friendly airport is accepted"
|
|
);
|
|
this.assertEqual(unit.path.length, 2, "the ferry is a straight line");
|
|
// The plane reaches the far airport and lands, refuelling there.
|
|
for (let hour = 0; hour < 40 && unit.coords.x !== away.coords.x; hour++) {
|
|
state.advanceHour();
|
|
}
|
|
this.assertEqual(unit.coords.x, away.coords.x, "it crossed in a straight line");
|
|
this.assertEqual(unit.coords.y, away.coords.y);
|
|
this.assertFalse(unit.airborne, "and landed");
|
|
}
|
|
|
|
test_air_defense_fires_across_its_corridor() {
|
|
const state = smallState();
|
|
const city = state.cities.find((entry) => entry.civ === 1);
|
|
giveBuilding(state, city, "aa_building", 1);
|
|
const jet = protoOf(state, "jet_fighter");
|
|
const unit = state._spawnUnit(state.landCells[0], 0, jet);
|
|
|
|
// Inside the corridor but not standing on the city tile: the old tile-entry
|
|
// volley would have missed it.
|
|
const near = tileAtDistance(state, city.coords, AIR_DEFENSE.rangeTiles);
|
|
this.assertNotNull(near, "there is a tile inside the corridor");
|
|
unit.coords = { x: near.x, y: near.y };
|
|
this.assertFalse(
|
|
unit.coords.x === city.coords.x && unit.coords.y === city.coords.y,
|
|
"the plane is not over the city"
|
|
);
|
|
const before = unit.hp;
|
|
state._airDefenseVolley(unit);
|
|
this.assertLess(unit.hp, before, "a plane inside the corridor takes fire");
|
|
|
|
// One step further out is beyond the battery's reach.
|
|
const far = tileAtDistance(state, city.coords, AIR_DEFENSE.rangeTiles + 1);
|
|
this.assertNotNull(far, "there is a tile outside the corridor");
|
|
unit.hp = unit.maxHp;
|
|
unit.coords = { x: far.x, y: far.y };
|
|
const safe = unit.hp;
|
|
state._airDefenseVolley(unit);
|
|
this.assertEqual(unit.hp, safe, "a plane outside the corridor is untouched");
|
|
}
|
|
}
|