mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 16:56:38 +00:00
fixes
This commit is contained in:
@@ -169,7 +169,11 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
const focusedPlayer = this.game.focusedPlayer();
|
||||
if (focusedPlayer !== this.lastFocusedPlayer) {
|
||||
if (!this.territoryRenderer) {
|
||||
if (this.territoryRenderer) {
|
||||
// Force a full repaint so the GPU textures match the new focus context
|
||||
// (e.g., when jumping to another location during spawn).
|
||||
this.redraw();
|
||||
} else {
|
||||
if (this.lastFocusedPlayer) {
|
||||
this.paintPlayerBorder(this.lastFocusedPlayer);
|
||||
}
|
||||
@@ -294,6 +298,7 @@ export class TerritoryLayer implements Layer {
|
||||
this.eventBus.on(AlternateViewEvent, (e) => {
|
||||
this.alternativeView = e.alternateView;
|
||||
this.territoryRenderer?.setAlternativeView(this.alternativeView);
|
||||
this.territoryRenderer?.markAllDirty();
|
||||
this.territoryRenderer?.setHoverHighlightOptions(
|
||||
this.hoverHighlightOptions(),
|
||||
);
|
||||
@@ -385,11 +390,15 @@ export class TerritoryLayer implements Layer {
|
||||
);
|
||||
this.initImageData();
|
||||
|
||||
this.context.putImageData(
|
||||
this.alternativeView ? this.alternativeImageData : this.imageData,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
if (!this.territoryRenderer) {
|
||||
this.context.putImageData(
|
||||
this.alternativeView ? this.alternativeImageData : this.imageData,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
} else {
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
this.configureRenderers();
|
||||
|
||||
@@ -515,6 +524,7 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
const now = Date.now();
|
||||
// When WebGL is available, rely entirely on the GPU renderer (even in alt view).
|
||||
const gpuTerritoryActive = this.territoryRenderer !== null;
|
||||
const skipTerritoryCanvas = gpuTerritoryActive;
|
||||
|
||||
@@ -540,7 +550,7 @@ export class TerritoryLayer implements Layer {
|
||||
// territory buffer (alternativeImageData) is effectively transparent and
|
||||
// all visible work is done by the WebGL layer. Skip putImageData in that
|
||||
// case to avoid unnecessary CPU work each frame.
|
||||
const shouldBlitTerritories = !skipTerritoryCanvas && !gpuTerritoryActive;
|
||||
const shouldBlitTerritories = !gpuTerritoryActive && !skipTerritoryCanvas;
|
||||
|
||||
if (w > 0 && h > 0 && shouldBlitTerritories) {
|
||||
const putImageStart = FrameProfiler.start();
|
||||
@@ -630,7 +640,13 @@ export class TerritoryLayer implements Layer {
|
||||
const cpuStart = FrameProfiler.start();
|
||||
const useGpuTerritory = this.territoryRenderer !== null;
|
||||
const hasOwner = this.game.hasOwner(tile);
|
||||
const owner = hasOwner ? (this.game.owner(tile) as PlayerView) : null;
|
||||
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);
|
||||
let isDefended = false;
|
||||
@@ -645,6 +661,17 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
if (useGpuTerritory) {
|
||||
this.territoryRenderer?.markTile(tile);
|
||||
if (!owner || !isBorderTile) {
|
||||
this.territoryRenderer?.clearBorderColor(tile);
|
||||
} else {
|
||||
const borderCol = owner.borderColor(tile, isDefended).rgba;
|
||||
this.territoryRenderer?.setBorderColor(tile, {
|
||||
r: borderCol.r,
|
||||
g: borderCol.g,
|
||||
b: borderCol.b,
|
||||
a: Math.round((borderCol.a ?? 1) * 255),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!owner) {
|
||||
if (hasFallout) {
|
||||
|
||||
@@ -33,11 +33,13 @@ export class TerritoryWebGLRenderer {
|
||||
private readonly stateTexture: WebGLTexture | null;
|
||||
private readonly paletteTexture: WebGLTexture | null;
|
||||
private readonly relationTexture: WebGLTexture | null;
|
||||
private readonly borderColorTexture: WebGLTexture | null;
|
||||
private readonly uniforms: {
|
||||
resolution: WebGLUniformLocation | null;
|
||||
state: WebGLUniformLocation | null;
|
||||
palette: WebGLUniformLocation | null;
|
||||
relations: WebGLUniformLocation | null;
|
||||
borderColor: WebGLUniformLocation | null;
|
||||
fallout: WebGLUniformLocation | null;
|
||||
altSelf: WebGLUniformLocation | null;
|
||||
altAlly: WebGLUniformLocation | null;
|
||||
@@ -55,7 +57,9 @@ export class TerritoryWebGLRenderer {
|
||||
|
||||
private readonly state: Uint16Array;
|
||||
private readonly dirtyRows: Map<number, DirtySpan> = new Map();
|
||||
private readonly borderDirtyRows: Map<number, DirtySpan> = new Map();
|
||||
private needsFullUpload = true;
|
||||
private borderNeedsFullUpload = true;
|
||||
private alternativeView = false;
|
||||
private paletteWidth = 0;
|
||||
private hoverHighlightStrength = 0.7;
|
||||
@@ -64,6 +68,7 @@ export class TerritoryWebGLRenderer {
|
||||
private hoverPulseSpeed = Math.PI * 2;
|
||||
private hoveredPlayerId = -1;
|
||||
private animationStartTime = Date.now();
|
||||
private borderColorData: Uint8Array;
|
||||
|
||||
private constructor(
|
||||
private readonly game: GameView,
|
||||
@@ -75,6 +80,9 @@ export class TerritoryWebGLRenderer {
|
||||
this.canvas.height = game.height();
|
||||
|
||||
this.state = new Uint16Array(sharedState);
|
||||
this.borderColorData = new Uint8Array(
|
||||
this.canvas.width * this.canvas.height * 4,
|
||||
);
|
||||
|
||||
this.gl = this.canvas.getContext("webgl2", {
|
||||
premultipliedAlpha: true,
|
||||
@@ -89,11 +97,13 @@ export class TerritoryWebGLRenderer {
|
||||
this.stateTexture = null;
|
||||
this.paletteTexture = null;
|
||||
this.relationTexture = null;
|
||||
this.borderColorTexture = null;
|
||||
this.uniforms = {
|
||||
resolution: null,
|
||||
state: null,
|
||||
palette: null,
|
||||
relations: null,
|
||||
borderColor: null,
|
||||
fallout: null,
|
||||
altSelf: null,
|
||||
altAlly: null,
|
||||
@@ -119,11 +129,13 @@ export class TerritoryWebGLRenderer {
|
||||
this.stateTexture = null;
|
||||
this.paletteTexture = null;
|
||||
this.relationTexture = null;
|
||||
this.borderColorTexture = null;
|
||||
this.uniforms = {
|
||||
resolution: null,
|
||||
state: null,
|
||||
palette: null,
|
||||
relations: null,
|
||||
borderColor: null,
|
||||
fallout: null,
|
||||
altSelf: null,
|
||||
altAlly: null,
|
||||
@@ -146,6 +158,7 @@ export class TerritoryWebGLRenderer {
|
||||
state: gl.getUniformLocation(this.program, "u_state"),
|
||||
palette: gl.getUniformLocation(this.program, "u_palette"),
|
||||
relations: gl.getUniformLocation(this.program, "u_relations"),
|
||||
borderColor: gl.getUniformLocation(this.program, "u_borderColor"),
|
||||
fallout: gl.getUniformLocation(this.program, "u_fallout"),
|
||||
altSelf: gl.getUniformLocation(this.program, "u_altSelf"),
|
||||
altAlly: gl.getUniformLocation(this.program, "u_altAlly"),
|
||||
@@ -220,12 +233,33 @@ export class TerritoryWebGLRenderer {
|
||||
this.state,
|
||||
);
|
||||
|
||||
this.borderColorTexture = gl.createTexture();
|
||||
gl.activeTexture(gl.TEXTURE3);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.borderColorTexture);
|
||||
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.RGBA8,
|
||||
this.canvas.width,
|
||||
this.canvas.height,
|
||||
0,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
this.borderColorData,
|
||||
);
|
||||
|
||||
this.uploadPalette();
|
||||
|
||||
gl.useProgram(this.program);
|
||||
gl.uniform1i(this.uniforms.state, 0);
|
||||
gl.uniform1i(this.uniforms.palette, 1);
|
||||
gl.uniform1i(this.uniforms.relations, 2);
|
||||
gl.uniform1i(this.uniforms.borderColor, 3);
|
||||
|
||||
if (this.uniforms.resolution) {
|
||||
gl.uniform2f(
|
||||
@@ -352,6 +386,27 @@ export class TerritoryWebGLRenderer {
|
||||
this.alternativeView = enabled;
|
||||
}
|
||||
|
||||
setBorderColor(
|
||||
tile: TileRef,
|
||||
rgba: { r: number; g: number; b: number; a: number },
|
||||
) {
|
||||
const offset = tile * 4;
|
||||
this.borderColorData[offset] = rgba.r;
|
||||
this.borderColorData[offset + 1] = rgba.g;
|
||||
this.borderColorData[offset + 2] = rgba.b;
|
||||
this.borderColorData[offset + 3] = rgba.a;
|
||||
this.markBorderDirty(tile);
|
||||
}
|
||||
|
||||
clearBorderColor(tile: TileRef) {
|
||||
const offset = tile * 4;
|
||||
this.borderColorData[offset] = 0;
|
||||
this.borderColorData[offset + 1] = 0;
|
||||
this.borderColorData[offset + 2] = 0;
|
||||
this.borderColorData[offset + 3] = 0;
|
||||
this.markBorderDirty(tile);
|
||||
}
|
||||
|
||||
setHoveredPlayerId(playerSmallId: number | null) {
|
||||
const encoded = playerSmallId ?? -1;
|
||||
this.hoveredPlayerId = encoded;
|
||||
@@ -391,9 +446,26 @@ export class TerritoryWebGLRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
private markBorderDirty(tile: TileRef) {
|
||||
if (this.borderNeedsFullUpload) {
|
||||
return;
|
||||
}
|
||||
const x = tile % this.canvas.width;
|
||||
const y = Math.floor(tile / this.canvas.width);
|
||||
const span = this.borderDirtyRows.get(y);
|
||||
if (span === undefined) {
|
||||
this.borderDirtyRows.set(y, { minX: x, maxX: x });
|
||||
} else {
|
||||
span.minX = Math.min(span.minX, x);
|
||||
span.maxX = Math.max(span.maxX, x);
|
||||
}
|
||||
}
|
||||
|
||||
markAllDirty() {
|
||||
this.needsFullUpload = true;
|
||||
this.dirtyRows.clear();
|
||||
this.borderNeedsFullUpload = true;
|
||||
this.borderDirtyRows.clear();
|
||||
}
|
||||
|
||||
refreshPalette() {
|
||||
@@ -411,6 +483,7 @@ export class TerritoryWebGLRenderer {
|
||||
|
||||
const uploadSpan = FrameProfiler.start();
|
||||
this.uploadStateTexture();
|
||||
this.uploadBorderTexture();
|
||||
FrameProfiler.end("TerritoryWebGLRenderer:uploadState", uploadSpan);
|
||||
|
||||
const renderSpan = FrameProfiler.start();
|
||||
@@ -444,6 +517,8 @@ export class TerritoryWebGLRenderer {
|
||||
gl.uniform1f(this.uniforms.time, currentTime);
|
||||
}
|
||||
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
gl.bindVertexArray(null);
|
||||
FrameProfiler.end("TerritoryWebGLRenderer:draw", renderSpan);
|
||||
@@ -495,6 +570,55 @@ export class TerritoryWebGLRenderer {
|
||||
this.dirtyRows.clear();
|
||||
}
|
||||
|
||||
private uploadBorderTexture() {
|
||||
if (!this.gl || !this.borderColorTexture) return;
|
||||
const gl = this.gl;
|
||||
gl.activeTexture(gl.TEXTURE3);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.borderColorTexture);
|
||||
|
||||
if (this.borderNeedsFullUpload) {
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RGBA8,
|
||||
this.canvas.width,
|
||||
this.canvas.height,
|
||||
0,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
this.borderColorData,
|
||||
);
|
||||
this.borderNeedsFullUpload = false;
|
||||
this.borderDirtyRows.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.borderDirtyRows.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [y, span] of this.borderDirtyRows) {
|
||||
const width = span.maxX - span.minX + 1;
|
||||
const offset = (y * this.canvas.width + span.minX) * 4;
|
||||
const rowSlice = this.borderColorData.subarray(
|
||||
offset,
|
||||
offset + width * 4,
|
||||
);
|
||||
gl.texSubImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
span.minX,
|
||||
y,
|
||||
width,
|
||||
1,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
rowSlice,
|
||||
);
|
||||
}
|
||||
this.borderDirtyRows.clear();
|
||||
}
|
||||
|
||||
private uploadPalette() {
|
||||
if (!this.gl || !this.paletteTexture || !this.relationTexture) return;
|
||||
const gl = this.gl;
|
||||
@@ -596,6 +720,7 @@ export class TerritoryWebGLRenderer {
|
||||
uniform usampler2D u_state;
|
||||
uniform sampler2D u_palette;
|
||||
uniform usampler2D u_relations;
|
||||
uniform sampler2D u_borderColor;
|
||||
uniform vec2 u_resolution;
|
||||
uniform vec4 u_fallout;
|
||||
uniform vec4 u_altSelf;
|
||||
@@ -675,8 +800,17 @@ export class TerritoryWebGLRenderer {
|
||||
}
|
||||
|
||||
vec4 base = texelFetch(u_palette, ivec2(int(owner), 0), 0);
|
||||
float a = isBorder ? 1.0 : u_alpha;
|
||||
vec4 borderColor = texelFetch(u_borderColor, texCoord, 0);
|
||||
vec3 color = base.rgb;
|
||||
float a = u_alpha;
|
||||
|
||||
if (isBorder && borderColor.a > 0.0) {
|
||||
color = borderColor.rgb;
|
||||
a = borderColor.a;
|
||||
}
|
||||
if (isBorder && borderColor.a <= 0.0) {
|
||||
a = 1.0;
|
||||
}
|
||||
|
||||
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
|
||||
float pulse = u_hoverPulseStrength > 0.0
|
||||
|
||||
@@ -586,6 +586,7 @@ export class GameView implements GameMap {
|
||||
|
||||
private _map: GameMap;
|
||||
private readonly usesSharedTileState: boolean;
|
||||
private readonly terraNullius = new TerraNulliusImpl();
|
||||
|
||||
constructor(
|
||||
public worker: WorkerClient,
|
||||
@@ -741,11 +742,11 @@ export class GameView implements GameMap {
|
||||
|
||||
playerBySmallID(id: number): PlayerView | TerraNullius {
|
||||
if (id === 0) {
|
||||
return new TerraNulliusImpl();
|
||||
return this.terraNullius;
|
||||
}
|
||||
const playerId = this.smallIDToID.get(id);
|
||||
if (playerId === undefined) {
|
||||
throw new Error(`small id ${id} not found`);
|
||||
return this.terraNullius;
|
||||
}
|
||||
return this.player(playerId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user