mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:50:43 +00:00
2661 PR 2/3 Warship Port Healing, Docking Capacity, and Waiting Behavior (#3499)
Part of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661) (split into 3 PRs so they are not too large..) ## Description: Part 2/3 of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661). This PR adds port-based healing and docking behavior: - Passive healing near friendly ports - Active docked healing pool scaled by port level and shared across docked ships - Docking radius and capacity-by-port-level behavior - Waiting behavior near full ports until a slot opens - Auto-undock once fully healed For the active healing, it works like `ActiveHeal = (PortLevel * 5) / DockedShipsAtThatPort` Ex: 1 ship at level 1 port -> +5 HP/tick 1 ship at level 2 port → +10 HP/tick 2 ships at level 3 port → +7.5 HP/tick each Includes regression tests covering healing math and docking/waiting behavior. ## 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: zixer._
This commit is contained in:
@@ -153,7 +153,12 @@ export interface Config {
|
||||
warshipPatrolRange(): number;
|
||||
warshipShellAttackRate(): number;
|
||||
warshipTargettingRange(): number;
|
||||
warshipDockingRange(): number;
|
||||
warshipPortHealingBonusPerLevel(): number;
|
||||
warshipRetreatHealthThreshold(): number;
|
||||
warshipPassiveHealing(): number;
|
||||
warshipPassiveHealingRange(): number;
|
||||
warshipPortSwitchThreshold(): number;
|
||||
defensePostShellAttackRate(): number;
|
||||
defensePostTargettingRange(): number;
|
||||
// 0-1
|
||||
|
||||
@@ -969,10 +969,30 @@ export class DefaultConfig implements Config {
|
||||
return 20;
|
||||
}
|
||||
|
||||
warshipDockingRange(): number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
warshipPortHealingBonusPerLevel(): number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
warshipRetreatHealthThreshold(): number {
|
||||
return 750;
|
||||
}
|
||||
|
||||
warshipPassiveHealing(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
warshipPassiveHealingRange(): number {
|
||||
return 150;
|
||||
}
|
||||
|
||||
warshipPortSwitchThreshold(): number {
|
||||
return 0.75;
|
||||
}
|
||||
|
||||
defensePostShellAttackRate(): number {
|
||||
return 100;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export class WarshipExecution implements Execution {
|
||||
private alreadySentShell = new Set<Unit>();
|
||||
private retreatPortTile: TileRef | undefined;
|
||||
private retreatingForRepair = false;
|
||||
private docked = false;
|
||||
private activeHealingRemainder = 0;
|
||||
|
||||
constructor(
|
||||
private input: (UnitParams<UnitType.Warship> & OwnerComp) | Unit,
|
||||
@@ -62,6 +64,20 @@ export class WarshipExecution implements Execution {
|
||||
|
||||
this.healWarship();
|
||||
|
||||
if (this.docked) {
|
||||
if (this.currentRetreatPort() === undefined) {
|
||||
this.docked = false;
|
||||
this.cancelRepairRetreat();
|
||||
}
|
||||
if (this.isFullyHealed()) {
|
||||
this.docked = false;
|
||||
this.cancelRepairRetreat();
|
||||
}
|
||||
if (this.docked) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.handleRepairRetreat()) {
|
||||
return;
|
||||
}
|
||||
@@ -79,6 +95,9 @@ export class WarshipExecution implements Execution {
|
||||
// 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();
|
||||
@@ -99,14 +118,28 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
private healWarship(): void {
|
||||
if (this.warship.owner().unitCount(UnitType.Port) > 0) {
|
||||
this.warship.modifyHealth(1);
|
||||
const owner = this.warship.owner();
|
||||
const passiveHealing = this.mg.config().warshipPassiveHealing();
|
||||
const passiveHealingRange = this.mg.config().warshipPassiveHealingRange();
|
||||
const warshipTile = this.warship.tile();
|
||||
|
||||
const isNearPort = this.mg
|
||||
.nearbyUnits(warshipTile, passiveHealingRange, [UnitType.Port])
|
||||
.some(({ unit }) => unit.owner() === owner);
|
||||
|
||||
if (isNearPort) {
|
||||
this.warship.modifyHealth(passiveHealing);
|
||||
}
|
||||
|
||||
if (this.docked) {
|
||||
this.applyActiveDockedHealing();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -162,12 +195,15 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
this.retreatingForRepair = true;
|
||||
this.retreatPortTile = portTile;
|
||||
this.docked = false;
|
||||
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;
|
||||
if (clearTargetTile) {
|
||||
@@ -185,6 +221,11 @@ export class WarshipExecution implements Execution {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.refreshRetreatPortTile()) {
|
||||
this.cancelRepairRetreat();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.warship.setTargetUnit(undefined);
|
||||
|
||||
const retreatPortTile = this.retreatPortTile;
|
||||
@@ -192,9 +233,28 @@ export class WarshipExecution implements Execution {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.warship.tile() === retreatPortTile) {
|
||||
this.warship.setTargetTile(undefined);
|
||||
return true;
|
||||
const dockingRadius = this.mg.config().warshipDockingRange();
|
||||
const dockingRadiusSq = dockingRadius * dockingRadius;
|
||||
const distToPort = this.mg.euclideanDistSquared(
|
||||
this.warship.tile(),
|
||||
retreatPortTile,
|
||||
);
|
||||
|
||||
if (distToPort <= dockingRadiusSq) {
|
||||
// Check if the port has capacity available (excluding this warship from capacity check)
|
||||
const port = this.warship
|
||||
.owner()
|
||||
.units(UnitType.Port)
|
||||
.find((p) => p.tile() === retreatPortTile);
|
||||
if (port && !this.isPortFullOfHealing(port, this.warship)) {
|
||||
// Port has capacity - dock here
|
||||
this.warship.setTargetTile(undefined);
|
||||
this.docked = true;
|
||||
return true;
|
||||
} else {
|
||||
// Port is full - don't cancel retreat, keep waiting near port
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.warship.setTargetTile(retreatPortTile);
|
||||
@@ -210,7 +270,7 @@ export class WarshipExecution implements Execution {
|
||||
this.warship.move(result.node);
|
||||
break;
|
||||
case PathStatus.NOT_FOUND:
|
||||
this.retreatPortTile = this.findNearestPort();
|
||||
this.retreatPortTile = this.findNearestAvailablePortTile(this.warship);
|
||||
if (this.retreatPortTile === undefined) {
|
||||
this.cancelRepairRetreat();
|
||||
}
|
||||
@@ -220,6 +280,156 @@ export class WarshipExecution implements Execution {
|
||||
return true;
|
||||
}
|
||||
|
||||
private refreshRetreatPortTile(): boolean {
|
||||
const ports = this.warship.owner().units(UnitType.Port);
|
||||
if (ports.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if current retreat port still exists
|
||||
const currentPortExists =
|
||||
this.retreatPortTile !== undefined &&
|
||||
ports.some((port) => port.tile() === this.retreatPortTile);
|
||||
|
||||
if (!currentPortExists) {
|
||||
this.retreatPortTile = this.findNearestAvailablePortTile(this.warship);
|
||||
return this.retreatPortTile !== undefined;
|
||||
}
|
||||
|
||||
// Check if current port is now full of healing (not counting arrived warships)
|
||||
const currentPort = ports.find((p) => p.tile() === this.retreatPortTile);
|
||||
if (currentPort && this.isPortFullOfHealing(currentPort)) {
|
||||
// Current port is at healing capacity, look for alternatives
|
||||
const alternativePort = this.findNearestAvailablePortTile();
|
||||
if (alternativePort) {
|
||||
this.retreatPortTile = alternativePort;
|
||||
}
|
||||
return this.retreatPortTile !== undefined;
|
||||
}
|
||||
|
||||
// Check if a significantly closer port is available
|
||||
const closerPort = this.findBetterPortTile();
|
||||
if (closerPort && closerPort !== this.retreatPortTile) {
|
||||
this.retreatPortTile = closerPort;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private isPortFullOfHealing(port: Unit, excludeShip?: Unit): boolean {
|
||||
const maxShipsHealing = port.level();
|
||||
return this.dockedShipsAtPort(port, excludeShip).length >= maxShipsHealing;
|
||||
}
|
||||
|
||||
private dockedShipsAtPort(port: Unit, excludeShip?: Unit): Unit[] {
|
||||
const dockingRadius = this.mg.config().warshipDockingRange();
|
||||
const owner = this.warship.owner();
|
||||
|
||||
return this.mg
|
||||
.nearbyUnits(port.tile(), dockingRadius, [UnitType.Warship])
|
||||
.filter(({ unit: ship }) => {
|
||||
if (excludeShip && ship === excludeShip) return false;
|
||||
if (ship.owner() !== owner) return false;
|
||||
if (!ship.retreating()) return false;
|
||||
if (ship.targetTile() !== undefined) return false;
|
||||
return true;
|
||||
})
|
||||
.map(({ unit }) => unit);
|
||||
}
|
||||
|
||||
private applyActiveDockedHealing(): void {
|
||||
const dockedPort = this.currentRetreatPort();
|
||||
if (!dockedPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dockedShips = this.dockedShipsAtPort(dockedPort);
|
||||
|
||||
const healingPool =
|
||||
dockedPort.level() * this.mg.config().warshipPortHealingBonusPerLevel();
|
||||
if (healingPool <= 0 || dockedShips.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Preserve fractional split healing over time with a per-ship remainder.
|
||||
const activeHealing = healingPool / dockedShips.length;
|
||||
this.activeHealingRemainder += activeHealing;
|
||||
const integerHealing = Math.floor(this.activeHealingRemainder);
|
||||
if (integerHealing <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeHealingRemainder -= integerHealing;
|
||||
this.warship.modifyHealth(integerHealing);
|
||||
}
|
||||
|
||||
private currentRetreatPort(): Unit | undefined {
|
||||
if (this.retreatPortTile === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.warship
|
||||
.owner()
|
||||
.units(UnitType.Port)
|
||||
.find((port) => port.tile() === this.retreatPortTile);
|
||||
}
|
||||
|
||||
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(
|
||||
excludeShip?: Unit,
|
||||
): TileRef | 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) {
|
||||
throw new Error(`Warship at tile ${warshipTile} has no water component`);
|
||||
}
|
||||
|
||||
let bestTile: TileRef | undefined = undefined;
|
||||
let bestDistance = Infinity;
|
||||
for (const port of ports) {
|
||||
if (this.isPortFullOfHealing(port, excludeShip)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const portTile = port.tile();
|
||||
if (!this.mg.hasWaterComponent(portTile, warshipComponent)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const distance = this.mg.euclideanDistSquared(warshipTile, portTile);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
bestTile = portTile;
|
||||
}
|
||||
}
|
||||
|
||||
return bestTile;
|
||||
}
|
||||
|
||||
private findTargetUnit(): Unit | undefined {
|
||||
const mg = this.mg;
|
||||
const config = mg.config();
|
||||
@@ -390,6 +600,10 @@ export class WarshipExecution implements Execution {
|
||||
return this.warship?.isActive();
|
||||
}
|
||||
|
||||
isDocked(): boolean {
|
||||
return this.docked;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -221,11 +221,18 @@ export class UnitImpl implements Unit {
|
||||
}
|
||||
|
||||
modifyHealth(delta: number, attacker?: Player): void {
|
||||
this._health = withinInt(
|
||||
const previousHealth = this._health;
|
||||
const nextHealth = withinInt(
|
||||
this._health + toInt(delta),
|
||||
0n,
|
||||
toInt(this.info().maxHealth ?? 1),
|
||||
);
|
||||
|
||||
if (nextHealth === previousHealth) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._health = nextHealth;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
if (this._health === 0n) {
|
||||
this.delete(true, attacker);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
UnitType,
|
||||
} from "../src/core/game/Game";
|
||||
import { TileRef } from "../src/core/game/GameMap";
|
||||
import { PathStatus } from "../src/core/pathfinding/types";
|
||||
import { setup } from "./util/Setup";
|
||||
import { executeTicks } from "./util/utils";
|
||||
|
||||
@@ -327,6 +328,7 @@ describe("Warship", () => {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
game.config().warshipPortHealingBonusPerLevel = () => 0;
|
||||
game.config().warshipRetreatHealthThreshold = () => 600;
|
||||
|
||||
const homePort = player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
|
||||
@@ -353,4 +355,200 @@ describe("Warship", () => {
|
||||
distanceToPort <= 25 || warship.targetTile() === homePort.tile(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("Warship gets active healing when docked at a friendly port", async () => {
|
||||
const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth;
|
||||
if (typeof maxHealth !== "number") {
|
||||
expect(typeof maxHealth).toBe("number");
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
game.config().warshipPassiveHealing = () => 0;
|
||||
game.config().warshipPortHealingBonusPerLevel = () => 6;
|
||||
game.config().warshipDockingRange = () => 5;
|
||||
game.config().warshipRetreatHealthThreshold = () => 900;
|
||||
|
||||
const portTile = game.ref(coastX, 10);
|
||||
player1.buildUnit(UnitType.Port, portTile, {});
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 11),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 11),
|
||||
},
|
||||
);
|
||||
const warshipExecution = new WarshipExecution(warship);
|
||||
game.addExecution(warshipExecution);
|
||||
|
||||
game.executeNextTick();
|
||||
warship.modifyHealth(-300);
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
game.executeNextTick();
|
||||
if (warshipExecution.isDocked()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(warshipExecution.isDocked()).toBe(true);
|
||||
const before = warship.health();
|
||||
game.executeNextTick();
|
||||
expect(warship.health()).toBe(before + 6);
|
||||
});
|
||||
|
||||
test("Warship waits at port when capacity is full", async () => {
|
||||
game.config().warshipPassiveHealing = () => 0;
|
||||
game.config().warshipDockingRange = () => 5;
|
||||
game.config().warshipRetreatHealthThreshold = () => 900;
|
||||
|
||||
const portTile = game.ref(coastX, 10);
|
||||
const warship1Tile = game.ref(coastX + 1, 11);
|
||||
const warship2Tile = game.ref(coastX + 1, 12);
|
||||
|
||||
player1.buildUnit(UnitType.Port, portTile, {});
|
||||
const warship1 = player1.buildUnit(UnitType.Warship, warship1Tile, {
|
||||
patrolTile: warship1Tile,
|
||||
});
|
||||
const warship2 = player1.buildUnit(UnitType.Warship, warship2Tile, {
|
||||
patrolTile: warship2Tile,
|
||||
});
|
||||
|
||||
const exec1 = new WarshipExecution(warship1);
|
||||
const exec2 = new WarshipExecution(warship2);
|
||||
game.addExecution(exec1);
|
||||
game.addExecution(exec2);
|
||||
|
||||
game.executeNextTick();
|
||||
warship1.modifyHealth(-300);
|
||||
warship2.modifyHealth(-300);
|
||||
|
||||
for (let i = 0; i < 80; i++) {
|
||||
game.executeNextTick();
|
||||
const warship2DistanceToPort = game.euclideanDistSquared(
|
||||
warship2.tile(),
|
||||
portTile,
|
||||
);
|
||||
if (
|
||||
exec1.isDocked() &&
|
||||
!exec2.isDocked() &&
|
||||
warship2DistanceToPort <= 25 &&
|
||||
warship2.retreating()
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const warship2DistanceToPort = game.euclideanDistSquared(
|
||||
warship2.tile(),
|
||||
portTile,
|
||||
);
|
||||
expect(exec1.isDocked()).toBe(true);
|
||||
expect(exec2.isDocked()).toBe(false);
|
||||
expect(warship2DistanceToPort).toBeLessThanOrEqual(25);
|
||||
expect(warship2.retreating()).toBe(true);
|
||||
});
|
||||
|
||||
test("Warship cancels docking if its retreat port is destroyed", async () => {
|
||||
game.config().warshipPassiveHealing = () => 0;
|
||||
game.config().warshipPortHealingBonusPerLevel = () => 0;
|
||||
game.config().warshipDockingRange = () => 5;
|
||||
game.config().warshipRetreatHealthThreshold = () => 900;
|
||||
|
||||
const homePort = player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 11),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 11),
|
||||
},
|
||||
);
|
||||
const warshipExecution = new WarshipExecution(warship);
|
||||
game.addExecution(warshipExecution);
|
||||
|
||||
game.executeNextTick();
|
||||
warship.modifyHealth(-300);
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
game.executeNextTick();
|
||||
if (warshipExecution.isDocked()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(warshipExecution.isDocked()).toBe(true);
|
||||
|
||||
homePort.delete();
|
||||
game.executeNextTick();
|
||||
|
||||
expect(warshipExecution.isDocked()).toBe(false);
|
||||
expect(warship.retreating()).toBe(false);
|
||||
});
|
||||
|
||||
test("Warship drops a stale target after patrol movement changes range", async () => {
|
||||
game.config().warshipTargettingRange = () => 1;
|
||||
game.config().warshipShellAttackRate = () => Number.MAX_SAFE_INTEGER;
|
||||
const startTile = game.ref(coastX + 1, 10);
|
||||
const movedTile = game
|
||||
.map()
|
||||
.neighbors(startTile)
|
||||
.find((tile) => game.isOcean(tile));
|
||||
|
||||
expect(movedTile).toBeDefined();
|
||||
|
||||
const warship = player1.buildUnit(UnitType.Warship, startTile, {
|
||||
patrolTile: startTile,
|
||||
});
|
||||
warship.setTargetTile(movedTile!);
|
||||
const transport = player2.buildUnit(UnitType.TransportShip, movedTile!, {
|
||||
targetTile: movedTile!,
|
||||
});
|
||||
|
||||
const execution = new WarshipExecution(warship);
|
||||
const executionInternals = execution as unknown as {
|
||||
findTargetUnit: () => typeof transport | undefined;
|
||||
pathfinder: {
|
||||
next: () => { status: PathStatus; node: number };
|
||||
};
|
||||
};
|
||||
execution.init(game, game.ticks());
|
||||
|
||||
vi.spyOn(executionInternals, "findTargetUnit")
|
||||
.mockReturnValueOnce(transport)
|
||||
.mockReturnValueOnce(undefined);
|
||||
vi.spyOn(executionInternals.pathfinder, "next").mockReturnValue({
|
||||
status: PathStatus.NEXT,
|
||||
node: movedTile!,
|
||||
});
|
||||
|
||||
execution.tick(game.ticks());
|
||||
|
||||
expect(warship.tile()).toBe(movedTile);
|
||||
expect(warship.targetUnit()).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Warship cancels retreat if no friendly port is reachable by water", async () => {
|
||||
game.config().warshipRetreatHealthThreshold = () => 900;
|
||||
|
||||
player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 11),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 11),
|
||||
},
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
const warshipTile = warship.tile();
|
||||
vi.spyOn(game, "getWaterComponent").mockImplementation((tile) =>
|
||||
tile === warshipTile ? 1 : 2,
|
||||
);
|
||||
vi.spyOn(game, "hasWaterComponent").mockReturnValue(false);
|
||||
|
||||
game.executeNextTick();
|
||||
warship.modifyHealth(-300);
|
||||
game.executeNextTick();
|
||||
|
||||
expect(warship.retreating()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user