dual reload bars, ping/ms, fps, crosshair — press ` to toggle menu (Note: crosshair currently doesn't work i will be working on that in net]xt update)
// ==UserScript==
// @name Customization mod
// @namespace wat
// @version 6.0
// @description dual reload bars, ping/ms, fps, crosshair — press ` to toggle menu (Note: crosshair currently doesn't work i will be working on that in net]xt update)
// @author wat
// @license MIT
// @match *://moomoo.io/*
// @match *://sandbox.moomoo.io/*
// @run-at document-start
// @grant none
// ==/UserScript==
;(function () {
'use strict'
// ╔══════════════════════════════════════════════════════════════════╗
// ║ SETTINGS ║
// ╚══════════════════════════════════════════════════════════════════╝
const DEFAULTS = {
pri_bar: true,
pri_color: '#00ccff',
sec_bar: true,
sec_color: '#ff8800',
bar_pos: 'bottom', // 'bottom' | 'topleft' | 'topright'
bar_scale: 100,
fps: false,
ping: true,
xhair: 'off', // 'off' | 'cross' | 'dot' | 'circle' | 'tcross'
xhair_color: '#ffffff',
xhair_size: 10,
chat_opacity: 85,
chat_hide: false,
}
const S = (() => {
try { return Object.assign({}, DEFAULTS, JSON.parse(localStorage.getItem('watmod6') || '{}')) }
catch (_) { return Object.assign({}, DEFAULTS) }
})()
function save () {
try { localStorage.setItem('watmod6', JSON.stringify(S)) } catch (_) {}
}
// ╔══════════════════════════════════════════════════════════════════╗
// ║ WEAPON COOLDOWN TABLES (ms) ║
// ╚══════════════════════════════════════════════════════════════════╝
const PRI_CD = {
0: 400, 1: 400, 2: 400, 3: 500, 4: 500, 5: 500, // hammers / fist
6: 400, 7: 350, 8: 700, 9: 500, 10: 300, 11: 400,
12: 600, 13: 600, // melee
14: 800, 15: 900, 16: 1100, 17: 700, 18: 750, // bows
51: 550, 52: 500, // guns
}
// secondary = building placements, traps, etc.
const SEC_CD = {
default: 350,
}
function getPriCd () {
try {
const p = window.myPlayer || window.player || {}
const idx = p.weaponIndex ?? p.weapon ?? p.selectedWeapon ?? 0
return PRI_CD[idx] ?? 400
} catch (_) { return 400 }
}
function getSecCd () {
try {
const p = window.myPlayer || window.player || {}
// secondary items (spikes, traps, walls) generally 350ms
return p.buildCooldown ?? SEC_CD.default
} catch (_) { return 350 }
}
// ╔══════════════════════════════════════════════════════════════════╗
// ║ PING MEASUREMENT ║
// ║ Hook the constructor so we catch the WS before the game does ║
// ╚══════════════════════════════════════════════════════════════════╝
let ping = 0 // smoothed ms value shown in HUD
let pingRaw = 0 // last raw sample
;(function patchWS () {
const Native = window.WebSocket
function Patched (url, protos) {
const ws = protos != null ? new Native(url, protos) : new Native(url)
// ring buffer for outgoing timestamps
const OUT_SIZE = 64
const outBuf = new Float64Array(OUT_SIZE)
let outW = 0 // write cursor
let outR = 0 // read cursor (oldest unmatched)
// rolling sample pool — last 16 RTT measurements
const POOL = new Float64Array(16)
let poolW = 0
let poolN = 0
const origSend = ws.send.bind(ws)
ws.send = function (data) {
outBuf[outW & (OUT_SIZE - 1)] = performance.now()
outW++
return origSend(data)
}
ws.addEventListener('message', () => {
// only process if we have an unmatched send
if (outR >= outW) return
const rtt = performance.now() - outBuf[outR & (OUT_SIZE - 1)]
outR++
// discard noise — keep sensible RTT values
if (rtt < 0.5 || rtt > 1200) return
POOL[poolW & 15] = rtt
poolW++
poolN = Math.min(poolN + 1, 16)
// weighted average (recent samples weigh more)
let sum = 0, wsum = 0
for (let i = 0; i < poolN; i++) {
const idx = ((poolW - 1 - i) & 15)
const w = poolN - i
sum += POOL[idx] * w
wsum += w
}
pingRaw = Math.round(sum / wsum)
// lerp toward new value so display doesn't thrash
ping = Math.round(ping + (pingRaw - ping) * 0.3)
})
return ws
}
// keep instanceof working
Patched.prototype = Native.prototype
Object.setPrototypeOf(Patched, Native)
window.WebSocket = Patched
})()
// ╔══════════════════════════════════════════════════════════════════╗
// ║ OVERLAY CANVAS — all HUD elements draw here ║
// ╚══════════════════════════════════════════════════════════════════╝
let ov, oc
function initOverlay () {
if (document.getElementById('wat-ov')) return
ov = document.createElement('canvas')
ov.id = 'wat-ov'
ov.style.cssText = 'position:fixed;top:0;left:0;pointer-events:none;z-index:999999;'
ov.width = innerWidth
ov.height = innerHeight
document.body.appendChild(ov)
oc = ov.getContext('2d')
addEventListener('resize', () => { ov.width = innerWidth; ov.height = innerHeight })
}
// ╔══════════════════════════════════════════════════════════════════╗
// ║ RELOAD BAR STATE ║
// ╚══════════════════════════════════════════════════════════════════╝
const bars = {
pri: { startT: 0, dur: 400, active: false, displayed: false },
sec: { startT: 0, dur: 350, active: false, displayed: false },
}
function arm (which, cd) {
const b = bars[which]
b.startT = performance.now()
b.dur = cd
b.active = true
}
function initInputHooks () {
// ── primary (LMB) ──────────────────────────────────────────────
let lmbHeld = false
let lmbTimer = null
function firePri () {
arm('pri', getPriCd())
}
document.addEventListener('mousedown', e => {
if (e.button !== 0) return
lmbHeld = true
firePri()
clearInterval(lmbTimer)
// auto-repeat while held — mirrors how the base hammer fires
lmbTimer = setInterval(() => {
if (!lmbHeld) { clearInterval(lmbTimer); return }
firePri()
}, 50) // tight interval; actual bar duration gates the visual
}, true)
document.addEventListener('mouseup', e => {
if (e.button !== 0) return
lmbHeld = false
clearInterval(lmbTimer)
}, true)
// ── secondary (RMB) ────────────────────────────────────────────
let rmbHeld = false
let rmbTimer = null
function fireSec () {
arm('sec', getSecCd())
}
document.addEventListener('mousedown', e => {
if (e.button !== 2) return
rmbHeld = true
fireSec()
clearInterval(rmbTimer)
rmbTimer = setInterval(() => {
if (!rmbHeld) { clearInterval(rmbTimer); return }
fireSec()
}, 50)
}, true)
document.addEventListener('mouseup', e => {
if (e.button !== 2) return
rmbHeld = false
clearInterval(rmbTimer)
}, true)
}
// ╔══════════════════════════════════════════════════════════════════╗
// ║ CROSSHAIR CANVAS ║
// ╚══════════════════════════════════════════════════════════════════╝
let cxCv, cxCtx, cxX = 0, cxY = 0
let cursorCSS
function initCrosshair () {
if (document.getElementById('wat-cx')) return
cxCv = document.createElement('canvas')
cxCv.id = 'wat-cx'
cxCv.style.cssText = 'position:fixed;top:0;left:0;pointer-events:none;z-index:999998;'
cxCv.width = innerWidth
cxCv.height = innerHeight
document.body.appendChild(cxCv)
cxCtx = cxCv.getContext('2d')
addEventListener('resize', () => { cxCv.width = innerWidth; cxCv.height = innerHeight })
addEventListener('mousemove', e => { cxX = e.clientX; cxY = e.clientY })
cursorCSS = document.createElement('style')
cursorCSS.id = 'wat-cursor'
document.head?.appendChild(cursorCSS)
;(function frame () {
requestAnimationFrame(frame)
if (!cxCtx) return
cxCtx.clearRect(0, 0, cxCv.width, cxCv.height)
if (S.xhair === 'off') return
renderCrosshair(cxCtx, cxX, cxY, S.xhair, S.xhair_color, S.xhair_size)
})()
}
function syncCursor () {
if (!cursorCSS) return
cursorCSS.textContent = S.xhair !== 'off' ? 'canvas{cursor:none!important}' : ''
}
function renderCrosshair (ctx, x, y, style, col, sz) {
const gap = 4
ctx.save()
ctx.strokeStyle = col
ctx.fillStyle = col
ctx.lineWidth = 1.5
ctx.shadowColor = 'rgba(0,0,0,0.9)'
ctx.shadowBlur = 4
switch (style) {
case 'cross':
ctx.beginPath()
ctx.moveTo(x - sz - gap, y); ctx.lineTo(x - gap, y)
ctx.moveTo(x + gap, y); ctx.lineTo(x + sz + gap, y)
ctx.moveTo(x, y - sz - gap); ctx.lineTo(x, y - gap)
ctx.moveTo(x, y + gap); ctx.lineTo(x, y + sz + gap)
ctx.stroke()
ctx.beginPath(); ctx.arc(x, y, 1.5, 0, Math.PI*2); ctx.fill()
break
case 'dot':
ctx.beginPath(); ctx.arc(x, y, 3.5, 0, Math.PI*2); ctx.fill()
ctx.globalAlpha = 0.35
ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI*2); ctx.stroke()
break
case 'circle':
ctx.beginPath(); ctx.arc(x, y, sz, 0, Math.PI*2); ctx.stroke()
ctx.beginPath(); ctx.arc(x, y, 1.5, 0, Math.PI*2); ctx.fill()
break
case 'tcross': // T-shape (bottom only, like a sniper)
ctx.beginPath()
ctx.moveTo(x - sz - gap, y); ctx.lineTo(x - gap, y)
ctx.moveTo(x + gap, y); ctx.lineTo(x + sz + gap, y)
ctx.moveTo(x, y + gap); ctx.lineTo(x, y + sz + gap)
ctx.stroke()
ctx.beginPath(); ctx.arc(x, y, 1.5, 0, Math.PI*2); ctx.fill()
break
}
ctx.restore()
}
// ╔══════════════════════════════════════════════════════════════════╗
// ║ BAR RENDERING ║
// ╚══════════════════════════════════════════════════════════════════╝
function hexToRGBA (hex, a) {
const n = parseInt(hex.replace('#',''), 16)
return `rgba(${(n>>16)&255},${(n>>8)&255},${n&255},${a})`
}
function roundRect (ctx, x, y, w, h, r) {
r = Math.min(r, w/2, h/2)
ctx.beginPath()
ctx.moveTo(x+r, y)
ctx.arcTo(x+w, y, x+w, y+h, r)
ctx.arcTo(x+w, y+h, x, y+h, r)
ctx.arcTo(x, y+h, x, y, r)
ctx.arcTo(x, y, x+w, y, r)
ctx.closePath()
}
function drawReloadBar (ctx, x, y, W, H, progress, color, label) {
const R = 5
const PAD = 4
const fw = Math.max(R*2, W * progress)
// ── track ──
ctx.save()
roundRect(ctx, x-PAD, y-PAD, W+PAD*2, H+PAD*2, R+PAD)
ctx.fillStyle = 'rgba(4,6,14,0.82)'
ctx.fill()
ctx.strokeStyle = 'rgba(255,255,255,0.07)'
ctx.lineWidth = 1
ctx.stroke()
ctx.restore()
// ── quarter tick marks ──
ctx.save()
ctx.globalAlpha = 0.15
ctx.strokeStyle = '#fff'
ctx.lineWidth = 1
for (let i = 1; i < 4; i++) {
const tx = x + (W/4)*i
ctx.beginPath(); ctx.moveTo(tx, y+2); ctx.lineTo(tx, y+H-2); ctx.stroke()
}
ctx.restore()
// ── fill bar ──
ctx.save()
roundRect(ctx, x, y, fw, H, R)
const g = ctx.createLinearGradient(x, y, x+fw, y)
g.addColorStop(0, hexToRGBA(color, 0.5))
g.addColorStop(1, hexToRGBA(color, 1.0))
ctx.fillStyle = g
ctx.fill()
ctx.restore()
// ── glow ──
ctx.save()
ctx.globalAlpha = 0.38
ctx.shadowColor = color
ctx.shadowBlur = 20
roundRect(ctx, x, y, fw, H, R)
ctx.fillStyle = color
ctx.fill()
ctx.restore()
// ── label ──
ctx.save()
ctx.font = '700 9px "Courier New",monospace'
ctx.fillStyle = 'rgba(205,228,250,0.82)'
ctx.textAlign = 'center'
ctx.textBaseline = 'bottom'
ctx.shadowColor = 'rgba(0,0,0,1)'
ctx.shadowBlur = 6
ctx.fillText(label, x + W/2, y - 5)
ctx.restore()
}
function getBarLayout (slot) {
const scale = S.bar_scale / 100
const W = 200 * scale
const H = 11 * scale
const STACK = 32 * scale
switch (S.bar_pos) {
case 'topleft':
return { x: 16, y: 16 + slot*STACK, W, H }
case 'topright':
return { x: innerWidth - W - 16, y: 16 + slot*STACK, W, H }
default: { // bottom center
return { x: (innerWidth - W) / 2, y: innerHeight - 72 - slot*STACK, W, H }
}
}
}
// ╔══════════════════════════════════════════════════════════════════╗
// ║ FPS TRACKING ║
// ╚══════════════════════════════════════════════════════════════════╝
const fpsBuf = []
let fpsVal = 0
function tickFPS () {
const now = performance.now()
fpsBuf.push(now)
while (fpsBuf.length > 0 && fpsBuf[0] < now - 1000) fpsBuf.shift()
fpsVal = fpsBuf.length
}
// ╔══════════════════════════════════════════════════════════════════╗
// ║ MAIN RENDER LOOP ║
// ╚══════════════════════════════════════════════════════════════════╝
function renderLoop () {
requestAnimationFrame(renderLoop)
if (!oc) return
tickFPS()
oc.clearRect(0, 0, ov.width, ov.height)
// ── hud text (fps / ping) ──────────────────────────────────────
let hy = 14
// position HUD on the side that bars aren't on
const hx = S.bar_pos === 'topleft' ? ov.width - 14 : 14
const halign = S.bar_pos === 'topleft' ? 'right' : 'left'
if (S.fps) {
const c = fpsVal < 30 ? '#ff4040' : fpsVal < 55 ? '#ffaa22' : '#22ee88'
hudText(`${fpsVal} fps`, hx, hy, c, halign)
hy += 19
}
if (S.ping) {
const c = ping > 150 ? '#ff4848' : ping > 80 ? '#ffaa22' : '#22ccff'
// show both raw + smoothed so you can see if it's stable
const label = pingRaw === ping ? `${ping} ms` : `${ping} ms`
hudText(label, hx, hy, c, halign)
}
// ── reload bars ────────────────────────────────────────────────
const now = performance.now()
let nextSlot = 0
for (const [key, b] of [['pri', bars.pri], ['sec', bars.sec]]) {
const enabled = key === 'pri' ? S.pri_bar : S.sec_bar
if (!enabled) continue
if (!b.active) continue
const elapsed = now - b.startT
const progress = Math.min(elapsed / b.dur, 1)
if (progress >= 1) { b.active = false; continue }
const color = key === 'pri' ? S.pri_color : S.sec_color
const lname = key === 'pri' ? 'primary' : 'secondary'
const pct = Math.round(progress * 100)
const { x, y, W, H } = getBarLayout(nextSlot)
drawReloadBar(oc, x, y, W, H, progress, color, `${lname} ${pct}%`)
nextSlot++
}
}
function hudText (txt, x, y, color, align) {
oc.save()
oc.font = '700 11px "Courier New",monospace'
oc.fillStyle = color
oc.textAlign = align
oc.textBaseline = 'top'
oc.shadowColor = 'rgba(0,0,0,0.97)'
oc.shadowBlur = 5
oc.fillText(txt, x, y)
oc.restore()
}
// ╔══════════════════════════════════════════════════════════════════╗
// ║ CHAT STYLE ║
// ╚══════════════════════════════════════════════════════════════════╝
let chatStyleEl
function syncChat () {
if (!chatStyleEl) {
chatStyleEl = document.createElement('style')
chatStyleEl.id = 'wat-chat'
document.head?.appendChild(chatStyleEl)
}
if (S.chat_hide) {
chatStyleEl.textContent = '#chatBox,.chatBox{display:none!important}'
return
}
const op = S.chat_opacity / 100
chatStyleEl.textContent = `
#chatBox,.chatBox{opacity:${op}!important;transition:opacity .18s ease}
#chatBox:hover,.chatBox:hover{opacity:1!important}
`
}
// ╔══════════════════════════════════════════════════════════════════╗
// ║ MENU ║
// ╚══════════════════════════════════════════════════════════════════╝
function buildMenu () {
if (document.getElementById('wat-wrap')) return
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap'
document.head?.appendChild(link)
const css = document.createElement('style')
css.id = 'wat-css'
css.textContent = `
#wat-wrap {
position: fixed;
top: 52px;
right: 14px;
z-index: 1000000;
font-family: 'JetBrains Mono','Courier New',monospace;
}
#wat-menu {
width: 232px;
background: #070a14;
border: 1px solid rgba(0,185,255,0.18);
border-radius: 10px;
overflow: hidden;
box-shadow:
0 28px 70px rgba(0,0,0,0.82),
0 0 0 1px rgba(0,0,0,0.6),
inset 0 1px 0 rgba(255,255,255,0.04);
transform: scaleY(0) translateY(-8px);
transform-origin: top right;
opacity: 0;
pointer-events: none;
transition:
transform 0.2s cubic-bezier(0.22,1,0.36,1),
opacity 0.18s ease;
}
#wat-menu.open {
transform: scaleY(1) translateY(0);
opacity: 1;
pointer-events: all;
}
/* header / drag handle */
#wat-hdr {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 13px 9px;
background: rgba(0,165,255,0.055);
border-bottom: 1px solid rgba(0,185,255,0.1);
cursor: move;
}
#wat-hdr .t {
font-size: 12.5px;
font-weight: 700;
color: #00c6ff;
letter-spacing: 0.2px;
}
#wat-hdr .v {
font-size: 9px;
color: rgba(0,175,230,0.32);
}
/* section heading */
.ws {
padding: 8px 13px 3px;
font-size: 8.5px;
font-weight: 700;
letter-spacing: 1.8px;
color: rgba(0,185,255,0.35);
text-transform: uppercase;
}
/* row */
.wr {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 13px;
gap: 8px;
transition: background .1s;
}
.wr:hover { background: rgba(0,185,255,0.04); }
.wl {
flex: 1;
font-size: 11px;
color: #bacde0;
white-space: nowrap;
}
.wl small {
display: block;
font-size: 9px;
color: rgba(130,165,195,0.42);
margin-top: 1px;
}
/* toggle switch */
.wtog { position: relative; width: 30px; height: 16px; flex-shrink: 0; }
.wtog input { opacity: 0; width: 0; height: 0; }
.wtrk {
position: absolute; inset: 0;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.09);
border-radius: 16px;
cursor: pointer;
transition: background .17s, border-color .17s;
}
.wtrk::after {
content: '';
position: absolute;
left: 2px; top: 50%;
transform: translateY(-50%);
width: 10px; height: 10px;
border-radius: 50%;
background: rgba(255,255,255,0.28);
transition: left .18s cubic-bezier(.22,1,.36,1), background .17s;
}
.wtog input:checked + .wtrk {
background: rgba(0,190,255,0.14);
border-color: rgba(0,190,255,0.42);
}
.wtog input:checked + .wtrk::after {
left: 17px;
background: #00c6ff;
box-shadow: 0 0 7px rgba(0,190,255,0.6);
}
/* color picker */
.wcol {
width: 22px; height: 22px;
border-radius: 4px;
border: 1px solid rgba(255,255,255,0.11);
cursor: pointer; background: none; padding: 0; flex-shrink: 0;
}
.wcol::-webkit-color-swatch-wrapper { padding: 0; }
.wcol::-webkit-color-swatch { border: none; border-radius: 3px; }
/* range */
.wrng {
-webkit-appearance: none;
width: 86px; height: 3px;
background: rgba(255,255,255,0.09);
border-radius: 3px; outline: none; flex-shrink: 0;
}
.wrng::-webkit-slider-thumb {
-webkit-appearance: none;
width: 11px; height: 11px;
border-radius: 50%;
background: #00c6ff;
cursor: pointer;
box-shadow: 0 0 6px rgba(0,190,255,0.55);
}
/* pill buttons */
.wpills { display: flex; gap: 3px; flex-wrap: wrap; justify-content: flex-end; }
.wpill {
background: rgba(255,255,255,0.045);
border: 1px solid rgba(255,255,255,0.09);
color: rgba(155,195,225,0.5);
border-radius: 4px;
padding: 2px 6px;
font-family: inherit; font-size: 9px;
cursor: pointer;
transition: all .12s;
}
.wpill:hover { border-color: rgba(0,190,255,0.32); color: #80c5e2; }
.wpill.on {
background: rgba(0,190,255,0.12);
border-color: rgba(0,190,255,0.48);
color: #00c6ff;
}
/* val badge */
.wval { font-size: 9px; color: rgba(0,185,255,0.48); min-width: 26px; text-align: right; }
/* divider */
.wdiv { border: none; border-top: 1px solid rgba(255,255,255,0.045); margin: 4px 13px; }
/* bottom hint row */
.wfoot {
padding: 6px 13px 10px;
font-size: 9px;
color: rgba(100,145,175,0.38);
letter-spacing: 0.3px;
}
.wfoot kbd {
background: rgba(255,255,255,0.06);
padding: 1px 5px;
border-radius: 3px;
color: rgba(0,185,255,0.55);
}
/* watermark */
#wat-wm {
position: fixed;
bottom: 7px; right: 10px;
font-family: 'JetBrains Mono','Courier New',monospace;
font-size: 9px;
color: rgba(0,185,255,0.18);
pointer-events: none;
z-index: 999994;
user-select: none;
letter-spacing: 1px;
}
`
document.head?.appendChild(css)
// ── DOM ────────────────────────────────────────────────────────
const wrap = document.createElement('div')
wrap.id = 'wat-wrap'
wrap.innerHTML = `
<div id="wat-menu">
<div id="wat-hdr">
<span class="t">wat's mod</span>
<span class="v">v6.0</span>
</div>
<div class="ws">reload bars</div>
<div class="wr">
<span class="wl">primary bar <small>left click</small></span>
<label class="wtog"><input type="checkbox" id="wp-pri" ${S.pri_bar?'checked':''}><span class="wtrk"></span></label>
</div>
<div class="wr">
<span class="wl">primary color</span>
<input class="wcol" type="color" id="wp-pricol" value="${S.pri_color}">
</div>
<div class="wr">
<span class="wl">secondary bar <small>right click</small></span>
<label class="wtog"><input type="checkbox" id="wp-sec" ${S.sec_bar?'checked':''}><span class="wtrk"></span></label>
</div>
<div class="wr">
<span class="wl">secondary color</span>
<input class="wcol" type="color" id="wp-seccol" value="${S.sec_color}">
</div>
<div class="wr">
<span class="wl">position</span>
<div class="wpills" id="wp-pos">
<button class="wpill" data-v="topleft">TL</button>
<button class="wpill" data-v="bottom">BTM</button>
<button class="wpill" data-v="topright">TR</button>
</div>
</div>
<div class="wr">
<div><span class="wl">scale</span></div>
<span class="wval" id="wp-scalev">${S.bar_scale}%</span>
<input class="wrng" type="range" id="wp-scale" min="60" max="150" value="${S.bar_scale}">
</div>
<hr class="wdiv">
<div class="ws">hud</div>
<div class="wr">
<span class="wl">fps counter</span>
<label class="wtog"><input type="checkbox" id="wp-fps" ${S.fps?'checked':''}><span class="wtrk"></span></label>
</div>
<div class="wr">
<span class="wl">ping / ms</span>
<label class="wtog"><input type="checkbox" id="wp-ping" ${S.ping?'checked':''}><span class="wtrk"></span></label>
</div>
<hr class="wdiv">
<div class="ws">crosshair</div>
<div class="wr">
<span class="wl">style</span>
<div class="wpills" id="wp-xhair">
<button class="wpill" data-v="off">off</button>
<button class="wpill" data-v="cross">+</button>
<button class="wpill" data-v="dot">·</button>
<button class="wpill" data-v="circle">○</button>
<button class="wpill" data-v="tcross">T</button>
</div>
</div>
<div class="wr">
<span class="wl">color</span>
<input class="wcol" type="color" id="wp-xcol" value="${S.xhair_color}">
</div>
<div class="wr">
<div><span class="wl">size</span></div>
<span class="wval" id="wp-xsizev">${S.xhair_size}px</span>
<input class="wrng" type="range" id="wp-xsize" min="4" max="26" value="${S.xhair_size}">
</div>
<hr class="wdiv">
<div class="ws">chat</div>
<div class="wr">
<span class="wl">hide chat</span>
<label class="wtog"><input type="checkbox" id="wp-chide" ${S.chat_hide?'checked':''}><span class="wtrk"></span></label>
</div>
<div class="wr">
<div><span class="wl">opacity</span></div>
<span class="wval" id="wp-copv">${S.chat_opacity}%</span>
<input class="wrng" type="range" id="wp-cop" min="10" max="100" value="${S.chat_opacity}">
</div>
<hr class="wdiv">
<div class="wfoot">
<kbd>\`</kbd> open menu · drag header to move
</div>
</div>
`
document.body.appendChild(wrap)
const menu = document.getElementById('wat-menu')
// watermark
const wm = document.createElement('div')
wm.id = 'wat-wm'
wm.textContent = "wat's mod"
document.body.appendChild(wm)
// ── toggle open/close ──────────────────────────────────────────
let open = false
document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT' && e.target.type !== 'checkbox') return
if (e.key === '`' || e.key === '~') {
open = !open
menu.classList.toggle('open', open)
}
})
// ── dragging ──────────────────────────────────────────────────
const hdr = document.getElementById('wat-hdr')
let drag = false, dox = 0, doy = 0
hdr.addEventListener('mousedown', e => {
drag = true
const r = wrap.getBoundingClientRect()
dox = e.clientX - r.left
doy = e.clientY - r.top
e.stopPropagation()
})
document.addEventListener('mousemove', e => {
if (!drag) return
wrap.style.right = 'auto'
wrap.style.left = `${e.clientX - dox}px`
wrap.style.top = `${e.clientY - doy}px`
})
document.addEventListener('mouseup', () => { drag = false })
// ── control binding helpers ────────────────────────────────────
const $ = id => document.getElementById(id)
function onToggle (id, key, cb) {
$(id).addEventListener('change', e => { S[key] = e.target.checked; save(); cb?.() })
}
function onColor (id, key, cb) {
$(id).addEventListener('input', e => { S[key] = e.target.value; save(); cb?.() })
}
function onRange (id, key, valId, suffix, cb) {
$(id).addEventListener('input', e => {
S[key] = +e.target.value
if (valId) $(valId).textContent = S[key] + suffix
save(); cb?.()
})
}
function pillGroup (groupId, key, cb) {
const g = $(groupId)
const sync = () => g.querySelectorAll('.wpill').forEach(b =>
b.classList.toggle('on', b.dataset.v === S[key]))
sync()
g.addEventListener('click', e => {
const b = e.target.closest('.wpill')
if (!b) return
S[key] = b.dataset.v
sync(); save(); cb?.()
})
}
// ── wire up ───────────────────────────────────────────────────
onToggle('wp-pri', 'pri_bar')
onColor ('wp-pricol', 'pri_color')
onToggle('wp-sec', 'sec_bar')
onColor ('wp-seccol', 'sec_color')
pillGroup('wp-pos', 'bar_pos')
onRange ('wp-scale', 'bar_scale', 'wp-scalev', '%')
onToggle('wp-fps', 'fps')
onToggle('wp-ping', 'ping')
pillGroup('wp-xhair', 'xhair', syncCursor)
onColor ('wp-xcol', 'xhair_color')
onRange ('wp-xsize', 'xhair_size', 'wp-xsizev', 'px')
onToggle('wp-chide', 'chat_hide', syncChat)
onRange ('wp-cop', 'chat_opacity', 'wp-copv', '%', syncChat)
}
// ╔══════════════════════════════════════════════════════════════════╗
// ║ BOOT ║
// ╚══════════════════════════════════════════════════════════════════╝
function boot () {
initOverlay()
initInputHooks()
initCrosshair()
buildMenu()
syncChat()
syncCursor()
renderLoop()
}
if (document.body) {
boot()
} else {
document.addEventListener('DOMContentLoaded', boot)
}
})()