diff --git a/TODO.txt b/TODO.txt index 4e87f4b14..40e1de4b1 100644 --- a/TODO.txt +++ b/TODO.txt @@ -139,9 +139,12 @@ * make radial menu buttons grayed out DONE 9/27/2024 * add request alliance radial button DONE 9/27/2024 * add break alliance radial button DONE 9/27/2024 -* add send boat radial button +* add send boat radial button DONE 9/27/2024 * attack radial center button only on enemy * Make buttons icons +* better color scheme radial menu +* test on mobile +* make event box work on mobile * add emoji button * buttons greyed out when not active * make alliance request mobile friendly diff --git a/src/client/ClientGame.ts b/src/client/ClientGame.ts index b46b7744a..da7f760ed 100644 --- a/src/client/ClientGame.ts +++ b/src/client/ClientGame.ts @@ -122,7 +122,6 @@ export class ClientGame { this.join() } }; - } public start() { @@ -216,79 +215,15 @@ export class ClientGame { return } - let bordersOcean = false - let bordersEnemy = false if (tile.isLand()) { - const bordersWithDists: Tile[] = [] for (const border of this.myPlayer.borderTiles()) { - if (border.isOceanShore()) { - bordersOcean = true - } for (const n of border.neighbors()) { if (n.owner() == tile.owner()) { - bordersWithDists.push(n) - bordersEnemy = true + this.eventBus.emit(new SendAttackIntentEvent(targetID)) + return } } } - - // Border with enemy sorted by distance to click tile. - const borderWithDists = bordersWithDists.map(t => ({ - dist: manhattanDist(t.cell(), tile.cell()), - tile: t - })).sort((a, b) => a.dist - b.dist); - - const enemyShoreDists = Array.from(bfs( - tile, - and((t) => t.isLand() && t.owner() == tile.owner(), dist(tile, 10)) - )).filter(t => t.isOceanShore()).map(t => ({ - dist: manhattanDist(t.cell(), tile.cell()), - tile: t - })).sort((a, b) => a.dist - b.dist); - - if (!bordersEnemy && !bordersOcean) { - return - } - - let borderTileClosest = 10000000 - let enemyShoreClosest = 10000 - if (borderWithDists.length == 0 && enemyShoreDists.length == 0) { - return - } - - if (bordersWithDists.length > 0) { - borderTileClosest = borderWithDists[0].dist - } - if (enemyShoreDists.length > 0) { - enemyShoreClosest = enemyShoreDists[0].dist - } - if (enemyShoreClosest < borderTileClosest / 6) { - this.eventBus.emit(new SendBoatAttackIntentEvent( - targetID, - enemyShoreDists[0].tile.cell(), - this.gs.config().boatAttackAmount(this.myPlayer, owner) - )) - } else { - this.eventBus.emit(new SendAttackIntentEvent(targetID)) - } - } - - if (tile.isOcean()) { - const bordersOcean = Array.from(this.myPlayer.borderTiles()).filter(t => t.isOceanShore()).length > 0 - if (!bordersOcean) { - return - } - const tn = Array.from(bfs(tile, dist(tile, 10))) - .filter(t => t.isOceanShore()) - .filter(t => t.owner() != this.myPlayer) - .sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell())) - if (tn.length > 0) { - this.eventBus.emit(new SendBoatAttackIntentEvent( - tn[0].owner().id(), - tn[0].cell(), - this.gs.config().boatAttackAmount(this.myPlayer, owner) - )) - } } } } \ No newline at end of file diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index d0760a886..e9e94a709 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -1,8 +1,9 @@ import {EventBus} from "../../../core/EventBus"; import {Cell, Game, Player, PlayerID} from "../../../core/game/Game"; import {ClientID} from "../../../core/Schemas"; +import {manhattanDist, sourceDstOceanShore} from "../../../core/Util"; import {ContextMenuEvent, MouseUpEvent} from "../../InputHandler"; -import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBreakAllianceIntentEvent} from "../../Transport"; +import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent} from "../../Transport"; import {TransformHandler} from "../TransformHandler"; import {MessageType} from "./EventsDisplay"; import {Layer} from "./Layer"; @@ -21,8 +22,8 @@ export class RadialMenu implements Layer { private isVisible: boolean = false; private readonly menuItems = new Map([ [RadialElement.RequestAlliance, {name: "alliance", color: "#3498db", disabled: true, action: () => { }}], - [RadialElement.BreakAlliance, {name: "breakAlliance", color: "#3498db", disabled: true, action: () => { }}], [RadialElement.BoatAttack, {name: "boat", color: "#3498db", disabled: true, action: () => { }}], + [RadialElement.BreakAlliance, {name: "breakAlliance", color: "#3498db", disabled: true, action: () => { }}], ]); private readonly menuSize = 190; private readonly centerButtonSize = 30; @@ -179,42 +180,87 @@ export class RadialMenu implements Layer { item.disabled = true this.updateMenuItemState(item) } - - const cell = this.transformHandler.screenToWorldCoordinates(event.x, event.y) - if (!this.game.isOnMap(cell)) { - return - } - const tile = this.game.tile(cell) - if (!tile.hasOwner()) { - return - } - const owner = tile.owner() as Player - if (owner.clientID() == this.clientID) { - return - } const myPlayer = this.game.players().find(p => p.clientID() == this.clientID) if (!myPlayer) { console.warn('my player not found') return } - if (myPlayer.pendingAllianceRequestWith(owner)) { + + this.clickedCell = this.transformHandler.screenToWorldCoordinates(event.x, event.y) + if (!this.game.isOnMap(this.clickedCell)) { + return + } + const tile = this.game.tile(this.clickedCell) + const other = tile.owner() + + if (tile.hasOwner()) { + const other = tile.owner() as Player + if (other.clientID() == this.clientID) { + return + } + + if (myPlayer.pendingAllianceRequestWith(other)) { + return + } + + if (myPlayer.isAlliedWith(other)) { + this.activateMenuElement(RadialElement.BreakAlliance, () => { + this.eventBus.emit( + new SendBreakAllianceIntentEvent(myPlayer, other) + ) + }) + } else { + this.activateMenuElement(RadialElement.RequestAlliance, () => { + this.eventBus.emit( + new SendAllianceRequestIntentEvent(myPlayer, other) + ) + this.game.displayMessage(`sending alliance request to ${other.name()}`, MessageType.INFO, myPlayer.id()) + }) + } + } + + if (!tile.isLand()) { + return + } + if (myPlayer.boats().length >= this.game.config().boatMaxNumber()) { return } - if (myPlayer.isAlliedWith(owner)) { - this.activateMenuElement(RadialElement.BreakAlliance, () => { - this.eventBus.emit( - new SendBreakAllianceIntentEvent(myPlayer, owner) - ) - }) + let myPlayerBordersOcean = false + for (const bt of myPlayer.borderTiles()) { + if (bt.isOceanShore()) { + myPlayerBordersOcean = true + break + } + } + let otherPlayerBordersOcean = false + if (!tile.hasOwner()) { + otherPlayerBordersOcean = true } else { - this.activateMenuElement(RadialElement.RequestAlliance, () => { - this.eventBus.emit( - new SendAllianceRequestIntentEvent(myPlayer, owner) - ) - this.game.displayMessage(`sending alliance request to ${owner.name()}`, MessageType.INFO, myPlayer.id()) - }) + for (const bt of (other as Player).borderTiles()) { + if (bt.isOceanShore()) { + otherPlayerBordersOcean = true + break + } + } + } + + if (other.isPlayer() && myPlayer.allianceWith(other)) { + return + } + + if (myPlayerBordersOcean && otherPlayerBordersOcean) { + const [src, dst] = sourceDstOceanShore(this.game, myPlayer, other, this.clickedCell) + if (src != null && dst != null) { + if (manhattanDist(src.cell(), dst.cell()) < this.game.config().boatMaxDistance()) { + this.activateMenuElement(RadialElement.BoatAttack, () => { + this.eventBus.emit( + new SendBoatAttackIntentEvent(other.id(), this.clickedCell, null) + ) + }) + } + } } } @@ -266,4 +312,8 @@ export class RadialMenu implements Layer { const gray = rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114; return d3.rgb(gray, gray, gray).toString(); } -} \ No newline at end of file +} + +function closestOceanShoreOwnedByPlayer(attacker: any, targetShore: any) { + throw new Error("Function not implemented."); +} diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 0249506c7..38a2f1708 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -75,7 +75,7 @@ export const BoatAttackIntentSchema = BaseIntentSchema.extend({ type: z.literal('boat'), attackerID: z.string(), targetID: z.string().nullable(), - troops: z.number(), + troops: z.number().nullable(), x: z.number(), y: z.number(), }) diff --git a/src/core/Util.ts b/src/core/Util.ts index ced73311a..7e8f2cf9f 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -1,7 +1,7 @@ import {v4 as uuidv4} from 'uuid'; -import {Cell, Player, Tile} from "./game/Game"; +import {Cell, Game, Player, TerraNullius, Tile} from "./game/Game"; export function manhattanDist(c1: Cell, c2: Cell): number { return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y); @@ -32,6 +32,41 @@ export function and(x: (tile: Tile) => boolean, y: (tile: Tile) => boolean): (ti return (tile: Tile) => x(tile) && y(tile) } +// TODO: refactor to new file +export function sourceDstOceanShore(game: Game, src: Player, dst: Player | TerraNullius, cell: Cell): [Tile | null, Tile | null] { + let srcTile = closestOceanShoreFromPlayer(src, cell, game.width()) + let dstTile: Tile | null = null + if (dst.isPlayer()) { + dstTile = closestOceanShoreFromPlayer(dst as Player, cell, game.width()) + } else { + dstTile = closestOceanShoreTN(game.tile(cell), 300) + } + return [srcTile, dstTile] +} + +function closestOceanShoreFromPlayer(player: Player, target: Cell, width: number): Tile | null { + const shoreTiles = Array.from(player.borderTiles()).filter(t => t.isOceanShore()) + if (shoreTiles.length == 0) { + return null + } + + return shoreTiles.reduce((closest, current) => { + const closestDistance = manhattanDistWrapped(target, closest.cell(), width); + const currentDistance = manhattanDistWrapped(target, current.cell(), width); + return currentDistance < closestDistance ? current : closest; + }); +} + +function closestOceanShoreTN(tile: Tile, searchDist: number): Tile { + const tn = Array.from(bfs(tile, and(t => !t.hasOwner(), dist(tile, searchDist)))) + .filter(t => t.isOceanShore()) + .sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell())) + if (tn.length == 0) { + return null + } + return tn[0] +} + export function bfs(tile: Tile, filter: (tile: Tile) => boolean): Set { const seen = new Set const q: Tile[] = [] diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 013679452..4d1daf00c 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -7,7 +7,7 @@ export const devConfig = new class extends DefaultConfig { return 95 } numSpawnPhaseTurns(): number { - return 80 + return 40 } gameCreationRate(): number { return 2 * 1000 @@ -27,9 +27,9 @@ export const devConfig = new class extends DefaultConfig { // return 10 * 10 // } - // numFakeHumans(gameID: GameID): number { - // return 0 - // } + numFakeHumans(gameID: GameID): number { + return 0 + } // startTroops(playerInfo: PlayerInfo): number { // if (playerInfo.isBot) { diff --git a/src/core/execution/BoatAttackExecution.ts b/src/core/execution/BoatAttackExecution.ts index 00445f2d2..53f4478ab 100644 --- a/src/core/execution/BoatAttackExecution.ts +++ b/src/core/execution/BoatAttackExecution.ts @@ -1,9 +1,7 @@ import {PriorityQueue} from "@datastructures-js/priority-queue"; import {Boat, Cell, Execution, MutableBoat, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, Tile, TileEvent} from "../game/Game"; -import {manhattanDist, manhattanDistWrapped} from "../Util"; +import {and, bfs, manhattanDistWrapped, sourceDstOceanShore} from "../Util"; import {AttackExecution} from "./AttackExecution"; -import {Config} from "../configuration/Config"; -import {EventBus} from "../EventBus"; import {DisplayMessageEvent, MessageType} from "../../client/graphics/layers/EventsDisplay"; export class BoatAttackExecution implements Execution { @@ -21,8 +19,8 @@ export class BoatAttackExecution implements Execution { // TODO make private public path: Tile[] - private src: Tile - private dst: Tile + private src: Tile | null + private dst: Tile | null private currTileIndex: number = 0 @@ -37,7 +35,7 @@ export class BoatAttackExecution implements Execution { private attackerID: PlayerID, private targetID: PlayerID | null, private cell: Cell, - private troops: number, + private troops: number | null, ) { } activeDuringSpawnPhase(): boolean { @@ -63,11 +61,16 @@ export class BoatAttackExecution implements Execution { this.target = mg.player(this.targetID) } + if (this.troops == null) { + this.troops = this.mg.config().boatAttackAmount(this.attacker, this.target) + } + this.troops = Math.min(this.troops, this.attacker.troops()) this.attacker.removeTroops(this.troops) - this.src = this.closestShoreTile(this.attacker, this.cell) - this.dst = this.mg.tile(this.cell) + const [srcTile, dstTile]: [Tile | null, Tile | null] = sourceDstOceanShore(this.mg, this.attacker, this.target, this.cell); + this.src = srcTile + this.dst = dstTile if (this.src == null || this.dst == null) { this.active = false @@ -135,18 +138,6 @@ export class BoatAttackExecution implements Execution { return this.active } - private closestShoreTile(player: Player, target: Cell): Tile | null { - const shoreTiles = Array.from(player.borderTiles()).filter(t => t.isOceanShore()) - if (shoreTiles.length == 0) { - return null - } - - return shoreTiles.reduce((closest, current) => { - const closestDistance = manhattanDistWrapped(target, closest.cell(), this.mg.width()); - const currentDistance = manhattanDistWrapped(target, current.cell(), this.mg.width()); - return currentDistance < closestDistance ? current : closest; - }); - } } export class AStar {