Files
Battle-for-Tismo/tests/game_screen_test.js
T

742 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { TestCase } from "./framework/test_case.js";
import { setupDom, teardownDom } from "./framework/dom.js";
import { GameScreen } from "../client/js/game_screen.js";
import { BUILDINGS, CIVILISATIONS } from "../shared/data.js";
import { smallState, adjacentLand } from "./framework/helpers.js";
function stubNetwork() {
return {
isAdmin: false,
localPlayerId: 2,
players: { 2: { id: 2, name: "Carol", admin: false } },
orders: [],
setup: null,
sendOrder(order) {
this.orders.push(order);
},
sendGameSetup(setup) {
this.setup = setup;
},
};
}
function makeConfig() {
return {
playerName: "Carol",
playerCivId: "france",
civilisations: [CIVILISATIONS[0], CIVILISATIONS[1]],
};
}
export class GameScreenTest extends TestCase {
async test_renders_snapshot_and_sends_orders() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
this.assertTrue(env.$("#screen-game").hasClass("active"));
const state = smallState();
screen.onState(state.snapshot(0));
this.assertEqual(env.$("#civ-name").text(), "France");
this.assertEqual(env.$("#player-name").text(), "Carol");
this.assertTrue(env.$("#stat-pop").text() !== "0");
this.assertTrue(env.$("#stat-date").text().includes("2000"));
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
screen.map.onCitySelected(capital.id);
this.assertFalse(env.$("#city-info").hasClass("hidden"));
this.assertEqual(env.$("#city-name").text(), capital.name);
env.$("#city-manage").click();
this.assertFalse(env.$("#modal-city").hasClass("hidden"));
this.assertSize(env.$("#city-train-list button"), 2, "infantry and artillery can be trained");
env.$("#city-train-list button").first().click();
this.assertEqual(network.orders[0], { type: "train", city: capital.id, proto: 0 });
const unit = state.units.find((u) => u.civ === 0);
const goal = adjacentLand(state, unit);
await screen.map.onMoveOrdered([unit.id], goal);
this.assertEqual(network.orders[1], { type: "move", unit: unit.id, coords: [goal.x, goal.y] });
this.assertTrue(screen.map._unitMotion.get(unit.id).predicting, "the move was predicted");
screen.sendSetup();
this.assertEqual(network.setup.player_civ, 0);
this.assertEqual(network.setup.civilisations, ["france", "britain"]);
screen.leave();
this.assertFalse(env.$("#screen-game").hasClass("active"));
} finally {
teardownDom(env);
}
}
async test_nation_panel_opens() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
screen.onState(smallState().snapshot(0));
env.$("#civ-flag").click();
this.assertFalse(env.$("#modal-nation").hasClass("hidden"));
this.assertSize(env.$("#government-list .row-card"), 3);
this.assertSize(env.$("#technology-list .row-card"), 5);
screen.leave();
} finally {
teardownDom(env);
}
}
async test_buildings_and_nation_modals_refresh_on_snapshot() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
state.budgets.set(0, 1.0e15);
screen.onState(state.snapshot(0));
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
screen.map.onCitySelected(capital.id);
env.$("#city-manage").click();
this.assertFalse(env.$("#modal-city").hasClass("hidden"));
this.assertSize(env.$("#city-buildings-list .row-card"), BUILDINGS.length);
screen.onState(state.snapshot(0));
this.assertFalse(env.$("#modal-city").hasClass("hidden"));
env.$("#civ-flag").click();
this.assertFalse(env.$("#modal-nation").hasClass("hidden"));
screen.onState(state.snapshot(0));
this.assertFalse(env.$("#modal-nation").hasClass("hidden"));
screen.leave();
} finally {
teardownDom(env);
}
}
async test_city_budget_tab_shows_the_region_budget() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
screen.onState(state.snapshot(0));
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
screen.map.onCitySelected(capital.id);
env.$("#city-manage").click();
env.$("#modal-city .tab[data-tab=budget]").click();
this.assertTrue(env.$("#city-tab-budget").hasClass("active"), "the budget tab opens");
const cityStats = state.snapshot(0).cityStats.find((c) => c.id === capital.id);
this.assertNotNull(cityStats.budget, "the city stats carry a region budget");
const text = env.$("#city-budget-list").text();
this.assertTrue(text.includes("Tax income"), "region income is listed");
this.assertTrue(text.includes("TOTAL"), "the region net is totalled");
screen.leave();
} finally {
teardownDom(env);
}
}
async test_training_modal_tracks_progress() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
state.budgets.set(0, 1.0e15);
screen.onState(state.snapshot(0));
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
screen.map.onCitySelected(capital.id);
env.$("#city-manage").click();
this.assertTrue(state.requestTrain(capital.id, 0));
screen.onState(state.snapshot(0));
this.assertFalse(env.$("#city-train-status").hasClass("hidden"));
this.assertTrue(env.$("#city-train-label").text().includes("Training"));
state.training.get(capital.id)[0].elapsedHours = 3;
screen.onState(state.snapshot(0));
this.assertTrue(env.$("#city-train-label").text().includes("left"));
state.training.delete(capital.id);
screen.onState(state.snapshot(0));
this.assertEqual(env.$("#city-train-label").text(), "Training complete");
screen.leave();
} finally {
teardownDom(env);
}
}
async test_attack_and_scheduled_move_orders_are_serialised() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
const state = smallState();
screen.onState(state.snapshot(0));
const raw = state.units.find((u) => u.civ === 0);
const unit = screen._unit(raw.id);
const goal = adjacentLand(state, raw);
await screen._sendMove([unit], goal, null, "attack");
this.assertEqual(network.orders[0], {
type: "attack",
unit: unit.id,
coords: [goal.x, goal.y],
});
await screen._sendMove([unit], goal, null, "schedule");
this.assertEqual(network.orders[1], {
type: "schedule",
unit: unit.id,
coords: [goal.x, goal.y],
goals: [[goal.x, goal.y]],
append: true,
});
screen.leave();
} finally {
teardownDom(env);
}
}
async test_ranged_attack_is_sent_without_predicting_a_march() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
const state = smallState();
const home = state.units.find((u) => u.civ === 0);
const artillery = state._spawnUnit(
home.coords,
0,
state.protoUnits.find((u) => u.id === "artillery")
);
const goal = adjacentLand(state, artillery);
const defender = state._spawnUnit(goal, 1, state.protoUnits[0]);
screen.onState(state.snapshot(0));
const unit = screen._unit(artillery.id);
this.assertNotNull(screen._unit(defender.id), "the enemy stack is visible");
await screen._sendMove([unit], goal, null, "attack");
this.assertEqual(network.orders[0], {
type: "attack",
unit: unit.id,
coords: [goal.x, goal.y],
});
this.assertFalse(
screen.map._unitMotion.get(unit.id).predicting,
"the icon does not march toward a target hit from range"
);
screen.leave();
} finally {
teardownDom(env);
}
}
async test_training_modal_lists_the_queue_and_cancels() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
const state = smallState();
screen.onState(state.snapshot(0));
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
screen.map.onCitySelected(capital.id);
env.$("#city-manage").click();
state.requestTrain(capital.id, 0);
state.requestTrain(capital.id, 0);
screen.onState(state.snapshot(0));
this.assertSize(env.$("#city-train-queue .queue-row"), 1, "contiguous identical units merge");
this.assertTrue(
env.$("#city-train-queue .queue-row").first().text().includes("×2"),
"the merged row shows how many units it holds"
);
this.assertTrue(
env.$("#city-train-queue .queue-row").first().text().includes("all ready in"),
"the merged row forecasts the whole run"
);
env.$("#city-train-queue .queue-row").first().find("button").click();
this.assertEqual(network.orders[0], { type: "cancel_train", city: capital.id, index: 1 });
screen.leave();
} finally {
teardownDom(env);
}
}
async test_bottom_panel_information() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
screen.onState(state.snapshot(0));
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
screen.map.onTileRequested(capital.coords);
this.assertFalse(env.$("#tile-info").hasClass("hidden"));
this.assertTrue(env.$("#tile-coords").text().includes("Tile"));
this.assertTrue(env.$("#tile-gdp").text().includes("GDP/capita"));
this.assertTrue(
env.$("#tile-gdp").text().includes("Capital"),
"the tile panel explains the capital GDP bonus"
);
screen._renderTileGdp({
base: 10000,
factors: [
{ label: "Terrain", multiplier: 1 },
{ label: "City", multiplier: 2 },
{ label: "Battle damage", amount: 0 },
],
total: 20000,
});
const gdpText = env.$("#tile-gdp").text();
this.assertFalse(gdpText.includes("Terrain"), "a no-op multiplier is hidden");
this.assertFalse(gdpText.includes("Battle damage"), "a zero modifier is hidden");
this.assertTrue(gdpText.includes("City"), "a real modifier is still shown");
const unit = state.units.find((u) => u.civ === 0);
screen.map.onUnitSelected(unit.id);
this.assertFalse(env.$("#unit-info").hasClass("hidden"));
this.assertTrue(env.$("#unit-name").text().length > 0);
this.assertEqual(
env.$("#unit-health-label").text(),
`${Math.round(unit.hp)} / ${Math.round(unit.maxHp)} HP`,
"the panel shows hit points next to the bar"
);
screen._showUnit(null);
this.assertTrue(env.$("#unit-info").hasClass("hidden"));
screen._showCity(null);
this.assertTrue(env.$("#city-info").hasClass("hidden"));
this.assertNull(screen._unit(999));
this.assertNull(screen._city(999));
screen.leave();
} finally {
teardownDom(env);
}
}
async test_keyboard_and_render_loop() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
screen.onState(smallState().snapshot(0));
const before = screen.map.camera.x;
env.$(env.window).trigger(env.$.Event("keydown", { key: "d" }));
this.assertApprox(screen.map.camera.x, before - 40, 1e-9);
env.$(env.window).trigger(env.$.Event("keydown", { key: "a", shiftKey: true }));
this.assertApprox(screen.map.camera.x, before - 40 + 120, 1e-9);
const zoom = screen.map.camera.zoom;
env.$(env.window).trigger(env.$.Event("keydown", { key: "+" }));
this.assertGreater(screen.map.camera.zoom, zoom);
const zoomedIn = screen.map.camera.zoom;
env.$(env.window).trigger(env.$.Event("keydown", { key: "-" }));
this.assertLess(screen.map.camera.zoom, zoomedIn);
env.$(env.window).trigger(env.$.Event("keydown", { key: "z" }));
env.$(env.window).trigger(env.$.Event("keydown", { key: "Escape" }));
screen._lastFrame = 0;
screen._frame(1000);
screen._frame(2000);
screen._frame(3000);
this.assertTrue(env.$("#fps-label").text().includes("FPS"));
screen.leave();
screen._frame(4000);
} finally {
teardownDom(env);
}
}
async test_snapshot_refreshes_researched_and_buildings() { const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
state.research.set(0, 1.0e6);
state.budgets.set(0, 1.0e15);
const method = state.technologies.findIndex((t) => t.id === "scientific_method");
state.requestResearchTechnology(0, method);
screen.onState(state.snapshot(0));
this.assertTrue(screen._researched.has(method), "researched technologies are mirrored");
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
state.requestBuild(capital.id, 0);
for (let i = 0; i < 1000 && state.training.size > 0; i++) state.advanceHour();
screen.onState(state.snapshot(0));
this.assertEqual(screen._cityBuildings.get(capital.id)["0"], 1, "building levels are mirrored");
env.$("#civ-flag").click();
this.assertFalse(env.$("#modal-nation").hasClass("hidden"));
screen.leave();
} finally {
teardownDom(env);
}
}
async test_unit_panel_shows_terrain_defense_and_tooltip() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
const mountain = state.landCells.find(
(c) => state.tiles[`${c.x},${c.y}`].terrainType === "Mountain"
);
this.assertNotNull(mountain, "the map has a mountain");
const unit = state._spawnUnit(mountain, 0, state.protoUnits[0]);
screen.onState(state.snapshot(0));
const data = state.snapshot(0).units.find((u) => u.id === unit.id);
screen._showUnit(data);
const proto = screen.protoUnits[unit.proto];
this.assertEqual(env.$("#unit-attack").text(), `Attack ${proto.attack}`);
this.assertEqual(env.$("#unit-attack").attr("data-tooltip"), `Base ${proto.attack}`);
this.assertEqual(env.$("#unit-defense").text(), `Defense ${proto.defense * 2}`);
this.assertEqual(
env.$("#unit-defense").attr("data-tooltip"),
`Base ${proto.defense}, Mountain +${proto.defense}`
);
env.$("#unit-defense").trigger("mouseenter");
this.assertFalse(env.$("#stat-tooltip").hasClass("hidden"));
this.assertEqual(
env.$("#stat-tooltip").text(),
`Base ${proto.defense}, Mountain +${proto.defense}`
);
env.$("#unit-defense").trigger("mouseleave");
this.assertTrue(env.$("#stat-tooltip").hasClass("hidden"));
screen.leave();
} finally {
teardownDom(env);
}
}
async test_unit_panel_shows_city_defense_bonus() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
const plain = state.landCells.find(
(c) => state.tiles[`${c.x},${c.y}`].terrainType === "Land" &&
!state.cityAt(c) && state.topology.neighbours(c.x, c.y).some((n) => state._isLand(n))
);
this.assertNotNull(plain, "found a plain tile");
state._spawnCity({ civIndex: 0, coords: { x: plain.x, y: plain.y }, isCapital: false });
state._rebuildCityIndex();
const unit = state._spawnUnit(plain, 0, state.protoUnits[0]);
screen.onState(state.snapshot(0));
screen._showUnit(state.snapshot(0).units.find((u) => u.id === unit.id));
const proto = screen.protoUnits[unit.proto];
this.assertEqual(env.$("#unit-defense").text(), `Defense ${Math.round(proto.defense * 1.5)}`);
this.assertEqual(
env.$("#unit-defense").attr("data-tooltip"),
`Base ${proto.defense}, City +${Math.round(proto.defense * 0.5)}`
);
screen.leave();
} finally {
teardownDom(env);
}
}
async test_move_into_peaceful_territory_prompts_for_war() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
const state = smallState();
screen.onState(state.snapshot(0));
const unit = state.units.find((u) => u.civ === 0);
const goal = { x: 900, y: 900 };
screen.map.territory.set("900,900", 1);
screen.map.planMove = () => [{ x: 1, y: 1 }, goal];
let predicted = null;
screen.map.predictMove = (u, coords) => {
predicted = { u, coords };
};
await screen.map.onMoveOrdered([unit.id], goal);
this.assertFalse(env.$("#modal-confirm").hasClass("hidden"), "the war prompt opens");
this.assertEmpty(network.orders, "nothing is sent before confirming");
this.assertTrue(env.$("#confirm-message").text().includes("Britain"));
env.$("#confirm-ok").click();
this.assertEqual(network.orders[0], { type: "move", unit: unit.id, coords: [900, 900] });
this.assertNotNull(predicted, "the confirmed move is predicted locally");
screen.leave();
} finally {
teardownDom(env);
}
}
async test_cancelling_the_war_prompt_sends_nothing() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
const state = smallState();
screen.onState(state.snapshot(0));
const unit = state.units.find((u) => u.civ === 0);
const goal = { x: 902, y: 902 };
screen.map.territory.set("902,902", 1);
screen.map.planMove = () => [{ x: 1, y: 1 }, goal];
await screen.map.onMoveOrdered([unit.id], goal);
env.$("#confirm-cancel").click();
this.assertEmpty(network.orders, "cancelling keeps the peace");
screen.leave();
} finally {
teardownDom(env);
}
}
async test_move_into_enemy_territory_skips_the_prompt() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
const state = smallState();
state.requestDeclareWar(0, 1);
screen.onState(state.snapshot(0));
const unit = state.units.find((u) => u.civ === 0);
const goal = { x: 901, y: 901 };
screen.map.territory.set("901,901", 1);
screen.map.planMove = () => [{ x: 1, y: 1 }, goal];
screen.map.predictMove = () => null;
await screen.map.onMoveOrdered([unit.id], goal);
this.assertTrue(env.$("#modal-confirm").hasClass("hidden"));
this.assertEqual(network.orders[0], { type: "move", unit: unit.id, coords: [901, 901] });
screen.leave();
} finally {
teardownDom(env);
}
}
async test_diplomacy_tab_sends_a_declare_war_order() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
screen.onState(smallState().snapshot(0));
env.$("#civ-flag").click();
env.$("#modal-nation .tab[data-tab=diplomacy]").click();
const row = env.$("#diplomacy-list tbody tr").first();
this.assertTrue(row.find(".name").text().length > 0);
row.find("button").click();
this.assertEqual(network.orders[0].type, "declare_war");
this.assertEqual(network.orders[0].target, 1);
screen.leave();
} finally {
teardownDom(env);
}
}
async test_news_feed_shows_declarations() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
state.requestDeclareWar(0, 1);
screen.onState(state.snapshot(0));
env.$("#news-button").click();
this.assertFalse(env.$("#modal-news").hasClass("hidden"));
this.assertTrue(env.$("#news-list").text().includes("declared war on Britain"));
screen.leave();
} finally {
teardownDom(env);
}
}
async test_new_news_pops_up_in_the_corner_feed() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
// The first snapshot is history: it must not flood the feed.
screen.onState(state.snapshot(0));
this.assertSize(env.$("#game-feed .feed-line"), 0);
state.requestDeclareWar(0, 1);
screen.onState(state.snapshot(0));
this.assertSize(env.$("#game-feed .feed-line"), 1);
this.assertTrue(env.$("#game-feed .feed-line").text().includes("declared war on Britain"));
screen.leave();
} finally {
teardownDom(env);
}
}
async test_war_declared_on_the_player_is_an_alert() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
screen.onState(state.snapshot(0));
state.requestDeclareWar(1, 0);
screen.onState(state.snapshot(0));
const alert = env.$("#game-feed .feed-line.feed-alert");
this.assertSize(alert, 1);
this.assertTrue(alert.text().includes("declared war on you"));
screen.leave();
this.assertSize(env.$("#game-feed .feed-line"), 0, "leaving clears the feed");
} finally {
teardownDom(env);
}
}
async test_stack_menu_selects_all_and_splits_one() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
screen.onState(state.snapshot(0));
screen.map.onStackMenu(unit.coords, [unit.id, second.id], 100, 120);
this.assertFalse(env.$("#stack-menu").hasClass("hidden"));
this.assertSize(env.$("#stack-menu .stack-item"), 3, "two units plus select all");
env.$("#stack-menu .stack-all").click();
this.assertEqual(screen.selectedUnitIds, [unit.id, second.id]);
this.assertTrue(env.$("#unit-name").text().includes("2 units selected"));
this.assertTrue(env.$("#stack-menu").hasClass("hidden"), "the menu closes on pick");
screen.map.onStackMenu(unit.coords, [unit.id, second.id], 100, 120);
env.$("#stack-menu .stack-item").not(".stack-move").not(".stack-all").first().click();
this.assertEqual(screen.selectedUnitIds, [unit.id], "picking one splits the stack");
screen.leave();
} finally {
teardownDom(env);
}
}
async test_clicking_a_stacked_unit_selects_the_whole_stack() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
screen.onState(state.snapshot(0));
screen.map.onUnitSelected(unit.id);
this.assertEqual(
screen.selectedUnitIds.slice().sort((a, b) => a - b),
[unit.id, second.id].sort((a, b) => a - b),
"the whole stack is selected"
);
// The next order is given to every unit in the stack.
const goal = adjacentLand(state, unit);
await screen.map._onRightClick(goal, {});
this.assertEqual(network.orders[0].type, "move");
this.assertEqual(
network.orders[0].units.slice().sort((a, b) => a - b),
[unit.id, second.id].sort((a, b) => a - b)
);
screen.leave();
} finally {
teardownDom(env);
}
}
async test_group_move_uses_the_slowest_speed() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
const state = smallState();
const capital = state.units.find((u) => u.civ === 0);
const infantry = state._spawnUnit(capital.coords, 0, state.protoUnits[0]);
const artillery = state._spawnUnit(capital.coords, 0, state.protoUnits[2]);
screen.onState(state.snapshot(0));
screen._selectUnits([infantry.id, artillery.id]);
const goal = adjacentLand(state, capital);
await screen.map.onMoveOrdered([infantry.id, artillery.id], goal);
this.assertEqual(network.orders[0].type, "move");
this.assertEqual(network.orders[0].units, [infantry.id, artillery.id]);
const infHours = screen.map._unitMotion.get(infantry.id).segmentHours[0];
const artHours = screen.map._unitMotion.get(artillery.id).segmentHours[0];
this.assertApprox(infHours, artHours, 1e-9, "both move at the slowest member's pace");
screen.leave();
} finally {
teardownDom(env);
}
}
async test_pillage_button_forecasts_and_sends_the_order() {
const env = await setupDom();
try {
const network = stubNetwork();
const screen = new GameScreen(network, makeConfig());
screen.enter();
const state = smallState();
const coords = findEnemyTile(state);
this.assertNotNull(coords, "found an enemy tile");
state.tilePopulation.set(`${coords.x},${coords.y}`, 100000);
const first = state._spawnUnit(coords, 0, state.protoUnits[0]);
const second = state._spawnUnit(coords, 0, state.protoUnits[0]);
first.hp = 100;
second.hp = 90;
screen.onState(state.snapshot(0));
screen._selectUnits([first.id, second.id]);
const $button = env.$("#unit-pillage");
this.assertFalse($button.hasClass("hidden"), "the pillage button shows");
const tooltip = $button.attr("data-tooltip") || "";
this.assertTrue(tooltip.includes("19 000"), `forecast removes 19k: ${tooltip}`);
this.assertTrue(tooltip.includes("0.49"), "forecast shows the GDP multiplier");
this.assertTrue(tooltip.includes("+10") && tooltip.includes("+9"), "forecast shows both heals");
$button.click();
this.assertEqual(network.orders[0], { type: "pillage", units: [first.id, second.id] });
screen.leave();
} finally {
teardownDom(env);
}
}
async test_pillage_button_hides_in_friendly_land() {
const env = await setupDom();
try {
const screen = new GameScreen(stubNetwork(), makeConfig());
screen.enter();
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
screen.onState(state.snapshot(0));
screen._selectUnits([unit.id]);
this.assertTrue(env.$("#unit-pillage").hasClass("hidden"));
screen.leave();
} finally {
teardownDom(env);
}
}
}
function findEnemyTile(state) {
for (const [k, owner] of state.territory) {
if (owner !== 1) continue;
const i = k.indexOf(",");
const coords = { x: Number(k.slice(0, i)), y: Number(k.slice(i + 1)) };
if (state.unitAt(coords) || state.cityAt(coords)) continue;
return coords;
}
return null;
}