mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-18 17:53:29 +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
|
* Make boats more intuitive (larger area to click off coast) DONE 9/11/2024
|
||||||
* FakeHumans retaliate when attacked DONE 9/11/2024
|
* FakeHumans retaliate when attacked DONE 9/11/2024
|
||||||
|
|
||||||
--- v3 Release DONE
|
|
||||||
|
|
||||||
* Add discord link DONE 9/14/2024
|
* Add discord link DONE 9/14/2024
|
||||||
* front page mobile friendly DONE 9/15/2024
|
* front page mobile friendly DONE 9/15/2024
|
||||||
* game mobile friendly DONE 9/16/2024
|
* game mobile friendly DONE 9/16/2024
|
||||||
* UI: basic win condition & popup DONE 9/16/2024
|
* UI: basic win condition & popup DONE 9/16/2024
|
||||||
* right click popup alliance option
|
* right click popup alliance option
|
||||||
|
* make fake humans easier
|
||||||
* click alliance sends alliance request
|
* click alliance sends alliance request
|
||||||
* notification for alliance request
|
* notification for alliance request
|
||||||
* comfirm alliance
|
* comfirm alliance
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Play
|
|||||||
import {createGame} from "../core/GameImpl";
|
import {createGame} from "../core/GameImpl";
|
||||||
import {EventBus} from "../core/EventBus";
|
import {EventBus} from "../core/EventBus";
|
||||||
import {Config} from "../core/configuration/Config";
|
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 {InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent} from "./InputHandler"
|
||||||
import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn} from "../core/Schemas";
|
import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn} from "../core/Schemas";
|
||||||
import {TerrainMap} from "../core/TerrainMapLoader";
|
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 {
|
export function createClientGame(name: string, clientID: ClientID, playerID: PlayerID, ip: string | null, gameID: GameID, config: Config, terrainMap: TerrainMap): ClientGame {
|
||||||
let eventBus = new EventBus()
|
let eventBus = new EventBus()
|
||||||
let game = createGame(terrainMap, eventBus, config)
|
let game = createGame(terrainMap, eventBus, config)
|
||||||
let terrainRenderer = new TerrainRenderer(game)
|
let gameRenderer = createRenderer(game, eventBus, clientID)
|
||||||
let gameRenderer = new GameRenderer(eventBus, game, clientID, terrainRenderer)
|
|
||||||
|
|
||||||
return new ClientGame(
|
return new ClientGame(
|
||||||
name,
|
name,
|
||||||
@@ -119,14 +118,9 @@ export class ClientGame {
|
|||||||
public start() {
|
public start() {
|
||||||
console.log('version 3')
|
console.log('version 3')
|
||||||
this.isActive = true
|
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(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(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.renderer.initialize()
|
||||||
this.input.initialize()
|
this.input.initialize()
|
||||||
@@ -179,14 +173,13 @@ export class ClientGame {
|
|||||||
console.log('setting name')
|
console.log('setting name')
|
||||||
this.myPlayer = event.player
|
this.myPlayer = event.player
|
||||||
}
|
}
|
||||||
this.renderer.playerEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inputEvent(event: MouseUpEvent) {
|
private inputEvent(event: MouseUpEvent) {
|
||||||
if (!this.isActive) {
|
if (!this.isActive) {
|
||||||
return
|
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)) {
|
if (!this.gs.isOnMap(cell)) {
|
||||||
return
|
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 {
|
export class ZoomEvent implements GameEvent {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly x: number,
|
public readonly x: number,
|
||||||
@@ -32,6 +39,8 @@ export class DragEvent implements GameEvent {
|
|||||||
|
|
||||||
export class InputHandler {
|
export class InputHandler {
|
||||||
|
|
||||||
|
private contextMenuActive = false
|
||||||
|
|
||||||
private lastPointerX: number = 0;
|
private lastPointerX: number = 0;
|
||||||
private lastPointerY: number = 0;
|
private lastPointerY: number = 0;
|
||||||
|
|
||||||
@@ -51,10 +60,23 @@ export class InputHandler {
|
|||||||
document.addEventListener("pointerup", (e) => this.onPointerUp(e));
|
document.addEventListener("pointerup", (e) => this.onPointerUp(e));
|
||||||
document.addEventListener("wheel", (e) => this.onScroll(e), {passive: false});
|
document.addEventListener("wheel", (e) => this.onScroll(e), {passive: false});
|
||||||
document.addEventListener('pointermove', this.onPointerMove.bind(this));
|
document.addEventListener('pointermove', this.onPointerMove.bind(this));
|
||||||
|
document.addEventListener('contextmenu', (e: MouseEvent) => {
|
||||||
|
this.onRightClick(e)
|
||||||
|
});
|
||||||
|
|
||||||
this.pointers.clear()
|
this.pointers.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPointerDown(event: PointerEvent) {
|
private onPointerDown(event: PointerEvent) {
|
||||||
|
if (this.contextMenuActive) {
|
||||||
|
this.contextMenuActive = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.button > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.pointerDown = true
|
this.pointerDown = true
|
||||||
this.pointers.set(event.pointerId, event);
|
this.pointers.set(event.pointerId, event);
|
||||||
|
|
||||||
@@ -72,6 +94,9 @@ export class InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPointerUp(event: PointerEvent) {
|
onPointerUp(event: PointerEvent) {
|
||||||
|
if (event.button > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.pointerDown = false
|
this.pointerDown = false
|
||||||
this.pointers.delete(event.pointerId);
|
this.pointers.delete(event.pointerId);
|
||||||
const dist = Math.abs(event.x - this.lastPointerDownX) + Math.abs(event.y - this.lastPointerDownY);
|
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) {
|
private onPointerMove(event: PointerEvent) {
|
||||||
|
if (event.button > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
this.pointers.set(event.pointerId, event);
|
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 {
|
private getPinchDistance(): number {
|
||||||
const pointerEvents = Array.from(this.pointers.values());
|
const pointerEvents = Array.from(this.pointers.values());
|
||||||
const dx = pointerEvents[0].clientX - pointerEvents[1].clientX;
|
const dx = pointerEvents[0].clientX - pointerEvents[1].clientX;
|
||||||
|
|||||||
@@ -6,64 +6,44 @@ import {NameRenderer} from "./NameRenderer";
|
|||||||
import {TerrainRenderer} from "./TerrainRenderer";
|
import {TerrainRenderer} from "./TerrainRenderer";
|
||||||
import {TerritoryRenderer} from "./TerritoryRenderer";
|
import {TerritoryRenderer} from "./TerritoryRenderer";
|
||||||
import {ClientID} from "../../core/Schemas";
|
import {ClientID} from "../../core/Schemas";
|
||||||
import {renderTroops} from "./Utils";
|
import {createCanvas, renderTroops} from "./Utils";
|
||||||
import {UIRenderer} from "./UIRenderer";
|
import {UIRenderer} from "./UIRenderer";
|
||||||
import {EventBus} from "../../core/EventBus";
|
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 {
|
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 context: CanvasRenderingContext2D
|
||||||
|
|
||||||
private nameRenderer: NameRenderer;
|
constructor(private game: Game, private eventBus: EventBus, private canvas: HTMLCanvasElement, public transformHandler: TransformHandler, private layers: Layer[]) {
|
||||||
private territoryRenderer: TerritoryRenderer;
|
this.context = canvas.getContext("2d")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
this.canvas = document.createElement('canvas');
|
this.layers.forEach(l => l.init())
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
document.body.appendChild(this.canvas);
|
document.body.appendChild(this.canvas);
|
||||||
window.addEventListener('resize', () => this.resizeCanvas());
|
window.addEventListener('resize', () => this.resizeCanvas());
|
||||||
this.resizeCanvas();
|
this.resizeCanvas();
|
||||||
|
|
||||||
|
this.transformHandler = new TransformHandler(this.game, this.eventBus, this.canvas.getBoundingClientRect())
|
||||||
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;
|
|
||||||
|
|
||||||
requestAnimationFrame(() => this.renderGame());
|
requestAnimationFrame(() => this.renderGame());
|
||||||
}
|
}
|
||||||
@@ -76,55 +56,43 @@ export class GameRenderer {
|
|||||||
|
|
||||||
renderGame() {
|
renderGame() {
|
||||||
// Set background
|
// 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);
|
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
// Save the current context state
|
// Save the current context state
|
||||||
this.context.save();
|
this.context.save();
|
||||||
|
|
||||||
|
this.transformHandler.handleTransform(this.context)
|
||||||
|
|
||||||
// Disable image smoothing for pixelated effect
|
this.layers.forEach(l => {
|
||||||
if (this.scale > 3) {
|
if (l.shouldTransform()) {
|
||||||
this.context.imageSmoothingEnabled = false;
|
l.render(this.context, this.transformHandler)
|
||||||
} 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.context.restore()
|
this.context.restore()
|
||||||
|
|
||||||
|
this.layers.forEach(l => {
|
||||||
|
if (!l.shouldTransform()) {
|
||||||
|
l.render(this.context, this.transformHandler)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.renderSpawnBar()
|
this.renderSpawnBar()
|
||||||
this.uiRenderer.render(this.context)
|
|
||||||
|
|
||||||
requestAnimationFrame(() => this.renderGame());
|
requestAnimationFrame(() => this.renderGame());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO: move to UIRenderer
|
// TODO: move to UIRenderer
|
||||||
renderSpawnBar() {
|
renderSpawnBar() {
|
||||||
if (!this.gs.inSpawnPhase()) {
|
if (!this.game.inSpawnPhase()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const barHeight = 15;
|
const barHeight = 15;
|
||||||
const barBackgroundWidth = this.canvas.width;
|
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
|
// Draw bar background
|
||||||
this.context.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
this.context.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||||||
@@ -135,19 +103,7 @@ export class GameRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
this.nameRenderer.tick()
|
this.layers.forEach(l => l.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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(width: number, height: number): void {
|
resize(width: number, height: number): void {
|
||||||
@@ -155,77 +111,4 @@ export class GameRenderer {
|
|||||||
this.canvas.height = Math.ceil(height / window.devicePixelRatio);
|
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 {PseudoRandom} from "../../core/PseudoRandom"
|
||||||
import {calculateBoundingBox} from "../../core/Util"
|
import {calculateBoundingBox} from "../../core/Util"
|
||||||
import {Theme} from "../../core/configuration/Config"
|
import {Theme} from "../../core/configuration/Config"
|
||||||
|
import {Layer} from "./Layer"
|
||||||
import {placeName} from "./NameBoxCalculator"
|
import {placeName} from "./NameBoxCalculator"
|
||||||
|
import {TransformHandler} from "./TransformHandler"
|
||||||
import {renderTroops} from "./Utils"
|
import {renderTroops} from "./Utils"
|
||||||
|
|
||||||
class RenderInfo {
|
class RenderInfo {
|
||||||
@@ -17,7 +19,7 @@ class RenderInfo {
|
|||||||
) { }
|
) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NameRenderer {
|
export class NameRenderer implements Layer {
|
||||||
|
|
||||||
private lastChecked = 0
|
private lastChecked = 0
|
||||||
private refreshRate = 1000
|
private refreshRate = 1000
|
||||||
@@ -32,9 +34,12 @@ export class NameRenderer {
|
|||||||
constructor(private game: Game, private theme: Theme) {
|
constructor(private game: Game, private theme: Theme) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
shouldTransform(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public initialize() {
|
public init() {
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement('canvas');
|
||||||
this.context = this.canvas.getContext('2d');
|
this.context = this.canvas.getContext('2d');
|
||||||
|
|
||||||
@@ -45,6 +50,7 @@ export class NameRenderer {
|
|||||||
this.canvas.height = this.game.height();
|
this.canvas.height = this.game.height();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove tick, move this to render
|
||||||
public tick() {
|
public tick() {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (now - this.lastChecked > this.refreshRate) {
|
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) {
|
for (const render of this.renders) {
|
||||||
render.isVisible = this.isVisible(render, uppperLeft, bottomRight)
|
render.isVisible = this.isVisible(render, upperLeft, bottomRight)
|
||||||
if (render.player.isAlive() && render.isVisible && render.fontSize * scale > 10) {
|
if (render.player.isAlive() && render.isVisible && render.fontSize * transformHandler.scale > 10) {
|
||||||
this.renderPlayerInfo(render, mainContex, scale, uppperLeft, bottomRight)
|
this.renderPlayerInfo(render, mainContex, transformHandler.scale, upperLeft, bottomRight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import {inherits} from "util"
|
import {inherits} from "util"
|
||||||
import {Game} from "../../core/Game";
|
import {Game} from "../../core/Game";
|
||||||
import {throws} from "assert";
|
import {throws} from "assert";
|
||||||
|
import {Layer} from "./Layer";
|
||||||
|
import {TransformHandler} from "./TransformHandler";
|
||||||
|
|
||||||
export class TerrainRenderer {
|
export class TerrainRenderer implements Layer {
|
||||||
private canvas: HTMLCanvasElement
|
private canvas: HTMLCanvasElement
|
||||||
private context: CanvasRenderingContext2D
|
private context: CanvasRenderingContext2D
|
||||||
private imageData: ImageData
|
private imageData: ImageData
|
||||||
|
|
||||||
|
|
||||||
constructor(private game: Game) { }
|
constructor(private game: Game) { }
|
||||||
|
shouldTransform(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
tick() {
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement('canvas');
|
||||||
@@ -34,7 +41,7 @@ export class TerrainRenderer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(context: CanvasRenderingContext2D) {
|
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) {
|
||||||
context.drawImage(
|
context.drawImage(
|
||||||
this.canvas,
|
this.canvas,
|
||||||
-this.game.width() / 2,
|
-this.game.width() / 2,
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import {PseudoRandom} from "../../core/PseudoRandom";
|
|||||||
import {Colord} from "colord";
|
import {Colord} from "colord";
|
||||||
import {bfs, dist} from "../../core/Util";
|
import {bfs, dist} from "../../core/Util";
|
||||||
import {Theme} from "../../core/configuration/Config";
|
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 canvas: HTMLCanvasElement
|
||||||
private context: CanvasRenderingContext2D
|
private context: CanvasRenderingContext2D
|
||||||
private imageData: ImageData
|
private imageData: ImageData
|
||||||
@@ -17,8 +20,15 @@ export class TerritoryRenderer {
|
|||||||
private boatToTrail = new Map<Boat, Set<Tile>>()
|
private boatToTrail = new Map<Boat, Set<Tile>>()
|
||||||
|
|
||||||
|
|
||||||
constructor(private game: Game) {
|
constructor(private game: Game, eventBus: EventBus) {
|
||||||
this.theme = game.config().theme()
|
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() {
|
init() {
|
||||||
@@ -40,7 +50,7 @@ export class TerritoryRenderer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(context: CanvasRenderingContext2D) {
|
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) {
|
||||||
this.renderTerritory()
|
this.renderTerritory()
|
||||||
this.context.putImageData(this.imageData, 0, 0);
|
this.context.putImageData(this.imageData, 0, 0);
|
||||||
context.drawImage(
|
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 {ClientID} from "../../core/Schemas";
|
||||||
import {renderTroops} from "./Utils";
|
import {renderTroops} from "./Utils";
|
||||||
import winModalHtml from '../WinModal.html';
|
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 exitButton: HTMLButtonElement;
|
||||||
private winModal: HTMLElement | null = null;
|
private winModal: HTMLElement | null = null;
|
||||||
|
|
||||||
|
private customMenu = document.getElementById('customMenu');
|
||||||
|
|
||||||
|
|
||||||
constructor(private eventBus: EventBus, private game: Game, private theme: Theme, private clientID: ClientID) {
|
constructor(private eventBus: EventBus, private game: Game, private theme: Theme, private clientID: ClientID) {
|
||||||
|
|
||||||
|
}
|
||||||
|
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) {
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldTransform(): boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.createExitButton()
|
this.createExitButton()
|
||||||
this.createWinModal()
|
this.createWinModal()
|
||||||
|
this.initRightClickMenu()
|
||||||
this.eventBus.on(WinEvent, (e) => this.onWinEvent(e))
|
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() {
|
createWinModal() {
|
||||||
@@ -124,9 +165,6 @@ export class UIRenderer {
|
|||||||
document.body.appendChild(this.exitButton);
|
document.body.appendChild(this.exitButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(context: CanvasRenderingContext2D) {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onWinEvent(event: WinEvent) {
|
onWinEvent(event: WinEvent) {
|
||||||
console.log(`${event.winner.name()} won the game!!}`)
|
console.log(`${event.winner.name()} won the game!!}`)
|
||||||
@@ -165,4 +203,37 @@ export class UIRenderer {
|
|||||||
window.location.reload();
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -15,3 +15,17 @@ export function renderTroops(troops: number): string {
|
|||||||
}
|
}
|
||||||
return troopsStr
|
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>
|
<h3 id="next-game"> </h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="customMenu">
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -208,3 +208,27 @@ h3 {
|
|||||||
height: 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