feat: add rank progress tracker and rank distribution popup
Build and Release / build-and-release (push) Failing after 3m58s

This commit is contained in:
2026-04-04 09:24:01 -07:00
parent 3fa3acc36f
commit 193530c03e
3 changed files with 190 additions and 7 deletions
+25 -1
View File
@@ -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}`;
+160 -4
View File
@@ -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<typeof setTimeout> | null = null;
let hpCheckInterval: ReturnType<typeof setInterval> | 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 += `<div class="kpc-rank-grid-item">
<img src="${RANK_IMG_BASE}${r.image}" loading="lazy">
<div><div class="kpc-rank-name" style="color:${r.color}">${r.rank}</div>
<div class="kpc-rank-elo">${r.elo !== null ? r.elo + '+' : 'Placement'}</div></div></div>`;
}
overlay.innerHTML = `<div class="kpc-rank-popup">
<div class="kpc-rank-popup-header"><h2>Rank Distribution</h2>
<div class="kpc-rank-popup-close" id="kpc-rank-close">\u2715</div></div>
<div class="kpc-rank-grid">${grid}</div></div>`;
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 ? '' :
`<div class="kpc-rank-container"><img src="${RANK_IMG_BASE}${data.next.image}" class="kpc-elo-rank-img"><span>${data.next.rank}</span></div>`;
const barText = data.isMax ? `${currentElo}` : `${currentElo} / ${data.next.elo}`;
wrapper.innerHTML = `<div class="kpc-elo-info-row">
<div class="kpc-rank-container"><img src="${RANK_IMG_BASE}${data.current.image}" class="kpc-elo-rank-img"><span>${data.current.rank}</span></div>
<div class="kpc-elo-bar-bg"><div class="kpc-elo-bar-fill" style="width:${data.progress}%"></div>
<div class="kpc-elo-bar-text">${barText}</div></div>${nextHtml}</div>`;
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 = '<span class="material-icons" style="font-size:16px;vertical-align:middle;margin-right:4px;">list</span> 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<typeof setInterval> | 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);
}
+5 -2
View File
@@ -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) {