diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index 3bbd52e..ffbb042 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -602,6 +602,43 @@ export default class EloSettingsTab extends PluginSettingTab { }) .setDisabled(!hs.drawGapBoost.enabled); }); + + // Stability progress bar accordion + const stabAcc = containerEl.createEl('details', { cls: 'elo-settings-accordion' }); + stabAcc.open = false; + stabAcc.createEl('summary', { text: 'Progress bar' }); + const stabBody = stabAcc.createEl('div', { cls: 'elo-settings-body' }); + + new Setting(stabBody) + .setName('Highlight surprising results') + .setDesc( + `Wobble the progress bar when a match result is unexpected, alterting you that your choice was unexpected. Default: ${DEFAULT_SETTINGS.surpriseJitter ? 'On' : 'Off'}.`, + ) + .addToggle((t) => + t + .setValue(this.plugin.settings.surpriseJitter ?? DEFAULT_SETTINGS.surpriseJitter) + .onChange(async (v) => { + this.plugin.settings.surpriseJitter = v; + await this.plugin.saveSettings(); + }), + ); + + new Setting(stabBody) + .setName('Stability threshold') + .setDesc( + `Sigma value at which the progress bar reaches 100%. Lower values require more matches. Default: ${DEFAULT_SETTINGS.stabilityThreshold}.`, + ) + .addSlider((sl) => { + const current = + this.plugin.settings.stabilityThreshold ?? DEFAULT_SETTINGS.stabilityThreshold; + sl.setLimits(80, 250, 10) + .setValue(Math.max(80, Math.min(250, current))) + .setDynamicTooltip() + .onChange(async (value) => { + this.plugin.settings.stabilityThreshold = Math.round(value); + await this.plugin.saveSettings(); + }); + }); } private async deleteCohortWithConfirm(cohortKey: string): Promise { diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 3b79fce..8842e20 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -45,6 +45,8 @@ export interface EloSettings { frontmatterProperties: FrontmatterPropertiesSettings; askForOverridesOnCohortCreation: boolean; heuristics: EloHeuristicsSettings; + stabilityThreshold: number; + surpriseJitter: boolean; templatesFolderPath: string; } @@ -85,6 +87,9 @@ export const DEFAULT_SETTINGS: EloSettings = { }, }, + stabilityThreshold: 150, + surpriseJitter: true, + templatesFolderPath: '', }; diff --git a/src/ui/ArenaSession.ts b/src/ui/ArenaSession.ts index d3fe7cd..b96387a 100644 --- a/src/ui/ArenaSession.ts +++ b/src/ui/ArenaSession.ts @@ -451,11 +451,6 @@ export default class ArenaSession { // ---- Helpers - Indicate convergence on stable ratings ---- - // Sigma at which rankings are practically stable (well above the - // theoretical minimum (MIN_SIGMA = 30) but low enough that further - // matches produce negligible rating changes) - private static readonly STABLE_SIGMA = 150; - private computeStabilityPercent(): number { const n = this.files.length; if (n < 2) return 0; @@ -472,7 +467,8 @@ export default class ArenaSession { const unmatched = Math.max(0, n - players.length); const avgSigma = (playedSum + unmatched * DEFAULT_SIGMA) / n; - const range = DEFAULT_SIGMA - ArenaSession.STABLE_SIGMA; + const stableSigma = this.plugin.settings.stabilityThreshold ?? 150; + const range = DEFAULT_SIGMA - stableSigma; return Math.max(0, Math.min(100, ((DEFAULT_SIGMA - avgSigma) / range) * 100)); } @@ -506,36 +502,46 @@ export default class ArenaSession { this.stabilityJitterTimer = undefined; } + const jitterEnabled = this.plugin.settings.surpriseJitter ?? true; const prefersReduced = win.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true; // Jitter: slide back, overshoot forward, then settle at the true value. // Amplitude scales with surprise (0–1) so mild upsets barely wobble. const SURPRISE_THRESHOLD = 0.15; - if (surprise > SURPRISE_THRESHOLD && !prefersReduced) { - const amp = Math.min(8, surprise * 16); - const back1 = Math.max(0, pct - amp); - const fwd1 = Math.min(100, pct + amp * 0.6); - const back2 = Math.max(0, pct - amp * 0.5); - const fwd2 = Math.min(100, pct + amp * 0.3); - const step = 150; - - const setWidth = (v: number) => - this.stabilityBarFillEl?.setCssProps({ '--stability-width': `${v}%` }); - - setWidth(back1); - this.stabilityJitterTimer = win.setTimeout(() => { - setWidth(fwd1); + if (jitterEnabled && surprise > SURPRISE_THRESHOLD) { + if (prefersReduced) { + this.stabilityBarFillEl.setCssProps({ '--stability-width': `${pct}%` }); + this.stabilityBarFillEl.classList.add('is-surprise'); this.stabilityJitterTimer = win.setTimeout(() => { - setWidth(back2); + this.stabilityBarFillEl?.classList.remove('is-surprise'); + this.stabilityJitterTimer = undefined; + }, 600); + } else { + const amp = Math.min(8, surprise * 16); + const back1 = Math.max(0, pct - amp); + const fwd1 = Math.min(100, pct + amp * 0.6); + const back2 = Math.max(0, pct - amp * 0.5); + const fwd2 = Math.min(100, pct + amp * 0.3); + const step = 150; + + const setWidth = (v: number) => + this.stabilityBarFillEl?.setCssProps({ '--stability-width': `${v}%` }); + + setWidth(back1); + this.stabilityJitterTimer = win.setTimeout(() => { + setWidth(fwd1); this.stabilityJitterTimer = win.setTimeout(() => { - setWidth(fwd2); + setWidth(back2); this.stabilityJitterTimer = win.setTimeout(() => { - setWidth(pct); - this.stabilityJitterTimer = undefined; + setWidth(fwd2); + this.stabilityJitterTimer = win.setTimeout(() => { + setWidth(pct); + this.stabilityJitterTimer = undefined; + }, step); }, step); }, step); }, step); - }, step); + } } else { this.stabilityBarFillEl.setCssProps({ '--stability-width': `${pct}%` }); } diff --git a/styles.css b/styles.css index 3e0691e..c0c5c10 100644 --- a/styles.css +++ b/styles.css @@ -169,6 +169,10 @@ background-color: var(--color-green); } +.elo-stability-fill.is-surprise { + background-color: var(--color-red); +} + @media (prefers-reduced-motion: reduce) { .elo-warning { transition: none;