Every road, railway and tile work now rises as an economic construction site instead of a queue entry. A site gathers its materials over days from reachable regions, refusing any supplier quoting above the price it was budgeted at (a 1% tolerance); a day that buys nothing stalls the site, raising a red mark and letting the nation raise the budget to current prices or cancel it. The upfront budget covers the build cost plus the market value of its steel and high-tech, energy stays a grid draw, and any money left over stays with the finished work as a maintenance reserve. That reserve now pays the work's own daily steel and high-tech wear before the treasury is billed, so a well funded work spares the state until it runs dry; the spent money is paid into the nation's regions. Building upgrades run through the same gather-build-maintain machinery, sharing the site's kind flag and target level. The legacy tile-work queue and the market building are gone. Bundles the in-progress chart sizing for the central bank panel.
213 lines
8.4 KiB
JavaScript
213 lines
8.4 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,
|
|
RESOURCE_BUILDINGS,
|
|
} 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("barracks"), BUILDINGS.find((b) => b.id === "barracks"));
|
|
this.assertEqual(governmentById("republic"), GOVERNMENTS[0]);
|
|
this.assertEqual(technologyById("scientific_method"), TECHNOLOGIES[0]);
|
|
}
|
|
|
|
test_resource_building_output_is_scaled_up() {
|
|
// One work stands for a whole industry: the authored output is multiplied
|
|
// tenfold so fewer of them stand on the map.
|
|
const mill = RESOURCE_BUILDINGS.find((b) => b.id === "steel_mill");
|
|
this.assertApprox(mill.outputPerDay, 20_000, 1e-6);
|
|
}
|
|
|
|
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.productionMultiplier, PILLAGE.productionPenalty, 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.productionMultiplier, 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_gone() {
|
|
// The Market was removed and the tax-raising Bank is gone too: the state
|
|
// lives on trade taxes alone.
|
|
this.assertEqual(buildingById("market"), null);
|
|
this.assertEqual(buildingById("bank"), null);
|
|
}
|
|
|
|
test_new_buildings_declare_their_mechanics() {
|
|
const expected = {
|
|
barracks: "parallel_training",
|
|
shelters: "civilian_shelter",
|
|
aa_building: "air_defense",
|
|
shipyard: "parallel_building",
|
|
port: "port_production",
|
|
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.productionMultiplier, 0.2, 1e-9);
|
|
this.assertApprox(tundra.movementCostMultiplier, 2.0, 1e-9);
|
|
}
|
|
}
|