mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-23 20:10:43 +00:00
feat(highlight): replace the small-player highlight toggle with a strength slider (#4588)
## Description: Replaces the "Highlight small players" on/off toggle with a **0-500% Strength slider** controlling the glow's **width** (0 = off, 100% = the previous default). - `UserSettings`: `highlightSmallPlayers()`/toggle → `highlightGlowStrength()`/set, float-backed, clamped to [0, 5]. - `SmallPlayerGlowPass` takes the strength via a setter (pushed by `ClientGameRunner` on the settings-changed event), so it stays a pure consumer and the slider still updates live while the settings modal has the sim paused. Width = a non-linear blur-iteration count; intensity compensated so wider stays about as bright. - `MapRenderer` persists the strength across a WebGL context restore. - `SettingsModal`: the toggle row becomes a range input. Behavior note: with the toggle gone the glow **defaults ON at 100%** (old toggle defaulted OFF); can default to 0 (opt-in) if preferred. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory
This commit is contained in:
@@ -572,6 +572,17 @@ async function createClientGame(
|
||||
{ signal: graphicsListenerAbort.signal },
|
||||
);
|
||||
|
||||
// Push the small-player glow Strength to the pass (which is a pure consumer)
|
||||
// on the settings-changed event, so moving the slider updates the glow live
|
||||
// even while the settings modal has the sim paused.
|
||||
view.setSmallPlayerGlowStrength(userSettings.highlightGlowStrength());
|
||||
globalThis.addEventListener(
|
||||
`${USER_SETTINGS_CHANGED_EVENT}:settings.highlightGlowStrength`,
|
||||
() =>
|
||||
view.setSmallPlayerGlowStrength(userSettings.highlightGlowStrength()),
|
||||
{ signal: graphicsListenerAbort.signal },
|
||||
);
|
||||
|
||||
// Re-resolve names drawn on the map when the anonymous-names setting toggles
|
||||
// so they switch live, like the leaderboard.
|
||||
globalThis.addEventListener(
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "../core/CosmeticSchemas";
|
||||
import { decodePatternData } from "../core/PatternDecoder";
|
||||
import { PlayerType } from "../core/game/Game";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import { getCachedCosmetics } from "./Cosmetics";
|
||||
import { uploadFrameData } from "./render/frame/Upload";
|
||||
// Type-only: a value import would pull GPURenderer and its `.glsl?raw` shader
|
||||
@@ -197,7 +196,6 @@ export class WebGLFrameBuilder {
|
||||
}
|
||||
|
||||
private readonly highlightSetBuf = new Uint8Array(PALETTE_SIZE);
|
||||
private readonly userSettings = new UserSettings();
|
||||
private glowRescanTick = 0;
|
||||
|
||||
update(gameView: GameView): void {
|
||||
@@ -350,8 +348,9 @@ export class WebGLFrameBuilder {
|
||||
* Client-only view — toggle it live in the settings.
|
||||
*/
|
||||
private syncSmallPlayerGlow(gameView: GameView): void {
|
||||
// Strength (incl. off at 0) is read live in the glow pass; here we only
|
||||
// decide who qualifies. Skip spawn + the first minute.
|
||||
if (
|
||||
!this.userSettings.highlightSmallPlayers() ||
|
||||
gameView.inSpawnPhase() ||
|
||||
gameView.elapsedGameSeconds() < SMALL_PLAYER_GLOW_GRACE_SECONDS
|
||||
) {
|
||||
|
||||
@@ -135,11 +135,6 @@ export class SettingsModal extends LitElement implements Controller {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onToggleHighlightSmallPlayersButtonClick() {
|
||||
this.userSettings.toggleHighlightSmallPlayers();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onToggleAlertFrameButtonClick() {
|
||||
this.userSettings.toggleAlertFrame();
|
||||
this.requestUpdate();
|
||||
@@ -210,6 +205,12 @@ export class SettingsModal extends LitElement implements Controller {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onHighlightGlowStrengthChange(event: Event) {
|
||||
const strength = parseFloat((event.target as HTMLInputElement).value) / 100;
|
||||
this.userSettings.setHighlightGlowStrength(strength);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return null;
|
||||
@@ -356,30 +357,35 @@ export class SettingsModal extends LitElement implements Controller {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
<div
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click="${this.onToggleHighlightSmallPlayersButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${highlightIcon}
|
||||
alt="highlightSmallPlayers"
|
||||
alt="highlightGlowStrength"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
${translateText("user_setting.highlight_small_players_label")}
|
||||
${translateText("user_setting.highlight_glow_strength_label")}
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${translateText("user_setting.highlight_small_players_desc")}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
.value=${this.userSettings.highlightGlowStrength() * 100}
|
||||
@input=${this.onHighlightGlowStrengthChange}
|
||||
class="w-full border border-slate-500 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${this.userSettings.highlightSmallPlayers()
|
||||
? translateText("user_setting.on")
|
||||
: translateText("user_setting.off")}
|
||||
${Math.round(this.userSettings.highlightGlowStrength() * 100)}%
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
|
||||
@@ -35,6 +35,10 @@ import type { RenderSettings } from "./RenderSettings";
|
||||
export class MapRenderer {
|
||||
private renderer: GPURenderer | null = null;
|
||||
private resizeObs: ResizeObserver | null = null;
|
||||
// Persisted so a WebGL context restore (which recreates GPURenderer via
|
||||
// initRenderer) reapplies the user's chosen glow strength instead of
|
||||
// resetting it to the pass default until the next settings change.
|
||||
private smallPlayerGlowStrength = 1;
|
||||
|
||||
/**
|
||||
* Called after a lost WebGL context is restored and the renderer has been
|
||||
@@ -91,6 +95,8 @@ export class MapRenderer {
|
||||
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
if (rect.width > 0) this.renderer.resize(rect.width, rect.height);
|
||||
// Reapply state that lives outside RenderSettings so it survives a restore.
|
||||
this.renderer.setSmallPlayerGlowStrength(this.smallPlayerGlowStrength);
|
||||
};
|
||||
|
||||
private handleContextLost = (e: Event) => {
|
||||
@@ -244,6 +250,12 @@ export class MapRenderer {
|
||||
this.renderer?.updateSmallPlayerGlow(set);
|
||||
}
|
||||
|
||||
/** Set the small-player glow Strength (0 = off, 1 = default, capped at 5). */
|
||||
setSmallPlayerGlowStrength(strength: number): void {
|
||||
this.smallPlayerGlowStrength = strength;
|
||||
this.renderer?.setSmallPlayerGlowStrength(strength);
|
||||
}
|
||||
|
||||
// ---- Selection box ----
|
||||
|
||||
/** Set multiple selected units (multi-select). Pass [] to clear. */
|
||||
|
||||
@@ -1002,6 +1002,10 @@ export class GPURenderer {
|
||||
this.smallPlayerGlowPass.update(set);
|
||||
}
|
||||
|
||||
setSmallPlayerGlowStrength(strength: number): void {
|
||||
this.smallPlayerGlowPass.setGlowStrength(strength);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -57,6 +57,8 @@ export class SmallPlayerGlowPass {
|
||||
private dirty = false; // aura needs rebuilding (set changed)
|
||||
private animTime = 0;
|
||||
private lastTime = 0;
|
||||
private lastPasses = 1; // last width the aura was blurred at (to detect change)
|
||||
private glowStrength = 1; // pushed via setGlowStrength; 0 = off, 1 = default, capped at 5
|
||||
|
||||
constructor(
|
||||
gl: WebGL2RenderingContext,
|
||||
@@ -121,6 +123,16 @@ export class SmallPlayerGlowPass {
|
||||
this.quadVao = createFullscreenQuad(gl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push the glow Strength (0 = off, 1 = default, capped at 5). The client reads
|
||||
* it from UserSettings and pushes it here so the pass stays a pure consumer
|
||||
* (passes never read game/DOM state directly); pushing on the settings-changed
|
||||
* event keeps it live even while the settings modal has the sim paused.
|
||||
*/
|
||||
setGlowStrength(strength: number): void {
|
||||
this.glowStrength = strength;
|
||||
}
|
||||
|
||||
/** Push the highlight set (1 byte per owner smallID), or null to disable. */
|
||||
update(set: Uint8Array | null): void {
|
||||
if (set === null) {
|
||||
@@ -144,8 +156,34 @@ export class SmallPlayerGlowPass {
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
// Strength (0 = off, 1 = default width) -> blur iterations; each iteration
|
||||
// widens the Gaussian aura. Non-linear so the slider ramps gently low and
|
||||
// grows fast toward the top: 500% ≈ 82 passes (very wide). Clamped for perf.
|
||||
private passesFor(strength: number): number {
|
||||
return Math.min(96, Math.max(1, Math.round(strength ** 2.74)));
|
||||
}
|
||||
|
||||
// One separable-blur axis: sample `src`, write the blurred result into `dst`.
|
||||
private blurAxis(
|
||||
src: RenderTarget,
|
||||
dst: RenderTarget,
|
||||
dx: number,
|
||||
dy: number,
|
||||
): void {
|
||||
const gl = this.gl;
|
||||
toTarget(gl, dst, () => {
|
||||
gl.uniform2f(this.uBlurDir, dx, dy);
|
||||
gl.bindTexture(gl.TEXTURE_2D, src.tex);
|
||||
gl.bindVertexArray(this.quadVao);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
});
|
||||
}
|
||||
|
||||
draw(cameraMatrix: Float32Array): void {
|
||||
if (!this.active) return;
|
||||
// Strength is pushed by the client (not tick-gated), so a slider change is
|
||||
// live even while the sim is paused (e.g. the settings modal is open).
|
||||
const strength = this.glowStrength;
|
||||
if (!this.active || strength <= 0) return;
|
||||
|
||||
const gl = this.gl;
|
||||
const s = this.settings;
|
||||
@@ -161,6 +199,11 @@ export class SmallPlayerGlowPass {
|
||||
}
|
||||
this.lastTime = now;
|
||||
const pulse = 0.5 + 0.5 * Math.sin(this.animTime);
|
||||
const passes = this.passesFor(strength);
|
||||
if (passes !== this.lastPasses) {
|
||||
this.dirty = true; // width changed -> rebuild the aura this frame
|
||||
this.lastPasses = passes;
|
||||
}
|
||||
|
||||
// Rebuild the blurred aura only when the set changed (~1/s); its inputs
|
||||
// don't move faster than that. The composite below still runs every frame.
|
||||
@@ -179,21 +222,14 @@ export class SmallPlayerGlowPass {
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
});
|
||||
|
||||
// Separable blur: horizontal into B, vertical back into A.
|
||||
// Separable blur, iterated `passes` times to widen the aura (more
|
||||
// iterations = wider Gaussian). Each pass: horizontal A->B, vertical B->A.
|
||||
gl.useProgram(this.blurProg);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
toTarget(gl, b, () => {
|
||||
gl.uniform2f(this.uBlurDir, 1 / a.w, 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, a.tex);
|
||||
gl.bindVertexArray(this.quadVao);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
});
|
||||
toTarget(gl, a, () => {
|
||||
gl.uniform2f(this.uBlurDir, 0, 1 / b.h);
|
||||
gl.bindTexture(gl.TEXTURE_2D, b.tex);
|
||||
gl.bindVertexArray(this.quadVao);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
});
|
||||
for (let k = 0; k < passes; k++) {
|
||||
this.blurAxis(a, b, 1 / a.w, 0); // horizontal A->B
|
||||
this.blurAxis(b, a, 0, 1 / b.h); // vertical B->A
|
||||
}
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
@@ -207,7 +243,9 @@ export class SmallPlayerGlowPass {
|
||||
gl.uniformMatrix3fv(this.uCompositeCam, false, cameraMatrix);
|
||||
gl.uniform2f(this.uCompositeMapSize, this.mapW, this.mapH);
|
||||
gl.uniform3fv(this.uGlowColor, s.color);
|
||||
gl.uniform1f(this.uIntensity, s.alpha * pulse);
|
||||
// Widening spreads the mask thinner, so scale intensity up to keep the
|
||||
// glow about as bright (wider, not darker), capped by the shader's alpha.
|
||||
gl.uniform1f(this.uIntensity, s.alpha * pulse * Math.sqrt(passes));
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, a.tex);
|
||||
gl.bindVertexArray(this.mapVao);
|
||||
|
||||
Reference in New Issue
Block a user