mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:50:43 +00:00
Refactor TerritoryLayer and TerritoryWebGLRenderer for contest management
- Introduced contest management in TerritoryLayer, replacing previous transition handling with a new system for managing contests between tile owners. - Added methods to handle contest state, including starting, updating, and expiring contests. - Updated TerritoryWebGLRenderer to support rendering contest data, including new textures and uniforms for contest owners, IDs, and times. - Enhanced rendering logic to visually represent contest states, including color changes and border effects during contests.
This commit is contained in:
@@ -17,6 +17,18 @@ import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
import { TerritoryWebGLRenderer } from "./TerritoryWebGLRenderer";
|
||||
|
||||
const CONTEST_ID_MASK = 0x7fff;
|
||||
const CONTEST_ATTACKER_EVER_BIT = 0x8000;
|
||||
const CONTEST_TIME_WRAP = 32768;
|
||||
|
||||
type ContestComponent = {
|
||||
id: number;
|
||||
attacker: number;
|
||||
defender: number;
|
||||
lastActivityPacked: number;
|
||||
tiles: TileRef[];
|
||||
};
|
||||
|
||||
export class TerritoryLayer implements Layer {
|
||||
profileName(): string {
|
||||
return "TerritoryLayer:renderLayer";
|
||||
@@ -42,14 +54,15 @@ export class TerritoryLayer implements Layer {
|
||||
private lastFocusedPlayer: PlayerView | null = null;
|
||||
private lastMyPlayerSmallId: number | null = null;
|
||||
private lastPaletteSignature: string | null = null;
|
||||
private transitionActive = false;
|
||||
private transitionDurationMs = 500;
|
||||
private transitionTiles: TileRef[] = [];
|
||||
private transitionStartTimes: Uint16Array | null = null;
|
||||
private transitionActiveMask: Uint8Array | null = null;
|
||||
private lastGameTick = 0;
|
||||
private lastTickTime = 0;
|
||||
private lastTickDurationMs = 100;
|
||||
private contestDurationMs = 5000;
|
||||
private contestActive = false;
|
||||
private contestNextId = 1;
|
||||
private contestFreeIds: number[] = [];
|
||||
private contestComponentIds: Uint16Array | null = null;
|
||||
private contestPrevOwners: Uint16Array | null = null;
|
||||
private contestAttackers: Uint16Array | null = null;
|
||||
private contestTileIndices: Int32Array | null = null;
|
||||
private contestComponents = new Map<number, ContestComponent>();
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
@@ -59,7 +72,6 @@ export class TerritoryLayer implements Layer {
|
||||
this.theme = game.config().theme();
|
||||
this.cachedTerritoryPatternsEnabled = undefined;
|
||||
this.lastMyPlayerSmallId = game.myPlayer()?.smallID() ?? null;
|
||||
this.lastTickTime = this.nowMs();
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
@@ -69,7 +81,6 @@ export class TerritoryLayer implements Layer {
|
||||
tick() {
|
||||
const tickProfile = FrameProfiler.start();
|
||||
const now = this.nowMs();
|
||||
this.updateTickTiming(now);
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.spawnHighlight();
|
||||
}
|
||||
@@ -82,7 +93,7 @@ export class TerritoryLayer implements Layer {
|
||||
this.refreshPaletteIfNeeded();
|
||||
|
||||
this.game.recentlyUpdatedTiles().forEach((t) => this.markTile(t));
|
||||
this.beginTileTransitions(this.game.recentlyUpdatedOwnerTiles(), now);
|
||||
this.applyContestChanges(this.game.recentlyUpdatedOwnerTiles(), now);
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
|
||||
// Detect alliance mutations
|
||||
@@ -343,11 +354,8 @@ export class TerritoryLayer implements Layer {
|
||||
this.lastMyPlayerSmallId = this.game.myPlayer()?.smallID() ?? null;
|
||||
this.cachedTerritoryPatternsEnabled = this.userSettings.territoryPatterns();
|
||||
this.configureRenderers();
|
||||
this.transitionActive = false;
|
||||
this.transitionTiles = [];
|
||||
this.ensureTransitionScratch();
|
||||
this.transitionStartTimes?.fill(0);
|
||||
this.transitionActiveMask?.fill(0);
|
||||
this.ensureContestScratch();
|
||||
this.syncContestStateToRenderer();
|
||||
|
||||
// Add a second canvas for highlights
|
||||
this.highlightCanvas = document.createElement("canvas");
|
||||
@@ -408,7 +416,7 @@ export class TerritoryLayer implements Layer {
|
||||
return;
|
||||
}
|
||||
const now = this.nowMs();
|
||||
this.updateTransitionState(now);
|
||||
this.updateContestState(now);
|
||||
|
||||
const renderTerritoryStart = FrameProfiler.start();
|
||||
this.territoryRenderer.render();
|
||||
@@ -500,116 +508,353 @@ export class TerritoryLayer implements Layer {
|
||||
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
||||
}
|
||||
|
||||
private ensureTransitionScratch() {
|
||||
private ensureContestScratch() {
|
||||
const size = this.game.width() * this.game.height();
|
||||
if (
|
||||
!this.transitionStartTimes ||
|
||||
this.transitionStartTimes.length !== size
|
||||
) {
|
||||
this.transitionStartTimes = new Uint16Array(size);
|
||||
this.transitionActiveMask = new Uint8Array(size);
|
||||
if (!this.contestComponentIds || this.contestComponentIds.length !== size) {
|
||||
this.contestComponentIds = new Uint16Array(size);
|
||||
this.contestPrevOwners = new Uint16Array(size);
|
||||
this.contestAttackers = new Uint16Array(size);
|
||||
this.contestTileIndices = new Int32Array(size);
|
||||
this.contestTileIndices.fill(-1);
|
||||
this.contestComponents.clear();
|
||||
this.contestFreeIds = [];
|
||||
this.contestNextId = 1;
|
||||
this.contestActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
private updateTickTiming(now: number) {
|
||||
const currentTick = this.game.ticks();
|
||||
if (currentTick === this.lastGameTick) {
|
||||
return;
|
||||
}
|
||||
if (this.lastGameTick !== 0) {
|
||||
const tickDelta = Math.max(1, currentTick - this.lastGameTick);
|
||||
const elapsed = now - this.lastTickTime;
|
||||
const estimate = elapsed / tickDelta;
|
||||
this.lastTickDurationMs = Math.max(50, Math.min(200, estimate));
|
||||
}
|
||||
this.lastGameTick = currentTick;
|
||||
this.lastTickTime = now;
|
||||
}
|
||||
|
||||
private beginTileTransitions(
|
||||
private applyContestChanges(
|
||||
changes: Array<{ tile: TileRef; previousOwner: number; newOwner: number }>,
|
||||
now: number,
|
||||
) {
|
||||
if (!this.territoryRenderer) {
|
||||
if (!this.territoryRenderer || changes.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.ensureTransitionScratch();
|
||||
const startTimes = this.transitionStartTimes!;
|
||||
const activeMask = this.transitionActiveMask!;
|
||||
const renderer = this.territoryRenderer;
|
||||
if (changes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const nowPacked = this.packTransitionTime(now);
|
||||
const startPacked = nowPacked | 0x8000;
|
||||
this.ensureContestScratch();
|
||||
const nowPacked = this.packContestTime(now);
|
||||
|
||||
for (const change of changes) {
|
||||
if (change.newOwner === change.previousOwner) {
|
||||
continue;
|
||||
}
|
||||
const tile = change.tile;
|
||||
if (activeMask[tile] === 0) {
|
||||
activeMask[tile] = 1;
|
||||
this.transitionTiles.push(tile);
|
||||
const currentId = this.contestId(tile);
|
||||
if (currentId === 0) {
|
||||
this.startContestForTile(
|
||||
tile,
|
||||
change.previousOwner,
|
||||
change.newOwner,
|
||||
nowPacked,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
startTimes[tile] = nowPacked;
|
||||
renderer.setTransitionTile(tile, change.previousOwner, startPacked);
|
||||
}
|
||||
|
||||
this.transitionActive = this.transitionTiles.length > 0;
|
||||
const component = this.contestComponents.get(currentId);
|
||||
if (!component) {
|
||||
this.clearContestTile(tile);
|
||||
this.startContestForTile(
|
||||
tile,
|
||||
change.previousOwner,
|
||||
change.newOwner,
|
||||
nowPacked,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
change.newOwner === component.attacker ||
|
||||
change.newOwner === component.defender
|
||||
) {
|
||||
const attackerEver =
|
||||
change.newOwner === component.attacker || this.hasAttackerEver(tile);
|
||||
this.setContestTileData(
|
||||
tile,
|
||||
component.defender,
|
||||
component.attacker,
|
||||
component.id,
|
||||
attackerEver,
|
||||
);
|
||||
component.lastActivityPacked = nowPacked;
|
||||
this.territoryRenderer.setContestTime(component.id, nowPacked);
|
||||
} else {
|
||||
this.removeTileFromComponent(tile, component);
|
||||
this.startContestForTile(
|
||||
tile,
|
||||
change.previousOwner,
|
||||
change.newOwner,
|
||||
nowPacked,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateTransitionState(now: number) {
|
||||
private updateContestState(now: number) {
|
||||
if (!this.territoryRenderer) {
|
||||
return;
|
||||
}
|
||||
this.ensureTransitionScratch();
|
||||
const nowPacked = this.packTransitionTime(now);
|
||||
this.territoryRenderer.setTransitionTime(
|
||||
nowPacked,
|
||||
this.transitionDurationMs,
|
||||
);
|
||||
this.ensureContestScratch();
|
||||
const nowPacked = this.packContestTime(now);
|
||||
this.territoryRenderer.setContestNow(nowPacked, this.contestDurationMs);
|
||||
|
||||
if (!this.transitionActive || this.transitionTiles.length === 0) {
|
||||
if (!this.contestActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startTimes = this.transitionStartTimes!;
|
||||
const activeMask = this.transitionActiveMask!;
|
||||
const tiles = this.transitionTiles;
|
||||
const duration = this.transitionDurationMs;
|
||||
let writeIndex = 0;
|
||||
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const tile = tiles[i];
|
||||
const start = startTimes[tile];
|
||||
const elapsed = this.transitionElapsed(nowPacked, start);
|
||||
if (elapsed >= duration) {
|
||||
activeMask[tile] = 0;
|
||||
startTimes[tile] = 0;
|
||||
this.territoryRenderer.clearTransitionTile(
|
||||
tile,
|
||||
this.game.ownerID(tile),
|
||||
);
|
||||
} else {
|
||||
tiles[writeIndex++] = tile;
|
||||
const expired: ContestComponent[] = [];
|
||||
for (const component of this.contestComponents.values()) {
|
||||
const elapsed = this.contestElapsed(
|
||||
nowPacked,
|
||||
component.lastActivityPacked,
|
||||
);
|
||||
if (elapsed >= this.contestDurationMs) {
|
||||
expired.push(component);
|
||||
}
|
||||
}
|
||||
tiles.length = writeIndex;
|
||||
this.transitionActive = tiles.length > 0;
|
||||
|
||||
for (const component of expired) {
|
||||
this.expireContestComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
private packTransitionTime(now: number): number {
|
||||
const wrap = 32768;
|
||||
return Math.floor(now) % wrap | 0;
|
||||
private startContestForTile(
|
||||
tile: TileRef,
|
||||
defender: number,
|
||||
attacker: number,
|
||||
nowPacked: number,
|
||||
) {
|
||||
if (attacker === defender || attacker === 0 || defender === 0) {
|
||||
return;
|
||||
}
|
||||
const neighbors = this.collectNeighborComponents(tile, attacker, defender);
|
||||
let component: ContestComponent;
|
||||
if (neighbors.length === 0) {
|
||||
component = this.createContestComponent(attacker, defender, nowPacked);
|
||||
} else {
|
||||
component = neighbors[0];
|
||||
for (let i = 1; i < neighbors.length; i++) {
|
||||
this.mergeContestComponents(component, neighbors[i]);
|
||||
}
|
||||
}
|
||||
|
||||
this.addTileToComponent(tile, component, true);
|
||||
component.lastActivityPacked = nowPacked;
|
||||
this.territoryRenderer?.setContestTime(component.id, nowPacked);
|
||||
}
|
||||
|
||||
private transitionElapsed(nowPacked: number, startPacked: number): number {
|
||||
const wrap = 32768;
|
||||
private collectNeighborComponents(
|
||||
tile: TileRef,
|
||||
attacker: number,
|
||||
defender: number,
|
||||
): ContestComponent[] {
|
||||
const components: ContestComponent[] = [];
|
||||
const seen = new Set<number>();
|
||||
for (const neighbor of this.game.neighbors(tile)) {
|
||||
const id = this.contestId(neighbor);
|
||||
if (id === 0 || seen.has(id)) {
|
||||
continue;
|
||||
}
|
||||
const component = this.contestComponents.get(id);
|
||||
if (!component) {
|
||||
continue;
|
||||
}
|
||||
if (component.attacker === attacker && component.defender === defender) {
|
||||
components.push(component);
|
||||
seen.add(id);
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
private createContestComponent(
|
||||
attacker: number,
|
||||
defender: number,
|
||||
nowPacked: number,
|
||||
): ContestComponent {
|
||||
const id = this.allocateContestComponentId();
|
||||
const component: ContestComponent = {
|
||||
id,
|
||||
attacker,
|
||||
defender,
|
||||
lastActivityPacked: nowPacked,
|
||||
tiles: [],
|
||||
};
|
||||
this.contestComponents.set(id, component);
|
||||
this.contestActive = true;
|
||||
this.territoryRenderer?.ensureContestTimeCapacity(id);
|
||||
return component;
|
||||
}
|
||||
|
||||
private allocateContestComponentId(): number {
|
||||
const reused = this.contestFreeIds.pop();
|
||||
if (reused !== undefined) {
|
||||
return reused;
|
||||
}
|
||||
return this.contestNextId++;
|
||||
}
|
||||
|
||||
private releaseContestComponentId(id: number) {
|
||||
if (id <= 0) {
|
||||
return;
|
||||
}
|
||||
this.contestFreeIds.push(id);
|
||||
}
|
||||
|
||||
private addTileToComponent(
|
||||
tile: TileRef,
|
||||
component: ContestComponent,
|
||||
attackerEver: boolean,
|
||||
) {
|
||||
this.setContestTileData(
|
||||
tile,
|
||||
component.defender,
|
||||
component.attacker,
|
||||
component.id,
|
||||
attackerEver,
|
||||
);
|
||||
this.contestTileIndices![tile] = component.tiles.length;
|
||||
component.tiles.push(tile);
|
||||
this.contestActive = true;
|
||||
}
|
||||
|
||||
private removeTileFromComponent(tile: TileRef, component: ContestComponent) {
|
||||
const tileIndex = this.contestTileIndices![tile];
|
||||
const tiles = component.tiles;
|
||||
const lastIndex = tiles.length - 1;
|
||||
if (tileIndex >= 0 && tileIndex <= lastIndex) {
|
||||
if (tileIndex !== lastIndex) {
|
||||
const swapTile = tiles[lastIndex];
|
||||
tiles[tileIndex] = swapTile;
|
||||
this.contestTileIndices![swapTile] = tileIndex;
|
||||
}
|
||||
tiles.pop();
|
||||
}
|
||||
this.contestTileIndices![tile] = -1;
|
||||
this.clearContestTile(tile);
|
||||
if (component.tiles.length === 0) {
|
||||
this.contestComponents.delete(component.id);
|
||||
this.releaseContestComponentId(component.id);
|
||||
this.contestActive = this.contestComponents.size > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private mergeContestComponents(
|
||||
target: ContestComponent,
|
||||
source: ContestComponent,
|
||||
) {
|
||||
for (const tile of source.tiles) {
|
||||
const attackerEver = this.hasAttackerEver(tile);
|
||||
this.setContestTileData(
|
||||
tile,
|
||||
target.defender,
|
||||
target.attacker,
|
||||
target.id,
|
||||
attackerEver,
|
||||
);
|
||||
this.contestTileIndices![tile] = target.tiles.length;
|
||||
target.tiles.push(tile);
|
||||
}
|
||||
target.lastActivityPacked = Math.max(
|
||||
target.lastActivityPacked,
|
||||
source.lastActivityPacked,
|
||||
);
|
||||
this.territoryRenderer?.setContestTime(
|
||||
target.id,
|
||||
target.lastActivityPacked,
|
||||
);
|
||||
this.contestComponents.delete(source.id);
|
||||
this.releaseContestComponentId(source.id);
|
||||
}
|
||||
|
||||
private expireContestComponent(component: ContestComponent) {
|
||||
for (const tile of component.tiles) {
|
||||
this.contestTileIndices![tile] = -1;
|
||||
this.clearContestTile(tile);
|
||||
}
|
||||
component.tiles.length = 0;
|
||||
this.contestComponents.delete(component.id);
|
||||
this.releaseContestComponentId(component.id);
|
||||
this.contestActive = this.contestComponents.size > 0;
|
||||
}
|
||||
|
||||
private setContestTileData(
|
||||
tile: TileRef,
|
||||
defender: number,
|
||||
attacker: number,
|
||||
componentId: number,
|
||||
attackerEver: boolean,
|
||||
) {
|
||||
this.contestPrevOwners![tile] = defender;
|
||||
this.contestAttackers![tile] = attacker;
|
||||
this.contestComponentIds![tile] =
|
||||
(componentId & CONTEST_ID_MASK) |
|
||||
(attackerEver ? CONTEST_ATTACKER_EVER_BIT : 0);
|
||||
this.territoryRenderer?.setContestTile(
|
||||
tile,
|
||||
defender,
|
||||
attacker,
|
||||
componentId,
|
||||
attackerEver,
|
||||
);
|
||||
}
|
||||
|
||||
private clearContestTile(tile: TileRef) {
|
||||
this.contestPrevOwners![tile] = 0;
|
||||
this.contestAttackers![tile] = 0;
|
||||
this.contestComponentIds![tile] = 0;
|
||||
this.territoryRenderer?.clearContestTile(tile);
|
||||
}
|
||||
|
||||
private contestId(tile: TileRef): number {
|
||||
return this.contestComponentIds![tile] & CONTEST_ID_MASK;
|
||||
}
|
||||
|
||||
private hasAttackerEver(tile: TileRef): boolean {
|
||||
return (this.contestComponentIds![tile] & CONTEST_ATTACKER_EVER_BIT) !== 0;
|
||||
}
|
||||
|
||||
private packContestTime(now: number): number {
|
||||
return Math.floor(now) % CONTEST_TIME_WRAP;
|
||||
}
|
||||
|
||||
private contestElapsed(nowPacked: number, startPacked: number): number {
|
||||
if (nowPacked >= startPacked) {
|
||||
return nowPacked - startPacked;
|
||||
}
|
||||
return wrap - startPacked + nowPacked;
|
||||
return CONTEST_TIME_WRAP - startPacked + nowPacked;
|
||||
}
|
||||
|
||||
private syncContestStateToRenderer() {
|
||||
if (!this.territoryRenderer) {
|
||||
return;
|
||||
}
|
||||
if (!this.contestComponentIds) {
|
||||
return;
|
||||
}
|
||||
this.contestActive = this.contestComponents.size > 0;
|
||||
let maxId = 0;
|
||||
for (const component of this.contestComponents.values()) {
|
||||
maxId = Math.max(maxId, component.id);
|
||||
}
|
||||
if (maxId > 0) {
|
||||
this.territoryRenderer.ensureContestTimeCapacity(maxId);
|
||||
}
|
||||
for (const component of this.contestComponents.values()) {
|
||||
this.territoryRenderer.setContestTime(
|
||||
component.id,
|
||||
component.lastActivityPacked,
|
||||
);
|
||||
for (const tile of component.tiles) {
|
||||
const packed = this.contestComponentIds![tile];
|
||||
const attackerEver = (packed & CONTEST_ATTACKER_EVER_BIT) !== 0;
|
||||
this.territoryRenderer.setContestTile(
|
||||
tile,
|
||||
component.defender,
|
||||
component.attacker,
|
||||
component.id,
|
||||
attackerEver,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private computePaletteSignature(): string {
|
||||
|
||||
@@ -35,16 +35,20 @@ export class TerritoryWebGLRenderer {
|
||||
private readonly paletteTexture: WebGLTexture | null;
|
||||
private readonly relationTexture: WebGLTexture | null;
|
||||
private readonly patternTexture: WebGLTexture | null;
|
||||
private readonly transitionTexture: WebGLTexture | null;
|
||||
private readonly contestOwnersTexture: WebGLTexture | null;
|
||||
private readonly contestIdsTexture: WebGLTexture | null;
|
||||
private readonly contestTimesTexture: WebGLTexture | null;
|
||||
private readonly uniforms: {
|
||||
resolution: WebGLUniformLocation | null;
|
||||
state: WebGLUniformLocation | null;
|
||||
palette: WebGLUniformLocation | null;
|
||||
relations: WebGLUniformLocation | null;
|
||||
patterns: WebGLUniformLocation | null;
|
||||
transitions: WebGLUniformLocation | null;
|
||||
transitionNow: WebGLUniformLocation | null;
|
||||
transitionDuration: WebGLUniformLocation | null;
|
||||
contestOwners: WebGLUniformLocation | null;
|
||||
contestIds: WebGLUniformLocation | null;
|
||||
contestTimes: WebGLUniformLocation | null;
|
||||
contestNow: WebGLUniformLocation | null;
|
||||
contestDuration: WebGLUniformLocation | null;
|
||||
patternStride: WebGLUniformLocation | null;
|
||||
patternRows: WebGLUniformLocation | null;
|
||||
fallout: WebGLUniformLocation | null;
|
||||
@@ -64,11 +68,14 @@ export class TerritoryWebGLRenderer {
|
||||
};
|
||||
|
||||
private readonly state: Uint16Array;
|
||||
private readonly transitionState: Uint16Array;
|
||||
private contestOwnersState: Uint16Array;
|
||||
private contestIdsState: Uint16Array;
|
||||
private contestTimesState: Uint16Array;
|
||||
private readonly dirtyRows: Map<number, DirtySpan> = new Map();
|
||||
private readonly transitionDirtyRows: Map<number, DirtySpan> = new Map();
|
||||
private readonly contestDirtyRows: Map<number, DirtySpan> = new Map();
|
||||
private needsFullUpload = true;
|
||||
private needsTransitionFullUpload = true;
|
||||
private needsContestFullUpload = true;
|
||||
private needsContestTimesUpload = true;
|
||||
private alternativeView = false;
|
||||
private paletteWidth = 0;
|
||||
private hoverHighlightStrength = 0.7;
|
||||
@@ -77,8 +84,8 @@ export class TerritoryWebGLRenderer {
|
||||
private hoverPulseSpeed = Math.PI * 2;
|
||||
private hoveredPlayerId = -1;
|
||||
private animationStartTime = Date.now();
|
||||
private transitionNow = 0;
|
||||
private transitionDurationMs = 500;
|
||||
private contestNow = 0;
|
||||
private contestDurationMs = 5000;
|
||||
private readonly userSettings = new UserSettings();
|
||||
private readonly patternBytesCache = new Map<string, Uint8Array>();
|
||||
|
||||
@@ -92,11 +99,9 @@ export class TerritoryWebGLRenderer {
|
||||
this.canvas.height = game.height();
|
||||
|
||||
this.state = state;
|
||||
this.transitionState = new Uint16Array(state.length * 2);
|
||||
for (let i = 0; i < state.length; i++) {
|
||||
this.transitionState[i * 2] = state[i] & 0x0fff;
|
||||
this.transitionState[i * 2 + 1] = 0;
|
||||
}
|
||||
this.contestOwnersState = new Uint16Array(state.length * 2);
|
||||
this.contestIdsState = new Uint16Array(state.length);
|
||||
this.contestTimesState = new Uint16Array(1);
|
||||
|
||||
this.gl = this.canvas.getContext("webgl2", {
|
||||
premultipliedAlpha: true,
|
||||
@@ -112,16 +117,20 @@ export class TerritoryWebGLRenderer {
|
||||
this.paletteTexture = null;
|
||||
this.relationTexture = null;
|
||||
this.patternTexture = null;
|
||||
this.transitionTexture = null;
|
||||
this.contestOwnersTexture = null;
|
||||
this.contestIdsTexture = null;
|
||||
this.contestTimesTexture = null;
|
||||
this.uniforms = {
|
||||
resolution: null,
|
||||
state: null,
|
||||
palette: null,
|
||||
relations: null,
|
||||
patterns: null,
|
||||
transitions: null,
|
||||
transitionNow: null,
|
||||
transitionDuration: null,
|
||||
contestOwners: null,
|
||||
contestIds: null,
|
||||
contestTimes: null,
|
||||
contestNow: null,
|
||||
contestDuration: null,
|
||||
patternStride: null,
|
||||
patternRows: null,
|
||||
fallout: null,
|
||||
@@ -151,16 +160,20 @@ export class TerritoryWebGLRenderer {
|
||||
this.paletteTexture = null;
|
||||
this.relationTexture = null;
|
||||
this.patternTexture = null;
|
||||
this.transitionTexture = null;
|
||||
this.contestOwnersTexture = null;
|
||||
this.contestIdsTexture = null;
|
||||
this.contestTimesTexture = null;
|
||||
this.uniforms = {
|
||||
resolution: null,
|
||||
state: null,
|
||||
palette: null,
|
||||
relations: null,
|
||||
patterns: null,
|
||||
transitions: null,
|
||||
transitionNow: null,
|
||||
transitionDuration: null,
|
||||
contestOwners: null,
|
||||
contestIds: null,
|
||||
contestTimes: null,
|
||||
contestNow: null,
|
||||
contestDuration: null,
|
||||
patternStride: null,
|
||||
patternRows: null,
|
||||
fallout: null,
|
||||
@@ -187,11 +200,13 @@ export class TerritoryWebGLRenderer {
|
||||
palette: gl.getUniformLocation(this.program, "u_palette"),
|
||||
relations: gl.getUniformLocation(this.program, "u_relations"),
|
||||
patterns: gl.getUniformLocation(this.program, "u_patterns"),
|
||||
transitions: gl.getUniformLocation(this.program, "u_transitions"),
|
||||
transitionNow: gl.getUniformLocation(this.program, "u_transitionNow"),
|
||||
transitionDuration: gl.getUniformLocation(
|
||||
contestOwners: gl.getUniformLocation(this.program, "u_contestOwners"),
|
||||
contestIds: gl.getUniformLocation(this.program, "u_contestIds"),
|
||||
contestTimes: gl.getUniformLocation(this.program, "u_contestTimes"),
|
||||
contestNow: gl.getUniformLocation(this.program, "u_contestNow"),
|
||||
contestDuration: gl.getUniformLocation(
|
||||
this.program,
|
||||
"u_transitionDurationMs",
|
||||
"u_contestDurationMs",
|
||||
),
|
||||
patternStride: gl.getUniformLocation(this.program, "u_patternStride"),
|
||||
patternRows: gl.getUniformLocation(this.program, "u_patternRows"),
|
||||
@@ -251,7 +266,9 @@ export class TerritoryWebGLRenderer {
|
||||
this.paletteTexture = gl.createTexture();
|
||||
this.relationTexture = gl.createTexture();
|
||||
this.patternTexture = gl.createTexture();
|
||||
this.transitionTexture = gl.createTexture();
|
||||
this.contestOwnersTexture = gl.createTexture();
|
||||
this.contestIdsTexture = gl.createTexture();
|
||||
this.contestTimesTexture = gl.createTexture();
|
||||
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.stateTexture);
|
||||
@@ -275,7 +292,7 @@ export class TerritoryWebGLRenderer {
|
||||
this.uploadPalette();
|
||||
|
||||
gl.activeTexture(gl.TEXTURE4);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.transitionTexture);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.contestOwnersTexture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
@@ -290,7 +307,45 @@ export class TerritoryWebGLRenderer {
|
||||
0,
|
||||
gl.RG_INTEGER,
|
||||
gl.UNSIGNED_SHORT,
|
||||
this.transitionState,
|
||||
this.contestOwnersState,
|
||||
);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE5);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.contestIdsTexture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.R16UI,
|
||||
this.canvas.width,
|
||||
this.canvas.height,
|
||||
0,
|
||||
gl.RED_INTEGER,
|
||||
gl.UNSIGNED_SHORT,
|
||||
this.contestIdsState,
|
||||
);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE6);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.contestTimesTexture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.R16UI,
|
||||
this.contestTimesState.length,
|
||||
1,
|
||||
0,
|
||||
gl.RED_INTEGER,
|
||||
gl.UNSIGNED_SHORT,
|
||||
this.contestTimesState,
|
||||
);
|
||||
|
||||
gl.useProgram(this.program);
|
||||
@@ -298,7 +353,9 @@ export class TerritoryWebGLRenderer {
|
||||
gl.uniform1i(this.uniforms.palette, 1);
|
||||
gl.uniform1i(this.uniforms.relations, 2);
|
||||
gl.uniform1i(this.uniforms.patterns, 3);
|
||||
gl.uniform1i(this.uniforms.transitions, 4);
|
||||
gl.uniform1i(this.uniforms.contestOwners, 4);
|
||||
gl.uniform1i(this.uniforms.contestIds, 5);
|
||||
gl.uniform1i(this.uniforms.contestTimes, 6);
|
||||
|
||||
if (this.uniforms.resolution) {
|
||||
gl.uniform2f(
|
||||
@@ -386,11 +443,11 @@ export class TerritoryWebGLRenderer {
|
||||
if (this.uniforms.hoverPulseSpeed) {
|
||||
gl.uniform1f(this.uniforms.hoverPulseSpeed, this.hoverPulseSpeed);
|
||||
}
|
||||
if (this.uniforms.transitionNow) {
|
||||
gl.uniform1i(this.uniforms.transitionNow, this.transitionNow);
|
||||
if (this.uniforms.contestNow) {
|
||||
gl.uniform1i(this.uniforms.contestNow, this.contestNow);
|
||||
}
|
||||
if (this.uniforms.transitionDuration) {
|
||||
gl.uniform1f(this.uniforms.transitionDuration, this.transitionDurationMs);
|
||||
if (this.uniforms.contestDuration) {
|
||||
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationMs);
|
||||
}
|
||||
|
||||
gl.enable(gl.BLEND);
|
||||
@@ -465,46 +522,83 @@ export class TerritoryWebGLRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
setTransitionTile(tile: TileRef, previousOwner: number, startPacked: number) {
|
||||
setContestTile(
|
||||
tile: TileRef,
|
||||
defenderOwner: number,
|
||||
attackerOwner: number,
|
||||
componentId: number,
|
||||
attackerEver: boolean,
|
||||
) {
|
||||
const offset = tile * 2;
|
||||
const ownerValue = previousOwner & 0xffff;
|
||||
const startValue = startPacked & 0xffff;
|
||||
const defenderValue = defenderOwner & 0xffff;
|
||||
const attackerValue = attackerOwner & 0xffff;
|
||||
const idValue = (componentId & 0x7fff) | (attackerEver ? 0x8000 : 0);
|
||||
if (
|
||||
this.transitionState[offset] === ownerValue &&
|
||||
this.transitionState[offset + 1] === startValue
|
||||
this.contestOwnersState[offset] === defenderValue &&
|
||||
this.contestOwnersState[offset + 1] === attackerValue &&
|
||||
this.contestIdsState[tile] === idValue
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.transitionState[offset] = ownerValue;
|
||||
this.transitionState[offset + 1] = startValue;
|
||||
if (this.needsTransitionFullUpload) {
|
||||
this.contestOwnersState[offset] = defenderValue;
|
||||
this.contestOwnersState[offset + 1] = attackerValue;
|
||||
this.contestIdsState[tile] = idValue;
|
||||
if (this.needsContestFullUpload) {
|
||||
return;
|
||||
}
|
||||
const x = tile % this.canvas.width;
|
||||
const y = Math.floor(tile / this.canvas.width);
|
||||
const span = this.transitionDirtyRows.get(y);
|
||||
const span = this.contestDirtyRows.get(y);
|
||||
if (span === undefined) {
|
||||
this.transitionDirtyRows.set(y, { minX: x, maxX: x });
|
||||
this.contestDirtyRows.set(y, { minX: x, maxX: x });
|
||||
} else {
|
||||
span.minX = Math.min(span.minX, x);
|
||||
span.maxX = Math.max(span.maxX, x);
|
||||
}
|
||||
}
|
||||
|
||||
clearTransitionTile(tile: TileRef, currentOwner: number) {
|
||||
this.setTransitionTile(tile, currentOwner, 0);
|
||||
clearContestTile(tile: TileRef) {
|
||||
this.setContestTile(tile, 0, 0, 0, false);
|
||||
}
|
||||
|
||||
setTransitionTime(nowPacked: number, durationMs: number) {
|
||||
this.transitionNow = nowPacked | 0;
|
||||
this.transitionDurationMs = Math.max(1, durationMs);
|
||||
setContestTime(componentId: number, nowPacked: number) {
|
||||
if (componentId <= 0) {
|
||||
return;
|
||||
}
|
||||
this.ensureContestTimeCapacity(componentId);
|
||||
const packed = nowPacked & 0xffff;
|
||||
if (this.contestTimesState[componentId] === packed) {
|
||||
return;
|
||||
}
|
||||
this.contestTimesState[componentId] = packed;
|
||||
this.needsContestTimesUpload = true;
|
||||
}
|
||||
|
||||
ensureContestTimeCapacity(componentId: number) {
|
||||
if (componentId < this.contestTimesState.length) {
|
||||
return;
|
||||
}
|
||||
let nextLength = Math.max(1, this.contestTimesState.length);
|
||||
while (nextLength <= componentId) {
|
||||
nextLength *= 2;
|
||||
}
|
||||
const nextState = new Uint16Array(nextLength);
|
||||
nextState.set(this.contestTimesState);
|
||||
this.contestTimesState = nextState;
|
||||
this.needsContestTimesUpload = true;
|
||||
}
|
||||
|
||||
setContestNow(nowPacked: number, durationMs: number) {
|
||||
this.contestNow = nowPacked | 0;
|
||||
this.contestDurationMs = Math.max(1, durationMs);
|
||||
}
|
||||
|
||||
markAllDirty() {
|
||||
this.needsFullUpload = true;
|
||||
this.dirtyRows.clear();
|
||||
this.needsTransitionFullUpload = true;
|
||||
this.transitionDirtyRows.clear();
|
||||
this.needsContestFullUpload = true;
|
||||
this.needsContestTimesUpload = true;
|
||||
this.contestDirtyRows.clear();
|
||||
}
|
||||
|
||||
refreshPalette() {
|
||||
@@ -524,11 +618,18 @@ export class TerritoryWebGLRenderer {
|
||||
this.uploadStateTexture();
|
||||
FrameProfiler.end("TerritoryWebGLRenderer:uploadState", uploadStateSpan);
|
||||
|
||||
const uploadTransitionSpan = FrameProfiler.start();
|
||||
this.uploadTransitionTexture();
|
||||
const uploadContestSpan = FrameProfiler.start();
|
||||
this.uploadContestTexture();
|
||||
FrameProfiler.end(
|
||||
"TerritoryWebGLRenderer:uploadTransitions",
|
||||
uploadTransitionSpan,
|
||||
"TerritoryWebGLRenderer:uploadContests",
|
||||
uploadContestSpan,
|
||||
);
|
||||
|
||||
const uploadContestTimesSpan = FrameProfiler.start();
|
||||
this.uploadContestTimesTexture();
|
||||
FrameProfiler.end(
|
||||
"TerritoryWebGLRenderer:uploadContestTimes",
|
||||
uploadContestTimesSpan,
|
||||
);
|
||||
|
||||
const renderSpan = FrameProfiler.start();
|
||||
@@ -565,11 +666,11 @@ export class TerritoryWebGLRenderer {
|
||||
const viewerId = this.game.myPlayer()?.smallID() ?? 0;
|
||||
gl.uniform1i(this.uniforms.viewerId, viewerId);
|
||||
}
|
||||
if (this.uniforms.transitionNow) {
|
||||
gl.uniform1i(this.uniforms.transitionNow, this.transitionNow);
|
||||
if (this.uniforms.contestNow) {
|
||||
gl.uniform1i(this.uniforms.contestNow, this.contestNow);
|
||||
}
|
||||
if (this.uniforms.transitionDuration) {
|
||||
gl.uniform1f(this.uniforms.transitionDuration, this.transitionDurationMs);
|
||||
if (this.uniforms.contestDuration) {
|
||||
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationMs);
|
||||
}
|
||||
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
@@ -634,17 +735,21 @@ export class TerritoryWebGLRenderer {
|
||||
return { rows: rowsUploaded, bytes: bytesUploaded };
|
||||
}
|
||||
|
||||
private uploadTransitionTexture(): { rows: number; bytes: number } {
|
||||
if (!this.gl || !this.transitionTexture) return { rows: 0, bytes: 0 };
|
||||
private uploadContestTexture(): { rows: number; bytes: number } {
|
||||
if (!this.gl || !this.contestOwnersTexture || !this.contestIdsTexture) {
|
||||
return { rows: 0, bytes: 0 };
|
||||
}
|
||||
const gl = this.gl;
|
||||
gl.activeTexture(gl.TEXTURE4);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.transitionTexture);
|
||||
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
||||
|
||||
const bytesPerPixel = Uint16Array.BYTES_PER_ELEMENT * 2;
|
||||
const bytesPerOwnerPixel = Uint16Array.BYTES_PER_ELEMENT * 2;
|
||||
const bytesPerIdPixel = Uint16Array.BYTES_PER_ELEMENT;
|
||||
let rowsUploaded = 0;
|
||||
let bytesUploaded = 0;
|
||||
|
||||
if (this.needsTransitionFullUpload) {
|
||||
if (this.needsContestFullUpload) {
|
||||
gl.activeTexture(gl.TEXTURE4);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.contestOwnersTexture);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
@@ -654,26 +759,47 @@ export class TerritoryWebGLRenderer {
|
||||
0,
|
||||
gl.RG_INTEGER,
|
||||
gl.UNSIGNED_SHORT,
|
||||
this.transitionState,
|
||||
this.contestOwnersState,
|
||||
);
|
||||
this.needsTransitionFullUpload = false;
|
||||
this.transitionDirtyRows.clear();
|
||||
|
||||
gl.activeTexture(gl.TEXTURE5);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.contestIdsTexture);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.R16UI,
|
||||
this.canvas.width,
|
||||
this.canvas.height,
|
||||
0,
|
||||
gl.RED_INTEGER,
|
||||
gl.UNSIGNED_SHORT,
|
||||
this.contestIdsState,
|
||||
);
|
||||
|
||||
this.needsContestFullUpload = false;
|
||||
this.contestDirtyRows.clear();
|
||||
rowsUploaded = this.canvas.height;
|
||||
bytesUploaded = this.canvas.width * this.canvas.height * bytesPerPixel;
|
||||
bytesUploaded =
|
||||
this.canvas.width *
|
||||
this.canvas.height *
|
||||
(bytesPerOwnerPixel + bytesPerIdPixel);
|
||||
return { rows: rowsUploaded, bytes: bytesUploaded };
|
||||
}
|
||||
|
||||
if (this.transitionDirtyRows.size === 0) {
|
||||
if (this.contestDirtyRows.size === 0) {
|
||||
return { rows: 0, bytes: 0 };
|
||||
}
|
||||
|
||||
for (const [y, span] of this.transitionDirtyRows) {
|
||||
for (const [y, span] of this.contestDirtyRows) {
|
||||
const width = span.maxX - span.minX + 1;
|
||||
const offset = (y * this.canvas.width + span.minX) * 2;
|
||||
const rowSlice = this.transitionState.subarray(
|
||||
offset,
|
||||
offset + width * 2,
|
||||
const ownerOffset = (y * this.canvas.width + span.minX) * 2;
|
||||
const ownerSlice = this.contestOwnersState.subarray(
|
||||
ownerOffset,
|
||||
ownerOffset + width * 2,
|
||||
);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE4);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.contestOwnersTexture);
|
||||
gl.texSubImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
@@ -683,15 +809,59 @@ export class TerritoryWebGLRenderer {
|
||||
1,
|
||||
gl.RG_INTEGER,
|
||||
gl.UNSIGNED_SHORT,
|
||||
rowSlice,
|
||||
ownerSlice,
|
||||
);
|
||||
|
||||
const idOffset = y * this.canvas.width + span.minX;
|
||||
const idSlice = this.contestIdsState.subarray(idOffset, idOffset + width);
|
||||
gl.activeTexture(gl.TEXTURE5);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.contestIdsTexture);
|
||||
gl.texSubImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
span.minX,
|
||||
y,
|
||||
width,
|
||||
1,
|
||||
gl.RED_INTEGER,
|
||||
gl.UNSIGNED_SHORT,
|
||||
idSlice,
|
||||
);
|
||||
|
||||
rowsUploaded++;
|
||||
bytesUploaded += width * bytesPerPixel;
|
||||
bytesUploaded += width * (bytesPerOwnerPixel + bytesPerIdPixel);
|
||||
}
|
||||
this.transitionDirtyRows.clear();
|
||||
this.contestDirtyRows.clear();
|
||||
return { rows: rowsUploaded, bytes: bytesUploaded };
|
||||
}
|
||||
|
||||
private uploadContestTimesTexture(): { rows: number; bytes: number } {
|
||||
if (!this.gl || !this.contestTimesTexture) {
|
||||
return { rows: 0, bytes: 0 };
|
||||
}
|
||||
if (!this.needsContestTimesUpload) {
|
||||
return { rows: 0, bytes: 0 };
|
||||
}
|
||||
const gl = this.gl;
|
||||
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
||||
gl.activeTexture(gl.TEXTURE6);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.contestTimesTexture);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.R16UI,
|
||||
this.contestTimesState.length,
|
||||
1,
|
||||
0,
|
||||
gl.RED_INTEGER,
|
||||
gl.UNSIGNED_SHORT,
|
||||
this.contestTimesState,
|
||||
);
|
||||
this.needsContestTimesUpload = false;
|
||||
const bytes = this.contestTimesState.length * Uint16Array.BYTES_PER_ELEMENT;
|
||||
return { rows: 1, bytes };
|
||||
}
|
||||
|
||||
private uploadPalette() {
|
||||
if (
|
||||
!this.gl ||
|
||||
@@ -879,9 +1049,11 @@ export class TerritoryWebGLRenderer {
|
||||
uniform sampler2D u_palette;
|
||||
uniform usampler2D u_relations;
|
||||
uniform usampler2D u_patterns;
|
||||
uniform usampler2D u_transitions;
|
||||
uniform int u_transitionNow;
|
||||
uniform float u_transitionDurationMs;
|
||||
uniform usampler2D u_contestOwners;
|
||||
uniform usampler2D u_contestIds;
|
||||
uniform usampler2D u_contestTimes;
|
||||
uniform int u_contestNow;
|
||||
uniform float u_contestDurationMs;
|
||||
uniform int u_patternStride;
|
||||
uniform int u_patternRows;
|
||||
uniform int u_viewerId;
|
||||
@@ -911,22 +1083,22 @@ export class TerritoryWebGLRenderer {
|
||||
return texelFetch(u_state, clamped, 0).r & 0xFFFu;
|
||||
}
|
||||
|
||||
uint prevOwnerAtTex(ivec2 texCoord) {
|
||||
uvec2 contestOwnersAtTex(ivec2 texCoord) {
|
||||
ivec2 clamped = clamp(
|
||||
texCoord,
|
||||
ivec2(0, 0),
|
||||
ivec2(int(u_resolution.x) - 1, int(u_resolution.y) - 1)
|
||||
);
|
||||
return texelFetch(u_transitions, clamped, 0).r & 0xFFFu;
|
||||
return texelFetch(u_contestOwners, clamped, 0).rg;
|
||||
}
|
||||
|
||||
uint transitionPackedAtTex(ivec2 texCoord) {
|
||||
uint contestIdRawAtTex(ivec2 texCoord) {
|
||||
ivec2 clamped = clamp(
|
||||
texCoord,
|
||||
ivec2(0, 0),
|
||||
ivec2(int(u_resolution.x) - 1, int(u_resolution.y) - 1)
|
||||
);
|
||||
return texelFetch(u_transitions, clamped, 0).g;
|
||||
return texelFetch(u_contestIds, clamped, 0).r;
|
||||
}
|
||||
|
||||
uint relationCode(uint owner, uint other) {
|
||||
@@ -979,34 +1151,51 @@ export class TerritoryWebGLRenderer {
|
||||
return (byteVal & (1u << bitIndex)) == 0u;
|
||||
}
|
||||
|
||||
vec3 applyDefended(vec3 color, bool defended, ivec2 texCoord) {
|
||||
if (!defended) {
|
||||
return color;
|
||||
}
|
||||
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
|
||||
const float LIGHT_FACTOR = 1.2;
|
||||
const float DARK_FACTOR = 0.8;
|
||||
return color * (isLightTile ? LIGHT_FACTOR : DARK_FACTOR);
|
||||
}
|
||||
|
||||
void main() {
|
||||
ivec2 fragCoord = ivec2(gl_FragCoord.xy);
|
||||
ivec2 texCoord = ivec2(fragCoord.x, int(u_resolution.y) - 1 - fragCoord.y);
|
||||
|
||||
uint state = texelFetch(u_state, texCoord, 0).r;
|
||||
uint owner = state & 0xFFFu;
|
||||
uint prevOwner = prevOwnerAtTex(texCoord);
|
||||
uint startPacked = transitionPackedAtTex(texCoord);
|
||||
const uint TRANSITION_FLAG = 0x8000u;
|
||||
const uint TRANSITION_TIME_MASK = 0x7FFFu;
|
||||
const uint TRANSITION_WRAP = 32768u;
|
||||
bool hasTransition = (startPacked & TRANSITION_FLAG) != 0u;
|
||||
float t = 1.0;
|
||||
if (hasTransition) {
|
||||
uint start = startPacked & TRANSITION_TIME_MASK;
|
||||
uint nowTime = uint(u_transitionNow);
|
||||
uint elapsed = nowTime >= start
|
||||
? (nowTime - start)
|
||||
: (TRANSITION_WRAP - start + nowTime);
|
||||
t = clamp(float(elapsed) / u_transitionDurationMs, 0.0, 1.0);
|
||||
}
|
||||
bool doTransition = hasTransition && t < 1.0 && prevOwner != owner;
|
||||
bool hasFallout = (state & 0x2000u) != 0u;
|
||||
bool isDefended = (state & 0x1000u) != 0u;
|
||||
|
||||
uint contestIdRaw = contestIdRawAtTex(texCoord);
|
||||
const uint CONTEST_ID_MASK = 0x7FFFu;
|
||||
const uint CONTEST_ATTACKER_EVER = 0x8000u;
|
||||
uint contestId = contestIdRaw & CONTEST_ID_MASK;
|
||||
bool attackerEver = (contestIdRaw & CONTEST_ATTACKER_EVER) != 0u;
|
||||
uvec2 contestOwners = contestOwnersAtTex(texCoord);
|
||||
uint defender = contestOwners.r & 0xFFFu;
|
||||
uint attacker = contestOwners.g & 0xFFFu;
|
||||
|
||||
bool contested = false;
|
||||
if (contestId != 0u) {
|
||||
uint lastTime = texelFetch(u_contestTimes, ivec2(int(contestId), 0), 0).r;
|
||||
const uint CONTEST_WRAP = 32768u;
|
||||
uint nowTime = uint(u_contestNow);
|
||||
uint elapsed = nowTime >= lastTime
|
||||
? (nowTime - lastTime)
|
||||
: (CONTEST_WRAP - lastTime + nowTime);
|
||||
contested = float(elapsed) < u_contestDurationMs;
|
||||
}
|
||||
|
||||
bool isBorder = false;
|
||||
bool hasFriendlyRelation = false;
|
||||
bool hasEmbargoRelation = false;
|
||||
bool pushedBorder = false;
|
||||
bool regainedBorder = false;
|
||||
|
||||
uint nOwner = ownerAtTex(texCoord + ivec2(1, 0));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
@@ -1014,6 +1203,19 @@ export class TerritoryWebGLRenderer {
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
if (contested) {
|
||||
uint nContestRaw = contestIdRawAtTex(texCoord + ivec2(1, 0));
|
||||
uint nContestId = nContestRaw & CONTEST_ID_MASK;
|
||||
bool sameComponent = nContestId == contestId;
|
||||
bool nAttackerEver = sameComponent && ((nContestRaw & CONTEST_ATTACKER_EVER) != 0u);
|
||||
if (attackerEver && !nAttackerEver) {
|
||||
pushedBorder = true;
|
||||
}
|
||||
if (sameComponent && owner == defender && nOwner == attacker) {
|
||||
regainedBorder = true;
|
||||
}
|
||||
}
|
||||
|
||||
nOwner = ownerAtTex(texCoord + ivec2(-1, 0));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
@@ -1021,6 +1223,19 @@ export class TerritoryWebGLRenderer {
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
if (contested) {
|
||||
uint nContestRaw = contestIdRawAtTex(texCoord + ivec2(-1, 0));
|
||||
uint nContestId = nContestRaw & CONTEST_ID_MASK;
|
||||
bool sameComponent = nContestId == contestId;
|
||||
bool nAttackerEver = sameComponent && ((nContestRaw & CONTEST_ATTACKER_EVER) != 0u);
|
||||
if (attackerEver && !nAttackerEver) {
|
||||
pushedBorder = true;
|
||||
}
|
||||
if (sameComponent && owner == defender && nOwner == attacker) {
|
||||
regainedBorder = true;
|
||||
}
|
||||
}
|
||||
|
||||
nOwner = ownerAtTex(texCoord + ivec2(0, 1));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
@@ -1028,6 +1243,19 @@ export class TerritoryWebGLRenderer {
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
if (contested) {
|
||||
uint nContestRaw = contestIdRawAtTex(texCoord + ivec2(0, 1));
|
||||
uint nContestId = nContestRaw & CONTEST_ID_MASK;
|
||||
bool sameComponent = nContestId == contestId;
|
||||
bool nAttackerEver = sameComponent && ((nContestRaw & CONTEST_ATTACKER_EVER) != 0u);
|
||||
if (attackerEver && !nAttackerEver) {
|
||||
pushedBorder = true;
|
||||
}
|
||||
if (sameComponent && owner == defender && nOwner == attacker) {
|
||||
regainedBorder = true;
|
||||
}
|
||||
}
|
||||
|
||||
nOwner = ownerAtTex(texCoord + ivec2(0, -1));
|
||||
isBorder = isBorder || (nOwner != owner);
|
||||
if (nOwner != owner && nOwner != 0u) {
|
||||
@@ -1035,44 +1263,22 @@ export class TerritoryWebGLRenderer {
|
||||
hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel);
|
||||
hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
|
||||
bool oldIsBorder = false;
|
||||
bool oldFriendlyRelation = false;
|
||||
bool oldEmbargoRelation = false;
|
||||
if (doTransition && prevOwner != 0u) {
|
||||
uint prevNeighbor = prevOwnerAtTex(texCoord + ivec2(1, 0));
|
||||
oldIsBorder = oldIsBorder || (prevNeighbor != prevOwner);
|
||||
if (prevNeighbor != prevOwner && prevNeighbor != 0u) {
|
||||
uint rel = relationCode(prevOwner, prevNeighbor);
|
||||
oldEmbargoRelation = oldEmbargoRelation || isEmbargo(rel);
|
||||
oldFriendlyRelation = oldFriendlyRelation || isFriendly(rel);
|
||||
if (contested) {
|
||||
uint nContestRaw = contestIdRawAtTex(texCoord + ivec2(0, -1));
|
||||
uint nContestId = nContestRaw & CONTEST_ID_MASK;
|
||||
bool sameComponent = nContestId == contestId;
|
||||
bool nAttackerEver = sameComponent && ((nContestRaw & CONTEST_ATTACKER_EVER) != 0u);
|
||||
if (attackerEver && !nAttackerEver) {
|
||||
pushedBorder = true;
|
||||
}
|
||||
prevNeighbor = prevOwnerAtTex(texCoord + ivec2(-1, 0));
|
||||
oldIsBorder = oldIsBorder || (prevNeighbor != prevOwner);
|
||||
if (prevNeighbor != prevOwner && prevNeighbor != 0u) {
|
||||
uint rel = relationCode(prevOwner, prevNeighbor);
|
||||
oldEmbargoRelation = oldEmbargoRelation || isEmbargo(rel);
|
||||
oldFriendlyRelation = oldFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
prevNeighbor = prevOwnerAtTex(texCoord + ivec2(0, 1));
|
||||
oldIsBorder = oldIsBorder || (prevNeighbor != prevOwner);
|
||||
if (prevNeighbor != prevOwner && prevNeighbor != 0u) {
|
||||
uint rel = relationCode(prevOwner, prevNeighbor);
|
||||
oldEmbargoRelation = oldEmbargoRelation || isEmbargo(rel);
|
||||
oldFriendlyRelation = oldFriendlyRelation || isFriendly(rel);
|
||||
}
|
||||
prevNeighbor = prevOwnerAtTex(texCoord + ivec2(0, -1));
|
||||
oldIsBorder = oldIsBorder || (prevNeighbor != prevOwner);
|
||||
if (prevNeighbor != prevOwner && prevNeighbor != 0u) {
|
||||
uint rel = relationCode(prevOwner, prevNeighbor);
|
||||
oldEmbargoRelation = oldEmbargoRelation || isEmbargo(rel);
|
||||
oldFriendlyRelation = oldFriendlyRelation || isFriendly(rel);
|
||||
if (sameComponent && owner == defender && nOwner == attacker) {
|
||||
regainedBorder = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (u_alternativeView) {
|
||||
vec3 newColor = vec3(0.0);
|
||||
float newAlpha = 0.0;
|
||||
vec3 color = vec3(0.0);
|
||||
float a = 0.0;
|
||||
if (owner != 0u) {
|
||||
uint relationAlt = relationCode(owner, uint(u_viewerId));
|
||||
vec4 altColor = u_altNeutral;
|
||||
@@ -1083,32 +1289,9 @@ export class TerritoryWebGLRenderer {
|
||||
} else if (isEmbargo(relationAlt)) {
|
||||
altColor = u_altEnemy;
|
||||
}
|
||||
newColor = altColor.rgb;
|
||||
newAlpha = isBorder ? 1.0 : 0.0;
|
||||
color = altColor.rgb;
|
||||
a = isBorder ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
vec3 color = newColor;
|
||||
float a = newAlpha;
|
||||
if (doTransition) {
|
||||
vec3 oldColor = vec3(0.0);
|
||||
float oldAlpha = 0.0;
|
||||
if (prevOwner != 0u) {
|
||||
uint relationAltOld = relationCode(prevOwner, uint(u_viewerId));
|
||||
vec4 altColorOld = u_altNeutral;
|
||||
if (isSelf(relationAltOld)) {
|
||||
altColorOld = u_altSelf;
|
||||
} else if (isFriendly(relationAltOld)) {
|
||||
altColorOld = u_altAlly;
|
||||
} else if (isEmbargo(relationAltOld)) {
|
||||
altColorOld = u_altEnemy;
|
||||
}
|
||||
oldColor = altColorOld.rgb;
|
||||
oldAlpha = oldIsBorder ? 1.0 : 0.0;
|
||||
}
|
||||
color = mix(oldColor, newColor, t);
|
||||
a = mix(oldAlpha, newAlpha, t);
|
||||
}
|
||||
|
||||
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
|
||||
float pulse = u_hoverPulseStrength > 0.0
|
||||
? (1.0 - u_hoverPulseStrength) +
|
||||
@@ -1120,12 +1303,17 @@ export class TerritoryWebGLRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
vec3 newColor = vec3(0.0);
|
||||
float newAlpha = 0.0;
|
||||
vec3 fillColor = vec3(0.0);
|
||||
float fillAlpha = 0.0;
|
||||
vec3 borderColor = vec3(0.0);
|
||||
float borderAlpha = 0.0;
|
||||
vec3 ownerBase = vec3(0.0);
|
||||
vec4 ownerBorder = vec4(0.0);
|
||||
|
||||
if (owner == 0u) {
|
||||
if (hasFallout) {
|
||||
newColor = u_fallout.rgb;
|
||||
newAlpha = u_alpha;
|
||||
fillColor = u_fallout.rgb;
|
||||
fillAlpha = u_alpha;
|
||||
}
|
||||
} else {
|
||||
vec4 base = texelFetch(u_palette, ivec2(int(owner) * 2, 0), 0);
|
||||
@@ -1134,92 +1322,85 @@ export class TerritoryWebGLRenderer {
|
||||
ivec2(int(owner) * 2 + 1, 0),
|
||||
0
|
||||
);
|
||||
ownerBase = base.rgb;
|
||||
ownerBorder = baseBorder;
|
||||
if (isBorder) {
|
||||
vec3 borderColor = baseBorder.rgb;
|
||||
vec3 bColor = baseBorder.rgb;
|
||||
|
||||
const float BORDER_TINT_RATIO = 0.35;
|
||||
const vec3 FRIENDLY_TINT_TARGET = vec3(0.0, 1.0, 0.0);
|
||||
const vec3 EMBARGO_TINT_TARGET = vec3(1.0, 0.0, 0.0);
|
||||
|
||||
if (hasFriendlyRelation) {
|
||||
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
bColor = bColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
}
|
||||
if (hasEmbargoRelation) {
|
||||
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
EMBARGO_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
bColor = bColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
EMBARGO_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
}
|
||||
|
||||
if (isDefended) {
|
||||
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
|
||||
const float LIGHT_FACTOR = 1.2;
|
||||
const float DARK_FACTOR = 0.8;
|
||||
borderColor *= isLightTile ? LIGHT_FACTOR : DARK_FACTOR;
|
||||
}
|
||||
|
||||
newColor = borderColor;
|
||||
newAlpha = baseBorder.a;
|
||||
borderColor = applyDefended(bColor, isDefended, texCoord);
|
||||
borderAlpha = baseBorder.a;
|
||||
} else {
|
||||
bool isPrimary = patternIsPrimary(owner, texCoord);
|
||||
newColor = isPrimary ? base.rgb : baseBorder.rgb;
|
||||
newAlpha = u_alpha;
|
||||
fillColor = isPrimary ? base.rgb : baseBorder.rgb;
|
||||
fillAlpha = u_alpha;
|
||||
}
|
||||
}
|
||||
|
||||
vec3 color = newColor;
|
||||
float a = newAlpha;
|
||||
if (doTransition) {
|
||||
vec3 oldColor = vec3(0.0);
|
||||
float oldAlpha = 0.0;
|
||||
if (prevOwner == 0u) {
|
||||
if (hasFallout) {
|
||||
oldColor = u_fallout.rgb;
|
||||
oldAlpha = u_alpha;
|
||||
}
|
||||
} else {
|
||||
vec4 oldBase = texelFetch(
|
||||
vec3 contestedFillColor = fillColor;
|
||||
float contestedFillAlpha = fillAlpha;
|
||||
if (contested && owner != 0u) {
|
||||
vec3 defenderBase = ownerBase;
|
||||
if (defender != 0u) {
|
||||
vec4 defenderColor = texelFetch(
|
||||
u_palette,
|
||||
ivec2(int(prevOwner) * 2, 0),
|
||||
ivec2(int(defender) * 2, 0),
|
||||
0
|
||||
);
|
||||
vec4 oldBorder = texelFetch(
|
||||
u_palette,
|
||||
ivec2(int(prevOwner) * 2 + 1, 0),
|
||||
0
|
||||
);
|
||||
if (oldIsBorder) {
|
||||
vec3 borderColor = oldBorder.rgb;
|
||||
|
||||
const float BORDER_TINT_RATIO = 0.35;
|
||||
const vec3 FRIENDLY_TINT_TARGET = vec3(0.0, 1.0, 0.0);
|
||||
const vec3 EMBARGO_TINT_TARGET = vec3(1.0, 0.0, 0.0);
|
||||
|
||||
if (oldFriendlyRelation) {
|
||||
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
}
|
||||
if (oldEmbargoRelation) {
|
||||
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
EMBARGO_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
}
|
||||
|
||||
if (isDefended) {
|
||||
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
|
||||
const float LIGHT_FACTOR = 1.2;
|
||||
const float DARK_FACTOR = 0.8;
|
||||
borderColor *= isLightTile ? LIGHT_FACTOR : DARK_FACTOR;
|
||||
}
|
||||
|
||||
oldColor = borderColor;
|
||||
oldAlpha = oldBorder.a;
|
||||
} else {
|
||||
bool isPrimary = patternIsPrimary(prevOwner, texCoord);
|
||||
oldColor = isPrimary ? oldBase.rgb : oldBorder.rgb;
|
||||
oldAlpha = u_alpha;
|
||||
}
|
||||
defenderBase = defenderColor.rgb;
|
||||
}
|
||||
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
|
||||
contestedFillColor = isLightTile ? ownerBase : defenderBase;
|
||||
contestedFillAlpha = u_alpha;
|
||||
}
|
||||
|
||||
vec3 attackerBorderColor = vec3(0.0);
|
||||
float attackerBorderAlpha = 0.0;
|
||||
if (attacker != 0u) {
|
||||
vec4 attackerBorder = texelFetch(
|
||||
u_palette,
|
||||
ivec2(int(attacker) * 2 + 1, 0),
|
||||
0
|
||||
);
|
||||
attackerBorderColor = applyDefended(attackerBorder.rgb, isDefended, texCoord);
|
||||
attackerBorderAlpha = attackerBorder.a;
|
||||
}
|
||||
|
||||
vec3 color = contested ? contestedFillColor : fillColor;
|
||||
float a = contested ? contestedFillAlpha : fillAlpha;
|
||||
|
||||
if (isBorder && owner != 0u) {
|
||||
color = borderColor;
|
||||
a = borderAlpha;
|
||||
}
|
||||
|
||||
if (contested) {
|
||||
if (regainedBorder) {
|
||||
vec3 regained = applyDefended(vec3(1.0, 0.2, 0.2), isDefended, texCoord);
|
||||
color = regained;
|
||||
a = 1.0;
|
||||
} else if (pushedBorder) {
|
||||
color = attackerBorderColor;
|
||||
a = attackerBorderAlpha;
|
||||
} else if (isBorder && owner != 0u) {
|
||||
color = borderColor;
|
||||
a = borderAlpha;
|
||||
} else if (owner != 0u) {
|
||||
color = contestedFillColor;
|
||||
a = contestedFillAlpha;
|
||||
}
|
||||
color = mix(oldColor, newColor, t);
|
||||
a = mix(oldAlpha, newAlpha, t);
|
||||
}
|
||||
|
||||
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
|
||||
|
||||
Reference in New Issue
Block a user