Files
Battle-for-Tismo/tests/framework/helpers.js
T
adrien bd9b206708 Gave the new buildings real effects
The catalogue had grown a barracks, wartime shelters, an air-defense
battery, a warm-water port and a shipyard, but only as data: infantry
still trained without a barracks, shelters did nothing, and an airport was
just an airbase. Each building now names the rule it grants with a
`mechanic` id, so the simulation and the city panel describe the same
thing without magic strings.

- Barracks and shipyards open one more production line each. A line sticks
  to one entry at full rate, so two units build at once rather than the
  head absorbing every line; the base behaviour (one queue, overflow
  carrying into the next entry) is unchanged.
- Wartime shelters keep 10% of civilians per level safe from bombardment
  and are very hard to knock down, and ranged fire can now destroy a
  building level outright, with no refund. Each building rolls its own
  chance per point of damage, from a generator seeded off the world so a
  bombardment replays identically.
- Air defense fires one volley at a hostile aircraft the moment it enters
  the city tile, scaled by its level; a plane shot down never drops its
  bombs.
- A port adds 1000 GDP per capita per level, per adjacent sea tile, to
  every tile in its region.
- An airport draws a daily trickle of people from every other airport city
  in the world, scaled by the destination's level.
- Wired the new effects into the city panel and taught the repeatable
  Industrial Automation to satisfy other technologies' prerequisites.

Added tests for each mechanic and for the parallel lines.
2026-09-21 11:38:01 +02:00

116 lines
3.4 KiB
JavaScript

// Shared fixtures for the test suite. A small deterministic map is used
// throughout so tests are fast and their results stable.
import { GameState } from "../../shared/game_state.js";
import { parseKey } from "../../shared/hex.js";
import { MAP_CONFIG, CIVILISATIONS } from "../../shared/data.js";
import { startServer } from "../../server/server.js";
// Boots the real server on an ephemeral port for an integration test, then
// guarantees it is closed afterwards.
export async function withServer(fn) {
const server = startServer({ port: 0, bind: "127.0.0.1", log: () => {} });
const address = await server.listen;
try {
await fn(address.port, server);
} finally {
server.close();
}
}
export const SEED = 12345;
export const SEA = "Sea";
export function smallConfig(overrides = {}) {
return {
...MAP_CONFIG,
mapSize: { x: 24, y: 16 },
minCapitalLandmass: 1,
minCapitalDistance: 2,
citiesPerCiv: 2,
citySpacing: 2,
...overrides,
};
}
export function smallState(civIds = ["france", "britain"], seed = SEED, overrides = {}) {
const state = new GameState();
state.mapConfig = smallConfig(overrides);
const civs = civIds.map((id) => CIVILISATIONS.find((c) => c.id === id));
state.configure(civs, seed);
return state;
}
// A state on the full default map, so its coordinates line up with the terrain
// the browser view regenerates from MAP_CONFIG.
export function defaultState(civIds = ["france", "britain"], seed = SEED) {
const state = new GameState();
const civs = civIds.map((id) => CIVILISATIONS.find((c) => c.id === id));
state.configure(civs, seed);
return state;
}
// The six render layers of the world map, as jQuery objects.
export function mapLayers($) {
return {
terrain: $("#layer-terrain"),
roads: $("#layer-roads"),
borders: $("#layer-borders"),
fog: $("#layer-fog"),
highlight: $("#layer-highlight"),
paths: $("#layer-paths"),
targets: $("#layer-targets"),
entities: $("#layer-entities"),
labels: $("#layer-labels"),
};
}
export function adjacentLand(state, unit) {
for (const neighbour of state.topology.neighbours(unit.coords.x, unit.coords.y)) {
if (state._isLand(neighbour)) return neighbour;
}
return null;
}
export function seaTile(state) {
for (const k in state.tiles) {
if (state.tiles[k].terrainClass === SEA) return parseKey(k);
}
return null;
}
export function cityOf(state, civ) {
return state.cities.find((city) => city.civ === civ) || null;
}
// Gives a city a level of a building directly, so a test that trains a unit
// gated behind a building does not have to walk the construction queue first.
export function grantBuilding(state, city, id, level = 1) {
const index = state.protoBuildings.findIndex((building) => building.id === id);
if (index < 0) return -1;
city.buildings[index] = level;
state._gdpPerCapitaCache.clear();
state._touchModifiers();
return index;
}
export function unitsOf(state, civ) {
return state.units.filter((unit) => unit.civ === civ).length;
}
export function snapshotUnit(state, unitId, viewer = 0) {
return (state.snapshot(viewer).units || []).find((u) => u.id === unitId) || null;
}
export function gdpBuilding(overrides = {}) {
return {
id: "test_market",
name: "Test market",
baseCost: 100,
costAlpha: 2.0,
refundFraction: 0.5,
effects: [{ stat: "gdp", baseValue: 0.5, exponent: 1.0 }],
...overrides,
};
}