From 7c5ebaf4561307f601f640dcb85ec2343c60d89d Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 16 Sep 2024 17:09:01 -0700 Subject: [PATCH] created right click handler, refacter renderers --- TODO.txt | 2 +- src/client/ClientGame.ts | 15 +- src/client/InputHandler.ts | 35 ++++ src/client/graphics/GameRenderer.ts | 195 +++++------------------ src/client/graphics/Layer.ts | 8 + src/client/graphics/NameRenderer.ts | 19 ++- src/client/graphics/TerrainRenderer.ts | 11 +- src/client/graphics/TerritoryRenderer.ts | 16 +- src/client/graphics/TransformHandler.ts | 94 +++++++++++ src/client/graphics/UIRenderer.ts | 79 ++++++++- src/client/graphics/Utils.ts | 14 ++ src/client/index.html | 6 + src/client/styles.css | 24 +++ 13 files changed, 335 insertions(+), 183 deletions(-) create mode 100644 src/client/graphics/Layer.ts create mode 100644 src/client/graphics/TransformHandler.ts diff --git a/TODO.txt b/TODO.txt index 63541cc06..f9a7bd6c5 100644 --- a/TODO.txt +++ b/TODO.txt @@ -109,13 +109,13 @@ * Make boats more intuitive (larger area to click off coast) DONE 9/11/2024 * FakeHumans retaliate when attacked DONE 9/11/2024 ---- v3 Release DONE * Add discord link DONE 9/14/2024 * front page mobile friendly DONE 9/15/2024 * game mobile friendly DONE 9/16/2024 * UI: basic win condition & popup DONE 9/16/2024 * right click popup alliance option +* make fake humans easier * click alliance sends alliance request * notification for alliance request * comfirm alliance diff --git a/src/client/ClientGame.ts b/src/client/ClientGame.ts index 38c5da72b..4c5d071f0 100644 --- a/src/client/ClientGame.ts +++ b/src/client/ClientGame.ts @@ -3,7 +3,7 @@ import {Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Play import {createGame} from "../core/GameImpl"; import {EventBus} from "../core/EventBus"; import {Config} from "../core/configuration/Config"; -import {GameRenderer} from "./graphics/GameRenderer"; +import {createRenderer, GameRenderer} from "./graphics/GameRenderer"; import {InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent} from "./InputHandler" import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn} from "../core/Schemas"; import {TerrainMap} from "../core/TerrainMapLoader"; @@ -16,8 +16,7 @@ import {WinCheckExecution} from "../core/execution/WinCheckExecution"; export function createClientGame(name: string, clientID: ClientID, playerID: PlayerID, ip: string | null, gameID: GameID, config: Config, terrainMap: TerrainMap): ClientGame { let eventBus = new EventBus() let game = createGame(terrainMap, eventBus, config) - let terrainRenderer = new TerrainRenderer(game) - let gameRenderer = new GameRenderer(eventBus, game, clientID, terrainRenderer) + let gameRenderer = createRenderer(game, eventBus, clientID) return new ClientGame( name, @@ -119,14 +118,9 @@ export class ClientGame { public start() { console.log('version 3') this.isActive = true - // TODO: make each class do this, or maybe have client intercept all requests? - //this.eventBus.on(TickEvent, (e) => this.tick(e)) - this.eventBus.on(TileEvent, (e) => this.renderer.tileUpdate(e)) + this.eventBus.on(PlayerEvent, (e) => this.playerEvent(e)) - this.eventBus.on(BoatEvent, (e) => this.renderer.boatEvent(e)) this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e)) - this.eventBus.on(ZoomEvent, (e) => this.renderer.onZoom(e)) - this.eventBus.on(DragEvent, (e) => this.renderer.onMove(e)) this.renderer.initialize() this.input.initialize() @@ -179,14 +173,13 @@ export class ClientGame { console.log('setting name') this.myPlayer = event.player } - this.renderer.playerEvent(event) } private inputEvent(event: MouseUpEvent) { if (!this.isActive) { return } - const cell = this.renderer.screenToWorldCoordinates(event.x, event.y) + const cell = this.renderer.transformHandler.screenToWorldCoordinates(event.x, event.y) if (!this.gs.isOnMap(cell)) { return } diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 13faf254f..1d6c781d3 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -15,6 +15,13 @@ export class MouseDownEvent implements GameEvent { ) { } } +export class RightClickEvent implements GameEvent { + constructor( + public readonly x: number, + public readonly y: number, + ) { } +} + export class ZoomEvent implements GameEvent { constructor( public readonly x: number, @@ -32,6 +39,8 @@ export class DragEvent implements GameEvent { export class InputHandler { + private contextMenuActive = false + private lastPointerX: number = 0; private lastPointerY: number = 0; @@ -51,10 +60,23 @@ export class InputHandler { document.addEventListener("pointerup", (e) => this.onPointerUp(e)); document.addEventListener("wheel", (e) => this.onScroll(e), {passive: false}); document.addEventListener('pointermove', this.onPointerMove.bind(this)); + document.addEventListener('contextmenu', (e: MouseEvent) => { + this.onRightClick(e) + }); + this.pointers.clear() } private onPointerDown(event: PointerEvent) { + if (this.contextMenuActive) { + this.contextMenuActive = false + return + } + + if (event.button > 0) { + return + } + this.pointerDown = true this.pointers.set(event.pointerId, event); @@ -72,6 +94,9 @@ export class InputHandler { } onPointerUp(event: PointerEvent) { + if (event.button > 0) { + return + } this.pointerDown = false this.pointers.delete(event.pointerId); const dist = Math.abs(event.x - this.lastPointerDownX) + Math.abs(event.y - this.lastPointerDownY); @@ -85,6 +110,10 @@ export class InputHandler { } private onPointerMove(event: PointerEvent) { + if (event.button > 0) { + return + } + this.pointers.set(event.pointerId, event); @@ -112,6 +141,12 @@ export class InputHandler { } } + private onRightClick(event: MouseEvent) { + event.preventDefault() + this.contextMenuActive = true + this.eventBus.emit(new RightClickEvent(event.clientX, event.clientY)) + } + private getPinchDistance(): number { const pointerEvents = Array.from(this.pointers.values()); const dx = pointerEvents[0].clientX - pointerEvents[1].clientX; diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 7da863126..db2184fc4 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -6,64 +6,44 @@ import {NameRenderer} from "./NameRenderer"; import {TerrainRenderer} from "./TerrainRenderer"; import {TerritoryRenderer} from "./TerritoryRenderer"; import {ClientID} from "../../core/Schemas"; -import {renderTroops} from "./Utils"; +import {createCanvas, renderTroops} from "./Utils"; import {UIRenderer} from "./UIRenderer"; import {EventBus} from "../../core/EventBus"; +import {TransformHandler} from "./TransformHandler"; +import {Layer} from "./Layer"; + + +export function createRenderer(game: Game, eventBus: EventBus, clientID: ClientID): GameRenderer { + const canvas = createCanvas() + const transformHandler = new TransformHandler(game, eventBus, canvas.getBoundingClientRect()) + + const layers: Layer[] = [ + new TerrainRenderer(game), + new TerritoryRenderer(game, eventBus), + new NameRenderer(game, game.config().theme()), + new UIRenderer(eventBus, game, game.config().theme(), clientID) + ] + + return new GameRenderer(game, eventBus, canvas, transformHandler, layers) +} + export class GameRenderer { - private territoryCanvas: HTMLCanvasElement - private canvas: HTMLCanvasElement - - private territoryContext: CanvasRenderingContext2D - - private scale: number = 1.8 - private offsetX: number = -350 - private offsetY: number = -200 private context: CanvasRenderingContext2D - private nameRenderer: NameRenderer; - private territoryRenderer: TerritoryRenderer; - private uiRenderer: UIRenderer; - - private theme: Theme - - - constructor(private eventBus: EventBus, private gs: Game, private clientID: ClientID, private terrainRenderer: TerrainRenderer) { - this.theme = gs.config().theme() - this.nameRenderer = new NameRenderer(gs, this.theme) - this.territoryRenderer = new TerritoryRenderer(gs) - this.uiRenderer = new UIRenderer(eventBus, gs, this.theme, clientID) + constructor(private game: Game, private eventBus: EventBus, private canvas: HTMLCanvasElement, public transformHandler: TransformHandler, private layers: Layer[]) { + this.context = canvas.getContext("2d") } initialize() { - this.canvas = document.createElement('canvas'); - this.context = this.canvas.getContext('2d'); - - // Set canvas style to fill the screen - this.canvas.style.position = 'fixed'; - this.canvas.style.left = '0'; - this.canvas.style.top = '0'; - this.canvas.style.width = '100%'; - this.canvas.style.height = '100%'; - this.canvas.style.touchAction = 'none'; - - this.nameRenderer.initialize() - this.terrainRenderer.init() - this.territoryRenderer.init() - this.uiRenderer.init() - + this.layers.forEach(l => l.init()) document.body.appendChild(this.canvas); window.addEventListener('resize', () => this.resizeCanvas()); this.resizeCanvas(); - - this.territoryCanvas = document.createElement('canvas') - this.territoryCanvas.width = this.gs.width(); - this.territoryCanvas.height = this.gs.height(); - this.territoryContext = this.territoryCanvas.getContext('2d') - this.territoryContext.globalAlpha = 0.4; + this.transformHandler = new TransformHandler(this.game, this.eventBus, this.canvas.getBoundingClientRect()) requestAnimationFrame(() => this.renderGame()); } @@ -76,55 +56,43 @@ export class GameRenderer { renderGame() { // Set background - this.context.fillStyle = this.theme.backgroundColor().toHex(); + this.context.fillStyle = this.game.config().theme().backgroundColor().toHex(); this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); // Save the current context state this.context.save(); + this.transformHandler.handleTransform(this.context) - // Disable image smoothing for pixelated effect - if (this.scale > 3) { - this.context.imageSmoothingEnabled = false; - } else { - this.context.imageSmoothingEnabled = true; - } - - // Apply zoom and pan - this.context.setTransform( - this.scale, - 0, - 0, - this.scale, - this.gs.width() / 2 - this.offsetX * this.scale, - this.gs.height() / 2 - this.offsetY * this.scale - ); - - this.terrainRenderer.draw(this.context) - this.territoryRenderer.draw(this.context) - - const [upperLeft, bottomRight] = this.boundingRect() - this.nameRenderer.render(this.context, this.scale, upperLeft, bottomRight) + this.layers.forEach(l => { + if (l.shouldTransform()) { + l.render(this.context, this.transformHandler) + } + }) this.context.restore() + this.layers.forEach(l => { + if (!l.shouldTransform()) { + l.render(this.context, this.transformHandler) + } + }) + this.renderSpawnBar() - this.uiRenderer.render(this.context) requestAnimationFrame(() => this.renderGame()); } - // TODO: move to UIRenderer renderSpawnBar() { - if (!this.gs.inSpawnPhase()) { + if (!this.game.inSpawnPhase()) { return } const barHeight = 15; const barBackgroundWidth = this.canvas.width; - const ratio = this.gs.ticks() / this.gs.config().numSpawnPhaseTurns() + const ratio = this.game.ticks() / this.game.config().numSpawnPhaseTurns() // Draw bar background this.context.fillStyle = 'rgba(0, 0, 0, 0.5)'; @@ -135,19 +103,7 @@ export class GameRenderer { } tick() { - this.nameRenderer.tick() - } - - tileUpdate(event: TileEvent) { - this.territoryRenderer.tileUpdate(event) - // this.tileToRenderQueue.push({tileEvent: event, lastUpdate: this.gs.ticks() + this.random.nextFloat(0, .5)}) - } - - playerEvent(event: PlayerEvent) { - } - - boatEvent(event: BoatEvent) { - this.territoryRenderer.boatEvent(event) + this.layers.forEach(l => l.tick()) } resize(width: number, height: number): void { @@ -155,77 +111,4 @@ export class GameRenderer { this.canvas.height = Math.ceil(height / window.devicePixelRatio); } - paintCell(cell: Cell, color: Colord) { - color = color.alpha(10) // Assign the result back to color - this.territoryContext.fillStyle = color.toHslString() - this.territoryContext.fillRect(cell.x, cell.y, 1, 1); - } - - clearCell(cell: Cell) { - this.territoryContext.clearRect(cell.x, cell.y, 1, 1); - } - - onZoom(event: ZoomEvent) { - const oldScale = this.scale; - const zoomFactor = 1 + event.delta / 600; - this.scale /= zoomFactor; - - // Clamp the scale to prevent extreme zooming - this.scale = Math.max(0.5, Math.min(20, this.scale)); - - const canvasRect = this.canvas.getBoundingClientRect(); - const canvasX = event.x - canvasRect.left; - const canvasY = event.y - canvasRect.top; - - // Calculate the world point we want to zoom towards - const zoomPointX = (canvasX - this.gs.width() / 2) / oldScale + this.offsetX; - const zoomPointY = (canvasY - this.gs.height() / 2) / oldScale + this.offsetY; - - // Adjust the offset - this.offsetX = zoomPointX - (canvasX - this.gs.width() / 2) / this.scale; - this.offsetY = zoomPointY - (canvasY - this.gs.height() / 2) / this.scale; - } - - onMove(event: DragEvent) { - this.offsetX -= event.deltaX / this.scale; - this.offsetY -= event.deltaY / this.scale; - } - - - screenToWorldCoordinates(screenX: number, screenY: number): Cell { - - const canvasRect = this.canvas.getBoundingClientRect(); - const canvasX = screenX - canvasRect.left; - const canvasY = screenY - canvasRect.top; - - // Calculate the world point we want to zoom towards - const centerX = (canvasX - this.gs.width() / 2) / this.scale + this.offsetX; - const centerY = (canvasY - this.gs.height() / 2) / this.scale + this.offsetY; - - const gameX = centerX + this.gs.width() / 2 - const gameY = centerY + this.gs.height() / 2 - - return new Cell(Math.floor(gameX), Math.floor(gameY)); - } - - boundingRect(): [Cell, Cell] { - - // Calculate the world point we want to zoom towards - const LeftX = (- this.gs.width() / 2) / this.scale + this.offsetX; - const TopY = (- this.gs.height() / 2) / this.scale + this.offsetY; - - const gameLeftX = LeftX + this.gs.width() / 2 - const gameTopY = TopY + this.gs.height() / 2 - - - // Calculate the world point we want to zoom towards - const rightX = (screen.width - this.gs.width() / 2) / this.scale + this.offsetX; - const rightY = (screen.height - this.gs.height() / 2) / this.scale + this.offsetY; - - const gameRightX = rightX + this.gs.width() / 2 - const gameBottomY = rightY + this.gs.height() / 2 - - return [new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)), new Cell(Math.floor(gameRightX), Math.floor(gameBottomY))] - } - } \ No newline at end of file diff --git a/src/client/graphics/Layer.ts b/src/client/graphics/Layer.ts new file mode 100644 index 000000000..1ca1545b2 --- /dev/null +++ b/src/client/graphics/Layer.ts @@ -0,0 +1,8 @@ +import {TransformHandler} from "./TransformHandler" + +export interface Layer { + init() + tick() + render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) + shouldTransform(): boolean +} \ No newline at end of file diff --git a/src/client/graphics/NameRenderer.ts b/src/client/graphics/NameRenderer.ts index d6adb3e96..fdfd9323e 100644 --- a/src/client/graphics/NameRenderer.ts +++ b/src/client/graphics/NameRenderer.ts @@ -2,7 +2,9 @@ import {Cell, Game, Player, PlayerType} from "../../core/Game" import {PseudoRandom} from "../../core/PseudoRandom" import {calculateBoundingBox} from "../../core/Util" import {Theme} from "../../core/configuration/Config" +import {Layer} from "./Layer" import {placeName} from "./NameBoxCalculator" +import {TransformHandler} from "./TransformHandler" import {renderTroops} from "./Utils" class RenderInfo { @@ -17,7 +19,7 @@ class RenderInfo { ) { } } -export class NameRenderer { +export class NameRenderer implements Layer { private lastChecked = 0 private refreshRate = 1000 @@ -32,9 +34,12 @@ export class NameRenderer { constructor(private game: Game, private theme: Theme) { } + shouldTransform(): boolean { + return true + } - public initialize() { + public init() { this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); @@ -45,6 +50,7 @@ export class NameRenderer { this.canvas.height = this.game.height(); } + // TODO: remove tick, move this to render public tick() { const now = Date.now() if (now - this.lastChecked > this.refreshRate) { @@ -74,11 +80,12 @@ export class NameRenderer { } } - public render(mainContex: CanvasRenderingContext2D, scale: number, uppperLeft: Cell, bottomRight: Cell) { + public render(mainContex: CanvasRenderingContext2D, transformHandler: TransformHandler) { + const [upperLeft, bottomRight] = transformHandler.screenBoundingRect() for (const render of this.renders) { - render.isVisible = this.isVisible(render, uppperLeft, bottomRight) - if (render.player.isAlive() && render.isVisible && render.fontSize * scale > 10) { - this.renderPlayerInfo(render, mainContex, scale, uppperLeft, bottomRight) + render.isVisible = this.isVisible(render, upperLeft, bottomRight) + if (render.player.isAlive() && render.isVisible && render.fontSize * transformHandler.scale > 10) { + this.renderPlayerInfo(render, mainContex, transformHandler.scale, upperLeft, bottomRight) } } } diff --git a/src/client/graphics/TerrainRenderer.ts b/src/client/graphics/TerrainRenderer.ts index 1c2fb9d4e..44a250ba2 100644 --- a/src/client/graphics/TerrainRenderer.ts +++ b/src/client/graphics/TerrainRenderer.ts @@ -1,14 +1,21 @@ import {inherits} from "util" import {Game} from "../../core/Game"; import {throws} from "assert"; +import {Layer} from "./Layer"; +import {TransformHandler} from "./TransformHandler"; -export class TerrainRenderer { +export class TerrainRenderer implements Layer { private canvas: HTMLCanvasElement private context: CanvasRenderingContext2D private imageData: ImageData constructor(private game: Game) { } + shouldTransform(): boolean { + return true + } + tick() { + } init() { this.canvas = document.createElement('canvas'); @@ -34,7 +41,7 @@ export class TerrainRenderer { }) } - draw(context: CanvasRenderingContext2D) { + render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) { context.drawImage( this.canvas, -this.game.width() / 2, diff --git a/src/client/graphics/TerritoryRenderer.ts b/src/client/graphics/TerritoryRenderer.ts index 5321564e9..ffb245859 100644 --- a/src/client/graphics/TerritoryRenderer.ts +++ b/src/client/graphics/TerritoryRenderer.ts @@ -4,8 +4,11 @@ import {PseudoRandom} from "../../core/PseudoRandom"; import {Colord} from "colord"; import {bfs, dist} from "../../core/Util"; import {Theme} from "../../core/configuration/Config"; +import {Layer} from "./Layer"; +import {TransformHandler} from "./TransformHandler"; +import {EventBus} from "../../core/EventBus"; -export class TerritoryRenderer { +export class TerritoryRenderer implements Layer { private canvas: HTMLCanvasElement private context: CanvasRenderingContext2D private imageData: ImageData @@ -17,8 +20,15 @@ export class TerritoryRenderer { private boatToTrail = new Map>() - constructor(private game: Game) { + constructor(private game: Game, eventBus: EventBus) { this.theme = game.config().theme() + eventBus.on(TileEvent, e => this.tileUpdate(e)) + eventBus.on(BoatEvent, e => this.boatEvent(e)) + } + shouldTransform(): boolean { + return true + } + tick() { } init() { @@ -40,7 +50,7 @@ export class TerritoryRenderer { }) } - draw(context: CanvasRenderingContext2D) { + render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) { this.renderTerritory() this.context.putImageData(this.imageData, 0, 0); context.drawImage( diff --git a/src/client/graphics/TransformHandler.ts b/src/client/graphics/TransformHandler.ts new file mode 100644 index 000000000..8562fd1b9 --- /dev/null +++ b/src/client/graphics/TransformHandler.ts @@ -0,0 +1,94 @@ +import {EventBus} from "../../core/EventBus" +import {Cell, Game} from "../../core/Game"; +import {ZoomEvent, DragEvent} from "../InputHandler"; + +export class TransformHandler { + public scale: number = 1.8 + private offsetX: number = -350 + private offsetY: number = -200 + + constructor(private game: Game, private eventBus: EventBus, private boundingRect: DOMRect) { + this.eventBus.on(ZoomEvent, (e) => this.onZoom(e)) + this.eventBus.on(DragEvent, (e) => this.onMove(e)) + } + + handleTransform(context: CanvasRenderingContext2D) { + // Disable image smoothing for pixelated effect + if (this.scale > 3) { + context.imageSmoothingEnabled = false; + } else { + context.imageSmoothingEnabled = true; + } + + // Apply zoom and pan + context.setTransform( + this.scale, + 0, + 0, + this.scale, + this.game.width() / 2 - this.offsetX * this.scale, + this.game.height() / 2 - this.offsetY * this.scale + ); + } + + screenToWorldCoordinates(screenX: number, screenY: number): Cell { + const canvasRect = this.boundingRect; + const canvasX = screenX - canvasRect.left; + const canvasY = screenY - canvasRect.top; + + // Calculate the world point we want to zoom towards + const centerX = (canvasX - this.game.width() / 2) / this.scale + this.offsetX; + const centerY = (canvasY - this.game.height() / 2) / this.scale + this.offsetY; + + const gameX = centerX + this.game.width() / 2 + const gameY = centerY + this.game.height() / 2 + + return new Cell(Math.floor(gameX), Math.floor(gameY)); + } + + screenBoundingRect(): [Cell, Cell] { + + // Calculate the world point we want to zoom towards + const LeftX = (- this.game.width() / 2) / this.scale + this.offsetX; + const TopY = (- this.game.height() / 2) / this.scale + this.offsetY; + + const gameLeftX = LeftX + this.game.width() / 2 + const gameTopY = TopY + this.game.height() / 2 + + + // Calculate the world point we want to zoom towards + const rightX = (screen.width - this.game.width() / 2) / this.scale + this.offsetX; + const rightY = (screen.height - this.game.height() / 2) / this.scale + this.offsetY; + + const gameRightX = rightX + this.game.width() / 2 + const gameBottomY = rightY + this.game.height() / 2 + + return [new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)), new Cell(Math.floor(gameRightX), Math.floor(gameBottomY))] + } + + onZoom(event: ZoomEvent) { + const oldScale = this.scale; + const zoomFactor = 1 + event.delta / 600; + this.scale /= zoomFactor; + + // Clamp the scale to prevent extreme zooming + this.scale = Math.max(0.5, Math.min(20, this.scale)); + + const canvasRect = this.boundingRect + const canvasX = event.x - canvasRect.left; + const canvasY = event.y - canvasRect.top; + + // Calculate the world point we want to zoom towards + const zoomPointX = (canvasX - this.game.width() / 2) / oldScale + this.offsetX; + const zoomPointY = (canvasY - this.game.height() / 2) / oldScale + this.offsetY; + + // Adjust the offset + this.offsetX = zoomPointX - (canvasX - this.game.width() / 2) / this.scale; + this.offsetY = zoomPointY - (canvasY - this.game.height() / 2) / this.scale; + } + + onMove(event: DragEvent) { + this.offsetX -= event.deltaX / this.scale; + this.offsetY -= event.deltaY / this.scale; + } +} \ No newline at end of file diff --git a/src/client/graphics/UIRenderer.ts b/src/client/graphics/UIRenderer.ts index 5a1a87a3f..5c6c9c8a9 100644 --- a/src/client/graphics/UIRenderer.ts +++ b/src/client/graphics/UIRenderer.ts @@ -5,20 +5,61 @@ import {Game, Player} from "../../core/Game"; import {ClientID} from "../../core/Schemas"; import {renderTroops} from "./Utils"; import winModalHtml from '../WinModal.html'; +import {RightClickEvent} from "../InputHandler"; +import {Layer} from "./Layer"; +import {TransformHandler} from "./TransformHandler"; -export class UIRenderer { + +interface MenuOption { + label: string; + action: () => void; +} + +export class UIRenderer implements Layer { private exitButton: HTMLButtonElement; private winModal: HTMLElement | null = null; + private customMenu = document.getElementById('customMenu'); + constructor(private eventBus: EventBus, private game: Game, private theme: Theme, private clientID: ClientID) { + } + render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) { + } + + shouldTransform(): boolean { + return false + } + + tick() { } init() { this.createExitButton() this.createWinModal() + this.initRightClickMenu() this.eventBus.on(WinEvent, (e) => this.onWinEvent(e)) + this.eventBus.on(RightClickEvent, (e) => this.onRightClick(e)) + } + + initRightClickMenu() { + if (!this.customMenu) { + console.error('Custom menu not found'); + return; + } + + document.addEventListener('click', () => { + this.customMenu!.style.display = 'none'; + }); + + const menuItems = this.customMenu.querySelectorAll('li'); + menuItems.forEach(item => { + item.addEventListener('click', () => { + alert(`You clicked: ${item.textContent}`); + this.customMenu!.style.display = 'none'; + }); + }); } createWinModal() { @@ -124,9 +165,6 @@ export class UIRenderer { document.body.appendChild(this.exitButton); } - render(context: CanvasRenderingContext2D) { - } - onWinEvent(event: WinEvent) { console.log(`${event.winner.name()} won the game!!}`) @@ -165,4 +203,37 @@ export class UIRenderer { window.location.reload(); } + private onRightClick(e: RightClickEvent) { + this.customMenu!.style.display = 'block'; + this.customMenu!.style.left = `${e.x}px`; + this.customMenu!.style.top = `${e.y}px`; + } + + private populateMenu(options: MenuOption[]) { + if (!this.customMenu) return; + + // Clear existing menu items + this.customMenu.innerHTML = ''; + + // Create new menu items + const ul = document.createElement('ul'); + options.forEach(option => { + const li = document.createElement('li'); + li.textContent = option.label; + li.onclick = () => { + option.action(); + this.hideMenu(); + }; + ul.appendChild(li); + }); + + this.customMenu.appendChild(ul); + } + + private hideMenu() { + if (this.customMenu) { + this.customMenu.style.display = 'none'; + } + } + } \ No newline at end of file diff --git a/src/client/graphics/Utils.ts b/src/client/graphics/Utils.ts index ddabb5383..f22b7b72b 100644 --- a/src/client/graphics/Utils.ts +++ b/src/client/graphics/Utils.ts @@ -14,4 +14,18 @@ export function renderTroops(troops: number): string { troopsStr = String(Math.floor(troops)) } return troopsStr +} + +export function createCanvas(): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + + // Set canvas style to fill the screen + canvas.style.position = 'fixed'; + canvas.style.left = '0'; + canvas.style.top = '0'; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + canvas.style.touchAction = 'none'; + + return canvas } \ No newline at end of file diff --git a/src/client/index.html b/src/client/index.html index 1d0cb812f..4ef958d42 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -41,6 +41,12 @@

+
+
    +
+
+ +