From 193530c03ee1a03fee87722d88a035dde88ef40b Mon Sep 17 00:00:00 2001 From: bigjakk Date: Sat, 4 Apr 2026 09:24:01 -0700 Subject: [PATCH] feat: add rank progress tracker and rank distribution popup --- src/main/client-ui.ts | 26 +++++- src/preload/competitive.ts | 164 ++++++++++++++++++++++++++++++++++++- src/preload/index.ts | 7 +- 3 files changed, 190 insertions(+), 7 deletions(-) diff --git a/src/main/client-ui.ts b/src/main/client-ui.ts index eb87f4b..76ef44d 100644 --- a/src/main/client-ui.ts +++ b/src/main/client-ui.ts @@ -684,5 +684,29 @@ export const BP_CLAIM_ALL_CSS = ` #claimAllBtn.disabled { opacity: 0.4; pointer-events: none; } `; +// ── Rank progress tracker CSS ── +export const RANK_TRACKER_CSS = ` +#kpc-elo-tracker { width: 100%; margin: 8px 0; } +.kpc-elo-info-row { display: flex; align-items: center; gap: 8px; } +.kpc-rank-container { display: flex; align-items: center; gap: 4px; white-space: nowrap; font-size: 12px; color: #ccc; } +.kpc-elo-rank-img { width: 20px; height: 20px; } +.kpc-elo-bar-bg { flex: 1; height: 14px; background: rgba(255,255,255,0.1); border-radius: 7px; position: relative; overflow: hidden; } +.kpc-elo-bar-fill { height: 100%; background: linear-gradient(90deg, #388E3C, #4CAF50); border-radius: 7px; transition: width 0.3s; } +.kpc-elo-bar-text { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.5); } +#kpc-rank-list-btn { position: absolute; bottom: 8px; right: 8px; cursor: pointer; padding: 6px 14px; border-radius: 6px; font-size: 12px; background: rgba(76,175,80,0.3); color: #4CAF50; border: 1px solid rgba(76,175,80,0.4); z-index: 1; } +#kpc-rank-list-btn:hover { background: rgba(76,175,80,0.5); color: #fff; } +#kpc-rank-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.75); z-index: 9998; display: flex; justify-content: center; align-items: center; } +.kpc-rank-popup { background: #1a1a2e; border-radius: 12px; padding: 20px; min-width: 340px; max-width: 500px; box-shadow: 0 8px 32px rgba(0,0,0,0.6); } +.kpc-rank-popup-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } +.kpc-rank-popup-header h2 { margin: 0; color: #fff; font-size: 16px; } +.kpc-rank-popup-close { cursor: pointer; color: #888; font-size: 18px; padding: 4px 8px; } +.kpc-rank-popup-close:hover { color: #fff; } +.kpc-rank-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; max-height: 60vh; overflow-y: auto; } +.kpc-rank-grid-item { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: rgba(255,255,255,0.05); border-radius: 6px; } +.kpc-rank-grid-item img { width: 28px; height: 28px; } +.kpc-rank-name { font-size: 13px; font-weight: 600; } +.kpc-rank-elo { font-size: 11px; color: #888; } +`; + /** Pre-concatenated CSS for single-call injection (excludes HIDE_ADS_CSS which is separate) */ -export const ALL_CLIENT_CSS = `${CLIENT_SETTINGS_CSS}\n${MATCHMAKER_SETTINGS_CSS}\n${TRANSLATOR_CSS}\n${ALT_MANAGER_CSS}\n${HP_COUNTER_CSS}\n${BP_CLAIM_ALL_CSS}`; +export const ALL_CLIENT_CSS = `${CLIENT_SETTINGS_CSS}\n${MATCHMAKER_SETTINGS_CSS}\n${TRANSLATOR_CSS}\n${ALT_MANAGER_CSS}\n${HP_COUNTER_CSS}\n${BP_CLAIM_ALL_CSS}\n${RANK_TRACKER_CSS}`; diff --git a/src/preload/competitive.ts b/src/preload/competitive.ts index d463c0e..6a46fb9 100644 --- a/src/preload/competitive.ts +++ b/src/preload/competitive.ts @@ -1,5 +1,4 @@ -// ── Hardpoint Enemy Counter ── -// Displays enemy capture points being scored in Hardpoint mode. +// ── Competitive features: Hardpoint enemy counter + Rank progress tracker ── let hpObserver: MutationObserver | null = null; let hpCounterEl: HTMLElement | null = null; @@ -8,6 +7,8 @@ let hpEnemyOBJ = 0; let hpTimeout: ReturnType | null = null; let hpCheckInterval: ReturnType | null = null; +// ── Hardpoint Enemy Counter ── + function processTeamScores(): void { const teams = document.querySelectorAll('#tScoreC1, #tScoreC2'); for (const team of teams) { @@ -49,7 +50,7 @@ function setupHPDisplay(): void { } } -export function initHPCounter(): void { +function startHPCounter(): void { hpCheckInterval = setInterval(() => { if (document.querySelector('.cmpTmHed')) { if (hpCheckInterval) { clearInterval(hpCheckInterval); hpCheckInterval = null; } @@ -58,7 +59,7 @@ export function initHPCounter(): void { }, 2000); } -export function destroyHPCounter(): void { +function stopHPCounter(): void { if (hpCheckInterval) { clearInterval(hpCheckInterval); hpCheckInterval = null; } if (hpObserver) { hpObserver.disconnect(); hpObserver = null; } if (hpCounterEl) { hpCounterEl.remove(); hpCounterEl = null; } @@ -66,3 +67,158 @@ export function destroyHPCounter(): void { hpPointCounter = null; hpEnemyOBJ = 0; } + +export function initHPCounter(): void { startHPCounter(); } +export function destroyHPCounter(): void { stopHPCounter(); } + +export function setHPCounterEnabled(enabled: boolean): void { + stopHPCounter(); + if (enabled) startHPCounter(); +} + +// ── Rank Progress Tracker ── + +interface RankInfo { + rank: string; + elo: number | null; + color: string; + image: string; +} + +const RANKS: RankInfo[] = [ + { rank: 'Unranked', elo: null, color: '#FFFFFF', image: 'rank_unranked.svg' }, + { rank: 'Bronze 1', elo: 0, color: '#CD7F32', image: 'rank_bronze.svg' }, + { rank: 'Bronze 2', elo: 200, color: '#CD7F32', image: 'rank_bronze.svg' }, + { rank: 'Bronze 3', elo: 400, color: '#CD7F32', image: 'rank_bronze.svg' }, + { rank: 'Silver 1', elo: 700, color: '#C0C0C0', image: 'rank_silver.svg' }, + { rank: 'Silver 2', elo: 900, color: '#C0C0C0', image: 'rank_silver.svg' }, + { rank: 'Silver 3', elo: 1100, color: '#C0C0C0', image: 'rank_silver.svg' }, + { rank: 'Gold 1', elo: 1300, color: '#FFD700', image: 'rank_gold.svg' }, + { rank: 'Gold 2', elo: 1600, color: '#FFD700', image: 'rank_gold.svg' }, + { rank: 'Gold 3', elo: 2000, color: '#FFD700', image: 'rank_gold.svg' }, + { rank: 'Platinum', elo: 2300, color: '#4B69FF', image: 'rank_platinum.svg' }, + { rank: 'Diamond', elo: 3000, color: '#4B69FF', image: 'rank_diamond.svg' }, + { rank: 'Master', elo: 3300, color: '#EE7032', image: 'rank_master.svg' }, + { rank: 'Kracked', elo: 4700, color: '#FF0000', image: 'rank_kracked.svg' }, +]; + +const RANK_IMG_BASE = 'https://assets.krunker.io/img/ranked/ranks/'; + +function getRankData(currentElo: number): { current: RankInfo; next: RankInfo; progress: number; isMax: boolean } { + let idx = 0; + for (let i = RANKS.length - 1; i >= 0; i--) { + if (RANKS[i].elo !== null && currentElo >= RANKS[i].elo!) { idx = i; break; } + } + const current = RANKS[idx]; + const next = RANKS[idx + 1] || current; + const isMax = idx === RANKS.length - 1; + let progress = 0; + if (!isMax && current.elo !== null && next.elo !== null) { + progress = Math.min(100, Math.max(0, ((currentElo - current.elo!) / (next.elo! - current.elo!)) * 100)); + } else if (isMax) { + progress = 100; + } + return { current, next, progress, isMax }; +} + +function openRankPopup(): void { + if (document.getElementById('kpc-rank-overlay')) return; + const overlay = document.createElement('div'); + overlay.id = 'kpc-rank-overlay'; + overlay.addEventListener('mousedown', (e) => { if (e.target === overlay) overlay.remove(); }); + + let grid = ''; + for (const r of RANKS) { + grid += `
+ +
${r.rank}
+
${r.elo !== null ? r.elo + '+' : 'Placement'}
`; + } + + overlay.innerHTML = `
+

