mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:20:45 +00:00
created right click handler, refacter renderers
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import {TransformHandler} from "./TransformHandler"
|
||||
|
||||
export interface Layer {
|
||||
init()
|
||||
tick()
|
||||
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler)
|
||||
shouldTransform(): boolean
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Boat, Set<Tile>>()
|
||||
|
||||
|
||||
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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -41,6 +41,12 @@
|
||||
<h3 id="next-game"> </h3>
|
||||
</div>
|
||||
|
||||
<div id="customMenu">
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
|
||||
@@ -207,4 +207,28 @@ h3 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
#customMenu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#customMenu ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#customMenu ul li {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#customMenu ul li:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
Reference in New Issue
Block a user