// ==UserScript==
// @name Meeland Enhancement Suite
// @namespace meeland-script
// @version 5.3.0
// @match *://*/*
// @run-at document-end
// @license MIT
// @grant none
// @description Meeland.io cheat script with auto-lock, speed boost, infinite jump, teleportation & more! Works on CrazyGames, twoplayergames.org, meeland.io, iogames.onl, sprunki-game.io, gameflare.com and other sites hosting Meeland
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
const W = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
if (!W.pc?.app?.root) return;
const STORE_KEY = 'ml_wp';
const CFG_KEY = 'ml_cfg';
let ACCEL = 6;
let FLY_MIN_SPEED = 10;
let SPEED_CAP = 100;
let GRAVITY = -18;
let SPEED_DEFAULT = 7;
let flyActive = false;
const slots = new Array(10).fill(null);
let flyUp = false;
let flyDown = false;
let flyVelY = 0;
let prevTick = Date.now();
let sprinting = false;
let sprintSpeed = SPEED_DEFAULT;
let homePos = null;
let backPos = null;
let cuddleTarget = null;
let cuddling = false;
let accelEnabled = true;
let featFly = true;
let featSprint = true;
let featWaypoints = true;
let featCuddle = true;
let featCuddleFollow = true;
let featPets = true;
let featAutoLock = true;
let autoRefresh = true;
let refreshInterval = 10;
let kbListeningRow = null;
let petFilter = '';
let petAutoRefresh = true;
let petRefreshInterval = 1;
const KEYBINDS = {
fly: 'Space',
flyDown: 'KeyF',
setHome: 'KeyQ',
home: 'Backquote',
back: 'KeyZ',
cuddle: 'KeyJ',
settings: 'KeyM',
pets: 'KeyK',
};
const DEFAULT_KEYBINDS = { ...KEYBINDS };
function saveSettings() {
try {
localStorage.setItem(CFG_KEY, JSON.stringify({
ACCEL, SPEED_CAP, SPEED_DEFAULT, accelEnabled,
refreshInterval, autoRefresh,
featFly, featSprint, featWaypoints, featCuddle, featCuddleFollow, featPets, featAutoLock,
keybinds: { ...KEYBINDS },
petFilter, petAutoRefresh, petRefreshInterval,
}));
} catch (_) {}
}
function loadSettings() {
try {
const raw = localStorage.getItem(CFG_KEY);
if (!raw) return;
const d = JSON.parse(raw);
const assign = (key, fn) => { if (d[key] !== undefined) fn(d[key]); };
assign('ACCEL', v => ACCEL = v);
assign('SPEED_CAP', v => SPEED_CAP = v);
assign('SPEED_DEFAULT', v => SPEED_DEFAULT = v);
assign('accelEnabled', v => accelEnabled = v);
assign('refreshInterval', v => refreshInterval = v);
assign('autoRefresh', v => autoRefresh = v);
assign('featFly', v => featFly = v);
assign('featSprint', v => featSprint = v);
assign('featWaypoints', v => featWaypoints = v);
assign('featCuddle', v => featCuddle = v); assign('featVisit', v => featCuddle = v);
assign('featCuddleFollow', v => featCuddleFollow = v); assign('featStalk', v => featCuddleFollow = v);
assign('featPets', v => featPets = v);
assign('featAutoLock', v => featAutoLock = v);
assign('petFilter', v => petFilter = v);
assign('petAutoRefresh', v => petAutoRefresh = v);
assign('petRefreshInterval', v => petRefreshInterval = v);
if (d.keybinds) {
for (const k of Object.keys(KEYBINDS)) {
if (typeof d.keybinds[k] === 'string') KEYBINDS[k] = d.keybinds[k];
}
}
} catch (_) {}
}
loadSettings();
const getPlayer = () => W.pc?.app?.root?.findByName('Player') ?? null;
const getKcc = p => p?.script?.kcc ?? null;
const getPC = p => p?.script?.playerController ?? null;
const $ = id => document.getElementById(id);
const flash = id => { const el = $(id); if (el) { el.classList.add('fresh'); setTimeout(() => el.classList.remove('fresh'), 400); } };
const syncSlider = (id, valId, v) => { const el = $(id), ve = $(valId); if (el) el.value = v; if (ve) ve.textContent = v; };
const vec3 = v => ({ x: v.x, y: v.y, z: v.z });
const FEAT_IDS = ['ml-f-fly','ml-f-sprint','ml-f-waypoints','ml-f-cuddle','ml-f-cuddle-follow','ml-f-pets','ml-f-autolock'];
const MOVE_KEYS = new Set(['KeyW','KeyA','KeyS','KeyD','ArrowUp','ArrowDown','ArrowLeft','ArrowRight']);
function getPlayerState() {
const nm = W.pc?.app?.root?.findByName('NetworkManager')?.script?.networkManager;
if (!nm?.room?.state?.players) return null;
return nm.room.state.players.get(nm.room.sessionId) ?? null;
}
const setSpeed = (pc, kcc, v) => {
if (pc) { pc.currentSpeed = v; pc.speed = v; }
if (kcc.speed !== undefined) kcc.speed = v;
const ps = getPlayerState();
if (ps && ps.movementSpeed !== undefined) ps.movementSpeed = v / SPEED_DEFAULT;
};
function teleport(player, pos) {
player.setPosition(pos.x, pos.y, pos.z);
player.rigidbody?.teleport(pos.x, pos.y, pos.z);
}
function saveWaypoints() {
try {
localStorage.setItem(STORE_KEY, JSON.stringify({
home: homePos ? vec3(homePos) : null,
back: backPos ? vec3(backPos) : null,
slots: slots.map(s => s ? vec3(s) : null),
}));
} catch (_) {}
}
function loadWaypoints() {
try {
const raw = localStorage.getItem(STORE_KEY);
if (!raw) return;
const d = JSON.parse(raw);
if (d.home) homePos = d.home;
if (d.back) backPos = d.back;
if (Array.isArray(d.slots)) d.slots.forEach((s, i) => { if (s) slots[i] = s; });
} catch (_) {}
}
function flyOn(kcc) {
flyActive = true;
flyUp = true;
flyVelY = 0;
kcc.gravity = 0;
kcc._velY = 0;
}
function flyOff(kcc, resetVel = true) {
flyActive = false;
flyUp = false;
flyDown = false;
flyVelY = 0;
kcc.gravity = GRAVITY;
if (resetVel) kcc._velY = 0;
}
const DESCRIPTION = `
Meeland Script — Cheat & Enhancement Suite
General-purpose cheat script for Meeland.io. Works across all game modes — Meeland Hub , Escape Waves , Steal a Pet , and Obby Tower — on all supported platforms.
Designed for PC. Mobile has basic support (collapsible HUD) but keybinds require a keyboard.
Sorry for the long gap between updates — a rating helps if it works for you. ⭐
Features
🚀 Fly Hack — Press Space to activate. Hold Space to ascend, F to descend. Throttle-based acceleration with configurable speed cap and accel rate. Auto-lands on ground contact.
⚡ Speed Hack — Hold Shift for accelerating speed boost. Configurable cap and base speed.
📍 Waypoints — Q saves home, \` teleports to home, Z toggles back position. 10 save slots via Ctrl+Numpad 0-9 , recall with Numpad 0-9 .
🐾 Cuddle Panel — J opens a player list sorted by distance. Click a player to teleport to them. Continuous cuddle follows the target in real time (cancelled by moving).
🐕 Pet Browser — K opens a full pet table with sortable columns (name, mutation, rarity, owner, worth, income, distance). Filter pets with a powerful search supporting AND, OR, and brackets. Teleport to any pet or grab wild/others' pets directly from the table. Configurable auto-refresh interval.
🔒 Auto-Lock Base — Automatically locks your base in Steal a Pet when the lockdown timer expires. Shows lock status and countdown in the HUD.
🛡️ Anti-Disconnect — Prevents getting kicked when entering another player's locked base by spoofing your position to the server.
⚙️ Settings Panel — M opens settings. Adjust speed cap, accel rate, base speed, cuddle panel refresh interval, and toggle individual features on/off. Reset to defaults with one click.
🎮 Rebindable Keybindings — All hotkeys can be rebound from the settings panel. Changes persist across sessions.
🖱️ Draggable & Resizable Panels — All panels (cuddle, settings, keybinds, pets, help) can be dragged by their headers and resized. Positions reset on close.
Keyboard Shortcuts
All keybindings are rebindable via Settings → Keybindings.
Space — Toggle fly / fly up
F (hold) — Fly down
Shift (hold) — Speed hack
Q — Set home waypoint
\` — Teleport to home
Z — Teleport back (toggle)
J — Cuddle panel (player list)
K — Pet browser
M — Settings panel
? — This help dialog
Ctrl+Numpad 0-9 — Save position to slot
Numpad 0-9 — Recall position from slot
Pet Browser
Click column headers to sort (name, mutation, rarity, owner, worth, income, distance)
Use the search bar to filter — words are AND'd, use OR for alternatives, brackets to group
➜ button teleports to a pet
✋ button grabs a pet (teleports, buys, returns, drops at your position)
Grabbed pets show ✔ for 5 seconds
Toggle auto-refresh and adjust interval (1-10s) in the header
Installation
Install a userscript manager:
Click Install above
Load any Meeland game — the script activates automatically
Supported Sites
✅ meeland.io
✅ CrazyGames
✅ twoplayergames.org
✅ iogames.onl
✅ sprunki-game.io
✅ gameflare.com
✅ Any site embedding Meeland in an iframe
Privacy
✅ Client-side only — no data collected, no external requests
Disclaimer
For educational and entertainment purposes. Use at your own risk.
`;
function createHUD() {
if ($('ml-hud')) return;
const s = document.createElement('style');
s.textContent = [
'#ml-hud{position:fixed;bottom:14px;right:14px;display:flex;flex-wrap:wrap-reverse;justify-content:flex-end;align-items:flex-end;gap:5px;z-index:99999;pointer-events:none;font-family:monospace;user-select:none;max-width:calc(100vw - 28px)}',
'#ml-hud-toggle{display:none;pointer-events:auto;cursor:pointer;background:rgba(0,0,0,.6);color:#daa520;font-weight:900;font-size:11px;border:1px solid rgba(218,165,32,.4);border-radius:99px;padding:3px 8px;font-family:monospace;line-height:1}',
'#ml-hud.collapsed .ml-b{display:none}',
'#ml-hud.collapsed #ml-hud-toggle{display:inline-block}',
'@media (pointer:coarse),(max-width:768px){#ml-hud{bottom:4px;right:4px;gap:3px;max-width:calc(100vw - 8px)}#ml-hud-toggle{display:inline-block}.ml-b{font-size:9px;padding:2px 4px}}',
'.ml-b{padding:2px 5px;border-radius:99px;font-size:11px;font-weight:700;letter-spacing:0;background:rgba(0,0,0,.45);color:#daa520;transition:color .12s,background .12s;position:relative}',
'.ml-b.act{pointer-events:auto;cursor:pointer}',
'.ml-b.act:hover{color:#fff;background:rgba(255,255,255,.1)}',
'.ml-b.on{color:#fff;background:rgba(60,220,140,.55)}',
'.ml-b.fresh{color:#fff;background:rgba(60,220,140,.75)}',
'.ml-b.disabled{color:rgba(255,60,60,.45);text-decoration:line-through;text-decoration-color:rgba(255,40,40,.7);text-decoration-thickness:2px;background:rgba(120,0,0,.25)}',
'#ml-dialog{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.75);z-index:100000;display:none;overflow:auto;padding:40px 20px;box-sizing:border-box}',
'#ml-dialog.open{display:block}',
'#ml-inner{background:#181818;color:#ccc;max-width:580px;margin:0 auto;border-radius:8px;padding:24px 28px;font-family:sans-serif;font-size:14px;line-height:1.65;position:relative}',
'#ml-inner h1{color:#fff;font-size:1.2em;margin:0 0 10px}',
'#ml-inner h2{color:#aaa;font-size:.85em;text-transform:uppercase;letter-spacing:.08em;margin:18px 0 6px;border-top:1px solid #333;padding-top:12px}',
'#ml-inner li{margin:4px 0}',
'#ml-inner kbd{background:#2a2a2a;border:1px solid #444;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.9em}',
'#ml-inner a{color:#4af}',
'#ml-inner blockquote{border-left:3px solid #333;margin:8px 0 0;padding:6px 12px;color:#888;font-size:.9em}',
'#ml-close{position:absolute;top:10px;right:14px;cursor:pointer;color:#555;font-size:20px;background:none;border:none;font-family:monospace;line-height:1}',
'#ml-close:hover{color:#fff}',
'#ml-plist{position:fixed;top:50%;right:20px;transform:translateY(-50%);width:260px;max-height:70vh;display:none;z-index:100001;pointer-events:auto;overflow:hidden;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:linear-gradient(170deg,rgba(15,20,35,.94) 0%,rgba(20,28,50,.96) 50%,rgba(12,16,30,.97) 100%);backdrop-filter:blur(24px) saturate(1.4);-webkit-backdrop-filter:blur(24px) saturate(1.4);border:1px solid rgba(100,180,255,.22);box-shadow:0 0 0 1px rgba(60,140,255,.08),0 4px 24px rgba(0,0,0,.7),0 12px 48px rgba(0,0,0,.5),inset 0 1px 0 rgba(140,200,255,.15),inset 0 -1px 0 rgba(0,0,0,.3),0 0 20px rgba(60,140,255,.06);user-select:none}',
'#ml-plist.open{display:flex;flex-direction:column}',
'#ml-plist-head{padding:11px 14px 9px;border-bottom:1px solid rgba(80,160,255,.2);display:flex;align-items:center;justify-content:space-between;background:linear-gradient(180deg,rgba(40,100,200,.12) 0%,rgba(30,70,150,.04) 100%)}',
'#ml-plist-title{font-size:12px;font-weight:800;color:rgba(200,225,255,.95);text-transform:uppercase;letter-spacing:.12em;text-shadow:0 0 8px rgba(80,160,255,.3),0 1px 2px rgba(0,0,0,.5)}',
'#ml-plist-timer{font-size:10px;color:rgba(120,180,255,.55);font-variant-numeric:tabular-nums;margin-left:6px;text-shadow:0 0 4px rgba(60,140,255,.2)}',
'#ml-plist-refresh{cursor:pointer;background:linear-gradient(180deg,rgba(50,110,200,.2) 0%,rgba(40,90,170,.12) 100%);border:1px solid rgba(80,160,255,.28);color:rgba(170,210,255,.8);font-size:12px;padding:3px 9px;border-radius:6px;font-family:inherit;transition:all .15s;text-shadow:0 1px 2px rgba(0,0,0,.3)}',
'#ml-plist-refresh:hover{background:linear-gradient(180deg,rgba(50,120,220,.35) 0%,rgba(40,100,190,.2) 100%);color:#e0edff;border-color:rgba(80,170,255,.45);box-shadow:0 0 10px rgba(60,140,255,.15)}',
'#ml-plist-close{cursor:pointer;background:none;border:none;color:rgba(180,210,255,.45);font-size:18px;padding:0 4px;font-family:monospace;line-height:1;transition:all .15s}',
'#ml-plist-close:hover{color:#ff6b6b;text-shadow:0 0 8px rgba(255,80,80,.4)}',
'#ml-plist-body{overflow-y:auto;flex:1;padding:4px 8px 6px;scrollbar-width:thin;scrollbar-color:rgba(80,160,255,.2) transparent}',
'#ml-plist-body::-webkit-scrollbar{width:5px}',
'#ml-plist-body::-webkit-scrollbar-thumb{background:linear-gradient(180deg,rgba(60,140,255,.25),rgba(80,160,255,.15));border-radius:3px}',
'#ml-plist-empty{color:rgba(140,190,255,.45);text-align:center;padding:20px 8px;font-size:12px;font-style:italic;text-shadow:0 1px 2px rgba(0,0,0,.3)}',
'.ml-prow{display:flex;align-items:center;gap:8px;padding:6px 9px;margin:2px 0;border-radius:7px;cursor:pointer;transition:all .15s;border:1px solid transparent;background:rgba(20,35,60,.25)}',
'.ml-prow:hover{background:linear-gradient(90deg,rgba(40,90,180,.2) 0%,rgba(50,110,200,.12) 100%);border-color:rgba(80,160,255,.2);box-shadow:0 0 8px rgba(60,140,255,.08),inset 0 0 12px rgba(60,140,255,.04)}',
'.ml-prow:active{background:rgba(50,110,200,.28);transform:scale(.98);border-color:rgba(80,160,255,.3)}',
'.ml-pnum{font-size:10px;color:rgba(100,170,255,.6);font-weight:700;min-width:16px;text-align:right;font-variant-numeric:tabular-nums;text-shadow:0 0 4px rgba(60,140,255,.15)}',
'.ml-pinfo{flex:1;overflow:hidden}',
'.ml-pname{font-size:12.5px;font-weight:600;color:rgba(230,240,255,.95);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.3;text-shadow:0 1px 3px rgba(0,0,0,.4)}',
'.ml-pdist{font-size:10px;color:rgba(120,180,255,.6);line-height:1.2;text-shadow:0 1px 2px rgba(0,0,0,.3)}',
'.ml-parrow{color:rgba(80,160,255,.3);font-size:14px;transition:all .15s;text-shadow:0 0 4px rgba(60,140,255,.1)}',
'.ml-prow:hover .ml-parrow{color:rgba(100,180,255,.75);transform:translateX(3px);text-shadow:0 0 8px rgba(60,140,255,.3)}',
'#ml-settings{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:340px;max-height:80vh;display:none;z-index:100002;pointer-events:auto;overflow:hidden;border-radius:4px;font-family:"Courier New",Courier,monospace;user-select:none}',
'#ml-settings.open{display:flex;flex-direction:column}',
'#ml-settings-bg{position:absolute;inset:0;border-radius:4px;background:linear-gradient(175deg,#10141e 0%,#171d2c 15%,#1a2236 30%,#1e2840 50%,#1a2236 70%,#171d2c 85%,#10141e 100%);box-shadow:inset 0 2px 0 rgba(160,200,255,.18),inset 0 -2px 0 rgba(0,0,0,.5),inset 2px 0 0 rgba(100,160,240,.06),inset -2px 0 0 rgba(100,160,240,.06),0 0 0 1px rgba(70,130,220,.3),0 0 0 3px rgba(10,15,25,.8),0 0 0 4px rgba(70,130,220,.15),0 20px 60px rgba(0,0,0,.75),0 0 30px rgba(40,100,200,.08);border:none}',
'#ml-settings-bg::before{content:"";position:absolute;inset:0;border-radius:4px;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(100,170,255,.018) 2px,rgba(100,170,255,.018) 3px),repeating-linear-gradient(90deg,transparent,transparent 40px,rgba(100,170,255,.012) 40px,rgba(100,170,255,.012) 41px);pointer-events:none}',
'#ml-settings-bg::after{content:"";position:absolute;inset:0;border-radius:4px;background:radial-gradient(ellipse at 30% 0%,rgba(60,140,255,.12) 0%,transparent 45%),radial-gradient(ellipse at 70% 100%,rgba(40,100,200,.08) 0%,transparent 40%),radial-gradient(circle at 50% 50%,rgba(50,120,220,.02) 0%,transparent 70%);pointer-events:none}',
'#ml-settings-head{position:relative;padding:14px 18px 11px;border-bottom:2px solid rgba(60,140,255,.25);display:flex;align-items:center;justify-content:space-between;background:linear-gradient(180deg,rgba(50,110,200,.14) 0%,rgba(30,70,150,.05) 60%,transparent 100%);box-shadow:0 1px 0 rgba(60,140,255,.1)}',
'#ml-settings-title{font-size:14px;font-weight:800;color:rgba(180,215,255,.95);letter-spacing:.2em;text-transform:uppercase;text-shadow:0 0 14px rgba(60,140,255,.4),0 0 28px rgba(60,140,255,.15),0 1px 2px rgba(0,0,0,.6)}',
'#ml-settings-close{cursor:pointer;background:none;border:none;color:rgba(160,200,255,.4);font-size:20px;padding:0 4px;font-family:monospace;line-height:1;transition:all .15s}',
'#ml-settings-close:hover{color:#ff5555;text-shadow:0 0 12px rgba(255,60,60,.5)}',
'#ml-settings-reset{cursor:pointer;background:linear-gradient(180deg,rgba(50,100,180,.18) 0%,rgba(35,75,140,.1) 100%);border:1px solid rgba(60,140,255,.3);color:rgba(160,205,255,.7);font-size:9px;padding:4px 12px;border-radius:2px;font-family:"Courier New",Courier,monospace;letter-spacing:.12em;text-transform:uppercase;transition:all .15s;text-shadow:0 0 6px rgba(60,140,255,.2);box-shadow:0 0 0 1px rgba(60,140,255,.05)}',
'#ml-settings-reset:hover{color:#e0edff;background:linear-gradient(180deg,rgba(50,110,200,.3) 0%,rgba(40,90,170,.18) 100%);border-color:rgba(60,150,255,.5);box-shadow:0 0 12px rgba(60,140,255,.15),0 0 0 1px rgba(60,140,255,.1)}',
'#ml-settings-body{position:relative;overflow-y:auto;flex:1;padding:10px 14px 14px;scrollbar-width:thin;scrollbar-color:rgba(60,140,255,.25) transparent}',
'#ml-settings-body::-webkit-scrollbar{width:4px}',
'#ml-settings-body::-webkit-scrollbar-thumb{background:linear-gradient(180deg,rgba(60,140,255,.3),rgba(80,160,255,.15));border-radius:2px}',
'.ml-sgroup{margin:0 0 8px}',
'.ml-slabel{font-size:9.5px;color:rgba(80,160,255,.7);text-transform:uppercase;letter-spacing:.2em;padding:8px 2px 4px;font-weight:800;border-bottom:1px solid rgba(60,140,255,.15);text-shadow:0 0 8px rgba(60,140,255,.2);background:linear-gradient(90deg,rgba(60,140,255,.04) 0%,transparent 80%)}',
'.ml-srow{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;border-radius:3px;transition:all .12s;border:1px solid transparent}',
'.ml-srow:hover{background:linear-gradient(90deg,rgba(40,100,200,.12) 0%,rgba(50,110,200,.06) 100%);border-color:rgba(60,140,255,.08)}',
'.ml-srow-label{font-size:11.5px;color:rgba(200,220,250,.85);flex:1;text-shadow:0 1px 2px rgba(0,0,0,.4)}',
'.ml-srow-value{font-size:10.5px;color:rgba(100,175,255,.75);font-variant-numeric:tabular-nums;min-width:34px;text-align:right;margin-right:6px;text-shadow:0 0 6px rgba(60,140,255,.15)}',
'.ml-toggle{position:relative;width:34px;height:18px;border-radius:2px;background:linear-gradient(180deg,rgba(25,35,55,.9) 0%,rgba(20,30,48,.95) 100%);border:1px solid rgba(60,130,220,.3);cursor:pointer;transition:all .18s;flex-shrink:0;box-shadow:inset 0 1px 3px rgba(0,0,0,.4),0 0 0 1px rgba(60,140,255,.05)}',
'.ml-toggle::after{content:"";position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:2px;background:linear-gradient(180deg,rgba(100,160,240,.3) 0%,rgba(80,140,220,.2) 100%);transition:all .18s;box-shadow:0 1px 4px rgba(0,0,0,.4)}',
'.ml-toggle.on{background:linear-gradient(180deg,rgba(30,70,140,.6) 0%,rgba(25,60,120,.7) 100%);border-color:rgba(60,150,255,.55);box-shadow:inset 0 1px 3px rgba(0,0,0,.3),0 0 8px rgba(60,140,255,.12)}',
'.ml-toggle.on::after{left:18px;background:linear-gradient(180deg,rgba(120,190,255,.95) 0%,rgba(80,160,255,.85) 100%);box-shadow:0 0 10px rgba(60,150,255,.5),0 0 20px rgba(60,140,255,.2)}',
'.ml-slider{-webkit-appearance:none;appearance:none;width:84px;height:4px;border-radius:2px;background:linear-gradient(90deg,rgba(30,50,80,.6) 0%,rgba(40,70,120,.4) 100%);outline:none;cursor:pointer;border:1px solid rgba(60,140,255,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}',
'.ml-slider::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:2px;background:linear-gradient(180deg,rgba(140,200,255,.9) 0%,rgba(100,170,255,.75) 100%);border:1px solid rgba(60,150,255,.6);box-shadow:0 0 8px rgba(60,150,255,.35),0 0 16px rgba(60,140,255,.12),0 1px 3px rgba(0,0,0,.4);cursor:pointer}',
'.ml-slider::-moz-range-thumb{width:14px;height:14px;border-radius:2px;background:linear-gradient(180deg,rgba(140,200,255,.9) 0%,rgba(100,170,255,.75) 100%);border:1px solid rgba(60,150,255,.6);box-shadow:0 0 8px rgba(60,150,255,.35),0 0 16px rgba(60,140,255,.12),0 1px 3px rgba(0,0,0,.4);cursor:pointer}',
'#ml-keybinds{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:320px;max-height:70vh;display:none;z-index:100003;pointer-events:auto;overflow:hidden;border-radius:4px;font-family:"Courier New",Courier,monospace;user-select:none}',
'#ml-keybinds.open{display:flex;flex-direction:column}',
'#ml-keybinds-bg{position:absolute;inset:0;border-radius:4px;background:linear-gradient(175deg,#10141e 0%,#171d2c 15%,#1a2236 30%,#1e2840 50%,#1a2236 70%,#171d2c 85%,#10141e 100%);box-shadow:inset 0 2px 0 rgba(160,200,255,.18),inset 0 -2px 0 rgba(0,0,0,.5),0 0 0 1px rgba(70,130,220,.3),0 0 0 3px rgba(10,15,25,.8),0 0 0 4px rgba(70,130,220,.15),0 20px 60px rgba(0,0,0,.75)}',
'#ml-keybinds-head{position:relative;padding:14px 18px 11px;border-bottom:2px solid rgba(60,140,255,.25);display:flex;align-items:center;justify-content:space-between;background:linear-gradient(180deg,rgba(50,110,200,.14) 0%,rgba(30,70,150,.05) 60%,transparent 100%)}',
'#ml-keybinds-title{font-size:14px;font-weight:800;color:rgba(180,215,255,.95);letter-spacing:.2em;text-transform:uppercase;text-shadow:0 0 14px rgba(60,140,255,.4),0 1px 2px rgba(0,0,0,.6)}',
'#ml-keybinds-close{cursor:pointer;background:none;border:none;color:rgba(160,200,255,.4);font-size:20px;padding:0 4px;font-family:monospace;line-height:1;transition:all .15s}',
'#ml-keybinds-close:hover{color:#ff5555;text-shadow:0 0 12px rgba(255,60,60,.5)}',
'#ml-keybinds-body{position:relative;overflow-y:auto;flex:1;padding:10px 14px 14px;scrollbar-width:thin;scrollbar-color:rgba(60,140,255,.25) transparent}',
'.ml-krow{display:flex;align-items:center;justify-content:space-between;padding:7px 8px;border-radius:3px;cursor:pointer;transition:all .12s;border:1px solid transparent}',
'.ml-krow:hover{background:linear-gradient(90deg,rgba(40,100,200,.12) 0%,rgba(50,110,200,.06) 100%);border-color:rgba(60,140,255,.08)}',
'.ml-krow.listening{background:rgba(255,180,40,.1);border-color:rgba(255,180,40,.35)}',
'.ml-krow-action{font-size:11.5px;color:rgba(200,220,250,.85);text-shadow:0 1px 2px rgba(0,0,0,.4)}',
'.ml-krow-key{font-size:11px;color:rgba(100,175,255,.8);font-weight:700;padding:2px 8px;border-radius:3px;background:rgba(30,50,80,.5);border:1px solid rgba(60,140,255,.2);min-width:40px;text-align:center;transition:all .15s}',
'.ml-krow.listening .ml-krow-key{color:rgba(255,200,80,.9);background:rgba(120,80,0,.2);border-color:rgba(255,180,40,.4)}',
'#ml-pets{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:680px;max-height:82vh;display:none;z-index:100004;pointer-events:auto;overflow:hidden;border-radius:4px;font-family:"Courier New",Courier,monospace;user-select:none}',
'#ml-pets.open{display:flex;flex-direction:column}',
'#ml-pets-bg{position:absolute;inset:0;border-radius:4px;background:linear-gradient(175deg,#10141e 0%,#171d2c 15%,#1a2236 30%,#1e2840 50%,#1a2236 70%,#171d2c 85%,#10141e 100%);box-shadow:inset 0 2px 0 rgba(160,200,255,.18),inset 0 -2px 0 rgba(0,0,0,.5),0 0 0 1px rgba(70,130,220,.3),0 0 0 3px rgba(10,15,25,.8),0 0 0 4px rgba(70,130,220,.15),0 20px 60px rgba(0,0,0,.75)}',
'#ml-pets-bg::before{content:"";position:absolute;inset:0;border-radius:4px;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(100,170,255,.018) 2px,rgba(100,170,255,.018) 3px);pointer-events:none}',
'#ml-pets-head{position:relative;padding:12px 16px 10px;border-bottom:2px solid rgba(60,140,255,.25);display:flex;align-items:center;justify-content:space-between;background:linear-gradient(180deg,rgba(50,110,200,.14) 0%,transparent 100%)}',
'#ml-pets-title{font-size:14px;font-weight:800;color:rgba(180,215,255,.95);letter-spacing:.2em;text-transform:uppercase;text-shadow:0 0 14px rgba(60,140,255,.4),0 1px 2px rgba(0,0,0,.6)}',
'#ml-pets-count{font-size:10px;color:rgba(100,170,255,.5);margin-left:8px;letter-spacing:.1em}',
'#ml-pets-close{cursor:pointer;background:none;border:none;color:rgba(160,200,255,.4);font-size:20px;padding:0 4px;font-family:monospace;line-height:1;transition:all .15s}',
'#ml-pets-close:hover{color:#ff5555;text-shadow:0 0 12px rgba(255,60,60,.5)}',
'#ml-pets-filter{position:relative;padding:8px 14px;border-bottom:1px solid rgba(60,140,255,.12)}',
'#ml-pets-search{width:100%;background:rgba(20,30,50,.6);border:1px solid rgba(60,140,255,.2);color:rgba(200,225,255,.9);font-size:11px;padding:5px 10px;border-radius:3px;font-family:inherit;outline:none;box-sizing:border-box}',
'#ml-pets-search:focus{border-color:rgba(60,150,255,.45);box-shadow:0 0 8px rgba(60,140,255,.12)}',
'#ml-pets-search::placeholder{color:rgba(100,160,255,.35)}',
'#ml-pets-body{position:relative;overflow-y:auto;flex:1;padding:0;scrollbar-width:thin;scrollbar-color:rgba(60,140,255,.25) transparent}',
'#ml-pets-body::-webkit-scrollbar{width:4px}',
'#ml-pets-body::-webkit-scrollbar-thumb{background:linear-gradient(180deg,rgba(60,140,255,.3),rgba(80,160,255,.15));border-radius:2px}',
'#ml-ptable{width:100%;border-collapse:collapse;font-size:11px}',
'#ml-ptable th{position:sticky;top:0;background:linear-gradient(180deg,rgba(25,35,55,.98),rgba(20,30,48,.95));color:rgba(100,170,255,.7);font-size:9.5px;text-transform:uppercase;letter-spacing:.15em;padding:7px 8px;text-align:left;cursor:pointer;border-bottom:1px solid rgba(60,140,255,.2);white-space:nowrap;transition:color .12s;z-index:1}',
'#ml-ptable th:hover{color:rgba(140,200,255,.95)}',
'#ml-ptable th.sort-asc::after{content:" ▲";font-size:8px}',
'#ml-ptable th.sort-desc::after{content:" ▼";font-size:8px}',
'#ml-ptable td{padding:5px 8px;border-bottom:1px solid rgba(60,140,255,.06);color:rgba(200,220,250,.8);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px}',
'#ml-ptable tr:hover td{background:rgba(40,90,180,.12)}',
'#ml-ptable .own-you{color:rgba(80,255,120,.9);font-weight:600}',
'#ml-ptable .own-wild{color:rgba(255,190,60,.85);font-style:italic}',
'#ml-ptable .pet-var{color:rgba(160,120,255,.85);font-style:italic}',
'#ml-ptable .pet-mut-Golden{color:rgba(255,215,0,.95)}',
'#ml-ptable .pet-mut-Diamond{color:rgba(185,242,255,.95)}',
'#ml-ptable .pet-mut-Emerald{color:rgba(80,255,120,.95)}',
'#ml-ptable .pet-mut-Rainbow{color:rgba(255,120,200,.95)}',
'#ml-ptable .pet-mut-Galaxy{color:rgba(200,140,255,.95)}',
'#ml-ptable .pet-rar{font-size:10px;letter-spacing:.05em}',
'.pet-tp{background:rgba(60,140,255,.2);border:1px solid rgba(60,140,255,.3);color:rgba(140,200,255,.8);font-size:11px;padding:2px 6px;border-radius:3px;cursor:pointer;transition:all .12s;line-height:1}',
'.pet-tp:hover{background:rgba(60,140,255,.4);color:#fff;border-color:rgba(80,170,255,.6)}',
'.pet-grab{background:rgba(60,200,60,.2);border:1px solid rgba(60,200,60,.3);color:rgba(140,255,140,.8);font-size:11px;padding:2px 6px;border-radius:3px;cursor:pointer;transition:all .12s;line-height:1;margin-left:3px}',
'.pet-grab:hover{background:rgba(60,200,60,.4);color:#fff;border-color:rgba(80,230,80,.6)}',
'#ml-pets-empty{color:rgba(140,190,255,.45);text-align:center;padding:30px 8px;font-size:12px;font-style:italic}',
].join('');
document.head.appendChild(s);
const h = document.createElement('div');
h.id = 'ml-hud';
h.innerHTML = 'ML FLY (SPC) SPR (Shft) SETHOME (Q) HOME (`) BACK (Z) SLOTS (0/10) 🔒 LOCK CUDDLE (J) PETS (K) ⚙ (M) ? ';
document.body.appendChild(h);
const d = document.createElement('div');
d.id = 'ml-dialog';
d.innerHTML = `✕ ${DESCRIPTION}
`;
document.body.appendChild(d);
const plist = document.createElement('div');
plist.id = 'ml-plist';
plist.innerHTML = '';
document.body.appendChild(plist);
const settings = document.createElement('div');
settings.id = 'ml-settings';
settings.innerHTML = `
`;
document.body.appendChild(settings);
const kbPanel = document.createElement('div');
kbPanel.id = 'ml-keybinds';
const KB_LABELS = {
fly: 'Fly (toggle/up)',
flyDown: 'Fly Down',
setHome: 'Set Home',
home: 'Teleport Home',
back: 'Back Teleport',
cuddle: 'Cuddle Panel',
settings: 'Settings',
pets: 'Pet Browser',
};
function keyCodeLabel(code) {
if (code === 'Space') return 'Space';
if (code === 'Backquote') return '`';
if (code.startsWith('Key')) return code.slice(3);
if (code.startsWith('Digit')) return code.slice(5);
return code;
}
function buildKeybindsHTML() {
let rows = '';
for (const [action, code] of Object.entries(KEYBINDS)) {
rows += '' + (KB_LABELS[action] || action) + ' ' + keyCodeLabel(code) + '
';
}
kbPanel.innerHTML = '
Keybindings ✕
' + rows + '
';
}
buildKeybindsHTML();
document.body.appendChild(kbPanel);
const petsPanel = document.createElement('div');
petsPanel.id = 'ml-pets';
petsPanel.innerHTML = '
';
document.body.appendChild(petsPanel);
[plist, settings, d, kbPanel, petsPanel].forEach(el => {
['mousedown','mouseup','click','pointerdown','pointerup','mousemove','mouseover','mouseenter','mouseleave','wheel','contextmenu','pointermove','pointerover','pointerenter'].forEach(evt => {
el.addEventListener(evt, e => e.stopPropagation());
});
});
function makeDraggable(panel, handle) {
let dragging = false, ox = 0, oy = 0;
handle.style.cursor = 'grab';
panel.style.resize = 'both';
panel.style.overflow = 'auto';
handle.addEventListener('mousedown', e => {
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'LABEL') return;
dragging = true;
handle.style.cursor = 'grabbing';
const rect = panel.getBoundingClientRect();
panel.style.left = rect.left + 'px';
panel.style.top = rect.top + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
panel.style.transform = 'none';
ox = e.clientX - rect.left;
oy = e.clientY - rect.top;
e.preventDefault();
});
window.addEventListener('mousemove', e => {
if (!dragging) return;
panel.style.left = Math.max(0, Math.min(e.clientX - ox, window.innerWidth - 40)) + 'px';
panel.style.top = Math.max(0, Math.min(e.clientY - oy, window.innerHeight - 40)) + 'px';
}, true);
window.addEventListener('mouseup', () => {
if (dragging) { dragging = false; handle.style.cursor = 'grab'; }
}, true);
panel._resetPos = () => {
panel.style.left = '';
panel.style.top = '';
panel.style.right = '';
panel.style.bottom = '';
panel.style.transform = '';
panel.style.width = '';
panel.style.height = '';
};
}
makeDraggable(plist, $('ml-plist-head'));
makeDraggable(settings, $('ml-settings-head'));
makeDraggable(kbPanel, $('ml-keybinds-head'));
makeDraggable(petsPanel, $('ml-pets-head'));
makeDraggable(d.querySelector('#ml-inner'), d.querySelector('#ml-inner h1'));
function syncSettingsUI() {
syncSlider('ml-s-cap', 'ml-sv-cap', SPEED_CAP);
syncSlider('ml-s-accel', 'ml-sv-accel', ACCEL);
syncSlider('ml-s-base', 'ml-sv-base', SPEED_DEFAULT);
syncSlider('ml-s-interval', 'ml-sv-interval', refreshInterval);
const ae = $('ml-s-accel-en');
if (ae) ae.classList.toggle('on', accelEnabled);
const ar = $('ml-s-autorefresh');
if (ar) ar.classList.toggle('on', autoRefresh);
const featVals = [featFly, featSprint, featWaypoints, featCuddle, featCuddleFollow, featPets, featAutoLock];
FEAT_IDS.forEach((id, i) => { const el = $(id); if (el) el.classList.toggle('on', featVals[i]); });
}
syncSettingsUI();
function getPlayers() {
const holder = W.pc?.app?.root?.findByName('EnemyHolder');
if (!holder) return [];
const player = getPlayer();
const playerPos = player?.getPosition();
const results = [];
for (const enemy of holder.children) {
const pos = enemy.getPosition();
let username = null;
try { username = enemy.script?.enemy?.usernameEntity?.element?.text; } catch (_) {}
if (!username) {
const ue = enemy.findByName?.('Username');
if (ue?.element?.text) username = ue.element.text;
}
if (!username || username === 'Enemy') username = enemy.name !== 'Enemy' ? enemy.name : null;
if (!username) username = 'Player';
const dist = playerPos ? Math.floor(playerPos.distance(pos)) : '?';
results.push({ name: username, dist, pos: vec3(pos), entity: enemy });
}
results.sort((a, b) => (typeof a.dist === 'number' && typeof b.dist === 'number') ? a.dist - b.dist : 0);
return results;
}
function refreshPlayerList() {
const body = $('ml-plist-body');
if (!body) return;
const players = getPlayers();
if (players.length === 0) {
body.innerHTML = 'No other players found
';
return;
}
body.innerHTML = '';
players.forEach((p, i) => {
const row = document.createElement('div');
row.className = 'ml-prow';
row.innerHTML = `${i + 1} → `;
row.querySelector('.ml-pname').textContent = p.name;
row.addEventListener('click', () => {
const player = getPlayer();
if (!player) return;
const freshPos = p.entity?.getPosition();
const target = freshPos || p.pos;
backPos = player.getPosition().clone();
teleport(player, vec3(target));
saveWaypoints();
if (featCuddleFollow) {
cuddleTarget = p.entity;
cuddling = true;
console.log(`[cuddle] locked → ${p.name}`);
}
flash('ml-back');
const app = W.pc?.app;
if (app) app.fire('GameManager:GameResumed');
});
body.appendChild(row);
});
}
function getPetOwnerId(pet) {
if (!pet) return null;
const d = pet.owner;
if (d === false) return false;
if (d != null) return d;
return pet?.data?.owner ?? pet?.ownerId ?? pet?.data?.ownerId ?? null;
}
let _ownerMapCache = null;
let _ownerMapTime = 0;
function buildOwnerMap() {
const now = Date.now();
if (_ownerMapCache && now - _ownerMapTime < 2000) return _ownerMapCache;
const map = {};
const myId = W.pc?.sessionId;
if (myId) map[myId] = 'Yours';
const nm = W.pc?.app?.root?.findByName('NetworkManager')?.script?.networkManager;
if (nm?.sessionId && !map[nm.sessionId]) map[nm.sessionId] = 'Yours';
const holder = W.pc?.app?.root?.findByName('EnemyHolder');
if (holder) {
for (const child of holder.children) {
let id = child.id || child.sessionId || child.playerId;
if (!id && child.script) {
for (const sk of Object.keys(child.script._scriptsIndex || {})) {
const inst = child.script[sk];
id = inst?.id || inst?.sessionId || inst?.playerId;
if (id) break;
}
}
if (!id) continue;
let name = null;
try { name = child.script?.enemy?.usernameEntity?.element?.text; } catch (_) {}
if (!name) { const ue = child.findByName?.('Username'); if (ue?.element?.text) name = ue.element.text; }
if (name && name !== 'Enemy') map[id] = name;
}
}
_ownerMapCache = map;
_ownerMapTime = now;
return map;
}
function getPetOwnerName(pet, ownerMap) {
const ownerId = getPetOwnerId(pet);
if (ownerId === false || ownerId == null) return 'Wild';
if (ownerMap?.[ownerId]) return ownerMap[ownerId];
return ownerId.length > 10 ? ownerId.substring(0, 8) + '…' : ownerId;
}
let _pmCache = null;
let _pmCacheTime = 0;
function getPetsManager() {
const now = Date.now();
if (_pmCache && now - _pmCacheTime < 5000 && (_pmCache.activePets || _pmCache.basePets)) return _pmCache;
const pmEntity = W.pc?.app?.root?.findByName('PetsManager');
let pm = pmEntity?.script?.petsManager;
if (!pm && pmEntity?.script) {
for (const k of Object.keys(pmEntity.script)) {
const s = pmEntity.script[k];
if (s?.activePets || s?.basePets) { pm = s; break; }
}
}
if (!pm) {
const root = W.pc?.app?.root;
if (!root) return null;
root.find(e => {
if (!e.script) return false;
for (const k of Object.keys(e.script)) {
const s = e.script[k];
if (s?.activePets || s?.basePets) { pm = s; return true; }
}
return false;
});
}
_pmCache = pm || null;
_pmCacheTime = now;
return _pmCache;
}
function getAllPets() {
const pm = getPetsManager();
if (!pm) return [];
hookPetSpawn();
const ownerMap = buildOwnerMap();
const pets = [];
const scan = (map, type) => {
if (!map) return;
map.forEach((pet, token) => {
const name = pet.name || String(token).substring(0, 8);
const sd = petSpawnData.get(token);
let mutation = sd?.mutation ?? '';
let rarity = sd?.rarity ?? '';
let profit = sd?.profit ?? null;
if (mutation === 'Default') mutation = '';
if (!mutation) {
try {
const mutNames = ['Golden', 'Diamond', 'Emerald', 'Rainbow', 'Galaxy'];
for (const mn of mutNames) {
if (pet.findByName?.(`Mutation ${mn} Effect`)) { mutation = mn; break; }
}
} catch (_) {}
}
if (!profit) {
try {
const statsEl = pet.findByName?.('PetStats');
if (statsEl) {
const profitEl = statsEl.findByName?.('Profit') || statsEl.findByName?.('PetProfit');
if (profitEl?.element?.text) {
const m = profitEl.element.text.match(/\$\s*([\d,.]+)\s*([kmbtqsxi]*)/);
if (m) {
let v = parseFloat(m[1].replace(/,/g, ''));
const suf = m[2]?.toLowerCase();
if (suf === 'k') v *= 1e3;
else if (suf === 'm') v *= 1e6;
else if (suf === 'b') v *= 1e9;
else if (suf === 't') v *= 1e12;
else if (suf === 'q') v *= 1e15;
profit = v;
}
}
}
} catch (_) {}
}
const pos = pet.getPosition ? pet.getPosition() : pet.position;
const price = pet.price ?? 0;
pets.push({
token,
name,
price,
income: profit ?? 0,
owner: getPetOwnerName(pet, ownerMap),
ownerId: getPetOwnerId(pet),
type,
mutation,
rarity,
x: pos?.x ?? 0,
y: pos?.y ?? 0,
z: pos?.z ?? 0,
});
});
};
scan(pm.activePets, 'active');
scan(pm.basePets, 'base');
return pets;
}
let petSortCol = 'income';
let petSortDir = -1;
let lastRenderedPets = [];
const grabbedTokens = new Map(); // token → expiry timestamp
function renderPetTable() {
const body = $('ml-pets-body');
const countEl = $('ml-pets-count');
if (!body) return;
let pets = getAllPets();
if (countEl) countEl.textContent = `(${pets.length})`;
if (pets.length === 0) {
lastRenderedPets = [];
body.innerHTML = 'No pets found in this room
';
return;
}
if (petFilter) pets = pets.filter(p => petMatchesFilter(p, petFilter));
const playerPos = getPlayer()?.getPosition();
if (playerPos) pets.forEach(p => { p._dist = Math.hypot(p.x - playerPos.x, p.y - playerPos.y, p.z - playerPos.z); });
pets.sort((a, b) => {
const key = petSortCol === 'dist' ? '_dist' : petSortCol;
let av = a[key], bv = b[key];
if (av == null) av = Infinity;
if (bv == null) bv = Infinity;
if (typeof av === 'string') av = av.toLowerCase();
if (typeof bv === 'string') bv = bv.toLowerCase();
if (av < bv) return -petSortDir;
if (av > bv) return petSortDir;
return 0;
});
lastRenderedPets = pets;
const cols = ['name','mutation','rarity','owner','price','income','dist','go'];
const labels = { name:'Name', mutation:'Mutation', rarity:'Rarity', owner:'Owner', price:'Worth', income:'Income/s', dist:'Dist', go:'' };
let html = '';
cols.forEach(c => {
const cls = petSortCol === c ? (petSortDir === 1 ? 'sort-asc' : 'sort-desc') : '';
html += c === 'go' ? ' ' : `${labels[c]} `;
});
html += ' ';
const now = Date.now();
pets.forEach(p => {
const ownerCls = p.owner === 'Yours' ? 'own-you' : p.owner === 'Wild' ? 'own-wild' : '';
const mutCls = p.mutation ? `pet-mut-${p.mutation}` : 'pet-var';
const tokenStr = String(p.token);
const grabbed = grabbedTokens.has(tokenStr) && grabbedTokens.get(tokenStr) > now;
const grabBtn = p.owner !== 'Yours' ? `${grabbed ? '\u2714' : '\u270B'} ` : '';
html += `${esc(p.name)} ${esc(p.mutation) || '\u2014'} ${esc(p.rarity) || '\u2014'} ${esc(p.owner)} ${numFmt(p.price)} ${numFmt(p.income)}/s ${p._dist != null ? numFmt(Math.round(p._dist)) : '\u2014'} \u279C ${grabBtn} `;
});
html += '
';
body.innerHTML = html;
}
function findPetByToken(token) {
return lastRenderedPets.find(p => String(p.token) === token) || null;
}
$('ml-pets-body').addEventListener('click', e => {
const th = e.target.closest('th[data-col]');
if (th) {
const col = th.dataset.col;
if (petSortCol === col) petSortDir *= -1;
else { petSortCol = col; petSortDir = col === 'price' || col === 'income' || col === 'dist' ? -1 : 1; }
renderPetTable();
return;
}
const tpBtn = e.target.closest('.pet-tp');
if (tpBtn) {
const p = findPetByToken(tpBtn.dataset.token);
if (!p) return;
const player = getPlayer();
if (!player) return;
player.setPosition(p.x, p.y + 1, p.z);
togglePetsPanel(false);
return;
}
const grabBtn = e.target.closest('.pet-grab');
if (grabBtn) {
const p = findPetByToken(grabBtn.dataset.token);
if (!p) return;
const app = W.pc?.app;
if (!app) return;
const player = getPlayer();
if (!player) return;
const tokenStr = String(p.token);
const orig = player.getPosition().clone();
player.setPosition(p.x, p.y + 0.5, p.z);
grabBtn.disabled = true;
grabBtn.textContent = '…';
setTimeout(() => {
app.fire('ModeOverlay:BuyPet', p.token);
console.log(`[ml] grab pet: ${p.name} (${p.token})`);
setTimeout(() => {
player.setPosition(orig.x, orig.y, orig.z);
setTimeout(() => {
const tgt = app.graphicsDevice?.canvas || document;
const opts = { code: 'KeyX', key: 'x', keyCode: 88, which: 88, bubbles: true, cancelable: true };
tgt.dispatchEvent(new KeyboardEvent('keydown', opts));
setTimeout(() => {
tgt.dispatchEvent(new KeyboardEvent('keyup', opts));
console.log(`[ml] drop pet at origin`);
grabbedTokens.set(tokenStr, Date.now() + 5000);
renderPetTable();
}, 50);
}, 150);
}, 150);
}, 100);
}
});
function esc(s) {
return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
function numFmt(n) {
if (n >= 1e15) return (n / 1e15).toFixed(1) + 'q';
if (n >= 1e12) return (n / 1e12).toFixed(1) + 't';
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'b';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'm';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
return n % 1 === 0 ? String(n) : n.toFixed(1);
}
function petMatchesFilter(p, raw) {
if (!raw) return true;
const text = [p.name, p.owner, p.mutation, p.rarity].join(' ').toLowerCase();
const tokens = raw.match(/\(|\)|AND|OR|[^\s()]+/gi) || [];
function parseOr(i) {
let [result, j] = parseAnd(i);
while (j < tokens.length && tokens[j]?.toUpperCase() === 'OR') {
const [r2, j2] = parseAnd(j + 1);
result = result || r2;
j = j2;
}
return [result, j];
}
function parseAnd(i) {
let [result, j] = parseAtom(i);
while (j < tokens.length) {
const up = tokens[j]?.toUpperCase();
if (up === 'AND') { j++; }
else if (up === 'OR' || tokens[j] === ')') break;
const [r2, j2] = parseAtom(j);
result = result && r2;
j = j2;
}
return [result, j];
}
function parseAtom(i) {
if (i >= tokens.length) return [false, i];
if (tokens[i] === '(') {
const [result, j] = parseOr(i + 1);
return [result, j < tokens.length && tokens[j] === ')' ? j + 1 : j];
}
const t = tokens[i].toLowerCase();
return [text.includes(t), i + 1];
}
try { return parseOr(0)[0]; } catch (_) { return text.includes(raw.toLowerCase()); }
}
let petRefreshTimer = null;
function startPetRefresh() {
stopPetRefresh();
petRefreshTimer = setInterval(() => renderPetTable(), petRefreshInterval * 1000);
}
function stopPetRefresh() {
if (petRefreshTimer) { clearInterval(petRefreshTimer); petRefreshTimer = null; }
}
function resumeGame() {
const app = W.pc?.app;
const canvas = app?.graphicsDevice?.canvas;
if (canvas) canvas.requestPointerLock();
if (app) app.fire('GameManager:GameResumed');
}
function togglePetsPanel(forceOpen) {
const open = forceOpen !== undefined ? forceOpen : !petsPanel.classList.contains('open');
petsPanel.classList.toggle('open', open);
if (open) {
$('ml-pets-search').value = petFilter;
$('ml-pets-auto').checked = petAutoRefresh;
syncSlider('ml-pets-interval', 'ml-pets-iv', petRefreshInterval);
renderPetTable();
if (petAutoRefresh) startPetRefresh();
if (document.pointerLockElement) document.exitPointerLock();
} else {
stopPetRefresh();
petsPanel._resetPos?.();
resumeGame();
}
}
$('ml-pets-btn').addEventListener('click', () => togglePetsPanel());
$('ml-pets-close').addEventListener('click', () => togglePetsPanel(false));
$('ml-pets-search').addEventListener('input', e => {
petFilter = e.target.value;
saveSettings();
renderPetTable();
});
$('ml-pets-auto').addEventListener('change', e => {
petAutoRefresh = e.target.checked;
saveSettings();
if (petAutoRefresh && petsPanel.classList.contains('open')) startPetRefresh();
else stopPetRefresh();
});
$('ml-pets-interval').addEventListener('input', e => {
petRefreshInterval = parseInt(e.target.value);
const iv = $('ml-pets-iv');
if (iv) iv.textContent = petRefreshInterval;
saveSettings();
if (petAutoRefresh && petsPanel.classList.contains('open')) startPetRefresh();
});
function toggleCuddlePanel(forceOpen) {
const wasOpen = plist.classList.contains('open');
const open = forceOpen !== undefined ? forceOpen : !wasOpen;
plist.classList.toggle('open', open);
if (open) {
refreshPlayerList();
if (document.pointerLockElement) document.exitPointerLock();
if (!wasOpen && autoRefresh) startRefreshTimer();
} else if (wasOpen) {
stopRefreshTimer();
plist._resetPos?.();
resumeGame();
}
}
const origRequestPointerLock = Element.prototype.requestPointerLock;
Element.prototype.requestPointerLock = function() {
if (plist.classList.contains('open') || settings.classList.contains('open') || kbPanel.classList.contains('open') || petsPanel.classList.contains('open') || d.classList.contains('open')) return;
return origRequestPointerLock.call(this);
};
const anyPanelOpen = () => plist.classList.contains('open') || settings.classList.contains('open') || kbPanel.classList.contains('open') || petsPanel.classList.contains('open') || d.classList.contains('open');
document.addEventListener('pointerlockchange', e => {
if (anyPanelOpen() && !document.pointerLockElement) {
e.stopImmediatePropagation();
}
}, true);
let refreshCountdown = refreshInterval;
let refreshTimer = null;
function startRefreshTimer() {
stopRefreshTimer();
refreshCountdown = refreshInterval;
const timerEl = $('ml-plist-timer');
if (timerEl) timerEl.textContent = refreshCountdown + 's';
refreshTimer = setInterval(() => {
refreshCountdown--;
if (timerEl) timerEl.textContent = refreshCountdown + 's';
if (refreshCountdown <= 0) {
refreshPlayerList();
refreshCountdown = refreshInterval;
if (timerEl) timerEl.textContent = refreshCountdown + 's';
}
}, 1000);
}
function stopRefreshTimer() {
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
}
$('ml-tp').addEventListener('click', () => toggleCuddlePanel());
$('ml-plist-refresh').addEventListener('click', () => {
refreshPlayerList();
if (autoRefresh && plist.classList.contains('open')) startRefreshTimer();
});
$('ml-plist-close').addEventListener('click', () => toggleCuddlePanel(false));
function toggleSettings(forceOpen) {
const open = forceOpen !== undefined ? forceOpen : !settings.classList.contains('open');
settings.classList.toggle('open', open);
if (open && document.pointerLockElement) document.exitPointerLock();
else if (!open) { settings._resetPos?.(); resumeGame(); }
}
$('ml-cfg').addEventListener('click', () => toggleSettings());
$('ml-settings-close').addEventListener('click', () => toggleSettings(false));
const DEFAULTS = { ACCEL: 6, SPEED_CAP: 100, SPEED_DEFAULT: 7, refreshInterval: 10, petRefreshInterval: 1 };
$('ml-settings-reset').addEventListener('click', () => {
ACCEL = DEFAULTS.ACCEL;
SPEED_CAP = DEFAULTS.SPEED_CAP;
SPEED_DEFAULT = DEFAULTS.SPEED_DEFAULT;
refreshInterval = DEFAULTS.refreshInterval;
petRefreshInterval = DEFAULTS.petRefreshInterval;
accelEnabled = true;
autoRefresh = true;
featFly = true; featSprint = true; featWaypoints = true; featCuddle = true; featCuddleFollow = true; featPets = true; featAutoLock = true;
Object.assign(KEYBINDS, DEFAULT_KEYBINDS);
syncSettingsUI();
updateHUDBadgeLabels();
console.log('[settings] reset');
saveSettings();
});
const sliderMap = [
{ id: 'ml-s-cap', valId: 'ml-sv-cap', apply: v => { SPEED_CAP = v; } },
{ id: 'ml-s-accel', valId: 'ml-sv-accel', apply: v => { ACCEL = v; } },
{ id: 'ml-s-base', valId: 'ml-sv-base', apply: v => { SPEED_DEFAULT = v; } },
{ id: 'ml-s-interval',valId: 'ml-sv-interval', apply: v => { refreshInterval = v; } },
];
sliderMap.forEach(s => {
const el = $(s.id);
const valEl = $(s.valId);
if (!el) return;
el.addEventListener('input', () => {
const v = parseInt(el.value);
if (valEl) valEl.textContent = v;
s.apply(v);
saveSettings();
});
});
const arToggle = $('ml-s-autorefresh');
if (arToggle) {
arToggle.addEventListener('click', () => {
autoRefresh = !autoRefresh;
arToggle.classList.toggle('on', autoRefresh);
saveSettings();
const timerEl = $('ml-plist-timer');
if (!autoRefresh) {
stopRefreshTimer();
if (timerEl) timerEl.textContent = 'off';
} else if (plist.classList.contains('open')) {
startRefreshTimer();
}
});
}
const accelToggle = $('ml-s-accel-en');
if (accelToggle) {
accelToggle.addEventListener('click', () => {
accelEnabled = !accelEnabled;
accelToggle.classList.toggle('on', accelEnabled);
saveSettings();
});
}
const featToggles = [
{ id: 'ml-f-fly', get: () => featFly, set: v => { featFly = v; if (!v) { const p = getPlayer(); const k = getKcc(p); if (k && flyActive) flyOff(k); } } },
{ id: 'ml-f-sprint', get: () => featSprint, set: v => { featSprint = v; if (!v) sprinting = false; } },
{ id: 'ml-f-waypoints', get: () => featWaypoints, set: v => { featWaypoints = v; } },
{ id: 'ml-f-cuddle', get: () => featCuddle, set: v => { featCuddle = v; if (!v) toggleCuddlePanel(false); } },
{ id: 'ml-f-cuddle-follow', get: () => featCuddleFollow, set: v => { featCuddleFollow = v; if (!v) { cuddling = false; cuddleTarget = null; } } },
{ id: 'ml-f-pets', get: () => featPets, set: v => { featPets = v; if (!v) togglePetsPanel(false); } },
{ id: 'ml-f-autolock', get: () => featAutoLock, set: v => { featAutoLock = v; } },
];
featToggles.forEach(ft => {
const el = $(ft.id);
if (!el) return;
el.addEventListener('click', () => {
const nv = !ft.get();
ft.set(nv);
el.classList.toggle('on', nv);
saveSettings();
console.log(`[feat] ${ft.id.replace('ml-f-','')} = ${nv}`);
});
});
function toggleKeybinds(forceOpen) {
const open = forceOpen !== undefined ? forceOpen : !kbPanel.classList.contains('open');
kbPanel.classList.toggle('open', open);
if (open) {
buildKeybindsHTML();
if (document.pointerLockElement) document.exitPointerLock();
} else {
kbListeningRow = null;
kbPanel._resetPos?.();
resumeGame();
}
}
kbPanel.addEventListener('click', e => {
if (e.target.closest('#ml-keybinds-close')) { toggleKeybinds(false); return; }
const row = e.target.closest('.ml-krow');
if (!row) return;
kbPanel.querySelectorAll('.ml-krow.listening').forEach(r => r.classList.remove('listening'));
if (kbListeningRow === row) { kbListeningRow = null; return; }
row.classList.add('listening');
row.querySelector('.ml-krow-key').textContent = '...';
kbListeningRow = row;
});
window.addEventListener('keydown', e => {
if (!kbListeningRow) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const action = kbListeningRow.dataset.action;
if (!action || !(action in KEYBINDS)) return;
if (e.code === 'Escape') {
kbListeningRow.classList.remove('listening');
kbListeningRow.querySelector('.ml-krow-key').textContent = keyCodeLabel(KEYBINDS[action]);
kbListeningRow = null;
return;
}
KEYBINDS[action] = e.code;
kbListeningRow.querySelector('.ml-krow-key').textContent = keyCodeLabel(e.code);
kbListeningRow.classList.remove('listening');
kbListeningRow = null;
saveSettings();
updateHUDBadgeLabels();
console.log('[keybind] ' + action + ' → ' + e.code);
}, true);
function updateHUDBadgeLabels() {
const map = {
'ml-fly': ['FLY', 'fly'],
'ml-spr': ['SPR', null],
'ml-home': ['SETHOME', 'setHome'],
'ml-go': ['HOME', 'home'],
'ml-back': ['BACK', 'back'],
'ml-tp': ['CUDDLE', 'cuddle'],
'ml-pets-btn': ['PETS', 'pets'],
'ml-cfg': ['⚙', 'settings'],
};
for (const [id, [label, action]] of Object.entries(map)) {
const el = $(id);
if (!el) continue;
if (!action) { el.textContent = label + ' (Shft)'; continue; }
el.textContent = label + ' (' + keyCodeLabel(KEYBINDS[action]) + ')';
}
}
updateHUDBadgeLabels();
$('ml-s-keybinds').addEventListener('click', () => {
settings.classList.remove('open');
toggleKeybinds(true);
});
$('ml-hud-toggle').addEventListener('click', e => {
if (e.target._wasDragged) { e.target._wasDragged = false; return; }
const hud = $('ml-hud');
const wasCollapsed = hud.classList.contains('collapsed');
hud.classList.toggle('collapsed');
if (wasCollapsed) {
hud.style.left = '';
hud.style.top = '';
hud.style.right = '';
hud.style.bottom = '';
}
});
(function() {
const toggle = $('ml-hud-toggle');
const hud = $('ml-hud');
let dragging = false, ox = 0, oy = 0, moved = false;
toggle.addEventListener('pointerdown', e => {
dragging = true; moved = false;
const rect = hud.getBoundingClientRect();
ox = e.clientX - rect.left;
oy = e.clientY - rect.top;
toggle.setPointerCapture(e.pointerId);
});
toggle.addEventListener('pointermove', e => {
if (!dragging) return;
moved = true;
hud.style.left = Math.max(0, Math.min(e.clientX - ox, window.innerWidth - 40)) + 'px';
hud.style.top = Math.max(0, Math.min(e.clientY - oy, window.innerHeight - 40)) + 'px';
hud.style.right = 'auto';
hud.style.bottom = 'auto';
});
toggle.addEventListener('pointerup', () => {
dragging = false;
if (moved) toggle._wasDragged = true;
});
})();
$('ml-help').addEventListener('click', () => {
d.classList.add('open');
if (document.pointerLockElement) document.exitPointerLock();
});
$('ml-close').addEventListener('click', () => { d.classList.remove('open'); d.querySelector('#ml-inner')._resetPos?.(); resumeGame(); });
d.addEventListener('click', e => { if (e.target === d) { d.classList.remove('open'); d.querySelector('#ml-inner')._resetPos?.(); resumeGame(); } });
d.addEventListener('wheel', e => { e.stopPropagation(); e.preventDefault(); d.scrollTop += e.deltaY; }, { passive: false });
document.addEventListener('click', e => {
if (plist.classList.contains('open') && !plist.contains(e.target) && e.target.id !== 'ml-tp') {
toggleCuddlePanel(false);
}
if (settings.classList.contains('open') && !settings.contains(e.target) && e.target.id !== 'ml-cfg') {
toggleSettings(false);
}
if (kbPanel.classList.contains('open') && !kbPanel.contains(e.target)) {
toggleKeybinds(false);
}
if (petsPanel.classList.contains('open') && !petsPanel.contains(e.target) && e.target.id !== 'ml-pets-btn') {
togglePetsPanel(false);
}
});
}
const petSpawnData = new Map();
const PET_SPAWN_CAP = 2000;
function hookPetSpawn() {
const app = W.pc?.app;
if (!app) return;
if (app._mlPetSpawnHooked) return;
app._mlPetSpawnHooked = true;
const onPetsSpawn = data => {
if (!Array.isArray(data)) return;
if (petSpawnData.size > PET_SPAWN_CAP) {
const excess = petSpawnData.size - PET_SPAWN_CAP + data.length;
const it = petSpawnData.keys();
for (let i = 0; i < excess; i++) petSpawnData.delete(it.next().value);
}
for (const p of data) {
if (!p.token) continue;
petSpawnData.set(p.token, {
profit: p.profit ?? null,
rarity: p.rarity ?? null,
mutation: p.mutation ?? null,
isEgg: !!p.isEgg,
});
}
};
app.on('PetsManager:PetsSpawn', onPetsSpawn);
app.on('BasesManager:PetsSpawn', onPetsSpawn);
console.log('[ml] pet spawn hook installed');
}
// ── Auto-Lock Base ──────────────────────────────────────
function getMyBase() {
const app = W.pc?.app;
if (!app?.root) return null;
const basesEntity = app.root.findByName('Bases');
const basesScript = basesEntity?.script?.petTycoonBasesManager;
if (!basesScript?.activeBases) return null;
const nm = app.root.findByName('NetworkManager')?.script?.networkManager;
const sessionId = nm?.room?.sessionId || W.pc?.sessionId;
if (!sessionId) return null;
for (const bd of basesScript.activeBases) {
if (bd?.data?.sessionId === sessionId) return basesScript.baseEntities?.[bd.data.id] ?? null;
}
return null;
}
function getLockBtn() {
const base = getMyBase();
if (!base) return null;
const btn = base.findByName('LockdownButton');
return btn?.script?.lockdownButton ?? null;
}
function triggerLock() {
const btn = getLockBtn();
if (!btn || typeof btn.onTriggerEnter !== 'function') return false;
const player = getPlayer();
if (!player) return false;
const wasTrig = btn.hasTriggered;
btn.onTriggerEnter(player);
console.log('[ml] auto-lock: triggered (wasTriggered=%s)', wasTrig);
return true;
}
let lastLockCheck = 0;
// ── Anti-Disconnect ─────────────────────────────────────
// Server kicks players whose position is inside a locked base.
// Spoof outgoing position to last safe location when inside one.
let lastSafePos = null;
const BASE_RADIUS_SQ = 20 * 20;
function isInsideLockedBase(x, z) {
const basesScript = W.pc?.app?.root?.findByName('Bases')?.script?.petTycoonBasesManager;
if (!basesScript?.activeBases) return false;
const myId = W.pc?.app?.root?.findByName('NetworkManager')?.script?.networkManager?.room?.sessionId || W.pc?.sessionId;
for (const entry of basesScript.activeBases) {
if (!entry?.entity || entry.data?.sessionId === myId) continue;
const lockBtn = entry.entity.findByName('LockdownButton')?.script?.lockdownButton;
if (!lockBtn?.isLockdownActive || !(lockBtn.lockdownTimeLeft > 0)) continue;
const bp = entry.entity.getPosition();
const dx = x - bp.x, dz = z - bp.z;
if (dx * dx + dz * dz < BASE_RADIUS_SQ) return true;
}
return false;
}
function hookAntiDisconnect() {
const nm = W.pc?.app?.root?.findByName('NetworkManager')?.script?.networkManager;
const room = nm?.room;
if (!room || room.__mlSendHooked) return;
room.__mlSendHooked = true;
const origSend = room.send.bind(room);
room.send = function (type, message) {
if (type === 'p' && message) {
if (isInsideLockedBase(message.x, message.z)) {
const safe = lastSafePos || (() => {
const b = getMyBase()?.getPosition();
return b ? { x: b.x, y: b.y + 1, z: b.z } : null;
})();
if (safe) return origSend(type, { x: safe.x, y: safe.y, z: safe.z, w: message.w });
} else {
lastSafePos = { x: message.x, y: message.y, z: message.z };
}
}
return origSend(type, message);
};
console.log('[ml] anti-disconnect: position spoof active');
}
createHUD();
loadWaypoints();
window.addEventListener('keydown', e => {
const active = document.activeElement;
if (active?.tagName === 'INPUT' || active?.tagName === 'TEXTAREA') return;
if (kbListeningRow) return;
if (e.code === KEYBINDS.cuddle) {
e.preventDefault();
if (featCuddle) $('ml-tp')?.click();
return;
}
if (e.code === KEYBINDS.settings) {
e.preventDefault();
$('ml-cfg')?.click();
return;
}
if (e.code === KEYBINDS.pets) {
e.preventDefault();
if (featPets) $('ml-pets-btn')?.click();
return;
}
const player = getPlayer();
const kcc = getKcc(player);
if (!kcc) return;
if (cuddling && MOVE_KEYS.has(e.code)) {
cuddling = false;
cuddleTarget = null;
console.log('[cuddle] cancelled');
}
const numpadMatch = e.code.match(/^Numpad(\d)$/);
if (numpadMatch && featWaypoints) {
const idx = parseInt(numpadMatch[1]);
if (e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
slots[idx] = player.getPosition().clone();
saveWaypoints();
flash('ml-slots');
console.log(`[slot ${idx}] saved`, slots[idx]);
return;
} else if (slots[idx]) {
e.preventDefault();
e.stopPropagation();
backPos = player.getPosition().clone();
teleport(player, slots[idx]);
saveWaypoints();
flash('ml-slots');
console.log(`[slot ${idx}] teleported to`, slots[idx]);
return;
}
}
if (e.code === KEYBINDS.fly) {
e.stopPropagation();
if (!featFly) return;
if (flyActive) {
flyUp = true;
} else {
flyOn(kcc);
}
return;
}
if (e.code === KEYBINDS.flyDown && flyActive) { flyDown = true; return; }
if (e.key === 'Shift' && !sprinting && featSprint) { sprinting = true; return; }
if (e.code === KEYBINDS.setHome) {
e.preventDefault();
if (!featWaypoints) return;
homePos = player.getPosition().clone();
saveWaypoints();
flash('ml-home');
return;
}
if (e.code === KEYBINDS.home) {
e.preventDefault();
if (!featWaypoints) return;
if (homePos) {
backPos = player.getPosition().clone();
teleport(player, homePos);
saveWaypoints();
flash('ml-go');
}
return;
}
if (e.code === KEYBINDS.back) {
e.preventDefault();
if (!featWaypoints) return;
if (backPos) {
const cur = player.getPosition().clone(); teleport(player, backPos); backPos = cur;
saveWaypoints();
flash('ml-back');
}
}
}, true);
window.addEventListener('keyup', e => {
if (e.code === KEYBINDS.fly) flyUp = false;
if (e.code === KEYBINDS.flyDown) flyDown = false;
if (e.key === 'Shift' && sprinting) { sprinting = false; }
}, true);
const hud = { fly: $('ml-fly'), spr: $('ml-spr'), home: $('ml-home'), go: $('ml-go'), back: $('ml-back'), slots: $('ml-slots'), lock: $('ml-lock'), tp: $('ml-tp'), petsBtn: $('ml-pets-btn'), cfg: $('ml-cfg'), help: $('ml-help') };
const panels = { plist: $('ml-plist'), settings: $('ml-settings'), dialog: $('ml-dialog'), pets: $('ml-pets') };
setInterval(() => {
hookPetSpawn();
hookAntiDisconnect();
const player = getPlayer();
if (!player) return;
const kcc = getKcc(player);
if (!kcc) return;
const now = Date.now();
const dt = Math.min((now - prevTick) / 1000, 0.1);
prevTick = now;
const pc = getPC(player);
if (sprinting) {
const kb = W.pc?.app?.keyboard;
if (kb && !kb.isPressed(W.pc.KEY_SHIFT)) { sprinting = false; }
}
if (sprinting) {
const sprintMin = SPEED_DEFAULT * 3;
if (sprintSpeed < sprintMin) sprintSpeed = sprintMin;
if (accelEnabled) sprintSpeed = Math.min(sprintSpeed + ACCEL * dt, SPEED_CAP);
setSpeed(pc, kcc, sprintSpeed);
if (pc?.isSlowedSpeed) pc.isSlowedSpeed = false;
} else if (sprintSpeed !== SPEED_DEFAULT) {
sprintSpeed = SPEED_DEFAULT;
setSpeed(pc, kcc, SPEED_DEFAULT);
}
if (hud.fly) { hud.fly.classList.toggle('on', flyActive); hud.fly.classList.toggle('disabled', !featFly); }
if (hud.spr) { hud.spr.classList.toggle('on', sprinting); hud.spr.classList.toggle('disabled', !featSprint); }
if (hud.home) hud.home.classList.toggle('disabled', !featWaypoints);
if (hud.go) hud.go.classList.toggle('disabled', !featWaypoints);
if (hud.back) hud.back.classList.toggle('disabled', !featWaypoints);
if (hud.slots) { const n = slots.filter(Boolean).length; hud.slots.textContent = `SLOTS ${n}/10`; hud.slots.classList.toggle('disabled', !featWaypoints); }
if (hud.tp) { hud.tp.classList.toggle('disabled', !featCuddle); hud.tp.classList.toggle('on', !!panels.plist?.classList.contains('open')); }
if (hud.petsBtn) { hud.petsBtn.classList.toggle('disabled', !featPets); hud.petsBtn.classList.toggle('on', !!panels.pets?.classList.contains('open')); }
if (hud.cfg) hud.cfg.classList.toggle('on', !!panels.settings?.classList.contains('open'));
if (hud.help) hud.help.classList.toggle('on', !!panels.dialog?.classList.contains('open'));
// Auto-lock base: check every ~100ms
if (featAutoLock && now - lastLockCheck > 100) {
lastLockCheck = now;
const lockBtn = getLockBtn();
if (lockBtn) {
const timeLeft = lockBtn.lockdownTimeLeft || 0;
const isActive = lockBtn.isLockdownActive || false;
if (hud.lock) {
hud.lock.classList.toggle('on', isActive && timeLeft > 0);
hud.lock.textContent = (isActive && timeLeft > 0) ? `\ud83d\udd12 ${Math.ceil(timeLeft)}s` : '\ud83d\udd13 UNLOCKED';
}
if (!isActive || timeLeft <= 0) triggerLock();
} else if (hud.lock) {
hud.lock.textContent = '\ud83d\udd12 LOCK';
hud.lock.classList.remove('on');
}
}
if (hud.lock) hud.lock.classList.toggle('disabled', !featAutoLock);
if (cuddling && cuddleTarget && featCuddleFollow) {
const tPos = cuddleTarget.getPosition?.();
if (tPos) {
kcc.gravity = 0;
kcc._velY = 0;
teleport(player, vec3(tPos));
} else {
cuddling = false;
cuddleTarget = null;
console.log('[cuddle] target lost');
}
}
if (!flyActive) return;
if (kcc._grounded) { flyOff(kcc, false); return; }
if (kcc.gravity !== 0) kcc.gravity = 0;
flyVelY = kcc._velY;
if (flyUp) {
if (flyVelY < 0) {
flyVelY = 0;
} else if (accelEnabled) {
if (flyVelY < FLY_MIN_SPEED) flyVelY = FLY_MIN_SPEED;
flyVelY = Math.min(flyVelY + ACCEL * dt, SPEED_CAP);
} else {
flyVelY = SPEED_CAP;
}
} else if (flyDown) {
if (flyVelY > 0) {
flyVelY = 0;
} else if (accelEnabled) {
if (flyVelY > -FLY_MIN_SPEED) flyVelY = -FLY_MIN_SPEED;
flyVelY = Math.max(flyVelY - ACCEL * dt, -SPEED_CAP);
} else {
flyVelY = -SPEED_CAP;
}
} else {
flyVelY = 0;
}
kcc._velY = flyVelY;
}, 16);
})();