Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e046cf19ec | |||
| 71aaee391f | |||
| c915fff113 | |||
| 193530c03e | |||
| 3fa3acc36f |
@@ -110,4 +110,17 @@ jobs:
|
|||||||
--repo "$GITHUB_REPOSITORY" \
|
--repo "$GITHUB_REPOSITORY" \
|
||||||
--title "${{ steps.version-check.outputs.TAG }}" \
|
--title "${{ steps.version-check.outputs.TAG }}" \
|
||||||
--notes-file /tmp/release-notes.md \
|
--notes-file /tmp/release-notes.md \
|
||||||
|
--draft=false \
|
||||||
|
--latest \
|
||||||
"${ASSETS[@]}"
|
"${ASSETS[@]}"
|
||||||
|
|
||||||
|
# Gitea mirror force-pushes tags which can reset releases to draft.
|
||||||
|
# Re-publish any drafts on every run to counteract this.
|
||||||
|
- name: Publish any draft releases
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
gh release list --repo "$GITHUB_REPOSITORY" --json tagName,isDraft --jq '.[] | select(.isDraft) | .tagName' | while read -r tag; do
|
||||||
|
echo "Publishing draft release: $tag"
|
||||||
|
gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --draft=false
|
||||||
|
done
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "krunker-civilian-client",
|
"name": "krunker-civilian-client",
|
||||||
"version": "0.6.3",
|
"version": "0.7.0",
|
||||||
"description": "Cross-platform Krunker game client",
|
"description": "Cross-platform Krunker game client",
|
||||||
"main": "dist/main/index.js",
|
"main": "dist/main/index.js",
|
||||||
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
|
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
|
||||||
|
|||||||
+42
-1
@@ -684,5 +684,46 @@ export const BP_CLAIM_ALL_CSS = `
|
|||||||
#claimAllBtn.disabled { opacity: 0.4; pointer-events: none; }
|
#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; }
|
||||||
|
|
||||||
|
/* Ranked queue button in ranked menu footer */
|
||||||
|
#kpc-ranked-queue-btn {
|
||||||
|
background-color: #5ce05a;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 9px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
#kpc-ranked-queue-btn:hover { background-color: #4bc94a; }
|
||||||
|
`;
|
||||||
|
|
||||||
/** Pre-concatenated CSS for single-call injection (excludes HIDE_ADS_CSS which is separate) */
|
/** 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}`;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { showUpdateWindow } from './update-window';
|
|||||||
import { DiscordRPC } from './discord-rpc';
|
import { DiscordRPC } from './discord-rpc';
|
||||||
import { listThemes, getThemeCSS, listLoadingThemes, getLoadingScreenCSS } from './css-themes';
|
import { listThemes, getThemeCSS, listLoadingThemes, getLoadingScreenCSS } from './css-themes';
|
||||||
import { TabManager } from './tab-manager';
|
import { TabManager } from './tab-manager';
|
||||||
|
import { openRankedQueue } from './ranked-queue';
|
||||||
|
|
||||||
// ── App version for API calls ──
|
// ── App version for API calls ──
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
@@ -651,6 +652,11 @@ async function launchApp(): Promise<void> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Ranked queue IPC handler ──
|
||||||
|
ipcMain.on('open-ranked-queue', (_e, token: string, region: string, allRegions: boolean) => {
|
||||||
|
openRankedQueue(token, region, allRegions);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Discord Rich Presence IPC handler ──
|
// ── Discord Rich Presence IPC handler ──
|
||||||
ipcMain.on('discord-update', (_e, activity: any) => {
|
ipcMain.on('discord-update', (_e, activity: any) => {
|
||||||
discordRpc?.setActivity(activity);
|
discordRpc?.setActivity(activity);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,516 @@
|
|||||||
|
import { BrowserWindow } from 'electron';
|
||||||
|
import { QUEUE_NOTIFICATION_AUDIO } from './ranked-queue-audio';
|
||||||
|
|
||||||
|
let queueWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
const RANKED_QUEUE_WS = 'wss://gamefrontend.svc.krunker.io/v1/matchmaking/queue';
|
||||||
|
|
||||||
|
const RANKED_MAPS: Record<string, { number: number; image: string }> = {
|
||||||
|
sandstorm_v3: { number: 2, image: 'https://assets.krunker.io/img/maps/map_2.png' },
|
||||||
|
undergrowth: { number: 4, image: 'https://assets.krunker.io/img/maps/map_4.png' },
|
||||||
|
industry: { number: 11, image: 'https://assets.krunker.io/img/maps/map_11.png' },
|
||||||
|
site: { number: 14, image: 'https://assets.krunker.io/img/maps/map_14.png' },
|
||||||
|
bureau: { number: 17, image: 'https://assets.krunker.io/img/maps/map_17.png' },
|
||||||
|
burg_new: { number: 0, image: 'https://assets.krunker.io/img/maps/map_0.png' },
|
||||||
|
eterno_sim: { number: 39, image: 'https://assets.krunker.io/img/maps/map_39.png' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const RANKED_REGIONS: Record<string, string> = {
|
||||||
|
na: 'North America',
|
||||||
|
eu: 'Europe',
|
||||||
|
as: 'Asia',
|
||||||
|
};
|
||||||
|
|
||||||
|
const QUEUE_CSS = `
|
||||||
|
* { user-select: none; margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: "Trebuchet MS", sans-serif;
|
||||||
|
background: #0d0d0d;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #e0e0e0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.queuer-container {
|
||||||
|
position: relative;
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 40px 52px;
|
||||||
|
max-width: 1000px;
|
||||||
|
width: 90vw;
|
||||||
|
border: 2px solid #2a2a2a;
|
||||||
|
border-top: 3px solid #06b6d4;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.main-content { display: flex; align-items: center; gap: 56px; }
|
||||||
|
.left-section { flex: 1; display: flex; flex-direction: column; gap: 24px; }
|
||||||
|
.status-area {
|
||||||
|
display: flex; align-items: center; gap: 14px;
|
||||||
|
position: relative; padding-left: 18px;
|
||||||
|
}
|
||||||
|
.status-area::before {
|
||||||
|
content: ""; position: absolute; left: 0;
|
||||||
|
width: 8px; height: 8px; background: #666;
|
||||||
|
border-radius: 50%; transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
.status-area.active::before {
|
||||||
|
background: #06b6d4;
|
||||||
|
box-shadow: 0 0 12px rgba(6, 182, 212, 0.6);
|
||||||
|
}
|
||||||
|
#queueStatus {
|
||||||
|
font-size: 14px; font-weight: 600; color: #666;
|
||||||
|
text-transform: uppercase; letter-spacing: 1px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
#queueStatus.active { color: #06b6d4; }
|
||||||
|
.timer-display {
|
||||||
|
font-size: 52px; font-weight: 700; color: #fff;
|
||||||
|
font-variant-numeric: tabular-nums; letter-spacing: 0.5px;
|
||||||
|
padding: 12px 16px; background: #222;
|
||||||
|
border-left: 3px solid #06b6d4; border-radius: 2px;
|
||||||
|
}
|
||||||
|
.region-controls { display: flex; gap: 12px; }
|
||||||
|
.region-option { position: relative; }
|
||||||
|
.region-option input { display: none; }
|
||||||
|
.region-option label {
|
||||||
|
display: block; padding: 12px 24px; background: #222;
|
||||||
|
border: 2px solid #2d2d2d; border-radius: 4px;
|
||||||
|
color: #888; font-size: 16px; font-weight: 600;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.5px;
|
||||||
|
cursor: pointer; transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.region-option label:hover { background: #2a2a2a; border-color: #3a3a3a; }
|
||||||
|
.region-option input:checked + label {
|
||||||
|
background: rgba(6, 182, 212, 0.1);
|
||||||
|
border-color: #06b6d4; color: #06b6d4;
|
||||||
|
}
|
||||||
|
.divider { width: 1px; height: 120px; background: #2a2a2a; }
|
||||||
|
.right-section { display: flex; flex-direction: column; gap: 14px; }
|
||||||
|
.btn {
|
||||||
|
padding: 16px 42px; border: 2px solid transparent;
|
||||||
|
font-size: 20px; font-weight: 600; cursor: pointer;
|
||||||
|
transition: all 0.2s ease; font-family: "Trebuchet MS", sans-serif;
|
||||||
|
text-transform: uppercase; letter-spacing: 1px; border-radius: 4px;
|
||||||
|
}
|
||||||
|
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
.btn-secondary { background: #222; color: #999; border-color: #2a2a2a; }
|
||||||
|
.btn-secondary:hover:not(:disabled) { background: #2a2a2a; border-color: #3a3a3a; }
|
||||||
|
.btn-primary { background: #06b6d4; color: #fff; border-color: #06b6d4; }
|
||||||
|
.btn-primary:hover:not(:disabled) { background: #0ea5ca; border-color: #0ea5ca; }
|
||||||
|
.btn-primary:active:not(:disabled) { transform: scale(0.98); }
|
||||||
|
.btn-primary.in-queue { background: #222; border-color: #06b6d4; color: #06b6d4; }
|
||||||
|
.btn-primary.in-queue:hover:not(:disabled) { background: rgba(6, 182, 212, 0.1); }
|
||||||
|
.overlay {
|
||||||
|
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.9); display: flex;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
opacity: 0; visibility: hidden; transition: all 0.3s ease; z-index: 1000;
|
||||||
|
}
|
||||||
|
.overlay.active { opacity: 1; visibility: visible; }
|
||||||
|
.popup {
|
||||||
|
background: #1a1a1a; border: 2px solid #2a2a2a;
|
||||||
|
border-top: 3px solid #06b6d4; max-width: 560px; width: 90vw;
|
||||||
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.8);
|
||||||
|
text-align: center; transform: scale(0.95);
|
||||||
|
transition: transform 0.3s ease; border-radius: 4px;
|
||||||
|
}
|
||||||
|
.overlay.active .popup { transform: scale(1); }
|
||||||
|
.popup h2 {
|
||||||
|
margin-top: 12px; font-size: 32px; font-weight: 700;
|
||||||
|
color: #06b6d4; text-transform: uppercase; letter-spacing: 1.5px;
|
||||||
|
}
|
||||||
|
.popup-content { margin: 20px 0; }
|
||||||
|
.popup-content p {
|
||||||
|
font-size: 15px; color: #888; margin-bottom: 12px;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;
|
||||||
|
}
|
||||||
|
.region-found {
|
||||||
|
font-size: 18px; font-weight: 700; color: #fff;
|
||||||
|
text-transform: uppercase; letter-spacing: 1px;
|
||||||
|
display: inline-block; padding: 12px 24px;
|
||||||
|
background: rgba(6, 182, 212, 0.15);
|
||||||
|
border: 2px solid #06b6d4; border-radius: 4px;
|
||||||
|
}
|
||||||
|
.countdown-large {
|
||||||
|
font-size: 48px; font-weight: 700; color: #06b6d4;
|
||||||
|
margin: 16px 0; font-variant-numeric: tabular-nums; line-height: 1;
|
||||||
|
}
|
||||||
|
#matchFoundMessage {
|
||||||
|
margin-top: 12px; font-size: 20px; color: #fff;
|
||||||
|
text-align: center; width: 400px; margin-left: auto; margin-right: auto;
|
||||||
|
}
|
||||||
|
#closeButton {
|
||||||
|
position: absolute; right: 0; top: 0;
|
||||||
|
margin: 10px 20px 0 0; font-size: 20px; cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function buildMapsJson(): string {
|
||||||
|
const entries: string[] = [];
|
||||||
|
for (const [name, data] of Object.entries(RANKED_MAPS)) {
|
||||||
|
entries.push(`${JSON.stringify(name)}: { number: ${data.number}, image: ${JSON.stringify(data.image)} }`);
|
||||||
|
}
|
||||||
|
return `{ ${entries.join(', ')} }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRegionsJson(): string {
|
||||||
|
return JSON.stringify(RANKED_REGIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRegionCheckboxes(): string {
|
||||||
|
return Object.entries(RANKED_REGIONS).map(([code, name]) => {
|
||||||
|
const inputId = code === 'as' ? 'asia' : code;
|
||||||
|
return `<div class="region-option"><input type="checkbox" id="${inputId}" value="${code}"><label for="${inputId}">${name === 'North America' ? 'NA' : name === 'Europe' ? 'EU' : 'Asia'}</label></div>`;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQueueScript(token: string, region: string, allRegions: boolean): string {
|
||||||
|
return `
|
||||||
|
let isQueued = false;
|
||||||
|
let queueStartTime = null;
|
||||||
|
let queueInterval = null;
|
||||||
|
let queueConnection = null;
|
||||||
|
let countdownInterval = null;
|
||||||
|
let isConnecting = false;
|
||||||
|
let audioContext = null;
|
||||||
|
let notificationBuffer = null;
|
||||||
|
let currentSource = null;
|
||||||
|
let audioInitialized = false;
|
||||||
|
const selectedMaps = new Set();
|
||||||
|
|
||||||
|
const WS_URL = ${JSON.stringify(RANKED_QUEUE_WS)};
|
||||||
|
const INIT_TOKEN = ${JSON.stringify(token)};
|
||||||
|
const INIT_REGION = ${JSON.stringify(region)};
|
||||||
|
const INIT_ALL_REGIONS = ${JSON.stringify(allRegions)};
|
||||||
|
const maps = ${buildMapsJson()};
|
||||||
|
const regions = ${buildRegionsJson()};
|
||||||
|
|
||||||
|
const queueStatus = document.getElementById('queueStatus');
|
||||||
|
const statusArea = document.getElementById('statusArea');
|
||||||
|
const queueTimerDisplay = document.getElementById('queueTimerDisplay');
|
||||||
|
const regionCheckboxes = document.getElementById('regionCheckboxes');
|
||||||
|
const matchPopupOverlay = document.getElementById('matchPopupOverlay');
|
||||||
|
const countdownTimer = document.getElementById('countDownTimer');
|
||||||
|
const foundRegion = document.getElementById('foundRegion');
|
||||||
|
const queueButton = document.getElementById('queueButton');
|
||||||
|
const closeButton = document.getElementById('closeButton');
|
||||||
|
|
||||||
|
const base64String = ${JSON.stringify(QUEUE_NOTIFICATION_AUDIO)};
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
const selectedRegions = Array.from(document.querySelectorAll('#regionCheckboxes input:checked')).map(el => el.value);
|
||||||
|
localStorage.setItem('queue_selectedRegions', JSON.stringify(selectedRegions));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
const savedRegions = localStorage.getItem('queue_selectedRegions');
|
||||||
|
if (savedRegions) {
|
||||||
|
for (const regionId of JSON.parse(savedRegions)) {
|
||||||
|
const checkbox = document.getElementById(regionId === 'as' ? 'asia' : regionId);
|
||||||
|
if (checkbox) checkbox.checked = true;
|
||||||
|
}
|
||||||
|
} else if (INIT_REGION) {
|
||||||
|
const checkbox = document.getElementById(INIT_REGION === 'as' ? 'asia' : INIT_REGION);
|
||||||
|
if (checkbox) checkbox.checked = true;
|
||||||
|
if (INIT_ALL_REGIONS) {
|
||||||
|
for (const el of document.querySelectorAll('#regionCheckboxes input')) el.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64ToArrayBuffer = (b64) => {
|
||||||
|
const bin = atob(b64);
|
||||||
|
const bytes = new Uint8Array(bin.length);
|
||||||
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||||
|
return bytes.buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function initializeAudio() {
|
||||||
|
audioContext = new AudioContext();
|
||||||
|
if (audioContext.state === 'suspended') await audioContext.resume();
|
||||||
|
const arrayBuffer = base64ToArrayBuffer(base64String);
|
||||||
|
notificationBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||||
|
audioInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function playNotificationSound() {
|
||||||
|
if (!notificationBuffer || !audioContext) return;
|
||||||
|
try {
|
||||||
|
const source = audioContext.createBufferSource();
|
||||||
|
source.buffer = notificationBuffer;
|
||||||
|
source.connect(audioContext.destination);
|
||||||
|
source.start(0);
|
||||||
|
currentSource = source;
|
||||||
|
} catch (e) { console.error('Audio play error:', e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopNotificationSound() {
|
||||||
|
if (currentSource) {
|
||||||
|
try { currentSource.stop(); currentSource.disconnect(); } catch {}
|
||||||
|
currentSource = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds) {
|
||||||
|
const h = String(Math.floor(seconds / 3600)).padStart(2, '0');
|
||||||
|
const m = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
|
||||||
|
const s = String(Math.floor(seconds % 60)).padStart(2, '0');
|
||||||
|
return h + ':' + m + ':' + s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCooldownTimer(ms) {
|
||||||
|
const endTime = Date.now() + ms;
|
||||||
|
function updateDisplay() {
|
||||||
|
const remaining = Math.ceil((endTime - Date.now()) / 1000);
|
||||||
|
if (remaining <= 0) {
|
||||||
|
queueStatus.textContent = 'Ready';
|
||||||
|
queueStatus.classList.remove('active');
|
||||||
|
statusArea.classList.remove('active');
|
||||||
|
queueButton.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queueStatus.textContent = 'Cooldown: ' + formatTime(remaining);
|
||||||
|
queueButton.disabled = true;
|
||||||
|
setTimeout(updateDisplay, 1000);
|
||||||
|
}
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQueueTimer() {
|
||||||
|
if (queueStartTime) {
|
||||||
|
const elapsed = Math.floor((Date.now() - queueStartTime) / 1000);
|
||||||
|
queueTimerDisplay.textContent = formatTime(elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startQueue() {
|
||||||
|
const selectedRegions = Array.from(document.querySelectorAll('#regionCheckboxes input:checked')).map(el => el.value);
|
||||||
|
|
||||||
|
if (selectedRegions.length === 0) {
|
||||||
|
queueStatus.textContent = 'Select at least one region';
|
||||||
|
queueButton.disabled = false;
|
||||||
|
isConnecting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedMaps.size === 0) {
|
||||||
|
queueStatus.textContent = 'Select at least one map';
|
||||||
|
queueButton.disabled = false;
|
||||||
|
isConnecting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsUrl = WS_URL + '?token=' + INIT_TOKEN + '&maps=' + Array.from(selectedMaps).join(',') + '®ions=' + selectedRegions.join(',');
|
||||||
|
|
||||||
|
try {
|
||||||
|
queueConnection = new WebSocket(wsUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebSocket creation error:', error);
|
||||||
|
queueStatus.textContent = 'Connection failed';
|
||||||
|
queueButton.disabled = false;
|
||||||
|
isConnecting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueConnection.onerror = (error) => {
|
||||||
|
console.error('queueConnection error:', error);
|
||||||
|
queueStatus.textContent = 'Connection error';
|
||||||
|
queueStatus.classList.remove('active');
|
||||||
|
statusArea.classList.remove('active');
|
||||||
|
isQueued = false;
|
||||||
|
isConnecting = false;
|
||||||
|
queueButton.disabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
queueConnection.onopen = () => {
|
||||||
|
isQueued = true;
|
||||||
|
isConnecting = false;
|
||||||
|
queueStartTime = Date.now();
|
||||||
|
queueButton.textContent = 'Leave Queue';
|
||||||
|
queueButton.classList.add('in-queue');
|
||||||
|
queueStatus.textContent = 'In queue';
|
||||||
|
queueStatus.classList.add('active');
|
||||||
|
statusArea.classList.add('active');
|
||||||
|
updateQueueTimer();
|
||||||
|
queueInterval = setInterval(updateQueueTimer, 1000);
|
||||||
|
queueButton.disabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
queueConnection.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
switch (data.type) {
|
||||||
|
case 'QUEUE_STATUS':
|
||||||
|
if (data.payload.status === 'MATCHED')
|
||||||
|
matchFound(data.payload.assignment.extensions.map.trim(), data.payload.assignment.extensions.region);
|
||||||
|
break;
|
||||||
|
case 'ERROR':
|
||||||
|
if (data.payload.code === 'COOLDOWN') {
|
||||||
|
queueConnection.close();
|
||||||
|
isQueued = false;
|
||||||
|
isConnecting = false;
|
||||||
|
queueButton.disabled = false;
|
||||||
|
updateCooldownTimer(data.payload.payload.cooldown);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'INTERNAL_ERROR':
|
||||||
|
queueConnection.close();
|
||||||
|
isQueued = false;
|
||||||
|
isConnecting = false;
|
||||||
|
queueButton.disabled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
queueConnection.onclose = () => {
|
||||||
|
isQueued = false;
|
||||||
|
isConnecting = false;
|
||||||
|
clearInterval(queueInterval);
|
||||||
|
queueButton.textContent = 'Start Queue';
|
||||||
|
queueButton.classList.remove('in-queue');
|
||||||
|
queueStatus.textContent = 'Ready';
|
||||||
|
queueStatus.classList.remove('active');
|
||||||
|
statusArea.classList.remove('active');
|
||||||
|
queueTimerDisplay.textContent = '00:00:00';
|
||||||
|
queueButton.disabled = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchFound(map, region) {
|
||||||
|
playNotificationSound();
|
||||||
|
matchPopupOverlay.classList.add('active');
|
||||||
|
region = region.slice(2);
|
||||||
|
const regionName = regions[region] || region;
|
||||||
|
let foundMapName = 'unknown';
|
||||||
|
for (const [mapName, mapData] of Object.entries(maps)) {
|
||||||
|
if (mapData.number === parseInt(map, 10)) { foundMapName = mapName; break; }
|
||||||
|
}
|
||||||
|
foundRegion.textContent = regionName + ', ' + foundMapName;
|
||||||
|
|
||||||
|
const duration = 60;
|
||||||
|
const startTime = Date.now();
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||||
|
const remaining = Math.max(0, duration - elapsed);
|
||||||
|
countdownTimer.textContent = formatTime(remaining);
|
||||||
|
if (remaining <= 0) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
matchPopupOverlay.classList.remove('active');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
isQueued = false;
|
||||||
|
isConnecting = false;
|
||||||
|
clearInterval(queueInterval);
|
||||||
|
queueButton.textContent = 'Start Queue';
|
||||||
|
queueButton.classList.remove('in-queue');
|
||||||
|
queueStatus.textContent = 'Ready';
|
||||||
|
queueStatus.classList.remove('active');
|
||||||
|
statusArea.classList.remove('active');
|
||||||
|
queueTimerDisplay.textContent = '00:00:00';
|
||||||
|
queueButton.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue button
|
||||||
|
queueButton.onclick = async () => {
|
||||||
|
if (isConnecting) return;
|
||||||
|
queueButton.disabled = true;
|
||||||
|
isConnecting = true;
|
||||||
|
if (isQueued) {
|
||||||
|
queueConnection.close();
|
||||||
|
} else {
|
||||||
|
if (!audioInitialized) await initializeAudio();
|
||||||
|
startQueue();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close match popup
|
||||||
|
closeButton.onclick = () => {
|
||||||
|
matchPopupOverlay.classList.remove('active');
|
||||||
|
stopNotificationSound();
|
||||||
|
if (countdownInterval) clearInterval(countdownInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Region checkbox changes
|
||||||
|
for (const sel of regionCheckboxes.querySelectorAll('input')) {
|
||||||
|
sel.onclick = () => {
|
||||||
|
if (isQueued && queueConnection) queueConnection.close();
|
||||||
|
saveSettings();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init — select all maps unconditionally (ranked doesn't allow map choice)
|
||||||
|
for (const data of Object.values(maps)) selectedMaps.add(data.number);
|
||||||
|
loadSettings();
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQueueHtml(token: string, region: string, allRegions: boolean): string {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Ranked Queue</title>
|
||||||
|
<style>${QUEUE_CSS}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="queuer-container">
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="left-section">
|
||||||
|
<div class="status-area" id="statusArea">
|
||||||
|
<span id="queueStatus">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="timer-display" id="queueTimerDisplay">00:00:00</div>
|
||||||
|
<div class="region-controls" id="regionCheckboxes">
|
||||||
|
${buildRegionCheckboxes()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="right-section">
|
||||||
|
<button type="button" class="btn btn-primary" id="queueButton">Start Queue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overlay" id="matchPopupOverlay">
|
||||||
|
<div class="popup">
|
||||||
|
<h2>Match Found</h2>
|
||||||
|
<div class="popup-content">
|
||||||
|
<div id="closeButton">X</div>
|
||||||
|
<div class="region-found" id="foundRegion">Region: </div>
|
||||||
|
<div id="matchFoundMessage">open the client and rejoin the game from the ranked menu</div>
|
||||||
|
<div class="countdown-large" id="countDownTimer">00:00:60</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>${buildQueueScript(token, region, allRegions)}</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openRankedQueue(token: string, region: string, allRegions: boolean): void {
|
||||||
|
if (queueWindow && !queueWindow.isDestroyed()) {
|
||||||
|
queueWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 850,
|
||||||
|
height: 350,
|
||||||
|
resizable: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
backgroundColor: '#0d0d0d',
|
||||||
|
title: 'Ranked Queue',
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
sandbox: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
win.removeMenu();
|
||||||
|
|
||||||
|
queueWindow = win;
|
||||||
|
win.on('closed', () => { queueWindow = null; });
|
||||||
|
|
||||||
|
const html = buildQueueHtml(token, region, allRegions);
|
||||||
|
win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
|
||||||
|
}
|
||||||
+194
-4
@@ -1,5 +1,5 @@
|
|||||||
// ── Hardpoint Enemy Counter ──
|
// ── Competitive features: Hardpoint enemy counter + Rank progress tracker + Ranked queue ──
|
||||||
// Displays enemy capture points being scored in Hardpoint mode.
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
let hpObserver: MutationObserver | null = null;
|
let hpObserver: MutationObserver | null = null;
|
||||||
let hpCounterEl: HTMLElement | null = null;
|
let hpCounterEl: HTMLElement | null = null;
|
||||||
@@ -8,6 +8,8 @@ let hpEnemyOBJ = 0;
|
|||||||
let hpTimeout: ReturnType<typeof setTimeout> | null = null;
|
let hpTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
let hpCheckInterval: ReturnType<typeof setInterval> | null = null;
|
let hpCheckInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
// ── Hardpoint Enemy Counter ──
|
||||||
|
|
||||||
function processTeamScores(): void {
|
function processTeamScores(): void {
|
||||||
const teams = document.querySelectorAll('#tScoreC1, #tScoreC2');
|
const teams = document.querySelectorAll('#tScoreC1, #tScoreC2');
|
||||||
for (const team of teams) {
|
for (const team of teams) {
|
||||||
@@ -49,7 +51,7 @@ function setupHPDisplay(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initHPCounter(): void {
|
function startHPCounter(): void {
|
||||||
hpCheckInterval = setInterval(() => {
|
hpCheckInterval = setInterval(() => {
|
||||||
if (document.querySelector('.cmpTmHed')) {
|
if (document.querySelector('.cmpTmHed')) {
|
||||||
if (hpCheckInterval) { clearInterval(hpCheckInterval); hpCheckInterval = null; }
|
if (hpCheckInterval) { clearInterval(hpCheckInterval); hpCheckInterval = null; }
|
||||||
@@ -58,7 +60,7 @@ export function initHPCounter(): void {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function destroyHPCounter(): void {
|
function stopHPCounter(): void {
|
||||||
if (hpCheckInterval) { clearInterval(hpCheckInterval); hpCheckInterval = null; }
|
if (hpCheckInterval) { clearInterval(hpCheckInterval); hpCheckInterval = null; }
|
||||||
if (hpObserver) { hpObserver.disconnect(); hpObserver = null; }
|
if (hpObserver) { hpObserver.disconnect(); hpObserver = null; }
|
||||||
if (hpCounterEl) { hpCounterEl.remove(); hpCounterEl = null; }
|
if (hpCounterEl) { hpCounterEl.remove(); hpCounterEl = null; }
|
||||||
@@ -66,3 +68,191 @@ export function destroyHPCounter(): void {
|
|||||||
hpPointCounter = null;
|
hpPointCounter = null;
|
||||||
hpEnemyOBJ = 0;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ranked Queue Button ──
|
||||||
|
|
||||||
|
function injectQueueButton(): void {
|
||||||
|
const footer = document.querySelector('.footer-controls');
|
||||||
|
if (!footer || footer.querySelector('#kpc-ranked-queue-btn')) return;
|
||||||
|
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.id = 'kpc-ranked-queue-btn';
|
||||||
|
btn.className = 'kpc-ranked-queue-btn';
|
||||||
|
btn.innerHTML = '<span class="material-icons" style="font-size:20px;vertical-align:middle;">open_in_new</span>';
|
||||||
|
btn.title = 'Open External Queue';
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
let token = localStorage.getItem('__FRVR_auth_access_token') || '';
|
||||||
|
token = token.replace(/"/g, '').replace(/\//g, '');
|
||||||
|
const regionEl = document.querySelector('.region-indicator');
|
||||||
|
let region = 'na';
|
||||||
|
if (regionEl) {
|
||||||
|
const text = regionEl.textContent || '';
|
||||||
|
const parts = text.split(': ');
|
||||||
|
const regionName = parts[1] || parts[0];
|
||||||
|
if (regionName.includes('Europe')) region = 'eu';
|
||||||
|
else if (regionName.includes('Asia')) region = 'as';
|
||||||
|
}
|
||||||
|
const allRegions = localStorage.getItem('s_rankedAllRegions') === 'true';
|
||||||
|
ipcRenderer.send('open-ranked-queue', token, region, allRegions);
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastChild = footer.lastElementChild;
|
||||||
|
if (lastChild) footer.insertBefore(btn, lastChild);
|
||||||
|
else footer.appendChild(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
injectQueueButton();
|
||||||
|
|
||||||
|
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 { initTranslator, updateTranslatorConfig } from './translator';
|
||||||
import { setDeathAnimBlock, setCleanerMenu, setMenuTimer, escapeHtml } from './utils';
|
import { setDeathAnimBlock, setCleanerMenu, setMenuTimer, escapeHtml } from './utils';
|
||||||
import { initChat, setBetterChat, setChatHistorySize } from './chat';
|
import { initChat, setBetterChat, setChatHistorySize } from './chat';
|
||||||
import { initHPCounter, destroyHPCounter } from './competitive';
|
import { initHPCounter, destroyHPCounter, initRankProgress } from './competitive';
|
||||||
import { checkChangelog } from './changelog';
|
import { checkChangelog } from './changelog';
|
||||||
import type { Keybind } from '../main/config';
|
import type { Keybind } from '../main/config';
|
||||||
|
|
||||||
@@ -1675,10 +1675,13 @@ ipcRenderer.on('main_did-finish-load', () => {
|
|||||||
}, _console);
|
}, _console);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hardpoint enemy counter ──
|
// ── Competitive features ──
|
||||||
if (isGamePage && (gameConf?.hpEnemyCounter ?? true)) {
|
if (isGamePage && (gameConf?.hpEnemyCounter ?? true)) {
|
||||||
initHPCounter();
|
initHPCounter();
|
||||||
}
|
}
|
||||||
|
if (isGamePage) {
|
||||||
|
initRankProgress();
|
||||||
|
}
|
||||||
|
|
||||||
// ── CPU throttle state notifications ──
|
// ── CPU throttle state notifications ──
|
||||||
if (isGamePage) {
|
if (isGamePage) {
|
||||||
|
|||||||
Reference in New Issue
Block a user