Customization mod

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)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 &nbsp;·&nbsp; 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)
}

})()