Rank Distribution

+
\u2715
+
${grid}
`; + document.body.appendChild(overlay); + document.getElementById('kpc-rank-close')?.addEventListener('click', () => overlay.remove()); +} + +function injectRankBar(container: Element): void { + if (container.querySelector('#kpc-elo-tracker')) return; + const statValues = container.querySelectorAll('.quick-stat-value'); + if (!statValues.length) return; + const currentElo = Number(statValues[0].textContent); + if (isNaN(currentElo)) return; + + const data = getRankData(currentElo); + const wrapper = document.createElement('div'); + wrapper.id = 'kpc-elo-tracker'; + + const nextHtml = data.isMax ? '' : + `
${data.next.rank}
`; + const barText = data.isMax ? `${currentElo}` : `${currentElo} / ${data.next.elo}`; + + wrapper.innerHTML = `
+
${data.current.rank}
+
+
${barText}
${nextHtml}
`; + + const statsBlock = container.querySelector('.quick-stats'); + if (statsBlock) container.insertBefore(wrapper, statsBlock); + else container.appendChild(wrapper); +} + +function injectRankButton(card: Element): void { + if (card.querySelector('#kpc-rank-list-btn')) return; + const btn = document.createElement('div'); + btn.id = 'kpc-rank-list-btn'; + btn.innerHTML = 'list Ranks'; + btn.addEventListener('click', openRankPopup); + if (getComputedStyle(card as HTMLElement).position === 'static') (card as HTMLElement).style.position = 'relative'; + card.appendChild(btn); +} + +function checkRankedMenu(): void { + const card = document.querySelector('.rank-card'); + const container = document.querySelector('.rank-and-stats'); + if (card && container) { + injectRankBar(container); + injectRankButton(card); + } +} + +export function initRankProgress(): void { + // Poll for window.openRankedMenu — Krunker defines it async after DOM load + let attempts = 0; + const poll = setInterval(() => { + const origRanked = (window as any).openRankedMenu; + if (origRanked && !origRanked.__kpcRankPatched) { + clearInterval(poll); + + let rankObserver: MutationObserver | null = null; + let cleanupInterval: ReturnType | null = null; + + const patched = function (this: any, ...args: any[]) { + origRanked.apply(this, args); + + const modal = document.querySelector('.rankedMenuModal'); + if (!modal) return; + + rankObserver = new MutationObserver(checkRankedMenu); + rankObserver.observe(modal, { childList: true, subtree: true }); + checkRankedMenu(); + + cleanupInterval = setInterval(() => { + if (!document.querySelector('.rankedMenuModal')) { + if (rankObserver) { rankObserver.disconnect(); rankObserver = null; } + if (cleanupInterval) { clearInterval(cleanupInterval); cleanupInterval = null; } + } + }, 5000); + }; + (patched as any).__kpcRankPatched = true; + (window as any).openRankedMenu = patched; + } else if (++attempts > 75) { // 15s timeout + clearInterval(poll); + } + }, 200); +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 427f277..0dae501 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -6,7 +6,7 @@ import type { UserscriptInstance } from './userscripts'; import { initTranslator, updateTranslatorConfig } from './translator'; import { setDeathAnimBlock, setCleanerMenu, setMenuTimer, escapeHtml } from './utils'; import { initChat, setBetterChat, setChatHistorySize } from './chat'; -import { initHPCounter, destroyHPCounter } from './competitive'; +import { initHPCounter, destroyHPCounter, initRankProgress } from './competitive'; import { checkChangelog } from './changelog'; import type { Keybind } from '../main/config'; @@ -1675,10 +1675,13 @@ ipcRenderer.on('main_did-finish-load', () => { }, _console); } - // ── Hardpoint enemy counter ── + // ── Competitive features ── if (isGamePage && (gameConf?.hpEnemyCounter ?? true)) { initHPCounter(); } + if (isGamePage) { + initRankProgress(); + } // ── CPU throttle state notifications ── if (isGamePage) {