您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tracks your Bonk.io wins & losses with server-side verified accounts using BonkBot API. Secure sessions, rate limiting, and server-determined usernames only. Auto-cleans session on tab close.
// ==UserScript== // @name Bonk.io Win/Loss Tracker (Bonkverse, verified) // @namespace http://tampermonkey.net/ // @version 4.0 // @description Tracks your Bonk.io wins & losses with server-side verified accounts using BonkBot API. Secure sessions, rate limiting, and server-determined usernames only. Auto-cleans session on tab close. // @author you // @match https://bonk.io/gameframe-release.html // @grant none // @license MIT // ==/UserScript== (() => { console.log("Loading Bonk.io Win/Loss Tracker (Bonkverse, verified)..."); // ---------- Config ---------- const API_BASE = "https://bonkverse.io"; const API_REQUEST_VERIFICATION = `${API_BASE}/api/request_verification/`; const API_COMPLETE_VERIFICATION = `${API_BASE}/api/complete_verification/`; const API_WINS = `${API_BASE}/api/wins/`; const API_LOSSES = `${API_BASE}/api/losses/`; const API_HEARTBEAT = `${API_BASE}/api/heartbeat/`; const API_STOP = `${API_BASE}/api/stop_tracking/`; // Make sure to update version above as well const VERSION = "4.0"; // ---------- State ---------- let currentUser = null; let winsTotal = 0; let lossesTotal = 0; let lastWinName = null; let lastWinTime = 0; let lastLossName = null; let lastLossTime = 0; let statusMessage = ""; let sessionToken = null; let heartbeatTimer = null; let verificationId = null; let verificationRoom = null; // ---------- Utils ---------- function getCurrentMap() { const el = document.getElementById("newbonklobby_maptext"); return el ? (el.textContent || "").trim() : null; } function setStatus(msg, color = "#aaa") { statusMessage = `<div style="margin-top:6px; font-size:12px; color:${color};">${msg}</div>`; updateUI(); } // ---------- Verification Flow ---------- function startVerification() { setStatus("⏳ Requesting verification room..."); fetch(API_REQUEST_VERIFICATION, { method: "POST" }) .then((res) => res.json()) .then((data) => { if (data.success) { verificationId = data.verification_id; verificationRoom = data.room_url; setStatus( `🔗 Join room to verify: <a href="${verificationRoom}" target="_blank" style="color:#6cf">Click here</a>`, "#80ff80" ); pollVerification(); } else { setStatus(`❌ Failed: ${data.reason}`, "#ff4d4d"); } }) .catch((err) => { console.error(err); setStatus("❌ Error contacting server.", "#ff4d4d"); }); } function pollVerification() { const start = Date.now(); const interval = setInterval(() => { fetch(API_COMPLETE_VERIFICATION, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ verification_id: verificationId }), }) .then((res) => res.json()) .then((data) => { if (data.success) { clearInterval(interval); currentUser = data.username; sessionToken = data.token; setStatus(`✅ Verified as ${currentUser}`, "#80ff80"); document.getElementById("startTrackBtn").style.display = "none"; document.getElementById("stopTrackBtn").style.display = "inline-block"; heartbeatTimer = setInterval(() => { fetch(API_HEARTBEAT, { method: "POST", headers: { Authorization: "Bearer " + sessionToken }, }); }, 30000); } else { const reason = (data.reason || "").toLowerCase(); if (reason.includes("not verified yet")) { // still pending } else { clearInterval(interval); setStatus(`❌ Verification failed: ${data.reason}`, "#ff4d4d"); document.getElementById("stopTrackBtn").style.display = "none"; document.getElementById("startTrackBtn").style.display = "inline-block"; } } if (Date.now() - start > 70000) { clearInterval(interval); setStatus("❌ Verification timed out", "#ff4d4d"); document.getElementById("stopTrackBtn").style.display = "none"; document.getElementById("startTrackBtn").style.display = "inline-block"; } }) .catch((err) => { console.error("pollVerification error:", err); clearInterval(interval); setStatus("❌ Error contacting server.", "#ff4d4d"); document.getElementById("stopTrackBtn").style.display = "none"; document.getElementById("startTrackBtn").style.display = "inline-block"; }); }, 3000); } // ---------- Session Stop ---------- function stopTracking(sync = false) { if (!sessionToken) return; if (sync && navigator.sendBeacon) { const headers = { type: "application/json" }; const blob = new Blob([JSON.stringify({})], headers); navigator.sendBeacon(API_STOP, blob); } else { fetch(API_STOP, { method: "POST", headers: { Authorization: "Bearer " + sessionToken }, }).finally(() => { clearInterval(heartbeatTimer); sessionToken = null; currentUser = null; setStatus("ℹ️ Tracking stopped", "#aaa"); document.getElementById("stopTrackBtn").style.display = "none"; document.getElementById("startTrackBtn").style.display = "inline-block"; }); } } // ---------- UI ---------- function injectUI() { if (!document.getElementById("winTrackerBox")) { const box = document.createElement("div"); box.id = "winTrackerBox"; box.innerHTML = ` <div id="winTrackerHeader"> <span id="winTrackerTitle">🏆 Bonkverse Tracker v${VERSION}</span> <button id="winTrackerToggle">–</button> </div> <div id="winTrackerContent"></div> <div id="winTrackerControls" style="margin:10px; display:flex; gap:10px; justify-content:center;"> <button id="startTrackBtn" class="bv-btn bv-btn-start">▶ Verify & Start</button> <button id="stopTrackBtn" class="bv-btn bv-btn-stop" style="display:none;">⏹ Stop</button> </div> <div style="margin-top:8px; font-size:12px; color:#aaa;"> Check your rank: <a href="https://bonkverse.io/leaderboards/wins/today/" target="_blank" style="color:#00e6c3; text-decoration:underline;"> Bonkverse Wins </a> | <a href="https://bonkverse.io/leaderboards/losses/today/" target="_blank" style="color:#ff4d4d; text-decoration:underline;"> Bonkverse Losses </a> </div> `; Object.assign(box.style, { position: "fixed", top: "100px", right: "20px", background: "rgba(26,39,51,0.9)", border: "2px solid #009688", boxShadow: "0px 0px 12px rgba(0, 230, 195, 0.6)", borderRadius: "10px", fontFamily: '"Oxanium", sans-serif', color: "#ddd", zIndex: "99999", minWidth: "240px", textAlign: "center", userSelect: "none", }); const header = box.querySelector("#winTrackerHeader"); Object.assign(header.style, { display: "flex", justifyContent: "space-between", alignItems: "center", background: "rgba(0,150,136,0.2)", padding: "6px 10px", cursor: "move", borderBottom: "1px solid rgba(255,255,255,0.1)" }); makeDraggable(box, header); const toggleBtn = box.querySelector("#winTrackerToggle"); Object.assign(toggleBtn.style, { background: "none", border: "none", color: "#ddd", fontSize: "16px", cursor: "pointer" }); toggleBtn.addEventListener("click", () => { const content = box.querySelector("#winTrackerContent"); const controls = box.querySelector("#winTrackerControls"); if (content.style.display === "none") { content.style.display = "block"; controls.style.display = "flex"; toggleBtn.textContent = "–"; } else { content.style.display = "none"; controls.style.display = "none"; toggleBtn.textContent = "+"; } }); document.body.appendChild(box); document.getElementById("startTrackBtn").onclick = startVerification; document.getElementById("stopTrackBtn").onclick = () => stopTracking(false); } } function makeDraggable(el, handle) { let offsetX = 0, offsetY = 0, dragging = false; handle.onmousedown = (e) => { dragging = true; offsetX = e.clientX - el.offsetLeft; offsetY = e.clientY - el.offsetTop; document.onmousemove = (e) => { if (dragging) { el.style.left = e.clientX - offsetX + "px"; el.style.top = e.clientY - offsetY + "px"; el.style.right = "auto"; } }; document.onmouseup = () => { dragging = false; document.onmousemove = null; document.onmouseup = null; }; }; } function updateUI() { injectUI(); const content = document.querySelector("#winTrackerContent"); if (content) { if (currentUser) { const mapName = getCurrentMap() || "Unknown"; content.innerHTML = ` <div style="margin:8px 0;">👤 <span style="color:#6cf">${currentUser}</span></div> <div>🏆 Wins this session: <span style="color:#00ffcc">${winsTotal}</span></div> <div>💀 Losses this session: <span style="color:#ff4d4d">${lossesTotal}</span></div> <div>🗺 Map: <span style="color:#ffcc00">${mapName}</span></div> ${statusMessage} `; } else { content.innerHTML = `<div style="color:#ff4d4d">❌ Not verified</div>${statusMessage}`; } } } // ---------- API send ---------- function sendWinToServer(username) { if (!sessionToken) return; const mapName = getCurrentMap(); const now = Date.now(); if (now - lastWinTime < 5000) return; lastWinTime = now; fetch(API_WINS, { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + sessionToken, }, body: JSON.stringify({ username, ts: now, map_name: mapName || "Unknown" }), }); } function sendLossToServer(username) { if (!sessionToken) return; const mapName = getCurrentMap(); const now = Date.now(); if (now - lastLossTime < 5000) return; lastLossTime = now; fetch(API_LOSSES, { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + sessionToken, }, body: JSON.stringify({ username, ts: now, map_name: mapName || "Unknown" }), }); } // ---------- Detect win/loss ---------- function updateResult() { const topEl = document.getElementById("ingamewinner_top"); if (!topEl) return; const winnerName = (topEl.textContent || "").trim(); if (!currentUser || !winnerName) return; const teamNames = ["red team", "blue team", "yellow team", "green team"]; if (winnerName === currentUser && lastWinName !== winnerName) { winsTotal++; lastWinName = winnerName; console.log(`Win detected for ${winnerName} → total=${winsTotal}`); updateUI(); sendWinToServer(currentUser); } else if (winnerName !== currentUser && lastLossName !== winnerName) { // 🛑 Skip team-based wins const lower = winnerName.toLowerCase(); if (teamNames.includes(lower)) { console.log(`Ignoring team result: ${winnerName}`); return; } lossesTotal++; lastLossName = winnerName; console.log(`Loss detected for ${currentUser} → total=${lossesTotal}`); updateUI(); sendLossToServer(currentUser); } } // ---------- Winner observer ---------- function hookWinnerObserver() { const target = document.getElementById("ingamewinner"); if (!target) { setTimeout(hookWinnerObserver, 1000); return; } const observer = new MutationObserver(() => { const visible = target.style.visibility !== "hidden"; if (visible) updateResult(); else { lastWinName = null; lastLossName = null; } }); observer.observe(target, { attributes: true, attributeFilter: ["style"], subtree: true }); console.log("Win/Loss Tracker: observer attached to #ingamewinner"); } // ---------- Inject CSS ---------- const style = document.createElement("style"); style.textContent = ` .bv-btn { padding: 8px 14px; border-radius: 10px; font-family: "Oxanium", sans-serif; font-size: 14px; font-weight: bold; cursor: pointer; transition: border-color .15s ease, transform .05s ease, box-shadow .2s ease; border: 1px solid rgba(0, 230, 195, .35); background: linear-gradient(180deg, rgba(0,150,136,.25), rgba(0,150,136,.15)); color: #fff; text-shadow: 0 0 4px rgba(0,0,0,0.6); } .bv-btn:hover { border-color: rgba(0,230,195,.75); box-shadow: 0 0 8px rgba(0,230,195,.6); } .bv-btn:active { transform: translateY(1px) scale(.97); } .bv-btn-start { background: linear-gradient(180deg, rgba(40,167,69,.4), rgba(40,167,69,.2)); border-color: rgba(40,167,69,.55); } .bv-btn-start:hover { border-color: rgba(40,167,69,.9); box-shadow: 0 0 10px rgba(40,167,69,.7); } .bv-btn-stop { background: linear-gradient(180deg, rgba(220,53,69,.4), rgba(220,53,69,.2)); border-color: rgba(220,53,69,.55); } .bv-btn-stop:hover { border-color: rgba(220,53,69,.9); box-shadow: 0 0 10px rgba(220,53,69,.7); } `; document.head.appendChild(style); // ---------- Init ---------- hookWinnerObserver(); setInterval(updateUI, 2000); window.addEventListener("beforeunload", () => stopTracking(true)); })();