use html for rendering text

This commit is contained in:
Evan
2025-01-10 12:44:43 -08:00
parent 8c947a9fbf
commit 8e442fe9ce
5 changed files with 250 additions and 152 deletions
+1 -1
View File
@@ -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),
+38
View File
@@ -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() {
+204 -144
View File
@@ -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<string, HTMLImageElement> = 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
}
}
}
+1 -1
View File
@@ -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)))
}
+6 -6
View File
@@ -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