Files
OpenFrontIO/tests/economy/ConstructionGold.test.ts
CrackeRR11 8f53785a80 BUG FIX: Gold double deduction + Rmoval of UnitType.Construction (#2378)
## Description:

- Removed the temporary UnitType.Construction and embedded construction
state into real units via isUnderConstruction().
- Centralized non-structure spawning to perform a single validation
right before unit creation/launch.
- Updated UI layers to render construction state without relying on the
removed enum.
- Adjusted and created tests to match the new flow and to cover the
no-refundscenarios.

# Tests updated 
- tests/economy/ConstructionGold.test.ts: covers structure cost
deduction and income, tolerant of passive income; ensures no refunds
during construction.
- tests/nukes/HydrogenAndMirv.test.ts: accounts for single-check launch
flow; MIRV test targets a player-owned tile; ensures launch after
payment.
- tests/client/graphics/UILayer.test.ts: mocks now provide
isUnderConstruction and real type strings;

## 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:

CrackeRR1

---------

Co-authored-by: Evan <evanpelle@gmail.com>
2025-11-26 14:45:14 -08:00

72 lines
2.4 KiB
TypeScript

import { ConstructionExecution } from "../../src/core/execution/ConstructionExecution";
import { SpawnExecution } from "../../src/core/execution/SpawnExecution";
import {
Game,
Player,
PlayerInfo,
PlayerType,
UnitType,
} from "../../src/core/game/Game";
import { setup } from "../util/Setup";
describe("Construction economy", () => {
let game: Game;
let player: Player;
beforeEach(async () => {
game = await setup("ocean_and_land", {
infiniteGold: false,
instantBuild: false,
infiniteTroops: true,
});
const info = new PlayerInfo(
"builder",
PlayerType.Human,
null,
"builder_id",
);
game.addPlayer(info);
const spawn = game.ref(0, 10);
game.addExecution(new SpawnExecution(info, spawn));
while (game.inSpawnPhase()) {
game.executeNextTick();
}
player = game.player(info.id);
});
test("City charges gold once and no refund thereafter (allow passive income)", () => {
const target = game.ref(0, 10);
const cost = game.unitInfo(UnitType.City).cost(player);
player.addGold(cost);
expect(player.gold()).toBe(cost);
const startTick = game.ticks();
game.addExecution(new ConstructionExecution(player, UnitType.City, target));
// First tick usually initializes the execution, second tick performs build and deduction
game.executeNextTick();
game.executeNextTick();
const afterBuild = player.gold();
const ticksAfterBuild = BigInt(game.ticks() - startTick);
const passivePerTick = 100n; // DefaultConfig goldAdditionRate for humans
expect(afterBuild < cost).toBe(true); // cost was deducted
expect(afterBuild <= ticksAfterBuild * passivePerTick).toBe(true); // only passive income allowed
// Advance through construction duration
const duration = game.unitInfo(UnitType.City).constructionDuration ?? 0;
for (let i = 0; i <= duration + 2; i++) game.executeNextTick();
const finalGold = player.gold();
const ticksElapsed = BigInt(game.ticks() - startTick);
// Ensure no refund equal to cost snuck back in; only passive income accumulated
expect(finalGold < cost).toBe(true);
expect(finalGold <= ticksElapsed * passivePerTick).toBe(true);
// Structure exists and is active
expect(player.units(UnitType.City)).toHaveLength(1);
expect(
(player.units(UnitType.City)[0] as any).isUnderConstruction?.() ?? false,
).toBe(false);
});
});