diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 208a7f098..11ac917b9 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -1,6 +1,6 @@ import { Config } from "../core/configuration/Config" import { EventBus, GameEvent } from "../core/EventBus" -import { AllianceRequest, AllPlayers, Cell, Item, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game" +import { AllianceRequest, AllPlayers, Cell, BuildItem, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game" import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, BuildUnitIntentSchema, GameID, Intent, ServerMessage, ServerMessageSchema } from "../core/Schemas" import { LocalServer } from "./LocalServer" diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index a8c101266..aae132898 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -15,6 +15,7 @@ import { ControlPanel } from "./layers/ControlPanel"; import { UIState } from "./UIState"; import { BuildMenu } from "./layers/radial/BuildMenu"; import { UnitLayer } from "./layers/UnitLayer"; +import { BuildValidator } from "../../core/game/BuildValidator"; export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus: EventBus, clientID: ClientID): GameRenderer { @@ -33,7 +34,7 @@ export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus: } buildMenu.game = game buildMenu.eventBus = eventBus - buildMenu.init() + buildMenu.buildValidator = new BuildValidator(game) const leaderboard = document.querySelector('leader-board') as Leaderboard; if (!emojiTable || !(leaderboard instanceof Leaderboard)) { diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index 0f174e941..e3b0f3600 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -1,21 +1,21 @@ import { LitElement, html, css } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { EventBus } from '../../../../core/EventBus'; -import { Cell, Game, Item, BuildItems, Player, UnitType } from '../../../../core/game/Game'; -import { BuildUnitIntentEvent as BuildItemIntentEvent, BuildUnitIntentEvent, SendNukeIntentEvent } from '../../../Transport'; +import { Cell, Game, BuildItem, BuildItems, Player, UnitType } from '../../../../core/game/Game'; +import { BuildUnitIntentEvent, SendNukeIntentEvent } from '../../../Transport'; import nukeIcon from '../../../../../resources/images/NukeIconWhite.svg'; import destroyerIcon from '../../../../../resources/images/DestroyerIconWhite.svg'; import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg'; import portIcon from '../../../../../resources/images/PortIcon.svg'; import { renderNumber } from '../../Utils'; -import { ContextMenuEvent } from '../../../InputHandler'; +import { BuildValidator } from '../../../../core/game/BuildValidator'; -interface BuildItem { - item: Item +interface BuildItemDisplay { + item: BuildItem icon: string; } -const buildTable: BuildItem[][] = [ +const buildTable: BuildItemDisplay[][] = [ [ { item: BuildItems.Nuke, icon: nukeIcon }, { item: BuildItems.Destroyer, icon: destroyerIcon }, @@ -27,6 +27,7 @@ const buildTable: BuildItem[][] = [ export class BuildMenu extends LitElement { public game: Game; public eventBus: EventBus; + public buildValidator: BuildValidator; private myPlayer: Player; private clickedCell: Cell; @@ -145,19 +146,14 @@ export class BuildMenu extends LitElement { @state() private _hidden = true; - private canBuild(item: BuildItem): boolean { - if (!this.myPlayer || this.myPlayer.gold() < item.item.cost) { + private canBuild(item: BuildItemDisplay): boolean { + if(this.myPlayer == null) { return false } - switch (item.item) { - case BuildItems.Destroyer: - return this.myPlayer.units(UnitType.Port).length > 0 - default: - return true - } + return this.buildValidator.canBuild(this.myPlayer, this.game.tile(this.clickedCell), item.item) } - public onBuildSelected = (item: BuildItem) => { + public onBuildSelected = (item: BuildItemDisplay) => { switch (item.item) { case BuildItems.Nuke: this.eventBus.emit(new SendNukeIntentEvent(this.myPlayer, this.clickedCell, null)) diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 24ad7785a..b6c7ce048 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -1,4 +1,6 @@ -import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, PlayerID, UnitType } from "../game/Game"; +import { BuildValidator } from "../game/BuildValidator"; +import { AllPlayers, BuildItem, BuildItems, Cell, Execution, MutableGame, MutablePlayer, PlayerID, UnitType } from "../game/Game"; +import { bfs, dist, manhattanDist } from "../Util"; export class PortExecution implements Execution { @@ -18,7 +20,23 @@ export class PortExecution implements Execution { } tick(ticks: number): void { - this.player.addUnit(UnitType.Port, 0, this.mg.tile(this.cell)) + const tile = this.mg.tile(this.cell) + if (!new BuildValidator(this.mg).canBuild(this.player, tile, BuildItems.Port)) { + console.warn(`player ${this.player} cannot build port at ${this.cell}`) + this.active = false + return + } + const spawns = Array.from(bfs(tile, dist(tile, 20))) + .filter(t => t.isOceanShore() && t.owner() == this.player) + .sort((a, b) => manhattanDist(a.cell(), tile.cell()) - manhattanDist(b.cell(), tile.cell())) + + if (spawns.length == 0) { + console.warn(`cannot find spawn for port`) + this.active = false + return + } + + this.player.addUnit(UnitType.Port, 0, spawns[0]) this.active = false } diff --git a/src/core/game/BuildValidator.ts b/src/core/game/BuildValidator.ts new file mode 100644 index 000000000..6a61198cf --- /dev/null +++ b/src/core/game/BuildValidator.ts @@ -0,0 +1,33 @@ +import { bfs, dist, manhattanDist } from "../Util"; +import { BuildItem, BuildItems, Game, Player, Tile, UnitType } from "./Game"; + +export class BuildValidator { + constructor(private game: Game) { } + + canBuild(player: Player, tile: Tile, item: BuildItem): boolean { + if (!player.isAlive() || player.gold() < item.cost) { + return false + } + switch (item) { + case BuildItems.Nuke: + return true + case BuildItems.Port: + return this.canBuildPort(player, tile) + case BuildItems.Destroyer: + return this.canBuildDestroyer(player, tile) + default: + throw Error(`item ${item.type} not supported`) + } + } + + canBuildPort(player: Player, tile: Tile): boolean { + return Array.from(bfs(tile, dist(tile, 20))) + .filter(t => t.owner() == player && t.isOceanShore()).length > 0 + + } + + canBuildDestroyer(player: Player, tile: Tile): boolean { + return player.units(UnitType.Port) + .filter(u => manhattanDist(u.tile().cell(), tile.cell()) < this.game.config().boatMaxDistance()).length > 0 + } +} \ No newline at end of file diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 7a8b8d6a0..a2c22af84 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -32,16 +32,17 @@ export enum UnitType { Nuke = "Nuke", } -export class Item { - constructor(public readonly type: UnitType, +export class BuildItem { + constructor( + public readonly type: UnitType, public readonly cost: Gold ) { } } export const BuildItems = { - Nuke: new Item(UnitType.Nuke, 1_000_000), - Destroyer: new Item(UnitType.Destroyer, 10), - Port: new Item(UnitType.Port, 0) + Nuke: new BuildItem(UnitType.Nuke, 1_000_000), + Destroyer: new BuildItem(UnitType.Destroyer, 10), + Port: new BuildItem(UnitType.Port, 0) } as const; export class Nation {