Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 60 additions & 21 deletions src/core/execution/TradeShipExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export class TradeShipExecution implements Execution {

const tradeShipOwner = this.tradeShip.owner();
const dstPortOwner = this._dstPort.owner();
const curTile = this.tradeShip.tile();

if (this.wasCaptured !== true && this.origOwner !== tradeShipOwner) {
// Store as variable in case ship is recaptured by previous owner
this.wasCaptured = true;
Expand All @@ -79,42 +81,51 @@ export class TradeShipExecution implements Execution {
return;
}

if (!this.tradeShip.isActive()) {
this.active = false;
return;
}

// Handle embargo or inactives ports destinations
if (
!this.wasCaptured &&
(!this._dstPort.isActive() || !tradeShipOwner.canTrade(dstPortOwner))
) {
this.tradeShip.delete(false);
this.active = false;
return;
}
const newPort = this.findNearestValidPort(
curTile,
(p) => p !== this.origOwner && p.canTrade(tradeShipOwner),
);
Comment thread
lnoss marked this conversation as resolved.

const curTile = this.tradeShip.tile();
if (newPort === null) {
this.tradeShip.delete(false);
this.active = false;
return;
}
this._dstPort = newPort;
this.tradeShip.setTargetUnit(this._dstPort);
this.tradeShip.touch();
this.motionPlanDst = null; // Force motion plan re-recording
}

// Handle captured ship that needs rerouting to nearest new owner port
if (
this.wasCaptured &&
(tradeShipOwner !== dstPortOwner || !this._dstPort.isActive())
) {
const myComponent = this.mg.getWaterComponent(curTile);
const nearestPort = findClosestBy(
tradeShipOwner.units(UnitType.Port),
(port) => this.mg.manhattanDist(port.tile(), curTile),
(port) =>
port.isActive() &&
!port.isMarkedForDeletion() &&
!port.isUnderConstruction() &&
myComponent !== null &&
this.mg.hasWaterComponent(port.tile(), myComponent),
const newPort = this.findNearestValidPort(
curTile,
(p) => p === tradeShipOwner,
);
if (nearestPort === null) {

if (newPort === null) {
this.tradeShip.delete(false);
this.active = false;
return;
} else {
this._dstPort = nearestPort;
this.tradeShip.setTargetUnit(this._dstPort);
// Plan-driven units don't emit per-tick unit updates, so force a sync for the new target.
this.tradeShip.touch();
}
this._dstPort = newPort;
this.tradeShip.setTargetUnit(this._dstPort);
this.tradeShip.touch();
this.motionPlanDst = null; // Force path recalc
}
Comment thread
lnoss marked this conversation as resolved.

if (curTile === this.dstPort()) {
Expand Down Expand Up @@ -230,4 +241,32 @@ export class TradeShipExecution implements Execution {
dstPort(): TileRef {
return this._dstPort.tile();
}

private findNearestValidPort(
curTile: TileRef,
playerFilter: (p: Player) => boolean,
): Unit | null {
const myComponent = this.mg.getWaterComponent(curTile);
if (myComponent === null) return null;

const eligiblePlayers = this.mg.players().filter(playerFilter);

const candidatePorts = eligiblePlayers.flatMap((p) =>
p.units(UnitType.Port),
);

const validPorts = candidatePorts.filter(
(port) =>
port.isActive() &&
!port.isMarkedForDeletion() &&
!port.isUnderConstruction() &&
this.mg.hasWaterComponent(port.tile(), myComponent),
);

if (validPorts.length === 0) return null;

return findClosestBy(validPorts, (port) =>
this.mg.manhattanDist(port.tile(), curTile),
);
}
}
56 changes: 54 additions & 2 deletions tests/core/executions/TradeShipExecution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ describe("TradeShipExecution", () => {
let origOwner: Player;
let dstOwner: Player;
let pirate: Player;
let halfPirate: Player;
let srcPort: Unit;
let piratePort: Unit;
let piratePort2: Unit;
let halfPiratePort: Unit;
let tradeShip: Unit;
let dstPort: Unit;
let tradeShipExecution: TradeShipExecution;
let currentTarget: Unit | null = null;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

beforeEach(async () => {
// Mock Game, Player, Unit, and required methods
Expand Down Expand Up @@ -48,12 +51,21 @@ describe("TradeShipExecution", () => {
pirate = {
id: vi.fn(() => 3),
addGold: vi.fn(),
displayName: vi.fn(() => "Destination"),
displayName: vi.fn(() => "Destination 1"),
units: vi.fn(() => [piratePort, piratePort2]),
unitCount: vi.fn(() => 2),
canTrade: vi.fn(() => true),
} as any;

halfPirate = {
id: vi.fn(() => 4),
addGold: vi.fn(),
displayName: vi.fn(() => "Destination 2"),
units: vi.fn(() => [halfPiratePort]),
unitCount: vi.fn(() => 1),
canTrade: vi.fn(() => true),
} as any;

piratePort = {
id: vi.fn(() => 201),
tile: vi.fn(() => 56),
Expand All @@ -72,6 +84,15 @@ describe("TradeShipExecution", () => {
isMarkedForDeletion: vi.fn(() => false),
} as any;

halfPiratePort = {
id: vi.fn(() => 301),
tile: vi.fn(() => 11),
owner: vi.fn(() => halfPirate),
isActive: vi.fn(() => true),
isUnderConstruction: vi.fn(() => false),
isMarkedForDeletion: vi.fn(() => false),
} as any;

srcPort = {
id: vi.fn(() => 101),
tile: vi.fn(() => 10),
Expand All @@ -95,7 +116,10 @@ describe("TradeShipExecution", () => {
owner: vi.fn(() => origOwner),
id: vi.fn(() => 123),
move: vi.fn(),
setTargetUnit: vi.fn(),
setTargetUnit: vi.fn((port: Unit) => {
currentTarget = port;
}),
targetUnit: vi.fn(() => currentTarget),
setSafeFromPirates: vi.fn(),
touch: vi.fn(),
delete: vi.fn(),
Expand All @@ -109,6 +133,18 @@ describe("TradeShipExecution", () => {
findPath: vi.fn((from: number) => [from]),
} as any;
tradeShipExecution["tradeShip"] = tradeShip;

vi.spyOn(game, "players").mockReturnValue([
origOwner,
dstOwner,
pirate,
halfPirate,
]);
vi.spyOn(game, "getWaterComponent").mockReturnValue(1);
vi.spyOn(game, "hasWaterComponent").mockReturnValue(true);
vi.spyOn(game, "manhattanDist").mockReturnValue(10);
Comment thread
lnoss marked this conversation as resolved.

currentTarget = null;
});

it("should initialize and tick without errors", () => {
Expand All @@ -135,6 +171,22 @@ describe("TradeShipExecution", () => {
expect(tradeShip.setTargetUnit).toHaveBeenCalledWith(piratePort);
});

it("should pick another port if destination is embargoed and ship not captured", () => {
dstOwner.canTrade = vi.fn(() => false);
halfPirate.canTrade = vi.fn(() => true);
pirate.canTrade = vi.fn(() => false);
origOwner.canTrade = vi.fn((target: Player) => {
if (target.id() === halfPirate.id()) return true;
return false;
});

tradeShipExecution.tick(1);
expect(tradeShip.delete).not.toHaveBeenCalled();
expect(tradeShipExecution.isActive()).toBe(true);
expect(tradeShip.targetUnit()).toBe(halfPiratePort);
expect(tradeShip.touch).toHaveBeenCalled();
});

it("should complete trade and award gold", () => {
tradeShipExecution["pathFinder"] = {
next: vi.fn(() => ({ status: PathStatus.COMPLETE, node: 32 })),
Expand Down
Loading