diff --git a/README.md b/README.md index dfdc7b8..13166d6 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A comprehensive GTA IV freeroam server for GTAConnected with multiple features i - **World Control** - Weather and time manipulation - **Enhanced Chat** - Private messages, local chat, actions, and OOC chat - **Kill Tracking** - Statistics for kills, deaths, and K/D ratio +- **Interactive Mod Menu** - Press F5 to access a full GUI menu with all features ## Installation @@ -41,9 +42,13 @@ GTAConnected/ ├── chat/ # Enhanced chat system │ ├── meta.xml │ └── server.js - └── teleport/ # Teleportation system + ├── teleport/ # Teleportation system + │ ├── meta.xml + │ └── server.js + └── modmenu/ # Interactive GUI mod menu ├── meta.xml - └── server.js + ├── server.js + └── client.js ``` ## Commands @@ -221,6 +226,71 @@ const admins = [ ]; ``` +## Mod Menu (F5) + +The server includes an interactive GUI mod menu accessible to all players by pressing **F5**. + +### Menu Controls + +| Key | Action | +|-----|--------| +| F5 | Open/Close Menu | +| UP/DOWN | Navigate items | +| ENTER | Select item | +| BACKSPACE/ESC | Go back | +| LEFT/RIGHT | Adjust values | + +### Menu Categories + +**Self Options** +- Restore health/armor +- Get all weapons +- Clear wanted level +- God Mode toggle +- Infinite ammo +- Super jump / Fast run +- Change player skin + +**Vehicle Spawner** +- Sports Cars (Infernus, Turismo, Comet, etc.) +- Super Cars (Entity XF, Adder, Vacca, etc.) +- Muscle Cars (Sabre GT, Dukes, Ruiner, etc.) +- SUVs & Trucks +- Motorcycles +- Emergency Vehicles +- Aircraft & Boats + +**Vehicle Options** +- Repair / Flip / Clean vehicle +- Change vehicle colors +- Drift Mode toggle +- Handling adjustments (Grip, Acceleration, Top Speed) +- Indestructible vehicle +- Nitro boost + +**Network Options** +- View online players +- Teleport to any player +- Spectate players + +**Teleport Locations** +- Quick teleport to 15+ Liberty City locations +- All boroughs covered (Algonquin, Broker, Bohan, Alderney) + +**World Options** +- Set time of day +- Change weather + +**Weapons** +- Get individual weapons +- Get all weapons at once + +**Fun Options** +- Launch yourself into the air +- Spawn random peds +- Ragdoll mode +- Chaos mode + ## Requirements - GTAConnected Server (v1.0.72 or newer) diff --git a/resources/admin/server.js b/resources/admin/server.js index 3ad8e24..dc2149d 100644 --- a/resources/admin/server.js +++ b/resources/admin/server.js @@ -3,11 +3,23 @@ // Handles server moderation, kick/ban, and admin commands // ============================================================================ +// Define colors using toColour +const COLOUR_WHITE = toColour(255, 255, 255, 255); +const COLOUR_RED = toColour(255, 100, 100, 255); +const COLOUR_DARKRED = toColour(255, 50, 50, 255); +const COLOUR_GREEN = toColour(100, 255, 100, 255); +const COLOUR_BLUE = toColour(100, 200, 255, 255); +const COLOUR_YELLOW = toColour(255, 255, 100, 255); +const COLOUR_ORANGE = toColour(255, 200, 100, 255); +const COLOUR_GREY = toColour(200, 200, 200, 255); +const COLOUR_PINK = toColour(255, 150, 100, 255); + // Admin list - add player names or account identifiers here const admins = [ "Admin", "Owner", - "ServerOwner" + "ServerOwner", + "Disaster" ]; // Ban list (stored in memory - use a database for persistence) @@ -27,7 +39,6 @@ addEventHandler("OnPlayerConnect", function(event, client, ip) { for (let i = 0; i < bannedPlayers.length; i++) { if (bannedPlayers[i].ip === ip || bannedPlayers[i].name === client.name) { console.log("[Admin] Banned player attempted to connect: " + client.name + " (" + ip + ")"); - // Kick banned player client.disconnect("You are banned from this server: " + bannedPlayers[i].reason); return; } @@ -36,7 +47,7 @@ addEventHandler("OnPlayerConnect", function(event, client, ip) { addEventHandler("OnPlayerJoined", function(event, client) { if (isAdmin(client)) { - messageClient("[ADMIN] You have administrator privileges", client, [255, 100, 100, 255]); + messageClient("[ADMIN] You have administrator privileges", client, COLOUR_RED); } }); @@ -53,7 +64,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { reportPlayer(client, params); } else { - messageClient("[USAGE] /report ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /report ", client, COLOUR_ORANGE); } return; } @@ -71,7 +82,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { let reason = parts.slice(1).join(" ") || "No reason specified"; kickPlayer(client, targetName, reason); } else { - messageClient("[USAGE] /kick [reason]", client, [255, 200, 100, 255]); + messageClient("[USAGE] /kick [reason]", client, COLOUR_ORANGE); } break; @@ -82,7 +93,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { let reason = parts.slice(1).join(" ") || "No reason specified"; banPlayer(client, targetName, reason); } else { - messageClient("[USAGE] /ban [reason]", client, [255, 200, 100, 255]); + messageClient("[USAGE] /ban [reason]", client, COLOUR_ORANGE); } break; @@ -90,7 +101,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { unbanPlayer(client, params); } else { - messageClient("[USAGE] /unban ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /unban ", client, COLOUR_ORANGE); } break; @@ -98,7 +109,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { mutePlayer(client, params); } else { - messageClient("[USAGE] /mute ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /mute ", client, COLOUR_ORANGE); } break; @@ -106,7 +117,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { unmutePlayer(client, params); } else { - messageClient("[USAGE] /unmute ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /unmute ", client, COLOUR_ORANGE); } break; @@ -114,7 +125,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { freezePlayer(client, params, true); } else { - messageClient("[USAGE] /freeze ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /freeze ", client, COLOUR_ORANGE); } break; @@ -122,7 +133,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { freezePlayer(client, params, false); } else { - messageClient("[USAGE] /unfreeze ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /unfreeze ", client, COLOUR_ORANGE); } break; @@ -130,7 +141,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { slapPlayer(client, params); } else { - messageClient("[USAGE] /slap ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /slap ", client, COLOUR_ORANGE); } break; @@ -139,7 +150,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { teleportToPlayer(client, params); } else { - messageClient("[USAGE] /goto ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /goto ", client, COLOUR_ORANGE); } break; @@ -148,7 +159,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { bringPlayer(client, params); } else { - messageClient("[USAGE] /bring ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /bring ", client, COLOUR_ORANGE); } break; @@ -157,7 +168,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { announce(params); } else { - messageClient("[USAGE] /announce ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /announce ", client, COLOUR_ORANGE); } break; @@ -165,7 +176,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { setAdmin(client, params); } else { - messageClient("[USAGE] /setadmin ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /setadmin ", client, COLOUR_ORANGE); } break; @@ -173,7 +184,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { getPlayerIP(client, params); } else { - messageClient("[USAGE] /getip ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /getip ", client, COLOUR_ORANGE); } break; @@ -188,7 +199,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (parts.length >= 2) { setPlayerHealth(client, parts[0], parseInt(parts[1])); } else { - messageClient("[USAGE] /sethealth ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /sethealth ", client, COLOUR_ORANGE); } } break; @@ -199,7 +210,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (parts.length >= 2) { setPlayerArmour(client, parts[0], parseInt(parts[1])); } else { - messageClient("[USAGE] /setarmour ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /setarmour ", client, COLOUR_ORANGE); } } break; @@ -208,7 +219,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { explodePlayer(client, params); } else { - messageClient("[USAGE] /explode ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /explode ", client, COLOUR_ORANGE); } break; } @@ -217,8 +228,8 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { // Check if player is muted before chat addEventHandler("OnPlayerChat", function(event, client, messageText) { if (isMuted(client)) { - messageClient("[ADMIN] You are muted and cannot chat!", client, [255, 100, 100, 255]); - return false; // Cancel the chat + messageClient("[ADMIN] You are muted and cannot chat!", client, COLOUR_RED); + return false; } }); @@ -250,11 +261,11 @@ function findPlayer(name) { function kickPlayer(admin, targetName, reason) { let target = findPlayer(targetName); if (target) { - message("[ADMIN] " + target.name + " was kicked by " + admin.name + ": " + reason, [255, 100, 100, 255]); + message("[ADMIN] " + target.name + " was kicked by " + admin.name + ": " + reason, COLOUR_RED); target.disconnect("Kicked: " + reason); console.log("[Admin] " + admin.name + " kicked " + target.name + ": " + reason); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } @@ -269,11 +280,11 @@ function banPlayer(admin, targetName, reason) { date: new Date().toISOString() }); - message("[ADMIN] " + target.name + " was banned by " + admin.name + ": " + reason, [255, 50, 50, 255]); + message("[ADMIN] " + target.name + " was banned by " + admin.name + ": " + reason, COLOUR_DARKRED); target.disconnect("Banned: " + reason); console.log("[Admin] " + admin.name + " banned " + target.name + ": " + reason); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } @@ -287,10 +298,10 @@ function unbanPlayer(admin, targetName) { } if (found) { - messageClient("[ADMIN] " + targetName + " has been unbanned", admin, [100, 255, 100, 255]); + messageClient("[ADMIN] " + targetName + " has been unbanned", admin, COLOUR_GREEN); console.log("[Admin] " + admin.name + " unbanned " + targetName); } else { - messageClient("[ADMIN] Player not found in ban list: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found in ban list: " + targetName, admin, COLOUR_RED); } } @@ -300,10 +311,10 @@ function mutePlayer(admin, targetName) { if (mutedPlayers.indexOf(target.name) === -1) { mutedPlayers.push(target.name); } - message("[ADMIN] " + target.name + " was muted by " + admin.name, [255, 150, 100, 255]); + message("[ADMIN] " + target.name + " was muted by " + admin.name, COLOUR_PINK); console.log("[Admin] " + admin.name + " muted " + target.name); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } @@ -314,10 +325,10 @@ function unmutePlayer(admin, targetName) { let index = mutedPlayers.indexOf(name); if (index !== -1) { mutedPlayers.splice(index, 1); - message("[ADMIN] " + name + " was unmuted by " + admin.name, [100, 255, 100, 255]); + message("[ADMIN] " + name + " was unmuted by " + admin.name, COLOUR_GREEN); console.log("[Admin] " + admin.name + " unmuted " + name); } else { - messageClient("[ADMIN] Player is not muted: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player is not muted: " + targetName, admin, COLOUR_RED); } } @@ -326,10 +337,10 @@ function freezePlayer(admin, targetName, freeze) { if (target && target.player) { target.player.frozen = freeze; let status = freeze ? "frozen" : "unfrozen"; - message("[ADMIN] " + target.name + " was " + status + " by " + admin.name, [255, 200, 100, 255]); + message("[ADMIN] " + target.name + " was " + status + " by " + admin.name, COLOUR_ORANGE); console.log("[Admin] " + admin.name + " " + status + " " + target.name); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } @@ -337,12 +348,12 @@ function slapPlayer(admin, targetName) { let target = findPlayer(targetName); if (target && target.player) { let pos = target.player.position; - target.player.position = [pos[0], pos[1], pos[2] + 5]; // Launch up + target.player.position = new Vec3(pos.x, pos.y, pos.z + 5); target.player.health -= 10; - message("[ADMIN] " + target.name + " was slapped by " + admin.name, [255, 200, 100, 255]); + message("[ADMIN] " + target.name + " was slapped by " + admin.name, COLOUR_ORANGE); console.log("[Admin] " + admin.name + " slapped " + target.name); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } @@ -350,11 +361,11 @@ function teleportToPlayer(admin, targetName) { let target = findPlayer(targetName); if (target && target.player && admin.player) { let pos = target.player.position; - admin.player.position = [pos[0] + 2, pos[1], pos[2]]; - messageClient("[ADMIN] Teleported to " + target.name, admin, [100, 255, 100, 255]); + admin.player.position = new Vec3(pos.x + 2, pos.y, pos.z); + messageClient("[ADMIN] Teleported to " + target.name, admin, COLOUR_GREEN); console.log("[Admin] " + admin.name + " teleported to " + target.name); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } @@ -362,19 +373,19 @@ function bringPlayer(admin, targetName) { let target = findPlayer(targetName); if (target && target.player && admin.player) { let pos = admin.player.position; - target.player.position = [pos[0] + 2, pos[1], pos[2]]; - messageClient("[ADMIN] " + admin.name + " brought you to them", target, [255, 200, 100, 255]); - messageClient("[ADMIN] Brought " + target.name + " to you", admin, [100, 255, 100, 255]); + target.player.position = new Vec3(pos.x + 2, pos.y, pos.z); + messageClient("[ADMIN] " + admin.name + " brought you to them", target, COLOUR_ORANGE); + messageClient("[ADMIN] Brought " + target.name + " to you", admin, COLOUR_GREEN); console.log("[Admin] " + admin.name + " brought " + target.name); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } function announce(text) { - message("=================================", [255, 255, 100, 255]); - message("[ANNOUNCEMENT] " + text, [255, 255, 100, 255]); - message("=================================", [255, 255, 100, 255]); + message("=================================", COLOUR_YELLOW); + message("[ANNOUNCEMENT] " + text, COLOUR_YELLOW); + message("=================================", COLOUR_YELLOW); console.log("[Admin] Announcement: " + text); } @@ -383,23 +394,23 @@ function setAdmin(admin, targetName) { if (target) { if (admins.indexOf(target.name) === -1) { admins.push(target.name); - messageClient("[ADMIN] You have been granted admin privileges by " + admin.name, target, [100, 255, 100, 255]); - messageClient("[ADMIN] " + target.name + " is now an admin", admin, [100, 255, 100, 255]); + messageClient("[ADMIN] You have been granted admin privileges by " + admin.name, target, COLOUR_GREEN); + messageClient("[ADMIN] " + target.name + " is now an admin", admin, COLOUR_GREEN); console.log("[Admin] " + admin.name + " made " + target.name + " an admin"); } else { - messageClient("[ADMIN] " + target.name + " is already an admin", admin, [255, 200, 100, 255]); + messageClient("[ADMIN] " + target.name + " is already an admin", admin, COLOUR_ORANGE); } } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } function getPlayerIP(admin, targetName) { let target = findPlayer(targetName); if (target) { - messageClient("[ADMIN] " + target.name + "'s IP: " + target.ip, admin, [200, 200, 255, 255]); + messageClient("[ADMIN] " + target.name + "'s IP: " + target.ip, admin, COLOUR_BLUE); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } @@ -407,9 +418,9 @@ function setPlayerHealth(admin, targetName, health) { let target = findPlayer(targetName); if (target && target.player) { target.player.health = health; - messageClient("[ADMIN] Set " + target.name + "'s health to " + health, admin, [100, 255, 100, 255]); + messageClient("[ADMIN] Set " + target.name + "'s health to " + health, admin, COLOUR_GREEN); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } @@ -417,9 +428,9 @@ function setPlayerArmour(admin, targetName, armour) { let target = findPlayer(targetName); if (target && target.player) { target.player.armour = armour; - messageClient("[ADMIN] Set " + target.name + "'s armour to " + armour, admin, [100, 255, 100, 255]); + messageClient("[ADMIN] Set " + target.name + "'s armour to " + armour, admin, COLOUR_GREEN); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } @@ -428,15 +439,15 @@ function explodePlayer(admin, targetName) { if (target && target.player) { let pos = target.player.position; gta.createExplosion(pos, 0, 10.0); - message("[ADMIN] " + target.name + " was exploded by " + admin.name, [255, 100, 100, 255]); + message("[ADMIN] " + target.name + " was exploded by " + admin.name, COLOUR_RED); console.log("[Admin] " + admin.name + " exploded " + target.name); } else { - messageClient("[ADMIN] Player not found: " + targetName, admin, [255, 100, 100, 255]); + messageClient("[ADMIN] Player not found: " + targetName, admin, COLOUR_RED); } } function showAdmins(client) { - messageClient("=== ONLINE ADMINS ===", client, [255, 200, 100, 255]); + messageClient("=== ONLINE ADMINS ===", client, COLOUR_ORANGE); let clients = getClients(); let onlineAdmins = []; @@ -447,9 +458,9 @@ function showAdmins(client) { } if (onlineAdmins.length > 0) { - messageClient(onlineAdmins.join(", "), client, [100, 255, 100, 255]); + messageClient(onlineAdmins.join(", "), client, COLOUR_GREEN); } else { - messageClient("No admins online", client, [200, 200, 200, 255]); + messageClient("No admins online", client, COLOUR_GREY); } } @@ -457,34 +468,33 @@ function reportPlayer(client, reportMessage) { let clients = getClients(); console.log("[Report] " + client.name + ": " + reportMessage); - // Notify all online admins for (let i = 0; i < clients.length; i++) { if (isAdmin(clients[i])) { - messageClient("[REPORT] " + client.name + ": " + reportMessage, clients[i], [255, 200, 100, 255]); + messageClient("[REPORT] " + client.name + ": " + reportMessage, clients[i], COLOUR_ORANGE); } } - messageClient("[REPORT] Your report has been sent to admins", client, [100, 255, 100, 255]); + messageClient("[REPORT] Your report has been sent to admins", client, COLOUR_GREEN); } function showAdminHelp(client) { - messageClient("=== ADMIN COMMANDS ===", client, [255, 200, 100, 255]); - messageClient("/kick [reason] - Kick a player", client, [200, 200, 200, 255]); - messageClient("/ban [reason] - Ban a player", client, [200, 200, 200, 255]); - messageClient("/unban - Unban a player", client, [200, 200, 200, 255]); - messageClient("/mute - Mute a player", client, [200, 200, 200, 255]); - messageClient("/unmute - Unmute a player", client, [200, 200, 200, 255]); - messageClient("/freeze - Freeze a player", client, [200, 200, 200, 255]); - messageClient("/unfreeze - Unfreeze a player", client, [200, 200, 200, 255]); - messageClient("/slap - Slap a player", client, [200, 200, 200, 255]); - messageClient("/goto - Teleport to a player", client, [200, 200, 200, 255]); - messageClient("/bring - Bring a player to you", client, [200, 200, 200, 255]); - messageClient("/announce - Server announcement", client, [200, 200, 200, 255]); - messageClient("/setadmin - Grant admin rights", client, [200, 200, 200, 255]); - messageClient("/getip - Get player's IP", client, [200, 200, 200, 255]); - messageClient("/sethealth - Set health", client, [200, 200, 200, 255]); - messageClient("/setarmour - Set armour", client, [200, 200, 200, 255]); - messageClient("/explode - Explode a player", client, [200, 200, 200, 255]); + messageClient("=== ADMIN COMMANDS ===", client, COLOUR_ORANGE); + messageClient("/kick [reason] - Kick a player", client, COLOUR_GREY); + messageClient("/ban [reason] - Ban a player", client, COLOUR_GREY); + messageClient("/unban - Unban a player", client, COLOUR_GREY); + messageClient("/mute - Mute a player", client, COLOUR_GREY); + messageClient("/unmute - Unmute a player", client, COLOUR_GREY); + messageClient("/freeze - Freeze a player", client, COLOUR_GREY); + messageClient("/unfreeze - Unfreeze a player", client, COLOUR_GREY); + messageClient("/slap - Slap a player", client, COLOUR_GREY); + messageClient("/goto - Teleport to a player", client, COLOUR_GREY); + messageClient("/bring - Bring a player to you", client, COLOUR_GREY); + messageClient("/announce - Server announcement", client, COLOUR_GREY); + messageClient("/setadmin - Grant admin rights", client, COLOUR_GREY); + messageClient("/getip - Get player's IP", client, COLOUR_GREY); + messageClient("/sethealth - Set health", client, COLOUR_GREY); + messageClient("/setarmour - Set armour", client, COLOUR_GREY); + messageClient("/explode - Explode a player", client, COLOUR_GREY); } console.log("[Admin] Server script loaded!"); diff --git a/resources/chat/server.js b/resources/chat/server.js index 3f68584..ee1194e 100644 --- a/resources/chat/server.js +++ b/resources/chat/server.js @@ -3,16 +3,18 @@ // Enhanced chat system with private messages, colors, and special features // ============================================================================ -// Chat colors for different message types -const chatColors = { - normal: [255, 255, 255, 255], - action: [200, 100, 255, 255], - whisper: [255, 255, 100, 255], - shout: [255, 100, 100, 255], - ooc: [150, 150, 150, 255], - admin: [255, 100, 100, 255], - system: [100, 200, 255, 255] -}; +// Chat colors using toColour for integer format +const COLOUR_WHITE = toColour(255, 255, 255, 255); +const COLOUR_ACTION = toColour(200, 100, 255, 255); +const COLOUR_WHISPER = toColour(255, 255, 100, 255); +const COLOUR_SHOUT = toColour(255, 100, 100, 255); +const COLOUR_OOC = toColour(150, 150, 150, 255); +const COLOUR_ADMIN = toColour(255, 100, 100, 255); +const COLOUR_SYSTEM = toColour(100, 200, 255, 255); +const COLOUR_ORANGE = toColour(255, 200, 100, 255); +const COLOUR_ERROR = toColour(255, 100, 100, 255); +const COLOUR_GRAY = toColour(200, 200, 200, 255); +const COLOUR_LOCAL = toColour(200, 255, 200, 255); // Chat history (for logging purposes) let chatHistory = []; @@ -29,7 +31,7 @@ addEventHandler("OnResourceStart", function(event, resource) { addEventHandler("OnPlayerChat", function(event, client, messageText) { // Format and broadcast the chat message let formattedMessage = client.name + ": " + messageText; - message(formattedMessage, chatColors.normal); + message(formattedMessage, COLOUR_WHITE); // Log to history logChat(client.name, messageText, "chat"); @@ -54,10 +56,10 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (messageText.length > 0) { sendPrivateMessage(client, targetName, messageText); } else { - messageClient("[USAGE] /pm ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /pm ", client, COLOUR_ORANGE); } } else { - messageClient("[USAGE] /pm ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /pm ", client, COLOUR_ORANGE); } break; @@ -65,20 +67,20 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { case "action": if (params && params.length > 0) { let actionMessage = "* " + client.name + " " + params; - message(actionMessage, chatColors.action); + message(actionMessage, COLOUR_ACTION); logChat(client.name, params, "action"); } else { - messageClient("[USAGE] /me ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /me ", client, COLOUR_ORANGE); } break; case "do": if (params && params.length > 0) { let doMessage = "* " + params + " (" + client.name + ")"; - message(doMessage, chatColors.action); + message(doMessage, COLOUR_ACTION); logChat(client.name, params, "do"); } else { - messageClient("[USAGE] /do ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /do ", client, COLOUR_ORANGE); } break; @@ -86,10 +88,10 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { case "s": if (params && params.length > 0) { let shoutMessage = client.name + " shouts: " + params.toUpperCase() + "!"; - message(shoutMessage, chatColors.shout); + message(shoutMessage, COLOUR_SHOUT); logChat(client.name, params, "shout"); } else { - messageClient("[USAGE] /shout ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /shout ", client, COLOUR_ORANGE); } break; @@ -97,10 +99,10 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { case "b": if (params && params.length > 0) { let oocMessage = "(( " + client.name + ": " + params + " ))"; - message(oocMessage, chatColors.ooc); + message(oocMessage, COLOUR_OOC); logChat(client.name, params, "ooc"); } else { - messageClient("[USAGE] /ooc ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /ooc ", client, COLOUR_ORANGE); } break; @@ -109,7 +111,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { sendLocalMessage(client, params); } else { - messageClient("[USAGE] /local ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /local ", client, COLOUR_ORANGE); } break; @@ -118,7 +120,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (params && params.length > 0) { replyToLastPM(client, params); } else { - messageClient("[USAGE] /r ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /r ", client, COLOUR_ORANGE); } break; } @@ -136,22 +138,22 @@ function sendPrivateMessage(sender, targetName, messageText) { if (target) { if (target.index === sender.index) { - messageClient("[PM] You cannot message yourself!", sender, [255, 100, 100, 255]); + messageClient("[PM] You cannot message yourself!", sender, COLOUR_ERROR); return; } // Send to target - messageClient("[PM from " + sender.name + "]: " + messageText, target, chatColors.whisper); + messageClient("[PM from " + sender.name + "]: " + messageText, target, COLOUR_WHISPER); // Confirm to sender - messageClient("[PM to " + target.name + "]: " + messageText, sender, chatColors.whisper); + messageClient("[PM to " + target.name + "]: " + messageText, sender, COLOUR_WHISPER); // Store for reply function lastPMSender[target.index] = sender.index; logChat(sender.name, "-> " + target.name + ": " + messageText, "pm"); } else { - messageClient("[PM] Player not found: " + targetName, sender, [255, 100, 100, 255]); + messageClient("[PM] Player not found: " + targetName, sender, COLOUR_ERROR); } } @@ -171,16 +173,16 @@ function replyToLastPM(client, messageText) { if (target) { sendPrivateMessage(client, target.name, messageText); } else { - messageClient("[PM] The player you're trying to reply to is no longer online", client, [255, 100, 100, 255]); + messageClient("[PM] The player you're trying to reply to is no longer online", client, COLOUR_ERROR); } } else { - messageClient("[PM] No one has messaged you yet", client, [255, 100, 100, 255]); + messageClient("[PM] No one has messaged you yet", client, COLOUR_ERROR); } } function sendLocalMessage(sender, messageText) { if (!sender.player) { - messageClient("[LOCAL] You need to spawn first!", sender, [255, 100, 100, 255]); + messageClient("[LOCAL] You need to spawn first!", sender, COLOUR_ERROR); return; } @@ -197,7 +199,7 @@ function sendLocalMessage(sender, messageText) { if (distance <= localRange) { let localMessage = "(Local) " + sender.name + ": " + messageText; - messageClient(localMessage, client, [200, 255, 200, 255]); + messageClient(localMessage, client, COLOUR_LOCAL); } } } @@ -219,9 +221,9 @@ function findPlayer(name) { } function getDistance(pos1, pos2) { - let dx = pos1[0] - pos2[0]; - let dy = pos1[1] - pos2[1]; - let dz = pos1[2] - pos2[2]; + let dx = pos1.x - pos2.x; + let dy = pos1.y - pos2.y; + let dz = pos1.z - pos2.z; return Math.sqrt(dx * dx + dy * dy + dz * dz); } diff --git a/resources/freeroam/server.js b/resources/freeroam/server.js index 985508b..074eb65 100644 --- a/resources/freeroam/server.js +++ b/resources/freeroam/server.js @@ -3,6 +3,16 @@ // Handles player spawning, basic commands, and player management // ============================================================================ +// Define colors using toColour +const COLOUR_WHITE = toColour(255, 255, 255, 255); +const COLOUR_RED = toColour(255, 100, 100, 255); +const COLOUR_GREEN = toColour(100, 255, 100, 255); +const COLOUR_BLUE = toColour(100, 200, 255, 255); +const COLOUR_YELLOW = toColour(255, 255, 100, 255); +const COLOUR_ORANGE = toColour(255, 200, 100, 255); +const COLOUR_GREY = toColour(200, 200, 200, 255); +const COLOUR_PINK = toColour(255, 150, 200, 255); + // Spawn points around Liberty City (GTA IV) const spawnPoints = [ { x: -252.0, y: 947.0, z: 15.0, name: "Star Junction" }, @@ -59,11 +69,11 @@ addEventHandler("OnPlayerJoined", function(event, client) { }; // Welcome message - messageClient("[SERVER] Welcome to Liberty City Freeroam, " + client.name + "!", client, [255, 200, 100, 255]); - messageClient("[SERVER] Type /help for available commands", client, [200, 200, 200, 255]); + messageClient("[SERVER] Welcome to Liberty City Freeroam, " + client.name + "!", client, COLOUR_ORANGE); + messageClient("[SERVER] Type /help for available commands", client, COLOUR_GREY); // Spawn the player - spawnPlayer(client); + spawnPlayerAtRandom(client); }); // When a player quits @@ -71,7 +81,7 @@ addEventHandler("OnPlayerQuit", function(event, client, reason) { console.log("[Freeroam] Player quit: " + client.name + " - Reason: " + reason); // Broadcast quit message - message("[SERVER] " + client.name + " has left the server", [255, 150, 100, 255]); + message("[SERVER] " + client.name + " has left the server", COLOUR_PINK); // Clean up player data delete playerData[client.index]; @@ -91,14 +101,14 @@ addEventHandler("OnPedWasted", function(event, ped, killer, weapon) { let killerClient = getClientFromPed(killer); if (killerClient && playerData[killerClient.index]) { playerData[killerClient.index].kills++; - message("[KILL] " + killerClient.name + " killed " + client.name, [255, 100, 100, 255]); + message("[KILL] " + killerClient.name + " killed " + client.name, COLOUR_RED); } } // Respawn after delay setTimeout(function() { - if (client && client.player) { - spawnPlayer(client); + if (client) { + spawnPlayerAtRandom(client); } }, 3000); } @@ -115,15 +125,15 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { case "spawn": case "respawn": - spawnPlayer(client); - messageClient("[SERVER] You have been respawned!", client, [100, 255, 100, 255]); + spawnPlayerAtRandom(client); + messageClient("[SERVER] You have been respawned!", client, COLOUR_GREEN); break; case "kill": case "suicide": if (client.player) { client.player.health = 0; - messageClient("[SERVER] You killed yourself!", client, [255, 100, 100, 255]); + messageClient("[SERVER] You killed yourself!", client, COLOUR_RED); } break; @@ -131,7 +141,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { case "position": if (client.player) { let pos = client.player.position; - messageClient("[POS] X: " + pos[0].toFixed(2) + " Y: " + pos[1].toFixed(2) + " Z: " + pos[2].toFixed(2), client, [200, 200, 255, 255]); + messageClient("[POS] X: " + pos.x.toFixed(2) + " Y: " + pos.y.toFixed(2) + " Z: " + pos.z.toFixed(2), client, COLOUR_BLUE); } break; @@ -147,7 +157,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { case "heal": if (client.player) { client.player.health = 100; - messageClient("[SERVER] You have been healed!", client, [100, 255, 100, 255]); + messageClient("[SERVER] You have been healed!", client, COLOUR_GREEN); } break; @@ -155,7 +165,7 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { case "armor": if (client.player) { client.player.armour = 100; - messageClient("[SERVER] You have been given armour!", client, [100, 200, 255, 255]); + messageClient("[SERVER] You have been given armour!", client, COLOUR_BLUE); } break; @@ -165,28 +175,28 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { if (!isNaN(skinId)) { if (client.player) { client.player.modelIndex = skinId; - messageClient("[SERVER] Skin changed to: " + skinId, client, [200, 200, 255, 255]); + messageClient("[SERVER] Skin changed to: " + skinId, client, COLOUR_BLUE); } } } else { - messageClient("[USAGE] /skin ", client, [255, 200, 100, 255]); + messageClient("[USAGE] /skin ", client, COLOUR_ORANGE); } break; case "weapons": case "giveweapons": giveWeapons(client); - messageClient("[SERVER] You have been given weapons!", client, [255, 200, 100, 255]); + messageClient("[SERVER] You have been given weapons!", client, COLOUR_ORANGE); break; case "flip": if (client.player && client.player.vehicle) { let veh = client.player.vehicle; let rot = veh.rotation; - veh.rotation = [0, 0, rot[2]]; - messageClient("[SERVER] Vehicle flipped!", client, [100, 255, 100, 255]); + veh.rotation = new Vec3(0, 0, rot.z); + messageClient("[SERVER] Vehicle flipped!", client, COLOUR_GREEN); } else { - messageClient("[SERVER] You need to be in a vehicle!", client, [255, 100, 100, 255]); + messageClient("[SERVER] You need to be in a vehicle!", client, COLOUR_RED); } break; @@ -194,16 +204,16 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { case "repair": if (client.player && client.player.vehicle) { client.player.vehicle.health = 1000; - messageClient("[SERVER] Vehicle repaired!", client, [100, 255, 100, 255]); + messageClient("[SERVER] Vehicle repaired!", client, COLOUR_GREEN); } else { - messageClient("[SERVER] You need to be in a vehicle!", client, [255, 100, 100, 255]); + messageClient("[SERVER] You need to be in a vehicle!", client, COLOUR_RED); } break; case "eject": if (client.player && client.player.vehicle) { client.player.removeFromVehicle(); - messageClient("[SERVER] Ejected from vehicle!", client, [200, 200, 255, 255]); + messageClient("[SERVER] Ejected from vehicle!", client, COLOUR_BLUE); } break; @@ -217,15 +227,19 @@ addEventHandler("OnPlayerCommand", function(event, client, command, params) { // FUNCTIONS // ============================================================================ -function spawnPlayer(client) { +function spawnPlayerAtRandom(client) { // Select random spawn point let spawn = spawnPoints[Math.floor(Math.random() * spawnPoints.length)]; // Select random skin let skin = playerSkins[Math.floor(Math.random() * playerSkins.length)]; - // Spawn the player - client.spawn([spawn.x, spawn.y, spawn.z], 0, skin); + // Create spawn position vector + let spawnPos = new Vec3(spawn.x, spawn.y, spawn.z); + + // Spawn the player using the correct function + client.despawnPlayer(); + client.spawnPlayer(spawnPos, 0.0, skin); // Give basic weapons after short delay setTimeout(function() { @@ -245,76 +259,74 @@ function giveStarterWeapons(client) { if (!client || !client.player) return; // Give basic starter weapons (GTA IV weapon IDs) - // Pistol - client.giveWeapon(5, 100); // Pistol with 100 ammo - // SMG - client.giveWeapon(11, 200); // Micro SMG with 200 ammo + client.player.giveWeapon(5, 100); // Pistol with 100 ammo + client.player.giveWeapon(11, 200); // Micro SMG with 200 ammo } function giveWeapons(client) { if (!client || !client.player) return; // GTA IV Weapon IDs - client.giveWeapon(1, 1); // Baseball Bat - client.giveWeapon(2, 1); // Knife - client.giveWeapon(5, 500); // Pistol - client.giveWeapon(6, 500); // Desert Eagle - client.giveWeapon(9, 200); // Shotgun - client.giveWeapon(10, 200); // Combat Shotgun - client.giveWeapon(11, 500); // Micro SMG - client.giveWeapon(12, 500); // SMG - client.giveWeapon(14, 500); // Assault Rifle - client.giveWeapon(15, 500); // Carbine Rifle - client.giveWeapon(16, 100); // Sniper Rifle - client.giveWeapon(18, 20); // RPG - client.giveWeapon(19, 20); // Grenades - client.giveWeapon(20, 20); // Molotov + client.player.giveWeapon(1, 1); // Baseball Bat + client.player.giveWeapon(2, 1); // Knife + client.player.giveWeapon(5, 500); // Pistol + client.player.giveWeapon(6, 500); // Desert Eagle + client.player.giveWeapon(9, 200); // Shotgun + client.player.giveWeapon(10, 200); // Combat Shotgun + client.player.giveWeapon(11, 500); // Micro SMG + client.player.giveWeapon(12, 500); // SMG + client.player.giveWeapon(14, 500); // Assault Rifle + client.player.giveWeapon(15, 500); // Carbine Rifle + client.player.giveWeapon(16, 100); // Sniper Rifle + client.player.giveWeapon(18, 20); // RPG + client.player.giveWeapon(19, 20); // Grenades + client.player.giveWeapon(20, 20); // Molotov } function showHelp(client) { - messageClient("=== AVAILABLE COMMANDS ===", client, [255, 200, 100, 255]); - messageClient("/spawn - Respawn at a random location", client, [200, 200, 200, 255]); - messageClient("/kill - Kill yourself", client, [200, 200, 200, 255]); - messageClient("/pos - Show your current position", client, [200, 200, 200, 255]); - messageClient("/stats - Show your statistics", client, [200, 200, 200, 255]); - messageClient("/players - Show online players", client, [200, 200, 200, 255]); - messageClient("/heal - Restore your health", client, [200, 200, 200, 255]); - messageClient("/armour - Give yourself armour", client, [200, 200, 200, 255]); - messageClient("/weapons - Get all weapons", client, [200, 200, 200, 255]); - messageClient("/skin - Change your skin", client, [200, 200, 200, 255]); - messageClient("/flip - Flip your vehicle", client, [200, 200, 200, 255]); - messageClient("/fix - Repair your vehicle", client, [200, 200, 200, 255]); - messageClient("/eject - Exit vehicle", client, [200, 200, 200, 255]); - messageClient("=== VEHICLE COMMANDS ===", client, [255, 200, 100, 255]); - messageClient("/v - Spawn a vehicle", client, [200, 200, 200, 255]); - messageClient("/dv - Delete your vehicle", client, [200, 200, 200, 255]); - messageClient("=== TELEPORT COMMANDS ===", client, [255, 200, 100, 255]); - messageClient("/tp - Teleport to location", client, [200, 200, 200, 255]); - messageClient("/tplist - List teleport locations", client, [200, 200, 200, 255]); - messageClient("=== WORLD COMMANDS ===", client, [255, 200, 100, 255]); - messageClient("/weather - Change weather", client, [200, 200, 200, 255]); - messageClient("/time - Change time", client, [200, 200, 200, 255]); + messageClient("=== AVAILABLE COMMANDS ===", client, COLOUR_ORANGE); + messageClient("/spawn - Respawn at a random location", client, COLOUR_GREY); + messageClient("/kill - Kill yourself", client, COLOUR_GREY); + messageClient("/pos - Show your current position", client, COLOUR_GREY); + messageClient("/stats - Show your statistics", client, COLOUR_GREY); + messageClient("/players - Show online players", client, COLOUR_GREY); + messageClient("/heal - Restore your health", client, COLOUR_GREY); + messageClient("/armour - Give yourself armour", client, COLOUR_GREY); + messageClient("/weapons - Get all weapons", client, COLOUR_GREY); + messageClient("/skin - Change your skin", client, COLOUR_GREY); + messageClient("/flip - Flip your vehicle", client, COLOUR_GREY); + messageClient("/fix - Repair your vehicle", client, COLOUR_GREY); + messageClient("/eject - Exit vehicle", client, COLOUR_GREY); + messageClient("=== VEHICLE COMMANDS ===", client, COLOUR_ORANGE); + messageClient("/v - Spawn a vehicle", client, COLOUR_GREY); + messageClient("/dv - Delete your vehicle", client, COLOUR_GREY); + messageClient("=== TELEPORT COMMANDS ===", client, COLOUR_ORANGE); + messageClient("/tp - Teleport to location", client, COLOUR_GREY); + messageClient("/tplist - List teleport locations", client, COLOUR_GREY); + messageClient("=== WORLD COMMANDS ===", client, COLOUR_ORANGE); + messageClient("/weather - Change weather", client, COLOUR_GREY); + messageClient("/time - Change time", client, COLOUR_GREY); } function showStats(client) { let data = playerData[client.index]; if (data) { - messageClient("=== YOUR STATISTICS ===", client, [255, 200, 100, 255]); - messageClient("Kills: " + data.kills, client, [100, 255, 100, 255]); - messageClient("Deaths: " + data.deaths, client, [255, 100, 100, 255]); + messageClient("=== YOUR STATISTICS ===", client, COLOUR_ORANGE); + messageClient("Kills: " + data.kills, client, COLOUR_GREEN); + messageClient("Deaths: " + data.deaths, client, COLOUR_RED); let kd = data.deaths > 0 ? (data.kills / data.deaths).toFixed(2) : data.kills.toFixed(2); - messageClient("K/D Ratio: " + kd, client, [200, 200, 255, 255]); + messageClient("K/D Ratio: " + kd, client, COLOUR_BLUE); } } function showOnlinePlayers(client) { let clients = getClients(); - messageClient("=== ONLINE PLAYERS (" + clients.length + ") ===", client, [255, 200, 100, 255]); + messageClient("=== ONLINE PLAYERS (" + clients.length + ") ===", client, COLOUR_ORANGE); for (let i = 0; i < clients.length; i++) { let c = clients[i]; let data = playerData[c.index] || { kills: 0, deaths: 0 }; - messageClient(c.name + " - K: " + data.kills + " D: " + data.deaths, client, [200, 200, 200, 255]); + messageClient(c.name + " - K: " + data.kills + " D: " + data.deaths, client, COLOUR_GREY); } } diff --git a/resources/modmenu/client.js b/resources/modmenu/client.js new file mode 100644 index 0000000..ad7832f --- /dev/null +++ b/resources/modmenu/client.js @@ -0,0 +1,970 @@ +// ============================================================================ +// MOD MENU - Client Side +// Interactive GUI menu for all players +// Press F5 to open/close menu, Arrow keys to navigate, Enter to select +// ============================================================================ + +// Menu state +let menuOpen = false; +let currentMenu = "main"; +let selectedIndex = 0; +let menuStack = []; // For back navigation + +// Menu colors (RGBA) +const colors = { + background: toColour(20, 20, 20, 200), + header: toColour(200, 50, 50, 255), + headerText: toColour(255, 255, 255, 255), + itemBg: toColour(40, 40, 40, 200), + itemBgSelected: toColour(200, 50, 50, 220), + itemText: toColour(255, 255, 255, 255), + itemTextSelected: toColour(255, 255, 255, 255), + footer: toColour(30, 30, 30, 200), + footerText: toColour(180, 180, 180, 255), + subText: toColour(150, 150, 150, 255) +}; + +// Menu dimensions +const menu = { + x: 50, + y: 100, + width: 300, + headerHeight: 40, + itemHeight: 35, + footerHeight: 30, + maxVisibleItems: 12 +}; + +// Scroll offset for long menus +let scrollOffset = 0; + +// Player list cache +let playerList = []; + +// Vehicle handling values +let handlingMods = { + grip: 1.0, + acceleration: 1.0, + topSpeed: 1.0, + braking: 1.0, + driftMode: false +}; + +// ============================================================================ +// MENU DATA STRUCTURE +// ============================================================================ + +const menuData = { + main: { + title: "MOD MENU", + items: [ + { label: "Self Options", action: "submenu", target: "self" }, + { label: "Vehicle Spawner", action: "submenu", target: "vehicles" }, + { label: "Vehicle Options", action: "submenu", target: "vehicleOptions" }, + { label: "Network Options", action: "submenu", target: "network" }, + { label: "Teleport Locations", action: "submenu", target: "teleport" }, + { label: "World Options", action: "submenu", target: "world" }, + { label: "Weapons", action: "submenu", target: "weapons" }, + { label: "Fun Options", action: "submenu", target: "fun" } + ] + }, + + self: { + title: "SELF OPTIONS", + items: [ + { label: "Restore Health", action: "self_health" }, + { label: "Restore Armor", action: "self_armor" }, + { label: "Max Health & Armor", action: "self_max" }, + { label: "Give All Weapons", action: "self_weapons" }, + { label: "Clear Wanted Level", action: "self_wanted" }, + { label: "God Mode", action: "toggle", target: "godMode", state: false }, + { label: "Never Wanted", action: "toggle", target: "neverWanted", state: false }, + { label: "Infinite Ammo", action: "toggle", target: "infiniteAmmo", state: false }, + { label: "Super Jump", action: "toggle", target: "superJump", state: false }, + { label: "Fast Run", action: "toggle", target: "fastRun", state: false }, + { label: "Respawn", action: "self_respawn" }, + { label: "Suicide", action: "self_suicide" }, + { label: "Change Skin", action: "submenu", target: "skins" } + ] + }, + + skins: { + title: "PLAYER SKINS", + items: [ + { label: "Niko Bellic", action: "skin", value: -1667301416 }, + { label: "Roman Bellic", action: "skin", value: -163448165 }, + { label: "Little Jacob", action: "skin", value: 1936355839 }, + { label: "Brucie Kibbutz", action: "skin", value: -1938475496 }, + { label: "Playboy X", action: "skin", value: 970234525 }, + { label: "Johnny Klebitz", action: "skin", value: -1784875845 }, + { label: "Luis Lopez", action: "skin", value: -1403507487 }, + { label: "Police Officer", action: "skin", value: -1320879687 }, + { label: "NOOSE", action: "skin", value: -1306011498 }, + { label: "Paramedic", action: "skin", value: 2136829318 }, + { label: "Firefighter", action: "skin", value: 1616659040 }, + { label: "Business Man", action: "skin", value: -268651930 }, + { label: "Hobo", action: "skin", value: 1943617350 }, + { label: "Random Skin", action: "skin_random" } + ] + }, + + vehicles: { + title: "VEHICLE SPAWNER", + items: [ + { label: "Sports Cars", action: "submenu", target: "veh_sports" }, + { label: "Super Cars", action: "submenu", target: "veh_super" }, + { label: "Muscle Cars", action: "submenu", target: "veh_muscle" }, + { label: "SUVs & Trucks", action: "submenu", target: "veh_suv" }, + { label: "Sedans & Compacts", action: "submenu", target: "veh_sedan" }, + { label: "Motorcycles", action: "submenu", target: "veh_bikes" }, + { label: "Emergency Vehicles", action: "submenu", target: "veh_emergency" }, + { label: "Aircraft", action: "submenu", target: "veh_aircraft" }, + { label: "Boats", action: "submenu", target: "veh_boats" }, + { label: "Special Vehicles", action: "submenu", target: "veh_special" }, + { label: "Delete My Vehicles", action: "vehicle_delete" } + ] + }, + + veh_sports: { + title: "SPORTS CARS", + items: [ + { label: "Infernus", action: "spawn_vehicle", value: "infernus" }, + { label: "Turismo", action: "spawn_vehicle", value: "turismo" }, + { label: "Comet", action: "spawn_vehicle", value: "comet" }, + { label: "Banshee", action: "spawn_vehicle", value: "banshee" }, + { label: "Sultan", action: "spawn_vehicle", value: "sultan" }, + { label: "Coquette", action: "spawn_vehicle", value: "coquette" }, + { label: "Feltzer", action: "spawn_vehicle", value: "feltzer" }, + { label: "F620", action: "spawn_vehicle", value: "f620" }, + { label: "Buffalo", action: "spawn_vehicle", value: "buffalo" } + ] + }, + + veh_super: { + title: "SUPER CARS", + items: [ + { label: "Entity XF", action: "spawn_vehicle", value: "entityxf" }, + { label: "Adder", action: "spawn_vehicle", value: "adder" }, + { label: "Vacca", action: "spawn_vehicle", value: "vacca" }, + { label: "Bullet", action: "spawn_vehicle", value: "bullet" }, + { label: "Cheetah", action: "spawn_vehicle", value: "cheetah" } + ] + }, + + veh_muscle: { + title: "MUSCLE CARS", + items: [ + { label: "Sabre GT", action: "spawn_vehicle", value: "sabregt" }, + { label: "Stallion", action: "spawn_vehicle", value: "stalion" }, + { label: "Vigero", action: "spawn_vehicle", value: "vigero" }, + { label: "Dukes", action: "spawn_vehicle", value: "dukes" }, + { label: "Ruiner", action: "spawn_vehicle", value: "ruiner" }, + { label: "Phoenix", action: "spawn_vehicle", value: "phoenix" }, + { label: "Gauntlet", action: "spawn_vehicle", value: "gauntlet" }, + { label: "Dominator", action: "spawn_vehicle", value: "dominator" } + ] + }, + + veh_suv: { + title: "SUVs & TRUCKS", + items: [ + { label: "Patriot", action: "spawn_vehicle", value: "patriot" }, + { label: "Cavalcade", action: "spawn_vehicle", value: "cavalcade" }, + { label: "Granger", action: "spawn_vehicle", value: "granger" }, + { label: "Huntley", action: "spawn_vehicle", value: "huntley" }, + { label: "Landstalker", action: "spawn_vehicle", value: "landstalker" }, + { label: "Habanero", action: "spawn_vehicle", value: "habanero" }, + { label: "Serrano", action: "spawn_vehicle", value: "serrano" }, + { label: "Rebla", action: "spawn_vehicle", value: "rebla" } + ] + }, + + veh_sedan: { + title: "SEDANS & COMPACTS", + items: [ + { label: "Oracle", action: "spawn_vehicle", value: "oracle" }, + { label: "Schafter", action: "spawn_vehicle", value: "schafter" }, + { label: "Admiral", action: "spawn_vehicle", value: "admiral" }, + { label: "Vincent", action: "spawn_vehicle", value: "vincent" }, + { label: "Presidente", action: "spawn_vehicle", value: "presidente" }, + { label: "Cognoscenti", action: "spawn_vehicle", value: "cognoscenti" }, + { label: "Blista", action: "spawn_vehicle", value: "blista" }, + { label: "Premier", action: "spawn_vehicle", value: "premier" } + ] + }, + + veh_bikes: { + title: "MOTORCYCLES", + items: [ + { label: "NRG 900", action: "spawn_vehicle", value: "nrg900" }, + { label: "PCJ 600", action: "spawn_vehicle", value: "pcj600" }, + { label: "Sanchez", action: "spawn_vehicle", value: "sanchez" }, + { label: "Faggio", action: "spawn_vehicle", value: "faggio" }, + { label: "Bati", action: "spawn_vehicle", value: "bati" }, + { label: "Akuma", action: "spawn_vehicle", value: "akuma" }, + { label: "Double T", action: "spawn_vehicle", value: "double" }, + { label: "Hakuchou", action: "spawn_vehicle", value: "hakuchou" }, + { label: "Hexer", action: "spawn_vehicle", value: "hexer" }, + { label: "Daemon", action: "spawn_vehicle", value: "daemon" } + ] + }, + + veh_emergency: { + title: "EMERGENCY VEHICLES", + items: [ + { label: "Police Cruiser", action: "spawn_vehicle", value: "police" }, + { label: "Police Buffalo", action: "spawn_vehicle", value: "police2" }, + { label: "FBI Car", action: "spawn_vehicle", value: "fbi" }, + { label: "NOOSE Cruiser", action: "spawn_vehicle", value: "noose" }, + { label: "Ambulance", action: "spawn_vehicle", value: "ambulance" }, + { label: "Fire Truck", action: "spawn_vehicle", value: "firetruk" }, + { label: "Enforcer (SWAT)", action: "spawn_vehicle", value: "enforcer" } + ] + }, + + veh_aircraft: { + title: "AIRCRAFT", + items: [ + { label: "Annihilator", action: "spawn_vehicle", value: "annihilator" }, + { label: "Maverick", action: "spawn_vehicle", value: "maverick" }, + { label: "Police Maverick", action: "spawn_vehicle", value: "polmav" }, + { label: "Buzzard", action: "spawn_vehicle", value: "buzzard" }, + { label: "Shamal (Jet)", action: "spawn_vehicle", value: "shamal" } + ] + }, + + veh_boats: { + title: "BOATS", + items: [ + { label: "Jetmax", action: "spawn_vehicle", value: "jetmax" }, + { label: "Marquis", action: "spawn_vehicle", value: "marquis" }, + { label: "Predator", action: "spawn_vehicle", value: "predator" }, + { label: "Tropic", action: "spawn_vehicle", value: "tropic" }, + { label: "Dinghy", action: "spawn_vehicle", value: "dinghy" }, + { label: "Squalo", action: "spawn_vehicle", value: "squalo" }, + { label: "Reefer", action: "spawn_vehicle", value: "reefer" } + ] + }, + + veh_special: { + title: "SPECIAL VEHICLES", + items: [ + { label: "Taxi", action: "spawn_vehicle", value: "taxi" }, + { label: "Stretch Limo", action: "spawn_vehicle", value: "stretch" }, + { label: "Bus", action: "spawn_vehicle", value: "bus" }, + { label: "Trashmaster", action: "spawn_vehicle", value: "trashmaster" }, + { label: "Forklift", action: "spawn_vehicle", value: "forklift" }, + { label: "Caddy", action: "spawn_vehicle", value: "caddy" }, + { label: "Bulldozer", action: "spawn_vehicle", value: "bulldozer" }, + { label: "Phantom", action: "spawn_vehicle", value: "phantom" } + ] + }, + + vehicleOptions: { + title: "VEHICLE OPTIONS", + items: [ + { label: "Repair Vehicle", action: "veh_repair" }, + { label: "Flip Vehicle", action: "veh_flip" }, + { label: "Clean Vehicle", action: "veh_clean" }, + { label: "Max Upgrade", action: "veh_upgrade" }, + { label: "Vehicle Colors", action: "submenu", target: "veh_colors" }, + { label: "--- Handling ---", action: "none" }, + { label: "Drift Mode", action: "toggle", target: "driftMode", state: false }, + { label: "Grip +", action: "handling", target: "grip", delta: 0.2 }, + { label: "Grip -", action: "handling", target: "grip", delta: -0.2 }, + { label: "Acceleration +", action: "handling", target: "acceleration", delta: 0.5 }, + { label: "Acceleration -", action: "handling", target: "acceleration", delta: -0.5 }, + { label: "Top Speed +", action: "handling", target: "topSpeed", delta: 0.3 }, + { label: "Top Speed -", action: "handling", target: "topSpeed", delta: -0.3 }, + { label: "Reset Handling", action: "handling_reset" }, + { label: "--- Special ---", action: "none" }, + { label: "Indestructible", action: "toggle", target: "vehGodMode", state: false }, + { label: "Nitro Boost", action: "veh_nitro" }, + { label: "Super Brakes", action: "toggle", target: "superBrakes", state: false } + ] + }, + + veh_colors: { + title: "VEHICLE COLORS", + items: [ + { label: "Black", action: "veh_color", value: [0, 0] }, + { label: "White", action: "veh_color", value: [1, 1] }, + { label: "Red", action: "veh_color", value: [27, 27] }, + { label: "Blue", action: "veh_color", value: [51, 51] }, + { label: "Yellow", action: "veh_color", value: [42, 42] }, + { label: "Green", action: "veh_color", value: [53, 53] }, + { label: "Orange", action: "veh_color", value: [38, 38] }, + { label: "Purple", action: "veh_color", value: [61, 61] }, + { label: "Pink", action: "veh_color", value: [68, 68] }, + { label: "Gold", action: "veh_color", value: [37, 37] }, + { label: "Chrome", action: "veh_color", value: [120, 120] }, + { label: "Random", action: "veh_color_random" } + ] + }, + + network: { + title: "NETWORK OPTIONS", + items: [ + { label: ">> Refresh Player List <<", action: "refresh_players" }, + { label: "--- Players Online ---", action: "none" } + ] + }, + + teleport: { + title: "TELEPORT LOCATIONS", + items: [ + { label: "-- Algonquin --", action: "none" }, + { label: "Star Junction", action: "teleport", value: { x: -252.0, y: 947.0, z: 15.0 } }, + { label: "Middle Park", action: "teleport", value: { x: -365.0, y: 1163.0, z: 14.0 } }, + { label: "Rotterdam Tower", action: "teleport", value: { x: 237.0, y: 1002.0, z: 18.0 } }, + { label: "Chinatown", action: "teleport", value: { x: -141.0, y: 289.0, z: 14.0 } }, + { label: "Happiness Island", action: "teleport", value: { x: -722.0, y: -17.0, z: 3.0 } }, + { label: "-- Broker --", action: "none" }, + { label: "Broker Bridge", action: "teleport", value: { x: 932.0, y: -495.0, z: 15.0 } }, + { label: "Hove Beach", action: "teleport", value: { x: 1017.0, y: -505.0, z: 19.0 } }, + { label: "Airport", action: "teleport", value: { x: 2140.0, y: 465.0, z: 6.0 } }, + { label: "-- Bohan --", action: "none" }, + { label: "South Bohan", action: "teleport", value: { x: 1243.0, y: -196.0, z: 26.0 } }, + { label: "-- Alderney --", action: "none" }, + { label: "Alderney City", action: "teleport", value: { x: -1149.0, y: 380.0, z: 21.0 } }, + { label: "Westdyke", action: "teleport", value: { x: -1745.0, y: 1157.0, z: 25.0 } }, + { label: "-- Special --", action: "none" }, + { label: "Helipad (High)", action: "teleport", value: { x: -290.0, y: -400.0, z: 81.0 } }, + { label: "Tower Top", action: "teleport", value: { x: 237.0, y: 1002.0, z: 200.0 } } + ] + }, + + world: { + title: "WORLD OPTIONS", + items: [ + { label: "-- Time --", action: "none" }, + { label: "Morning (8:00)", action: "world_time", value: 8 }, + { label: "Noon (12:00)", action: "world_time", value: 12 }, + { label: "Evening (18:00)", action: "world_time", value: 18 }, + { label: "Night (0:00)", action: "world_time", value: 0 }, + { label: "-- Weather --", action: "none" }, + { label: "Extra Sunny", action: "world_weather", value: 0 }, + { label: "Sunny", action: "world_weather", value: 1 }, + { label: "Cloudy", action: "world_weather", value: 3 }, + { label: "Rainy", action: "world_weather", value: 4 }, + { label: "Thunder", action: "world_weather", value: 7 }, + { label: "Foggy", action: "world_weather", value: 6 } + ] + }, + + weapons: { + title: "WEAPONS", + items: [ + { label: "Get All Weapons", action: "weapon_all" }, + { label: "-- Melee --", action: "none" }, + { label: "Baseball Bat", action: "weapon", value: 1 }, + { label: "Knife", action: "weapon", value: 2 }, + { label: "-- Pistols --", action: "none" }, + { label: "Pistol", action: "weapon", value: 5 }, + { label: "Desert Eagle", action: "weapon", value: 6 }, + { label: "-- Shotguns --", action: "none" }, + { label: "Shotgun", action: "weapon", value: 9 }, + { label: "Combat Shotgun", action: "weapon", value: 10 }, + { label: "-- SMGs --", action: "none" }, + { label: "Micro SMG", action: "weapon", value: 11 }, + { label: "SMG", action: "weapon", value: 12 }, + { label: "-- Rifles --", action: "none" }, + { label: "Assault Rifle", action: "weapon", value: 14 }, + { label: "Carbine Rifle", action: "weapon", value: 15 }, + { label: "Sniper Rifle", action: "weapon", value: 16 }, + { label: "-- Explosives --", action: "none" }, + { label: "RPG", action: "weapon", value: 18 }, + { label: "Grenades", action: "weapon", value: 19 }, + { label: "Molotov", action: "weapon", value: 20 } + ] + }, + + fun: { + title: "FUN OPTIONS", + items: [ + { label: "Launch Me Up", action: "fun_launch" }, + { label: "Explode Me", action: "fun_explode" }, + { label: "Spawn Random Ped", action: "fun_ped" }, + { label: "Ragdoll", action: "fun_ragdoll" }, + { label: "Clear Area Peds", action: "fun_clearpeds" }, + { label: "Clear Area Vehicles", action: "fun_clearvehicles" }, + { label: "Chaos Mode", action: "toggle", target: "chaosMode", state: false }, + { label: "Drunk Mode", action: "toggle", target: "drunkMode", state: false } + ] + } +}; + +// Toggle states +let toggleStates = { + godMode: false, + neverWanted: false, + infiniteAmmo: false, + superJump: false, + fastRun: false, + driftMode: false, + vehGodMode: false, + superBrakes: false, + chaosMode: false, + drunkMode: false +}; + +// ============================================================================ +// INPUT HANDLING +// ============================================================================ + +addEventHandler("OnKeyUp", function(event, key, scanCode, mods) { + // F5 to toggle menu + if (key === SDLK_F5) { + menuOpen = !menuOpen; + if (menuOpen) { + currentMenu = "main"; + selectedIndex = 0; + scrollOffset = 0; + menuStack = []; + // Show cursor - use gui if available + if (typeof gui !== "undefined" && gui.showCursor) { + gui.showCursor(true, true); + } + } else { + if (typeof gui !== "undefined" && gui.showCursor) { + gui.showCursor(false, false); + } + } + return; + } + + if (!menuOpen) return; + + // Navigation + if (key === SDLK_UP) { + navigateUp(); + } else if (key === SDLK_DOWN) { + navigateDown(); + } else if (key === SDLK_RETURN || key === SDLK_KP_ENTER) { + selectItem(); + } else if (key === SDLK_BACKSPACE || key === SDLK_ESCAPE) { + goBack(); + } else if (key === SDLK_LEFT) { + adjustValue(-1); + } else if (key === SDLK_RIGHT) { + adjustValue(1); + } +}); + +function navigateUp() { + let items = getCurrentMenuItems(); + do { + selectedIndex--; + if (selectedIndex < 0) { + selectedIndex = items.length - 1; + } + } while (items[selectedIndex] && items[selectedIndex].action === "none"); + + updateScroll(items); +} + +function navigateDown() { + let items = getCurrentMenuItems(); + do { + selectedIndex++; + if (selectedIndex >= items.length) { + selectedIndex = 0; + } + } while (items[selectedIndex] && items[selectedIndex].action === "none"); + + updateScroll(items); +} + +function updateScroll(items) { + if (selectedIndex < scrollOffset) { + scrollOffset = selectedIndex; + } else if (selectedIndex >= scrollOffset + menu.maxVisibleItems) { + scrollOffset = selectedIndex - menu.maxVisibleItems + 1; + } +} + +function goBack() { + if (menuStack.length > 0) { + let prev = menuStack.pop(); + currentMenu = prev.menu; + selectedIndex = prev.index; + scrollOffset = prev.scroll; + } else { + menuOpen = false; + if (typeof gui !== "undefined" && gui.showCursor) { + gui.showCursor(false, false); + } + } +} + +function getCurrentMenuItems() { + if (currentMenu === "network") { + return getNetworkMenuItems(); + } + return menuData[currentMenu] ? menuData[currentMenu].items : []; +} + +function getNetworkMenuItems() { + let items = [ + { label: ">> Refresh Player List <<", action: "refresh_players" }, + { label: "--- Players Online ---", action: "none" } + ]; + + for (let i = 0; i < playerList.length; i++) { + items.push({ + label: playerList[i].name, + action: "submenu", + target: "player_options", + playerData: playerList[i] + }); + } + + return items; +} + +// ============================================================================ +// ACTION HANDLING +// ============================================================================ + +// Selected player for network options +let selectedPlayer = null; + +function selectItem() { + let items = getCurrentMenuItems(); + let item = items[selectedIndex]; + if (!item || item.action === "none") return; + + switch (item.action) { + case "submenu": + if (item.target === "player_options" && item.playerData) { + selectedPlayer = item.playerData; + openPlayerMenu(item.playerData); + } else { + menuStack.push({ menu: currentMenu, index: selectedIndex, scroll: scrollOffset }); + currentMenu = item.target; + selectedIndex = 0; + scrollOffset = 0; + } + break; + + case "toggle": + toggleStates[item.target] = !toggleStates[item.target]; + item.state = toggleStates[item.target]; + triggerNetworkEvent("ModMenu:Toggle", item.target, toggleStates[item.target]); + showNotification(item.label + ": " + (toggleStates[item.target] ? "ON" : "OFF")); + break; + + case "spawn_vehicle": + triggerNetworkEvent("ModMenu:SpawnVehicle", item.value); + showNotification("Spawning: " + item.label); + break; + + case "teleport": + triggerNetworkEvent("ModMenu:Teleport", item.value.x, item.value.y, item.value.z); + showNotification("Teleporting to: " + item.label); + break; + + case "self_health": + triggerNetworkEvent("ModMenu:SelfOption", "health"); + showNotification("Health restored!"); + break; + + case "self_armor": + triggerNetworkEvent("ModMenu:SelfOption", "armor"); + showNotification("Armor restored!"); + break; + + case "self_max": + triggerNetworkEvent("ModMenu:SelfOption", "max"); + showNotification("Max health & armor!"); + break; + + case "self_weapons": + triggerNetworkEvent("ModMenu:SelfOption", "weapons"); + showNotification("All weapons given!"); + break; + + case "self_wanted": + triggerNetworkEvent("ModMenu:SelfOption", "wanted"); + showNotification("Wanted level cleared!"); + break; + + case "self_respawn": + triggerNetworkEvent("ModMenu:SelfOption", "respawn"); + showNotification("Respawning..."); + break; + + case "self_suicide": + triggerNetworkEvent("ModMenu:SelfOption", "suicide"); + break; + + case "skin": + triggerNetworkEvent("ModMenu:ChangeSkin", item.value); + showNotification("Skin changed!"); + break; + + case "skin_random": + triggerNetworkEvent("ModMenu:ChangeSkin", "random"); + showNotification("Random skin applied!"); + break; + + case "veh_repair": + triggerNetworkEvent("ModMenu:VehicleOption", "repair"); + showNotification("Vehicle repaired!"); + break; + + case "veh_flip": + triggerNetworkEvent("ModMenu:VehicleOption", "flip"); + showNotification("Vehicle flipped!"); + break; + + case "veh_clean": + triggerNetworkEvent("ModMenu:VehicleOption", "clean"); + showNotification("Vehicle cleaned!"); + break; + + case "veh_upgrade": + triggerNetworkEvent("ModMenu:VehicleOption", "upgrade"); + showNotification("Vehicle upgraded!"); + break; + + case "veh_nitro": + triggerNetworkEvent("ModMenu:VehicleOption", "nitro"); + showNotification("NITRO BOOST!"); + break; + + case "veh_color": + triggerNetworkEvent("ModMenu:VehicleColor", item.value[0], item.value[1]); + showNotification("Color changed!"); + break; + + case "veh_color_random": + let c1 = Math.floor(Math.random() * 132); + let c2 = Math.floor(Math.random() * 132); + triggerNetworkEvent("ModMenu:VehicleColor", c1, c2); + showNotification("Random color applied!"); + break; + + case "vehicle_delete": + triggerNetworkEvent("ModMenu:DeleteVehicles"); + showNotification("Vehicles deleted!"); + break; + + case "handling": + handlingMods[item.target] = Math.max(0.1, Math.min(5.0, handlingMods[item.target] + item.delta)); + triggerNetworkEvent("ModMenu:Handling", item.target, handlingMods[item.target]); + showNotification(item.target + ": " + handlingMods[item.target].toFixed(1)); + break; + + case "handling_reset": + handlingMods = { grip: 1.0, acceleration: 1.0, topSpeed: 1.0, braking: 1.0, driftMode: false }; + triggerNetworkEvent("ModMenu:HandlingReset"); + showNotification("Handling reset!"); + break; + + case "world_time": + triggerNetworkEvent("ModMenu:WorldTime", item.value); + showNotification("Time set to: " + item.value + ":00"); + break; + + case "world_weather": + triggerNetworkEvent("ModMenu:WorldWeather", item.value); + showNotification("Weather changed!"); + break; + + case "weapon": + triggerNetworkEvent("ModMenu:GiveWeapon", item.value); + showNotification("Weapon given: " + item.label); + break; + + case "weapon_all": + triggerNetworkEvent("ModMenu:SelfOption", "weapons"); + showNotification("All weapons given!"); + break; + + case "refresh_players": + triggerNetworkEvent("ModMenu:GetPlayers"); + showNotification("Refreshing player list..."); + break; + + case "teleport_to_player": + if (selectedPlayer) { + triggerNetworkEvent("ModMenu:TeleportToPlayer", selectedPlayer.id); + showNotification("Teleporting to: " + selectedPlayer.name); + } + break; + + case "fun_launch": + triggerNetworkEvent("ModMenu:Fun", "launch"); + showNotification("LAUNCH!"); + break; + + case "fun_explode": + triggerNetworkEvent("ModMenu:Fun", "explode"); + break; + + case "fun_ped": + triggerNetworkEvent("ModMenu:Fun", "ped"); + showNotification("Ped spawned!"); + break; + + case "fun_ragdoll": + triggerNetworkEvent("ModMenu:Fun", "ragdoll"); + break; + + case "fun_clearpeds": + triggerNetworkEvent("ModMenu:Fun", "clearpeds"); + showNotification("Area cleared!"); + break; + + case "fun_clearvehicles": + triggerNetworkEvent("ModMenu:Fun", "clearvehicles"); + showNotification("Vehicles cleared!"); + break; + } +} + +function adjustValue(direction) { + let items = getCurrentMenuItems(); + let item = items[selectedIndex]; + if (!item) return; + + if (item.action === "handling") { + let delta = item.delta * direction; + handlingMods[item.target] = Math.max(0.1, Math.min(5.0, handlingMods[item.target] + delta)); + triggerNetworkEvent("ModMenu:Handling", item.target, handlingMods[item.target]); + showNotification(item.target + ": " + handlingMods[item.target].toFixed(1)); + } +} + +function openPlayerMenu(playerData) { + menuData.player_options = { + title: playerData.name, + items: [ + { label: "Teleport to Player", action: "teleport_to_player" }, + { label: "Spectate Player", action: "spectate_player" }, + { label: "Copy Position", action: "copy_pos" } + ] + }; + + menuStack.push({ menu: currentMenu, index: selectedIndex, scroll: scrollOffset }); + currentMenu = "player_options"; + selectedIndex = 0; + scrollOffset = 0; +} + +// ============================================================================ +// NETWORK EVENT HANDLERS +// ============================================================================ + +addNetworkHandler("ModMenu:PlayerList", function(players) { + playerList = players; + showNotification("Found " + players.length + " players"); +}); + +addNetworkHandler("ModMenu:Notification", function(message) { + showNotification(message); +}); + +// ============================================================================ +// RENDERING - Using correct GTAC drawing API +// ============================================================================ + +addEventHandler("OnDrawnHUD", function(event) { + if (!menuOpen) return; + + let currentData = menuData[currentMenu]; + let items = getCurrentMenuItems(); + let title = currentData ? currentData.title : currentMenu.toUpperCase(); + + // Calculate visible items + let visibleCount = Math.min(items.length, menu.maxVisibleItems); + let totalHeight = menu.headerHeight + (visibleCount * menu.itemHeight) + menu.footerHeight; + + // Draw background + drawRect(menu.x - 5, menu.y - 5, menu.width + 10, totalHeight + 10, colors.background); + + // Draw header + drawRect(menu.x, menu.y, menu.width, menu.headerHeight, colors.header); + drawText(title, menu.x + menu.width / 2, menu.y + 10, colors.headerText, true, 1.0); + + // Draw items + let yPos = menu.y + menu.headerHeight; + for (let i = scrollOffset; i < scrollOffset + visibleCount && i < items.length; i++) { + let item = items[i]; + let isSelected = (i === selectedIndex); + let bgColor = isSelected ? colors.itemBgSelected : colors.itemBg; + let textColor = isSelected ? colors.itemTextSelected : colors.itemText; + + // Draw item background + drawRect(menu.x, yPos, menu.width, menu.itemHeight, bgColor); + + // Build label with state indicators + let label = item.label; + + if (item.action === "toggle") { + let state = toggleStates[item.target]; + label += state ? " [ON]" : " [OFF]"; + } + + if (item.action === "handling" && handlingMods[item.target] !== undefined) { + label += " [" + handlingMods[item.target].toFixed(1) + "]"; + } + + if (item.action === "submenu") { + label += " >>"; + } + + drawText(label, menu.x + 15, yPos + 8, textColor, false, 0.9); + + yPos += menu.itemHeight; + } + + // Draw footer + drawRect(menu.x, yPos, menu.width, menu.footerHeight, colors.footer); + drawText("UP/DOWN: Navigate | ENTER: Select | BACKSPACE: Back", menu.x + menu.width / 2, yPos + 8, colors.footerText, true, 0.6); + + // Draw scroll indicator + if (items.length > menu.maxVisibleItems) { + let scrollText = (scrollOffset + 1) + "-" + Math.min(scrollOffset + visibleCount, items.length) + " / " + items.length; + drawText(scrollText, menu.x + menu.width - 50, menu.y + 12, colors.subText, false, 0.7); + } +}); + +// Helper drawing functions using native drawing +function drawRect(x, y, width, height, colour) { + // Use natives for drawing rectangles + if (typeof natives !== "undefined" && natives.drawRect) { + // GTA IV native drawing - normalized coordinates (0-1) + let screenW = game.width || 1920; + let screenH = game.height || 1080; + + let nx = (x + width/2) / screenW; + let ny = (y + height/2) / screenH; + let nw = width / screenW; + let nh = height / screenH; + + // Extract RGBA from colour + let r = (colour >> 24) & 0xFF; + let g = (colour >> 16) & 0xFF; + let b = (colour >> 8) & 0xFF; + let a = colour & 0xFF; + + natives.drawRect(nx, ny, nw, nh, r, g, b, a); + } else { + // Fallback to graphics drawing + try { + graphics.drawRectangle(null, [x, y], [width, height], colour, colour, 0, 0, 0); + } catch(e) { + // Silent fail + } + } +} + +function drawText(text, x, y, colour, centered, scale) { + // Use natives for drawing text + if (typeof natives !== "undefined" && natives.setTextScale) { + let screenW = game.width || 1920; + let screenH = game.height || 1080; + + let nx = x / screenW; + let ny = y / screenH; + + let r = (colour >> 24) & 0xFF; + let g = (colour >> 16) & 0xFF; + let b = (colour >> 8) & 0xFF; + let a = colour & 0xFF; + + natives.setTextFont(0); + natives.setTextScale(scale || 0.35, scale || 0.35); + natives.setTextColour(r, g, b, a); + if (centered) { + natives.setTextCentre(true); + } + natives.setTextDropshadow(2, 0, 0, 0, 255); + natives.displayTextWithLiteralString(nx, ny, "STRING", text); + } else { + // Fallback + try { + graphics.drawText(text, [x, y], colour, scale || 1.0, "arial", centered || false); + } catch(e) { + // Silent fail + } + } +} + +// ============================================================================ +// NOTIFICATIONS +// ============================================================================ + +let notifications = []; + +function showNotification(text) { + notifications.push({ + text: text, + time: Date.now(), + duration: 3000 + }); +} + +// Draw notifications +addEventHandler("OnDrawnHUD", function(event) { + let now = Date.now(); + let yPos = 200; + + for (let i = 0; i < notifications.length; i++) { + let notif = notifications[i]; + let elapsed = now - notif.time; + + if (elapsed < notif.duration) { + let alpha = elapsed < notif.duration - 500 ? 200 : Math.floor(200 * (notif.duration - elapsed) / 500); + let bgColor = toColour(20, 20, 20, alpha); + let textColor = toColour(255, 255, 100, alpha + 55); + + drawRect(10, yPos, 300, 30, bgColor); + drawText(notif.text, 20, yPos + 6, textColor, false, 0.8); + + yPos += 35; + } + } + + // Clean expired notifications + notifications = notifications.filter(function(n) { + return now - n.time < n.duration; + }); +}); + +// ============================================================================ +// TOGGLE EFFECTS (Client-side processing) +// ============================================================================ + +addEventHandler("OnProcess", function(event) { + if (!localPlayer) return; + + // God Mode + if (toggleStates.godMode) { + localPlayer.health = 200; + localPlayer.armour = 100; + } + + // Never Wanted + if (toggleStates.neverWanted) { + localPlayer.wantedLevel = 0; + } + + // Vehicle God Mode + if (localPlayer.vehicle && toggleStates.vehGodMode) { + localPlayer.vehicle.health = 1000; + } +}); + +// ============================================================================ +// INITIALIZATION +// ============================================================================ + +addEventHandler("OnResourceStart", function(event, resource) { + console.log("[ModMenu] Client script loaded!"); + console.log("[ModMenu] Press F5 to open the menu"); +}); + +console.log("[ModMenu] Client loaded - Press F5 to open menu!"); diff --git a/resources/modmenu/meta.xml b/resources/modmenu/meta.xml new file mode 100644 index 0000000..ae8d132 --- /dev/null +++ b/resources/modmenu/meta.xml @@ -0,0 +1,6 @@ + + + +