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

465 lines
18 KiB
JavaScript

import { teardownDom } from "./framework/dom.js";
import { MapViewFixture } from "./framework/map_view_fixture.js";
import { mapToLocal, COL_STEP, HEX_W, parseKey } from "../shared/hex.js";
import { CAMERA_TILT, HEIGHT_LIFT } from "../client/js/map_view/constants.js";
import { smallState, adjacentLand, seaTile } from "./framework/helpers.js";
import { TREATY_ALLIANCE } from "../shared/data.js";
export class MapViewInputTest extends MapViewFixture {
async test_selecting_a_unit_marks_its_view() {
const state = smallState();
const { env, map, snapshot } = await this.build(state);
try {
const unit = snapshot.units.find((u) => u.civ === 0);
map.setSelectedUnit(unit.id);
this.assertSize(env.$("#layer-entities .unit.selected"), 1);
this.assertEqual(map.unitAt({ x: unit.coords[0], y: unit.coords[1] }), unit.id);
} finally {
teardownDom(env);
}
}
async test_click_handling_selects_and_orders() {
const state = smallState();
const { env, map, snapshot } = await this.build(state);
try {
const unit = snapshot.units.find((u) => u.civ === 0);
let selected = -1;
map.onUnitSelected = (id) => {
selected = id;
};
map._onLeftClick({ x: unit.coords[0], y: unit.coords[1] });
this.assertEqual(selected, unit.id);
let order = null;
map.onMoveOrdered = (ids, coords) => {
order = { ids, coords };
};
map.setSelectedUnit(unit.id);
const stateUnit = state.units.find((u) => u.id === unit.id);
const goal = adjacentLand(state, stateUnit);
map._onRightClick(goal);
this.assertEqual(order.ids, [unit.id]);
this.assertEqual(order.coords, goal);
let tile = null;
map.onTileRequested = (coords) => {
tile = coords;
};
map.clearSelection();
map._onRightClick(goal);
this.assertEqual(tile, goal);
} finally {
teardownDom(env);
}
}
async test_right_click_on_a_unit_icon_orders_the_selected_stack() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const goal = adjacentLand(state, unit);
const target = state._spawnUnit(goal, 0, state.protoUnits[0]);
const { env, map } = await this.build(state);
try {
let moved = null;
map.onMoveOrdered = (ids, coords) => {
moved = { ids, coords };
};
map.setSelectedUnit(unit.id);
const local = mapToLocal(goal.x, goal.y);
const clientX = 400 + (local.x - map.camera.x) * map.camera.zoom;
const clientY = 300 + (local.y - map.camera.y) * map.camera.zoom * CAMERA_TILT;
const view = map._unitViews.get(target.id);
this.assertNotNull(view, "the target unit has a view");
// Real bubbling events, so the icon's own native handler is exercised:
// it must let right-clicks reach the viewport's mousedown listener.
const press = new env.window.MouseEvent("mousedown", { button: 2, bubbles: true, clientX, clientY });
view.dispatchEvent(press);
const release = new env.window.MouseEvent("mouseup", { button: 2, bubbles: true, clientX, clientY });
env.window.dispatchEvent(release);
this.assertNotNull(moved, "right-clicking an icon gives the order");
this.assertEqual(moved.ids, [unit.id]);
this.assertEqual(moved.coords, goal);
} finally {
teardownDom(env);
}
}
async test_left_click_selects_city_or_clears() {
const state = smallState();
const { env, map, snapshot } = await this.build(state);
try {
let chosen = null;
map.onCitySelected = (id) => {
chosen = id;
};
// A non-capital city tile has no unit on it.
const city = snapshot.cities.find((c) => c.civ === 0 && !c.isCapital);
const coords = { x: city.coords[0], y: city.coords[1] };
map._onLeftClick(coords);
this.assertEqual(chosen, city.id);
this.assertEqual(map.cityAt(coords).id, city.id);
let cleared = -1;
map.onUnitSelected = (id) => {
cleared = id;
};
map.setSelectedUnit(snapshot.units[0].id);
const sea = seaTile(state);
map._onLeftClick(sea);
this.assertEqual(cleared, 0, "clicking empty ground clears the selection");
this.assertEqual(map.unitAt(sea), 0);
this.assertNull(map.cityAt(sea));
} finally {
teardownDom(env);
}
}
async test_city_marker_click_reports_the_city() {
const state = smallState();
const { env, map, snapshot } = await this.build(state);
try {
let chosen = null;
map.onCitySelected = (id) => {
chosen = id;
};
const owned = env.$("#layer-entities .city[data-owned='1']");
this.assertGreater(owned.length, 0);
owned.first().find(".marker").click();
this.assertEqual(chosen, snapshot.cities.find((c) => c.civ === 0).id);
} finally {
teardownDom(env);
}
}
async test_clear_selection_notifies() {
const state = smallState();
const { env, map, snapshot } = await this.build(state);
try {
const unit = snapshot.units.find((u) => u.civ === 0);
map.setSelectedUnit(unit.id);
let selected = -1;
map.onUnitSelected = (id) => {
selected = id;
};
map.clearSelection();
this.assertEqual(selected, 0);
this.assertSize(env.$("#layer-entities .unit.selected"), 0);
} finally {
teardownDom(env);
}
}
async test_selected_tile_is_outlined() {
const state = smallState();
const { env, map } = await this.build(state);
try {
map.setSelectedTile({ x: 0, y: 0 });
const hex = env.$("#layer-highlight .tile-selection");
this.assertSize(hex, 1, "the outline is created");
this.assertTrue(hex.hasClass("active"), "the outline is shown");
const points = (hex.find("polygon").attr("points") || "").split(" ");
this.assertSize(points, 6, "a hexagon outline");
const wrapped = map._wrappedPosition(mapToLocal(0, 0));
this.assertApprox(parseFloat(hex[0].style.left), wrapped.x - HEX_W / 2, 1e-6);
map.clearSelectedTile();
this.assertFalse(hex.hasClass("active"), "the outline is hidden again");
} finally {
teardownDom(env);
}
}
async test_selected_tile_reports_its_region() {
const state = smallState();
const { env, map } = await this.build(state);
try {
this.assertGreater(map.regions.size, 0, "regions are mirrored from the snapshot");
const [tileKey, cityId] = map.regions.entries().next().value;
map.setSelectedTile(parseKey(tileKey));
this.assertEqual(map._selectedRegion, cityId, "the tile's region is selected as a whole");
map.clearSelectedTile();
this.assertNull(map._selectedRegion, "clearing drops the region");
} finally {
teardownDom(env);
}
}
async test_shift_click_toggles_one_unit_instead_of_the_whole_stack() {
const state = smallState();
const { env, map, snapshot } = await this.build(state);
try {
const unit = snapshot.units.find((u) => u.civ === 0);
let selected = -1;
let toggled = -1;
map.onUnitSelected = (id) => { selected = id; };
map.onUnitToggled = (id) => { toggled = id; };
const coords = { x: unit.coords[0], y: unit.coords[1] };
map._onLeftClick(coords, { shiftKey: true });
this.assertEqual(toggled, unit.id, "shift-click asks to toggle that one unit");
this.assertEqual(selected, -1, "and does not grab the whole stack");
map._onLeftClick(coords, {});
this.assertEqual(selected, unit.id, "a plain click still takes the stack");
} finally {
teardownDom(env);
}
}
async test_prediction_crosses_allied_and_at_war_land_but_not_a_neutral() {
const state = smallState(["france", "britain", "poland"]);
const { env, map } = await this.build(state);
try {
const proto = map.protoUnits[0];
const occupied = (c) =>
Array.from(map._unitData.values()).some((u) => u.coords[0] === c.x && u.coords[1] === c.y);
const tile = Object.keys(map.tiles)
.map((k) => parseKey(k))
.find(
(c) =>
map.tiles[`${c.x},${c.y}`].terrainClass === "Land" &&
!map._cityCiv.has(`${c.x},${c.y}`) &&
!occupied(c)
);
this.assertNotNull(tile, "the fixture has an empty land tile");
map.territory.set(`${tile.x},${tile.y}`, 2);
this.assertFalse(map._canEnter(0, proto, tile), "a neutral third party's land is a wall");
state.requestDeclareWar(0, 2);
map.applySnapshot(state.snapshot(0));
this.assertTrue(map._isAtWar(0, 2), "the war is mirrored for prediction");
this.assertTrue(map._canEnter(0, proto, tile), "a nation at war may be crossed");
const allied = smallState(["france", "britain", "poland"]);
const { env: env2, map: map2 } = await this.build(allied);
try {
allied.requestProposeTreaty(0, 2, TREATY_ALLIANCE);
allied.requestRespondTreaty(2, allied.treatyProposals[0].id, true);
map2.applySnapshot(allied.snapshot(0));
map2.territory.set(`${tile.x},${tile.y}`, 2);
this.assertTrue(map2._isAllied(0, 2), "the alliance is mirrored for prediction");
this.assertTrue(map2._canEnter(0, proto, tile), "an ally's land may be crossed");
} finally {
teardownDom(env2);
}
} finally {
teardownDom(env);
}
}
async test_shift_mousedown_on_a_unit_icon_detaches_that_unit() {
const state = smallState();
const { env, map, snapshot } = await this.build(state);
try {
const unit = snapshot.units.find((u) => u.civ === 0);
let toggled = -1;
let selected = -1;
map.onUnitToggled = (id) => { toggled = id; };
map.onUnitSelected = (id) => { selected = id; };
const view = map._unitViews.get(unit.id);
this.assertNotNull(view, "the unit has a view");
view.dispatchEvent(
new env.window.MouseEvent("mousedown", { button: 0, shiftKey: true, bubbles: true })
);
this.assertEqual(toggled, unit.id, "shift-mousedown detaches that one unit");
this.assertEqual(selected, -1, "it does not grab the whole stack");
view.dispatchEvent(new env.window.MouseEvent("mousedown", { button: 0, bubbles: true }));
this.assertEqual(selected, unit.id, "a plain mousedown still takes the stack");
} finally {
teardownDom(env);
}
}
async test_stacked_units_show_one_icon_with_a_count_badge() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
const { env, map } = await this.build(state);
try {
const first = map._unitViews.get(unit.id);
const other = map._unitViews.get(second.id);
this.assertEqual(first.querySelector(".unit-count").style.display, "block");
this.assertEqual(first.querySelector(".unit-count").textContent, "2");
this.assertEqual(other.style.visibility, "hidden", "the follower is not drawn");
const ids = map.unitIdsAt({ x: unit.coords.x, y: unit.coords.y }).sort((a, b) => a - b);
this.assertEqual(ids, [unit.id, second.id].sort((a, b) => a - b), "both are selectable");
} finally {
teardownDom(env);
}
}
async test_right_click_on_a_friendly_stack_opens_the_menu() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
const { env, map } = await this.build(state);
try {
let menu = null;
map.onStackMenu = (coords, ids, x, y) => {
menu = { coords, ids, x, y };
};
map._onRightClick({ x: unit.coords.x, y: unit.coords.y }, { clientX: 11, clientY: 22 });
this.assertNotNull(menu, "the stack menu opens");
this.assertEqual(menu.ids.sort((a, b) => a - b), [unit.id, second.id].sort((a, b) => a - b));
this.assertEqual(menu.x, 11);
this.assertEqual(menu.y, 22);
} finally {
teardownDom(env);
}
}
async test_stack_ids_group_units_sharing_a_tile() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
const { env, map } = await this.build(state);
try {
const ids = map.stackIds(unit.id).sort((a, b) => a - b);
this.assertEqual(ids, [unit.id, second.id].sort((a, b) => a - b));
} finally {
teardownDom(env);
}
}
async test_right_click_on_a_friendly_stack_while_selected_moves_there() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
const { env, map } = await this.build(state);
try {
let menu = null;
let moved = null;
map.onStackMenu = () => { menu = true; };
map.onMoveOrdered = (ids, coords) => { moved = { ids, coords }; };
map.setSelectedUnit(second.id);
map._onRightClick({ x: unit.coords.x, y: unit.coords.y }, {});
this.assertNull(menu, "the stack menu does not open over a selection");
this.assertNotNull(moved, "the selected stack is ordered onto the friendly tile");
this.assertEqual(moved.ids, [second.id]);
this.assertEqual(moved.coords, { x: unit.coords.x, y: unit.coords.y });
} finally {
teardownDom(env);
}
}
async test_right_click_on_an_enemy_stack_is_a_plain_move() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const goal = adjacentLand(state, unit);
state._spawnUnit(goal, 1, state.protoUnits[0]);
const { env, map } = await this.build(state);
try {
let move = null;
map.onMoveOrdered = (ids, coords) => { move = { ids, coords }; };
map.setSelectedUnit(unit.id);
map._onRightClick(goal, {});
this.assertNotNull(move, "a right-click is a move, never a ranged attack");
this.assertEqual(move.coords, goal);
} finally {
teardownDom(env);
}
}
async test_left_click_in_target_mode_chooses_the_target() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const goal = adjacentLand(state, unit);
const { env, map } = await this.build(state);
try {
let chosen = null;
let selected = null;
map.onTargetChosen = (coords) => { chosen = coords; };
map.onTargetCancelled = () => map.clearTargeting();
map.onUnitSelected = () => { selected = true; };
map.beginTargeting({
unitIds: [unit.id],
origin: { x: unit.coords.x, y: unit.coords.y },
range: 3,
});
map._onLeftClick(goal);
this.assertEqual(chosen, goal, "the left-click picks the target tile");
this.assertNull(selected, "the click does not also select a unit");
this.assertNotNull(map._targeting, "target mode stays on until the controller clears it");
map._onRightClick(goal, {});
this.assertNull(map._targeting, "a right-click cancels target mode");
} finally {
teardownDom(env);
}
}
async test_a_unit_icon_does_not_steal_a_target_click() {
const state = smallState();
const attacker = state.units.find((u) => u.civ === 0);
const goal = adjacentLand(state, attacker);
const defender = state._spawnUnit(goal, 1, state.protoUnits[0]);
const { env, map } = await this.build(state);
try {
let selected = -1;
let reachedMap = false;
map.onUnitSelected = (id) => { selected = id; };
map.$viewport.on("mousedown", () => { reachedMap = true; });
map.beginTargeting({ unitIds: [attacker.id], origin: attacker.coords, range: 3 });
const view = map._unitViews.get(defender.id);
this.assertNotNull(view, "the defender has a view");
// Mousedown on the enemy icon while picking a target must fall through to
// the map, not select the enemy (which would clear the attacker).
view.dispatchEvent(new env.window.MouseEvent("mousedown", { button: 0, bubbles: true }));
this.assertEqual(selected, -1, "the target icon does not steal the click");
this.assertTrue(reachedMap, "the click reaches the map so the target is chosen");
} finally {
teardownDom(env);
}
}
async test_ctrl_right_click_schedules_a_destination() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const goal = adjacentLand(state, unit);
const { env, map } = await this.build(state);
try {
let schedule = null;
let move = null;
map.onScheduleOrdered = (ids, coords) => { schedule = { ids, coords }; };
map.onMoveOrdered = (ids, coords) => { move = { ids, coords }; };
map.setSelectedUnit(unit.id);
map._onRightClick(goal, { ctrlKey: true });
this.assertNotNull(schedule, "Ctrl-right-click queues a destination");
this.assertNull(move, "a scheduled click is not a plain move");
this.assertEqual(schedule.coords, goal);
} finally {
teardownDom(env);
}
}
async test_a_moving_unit_splits_from_its_stack_at_once() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
const { env, map } = await this.build(state);
try {
const tile = { x: unit.coords[0], y: unit.coords[1] };
map._unitMotion.get(unit.id).path = [tile, { x: tile.x + 1, y: tile.y }];
map._unitMotion.get(unit.id).segmentHours = [1];
map._unitMotion.get(unit.id).pathHours = 0.1;
map._assignStackOffsets();
this.assertEqual(
map._unitViews.get(unit.id).style.visibility,
"",
"the moving unit is drawn on its own"
);
this.assertEqual(
map._unitViews.get(second.id).style.visibility,
"",
"the unit left behind stays visible"
);
this.assertEqual(
map._unitViews.get(second.id).querySelector(".unit-count").style.display,
"none",
"the stack badge is gone the moment one member leaves"
);
} finally {
teardownDom(env);
}
}
}