mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 10:21:27 +00:00
17c1a6300f
## Description: - Widened port placement and warship spawn/patrol checks from `isOcean`/`isOceanShore` to `isWater`/`isShore`, so ports can be built on lake shores and ships can operate on lakes, we discussed it here: <img width="996" height="423" alt="image" src="https://github.com/user-attachments/assets/acf1e970-9631-4848-a0ed-6d0470616e1d" /> - Filtered `tradingPorts()` by water component so ports only attempt trades with reachable ports - prevents silent path-not-found failures across disconnected water bodies - Applied the same water component filter when a captured trade ship reroutes to its new owner's nearest port - Removed the `WaterManager` fallback that force-marked isolated water-nuked-tiles as ocean (no longer needed since lakes are now navigable) - Added a check to prevent nations from building ports on water bodies that aren't accessible to other players ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin --------- Co-authored-by: Evan <evanpelle@gmail.com>
211 lines
7.3 KiB
TypeScript
211 lines
7.3 KiB
TypeScript
import { NukeExecution } from "../../src/core/execution/NukeExecution";
|
|
import { SpawnExecution } from "../../src/core/execution/SpawnExecution";
|
|
import {
|
|
Game,
|
|
Player,
|
|
PlayerInfo,
|
|
PlayerType,
|
|
UnitType,
|
|
} from "../../src/core/game/Game";
|
|
import { TileRef } from "../../src/core/game/GameMap";
|
|
import { GameID } from "../../src/core/Schemas";
|
|
import { setup } from "../util/Setup";
|
|
import { constructionExecution } from "../util/utils";
|
|
|
|
const gameID: GameID = "game_id";
|
|
|
|
function launchNukeAt(game: Game, player: Player, target: TileRef): void {
|
|
game.addExecution(new NukeExecution(UnitType.AtomBomb, player, target, null));
|
|
// init + build
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
}
|
|
|
|
function tickUntilNukeLands(game: Game, maxTicks = 50): void {
|
|
for (let i = 0; i < maxTicks; i++) {
|
|
game.executeNextTick();
|
|
}
|
|
}
|
|
|
|
describe("Water Nukes", () => {
|
|
let game: Game;
|
|
let player: Player;
|
|
|
|
describe("when waterNukes is enabled", () => {
|
|
beforeEach(async () => {
|
|
game = await setup("plains", {
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
waterNukes: true,
|
|
});
|
|
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
|
game.addPlayer(info);
|
|
game.addExecution(new SpawnExecution(gameID, info, game.ref(1, 1)));
|
|
while (game.inSpawnPhase()) game.executeNextTick();
|
|
player = game.player(info.id);
|
|
|
|
// Build a missile silo
|
|
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
|
|
});
|
|
|
|
test("nuke converts land tiles to water instead of fallout", () => {
|
|
const target = game.ref(10, 10);
|
|
// Confirm target is land before nuke
|
|
expect(game.isLand(target)).toBe(true);
|
|
|
|
launchNukeAt(game, player, target);
|
|
tickUntilNukeLands(game);
|
|
|
|
// Target should now be water, not land
|
|
expect(game.isLand(target)).toBe(false);
|
|
expect(game.isWater(target)).toBe(true);
|
|
// Should NOT have fallout
|
|
expect(game.hasFallout(target)).toBe(false);
|
|
});
|
|
|
|
test("converted tiles get shoreline bits updated", () => {
|
|
const target = game.ref(10, 10);
|
|
launchNukeAt(game, player, target);
|
|
tickUntilNukeLands(game);
|
|
|
|
// With nukeMagnitudes { inner: 1, outer: 1 }, the target and its
|
|
// cardinal neighbors (dist² <= 1) are all converted to water.
|
|
// Shoreline tiles are the land tiles just outside the blast radius.
|
|
const x = game.x(target);
|
|
const y = game.y(target);
|
|
|
|
// 2 tiles away should still be land and now be shoreline
|
|
const outerNeighbors: TileRef[] = [];
|
|
if (game.isValidCoord(x - 2, y)) outerNeighbors.push(game.ref(x - 2, y));
|
|
if (game.isValidCoord(x + 2, y)) outerNeighbors.push(game.ref(x + 2, y));
|
|
if (game.isValidCoord(x, y - 2)) outerNeighbors.push(game.ref(x, y - 2));
|
|
if (game.isValidCoord(x, y + 2)) outerNeighbors.push(game.ref(x, y + 2));
|
|
|
|
for (const n of outerNeighbors) {
|
|
expect(game.isLand(n)).toBe(true);
|
|
expect(game.isShoreline(n)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("queueWaterConversion skips tiles conquered before flush", () => {
|
|
// Pick an unowned land tile and queue it for water conversion directly
|
|
const target = game.ref(10, 10);
|
|
expect(game.isLand(target)).toBe(true);
|
|
expect(game.hasOwner(target)).toBe(false);
|
|
|
|
// Queue the tile for water conversion (simulates nuke queueing)
|
|
game.queueWaterConversion(target);
|
|
|
|
// Another actor conquers the tile before the tick flushes the queue
|
|
player.conquer(target);
|
|
expect(game.hasOwner(target)).toBe(true);
|
|
|
|
// Flush: the pending conversion should be skipped because the tile is now owned
|
|
game.executeNextTick();
|
|
|
|
// Tile should remain land and owned
|
|
expect(game.isLand(target)).toBe(true);
|
|
expect(game.hasOwner(target)).toBe(true);
|
|
expect(game.isWater(target)).toBe(false);
|
|
});
|
|
|
|
test("waterGraphVersion increments after water conversion", async () => {
|
|
// Need a game with nav mesh enabled for graph rebuilds
|
|
const navGame = await setup("plains", {
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
waterNukes: true,
|
|
disableNavMesh: false,
|
|
});
|
|
const info2 = new PlayerInfo("p2", PlayerType.Human, null, "p2");
|
|
navGame.addPlayer(info2);
|
|
navGame.addExecution(
|
|
new SpawnExecution(gameID, info2, navGame.ref(1, 1)),
|
|
);
|
|
while (navGame.inSpawnPhase()) navGame.executeNextTick();
|
|
const player2 = navGame.player(info2.id);
|
|
constructionExecution(navGame, player2, 1, 1, UnitType.MissileSilo);
|
|
|
|
const versionBefore = navGame.waterGraphVersion();
|
|
|
|
// Launch multiple nukes in a cluster to ensure enough tiles convert
|
|
// for at least one minimap tile to flip (need >= 3 of 4 source tiles)
|
|
const target = navGame.ref(50, 50);
|
|
navGame.addExecution(
|
|
new NukeExecution(UnitType.AtomBomb, player2, target, null),
|
|
);
|
|
// Tick enough for nuke to land + graph rebuild throttle (20 ticks)
|
|
for (let i = 0; i < 80; i++) navGame.executeNextTick();
|
|
|
|
expect(navGame.waterGraphVersion()).toBeGreaterThan(versionBefore);
|
|
});
|
|
});
|
|
|
|
describe("when waterNukes is disabled (default)", () => {
|
|
beforeEach(async () => {
|
|
game = await setup("plains", {
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
waterNukes: false,
|
|
});
|
|
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
|
game.addPlayer(info);
|
|
game.addExecution(new SpawnExecution(gameID, info, game.ref(1, 1)));
|
|
while (game.inSpawnPhase()) game.executeNextTick();
|
|
player = game.player(info.id);
|
|
|
|
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
|
|
});
|
|
|
|
test("nuke applies fallout instead of converting to water", () => {
|
|
const target = game.ref(10, 10);
|
|
expect(game.isLand(target)).toBe(true);
|
|
|
|
launchNukeAt(game, player, target);
|
|
tickUntilNukeLands(game);
|
|
|
|
// Should remain land with fallout
|
|
expect(game.isLand(target)).toBe(true);
|
|
expect(game.hasFallout(target)).toBe(true);
|
|
});
|
|
|
|
test("waterGraphVersion does not change", () => {
|
|
const versionBefore = game.waterGraphVersion();
|
|
const target = game.ref(10, 10);
|
|
|
|
launchNukeAt(game, player, target);
|
|
tickUntilNukeLands(game);
|
|
|
|
expect(game.waterGraphVersion()).toBe(versionBefore);
|
|
});
|
|
});
|
|
|
|
describe("updateTile terrain byte round-trip", () => {
|
|
test("terrain byte is packed and unpacked correctly", async () => {
|
|
game = await setup("plains", {
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
waterNukes: true,
|
|
});
|
|
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
|
game.addPlayer(info);
|
|
game.addExecution(new SpawnExecution(gameID, info, game.ref(1, 1)));
|
|
while (game.inSpawnPhase()) game.executeNextTick();
|
|
player = game.player(info.id);
|
|
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
|
|
|
|
const target = game.ref(10, 10);
|
|
const terrainBefore = game.terrainByte(target);
|
|
expect(game.isLand(target)).toBe(true);
|
|
|
|
launchNukeAt(game, player, target);
|
|
tickUntilNukeLands(game);
|
|
|
|
const terrainAfter = game.terrainByte(target);
|
|
// Terrain should have changed (was land, now water)
|
|
expect(terrainAfter).not.toBe(terrainBefore);
|
|
expect(game.isWater(target)).toBe(true);
|
|
});
|
|
});
|
|
});
|