Files
Battle-for-Tismo/tests/air_movement_test.js

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");
}
}