- Tile improvements (fortifications, radar towers, coastal cannons), trenches, zone of control, support-unit capture and launch transports, with parabolic barrage arcs drawn between attacker and target. - Focused research that pours points straight into the chosen technology, with per-technology progress kept when the focus changes, plus repeatable levels. - Popularity reworked to integer points, war and pillage effects, minority policies and month-over-month arrows on the nation summary. - City progress bars, square improvement buttons, a technology tab of wide short cards with progress bars, transport cargo readout and double-click to open a city panel.
192 lines
8.4 KiB
JavaScript
192 lines
8.4 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { smallState } from "./framework/helpers.js";
|
|
import { TECHNOLOGIES, EFFECT_INFANTRY_ATTACK, EFFECT_CONSTRUCTION_SPEED } from "../shared/data.js";
|
|
import { NEWS_TECHNOLOGY } from "../shared/data/relations.js";
|
|
|
|
export class GameStateTechnologyTest extends TestCase {
|
|
index(state, id) {
|
|
return state.technologies.findIndex((t) => t.id === id);
|
|
}
|
|
|
|
// Focuses `id` and pours exactly its cost of points into it, the way a nation
|
|
// would over time; returns the index.
|
|
complete(state, id, civ = 0) {
|
|
const index = this.index(state, id);
|
|
this.assertTrue(state.requestFocusTechnology(civ, index), `focusing ${id}`);
|
|
state._addResearch(civ, state.getTechnologyCost(civ, index));
|
|
return index;
|
|
}
|
|
|
|
test_catalogue_links_prerequisites() {
|
|
const method = TECHNOLOGIES.find((t) => t.id === "scientific_method");
|
|
this.assertEmpty(method.prerequisites);
|
|
this.assertGreater(method.cost, 0);
|
|
const automation = TECHNOLOGIES.find((t) => t.id === "industrial_automation");
|
|
this.assertEqual(automation.prerequisites, ["scientific_method"]);
|
|
}
|
|
|
|
test_every_civilisation_starts_with_nothing_researched() {
|
|
const state = smallState();
|
|
this.assertSize(state.technologies, TECHNOLOGIES.length);
|
|
this.assertFalse(state.isResearched(0, 0));
|
|
this.assertEmpty(state.getResearchedIndices(0));
|
|
}
|
|
|
|
test_research_points_fill_the_focused_technology() {
|
|
const state = smallState();
|
|
const index = this.index(state, "scientific_method");
|
|
this.assertTrue(state.requestFocusTechnology(0, index), "the focus is accepted");
|
|
const cost = state.getTechnologyCost(0, index);
|
|
// Points flow in as they are created; just short of the cost leaves it in
|
|
// progress rather than in a bank.
|
|
state._addResearch(0, cost - 1);
|
|
this.assertFalse(state.isResearched(0, index), "still researching");
|
|
this.assertApprox(state.getTechnologyProgress(0), cost - 1, 1e-6);
|
|
state._addResearch(0, 1);
|
|
this.assertTrue(state.isResearched(0, index), "the technology is discovered");
|
|
this.assertEqual(state.getFocusedTechnology(0), -1, "the focus clears");
|
|
}
|
|
|
|
test_switching_focus_keeps_committed_progress() {
|
|
const state = smallState();
|
|
this.complete(state, "scientific_method");
|
|
const weapons = this.index(state, "infantry_weapons");
|
|
const armour = this.index(state, "body_armour");
|
|
this.assertTrue(state.requestFocusTechnology(0, weapons));
|
|
state._addResearch(0, 7);
|
|
this.assertApprox(state.getTechnologyProgress(0, weapons), 7, 1e-9);
|
|
// Switch focus: the weapons points stay committed.
|
|
this.assertTrue(state.requestFocusTechnology(0, armour));
|
|
this.assertApprox(state.getTechnologyProgress(0, weapons), 7, 1e-9, "weapons kept its work");
|
|
this.assertApprox(state.getTechnologyProgress(0, armour), 0, 1e-9);
|
|
state._addResearch(0, 5);
|
|
this.assertApprox(state.getTechnologyProgress(0, armour), 5, 1e-9);
|
|
// Switching back resumes from the seven points already committed.
|
|
this.assertTrue(state.requestFocusTechnology(0, weapons));
|
|
this.assertApprox(state.getTechnologyProgress(0, weapons), 7, 1e-9);
|
|
this.assertEqual(state.getTechnologyProgress(0), 7, "the focus reports its own progress");
|
|
this.assertApprox(state.snapshot(0).techProgress[0][weapons], 7, 1e-9, "progress is shipped");
|
|
}
|
|
|
|
test_nothing_is_researched_without_a_focus() {
|
|
const state = smallState();
|
|
const index = this.index(state, "scientific_method");
|
|
// Points created with nothing focused are simply not stored anywhere.
|
|
state._addResearch(0, state.getTechnologyCost(0, index) * 10);
|
|
this.assertFalse(state.isResearched(0, index));
|
|
this.assertApprox(state.getTechnologyProgress(0), 0, 1e-9);
|
|
}
|
|
|
|
test_focus_rejects_unknown_technology_and_civilisation() {
|
|
const state = smallState();
|
|
this.assertFalse(state.requestFocusTechnology(0, 999), "unknown technology");
|
|
this.assertFalse(state.requestFocusTechnology(999, 0), "unknown civilisation");
|
|
}
|
|
|
|
test_focus_rejects_when_prerequisites_are_missing() {
|
|
const state = smallState();
|
|
const space = this.index(state, "space_program");
|
|
this.assertFalse(state.canResearchTechnology(0, space));
|
|
this.assertFalse(state.requestFocusTechnology(0, space));
|
|
}
|
|
|
|
test_prerequisites_unlock_the_tree() {
|
|
const state = smallState();
|
|
const automation = this.index(state, "industrial_automation");
|
|
this.assertFalse(state.requestFocusTechnology(0, automation), "locked before its prerequisite");
|
|
this.complete(state, "scientific_method");
|
|
this.assertFalse(state.isResearched(0, automation), "a prerequisite is not auto-granted");
|
|
this.complete(state, "industrial_automation");
|
|
this.assertTrue(state.isResearched(0, automation));
|
|
this.complete(state, "quantum_computing");
|
|
this.assertTrue(state.requestFocusTechnology(0, this.index(state, "space_program")));
|
|
}
|
|
|
|
test_technology_effects_are_folded_into_gdp() {
|
|
const state = smallState();
|
|
const before = state.getPlayerGdp(0);
|
|
this.complete(state, "scientific_method");
|
|
this.assertApprox(state.getPlayerGdp(0), (before * 1.07) / 1.05, 0.01);
|
|
}
|
|
|
|
test_researched_technology_adds_its_research_rate() {
|
|
const withTech = smallState();
|
|
const without = smallState();
|
|
this.complete(withTech, "scientific_method");
|
|
this.assertApprox(
|
|
withTech.viewerStats(0).researchRate - without.viewerStats(0).researchRate,
|
|
1.0,
|
|
1e-9
|
|
);
|
|
}
|
|
|
|
test_snapshot_exposes_technologies_and_progress() {
|
|
const state = smallState();
|
|
const index = this.complete(state, "scientific_method");
|
|
const snapshot = state.snapshot(0);
|
|
this.assertSize(snapshot.technologies, state.technologies.length);
|
|
this.assertSize(snapshot.researched, state.civilisations.length);
|
|
this.assertTrue(snapshot.researched[0].includes(index));
|
|
this.assertEmpty(snapshot.researched[1]);
|
|
}
|
|
|
|
test_repeatable_research_costs_two_percent_more_each_level() {
|
|
const state = smallState();
|
|
state.researched.get(0).add(this.index(state, "scientific_method"));
|
|
const index = this.index(state, "infantry_weapons");
|
|
const base = state.getTechnologyCost(0, index);
|
|
this.assertTrue(state.requestFocusTechnology(0, index), "first level");
|
|
state._addResearch(0, base);
|
|
this.assertEqual(state.getTechnologyRepeatCount(0, index), 1);
|
|
this.assertApprox(
|
|
state.getTechnologyCost(0, index),
|
|
base * 1.02,
|
|
1e-6,
|
|
"the next level costs 2% more"
|
|
);
|
|
this.assertTrue(state.requestFocusTechnology(0, index), "second level");
|
|
state._addResearch(0, state.getTechnologyCost(0, index));
|
|
this.assertEqual(state.getTechnologyRepeatCount(0, index), 2);
|
|
this.assertApprox(state.getTechnologyCost(0, index), base * 1.02 * 1.02, 1e-6);
|
|
}
|
|
|
|
test_repeatable_effects_stack_with_each_level() {
|
|
const state = smallState();
|
|
state.researched.get(0).add(this.index(state, "scientific_method"));
|
|
const weapons = this.index(state, "infantry_weapons");
|
|
const construction = this.index(state, "construction_technology");
|
|
for (let level = 0; level < 2; level++) {
|
|
state.requestFocusTechnology(0, weapons);
|
|
state._addResearch(0, state.getTechnologyCost(0, weapons));
|
|
}
|
|
state.requestFocusTechnology(0, construction);
|
|
state._addResearch(0, state.getTechnologyCost(0, construction));
|
|
const modifiers = state.getCivModifiers(0);
|
|
this.assertApprox(modifiers[EFFECT_INFANTRY_ATTACK], 0.02, 1e-9, "two levels stack");
|
|
this.assertApprox(modifiers[EFFECT_CONSTRUCTION_SPEED], 0.01, 1e-9);
|
|
this.assertApprox(state.constructionSpeedMultiplier(0), 1 / 1.01, 1e-9);
|
|
}
|
|
|
|
test_repeatable_research_notifies_without_news() {
|
|
const state = smallState();
|
|
this.complete(state, "scientific_method");
|
|
this.complete(state, "body_armour");
|
|
const notices = state.news.filter((entry) => entry.type === NEWS_TECHNOLOGY);
|
|
const normalNotice = notices.find((entry) => entry.technology === "scientific_method");
|
|
const repeatNotice = notices.find((entry) => entry.technology === "body_armour");
|
|
this.assertNotNull(normalNotice);
|
|
this.assertFalse(!!normalNotice.repeatable, "a discovery is a real news event");
|
|
this.assertNotNull(repeatNotice);
|
|
this.assertTrue(repeatNotice.repeatable, "a repeat level is only a notice");
|
|
}
|
|
|
|
test_repeatable_technologies_have_a_theme() {
|
|
for (const proto of TECHNOLOGIES) {
|
|
this.assertTrue(
|
|
typeof proto.theme === "string" && proto.theme.length > 0,
|
|
`${proto.id} is filed under a theme`
|
|
);
|
|
}
|
|
}
|
|
}
|