From 4936ae3d59854c786c42b4c863bbc54d1a50fb4e Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 18 May 2026 09:43:14 -0700 Subject: [PATCH] restore spawn-phase glow with a true breathing animation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SpawnOverlayPass had everything wired except a caller. WebGLFrameBuilder now collects spawned human players each tick during spawn phase and pushes their territory centroid + color through view.updateSpawnOverlay. myPlayer reads as white so the local-player ring stands out. Reshaped the shader animation: dropped the growing-disc effect, gave the ring a true breath — radius scales 0.5×→1.15× while opacity pulses 35%→100% in phase. Replaced the sharp inner-edge ramp with a smooth center-to-boundary fill so there's no hard cutoff or empty hole in the middle. animSpeed bumped to 0.0035 (~1 breath/sec). --- src/client/WebGLFrameBuilder.ts | 47 +++++++++++++++++- src/client/render/gl/render-settings.json | 2 +- .../spawn-overlay/spawn-overlay.frag.glsl | 48 +++++++++---------- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/src/client/WebGLFrameBuilder.ts b/src/client/WebGLFrameBuilder.ts index 065fc4ebb..99bb15d40 100644 --- a/src/client/WebGLFrameBuilder.ts +++ b/src/client/WebGLFrameBuilder.ts @@ -1,7 +1,12 @@ import { Colord } from "colord"; +import { PlayerType } from "../core/game/Game"; import { GameView } from "../core/game/GameView"; import { uploadFrameData } from "./render/frame/Upload"; -import { PlayerStatic, GameView as WebGLGameView } from "./render/gl"; +import { + PlayerStatic, + SpawnCenter, + GameView as WebGLGameView, +} from "./render/gl"; const PALETTE_SIZE = 4096; @@ -31,6 +36,7 @@ export class WebGLFrameBuilder { update(gameView: GameView): void { this.syncPlayers(gameView); this.syncLocalPlayer(gameView); + this.syncSpawnOverlay(gameView); uploadFrameData(this.view, gameView.frameData()); } @@ -41,6 +47,45 @@ export class WebGLFrameBuilder { this.view.setLocalPlayerID(sid); } + /** + * Spawn-phase highlights: each already-spawned human player gets a colored + * ring + tile glow around their starting territory. Pushed every tick + * during spawn phase; the pass animates locally from the snapshot. + */ + private syncSpawnOverlay(gameView: GameView): void { + const inSpawnPhase = gameView.inSpawnPhase(); + if (!inSpawnPhase) { + this.view.updateSpawnOverlay(false, []); + return; + } + const me = gameView.myPlayer(); + const myTeam = me?.team() ?? null; + const centers: SpawnCenter[] = []; + for (const p of gameView.players()) { + if (!p.isPlayer() || p.type() !== PlayerType.Human) continue; + if (!p.hasSpawned()) continue; + const isSelf = me !== null && p.smallID() === me.smallID(); + // myPlayer reads as plain white so the local-player ring is visually + // distinct from any team color; everyone else uses their territory tint. + const c = isSelf + ? { r: 255, g: 255, b: 255 } + : p.territoryColor().toRgb(); + centers.push({ + x: p.nameData?.x ?? 0, + y: p.nameData?.y ?? 0, + r: c.r / 255, + g: c.g / 255, + b: c.b / 255, + isSelf, + isTeammate: + myTeam !== null && + p.team() === myTeam && + p.smallID() !== me?.smallID(), + }); + } + this.view.updateSpawnOverlay(true, centers); + } + private syncPlayers(gameView: GameView): void { const newPlayers: PlayerStatic[] = []; for (const p of gameView.players()) { diff --git a/src/client/render/gl/render-settings.json b/src/client/render/gl/render-settings.json index e7d2058ec..e51eaef0f 100644 --- a/src/client/render/gl/render-settings.json +++ b/src/client/render/gl/render-settings.json @@ -245,7 +245,7 @@ "selfMaxRad": 24, "mateMinRad": 5, "mateMaxRad": 14, - "animSpeed": 0.005, + "animSpeed": 0.0035, "gradientInnerEdge": 0.01, "gradientSolidEnd": 0.1 }, diff --git a/src/client/render/gl/shaders/spawn-overlay/spawn-overlay.frag.glsl b/src/client/render/gl/shaders/spawn-overlay/spawn-overlay.frag.glsl index fd0578d82..8e116ebd2 100644 --- a/src/client/render/gl/shaders/spawn-overlay/spawn-overlay.frag.glsl +++ b/src/client/render/gl/shaders/spawn-overlay/spawn-overlay.frag.glsl @@ -67,34 +67,32 @@ void main() { continue; } - // Static outer ring: radial gradient from minR to maxR - float range = maxR - minR; - float t = (dist - minR) / range; - if (t > 0.0 && t <= 1.0) { - float innerEdge = uGradientStops.x; - float solidEnd = uGradientStops.y; - float alpha; - if (t < innerEdge) { - alpha = t / innerEdge; - } else if (t < solidEnd) { - alpha = 1.0; - } else { - alpha = 1.0 - (t - solidEnd) / (1.0 - solidEnd); - } - + // Breathing ring: the gradient halo shrinks/expands in radius AND its + // opacity pulses in phase with the breath — both driven by uBreathRadius. + // Smooth bell shape: glow ramps up from center to the inner edge, stays + // solid through the ring's body, then fades out past solidEnd. No hard + // cutoffs at either side. + float scale = 0.5 + 0.65 * uBreathRadius; // 0.5 → 1.15 of base radius + float bMinR = minR * scale; + float bMaxR = maxR * scale; + float range = bMaxR - bMinR; + float t = (dist - bMinR) / range; + float solidEnd = uGradientStops.y; + float alpha = 0.0; + if (dist < bMinR) { + // Inner glow: linear ramp from 0 at center to 1 at the ring's inner edge. + alpha = dist / max(bMinR, 0.001); + } else if (t < solidEnd) { + alpha = 1.0; + } else if (t < 1.0) { + alpha = 1.0 - (t - solidEnd) / (1.0 - solidEnd); + } + if (alpha > 0.0) { + // Opacity pulses 35% → 100% in phase with the radius. + alpha *= 0.35 + 0.65 * uBreathRadius; result.rgb = mix(result.rgb, color, alpha * (1.0 - result.a)); result.a = result.a + alpha * (1.0 - result.a); } - - // Breathing ring: solid colored disc from minR to breathR - float breathR = minR + range * uBreathRadius; - if (breathR > minR + 0.01) { - if (dist >= minR && dist <= breathR) { - float edge = smoothstep(minR, minR + 0.1, dist); - result.rgb = color; - result.a = max(result.a, edge); - } - } } if (result.a < 0.001) discard;