Greasy Fork is available in English.
Bonk Enhanced — tracker, encrypted account manager, keybind overlay, stats, fullscreen and a recording option in the topbar of bonk itself
// ==UserScript==
// @name Bonk Enhanced
// @namespace Bonk Enhanced
// @version 3.5.1
// @description Bonk Enhanced — tracker, encrypted account manager, keybind overlay, stats, fullscreen and a recording option in the topbar of bonk itself
// @match https://bonk.io/*
// @match https://bonk.io/gameframe-release.html*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
/** Keep in sync with // @version in the userscript header. */
const BE_SCRIPT_VERSION = "3.5.1";
const BE_SCRIPT_AUTHOR = "Elysiun";
const UI = {};
const UI_SETTINGS_KEY = "be_ui_settings_v5";
const UI_DEFAULT_SETTINGS = {
theme: "dark",
size: "medium",
overlayHotkey: "Delete",
widgetHotkey: "Home",
keybindOverlayEnabled: false,
keybindOverlayOpacity: 1,
keybindOverlayScale: 1,
keybindOverlayCompact: false,
graphWeek: "",
overviewSlots: {
tiny: ["wins", "rate", "lastWin"],
medium: ["xp", "wins", "rate", "lastWin"],
large: ["xp", "wins", "rate", "lastWin", "session", "level"]
},
dashboardStats: {
xp: true,
wins: true,
rate: true,
lastWin: true
},
uiPresets: []
};
let uiSettings = loadUISettings();
let isCapturingHotkey = false;
const isGameFrame = window.location.href.includes("gameframe-release.html");
const BE_CONFIG = {
env: localStorage.getItem("be_env") || "test",
debug: localStorage.getItem("be_debug") === "1",
featureFlags: {
enableOuterMods: true,
enableTracker: true,
safeModeGuards: true
}
};
const BE_HEALTH = {
scriptBoot: { ok: true, detail: "started", ts: Date.now() }
};
function markHealth(key, ok, detail) {
BE_HEALTH[key] = { ok: !!ok, detail: detail || "", ts: Date.now() };
}
const BE_LOG = {
info(...args) { console.info("[BonkEnhanced]", ...args); },
warn(...args) { console.warn("[BonkEnhanced]", ...args); },
error(...args) { console.error("[BonkEnhanced]", ...args); },
debug(...args) {
if (BE_CONFIG.debug || BE_CONFIG.env === "test") {
console.debug("[BonkEnhanced:debug]", ...args);
}
}
};
function safeRun(key, fn) {
try {
const out = fn();
markHealth(key, true, "ok");
return out;
} catch (err) {
markHealth(key, false, err?.message || "failed");
BE_LOG.error(`${key} failed`, err);
return null;
}
}
function beTabHidden() {
try {
return document.visibilityState === "hidden";
} catch {
return false;
}
}
/** True when the custom games room list UI is visible (top window or #maingameframe iframe). */
function beIsBonkRoomListVisible() {
try {
const vis = (el) => el && el.offsetParent !== null;
if (vis(document.getElementById("roomListContainer"))) return true;
const f = document.getElementById("maingameframe");
const inner = f?.contentDocument?.getElementById("roomListContainer");
return vis(inner);
} catch {
return false;
}
}
function beBackoffMs(base, mult = 4, cap = 120000) {
if (!beTabHidden()) return base;
return Math.min(cap, Math.max(base, Math.floor(base * mult)));
}
/** Bump when Bonk DOM hook lists change (health / diagnostics). */
const BE_SELECTOR_SCHEMA = "bonk_dom_v1";
function loadUISettings() {
try {
const raw = localStorage.getItem(UI_SETTINGS_KEY);
if (!raw) return { ...UI_DEFAULT_SETTINGS };
const parsed = JSON.parse(raw);
return {
theme: parsed?.theme === "light" ? "light" : "dark",
size: ["tiny", "medium", "large"].includes(parsed?.size) ? parsed.size : "medium",
overlayHotkey: typeof parsed?.overlayHotkey === "string" && parsed.overlayHotkey.trim()
? parsed.overlayHotkey
: "Delete",
widgetHotkey: typeof parsed?.widgetHotkey === "string" && parsed.widgetHotkey.trim()
? parsed.widgetHotkey
: "Home",
keybindOverlayEnabled: parsed?.keybindOverlayEnabled === true,
graphWeek: typeof parsed?.graphWeek === "string" ? parsed.graphWeek : "",
overviewSlots: {
tiny: Array.isArray(parsed?.overviewSlots?.tiny) ? parsed.overviewSlots.tiny.slice(0, 3) : ["wins", "rate", "lastWin"],
medium: Array.isArray(parsed?.overviewSlots?.medium) ? parsed.overviewSlots.medium.slice(0, 4) : ["xp", "wins", "rate", "lastWin"],
large: Array.isArray(parsed?.overviewSlots?.large) ? parsed.overviewSlots.large.slice(0, 6) : ["xp", "wins", "rate", "lastWin", "session", "level"]
},
dashboardStats: {
xp: parsed?.dashboardStats?.xp !== false,
wins: parsed?.dashboardStats?.wins !== false,
rate: parsed?.dashboardStats?.rate !== false,
lastWin: parsed?.dashboardStats?.lastWin !== false
},
keybindOverlayOpacity: typeof parsed?.keybindOverlayOpacity === "number"
? Math.min(1, Math.max(0.35, parsed.keybindOverlayOpacity))
: 1,
keybindOverlayScale: typeof parsed?.keybindOverlayScale === "number"
? Math.min(1.35, Math.max(0.65, parsed.keybindOverlayScale))
: 1,
keybindOverlayCompact: parsed?.keybindOverlayCompact === true,
uiPresets: Array.isArray(parsed?.uiPresets)
? parsed.uiPresets.filter((p) => p && typeof p === "object" && p.name && p.data).slice(0, 8)
: []
};
} catch {
return { ...UI_DEFAULT_SETTINGS };
}
}
function saveUISettings() {
localStorage.setItem(UI_SETTINGS_KEY, JSON.stringify(uiSettings));
}
function applyOverlaySettings() {
if (!UI.root) return;
UI.root.classList.toggle("be-theme-light", uiSettings.theme === "light");
UI.root.classList.toggle("be-theme-dark", uiSettings.theme !== "light");
UI.root.dataset.size = uiSettings.size;
const hotkeyLabel = document.getElementById("beHotkeyCurrent");
if (hotkeyLabel) hotkeyLabel.textContent = uiSettings.overlayHotkey;
const widgetHotkeyLabel = document.getElementById("beWidgetHotkeyCurrent");
if (widgetHotkeyLabel) widgetHotkeyLabel.textContent = uiSettings.widgetHotkey;
if (UI.launcherLabel) UI.launcherLabel.textContent = uiSettings.overlayHotkey;
const topLauncherHk = document.getElementById("pretty_top_be_launcher_hk");
if (topLauncherHk) topLauncherHk.textContent = uiSettings.overlayHotkey;
if (UI.widgetHubHint) UI.widgetHubHint.textContent = uiSettings.widgetHotkey;
applyKeybindOverlayVisibility();
}
/** Physical key (event.code) → action slots currently held by that key (shared keys light multiple cells). */
const BE_KB_PRESSED = new Map();
let __beKeybindOverlayListenersBound = false;
let __beKeybindPollTimer = null;
/** Match Bonk’s “Key 1/2/3” display strings to the same style from a KeyboardEvent. */
function beBonkStyleKeyLabel(ev) {
if (!ev) return "";
const k = ev.key;
const c = ev.code || "";
if (k === "ArrowUp") return "UP ARROW";
if (k === "ArrowDown") return "DOWN ARROW";
if (k === "ArrowLeft") return "LEFT ARROW";
if (k === "ArrowRight") return "RIGHT ARROW";
if (k === " " || c === "Space") return "SPACE";
if (k === "Shift") return "SHIFT";
if (k === "Control") return "CONTROL";
if (k === "Alt") return "ALT";
if (k === "Meta") return "META";
if (k === "Enter") return "ENTER";
if (k === "Tab") return "TAB";
if (k === "Backspace") return "BACKSPACE";
if (k === "Delete") return "DELETE";
if (k === "Escape") return "ESCAPE";
if (/^Numpad\d$/.test(c)) return "NUMPAD " + c.replace("Numpad", "");
if (k && k.length === 1) return k.toUpperCase();
return String(k || c || "").toUpperCase();
}
function beMatcherFromBonkLabel(label) {
const raw = String(label || "")
.replace(/\s+/g, " ")
.trim();
if (!raw) return null;
const u = raw.toUpperCase();
if (u === "LEFT ARROW") return (e) => e.key === "ArrowLeft";
if (u === "RIGHT ARROW") return (e) => e.key === "ArrowRight";
if (u === "UP ARROW") return (e) => e.key === "ArrowUp";
if (u === "DOWN ARROW") return (e) => e.key === "ArrowDown";
if (u === "SPACE" || u === "SPACEBAR") return (e) => e.code === "Space" || e.key === " ";
if (u === "SHIFT") return (e) => e.key === "Shift";
if (u === "CONTROL" || u === "CTRL") return (e) => e.key === "Control";
if (u === "ALT") return (e) => e.key === "Alt";
if (u === "META" || u === "WINDOWS" || u === "WIN") return (e) => e.key === "Meta";
if (u === "ENTER" || u === "RETURN") return (e) => e.key === "Enter";
if (u === "TAB") return (e) => e.key === "Tab";
if (u === "BACKSPACE") return (e) => e.key === "Backspace";
if (u === "DELETE" || u === "DEL") return (e) => e.key === "Delete";
if (u === "ESCAPE" || u === "ESC") return (e) => e.key === "Escape";
const np = u.match(/^NUMPAD\s*(\d)$/);
if (np) {
const code = "Numpad" + np[1];
return (e) => e.code === code;
}
if (/^F([1-9]|1[0-2])$/i.test(raw)) return (e) => e.key.toUpperCase() === u;
if (raw.length === 1) return (e) => e.key && e.key.length === 1 && e.key.toUpperCase() === u;
return (e) => beBonkStyleKeyLabel(e) === u;
}
function beDefaultKeybindMatchers() {
return {
left: [(e) => e.key === "ArrowLeft"],
right: [(e) => e.key === "ArrowRight"],
up: [(e) => e.key === "ArrowUp"],
down: [(e) => e.key === "ArrowDown"],
heavy: [(e) => e.key === "x" || e.key === "X"],
special: [(e) => {
const x = e.key;
return x === "z" || x === "Z" || x === "y" || x === "Y";
}]
};
}
let BE_KEYBIND_MATCHERS = beDefaultKeybindMatchers();
function beGetBonkGameDocument() {
try {
const href = window.location.href || "";
if (href.indexOf("gameframe-release.html") >= 0) return document;
const f = document.getElementById("maingameframe");
if (f && f.contentDocument) return f.contentDocument;
} catch (e) {
BE_LOG.debug("beGetBonkGameDocument", e);
}
return null;
}
function beFindRedefineControlsTable(doc) {
if (!doc) return null;
const ids = ["redefineControls_table", "bonk_redefineControls_table", "redefine_controls_table"];
for (let i = 0; i < ids.length; i++) {
const t = doc.getElementById(ids[i]);
if (t) {
markHealth("bonk.redefineTableNode", true, `${BE_SELECTOR_SCHEMA}:${ids[i]}`);
return t;
}
}
try {
const q = doc.querySelector("[id*='edefineControl'][id*='table'], table[id*='controls_table']");
if (q) {
markHealth("bonk.redefineTableNode", true, `${BE_SELECTOR_SCHEMA}:fuzzy`);
return q;
}
} catch (e) {
BE_LOG.debug("beFindRedefineControlsTable fuzzy", e);
}
markHealth("bonk.redefineTableNode", false, `${BE_SELECTOR_SCHEMA}:not found`);
return null;
}
function beParseRedefineControlsTable(doc) {
const table = beFindRedefineControlsTable(doc);
if (!table) return null;
const slotByAction = {
left: "left",
right: "right",
up: "up",
down: "down",
heavy: "heavy",
special: "special"
};
const matchers = {
left: [],
right: [],
up: [],
down: [],
heavy: [],
special: []
};
let count = 0;
table.querySelectorAll("tr").forEach((tr) => {
const tds = tr.querySelectorAll("td");
if (tds.length < 4) return;
const actionText = (tds[0].textContent || "").trim().toLowerCase();
const slot = slotByAction[actionText];
if (!slot) return;
for (let c = 1; c <= 3; c++) {
const cellText = (tds[c].textContent || "").replace(/\s+/g, " ").trim();
if (!cellText) continue;
const m = beMatcherFromBonkLabel(cellText);
if (m) {
matchers[slot].push(m);
count += 1;
}
}
});
if (count === 0) return null;
return { matchers };
}
/** If Bonk’s table omits a row or a slot has no keys, keep default matchers for that slot so highlights still work. */
function beCoalesceMatchersWithDefaults(parsedMatchers) {
const d = beDefaultKeybindMatchers();
const slots = ["left", "right", "up", "down", "heavy", "special"];
const out = {};
slots.forEach((slot) => {
const c = parsedMatchers && parsedMatchers[slot] && parsedMatchers[slot].length ? parsedMatchers[slot] : null;
out[slot] = c && c.length ? c.slice() : (d[slot] ? d[slot].slice() : []);
});
return out;
}
function beGetKeybindOverlayRoot() {
try {
const a = document.getElementById("beKeybindOverlay");
if (a) return a;
if (window.top && window.top !== window && window.top.document) {
return window.top.document.getElementById("beKeybindOverlay");
}
} catch (e) {
BE_LOG.debug("beGetKeybindOverlayRoot", e);
}
return null;
}
function beIsKeybindOverlayShown(root) {
if (!root) return false;
try {
if (root.style && root.style.display === "none") return false;
return window.getComputedStyle(root).display !== "none";
} catch {
return true;
}
}
/** Overlay cells always show action names, not physical keys (matchers still come from Bonk’s table). */
function beApplyKeybindOverlayActionLabels() {
const root = beGetKeybindOverlayRoot();
if (!root) return;
const wide = { up: "UP", left: "LEFT", down: "DOWN", right: "RIGHT", heavy: "HEAVY", special: "SPECIAL" };
const narrow = { up: "U", left: "L", down: "D", right: "R", heavy: "H", special: "S" };
const cap = root.classList.contains("be-kb-compact") ? narrow : wide;
Object.keys(wide).forEach((slot) => {
root.querySelectorAll(`[data-be-kb="${slot}"] span`).forEach((el) => {
el.textContent = cap[slot];
});
});
}
const BE_KB_OVERLAY_POS_KEY = "be_keybind_overlay_pos_v1";
function beInitKeybindOverlayDrag(overlayEl) {
if (!overlayEl || overlayEl.__beKbDragInit) return;
overlayEl.__beKbDragInit = true;
const handle = overlayEl.querySelector("#beKbDragHandle");
if (!handle) return;
try {
const raw = localStorage.getItem(BE_KB_OVERLAY_POS_KEY);
if (raw) {
const p = JSON.parse(raw);
if (typeof p.left === "number" && typeof p.top === "number") {
overlayEl.style.left = p.left + "px";
overlayEl.style.top = p.top + "px";
overlayEl.style.bottom = "auto";
overlayEl.style.right = "auto";
}
}
} catch (e) {
BE_LOG.debug("beInitKeybindOverlayDrag load pos", e);
}
let drag = null;
handle.addEventListener("mousedown", (e) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
const r = overlayEl.getBoundingClientRect();
drag = {
dx: e.clientX - r.left,
dy: e.clientY - r.top,
w: r.width,
h: r.height
};
overlayEl.style.bottom = "auto";
overlayEl.style.right = "auto";
overlayEl.style.left = r.left + "px";
overlayEl.style.top = r.top + "px";
handle.classList.add("be-kb-dragging");
try {
document.body.style.userSelect = "none";
} catch {}
});
const move = (e) => {
if (!drag) return;
let nl = e.clientX - drag.dx;
let nt = e.clientY - drag.dy;
const pad = 6;
const maxL = Math.max(pad, window.innerWidth - drag.w - pad);
const maxT = Math.max(pad, window.innerHeight - drag.h - pad);
nl = Math.min(maxL, Math.max(pad, nl));
nt = Math.min(maxT, Math.max(pad, nt));
overlayEl.style.left = nl + "px";
overlayEl.style.top = nt + "px";
};
const up = () => {
if (!drag) return;
drag = null;
handle.classList.remove("be-kb-dragging");
try {
document.body.style.userSelect = "";
} catch {}
try {
const l = parseFloat(overlayEl.style.left) || 0;
const t = parseFloat(overlayEl.style.top) || 0;
localStorage.setItem(BE_KB_OVERLAY_POS_KEY, JSON.stringify({ left: l, top: t }));
} catch (err) {
BE_LOG.debug("beInitKeybindOverlayDrag save pos", err);
}
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", up);
}
function beAttachRedefineTableObserver(doc) {
if (!doc || doc.__beKeybindTableHooked) return;
const table = beFindRedefineControlsTable(doc);
if (!table) return;
doc.__beKeybindTableHooked = true;
try {
doc.__beKeybindTableMo = new MutationObserver(() => {
if (uiSettings.keybindOverlayEnabled) refreshBonkKeybindOverlayFromGame();
});
doc.__beKeybindTableMo.observe(table, { childList: true, subtree: true, characterData: true });
} catch (e) {
BE_LOG.debug("beAttachRedefineTableObserver", e);
}
}
function refreshBonkKeybindOverlayFromGame() {
let parsed = null;
try {
const doc = beGetBonkGameDocument();
if (doc) {
beAttachRedefineTableObserver(doc);
parsed = beParseRedefineControlsTable(doc);
}
} catch (e) {
BE_LOG.debug("refreshBonkKeybindOverlayFromGame", e);
}
if (parsed && parsed.matchers) {
BE_KEYBIND_MATCHERS = beCoalesceMatchersWithDefaults(parsed.matchers);
markHealth("bonk.keybindParse", true, `${BE_SELECTOR_SCHEMA}:parsed`);
} else {
BE_KEYBIND_MATCHERS = beDefaultKeybindMatchers();
markHealth("bonk.keybindParse", false, `${BE_SELECTOR_SCHEMA}:defaults`);
}
beApplyKeybindOverlayActionLabels();
beHookKeybindToMaingameframe();
}
/** All action slots this key event maps to (one physical key may map to several). */
function beKeybindSlotsForEvent(ev) {
const order = ["left", "right", "up", "down", "heavy", "special"];
const out = [];
for (let i = 0; i < order.length; i++) {
const slot = order[i];
const fns = BE_KEYBIND_MATCHERS[slot];
if (!fns || !fns.length) continue;
let hit = false;
for (let j = 0; j < fns.length; j++) {
try {
if (fns[j](ev)) {
hit = true;
break;
}
} catch {}
}
if (hit) out.push(slot);
}
return out;
}
function beKbActiveSlotsUnion() {
const on = new Set();
BE_KB_PRESSED.forEach((slots) => {
(slots || []).forEach((s) => on.add(s));
});
return on;
}
function beSyncKeybindOverlayHighlights() {
if (!uiSettings.keybindOverlayEnabled) return;
const root = beGetKeybindOverlayRoot();
if (!root || !beIsKeybindOverlayShown(root)) return;
const on = beKbActiveSlotsUnion();
root.querySelectorAll("[data-be-kb]").forEach((node) => {
const s = node.getAttribute("data-be-kb");
if (!s) return;
node.classList.toggle("be-kb-active", on.has(s));
});
}
function applyKeybindOverlayVisibility() {
let el = beGetKeybindOverlayRoot();
if (uiSettings.keybindOverlayEnabled && !el) {
ensureBonkKeybindOverlayMounted();
el = beGetKeybindOverlayRoot();
}
const toggle = document.getElementById("beKeybindOverlayToggle");
const st = document.getElementById("beKeybindOverlayState");
if (toggle) toggle.checked = uiSettings.keybindOverlayEnabled === true;
if (st) st.textContent = uiSettings.keybindOverlayEnabled ? "On" : "Off";
if (el) {
el.style.display = uiSettings.keybindOverlayEnabled ? "block" : "none";
if (!uiSettings.keybindOverlayEnabled) {
BE_KB_PRESSED.clear();
el.querySelectorAll(".be-kb-active").forEach((n) => n.classList.remove("be-kb-active"));
} else {
try {
refreshBonkKeybindOverlayFromGame();
} catch (e) {
BE_LOG.debug("applyKeybindOverlayVisibility refresh", e);
}
}
}
applyKeybindOverlayStyle();
}
function applyKeybindOverlayStyle() {
const el = beGetKeybindOverlayRoot();
if (!el) return;
const op = typeof uiSettings.keybindOverlayOpacity === "number" ? uiSettings.keybindOverlayOpacity : 1;
const sc = typeof uiSettings.keybindOverlayScale === "number" ? uiSettings.keybindOverlayScale : 1;
el.style.opacity = String(op);
el.style.setProperty("--be-kb-scale", String(sc));
el.classList.toggle("be-kb-compact", uiSettings.keybindOverlayCompact === true);
beApplyKeybindOverlayActionLabels();
}
function beEnsureKeybindKeyboardHandlers() {
if (window.__beKbKeyHandlers) return;
const onDown = (e) => {
if (!uiSettings.keybindOverlayEnabled) return;
if (isCapturingHotkey) return;
const tag = e.target?.tagName?.toLowerCase?.() || "";
if (tag === "input" || tag === "textarea" || e.target?.isContentEditable) return;
const slots = beKeybindSlotsForEvent(e);
if (!slots.length || e.repeat) return;
let kc = e.code || "";
if (!kc && e.key && e.key.length === 1) kc = "Key" + e.key.toUpperCase();
if (!kc) return;
BE_KB_PRESSED.set(kc, slots);
beSyncKeybindOverlayHighlights();
};
const onUp = (e) => {
if (!uiSettings.keybindOverlayEnabled) return;
let kc = e.code || "";
if (!kc && e.key && e.key.length === 1) kc = "Key" + e.key.toUpperCase();
if (kc) BE_KB_PRESSED.delete(kc);
beSyncKeybindOverlayHighlights();
};
const onBlur = () => {
BE_KB_PRESSED.clear();
beSyncKeybindOverlayHighlights();
};
window.__beKbKeyHandlers = { onDown, onUp, onBlur };
window.addEventListener("keydown", onDown, true);
window.addEventListener("keyup", onUp, true);
window.addEventListener("blur", onBlur);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") onBlur();
});
}
/** While the canvas has focus, key events go to the iframe window — not the top page. */
function beHookKeybindToMaingameframe() {
const h = window.__beKbKeyHandlers;
if (!h) return;
try {
if ((window.location.href || "").indexOf("gameframe-release.html") >= 0) return;
const f = document.getElementById("maingameframe");
const fw = f && f.contentWindow;
if (!fw || fw.__beKbGameFrameHooked) return;
fw.__beKbGameFrameHooked = true;
fw.addEventListener("keydown", h.onDown, true);
fw.addEventListener("keyup", h.onUp, true);
fw.addEventListener("blur", h.onBlur);
try {
fw.document.addEventListener("visibilitychange", () => {
try {
if (fw.document.visibilityState === "hidden") h.onBlur();
} catch {}
});
} catch {}
} catch (e) {
BE_LOG.debug("beHookKeybindToMaingameframe", e);
}
}
function beEnsureKeybindPollInterval() {
if (__beKeybindPollTimer) {
try {
clearInterval(__beKeybindPollTimer);
} catch {}
__beKeybindPollTimer = null;
}
const ms = beTabHidden() ? 3600 : 900;
__beKeybindPollTimer = window.setInterval(() => {
if (!uiSettings.keybindOverlayEnabled) return;
refreshBonkKeybindOverlayFromGame();
beHookKeybindToMaingameframe();
}, ms);
}
function bindBonkKeybindOverlayListeners() {
beEnsureKeybindKeyboardHandlers();
if (!__beKeybindOverlayListenersBound) {
__beKeybindOverlayListenersBound = true;
if (!window.__beKbPollVisHooked) {
window.__beKbPollVisHooked = true;
document.addEventListener("visibilitychange", () => beEnsureKeybindPollInterval());
}
}
beEnsureKeybindPollInterval();
beHookKeybindToMaingameframe();
try {
const f = document.getElementById("maingameframe");
if (f && !f.__beKbFrameLoadHooked) {
f.__beKbFrameLoadHooked = true;
f.addEventListener("load", () => {
try {
const w = f.contentWindow;
if (w) delete w.__beKbGameFrameHooked;
} catch {}
beHookKeybindToMaingameframe();
});
}
} catch (e) {
BE_LOG.debug("bindBonkKeybindOverlayListeners frame load", e);
}
}
function ensureBonkKeybindOverlayMounted() {
try {
if (window.top !== window) {
let topHasOverlay = false;
try {
topHasOverlay = !!(window.top && window.top.document && window.top.document.getElementById("beKeybindOverlay"));
} catch {
topHasOverlay = false;
}
if (topHasOverlay) return;
}
} catch {}
const existingKb = document.getElementById("beKeybindOverlay");
if (existingKb) {
beInitKeybindOverlayDrag(existingKb);
bindBonkKeybindOverlayListeners();
applyKeybindOverlayVisibility();
refreshBonkKeybindOverlayFromGame();
return;
}
const wrap = document.createElement("div");
wrap.id = "beKeybindOverlay";
wrap.setAttribute("aria-hidden", "true");
wrap.innerHTML = `
<div class="be-kb-panel">
<div class="be-kb-draghead" id="beKbDragHandle" title="Drag to move">
<span class="be-kb-drag-grip" aria-hidden="true"></span>
<span class="be-kb-drag-title">Bonk controls</span>
</div>
<div class="be-kb-body">
<div class="be-kb-grid">
<div class="be-kb-spacer"></div>
<div class="be-kb-key" data-be-kb="up" title="Up"><span id="be-kb-txt-up">UP</span></div>
<div class="be-kb-spacer"></div>
<div class="be-kb-key" data-be-kb="left" title="Left"><span id="be-kb-txt-left">LEFT</span></div>
<div class="be-kb-key" data-be-kb="down" title="Down"><span id="be-kb-txt-down">DOWN</span></div>
<div class="be-kb-key" data-be-kb="right" title="Right"><span id="be-kb-txt-right">RIGHT</span></div>
</div>
<div class="be-kb-strip" aria-hidden="true">
<span class="be-kb-key be-kb-key-sm" data-be-kb="up" title="Up"><span>U</span></span>
<span class="be-kb-key be-kb-key-sm" data-be-kb="left" title="Left"><span>L</span></span>
<span class="be-kb-key be-kb-key-sm" data-be-kb="down" title="Down"><span>D</span></span>
<span class="be-kb-key be-kb-key-sm" data-be-kb="right" title="Right"><span>R</span></span>
<span class="be-kb-key be-kb-key-sm" data-be-kb="heavy" title="Heavy"><span>H</span></span>
<span class="be-kb-key be-kb-key-sm" data-be-kb="special" title="Special"><span>S</span></span>
</div>
<div class="be-kb-actions">
<div class="be-kb-action be-kb-action-wideonly">
<span class="be-kb-key be-kb-key-wide" data-be-kb="heavy" title="Heavy"><span id="be-kb-txt-heavy">HEAVY</span></span>
</div>
<div class="be-kb-action be-kb-action-wideonly">
<span class="be-kb-key be-kb-key-wide" data-be-kb="special" title="Special"><span id="be-kb-txt-special">SPECIAL</span></span>
</div>
</div>
</div>
</div>`;
document.body.appendChild(wrap);
beInitKeybindOverlayDrag(wrap);
bindBonkKeybindOverlayListeners();
applyKeybindOverlayVisibility();
refreshBonkKeybindOverlayFromGame();
}
function applySizePreset(size) {
if (!UI.root || !UI.card) return;
uiSettings.size = ["tiny", "medium", "large"].includes(size) ? size : "medium";
UI.card.style.width = "";
UI.card.style.height = "";
UI.card.style.maxHeight = "";
saveUISettings();
applyOverlaySettings();
}
function beSnapshotUiPresetData() {
let pos = null;
try {
pos = JSON.parse(localStorage.getItem("bonk_ui_pos") || "null");
} catch {
pos = null;
}
let kb = null;
try {
kb = JSON.parse(localStorage.getItem(BE_KB_OVERLAY_POS_KEY) || "null");
} catch {
kb = null;
}
const card = UI.card;
return {
v: 1,
theme: uiSettings.theme,
size: uiSettings.size,
graphWeek: uiSettings.graphWeek,
overviewSlots: JSON.parse(JSON.stringify(uiSettings.overviewSlots)),
dashboardStats: JSON.parse(JSON.stringify(uiSettings.dashboardStats)),
overlayHotkey: uiSettings.overlayHotkey,
widgetHotkey: uiSettings.widgetHotkey,
keybindOverlayEnabled: uiSettings.keybindOverlayEnabled,
keybindOverlayOpacity: uiSettings.keybindOverlayOpacity,
keybindOverlayScale: uiSettings.keybindOverlayScale,
keybindOverlayCompact: uiSettings.keybindOverlayCompact,
trackerPos: pos,
cardW: card ? card.style.width || "" : "",
cardH: card ? card.style.height || "" : "",
keybindOverlayPos: kb
};
}
function beApplyUiPresetData(data) {
if (!data || data.v !== 1) return false;
uiSettings.theme = data.theme === "light" ? "light" : "dark";
uiSettings.size = ["tiny", "medium", "large"].includes(data.size) ? data.size : "medium";
uiSettings.graphWeek = typeof data.graphWeek === "string" ? data.graphWeek : "";
if (data.overviewSlots && typeof data.overviewSlots === "object") {
uiSettings.overviewSlots = {
tiny: Array.isArray(data.overviewSlots.tiny) ? data.overviewSlots.tiny.slice(0, 3) : uiSettings.overviewSlots.tiny,
medium: Array.isArray(data.overviewSlots.medium) ? data.overviewSlots.medium.slice(0, 4) : uiSettings.overviewSlots.medium,
large: Array.isArray(data.overviewSlots.large) ? data.overviewSlots.large.slice(0, 6) : uiSettings.overviewSlots.large
};
}
if (data.dashboardStats && typeof data.dashboardStats === "object") {
uiSettings.dashboardStats = {
xp: data.dashboardStats.xp !== false,
wins: data.dashboardStats.wins !== false,
rate: data.dashboardStats.rate !== false,
lastWin: data.dashboardStats.lastWin !== false
};
}
if (typeof data.overlayHotkey === "string" && data.overlayHotkey) uiSettings.overlayHotkey = data.overlayHotkey;
if (typeof data.widgetHotkey === "string" && data.widgetHotkey) uiSettings.widgetHotkey = data.widgetHotkey;
uiSettings.keybindOverlayEnabled = data.keybindOverlayEnabled === true;
if (typeof data.keybindOverlayOpacity === "number") {
uiSettings.keybindOverlayOpacity = Math.min(1, Math.max(0.35, data.keybindOverlayOpacity));
}
if (typeof data.keybindOverlayScale === "number") {
uiSettings.keybindOverlayScale = Math.min(1.35, Math.max(0.65, data.keybindOverlayScale));
}
uiSettings.keybindOverlayCompact = data.keybindOverlayCompact === true;
saveUISettings();
const ts = document.getElementById("beThemeSelect");
const ss = document.getElementById("beSizeSelect");
const gw = document.getElementById("beGraphWeekSelect");
if (ts) ts.value = uiSettings.theme;
if (ss) ss.value = uiSettings.size;
if (gw) gw.value = uiSettings.graphWeek || "";
const kbt = document.getElementById("beKeybindOverlayToggle");
if (kbt) kbt.checked = uiSettings.keybindOverlayEnabled;
const opR = document.getElementById("beKbOverlayOpacity");
if (opR) opR.value = String(Math.round((uiSettings.keybindOverlayOpacity ?? 1) * 100));
const scR = document.getElementById("beKbOverlayScale");
if (scR) scR.value = String(Math.round((uiSettings.keybindOverlayScale ?? 1) * 100));
const cpR = document.getElementById("beKbOverlayCompact");
if (cpR) cpR.checked = uiSettings.keybindOverlayCompact === true;
if (data.trackerPos && typeof data.trackerPos === "object" && UI.root) {
const left = data.trackerPos.left;
const top = data.trackerPos.top;
if (typeof left === "string" && typeof top === "string") {
UI.root.style.left = left;
UI.root.style.top = top;
UI.root.style.right = "auto";
try {
localStorage.setItem("bonk_ui_pos", JSON.stringify({ left, top }));
} catch {
// ignore
}
}
}
applyOverlaySettings();
if (UI.root) UI.root.dataset.size = uiSettings.size;
if (data.cardW && data.cardH && UI.card) {
UI.card.style.width = data.cardW;
UI.card.style.height = data.cardH;
}
if (data.keybindOverlayPos && typeof data.keybindOverlayPos === "object") {
try {
localStorage.setItem(BE_KB_OVERLAY_POS_KEY, JSON.stringify(data.keybindOverlayPos));
} catch {
// ignore
}
}
applyDashboardStatVisibility();
applyOverviewLayout();
syncOverviewSlotControls();
ensureBonkKeybindOverlayMounted();
return true;
}
function beRefreshPresetSelect() {
const sel = document.getElementById("bePresetSelect");
if (!sel) return;
const list = Array.isArray(uiSettings.uiPresets) ? uiSettings.uiPresets : [];
sel.innerHTML = "";
if (!list.length) {
const o = document.createElement("option");
o.value = "";
o.textContent = "(no presets)";
sel.appendChild(o);
return;
}
list.forEach((p) => {
const o = document.createElement("option");
o.value = p.id || "";
o.textContent = p.name || p.id || "Preset";
sel.appendChild(o);
});
}
function refreshScriptHealthPanel() {
const out = document.getElementById("beScriptHealthOut");
const lab = document.getElementById("beSelectorSchemaLabel");
if (lab) lab.textContent = BE_SELECTOR_SCHEMA;
if (!out) return;
let lsOk = true;
let lsDetail = "ok";
try {
localStorage.setItem("__be_ls_probe", "1");
localStorage.removeItem("__be_ls_probe");
} catch (e) {
lsOk = false;
lsDetail = e?.message || "blocked";
}
const frame = document.getElementById("maingameframe");
let cdOk = false;
let cdDetail = "null";
try {
const cd = beGetBonkGameDocument();
cdOk = !!cd;
cdDetail = cd ? "readable" : "null";
} catch (e) {
cdDetail = e?.message || "err";
}
const gdoc = beGetBonkGameDocument();
const rt = beFindRedefineControlsTable(gdoc);
const summary = {
schema: BE_SELECTOR_SCHEMA,
localStorage: { ok: lsOk, detail: lsDetail },
maingameframe: { ok: !!frame },
gameDocument: { ok: cdOk, detail: cdDetail },
redefineControlsTable: { ok: !!rt, detail: rt ? "found" : "missing" },
health: BE_HEALTH
};
try {
out.textContent = JSON.stringify(summary, null, 2);
} catch {
out.textContent = "(could not stringify health)";
}
}
function beExportStatsToCsv() {
const cache = Storage.load();
const data = cache?.data;
if (!data || typeof data !== "object") {
showDiagnosticsToast("No stats data to export.");
return;
}
const dayKeys = Object.keys(data)
.filter((k) => /^\d{4}-\d{2}-\d{2}$/.test(k))
.sort();
const rows = [["date", "xp", "wins", "synced"].join(",")];
dayKeys.forEach((k) => {
const d = data[k] || {};
const line = [
k,
d.xp != null ? d.xp : "",
d.wins != null ? d.wins : "",
d._synced ? "1" : "0"
].map((cell) => `"${String(cell).replace(/"/g, '""')}"`);
rows.push(line.join(","));
});
const sess = Array.isArray(data._sessions) ? data._sessions : [];
if (sess.length) {
rows.push("");
rows.push(["session_at_iso", "gained_wins", "duration_sec"].join(","));
sess.forEach((s) => {
rows.push(
[s.at, s.gainedWins != null ? s.gainedWins : "", s.durationSec != null ? s.durationSec : ""]
.map((cell) => `"${String(cell).replace(/"/g, '""')}"`)
.join(",")
);
});
}
const blob = new Blob([rows.join("\r\n")], { type: "text/csv;charset=utf-8" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `bonk-stats-${(Storage.getPlayerKey() || "player").replace(/[^\w\-]+/g, "_")}-${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
setTimeout(() => {
try {
URL.revokeObjectURL(a.href);
} catch {}
}, 4000);
}
function setOverlayVisibility(visible) {
if (!UI.root) return;
const topHint = document.getElementById("pretty_top_be_launcher");
if (visible) {
UI.root.style.display = "block";
requestAnimationFrame(() => UI.root.classList.add("be-overlay-open"));
if (UI.launcher) UI.launcher.style.display = "none";
if (topHint) topHint.style.setProperty("display", "none", "important");
return;
}
UI.root.classList.remove("be-overlay-open");
setTimeout(() => {
if (!UI.root.classList.contains("be-overlay-open")) {
UI.root.style.display = "none";
if (UI.launcher) UI.launcher.style.display = "none";
if (topHint) topHint.style.setProperty("display", "flex", "important");
}
}, 170);
}
function toggleOverlayVisibility() {
if (!UI.root) return;
const isVisible = UI.root.style.display !== "none";
setOverlayVisibility(!isVisible);
}
function setWidgetHubVisibility(visible) {
if (!UI.widgetHub) return;
UI.widgetHub.style.display = visible ? "flex" : "none";
UI.widgetHub.style.pointerEvents = visible ? "auto" : "none";
}
function openWidgetHub() {
setOverlayVisibility(false);
setWidgetHubVisibility(true);
}
function toggleWidgetHub() {
if (!UI.widgetHub) return;
const open = UI.widgetHub.style.display === "flex";
if (open) setWidgetHubVisibility(false);
else openWidgetHub();
}
window.BonkEnhanced = window.BonkEnhanced || {};
window.BonkEnhanced.diagnose = function () {
const snapshot = {
env: BE_CONFIG.env,
debug: BE_CONFIG.debug,
page: window.location.href,
health: BE_HEALTH
};
const rows = Object.entries(BE_HEALTH).map(([k, v]) => ({
check: k,
ok: v.ok,
detail: v.detail,
at: new Date(v.ts).toLocaleTimeString()
}));
console.groupCollapsed(`[BonkEnhanced] Diagnostics (${rows.length} checks)`);
console.table(rows);
console.log("Snapshot:", snapshot);
console.groupEnd();
return snapshot;
};
window.BonkEnhanced.setDebug = function (enabled) {
BE_CONFIG.debug = !!enabled;
localStorage.setItem("be_debug", enabled ? "1" : "0");
return BE_CONFIG.debug;
};
window.BonkEnhanced.setEnv = function (env) {
const next = env === "prod" ? "prod" : "test";
BE_CONFIG.env = next;
localStorage.setItem("be_env", next);
return BE_CONFIG.env;
};
window.BonkEnhanced.getConfig = function () {
return JSON.parse(JSON.stringify(BE_CONFIG));
};
/** v9: lowercase friend names for in-game nameText tint (Pixi injector reads this). */
window.BonkEnhanced.friendNamesLower = [];
window.BonkEnhanced.syncFriendsFromStorage = function () {
try {
const raw = localStorage.getItem("be_friends_v9");
const arr = raw ? JSON.parse(raw) : [];
window.BonkEnhanced.friendNamesLower = Array.isArray(arr)
? arr.map((s) => String(s || "").trim().toLowerCase()).filter(Boolean)
: [];
} catch {
window.BonkEnhanced.friendNamesLower = [];
}
return window.BonkEnhanced.friendNamesLower.slice();
};
window.BonkEnhanced.syncFriendsFromStorage();
/** Main bonk.io tab: pending login fields (short TTL). Game frame queues; host page fills form. */
const BE_AUTOFILL_PENDING_KEY = "be_autofill_login_v1";
const BE_AUTOFILL_TTL_MS = 120000;
function beMirrorAutofillPendingToBrowsingContexts(payload) {
const seen = new Set();
const write = (w) => {
if (!w || seen.has(w)) return;
seen.add(w);
try {
w.localStorage.setItem(BE_AUTOFILL_PENDING_KEY, payload);
} catch (e) {
BE_LOG.debug("beMirrorAutofillPendingToBrowsingContexts", e);
}
};
write(window);
try {
write(window.top);
} catch {}
try {
write(window.parent);
} catch {}
}
function beReadAutofillPendingRaw() {
const tryWin = (w) => {
try {
return w.localStorage.getItem(BE_AUTOFILL_PENDING_KEY);
} catch {
return null;
}
};
let v = tryWin(window) || tryWin(window.top) || tryWin(window.parent);
if (v) return v;
try {
if (window.opener) {
v = tryWin(window.opener) || (window.opener.top && tryWin(window.opener.top));
if (v) return v;
}
} catch {}
try {
const mem = window.top && window.top.__beAutofillPendingPayload;
if (typeof mem === "string" && mem.length > 0) return mem;
} catch {}
try {
const mem2 = window.__beAutofillPendingPayload;
if (typeof mem2 === "string" && mem2.length > 0) return mem2;
} catch {}
return null;
}
function beClearAutofillPendingEverywhere() {
const seen = new Set();
const clear = (w) => {
if (!w || seen.has(w)) return;
seen.add(w);
try {
w.localStorage.removeItem(BE_AUTOFILL_PENDING_KEY);
} catch {}
};
clear(window);
try {
clear(window.top);
} catch {}
try {
clear(window.parent);
} catch {}
try {
delete window.top.__beAutofillPendingPayload;
} catch {}
try {
delete window.__beAutofillPendingPayload;
} catch {}
}
function queueBonkLoginAutofill(username, password) {
try {
const payload = JSON.stringify({
v: 1,
username: String(username || ""),
password: String(password || ""),
exp: Date.now() + BE_AUTOFILL_TTL_MS
});
beMirrorAutofillPendingToBrowsingContexts(payload);
try {
window.top.__beAutofillPendingPayload = payload;
} catch {}
try {
window.__beAutofillPendingPayload = payload;
} catch {}
let ok = false;
try {
ok = localStorage.getItem(BE_AUTOFILL_PENDING_KEY) === payload;
} catch {}
if (!ok) {
try {
ok = window.top.localStorage.getItem(BE_AUTOFILL_PENDING_KEY) === payload;
} catch {}
}
if (!ok) {
BE_LOG.error("queueBonkLoginAutofill verify mismatch");
} else {
BE_LOG.debug("queueBonkLoginAutofill stored");
try {
if (typeof showDiagnosticsToast === "function") {
showDiagnosticsToast(
"Autofill queued — bottom-right toast. On Guest/Account screen, script will click Login or Register next; “Bonk login filled” only after that form is visible."
);
}
} catch (e) {
BE_LOG.debug("queue toast", e);
}
}
const peekNow = beReadAutofillPendingRaw();
return {
stored: ok,
note: "Queued toast = storage OK. “Bonk login filled” only when #loginwindow_* is visible and filled. On Guest/Account, consumer clicks LoR first — no fill toast until then.",
peekNow
};
} catch (e) {
BE_LOG.error("queueBonkLoginAutofill", e);
return { stored: false, error: String(e && e.message ? e.message : e), peekNow: null };
}
}
window.BonkEnhanced.queueBonkLoginAutofill = queueBonkLoginAutofill;
window.BonkEnhanced.peekAutofillQueue = function () {
return beReadAutofillPendingRaw();
};
/** Console: BonkEnhanced.probeAutofillStorage() — verify localStorage read/write works in this frame. */
window.BonkEnhanced.probeAutofillStorage = function () {
const key = "__be_storage_probe_v1";
try {
localStorage.setItem(key, "1");
const ok = localStorage.getItem(key) === "1";
localStorage.removeItem(key);
return { localStorageOk: ok, topSame: window.top === window, href: window.location.href };
} catch (e) {
return { localStorageOk: false, error: String(e && e.message ? e.message : e), href: window.location.href };
}
};
function beSetInputValue(el, value) {
if (!el) return;
try {
const proto = el.constructor?.prototype || HTMLInputElement.prototype;
const desc = Object.getOwnPropertyDescriptor(proto, "value");
if (desc && desc.set) desc.set.call(el, value);
else el.value = value;
} catch {
el.value = value;
}
el.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
}
/**
* Reduce fights with built-in autofill and extensions (LastPass, 1Password, Bitwarden, etc.).
* Browsers may still ignore hints; user can turn off saved passwords for bonk.io in browser settings.
*/
function beHardenBonkLoginFields(u, p) {
if (!u || !p) return;
try {
u.setAttribute("autocomplete", "off");
u.setAttribute("data-lpignore", "true");
u.setAttribute("data-1p-ignore", "true");
u.setAttribute("data-bwignore", "true");
u.setAttribute("data-form-type", "other");
p.setAttribute("autocomplete", "new-password");
p.setAttribute("data-lpignore", "true");
p.setAttribute("data-1p-ignore", "true");
p.setAttribute("data-bwignore", "true");
p.setAttribute("data-form-type", "other");
} catch (err) {
BE_LOG.debug("beHardenBonkLoginFields", err);
}
}
/** Focus the iframe element in the parent page so inner clicks can register (main bonk.io tab). */
function beFocusOwningIframe(el) {
if (!el || !el.ownerDocument) return;
const w = el.ownerDocument.defaultView;
if (!w || w === window) return;
try {
const fe = w.frameElement;
if (fe && typeof fe.focus === "function") fe.focus();
if (typeof w.focus === "function") w.focus();
} catch (e) {
BE_LOG.debug("beFocusOwningIframe", e);
}
}
/** Dispatch a more reliable click for Bonk’s div-based buttons (some builds ignore el.click()). */
function beSyntheticClick(el) {
if (!el) return;
const view =
el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : window;
try {
el.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
} catch {
try {
el.scrollIntoView(true);
} catch {}
}
beFocusOwningIframe(el);
try {
el.click();
} catch (e) {
BE_LOG.warn("beSyntheticClick native", e);
}
try {
el.click();
} catch {}
try {
el.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true, view }));
} catch {
try {
el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view }));
} catch {}
}
try {
el.dispatchEvent(new PointerEvent("pointerup", { bubbles: true, cancelable: true, view }));
} catch {
try {
el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view }));
} catch {}
}
try {
el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view }));
} catch {}
try {
el.click();
} catch (e) {
BE_LOG.warn("beSyntheticClick", e);
}
}
/** True if node looks painted and hittable; getComputedStyle uses the element’s document (iframe-safe). */
function beIsLikelyVisibleClickTarget(el) {
if (!el) return false;
try {
const dv = el.ownerDocument && el.ownerDocument.defaultView;
const g = dv && dv.getComputedStyle ? dv.getComputedStyle(el) : window.getComputedStyle(el);
if (!g || g.display === "none" || g.visibility === "hidden" || Number(g.opacity) === 0) return false;
const r = el.getBoundingClientRect();
return r.width >= 2 && r.height >= 2;
} catch {
return false;
}
}
/**
* “Login or Register” in #maingameframe — some builds ignore bare .click(); add coords, hit-test, and rAF settle.
*/
function beClickLoRStrategies(btn, cd) {
if (!btn || !cd) return;
const view = cd.defaultView || window;
try {
btn.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
} catch {
try {
btn.scrollIntoView(true);
} catch {}
}
beFocusOwningIframe(btn);
const fire = () => {
const r = btn.getBoundingClientRect();
const x = Math.round(r.left + r.width / 2);
const y = Math.round(r.top + r.height / 2);
if (r.width < 2 || r.height < 2) return;
try {
btn.click();
} catch {}
try {
btn.dispatchEvent(
new MouseEvent("mousedown", {
bubbles: true,
cancelable: true,
view,
clientX: x,
clientY: y,
button: 0,
buttons: 1,
})
);
btn.dispatchEvent(
new MouseEvent("mouseup", {
bubbles: true,
cancelable: true,
view,
clientX: x,
clientY: y,
button: 0,
buttons: 0,
})
);
btn.dispatchEvent(
new MouseEvent("click", {
bubbles: true,
cancelable: true,
view,
clientX: x,
clientY: y,
button: 0,
})
);
} catch (e) {
BE_LOG.debug("LoR coord MouseEvent", e);
}
try {
const hit = cd.elementFromPoint(x, y);
if (hit && typeof hit.click === "function") hit.click();
} catch (e) {
BE_LOG.debug("LoR elementFromPoint", e);
}
try {
btn.dispatchEvent(
new PointerEvent("pointerdown", {
bubbles: true,
cancelable: true,
view,
clientX: x,
clientY: y,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
})
);
btn.dispatchEvent(
new PointerEvent("pointerup", {
bubbles: true,
cancelable: true,
view,
clientX: x,
clientY: y,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
})
);
} catch {}
try {
beSyntheticClick(btn);
} catch {}
try {
btn.click();
} catch {}
};
try {
if (typeof view.requestAnimationFrame === "function") {
view.requestAnimationFrame(() => {
view.requestAnimationFrame(fire);
});
} else {
window.setTimeout(fire, 0);
}
} catch {
fire();
}
}
/** Depth-first walk of document / subtree including open shadow roots (Bonk may host menus in shadow DOM). */
function beWalkDeep(entry, visitor) {
const go = (node) => {
if (!node) return;
if (node.nodeType === 1) {
visitor(node);
const sr = node.shadowRoot;
if (sr) {
const kids = sr.childNodes;
for (let i = 0; i < kids.length; i++) {
const c = kids[i];
if (c && c.nodeType === 1) go(c);
}
}
const ch = node.children;
for (let j = 0; j < ch.length; j++) go(ch[j]);
} else if (node.nodeType === 9) {
if (node.documentElement) go(node.documentElement);
}
};
try {
if (entry && entry.nodeType === 9) go(entry);
else go(entry);
} catch (e) {
BE_LOG.debug("beWalkDeep", e);
}
}
function beDeepGetElementById(rootDoc, id) {
if (!rootDoc || !id) return null;
let found = null;
try {
if (rootDoc.getElementById) {
const n = rootDoc.getElementById(id);
if (n) return n;
}
} catch {}
beWalkDeep(rootDoc, (el) => {
if (found) return;
try {
if (el.id === id) found = el;
} catch {}
});
return found;
}
function beDeepQuerySelectorAll(rootDoc, selector) {
const out = [];
const seen = new Set();
beWalkDeep(rootDoc, (el) => {
try {
if (el.matches && el.matches(selector) && !seen.has(el)) {
seen.add(el);
out.push(el);
}
} catch {}
});
return out;
}
/** If the Account panel is closed, click the in-game main-menu brown “Account” button (not the start-screen green “Account” title). */
function tryOpenBonkAccountPanel(root) {
if (!root || !root.querySelector) return false;
if (beDeepGetElementById(root, "loginwindow_username")) return false;
const ac = beDeepGetElementById(root, "accountContainer");
if (ac) {
const st = window.getComputedStyle(ac);
const visible = ac.offsetParent !== null && st.display !== "none" && st.visibility !== "hidden" && st.opacity !== "0";
if (visible) return false;
}
const hits = beDeepQuerySelectorAll(root, ".brownButton, div[class*='brownButton'], div[class*='brown']");
for (let i = 0; i < hits.length; i++) {
const el = hits[i];
const t = (el.textContent || "").replace(/\s+/g, " ").trim();
if (/^account$/i.test(t)) {
try {
beSyntheticClick(el);
return true;
} catch (e) {
BE_LOG.warn("autofill Account click", e);
}
}
}
return false;
}
/**
* Account screen shows “Login or Register” first; the Log In fields appear after click.
* If #loginwindow_username is missing, find that control and click (same DOM position each build).
*/
function tryClickBonkLoginOrRegister(root) {
if (!root || !root.querySelector) return false;
if (beDeepGetElementById(root, "loginwindow_username")) return false;
const tryActivate = (el) => {
if (!el) return false;
try {
beSyntheticClick(el);
return true;
} catch (e) {
BE_LOG.warn("autofill Login or Register click", e);
return false;
}
};
const byStableId = beDeepGetElementById(root, "guestOrAccountContainer_accountButton");
if (byStableId && beIsLikelyVisibleClickTarget(byStableId) && tryActivate(byStableId)) return true;
const candidates = beDeepQuerySelectorAll(
root,
"div.brownButton, div[class*='brownButton'], div[class*='brown'], div[class*='Brown'], button, [role='button'], a"
);
for (let i = 0; i < candidates.length; i++) {
const el = candidates[i];
const t = (el.textContent || "").replace(/\s+/g, " ").trim();
if (!t || t.length > 96) continue;
if (/login\s*or\s*register/i.test(t)) {
if (tryActivate(el)) return true;
}
}
const exactHits = [];
beWalkDeep(root, (el) => {
try {
const t = (el.innerText || el.textContent || "").replace(/\s+/g, " ").trim();
if (!/^login\s*or\s*register$/i.test(t)) return;
const r = el.getBoundingClientRect();
if (r.width < 2 || r.height < 2) return;
exactHits.push({ el, area: r.width * r.height });
} catch {}
});
exactHits.sort((a, b) => a.area - b.area);
for (let j = 0; j < exactHits.length; j++) {
if (tryActivate(exactHits[j].el)) return true;
}
return false;
}
/** Bonk game UI (main menu) — IDs from DevTools. Submit is a div, not <button>. */
function beIsLikelyVisibleInput(el) {
if (!el) return false;
try {
if (el.disabled || el.getAttribute("aria-hidden") === "true") return false;
const r = el.getBoundingClientRect();
if (r.width < 3 || r.height < 3) return false;
const st = window.getComputedStyle(el);
if (st.display === "none" || st.visibility === "hidden" || Number(st.opacity) === 0) return false;
return true;
} catch {
return false;
}
}
function tryBonkLoginWindowFill(root, username, password) {
if (!root || !root.querySelector) return false;
const u = beDeepGetElementById(root, "loginwindow_username");
const p = beDeepGetElementById(root, "loginwindow_password");
const btn = beDeepGetElementById(root, "loginwindow_submitbutton");
if (!u || !p) return false;
if (!beIsLikelyVisibleInput(u) || !beIsLikelyVisibleInput(p)) return false;
beHardenBonkLoginFields(u, p);
beSetInputValue(u, username);
beSetInputValue(p, password);
if (btn) {
window.setTimeout(() => {
try {
btn.click();
} catch (err) {
BE_LOG.warn("autofill loginwindow_submitbutton", err);
}
}, 180);
}
showDiagnosticsToast("Bonk login filled");
return true;
}
function getBonkLoginRootDocuments() {
const out = [];
const seen = new Set();
const addDoc = (d) => {
if (!d || seen.has(d)) return;
try {
if (d.documentElement) {
seen.add(d);
out.push(d);
}
} catch {}
};
const collectIframes = (doc) => {
if (!doc || !doc.querySelectorAll) return;
let list;
try {
list = doc.querySelectorAll("iframe, frame");
} catch {
return;
}
for (let i = 0; i < list.length; i++) {
try {
const cd = list[i].contentDocument;
if (cd) {
addDoc(cd);
collectIframes(cd);
}
} catch {}
}
};
try {
if (window.location.href.includes("gameframe-release.html")) {
addDoc(document);
return out;
}
addDoc(document);
collectIframes(document);
const topDoc = document;
const rest = out.filter((d) => d && d !== topDoc);
if (rest.length) {
return [...rest, topDoc];
}
} catch (e) {
BE_LOG.debug("autofill: getBonkLoginRootDocuments", e);
}
return out;
}
/** Disabled: generic password heuristic matched stray/hidden inputs and cleared the queue. Bonk login uses #loginwindow_* via tryBonkLoginWindowFill only. */
function fillBonkLoginFormHeuristic(root, username, password) {
void root;
void username;
void password;
return false;
}
function fillBonkLoginFormAnywhere(username, password) {
if (!window.location.href.includes("gameframe-release.html") && window === window.top) {
try {
const f = document.getElementById("maingameframe");
const cd = f && f.contentDocument;
if (cd && !cd.getElementById("loginwindow_username")) {
const btn = cd.getElementById("guestOrAccountContainer_accountButton");
if (btn && beIsLikelyVisibleClickTarget(btn)) {
try {
f.contentWindow.focus();
} catch {}
try {
f.focus();
} catch {}
beClickLoRStrategies(btn, cd);
const retry = (delay) => {
window.setTimeout(() => {
try {
if (cd.getElementById("loginwindow_username")) return;
const b = cd.getElementById("guestOrAccountContainer_accountButton");
if (b && beIsLikelyVisibleClickTarget(b)) beClickLoRStrategies(b, cd);
} catch {}
}, delay);
};
retry(130);
retry(420);
retry(1100);
return false;
}
}
} catch (e) {
BE_LOG.debug("fillBonkLoginFormAnywhere top+maingameframe", e);
}
}
const roots = getBonkLoginRootDocuments();
let clickedOpener = false;
for (let k = 0; k < roots.length; k++) {
if (tryClickBonkLoginOrRegister(roots[k])) clickedOpener = true;
}
if (!clickedOpener) {
for (let a = 0; a < roots.length; a++) {
if (tryOpenBonkAccountPanel(roots[a])) clickedOpener = true;
}
}
if (clickedOpener) return false;
for (let i = 0; i < roots.length; i++) {
if (tryBonkLoginWindowFill(roots[i], username, password)) return true;
}
return false;
}
function tickBonkMainLoginAutofill() {
let raw = null;
try {
raw = beReadAutofillPendingRaw();
} catch {
return;
}
if (!raw) {
window.__beAutofillBurst = 0;
return;
}
let o;
try {
o = JSON.parse(raw);
} catch {
beClearAutofillPendingEverywhere();
window.__beAutofillBurst = 0;
return;
}
if (!o || o.v !== 1 || !o.username) {
beClearAutofillPendingEverywhere();
window.__beAutofillBurst = 0;
return;
}
if (o.exp < Date.now()) {
beClearAutofillPendingEverywhere();
window.__beAutofillBurst = 0;
return;
}
if (fillBonkLoginFormAnywhere(o.username, o.password)) {
beClearAutofillPendingEverywhere();
window.__beAutofillBurst = 0;
} else {
const b = (window.__beAutofillBurst = (window.__beAutofillBurst || 0) + 1);
if (b < 80) {
window.setTimeout(() => safeRun("autofill.burst", tickBonkMainLoginAutofill), 90);
}
}
}
function initBonkMainLoginAutofillConsumer() {
if (window.__beAutofillMainPoll) return;
const autofillMs = () => (beTabHidden() ? 1100 : 200);
window.__beAutofillMainPoll = window.setInterval(() => {
safeRun("autofill.tick", tickBonkMainLoginAutofill);
}, autofillMs());
if (!window.__beAutofillVisHooked) {
window.__beAutofillVisHooked = true;
document.addEventListener("visibilitychange", () => {
if (!window.__beAutofillMainPoll) return;
try {
clearInterval(window.__beAutofillMainPoll);
} catch {}
window.__beAutofillMainPoll = window.setInterval(() => {
safeRun("autofill.tick", tickBonkMainLoginAutofill);
}, autofillMs());
});
}
let moThrottleAt = 0;
const moTick = () => {
const now = Date.now();
if (now - moThrottleAt < 140) return;
moThrottleAt = now;
safeRun("autofill.domMut", tickBonkMainLoginAutofill);
};
const attachDomObserver = (root) => {
if (!root || root.__beAutofillDomObs) return;
root.__beAutofillDomObs = true;
try {
const mo = new MutationObserver(moTick);
mo.observe(root, { childList: true, subtree: true });
} catch (e) {
BE_LOG.debug("autofill dom observer", e);
}
};
attachDomObserver(document.documentElement);
const hookIframe = () => {
const frame = document.getElementById("maingameframe");
if (!frame || frame._beAutofillIframeHooked) return;
frame._beAutofillIframeHooked = true;
const onIframeReady = () => {
safeRun("autofill.iframeLoad", tickBonkMainLoginAutofill);
try {
const cd = frame.contentDocument;
if (cd && cd.documentElement) attachDomObserver(cd.documentElement);
} catch (e) {
BE_LOG.debug("autofill iframe observe", e);
}
};
frame.addEventListener("load", onIframeReady);
try {
if (frame.contentDocument && frame.contentDocument.readyState === "complete") {
window.setTimeout(() => safeRun("autofill.iframeAlreadyLoaded", onIframeReady), 0);
}
} catch (e) {
BE_LOG.debug("autofill iframe ready check", e);
}
};
hookIframe();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", hookIframe, { once: true });
}
let hookScan = 0;
const iframeScanMs = () => (beTabHidden() ? 2000 : 500);
window.__beAutofillIframeScan = window.setInterval(() => {
hookIframe();
if (++hookScan > 60) {
try {
clearInterval(window.__beAutofillIframeScan);
} catch {}
window.__beAutofillIframeScan = null;
}
}, iframeScanMs());
document.addEventListener("visibilitychange", () => {
if (!window.__beAutofillIframeScan || hookScan > 60) return;
try {
clearInterval(window.__beAutofillIframeScan);
} catch {}
window.__beAutofillIframeScan = window.setInterval(() => {
hookIframe();
if (++hookScan > 60) {
try {
clearInterval(window.__beAutofillIframeScan);
} catch {}
window.__beAutofillIframeScan = null;
}
}, iframeScanMs());
});
try {
window.addEventListener("storage", (ev) => {
if (ev.key === BE_AUTOFILL_PENDING_KEY) safeRun("autofill.storage", tickBonkMainLoginAutofill);
});
} catch (e) {
BE_LOG.debug("autofill storage listener", e);
}
tickBonkMainLoginAutofill();
}
/** Start login autofill as soon as DOM is interactive — waiting only for `load` delays fill behind ads/assets. */
function scheduleBonkMainLoginAutofillBootstrap(healthKey) {
const go = () => safeRun(healthKey, initBonkMainLoginAutofillConsumer);
if (document.readyState !== "loading") go();
else document.addEventListener("DOMContentLoaded", go, { once: true });
window.addEventListener("load", go);
}
window.BonkEnhanced.tickLoginAutofill = tickBonkMainLoginAutofill;
window.BonkEnhanced.tickLoginAutoFill = tickBonkMainLoginAutofill;
function showDiagnosticsToast(message) {
const existing = document.getElementById("beDiagToast");
if (existing) existing.remove();
const toast = document.createElement("div");
toast.id = "beDiagToast";
toast.textContent = message;
toast.style.cssText = [
"position:fixed",
"right:16px",
"bottom:16px",
"z-index:2147483647",
"padding:8px 10px",
"border-radius:8px",
"font:12px 'Segoe UI',sans-serif",
"background:rgba(15,22,36,0.92)",
"color:#9cffb0",
"border:1px solid rgba(76,175,80,0.5)",
"box-shadow:0 6px 22px rgba(0,0,0,0.35)",
"opacity:0",
"transform:translateY(6px)",
"transition:opacity 0.2s ease, transform 0.2s ease",
"pointer-events:none"
].join(";");
document.body.appendChild(toast);
requestAnimationFrame(() => {
toast.style.opacity = "1";
toast.style.transform = "translateY(0)";
});
setTimeout(() => {
toast.style.opacity = "0";
toast.style.transform = "translateY(6px)";
setTimeout(() => toast.remove(), 220);
}, 1400);
}
function registerDiagnosticsHotkey() {
if (window.__beDiagHotkeyBound) return;
window.__beDiagHotkeyBound = true;
let lastEndPressAt = 0;
function buildCompactDiagnosticsReport(snapshot) {
const health = snapshot?.health || {};
const entries = Object.entries(health);
const okCount = entries.filter(([, v]) => !!v.ok).length;
const failEntries = entries.filter(([, v]) => !v.ok);
const failList = failEntries.length
? failEntries.map(([k, v]) => `${k}=${v?.detail || "failed"}`).join(" | ")
: "none";
return [
`BonkEnhanced Diagnostics`,
`env=${snapshot?.env || "unknown"} debug=${snapshot?.debug ? "1" : "0"}`,
`page=${snapshot?.page || window.location.href}`,
`checks=${entries.length} ok=${okCount} failed=${entries.length - okCount}`,
`failed_list=${failList}`
].join("\n");
}
async function copyDiagnosticsReport(snapshot) {
const text = buildCompactDiagnosticsReport(snapshot);
await navigator.clipboard.writeText(text);
return text;
}
document.addEventListener("keydown", (ev) => {
if (ev.key !== "End") return;
if (ev.repeat) return;
const target = ev.target;
const tag = target?.tagName?.toLowerCase?.() || "";
const isEditable = tag === "input" || tag === "textarea" || target?.isContentEditable;
if (isEditable) return;
ev.preventDefault();
const report = window.BonkEnhanced.diagnose();
const now = Date.now();
const isDoublePress = now - lastEndPressAt <= 2000;
lastEndPressAt = now;
if (isDoublePress) {
copyDiagnosticsReport(report)
.then(() => {
BE_LOG.info("Diagnostics report copied to clipboard");
showDiagnosticsToast("Diagnostics copied to clipboard");
})
.catch((err) => {
BE_LOG.warn("Clipboard copy failed", err);
showDiagnosticsToast("Copy failed (clipboard blocked)");
});
} else {
BE_LOG.info("Diagnostics snapshot:", report);
showDiagnosticsToast("Diagnostics logged (press End again to copy)");
}
}, true);
}
registerDiagnosticsHotkey();
function initOuterCustomMods() {
const frame = document.getElementById("maingameframe");
if (!frame) {
markHealth("outer.frame", false, "maingameframe not found");
return;
}
markHealth("outer.frame", true, "maingameframe found");
const cw = frame.contentWindow;
const cd = frame.contentDocument;
if (!cw || !cd) {
markHealth("outer.frameContext", false, "contentWindow/contentDocument missing");
return;
}
markHealth("outer.frameContext", true, "frame context ready");
// Keep browser shortcuts like Ctrl/Shift/F-keys working (keyboard only).
// Applying this to all Event types broke mouse flows (e.g. room list) when Shift/Ctrl was held.
const originalPreventDefault = cw.Event.prototype.preventDefault;
const KB = cw.KeyboardEvent;
cw.Event.prototype.preventDefault = function () {
if (KB && this instanceof KB) {
const ev = this;
if (ev.ctrlKey || ev.shiftKey || (ev.key?.[0] === "F" && ev.key?.length > 1)) return;
}
originalPreventDefault.call(this);
};
markHealth("outer.preventDefaultPatch", true, "patched");
const hostStyle = `
<style>
body { overflow: hidden; }
#bonkioheader { display: none; }
#adboxverticalleftCurse { display: none; }
#adboxverticalCurse { display: none; }
#descriptioncontainer { display: none; }
#maingameframe { margin: 0 !important; }
</style>`;
document.head.insertAdjacentHTML("beforeend", hostStyle);
markHealth("outer.hostStyle", true, "inserted");
const setup = () => {
if (!cd.getElementById("roomlisttopbar")) {
markHealth("outer.roomTopbar", false, "roomlisttopbar not ready");
return;
}
markHealth("outer.roomTopbar", true, "roomlisttopbar ready");
try {
if (cd._beRoomSearchDedupeObs) {
cd._beRoomSearchDedupeObs.disconnect();
cd._beRoomSearchDedupeObs = null;
}
} catch {
// ignore
}
/* Re-run when room list is recreated (e.g. opening Custom Games). Skip if our bar already attached. */
if (cd.getElementById("beRoomFilterBar")) {
return;
}
if (!cd.getElementById("beRoomSearchPlaceholderHack")) {
const placeholderStyler = `
<style id="beRoomSearchPlaceholderHack">
[contenteditable=true]:empty:before {
content: attr(placeholder);
pointer-events: none;
display: block;
color: #757575;
}
</style>`;
cd.head.insertAdjacentHTML("beforeend", placeholderStyler);
markHealth("outer.placeholderStyle", true, "inserted");
}
const $ = cw.$;
if (!$) {
markHealth("outer.jquery", false, "jQuery unavailable in frame");
return;
}
markHealth("outer.jquery", true, "available");
const ROOM_SETTINGS_KEY = "be_room_settings_v8";
function loadRoomSettings() {
try {
const raw = localStorage.getItem(ROOM_SETTINGS_KEY);
if (!raw) return {};
const o = JSON.parse(raw);
return {
hideSolo: !!o.hideSolo,
minPlayers: o.minPlayers === "" || o.minPlayers == null ? "" : String(o.minPlayers),
maxPlayers: o.maxPlayers === "" || o.maxPlayers == null ? "" : String(o.maxPlayers),
favoritesOnly: !!o.favoritesOnly,
favorites: Array.isArray(o.favorites) ? o.favorites.map(String) : [],
hidePassworded: !!o.hidePassworded
};
} catch {
return {};
}
}
let roomSettings = {
hideSolo: false,
minPlayers: "",
maxPlayers: "",
favoritesOnly: false,
favorites: [],
hidePassworded: false,
...loadRoomSettings()
};
function saveRoomSettings() {
try {
localStorage.setItem(ROOM_SETTINGS_KEY, JSON.stringify(roomSettings));
} catch {}
}
function getRoomNameFromRow(el) {
return (el.children?.[0]?.textContent || "").trim();
}
/** First "cur / max" pair in row (Bonk room list player counts). */
function getPlayerFractionFromRow(el) {
const text = el.textContent || "";
const m = /\b(\d+)\s*\/\s*(\d+)\b/.exec(text);
if (!m) return null;
return { cur: +m[1], max: +m[2] };
}
function isFavoriteName(name) {
const n = (name || "").toLowerCase();
return roomSettings.favorites.some((f) => (f || "").toLowerCase() === n);
}
function isRoomDataRow(el) {
return el && el.closest && !el.closest("thead");
}
/** Bonk Enhanced uses #beRoomSearchInputBox; other userscripts (e.g. Bonk.io Custom Mods) use #roomSearchInputBox — never two boxes. */
function getRoomSearchInputEl() {
return cd.getElementById("beRoomSearchInputBox") || cd.getElementById("roomSearchInputBox");
}
function beRemoveDuplicateLegacyRoomSearch() {
if (!cd.getElementById("beRoomSearchInputBox")) return;
const bar = cd.getElementById("roomlisttopbar");
if (!bar) return;
try {
bar.querySelectorAll("#roomSearchInputBox").forEach((n) => {
n.remove();
});
} catch {
// ignore
}
}
function beAttachRoomSearchDedupeObserver() {
if (cd._beRoomSearchDedupeObs) {
try {
cd._beRoomSearchDedupeObs.disconnect();
} catch {
// ignore
}
cd._beRoomSearchDedupeObs = null;
}
const bar = cd.getElementById("roomlisttopbar");
if (!bar || !cd.getElementById("beRoomSearchInputBox")) return;
cd._beRoomSearchDedupeObs = new MutationObserver(() => {
beRemoveDuplicateLegacyRoomSearch();
});
cd._beRoomSearchDedupeObs.observe(bar, { childList: true });
}
/** Bonk password column is 4th td (lock icon when passworded). */
function isPasswordProtectedRow(el) {
const pwCell = el.children?.[3];
if (!pwCell) return false;
return !!pwCell.querySelector("img[src*=\"lock\"], img[src*=\"Lock\"]");
}
function markFavoriteRows() {
$("#roomlisttable tr").each((_, el) => {
if (!isRoomDataRow(el)) return;
const name = getRoomNameFromRow(el);
if (!name) return;
el.classList.toggle("be-room-fav", isFavoriteName(name));
});
}
function applyAllFilters() {
const qel = getRoomSearchInputEl();
const s = ((qel?.textContent || "") + "").toLowerCase();
const minP = roomSettings.minPlayers === "" ? null : Number(roomSettings.minPlayers);
const maxP = roomSettings.maxPlayers === "" ? null : Number(roomSettings.maxPlayers);
const hideSolo = !!roomSettings.hideSolo;
const favOnly = !!roomSettings.favoritesOnly;
const barPwEl = cd.getElementById("beHidePasswordedBar");
const nativePwEl = cd.getElementById("roomlisthidepasswordedcheckbox");
let hidePassworded = false;
if (barPwEl) hidePassworded = !!barPwEl.checked;
else if (nativePwEl) hidePassworded = !!nativePwEl.checked;
else hidePassworded = !!roomSettings.hidePassworded;
$("#roomlisttable tr").each((_, el) => {
if (!isRoomDataRow(el)) return;
const roomName = (el.children?.[0]?.textContent || "").toLowerCase();
if (!roomName.includes(s)) {
el.hidden = true;
return;
}
const frac = getPlayerFractionFromRow(el);
if (hideSolo && frac && frac.cur === 1 && frac.max === 1) {
el.hidden = true;
return;
}
if (minP != null && Number.isFinite(minP) && frac && frac.cur < minP) {
el.hidden = true;
return;
}
if (maxP != null && Number.isFinite(maxP) && frac && frac.cur > maxP) {
el.hidden = true;
return;
}
const rawName = getRoomNameFromRow(el);
if (favOnly && !isFavoriteName(rawName)) {
el.hidden = true;
return;
}
if (hidePassworded && isPasswordProtectedRow(el)) {
el.hidden = true;
return;
}
el.hidden = false;
});
}
if (!cd.getElementById("beRoomSearchInputBox") && !cd.getElementById("roomSearchInputBox")) {
const inputHtml = `<span contentEditable="true" id="beRoomSearchInputBox" placeholder="Search Rooms.." style="
float:right;
padding:2px 8px;
margin:5px 20px;
border:2px solid #006157;
border-radius:5px;
font:large futurept_b1;
width:20%;
background:white;
color:#111;
caret-color:#111;
text-shadow:none;
"></span>`;
$("#roomlisttopbar").append(inputHtml);
beRemoveDuplicateLegacyRoomSearch();
beAttachRoomSearchDedupeObserver();
markHealth("outer.roomSearch", true, "injected-beRoomSearchInputBox");
} else if (cd.getElementById("beRoomSearchInputBox")) {
beRemoveDuplicateLegacyRoomSearch();
beAttachRoomSearchDedupeObserver();
markHealth("outer.roomSearch", true, "already-beRoomSearchInputBox");
} else {
markHealth("outer.roomSearch", true, "compat-roomSearchInputBox");
}
const filterBarHtml = `
<div id="beRoomFilterBar" style="clear:both;width:100%;padding:4px 6px;box-sizing:border-box;font:11px Arial,sans-serif;display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;background:rgba(0,20,30,0.35);border-bottom:1px solid rgba(0,97,87,0.35);color:#e8f4ff;">
<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;user-select:none;"><input type="checkbox" id="beHidePasswordedBar"/> Hide passworded</label>
<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;user-select:none;"><input type="checkbox" id="beHideSolo"/> Hide 1/1</label>
<label style="display:inline-flex;align-items:center;gap:4px;">Min <input type="number" id="beMinPl" min="0" max="99" placeholder="—" style="width:38px;padding:1px 4px;border-radius:4px;border:1px solid #006157;"/></label>
<label style="display:inline-flex;align-items:center;gap:4px;">Max <input type="number" id="beMaxPl" min="0" max="99" placeholder="—" style="width:38px;padding:1px 4px;border-radius:4px;border:1px solid #006157;"/></label>
<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;user-select:none;"><input type="checkbox" id="beFavOnly"/> Favorites only</label>
<span style="opacity:0.75;font-size:10px;">Alt+click row: favorite</span>
</div>`;
if (!cd.getElementById("beRoomFilterBar")) {
$("#roomlisttopbar").after(filterBarHtml);
}
const nativePwLabel = cd.getElementById("roomlisthidepasswordedcheckbox")?.closest("label.control-checkbox");
if (nativePwLabel) {
nativePwLabel.style.setProperty("display", "none", "important");
}
if (!cd.getElementById("beRoomFilterStyles")) {
const roomFilterStyles = `
<style id="beRoomFilterStyles">
#roomlisttable tr.be-room-fav td { background: rgba(255, 180, 60, 0.12) !important; }
</style>`;
cd.head.insertAdjacentHTML("beforeend", roomFilterStyles);
}
const hidePwBarEl = cd.getElementById("beHidePasswordedBar");
const nativePwForInit = cd.getElementById("roomlisthidepasswordedcheckbox");
if (hidePwBarEl) {
if (nativePwForInit) hidePwBarEl.checked = !!nativePwForInit.checked;
else hidePwBarEl.checked = !!roomSettings.hidePassworded;
}
const hideSoloEl = cd.getElementById("beHideSolo");
const minPlEl = cd.getElementById("beMinPl");
const maxPlEl = cd.getElementById("beMaxPl");
const favOnlyEl = cd.getElementById("beFavOnly");
if (hideSoloEl) hideSoloEl.checked = roomSettings.hideSolo;
if (minPlEl) minPlEl.value = roomSettings.minPlayers;
if (maxPlEl) maxPlEl.value = roomSettings.maxPlayers;
if (favOnlyEl) favOnlyEl.checked = roomSettings.favoritesOnly;
function bindRoomFilterControls() {
if (hideSoloEl) {
hideSoloEl.addEventListener("change", () => {
roomSettings.hideSolo = hideSoloEl.checked;
saveRoomSettings();
applyAllFilters();
});
}
const onNum = () => {
roomSettings.minPlayers = minPlEl?.value ?? "";
roomSettings.maxPlayers = maxPlEl?.value ?? "";
saveRoomSettings();
applyAllFilters();
};
if (minPlEl) minPlEl.addEventListener("input", onNum);
if (maxPlEl) maxPlEl.addEventListener("input", onNum);
if (favOnlyEl) {
favOnlyEl.addEventListener("change", () => {
roomSettings.favoritesOnly = favOnlyEl.checked;
saveRoomSettings();
applyAllFilters();
});
}
}
bindRoomFilterControls();
if (hidePwBarEl) {
hidePwBarEl.addEventListener("change", () => {
const nat = cd.getElementById("roomlisthidepasswordedcheckbox");
if (nat) {
nat.checked = hidePwBarEl.checked;
const win = cd.defaultView || cw;
try {
nat.dispatchEvent(new win.Event("change", { bubbles: true }));
} catch {
const ev = cd.createEvent("HTMLEvents");
ev.initEvent("change", true, false);
nat.dispatchEvent(ev);
}
}
roomSettings.hidePassworded = hidePwBarEl.checked;
saveRoomSettings();
applyAllFilters();
});
}
const nativeHidePw = cd.getElementById("roomlisthidepasswordedcheckbox");
if (nativeHidePw && !nativeHidePw.dataset.beBarSync) {
nativeHidePw.dataset.beBarSync = "1";
nativeHidePw.addEventListener("change", () => {
const bar = cd.getElementById("beHidePasswordedBar");
if (bar) bar.checked = nativeHidePw.checked;
applyAllFilters();
});
}
markHealth("outer.roomFilters", true, "injected");
$(cd.body).off(".beRoomEnhanced");
$(cd.body).on("click.beRoomEnhanced", "#roomlisttable tr", function (ev) {
if (!ev.altKey) return;
if (!isRoomDataRow(this)) return;
ev.preventDefault();
ev.stopPropagation();
const name = getRoomNameFromRow(this);
if (!name) return;
const low = name.toLowerCase();
const idx = roomSettings.favorites.findIndex((f) => (f || "").toLowerCase() === low);
if (idx >= 0) roomSettings.favorites.splice(idx, 1);
else roomSettings.favorites.push(name);
saveRoomSettings();
markFavoriteRows();
applyAllFilters();
});
$(cd.body).on("click.beRoomEnhanced", "#roomlistrefreshbutton", () => {
cw.setTimeout(() => {
markFavoriteRows();
applyAllFilters();
}, 400);
});
function debounceWithRAF(fn) {
let timeout;
return function (...args) {
if (timeout) cw.cancelAnimationFrame(timeout);
timeout = cw.requestAnimationFrame(() => fn.apply(this, args));
};
}
$(cd.body).on(
"keyup.beRoomEnhanced",
"#beRoomSearchInputBox, #roomSearchInputBox",
debounceWithRAF(() => {
applyAllFilters();
})
);
$(cd.body).on("keydown.beRoomEnhanced", debounceWithRAF((ev) => {
if (ev.altKey) {
if (ev.key === "q" && $("#roomListContainer")[0]?.offsetParent === null) {
$("#pretty_top_exit").click();
$("#leaveconfirmwindow_okbutton").click();
} else if (ev.key === "r") {
$("#roomlistrefreshbutton").click();
}
}
if (ev.key === "/") {
const rs = getRoomSearchInputEl();
if (rs) $(rs).focus();
}
}));
if (cd._beRoomListTableObs) {
try {
cd._beRoomListTableObs.disconnect();
} catch {}
cd._beRoomListTableObs = null;
}
if (cd._beRoomFilterDebounceTimer) {
try {
cw.clearTimeout(cd._beRoomFilterDebounceTimer);
} catch {}
cd._beRoomFilterDebounceTimer = null;
}
const roomTableEl = cd.getElementById("roomlisttable");
if (roomTableEl) {
const runRoomTableSync = () => {
markFavoriteRows();
applyAllFilters();
};
const scheduleRoomTableSync = () => {
if (cd._beRoomFilterDebounceTimer) cw.clearTimeout(cd._beRoomFilterDebounceTimer);
cd._beRoomFilterDebounceTimer = cw.setTimeout(() => {
cd._beRoomFilterDebounceTimer = null;
runRoomTableSync();
}, 48);
};
cd._beRoomListTableObs = new MutationObserver(() => {
scheduleRoomTableSync();
});
cd._beRoomListTableObs.observe(roomTableEl, { childList: true, subtree: true });
}
markFavoriteRows();
applyAllFilters();
markHealth("outer.bindings", true, "events attached");
};
let beRoomSetupTimer = null;
const scheduleRoomSetup = () => {
if (beRoomSetupTimer) cw.clearTimeout(beRoomSetupTimer);
beRoomSetupTimer = cw.setTimeout(() => {
beRoomSetupTimer = null;
if (!cd.getElementById("roomlisttopbar")) return;
if (cd.getElementById("beRoomFilterBar")) return;
safeRun("outer.setupRetry", setup);
}, 100);
};
const observer = new MutationObserver(() => scheduleRoomSetup());
observer.observe(cd, { childList: true, subtree: true });
markHealth("outer.observer", true, "active");
safeRun("outer.setupInitial", setup);
scheduleRoomSetup();
}
function beInjectDocStyle(doc, id, css) {
try {
if (!doc || doc.getElementById(id)) return;
const s = doc.createElement("style");
s.id = id;
s.textContent = css;
(doc.head || doc.documentElement).appendChild(s);
} catch (e) {
BE_LOG.debug("beInjectDocStyle", e);
}
}
function beFullscreenGatherRootDocs(parentDoc) {
const out = [];
const seen = new Set();
const push = (d) => {
if (!d || !d.body || seen.has(d)) return;
seen.add(d);
out.push(d);
};
push(document);
push(parentDoc);
try {
if (window.top && window.top.document) push(window.top.document);
} catch (e) {
void e;
}
return out;
}
/** Hide Bonk’s password-warning strip (copy can be split across nodes; banner may live on top window). */
function beFullscreenHidePasswordBanners(doc) {
const hidden = [];
if (!doc || !doc.body) return hidden;
/* Cheap probe: skip expensive XPath / querySelectorAll when the copy isn’t in this document. */
try {
const probe = (doc.body.textContent || "").slice(0, 120000).replace(/\s+/g, " ");
if (!/easy\s+to\s+guess/i.test(probe)) return hidden;
if (
!/risk\s+losing\s+your\s+account/i.test(probe) &&
!/change\s+it\s+in\s+settings/i.test(probe)
) {
return hidden;
}
} catch (eQuick) {
void eQuick;
}
function isPasswordStripText(t) {
const z = String(t || "")
.replace(/\s+/g, " ")
.trim();
if (z.length > 800) return false;
const hasGuess = /easy\s+to\s+guess|password\s+easy/i.test(z);
const hasFooter = /risk\s+losing\s+your\s+account|change\s+it\s+in\s+settings/i.test(z);
return hasGuess && hasFooter;
}
function hideEl(el) {
if (!el || el.nodeType !== 1 || el.dataset.beFsHidden === "1") return;
el.dataset.beFsHidden = "1";
el.dataset.bePasswordStripHidden = "1";
el.style.setProperty("display", "none", "important");
el.style.setProperty("visibility", "hidden", "important");
el.style.setProperty("pointer-events", "none", "important");
el.style.setProperty("max-height", "0", "important");
el.style.setProperty("overflow", "hidden", "important");
el.style.setProperty("margin", "0", "important");
el.style.setProperty("padding", "0", "important");
hidden.push(el);
}
try {
const candidates = [];
/* 1) XPath: subtree text can be split so string-value (.) matches even when no single text node does. */
try {
const xp =
"//*[contains(., 'password') and (contains(., 'guess') or contains(., 'Settings') or contains(., 'account'))]";
const xr = doc.evaluate(xp, doc.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0; i < xr.snapshotLength; i++) {
const el = xr.snapshotItem(i);
if (el && el.nodeType === 1) candidates.push(el);
}
} catch (e1) {
void e1;
}
/* 2) innerText pass (layout text; good when XPath isn’t available). */
const tags = "div,section,footer,aside,p,header,nav,span,li,a,button,label";
doc.querySelectorAll(tags).forEach((el) => {
const t = (el.innerText || "").replace(/\s+/g, " ").trim();
if (isPasswordStripText(t)) candidates.push(el);
});
const uniq = [];
const seen = new Set();
for (let u = 0; u < candidates.length; u++) {
const el = candidates[u];
if (!el || seen.has(el)) continue;
seen.add(el);
const t = (el.textContent || "").replace(/\s+/g, " ").trim();
if (!isPasswordStripText(t)) continue;
uniq.push(el);
}
/* Outermost nodes only (hide one wrapper, not every nested match). */
const outer = uniq.filter(
(a) => !uniq.some((b) => b !== a && b.contains(a))
);
for (let o = 0; o < outer.length; o++) hideEl(outer[o]);
} catch (e) {
BE_LOG.debug("beFullscreenHidePasswordBanners", e);
}
return hidden;
}
function beFullscreenRestoreHidden(els) {
if (!els || !els.length) return;
for (let i = 0; i < els.length; i++) {
const el = els[i];
try {
if (el && el.dataset && el.dataset.beFsHidden === "1") {
if (el.dataset.bePasswordStripHidden === "1") continue;
el.style.removeProperty("display");
delete el.dataset.beFsHidden;
}
} catch (e) {
void e;
}
}
}
/** Run on load and a few delayed passes so the strip stays hidden even when not in fullscreen. */
function beScheduleGlobalPasswordBannerHiding() {
let parentDoc = null;
try {
if (window.self !== window.top) parentDoc = window.parent.document;
} catch (e) {
parentDoc = null;
}
const sweep = () => {
try {
const docs = beFullscreenGatherRootDocs(parentDoc);
for (let d = 0; d < docs.length; d++) {
beFullscreenHidePasswordBanners(docs[d]);
}
} catch (e) {
BE_LOG.debug("beScheduleGlobalPasswordBannerHiding", e);
}
};
sweep();
[200, 1200, 4000, 8000].forEach((ms) => window.setTimeout(sweep, ms));
}
if (!isGameFrame) {
scheduleBonkMainLoginAutofillBootstrap("outer.autofillInit");
safeRun("outer.passwordBannerHide", () => {
const run = () => beScheduleGlobalPasswordBannerHiding();
if (document.body) run();
else document.addEventListener("DOMContentLoaded", run, { once: true });
});
if (BE_CONFIG.featureFlags.enableOuterMods) {
window.addEventListener("load", () => safeRun("outer.init", initOuterCustomMods));
} else {
markHealth("outer.init", false, "feature flag disabled");
}
return;
}
scheduleBonkMainLoginAutofillBootstrap("gameframe.autofillInit");
safeRun("gameframe.passwordBannerHide", () => {
const run = () => beScheduleGlobalPasswordBannerHiding();
if (document.body) run();
else document.addEventListener("DOMContentLoaded", run, { once: true });
});
const BE_FS_SCALE_CLASSES = [
"be-fs-scale-contain",
"be-fs-scale-contain-left",
"be-fs-scale-contain-right",
"be-fs-scale-wdb-left",
"be-fs-scale-wdb-right",
"be-fs-scale-cover",
"be-fs-scale-fill"
];
/** Stretch layout in fullscreen is required for scale modes (letterbox/cover/fill). The old checkbox is gone — always on so the settings dropdown always takes effect. */
function beFullscreenStretchWanted() {
return true;
}
/**
* With stretch on: how the canvas maps into the fullscreen box.
* contain = letterbox, full map, uniform scale, centered.
* contain-left / contain-right = same as contain but one pillar (asymmetric letterbox).
* wdb-left / wdb-right = contain + pin, scale(~1.03) + translateY(vh) so overflow clips less
* off the map top; not cover/fill.
* cover = no letterboxing, uniform scale, may crop.
* fill = no letterboxing, no crop, non-uniform scale (slight stretch) to match the monitor.
*/
function beFullscreenScaleMode() {
try {
const m = localStorage.getItem("be_fullscreen_scale_mode");
if (
m === "contain" ||
m === "contain-left" ||
m === "contain-right" ||
m === "wdb-left" ||
m === "wdb-right" ||
m === "cover" ||
m === "fill"
) {
return m;
}
return localStorage.getItem("be_fullscreen_fill_cover") === "1" ? "cover" : "contain";
} catch {
return "contain";
}
}
function beApplyFullscreenScaleBodyClasses(bodyEl) {
if (!bodyEl) return;
for (let i = 0; i < BE_FS_SCALE_CLASSES.length; i++) {
bodyEl.classList.remove(BE_FS_SCALE_CLASSES[i]);
}
bodyEl.classList.add(`be-fs-scale-${beFullscreenScaleMode()}`);
}
function beSyncFullscreenFitClasses() {
try {
if (!document.body.classList.contains("fullscreen")) return;
document.body.classList.toggle("be-fs-stretch", beFullscreenStretchWanted());
beApplyFullscreenScaleBodyClasses(document.body);
} catch (e) {
BE_LOG.debug("beSyncFullscreenFitClasses", e);
}
}
/**
* Excigma-style fullscreen: toggles body.fullscreen on the gameframe (and parent when embedded),
* with the control on Bonk’s native top bar — left of the theme / paint button when detectable.
*/
function beFindPrettyTopPaintAnchor(bar) {
const byId = [
"pretty_top_bonk_theme",
"pretty_top_theme",
"pretty_top_colormap",
"pretty_top_colours",
"pretty_top_colors"
];
for (let i = 0; i < byId.length; i++) {
const el = document.getElementById(byId[i]);
if (el && bar.contains(el)) return el;
}
const btns = bar.querySelectorAll(":scope > .pretty_top_button.niceborderleft[id]");
for (let j = 0; j < btns.length; j++) {
const id = (btns[j].id || "").toLowerCase();
if (/theme|colour|color|skin|paint|roller/.test(id)) return btns[j];
}
return null;
}
function beInjectPrettyTopClipRecordCss() {
beInjectDocStyle(
document,
"be-pretty-top-clip-record",
`
#pretty_top_clip_wrap {
position: absolute;
top: 0;
width: 58px;
height: 34px;
z-index: 100002;
}
#pretty_top_clip_record {
width: 58px;
height: 34px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 24 24'%3E%3Cpath d='M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z'/%3E%3C/svg%3E");
background-size: 24px 24px;
background-repeat: no-repeat;
background-position: center;
position: relative;
left: 0;
top: 0;
cursor: pointer;
}
#pretty_top_clip_record.be-recording {
box-shadow: inset 0 0 0 2px rgba(255, 72, 72, 0.95);
}
/* Parent stays “held” while pointer is over the submenu (same wrap:hover). */
#pretty_top_clip_wrap:hover #pretty_top_clip_record:not(.be-recording) {
filter: brightness(1.14);
}
#pretty_top_clip_wrap:hover #pretty_top_clip_record.be-recording {
filter: none;
}
#pretty_top_clip_menu {
display: none;
position: absolute;
left: 0;
top: calc(100% - 1px);
z-index: 100003;
margin: 0;
padding: 0;
width: 58px;
height: 34px;
background: transparent;
border: none;
box-shadow: none;
border-radius: 0;
pointer-events: auto;
}
#pretty_top_clip_wrap:hover #pretty_top_clip_menu {
display: block;
}
/* Same cell as native .pretty_top_button (58×34 including Bonk borders). */
#pretty_top_clip_last15.pretty_top_button {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 58px !important;
height: 34px !important;
min-width: 58px !important;
min-height: 34px !important;
max-width: 58px !important;
max-height: 34px !important;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
font: inherit;
color: #f5f5f5 !important;
background-color: rgba(38, 36, 34, 0.98) !important;
background-image: none !important;
}
#pretty_top_clip_last15.pretty_top_button:hover {
background-color: rgba(50, 48, 45, 0.99) !important;
filter: brightness(1.06);
}
#pretty_top_clip_last15 svg {
display: block;
pointer-events: none;
width: 46px;
height: 26px;
flex-shrink: 0;
}
`
);
}
const BE_CLIP_RING_MS = 15000;
const beClipRing = {
recorder: null,
chunks: [],
stream: null,
started: false,
ensureRetries: 0
};
function beTrimClipRingChunks() {
const now = Date.now();
while (beClipRing.chunks.length && now - beClipRing.chunks[0].t > BE_CLIP_RING_MS) {
beClipRing.chunks.shift();
}
}
function beStopClipRingBuffer() {
try {
if (beClipRing.recorder && beClipRing.recorder.state !== "inactive") {
beClipRing.recorder.stop();
}
} catch (e) {
void e;
}
beClipRing.recorder = null;
try {
if (beClipRing.stream) beClipRing.stream.getTracks().forEach((t) => t.stop());
} catch (e2) {
void e2;
}
beClipRing.stream = null;
beClipRing.chunks = [];
beClipRing.started = false;
}
function beStartClipRingBuffer() {
if (beClipRing.started) return;
const canvas = document.querySelector("#gamerenderer canvas");
if (!canvas || !window.MediaRecorder) return;
let stream;
try {
stream = canvas.captureStream(30);
} catch (e) {
return;
}
const rec = beCreateMediaRecorderPreferMp4(stream);
if (!rec) {
try {
stream.getTracks().forEach((t) => t.stop());
} catch (e2) {
void e2;
}
return;
}
beClipRing.chunks = [];
rec.ondataavailable = (ev) => {
if (ev.data && ev.data.size) {
beClipRing.chunks.push({ t: Date.now(), data: ev.data });
beTrimClipRingChunks();
}
};
try {
rec.start(400);
beClipRing.recorder = rec;
beClipRing.stream = stream;
beClipRing.started = true;
} catch (e6) {
BE_LOG.debug("clipRing.start", e6);
try {
stream.getTracks().forEach((t) => t.stop());
} catch (e7) {
void e7;
}
}
}
function beSaveClipLast15Seconds() {
beTrimClipRingChunks();
if (!beClipRing.chunks.length) {
window.alert(
"No buffered video yet — stay in a match for a few seconds so the rolling buffer can fill, then try again."
);
return;
}
const blobs = beClipRing.chunks.map((c) => c.data);
const mime =
(beClipRing.recorder && beClipRing.recorder.mimeType) ||
(beClipRing.chunks[0] && beClipRing.chunks[0].data && beClipRing.chunks[0].data.type) ||
"video/webm";
try {
const blob = new Blob(blobs, { type: mime });
const ext = beClipFileExtensionForMime(mime);
beDownloadBlobToPc(blob, `bonk-last15-${Date.now()}.${ext}`);
} catch (e) {
BE_LOG.debug("clipRing.save", e);
window.alert("Could not build the last-15s clip.");
}
}
function beEnsureClipRingBuffer() {
if (beClipRing.started) {
beClipRing.ensureRetries = 0;
return;
}
beStartClipRingBuffer();
if (!beClipRing.started && beClipRing.ensureRetries < 40) {
beClipRing.ensureRetries++;
window.setTimeout(beEnsureClipRingBuffer, 2000);
}
}
function beBuildPrettyTopClipWrap(rightCss) {
const wrap = document.createElement("div");
wrap.id = "pretty_top_clip_wrap";
wrap.style.position = "absolute";
wrap.style.top = "0";
wrap.style.right = rightCss;
wrap.style.width = "58px";
wrap.style.height = "34px";
const clipBtn = document.createElement("div");
clipBtn.id = "pretty_top_clip_record";
clipBtn.title = "Record clip to PC (click to start) — hover for last 15s";
clipBtn.classList.add("pretty_top_button", "niceborderleft");
const menu = document.createElement("div");
menu.id = "pretty_top_clip_menu";
menu.setAttribute("role", "menu");
const last15 = document.createElement("button");
last15.type = "button";
last15.id = "pretty_top_clip_last15";
last15.classList.add("pretty_top_button", "niceborderleft");
last15.setAttribute("aria-label", "Record last 15 seconds to PC");
last15.title = "Save the last 15 seconds of gameplay (MP4 or WebM — depends on browser)";
last15.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 46" aria-hidden="true">' +
'<path fill="none" stroke="#ffffff" stroke-width="2.1" stroke-linecap="round" d="M41 33.5A15 15 0 1 1 28 11"/>' +
'<path fill="#ffffff" d="M29 8.5l7 2.5-7 2.5v-5z"/>' +
'<text x="28" y="31" text-anchor="middle" fill="#ffffff" font-size="12" font-weight="700" font-family="Segoe UI,system-ui,sans-serif">15</text>' +
'<text x="28" y="42" text-anchor="middle" fill="#ffffff" font-size="7.5" font-weight="700" font-family="Segoe UI,system-ui,sans-serif">SEC</text>' +
"</svg>";
menu.appendChild(last15);
wrap.appendChild(clipBtn);
wrap.appendChild(menu);
return wrap;
}
/** Prefer MP4 (Chrome/Edge often support H.264); fall back to WebM. Returns null if nothing works. */
function beCreateMediaRecorderPreferMp4(stream) {
if (!window.MediaRecorder) return null;
const candidates = [
"video/mp4;codecs=avc1.42E01E",
"video/mp4;codecs=avc1.4D401E",
"video/mp4;codecs=avc1.640028",
"video/mp4",
"video/webm;codecs=vp9,opus",
"video/webm;codecs=vp8,opus",
"video/webm;codecs=vp9",
"video/webm;codecs=vp8",
"video/webm"
];
for (let i = 0; i < candidates.length; i++) {
try {
if (!MediaRecorder.isTypeSupported(candidates[i])) continue;
const r = new MediaRecorder(stream, { mimeType: candidates[i] });
return r;
} catch (e) {
void e;
}
}
try {
return new MediaRecorder(stream);
} catch (e2) {
BE_LOG.debug("beCreateMediaRecorderPreferMp4.fallback", e2);
return null;
}
}
function beClipFileExtensionForMime(mime) {
const m = (mime || "").toLowerCase();
if (m.includes("mp4")) return "mp4";
if (m.includes("webm")) return "webm";
return "webm";
}
function beDownloadBlobToPc(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function beUpgradeClipRecordToWrap(bar) {
const old = document.getElementById("pretty_top_clip_record");
if (!old || document.getElementById("pretty_top_clip_wrap")) return;
let right = old.style.right;
if (!right) {
try {
right = getComputedStyle(old).right;
} catch (e) {
return;
}
}
old.remove();
beInjectPrettyTopClipRecordCss();
const wrap = beBuildPrettyTopClipWrap(right);
bar.appendChild(wrap);
beWirePrettyTopClipRecord(wrap.querySelector("#pretty_top_clip_record"));
}
function beWirePrettyTopClipRecord(clipBtn) {
let recorder = null;
let chunks = [];
let recording = false;
let stream = null;
const last15 = document.getElementById("pretty_top_clip_last15");
if (last15) {
last15.addEventListener("mousedown", (e) => e.stopPropagation());
last15.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
beSaveClipLast15Seconds();
});
}
clipBtn.addEventListener("click", (e) => {
e.stopPropagation();
if (!window.MediaRecorder) {
window.alert("Clip recording is not supported in this browser.");
return;
}
const canvas = document.querySelector("#gamerenderer canvas");
if (!canvas) {
window.alert("Game canvas not found — enter a match and try again.");
return;
}
if (recording) {
try {
if (recorder && recorder.state !== "inactive") recorder.stop();
} catch (e2) {
BE_LOG.debug("clipRecord.stop", e2);
}
recording = false;
clipBtn.classList.remove("be-recording");
clipBtn.title = "Record clip to PC (click to start) — hover for last 15s";
return;
}
beStopClipRingBuffer();
chunks = [];
try {
stream = canvas.captureStream(30);
} catch (e3) {
window.alert("Could not capture the canvas: " + (e3 && e3.message ? e3.message : String(e3)));
return;
}
recorder = beCreateMediaRecorderPreferMp4(stream);
if (!recorder) {
try {
stream.getTracks().forEach((t) => t.stop());
} catch (e4) {
void e4;
}
window.alert("No supported video recording codec (MP4/WebM) in this browser.");
return;
}
recorder.ondataavailable = (ev) => {
if (ev.data && ev.data.size) chunks.push(ev.data);
};
recorder.onstop = () => {
recording = false;
clipBtn.classList.remove("be-recording");
clipBtn.title = "Record clip to PC (click to start) — hover for last 15s";
try {
const mt = recorder.mimeType || "video/webm";
const blob = new Blob(chunks, { type: mt });
const ext = beClipFileExtensionForMime(mt);
beDownloadBlobToPc(blob, `bonk-clip-${Date.now()}.${ext}`);
} catch (e8) {
BE_LOG.debug("clipRecord.blob", e8);
window.alert("Could not save the clip.");
}
chunks = [];
recorder = null;
try {
if (stream) stream.getTracks().forEach((t) => t.stop());
} catch (e9) {
void e9;
}
stream = null;
window.setTimeout(() => {
beClipRing.ensureRetries = 0;
beEnsureClipRingBuffer();
}, 450);
};
try {
recorder.start(250);
recording = true;
clipBtn.classList.add("be-recording");
clipBtn.title = "Recording… click again to stop and save (MP4 or WebM)";
} catch (e10) {
BE_LOG.debug("clipRecord.start", e10);
window.alert("Could not start recording.");
try {
stream.getTracks().forEach((t) => t.stop());
} catch (e11) {
void e11;
}
stream = null;
recorder = null;
window.setTimeout(() => {
beClipRing.ensureRetries = 0;
beEnsureClipRingBuffer();
}, 450);
}
});
window.setTimeout(() => {
beClipRing.ensureRetries = 0;
beEnsureClipRingBuffer();
}, 500);
}
/**
* Upgrade path: fullscreen already installed from an older build — add clip button + layout only.
*/
function beInstallPrettyTopClipRecordOnly(bar) {
beInjectPrettyTopClipRecordCss();
const fs = document.getElementById("pretty_top_fullscreen");
if (!fs) return;
if (document.getElementById("pretty_top_clip_wrap")) return;
if (document.getElementById("pretty_top_clip_record")) {
beUpgradeClipRecordToWrap(bar);
return;
}
const paint = beFindPrettyTopPaintAnchor(bar);
let pr;
try {
const fsR = parseFloat(getComputedStyle(fs).right);
const paintR = paint ? parseFloat(getComputedStyle(paint).right) : NaN;
if (Number.isFinite(fsR) && Number.isFinite(paintR)) {
if (paintR > fsR) {
pr = fsR;
} else {
pr = paintR;
paint.style.right = `${pr + 116}px`;
fs.style.right = `${pr}px`;
}
} else if (Number.isFinite(fsR)) {
pr = fsR - 58;
if (paint) paint.style.right = `${pr + 116}px`;
fs.style.right = `${pr}px`;
}
} catch (e) {
BE_LOG.debug("beInstallPrettyTopClipRecordOnly.layout", e);
return;
}
if (!Number.isFinite(pr)) return;
const lc = document.getElementById("pretty_top_be_launcher");
if (lc) lc.style.right = `${pr + 174}px`;
const wrap = beBuildPrettyTopClipWrap(`${pr + 58}px`);
bar.appendChild(wrap);
beWirePrettyTopClipRecord(wrap.querySelector("#pretty_top_clip_record"));
}
function beInstallPrettyTopFullscreenOnce() {
if (document.getElementById("pretty_top_fullscreen")) {
const bar = document.getElementById("pretty_top_bar");
if (bar && !document.getElementById("pretty_top_clip_wrap")) {
if (document.getElementById("pretty_top_clip_record")) {
beUpgradeClipRecordToWrap(bar);
} else {
beInstallPrettyTopClipRecordOnly(bar);
}
}
return true;
}
const bar = document.getElementById("pretty_top_bar");
if (!bar) return false;
const inIframe = window.self !== window.top;
let parentDoc = null;
try {
if (inIframe) parentDoc = window.parent.document;
} catch (e) {
parentDoc = null;
}
const FULLSCREEN_INNER_CSS = `
#pretty_top_fullscreen {
width: 58px;
height: 34px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 24 24'%3E%3Cpath d='M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z'/%3E%3C/svg%3E");
background-size: 24px 24px;
background-repeat: no-repeat;
background-position: center;
position: absolute;
top: 0;
}
.fullscreen #pretty_top_fullscreen {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 24 24'%3E%3Cpath d='M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z'/%3E%3C/svg%3E") !important;
}
.fullscreen #bonkiocontainer {
height: 100vh !important;
width: 100vw !important;
border: none !important;
}
.fullscreen #ingamecountdown {
opacity: 75%;
}
.fullscreen #gamerenderer {
text-align: center !important;
}
.fullscreen #gamerenderer>canvas {
display: inline-block !important;
}
.fullscreen #bgreplay {
text-align: center !important;
}
.fullscreen #bgreplay>canvas {
display: inline-block !important;
}
.fullscreen #xpbarcontainer {
top: -3px !important;
}
html.fullscreen,
html.fullscreen body {
overflow: hidden !important;
height: 100% !important;
max-height: 100vh !important;
}
body.fullscreen {
margin: 0 !important;
}
/* Edge-to-edge canvas: reduces pillarboxing (optional via Settings / localStorage). */
.fullscreen.be-fs-stretch #bonkiocontainer {
display: flex !important;
flex-direction: column !important;
align-items: stretch !important;
}
.fullscreen.be-fs-stretch #gamerenderer {
flex: 1 1 auto !important;
min-height: 0 !important;
width: 100% !important;
max-width: none !important;
text-align: center !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-contain #gamerenderer canvas,
.fullscreen.be-fs-stretch.be-fs-scale-contain-left #gamerenderer canvas,
.fullscreen.be-fs-stretch.be-fs-scale-contain-right #gamerenderer canvas {
width: 100% !important;
height: 100% !important;
max-width: none !important;
object-fit: contain !important;
}
/* Letterbox alignment: canvas is full-size; object-fit scales the map — use object-position to pin the fitted image (flex justify does nothing at 100% width). */
.fullscreen.be-fs-stretch.be-fs-scale-contain #gamerenderer canvas {
object-position: center center !important;
}
/* Pillar left → map on the right */
.fullscreen.be-fs-stretch.be-fs-scale-contain-left #gamerenderer canvas {
object-position: right center !important;
}
/* Pillar right → map on the left */
.fullscreen.be-fs-stretch.be-fs-scale-contain-right #gamerenderer canvas {
object-position: left center !important;
}
/* WDB: contain + pin, then light scale + small translateY (after scale) so overflow clips less from the map top. */
.fullscreen.be-fs-stretch.be-fs-scale-wdb-left #gamerenderer,
.fullscreen.be-fs-stretch.be-fs-scale-wdb-right #gamerenderer {
overflow: hidden !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-wdb-left #gamerenderer canvas {
width: 100% !important;
height: 100% !important;
max-width: none !important;
object-fit: contain !important;
object-position: right center !important;
transform: scale(1.03) translateY(1.5vh) !important;
transform-origin: right center !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-wdb-right #gamerenderer canvas {
width: 100% !important;
height: 100% !important;
max-width: none !important;
object-fit: contain !important;
object-position: left center !important;
transform: scale(1.03) translateY(1.5vh) !important;
transform-origin: left center !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-cover #gamerenderer canvas {
width: 100% !important;
height: 100% !important;
max-width: none !important;
object-fit: cover !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-fill #gamerenderer canvas {
width: 100% !important;
height: 100% !important;
max-width: none !important;
object-fit: fill !important;
}
.fullscreen.be-fs-stretch #bgreplay {
width: 100% !important;
text-align: center !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-contain #bgreplay canvas,
.fullscreen.be-fs-stretch.be-fs-scale-contain-left #bgreplay canvas,
.fullscreen.be-fs-stretch.be-fs-scale-contain-right #bgreplay canvas {
width: 100% !important;
max-width: none !important;
object-fit: contain !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-contain #bgreplay canvas {
object-position: center center !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-contain-left #bgreplay canvas {
object-position: right center !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-contain-right #bgreplay canvas {
object-position: left center !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-wdb-left #bgreplay,
.fullscreen.be-fs-stretch.be-fs-scale-wdb-right #bgreplay {
overflow: hidden !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-wdb-left #bgreplay canvas {
width: 100% !important;
max-width: none !important;
object-fit: contain !important;
object-position: right center !important;
transform: scale(1.03) translateY(1.5vh) !important;
transform-origin: right center !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-wdb-right #bgreplay canvas {
width: 100% !important;
max-width: none !important;
object-fit: contain !important;
object-position: left center !important;
transform: scale(1.03) translateY(1.5vh) !important;
transform-origin: left center !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-cover #bgreplay canvas {
width: 100% !important;
max-width: none !important;
object-fit: cover !important;
}
.fullscreen.be-fs-stretch.be-fs-scale-fill #bgreplay canvas {
width: 100% !important;
height: 100% !important;
max-width: none !important;
object-fit: fill !important;
}
`;
const FULLSCREEN_PARENT_CSS = `
html.fullscreen,
html.fullscreen body {
overflow: hidden !important;
height: 100% !important;
max-height: 100vh !important;
margin: 0 !important;
}
body.fullscreen {
overflow: hidden !important;
}
.fullscreen #maingameframe {
position: fixed !important;
margin-top: 0px !important;
left: 0 !important;
top: 0 !important;
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
z-index: 99999 !important;
border: none !important;
}
.fullscreen #bonkioheader {
display: none !important;
}
.fullscreen #descriptioncontainer {
display: none !important;
}
`;
beInjectDocStyle(document, "be-fullscreen-inner", FULLSCREEN_INNER_CSS);
if (parentDoc) beInjectDocStyle(parentDoc, "be-fullscreen-parent", FULLSCREEN_PARENT_CSS);
beInjectDocStyle(
document,
"be-pretty-top-overlay-hint",
`
/* Same cell size as #pretty_top_fullscreen / native .pretty_top_button (58×34). */
#pretty_top_be_launcher {
display: none;
width: 58px !important;
height: 34px !important;
min-width: 58px !important;
min-height: 34px !important;
box-sizing: border-box !important;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
padding: 0 1px !important;
margin: 0 !important;
overflow: hidden;
cursor: pointer;
font: 700 10px/1 "Segoe UI", system-ui, sans-serif;
color: rgba(255, 255, 255, 0.92);
letter-spacing: 0.02em;
}
#pretty_top_be_launcher .be-top-launcher-row {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
line-height: 1;
}
#pretty_top_be_launcher .be-top-launcher-dot {
width: 6px;
height: 6px;
border-radius: 50%;
/* Default matches tracker idle border; JS syncs to capped/farming/idle. */
background: rgba(255, 170, 0, 0.9);
flex-shrink: 0;
}
#pretty_top_be_launcher .be-top-launcher-hk {
font-size: 8px;
font-weight: 700;
line-height: 1;
opacity: 0.8;
max-width: 54px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`
);
const mapEditor = document.getElementById("mapeditor");
const lobby = document.getElementById("newbonklobby");
const limitSize = () => {
if (!mapEditor || !lobby) return;
const mapEditorStyle = mapEditor.style;
const lobbyStyle = lobby.style;
const vw = inIframe ? window.parent.innerWidth : window.innerWidth;
const vh = inIframe ? window.parent.innerHeight : window.innerHeight;
const max_width = `${Math.min(vw / 730, vh / 500) * 730 * 0.9}px`;
mapEditorStyle.maxHeight = "calc(100vh - 70px)";
lobbyStyle.maxHeight = "calc(100vh - 70px)";
mapEditorStyle.maxWidth = max_width;
lobbyStyle.maxWidth = max_width;
};
let fullscreenOn = false;
let beFullscreenHiddenBannerEls = [];
let beFsToggleGen = 0;
beInjectPrettyTopClipRecordCss();
const btn = document.createElement("div");
btn.id = "pretty_top_fullscreen";
btn.title = "Toggle fullscreen";
btn.classList.add("pretty_top_button", "niceborderleft");
const paint = beFindPrettyTopPaintAnchor(bar);
let rightPx;
if (paint) {
try {
const cs = getComputedStyle(paint);
if (cs.position === "absolute") {
const pr0 = parseFloat(cs.right);
if (Number.isFinite(pr0)) {
rightPx = pr0;
paint.style.right = `${pr0 + 116}px`;
}
}
} catch (e2) {
void e2;
}
}
if (rightPx === undefined) {
const n = bar.querySelectorAll(":scope > .pretty_top_button.niceborderleft").length;
rightPx = n * 58 + 1;
}
const clipWrap = beBuildPrettyTopClipWrap(`${rightPx + 58}px`);
btn.style.right = `${rightPx}px`;
const launcherChip = document.createElement("div");
launcherChip.id = "pretty_top_be_launcher";
launcherChip.className = "pretty_top_button niceborderleft";
launcherChip.title = "Open Bonk Enhanced overlay";
launcherChip.innerHTML =
'<div class="be-top-launcher-row"><span class="be-top-launcher-dot" aria-hidden="true"></span><span>BE</span></div><span id="pretty_top_be_launcher_hk" class="be-top-launcher-hk"></span>';
launcherChip.style.position = "absolute";
launcherChip.style.top = "0";
launcherChip.style.right = `${rightPx + 174}px`;
launcherChip.style.setProperty("display", "none", "important");
launcherChip.addEventListener("click", (e) => {
e.stopPropagation();
setOverlayVisibility(true);
});
bar.appendChild(launcherChip);
bar.appendChild(clipWrap);
bar.appendChild(btn);
beWirePrettyTopClipRecord(clipWrap.querySelector("#pretty_top_clip_record"));
window.setTimeout(() => {
const b = document.getElementById("pretty_top_fullscreen");
const clip = document.getElementById("pretty_top_clip_wrap");
const lc = document.getElementById("pretty_top_be_launcher");
const br = document.getElementById("pretty_top_bar");
if (!b || !br) return;
const paintLate = beFindPrettyTopPaintAnchor(br);
if (!paintLate) return;
try {
const cs = getComputedStyle(paintLate);
if (cs.position !== "absolute") return;
const paintR = parseFloat(cs.right);
const fsR = parseFloat(getComputedStyle(b).right);
if (!Number.isFinite(paintR) || !Number.isFinite(fsR)) return;
const pr = paintR > fsR ? fsR : paintR;
b.style.right = `${pr}px`;
if (clip) clip.style.right = `${pr + 58}px`;
paintLate.style.right = `${pr + 116}px`;
if (lc) lc.style.right = `${pr + 174}px`;
} catch (e4) {
void e4;
}
}, 800);
const applyFullscreenDom = (on) => {
const rootBody = document.body;
let outerBody = null;
let outerHtml = null;
try {
if (inIframe && parentDoc) {
outerBody = parentDoc.body;
outerHtml = parentDoc.documentElement;
}
} catch (e3) {
void e3;
}
const mergeBannerHidden = (add) => {
for (let i = 0; i < add.length; i++) {
if (beFullscreenHiddenBannerEls.indexOf(add[i]) < 0) {
beFullscreenHiddenBannerEls.push(add[i]);
}
}
};
const sweepPasswordBanners = () => {
const docs = beFullscreenGatherRootDocs(parentDoc);
for (let d = 0; d < docs.length; d++) {
mergeBannerHidden(beFullscreenHidePasswordBanners(docs[d]));
}
};
if (on) {
const g = ++beFsToggleGen;
document.documentElement.classList.add("fullscreen");
rootBody.classList.add("fullscreen");
rootBody.classList.toggle("be-fs-stretch", beFullscreenStretchWanted());
beApplyFullscreenScaleBodyClasses(rootBody);
if (outerHtml) outerHtml.classList.add("fullscreen");
if (outerBody) outerBody.classList.add("fullscreen");
beFullscreenRestoreHidden(beFullscreenHiddenBannerEls);
beFullscreenHiddenBannerEls = [];
sweepPasswordBanners();
/* A few delayed sweeps only (no MutationObserver — observing the whole game DOM caused severe lag). */
[200, 1200, 4000].forEach((ms) => {
window.setTimeout(() => {
if (!fullscreenOn || g !== beFsToggleGen) return;
sweepPasswordBanners();
}, ms);
});
limitSize();
} else {
beFsToggleGen++;
beFullscreenRestoreHidden(beFullscreenHiddenBannerEls);
beFullscreenHiddenBannerEls = [];
document.documentElement.classList.remove("fullscreen");
rootBody.classList.remove("fullscreen", "be-fs-stretch", ...BE_FS_SCALE_CLASSES);
if (outerHtml) outerHtml.classList.remove("fullscreen");
if (outerBody) outerBody.classList.remove("fullscreen");
if (mapEditor) {
mapEditor.style.maxHeight = "calc(100vh - 70px)";
mapEditor.style.maxWidth = "90vw";
}
if (lobby) {
lobby.style.maxHeight = "100%";
lobby.style.maxWidth = "100%";
}
}
};
btn.addEventListener("click", () => {
fullscreenOn = !fullscreenOn;
applyFullscreenDom(fullscreenOn);
});
window.addEventListener("resize", () => {
if (fullscreenOn) window.setTimeout(limitSize, 50);
});
return true;
}
safeRun("gameframe.prettyTopFullscreen", () => {
const tryInstall = () => {
if (document.getElementById("pretty_top_fullscreen")) return true;
if (!document.getElementById("pretty_top_bar")) return false;
return beInstallPrettyTopFullscreenOnce();
};
if (tryInstall()) return;
const mo = new MutationObserver(() => {
if (tryInstall()) mo.disconnect();
});
mo.observe(document.documentElement, { childList: true, subtree: true });
window.setTimeout(() => mo.disconnect(), 120000);
});
const Events = {
events: {},
on(name, fn) {
if (!this.events[name]) this.events[name] = [];
this.events[name].push(fn);
},
emit(name, data) {
if (!this.events[name]) return;
this.events[name].forEach(fn => fn(data));
}
};
const Storage = {
cache: null,
/** Calendar day for stats buckets (UTC YYYY-MM-DD). Matches existing localStorage keys. */
getTodayKey() {
return new Date().toISOString().slice(0, 10);
},
getPlayerKey() {
return document.getElementById("pretty_top_name")?.innerText || "Guest";
},
load() {
const key = this.getTodayKey();
const player = this.getPlayerKey();
const storageKey = "bonk_stats_" + player;
/* Must not reuse cache across midnight or account switch — otherwise wins/XP keep updating yesterday's key. */
if (this.cache && this.cache.storageKey === storageKey && this.cache.key === key) {
return this.cache;
}
const data = JSON.parse(localStorage.getItem(storageKey) || "{}");
if (!data[key]) {
data[key] = {
wins: 0,
xp: 0,
baselineXP: null,
_synced: !!data._globalXP // 🔥 treat cache as synced if exists
};
}
if (data[key]._synced === undefined) {
data[key]._synced = !!data._globalXP;
}
this.cache = { data, key, storageKey };
return this.cache;
},
save(data, storageKey) {
localStorage.setItem(storageKey, JSON.stringify(data));
if (this.cache) {
this.cache.data = data;
}
},
invalidate() {
this.cache = null;
}
};
const BE_ALTS_KEY = "be_alts_registry_v9";
const BE_FRIENDS_KEY = "be_friends_v9";
let beAltsFilterQuery = "";
function loadAltsRegistry() {
try {
const raw = localStorage.getItem(BE_ALTS_KEY);
const o = raw ? JSON.parse(raw) : {};
const alts = Array.isArray(o.alts) ? o.alts : [];
return alts
.map((a) => ({
name: String(a?.name || "").trim(),
level: Math.max(0, Math.floor(Number(a?.level) || 0)),
xp: Math.max(0, Math.floor(Number(a?.xp) || 0)),
lastSeen: typeof a?.lastSeen === "number" ? a.lastSeen : 0,
tags: Array.isArray(a?.tags)
? a.tags.map((t) => String(t || "").trim()).filter(Boolean).slice(0, 16)
: []
}))
.filter((a) => a.name && a.name !== "Guest");
} catch {
return [];
}
}
function saveAltsRegistry(alts) {
localStorage.setItem(BE_ALTS_KEY, JSON.stringify({ alts, updatedAt: Date.now() }));
}
let lastAltRecordAt = 0;
function recordCurrentAltSnapshot(name, level, xp) {
if (!name || name === "Guest" || !xp) return;
const now = Date.now();
if (now - lastAltRecordAt < 20000) return;
lastAltRecordAt = now;
const alts = loadAltsRegistry();
const key = name.trim().toLowerCase();
let idx = alts.findIndex((a) => a.name.trim().toLowerCase() === key);
const row = {
name: name.trim(),
level,
xp,
lastSeen: now,
tags: idx >= 0 ? (alts[idx].tags || []) : []
};
if (idx >= 0) alts[idx] = { ...alts[idx], ...row };
else alts.push(row);
alts.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
saveAltsRegistry(alts.slice(0, 40));
}
function loadFriendsList() {
try {
const raw = localStorage.getItem(BE_FRIENDS_KEY);
const arr = raw ? JSON.parse(raw) : [];
return Array.isArray(arr) ? arr.map((s) => String(s || "").trim()).filter(Boolean) : [];
} catch {
return [];
}
}
function saveFriendsList(names) {
const clean = [...new Set(names.map((s) => String(s || "").trim()).filter(Boolean))].slice(0, 200);
localStorage.setItem(BE_FRIENDS_KEY, JSON.stringify(clean));
window.BonkEnhanced.syncFriendsFromStorage();
}
/* ----- Encrypted credential vault (master password) ----- */
const BE_VAULT_STORAGE_KEY = "be_cred_vault_v1";
const BE_VAULT_LOCK_MS = 2 * 60 * 60 * 1000;
const BE_VAULT_PBKDF2_ITERS = 250000;
/** Optional: store master password in plain text locally so the vault auto-unlocks (user opt-in). */
const BE_VAULT_REMEMBER_KEY = "be_vault_remember_device_v1";
const BE_VAULT_MASTER_PLAIN_KEY = "be_vault_master_plain_v1";
function isVaultRememberDevice() {
try {
return localStorage.getItem(BE_VAULT_REMEMBER_KEY) === "1" && !!localStorage.getItem(BE_VAULT_MASTER_PLAIN_KEY);
} catch {
return false;
}
}
function saveVaultRememberMaster(password) {
try {
localStorage.setItem(BE_VAULT_REMEMBER_KEY, "1");
localStorage.setItem(BE_VAULT_MASTER_PLAIN_KEY, String(password || ""));
} catch (e) {
BE_LOG.error("saveVaultRememberMaster", e);
}
}
function clearVaultRememberDevice() {
try {
localStorage.removeItem(BE_VAULT_REMEMBER_KEY);
localStorage.removeItem(BE_VAULT_MASTER_PLAIN_KEY);
} catch (e) {
BE_LOG.error("clearVaultRememberDevice", e);
}
}
const beVaultSession = {
unlocked: false,
/** @type {string|null} */
password: null,
/** @type {{ id: string, username: string, password: string, label?: string }[]} */
entries: [],
unlockUntil: 0,
lockTimer: null
};
function beVaultScheduleLockRefresh() {
if (isVaultRememberDevice()) {
if (beVaultSession.lockTimer) {
clearTimeout(beVaultSession.lockTimer);
beVaultSession.lockTimer = null;
}
return;
}
if (beVaultSession.lockTimer) clearTimeout(beVaultSession.lockTimer);
beVaultSession.lockTimer = setTimeout(() => {
beVaultLock();
renderSocialPanel();
}, BE_VAULT_LOCK_MS);
}
function beVaultTouchActivity() {
if (!beVaultSession.unlocked) return;
beVaultSession.unlockUntil = Date.now() + BE_VAULT_LOCK_MS;
beVaultScheduleLockRefresh();
}
function beVaultLock() {
beVaultSession.unlocked = false;
beVaultSession.password = null;
beVaultSession.entries = [];
beVaultSession.unlockUntil = 0;
if (beVaultSession.lockTimer) {
clearTimeout(beVaultSession.lockTimer);
beVaultSession.lockTimer = null;
}
}
function beVaultReadStored() {
try {
const raw = localStorage.getItem(BE_VAULT_STORAGE_KEY);
if (!raw) return null;
const o = JSON.parse(raw);
if (!o || o.v !== 1 || typeof o.salt !== "string" || typeof o.iv !== "string" || typeof o.data !== "string") return null;
return o;
} catch {
return null;
}
}
function u8FromB64(b64) {
const bin = atob(b64);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
function u8ToB64(buf) {
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
let s = "";
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
return btoa(s);
}
async function beVaultDeriveAesKey(password, saltU8) {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
enc.encode(password),
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
);
return crypto.subtle.deriveKey(
{ name: "PBKDF2", salt: saltU8, iterations: BE_VAULT_PBKDF2_ITERS, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
async function beVaultEncrypt(password, payloadObj) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await beVaultDeriveAesKey(password, salt);
const enc = new TextEncoder();
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode(JSON.stringify(payloadObj)));
return {
v: 1,
salt: u8ToB64(salt),
iv: u8ToB64(iv),
data: u8ToB64(new Uint8Array(ct))
};
}
async function beVaultDecrypt(password, stored) {
const salt = u8FromB64(stored.salt);
const iv = u8FromB64(stored.iv);
const ct = u8FromB64(stored.data);
const key = await beVaultDeriveAesKey(password, salt);
const buf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
const dec = new TextDecoder().decode(buf);
return JSON.parse(dec);
}
async function beVaultPersist() {
if (!beVaultSession.unlocked || !beVaultSession.password) throw new Error("Vault locked");
const payload = { entries: beVaultSession.entries };
const enc = await beVaultEncrypt(beVaultSession.password, payload);
localStorage.setItem(BE_VAULT_STORAGE_KEY, JSON.stringify(enc));
}
async function beVaultUnlockWithPassword(pw) {
const stored = beVaultReadStored();
if (!stored) throw new Error("No vault");
const plain = await beVaultDecrypt(pw, stored);
const entries = Array.isArray(plain?.entries) ? plain.entries : [];
beVaultSession.unlocked = true;
beVaultSession.password = pw;
beVaultSession.entries = entries
.map((e) => ({
id: String(e?.id || crypto.randomUUID?.() || `e_${Date.now()}_${Math.random()}`),
username: String(e?.username || "").trim(),
password: String(e?.password || ""),
label: e?.label ? String(e.label).trim() : ""
}))
.filter((e) => e.username);
beVaultTouchActivity();
beVaultScheduleLockRefresh();
}
async function beVaultCreateVault(pw) {
beVaultSession.entries = [];
beVaultSession.unlocked = true;
beVaultSession.password = pw;
await beVaultPersist();
beVaultTouchActivity();
beVaultScheduleLockRefresh();
}
function beNewEntryId() {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
return `e_${Date.now()}_${Math.floor(Math.random() * 1e9)}`;
}
async function tryVaultAutoUnlockFromRemembered() {
if (!beVaultReadStored()) return;
if (beVaultSession.unlocked) return;
if (!isVaultRememberDevice()) return;
let pw = "";
try {
pw = localStorage.getItem(BE_VAULT_MASTER_PLAIN_KEY) || "";
} catch {
return;
}
if (!pw) return;
try {
await beVaultUnlockWithPassword(pw);
if (beVaultSession.lockTimer) {
clearTimeout(beVaultSession.lockTimer);
beVaultSession.lockTimer = null;
}
renderSocialPanel();
} catch {
clearVaultRememberDevice();
try {
renderSocialPanel();
} catch (e) {
BE_LOG.debug("tryVaultAutoUnlockFromRemembered render after clear", e);
}
}
}
/** One-off poll used by initial trySync / wake — not the autofill interval (see XP Farm addon). */
function beRequestOneXpPoll() {
if (!bonkWSS || bonkWSS.readyState !== WebSocket.OPEN) return;
if (beIsBonkRoomListVisible()) return;
try {
bonkWSS.send("42[38]");
} catch {}
}
const XPEngine = {
handleMessage(event) {
try {
const rawAny = event.data;
let text;
if (typeof rawAny === "string") {
text = rawAny;
} else if (typeof Blob !== "undefined" && rawAny instanceof Blob) {
void rawAny.text().then((t) => {
XPEngine.applySocketPayload(t, event.target);
});
return;
} else if (typeof ArrayBuffer !== "undefined" && rawAny instanceof ArrayBuffer) {
text = new TextDecoder("utf-8").decode(rawAny);
} else {
return;
}
XPEngine.applySocketPayload(text, event.target);
} catch {}
},
/**
* @param {string} eventData
* @param {EventTarget|null} sourceWs
*/
applySocketPayload(eventData, sourceWs) {
try {
if (!eventData || !eventData.startsWith("42[")) return;
const raw = eventData.slice(2);
/* Avoid JSON.parse on the bulk of socket.io traffic (chat, game state, etc.). */
if (!/^\[\s*46\s*,/.test(raw)) return;
const msg = JSON.parse(raw);
if (msg[0] !== 46) return;
const xp = msg[1]?.newXP;
if (typeof xp !== "number") return;
/* This frame came from the real game socket — always pin polls to it (fixes wrong `bonkWSS` when multiple WebSockets exist, common on Brave). */
try {
if (sourceWs && sourceWs.readyState === WebSocket.OPEN) {
bonkWSS = /** @type {WebSocket} */ (sourceWs);
}
} catch {}
const { data, key, storageKey } = Storage.load();
const lastXP = data[key].xp || 0;
const baselineWasNull = data[key].baselineXP == null;
/* Today’s wins/rate use xpToday = xp - baseline. If the first server packet already
includes several wins (200 XP) while lastXP was 0, old logic set baseline = xp and
wins stayed 0 forever. Anchor at 0 when we had no prior XP for this UTC day; else
resume from lastXP (minus seeded wins for legacy imports). */
if (baselineWasNull) {
const seededWins = Number(data[key].wins || 0);
if (lastXP <= 0) {
data[key].baselineXP = 0;
} else {
data[key].baselineXP = Math.max(0, lastXP - seededWins * 100);
}
}
data[key].xp = xp;
data._globalXP = xp;
data[key]._synced = true;
if (xp > lastXP) {
const xpDelta = xp - lastXP;
if (xpDelta > 100) {
console.debug("[Bonk Enhanced] XP batch detected:", xpDelta);
}
noXpCount = 0;
lastXpChangeTime = Date.now();
} else {
const timeSinceLastXP = Date.now() - lastXpChangeTime;
if (timeSinceLastXP > delay + 2000) {
noXpCount++;
}
}
/* Same in-memory state as before; skip localStorage + UI when XP unchanged (reduces hitches). */
const xpChanged = xp !== lastXP;
if (xpChanged || baselineWasNull) {
Storage.save(data, storageKey);
}
if (xpChanged) {
Events.emit("xpUpdate");
}
} catch {}
}
};
function waitForPlayer(callback) {
const el = document.getElementById("pretty_top_name");
if (el && el.innerText.trim()) {
callback();
return;
}
const observer = new MutationObserver(() => {
const el = document.getElementById("pretty_top_name");
if (el && el.innerText.trim()) {
observer.disconnect();
callback();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
function waitForSocket(callback) {
const check = () => {
if (bonkWSS && bonkWSS.readyState === 1) {
callback();
} else {
setTimeout(check, 100);
}
};
check();
}
const UIEngine = {
async init() {
await createUI();
},
render() {
updateUI();
},
updateSession() {
updateSessionOnly();
}
};
window.BonkVisuals = {
players: {
usernames: {
visible: true,
alpha: 1
},
skins: true,
visible: true,
alpha: 1
},
chat: {
visible: true,
alpha: 1
}
};
const DAILY_CAP = 180;
let sessionStart = Date.now();
let sessionStartXP = 0;
let sessionStartWins = 0;
let previousWins = 0;
let currentPlayer = null;
let hasInitializedPlayer = false;
let bonkWSS = null;
let xpEnabled = false;
/** After farm has been turned on at least once (this page / account); IDLE label only shows when this is true. */
let beFarmEverOnThisSession = false;
let delay = 18500;
/** Exposes XP farm hooks for the optional Bonk Enhanced — XP Farm addon userscript only. */
function beRegisterFarmApi() {
const BE = window.BonkEnhanced;
if (!BE || BE._farmApiRegistered) return;
BE._farmApiRegistered = true;
BE._farmApiVersion = 1;
BE.setFarmActive = (on) => {
xpEnabled = !!on;
if (on) beFarmEverOnThisSession = true;
Events.emit("xpUpdate");
};
BE.getFarmActive = () => xpEnabled;
BE.getBonkWSS = () => bonkWSS;
BE.emitXpUpdate = () => Events.emit("xpUpdate");
BE.beIsBonkRoomListVisible = beIsBonkRoomListVisible;
BE.beBackoffMs = beBackoffMs;
BE.getXpFarmDelay = () => delay;
}
let noXpCount = 0;
let lastXpChangeTime = 0;
let lastSkinSrc = null;
let topBarEl = null;
let statsCache = null;
let lastRender = 0;
const CIRCLE_RADIUS = 34;
const CIRCLE_CIRCUMFERENCE = 2 * Math.PI * CIRCLE_RADIUS;
let hasRenderedOnce = false;
let lastUIState = {
xp: null,
wins: null,
level: null,
player: null
};
function getPlayerKey() {
return document.getElementById("pretty_top_name")?.innerText || "Guest";
}
function getRealLevel() {
try {
const container = document.getElementById("pretty_top_name")?.parentElement;
if (!container) return 0;
const text = container.innerText;
const match = text.match(/lv\s*(\d+)/i);
if (match) return parseInt(match[1]);
} catch {}
return 0;
}
async function createUI() {
if (document.getElementById("winTrackerBox")) return;
const div = document.createElement("div");
div.id = "winTrackerBox";
const style = document.createElement("style");
style.innerHTML = `
#winTrackerBox {
--be-space-xs: 4px;
--be-space-sm: 8px;
--be-space-md: 12px;
--be-space-lg: 16px;
--be-radius-md: 14px;
--be-radius-sm: 8px;
--be-font-sans: "Segoe UI", system-ui, sans-serif;
--be-font-mono: ui-monospace, monospace;
}
.tracker-card {
width: 240px;
padding: var(--be-space-md);
min-height: 160px;
position: relative;
border-radius: var(--be-radius-md);
background: linear-gradient(180deg, #0f1a2b, #0a1220);
border: 1px solid rgba(255, 170, 0, 0.55);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
padding-top: 52px;
overflow: hidden;
/* Column flex so .tracker-view fills a user-resized height and scrolls reliably (not just max(72vh,560)). */
display: flex;
flex-direction: column;
min-height: 0;
/* Cap height (raised so resize-handle can grow the panel); content scrolls in .tracker-view */
max-height: min(720px, 90vh) !important;
color: white;
font-family: monospace;
transform: scale(0.98);
opacity: 0;
transition: transform 0.18s ease, opacity 0.18s ease;
}
#winTrackerBox.be-overlay-open .tracker-card {
transform: scale(1);
opacity: 1;
}
.be-overlay-launcher {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 999999999;
height: 36px;
padding: 0 12px;
border-radius: 999px;
display: none !important;
align-items: center;
gap: 8px;
font: 600 12px "Segoe UI", sans-serif;
letter-spacing: 0.2px;
color: #d8e7ff;
background: linear-gradient(180deg, rgba(12, 25, 45, 0.95), rgba(7, 16, 30, 0.95));
border: 1px solid rgba(89, 138, 221, 0.45);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
cursor: pointer;
user-select: none;
pointer-events: none;
visibility: hidden;
}
.be-overlay-launcher:hover {
filter: brightness(1.08);
}
.be-overlay-launcher-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #5ea2ff;
}
#beKeybindOverlay {
position: fixed;
left: 18px;
bottom: 18px;
top: auto;
z-index: 999999990;
pointer-events: none;
display: none;
font-family: "Segoe UI", system-ui, sans-serif;
user-select: none;
--be-kb-scale: 1;
transform: scale(var(--be-kb-scale));
transform-origin: left bottom;
}
.be-kb-strip {
display: none;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
align-items: center;
padding-top: 4px;
margin-top: 2px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.be-kb-compact .be-kb-grid,
.be-kb-compact .be-kb-actions {
display: none !important;
}
.be-kb-compact .be-kb-strip {
display: flex !important;
border-top: none;
padding-top: 0;
margin-top: 0;
}
.be-kb-key-sm {
width: 34px;
height: 26px;
border-radius: 6px;
font-size: 10px;
font-weight: 800;
}
.be-kb-panel {
pointer-events: auto;
padding: 0;
border-radius: 14px;
background: linear-gradient(180deg, rgba(12, 22, 38, 0.96), rgba(8, 14, 26, 0.96));
border: 1px solid rgba(255, 170, 0, 0.45);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.38);
min-width: 176px;
overflow: hidden;
}
.be-kb-draghead {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 10px;
cursor: grab;
background: linear-gradient(180deg, rgba(22, 36, 58, 0.98), rgba(14, 24, 42, 0.95));
border-bottom: 1px solid rgba(255, 200, 120, 0.18);
pointer-events: auto;
}
.be-kb-draghead:active,
.be-kb-draghead.be-kb-dragging {
cursor: grabbing;
}
.be-kb-drag-grip {
display: inline-block;
width: 10px;
height: 12px;
opacity: 0.45;
background: repeating-linear-gradient(
180deg,
rgba(255, 210, 160, 0.85) 0 2px,
transparent 2px 4px
);
background-size: 3px 12px;
background-repeat: no-repeat;
background-position: 0 0;
flex-shrink: 0;
}
.be-kb-drag-title {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(255, 210, 140, 0.92);
}
.be-kb-body {
padding: 10px 12px 11px;
}
.be-kb-grid {
display: grid;
grid-template-columns: repeat(3, 44px);
grid-template-rows: auto auto;
gap: 5px;
justify-content: center;
align-items: center;
}
.be-kb-spacer {
min-height: 1px;
}
.be-kb-key {
width: 44px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
font-weight: 700;
color: #e8f0ff;
background: linear-gradient(180deg, rgba(30, 48, 78, 0.95), rgba(18, 30, 52, 0.95));
border: 1px solid rgba(100, 140, 200, 0.35);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
transition: background 0.08s ease, border-color 0.08s ease, box-shadow 0.08s ease, color 0.08s ease, transform 0.06s ease;
}
.be-kb-key-wide {
min-width: 72px;
width: auto;
padding: 0 10px;
font-size: 13px;
font-weight: 700;
}
.be-kb-key > span,
.be-kb-key-wide > span {
display: inline-block;
max-width: 112px;
line-height: 1.2;
text-align: center;
white-space: normal;
word-break: break-word;
font-size: 12px;
font-weight: 700;
}
.be-kb-grid .be-kb-key > span {
font-size: 10px;
font-weight: 800;
letter-spacing: 0.02em;
max-width: 44px;
white-space: nowrap;
}
.be-kb-action-wideonly {
justify-content: center;
}
.be-kb-key.be-kb-active {
background: linear-gradient(180deg, rgba(255, 170, 60, 0.95), rgba(230, 120, 20, 0.92));
border-color: rgba(255, 220, 160, 0.75);
color: #1a0d00;
box-shadow: 0 0 0 2px rgba(255, 200, 100, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.35);
transform: translateY(1px);
}
.be-kb-actions {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.be-kb-action {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.be-kb-label {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(200, 215, 240, 0.65);
}
.be-widget-hub {
position: fixed;
inset: 0;
z-index: 999999999;
display: none;
pointer-events: none;
align-items: center;
justify-content: center;
background: rgba(5, 10, 18, 0.52);
backdrop-filter: blur(8px);
}
.be-widget-hub-panel {
width: min(620px, 92vw);
border-radius: 18px;
padding: 14px;
background: linear-gradient(180deg, rgba(17, 29, 49, 0.96), rgba(11, 20, 36, 0.96));
border: 1px solid rgba(95, 145, 230, 0.35);
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.42);
}
.be-widget-hub-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
color: #dce9ff;
font: 600 13px "Segoe UI", sans-serif;
}
.be-widget-hub-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.be-widget-card.be-widget-hud {
position: relative;
overflow: hidden;
border-radius: 14px;
min-height: 92px;
padding: 12px 14px 12px 12px;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
text-align: left;
color: #fff;
cursor: pointer;
font: 600 12px "Segoe UI", sans-serif;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.12),
0 4px 18px rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.be-widget-hud-glyph {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.22);
color: #fff;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
.be-widget-hud-glyph svg {
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.25));
}
.be-widget-hud-copy {
flex: 1;
min-width: 0;
}
.be-widget-title {
display: block;
font-size: 12px;
line-height: 1.2;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
text-shadow: 0 0 12px rgba(255, 255, 255, 0.25);
}
.be-widget-card.be-widget-hud small {
display: block;
margin-top: 5px;
opacity: 0.88;
font-weight: 500;
font-size: 11px;
letter-spacing: 0.02em;
text-transform: none;
}
.be-widget-card.be-widget-hud:hover {
transform: translateY(-2px);
filter: brightness(1.05);
}
.be-widget-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 7px;
margin-bottom: 8px;
background: rgba(255, 255, 255, 0.22);
font-size: 13px;
}
.be-widget-dashboard,
.be-widget-stats,
.be-widget-calculator,
.be-widget-social,
.be-widget-visuals,
.be-widget-settings {
position: relative;
overflow: hidden;
}
.be-widget-dashboard {
background: linear-gradient(145deg, #1d4ed8, #2563eb 55%, #38bdf8);
}
.be-widget-dashboard::before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(90deg, rgba(255,255,255,0.08) 1px, transparent 1px) 0 0/18px 18px,
linear-gradient(rgba(255,255,255,0.08) 1px, transparent 1px) 0 0/18px 18px;
opacity: 0.35;
pointer-events: none;
}
.be-widget-dashboard::after {
content: "";
position: absolute;
right: -22px;
top: -20px;
width: 92px;
height: 92px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.35), rgba(255,255,255,0) 65%);
pointer-events: none;
}
.be-widget-dashboard .be-widget-hud-glyph {
background: linear-gradient(145deg, rgba(10, 31, 87, 0.75), rgba(11, 52, 126, 0.78));
box-shadow: inset 0 1px 0 rgba(255,255,255,0.22);
}
.be-widget-stats {
background: linear-gradient(145deg, #6d28d9, #9333ea 55%, #ec4899);
}
.be-widget-stats::before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(transparent 62%, rgba(255,255,255,0.15) 62% 66%, transparent 66%),
linear-gradient(120deg, transparent 52%, rgba(255,255,255,0.18) 52% 56%, transparent 56%);
opacity: 0.45;
pointer-events: none;
}
.be-widget-stats::after {
content: "";
position: absolute;
right: 10px;
bottom: 8px;
width: 56px;
height: 26px;
border-left: 2px solid rgba(255,255,255,0.45);
border-bottom: 2px solid rgba(255,255,255,0.45);
border-radius: 0 0 0 8px;
pointer-events: none;
}
.be-widget-stats .be-widget-hud-glyph {
background: linear-gradient(145deg, rgba(62, 18, 122, 0.8), rgba(112, 29, 150, 0.8));
box-shadow: inset 0 1px 0 rgba(255,255,255,0.22);
}
.be-widget-calculator {
background: linear-gradient(145deg, #b45309, #d97706 55%, #fbbf24);
}
.be-widget-calculator::before {
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(
-12deg,
transparent,
transparent 14px,
rgba(255, 255, 255, 0.04) 14px,
rgba(255, 255, 255, 0.04) 15px
);
opacity: 0.4;
pointer-events: none;
}
.be-widget-calculator .be-widget-hud-glyph {
background: linear-gradient(145deg, rgba(80, 40, 8, 0.55), rgba(120, 60, 10, 0.65));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.be-widget-social {
background: linear-gradient(145deg, #9d174d, #db2777 55%, #f472b6);
}
.be-widget-social::before {
content: "";
position: absolute;
right: -20px;
top: -24px;
width: 100px;
height: 100px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.12);
pointer-events: none;
}
.be-widget-social .be-widget-hud-glyph {
background: linear-gradient(145deg, rgba(90, 12, 50, 0.65), rgba(130, 20, 75, 0.72));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.be-widget-visuals {
background: linear-gradient(145deg, #0f766e, #0ea5a4 58%, #22c55e);
}
.be-widget-visuals::before {
content: "";
position: absolute;
left: -18px;
bottom: -18px;
width: 90px;
height: 90px;
border-radius: 50%;
border: 11px solid rgba(255,255,255,0.2);
pointer-events: none;
}
.be-widget-visuals::after {
content: "";
position: absolute;
right: 16px;
top: 12px;
width: 34px;
height: 18px;
border-radius: 999px;
border: 2px solid rgba(255,255,255,0.42);
pointer-events: none;
}
.be-widget-visuals .be-widget-hud-glyph {
background: linear-gradient(145deg, rgba(9, 74, 72, 0.78), rgba(6, 121, 95, 0.8));
box-shadow: inset 0 1px 0 rgba(255,255,255,0.22);
}
.be-widget-settings {
position: relative;
overflow: hidden;
background: linear-gradient(145deg, #e5e7eb, #cbd5e1) !important;
color: #1f2937;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.65), 0 5px 16px rgba(15, 23, 42, 0.2);
}
.be-widget-settings::after {
content: "";
position: absolute;
right: -10px;
bottom: -10px;
width: 68px;
height: 68px;
border-radius: 50%;
border: 10px solid rgba(71, 85, 105, 0.25);
opacity: 0.9;
}
.be-widget-settings::before {
content: "";
position: absolute;
left: -16px;
top: -16px;
width: 74px;
height: 74px;
border-radius: 50%;
border: 12px solid rgba(75, 85, 99, 0.55);
clip-path: polygon(0 0, 70% 0, 70% 35%, 100% 35%, 100% 100%, 0 100%);
transform: rotate(-18deg);
pointer-events: none;
}
.be-widget-settings .be-widget-hud-glyph {
position: relative;
background: linear-gradient(145deg, #6b7280, #374151);
color: #e5e7eb;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.28), 0 4px 10px rgba(17, 24, 39, 0.35);
}
.be-widget-settings .be-widget-hud-glyph::before {
content: "";
position: absolute;
inset: 5px;
border-radius: 50%;
border: 2px dashed rgba(229, 231, 235, 0.9);
}
.be-widget-settings small {
color: rgba(31, 41, 55, 0.8);
}
/* sections */
.tracker-section {
margin-bottom: 10px;
}
.tracker-name {
font-size: 18px;
font-weight: 700;
color: #ffffff;
letter-spacing: 0.5px;
line-height: 1;
font-family: 'Segoe UI', 'Inter', sans-serif;
min-width: 0;
max-width: 100%;
}
/* Long names: ~8 characters wide, slightly smaller type; full string scrolls inside */
.tracker-name.tracker-name--scroll {
overflow: hidden;
width: 8ch;
max-width: 100%;
box-sizing: border-box;
flex-shrink: 0;
font-size: 16px;
}
.tracker-name.tracker-name--scroll .tracker-name-text {
display: inline-block;
white-space: nowrap;
will-change: transform;
animation: be-tracker-name-marquee var(--be-name-marquee-duration, 10s) linear infinite alternate;
}
@keyframes be-tracker-name-marquee {
from { transform: translateX(0); }
to { transform: translateX(calc(-1 * var(--be-name-scroll, 0px))); }
}
.tracker-header {
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 10px;
margin-top: 14px;
/* Containing block for .tracker-circle so it scrolls with this view (not fixed to .tracker-card) */
position: relative;
}
.tracker-level {
font-size: 14px;
font-weight: 500;
opacity: 0.7;
margin: 0;
}
/* BAR */
.tracker-bar {
position: relative;
height: 12px;
background: #1a2235;
border-radius: 8px;
overflow: hidden;
}
.tracker-bar-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #4caf50, #7cff7c);
transition: width 0.3s ease;
}
.tracker-bar-text {
position: absolute;
right: 6px;
top: -18px;
font-size: 11px;
opacity: 0.8;
}
/* SUBTEXT */
.tracker-subtext {
font-size: 11px;
opacity: 0.7;
margin-top: 4px;
}
/* STATS */
.tracker-stats div {
display: flex;
justify-content: space-between;
font-size: 12px;
margin: 2px 0;
}
.tracker-stats span:first-child {
opacity: 0.6;
}
.tracker-stats {
margin-top: 70px;
box-sizing: border-box;
}
/* STATUS */
.tracker-status {
display: flex;
justify-content: space-between;
font-size: 12px;
margin-top: 6px;
box-sizing: border-box;
}
/* CIRCLE PROGRESS */
/* top was 34px vs .tracker-card; header is now the containing block (scroll fix), so pull up to match old card-space alignment */
.tracker-circle {
position: absolute;
top: -10px;
/* Flush with content edge: card padding is symmetric; extra right:12px made the right gap larger than the left */
right: 0;
width: 80px;
height: 80px;
}
.tracker-circle-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 16px;
font-weight: 600;
color: #fff;
font-family: 'Segoe UI', 'Inter', sans-serif;
text-shadow:
0 0 4px rgba(76, 175, 80, 0.6),
0 0 10px rgba(124, 255, 124, 0.4);
}
.tracker-xp-text {
position: absolute;
top: 88px;
right: 0;
left: auto;
transform: none;
text-align: right;
font-size: 11px;
opacity: 0.8;
white-space: nowrap;
}
/* Daily wins bar: full content width (same horizontal inset as left via card padding) */
.be-dashboard-daily-bar {
margin: 4px 0 6px 0;
height: 4px;
background: #1a2235;
border-radius: 4px;
overflow: hidden;
box-sizing: border-box;
}
/* smooth animation */
#xpCircle {
transition: stroke-dashoffset 0.4s ease;
}
/* SIDEBAR — compact HUD chips (pairs with widget hub tiles) */
.tracker-sidebar {
position: absolute;
top: 0;
left: 0;
width: 128px;
height: 100%;
background: rgba(6, 10, 18, 0.72);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-right: 1px solid rgba(255, 170, 0, 0.22);
box-shadow: 4px 0 18px rgba(0, 0, 0, 0.45);
transform: translateX(-100%);
transition: transform 0.25s ease;
z-index: 999999998;
}
.tracker-sidebar.open {
transform: translateX(0);
}
.tracker-sidebar-content.be-side-grid {
padding: 8px 8px 10px;
font-family: "Segoe UI", system-ui, sans-serif;
color: #e8f0ff;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
align-content: start;
}
.be-side-nav-title {
grid-column: 1 / -1;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.28em;
margin: 4px 2px 8px;
opacity: 0.55;
color: rgba(255, 210, 160, 0.95);
}
.sidebar-item.be-side-item {
display: block;
padding: 0;
margin: 0;
border-radius: 0;
color: inherit;
}
.be-side-chip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
min-height: 52px;
padding: 6px 4px 5px;
border-radius: 10px;
background: linear-gradient(165deg, rgba(18, 28, 48, 0.95), rgba(8, 12, 22, 0.98));
border: 1px solid rgba(100, 140, 200, 0.22);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.06),
0 0 0 1px rgba(0, 0, 0, 0.35);
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
}
.be-side-glyph {
display: flex;
align-items: center;
justify-content: center;
color: var(--be-side-accent, #8ab4ff);
filter: drop-shadow(0 0 5px color-mix(in srgb, var(--be-side-accent) 55%, transparent));
}
.be-side-abbr {
font-size: 8px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.82;
color: rgba(220, 232, 255, 0.88);
line-height: 1;
}
.be-side--dashboard { --be-side-accent: #5ea2ff; }
.be-side--graph { --be-side-accent: #c084fc; }
.be-side--stats { --be-side-accent: #38bdf8; }
.be-side--calc { --be-side-accent: #fbbf24; }
.be-side--social { --be-side-accent: #f472b6; }
.be-side--visuals { --be-side-accent: #34d399; }
.be-side--settings { --be-side-accent: #94a3b8; }
.be-side-item.be-side--settings {
grid-column: 1 / -1;
}
.be-side-item.be-side--settings .be-side-chip {
flex-direction: row;
justify-content: center;
gap: 10px;
min-height: 46px;
}
.be-side-item.be-side--settings .be-side-abbr {
font-size: 9px;
}
.sidebar-item.be-side-item:hover .be-side-chip {
border-color: color-mix(in srgb, var(--be-side-accent) 55%, rgba(255,255,255,0.2));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.1),
0 0 12px color-mix(in srgb, var(--be-side-accent) 35%, transparent);
}
.sidebar-item.be-side-item.active .be-side-chip {
border-color: var(--be-side-accent);
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--be-side-accent) 65%, transparent),
0 0 14px color-mix(in srgb, var(--be-side-accent) 45%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
.sidebar-item.be-side-item.active .be-side-abbr {
color: color-mix(in srgb, var(--be-side-accent) 92%, #fff);
opacity: 1;
}
.sidebar-item.be-side-item:active .be-side-chip {
transform: scale(0.97);
}
#winTrackerBox[data-size="tiny"] .be-side-abbr {
display: none;
}
#winTrackerBox[data-size="tiny"] .be-side-chip {
min-height: 44px;
padding: 5px 2px;
}
#winTrackerBox[data-size="tiny"] .tracker-sidebar {
width: 112px;
}
#winTrackerBox[data-size="large"] .tracker-sidebar {
width: 144px;
}
#winTrackerBox[data-size="large"] .be-side-chip {
min-height: 58px;
gap: 5px;
}
#winTrackerBox[data-size="large"] .be-side-glyph svg {
width: 22px;
height: 22px;
}
.sidebar-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
opacity: 0.8;
}
.sidebar-item:not(.be-side-item) {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 8px;
margin-bottom: 6px;
cursor: pointer;
transition: background 0.15s ease, transform 0.1s ease;
color: rgba(255,255,255,0.85);
}
.sidebar-item:not(.be-side-item):hover {
background: rgba(255, 255, 255, 0.08);
}
.sidebar-item .icon {
width: 18px;
text-align: center;
opacity: 0.8;
}
.sidebar-item:not(.be-side-item).active {
background: rgba(76, 175, 80, 0.15);
color: #7cff7c;
}
.sidebar-item:not(.be-side-item).active .icon {
opacity: 1;
}
.sidebar-item:not(.be-side-item):active {
transform: scale(0.96);
}
.tracker-topbar {
position: absolute;
top: 0;
left: 0;
width: 100%;
min-height: 44px;
height: auto;
box-sizing: border-box;
display: flex;
align-items: center;
padding: 7px 0 7px 10px;
border-bottom: 1px solid rgba(255,255,255,0.08);
background: rgba(10, 18, 32, 0.6);
backdrop-filter: blur(4px);
cursor: grab;
z-index: 2;
}
#beOverlayTitleBlock {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 5px;
padding-left: 2px;
padding-right: 8px;
font-family: "Segoe UI", system-ui, sans-serif;
}
#beOverlayTitleBlock .be-overlay-title-main {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.06em;
color: rgba(255, 255, 255, 0.94);
line-height: 1.22;
}
#beOverlayTitleBlock .be-overlay-title-meta {
font-size: 9.5px;
font-weight: 400;
letter-spacing: 0.02em;
color: rgba(255, 255, 255, 0.52);
line-height: 1.4;
white-space: nowrap;
}
#beOverlayTitleBlock .be-overlay-title-version {
color: #d4b44a;
font-weight: 500;
}
.tracker-menu-icon {
width: 24px;
height: 24px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 3px;
border-radius: 6px; /* 🔥 rounded square */
transition: background 0.15s ease;
cursor: pointer;
}
.tracker-menu-icon:hover {
background: rgba(255, 255, 255, 0.08);
}
.tracker-menu-icon:active {
background: rgba(255, 255, 255, 0.14);
}
.tracker-menu-icon div {
width: 14px;
height: 2px;
background: #ffffff;
border-radius: 2px;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(6px); }
to { opacity: 1; transform: translateX(0); }
}
/* 🔥 FADE ANIMATION */
.tracker-fade {
opacity: 0;
transition: opacity 0.25s ease;
}
.tracker-visible {
opacity: 1;
transition: opacity 0.25s ease;
}
/* 🔥 HEADER LAYOUT */
.tracker-header-row {
display: flex;
align-items: center;
gap: 10px;
}
/* 🔥 NAME + LEVEL STACK */
.tracker-name-group {
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
flex: 1;
/* Keep text out from under the absolute-positioned XP ring */
padding-right: 76px;
}
/* 🔥 SKIN BOX */
.tracker-skin {
width: 38px;
height: 38px;
border-radius: 50%; /* 🔥 make it circular like bonk */
background: transparent;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* 🔥 SKIN IMAGE */
.tracker-skin img {
width: 100%;
height: 100%;
object-fit: contain;
}
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
margin: 2px 0;
}
.stat-label {
display: flex;
align-items: center;
gap: 6px;
opacity: 0.8;
}
.stat-icon {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
opacity: 0.7;
}
.stat-icon svg {
width: 100%;
height: 100%;
stroke: currentColor;
}
/* 🔥 RESIZE HANDLE */
.tracker-resize {
position: absolute;
bottom: 6px;
right: 6px;
width: 18px;
height: 18px;
opacity: 0.25;
cursor: nwse-resize;
}
.tracker-resize:hover {
opacity: 0.8;
}
/* Calculator — same panels & inputs as Settings (settings-group) */
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-row {
display: flex;
align-items: center;
gap: 8px;
margin: 6px 0;
font-size: 11px;
flex-wrap: wrap;
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-row > span:first-child {
min-width: 88px;
opacity: 0.85;
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-pill {
flex: 1;
min-width: 0;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 8px;
background: rgba(0, 0, 0, 0.22);
color: inherit;
padding: 6px 10px;
font-size: 12px;
outline: none;
font-family: "Segoe UI", sans-serif;
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-pill:focus {
border-color: rgba(120, 180, 255, 0.5);
box-shadow: 0 0 0 1px rgba(120, 180, 255, 0.12);
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-pill::placeholder {
color: rgba(200, 210, 230, 0.45);
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-out {
font-size: 12px;
font-weight: 600;
color: rgba(160, 205, 255, 0.95);
word-break: break-word;
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-split {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
align-items: end;
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-split .be-calc-pill {
width: 100%;
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-row-stack {
flex-direction: column;
align-items: stretch;
gap: 6px;
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-row-stack > span:first-child {
min-width: 0;
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-inline {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-inline .be-calc-pill {
flex: 1;
min-width: 100px;
max-width: 220px;
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-goal-result {
margin-top: 10px;
font-size: 12px;
font-weight: 600;
line-height: 1.4;
color: rgba(160, 205, 255, 0.92);
}
#winTrackerBox .tracker-view[data-view="calculator"] .be-calc-label-sm {
font-size: 10px;
opacity: 0.65;
margin-bottom: 2px;
}
#winTrackerBox.be-theme-light .tracker-view[data-view="calculator"] .be-calc-pill {
background: rgba(255, 255, 255, 0.92);
color: #1a2740;
border-color: rgba(77, 108, 159, 0.35);
}
#winTrackerBox.be-theme-light .tracker-view[data-view="calculator"] .be-calc-pill:focus {
border-color: rgba(89, 138, 221, 0.55);
box-shadow: 0 0 0 1px rgba(89, 138, 221, 0.2);
}
#winTrackerBox.be-theme-light .tracker-view[data-view="calculator"] .be-calc-out,
#winTrackerBox.be-theme-light .tracker-view[data-view="calculator"] .be-calc-goal-result {
color: #1e5688;
}
.tracker-view {
display: none;
width: 100%;
min-width: 0;
box-sizing: border-box;
flex: 1 1 0%;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
/* Invisible scrollbar but keep a real scroll lane (display:none / scrollbar-width:none breaks wheel scroll in some Chromium builds) */
scrollbar-width: thin;
scrollbar-color: transparent transparent;
-ms-overflow-style: none;
padding-right: 2px;
}
.tracker-view.active {
display: flex;
flex-direction: column;
}
.tracker-view::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.tracker-view::-webkit-scrollbar-track {
background: transparent;
}
.tracker-view::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 5px;
}
/* Nested panels: same — transparent thumb, functional scroll */
.stats-history,
#beScriptHealthOut {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
-ms-overflow-style: none;
}
.stats-history::-webkit-scrollbar,
#beScriptHealthOut::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.stats-history::-webkit-scrollbar-track,
#beScriptHealthOut::-webkit-scrollbar-track {
background: transparent;
}
.stats-history::-webkit-scrollbar-thumb,
#beScriptHealthOut::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 5px;
}
.tracker-view.fade-in {
animation: trackerFadeIn 0.22s ease;
}
@keyframes trackerFadeIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tracker-topbar-btn {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
opacity: 0.7;
transition: background 0.15s ease, opacity 0.15s ease;
margin-right: 2px;
}
.tracker-topbar-btn:hover {
background: rgba(255,255,255,0.1);
opacity: 1;
}
.tracker-topbar-btn:active {
background: rgba(255,255,255,0.18);
}
.settings-group {
margin-bottom: 12px;
padding: 10px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.04);
font-family: "Segoe UI", sans-serif;
}
.settings-title {
font-size: 13px;
font-weight: 600;
opacity: 0.9;
margin-bottom: 8px;
}
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: 6px 0;
font-size: 12px;
}
.settings-row select,
.settings-row button {
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(13, 20, 33, 0.9);
color: #fff;
border-radius: 6px;
padding: 4px 8px;
font-size: 12px;
cursor: pointer;
}
.settings-row button:hover {
background: rgba(255, 255, 255, 0.1);
}
.settings-row input[type="range"] {
flex: 1;
min-width: 0;
accent-color: rgba(120, 180, 255, 0.85);
}
/* UI presets: grid so preset name input and select share the same column width (match each other) */
.be-ui-presets-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
column-gap: 8px;
row-gap: 8px;
min-width: 0;
margin: 6px 0;
}
.be-ui-presets-grid #bePresetName {
min-width: 0;
}
.be-ui-presets-grid #bePresetSelect {
min-width: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(13, 20, 33, 0.9);
color: #fff;
font-size: 12px;
cursor: pointer;
padding: 6px 28px 6px 8px;
line-height: 1.25;
border-radius: 8px;
}
.be-ui-presets-load-del {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
/* Fullscreen scaling (top bar): single full-width select under the section title */
#winTrackerBox .settings-row.be-fs-scale-row {
flex-direction: column;
align-items: stretch;
gap: 0;
margin-top: 2px;
}
#winTrackerBox #beFullscreenScaleModeSelect {
width: 100%;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
flex: none;
}
/* Visuals tab — single card, denser layout */
#winTrackerBox .be-visuals-panel {
padding: 8px 10px 10px;
opacity: 0.9;
font-family: "Segoe UI", sans-serif;
}
#winTrackerBox .be-visuals-panel .settings-group {
margin-bottom: 0;
padding: 6px 8px;
}
#winTrackerBox .be-visuals-panel .settings-title {
font-size: 12px;
margin-bottom: 6px;
}
#winTrackerBox .be-visuals-panel .be-visuals-sub + .be-visuals-sub {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
#winTrackerBox.be-theme-light .be-visuals-panel .be-visuals-sub + .be-visuals-sub {
border-top-color: rgba(30, 50, 80, 0.12);
}
#winTrackerBox .be-visuals-panel .be-visuals-subtitle {
font-size: 11px;
font-weight: 600;
opacity: 0.82;
margin-bottom: 4px;
letter-spacing: 0.02em;
}
#winTrackerBox .be-visuals-panel .settings-row {
margin: 2px 0;
gap: 6px;
font-size: 11px;
}
#winTrackerBox .be-visuals-panel .be-visuals-slider-row {
flex-direction: column;
align-items: stretch;
gap: 3px;
margin-top: 1px;
}
#winTrackerBox .be-visuals-panel .be-visuals-slider-row > span {
opacity: 0.82;
font-size: 10px;
}
.dashboard-stat-hidden {
display: none !important;
}
.be-gain-flash {
animation: beGainFlash 0.45s ease;
}
@keyframes beGainFlash {
0% { filter: brightness(1); transform: scaleY(1); }
50% { filter: brightness(1.35); transform: scaleY(1.08); }
100% { filter: brightness(1); transform: scaleY(1); }
}
.stats-wrap {
padding: 14px;
font-family: "Segoe UI", sans-serif;
}
.stats-controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
gap: 8px;
}
.stats-controls select {
border: 1px solid rgba(255,255,255,0.2);
background: rgba(14, 22, 36, 0.9);
color: #fff;
border-radius: 6px;
padding: 4px 8px;
font-size: 12px;
}
.stats-controls input[type="week"] {
border: 1px solid rgba(255,255,255,0.2);
background: rgba(14, 22, 36, 0.9);
color: #fff;
border-radius: 6px;
padding: 4px 8px;
font-size: 12px;
}
.stats-chart {
height: 110px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(5, 12, 24, 0.5);
display: flex;
align-items: end;
gap: 3px;
padding: 8px;
margin-bottom: 10px;
}
.stats-bar {
flex: 1;
min-width: 6px;
border-radius: 4px 4px 2px 2px;
background: linear-gradient(180deg, #72b1ff, #4e8fe8);
}
.stats-bar.wins {
background: linear-gradient(180deg, #7cff7c, #44b464);
opacity: 0.85;
}
.stats-legend {
font-size: 11px;
opacity: 0.8;
margin-bottom: 10px;
}
.stats-history {
font-size: 11px;
max-height: 110px;
overflow: auto;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.08);
padding: 6px 8px;
background: rgba(8, 16, 29, 0.5);
}
.stats-history-item {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 3px 0;
}
/* Accounts tab list — avoid crushed one-letter lines when panel is narrow */
#beAltsTableWrap {
container-type: inline-size;
container-name: be-alts;
}
#beAltsTableWrap .stats-history-item.be-alt-row {
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-start;
gap: 8px 10px;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
#beAltsTableWrap .stats-history-item.be-alt-row:last-child {
border-bottom: none;
}
#beAltsTableWrap .be-alt-row-main {
flex: 1 1 200px;
min-width: min(100%, 11rem);
max-width: 100%;
}
#beAltsTableWrap .be-alt-row-main b {
word-break: normal;
overflow-wrap: anywhere;
}
#beAltsTableWrap .be-alt-row-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
flex: 0 1 auto;
margin-left: auto;
}
@container be-alts (max-width: 300px) {
#beAltsTableWrap .be-alt-row-main {
flex: 1 1 100%;
min-width: 0;
width: 100%;
}
#beAltsTableWrap .be-alt-row-actions {
margin-left: 0;
width: 100%;
justify-content: flex-start;
}
}
.pulse-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: #4caf50;
margin-right: 5px;
vertical-align: middle;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); }
70% { box-shadow: 0 0 0 5px rgba(76, 175, 80, 0); }
100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
}
#winTrackerBox[data-size="tiny"] .tracker-card {
width: 210px;
min-height: 138px;
padding-top: 50px;
max-height: min(620px, 88vh) !important;
}
#winTrackerBox[data-size="tiny"] .tracker-circle {
top: -7px;
transform: scale(0.88);
transform-origin: top right;
}
#winTrackerBox[data-size="tiny"] .tracker-header {
margin-top: 10px;
margin-bottom: 8px;
}
#winTrackerBox[data-size="tiny"] .tracker-name-group {
padding-right: 64px;
}
#winTrackerBox[data-size="tiny"] .tracker-name.tracker-name--scroll {
font-size: 14px;
}
#winTrackerBox[data-size="tiny"] .tracker-name {
font-size: 15px;
}
#winTrackerBox[data-size="tiny"] .tracker-level,
#winTrackerBox[data-size="tiny"] .stat-row {
font-size: 11px;
}
#winTrackerBox[data-size="tiny"] .tracker-stats {
margin-top: 54px;
}
#winTrackerBox[data-size="large"] .tracker-card {
width: 290px;
min-height: 190px;
max-height: min(820px, 92vh) !important;
}
#winTrackerBox[data-size="large"] .tracker-header {
margin-top: 18px;
}
#winTrackerBox[data-size="large"] .tracker-circle {
top: -14px;
}
#winTrackerBox[data-size="large"] .tracker-name-group {
padding-right: 82px;
}
#winTrackerBox[data-size="large"] .tracker-name.tracker-name--scroll {
font-size: 18px;
}
#winTrackerBox[data-size="large"] .tracker-name {
font-size: 20px;
}
#winTrackerBox[data-size="large"] .tracker-level,
#winTrackerBox[data-size="large"] .stat-row {
font-size: 13px;
}
#winTrackerBox[data-size="large"] .tracker-stats {
margin-top: 12px;
}
#winTrackerBox.be-theme-light .tracker-card {
background: linear-gradient(180deg, #e8eef9, #dce6f5 72%, #d8e2f3);
color: #1b2a40;
border-color: rgba(77, 104, 148, 0.45);
box-shadow: 0 10px 24px rgba(33, 54, 92, 0.14), inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
#winTrackerBox.be-theme-light .tracker-topbar {
background: rgba(215, 226, 246, 0.92);
border-bottom-color: rgba(70, 98, 142, 0.22);
}
#winTrackerBox.be-theme-light #beOverlayTitleBlock .be-overlay-title-main {
color: rgba(22, 38, 62, 0.94);
}
#winTrackerBox.be-theme-light #beOverlayTitleBlock .be-overlay-title-meta {
color: rgba(30, 50, 85, 0.52);
}
#winTrackerBox.be-theme-light #beOverlayTitleBlock .be-overlay-title-version {
color: #7a6220;
}
#winTrackerBox.be-theme-light .tracker-menu-icon div {
background: #304c78;
}
#winTrackerBox.be-theme-light .tracker-name,
#winTrackerBox.be-theme-light .tracker-circle-text,
#winTrackerBox.be-theme-light .tracker-level,
#winTrackerBox.be-theme-light .tracker-xp-text,
#winTrackerBox.be-theme-light .tracker-status {
color: #223657;
text-shadow: none;
}
#winTrackerBox.be-theme-light .tracker-sidebar {
background: linear-gradient(180deg, rgba(230, 238, 252, 0.97), rgba(218, 228, 246, 0.98));
color: #1a2f4d;
border-right-color: rgba(200, 155, 80, 0.35);
}
#winTrackerBox.be-theme-light .be-side-chip {
background: linear-gradient(165deg, rgba(255, 255, 255, 0.92), rgba(230, 238, 250, 0.95));
border-color: rgba(77, 108, 159, 0.28);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85), 0 1px 3px rgba(30, 50, 90, 0.12);
}
#winTrackerBox.be-theme-light .be-side-abbr {
color: rgba(30, 50, 85, 0.88);
}
#winTrackerBox.be-theme-light .be-side-nav-title {
color: rgba(120, 90, 40, 0.75);
}
#winTrackerBox.be-theme-light .sidebar-item:not(.be-side-item) {
color: rgba(29, 50, 80, 0.92);
}
#winTrackerBox.be-theme-light .sidebar-item:not(.be-side-item):hover,
#winTrackerBox.be-theme-light .tracker-topbar-btn:hover {
background: rgba(81, 117, 173, 0.18);
}
#winTrackerBox.be-theme-light .sidebar-item:not(.be-side-item).active {
background: rgba(73, 108, 164, 0.26);
color: #173b70;
}
#winTrackerBox.be-theme-light .tracker-bar {
background: rgba(126, 148, 186, 0.28);
}
#winTrackerBox.be-theme-light .settings-group {
background: linear-gradient(180deg, rgba(123, 154, 205, 0.18), rgba(123, 154, 205, 0.12));
border: 1px solid rgba(98, 127, 176, 0.22);
}
#winTrackerBox.be-theme-light .settings-row select,
#winTrackerBox.be-theme-light .settings-row button,
#winTrackerBox.be-theme-light .be-ui-presets-grid #bePresetSelect {
background: rgba(245, 250, 255, 0.9);
color: #1f3355;
border-color: rgba(77, 108, 159, 0.35);
}
#winTrackerBox.be-theme-light .settings-row button:hover {
background: rgba(232, 241, 255, 0.95);
}
#winTrackerBox.be-theme-light .tracker-stats span:first-child,
#winTrackerBox.be-theme-light .tracker-level,
#winTrackerBox.be-theme-light .tracker-subtext,
#winTrackerBox.be-theme-light .stat-label,
#winTrackerBox.be-theme-light .sidebar-title {
color: rgba(33, 54, 85, 0.75);
}
#winTrackerBox.be-theme-light .tracker-topbar-btn,
#winTrackerBox.be-theme-light .tracker-topbar svg,
#winTrackerBox.be-theme-light #topbarClose {
color: #24406a;
fill: #24406a;
opacity: 0.85;
}
#winTrackerBox.be-theme-light #xpCircleWrapper circle:first-child {
stroke: rgba(109, 131, 168, 0.35);
}
#winTrackerBox.be-theme-light #dailyProgressBar {
background: linear-gradient(90deg, #4e8fe8, #7ec3ff) !important;
}
#winTrackerBox.be-theme-light .be-dashboard-daily-bar {
background: rgba(200, 212, 235, 0.55);
}
#winTrackerBox.be-theme-light .stats-controls select {
background: rgba(245, 250, 255, 0.92);
color: #1f3355;
border-color: rgba(77, 108, 159, 0.35);
}
#winTrackerBox.be-theme-light .stats-controls input[type="week"] {
background: rgba(245, 250, 255, 0.92);
color: #1f3355;
border-color: rgba(77, 108, 159, 0.35);
}
#winTrackerBox.be-theme-light .stats-chart,
#winTrackerBox.be-theme-light .stats-history {
background: rgba(236, 244, 255, 0.72);
border-color: rgba(84, 111, 154, 0.22);
}
#winTrackerBox.be-theme-light ~ .be-overlay-launcher {
color: #193965;
background: linear-gradient(180deg, rgba(234, 242, 255, 0.96), rgba(214, 228, 248, 0.96));
border-color: rgba(79, 112, 167, 0.42);
box-shadow: 0 8px 18px rgba(46, 67, 105, 0.2);
}
#winTrackerBox.be-theme-light ~ .be-widget-hub {
background: rgba(234, 241, 253, 0.55);
}
#winTrackerBox.be-theme-light ~ .be-widget-hub .be-widget-hub-panel {
background: linear-gradient(180deg, rgba(234, 242, 255, 0.98), rgba(221, 232, 250, 0.98));
border-color: rgba(88, 118, 168, 0.36);
}
#winTrackerBox.be-theme-light ~ .be-widget-hub .be-widget-hub-head {
color: #1f385c;
}
`;
document.head.appendChild(style);
div.style.position = "fixed";
// Default position; saved coords applied after mount (see beClampOverlayToViewport).
div.style.top = "120px";
div.style.right = "20px";
div.style.color = "white";
div.style.fontFamily = "monospace";
div.style.zIndex = "999999999";
div.innerHTML = `
<div class="tracker-card">
<div id="trackerSidebar" class="tracker-sidebar">
<div class="tracker-sidebar-content be-side-grid">
<div class="sidebar-title be-side-nav-title">NAV</div>
<div class="sidebar-item be-side-item be-side--dashboard active" data-tab="dashboard" title="Dashboard — main tracker panel">
<div class="be-side-chip">
<span class="be-side-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20" fill="none"><path d="M4 10.5L12 4l8 6.5V20a1 1 0 0 1-1 1h-5v-6H10v6H5a1 1 0 0 1-1-1v-9.5z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg></span>
<span class="be-side-abbr">Main</span>
</div>
</div>
<div class="sidebar-item be-side-item be-side--graph" data-tab="graph" title="Graph — weekly XP & wins">
<div class="be-side-chip">
<span class="be-side-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20" fill="none"><path d="M5 19V5m4 14V9m4 10v-6m4 6v-3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>
<span class="be-side-abbr">Graph</span>
</div>
</div>
<div class="sidebar-item be-side-item be-side--stats" data-tab="statsOverview" title="Stats overview">
<div class="be-side-chip">
<span class="be-side-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20" fill="none"><path d="M8 6h12M8 12h12M8 18h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="4" y="4" width="14" height="16" rx="2" stroke="currentColor" stroke-width="1.5"/></svg></span>
<span class="be-side-abbr">Stats</span>
</div>
</div>
<div class="sidebar-item be-side-item be-side--calc" data-tab="calculator" title="Calculator — XP / level / goals">
<div class="be-side-chip">
<span class="be-side-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20" fill="none"><rect x="5" y="3" width="14" height="18" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M8 8h8M9 12h1.5v1.5H9V15M13 12h2M8 17h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg></span>
<span class="be-side-abbr">Calc</span>
</div>
</div>
<div class="sidebar-item be-side-item be-side--social" data-tab="social" title="Accounts — vault & friends">
<div class="be-side-chip">
<span class="be-side-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20" fill="none"><circle cx="9" cy="8" r="2.5" stroke="currentColor" stroke-width="1.5"/><path d="M4 19v-1a4 4 0 0 1 4-4h2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="17" cy="9" r="2" stroke="currentColor" stroke-width="1.5"/><path d="M21 19v0a4 4 0 0 0-3-3.87" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>
<span class="be-side-abbr">Acct</span>
</div>
</div>
<div class="sidebar-item be-side-item be-side--visuals" data-tab="visuals" title="Visuals — players & chat">
<div class="be-side-chip">
<span class="be-side-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20" fill="none"><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5"/><path d="M3 12h0a9 9 0 0 1 9-9h0a9 9 0 0 1 9 9h0M3 12a9 9 0 0 0 9 9M21 12a9 9 0 0 1-9 9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg></span>
<span class="be-side-abbr">View</span>
</div>
</div>
<div class="sidebar-item be-side-item be-side--settings" data-tab="settings" title="Settings">
<div class="be-side-chip">
<span class="be-side-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20" fill="none"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z" stroke="currentColor" stroke-width="1.4"/><path d="M19.4 15a1.8 1.8 0 0 0 .36 1.99l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.8 1.8 0 0 0-1.99-.36 1.8 1.8 0 0 0-1 1.64V21a2 2 0 1 1-4 0v-.09a1.8 1.8 0 0 0-1-1.64 1.8 1.8 0 0 0-1.99.36l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.8 1.8 0 0 0 .36-1.99 1.8 1.8 0 0 0-1.64-1h-.09a2 2 0 1 1 0-4h.09a1.8 1.8 0 0 0 1.64-1 1.8 1.8 0 0 0-.36-1.99l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.8 1.8 0 0 0 1.99.36h.09a1.8 1.8 0 0 0 1-1.64V3a2 2 0 1 1 4 0v.09a1.8 1.8 0 0 0 1 1.64h.09a1.8 1.8 0 0 0 1.99-.36l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.8 1.8 0 0 0-.36 1.99v.09a1.8 1.8 0 0 0 1.64 1H21a2 2 0 1 1 0 4h-.09a1.8 1.8 0 0 0-1.64 1z" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" opacity="0.9"/></svg></span>
<span class="be-side-abbr">Set</span>
</div>
</div>
</div>
</div>
<div class="tracker-topbar" id="dragHandle">
<div class="tracker-menu-icon">
<div></div>
<div></div>
<div></div>
</div>
<span id="beOverlayTitleBlock">
<span class="be-overlay-title-main">BONK ENHANCED</span>
<span class="be-overlay-title-meta">By ${BE_SCRIPT_AUTHOR}<span aria-hidden="true"> · </span><span class="be-overlay-title-version">v${BE_SCRIPT_VERSION}</span></span>
</span>
<div id="topbarSettings" class="tracker-topbar-btn" title="Settings">
<svg viewBox="0 0 24 24" width="13" height="13" fill="white" opacity="0.8">
<path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.92c.04-.34.07-.69.07-1.08s-.03-.74-.07-1.08l2.32-1.84c.21-.16.27-.45.13-.69l-2.2-3.84c-.14-.23-.42-.32-.66-.23l-2.74 1.1c-.57-.44-1.18-.81-1.86-1.08l-.42-2.93A.54.54 0 0 0 14 2h-4c-.27 0-.5.19-.54.46l-.42 2.93c-.68.27-1.29.64-1.86 1.08L4.44 5.37c-.24-.09-.52 0-.66.23L1.58 9.44c-.14.24-.08.53.13.69l2.32 1.84C4.03 12.26 4 12.61 4 13s.03.74.07 1.08L1.71 15.92c-.21.16-.27.45-.13.69l2.2 3.84c.14.23.42.32.66.23l2.74-1.1c.57.44 1.18.81 1.86 1.08l.42 2.93c.04.27.27.46.54.46h4c.27 0 .5-.19.54-.46l.42-2.93c.68-.27 1.29-.64 1.86-1.08l2.74 1.1c.24.09.52 0 .66-.23l2.2-3.84c.14-.24.08-.53-.13-.69l-2.32-1.84z"/>
</svg>
</div>
<div id="topbarClose" class="tracker-topbar-btn" title="Hide overlay" style="margin-right:10px;">✕</div>
</div>
<!-- DASHBOARD -->
<div class="tracker-view active" data-view="dashboard">
<div class="tracker-section tracker-header">
<div class="tracker-header-row">
<div id="playerSkin" class="tracker-skin"></div>
<div class="tracker-name-group">
<div id="playerName" class="tracker-name"><span class="tracker-name-text"></span></div>
<div class="tracker-level" id="levelLine">Level 0</div>
</div>
</div>
<div class="tracker-circle" id="xpCircleWrapper">
<svg width="80" height="80">
<defs>
<linearGradient id="xpGradient" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#2e7d32"/>
<stop offset="100%" stop-color="#7cff7c"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<circle cx="40" cy="40" r="34" stroke="#1a2235" stroke-width="7" fill="none"/>
<circle id="xpCircle"
filter="url(#glow)"
cx="40" cy="40" r="34"
stroke="url(#xpGradient)"
stroke-width="7"
fill="none"
stroke-linecap="round"
transform="rotate(-90 40 40)"
stroke-dasharray="213.6"
stroke-dashoffset="213.6"
/>
</svg>
<div id="xpPercent" class="tracker-circle-text">0%</div>
<div id="xpText" class="tracker-xp-text">0 / 0 XP</div>
</div>
<div class="tracker-section tracker-stats" id="dashboardStatsRows">
<div class="stat-row" id="rowXp">
<span class="stat-label">
<span class="stat-icon">⚡</span>XP
</span>
<span id="xpGain">+0</span>
</div>
<div class="stat-row" id="rowWins">
<span class="stat-label">
<span class="stat-icon">🏆</span>Daily wins
</span>
<span id="winsLine">0</span>
</div>
<div class="stat-row" id="rowRate">
<span class="stat-label">
<span class="stat-icon">📈</span>Rate
</span>
<span id="rateLine">0</span>
</div>
<div class="stat-row" id="rowLastWin">
<span class="stat-label">
<span class="stat-icon">🕒</span>Last win
</span>
<span id="lastWinLine">--</span>
</div>
<div class="stat-row" id="rowSession">
<span class="stat-label">
<span class="stat-icon">⏱</span>Session
</span>
<span id="sessionStatLine">0:00</span>
</div>
<div class="stat-row" id="rowLevel">
<span class="stat-label">
<span class="stat-icon">⭐</span>Level
</span>
<span id="levelStatLine">0</span>
</div>
</div>
<div class="be-dashboard-daily-bar">
<div id="dailyProgressBar" style="height: 100%; width: 0%; background: linear-gradient(90deg, #4caf50, #7cff7c); border-radius: 4px; transition: width 0.4s ease;"></div>
</div>
<div class="tracker-section tracker-status">
<span id="sessionLine">⏱ 0:00</span>
<span id="statusText" style="display:none;"></span>
</div>
</div>
</div> <!-- ✅ DASHBOARD CLOSED PROPERLY -->
<!-- GRAPH -->
<div class="tracker-view" data-view="graph">
<div class="stats-wrap">
<div class="stats-controls">
<span>Week</span>
<input id="beGraphWeekSelect" type="week">
<button type="button" id="beExportStatsCsv" style="cursor:pointer;padding:4px 10px;border-radius:8px;border:1px solid rgba(120,180,255,0.45);background:rgba(25,45,80,0.55);color:#e8f0ff;font:600 11px 'Segoe UI',sans-serif;">Export CSV</button>
</div>
<div id="beStatsChart" class="stats-chart"></div>
<div id="beStatsLegend" class="stats-legend">XP and wins trend</div>
<div style="margin-bottom:6px;font-size:12px;opacity:0.85;">Last 10 sessions</div>
<div id="beSessionHistory" class="stats-history"></div>
</div>
</div>
<!-- STATS OVERVIEW -->
<div class="tracker-view" data-view="statsOverview">
<div class="stats-wrap">
<div id="beOverviewSizeHint" style="margin-bottom:8px;font-size:11px;opacity:0.8;">Current size: Medium (4 slots)</div>
<div class="settings-group">
<div class="settings-title">Dashboard Rows (slot layout)</div>
<div id="beOverviewSlots">
<div class="settings-row"><span>Slot 1</span><select data-slot-idx="0"><option value="">None</option><option value="xp">XP</option><option value="wins">Daily wins</option><option value="rate">Rate</option><option value="lastWin">Last win</option><option value="session">Session</option><option value="level">Level</option></select></div>
<div class="settings-row"><span>Slot 2</span><select data-slot-idx="1"><option value="">None</option><option value="xp">XP</option><option value="wins">Daily wins</option><option value="rate">Rate</option><option value="lastWin">Last win</option><option value="session">Session</option><option value="level">Level</option></select></div>
<div class="settings-row"><span>Slot 3</span><select data-slot-idx="2"><option value="">None</option><option value="xp">XP</option><option value="wins">Daily wins</option><option value="rate">Rate</option><option value="lastWin">Last win</option><option value="session">Session</option><option value="level">Level</option></select></div>
<div class="settings-row"><span>Slot 4</span><select data-slot-idx="3"><option value="">None</option><option value="xp">XP</option><option value="wins">Daily wins</option><option value="rate">Rate</option><option value="lastWin">Last win</option><option value="session">Session</option><option value="level">Level</option></select></div>
<div class="settings-row"><span>Slot 5</span><select data-slot-idx="4"><option value="">None</option><option value="xp">XP</option><option value="wins">Daily wins</option><option value="rate">Rate</option><option value="lastWin">Last win</option><option value="session">Session</option><option value="level">Level</option></select></div>
<div class="settings-row"><span>Slot 6</span><select data-slot-idx="5"><option value="">None</option><option value="xp">XP</option><option value="wins">Daily wins</option><option value="rate">Rate</option><option value="lastWin">Last win</option><option value="session">Session</option><option value="level">Level</option></select></div>
</div>
</div>
<div style="margin-bottom:8px;font-size:12px;opacity:0.85;">All tracked stats</div>
<div id="beStatsOverview" class="stats-history"></div>
</div>
</div>
<!-- CALCULATOR (matches Settings-style panels) -->
<div class="tracker-view" data-view="calculator">
<div style="padding:16px;opacity:0.9;font-family:'Segoe UI',sans-serif;">
<div class="settings-group">
<div class="settings-title">XP / Level</div>
<p style="font-size:11px;opacity:0.75;margin:0 0 8px 0;line-height:1.35;">Bonk uses total cumulative XP. Min XP to be at level L is (L−1)²×100.</p>
<div class="be-calc-row be-calc-row-stack">
<span>Level → XP:</span>
<div class="be-calc-inline">
<input class="be-calc-pill" id="beCalcLv2XpIn" type="number" inputmode="numeric" min="1" placeholder="Level" />
<span class="be-calc-out" id="beCalcLv2XpOut">—</span>
</div>
</div>
<div class="be-calc-row be-calc-row-stack">
<span>XP → Level:</span>
<div class="be-calc-inline">
<input class="be-calc-pill" id="beCalcXp2LvIn" type="number" inputmode="numeric" min="0" placeholder="XP" />
<span class="be-calc-out" id="beCalcXp2LvOut">—</span>
</div>
</div>
</div>
<div class="settings-group" style="margin-top:12px;">
<div class="settings-title">Goal</div>
<p style="font-size:11px;opacity:0.75;margin:0 0 8px 0;line-height:1.35;">Start defaults from your tracker when you open this tab. Enter a goal level or total XP (wins ≈ 100 XP each).</p>
<div class="be-calc-row"><span>Start:</span></div>
<div class="be-calc-split">
<div>
<div class="be-calc-label-sm">Level</div>
<input class="be-calc-pill" id="beCalcStartLv" type="number" inputmode="numeric" min="1" placeholder="Level" />
</div>
<div>
<div class="be-calc-label-sm">Total XP</div>
<input class="be-calc-pill" id="beCalcStartXp" type="number" inputmode="numeric" min="0" placeholder="XP" />
</div>
</div>
<div class="be-calc-row" style="margin-top:10px"><span>Goal:</span></div>
<div class="be-calc-split">
<div>
<div class="be-calc-label-sm">Level</div>
<input class="be-calc-pill" id="beCalcGoalLv" type="number" inputmode="numeric" min="1" placeholder="Level" />
</div>
<div>
<div class="be-calc-label-sm">Total XP</div>
<input class="be-calc-pill" id="beCalcGoalXp" type="number" inputmode="numeric" min="0" placeholder="XP" />
</div>
</div>
<div class="be-calc-goal-result" id="beCalcGoalResult">Enter a goal level or total XP.</div>
</div>
</div>
</div>
<!-- ACCOUNTS (data-view social) -->
<div class="tracker-view" data-view="social">
<div class="stats-wrap" style="padding:12px;">
<div class="settings-group">
<div class="settings-title">Saved logins (encrypted)</div>
<div id="beVaultMount"></div>
</div>
<div class="settings-group" style="margin-top:12px;">
<div class="settings-title">Seen in-game (stats)</div>
<p style="font-size:11px;opacity:0.85;margin:0 0 8px 0;line-height:1.35;">From the tracker while you play (not secret). Open Bonk in another tab to switch accounts.</p>
<input type="text" id="beAltsFilter" placeholder="Filter by name or tag…" style="width:100%;box-sizing:border-box;margin-bottom:8px;padding:7px 9px;border-radius:8px;border:1px solid rgba(255,255,255,0.14);background:rgba(0,0,0,0.22);color:inherit;font:12px var(--be-font-sans, 'Segoe UI',sans-serif);" />
<div id="beAltsTableWrap"></div>
</div>
<div class="settings-group" style="margin-top:14px;">
<div class="settings-title">Friends</div>
<p style="font-size:11px;opacity:0.85;margin:0 0 8px 0;line-height:1.35;">One username per line, matching in-game spelling. Friends get a gold highlight on their name in the game.</p>
<textarea id="beFriendsTextarea" rows="6" spellcheck="false" style="width:100%;box-sizing:border-box;font:12px ui-monospace,monospace;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.14);background:rgba(0,0,0,0.22);color:inherit;resize:vertical;"></textarea>
<div style="display:flex;gap:10px;margin-top:10px;align-items:center;flex-wrap:wrap;">
<button type="button" id="beFriendsSaveBtn" style="cursor:pointer;padding:6px 12px;border-radius:8px;border:1px solid rgba(120,180,255,0.45);background:rgba(25,45,80,0.6);color:#e8f0ff;font:600 12px 'Segoe UI',sans-serif;">Save friends</button>
<span id="beFriendsSaveHint" style="font-size:11px;opacity:0.75;"></span>
</div>
</div>
</div>
</div>
<!-- VISUALS — one card: players, usernames, chat -->
<div class="tracker-view" data-view="visuals">
<div class="be-visuals-panel">
<div class="settings-group">
<div class="settings-title">Visuals</div>
<div class="be-visuals-sub">
<div class="be-visuals-subtitle">Players</div>
<div class="settings-row"><span>Visible</span><input type="checkbox" checked id="togglePlayers" /></div>
<div class="settings-row"><span>Skins</span><input type="checkbox" checked id="toggleSkins" /></div>
<div class="settings-row be-visuals-slider-row">
<span>Opacity</span>
<input type="range" min="0" max="100" value="100" id="playersOpacity" style="width:100%;" />
</div>
</div>
<div class="be-visuals-sub">
<div class="be-visuals-subtitle">Usernames</div>
<div class="settings-row"><span>Visible</span><input type="checkbox" checked id="toggleNames" /></div>
<div class="settings-row be-visuals-slider-row">
<span>Opacity</span>
<input type="range" min="0" max="100" value="100" id="namesOpacity" style="width:100%;" />
</div>
</div>
<div class="be-visuals-sub">
<div class="be-visuals-subtitle">Chat</div>
<div class="settings-row"><span>Visible</span><input type="checkbox" checked id="toggleChat" /></div>
<div class="settings-row be-visuals-slider-row">
<span>Opacity</span>
<input type="range" min="0" max="100" value="100" id="chatOpacity" style="width:100%;" />
</div>
</div>
</div>
</div>
</div>
<!-- SETTINGS -->
<div class="tracker-view" data-view="settings">
<div style="padding:16px; opacity:0.9;">
<div class="settings-group">
<div class="settings-title">Appearance</div>
<div class="settings-row">
<span>Theme</span>
<select id="beThemeSelect">
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
<div class="settings-row">
<span>Size</span>
<select id="beSizeSelect">
<option value="tiny">Tiny</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</div>
<p style="font-size:10px;opacity:0.68;margin:8px 0 0;line-height:1.45;">While this overlay is open: <b>Alt+1–7</b> = Main · Graph · Stats · Calc · Accounts · Visuals · Settings. <b>Esc</b> closes the widget hub (if open), then the overlay. The <b>widget hub hotkey</b> (see below) <b>toggles</b> the hub open and closed. Other keys go to the game when the overlay is closed.</p>
</div>
<div class="settings-group">
<div class="settings-title">Fullscreen scaling (top bar)</div>
<div class="settings-row be-fs-scale-row">
<select id="beFullscreenScaleModeSelect" title="WDB: pillar pin + ~3% scale and small vertical shift so the map top stays visible. Letterbox: no extra zoom. Cover: fill viewport crop. Stretch: non-uniform."
style="padding:6px 8px;border-radius:8px;border:1px solid rgba(255,255,255,0.14);background:rgba(0,0,0,0.25);color:inherit;">
<option value="contain">Letterbox — centered</option>
<option value="contain-left">Letterbox — pillar left (map right)</option>
<option value="contain-right">Letterbox — pillar right (map left)</option>
<option value="wdb-left">WDB — map right (+slight zoom)</option>
<option value="wdb-right">WDB — map left (+slight zoom)</option>
<option value="cover">Cover — crop edges</option>
<option value="fill">Stretch — fill screen</option>
</select>
</div>
</div>
<div class="settings-group">
<div class="settings-title">Overlay Hotkey</div>
<div class="settings-row">
<span id="beHotkeyCurrent">Delete</span>
<button id="beSetHotkeyBtn">Set hotkey</button>
</div>
</div>
<div class="settings-group">
<div class="settings-title">Widget Hub Hotkey</div>
<div class="settings-row">
<span id="beWidgetHotkeyCurrent">Home</span>
<button id="beSetWidgetHotkeyBtn">Set hotkey</button>
</div>
</div>
<div class="settings-group">
<div class="settings-title">Keybind overlay</div>
<div class="settings-row">
<span>Show on-screen controls</span>
<label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" id="beKeybindOverlayToggle" />
<span id="beKeybindOverlayState">Off</span>
</label>
</div>
<div style="font-size:11px;opacity:0.72;line-height:1.45;margin-top:8px;">Open Bonk’s in-game <b>Settings</b> (gear) at least <b>once</b> so the Change Controls table loads.</div>
<div class="settings-row" style="margin-top:10px;flex-wrap:wrap;gap:10px;">
<span>Opacity</span>
<input type="range" id="beKbOverlayOpacity" min="35" max="100" value="100" style="width:120px;" />
</div>
<div class="settings-row" style="flex-wrap:wrap;gap:10px;">
<span>Size</span>
<input type="range" id="beKbOverlayScale" min="65" max="135" value="100" style="width:120px;" />
</div>
<div class="settings-row">
<label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" id="beKbOverlayCompact" />
<span>Compact single-row layout</span>
</label>
</div>
</div>
<div class="settings-group">
<div class="settings-title">UI presets</div>
<p style="font-size:11px;opacity:0.75;margin:0 0 8px 0;line-height:1.4;">Save theme, size, layout slots, tracker position, and keybind overlay options.</p>
<div class="be-ui-presets-grid">
<input type="text" id="bePresetName" placeholder="Preset name" style="padding:6px 8px;border-radius:8px;border:1px solid rgba(255,255,255,0.14);background:rgba(0,0,0,0.2);color:inherit;" />
<button type="button" id="bePresetSaveBtn" style="cursor:pointer;padding:6px 10px;border-radius:8px;border:1px solid rgba(120,180,255,0.45);background:rgba(25,45,80,0.55);color:#e8f0ff;font:600 11px 'Segoe UI',sans-serif;">Save</button>
<select id="bePresetSelect"></select>
<div class="be-ui-presets-load-del">
<button type="button" id="bePresetLoadBtn" style="cursor:pointer;padding:6px 10px;border-radius:8px;border:1px solid rgba(120,200,160,0.45);background:rgba(20,55,40,0.45);color:#c8ffd8;font:600 11px 'Segoe UI',sans-serif;">Load</button>
<button type="button" id="bePresetDeleteBtn" style="cursor:pointer;padding:6px 10px;border-radius:8px;border:1px solid rgba(255,150,120,0.35);background:rgba(55,25,20,0.45);color:#ffd0c8;font:600 11px 'Segoe UI',sans-serif;">Delete</button>
</div>
</div>
</div>
<div class="settings-group">
<div class="settings-title">Script health</div>
<p style="font-size:11px;opacity:0.75;margin:0 0 8px 0;line-height:1.4;">Diagnostics for iframe hooks, storage, and Bonk UI selectors (<span id="beSelectorSchemaLabel"></span>).</p>
<pre id="beScriptHealthOut" style="font-size:10px;line-height:1.35;max-height:180px;overflow:auto;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.1);background:rgba(0,0,0,0.25);white-space:pre-wrap;margin:0;"></pre>
<button type="button" id="beScriptHealthRefresh" style="cursor:pointer;margin-top:8px;padding:6px 12px;border-radius:8px;border:1px solid rgba(120,180,255,0.45);background:rgba(25,45,80,0.55);color:#e8f0ff;font:600 11px 'Segoe UI',sans-serif;">Refresh</button>
</div>
</div>
</div>
<div id="resizeHandle" class="tracker-resize">
<svg viewBox="0 0 24 24">
<path d="M8 20L20 8M12 20L20 12M16 20L20 16"
stroke="currentColor"
stroke-width="1"
stroke-linecap="round"
fill="none"/>
</svg>
</div>
</div> <!-- tracker-card -->
`;
document.body.appendChild(div);
(function beApplyOverlayTitleBranding() {
const el = document.getElementById("beOverlayTitleBlock");
if (!el) return;
el.innerHTML =
'<span class="be-overlay-title-main">BONK ENHANCED</span>' +
'<span class="be-overlay-title-meta">By ' +
BE_SCRIPT_AUTHOR +
'<span aria-hidden="true"> · </span><span class="be-overlay-title-version">v' +
BE_SCRIPT_VERSION +
"</span></span>";
})();
const BE_OVERLAY_DRAG_PAD = 4;
/** How many pixels of the overlay must stay visible on an edge — allows dragging most of the panel past the bottom (or side) instead of forcing the whole box on-screen. */
const BE_OVERLAY_EDGE_PEEK = 40;
function beClampOverlayToViewport() {
if (!div.parentNode) return;
div.style.right = "auto";
const w = div.offsetWidth;
const h = div.offsetHeight;
if (w < 8 || h < 8) return;
const vw = window.innerWidth;
const vh = window.innerHeight;
const r = div.getBoundingClientRect();
const parsedL = parseInt(div.style.left, 10);
const parsedT = parseInt(div.style.top, 10);
let left = Number.isFinite(parsedL) ? parsedL : r.left;
let top = Number.isFinite(parsedT) ? parsedT : r.top;
const pad = BE_OVERLAY_DRAG_PAD;
const peek = BE_OVERLAY_EDGE_PEEK;
const maxL = Math.max(pad, vw - peek);
const maxT = Math.max(pad, vh - peek);
left = Math.min(Math.max(pad, left), maxL);
top = Math.min(Math.max(pad, top), maxT);
div.style.left = Math.round(left) + "px";
div.style.top = Math.round(top) + "px";
}
try {
const rawPos = localStorage.getItem("bonk_ui_pos");
const savedPos = rawPos ? JSON.parse(rawPos) : null;
if (savedPos && typeof savedPos.left === "string" && typeof savedPos.top === "string") {
div.style.left = savedPos.left;
div.style.top = savedPos.top;
div.style.right = "auto";
}
} catch {
/* ignore bad storage */
}
requestAnimationFrame(() => {
requestAnimationFrame(() => beClampOverlayToViewport());
});
const launcher = document.createElement("button");
launcher.type = "button";
launcher.className = "be-overlay-launcher";
launcher.id = "beOverlayLauncher";
launcher.innerHTML = `<span class="be-overlay-launcher-dot"></span><span>Open overlay</span><span id="beLauncherHotkey">Delete</span>`;
launcher.addEventListener("click", () => setOverlayVisibility(true));
document.body.appendChild(launcher);
const widgetHub = document.createElement("div");
widgetHub.className = "be-widget-hub";
widgetHub.id = "beWidgetHub";
widgetHub.innerHTML = `
<div class="be-widget-hub-panel">
<div class="be-widget-hub-head">
<span>Widgets</span>
<span>Hotkey: <b id="beWidgetHubHint">Home</b></span>
</div>
<div class="be-widget-hub-grid">
<button type="button" class="be-widget-card be-widget-hud be-widget-dashboard" data-open-tab="dashboard"><span class="be-widget-hud-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="40" height="40" fill="none"><path d="M4 10.5L12 4l8 6.5V20a1 1 0 0 1-1 1h-5v-6H10v6H5a1 1 0 0 1-1-1v-9.5z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg></span><span class="be-widget-hud-copy"><span class="be-widget-title">Dashboard</span><small>Main tracker panel</small></span></button>
<button type="button" class="be-widget-card be-widget-hud be-widget-stats" data-open-tab="graph"><span class="be-widget-hud-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="40" height="40" fill="none"><path d="M5 19V5m4 14V9m4 10v-6m4 6v-3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span><span class="be-widget-hud-copy"><span class="be-widget-title">Graph</span><small>Weekly XP and wins</small></span></button>
<button type="button" class="be-widget-card be-widget-hud be-widget-stats" data-open-tab="statsOverview"><span class="be-widget-hud-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="40" height="40" fill="none"><path d="M8 6h12M8 12h12M8 18h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="4" y="4" width="14" height="16" rx="2" stroke="currentColor" stroke-width="1.5"/></svg></span><span class="be-widget-hud-copy"><span class="be-widget-title">Stats</span><small>All tracked stats overview</small></span></button>
<button type="button" class="be-widget-card be-widget-hud be-widget-calculator" data-open-tab="calculator"><span class="be-widget-hud-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="40" height="40" fill="none"><rect x="5" y="3" width="14" height="18" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M8 8h8M9 12h1.5v1.5H9V15M13 12h2M8 17h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg></span><span class="be-widget-hud-copy"><span class="be-widget-title">Calculator</span><small>XP, level, and goal wins</small></span></button>
<button type="button" class="be-widget-card be-widget-hud be-widget-social" data-open-tab="social"><span class="be-widget-hud-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="40" height="40" fill="none"><circle cx="9" cy="8" r="2.5" stroke="currentColor" stroke-width="1.5"/><path d="M4 19v-1a4 4 0 0 1 4-4h2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="17" cy="9" r="2" stroke="currentColor" stroke-width="1.5"/><path d="M21 19v0a4 4 0 0 0-3-3.87" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span><span class="be-widget-hud-copy"><span class="be-widget-title">Accounts</span><small>Vault, logins, friends</small></span></button>
<button type="button" class="be-widget-card be-widget-hud be-widget-visuals" data-open-tab="visuals"><span class="be-widget-hud-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="40" height="40" fill="none"><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5"/><path d="M3 12h0a9 9 0 0 1 9-9h0a9 9 0 0 1 9 9h0M3 12a9 9 0 0 0 9 9M21 12a9 9 0 0 1-9 9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg></span><span class="be-widget-hud-copy"><span class="be-widget-title">Visuals</span><small>Player and chat controls</small></span></button>
<button type="button" class="be-widget-card be-widget-hud be-widget-settings" data-open-tab="settings"><span class="be-widget-hud-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" width="40" height="40" fill="none"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z" stroke="currentColor" stroke-width="1.4"/><path d="M19.4 15a1.8 1.8 0 0 0 .36 1.99l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.8 1.8 0 0 0-1.99-.36 1.8 1.8 0 0 0-1 1.64V21a2 2 0 1 1-4 0v-.09a1.8 1.8 0 0 0-1-1.64 1.8 1.8 0 0 0-1.99.36l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.8 1.8 0 0 0 .36-1.99 1.8 1.8 0 0 0-1.64-1h-.09a2 2 0 1 1 0-4h.09a1.8 1.8 0 0 0 1.64-1 1.8 1.8 0 0 0-.36-1.99l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.8 1.8 0 0 0 1.99.36h.09a1.8 1.8 0 0 0 1-1.64V3a2 2 0 1 1 4 0v.09a1.8 1.8 0 0 0 1 1.64h.09a1.8 1.8 0 0 0 1.99-.36l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.8 1.8 0 0 0-.36 1.99v.09a1.8 1.8 0 0 0 1.64 1H21a2 2 0 1 1 0 4h-.09a1.8 1.8 0 0 0-1.64 1z" stroke="currentColor" stroke-width="1.05" stroke-linecap="round" opacity="0.95"/></svg></span><span class="be-widget-hud-copy"><span class="be-widget-title">Settings</span><small>Theme, size, and hotkeys</small></span></button>
</div>
</div>
`;
widgetHub.addEventListener("click", (ev) => {
if (ev.target === widgetHub) setWidgetHubVisibility(false);
});
document.body.appendChild(widgetHub);
ensureBonkKeybindOverlayMounted();
setTimeout(() => {
// 🔥 SAFE VISUAL TOGGLES (delayed + null-safe)
const v = window.BonkVisuals;
// PLAYERS
document.getElementById("togglePlayers").onchange = function () {
v.players.visible = this.checked;
};
document.getElementById("toggleSkins").onchange = function () {
v.players.skins = this.checked;
};
document.getElementById("playersOpacity").oninput = function () {
v.players.alpha = this.value / 100;
};
// USERNAMES
document.getElementById("toggleNames").onchange = function () {
v.players.usernames.visible = this.checked;
};
document.getElementById("namesOpacity").oninput = function () {
v.players.usernames.alpha = this.value / 100;
};
// CHAT
document.getElementById("toggleChat").onchange = function () {
v.chat.visible = this.checked;
if (v.chatWindow) {
v.chatWindow.style.opacity = this.checked ? v.chat.alpha : 0;
}
};
document.getElementById("chatOpacity").oninput = function () {
v.chat.alpha = this.value / 100;
if (v.chatWindow) {
v.chatWindow.style.opacity = v.chat.alpha;
}
};
}, 50);
UI.root = div;
UI.root.classList.add("tracker-visible");
UI.root.classList.add("be-overlay-open");
UI.launcher = launcher;
UI.launcherLabel = launcher.querySelector("#beLauncherHotkey");
UI.widgetHub = widgetHub;
UI.widgetHubHint = widgetHub.querySelector("#beWidgetHubHint");
applyOverlaySettings();
await tryVaultAutoUnlockFromRemembered();
const themeSelect = document.getElementById("beThemeSelect");
const sizeSelect = document.getElementById("beSizeSelect");
const setHotkeyBtn = document.getElementById("beSetHotkeyBtn");
const setWidgetHotkeyBtn = document.getElementById("beSetWidgetHotkeyBtn");
const graphWeekSelect = document.getElementById("beGraphWeekSelect");
if (themeSelect) themeSelect.value = uiSettings.theme;
if (sizeSelect) sizeSelect.value = uiSettings.size;
if (graphWeekSelect) graphWeekSelect.value = uiSettings.graphWeek || "";
if (themeSelect) {
themeSelect.addEventListener("change", () => {
uiSettings.theme = themeSelect.value === "light" ? "light" : "dark";
saveUISettings();
applyOverlaySettings();
});
}
if (sizeSelect) {
sizeSelect.addEventListener("change", () => {
applySizePreset(sizeSelect.value);
applyOverviewLayout();
syncOverviewSlotControls();
});
}
const beFullscreenScaleModeSelect = document.getElementById("beFullscreenScaleModeSelect");
function bePersistFullscreenScaleMode(mode) {
try {
localStorage.setItem("be_fullscreen_scale_mode", mode);
const coverish = mode === "cover";
localStorage.setItem("be_fullscreen_fill_cover", coverish ? "1" : "0");
} catch (e) {
void e;
}
}
if (beFullscreenScaleModeSelect) {
try {
const m = beFullscreenScaleMode();
const ok =
m === "contain" ||
m === "contain-left" ||
m === "contain-right" ||
m === "wdb-left" ||
m === "wdb-right" ||
m === "cover" ||
m === "fill";
beFullscreenScaleModeSelect.value = ok ? m : "contain";
} catch (e) {
beFullscreenScaleModeSelect.value = "contain";
}
beFullscreenScaleModeSelect.addEventListener("change", () => {
const v = beFullscreenScaleModeSelect.value;
if (
v !== "contain" &&
v !== "contain-left" &&
v !== "contain-right" &&
v !== "wdb-left" &&
v !== "wdb-right" &&
v !== "cover" &&
v !== "fill"
) {
return;
}
bePersistFullscreenScaleMode(v);
beSyncFullscreenFitClasses();
});
}
if (graphWeekSelect) {
graphWeekSelect.addEventListener("change", () => {
uiSettings.graphWeek = graphWeekSelect.value || "";
saveUISettings();
const cache = Storage.load();
renderStatsTabFromData(cache?.data || {});
});
}
if (setHotkeyBtn) {
setHotkeyBtn.addEventListener("click", () => {
if (isCapturingHotkey) return;
isCapturingHotkey = true;
setHotkeyBtn.textContent = "Press any key...";
const onSetKey = (ev) => {
ev.preventDefault();
ev.stopPropagation();
const nextKey = ev.key;
if (!nextKey) return;
uiSettings.overlayHotkey = nextKey;
saveUISettings();
applyOverlaySettings();
setHotkeyBtn.textContent = "Set hotkey";
isCapturingHotkey = false;
document.removeEventListener("keydown", onSetKey, true);
};
document.addEventListener("keydown", onSetKey, true);
});
}
if (setWidgetHotkeyBtn) {
setWidgetHotkeyBtn.addEventListener("click", () => {
if (isCapturingHotkey) return;
isCapturingHotkey = true;
setWidgetHotkeyBtn.textContent = "Press any key...";
const onSetWidgetKey = (ev) => {
ev.preventDefault();
ev.stopPropagation();
const nextKey = ev.key;
if (!nextKey) return;
uiSettings.widgetHotkey = nextKey;
saveUISettings();
applyOverlaySettings();
setWidgetHotkeyBtn.textContent = "Set hotkey";
isCapturingHotkey = false;
document.removeEventListener("keydown", onSetWidgetKey, true);
};
document.addEventListener("keydown", onSetWidgetKey, true);
});
}
const keybindOverlayToggle = document.getElementById("beKeybindOverlayToggle");
if (keybindOverlayToggle) {
keybindOverlayToggle.checked = uiSettings.keybindOverlayEnabled === true;
keybindOverlayToggle.addEventListener("change", () => {
uiSettings.keybindOverlayEnabled = keybindOverlayToggle.checked;
saveUISettings();
applyKeybindOverlayVisibility();
});
}
const kbOp = document.getElementById("beKbOverlayOpacity");
const kbSc = document.getElementById("beKbOverlayScale");
const kbCp = document.getElementById("beKbOverlayCompact");
if (kbOp) {
kbOp.value = String(Math.round((uiSettings.keybindOverlayOpacity ?? 1) * 100));
kbOp.addEventListener("input", () => {
uiSettings.keybindOverlayOpacity = Number(kbOp.value) / 100;
saveUISettings();
applyKeybindOverlayStyle();
});
}
if (kbSc) {
kbSc.value = String(Math.round((uiSettings.keybindOverlayScale ?? 1) * 100));
kbSc.addEventListener("input", () => {
uiSettings.keybindOverlayScale = Number(kbSc.value) / 100;
saveUISettings();
applyKeybindOverlayStyle();
});
}
if (kbCp) {
kbCp.checked = uiSettings.keybindOverlayCompact === true;
kbCp.addEventListener("change", () => {
uiSettings.keybindOverlayCompact = kbCp.checked;
saveUISettings();
applyKeybindOverlayStyle();
});
}
applyKeybindOverlayStyle();
const presetSave = document.getElementById("bePresetSaveBtn");
const presetLoad = document.getElementById("bePresetLoadBtn");
const presetDel = document.getElementById("bePresetDeleteBtn");
if (presetSave) {
presetSave.addEventListener("click", () => {
const nameInp = document.getElementById("bePresetName");
const name = (nameInp?.value || "").trim() || `Preset ${(uiSettings.uiPresets?.length || 0) + 1}`;
const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: `p_${Date.now()}`;
const data = beSnapshotUiPresetData();
const list = Array.isArray(uiSettings.uiPresets) ? uiSettings.uiPresets.filter((p) => p.name !== name) : [];
list.push({ id, name, data });
uiSettings.uiPresets = list.slice(-8);
saveUISettings();
beRefreshPresetSelect();
});
}
if (presetLoad) {
presetLoad.addEventListener("click", () => {
const sel = document.getElementById("bePresetSelect");
const id = sel?.value;
if (!id) return;
const p = uiSettings.uiPresets.find((x) => x.id === id);
if (p?.data) beApplyUiPresetData(p.data);
});
}
if (presetDel) {
presetDel.addEventListener("click", () => {
const sel = document.getElementById("bePresetSelect");
const id = sel?.value;
if (!id) return;
uiSettings.uiPresets = uiSettings.uiPresets.filter((x) => x.id !== id);
saveUISettings();
beRefreshPresetSelect();
});
}
beRefreshPresetSelect();
const healthRef = document.getElementById("beScriptHealthRefresh");
if (healthRef) healthRef.addEventListener("click", () => refreshScriptHealthPanel());
const csvBtn = document.getElementById("beExportStatsCsv");
if (csvBtn) csvBtn.addEventListener("click", () => beExportStatsToCsv());
// 🔥 cache DOM elements (DO THIS ONCE)
UI.xpGain = document.getElementById("xpGain");
UI.winsLine = document.getElementById("winsLine");
UI.rateLine = document.getElementById("rateLine");
UI.sessionLine = document.getElementById("sessionLine");
UI.statusText = document.getElementById("statusText");
UI.levelLine = document.getElementById("levelLine");
UI.playerName = document.getElementById("playerName");
UI.playerSkin = document.getElementById("playerSkin");
UI.xpPercent = document.getElementById("xpPercent");
UI.xpCircle = document.getElementById("xpCircle");
UI.xpText = document.getElementById("xpText");
UI.xpCircleWrapper = document.getElementById("xpCircleWrapper");
UI.lastWinLine = document.getElementById("lastWinLine");
UI.dailyProgressBar = document.getElementById("dailyProgressBar");
UI.card = div.querySelector(".tracker-card");
UI.sessionStatLine = document.getElementById("sessionStatLine");
UI.levelStatLine = document.getElementById("levelStatLine");
applyDashboardStatVisibility();
applyOverviewLayout();
syncOverviewSlotControls();
const overviewSlotWrap = document.getElementById("beOverviewSlots");
if (overviewSlotWrap) {
overviewSlotWrap.querySelectorAll("select[data-slot-idx]").forEach((sel) => {
sel.addEventListener("change", () => {
const idx = Number(sel.getAttribute("data-slot-idx") || "0");
const size = uiSettings.size === "tiny" || uiSettings.size === "large" ? uiSettings.size : "medium";
const slots = getOverviewSlotsForCurrentSize();
slots[idx] = sel.value;
uiSettings.overviewSlots[size] = slots;
saveUISettings();
applyOverviewLayout();
syncOverviewSlotControls();
});
});
}
let isDragging = false, offsetX, offsetY;
const dragHandle = div.querySelector("#dragHandle");
dragHandle.addEventListener("contextmenu", (e) => {
e.preventDefault();
});
const menuBtn = div.querySelector(".tracker-menu-icon");
const sidebar = document.getElementById("trackerSidebar");
menuBtn.addEventListener("click", (e) => {
e.stopPropagation();
sidebar.classList.toggle("open");
});
document.getElementById("topbarSettings").addEventListener("click", (e) => {
e.stopPropagation();
switchTab("settings");
sidebar.classList.remove("open");
});
document.getElementById("topbarClose").addEventListener("click", (e) => {
e.stopPropagation();
setOverlayVisibility(false);
});
function parseCalcField(el) {
if (!el) return NaN;
const v = String(el.value ?? "").trim();
if (v === "") return NaN;
const n = Number(v);
return Number.isFinite(n) ? n : NaN;
}
function recalcBeCalculator() {
const lv2xpIn = document.getElementById("beCalcLv2XpIn");
const lv2xpOut = document.getElementById("beCalcLv2XpOut");
if (lv2xpOut) {
const lv = parseCalcField(lv2xpIn);
if (lv >= 1) lv2xpOut.textContent = `${getXPForLevel(Math.floor(lv)).toLocaleString()} XP`;
else lv2xpOut.textContent = "—";
}
const xp2lvIn = document.getElementById("beCalcXp2LvIn");
const xp2lvOut = document.getElementById("beCalcXp2LvOut");
if (xp2lvOut) {
const x = parseCalcField(xp2lvIn);
if (Number.isFinite(x) && x >= 0) xp2lvOut.textContent = `Level ${getLevelFromXP(Math.floor(x))}`;
else xp2lvOut.textContent = "—";
}
const startLvEl = document.getElementById("beCalcStartLv");
const startXpEl = document.getElementById("beCalcStartXp");
const goalLvEl = document.getElementById("beCalcGoalLv");
const goalXpEl = document.getElementById("beCalcGoalXp");
const goalRes = document.getElementById("beCalcGoalResult");
const rawStartXp = parseCalcField(startXpEl);
const rawStartLv = parseCalcField(startLvEl);
let startXp = 0;
if (Number.isFinite(rawStartXp) && rawStartXp >= 0) {
startXp = Math.floor(rawStartXp);
} else if (rawStartLv >= 1) {
startXp = getXPForLevel(Math.floor(rawStartLv));
}
let goalXpVal = NaN;
const gx = parseCalcField(goalXpEl);
const glv = parseCalcField(goalLvEl);
if (Number.isFinite(gx) && gx >= 0) goalXpVal = Math.floor(gx);
else if (glv >= 1) goalXpVal = getXPForLevel(Math.floor(glv));
if (goalRes) {
if (!Number.isFinite(goalXpVal)) {
goalRes.textContent = "Enter a goal level or total XP.";
goalRes.style.color = "";
} else {
const delta = goalXpVal - startXp;
const wins = Math.ceil(Math.max(0, delta) / 100);
if (delta <= 0) {
goalRes.textContent = `Already at or past goal (${delta.toLocaleString()} XP Δ).`;
goalRes.style.color = "#ffb86b";
} else {
goalRes.textContent = `Need ${delta.toLocaleString()} XP (~${wins} win${wins === 1 ? "" : "s"}).`;
goalRes.style.color = "#9cffb0";
}
}
}
}
function refreshBeCalculatorFromTracker() {
const sl = document.getElementById("beCalcStartLv");
const sx = document.getElementById("beCalcStartXp");
if (!sl || !sx) return;
const { data, key } = Storage.load();
let xp = data[key]?.xp;
if (xp === undefined || xp === null) xp = data?._globalXP;
xp = Math.max(0, Math.floor(Number(xp) || 0));
const level = getLevelFromXP(xp);
sl.value = String(level);
sx.value = String(xp);
recalcBeCalculator();
}
function renderVaultMount() {
const mount = document.getElementById("beVaultMount");
if (!mount) return;
const stored = beVaultReadStored();
if (!stored) {
mount.innerHTML = `
<div style="display:flex;flex-direction:column;gap:8px;max-width:340px;">
<span style="font-size:11px;opacity:0.9;">Create a strong master password. It encrypts your saved Bonk logins (not the stats list below).</span>
<input type="password" id="beVaultNewMp1" autocomplete="off" placeholder="Master password" style="padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:rgba(0,0,0,0.25);color:inherit;" />
<input type="password" id="beVaultNewMp2" autocomplete="off" placeholder="Confirm master password" style="padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:rgba(0,0,0,0.25);color:inherit;" />
<label style="font-size:11px;opacity:0.9;display:flex;align-items:center;gap:6px;cursor:pointer;">
<input type="checkbox" id="beVaultRememberCb" checked />
Remember master password on this PC (plain text in localStorage — only if you trust this machine)
</label>
<button type="button" data-be-vault="create" style="cursor:pointer;padding:8px 12px;border-radius:8px;border:1px solid rgba(120,180,255,0.45);background:rgba(25,45,80,0.6);color:#e8f0ff;font:600 12px 'Segoe UI',sans-serif;">Create encrypted vault</button>
<span id="beVaultErr" style="font-size:11px;color:#ff8e8e;min-height:14px;"></span>
</div>`;
return;
}
if (!beVaultSession.unlocked) {
const rem = isVaultRememberDevice();
mount.innerHTML = `
<div style="display:flex;flex-direction:column;gap:8px;max-width:340px;">
<input type="password" id="beVaultMp" autocomplete="off" placeholder="Master password" style="padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:rgba(0,0,0,0.25);color:inherit;" />
<label style="font-size:11px;opacity:0.9;display:flex;align-items:center;gap:6px;cursor:pointer;">
<input type="checkbox" id="beVaultRememberCb" ${rem ? "checked" : ""} />
Remember on this PC (plain text in localStorage)
</label>
<button type="button" data-be-vault="unlock" style="cursor:pointer;padding:8px 12px;border-radius:8px;border:1px solid rgba(120,180,255,0.45);background:rgba(25,45,80,0.6);color:#e8f0ff;font:600 12px 'Segoe UI',sans-serif;">Unlock</button>
<span id="beVaultErr" style="font-size:11px;color:#ff8e8e;min-height:14px;"></span>
</div>`;
return;
}
const rows = beVaultSession.entries.length
? beVaultSession.entries.map((e) => {
const lab = e.label ? `${escapeHtml(e.label)} · ` : "";
return `<div class="stats-history-item" style="flex-wrap:wrap;gap:8px;align-items:flex-start;">
<span style="min-width:0;"><b>${lab}${escapeHtml(e.username)}</b><br>
<span style="font-size:10px;opacity:0.75;">Password hidden — use copy</span></span>
<span style="display:flex;flex-wrap:wrap;gap:6px;">
<button type="button" data-be-vault="copy-user" data-entry-id="${escapeHtml(e.id)}" style="cursor:pointer;padding:4px 8px;border-radius:6px;border:1px solid rgba(120,180,255,0.4);background:rgba(20,40,70,0.5);color:#dbeaff;font:11px 'Segoe UI',sans-serif;">Copy user</button>
<button type="button" data-be-vault="copy-pass" data-entry-id="${escapeHtml(e.id)}" style="cursor:pointer;padding:4px 8px;border-radius:6px;border:1px solid rgba(120,180,255,0.4);background:rgba(20,40,70,0.5);color:#dbeaff;font:11px 'Segoe UI',sans-serif;">Copy password</button>
<button type="button" data-be-vault="autofill-login" data-entry-id="${escapeHtml(e.id)}" style="cursor:pointer;padding:4px 8px;border-radius:6px;border:1px solid rgba(120,255,180,0.45);background:rgba(15,55,40,0.55);color:#c8ffd8;font:11px 'Segoe UI',sans-serif;">Autofill login</button>
<button type="button" data-be-vault="open-bonk" style="cursor:pointer;padding:4px 8px;border-radius:6px;border:1px solid rgba(180,220,255,0.35);background:rgba(20,35,55,0.45);color:#cfe6ff;font:11px 'Segoe UI',sans-serif;">Open bonk.io</button>
<button type="button" data-be-vault="remove-entry" data-entry-id="${escapeHtml(e.id)}" style="cursor:pointer;padding:4px 8px;border-radius:6px;border:1px solid rgba(255,120,120,0.35);background:rgba(50,20,20,0.4);color:#ffc9c9;font:11px 'Segoe UI',sans-serif;">Remove</button>
</span>
</div>`;
}).join("")
: `<div class="stats-history-item"><span>No saved logins yet</span><span>—</span></div>`;
const rememberOn = isVaultRememberDevice();
const unlockHint = rememberOn
? "Unlocked — remembered on this PC (no auto-lock)"
: "Unlocked — auto-locks after 2 hours idle";
mount.innerHTML = `
<div style="display:flex;flex-direction:column;gap:10px;">
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
<button type="button" data-be-vault="lock" style="cursor:pointer;padding:6px 12px;border-radius:8px;border:1px solid rgba(255,200,120,0.4);background:rgba(60,45,20,0.45);color:#ffe6b0;font:600 12px 'Segoe UI',sans-serif;">Lock vault</button>
${rememberOn ? `<button type="button" data-be-vault="forget-remember" style="cursor:pointer;padding:6px 10px;border-radius:8px;border:1px solid rgba(255,120,120,0.35);background:rgba(45,20,20,0.45);color:#ffc9c9;font:600 11px 'Segoe UI',sans-serif;">Forget saved master</button>` : ""}
<span style="font-size:10px;opacity:0.75;">${unlockHint}</span>
</div>
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
<button type="button" data-be-vault="export-file" style="cursor:pointer;padding:6px 10px;border-radius:8px;border:1px solid rgba(120,200,255,0.4);background:rgba(18,40,65,0.55);color:#d6ecff;font:600 11px 'Segoe UI',sans-serif;">Export vault backup file</button>
<label style="font-size:11px;cursor:pointer;opacity:0.95;display:inline-flex;align-items:center;gap:6px;">
<input type="file" id="beVaultImportFile" accept="application/json,.json" style="display:none;" />
<span style="padding:6px 10px;border-radius:8px;border:1px solid rgba(200,200,255,0.35);background:rgba(30,30,55,0.45);">Import backup…</span>
</label>
</div>
<p style="font-size:10px;opacity:0.72;margin:0;line-height:1.35;">Backup is the encrypted blob only — still protect the file. Import replaces the stored vault; unlock again afterward.</p>
<span id="beVaultMsg" style="font-size:11px;color:#9cffb0;min-height:14px;"></span>
<div style="font-size:11px;opacity:0.85;margin-bottom:4px;">Add login</div>
<div style="display:grid;gap:6px;max-width:400px;">
<input type="text" id="beVaultAddLabel" autocomplete="off" placeholder="Label (optional)" style="padding:7px;border-radius:8px;border:1px solid rgba(255,255,255,0.12);background:rgba(0,0,0,0.22);color:inherit;" />
<input type="text" id="beVaultAddUser" autocomplete="off" placeholder="Bonk username" style="padding:7px;border-radius:8px;border:1px solid rgba(255,255,255,0.12);background:rgba(0,0,0,0.22);color:inherit;" />
<input type="password" id="beVaultAddPass" autocomplete="off" placeholder="Password" style="padding:7px;border-radius:8px;border:1px solid rgba(255,255,255,0.12);background:rgba(0,0,0,0.22);color:inherit;" />
<button type="button" data-be-vault="add-entry" style="cursor:pointer;padding:7px 12px;border-radius:8px;border:1px solid rgba(76,175,80,0.45);background:rgba(20,50,30,0.5);color:#c8ffc8;font:600 12px 'Segoe UI',sans-serif;width:fit-content;">Save login</button>
</div>
<div style="margin-top:8px;font-size:11px;opacity:0.85;">Saved accounts</div>
<div>${rows}</div>
<details style="margin-top:8px;font-size:11px;">
<summary style="cursor:pointer;opacity:0.9;">Change master password</summary>
<div style="display:grid;gap:6px;margin-top:8px;max-width:340px;">
<input type="password" id="beVaultOldMp" autocomplete="off" placeholder="Current master password" style="padding:7px;border-radius:8px;border:1px solid rgba(255,255,255,0.12);background:rgba(0,0,0,0.22);color:inherit;" />
<input type="password" id="beVaultNewMpCh1" autocomplete="off" placeholder="New master password" style="padding:7px;border-radius:8px;border:1px solid rgba(255,255,255,0.12);background:rgba(0,0,0,0.22);color:inherit;" />
<input type="password" id="beVaultNewMpCh2" autocomplete="off" placeholder="Confirm new" style="padding:7px;border-radius:8px;border:1px solid rgba(255,255,255,0.12);background:rgba(0,0,0,0.22);color:inherit;" />
<button type="button" data-be-vault="changemp" style="cursor:pointer;padding:7px 12px;border-radius:8px;border:1px solid rgba(120,180,255,0.45);background:rgba(25,45,80,0.6);color:#e8f0ff;font:600 12px 'Segoe UI',sans-serif;width:fit-content;">Update master password</button>
</div>
</details>
</div>`;
}
function renderGameAltsTable() {
const wrap = document.getElementById("beAltsTableWrap");
if (!wrap) return;
const filtEl = document.getElementById("beAltsFilter");
if (filtEl) beAltsFilterQuery = filtEl.value || "";
const q = (beAltsFilterQuery || "").trim().toLowerCase();
const alts = loadAltsRegistry();
const current = (getPlayerKey() || "").trim().toLowerCase();
const filtered = !q
? alts
: alts.filter((a) => {
const nm = a.name.toLowerCase();
if (nm.includes(q)) return true;
return (a.tags || []).some((t) => t.toLowerCase().includes(q));
});
if (!alts.length) {
wrap.innerHTML = `<div class="stats-history-item"><span>No alts recorded yet</span><span>—</span></div>`;
return;
}
if (!filtered.length) {
wrap.innerHTML = `<div class="stats-history-item"><span>No matches</span><span>—</span></div>`;
return;
}
wrap.innerHTML = filtered.map((a) => {
const isYou = a.name.trim().toLowerCase() === current;
const when = a.lastSeen ? new Date(a.lastSeen).toLocaleString() : "—";
const tag = isYou ? ` <span style="opacity:0.85;font-size:10px;">(this tab)</span>` : "";
const enc = encodeURIComponent(a.name);
const tagsStr = (a.tags || []).join(", ");
return `<div class="stats-history-item be-alt-row">
<div class="be-alt-row-main"><b>${escapeHtml(a.name)}</b>${tag}<br><span style="font-size:10px;opacity:0.8;">Lv ${a.level} · ${a.xp.toLocaleString()} XP · ${when}</span>
<div style="margin-top:6px;font-size:10px;opacity:0.85;">Tags <input type="text" class="be-alt-tags" data-alt="${enc}" value="${escapeHtml(tagsStr)}" placeholder="comma separated" style="width:100%;max-width:100%;box-sizing:border-box;padding:4px 6px;border-radius:6px;border:1px solid rgba(255,255,255,0.12);background:rgba(0,0,0,0.2);color:inherit;" /></div></div>
<div class="be-alt-row-actions">
<button type="button" class="be-alt-open" data-alt="${enc}" style="cursor:pointer;padding:4px 8px;border-radius:6px;border:1px solid rgba(120,180,255,0.4);background:rgba(20,40,70,0.5);color:#dbeaff;font:11px 'Segoe UI',sans-serif;">Open login</button>
<button type="button" class="be-alt-remove" data-alt="${enc}" style="cursor:pointer;padding:4px 8px;border-radius:6px;border:1px solid rgba(255,120,120,0.35);background:rgba(50,20,20,0.4);color:#ffc9c9;font:11px 'Segoe UI',sans-serif;">Remove</button>
</div>
</div>`;
}).join("");
if (filtEl && !filtEl.__beAltFilterHooked) {
filtEl.__beAltFilterHooked = true;
filtEl.addEventListener("input", () => {
renderGameAltsTable();
});
}
wrap.querySelectorAll("button.be-alt-open").forEach((btn) => {
btn.addEventListener("click", () => {
window.open("https://bonk.io/", "_blank");
});
});
wrap.querySelectorAll("button.be-alt-remove").forEach((btn) => {
btn.addEventListener("click", () => {
let raw = "";
try {
raw = decodeURIComponent(btn.getAttribute("data-alt") || "");
} catch {
raw = "";
}
const next = loadAltsRegistry().filter((x) => x.name !== raw);
saveAltsRegistry(next);
renderSocialPanel();
});
});
wrap.querySelectorAll("input.be-alt-tags").forEach((inp) => {
inp.addEventListener("change", () => {
let raw = "";
try {
raw = decodeURIComponent(inp.getAttribute("data-alt") || "");
} catch {
raw = "";
}
const tags = (inp.value || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean)
.slice(0, 16);
const all = loadAltsRegistry();
const i = all.findIndex((x) => x.name === raw);
if (i < 0) return;
all[i] = { ...all[i], tags };
saveAltsRegistry(all);
});
});
}
function renderSocialPanel() {
const ta = document.getElementById("beFriendsTextarea");
const hint = document.getElementById("beFriendsSaveHint");
if (ta) ta.value = loadFriendsList().join("\n");
if (hint) hint.textContent = "";
renderVaultMount();
renderGameAltsTable();
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
function switchTab(tab) {
if (!tab) return;
const views = div.querySelectorAll(".tracker-view");
const items = div.querySelectorAll(".sidebar-item");
if (!views.length) {
console.warn("[UI] No views found");
return;
}
// stable reset: hide everything first
views.forEach(v => {
v.classList.remove("active");
v.classList.remove("fade-in");
v.style.display = "none";
});
items.forEach(i => i.classList.remove("active"));
// 🔥 activate sidebar item
const activeItem = div.querySelector(`.sidebar-item[data-tab="${tab}"]`);
if (activeItem) {
activeItem.classList.add("active");
} else {
console.warn("[UI] Missing sidebar item for:", tab);
}
// 🔥 activate view
const target = div.querySelector(`.tracker-view[data-view="${tab}"]`);
if (!target) {
console.warn("[UI] Missing view for:", tab);
return;
}
// show + fade-in target only (no overlap)
target.style.display = "block";
target.classList.add("active");
target.classList.add("fade-in");
setTimeout(() => target.classList.remove("fade-in"), 240);
if (tab === "graph") {
const cache = Storage.load();
renderStatsTabFromData(cache?.data || {});
} else if (tab === "statsOverview") {
const cache = Storage.load();
renderStatsOverview(cache?.data || {});
} else if (tab === "calculator") {
refreshBeCalculatorFromTracker();
} else if (tab === "social") {
renderSocialPanel();
} else if (tab === "settings") {
refreshScriptHealthPanel();
beRefreshPresetSelect();
}
// 🔥 DEBUG
console.debug("[UI] Switched to:", tab);
}
UI.switchTab = switchTab;
(function attachBeCalculatorListenersOnce() {
const cv = div.querySelector('.tracker-view[data-view="calculator"]');
if (!cv || cv._beCalcBound) return;
cv._beCalcBound = true;
["beCalcLv2XpIn", "beCalcXp2LvIn", "beCalcStartLv", "beCalcStartXp", "beCalcGoalLv", "beCalcGoalXp"].forEach((id) => {
const el = document.getElementById(id);
if (el) el.addEventListener("input", recalcBeCalculator);
});
})();
(function attachSocialListenersOnce() {
const sv = div.querySelector('.tracker-view[data-view="social"]');
if (!sv || sv._beSocialBound) return;
sv._beSocialBound = true;
const btn = document.getElementById("beFriendsSaveBtn");
if (btn) {
btn.addEventListener("click", () => {
const ta = document.getElementById("beFriendsTextarea");
const hint = document.getElementById("beFriendsSaveHint");
const lines = (ta?.value || "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
saveFriendsList(lines);
if (hint) {
hint.textContent = "Saved — gold name tint applies in matches.";
setTimeout(() => {
if (hint) hint.textContent = "";
}, 3200);
}
});
}
sv.addEventListener("click", (e) => {
let t = e.target instanceof Element ? e.target.closest("[data-be-vault]") : null;
if (!t && typeof e.composedPath === "function") {
const path = e.composedPath();
for (let i = 0; i < path.length; i++) {
const node = path[i];
if (node instanceof HTMLElement && node.hasAttribute("data-be-vault")) {
t = node;
break;
}
}
}
if (!(t instanceof HTMLElement)) return;
const act = t.getAttribute("data-be-vault");
if (!act) return;
const run = async () => {
const errEl = () => document.getElementById("beVaultErr");
const msgEl = () => document.getElementById("beVaultMsg");
if (act === "create") {
const p1 = (document.getElementById("beVaultNewMp1")?.value || "").trim();
const p2 = (document.getElementById("beVaultNewMp2")?.value || "").trim();
const err = errEl();
if (p1.length < 8) {
if (err) err.textContent = "Use at least 8 characters.";
return;
}
if (p1 !== p2) {
if (err) err.textContent = "Passwords do not match.";
return;
}
try {
await beVaultCreateVault(p1);
if (document.getElementById("beVaultRememberCb")?.checked) {
saveVaultRememberMaster(p1);
if (beVaultSession.lockTimer) {
clearTimeout(beVaultSession.lockTimer);
beVaultSession.lockTimer = null;
}
} else {
clearVaultRememberDevice();
}
if (err) err.textContent = "";
renderSocialPanel();
} catch (ex) {
if (err) err.textContent = "Could not create vault.";
BE_LOG.error("vault create", ex);
}
return;
}
if (act === "unlock") {
const pw = (document.getElementById("beVaultMp")?.value || "").trim();
const err = errEl();
if (!pw) {
if (err) err.textContent = "Enter master password.";
return;
}
try {
await beVaultUnlockWithPassword(pw);
if (document.getElementById("beVaultRememberCb")?.checked) {
saveVaultRememberMaster(pw);
if (beVaultSession.lockTimer) {
clearTimeout(beVaultSession.lockTimer);
beVaultSession.lockTimer = null;
}
} else {
clearVaultRememberDevice();
}
const inp = document.getElementById("beVaultMp");
if (inp) inp.value = "";
if (err) err.textContent = "";
renderSocialPanel();
} catch {
if (err) err.textContent = "Wrong password or unreadable vault.";
}
return;
}
if (act === "forget-remember") {
clearVaultRememberDevice();
beVaultLock();
renderSocialPanel();
return;
}
if (act === "lock") {
beVaultLock();
renderSocialPanel();
return;
}
if (act === "export-file") {
const msg = msgEl();
const raw = localStorage.getItem(BE_VAULT_STORAGE_KEY);
if (!raw) {
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "No vault data to export.";
}
return;
}
try {
let payload;
try {
payload = JSON.parse(raw);
} catch {
payload = raw;
}
const blob = new Blob(
[JSON.stringify({ v: 1, beVaultExport: true, exportedAt: Date.now(), payload })],
{ type: "application/json" }
);
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `bonk-enhanced-vault-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
setTimeout(() => {
try {
URL.revokeObjectURL(a.href);
} catch {}
}, 5000);
if (msg) {
msg.style.color = "#9cffb0";
msg.textContent = "Backup downloaded — keep the file private.";
}
} catch (ex) {
BE_LOG.error("vault export file", ex);
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "Export failed.";
}
}
return;
}
if (act === "autofill-login") {
const msg = msgEl();
if (!beVaultSession.unlocked) {
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "Unlock the vault first (session not unlocked).";
}
BE_LOG.warn("autofill-login skipped: vault locked");
return;
}
beVaultTouchActivity();
const eid = t.getAttribute("data-entry-id");
const ent = eid ? beVaultSession.entries.find((x) => x.id === eid) : null;
if (!ent || !ent.username) {
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "Entry not found.";
}
return;
}
queueBonkLoginAutofill(ent.username, ent.password);
window.open("https://bonk.io/", "_blank");
if (msg) {
msg.style.color = "#9cffb0";
msg.textContent = "Queued autofill — check the new tab (main bonk.io page).";
setTimeout(() => {
const m = msgEl();
if (m) m.textContent = "";
}, 4500);
}
return;
}
if (!beVaultSession.unlocked) return;
beVaultTouchActivity();
if (act === "open-bonk") {
window.open("https://bonk.io/", "_blank");
return;
}
if (act === "add-entry") {
const label = (document.getElementById("beVaultAddLabel")?.value || "").trim();
const username = (document.getElementById("beVaultAddUser")?.value || "").trim();
const password = document.getElementById("beVaultAddPass")?.value || "";
const msg = msgEl();
if (!username) {
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "Enter a username.";
}
return;
}
if (beVaultSession.entries.length >= 40) {
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "Maximum 40 entries.";
}
return;
}
try {
beVaultSession.entries.push({
id: beNewEntryId(),
username,
password,
label
});
await beVaultPersist();
const u = document.getElementById("beVaultAddUser");
const p = document.getElementById("beVaultAddPass");
const l = document.getElementById("beVaultAddLabel");
if (u) u.value = "";
if (p) p.value = "";
if (l) l.value = "";
if (msg) {
msg.style.color = "#9cffb0";
msg.textContent = "Saved.";
setTimeout(() => {
const m = msgEl();
if (m) m.textContent = "";
}, 2400);
}
renderSocialPanel();
} catch (ex) {
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "Could not save.";
}
BE_LOG.error("vault add", ex);
}
return;
}
const entryId = t.getAttribute("data-entry-id");
const entry = entryId ? beVaultSession.entries.find((x) => x.id === entryId) : null;
if (act === "copy-user" && entry) {
try {
await navigator.clipboard.writeText(entry.username);
const msg = msgEl();
if (msg) {
msg.style.color = "#9cffb0";
msg.textContent = "Username copied.";
setTimeout(() => {
const m = msgEl();
if (m) m.textContent = "";
}, 2000);
}
} catch {
BE_LOG.warn("clipboard user");
}
return;
}
if (act === "copy-pass" && entry) {
try {
await navigator.clipboard.writeText(entry.password);
const msg = msgEl();
if (msg) {
msg.style.color = "#9cffb0";
msg.textContent = "Password copied.";
setTimeout(() => {
const m = msgEl();
if (m) m.textContent = "";
}, 2000);
}
} catch {
BE_LOG.warn("clipboard pass");
}
return;
}
if (act === "remove-entry" && entryId) {
try {
beVaultSession.entries = beVaultSession.entries.filter((x) => x.id !== entryId);
await beVaultPersist();
renderSocialPanel();
} catch (ex) {
BE_LOG.error("vault remove", ex);
}
return;
}
if (act === "changemp") {
const oldPw = (document.getElementById("beVaultOldMp")?.value || "").trim();
const n1 = (document.getElementById("beVaultNewMpCh1")?.value || "").trim();
const n2 = (document.getElementById("beVaultNewMpCh2")?.value || "").trim();
const msg = msgEl();
if (!oldPw || !n1) {
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "Fill current and new password.";
}
return;
}
if (n1.length < 8) {
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "New password: at least 8 characters.";
}
return;
}
if (n1 !== n2) {
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "New passwords do not match.";
}
return;
}
try {
const st = beVaultReadStored();
if (!st) throw new Error("no vault");
await beVaultDecrypt(oldPw, st);
beVaultSession.password = n1;
await beVaultPersist();
if (isVaultRememberDevice()) saveVaultRememberMaster(n1);
const o = document.getElementById("beVaultOldMp");
const a = document.getElementById("beVaultNewMpCh1");
const b = document.getElementById("beVaultNewMpCh2");
if (o) o.value = "";
if (a) a.value = "";
if (b) b.value = "";
if (msg) {
msg.style.color = "#9cffb0";
msg.textContent = "Master password updated.";
setTimeout(() => {
const m = msgEl();
if (m) m.textContent = "";
}, 3200);
}
beVaultTouchActivity();
renderSocialPanel();
} catch {
if (msg) {
msg.style.color = "#ff8e8e";
msg.textContent = "Current password wrong or save failed.";
}
}
}
};
void run();
});
sv.addEventListener("change", (e) => {
const inp = e.target;
if (!(inp instanceof HTMLInputElement) || inp.id !== "beVaultImportFile") return;
const f = inp.files?.[0];
if (!f) return;
const reader = new FileReader();
reader.onload = () => {
const msg = document.getElementById("beVaultMsg");
try {
const text = String(reader.result || "");
const o = JSON.parse(text);
const inner = o.payload !== undefined && o.payload !== null ? o.payload : o;
if (typeof inner !== "object" || inner === null) throw new Error("bad payload");
localStorage.setItem(BE_VAULT_STORAGE_KEY, JSON.stringify(inner));
beVaultLock();
if (msg) {
msg.style.color = "#9cffb0";
msg.textContent = "Imported — unlock with your master password.";
}
renderSocialPanel();
} catch (ex) {
BE_LOG.error("vault import", ex);
const m2 = document.getElementById("beVaultMsg");
if (m2) {
m2.style.color = "#ff8e8e";
m2.textContent = "Import failed — not a valid backup.";
}
}
};
reader.readAsText(f);
inp.value = "";
});
})();
// 🔥 bind safely AFTER UI exists
const items = div.querySelectorAll(".tracker-sidebar .sidebar-item");
items.forEach(item => {
item.addEventListener("click", () => {
const tab = item.dataset.tab;
switchTab(tab);
});
});
const widgetCards = widgetHub.querySelectorAll(".be-widget-card[data-open-tab]");
widgetCards.forEach(btn => {
btn.addEventListener("click", () => {
const tab = btn.getAttribute("data-open-tab");
setWidgetHubVisibility(false);
setOverlayVisibility(true);
if (tab) switchTab(tab);
});
});
document.addEventListener("click", (e) => {
if (!sidebar.contains(e.target) && !menuBtn.contains(e.target)) {
sidebar.classList.remove("open");
}
});
dragHandle.addEventListener("mousedown", (e) => {
if (e.button === 2) {
e.preventDefault();
return;
}
if (e.button !== 0) return;
isDragging = true;
div.style.cursor = "grabbing";
div.style.right = "auto";
const r = div.getBoundingClientRect();
offsetX = e.clientX - r.left;
offsetY = e.clientY - r.top;
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
div.style.right = "auto";
let left = e.clientX - offsetX;
let top = e.clientY - offsetY;
const vw = window.innerWidth;
const vh = window.innerHeight;
const pad = BE_OVERLAY_DRAG_PAD;
const peek = BE_OVERLAY_EDGE_PEEK;
const maxL = Math.max(pad, vw - peek);
const maxT = Math.max(pad, vh - peek);
left = Math.min(Math.max(pad, left), maxL);
top = Math.min(Math.max(pad, top), maxT);
div.style.left = left + "px";
div.style.top = top + "px";
});
document.addEventListener("mouseup", (e) => {
if (e.button !== 0) return;
isDragging = false;
div.style.cursor = "";
document.body.style.userSelect = "";
// 🔥 save position after drag
if (div.style.left && div.style.top) {
localStorage.setItem("bonk_ui_pos", JSON.stringify({
left: div.style.left,
top: div.style.top
}));
}
});
// 🔥 ensure one valid active tab at start
setTimeout(() => {
const active = div.querySelector(".sidebar-item.active");
if (active) {
switchTab(active.dataset.tab);
} else {
switchTab("dashboard");
}
}, 0);
// 🔥 RESIZE SYSTEM
const resizeHandle = div.querySelector("#resizeHandle");
const card = UI.card;
let isResizing = false;
resizeHandle.addEventListener("mousedown", (e) => {
if (!card) return;
e.stopPropagation(); // prevent drag conflict
isResizing = true;
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", (e) => {
if (!isResizing || !card) return;
const minWidth = 200;
const minHeight = 140;
const rect = div.getBoundingClientRect();
const newWidth = Math.max(minWidth, e.clientX - rect.left);
const newHeight = Math.max(minHeight, e.clientY - rect.top);
card.style.width = newWidth + "px";
card.style.height = newHeight + "px";
});
document.addEventListener("mouseup", () => {
if (isResizing) {
isResizing = false;
document.body.style.userSelect = "";
/* After shrinking the card, Chromium can leave scrollHeight/clientHeight stale — reflow + clamp scrollTop */
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const v = UI.root?.querySelector(".tracker-view.active");
if (!v) return;
void v.offsetHeight;
const maxScroll = Math.max(0, v.scrollHeight - v.clientHeight);
if (v.scrollTop > maxScroll) v.scrollTop = maxScroll;
});
});
}
});
}
function applyDashboardStatVisibility() {
const map = [
["xp", "rowXp"],
["wins", "rowWins"],
["rate", "rowRate"],
["lastWin", "rowLastWin"]
];
map.forEach(([key, id]) => {
const row = document.getElementById(id);
if (!row) return;
row.classList.remove("dashboard-stat-hidden");
});
}
function getOverviewSlotCount() {
if (uiSettings.size === "tiny") return 3;
if (uiSettings.size === "large") return 6;
return 4;
}
function getOverviewSlotsForCurrentSize() {
const size = uiSettings.size === "tiny" || uiSettings.size === "large" ? uiSettings.size : "medium";
const defaults = {
tiny: ["wins", "rate", "lastWin"],
medium: ["xp", "wins", "rate", "lastWin"],
large: ["xp", "wins", "rate", "lastWin", "session", "level"]
};
const count = getOverviewSlotCount();
const slots = Array.isArray(uiSettings.overviewSlots?.[size]) ? uiSettings.overviewSlots[size].slice(0, count) : defaults[size].slice(0, count);
while (slots.length < count) slots.push(defaults[size][slots.length] || "wins");
return slots;
}
function applyOverviewLayout() {
const rowMap = {
xp: document.getElementById("rowXp"),
wins: document.getElementById("rowWins"),
rate: document.getElementById("rowRate"),
lastWin: document.getElementById("rowLastWin"),
session: document.getElementById("rowSession"),
level: document.getElementById("rowLevel")
};
const parent = document.getElementById("dashboardStatsRows");
if (!parent) return;
Object.values(rowMap).forEach((el) => {
if (el) el.classList.add("dashboard-stat-hidden");
});
const slots = getOverviewSlotsForCurrentSize();
slots.forEach((key) => {
const row = rowMap[key];
if (!row) return;
row.classList.remove("dashboard-stat-hidden");
parent.appendChild(row);
});
}
function syncOverviewSlotControls() {
const container = document.getElementById("beOverviewSlots");
const sizeHint = document.getElementById("beOverviewSizeHint");
if (!container) return;
const selects = container.querySelectorAll("select[data-slot-idx]");
const slotCount = getOverviewSlotCount();
const sizeLabel = uiSettings.size === "tiny" ? "Tiny" : (uiSettings.size === "large" ? "Large" : "Medium");
if (sizeHint) sizeHint.textContent = `Current size: ${sizeLabel} (${slotCount} slots)`;
const activeSlots = getOverviewSlotsForCurrentSize();
selects.forEach((sel) => {
const idx = Number(sel.getAttribute("data-slot-idx") || "0");
const row = sel.closest(".settings-row");
if (row) row.style.display = idx < slotCount ? "flex" : "none";
sel.value = activeSlots[idx] !== undefined ? activeSlots[idx] : "wins";
});
}
function buildRecentDateKeys(days) {
const out = [];
for (let i = days - 1; i >= 0; i--) {
const d = new Date();
d.setDate(d.getDate() - i);
out.push(d.toISOString().slice(0, 10));
}
return out;
}
function getISOWeekString(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
}
function parseISOWeekToMonday(weekValue) {
const match = /^(\d{4})-W(\d{2})$/.exec(weekValue || "");
if (!match) return null;
const year = Number(match[1]);
const week = Number(match[2]);
const jan4 = new Date(Date.UTC(year, 0, 4));
const jan4Day = jan4.getUTCDay() || 7;
const monday = new Date(jan4);
monday.setUTCDate(jan4.getUTCDate() - jan4Day + 1 + (week - 1) * 7);
return new Date(monday.getUTCFullYear(), monday.getUTCMonth(), monday.getUTCDate());
}
function buildWeekDateKeys(weekValue) {
const monday = parseISOWeekToMonday(weekValue) || (() => {
const now = new Date();
const day = now.getDay() || 7;
const m = new Date(now);
m.setDate(now.getDate() - day + 1);
return m;
})();
const out = [];
for (let i = 0; i < 7; i++) {
const d = new Date(monday);
d.setDate(monday.getDate() + i);
out.push(d.toISOString().slice(0, 10));
}
return out;
}
function updateStatsStorage(data, key, xpToday, wins) {
data._dailyHistory = data._dailyHistory || {};
data._dailyHistory[key] = {
xp: Math.max(0, Math.floor(xpToday || 0)),
wins: Math.max(0, Math.floor(wins || 0))
};
data._sessions = Array.isArray(data._sessions) ? data._sessions : [];
}
function maybeSaveSession(data) {
if (!data) return;
data._sessions = Array.isArray(data._sessions) ? data._sessions : [];
const now = Date.now();
const durationSec = Math.floor((now - sessionStart) / 1000);
if (durationSec < 20) return;
const tKey = Storage.getTodayKey();
const currXP = Number(data?.[tKey]?.xp || data?._globalXP || 0);
const rawB = data?.[tKey]?.baselineXP;
const baselineXP = rawB == null ? currXP : Number(rawB);
const currWins = Math.max(0, Math.floor((currXP - baselineXP) / 100));
const gainedXP = Math.max(0, currXP - sessionStartXP);
const gainedWins = Math.max(0, currWins - sessionStartWins);
if (gainedXP === 0 && gainedWins === 0) return;
data._sessions.unshift({
at: new Date(now).toISOString(),
durationSec,
gainedXP,
gainedWins
});
data._sessions = data._sessions.slice(0, 10);
}
function renderStatsTabFromData(data) {
const chart = document.getElementById("beStatsChart");
const legend = document.getElementById("beStatsLegend");
const history = document.getElementById("beSessionHistory");
if (!chart || !legend || !history) return;
const selectedWeek = uiSettings.graphWeek || getISOWeekString(new Date());
if (uiSettings.graphWeek !== selectedWeek) {
uiSettings.graphWeek = selectedWeek;
saveUISettings();
}
const weekInput = document.getElementById("beGraphWeekSelect");
if (weekInput && weekInput.value !== selectedWeek) weekInput.value = selectedWeek;
const keys = buildWeekDateKeys(selectedWeek);
const daily = data?._dailyHistory || {};
const points = keys.map((k) => ({ key: k, xp: daily[k]?.xp || 0, wins: daily[k]?.wins || 0 }));
const maxXP = Math.max(1, ...points.map((p) => p.xp));
const maxWins = Math.max(1, ...points.map((p) => p.wins));
chart.innerHTML = "";
points.forEach((p) => {
const xpBar = document.createElement("div");
xpBar.className = "stats-bar";
xpBar.style.height = `${Math.max(4, Math.round((p.xp / maxXP) * 100))}%`;
xpBar.title = `${p.key} XP: ${p.xp}`;
chart.appendChild(xpBar);
const winsBar = document.createElement("div");
winsBar.className = "stats-bar wins";
winsBar.style.height = `${Math.max(3, Math.round((p.wins / maxWins) * 75))}%`;
winsBar.title = `${p.key} Wins: ${p.wins}`;
chart.appendChild(winsBar);
});
const totalXP = points.reduce((n, p) => n + p.xp, 0);
const totalWins = points.reduce((n, p) => n + p.wins, 0);
legend.textContent = `Week ${selectedWeek}: XP ${totalXP.toLocaleString()} | Wins ${totalWins}`;
const sessions = Array.isArray(data?._sessions) ? data._sessions : [];
if (!sessions.length) {
history.innerHTML = `<div class="stats-history-item"><span>No sessions yet</span><span>--</span></div>`;
return;
}
history.innerHTML = sessions.slice(0, 10).map((s) => {
const date = new Date(s.at);
const stamp = `${date.toLocaleDateString()} ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
const mins = Math.max(1, Math.round((s.durationSec || 0) / 60));
return `<div class="stats-history-item"><span>${stamp}</span><span>+${s.gainedWins}w / ${mins}m</span></div>`;
}).join("");
}
function renderStatsOverview(data) {
const wrap = document.getElementById("beStatsOverview");
if (!wrap) return;
const todayKey = Storage.getTodayKey();
const today = data?.[todayKey] || {};
const xp = Number(today.xp || data?._globalXP || 0);
const rawStatsB = today.baselineXP;
const baselineXP = rawStatsB == null ? xp : Number(rawStatsB);
const xpToday = Math.max(0, xp - baselineXP);
const wins = Math.max(Number(today.wins || 0), Math.floor(xpToday / 100));
const level = getLevelFromXP(xp);
const sessionSecs = Math.floor((Date.now() - sessionStart) / 1000);
const rate = sessionSecs > 30 ? ((wins / (sessionSecs / 60)) * 60).toFixed(1) : "0.0";
const rows = [
["Total XP", xp.toLocaleString()],
["XP Today", xpToday.toLocaleString()],
["Wins Today", String(wins)],
["Level", String(level)],
["Session", formatTime(sessionSecs * 1000)],
["Rate", `${rate} w/h`],
["Last Win", lastXpChangeTime ? `${Math.floor((Date.now() - lastXpChangeTime) / 60000)}m ago` : "--"]
];
wrap.innerHTML = rows.map(([k, v]) => `<div class="stats-history-item"><span>${k}</span><span>${v}</span></div>`).join("");
}
function formatTime(ms) {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
const sec = s % 60;
return `${m}:${sec.toString().padStart(2, '0')}`;
}
/** Names longer than this use an 8ch-wide viewport + marquee */
const TRACKER_NAME_MAX_CHARS = 8;
function applyTrackerPlayerName(nameEl, player) {
if (!nameEl) return;
let inner = nameEl.querySelector(".tracker-name-text");
if (!inner) {
inner = document.createElement("span");
inner.className = "tracker-name-text";
nameEl.textContent = "";
nameEl.appendChild(inner);
}
const text = String(player ?? "");
inner.textContent = text;
nameEl.title = text;
const long = text.length > TRACKER_NAME_MAX_CHARS;
if (long) {
nameEl.classList.add("tracker-name--scroll");
} else {
nameEl.classList.remove("tracker-name--scroll");
nameEl.style.removeProperty("--be-name-scroll");
nameEl.style.removeProperty("--be-name-marquee-duration");
return;
}
const measure = () => {
if (!nameEl.classList.contains("tracker-name--scroll")) return;
const w = nameEl.clientWidth;
const sw = inner.scrollWidth;
const dist = Math.max(0, sw - w);
nameEl.style.setProperty("--be-name-scroll", `${dist}px`);
const sec = Math.min(24, Math.max(6, 5 + dist / 25));
nameEl.style.setProperty("--be-name-marquee-duration", `${sec}s`);
};
requestAnimationFrame(() => requestAnimationFrame(measure));
if (!nameEl.dataset.beNameResizeObs) {
nameEl.dataset.beNameResizeObs = "1";
if (typeof ResizeObserver !== "undefined") {
const ro = new ResizeObserver(() => requestAnimationFrame(measure));
ro.observe(nameEl);
}
}
}
function updateUI() {
lastRender = Date.now();
const player = getPlayerKey();
const { data, key, storageKey } = Storage.load();
let xp = data[key].xp;
if (!xp && data._globalXP) {
xp = data._globalXP; // 🔥 hard fallback
}
if (data[key].baselineXP == null) {
const seededWins = Number(data[key].wins || 0);
const priorXp = Math.max(0, Number(data[key].xp || 0));
if (priorXp <= 0) {
data[key].baselineXP = 0;
} else {
data[key].baselineXP = Math.max(0, priorXp - seededWins * 100);
}
}
const xpToday = (() => {
const b = data[key].baselineXP;
return Math.max(0, xp - (b == null ? xp : b));
})();
const winsFromXP = Math.max(0, Math.floor(xpToday / 100));
const wins = Math.max(Number(data[key].wins || 0), winsFromXP);
if (wins !== Number(data[key].wins || 0)) {
data[key].wins = wins;
}
const level = getLevelFromXP(xp);
// 🔥 detect login/logout change
if (player !== currentPlayer) {
maybeSaveSession(data);
Storage.save(data, storageKey);
currentPlayer = player;
// 🔥 reset storage cache
Storage.invalidate();
// 🔥 reset state
lastUIState = {};
hasRenderedOnce = false;
sessionStart = Date.now();
sessionStartXP = xp || 0;
sessionStartWins = wins || 0;
previousWins = wins || 0;
noXpCount = 0;
lastXpChangeTime = 0;
beFarmEverOnThisSession = false;
// 🔥 reset socket
bonkWSS = null;
// 🔥 UI refresh (with fade)
UI.root.classList.remove("tracker-visible");
UI.root.classList.add("tracker-fade");
setTimeout(() => {
UI.root.classList.remove("tracker-fade");
UI.root.classList.add("tracker-visible");
Events.emit("xpUpdate");
}, 200);
return;
}
// optional: force fresh structure
if (!data._globalXP) {
data._globalXP = 0;
Storage.save(data, storageKey);
}
///
updateStatsStorage(data, key, xpToday, wins);
Storage.save(data, storageKey);
renderStatsTabFromData(data);
renderStatsOverview(data);
UI.xpGain.innerText =
"+" + xp.toLocaleString();
const isGuest = !xp; // 🔥 guest = no XP
if (!isGuest && player && player !== "Guest") {
recordCurrentAltSnapshot(player, level, xp);
}
UI.winsLine.innerText =
`${wins} / ${DAILY_CAP}`;
const sessionTime = Date.now() - sessionStart;
UI.sessionLine.innerText =
"⏱ " + formatTime(sessionTime);
if (UI.sessionStatLine) UI.sessionStatLine.innerText = formatTime(sessionTime);
const minutes = sessionTime / 60000;
// 🔥 rate based on DAILY wins
let rate = minutes > 0.5 ? (wins / minutes) * 60 : 0;
UI.rateLine.innerText =
rate.toFixed(1) + " w/h";
const safeLevel = Math.max(1, level);
const currentLevelXP = getXPForLevel(safeLevel);
const nextLevelXP = getXPForLevel(safeLevel + 1);
const xpNeeded = nextLevelXP - currentLevelXP;
const xpIntoLevel = Math.min(
xpNeeded,
Math.max(0, xp - currentLevelXP)
);
lastUIState = { xp, wins, level, player };
hasRenderedOnce = true;
const progress = xpNeeded > 0 ? (xpIntoLevel / xpNeeded) * 100 : 0;
UI.xpPercent.innerText =
Math.floor(progress) + "%";
const nameEl = UI.playerName;
// 🔥 SET PLAYER SKIN
if (UI.playerSkin) {
if (!topBarEl) {
topBarEl = document.getElementById("pretty_top_bar");
}
const topBar = topBarEl;
if (topBar) {
const skinElement = topBar.querySelector("img");
if (skinElement) {
const src = skinElement.src;
// 🔥 ONLY update if changed
if (src !== lastSkinSrc) {
lastSkinSrc = src;
const clone = skinElement.cloneNode(true);
clone.style.width = "100%";
clone.style.height = "100%";
clone.style.display = "block";
clone.style.margin = "0";
UI.playerSkin.innerHTML = "";
UI.playerSkin.appendChild(clone);
}
}
}
}
if (nameEl) {
applyTrackerPlayerName(nameEl, player);
}
if (isGuest) {
UI.levelLine.innerText = ""; // 🔥 hide completely
} else {
UI.levelLine.innerText = `Level ${level}`;
}
if (UI.levelStatLine) UI.levelStatLine.innerText = String(level);
const circle = UI.xpCircle;
const offset = CIRCLE_CIRCUMFERENCE - (progress / 100) * CIRCLE_CIRCUMFERENCE;
circle.style.strokeDashoffset = offset;
const xpTextEl = UI.xpText;
if (xpTextEl) {
const winsIntoLevel = Math.floor(xpIntoLevel / 100);
const winsNeeded = Math.floor(xpNeeded / 100);
const winsLeft = winsNeeded - winsIntoLevel;
xpTextEl.innerText = `${winsLeft} wins left`;
xpTextEl.title = `${winsIntoLevel} / ${winsNeeded} wins to next level`;
}
if (UI.xpCircleWrapper) {
const winsIntoLevel = Math.floor(xpIntoLevel / 100);
const winsNeeded = Math.floor(xpNeeded / 100);
UI.xpCircleWrapper.title = `${winsIntoLevel} / ${winsNeeded} wins to next level`;
}
// last win time
if (UI.lastWinLine) {
if (lastXpChangeTime === 0) {
UI.lastWinLine.innerText = "--";
} else {
const minAgo = Math.floor((Date.now() - lastXpChangeTime) / 60000);
UI.lastWinLine.innerText = minAgo === 0 ? "just now" : `${minAgo}m ago`;
}
}
// daily progress bar
if (UI.dailyProgressBar) {
UI.dailyProgressBar.style.width = Math.min(100, (wins / DAILY_CAP) * 100) + "%";
if (wins > previousWins) {
UI.dailyProgressBar.classList.remove("be-gain-flash");
void UI.dailyProgressBar.offsetWidth;
UI.dailyProgressBar.classList.add("be-gain-flash");
}
}
previousWins = wins;
const statusEl = UI.statusText;
// CAPPED border + label only after today’s wins reach the daily cap (see DAILY_CAP). Not tied to bonk_xpStopped.
const isCapped = wins >= DAILY_CAP;
if (isCapped) {
if (statusEl) {
statusEl.style.display = "";
statusEl.innerHTML = "⛔ CAPPED";
statusEl.style.color = "#ff4c4c";
}
setTrackerBorderByStatus("capped");
} else if (xpEnabled) {
if (statusEl) {
statusEl.style.display = "";
statusEl.innerHTML = `<span class="pulse-dot"></span>FARMING`;
statusEl.style.color = "#4caf50";
}
setTrackerBorderByStatus("farming");
} else {
if (statusEl) {
if (beFarmEverOnThisSession) {
statusEl.style.display = "";
statusEl.innerHTML = "⏸ IDLE";
statusEl.style.color = "#aaa";
} else {
statusEl.innerHTML = "";
statusEl.style.display = "none";
}
}
setTrackerBorderByStatus("idle");
}
}
function updateSessionOnly() {
const sessionTime = Date.now() - sessionStart;
UI.sessionLine.innerText = "⏱ " + formatTime(sessionTime);
if (UI.sessionStatLine) UI.sessionStatLine.innerText = formatTime(sessionTime);
if (UI.lastWinLine && lastXpChangeTime > 0) {
const minAgo = Math.floor((Date.now() - lastXpChangeTime) / 60000);
UI.lastWinLine.innerText = minAgo === 0 ? "just now" : `${minAgo}m ago`;
}
}
function setTrackerBorderByStatus(status) {
if (!UI.root) return;
const card = UI.root.querySelector(".tracker-card");
if (!card) return;
let border;
let glow;
if (status === "capped") {
border = "rgba(255, 76, 76, 0.9)";
glow = "0 0 14px rgba(255, 76, 76, 0.25)";
} else if (status === "farming") {
border = "rgba(76, 175, 80, 0.9)";
glow = "0 0 14px rgba(76, 175, 80, 0.25)";
} else {
border = "rgba(255, 170, 0, 0.9)";
glow = "0 0 14px rgba(255, 170, 0, 0.2)";
}
card.style.borderColor = border;
card.style.boxShadow = glow;
const dot = document.querySelector("#pretty_top_be_launcher .be-top-launcher-dot");
if (dot) dot.style.background = border;
}
function getXPForLevel(n) {
return (n - 1) * (n - 1) * 100;
}
/** Prefer Bonk / engine.io sockets over analytics or other extras (Brave often opens several). */
function beXpSocketUrlScore(urlStr) {
const s = String(urlStr || "").toLowerCase();
if (s.includes("peerjs")) return -1;
if (s.includes("bonk") && (s.includes("socket") || s.includes("engine"))) return 100;
if (s.includes("bonk.io")) return 90;
if (s.includes("socket.io") || s.includes("engine.io")) return 50;
return 5;
}
function beMaybePreferBonkWsForPoll(ws) {
if (!ws) return;
const u = typeof ws.url === "string" ? ws.url : "";
if (beXpSocketUrlScore(u) < 0) return;
const cur = bonkWSS;
const curOpen = cur && cur.readyState === WebSocket.OPEN;
const curSc = curOpen ? beXpSocketUrlScore(cur.url) : -100;
const sc = beXpSocketUrlScore(u);
if (!curOpen || sc >= curSc) {
bonkWSS = ws;
}
}
const originalSend = WebSocket.prototype.send;
WebSocket.prototype.send = function (...args) {
const u = typeof this.url === "string" ? this.url : "";
if (!u.includes("peerjs")) {
if (!this._xpHooked) {
this._xpHooked = true;
this.addEventListener("message", (event) => {
XPEngine.handleMessage(event);
});
}
beMaybePreferBonkWsForPoll(this);
}
return originalSend.apply(this, args);
};
function getLevelFromXP(xp) {
return Math.floor(Math.sqrt(xp / 100)) + 1;
}
function init() {
if (!BE_CONFIG.featureFlags.enableTracker) {
markHealth("tracker.init", false, "feature flag disabled");
return;
}
beRegisterFarmApi();
safeRun("tracker.legacyThemeBridgeCleanup", () => {
try {
document.getElementById("be-bonk-excigma-theme-bridge")?.remove();
} catch {}
try {
const fr = document.getElementById("maingameframe");
const ocd = fr && fr.contentDocument;
if (ocd) {
ocd.getElementById("be-bonk-excigma-theme-bridge")?.remove();
ocd.getElementById("be-bonk-excigma-theme-bridge-outer")?.remove();
}
} catch {}
});
void (async () => {
try {
await UIEngine.init();
markHealth("tracker.uiInit", true, "createUI awaited");
} catch (e) {
markHealth("tracker.uiInit", false, e?.message || "failed");
BE_LOG.error("tracker.uiInit", e);
}
waitForPlayer(() => {
markHealth("tracker.playerReady", true, "player element ready");
// 🔥 INSTANT render using cached data
const { data, key } = Storage.load();
// 🔥 FORCE instant XP from cache
if (data._globalXP && !data[key].xp) {
data[key].xp = data._globalXP;
}
const initialXP = data[key].xp || data._globalXP || 0;
const _ib = data[key].baselineXP;
const initialBaseline = _ib == null ? initialXP : _ib;
sessionStartXP = initialXP;
sessionStartWins = Math.max(0, Math.floor((initialXP - initialBaseline) / 100));
previousWins = sessionStartWins;
lastUIState = {};
safeRun("tracker.firstRender", () => updateUI());
// 🔥 force a second render after everything stabilizes
setTimeout(() => {
lastUIState = {};
safeRun("tracker.secondRenderEmit", () => Events.emit("xpUpdate"));
}, 300);
waitForSocket(() => {
markHealth("tracker.socketReady", true, "socket open");
let attempts = 0;
// 🔥 wake up server (minimal activity)
setTimeout(() => {
try {
if (bonkWSS && bonkWSS.readyState === WebSocket.OPEN && !beIsBonkRoomListVisible()) {
bonkWSS.send("42[0]");
bonkWSS.send("42[38]");
setTimeout(() => {
try {
if (bonkWSS && bonkWSS.readyState === WebSocket.OPEN && !beIsBonkRoomListVisible()) {
bonkWSS.send("42[38]");
}
} catch {}
}, 300);
}
} catch {}
}, 500);
const trySync = () => {
if (!bonkWSS || bonkWSS.readyState !== WebSocket.OPEN) {
setTimeout(trySync, beBackoffMs(300, 4));
return;
}
attempts++;
beRequestOneXpPoll();
const { data, key } = Storage.load();
const synced = data[key]?._synced;
if (synced) {
lastUIState = {};
safeRun("tracker.syncRenderEmit", () => Events.emit("xpUpdate"));
return;
}
setTimeout(trySync, beBackoffMs(1000, 3));
};
trySync();
});
});
// lightweight timer — slower while tab is hidden
let __beSessionTicker = null;
function beRestartSessionTicker() {
if (__beSessionTicker) {
try {
clearInterval(__beSessionTicker);
} catch {}
__beSessionTicker = null;
}
const ms = beTabHidden() ? 5000 : 1000;
__beSessionTicker = setInterval(() => UIEngine.updateSession(), ms);
}
if (!window.__beSessionVisHooked) {
window.__beSessionVisHooked = true;
document.addEventListener("visibilitychange", () => beRestartSessionTicker());
}
beRestartSessionTicker();
markHealth("tracker.sessionTicker", true, "active");
// real updates when XP changes
Events.on("xpUpdate", UIEngine.render);
markHealth("tracker.xpEventBinding", true, "bound");
// 🔥 observe login/name changes (replaces 2s polling)
let nameEl = document.getElementById("pretty_top_name");
if (!nameEl) {
setTimeout(() => {
nameEl = document.getElementById("pretty_top_name");
if (!nameEl) return;
const observer = new MutationObserver(() => {
const newPlayer = getPlayerKey();
if (newPlayer !== currentPlayer) Events.emit("xpUpdate");
});
observer.observe(nameEl, {
childList: true,
characterData: true,
subtree: true
});
}, 2000);
} else {
const observer = new MutationObserver(() => {
const newPlayer = getPlayerKey();
if (newPlayer !== currentPlayer) Events.emit("xpUpdate");
});
observer.observe(nameEl, {
childList: true,
characterData: true,
subtree: true
});
}
safeRun("tracker.loginPwGuard", () => {
function hardenIfPresent() {
const u = document.getElementById("loginwindow_username");
const p = document.getElementById("loginwindow_password");
if (u && p) beHardenBonkLoginFields(u, p);
}
hardenIfPresent();
if (document.body) {
const mo = new MutationObserver(hardenIfPresent);
mo.observe(document.body, { childList: true, subtree: true });
}
});
})();
}
window.addEventListener("load", () => {
markHealth("tracker.windowLoad", true, "loaded");
setTimeout(() => safeRun("tracker.init", init), 3000);
// 🔥 ultra-light fallback (only if UI hasn't rendered in a long time)
let __beFallbackTicker = null;
function beRestartFallbackTicker() {
if (__beFallbackTicker) {
try {
clearInterval(__beFallbackTicker);
} catch {}
__beFallbackTicker = null;
}
const ms = beTabHidden() ? 40000 : 10000;
__beFallbackTicker = setInterval(() => {
if (Date.now() - lastRender > 20000) {
lastUIState = {};
safeRun("tracker.fallbackRenderEmit", () => Events.emit("xpUpdate"));
}
}, ms);
}
if (!window.__beFallbackVisHooked) {
window.__beFallbackVisHooked = true;
document.addEventListener("visibilitychange", () => beRestartFallbackTicker());
}
beRestartFallbackTicker();
});
window.addEventListener("beforeunload", () => {
try {
const cache = Storage.load();
if (!cache) return;
maybeSaveSession(cache.data);
Storage.save(cache.data, cache.storageKey);
} catch {}
});
document.addEventListener("keydown", (e) => {
if (!UI.root) return;
if (isCapturingHotkey) return;
if (e.repeat) return;
const target = e.target;
const tag = target?.tagName?.toLowerCase?.() || "";
const isEditable = tag === "input" || tag === "textarea" || target?.isContentEditable;
if (isEditable) return;
if (
e.altKey &&
!e.ctrlKey &&
!e.metaKey &&
!e.shiftKey &&
!e.repeat &&
UI.root &&
UI.root.style.display !== "none" &&
UI.root.classList.contains("be-overlay-open") &&
typeof UI.switchTab === "function"
) {
const k = e.key;
if (k >= "1" && k <= "7") {
const tabs = [
"dashboard",
"graph",
"statsOverview",
"calculator",
"social",
"visuals",
"settings"
];
e.preventDefault();
UI.switchTab(tabs[parseInt(k, 10) - 1]);
return;
}
}
if (e.ctrlKey || e.metaKey || e.altKey) return;
if (e.key === "Escape") {
if (UI.widgetHub && UI.widgetHub.style.display !== "none") {
e.preventDefault();
setWidgetHubVisibility(false);
return;
}
if (UI.root.style.display !== "none" && UI.root.classList.contains("be-overlay-open")) {
e.preventDefault();
setOverlayVisibility(false);
return;
}
}
if (e.key === uiSettings.widgetHotkey) {
e.preventDefault();
toggleWidgetHub();
return;
}
if (e.key === uiSettings.overlayHotkey) {
e.preventDefault();
toggleOverlayVisibility();
}
}, true);
if (!window.bonkCodeInjectors) window.bonkCodeInjectors = [];
window.bonkCodeInjectors.push(bonkCode => {
let newSrc = bonkCode;
let discID = newSrc.match(/this\.discGraphics\[([\w$]{2,4})\]=null;\}/)[1];
newSrc = newSrc.replace(`this.discGraphics[${discID}]=null;}`, `this.discGraphics[${discID}]=null;} else {
if(this.discGraphics[${discID}]){
const _v = window.BonkVisuals;
if(this.discGraphics[${discID}].sfwSkin){
this.discGraphics[${discID}].playerGraphic.alpha = _v.players.skins ? 1 : 0;
this.discGraphics[${discID}].sfwSkin.visible = !_v.players.skins;
} else if(this.discGraphics[${discID}]?.avatar?.bc != undefined){
this.discGraphics[${discID}].sfwSkin = new PIXI.Graphics;
this.discGraphics[${discID}].sfwSkin.beginFill(this.discGraphics[${discID}].teamify(this.discGraphics[${discID}].avatar.bc, this.discGraphics[${discID}].team));
this.discGraphics[${discID}].sfwSkin.drawCircle(0,0,this.discGraphics[${discID}].radius);
this.discGraphics[${discID}].sfwSkin.endFill();
this.discGraphics[${discID}].container.addChildAt(this.discGraphics[${discID}].sfwSkin, 3);
}
this.discGraphics[${discID}].nameText.alpha = _v.players.usernames.visible ? _v.players.usernames.alpha : 0;
var _beFn=window.BonkEnhanced&&window.BonkEnhanced.friendNamesLower;
if(_beFn&&_beFn.length&&this.discGraphics[${discID}].nameText){
var _beT=this.discGraphics[${discID}].nameText;
var _beN=(_beT.text!==undefined&&_beT.text!==null?String(_beT.text):"").trim().toLowerCase();
if(_beN&&_beFn.indexOf(_beN)>=0){
_beT.tint=16766720;
}
}
if(this.discGraphics[${discID}].playerID != this.localPlayerID){
this.discGraphics[${discID}].container.visible = _v.players.visible;
this.discGraphics[${discID}].container.alpha = _v.players.alpha;
}
}
}`);
let buildRendererFunction = newSrc.match(/(build\([\w$]{2,4},[\w$]{2,4}\)) \{.{30,150}=new [\w$]{2,4}\[[0-9]+\]\(/)[1];
newSrc = newSrc.replace(`${buildRendererFunction} {`, `${buildRendererFunction} {
window.BonkVisuals.chatWindow = document.querySelector('#ingamechatbox');
`);
return newSrc;
});
})();