Better map generation
This commit is contained in:
+219
-6
@@ -57,7 +57,7 @@ export class GameScreenTest extends TestCase {
|
||||
|
||||
const unit = state.units.find((u) => u.civ === 0);
|
||||
const goal = adjacentLand(state, unit);
|
||||
screen.map.onMoveOrdered(unit.id, goal);
|
||||
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");
|
||||
|
||||
@@ -131,7 +131,7 @@ export class GameScreenTest extends TestCase {
|
||||
this.assertFalse(env.$("#train-status").hasClass("hidden"));
|
||||
this.assertTrue(env.$("#train-label").text().includes("Training"));
|
||||
|
||||
state.training.get(capital.id).elapsedHours = 3;
|
||||
state.training.get(capital.id)[0].elapsedHours = 3;
|
||||
screen.onState(state.snapshot(0));
|
||||
this.assertTrue(env.$("#train-label").text().includes("left"));
|
||||
|
||||
@@ -144,6 +144,68 @@ export class GameScreenTest extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -160,6 +222,11 @@ export class GameScreenTest extends TestCase {
|
||||
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);
|
||||
@@ -314,7 +381,7 @@ export class GameScreenTest extends TestCase {
|
||||
screen.map.predictMove = (u, coords) => {
|
||||
predicted = { u, coords };
|
||||
};
|
||||
screen.map.onMoveOrdered(unit.id, goal);
|
||||
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"));
|
||||
@@ -340,7 +407,7 @@ export class GameScreenTest extends TestCase {
|
||||
const goal = { x: 902, y: 902 };
|
||||
screen.map.territory.set("902,902", 1);
|
||||
screen.map.planMove = () => [{ x: 1, y: 1 }, goal];
|
||||
screen.map.onMoveOrdered(unit.id, goal);
|
||||
await screen.map.onMoveOrdered([unit.id], goal);
|
||||
env.$("#confirm-cancel").click();
|
||||
this.assertEmpty(network.orders, "cancelling keeps the peace");
|
||||
screen.leave();
|
||||
@@ -363,7 +430,7 @@ export class GameScreenTest extends TestCase {
|
||||
screen.map.territory.set("901,901", 1);
|
||||
screen.map.planMove = () => [{ x: 1, y: 1 }, goal];
|
||||
screen.map.predictMove = () => null;
|
||||
screen.map.onMoveOrdered(unit.id, goal);
|
||||
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();
|
||||
@@ -381,7 +448,7 @@ export class GameScreenTest extends TestCase {
|
||||
screen.onState(defaultState().snapshot(0));
|
||||
env.$("#civ-flag").click();
|
||||
env.$("#modal-nation .tab[data-tab=diplomacy]").click();
|
||||
const row = env.$("#diplomacy-list .row-card").first();
|
||||
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");
|
||||
@@ -448,4 +515,150 @@ export class GameScreenTest extends TestCase {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user