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)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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)
}

})()