diff --git a/src/client/controllers/BuildPreviewController.ts b/src/client/controllers/BuildPreviewController.ts index ea9338e93..3a9de6cdb 100644 --- a/src/client/controllers/BuildPreviewController.ts +++ b/src/client/controllers/BuildPreviewController.ts @@ -163,18 +163,13 @@ export class BuildPreviewController implements Controller { } } - // targetingAlly is computed above for state purposes; the renderer's - // ghost passes derive their own "warning" visual from canBuild/canUpgrade - // if needed. (Leave the variable here so its eslint-no-unused doesn't trip.) - void targetingAlly; - this.game ?.myPlayer() ?.buildables(tileRef, [this.ghostUnit?.buildableUnit.type]) .then((buildables) => { if (!this.ghostUnit) { this.pendingConfirm = null; - this.emitGhostPreview(tileRef); + this.emitGhostPreview(tileRef, targetingAlly); return; } @@ -187,7 +182,7 @@ export class BuildPreviewController implements Controller { canUpgrade: false, }); this.pendingConfirm = null; - this.emitGhostPreview(tileRef); + this.emitGhostPreview(tileRef, targetingAlly); return; } @@ -201,7 +196,7 @@ export class BuildPreviewController implements Controller { } } - this.emitGhostPreview(tileRef); + this.emitGhostPreview(tileRef, targetingAlly); }); } @@ -211,8 +206,11 @@ export class BuildPreviewController implements Controller { * the ghost can't be placed. smoothLoop interpolates displayed position * toward the target tile each frame. */ - private emitGhostPreview(tileRef: TileRef | undefined): void { - const data = this.buildGhostPreviewData(tileRef); + private emitGhostPreview( + tileRef: TileRef | undefined, + targetingAlly: boolean, + ): void { + const data = this.buildGhostPreviewData(tileRef, targetingAlly); if (data === null) { this.lastGhostData = null; this.view.updateGhostPreview(null); @@ -302,6 +300,7 @@ export class BuildPreviewController implements Controller { private buildGhostPreviewData( tileRef: TileRef | undefined, + targetingAlly: boolean, ): GhostPreviewData | null { if (!this.ghostUnit) return null; if (tileRef === undefined) return null; @@ -352,6 +351,7 @@ export class BuildPreviewController implements Controller { ownerID: myPlayer.smallID(), upgradeTargetTile, rangeRadius, + rangeWarning: targetingAlly, }; } diff --git a/src/client/render/gl/passes/RangeCirclePass.ts b/src/client/render/gl/passes/RangeCirclePass.ts index 5fb38e805..304f81630 100644 --- a/src/client/render/gl/passes/RangeCirclePass.ts +++ b/src/client/render/gl/passes/RangeCirclePass.ts @@ -1,6 +1,7 @@ /** - * RangeCirclePass — draws a translucent white circle showing the effective - * range of a structure during build-mode ghost preview. + * RangeCirclePass — draws a translucent circle showing the effective + * range of a structure during build-mode ghost preview. White by default, + * red when the ghost flags a warning (e.g. nuking would break an alliance). * * Single quad with circle SDF in the fragment shader. * Active only when a ghost preview with rangeRadius > 0 is set. @@ -20,10 +21,12 @@ export class RangeCirclePass { private uCamera: WebGLUniformLocation; private uCenter: WebGLUniformLocation; private uRadius: WebGLUniformLocation; + private uColor: WebGLUniformLocation; private centerX = 0; private centerY = 0; private radius = 0; + private warning = false; constructor(gl: WebGL2RenderingContext) { this.gl = gl; @@ -32,6 +35,7 @@ export class RangeCirclePass { this.uCamera = gl.getUniformLocation(this.program, "uCamera")!; this.uCenter = gl.getUniformLocation(this.program, "uCenter")!; this.uRadius = gl.getUniformLocation(this.program, "uRadius")!; + this.uColor = gl.getUniformLocation(this.program, "uColor")!; // Unit quad [0,1] this.vao = gl.createVertexArray()!; @@ -53,8 +57,10 @@ export class RangeCirclePass { this.centerX = data.tileX; this.centerY = data.tileY; this.radius = data.rangeRadius; + this.warning = data.rangeWarning; } else { this.radius = 0; + this.warning = false; } } @@ -66,6 +72,11 @@ export class RangeCirclePass { gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix); gl.uniform2f(this.uCenter, this.centerX, this.centerY); gl.uniform1f(this.uRadius, this.radius); + if (this.warning) { + gl.uniform3f(this.uColor, 1.0, 0.2, 0.2); + } else { + gl.uniform3f(this.uColor, 1.0, 1.0, 1.0); + } gl.bindVertexArray(this.vao); gl.drawArrays(gl.TRIANGLES, 0, 6); diff --git a/src/client/render/gl/shaders/range-circle/range-circle.frag.glsl b/src/client/render/gl/shaders/range-circle/range-circle.frag.glsl index 62744ab37..7c89c77d8 100644 --- a/src/client/render/gl/shaders/range-circle/range-circle.frag.glsl +++ b/src/client/render/gl/shaders/range-circle/range-circle.frag.glsl @@ -4,6 +4,7 @@ precision highp float; in vec2 vLocal; // [-1, +1] uniform float uRadius; +uniform vec3 uColor; out vec4 fragColor; @@ -23,5 +24,5 @@ void main() { float alpha = fill * 0.2 + stroke * 0.5; if (alpha < 0.001) discard; - fragColor = vec4(1.0, 1.0, 1.0, alpha); + fragColor = vec4(uColor, alpha); } diff --git a/src/client/render/types/Renderer.ts b/src/client/render/types/Renderer.ts index 26fb67383..652a5e338 100644 --- a/src/client/render/types/Renderer.ts +++ b/src/client/render/types/Renderer.ts @@ -166,6 +166,8 @@ export interface GhostPreviewData { upgradeTargetTile: number | null; /** Range radius in tiles for the placement circle (0 = no circle). */ rangeRadius: number; + /** True if placing here would carry a penalty (e.g. nuking an ally → traitor). */ + rangeWarning: boolean; } /** Nuke trajectory preview data — Bezier control points + color thresholds. */