From 5c4692bc29c19bf6d6b889512112e8732d52441d Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Mon, 23 Jun 2025 00:23:20 -0400 Subject: [PATCH] Discounts can only be used one time (#892) ## Description: Discounts on structures and warships can only be used one time. ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --------- Co-authored-by: Scott Anderson --- src/core/configuration/DefaultConfig.ts | 27 ++++----------- src/core/execution/FakeHumanExecution.ts | 3 +- src/core/game/Game.ts | 3 +- src/core/game/PlayerImpl.ts | 44 +++++++++++++++++++----- 4 files changed, 45 insertions(+), 32 deletions(-) diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 97d474f0d..2a0a47e2c 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -332,9 +332,7 @@ export class DefaultConfig implements Config { : BigInt( Math.min( 1_000_000, - (p.unitsIncludingConstruction(UnitType.Warship).length + - 1) * - 250_000, + (p.unitsConstructed(UnitType.Warship) + 1) * 250_000, ), ), territoryBound: false, @@ -359,10 +357,7 @@ export class DefaultConfig implements Config { : BigInt( Math.min( 1_000_000, - Math.pow( - 2, - p.unitsIncludingConstruction(UnitType.Port).length, - ) * 125_000, + Math.pow(2, p.unitsConstructed(UnitType.Port)) * 125_000, ), ), territoryBound: true, @@ -421,9 +416,7 @@ export class DefaultConfig implements Config { : BigInt( Math.min( 250_000, - (p.unitsIncludingConstruction(UnitType.DefensePost).length + - 1) * - 50_000, + (p.unitsConstructed(UnitType.DefensePost) + 1) * 50_000, ), ), territoryBound: true, @@ -438,9 +431,7 @@ export class DefaultConfig implements Config { : BigInt( Math.min( 3_000_000, - (p.unitsIncludingConstruction(UnitType.SAMLauncher).length + - 1) * - 1_500_000, + (p.unitsConstructed(UnitType.SAMLauncher) + 1) * 1_500_000, ), ), territoryBound: true, @@ -455,10 +446,7 @@ export class DefaultConfig implements Config { : BigInt( Math.min( 1_000_000, - Math.pow( - 2, - p.unitsIncludingConstruction(UnitType.City).length, - ) * 125_000, + Math.pow(2, p.unitsConstructed(UnitType.City)) * 125_000, ), ), territoryBound: true, @@ -473,10 +461,7 @@ export class DefaultConfig implements Config { : BigInt( Math.min( 1_000_000, - Math.pow( - 2, - p.unitsIncludingConstruction(UnitType.Factory).length, - ) * 125_000, + Math.pow(2, p.unitsConstructed(UnitType.Factory)) * 125_000, ), ), territoryBound: true, diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 8c040a17e..ad10a8c70 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -466,8 +466,7 @@ export class FakeHumanExecution implements Execution { private maybeSpawnStructure(type: UnitType, maxNum: number): boolean { if (this.player === null) throw new Error("not initialized"); - const units = this.player.unitsIncludingConstruction(type); - if (units.length >= maxNum) { + if (this.player.unitsOwned(type) >= maxNum) { return false; } if (this.player.gold() < this.cost(type)) { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 4d64c8dfe..aa19683f3 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -508,7 +508,8 @@ export interface Player { // Units units(...types: UnitType[]): Unit[]; - unitsIncludingConstruction(type: UnitType): Unit[]; + unitsConstructed(type: UnitType): number; + unitsOwned(type: UnitType): number; buildableUnits(tile: TileRef): BuildableUnit[]; canBuild(type: UnitType, targetTile: TileRef): TileRef | false; buildUnit( diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 9bdbcb6e1..b9f045d00 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -216,14 +216,41 @@ export class PlayerImpl implements Player { return this._units.filter((u) => ts.has(u.type())); } - unitsIncludingConstruction(type: UnitType): Unit[] { - const units = this.units(type); - units.push( - ...this.units(UnitType.Construction).filter( - (u) => u.constructionType() === type, - ), - ); - return units; + private numUnitsConstructed: number[] = []; + private recordUnitConstructed(type: UnitType): void { + if (type in this.numUnitsConstructed) { + this.numUnitsConstructed[type]++; + } else { + this.numUnitsConstructed[type] = 1; + } + } + + // Count of units built by the player, including construction + unitsConstructed(type: UnitType): number { + const built = this.numUnitsConstructed[type] ?? 0; + let constructing = 0; + for (const unit of this._units) { + if (unit.type() !== UnitType.Construction) continue; + if (unit.constructionType() !== type) continue; + constructing++; + } + const total = constructing + built; + return total; + } + + // Count of units owned by the player, including construction + unitsOwned(type: UnitType): number { + let total = 0; + for (const unit of this._units) { + if (unit.type() === type) { + total++; + continue; + } + if (unit.type() !== UnitType.Construction) continue; + if (unit.constructionType() !== type) continue; + total++; + } + return total; } sharesBorderWith(other: Player | TerraNullius): boolean { @@ -749,6 +776,7 @@ export class PlayerImpl implements Player { params, ); this._units.push(b); + this.recordUnitConstructed(type); this.removeGold(cost); this.removeTroops("troops" in params ? (params.troops ?? 0) : 0); this.mg.addUpdate(b.toUpdate());