665 lines
24 KiB
JavaScript
665 lines
24 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
||
import { setupDom, teardownDom } from "./framework/dom.js";
|
||
import { GameScreen } from "../client/js/game_screen.js";
|
||
import { CIVILISATIONS } from "../shared/data.js";
|
||
import { defaultState, 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 = defaultState();
|
||
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-train").click();
|
||
this.assertFalse(env.$("#modal-train").hasClass("hidden"));
|
||
this.assertSize(env.$("#train-list button"), 2, "infantry and artillery can be trained");
|
||
env.$("#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(defaultState().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 = defaultState();
|
||
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-build").click();
|
||
this.assertFalse(env.$("#modal-buildings").hasClass("hidden"));
|
||
this.assertSize(env.$("#buildings-list .row-card"), 6);
|
||
screen.onState(state.snapshot(0));
|
||
this.assertFalse(env.$("#modal-buildings").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_training_modal_tracks_progress() {
|
||
const env = await setupDom();
|
||
try {
|
||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||
screen.enter();
|
||
const state = defaultState();
|
||
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-train").click();
|
||
this.assertTrue(state.requestTrain(capital.id, 0));
|
||
screen.onState(state.snapshot(0));
|
||
this.assertFalse(env.$("#train-status").hasClass("hidden"));
|
||
this.assertTrue(env.$("#train-label").text().includes("Training"));
|
||
|
||
state.training.get(capital.id)[0].elapsedHours = 3;
|
||
screen.onState(state.snapshot(0));
|
||
this.assertTrue(env.$("#train-label").text().includes("left"));
|
||
|
||
state.training.delete(capital.id);
|
||
screen.onState(state.snapshot(0));
|
||
this.assertEqual(env.$("#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 = defaultState();
|
||
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_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 = defaultState();
|
||
screen.onState(state.snapshot(0));
|
||
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
||
screen.map.onCitySelected(capital.id);
|
||
env.$("#city-train").click();
|
||
state.requestTrain(capital.id, 0);
|
||
state.requestTrain(capital.id, 0);
|
||
screen.onState(state.snapshot(0));
|
||
this.assertSize(env.$("#train-queue .queue-row"), 1, "contiguous identical units merge");
|
||
this.assertTrue(
|
||
env.$("#train-queue .queue-row").first().text().includes("×2"),
|
||
"the merged row shows how many units it holds"
|
||
);
|
||
this.assertTrue(
|
||
env.$("#train-queue .queue-row").first().text().includes("all ready in"),
|
||
"the merged row forecasts the whole run"
|
||
);
|
||
env.$("#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 = defaultState();
|
||
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"));
|
||
|
||
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(defaultState().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 = defaultState();
|
||
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);
|
||
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 = defaultState();
|
||
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 = defaultState();
|
||
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 = defaultState();
|
||
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 = defaultState();
|
||
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 = defaultState();
|
||
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(defaultState().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 = defaultState();
|
||
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 = defaultState();
|
||
// 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 = defaultState();
|
||
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 = defaultState();
|
||
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 = defaultState();
|
||
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 = defaultState();
|
||
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 = defaultState();
|
||
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 = defaultState();
|
||
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;
|
||
}
|