mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-15 09:42:50 +00:00
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>
This commit is contained in:
@@ -1,40 +1,26 @@
|
||||
import { Execution, Game, Player, Unit, UnitType } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { Execution, Game, Unit, UnitType } from "../game/Game";
|
||||
import { TrainStationExecution } from "./TrainStationExecution";
|
||||
|
||||
export class CityExecution implements Execution {
|
||||
private mg: Game;
|
||||
private city: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
private stationCreated = false;
|
||||
|
||||
constructor(
|
||||
private player: Player,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
constructor(private city: Unit) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.city === null) {
|
||||
const spawnTile = this.player.canBuild(UnitType.City, this.tile);
|
||||
if (spawnTile === false) {
|
||||
console.warn("cannot build city");
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.city = this.player.buildUnit(UnitType.City, spawnTile, {});
|
||||
if (!this.stationCreated) {
|
||||
this.createStation();
|
||||
this.stationCreated = true;
|
||||
}
|
||||
if (!this.city.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player !== this.city.owner()) {
|
||||
this.player = this.city.owner();
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
@@ -45,16 +31,14 @@ export class CityExecution implements Execution {
|
||||
return false;
|
||||
}
|
||||
|
||||
createStation(): void {
|
||||
if (this.city !== null) {
|
||||
const nearbyFactory = this.mg.hasUnitNearby(
|
||||
this.city.tile()!,
|
||||
this.mg.config().trainStationMaxRange(),
|
||||
UnitType.Factory,
|
||||
);
|
||||
if (nearbyFactory) {
|
||||
this.mg.addExecution(new TrainStationExecution(this.city));
|
||||
}
|
||||
private createStation(): void {
|
||||
const nearbyFactory = this.mg.hasUnitNearby(
|
||||
this.city.tile()!,
|
||||
this.mg.config().trainStationMaxRange(),
|
||||
UnitType.Factory,
|
||||
);
|
||||
if (nearbyFactory) {
|
||||
this.mg.addExecution(new TrainStationExecution(this.city));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
Gold,
|
||||
Player,
|
||||
Tick,
|
||||
Unit,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { Execution, Game, Player, Tick, Unit, UnitType } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { CityExecution } from "./CityExecution";
|
||||
import { DefensePostExecution } from "./DefensePostExecution";
|
||||
@@ -19,14 +11,12 @@ import { SAMLauncherExecution } from "./SAMLauncherExecution";
|
||||
import { WarshipExecution } from "./WarshipExecution";
|
||||
|
||||
export class ConstructionExecution implements Execution {
|
||||
private construction: Unit | null = null;
|
||||
private structure: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
private mg: Game;
|
||||
|
||||
private ticksUntilComplete: Tick;
|
||||
|
||||
private cost: Gold;
|
||||
|
||||
constructor(
|
||||
private player: Player,
|
||||
private constructionType: UnitType,
|
||||
@@ -52,45 +42,52 @@ export class ConstructionExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.construction === null) {
|
||||
if (this.structure === null) {
|
||||
const info = this.mg.unitInfo(this.constructionType);
|
||||
if (info.constructionDuration === undefined) {
|
||||
// For non-structure units (nukes/warship), charge once and delegate to specialized executions.
|
||||
const isStructure = this.isStructure(this.constructionType);
|
||||
if (!isStructure) {
|
||||
// Defer validation and gold deduction to the specific execution
|
||||
this.completeConstruction();
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Structures: build real unit and mark under construction
|
||||
const spawnTile = this.player.canBuild(this.constructionType, this.tile);
|
||||
if (spawnTile === false) {
|
||||
console.warn(`cannot build ${this.constructionType}`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.construction = this.player.buildUnit(
|
||||
UnitType.Construction,
|
||||
this.structure = this.player.buildUnit(
|
||||
this.constructionType,
|
||||
spawnTile,
|
||||
{},
|
||||
);
|
||||
this.cost = this.mg.unitInfo(this.constructionType).cost(this.player);
|
||||
this.player.removeGold(this.cost);
|
||||
this.construction.setConstructionType(this.constructionType);
|
||||
this.ticksUntilComplete = info.constructionDuration;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.construction.isActive()) {
|
||||
const duration = info.constructionDuration ?? 0;
|
||||
if (duration > 0) {
|
||||
this.structure.setUnderConstruction(true);
|
||||
this.ticksUntilComplete = duration;
|
||||
return;
|
||||
}
|
||||
// No construction time
|
||||
this.completeConstruction();
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player !== this.construction.owner()) {
|
||||
this.player = this.construction.owner();
|
||||
if (!this.structure.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player !== this.structure.owner()) {
|
||||
this.player = this.structure.owner();
|
||||
}
|
||||
|
||||
if (this.ticksUntilComplete === 0) {
|
||||
this.player = this.construction.owner();
|
||||
this.construction.delete(false);
|
||||
// refund the cost so player has the gold to build the unit
|
||||
this.player.addGold(this.cost);
|
||||
this.player = this.structure.owner();
|
||||
this.completeConstruction();
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -99,6 +96,9 @@ export class ConstructionExecution implements Execution {
|
||||
}
|
||||
|
||||
private completeConstruction() {
|
||||
if (this.structure) {
|
||||
this.structure.setUnderConstruction(false);
|
||||
}
|
||||
const player = this.player;
|
||||
switch (this.constructionType) {
|
||||
case UnitType.AtomBomb:
|
||||
@@ -116,22 +116,24 @@ export class ConstructionExecution implements Execution {
|
||||
);
|
||||
break;
|
||||
case UnitType.Port:
|
||||
this.mg.addExecution(new PortExecution(player, this.tile));
|
||||
this.mg.addExecution(new PortExecution(this.structure!));
|
||||
break;
|
||||
case UnitType.MissileSilo:
|
||||
this.mg.addExecution(new MissileSiloExecution(player, this.tile));
|
||||
this.mg.addExecution(new MissileSiloExecution(this.structure!));
|
||||
break;
|
||||
case UnitType.DefensePost:
|
||||
this.mg.addExecution(new DefensePostExecution(player, this.tile));
|
||||
this.mg.addExecution(new DefensePostExecution(this.structure!));
|
||||
break;
|
||||
case UnitType.SAMLauncher:
|
||||
this.mg.addExecution(new SAMLauncherExecution(player, this.tile));
|
||||
this.mg.addExecution(
|
||||
new SAMLauncherExecution(player, null, this.structure!),
|
||||
);
|
||||
break;
|
||||
case UnitType.City:
|
||||
this.mg.addExecution(new CityExecution(player, this.tile));
|
||||
this.mg.addExecution(new CityExecution(this.structure!));
|
||||
break;
|
||||
case UnitType.Factory:
|
||||
this.mg.addExecution(new FactoryExecution(player, this.tile));
|
||||
this.mg.addExecution(new FactoryExecution(this.structure!));
|
||||
break;
|
||||
default:
|
||||
console.warn(
|
||||
@@ -141,6 +143,20 @@ export class ConstructionExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private isStructure(type: UnitType): boolean {
|
||||
switch (type) {
|
||||
case UnitType.Port:
|
||||
case UnitType.MissileSilo:
|
||||
case UnitType.DefensePost:
|
||||
case UnitType.SAMLauncher:
|
||||
case UnitType.City:
|
||||
case UnitType.Factory:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Execution, Game, Player, Unit, UnitType } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { Execution, Game, Unit } from "../game/Game";
|
||||
import { ShellExecution } from "./ShellExecution";
|
||||
|
||||
export class DefensePostExecution implements Execution {
|
||||
private mg: Game;
|
||||
private post: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
|
||||
private target: Unit | null = null;
|
||||
@@ -12,17 +10,13 @@ export class DefensePostExecution implements Execution {
|
||||
|
||||
private alreadySentShell = new Set<Unit>();
|
||||
|
||||
constructor(
|
||||
private player: Player,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
constructor(private post: Unit) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
}
|
||||
|
||||
private shoot() {
|
||||
if (this.post === null) return;
|
||||
if (this.target === null) return;
|
||||
const shellAttackRate = this.mg.config().defensePostShellAttackRate();
|
||||
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
|
||||
@@ -45,22 +39,14 @@ export class DefensePostExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.post === null) {
|
||||
const spawnTile = this.player.canBuild(UnitType.DefensePost, this.tile);
|
||||
if (spawnTile === false) {
|
||||
console.warn("cannot build Defense Post");
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.post = this.player.buildUnit(UnitType.DefensePost, spawnTile, {});
|
||||
}
|
||||
if (!this.post.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player !== this.post.owner()) {
|
||||
this.player = this.post.owner();
|
||||
// Do nothing while the structure is under construction
|
||||
if (this.post.isUnderConstruction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.target !== null && !this.target.isActive()) {
|
||||
|
||||
@@ -1,39 +1,26 @@
|
||||
import { Execution, Game, Player, Unit, UnitType } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { Execution, Game, Unit, UnitType } from "../game/Game";
|
||||
import { TrainStationExecution } from "./TrainStationExecution";
|
||||
|
||||
export class FactoryExecution implements Execution {
|
||||
private factory: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
private game: Game;
|
||||
constructor(
|
||||
private player: Player,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
private stationCreated = false;
|
||||
|
||||
constructor(private factory: Unit) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.game = mg;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (!this.factory) {
|
||||
const spawnTile = this.player.canBuild(UnitType.Factory, this.tile);
|
||||
if (spawnTile === false) {
|
||||
console.warn("cannot build factory");
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.factory = this.player.buildUnit(UnitType.Factory, spawnTile, {});
|
||||
if (!this.stationCreated) {
|
||||
this.createStation();
|
||||
this.stationCreated = true;
|
||||
}
|
||||
if (!this.factory.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player !== this.factory.owner()) {
|
||||
this.player = this.factory.owner();
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
@@ -44,19 +31,17 @@ export class FactoryExecution implements Execution {
|
||||
return false;
|
||||
}
|
||||
|
||||
createStation(): void {
|
||||
if (this.factory !== null) {
|
||||
const structures = this.game.nearbyUnits(
|
||||
this.factory.tile()!,
|
||||
this.game.config().trainStationMaxRange(),
|
||||
[UnitType.City, UnitType.Port, UnitType.Factory],
|
||||
);
|
||||
private createStation(): void {
|
||||
const structures = this.game.nearbyUnits(
|
||||
this.factory.tile()!,
|
||||
this.game.config().trainStationMaxRange(),
|
||||
[UnitType.City, UnitType.Port, UnitType.Factory],
|
||||
);
|
||||
|
||||
this.game.addExecution(new TrainStationExecution(this.factory, true));
|
||||
for (const { unit } of structures) {
|
||||
if (!unit.hasTrainStation()) {
|
||||
this.game.addExecution(new TrainStationExecution(unit));
|
||||
}
|
||||
this.game.addExecution(new TrainStationExecution(this.factory, true));
|
||||
for (const { unit } of structures) {
|
||||
if (!unit.hasTrainStation()) {
|
||||
this.game.addExecution(new TrainStationExecution(unit));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
import { Execution, Game, Player, Unit, UnitType } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { Execution, Game, Unit } from "../game/Game";
|
||||
|
||||
export class MissileSiloExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
private silo: Unit | null = null;
|
||||
private silo: Unit;
|
||||
|
||||
constructor(
|
||||
private player: Player,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
constructor(silo: Unit) {
|
||||
this.silo = silo;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.silo === null) {
|
||||
const spawn = this.player.canBuild(UnitType.MissileSilo, this.tile);
|
||||
if (spawn === false) {
|
||||
console.warn(
|
||||
`player ${this.player} cannot build missile silo at ${this.tile}`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.silo = this.player.buildUnit(UnitType.MissileSilo, spawn, {});
|
||||
|
||||
if (this.player !== this.silo.owner()) {
|
||||
this.player = this.silo.owner();
|
||||
}
|
||||
if (this.silo.isUnderConstruction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// frontTime is the time the earliest missile fired.
|
||||
|
||||
@@ -103,7 +103,7 @@ export class NukeExecution implements Execution {
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.nuke === null) {
|
||||
const spawn = this.src ?? this.player.canBuild(this.nukeType, this.dst);
|
||||
const spawn = this.player.canBuild(this.nukeType, this.dst);
|
||||
if (spawn === false) {
|
||||
console.warn(`cannot build Nuke`);
|
||||
this.active = false;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Execution, Game, Player, Unit, UnitType } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { Execution, Game, Unit, UnitType } from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { TradeShipExecution } from "./TradeShipExecution";
|
||||
import { TrainStationExecution } from "./TrainStationExecution";
|
||||
@@ -7,14 +6,13 @@ import { TrainStationExecution } from "./TrainStationExecution";
|
||||
export class PortExecution implements Execution {
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
private port: Unit | null = null;
|
||||
private port: Unit;
|
||||
private random: PseudoRandom;
|
||||
private checkOffset: number;
|
||||
|
||||
constructor(
|
||||
private player: Player,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
constructor(port: Unit) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
@@ -26,27 +24,18 @@ export class PortExecution implements Execution {
|
||||
if (this.mg === null || this.random === null || this.checkOffset === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
if (this.port === null) {
|
||||
const tile = this.tile;
|
||||
const spawn = this.player.canBuild(UnitType.Port, tile);
|
||||
if (spawn === false) {
|
||||
console.warn(
|
||||
`player ${this.player.id()} cannot build port at ${this.tile}`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.port = this.player.buildUnit(UnitType.Port, spawn, {});
|
||||
this.createStation();
|
||||
}
|
||||
|
||||
if (!this.port.isActive()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player.id() !== this.port.owner().id()) {
|
||||
this.player = this.port.owner();
|
||||
if (this.port.isUnderConstruction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.port.hasTrainStation()) {
|
||||
this.createStation();
|
||||
}
|
||||
|
||||
// Only check every 10 ticks for performance.
|
||||
@@ -65,7 +54,9 @@ export class PortExecution implements Execution {
|
||||
}
|
||||
|
||||
const port = this.random.randElement(ports);
|
||||
this.mg.addExecution(new TradeShipExecution(this.player, this.port, port));
|
||||
this.mg.addExecution(
|
||||
new TradeShipExecution(this.port.owner(), this.port, port),
|
||||
);
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
@@ -78,8 +69,10 @@ export class PortExecution implements Execution {
|
||||
|
||||
shouldSpawnTradeShip(): boolean {
|
||||
const numTradeShips = this.mg.unitCount(UnitType.TradeShip);
|
||||
const numPlayerPorts = this.player.unitCount(UnitType.Port);
|
||||
const numPlayerTradeShips = this.player.unitCount(UnitType.TradeShip);
|
||||
const numPlayerPorts = this.port!.owner().unitCount(UnitType.Port);
|
||||
const numPlayerTradeShips = this.port!.owner().unitCount(
|
||||
UnitType.TradeShip,
|
||||
);
|
||||
const spawnRate = this.mg
|
||||
.config()
|
||||
.tradeShipSpawnRate(numTradeShips, numPlayerPorts, numPlayerTradeShips);
|
||||
@@ -92,15 +85,13 @@ export class PortExecution implements Execution {
|
||||
}
|
||||
|
||||
createStation(): void {
|
||||
if (this.port !== null) {
|
||||
const nearbyFactory = this.mg.hasUnitNearby(
|
||||
this.port.tile()!,
|
||||
this.mg.config().trainStationMaxRange(),
|
||||
UnitType.Factory,
|
||||
);
|
||||
if (nearbyFactory) {
|
||||
this.mg.addExecution(new TrainStationExecution(this.port));
|
||||
}
|
||||
const nearbyFactory = this.mg.hasUnitNearby(
|
||||
this.port.tile()!,
|
||||
this.mg.config().trainStationMaxRange(),
|
||||
UnitType.Factory,
|
||||
);
|
||||
if (nearbyFactory) {
|
||||
this.mg.addExecution(new TrainStationExecution(this.port));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -216,6 +216,10 @@ export class SAMLauncherExecution implements Execution {
|
||||
}
|
||||
this.targetingSystem ??= new SAMTargetingSystem(this.mg, this.sam);
|
||||
|
||||
if (this.sam.isUnderConstruction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.sam.isInCooldown()) {
|
||||
const frontTime = this.sam.missileTimerQueue()[0];
|
||||
if (frontTime === undefined) {
|
||||
|
||||
Reference in New Issue
Block a user