blackhack

Cheat for brofist.io

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name           blackhack
// @version        20.0
// @description    Cheat for brofist.io
// @author         CiNoP
// @match          *://*.brofist.io/*

// @require        https://update.greasyfork.org/scripts/571913/1787411/blackhack-utils.js
// @require        https://update.greasyfork.org/scripts/571914/1789017/blackhack-map.js
// @require        https://update.greasyfork.org/scripts/571915/1788773/blackhack-ui.js
// @require

// @icon           https://www.google.com/s2/favicons?sz=64&domain=brofist.io
// @grant          none
// @license        GPL-3.0-only
// @namespace      brofist.io 1st-cheat (FOR ALL MODES)
// ==/UserScript==
/* jshint esversion: 11 */
/* jshint asi: true */

(() => {
     'use strict';
    const BH = window.BH;

    console.log(`utilsVer:, ${BH.utilsVer.toFixed(2)}, uiVer: ${BH.uiVer.toFixed(2)}, mapVer: ${BH.mapVer.toFixed(2)}, xrayVer: BH.xrayVer.toFixed(2)}`)

    const _bind = Function.prototype.bind;
    const pathname = location.pathname.toLowerCase();
    const isTwoPlayer = pathname.includes("twoplayer");

	window.hack = {
		mode: null, modeController: null, gp: null, networkHandler: null, client: null,
		originalMakeMeGhost: null, originalSocketEmit: null,
		_isMapReloading: false, _mapSocket: null
	};

	const bm = { processOthers: false, movement: false, _allFound: false, _origPO: null, _origMV: null };
	Function.prototype.bind = function (ctx, ...args) {
		const b = _bind.apply(this, [ctx, ...args]);
		if (!bm._allFound) {
			try {
				const s = this.toString();
				if (!bm.processOthers && s.includes('othersPlayerNetworkData') && s.includes('oldPlayersIndex'))
					{ b.__hk_processOthers = true; bm.processOthers = true; bm._origPO = this; }
				if (!bm.movement && s.includes('moveLeft') && (s.includes('raycast') || s.includes('velocity[1]')))
					{ b.__hk_movement = true; bm.movement = true; bm._origMV = this; }
				if (bm.processOthers && bm.movement) bm._allFound = true;
			} catch (_) {}
		} else {
			if (this === bm._origPO) b.__hk_processOthers = true;
			if (this === bm._origMV) b.__hk_movement = true;
		}
		if (isTwoPlayer && !window.hack.mode && ctx && ctx.playerData && ctx.twoPlayerController) {
			window.hack.mode = ctx; window.hack.modeController = ctx.twoPlayerController;
			window.hack.gp = ctx.gp; window.hack.networkHandler = ctx.networkHandler;
			window.hack.client = ctx.client;
		}
		return b;
	};

    const hackReady = new Promise(resolve => {
        if (isTwoPlayer) {
            const c = () => { if (window.hack.mode) resolve(window.hack); else requestAnimationFrame(c); };
            requestAnimationFrame(c);
        } else {
            const traps = { pingCheckCount: ['networkHandler'], othersPlayerNetworkData: ['mode', 'modeController'] };
            let cap = 0; const tot = Object.keys(traps).length;
            for (const [p, tgts] of Object.entries(traps))
                Object.defineProperty(Object.prototype, p, {
                    set(v) {
                        delete Object.prototype[p]; this[p] = v;
                        for (const t of tgts) window.hack[t] = this;
                        if (++cap >= tot) { window.hack.gp = window.gp; window.hack.client = window.client; resolve(window.hack); }
                    }, configurable: true
                });
        }
    });

    // Запускаем main только когда ОБА готовы
    Promise.all([hackReady, BH.lzmaReady]).then(() => main(isTwoPlayer));

	function main(is2pa) {
		const hack = window.hack;
		Object.defineProperty(hack.client, 'runPhysics', { get: () => true, set: () => {} });
		const st = document.createElement('style'); st.textContent = '#startTime{display:none!important}'; document.head.appendChild(st);

		const getLP = () => is2pa ? hack.modeController.localPlayer : hack.modeController.player?.gpData ?? null;
		const getGPD = () => is2pa ? hack.mode.player?.gpData ?? null : getLP();
		const diff = (a, b) => a.filter(x => !b.includes(x));

		// ─── Socket ───
		function patchSocket(sock) {
			if (!sock || sock.__patched) return; sock.__patched = true;
			hack.originalSocketEmit = sock.emit;
			sock.emit = function (ev, ...a) { if (ev === "rGho" && hack.vars.imm) return; return hack.originalSocketEmit.apply(this, [ev, ...a]); };
			const oo = sock.onevent;
			sock.onevent = function (pkt) {
				if (pkt?.data?.[0] === 'changeMap') {
					const raw = pkt.data[1];
					if (!hack._isMapReloading) hack.vars.mapDataBackup = Array.isArray(raw) ? raw.slice() : raw;

                    if (hack.vars.xrayGates) {
                        hack.vars.xrayGates.forEach(item => {
                            if (item.gfx && item.gfx.parent) item.gfx.parent.removeChild(item.gfx);
                            if (item.gfx && typeof item.gfx.destroy === 'function') item.gfx.destroy();
                        });
                        hack.vars.xrayGates = [];
                    }

					try {
						let d = raw;
						if (typeof d === 'string') d = JSON.parse(d);
						if (Array.isArray(d) && typeof d[0] === 'number' && window.LZMA) d = JSON.parse(window.LZMA.decompress(d));
						window.mapData = d;
					} catch (_) { window.mapData = raw; }
					if (hack.vars.layoutMode) {
						try {
							const cl = Array.isArray(raw) ? raw.slice() : raw;
							const clean = BH.parseMapToLayout(cl);
							const j = JSON.stringify(clean);
							pkt.data[1] = window.LZMA?.compress ? LZMA.compress(j, 1) : j;
						} catch (_) {}
					}
				}
				return oo.call(this, pkt);
			};
			hack._mapSocket = sock;
		}

		function reloadMap() {
			if (!hack.vars.mapDataBackup || !hack._mapSocket?.onevent) return false;
			hack._isMapReloading = true;
			const bk = Array.isArray(hack.vars.mapDataBackup) ? hack.vars.mapDataBackup.slice() : hack.vars.mapDataBackup;
			hack._mapSocket.onevent({ type: 2, data: ['changeMap', bk] });
			hack._isMapReloading = false; return true;
		}

		// ─── Physics helpers ───
		function applyNoclipState(en) {
			const lp = getLP();
			if (lp?.p) { lp.p.collisionResponse = !en; lp.p.massMultiplier[1] = en ? 0 : hack.vars.playerMass; }
		}
		function applyGravNoclipState(en) {
			const g = getGPD(), lp = getLP();
			if (lp?.p) { lp.p.collisionResponse = !en; lp.p.gravityScale = en ? 0 : 1; if (!en) lp.p.massMultiplier[1] = hack.vars.playerMass; }
			if (g?.p) g.p.gravityScale = en ? 0 : 1;
			if (!en) { g?.setAngle(0); if (lp && lp !== g) lp.setAngle(0); }
		}

		if (hack.networkHandler) {
			patchSocket(hack.networkHandler.gsSocket);
			const oc = hack.networkHandler.connectToGs;
			if (oc) hack.networkHandler.connectToGs = function (...a) { oc.apply(this, a); patchSocket(this.gsSocket); };
		}

		// ─── Settings & state ───
		const cfg = BH.loadSettings();
		hack.vars = {
			imm: false, noclip: false, gravNoclip: false,
			mult: { enabled: false, value: 1, uiValue: cfg.mult },
			gravNoclipGravScale: cfg.gravScale, jumpHeight: cfg.jumpHeight,
			playerMass: cfg.mass, playerDamping: cfg.damping,
			blacklisted: { names: [], data: [] },
			shiftUsedForBlacklist: false, arrowFirstPressed: null,
			layoutMode: false, mapDataBackup: null, layoutSavedPos: null,
			layoutAlpha: { collision: cfg.alphaCollision, poison: cfg.alphaPoison, functional: cfg.alphaFunctional, background: 0xCCCCCC },
			numpadMode: false,
            xrayGates: []
		};

		// ─── Keys ───
		hack.keyBinds = { F1:false,F2:false,F3:false,F5:false,SHIFT:false,HOME:false,END:false,ARROW_UP:false,ARROW_DOWN:false,NUMPAD_MINUS:false,DIGIT_1:false,DIGIT_2:false,DIGIT_3:false,DIGIT_5:false };
		hack.prevKeys = {};
		const keyMap = { F1:'F1',F2:'F2',F3:'F3',F5:'F5',Shift:'SHIFT',Home:'HOME',End:'END',ArrowUp:'ARROW_UP',ArrowDown:'ARROW_DOWN' };
		const codeMap = { NumpadSubtract:'NUMPAD_MINUS',Digit1:'DIGIT_1',Numpad1:'DIGIT_1',Digit2:'DIGIT_2',Numpad2:'DIGIT_2',Digit3:'DIGIT_3',Numpad3:'DIGIT_3',Digit5:'DIGIT_5',Numpad5:'DIGIT_5' };

		document.addEventListener('keydown', e => {
			const cm = codeMap[e.code];
			if (cm) { if (cm.startsWith('DIGIT_') && hack.vars.numpadMode) { const t = (e.target.tagName || '').toLowerCase(); if (t !== 'input' && t !== 'textarea' && !e.target.isContentEditable) e.preventDefault(); } if (cm === 'NUMPAD_MINUS') e.preventDefault(); hack.keyBinds[cm] = true; }
			const m = keyMap[e.key]; if (!m) return; e.preventDefault();
			if (m === 'ARROW_UP' && !hack.keyBinds.ARROW_UP) hack.vars.arrowFirstPressed = 'up';
			if (m === 'ARROW_DOWN' && !hack.keyBinds.ARROW_DOWN) hack.vars.arrowFirstPressed = 'down';
			hack.keyBinds[m] = true;
		});
		document.addEventListener('keyup', e => {
			const cm = codeMap[e.code]; if (cm) hack.keyBinds[cm] = false;
			const m = keyMap[e.key]; if (!m) return; hack.keyBinds[m] = false;
			if (m === 'ARROW_UP' || m === 'ARROW_DOWN') {
				if (!hack.keyBinds.ARROW_UP && !hack.keyBinds.ARROW_DOWN) hack.vars.arrowFirstPressed = null;
				else hack.vars.arrowFirstPressed = hack.keyBinds.ARROW_UP ? 'up' : 'down';
			}
		});
		window.addEventListener('blur', () => { for (const k in hack.keyBinds) hack.keyBinds[k] = false; hack.vars.arrowFirstPressed = null; });

		// ─── Blacklist ───
		function getIdxByName(n) { const r = [], o = hack.mode.otherPlayers; for (let i = 0; i < o.length; i++) if (o[i]?.myName === n) r.push(i); return r; }
		function isPlayerOnline(n) { const o = hack.mode.otherPlayers; if (!o) return false; for (let i = 0; i < o.length; i++) if (o[i]?.myName === n) return true; return false; }
		function processBL() {
			const bl = hack.vars.blacklisted;
			for (const n of bl.names) for (const i of getIdxByName(n)) if (!bl.data.some(e => e?.[0] === i && e?.[1] === n)) bl.data.push([i, n]);
			if (bl.data.length) bl.data = bl.data.filter(e => e?.[1] && bl.names.includes(e[1]));
		}
		function isIdxBL(i) { return hack.vars.blacklisted.data.some(e => e?.[0] === i); }
		function enforceBL() {
			const bl = hack.vars.blacklisted; if (!bl.data.length) return;
			const o = hack.mode.otherPlayers;
			for (const [i] of bl.data) { const p = o[i]; if (!p?.gpData) continue; p.gpData.g.visible = false; if (p.gpData.p) p.gpData.p.collisionResponse = false; }
			const msgs = hack.mode.othersPlayerMsgNetworkData;
			if (msgs) { const s = new Set(bl.data.map(e => e[0])); for (let i = 0; i < msgs.length; i++) msgs[i] = msgs[i].filter(m => !s.has(m[0])); }
		}

		// ─── Custom processOthers ───
		const customPO = function () {
			const mode = hack.mode, gp = hack.gp, nh = hack.networkHandler;
			processBL(); if (!mode.othersPlayerNetworkData.length) return;
			var cur = [];
			for (var x = 0; x < mode.othersPlayerNetworkData.length; x++) {
				for (var y = 0; y < mode.othersPlayerNetworkData[x].length; y++) {
					var d = mode.othersPlayerNetworkData[x][y], idx = d[0], xV = d[1], yV = d[2], xA = d[3], yA = d[4];
					if (isIdxBL(idx)) continue;
					if (mode.otherPlayers[idx] == null) {
						mode.oldPlayersIndex.push(idx);
						var p = {}; p.myName = ""; p.mySkin = 0; p.reset = Infinity;
						p.gpData = is2pa ? mode.basePlayer.createPlayer(mode.playerData) : mode.createPlayer();
						p.gpData.g.myIndex = idx; mode.otherPlayers[idx] = p;
						if (nh?.gsSocket) nh.gsSocket.emit("rBio", idx);
						gp.gWorld.removeChild(p.gpData.g); gp.gWorld.mid.addChild(p.gpData.g);
					}
					if (10 < mode.otherPlayers[idx].reset) { mode.otherPlayers[idx].gpData.setX(xA); mode.otherPlayers[idx].gpData.setY(yA); mode.otherPlayers[idx].reset = 0; }
					mode.otherPlayers[idx].reset++;
					mode.otherPlayers[idx].gpData.p.velocity[0] = xV;
					mode.otherPlayers[idx].gpData.p.velocity[1] = yV;
					cur.push(idx);
				}
			}
			var r = diff(mode.oldPlayersIndex, cur);
			for (var i = 0; i < r.length; i++) { var p = mode.otherPlayers[r[i]]; if (p != null) { gp.gWorld.mid.removeChild(p.gpData.g); gp.pWorld.removeBody(p.gpData.p); gp.list[gp.list.indexOf(p.gpData)] = null; gp.deleteCounter++; mode.otherPlayers[r[i]] = null; } }
			mode.oldPlayersIndex = cur; mode.othersPlayerNetworkData = [];
		};

		// ─── Custom movement ───
		function getEV() {
			const u = hack.keyBinds.ARROW_UP, d = hack.keyBinds.ARROW_DOWN;
			if (u && d) { if (hack.vars.arrowFirstPressed === 'up') return 'down'; if (hack.vars.arrowFirstPressed === 'down') return 'up'; return null; }
			return u ? 'up' : d ? 'down' : null;
		}

		const customMV = function () {
			const ctrl = hack.modeController, p = getLP();
			const mult = hack.vars.mult.value, jh = hack.vars.jumpHeight, gs = hack.vars.gravNoclipGravScale;
			if (!p || !ctrl) return;

			if (hack.vars.noclip) {
				p.p.velocity[0] = ctrl.moveRight ? 3 * mult : ctrl.moveLeft ? -3 * mult : 0;
				p.p.velocity[1] = ctrl.moveUp ? 3 * mult : ctrl.moveDown ? -3 * mult : 0;
				return;
			}

			if (hack.vars.gravNoclip) {
				p.p.velocity[0] = ctrl.moveRight ? 3 * mult : ctrl.moveLeft ? -3 * mult : 0;
				const v = getEV();
				if (v === 'up') { p.p.gravityScale = -gs; p.setAngle(180); }
				else if (v === 'down') { p.p.gravityScale = gs; p.setAngle(0); }
				else p.p.gravityScale = 0;
				const gd = p.p.gravityScale;
				let sj = false, jd = 0, rsY, reY;
				const ex = p.getX(), ty = p.getY(), hH = 50, rL = 50, n = ex - 15, r = 30 / 11;
				if (gd > 0 && ctrl.moveUp) { sj = true; jd = jh; rsY = ty + (hH - 1); reY = rsY + rL; }
				else if (gd < 0 && ctrl.moveDown) { sj = true; jd = -jh; rsY = ty - (hH - 1); reY = rsY - rL; }
				if (sj) {
					const ph = is2pa ? ctrl.physics : hack.gp;
					for (let i = 0; i < 12; i++) {
						const ox = n + i * r;
						p.ray.from = [ph.xAxis(ox, 0), ph.yAxis(rsY, 0)]; p.ray.to = [ph.xAxis(ox, 0), ph.yAxis(reY, 0)];
						p.ray.update(); p.ray.result.reset(); p.ray.hitPoint = [Infinity, Infinity];
						if (hack.gp.pWorld.raycast(p.ray.result, p.ray))
							if (p.ray.result.shape.ref.getCollision() && p.ray.result.getHitDistance(p.ray) < 0.15) { p.p.velocity[1] = jd; break; }
					}
				}
				return;
			}

			if (ctrl.moveRight) p.p.velocity[0] = 3;
			if (ctrl.moveLeft) p.p.velocity[0] = -3;
			if (!ctrl.moveUp) return;
			const ex = p.getX(), ty = p.getY(), n = ex - 15, r = 30 / 11;
			const ph = is2pa ? ctrl.physics : hack.gp;
			for (let i = 0; i < 12; i++) {
				const ox = n + i * r, oy1 = ty + 49, oy2 = oy1 + 50;
				p.ray.from = [ph.xAxis(ox, 0), ph.yAxis(oy1, 0)]; p.ray.to = [ph.xAxis(ox, 0), ph.yAxis(oy2, 0)];
				p.ray.update(); p.ray.result.reset(); p.ray.hitPoint = [Infinity, Infinity];
				if (hack.gp.pWorld.raycast(p.ray.result, p.ray))
					if (p.ray.result.shape.ref.getCollision() && p.ray.result.getHitDistance(p.ray) < 0.05) { p.p.velocity[1] = jh; break; }
			}
		};

		// ─── Inject into loop ───
		let poI = false, mvI = false;
		function cih() {
			const lf = hack.client.loopFunctions;
			if (!poI) { const i = lf.findIndex(e => e?.fun?.__hk_processOthers || (e?.fun?.toString().includes('othersPlayerNetworkData') && e.fun.toString().includes('oldPlayersIndex'))); if (i !== -1) { lf[i].fun = customPO; poI = true; } }
			if (!mvI) { const i = lf.findIndex(e => e?.fun?.__hk_movement || (e?.fun?.toString().includes('moveLeft') && (e.fun.toString().includes('raycast') || e.fun.toString().includes('velocity[1]')))); if (i !== -1) { lf[i].fun = customMV; mvI = true; } else if (is2pa && lf.length > 7) { lf[7].fun = customMV; mvI = true; } }
			if (!poI || !mvI) requestAnimationFrame(cih);
		}
		cih();

		const oal = hack.client.addLoopFunction;
		hack.client.addLoopFunction = function (f, p, t, e) {
			if (f) {
				if (f.__hk_processOthers) return oal.call(this, customPO, p, t, e);
				if (f.__hk_movement) return oal.call(this, customMV, p, t, e);
				const s = f.toString();
				if (s.includes('othersPlayerNetworkData') && s.includes('oldPlayersIndex')) return oal.call(this, customPO, p, t, e);
				if (s.includes('moveLeft') && (s.includes('raycast') || s.includes('velocity[1]'))) return oal.call(this, customMV, p, t, e);
			}
			return oal.call(this, f, p, t, e);
		};

		// ─── KickGui patch ───
		if (hack.mode.createKickGui) {
			const _o = hack.mode.createKickGui;
			hack.mode.createKickGui = function (e) {
				const _on = e.on.bind(e);
				e.on = function (ev, fn) {
					return _on(ev, function (...a) {
						if (hack.keyBinds.SHIFT) {
							const nm = this.ref.refP.name.getText();
							if (!hack.vars.blacklisted.names.includes(nm)) {
								hack.vars.blacklisted.names.push(nm);
								hack.vars.blacklisted.data.push([this.ref.refP.g.myIndex, nm]);
								this.ref.refP.g.visible = false;
								if (this.ref.refP.p) this.ref.refP.p.collisionResponse = false;
							}
							hack.vars.shiftUsedForBlacklist = true;
							if (hack._refreshBL) hack._refreshBL();
							return;
						}
						return fn.apply(this, a);
					});
				};
				_o.call(this, e); e.on = _on;
			};
		}

		// ─── Restore after map reload ───
		function restoreAfterReload() {
			let f = 0;
			const r = () => {
				if (++f < 3) { requestAnimationFrame(r); return; }
				if (hack.vars.noclip) applyNoclipState(true);
				if (hack.vars.gravNoclip) applyGravNoclipState(true);
				if (hack.vars.imm) { const g = getGPD(); if (g) g.me = void 0; }
				const pos = hack.vars.layoutSavedPos;
				if (pos) { const lp = getLP(); if (lp) { lp.setX(pos.x); lp.setY(pos.y); if (lp.p) { lp.p.velocity[0] = 0; lp.p.velocity[1] = 0; } } }
			};
			requestAnimationFrame(r);
		}

		// ─── Handlers ───
		hack.funcs = { handlers: {
			toggleImm: en => {
				hack.vars.imm = en; const g = getGPD(); if (g) g.me = en ? void 0 : true;
				if (en) { if (hack.mode.makeMeGhost && !hack.originalMakeMeGhost) hack.originalMakeMeGhost = hack.mode.makeMeGhost; if (hack.mode.makeMeGhost) hack.mode.makeMeGhost = function () {}; }
				else if (hack.originalMakeMeGhost) hack.mode.makeMeGhost = hack.originalMakeMeGhost;
			},
			toggleNoclip: en => {
				hack.vars.noclip = en; applyNoclipState(en);
				if (hack.vars.mult.enabled) hack.vars.mult.value = en ? hack.vars.mult.uiValue : 1;
				if (en && hack.vars.gravNoclip) { hack.vars.gravNoclip = false; applyGravNoclipState(false); }
			},
			toggleGravNoclip: en => {
				if (en && hack.vars.noclip) hack.funcs.handlers.toggleNoclip(false);
				hack.vars.gravNoclip = en; applyGravNoclipState(en);
				if (hack.vars.mult.enabled) hack.vars.mult.value = en ? hack.vars.mult.uiValue : 1;
			},
			toggleMult: en => {
				if (!hack.vars.noclip && !hack.vars.gravNoclip) return;
				hack.vars.mult.enabled = en; hack.vars.mult.value = en ? hack.vars.mult.uiValue : 1;
			},
			toggleLayout: en => {
				hack.vars.layoutMode = en;
				const lp = getLP(); if (lp) hack.vars.layoutSavedPos = { x: lp.getX(), y: lp.getY() };
				if (reloadMap()) restoreAfterReload();
				try {
					const r = hack.gp.renderer || hack.gp.app?.renderer;
					if (r) { if (en) { if (hack._origBg === undefined) hack._origBg = r.backgroundColor; r.backgroundColor = hack.vars.layoutAlpha.background; } else if (hack._origBg !== undefined) r.backgroundColor = hack._origBg; }
				} catch (_) {}
			},
			tpPlayer: (x, y) => { const lp = getLP(); if (lp) { lp.setX(x); lp.setY(y); } },
			toggleNumpadMode: en => { hack.vars.numpadMode = en; },
			blacklistAdd: n => {
				n = (n || '').trim(); if (!n || hack.vars.blacklisted.names.includes(n)) return;
				hack.vars.blacklisted.names.push(n);
				for (const i of getIdxByName(n)) { hack.vars.blacklisted.data.push([i, n]); const p = hack.mode.otherPlayers[i]; if (p?.gpData) { p.gpData.g.visible = false; if (p.gpData.p) p.gpData.p.collisionResponse = false; } }
				if (hack._refreshBL) hack._refreshBL();
			},
			blacklistRemove: n => {
				const ni = hack.vars.blacklisted.names.indexOf(n); if (ni === -1) return;
				hack.vars.blacklisted.names.splice(ni, 1);
				for (const [i] of hack.vars.blacklisted.data.filter(e => e?.[1] === n)) { const p = hack.mode.otherPlayers[i]; if (p?.gpData) { p.gpData.g.visible = true; if (p.gpData.p) p.gpData.p.collisionResponse = true; } }
				hack.vars.blacklisted.data = hack.vars.blacklisted.data.filter(e => e[1] !== n);
				if (hack._refreshBL) hack._refreshBL();
			}
		}};
		window.hackFunctions = hack.funcs.handlers;

		// ─── Expose for UI ───
		hack.getLP = getLP;
		hack.reloadMap = reloadMap;
		hack.restoreAfterReload = restoreAfterReload;
		hack.isPlayerOnline = isPlayerOnline;

		// ─── Update loop ───
		function updateLoop() {
			const binds = hack.keyBinds, prev = hack.prevKeys, fn = hack.funcs.handlers, v = hack.vars;
			const jp = k => binds[k] && !prev[k], jr = k => !binds[k] && prev[k];

			if (jp('NUMPAD_MINUS')) fn.toggleNumpadMode(!v.numpadMode);
			if (is2pa) {
				if (jp('HOME')) { const s = hack.gp.list.find(i => i?.id?.includes('spawn')); if (s) fn.tpPlayer(s.getX(), s.getY()); }
				if (jp('END')) { const d = hack.gp.list.find(i => i?.id?.includes('door')); if (d) fn.tpPlayer(d.getX(), d.getY()); }
			}
			if (jp('F1') || (v.numpadMode && jp('DIGIT_1'))) fn.toggleNoclip(!v.noclip);
			if (jp('F2') || (v.numpadMode && jp('DIGIT_2'))) fn.toggleImm(!v.imm);
			if (jp('F3') || (v.numpadMode && jp('DIGIT_3'))) fn.toggleGravNoclip(!v.gravNoclip);
			if (jp('F5') || (v.numpadMode && jp('DIGIT_5'))) fn.toggleLayout(!v.layoutMode);
			if (jr('SHIFT')) { if (v.shiftUsedForBlacklist) v.shiftUsedForBlacklist = false; else fn.toggleMult(!v.mult.enabled); }

			if (v.noclip) applyNoclipState(true);
			if (v.gravNoclip) { const lp = getLP(); if (lp?.p) lp.p.collisionResponse = false; }
			if (is2pa && v.imm) { const g = getGPD(); if (g?.me !== undefined) g.me = void 0; }
			if (!v.noclip && !v.gravNoclip) { const lp = getLP(); if (lp?.p) { lp.p.massMultiplier[1] = v.playerMass; lp.p.damping = v.playerDamping; } }
			enforceBL();

			const dNc = document.getElementById('hk-d-nc'), dIm = document.getElementById('hk-d-im');
			const dGn = document.getElementById('hk-d-gn'), dLm = document.getElementById('hk-d-lm');
			if (dNc) dNc.style.background = v.noclip ? '#0f0' : '#f44';
			if (dIm) dIm.style.background = v.imm ? '#0f0' : '#f44';
			if (dGn) dGn.style.background = v.gravNoclip ? '#0f0' : '#f44';
			if (dLm) dLm.style.background = v.layoutMode ? '#0f0' : '#f44';




            try {
                if (v.layoutMode) {
                    // 1. Инициализация рамки ворот
                    if (v.xrayGates.length === 0 && hack.gp && hack.gp.list) {
                        const pixi = window.pixi || window.PIXI;
                        if (pixi && pixi.Graphics) {
                            hack.gp.list.forEach(obj => {
                                if (obj && obj.id && obj.id.toLowerCase().startsWith('gate')) {
                                    const parts = obj.id.split(':');
                                    const action = parts[parts.length - 1];

                                    obj.shapes.forEach((shape, idx) => {
                                        // Пропускаем текст (make === 3)
                                        if (shape && shape.make !== 3 && shape.g && shape.g.parent) {
                                            const gfx = new pixi.Graphics();
                                            const color = action === "1" ? 0xFF00FF : 0x00FFFF;

                                            // === БЕЗОПАСНОЕ ИЗВЛЕЧЕНИЕ РАЗМЕРОВ ИЗ ДВИЖКА ===
                                            let baseW = 50, baseH = 50, baseR = 25;

                                            // Если у шейпа есть физика, берем размеры оттуда (умножаем на 100)
                                            if (shape.p) {
                                                if (shape.p.width) baseW = shape.p.width * 100;
                                                if (shape.p.height) baseH = shape.p.height * 100;
                                                if (shape.p.radius) baseR = shape.p.radius * 100;
                                            } else if (shape.g) {
                                                // Иначе вычисляем из графики, отменяя текущий scale
                                                const sx = shape.g.scale ? Math.abs(shape.g.scale.x) : 1;
                                                const sy = shape.g.scale ? Math.abs(shape.g.scale.y) : 1;
                                                if (shape.g.width) baseW = shape.g.width / sx;
                                                if (shape.g.height) baseH = shape.g.height / sy;
                                                baseR = baseW / 2;
                                            }

                                            // Тип: 2 = Круг, 1 = Квадрат, 5 = Полигон
                                            const type = typeof shape.getType === 'function' ? shape.getType() : shape.type;

                                            // Тип: 2 = Круг, 1 и 3 = Квадрат, 5 = Полигон (Треугольник)
                                            if (type === 2) {
                                                gfx.circle(0, 0, baseR);
                                            } else if (type === 5) {
                                                // В Brofist.io тип 5 — это всегда равносторонний треугольник
                                                const triH = baseW * Math.sqrt(3) / 2; // Высота равностороннего треугольника
                                                const rOff = triH / 6; // Смещение для центра масс

                                                // Рисуем по трем точкам (PIXI API)
                                                gfx.moveTo(-baseW / 2, triH / 2 - rOff); // Левый нижний угол
                                                gfx.lineTo(baseW / 2, triH / 2 - rOff);  // Правый нижний угол
                                                gfx.lineTo(0, -triH / 2 - rOff);         // Верхний "острый" угол
                                                gfx.closePath();
                                            } else {
                                                // Все остальные типы (1, 3) рисуем как прямоугольники
                                                gfx.rect(-baseW / 2, -baseH / 2, baseW, baseH);
                                            }

                                            gfx.fill({ color: color, alpha: 0.15 });
                                            gfx.stroke({ width: 3, color: color, alpha: 1 });

                                            // Добавляем в глобальный слой карты
                                            if (hack.gp.gWorld && hack.gp.gWorld.mid) {
                                                hack.gp.gWorld.mid.addChild(gfx);
                                            }

                                            // Добавляем текст с ID над первым элементом ловушки
                                            let textObj = null;
                                            if (idx === 0) {
                                                textObj = new pixi.Text(obj.id, {
                                                    fontFamily: 'monospace',
                                                    fontSize: 14,
                                                    fill: color,
                                                    stroke: 0x000000,
                                                    strokeThickness: 3,
                                                    fontWeight: 'bold'
                                                });
                                                textObj.anchor.set(0.5, 1);
                                                hack.gp.gWorld.mid.addChild(textObj);
                                            }

                                            v.xrayGates.push({
                                                gfx: gfx,
                                                src: shape,
                                                text: textObj,
                                                baseH: baseH,
                                                baseR: baseR,
                                                type: type
                                            });
                                        }
                                    });
                                }
                            });
                        }
                    }
// 2. Синхронизация
                    v.xrayGates.forEach(item => {
                        const { src, gfx, text, baseH, baseR, type } = item;
                        if (src && gfx) {
                            if (!src.g || !src.g.parent) {
                                gfx.visible = false;
                                if (text) text.visible = false;
                                return;
                            }

                            gfx.visible = true;

                            // Сбрасываем скейл в 1.
                            // Слой gWorld.mid сам масштабирует наш gfx под зум камеры.
                            gfx.scale.set(1);

                            // Конвертация координат
                            if (typeof src.getGlobalPosition === 'function') {
                                const pos = src.getGlobalPosition();
                                gfx.x = pos.x * 100;
                                gfx.y = pos.y * -100;
                                gfx.rotation = -pos.angle;
                            } else {
                                gfx.x = src.g.x;
                                gfx.y = src.g.y;
                                gfx.rotation = src.g.rotation || 0;
                            }

                            // === ЛОГИКА X-RAY ===
                            const hasCollision = typeof src.getCollision === 'function' ? src.getCollision() : src.collision;

                            if (!hasCollision) {
                                gfx.alpha = 1.0; // Открыто (исчезло в игре) -> Горит ярко
                                if (text) text.alpha = 1.0;
                            } else {
                                gfx.alpha = 0.15; // Закрыто (видно в игре) -> Тускло
                                if (text) text.alpha = 0.5;
                            }

                            // Приклеиваем текст над рамкой
                            if (text) {
                                text.visible = true;
                                text.x = gfx.x;
                                // Убрали умножение на absScaleY!
                                const objHeight = type === 2 ? baseR * 2 : baseH;
                                text.y = gfx.y - (objHeight / 2) - 5;
                                text.scale.set(1); // Чтобы текст не сплющило
                            }
                        }
                    });

                } else if (v.xrayGates.length > 0) {
                    v.xrayGates.forEach(item => {
                        if (item.gfx && item.gfx.parent) item.gfx.parent.removeChild(item.gfx);
                        if (item.gfx && typeof item.gfx.destroy === 'function') item.gfx.destroy();
                        if (item.text && item.text.parent) item.text.parent.removeChild(item.text);
                        if (item.text && typeof item.text.destroy === 'function') item.text.destroy();
                    });
                    v.xrayGates = [];
                }
            } catch (e) {
                console.error("[BH] Render loop error: ", e);
            }




			for (const k in binds) prev[k] = binds[k];
			requestAnimationFrame(updateLoop);
		}
		requestAnimationFrame(updateLoop);

		// ─── Speed fix ───
		const cSS = () => { const lf = hack.client.loopFunctions; for (let i = 0; i < lf.length; i++) if (lf[i] && (lf[i].timeOut === 200 || lf[i].timeOut === 150)) { lf[i].timeOut = 10; return; } requestAnimationFrame(cSS); };
		cSS();

		BH.createZoom();
		BH.createUI();
		console.log("blackhack v20.0 loaded");
	}
})();