created Layers dir

This commit is contained in:
evanpelle
2024-09-16 17:14:20 -07:00
parent 9d4a146a73
commit aaa16683e1
7 changed files with 38 additions and 38 deletions
+8
View File
@@ -0,0 +1,8 @@
import {TransformHandler} from "../TransformHandler"
export interface Layer {
init()
tick()
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler)
shouldTransform(): boolean
}
+137
View File
@@ -0,0 +1,137 @@
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 {
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 NameLayer implements Layer {
private lastChecked = 0
private refreshRate = 1000
private rand = new PseudoRandom(10)
private renderInfo: Map<Player, RenderInfo> = new Map()
private context: CanvasRenderingContext2D
private canvas: HTMLCanvasElement
private renders: RenderInfo[] = []
private seenPlayers: Set<Player> = new Set()
constructor(private game: Game, private theme: Theme) {
}
shouldTransform(): boolean {
return true
}
public init() {
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
this.canvas.style.position = 'fixed';
this.canvas.style.left = '0';
this.canvas.style.top = '0';
this.canvas.width = this.game.width();
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) {
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.borderTiles());
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, transformHandler: TransformHandler) {
const [upperLeft, bottomRight] = transformHandler.screenBoundingRect()
for (const render of this.renders) {
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)
}
}
}
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.type() == PlayerType.Bot) {
if (ratio > 35) {
return false
}
} else {
if (ratio > 35) {
return false
}
}
if (render.boundingBox.max.x < min.x || render.boundingBox.max.y < min.y || render.boundingBox.min.x > max.x || render.boundingBox.min.y > max.y) {
return false
}
return true
}
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) {
const nameCenterX = Math.floor(render.location.x - this.game.width() / 2)
const nameCenterY = Math.floor(render.location.y - this.game.height() / 2)
context.textRendering = "optimizeSpeed";
context.font = `${render.fontSize}px ${this.theme.font()}`;
context.fillStyle = this.theme.playerInfoColor(render.player.id()).toHex();
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(render.player.name(), nameCenterX, nameCenterY - render.fontSize / 2);
context.font = `bold ${render.fontSize}px ${this.theme.font()}`;
context.fillText(renderTroops(render.player.troops()), nameCenterX, nameCenterY + render.fontSize);
}
}
@@ -0,0 +1,53 @@
import {inherits} from "util"
import {Game} from "../../../core/Game";
import {throws} from "assert";
import {Layer} from "./Layer";
import {TransformHandler} from "../TransformHandler";
export class TerrainLayer 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');
this.context = this.canvas.getContext("2d")
this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height())
this.initImageData()
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
this.context.putImageData(this.imageData, 0, 0);
}
initImageData() {
const theme = this.game.config().theme()
this.game.forEachTile((tile) => {
let terrainColor = theme.terrainColor(tile)
const index = (tile.cell().y * this.game.width()) + tile.cell().x
const offset = index * 4
this.imageData.data[offset] = terrainColor.rgba.r;
this.imageData.data[offset + 1] = terrainColor.rgba.g;
this.imageData.data[offset + 2] = terrainColor.rgba.b;
this.imageData.data[offset + 3] = terrainColor.rgba.a * 255 | 0
})
}
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) {
context.drawImage(
this.canvas,
-this.game.width() / 2,
-this.game.height() / 2,
this.game.width(),
this.game.height()
)
}
}
@@ -0,0 +1,143 @@
import {PriorityQueue} from "@datastructures-js/priority-queue";
import {Boat, BoatEvent, Cell, Game, Player, Tile, TileEvent} from "../../../core/Game";
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 TerritoryLayer implements Layer {
private canvas: HTMLCanvasElement
private context: CanvasRenderingContext2D
private imageData: ImageData
private tileToRenderQueue: PriorityQueue<{tileEvent: TileEvent, lastUpdate: number}> = new PriorityQueue((a, b) => {return a.lastUpdate - b.lastUpdate})
private random = new PseudoRandom(123)
private theme: Theme = null
private boatToTrail = new Map<Boat, Set<Tile>>()
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() {
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext("2d")
this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height())
this.initImageData()
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
this.context.putImageData(this.imageData, 0, 0);
}
initImageData() {
this.game.forEachTile((tile) => {
const index = (tile.cell().y * this.game.width()) + tile.cell().x
const offset = index * 4
this.imageData.data[offset + 3] = 0
})
}
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) {
this.renderTerritory()
this.context.putImageData(this.imageData, 0, 0);
context.drawImage(
this.canvas,
-this.game.width() / 2,
-this.game.height() / 2,
this.game.width(),
this.game.height()
)
}
boatEvent(event: BoatEvent) {
if (!this.boatToTrail.has(event.boat)) {
this.boatToTrail.set(event.boat, new Set<Tile>())
}
const trail = this.boatToTrail.get(event.boat)
trail.add(event.oldTile)
bfs(event.oldTile, dist(event.oldTile, 3)).forEach(t => {
this.paintTerritory(t)
})
if (event.boat.isActive()) {
bfs(event.boat.tile(), dist(event.boat.tile(), 4)).forEach(
t => {
if (trail.has(t)) {
this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().info()), 150)
}
}
)
bfs(event.boat.tile(), dist(event.boat.tile(), 2)).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().info()), 255))
bfs(event.boat.tile(), dist(event.boat.tile(), 1)).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().info()), 180))
} else {
trail.forEach(t => this.paintTerritory(t))
this.boatToTrail.delete(event.boat)
}
}
renderTerritory() {
let numToRender = Math.floor(this.tileToRenderQueue.size() / 10)
if (numToRender == 0) {
numToRender = this.tileToRenderQueue.size()
}
while (numToRender > 0) {
numToRender--
const event = this.tileToRenderQueue.pop().tileEvent
this.paintTerritory(event.tile)
event.tile.neighbors().forEach(t => this.paintTerritory(t))
}
}
paintTerritory(tile: Tile) {
if (!tile.hasOwner()) {
this.clearCell(tile.cell())
return
}
const owner = tile.owner() as Player
if (tile.isBorder()) {
this.paintCell(
tile.cell(),
this.theme.borderColor(owner.info()),
255
)
} else {
this.paintCell(
tile.cell(),
this.theme.territoryColor(owner.info()),
110
)
}
}
paintCell(cell: Cell, color: Colord, alpha: number) {
const index = (cell.y * this.game.width()) + cell.x
const offset = index * 4
this.imageData.data[offset] = color.rgba.r;
this.imageData.data[offset + 1] = color.rgba.g;
this.imageData.data[offset + 2] = color.rgba.b;
this.imageData.data[offset + 3] = alpha
}
clearCell(cell: Cell) {
const index = (cell.y * this.game.width()) + cell.x;
const offset = index * 4;
this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
}
tileUpdate(event: TileEvent) {
this.tileToRenderQueue.push({tileEvent: event, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5)})
}
}
+254
View File
@@ -0,0 +1,254 @@
import {Theme} from "../../../core/configuration/Config";
import {EventBus} from "../../../core/EventBus";
import {WinEvent} from "../../../core/execution/WinCheckExecution";
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";
interface MenuOption {
label: string;
action: () => void;
}
export class UILayer 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) {
if (!this.game.inSpawnPhase()) {
return
}
const barHeight = 15;
const barBackgroundWidth = transformHandler.width();
const ratio = this.game.ticks() / this.game.config().numSpawnPhaseTurns()
// Draw bar background
context.fillStyle = 'rgba(0, 0, 0, 0.5)';
context.fillRect(0, 0, barBackgroundWidth, barHeight);
context.fillStyle = 'rgba(0, 128, 255, 0.7)';
context.fillRect(0, 0, barBackgroundWidth * ratio, barHeight);
}
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() {
console.log("Creating win modal");
this.winModal = document.createElement('div');
this.winModal.style.cssText = `
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border: 2px solid black;
border-radius: 10px;
z-index: 2000;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
`;
const content = document.createElement('div');
const title = document.createElement('h2');
title.textContent = 'Game Over';
title.id = 'winTitle';
title.style.marginTop = '0';
const message = document.createElement('p');
message.id = 'winMessage';
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'space-between';
buttonContainer.style.marginTop = '20px';
const exitButton = document.createElement('button');
exitButton.textContent = 'Exit Game';
exitButton.onclick = () => this.exitGame();
this.styleButton(exitButton);
const continueButton = document.createElement('button');
continueButton.textContent = 'Keep Playing';
continueButton.onclick = () => this.closeWinModal();
this.styleButton(continueButton);
buttonContainer.appendChild(exitButton);
buttonContainer.appendChild(continueButton);
content.appendChild(title);
content.appendChild(message);
content.appendChild(buttonContainer);
this.winModal.appendChild(content);
document.body.appendChild(this.winModal);
console.log("Win modal appended to body");
}
styleButton(button: HTMLButtonElement) {
button.style.cssText = `
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #4A90E2;
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.3s;
`;
button.onmouseover = () => button.style.backgroundColor = '#3A7BCE';
button.onmouseout = () => button.style.backgroundColor = '#4A90E2';
}
createExitButton() {
this.exitButton = document.createElement('button');
this.exitButton.innerHTML = '&#10005;'; // HTML entity for "×" (multiplication sign)
this.exitButton.style.position = 'fixed';
this.exitButton.style.top = '20px';
this.exitButton.style.right = '20px';
this.exitButton.style.zIndex = '1000';
this.exitButton.style.width = '40px';
this.exitButton.style.height = '40px';
this.exitButton.style.fontSize = '20px';
this.exitButton.style.fontWeight = 'bold';
this.exitButton.style.backgroundColor = 'rgba(255, 0, 0, 0.4)'; // More translucent red
this.exitButton.style.color = 'white';
this.exitButton.style.border = 'none';
this.exitButton.style.borderRadius = '50%';
this.exitButton.style.cursor = 'pointer';
this.exitButton.style.display = 'flex';
this.exitButton.style.justifyContent = 'center';
this.exitButton.style.alignItems = 'center';
this.exitButton.style.transition = 'background-color 0.3s';
this.exitButton.addEventListener('mouseover', () => {
this.exitButton.style.backgroundColor = 'rgba(255, 0, 0, 0.5)'; // Less translucent on hover
});
this.exitButton.addEventListener('mouseout', () => {
this.exitButton.style.backgroundColor = 'rgba(255, 0, 0, 0.3)'; // Back to more translucent
});
this.exitButton.addEventListener('click', () => this.onExitButtonClick());
document.body.appendChild(this.exitButton);
}
onWinEvent(event: WinEvent) {
console.log(`${event.winner.name()} won the game!!}`)
this.showWinModal(event.winner)
}
showWinModal(winner: Player) {
if (this.winModal) {
const message = this.winModal.querySelector('#winMessage');
if (message) {
message.textContent = `${winner.name()} won the game!`;
}
const title = this.winModal.querySelector('#winTitle')
if (winner.clientID() == this.clientID) {
title.textContent = 'You Won!!!'
} else {
title.textContent = 'You Lost!!!'
}
this.winModal.style.display = 'block';
}
}
onExitButtonClick() {
console.log('Button clicked!');
window.location.reload();
}
closeWinModal() {
if (this.winModal) {
this.winModal.style.display = 'none';
}
}
exitGame() {
this.closeWinModal();
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';
}
}
}