mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:20:43 +00:00
have buildings take time to construct
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
Nation,
|
||||
UnitType,
|
||||
UnitInfo,
|
||||
AllPlayers,
|
||||
GameUpdates,
|
||||
TerrainType,
|
||||
EmojiMessage,
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface UnitUpdate {
|
||||
lastPos: MapPos;
|
||||
isActive: boolean;
|
||||
health?: number;
|
||||
constructionType?: UnitType;
|
||||
}
|
||||
|
||||
export interface PlayerUpdate {
|
||||
|
||||
@@ -86,6 +86,9 @@ export class UnitView {
|
||||
health(): number {
|
||||
return this.data.health ?? 0;
|
||||
}
|
||||
constructionType(): UnitType | undefined {
|
||||
return this.data.constructionType;
|
||||
}
|
||||
}
|
||||
|
||||
export class PlayerView {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user