mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 06:03:27 +00:00
Merge branch 'main' into local-attack
This commit is contained in:
@@ -8,7 +8,9 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { ParabolaPathFinder } from "../pathfinding/PathFinding";
|
||||
import { UniversalPathFinding } from "../pathfinding/PathFinder";
|
||||
import { ParabolaUniversalPathFinder } from "../pathfinding/PathFinder.Parabola";
|
||||
import { PathStatus } from "../pathfinding/types";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { simpleHash } from "../Util";
|
||||
import { NukeExecution } from "./NukeExecution";
|
||||
@@ -30,11 +32,12 @@ export class MirvExecution implements Execution {
|
||||
|
||||
private random: PseudoRandom;
|
||||
|
||||
private pathFinder: ParabolaPathFinder;
|
||||
private pathFinder: ParabolaUniversalPathFinder;
|
||||
|
||||
private targetPlayer: Player | TerraNullius;
|
||||
|
||||
private separateDst: TileRef;
|
||||
private spawnTile: TileRef;
|
||||
|
||||
private speed: number = -1;
|
||||
|
||||
@@ -46,9 +49,11 @@ export class MirvExecution implements Execution {
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.random = new PseudoRandom(mg.ticks() + simpleHash(this.player.id()));
|
||||
this.mg = mg;
|
||||
this.pathFinder = new ParabolaPathFinder(mg);
|
||||
this.targetPlayer = this.mg.owner(this.dst);
|
||||
this.speed = this.mg.config().defaultNukeSpeed();
|
||||
this.pathFinder = UniversalPathFinding.Parabola(mg, {
|
||||
increment: this.speed,
|
||||
});
|
||||
|
||||
// Betrayal on launch
|
||||
if (this.targetPlayer.isPlayer()) {
|
||||
@@ -70,6 +75,7 @@ export class MirvExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.spawnTile = spawn;
|
||||
this.nuke = this.player.buildUnit(UnitType.MIRV, spawn, {
|
||||
targetTile: this.dst,
|
||||
});
|
||||
@@ -79,7 +85,6 @@ export class MirvExecution implements Execution {
|
||||
);
|
||||
const y = Math.max(0, this.mg.y(this.dst) - 500) + 50;
|
||||
this.separateDst = this.mg.ref(x, y);
|
||||
this.pathFinder.computeControlPoints(spawn, this.separateDst);
|
||||
|
||||
this.mg.displayIncomingUnit(
|
||||
this.nuke.id(),
|
||||
@@ -90,15 +95,19 @@ export class MirvExecution implements Execution {
|
||||
);
|
||||
}
|
||||
|
||||
const result = this.pathFinder.nextTile(this.speed);
|
||||
if (result === true) {
|
||||
const result = this.pathFinder.next(
|
||||
this.spawnTile,
|
||||
this.separateDst,
|
||||
this.speed,
|
||||
);
|
||||
if (result.status === PathStatus.COMPLETE) {
|
||||
this.separate();
|
||||
this.active = false;
|
||||
// Record stats
|
||||
this.mg.stats().bombLand(this.player, this.targetPlayer, UnitType.MIRV);
|
||||
return;
|
||||
} else {
|
||||
this.nuke.move(result);
|
||||
} else if (result.status === PathStatus.NEXT) {
|
||||
this.nuke.move(result.node);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,16 @@ import {
|
||||
isStructureType,
|
||||
MessageType,
|
||||
Player,
|
||||
StructureTypes,
|
||||
TerraNullius,
|
||||
TrajectoryTile,
|
||||
Unit,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { ParabolaPathFinder } from "../pathfinding/PathFinding";
|
||||
import { UniversalPathFinding } from "../pathfinding/PathFinder";
|
||||
import { ParabolaUniversalPathFinder } from "../pathfinding/PathFinder.Parabola";
|
||||
import { PathStatus } from "../pathfinding/types";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { NukeType } from "../StatsSchemas";
|
||||
import { computeNukeBlastCounts } from "./Util";
|
||||
@@ -22,7 +25,7 @@ export class NukeExecution implements Execution {
|
||||
private mg: Game;
|
||||
private nuke: Unit | null = null;
|
||||
private tilesToDestroyCache: Set<TileRef> | undefined;
|
||||
private pathFinder: ParabolaPathFinder;
|
||||
private pathFinder: ParabolaUniversalPathFinder;
|
||||
|
||||
constructor(
|
||||
private nukeType: NukeType,
|
||||
@@ -39,7 +42,11 @@ export class NukeExecution implements Execution {
|
||||
if (this.speed === -1) {
|
||||
this.speed = this.mg.config().defaultNukeSpeed();
|
||||
}
|
||||
this.pathFinder = new ParabolaPathFinder(mg);
|
||||
this.pathFinder = UniversalPathFinding.Parabola(mg, {
|
||||
increment: this.speed,
|
||||
distanceBasedHeight: this.nukeType !== UnitType.MIRVWarhead,
|
||||
directionUp: this.rocketDirectionUp,
|
||||
});
|
||||
}
|
||||
|
||||
public target(): Player | TerraNullius {
|
||||
@@ -66,7 +73,7 @@ export class NukeExecution implements Execution {
|
||||
|
||||
/**
|
||||
* Break alliances with players significantly affected by the nuke strike.
|
||||
* Uses weighted tile counting (inner=1, outer=0.5).
|
||||
* Uses weighted tile counting (inner=1, outer=0.5) OR if any allied structure would be destroyed.
|
||||
*/
|
||||
private maybeBreakAlliances() {
|
||||
if (this.nuke === null) {
|
||||
@@ -87,29 +94,48 @@ export class NukeExecution implements Execution {
|
||||
magnitude,
|
||||
});
|
||||
|
||||
// Collect all players that should have alliance broken:
|
||||
// either exceeds tile threshold OR has a structure in blast radius
|
||||
const playersToBreakAllianceWith = new Set<number>();
|
||||
|
||||
for (const [playerSmallId, totalWeight] of blastCounts) {
|
||||
if (totalWeight > threshold) {
|
||||
const attackedPlayer = this.mg.playerBySmallID(playerSmallId);
|
||||
if (!attackedPlayer.isPlayer()) {
|
||||
continue;
|
||||
}
|
||||
playersToBreakAllianceWith.add(playerSmallId);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolves exploit of alliance breaking in which a pending alliance request
|
||||
// was accepted in the middle of a missile attack.
|
||||
const allianceRequest = attackedPlayer
|
||||
.incomingAllianceRequests()
|
||||
.find((ar) => ar.requestor() === this.player);
|
||||
if (allianceRequest) {
|
||||
allianceRequest.reject();
|
||||
}
|
||||
// Also check if any allied structures would be destroyed
|
||||
this.mg
|
||||
.nearbyUnits(this.dst, magnitude.outer, [...StructureTypes])
|
||||
.filter(
|
||||
({ unit }) =>
|
||||
unit.owner().isPlayer() && this.player.isAlliedWith(unit.owner()),
|
||||
)
|
||||
.forEach(({ unit }) =>
|
||||
playersToBreakAllianceWith.add(unit.owner().smallID()),
|
||||
);
|
||||
|
||||
const alliance = this.player.allianceWith(attackedPlayer);
|
||||
if (alliance !== null) {
|
||||
this.player.breakAlliance(alliance);
|
||||
}
|
||||
if (attackedPlayer !== this.player) {
|
||||
attackedPlayer.updateRelation(this.player, -100);
|
||||
}
|
||||
for (const playerSmallId of playersToBreakAllianceWith) {
|
||||
const attackedPlayer = this.mg.playerBySmallID(playerSmallId);
|
||||
if (!attackedPlayer.isPlayer()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolves exploit of alliance breaking in which a pending alliance request
|
||||
// was accepted in the middle of a missile attack.
|
||||
const allianceRequest = attackedPlayer
|
||||
.incomingAllianceRequests()
|
||||
.find((ar) => ar.requestor() === this.player);
|
||||
if (allianceRequest) {
|
||||
allianceRequest.reject();
|
||||
}
|
||||
|
||||
const alliance = this.player.allianceWith(attackedPlayer);
|
||||
if (alliance !== null) {
|
||||
this.player.breakAlliance(alliance);
|
||||
}
|
||||
if (attackedPlayer !== this.player) {
|
||||
attackedPlayer.updateRelation(this.player, -100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,13 +149,6 @@ export class NukeExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
this.src = spawn;
|
||||
this.pathFinder.computeControlPoints(
|
||||
spawn,
|
||||
this.dst,
|
||||
this.speed,
|
||||
this.nukeType !== UnitType.MIRVWarhead,
|
||||
this.rocketDirectionUp,
|
||||
);
|
||||
this.nuke = this.player.buildUnit(this.nukeType, spawn, {
|
||||
targetTile: this.dst,
|
||||
trajectory: this.getTrajectory(this.dst),
|
||||
@@ -186,13 +205,13 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
|
||||
// Move to next tile
|
||||
const nextTile = this.pathFinder.nextTile(this.speed);
|
||||
if (nextTile === true) {
|
||||
const result = this.pathFinder.next(this.src!, this.dst, this.speed);
|
||||
if (result.status === PathStatus.COMPLETE) {
|
||||
this.detonate();
|
||||
return;
|
||||
} else {
|
||||
} else if (result.status === PathStatus.NEXT) {
|
||||
this.updateNukeTargetable();
|
||||
this.nuke.move(nextTile);
|
||||
this.nuke.move(result.node);
|
||||
// Update index so SAM can interpolate future position
|
||||
this.nuke.setTrajectoryIndex(this.pathFinder.currentIndex());
|
||||
}
|
||||
@@ -206,7 +225,7 @@ export class NukeExecution implements Execution {
|
||||
const trajectoryTiles: TrajectoryTile[] = [];
|
||||
const targetRangeSquared =
|
||||
this.mg.config().defaultNukeTargetableRange() ** 2;
|
||||
const allTiles: TileRef[] = this.pathFinder.allTiles();
|
||||
const allTiles = this.pathFinder.findPath(this.src!, target) ?? [];
|
||||
for (const tile of allTiles) {
|
||||
trajectoryTiles.push({
|
||||
tile,
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { AirPathFinder } from "../pathfinding/PathFinding";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { NukeType } from "../StatsSchemas";
|
||||
|
||||
export class SAMMissileExecution implements Execution {
|
||||
private active = true;
|
||||
private pathFinder: AirPathFinder;
|
||||
private pathFinder: SteppingPathFinder<TileRef>;
|
||||
private SAMMissile: Unit | undefined;
|
||||
private mg: Game;
|
||||
private speed: number = 0;
|
||||
@@ -27,7 +27,7 @@ export class SAMMissileExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathFinder = new AirPathFinder(mg, new PseudoRandom(mg.ticks()));
|
||||
this.pathFinder = PathFinding.Air(mg);
|
||||
this.mg = mg;
|
||||
this.speed = this.mg.config().defaultSamMissileSpeed();
|
||||
}
|
||||
@@ -55,11 +55,11 @@ export class SAMMissileExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.speed; i++) {
|
||||
const result = this.pathFinder.nextTile(
|
||||
const result = this.pathFinder.next(
|
||||
this.SAMMissile.tile(),
|
||||
this.targetTile,
|
||||
);
|
||||
if (result === true) {
|
||||
if (result.status === PathStatus.COMPLETE) {
|
||||
this.mg.displayMessage(
|
||||
"events_display.missile_intercepted",
|
||||
MessageType.SAM_HIT,
|
||||
@@ -76,8 +76,8 @@ export class SAMMissileExecution implements Execution {
|
||||
.stats()
|
||||
.bombIntercept(this._owner, this.target.type() as NukeType, 1);
|
||||
return;
|
||||
} else {
|
||||
this.SAMMissile.move(result);
|
||||
} else if (result.status === PathStatus.NEXT) {
|
||||
this.SAMMissile.move(result.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Execution, Game, Player, Unit, UnitType } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { AirPathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
|
||||
export class ShellExecution implements Execution {
|
||||
private active = true;
|
||||
private pathFinder: AirPathFinder;
|
||||
private pathFinder: SteppingPathFinder<TileRef>;
|
||||
private shell: Unit | undefined;
|
||||
private mg: Game;
|
||||
private destroyAtTick: number = -1;
|
||||
@@ -19,7 +20,7 @@ export class ShellExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathFinder = new AirPathFinder(mg, new PseudoRandom(mg.ticks()));
|
||||
this.pathFinder = PathFinding.Air(mg);
|
||||
this.mg = mg;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
}
|
||||
@@ -45,18 +46,18 @@ export class ShellExecution implements Execution {
|
||||
}
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const result = this.pathFinder.nextTile(
|
||||
const result = this.pathFinder.next(
|
||||
this.shell.tile(),
|
||||
this.target.tile(),
|
||||
);
|
||||
if (result === true) {
|
||||
if (result.status === PathStatus.COMPLETE) {
|
||||
this.active = false;
|
||||
this.target.modifyHealth(-this.effectOnTarget(), this._owner);
|
||||
this.shell.setReachedTarget();
|
||||
this.shell.delete(false);
|
||||
return;
|
||||
} else {
|
||||
this.shell.move(result);
|
||||
} else if (result.status === PathStatus.NEXT) {
|
||||
this.shell.move(result.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { PathFinder, PathFinders, PathStatus } from "../pathfinding/PathFinder";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { distSortUnit } from "../Util";
|
||||
|
||||
export class TradeShipExecution implements Execution {
|
||||
@@ -16,7 +17,7 @@ export class TradeShipExecution implements Execution {
|
||||
private mg: Game;
|
||||
private tradeShip: Unit | undefined;
|
||||
private wasCaptured = false;
|
||||
private pathFinder: PathFinder;
|
||||
private pathFinder: SteppingPathFinder<TileRef>;
|
||||
private tilesTraveled = 0;
|
||||
|
||||
constructor(
|
||||
@@ -27,7 +28,7 @@ export class TradeShipExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinders.Water(mg);
|
||||
this.pathFinder = PathFinding.Water(mg);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { targetTransportTile } from "../game/TransportShipUtils";
|
||||
import { PathFinder, PathFinders, PathStatus } from "../pathfinding/PathFinder";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
|
||||
const malusForRetreat = 25;
|
||||
@@ -29,11 +30,10 @@ export class TransportShipExecution implements Execution {
|
||||
// TODO make private
|
||||
public path: TileRef[];
|
||||
private dst: TileRef | null;
|
||||
private dstShore: TileRef | null;
|
||||
|
||||
private boat: Unit;
|
||||
|
||||
private pathFinder: PathFinder;
|
||||
private pathFinder: SteppingPathFinder<TileRef>;
|
||||
|
||||
private originalOwner: Player;
|
||||
|
||||
@@ -70,7 +70,7 @@ export class TransportShipExecution implements Execution {
|
||||
|
||||
this.lastMove = ticks;
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinders.Water(mg);
|
||||
this.pathFinder = PathFinding.Water(mg);
|
||||
|
||||
if (
|
||||
this.attacker.unitCount(UnitType.TransportShip) >=
|
||||
@@ -106,8 +106,8 @@ export class TransportShipExecution implements Execution {
|
||||
|
||||
this.startTroops = Math.min(this.startTroops, this.attacker.troops());
|
||||
|
||||
this.dstShore = targetTransportTile(this.mg, this.ref);
|
||||
if (this.dstShore === null) {
|
||||
this.dst = targetTransportTile(this.mg, this.ref);
|
||||
if (this.dst === null) {
|
||||
console.warn(
|
||||
`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`,
|
||||
);
|
||||
@@ -115,18 +115,9 @@ export class TransportShipExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dst = this.adjacentWater(this.dstShore);
|
||||
if (this.dst === null) {
|
||||
console.warn(
|
||||
`${this.attacker} cannot find water tile adjacent to destination`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const closestTileSrc = this.attacker.canBuild(
|
||||
UnitType.TransportShip,
|
||||
this.dstShore,
|
||||
this.dst,
|
||||
);
|
||||
if (closestTileSrc === false) {
|
||||
console.warn(`can't build transport ship`);
|
||||
@@ -152,21 +143,10 @@ export class TransportShipExecution implements Execution {
|
||||
|
||||
this.boat = this.attacker.buildUnit(UnitType.TransportShip, this.src, {
|
||||
troops: this.startTroops,
|
||||
targetTile: this.dst ?? undefined,
|
||||
});
|
||||
|
||||
// Move boat from shore to adjacent water for pathfinding
|
||||
const spawnWater = this.adjacentWater(this.src);
|
||||
if (spawnWater === null) {
|
||||
console.warn(`No adjacent water for transport ship spawn`);
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.boat.move(spawnWater);
|
||||
|
||||
if (this.dstShore !== null) {
|
||||
this.boat.setTargetTile(this.dstShore);
|
||||
if (this.dst !== null) {
|
||||
this.boat.setTargetTile(this.dst);
|
||||
} else {
|
||||
this.boat.setTargetTile(undefined);
|
||||
}
|
||||
@@ -222,7 +202,6 @@ export class TransportShipExecution implements Execution {
|
||||
if (this.mg.owner(this.src!) !== this.attacker) {
|
||||
// Use bestTransportShipSpawn, not canBuild because of its max boats check etc
|
||||
const newSrc = this.attacker.bestTransportShipSpawn(this.dst);
|
||||
|
||||
if (newSrc === false) {
|
||||
this.src = null;
|
||||
} else {
|
||||
@@ -239,19 +218,10 @@ export class TransportShipExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
} else {
|
||||
this.dstShore = this.src;
|
||||
const retreatWater = this.adjacentWater(this.src);
|
||||
if (retreatWater === null) {
|
||||
console.warn(`No adjacent water for retreat destination`);
|
||||
this.attacker.addTroops(this.boat.troops());
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.dst = retreatWater;
|
||||
this.dst = this.src;
|
||||
|
||||
if (this.boat.targetTile() !== this.dstShore) {
|
||||
this.boat.setTargetTile(this.dstShore!);
|
||||
if (this.boat.targetTile() !== this.dst) {
|
||||
this.boat.setTargetTile(this.dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,7 +229,7 @@ export class TransportShipExecution implements Execution {
|
||||
const result = this.pathFinder.next(this.boat.tile(), this.dst);
|
||||
switch (result.status) {
|
||||
case PathStatus.COMPLETE:
|
||||
if (this.mg.owner(this.dstShore!) === this.attacker) {
|
||||
if (this.mg.owner(this.dst) === this.attacker) {
|
||||
const deaths = this.boat.troops() * (malusForRetreat / 100);
|
||||
const survivors = this.boat.troops() - deaths;
|
||||
this.attacker.addTroops(survivors);
|
||||
@@ -281,7 +251,7 @@ export class TransportShipExecution implements Execution {
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.attacker.conquer(this.dstShore!);
|
||||
this.attacker.conquer(this.dst);
|
||||
if (this.target.isPlayer() && this.attacker.isFriendly(this.target)) {
|
||||
this.attacker.addTroops(this.boat.troops());
|
||||
} else {
|
||||
@@ -290,7 +260,7 @@ export class TransportShipExecution implements Execution {
|
||||
this.boat.troops(),
|
||||
this.attacker,
|
||||
this.targetID,
|
||||
this.dstShore!,
|
||||
this.dst,
|
||||
false,
|
||||
),
|
||||
);
|
||||
@@ -308,13 +278,18 @@ export class TransportShipExecution implements Execution {
|
||||
break;
|
||||
case PathStatus.PENDING:
|
||||
break;
|
||||
case PathStatus.NOT_FOUND:
|
||||
case PathStatus.NOT_FOUND: {
|
||||
// TODO: add to poisoned port list
|
||||
console.warn(`path not found to dst`);
|
||||
const map = this.mg.map();
|
||||
const boatTile = this.boat.tile();
|
||||
console.warn(
|
||||
`TransportShip path not found: boat@(${map.x(boatTile)},${map.y(boatTile)}) -> dst@(${map.x(this.dst)},${map.y(this.dst)}), attacker=${this.attacker.id()}, target=${this.targetID}`,
|
||||
);
|
||||
this.attacker.addTroops(this.boat.troops());
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,17 +300,4 @@ export class TransportShipExecution implements Execution {
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
private adjacentWater(tile: TileRef): TileRef | null {
|
||||
if (this.mg.isWater(tile)) {
|
||||
return tile;
|
||||
}
|
||||
|
||||
for (const neighbor of this.mg.neighbors(tile)) {
|
||||
if (this.mg.isWater(neighbor)) {
|
||||
return neighbor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+36
-15
@@ -1,6 +1,7 @@
|
||||
import { NukeMagnitude } from "../configuration/Config";
|
||||
import { Game, Player } from "../game/Game";
|
||||
import { Game, Player, StructureTypes } from "../game/Game";
|
||||
import { euclDistFN, GameMap, TileRef } from "../game/GameMap";
|
||||
import { GameView } from "../game/GameView";
|
||||
|
||||
export interface NukeBlastParams {
|
||||
gm: GameMap;
|
||||
@@ -34,40 +35,60 @@ export function computeNukeBlastCounts(
|
||||
return counts;
|
||||
}
|
||||
|
||||
export interface NukeAllianceCheckParams extends NukeBlastParams {
|
||||
export interface NukeAllianceCheckParams {
|
||||
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 { gm, targetTile, magnitude, allySmallIds, threshold } = params;
|
||||
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;
|
||||
|
||||
gm.circleSearch(targetTile, magnitude.outer, (tile: TileRef, d2: number) => {
|
||||
const ownerSmallId = gm.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);
|
||||
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.
|
||||
if (newCount > threshold) {
|
||||
result = true;
|
||||
return false; // Found one! Stop searching.
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { PathFinder, PathFinders, PathStatus } from "../pathfinding/PathFinder";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { ShellExecution } from "./ShellExecution";
|
||||
|
||||
@@ -16,7 +17,7 @@ export class WarshipExecution implements Execution {
|
||||
private random: PseudoRandom;
|
||||
private warship: Unit;
|
||||
private mg: Game;
|
||||
private pathfinder: PathFinder;
|
||||
private pathfinder: SteppingPathFinder<TileRef>;
|
||||
private lastShellAttack = 0;
|
||||
private alreadySentShell = new Set<Unit>();
|
||||
|
||||
@@ -26,7 +27,7 @@ export class WarshipExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.pathfinder = PathFinders.Water(mg);
|
||||
this.pathfinder = PathFinding.Water(mg);
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
if (isUnit(this.input)) {
|
||||
this.warship = this.input;
|
||||
@@ -193,9 +194,10 @@ export class WarshipExecution implements Execution {
|
||||
case PathStatus.PENDING:
|
||||
this.warship.touch();
|
||||
break;
|
||||
case PathStatus.NOT_FOUND:
|
||||
case PathStatus.NOT_FOUND: {
|
||||
console.log(`path not found to target`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,10 +225,10 @@ export class WarshipExecution implements Execution {
|
||||
case PathStatus.PENDING:
|
||||
this.warship.touch();
|
||||
return;
|
||||
case PathStatus.NOT_FOUND:
|
||||
console.warn(`path not found to target tile`);
|
||||
this.warship.setTargetTile(undefined);
|
||||
case PathStatus.NOT_FOUND: {
|
||||
console.log(`path not found to target`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +245,10 @@ export class WarshipExecution implements Execution {
|
||||
const maxAttemptBeforeExpand: number = 500;
|
||||
let attempts: number = 0;
|
||||
let expandCount: number = 0;
|
||||
|
||||
// Get warship's water component for connectivity check
|
||||
const warshipComponent = this.mg.getWaterComponent(this.warship.tile());
|
||||
|
||||
while (expandCount < 3) {
|
||||
const x =
|
||||
this.mg.x(this.warship.patrolTile()!) +
|
||||
@@ -267,6 +273,20 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Check water component connectivity
|
||||
if (
|
||||
warshipComponent !== null &&
|
||||
!this.mg.hasWaterComponent(tile, warshipComponent)
|
||||
) {
|
||||
attempts++;
|
||||
if (attempts === maxAttemptBeforeExpand) {
|
||||
expandCount++;
|
||||
attempts = 0;
|
||||
warshipPatrolRange =
|
||||
warshipPatrolRange + Math.floor(warshipPatrolRange / 2);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
console.warn(
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
Game,
|
||||
GameMode,
|
||||
Player,
|
||||
PlayerType,
|
||||
RankedType,
|
||||
Team,
|
||||
} from "../game/Game";
|
||||
|
||||
@@ -44,6 +46,19 @@ export class WinCheckExecution implements Execution {
|
||||
if (sorted.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mg.config().gameConfig().rankedType === RankedType.OneVOne) {
|
||||
const humans = sorted.filter(
|
||||
(p) => p.type() === PlayerType.Human && !p.isDisconnected(),
|
||||
);
|
||||
if (humans.length === 1) {
|
||||
this.mg.setWinner(humans[0], this.mg.stats().stats());
|
||||
console.log(`${humans[0].name()} has won the game`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const max = sorted[0];
|
||||
const timeElapsed =
|
||||
(this.mg.ticks() - this.mg.config().numSpawnPhaseTurns()) / 10;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
UnitType,
|
||||
} from "../../game/Game";
|
||||
import { TileRef, euclDistFN } from "../../game/GameMap";
|
||||
import { ParabolaPathFinder } from "../../pathfinding/PathFinding";
|
||||
import { UniversalPathFinding } from "../../pathfinding/PathFinder";
|
||||
import { PseudoRandom } from "../../PseudoRandom";
|
||||
import { assertNever, boundingBoxTiles } from "../../Util";
|
||||
import { NukeExecution } from "../NukeExecution";
|
||||
@@ -456,20 +456,14 @@ export class NationNukeBehavior {
|
||||
spawnTile: TileRef,
|
||||
targetTile: TileRef,
|
||||
): boolean {
|
||||
const pathFinder = new ParabolaPathFinder(this.game);
|
||||
const speed = this.game.config().defaultNukeSpeed();
|
||||
const distanceBasedHeight = true; // Atom/Hydrogen bombs use distance-based height
|
||||
const rocketDirectionUp = true; // AI nukes always go "up" for now
|
||||
const pathFinder = UniversalPathFinding.Parabola(this.game, {
|
||||
increment: speed,
|
||||
distanceBasedHeight: true, // Atom/Hydrogen bombs use distance-based height
|
||||
directionUp: true, // AI nukes always go "up" for now
|
||||
});
|
||||
|
||||
pathFinder.computeControlPoints(
|
||||
spawnTile,
|
||||
targetTile,
|
||||
speed,
|
||||
distanceBasedHeight,
|
||||
rocketDirectionUp,
|
||||
);
|
||||
|
||||
const trajectory = pathFinder.allTiles();
|
||||
const trajectory = pathFinder.findPath(spawnTile, targetTile) ?? [];
|
||||
if (trajectory.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user