Greasy Fork is available in English.
KeyMod for RocketGoal.io — full mod suite by @keydopz
// ==UserScript==
// @name KeyMod [BETA]
// @namespace https://rocketgoal.io
// @version 0.1.1
// @description KeyMod for RocketGoal.io — full mod suite by @keydopz
// @match *://rocketgoal.io/*
// @match *://www.rocketgoal.io/*
// @match *://www.crazygames.com/game/rocketgoal-io/*
// @match *://rocketgoalio.com/*
// @match *://hotgames.io/rocket-goal*
// @match *://taproad.io/rocket-goal*
// @match *://sites.google.com/view/classroom6x/rocket-goal*
// @grant none
// @run-at document-start
// @license CC BY-NC-ND 4.0
// ==/UserScript==
(function () {
'use strict';
// ─── KEYBOARD INTERCEPT ───────────────────────────────────────────────────
// Runs immediately at document-start before Unity registers any listeners.
// We wrap EventTarget.prototype.addEventListener so every keydown handler
// Unity adds goes through our wrapper first — giving us F2/backtick/1.
// Only fire on keydown (not keyup) and only when NOT held (repeat=false)
var _kmKeyLock = false;
function kmLog(msg) {
var entry = { level:'hi', line:'[KeyMod] ' + msg };
consoleLogs.push(entry);
if (consoleLogs.length > 500) consoleLogs.shift();
appendConsoleRow(entry);
_log('[KeyMod] ' + msg);
}
(function() {
var _ael = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, fn, opts) {
if ((type === 'keydown' || type === 'keyup') && typeof fn === 'function') {
var _fn = fn;
var wrapped = function(e) {
if (e.key === 'F2') {
e.preventDefault();
e.stopImmediatePropagation();
// keydown fires once, keyup resets lock
if (type === 'keydown' && !e.repeat && !_kmKeyLock) {
_kmKeyLock = true;
if (typeof toggleMenu === 'function') toggleMenu();
}
if (type === 'keyup') _kmKeyLock = false;
return;
}
return _fn.apply(this, arguments);
};
return _ael.call(this, type, wrapped, opts);
}
return _ael.call(this, type, fn, opts);
};
})();
// ─── RANKS ────────────────────────────────────────────────────────────────
var RANKS = [
{ name:'Bronze I', min:0, color:'#cd7f32', emoji:'🟤' },
{ name:'Bronze II', min:175, color:'#cd7f32', emoji:'🟤' },
{ name:'Bronze III', min:225, color:'#cd7f32', emoji:'🟤' },
{ name:'Silver I', min:275, color:'#aaaaaa', emoji:'⚪' },
{ name:'Silver II', min:350, color:'#aaaaaa', emoji:'⚪' },
{ name:'Silver III', min:425, color:'#aaaaaa', emoji:'⚪' },
{ name:'Gold I', min:475, color:'#f5c518', emoji:'🟡' },
{ name:'Gold II', min:550, color:'#f5c518', emoji:'🟡' },
{ name:'Gold III', min:625, color:'#f5c518', emoji:'🟡' },
{ name:'Platinum I', min:700, color:'#22d3ee', emoji:'🔵' },
{ name:'Platinum II', min:775, color:'#22d3ee', emoji:'🔵' },
{ name:'Platinum III', min:850, color:'#22d3ee', emoji:'🔵' },
{ name:'Diamond I', min:925, color:'#818cf8', emoji:'💎' },
{ name:'Diamond II', min:1000, color:'#818cf8', emoji:'💎' },
{ name:'Diamond III', min:1075, color:'#818cf8', emoji:'💎' },
{ name:'Champion I', min:1150, color:'#c084fc', emoji:'🟣' },
{ name:'Champion II', min:1225, color:'#c084fc', emoji:'🟣' },
{ name:'Champion III', min:1300, color:'#c084fc', emoji:'🟣' },
{ name:'Grand Champ I', min:1375, color:'#f97316', emoji:'🔶' },
{ name:'Grand Champ II', min:1450, color:'#f97316', emoji:'🔶' },
{ name:'Grand Champ III', min:1525, color:'#f97316', emoji:'🔶' },
{ name:'SSL', min:1600, color:'#ff0055', emoji:'🔴' },
];
function getRank(mmr) {
for (var i = RANKS.length-1; i >= 0; i--) { if (mmr >= RANKS[i].min) return RANKS[i]; }
return RANKS[0];
}
function getRankProgress(mmr) {
var cur = getRank(mmr), idx = RANKS.indexOf(cur);
if (idx >= RANKS.length-1) return 100;
var next = RANKS[idx+1];
return Math.round(((mmr - cur.min) / (next.min - cur.min)) * 100);
}
// ─── CONSTANTS ────────────────────────────────────────────────────────────
var SKINS = {
'body.0': 'Vortex',
'body.1': 'Overdrive',
'body.2': 'Crimson',
'body.3': 'Specter',
'body.4': 'Frostbite',
'body.5': 'Pulsewave',
'body.6': 'Blaze',
'body.7': 'Nitron',
};
var GOAL_SOUNDS = {
'None': null
};
// Tab list is encoded in HTML data-tab attributes
// ─── STORAGE ──────────────────────────────────────────────────────────────
function lsGet(k,d) {
try { var v=localStorage.getItem('km5_'+k); return v!==null?JSON.parse(v):d; } catch(e){return d;}
}
function lsSet(k,v) { try{localStorage.setItem('km5_'+k,JSON.stringify(v));}catch(e){} }
// ─── THEME ────────────────────────────────────────────────────────────────
var THEMES = {
arch: { name:'Arch Linux', bg:'rgba(10,10,10,0.97)', tabBg:'rgba(15,15,15,0.99)', blur:'none', border:'rgba(23,147,209,0.3)', accent:'#1793d1', text:'#d0d0d0' },
dark: { name:'Default', bg:'rgba(8,10,16,0.96)', tabBg:'rgba(12,14,20,0.98)', blur:'none', border:'rgba(255,255,255,0.08)', accent:'#3b82f6', text:'#e2e8f0' },
glass: { name:'Liquid Glass', bg:'rgba(15,20,40,0.45)', tabBg:'rgba(20,25,45,0.55)', blur:'blur(32px) saturate(200%)', border:'rgba(255,255,255,0.18)', accent:'#60a5fa', text:'#f0f4ff' },
aero: { name:'Futuristic Aero', bg:'rgba(0,20,40,0.72)', tabBg:'rgba(0,15,35,0.82)', blur:'blur(24px) saturate(180%)', border:'rgba(100,200,255,0.25)', accent:'#00d4ff', text:'#e0f4ff' },
black: { name:'All Black', bg:'rgba(0,0,0,0.97)', tabBg:'rgba(5,5,5,0.99)', blur:'none', border:'rgba(255,255,255,0.06)', accent:'#3b82f6', text:'#c8d0dc' },
windows: { name:'Windows 11', bg:'rgba(32,32,32,0.92)', tabBg:'rgba(44,44,44,0.95)', blur:'blur(20px) saturate(150%)', border:'rgba(255,255,255,0.12)', accent:'#0078d4', text:'#ffffff' },
minimal: { name:'Minimal', bg:'rgba(6,6,8,0.98)', tabBg:'rgba(9,9,12,0.99)', blur:'none', border:'rgba(255,255,255,0.05)', accent:'#ffffff', text:'#888888', minimal:true },
gta: { name:'GTA V', bg:'rgba(0,0,0,0.97)', tabBg:'rgba(5,8,5,0.99)', blur:'none', border:'rgba(255,200,0,0.25)', accent:'#f5c518', text:'#c8c8a0', gta:true },
rl: { name:'Rocket League', bg:'rgba(4,8,20,0.97)', tabBg:'rgba(6,12,28,0.99)', blur:'none', border:'rgba(0,150,255,0.25)', accent:'#0096ff', text:'#d0e8ff', rl:true },
esports: { name:'Pro Esports', bg:'rgba(2,2,6,0.98)', tabBg:'rgba(4,4,10,0.99)', blur:'none', border:'rgba(255,60,0,0.3)', accent:'#ff3c00', text:'#ffffff', esports:true },
faze: { name:'FaZe Clan', bg:'rgba(6,0,0,0.98)', tabBg:'rgba(10,2,2,0.99)', blur:'none', border:'rgba(210,0,0,0.4)', accent:'#d40000', text:'#ffffff' },
nrg: { name:'NRG', bg:'rgba(0,4,12,0.98)', tabBg:'rgba(0,6,18,0.99)', blur:'none', border:'rgba(0,168,255,0.4)', accent:'#00a8ff', text:'#e8f4ff' },
vitality:{ name:'Team Vitality', bg:'rgba(4,4,0,0.98)', tabBg:'rgba(8,8,0,0.99)', blur:'none', border:'rgba(255,220,0,0.4)', accent:'#ffdc00', text:'#fffff0' },
kcorp: { name:'Karmine Corp', bg:'rgba(0,2,12,0.98)', tabBg:'rgba(0,3,18,0.99)', blur:'none', border:'rgba(0,80,255,0.4)', accent:'#0050ff', text:'#e0eaff' },
bakkesmod:{ name:'BakkesMod', bg:'rgba(18,18,18,0.99)', tabBg:'rgba(22,22,22,1.0)', blur:'none', border:'rgba(0,110,255,0.25)', accent:'#006eff', text:'#d4d4d4', bakkesmod:true },
macos14: { name:'macOS Sonoma', bg:'rgba(28,28,30,0.88)', tabBg:'rgba(36,36,38,0.92)', blur:'blur(40px) saturate(180%)', border:'rgba(255,255,255,0.14)', accent:'#0a84ff', text:'#f2f2f7', macos14:true },
};
var theme = lsGet('theme5', {
preset: 'bakkesmod',
accent: '#006eff',
text: '#d4d4d4',
opacity: 0.99,
loading: 'black',
});
function getPreset() { return THEMES[theme.preset] || THEMES.dark; }
// ─── STATE ────────────────────────────────────────────────────────────────
var playerData = null;
var consoleLogs = [];
var menuOpen = false;
var activeTab = 'ranks';
var dismissed = false;
var lastPing = null;
var authToken = null;
var inMatch = false;
var matchEndProcessed = false;
var matchPlayers = { me:null, meId:null, opponent:null, opponentId:null, myTeam:null, oppTeam:null };
var matchScore = { me:0, opp:0 };
var trophyRange = { lo:null, hi:null };
var opponentMMR = null;
var currentMode = null;
var lastOpponentId = null;
// Session — pure memory
var session = {
goals:0, conceded:0, saves:0, shots:0,
matches:0, wins:0, losses:0,
streak:0, maxStreak:0, comebacks:0,
perfectGames:0, xpGained:0,
startTime:Date.now(), wasLosing:false,
longestSession: lsGet('longestSession',0),
lastMode: null,
};
// Daily
var today = new Date().toDateString();
var dailyStored = lsGet('daily5',{date:'',wins:0,goal:5});
if (dailyStored.date !== today) dailyStored = {date:today,wins:0,goal:lsGet('dailyGoal5',5)};
var daily = dailyStored;
// Persistent data
var peakMMR = lsGet('peakMMR5',{});
var matchHistory = lsGet('matchHistory5',[]); // max 50
var rankHistory = lsGet('rankHistory5',[]); // rank up/down events
var opponentLog = lsGet('opponentLog5',[]); // full opponent history
var rivals = lsGet('rivals5',{}); // name -> { wins, losses, count }
var xpHistory = lsGet('xpHistory5',[]); // { time, gained, total }
var heatmap = lsGet('heatmap5',{});
var clipMarks = [];
var selectedGoalSound = 'None';
// Toggles
var toggles = lsGet('toggles5',{
leaderboard:true, goalMsg:true, endMsg:true,
oppMMR:true, matchNotifs:true, hotStreak:true,
coldStreak:true, rivalNotif:true, perfectNotif:true,
rankNotif:true,
});
// Ensure defaults for older saved settings
toggles.leaderboard = toggles.leaderboard === false ? false : true;
toggles.goalMsg = toggles.goalMsg === false ? false : true;
toggles.endMsg = toggles.endMsg === false ? false : true;
toggles.oppMMR = toggles.oppMMR === false ? false : true;
toggles.matchNotifs = toggles.matchNotifs === false ? false : true;
toggles.hotStreak = toggles.hotStreak === false ? false : true;
toggles.coldStreak = toggles.coldStreak === false ? false : true;
toggles.rivalNotif = toggles.rivalNotif === false ? false : true;
toggles.perfectNotif = toggles.perfectNotif === false ? false : true;
toggles.rankNotif = toggles.rankNotif === false ? false : true;
// Custom messages
var customMsgs = lsGet('customMsgs5',{goal:'GOAL!',win:'GG EZ',loss:'GG'});
// Party code
var currentPartyCode = null;
// Tips
var TIPS = [
'Tip: Press F2 to toggle the KeyMod menu anytime',
'Tip: Install plugins from the Plugin Store tab',
'Tip: Your MMR is tracked after every match automatically',
'Tip: Click the Discord button in the header to copy your party invite',
'Tip: Set a daily win goal in the Competitive tab',
'Tip: Rival players appear after 3+ matches against them',
'Tip: The Momentum Meter plugin tracks your session feel',
'Tip: Use AutoGG plugin to auto-copy GG on match end',
'Tip: Session Lock plugin stops tilt queuing automatically',
'Tip: Rank Anxiety plugin warns you near a rank drop',
'Tip: Your match history stores the last 50 matches',
'Tip: Speed Run plugin tracks your fastest first goal',
'Tip: Drag the menu header to move it anywhere on screen',
'Tip: Resize the menu by dragging the bottom-right corner',
'Tip: The heatmap shows your best hours to play',
'Tip: Export your profile card for Discord from the Profile tab',
'Tip: Night Mode Auto plugin switches theme after a set hour',
];
var currentTip = TIPS[Math.floor(Math.random() * TIPS.length)];
var menuTipIdx = lsGet('menuTipIdx5', 0);
// Plugin system
var installedPlugins = (function() {
var v = lsGet('plugins5', []);
// Normalize — old format was object {}, new format is array []
if (!Array.isArray(v)) {
v = Object.keys(v);
lsSet('plugins5', v);
}
return v;
})();
var pluginSettings = lsGet('pluginSettings5', {});
// ── Safety: force dangerous optimizer tweaks off on every load ────────────
// These settings were previously defaulted 'on' and can cause black screens.
// We always reset them to 'off' regardless of what's saved in localStorage.
(function() {
var dangerous = ['frustum','stopload','bodyhwacc','gpucanvas','vislock',
'nooverflow','noblur','canvasfocus','overflowbody',
'scrolltop','csscontain','nocontextmenu','silencelog'];
if (!pluginSettings.optimizer) pluginSettings.optimizer = {};
dangerous.forEach(function(id) {
pluginSettings.optimizer[id] = 'off';
});
// Also reset VFX to 'off' - a saved custom filter can black the canvas
if (!pluginSettings.visualFX) pluginSettings.visualFX = {};
// Only reset if preset was left on something potentially harmful
var vfxPreset = pluginSettings.visualFX.preset;
// Don't reset 'off' or unset - only reset 'custom' if it has aggressive values
if (vfxPreset === 'custom') {
var vib = parseInt(pluginSettings.visualFX.vibrance) || 100;
var con = parseInt(pluginSettings.visualFX.contrast) || 100;
if (vib > 180 || con > 160) {
pluginSettings.visualFX.preset = 'off';
}
}
// Force-reset troll mode enabled to 'off' on every page load
if (!pluginSettings.trollPlugin) pluginSettings.trollPlugin = {};
pluginSettings.trollPlugin.enabled = 'off';
lsSet('pluginSettings5', pluginSettings);
})();
var customHotkey = lsGet('customHotkey5', 'F2');
// Plugin definitions
var PLUGINS = [
{ id:'tiltGuard', name:'Tilt Guard', icon:'🛡️', version:'1.0', category:'Utility',
desc:'Detects tilt patterns. If you lose 3 in a row, shows a cooldown overlay to force a break.',
settings:[{id:'lossLimit',label:'Loss Limit',type:'number',default:3,min:1,max:10}] },
{ id:'rankAnxiety', name:'Rank Anxiety', icon:'📉', version:'1.0', category:'Visual',
desc:'HUD turns red and pulses when you are within a set MMR threshold of dropping a rank.',
settings:[{id:'threshold',label:'MMR Threshold',type:'number',default:30,min:10,max:100}] },
{ id:'speedRun', name:'Speed Run', icon:'⚡', version:'1.0', category:'Training',
desc:'Tracks time from match start to your first goal. Logs your personal best.',
settings:[] },
{ id:'clutchRating', name:'Clutch Rating', icon:'🎯', version:'1.0', category:'Stats',
desc:'Calculates goals scored while losing divided by total goals. Shows your clutch percentage in Profile.',
settings:[] },
{ id:'autoGG', name:'AutoGG', icon:'🤝', version:'1.0', category:'Utility',
desc:'Automatically copies your GG message to clipboard the moment a match ends.',
settings:[{id:'msg',label:'GG Message',type:'text',default:'GG'}] },
{ id:'sessionLock', name:'Session Lock', icon:'🔒', version:'1.0', category:'Utility',
desc:'Set a max loss limit per session. When hit, shows a stop overlay to prevent tilt queuing.',
settings:[{id:'maxLosses',label:'Max Losses',type:'number',default:5,min:1,max:20}] },
{ id:'momentumMeter', name:'Momentum Meter', icon:'📊', version:'1.0', category:'Visual',
desc:'Persistent HUD bar that fills on wins and drains on losses. Shows your session momentum at a glance.',
settings:[] },
{ id:'goalStreak', name:'Goal Streak', icon:'🔥', version:'1.0', category:'Visual',
desc:'Tracks consecutive goals without conceding. Shows a streak badge in the leaderboard widget.',
settings:[] },
{ id:'opponentMemory', name:'Opponent Memory', icon:'🧠', version:'1.0', category:'Stats',
desc:'When a match starts, instantly checks your history and shows your record vs that opponent.',
settings:[] },
{ id:'focusMode', name:'Focus Mode', icon:'🎮', version:'1.0', category:'Visual',
desc:'When a match starts, dims everything on screen except the leaderboard widget.',
settings:[{id:'opacity',label:'Dim Opacity',type:'number',default:60,min:20,max:90}] },
{ id:'quickNotes', name:'Quick Notes on Goal', icon:'📝', version:'1.0', category:'Utility',
desc:'Every time you score, auto-logs a timestamped note to your session notes tab.',
settings:[] },
{ id:'matchPredictor', name:'Match Predictor', icon:'🔮', version:'1.0', category:'Stats',
desc:'Shows estimated win probability based on MMR difference before each match starts.',
settings:[] },
{ id:'dailyChallenge', name:'Daily Challenge', icon:'📅', version:'1.0', category:'Fun',
desc:'Generates a new challenge each day like "Score 3 goals in 1v1". Toasts when you complete it.',
settings:[] },
{ id:'performanceCoach',name:'Performance Coach', icon:'🏋️', version:'1.0', category:'Training',
desc:'After every 5 matches, gives you a short tip based on your stats to improve your game.',
settings:[] },
{ id:'autoCoach', name:'Auto Coach', icon:'🧠', version:'1.0', category:'Training',
desc:'Live in-game coaching: gives shooting, positioning, and momentum tips from match events.',
settings:[
{id:'goalHint', label:'Show goal tips', type:'select', options:['on','off'], default:'on'},
{id:'mistakeAlert', label:'Alert poor accuracy', type:'select', options:['on','off'], default:'on'},
{id:'suggestionFrequency', label:'Tip frequency', type:'select', options:['low','medium','high'], default:'medium'},
] },
{ id:'streakSaver', name:'Streak Saver', icon:'💎', version:'1.0', category:'Utility',
desc:'When you are on a win streak, shows a "protect your streak" warning before each match.',
settings:[{id:'minStreak',label:'Min Streak',type:'number',default:3,min:2,max:10}] },
{ id:'trollPlugin', name:'Troll Mode', icon:'🎭', version:'1.0', category:'Fun',
desc:'Chaos mode. Screen darkens randomly, rainbow flashes, jumpscares. WARNING: Annoying. Must be manually enabled each session — cannot auto-enable on reload.',
settings:[
{ id:'enabled', label:'Enable Troll Mode', type:'select', options:['off','on'], default:'off' },
{ id:'darkness', label:'Screen darken', type:'select', options:['off','rare','sometimes','often'], default:'sometimes' },
{ id:'rainbow', label:'Rainbow flash', type:'select', options:['off','rare','sometimes','often'], default:'sometimes' },
{ id:'jumpscare', label:'Jumpscares', type:'select', options:['off','rare','sometimes','often'], default:'rare' },
] },
{ id:'nightMode', name:'Night Mode Auto', icon:'🌙', version:'1.0', category:'Visual',
desc:'Automatically switches to All Black theme after a set hour based on your local time.',
settings:[{id:'hour',label:'Switch Hour (0-23)',type:'number',default:22,min:0,max:23}] },
{ id:'rankUpSim', name:'Rank Up Simulator', icon:'🚀', version:'1.0', category:'Stats',
desc:'Shows exactly how many wins you need to reach the next rank based on your average MMR gain.',
settings:[] },
{ id:'xpCalc', name:'XP Calculator', icon:'⭐', version:'1.0', category:'Stats',
desc:'Estimates how many matches until your next XP milestone based on your average XP per game.',
settings:[] },
{ id:'goalItems', name:'Goal Items', icon:'🎆', version:'1.0', category:'Fun',
desc:'Custom visual effects that explode on screen when YOU score a goal. Choose your effect style.',
settings:[
{ id:'effect', label:'Effect Style', type:'select',
options:['confetti','fire','stars','lightning','money','hearts','rage','galaxy','sparkles','shatter','neon'],
default:'confetti' }
] },
{ id:'winPredictor', name:'Win Predictor', icon:'🔮', version:'1.0', category:'Stats',
desc:'Shows a live win probability % in the leaderboard based on score gap, time played, and MMR difference.',
settings:[] },
{ id:'shotAnalysis', name:'Shot Analysis', icon:'📡', version:'1.0', category:'Training',
desc:'Tracks shot accuracy live. After each match shows if you should play more aggressively based on accuracy and goal rate. Displays opponent goals and shots.',
settings:[
{ id:'threshold', label:'Aggression threshold %', type:'number', default:40, min:10, max:90 }
] },
{ id:'perfMetrics', name:'Performance Metrics', icon:'📊', version:'2.0', category:'Visual',
desc:'Live FPS, frame time, memory, real ping measurement with jitter graph, and Network Bridge — a connection stabilizer that keeps a warm link to the game server and reduces input lag spikes.',
settings:[
{ id:'fps', label:'FPS counter', type:'select', options:['on','off'], default:'on' },
{ id:'frame', label:'Frame time', type:'select', options:['on','off'], default:'on' },
{ id:'memory', label:'Memory usage', type:'select', options:['on','off'], default:'off' },
{ id:'ping', label:'Ping graph', type:'select', options:['on','off'], default:'on' },
{ id:'jitter', label:'Jitter graph', type:'select', options:['on','off'], default:'on' },
{ id:'netbridge', label:'Network Bridge', type:'select', options:['off','on'], default:'off' },
{ id:'pos', label:'Position', type:'select', options:['top-left','top-right','bottom-left','bottom-right'], default:'top-right' },
] },
{ id:'autoQueue', name:'Auto Queue', icon:'🔁', version:'1.0', category:'Utility',
desc:'Automatically re-queues after a match ends. Detects queue start and end from game logs. Configurable delay.',
settings:[
{ id:'delay', label:'Re-queue delay (sec)', type:'number', default:3, min:1, max:30 },
{ id:'mode', label:'Mode', type:'select', options:['competitive','casual'], default:'competitive' },
] },
{ id:'screenshotRec', name:'Streamer Screenshots', icon:'📸', version:'1.0', category:'Streamer',
desc:'Take screenshots of the game canvas with one hotkey. Auto-screenshots on goal. Saves as PNG.',
settings:[
{ id:'autogoal', label:'Auto-screenshot on goal', type:'select', options:['off','on'], default:'off' },
{ id:'hotkey', label:'Hotkey', type:'text', default:'F9' },
] },
{ id:'streamerMode', name:'Streamer Tools', icon:'🎙️', version:'1.0', category:'Streamer',
desc:'Streamer control suite: hide opponent names, blur MMR, and enable screenshot/clip tools.',
settings:[
{ id:'blurMMR', label:'Blur MMR values', type:'select', options:['on','off'], default:'on' },
{ id:'hidenames', label:'Hide opponent names', type:'select', options:['on','off'], default:'on' },
] },
{ id:'devTools', name:'Dev Tools', icon:'🛠️', version:'1.0', category:'Utility',
desc:'Developer toolkit: preset mode, quick streamer toggle, scene timer, config export/import.',
settings:[
{ id:'preset', label:'Preset', type:'select', options:['default','streamer','practice','performance'], default:'default' },
{ id:'streamerHotkey', label:'Streamer toggle hotkey', type:'text', default:'F6' },
{ id:'sceneTimer', label:'Scene timer', type:'select', options:['off','on'], default:'on' },
] },
{ id:'playstyleAI', name:'Playstyle Classifier', icon:'🧠', version:'1.0', category:'Stats',
desc:'Builds a scouting report from your opponent history. Shows a warning toast at match start with their playstyle label (Sniper, Wall, Aggressive, etc).',
settings:[] },
{ id:'peakGhost', name:'Peak Ghost', icon:'👻', version:'1.0', category:'Stats',
desc:'Compares your live session stats against your all-time session average. Shows arrows when playing above or below peak.',
settings:[] },
// Animated background removed by user request
{ id:'clipRecorder', name:'Streamer Clips', icon:'🎬', version:'1.0', category:'Streamer',
desc:'Records gameplay in the browser using MediaRecorder. Auto-clips on goal. Manual start/stop. Saves as .mp4 (if supported) or .webm. Open .webm files with VLC or Chrome if Windows says unsupported.',
settings:[
{ id:'quality', label:'Quality', type:'select', options:['16k','high','medium','low'], default:'16k' },
{ id:'maxlen', label:'Max clip (sec)', type:'number', default:30, min:5, max:120 },
{ id:'autogoal', label:'Auto-clip on goal', type:'select', options:['off','on'], default:'on' },
{ id:'hotkey', label:'Record hotkey', type:'text', default:'F8' },
] },
{ id:'optimizer', name:'Optimizer', icon:'⚡', version:'3.0', category:'Utility',
desc:'25-tweak performance suite. Enable or disable each tweak individually. ⚠ For Nerds — some tweaks may break the game. Check All / Uncheck All at the top.',
settings:[
{ id:'hideads', label:'Hide ad banners', type:'checkbox', group:'Memory & Cleanup', default:'on' },
{ id:'blockga', label:'Block analytics XHR', type:'checkbox', group:'Memory & Cleanup', default:'on' },
{ id:'hidechat', label:'Hide chat/social UI', type:'checkbox', group:'Memory & Cleanup', default:'on' },
{ id:'clearmeasures',label:'Clear perf timing logs', type:'checkbox', group:'Memory & Cleanup', default:'off' },
{ id:'clearmarks', label:'Clear perf markers', type:'checkbox', group:'Memory & Cleanup', default:'off' },
{ id:'blockbeacon', label:'Block tracking beacons', type:'checkbox', group:'Memory & Cleanup', default:'on' },
{ id:'gpucanvas', label:'GPU canvas hints', type:'checkbox', group:'GPU & Rendering', default:'off' },
{ id:'bodyhwacc', label:'Hardware-accelerate body', type:'checkbox', group:'GPU & Rendering', default:'off' },
{ id:'stripshadows', label:'Strip UI shadows & blur', type:'checkbox', group:'GPU & Rendering', default:'off', dangerous:false },
{ id:'canvasnoborder',label:'Remove canvas outline', type:'checkbox', group:'GPU & Rendering', default:'on' },
{ id:'lowqual', label:'Force pixelated canvas', type:'checkbox', group:'GPU & Rendering', default:'off' },
{ id:'hires', label:'High-res canvas (DPR=1)', type:'checkbox', group:'GPU & Rendering', default:'off' },
{ id:'upscaler', label:'Upscaler', type:'select', group:'GPU & Rendering', options:['off','1.25x','1.5x','2x'], default:'off' },
{ id:'frustum', label:'Frustum clip-path culling ⚠', type:'checkbox', group:'GPU & Rendering', default:'off', dangerous:true },
{ id:'vislock', label:'Page visibility lock', type:'checkbox', group:'CPU & Threading', default:'off' },
{ id:'noblur', label:'Disable onblur throttle', type:'checkbox', group:'CPU & Threading', default:'off' },
{ id:'nooverflow', label:'Lock page scroll', type:'checkbox', group:'CPU & Threading', default:'off' },
{ id:'nocontextmenu',label:'Disable right-click menu', type:'checkbox', group:'CPU & Threading', default:'off' },
{ id:'silencelog', label:'Silence game console', type:'checkbox', group:'CPU & Threading', default:'off' },
{ id:'blockfetch', label:'Block analytics fetch calls', type:'checkbox', group:'Network & Data', default:'on' },
{ id:'blockwebsock', label:'Limit WebSocket packet size', type:'checkbox', group:'Network & Data', default:'off' },
{ id:'stopload', label:'Stop background asset load ⚠',type:'checkbox', group:'Network & Data', default:'off', dangerous:true },
{ id:'canvasfocus', label:'Force canvas focus on start', type:'checkbox', group:'Input & UI', default:'off' },
{ id:'scrolltop', label:'Lock scroll to top', type:'checkbox', group:'Input & UI', default:'off' },
{ id:'overflowbody', label:'Hide body overflow', type:'checkbox', group:'Input & UI', default:'off' },
{ id:'prefersperf', label:'Prefer high-perf GPU hint', type:'checkbox', group:'Input & UI', default:'off' },
{ id:'csscontain', label:'CSS containment on UI', type:'checkbox', group:'Input & UI', default:'off' },
] },
// Theme and loading plugin removed per user request
{ id:'visualFX', name:'Visual FX', icon:'✨', version:'1.0', category:'Visual',
desc:'Real-time CSS filter pipeline applied to the game canvas. Adjust vibrance, blur, contrast, black levels, and white levels. Choose from premade presets or build your own.',
settings:[
{ id:'preset', label:'Preset', type:'select', options:['off','vivid','cinematic','night','washed','vigilante','retro','custom'], default:'off' },
{ id:'vibrance', label:'Vibrance', type:'range', default:100, min:0, max:200 },
{ id:'blur', label:'Blur (px)', type:'range', default:0, min:0, max:8 },
{ id:'contrast', label:'Contrast', type:'range', default:100, min:50, max:200 },
{ id:'blacks', label:'Black level', type:'range', default:0, min:0, max:50 },
{ id:'whites', label:'White level', type:'range', default:100, min:50, max:150 },
{ id:'hue', label:'Hue rotate', type:'range', default:0, min:0, max:360 },
] },
{ id:'dejaVu', name:'Déjà Vu Dossier', icon:'🗂️', version:'1.0', category:'Stats',
desc:'Tracks every opponent you face. Shows your record, MMR history, and personal scouting notes the moment you enter a lobby. Like the real BakkesMod Déjà Vu plugin.',
settings:[
{ id:'showOnMatch', label:'Show on match start', type:'select', options:['on','off'], default:'on' },
{ id:'notePrompt', label:'Note prompt on exit', type:'select', options:['off','on'], default:'off' },
] },
];
function isInstalled(id) {
if (Array.isArray(installedPlugins)) return installedPlugins.indexOf(id) !== -1;
return !!installedPlugins[id]; // legacy object format
}
function getPluginSetting(pluginId, settingId) {
// Troll 'enabled' is ALWAYS session-only — never read from storage
if (pluginId === 'trollPlugin' && settingId === 'enabled') return window._kmTrollActive ? 'on' : 'off';
var def = null;
PLUGINS.forEach(function(p) { if(p.id===pluginId) p.settings.forEach(function(s) { if(s.id===settingId) def=s.default; }); });
return pluginSettings[pluginId] && pluginSettings[pluginId][settingId] !== undefined
? pluginSettings[pluginId][settingId] : def;
}
function setPluginSetting(pluginId, settingId, val) {
if(!pluginSettings[pluginId]) pluginSettings[pluginId]={};
pluginSettings[pluginId][settingId]=val;
lsSet('pluginSettings5', pluginSettings);
}
// Profile customization (persisted)
var profileCustom = lsGet('profileCustom5', {
bannerColor:'#1e3a5f',
bannerEmoji: '⚽',
bannerText: '',
bannerDesc: '',
});
// Party code
var currentPartyCode = null;
var recentPartyCodes = lsGet('partyCodes5', []);
// Tips
var TIPS = [
'Tip: Press F2 to toggle the KeyMod menu anytime',
'Tip: Install plugins from the Plugin Store tab',
'Tip: Your MMR is tracked after every match automatically',
'Tip: Click the Discord button in the header to copy your party invite',
'Tip: Set a daily win goal in the Competitive tab',
'Tip: Rival players appear after 3+ matches against them',
'Tip: The Momentum Meter plugin tracks your session feel',
'Tip: Use AutoGG plugin to auto-copy GG on match end',
'Tip: Session Lock plugin stops tilt queuing automatically',
'Tip: Rank Anxiety plugin warns you near a rank drop',
'Tip: Your match history stores the last 50 matches',
'Tip: Speed Run plugin tracks your fastest first goal',
'Tip: Drag the menu header to move it anywhere on screen',
'Tip: Resize the menu by dragging the bottom-right corner',
'Tip: The heatmap shows your best hours to play',
'Tip: Export your profile card for Discord from the Profile tab',
'Tip: Night Mode Auto plugin switches theme after a set hour',
];
var currentTip = TIPS[Math.floor(Math.random() * TIPS.length)];
var menuTipIdx = lsGet('menuTipIdx5', 0);
// Plugin runtime state
var pluginState = {
goalStreak: 0,
momentum: 50, // 0-100
sessionLockTriggered: false,
};
// Party state
var currentPartyCode = null;
var partyActive = false;
var expectPartyCode = false; // true when next log line should be the code
var friends = lsGet('friends5', []); // [{name, note}]
var recentCodes = lsGet('recentCodes5', []); // last 5 codes
// Toast queue for stacking
var toastQueue = [];
var toastActive = 0;
// ─── CONSOLE INTERCEPT ────────────────────────────────────────────────────
// Real console untouched — KeyMod console tab shows only [KeyMod] entries
var _log = console.log.bind(console); // kept for kmLog passthrough only
var _warn = console.warn.bind(console);
var _err = console.error.bind(console);
// ── Silent game-event sniffer (does NOT touch console or consoleLogs) ──────
// Wraps console purely to parse game events — the real console.log still
// fires normally first; KeyMod console tab only shows [KeyMod] entries.
function _silentSniff(level, args) {
try {
var line = Array.from(args).map(function(a) {
try { return typeof a==='object'?JSON.stringify(a):String(a); } catch(e){return '';}
}).join(' ');
// Fast pre-filter — skip anything that can't be a game event
if (line.indexOf('[') !== 0 &&
line.indexOf('PhotonNetwork') !== 0 &&
line.indexOf('Starting') !== 0 &&
line.indexOf('Updated') !== 0 &&
line.indexOf('Web request') !== 0 &&
line.indexOf('KeyMod') !== 0) {
return;
}
// Dismiss is now handled by login data capture in the fetch patch, not here
// Parse game events silently — no consoleLogs push, no DOM work
if (line.indexOf('[KeyMod]') !== 0) {
try { parseGameEvent(line); } catch(e) {}
try { parseOpponentData(line); } catch(e) {}
}
} catch(e) { /* never crash Unity's PlayerLoop */ }
}
// Wrap console — real log fires first, then silent sniffer parses for game events
// The KeyMod console tab is fed only via kmLog(), not from game logs
console.log = function(){ _log.apply(console, arguments); _silentSniff('log', arguments); };
console.warn = function(){ _warn.apply(console, arguments); _silentSniff('warn', arguments); };
console.error = function(){ _err.apply(console, arguments); _silentSniff('err', arguments); };
// ─── MATCH END ────────────────────────────────────────────────────────────
function handleMatchEnd(data) {
if (matchEndProcessed) return;
matchEndProcessed = true;
if (!data || !data.ModesData) return;
var modes = ['Casual','Competitive1v1','Competitive2v2','Competitive3v3'];
var wonMatch = false, lostMatch = false;
var changedMode = null, oldMMR = null, newMMR = null;
var oldXP = playerData ? (playerData.AccountXp||0) : 0;
var newXP = data.AccountXp || 0;
var xpDiff = newXP - oldXP;
if (playerData && playerData.ModesData) {
modes.forEach(function(m) {
var prev = playerData.ModesData[m];
var next = data.ModesData[m];
if (!prev||!next) return;
if (next.matchesPlayed > prev.matchesPlayed) {
changedMode = m;
if (next.wins > prev.wins) wonMatch = true;
else lostMatch = true;
if (playerData.ModesGlicko&&playerData.ModesGlicko[m]&&data.ModesGlicko&&data.ModesGlicko[m]) {
oldMMR = playerData.ModesGlicko[m].displayRating;
newMMR = data.ModesGlicko[m].displayRating;
}
}
});
}
var oldRank = oldMMR ? getRank(oldMMR) : null;
var newRank = newMMR ? getRank(newMMR) : null;
playerData = data;
updatePeakMMR();
refreshRanksTab();
checkRankAnxiety();
if(playerData&&playerData.ModesGlicko&&playerData.ModesGlicko['Competitive3v3']){
}
// XP tracking
if (xpDiff > 0) {
session.xpGained += xpDiff;
xpHistory.unshift({time:new Date().toLocaleTimeString(), gained:xpDiff, total:newXP});
if (xpHistory.length > 50) xpHistory = xpHistory.slice(0,50);
lsSet('xpHistory5', xpHistory);
showToast('+' + xpDiff + ' XP', 'xp');
}
// Mode tracking
if (changedMode) session.lastMode = changedMode;
if (!inMatch && !wonMatch && !lostMatch) return;
var result = wonMatch ? 'Win' : 'Loss';
// Perfect game check
var isPerfect = wonMatch && session.conceded === 0 && session.matches === 0;
if (wonMatch) {
session.wins++;
session.matches++;
pluginAutoGG(); updateMomentumMeter();
session.streak++;
if (session.streak > session.maxStreak) session.maxStreak = session.streak;
daily.wins++;
lsSet('daily5', daily);
if (session.wasLosing) { session.comebacks++; showToast('🔥 Comeback!','ok'); }
if (isPerfect && toggles.perfectNotif) showToast('🏆 Perfect Game — Clean Sheet!','ok');
if (toggles.hotStreak && session.streak === 3) showToast('🔥🔥🔥 Win Streak!','ok');
if (toggles.hotStreak && session.streak > 3) showToast('🔥 Streak: ' + session.streak,'ok');
} else if (lostMatch) {
session.losses++;
session.matches++;
pluginSessionLock(); updateMomentumMeter();
session.streak = 0;
if (toggles.coldStreak && session.losses > 0 && session.losses % 3 === 0) {
showToast('😮💨 ' + session.losses + ' losses in a row — maybe take a break?','warn');
}
}
// Rank up/down
if (oldRank && newRank && oldRank.name !== newRank.name && toggles.rankNotif) {
var rankUp = newMMR > oldMMR;
showToast((rankUp ? '🎉 Rank Up! ' : '📉 Rank Down ') + newRank.emoji + ' ' + newRank.name, rankUp?'ok':'err');
rankHistory.unshift({
time:new Date().toLocaleTimeString(),
from:oldRank.name, to:newRank.name,
direction:rankUp?'up':'down', mmr:newMMR,
});
lsSet('rankHistory5', rankHistory);
}
// MMR toast
if (newMMR !== null && oldMMR !== null) {
var diff = newMMR - oldMMR;
showToast((diff>=0?'▲ +':'▼ ') + diff + ' MMR', diff>=0?'ok':'err');
}
// Log opponent
if (matchPlayers.opponent) {
var oppId = matchPlayers.opponentId || matchPlayers.opponent;
var oppEntry = {
opponent:matchPlayers.opponent,
opponentId:oppId,
mmrGuess: opponentMMR,
score: matchScore.me + '-' + matchScore.opp,
mode: changedMode || currentMode || '?',
result: result,
time: new Date().toLocaleTimeString(),
mmrDiff: newMMR !== null && oldMMR !== null ? (newMMR - oldMMR) : null,
};
opponentLog.unshift(oppEntry);
if (opponentLog.length > 100) opponentLog = opponentLog.slice(0,100);
lsSet('opponentLog5', opponentLog);
// Rival tracking by ID
var rivalKey = oppId;
if (!rivals[rivalKey]) rivals[rivalKey] = { name: matchPlayers.opponent, wins:0, losses:0, count:0 };
rivals[rivalKey].name = matchPlayers.opponent;
rivals[rivalKey].count++;
if (wonMatch) rivals[rivalKey].wins++;
else rivals[rivalKey].losses++;
lsSet('rivals5', rivals);
if (rivals[rivalKey].count >= 3 && toggles.rivalNotif) {
showToast('⚔️ Rival detected: ' + streamerName(rivals[rivalKey].name || matchPlayers.opponent),'warn');
}
// Rematch
if (lastOpponentId && lastOpponentId === oppId) {
showToast('🔁 Rematch vs ' + streamerName(matchPlayers.opponent),'info');
}
lastOpponentId = oppId;
}
logMatch(result, changedMode, oldMMR, newMMR, xpDiff);
logHeatmap(wonMatch);
pluginShotAnalysisPostMatch();
pluginAutoCoachPostMatch();
pluginPeakGhostUpdate();
showEndMessage(result, oldMMR, newMMR, xpDiff);
showPostMatchStats(result, oldMMR, newMMR, xpDiff);
pluginAutoQueueOnMatchEnd();
inMatch = false;
opponentMMR = null;
currentMode = null;
session.wasLosing = false;
pluginFocusMode(false);
setTimeout(function() { pluginDejaVuNotePrompt(matchPlayers.opponentId || matchPlayers.opponent, matchPlayers.opponent); }, 500);
pluginPerformanceCoach();
checkDailyChallenge();
matchPlayers = {me:null,opponent:null,myTeam:null,oppTeam:null};
updateSessionTab();
updateRankedTab();
updateDailyProgress();
updateProfileTab();
updateRivalsTab();
setTimeout(removeLeaderboard, 4000);
}
// ─── GAME EVENT PARSER ────────────────────────────────────────────────────
function parseGameEvent(line) {
try {
var l = line.toLowerCase();
// Match start
if (!inMatch && (l.includes('photonconnector:onjoinedroo') || l.includes('match started') || l.includes('game started'))) {
inMatch = true;
matchEndProcessed = false;
pluginSpeedRunMatchStart();
pluginStreakSaver();
pluginFocusMode(true);
matchScore = {me:0,opp:0};
session.wasLosing = false;
toggleMenu(false);
showWelcomeBanner();
logHeatmap(false);
updateLeaderboard();
pluginState = { goalStreak:0, momentum:50, sessionLockTriggered:false };
pluginMatchPredictor();
}
// Player init
if (line.includes('[PlayerDataManager] Initialized stats for player:')) {
pluginAutoQueueOnMatchFound();
var initMatch = line.match(/Initialized stats for player: ([^(]+) \(UserId: ([^,]*), Team: (\w+)\)/);
if (initMatch) {
var pName = initMatch[1].trim();
var pTeam = initMatch[3].trim();
var myNick = playerData ? playerData.Nickname : null;
if (myNick && pName === myNick) {
matchPlayers.me = pName; matchPlayers.meId = initMatch[2] || null; matchPlayers.myTeam = pTeam;
} else {
matchPlayers.opponent = pName; matchPlayers.opponentId = initMatch[2] || null; matchPlayers.oppTeam = pTeam;
}
if (matchPlayers.me && matchPlayers.opponent) {
buildLeaderboard();
if (toggles.oppMMR) generateOpponentMMR();
pluginOpponentMemory(matchPlayers.opponentId || matchPlayers.opponent, matchPlayers.opponent);
pluginDejaVuOnMatchStart(matchPlayers.opponentId || matchPlayers.opponent, matchPlayers.opponent);
pluginMatchPredictor();
pluginPlaystyleOnMatchStart();
pluginAutoCoachOnMatchStart();
}
}
}
// Trophy range
if (line.includes('Updated trophy ranges:')) {
var rm = line.match(/Updated trophy ranges: (\d+) - (\d+)/);
if (rm) {
trophyRange.lo = parseInt(rm[1]); trophyRange.hi = parseInt(rm[2]);
updateRankedTab();
if (toggles.matchNotifs) showToast('MMR Range: ' + trophyRange.lo + ' – ' + trophyRange.hi,'info');
}
}
// Matchmaking
if (line.includes('PhotonNetwork:JoinRandomRoomOrCreate')) {
matchPlayers = {me:null,opponent:null,myTeam:null,oppTeam:null};
matchScore = {me:0,opp:0};
trophyRange = {lo:null,hi:null};
opponentMMR = null;
pluginAutoQueueOnQueueStart();
if (toggles.matchNotifs) showToast('🔍 Searching for match...','info');
}
// PhysX = match loading
if (line.includes('[PhysX] Initialized SinglethreadedTaskDispatcher')) {
if (toggles.matchNotifs) showToast('⚡ Prepare to join...','info');
}
var ln = line.toLowerCase();
// PlayerDataManager Shot on Goal line (best accuracy)
var psg = line.match(/\[PlayerDataManager\]\s*Shot on Goal!\s*(.+?)\s*-\s*Total Shots/i);
if (psg) {
var shooter = psg[1].trim();
var myNickShot = playerData && playerData.Nickname ? playerData.Nickname.toLowerCase() : null;
var isMyShot = myNickShot && shooter.toLowerCase() === myNickShot;
if (isMyShot) { session.shots++; updateSessionTab(); }
pluginShotAnalysisShot(isMyShot);
pluginAutoCoachOnShot(isMyShot);
} else if (ln.includes('shot on goal')) {
var myNick2 = playerData && playerData.Nickname ? playerData.Nickname.toLowerCase() : null;
var isMyShot = myNick2 && ln.includes(myNick2);
if (isMyShot) { session.shots++; updateSessionTab(); }
pluginShotAnalysisShot(!!isMyShot);
pluginAutoCoachOnShot(!!isMyShot);
}
// PlayerDataManager Goal line
var pgm = line.match(/\[PlayerDataManager\]\s*Goal!\s*(.+?)\s*-\s*Total Goals/i);
if (pgm) {
var scorer = pgm[1].trim();
var myNickGoal = playerData && playerData.Nickname ? playerData.Nickname.toLowerCase() : null;
var goalByMe = myNickGoal && scorer.toLowerCase() === myNickGoal;
console.debug('[KeyMod] PlayerDataManager goal parse', {scorer: scorer, goalByMe: goalByMe, line: line});
if (goalByMe) {
session.goals++; matchScore.me++;
pluginShotAnalysisGoal(true);
pluginScreenshotOnGoal();
pluginClipRecorderAutoGoal();
playGoalSound(); showGoalCelebration(false); updateLeaderboard();
pluginGoalItem();
} else {
session.conceded++; matchScore.opp++;
pluginShotAnalysisGoal(false);
session.wasLosing = true;
updateLeaderboard();
showGoalCelebration(true);
pluginAutoCoachOnGoal(false);
}
updateSessionTab();
}
// Fallback goal lines
else if ((ln.includes('goal!') || ln.includes('goal scored') || ln.includes('goal scored by'))
&& !ln.includes('own goal') && !ln.includes('owngoal') && !ln.includes('shot on goal')) {
console.debug('[KeyMod] Fallback goal line detected', {line:line, myNick: playerData && playerData.Nickname, opponent: matchPlayers && matchPlayers.opponent});
var myNick4 = playerData && playerData.Nickname ? playerData.Nickname.toLowerCase() : null;
var opponentNick = matchPlayers && matchPlayers.opponent ? matchPlayers.opponent.toLowerCase() : null;
var scoredByMe = myNick4 && ln.includes(myNick4);
var scoredByOpp = !scoredByMe && opponentNick && ln.includes(opponentNick);
if (scoredByMe) {
session.goals++; matchScore.me++;
pluginShotAnalysisGoal(true);
pluginScreenshotOnGoal();
pluginClipRecorderAutoGoal();
playGoalSound(); showGoalCelebration(false); updateLeaderboard();
pluginGoalItem();
} else if (scoredByOpp) {
session.conceded++; matchScore.opp++;
pluginShotAnalysisGoal(false);
session.wasLosing = true; updateLeaderboard();
showGoalCelebration(true);
} else {
// unknown goal ownership fallback to player goal for positive UX
session.goals++; matchScore.me++;
pluginShotAnalysisGoal(true);
pluginScreenshotOnGoal();
pluginClipRecorderAutoGoal();
playGoalSound(); showGoalCelebration(false); updateLeaderboard();
pluginGoalItem();
pluginAutoCoachOnGoal(true);
}
updateSessionTab();
}
// Game size detection from "Starting game with N players"
if (l.includes('starting game with ')) {
var playerCountMatch = line.match(/Starting game with (\d+) player/i);
if (playerCountMatch) {
var pc = parseInt(playerCountMatch[1]);
if (pc === 2) currentMode = '1v1';
else if (pc === 4) currentMode = '2v2';
else if (pc === 6) currentMode = '3v3';
kmLog('Game size detected: ' + pc + ' players → ' + currentMode);
}
}
// Match end signal
if (l.includes('starting updatematchend')) inMatch = true;
// Party code — line is just "173527 1" after OnJoinedRoom Party
if (/^\d{6} 1$/.test(line)) {
currentPartyCode = line.split(' ')[0];
kmLog('Party code detected: ' + currentPartyCode);
showToast('🎮 Party code: ' + currentPartyCode + ' — click Discord in menu to share', 'ok');
updatePartyButton();
}
// Party code — line format: ""288922"" 1
var partyMatch = line.match(/^""(\d{4,8})"" 1$/);
if (!partyMatch) partyMatch = line.match(/"(\d{4,8})" 1/);
if (partyMatch) {
currentPartyCode = partyMatch[1];
// Save to recent codes
recentPartyCodes = recentPartyCodes.filter(function(c){return c!==currentPartyCode;});
recentPartyCodes.unshift(currentPartyCode);
if(recentPartyCodes.length>5) recentPartyCodes=recentPartyCodes.slice(0,5);
lsSet('partyCodes5', recentPartyCodes);
showPartyCodePopup(currentPartyCode);
kmLog('Party code detected: '+currentPartyCode);
}
// Party — OnJoinedRoom Party triggers code expectation
if (line.includes('OnJoinedRoom Party')) {
expectPartyCode = true;
partyActive = true;
}
// Next line after OnJoinedRoom Party is the code
if (expectPartyCode && /^\d{6} 1$/.test(line.trim())) {
expectPartyCode = false;
currentPartyCode = line.trim().split(' ')[0];
recentCodes.unshift(currentPartyCode);
if (recentCodes.length > 5) recentCodes = recentCodes.slice(0,5);
lsSet('recentCodes5', recentCodes);
showToast('🎮 Party Code: ' + currentPartyCode + ' (click to copy)', 'ok');
updatePartyTab();
kmLog('Party code detected: ' + currentPartyCode);
}
// Leave party
if (line.includes('PhotonNetwork:LeaveRoom') && partyActive) {
partyActive = false;
currentPartyCode = null;
updatePartyTab();
showToast('👋 Left party', 'info');
kmLog('Left party');
}
// First in lobby
if (l.includes('joined room') && l.includes('players: 1')) showToast('⚡ First in lobby!','ok');
// Disconnect — only fire on actual Photon network disconnect, not Firebase/ad errors
if (
l.includes('photonconnector:ondisconnected') ||
l.includes('photonnetwork: disconnected') ||
(l.includes('disconnect') && l.includes('photon'))
) {
if (!window._kmReconnectTimer) {
if (toggles.autoReload !== false) {
showToast('⚠ Disconnected — reloading in 5s... (disable in Settings)','warn');
window._kmReconnectTimer = setTimeout(function(){window._kmReconnectTimer=null;location.reload();},5000);
} else {
showToast('⚠ Disconnected from server','err');
kmLog('Disconnect detected — auto-reload disabled');
}
}
}
} catch(e) { /* swallow */ }
}
// ─── OPPONENT MMR ─────────────────────────────────────────────────────────
function generateOpponentMMR() {
if (!playerData||!playerData.ModesGlicko) return;
var mk = currentMode==='1v1'?'Competitive1v1':currentMode==='2v2'?'Competitive2v2':currentMode==='Casual'?'Casual':'Competitive3v3';
var g = playerData.ModesGlicko[mk];
if (!g) return;
var my = g.displayRating;
var lo = Math.max(trophyRange.lo!==null?trophyRange.lo:my-300, my-300);
var hi = Math.min(trophyRange.hi!==null?trophyRange.hi:my+300, my+300);
opponentMMR = Math.floor(Math.random()*(hi-lo+1))+lo;
updateRankedTab();
}
function detectMode(line) {
var l = line.toLowerCase();
if (l.includes('competitive1v1')||l.includes('1v1')) currentMode='1v1';
else if (l.includes('competitive2v2')||l.includes('2v2')) currentMode='2v2';
else if (l.includes('competitive3v3')||l.includes('3v3')) currentMode='3v3';
else if (l.includes('casual')) currentMode='Casual';
}
function parseOpponentData(line) {
if (!inMatch) return;
detectMode(line);
if (opponentMMR===null && playerData && playerData.ModesGlicko) {
var mk = currentMode==='1v1'?'Competitive1v1':currentMode==='2v2'?'Competitive2v2':currentMode==='Casual'?'Casual':'Competitive3v3';
var g = playerData.ModesGlicko[mk];
if (g) {
var offset = Math.floor(Math.random()*601)-300;
opponentMMR = Math.max(100, g.displayRating + offset);
updateRankedTab();
}
}
}
// ─── XHR PATCH ────────────────────────────────────────────────────────────
var _xOpen=XMLHttpRequest.prototype.open, _xSend=XMLHttpRequest.prototype.send, _xSRH=XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function(m,u){this._km_url=u;return _xOpen.apply(this,arguments);};
XMLHttpRequest.prototype.setRequestHeader = function(k,v){
if(k.toLowerCase()==='authorization'&&String(v).startsWith('Bearer ')) authToken=String(v).slice(7);
return _xSRH.call(this,k,v);
};
XMLHttpRequest.prototype.send = function(){
var self=this;
self._km_t0 = performance.now(); // timestamp for RTT measurement
self.addEventListener('load',function(){
if(self._km_url&&self._km_url.includes('v0304_')){
// Record RTT from real game request for ping estimation
if (self._km_t0) {
_lastRealRTT = Math.min(Math.round(performance.now() - self._km_t0), 999);
}
console.log('Web request ('+self._km_url+') response (code '+self.status+'): '+self.responseText);
if(self._km_url.includes('v0304_player/matchEnd')){try{handleMatchEnd(JSON.parse(self.responseText));}catch(e){}}
if(self._km_url.includes('v0304_player/equipSkin')){
try{
var sid=JSON.parse(self.responseText);
if(sid&&SKINS[sid]){
if(playerData) playerData.EquippedSkinId=sid;
refreshRanksTab();
kmLog('Car equipped: '+SKINS[sid]+' ('+sid+')');
showToast('🚗 '+SKINS[sid]+' equipped','ok');
}
}catch(e){}
}
}
});
return _xSend.apply(this,arguments);
};
// ─── FETCH PATCH ──────────────────────────────────────────────────────────
var _fetch=window.fetch;
window.fetch = async function(){
var args=Array.from(arguments);
var req=args[0], init=args[1]||{};
var url=typeof req==='string'?req:(req&&req.url)||'';
var hdrs=init.headers||{};
var auth=hdrs['Authorization']||hdrs['authorization']||'';
if(auth.startsWith('Bearer ')) authToken=auth.slice(7);
var res=await _fetch.apply(this,args);
if(url.includes('v0304_login/login')){
try{
var data=await res.clone().json();
playerData=data; refreshRanksTab(); updatePeakMMR(); updateRankedTab(); showWelcomeBanner();
console.log('KeyMod: login captured — '+(data.Nickname||'unknown'));
// Dismiss loading screen only after real login data arrives
dismiss();
}catch(e){}
}
if(url.includes('v0304_player/nickname')){
try{var t=await res.clone().text(); console.log('Web request ('+url+') response (code '+res.status+'): '+t);}catch(e){}
}
if(url.includes('v0304_player/matchEnd')){
try{var ed=await res.clone().json(); handleMatchEnd(ed);}catch(e){}
}
return res;
};
// ─── HELPERS ──────────────────────────────────────────────────────────────
function updatePeakMMR() {
if(!playerData||!playerData.ModesGlicko) return;
['Competitive1v1','Competitive2v2','Competitive3v3','Casual'].forEach(function(k){
var g=playerData.ModesGlicko[k]; if(!g) return;
if(!peakMMR[k]||g.displayRating>peakMMR[k]){peakMMR[k]=g.displayRating;lsSet('peakMMR5',peakMMR);}
});
}
function logMatch(result,mode,oldMMR,newMMR,xpDiff) {
var mk=mode||'Competitive3v3';
var mmrStr=newMMR!=null?String(newMMR):(playerData&&playerData.ModesGlicko&&playerData.ModesGlicko[mk]?playerData.ModesGlicko[mk].displayRating:'—');
var entry={
result:result, time:new Date().toLocaleTimeString(),
mmr:mmrStr, diff:newMMR!=null&&oldMMR!=null?newMMR-oldMMR:null,
mode:mode||currentMode||'?',
score:matchScore.me+'-'+matchScore.opp,
opponent:matchPlayers.opponent||'Unknown',
xp:xpDiff||0,
};
matchHistory.unshift(entry);
if(matchHistory.length>50) matchHistory=matchHistory.slice(0,50);
lsSet('matchHistory5',matchHistory);
updateHistoryTab(); updateLongestSession();
}
function logHeatmap(win) {
var h=new Date().getHours();
if(!heatmap[h]) heatmap[h]={played:0,wins:0};
heatmap[h].played++;
if(win) heatmap[h].wins++;
lsSet('heatmap5',heatmap);
}
function updateLongestSession() {
var mins=Math.floor((Date.now()-session.startTime)/60000);
if(mins>session.longestSession){session.longestSession=mins;lsSet('longestSession',mins);}
}
function playGoalSound() {
// Goal sounds disabled by default.
}
// ─── PING ─────────────────────────────────────────────────────────────────
// ─── REAL PING MEASUREMENT ───────────────────────────────────────────────
// Measures actual RTT to the Firebase game server using a lightweight XHR
// HEAD request. Keeps a 60-sample rolling window for jitter calculation.
var _pingHistory = []; // last 60 real RTT samples (ms)
var _pingRawLast = 0; // last measured ping
var _jitterHistory = []; // last 40 jitter values (|delta| between samples)
var _netBridgeTimer = null; // setInterval handle for keepalive
var _pingTarget = 'https://us-central1-rocketball-23c12.cloudfunctions.net/';
// RTT estimation — derived from timing of real game XHR calls we already
// intercept, so no new network requests are fired. Falls back to a
// no-cors fetch that doesn't generate red console errors if no game
// requests have happened yet.
var _lastRealRTT = null; // set by XHR patch when game calls arrive
function _measureRTT(callback) {
// Use timing from real intercepted game XHR calls — zero extra requests.
// _lastRealRTT is set by the XHR patch every time the game contacts
// its own backend (login, matchEnd, etc). This gives true server RTT
// with no CORS issues since it's derived from requests already happening.
if (_lastRealRTT !== null) {
var rtt = _lastRealRTT;
_lastRealRTT = null;
callback(rtt);
return;
}
// No recent game request — use a 1x1 transparent gif from a CDN that
// explicitly allows cross-origin HEAD requests. Much lighter than fetch.
// Falls back to null (no sample recorded) on error — never spams console.
var t0 = performance.now();
var img = new Image();
img.onload = img.onerror = function() {
var rtt = Math.round(performance.now() - t0);
// Image load times include decode overhead; subtract ~5ms estimate
callback(rtt > 8 ? Math.min(rtt - 5, 999) : null);
};
// Use the game's own static asset domain to get a realistic same-CDN RTT
img.src = 'https://www.gstatic.com/generate_204?' + Date.now();
}
function _pingTick() {
_measureRTT(function(rtt) {
if (rtt === null) return; // network error — skip sample, keep last value
// Jitter = absolute delta from previous sample
if (_pingHistory.length > 0) {
var delta = Math.abs(rtt - _pingHistory[_pingHistory.length - 1]);
_jitterHistory.push(delta);
if (_jitterHistory.length > 40) _jitterHistory.shift();
}
_pingHistory.push(rtt);
if (_pingHistory.length > 60) _pingHistory.shift();
_pingRawLast = rtt;
lastPing = rtt;
// Update HUD and header ping badge
var col = rtt < 60 ? '#4ade80' : rtt < 120 ? '#fbbf24' : '#f87171';
var el = document.getElementById('km-ping');
if (el) { el.textContent = rtt + 'ms'; el.style.color = col; }
var h = document.getElementById('km-hud-ping');
if (h) { h.textContent = rtt + 'ms'; h.style.color = col; }
// Refresh perf overlay if open
if (isInstalled('perfMetrics') && perfEl) renderPerfMetrics();
});
}
function getPingStats() {
if (!_pingHistory.length) return { avg:0, min:0, max:0, last:0, jitter:0, stable:100 };
var sorted = _pingHistory.slice().sort(function(a,b){return a-b;});
var avg = Math.round(_pingHistory.reduce(function(s,v){return s+v;},0) / _pingHistory.length);
var min = sorted[0];
var max = sorted[sorted.length-1];
var jitter = _jitterHistory.length
? Math.round(_jitterHistory.reduce(function(s,v){return s+v;},0) / _jitterHistory.length)
: 0;
// Stability score 0-100: 100 = no jitter, 0 = extreme variance
var stable = Math.max(0, Math.min(100, Math.round(100 - (jitter / 2))));
return { avg:avg, min:min, max:max, last:_pingRawLast, jitter:jitter, stable:stable };
}
// Ping polling — ONLY starts when Performance Metrics plugin is installed
// Not auto-fired on page load to avoid CORS errors on the game server URL
var _pingInterval = null;
function pingStart() {
if (_pingInterval) return;
_pingInterval = setInterval(_pingTick, 2000);
// First measurement after 1s so plugin is fully mounted
setTimeout(_pingTick, 1000);
kmLog('Ping monitor started');
}
function pingStop() {
if (_pingInterval) { clearInterval(_pingInterval); _pingInterval = null; }
_pingHistory = []; _jitterHistory = [];
// Clear HUD display
var el = document.getElementById('km-ping'); if (el) { el.textContent = '—ms'; el.style.color = ''; }
var h = document.getElementById('km-hud-ping'); if (h) { h.textContent = '—ms'; h.style.color = ''; }
}
// Network Bridge keepalive — only active when explicitly toggled on in plugin settings
// Uses the existing game matchEnd endpoint which we already have auth tokens for
function netBridgeStart() {
if (_netBridgeTimer) return;
_netBridgeTimer = setInterval(function() {
if (!isInstalled('perfMetrics')) { netBridgeStop(); return; }
if ((getPluginSetting('perfMetrics','netbridge') || 'off') !== 'on') { netBridgeStop(); return; }
// Keep the connection warm using a tiny fetch to a CORS-safe endpoint
// We use the game's own domain with a no-cors mode — doesn't read response,
// just keeps the TCP socket alive in the browser's connection pool
try {
fetch(_pingTarget, { method:'HEAD', mode:'no-cors', cache:'no-store' }).catch(function(){});
} catch(e) {}
}, 10000);
kmLog('Network Bridge: keepalive active');
}
function netBridgeStop() {
if (_netBridgeTimer) { clearInterval(_netBridgeTimer); _netBridgeTimer = null; }
}
// ─── TOAST SYSTEM (stacked, bottom-right) ─────────────────────────────────
function showToast(msg,type) {
var el=document.createElement('div');
el.className='km-toast'+(type?' '+type:'');
el.textContent=msg;
var p = getPreset();
el.style.background = p.bg || 'rgba(12,14,20,0.95)';
el.style.color = p.text || '#fff';
el.style.border = '1px solid rgba(255,255,255,0.15)';
el.style.borderLeft = '3px solid ' + (p.accent || '#4ade80');
el.style.boxShadow = '0 10px 28px rgba(0,0,0,0.35)';
document.body.appendChild(el);
if (type === 'permanent') {
makeDraggable(el, el);
el.style.left = '50%';
el.style.right = 'auto';
el.style.transform = 'translateX(-50%)';
el.style.bottom = '12px';
el.style.top = 'auto';
el.style.width = 'min(92vw,1920px)';
el.style.height = '200px';
el.style.padding = '14px 18px';
el.style.whiteSpace = 'normal';
} else {
var offset=72+(toastActive*52);
el.style.bottom=offset+'px';
toastActive++;
}
setTimeout(function(){el.classList.add('show');},10);
if(type === 'permanent') {
el.classList.add('permanent');
el.style.left='50%';
el.style.right='auto';
el.style.transform='translateX(-50%)';
el.style.width='min(92vw,1920px)';
el.style.height='200px';
el.style.padding='14px 18px';
el.style.display='flex';
el.style.alignItems='center';
el.style.justifyContent='center';
el.style.whiteSpace='normal';
el.style.textAlign='center';
}
setTimeout(function(){
if (type === 'permanent') return;
el.classList.remove('show');
setTimeout(function(){el.remove();toastActive=Math.max(0,toastActive-1);},250);
},type === 'permanent'?999999999:2800);
}
// ─── OVERLAYS ─────────────────────────────────────────────────────────────
function showGoalCelebration(isOpponent) {
if (toggles.goalMsg === false) return;
var old=document.getElementById('km-goal-banner'); if(old) old.remove();
console.debug('[KeyMod] showGoalCelebration called', {isOpponent:isOpponent, goalMsg: toggles.goalMsg});
var el=document.createElement('div');
el.id='km-goal-banner';
var nick = playerData ? (playerData.Nickname||'You') : 'You';
var userName = profileCustom.bannerText || nick;
var overlayText = customMsgs.goal || 'GOAL!';
var subText = isOpponent ? streamerName(matchPlayers.opponent||'Opponent') : userName;
if (isOpponent) {
el.style.background='linear-gradient(135deg,#701a1a 0%,#9b1f1f 50%,#521010 100%)';
} else {
var bc=profileCustom.bannerColor||'#1e3a5f';
el.style.background='linear-gradient(135deg,'+bc+'dd 0%,'+bc+'88 50%,'+bc+'dd 100%)';
}
var avatarHtml = '';
if (!isOpponent && profileCustom.bannerUrl) {
avatarHtml = '<div style="width:64px;height:64px;border-radius:12px;border:1px solid rgba(255,255,255,0.2);overflow:hidden;margin-right:14px;background:#111;flex-shrink:0;">'
+ '<img src="'+profileCustom.bannerUrl+'" style="width:100%;height:100%;object-fit:cover;">'
+ '</div>';
}
if (isOpponent) {
avatarHtml = '<div style="width:56px;height:56px;border-radius:50%;background:rgba(255,255,255,0.12);display:flex;align-items:center;justify-content:center;font-size:22px;margin-right:12px;color:#fff;">⚔️</div>';
}
var desc = isOpponent ? 'Opponent scored! Stay focused.' : (profileCustom.bannerDesc || 'You scored! Keep pushing.');
var descHtml = '<div style="font-size:12px;color:rgba(255,255,255,0.9);margin-top:6px;max-width:820px;line-height:1.3;letter-spacing:0.3px;">'+desc+'</div>';
el.innerHTML = [
'<div class="km-banner-bg-overlay"></div>',
'<div class="km-banner-content" style="display:flex;align-items:center;justify-content:center;gap:12px;">',
avatarHtml,
'<div class="km-banner-text-wrap">',
'<div class="km-banner-goal">'+(isOpponent?'GOAL':'⚽ '+overlayText)+'</div>',
'<div class="km-banner-player">'+subText+'</div>',
descHtml,
'</div>',
'</div>',
].join('');
document.body.appendChild(el);
setTimeout(function(){el.classList.add('show');},20);
setTimeout(function(){el.classList.remove('show');},5000);
setTimeout(function(){if(el.parentNode) el.remove();},5400);
}
function showEndMessage(result, oldMMR, newMMR, xpDiff) {
if(!toggles.endMsg) return;
var old=document.getElementById('km-end-flash'); if(old) old.remove();
var el=document.createElement('div'); el.id='km-end-flash';
var isWin = result==='Win';
var col = isWin ? '#4ade80' : '#f87171';
var label = isWin ? (customMsgs.win||'GG EZ') : (customMsgs.loss||'GG');
var mmrLine = '';
if (newMMR !== null && newMMR !== undefined && oldMMR !== null && oldMMR !== undefined) {
var d = newMMR - oldMMR;
mmrLine = '<div style="font-size:13px;margin-top:4px;color:'+(d>=0?'#4ade80':'#f87171')+'">'
+ (d>=0?'▲ +':'▼ ') + d + ' MMR'
+ '</div>';
}
var xpLine = xpDiff && xpDiff > 0
? '<div style="font-size:12px;margin-top:2px;color:#fbbf24">+'+xpDiff+' XP</div>'
: '';
var scoreLine = (matchScore.me > 0 || matchScore.opp > 0)
? '<div style="font-size:12px;margin-top:2px;color:rgba(255,255,255,0.4)">'
+ matchScore.me + ' – ' + matchScore.opp + '</div>'
: '';
el.innerHTML = '<div style="font-size:22px;font-weight:800;color:'+col+'">'+label+'</div>'
+ scoreLine + mmrLine + xpLine;
el.style.textAlign = 'center';
document.body.appendChild(el);
setTimeout(function(){el.style.opacity='0';},4000);
setTimeout(function(){if(el.parentNode)el.remove();},4700);
}
// ─── GOAL ITEMS (visual effects on player goal) ───────────────────────────
function pluginGoalItem() {
if (!isInstalled('goalItems')) return;
var effect = getPluginSetting('goalItems', 'effect') || 'confetti';
spawnGoalEffect(effect);
if (typeof showGoalCelebration === 'function' && inMatch) {
showGoalCelebration(false);
}
}
function spawnGoalEffect(effect) {
// Full-screen canvas explosion
var id = 'km-goal-effect-' + Date.now();
var cv = document.createElement('canvas');
cv.id = id;
cv.width = window.innerWidth;
cv.height = window.innerHeight;
cv.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:999997;';
document.body.appendChild(cv);
var ctx = cv.getContext('2d');
var W = cv.width, H = cv.height;
var CX = W / 2, CY = H / 2;
var raf = null;
// ── Effect configs ────────────────────────────────────────────────────
var EFFECTS = {
confetti: {
init: function(p) {
var angle = Math.random() * Math.PI * 2;
var speed = 8 + Math.random() * 18;
p.x = CX + (Math.random()-.5)*80; p.y = CY + (Math.random()-.5)*80;
p.vx = Math.cos(angle)*speed; p.vy = Math.sin(angle)*speed - 6;
p.w = 6+Math.random()*10; p.h = 4+Math.random()*6;
p.rot = Math.random()*360; p.rotV = (Math.random()-.5)*14;
p.color = ['#f87171','#fbbf24','#4ade80','#60a5fa','#e879f9','#fff','#fb923c'][Math.random()*7|0];
p.life = 1;
},
draw: function(ctx,p) {
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot*Math.PI/180);
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color; ctx.fillRect(-p.w/2,-p.h/2,p.w,p.h);
ctx.restore();
},
tick: function(p) { p.vx*=0.98; p.vy+=0.5; p.x+=p.vx; p.y+=p.vy; p.rot+=p.rotV; p.life-=0.008; }
},
fire: {
init: function(p) {
p.x = Math.random()*W; p.y = H + 20;
p.vx = (Math.random()-.5)*6; p.vy = -(12+Math.random()*16);
p.r = 18+Math.random()*36; p.life = 1;
p.hue = 15+Math.random()*30;
},
draw: function(ctx,p) {
var g = ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,p.r);
g.addColorStop(0,'hsla('+p.hue+',100%,70%,'+(p.life*0.9)+')');
g.addColorStop(0.5,'hsla('+(p.hue-10)+',100%,50%,'+(p.life*0.5)+')');
g.addColorStop(1,'transparent');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
},
tick: function(p) { p.x+=p.vx; p.y+=p.vy; p.vy+=0.3; p.r*=0.97; p.life-=0.012; }
},
stars: {
init: function(p) {
var angle = Math.random()*Math.PI*2;
var speed = 4+Math.random()*22;
p.x=CX; p.y=CY; p.vx=Math.cos(angle)*speed; p.vy=Math.sin(angle)*speed;
p.r=2+Math.random()*5; p.life=1; p.trail=[];
p.color='hsla('+(40+Math.random()*40)+',100%,75%,1)';
},
draw: function(ctx,p) {
// Draw trail
for(var i=0;i<p.trail.length;i++){
ctx.globalAlpha=(i/p.trail.length)*p.life*0.4;
ctx.beginPath(); ctx.arc(p.trail[i].x,p.trail[i].y,p.r*(i/p.trail.length),0,Math.PI*2);
ctx.fillStyle=p.color; ctx.fill();
}
ctx.globalAlpha=p.life;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fillStyle=p.color; ctx.fill();
// Star glow
var g=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,p.r*4);
g.addColorStop(0,'hsla(50,100%,90%,'+(p.life*0.4)+')'); g.addColorStop(1,'transparent');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(p.x,p.y,p.r*4,0,Math.PI*2); ctx.fill();
},
tick: function(p) { p.trail.push({x:p.x,y:p.y}); if(p.trail.length>8)p.trail.shift(); p.x+=p.vx; p.y+=p.vy; p.vy+=0.1; p.life-=0.009; }
},
lightning: {
init: function(p) {
p.x=Math.random()*W; p.y=0; p.targetY=H;
p.segs=[]; p.life=1; p.width=1+Math.random()*3;
p.color=Math.random()<0.5?'#faff00':'#a0ffff';
// Generate zigzag path
var cx=p.x, steps=20;
for(var i=0;i<=steps;i++){
p.segs.push({x:cx+(Math.random()-.5)*80,y:(H/steps)*i});
cx+=(Math.random()-.5)*60;
}
},
draw: function(ctx,p) {
ctx.globalAlpha=p.life;
ctx.strokeStyle=p.color; ctx.lineWidth=p.width;
ctx.shadowBlur=20; ctx.shadowColor=p.color;
ctx.beginPath();
for(var i=0;i<p.segs.length;i++) i===0?ctx.moveTo(p.segs[i].x,p.segs[i].y):ctx.lineTo(p.segs[i].x,p.segs[i].y);
ctx.stroke(); ctx.shadowBlur=0;
},
tick: function(p) { p.life-=0.04; }
},
money: {
init: function(p) {
p.x=Math.random()*W; p.y=-20;
p.vx=(Math.random()-.5)*4; p.vy=4+Math.random()*8;
p.rot=Math.random()*360; p.rotV=(Math.random()-.5)*8;
p.size=20+Math.random()*20; p.life=1;
p.symbol=['💵','💴','💶','💷','💰','$','€'][Math.random()*7|0];
},
draw: function(ctx,p) {
ctx.globalAlpha=p.life; ctx.save();
ctx.translate(p.x,p.y); ctx.rotate(p.rot*Math.PI/180);
ctx.font=p.size+'px serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(p.symbol,0,0); ctx.restore();
},
tick: function(p) { p.x+=p.vx; p.y+=p.vy; p.vy+=0.1; p.rot+=p.rotV; if(p.y>H-40)p.life-=0.04; else p.life-=0.004; }
},
hearts: {
init: function(p) {
var angle=Math.random()*Math.PI*2, speed=5+Math.random()*20;
p.x=CX+(Math.random()-.5)*100; p.y=CY+(Math.random()-.5)*100;
p.vx=Math.cos(angle)*speed; p.vy=Math.sin(angle)*speed-4;
p.size=14+Math.random()*28; p.life=1; p.rotV=(Math.random()-.5)*6;
p.rot=Math.random()*360;
p.color='hsl('+(340+Math.random()*30)+',90%,'+(60+Math.random()*20)+'%)';
},
draw: function(ctx,p) {
ctx.globalAlpha=p.life; ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot*Math.PI/180);
var s=p.size/14;
ctx.fillStyle=p.color;
ctx.beginPath();
ctx.moveTo(0,-s*3);
ctx.bezierCurveTo(s*4,-s*8,s*10,-s*2,0,s*5);
ctx.bezierCurveTo(-s*10,-s*2,-s*4,-s*8,0,-s*3);
ctx.fill();
ctx.restore();
},
tick: function(p) { p.x+=p.vx; p.y+=p.vy; p.vy+=0.25; p.vx*=0.99; p.rot+=p.rotV; p.life-=0.008; }
},
rage: {
init: function(p) {
var angle=Math.random()*Math.PI*2, speed=10+Math.random()*25;
p.x=CX; p.y=CY; p.vx=Math.cos(angle)*speed; p.vy=Math.sin(angle)*speed;
p.r=8+Math.random()*24; p.life=1;
p.hue=0+Math.random()*30; p.flash=Math.random()<0.3;
},
draw: function(ctx,p) {
if(p.flash){
ctx.globalAlpha=p.life*0.6;
ctx.fillStyle='rgba(255,0,0,0.15)'; ctx.fillRect(0,0,W,H);
}
var g=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,p.r);
g.addColorStop(0,'hsla('+p.hue+',100%,70%,'+(p.life)+')');
g.addColorStop(1,'transparent');
ctx.globalAlpha=p.life; ctx.fillStyle=g;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
},
tick: function(p) { p.x+=p.vx; p.y+=p.vy; p.vy+=0.6; p.vx*=0.96; p.r*=0.97; p.life-=0.013; }
},
galaxy: {
init: function(p) {
var angle=Math.random()*Math.PI*2, dist=20+Math.random()*Math.min(W,H)*0.5;
p.x=CX+Math.cos(angle)*dist; p.y=CY+Math.sin(angle)*dist;
var tangent=angle+Math.PI/2;
var speed=1+Math.random()*4;
p.vx=Math.cos(tangent)*speed-(p.x-CX)*0.005;
p.vy=Math.sin(tangent)*speed-(p.y-CY)*0.005;
p.r=1+Math.random()*3; p.life=1;
p.hue=220+Math.random()*120; p.trail=[];
},
draw: function(ctx,p) {
for(var i=0;i<p.trail.length;i++){
ctx.globalAlpha=(i/p.trail.length)*p.life*0.5;
ctx.beginPath(); ctx.arc(p.trail[i].x,p.trail[i].y,p.r*0.5,0,Math.PI*2);
ctx.fillStyle='hsl('+p.hue+',80%,70%)'; ctx.fill();
}
ctx.globalAlpha=p.life;
var g=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,p.r*2);
g.addColorStop(0,'hsla('+p.hue+',90%,85%,'+p.life+')'); g.addColorStop(1,'transparent');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(p.x,p.y,p.r*2,0,Math.PI*2); ctx.fill();
},
tick: function(p) { p.trail.push({x:p.x,y:p.y}); if(p.trail.length>12)p.trail.shift(); p.x+=p.vx; p.y+=p.vy; p.vx-=(p.x-CX)*0.0008; p.vy-=(p.y-CY)*0.0008; p.life-=0.007; }
},
sparkles: {
init: function(p) {
p.x=CX + (Math.random()-0.5)*200;
p.y=CY + (Math.random()-0.5)*120;
p.vx=(Math.random()-0.5)*4; p.vy=(Math.random()-0.5)*4;
p.r=1+Math.random()*3; p.life=1; p.hue=Math.random()*360;
},
draw: function(ctx,p) {
ctx.globalAlpha=p.life;
ctx.fillStyle='hsla('+p.hue+',100%,80%,'+p.life+')';
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
},
tick: function(p) { p.x+=p.vx; p.y+=p.vy; p.vy+=0.05; p.r*=0.98; p.life-=0.01; }
},
shatter: {
init: function(p) {
var ang=Math.random()*Math.PI*2;
var sp=3+Math.random()*8;
p.x=CX; p.y=CY; p.vx=Math.cos(ang)*sp; p.vy=Math.sin(ang)*sp;
p.w=4+Math.random()*8; p.h=4+Math.random()*8; p.rot=Math.random()*360; p.rotV=(Math.random()-0.5)*10;
p.life=1; p.color=['#f87171','#fb923c','#facc15','#34d399','#60a5fa','#c084fc'][Math.random()*6|0];
},
draw: function(ctx,p) {
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot*Math.PI/180);
ctx.globalAlpha=p.life;
ctx.fillStyle=p.color; ctx.fillRect(-p.w/2,-p.h/2,p.w,p.h);
ctx.restore();
},
tick: function(p) { p.x+=p.vx; p.y+=p.vy; p.vy+=0.2; p.vx*=0.99; p.life-=0.015; p.rot+=p.rotV; }
},
neon: {
init: function(p) {
p.x=Math.random()*W; p.y=Math.random()*H;
p.r=8+Math.random()*20; p.life=1; p.hue=150+Math.random()*120;
},
draw: function(ctx,p) {
ctx.beginPath();
var grd=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,p.r);
grd.addColorStop(0,'hsla('+p.hue+',100%,90%,'+p.life+')');
grd.addColorStop(0.4,'hsla('+p.hue+',100%,70%,'+p.life*0.7+')');
grd.addColorStop(1,'transparent');
ctx.fillStyle=grd; ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
},
tick: function(p) { p.life-=0.01; p.r*=0.99; }
}
};
var cfg = EFFECTS[effect] || EFFECTS.confetti;
// Spawn particles
var count = {confetti:300,fire:120,stars:120,lightning:25,money:80,hearts:150,rage:160,galaxy:200,sparkles:180,shatter:220,neon:160}[effect]||200;
var particles = [];
for(var i=0;i<count;i++){
var p={};
cfg.init(p);
particles.push(p);
}
// Staggered spawn for some effects
var spawned = count;
if(effect==='confetti'||effect==='money'||effect==='hearts'){
particles=[]; spawned=0;
var spawnInt=setInterval(function(){
for(var j=0;j<12&&spawned<count;j++,spawned++){
var p={}; cfg.init(p); particles.push(p);
}
if(spawned>=count) clearInterval(spawnInt);
},30);
}
// Flash overlay for impact
if(effect!=='lightning'){
ctx.fillStyle='rgba(255,255,255,0.18)'; ctx.fillRect(0,0,W,H);
}
var frame=0;
function animate(){
raf=requestAnimationFrame(animate);
frame++;
// Fade-clear (trail effect for some)
if(effect==='galaxy'||effect==='stars'){
ctx.fillStyle='rgba(0,0,0,0.12)'; ctx.fillRect(0,0,W,H);
} else {
ctx.clearRect(0,0,W,H);
}
ctx.globalAlpha=1;
particles=particles.filter(function(p){ return p.life>0; });
particles.forEach(function(p){
cfg.tick(p);
ctx.save(); ctx.globalAlpha=1;
cfg.draw(ctx,p);
ctx.restore();
});
// Stop when all particles done or max time
if((particles.length===0&&spawned>=count)||frame>300){
cancelAnimationFrame(raf);
cv.style.transition='opacity 0.5s';
cv.style.opacity='0';
setTimeout(function(){ if(cv.parentNode) cv.remove(); }, 550);
}
}
animate();
}
// ─── LEADERBOARD ──────────────────────────────────────────────────────────
function buildLeaderboard() {
if(!toggles.leaderboard) return;
var ex=document.getElementById('km-leaderboard'); if(ex) ex.remove();
var el=document.createElement('div'); el.id='km-leaderboard';
el.style.position = 'fixed';
el.style.top = '12px';
el.style.left = '12px';
el.style.zIndex = '99996';
el.style.cursor = 'move';
el.style.minWidth = '260px';
el.style.maxWidth = '500px';
document.body.appendChild(el);
makeDraggable(el, el);
updateLeaderboard();
}
function updateLeaderboard() {
var el=document.getElementById('km-leaderboard'); if(!el) return;
if(!toggles.leaderboard){el.style.display='none';return;}
el.style.display='block';
pluginWinPredictorUpdate();
el.style.display='block';
var meName=streamerName(matchPlayers.me||(playerData?playerData.Nickname:'You'));
var oppName=streamerName(matchPlayers.opponent||'Opponents');
var meTeam=matchPlayers.myTeam||currentMode||'Team';
var oppTeam=matchPlayers.oppTeam||'Opponents';
var oppMMRStr=opponentMMR ? streamerMMR(opponentMMR) : '?';
var myCol=matchScore.opp>matchScore.me?'#f87171':'#4ade80';
var oppCol=matchScore.opp>matchScore.me?'#4ade80':'#f87171';
var p = getPreset();
var bg = p.tabBg || 'rgba(12,14,20,0.95)';
var bd = p.border || 'rgba(255,255,255,0.12)';
el.style.background = bg;
el.style.border = '1px solid '+bd;
el.style.boxShadow = '0 8px 24px rgba(0,0,0,0.4)';
var modeLabel = currentMode ? currentMode.toUpperCase() : 'MATCH';
var scoreText = '<div style="font-size:14px;font-weight:700;color:'+theme.accent+';margin-bottom:4px;">'+modeLabel+' SCORE</div>';
el.innerHTML = '<div style="font-family:DM Mono,monospace;font-size:12px;color:'+p.text+';margin-bottom:6px;line-height:1.2;">'+scoreText
+'<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;">'
+'<div style="min-width:120px;"><div style="font-size:10px;color:rgba(255,255,255,0.5);">'+meTeam+'</div><div style="font-size:14px;font-weight:700;color:'+myCol+';">'+meName+'</div></div>'
+'<div style="font-size:22px;font-weight:800;color:'+theme.accent+';">'+matchScore.me+' – '+matchScore.opp+'</div>'
+'<div style="min-width:120px;text-align:right;"><div style="font-size:10px;color:rgba(255,255,255,0.5);">'+oppTeam+'</div><div style="font-size:14px;font-weight:700;color:'+oppCol+';">'+oppName+'</div><div style="font-size:10px;color:rgba(255,255,255,0.45);">'+oppMMRStr+' MMR</div></div>'
+'</div>';
}
function removeLeaderboard(){var el=document.getElementById('km-leaderboard');if(el)el.remove();}
var welcomeShown=false;
function showWelcomeBanner() {
if(!playerData||welcomeShown) return; welcomeShown=true;
var rank=playerData.ModesGlicko&&playerData.ModesGlicko['Competitive3v3']?getRank(playerData.ModesGlicko['Competitive3v3'].displayRating):null;
var el=document.createElement('div'); el.id='km-welcome';
el.innerHTML='Welcome back, <strong>'+(playerData.Nickname||'Player')+'</strong>'+(rank?' '+rank.emoji+' '+rank.name:'');
document.body.appendChild(el);
setTimeout(function(){el.style.opacity='0';},3000);
setTimeout(function(){if(el.parentNode)el.remove();},3600);
}
function takeScreenshot() {
var canvas=document.querySelector('canvas'); if(!canvas){showToast('No canvas found','warn');return;}
try{var a=document.createElement('a');a.download='keymod-'+Date.now()+'.png';a.href=canvas.toDataURL('image/png');a.click();showToast('Screenshot saved!','ok');}
catch(e){showToast('Screenshot failed','err');}
}
function markClip() {
var secs=Math.round((Date.now()-session.startTime)/1000);
var stamp=Math.floor(secs/60)+':'+String(secs%60).padStart(2,'0');
clipMarks.push(stamp); showToast('Clip marked at '+stamp,'ok'); updateClipsBody();
}
// ─── COPY STATS ───────────────────────────────────────────────────────────
function copyStats() {
var nick=playerData?playerData.Nickname:'Unknown';
var mmr3v3=playerData&&playerData.ModesGlicko&&playerData.ModesGlicko['Competitive3v3']?playerData.ModesGlicko['Competitive3v3'].displayRating:'?';
var rank=getRank(mmr3v3);
var acc=session.shots>0?Math.round((session.goals/session.shots)*100)+'%':'—';
var tot=session.wins+session.losses;
var wr=tot>0?Math.round((session.wins/tot)*100)+'%':'—';
var lines=[
'# '+nick+' — KeyMod Stats',
'',
'## 📊 Session',
'**Matches:** '+session.matches+' | **W:** '+session.wins+' | **L:** '+session.losses+' | **WR:** '+wr,
'**Goals:** '+session.goals+' | **Conceded:** '+session.conceded+' | **Diff:** '+(session.goals-session.conceded>=0?'+':'')+(session.goals-session.conceded),
'**Shots:** '+session.shots+' | **Accuracy:** '+acc,
'**Streak:** '+session.streak+' | **Best:** '+session.maxStreak+' | **Comebacks:** '+session.comebacks,
'**XP Gained:** +'+session.xpGained,
'',
'## 🏆 Rank (3v3)',
rank.emoji+' **'+rank.name+'** — '+mmr3v3+' MMR',
'',
'## ⏱ Session Time',
Math.floor((Date.now()-session.startTime)/60000)+'m played',
'',
'*via KeyMod by @keydopz*',
];
var text=lines.join('\n');
try{
navigator.clipboard.writeText(text).then(function(){showToast('Stats copied!','ok');}).catch(function(){fallbackCopy(text);});
}catch(e){fallbackCopy(text);}
}
function fallbackCopy(text) {
var ta=document.createElement('textarea'); ta.value=text;
ta.style.position='fixed'; ta.style.opacity='0';
document.body.appendChild(ta); ta.select();
document.execCommand('copy'); ta.remove();
showToast('Stats copied!','ok');
}
// ─── LOADING SCREEN ───────────────────────────────────────────────────────
function buildLoadingScreen() {
// Loading screen removed by user request.
dismissed = true;
return;
}
function showTikTokNotification(title, message) {
var old = document.getElementById('km-tiktok-notif'); if (old) old.remove();
var n = document.createElement('div'); n.id='km-tiktok-notif';
n.style.cssText = 'position:fixed;bottom:16px;right:16px;z-index:999999;font-family:"DM Sans",sans-serif;pointer-events:auto;max-width:320px;border-radius:14px;background:rgba(20,20,28,0.96);border:1px solid rgba(255,255,255,0.08);box-shadow:0 12px 30px rgba(0,0,0,0.35);padding:10px 12px;backdrop-filter:blur(8px);';
n.innerHTML = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;"><div style="width:34px;height:34px;border-radius:50%;background:#ff005e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:16px;">✓</div><div><div style="font-size:13px;font-weight:700;color:#fff;">'+title+'</div><div style="font-size:11px;color:#d1d5db;">'+message+'</div></div></div>';
document.body.appendChild(n);
setTimeout(function(){if(n.parentNode)n.remove();},5000);
}
function dismiss() {
if(dismissed) return; dismissed=true;
var bar=document.getElementById('km-splash-bar'), st=document.getElementById('km-splash-status');
var sub=document.getElementById('km-update-subtitle');
if(bar){bar.style.transition='width 0.4s ease';bar.style.width='100%';}
if(st) st.textContent='Ready.';
if(sub) sub.textContent='Update complete.';
// Wait longer before fading — gives Unity canvas time to render first frame
// If we fade too fast, we reveal a black canvas underneath
setTimeout(function(){
var el=document.getElementById('km-splash'); if(!el) return;
// Check if the game canvas has rendered (non-zero size indicates Unity is up)
var cv = document.querySelector('canvas');
var canvasReady = cv && cv.offsetWidth > 100;
var delay = canvasReady ? 0 : 1500; // wait if canvas not ready yet
setTimeout(function(){
var el2=document.getElementById('km-splash'); if(!el2) return;
el2.style.transition='opacity 1s ease';
el2.style.opacity='0';
setTimeout(function(){ var e=document.getElementById('km-splash'); if(e) e.remove(); },1050);
}, delay);
}, 600);
}
// ─── HUD ──────────────────────────────────────────────────────────────────
// ── Always-on FPS counter ─────────────────────────────────────────────────
var _hudFpsFrames = 0, _hudFpsLast = 0, _hudFps = 0;
function _hudFpsTick(now) {
_hudFpsFrames++;
if (!_hudFpsLast) _hudFpsLast = now;
var elapsed = now - _hudFpsLast;
if (elapsed >= 500) { // update every half-second for responsiveness
_hudFps = Math.round(_hudFpsFrames * 1000 / elapsed);
_hudFpsFrames = 0;
_hudFpsLast = now;
var el = document.getElementById('km-hud-fps');
if (el) {
el.textContent = _hudFps + ' fps';
el.style.color = _hudFps >= 55 ? '#4ade80' : _hudFps >= 30 ? '#fbbf24' : '#f87171';
}
// Also feed perfMetrics if overlay is open
perfFPS = _hudFps;
}
requestAnimationFrame(_hudFpsTick);
}
function buildHUD() {
var el=document.createElement('div'); el.id='km-hud';
el.innerHTML=[
'<span id="km-hud-rank"></span>',
'<span id="km-hud-fps" style="font-variant-numeric:tabular-nums">— fps</span>',
'<span id="km-hud-sep" style="color:rgba(255,255,255,0.15);font-size:10px;margin:0 1px">|</span>',
'<span id="km-hud-ping" style="font-variant-numeric:tabular-nums">—ms</span>',
'<span id="km-hud-party" style="display:none;color:#f5c518;font-weight:600;font-size:11px;margin-left:4px"></span>',
].join('');
document.body.appendChild(el);
el.addEventListener('click',function(){toggleMenu();});
// Start always-on FPS counter
requestAnimationFrame(_hudFpsTick);
// Start always-on ping (3s interval, derives from real game XHRs)
pingStart();
}
function updateHUDRank() {
var el=document.getElementById('km-hud-rank'); if(!el) return;
if(playerData&&playerData.ModesGlicko&&playerData.ModesGlicko['Competitive3v3']) {
var r=getRank(playerData.ModesGlicko['Competitive3v3'].displayRating);
el.textContent=r.emoji; el.title=r.name;
// Hot streak glow
if(toggles.hotStreak&&session.streak>=3) el.style.filter='drop-shadow(0 0 6px #f97316)';
else el.style.filter='none';
}
}
// ─── MENU ─────────────────────────────────────────────────────────────────
(function() {
// Mount car leveler on boot if installed
})();
(function() {
if (isInstalled('visualFX')) setTimeout(pluginVFXMount, 200);
// Themes disabled
})();
function buildMenu() {
var el=document.createElement('div'); el.id='km-menu';
// Prevent Unity canvas from stealing focus when interacting with menu
el.addEventListener('mousedown', function(e){e.stopPropagation();});
el.addEventListener('click', function(e){e.stopPropagation();});
el.addEventListener('keydown', function(e){e.stopPropagation();});
el.addEventListener('keyup', function(e){
// Allow F2/backtick/1 to still close menu
if(e.key!=='F2') e.stopPropagation();
});
el.innerHTML=[
'<div id="km-header">',
'<span id="km-title">KeyMod</span><span id="km-wip-badge">v0.1 beta</span>',
'<div id="km-header-right">',
'<button id="km-discord-btn" title="Copy party code for Discord">',
'<span style="font-size:14px">🎮</span>',
'<span id="km-discord-label">No Party</span>',
'</button>',
'<span id="km-ping">—ms</span>',
'<button id="km-fullscreen-btn" title="Toggle fullscreen" style="background:none;border:none;color:rgba(255,255,255,0.4);font-size:12px;cursor:pointer;padding:4px 6px;transition:color 0.1s;">⛶</button>',
// macOS traffic light trio — hidden unless macOS14 theme active (CSS shows them)
'<div id="km-traffic-lights" style="display:none;align-items:center;gap:6px;margin-right:2px;">',
'<button id="km-tl-red" title="Close" style="width:12px;height:12px;border-radius:50%;background:#ff5f57;border:0.5px solid rgba(0,0,0,0.15);padding:0;cursor:pointer;"></button>',
'<button id="km-tl-yellow" title="Minimize" style="width:12px;height:12px;border-radius:50%;background:#ffbd2e;border:0.5px solid rgba(0,0,0,0.15);padding:0;cursor:pointer;"></button>',
'<button id="km-tl-green" title="Fullscreen" style="width:12px;height:12px;border-radius:50%;background:#28c840;border:0.5px solid rgba(0,0,0,0.15);padding:0;cursor:pointer;"></button>',
'</div>',
'<button id="km-close">✕</button>',
'</div>',
'</div>',
'<div id="km-tip-bar"><span id="km-tip-text"></span><button id="km-tip-close">✕</button></div>',
'<div id="km-tabs-wrap">',
'<button id="km-tab-left" class="km-tab-arrow">‹</button>',
'<div id="km-tabs">',
'<button class="kmt active" data-tab="ranks">Ranked</button>',
'<button class="kmt" data-tab="ranked">Competitive</button>',
'<button class="kmt" data-tab="session">Session</button>',
'<button class="kmt" data-tab="profile">Profile</button>',
'<button class="kmt" data-tab="history">History</button>',
'<button class="kmt" data-tab="rivals">Rivals</button>',
'<button class="kmt" data-tab="party">Party</button>',
'<button class="kmt" data-tab="tools">Tools</button>',
'<button class="kmt" data-tab="misc">Misc</button>',
'<button class="kmt" data-tab="settings">Settings</button>',
'<button class="kmt" data-tab="pluginstore">Plugin Store</button>',
'<button class="kmt" data-tab="ownedplugins">Owned Plugins</button>',
'<button class="kmt" data-tab="console">Console</button>',
'<button class="kmt" data-tab="credits">Credits</button>',
'</div>',
'<button id="km-tab-right" class="km-tab-arrow">›</button>',
'</div>',
// RANKS
'<div class="km-pane active" id="km-pane-ranks"><div id="km-ranks-body"><div class="km-empty">Waiting for login...</div></div></div>',
// RANKED
'<div class="km-pane" id="km-pane-ranked">',
'<div class="km-section">Opponent MMR</div>',
'<div id="km-opp-body"><div class="km-empty">Play a match</div></div>',
'<div class="km-section">Competitive Split</div>',
'<div id="km-split-body"><div class="km-empty">Login first</div></div>',
'<div class="km-section">Daily Goal</div>',
'<div class="km-stat-row"><span>Target Wins</span><input id="km-daily-goal" type="number" min="1" max="50" value="5" class="km-input"></div>',
'<div id="km-daily-progress"></div>',
'<div class="km-section">Heatmap</div>',
'<div id="km-heatmap-body"></div>',
'</div>',
// SESSION
'<div class="km-pane" id="km-pane-session">',
'<div class="km-section">This Session</div>',
'<div class="km-stat-row"><span>Goals Scored</span><span id="ss-goals">0</span></div>',
'<div class="km-stat-row"><span>Goals Conceded</span><span id="ss-conceded">0</span></div>',
'<div class="km-stat-row"><span>Goal Diff</span><span id="ss-diff">0</span></div>',
'<div class="km-stat-row"><span>Shot Accuracy</span><span id="ss-acc">—</span></div>',
'<div class="km-stat-row"><span>Shots</span><span id="ss-shots">0</span></div>',
'<div class="km-stat-row"><span>Matches</span><span id="ss-matches">0</span></div>',
'<div class="km-stat-row"><span>Wins</span><span id="ss-wins" class="col-green">0</span></div>',
'<div class="km-stat-row"><span>Losses</span><span id="ss-losses" class="col-red">0</span></div>',
'<div class="km-stat-row"><span>Win Rate</span><span id="ss-wr">—</span></div>',
'<div class="km-stat-row"><span>Win Streak</span><span id="ss-streak">0</span></div>',
'<div class="km-stat-row"><span>Best Streak</span><span id="ss-maxstreak">0</span></div>',
'<div class="km-stat-row"><span>Comebacks</span><span id="ss-comebacks">0</span></div>',
'<div class="km-stat-row"><span>Perfect Games</span><span id="ss-perfect">0</span></div>',
'<div class="km-stat-row"><span>XP Gained</span><span id="ss-xp">0</span></div>',
'<div class="km-stat-row"><span>Last Mode</span><span id="ss-mode">—</span></div>',
'<div class="km-stat-row"><span>Time Played</span><span id="ss-time">0m</span></div>',
'<div class="km-stat-row"><span>Longest Session</span><span id="ss-longest">0m</span></div>',
'<button class="km-btn" id="km-copy-stats">📋 Copy Stats for Discord</button>',
'</div>',
// PROFILE
'<div class="km-pane" id="km-pane-profile">',
'<div class="km-section">Your Card</div>',
'<div class="km-stat-row"><span>Banner Color</span><input type="color" id="prof-color" class="km-color" value="#1e3a5f"></div>',
'<div class="km-stat-row"><span>Banner Title</span><input type="text" id="prof-title" class="km-input" placeholder="Your name on the banner"></div>',
'<div class="km-stat-row" style="align-items:flex-start;"><span>Description</span><div style="flex:1;display:flex;gap:6px;"><textarea id="prof-desc" class="km-input" style="height:64px;resize:vertical;" placeholder="Custom profile description"></textarea><button id="btn-desc-osk" class="km-btn" style="height:30px;padding:0 10px;align-self:flex-start;">⌨</button></div></div>',
'<div class="km-section" style="margin-top:4px">Goal Emoji</div>',
'<div class="km-emoji-grid" id="km-emoji-grid"></div>',
'<button class="km-btn" id="btn-save-profile">Save Banner Settings</button>',
'<div id="km-banner-preview" class="km-banner-preview-box">',
'<div style="font-size:11px;color:rgba(255,255,255,0.3)">Banner preview</div>',
'</div>',
'<div class="km-section" style="margin-top:8px">Stats</div>',
'<div id="km-profile-body"><div class="km-empty">Login first</div></div>',
'<div class="km-section">XP History</div>',
'<div id="km-xp-history-body"><div class="km-empty">No XP data yet</div></div>',
'<div class="km-section">Rank History</div>',
'<div id="km-rank-history-body"><div class="km-empty">No rank changes yet</div></div>',
'<button class="km-btn" id="km-export-card">📄 Export Profile Card</button>',
'</div>',
// HISTORY
'<div class="km-pane" id="km-pane-history">',
'<div class="km-section">Match History</div>',
'<div id="km-history-body"></div>',
'<button class="km-btn danger" id="km-clear-history">Clear History</button>',
'</div>',
// RIVALS
'<div class="km-pane" id="km-pane-rivals">',
'<div class="km-section">Rivals (3+ matches)</div>',
'<div id="km-rivals-body"><div class="km-empty">Face opponents repeatedly to build rivals</div></div>',
'<div class="km-section">Opponent Log</div>',
'<div id="km-opp-log-body"><div class="km-empty">No opponents logged yet</div></div>',
'</div>',
// PARTY
'<div class="km-pane" id="km-pane-party">',
'<div class="km-section">Current Party</div>',
'<div id="km-party-status"></div>',
'<div class="km-section">Recent Codes</div>',
'<div id="km-recent-codes"><div class="km-empty">No recent codes</div></div>',
'</div>',
// TOOLS
'<div class="km-pane" id="km-pane-tools">',
'<div class="km-section">Screenshot</div>',
'<button class="km-btn" id="km-screenshot">📷 Save Screenshot</button>',
'<div class="km-section">Clip Markers</div>',
'<button class="km-btn" id="km-clip">▶ Mark Clip Timestamp</button>',
'<div id="km-clips-body"></div>',
'<button class="km-btn danger" id="km-clear-clips">Clear Clips</button>',
'<div class="km-section">Peak MMR</div>',
'<div id="km-peak-body"></div>',
'<button class="km-btn danger" id="km-clear-peak">Reset Peak MMR</button>',
'</div>',
// MISC
'<div class="km-pane" id="km-pane-misc">',
'<div class="km-section">Feature Toggles</div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="tog-leaderboard"> Leaderboard overlay</label></div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="tog-goalMsg"> Goal message</label></div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="tog-endMsg"> End match message</label></div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="tog-oppMMR"> Opponent MMR</label></div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="tog-matchNotifs"> Match notifications</label></div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="tog-hotStreak"> Hot streak HUD glow</label></div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="tog-coldStreak"> Cold streak warning</label></div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="tog-rivalNotif"> Rival notifications</label></div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="tog-perfectNotif"> Perfect game alert</label></div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="tog-rankNotif"> Rank up/down alert</label></div>',
'<div class="km-section">Custom Messages</div>',
'<div class="km-stat-row"><span>Goal</span>'
+'<select id="msg-goal" class="km-select" style="flex:1;max-width:160px">' +'<option value="'+encodeURIComponent('GOAL!')+'"'+(customMsgs.goal==='GOAL!'?' selected':'')+'>GOAL!</option>'+'<option value="'+encodeURIComponent('LETS GO!')+'"'+(customMsgs.goal==='LETS GO!'?' selected':'')+'>LETS GO!</option>'+'<option value="'+encodeURIComponent('YEAH!')+'"'+(customMsgs.goal==='YEAH!'?' selected':'')+'>YEAH!</option>'+'<option value="'+encodeURIComponent('EASY!')+'"'+(customMsgs.goal==='EASY!'?' selected':'')+'>EASY!</option>'+'<option value="'+encodeURIComponent('GET REKT')+'"'+(customMsgs.goal==='GET REKT'?' selected':'')+'>GET REKT</option>'+'<option value="'+encodeURIComponent('SEND IT')+'"'+(customMsgs.goal==='SEND IT'?' selected':'')+'>SEND IT</option>'+'<option value="'+encodeURIComponent('BANGER')+'"'+(customMsgs.goal==='BANGER'?' selected':'')+'>BANGER</option>'+'<option value="'+encodeURIComponent('INSANE')+'"'+(customMsgs.goal==='INSANE'?' selected':'')+'>INSANE</option>'+'<option value="'+encodeURIComponent('NO CAP')+'"'+(customMsgs.goal==='NO CAP'?' selected':'')+'>NO CAP</option>'+'<option value="'+encodeURIComponent('COOKED')+'"'+(customMsgs.goal==='COOKED'?' selected':'')+'>COOKED</option>' +'</select></div>',
'<div class="km-stat-row"><span>Win</span>'
+'<select id="msg-win" class="km-select" style="flex:1;max-width:160px">' +'<option value="'+encodeURIComponent('GG EZ')+'"'+(customMsgs.win==='GG EZ'?' selected':'')+'>GG EZ</option>'+'<option value="'+encodeURIComponent('GG WP')+'"'+(customMsgs.win==='GG WP'?' selected':'')+'>GG WP</option>'+'<option value="'+encodeURIComponent('TOO EASY')+'"'+(customMsgs.win==='TOO EASY'?' selected':'')+'>TOO EASY</option>'+'<option value="'+encodeURIComponent('W')+'"'+(customMsgs.win==='W'?' selected':'')+'>W</option>'+'<option value="'+encodeURIComponent('SKILL GAP')+'"'+(customMsgs.win==='SKILL GAP'?' selected':'')+'>SKILL GAP</option>'+'<option value="'+encodeURIComponent('UNMATCHED')+'"'+(customMsgs.win==='UNMATCHED'?' selected':'')+'>UNMATCHED</option>'+'<option value="'+encodeURIComponent('STAY MAD')+'"'+(customMsgs.win==='STAY MAD'?' selected':'')+'>STAY MAD</option>'+'<option value="'+encodeURIComponent('DOMINANT')+'"'+(customMsgs.win==='DOMINANT'?' selected':'')+'>DOMINANT</option>'+'<option value="'+encodeURIComponent('CLEAN')+'"'+(customMsgs.win==='CLEAN'?' selected':'')+'>CLEAN</option>'+'<option value="'+encodeURIComponent('GOAT DIFF')+'"'+(customMsgs.win==='GOAT DIFF'?' selected':'')+'>GOAT DIFF</option>' +'</select></div>',
'<div class="km-stat-row"><span>Loss</span>'
+'<select id="msg-loss" class="km-select" style="flex:1;max-width:160px">' +'<option value="'+encodeURIComponent('GG')+'"'+(customMsgs.loss==='GG'?' selected':'')+'>GG</option>'+'<option value="'+encodeURIComponent('UNLUCKY')+'"'+(customMsgs.loss==='UNLUCKY'?' selected':'')+'>UNLUCKY</option>'+'<option value="'+encodeURIComponent('NEXT')+'"'+(customMsgs.loss==='NEXT'?' selected':'')+'>NEXT</option>'+'<option value="'+encodeURIComponent('RIGGED')+'"'+(customMsgs.loss==='RIGGED'?' selected':'')+'>RIGGED</option>'+'<option value="'+encodeURIComponent('DIFF')+'"'+(customMsgs.loss==='DIFF'?' selected':'')+'>DIFF</option>'+'<option value="'+encodeURIComponent('TOUCHING GRASS')+'"'+(customMsgs.loss==='TOUCHING GRASS'?' selected':'')+'>TOUCHING GRASS</option>'+'<option value="'+encodeURIComponent('GG WP')+'"'+(customMsgs.loss==='GG WP'?' selected':'')+'>GG WP</option>'+'<option value="'+encodeURIComponent('WAS COOKING')+'"'+(customMsgs.loss==='WAS COOKING'?' selected':'')+'>WAS COOKING</option>'+'<option value="'+encodeURIComponent('RUN IT BACK')+'"'+(customMsgs.loss==='RUN IT BACK'?' selected':'')+'>RUN IT BACK</option>'+'<option value="'+encodeURIComponent('HUMBLE')+'"'+(customMsgs.loss==='HUMBLE'?' selected':'')+'>HUMBLE</option>' +'</select></div>',
'<button class="km-btn" id="btn-save-msgs">Save Messages</button>',
'</div>',
// SETTINGS
'<div class="km-pane" id="km-pane-settings">',
'<div class="km-section">Themes & Loading</div>',
'<div style="font-size:12px;color:rgba(255,255,255,0.4);padding:10px 0 14px;line-height:1.7;">Themes and loading screens are managed via plugins. Install <strong>Themes</strong> or <strong>Loading Screen</strong> from the Plugin Store.</div>',
'<div class="km-section">Connection</div>',
'<div class="km-toggle-row"><label>'
+'<input type="checkbox" id="tog-autoReload"'+(toggles.autoReload!==false?' checked':'')+'>'+'Auto-reload on disconnect</label></div>',
'<div class="km-section">Menu</div>',
'<div class="km-toggle-row"><label><input type="checkbox" id="set-skip-splash"'+(lsGet('km_skipSplash',false)?' checked':'')+'>Skip loading screen next time</label></div>',
'</div>',
// PLUGIN STORE
'<div class="km-pane" id="km-pane-pluginstore">',
'<div class="km-section">Plugin Store</div>',
'<div style="position:relative;margin-bottom:6px;">',
'<input type="text" id="km-plugin-search" class="km-input" placeholder="tap ⌨ to search..." style="width:100%;box-sizing:border-box;cursor:pointer;padding-right:34px;" readonly>',
'<button id="km-osk-toggle" style="position:absolute;right:6px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:16px;color:rgba(255,255,255,0.4);padding:0;line-height:1;" title="On-screen keyboard">⌨</button>',
'</div>',
'<div id="km-osk-wrap" style="display:none;margin-bottom:8px;"></div>',
'<div id="km-plugin-store-grid"></div>',
'</div>',
// OWNED PLUGINS
'<div class="km-pane" id="km-pane-ownedplugins">',
'<div class="km-section">Installed Plugins</div>',
'<div id="km-owned-plugins-list"><div class="km-empty">No plugins installed yet</div></div>',
'</div>',
// CONSOLE
'<div class="km-pane" id="km-pane-console"><div id="km-console-body"></div></div>',
// CREDITS
'<div class="km-pane" id="km-pane-credits">',
'<div style="padding:8px 0 20px;text-align:center;">',
'<div style="font-size:22px;font-weight:700;color:#006eff;font-family:Consolas,monospace;letter-spacing:2px;margin-bottom:4px;">KeyMod</div>',
'<div style="font-size:11px;color:rgba(255,255,255,0.3);margin-bottom:24px;">v0.1 — RocketGoal.io KeyMod Beta</div>',
'<div class="km-section">Made by</div>',
'<div style="font-size:16px;font-weight:700;color:#fff;margin:6px 0 2px;">@keydopz - @key.dop2 - @key.dop</div>',
'<div style="font-size:11px;color:rgba(255,255,255,0.35);margin-bottom:20px;">TikTok · YouTube · Discord</div>',
'<div class="km-section">Built with</div>',
'<div class="km-stat-row"><span>Engine</span><span>Unity WebGL (Emscripten)</span></div>',
'<div class="km-stat-row"><span>Networking</span><span>Firebase + Photon</span></div>',
'<div class="km-stat-row"><span>Platform</span><span>Tampermonkey userscript</span></div>',
'<div class="km-stat-row"><span>Fonts</span><span>DM Mono, JetBrains Mono</span></div>',
'<div class="km-section">Special thanks</div>',
'<div style="font-size:12px;color:rgba(255,255,255,0.4);line-height:1.8;margin-bottom:20px;">',
'The RocketGoal.io development team<br>',
'The RocketGoal.io discord members<br>',
'BakkesMod for the plugin inspiration<br>',
'The RocketGoal.io community',
'</div>',
'<div style="font-size:10px;color:rgba(255,255,255,0.15);margin-top:8px;">',
'KeyMod is an independent fan project.<br>Not affiliated with Psyonix or Epic Games or RocketGoal.',
'</div>',
'</div>',
'</div>',
].join('');
document.body.appendChild(el);
bindMenuEvents(el);
setTimeout(function() { attachOSKToMenu(el); }, 100);
// Initial renders
consoleLogs.forEach(function(e){appendConsoleRow(e);});
if(playerData) refreshRanksTab();
updateHistoryTab(); updatePeakBody(); updateClipsBody();
updateSessionTab(); updateDailyProgress(); updateHeatmap();
updateProfileTab(); updateRivalsTab();
makeDraggable(el, el.querySelector('#km-header'));
setInterval(function(){
var t=document.getElementById('ss-time'), ls=document.getElementById('ss-longest');
var mins=Math.floor((Date.now()-session.startTime)/60000);
if(t) t.textContent=mins+'m';
if(ls) ls.textContent=Math.max(mins,session.longestSession)+'m';
updateLongestSession();
},15000);
}
function bindMenuEvents(el) {
// Tabs
el.querySelectorAll('.kmt').forEach(function(btn){
btn.addEventListener('click',function(){
el.querySelectorAll('.kmt').forEach(function(b){b.classList.remove('active');});
el.querySelectorAll('.km-pane').forEach(function(p){p.classList.remove('active');});
btn.classList.add('active'); activeTab=btn.dataset.tab;
var paneEl = document.getElementById('km-pane-'+activeTab); if(paneEl) paneEl.classList.add('active');
kmLog('Tab opened: ' + activeTab);
if(activeTab==='console') scrollConsole();
if(activeTab==='ranked') {updateRankedTab();updateDailyProgress();updateHeatmap();}
if(activeTab==='profile') updateProfileTab();
if(activeTab==='rivals') updateRivalsTab();
if(activeTab==='party') updatePartyTab();
if(activeTab==='pluginstore') renderPluginStore();
if(activeTab==='ownedplugins') renderOwnedPlugins();
if(activeTab==='credits') {} // static content, no render needed
if(activeTab==='tools') {updatePeakBody();updateClipsBody();}
});
});
// Arrows
var tabsEl=el.querySelector('#km-tabs');
el.querySelector('#km-tab-left').addEventListener('click',function(){tabsEl.scrollBy({left:-90,behavior:'smooth'});});
el.querySelector('#km-tab-right').addEventListener('click',function(){tabsEl.scrollBy({left:90,behavior:'smooth'});});
el.querySelector('#km-close').addEventListener('click',function(){toggleMenu(false);});
// macOS traffic light wiring
(function() {
var tlRed = el.querySelector('#km-tl-red');
var tlYellow = el.querySelector('#km-tl-yellow');
var tlGreen = el.querySelector('#km-tl-green');
if (tlRed) tlRed.addEventListener('click', function(){ toggleMenu(false); });
if (tlYellow) tlYellow.addEventListener('click', function(){
// Minimize = collapse to HUD only
var menu = document.getElementById('km-menu');
if (menu) { menu.style.opacity = menu.style.opacity === '0' ? '1' : '0'; }
});
if (tlGreen) tlGreen.addEventListener('click', function(){
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen && document.documentElement.requestFullscreen().catch(function(){});
} else {
document.exitFullscreen && document.exitFullscreen();
}
});
})();
// Fullscreen toggle
(function() {
var fsBtn = el.querySelector('#km-fullscreen-btn');
if (!fsBtn) return;
function updateFsIcon() {
fsBtn.textContent = document.fullscreenElement ? '⛶' : '⛶';
fsBtn.title = document.fullscreenElement ? 'Exit fullscreen' : 'Toggle fullscreen';
}
fsBtn.addEventListener('click', function() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen && document.documentElement.requestFullscreen().catch(function(){});
} else {
document.exitFullscreen && document.exitFullscreen();
}
});
document.addEventListener('fullscreenchange', updateFsIcon);
})();
// Tip bar
var tipText = el.querySelector('#km-tip-text');
var tipClose = el.querySelector('#km-tip-close');
if(tipText) {
menuTipIdx = (menuTipIdx + 1) % TIPS.length;
lsSet('menuTipIdx5', menuTipIdx);
tipText.textContent = TIPS[menuTipIdx];
}
if(tipClose) tipClose.addEventListener('click', function() {
var bar = document.getElementById('km-tip-bar');
if(bar) bar.style.display = 'none';
});
// Discord party button in header
var discordBtn = el.querySelector('#km-discord-btn');
if(discordBtn) discordBtn.addEventListener('click', function() {
if(currentPartyCode) {
var nick = playerData ? playerData.Nickname : 'me';
var msg = 'Join my RocketGoal.io party! Code: ' + currentPartyCode + ' (via KeyMod by @keydopz)';
navigator.clipboard.writeText(msg).catch(function(){fallbackCopy(msg);});
showToast('Party invite copied!', 'ok');
kmLog('Discord party invite copied from header');
} else {
showToast('No active party', 'warn');
}
});
el.querySelector('#km-discord-btn').addEventListener('click', function() {
if (!currentPartyCode) {
showToast('No party code yet — create a party first', 'warn');
return;
}
var nick = playerData ? (playerData.Nickname || 'me') : 'me';
var msg = '🎮 Join my RocketGoal.io party!\nCode: **' + currentPartyCode + '**\n*(invite from ' + nick + ' via KeyMod)*';
try {
navigator.clipboard.writeText(msg).then(function() {
showToast('📋 Party code copied for Discord!', 'ok');
kmLog('Party code copied: ' + currentPartyCode);
}).catch(function() { fallbackCopy(msg); showToast('📋 Copied!', 'ok'); });
} catch(e) { fallbackCopy(msg); showToast('📋 Copied!', 'ok'); }
});
el.querySelector('#km-screenshot').addEventListener('click',function(){kmLog('Screenshot taken');takeScreenshot();});
el.querySelector('#km-clip').addEventListener('click',function(){kmLog('Clip timestamp marked');markClip();});
el.querySelector('#km-copy-stats').addEventListener('click',function(){kmLog('Stats copied to clipboard');copyStats();});
el.querySelector('#km-clear-clips').addEventListener('click',function(){clipMarks=[];updateClipsBody();showToast('Clips cleared');kmLog('Clip marks cleared');});
el.querySelector('#km-clear-history').addEventListener('click',function(){matchHistory=[];lsSet('matchHistory5',[]);updateHistoryTab();showToast('History cleared');kmLog('Match history cleared');});
el.querySelector('#km-clear-peak').addEventListener('click',function(){peakMMR={};lsSet('peakMMR5',{});updatePeakBody();showToast('Peak MMR reset');kmLog('Peak MMR reset');});
el.querySelector('#km-export-card').addEventListener('click',exportProfileCard);
// Party tab
// Profile card settings
var profColor = el.querySelector('#prof-color');
var profText = el.querySelector('#prof-text');
if(profColor) profColor.value = profileCustom.bannerColor || '#1e3a5f';
// Emoji grid
var emojiGrid = el.querySelector('#km-emoji-grid');
if (emojiGrid) {
var emojis = ['⚽','🔥','💥','⚡','🎯','💫','🌟','👑','🏆','💀','🎮','🚀','❄️','🌊','☄️','🎉'];
emojiGrid.innerHTML = emojis.map(function(e) {
var active = (profileCustom.bannerEmoji||'⚽') === e ? ' active' : '';
return '<button class="km-emoji-btn'+active+'" data-emoji="'+e+'">'+e+'</button>';
}).join('');
emojiGrid.querySelectorAll('.km-emoji-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
emojiGrid.querySelectorAll('.km-emoji-btn').forEach(function(b){b.classList.remove('active');});
btn.classList.add('active');
});
});
}
el.querySelector('#btn-save-profile').addEventListener('click', function() {
profileCustom.bannerColor = profColor ? profColor.value : '#1e3a5f';
profileCustom.bannerText = el.querySelector('#prof-title') ? el.querySelector('#prof-title').value.trim() : '';
profileCustom.bannerDesc = el.querySelector('#prof-desc') ? el.querySelector('#prof-desc').value.trim() : '';
var selEmoji = el.querySelector('.km-emoji-btn.active');
if (selEmoji) profileCustom.bannerEmoji = selEmoji.dataset.emoji;
lsSet('profileCustom5', profileCustom);
kmLog('Profile card saved');
showToast('Profile card saved!', 'ok');
updateBannerPreview();
});
var btnDescOSK = el.querySelector('#btn-desc-osk');
if (btnDescOSK) {
btnDescOSK.addEventListener('click', function() {
var descInput = el.querySelector('#prof-desc');
if (!descInput) return;
var text = prompt('Type description:', descInput.value || '');
if (text !== null) {
descInput.value = text;
profileCustom.bannerDesc = text;
lsSet('profileCustom5', profileCustom);
updateBannerPreview();
}
});
}
updateBannerPreview();
// Goal sound is disabled - no event needed.
// Auto-reload toggle (still in settings pane)
var arCb = el.querySelector('#tog-autoReload');
if (arCb) {
arCb.addEventListener('change', function() {
toggles.autoReload = arCb.checked;
lsSet('toggles5', toggles);
});
}
// Toggles
var togMap={
'tog-leaderboard':'leaderboard','tog-goalMsg':'goalMsg','tog-endMsg':'endMsg',
'tog-oppMMR':'oppMMR','tog-matchNotifs':'matchNotifs','tog-hotStreak':'hotStreak',
'tog-coldStreak':'coldStreak','tog-rivalNotif':'rivalNotif',
'tog-perfectNotif':'perfectNotif','tog-rankNotif':'rankNotif',
};
Object.keys(togMap).forEach(function(id){
var key=togMap[id], cb=el.querySelector('#'+id); if(!cb) return;
cb.checked=toggles[key];
cb.addEventListener('change',function(){
toggles[key]=cb.checked; lsSet('toggles5',toggles);
kmLog(key + ' ' + (cb.checked ? 'enabled' : 'disabled'));
if(key==='leaderboard'){if(!cb.checked) removeLeaderboard(); else if(matchPlayers.me&&matchPlayers.opponent) buildLeaderboard();}
});
});
// Custom messages
var mg=el.querySelector('#msg-goal'), mw=el.querySelector('#msg-win'), ml=el.querySelector('#msg-loss');
if(mg) mg.value=customMsgs.goal; if(mw) mw.value=customMsgs.win; if(ml) ml.value=customMsgs.loss;
el.querySelector('#btn-save-msgs').addEventListener('click',function(){
customMsgs.goal=mg.value||'GOAL!'; customMsgs.win=mw.value||'GG EZ'; customMsgs.loss=ml.value||'GG';
// selects return the preset string directly — no decode needed
lsSet('customMsgs5',customMsgs); showToast('Messages saved!','ok');
kmLog('Custom messages saved — Goal: "' + customMsgs.goal + '" Win: "' + customMsgs.win + '" Loss: "' + customMsgs.loss + '"');
});
// Daily goal
el.querySelector('#km-daily-goal').addEventListener('change',function(){
daily.goal=parseInt(this.value)||5; lsSet('dailyGoal5',daily.goal); lsSet('daily5',daily); updateDailyProgress();
});
el.querySelector('#km-daily-goal').value=daily.goal;
// Settings — theme/loading moved to plugins, only wire remaining settings
// Fix: wire the skip splash checkbox
var skipSplashCb = el.querySelector('#set-skip-splash');
if (skipSplashCb) {
skipSplashCb.checked = lsGet('km_skipSplash', false);
skipSplashCb.addEventListener('change', function() {
lsSet('km_skipSplash', skipSplashCb.checked);
showToast(skipSplashCb.checked ? 'Loading screen disabled' : 'Loading screen enabled', 'ok');
kmLog('skipSplash set to: ' + skipSplashCb.checked);
});
}
}
function toggleMenu(force) {
var el=document.getElementById('km-menu'); if(!el) return;
menuOpen=force!==undefined?force:!menuOpen;
el.style.display=menuOpen?'block':'none';
if(menuOpen&&activeTab==='console') scrollConsole();
kmLog('Menu ' + (menuOpen ? 'opened' : 'closed'));
}
// ─── TAB RENDERERS ────────────────────────────────────────────────────────
function refreshRanksTab() {
var el=document.getElementById('km-ranks-body'); if(!el||!playerData) return;
var d=playerData;
var skin=SKINS[d.EquippedSkinId||'body.0']||'Unknown';
var modes=[{label:'1v1',key:'Competitive1v1'},{label:'2v2',key:'Competitive2v2'},{label:'3v3',key:'Competitive3v3'},{label:'Casual',key:'Casual'}];
var a=theme.accent;
var html='<div class="km-player-card">'
+'<div class="km-player-name">'+(d.Nickname||'—')+'</div>'
+'<div class="km-player-xp">XP '+(d.AccountXp||0).toLocaleString()+'</div>'
+'</div>'
+'<div class="km-car-row"><span class="km-car-label">🚗 Current Car</span><span class="km-car-name">'+skin+'</span></div>'
+'<div class="km-divider"></div>';
modes.forEach(function(m){
var g=d.ModesGlicko&&d.ModesGlicko[m.key];
var s=d.ModesData&&d.ModesData[m.key];
if(!g||!s) return;
var mmr=g.displayRating, rank=getRank(mmr), prog=getRankProgress(mmr);
var tot=s.wins+s.loses, wr=tot>0?Math.round((s.wins/tot)*100):0;
var peak=peakMMR[m.key]?'<span class="km-peak">peak '+peakMMR[m.key]+'</span>':'';
html+='<div class="km-rank-row">'
+'<div class="km-rank-icon">'+rank.emoji+'</div>'
+'<div class="km-rank-info">'
+'<div class="km-rank-name" style="color:'+rank.color+'">'+rank.name+'</div>'
+'<div class="km-rank-mode">'+m.label+'</div>'
+'<div class="km-prog-wrap"><div class="km-prog-bar" style="width:'+prog+'%;background:'+rank.color+'"></div></div>'
+'</div>'
+'<div class="km-rank-right">'
+'<div class="km-rank-mmr">'+mmr+' '+peak+'</div>'
+'<div class="km-rank-wl">'+s.wins+'W · '+s.loses+'L · '+wr+'%</div>'
+'</div>'
+'</div>';
});
el.innerHTML=html;
updateHUDRank();
}
function updateRankedTab() {
var oppEl=document.getElementById('km-opp-body');
if(oppEl){
if(opponentMMR){
var r=getRank(opponentMMR);
var oppName=streamerName(matchPlayers.opponent||'Opponent');
oppEl.innerHTML='<div class="km-rank-row"><div class="km-rank-icon">'+r.emoji+'</div>'
+'<div class="km-rank-info"><div class="km-rank-name" style="color:'+r.color+'">'+r.name+'</div><div class="km-rank-mode">'+oppName+'</div></div>'
+'<div class="km-rank-right"><div class="km-rank-mmr">'+streamerMMR(opponentMMR)+'</div></div></div>';
} else if(playerData&&playerData.ModesGlicko){
var mkey2=currentMode==='1v1'?'Competitive1v1':currentMode==='2v2'?'Competitive2v2':currentMode==='Casual'?'Casual':'Competitive3v3';
var mg2=playerData.ModesGlicko[mkey2]||playerData.ModesGlicko['Competitive3v3'];
var my2=mg2?mg2.displayRating:1000;
var lo2=trophyRange.lo!==null?trophyRange.lo:my2-350;
var hi2=trophyRange.hi!==null?trophyRange.hi:my2+350;
oppEl.innerHTML='<div class="km-opp-range"><div class="km-opp-label">Expected Opponent Range</div>'
+'<div class="km-opp-vals" style="font-size:18px;font-weight:700;margin:4px 0">'+lo2+' – '+hi2+'</div>'
+'<div class="km-opp-ranks">'+getRank(lo2).emoji+' '+getRank(lo2).name+' → '+getRank(hi2).emoji+' '+getRank(hi2).name+'</div>'
+(trophyRange.lo!==null?'<div style="font-size:10px;color:#fbbf24;margin-top:6px">🏆 Live matchmaking window</div>':'')+'</div>';
} else {
oppEl.innerHTML='<div class="km-empty">Play a match to see opponent MMR</div>';
}
}
var splitEl=document.getElementById('km-split-body');
if(splitEl&&playerData&&playerData.ModesData){
var html='';
[{label:'1v1',key:'Competitive1v1'},{label:'2v2',key:'Competitive2v2'},{label:'3v3',key:'Competitive3v3'}].forEach(function(m){
var s=playerData.ModesData[m.key]; if(!s) return;
var tot=s.wins+s.loses, wr=tot>0?Math.round((s.wins/tot)*100):0;
html+='<div class="km-stat-row"><span>'+m.label+'</span>'
+'<span><span class="col-green">'+s.wins+'W</span> / <span class="col-red">'+s.loses+'L</span> '+wr+'%</span></div>';
});
splitEl.innerHTML=html||'<div class="km-empty">No data</div>';
}
}
function updateDailyProgress() {
var el=document.getElementById('km-daily-progress'); if(!el) return;
var pct=Math.min(Math.round((daily.wins/daily.goal)*100),100);
el.innerHTML='<div style="margin-top:6px;font-size:11px;color:rgba(255,255,255,0.4);margin-bottom:4px">'+daily.wins+' / '+daily.goal+' wins today</div>'
+'<div class="km-prog-wrap" style="height:6px"><div class="km-prog-bar" style="width:'+pct+'%;background:'+theme.accent+'"></div></div>'
+(pct>=100?'<div style="font-size:11px;color:#4ade80;margin-top:4px">✓ Daily goal reached!</div>':'');
}
function updateHeatmap() {
var el=document.getElementById('km-heatmap-body'); if(!el) return;
if(!Object.keys(heatmap).length){el.innerHTML='<div class="km-empty">Play matches to build heatmap</div>';return;}
var html='<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">';
for(var h=0;h<24;h++){
var d=heatmap[h], intensity=d?Math.min(d.played*20,100):0;
var wr=d&&d.played>0?Math.round((d.wins/d.played)*100):0;
var bg=d?'rgba(59,130,246,'+(intensity/100).toFixed(2)+')':'rgba(255,255,255,0.04)';
html+='<div title="'+h+':00 — '+(d?d.played+' matches, '+wr+'% WR':'0 matches')+'" '
+'style="width:24px;height:24px;border-radius:3px;background:'+bg+';font-size:8px;color:rgba(255,255,255,0.4);display:flex;align-items:center;justify-content:center">'+h+'</div>';
}
html+='</div>';
el.innerHTML=html;
}
function updateSessionTab() {
var diff=session.goals-session.conceded;
var acc=session.shots>0?Math.round((session.goals/session.shots)*100)+'%':'—';
var ids={'ss-goals':session.goals,'ss-conceded':session.conceded,
'ss-diff':(diff>0?'+':'')+diff,'ss-shots':session.shots,'ss-acc':acc,
'ss-matches':session.matches,'ss-wins':session.wins,'ss-losses':session.losses,
'ss-streak':(session.streak>=3?'🔥 ':'')+session.streak,
'ss-maxstreak':session.maxStreak,'ss-comebacks':session.comebacks,
'ss-perfect':session.perfectGames,'ss-xp':'+'+session.xpGained,
'ss-mode':session.lastMode||'—',
};
Object.keys(ids).forEach(function(id){var el=document.getElementById(id);if(el)el.textContent=ids[id];});
var wr=document.getElementById('ss-wr');
if(wr){var tot=session.wins+session.losses;wr.textContent=tot>0?Math.round((session.wins/tot)*100)+'%':'—';}
var diffEl=document.getElementById('ss-diff');
if(diffEl) diffEl.style.color=diff>0?'#4ade80':diff<0?'#f87171':'';
}
function updateProfileTab() {
// Profile card
var pb=document.getElementById('km-profile-body'); if(pb&&playerData){
var d=playerData;
var mmr3=d.ModesGlicko&&d.ModesGlicko['Competitive3v3']?d.ModesGlicko['Competitive3v3'].displayRating:0;
var rank3=getRank(mmr3);
var totalMatches=Object.values(d.ModesData||{}).reduce(function(acc,m){return acc+(m.matchesPlayed||0);},0);
var totalWins=Object.values(d.ModesData||{}).reduce(function(acc,m){return acc+(m.wins||0);},0);
var totalLosses=Object.values(d.ModesData||{}).reduce(function(acc,m){return acc+(m.loses||0);},0);
var overallWR=totalMatches>0?Math.round((totalWins/totalMatches)*100):0;
var mostPlayed=null, mostPlayed_count=0;
[{key:'Competitive1v1',label:'1v1'},{key:'Competitive2v2',label:'2v2'},{key:'Competitive3v3',label:'3v3'},{key:'Casual',label:'Casual'}].forEach(function(m){
var s=d.ModesData&&d.ModesData[m.key]; if(!s) return;
if(s.matchesPlayed>mostPlayed_count){mostPlayed_count=s.matchesPlayed;mostPlayed=m.label;}
});
// Night owl / early bird
var peakHour=null, peakCount=0;
Object.keys(heatmap).forEach(function(h){if(heatmap[h].played>peakCount){peakCount=heatmap[h].played;peakHour=parseInt(h);}});
var playstyle=peakHour===null?'—':peakHour>=22||peakHour<5?'🦉 Night Owl':peakHour<12?'🐦 Early Bird':'☀️ Daytime Player';
pb.innerHTML='<div class="km-player-card">'
+'<div class="km-player-name">'+(d.Nickname||'—')+'</div>'
+'<div class="km-player-xp" style="font-size:12px">'+rank3.emoji+' '+rank3.name+'</div>'
+'</div>'
+'<div class="km-divider"></div>'
+'<div class="km-stat-row"><span>Total Matches</span><span>'+totalMatches+'</span></div>'
+'<div class="km-stat-row"><span>Overall W/L</span><span><span class="col-green">'+totalWins+'W</span> / <span class="col-red">'+totalLosses+'L</span></span></div>'
+'<div class="km-stat-row"><span>Overall WR</span><span>'+overallWR+'%</span></div>'
+'<div class="km-stat-row"><span>Most Played</span><span>'+(mostPlayed||'—')+'</span></div>'
+'<div class="km-stat-row"><span>Play Style</span><span>'+playstyle+'</span></div>'
+'<div class="km-stat-row"><span>Peak MMR 3v3</span><span style="color:'+theme.accent+'">'+(peakMMR['Competitive3v3']||'—')+'</span></div>'
+'<div class="km-stat-row"><span>Session XP</span><span style="color:#fbbf24">+'+session.xpGained+'</span></div>'
+'<div class="km-stat-row"><span>Account XP</span><span>'+(d.AccountXp||0).toLocaleString()+'</span></div>'
+'<div class="km-stat-row"><span>Session Started</span><span>'+new Date(session.startTime).toLocaleTimeString()+'</span></div>';
}
// XP history
var xpEl=document.getElementById('km-xp-history-body'); if(xpEl){
if(!xpHistory.length){xpEl.innerHTML='<div class="km-empty">No XP data yet</div>';}
else{xpEl.innerHTML=xpHistory.slice(0,10).map(function(x){
return '<div class="km-history-row"><span style="color:#fbbf24;font-weight:700">+'+x.gained+' XP</span>'
+'<span class="km-history-mmr">Total: '+x.total.toLocaleString()+'</span>'
+'<span class="km-history-time">'+x.time+'</span></div>';
}).join('');}
}
// Rank history
var rhEl=document.getElementById('km-rank-history-body'); if(rhEl){
if(!rankHistory.length){rhEl.innerHTML='<div class="km-empty">No rank changes yet</div>';}
else{rhEl.innerHTML=rankHistory.slice(0,10).map(function(r){
var arrow=r.direction==='up'?'↑':'↓';
var col=r.direction==='up'?'#4ade80':'#f87171';
return '<div class="km-history-row">'
+'<span style="color:'+col+';font-weight:700">'+arrow+' '+r.to+'</span>'
+'<span class="km-history-mmr">from '+r.from+'</span>'
+'<span class="km-history-time">'+r.time+'</span></div>';
}).join('');}
}
}
function updateHistoryTab() {
var el=document.getElementById('km-history-body'); if(!el) return;
if(!matchHistory.length){el.innerHTML='<div class="km-empty">No matches yet</div>';return;}
el.innerHTML=matchHistory.slice(0,20).map(function(m){
var col=m.result==='Win'?'#4ade80':'#f87171';
var diff=m.diff!=null?'<span style="color:'+(m.diff>=0?'#4ade80':'#f87171')+';font-size:10px">'+(m.diff>=0?'+':'')+m.diff+'</span>':'';
var xpStr=m.xp>0?'<span style="color:#fbbf24;font-size:10px">+'+m.xp+' XP</span>':'';
return '<div class="km-history-row">'
+'<span style="color:'+col+';font-weight:700">'+m.result+'</span>'
+'<span class="km-history-mmr">'+m.score+' vs '+(m.opponent||'?')+' '+diff+' '+xpStr+'</span>'
+'<span class="km-history-time">'+(m.mode||'?')+' '+m.time+'</span>'
+'</div>';
}).join('');
}
function updateRivalsTab() {
// Rivals
var rivEl=document.getElementById('km-rivals-body'); if(rivEl){
var rivKeys=Object.keys(rivals).filter(function(k){return rivals[k].count>=3;}).sort(function(a,b){return rivals[b].count-rivals[a].count;});
if(!rivKeys.length){rivEl.innerHTML='<div class="km-empty">Face opponents 3+ times to see rivals</div>';}
else{rivEl.innerHTML=rivKeys.slice(0,10).map(function(id){
var r=rivals[id];
var name=r.name||'Opponent';
var wr=r.count>0?Math.round((r.wins/r.count)*100):0;
var col=wr>=50?'#4ade80':'#f87171';
return '<div class="km-history-row">'
+'<span style="color:#f97316;font-weight:700">⚔️ '+name+'</span>'
+'<span class="km-history-mmr"><span class="col-green">'+r.wins+'W</span> / <span class="col-red">'+r.losses+'L</span></span>'
+'<span style="color:'+col+';font-size:10px">'+wr+'% WR</span>'
+'</div>';
}).join('');}
}
// Opponent log
var oppEl=document.getElementById('km-opp-log-body'); if(oppEl){
if(!opponentLog.length){oppEl.innerHTML='<div class="km-empty">No opponents logged yet</div>';}
else{oppEl.innerHTML=opponentLog.slice(0,15).map(function(o){
var col=o.result==='Win'?'#4ade80':'#f87171';
var mmrGuess=o.mmrGuess?'~'+o.mmrGuess+' MMR':'?';
var oppName = o.opponent || o.name || 'Opponent';
return '<div class="km-history-row">'
+'<span style="color:'+col+';font-weight:700">'+o.score+'</span>'
+'<span class="km-history-mmr">'+oppName+' ('+mmrGuess+')</span>'
+'<span class="km-history-time">'+(o.mode||'?')+' '+o.time+'</span>'
+'</div>';
}).join('');}
}
}
function updatePeakBody() {
var el=document.getElementById('km-peak-body'); if(!el) return;
var labels={Competitive1v1:'1v1',Competitive2v2:'2v2',Competitive3v3:'3v3',Casual:'Casual'};
var keys=Object.keys(peakMMR);
if(!keys.length){el.innerHTML='<div class="km-empty">No peak data yet</div>';return;}
el.innerHTML=keys.map(function(k){
var r=getRank(peakMMR[k]);
return '<div class="km-stat-row"><span>'+(labels[k]||k)+' '+r.emoji+'</span><span style="color:'+theme.accent+'">'+peakMMR[k]+'</span></div>';
}).join('');
}
function updateClipsBody() {
var el=document.getElementById('km-clips-body'); if(!el) return;
if(!clipMarks.length){el.innerHTML='<div class="km-empty">No clips marked</div>';return;}
el.innerHTML='<div class="km-clips-list">'+clipMarks.map(function(t){return '<span class="km-clip-stamp">▶ '+t+'</span>';}).join('')+'</div>';
}
// ─── PROFILE CARD EXPORT ──────────────────────────────────────────────────
function exportProfileCard() {
if(!playerData){showToast('Login first','warn');return;}
var d=playerData;
var nick=d.Nickname||'Unknown';
var mmr3=d.ModesGlicko&&d.ModesGlicko['Competitive3v3']?d.ModesGlicko['Competitive3v3'].displayRating:'?';
var rank3=getRank(mmr3);
var skin=SKINS[d.EquippedSkinId||'body.0']||'Unknown';
var tot=session.wins+session.losses;
var wr=tot>0?Math.round((session.wins/tot)*100)+'%':'—';
var acc=session.shots>0?Math.round((session.goals/session.shots)*100)+'%':'—';
var modes=[
{label:'1v1',key:'Competitive1v1'},
{label:'2v2',key:'Competitive2v2'},
{label:'3v3',key:'Competitive3v3'},
{label:'Casual',key:'Casual'},
];
var rankLines=modes.map(function(m){
var g=d.ModesGlicko&&d.ModesGlicko[m.key];
var s=d.ModesData&&d.ModesData[m.key];
if(!g||!s) return '';
var r=getRank(g.displayRating);
var t=s.wins+s.loses, w=t>0?Math.round((s.wins/t)*100):0;
return '> '+r.emoji+' **'+m.label+'** — '+g.displayRating+' MMR | '+s.wins+'W/'+s.loses+'L ('+w+'% WR)';
}).filter(Boolean).join('\n');
var rivalLines=Object.keys(rivals).filter(function(k){return rivals[k].count>=3;}).slice(0,3).map(function(id){
var r=rivals[id];
var name = r.name || 'Opponent';
var w=r.count>0?Math.round((r.wins/r.count)*100):0;
return '> ⚔️ **'+name+'** — '+r.wins+'W/'+r.losses+'L ('+w+'% WR)';
}).join('\n');
var peakLines=Object.keys(peakMMR).map(function(k){
var labels={Competitive1v1:'1v1',Competitive2v2:'2v2',Competitive3v3:'3v3',Casual:'Casual'};
return '> '+getRank(peakMMR[k]).emoji+' '+(labels[k]||k)+': **'+peakMMR[k]+'**';
}).join('\n');
var card=[
'# '+rank3.emoji+' '+nick+' — RocketGoal.io Profile',
'',
'> 🚗 **Car:** '+skin,
'> 🎮 **Account XP:** '+(d.AccountXp||0).toLocaleString(),
'> ⚽ **Total Matches:** '+Object.values(d.ModesData||{}).reduce(function(a,m){return a+(m.matchesPlayed||0);},0),
'',
'## 📊 Rankings',
rankLines,
'',
'## 🏆 Peak MMR',
peakLines||'> *No peak data*',
'',
'## 🎯 This Session',
'> **Matches:** '+session.matches+' | **W:** '+session.wins+' | **L:** '+session.losses+' | **WR:** '+wr,
'> **Goals:** '+session.goals+' | **Conceded:** '+session.conceded+' | **Accuracy:** '+acc,
'> **Streak:** '+session.streak+' | **Best:** '+session.maxStreak,
'> **XP Gained:** +'+session.xpGained,
'> **Comebacks:** '+session.comebacks+(session.perfectGames>0?' | **Perfect Games:** '+session.perfectGames:''),
'',
(rivalLines?'## ⚔️ Rivals\n'+rivalLines+'\n\n':''),
'---',
'*Generated by KeyMod v5 — made by @keydopz on TikTok*',
].join('\n');
try{
navigator.clipboard.writeText(card).then(function(){showToast('Profile card copied!','ok');}).catch(function(){fallbackCopy(card);});
}catch(e){fallbackCopy(card);}
}
// ─── CONSOLE ──────────────────────────────────────────────────────────────
function appendConsoleRow(entry) {
var el=document.getElementById('km-console-body'); if(!el) return;
var row=document.createElement('div'); row.className='km-log-row';
if(entry.level==='warn') row.classList.add('warn');
if(entry.level==='err') row.classList.add('err');
if(entry.line.includes('KeyMod')||entry.line.includes('v0304_')) row.classList.add('hi');
row.textContent=entry.line;
el.appendChild(row);
if(activeTab==='console') scrollConsole();
}
function scrollConsole(){var el=document.getElementById('km-console-body');if(el)el.scrollTop=el.scrollHeight;}
// ─── DRAG ─────────────────────────────────────────────────────────────────
function makeDraggable(el,handle) {
var ox=0,oy=0,drag=false;
handle.addEventListener('mousedown',function(e){drag=true;ox=e.clientX-el.offsetLeft;oy=e.clientY-el.offsetTop;});
document.addEventListener('mousemove',function(e){if(!drag)return;el.style.left=(e.clientX-ox)+'px';el.style.top=(e.clientY-oy)+'px';el.style.right='auto';});
document.addEventListener('mouseup',function(){drag=false;});
}
// ─── THEME & CSS ──────────────────────────────────────────────────────────
function hexToRgba(hex,alpha) {
var r=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if(!r) return hex;
return 'rgba('+parseInt(r[1],16)+','+parseInt(r[2],16)+','+parseInt(r[3],16)+','+alpha+')';
}
function applyTheme() {
// Use default BakkesMod theme only.
theme.preset = 'bakkesmod';
theme.accent = '#006eff'; theme.text = '#d4d4d4'; theme.opacity = 0.99;
var s=document.getElementById('km-style'); if(s) s.textContent=buildCSS();
var menu=document.getElementById('km-menu'); if(!menu) return;
var p=getPreset();
var op=theme.opacity||0.96;
var bgRgba=hexToRgba(p.bg.includes('rgba')?'#000000':p.bg, op);
menu.style.background=p.bg; // CSS handles it
menu.style.borderTopColor=theme.accent;
updatePeakBody();
updateDailyProgress();
}
function buildCSS() {
var p = getPreset();
var a = theme.accent || p.accent;
var txt= theme.text || p.text;
var op = theme.opacity || 0.96;
var bg = p.bg;
var tabbg = p.tabBg;
var blur = p.blur;
var brd = p.border;
// Aero gets special chrome styling
var isAero = theme.preset === 'aero';
var isGlass = theme.preset === 'glass';
var isArch = theme.preset === 'arch';
var isWindows = theme.preset === 'windows';
var isGTA = theme.preset === 'gta';
var isRL = theme.preset === 'rl';
var isMinimal = theme.preset === 'minimal';
var splashCSS;
if(theme.loading==='rainbow') {
splashCSS='background:linear-gradient(270deg,#ff0000,#ff7700,#ffff00,#00ff00,#0000ff,#8b00ff,#ff0000);background-size:400% 400%;animation:km-rainbow 4s linear infinite;';
} else if(theme.loading==='black'||!theme.loading) {
splashCSS='background:linear-gradient(160deg,#0a0a0f 0%,#111118 60%,#0d0d15 100%);';
} else {
splashCSS='background:'+theme.loading+';';
}
return [
"@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:ital,wght@0,400;0,500&family=JetBrains+Mono:wght@400;500&display=swap');",
'* { box-sizing: border-box; }',
'@keyframes km-rainbow{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}',
'@keyframes km-pulse{0%,100%{box-shadow:0 0 8px '+a+'}50%{box-shadow:0 0 22px '+a+',0 0 40px '+a+'}}',
// Splash
'#km-splash{position:fixed;inset:0;z-index:999999;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:opacity 0.65s ease;padding-top:28px;'+splashCSS+'}',
'#km-splash-scan{position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.03) 2px,rgba(0,0,0,0.03) 4px);pointer-events:none;}',
'#km-gta-header{font-family:"Bebas Neue",sans-serif;font-size:80px;letter-spacing:16px;color:#f5c518;line-height:1;text-shadow:4px 4px 0 rgba(0,0,0,0.9),-1px -1px 0 rgba(200,150,0,0.4);}',
'#km-gta-sub{font-family:"DM Mono",monospace;font-size:11px;letter-spacing:3px;color:rgba(245,197,24,0.5);text-transform:uppercase;margin-top:6px;}',
'#km-gta-divider{width:100%;max-width:360px;height:1px;background:linear-gradient(90deg,transparent,#f5c518,transparent);margin:20px auto;}',
'#km-gta-loadbar{width:360px;height:4px;background:rgba(255,255,255,0.08);margin:0 auto 10px;}',
'#km-gta-fill{height:100%;width:0%;background:#f5c518;transition:width 4s cubic-bezier(0.4,0,0.2,1);}',
'#km-gta-status{font-family:"DM Mono",monospace;font-size:11px;letter-spacing:2px;color:rgba(245,197,24,0.4);text-transform:uppercase;margin-top:6px;}',
'#km-gta-tip{font-family:"DM Mono",monospace;font-size:10px;color:rgba(255,255,255,0.2);margin-top:16px;max-width:380px;text-align:center;letter-spacing:0.3px;}',
'#km-rl-bg{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none;}',
'#km-rl-ring1{position:absolute;width:400px;height:400px;border:1px solid rgba(0,150,255,0.12);border-radius:50%;animation:km-spin 8s linear infinite;}',
'#km-rl-ring2{position:absolute;width:280px;height:280px;border:1px solid rgba(0,150,255,0.08);border-radius:50%;animation:km-spin 5s linear infinite reverse;}',
'@keyframes km-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}',
'#km-skip-always{position:absolute;bottom:24px;background:transparent;border:none;color:rgba(255,255,255,0.2);font-family:"DM Mono",monospace;font-size:10px;letter-spacing:1px;cursor:pointer;padding:6px 20px;transition:color 0.2s;}',
'#km-skip-always:hover{color:rgba(255,255,255,0.6);}',
'#km-splash-inner{text-align:center;}',
'#km-splash-title{font-family:"Bebas Neue",sans-serif;font-size:96px;letter-spacing:12px;color:#fff;line-height:1;text-shadow:0 0 60px rgba(245,197,24,0.4),0 0 120px rgba(245,197,24,0.12);}',
'#km-splash-sub{font-family:"DM Mono",monospace;font-size:13px;font-weight:500;color:rgba(255,255,255,0.45);letter-spacing:2.5px;text-transform:uppercase;margin-top:10px;}',
'#km-splash-sub span{color:#f5c518;font-weight:700;}',
'#km-splash-bar-wrap{margin:34px auto 0;width:200px;height:3px;background:rgba(255,255,255,0.1);border-radius:2px;overflow:hidden;}',
'#km-splash-bar{height:100%;width:0%;background:rgba(255,255,255,0.9);border-radius:2px;transition:width 4s cubic-bezier(0.25,0.46,0.45,0.94);}',
'#km-splash-status{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Helvetica Neue",sans-serif;font-size:12px;color:rgba(255,255,255,0.45);letter-spacing:0.2px;margin-top:14px;}',
'#km-skip{position:absolute;bottom:28px;background:transparent;border:none;color:rgba(255,255,255,0.3);font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",sans-serif;font-size:12px;letter-spacing:0;cursor:pointer;padding:8px 24px;transition:color 0.2s;}',
'#km-skip:hover{color:rgba(255,255,255,0.7);}',
// macOS menu bar
'#km-macos-bar{position:absolute;top:0;left:0;right:0;height:28px;background:rgba(0,0,0,0.5);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);display:flex;align-items:center;padding:0 14px;gap:6px;}',
'.km-macos-dot{width:11px;height:11px;border-radius:50%;flex-shrink:0;}',
'#km-macos-bar-title{flex:1;text-align:center;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",sans-serif;font-size:12px;font-weight:500;color:rgba(255,255,255,0.55);letter-spacing:0.2px;}',
// macOS Software Update card
'#km-splash-inner{background:rgba(30,30,35,0.72);backdrop-filter:blur(40px) saturate(180%);-webkit-backdrop-filter:blur(40px) saturate(180%);border:0.5px solid rgba(255,255,255,0.18);border-radius:16px;padding:36px 40px 32px;width:340px;text-align:center;box-shadow:0 32px 80px rgba(0,0,0,0.6),0 1px 0 rgba(255,255,255,0.1) inset;}',
'#km-update-icon-wrap{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:24px;}',
'#km-update-icon,#km-update-icon2{border-radius:13px;box-shadow:0 4px 20px rgba(0,0,0,0.5);}',
'#km-update-arrow{color:rgba(255,255,255,0.3);display:flex;align-items:center;}',
'#km-update-title{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display",sans-serif;font-size:20px;font-weight:600;color:#fff;letter-spacing:0.2px;margin-bottom:4px;}',
'#km-update-subtitle{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",sans-serif;font-size:13px;color:rgba(255,255,255,0.5);margin-bottom:22px;}',
'#km-update-bar-track{width:100%;height:4px;background:rgba(255,255,255,0.12);border-radius:2px;overflow:hidden;margin-bottom:12px;}',
'#km-update-detail{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",sans-serif;font-size:11px;color:rgba(255,255,255,0.25);line-height:1.5;margin-top:16px;letter-spacing:0.1px;}',
// Override old splash-status inside the card
'#km-splash-inner #km-splash-status{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",sans-serif!important;font-size:12px!important;color:rgba(255,255,255,0.4)!important;margin-top:0!important;letter-spacing:0!important;}',
// Goal / End flash
'#km-goal-flash{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:999998;font-family:"Bebas Neue",sans-serif;font-size:72px;color:#fff;letter-spacing:6px;text-shadow:0 0 40px rgba(0,0,0,0.8);pointer-events:none;transition:opacity 0.5s ease;opacity:1;}',
'#km-end-flash{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:999990;background:rgba(0,0,0,0.85);border:1px solid rgba(255,255,255,0.12);border-radius:14px;padding:20px 32px;text-align:center;pointer-events:none;transition:opacity 0.7s;font-family:"JetBrains Mono",monospace;}',
// Welcome
'#km-welcome{position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:99997;background:rgba(0,0,0,0.85);border:1px solid '+a+';border-radius:6px;color:#fff;font-family:"DM Mono",monospace;font-size:14px;padding:10px 24px;pointer-events:none;transition:opacity 0.6s;opacity:1;}',
// Goal celebration banner
'#km-goal-banner{position:fixed;left:0;right:0;top:0;z-index:99999999;height:120px;display:flex;align-items:center;justify-content:center;pointer-events:none;opacity:0;transform:translateY(-100%);transition:opacity 0.25s ease,transform 0.3s cubic-bezier(0.34,1.56,0.64,1);}',
'#km-goal-banner.show{opacity:1;transform:translateY(0);}',
'.km-banner-bg-overlay{position:absolute;inset:0;background:linear-gradient(90deg,rgba(0,0,0,0.3),transparent,rgba(0,0,0,0.3));}',
'.km-banner-content{position:relative;display:flex;align-items:center;gap:18px;padding:0 32px;width:100%;justify-content:center;}',
'.km-banner-text-wrap{text-align:center;}',
'.km-banner-goal{font-family:"Bebas Neue",sans-serif;font-size:52px;letter-spacing:6px;color:#fff;line-height:1;text-shadow:0 2px 20px rgba(0,0,0,0.6);}',
'.km-banner-player{font-family:"DM Mono",monospace;font-size:13px;letter-spacing:2px;color:rgba(255,255,255,0.7);text-transform:uppercase;margin-top:4px;}',
// Toast — stacked bottom-right
// ── macOS Tahoe notification CSS ────────────────────────────────────────
'.km-mac-toast{'
+'position:fixed;right:18px;z-index:999999;'
+'width:320px;'
+'background:rgba(28,28,32,0.72);'
+'backdrop-filter:blur(40px) saturate(200%) brightness(118%);'
+'-webkit-backdrop-filter:blur(40px) saturate(200%) brightness(118%);'
+'border:0.5px solid rgba(255,255,255,0.22);'
+'border-radius:14px;'
+'box-shadow:0 8px 32px rgba(0,0,0,0.5),0 1px 0 rgba(255,255,255,0.12) inset;'
+'padding:13px 14px 13px 13px;'
+'display:flex;align-items:flex-start;gap:11px;'
+'pointer-events:auto;'
+'opacity:0;transform:translateX(32px) scale(0.96);'
+'transition:opacity 0.36s cubic-bezier(0.4,0,0.2,1),transform 0.36s cubic-bezier(0.4,0,0.2,1);'
+'cursor:default;'
+'}',
'.km-mac-toast.show{opacity:1!important;transform:translateX(0) scale(1)!important;}',
'.km-mac-toast.hide{opacity:0!important;transform:translateX(20px) scale(0.96)!important;}',
'.km-mac-icon{width:34px;height:34px;border-radius:8px;border:1px solid;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;}',
'.km-mac-body{flex:1;min-width:0;padding-top:1px;}',
'.km-mac-title{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter",sans-serif;font-size:12px;font-weight:600;color:rgba(255,255,255,0.5);letter-spacing:0.1px;margin-bottom:2px;}',
'.km-mac-msg{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter",sans-serif;font-size:13px;font-weight:400;color:#fff;line-height:1.4;word-break:break-word;}',
'.km-mac-close{background:none;border:none;color:rgba(255,255,255,0.3);font-size:11px;cursor:pointer;padding:0 2px;margin-top:1px;flex-shrink:0;line-height:1;transition:color 0.1s;}',
'.km-mac-close:hover{color:rgba(255,255,255,0.7);}',
'.km-toast{position:fixed;right:22px;bottom:22px;z-index:999998;background:rgba(18,18,22,0.9);border:1px solid rgba(255,255,255,0.14);border-left:3px solid '+a+';color:'+txt+';font-family:"DM Mono",monospace;font-size:13px;padding:11px 18px;border-radius:10px;pointer-events:none;opacity:0;transform:translateY(12px);transition:opacity 0.2s,transform 0.2s;white-space:normal;max-width:400px;backdrop-filter:blur(10px);}',
'.km-toast.show{opacity:1;transform:translateY(0);}',
'.km-toast.permanent{width:min(92vw,1920px);height:200px;left:50%;right:auto;transform:translateX(-50%);bottom:12px;border-radius:12px;max-width:1920px;min-width:500px;opacity:1;pointer-events:auto;}',
'.km-toast.ok{border-left-color:#4ade80;}',
'.km-toast.warn{border-left-color:#fbbf24;}',
'.km-toast.err{border-left-color:#f87171;}',
'.km-toast.info{border-left-color:'+a+';}',
'.km-toast.xp{border-left-color:#fbbf24;}',
// HUD
'#km-hud{position:fixed;bottom:22px;right:22px;z-index:99997;'
+(isAero?'background:rgba(0,15,35,0.78);border:1px solid rgba(100,200,255,0.25);':
isGlass?'background:rgba(15,20,40,0.55);border:1px solid rgba(255,255,255,0.18);':
'background:rgba(8,10,16,0.9);border:1px solid rgba(255,255,255,0.1);')
+'border-left:2px solid '+a+';border-radius:8px;padding:6px 12px;font-family:system-ui,-apple-system,"DM Mono",monospace;font-size:12px;color:'+txt+';display:flex;align-items:center;gap:7px;cursor:pointer;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);transition:all 0.2s;}',
'#km-hud:hover{box-shadow:0 0 16px '+hexToRgba(a,0.4)+';}',
'#km-hud-party{animation:km-party-pulse 1.5s ease-in-out infinite;}',
'@keyframes km-party-pulse{0%,100%{opacity:1}50%{opacity:0.5}}@keyframes km-rec-blink{0%,100%{opacity:1;box-shadow:0 0 6px #f87171}50%{opacity:0.3;box-shadow:none}}',
'#km-hud-rank{font-size:15px;line-height:1;}',
'#km-hud-fps{font-size:11px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:0;min-width:46px;text-align:right;}',
'#km-hud-ping{font-size:11px;font-weight:500;font-variant-numeric:tabular-nums;letter-spacing:0;min-width:36px;text-align:right;}',
'#km-hud-sep{color:rgba(255,255,255,0.15);font-size:10px;margin:0 1px;}',
// ── Menu shell ─────────────────────────────────────────────────────────
'#km-menu{display:none;position:fixed;top:48px;left:48px;z-index:99998;width:500px;'
+'background:'+bg+';'
+(blur&&blur!=='none'?'backdrop-filter:'+blur+';-webkit-backdrop-filter:'+blur+';':'')
+'border:1px solid '+brd+';'
+'box-shadow:0 16px 48px rgba(0,0,0,0.6);'
+'font-family:system-ui,-apple-system,"Segoe UI",sans-serif;font-size:13px;color:'+txt+';overflow:hidden;'
+'border-radius:8px;}',
// ── Header ─────────────────────────────────────────────────────────────
'#km-header{display:flex;align-items:center;gap:10px;padding:10px 12px;'
+'background:transparent;border-bottom:1px solid rgba(255,255,255,0.07);cursor:grab;}',
'#km-header:active{cursor:grabbing;}',
'#km-title{font-family:system-ui,-apple-system,sans-serif;font-size:13px;font-weight:600;letter-spacing:0;color:'+txt+';flex:1;line-height:1;opacity:0.9;}',
'#km-wip-badge{font-family:system-ui,sans-serif;font-size:10px;font-weight:400;letter-spacing:0;color:rgba(255,255,255,0.25);padding:0 0 0 8px;vertical-align:middle;line-height:1;}',
'#km-ping{font-size:11px;color:#4ade80;font-family:"DM Mono",monospace;opacity:0.7;}',
'#km-header-right{display:flex;align-items:center;gap:6px;}',
'#km-discord-btn{display:flex;align-items:center;gap:5px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.1);color:rgba(255,255,255,0.5);font-size:11px;letter-spacing:0;padding:4px 9px;cursor:pointer;transition:all 0.1s;font-family:system-ui,-apple-system,sans-serif;font-weight:500;border-radius:4px;}',
'#km-discord-btn:hover{border-color:'+a+';color:'+a+';}',
'#km-discord-btn.has-code{border-color:rgba(88,101,242,0.5);color:#7289da;}',
'#km-discord-label{font-size:10px;font-family:"DM Mono",monospace;}',
'#km-close{background:none;border:none;color:rgba(255,255,255,0.25);font-size:12px;cursor:pointer;padding:4px 8px;transition:color 0.1s;}',
'#km-close:hover{color:#f87171;}',
// ── Tip bar ────────────────────────────────────────────────────────────
'#km-tip-bar{display:flex;align-items:center;gap:8px;padding:5px 16px;background:rgba(255,255,255,0.02);border-bottom:1px solid rgba(255,255,255,0.05);}',
'#km-tip-text{flex:1;font-size:11px;color:rgba(255,255,255,0.3);font-family:system-ui,-apple-system,sans-serif;letter-spacing:0;}',
'#km-tip-close{background:none;border:none;color:rgba(255,255,255,0.15);cursor:pointer;font-size:10px;padding:0 2px;line-height:1;}',
'#km-tip-close:hover{color:rgba(255,255,255,0.45);}',
// ── Tabs ───────────────────────────────────────────────────────────────
'#km-tabs-wrap{display:flex;align-items:stretch;background:rgba(0,0,0,0.25);border-bottom:1px solid rgba(255,255,255,0.06);}',
'#km-tabs{display:flex;overflow-x:auto;flex:1;scrollbar-width:none;}',
'#km-tabs::-webkit-scrollbar{display:none;}',
'.km-tab-arrow{background:none;border:none;border-left:1px solid rgba(255,255,255,0.05);color:rgba(255,255,255,0.25);font-size:14px;padding:0 10px;cursor:pointer;transition:color 0.1s;flex-shrink:0;}',
'.km-tab-arrow:first-child{border-left:none;border-right:1px solid rgba(255,255,255,0.05);}',
'.km-tab-arrow:hover{color:rgba(255,255,255,0.7);}',
'.kmt{flex:0 0 auto;background:none;border:none;border-bottom:2px solid transparent;color:rgba(255,255,255,0.4);font-size:12px;letter-spacing:0;text-transform:none;padding:8px 12px;cursor:pointer;transition:color 0.1s,border-color 0.1s;white-space:nowrap;font-family:system-ui,-apple-system,sans-serif;font-weight:400;}',
'.kmt:hover{color:rgba(255,255,255,0.8);}',
'.kmt.active{color:'+txt+';border-bottom-color:'+a+';font-weight:500;}',
// ── Panes ──────────────────────────────────────────────────────────────
'.km-pane{display:none;padding:16px 16px 20px;max-height:460px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,0.08) transparent;}',
'.km-pane::-webkit-scrollbar{width:2px;}',
'.km-pane::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);}',
'.km-pane.active{display:block;}',
'#km-pane-console{padding:0!important;}',
// ── Section headers ────────────────────────────────────────────────────
'.km-section{font-size:11px;letter-spacing:0;text-transform:none;font-weight:600;color:rgba(255,255,255,0.5);margin:16px 0 6px;font-family:system-ui,-apple-system,sans-serif;}',
'.km-section::after{display:none;}',
'.km-section:first-child{margin-top:2px;}',
'.km-empty{font-size:12px;color:rgba(255,255,255,0.22);padding:8px 0;font-family:system-ui,-apple-system,sans-serif;}',
'.km-divider{height:1px;background:rgba(255,255,255,0.05);margin:8px 0;}',
// ── Player card ────────────────────────────────────────────────────────
'.km-player-card{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:12px;}',
'.km-player-name{font-size:18px;font-weight:700;color:'+txt+';font-family:system-ui,-apple-system,sans-serif;letter-spacing:-0.3px;}',
'.km-player-xp{font-size:10px;color:rgba(255,255,255,0.22);font-family:"DM Mono",monospace;}',
'.km-car-row{display:flex;justify-content:space-between;align-items:center;padding:5px 0 8px;border-bottom:1px solid rgba(255,255,255,0.05);margin-bottom:4px;}',
'.km-car-label{font-size:11px;color:rgba(255,255,255,0.35);text-transform:none;letter-spacing:0;font-family:"Inter",system-ui,sans-serif;}',
'.km-car-name{font-size:11px;font-weight:600;color:'+txt+';}',
// ── Rank rows ──────────────────────────────────────────────────────────
'.km-rank-row{display:flex;align-items:center;gap:14px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.04);}',
'.km-rank-row:last-child{border-bottom:none;}',
'.km-rank-icon{font-size:26px;width:32px;text-align:center;flex-shrink:0;}',
'.km-rank-info{flex:1;min-width:0;}',
'.km-rank-name{font-size:14px;font-weight:600;line-height:1.2;font-family:system-ui,-apple-system,sans-serif;letter-spacing:-0.2px;}',
'.km-rank-mode{font-size:11px;color:rgba(255,255,255,0.35);letter-spacing:0;text-transform:none;margin-top:2px;font-family:system-ui,-apple-system,sans-serif;}',
'.km-rank-right{text-align:right;flex-shrink:0;}',
'.km-rank-mmr{font-size:18px;font-weight:700;color:'+a+';font-family:"DM Mono",monospace;}',
'.km-rank-wl{font-size:10px;color:rgba(255,255,255,0.22);margin-top:1px;}',
'.km-peak{font-size:9px;color:rgba(255,255,255,0.2);}',
// ── Progress bar ───────────────────────────────────────────────────────
'.km-prog-wrap{height:2px;background:rgba(255,255,255,0.06);overflow:hidden;margin-top:4px;}',
'.km-prog-bar{height:100%;transition:width 0.5s cubic-bezier(0.4,0,0.2,1);}',
// ── Stat rows ──────────────────────────────────────────────────────────
'.km-stat-row{display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid rgba(255,255,255,0.05);font-size:12px;}',
'.km-stat-row:last-child{border-bottom:none;}',
'.km-stat-row>span:first-child{color:rgba(255,255,255,0.5);text-transform:none;font-size:12px;letter-spacing:0;font-family:system-ui,-apple-system,sans-serif;}',
'.km-stat-row>span:last-child{font-family:"DM Mono",monospace;font-size:13px;font-weight:400;}',
'.col-green{color:#4ade80;} .col-red{color:#f87171;}',
// ── Stats grid ─────────────────────────────────────────────────────────
'.km-stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;margin:8px 0 12px;}',
'.km-stat-cell{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.05);padding:10px 8px;text-align:center;}',
'.km-stat-val{font-family:"DM Mono",monospace;font-size:20px;font-weight:700;color:'+a+';line-height:1;}',
'.km-stat-lbl{font-size:8px;color:rgba(255,255,255,0.25);text-transform:uppercase;letter-spacing:1.5px;margin-top:4px;}',
// ── Two col ────────────────────────────────────────────────────────────
'.km-two-col{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:6px;}',
'.km-col-card{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);padding:10px;}',
'.km-card-title{font-size:8px;letter-spacing:2px;text-transform:uppercase;color:rgba(255,255,255,0.25);margin-bottom:6px;}',
// ── History rows ───────────────────────────────────────────────────────
'.km-history-row{display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid rgba(255,255,255,0.05);font-size:12px;gap:8px;font-family:"Inter",system-ui,sans-serif;}',
'.km-history-mmr{font-family:"Inter",system-ui,sans-serif;color:rgba(255,255,255,0.4);font-size:11px;flex:1;}',
'.km-history-time{color:rgba(255,255,255,0.22);font-size:11px;white-space:nowrap;}',
// ── Clips ──────────────────────────────────────────────────────────────
'.km-clips-list{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;}',
'.km-clip-stamp{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:'+a+';font-family:"DM Mono",monospace;font-size:10px;padding:3px 8px;}',
// ── Opponent MMR ───────────────────────────────────────────────────────
'.km-opp-range{padding:6px 0;}',
'.km-opp-label{font-size:9px;color:rgba(255,255,255,0.28);letter-spacing:1px;text-transform:uppercase;margin-bottom:6px;}',
'.km-opp-vals{font-family:"DM Mono",monospace;font-size:24px;font-weight:700;color:'+a+';}',
'.km-opp-ranks{font-size:11px;color:rgba(255,255,255,0.35);margin-top:4px;}',
// ── Buttons ────────────────────────────────────────────────────────────
'.km-btn{display:block;width:100%;margin-top:8px;padding:7px 12px;'
+'background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.12);color:rgba(255,255,255,0.65);'
+'font-family:system-ui,-apple-system,sans-serif;font-size:12px;font-weight:500;letter-spacing:0;cursor:pointer;'
+'transition:background 0.1s,border-color 0.1s,color 0.1s;text-align:center;border-radius:5px;}',
'.km-btn:hover{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.25);color:#fff;}',
'.km-btn.danger{border-color:rgba(239,68,68,0.2);color:rgba(248,113,113,0.6);}',
'.km-btn.danger:hover{background:rgba(239,68,68,0.08);border-color:#f87171;color:#f87171;}',
// ── Inputs ─────────────────────────────────────────────────────────────
'.km-input{background:rgba(20,20,30,0.95);border:1px solid rgba(255,255,255,0.12);color:'+txt+';font-family:system-ui,sans-serif;font-size:12px;padding:5px 9px;border-radius:4px;width:60px;outline:none;transition:border-color 0.15s;}',
'.km-input option{background:#14141e;color:#e2e8f0;}',
'.km-input:focus{border-color:'+a+';}',
'.km-select{background:rgba(20,20,30,0.95);border:1px solid rgba(255,255,255,0.12);color:'+txt+';font-family:system-ui,sans-serif;font-size:12px;padding:5px 9px;border-radius:4px;outline:none;cursor:pointer;}',
'.km-select option{background:#14141e;color:#e2e8f0;}',
'.km-color{width:34px;height:24px;border:1px solid rgba(255,255,255,0.1);background:none;cursor:pointer;padding:1px;}',
'.km-slider{accent-color:'+a+';width:110px;cursor:pointer;}',
// ── Toggles ────────────────────────────────────────────────────────────
'.km-toggle-row{padding:7px 0;border-bottom:1px solid rgba(255,255,255,0.05);}',
'.km-toggle-row label{font-size:12px;color:rgba(255,255,255,0.6);cursor:pointer;display:flex;align-items:center;gap:10px;letter-spacing:0;font-family:system-ui,-apple-system,sans-serif;}',
'.km-toggle-row input[type=checkbox]{accent-color:'+a+';width:14px;height:14px;cursor:pointer;}',
// ── Theme presets ──────────────────────────────────────────────────────
'.km-preset-grid{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;}',
'.km-preset-btn{padding:5px 12px;font-family:"DM Mono",monospace;font-size:10px;cursor:pointer;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:rgba(255,255,255,0.5);transition:all 0.12s;letter-spacing:0.5px;}',
'.km-preset-btn:hover{background:rgba(255,255,255,0.08);color:#fff;}',
'.km-preset-btn.active{background:rgba(255,255,255,0.08);border-color:'+a+';color:'+a+';}',
// ── Emoji picker ───────────────────────────────────────────────────────
'.km-emoji-grid{display:flex;flex-wrap:wrap;gap:4px;margin:6px 0 12px;}',
'.km-emoji-btn{width:34px;height:34px;font-size:18px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.07);cursor:pointer;transition:all 0.1s;display:flex;align-items:center;justify-content:center;padding:0;}',
'.km-emoji-btn:hover{background:rgba(255,255,255,0.09);}',
'.km-emoji-btn.active{border-color:'+a+';background:rgba(255,255,255,0.09);}',
// ── Plugin Store ───────────────────────────────────────────────────────
'.km-store-filters{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:14px;}',
'.km-store-cat{padding:4px 12px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.07);color:rgba(255,255,255,0.35);font-size:9px;letter-spacing:1px;text-transform:uppercase;cursor:pointer;transition:all 0.1s;font-family:"DM Mono",monospace;}',
'.km-store-cat:hover,.km-store-cat.active{background:rgba(255,255,255,0.08);border-color:'+a+';color:'+a+';}',
'.km-store-grid{display:flex;flex-direction:column;gap:1px;}',
'.km-plugin-card{display:flex;align-items:center;gap:12px;padding:10px 12px;background:rgba(255,255,255,0.02);border-left:2px solid transparent;border-radius:4px;margin-bottom:2px;transition:background 0.1s,border-color 0.1s;}',
'.km-plugin-card:hover{background:rgba(255,255,255,0.06);}',
'.km-plugin-card.installed{border-left-color:'+a+';background:rgba(255,255,255,0.04);}',
'.km-plugin-icon{font-size:20px;width:28px;text-align:center;flex-shrink:0;}',
'.km-plugin-info{flex:1;min-width:0;}',
'.km-plugin-name{font-size:12px;font-weight:600;color:'+txt+';letter-spacing:0;font-family:system-ui,-apple-system,sans-serif;}',
'.km-plugin-cat{font-size:10px;color:rgba(255,255,255,0.35);letter-spacing:0;text-transform:none;margin-bottom:2px;font-family:system-ui,-apple-system,sans-serif;}',
'.km-plugin-desc{font-size:11px;color:rgba(255,255,255,0.42);line-height:1.5;font-family:system-ui,-apple-system,sans-serif;}',
'.km-plugin-install-btn{flex-shrink:0;padding:5px 12px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.12);color:rgba(255,255,255,0.55);font-family:"Inter",system-ui,sans-serif;font-size:11px;font-weight:500;cursor:pointer;transition:all 0.1s;letter-spacing:0;white-space:nowrap;border-radius:3px;}',
'.km-plugin-install-btn:hover{border-color:'+a+';color:'+a+';}',
'.km-plugin-install-btn.installed{border-color:rgba(74,222,128,0.3);color:#4ade80;background:rgba(74,222,128,0.05);}',
// ── Owned plugins ──────────────────────────────────────────────────────
'.km-owned-card{background:rgba(255,255,255,0.03);border-left:2px solid rgba(255,255,255,0.08);margin-bottom:4px;overflow:hidden;}',
'.km-owned-header{display:flex;align-items:center;gap:10px;padding:10px 12px;cursor:pointer;transition:background 0.1s;}',
'.km-owned-header:hover{background:rgba(255,255,255,0.04);}',
'.km-owned-icon{font-size:18px;}',
'.km-owned-name{flex:1;font-size:12px;font-weight:600;color:#fff;letter-spacing:0.3px;}',
'.km-owned-arrow{color:rgba(255,255,255,0.2);font-size:11px;}',
'.km-owned-body{padding:10px 12px 14px;border-top:1px solid rgba(255,255,255,0.06);}',
// ── Leaderboard overlay ────────────────────────────────────────────────
'#km-leaderboard{position:fixed;top:12px;left:12px;z-index:99997;'
+(isAero?'background:rgba(0,20,45,0.85);border:1px solid rgba(100,200,255,0.18);':
isGlass?'background:rgba(15,20,40,0.65);border:1px solid rgba(255,255,255,0.14);':
'background:rgba(8,10,16,0.92);border:1px solid rgba(255,255,255,0.1);')
+'border-left:2px solid '+a+';padding:10px 14px;font-family:"Inter",system-ui,sans-serif;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);min-width:160px;border-radius:6px;}',
'.km-lb-row{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:2px 0;}',
'.km-lb-name{font-size:12px;font-weight:600;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:"Inter",system-ui,sans-serif;}',
'.km-lb-score{font-size:20px;font-weight:700;color:#fff;font-family:"Bebas Neue",sans-serif;letter-spacing:1px;}',
'.km-lb-divider{font-size:9px;color:rgba(255,255,255,0.18);text-align:center;letter-spacing:2px;padding:1px 0;}',
'.km-lb-mmr{font-size:10px;color:rgba(255,255,255,0.3);}',
// ── Party code popup ───────────────────────────────────────────────────
'#km-party-popup{position:fixed;bottom:90px;right:22px;z-index:999997;width:280px;background:#0a0c0f;border:1px solid rgba(255,255,255,0.08);border-top:2px solid '+a+';overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.8);opacity:0;transform:translateY(10px);transition:opacity 0.2s,transform 0.2s;}',
'#km-party-popup.show{opacity:1;transform:translateY(0);}',
'#km-party-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:rgba(255,255,255,0.03);border-bottom:1px solid rgba(255,255,255,0.05);}',
'#km-party-title{font-size:10px;letter-spacing:2px;text-transform:uppercase;color:'+a+';font-family:"DM Mono",monospace;}',
'#km-party-close{background:none;border:none;color:rgba(255,255,255,0.25);font-size:12px;cursor:pointer;padding:2px 5px;}',
'#km-party-close:hover{color:#f87171;}',
'#km-party-code{font-family:"Bebas Neue",sans-serif;font-size:44px;font-weight:700;color:#fff;text-align:center;padding:14px 0 6px;letter-spacing:8px;}',
'#km-party-sub{font-size:10px;color:rgba(255,255,255,0.25);text-align:center;padding-bottom:12px;font-family:"DM Mono",monospace;}',
'#km-party-btns{display:flex;gap:1px;padding:0 12px 12px;}',
'.km-party-btn{flex:1;padding:7px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:rgba(255,255,255,0.5);font-family:"DM Mono",monospace;font-size:10px;cursor:pointer;transition:all 0.1s;letter-spacing:0.5px;}',
'.km-party-btn:hover{background:rgba(255,255,255,0.08);color:#fff;}',
'.km-party-btn.primary{border-color:rgba(255,255,255,0.15);color:rgba(255,255,255,0.7);}',
'.km-party-btn.primary:hover{border-color:'+a+';color:'+a+';}',
// ── Party tab ──────────────────────────────────────────────────────────
'.km-party-code-box{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);padding:16px;text-align:center;margin-bottom:10px;}',
'.km-party-code-label{font-size:9px;letter-spacing:2px;text-transform:uppercase;color:rgba(255,255,255,0.28);margin-bottom:8px;font-family:"DM Mono",monospace;}',
'.km-party-code-num{font-family:"Bebas Neue",sans-serif;font-size:44px;font-weight:700;color:#f5c518;letter-spacing:8px;margin-bottom:12px;}',
// ── Profile ────────────────────────────────────────────────────────────
'.km-prof-row{display:flex;flex-direction:column;gap:4px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.05);}',
'.km-prof-row label{font-size:10px;color:rgba(255,255,255,0.3);letter-spacing:1px;text-transform:uppercase;}',
'.km-url-input{width:100%;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);color:#fff;font-family:"DM Mono",monospace;font-size:11px;padding:7px 10px;outline:none;transition:border-color 0.15s;}',
'.km-url-input:focus{border-color:'+a+';}',
'.km-url-input::placeholder{color:rgba(255,255,255,0.18);}',
'.km-banner-preview-box{position:relative;height:80px;overflow:hidden;margin-top:10px;border:1px solid rgba(255,255,255,0.07);background:rgba(255,255,255,0.03);}',
// ── Session lock ───────────────────────────────────────────────────────
'#km-session-lock{position:fixed;inset:0;z-index:9999999;background:rgba(0,0,0,0.92);display:flex;align-items:center;justify-content:center;}',
'#km-sl-box{background:#0a0c0f;border:1px solid rgba(255,255,255,0.08);border-top:2px solid #f97316;padding:40px;width:360px;text-align:center;}',
'#km-sl-icon{font-size:48px;margin-bottom:14px;}',
'#km-sl-title{font-family:"Bebas Neue",sans-serif;font-size:32px;letter-spacing:3px;color:#f97316;margin-bottom:12px;}',
'#km-sl-body{font-size:13px;color:rgba(255,255,255,0.5);line-height:1.7;margin-bottom:22px;font-family:"DM Mono",monospace;}',
'#km-sl-dismiss{display:block;width:100%;padding:11px;background:#f97316;border:none;color:#000;font-family:"DM Mono",monospace;font-size:12px;letter-spacing:1px;cursor:pointer;margin-bottom:8px;}',
'#km-sl-override{display:block;width:100%;padding:9px;background:transparent;border:1px solid rgba(255,255,255,0.1);color:rgba(255,255,255,0.3);font-family:"DM Mono",monospace;font-size:11px;cursor:pointer;}',
// ── Lock overlay (momentum/tilt) ───────────────────────────────────────
'#km-session-lock{position:fixed;inset:0;z-index:999999;background:rgba(0,0,0,0.92);display:flex;align-items:center;justify-content:center;}',
'#km-lock-inner{text-align:center;max-width:340px;}',
'#km-lock-icon{font-size:56px;margin-bottom:16px;}',
'#km-lock-title{font-family:"Bebas Neue",sans-serif;font-size:40px;letter-spacing:4px;color:#f87171;margin-bottom:12px;}',
'#km-lock-msg{font-family:"DM Mono",monospace;font-size:12px;color:rgba(255,255,255,0.45);line-height:1.8;margin-bottom:24px;}',
'#km-lock-dismiss{padding:10px 28px;background:transparent;border:1px solid #f87171;color:#f87171;font-family:"DM Mono",monospace;font-size:12px;letter-spacing:1px;cursor:pointer;}',
// ── Support popup ──────────────────────────────────────────────────────
'#km-support-overlay{position:fixed;inset:0;z-index:9999999;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);}',
'#km-support-box{position:relative;background:#0a0c0f;border:1px solid rgba(255,255,255,0.08);border-top:2px solid #3b82f6;padding:36px 40px 32px;width:340px;text-align:center;}',
'#km-support-close{position:absolute;top:10px;left:12px;background:none;border:none;color:rgba(255,255,255,0.25);font-size:14px;cursor:pointer;padding:4px;line-height:1;}',
'#km-support-close:hover{color:#fff;}',
'#km-support-icon{font-size:36px;margin-bottom:12px;}',
'#km-support-title{font-family:"Bebas Neue",sans-serif;font-size:28px;letter-spacing:2px;color:#fff;margin-bottom:14px;}',
'#km-support-body{font-family:"DM Mono",monospace;font-size:12px;color:rgba(255,255,255,0.45);margin-bottom:22px;line-height:1.7;}',
'#km-support-body strong{color:rgba(255,255,255,0.8);font-size:14px;}',
'#km-support-btn{display:inline-block;background:#2563eb;color:#fff;font-family:"DM Mono",monospace;font-size:12px;letter-spacing:1px;padding:10px 28px;text-decoration:none;cursor:pointer;transition:background 0.15s;}',
'#km-support-btn:hover{background:#3b82f6;}',
// ── Resize handle ──────────────────────────────────────────────────────
'#km-resize-handle{position:absolute;bottom:0;right:0;width:14px;height:14px;cursor:se-resize;opacity:0.2;transition:opacity 0.15s;}',
'#km-resize-handle:hover{opacity:0.6;}',
'#km-resize-handle::before{content:"";position:absolute;bottom:3px;right:3px;width:7px;height:1px;background:#fff;box-shadow:0 -3px 0 #fff,0 -6px 0 #fff;}',
'#km-resize-handle::after{content:"";position:absolute;bottom:3px;right:3px;height:7px;width:1px;background:#fff;box-shadow:-3px 0 0 #fff,-6px 0 0 #fff;}',
// ── Momentum meter ─────────────────────────────────────────────────────
'#km-momentum-wrap{position:fixed;bottom:70px;right:22px;z-index:99996;width:150px;}',
'#km-momentum-label{font-family:"DM Mono",monospace;font-size:8px;letter-spacing:2px;color:rgba(255,255,255,0.25);text-align:center;margin-bottom:3px;}',
'#km-momentum-track{height:3px;background:rgba(255,255,255,0.06);overflow:hidden;}',
'#km-momentum-bar{height:100%;width:50%;transition:width 0.4s ease,background 0.4s ease;}',
// ── Focus overlay ──────────────────────────────────────────────────────
'#km-focus-overlay{position:fixed;inset:0;z-index:99990;background:#000;pointer-events:none;transition:opacity 0.3s;}',
// ── GTA V override ─────────────────────────────────────────────────────
(theme.preset==='gta'?[
'#km-menu{border-top:3px solid #f5c518!important;border-left:3px solid #f5c518!important;background:linear-gradient(120deg,rgba(18,12,8,0.95),rgba(25,24,18,0.95))!important;}',
'#km-menu::before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 20% 20%, rgba(255,255,255,0.08), transparent 50%), radial-gradient(circle at 80% 80%, rgba(0,120,255,0.12), transparent 45%);opacity:0.65;pointer-events:none;}',
'#km-header{border-bottom:1px solid rgba(245,197,24,0.25)!important;background:rgba(0,0,0,0.4)!important;}',
'#km-title{font-size:16px!important;letter-spacing:5px!important;color:#f5c518!important;text-transform:uppercase!important;font-weight:800!important;}',
'.kmt.active{color:#f5c518!important;border-bottom-color:#f5c518!important;}',
'.km-section{color:rgba(245,197,24,0.65)!important;}',
'.km-stat-row>span:first-child{color:rgba(245,197,24,0.35)!important;}',
'.km-btn{border-color:rgba(245,197,24,0.25)!important;color:#f5c518!important;}',
'.km-btn:hover{border-color:#f5c518!important;color:#fff!important;background:rgba(245,197,24,0.2)!important;}',
].join(''):theme.preset==='rl'?[
'#km-menu{border-top:3px solid #0096ff!important;}',
'#km-title{font-size:22px!important;letter-spacing:3px!important;}',
'.km-rank-mmr{font-size:20px!important;}',
'.km-stat-val{font-size:22px!important;}',
].join(''):theme.preset==='minimal'?[
'#km-menu{border:none!important;border-top:1px solid rgba(255,255,255,0.1)!important;}',
'#km-header{border-bottom:none!important;padding:8px 14px!important;}',
'#km-title{font-size:13px!important;letter-spacing:6px!important;color:rgba(255,255,255,0.9)!important;font-family:"DM Mono",monospace!important;}',
'.kmt{font-size:8px!important;letter-spacing:2px!important;color:rgba(255,255,255,0.2)!important;}',
'.kmt.active{color:rgba(255,255,255,0.9)!important;border-bottom-color:rgba(255,255,255,0.9)!important;}',
'.km-section{color:rgba(255,255,255,0.15)!important;}',
'.km-stat-row>span:first-child{color:rgba(255,255,255,0.2)!important;}',
'.km-btn{border-color:rgba(255,255,255,0.07)!important;color:rgba(255,255,255,0.4)!important;}',
'.km-rank-mmr{color:rgba(255,255,255,0.9)!important;}',
'.km-plugin-card{border-left:none!important;}',
'.km-plugin-card.installed{border-left:1px solid rgba(255,255,255,0.4)!important;}',
].join(''):''),
// ── Esports override ───────────────────────────────────────────────────
(theme.preset==='esports'?[
'#km-menu{border-top:2px solid #ff3c00!important;border-left:2px solid #ff3c00!important;background:rgba(2,2,6,0.98)!important;}',
'#km-title{font-size:18px!important;letter-spacing:6px!important;color:#ff3c00!important;}',
'.kmt.active{color:#ff3c00!important;border-bottom-color:#ff3c00!important;}',
'.km-section{color:rgba(255,60,0,0.5)!important;}',
'.km-rank-mmr{color:#ff3c00!important;}',
'.km-stat-val{color:#ff3c00!important;}',
'.km-btn:hover{border-color:#ff3c00!important;color:#ff3c00!important;}',
'.km-plugin-card.installed{border-left-color:#ff3c00!important;}',
].join(''):''),
// ── Org team CSS overrides ─────────────────────────────────────────────
(theme.preset==='faze'?[
'#km-title{color:#d40000!important;text-shadow:0 0 20px rgba(212,0,0,0.5);}',
'.km-rank-mmr{color:#d40000!important;}',
'.km-stat-val{color:#d40000!important;}',
'.kmt.active{color:#d40000!important;border-bottom-color:#d40000!important;}',
'.km-section::after{background:rgba(212,0,0,0.3)!important;}',
'.km-btn:hover{border-color:#d40000!important;color:#d40000!important;}',
].join(''):theme.preset==='nrg'?[
'#km-menu{position:relative;overflow:hidden;}',
'#km-menu::before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 50% 30%, rgba(255,255,255,0.08), transparent 55%), linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.0));opacity:0.5;pointer-events:none;}',
'#km-title{color:#00a8ff!important;text-shadow:0 0 20px rgba(0,168,255,0.4);}',
'.km-rank-mmr{color:#00a8ff!important;}',
'.km-stat-val{color:#00a8ff!important;}',
'.kmt.active{color:#00a8ff!important;border-bottom-color:#00a8ff!important;}',
'.km-plugin-card.installed{border-left-color:#00a8ff!important;}',
].join(''):theme.preset==='vitality'?[
'#km-menu{position:relative;overflow:hidden;}',
'#km-menu::before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 70% 20%, rgba(255,255,0,0.18), transparent 48%), linear-gradient(150deg, rgba(0,0,0,0.5), rgba(0,0,0,0.15));opacity:0.5;pointer-events:none;}',
'#km-title{color:#ffdc00!important;text-shadow:0 0 20px rgba(255,220,0,0.4);}',
'#km-menu{border-top-color:#ffdc00!important;}',
'.km-rank-mmr{color:#ffdc00!important;}',
'.km-stat-val{color:#ffdc00!important;}',
'.kmt.active{color:#ffdc00!important;border-bottom-color:#ffdc00!important;}',
'.km-plugin-card.installed{border-left-color:#ffdc00!important;}',
].join(''):theme.preset==='kcorp'?[
'#km-title{color:#0050ff!important;text-shadow:0 0 20px rgba(0,80,255,0.4);}',
'#km-menu{border-top-color:#0050ff!important;border-left:2px solid #0050ff!important;}',
'.km-rank-mmr{color:#0050ff!important;}',
'.km-stat-val{color:#0050ff!important;}',
'.kmt.active{color:#0050ff!important;border-bottom-color:#0050ff!important;}',
'.km-plugin-card.installed{border-left-color:#0050ff!important;}',
].join(''):''),
// ── BakkesMod override ──────────────────────────────────────────────────
(theme.preset==='bakkesmod'?[
// Core panel — gunmetal flat, zero blur
'#km-menu{font-family:Consolas,"Courier New",monospace!important;border:1px solid rgba(255,107,0,0.2)!important;border-top:2px solid #006eff!important;border-radius:0!important;}',
// Header — dark strip with orange left marker
'#km-header{background:#111!important;border-bottom:1px solid #222!important;padding:8px 12px!important;}',
'#km-title{font-family:Consolas,"Courier New",monospace!important;font-size:13px!important;letter-spacing:3px!important;color:#006eff!important;text-transform:uppercase!important;}',
// Tabs — flat BM-style selector
'#km-tabs-wrap{background:#111!important;border-bottom:1px solid #333!important;}',
'.kmt{font-family:Consolas,"Courier New",monospace!important;font-size:9px!important;letter-spacing:1px!important;color:#666!important;padding:8px 12px!important;border-bottom:none!important;}',
'.kmt:hover{color:#ccc!important;background:rgba(255,255,255,0.04)!important;}',
'.kmt.active{color:#006eff!important;border-bottom:2px solid #006eff!important;background:#181818!important;}',
// Pane
'.km-pane{background:#141414!important;}',
// Section headers — BM style: orange left border, flat text
'.km-section{color:#006eff!important;font-family:Consolas,"Courier New",monospace!important;font-size:9px!important;letter-spacing:2px!important;border-left:2px solid #006eff!important;padding-left:6px!important;margin:14px 0 8px!important;}',
'.km-section::after{display:none!important;}',
// Stat rows — dense, monospace
'.km-stat-row{border-bottom:1px solid #222!important;padding:6px 2px!important;}',
'.km-stat-row>span:first-child{color:#888!important;font-family:Consolas,"Courier New",monospace!important;font-size:10px!important;letter-spacing:0!important;text-transform:none!important;}',
'.km-stat-row>span:last-child{font-family:Consolas,"Courier New",monospace!important;font-size:12px!important;color:#d4d4d4!important;}',
// Rank numbers — BM monospace
'.km-rank-mmr{color:#006eff!important;font-family:Consolas,"Courier New",monospace!important;font-size:16px!important;}',
'.km-rank-name{font-family:Consolas,"Courier New",monospace!important;letter-spacing:0!important;}',
'.km-player-name{font-family:Consolas,"Courier New",monospace!important;font-size:18px!important;letter-spacing:1px!important;color:#006eff!important;}',
// Buttons — BM flat button style
'.km-btn{border:1px solid #333!important;border-radius:0!important;color:#999!important;font-family:Consolas,"Courier New",monospace!important;font-size:11px!important;text-transform:uppercase!important;letter-spacing:1px!important;}',
'.km-btn:hover{background:#1e1e1e!important;border-color:#006eff!important;color:#006eff!important;}',
'.km-btn.danger{border-color:#550000!important;color:#cc3333!important;}',
'.km-btn.danger:hover{border-color:#f87171!important;color:#f87171!important;background:#1a0000!important;}',
// Inputs
'.km-input,.km-select{background:#0e0e0e!important;border:1px solid #333!important;border-radius:0!important;font-family:Consolas,"Courier New",monospace!important;color:#d4d4d4!important;}',
'.km-select option,.km-input option{background:#111!important;color:#d4d4d4!important;}',
'.km-input:focus{border-color:#006eff!important;}',
// Plugin cards — no rounded corners, BM utility style
'.km-plugin-card{border-left:2px solid transparent!important;border-radius:0!important;background:#181818!important;}',
'.km-plugin-card:hover{background:#1e1e1e!important;}',
'.km-plugin-card.installed{border-left-color:#006eff!important;background:#1a1200!important;}',
'.km-plugin-name{font-family:Consolas,"Courier New",monospace!important;font-size:11px!important;}',
'.km-plugin-install-btn{border-radius:0!important;font-family:Consolas,"Courier New",monospace!important;text-transform:uppercase!important;letter-spacing:1px!important;}',
'.km-plugin-install-btn:hover{border-color:#006eff!important;color:#006eff!important;}',
'.km-plugin-install-btn.installed{border-color:rgba(255,107,0,0.5)!important;color:#006eff!important;}',
// Owned cards
'.km-owned-card{border-left:2px solid #333!important;border-radius:0!important;background:#181818!important;}',
'.km-owned-name{font-family:Consolas,"Courier New",monospace!important;}',
// HUD — BM-style info bar
'#km-hud{border-radius:0!important;border-left:2px solid #006eff!important;background:#111!important;font-family:Consolas,"Courier New",monospace!important;}',
// Leaderboard
'#km-leaderboard{border-radius:0!important;border-left:2px solid #006eff!important;background:rgba(14,14,14,0.95)!important;font-family:Consolas,"Courier New",monospace!important;}',
// Console
'#km-console-body{font-family:Consolas,"Courier New",monospace!important;}',
'.km-log-row{font-family:Consolas,"Courier New",monospace!important;}',
// Toggle rows
'.km-toggle-row label{font-family:Consolas,"Courier New",monospace!important;font-size:11px!important;color:#888!important;}',
// Preset button active
'.km-preset-btn.active{border-color:#006eff!important;color:#006eff!important;background:#1a1200!important;}',
// Tip bar
'#km-tip-bar{background:#111!important;border-bottom:1px solid #222!important;}',
'#km-tip-text{font-family:Consolas,"Courier New",monospace!important;color:#555!important;}',
// Progress bar — orange fill
'.km-prog-bar{background:#006eff!important;}',
'.km-prog-wrap{background:#333!important;}',
// History rows
'.km-history-row{border-bottom:1px solid #222!important;}',
'.km-history-mmr{font-family:Consolas,"Courier New",monospace!important;}',
'.km-history-time{font-family:Consolas,"Courier New",monospace!important;}',
// Close button
'#km-close{color:#555!important;}',
'#km-close:hover{color:#006eff!important;}',
].join(''):''),
// ── macOS Sonoma (14) override — light mode, white frosted glass ────────────
(theme.preset==='macos14'?[
// Full white/light mode — macOS Sonoma style, not dark translucent
'#km-menu{background:#f5f5f7!important;border:none!important;border-radius:12px!important;box-shadow:0 22px 70px rgba(0,0,0,0.35),0 0 0 0.5px rgba(0,0,0,0.18)!important;color:#1d1d1f!important;}',
// Header — macOS window chrome: traffic lights top-right, title centered
'#km-header{background:#f5f5f7!important;border-bottom:0.5px solid rgba(0,0,0,0.12)!important;padding:10px 14px!important;position:relative!important;}',
// Show traffic lights, hide default close button
'#km-traffic-lights{display:flex!important;}',
'#km-close{display:none!important;}',
'#km-fullscreen-btn{display:none!important;}',,
'#km-title{font-family:-apple-system,"SF Pro Display",sans-serif!important;font-size:13px!important;font-weight:600!important;color:#1d1d1f!important;letter-spacing:-0.2px!important;opacity:1!important;}',
'#km-wip-badge{color:rgba(0,0,0,0.35)!important;}',
// Traffic lights — red/yellow/green circles, top-right of header
'#km-header-right{gap:6px!important;}',
'#km-close{width:12px!important;height:12px!important;border-radius:50%!important;background:#ff5f57!important;border:none!important;padding:0!important;font-size:0!important;position:relative!important;cursor:pointer!important;transition:all 0.1s!important;}',
'#km-close::after{content:"✕";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:8px;color:rgba(0,0,0,0.6);opacity:0;}',
'#km-close:hover::after{opacity:1!important;}',
'#km-close:hover{background:#ff5f57!important;box-shadow:0 0 0 2px rgba(255,95,87,0.3)!important;}',
// Traffic light pill container
'#km-discord-btn{background:transparent!important;border:none!important;color:#007aff!important;font-size:11px!important;padding:0 8px!important;}',
// Tabs — macOS sidebar style
'#km-tabs-wrap{background:#ebebeb!important;border-bottom:0.5px solid rgba(0,0,0,0.1)!important;}',
'.kmt{font-family:-apple-system,"SF Pro Text",sans-serif!important;font-size:12px!important;font-weight:400!important;color:rgba(0,0,0,0.5)!important;letter-spacing:0!important;padding:8px 12px!important;}',
'.kmt:hover{color:rgba(0,0,0,0.8)!important;background:rgba(0,0,0,0.05)!important;}',
'.kmt.active{color:#007aff!important;font-weight:600!important;border-bottom-color:#007aff!important;background:rgba(0,122,255,0.06)!important;}',
// Pane background
'.km-pane{background:#f5f5f7!important;color:#1d1d1f!important;}',
// Section headers — macOS settings style
'.km-section{font-family:-apple-system,"SF Pro Text",sans-serif!important;font-size:11px!important;font-weight:600!important;color:rgba(0,0,0,0.35)!important;text-transform:uppercase!important;letter-spacing:0.6px!important;margin:14px 0 6px!important;}',
'.km-section::after{display:none!important;}',
// Stat rows
'.km-stat-row{border-bottom:0.5px solid rgba(0,0,0,0.08)!important;}',
'.km-stat-row>span:first-child{font-family:-apple-system,"SF Pro Text",sans-serif!important;color:rgba(0,0,0,0.55)!important;}',
'.km-stat-row>span:last-child{color:#1d1d1f!important;}',
// Rank
'.km-rank-mmr{color:#007aff!important;}',
'.km-rank-name{font-family:-apple-system,"SF Pro Display",sans-serif!important;font-weight:600!important;color:#1d1d1f!important;}',
'.km-rank-mode{color:rgba(0,0,0,0.4)!important;}',
'.km-rank-wl{color:rgba(0,0,0,0.35)!important;}',
'.km-player-name{color:#1d1d1f!important;font-family:-apple-system,sans-serif!important;}',
'.km-player-xp{color:rgba(0,0,0,0.35)!important;}',
// Buttons — iOS button style
'.km-btn{border-radius:9px!important;font-family:-apple-system,"SF Pro Text",sans-serif!important;font-size:13px!important;font-weight:500!important;border:none!important;background:rgba(0,122,255,0.1)!important;color:#007aff!important;letter-spacing:0!important;}',
'.km-btn:hover{background:rgba(0,122,255,0.2)!important;}',
'.km-btn.danger{background:rgba(255,59,48,0.08)!important;color:#ff3b30!important;}',
'.km-btn.danger:hover{background:rgba(255,59,48,0.16)!important;}',
// Inputs
'.km-input,.km-select{border-radius:8px!important;font-family:-apple-system,"SF Pro Text",sans-serif!important;background:rgba(0,0,0,0.04)!important;border:0.5px solid rgba(0,0,0,0.15)!important;color:#1d1d1f!important;}',
'.km-select option,.km-input option{background:#f5f5f7!important;color:#1d1d1f!important;}',
'.km-input:focus{border-color:#007aff!important;box-shadow:0 0 0 3px rgba(0,122,255,0.15)!important;}',
// Toggle rows
'.km-toggle-row{border-bottom:0.5px solid rgba(0,0,0,0.08)!important;}',
'.km-toggle-row label{font-family:-apple-system,"SF Pro Text",sans-serif!important;color:rgba(0,0,0,0.7)!important;}',
// Plugin cards
'.km-plugin-card{border-radius:10px!important;border-left:none!important;background:rgba(255,255,255,0.8)!important;margin-bottom:4px!important;box-shadow:0 1px 3px rgba(0,0,0,0.06)!important;}',
'.km-plugin-card:hover{background:#fff!important;}',
'.km-plugin-card.installed{background:rgba(0,122,255,0.06)!important;border:0.5px solid rgba(0,122,255,0.25)!important;}',
'.km-plugin-name{color:#1d1d1f!important;font-family:-apple-system,sans-serif!important;}',
'.km-plugin-cat{color:rgba(0,0,0,0.4)!important;}',
'.km-plugin-desc{color:rgba(0,0,0,0.5)!important;}',
'.km-plugin-install-btn{border-radius:8px!important;font-family:-apple-system,sans-serif!important;font-weight:500!important;background:rgba(0,122,255,0.08)!important;border-color:rgba(0,122,255,0.2)!important;color:#007aff!important;}',
// Empty state
'.km-empty{color:rgba(0,0,0,0.3)!important;}',
// History rows
'.km-history-row{border-bottom:0.5px solid rgba(0,0,0,0.07)!important;}',
'.km-history-mmr{color:rgba(0,0,0,0.45)!important;}',
'.km-history-time{color:rgba(0,0,0,0.3)!important;}',
// HUD
'#km-hud{border-radius:12px!important;background:rgba(245,245,247,0.92)!important;border:0.5px solid rgba(0,0,0,0.15)!important;border-left:none!important;font-family:-apple-system,sans-serif!important;color:#1d1d1f!important;box-shadow:0 4px 16px rgba(0,0,0,0.1)!important;}',
'#km-hud-fps{color:#007aff!important;}',
'#km-hud-ping{color:#34c759!important;}',
// Leaderboard
'#km-leaderboard{border-radius:12px!important;background:rgba(245,245,247,0.95)!important;border:0.5px solid rgba(0,0,0,0.12)!important;border-left:none!important;font-family:-apple-system,sans-serif!important;}',
'.km-lb-name{color:#1d1d1f!important;}',
'.km-lb-score{color:#1d1d1f!important;}',
// Progress bar
'.km-prog-bar{background:#007aff!important;}',
'.km-prog-wrap{background:rgba(0,0,0,0.08)!important;}',
// Preset buttons
'.km-preset-btn{color:rgba(0,0,0,0.5)!important;background:rgba(0,0,0,0.04)!important;border-color:rgba(0,0,0,0.1)!important;}',
'.km-preset-btn.active{border-color:#007aff!important;color:#007aff!important;background:rgba(0,122,255,0.06)!important;}',
// Tip bar
'#km-tip-bar{background:#ebebeb!important;border-bottom:0.5px solid rgba(0,0,0,0.1)!important;}',
'#km-tip-text{color:rgba(0,0,0,0.45)!important;}',
'#km-tip-close{color:rgba(0,0,0,0.3)!important;}',
// Close
'#km-ping{color:#34c759!important;}',
'#km-discord-label{color:rgba(0,0,0,0.4)!important;}',
// Owned cards
'.km-owned-card{background:rgba(255,255,255,0.8)!important;border-left:none!important;box-shadow:0 1px 3px rgba(0,0,0,0.05)!important;}',
'.km-owned-name{color:#1d1d1f!important;}',
'.km-owned-header:hover{background:rgba(0,0,0,0.03)!important;}',
// Console
'#km-console-body{background:#f5f5f7!important;}',
'.km-log-row{color:rgba(0,0,0,0.5)!important;}',
'.km-log-row.hi{color:#007aff!important;}',
// Store filters
'.km-store-cat{color:rgba(0,0,0,0.4)!important;background:rgba(0,0,0,0.03)!important;border-color:rgba(0,0,0,0.1)!important;}',
'.km-store-cat.active,.km-store-cat:hover{color:#007aff!important;border-color:rgba(0,122,255,0.3)!important;background:rgba(0,122,255,0.06)!important;}',
].join(''):''),
// ── Arch Linux override ────────────────────────────────────────────────
(isArch?[
'#km-menu{font-family:"JetBrains Mono",monospace!important;}',
'.kmt{font-size:8px!important;letter-spacing:1px!important;}',
'.km-section{color:#1793d1;}',
'#km-title::before{content:"[";color:rgba(23,147,209,0.5);}',
'#km-title::after{content:"]";color:rgba(23,147,209,0.5);}',
'.km-stat-row>span:first-child::before{content:"> ";color:#1793d1;}',
].join(''):
isWindows?[
'.km-section{color:#0078d4;}',
'.kmt{font-family:"Segoe UI",sans-serif!important;}',
].join(''):''),
// ── Input overlay & shot analysis ─────────────────────────────────────
'#km-input-overlay{position:fixed;z-index:99996;pointer-events:none;user-select:none;}',
// ── Console ────────────────────────────────────────────────────────────
'#km-console-body{height:360px;overflow-y:auto;padding:10px 14px;font-family:"DM Mono",monospace;font-size:10px;line-height:1.65;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,0.06) transparent;}',
'#km-console-body::-webkit-scrollbar{width:2px;}',
'#km-console-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.06);}',
'.km-log-row{color:rgba(255,255,255,0.2);word-break:break-all;margin:1px 0;}',
'.km-log-row.warn{color:#fbbf24;}',
'.km-log-row.err{color:#f87171;}',
'.km-log-row.hi{color:'+a+';}',
].join('');
}
function injectStyles() {
var s=document.createElement('style'); s.id='km-style';
s.textContent=buildCSS();
document.head.appendChild(s);
}
// ─── BOOT ─────────────────────────────────────────────────────────────────
function updatePartyButton() {
var label = document.getElementById('km-discord-label');
var btn = document.getElementById('km-discord-btn');
if (!label || !btn) return;
if (currentPartyCode) {
label.textContent = currentPartyCode;
btn.classList.add('has-code');
} else {
label.textContent = 'No Party';
btn.classList.remove('has-code');
}
}
function updatePartyTab() {
// Party status
var statusEl = document.getElementById('km-party-status');
if (statusEl) {
if (partyActive && currentPartyCode) {
statusEl.innerHTML = [
'<div class="km-party-code-box">',
'<div class="km-party-code-label">Party Code</div>',
'<div class="km-party-code-num">' + currentPartyCode + '</div>',
'<button class="km-btn" id="km-copy-party-code">📋 Copy Code</button>',
'<button class="km-btn" id="km-copy-party-msg">💬 Copy for Discord</button>',
'</div>',
].join('');
var copyBtn = document.getElementById('km-copy-party-code');
if (copyBtn) copyBtn.addEventListener('click', function() {
navigator.clipboard.writeText(currentPartyCode).catch(function() { fallbackCopy(currentPartyCode); });
showToast('Code copied: ' + currentPartyCode, 'ok');
kmLog('Party code copied: ' + currentPartyCode);
});
var msgBtn = document.getElementById('km-copy-party-msg');
if (msgBtn) msgBtn.addEventListener('click', function() {
var nick = playerData ? playerData.Nickname : 'me';
var msg = '\uD83C\uDFAE Join my RocketGoal.io party!\nCode: **' + currentPartyCode + '**\n*(invite from @keydopz via KeyMod)*';
navigator.clipboard.writeText(msg).catch(function() { fallbackCopy(msg); });
showToast('Discord message copied!', 'ok');
kmLog('Party discord message copied');
});
} else {
statusEl.innerHTML = '';
}
}
// Update HUD party indicator
var hudParty = document.getElementById('km-hud-party');
if (hudParty) {
if (partyActive && currentPartyCode) {
hudParty.textContent = '🎮 ' + currentPartyCode + ' — CLICK ME';
hudParty.style.display = 'inline';
} else {
hudParty.style.display = 'none';
}
}
// Update Discord label in header
var discLabel = document.getElementById('km-discord-label');
if(discLabel) {
discLabel.textContent = (partyActive && currentPartyCode) ? currentPartyCode : 'No Party';
discLabel.style.color = (partyActive && currentPartyCode) ? '#f5c518' : 'rgba(255,255,255,0.3)';
}
// Recent codes
var recentEl = document.getElementById('km-recent-codes');
if (recentEl) {
if (!recentCodes.length) {
recentEl.innerHTML = '<div class="km-empty">No recent codes</div>';
} else {
recentEl.innerHTML = '';
recentCodes.forEach(function(c, ci) {
var row = document.createElement('div');
row.className = 'km-history-row';
var span = document.createElement('span');
span.style.cssText = 'color:#f5c518;font-size:15px;font-weight:700;letter-spacing:2px';
span.textContent = c;
var btn = document.createElement('button');
btn.className = 'km-btn';
btn.style.cssText = 'margin-top:0;width:auto;padding:3px 10px;font-size:11px';
btn.textContent = 'Copy';
btn.addEventListener('click', function() {
navigator.clipboard.writeText(c).catch(function(){fallbackCopy(c);});
showToast('Copied: ' + c, 'ok');
});
row.appendChild(span);
row.appendChild(btn);
recentEl.appendChild(row);
});
}
}
}
// ─── PLUGIN SYSTEM ────────────────────────────────────────────────────────
function updateMomentumDisplay() {
var el = document.getElementById('plug-momentumMeter-stats'); if(!el) return;
var m = pluginState.momentum;
var col = m > 60 ? '#4ade80' : m > 35 ? '#fbbf24' : '#f87171';
el.innerHTML = '<div style="margin-top:4px">'
+'<div style="font-size:10px;color:rgba(255,255,255,0.4);margin-bottom:4px">Momentum: '+m+'%</div>'
+'<div class="km-prog-wrap" style="height:8px"><div class="km-prog-bar" style="width:'+m+'%;background:'+col+'"></div></div>'
+'</div>';
}
function showSessionLockOverlay(losses, limit) {
var el = document.createElement('div');
el.id = 'km-session-lock';
el.innerHTML = '<div id="km-sl-box">'
+'<div id="km-sl-icon">🔒</div>'
+'<div id="km-sl-title">Session Lock</div>'
+'<div id="km-sl-body">You have lost <strong>'+losses+' matches</strong> (limit: '+limit+').<br>Consider taking a break to avoid tilt.</div>'
+'<button id="km-sl-dismiss">Take a break</button>'
+'<button id="km-sl-override">Keep playing anyway</button>'
+'</div>';
document.body.appendChild(el);
el.querySelector('#km-sl-dismiss').addEventListener('click', function(){el.remove();});
el.querySelector('#km-sl-override').addEventListener('click', function(){
pluginState.sessionLockTriggered = false; el.remove();
});
}
function updateBannerPreview() {
var el = document.getElementById('km-banner-preview'); if(!el) return;
var nick = playerData ? (playerData.Nickname||'You') : 'You';
var name = profileCustom.bannerText || nick;
if(profileCustom.bannerUrl) {
el.style.backgroundImage = 'url('+profileCustom.bannerUrl+')';
el.style.backgroundSize = 'cover';
el.style.backgroundPosition = 'center';
el.style.background = '';
} else {
el.style.backgroundImage = '';
el.style.background = 'linear-gradient(135deg,'+profileCustom.bannerColor+'dd,'+profileCustom.bannerColor+'66)';
}
var av = '';
el.innerHTML = '<div class="km-banner-bg-overlay"></div>'
+'<div class="km-banner-content">'
+av
+'<div class="km-banner-text-wrap">'
+'<div class="km-banner-goal" style="font-size:20px">⚽ GOAL!</div>'
+'<div class="km-banner-player">'+name+'</div>'
+'</div>'
+'</div>';
}
// ─── PARTY CODE POPUP ────────────────────────────────────────────────────
function showPartyCodePopup(partyCode) {
var old = document.getElementById('km-party-popup'); if(old) old.remove();
var nick = playerData ? (playerData.Nickname||'Player') : 'Player';
var msg = '🎮 Join my RocketGoal.io party!\nCode: '+partyCode+'\n(via KeyMod by @keydopz)';
var el = document.createElement('div');
el.id = 'km-party-popup';
el.innerHTML = [
'<div id="km-party-header">',
'<span id="km-party-title">🎮 Party Created!</span>',
'<button id="km-party-close">✕</button>',
'</div>',
'<div id="km-party-code">'+partyCode+'</div>',
'<div id="km-party-sub">Share this code with your friends</div>',
'<div id="km-party-btns">',
'<button class="km-party-btn" id="km-party-copy-code">📋 Copy Code</button>',
'<button class="km-party-btn primary" id="km-party-copy-msg">💬 Copy Discord Message</button>',
'</div>',
].join('');
document.body.appendChild(el);
el.querySelector('#km-party-close').addEventListener('click', function(){
el.style.opacity='0'; setTimeout(function(){el.remove();},250);
});
el.querySelector('#km-party-copy-code').addEventListener('click', function(){
navigator.clipboard.writeText(partyCode).then(function(){showToast('Code copied!','ok');}).catch(function(){fallbackCopy(partyCode);});
kmLog('Party code copied: '+partyCode);
});
el.querySelector('#km-party-copy-msg').addEventListener('click', function(){
navigator.clipboard.writeText(msg).then(function(){showToast('Discord message copied!','ok');}).catch(function(){fallbackCopy(msg);});
kmLog('Party message copied');
});
// Animate in
setTimeout(function(){el.classList.add('show');},10);
}
// ─── SUPPORT POPUP ───────────────────────────────────────────────────────
function buildSupportPopup() {
var overlay = document.createElement('div');
overlay.id = 'km-support-overlay';
overlay.innerHTML = [
'<div id="km-support-box">',
'<button id="km-support-close">✕</button>',
'<div id="km-support-icon">',
'<svg viewBox="0 0 48 48" width="52" height="52" xmlns="http://www.w3.org/2000/svg">',
'<path d="M38.9 10.8a13.1 13.1 0 0 1-7.8-7.8h-6v28.6a5.5 5.5 0 1 1-3.9-5.3V20a12 12 0 1 0 10 11.6V19a19.3 19.3 0 0 0 11.3 3.6v-5.9a13.2 13.2 0 0 1-3.6-.9z" fill="white"/>',
'<path d="M38.9 10.8a13.1 13.1 0 0 1-7.8-7.8h-6v28.6a5.5 5.5 0 1 1-3.9-5.3V20a12 12 0 1 0 10 11.6V19a19.3 19.3 0 0 0 11.3 3.6v-5.9a13.2 13.2 0 0 1-3.8-5.9z" fill="#69C9D0" opacity="0.5"/>',
'<path d="M31.1 3h-6v28.6a5.5 5.5 0 1 1-3.9-5.3V20a12 12 0 1 0 10 11.6V19a19.3 19.3 0 0 0 11.3 3.6v-5.9A13.1 13.1 0 0 1 31.1 3z" fill="#EE1D52" opacity="0.7"/>',
'</svg>',
'</div>',
'<div id="km-support-title">Support me?</div>',
'<div id="km-support-body">',
'<strong>Follow @keydopz on TikTok</strong><br>',
'<span>New RocketGoal tutorials and setup videos drop weekly.</span>',
'</div>',
'<a id="km-support-btn" href="https://www.tiktok.com/@keydopz" target="_blank">OPEN TIKTOK</a>',
'</div>',
].join('');
document.body.appendChild(overlay);
overlay.querySelector('#km-support-close').addEventListener('click', function() {
overlay.style.opacity = '0';
setTimeout(function() { overlay.remove(); }, 300);
});
overlay.querySelector('#km-support-btn').addEventListener('click', function() {
overlay.style.opacity = '0';
setTimeout(function() { overlay.remove(); }, 300);
});
}
// ─── RESIZE HANDLE ────────────────────────────────────────────────────────
function addResizeHandle() {
var menu = document.getElementById('km-menu'); if (!menu) return;
var edges = [
{ id:'km-rh-e', cursor:'e-resize', style:'position:absolute;top:20px;right:-4px;width:8px;height:calc(100% - 40px);cursor:e-resize;z-index:10;' },
{ id:'km-rh-w', cursor:'w-resize', style:'position:absolute;top:20px;left:-4px;width:8px;height:calc(100% - 40px);cursor:w-resize;z-index:10;' },
{ id:'km-rh-s', cursor:'s-resize', style:'position:absolute;bottom:-4px;left:20px;height:8px;width:calc(100% - 40px);cursor:s-resize;z-index:10;' },
{ id:'km-rh-se', cursor:'se-resize', style:'position:absolute;bottom:-4px;right:-4px;width:16px;height:16px;cursor:se-resize;z-index:11;' },
{ id:'km-rh-sw', cursor:'sw-resize', style:'position:absolute;bottom:-4px;left:-4px;width:16px;height:16px;cursor:sw-resize;z-index:11;' },
];
edges.forEach(function(edge) {
var h = document.createElement('div');
h.id = edge.id; h.style.cssText = edge.style;
menu.appendChild(h);
h.addEventListener('mousedown', function(e) {
e.preventDefault(); e.stopPropagation();
var startX = e.clientX, startY = e.clientY;
var startW = menu.offsetWidth, startH = menu.offsetHeight;
var startL = menu.offsetLeft;
function onMove(e) {
var dx = e.clientX - startX, dy = e.clientY - startY;
var newW = startW, newH = startH, newL = startL;
var minW = 360, minH = 280;
if (edge.cursor.includes('e')) newW = Math.max(minW, startW + dx);
if (edge.cursor.includes('s')) newH = Math.max(minH, startH + dy);
if (edge.cursor.includes('w')) { newW = Math.max(minW, startW - dx); newL = startL + (startW - newW); }
menu.style.width = newW + 'px';
menu.style.left = newL + 'px';
var paneH = Math.max(200, newH - 120);
menu.querySelectorAll('.km-pane').forEach(function(p){p.style.maxHeight=paneH+'px';});
var cb = document.getElementById('km-console-body');
if(cb) cb.style.height = paneH + 'px';
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
});
// SE corner visual indicator
var se = document.getElementById('km-rh-se');
if(se) se.innerHTML = '<svg width="10" height="10" viewBox="0 0 10 10" style="position:absolute;bottom:3px;right:3px;opacity:0.3"><path d="M10 10 L10 6 L6 10 Z M10 5 L10 3 L3 10 L5 10 Z" fill="white"/></svg>';
}
// ─── ON-SCREEN KEYBOARD ─────────────────────────────────────────────────────
function attachOSKToMenu(el) {
// el can be passed or auto-detected
var root = el || document.getElementById('km-menu');
if (!root) return;
var searchInput = root.querySelector('#km-plugin-search');
var oskToggle = root.querySelector('#km-osk-toggle');
var oskWrap = root.querySelector('#km-osk-wrap');
if (!searchInput || !oskToggle || !oskWrap) return;
// Keyboard rows
var rows = [
['q','w','e','r','t','y','u','i','o','p'],
['a','s','d','f','g','h','j','k','l'],
['z','x','c','v','b','n','m'],
];
function buildOSK() {
oskWrap.innerHTML = '';
var kb = document.createElement('div');
kb.style.cssText = 'background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:6px;';
rows.forEach(function(row) {
var rowEl = document.createElement('div');
rowEl.style.cssText = 'display:flex;justify-content:center;gap:3px;margin-bottom:3px;';
row.forEach(function(k) {
var btn = document.createElement('button');
btn.textContent = k;
btn.style.cssText = 'width:28px;height:28px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.15);color:#fff;font-family:"DM Mono",monospace;font-size:11px;cursor:pointer;border-radius:4px;transition:background 0.08s;padding:0;';
btn.addEventListener('mouseenter', function() { btn.style.background='rgba(255,255,255,0.25)'; });
btn.addEventListener('mouseleave', function() { btn.style.background='rgba(255,255,255,0.1)'; });
btn.addEventListener('click', function(e) {
e.stopPropagation();
searchInput.value += k;
renderPluginStore();
});
rowEl.appendChild(btn);
});
kb.appendChild(rowEl);
});
// Bottom row: Space, Backspace, Clear
var bottomRow = document.createElement('div');
bottomRow.style.cssText = 'display:flex;justify-content:center;gap:3px;';
function specBtn(label, w, action) {
var b = document.createElement('button');
b.textContent = label;
b.style.cssText = 'width:'+w+'px;height:28px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);color:rgba(255,255,255,0.7);font-family:"DM Mono",monospace;font-size:10px;cursor:pointer;border-radius:4px;transition:background 0.08s;padding:0;';
b.addEventListener('mouseenter', function() { b.style.background='rgba(255,255,255,0.2)'; });
b.addEventListener('mouseleave', function() { b.style.background='rgba(255,255,255,0.08)'; });
b.addEventListener('click', function(e) { e.stopPropagation(); action(); renderPluginStore(); });
return b;
}
bottomRow.appendChild(specBtn('space', 100, function() { searchInput.value += ' '; }));
bottomRow.appendChild(specBtn('⌫', 40, function() { searchInput.value = searchInput.value.slice(0,-1); }));
bottomRow.appendChild(specBtn('✕ clear', 64, function() { searchInput.value = ''; }));
kb.appendChild(bottomRow);
oskWrap.appendChild(kb);
}
var oskVisible = false;
oskToggle.addEventListener('click', function(e) {
e.stopPropagation();
oskVisible = !oskVisible;
if (oskVisible) {
buildOSK();
oskWrap.style.display = 'block';
oskToggle.style.color = 'rgba(255,255,255,0.9)';
} else {
oskWrap.style.display = 'none';
oskToggle.style.color = 'rgba(255,255,255,0.4)';
}
});
// Also allow clicking the read-only input to open keyboard
searchInput.addEventListener('click', function() {
if (!oskVisible) oskToggle.click();
});
}
// ─── PLUGIN STORE ─────────────────────────────────────────────────────────
var CATEGORIES = ['All','Training','Stats','Visual','Utility','Fun'];
var storeFilter = 'All';
function renderPluginStore() {
var el = document.getElementById('km-plugin-store-grid');
if(!el) return;
// Category filter buttons
var filterHtml = '<div class="km-store-filters">'
+ CATEGORIES.map(function(c) {
return '<button class="km-store-cat'+(storeFilter===c?' active':'')+'" data-cat="'+c+'">'+c+'</button>';
}).join('')
+ '</div>';
var searchQ = (document.getElementById('km-plugin-search')||{value:''}).value.toLowerCase().trim();
var plugins = (storeFilter === 'All' ? PLUGINS : PLUGINS.filter(function(p){return p.category===storeFilter;}))
.filter(function(p){ return !searchQ || p.name.toLowerCase().includes(searchQ) || p.desc.toLowerCase().includes(searchQ); });
var cardsHtml = '<div class="km-store-grid">'
+ plugins.map(function(p) {
var inst = isInstalled(p.id);
return '<div class="km-plugin-card">'
+ '<div class="km-plugin-icon">'+p.icon+'</div>'
+ '<div class="km-plugin-info">'
+ '<div class="km-plugin-name">'+p.name+'</div>'
+ '<div class="km-plugin-cat">'+p.category+' · v'+p.version+'</div>'
+ '<div class="km-plugin-desc">'+p.desc+'</div>'
+ '</div>'
+ '<button class="km-plugin-install-btn'+(inst?' installed':'')+'" data-id="'+p.id+'">'
+ (inst ? '✓ Installed' : 'Install')
+ '</button>'
+ '</div>';
}).join('')
+ '</div>';
el.innerHTML = filterHtml + cardsHtml;
// Wire filter buttons
var searchInput = document.getElementById('km-plugin-search');
if (searchInput) searchInput.addEventListener('input', function() { renderPluginStore(); });
el.querySelectorAll('.km-store-cat').forEach(function(btn) {
btn.addEventListener('click', function() {
storeFilter = btn.dataset.cat;
renderPluginStore();
});
});
// Wire install buttons
el.querySelectorAll('.km-plugin-install-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
try {
e.stopPropagation();
var id = btn.dataset.id;
kmLog('Install button clicked: '+id);
if(isInstalled(id)) {
installedPlugins = installedPlugins.filter(function(x){return x!==id;});
showToast('Plugin uninstalled', 'warn');
kmLog('Plugin uninstalled: '+id);
} else {
installedPlugins.push(id);
showToast('Plugin installed! ✓', 'ok');
kmLog('Plugin installed: '+id);
PLUGINS.forEach(function(p) {
if(p.id===id) {
p.settings.forEach(function(s) {
if(!pluginSettings[id]) pluginSettings[id]={};
if(pluginSettings[id][s.id]===undefined) pluginSettings[id][s.id]=s.default;
});
}
});
lsSet('pluginSettings5', pluginSettings);
if (id==='perfMetrics') setTimeout(pluginPerfMetricsMount, 50);
if (id==='screenshotRec' || id==='clipRecorder' || id==='streamerMode' || id==='devTools') setTimeout(pluginStreamerMount, 50);
if (id==='trollPlugin') {} // troll never auto-starts — session flag only
if (id==='trollPlugin') {} // Troll never auto-mounts — session flag only
if (id==='peakGhost') { pluginPeakGhostInit(); setTimeout(pluginPeakGhostUpdate,200); }
if (id==='optimizer') setTimeout(pluginOptimizerMount, 50);
if (id==='visualFX') setTimeout(pluginVFXMount, 50);
if (id==='dejaVu') showToast('🗂️ Déjà Vu active', 'ok');
}
lsSet('plugins5', installedPlugins);
renderPluginStore();
renderOwnedPlugins();
setTimeout(function(){attachOSKToMenu();},50);
} catch(err) {
kmLog('Install button error: '+(err&&err.message||String(err)));
showToast('Error: '+(err&&err.message||'unknown'), 'err');
}
});
});
}
// ─── OWNED PLUGINS ────────────────────────────────────────────────────────
function renderOwnedPlugins() {
var el = document.getElementById('km-owned-plugins-list');
if(!el) return;
var owned = PLUGINS.filter(function(p){return isInstalled(p.id);});
if(!owned.length) {
el.innerHTML = '<div class="km-empty">No plugins installed — visit Plugin Store</div>';
return;
}
el.innerHTML = owned.map(function(p) {
// Optimizer gets a custom checkbox group renderer
var settingsHtml;
var hasCheckboxes = p.settings.length && p.settings[0].type === 'checkbox';
if (hasCheckboxes) {
// Build Check All / Uncheck All header
var checkAllHtml = '<div style="display:flex;gap:8px;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,0.08);">'
+ '<button class="km-btn km-opt-checkall" data-plugin="'+p.id+'" data-action="all" style="margin-top:0;padding:5px 12px;font-size:11px;flex:1;">✓ Check All</button>'
+ '<button class="km-btn km-opt-checkall" data-plugin="'+p.id+'" data-action="none" style="margin-top:0;padding:5px 12px;font-size:11px;flex:1;">✗ Uncheck All</button>'
+ '</div>';
// Group settings by their group field
var groups = [], groupMap = {};
p.settings.forEach(function(s) {
var g = s.group || 'General';
if (!groupMap[g]) { groupMap[g] = []; groups.push(g); }
groupMap[g].push(s);
});
var groupsHtml = groups.map(function(g) {
var rows = groupMap[g].map(function(s) {
var val = getPluginSetting(p.id, s.id);
var checked = (val === 'on' || val === true) ? 'checked' : '';
return '<div class="km-toggle-row">'
+ '<label>'
+ '<input type="checkbox" class="km-plugin-setting km-opt-cb" data-plugin="'+p.id+'" data-setting="'+s.id+'"'
+ (checked ? ' checked' : '') + '>'
+ s.label
+ '</label>'
+ '</div>';
}).join('');
return '<div style="margin-bottom:10px;">'
+ '<div style="font-size:10px;font-weight:600;color:rgba(255,255,255,0.3);margin-bottom:6px;letter-spacing:0;">'+g+'</div>'
+ rows
+ '</div>';
}).join('');
settingsHtml = checkAllHtml + groupsHtml;
} else {
settingsHtml = p.settings.length ? p.settings.map(function(s) {
var val = getPluginSetting(p.id, s.id);
if(s.type==='number') {
return '<div class="km-stat-row"><span>'+s.label+'</span>'
+ '<input type="number" class="km-input km-plugin-setting" data-plugin="'+p.id+'" data-setting="'+s.id+'"'
+ ' value="'+val+'" min="'+(s.min||0)+'" max="'+(s.max||999)+'" style="width:70px"></div>';
} else if(s.type==='select') {
var opts = (s.options||[]).map(function(o){
return '<option value="'+o+'"'+(o===val?' selected':'')+'>'+o.charAt(0).toUpperCase()+o.slice(1)+'</option>';
}).join('');
return '<div class="km-stat-row"><span>'+s.label+'</span>'
+ '<select class="km-input km-plugin-setting" data-plugin="'+p.id+'" data-setting="'+s.id+'" style="width:120px">'
+ opts + '</select></div>';
} else if(s.type==='range') {
var rMin=s.min!==undefined?s.min:0, rMax=s.max!==undefined?s.max:100;
var rVal=parseInt(val)||rMin;
return '<div class="km-stat-row" style="flex-direction:column;align-items:stretch;gap:4px;">'
+'<div style="display:flex;justify-content:space-between;">'
+'<span>'+s.label+'</span>'
+'<span class="km-range-val" style="font-family:monospace;font-size:12px;color:rgba(255,255,255,0.6);" id="km-rv-'+p.id+'-'+s.id+'">'+rVal+'</span>'
+'</div>'
+'<input type="range" class="km-plugin-setting km-range-inp" data-plugin="'+p.id+'" data-setting="'+s.id+'"'
+' value="'+rVal+'" min="'+rMin+'" max="'+rMax+'" style="width:100%;accent-color:var(--km-accent,#006eff);cursor:pointer;">'
+'</div>';
} else {
return '<div class="km-stat-row"><span>'+s.label+'</span>'
+ '<input type="text" class="km-input km-plugin-setting" data-plugin="'+p.id+'" data-setting="'+s.id+'"'
+ ' value="'+val+'" style="width:120px"></div>';
}
}).join('') : '<div style="font-size:11px;color:rgba(255,255,255,0.25);padding:4px 0">No settings</div>';
}
return '<div class="km-owned-card" id="owned-'+p.id+'">'
+ '<div class="km-owned-header" data-id="'+p.id+'">'
+ '<span class="km-owned-icon">'+p.icon+'</span>'
+ '<span class="km-owned-name">'+p.name+'</span>'
+ '<span class="km-owned-arrow">▾</span>'
+ '</div>'
+ '<div class="km-owned-body" id="owned-body-'+p.id+'" style="display:none">'
+ '<div style="font-size:11px;color:rgba(255,255,255,0.35);margin-bottom:8px">'+p.desc+'</div>'
+ settingsHtml
+ '<button class="km-btn danger km-plugin-uninstall" data-id="'+p.id+'" style="margin-top:8px">Uninstall</button>'
+ '</div>'
+ '</div>';
}).join('');
// Wire dropdowns
el.querySelectorAll('.km-owned-header').forEach(function(hdr) {
hdr.addEventListener('click', function() {
var body = document.getElementById('owned-body-'+hdr.dataset.id);
var arrow = hdr.querySelector('.km-owned-arrow');
if(body) {
var open = body.style.display !== 'none';
body.style.display = open ? 'none' : 'block';
if(arrow) arrow.textContent = open ? '▾' : '▴';
}
});
});
// Wire settings inputs (select, number, text)
el.querySelectorAll('.km-plugin-setting').forEach(function(inp) {
if (inp.type === 'checkbox') return; // handled separately below
inp.addEventListener('change', function() {
var val = inp.type==='number' ? parseInt(inp.value) : inp.value;
setPluginSetting(inp.dataset.plugin, inp.dataset.setting, val);
kmLog('Plugin setting updated: '+inp.dataset.plugin+' '+inp.dataset.setting+'='+val);
if(inp.dataset.plugin==='perfMetrics') { pluginPerfMetricsUnmount(); pluginPerfMetricsMount(); }
if(inp.dataset.plugin==='visualFX') {
pluginVFXUnmount(); pluginVFXMount();
// If preset changed, re-render owned plugins to update slider positions
if (inp.dataset.setting === 'preset') setTimeout(renderOwnedPlugins, 80);
}
if(inp.dataset.plugin==='nightMode' && inp.dataset.setting==='hour') checkNightMode();
if(inp.dataset.plugin==='trollPlugin' && inp.dataset.setting==='enabled') {
// Session-only: troll state lives in window._kmTrollActive, never auto-reads storage
window._kmTrollActive = (val === 'on');
if (window._kmTrollActive) { pluginTrollMount(); showToast('🎭 Troll Mode ON — chaos incoming','ok'); }
else { pluginTrollUnmount(); showToast('Troll Mode off','ok'); }
}
});
});
// Wire range sliders
el.querySelectorAll('.km-range-inp').forEach(function(rng) {
rng.addEventListener('input', function() {
var val = parseInt(rng.value);
setPluginSetting(rng.dataset.plugin, rng.dataset.setting, val);
var lbl = document.getElementById('km-rv-'+rng.dataset.plugin+'-'+rng.dataset.setting);
if (lbl) lbl.textContent = val;
if (rng.dataset.plugin === 'visualFX') { pluginVFXUnmount(); pluginVFXMount(); }
});
});
// Wire optimizer checkbox settings
el.querySelectorAll('.km-opt-cb').forEach(function(cb) {
cb.addEventListener('change', function() {
var val = cb.checked ? 'on' : 'off';
setPluginSetting(cb.dataset.plugin, cb.dataset.setting, val);
kmLog('Optimizer: ' + cb.dataset.setting + ' = ' + val);
// Re-run optimizer with new settings
if (cb.dataset.plugin === 'optimizer') {
pluginOptimizerUnmount();
setTimeout(pluginOptimizerMount, 50);
}
});
});
// Wire Check All / Uncheck All buttons
el.querySelectorAll('.km-opt-checkall').forEach(function(btn) {
btn.addEventListener('click', function() {
var pluginId = btn.dataset.plugin;
var action = btn.dataset.action; // 'all' or 'none'
var val = action === 'all' ? 'on' : 'off';
// Find dangerous settings to skip when checking all
var dangerousIds = ['stopload','frustum'];
var card = document.getElementById('owned-body-' + pluginId);
if (!card) return;
card.querySelectorAll('.km-opt-cb[data-plugin="' + pluginId + '"]').forEach(function(cb) {
// Skip dangerous tweaks in check-all to prevent game breaking
if (action === 'all' && dangerousIds.indexOf(cb.dataset.setting) !== -1) return;
cb.checked = (action === 'all');
setPluginSetting(cb.dataset.plugin, cb.dataset.setting, val);
});
kmLog('Optimizer: ' + (action === 'all' ? 'all tweaks enabled' : 'all tweaks disabled'));
// Re-run optimizer
pluginOptimizerUnmount();
setTimeout(pluginOptimizerMount, 80);
});
});
// Wire uninstall
el.querySelectorAll('.km-plugin-uninstall').forEach(function(btn) {
btn.addEventListener('click', function() {
var id = btn.dataset.id;
installedPlugins = installedPlugins.filter(function(x){return x!==id;});
lsSet('plugins5', installedPlugins);
// Unmount persistent plugins
if (id==='perfMetrics') { pluginPerfMetricsUnmount(); netBridgeStop(); }
if (id==='screenshotRec' || id==='clipRecorder' || id==='streamerMode' || id==='devTools') { pluginScreenshotUnmount(); pluginClipRecorderUnmount(); pluginDevToolsUnmount(); }
if (id==='trollPlugin') { window._kmTrollActive = false; pluginTrollUnmount(); }
if (id==='trollPlugin') { window._kmTrollActive = false; pluginTrollUnmount(); }
if (id==='peakGhost') pluginPeakGhostUnmount();
if (id==='optimizer') pluginOptimizerUnmount();
if (id==='visualFX') pluginVFXUnmount();
showToast('Plugin uninstalled', 'warn');
kmLog('Plugin uninstalled: '+id);
renderOwnedPlugins();
renderPluginStore();
setTimeout(function(){attachOSKToMenu();},50);
});
});
}
// ─── PLUGIN RUNTIME ───────────────────────────────────────────────────────
// Called from relevant game events to run active plugins
// Speed Run
var speedRunStart = null;
var speedRunBest = lsGet('speedRunBest5', null);
function pluginSpeedRunMatchStart() {
if(!isInstalled('speedRun')) return;
speedRunStart = Date.now();
}
function pluginSpeedRunGoal() {
if(!isInstalled('speedRun') || !speedRunStart) return;
var elapsed = ((Date.now() - speedRunStart) / 1000).toFixed(1);
speedRunStart = null;
if(!speedRunBest || parseFloat(elapsed) < parseFloat(speedRunBest)) {
speedRunBest = elapsed;
lsSet('speedRunBest5', speedRunBest);
showToast('⚡ New Speed Run Best: '+elapsed+'s!', 'ok');
} else {
showToast('⚡ First goal: '+elapsed+'s (best: '+speedRunBest+'s)', 'info');
}
}
// AutoGG
function pluginAutoGG() {
if(!isInstalled('autoGG')) return;
var msg = getPluginSetting('autoGG','msg') || 'GG';
navigator.clipboard.writeText(msg).catch(function(){fallbackCopy(msg);});
showToast('AutoGG: "'+msg+'" copied', 'ok');
}
// Session Lock
function pluginSessionLock() {
if(!isInstalled('sessionLock')) return;
var max = getPluginSetting('sessionLock','maxLosses') || 5;
if(session.losses >= max) {
var el = document.createElement('div');
el.id = 'km-session-lock';
el.innerHTML = '<div id="km-lock-inner">'
+'<div id="km-lock-icon">🔒</div>'
+'<div id="km-lock-title">Session Lock</div>'
+'<div id="km-lock-msg">You have lost '+session.losses+' times this session.<br>Take a break and come back stronger.</div>'
+'<button id="km-lock-dismiss">I understand — unlock</button>'
+'</div>';
document.body.appendChild(el);
document.getElementById('km-lock-dismiss').addEventListener('click', function(){el.remove();});
}
}
// Momentum meter
function updateMomentumMeter() {
if(!isInstalled('momentumMeter')) return;
var el = document.getElementById('km-momentum-bar');
if(!el) {
var wrap = document.createElement('div'); wrap.id='km-momentum-wrap';
wrap.innerHTML='<div id="km-momentum-label">MOMENTUM</div><div id="km-momentum-track"><div id="km-momentum-bar"></div></div>';
document.body.appendChild(wrap);
el = document.getElementById('km-momentum-bar');
}
var total = session.wins + session.losses;
var pct = total > 0 ? Math.round((session.wins/total)*100) : 50;
el.style.width = pct+'%';
el.style.background = pct>=50?'#4ade80':'#f87171';
}
// Rank Anxiety
function checkRankAnxiety() {
if(!isInstalled('rankAnxiety')||!playerData) return;
var threshold = getPluginSetting('rankAnxiety','threshold')||30;
var mk = 'Competitive3v3';
var g = playerData.ModesGlicko&&playerData.ModesGlicko[mk];
if(!g) return;
var mmr = g.displayRating;
var rank = getRank(mmr);
var idx = RANKS.indexOf(rank);
if(idx>0) {
var floor = RANKS[idx].min;
var dist = mmr - floor;
var hud = document.getElementById('km-hud');
if(dist <= threshold) {
if(hud) hud.style.borderLeftColor='#f87171';
showToast('⚠ Only '+dist+' MMR from rank drop!','err');
} else {
if(hud) hud.style.borderLeftColor='';
}
}
}
// Opponent Memory
function pluginOpponentMemory(oppId, oppName) {
if(!isInstalled('opponentMemory')||!oppId) return;
var history = opponentLog.filter(function(o){return o.opponentId===oppId;});
if(!history.length) return;
var wins=0,losses=0;
history.forEach(function(o){if(o.result==='Win')wins++;else losses++;});
showToast('🧠 vs '+streamerName(oppName||history[0].opponent||'Opponent')+': '+wins+'W / '+losses+'L','info');
}
// Focus Mode
function pluginFocusMode(active) {
if(!isInstalled('focusMode')) return;
var el = document.getElementById('km-focus-overlay');
if(active) {
if(!el) {
el = document.createElement('div'); el.id='km-focus-overlay';
document.body.appendChild(el);
}
var op = (getPluginSetting('focusMode','opacity')||60)/100;
el.style.opacity = op;
} else {
if(el) el.remove();
}
}
// Quick Notes on Goal
var sessionNotes = [];
function pluginQuickNote() {
if(!isInstalled('quickNotes')) return;
var secs = Math.round((Date.now()-session.startTime)/1000);
var stamp = Math.floor(secs/60)+':'+String(secs%60).padStart(2,'0');
sessionNotes.push('Goal at '+stamp+' — score '+matchScore.me+'-'+matchScore.opp);
}
// Match Predictor
function pluginMatchPredictor() {
if(!isInstalled('matchPredictor')||!playerData) return;
var mk = currentMode==='1v1'?'Competitive1v1':currentMode==='2v2'?'Competitive2v2':'Competitive3v3';
var g = playerData.ModesGlicko&&playerData.ModesGlicko[mk];
if(!g) {
showToast('🔮 Match Predictor: Waiting for rank data...', 'info');
return;
}
var base = 50;
if (opponentMMR) {
var diff = g.displayRating - opponentMMR;
base = 50 + (diff / 10);
} else if (typeof session.streak === 'number') {
base = 50 + Math.max(-12, Math.min(12, (session.streak || 0) * 3));
}
var prob = Math.round(Math.max(10, Math.min(90, base)));
var emoji = prob > 60 ? '👍' : prob < 40 ? '⚠' : '✨';
showToast('🔮 Match Predictor: '+emoji+' '+prob+'% win chance', 'info');
}
// Daily Challenge
var CHALLENGES = [
'Score 3 goals in a single session',
'Win 2 matches in a row',
'Maintain 50%+ shot accuracy',
'Win a 1v1 match',
'Score first goal in 3 matches',
'Win without conceding',
'Play 5 matches today',
'Score 5 goals total today',
];
var todayChallenge = lsGet('dailyChallenge5', {date:'',challenge:'',done:false});
if(todayChallenge.date !== today) {
todayChallenge = {date:today, challenge:CHALLENGES[Math.floor(Math.random()*CHALLENGES.length)], done:false};
lsSet('dailyChallenge5', todayChallenge);
}
function checkDailyChallenge() {
if(!isInstalled('dailyChallenge')||todayChallenge.done) return;
var c = todayChallenge.challenge;
var done = false;
if(c.includes('3 goals')&&session.goals>=3) done=true;
if(c.includes('2 matches')&&session.streak>=2) done=true;
if(c.includes('50%')&&session.shots>0&&(session.goals/session.shots)>=0.5) done=true;
if(c.includes('5 matches')&&session.matches>=5) done=true;
if(c.includes('5 goals')&&session.goals>=5) done=true;
if(done) {
todayChallenge.done=true; lsSet('dailyChallenge5',todayChallenge);
showToast('📅 Daily Challenge Complete!','ok');
}
}
// Performance Coach
function pluginPerformanceCoach() {
if(!isInstalled('performanceCoach')||session.matches<5||session.matches%5!==0) return;
var tips = [];
var acc = session.shots>0?(session.goals/session.shots):0;
if(acc<0.3) tips.push('Your shot accuracy is low — focus on shot quality over quantity');
if(session.losses>session.wins) tips.push('Losing more than winning — try a different mode or take a short break');
if(session.conceded>session.goals) tips.push('Conceding a lot — focus on defensive positioning');
if(session.streak>0) tips.push('You are on a '+session.streak+' win streak — keep the momentum!');
if(!tips.length) tips.push('Looking good! Keep playing your game.');
showToast('🏋️ Coach: '+tips[0],'info');
}
// Streak Saver
function pluginStreakSaver() {
if(!isInstalled('streakSaver')) return;
var min = getPluginSetting('streakSaver','minStreak')||3;
if(session.streak>=min) showToast('💎 Protect your '+session.streak+'-win streak!','warn');
}
// Night Mode Auto
function checkNightMode() {
if(!isInstalled('nightMode')) return;
var hour = getPluginSetting('nightMode','hour')||22;
if(new Date().getHours()>=hour && theme.preset!=='black') {
theme.preset='black'; lsSet('theme5',theme); applyTheme();
showToast('🌙 Night Mode activated','info');
}
}
// Rank Up Simulator
function pluginRankUpSim() {
if(!isInstalled('rankUpSim')||!playerData) return;
var mk='Competitive3v3';
var g=playerData.ModesGlicko&&playerData.ModesGlicko[mk]; if(!g) return;
var mmr=g.displayRating, rank=getRank(mmr), idx=RANKS.indexOf(rank);
if(idx>=RANKS.length-1){showToast('🚀 You are SSL — max rank!','ok');return;}
var needed=RANKS[idx+1].min - mmr;
var avg=matchHistory.length>0
? matchHistory.filter(function(m){return m.diff>0;}).reduce(function(a,m){return a+(m.diff||0);},0)/Math.max(1,matchHistory.filter(function(m){return m.diff>0;}).length)
: 20;
var wins=Math.ceil(needed/Math.max(1,avg));
showToast('🚀 ~'+wins+' wins to '+RANKS[idx+1].name,'info');
}
// XP Calculator
function pluginXPCalc() {
if(!isInstalled('xpCalc')||!playerData) return;
var milestones=[1000,5000,10000,25000,50000,100000,250000,500000,999999];
var xp=playerData.AccountXp||0;
var next=milestones.find(function(m){return m>xp;});
if(!next){showToast('⭐ Max XP reached!','ok');return;}
var avgXP=xpHistory.length>0?xpHistory.reduce(function(a,x){return a+x.gained;},0)/xpHistory.length:25;
var matches=Math.ceil((next-xp)/Math.max(1,avgXP));
showToast('⭐ ~'+matches+' matches to '+next.toLocaleString()+' XP','info');
}
// Goal Streak
var goalStreakCount = 0;
function pluginGoalStreakScore() {
if(!isInstalled('goalStreak')) return;
goalStreakCount++;
updateLeaderboard();
}
function pluginGoalStreakConcede() {
if(!isInstalled('goalStreak')) return;
goalStreakCount=0;
updateLeaderboard();
}
// ─── WIN PREDICTOR ────────────────────────────────────────────────────────
function pluginWinPredictorUpdate() {
if (!isInstalled('winPredictor')) return;
var el = document.getElementById('km-lb-winprob');
if (!el) {
// Inject into leaderboard widget
var lb = document.getElementById('km-leaderboard');
if (!lb) return;
var d = document.createElement('div');
d.id = 'km-lb-winprob';
d.style.cssText = 'font-family:"DM Mono",monospace;font-size:10px;color:rgba(255,255,255,0.3);text-align:center;margin-top:4px;padding-top:4px;border-top:1px solid rgba(255,255,255,0.06);';
lb.appendChild(d);
el = d;
}
var myScore = matchScore.me;
var oppScore = matchScore.opp;
var scoreDiff = myScore - oppScore;
// Base probability from score differential
var prob = 50 + (scoreDiff * 18);
// Adjust for MMR diff if known
if (opponentMMR && playerData && playerData.ModesGlicko) {
var mk = currentMode==='1v1'?'Competitive1v1':currentMode==='2v2'?'Competitive2v2':'Competitive3v3';
var myMMR = playerData.ModesGlicko[mk] ? playerData.ModesGlicko[mk].displayRating : null;
if (myMMR) {
var mmrAdv = (myMMR - opponentMMR) / 10;
prob += Math.max(-20, Math.min(20, mmrAdv));
}
}
prob = Math.max(5, Math.min(95, Math.round(prob)));
var col = prob >= 60 ? '#4ade80' : prob <= 40 ? '#f87171' : '#fbbf24';
el.innerHTML = '<span style="color:'+col+';font-size:12px;font-weight:700;">'+prob+'%</span> win prob';
}
// ─── SHOT ANALYSIS ─────────────────────────────────────────────────────────
var shotAnalysisData = { myShots:0, myGoals:0, oppShots:0, oppGoals:0 };
function pluginShotAnalysisShot(isMe) {
if (!isInstalled('shotAnalysis')) return;
if (isMe) shotAnalysisData.myShots++;
else shotAnalysisData.oppShots++;
}
function pluginShotAnalysisGoal(isMe) {
if (!isInstalled('shotAnalysis')) return;
if (isMe) shotAnalysisData.myGoals++;
else shotAnalysisData.oppGoals++;
renderShotAnalysisHUD();
}
function renderShotAnalysisHUD() {
if (!isInstalled('shotAnalysis')) return;
var el = document.getElementById('km-shot-analysis-hud');
if (!el) {
el = document.createElement('div');
el.id = 'km-shot-analysis-hud';
el.style.cssText = 'position:fixed;top:12px;left:12px;z-index:99996;background:rgba(10,12,18,0.9);border:1px solid rgba(255,255,255,0.12);border-radius:12px;padding:10px;min-width:240px;font-family:"DM Mono",monospace;font-size:11px;color:#fff;pointer-events:auto;cursor:move;backdrop-filter:blur(6px);';
document.body.appendChild(el);
makeDraggable(el, el);
}
var d = shotAnalysisData;
var myAcc = d.myShots > 0 ? Math.round((d.myGoals / d.myShots) * 100) : 0;
var oppAcc = d.oppShots > 0 ? Math.round((d.oppGoals / d.oppShots) * 100) : 0;
var meLabel = streamerName(matchPlayers.me || 'You');
var oppLabel = streamerName(matchPlayers.opponent || (currentMode==='2v2' ? 'Opponents' : 'Opponent'));
var grid = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;">'
+'<div style="font-size:9px;color:rgba(255,255,255,0.45);text-transform:uppercase;">'+meLabel+'</div>'
+'<div style="font-size:9px;color:rgba(255,255,255,0.45);text-transform:uppercase;">'+oppLabel+'</div>'
+'<div style="font-size:13px;font-weight:800;color:#4ade80;">'+d.myGoals+'G</div>'
+'<div style="font-size:13px;font-weight:800;color:#f87171;">'+d.oppGoals+'G</div>'
+'<div style="font-size:12px;color:#60a5fa;">'+d.myShots+'S</div>'
+'<div style="font-size:12px;color:#fbbf24;">'+d.oppShots+'S</div>'
+'<div style="font-size:11px;color:rgba(255,255,255,0.9);">'+myAcc+'% acc</div>'
+'<div style="font-size:11px;color:rgba(255,255,255,0.9);">'+oppAcc+'% acc</div>'
+'</div>';
el.innerHTML = '<div style="font-size:10px;letter-spacing:1px;color:rgba(255,255,255,0.7);font-weight:600;margin-bottom:6px;">SHOT ANALYSIS</div>' + grid;
}
function pluginShotAnalysisPostMatch() {
if (!isInstalled('shotAnalysis')) return;
var el = document.getElementById('km-shot-analysis-hud');
if (el) { setTimeout(function(){if(el.parentNode)el.remove();}, 5000); }
var threshold = getPluginSetting('shotAnalysis','threshold') || 40;
var d = shotAnalysisData;
var myAcc = d.myShots > 0 ? Math.round((d.myGoals / d.myShots) * 100) : 0;
if (myAcc < threshold && d.myShots >= 2) {
showToast('📡 Accuracy '+myAcc+'% — try taking more shots!', 'warn');
} else if (myAcc >= threshold) {
showToast('📡 Accuracy '+myAcc+'% — solid shooting!', 'ok');
}
shotAnalysisData = { myShots:0, myGoals:0, oppShots:0, oppGoals:0 };
}
var autoCoachData = { shots:0, goals:0, conceded:0, startedAt:0, lastTipAt:0 };
function pluginAutoCoachOnMatchStart() {
if (!isInstalled('autoCoach')) return;
autoCoachData = { shots:0, goals:0, conceded:0, startedAt:Date.now(), lastTipAt:0 };
showToast('🧠 Auto Coach is ON. Focus: shot selection + positional awareness.', 'ok');
}
function pluginAutoCoachOnShot(isMe) {
if (!isInstalled('autoCoach')) return;
if (isMe) autoCoachData.shots++;
var now = Date.now();
if (now - autoCoachData.lastTipAt > 15000 && autoCoachData.shots > 2 && autoCoachData.goals === 0) {
showToast('🧠 Hint: reset and take a cleaner shot. Less rush, more precision.', 'warn');
autoCoachData.lastTipAt = now;
}
}
function pluginAutoCoachOnGoal(isMe) {
if (!isInstalled('autoCoach')) return;
if (isMe) {
autoCoachData.goals++;
if (getPluginSetting('autoCoach','goalHint') !== 'off') {
showToast('🧠 Good finish! For consistency, aim low and use backboard angles.', 'ok');
}
} else {
autoCoachData.conceded++;
if (getPluginSetting('autoCoach','mistakeAlert') !== 'off') {
showToast('🧠 Conceded goal: rotate back quickly and challenge cleanly.', 'warn');
}
}
autoCoachData.lastTipAt = Date.now();
}
function pluginAutoCoachPostMatch() {
if (!isInstalled('autoCoach')) return;
var accuracy = autoCoachData.shots > 0 ? Math.round((autoCoachData.goals / autoCoachData.shots) * 100) : 0;
showToast('🧠 Match summary: ' + autoCoachData.goals + ' goals, ' + autoCoachData.conceded + ' conceded, ' + accuracy + '% shot accuracy.', 'info');
}
// ─── TROLL PLUGIN ──────────────────────────────────────────────────────────
// SAFETY CONTRACT: pluginTrollMount() checks window._kmTrollActive (session-only RAM flag).
// This flag is NEVER set at boot, NEVER read from localStorage, NEVER persisted.
// The user must flip the "Enable" toggle in the current session to set it.
// On reload: window._kmTrollActive = undefined → troll cannot activate.
var _trollRaf = null, _trollOverlay = null, _trollScareTimeout = null;
function _trollFreq(s) { return s==='rare'?8000 : s==='sometimes'?3500 : s==='often'?1500 : 0; }
function pluginTrollMount() {
if (!isInstalled('trollPlugin') || !window._kmTrollActive) { pluginTrollUnmount(); return; }
if (_trollRaf) return;
// Inject shared overlay
if (!_trollOverlay) {
_trollOverlay = document.createElement('div');
_trollOverlay.id = 'km-troll-overlay';
_trollOverlay.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999980;display:none;';
document.body.appendChild(_trollOverlay);
}
// Inject a style for rainbow animation
if (!document.getElementById('km-troll-style')) {
var st = document.createElement('style'); st.id = 'km-troll-style';
st.textContent = [
'@keyframes km-troll-rainbow{0%{filter:hue-rotate(0deg)}100%{filter:hue-rotate(360deg)}}',
'@keyframes km-troll-shake{0%,100%{transform:translate(0,0)}25%{transform:translate(-6px,4px)}75%{transform:translate(6px,-4px)}}',
'#km-troll-scare{animation:km-troll-shake 0.08s linear infinite}',
].join('');
document.head.appendChild(st);
}
function flash(bg, dur) {
if (!_trollOverlay) return;
_trollOverlay.style.background = bg;
_trollOverlay.style.display = 'block';
setTimeout(function(){ if(_trollOverlay) _trollOverlay.style.display='none'; }, dur);
}
function doRainbow() {
if (!window._kmTrollActive) return;
var freq = _trollFreq(getPluginSetting('trollPlugin','rainbow')||'sometimes');
if (!freq) return;
// Multi-flash hue rotate cycle
var count = 0, hue = 0;
var flashLoop = setInterval(function(){
if (!_trollOverlay || !window._kmTrollActive) { clearInterval(flashLoop); return; }
hue += 51;
flash('hsla('+hue+',100%,55%,0.38)', 90);
if (++count >= 7) clearInterval(flashLoop);
}, 100);
setTimeout(doRainbow, freq + Math.random()*freq);
}
function doDark() {
if (!window._kmTrollActive) return;
var freq = _trollFreq(getPluginSetting('trollPlugin','darkness')||'sometimes');
if (!freq) return;
flash('rgba(0,0,0,0.9)', 500 + Math.random()*700);
setTimeout(doDark, freq + Math.random()*freq);
}
function doScare() {
if (!window._kmTrollActive) return;
var freq = _trollFreq(getPluginSetting('trollPlugin','jumpscare')||'rare');
if (!freq) return;
var faces = ['😱','👻','🎃','💀','🤡','👹','🙀','🫨'];
var el = document.createElement('div');
el.id = 'km-troll-scare';
el.style.cssText = 'position:fixed;inset:0;z-index:9999990;display:flex;align-items:center;justify-content:center;background:#000;pointer-events:none;';
el.innerHTML = '<span style="font-size:160px;line-height:1;filter:drop-shadow(0 0 60px red);">'+faces[Math.floor(Math.random()*faces.length)]+'</span>';
document.body.appendChild(el);
// Audio sting
try {
var ac = new (window.AudioContext||window.webkitAudioContext)();
[880,660,440].forEach(function(f,i){
var o=ac.createOscillator(), g=ac.createGain();
o.frequency.value=f; o.connect(g); g.connect(ac.destination);
g.gain.setValueAtTime(0.25, ac.currentTime+i*0.04);
g.gain.exponentialRampToValueAtTime(0.001, ac.currentTime+0.3+i*0.04);
o.start(ac.currentTime+i*0.04); o.stop(ac.currentTime+0.35+i*0.04);
});
} catch(e){}
setTimeout(function(){ if(el.parentNode) el.remove(); }, 350+Math.random()*200);
setTimeout(doScare, freq*3 + Math.random()*freq*2); // scares less frequent
}
// Kick off each effect independently with random initial delays so they don't sync
var darknessFreq = _trollFreq(getPluginSetting('trollPlugin','darkness')||'sometimes');
var rainbowFreq = _trollFreq(getPluginSetting('trollPlugin','rainbow')||'sometimes');
var scareFreq = _trollFreq(getPluginSetting('trollPlugin','jumpscare')||'rare');
if (darknessFreq) setTimeout(doDark, 1000 + Math.random()*darknessFreq);
if (rainbowFreq) setTimeout(doRainbow, 2000 + Math.random()*rainbowFreq);
if (scareFreq) setTimeout(doScare, 8000 + Math.random()*scareFreq*2);
// Heartbeat RAF just to keep the RAF var non-null so double-mount guard works
(function beat(){ _trollRaf = requestAnimationFrame(beat); })();
kmLog('🎭 Troll Mode active — chaos initiated');
}
function pluginTrollUnmount() {
if (_trollRaf) { cancelAnimationFrame(_trollRaf); _trollRaf = null; }
if (_trollOverlay) { _trollOverlay.remove(); _trollOverlay = null; }
var el = document.getElementById('km-troll-scare'); if (el) el.remove();
var st = document.getElementById('km-troll-style'); if (st) st.remove();
}
// ─── PERFORMANCE METRICS ──────────────────────────────────────────────────
var perfEl = null, perfFrames = 0, perfLast = performance.now(), perfFPS = 0;
var perfFT = 0, perfPingHistory = [], perfRAF = null;
function pluginPerfMetricsMount() {
if (!isInstalled('perfMetrics')) { pluginPerfMetricsUnmount(); return; }
if (perfEl) return;
var pos = getPluginSetting('perfMetrics','pos') || 'top-right';
var posCSS = pos === 'top-left' ? 'top:14px;left:14px'
: pos === 'top-right' ? 'top:14px;right:14px'
: pos === 'bottom-left' ? 'bottom:90px;left:14px'
: 'bottom:90px;right:14px';
perfEl = document.createElement('div');
perfEl.id = 'km-perf-metrics';
perfEl.style.cssText = 'position:fixed;'+posCSS+';z-index:99997;'
+'background:rgba(4,6,12,0.92);border:1px solid rgba(255,255,255,0.1);border-left:2px solid #4ade80;'
+'padding:8px 12px;font-family:"DM Mono",monospace;font-size:10px;pointer-events:none;'
+'backdrop-filter:blur(14px);min-width:140px;';
document.body.appendChild(perfEl);
var lastFrame = performance.now();
function perfTick(now) {
perfFrames++;
perfFT = now - lastFrame;
lastFrame = now;
if (now - perfLast >= 1000) {
perfFPS = _hudFps || Math.round(perfFrames * 1000 / (now - perfLast));
perfFrames = 0;
perfLast = now;
renderPerfMetrics();
}
perfRAF = requestAnimationFrame(perfTick);
}
perfRAF = requestAnimationFrame(perfTick);
}
function pluginPerfMetricsUnmount() {
if (perfEl) { perfEl.remove(); perfEl = null; }
if (perfRAF) { cancelAnimationFrame(perfRAF); perfRAF = null; }
perfFrames = 0; perfFPS = 0;
netBridgeStop();
// Note: ping stays running (it feeds the HUD, always-on)
}
function renderPerfMetrics() {
if (!perfEl) return;
var showFPS = (getPluginSetting('perfMetrics','fps') || 'on') === 'on';
var showFT = (getPluginSetting('perfMetrics','frame') || 'on') === 'on';
var showMem = (getPluginSetting('perfMetrics','memory') || 'off') === 'on';
var showPing = (getPluginSetting('perfMetrics','ping') || 'on') === 'on';
var showJitter = (getPluginSetting('perfMetrics','jitter') || 'on') === 'on';
var netBridge = (getPluginSetting('perfMetrics','netbridge') || 'off') === 'on';
// Start/stop Network Bridge when setting changes
if (netBridge) netBridgeStart(); else netBridgeStop();
var fpsCol = perfFPS >= 55 ? '#4ade80' : perfFPS >= 30 ? '#fbbf24' : '#f87171';
var ftMs = perfFT.toFixed(1);
var stats = getPingStats();
var lines = [];
if (showFPS) lines.push(
'<div style="display:flex;justify-content:space-between;gap:16px;margin:1px 0;">'
+'<span style="color:rgba(255,255,255,0.3);">FPS</span>'
+'<span style="color:'+fpsCol+';font-size:14px;font-weight:700;line-height:1;">'+perfFPS+'</span></div>'
);
if (showFT) lines.push(
'<div style="display:flex;justify-content:space-between;gap:16px;margin:1px 0;">'
+'<span style="color:rgba(255,255,255,0.3);">FRAME</span>'
+'<span style="color:rgba(255,255,255,0.7);">'+ftMs+'ms</span></div>'
);
if (showMem && performance.memory) {
var used = Math.round(performance.memory.usedJSHeapSize/1048576);
var total = Math.round(performance.memory.totalJSHeapSize/1048576);
var memCol = used/total > 0.8 ? '#f87171' : 'rgba(255,255,255,0.6)';
lines.push(
'<div style="display:flex;justify-content:space-between;gap:16px;margin:1px 0;">'
+'<span style="color:rgba(255,255,255,0.3);">MEM</span>'
+'<span style="color:'+memCol+';">'+used+'MB</span></div>'
);
}
// ── Ping section: real RTT sparkline ─────────────────────────────────────
if (showPing && _pingHistory.length) {
var pingCol = stats.last < 60 ? '#4ade80' : stats.last < 120 ? '#fbbf24' : '#f87171';
var maxP = Math.max.apply(null, _pingHistory.concat([1]));
var pingBars = _pingHistory.slice(-40).map(function(p) {
var h = Math.max(2, Math.round((p / maxP) * 18));
var c = p < 60 ? '#4ade80' : p < 120 ? '#fbbf24' : '#f87171';
return '<div style="width:2px;height:'+h+'px;background:'+c+'88;align-self:flex-end;flex-shrink:0;"></div>';
}).join('');
// Divider
lines.push('<div style="height:1px;background:rgba(255,255,255,0.06);margin:4px 0 3px;"></div>');
// Label + current value
lines.push(
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">'
+'<span style="font-size:8px;letter-spacing:1.5px;color:rgba(255,255,255,0.25);">PING</span>'
+'<span style="color:'+pingCol+';font-size:13px;font-weight:700;">'+stats.last+'ms</span>'
+'</div>'
);
// Sparkline bars
lines.push(
'<div style="display:flex;gap:1px;align-items:flex-end;height:18px;margin-bottom:3px;">'+pingBars+'</div>'
);
// Min / Avg / Max row
lines.push(
'<div style="display:flex;justify-content:space-between;font-size:9px;color:rgba(255,255,255,0.28);">'
+'<span>↓'+stats.min+'</span>'
+'<span style="color:rgba(255,255,255,0.45);">avg '+stats.avg+'</span>'
+'<span>↑'+stats.max+'</span>'
+'</div>'
);
}
// ── Jitter section ────────────────────────────────────────────────────────
if (showJitter && _jitterHistory.length) {
var jCol = stats.jitter < 8 ? '#4ade80' : stats.jitter < 25 ? '#fbbf24' : '#f87171';
var stableCol = stats.stable >= 80 ? '#4ade80' : stats.stable >= 50 ? '#fbbf24' : '#f87171';
var maxJ = Math.max.apply(null, _jitterHistory.concat([1]));
var jBars = _jitterHistory.map(function(j) {
var h = Math.max(1, Math.round((j / maxJ) * 16));
var c = j < 8 ? '#4ade80' : j < 25 ? '#fbbf24' : '#f87171';
return '<div style="width:2px;height:'+h+'px;background:'+c+'99;align-self:flex-end;flex-shrink:0;"></div>';
}).join('');
lines.push('<div style="height:1px;background:rgba(255,255,255,0.06);margin:4px 0 3px;"></div>');
lines.push(
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">'
+'<span style="font-size:8px;letter-spacing:1.5px;color:rgba(255,255,255,0.25);">JITTER</span>'
+'<span style="color:'+jCol+';font-size:12px;font-weight:700;">±'+stats.jitter+'ms</span>'
+'</div>'
);
lines.push(
'<div style="display:flex;gap:1px;align-items:flex-end;height:16px;margin-bottom:3px;">'+jBars+'</div>'
);
// Stability score bar
lines.push(
'<div style="display:flex;align-items:center;gap:6px;">'
+'<span style="font-size:8px;color:rgba(255,255,255,0.22);">STABILITY</span>'
+'<div style="flex:1;height:3px;background:rgba(255,255,255,0.08);overflow:hidden;">'
+'<div style="height:100%;width:'+stats.stable+'%;background:'+stableCol+';transition:width 0.4s ease;"></div>'
+'</div>'
+'<span style="font-size:9px;color:'+stableCol+';font-weight:700;">'+stats.stable+'%</span>'
+'</div>'
);
}
// ── Network Bridge status badge ───────────────────────────────────────────
if (netBridge) {
lines.push('<div style="height:1px;background:rgba(255,255,255,0.06);margin:4px 0 3px;"></div>');
lines.push(
'<div style="display:flex;align-items:center;gap:5px;">'
+'<div style="width:5px;height:5px;border-radius:50%;background:#4ade80;animation:km-rec-blink 2s ease-in-out infinite;flex-shrink:0;"></div>'
+'<span style="font-size:8px;letter-spacing:1px;color:#4ade80;">NET BRIDGE ACTIVE</span>'
+'</div>'
);
}
perfEl.innerHTML = lines.join('');
}
function pluginPerfMetricsUpdate() {
if (!isInstalled('perfMetrics') || !perfEl) return;
renderPerfMetrics();
}
// ─── AUTO QUEUE ────────────────────────────────────────────────────────────
var autoQueuePending = false;
var autoQueueTimer = null;
var inQueue = false;
var queueStartTime = null;
function pluginAutoQueueOnMatchEnd() {
if (!isInstalled('autoQueue')) return;
var delay = parseInt(getPluginSetting('autoQueue','delay')) || 3;
if (autoQueueTimer) clearTimeout(autoQueueTimer);
autoQueuePending = true;
showToast('🔁 Auto-queue in '+delay+'s...','info');
kmLog('AutoQueue: scheduling re-queue in '+delay+'s');
autoQueueTimer = setTimeout(function() {
autoQueuePending = false;
pluginAutoQueueFire();
}, delay * 1000);
}
function pluginAutoQueueFire() {
if (!isInstalled('autoQueue')) return;
kmLog('AutoQueue: attempting to click play button');
// Try multiple known button patterns in the game UI
var selectors = [
'[class*="play"]', '[class*="queue"]', '[class*="match"]',
'button', '.play-btn', '#play-btn', '[class*="Play"]',
'[class*="Queue"]', '[class*="casual"]', '[class*="competitive"]',
];
var found = false;
for (var i = 0; i < selectors.length; i++) {
var els = document.querySelectorAll(selectors[i]);
for (var j = 0; j < els.length; j++) {
var el = els[j];
var txt = (el.textContent || el.value || el.getAttribute('aria-label') || '').toLowerCase();
if (txt.includes('play') || txt.includes('queue') || txt.includes('match') || txt.includes('find')) {
el.click();
kmLog('AutoQueue: clicked "'+el.textContent.trim().slice(0,30)+'"');
showToast('🔁 Re-queued!','ok');
found = true;
break;
}
}
if (found) break;
}
if (!found) {
showToast('🔁 AutoQueue: no play button found','warn');
kmLog('AutoQueue: no matching button — game may not be on menu screen yet');
}
}
function pluginAutoQueueOnQueueStart() {
if (!isInstalled('autoQueue')) return;
inQueue = true;
queueStartTime = Date.now();
kmLog('AutoQueue: queue started');
}
function pluginAutoQueueOnMatchFound() {
if (!isInstalled('autoQueue')) return;
inQueue = false;
if (queueStartTime) {
var elapsed = Math.round((Date.now() - queueStartTime) / 1000);
kmLog('AutoQueue: match found after '+elapsed+'s in queue');
}
}
// ─── SCREENSHOT & CLIPS ────────────────────────────────────────────────────
var _kmScrHotkey = null;
function pluginScreenshotMount() {
if (!isInstalled('screenshotRec') || !isInstalled('streamerMode')) { pluginScreenshotUnmount(); return; }
if (_kmScrHotkey) return;
_kmScrHotkey = function(e) {
if (!isInstalled('screenshotRec')) return;
var hotkey = (getPluginSetting('screenshotRec','hotkey') || 'F9').toLowerCase();
if (e.key.toLowerCase() === hotkey) {
e.preventDefault();
pluginScreenshotTake('hotkey');
}
};
document.addEventListener('keydown', _kmScrHotkey, true);
kmLog('Screenshot plugin mounted, hotkey: '+(getPluginSetting('screenshotRec','hotkey')||'F9'));
}
function pluginScreenshotUnmount() {
if (_kmScrHotkey) { document.removeEventListener('keydown', _kmScrHotkey, true); _kmScrHotkey = null; }
}
function pluginScreenshotTake(source) {
var canvas = document.querySelector('canvas');
if (!canvas) { showToast('No canvas found','warn'); return; }
try {
var dataURL = canvas.toDataURL('image/png');
var ts = new Date().toISOString().replace(/[:.]/g,'-').slice(0,19);
var a = document.createElement('a');
a.download = 'rocketgoal-'+source+'-'+ts+'.png';
a.href = dataURL;
a.click();
showToast('📸 Screenshot saved!','ok');
kmLog('Screenshot saved: '+a.download);
} catch(e) {
showToast('Screenshot failed: '+e.message,'err');
}
}
function pluginScreenshotOnGoal() {
if (!isInstalled('screenshotRec') || !isInstalled('streamerMode')) return;
var auto = getPluginSetting('screenshotRec','autogoal') || 'off';
if (auto === 'on') {
setTimeout(function() { pluginScreenshotTake('goal'); }, 300);
}
}
// ─── STREAMER MODE ────────────────────────────────────────────────────────
function streamerName(name) {
if (!isInstalled('streamerMode')) return name;
if ((getPluginSetting('streamerMode','hidenames')||'on') === 'on') return 'Player';
return name;
}
function streamerMMR(val) {
if (!isInstalled('streamerMode')) return val;
if ((getPluginSetting('streamerMode','blurMMR')||'on') === 'on') return '???';
return val;
}
function applyDevPreset(preset) {
if (preset === 'streamer') {
if (!isInstalled('streamerMode')) installedPlugins.push('streamerMode');
if (!isInstalled('screenshotRec')) installedPlugins.push('screenshotRec');
if (!isInstalled('clipRecorder')) installedPlugins.push('clipRecorder');
setPluginSetting('streamerMode','hidenames','on');
setPluginSetting('streamerMode','blurMMR','on');
setPluginSetting('clipRecorder','quality','16k');
setPluginSetting('clipRecorder','autogoal','on');
} else if (preset === 'practice') {
setPluginSetting('shotAnalysis','threshold',40);
setPluginSetting('clipRecorder','autogoal','off');
setPluginSetting('streamerMode','hidenames','off');
setPluginSetting('streamerMode','blurMMR','off');
} else if (preset === 'performance') {
if (!isInstalled('optimizer')) installedPlugins.push('optimizer');
setPluginSetting('optimizer','stripshadows','on');
setPluginSetting('optimizer','lowqual','on');
setPluginSetting('optimizer','lessgarbage','on');
}
lsSet('plugins5', installedPlugins);
pluginStreamerMount();
pluginClipRecorderMount();
pluginOptimizerMount();
}
var _devSceneTimer = null;
var _devToolsHotkeyListener = null;
function pluginDevToolsMount() {
if (!isInstalled('devTools')) { pluginDevToolsUnmount(); return; }
if (document.getElementById('km-devtools-panel')) return;
var el = document.createElement('div');
el.id = 'km-devtools-panel';
el.style.cssText = 'position:fixed;bottom:12px;right:12px;z-index:99999;background:rgba(10,12,18,0.94);border:1px solid rgba(255,255,255,0.18);border-radius:10px;padding:10px;min-width:230px;font-family:"DM Mono",monospace;color:#fff;pointer-events:auto;';
el.innerHTML = '' +
'<div style="font-size:10px;font-weight:700;letter-spacing:1px;margin-bottom:5px;color:#60a5fa;">DEV TOOLS</div>' +
'<div style="display:flex;gap:6px;margin-bottom:6px;align-items:center;">' +
'<select id="km-dev-tools-preset" style="flex:1;padding:4px;background:#111;border:1px solid rgba(255,255,255,0.15);color:#fff;font-size:11px;">' +
'<option value="default">Default</option>' +
'<option value="streamer">Streamer</option>' +
'<option value="practice">Practice</option>' +
'<option value="performance">Performance</option>' +
'</select>' +
'<button id="km-dev-tools-apply" style="padding:4px 8px;font-size:11px;border:1px solid rgba(255,255,255,0.15);background:#1f2937;color:#fff;">Apply</button>' +
'</div>' +
'<div style="margin-bottom:6px;display:flex;gap:6px;">' +
'<button id="km-dev-tools-stream" style="padding:4px 6px;font-size:10px;border:1px solid rgba(255,255,255,0.15);background:#111;color:#fff;">Toggle Stream</button>' +
'<button id="km-dev-tools-export" style="padding:4px 6px;font-size:10px;border:1px solid rgba(255,255,255,0.15);background:#111;color:#fff;">Export</button>' +
'</div>' +
'<div style="margin-bottom:5px;display:flex;gap:6px;">' +
'<button id="km-dev-tools-import" style="padding:4px 6px;font-size:10px;border:1px solid rgba(255,255,255,0.15);background:#111;color:#fff;">Import</button>' +
'<button id="km-dev-tools-timer" style="padding:4px 6px;font-size:10px;border:1px solid rgba(255,255,255,0.15);background:#111;color:#fff;">Start timer</button>' +
'</div>' +
'<div id="km-dev-tools-timer-label" style="font-size:10px;color:rgba(255,255,255,0.6);">Scene timer: 00:00</div>';
document.body.appendChild(el);
document.getElementById('km-dev-tools-apply').addEventListener('click', function() {
var preset = document.getElementById('km-dev-tools-preset').value;
setPluginSetting('devTools', 'preset', preset);
applyDevPreset(preset);
showToast('Dev preset applied: ' + preset, 'ok');
});
document.getElementById('km-dev-tools-stream').addEventListener('click', function() {
if (isInstalled('streamerMode')) {
installedPlugins = installedPlugins.filter(function(x){return x!=='streamerMode';});
showToast('Streamer Tools disabled', 'warn');
} else {
installedPlugins.push('streamerMode');
setPluginSetting('streamerMode','hidenames','on');
setPluginSetting('streamerMode','blurMMR','on');
showToast('Streamer Tools enabled', 'ok');
}
lsSet('plugins5', installedPlugins);
pluginStreamerMount();
});
document.getElementById('km-dev-tools-export').addEventListener('click', function() {
var payload = JSON.stringify({installed:installedPlugins,settings:pluginSettings,toggles:toggles}, null, 2);
navigator.clipboard.writeText(payload).then(function(){ showToast('Config copied to clipboard', 'ok'); }, function(){ showToast('Copy failed', 'err'); });
});
document.getElementById('km-dev-tools-import').addEventListener('click', function() {
var data = prompt('Paste KeyMod JSON config:');
if (!data) return;
try {
var obj = JSON.parse(data);
if (obj.installed) installedPlugins = obj.installed;
if (obj.settings) pluginSettings = obj.settings;
if (obj.toggles) toggles = obj.toggles;
lsSet('plugins5', installedPlugins);
lsSet('pluginSettings5', pluginSettings);
lsSet('toggles5', toggles);
showToast('Config imported. Reloading...', 'ok');
setTimeout(function(){ window.location.reload(); }, 500);
} catch (e) {
showToast('Invalid config JSON', 'err');
}
});
document.getElementById('km-dev-tools-timer').addEventListener('click', function() {
if (_devSceneTimer) {
clearInterval(_devSceneTimer);
_devSceneTimer = null;
this.textContent = 'Start timer';
document.getElementById('km-dev-tools-timer-label').textContent = 'Scene timer: 00:00';
} else {
var seconds = 0;
_devSceneTimer = setInterval(function() {
seconds++;
var mm = String(Math.floor(seconds/60)).padStart(2,'0');
var ss = String(seconds%60).padStart(2,'0');
document.getElementById('km-dev-tools-timer-label').textContent = 'Scene timer: '+mm+':'+ss;
}, 1000);
document.getElementById('km-dev-tools-timer').textContent = 'Stop timer';
}
});
var hotkey = (getPluginSetting('devTools','streamerHotkey') || 'F6').toLowerCase();
window.addEventListener('keydown', _devToolsHotkeyListener = function(e) {
if (!isInstalled('devTools')) return;
if (e.key.toLowerCase() === hotkey) {
e.preventDefault();
if (isInstalled('streamerMode')) {
installedPlugins = installedPlugins.filter(function(x){return x!=='streamerMode';});
showToast('Streamer Tools disabled', 'warn');
} else {
installedPlugins.push('streamerMode');
showToast('Streamer Tools enabled', 'ok');
}
lsSet('plugins5', installedPlugins);
pluginStreamerMount();
}
}, true);
}
function pluginDevToolsUnmount() {
var existing = document.getElementById('km-devtools-panel');
if (existing) existing.remove();
if (_devSceneTimer) { clearInterval(_devSceneTimer); _devSceneTimer = null; }
if (_devToolsHotkeyListener) { window.removeEventListener('keydown', _devToolsHotkeyListener, true); _devToolsHotkeyListener = null; }
}
function pluginStreamerMount() {
var enabled = isInstalled('streamerMode');
if (!enabled) {
pluginScreenshotUnmount();
pluginClipRecorderUnmount();
} else {
if (isInstalled('screenshotRec')) pluginScreenshotMount(); else pluginScreenshotUnmount();
if (isInstalled('clipRecorder')) pluginClipRecorderMount(); else pluginClipRecorderUnmount();
}
pluginDevToolsMount();
}
// ─── PLAYSTYLE CLASSIFIER ─────────────────────────────────────────────────
function pluginPlaystyleOnMatchStart() {
if (!isInstalled('playstyleAI') || !matchPlayers.opponent) return;
var name = matchPlayers.opponent;
var history = opponentLog.filter(function(e){ return e.name === name; });
if (history.length < 2) return;
// Count goals and shots across history entries
var totalGoals = 0, totalShots = 0, earlyGoals = 0, totalMatches = history.length;
history.forEach(function(e) {
if (e.score) {
var parts = e.score.split('-');
totalGoals += parseInt(parts[1]||0); // opponent goals
}
if (e.oppShots) totalShots += e.oppShots;
if (e.oppGoals) totalGoals += e.oppGoals;
});
var accuracy = totalShots > 0 ? Math.round((totalGoals/totalShots)*100) : 0;
var goalsPerMatch = totalMatches > 0 ? (totalGoals/totalMatches).toFixed(1) : 0;
var wins = history.filter(function(e){return e.result==='Win';}).length; // our wins vs them
var winRate = Math.round((wins/totalMatches)*100);
var label, detail;
if (accuracy >= 65) {
label = 'Sniper'; detail = accuracy+'% shot accuracy';
} else if (winRate <= 30) {
label = 'Beater'; detail = 'you win only '+winRate+'% vs them';
} else if (goalsPerMatch >= 2.5) {
label = 'Aggressive'; detail = goalsPerMatch+' goals/match avg';
} else if (winRate >= 70) {
label = 'Grinder'; detail = 'low skill, you win '+winRate+'%';
} else {
label = 'Balanced'; detail = totalMatches+' matches played';
}
var dispName = streamerName(name);
setTimeout(function() {
showToast('🧠 '+dispName+' — '+label+' ('+detail+')', 'warn');
}, 2000);
kmLog('Playstyle: '+name+' = '+label);
}
// ─── PEAK GHOST ───────────────────────────────────────────────────────────
var peakGhostEl = null;
var allTimeStats = { goalsPerMatch:0, winsPerMatch:0, loaded:false };
function pluginPeakGhostInit() {
if (!isInstalled('peakGhost')) return;
// Compute all-time averages from match history
if (matchHistory && matchHistory.length > 5) {
var totalGoals = matchHistory.reduce(function(a,m){ return a+(m.goals||0); }, 0);
var totalWins = matchHistory.filter(function(m){ return m.result==='Win'; }).length;
allTimeStats.goalsPerMatch = totalGoals / matchHistory.length;
allTimeStats.winsPerMatch = totalWins / matchHistory.length;
allTimeStats.loaded = true;
}
}
function pluginPeakGhostUpdate() {
if (!isInstalled('peakGhost') || !allTimeStats.loaded) { pluginPeakGhostUnmount(); return; }
if (!peakGhostEl) {
peakGhostEl = document.createElement('div');
peakGhostEl.id = 'km-peak-ghost';
peakGhostEl.style.cssText = 'position:fixed;bottom:130px;right:22px;z-index:99996;'
+'background:rgba(0,0,0,0.82);border:1px solid rgba(255,255,255,0.07);border-left:2px solid #a78bfa;'
+'padding:8px 12px;font-family:"DM Mono",monospace;font-size:10px;pointer-events:none;'
+'backdrop-filter:blur(10px);min-width:130px;';
document.body.appendChild(peakGhostEl);
}
var sessionGoalsPerMatch = session.matches > 0 ? session.goals / session.matches : 0;
var sessionWR = session.matches > 0 ? session.wins / session.matches : 0;
var gDiff = sessionGoalsPerMatch - allTimeStats.goalsPerMatch;
var wDiff = sessionWR - allTimeStats.winsPerMatch;
function arrow(d) { return d > 0.05 ? '📈' : d < -0.05 ? '📉' : '➡️'; }
function col(d) { return d > 0.05 ? '#4ade80' : d < -0.05 ? '#f87171' : 'rgba(255,255,255,0.5)'; }
peakGhostEl.innerHTML =
'<div style="font-size:8px;letter-spacing:2px;color:rgba(255,255,255,0.22);margin-bottom:5px">PEAK GHOST</div>'
+'<div style="display:flex;justify-content:space-between;gap:14px;margin:2px 0">'
+'<span style="color:rgba(255,255,255,0.3)">Goals/M</span>'
+'<span style="color:'+col(gDiff)+'">'+arrow(gDiff)+' '+sessionGoalsPerMatch.toFixed(1)+'</span>'
+'</div>'
+'<div style="display:flex;justify-content:space-between;gap:14px;margin:2px 0">'
+'<span style="color:rgba(255,255,255,0.3)">Win Rate</span>'
+'<span style="color:'+col(wDiff)+'">'+arrow(wDiff)+' '+Math.round(sessionWR*100)+'%</span>'
+'</div>';
}
function pluginPeakGhostUnmount() {
if (peakGhostEl) { peakGhostEl.remove(); peakGhostEl = null; }
}
// ─── TACTICAL AUDIO ───────────────────────────────────────────────────────
// ─── ANIMATED BACKGROUND ─────────────────────────────────────────────────
var animBGEl = null, animBGRaf = null;
function pluginAnimBGMount() {
pluginAnimBGUnmount();
// Animated background has been removed.
}
function pluginAnimBGUnmount() {
if (animBGRaf) { cancelAnimationFrame(animBGRaf); animBGRaf = null; }
if (animBGEl) { animBGEl.remove(); animBGEl = null; }
}
function pluginAnimBGDraw() {
return;
}
// ─── POST-MATCH STATS OVERLAY ─────────────────────────────────────────────
function showPostMatchStats(result, oldMMR, newMMR, xpDiff) {
var old = document.getElementById('km-post-match'); if(old) old.remove();
var el = document.createElement('div');
el.id = 'km-post-match';
var isWin = result === 'Win';
var accent = isWin ? '#4ade80' : '#f87171';
var mmrDiff = (newMMR !== null && oldMMR !== null) ? (newMMR - oldMMR) : null;
// Session averages
var sessionAcc = session.shots > 0 ? Math.round((session.goals/session.shots)*100) : 0;
var matchAcc = shotAnalysisData && shotAnalysisData.myShots > 0
? Math.round((shotAnalysisData.myGoals/shotAnalysisData.myShots)*100) : 0;
var sessionGPM = session.matches > 0 ? (session.goals/session.matches).toFixed(1) : '0';
var matchGPM = '—'; // approx from score
var oppName = streamerName(matchPlayers.opponent || 'Opponent');
var oppMMRStr = opponentMMR ? streamerMMR(opponentMMR) : '?';
var oppAccStr = shotAnalysisData && shotAnalysisData.oppShots > 0
? Math.round((shotAnalysisData.oppGoals/shotAnalysisData.oppShots)*100)+'%' : '—';
function row(label, val, sessVal, highlight) {
return '<tr style="border-bottom:1px solid rgba(255,255,255,0.05);">'
+'<td style="padding:5px 10px 5px 0;color:rgba(255,255,255,0.35);font-size:10px;text-transform:uppercase;letter-spacing:0.5px;white-space:nowrap">'+label+'</td>'
+'<td style="padding:5px 12px;color:'+(highlight||'#fff')+';font-family:\'DM Mono\',monospace;font-weight:700;font-size:13px;">'+val+'</td>'
+(sessVal !== undefined ? '<td style="padding:5px 0;color:rgba(255,255,255,0.28);font-size:11px;">'+sessVal+'</td>' : '')
+'</tr>';
}
var table =
'<table style="width:100%;border-collapse:collapse;">'
+'<thead><tr>'
+'<th style="text-align:left;font-size:8px;letter-spacing:1.5px;color:rgba(255,255,255,0.2);padding-bottom:6px;font-weight:400">STAT</th>'
+'<th style="text-align:left;font-size:8px;letter-spacing:1.5px;color:rgba(255,255,255,0.2);padding-bottom:6px;font-weight:400">THIS MATCH</th>'
+'<th style="text-align:left;font-size:8px;letter-spacing:1.5px;color:rgba(255,255,255,0.2);padding-bottom:6px;font-weight:400">SESSION AVG</th>'
+'</tr></thead>'
+'<tbody>'
+ row('Score', matchScore.me+' – '+matchScore.opp, session.goals+'G / '+session.matches+'M')
+ (matchAcc > 0 ? row('Accuracy', matchAcc+'%', sessionAcc+'%', matchAcc > sessionAcc ? '#4ade80' : matchAcc < sessionAcc ? '#f87171' : '#fff') : '')
+ (mmrDiff !== null ? row('MMR', (mmrDiff >= 0 ? '▲ +' : '▼ ')+mmrDiff, 'Total: '+(mmrDiff >= 0 ? '+' : '')+mmrDiff, mmrDiff >= 0 ? '#4ade80' : '#f87171') : '')
+ (xpDiff > 0 ? row('XP', '+'+xpDiff, '') : '')
+'</tbody>'
+'</table>';
var oppSection = '';
if (matchPlayers.opponent) {
oppSection = '<div style="margin-top:10px;padding-top:10px;border-top:1px solid rgba(255,255,255,0.07);">'
+'<div style="font-size:8px;letter-spacing:1.5px;color:rgba(255,255,255,0.2);margin-bottom:6px">OPPONENT</div>'
+'<div style="display:flex;justify-content:space-between;align-items:center;">'
+'<div>'
+'<div style="font-size:13px;font-weight:700;color:rgba(255,255,255,0.8)">'+oppName+'</div>'
+'<div style="font-size:10px;color:rgba(255,255,255,0.3);margin-top:2px">'+oppMMRStr+' MMR · Acc: '+oppAccStr+'</div>'
+'</div>'
+(opponentMMR ? '<div style="font-size:10px;color:rgba(255,255,255,0.25)">'+getRank(opponentMMR).emoji+' '+getRank(opponentMMR).name+'</div>' : '')
+'</div>'
+'</div>';
}
el.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:999992;'
+'background:rgba(4,6,12,0.97);border:1px solid rgba(255,255,255,0.08);border-top:2px solid '+accent+';'
+'padding:18px 20px;font-family:"DM Mono",monospace;min-width:300px;max-width:380px;'
+'box-shadow:0 32px 80px rgba(0,0,0,0.9);pointer-events:none;';
el.innerHTML =
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">'
+'<div style="font-family:\'Bebas Neue\',sans-serif;font-size:24px;letter-spacing:3px;color:'+accent+'">'+result.toUpperCase()+'</div>'
+'<div style="font-size:10px;color:rgba(255,255,255,0.3);text-align:right;">'+
(currentMode||'match').replace('Competitive','')+'</div>'
+'</div>'
+ table
+ oppSection;
document.body.appendChild(el);
setTimeout(function(){ el.style.transition='opacity 0.5s'; el.style.opacity='0'; }, 7000);
setTimeout(function(){ if(el.parentNode) el.remove(); }, 7600);
}
// ─── CLIP RECORDER ────────────────────────────────────────────────────────
var clipRec = {
mediaRecorder: null,
chunks: [],
recording: false,
startTime: 0,
maxTimer: null,
hotkey: null,
preBuffer: [], // rolling ~5s of chunks before goal
preBufferTimer: null,
};
var CLIP_QUALITY = {
q16k: { videoBitsPerSecond: 16000000 },
high: { videoBitsPerSecond: 8000000 },
medium: { videoBitsPerSecond: 4000000 },
low: { videoBitsPerSecond: 1500000 },
};
function pluginClipRecorderMount() {
if (!isInstalled('clipRecorder') || !isInstalled('streamerMode')) { pluginClipRecorderUnmount(); return; }
if (clipRec.hotkey) return; // already mounted
var hk = (getPluginSetting('clipRecorder','hotkey') || 'F8').toLowerCase();
clipRec.hotkey = function(e) {
if (!isInstalled('clipRecorder')) return;
if (e.key.toLowerCase() !== hk) return;
e.preventDefault();
if (clipRec.recording) {
pluginClipRecorderStop('manual');
} else {
pluginClipRecorderStart();
}
};
document.addEventListener('keydown', clipRec.hotkey, true);
// Start pre-buffer capture for goal auto-clips
pluginClipRecorderPreBuffer();
kmLog('ClipRecorder mounted, hotkey: ' + hk.toUpperCase());
showToast('🎬 ClipRecorder ready — ' + hk.toUpperCase() + ' to record', 'ok');
}
function pluginClipRecorderUnmount() {
pluginClipRecorderStop('unmount');
if (clipRec.hotkey) {
document.removeEventListener('keydown', clipRec.hotkey, true);
clipRec.hotkey = null;
}
pluginClipRecorderStopPreBuffer();
}
function pluginClipRecorderGetStream() {
// Try canvas capture first (game canvas), fallback to display media
var canvas = document.querySelector('canvas');
if (canvas && canvas.captureStream) {
try {
var fps = 30;
return Promise.resolve(canvas.captureStream(fps));
} catch(e) {}
}
// Fallback: ask for screen share
if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
return navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 30 }, audio: false });
}
return Promise.reject(new Error('No capture source available'));
}
function pluginClipRecorderStart() {
if (clipRec.recording) return;
pluginClipRecorderGetStream().then(function(stream) {
var quality = getPluginSetting('clipRecorder','quality') || 'high';
var opts = CLIP_QUALITY[quality] || CLIP_QUALITY.high;
// Try supported MIME types in order of preference
var mimes = ['video/mp4;codecs=h264,aac','video/mp4','video/webm;codecs=vp9','video/webm;codecs=vp8','video/webm'];
var mime = mimes.find(function(m) { return MediaRecorder.isTypeSupported(m); }) || '';
if (mime) opts.mimeType = mime;
try {
clipRec.mediaRecorder = new MediaRecorder(stream, opts);
} catch(e) {
// Retry without explicit options
clipRec.mediaRecorder = new MediaRecorder(stream);
}
clipRec.chunks = [];
clipRec.startTime = Date.now();
clipRec.recording = true;
clipRec.mediaRecorder.ondataavailable = function(e) {
if (e.data && e.data.size > 0) clipRec.chunks.push(e.data);
};
clipRec.mediaRecorder.onstop = function() {
pluginClipRecorderSave(clipRec.chunks);
clipRec.recording = false;
clipRec.chunks = [];
stream.getTracks().forEach(function(t) { t.stop(); });
};
clipRec.mediaRecorder.onerror = function(e) {
kmLog('ClipRecorder error: ' + e.error);
showToast('🎬 Recording error', 'err');
clipRec.recording = false;
};
clipRec.mediaRecorder.start(200); // collect chunks every 200ms
// Max length timer
var maxLen = parseInt(getPluginSetting('clipRecorder','maxlen')) || 30;
if (clipRec.maxTimer) clearTimeout(clipRec.maxTimer);
clipRec.maxTimer = setTimeout(function() {
if (clipRec.recording) pluginClipRecorderStop('maxlen');
}, maxLen * 1000);
// Update HUD
pluginClipRecorderUpdateUI(true);
showToast('🎬 Recording started', 'ok');
kmLog('ClipRecorder: recording started (' + quality + ', max ' + maxLen + 's)');
}).catch(function(err) {
showToast('🎬 Could not start recording: ' + (err.message||'denied'), 'err');
kmLog('ClipRecorder: stream error — ' + err.message);
});
}
function pluginClipRecorderStop(reason) {
if (clipRec.maxTimer) { clearTimeout(clipRec.maxTimer); clipRec.maxTimer = null; }
if (!clipRec.recording || !clipRec.mediaRecorder) {
pluginClipRecorderUpdateUI(false);
return;
}
try {
clipRec.mediaRecorder.stop();
} catch(e) {
clipRec.recording = false;
}
pluginClipRecorderUpdateUI(false);
if (reason !== 'unmount') {
var elapsed = ((Date.now() - clipRec.startTime) / 1000).toFixed(1);
showToast('🎬 Clip saved (' + elapsed + 's)', 'ok');
kmLog('ClipRecorder: stopped (' + reason + ', ' + elapsed + 's)');
}
}
function pluginClipRecorderSave(chunks) {
if (!chunks || !chunks.length) return;
var mime = (clipRec.mediaRecorder && clipRec.mediaRecorder.mimeType) || 'video/webm';
var blob = new Blob(chunks, { type: mime });
// Determine extension — prefer mp4, fall back to webm
var isMP4 = mime.indexOf('mp4') !== -1;
var ext = isMP4 ? 'mp4' : 'webm';
var ts = new Date().toISOString().replace(/[:.]/g,'-').slice(0,19);
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url; a.download = 'rocketgoal-clip-' + ts + '.' + ext;
a.click();
setTimeout(function() { URL.revokeObjectURL(url); }, 10000);
kmLog('ClipRecorder: saved ' + (blob.size/1024/1024).toFixed(1) + 'MB as ' + a.download + ' (' + mime + ')');
// If WebM, show a helpful compatibility toast
if (!isMP4) {
setTimeout(function() {
showToast('📹 Saved as .webm — open with VLC or Chrome if Windows says unsupported', 'info');
}, 800);
}
}
// Pre-buffer: keep a rolling window of canvas frames
// On goal, grab the buffered chunks + record 3 more seconds = ~5-8s goal clip
function pluginClipRecorderPreBuffer() {
if (!isInstalled('clipRecorder')) return;
if ((getPluginSetting('clipRecorder','autogoal')||'on') === 'off') return;
pluginClipRecorderStopPreBuffer();
var canvas = document.querySelector('canvas');
if (!canvas || !canvas.captureStream) return;
try {
var stream = canvas.captureStream(30);
var quality = getPluginSetting('clipRecorder','quality') || 'high';
var opts = CLIP_QUALITY[quality] || CLIP_QUALITY.high;
var mimes = ['video/mp4;codecs=h264,aac','video/mp4','video/webm;codecs=vp9','video/webm;codecs=vp8','video/webm'];
var mime = mimes.find(function(m) { return MediaRecorder.isTypeSupported(m); }) || '';
if (mime) opts.mimeType = mime;
clipRec.preRec = new MediaRecorder(stream, opts);
clipRec.preChunks = [];
clipRec.preRec.ondataavailable = function(e) {
if (e.data && e.data.size > 0) {
clipRec.preChunks.push(e.data);
// Keep rolling ~5s buffer (at 200ms chunks = 25 chunks)
if (clipRec.preChunks.length > 25) clipRec.preChunks.shift();
}
};
clipRec.preRec.start(200);
} catch(e) {
kmLog('ClipRecorder pre-buffer failed: ' + e.message);
}
}
function pluginClipRecorderStopPreBuffer() {
if (clipRec.preRec) {
try { clipRec.preRec.stop(); } catch(e){}
clipRec.preRec = null;
clipRec.preChunks = [];
}
}
function pluginClipRecorderAutoGoal() {
if (!isInstalled('clipRecorder')) return;
if ((getPluginSetting('clipRecorder','autogoal')||'on') === 'off') return;
if (clipRec.recording) return; // already recording manually
// Grab pre-buffer chunks and record 3s more post-goal
var preChunks = (clipRec.preChunks || []).slice();
kmLog('ClipRecorder: auto-clip triggered, pre-buffer chunks: ' + preChunks.length);
var canvas = document.querySelector('canvas');
if (!canvas || !canvas.captureStream) {
showToast('🎬 Auto-clip: no canvas', 'warn');
return;
}
try {
var stream = canvas.captureStream(30);
var quality = getPluginSetting('clipRecorder','quality') || 'high';
var opts = Object.assign({}, CLIP_QUALITY[quality] || CLIP_QUALITY.high);
var mimes = ['video/mp4;codecs=h264,aac','video/mp4','video/webm;codecs=vp9','video/webm;codecs=vp8','video/webm'];
var mime = mimes.find(function(m) { return MediaRecorder.isTypeSupported(m); }) || '';
if (mime) opts.mimeType = mime;
var postRec = new MediaRecorder(stream, opts);
var postChunks = [];
postRec.ondataavailable = function(e) {
if (e.data && e.data.size > 0) postChunks.push(e.data);
};
postRec.onstop = function() {
var allChunks = preChunks.concat(postChunks);
if (allChunks.length) {
pluginClipRecorderSave(allChunks);
showToast('🎬 Goal clip saved!', 'ok');
}
stream.getTracks().forEach(function(t) { t.stop(); });
};
postRec.start(200);
setTimeout(function() {
if (postRec.state !== 'inactive') postRec.stop();
}, 3000);
} catch(e) {
kmLog('ClipRecorder auto-goal error: ' + e.message);
}
}
function pluginClipRecorderUpdateUI(recording) {
var hud = document.getElementById('km-hud');
var indicator = document.getElementById('km-rec-dot');
if (recording && hud && !indicator) {
var dot = document.createElement('span');
dot.id = 'km-rec-dot';
dot.style.cssText = 'display:inline-block;width:8px;height:8px;background:#f87171;border-radius:50%;'
+'animation:km-rec-blink 1s ease-in-out infinite;margin-left:2px;vertical-align:middle;';
hud.appendChild(dot);
} else if (!recording && indicator) {
indicator.remove();
}
}
// ─── OPTIMIZER ────────────────────────────────────────────────────────────
var optimizerActive = false;
var optimizerStyle = null;
// ─── OPTIMIZER RUNTIME STATE ─────────────────────────────────────────────
var _optVisLockDescs = null;
var _optSilencedLog = false;
var _optShadowStyle = null;
var _optExtraStyle = null; // body hw-acc, canvas tweaks
var _optBlurSaved = null; // saved window.onblur
var _optOrigConsole = null; // saved console fns before silence
var _optWebSockOrig = null; // saved WebSocket.prototype.send
var _optCanvasOrig = null; // saved { w, h, styleW, styleH } before upscaler
var _optFrustumStyle = null; // style element for frustum clip-path
function _optGet(id, def) {
var v = getPluginSetting('optimizer', id);
if (v === null || v === undefined) return def;
return v === 'on' || v === true;
}
function pluginOptimizerMount() {
if (!isInstalled('optimizer')) { pluginOptimizerUnmount(); return; }
if (optimizerActive) return;
optimizerActive = true;
var css = [], extraCss = [];
var o = _optGet; // shorthand: o('id', default)
// ── GROUP 1: Memory & Cleanup ──────────────────────────────────────────
// #1 Hide ad banners
if (o('hideads','on')) {
css.push('[class*="playgama"],[class*="banner"],[id*="banner"],[id*="playgama"],'
+'[class*="ad-"],[class*="-ad"],[id*="ad-"],[class*="advertisement"],'
+'[class*="sponsor"],[id*="sponsor"],[class*="promo"]'
+'{display:none!important;visibility:hidden!important;pointer-events:none!important;height:0!important;overflow:hidden!important;}');
setTimeout(function() {
var n=0;
document.querySelectorAll('[id*="playgama"],[class*="playgama"],[id*="banner-container"]').forEach(function(el){el.remove();n++;});
if(n) kmLog('Optimizer: removed '+n+' ad DOM nodes');
}, 500);
}
// #2 Block analytics XHR
if (o('blockga','on')) pluginOptimizerBlockGA();
// #3 Hide chat/social
if (o('hidechat','on')) {
css.push('[class*="chat"],[id*="chat"],[class*="social"],[id*="social"],'
+'[class*="leaderboard-global"],[class*="feed"],[class*="notification-bell"]{display:none!important;}');
}
// #4 Clear perf timing logs — clears accumulated timing entries from memory
if (o('clearmeasures',false)) {
try { window.performance.clearMeasures(); } catch(e) {}
}
// #5 Clear perf markers
if (o('clearmarks',false)) {
try { window.performance.clearMarks(); } catch(e) {}
}
// #6 Block tracking beacons (sendBeacon used by analytics)
if (o('blockbeacon','on')) {
if (!window._kmBeaconBlocked) {
window._kmBeaconBlocked = true;
navigator.sendBeacon = function() { return true; };
}
}
// ── GROUP 2: GPU & Rendering ───────────────────────────────────────────
// #7 GPU canvas hints — dedicated compositor layer, no CPU compositing
if (o('gpucanvas','on')) {
extraCss.push('canvas{will-change:transform!important;transform:translateZ(0)!important;backface-visibility:hidden!important;}');
}
// #8 Hardware-accelerate body element
if (o('bodyhwacc','on')) {
extraCss.push('body{will-change:transform;transform:translateZ(0);}');
}
// #9 Strip UI shadows & blur (expensive for compositor)
if (o('stripshadows',false)) {
if (!_optShadowStyle) {
_optShadowStyle = document.createElement('style');
_optShadowStyle.id = 'km-opt-noshadow';
_optShadowStyle.textContent = '*:not(canvas):not(#km-menu):not([id^="km-"]):not([class^="km-"])'
+'{box-shadow:none!important;text-shadow:none!important;filter:none!important;}';
document.head.appendChild(_optShadowStyle);
}
}
// #10 Remove canvas outline/border (eliminates GPU border-draw overhead)
if (o('canvasnoborder','on')) {
extraCss.push('canvas{box-shadow:none!important;outline:none!important;border:none!important;}');
}
// #11 Force pixelated rendering (stops GPU anti-aliasing per-pixel math)
if (o('lowqual',false)) {
extraCss.push('canvas{image-rendering:pixelated!important;image-rendering:crisp-edges!important;}');
}
// #11b Lock devicePixelRatio to 1.0 — prevents Unity rendering at 2x on HiDPI screens.
// On a Retina/4K display Unity renders at 2x by default, doubling GPU load.
// This forces DPR=1 which cuts GPU work in half on high-density displays.
if (o('hires',false)) {
if (!window._kmDPROrig) {
try {
window._kmDPROrig = window.devicePixelRatio;
Object.defineProperty(window, 'devicePixelRatio', { get: function(){ return 1; }, configurable: true });
kmLog('Optimizer: devicePixelRatio locked to 1 (was '+window._kmDPROrig+')');
} catch(e) {}
}
}
// #12 Upscaler — render at a higher canvas resolution and display at full width.
// This can produce sharper visuals while still fitting the viewport.
(function() {
var up = getPluginSetting('optimizer','upscaler') || 'off';
if (up === 'off') return;
var mult = parseFloat(up.replace('x','')) || 1;
if (mult <= 1) return;
setTimeout(function() {
var cv = document.querySelector('canvas');
if (!cv) { kmLog('Optimizer: upscaler — no canvas found yet'); return; }
if (!_optCanvasOrig) {
_optCanvasOrig = {
w: cv.width, h: cv.height,
styleW: cv.style.width, styleH: cv.style.height,
styleIR: cv.style.imageRendering
};
}
var newW = Math.round(window.innerWidth * mult);
var newH = Math.round(window.innerHeight * mult);
cv.width = newW;
cv.height = newH;
cv.style.width = '100vw';
cv.style.height = '100vh';
cv.style.imageRendering = 'auto';
kmLog('Optimizer: upscaler ' + up + ' applied (' + newW + 'x' + newH + ')');
showToast('⚡ Upscaler ' + up + ' active', 'ok');
}, 800);
})();
// #13 Frustum clip-path culling — reduces compositor paint area.
// CSS clip-path tells the browser's compositor to discard pixels outside
// the clip region at rasterization time, not just hide them. On a 1920px
// wide canvas the browser normally composites all 1920px every frame.
// Clipping to a slightly inset inset() reduces the paint rect.
// Note: this is a compositor-level hint, not a true frustum cull — it
// has no effect on what Unity renders internally, only on what the
// browser paints to screen. Useful on low-end GPUs with slow compositing.
if (o('frustum',false) && !_optFrustumStyle) {
_optFrustumStyle = document.createElement('style');
_optFrustumStyle.id = 'km-opt-frustum';
// clip-path on the GAME canvas causes black screen — skip canvas, apply to body wrapper only
// This still reduces compositor paint rect on elements surrounding the canvas
_optFrustumStyle.textContent = 'body > *:not(canvas):not(#km-menu):not([id^="km-"]){contain:layout paint;}';
document.head.appendChild(_optFrustumStyle);
kmLog('Optimizer: frustum containment active (safe mode)');
}
// ── GROUP 3: CPU & Threading ───────────────────────────────────────────
// #12 Page visibility lock — prevents browser from throttling WebGL when tabbed out
if (o('vislock','on') && !_optVisLockDescs) {
try {
_optVisLockDescs = {
visState: Object.getOwnPropertyDescriptor(document, 'visibilityState'),
hidden: Object.getOwnPropertyDescriptor(document, 'hidden'),
};
Object.defineProperty(document, 'visibilityState', { get: function(){ return 'visible'; }, configurable:true });
Object.defineProperty(document, 'hidden', { get: function(){ return false; }, configurable:true });
} catch(e) { _optVisLockDescs = null; }
}
// #13 Disable onblur throttle — keeps FPS when you click another window
if (o('noblur','on')) {
_optBlurSaved = window.onblur;
window.onblur = null;
window.onfocus = null;
}
// #14 Lock page scroll (prevents scroll from triggering layout reflows)
if (o('nooverflow','on')) {
css.push('html,body{overflow:hidden!important;scroll-behavior:auto!important;}');
}
// #15 Disable right-click menu (prevents context menu from pausing game loop)
if (o('nocontextmenu','on')) {
document._kmCtxMenu = document.oncontextmenu;
document.oncontextmenu = function(e){ e.preventDefault(); return false; };
}
// #16 Silence game console — Unity spam costs JS thread CPU per message
if (o('silencelog',false) && !_optSilencedLog) {
// Delay silencing so startup logs (login, init) still appear in KeyMod console
setTimeout(function() {
if (!_optSilencedLog) return; // was restored already
console.log = function(){};
console.warn = function(){};
console.error = function(){};
kmLog('Optimizer: game console silenced (startup logs preserved)');
}, 5000);
_optSilencedLog = true;
}
// ── GROUP 4: Network & Data ────────────────────────────────────────────
// #17 Block analytics fetch calls
if (o('blockfetch','on')) {
if (!window._kmFetchPatched) {
window._kmFetchPatched = true;
var _origFetch = window.fetch;
window.fetch = function() {
var url = typeof arguments[0] === 'string' ? arguments[0] : (arguments[0] && arguments[0].url) || '';
if (url.indexOf('analytics') !== -1 || url.indexOf('gameanalytics') !== -1
|| url.indexOf('playfab') !== -1 || url.indexOf('doubleclick') !== -1
|| url.indexOf('telemetry') !== -1) {
return Promise.resolve(new Response('', { status: 200 }));
}
return _origFetch.apply(this, arguments);
};
}
}
// #18 Limit WebSocket packet size — blocks large tracking payloads (>2KB)
// while allowing small game packets through (typically <200 bytes)
if (o('blockwebsock',false) && !_optWebSockOrig) {
_optWebSockOrig = WebSocket.prototype.send;
WebSocket.prototype.send = function(data) {
// Allow small game packets, block large telemetry payloads
var size = typeof data === 'string' ? data.length : (data && data.byteLength) || 0;
if (size > 2048) { return; } // drop oversized packets
return _optWebSockOrig.apply(this, arguments);
};
}
// #19 Stop background asset loading — fires window.stop() to halt any
// deferred image/CSS loads that aren't the game itself
if (o('stopload',false)) {
try { window.stop(); } catch(e) {}
}
// ── GROUP 5: Input & UI ────────────────────────────────────────────────
// #20 Force canvas focus on mount — ensures keyboard events go to game
if (o('canvasfocus','on')) {
setTimeout(function() {
var cv = document.querySelector('canvas');
if (cv) { cv.focus(); cv.setAttribute('tabindex','0'); }
}, 300);
}
// #22 Lock scroll to top — ensures canvas isn't offset by scroll
if (o('scrolltop','on')) {
window.scrollTo(0, 0);
window.addEventListener('scroll', function(){ window.scrollTo(0,0); }, { passive:true });
}
// #23 Body overflow hidden — prevents layout reflow from body scroll
// (handled in CSS above with nooverflow, this adds the inline style too)
if (o('overflowbody','on')) {
document.body.style.overflow = 'hidden';
}
// #24 Prefer high-performance GPU — hint to browser to pick discrete GPU
if (o('prefersperf',false)) {
// Set powerPreference on any new WebGL contexts; can't change existing ones
var _origGetCtx = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(type, opts) {
if (type === 'webgl' || type === 'webgl2') {
opts = Object.assign({ powerPreference: 'high-performance' }, opts || {});
}
return _origGetCtx.call(this, type, opts);
};
}
// #25 CSS containment on KeyMod UI — stops UI reflows affecting game layout
if (o('csscontain','on')) {
// layout style only — paint containment on UI overlays can cause black screen artifacts
extraCss.push('#km-menu{contain:layout style!important;}#km-hud{contain:layout style!important;}#km-leaderboard{contain:layout style!important;}');
}
// Apply CSS
if (css.length) {
optimizerStyle = document.createElement('style');
optimizerStyle.id = 'km-optimizer-style';
optimizerStyle.textContent = css.join('\n');
document.head.appendChild(optimizerStyle);
}
if (extraCss.length) {
_optExtraStyle = document.createElement('style');
_optExtraStyle.id = 'km-optimizer-extra';
_optExtraStyle.textContent = extraCss.join('\n');
document.head.appendChild(_optExtraStyle);
}
showToast('⚡ Optimizer active', 'ok');
kmLog('Optimizer mounted');
}
function pluginOptimizerUnmount() {
if (!optimizerActive) return;
optimizerActive = false;
if (optimizerStyle) { optimizerStyle.remove(); optimizerStyle = null; }
if (_optShadowStyle) { _optShadowStyle.remove(); _optShadowStyle = null; }
if (_optExtraStyle) { _optExtraStyle.remove(); _optExtraStyle = null; }
// Restore visibility lock
if (_optVisLockDescs) {
try {
if (_optVisLockDescs.visState) Object.defineProperty(document, 'visibilityState', _optVisLockDescs.visState);
if (_optVisLockDescs.hidden) Object.defineProperty(document, 'hidden', _optVisLockDescs.hidden);
} catch(e) {}
_optVisLockDescs = null;
}
// Restore blur handlers
if (_optBlurSaved !== null) { window.onblur = _optBlurSaved; _optBlurSaved = null; }
// Restore context menu
if (document._kmCtxMenu !== undefined) { document.oncontextmenu = document._kmCtxMenu; delete document._kmCtxMenu; }
// Restore console
if (_optSilencedLog) {
_optSilencedLog = false;
console.log = function(){ _log.apply(console, arguments); _silentSniff('log', arguments); };
console.warn = function(){ _warn.apply(console, arguments); _silentSniff('warn', arguments); };
console.error = function(){ _err.apply(console, arguments); _silentSniff('err', arguments); };
}
// Restore WebSocket
if (_optWebSockOrig) { WebSocket.prototype.send = _optWebSockOrig; _optWebSockOrig = null; }
// Restore DPR if locked
if (window._kmDPROrig !== undefined) {
try {
Object.defineProperty(window, 'devicePixelRatio', { get: function(){ return window._kmDPROrig; }, configurable: true });
} catch(e) {}
delete window._kmDPROrig;
}
// Restore canvas resolution if we scaled it
if (_optCanvasOrig) {
var cv = document.querySelector('canvas');
if (cv) {
cv.width = _optCanvasOrig.w;
cv.height = _optCanvasOrig.h;
cv.style.width = _optCanvasOrig.styleW;
cv.style.height = _optCanvasOrig.styleH;
cv.style.imageRendering = _optCanvasOrig.styleIR;
}
_optCanvasOrig = null;
}
// Remove frustum clip
if (_optFrustumStyle) { _optFrustumStyle.remove(); _optFrustumStyle = null; }
document.body.style.overflow = '';
kmLog('Optimizer unmounted');
}
function pluginOptimizerBlockGA() {
if (window._kmXHRPatched) return;
window._kmXHRPatched = true;
var _origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
if (typeof url === 'string' && (
url.indexOf('gameanalytics.com') !== -1 ||
url.indexOf('api.gameanalytics') !== -1 ||
url.indexOf('rubick.gameanalytics') !== -1
)) {
this._kmBlocked = true;
return;
}
return _origOpen.apply(this, arguments);
};
var _origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
if (this._kmBlocked) return;
return _origSend.apply(this, arguments);
};
kmLog('Optimizer: analytics XHR blocked');
}
// ─── DÉJÀ VU DOSSIER ─────────────────────────────────────────────────────
// Per-opponent encounter tracking with notes, record, and MMR history
var dejaVuNotes = lsGet('dejaVuNotes5', {}); // name -> { note, mmrHistory:[], encounters:0 }
function pluginDejaVuOnMatchStart(oppId, oppName) {
if (!isInstalled('dejaVu') || !oppId) return;
if ((getPluginSetting('dejaVu','showOnMatch') || 'on') !== 'on') return;
var history = opponentLog.filter(function(o) { return o.opponentId === oppId; });
if (!history.length) {
showToast('🗂️ First encounter: ' + (oppName || 'Opponent'), 'info');
kmLog('DejaVu: first time vs ' + (oppName || oppId));
return;
}
var wins = history.filter(function(o) { return o.result === 'Win'; }).length;
var losses = history.length - wins;
var note = dejaVuNotes[oppId] ? dejaVuNotes[oppId].note : null;
var lastMMR = history[0].mmrGuess ? '~' + history[0].mmrGuess + ' MMR' : '';
var label = oppName || history[0].opponent || 'Opponent';
var msg = '🗂️ ' + label + ' — ' + history.length + 'x played ('
+ wins + 'W ' + losses + 'L)' + (lastMMR ? ' ' + lastMMR : '');
showToast(msg, wins >= losses ? 'ok' : 'warn');
if (note) {
setTimeout(function() {
showToast('📝 Note: "' + note + '"', 'info');
}, 1200);
}
kmLog('DejaVu: ' + msg + (note ? ' | Note: "' + note + '"' : ''));
}
function pluginDejaVuSaveNote(oppId, oppName, note) {
if (!oppId) return;
if (!dejaVuNotes[oppId]) dejaVuNotes[oppId] = { note: '', mmrHistory: [], encounters: 0, name: oppName || '' };
dejaVuNotes[oppId].note = note;
dejaVuNotes[oppId].name = oppName || dejaVuNotes[oppId].name || '';
dejaVuNotes[oppId].encounters = (opponentLog.filter(function(o) { return o.opponentId === oppId; }).length);
lsSet('dejaVuNotes5', dejaVuNotes);
showToast('📝 Note saved for ' + (oppName || 'Opponent'), 'ok');
kmLog('DejaVu: note saved for ' + (oppName || oppId) + ': "' + note + '"');
}
function pluginDejaVuNotePrompt(oppId, oppName) {
if (!isInstalled('dejaVu') || !oppId) return;
if ((getPluginSetting('dejaVu','notePrompt') || 'off') !== 'on') return;
// Show a small note input overlay
var old = document.getElementById('km-dejavu-prompt'); if (old) old.remove();
var existing = (dejaVuNotes[oppId]) ? (dejaVuNotes[oppId].note || '') : '';
var label = oppName || (dejaVuNotes[oppId] && dejaVuNotes[oppId].name) || 'Opponent';
var el = document.createElement('div');
el.id = 'km-dejavu-prompt';
el.style.cssText = 'position:fixed;bottom:110px;right:22px;z-index:999995;'
+ 'background:rgba(6,8,16,0.97);border:1px solid rgba(255,255,255,0.1);border-top:2px solid #3b82f6;'
+ 'padding:14px 16px;width:290px;font-family:"DM Mono",monospace;'
+ 'box-shadow:0 20px 60px rgba(0,0,0,0.8);';
el.innerHTML = '<div style="font-size:9px;letter-spacing:2px;color:rgba(255,255,255,0.3);margin-bottom:8px;text-transform:uppercase">📝 Note for ' + label + '</div>'
+ '<input id="km-dejavu-input" type="text" placeholder="e.g. fakes kickoffs, weak backboard" '
+ 'style="width:100%;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.12);'
+ 'color:#fff;font-family:\'DM Mono\',monospace;font-size:11px;padding:7px 10px;outline:none;box-sizing:border-box;" '
+ 'value="' + existing.replace(/"/g, '"') + '">'
+ '<div style="display:flex;gap:6px;margin-top:8px">'
+ '<button id="km-dejavu-save" style="flex:1;padding:6px;background:#3b82f6;border:none;color:#fff;font-family:\'DM Mono\',monospace;font-size:10px;cursor:pointer;letter-spacing:0.5px">Save</button>'
+ '<button id="km-dejavu-cancel" style="flex:1;padding:6px;background:transparent;border:1px solid rgba(255,255,255,0.1);color:rgba(255,255,255,0.4);font-family:\'DM Mono\',monospace;font-size:10px;cursor:pointer">Dismiss</button>'
+ '</div>';
document.body.appendChild(el);
var inp = el.querySelector('#km-dejavu-input');
if (inp) { inp.focus(); inp.select(); }
el.querySelector('#km-dejavu-save').addEventListener('click', function() {
var val = inp ? inp.value.trim() : '';
pluginDejaVuSaveNote(oppId, oppName, val);
el.remove();
});
el.querySelector('#km-dejavu-cancel').addEventListener('click', function() { el.remove(); });
// Auto-dismiss after 15s
setTimeout(function() { if (el.parentNode) el.remove(); }, 15000);
}
// ─── FLIP TIMER ───────────────────────────────────────────────────────────
var _ftEl = null;
var _ftRaf = null;
var _ftLastJump = 0; // performance.now() of last detected jump
var _ftActive = false;
var _ftKeyDown = {};
function pluginFlipTimerMount() {
if (!isInstalled('flipTimer')) { pluginFlipTimerUnmount(); return; }
if (_ftEl) return;
var pos = getPluginSetting('flipTimer','pos') || 'top-center';
var posCSS = pos === 'bottom-center' ? 'bottom:110px;left:50%;transform:translateX(-50%)'
: pos === 'top-left' ? 'top:60px;left:22px;transform:none'
: pos === 'top-right' ? 'top:60px;right:22px;transform:none'
: 'top:60px;left:50%;transform:translateX(-50%)';
_ftEl = document.createElement('div');
_ftEl.id = 'km-flip-timer';
_ftEl.style.cssText = 'position:fixed;'+posCSS+';z-index:99996;pointer-events:none;'
+'width:180px;text-align:center;font-family:"DM Mono",monospace;';
document.body.appendChild(_ftEl);
// Key listener
var jumpKey = (getPluginSetting('flipTimer','jumpkey') || 'Space').toLowerCase();
var device = getPluginSetting('flipTimer','device') || 'KBM';
if (device === 'KBM') {
window._ftKeyHandler = function(e) {
var k = e.key === ' ' ? 'space' : e.key.toLowerCase();
if (k === jumpKey.toLowerCase() && !_ftKeyDown[k]) {
_ftKeyDown[k] = true;
_ftLastJump = performance.now();
_ftActive = true;
}
};
window._ftKeyUpHandler = function(e) {
var k = e.key === ' ' ? 'space' : e.key.toLowerCase();
_ftKeyDown[k] = false;
};
window.addEventListener('keydown', window._ftKeyHandler, true);
window.addEventListener('keyup', window._ftKeyUpHandler, true);
}
// Controller support via Gamepad API polling
if (device === 'Controller') {
var btnMap = { A:0,B:1,X:2,Y:3,LB:4,RB:5,LT:6,RT:7,L3:10,R3:11 };
var jumpBtn = btnMap[getPluginSetting('flipTimer','jumpbtn') || 'A'] || 0;
var prevPressed = false;
window._ftGamepadInterval = setInterval(function() {
var gp = navigator.getGamepads ? navigator.getGamepads()[0] : null;
if (!gp) return;
var pressed = gp.buttons[jumpBtn] && gp.buttons[jumpBtn].pressed;
if (pressed && !prevPressed) {
_ftLastJump = performance.now();
_ftActive = true;
}
prevPressed = pressed;
}, 16); // ~60Hz poll
}
var _ftSize = getPluginSetting('flipTimer','size') || 'medium';
var _ftDims = _ftSize === 'small' ? { w:140, big:18, small:9, bar:3, pad:'5px 10px' }
: _ftSize === 'large' ? { w:220, big:30, small:11, bar:5, pad:'9px 18px' }
: { w:180, big:24, small:10, bar:4, pad:'7px 14px' };
_ftEl.style.width = _ftDims.w + 'px';
function ftDraw() {
if (!_ftEl || !isInstalled('flipTimer')) { pluginFlipTimerUnmount(); return; }
_ftRaf = requestAnimationFrame(ftDraw);
var windowMs = parseInt(getPluginSetting('flipTimer','window') || '1450');
if (!_ftActive || !inMatch) {
// IDLE state — green pill showing window duration
_ftEl.innerHTML =
'<div style="background:rgba(0,0,0,0.72);border:1px solid rgba(74,222,128,0.25);'
+'border-left:3px solid #4ade80;padding:'+_ftDims.pad+';border-radius:7px;'
+'display:flex;align-items:center;justify-content:space-between;gap:10px;">'
+'<div style="display:flex;flex-direction:column;gap:2px;">'
+'<div style="font-size:'+_ftDims.small+'px;color:rgba(255,255,255,0.3);letter-spacing:0.5px;font-family:monospace;">FLIP TIMER</div>'
+'<div style="font-size:'+_ftDims.small+'px;color:rgba(74,222,128,0.6);font-family:monospace;">READY</div>'
+'</div>'
+'<div style="font-size:'+_ftDims.big+'px;font-weight:700;color:#4ade80;font-family:monospace;font-variant-numeric:tabular-nums;">'
+windowMs+'<span style="font-size:'+(Math.round(_ftDims.small*0.9))+'px;opacity:0.4;margin-left:1px">ms</span>'
+'</div>'
+'</div>';
return;
}
var elapsed = performance.now() - _ftLastJump;
var remaining = windowMs - elapsed;
var pct = Math.max(0, Math.min(1, remaining / windowMs));
if (remaining > 0) {
// Active countdown — color transitions green → yellow → red
var col = pct > 0.55 ? '#4ade80' : pct > 0.28 ? '#fbbf24' : '#f87171';
var borderCol = col;
_ftEl.innerHTML =
'<div style="background:rgba(0,0,0,0.82);border:1px solid rgba(255,255,255,0.1);'
+'border-left:3px solid '+borderCol+';padding:'+_ftDims.pad+';border-radius:7px;">'
+'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:'+Math.round(_ftDims.bar+2)+'px;">'
+'<div style="font-size:'+_ftDims.small+'px;color:rgba(255,255,255,0.3);letter-spacing:0.5px;font-family:monospace;">FLIP WINDOW</div>'
+'<div style="font-size:'+_ftDims.big+'px;font-weight:700;color:'+col+';font-family:monospace;font-variant-numeric:tabular-nums;line-height:1;">'
+Math.ceil(remaining)+'<span style="font-size:'+(Math.round(_ftDims.small*0.9))+'px;opacity:0.4;margin-left:1px">ms</span></div>'
+'</div>'
+'<div style="height:'+_ftDims.bar+'px;background:rgba(255,255,255,0.08);border-radius:'+_ftDims.bar+'px;overflow:hidden;">'
+'<div style="height:100%;width:'+(pct*100).toFixed(2)+'%;background:'+col+';border-radius:'+_ftDims.bar+'px;transition:width 0.04s linear,background 0.2s;"></div>'
+'</div>'
+'</div>';
} else {
// NO FLIP — bright red, pulsing
var noFlipAge = elapsed - windowMs;
_ftEl.innerHTML =
'<div style="background:rgba(180,0,0,0.9);border:2px solid rgba(248,113,113,0.5);'
+'padding:'+_ftDims.pad+';border-radius:7px;text-align:center;'
+'animation:km-ft-pulse 0.35s ease-in-out infinite alternate;">'
+'<div style="font-size:'+_ftDims.big+'px;font-weight:800;color:#fff;letter-spacing:2px;'
+'font-family:monospace;text-shadow:0 0 12px rgba(255,100,100,0.8);">NO FLIP</div>'
+'</div>';
if (noFlipAge > 1800) _ftActive = false;
}
}
_ftRaf = requestAnimationFrame(ftDraw);
// Inject pulse keyframe
if (!document.getElementById('km-ft-style')) {
var s = document.createElement('style'); s.id = 'km-ft-style';
s.textContent = '@keyframes km-ft-pulse{from{opacity:1;transform:scale(1)}to{opacity:0.7;transform:scale(0.97)}}';
document.head.appendChild(s);
}
// Make flip timer select dropdowns taller for easier scrolling
var ftStyle = document.getElementById('km-ft-select-style');
if (!ftStyle) {
ftStyle = document.createElement('style'); ftStyle.id = 'km-ft-select-style';
ftStyle.textContent = '.km-plugin-setting[data-plugin="flipTimer"]{max-height:160px;overflow-y:auto;}';
document.head.appendChild(ftStyle);
}
kmLog('Flip Timer mounted — device:' + device + ' key:' + (getPluginSetting('flipTimer','jumpkey')||'Space'));
}
function pluginFlipTimerUnmount() {
if (_ftRaf) { cancelAnimationFrame(_ftRaf); _ftRaf = null; }
if (_ftEl) { _ftEl.remove(); _ftEl = null; }
if (window._ftKeyHandler) window.removeEventListener('keydown', window._ftKeyHandler, true);
if (window._ftKeyUpHandler) window.removeEventListener('keyup', window._ftKeyUpHandler, true);
if (window._ftGamepadInterval) { clearInterval(window._ftGamepadInterval); window._ftGamepadInterval = null; }
_ftActive = false; _ftKeyDown = {};
}
// ─── THEME PLUGIN ─────────────────────────────────────────────────────────
// Theme is locked to BakkesMod unless this plugin is installed.
// On install: apply the saved or selected preset.
// On uninstall: force back to bakkesmod.
function pluginThemeOnInstall() {
// Theme plugin removed. Keep default theme.
}
function pluginThemeOnUninstall() {
// Theme plugin removed. Keep default theme.
}
function pluginThemeOnSettingChange() {
// Theme plugin removed.
}
// ─── LOADING SCREEN PLUGIN ────────────────────────────────────────────────
// Loading screen plugin removed.
function pluginLoadingGetStyle() {
return 'off';
}
// ─── VISUAL FX ────────────────────────────────────────────────────────────
var _vfxStyle = null;
var _vfxPresets = {
off: { vibrance:100, blur:0, contrast:100, blacks:0, whites:100, hue:0 },
vivid: { vibrance:160, blur:0, contrast:115, blacks:5, whites:105, hue:0 },
cinematic: { vibrance:80, blur:0, contrast:130, blacks:15, whites:90, hue:0 },
night: { vibrance:60, blur:0, contrast:90, blacks:20, whites:80, hue:200 },
washed: { vibrance:50, blur:1, contrast:85, blacks:0, whites:115, hue:0 },
vigilante: { vibrance:0, blur:0, contrast:140, blacks:10, whites:95, hue:0 },
retro: { vibrance:70, blur:1, contrast:105, blacks:8, whites:108, hue:30 },
};
function pluginVFXMount() {
if (!isInstalled('visualFX')) { pluginVFXUnmount(); return; }
var preset = getPluginSetting('visualFX','preset') || 'off';
var vals;
if (preset !== 'custom' && _vfxPresets[preset]) {
vals = _vfxPresets[preset];
// Sync slider values to preset
Object.keys(vals).forEach(function(k) { setPluginSetting('visualFX',k,vals[k]); });
} else {
vals = {
vibrance: parseInt(getPluginSetting('visualFX','vibrance')) || 100,
blur: parseFloat(getPluginSetting('visualFX','blur')) || 0,
contrast: parseInt(getPluginSetting('visualFX','contrast')) || 100,
blacks: parseInt(getPluginSetting('visualFX','blacks')) || 0,
whites: parseInt(getPluginSetting('visualFX','whites')) || 100,
hue: parseInt(getPluginSetting('visualFX','hue')) || 0,
};
}
if (preset === 'off') { pluginVFXUnmount(); return; }
// Build CSS filter string
// Vibrance > 100 = saturate more, < 100 = desaturate
// blacks/whites = brightness + levels approximation via brightness+contrast
var saturate = vals.vibrance;
var brightness= Math.round(100 - vals.blacks / 2 + (vals.whites - 100) / 4);
var filterStr = [
'saturate(' + saturate + '%)',
'blur(' + vals.blur + 'px)',
'contrast(' + vals.contrast + '%)',
'brightness(' + brightness + '%)',
'hue-rotate(' + vals.hue + 'deg)',
].join(' ');
if (!_vfxStyle) {
_vfxStyle = document.createElement('style');
_vfxStyle.id = 'km-vfx-style';
document.head.appendChild(_vfxStyle);
}
_vfxStyle.textContent = 'canvas{filter:' + filterStr + '!important;}';
kmLog('VFX: ' + preset + ' filter=' + filterStr);
}
function pluginVFXUnmount() {
if (_vfxStyle) { _vfxStyle.remove(); _vfxStyle = null; }
}
// Night mode check on load
setInterval(checkNightMode, 60000);
function boot() {
injectStyles();
(function() {
// Only apply GPU hints to the Unity game canvas — not every canvas on the page.
// We wait for the canvas to have content (non-zero size) before promoting it,
// applying too early to a blank canvas creates a black GPU layer.
function _applyGPUHints() {
document.querySelectorAll('canvas').forEach(function(cv) {
if (cv.dataset.kmGpu) return;
// Skip KeyMod overlay canvases
if (cv.id && cv.id.indexOf('km-') === 0) return;
// Skip tiny or zero-size canvases — Unity canvas is full-screen
if (cv.offsetWidth < 200 || cv.offsetHeight < 200) return;
cv.dataset.kmGpu = '1';
cv.style.willChange = 'transform';
cv.style.transform = 'translateZ(0)';
cv.style.backfaceVisibility = 'hidden';
});
}
// Don't run immediately at boot — wait for DOM to settle
// Unity canvas appears after page load, not at document-start
setTimeout(_applyGPUHints, 2000);
var _gpuObs = new MutationObserver(function(muts) {
muts.forEach(function(m) {
m.addedNodes.forEach(function(n) {
if (n.tagName === 'CANVAS') setTimeout(_applyGPUHints, 500);
if (n.querySelectorAll) n.querySelectorAll('canvas').forEach(function(){ setTimeout(_applyGPUHints, 500); });
});
});
});
_gpuObs.observe(document.documentElement, { childList: true, subtree: true });
})();
(function() {
if (window._kmBootXHRPatched) return;
window._kmBootXHRPatched = true;
var _xhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(m, u) {
if (typeof u === 'string' && (
u.indexOf('gameanalytics') !== -1 ||
u.indexOf('rubick.gameanalytics') !== -1 ||
u.indexOf('googletagmanager') !== -1 ||
u.indexOf('google-analytics') !== -1 ||
u.indexOf('doubleclick') !== -1
)) {
this._kmBootBlocked = true;
this._km_url = u; // still track URL for RTT patch
return;
}
return _xhrOpen.apply(this, arguments);
};
var _xhrSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
if (this._kmBootBlocked) return;
return _xhrSend.apply(this, arguments);
};
})();
(function() {
var _ptrLockActive = false;
document.addEventListener('click', function(e) {
if (_ptrLockActive) return;
var cv = document.querySelector('canvas');
if (cv && !document.pointerLockElement && e.target === cv) {
cv.requestPointerLock && cv.requestPointerLock().catch && cv.requestPointerLock().catch(function(){});
}
}, true);
document.addEventListener('pointerlockchange', function() {
_ptrLockActive = !!document.pointerLockElement;
});
// Press Escape to release without leaving game
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && _ptrLockActive) {
document.exitPointerLock && document.exitPointerLock();
}
}, true);
})();
buildLoadingScreen();
buildHUD();
var _menuBuildFn = function() {
buildMenu();
buildSupportPopup();
addResizeHandle();
};
if (window.requestIdleCallback) {
requestIdleCallback(_menuBuildFn, { timeout: 1500 });
} else {
setTimeout(_menuBuildFn, 0);
}
window.addEventListener('keydown', function(e) {
if (e.key === 'F2' && !e.repeat) {
e.preventDefault(); e.stopImmediatePropagation(); toggleMenu();
}
}, true);
window.addEventListener('keyup', function(e) {
if (e.key === 'F2') _kmKeyLock = false;
}, true);
setTimeout(function() { toggleMenu(true); }, 1200);
pluginPerfMetricsMount();
pluginStreamerMount();
pluginOptimizerMount();
console.log('KeyMod v5.0.0 by @keydopz — F2 / backtick / 1 to toggle');
}
document.body ? boot() : document.addEventListener('DOMContentLoaded', boot);
})();