diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 07606aced..65eed6d28 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -190,45 +190,17 @@ export class ClientGameRunner { return } } - - const owner = tile.owner() - const targetID = owner.isPlayer() ? owner.id() : null; - - if (tile.owner() == this.myPlayer) { - return - } - if (tile.owner().isPlayer() && this.myPlayer.isAlliedWith(tile.owner() as Player)) { - this.eventBus.emit(new DisplayMessageEvent("Cannot attack ally", MessageType.WARN)) - return - } - - if (tile.terrain().isLand()) { - if (tile.hasOwner()) { - this.myPlayer.sharesBorderWithAsync(tile.owner()).then(sharesBorder => { - if (sharesBorder) { - this.eventBus.emit( - - new SendAttackIntentEvent( - targetID, - this.myPlayer.troops() * this.renderer.uiState.attackRatio - ) - ) - } - }) - } else { - outer_loop: for (const t of bfs(tile, and(t => !t.hasOwner() && t.terrain().isLand(), dist(tile, 200)))) { - for (const n of t.neighbors()) { - if (n.owner().isPlayer()) { - console.log(`owner: ${(n.owner() as PlayerView).name()}`) - } - if (n.owner() == this.myPlayer) { - this.eventBus.emit(new SendAttackIntentEvent(targetID, this.myPlayer.troops() * this.renderer.uiState.attackRatio)) - break outer_loop - } - } - } + this.myPlayer.actions(tile).then(actions => { + console.log(`got actions: ${JSON.stringify(actions)}`) + if (actions.canAttack) { + this.eventBus.emit( + new SendAttackIntentEvent( + tile.owner().id(), + this.myPlayer.troops() * this.renderer.uiState.attackRatio + ) + ) } - } + }) } } diff --git a/src/client/graphics/layers/radial/RadialMenu.ts b/src/client/graphics/layers/radial/RadialMenu.ts index 07bc8106e..acfc9e4a7 100644 --- a/src/client/graphics/layers/radial/RadialMenu.ts +++ b/src/client/graphics/layers/radial/RadialMenu.ts @@ -1,5 +1,5 @@ import { EventBus } from "../../../../core/EventBus"; -import { AllPlayers, Cell, Game, Player, UnitType } from "../../../../core/game/Game"; +import { AllPlayers, Cell, Game, Player, Tile, UnitType } from "../../../../core/game/Game"; import { ClientID } from "../../../../core/Schemas"; import { and, bfs, dist, manhattanDist, manhattanDistWrapped, sourceDstOceanShore, targetTransportTile } from "../../../../core/Util"; import { ContextMenuEvent, MouseUpEvent, ShowBuildMenuEvent } from "../../../InputHandler"; @@ -20,7 +20,7 @@ import { EmojiTable } from "./EmojiTable"; import { UIState } from "../../UIState"; import { BuildMenu } from "./BuildMenu"; import { consolex } from "../../../../core/Consolex"; -import { GameView } from "../../../../core/GameView"; +import { GameView, PlayerActions, PlayerView } from "../../../../core/GameView"; enum Slot { @@ -237,7 +237,6 @@ export class RadialMenu implements Layer { return } const tile = this.game.tile(this.clickedCell) - const other = tile.owner() if (this.game.inSpawnPhase()) { if (tile.terrain().isLand() && !tile.hasOwner()) { @@ -246,134 +245,82 @@ export class RadialMenu implements Layer { return } - const myPlayer = this.game.players().find(p => p.clientID() == this.clientID) + const myPlayer = this.game.playerViews().find(p => p.clientID() == this.clientID) if (!myPlayer) { consolex.warn('my player not found') return } + myPlayer.actions(tile).then(actions => { + this.handlePlayerActions(myPlayer, actions, tile) + }) + } + private handlePlayerActions(myPlayer: PlayerView, actions: PlayerActions, tile: Tile) { this.activateMenuElement(Slot.Build, "#ebe250", buildIcon, () => { this.buildMenu.showMenu(myPlayer, this.clickedCell) }) - - if (tile.hasOwner()) { - const target = tile.owner() == myPlayer ? AllPlayers : (tile.owner() as Player) - if (myPlayer.canSendEmoji(target)) { - this.activateMenuElement(Slot.Emoji, "#00a6a4", emojiIcon, () => { - this.emojiTable.onEmojiClicked = (emoji: string) => { - this.emojiTable.hideTable() - this.eventBus.emit(new SendEmojiIntentEvent(target, emoji)) - } - this.emojiTable.showTable() - }) - } - } - - if (tile.owner() != myPlayer && tile.terrain().isLand() && myPlayer.sharesBorderWith(other)) { - if (other.isPlayer()) { - if (!myPlayer.isAlliedWith(other)) { - this.enableCenterButton(true) + if (actions.interaction?.canSendEmoji) { + this.activateMenuElement(Slot.Emoji, "#00a6a4", emojiIcon, () => { + const target = tile.owner() == myPlayer ? AllPlayers : (tile.owner() as Player) + this.emojiTable.onEmojiClicked = (emoji: string) => { + this.emojiTable.hideTable() + this.eventBus.emit(new SendEmojiIntentEvent(target, emoji)) } - } else { - outer_loop: for (const t of bfs(tile, and(t => !t.hasOwner() && t.terrain().isLand(), dist(tile, 200)))) { - for (const n of t.neighbors()) { - if (n.owner() == myPlayer) { - this.enableCenterButton(true) - break outer_loop - } - } - } - } + this.emojiTable.showTable() + }) } - if (tile.hasOwner()) { - const other = tile.owner() as Player - if (other.clientID() == this.clientID) { - return - } - - if (myPlayer.canDonate(other)) { - this.activateMenuElement(Slot.Target, "#53ac75", donateIcon, () => { - this.eventBus.emit( - new SendDonateIntentEvent(myPlayer, other, null) + if (actions.canBoat) { + this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => { + this.eventBus.emit( + new SendBoatAttackIntentEvent( + myPlayer.id(), + this.clickedCell, + this.uiState.attackRatio * myPlayer.troops() ) - }) - } - - if (myPlayer.isAlliedWith(other)) { - this.activateMenuElement(Slot.Alliance, "#c74848", traitorIcon, () => { - this.eventBus.emit( - new SendBreakAllianceIntentEvent(myPlayer, other) - ) - }) - } else if (!myPlayer.recentOrPendingAllianceRequestWith(other)) { - this.activateMenuElement(Slot.Alliance, "#53ac75", allianceIcon, () => { - this.eventBus.emit( - new SendAllianceRequestIntentEvent(myPlayer, other) - ) - }) - } - if (myPlayer.canTarget(other)) { - this.activateMenuElement(Slot.Target, "#c74848", targetIcon, () => { - this.eventBus.emit( - new SendTargetPlayerIntentEvent(other.id()) - ) - }) - } + ) + }) + } + if (actions.canAttack) { + this.enableCenterButton(true) } - if (!tile.terrain().isLand()) { - return - } - if (myPlayer.units(UnitType.TransportShip).length >= this.game.config().boatMaxNumber()) { - return - } - - let myPlayerBordersOcean = false - for (const bt of myPlayer.borderTiles()) { - if (bt.terrain().isOceanShore()) { - myPlayerBordersOcean = true - break - } - } - let otherPlayerBordersOcean = false if (!tile.hasOwner()) { - otherPlayerBordersOcean = true - } else { - for (const bt of (other as Player).borderTiles()) { - if (bt.terrain().isOceanShore()) { - otherPlayerBordersOcean = true - break - } - } - } - - if (other.isPlayer() && myPlayer.allianceWith(other)) { return } + const other = tile.owner() as Player - let nearOcean = false - for (const t of bfs(tile, and(t => t.owner() == tile.owner() && t.terrain().isLand(), dist(tile, 25)))) { - if (t.terrain().isOceanShore()) { - nearOcean = true - break - } - } - if (!nearOcean) { - return + + if (actions?.interaction.canDonate) { + this.activateMenuElement(Slot.Target, "#53ac75", donateIcon, () => { + this.eventBus.emit( + new SendDonateIntentEvent(myPlayer, other, null) + ) + }) } - if (myPlayerBordersOcean && otherPlayerBordersOcean) { - const dst = targetTransportTile(this.game.width(), tile) - if (dst != null) { - if (myPlayer.canBuild(UnitType.TransportShip, dst)) { - this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => { - this.eventBus.emit( - new SendBoatAttackIntentEvent(other.id(), this.clickedCell, this.uiState.attackRatio * myPlayer.troops()) - ) - }) - } - } + if (actions?.interaction.canTarget) { + this.activateMenuElement(Slot.Target, "#c74848", targetIcon, () => { + this.eventBus.emit( + new SendTargetPlayerIntentEvent(other.id()) + ) + }) + } + + if (actions?.interaction.canSendAllianceRequest) { + this.activateMenuElement(Slot.Alliance, "#53ac75", allianceIcon, () => { + this.eventBus.emit( + new SendAllianceRequestIntentEvent(myPlayer, other) + ) + }) + } + + if (actions?.interaction.canBreakAlliance) { + this.activateMenuElement(Slot.Alliance, "#c74848", traitorIcon, () => { + this.eventBus.emit( + new SendBreakAllianceIntentEvent(myPlayer, other) + ) + }) } } @@ -408,11 +355,9 @@ export class RadialMenu implements Layer { if (this.game.inSpawnPhase()) { this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell)) } else { - if (clicked.owner().clientID() != this.clientID) { - const myPlayer = this.game.players().find(p => p.clientID() == this.clientID) - if (myPlayer != null) { - this.eventBus.emit(new SendAttackIntentEvent(clicked.owner().id(), this.uiState.attackRatio * myPlayer.troops())) - } + const myPlayer = this.game.players().find(p => p.clientID() == this.clientID) + if (myPlayer != null && clicked.owner() != myPlayer) { + this.eventBus.emit(new SendAttackIntentEvent(clicked.owner().id(), this.uiState.attackRatio * myPlayer.troops())) } } this.hideRadialMenu(); diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index d4d1d678b..d9de3f8a7 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -3,11 +3,12 @@ import { getConfig } from "./configuration/Config"; import { EventBus } from "./EventBus"; import { Executor } from "./execution/ExecutionManager"; import { WinCheckExecution } from "./execution/WinCheckExecution"; -import { Game, MutableGame, MutableTile, PlayerID, Tile, TileEvent } from "./game/Game"; +import { Cell, DisplayMessageEvent, Game, MessageType, MutableGame, MutableTile, Player, PlayerID, Tile, TileEvent, UnitType } from "./game/Game"; import { createGame } from "./game/GameImpl"; import { loadTerrainMap } from "./game/TerrainMapLoader"; -import { GameUpdateViewData, NameViewData, packTileData, PlayerViewData } from "./GameView"; +import { GameUpdateViewData, NameViewData, packTileData, PlayerActions, PlayerViewData } from "./GameView"; import { GameConfig, Turn } from "./Schemas"; +import { and, bfs, dist, targetTransportTile } from "./Util"; export async function createGameRunner(gameID: string, gameConfig: GameConfig, callBack: (gu: GameUpdateViewData) => void): Promise { const config = getConfig(gameConfig) @@ -90,4 +91,103 @@ export class GameRunner { this.isExecuting = false } -} \ No newline at end of file + public playerActions(playerID: PlayerID, x: number, y: number): PlayerActions { + const player = this.game.player(playerID) + const tile = this.game.tile(new Cell(x, y)) + const actions = { + canBoat: this.canBoat(player, tile), + canAttack: this.canAttack(player, tile), + buildableUnits: Object.values(UnitType).filter(ut => player.canBuild(ut, tile) != false) + } as PlayerActions + + if (tile.hasOwner()) { + const other = tile.owner() as Player + actions.interaction = { + sharedBorder: player.sharesBorderWith(other), + canSendEmoji: player.canSendEmoji(other), + canTarget: player.canTarget(other), + canSendAllianceRequest: !player.recentOrPendingAllianceRequestWith(other), + canBreakAlliance: player.isAlliedWith(other), + canDonate: player.canDonate(other) + } + } + + return actions + } + + private canBoat(myPlayer: Player, tile: Tile): boolean { + const other = tile.owner() + if (myPlayer.units(UnitType.TransportShip).length >= this.game.config().boatMaxNumber()) { + return false + } + + let myPlayerBordersOcean = false + for (const bt of myPlayer.borderTiles()) { + if (bt.terrain().isOceanShore()) { + myPlayerBordersOcean = true + break + } + } + let otherPlayerBordersOcean = false + if (!tile.hasOwner()) { + otherPlayerBordersOcean = true + } else { + for (const bt of (other as Player).borderTiles()) { + if (bt.terrain().isOceanShore()) { + otherPlayerBordersOcean = true + break + } + } + } + + if (other.isPlayer() && myPlayer.allianceWith(other)) { + return false + } + + let nearOcean = false + for (const t of bfs(tile, and(t => t.owner() == tile.owner() && t.terrain().isLand(), dist(tile, 25)))) { + if (t.terrain().isOceanShore()) { + nearOcean = true + break + } + } + if (!nearOcean) { + return false + } + + if (myPlayerBordersOcean && otherPlayerBordersOcean) { + const dst = targetTransportTile(this.game.width(), tile) + if (dst != null) { + if (myPlayer.canBuild(UnitType.TransportShip, dst)) { + return true + } + } + } + } + + private canAttack(myPlayer: Player, tile: Tile): boolean { + if (tile.owner() == myPlayer) { + return false + } + // TODO: fix event bus + if (tile.owner().isPlayer() && myPlayer.isAlliedWith(tile.owner() as Player)) { + this.eventBus.emit(new DisplayMessageEvent("Cannot attack ally", MessageType.WARN)) + return false + } + if (!tile.terrain().isLand()) { + return false + } + if (tile.hasOwner()) { + return myPlayer.sharesBorderWith(tile.owner()) + } else { + for (const t of bfs(tile, and(t => !t.hasOwner() && t.terrain().isLand(), dist(tile, 200)))) { + for (const n of t.neighbors()) { + if (n.owner() == myPlayer) { + return true + } + } + } + return false + } + } +} diff --git a/src/core/GameView.ts b/src/core/GameView.ts index 605e2ac69..41accf38a 100644 --- a/src/core/GameView.ts +++ b/src/core/GameView.ts @@ -124,9 +124,29 @@ export interface PlayerViewData extends ViewData { targetTroopRatio: number } +export interface PlayerActions { + canBoat: boolean + canAttack: boolean + buildableUnits: UnitType[] + interaction?: PlayerInteraction +} + +export interface PlayerInteraction { + sharedBorder: boolean + canSendEmoji: boolean + canSendAllianceRequest: boolean + canBreakAlliance: boolean + canTarget: boolean + canDonate: boolean +} + export class PlayerView implements Player { constructor(private game: GameView, public data: PlayerViewData) { } + async actions(tile: Tile): Promise { + return this.game.worker.playerInteraction(this.id(), tile) + } + nameLocation(): NameViewData { return this.data.nameViewData } @@ -196,10 +216,6 @@ export class PlayerView implements Player { return false } - async sharesBorderWithAsync(other: Player | TerraNullius): Promise { - return this.game.worker.sharesBorderWith(this.id(), other.id()) - } - incomingAllianceRequests(): AllianceRequest[] { return [] } @@ -325,7 +341,7 @@ export class GameView { return false } playerViews(): PlayerView[] { - return Object.values(this.lastUpdate.players).map(data => new PlayerView(this, data)) + return Array.from(this._players.values()) } players(): Player[] { diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 359aab580..74e569537 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -19,8 +19,6 @@ import { DestroyerExecution } from "./DestroyerExecution"; import { PortExecution } from "./PortExecution"; import { MissileSiloExecution } from "./MissileSiloExecution"; import { BattleshipExecution } from "./BattleshipExecution"; -import { PathFinder } from "../pathfinding/PathFinding"; -import { WorkerClient } from "../worker/WorkerClient"; import { DefensePostExecution } from "./DefensePostExecution"; import { CityExecution } from "./CityExecution"; diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index 2aae32b39..bee7f2892 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -4,7 +4,7 @@ import { MainThreadMessage, WorkerMessage, InitializedMessage, - SharesBorderResultMessage + PlayerActionsResultMessage, } from './WorkerMessages'; const ctx: Worker = self as any; @@ -58,21 +58,18 @@ ctx.addEventListener('message', async (e: MessageEvent) => { } break; - case 'shares_border': + case 'player_actions': if (!gameRunner) { throw new Error('Game runner not initialized'); } try { - const game = (await gameRunner).game - const result = game.player(message.player1) - .sharesBorderWith(game.player(message.player2)) - + const actions = (await gameRunner).playerActions(message.playerID, message.x, message.y) sendMessage({ - type: 'shares_border_result', + type: 'player_actions_result', id: message.id, - result - } as SharesBorderResultMessage); + result: actions + } as PlayerActionsResultMessage); } catch (error) { console.error('Failed to check borders:', error); throw error; diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index 5d3ba0894..af0159744 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -1,5 +1,5 @@ -import { PlayerID } from "../game/Game"; -import { GameUpdateViewData } from "../GameView"; +import { PlayerID, Tile } from "../game/Game"; +import { GameUpdateViewData, PlayerActions, PlayerInteraction } from "../GameView"; import { GameConfig, GameID, Turn } from "../Schemas"; import { generateID } from "../Util"; import { WorkerMessage } from "./WorkerMessages"; @@ -29,7 +29,7 @@ export class WorkerClient { break; case 'initialized': - case 'shares_border_result': + case 'player_actions_result': if (message.id && this.messageHandlers.has(message.id)) { const handler = this.messageHandlers.get(message.id)!; handler(message); @@ -85,7 +85,7 @@ export class WorkerClient { }); } - sharesBorderWith(p1: PlayerID, p2: PlayerID): Promise { + playerInteraction(playerID: PlayerID, tile: Tile): Promise { return new Promise((resolve, reject) => { if (!this.isInitialized) { reject(new Error('Worker not initialized')); @@ -95,21 +95,23 @@ export class WorkerClient { const messageId = generateID() this.messageHandlers.set(messageId, (message) => { - if (message.type === 'shares_border_result' && message.result !== undefined) { + if (message.type === 'player_actions_result' && message.result !== undefined) { resolve(message.result); } }); this.worker.postMessage({ - type: 'shares_border', + type: 'player_actions', id: messageId, - player1: p1, - player2: p2 + playerID: playerID, + x: tile.cell().x, + y: tile.cell().y }); }); } + cleanup() { this.worker.terminate(); this.messageHandlers.clear(); diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index c93da57a3..6f068a5ff 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -1,4 +1,4 @@ -import { GameUpdateViewData } from "../GameView"; +import { GameUpdateViewData, PlayerActions, PlayerInteraction } from "../GameView"; import { GameConfig, GameID, Turn } from "../Schemas"; import { PlayerID } from "../game/Game"; @@ -7,8 +7,8 @@ export type WorkerMessageType = | 'initialized' | 'turn' | 'game_update' - | 'shares_border' - | 'shares_border_result'; + | 'player_actions' + | 'player_actions_result'; // Base interface for all messages interface BaseWorkerMessage { @@ -28,12 +28,6 @@ export interface TurnMessage extends BaseWorkerMessage { turn: Turn; } -export interface SharesBorderMessage extends BaseWorkerMessage { - type: 'shares_border'; - player1: PlayerID; - player2: PlayerID; -} - // Messages from worker to main thread export interface InitializedMessage extends BaseWorkerMessage { type: 'initialized'; @@ -44,11 +38,20 @@ export interface GameUpdateMessage extends BaseWorkerMessage { gameUpdate: GameUpdateViewData; } -export interface SharesBorderResultMessage extends BaseWorkerMessage { - type: 'shares_border_result'; - result: boolean; +export interface PlayerActionsMessage extends BaseWorkerMessage { + type: 'player_actions' + playerID: PlayerID + x: number, + y: number +} + +export interface PlayerActionsResultMessage extends BaseWorkerMessage { + type: 'player_actions_result'; + result: PlayerActions; } // Union types for type safety -export type MainThreadMessage = InitMessage | TurnMessage | SharesBorderMessage; -export type WorkerMessage = InitializedMessage | GameUpdateMessage | SharesBorderResultMessage; \ No newline at end of file +export type MainThreadMessage = InitMessage | TurnMessage | PlayerActionsMessage + +// Message send from worker +export type WorkerMessage = InitializedMessage | GameUpdateMessage | PlayerActionsResultMessage; \ No newline at end of file