mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 22:20:38 +00:00
299 lines
7.9 KiB
TypeScript
299 lines
7.9 KiB
TypeScript
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
|
import {
|
|
Cell,
|
|
Game,
|
|
Player,
|
|
PlayerType,
|
|
Unit,
|
|
UnitType,
|
|
} from "../../../core/game/Game";
|
|
import { GameUpdateType, UnitUpdate } from "../../../core/game/GameUpdates";
|
|
import { PseudoRandom } from "../../../core/PseudoRandom";
|
|
import { colord, Colord } from "colord";
|
|
import { Theme } from "../../../core/configuration/Config";
|
|
import { Layer } from "./Layer";
|
|
import { EventBus } from "../../../core/EventBus";
|
|
import {
|
|
AlternateViewEvent,
|
|
DragEvent,
|
|
MouseDownEvent,
|
|
} from "../../InputHandler";
|
|
import { GameView, PlayerView } from "../../../core/game/GameView";
|
|
import {
|
|
euclDistFN,
|
|
manhattanDistFN,
|
|
TileRef,
|
|
} from "../../../core/game/GameMap";
|
|
|
|
export class TerritoryLayer implements Layer {
|
|
private canvas: HTMLCanvasElement;
|
|
private context: CanvasRenderingContext2D;
|
|
private imageData: ImageData;
|
|
|
|
private tileToRenderQueue: PriorityQueue<{
|
|
tile: TileRef;
|
|
lastUpdate: number;
|
|
}> = new PriorityQueue((a, b) => {
|
|
return a.lastUpdate - b.lastUpdate;
|
|
});
|
|
private random = new PseudoRandom(123);
|
|
private theme: Theme = null;
|
|
|
|
// Used for spawn highlighting
|
|
private highlightCanvas: HTMLCanvasElement;
|
|
private highlightContext: CanvasRenderingContext2D;
|
|
|
|
private alternativeView = false;
|
|
private lastDragTime = 0;
|
|
private nodrawDragDuration = 200;
|
|
|
|
private refreshRate = 50;
|
|
private lastRefresh = 0;
|
|
|
|
constructor(
|
|
private game: GameView,
|
|
private eventBus: EventBus,
|
|
) {
|
|
this.theme = game.config().theme();
|
|
}
|
|
|
|
shouldTransform(): boolean {
|
|
return true;
|
|
}
|
|
|
|
tick() {
|
|
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
|
|
this.game.updatesSinceLastTick()[GameUpdateType.Unit].forEach((u) => {
|
|
const update = u as UnitUpdate;
|
|
if (update.unitType == UnitType.DefensePost && update.isActive) {
|
|
const tile = update.pos;
|
|
this.game
|
|
.bfs(
|
|
tile,
|
|
manhattanDistFN(tile, this.game.config().defensePostRange()),
|
|
)
|
|
.forEach((t) => {
|
|
if (
|
|
this.game.isBorder(t) &&
|
|
this.game.ownerID(t) == update.ownerID
|
|
) {
|
|
this.enqueueTile(t);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
if (!this.game.inSpawnPhase()) {
|
|
return;
|
|
}
|
|
if (this.game.ticks() % 5 == 0) {
|
|
return;
|
|
}
|
|
|
|
this.highlightContext.clearRect(
|
|
0,
|
|
0,
|
|
this.game.width(),
|
|
this.game.height(),
|
|
);
|
|
const humans = this.game
|
|
.playerViews()
|
|
.filter((p) => p.type() == PlayerType.Human);
|
|
|
|
for (const human of humans) {
|
|
const center = human.nameLocation();
|
|
if (!center) {
|
|
continue;
|
|
}
|
|
const centerTile = this.game.ref(center.x, center.y);
|
|
if (!centerTile) {
|
|
continue;
|
|
}
|
|
for (const tile of this.game.bfs(centerTile, euclDistFN(centerTile, 9))) {
|
|
if (!this.game.hasOwner(tile)) {
|
|
this.paintHighlightCell(
|
|
new Cell(this.game.x(tile), this.game.y(tile)),
|
|
this.theme.spawnHighlightColor(),
|
|
255,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
init() {
|
|
this.eventBus.on(AlternateViewEvent, (e) => {
|
|
this.alternativeView = e.alternateView;
|
|
});
|
|
this.eventBus.on(DragEvent, (e) => {
|
|
// TODO: consider re-enabling this on mobile or low end devices for smoother dragging.
|
|
// this.lastDragTime = Date.now();
|
|
});
|
|
this.redraw();
|
|
}
|
|
|
|
redraw() {
|
|
console.log("redrew territory layer");
|
|
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);
|
|
|
|
// Add a second canvas for highlights
|
|
this.highlightCanvas = document.createElement("canvas");
|
|
this.highlightContext = this.highlightCanvas.getContext("2d", {
|
|
alpha: true,
|
|
});
|
|
this.highlightCanvas.width = this.game.width();
|
|
this.highlightCanvas.height = this.game.height();
|
|
|
|
this.game.forEachTile((t) => {
|
|
this.paintTerritory(t);
|
|
});
|
|
}
|
|
|
|
initImageData() {
|
|
this.game.forEachTile((tile) => {
|
|
const cell = new Cell(this.game.x(tile), this.game.y(tile));
|
|
const index = cell.y * this.game.width() + cell.x;
|
|
const offset = index * 4;
|
|
this.imageData.data[offset + 3] = 0;
|
|
});
|
|
}
|
|
|
|
renderLayer(context: CanvasRenderingContext2D) {
|
|
if (
|
|
Date.now() > this.lastDragTime + this.nodrawDragDuration &&
|
|
Date.now() > this.lastRefresh + this.refreshRate
|
|
) {
|
|
this.lastRefresh = Date.now();
|
|
this.renderTerritory();
|
|
this.context.putImageData(this.imageData, 0, 0);
|
|
}
|
|
if (this.alternativeView) {
|
|
return;
|
|
}
|
|
|
|
context.drawImage(
|
|
this.canvas,
|
|
-this.game.width() / 2,
|
|
-this.game.height() / 2,
|
|
this.game.width(),
|
|
this.game.height(),
|
|
);
|
|
if (this.game.inSpawnPhase()) {
|
|
context.drawImage(
|
|
this.highlightCanvas,
|
|
-this.game.width() / 2,
|
|
-this.game.height() / 2,
|
|
this.game.width(),
|
|
this.game.height(),
|
|
);
|
|
}
|
|
}
|
|
|
|
renderTerritory() {
|
|
let numToRender = Math.floor(this.tileToRenderQueue.size() / 5);
|
|
if (numToRender == 0 || this.game.inSpawnPhase()) {
|
|
numToRender = this.tileToRenderQueue.size();
|
|
}
|
|
|
|
while (numToRender > 0) {
|
|
numToRender--;
|
|
const tile = this.tileToRenderQueue.pop().tile;
|
|
this.paintTerritory(tile);
|
|
for (const neighbor of this.game.neighbors(tile)) {
|
|
this.paintTerritory(neighbor, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
paintTerritory(tile: TileRef, isBorder: boolean = false) {
|
|
if (isBorder && !this.game.hasOwner(tile)) {
|
|
return;
|
|
}
|
|
if (!this.game.hasOwner(tile)) {
|
|
if (this.game.hasFallout(tile)) {
|
|
this.paintCell(
|
|
this.game.x(tile),
|
|
this.game.y(tile),
|
|
this.theme.falloutColor(),
|
|
150,
|
|
);
|
|
return;
|
|
}
|
|
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
|
|
return;
|
|
}
|
|
const owner = this.game.owner(tile) as PlayerView;
|
|
if (this.game.isBorder(tile)) {
|
|
if (
|
|
this.game.nearbyDefenses(tile).filter((u) => u.owner() == owner)
|
|
.length > 0
|
|
) {
|
|
this.paintCell(
|
|
this.game.x(tile),
|
|
this.game.y(tile),
|
|
this.theme.defendedBorderColor(owner.info()),
|
|
255,
|
|
);
|
|
} else {
|
|
this.paintCell(
|
|
this.game.x(tile),
|
|
this.game.y(tile),
|
|
this.theme.borderColor(owner.info()),
|
|
255,
|
|
);
|
|
}
|
|
} else {
|
|
this.paintCell(
|
|
this.game.x(tile),
|
|
this.game.y(tile),
|
|
this.theme.territoryColor(owner.info()),
|
|
150,
|
|
);
|
|
}
|
|
}
|
|
|
|
paintCell(x: number, y: number, color: Colord, alpha: number) {
|
|
const index = y * this.game.width() + 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)
|
|
}
|
|
|
|
enqueueTile(tile: TileRef) {
|
|
this.tileToRenderQueue.push({
|
|
tile: tile,
|
|
lastUpdate: this.game.ticks() + this.random.nextFloat(0, 0.5),
|
|
});
|
|
}
|
|
|
|
paintHighlightCell(cell: Cell, color: Colord, alpha: number) {
|
|
this.clearCell(cell);
|
|
this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString();
|
|
this.highlightContext.fillRect(cell.x, cell.y, 1, 1);
|
|
}
|
|
|
|
clearHighlightCell(cell: Cell) {
|
|
this.highlightContext.clearRect(cell.x, cell.y, 1, 1);
|
|
}
|
|
}
|