|
|
|
@@ -21,10 +21,10 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
private pathfinder: WaterPathFinder;
|
|
|
|
|
private lastShellAttack = 0;
|
|
|
|
|
private alreadySentShell = new Set<Unit>();
|
|
|
|
|
private retreatPortTile: TileRef | undefined;
|
|
|
|
|
private retreatingForRepair = false;
|
|
|
|
|
private docked = false;
|
|
|
|
|
private lastManualMoveTickRetreatDisabled = 0;
|
|
|
|
|
private lastObservedPatrolTile: TileRef | undefined;
|
|
|
|
|
private activeHealingRemainder = 0;
|
|
|
|
|
private lastEmittedCombat = false;
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private input: (UnitParams<UnitType.Warship> & OwnerComp) | Unit,
|
|
|
|
@@ -53,6 +53,7 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
this.input,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
this.lastObservedPatrolTile = this.warship.warshipState().patrolTile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tick(ticks: number): void {
|
|
|
|
@@ -60,20 +61,24 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
this.warship.delete();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const isInCombat = this.warship.warshipState().isInCombat ?? false;
|
|
|
|
|
if (this.lastEmittedCombat && !isInCombat) {
|
|
|
|
|
this.warship.touch();
|
|
|
|
|
}
|
|
|
|
|
this.lastEmittedCombat = isInCombat;
|
|
|
|
|
const healthBeforeHealing = this.warship.health();
|
|
|
|
|
|
|
|
|
|
this.healWarship();
|
|
|
|
|
this.handleManualPatrolOverride();
|
|
|
|
|
|
|
|
|
|
if (this.docked) {
|
|
|
|
|
if (this.warship.warshipState().state === "docked") {
|
|
|
|
|
if (this.currentRetreatPort() === undefined) {
|
|
|
|
|
this.docked = false;
|
|
|
|
|
this.cancelRepairRetreat();
|
|
|
|
|
}
|
|
|
|
|
if (this.isFullyHealed()) {
|
|
|
|
|
this.docked = false;
|
|
|
|
|
this.cancelRepairRetreat();
|
|
|
|
|
}
|
|
|
|
|
if (this.docked) {
|
|
|
|
|
if (this.warship.warshipState().state === "docked") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -92,21 +97,17 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
|
|
|
|
|
this.warship.setTargetUnit(this.findTargetUnit());
|
|
|
|
|
|
|
|
|
|
// Always patrol for movement
|
|
|
|
|
this.patrol();
|
|
|
|
|
|
|
|
|
|
// Movement can change what is actually in range, so recompute before acting.
|
|
|
|
|
this.warship.setTargetUnit(this.findTargetUnit());
|
|
|
|
|
|
|
|
|
|
// Priority 1: Shoot transport ship if in range
|
|
|
|
|
if (this.warship.targetUnit()?.type() === UnitType.TransportShip) {
|
|
|
|
|
this.shootTarget();
|
|
|
|
|
this.patrol();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Priority 2: Fight enemy warship if in range
|
|
|
|
|
if (this.warship.targetUnit()?.type() === UnitType.Warship) {
|
|
|
|
|
this.shootTarget();
|
|
|
|
|
this.patrol();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -115,23 +116,35 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
this.huntDownTradeShip();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.patrol();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private healWarship(): void {
|
|
|
|
|
const owner = this.warship.owner();
|
|
|
|
|
const passiveHealing = this.mg.config().warshipPassiveHealing();
|
|
|
|
|
const passiveHealingRange = this.mg.config().warshipPassiveHealingRange();
|
|
|
|
|
const passiveHealingRangeSquared =
|
|
|
|
|
passiveHealingRange * passiveHealingRange;
|
|
|
|
|
const warshipTile = this.warship.tile();
|
|
|
|
|
|
|
|
|
|
const isNearPort = this.mg
|
|
|
|
|
.nearbyUnits(warshipTile, passiveHealingRange, [UnitType.Port])
|
|
|
|
|
.some(({ unit }) => unit.owner() === owner);
|
|
|
|
|
let isNearPort = false;
|
|
|
|
|
for (const port of owner.units(UnitType.Port)) {
|
|
|
|
|
const distSquared = this.mg.euclideanDistSquared(
|
|
|
|
|
warshipTile,
|
|
|
|
|
port.tile(),
|
|
|
|
|
);
|
|
|
|
|
if (distSquared <= passiveHealingRangeSquared) {
|
|
|
|
|
isNearPort = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isNearPort) {
|
|
|
|
|
this.warship.modifyHealth(passiveHealing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.docked) {
|
|
|
|
|
if (this.warship.warshipState().state === "docked") {
|
|
|
|
|
this.applyActiveDockedHealing();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -139,7 +152,6 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
private isFullyHealed(): boolean {
|
|
|
|
|
const maxHealth = this.mg.config().unitInfo(UnitType.Warship).maxHealth;
|
|
|
|
|
if (typeof maxHealth !== "number") {
|
|
|
|
|
console.warn("Warship maxHealth is not a number, disabling retreat");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return this.warship.health() >= maxHealth;
|
|
|
|
@@ -148,7 +160,14 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
private shouldStartRepairRetreat(
|
|
|
|
|
healthBeforeHealing = this.warship.health(),
|
|
|
|
|
): boolean {
|
|
|
|
|
if (this.retreatingForRepair) {
|
|
|
|
|
if (this.warship.warshipState().state !== "patrolling") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const manualMoveRetreatDisabledDuration = 50;
|
|
|
|
|
if (
|
|
|
|
|
this.mg.ticks() - this.lastManualMoveTickRetreatDisabled <
|
|
|
|
|
manualMoveRetreatDisabledDuration
|
|
|
|
|
) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
@@ -156,7 +175,6 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
// Only retreat if there's a friendly port
|
|
|
|
|
const ports = this.warship.owner().units(UnitType.Port);
|
|
|
|
|
return ports.length > 0;
|
|
|
|
|
}
|
|
|
|
@@ -188,37 +206,159 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
return nearest?.tile();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findRetreatAggroTarget(): Unit | undefined {
|
|
|
|
|
return this.findBestTarget([UnitType.TransportShip, UnitType.Warship]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findTargetUnit(): Unit | undefined {
|
|
|
|
|
return this.findBestTarget(
|
|
|
|
|
[UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip],
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Shared target selection: searches nearby units of given types,
|
|
|
|
|
* filters common exclusions (self, friendly, docked, already-shelled),
|
|
|
|
|
* picks best by type priority (lower index = higher priority) then distance.
|
|
|
|
|
*
|
|
|
|
|
* When `includeTradeShips` is true, applies trade-ship-specific filters
|
|
|
|
|
* (safe from pirates, patrol range, water component, allied destination).
|
|
|
|
|
*/
|
|
|
|
|
private findBestTarget(
|
|
|
|
|
types: UnitType[],
|
|
|
|
|
includeTradeShips = false,
|
|
|
|
|
): Unit | undefined {
|
|
|
|
|
const mg = this.mg;
|
|
|
|
|
const config = mg.config();
|
|
|
|
|
const owner = this.warship.owner();
|
|
|
|
|
|
|
|
|
|
const ships = mg.nearbyUnits(
|
|
|
|
|
this.warship.tile(),
|
|
|
|
|
config.warshipTargettingRange(),
|
|
|
|
|
types,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Trade-ship-specific state, lazily computed.
|
|
|
|
|
let hasPort: boolean | undefined;
|
|
|
|
|
let patrolTile: number | undefined;
|
|
|
|
|
let patrolRangeSquared: number | undefined;
|
|
|
|
|
let warshipComponent: number | null | undefined = undefined;
|
|
|
|
|
|
|
|
|
|
let bestUnit: Unit | undefined = undefined;
|
|
|
|
|
let bestTypePriority = 0;
|
|
|
|
|
let bestDistSquared = 0;
|
|
|
|
|
|
|
|
|
|
for (const { unit, distSquared } of ships) {
|
|
|
|
|
if (
|
|
|
|
|
unit === this.warship ||
|
|
|
|
|
unit.owner() === owner ||
|
|
|
|
|
!owner.canAttackPlayer(unit.owner(), true) ||
|
|
|
|
|
this.alreadySentShell.has(unit) ||
|
|
|
|
|
(unit.type() === UnitType.Warship &&
|
|
|
|
|
unit.warshipState().state === "docked")
|
|
|
|
|
) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const type = unit.type();
|
|
|
|
|
|
|
|
|
|
if (includeTradeShips && type === UnitType.TradeShip) {
|
|
|
|
|
if (hasPort === undefined) {
|
|
|
|
|
hasPort = owner.unitCount(UnitType.Port) > 0;
|
|
|
|
|
patrolTile = this.warship.warshipState().patrolTile;
|
|
|
|
|
patrolRangeSquared = config.warshipPatrolRange() ** 2;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
!hasPort ||
|
|
|
|
|
patrolTile === undefined ||
|
|
|
|
|
unit.isSafeFromPirates() ||
|
|
|
|
|
unit.targetUnit()?.owner() === owner ||
|
|
|
|
|
unit.targetUnit()?.owner().isFriendly(owner)
|
|
|
|
|
) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (warshipComponent === undefined) {
|
|
|
|
|
warshipComponent = mg.getWaterComponent(this.warship.tile());
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
warshipComponent !== null &&
|
|
|
|
|
!mg.hasWaterComponent(unit.tile(), warshipComponent)
|
|
|
|
|
) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
mg.euclideanDistSquared(patrolTile, unit.tile()) > patrolRangeSquared!
|
|
|
|
|
) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const typePriority =
|
|
|
|
|
type === UnitType.TransportShip ? 0 : type === UnitType.Warship ? 1 : 2;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
bestUnit === undefined ||
|
|
|
|
|
typePriority < bestTypePriority ||
|
|
|
|
|
(typePriority === bestTypePriority && distSquared < bestDistSquared)
|
|
|
|
|
) {
|
|
|
|
|
bestUnit = unit;
|
|
|
|
|
bestTypePriority = typePriority;
|
|
|
|
|
bestDistSquared = distSquared;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bestUnit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private startRepairRetreat(): void {
|
|
|
|
|
const portTile = this.findNearestPort();
|
|
|
|
|
if (portTile === undefined) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.retreatingForRepair = true;
|
|
|
|
|
this.retreatPortTile = portTile;
|
|
|
|
|
this.docked = false;
|
|
|
|
|
this.warship.updateWarshipState({
|
|
|
|
|
retreatPort: portTile,
|
|
|
|
|
state: "retreating",
|
|
|
|
|
});
|
|
|
|
|
this.activeHealingRemainder = 0;
|
|
|
|
|
this.warship.setRetreating(true);
|
|
|
|
|
this.warship.setTargetUnit(undefined);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private cancelRepairRetreat(clearTargetTile = true): void {
|
|
|
|
|
this.retreatingForRepair = false;
|
|
|
|
|
this.activeHealingRemainder = 0;
|
|
|
|
|
this.warship.setRetreating(false);
|
|
|
|
|
this.retreatPortTile = undefined;
|
|
|
|
|
this.warship.updateWarshipState({
|
|
|
|
|
state: "patrolling",
|
|
|
|
|
retreatPort: undefined,
|
|
|
|
|
});
|
|
|
|
|
if (clearTargetTile) {
|
|
|
|
|
this.warship.setTargetTile(undefined);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleManualPatrolOverride(): void {
|
|
|
|
|
const patrolTile = this.warship.warshipState().patrolTile;
|
|
|
|
|
if (
|
|
|
|
|
this.lastObservedPatrolTile !== undefined &&
|
|
|
|
|
patrolTile !== this.lastObservedPatrolTile
|
|
|
|
|
) {
|
|
|
|
|
this.lastManualMoveTickRetreatDisabled = this.mg.ticks();
|
|
|
|
|
if (this.warship.warshipState().state !== "patrolling") {
|
|
|
|
|
this.cancelRepairRetreat(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.lastObservedPatrolTile = patrolTile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleRepairRetreat(): boolean {
|
|
|
|
|
if (!this.retreatingForRepair) {
|
|
|
|
|
if (this.warship.warshipState().state === "patrolling") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.isFullyHealed()) {
|
|
|
|
|
this.cancelRepairRetreat();
|
|
|
|
|
return false;
|
|
|
|
|
const retreatAggroTarget = this.findRetreatAggroTarget();
|
|
|
|
|
if (retreatAggroTarget) {
|
|
|
|
|
this.warship.setTargetUnit(retreatAggroTarget);
|
|
|
|
|
this.shootTarget();
|
|
|
|
|
// Fall through — continue retreating toward port even while firing back.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.refreshRetreatPortTile()) {
|
|
|
|
@@ -226,9 +366,12 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.warship.setTargetUnit(undefined);
|
|
|
|
|
// Only clear the target when there's no active aggro target this tick.
|
|
|
|
|
if (!retreatAggroTarget) {
|
|
|
|
|
this.warship.setTargetUnit(undefined);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const retreatPortTile = this.retreatPortTile;
|
|
|
|
|
const retreatPortTile = this.warship.warshipState().retreatPort;
|
|
|
|
|
if (retreatPortTile === undefined) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
@@ -249,10 +392,16 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
if (port && !this.isPortFullOfHealing(port, this.warship)) {
|
|
|
|
|
// Port has capacity - dock here
|
|
|
|
|
this.warship.setTargetTile(undefined);
|
|
|
|
|
this.docked = true;
|
|
|
|
|
this.warship.updateWarshipState({
|
|
|
|
|
state: "docked",
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
// Port is full - don't cancel retreat, keep waiting near port
|
|
|
|
|
// Port is full - wait near port, but leave if already fully healed
|
|
|
|
|
if (this.isFullyHealed()) {
|
|
|
|
|
this.cancelRepairRetreat();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -269,12 +418,16 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
case PathStatus.NEXT:
|
|
|
|
|
this.warship.move(result.node);
|
|
|
|
|
break;
|
|
|
|
|
case PathStatus.NOT_FOUND:
|
|
|
|
|
this.retreatPortTile = this.findNearestAvailablePortTile(this.warship);
|
|
|
|
|
if (this.retreatPortTile === undefined) {
|
|
|
|
|
case PathStatus.NOT_FOUND: {
|
|
|
|
|
const newPort = this.findNearestAvailablePortTile();
|
|
|
|
|
this.warship.updateWarshipState({
|
|
|
|
|
retreatPort: newPort,
|
|
|
|
|
});
|
|
|
|
|
if (newPort === undefined) {
|
|
|
|
|
this.cancelRepairRetreat();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
@@ -286,31 +439,40 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentRetreatPort = this.warship.warshipState().retreatPort;
|
|
|
|
|
|
|
|
|
|
// Check if current retreat port still exists
|
|
|
|
|
const currentPortExists =
|
|
|
|
|
this.retreatPortTile !== undefined &&
|
|
|
|
|
ports.some((port) => port.tile() === this.retreatPortTile);
|
|
|
|
|
currentRetreatPort !== undefined &&
|
|
|
|
|
ports.some((port) => port.tile() === currentRetreatPort);
|
|
|
|
|
|
|
|
|
|
if (!currentPortExists) {
|
|
|
|
|
this.retreatPortTile = this.findNearestAvailablePortTile(this.warship);
|
|
|
|
|
return this.retreatPortTile !== undefined;
|
|
|
|
|
const newPort = this.findNearestAvailablePortTile();
|
|
|
|
|
this.warship.updateWarshipState({
|
|
|
|
|
retreatPort: newPort,
|
|
|
|
|
});
|
|
|
|
|
return newPort !== undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if current port is now full of healing (not counting arrived warships)
|
|
|
|
|
const currentPort = ports.find((p) => p.tile() === this.retreatPortTile);
|
|
|
|
|
const currentPort = ports.find((p) => p.tile() === currentRetreatPort);
|
|
|
|
|
if (currentPort && this.isPortFullOfHealing(currentPort)) {
|
|
|
|
|
// Current port is at healing capacity, look for alternatives
|
|
|
|
|
const alternativePort = this.findNearestAvailablePortTile();
|
|
|
|
|
const alternativePort = this.findNearestAvailablePort();
|
|
|
|
|
if (alternativePort) {
|
|
|
|
|
this.retreatPortTile = alternativePort;
|
|
|
|
|
this.warship.updateWarshipState({
|
|
|
|
|
retreatPort: alternativePort,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return this.retreatPortTile !== undefined;
|
|
|
|
|
return this.warship.warshipState().retreatPort !== undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if a significantly closer port is available
|
|
|
|
|
const closerPort = this.findBetterPortTile();
|
|
|
|
|
if (closerPort && closerPort !== this.retreatPortTile) {
|
|
|
|
|
this.retreatPortTile = closerPort;
|
|
|
|
|
if (closerPort && closerPort !== currentRetreatPort) {
|
|
|
|
|
this.warship.updateWarshipState({
|
|
|
|
|
retreatPort: closerPort,
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -331,7 +493,7 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
.filter(({ unit: ship }) => {
|
|
|
|
|
if (excludeShip && ship === excludeShip) return false;
|
|
|
|
|
if (ship.owner() !== owner) return false;
|
|
|
|
|
if (!ship.retreating()) return false;
|
|
|
|
|
if (ship.warshipState().state === "patrolling") return false;
|
|
|
|
|
if (ship.targetTile() !== undefined) return false;
|
|
|
|
|
return true;
|
|
|
|
|
})
|
|
|
|
@@ -345,6 +507,9 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dockedShips = this.dockedShipsAtPort(dockedPort);
|
|
|
|
|
if (!dockedShips.some((ship) => ship === this.warship)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const healingPool =
|
|
|
|
|
dockedPort.level() * this.mg.config().warshipPortHealingBonusPerLevel();
|
|
|
|
@@ -365,43 +530,21 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private currentRetreatPort(): Unit | undefined {
|
|
|
|
|
if (this.retreatPortTile === undefined) {
|
|
|
|
|
const retreatPort = this.warship.warshipState().retreatPort;
|
|
|
|
|
if (retreatPort === undefined) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.warship
|
|
|
|
|
.owner()
|
|
|
|
|
.units(UnitType.Port)
|
|
|
|
|
.find((port) => port.tile() === this.retreatPortTile);
|
|
|
|
|
.find((port) => port.tile() === retreatPort);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findBetterPortTile(): TileRef | undefined {
|
|
|
|
|
const warshipTile = this.warship.tile();
|
|
|
|
|
const currentDistance = this.retreatPortTile
|
|
|
|
|
? this.mg.euclideanDistSquared(warshipTile, this.retreatPortTile)
|
|
|
|
|
: Infinity;
|
|
|
|
|
const bestTile = this.findNearestAvailablePortTile(this.warship);
|
|
|
|
|
if (!bestTile) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
const bestDistance = this.mg.euclideanDistSquared(warshipTile, bestTile);
|
|
|
|
|
if (
|
|
|
|
|
bestDistance <
|
|
|
|
|
currentDistance * this.mg.config().warshipPortSwitchThreshold()
|
|
|
|
|
) {
|
|
|
|
|
return bestTile;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findNearestAvailablePortTile(
|
|
|
|
|
private nearestAvailablePortTile(
|
|
|
|
|
excludeShip?: Unit,
|
|
|
|
|
): TileRef | undefined {
|
|
|
|
|
): { tile: TileRef; distSquared: number } | undefined {
|
|
|
|
|
const ports = this.warship.owner().units(UnitType.Port);
|
|
|
|
|
if (ports.length === 0) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const warshipTile = this.warship.tile();
|
|
|
|
|
const warshipComponent = this.mg.getWaterComponent(warshipTile);
|
|
|
|
|
if (warshipComponent === null) {
|
|
|
|
@@ -410,6 +553,7 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
|
|
|
|
|
let bestTile: TileRef | undefined = undefined;
|
|
|
|
|
let bestDistance = Infinity;
|
|
|
|
|
|
|
|
|
|
for (const port of ports) {
|
|
|
|
|
if (this.isPortFullOfHealing(port, excludeShip)) {
|
|
|
|
|
continue;
|
|
|
|
@@ -427,99 +571,43 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bestTile;
|
|
|
|
|
return bestTile !== undefined
|
|
|
|
|
? { tile: bestTile, distSquared: bestDistance }
|
|
|
|
|
: undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findTargetUnit(): Unit | undefined {
|
|
|
|
|
const mg = this.mg;
|
|
|
|
|
const config = mg.config();
|
|
|
|
|
const owner = this.warship.owner();
|
|
|
|
|
const hasPort = owner.unitCount(UnitType.Port) > 0;
|
|
|
|
|
const patrolTile = this.warship.patrolTile()!;
|
|
|
|
|
const patrolRangeSquared = config.warshipPatrolRange() ** 2;
|
|
|
|
|
private findNearestAvailablePort(): TileRef | undefined {
|
|
|
|
|
return this.nearestAvailablePortTile()?.tile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Lazy: only computed if a TradeShip candidate forces the component check.
|
|
|
|
|
// `undefined` = not yet computed; `null` = computed, no component found.
|
|
|
|
|
let warshipComponent: number | null | undefined = undefined;
|
|
|
|
|
private findBetterPortTile(): TileRef | undefined {
|
|
|
|
|
const result = this.nearestAvailablePortTile();
|
|
|
|
|
if (!result) return undefined;
|
|
|
|
|
|
|
|
|
|
const ships = mg.nearbyUnits(
|
|
|
|
|
this.warship.tile()!,
|
|
|
|
|
config.warshipTargettingRange(),
|
|
|
|
|
[UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let bestUnit: Unit | undefined = undefined;
|
|
|
|
|
let bestTypePriority = 0;
|
|
|
|
|
let bestDistSquared = 0;
|
|
|
|
|
|
|
|
|
|
for (const { unit, distSquared } of ships) {
|
|
|
|
|
if (
|
|
|
|
|
unit.owner() === owner ||
|
|
|
|
|
unit === this.warship ||
|
|
|
|
|
!owner.canAttackPlayer(unit.owner(), true) ||
|
|
|
|
|
this.alreadySentShell.has(unit)
|
|
|
|
|
) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const type = unit.type();
|
|
|
|
|
if (type === UnitType.TradeShip) {
|
|
|
|
|
if (
|
|
|
|
|
!hasPort ||
|
|
|
|
|
unit.isSafeFromPirates() ||
|
|
|
|
|
unit.targetUnit()?.owner() === owner || // trade ship is coming to my port
|
|
|
|
|
unit.targetUnit()?.owner().isFriendly(owner) // trade ship is coming to my ally
|
|
|
|
|
) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (warshipComponent === undefined) {
|
|
|
|
|
warshipComponent = mg.getWaterComponent(this.warship.tile());
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
warshipComponent !== null &&
|
|
|
|
|
!mg.hasWaterComponent(unit.tile(), warshipComponent)
|
|
|
|
|
) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
mg.euclideanDistSquared(patrolTile, unit.tile()) > patrolRangeSquared
|
|
|
|
|
) {
|
|
|
|
|
// Prevent warship from chasing trade ship that is too far away from
|
|
|
|
|
// the patrol tile to prevent warships from wandering around the map.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const typePriority =
|
|
|
|
|
type === UnitType.TransportShip ? 0 : type === UnitType.Warship ? 1 : 2;
|
|
|
|
|
|
|
|
|
|
if (bestUnit === undefined) {
|
|
|
|
|
bestUnit = unit;
|
|
|
|
|
bestTypePriority = typePriority;
|
|
|
|
|
bestDistSquared = distSquared;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Match existing `sort()` semantics:
|
|
|
|
|
// - Lower priority is better (TransportShip < Warship < TradeShip).
|
|
|
|
|
// - For same type, smaller distance is better.
|
|
|
|
|
// - For exact ties, keep the first encountered (stable sort behavior).
|
|
|
|
|
if (
|
|
|
|
|
typePriority < bestTypePriority ||
|
|
|
|
|
(typePriority === bestTypePriority && distSquared < bestDistSquared)
|
|
|
|
|
) {
|
|
|
|
|
bestUnit = unit;
|
|
|
|
|
bestTypePriority = typePriority;
|
|
|
|
|
bestDistSquared = distSquared;
|
|
|
|
|
}
|
|
|
|
|
let currentDistance = Infinity;
|
|
|
|
|
const currentRetreatPort = this.warship.warshipState().retreatPort;
|
|
|
|
|
if (currentRetreatPort !== undefined) {
|
|
|
|
|
currentDistance = this.mg.euclideanDistSquared(
|
|
|
|
|
this.warship.tile(),
|
|
|
|
|
currentRetreatPort,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bestUnit;
|
|
|
|
|
if (
|
|
|
|
|
result.distSquared <
|
|
|
|
|
currentDistance * this.mg.config().warshipPortSwitchThreshold()
|
|
|
|
|
) {
|
|
|
|
|
return result.tile;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findNearestAvailablePortTile(): TileRef | undefined {
|
|
|
|
|
return this.nearestAvailablePortTile(this.warship)?.tile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private shootTarget() {
|
|
|
|
|
this.warship.updateWarshipState({ isInCombat: true });
|
|
|
|
|
const shellAttackRate = this.mg.config().warshipShellAttackRate();
|
|
|
|
|
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
|
|
|
|
|
if (this.warship.targetUnit()?.type() !== UnitType.TransportShip) {
|
|
|
|
@@ -544,6 +632,7 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private huntDownTradeShip() {
|
|
|
|
|
this.warship.updateWarshipState({ isInCombat: true });
|
|
|
|
|
for (let i = 0; i < 2; i++) {
|
|
|
|
|
// target is trade ship so capture it.
|
|
|
|
|
const result = this.pathfinder.next(
|
|
|
|
@@ -601,7 +690,7 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isDocked(): boolean {
|
|
|
|
|
return this.docked;
|
|
|
|
|
return (this.warship?.warshipState().state ?? "patrolling") === "docked";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
activeDuringSpawnPhase(): boolean {
|
|
|
|
@@ -617,7 +706,7 @@ export class WarshipExecution implements Execution {
|
|
|
|
|
// Get warship's water component for connectivity check
|
|
|
|
|
const warshipComponent = this.mg.getWaterComponent(this.warship.tile());
|
|
|
|
|
|
|
|
|
|
const patrolTile = this.warship.patrolTile();
|
|
|
|
|
const patrolTile = this.warship.warshipState().patrolTile;
|
|
|
|
|
if (patrolTile === undefined) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|