mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:40:44 +00:00
af0b8a8d50
## Description: Resolve discussions about stalled PR https://github.com/openfrontio/OpenFrontIO/pull/2460 <img width="724" height="348" alt="image" src="https://github.com/user-attachments/assets/c2c9fa79-cace-431a-9ca4-b3656612fa9d" /> Changes: - Added a `Player::canAttackPlayer(other)` function to determine whether a player can be attacked. - This function is now used in most places where a fight can occur: - AttackExecution (land attacks) - Naval invasion - Warship fight - Nukes can't be thrown during the truce - Immunity only affect human players. Nations and bot will fight as usual, and can be fought against. - The immunity timer uses minutes in the modal window. UI: - The immunity phase is displayed with a timer bar at the top. This is from the original PR, to be discussed if it's not deemed visible enough: <img width="632" height="215" alt="image" src="https://github.com/user-attachments/assets/f5ab9aa0-bd4f-4503-b8d6-b40b121fba65" /> ## 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: IngloriousTom --------- Co-authored-by: newyearnewphil <git@nynp.dev>
263 lines
6.4 KiB
TypeScript
263 lines
6.4 KiB
TypeScript
import { PathFindResultType } from "../pathfinding/AStar";
|
|
import { MiniAStar } from "../pathfinding/MiniAStar";
|
|
import { Game, Player, UnitType } from "./Game";
|
|
import { andFN, GameMap, manhattanDistFN, TileRef } from "./GameMap";
|
|
|
|
export function canBuildTransportShip(
|
|
game: Game,
|
|
player: Player,
|
|
tile: TileRef,
|
|
): TileRef | false {
|
|
if (
|
|
player.unitCount(UnitType.TransportShip) >= game.config().boatMaxNumber()
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const dst = targetTransportTile(game, tile);
|
|
if (dst === null) {
|
|
return false;
|
|
}
|
|
|
|
const other = game.owner(tile);
|
|
if (other === player) {
|
|
return false;
|
|
}
|
|
if (other.isPlayer() && !player.canAttackPlayer(other)) {
|
|
return false;
|
|
}
|
|
|
|
if (game.isOceanShore(dst)) {
|
|
let myPlayerBordersOcean = false;
|
|
for (const bt of player.borderTiles()) {
|
|
if (game.isOceanShore(bt)) {
|
|
myPlayerBordersOcean = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let otherPlayerBordersOcean = false;
|
|
if (!game.hasOwner(tile)) {
|
|
otherPlayerBordersOcean = true;
|
|
} else {
|
|
for (const bt of (other as Player).borderTiles()) {
|
|
if (game.isOceanShore(bt)) {
|
|
otherPlayerBordersOcean = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (myPlayerBordersOcean && otherPlayerBordersOcean) {
|
|
return transportShipSpawn(game, player, dst);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Now we are boating in a lake, so do a bfs from target until we find
|
|
// a border tile owned by the player
|
|
|
|
const tiles = game.bfs(
|
|
dst,
|
|
andFN(
|
|
manhattanDistFN(dst, 300),
|
|
(_, t: TileRef) => game.isLake(t) || game.isShore(t),
|
|
),
|
|
);
|
|
|
|
const sorted = Array.from(tiles).sort(
|
|
(a, b) => game.manhattanDist(dst, a) - game.manhattanDist(dst, b),
|
|
);
|
|
|
|
for (const t of sorted) {
|
|
if (game.owner(t) === player) {
|
|
return transportShipSpawn(game, player, t);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function transportShipSpawn(
|
|
game: Game,
|
|
player: Player,
|
|
targetTile: TileRef,
|
|
): TileRef | false {
|
|
if (!game.isShore(targetTile)) {
|
|
return false;
|
|
}
|
|
const spawn = closestShoreFromPlayer(game, player, targetTile);
|
|
if (spawn === null) {
|
|
return false;
|
|
}
|
|
return spawn;
|
|
}
|
|
|
|
export function sourceDstOceanShore(
|
|
gm: Game,
|
|
src: Player,
|
|
tile: TileRef,
|
|
): [TileRef | null, TileRef | null] {
|
|
const dst = gm.owner(tile);
|
|
const srcTile = closestShoreFromPlayer(gm, src, tile);
|
|
let dstTile: TileRef | null = null;
|
|
if (dst.isPlayer()) {
|
|
dstTile = closestShoreFromPlayer(gm, dst as Player, tile);
|
|
} else {
|
|
dstTile = closestShoreTN(gm, tile, 50);
|
|
}
|
|
return [srcTile, dstTile];
|
|
}
|
|
|
|
export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null {
|
|
const dst = gm.playerBySmallID(gm.ownerID(tile));
|
|
let dstTile: TileRef | null = null;
|
|
if (dst.isPlayer()) {
|
|
dstTile = closestShoreFromPlayer(gm, dst as Player, tile);
|
|
} else {
|
|
dstTile = closestShoreTN(gm, tile, 50);
|
|
}
|
|
return dstTile;
|
|
}
|
|
|
|
export function closestShoreFromPlayer(
|
|
gm: GameMap,
|
|
player: Player,
|
|
target: TileRef,
|
|
): TileRef | null {
|
|
const shoreTiles = Array.from(player.borderTiles()).filter((t) =>
|
|
gm.isShore(t),
|
|
);
|
|
if (shoreTiles.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return shoreTiles.reduce((closest, current) => {
|
|
const closestDistance = gm.manhattanDist(target, closest);
|
|
const currentDistance = gm.manhattanDist(target, current);
|
|
return currentDistance < closestDistance ? current : closest;
|
|
});
|
|
}
|
|
|
|
export function bestShoreDeploymentSource(
|
|
gm: Game,
|
|
player: Player,
|
|
target: TileRef,
|
|
): TileRef | false {
|
|
const t = targetTransportTile(gm, target);
|
|
if (t === null) return false;
|
|
|
|
const candidates = candidateShoreTiles(gm, player, t);
|
|
if (candidates.length === 0) return false;
|
|
|
|
const aStar = new MiniAStar(gm, gm.miniMap(), candidates, t, 1_000_000, 1);
|
|
const result = aStar.compute();
|
|
if (result !== PathFindResultType.Completed) {
|
|
console.warn(`bestShoreDeploymentSource: path not found: ${result}`);
|
|
return false;
|
|
}
|
|
const path = aStar.reconstructPath();
|
|
if (path.length === 0) {
|
|
return false;
|
|
}
|
|
const potential = path[0];
|
|
// Since mini a* downscales the map, we need to check the neighbors
|
|
// of the potential tile to find a valid deployment point
|
|
const neighbors = gm
|
|
.neighbors(potential)
|
|
.filter((n) => gm.isShore(n) && gm.owner(n) === player);
|
|
if (neighbors.length === 0) {
|
|
return false;
|
|
}
|
|
return neighbors[0];
|
|
}
|
|
|
|
export function candidateShoreTiles(
|
|
gm: Game,
|
|
player: Player,
|
|
target: TileRef,
|
|
): TileRef[] {
|
|
let closestManhattanDistance = Infinity;
|
|
let minX = Infinity,
|
|
minY = Infinity,
|
|
maxX = -Infinity,
|
|
maxY = -Infinity;
|
|
|
|
let bestByManhattan: TileRef | null = null;
|
|
const extremumTiles: Record<string, TileRef | null> = {
|
|
minX: null,
|
|
minY: null,
|
|
maxX: null,
|
|
maxY: null,
|
|
};
|
|
|
|
const borderShoreTiles = Array.from(player.borderTiles()).filter((t) =>
|
|
gm.isShore(t),
|
|
);
|
|
|
|
for (const tile of borderShoreTiles) {
|
|
const distance = gm.manhattanDist(tile, target);
|
|
const cell = gm.cell(tile);
|
|
|
|
// Manhattan-closest tile
|
|
if (distance < closestManhattanDistance) {
|
|
closestManhattanDistance = distance;
|
|
bestByManhattan = tile;
|
|
}
|
|
|
|
// Extremum tiles
|
|
if (cell.x < minX) {
|
|
minX = cell.x;
|
|
extremumTiles.minX = tile;
|
|
} else if (cell.y < minY) {
|
|
minY = cell.y;
|
|
extremumTiles.minY = tile;
|
|
} else if (cell.x > maxX) {
|
|
maxX = cell.x;
|
|
extremumTiles.maxX = tile;
|
|
} else if (cell.y > maxY) {
|
|
maxY = cell.y;
|
|
extremumTiles.maxY = tile;
|
|
}
|
|
}
|
|
|
|
// Calculate sampling interval to ensure we get at most 50 tiles
|
|
const samplingInterval = Math.max(
|
|
10,
|
|
Math.ceil(borderShoreTiles.length / 50),
|
|
);
|
|
const sampledTiles = borderShoreTiles.filter(
|
|
(_, index) => index % samplingInterval === 0,
|
|
);
|
|
|
|
const candidates = [
|
|
bestByManhattan,
|
|
extremumTiles.minX,
|
|
extremumTiles.minY,
|
|
extremumTiles.maxX,
|
|
extremumTiles.maxY,
|
|
...sampledTiles,
|
|
].filter(Boolean) as number[];
|
|
|
|
return candidates;
|
|
}
|
|
|
|
function closestShoreTN(
|
|
gm: GameMap,
|
|
tile: TileRef,
|
|
searchDist: number,
|
|
): TileRef | null {
|
|
const tn = Array.from(
|
|
gm.bfs(
|
|
tile,
|
|
andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)),
|
|
),
|
|
)
|
|
.filter((t) => gm.isShore(t))
|
|
.sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b));
|
|
if (tn.length === 0) {
|
|
return null;
|
|
}
|
|
return tn[0];
|
|
}
|