mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 04:23:28 +00:00
3dadfbd23d
## Description: Nuke trajectory prediction now will show interception with allied SAMs if the alliance will break on nuke launch. Code was also refactored to be shared a bit more. In addition, if an incoming alliance would break if accepted, the nuke launch will break the alliance. <img width="1199" height="1002" alt="nukepr" src="https://github.com/user-attachments/assets/c31066d9-66cf-4eaa-be3c-e2fbcfe7965a" /> ## 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: bibizu
259 lines
7.0 KiB
TypeScript
259 lines
7.0 KiB
TypeScript
import { NukeMagnitude } from "../configuration/Config";
|
|
import { Game, Player, StructureTypes } from "../game/Game";
|
|
import { euclDistFN, GameMap, TileRef } from "../game/GameMap";
|
|
import { GameView } from "../game/GameView";
|
|
|
|
export interface NukeBlastParams {
|
|
gm: GameMap;
|
|
targetTile: TileRef;
|
|
magnitude: NukeMagnitude;
|
|
}
|
|
|
|
/**
|
|
* Counts how many tiles each player has in the nuke's blast zone.
|
|
*
|
|
* returns Map of player ID and weighted tile count
|
|
*/
|
|
export function computeNukeBlastCounts(
|
|
params: NukeBlastParams,
|
|
): Map<number, number> {
|
|
const { gm, targetTile, magnitude } = params;
|
|
|
|
const inner2 = magnitude.inner * magnitude.inner;
|
|
const counts = new Map<number, number>();
|
|
|
|
gm.circleSearch(targetTile, magnitude.outer, (tile: TileRef, d2: number) => {
|
|
const ownerSmallId = gm.ownerID(tile);
|
|
if (ownerSmallId > 0) {
|
|
const weight = d2 <= inner2 ? 1 : 0.5;
|
|
const prev = counts.get(ownerSmallId) ?? 0;
|
|
counts.set(ownerSmallId, prev + weight);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
return counts;
|
|
}
|
|
|
|
export interface NukeAllianceCheckParams {
|
|
game: Game | GameView;
|
|
targetTile: TileRef;
|
|
magnitude: NukeMagnitude;
|
|
allySmallIds: Set<number>;
|
|
threshold: number;
|
|
}
|
|
|
|
// Checks if nuking this tile would break an alliance.
|
|
// Returns true if either:
|
|
// 1. The weighted tile count for any ally exceeds the threshold
|
|
// 2. Any allied structure would be destroyed
|
|
export function wouldNukeBreakAlliance(
|
|
params: NukeAllianceCheckParams,
|
|
): boolean {
|
|
const { game, targetTile, magnitude, allySmallIds, threshold } = params;
|
|
|
|
if (allySmallIds.size === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check if any allied structure would be destroyed
|
|
const wouldDestroyAlliedStructure = game.anyUnitNearby(
|
|
targetTile,
|
|
magnitude.outer,
|
|
StructureTypes,
|
|
(unit) =>
|
|
unit.owner().isPlayer() && allySmallIds.has(unit.owner().smallID()),
|
|
);
|
|
if (wouldDestroyAlliedStructure) return true;
|
|
|
|
const inner2 = magnitude.inner * magnitude.inner;
|
|
const allyTileCounts = new Map<number, number>();
|
|
|
|
let result = false;
|
|
|
|
game.circleSearch(
|
|
targetTile,
|
|
magnitude.outer,
|
|
(tile: TileRef, d2: number) => {
|
|
const ownerSmallId = game.ownerID(tile);
|
|
if (ownerSmallId > 0 && allySmallIds.has(ownerSmallId)) {
|
|
const weight = d2 <= inner2 ? 1 : 0.5;
|
|
const newCount = (allyTileCounts.get(ownerSmallId) ?? 0) + weight;
|
|
allyTileCounts.set(ownerSmallId, newCount);
|
|
|
|
if (newCount > threshold) {
|
|
result = true;
|
|
return false; // Found one! Stop searching.
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
// Same as wouldNukeBreakAlliance(), but takes time to find every player
|
|
// that would be "angered" from this nuke.
|
|
// This includes unallied players!
|
|
export function listNukeBreakAlliance(
|
|
params: NukeAllianceCheckParams,
|
|
): Set<number> {
|
|
const { game, targetTile, magnitude, threshold } = params;
|
|
|
|
// Collect all players that should have alliance broken:
|
|
// either exceeds tile threshold OR has a structure in blast radius
|
|
const playersToBreakAllianceWith = new Set<number>();
|
|
|
|
// compute tile breakage threshold
|
|
const blastCounts = computeNukeBlastCounts({
|
|
gm: game,
|
|
targetTile,
|
|
magnitude,
|
|
});
|
|
for (const [playerSmallId, totalWeight] of blastCounts) {
|
|
if (totalWeight > threshold) {
|
|
playersToBreakAllianceWith.add(playerSmallId);
|
|
}
|
|
}
|
|
|
|
// Also check if any allied structures would be destroyed
|
|
game
|
|
.nearbyUnits(targetTile, magnitude.outer, [...StructureTypes])
|
|
.forEach(({ unit }) =>
|
|
playersToBreakAllianceWith.add(unit.owner().smallID()),
|
|
);
|
|
|
|
return playersToBreakAllianceWith;
|
|
}
|
|
|
|
export function getSpawnTiles(gm: GameMap, tile: TileRef): TileRef[] {
|
|
return Array.from(gm.bfs(tile, euclDistFN(tile, 4, true))).filter(
|
|
(t) => !gm.hasOwner(t) && gm.isLand(t),
|
|
);
|
|
}
|
|
|
|
export function closestTile(
|
|
gm: GameMap,
|
|
refs: Iterable<TileRef>,
|
|
tile: TileRef,
|
|
): [TileRef | null, number] {
|
|
let minDistance = Infinity;
|
|
let minRef: TileRef | null = null;
|
|
for (const ref of refs) {
|
|
const distance = gm.manhattanDist(ref, tile);
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
minRef = ref;
|
|
}
|
|
}
|
|
return [minRef, minDistance];
|
|
}
|
|
|
|
export function closestTwoTiles(
|
|
gm: GameMap,
|
|
x: Iterable<TileRef>,
|
|
y: Iterable<TileRef>,
|
|
): { x: TileRef; y: TileRef } | null {
|
|
const xSorted = Array.from(x).sort((a, b) => gm.x(a) - gm.x(b));
|
|
const ySorted = Array.from(y).sort((a, b) => gm.x(a) - gm.x(b));
|
|
|
|
if (xSorted.length === 0 || ySorted.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
let i = 0;
|
|
let j = 0;
|
|
let minDistance = Infinity;
|
|
let result = { x: xSorted[0], y: ySorted[0] };
|
|
|
|
while (i < xSorted.length && j < ySorted.length) {
|
|
const currentX = xSorted[i];
|
|
const currentY = ySorted[j];
|
|
|
|
const distance =
|
|
Math.abs(gm.x(currentX) - gm.x(currentY)) +
|
|
Math.abs(gm.y(currentX) - gm.y(currentY));
|
|
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
result = { x: currentX, y: currentY };
|
|
}
|
|
|
|
// If we're at the end of X, must move Y forward
|
|
if (i === xSorted.length - 1) {
|
|
j++;
|
|
}
|
|
// If we're at the end of Y, must move X forward
|
|
else if (j === ySorted.length - 1) {
|
|
i++;
|
|
}
|
|
// Otherwise, move whichever pointer has smaller x value
|
|
else if (gm.x(currentX) < gm.x(currentY)) {
|
|
i++;
|
|
} else {
|
|
j++;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculates the center of a player's territory using geometric approach.
|
|
* Uses the bounding box center and verifies ownership, falling back to nearest border tile if necessary.
|
|
*
|
|
* @param game - The game instance
|
|
* @param target - The player whose territory center to calculate
|
|
* @returns The tile reference for the territory center, or null if no valid center found
|
|
*/
|
|
export function calculateTerritoryCenter(
|
|
game: Game,
|
|
target: Player,
|
|
): TileRef | null {
|
|
const borderTiles = target.borderTiles();
|
|
if (borderTiles.size === 0) return null;
|
|
|
|
// Calculate bounding box center in a single pass through border tiles
|
|
let minX = Infinity,
|
|
maxX = -Infinity;
|
|
let minY = Infinity,
|
|
maxY = -Infinity;
|
|
|
|
for (const tile of borderTiles) {
|
|
const x = game.x(tile);
|
|
const y = game.y(tile);
|
|
if (x < minX) minX = x;
|
|
if (x > maxX) maxX = x;
|
|
if (y < minY) minY = y;
|
|
if (y > maxY) maxY = y;
|
|
}
|
|
|
|
const centerX = Math.floor((minX + maxX) / 2);
|
|
const centerY = Math.floor((minY + maxY) / 2);
|
|
|
|
const centerTile = game.ref(centerX, centerY);
|
|
|
|
// Verify ownership of the center tile
|
|
if (game.owner(centerTile) === target) {
|
|
return centerTile;
|
|
}
|
|
|
|
// Fall back to nearest border tile if center is not owned
|
|
let closestTile: TileRef | null = null;
|
|
let closestDistanceSquared = Infinity;
|
|
|
|
for (const tile of borderTiles) {
|
|
const dx = game.x(tile) - centerX;
|
|
const dy = game.y(tile) - centerY;
|
|
const distSquared = dx * dx + dy * dy;
|
|
|
|
if (distSquared < closestDistanceSquared) {
|
|
closestDistanceSquared = distSquared;
|
|
closestTile = tile;
|
|
}
|
|
}
|
|
|
|
return closestTile;
|
|
}
|