From 7d48dd1cbaba17cce4711cd83103bef4c68213a4 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 19 Aug 2024 13:07:15 -0700 Subject: [PATCH] improved algorithm for calculating player name location --- TODO.txt | 4 +- src/client/graphics/GameRenderer.ts | 2 +- .../{ => graphics}/NameBoxCalculator.ts | 56 ++++++--- src/client/graphics/NameRenderer.ts | 107 +++++++++++------- src/core/configuration/DevConfig.ts | 5 +- src/server/Server.ts | 3 - update-deploy.sh | 2 +- 7 files changed, 111 insertions(+), 68 deletions(-) rename src/client/{ => graphics}/NameBoxCalculator.ts (61%) diff --git a/TODO.txt b/TODO.txt index 5a56b861f..8202355bb 100644 --- a/TODO.txt +++ b/TODO.txt @@ -31,14 +31,14 @@ * BUG: boats freeze game on path calculation DONE 8/18/2024 * use analyitics DONE 8/18/2024 * Use Overpass font DONE 8/18/2024 -* BUG: invert zoom +* BUG: invert zoom DONE 8/19/2024 * better algorithm for name render placement * show how many players in each lobby * make boats larger +* make coasts look better * have boats not get close to shore * BUG: boat doesn't work if on lake on other player not on lake * Allow boats to attack TerraNullius -* make coasts look better * add shader to dim border * remove player.info() * fix enemy islands when attacking diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 350e6cba5..9a6ce565a 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -169,7 +169,7 @@ export class GameRenderer { this.scale /= zoomFactor; // Clamp the scale to prevent extreme zooming - this.scale = Math.max(0.1, Math.min(10, this.scale)); + this.scale = Math.max(0.1, Math.min(15, this.scale)); const canvasRect = this.canvas.getBoundingClientRect(); const canvasX = event.x - canvasRect.left; diff --git a/src/client/NameBoxCalculator.ts b/src/client/graphics/NameBoxCalculator.ts similarity index 61% rename from src/client/NameBoxCalculator.ts rename to src/client/graphics/NameBoxCalculator.ts index 68111c1fc..844e1893c 100644 --- a/src/client/NameBoxCalculator.ts +++ b/src/client/graphics/NameBoxCalculator.ts @@ -1,4 +1,5 @@ -import {Game, Player, Tile, Cell} from '../core/Game'; +import {Game, Player, Tile, Cell, TerrainTypes} from '../../core/Game'; +import {within} from '../../core/Util'; export interface Point { x: number; @@ -12,14 +13,23 @@ export interface Rectangle { height: number; } + export function placeName(game: Game, player: Player): [position: Cell, fontSize: number] { const boundingBox = calculateBoundingBox(player); - const grid = createGrid(game, player, boundingBox); + + const rawScalingFactor = (boundingBox.max.x - boundingBox.min.x) / 50 + const scalingFactor = within(Math.floor(rawScalingFactor), 1, 100) + + const grid = createGrid(game, player, boundingBox, scalingFactor); const largestRectangle = findLargestInscribedRectangle(grid); + largestRectangle.x = largestRectangle.x * scalingFactor + largestRectangle.y = largestRectangle.y * scalingFactor + largestRectangle.width = largestRectangle.width * scalingFactor + largestRectangle.height = largestRectangle.height * scalingFactor const center = new Cell( - largestRectangle.x + largestRectangle.width / 2, - largestRectangle.y + largestRectangle.height / 2, + Math.floor(largestRectangle.x + largestRectangle.width / 2 + boundingBox.min.x), + Math.floor(largestRectangle.y + largestRectangle.height / 2 + boundingBox.min.y), ) const fontSize = calculateFontSize(largestRectangle, player.info().name); @@ -27,7 +37,7 @@ export function placeName(game: Game, player: Player): [position: Cell, fontSize return [center, fontSize] } -export function calculateBoundingBox(player: Player): {min: Point; max: Point} { +export function calculateBoundingBox(player: Player): {min: Cell; max: Cell} { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; player.borderTiles().forEach((tile: Tile) => { @@ -38,20 +48,33 @@ export function calculateBoundingBox(player: Player): {min: Point; max: Point} { maxY = Math.max(maxY, cell.y); }); - return {min: {x: minX, y: minY}, max: {x: maxX, y: maxY}}; + return {min: new Cell(minX, minY), max: new Cell(maxX, maxY)} } -export function createGrid(game: Game, player: Player, boundingBox: {min: Point; max: Point}): boolean[][] { - const width = boundingBox.max.x - boundingBox.min.x + 1; - const height = boundingBox.max.y - boundingBox.min.y + 1; +export function createGrid(game: Game, player: Player, boundingBox: {min: Point; max: Point}, scalingFactor: number): boolean[][] { + const scaledBoundingBox: {min: Point; max: Point} = { + min: { + x: Math.floor(boundingBox.min.x / scalingFactor), + y: Math.floor(boundingBox.min.y / scalingFactor) + }, + max: { + x: Math.floor(boundingBox.max.x / scalingFactor), + y: Math.floor(boundingBox.max.y / scalingFactor) + } + } + + + const width = scaledBoundingBox.max.x - scaledBoundingBox.min.x + 1; + const height = scaledBoundingBox.max.y - scaledBoundingBox.min.y + 1; const grid: boolean[][] = Array(width).fill(null).map(() => Array(height).fill(false)); - for (let y = boundingBox.min.y; y <= boundingBox.max.y; y++) { - for (let x = boundingBox.min.x; x <= boundingBox.max.x; x++) { - const cell = new Cell(x, y); + + for (let x = scaledBoundingBox.min.x; x <= scaledBoundingBox.max.x; x++) { + for (let y = scaledBoundingBox.min.y; y <= scaledBoundingBox.max.y; y++) { + const cell = new Cell(x * scalingFactor, y * scalingFactor); if (game.isOnMap(cell)) { const tile = game.tile(cell); - grid[x - boundingBox.min.x][y - boundingBox.min.y] = tile.owner() === player; + grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] = tile.owner() === player; // TODO: okay if lake } } } @@ -68,9 +91,9 @@ export function findLargestInscribedRectangle(grid: boolean[][]): Rectangle { for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { if (grid[col][row]) { - heights[row]++; + heights[col]++; } else { - heights[row] = 0; + heights[col] = 0; } } @@ -120,8 +143,7 @@ export function largestRectangleInHistogram(widths: number[]): Rectangle { export function calculateFontSize(rectangle: Rectangle, name: string): number { // This is a simplified calculation. You might want to adjust it based on your specific font and rendering system. - const aspectRatio = name.length; // Assuming width:height ratio of 2:1 for each character const widthConstrained = rectangle.width / name.length; - const heightConstrained = rectangle.height / 2; + const heightConstrained = rectangle.height / name.length; return Math.min(widthConstrained, heightConstrained); } \ No newline at end of file diff --git a/src/client/graphics/NameRenderer.ts b/src/client/graphics/NameRenderer.ts index 3d9264d3d..a99024677 100644 --- a/src/client/graphics/NameRenderer.ts +++ b/src/client/graphics/NameRenderer.ts @@ -2,10 +2,18 @@ import PriorityQueue from "priority-queue-typescript" import {Cell, Game, Player} from "../../core/Game" import {PseudoRandom} from "../../core/PseudoRandom" import {Theme} from "../../core/configuration/Config" -import {calculateBoundingBox} from "../NameBoxCalculator" +import {calculateBoundingBox, placeName} from "./NameBoxCalculator" class RenderInfo { - constructor(public player: Player, public lastRendered: number, public location: Cell, public fontSize: number) { } + public isVisible = true + constructor( + public player: Player, + public lastRenderCalc: number, + public lastBoundingCalculated: number, + public boundingBox: {min: Cell, max: Cell}, + public location: Cell, + public fontSize: number + ) { } } export class NameRenderer { @@ -17,7 +25,7 @@ export class NameRenderer { private renderInfo: Map = new Map() private context: CanvasRenderingContext2D private canvas: HTMLCanvasElement - private toRender: PriorityQueue = new PriorityQueue(1000, (a: RenderInfo, b: RenderInfo) => a.lastRendered - b.lastRendered); + private renders: RenderInfo[] = [] private seenPlayers: Set = new Set() constructor(private game: Game, private theme: Theme) { @@ -36,63 +44,76 @@ export class NameRenderer { this.canvas.height = this.game.height(); } + public tick() { + const now = Date.now() + if (now - this.lastChecked > this.refreshRate) { + this.lastChecked = now + 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) + } + } + } + for (const render of this.renders) { + const now = Date.now() + if (now - render.lastBoundingCalculated > this.refreshRate) { + render.boundingBox = calculateBoundingBox(render.player); + render.lastBoundingCalculated = now + } + if (render.isVisible && now - render.lastRenderCalc > this.refreshRate) { + this.calculateRenderInfo(render) + render.lastRenderCalc = now + this.rand.nextInt(-50, 50) + } + } + } + public render(mainContex: CanvasRenderingContext2D, scale: number, uppperLeft: Cell, bottomRight: Cell) { - for (const render of this.toRender) { - if (render.player.isAlive()) { + 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) } } } - public tick() { - const now = Date.now() - if (now - this.lastChecked > this.refreshRate) { - this.lastChecked = now - for (const player of this.game.players()) { - if (!this.seenPlayers.has(player)) { - this.toRender.add(new RenderInfo(player, 0, null, null)) - this.seenPlayers.add(player) - } + isVisible(render: RenderInfo, min: Cell, max: Cell): boolean { + const ratio = (max.x - min.x) / Math.max(20, (render.boundingBox.max.x - render.boundingBox.min.x)) + if (render.player.info().isBot) { + if (ratio > 15) { + return false + } + } else { + if (ratio > 30) { + return false } } - - while (!this.toRender.empty() && now - this.toRender.peek().lastRendered > this.refreshRate) { - const renderInfo = this.toRender.poll() - this.calculateRenderInfo(renderInfo) - renderInfo.lastRendered = now + this.rand.nextInt(-50, 50) - this.toRender.add(renderInfo) + if (render.boundingBox.max.x < min.x || render.boundingBox.max.y < min.y || render.boundingBox.min.x > max.x || render.boundingBox.max.y > max.y) { + return false } - + return true } - calculateRenderInfo(render: RenderInfo): boolean { - - let wasUpdated = false - - render.lastRendered = Date.now() + this.rand.nextInt(0, 100) - wasUpdated = true - - const box = calculateBoundingBox(render.player) - const centerX = box.min.x + ((box.max.x - box.min.x) / 2) - const centerY = box.min.y + ((box.max.y - box.min.y) / 2) - render.location = new Cell(centerX, centerY) - render.fontSize = Math.max(Math.min(box.max.x - box.min.x, box.max.y - box.min.y) / render.player.info().name.length / 2, 1.5) - return wasUpdated + calculateRenderInfo(render: RenderInfo) { + if (render.player.numTilesOwned() == 0) { + render.fontSize = 0 + return + } + render.lastRenderCalc = Date.now() + this.rand.nextInt(0, 100) + const [cell, size] = placeName(this.game, render.player) + render.location = cell + render.fontSize = Math.max(1, Math.floor(size)) } renderPlayerInfo(render: RenderInfo, context: CanvasRenderingContext2D, scale: number, uppperLeft: Cell, bottomRight: Cell) { - if (render.fontSize * scale < 10) { - return - } - const nameCenterX = Math.floor(render.location.x - this.game.width() / 2) const nameCenterY = Math.floor(render.location.y - this.game.height() / 2) - if (render.location.x < uppperLeft.x || render.location.x > bottomRight.x || render.location.y < uppperLeft.y || render.location.y > bottomRight.y) { - return - } - - context.textRendering = "optimizeSpeed"; context.font = `${render.fontSize}px ${this.theme.font()}`; diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 8a108525f..eac5ac210 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -15,12 +15,15 @@ export const devConfig = new class extends DefaultConfig { player(): PlayerConfig { return devPlayerConfig } + numBots(): number { + return 500 + } } export const devPlayerConfig = new class extends DefaultPlayerConfig { startTroops(playerInfo: PlayerInfo): number { if (playerInfo.isBot) { - return 10 + return 5000 } return 5000 } diff --git a/src/server/Server.ts b/src/server/Server.ts index 30a18fd4e..45df25710 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -10,8 +10,6 @@ import {defaultConfig} from '../core/configuration/DefaultConfig'; import {GamePhase} from './GameServer'; import {getConfig} from '../core/configuration/Config'; - - const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -19,7 +17,6 @@ const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({server}); - // Serve static files from the 'out' directory app.use(express.static(path.join(__dirname, '../../out'))); app.use(express.json()) diff --git a/update-deploy.sh b/update-deploy.sh index 8ba3f77c5..afe8b5656 100755 --- a/update-deploy.sh +++ b/update-deploy.sh @@ -17,7 +17,7 @@ fi # Set the instance name based on the environment if [[ "$ENV" == "dev" ]]; then - INSTANCE_NAME="openfrontio-dev" + INSTANCE_NAME="openfrontio-dev-instance" echo "[DEV] Deploying to openfront.dev" else INSTANCE_NAME="openfrontio-instance"