332 lines
12 KiB
JavaScript
332 lines
12 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";
|
|
|
|
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_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_attacks_it() {
|
|
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 attack = null;
|
|
let move = null;
|
|
map.onAttackOrdered = (ids, coords) => { attack = { ids, coords }; };
|
|
map.onMoveOrdered = (ids, coords) => { move = { ids, coords }; };
|
|
map.setSelectedUnit(unit.id);
|
|
map._onRightClick(goal, {});
|
|
this.assertNotNull(attack, "the enemy stack is attacked");
|
|
this.assertNull(move, "the enemy stack is not treated as a plain move");
|
|
this.assertEqual(attack.coords, goal);
|
|
} 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);
|
|
}
|
|
}
|
|
}
|