mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-24 23:17:07 +00:00
## 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
130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import { Execution, Game, Player, Structures } from "../game/Game";
|
|
import { PseudoRandom } from "../PseudoRandom";
|
|
import { simpleHash } from "../Util";
|
|
import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution";
|
|
import { DeleteUnitExecution } from "./DeleteUnitExecution";
|
|
import { AiAttackBehavior } from "./utils/AiAttackBehavior";
|
|
|
|
export class TribeExecution implements Execution {
|
|
private active = true;
|
|
private random: PseudoRandom;
|
|
private mg: Game;
|
|
private neighborsTerraNullius = true;
|
|
|
|
private attackBehavior: AiAttackBehavior | null = null;
|
|
private attackRate: number;
|
|
private attackTick: number;
|
|
private triggerRatio: number;
|
|
private reserveRatio: number;
|
|
private expandRatio: number;
|
|
|
|
constructor(private tribe: Player) {
|
|
this.random = new PseudoRandom(simpleHash(tribe.id()));
|
|
this.attackRate = this.random.nextInt(40, 80);
|
|
this.attackTick = this.random.nextInt(0, this.attackRate);
|
|
this.triggerRatio = this.random.nextInt(50, 60) / 100;
|
|
this.reserveRatio = this.random.nextInt(30, 40) / 100;
|
|
this.expandRatio = this.random.nextInt(10, 20) / 100;
|
|
}
|
|
|
|
activeDuringSpawnPhase(): boolean {
|
|
return false;
|
|
}
|
|
|
|
init(mg: Game) {
|
|
this.mg = mg;
|
|
}
|
|
|
|
tick(ticks: number) {
|
|
if (ticks % this.attackRate !== this.attackTick) return;
|
|
|
|
if (!this.tribe.isAlive()) {
|
|
//removeOnDeath is called from tribe's PlayerExecution
|
|
this.active = false;
|
|
return;
|
|
}
|
|
|
|
if (this.attackBehavior === null) {
|
|
this.attackBehavior = new AiAttackBehavior(
|
|
this.random,
|
|
this.mg,
|
|
this.tribe,
|
|
this.triggerRatio,
|
|
this.reserveRatio,
|
|
this.expandRatio,
|
|
);
|
|
|
|
// Send an attack on the first tick
|
|
this.attackBehavior.sendAttack(this.mg.terraNullius());
|
|
return;
|
|
}
|
|
|
|
this.acceptAllAllianceRequests();
|
|
this.deleteNextStructure();
|
|
this.maybeAttack();
|
|
}
|
|
|
|
private acceptAllAllianceRequests() {
|
|
// Accept all alliance requests
|
|
for (const req of this.tribe.incomingAllianceRequests()) {
|
|
req.accept();
|
|
}
|
|
|
|
// Accept all alliance extension requests
|
|
for (const alliance of this.tribe.alliances()) {
|
|
// Alliance expiration tracked by Events Panel, only human ally can click Request to Renew
|
|
// Skip if no expiration yet/ ally didn't request extension yet / tribe already agreed to extend
|
|
if (!alliance.onlyOneAgreedToExtend()) continue;
|
|
|
|
const human = alliance.other(this.tribe);
|
|
this.mg.addExecution(
|
|
new AllianceExtensionExecution(this.tribe, human.id()),
|
|
);
|
|
}
|
|
}
|
|
|
|
private deleteNextStructure() {
|
|
if (!this.tribe.canDeleteUnit()) return;
|
|
for (const unit of this.tribe.units()) {
|
|
if (!Structures.has(unit.type())) continue;
|
|
if (unit.isMarkedForDeletion()) continue;
|
|
this.mg.addExecution(new DeleteUnitExecution(this.tribe, unit.id()));
|
|
return;
|
|
}
|
|
}
|
|
|
|
private maybeAttack() {
|
|
if (this.attackBehavior === null) {
|
|
throw new Error("not initialized");
|
|
}
|
|
const toAttack = this.attackBehavior.getNeighborTraitorToAttack();
|
|
if (toAttack !== null) {
|
|
const odds = this.tribe.isFriendly(toAttack) ? 6 : 3;
|
|
if (this.random.chance(odds)) {
|
|
// Check and break alliance before attacking if needed
|
|
const alliance = this.tribe.allianceWith(toAttack);
|
|
|
|
if (alliance !== null) {
|
|
this.tribe.breakAlliance(alliance);
|
|
}
|
|
|
|
if (this.attackBehavior.sendAttack(toAttack)) return;
|
|
}
|
|
}
|
|
|
|
if (this.neighborsTerraNullius) {
|
|
if (this.tribe.nearby().some((n) => !n.isPlayer())) {
|
|
if (this.attackBehavior.sendAttack(this.mg.terraNullius())) return;
|
|
} else {
|
|
this.neighborsTerraNullius = false;
|
|
}
|
|
}
|
|
|
|
this.attackBehavior.attackRandomTarget();
|
|
}
|
|
|
|
isActive(): boolean {
|
|
return this.active;
|
|
}
|
|
}
|