Greasy Fork is available in English.

⚔️EraBOT🕺

SCRIPT EVOWARS.IO ERABOTVN VIP - ULTIMATE STABLE (ALL FIXED)

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

Advertisement:

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

Advertisement:

// ==UserScript==
// @name         ⚔️EraBOT🕺
// @namespace    http://tampermonkey.net/
// @version      18.0.0
// @description  SCRIPT EVOWARS.IO ERABOTVN VIP - ULTIMATE STABLE (ALL FIXED)
// @author       #NLHH
// @match        https://evowars.io/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=evowars.io
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // =============================================================
    // 1. CONFIG (giữ nguyên)
    // =============================================================
    const C = {
        eraBot: false,
        autoAttack: true,
        filterLow: true,
        hitbox: true,
        zoom: 0.9,
        autoRejoin: true,
        autoDuel: true,
        collectFood: true,
        fleeDanger: true,
        sprintMode: 1,
        lvlMin: 0,
        lvlMax: 100,
        maxHigher: 5,
        minLower: 0,
        safeDistance: 1.5,
        showEnemyRange: true,
        showCooldown: true,
        showTracers: true,
        showTargetGlow: true,
        aggression: 0.7,
        dodgeReaction: 0.6,
        baitChance: 0.3,
    };

    // =============================================================
    // 2. CÁC HÀM CƠ BẢN (giữ nguyên)
    // =============================================================
    function toast(msg, type = 'success', dur = 2500) {
        const old = document.querySelector('.erabot-toast');
        if (old) old.remove();
        if (window._toastTimer) clearTimeout(window._toastTimer);
        const el = document.createElement('div');
        el.className = `erabot-toast ${type}`;
        el.textContent = msg;
        document.body.appendChild(el);
        setTimeout(() => el.classList.add('show'), 50);
        window._toastTimer = setTimeout(() => {
            el.classList.remove('show');
            setTimeout(() => el.remove(), 400);
        }, dur);
    }

    function isOrb(obj) {
        try {
            // Phát hiện orb bằng nhiều tiêu chí
            if (!obj || typeof obj.width !== 'number' || typeof obj.height !== 'number') return false;
            if (obj.width > 30 || obj.height > 30) return false;
            if (!obj.collision_poly) return false;
            // Thêm kiểm tra màu sắc nếu có
            if (obj.colorRgb) {
                const c = obj.colorRgb;
                if (Array.isArray(c) && c.length >= 3) {
                    // Màu orb thường: trắng, vàng, xanh, tím, hồng
                    const isOrbColor = (c[0] > 200 && c[1] > 200 && c[2] > 200) || // trắng
                                       (c[0] > 200 && c[1] > 150 && c[2] < 150) || // vàng
                                       (c[0] < 150 && c[1] > 200 && c[2] < 150) || // xanh
                                       (c[0] > 200 && c[1] < 150 && c[2] > 200) || // tím
                                       (c[0] > 200 && c[1] < 150 && c[2] < 150);   // hồng
                    if (!isOrbColor) return false;
                }
            }
            return true;
        } catch(e) { return false; }
    }

    function isLow(lv) { return lv >= 0 && lv <= 4; }

    function getPts(o) {
        try {
            if (!o) return [];
            if (o.bbox_changed && o.update_bbox) o.update_bbox();
            if (o.collision_poly && typeof o.collision_poly.is_empty === 'function' && !o.collision_poly.is_empty()) {
                o.collision_poly.cache_poly(o.width, o.height, o.angle);
                const pts = [], c = o.collision_poly.pts_cache;
                for (let i = 0; i < o.collision_poly.pts_count; i++) {
                    pts.push({ x: c[i*2] + o.x, y: c[i*2+1] + o.y });
                }
                return pts;
            }
            if (o.bquad) {
                const b = o.bquad;
                return [
                    { x: b.tlx, y: b.tly },
                    { x: b.trx, y: b.try_ },
                    { x: b.brx, y: b.bry },
                    { x: b.blx, y: b.bly }
                ];
            }
        } catch(e) {}
        return [];
    }

    // =============================================================
    // 3. UTILITY (cải tiến)
    // =============================================================
    const gl = o => o?.instance_vars?.[10] || 0;
    const gt = o => o?.instance_vars?.[11] || -1;
    const gh = o => o?.instance_vars?.[0] || 1000;
    const dist = (a,b) => Math.hypot(a.x-b.x, a.y-b.y);
    const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
    const angleDiff = (a,b) => { let d = b - a; while(d > Math.PI) d -= 2*Math.PI; while(d < -Math.PI) d += 2*Math.PI; return d; };
    const rand = (min, max) => Math.random() * (max - min) + min;
    const hash = (s) => { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) & 0xFFFFFFFF; return h; };

    function getReach(lv) {
        const STATIC = {
            0:{d:200,deg:125},2:{d:235,deg:90},3:{d:245,deg:125},4:{d:260,deg:125},
            5:{d:300,deg:133},6:{d:340,deg:125},7:{d:380,deg:131},8:{d:343,deg:130},
            9:{d:350,deg:125},10:{d:470,deg:133},11:{d:510,deg:129},12:{d:520,deg:133},
            13:{d:555,deg:134},14:{d:595,deg:125},15:{d:650,deg:129},16:{d:655,deg:131},
            17:{d:660,deg:125},18:{d:695,deg:125},19:{d:690,deg:125},20:{d:710,deg:130},
            21:{d:775,deg:130},22:{d:805,deg:136},23:{d:680,deg:122},24:{d:870,deg:125},
            25:{d:940,deg:137},26:{d:975,deg:130},27:{d:1050,deg:125},28:{d:1095,deg:125},
            29:{d:1000,deg:135},30:{d:995,deg:125},31:{d:1050,deg:130},32:{d:1145,deg:134},
            33:{d:1120,deg:139},34:{d:1125,deg:124},35:{d:1145,deg:135},36:{d:1250,deg:122},
            37:{d:1300,deg:125},38:{d:1300,deg:125},39:{d:1300,deg:125}
        };
        return STATIC[lv] || { d: Math.max(200, lv*30), deg: 125 };
    }
    function getMul(lv) {
        const m = {1:.6,2:.75,3:.7,4:.75,5:.8,6:.75,7:.76,8:.75,9:.9,10:.95,11:.8,12:.75,
            13:.75,14:.8,15:.8,16:.7,17:.75,18:.8,19:.8,20:.85,21:.85,22:.81,23:.82,24:1.05,
            25:.85,26:.8,27:.85,28:.78,29:.7,30:.8,31:.85,32:.85,33:.8,34:.8,35:.83,36:.9,
            37:.85,38:.9,39:.95,40:.91,41:1.06};
        return m[Math.floor(lv)+1] || .8;
    }
    function getWeaponTimings(lv) {
        const cooldown = Math.max(110, 110 + lv * 8);
        const windup = Math.max(120, 150 + lv * 2);
        return { windup, cooldown };
    }

    // =============================================================
    // 4. KALMAN FILTER + WEIGHTED PING
    // =============================================================
    class Kalman {
        constructor() {
            this.x = 0; this.y = 0;
            this.vx = 0; this.vy = 0;
            this.ax = 0; this.ay = 0;
            this.has = false;
            this.lastTime = 0;
            this.history = [];
            this.maxHistory = 20;
        }
        update(x, y, vx, vy, now) {
            if (!this.has) {
                this.x = x; this.y = y;
                this.vx = vx; this.vy = vy;
                this.has = true;
                this.lastTime = now;
                return;
            }
            const dt = Math.max(0.001, (now - this.lastTime) / 1000);
            // Lọc Kalman đơn giản: trung bình động có trọng số
            const alpha = 0.3;
            this.vx = this.vx * (1 - alpha) + vx * alpha;
            this.vy = this.vy * (1 - alpha) + vy * alpha;
            // Ước lượng gia tốc từ lịch sử
            if (this.history.length > 1) {
                const prev = this.history[this.history.length-1];
                const dt2 = Math.max(0.001, (now - prev.t) / 1000);
                const ax = (this.vx - prev.vx) / dt2;
                const ay = (this.vy - prev.vy) / dt2;
                this.ax = this.ax * 0.7 + ax * 0.3;
                this.ay = this.ay * 0.7 + ay * 0.3;
            }
            this.x = x; this.y = y;
            this.lastTime = now;
            this.history.push({ x, y, vx: this.vx, vy: this.vy, t: now });
            if (this.history.length > this.maxHistory) this.history.shift();
        }
        predict(dt) {
            if (!this.has) return null;
            const dtSec = Math.max(0.001, dt / 1000);
            return {
                x: this.x + this.vx * dtSec + 0.5 * this.ax * dtSec * dtSec,
                y: this.y + this.vy * dtSec + 0.5 * this.ay * dtSec * dtSec,
                vx: this.vx + this.ax * dtSec,
                vy: this.vy + this.ay * dtSec,
            };
        }
    }

    // =============================================================
    // 5. AI HỌC HÀNH VI (nâng cao: fake swing, bait, combo)
    // =============================================================
    class EnemyAI {
        constructor(uid) {
            this.uid = uid;
            this.h = [];
            this.maxH = 100;
            this.confidence = 0.3;
            this.lastAttack = 0;
            this.attackIntervals = [];
            this.swingAngles = [];
            this.baitFreq = 0;
            this.dodgePref = 0;
            this.lastUpdate = 0;
            this.lastSeen = 0;
            this.kalman = new Kalman();
            this.predCache = null;
            this.predTime = 0;
            this.prefDistance = 0;
            this.strafing = false;
            this.aggressiveness = 0.5;
            this.patternDetected = false;
            this.patternTime = 0;
            this.fakeSwingCount = 0;     // Đếm số lần fake swing
            this.baitCount = 0;           // Đếm số lần bait thành công
            this.comboCount = 0;          // Đếm số lần combo
            this.lastFakeSwing = 0;
            this.lastBait = 0;
        }
        update(pos, vx, vy, now) {
            const angle = Math.atan2(vy, vx);
            const speed = Math.hypot(vx, vy);
            this.h.push({ x: pos.x, y: pos.y, t: now, vx, vy, angle, speed });
            if (this.h.length > this.maxH) this.h.shift();
            this.lastSeen = now;
            this.kalman.update(pos.x, pos.y, vx, vy, now);
            if (now - this.lastUpdate > 50) {
                this._analyze(now);
                this.lastUpdate = now;
            }
            this.predCache = null;
        }
        _analyze(now) {
            if (this.h.length < 8) return;
            const recent = this.h.slice(-12);
            let angleChanges = 0, totalAngle = 0;
            let totalDist = 0, distCount = 0;
            let strafeCount = 0;
            let fakeSwingDetected = 0;
            let baitDetected = 0;

            for (let i = 1; i < recent.length; i++) {
                const da = angleDiff(recent[i-1].angle, recent[i].angle);
                if (Math.abs(da) > 0.3) angleChanges++;
                totalAngle += da;
                if (Math.abs(da) > 1) {
                    this.swingAngles.push(da);
                    // Phát hiện fake swing: góc thay đổi lớn nhưng không có tấn công
                    if (recent[i].speed < 2 && this.lastAttack > 0 && (now - this.lastAttack) > 500) {
                        fakeSwingDetected++;
                    }
                }
                const d = Math.hypot(recent[i].x - recent[i-1].x, recent[i].y - recent[i-1].y);
                totalDist += d;
                distCount++;
                if (Math.abs(da) > 0.2 && Math.abs(da) < 0.8 && recent[i].speed < 3) {
                    strafeCount++;
                    // Phát hiện bait: di chuyển vào rồi lùi nhanh
                    if (i > 1 && i < recent.length-1) {
                        const dPrev = Math.hypot(recent[i-1].x - recent[i-2].x, recent[i-1].y - recent[i-2].y);
                        const dNext = Math.hypot(recent[i+1].x - recent[i].x, recent[i+1].y - recent[i].y);
                        if (dPrev > 30 && dNext > 30 && Math.abs(dPrev - dNext) < 10) {
                            baitDetected++;
                        }
                    }
                }
            }

            if (this.swingAngles.length > 20) this.swingAngles.shift();
            this.baitFreq = Math.min(1, angleChanges / (recent.length - 1) * 1.5);
            this.dodgePref = clamp(totalAngle / (recent.length - 1) * 1.5, -1, 1);
            this.prefDistance = totalDist / distCount;
            this.strafing = strafeCount > 3;

            // Cập nhật fake swing và bait
            this.fakeSwingCount = Math.min(10, this.fakeSwingCount + fakeSwingDetected * 0.5);
            this.baitCount = Math.min(10, this.baitCount + baitDetected * 0.3);
            if (fakeSwingDetected > 1) this.lastFakeSwing = now;
            if (baitDetected > 1) this.lastBait = now;

            const avgSpeed = recent.reduce((s, p) => s + p.speed, 0) / recent.length;
            this.aggressiveness = clamp(avgSpeed / 10, 0, 1);

            if (this.h.length > 20) this.confidence = Math.min(1, this.confidence + 0.03);
            if (this.attackIntervals.length > 3) {
                const avg = this.attackIntervals.reduce((a,b) => a+b, 0) / this.attackIntervals.length;
                const std = Math.sqrt(this.attackIntervals.reduce((a,b) => a + (b-avg)*(b-avg), 0) / this.attackIntervals.length);
                if (std / avg < 0.25) {
                    this.confidence = Math.min(1, this.confidence + 0.05);
                    this.patternDetected = true;
                    this.patternTime = now;
                }
            }

            // Phát hiện combo: nhiều đòn liên tiếp cách nhau < 400ms
            if (this.attackIntervals.length > 2) {
                const last3 = this.attackIntervals.slice(-3);
                const avgCombo = last3.reduce((a,b) => a+b, 0) / last3.length;
                if (avgCombo < 400 && last3.every(x => x < 500)) {
                    this.comboCount = Math.min(5, this.comboCount + 1);
                } else {
                    this.comboCount = Math.max(0, this.comboCount - 0.5);
                }
            }
        }
        recordAttack(now) {
            if (this.lastAttack > 0) {
                const interval = now - this.lastAttack;
                if (interval > 100 && interval < 5000) {
                    this.attackIntervals.push(interval);
                    if (this.attackIntervals.length > 10) this.attackIntervals.shift();
                }
            }
            this.lastAttack = now;
            this.predCache = null;
        }
        predictNextAttack(now) {
            if (this.attackIntervals.length < 2 || this.confidence < 0.45) return null;
            const avg = this.attackIntervals.reduce((a,b) => a+b, 0) / this.attackIntervals.length;
            // Nếu đang combo, dự đoán nhanh hơn
            if (this.comboCount > 2) {
                const comboAvg = this.attackIntervals.slice(-3).reduce((a,b) => a+b, 0) / Math.min(3, this.attackIntervals.length);
                if (this.lastAttack > 0 && now - this.lastAttack > comboAvg * 0.5) {
                    return this.lastAttack + comboAvg;
                }
            }
            if (this.lastAttack > 0 && now - this.lastAttack > avg * 0.5) {
                return this.lastAttack + avg;
            }
            return null;
        }
        predictPos(dt) {
            if (this.predCache && (performance.now() - this.predTime) < 40) return this.predCache;
            const pred = this.kalman.predict(dt);
            if (pred) {
                this.predCache = pred;
                this.predTime = performance.now();
                return pred;
            }
            return null;
        }
        getSwingAngle() {
            if (this.swingAngles.length < 2) return 0;
            return this.swingAngles.reduce((a,b) => a+b, 0) / this.swingAngles.length;
        }
        predictDodgeDirection() {
            if (this.h.length < 5) return 0;
            const recent = this.h.slice(-5);
            let totalDodge = 0;
            for (let i = 1; i < recent.length; i++) {
                const dx = recent[i].x - recent[i-1].x;
                const dy = recent[i].y - recent[i-1].y;
                const angle = recent[i].angle;
                const moveAngle = Math.atan2(dy, dx);
                const diff = angleDiff(angle, moveAngle);
                if (Math.abs(diff) > 0.5) {
                    totalDodge += Math.sign(diff);
                }
            }
            return clamp(totalDodge / recent.length * 2, -1, 1);
        }
    }

    // =============================================================
    // 6. WEBSOCKET HOOK (robust: Proxy + binary + fallback)
    // =============================================================
    let wsHooked = false;
    let pingHistory = [];
    let pingEstimate = 50;
    let pingWeights = [];
    let wsSendTimes = new Map();

    // Weighted moving average ping
    function updatePing(ping) {
        pingHistory.push(ping);
        if (pingHistory.length > 20) pingHistory.shift();
        // Weighted: gần đây hơn có trọng số cao hơn
        const weights = pingHistory.map((_, i) => i + 1);
        const totalWeight = weights.reduce((a,b) => a+b, 0);
        pingEstimate = pingHistory.reduce((sum, p, i) => sum + p * weights[i], 0) / totalWeight;
    }

    function hookWebSocket() {
        if (wsHooked) return;
        try {
            const OriginalWebSocket = window.WebSocket;
            window.WebSocket = new Proxy(OriginalWebSocket, {
                construct(target, args) {
                    const socket = new target(...args);
                    // Hook send
                    const origSend = socket.send;
                    socket.send = function(data) {
                        try {
                            let parsed = null;
                            if (typeof data === 'string') {
                                try { parsed = JSON.parse(data); } catch(e) {}
                            } else if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
                                // Xử lý binary packet: thử decode text
                                try {
                                    const text = new TextDecoder().decode(data);
                                    parsed = JSON.parse(text);
                                } catch(e) {}
                            }
                            if (parsed) {
                                let uid = null;
                                if (parsed?.action === 'send' && parsed?.data?.a === 'ps') {
                                    uid = parsed.data.d?.id || parsed.data.d?.uid;
                                } else if (parsed?.a === 'ps' && parsed?.d) {
                                    uid = parsed.d.id || parsed.d.uid;
                                } else if (parsed?.type === 'attack' && parsed?.id) {
                                    uid = parsed.id;
                                } else if (parsed?.ps && parsed.ps.id) {
                                    uid = parsed.ps.id;
                                }
                                if (uid) {
                                    const now = performance.now();
                                    if (enemyMap.has(uid)) {
                                        const e = enemyMap.get(uid);
                                        e.attackStart = now;
                                        let ai = enemyAIs.get(uid);
                                        if (!ai) { ai = new EnemyAI(uid); enemyAIs.set(uid, ai); }
                                        ai.recordAttack(now);
                                    }
                                    wsSendTimes.set(uid, now);
                                }
                            }
                        } catch(e) {}
                        return origSend.call(this, data);
                    };
                    // Hook onmessage bằng addEventListener (bắt được cả listener khác)
                    socket.addEventListener('message', function(ev) {
                        try {
                            let parsed = null;
                            if (typeof ev.data === 'string') {
                                try { parsed = JSON.parse(ev.data); } catch(e) {}
                            } else if (ev.data instanceof ArrayBuffer || ev.data instanceof Uint8Array) {
                                try {
                                    const text = new TextDecoder().decode(ev.data);
                                    parsed = JSON.parse(text);
                                } catch(e) {}
                            }
                            if (parsed) {
                                if (parsed?.action === 'ps' || parsed?.a === 'ps') {
                                    const now = performance.now();
                                    const uid = parsed.d?.id || parsed.d?.uid;
                                    if (uid && wsSendTimes.has(uid)) {
                                        const sendTime = wsSendTimes.get(uid);
                                        const ping = now - sendTime;
                                        if (ping > 0 && ping < 500) {
                                            updatePing(ping);
                                            wsSendTimes.delete(uid);
                                        }
                                    }
                                }
                            }
                        } catch(e) {}
                    });
                    return socket;
                }
            });
            wsHooked = true;
            console.log('[EraBOT] WebSocket hooked (Proxy + binary support)');
        } catch(e) {
            console.warn('[EraBOT] WebSocket hook failed:', e);
        }
    }

    // Fallback: phát hiện attack từ DOM changes (nếu WebSocket không hoạt động)
    let domObserver = null;

    function startDOMFallback() {
        if (domObserver) return;
        domObserver = new MutationObserver(() => {
            // Kiểm tra nếu có enemy đang tấn công dựa trên thay đổi góc
            // Đây là fallback, không chính xác tuyệt đối
            try {
                if (!pt || !enemyMap) return;
                const now = performance.now();
                for (const [uid, data] of enemyMap) {
                    if (data.angleChanged && (now - data._lastAngleChange) < 100) {
                        // Nếu góc thay đổi đột ngột, coi như đang tấn công
                        if (!data.attackStart || (now - data.attackStart) > 300) {
                            data.attackStart = now;
                            let ai = enemyAIs.get(uid);
                            if (!ai) { ai = new EnemyAI(uid); enemyAIs.set(uid, ai); }
                            ai.recordAttack(now);
                        }
                    }
                }
            } catch(e) {}
        });
        domObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
    }

    // =============================================================
    // 7. SPRINT (Keyboard + Pointer + Canvas events)
    // =============================================================
    let shiftSimulated = false;

    function setSprint(active) {
        if (active && !shiftSimulated) {
            shiftSimulated = true;
            // Phương pháp 1: KeyboardEvent
            try {
                const ev = new KeyboardEvent('keydown', { key: 'Shift', code: 'ShiftLeft', keyCode: 16, which: 16, bubbles: true });
                Object.defineProperty(ev, 'isTrusted', { value: true });
                document.dispatchEvent(ev);
                if (cv) cv.dispatchEvent(ev);
            } catch(e) {}
            // Phương pháp 2: PointerEvent (nếu game bắt pointer)
            try {
                const ev2 = new PointerEvent('pointerdown', { button: 2, buttons: 2, pointerId: 1, pointerType: 'mouse', bubbles: true });
                Object.defineProperty(ev2, 'isTrusted', { value: true });
                document.dispatchEvent(ev2);
                if (cv) cv.dispatchEvent(ev2);
            } catch(e) {}
            // Phương pháp 3: MouseEvent
            try {
                const ev3 = new MouseEvent('mousedown', { button: 2, buttons: 2, bubbles: true });
                Object.defineProperty(ev3, 'isTrusted', { value: true });
                document.dispatchEvent(ev3);
                if (cv) cv.dispatchEvent(ev3);
            } catch(e) {}
            // Phương pháp 4: gửi sự kiện trực tiếp tới canvas
            if (cv) {
                try {
                    const rect = cv.getBoundingClientRect();
                    const ev4 = new MouseEvent('mousedown', {
                        clientX: rect.left + rect.width/2,
                        clientY: rect.top + rect.height/2,
                        button: 2, buttons: 2, bubbles: true
                    });
                    Object.defineProperty(ev4, 'isTrusted', { value: true });
                    cv.dispatchEvent(ev4);
                } catch(e) {}
            }
            // Lặp lại để đảm bảo
            setTimeout(() => {
                try {
                    const ev5 = new KeyboardEvent('keydown', { key: 'Shift', code: 'ShiftLeft', keyCode: 16, which: 16, bubbles: true });
                    Object.defineProperty(ev5, 'isTrusted', { value: true });
                    document.dispatchEvent(ev5);
                } catch(e) {}
            }, 50);
        } else if (!active && shiftSimulated) {
            shiftSimulated = false;
            try {
                const ev = new KeyboardEvent('keyup', { key: 'Shift', code: 'ShiftLeft', keyCode: 16, which: 16, bubbles: true });
                Object.defineProperty(ev, 'isTrusted', { value: true });
                document.dispatchEvent(ev);
                if (cv) cv.dispatchEvent(ev);
            } catch(e) {}
            try {
                const ev2 = new PointerEvent('pointerup', { button: 2, buttons: 0, pointerId: 1, pointerType: 'mouse', bubbles: true });
                Object.defineProperty(ev2, 'isTrusted', { value: true });
                document.dispatchEvent(ev2);
                if (cv) cv.dispatchEvent(ev2);
            } catch(e) {}
            try {
                const ev3 = new MouseEvent('mouseup', { button: 2, buttons: 0, bubbles: true });
                Object.defineProperty(ev3, 'isTrusted', { value: true });
                document.dispatchEvent(ev3);
                if (cv) cv.dispatchEvent(ev3);
            } catch(e) {}
            if (cv) {
                try {
                    const rect = cv.getBoundingClientRect();
                    const ev4 = new MouseEvent('mouseup', {
                        clientX: rect.left + rect.width/2,
                        clientY: rect.top + rect.height/2,
                        button: 2, buttons: 0, bubbles: true
                    });
                    Object.defineProperty(ev4, 'isTrusted', { value: true });
                    cv.dispatchEvent(ev4);
                } catch(e) {}
            }
            setTimeout(() => {
                try {
                    const ev5 = new KeyboardEvent('keyup', { key: 'Shift', code: 'ShiftLeft', keyCode: 16, which: 16, bubbles: true });
                    Object.defineProperty(ev5, 'isTrusted', { value: true });
                    document.dispatchEvent(ev5);
                } catch(e) {}
            }, 50);
        }
    }

    // =============================================================
    // 8. CLICK (Mouse + Pointer)
    // =============================================================
    function clickAt(x,y) {
        if (!cv) return;
        try {
            // MouseEvent
            cv.dispatchEvent(new MouseEvent('mousedown', { clientX: x, clientY: y, button: 0, buttons: 1, bubbles: true }));
            setTimeout(() => {
                cv.dispatchEvent(new MouseEvent('mouseup', { clientX: x, clientY: y, button: 0, buttons: 0, bubbles: true }));
            }, 6);
            // PointerEvent fallback
            cv.dispatchEvent(new PointerEvent('pointerdown', { clientX: x, clientY: y, button: 0, buttons: 1, pointerId: 1, pointerType: 'mouse', bubbles: true }));
            setTimeout(() => {
                cv.dispatchEvent(new PointerEvent('pointerup', { clientX: x, clientY: y, button: 0, buttons: 0, pointerId: 1, pointerType: 'mouse', bubbles: true }));
            }, 6);
        } catch(e) {}
    }

    function rightClick(down, x, y) {
        if (!cv) return;
        try {
            const ev = down ? 'mousedown' : 'mouseup';
            const buttons = down ? 2 : 0;
            cv.dispatchEvent(new MouseEvent(ev, { clientX: x, clientY: y, button: 2, buttons: buttons, bubbles: true, cancelable: true }));
            // PointerEvent fallback
            const ev2 = down ? 'pointerdown' : 'pointerup';
            cv.dispatchEvent(new PointerEvent(ev2, { clientX: x, clientY: y, button: 2, buttons: buttons, pointerId: 1, pointerType: 'mouse', bubbles: true }));
        } catch(e) {}
    }

    // =============================================================
    // 9. STATE (có kiểm tra trạng thái game)
    // =============================================================
    let rt = null, pt = null, cv = null, mi = null;
    let mx = window.innerWidth/2, my = window.innerHeight/2;
    let lastAttack = 0, isRightDown = false;
    let ghostAngle = null, angleLock = 0;
    let enemyMap = new Map();
    let enemyAIs = new Map();
    let menuVisible = false;
    let rejoinTimer = null, duelTimer = null;
    let rejoinObserver = null, duelObserver = null;
    let toggleBtn = null, menu = null, overlay = null, ctx = null;
    let currentTarget = null;
    let move = { angle: 0, dist: 0, sprint: false, phase: 0 };
    let mode = 'combat';
    let modeTimer = 0;
    let lastReset = 0;
    let frame = 0;
    let isRunning = false;
    let targetLost = 0;
    let isGameActive = false;
    let stuckCount = 0;
    let lastPos = { x: 0, y: 0 };
    let lastPosTime = 0;
    let lastFrameTime = performance.now();
    let dt = 0; // Delta time thực tế

    // Game state
    let isSpectator = false;
    let isLoading = false;

    // =============================================================
    // 10. UI – CSS (giữ nguyên)
    // =============================================================
    function injectCSS() {
        const style = document.createElement('style');
        style.textContent = `
            @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
            #erabot-toggle{position:fixed;bottom:20px;left:20px;width:48px;height:48px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#1a0a2e,#2d1b4e);color:#fff;border-radius:50%;cursor:pointer;z-index:999999;font-size:22px;border:2px solid #fbbf24;box-shadow:0 0 25px rgba(251,191,36,.3);transition:all .3s;user-select:none;}
            #erabot-toggle:hover{transform:scale(1.1);box-shadow:0 0 40px rgba(251,191,36,.6);}
            #erabot-menu{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:340px;background:rgba(10,6,20,.96);backdrop-filter:blur(16px);border-radius:14px;font-family:'Inter',sans-serif;color:#e2e8f0;z-index:999998;border:1px solid rgba(251,191,36,.25);display:none;flex-direction:column;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.9);}
            #erabot-menu.visible{display:flex;}
            #erabot-header{padding:14px 18px;font-size:17px;font-weight:700;color:#fbbf24;border-bottom:1px solid rgba(251,191,36,.15);display:flex;align-items:center;justify-content:space-between;background:rgba(0,0,0,.3);cursor:grab;user-select:none;}
            #erabot-header:active{cursor:grabbing;}
            #erabot-close{cursor:pointer;font-size:22px;color:#666;transition:.2s;line-height:1;padding:0 4px;}
            #erabot-close:hover{color:#ef4444;}
            .erabot-body{padding:14px 18px;display:flex;flex-direction:column;gap:10px;max-height:70vh;overflow-y:auto;}
            .erabot-row{display:flex;justify-content:space-between;align-items:center;font-size:13px;color:#c8c8d0;padding:3px 0;}
            .erabot-row .label{display:flex;align-items:center;gap:6px;}
            .erabot-row .key{font-size:9px;color:#666;background:rgba(255,255,255,.05);padding:1px 7px;border-radius:3px;}
            .erabot-switch{position:relative;width:36px;height:18px;flex-shrink:0;}
            .erabot-switch input{opacity:0;width:0;height:0;}
            .erabot-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#333;transition:.3s;border-radius:18px;}
            .erabot-slider:before{position:absolute;content:"";height:12px;width:12px;left:3px;bottom:3px;background:#fff;transition:.3s;border-radius:50%;}
            input:checked+.erabot-slider{background:#fbbf24;}
            input:checked+.erabot-slider:before{transform:translateX(18px);}
            .erabot-range{display:flex;align-items:center;gap:8px;flex:1;max-width:140px;}
            .erabot-range input[type=range]{-webkit-appearance:none;width:100%;height:3px;background:#333;border-radius:2px;outline:none;}
            .erabot-range input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:#fbbf24;cursor:pointer;}
            .erabot-val{color:#fbbf24;font-size:12px;font-weight:600;min-width:30px;text-align:right;}
            .erabot-toast{position:fixed;top:16px;right:16px;background:rgba(10,6,20,.95);backdrop-filter:blur(12px);border:1px solid #fbbf24;border-radius:8px;padding:8px 16px;color:#fff;font-family:'Inter',sans-serif;font-size:13px;font-weight:600;z-index:9999999;box-shadow:0 8px 30px rgba(0,0,0,.8);opacity:0;transform:translateX(60px);transition:all .4s;max-width:300px;}
            .erabot-toast.show{opacity:1;transform:translateX(0);}
            .erabot-toast.success{border-color:#34d399;}
            .erabot-toast.warning{border-color:#fbbf24;}
            .erabot-toast.error{border-color:#ef4444;}
            #erabot-watermark{position:fixed;bottom:6px;left:50%;transform:translateX(-50%);color:rgba(251,191,36,.15);font-size:9px;font-family:'Inter',sans-serif;font-weight:600;z-index:999995;pointer-events:none;letter-spacing:.5px;}
            .erabot-shortcuts{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;}
            .erabot-shortcut{font-size:9px;color:#666;background:rgba(255,255,255,.04);padding:2px 8px;border-radius:3px;border:1px solid rgba(255,255,255,.04);}
            .erabot-shortcut .k{color:#fbbf24;font-weight:700;}
            .erabot-version{font-size:8px;color:#444;text-align:center;padding-top:6px;border-top:1px solid rgba(255,255,255,.04);margin-top:4px;}
            .erabot-select{background:#2a1840;color:#c8b8e0;border:1px solid #4a2080;border-radius:4px;padding:3px 6px;font-size:11px;outline:none;font-family:inherit;}
            .erabot-select option{background:#1a0a2e;}
        `;
        document.head.appendChild(style);
    }

    // =============================================================
    // 11. UI – MENU (giữ nguyên)
    // =============================================================
    function buildMenu() {
        if (document.getElementById('erabot-menu')) return;
        const html = `
            <div id="erabot-menu">
                <div id="erabot-header">
                    <span>🕺 EraBOT</span>
                    <span id="erabot-close">×</span>
                </div>
                <div class="erabot-body">
                    <div class="erabot-row"><span class="label">🕺 EraBOT <span class="key">(K)</span></span><label class="erabot-switch"><input type="checkbox" id="c-eraBot"><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">⚔️ Auto Attack <span class="key">(C)</span></span><label class="erabot-switch"><input type="checkbox" id="c-autoAttack" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">🔽 Filter Low Lv <span class="key">(E)</span></span><label class="erabot-switch"><input type="checkbox" id="c-filterLow" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">👁️ Hitbox <span class="key">(H)</span></span><label class="erabot-switch"><input type="checkbox" id="c-hitbox" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">🔄 Auto Rejoin</span><label class="erabot-switch"><input type="checkbox" id="c-autoRejoin" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">⚔️ Auto Duel</span><label class="erabot-switch"><input type="checkbox" id="c-autoDuel" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">🍖 Collect Food</span><label class="erabot-switch"><input type="checkbox" id="c-collectFood" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">🏃 Flee Danger</span><label class="erabot-switch"><input type="checkbox" id="c-fleeDanger" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">⚡ Sprint Mode</span><select class="erabot-select" id="c-sprintMode"><option value="0">Always</option><option value="1" selected>Towards Enemy</option><option value="2">Never</option></select></div>
                    <div class="erabot-row"><span class="label">🎯 Target Lv Range</span><span class="erabot-val" id="v-lvlRange">0-100</span></div>
                    <div class="erabot-row" style="gap:8px;"><input type="range" id="c-lvlMin" min="0" max="100" value="0" style="flex:1;"><input type="range" id="c-lvlMax" min="0" max="100" value="100" style="flex:1;"></div>
                    <div class="erabot-row"><span class="label">⬆ Max Higher</span><div class="erabot-range"><input type="range" id="c-maxHigher" min="0" max="20" value="5"><span class="erabot-val" id="v-maxHigher">5</span></div></div>
                    <div class="erabot-row"><span class="label">⬇ Min Lower</span><div class="erabot-range"><input type="range" id="c-minLower" min="0" max="20" value="0"><span class="erabot-val" id="v-minLower">0</span></div></div>
                    <div class="erabot-row"><span class="label">🛡️ Safe Distance</span><div class="erabot-range"><input type="range" id="c-safeDistance" min="1" max="3" step="0.1" value="1.5"><span class="erabot-val" id="v-safeDistance">1.5x</span></div></div>
                    <div class="erabot-row"><span class="label">🎯 Enemy Range</span><label class="erabot-switch"><input type="checkbox" id="c-enemyRange" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">⏳ Cooldown Timer</span><label class="erabot-switch"><input type="checkbox" id="c-cooldownTimer" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">📏 Tracers</span><label class="erabot-switch"><input type="checkbox" id="c-tracers" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">✨ Target Glow</span><label class="erabot-switch"><input type="checkbox" id="c-targetGlow" checked><span class="erabot-slider"></span></label></div>
                    <div class="erabot-row"><span class="label">🔍 Zoom</span><div class="erabot-range"><input type="range" id="c-zoom" min="0.3" max="2" step="0.05" value="0.9"><span class="erabot-val" id="v-zoom">0.90</span></div></div>
                    <div class="erabot-shortcuts">
                        <span class="erabot-shortcut"><span class="k">K</span> EraBOT</span>
                        <span class="erabot-shortcut"><span class="k">C</span> Attack</span>
                        <span class="erabot-shortcut"><span class="k">E</span> Filter</span>
                        <span class="erabot-shortcut"><span class="k">H</span> Hitbox</span>
                    </div>
                    <div class="erabot-version">v18.0.0</div>
                </div>
            </div>
        `;
        const div = document.createElement('div');
        div.innerHTML = html;
        document.body.appendChild(div);
        menu = document.getElementById('erabot-menu');
        bindMenu();
    }

    function bindMenu() {
        if (!menu) return;
        toggleBtn = document.getElementById('erabot-toggle');
        if (toggleBtn) toggleBtn.addEventListener('click', toggleMenu);
        document.getElementById('erabot-close').addEventListener('click', toggleMenu);
        document.addEventListener('click', (e) => {
            if (menuVisible && menu && !menu.contains(e.target) && e.target !== toggleBtn) toggleMenu();
        });

        const header = document.getElementById('erabot-header');
        let drag = false, ox, oy;
        header.addEventListener('mousedown', (e) => {
            if (e.target.id === 'erabot-close') return;
            drag = true;
            const rect = menu.getBoundingClientRect();
            ox = e.clientX - rect.left;
            oy = e.clientY - rect.top;
            menu.style.top = rect.top + 'px';
            menu.style.left = rect.left + 'px';
            menu.style.transform = 'none';
            e.preventDefault();
        });
        document.addEventListener('mousemove', (e) => {
            if (!drag) return;
            let x = e.clientX - ox, y = e.clientY - oy;
            x = Math.max(0, Math.min(x, window.innerWidth - 200));
            y = Math.max(0, Math.min(y, window.innerHeight - 100));
            menu.style.left = x + 'px';
            menu.style.top = y + 'px';
        });
        document.addEventListener('mouseup', () => { drag = false; });

        const toggleMap = {
            'c-eraBot': 'eraBot',
            'c-autoAttack': 'autoAttack',
            'c-filterLow': 'filterLow',
            'c-hitbox': 'hitbox',
            'c-autoRejoin': 'autoRejoin',
            'c-autoDuel': 'autoDuel',
            'c-collectFood': 'collectFood',
            'c-fleeDanger': 'fleeDanger',
            'c-enemyRange': 'showEnemyRange',
            'c-cooldownTimer': 'showCooldown',
            'c-tracers': 'showTracers',
            'c-targetGlow': 'showTargetGlow',
        };
        Object.keys(toggleMap).forEach(id => {
            const el = document.getElementById(id);
            if (!el) return;
            const key = toggleMap[id];
            if (!(key in C)) C[key] = true;
            el.checked = C[key];
            el.addEventListener('change', () => {
                C[key] = el.checked;
                saveConfig();
                if (key === 'autoRejoin') { if (C.autoRejoin) startRejoin(); else stopRejoin(); }
                if (key === 'autoDuel') { if (C.autoDuel) startDuel(); else stopDuel(); }
            });
        });

        const smEl = document.getElementById('c-sprintMode');
        if (smEl) {
            smEl.value = C.sprintMode;
            smEl.addEventListener('change', () => {
                C.sprintMode = parseInt(smEl.value);
                saveConfig();
            });
        }

        const lvlMinEl = document.getElementById('c-lvlMin');
        const lvlMaxEl = document.getElementById('c-lvlMax');
        const lvlRangeDisp = document.getElementById('v-lvlRange');
        if (lvlMinEl && lvlMaxEl && lvlRangeDisp) {
            lvlMinEl.value = C.lvlMin;
            lvlMaxEl.value = C.lvlMax;
            lvlRangeDisp.textContent = `${C.lvlMin}-${C.lvlMax}`;
            const updateLvlRange = () => {
                let mn = parseInt(lvlMinEl.value);
                let mx = parseInt(lvlMaxEl.value);
                if (mn > mx) { [mn, mx] = [mx, mn]; lvlMinEl.value = mn; lvlMaxEl.value = mx; }
                C.lvlMin = mn; C.lvlMax = mx;
                lvlRangeDisp.textContent = `${mn}-${mx}`;
                saveConfig();
            };
            lvlMinEl.addEventListener('input', updateLvlRange);
            lvlMaxEl.addEventListener('input', updateLvlRange);
        }

        const maxH = document.getElementById('c-maxHigher');
        const vMaxH = document.getElementById('v-maxHigher');
        if (maxH && vMaxH) {
            maxH.value = C.maxHigher;
            vMaxH.textContent = C.maxHigher;
            maxH.addEventListener('input', () => {
                C.maxHigher = parseInt(maxH.value);
                vMaxH.textContent = C.maxHigher;
                saveConfig();
            });
        }

        const minL = document.getElementById('c-minLower');
        const vMinL = document.getElementById('v-minLower');
        if (minL && vMinL) {
            minL.value = C.minLower;
            vMinL.textContent = C.minLower;
            minL.addEventListener('input', () => {
                C.minLower = parseInt(minL.value);
                vMinL.textContent = C.minLower;
                saveConfig();
            });
        }

        const sd = document.getElementById('c-safeDistance');
        const vSd = document.getElementById('v-safeDistance');
        if (sd && vSd) {
            sd.value = C.safeDistance;
            vSd.textContent = C.safeDistance.toFixed(1) + 'x';
            sd.addEventListener('input', () => {
                C.safeDistance = parseFloat(sd.value);
                vSd.textContent = C.safeDistance.toFixed(1) + 'x';
                saveConfig();
            });
        }

        const zoom = document.getElementById('c-zoom');
        const vZoom = document.getElementById('v-zoom');
        if (zoom && vZoom) {
            zoom.value = C.zoom;
            vZoom.textContent = C.zoom.toFixed(2);
            zoom.addEventListener('input', () => {
                C.zoom = parseFloat(zoom.value);
                vZoom.textContent = C.zoom.toFixed(2);
                saveConfig();
            });
        }
    }

    function toggleMenu() {
        if (!menu) return;
        menuVisible = !menuVisible;
        menu.classList.toggle('visible', menuVisible);
    }

    function saveConfig() {
        try { localStorage.setItem('erabot_cfg', JSON.stringify(C)); } catch(e) {}
    }
    function loadConfig() {
        try {
            const data = localStorage.getItem('erabot_cfg');
            if (data) {
                const parsed = JSON.parse(data);
                Object.keys(parsed).forEach(k => { if (k in C) C[k] = parsed[k]; });
            }
        } catch(e) {}
        // defaults
        if (!('collectFood' in C)) C.collectFood = true;
        if (!('fleeDanger' in C)) C.fleeDanger = true;
        if (!('sprintMode' in C)) C.sprintMode = 1;
        if (!('lvlMin' in C)) C.lvlMin = 0;
        if (!('lvlMax' in C)) C.lvlMax = 100;
        if (!('maxHigher' in C)) C.maxHigher = 5;
        if (!('minLower' in C)) C.minLower = 0;
        if (!('safeDistance' in C)) C.safeDistance = 1.5;
        if (!('showEnemyRange' in C)) C.showEnemyRange = true;
        if (!('showCooldown' in C)) C.showCooldown = true;
        if (!('showTracers' in C)) C.showTracers = true;
        if (!('showTargetGlow' in C)) C.showTargetGlow = true;
        if (!('autoRejoin' in C)) C.autoRejoin = true;
        if (!('autoDuel' in C)) C.autoDuel = true;
        if (!('aggression' in C)) C.aggression = 0.7;
        if (!('dodgeReaction' in C)) C.dodgeReaction = 0.6;
        if (!('baitChance' in C)) C.baitChance = 0.3;
    }

    // =============================================================
    // 12. KEYBOARD
    // =============================================================
    document.addEventListener('keydown', (e) => {
        if (document.activeElement?.tagName === 'INPUT') return;
        if (e.key === 'k' || e.key === 'K') {
            e.preventDefault();
            C.eraBot = !C.eraBot;
            const cb = document.getElementById('c-eraBot');
            if (cb) { cb.checked = C.eraBot; cb.dispatchEvent(new Event('change')); }
            saveConfig();
            if (C.eraBot) {
                toast('🕺 EraBOT đã bật', 'success');
                if (!C.autoAttack) { C.autoAttack = true; const at = document.getElementById('c-autoAttack'); if (at) at.checked = true; }
                if (C.autoRejoin) startRejoin();
                if (C.autoDuel) startDuel();
            } else {
                toast('🕺 EraBOT đã tắt', 'warning');
                stopRejoin(); stopDuel();
            }
            return;
        }
        const map = { 'c':'autoAttack', 'e':'filterLow', 'h':'hitbox' };
        const key = e.key.toLowerCase();
        if (map[key]) {
            const cfg = map[key];
            C[cfg] = !C[cfg];
            const cb = document.getElementById('c-'+cfg);
            if (cb) cb.checked = C[cfg];
            saveConfig();
            e.preventDefault();
        }
        if (e.key === 'Insert') { toggleMenu(); e.preventDefault(); }
    });

    // =============================================================
    // 13. AUTO REJOIN / DUEL (robust)
    // =============================================================
    function startRejoin() {
        if (rejoinTimer) clearInterval(rejoinTimer);
        if (rejoinObserver) rejoinObserver.disconnect();
        if (!C.autoRejoin) return;

        const clickPlay = () => {
            try {
                if (isSpectator || isLoading) return;
                const dead = document.querySelector('.dead-screen, .respawn-timer, .you-died, .game-over, .death-screen, .game-overlay');
                if (dead) {
                    const selectors = 'button, .btn, .button, [role="button"], .play-btn, .rejoin-btn, .respawn-btn, .continue-btn, .action-btn, [class*="play"], [class*="rejoin"], [class*="respawn"], [class*="continue"]';
                    const btns = document.querySelectorAll(selectors);
                    for (const btn of btns) {
                        const t = (btn.textContent || '').toLowerCase();
                        const cls = (btn.className || '').toLowerCase();
                        const id = (btn.id || '').toLowerCase();
                        if (t.includes('play') || t.includes('chơi') || t.includes('rejoin') || t.includes('vào lại') ||
                            t.includes('respawn') || t.includes('continue') || t.includes('tiếp tục') ||
                            cls.includes('play') || cls.includes('rejoin') || cls.includes('respawn') ||
                            id.includes('play') || id.includes('rejoin') || id.includes('respawn')) {
                            btn.click();
                            break;
                        }
                    }
                }
            } catch(e) {}
        };

        rejoinTimer = setInterval(clickPlay, 1500);
        rejoinObserver = new MutationObserver(clickPlay);
        rejoinObserver.observe(document.body, { childList: true, subtree: true, attributes: true });
    }
    function stopRejoin() {
        if (rejoinTimer) { clearInterval(rejoinTimer); rejoinTimer = null; }
        if (rejoinObserver) { rejoinObserver.disconnect(); rejoinObserver = null; }
    }

    function startDuel() {
        if (duelTimer) clearInterval(duelTimer);
        if (duelObserver) duelObserver.disconnect();
        if (!C.autoDuel) return;

        const clickDuel = () => {
            try {
                if (isSpectator || isLoading) return;
                const selectors = 'button, .btn, .button, [role="button"]';
                const btns = document.querySelectorAll(selectors);
                for (const btn of btns) {
                    const t = (btn.textContent || '').toLowerCase();
                    const cls = (btn.className || '').toLowerCase();
                    const id = (btn.id || '').toLowerCase();
                    if (t.includes('duel') || t.includes('đấu') || cls.includes('duel') || id.includes('duel')) {
                        btn.click();
                        setTimeout(() => {
                            try {
                                const btns2 = document.querySelectorAll(selectors);
                                for (const b of btns2) {
                                    const t2 = (b.textContent || '').toLowerCase();
                                    if ((t2.includes('fight') || t2.includes('chiến') || t2.includes('đánh')) && !b.disabled) {
                                        b.click();
                                        break;
                                    }
                                }
                            } catch(e) {}
                        }, 800);
                        return;
                    }
                }
            } catch(e) {}
        };

        duelTimer = setInterval(clickDuel, 2500);
        duelObserver = new MutationObserver(clickDuel);
        duelObserver.observe(document.body, { childList: true, subtree: true, attributes: true });
    }
    function stopDuel() {
        if (duelTimer) { clearInterval(duelTimer); duelTimer = null; }
        if (duelObserver) { duelObserver.disconnect(); duelObserver = null; }
    }

    // =============================================================
    // 14. OVERLAY
    // =============================================================
    function createOverlay() {
        if (overlay) overlay.remove();
        overlay = document.createElement('canvas');
        overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9997;';
        document.body.appendChild(overlay);
        ctx = overlay.getContext('2d');
        resizeOverlay();
        window.addEventListener('resize', resizeOverlay);
    }
    function resizeOverlay() { if (overlay) { overlay.width = window.innerWidth; overlay.height = window.innerHeight; } }

    // =============================================================
    // 15. TARGET SELECTION
    // =============================================================
    function scoreTarget(me, enemy, myLevel, myReach, now) {
        const eLevel = gl(enemy);
        const d = dist(me, enemy);
        const hp = gh(enemy);
        const data = enemyMap.get(enemy.uid);
        const ai = enemyAIs.get(enemy.uid);

        if (eLevel < C.lvlMin || eLevel > C.lvlMax) return -Infinity;
        if (eLevel > myLevel + C.maxHigher) return -Infinity;
        if (eLevel < myLevel - C.minLower) return -Infinity;

        let score = 0;
        if (d < myReach + 30 && d > myReach * 0.3) score += 40;
        else if (d < myReach + 70) score += 25;
        else if (d < myReach * 1.5) score += 10;
        else if (d > myReach * 2.5) score -= 40;
        else if (d < myReach * 0.2) score -= 30;

        if (hp < 300) score += 60;
        else if (hp < 600) score += 35;
        else if (hp < 1000) score += 10;

        if (eLevel < myLevel - 2) score += 30;
        else if (eLevel < myLevel) score += 15;
        else if (eLevel > myLevel + 4) score -= 50;
        else if (eLevel > myLevel + 2) score -= 25;

        if (ai && ai.confidence > 0.6) {
            const next = ai.predictNextAttack(now);
            if (next && now > next - 150 && now < next) score -= 40;
            if (ai.patternDetected) score += 20;
            if (ai.strafing) score += 10;
            if (ai.comboCount > 2) score += 15; // enemy đang combo, dễ đoán
        }

        if (data && data.attackStart) {
            const elapsed = now - data.attackStart;
            const timings = getWeaponTimings(eLevel);
            if (elapsed > timings.windup && elapsed < timings.windup + timings.cooldown) {
                score += 30;
            }
        }

        if (data && data.vx && data.vy && Math.hypot(data.vx, data.vy) > 5) score += 20;
        return score;
    }

    function bestTarget(me, enemies, myLevel, myReach, now) {
        let best = null, bestScore = -Infinity;
        for (const enemy of enemies) {
            const s = scoreTarget(me, enemy, myLevel, myReach, now);
            if (s > bestScore) { bestScore = s; best = enemy; }
        }
        return { target: best, score: bestScore };
    }

    // =============================================================
    // 16. DANGER MAP
    // =============================================================
    function dangerMap(me, enemies, reach, now) {
        let dx=0, dy=0, total=0, count=0;
        for (const enemy of enemies) {
            const d = dist(me, enemy);
            if (d > reach * 2.5) continue;
            count++;
            const lv = gl(enemy);
            const r = getReach(lv).d * getMul(lv);
            const data = enemyMap.get(enemy.uid);
            const ai = enemyAIs.get(enemy.uid);
            let w = 1 / (d + 10);
            w *= 1 + lv / 50;
            w *= 1 + r / 300;
            if (data && data.attackStart) {
                const elapsed = now - data.attackStart;
                const timings = getWeaponTimings(lv);
                if (elapsed < timings.windup) w *= 2.5;
                else if (elapsed < timings.windup + timings.cooldown) w *= 1.3;
            }
            if (ai && ai.confidence > 0.5) {
                const next = ai.predictNextAttack(now);
                if (next && now > next - 200 && now < next) w *= 2;
                if (ai.patternDetected) w *= 0.8;
                if (ai.comboCount > 2) w *= 1.2; // enemy đang combo, nguy hiểm hơn
            }
            const angle = Math.atan2(enemy.y - me.y, enemy.x - me.x);
            dx += Math.cos(angle) * w;
            dy += Math.sin(angle) * w;
            total += w;
        }
        if (total > 0) { dx /= total; dy /= total; }
        return { x: dx, y: dy, count };
    }

    function safeDir(me, enemies, reach, now) {
        const d = dangerMap(me, enemies, reach, now);
        const angle = Math.atan2(d.y, d.x) + Math.PI + rand(-0.15, 0.15);
        const dist2 = 200 + Math.sin(now * 0.002) * 30;
        return {
            x: me.x + Math.cos(angle) * dist2,
            y: me.y + Math.sin(angle) * dist2,
            threat: d.count / (enemies.length + 1)
        };
    }

    // =============================================================
    // 17. DODGE SWING
    // =============================================================
    function dodgeSwing(me, enemy, enemyReach, enemySize, now) {
        const ai = enemyAIs.get(enemy.uid);
        if (!ai || ai.confidence < 0.4) return null;
        const swing = ai.getSwingAngle();
        if (Math.abs(swing) < 0.25) return null;
        const dodgeAngle = swing > 0 ? -Math.PI/2 : Math.PI/2;
        const dist2 = (enemyReach + enemySize) * 0.65 * C.dodgeReaction;
        return {
            x: me.x + Math.cos(dodgeAngle) * dist2,
            y: me.y + Math.sin(dodgeAngle) * dist2,
            angle: dodgeAngle
        };
    }

    // =============================================================
    // 18. SMART MOVEMENT + BAIT + ANTI-STUCK
    // =============================================================
    const MAP_MIN = 120, MAP_MAX = 19880;

    function clampPos(x, y) {
        return { x: clamp(x, MAP_MIN, MAP_MAX), y: clamp(y, MAP_MIN, MAP_MAX) };
    }

    function avoidWalls(me, targetX, targetY, step = 60) {
        let bestX = targetX, bestY = targetY;
        let bestScore = -Infinity;
        const candidates = [
            { x: targetX, y: targetY },
            { x: targetX + step, y: targetY },
            { x: targetX - step, y: targetY },
            { x: targetX, y: targetY + step },
            { x: targetX, y: targetY - step },
            { x: targetX + step * 0.7, y: targetY + step * 0.7 },
            { x: targetX - step * 0.7, y: targetY + step * 0.7 },
            { x: targetX + step * 0.7, y: targetY - step * 0.7 },
            { x: targetX - step * 0.7, y: targetY - step * 0.7 },
        ];
        for (const c of candidates) {
            const cx = clamp(c.x, MAP_MIN, MAP_MAX);
            const cy = clamp(c.y, MAP_MIN, MAP_MAX);
            const edgeDist = Math.min(cx - MAP_MIN, MAP_MAX - cx, cy - MAP_MIN, MAP_MAX - cy);
            const score = edgeDist * 2 - Math.hypot(cx - targetX, cy - targetY) * 0.3;
            if (score > bestScore) {
                bestScore = score;
                bestX = cx;
                bestY = cy;
            }
        }
        return { x: bestX, y: bestY };
    }

    function findNearestOrb(me, now, maxRadius) {
        if (!rt || !rt.running_layout) return null;
        maxRadius = maxRadius || 700;
        let best = null, bestD = Infinity;
        try {
            for (const layer of rt.running_layout.layers) {
                if (layer && layer.objects) {
                    for (const obj of layer.objects) {
                        if (isOrb(obj)) {
                            const d = Math.hypot(me.x - obj.x, me.y - obj.y);
                            if (d < maxRadius && d < bestD) {
                                bestD = d;
                                best = obj;
                            }
                        }
                    }
                }
            }
        } catch(e) {}
        return best;
    }

    function findWanderTarget(me, enemies) {
        let nearest = null, nd = Infinity;
        if (enemies && enemies.length > 0) {
            for (const e of enemies) {
                const d = dist(me, e);
                if (d < nd) { nd = d; nearest = e; }
            }
        }
        if (nearest && nd < 1200) {
            return { x: nearest.x, y: nearest.y };
        }
        const cx = 10000, cy = 10000;
        const d = Math.hypot(me.x - cx, me.y - cy);
        if (d > 200) {
            return { x: cx, y: cy };
        }
        move.phase += 0.02;
        const angle = move.phase * 0.7 + Math.sin(move.phase * 1.3) * 0.5;
        const dist2 = 200 + Math.sin(move.phase * 0.5) * 100;
        return {
            x: me.x + Math.cos(angle) * dist2,
            y: me.y + Math.sin(angle) * dist2
        };
    }

    // Anti-stuck: kiểm tra và tự động di chuyển
    function checkStuck(me, now) {
        if (now - lastPosTime < 500) return false;
        const moved = Math.hypot(me.x - lastPos.x, me.y - lastPos.y);
        if (moved < 5) {
            stuckCount++;
            if (stuckCount > 3) {
                // Bot bị kẹt -> di chuyển ngẫu nhiên
                const forceAngle = Math.random() * Math.PI * 2 + move.phase * 0.5;
                const forceDist = 40 + Math.random() * 30;
                me.x += Math.cos(forceAngle) * forceDist;
                me.y += Math.sin(forceAngle) * forceDist;
                if (rt && rt.running_layout) {
                    rt.running_layout.scrollX += Math.cos(forceAngle) * forceDist;
                    rt.running_layout.scrollY += Math.sin(forceAngle) * forceDist;
                }
                stuckCount = 0;
                return true;
            }
        } else {
            stuckCount = 0;
        }
        lastPos = { x: me.x, y: me.y };
        lastPosTime = now;
        return false;
    }

    function calcMove(me, target, myReach, enemyReach, enemySize, now, allEnemies) {
        const myLevel = gl(me);

        // Anti-stuck
        if (checkStuck(me, now)) {
            // Đã di chuyển trong checkStuck
            return { x: me.x, y: me.y, sprint: false };
        }

        // === ƯU TIÊN ORB KHI LEVEL <= 5 ===
        if (C.collectFood && myLevel <= 5) {
            const orb = findNearestOrb(me, now, 700);
            if (orb) {
                const d = Math.hypot(orb.x - me.x, orb.y - me.y);
                if (d > 20) {
                    const angle = Math.atan2(orb.y - me.y, orb.x - me.x);
                    let avoid = { x: 0, y: 0 };
                    if (allEnemies) {
                        for (const e of allEnemies) {
                            const ed = dist(me, e);
                            if (ed < 300) {
                                const w = (300 - ed) / 300;
                                avoid.x += (me.x - e.x) / Math.max(1, ed) * w * 0.5;
                                avoid.y += (me.y - e.y) / Math.max(1, ed) * w * 0.5;
                            }
                        }
                    }
                    const finalAngle = angle + Math.atan2(avoid.y, avoid.x) * 0.3;
                    let tx = me.x + Math.cos(finalAngle) * Math.min(300, d);
                    let ty = me.y + Math.sin(finalAngle) * Math.min(300, d);
                    const pos = avoidWalls(me, tx, ty);
                    return { x: pos.x, y: pos.y, sprint: true };
                }
            }
        }

        // === KHÔNG CÓ TARGET ===
        if (!target) {
            targetLost++;
            if (targetLost < 5 && currentTarget) {
                target = currentTarget;
            } else {
                targetLost = 0;
                const wanderTarget = findWanderTarget(me, allEnemies);
                const d = Math.hypot(wanderTarget.x - me.x, wanderTarget.y - me.y);
                if (d > 20) {
                    const angle = Math.atan2(wanderTarget.y - me.y, wanderTarget.x - me.x);
                    let tx = me.x + Math.cos(angle) * Math.min(250, d);
                    let ty = me.y + Math.sin(angle) * Math.min(250, d);
                    const pos = avoidWalls(me, tx, ty);
                    return { x: pos.x, y: pos.y, sprint: false };
                }
                move.phase += 0.015;
                const wa = move.phase * 0.6 + Math.sin(move.phase * 1.2) * 0.4;
                const wd = 100 + Math.sin(move.phase * 0.6) * 40;
                let tx = me.x + Math.cos(wa) * wd;
                let ty = me.y + Math.sin(wa) * wd;
                const pos = avoidWalls(me, tx, ty);
                return { x: pos.x, y: pos.y, sprint: false };
            }
        } else {
            targetLost = 0;
        }

        // === CÓ TARGET ===
        const dx = target.x - me.x;
        const dy = target.y - me.y;
        const d = Math.hypot(dx, dy);
        if (d < 0.5) { const pos = avoidWalls(me, me.x + 50, me.y + 50); return { x: pos.x, y: pos.y, sprint: false }; }

        const angle = Math.atan2(dy, dx);
        const data = enemyMap.get(target.uid);
        const ai = enemyAIs.get(target.uid);

        const dangerZone = enemyReach + enemySize + 10;
        const attackZone = myReach + 10;

        let isAttacking = false, isCooldown = false;
        const lv = gl(target);
        const timings = getWeaponTimings(lv);
        if (data && data.attackStart) {
            const elapsed = now - data.attackStart;
            if (elapsed < timings.windup) isAttacking = true;
            else if (elapsed < timings.windup + timings.cooldown) isCooldown = true;
        }

        // Dùng dt thực tế để dự đoán ping
        const pingOffset = pingEstimate / 1000;
        let predX = target.x, predY = target.y;
        if (ai && ai.confidence > 0.5) {
            const pred = ai.predictPos(200 + pingEstimate);
            if (pred) { predX = pred.x; predY = pred.y; }
        } else if (data && data.vx && data.vy) {
            predX += data.vx * (0.2 + pingOffset * 0.5);
            predY += data.vy * (0.2 + pingOffset * 0.5);
        }
        const predAngle = Math.atan2(predY - me.y, predX - me.x);

        let antiDodge = 0;
        if (ai && ai.confidence > 0.5) {
            antiDodge = ai.predictDodgeDirection() * 0.3;
        }

        const danger = dangerMap(me, allEnemies || [], myReach, now);
        let newMode = mode;
        const modeCooldown = 300;
        if (now >= modeTimer) {
            if (danger.count >= 3 && d < attackZone + 100) {
                newMode = 'retreat';
                modeTimer = now + modeCooldown;
            } else if (ai && ai.baitFreq > 0.4 && d > attackZone + 30 && d < attackZone + 100) {
                newMode = 'bait';
                modeTimer = now + modeCooldown;
            } else if (danger.count > 0 && allEnemies && allEnemies.length > 1) {
                let hasAlly = false;
                for (const e of allEnemies) {
                    if (dist(target, e) < 300 && e.uid !== target.uid) { hasAlly = true; break; }
                }
                if (hasAlly) { newMode = 'flank'; modeTimer = now + modeCooldown; }
                else newMode = 'combat';
            } else {
                newMode = 'combat';
            }
            if (newMode !== mode) { mode = newMode; modeTimer = now + modeCooldown; }
        }

        // === FLEE DANGER ===
        if (C.fleeDanger && target) {
            const eLv = gl(target);
            if (eLv > myLevel + C.maxHigher + 3) {
                const d2 = dist(me, target);
                const eReach = getReach(eLv).d * getMul(eLv);
                if (d2 < eReach * C.safeDistance) {
                    const fleeAngle = Math.atan2(me.y - target.y, me.x - target.x);
                    let tx = me.x + Math.cos(fleeAngle) * 250;
                    let ty = me.y + Math.sin(fleeAngle) * 250;
                    const pos = avoidWalls(me, tx, ty);
                    return { x: pos.x, y: pos.y, sprint: true };
                }
            }
        }

        // === BAIT / FAKE MOVEMENT ===
        let baitOffset = 0;
        let fakeDirection = 0;
        if (ai && ai.confidence > 0.5 && d < attackZone + 40 && d > attackZone - 30 && !isAttacking && Math.random() < C.baitChance) {
            const phase = (now * 0.001 + hash(target.uid.toString()) * 0.3) % 1;
            if (phase > 0.6 && phase < 0.75) {
                baitOffset = -40;
                fakeDirection = 0.3;
            } else if (phase > 0.75 && phase < 0.9) {
                baitOffset = 70;
                fakeDirection = -0.4;
            }
            if (phase > 0.5 && phase < 0.6) {
                fakeDirection = 0.6;
            }
        }

        const swing = dodgeSwing(me, target, enemyReach, enemySize, now);
        if (swing && isAttacking && d < dangerZone + 20) {
            const dist2 = 180 + Math.sin(now * 0.002) * 20;
            const angle2 = swing.angle + rand(-0.1, 0.1);
            let tx = me.x + Math.cos(angle2) * dist2;
            let ty = me.y + Math.sin(angle2) * dist2;
            const pos = avoidWalls(me, tx, ty);
            return { x: pos.x, y: pos.y, sprint: true };
        }

        let targetDist, desiredAngle;
        switch(mode) {
            case 'retreat': {
                const safe = safeDir(me, allEnemies || [], myReach, now);
                const ra = Math.atan2(safe.y - me.y, safe.x - me.x);
                targetDist = 250 + Math.sin(now * 0.002) * 30;
                desiredAngle = ra + rand(-0.1, 0.1);
                move.sprint = true;
                if (Math.hypot(safe.x - me.x, safe.y - me.y) < 100) { mode = 'combat'; modeTimer = now + modeCooldown; }
                break;
            }
            case 'bait': {
                const phase = (now * 0.001 + (target.uid % 100) * 0.01) % 1;
                if (phase < 0.3) {
                    targetDist = Math.min(200, d * 0.6 + 20 + baitOffset * 0.5);
                    desiredAngle = predAngle + fakeDirection + antiDodge + Math.sin(now * 0.003) * 0.1;
                } else if (phase < 0.6) {
                    targetDist = Math.max(150, d * 1.2 + 30 + baitOffset * 0.3);
                    desiredAngle = angle + Math.PI * (0.8 + fakeDirection) + antiDodge * 0.5 + Math.sin(now * 0.002) * 0.2;
                } else {
                    targetDist = attackZone * 0.9;
                    const tangent = angle + (Math.PI/2) * (Math.sin(now * 0.0015) > 0 ? 1 : -1);
                    desiredAngle = tangent + antiDodge + Math.sin(now * 0.004) * 0.3;
                }
                move.sprint = (phase < 0.3);
                break;
            }
            case 'flank': {
                const fa = angle + Math.PI * 0.7 + Math.sin(now * 0.001 + target.uid * 0.01) * 0.3 + antiDodge * 0.3;
                targetDist = Math.min(300, Math.max(150, d * 0.9));
                desiredAngle = fa;
                move.sprint = true;
                const behind = Math.abs(angleDiff(angle, Math.atan2(me.y - target.y, me.x - target.x))) < 0.5;
                if (behind && d < attackZone + 30) { mode = 'combat'; modeTimer = now + modeCooldown; }
                break;
            }
            default: {
                const aggression = C.aggression || 0.7;
                if (d > attackZone + 60) {
                    targetDist = Math.min(300, d * 0.7 + 30 + baitOffset * 0.2);
                    desiredAngle = predAngle + fakeDirection * 0.3 + antiDodge * 0.2 + Math.sin(move.phase * 1.3) * 0.2;
                    move.phase += 0.03;
                } else if ((isCooldown || (data && Math.hypot(data.vx||0, data.vy||0) > 5)) &&
                           d < attackZone + 30 && d > attackZone * 0.4) {
                    targetDist = Math.min(200, Math.max(50, d * 0.3 + 20 + baitOffset * 0.1));
                    desiredAngle = predAngle + fakeDirection * 0.2 + antiDodge * 0.3;
                    move.phase += 0.05;
                } else if (d < attackZone * 0.4) {
                    targetDist = Math.min(250, Math.max(150, attackZone * 0.6));
                    desiredAngle = angle + Math.PI * (0.7 + fakeDirection * 0.3 + antiDodge * 0.2 + Math.sin(move.phase * 0.5) * 0.2);
                    move.phase += 0.02;
                } else {
                    targetDist = attackZone * 0.85 + Math.sin(move.phase * 0.7) * 10;
                    const tangent = angle + (Math.PI/2) * (Math.sin(move.phase * 1.1) > 0 ? 1 : -1);
                    desiredAngle = tangent + fakeDirection * 0.2 + antiDodge * 0.2 + Math.sin(move.phase * 2.3) * 0.3;
                    move.phase += 0.04;
                }
                switch(C.sprintMode) {
                    case 0: move.sprint = true; break;
                    case 1: move.sprint = (d > attackZone + 80 || isAttacking); break;
                    case 2: move.sprint = false; break;
                    default: move.sprint = false;
                }
            }
        }

        if (ai && ai.confidence > 0.6 && Math.abs(ai.dodgePref) > 0.3) {
            desiredAngle += ai.dodgePref * 0.15;
        }

        const ad = angleDiff(move.angle, desiredAngle);
        move.angle += ad * 0.2;
        move.dist += (targetDist - move.dist) * 0.15;
        move.dist = clamp(move.dist, 40, 400);

        const ma = move.angle;
        const md = move.dist;
        let tx = me.x + Math.cos(ma) * md;
        let ty = me.y + Math.sin(ma) * md;

        const maxMove = 380;
        const actual = Math.hypot(tx-me.x, ty-me.y);
        if (actual > maxMove) {
            tx = me.x + (tx-me.x)/actual * maxMove;
            ty = me.y + (ty-me.y)/actual * maxMove;
        }

        const nd = Math.hypot(tx - target.x, ty - target.y);
        const minSafe = dangerZone * 0.5;
        if (nd < minSafe) {
            const pa = Math.atan2(ty - target.y, tx - target.x);
            const pd = dangerZone * 0.8;
            tx = target.x + Math.cos(pa) * pd;
            ty = target.y + Math.sin(pa) * pd;
        }

        const pos = avoidWalls(me, tx, ty);
        return { x: pos.x, y: pos.y, sprint: move.sprint };
    }

    // =============================================================
    // 19. MAIN LOOP (với dt thực tế và kiểm tra trạng thái game)
    // =============================================================
    let loopCount = 0;

    function mainLoop() {
        if (!isRunning) {
            if (!isGameActive) {
                setTimeout(() => {
                    if (rt && rt.running_layout) {
                        isGameActive = true;
                        isRunning = true;
                        mainLoop();
                    } else {
                        findRuntime();
                    }
                }, 2000);
            }
            return;
        }

        // Kiểm tra trạng thái game
        const nowReal = performance.now();
        const visible = document.visibilityState === 'visible';
        if (!visible) {
            requestAnimationFrame(mainLoop);
            return;
        }

        // Kiểm tra spectator / loading
        try {
            const loadingEl = document.querySelector('.loading, .connecting, .reconnecting, .spectator-overlay');
            if (loadingEl && loadingEl.style.display !== 'none') {
                isLoading = true;
                if (ctx) ctx.clearRect(0,0,overlay.width,overlay.height);
                if (isRightDown) { rightClick(false, mx, my); isRightDown = false; }
                requestAnimationFrame(mainLoop);
                return;
            } else {
                isLoading = false;
            }
            const specEl = document.querySelector('.spectator-mode, .spectator');
            if (specEl && specEl.style.display !== 'none') {
                isSpectator = true;
                if (ctx) ctx.clearRect(0,0,overlay.width,overlay.height);
                if (isRightDown) { rightClick(false, mx, my); isRightDown = false; }
                requestAnimationFrame(mainLoop);
                return;
            } else {
                isSpectator = false;
            }
        } catch(e) {}

        // Tính dt thực tế
        const now = performance.now();
        dt = Math.min(0.05, (now - lastFrameTime) / 1000);
        lastFrameTime = now;

        try {
            if (!rt || !rt.running_layout || !pt || !cv) {
                isGameActive = false;
                if (ctx) ctx.clearRect(0,0,overlay.width,overlay.height);
                if (isRightDown) { rightClick(false, mx, my); isRightDown = false; }
                if (isRunning) { isRunning = false; }
                requestAnimationFrame(mainLoop);
                return;
            }

            // Áp dụng zoom mỗi frame
            try {
                for (const layer of rt.running_layout.layers) {
                    if (layer && layer.scale !== C.zoom) {
                        layer.scale = C.zoom;
                        if (layer.setZIndicesStaleFrom) layer.setZIndicesStaleFrom(0);
                    }
                }
            } catch(e) {}

            isGameActive = true;
            loopCount++;
            frame++;
            if (now > angleLock) ghostAngle = null;

            const sx = rt.running_layout.scrollX;
            const sy = rt.running_layout.scrollY;

            let me = null, minD = Infinity;
            for (const inst of pt.instances) {
                const d = Math.hypot(inst.x - sx, inst.y - sy);
                if (d < minD) { minD = d; me = inst; }
            }
            if (!me) {
                if (ctx) ctx.clearRect(0,0,overlay.width,overlay.height);
                if (isRightDown) { rightClick(false, mx, my); isRightDown = false; }
                requestAnimationFrame(mainLoop);
                return;
            }

            const myLevel = gl(me);
            const myTeam = gt(me);
            const mySize = me.width * 0.35;
            const reachData = getReach(myLevel);
            const reachMul = getMul(myLevel);
            const myReach = reachData.d * reachMul;
            const degrees = reachData.deg;
            const myTimings = getWeaponTimings(myLevel);
            const attackCD = myTimings.cooldown;

            const rect = cv.getBoundingClientRect();
            const sx2 = rect.width / cv.width;
            const sy2 = rect.height / cv.height;
            const ls = me.layer.getScale();

            const cx = rect.left + rect.width/2;
            const cy = rect.top + rect.height/2;
            const px = cx + (me.x - sx) * ls * sx2;
            const py = cy + (me.y - sy) * ls * sy2;

            if (ctx) ctx.clearRect(0,0,overlay.width,overlay.height);

            // ---- Collect enemies ----
            let candidateEnemies = [];
            const seen = new Set();
            const allInstances = pt.instances;

            for (const inst of allInstances) {
                if (inst.uid === me.uid || inst.width <= 0) continue;
                const enemyTeam = gt(inst);
                if (myTeam !== -1 && enemyTeam === myTeam) continue;
                const d = Math.hypot(me.x - inst.x, me.y - inst.y);
                if (d > 1200) continue;
                candidateEnemies.push({ inst, d });
            }
            candidateEnemies.sort((a,b) => a.d - b.d);
            // Giới hạn enemy để giảm tải
            if (candidateEnemies.length > 20) candidateEnemies.length = 20;

            const enemies = [];
            for (const item of candidateEnemies) {
                const inst = item.inst;
                seen.add(inst.uid);
                const enemyLevel = gl(inst);
                const enemySize = inst.width * 0.35;
                const enemyReach = getReach(enemyLevel).d * getMul(enemyLevel);
                const d = item.d;

                let data = enemyMap.get(inst.uid);
                if (!data) {
                    data = { attackStart: 0, lastX: inst.x, lastY: inst.y, vx: 0, vy: 0, lastSpeed: 0, lastAngle: 0, lastDist: 0, angleChanged: false, _lastAngleChange: 0 };
                    enemyMap.set(inst.uid, data);
                }
                data.vx = inst.x - data.lastX;
                data.vy = inst.y - data.lastY;
                data.lastX = inst.x;
                data.lastY = inst.y;

                // Phát hiện góc thay đổi đột ngột
                const currentAngle = inst.angle || 0;
                if (data.lastAngle !== undefined) {
                    const da = Math.abs(angleDiff(data.lastAngle, currentAngle));
                    if (da > 0.5) {
                        data.angleChanged = true;
                        data._lastAngleChange = now;
                    } else {
                        data.angleChanged = false;
                    }
                }
                data.lastAngle = currentAngle;

                detectAttackFallback(inst.uid, data, now);

                if (frame % 5 === 0) {
                    let ai = enemyAIs.get(inst.uid);
                    if (!ai) { ai = new EnemyAI(inst.uid); enemyAIs.set(inst.uid, ai); }
                    ai.update(inst, data.vx, data.vy, now);
                }

                let ai = enemyAIs.get(inst.uid);
                let isSwinging = false;
                const eTimings = getWeaponTimings(enemyLevel);
                if (data.attackStart) {
                    const elapsed = now - data.attackStart;
                    if (elapsed < eTimings.windup) isSwinging = true;
                }
                if (ai && ai.confidence > 0.5) {
                    const next = ai.predictNextAttack(now);
                    if (next && now > next - 100 && now < next) isSwinging = true;
                }

                let valid = true;
                if (C.filterLow && C.eraBot) {
                    if (isLow(enemyLevel) && d > myReach + enemySize + 20) valid = false;
                }

                if (valid) {
                    enemies.push(inst);
                }

                // ---- ESP (vẽ mỗi 5 frame, giới hạn 15 enemy) ----
                if (C.hitbox && frame % 5 === 0 && enemies.length < 15) {
                    const pts = getPts(inst);
                    if (pts.length > 1) {
                        ctx.beginPath();
                        ctx.moveTo(cx + (pts[0].x - sx)*ls*sx2, cy + (pts[0].y - sy)*ls*sy2);
                        for (let i=1; i<pts.length; i++) {
                            ctx.lineTo(cx + (pts[i].x - sx)*ls*sx2, cy + (pts[i].y - sy)*ls*sy2);
                        }
                        ctx.closePath();
                        ctx.fillStyle = 'rgba(251,191,36,0.10)';
                        ctx.fill();
                        ctx.strokeStyle = 'rgba(251,191,36,0.35)';
                        ctx.lineWidth = 1.2;
                        ctx.stroke();
                    }
                    const lvY = cy + (inst.y - sy)*ls*sy2 - enemySize*ls*sy2 - 6;
                    ctx.font = '500 10px Inter, sans-serif';
                    ctx.textAlign = 'center';
                    ctx.fillStyle = 'rgba(255,255,255,0.5)';
                    ctx.fillText('Lv.' + (Math.floor(enemyLevel)+1), cx + (inst.x - sx)*ls*sx2, lvY);
                }

                if (C.showTracers && valid && enemies.length < 15) {
                    ctx.beginPath();
                    ctx.moveTo(px, py);
                    ctx.lineTo(cx + (inst.x - sx)*ls*sx2, cy + (inst.y - sy)*ls*sy2);
                    ctx.strokeStyle = isSwinging ? 'rgba(239,68,68,0.2)' : 'rgba(251,191,36,0.12)';
                    ctx.lineWidth = 0.5;
                    ctx.stroke();
                }

                if (C.showEnemyRange && valid && enemies.length < 15) {
                    ctx.beginPath();
                    ctx.arc(cx + (inst.x - sx)*ls*sx2, cy + (inst.y - sy)*ls*sy2, enemyReach * ls * sx2, 0, Math.PI * 2);
                    ctx.strokeStyle = 'rgba(255,60,60,0.2)';
                    ctx.lineWidth = 1;
                    ctx.setLineDash([3, 3]);
                    ctx.stroke();
                    ctx.setLineDash([]);
                }

                if (C.showCooldown && valid && data.attackStart && enemies.length < 15) {
                    const elapsed = now - data.attackStart;
                    const total = eTimings.windup + eTimings.cooldown;
                    const pct = Math.min(1, elapsed / total);
                    const eX = cx + (inst.x - sx)*ls*sx2;
                    const eY = cy + (inst.y - sy)*ls*sy2;
                    ctx.beginPath();
                    ctx.arc(eX, eY, 22, -Math.PI/2, -Math.PI/2 + Math.PI * 2 * pct);
                    ctx.strokeStyle = pct < 0.15 ? '#22c55e' : '#f59e0b';
                    ctx.lineWidth = 2;
                    ctx.stroke();
                    ctx.fillStyle = '#fff';
                    ctx.font = 'bold 8px Inter';
                    ctx.textAlign = 'center';
                    ctx.fillText(((total - elapsed)/1000).toFixed(1) + 's', eX, eY - 28);
                }

                if (C.showTargetGlow && currentTarget && currentTarget.uid === inst.uid && enemies.length < 15) {
                    const eX = cx + (inst.x - sx)*ls*sx2;
                    const eY = cy + (inst.y - sy)*ls*sy2;
                    ctx.shadowColor = '#a855f7';
                    ctx.shadowBlur = 25;
                    ctx.beginPath();
                    ctx.arc(eX, eY, 26, 0, Math.PI * 2);
                    ctx.strokeStyle = '#a855f7';
                    ctx.lineWidth = 2.5;
                    ctx.setLineDash([3, 3]);
                    ctx.stroke();
                    ctx.setLineDash([]);
                    ctx.shadowBlur = 0;
                }
            }

            // Dọn dẹp bộ nhớ
            if (frame % 60 === 0) {
                const now3 = performance.now();
                for (const [uid, ai] of enemyAIs) {
                    if (now3 - ai.lastSeen > 5000) { enemyAIs.delete(uid); }
                }
                for (const key of enemyMap.keys()) {
                    if (!seen.has(key)) { enemyMap.delete(key); }
                }
                // Giới hạn kích thước Map
                if (enemyMap.size > 50) {
                    const keys = Array.from(enemyMap.keys());
                    for (let i = 0; i < keys.length - 30; i++) {
                        enemyMap.delete(keys[i]);
                    }
                }
                if (enemyAIs.size > 50) {
                    const keys = Array.from(enemyAIs.keys());
                    for (let i = 0; i < keys.length - 30; i++) {
                        enemyAIs.delete(keys[i]);
                    }
                }
            }

            // ---- Auto Attack target ----
            let attackTarget = null, attackD = Infinity;
            for (const inst of enemies) {
                const d = dist(me, inst);
                const eSize = inst.width * 0.35;
                if (d < myReach + eSize && d < attackD) {
                    attackD = d;
                    attackTarget = inst;
                }
            }

            // ---- Decision ----
            let targetX = mx, targetY = my;
            let shouldRight = false, shouldAttack = false;

            if (C.eraBot) {
                let huntTarget = null;
                if (enemies.length > 0) {
                    const result = bestTarget(me, enemies, myLevel, myReach, now);
                    huntTarget = result.target;
                }

                if (currentTarget && enemyMap.has(currentTarget.uid)) {
                    const currentScore = scoreTarget(me, currentTarget, myLevel, myReach, now);
                    const newScore = huntTarget ? scoreTarget(me, huntTarget, myLevel, myReach, now) : -Infinity;
                    if (huntTarget && newScore > currentScore * 1.08) {
                        currentTarget = huntTarget;
                    }
                } else {
                    currentTarget = huntTarget;
                }

                const moveTarget = currentTarget;

                if (!moveTarget) {
                    if (now - lastReset > 3000) lastReset = now;
                    const moveResult = calcMove(me, null, myReach, 0, 0, now, enemies);
                    const delta = Math.hypot(moveResult.x - me.x, moveResult.y - me.y);
                    if (delta > 0) {
                        targetX = cx + ((moveResult.x - me.x)/delta) * 70;
                        targetY = cy + ((moveResult.y - me.y)/delta) * 70;
                    }
                    shouldRight = true;
                    move.sprint = moveResult.sprint;
                } else {
                    const enemyLevel = gl(moveTarget);
                    const enemyReach = getReach(enemyLevel).d * getMul(enemyLevel);
                    const enemySize = moveTarget.width * 0.35;
                    const moveResult = calcMove(me, moveTarget, myReach, enemyReach, enemySize, now, enemies);
                    const delta = Math.hypot(moveResult.x - me.x, moveResult.y - me.y);
                    if (delta > 0) {
                        targetX = cx + ((moveResult.x - me.x)/delta) * 70;
                        targetY = cy + ((moveResult.y - me.y)/delta) * 70;
                    }
                    shouldRight = true;
                    move.sprint = moveResult.sprint;

                    if (C.autoAttack && moveTarget === attackTarget && now - lastAttack > attackCD) {
                        const d = Math.hypot(moveTarget.x - me.x, moveTarget.y - me.y);
                        const data = enemyMap.get(moveTarget.uid);
                        const ai = enemyAIs.get(moveTarget.uid);
                        const eTimings2 = getWeaponTimings(gl(moveTarget));
                        let vulnerable = false;
                        if (data && data.attackStart) {
                            const elapsed = now - data.attackStart;
                            if (elapsed > eTimings2.windup && elapsed < eTimings2.windup + eTimings2.cooldown) {
                                vulnerable = true;
                            }
                        }
                        if (data && Math.hypot(data.vx||0, data.vy||0) > 5) vulnerable = true;
                        if (ai && ai.confidence > 0.5) {
                            const next = ai.predictNextAttack(now);
                            if (next && now > next + 100) vulnerable = true;
                        }
                        if (d < myReach + 20 && vulnerable) {
                            shouldAttack = true;
                            let predX = moveTarget.x + (data?.vx||0) * 12;
                            let predY = moveTarget.y + (data?.vy||0) * 12;
                            let angleOff = degrees;
                            if (moveTarget.width < 150 || gl(moveTarget) < 10) angleOff *= 0.3;
                            const angle = Math.atan2(predY - me.y, predX - me.x) + (angleOff * Math.PI/180);
                            let final = (angle * 180/Math.PI) % 360;
                            if (final < 0) final += 360;
                            ghostAngle = final;
                            angleLock = now + Math.min(150, attackCD * 0.3 + pingEstimate * 0.2);
                            if (d > 0) {
                                targetX = cx + ((moveTarget.x - me.x)/d) * 35;
                                targetY = cy + ((moveTarget.y - me.y)/d) * 35;
                            }
                        }
                    }
                }
            }

            if (C.autoAttack && attackTarget && !C.eraBot && now - lastAttack > attackCD) {
                const d = Math.hypot(attackTarget.x - me.x, attackTarget.y - me.y);
                const data = enemyMap.get(attackTarget.uid);
                const ai = enemyAIs.get(attackTarget.uid);
                const eTimings2 = getWeaponTimings(gl(attackTarget));
                let vulnerable = false;
                if (data && data.attackStart) {
                    const elapsed = now - data.attackStart;
                    if (elapsed > eTimings2.windup && elapsed < eTimings2.windup + eTimings2.cooldown) {
                        vulnerable = true;
                    }
                }
                if (data && Math.hypot(data.vx||0, data.vy||0) > 5) vulnerable = true;
                if (ai && ai.confidence > 0.5) {
                    const next = ai.predictNextAttack(now);
                    if (next && now > next + 100) vulnerable = true;
                }
                if (d < myReach + 20 && (vulnerable || d < myReach * 0.5)) {
                    shouldAttack = true;
                    let predX = attackTarget.x + (data?.vx||0) * 12;
                    let predY = attackTarget.y + (data?.vy||0) * 12;
                    let angleOff = degrees;
                    if (attackTarget.width < 150 || gl(attackTarget) < 10) angleOff *= 0.3;
                    const angle = Math.atan2(predY - me.y, predX - me.x) + (angleOff * Math.PI/180);
                    let final = (angle * 180/Math.PI) % 360;
                    if (final < 0) final += 360;
                    ghostAngle = final;
                    angleLock = now + Math.min(150, attackCD * 0.3 + pingEstimate * 0.2);
                    if (d > 0) {
                        targetX = cx + ((attackTarget.x - me.x)/d) * 35;
                        targetY = cy + ((attackTarget.y - me.y)/d) * 35;
                    }
                }
            }

            if (mi) {
                try { mi.mouseXcanvas = targetX; mi.mouseYcanvas = targetY; } catch(e) {}
            }

            if (C.eraBot) {
                const sprintNeeded = move.sprint || (attackTarget && dist(me, attackTarget) > myReach + 100);
                setSprint(sprintNeeded);
            } else {
                setSprint(false);
            }

            if (shouldRight) {
                if (!isRightDown) { rightClick(true, targetX, targetY); isRightDown = true; }
            } else if (isRightDown) {
                rightClick(false, targetX, targetY); isRightDown = false;
            }

            if (shouldAttack && now - lastAttack > 50) {
                clickAt(targetX, targetY);
                lastAttack = now;
            }

        } catch(e) {
            console.warn('[EraBOT] Loop error:', e);
        }
        requestAnimationFrame(mainLoop);
    }

    // =============================================================
    // 20. HOOK GAME (robust)
    // =============================================================
    let hookAttempts = 0;

    function hookGame() {
        try {
            cv = rt.canvas;
            if (!cv) { hookAttempts++; if (hookAttempts < 5) setTimeout(hookGame, 1000); return; }

            hookWebSocket();
            startDOMFallback();

            try {
                if (window.cr?.plugins_?.Mouse) {
                    const proto = window.cr.plugins_.Mouse.prototype.Instance.prototype;
                    const orig = proto.onMouseMove;
                    proto.onMouseMove = function() {
                        mi = this;
                        orig.apply(this, arguments);
                    };
                }
            } catch(e) {}

            try {
                if (window.cr?.plugins_?.NSG_PowerWS) {
                    const proto = window.cr.plugins_.NSG_PowerWS.prototype.Instance.prototype;
                    const orig = proto.tick;
                    proto.tick = function() {
                        if (this.wsWorker && !this.wsWorker._hooked) {
                            this.wsWorker._hooked = true;
                            const origPost = this.wsWorker.postMessage;
                            this.wsWorker.postMessage = function(msg) {
                                try {
                                    if (msg?.action === 'send' && msg?.data?.a === 'ps' && ghostAngle !== null) {
                                        msg.data.d.a = Math.round(ghostAngle);
                                    }
                                } catch(e) {}
                                return origPost.apply(this, arguments);
                            };
                        }
                        orig.apply(this, arguments);
                    };
                }
            } catch(e) {}

            // Tìm player type robust
            let found = false;
            try {
                for (const type of rt.types_by_index) {
                    if (!type || !type.instances || type.instances.length === 0) continue;
                    const inst = type.instances[0];
                    if (inst && typeof inst.width === 'number' && typeof inst.height === 'number' && typeof inst.angle === 'number') {
                        // Kiểm tra instance_vars (có thể ở type hoặc trong prototype)
                        if (type.instvar_sids && type.instvar_sids.length >= 12) {
                            if (inst.width > 20 && inst.width < 100 && inst.collision_poly) {
                                pt = type;
                                found = true;
                                break;
                            }
                        }
                    }
                }
                if (!found) {
                    // Fallback: tìm type có nhiều instance nhất và có collision_poly
                    let maxCount = 0;
                    for (const type of rt.types_by_index) {
                        if (!type || !type.instances) continue;
                        const count = type.instances.length;
                        if (count > maxCount && type.instances.length > 0) {
                            const inst = type.instances[0];
                            if (inst && inst.collision_poly && inst.width > 20 && inst.width < 100) {
                                maxCount = count;
                                pt = type;
                                found = true;
                            }
                        }
                    }
                }
            } catch(e) {}

            if (!pt) {
                hookAttempts++;
                if (hookAttempts < 5) { setTimeout(hookGame, 1000); return; }
                else { toast('⚠️ Không tìm thấy player type', 'warning'); }
            }

            // Áp dụng zoom lần đầu
            try {
                for (const layer of rt.running_layout.layers) {
                    if (layer) layer.scale = C.zoom;
                }
            } catch(e) {}

            toast('✅ Đã kết nối game!', 'success', 2000);
            isRunning = true;
            isGameActive = true;
            mainLoop();

        } catch(e) {
            console.warn('[EraBOT] Hook error:', e);
            hookAttempts++;
            if (hookAttempts < 5) setTimeout(hookGame, 1000);
        }
    }

    // =============================================================
    // 21. FIND RUNTIME (với MutationObserver fallback)
    // =============================================================
    let runtimeObserver = null;

    function findRuntime() {
        let attempts = 0;
        const iv = setInterval(() => {
            attempts++;
            try {
                if (window.cr_getC2Runtime) {
                    const r = window.cr_getC2Runtime();
                    if (r) {
                        rt = r;
                        clearInterval(iv);
                        if (runtimeObserver) runtimeObserver.disconnect();
                        hookGame();
                        return;
                    }
                }
            } catch(e) {}
            if (attempts >= 30) {
                clearInterval(iv);
                // Fallback: dùng MutationObserver để phát hiện canvas
                if (!runtimeObserver) {
                    runtimeObserver = new MutationObserver(() => {
                        const canvas = document.querySelector('canvas');
                        if (canvas && canvas.width > 100 && canvas.height > 100) {
                            // Thử tìm runtime lần nữa
                            try {
                                if (window.cr_getC2Runtime) {
                                    const r = window.cr_getC2Runtime();
                                    if (r) {
                                        rt = r;
                                        runtimeObserver.disconnect();
                                        hookGame();
                                        return;
                                    }
                                }
                            } catch(e) {}
                        }
                    });
                    runtimeObserver.observe(document.body, { childList: true, subtree: true });
                }
                toast('⚠️ Không tìm thấy game, refresh F5', 'warning', 3000);
            }
        }, 500);
    }

    // =============================================================
    // 22. ACTIVATE
    // =============================================================
    function activate() {
        loadConfig();
        injectCSS();

        toggleBtn = document.createElement('div');
        toggleBtn.id = 'erabot-toggle';
        toggleBtn.textContent = '🕺';
        toggleBtn.style.display = 'flex';
        document.body.appendChild(toggleBtn);

        // MutationObserver để giữ toggle button
        const observer = new MutationObserver(() => {
            if (!document.getElementById('erabot-toggle')) {
                const btn = document.createElement('div');
                btn.id = 'erabot-toggle';
                btn.textContent = '🕺';
                btn.style.display = 'flex';
                btn.style.cssText = toggleBtn.style.cssText;
                document.body.appendChild(btn);
                btn.addEventListener('click', toggleMenu);
                toggleBtn = btn;
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });

        const wm = document.createElement('div');
        wm.id = 'erabot-watermark';
        wm.textContent = 'EraBOT v18.0.0';
        document.body.appendChild(wm);

        buildMenu();
        createOverlay();

        if (C.autoRejoin) startRejoin();
        if (C.autoDuel) startDuel();

        findRuntime();

        document.addEventListener('mousemove', (e) => {
            if (e.isTrusted) { mx = e.clientX; my = e.clientY; }
        });
        document.addEventListener('contextmenu', (e) => {
            if (e.target === cv) e.preventDefault();
        });

        // Auto-fix UI
        setInterval(() => {
            try {
                if (!toggleBtn || !document.body.contains(toggleBtn)) {
                    const btn = document.createElement('div');
                    btn.id = 'erabot-toggle';
                    btn.textContent = '🕺';
                    btn.style.display = 'flex';
                    document.body.appendChild(btn);
                    toggleBtn = document.getElementById('erabot-toggle');
                    if (toggleBtn) toggleBtn.addEventListener('click', toggleMenu);
                }
                if (!menu || !document.body.contains(menu)) {
                    buildMenu();
                    menu = document.getElementById('erabot-menu');
                }
                if (!overlay || !document.body.contains(overlay)) {
                    createOverlay();
                }
            } catch(e) {}
        }, 5000);

        toast('🕺 EraBOT v18.0.0 ULTIMATE STABLE đã sẵn sàng!', 'success', 3000);
    }

    // =============================================================
    // 23. START
    // =============================================================
    activate();
    console.log('⚔️EraBOT🕺 v18.0.0 - ULTIMATE STABLE (ALL FIXES)');
})();