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.
116 lines
3.4 KiB
JavaScript
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,
|
|
};
|
|
}
|