diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index db807385f..934e5bb4d 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -445,6 +445,13 @@ async function createClientGame( (e) => applyDayNightMode((e as CustomEvent).detail === "true"), ); + // The WebGL canvas has pointer-events: none so input flows through the + // overlay div. Forward pointermove to the WebGL view's MapInteraction so + // hover-driven features (highlight owner, etc.) still work. + inputOverlay.addEventListener("pointermove", (e) => + view.handlePointerMove(e), + ); + const gameRenderer = createRenderer( inputOverlay, gameView, diff --git a/src/client/WebGLFrameBuilder.ts b/src/client/WebGLFrameBuilder.ts index 5393b9279..b98228ef2 100644 --- a/src/client/WebGLFrameBuilder.ts +++ b/src/client/WebGLFrameBuilder.ts @@ -19,6 +19,10 @@ const PALETTE_SIZE = 4096; export class WebGLFrameBuilder { private readonly palette: Float32Array; private readonly knownSmallIDs = new Set(); + // The renderer needs to know which player is "me" so affiliation tint, + // unit colors, and SAM-radius perspective work. Push it once the local + // player's update arrives (may take several ticks during join). + private localPlayerSmallID = 0; constructor(private readonly view: WebGLGameView) { this.palette = new Float32Array(PALETTE_SIZE * 2 * 4); @@ -26,9 +30,17 @@ export class WebGLFrameBuilder { update(gameView: GameView): void { this.syncPlayers(gameView); + this.syncLocalPlayer(gameView); uploadFrameData(this.view, gameView.frameData()); } + private syncLocalPlayer(gameView: GameView): void { + const sid = gameView.myPlayer()?.smallID() ?? 0; + if (sid === this.localPlayerSmallID) return; + this.localPlayerSmallID = sid; + this.view.setLocalPlayerID(sid); + } + private syncPlayers(gameView: GameView): void { const newPlayers: PlayerStatic[] = []; for (const p of gameView.players()) { diff --git a/src/client/render/gl/debug/layout.ts b/src/client/render/gl/debug/layout.ts index 14551bf2e..e94f2ef9b 100644 --- a/src/client/render/gl/debug/layout.ts +++ b/src/client/render/gl/debug/layout.ts @@ -116,7 +116,16 @@ export function buildTree(s: RenderSettings, d: RenderSettings): DebugNode[] { 0, 1, 0.01, - "Highlight Brighten", + "Highlight Brighten (border)", + ), + slider( + s.mapOverlay, + "highlightFillBrighten", + d.mapOverlay, + 0, + 1, + 0.01, + "Highlight Brighten (fill)", ), slider( s.mapOverlay, diff --git a/src/client/render/gl/game-view.ts b/src/client/render/gl/game-view.ts index 6786d9cc1..48d7f0306 100644 --- a/src/client/render/gl/game-view.ts +++ b/src/client/render/gl/game-view.ts @@ -110,6 +110,18 @@ export class GameView { if (rect.width > 0) this.renderer.resize(rect.width, rect.height); } + /** + * Forward a pointermove event into the MapInteraction handler. The WebGL + * canvas itself has pointer-events: none (input flows through a separate + * overlay div in the main client), so the listener bound to `canvas` in + * the constructor never actually fires for game-mode input. Callers that + * own the active input element forward pointermove events here so hover + * tracking + setHighlightOwner still work. + */ + handlePointerMove(e: PointerEvent): void { + this.interaction.handlePointerMove(e); + } + // ---- Event system ---- on( diff --git a/src/client/render/gl/passes/territory-pass.ts b/src/client/render/gl/passes/territory-pass.ts index c2e748575..63c0a664a 100644 --- a/src/client/render/gl/passes/territory-pass.ts +++ b/src/client/render/gl/passes/territory-pass.ts @@ -34,6 +34,9 @@ export class TerritoryPass { private uCharcoalBase: WebGLUniformLocation; private uCharcoalVariation: WebGLUniformLocation; private uCharcoalAlpha: WebGLUniformLocation; + private uHighlightOwner: WebGLUniformLocation; + private uHighlightBrighten: WebGLUniformLocation; + private highlightOwner = 0; private vao: WebGLVertexArrayObject; private tileTex: WebGLTexture; @@ -101,6 +104,14 @@ export class TerritoryPass { this.program, "uCharcoalAlpha", )!; + this.uHighlightOwner = gl.getUniformLocation( + this.program, + "uHighlightOwner", + )!; + this.uHighlightBrighten = gl.getUniformLocation( + this.program, + "uHighlightBrighten", + )!; gl.useProgram(this.program); gl.uniform1i(gl.getUniformLocation(this.program, "uTileTex"), 0); @@ -319,6 +330,11 @@ export class TerritoryPass { this.altView = active; } + /** Set the hovered player's smallID for territory-fill brightening (0 = off). */ + setHighlightOwner(ownerID: number): void { + this.highlightOwner = ownerID; + } + /** Draw territory fill + fallout charcoal. Blending must be enabled by caller. */ draw(cameraMatrix: Float32Array): void { this.flushTileTexture(); @@ -334,6 +350,8 @@ export class TerritoryPass { gl.uniform1f(this.uCharcoalBase, mo.charcoalBase); gl.uniform1f(this.uCharcoalVariation, mo.charcoalVariation); gl.uniform1f(this.uCharcoalAlpha, mo.charcoalAlpha); + gl.uniform1ui(this.uHighlightOwner, this.highlightOwner); + gl.uniform1f(this.uHighlightBrighten, mo.highlightFillBrighten); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.tileTex); diff --git a/src/client/render/gl/render-settings.json b/src/client/render/gl/render-settings.json index e38b0e3c1..e7d2058ec 100644 --- a/src/client/render/gl/render-settings.json +++ b/src/client/render/gl/render-settings.json @@ -65,7 +65,8 @@ "emberColorBrightG": 0.5, "emberColorBrightB": 0.05, "emberStrengthUnowned": 0.5, - "highlightBrighten": 0.6, + "highlightBrighten": 0.25, + "highlightFillBrighten": 0.15, "highlightThicken": 2, "defensePostRange": 30, "embargoTintRatio": 0.35, diff --git a/src/client/render/gl/render-settings.ts b/src/client/render/gl/render-settings.ts index d9bda1b20..2aa9cfc60 100644 --- a/src/client/render/gl/render-settings.ts +++ b/src/client/render/gl/render-settings.ts @@ -68,6 +68,7 @@ export interface RenderSettings { emberColorBrightB: number; emberStrengthUnowned: number; highlightBrighten: number; + highlightFillBrighten: number; highlightThicken: number; defensePostRange: number; embargoTintRatio: number; diff --git a/src/client/render/gl/renderer.ts b/src/client/render/gl/renderer.ts index a1e831cae..d71521712 100644 --- a/src/client/render/gl/renderer.ts +++ b/src/client/render/gl/renderer.ts @@ -694,10 +694,6 @@ export class GPURenderer { this.railroadPass.updateGhostPreview(data); this.rangeCirclePass.updateGhostPreview(data); this.crosshairPass.updateGhostPreview(data); - if (data) this.localPlayerID = data.ownerID; - this.samRadiusPass.setLocalPlayer(this.localPlayerID); - this.affiliationPalette.setLocalPlayer(this.localPlayerID); - this.unitPass.setLocalPlayer(this.localPlayerID); this.samGhostVisible = data !== null && SAM_RADIUS_GHOST_TYPES.has(data.ghostType); this.samRadiusPass.setVisible( @@ -723,6 +719,7 @@ export class GPURenderer { setHighlightOwner(ownerID: number): void { this.borderPass.setHighlightOwner(ownerID); + this.territoryPass.setHighlightOwner(ownerID); } setHighlightStructureTypes(unitTypes: string[] | null): void { this.structurePass.setHighlightTypes(unitTypes); diff --git a/src/client/render/gl/shaders/map-overlay/territory.frag.glsl b/src/client/render/gl/shaders/map-overlay/territory.frag.glsl index 94c0579b4..91664f4b4 100644 --- a/src/client/render/gl/shaders/map-overlay/territory.frag.glsl +++ b/src/client/render/gl/shaders/map-overlay/territory.frag.glsl @@ -10,6 +10,8 @@ uniform int uAltView; uniform float uCharcoalBase; uniform float uCharcoalVariation; uniform float uCharcoalAlpha; +uniform uint uHighlightOwner; // 0 = no highlight; otherwise smallID of hovered owner +uniform float uHighlightBrighten; // mix amount toward white for highlighted tiles in vec2 vWorldPos; out vec4 fragColor; @@ -38,5 +40,12 @@ void main() { // --- Territory fill (owned) --- float u = (float(owner) + 0.5) / float(PALETTE_SIZE); - fragColor = texture(uPalette, vec2(u, 0.25)); + vec4 color = texture(uPalette, vec2(u, 0.25)); + + // Hover highlight: brighten every tile owned by the hovered player. + if (uHighlightOwner != 0u && owner == uHighlightOwner) { + color.rgb = mix(color.rgb, vec3(1.0), uHighlightBrighten); + } + + fragColor = color; }