mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-07 19:21:30 +00:00
Removed the CanvasTerritoryRenderer and related logic
Refactor TerritoryLayer and Renderer for improved transition handling - Removed unused imports and types, streamlining the TerritoryLayer code. - Replaced the `TerritoryRendererStrategy` interface with direct usage of `TerritoryWebGLRenderer`. - Enhanced transition management by introducing epoch and progress tracking for tile transitions. - Updated rendering logic to utilize the new transition properties, ensuring smoother visual updates. - Removed the CanvasTerritoryRenderer and related logic, consolidating rendering responsibilities within the WebGL renderer.
This commit is contained in:
@@ -1,41 +1,21 @@
|
|||||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
|
||||||
import { Colord } from "colord";
|
import { Colord } from "colord";
|
||||||
import { Theme } from "../../../core/configuration/Config";
|
import { Theme } from "../../../core/configuration/Config";
|
||||||
import { EventBus } from "../../../core/EventBus";
|
import { EventBus } from "../../../core/EventBus";
|
||||||
import {
|
import { ColoredTeams, PlayerType, Team } from "../../../core/game/Game";
|
||||||
ColoredTeams,
|
|
||||||
PlayerType,
|
|
||||||
Team,
|
|
||||||
UnitType,
|
|
||||||
} from "../../../core/game/Game";
|
|
||||||
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
|
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
|
||||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||||
import { UserSettings } from "../../../core/game/UserSettings";
|
import { UserSettings } from "../../../core/game/UserSettings";
|
||||||
import { PseudoRandom } from "../../../core/PseudoRandom";
|
|
||||||
import {
|
import {
|
||||||
AlternateViewEvent,
|
AlternateViewEvent,
|
||||||
ContextMenuEvent,
|
ContextMenuEvent,
|
||||||
DragEvent,
|
|
||||||
MouseOverEvent,
|
MouseOverEvent,
|
||||||
} from "../../InputHandler";
|
} from "../../InputHandler";
|
||||||
import { FrameProfiler } from "../FrameProfiler";
|
import { FrameProfiler } from "../FrameProfiler";
|
||||||
import { TransformHandler } from "../TransformHandler";
|
import { TransformHandler } from "../TransformHandler";
|
||||||
import { Layer } from "./Layer";
|
import { Layer } from "./Layer";
|
||||||
import {
|
|
||||||
CanvasTerritoryRenderer,
|
|
||||||
TerritoryRendererStrategy,
|
|
||||||
WebglTerritoryRenderer,
|
|
||||||
} from "./TerritoryRenderers";
|
|
||||||
import { TerritoryWebGLRenderer } from "./TerritoryWebGLRenderer";
|
import { TerritoryWebGLRenderer } from "./TerritoryWebGLRenderer";
|
||||||
|
|
||||||
type TileTransition = {
|
|
||||||
startTime: number;
|
|
||||||
durationMs: number;
|
|
||||||
highlight: boolean;
|
|
||||||
lastProgressByte: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class TerritoryLayer implements Layer {
|
export class TerritoryLayer implements Layer {
|
||||||
profileName(): string {
|
profileName(): string {
|
||||||
return "TerritoryLayer:renderLayer";
|
return "TerritoryLayer:renderLayer";
|
||||||
@@ -46,13 +26,6 @@ export class TerritoryLayer implements Layer {
|
|||||||
|
|
||||||
private cachedTerritoryPatternsEnabled: boolean | undefined;
|
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;
|
private theme: Theme;
|
||||||
|
|
||||||
// Used for spawn highlighting
|
// Used for spawn highlighting
|
||||||
@@ -60,22 +33,19 @@ export class TerritoryLayer implements Layer {
|
|||||||
private highlightContext: CanvasRenderingContext2D;
|
private highlightContext: CanvasRenderingContext2D;
|
||||||
|
|
||||||
private highlightedTerritory: PlayerView | null = null;
|
private highlightedTerritory: PlayerView | null = null;
|
||||||
private territoryRenderer: TerritoryRendererStrategy | null = null;
|
private territoryRenderer: TerritoryWebGLRenderer | null = null;
|
||||||
|
|
||||||
private alternativeView = false;
|
private alternativeView = false;
|
||||||
private lastDragTime = 0;
|
|
||||||
private nodrawDragDuration = 200;
|
|
||||||
private lastMousePosition: { x: number; y: number } | null = null;
|
private lastMousePosition: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
private refreshRate = 10; //refresh every 10ms
|
|
||||||
private lastRefresh = 0;
|
|
||||||
|
|
||||||
private lastFocusedPlayer: PlayerView | null = null;
|
private lastFocusedPlayer: PlayerView | null = null;
|
||||||
private lastMyPlayerSmallId: number | null = null;
|
private lastMyPlayerSmallId: number | null = null;
|
||||||
private lastPaletteSignature: string | null = null;
|
private lastPaletteSignature: string | null = null;
|
||||||
private tileTransitions: Map<TileRef, TileTransition> = new Map();
|
private transitionActive = false;
|
||||||
private transitionHighlightTiles: TileRef[] = [];
|
private transitionEpoch = 1;
|
||||||
private transitionHighlightAlphas: number[] = [];
|
private transitionProgress = 1;
|
||||||
|
private transitionStartTime = 0;
|
||||||
|
private transitionDurationMs = 100;
|
||||||
private lastGameTick = 0;
|
private lastGameTick = 0;
|
||||||
private lastTickTime = 0;
|
private lastTickTime = 0;
|
||||||
private lastTickDurationMs = 100;
|
private lastTickDurationMs = 100;
|
||||||
@@ -90,23 +60,16 @@ export class TerritoryLayer implements Layer {
|
|||||||
this.theme = game.config().theme();
|
this.theme = game.config().theme();
|
||||||
this.cachedTerritoryPatternsEnabled = undefined;
|
this.cachedTerritoryPatternsEnabled = undefined;
|
||||||
this.lastMyPlayerSmallId = game.myPlayer()?.smallID() ?? null;
|
this.lastMyPlayerSmallId = game.myPlayer()?.smallID() ?? null;
|
||||||
this.lastTickTime = Date.now();
|
this.lastTickTime = this.nowMs();
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldTransform(): boolean {
|
shouldTransform(): boolean {
|
||||||
return true;
|
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() {
|
tick() {
|
||||||
const tickProfile = FrameProfiler.start();
|
const tickProfile = FrameProfiler.start();
|
||||||
const now = Date.now();
|
const now = this.nowMs();
|
||||||
this.updateTickTiming(now);
|
this.updateTickTiming(now);
|
||||||
if (this.game.inSpawnPhase()) {
|
if (this.game.inSpawnPhase()) {
|
||||||
this.spawnHighlight();
|
this.spawnHighlight();
|
||||||
@@ -119,31 +82,9 @@ export class TerritoryLayer implements Layer {
|
|||||||
}
|
}
|
||||||
this.refreshPaletteIfNeeded();
|
this.refreshPaletteIfNeeded();
|
||||||
|
|
||||||
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
|
this.game.recentlyUpdatedTiles().forEach((t) => this.markTile(t));
|
||||||
this.beginTileTransitions(this.game.recentlyUpdatedOwnerTiles(), now);
|
this.beginTileTransitions(this.game.recentlyUpdatedOwnerTiles(), now);
|
||||||
const updates = this.game.updatesSinceLastTick();
|
const updates = this.game.updatesSinceLastTick();
|
||||||
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
|
|
||||||
unitUpdates.forEach((update) => {
|
|
||||||
if (update.unitType === UnitType.DefensePost) {
|
|
||||||
// Only update borders if the defense post is not under construction
|
|
||||||
if (update.underConstruction) {
|
|
||||||
return; // Skip barrier creation while under construction
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Detect alliance mutations
|
// Detect alliance mutations
|
||||||
const myPlayer = this.game.myPlayer();
|
const myPlayer = this.game.myPlayer();
|
||||||
@@ -151,7 +92,7 @@ export class TerritoryLayer implements Layer {
|
|||||||
updates?.[GameUpdateType.BrokeAlliance]?.forEach((update) => {
|
updates?.[GameUpdateType.BrokeAlliance]?.forEach((update) => {
|
||||||
const territory = this.game.playerBySmallID(update.betrayedID);
|
const territory = this.game.playerBySmallID(update.betrayedID);
|
||||||
if (territory && territory instanceof PlayerView) {
|
if (territory && territory instanceof PlayerView) {
|
||||||
this.redrawBorder(territory);
|
this.territoryRenderer?.refreshPalette();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,7 +108,7 @@ export class TerritoryLayer implements Layer {
|
|||||||
: update.request.requestorID;
|
: update.request.requestorID;
|
||||||
const territory = this.game.playerBySmallID(territoryId);
|
const territory = this.game.playerBySmallID(territoryId);
|
||||||
if (territory && territory instanceof PlayerView) {
|
if (territory && territory instanceof PlayerView) {
|
||||||
this.redrawBorder(territory);
|
this.territoryRenderer?.refreshPalette();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -181,23 +122,14 @@ export class TerritoryLayer implements Layer {
|
|||||||
player.id() === myPlayer?.id() ||
|
player.id() === myPlayer?.id() ||
|
||||||
embargoed.id() === myPlayer?.id()
|
embargoed.id() === myPlayer?.id()
|
||||||
) {
|
) {
|
||||||
this.redrawBorder(player, embargoed);
|
this.territoryRenderer?.refreshPalette();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusedPlayer = this.game.focusedPlayer();
|
const focusedPlayer = this.game.focusedPlayer();
|
||||||
if (focusedPlayer !== this.lastFocusedPlayer) {
|
if (focusedPlayer !== this.lastFocusedPlayer) {
|
||||||
if (this.territoryRenderer?.isWebGL()) {
|
this.redraw();
|
||||||
this.redraw();
|
|
||||||
} else {
|
|
||||||
if (this.lastFocusedPlayer) {
|
|
||||||
this.paintPlayerBorder(this.lastFocusedPlayer);
|
|
||||||
}
|
|
||||||
if (focusedPlayer) {
|
|
||||||
this.paintPlayerBorder(focusedPlayer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.lastFocusedPlayer = focusedPlayer;
|
this.lastFocusedPlayer = focusedPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,10 +305,6 @@ export class TerritoryLayer implements Layer {
|
|||||||
this.hoverHighlightOptions(),
|
this.hoverHighlightOptions(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.eventBus.on(DragEvent, () => {
|
|
||||||
// TODO: consider re-enabling this on mobile or low end devices for smoother dragging.
|
|
||||||
// this.lastDragTime = Date.now();
|
|
||||||
});
|
|
||||||
this.redraw();
|
this.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,13 +314,7 @@ export class TerritoryLayer implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateHighlightedTerritory() {
|
private updateHighlightedTerritory() {
|
||||||
const supportsHover =
|
if (!this.lastMousePosition || !this.territoryRenderer) {
|
||||||
this.alternativeView || this.territoryRenderer?.isWebGL() === true;
|
|
||||||
if (!supportsHover) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.lastMousePosition) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,20 +336,9 @@ export class TerritoryLayer implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (previousTerritory?.id() !== this.highlightedTerritory?.id()) {
|
if (previousTerritory?.id() !== this.highlightedTerritory?.id()) {
|
||||||
if (this.territoryRenderer?.isWebGL()) {
|
this.territoryRenderer.setHoveredPlayerId(
|
||||||
this.territoryRenderer.setHover(
|
this.highlightedTerritory?.smallID() ?? null,
|
||||||
this.highlightedTerritory?.smallID() ?? null,
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const territories: PlayerView[] = [];
|
|
||||||
if (previousTerritory) {
|
|
||||||
territories.push(previousTerritory);
|
|
||||||
}
|
|
||||||
if (this.highlightedTerritory) {
|
|
||||||
territories.push(this.highlightedTerritory);
|
|
||||||
}
|
|
||||||
this.redrawBorder(...territories);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,14 +355,10 @@ export class TerritoryLayer implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redraw() {
|
redraw() {
|
||||||
console.log("redrew territory layer");
|
|
||||||
this.lastMyPlayerSmallId = this.game.myPlayer()?.smallID() ?? null;
|
this.lastMyPlayerSmallId = this.game.myPlayer()?.smallID() ?? null;
|
||||||
this.cachedTerritoryPatternsEnabled = this.userSettings.territoryPatterns();
|
this.cachedTerritoryPatternsEnabled = this.userSettings.territoryPatterns();
|
||||||
this.configureRenderers();
|
this.configureRenderers();
|
||||||
this.tileTransitions.clear();
|
this.transitionActive = false;
|
||||||
this.transitionHighlightTiles.length = 0;
|
|
||||||
this.transitionHighlightAlphas.length = 0;
|
|
||||||
this.territoryRenderer?.redraw();
|
|
||||||
|
|
||||||
// Add a second canvas for highlights
|
// Add a second canvas for highlights
|
||||||
this.highlightCanvas = document.createElement("canvas");
|
this.highlightCanvas = document.createElement("canvas");
|
||||||
@@ -462,34 +369,28 @@ export class TerritoryLayer implements Layer {
|
|||||||
this.highlightContext = highlightContext;
|
this.highlightContext = highlightContext;
|
||||||
this.highlightCanvas.width = this.game.width();
|
this.highlightCanvas.width = this.game.width();
|
||||||
this.highlightCanvas.height = this.game.height();
|
this.highlightCanvas.height = this.game.height();
|
||||||
|
|
||||||
this.game.forEachTile((t) => {
|
|
||||||
this.paintTerritory(t);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private configureRenderers() {
|
private configureRenderers() {
|
||||||
this.territoryRenderer = null;
|
const { renderer, reason } = TerritoryWebGLRenderer.create(
|
||||||
|
this.game,
|
||||||
const { renderer } = TerritoryWebGLRenderer.create(this.game, this.theme);
|
this.theme,
|
||||||
if (renderer) {
|
);
|
||||||
const strategy = new WebglTerritoryRenderer(renderer, this.game);
|
if (!renderer) {
|
||||||
strategy.setAlternativeView(this.alternativeView);
|
throw new Error(reason ?? "WebGL2 is required for territory rendering.");
|
||||||
strategy.markAllDirty();
|
|
||||||
strategy.refreshPalette();
|
|
||||||
strategy.setHoverHighlightOptions(this.hoverHighlightOptions());
|
|
||||||
strategy.setHover(this.highlightedTerritory?.smallID() ?? null);
|
|
||||||
this.territoryRenderer = strategy;
|
|
||||||
this.lastPaletteSignature = this.computePaletteSignature();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.territoryRenderer = new CanvasTerritoryRenderer(this.game, this.theme);
|
this.territoryRenderer = renderer;
|
||||||
this.territoryRenderer.setAlternativeView(this.alternativeView);
|
this.territoryRenderer.setAlternativeView(this.alternativeView);
|
||||||
|
this.territoryRenderer.markAllDirty();
|
||||||
|
this.territoryRenderer.refreshPalette();
|
||||||
this.territoryRenderer.setHoverHighlightOptions(
|
this.territoryRenderer.setHoverHighlightOptions(
|
||||||
this.hoverHighlightOptions(),
|
this.hoverHighlightOptions(),
|
||||||
);
|
);
|
||||||
this.lastPaletteSignature = null;
|
this.territoryRenderer.setHoveredPlayerId(
|
||||||
|
this.highlightedTerritory?.smallID() ?? null,
|
||||||
|
);
|
||||||
|
this.lastPaletteSignature = this.computePaletteSignature();
|
||||||
}
|
}
|
||||||
|
|
||||||
private hoverHighlightOptions() {
|
private hoverHighlightOptions() {
|
||||||
@@ -513,58 +414,26 @@ export class TerritoryLayer implements Layer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
redrawBorder(...players: PlayerView[]) {
|
|
||||||
const shouldRefreshPalette = this.territoryRenderer?.isWebGL() ?? false;
|
|
||||||
return Promise.all(
|
|
||||||
players.map(async (player) => {
|
|
||||||
const tiles = await player.borderTiles();
|
|
||||||
tiles.borderTiles.forEach((tile: TileRef) => {
|
|
||||||
this.paintTerritory(tile, true);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
).then(() => {
|
|
||||||
if (shouldRefreshPalette) {
|
|
||||||
this.territoryRenderer?.refreshPalette();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLayer(context: CanvasRenderingContext2D) {
|
renderLayer(context: CanvasRenderingContext2D) {
|
||||||
const now = Date.now();
|
if (!this.territoryRenderer) {
|
||||||
const canRefresh =
|
return;
|
||||||
now > this.lastDragTime + this.nodrawDragDuration &&
|
|
||||||
now > this.lastRefresh + this.refreshRate;
|
|
||||||
if (canRefresh) {
|
|
||||||
this.lastRefresh = now;
|
|
||||||
const renderTerritoryStart = FrameProfiler.start();
|
|
||||||
this.renderTerritory();
|
|
||||||
FrameProfiler.end("TerritoryLayer:renderTerritory", renderTerritoryStart);
|
|
||||||
}
|
}
|
||||||
|
const now = this.nowMs();
|
||||||
this.updateTransitionProgress(now);
|
this.updateTransitionProgress(now);
|
||||||
|
|
||||||
const [topLeft, bottomRight] = this.transformHandler.screenBoundingRect();
|
const renderTerritoryStart = FrameProfiler.start();
|
||||||
const vx0 = Math.max(0, topLeft.x);
|
this.territoryRenderer.render();
|
||||||
const vy0 = Math.max(0, topLeft.y);
|
FrameProfiler.end("TerritoryLayer:renderTerritory", renderTerritoryStart);
|
||||||
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 drawTerritoryStart = FrameProfiler.start();
|
||||||
const h = vy1 - vy0 + 1;
|
context.drawImage(
|
||||||
if (this.territoryRenderer) {
|
this.territoryRenderer.canvas,
|
||||||
this.territoryRenderer.render(
|
-this.game.width() / 2,
|
||||||
context,
|
-this.game.height() / 2,
|
||||||
{
|
this.game.width(),
|
||||||
x: vx0,
|
this.game.height(),
|
||||||
y: vy0,
|
);
|
||||||
width: w,
|
FrameProfiler.end("TerritoryLayer:drawTerritoryCanvas", drawTerritoryStart);
|
||||||
height: h,
|
|
||||||
},
|
|
||||||
canRefresh,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.drawTransitionHighlights(context, now);
|
|
||||||
|
|
||||||
if (this.game.inSpawnPhase()) {
|
if (this.game.inSpawnPhase()) {
|
||||||
const highlightDrawStart = FrameProfiler.start();
|
const highlightDrawStart = FrameProfiler.start();
|
||||||
@@ -582,73 +451,11 @@ export class TerritoryLayer implements Layer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTerritory() {
|
private markTile(tile: TileRef) {
|
||||||
if (!this.territoryRenderer) {
|
this.territoryRenderer?.markTile(tile);
|
||||||
return;
|
|
||||||
}
|
|
||||||
let numToRender = this.tileToRenderQueue.size();
|
|
||||||
if (numToRender === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useNeighborPaint = !(this.territoryRenderer?.isWebGL() ?? false);
|
|
||||||
const neighborsToPaint: TileRef[] = [];
|
|
||||||
const mainSpan = FrameProfiler.start();
|
|
||||||
while (numToRender > 0) {
|
|
||||||
numToRender--;
|
|
||||||
|
|
||||||
const entry = this.tileToRenderQueue.pop();
|
|
||||||
if (!entry) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tile = entry.tile;
|
|
||||||
this.paintTerritory(tile);
|
|
||||||
|
|
||||||
if (useNeighborPaint) {
|
|
||||||
for (const neighbor of this.game.neighbors(tile)) {
|
|
||||||
neighborsToPaint.push(neighbor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FrameProfiler.end("TerritoryLayer:renderTerritory.mainPaint", mainSpan);
|
|
||||||
|
|
||||||
if (useNeighborPaint && neighborsToPaint.length > 0) {
|
|
||||||
const neighborSpan = FrameProfiler.start();
|
|
||||||
for (const neighbor of neighborsToPaint) {
|
|
||||||
this.paintTerritory(neighbor, true);
|
|
||||||
}
|
|
||||||
FrameProfiler.end(
|
|
||||||
"TerritoryLayer:renderTerritory.neighborPaint",
|
|
||||||
neighborSpan,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
paintTerritory(tile: TileRef, _maybeStaleBorder: boolean = false) {
|
|
||||||
this.territoryRenderer?.paintTile(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTile(tile: TileRef) {
|
|
||||||
this.territoryRenderer?.clearTile(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
paintHighlightTile(tile: TileRef, color: Colord, alpha: number) {
|
||||||
this.clearTile(tile);
|
|
||||||
const x = this.game.x(tile);
|
const x = this.game.x(tile);
|
||||||
const y = this.game.y(tile);
|
const y = this.game.y(tile);
|
||||||
this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString();
|
this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString();
|
||||||
@@ -700,6 +507,10 @@ export class TerritoryLayer implements Layer {
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private nowMs(): number {
|
||||||
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
private updateTickTiming(now: number) {
|
private updateTickTiming(now: number) {
|
||||||
const currentTick = this.game.ticks();
|
const currentTick = this.game.ticks();
|
||||||
if (currentTick === this.lastGameTick) {
|
if (currentTick === this.lastGameTick) {
|
||||||
@@ -722,87 +533,46 @@ export class TerritoryLayer implements Layer {
|
|||||||
if (changes.length === 0) {
|
if (changes.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const durationMs = this.lastTickDurationMs;
|
this.transitionEpoch = (this.transitionEpoch + 1) & 0xff;
|
||||||
|
if (this.transitionEpoch === 0) {
|
||||||
|
this.transitionEpoch = 1;
|
||||||
|
}
|
||||||
|
this.transitionStartTime = now;
|
||||||
|
this.transitionDurationMs = this.lastTickDurationMs;
|
||||||
|
this.transitionProgress = 0;
|
||||||
|
this.transitionActive = false;
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
if (change.newOwner === change.previousOwner) {
|
if (change.newOwner === change.previousOwner) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (change.newOwner === 0) {
|
if (this.territoryRenderer) {
|
||||||
this.tileTransitions.delete(change.tile);
|
this.territoryRenderer.setTransitionEpoch(
|
||||||
this.territoryRenderer?.setTransitionProgress(change.tile, 1);
|
change.tile,
|
||||||
continue;
|
this.transitionEpoch,
|
||||||
|
);
|
||||||
|
this.transitionActive = true;
|
||||||
}
|
}
|
||||||
this.tileTransitions.set(change.tile, {
|
|
||||||
startTime: now,
|
|
||||||
durationMs,
|
|
||||||
highlight: change.newOwner !== 0,
|
|
||||||
lastProgressByte: -1,
|
|
||||||
});
|
|
||||||
this.territoryRenderer?.setTransitionProgress(change.tile, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTransitionProgress(now: number) {
|
private updateTransitionProgress(now: number) {
|
||||||
this.transitionHighlightTiles.length = 0;
|
if (!this.territoryRenderer || !this.transitionActive) {
|
||||||
this.transitionHighlightAlphas.length = 0;
|
|
||||||
if (!this.territoryRenderer || this.tileTransitions.size === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const elapsed = now - this.transitionStartTime;
|
||||||
const toDelete: TileRef[] = [];
|
const duration =
|
||||||
for (const [tile, transition] of this.tileTransitions) {
|
this.transitionDurationMs > 0 ? this.transitionDurationMs : 1;
|
||||||
const elapsed = now - transition.startTime;
|
const progress = Math.max(0, Math.min(1, elapsed / duration));
|
||||||
const duration = transition.durationMs > 0 ? transition.durationMs : 1;
|
const eased = progress * progress * (3 - 2 * progress);
|
||||||
const progress = Math.max(0, Math.min(1, elapsed / duration));
|
this.transitionProgress = eased;
|
||||||
const progressByte = Math.round(progress * 255);
|
this.territoryRenderer.setTransitionProgress(
|
||||||
if (progressByte !== transition.lastProgressByte) {
|
this.transitionProgress,
|
||||||
transition.lastProgressByte = progressByte;
|
this.transitionEpoch,
|
||||||
this.territoryRenderer.setTransitionProgress(tile, progress);
|
);
|
||||||
}
|
if (progress >= 1) {
|
||||||
if (transition.highlight && progress < 1) {
|
this.transitionActive = false;
|
||||||
const alpha = (1 - progress) * 0.35;
|
|
||||||
if (alpha > 0.01) {
|
|
||||||
this.transitionHighlightTiles.push(tile);
|
|
||||||
this.transitionHighlightAlphas.push(alpha);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (progress >= 1) {
|
|
||||||
toDelete.push(tile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const tile of toDelete) {
|
|
||||||
this.tileTransitions.delete(tile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawTransitionHighlights(
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
now: number,
|
|
||||||
) {
|
|
||||||
if (this.transitionHighlightTiles.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pulse = 0.75 + 0.25 * Math.sin((now - this.lastTickTime) * 0.015);
|
|
||||||
const highlight = this.theme.spawnHighlightColor();
|
|
||||||
const offsetX = -this.game.width() / 2;
|
|
||||||
const offsetY = -this.game.height() / 2;
|
|
||||||
context.save();
|
|
||||||
context.fillStyle = highlight.toRgbString();
|
|
||||||
for (let i = 0; i < this.transitionHighlightTiles.length; i++) {
|
|
||||||
const alpha = this.transitionHighlightAlphas[i] * pulse;
|
|
||||||
if (alpha <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const tile = this.transitionHighlightTiles[i];
|
|
||||||
context.globalAlpha = alpha;
|
|
||||||
context.fillRect(
|
|
||||||
this.game.x(tile) + offsetX,
|
|
||||||
this.game.y(tile) + offsetY,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
context.restore();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private computePaletteSignature(): string {
|
private computePaletteSignature(): string {
|
||||||
@@ -815,7 +585,7 @@ export class TerritoryLayer implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private refreshPaletteIfNeeded() {
|
private refreshPaletteIfNeeded() {
|
||||||
if (!this.territoryRenderer?.isWebGL()) {
|
if (!this.territoryRenderer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const signature = this.computePaletteSignature();
|
const signature = this.computePaletteSignature();
|
||||||
|
|||||||
@@ -1,324 +0,0 @@
|
|||||||
import { Colord } from "colord";
|
|
||||||
import { Theme } from "../../../core/configuration/Config";
|
|
||||||
import { TileRef } from "../../../core/game/GameMap";
|
|
||||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
|
||||||
import { FrameProfiler } from "../FrameProfiler";
|
|
||||||
import {
|
|
||||||
HoverHighlightOptions,
|
|
||||||
TerritoryWebGLRenderer,
|
|
||||||
} from "./TerritoryWebGLRenderer";
|
|
||||||
|
|
||||||
export interface TerritoryRendererStrategy {
|
|
||||||
isWebGL(): boolean;
|
|
||||||
redraw(): void;
|
|
||||||
markAllDirty(): void;
|
|
||||||
paintTile(tile: TileRef): void;
|
|
||||||
setTransitionProgress(tile: TileRef, progress: number): void;
|
|
||||||
render(
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
viewport: { x: number; y: number; width: number; height: number },
|
|
||||||
shouldBlit: boolean,
|
|
||||||
): void;
|
|
||||||
setAlternativeView(enabled: boolean): void;
|
|
||||||
setHover(playerSmallId: number | null): void;
|
|
||||||
setHoverHighlightOptions(options: HoverHighlightOptions): void;
|
|
||||||
refreshPalette(): void;
|
|
||||||
clearTile(tile: TileRef): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CanvasTerritoryRenderer implements TerritoryRendererStrategy {
|
|
||||||
private canvas: HTMLCanvasElement;
|
|
||||||
private context: CanvasRenderingContext2D;
|
|
||||||
private imageData: ImageData;
|
|
||||||
private alternativeImageData: ImageData;
|
|
||||||
private alternativeView = false;
|
|
||||||
private transitionProgress: Map<TileRef, number> = new Map();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly game: GameView,
|
|
||||||
private readonly theme: Theme,
|
|
||||||
) {
|
|
||||||
this.canvas = document.createElement("canvas");
|
|
||||||
const context = this.canvas.getContext("2d");
|
|
||||||
if (!context) throw new Error("2d context not supported");
|
|
||||||
this.context = context;
|
|
||||||
this.imageData = context.createImageData(1, 1);
|
|
||||||
this.alternativeImageData = context.createImageData(1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
isWebGL(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw() {
|
|
||||||
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.alternativeImageData = this.context.getImageData(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
this.canvas.width,
|
|
||||||
this.canvas.height,
|
|
||||||
);
|
|
||||||
this.initImageData();
|
|
||||||
}
|
|
||||||
|
|
||||||
markAllDirty(): void {
|
|
||||||
// No special handling needed for canvas path.
|
|
||||||
}
|
|
||||||
|
|
||||||
paintTile(tile: TileRef) {
|
|
||||||
const cpuStart = FrameProfiler.start();
|
|
||||||
const hasOwner = this.game.hasOwner(tile);
|
|
||||||
const rawOwner = hasOwner ? this.game.owner(tile) : null;
|
|
||||||
const owner =
|
|
||||||
rawOwner &&
|
|
||||||
typeof (rawOwner as any).isPlayer === "function" &&
|
|
||||||
(rawOwner as any).isPlayer()
|
|
||||||
? (rawOwner as PlayerView)
|
|
||||||
: null;
|
|
||||||
const isBorderTile = this.game.isBorder(tile);
|
|
||||||
const hasFallout = this.game.hasFallout(tile);
|
|
||||||
const isDefended =
|
|
||||||
owner && isBorderTile ? this.game.isDefended(tile) : false;
|
|
||||||
|
|
||||||
const transitionFactor = this.transitionProgress.get(tile) ?? 1;
|
|
||||||
if (!owner) {
|
|
||||||
if (hasFallout) {
|
|
||||||
this.paintTileColor(
|
|
||||||
this.imageData,
|
|
||||||
tile,
|
|
||||||
this.theme.falloutColor(),
|
|
||||||
Math.round(150 * transitionFactor),
|
|
||||||
);
|
|
||||||
this.paintTileColor(
|
|
||||||
this.alternativeImageData,
|
|
||||||
tile,
|
|
||||||
this.theme.falloutColor(),
|
|
||||||
Math.round(150 * transitionFactor),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.clearTile(tile);
|
|
||||||
}
|
|
||||||
FrameProfiler.end("CanvasTerritoryRenderer:paintTile", cpuStart);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const myPlayer = this.game.myPlayer();
|
|
||||||
|
|
||||||
if (isBorderTile) {
|
|
||||||
if (myPlayer) {
|
|
||||||
const alternativeColor = this.alternateViewColor(owner);
|
|
||||||
this.paintTileColor(
|
|
||||||
this.alternativeImageData,
|
|
||||||
tile,
|
|
||||||
alternativeColor,
|
|
||||||
Math.round(255 * transitionFactor),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.paintTileColor(
|
|
||||||
this.imageData,
|
|
||||||
tile,
|
|
||||||
owner.borderColor(tile, isDefended),
|
|
||||||
Math.round(255 * transitionFactor),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Alternative view only shows borders.
|
|
||||||
this.clearAlternativeTile(tile);
|
|
||||||
this.paintTileColor(
|
|
||||||
this.imageData,
|
|
||||||
tile,
|
|
||||||
owner.territoryColor(tile),
|
|
||||||
Math.round(150 * transitionFactor),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
FrameProfiler.end("CanvasTerritoryRenderer:paintTile", cpuStart);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
viewport: { x: number; y: number; width: number; height: number },
|
|
||||||
shouldBlit: boolean,
|
|
||||||
) {
|
|
||||||
const { x, y, width, height } = viewport;
|
|
||||||
if (width <= 0 || height <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (shouldBlit) {
|
|
||||||
const putImageStart = FrameProfiler.start();
|
|
||||||
this.context.putImageData(
|
|
||||||
this.alternativeView ? this.alternativeImageData : this.imageData,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
);
|
|
||||||
FrameProfiler.end("CanvasTerritoryRenderer:putImageData", putImageStart);
|
|
||||||
}
|
|
||||||
|
|
||||||
const drawCanvasStart = FrameProfiler.start();
|
|
||||||
context.drawImage(
|
|
||||||
this.canvas,
|
|
||||||
-this.game.width() / 2,
|
|
||||||
-this.game.height() / 2,
|
|
||||||
this.game.width(),
|
|
||||||
this.game.height(),
|
|
||||||
);
|
|
||||||
FrameProfiler.end("CanvasTerritoryRenderer:drawCanvas", drawCanvasStart);
|
|
||||||
}
|
|
||||||
|
|
||||||
setAlternativeView(enabled: boolean): void {
|
|
||||||
this.alternativeView = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTransitionProgress(tile: TileRef, progress: number): void {
|
|
||||||
const clamped = Math.max(0, Math.min(1, progress));
|
|
||||||
if (clamped >= 1) {
|
|
||||||
if (this.transitionProgress.delete(tile)) {
|
|
||||||
this.paintTile(tile);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const previous = this.transitionProgress.get(tile);
|
|
||||||
if (previous !== undefined && Math.abs(previous - clamped) < 1 / 255) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.transitionProgress.set(tile, clamped);
|
|
||||||
this.paintTile(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
setHover(): void {
|
|
||||||
// Canvas path relies on CPU highlight redraw in TerritoryLayer.
|
|
||||||
}
|
|
||||||
|
|
||||||
setHoverHighlightOptions(): void {
|
|
||||||
// Not used in canvas mode.
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshPalette(): void {
|
|
||||||
// Nothing to refresh for canvas path.
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTile(tile: TileRef) {
|
|
||||||
const offset = tile * 4;
|
|
||||||
this.imageData.data[offset + 3] = 0;
|
|
||||||
this.alternativeImageData.data[offset + 3] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private alternateViewColor(other: PlayerView): Colord {
|
|
||||||
const myPlayer = this.game.myPlayer();
|
|
||||||
if (!myPlayer) {
|
|
||||||
return this.theme.neutralColor();
|
|
||||||
}
|
|
||||||
if (other.smallID() === myPlayer.smallID()) {
|
|
||||||
return this.theme.selfColor();
|
|
||||||
}
|
|
||||||
if (other.isFriendly(myPlayer)) {
|
|
||||||
return this.theme.allyColor();
|
|
||||||
}
|
|
||||||
if (!other.hasEmbargo(myPlayer)) {
|
|
||||||
return this.theme.neutralColor();
|
|
||||||
}
|
|
||||||
return this.theme.enemyColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
private paintTileColor(
|
|
||||||
imageData: ImageData,
|
|
||||||
tile: TileRef,
|
|
||||||
color: Colord,
|
|
||||||
alpha: number,
|
|
||||||
) {
|
|
||||||
const offset = tile * 4;
|
|
||||||
imageData.data[offset] = color.rgba.r;
|
|
||||||
imageData.data[offset + 1] = color.rgba.g;
|
|
||||||
imageData.data[offset + 2] = color.rgba.b;
|
|
||||||
imageData.data[offset + 3] = alpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearAlternativeTile(tile: TileRef) {
|
|
||||||
const offset = tile * 4;
|
|
||||||
this.alternativeImageData.data[offset + 3] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private initImageData() {
|
|
||||||
this.game.forEachTile((tile) => {
|
|
||||||
const offset = tile * 4;
|
|
||||||
this.imageData.data[offset + 3] = 0;
|
|
||||||
this.alternativeImageData.data[offset + 3] = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WebglTerritoryRenderer implements TerritoryRendererStrategy {
|
|
||||||
constructor(
|
|
||||||
private readonly renderer: TerritoryWebGLRenderer,
|
|
||||||
private readonly game: GameView,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
isWebGL(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw(): void {
|
|
||||||
this.markAllDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
markAllDirty(): void {
|
|
||||||
this.renderer.markAllDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
paintTile(tile: TileRef): void {
|
|
||||||
this.renderer.markTile(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTransitionProgress(tile: TileRef, progress: number): void {
|
|
||||||
this.renderer.setTransitionProgress(tile, progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
_viewport: { x: number; y: number; width: number; height: number },
|
|
||||||
_shouldBlit: boolean,
|
|
||||||
): void {
|
|
||||||
const webglRenderStart = FrameProfiler.start();
|
|
||||||
this.renderer.render();
|
|
||||||
FrameProfiler.end("WebglTerritoryRenderer:render", webglRenderStart);
|
|
||||||
|
|
||||||
const drawCanvasStart = FrameProfiler.start();
|
|
||||||
context.drawImage(
|
|
||||||
this.renderer.canvas,
|
|
||||||
-this.game.width() / 2,
|
|
||||||
-this.game.height() / 2,
|
|
||||||
this.game.width(),
|
|
||||||
this.game.height(),
|
|
||||||
);
|
|
||||||
FrameProfiler.end("WebglTerritoryRenderer:drawImage", drawCanvasStart);
|
|
||||||
}
|
|
||||||
|
|
||||||
setAlternativeView(enabled: boolean): void {
|
|
||||||
this.renderer.setAlternativeView(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
setHover(playerSmallId: number | null): void {
|
|
||||||
this.renderer.setHoveredPlayerId(playerSmallId ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
setHoverHighlightOptions(options: HoverHighlightOptions): void {
|
|
||||||
this.renderer.setHoverHighlightOptions(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshPalette(): void {
|
|
||||||
this.renderer.refreshPalette();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTile(): void {
|
|
||||||
// No-op for WebGL; canvas alpha clearing is not used.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -43,6 +43,10 @@ export class TerritoryWebGLRenderer {
|
|||||||
relations: WebGLUniformLocation | null;
|
relations: WebGLUniformLocation | null;
|
||||||
patterns: WebGLUniformLocation | null;
|
patterns: WebGLUniformLocation | null;
|
||||||
transitions: WebGLUniformLocation | null;
|
transitions: WebGLUniformLocation | null;
|
||||||
|
transitionEpoch: WebGLUniformLocation | null;
|
||||||
|
transitionProgress: WebGLUniformLocation | null;
|
||||||
|
transitionHintColor: WebGLUniformLocation | null;
|
||||||
|
transitionHintStrength: WebGLUniformLocation | null;
|
||||||
patternStride: WebGLUniformLocation | null;
|
patternStride: WebGLUniformLocation | null;
|
||||||
patternRows: WebGLUniformLocation | null;
|
patternRows: WebGLUniformLocation | null;
|
||||||
fallout: WebGLUniformLocation | null;
|
fallout: WebGLUniformLocation | null;
|
||||||
@@ -62,7 +66,7 @@ export class TerritoryWebGLRenderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly state: Uint16Array;
|
private readonly state: Uint16Array;
|
||||||
private readonly transitionState: Uint8Array;
|
private readonly transitionEpochState: Uint8Array;
|
||||||
private readonly dirtyRows: Map<number, DirtySpan> = new Map();
|
private readonly dirtyRows: Map<number, DirtySpan> = new Map();
|
||||||
private readonly transitionDirtyRows: Map<number, DirtySpan> = new Map();
|
private readonly transitionDirtyRows: Map<number, DirtySpan> = new Map();
|
||||||
private needsFullUpload = true;
|
private needsFullUpload = true;
|
||||||
@@ -75,6 +79,10 @@ export class TerritoryWebGLRenderer {
|
|||||||
private hoverPulseSpeed = Math.PI * 2;
|
private hoverPulseSpeed = Math.PI * 2;
|
||||||
private hoveredPlayerId = -1;
|
private hoveredPlayerId = -1;
|
||||||
private animationStartTime = Date.now();
|
private animationStartTime = Date.now();
|
||||||
|
private transitionEpoch = 1;
|
||||||
|
private transitionProgress = 1;
|
||||||
|
private transitionHintColor: [number, number, number] = [1, 1, 1];
|
||||||
|
private transitionHintStrength = 0.25;
|
||||||
private readonly userSettings = new UserSettings();
|
private readonly userSettings = new UserSettings();
|
||||||
private readonly patternBytesCache = new Map<string, Uint8Array>();
|
private readonly patternBytesCache = new Map<string, Uint8Array>();
|
||||||
|
|
||||||
@@ -88,8 +96,7 @@ export class TerritoryWebGLRenderer {
|
|||||||
this.canvas.height = game.height();
|
this.canvas.height = game.height();
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.transitionState = new Uint8Array(state.length);
|
this.transitionEpochState = new Uint8Array(state.length);
|
||||||
this.transitionState.fill(255);
|
|
||||||
|
|
||||||
this.gl = this.canvas.getContext("webgl2", {
|
this.gl = this.canvas.getContext("webgl2", {
|
||||||
premultipliedAlpha: true,
|
premultipliedAlpha: true,
|
||||||
@@ -113,6 +120,10 @@ export class TerritoryWebGLRenderer {
|
|||||||
relations: null,
|
relations: null,
|
||||||
patterns: null,
|
patterns: null,
|
||||||
transitions: null,
|
transitions: null,
|
||||||
|
transitionEpoch: null,
|
||||||
|
transitionProgress: null,
|
||||||
|
transitionHintColor: null,
|
||||||
|
transitionHintStrength: null,
|
||||||
patternStride: null,
|
patternStride: null,
|
||||||
patternRows: null,
|
patternRows: null,
|
||||||
fallout: null,
|
fallout: null,
|
||||||
@@ -150,6 +161,10 @@ export class TerritoryWebGLRenderer {
|
|||||||
relations: null,
|
relations: null,
|
||||||
patterns: null,
|
patterns: null,
|
||||||
transitions: null,
|
transitions: null,
|
||||||
|
transitionEpoch: null,
|
||||||
|
transitionProgress: null,
|
||||||
|
transitionHintColor: null,
|
||||||
|
transitionHintStrength: null,
|
||||||
patternStride: null,
|
patternStride: null,
|
||||||
patternRows: null,
|
patternRows: null,
|
||||||
fallout: null,
|
fallout: null,
|
||||||
@@ -177,6 +192,19 @@ export class TerritoryWebGLRenderer {
|
|||||||
relations: gl.getUniformLocation(this.program, "u_relations"),
|
relations: gl.getUniformLocation(this.program, "u_relations"),
|
||||||
patterns: gl.getUniformLocation(this.program, "u_patterns"),
|
patterns: gl.getUniformLocation(this.program, "u_patterns"),
|
||||||
transitions: gl.getUniformLocation(this.program, "u_transitions"),
|
transitions: gl.getUniformLocation(this.program, "u_transitions"),
|
||||||
|
transitionEpoch: gl.getUniformLocation(this.program, "u_transitionEpoch"),
|
||||||
|
transitionProgress: gl.getUniformLocation(
|
||||||
|
this.program,
|
||||||
|
"u_transitionProgress",
|
||||||
|
),
|
||||||
|
transitionHintColor: gl.getUniformLocation(
|
||||||
|
this.program,
|
||||||
|
"u_transitionHintColor",
|
||||||
|
),
|
||||||
|
transitionHintStrength: gl.getUniformLocation(
|
||||||
|
this.program,
|
||||||
|
"u_transitionHintStrength",
|
||||||
|
),
|
||||||
patternStride: gl.getUniformLocation(this.program, "u_patternStride"),
|
patternStride: gl.getUniformLocation(this.program, "u_patternStride"),
|
||||||
patternRows: gl.getUniformLocation(this.program, "u_patternRows"),
|
patternRows: gl.getUniformLocation(this.program, "u_patternRows"),
|
||||||
fallout: gl.getUniformLocation(this.program, "u_fallout"),
|
fallout: gl.getUniformLocation(this.program, "u_fallout"),
|
||||||
@@ -274,7 +302,7 @@ export class TerritoryWebGLRenderer {
|
|||||||
0,
|
0,
|
||||||
gl.RED_INTEGER,
|
gl.RED_INTEGER,
|
||||||
gl.UNSIGNED_BYTE,
|
gl.UNSIGNED_BYTE,
|
||||||
this.transitionState,
|
this.transitionEpochState,
|
||||||
);
|
);
|
||||||
|
|
||||||
gl.useProgram(this.program);
|
gl.useProgram(this.program);
|
||||||
@@ -370,6 +398,27 @@ export class TerritoryWebGLRenderer {
|
|||||||
if (this.uniforms.hoverPulseSpeed) {
|
if (this.uniforms.hoverPulseSpeed) {
|
||||||
gl.uniform1f(this.uniforms.hoverPulseSpeed, this.hoverPulseSpeed);
|
gl.uniform1f(this.uniforms.hoverPulseSpeed, this.hoverPulseSpeed);
|
||||||
}
|
}
|
||||||
|
if (this.uniforms.transitionEpoch) {
|
||||||
|
gl.uniform1i(this.uniforms.transitionEpoch, this.transitionEpoch);
|
||||||
|
}
|
||||||
|
if (this.uniforms.transitionProgress) {
|
||||||
|
gl.uniform1f(this.uniforms.transitionProgress, this.transitionProgress);
|
||||||
|
}
|
||||||
|
if (this.uniforms.transitionHintColor) {
|
||||||
|
this.transitionHintColor = [1, 1, 1];
|
||||||
|
gl.uniform3f(
|
||||||
|
this.uniforms.transitionHintColor,
|
||||||
|
this.transitionHintColor[0],
|
||||||
|
this.transitionHintColor[1],
|
||||||
|
this.transitionHintColor[2],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.uniforms.transitionHintStrength) {
|
||||||
|
gl.uniform1f(
|
||||||
|
this.uniforms.transitionHintStrength,
|
||||||
|
this.transitionHintStrength,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
gl.enable(gl.BLEND);
|
gl.enable(gl.BLEND);
|
||||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
@@ -382,8 +431,7 @@ export class TerritoryWebGLRenderer {
|
|||||||
if (state.length !== expected) {
|
if (state.length !== expected) {
|
||||||
return {
|
return {
|
||||||
renderer: null,
|
renderer: null,
|
||||||
reason:
|
reason: "Tile state buffer size mismatch; WebGL renderer disabled.",
|
||||||
"Tile state buffer size mismatch; falling back to canvas territory draw.",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +439,7 @@ export class TerritoryWebGLRenderer {
|
|||||||
if (!renderer.isValid()) {
|
if (!renderer.isValid()) {
|
||||||
return {
|
return {
|
||||||
renderer: null,
|
renderer: null,
|
||||||
reason: "WebGL2 not available; falling back to canvas territory draw.",
|
reason: "WebGL2 not available; WebGL renderer disabled.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { renderer };
|
return { renderer };
|
||||||
@@ -444,13 +492,12 @@ export class TerritoryWebGLRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTransitionProgress(tile: TileRef, progress: number) {
|
setTransitionEpoch(tile: TileRef, epoch: number) {
|
||||||
const clamped = Math.max(0, Math.min(1, progress));
|
const value = epoch & 0xff;
|
||||||
const value = Math.round(clamped * 255);
|
if (this.transitionEpochState[tile] === value) {
|
||||||
if (this.transitionState[tile] === value) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.transitionState[tile] = value;
|
this.transitionEpochState[tile] = value;
|
||||||
if (this.needsTransitionFullUpload) {
|
if (this.needsTransitionFullUpload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -465,9 +512,16 @@ export class TerritoryWebGLRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTransitionProgress(progress: number, epoch: number) {
|
||||||
|
this.transitionEpoch = epoch & 0xff;
|
||||||
|
this.transitionProgress = Math.max(0, Math.min(1, progress));
|
||||||
|
}
|
||||||
|
|
||||||
markAllDirty() {
|
markAllDirty() {
|
||||||
this.needsFullUpload = true;
|
this.needsFullUpload = true;
|
||||||
this.dirtyRows.clear();
|
this.dirtyRows.clear();
|
||||||
|
this.needsTransitionFullUpload = true;
|
||||||
|
this.transitionDirtyRows.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshPalette() {
|
refreshPalette() {
|
||||||
@@ -528,6 +582,12 @@ export class TerritoryWebGLRenderer {
|
|||||||
const viewerId = this.game.myPlayer()?.smallID() ?? 0;
|
const viewerId = this.game.myPlayer()?.smallID() ?? 0;
|
||||||
gl.uniform1i(this.uniforms.viewerId, viewerId);
|
gl.uniform1i(this.uniforms.viewerId, viewerId);
|
||||||
}
|
}
|
||||||
|
if (this.uniforms.transitionEpoch) {
|
||||||
|
gl.uniform1i(this.uniforms.transitionEpoch, this.transitionEpoch);
|
||||||
|
}
|
||||||
|
if (this.uniforms.transitionProgress) {
|
||||||
|
gl.uniform1f(this.uniforms.transitionProgress, this.transitionProgress);
|
||||||
|
}
|
||||||
|
|
||||||
gl.clearColor(0, 0, 0, 0);
|
gl.clearColor(0, 0, 0, 0);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
@@ -611,7 +671,7 @@ export class TerritoryWebGLRenderer {
|
|||||||
0,
|
0,
|
||||||
gl.RED_INTEGER,
|
gl.RED_INTEGER,
|
||||||
gl.UNSIGNED_BYTE,
|
gl.UNSIGNED_BYTE,
|
||||||
this.transitionState,
|
this.transitionEpochState,
|
||||||
);
|
);
|
||||||
this.needsTransitionFullUpload = false;
|
this.needsTransitionFullUpload = false;
|
||||||
this.transitionDirtyRows.clear();
|
this.transitionDirtyRows.clear();
|
||||||
@@ -627,7 +687,10 @@ export class TerritoryWebGLRenderer {
|
|||||||
for (const [y, span] of this.transitionDirtyRows) {
|
for (const [y, span] of this.transitionDirtyRows) {
|
||||||
const width = span.maxX - span.minX + 1;
|
const width = span.maxX - span.minX + 1;
|
||||||
const offset = y * this.canvas.width + span.minX;
|
const offset = y * this.canvas.width + span.minX;
|
||||||
const rowSlice = this.transitionState.subarray(offset, offset + width);
|
const rowSlice = this.transitionEpochState.subarray(
|
||||||
|
offset,
|
||||||
|
offset + width,
|
||||||
|
);
|
||||||
gl.texSubImage2D(
|
gl.texSubImage2D(
|
||||||
gl.TEXTURE_2D,
|
gl.TEXTURE_2D,
|
||||||
0,
|
0,
|
||||||
@@ -834,6 +897,10 @@ export class TerritoryWebGLRenderer {
|
|||||||
uniform usampler2D u_relations;
|
uniform usampler2D u_relations;
|
||||||
uniform usampler2D u_patterns;
|
uniform usampler2D u_patterns;
|
||||||
uniform usampler2D u_transitions;
|
uniform usampler2D u_transitions;
|
||||||
|
uniform int u_transitionEpoch;
|
||||||
|
uniform float u_transitionProgress;
|
||||||
|
uniform vec3 u_transitionHintColor;
|
||||||
|
uniform float u_transitionHintStrength;
|
||||||
uniform int u_patternStride;
|
uniform int u_patternStride;
|
||||||
uniform int u_patternRows;
|
uniform int u_patternRows;
|
||||||
uniform int u_viewerId;
|
uniform int u_viewerId;
|
||||||
@@ -918,7 +985,11 @@ export class TerritoryWebGLRenderer {
|
|||||||
ivec2 texCoord = ivec2(fragCoord.x, int(u_resolution.y) - 1 - fragCoord.y);
|
ivec2 texCoord = ivec2(fragCoord.x, int(u_resolution.y) - 1 - fragCoord.y);
|
||||||
|
|
||||||
uint state = texelFetch(u_state, texCoord, 0).r;
|
uint state = texelFetch(u_state, texCoord, 0).r;
|
||||||
float transition = float(texelFetch(u_transitions, texCoord, 0).r) / 255.0;
|
uint transitionEpoch = texelFetch(u_transitions, texCoord, 0).r;
|
||||||
|
bool inTransition =
|
||||||
|
(u_transitionEpoch != 0) && (transitionEpoch == uint(u_transitionEpoch));
|
||||||
|
float transition = inTransition ? u_transitionProgress : 1.0;
|
||||||
|
float hintMix = inTransition ? u_transitionHintStrength * (1.0 - transition) : 0.0;
|
||||||
uint owner = state & 0xFFFu;
|
uint owner = state & 0xFFFu;
|
||||||
bool hasFallout = (state & 0x2000u) != 0u;
|
bool hasFallout = (state & 0x2000u) != 0u;
|
||||||
bool isDefended = (state & 0x1000u) != 0u;
|
bool isDefended = (state & 0x1000u) != 0u;
|
||||||
@@ -926,7 +997,7 @@ export class TerritoryWebGLRenderer {
|
|||||||
if (owner == 0u) {
|
if (owner == 0u) {
|
||||||
if (hasFallout) {
|
if (hasFallout) {
|
||||||
vec3 color = u_fallout.rgb;
|
vec3 color = u_fallout.rgb;
|
||||||
float a = u_alpha * transition;
|
float a = u_alpha;
|
||||||
outColor = vec4(color * a, a);
|
outColor = vec4(color * a, a);
|
||||||
} else {
|
} else {
|
||||||
outColor = vec4(0.0);
|
outColor = vec4(0.0);
|
||||||
@@ -982,7 +1053,7 @@ export class TerritoryWebGLRenderer {
|
|||||||
float pulse = u_hoverPulseStrength > 0.0
|
float pulse = u_hoverPulseStrength > 0.0
|
||||||
? (1.0 - u_hoverPulseStrength) +
|
? (1.0 - u_hoverPulseStrength) +
|
||||||
u_hoverPulseStrength * (0.5 + 0.5 * sin(u_time * u_hoverPulseSpeed))
|
u_hoverPulseStrength * (0.5 + 0.5 * sin(u_time * u_hoverPulseSpeed))
|
||||||
: 1.0;
|
: 1.0;
|
||||||
color = mix(color, u_hoverHighlightColor, u_hoverHighlightStrength * pulse);
|
color = mix(color, u_hoverHighlightColor, u_hoverHighlightStrength * pulse);
|
||||||
}
|
}
|
||||||
outColor = vec4(color * a, a);
|
outColor = vec4(color * a, a);
|
||||||
@@ -1018,13 +1089,17 @@ export class TerritoryWebGLRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
color = borderColor;
|
color = borderColor;
|
||||||
a = baseBorder.a;
|
a = baseBorder.a * transition;
|
||||||
} else {
|
} else {
|
||||||
bool isPrimary = patternIsPrimary(owner, texCoord);
|
bool isPrimary = patternIsPrimary(owner, texCoord);
|
||||||
color = isPrimary ? base.rgb : baseBorder.rgb;
|
color = isPrimary ? base.rgb : baseBorder.rgb;
|
||||||
a = u_alpha;
|
a = u_alpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hintMix > 0.0) {
|
||||||
|
color = mix(color, u_transitionHintColor, hintMix);
|
||||||
|
}
|
||||||
|
|
||||||
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
|
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
|
||||||
float pulse = u_hoverPulseStrength > 0.0
|
float pulse = u_hoverPulseStrength > 0.0
|
||||||
? (1.0 - u_hoverPulseStrength) +
|
? (1.0 - u_hoverPulseStrength) +
|
||||||
@@ -1033,7 +1108,6 @@ export class TerritoryWebGLRenderer {
|
|||||||
color = mix(color, u_hoverHighlightColor, u_hoverHighlightStrength * pulse);
|
color = mix(color, u_hoverHighlightColor, u_hoverHighlightStrength * pulse);
|
||||||
}
|
}
|
||||||
|
|
||||||
a *= transition;
|
|
||||||
outColor = vec4(color * a, a);
|
outColor = vec4(color * a, a);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user