feat: add rank progress tracker and rank distribution popup
Build and Release / build-and-release (push) Failing after 3m58s
Build and Release / build-and-release (push) Failing after 3m58s
This commit is contained in:
+25
-1
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user