diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 7a1cf099a..01edf5163 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -80,7 +80,7 @@ export function createRenderer(canvas: HTMLCanvasElement, game: GameView, eventB new TerritoryLayer(game, eventBus), new StructureLayer(game, eventBus), new UnitLayer(game, eventBus, clientID), - new NameLayer(game, eventBus, game.config().theme(), transformHandler, clientID), + new NameLayer(game, game.config().theme(), transformHandler, clientID), new UILayer(eventBus, game, clientID, transformHandler), eventsDisplay, new RadialMenu(eventBus, game, transformHandler, clientID, emojiTable as EmojiTable, buildMenu, uiState), diff --git a/src/client/graphics/TransformHandler.ts b/src/client/graphics/TransformHandler.ts index 860182ccf..d7a72e7ea 100644 --- a/src/client/graphics/TransformHandler.ts +++ b/src/client/graphics/TransformHandler.ts @@ -14,6 +14,7 @@ export class TransformHandler { private target: Cell private intervalID = null + private changed = false constructor(private game: GameView, private eventBus: EventBus, private canvas: HTMLCanvasElement) { this.eventBus.on(ZoomEvent, (e) => this.onZoom(e)) @@ -28,6 +29,9 @@ export class TransformHandler { width(): number { return this.boundingRect().width } + hasChanged(): boolean { + return this.changed + } handleTransform(context: CanvasRenderingContext2D) { // Disable image smoothing for pixelated effect @@ -43,6 +47,32 @@ export class TransformHandler { this.game.width() / 2 - this.offsetX * this.scale, this.game.height() / 2 - this.offsetY * this.scale ); + this.changed = false + } + + worldToScreenCoordinates(cell: Cell): { x: number, y: number } { + // Step 1: Convert from Cell coordinates to game coordinates + // (reverse of Math.floor operation - we'll use the exact values) + const gameX = cell.x; + const gameY = cell.y; + + // Step 2: Reverse the game center offset calculation + // Original: gameX = centerX + this.game.width() / 2 + // Therefore: centerX = gameX - this.game.width() / 2 + const centerX = gameX - this.game.width() / 2; + const centerY = gameY - this.game.height() / 2; + + // Step 3: Reverse the world point calculation + // Original: centerX = (canvasX - this.game.width() / 2) / this.scale + this.offsetX + // Therefore: canvasX = (centerX - this.offsetX) * this.scale + this.game.width() / 2 + const canvasX = (centerX - this.offsetX) * this.scale + this.game.width() / 2; + const canvasY = (centerY - this.offsetY) * this.scale + this.game.height() / 2; + + // Step 4: Convert canvas coordinates back to screen coordinates + const canvasRect = this.boundingRect(); + const screenX = canvasX + canvasRect.left; + const screenY = canvasY + canvasRect.top; + return { x: screenX, y: screenY } } screenToWorldCoordinates(screenX: number, screenY: number): Cell { @@ -78,6 +108,11 @@ export class TransformHandler { return [new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)), new Cell(Math.floor(gameRightX), Math.floor(gameBottomY))] } + isOnScreen(cell: Cell): boolean { + const [topLeft, bottomRight] = this.screenBoundingRect() + return cell.x > topLeft.x && cell.x < bottomRight.x && cell.y > topLeft.y && cell.y < bottomRight.y + } + screenCenter(): { screenX: number, screenY: number } { const [upperLeft, bottomRight] = this.screenBoundingRect() return { @@ -121,6 +156,7 @@ export class TransformHandler { this.offsetY += offsetDy } } + this.changed = true } onZoom(event: ZoomEvent) { @@ -143,12 +179,14 @@ export class TransformHandler { // Adjust the offset this.offsetX = zoomPointX - (canvasX - this.game.width() / 2) / this.scale; this.offsetY = zoomPointY - (canvasY - this.game.height() / 2) / this.scale; + this.changed = true } onMove(event: DragEvent) { this.clearTarget() this.offsetX -= event.deltaX / this.scale; this.offsetY -= event.deltaY / this.scale; + this.changed = true } private clearTarget() { diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index 24dfd5f80..63bd1d4f2 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -1,37 +1,39 @@ -import { AllPlayers, Cell, Game, Player, PlayerType, Tick } from "../../../core/game/Game" +import { AllPlayers, Cell, Game, Player, PlayerType } from "../../../core/game/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" import traitorIcon from '../../../../resources/images/TraitorIcon.png'; import allianceIcon from '../../../../resources/images/AllianceIcon.png'; import crownIcon from '../../../../resources/images/CrownIcon.png'; import targetIcon from '../../../../resources/images/TargetIcon.png'; import { ClientID } from "../../../core/Schemas" -import { EventBus } from "../../../core/EventBus" -import { AlternateViewEvent } from "../../InputHandler" import { GameView, PlayerView } from "../../../core/GameView" +import { createCanvas, renderTroops } from "../../Utils" class RenderInfo { - public isVisible = true + public icons: Map = new Map() // Track icon elements + constructor( - public player: Player, - public lastRenderCalcTick: Tick, - public lastBoundingCalculatedTick: Tick, - public boundingBox: { min: Cell, max: Cell }, + public player: PlayerView, + public lastRenderCalc: number, public location: Cell, - public fontSize: number + public fontSize: number, + public element: HTMLElement ) { } } export class NameLayer implements Layer { + private canvas: HTMLCanvasElement + private context: CanvasRenderingContext2D + private lastChecked = 0 - private refreshRate = 1000 + + private renderCheckRate = 100 + private renderRefreshRate = 500 private rand = new PseudoRandom(10) private renders: RenderInfo[] = [] @@ -41,21 +43,19 @@ export class NameLayer implements Layer { private targetIconImage: HTMLImageElement; private crownIconImage: HTMLImageElement; + private container: HTMLDivElement + + private myPlayer: Player | null = null private firstPlace: Player | null = null - private alternateView = false + private lastUpdate = 0 + private updateFrequency = 250 - constructor( - private game: GameView, - private eventBus: EventBus, - private theme: Theme, - private transformHandler: TransformHandler, - private clientID: ClientID - ) { - this.eventBus.on(AlternateViewEvent, e => { this.alternateView = e.alternateView }) + private lastRect = null; + constructor(private game: GameView, private theme: Theme, private transformHandler: TransformHandler, private clientID: ClientID) { this.traitorIconImage = new Image(); this.traitorIconImage.src = traitorIcon; @@ -69,159 +69,219 @@ export class NameLayer implements Layer { this.targetIconImage.src = targetIcon } + resizeCanvas() { + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + //this.redraw() + } + + shouldTransform(): boolean { - return true + return false } public init() { + // this.canvas = document.createElement('canvas'); + this.canvas = createCanvas() + this.context = this.canvas.getContext("2d") + + + window.addEventListener('resize', () => this.resizeCanvas()); + this.resizeCanvas(); + + this.container = document.createElement('div') + this.container.style.position = 'fixed' + this.container.style.left = '50%' + this.container.style.top = '50%' + this.container.style.pointerEvents = 'none' // Don't interfere with game interaction + this.container.style.zIndex = '1000' // Add this line + document.body.appendChild(this.container) } - // TODO: remove tick, move this to render public tick() { - const now = Date.now() - if (now - this.lastChecked > this.refreshRate) { - this.lastChecked = now - - const sorted = this.game.players().sort((a, b) => b.numTilesOwned() - a.numTilesOwned()) - if (sorted.length > 0) { - this.firstPlace = sorted[0] - } - - this.renders = this.renders.filter(r => r.player.isAlive()) - for (const player of this.game.players()) { - if (player.isAlive()) { - if (!this.seenPlayers.has(player)) { - this.seenPlayers.add(player) - this.renders.push(new RenderInfo(player, 0, 0, null, null, 0)) - } - } else { - this.seenPlayers.delete(player) - } - } + if (this.game.ticks() % 10 != 0) { + return } - const currTick = this.game.ticks() - const recalcRate = this.game.inSpawnPhase() ? 2 : 10 - for (const render of this.renders) { - // const territoryUpdated = render.boundingBox == null || render.player.lastTileChange() > render.lastBoundingCalculatedTick - // if (!territoryUpdated) { - // continue - // } - if (currTick - render.lastBoundingCalculatedTick > recalcRate) { - render.lastBoundingCalculatedTick = currTick - render.boundingBox = calculateBoundingBox(render.player.borderTiles()); - } - if (render.isVisible && currTick - render.lastRenderCalcTick > recalcRate) { - render.lastRenderCalcTick = currTick - this.calculateRenderInfo(render) + const sorted = this.game.players().sort((a, b) => b.numTilesOwned() - a.numTilesOwned()) + if (sorted.length > 0) { + this.firstPlace = sorted[0] + } + + for (const player of this.game.playerViews()) { + if (player.isAlive()) { + if (!this.seenPlayers.has(player)) { + this.seenPlayers.add(player) + this.renders.push(new RenderInfo(player, 0, null, 0, this.createPlayerElement(player))) + } } } } public renderLayer(mainContex: CanvasRenderingContext2D) { - const [upperLeft, bottomRight] = this.transformHandler.screenBoundingRect() - for (const player of this.game.playerViews()) { - if (player.isAlive()) { + const screenPosOld = this.transformHandler.worldToScreenCoordinates(new Cell(0, 0)) + const screenPos = new Cell(screenPosOld.x - window.innerWidth / 2, screenPosOld.y - window.innerHeight / 2) - this.renderPlayerInfo(player, mainContex, this.transformHandler.scale, upperLeft, bottomRight) + // render.element.style.fontSize = `${render.fontSize}px` + this.container.style.transform = `translate(${screenPos.x}px, ${screenPos.y}px) scale(${this.transformHandler.scale})` + + const now = Date.now() + if (now + this.lastChecked > this.renderRefreshRate) { + this.lastChecked = now + for (const render of this.renders) { + this.renderPlayerInfo(render) } } + + mainContex.drawImage( + this.canvas, + 0, + 0, + mainContex.canvas.width, + mainContex.canvas.height + ) + } - calculateRenderInfo(render: RenderInfo) { - if (render.player.numTilesOwned() == 0) { - render.fontSize = 0 - return - } - // const [cell, size] = placeName(this.game, render.player) - // render.location = cell - // render.fontSize = Math.max(1, Math.floor(size)) + private createPlayerElement(player: Player): HTMLDivElement { + const element = document.createElement('div') + element.style.position = 'absolute' + element.style.display = 'flex' + element.style.flexDirection = 'column' + element.style.alignItems = 'center' + // Don't set initial transform, will be handled in renderPlayerInfo + + const nameDiv = document.createElement('div') + nameDiv.innerHTML = player.displayName() + nameDiv.style.color = this.theme.playerInfoColor(player.id()).toHex() + nameDiv.style.fontFamily = this.theme.font() + element.appendChild(nameDiv) + + const troopsDiv = document.createElement('div') + troopsDiv.textContent = renderTroops(player.troops()) + troopsDiv.style.color = this.theme.playerInfoColor(player.id()).toHex() + troopsDiv.style.fontFamily = this.theme.font() + troopsDiv.style.fontWeight = 'bold' + element.appendChild(troopsDiv) + + const iconsDiv = document.createElement('div') + iconsDiv.style.position = 'absolute' + iconsDiv.style.display = 'flex' + element.appendChild(iconsDiv) + + this.container.appendChild(element) + return element } - renderPlayerInfo(player: PlayerView, context: CanvasRenderingContext2D, scale: number, uppperLeft: Cell, bottomRight: Cell) { - if (this.alternateView) { + renderPlayerInfo(render: RenderInfo) { + if (!render.player.nameLocation() || !render.player.isAlive()) { + console.log(`remove ${render.player.name()}`) + this.renders = this.renders.filter(r => r != render) + render.element.remove() return } - const name = player.nameLocation() - if (!name) { + const oldLocation = render.location + render.location = new Cell(render.player.nameLocation().x, render.player.nameLocation().y) + render.fontSize = Math.max(1, Math.floor(render.player.nameLocation().size)) + // console.log(`zoom ${this.transformHandler.scale}, size: ${render.player.nameLocation().size}`) + const size = this.transformHandler.scale * render.player.nameLocation().size + if (size < 5) { + if (render.element.style.display != 'none') { + render.element.style.display = 'none' + } + return + } + if (!this.transformHandler.isOnScreen(render.location)) { + if (render.element.style.display != 'none') { + render.element.style.display = 'none' + } + return + } + if (render.element.style.display != 'flex') { + render.element.style.display = 'flex' + } + const now = Date.now() + if (now - render.lastRenderCalc > this.renderRefreshRate) { + render.lastRenderCalc = now + this.rand.nextInt(0, 100) + } else { return } + // Update troops count + const troopsDiv = render.element.children[1] as HTMLDivElement + troopsDiv.textContent = renderTroops(render.player.troops()) - const nameCenterX = Math.floor(name.x - this.game.width() / 2) - const nameCenterY = Math.floor(name.y - this.game.height() / 2) - - const iconSize = name.size * 2; // Adjust size as needed - // const iconX = nameCenterX + render.fontSize * 2; // Position to the right of the name - // const iconY = nameCenterY - render.fontSize / 2; - - if (player == this.firstPlace) { - context.drawImage( - this.crownIconImage, - nameCenterX - iconSize / 2, - nameCenterY - iconSize / 2, - iconSize, - iconSize - ); - } - - - if (player.isTraitor() && this.traitorIconImage.complete) { - context.drawImage( - this.traitorIconImage, - nameCenterX - iconSize / 2, - nameCenterY - iconSize / 2, - iconSize, - iconSize - ); - } - + // Get icons container + const iconsDiv = render.element.children[2] as HTMLDivElement + const iconSize = Math.floor(render.fontSize * 2) const myPlayer = this.getPlayer() - if (myPlayer != null && myPlayer.isAlliedWith(player)) { - context.drawImage( - this.allianceIconImage, - nameCenterX - iconSize / 2, - nameCenterY - iconSize / 2, - iconSize, - iconSize - ); - } - if (myPlayer != null && new Set(myPlayer.transitiveTargets()).has(player)) { - context.drawImage( - this.targetIconImage, - nameCenterX - iconSize / 2, - nameCenterY - iconSize / 2, - iconSize, - iconSize - ); - } - - - context.textRendering = "optimizeSpeed"; - - context.font = `${name.size}px ${this.theme.font()}`; - context.fillStyle = this.theme.playerInfoColor(player.id()).toHex(); - context.textAlign = 'center'; - context.textBaseline = 'middle'; - - context.fillText(player.name(), nameCenterX, nameCenterY - name.size / 2); - context.font = `bold ${name.size}px ${this.theme.font()}`; - - context.fillText(renderTroops(player.troops()), nameCenterX, nameCenterY + name.size); - - - if (myPlayer != null) { - const emojis = player.outgoingEmojis().filter(e => e.recipient == AllPlayers || e.recipient == myPlayer) - if (emojis.length > 0) { - context.font = `${name.size * 4}px ${this.theme.font()}`; - context.fillStyle = this.theme.playerInfoColor(player.id()).toHex(); - context.textAlign = 'center'; - context.textBaseline = 'middle'; - - context.fillText(emojis[0].emoji, nameCenterX, nameCenterY + name.size / 2); + // Handle crown icon + const existingCrown = iconsDiv.querySelector('[data-icon="crown"]') + if (render.player === this.firstPlace) { + if (!existingCrown) { + iconsDiv.appendChild(this.createIconElement(this.crownIconImage.src, iconSize, 'crown')) } + } else if (existingCrown) { + existingCrown.remove() } + + // Handle traitor icon + const existingTraitor = iconsDiv.querySelector('[data-icon="traitor"]') + if (render.player.isTraitor()) { + if (!existingTraitor) { + iconsDiv.appendChild(this.createIconElement(this.traitorIconImage.src, iconSize, 'traitor')) + } + } else if (existingTraitor) { + existingTraitor.remove() + } + + // Handle alliance icon + const existingAlliance = iconsDiv.querySelector('[data-icon="alliance"]') + if (myPlayer != null && myPlayer.isAlliedWith(render.player)) { + if (!existingAlliance) { + iconsDiv.appendChild(this.createIconElement(this.allianceIconImage.src, iconSize, 'alliance')) + } + } else if (existingAlliance) { + existingAlliance.remove() + } + + // Handle target icon + const existingTarget = iconsDiv.querySelector('[data-icon="target"]') + if (myPlayer != null && new Set(myPlayer.transitiveTargets()).has(render.player)) { + if (!existingTarget) { + iconsDiv.appendChild(this.createIconElement(this.targetIconImage.src, iconSize, 'target')) + } + } else if (existingTarget) { + existingTarget.remove() + } + + // Update icon sizes based on scale + const icons = iconsDiv.getElementsByTagName('img') + for (const icon of icons) { + icon.style.width = `${iconSize}px` + icon.style.height = `${iconSize}px` + icon.style.transform = `translateY(${iconSize / 4}px)` + } + + if (!render.location) { + return + } + + if (render.location != oldLocation) { + // Handle all positioning in a single transform + render.element.style.transform = `translate(${render.location.x}px, ${render.location.y}px) translate(-50%, -50%) scale(${render.fontSize * 0.1})` + } + } + + private createIconElement(src: string, size: number, id: string): HTMLImageElement { + const icon = document.createElement('img') + icon.src = src + icon.style.width = `${size}px` + icon.style.height = `${size}px` + icon.setAttribute('data-icon', id) + icon.style.transform = `translateY(${size / 4}px)` + return icon } private getPlayer(): Player | null { @@ -231,4 +291,4 @@ export class NameLayer implements Layer { this.myPlayer = this.game.players().find(p => p.clientID() == this.clientID) return this.myPlayer } -} +} \ No newline at end of file diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index a009b183e..edc96801b 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -62,7 +62,7 @@ export class GameRunner { this.currTurn++ const updates = this.game.executeNextTick() - if (this.game.inSpawnPhase() || this.game.ticks() % 10 == 0) { + if (this.game.inSpawnPhase() || this.game.ticks() % 20 == 0) { this.game.players() .forEach(p => this.playerToName.set(p.id(), placeName(this.game, p))) } diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 7e4999d6f..59d83dc33 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -23,12 +23,12 @@ export class DevConfig extends DefaultConfig { // return 100 } - unitInfo(type: UnitType): UnitInfo { - const info = super.unitInfo(type) - const oldCost = info.cost - info.cost = (p: Player) => oldCost(p) / 10000 - return info - } + // unitInfo(type: UnitType): UnitInfo { + // const info = super.unitInfo(type) + // const oldCost = info.cost + // info.cost = (p: Player) => oldCost(p) / 10000 + // return info + // } // tradeShipSpawnRate(): number { // return 10