mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:50:45 +00:00
Much better river handling for nations and tribes! 🏞️ (#3786)
## Description: In this example, the two nations DONT see each other as neighbors, but as ISLANDERS. Because they dont have a direct border connection, there is water in between. <img width="526" height="329" alt="image" src="https://github.com/user-attachments/assets/cf2c15b5-7793-4445-afd2-920d6cd50a2a" /> This is a big problem, because most of the logic in AiAttackBehavior gets ignored. Only the "islander" strategy runs (late, because its a not-important strat). ### Summary - `PlayerImpl.neighbors()` now includes cross-water neighbors: a new `shoreReachableNeighbors()` helper samples every 10th shore border tile and looks up to 5 tiles in each cardinal direction across water, finding land owners on the other side (covers rivers up to 4 tiles wide). - `AiAttackBehavior.maybeAttack()` extends the `hasNonNukedTerraNullius` check to also trigger on TN detected via `player.neighbors()`, so nations notice and pursue TN that is only reachable across a river. - `sendAttack()` uses a new `hasLandBorderWithTerraNullius()` land-only adjacency check to decide between a land attack and a boat attack for TN, rather than `sharesBorderWith()` which includes water tiles. - Added `sendBoatAttackToNearbyTerraNullius()`: when no TN land is directly adjacent, the AI scans its shore border tiles for unowned land across water and dispatches a transport ship. ### Also works for Tribes! Tribes can boat rivers now, really cool. https://github.com/user-attachments/assets/382e85aa-c437-4e0c-afc2-0c381432da3d ## 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
This commit is contained in:
@@ -108,17 +108,16 @@ export class TribeExecution implements Execution {
|
||||
this.tribe.breakAlliance(alliance);
|
||||
}
|
||||
|
||||
this.attackBehavior.sendAttack(toAttack);
|
||||
return;
|
||||
if (this.attackBehavior.sendAttack(toAttack)) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.neighborsTerraNullius) {
|
||||
if (this.tribe.neighbors().some((n) => !n.isPlayer())) {
|
||||
this.attackBehavior.sendAttack(this.mg.terraNullius());
|
||||
return;
|
||||
if (this.tribe.nearby().some((n) => !n.isPlayer())) {
|
||||
if (this.attackBehavior.sendAttack(this.mg.terraNullius())) return;
|
||||
} else {
|
||||
this.neighborsTerraNullius = false;
|
||||
}
|
||||
this.neighborsTerraNullius = false;
|
||||
}
|
||||
|
||||
this.attackBehavior.attackRandomTarget();
|
||||
|
||||
@@ -38,7 +38,7 @@ export class BreakAllianceExecution implements Execution {
|
||||
this.recipient.updateRelation(this.requestor, -100);
|
||||
|
||||
const neighbors = this.requestor
|
||||
.neighbors()
|
||||
.nearby()
|
||||
.filter(
|
||||
(n): n is Player => n.isPlayer() && !n.isOnSameTeam(this.recipient!),
|
||||
);
|
||||
|
||||
@@ -282,7 +282,7 @@ export class NationAllianceBehavior {
|
||||
case Difficulty.Impossible: {
|
||||
// On hard and impossible we try to not ally with all our neighbors (If we have 2+ neighbors)
|
||||
const borderingPlayers = this.player
|
||||
.neighbors()
|
||||
.nearby()
|
||||
.filter(
|
||||
(n): n is Player => n.isPlayer() && n.type() !== PlayerType.Bot,
|
||||
);
|
||||
|
||||
@@ -210,7 +210,7 @@ export class NationEmojiBehavior {
|
||||
if (!this.random.chance(250)) return;
|
||||
|
||||
const nearbyHumans = this.player
|
||||
.neighbors()
|
||||
.nearby()
|
||||
.filter(
|
||||
(p): p is Player => p.isPlayer() && p.type() === PlayerType.Human,
|
||||
);
|
||||
|
||||
@@ -878,7 +878,7 @@ export class NationStructureBehavior {
|
||||
}
|
||||
|
||||
// Neighbor structures — all non-embargoed non-bot neighbors.
|
||||
for (const neighbor of player.neighbors()) {
|
||||
for (const neighbor of player.nearby()) {
|
||||
if (!neighbor.isPlayer()) continue;
|
||||
if (neighbor.type() === PlayerType.Bot) continue;
|
||||
if (!player.canTrade(neighbor)) continue;
|
||||
@@ -1023,7 +1023,7 @@ export class NationStructureBehavior {
|
||||
// Check if we have any non-friendly non-bot neighbors with more troops
|
||||
const hasHostileNeighbor =
|
||||
player
|
||||
.neighbors()
|
||||
.nearby()
|
||||
.filter(
|
||||
(n): n is Player =>
|
||||
n.isPlayer() &&
|
||||
|
||||
@@ -59,13 +59,18 @@ export class AiAttackBehavior {
|
||||
this.game.isLand(t) &&
|
||||
this.game.ownerID(t) !== this.player?.smallID(),
|
||||
);
|
||||
const borderingPlayers = [
|
||||
...new Set(
|
||||
border
|
||||
.map((t) => this.game.playerBySmallID(this.game.ownerID(t)))
|
||||
.filter((o): o is Player => o.isPlayer()),
|
||||
),
|
||||
].sort((a, b) => a.troops() - b.troops());
|
||||
const playerNeighbors = this.player.nearby();
|
||||
const borderingPlayerSet = new Set<Player>(
|
||||
border
|
||||
.map((t) => this.game.playerBySmallID(this.game.ownerID(t)))
|
||||
.filter((o): o is Player => o.isPlayer()),
|
||||
);
|
||||
for (const n of playerNeighbors) {
|
||||
if (n.isPlayer()) borderingPlayerSet.add(n);
|
||||
}
|
||||
const borderingPlayers = [...borderingPlayerSet].sort(
|
||||
(a, b) => a.troops() - b.troops(),
|
||||
);
|
||||
const borderingFriends = borderingPlayers.filter(
|
||||
(o) => this.player?.isFriendly(o) === true,
|
||||
);
|
||||
@@ -73,13 +78,12 @@ export class AiAttackBehavior {
|
||||
(o) => this.player?.isFriendly(o) === false,
|
||||
);
|
||||
|
||||
// Attack TerraNullius but not nuked territory
|
||||
const hasNonNukedTerraNullius = border.some(
|
||||
(t) => !this.game.hasOwner(t) && !this.game.hasFallout(t),
|
||||
);
|
||||
// Attack TerraNullius but not nuked territory (direct border or across a river)
|
||||
const hasNonNukedTerraNullius =
|
||||
border.some((t) => !this.game.hasOwner(t) && !this.game.hasFallout(t)) ||
|
||||
playerNeighbors.some((n) => !n.isPlayer());
|
||||
if (hasNonNukedTerraNullius) {
|
||||
this.sendAttack(this.game.terraNullius());
|
||||
return;
|
||||
if (this.sendAttack(this.game.terraNullius())) return;
|
||||
}
|
||||
|
||||
if (borderingEnemies.length === 0) {
|
||||
@@ -243,8 +247,7 @@ export class AiAttackBehavior {
|
||||
const retaliate = (): boolean => {
|
||||
const attacker = this.findIncomingAttackPlayer();
|
||||
if (attacker) {
|
||||
this.sendAttack(attacker, true);
|
||||
return true;
|
||||
return this.sendAttack(attacker, true);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -256,8 +259,7 @@ export class AiAttackBehavior {
|
||||
const traitor = (): boolean => {
|
||||
const traitor = this.findTraitor(borderingEnemies);
|
||||
if (traitor) {
|
||||
this.sendAttack(traitor);
|
||||
return true;
|
||||
return this.sendAttack(traitor);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -270,8 +272,7 @@ export class AiAttackBehavior {
|
||||
(!this.isFFA() || enemy.troops() < this.player.troops() * 3),
|
||||
);
|
||||
if (afk) {
|
||||
this.sendAttack(afk);
|
||||
return true;
|
||||
return this.sendAttack(afk);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -281,8 +282,7 @@ export class AiAttackBehavior {
|
||||
|
||||
const nuked = (): boolean => {
|
||||
if (this.isBorderingNukedTerritory()) {
|
||||
this.sendAttack(this.game.terraNullius());
|
||||
return true;
|
||||
return this.sendAttack(this.game.terraNullius());
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -290,8 +290,7 @@ export class AiAttackBehavior {
|
||||
const victim = (): boolean => {
|
||||
const victim = this.findVictim(borderingEnemies);
|
||||
if (victim) {
|
||||
this.sendAttack(victim);
|
||||
return true;
|
||||
return this.sendAttack(victim);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -302,8 +301,7 @@ export class AiAttackBehavior {
|
||||
const other = relation.player;
|
||||
if (this.player.isFriendly(other)) continue;
|
||||
if (this.isFFA() && other.troops() > this.player.troops() * 3) continue;
|
||||
this.sendAttack(other);
|
||||
return true;
|
||||
return this.sendAttack(other);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -311,8 +309,7 @@ export class AiAttackBehavior {
|
||||
const veryWeak = (): boolean => {
|
||||
const veryWeak = this.findVeryWeakEnemy(borderingEnemies);
|
||||
if (veryWeak) {
|
||||
this.sendAttack(veryWeak);
|
||||
return true;
|
||||
return this.sendAttack(veryWeak);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -323,8 +320,7 @@ export class AiAttackBehavior {
|
||||
const weakest = borderingEnemies[0];
|
||||
// In FFA, don't attack if they have more troops than us
|
||||
if (!this.isFFA() || weakest.troops() < this.player.troops()) {
|
||||
this.sendAttack(weakest);
|
||||
return true;
|
||||
return this.sendAttack(weakest);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -334,8 +330,7 @@ export class AiAttackBehavior {
|
||||
if (borderingEnemies.length === 0) {
|
||||
const enemy = this.findNearestIslandEnemy();
|
||||
if (enemy) {
|
||||
this.sendAttack(enemy);
|
||||
return true;
|
||||
return this.sendAttack(enemy);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -365,7 +360,7 @@ export class AiAttackBehavior {
|
||||
|
||||
private hasNeighboringBotWithStructures(): boolean {
|
||||
return this.player
|
||||
.neighbors()
|
||||
.nearby()
|
||||
.some(
|
||||
(n) =>
|
||||
n.isPlayer() &&
|
||||
@@ -415,7 +410,7 @@ export class AiAttackBehavior {
|
||||
// Bots that own structures are prioritized as targets (they might have stolen our structures and they will delete them!)
|
||||
private attackBots(): boolean {
|
||||
const bots = this.player
|
||||
.neighbors()
|
||||
.nearby()
|
||||
.filter(
|
||||
(n): n is Player =>
|
||||
n.isPlayer() &&
|
||||
@@ -489,9 +484,8 @@ export class AiAttackBehavior {
|
||||
this.emojiBehavior.sendEmoji(ally, EMOJI_ASSIST_TARGET_ALLY);
|
||||
continue;
|
||||
}
|
||||
// All checks passed, assist them
|
||||
if (!this.sendAttack(target)) continue;
|
||||
this.player.updateRelation(ally, -20);
|
||||
this.sendAttack(target);
|
||||
this.emojiBehavior.sendEmoji(ally, EMOJI_ASSIST_ACCEPT);
|
||||
return true;
|
||||
}
|
||||
@@ -529,8 +523,7 @@ export class AiAttackBehavior {
|
||||
borderingFriends.length + borderingEnemies.length,
|
||||
)
|
||||
) {
|
||||
this.sendAttack(friend, true);
|
||||
return true;
|
||||
return this.sendAttack(friend, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -689,21 +682,19 @@ export class AiAttackBehavior {
|
||||
// Retaliate against incoming attacks
|
||||
const incomingAttackPlayer = this.findIncomingAttackPlayer();
|
||||
if (incomingAttackPlayer) {
|
||||
this.sendAttack(incomingAttackPlayer, true);
|
||||
return;
|
||||
if (this.sendAttack(incomingAttackPlayer, true)) return;
|
||||
}
|
||||
|
||||
// Select a traitor as an enemy
|
||||
const toAttack = this.getNeighborTraitorToAttack();
|
||||
if (toAttack !== null) {
|
||||
if (this.random.chance(3)) {
|
||||
this.sendAttack(toAttack);
|
||||
return;
|
||||
if (this.sendAttack(toAttack)) return;
|
||||
}
|
||||
}
|
||||
|
||||
// Choose a new enemy randomly
|
||||
const neighbors = this.player.neighbors();
|
||||
const neighbors = this.player.nearby();
|
||||
for (const neighbor of this.random.shuffleArray(neighbors)) {
|
||||
if (!neighbor.isPlayer()) continue;
|
||||
if (this.player.isFriendly(neighbor)) continue;
|
||||
@@ -715,8 +706,7 @@ export class AiAttackBehavior {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.sendAttack(neighbor);
|
||||
return;
|
||||
if (this.sendAttack(neighbor)) return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,7 +714,7 @@ export class AiAttackBehavior {
|
||||
if (this.game.config().disableAlliances()) return null;
|
||||
|
||||
const traitors = this.player
|
||||
.neighbors()
|
||||
.nearby()
|
||||
.filter(
|
||||
(n): n is Player =>
|
||||
n.isPlayer() && this.player.isFriendly(n) === false && n.isTraitor(),
|
||||
@@ -742,16 +732,94 @@ export class AiAttackBehavior {
|
||||
);
|
||||
}
|
||||
|
||||
sendAttack(target: Player | TerraNullius, force = false) {
|
||||
if (!force && !this.shouldAttack(target)) return;
|
||||
sendAttack(target: Player | TerraNullius, force = false): boolean {
|
||||
if (!force && !this.shouldAttack(target)) return false;
|
||||
|
||||
if (this.player.sharesBorderWith(target)) {
|
||||
this.sendLandAttack(target);
|
||||
} else if (target.isPlayer()) {
|
||||
this.sendBoatAttack(target);
|
||||
if (target.isPlayer()) {
|
||||
if (this.player.sharesBorderWith(target)) {
|
||||
return this.sendLandAttack(target);
|
||||
} else {
|
||||
return this.sendBoatAttack(target);
|
||||
}
|
||||
} else {
|
||||
// sharesBorderWith(TerraNullius) counts water tiles as TN (ownerID 0 = TN smallID),
|
||||
// so use a land-only adjacency check to decide land vs boat attack.
|
||||
if (this.hasLandBorderWithTerraNullius()) {
|
||||
return this.sendLandAttack(target);
|
||||
} else {
|
||||
return this.sendBoatAttackToNearbyTerraNullius();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hasLandBorderWithTerraNullius(): boolean {
|
||||
for (const border of this.player.borderTiles()) {
|
||||
for (const neighbor of this.game.neighbors(border)) {
|
||||
if (
|
||||
this.game.isLand(neighbor) &&
|
||||
!this.game.hasOwner(neighbor) &&
|
||||
!this.game.hasFallout(neighbor)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scans shore border tiles (every 10th) for unowned land within 5 water tiles
|
||||
// in each cardinal direction, then sends a transport ship to the first match.
|
||||
private sendBoatAttackToNearbyTerraNullius(): boolean {
|
||||
if (this.game.config().isUnitDisabled(UnitType.TransportShip)) return false;
|
||||
if (
|
||||
this.player.unitCount(UnitType.TransportShip) >=
|
||||
this.game.config().boatMaxNumber()
|
||||
)
|
||||
return false;
|
||||
|
||||
const directions: [number, number][] = [
|
||||
[0, -1],
|
||||
[0, 1],
|
||||
[-1, 0],
|
||||
[1, 0],
|
||||
];
|
||||
const shores = Array.from(this.player.borderTiles()).filter((t) =>
|
||||
this.game.isShore(t),
|
||||
);
|
||||
|
||||
for (let i = 0; i < shores.length; i += 10) {
|
||||
const border = shores[i];
|
||||
|
||||
const bx = this.game.x(border);
|
||||
const by = this.game.y(border);
|
||||
|
||||
for (const [dx, dy] of directions) {
|
||||
const x1 = bx + dx;
|
||||
const y1 = by + dy;
|
||||
if (!this.game.isValidCoord(x1, y1)) continue;
|
||||
if (!this.game.isWater(this.game.ref(x1, y1))) continue;
|
||||
|
||||
const nx = bx + dx * 5;
|
||||
const ny = by + dy * 5;
|
||||
if (!this.game.isValidCoord(nx, ny)) continue;
|
||||
const tile = this.game.ref(nx, ny);
|
||||
if (!this.game.isLand(tile)) continue;
|
||||
if (this.game.hasOwner(tile)) continue;
|
||||
if (this.game.hasFallout(tile)) continue;
|
||||
if (!canBuildTransportShip(this.game, this.player, tile)) continue;
|
||||
|
||||
const troops = this.player.troops() / 5;
|
||||
if (troops < 1) return false;
|
||||
|
||||
this.game.addExecution(
|
||||
new TransportShipExecution(this.player, tile, troops),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
shouldAttack(other: Player | TerraNullius): boolean {
|
||||
if (
|
||||
// Always attack Terra Nullius, non-humans and traitors
|
||||
@@ -776,7 +844,7 @@ export class AiAttackBehavior {
|
||||
return true;
|
||||
}
|
||||
|
||||
private sendLandAttack(target: Player | TerraNullius) {
|
||||
private sendLandAttack(target: Player | TerraNullius): boolean {
|
||||
const maxTroops = this.game.config().maxTroops(this.player);
|
||||
const botWithStructures =
|
||||
target.isPlayer() &&
|
||||
@@ -803,7 +871,7 @@ export class AiAttackBehavior {
|
||||
}
|
||||
|
||||
if (troops < 1) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.isPlayer() && this.player.type() === PlayerType.Nation) {
|
||||
@@ -818,11 +886,12 @@ export class AiAttackBehavior {
|
||||
target.isPlayer() ? target.id() : this.game.terraNullius().id(),
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private sendBoatAttack(target: Player) {
|
||||
private sendBoatAttack(target: Player): boolean {
|
||||
if (this.game.config().isUnitDisabled(UnitType.TransportShip)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const closest = closestTwoTiles(
|
||||
@@ -831,11 +900,11 @@ export class AiAttackBehavior {
|
||||
Array.from(target.borderTiles()).filter((t) => this.game.isShore(t)),
|
||||
);
|
||||
if (closest === null) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!canBuildTransportShip(this.game, this.player, closest.y)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let troops;
|
||||
@@ -846,7 +915,7 @@ export class AiAttackBehavior {
|
||||
}
|
||||
|
||||
if (troops < 1) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.isPlayer() && this.player.type() === PlayerType.Nation) {
|
||||
@@ -857,6 +926,7 @@ export class AiAttackBehavior {
|
||||
this.game.addExecution(
|
||||
new TransportShipExecution(this.player, closest.y, troops),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private calculateBotAttackTroops(target: Player, maxTroops: number): number {
|
||||
|
||||
@@ -739,7 +739,7 @@ export interface Player {
|
||||
captureUnit(unit: Unit): void;
|
||||
|
||||
// Relations & Diplomacy
|
||||
neighbors(): (Player | TerraNullius)[];
|
||||
nearby(): (Player | TerraNullius)[];
|
||||
sharesBorderWith(other: Player | TerraNullius): boolean;
|
||||
relation(other: Player): Relation;
|
||||
allRelationsSorted(): { player: Player; relation: Relation }[];
|
||||
|
||||
@@ -319,6 +319,7 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
numTilesOwned(): number {
|
||||
return this._tiles.size;
|
||||
}
|
||||
@@ -331,7 +332,7 @@ export class PlayerImpl implements Player {
|
||||
return this._borderTiles;
|
||||
}
|
||||
|
||||
neighbors(): (Player | TerraNullius)[] {
|
||||
nearby(): (Player | TerraNullius)[] {
|
||||
const ns: Set<Player | TerraNullius> = new Set();
|
||||
for (const border of this.borderTiles()) {
|
||||
for (const neighbor of this.mg.map().neighbors(border)) {
|
||||
@@ -345,9 +346,58 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const n of this.shoreReachableNeighbors()) {
|
||||
ns.add(n);
|
||||
}
|
||||
return Array.from(ns);
|
||||
}
|
||||
|
||||
// Samples every 10th border tile for shore tiles, checks the tile 5 steps
|
||||
// away in each cardinal direction that immediately enters water, to detect
|
||||
// players separated by a small river (up to 4 water tiles wide)
|
||||
private shoreReachableNeighbors(): Set<Player | TerraNullius> {
|
||||
const ns: Set<Player | TerraNullius> = new Set();
|
||||
const map = this.mg.map();
|
||||
const shores = Array.from(this.borderTiles()).filter((t) => map.isShore(t));
|
||||
const directions: [number, number][] = [
|
||||
[0, -1],
|
||||
[0, 1],
|
||||
[-1, 0],
|
||||
[1, 0],
|
||||
];
|
||||
|
||||
for (let i = 0; i < shores.length; i += 10) {
|
||||
const border = shores[i];
|
||||
|
||||
const bx = map.x(border);
|
||||
const by = map.y(border);
|
||||
|
||||
for (const [dx, dy] of directions) {
|
||||
// Only follow directions that immediately enter water; land-adjacent
|
||||
// directions are already covered by the direct neighbors() loop.
|
||||
const x1 = bx + dx;
|
||||
const y1 = by + dy;
|
||||
if (!map.isValidCoord(x1, y1) || !map.isWater(map.ref(x1, y1)))
|
||||
continue;
|
||||
|
||||
const nx = bx + dx * 5;
|
||||
const ny = by + dy * 5;
|
||||
if (!map.isValidCoord(nx, ny)) continue;
|
||||
const tile = map.ref(nx, ny);
|
||||
if (!map.isLand(tile)) continue;
|
||||
if (!map.hasOwner(tile) && map.hasFallout(tile)) continue;
|
||||
const owner = map.ownerID(tile);
|
||||
if (owner !== this.smallID()) {
|
||||
ns.add(
|
||||
this.mg.playerBySmallID(owner) satisfies Player | TerraNullius,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ns;
|
||||
}
|
||||
|
||||
isPlayer(): this is Player {
|
||||
return true as const;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ function makePlayer(
|
||||
): any {
|
||||
return {
|
||||
units: vi.fn(() => ownUnits),
|
||||
neighbors: vi.fn(() => neighborList),
|
||||
nearby: vi.fn(() => neighborList),
|
||||
canTrade: vi.fn((n: any) => opts.canTrade?.(n) ?? true),
|
||||
isOnSameTeam: vi.fn((n: any) => opts.isOnSameTeam?.(n) ?? false),
|
||||
isAlliedWith: vi.fn((n: any) => opts.isAlliedWith?.(n) ?? false),
|
||||
|
||||
Reference in New Issue
Block a user