diff --git a/TODO.txt b/TODO.txt index d17d7f272..679616ecd 100644 --- a/TODO.txt +++ b/TODO.txt @@ -172,11 +172,12 @@ * rewrite EventsDisplay DONE 11/1/2024 * update Mena NPC locations DONE 11/1/2024 * create build menu DONE 11/3/2024 -* add gold -* add troop/worker slider -* add battleship +* add gold DONE 11/4/2024 +* add troop/worker slider DONE 11/4/2024 +* create Unit layer DONE 11/9/2024 +* create Unit interface +* add destroyer * NPC has relations -* fix name rendering * use twitter emojis * private game shows how many players joined * optimize sendBoat function diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 579de4a00..a8c101266 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -14,6 +14,7 @@ import { Leaderboard } from "./layers/Leaderboard"; import { ControlPanel } from "./layers/ControlPanel"; import { UIState } from "./UIState"; import { BuildMenu } from "./layers/radial/BuildMenu"; +import { UnitLayer } from "./layers/UnitLayer"; export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus: EventBus, clientID: ClientID): GameRenderer { @@ -61,6 +62,7 @@ export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus: const layers: Layer[] = [ new TerrainLayer(game), new TerritoryLayer(game, eventBus), + new UnitLayer(game, eventBus), new NameLayer(game, game.config().theme(), transformHandler, clientID), new UILayer(eventBus, game, clientID, transformHandler), eventsDisplay, diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 85d76d87a..c6dfd6c31 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -1,33 +1,33 @@ -import {PriorityQueue} from "@datastructures-js/priority-queue"; -import {Boat, BoatEvent, Cell, Game, Player, Tile, TileEvent} from "../../../core/game/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"; +import { PriorityQueue } from "@datastructures-js/priority-queue"; +import { Cell, Game, Player, Tile, TileEvent } from "../../../core/game/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 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>() 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() { } @@ -62,31 +62,6 @@ export class TerritoryLayer implements Layer { ) } - boatEvent(event: BoatEvent) { - if (!this.boatToTrail.has(event.boat)) { - this.boatToTrail.set(event.boat, new Set()) - } - 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) { @@ -138,6 +113,6 @@ export class TerritoryLayer implements Layer { } tileUpdate(event: TileEvent) { - this.tileToRenderQueue.push({tileEvent: event, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5)}) + this.tileToRenderQueue.push({ tileEvent: event, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5) }) } } \ No newline at end of file diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts new file mode 100644 index 000000000..a5dbd79db --- /dev/null +++ b/src/client/graphics/layers/UnitLayer.ts @@ -0,0 +1,103 @@ +import { Colord } from "colord"; +import { Theme } from "../../../core/configuration/Config"; +import { Boat, BoatEvent, Cell, Game, Tile } from "../../../core/game/Game"; +import { bfs, dist } from "../../../core/Util"; +import { Layer } from "./Layer"; +import { EventBus } from "../../../core/EventBus"; + +export class UnitLayer implements Layer { + private canvas: HTMLCanvasElement + private context: CanvasRenderingContext2D + private imageData: ImageData + + private boatToTrail = new Map>() + + private theme: Theme = null + + constructor(private game: Game, private eventBus: EventBus) { + this.theme = game.config().theme() + } + + shouldTransform(): boolean { + return true + } + + tick() { + } + + init(game: Game) { + 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.canvas.width = this.game.width(); + this.canvas.height = this.game.height(); + this.context.putImageData(this.imageData, 0, 0); + this.initImageData() + + this.eventBus.on(BoatEvent, e => this.onBoatEvent(e)) + } + + 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 + }) + } + + renderLayer(context: CanvasRenderingContext2D) { + 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() + ) + } + + + onBoatEvent(event: BoatEvent) { + if (!this.boatToTrail.has(event.boat)) { + this.boatToTrail.set(event.boat, new Set()) + } + const trail = this.boatToTrail.get(event.boat) + trail.add(event.oldTile) + bfs(event.oldTile, dist(event.oldTile, 3)).forEach(t => { + this.clearCell(t.cell()) + }) + 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.clearCell(t.cell())) + this.boatToTrail.delete(event.boat) + } + } + + + 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) + } + + +} \ No newline at end of file