From d05f5e748c6d799cb27815465d9d1fec2a782fa5 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Wed, 15 Apr 2026 11:35:32 +0900 Subject: [PATCH] Fix external train trade attribution in match results (#3589) ## Description: A player reported that income received from opponents' trains was missing from match result logs. Fix external train trade attribution to the station owner and add regression tests for train income stats. ## 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: aotumuri --- src/core/game/TrainStation.ts | 2 +- tests/Stats.test.ts | 9 ++++++ tests/core/game/TrainStation.test.ts | 47 +++++++++++++++++++++++++--- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/core/game/TrainStation.ts b/src/core/game/TrainStation.ts index 8f64a0c69..66c2be111 100644 --- a/src/core/game/TrainStation.ts +++ b/src/core/game/TrainStation.ts @@ -29,7 +29,7 @@ class TradeStationStopHandler implements TrainStopHandler { // Share revenue with the station owner if it's not the current player if (trainOwner !== stationOwner) { stationOwner.addGold(gold, station.tile()); - mg.stats().trainExternalTrade(trainOwner, gold); + mg.stats().trainExternalTrade(stationOwner, gold); } trainOwner.addGold(gold, station.tile()); mg.stats().trainSelfTrade(trainOwner, gold); diff --git a/tests/Stats.test.ts b/tests/Stats.test.ts index 4cab4b2fe..920d363ee 100644 --- a/tests/Stats.test.ts +++ b/tests/Stats.test.ts @@ -181,6 +181,15 @@ describe("Stats", () => { }); }); + test("train trade gold", () => { + stats.trainSelfTrade(player1, 2); + stats.trainExternalTrade(player2, 1); + expect(stats.stats()).toStrictEqual({ + client1: { gold: [0n, 0n, 0n, 0n, 2n] }, + client2: { gold: [0n, 0n, 0n, 0n, 0n, 1n] }, + }); + }); + test("unitBuild", () => { stats.unitBuild(player1, UnitType.City); expect(stats.stats()).toStrictEqual({ diff --git a/tests/core/game/TrainStation.test.ts b/tests/core/game/TrainStation.test.ts index 03b84d8ca..703495c5f 100644 --- a/tests/core/game/TrainStation.test.ts +++ b/tests/core/game/TrainStation.test.ts @@ -24,11 +24,19 @@ vi.mock("../../../src/core/PseudoRandom"); describe("TrainStation", () => { let game: Mocked; + let gameStats: { + trainExternalTrade: ReturnType; + trainSelfTrade: ReturnType; + }; let unit: Mocked; let player: Mocked; let trainExecution: Mocked; beforeEach(() => { + gameStats = { + trainExternalTrade: vi.fn(), + trainSelfTrade: vi.fn(), + }; game = { ticks: vi.fn().mockReturnValue(123), config: vi.fn().mockReturnValue({ @@ -37,16 +45,15 @@ describe("TrainStation", () => { }), addUpdate: vi.fn(), addExecution: vi.fn(), - stats: vi.fn().mockReturnValue({ - trainExternalTrade: vi.fn(), - trainSelfTrade: vi.fn(), - }), + stats: vi.fn().mockReturnValue(gameStats), } as any; player = { addGold: vi.fn(), id: 1, canTrade: vi.fn().mockReturnValue(true), + isAlliedWith: vi.fn().mockReturnValue(false), + isOnSameTeam: vi.fn().mockReturnValue(false), isFriendly: vi.fn().mockReturnValue(false), } as any; @@ -89,6 +96,38 @@ describe("TrainStation", () => { ); }); + it("records external trade on the station owner", () => { + const stationOwner = { + addGold: vi.fn(), + id: 1, + canTrade: vi.fn().mockReturnValue(true), + isAlliedWith: vi.fn().mockReturnValue(false), + isOnSameTeam: vi.fn().mockReturnValue(false), + } as any; + const trainOwner = { + addGold: vi.fn(), + id: 2, + canTrade: vi.fn().mockReturnValue(true), + isAlliedWith: vi.fn().mockReturnValue(false), + isOnSameTeam: vi.fn().mockReturnValue(false), + } as any; + + unit.type.mockReturnValue(UnitType.City); + unit.owner.mockReturnValue(stationOwner); + trainExecution.owner.mockReturnValue(trainOwner); + const station = new TrainStation(game, unit); + + station.onTrainStop(trainExecution); + + expect(stationOwner.addGold).toHaveBeenCalledWith(500n, unit.tile()); + expect(trainOwner.addGold).toHaveBeenCalledWith(500n, unit.tile()); + expect(gameStats.trainExternalTrade).toHaveBeenCalledWith( + stationOwner, + 500n, + ); + expect(gameStats.trainSelfTrade).toHaveBeenCalledWith(trainOwner, 500n); + }); + it("passes tradeStopsVisited to trainGold", () => { unit.type.mockReturnValue(UnitType.City); const trainGoldSpy = vi.fn().mockReturnValue(500n);