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.
203 lines
8.0 KiB
JavaScript
203 lines
8.0 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import {
|
|
CIVILISATIONS,
|
|
PROTO_UNITS,
|
|
BUILDINGS,
|
|
GOVERNMENTS,
|
|
TECHNOLOGIES,
|
|
TRAINABLE_UNIT_IDS,
|
|
TERRAIN_TILES,
|
|
protoUnitById,
|
|
buildingById,
|
|
governmentById,
|
|
technologyById,
|
|
effectValue,
|
|
statusById,
|
|
STATUS_BATTLE,
|
|
PILLAGE,
|
|
pillageSteps,
|
|
POLICIES,
|
|
} from "../shared/data.js";
|
|
|
|
export class DataTest extends TestCase {
|
|
test_lookups_return_the_matching_entry() {
|
|
this.assertEqual(protoUnitById("modern_infantry"), PROTO_UNITS.find((u) => u.id === "modern_infantry"));
|
|
this.assertEqual(buildingById("market"), BUILDINGS.find((b) => b.id === "market"));
|
|
this.assertEqual(governmentById("republic"), GOVERNMENTS[0]);
|
|
this.assertEqual(technologyById("scientific_method"), TECHNOLOGIES[0]);
|
|
}
|
|
|
|
test_unknown_lookups_return_null() {
|
|
this.assertNull(protoUnitById("nope"));
|
|
this.assertNull(buildingById("nope"));
|
|
this.assertNull(governmentById("nope"));
|
|
this.assertNull(technologyById("nope"));
|
|
}
|
|
|
|
test_every_civilisation_is_well_formed() {
|
|
this.assertGreaterOrEqual(CIVILISATIONS.length, 2);
|
|
for (const civ of CIVILISATIONS) {
|
|
this.assertTrue(civ.id.length > 0, "id");
|
|
this.assertTrue(civ.name.length > 0, "name");
|
|
this.assertTrue(civ.primaryColour.startsWith("#"), `primary colour of ${civ.id}`);
|
|
this.assertTrue(civ.flag.endsWith(".png"), `flag of ${civ.id}`);
|
|
this.assertTrue(civ.demonym.length > 0, `demonym of ${civ.id}`);
|
|
this.assertNotEmpty(civ.cityNames, `city names of ${civ.id}`);
|
|
}
|
|
}
|
|
|
|
test_battle_status_is_rendered_and_triples_upkeep() {
|
|
const battle = statusById(STATUS_BATTLE);
|
|
this.assertNotNull(battle, "the battle status exists");
|
|
this.assertEqual(battle.icon, "icon_battle.svg");
|
|
this.assertEqual(battle.upkeepMultiplier, 3);
|
|
}
|
|
|
|
test_pillage_steps_compound_per_unit() {
|
|
const one = pillageSteps(100000, 1);
|
|
this.assertApprox(one.steps[0].removed, 10000, 1e-6);
|
|
this.assertApprox(one.steps[0].heal, 10, 1e-9);
|
|
this.assertApprox(one.gdpMultiplier, PILLAGE.gdpPenalty, 1e-9);
|
|
|
|
const two = pillageSteps(100000, 2);
|
|
this.assertApprox(two.steps[0].removed, 10000, 1e-6);
|
|
this.assertApprox(two.steps[1].removed, 9000, 1e-6);
|
|
this.assertApprox(two.steps[1].heal, 9, 1e-9);
|
|
this.assertApprox(two.steps[1].remaining, 81000, 1e-6);
|
|
this.assertApprox(two.gdpMultiplier, 0.49, 1e-9);
|
|
}
|
|
|
|
test_trainable_units_exist_in_the_catalogue() {
|
|
this.assertNotEmpty(TRAINABLE_UNIT_IDS);
|
|
for (const id of TRAINABLE_UNIT_IDS) {
|
|
this.assertNotNull(protoUnitById(id), `trainable unit ${id}`);
|
|
}
|
|
}
|
|
|
|
test_artillery_is_expensive_slow_and_long_ranged() {
|
|
const infantry = protoUnitById("modern_infantry");
|
|
const artillery = protoUnitById("artillery");
|
|
this.assertNotNull(artillery, "artillery exists");
|
|
this.assertTrue(artillery.military, "artillery is military");
|
|
this.assertGreater(artillery.cost, infantry.cost * 2, "much more expensive");
|
|
this.assertGreater(artillery.upkeep, infantry.upkeep, "costlier to keep");
|
|
this.assertLess(artillery.speed, infantry.speed, "slower than infantry");
|
|
this.assertLess(artillery.defense, infantry.defense / 4, "nearly no defense");
|
|
this.assertGreater(artillery.attack, infantry.attack, "hits harder");
|
|
this.assertGreater(artillery.range, infantry.range, "outranges infantry");
|
|
this.assertEqual(artillery.range, 2);
|
|
this.assertTrue(TRAINABLE_UNIT_IDS.includes("artillery"), "artillery is trainable");
|
|
this.assertTrue(artillery.icon.endsWith(".svg"));
|
|
}
|
|
|
|
test_buildings_and_governments_carry_effects() {
|
|
for (const building of BUILDINGS) {
|
|
// A building carries either stat effects or a rule-based mechanic.
|
|
this.assertTrue(
|
|
building.effects.length > 0 || typeof building.mechanic === "string",
|
|
`${building.id} effects or mechanic`
|
|
);
|
|
this.assertGreater(building.costAlpha, 1, `${building.id} cost growth`);
|
|
this.assertTrue(building.icon && building.icon.endsWith(".svg"), `${building.id} icon`);
|
|
for (const effect of building.effects) {
|
|
this.assertTrue(effect.stat.length > 0);
|
|
this.assertGreater(effect.baseValue, 0);
|
|
}
|
|
}
|
|
for (const government of GOVERNMENTS) {
|
|
this.assertNotEmpty(government.effects, `${government.id} effects`);
|
|
}
|
|
}
|
|
|
|
test_policy_effects_are_popularity_points_not_percentages() {
|
|
for (const policy of POLICIES) {
|
|
this.assertFalse(
|
|
policy.targetEffect.includes("%"),
|
|
`${policy.id} target effect is stated in popularity points`
|
|
);
|
|
this.assertFalse(
|
|
policy.politicalCost.includes("%"),
|
|
`${policy.id} political cost is stated in popularity points`
|
|
);
|
|
}
|
|
}
|
|
|
|
test_market_and_bank_are_cheap_starter_buildings() {
|
|
this.assertEqual(buildingById("market").baseCost, 500_000);
|
|
this.assertEqual(buildingById("bank").baseCost, 1_000_000);
|
|
}
|
|
|
|
test_new_buildings_declare_their_mechanics() {
|
|
const expected = {
|
|
barracks: "parallel_training",
|
|
shelters: "civilian_shelter",
|
|
aa_building: "air_defense",
|
|
shipyard: "parallel_building",
|
|
port: "port_gdp",
|
|
airport: "air_immigration",
|
|
};
|
|
for (const [id, mechanic] of Object.entries(expected)) {
|
|
this.assertEqual(buildingById(id).mechanic, mechanic, `${id} mechanic`);
|
|
}
|
|
}
|
|
|
|
test_technologies_form_a_tree() {
|
|
const ids = new Set(TECHNOLOGIES.map((t) => t.id));
|
|
for (const tech of TECHNOLOGIES) {
|
|
this.assertGreater(tech.cost, 0, `${tech.id} cost`);
|
|
this.assertNotEmpty(tech.effects, `${tech.id} effects`);
|
|
for (const prerequisite of tech.prerequisites) {
|
|
this.assertTrue(ids.has(prerequisite), `${tech.id} prerequisite ${prerequisite} exists`);
|
|
}
|
|
}
|
|
}
|
|
|
|
test_ports_airports_and_shipyards_declare_their_requirements() {
|
|
const port = buildingById("port");
|
|
const airport = buildingById("airport");
|
|
const shipyard = buildingById("shipyard");
|
|
this.assertTrue(port.coastal, "a port needs the coast");
|
|
this.assertNull(port.requiresBuilding, "a port stands on its own");
|
|
this.assertTrue(shipyard.requiresBuilding === "port", "a shipyard needs a port");
|
|
this.assertTrue(shipyard.coastal, "a shipyard needs the coast too");
|
|
this.assertNull(airport.requiresBuilding, "an airport stands on its own");
|
|
this.assertTrue(port.icon.endsWith(".svg"));
|
|
this.assertTrue(airport.icon.endsWith(".svg"));
|
|
this.assertTrue(shipyard.icon.endsWith(".svg"));
|
|
}
|
|
|
|
test_aircraft_and_warships_are_gated_and_fuelled() {
|
|
for (const id of ["jet_fighter", "bomber"]) {
|
|
const plane = protoUnitById(id);
|
|
this.assertTrue(plane.air, `${id} is an aircraft`);
|
|
this.assertEqual(plane.requiresBuilding, "airport", `${id} needs an airport`);
|
|
this.assertGreater(plane.speed, 1, `${id} is fast`);
|
|
this.assertGreater(plane.enduranceHours, 0, `${id} has endurance`);
|
|
this.assertGreater(plane.missionRange, 0, `${id} has a mission radius`);
|
|
this.assertTrue(plane.icon.endsWith(".svg"));
|
|
}
|
|
this.assertTrue(protoUnitById("bomber").strike, "the bomber strikes from afar");
|
|
const battleship = protoUnitById("battleship");
|
|
this.assertEqual(battleship.requiresBuilding, "shipyard");
|
|
this.assertEqual(battleship.traversableTerrains, ["Sea"]);
|
|
this.assertTrue(battleship.icon.endsWith(".svg"));
|
|
}
|
|
|
|
test_effect_value_scales_with_level() {
|
|
const effect = { baseValue: 10, exponent: 1 };
|
|
this.assertApprox(effectValue(effect, 1), 10);
|
|
this.assertApprox(effectValue(effect, 3), 30);
|
|
this.assertEqual(effectValue(effect, 0), 0);
|
|
}
|
|
|
|
test_tundra_is_rocky_but_flat_and_unproductive() {
|
|
const tundra = TERRAIN_TILES.find((t) => t.terrainType === "Tundra");
|
|
this.assertNotNull(tundra, "tundra exists");
|
|
this.assertEqual(tundra.terrainClass, "Land", "tundra is land");
|
|
this.assertEqual(tundra.defenseBonus, 0, "tundra stays flat");
|
|
this.assertApprox(tundra.populationMultiplier, 0.05, 1e-9);
|
|
this.assertApprox(tundra.gdpMultiplier, 0.2, 1e-9);
|
|
this.assertApprox(tundra.movementCostMultiplier, 2.0, 1e-9);
|
|
}
|
|
}
|