mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-18 00:28:14 +00:00
2661 PR 3/3 Warship Manual Override, Aggro Override, and Heal-at-Port Command (#3501)
Part of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661) (split into 3 PRs so they are not too large..) ## Description: Part 3/3 of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661). This PR adds the retreat control and override behavior for warships: - Manual override: moving a warship manually cancels retreat and suppresses auto-retreat for 5 seconds - Aggro override: a retreating warship will aggro a nearby enemy transport or warship before continuing retreat - Heal-at-port command for sending a warship to a friendly port manually - Friendly-port validation for HealAtPortExecution - Regression tests for manual override, aggro override, and heal-at-port 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._ --------- Co-authored-by: iamlewis <lewismmmm@gmail.com> Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
co-authored by
iamlewis
evanpelle
parent
f1d0136a06
commit
742a544a69
+310
-9
@@ -174,7 +174,7 @@ describe("Warship", () => {
|
||||
|
||||
executeTicks(game, 10);
|
||||
|
||||
expect(warship.patrolTile()).toBe(game.ref(coastX + 5, 15));
|
||||
expect(warship.warshipState().patrolTile).toBe(game.ref(coastX + 5, 15));
|
||||
});
|
||||
|
||||
test("Warship does not not target trade ships outside of patrol range", async () => {
|
||||
@@ -283,7 +283,7 @@ describe("Warship", () => {
|
||||
[warship.id()],
|
||||
game.ref(coastX + 5, 15),
|
||||
).init(game, 0);
|
||||
expect(warship.patrolTile()).toBe(originalPatrolTile);
|
||||
expect(warship.warshipState().patrolTile).toBe(originalPatrolTile);
|
||||
});
|
||||
|
||||
test("MoveWarshipExecution fails if warship is not active", async () => {
|
||||
@@ -301,7 +301,7 @@ describe("Warship", () => {
|
||||
[warship.id()],
|
||||
game.ref(coastX + 5, 15),
|
||||
).init(game, 0);
|
||||
expect(warship.patrolTile()).toBe(originalPatrolTile);
|
||||
expect(warship.warshipState().patrolTile).toBe(originalPatrolTile);
|
||||
});
|
||||
|
||||
test("MoveWarshipExecution fails gracefully if warship not found", async () => {
|
||||
@@ -346,7 +346,7 @@ describe("Warship", () => {
|
||||
|
||||
game.executeNextTick();
|
||||
|
||||
expect(warship.retreating()).toBe(true);
|
||||
expect(warship.warshipState().state).not.toBe("patrolling");
|
||||
const distanceToPort = game.euclideanDistSquared(
|
||||
warship.tile(),
|
||||
homePort.tile(),
|
||||
@@ -432,7 +432,7 @@ describe("Warship", () => {
|
||||
exec1.isDocked() &&
|
||||
!exec2.isDocked() &&
|
||||
warship2DistanceToPort <= 25 &&
|
||||
warship2.retreating()
|
||||
warship2.warshipState().state !== "patrolling"
|
||||
) {
|
||||
break;
|
||||
}
|
||||
@@ -445,7 +445,7 @@ describe("Warship", () => {
|
||||
expect(exec1.isDocked()).toBe(true);
|
||||
expect(exec2.isDocked()).toBe(false);
|
||||
expect(warship2DistanceToPort).toBeLessThanOrEqual(25);
|
||||
expect(warship2.retreating()).toBe(true);
|
||||
expect(warship2.warshipState().state).not.toBe("patrolling");
|
||||
});
|
||||
|
||||
test("Warship cancels docking if its retreat port is destroyed", async () => {
|
||||
@@ -481,7 +481,7 @@ describe("Warship", () => {
|
||||
game.executeNextTick();
|
||||
|
||||
expect(warshipExecution.isDocked()).toBe(false);
|
||||
expect(warship.retreating()).toBe(false);
|
||||
expect(warship.warshipState().state).toBe("patrolling");
|
||||
});
|
||||
|
||||
test("Warship drops a stale target after patrol movement changes range", async () => {
|
||||
@@ -521,8 +521,9 @@ describe("Warship", () => {
|
||||
});
|
||||
|
||||
execution.tick(game.ticks());
|
||||
|
||||
expect(warship.tile()).toBe(movedTile);
|
||||
|
||||
execution.tick(game.ticks());
|
||||
expect(warship.targetUnit()).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -549,6 +550,306 @@ describe("Warship", () => {
|
||||
warship.modifyHealth(-300);
|
||||
game.executeNextTick();
|
||||
|
||||
expect(warship.retreating()).toBe(false);
|
||||
expect(warship.warshipState().state).toBe("patrolling");
|
||||
});
|
||||
|
||||
test("Low-health warship retreats AND fires at nearby enemy warship", async () => {
|
||||
game.config().warshipPortHealingBonusPerLevel = () => 0;
|
||||
game.config().warshipRetreatHealthThreshold = () => 600;
|
||||
game.config().warshipTargettingRange = () => 5;
|
||||
game.config().warshipShellAttackRate = () => 10_000;
|
||||
|
||||
player1.buildUnit(UnitType.Port, game.ref(coastX, 5), {});
|
||||
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 15),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 15),
|
||||
},
|
||||
);
|
||||
const enemyWarship = player2.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 2, 15),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 2, 15),
|
||||
},
|
||||
);
|
||||
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
game.addExecution(new WarshipExecution(enemyWarship));
|
||||
|
||||
game.executeNextTick();
|
||||
warship.modifyHealth(-700);
|
||||
game.executeNextTick();
|
||||
|
||||
// New behavior: retreat starts immediately even with enemy nearby
|
||||
expect(warship.warshipState().state).not.toBe("patrolling");
|
||||
// AND the warship still targets the enemy to fire back while retreating
|
||||
expect(warship.targetUnit()).toBe(enemyWarship);
|
||||
});
|
||||
|
||||
test("Retreating warship aggroes nearby enemy transport before continuing retreat", async () => {
|
||||
game.config().warshipPortHealingBonusPerLevel = () => 0;
|
||||
game.config().warshipRetreatHealthThreshold = () => 600;
|
||||
game.config().warshipTargettingRange = () => 5;
|
||||
game.config().warshipShellAttackRate = () => 10_000;
|
||||
|
||||
const homePort = player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 6, 12),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 6, 12),
|
||||
},
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
game.executeNextTick();
|
||||
warship.modifyHealth(-700);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
game.executeNextTick();
|
||||
if (
|
||||
warship.warshipState().state !== "patrolling" &&
|
||||
warship.targetTile() === homePort.tile() &&
|
||||
warship.tile() !== homePort.tile()
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(warship.warshipState().state).not.toBe("patrolling");
|
||||
expect(warship.targetTile()).toBe(homePort.tile());
|
||||
|
||||
const enemyTransport = player2.buildUnit(
|
||||
UnitType.TransportShip,
|
||||
game.ref(coastX + 5, 12),
|
||||
{
|
||||
targetTile: game.ref(coastX + 5, 12),
|
||||
},
|
||||
);
|
||||
|
||||
game.executeNextTick();
|
||||
|
||||
expect(warship.warshipState().state).not.toBe("patrolling");
|
||||
expect(warship.targetTile()).toBe(homePort.tile());
|
||||
expect(warship.targetUnit()).toBe(enemyTransport);
|
||||
});
|
||||
|
||||
test("Manual MoveWarshipExecution cancels retreat and keeps manual order", async () => {
|
||||
game.config().warshipPortHealingBonusPerLevel = () => 0;
|
||||
game.config().warshipRetreatHealthThreshold = () => 600;
|
||||
|
||||
const homePortTile = game.ref(coastX, 10);
|
||||
player1.buildUnit(UnitType.Port, homePortTile, {});
|
||||
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 11),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 11),
|
||||
},
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
game.executeNextTick();
|
||||
warship.modifyHealth(-700);
|
||||
executeTicks(game, 20);
|
||||
|
||||
expect(warship.warshipState().state).not.toBe("patrolling");
|
||||
|
||||
const manualPatrolTile = game.ref(coastX + 5, 15);
|
||||
game.addExecution(
|
||||
new MoveWarshipExecution(player1, [warship.id()], manualPatrolTile),
|
||||
);
|
||||
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(warship.warshipState().state).toBe("patrolling");
|
||||
expect(warship.warshipState().patrolTile).toBe(manualPatrolTile);
|
||||
expect(warship.targetTile()).not.toBe(homePortTile);
|
||||
});
|
||||
|
||||
test("Manual MoveWarshipExecution suppresses auto-retreat for 5 seconds before retreat starts", async () => {
|
||||
game.config().warshipPortHealingBonusPerLevel = () => 0;
|
||||
game.config().warshipRetreatHealthThreshold = () => 600;
|
||||
|
||||
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));
|
||||
game.executeNextTick();
|
||||
|
||||
const manualPatrolTile = game.ref(coastX + 6, 15);
|
||||
game.addExecution(
|
||||
new MoveWarshipExecution(player1, [warship.id()], manualPatrolTile),
|
||||
);
|
||||
game.executeNextTick();
|
||||
|
||||
warship.modifyHealth(-700);
|
||||
|
||||
game.executeNextTick();
|
||||
expect(warship.warshipState().state).toBe("patrolling");
|
||||
expect(warship.warshipState().patrolTile).toBe(manualPatrolTile);
|
||||
|
||||
executeTicks(game, 48);
|
||||
expect(warship.warshipState().state).toBe("patrolling");
|
||||
|
||||
let resumedRetreat = false;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
if (warship.warshipState().state !== "patrolling") {
|
||||
resumedRetreat = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(resumedRetreat).toBe(true);
|
||||
});
|
||||
|
||||
test("Warship isInCombat becomes true when hit by a shell from an enemy", async () => {
|
||||
game.config().warshipPassiveHealing = () => 0;
|
||||
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 10),
|
||||
{ patrolTile: game.ref(coastX + 1, 10) },
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
game.executeNextTick();
|
||||
|
||||
expect(warship.warshipState().isInCombat).toBe(false);
|
||||
|
||||
// Simulate incoming shell damage from an enemy player
|
||||
warship.modifyHealth(-50, player2);
|
||||
|
||||
expect(warship.warshipState().isInCombat).toBe(true);
|
||||
});
|
||||
|
||||
test("Warship isInCombat becomes true when firing at an enemy", async () => {
|
||||
game.config().warshipPortHealingBonusPerLevel = () => 0;
|
||||
game.config().warshipShellAttackRate = () => 0;
|
||||
game.config().warshipTargettingRange = () => 5;
|
||||
|
||||
player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 10),
|
||||
{ patrolTile: game.ref(coastX + 1, 10) },
|
||||
);
|
||||
const enemyWarship = player2.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 2, 10),
|
||||
{ patrolTile: game.ref(coastX + 2, 10) },
|
||||
);
|
||||
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
game.executeNextTick();
|
||||
|
||||
expect(warship.warshipState().isInCombat).toBe(false);
|
||||
|
||||
// Give warship a target and tick — shootTarget sets inCombat
|
||||
warship.setTargetUnit(enemyWarship);
|
||||
game.executeNextTick();
|
||||
|
||||
expect(warship.warshipState().isInCombat).toBe(true);
|
||||
});
|
||||
|
||||
test("Docked warship is not targeted by enemy warship", async () => {
|
||||
game.config().warshipPassiveHealing = () => 0;
|
||||
game.config().warshipDockingRange = () => 5;
|
||||
game.config().warshipRetreatHealthThreshold = () => 900;
|
||||
game.config().warshipTargettingRange = () => 20;
|
||||
|
||||
const portTile = game.ref(coastX, 10);
|
||||
player1.buildUnit(UnitType.Port, portTile, {});
|
||||
const friendlyWarship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 10),
|
||||
{ patrolTile: game.ref(coastX + 1, 10) },
|
||||
);
|
||||
const exec = new WarshipExecution(friendlyWarship);
|
||||
game.addExecution(exec);
|
||||
|
||||
const enemyWarship = player2.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 2, 10),
|
||||
{ patrolTile: game.ref(coastX + 2, 10) },
|
||||
);
|
||||
const enemyExec = new WarshipExecution(enemyWarship);
|
||||
game.addExecution(enemyExec);
|
||||
|
||||
game.executeNextTick();
|
||||
friendlyWarship.modifyHealth(-300);
|
||||
|
||||
// Wait until friendly warship docks
|
||||
for (let i = 0; i < 80; i++) {
|
||||
game.executeNextTick();
|
||||
if (exec.isDocked()) break;
|
||||
}
|
||||
|
||||
expect(exec.isDocked()).toBe(true);
|
||||
expect(friendlyWarship.warshipState().state).toBe("docked");
|
||||
|
||||
// Enemy warship should not be targeting the docked warship
|
||||
game.executeNextTick();
|
||||
expect(enemyWarship.targetUnit()).not.toBe(friendlyWarship);
|
||||
});
|
||||
|
||||
test("Retreating warship continues moving to port after firing back", async () => {
|
||||
game.config().warshipPortHealingBonusPerLevel = () => 0;
|
||||
game.config().warshipRetreatHealthThreshold = () => 600;
|
||||
game.config().warshipTargettingRange = () => 5;
|
||||
game.config().warshipShellAttackRate = () => 10_000;
|
||||
|
||||
const homePort = player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 6, 12),
|
||||
{ patrolTile: game.ref(coastX + 6, 12) },
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
game.executeNextTick();
|
||||
warship.modifyHealth(-700);
|
||||
|
||||
// Wait until retreating and heading to port
|
||||
for (let i = 0; i < 15; i++) {
|
||||
game.executeNextTick();
|
||||
if (
|
||||
warship.warshipState().state !== "patrolling" &&
|
||||
warship.targetTile() === homePort.tile()
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(warship.warshipState().state).not.toBe("patrolling");
|
||||
expect(warship.targetTile()).toBe(homePort.tile());
|
||||
|
||||
const tileBeforeCombat = warship.tile();
|
||||
|
||||
const enemyTransport = player2.buildUnit(
|
||||
UnitType.TransportShip,
|
||||
game.ref(coastX + 5, 12),
|
||||
{ targetTile: game.ref(coastX + 5, 12) },
|
||||
);
|
||||
|
||||
// After encountering enemy: still retreating, still targeting port,
|
||||
// AND targeting the enemy transport simultaneously
|
||||
game.executeNextTick();
|
||||
|
||||
expect(warship.warshipState().state).not.toBe("patrolling");
|
||||
expect(warship.targetTile()).toBe(homePort.tile());
|
||||
expect(warship.targetUnit()).toBe(enemyTransport);
|
||||
|
||||
// Warship should still be moving (not frozen at tileBeforeCombat)
|
||||
game.executeNextTick();
|
||||
expect(warship.tile()).not.toBe(tileBeforeCombat);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user