mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 13:12:03 +00:00
47ccbc0473
## Description: Replace the HTML element pattern grid with a PNG image.   ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors
367 lines
11 KiB
TypeScript
367 lines
11 KiB
TypeScript
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
|
import { Colord } from "colord";
|
|
import { Theme } from "../../../core/configuration/Config";
|
|
import { EventBus } from "../../../core/EventBus";
|
|
import { Cell, PlayerType, UnitType } from "../../../core/game/Game";
|
|
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
|
|
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
|
import { GameView, PlayerView } from "../../../core/game/GameView";
|
|
import { UserSettings } from "../../../core/game/UserSettings";
|
|
import { PseudoRandom } from "../../../core/PseudoRandom";
|
|
import {
|
|
AlternateViewEvent,
|
|
DragEvent,
|
|
RefreshGraphicsEvent,
|
|
} from "../../InputHandler";
|
|
import { TransformHandler } from "../TransformHandler";
|
|
import { Layer } from "./Layer";
|
|
|
|
export class TerritoryLayer implements Layer {
|
|
private userSettings: UserSettings;
|
|
private canvas: HTMLCanvasElement;
|
|
private context: CanvasRenderingContext2D;
|
|
private imageData: ImageData;
|
|
|
|
private cachedTerritoryPatternsEnabled: boolean | undefined;
|
|
|
|
private tileToRenderQueue: PriorityQueue<{
|
|
tile: TileRef;
|
|
lastUpdate: number;
|
|
}> = new PriorityQueue((a, b) => {
|
|
return a.lastUpdate - b.lastUpdate;
|
|
});
|
|
private random = new PseudoRandom(123);
|
|
private theme: Theme;
|
|
|
|
// Used for spawn highlighting
|
|
private highlightCanvas: HTMLCanvasElement;
|
|
private highlightContext: CanvasRenderingContext2D;
|
|
|
|
private alternativeView = false;
|
|
private lastDragTime = 0;
|
|
private nodrawDragDuration = 200;
|
|
|
|
private refreshRate = 10; //refresh every 10ms
|
|
private lastRefresh = 0;
|
|
|
|
private lastFocusedPlayer: PlayerView | null = null;
|
|
|
|
constructor(
|
|
private game: GameView,
|
|
private eventBus: EventBus,
|
|
private transformHandler: TransformHandler,
|
|
userSettings: UserSettings,
|
|
) {
|
|
this.userSettings = userSettings;
|
|
this.theme = game.config().theme();
|
|
this.cachedTerritoryPatternsEnabled = undefined;
|
|
}
|
|
|
|
shouldTransform(): boolean {
|
|
return true;
|
|
}
|
|
|
|
async paintPlayerBorder(player: PlayerView) {
|
|
const tiles = await player.borderTiles();
|
|
tiles.borderTiles.forEach((tile: TileRef) => {
|
|
this.paintTerritory(tile, true); // Immediately paint the tile instead of enqueueing
|
|
});
|
|
}
|
|
|
|
tick() {
|
|
const prev = this.cachedTerritoryPatternsEnabled;
|
|
this.cachedTerritoryPatternsEnabled = this.userSettings.territoryPatterns();
|
|
if (prev !== undefined && prev !== this.cachedTerritoryPatternsEnabled) {
|
|
this.eventBus.emit(new RefreshGraphicsEvent());
|
|
}
|
|
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
|
|
const updates = this.game.updatesSinceLastTick();
|
|
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
|
|
unitUpdates.forEach((update) => {
|
|
if (update.unitType === UnitType.DefensePost) {
|
|
const tile = update.pos;
|
|
this.game
|
|
.bfs(tile, euclDistFN(tile, this.game.config().defensePostRange()))
|
|
.forEach((t) => {
|
|
if (
|
|
this.game.isBorder(t) &&
|
|
(this.game.ownerID(t) === update.ownerID ||
|
|
this.game.ownerID(t) === update.lastOwnerID)
|
|
) {
|
|
this.enqueueTile(t);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const focusedPlayer = this.game.focusedPlayer();
|
|
if (focusedPlayer !== this.lastFocusedPlayer) {
|
|
if (this.lastFocusedPlayer) {
|
|
this.paintPlayerBorder(this.lastFocusedPlayer);
|
|
}
|
|
if (focusedPlayer) {
|
|
this.paintPlayerBorder(focusedPlayer);
|
|
}
|
|
this.lastFocusedPlayer = focusedPlayer;
|
|
}
|
|
|
|
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;
|
|
}
|
|
let color = this.theme.spawnHighlightColor();
|
|
const myPlayer = this.game.myPlayer();
|
|
if (
|
|
myPlayer !== null &&
|
|
myPlayer !== human &&
|
|
myPlayer.isFriendly(human)
|
|
) {
|
|
color = this.theme.selfColor();
|
|
}
|
|
for (const tile of this.game.bfs(
|
|
centerTile,
|
|
euclDistFN(centerTile, 9, true),
|
|
)) {
|
|
if (!this.game.hasOwner(tile)) {
|
|
this.paintHighlightTile(tile, color, 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");
|
|
const context = this.canvas.getContext("2d");
|
|
if (context === null) throw new Error("2d context not supported");
|
|
this.context = context;
|
|
this.canvas.width = this.game.width();
|
|
this.canvas.height = this.game.height();
|
|
|
|
this.imageData = this.context.getImageData(
|
|
0,
|
|
0,
|
|
this.canvas.width,
|
|
this.canvas.height,
|
|
);
|
|
this.initImageData();
|
|
this.context.putImageData(this.imageData, 0, 0);
|
|
|
|
// Add a second canvas for highlights
|
|
this.highlightCanvas = document.createElement("canvas");
|
|
const highlightContext = this.highlightCanvas.getContext("2d", {
|
|
alpha: true,
|
|
});
|
|
if (highlightContext === null) throw new Error("2d context not supported");
|
|
this.highlightContext = highlightContext;
|
|
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) {
|
|
const now = Date.now();
|
|
if (
|
|
now > this.lastDragTime + this.nodrawDragDuration &&
|
|
now > this.lastRefresh + this.refreshRate
|
|
) {
|
|
this.lastRefresh = now;
|
|
this.renderTerritory();
|
|
|
|
const [topLeft, bottomRight] = this.transformHandler.screenBoundingRect();
|
|
const vx0 = Math.max(0, topLeft.x);
|
|
const vy0 = Math.max(0, topLeft.y);
|
|
const vx1 = Math.min(this.game.width() - 1, bottomRight.x);
|
|
const vy1 = Math.min(this.game.height() - 1, bottomRight.y);
|
|
|
|
const w = vx1 - vx0 + 1;
|
|
const h = vy1 - vy0 + 1;
|
|
|
|
if (w > 0 && h > 0) {
|
|
this.context.putImageData(this.imageData, 0, 0, vx0, vy0, w, h);
|
|
}
|
|
}
|
|
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() / 10);
|
|
if (numToRender === 0 || this.game.inSpawnPhase()) {
|
|
numToRender = this.tileToRenderQueue.size();
|
|
}
|
|
|
|
while (numToRender > 0) {
|
|
numToRender--;
|
|
|
|
const entry = this.tileToRenderQueue.pop();
|
|
if (!entry) {
|
|
break;
|
|
}
|
|
|
|
const tile = entry.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.paintTile(tile, this.theme.falloutColor(), 150);
|
|
return;
|
|
}
|
|
this.clearTile(tile);
|
|
return;
|
|
}
|
|
const owner = this.game.owner(tile) as PlayerView;
|
|
if (this.game.isBorder(tile)) {
|
|
const playerIsFocused = owner && this.game.focusedPlayer() === owner;
|
|
if (
|
|
this.game.hasUnitNearby(
|
|
tile,
|
|
this.game.config().defensePostRange(),
|
|
UnitType.DefensePost,
|
|
owner.id(),
|
|
)
|
|
) {
|
|
const borderColors = this.theme.defendedBorderColors(owner);
|
|
const x = this.game.x(tile);
|
|
const y = this.game.y(tile);
|
|
const lightTile =
|
|
(x % 2 === 0 && y % 2 === 0) || (y % 2 === 1 && x % 2 === 1);
|
|
const borderColor = lightTile ? borderColors.light : borderColors.dark;
|
|
this.paintTile(tile, borderColor, 255);
|
|
} else {
|
|
const useBorderColor = playerIsFocused
|
|
? this.theme.focusedBorderColor()
|
|
: this.theme.borderColor(owner);
|
|
this.paintTile(tile, useBorderColor, 255);
|
|
}
|
|
} else {
|
|
const pattern = owner.cosmetics.pattern;
|
|
const patternsEnabled = this.cachedTerritoryPatternsEnabled ?? false;
|
|
if (pattern === undefined || patternsEnabled === false) {
|
|
this.paintTile(tile, this.theme.territoryColor(owner), 150);
|
|
} else {
|
|
const x = this.game.x(tile);
|
|
const y = this.game.y(tile);
|
|
const baseColor = this.theme.territoryColor(owner);
|
|
|
|
const decoder = owner.patternDecoder();
|
|
const color = decoder?.isSet(x, y) ? baseColor.darken(0.2) : baseColor;
|
|
this.paintTile(tile, color, 150);
|
|
}
|
|
}
|
|
}
|
|
|
|
paintTile(tile: TileRef, color: Colord, alpha: number) {
|
|
const offset = tile * 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;
|
|
}
|
|
|
|
clearTile(tile: TileRef) {
|
|
const offset = tile * 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),
|
|
});
|
|
}
|
|
|
|
async enqueuePlayerBorder(player: PlayerView) {
|
|
const playerBorderTiles = await player.borderTiles();
|
|
playerBorderTiles.borderTiles.forEach((tile: TileRef) => {
|
|
this.enqueueTile(tile);
|
|
});
|
|
}
|
|
|
|
paintHighlightTile(tile: TileRef, color: Colord, alpha: number) {
|
|
this.clearTile(tile);
|
|
const x = this.game.x(tile);
|
|
const y = this.game.y(tile);
|
|
this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString();
|
|
this.highlightContext.fillRect(x, y, 1, 1);
|
|
}
|
|
|
|
clearHighlightTile(tile: TileRef) {
|
|
const x = this.game.x(tile);
|
|
const y = this.game.y(tile);
|
|
this.highlightContext.clearRect(x, y, 1, 1);
|
|
}
|
|
}
|