diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index ae9119509..ff7b1be73 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -8,10 +8,12 @@ import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png"; import shieldIcon from "../../../../resources/images/ShieldIcon.png"; import cityIcon from "../../../../resources/images/CityIcon.png"; import { GameView, UnitView } from "../../../core/game/GameView"; -import { Cell, Unit, UnitType } from "../../../core/game/Game"; +import { Cell, UnitType } from "../../../core/game/Game"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { euclDistFN } from "../../../core/game/GameMap"; +const underConstructionColor = colord({ r: 150, g: 150, b: 150 }); + interface UnitRenderConfig { icon: string; borderRadius: number; @@ -48,10 +50,7 @@ export class StructureLayer implements Layer { }, }; - constructor( - private game: GameView, - private eventBus: EventBus, - ) { + constructor(private game: GameView, private eventBus: EventBus) { this.theme = game.config().theme(); this.loadIconData(); } @@ -73,11 +72,11 @@ export class StructureLayer implements Layer { 0, 0, tempCanvas.width, - tempCanvas.height, + tempCanvas.height ); this.unitIcons.set(unitType, iconData); console.log( - `icond data width height: ${iconData.width}, ${iconData.height}`, + `icond data width height: ${iconData.width}, ${iconData.height}` ); }; }); @@ -90,9 +89,9 @@ export class StructureLayer implements Layer { tick() { this.game .updatesSinceLastTick() - [ - GameUpdateType.Unit - ].forEach((u) => this.handleUnitRendering(this.game.unit(u.id))); + [GameUpdateType.Unit].forEach((u) => + this.handleUnitRendering(this.game.unit(u.id)) + ); } init() { @@ -114,7 +113,7 @@ export class StructureLayer implements Layer { -this.game.width() / 2, -this.game.height() / 2, this.game.width(), - this.game.height(), + this.game.height() ); } @@ -123,7 +122,7 @@ export class StructureLayer implements Layer { } private handleUnitRendering(unit: UnitView) { - const unitType = unit.type(); + const unitType = unit.constructionType() ?? unit.type(); if (!this.isUnitTypeSupported(unitType)) return; const config = this.unitConfigs[unitType]; @@ -134,7 +133,7 @@ export class StructureLayer implements Layer { // Clear previous rendering for (const tile of this.game.bfs( unit.tile(), - euclDistFN(unit.tile(), config.borderRadius), + euclDistFN(unit.tile(), config.borderRadius) )) { this.clearCell(new Cell(this.game.x(tile), this.game.y(tile))); } @@ -146,23 +145,27 @@ export class StructureLayer implements Layer { // Draw border and territory for (const tile of this.game.bfs( unit.tile(), - euclDistFN(unit.tile(), config.borderRadius), + euclDistFN(unit.tile(), config.borderRadius) )) { this.paintCell( new Cell(this.game.x(tile), this.game.y(tile)), - this.theme.borderColor(unit.owner().info()), - 255, + unit.type() == UnitType.Construction + ? underConstructionColor + : this.theme.borderColor(unit.owner().info()), + 255 ); } for (const tile of this.game.bfs( unit.tile(), - euclDistFN(unit.tile(), config.territoryRadius), + euclDistFN(unit.tile(), config.territoryRadius) )) { this.paintCell( new Cell(this.game.x(tile), this.game.y(tile)), - this.theme.territoryColor(unit.owner().info()), - 130, + unit.type() == UnitType.Construction + ? underConstructionColor + : this.theme.territoryColor(unit.owner().info()), + 130 ); } @@ -178,8 +181,12 @@ export class StructureLayer implements Layer { startY: number, width: number, height: number, - unit: UnitView, + unit: UnitView ) { + let color = this.theme.borderColor(unit.owner().info()); + if (unit.type() == UnitType.Construction) { + color = underConstructionColor; + } for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const iconIndex = (y * width + x) * 4; @@ -195,11 +202,7 @@ export class StructureLayer implements Layer { targetY >= 0 && targetY < this.game.height() ) { - this.paintCell( - new Cell(targetX, targetY), - this.theme.borderColor(unit.owner().info()), - alpha, - ); + this.paintCell(new Cell(targetX, targetY), color, alpha); } } } diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 2e36696d5..2afc300bd 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -1,7 +1,6 @@ import { Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; import { Unit, UnitType, Player } from "../../../core/game/Game"; -import { UnitUpdate } from "../../../core/game/GameUpdates"; import { Layer } from "./Layer"; import { EventBus } from "../../../core/EventBus"; import { AlternateViewEvent } from "../../InputHandler"; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 52aeb4dbf..5d7d75fe6 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -109,6 +109,7 @@ export class DefaultConfig implements Config { Math.pow(2, p.units(UnitType.Port).length) * 250_000 ), territoryBound: true, + constructionDuration: 2 * 10, }; case UnitType.AtomBomb: return { @@ -139,6 +140,7 @@ export class DefaultConfig implements Config { return { cost: () => 1_000_000, territoryBound: true, + constructionDuration: 10 * 10, }; case UnitType.DefensePost: return { @@ -148,6 +150,7 @@ export class DefaultConfig implements Config { (p.units(UnitType.DefensePost).length + 1) * 50_000 ), territoryBound: true, + constructionDuration: 5 * 10, }; case UnitType.City: return { @@ -157,6 +160,12 @@ export class DefaultConfig implements Config { Math.pow(2, p.units(UnitType.City).length) * 125_000 ), territoryBound: true, + constructionDuration: 2 * 10, + }; + case UnitType.Construction: + return { + cost: () => 0, + territoryBound: true, }; default: assertNever(type); diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 93f42456d..717a8c3ed 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -45,10 +45,10 @@ export class DevConfig extends DefaultConfig { // return 5000 // } - // numBots(): number { - // return 0; - // } - // spawnNPCs(): boolean { - // return false; - // } + // numBots(): number { + // return 0; + // } + // spawnNPCs(): boolean { + // return false; + // } } diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts new file mode 100644 index 000000000..0e5ff2048 --- /dev/null +++ b/src/core/execution/ConstructionExecution.ts @@ -0,0 +1,122 @@ +import { consolex } from "../Consolex"; +import { + Cell, + Execution, + Game, + Player, + Unit, + PlayerID, + UnitType, + Tick, +} from "../game/Game"; +import { TileRef } from "../game/GameMap"; +import { CityExecution } from "./CityExecution"; +import { DefensePostExecution } from "./DefensePostExecution"; +import { MirvExecution } from "./MIRVExecution"; +import { MissileSiloExecution } from "./MissileSiloExecution"; +import { NukeExecution } from "./NukeExecution"; +import { PortExecution } from "./PortExecution"; +import { WarshipExecution } from "./WarshipExecution"; + +export class ConstructionExecution implements Execution { + private player: Player; + private construction: Unit; + private active: boolean = true; + private mg: Game; + + private ticksUntilComplete: Tick; + + constructor( + private ownerId: PlayerID, + private tile: TileRef, + private constructionType: UnitType + ) {} + + init(mg: Game, ticks: number): void { + this.mg = mg; + this.player = mg.player(this.ownerId); + } + + tick(ticks: number): void { + if (this.construction == null) { + const info = this.mg.unitInfo(this.constructionType); + if (info.constructionDuration == null) { + this.completeConstruction(); + this.active = false; + return; + } + const spawnTile = this.player.canBuild(this.constructionType, this.tile); + if (spawnTile == false) { + consolex.warn(`cannot build ${UnitType.Construction}`); + this.active = false; + return; + } + this.construction = this.player.buildUnit( + UnitType.Construction, + 0, + spawnTile + ); + this.construction.setConstructionType(this.constructionType); + this.ticksUntilComplete = info.constructionDuration; + return; + } + + if (!this.construction.isActive()) { + this.active = false; + return; + } + + if (this.ticksUntilComplete == 0) { + this.player = this.construction.owner(); + this.construction.delete(false); + this.completeConstruction(); + this.active = false; + return; + } + this.ticksUntilComplete--; + } + + private completeConstruction() { + const player = this.player; + switch (this.constructionType) { + case UnitType.AtomBomb: + case UnitType.HydrogenBomb: + this.mg.addExecution( + new NukeExecution(this.constructionType, player.id(), this.tile) + ); + break; + case UnitType.MIRV: + this.mg.addExecution(new MirvExecution(player.id(), this.tile)); + break; + case UnitType.Warship: + this.mg.addExecution(new WarshipExecution(player.id(), this.tile)); + break; + case UnitType.Port: + this.mg.addExecution(new PortExecution(player.id(), this.tile)); + break; + case UnitType.MissileSilo: + this.mg.addExecution(new MissileSiloExecution(player.id(), this.tile)); + break; + case UnitType.DefensePost: + this.mg.addExecution(new DefensePostExecution(player.id(), this.tile)); + break; + case UnitType.City: + this.mg.addExecution(new CityExecution(player.id(), this.tile)); + break; + default: + throw Error(`unit type ${this.constructionType} not supported`); + } + } + + owner(): Player { + return null; + } + + isActive(): boolean { + return this.active; + } + + activeDuringSpawnPhase(): boolean { + return false; + } +} diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 9564b3f8f..49a6e825f 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -38,6 +38,7 @@ import { DefensePostExecution } from "./DefensePostExecution"; import { CityExecution } from "./CityExecution"; import { TileRef } from "../game/GameMap"; import { MirvExecution } from "./MIRVExecution"; +import { ConstructionExecution } from "./ConstructionExecution"; export class Executor { // private random = new PseudoRandom(999) @@ -106,47 +107,11 @@ export class Executor { case "troop_ratio": return new SetTargetTroopRatioExecution(intent.player, intent.ratio); case "build_unit": - switch (intent.unit) { - case UnitType.AtomBomb: - case UnitType.HydrogenBomb: - return new NukeExecution( - intent.unit, - intent.player, - this.mg.ref(intent.x, intent.y) - ); - case UnitType.MIRV: - return new MirvExecution( - intent.player, - this.mg.ref(intent.x, intent.y) - ); - case UnitType.Warship: - return new WarshipExecution( - intent.player, - this.mg.ref(intent.x, intent.y) - ); - case UnitType.Port: - return new PortExecution( - intent.player, - this.mg.ref(intent.x, intent.y) - ); - case UnitType.MissileSilo: - return new MissileSiloExecution( - intent.player, - this.mg.ref(intent.x, intent.y) - ); - case UnitType.DefensePost: - return new DefensePostExecution( - intent.player, - this.mg.ref(intent.x, intent.y) - ); - case UnitType.City: - return new CityExecution( - intent.player, - this.mg.ref(intent.x, intent.y) - ); - default: - throw Error(`unit type ${intent.unit} not supported`); - } + return new ConstructionExecution( + intent.player, + this.mg.ref(intent.x, intent.y), + intent.unit + ); default: throw new Error(`intent type ${intent} not found`); } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 5ddf7bbea..fb73b22a9 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -29,6 +29,7 @@ import { AllianceRequestReplyExecution } from "./alliance/AllianceRequestReplyEx import { closestTwoTiles } from "./Util"; import { calculateBoundingBox, simpleHash } from "../Util"; import { andFN, manhattanDistFN, TileRef } from "../game/GameMap"; +import { ConstructionExecution } from "./ConstructionExecution"; export class FakeHumanExecution implements Execution { private firstMove = true; @@ -304,14 +305,16 @@ export class FakeHumanExecution implements Execution { ); if (oceanTiles.length > 0) { const buildTile = this.random.randElement(oceanTiles); - this.mg.addExecution(new PortExecution(this.player.id(), buildTile)); + this.mg.addExecution( + new ConstructionExecution(this.player.id(), buildTile, UnitType.Port) + ); } return; } this.maybeSpawnStructure( UnitType.City, 2, - (t) => new CityExecution(this.player.id(), t) + (t) => new ConstructionExecution(this.player.id(), t, UnitType.City) ); if (this.maybeSpawnWarship()) { return; @@ -319,7 +322,8 @@ export class FakeHumanExecution implements Execution { this.maybeSpawnStructure( UnitType.MissileSilo, 1, - (t) => new MissileSiloExecution(this.player.id(), t) + (t) => + new ConstructionExecution(this.player.id(), t, UnitType.MissileSilo) ); } @@ -369,7 +373,13 @@ export class FakeHumanExecution implements Execution { consolex.warn("cannot spawn destroyer"); return false; } - this.mg.addExecution(new WarshipExecution(this.player.id(), targetTile)); + this.mg.addExecution( + new ConstructionExecution( + this.player.id(), + targetTile, + UnitType.Warship + ) + ); return true; } return false; diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index d12497f74..371c3e905 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -58,6 +58,7 @@ export interface UnitInfo { territoryBound: boolean; maxHealth?: number; damage?: number; + constructionDuration?: number; } export enum UnitType { @@ -73,6 +74,7 @@ export enum UnitType { City = "City", MIRV = "MIRV", MIRVWarhead = "MIRV Warhead", + Construction = "Construction", } export enum Relation { @@ -187,6 +189,10 @@ export interface Unit { setTroops(troops: number): void; delete(displayerMessage?: boolean): void; + // Only for Construction type + constructionType(): UnitType | null; + setConstructionType(type: UnitType): void; + // Updates toUpdate(): UnitUpdate; } diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index ea6114a11..9b76a45de 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -13,7 +13,6 @@ import { Nation, UnitType, UnitInfo, - AllPlayers, GameUpdates, TerrainType, EmojiMessage, diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index ad1c3eaf8..7bd89182a 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -67,6 +67,7 @@ export interface UnitUpdate { lastPos: MapPos; isActive: boolean; health?: number; + constructionType?: UnitType; } export interface PlayerUpdate { diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 7b87026f5..bde421c9e 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -86,6 +86,9 @@ export class UnitView { health(): number { return this.data.health ?? 0; } + constructionType(): UnitType | undefined { + return this.data.constructionType; + } } export class PlayerView { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 97dac7417..42df4622d 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -580,15 +580,14 @@ export class PlayerImpl implements Player { return this.warshipSpawn(targetTile); case UnitType.Shell: return targetTile; - case UnitType.MissileSilo: - return this.landBasedStructureSpawn(targetTile); - case UnitType.DefensePost: - return this.landBasedStructureSpawn(targetTile); case UnitType.TransportShip: return this.transportShipSpawn(targetTile); case UnitType.TradeShip: return this.tradeShipSpawn(targetTile); + case UnitType.MissileSilo: + case UnitType.DefensePost: case UnitType.City: + case UnitType.Construction: return this.landBasedStructureSpawn(targetTile); default: assertNever(unitType); diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 4429714c9..1cc90a682 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -12,6 +12,8 @@ export class UnitImpl implements Unit { private _health: number; private _lastTile: TileRef = null; + private _constructionType: UnitType = undefined; + constructor( private _type: UnitType, private mg: GameImpl, @@ -36,6 +38,7 @@ export class UnitImpl implements Unit { pos: { x: this.mg.x(this._tile), y: this.mg.y(this._tile) }, lastPos: { x: this.mg.x(this._lastTile), y: this.mg.y(this._lastTile) }, health: this.hasHealth() ? this._health : undefined, + constructionType: this._constructionType, }; } @@ -116,6 +119,21 @@ export class UnitImpl implements Unit { return this._active; } + constructionType(): UnitType | null { + if (this.type() != UnitType.Construction) { + throw new Error(`Cannot get construction type on ${this.type()}`); + } + return this._constructionType; + } + + setConstructionType(type: UnitType): void { + if (this.type() != UnitType.Construction) { + throw new Error(`Cannot set construction type on ${this.type()}`); + } + this._constructionType = type; + this.mg.addUpdate(this.toUpdate()); + } + hash(): number { return this.tile() + simpleHash(this.type()); }