Files
GTAConnectedDocs/Trident/menu.js
2026-01-18 20:37:49 +04:00

890 lines
29 KiB
JavaScript

/**
* MD TRIDENT Menu - GTAConnected Port
* Original by DEVILSDESIGN, IIV_NATHAN_VII, SHOCKixiXixiWAVE
* Ported for GTA IV GTAConnected Server
*/
// ============================================================================
// MENU CONFIGURATION & CONSTANTS
// ============================================================================
const MENU_CONFIG = {
// Menu Position
posX: 0.695,
posY: 0.155,
// Menu Dimensions
width: 0.25,
itemHeight: 0.028,
headerHeight: 0.067,
// Max items visible
maxVisibleItems: 12,
startScrolling: 8,
// Colors (RGBA)
colors: {
background: [70, 130, 180, 180], // Steel Blue
header: [164, 134, 35, 255], // Gold
subHeader: [58, 95, 205, 255], // Royal Blue
item: [180, 180, 180, 255], // Gray
itemHighlight: [255, 143, 0, 255], // Orange
scrollbar: [100, 100, 200, 150], // Light Blue
line: [255, 255, 255, 255], // White
bool: [255, 128, 0, 255], // Orange
submenu: [139, 134, 130, 255], // Gray
jumpover: [58, 95, 205, 255], // Blue
error: [177, 19, 26, 255], // Red
boolOn: [0, 204, 0, 255], // Green
boolOff: [204, 0, 0, 255], // Red
},
// Text Sizes
textSize: {
header: 0.45,
subHeader: 0.32,
item: 0.35,
version: 0.95
},
// Header text
headerText: "TRIDENT",
subHeaderText: "MD EXTEND+ v13",
versionText: "MD"
};
// ============================================================================
// MENU ITEM TYPES
// ============================================================================
const ITEM_TYPE = {
SUBMENU: 1,
FUNCTION: 2,
BOOL: 3,
VALUE_NUM: 4,
VALUE_STRING: 5,
VEHICLE: 6,
PLAYER: 7,
JUMPOVER: 8,
ERROR: 9,
NOT_PRESENT: 10
};
// ============================================================================
// MENU STATE
// ============================================================================
let menuState = {
isOpen: false,
currentLevel: 0,
selectedIndex: 0,
scrollOffset: 0,
items: [],
menuStack: [],
lastSelected: [],
glowValue: 0,
glowIncrement: true,
flashValue: 255,
flashIncrement: false,
// Player selection for network options
selectedPlayer: null,
// Input state
inputCooldown: 0,
holdCounter: 0,
pressCounter: 2,
pressMultiplier: 1
};
// ============================================================================
// MENU ITEMS DATA
// ============================================================================
function createMenuItem(name, type, options = {}) {
return {
name: name,
type: type,
action: options.action || null,
submenu: options.submenu || null,
value: options.value !== undefined ? options.value : 0,
maxValue: options.maxValue || 0,
minValue: options.minValue || 0,
boolState: options.boolState || false,
stringValues: options.stringValues || [],
vehicleModel: options.vehicleModel || 0,
playerId: options.playerId || -1
};
}
// ============================================================================
// MAIN MENU STRUCTURE
// ============================================================================
function getMainMenuItems() {
return [
createMenuItem("Player Options", ITEM_TYPE.SUBMENU, { submenu: "player" }),
createMenuItem("Network Options", ITEM_TYPE.SUBMENU, { submenu: "network" }),
createMenuItem("Vehicle Garage", ITEM_TYPE.SUBMENU, { submenu: "vehicle" }),
createMenuItem("Weapon Options", ITEM_TYPE.SUBMENU, { submenu: "weapon" }),
createMenuItem("Teleport Options", ITEM_TYPE.SUBMENU, { submenu: "teleport" }),
createMenuItem("Weather / Time", ITEM_TYPE.SUBMENU, { submenu: "weather" }),
createMenuItem("Model Changer", ITEM_TYPE.SUBMENU, { submenu: "model" }),
createMenuItem("Animations", ITEM_TYPE.SUBMENU, { submenu: "animation" }),
createMenuItem("Object Spawner", ITEM_TYPE.SUBMENU, { submenu: "objects" }),
createMenuItem("~~ M E N U S E T T I N G S ~~", ITEM_TYPE.JUMPOVER),
createMenuItem("Menu Settings", ITEM_TYPE.SUBMENU, { submenu: "settings" }),
createMenuItem("~~ C R E D I T S ~~", ITEM_TYPE.JUMPOVER),
createMenuItem("Credits", ITEM_TYPE.SUBMENU, { submenu: "credits" })
];
}
// ============================================================================
// DRAWING FUNCTIONS
// ============================================================================
function drawRect(x, y, width, height, r, g, b, a) {
natives.DRAW_RECT(x, y, width, height, r, g, b, a);
}
function drawText(text, x, y, size, r, g, b, a, centered = false, rightJustify = false) {
natives.SET_TEXT_FONT(0);
natives.SET_TEXT_SCALE(size, size);
natives.SET_TEXT_COLOUR(r, g, b, a);
if (centered) {
natives.SET_TEXT_CENTRE(true);
}
if (rightJustify) {
natives.SET_TEXT_RIGHT_JUSTIFY(true);
natives.SET_TEXT_WRAP(0.0, x);
}
natives.SET_TEXT_DROPSHADOW(0, 0, 0, 0, 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(x, y, "STRING", text);
}
function setupDraw(sizeX, sizeY, r, g, b, a) {
natives.SET_TEXT_FONT(0);
natives.SET_TEXT_SCALE(sizeX, sizeY);
natives.SET_TEXT_COLOUR(r, g, b, a);
natives.SET_TEXT_DROPSHADOW(0, 0, 0, 0, 255);
}
// ============================================================================
// MENU RENDERING
// ============================================================================
function renderMenu() {
if (!menuState.isOpen) return;
updateEffects();
let posX = MENU_CONFIG.posX;
let posY = MENU_CONFIG.posY;
let width = MENU_CONFIG.width;
// Calculate window height based on visible items
let visibleCount = Math.min(menuState.items.length, MENU_CONFIG.maxVisibleItems);
let windowHeight = MENU_CONFIG.headerHeight + (visibleCount * MENU_CONFIG.itemHeight) + 0.02;
// Draw background window
let bgColor = MENU_CONFIG.colors.background;
drawRect(
posX + width / 2,
posY + windowHeight / 2,
width,
windowHeight,
bgColor[0], bgColor[1], bgColor[2], bgColor[3]
);
// Draw header
renderHeader(posX, posY);
// Draw separator line
let lineY = posY + MENU_CONFIG.headerHeight;
drawRect(
posX + width / 2,
lineY,
width,
0.002,
MENU_CONFIG.colors.line[0],
MENU_CONFIG.colors.line[1],
MENU_CONFIG.colors.line[2],
MENU_CONFIG.colors.line[3]
);
// Draw items
renderItems(posX, lineY + 0.01);
// Draw scroll indicators if needed
if (menuState.items.length > MENU_CONFIG.maxVisibleItems) {
renderScrollIndicators(posX, posY, windowHeight);
}
// Draw helper text at bottom
renderHelperText();
}
function renderHeader(x, y) {
let headerColor = MENU_CONFIG.colors.header;
let subHeaderColor = MENU_CONFIG.colors.subHeader;
// MD Text (version prefix)
setupDraw(0.42, 0.95, subHeaderColor[0], subHeaderColor[1], subHeaderColor[2], 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(x + 0.02, y + 0.005, "STRING", MENU_CONFIG.versionText);
// Header Text (TRIDENT)
setupDraw(0.303, 0.45, headerColor[0], headerColor[1], headerColor[2], menuState.flashValue);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(x + 0.055, y + 0.01, "STRING", MENU_CONFIG.headerText);
// Sub Header (version/description)
setupDraw(0.2, 0.32, subHeaderColor[0], subHeaderColor[1], subHeaderColor[2], 255);
natives.SET_TEXT_CENTRE(true);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(x + MENU_CONFIG.width / 2, y + 0.04, "STRING", MENU_CONFIG.subHeaderText);
}
function renderItems(startX, startY) {
let items = menuState.items;
let scrollOffset = menuState.scrollOffset;
let selectedIndex = menuState.selectedIndex;
let visibleCount = Math.min(items.length, MENU_CONFIG.maxVisibleItems);
let itemY = startY;
for (let i = 0; i < visibleCount; i++) {
let itemIndex = scrollOffset + i;
if (itemIndex >= items.length) break;
let item = items[itemIndex];
let isSelected = (itemIndex === selectedIndex);
// Draw selection highlight
if (isSelected) {
drawRect(
startX + MENU_CONFIG.width / 2,
itemY + MENU_CONFIG.itemHeight / 2,
MENU_CONFIG.width,
MENU_CONFIG.itemHeight,
menuState.glowValue,
menuState.glowValue,
200,
150
);
}
// Render item based on type
renderItem(item, startX, itemY, isSelected);
itemY += MENU_CONFIG.itemHeight;
}
}
function renderItem(item, x, y, isSelected) {
let textX = x + 0.02;
let textY = y + 0.003;
let color;
let text = item.name;
switch (item.type) {
case ITEM_TYPE.SUBMENU:
color = isSelected ? MENU_CONFIG.colors.itemHighlight : MENU_CONFIG.colors.submenu;
setupDraw(0.19, 0.35, color[0], color[1], color[2], 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(textX, textY, "STRING", "MD " + text + " >>");
break;
case ITEM_TYPE.FUNCTION:
color = isSelected ? MENU_CONFIG.colors.itemHighlight : MENU_CONFIG.colors.item;
setupDraw(0.19, 0.35, color[0], color[1], color[2], 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(textX, textY, "STRING", text);
break;
case ITEM_TYPE.BOOL:
color = item.boolState ? MENU_CONFIG.colors.boolOn : MENU_CONFIG.colors.boolOff;
if (isSelected) color = MENU_CONFIG.colors.itemHighlight;
setupDraw(0.19, 0.35, color[0], color[1], color[2], 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(textX, textY, "STRING", text);
// Draw ON/OFF indicator
let stateText = item.boolState ? "ON" : "OFF";
let stateColor = item.boolState ? MENU_CONFIG.colors.boolOn : MENU_CONFIG.colors.boolOff;
setupDraw(0.19, 0.35, stateColor[0], stateColor[1], stateColor[2], 255);
natives.SET_TEXT_RIGHT_JUSTIFY(true);
natives.SET_TEXT_WRAP(0.0, x + MENU_CONFIG.width - 0.02);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(0.0, textY, "STRING", stateText);
break;
case ITEM_TYPE.VALUE_NUM:
color = isSelected ? MENU_CONFIG.colors.itemHighlight : MENU_CONFIG.colors.item;
setupDraw(0.19, 0.35, color[0], color[1], color[2], 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(textX, textY, "STRING", text);
// Draw value
setupDraw(0.19, 0.35, 255, 255, 255, 255);
natives.SET_TEXT_RIGHT_JUSTIFY(true);
natives.SET_TEXT_WRAP(0.0, x + MENU_CONFIG.width - 0.02);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(0.0, textY, "STRING", "< " + item.value.toString() + " >");
break;
case ITEM_TYPE.VALUE_STRING:
color = isSelected ? MENU_CONFIG.colors.itemHighlight : MENU_CONFIG.colors.item;
setupDraw(0.19, 0.35, color[0], color[1], color[2], 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(textX, textY, "STRING", text);
// Draw string value
let strValue = item.stringValues[item.value] || "None";
setupDraw(0.19, 0.35, 255, 255, 255, 255);
natives.SET_TEXT_RIGHT_JUSTIFY(true);
natives.SET_TEXT_WRAP(0.0, x + MENU_CONFIG.width - 0.02);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(0.0, textY, "STRING", "< " + strValue + " >");
break;
case ITEM_TYPE.VEHICLE:
color = isSelected ? MENU_CONFIG.colors.itemHighlight : MENU_CONFIG.colors.item;
setupDraw(0.19, 0.35, color[0], color[1], color[2], 255);
// Get vehicle display name
let vehName = natives.GET_DISPLAY_NAME_FROM_VEHICLE_MODEL(item.vehicleModel);
let displayName = natives.GET_STRING_FROM_TEXT_FILE(vehName) || text;
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(textX, textY, "STRING", displayName);
break;
case ITEM_TYPE.PLAYER:
// Get player color
let playerColor = getPlayerColor(item.playerId);
if (isSelected) playerColor = MENU_CONFIG.colors.itemHighlight;
setupDraw(0.19, 0.35, playerColor[0], playerColor[1], playerColor[2], 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(textX, textY, "STRING", text);
break;
case ITEM_TYPE.JUMPOVER:
color = MENU_CONFIG.colors.jumpover;
setupDraw(0.21, 0.385, color[0], color[1], color[2], menuState.flashValue);
natives.SET_TEXT_CENTRE(true);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(x + MENU_CONFIG.width / 2, textY, "STRING", text);
break;
case ITEM_TYPE.ERROR:
color = MENU_CONFIG.colors.error;
setupDraw(0.19, 0.35, color[0], color[1], menuState.glowValue, 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(textX, textY, "STRING", text);
break;
case ITEM_TYPE.NOT_PRESENT:
color = MENU_CONFIG.colors.error;
setupDraw(0.19, 0.35, color[0], menuState.glowValue, menuState.glowValue, 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(textX, textY, "STRING", text + " (N/A)");
break;
default:
color = MENU_CONFIG.colors.item;
setupDraw(0.19, 0.35, color[0], color[1], color[2], 255);
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(textX, textY, "STRING", text);
}
}
function renderScrollIndicators(x, y, height) {
// Draw scroll position indicator
let totalItems = menuState.items.length;
let visibleItems = MENU_CONFIG.maxVisibleItems;
let scrollOffset = menuState.scrollOffset;
let scrollBarHeight = height - MENU_CONFIG.headerHeight - 0.04;
let scrollThumbHeight = (visibleItems / totalItems) * scrollBarHeight;
let scrollThumbY = y + MENU_CONFIG.headerHeight + 0.02 + (scrollOffset / (totalItems - visibleItems)) * (scrollBarHeight - scrollThumbHeight);
// Draw scroll track
drawRect(
x + MENU_CONFIG.width - 0.008,
y + MENU_CONFIG.headerHeight + scrollBarHeight / 2,
0.004,
scrollBarHeight,
50, 50, 50, 150
);
// Draw scroll thumb
drawRect(
x + MENU_CONFIG.width - 0.008,
scrollThumbY + scrollThumbHeight / 2,
0.004,
scrollThumbHeight,
MENU_CONFIG.colors.header[0],
MENU_CONFIG.colors.header[1],
MENU_CONFIG.colors.header[2],
255
);
}
function renderHelperText() {
let y = 0.92;
setupDraw(0.15, 0.28, 255, 255, 255, 200);
if (menuState.currentLevel === 0) {
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(0.3, y, "STRING", "UP/DOWN: Navigate | ENTER: Select | BACKSPACE: Close");
} else {
natives.DISPLAY_TEXT_WITH_LITERAL_STRING(0.3, y, "STRING", "UP/DOWN: Navigate | ENTER: Select | BACKSPACE: Back");
}
}
// ============================================================================
// MENU EFFECTS
// ============================================================================
function updateEffects() {
// Glow effect
if (menuState.glowIncrement) {
menuState.glowValue += 3;
if (menuState.glowValue >= 190) {
menuState.glowIncrement = false;
}
} else {
menuState.glowValue -= 3;
if (menuState.glowValue <= 0) {
menuState.glowIncrement = true;
}
}
// Flash effect
if (menuState.flashIncrement) {
menuState.flashValue += 3;
if (menuState.flashValue >= 255) {
menuState.flashIncrement = false;
}
} else {
menuState.flashValue -= 2;
if (menuState.flashValue <= 150) {
menuState.flashIncrement = true;
}
}
}
// ============================================================================
// MENU INPUT HANDLING
// ============================================================================
function handleMenuInput() {
if (menuState.inputCooldown > 0) {
menuState.inputCooldown--;
}
// Open/Close menu with specific key combo (F5 or Insert)
if (natives.IS_GAME_KEYBOARD_KEY_JUST_PRESSED(0x74) || // F5
natives.IS_GAME_KEYBOARD_KEY_JUST_PRESSED(0x2D)) { // Insert
toggleMenu();
return;
}
if (!menuState.isOpen) return;
// Navigation
if (isInputPressed("up")) {
navigateUp();
}
if (isInputPressed("down")) {
navigateDown();
}
if (isInputPressed("left")) {
adjustValue(-1);
}
if (isInputPressed("right")) {
adjustValue(1);
}
// Selection
if (isInputJustPressed("select")) {
selectItem();
menuState.inputCooldown = 8;
}
// Back
if (isInputJustPressed("back")) {
goBack();
menuState.inputCooldown = 8;
}
}
function isInputPressed(action) {
let pressed = false;
let justPressed = false;
switch (action) {
case "up":
pressed = natives.IS_GAME_KEYBOARD_KEY_PRESSED(0x26) || // UP Arrow
natives.IS_BUTTON_PRESSED(0, 1); // DPAD UP
justPressed = natives.IS_GAME_KEYBOARD_KEY_JUST_PRESSED(0x26) ||
natives.IS_BUTTON_JUST_PRESSED(0, 1);
break;
case "down":
pressed = natives.IS_GAME_KEYBOARD_KEY_PRESSED(0x28) || // DOWN Arrow
natives.IS_BUTTON_PRESSED(0, 2); // DPAD DOWN
justPressed = natives.IS_GAME_KEYBOARD_KEY_JUST_PRESSED(0x28) ||
natives.IS_BUTTON_JUST_PRESSED(0, 2);
break;
case "left":
pressed = natives.IS_GAME_KEYBOARD_KEY_PRESSED(0x25) || // LEFT Arrow
natives.IS_BUTTON_PRESSED(0, 3); // DPAD LEFT
justPressed = natives.IS_GAME_KEYBOARD_KEY_JUST_PRESSED(0x25) ||
natives.IS_BUTTON_JUST_PRESSED(0, 3);
break;
case "right":
pressed = natives.IS_GAME_KEYBOARD_KEY_PRESSED(0x27) || // RIGHT Arrow
natives.IS_BUTTON_PRESSED(0, 4); // DPAD RIGHT
justPressed = natives.IS_GAME_KEYBOARD_KEY_JUST_PRESSED(0x27) ||
natives.IS_BUTTON_JUST_PRESSED(0, 4);
break;
}
// Handle press with repeat
if (justPressed) {
menuState.holdCounter = 0;
menuState.pressCounter = 2;
menuState.pressMultiplier = 1;
return true;
}
if (pressed && menuState.inputCooldown === 0) {
menuState.holdCounter++;
if (menuState.holdCounter > 15) {
menuState.pressCounter++;
menuState.pressCounter *= menuState.pressMultiplier;
if (menuState.holdCounter > 40) {
menuState.pressMultiplier = 2;
menuState.holdCounter = 16;
}
if (menuState.pressCounter > 6) {
menuState.inputCooldown = 3;
return true;
}
}
} else if (!pressed) {
menuState.holdCounter = 0;
menuState.pressMultiplier = 1;
}
return false;
}
function isInputJustPressed(action) {
switch (action) {
case "select":
return natives.IS_GAME_KEYBOARD_KEY_JUST_PRESSED(0x0D) || // Enter
natives.IS_BUTTON_JUST_PRESSED(0, 16); // A button
case "back":
return natives.IS_GAME_KEYBOARD_KEY_JUST_PRESSED(0x08) || // Backspace
natives.IS_BUTTON_JUST_PRESSED(0, 32); // B button
}
return false;
}
// ============================================================================
// MENU NAVIGATION
// ============================================================================
function toggleMenu() {
menuState.isOpen = !menuState.isOpen;
if (menuState.isOpen) {
openMenu();
} else {
closeMenu();
}
}
function openMenu() {
menuState.isOpen = true;
menuState.currentLevel = 0;
menuState.items = getMainMenuItems();
menuState.selectedIndex = 0;
menuState.scrollOffset = 0;
menuState.menuStack = [];
menuState.lastSelected = [];
// Block weapon switching while menu is open
let playerPed = natives.GET_PLAYER_PED(natives.GET_PLAYER_ID());
natives.BLOCK_PED_WEAPON_SWITCHING(playerPed, true);
// Play open sound
natives.PLAY_AUDIO_EVENT("FRONTEND_MENU_MP_READY");
// Notification
showNotification("~b~MD TRIDENT ~s~Menu Opened");
}
function closeMenu() {
menuState.isOpen = false;
// Unblock weapon switching
let playerPed = natives.GET_PLAYER_PED(natives.GET_PLAYER_ID());
natives.BLOCK_PED_WEAPON_SWITCHING(playerPed, false);
// Play close sound
natives.PLAY_AUDIO_EVENT("FRONTEND_MENU_MP_UNREADY");
}
function navigateUp() {
// Skip jumpover items
do {
if (menuState.selectedIndex === 0) {
menuState.selectedIndex = menuState.items.length - 1;
// Adjust scroll for wrap-around
if (menuState.items.length > MENU_CONFIG.maxVisibleItems) {
menuState.scrollOffset = menuState.items.length - MENU_CONFIG.maxVisibleItems;
}
} else {
menuState.selectedIndex--;
// Adjust scroll
if (menuState.selectedIndex < menuState.scrollOffset) {
menuState.scrollOffset = menuState.selectedIndex;
}
}
} while (menuState.items[menuState.selectedIndex].type === ITEM_TYPE.JUMPOVER);
natives.PLAY_AUDIO_EVENT("RADIO_RETUNE_BEEP");
}
function navigateDown() {
// Skip jumpover items
do {
if (menuState.selectedIndex === menuState.items.length - 1) {
menuState.selectedIndex = 0;
menuState.scrollOffset = 0;
} else {
menuState.selectedIndex++;
// Adjust scroll
if (menuState.selectedIndex >= menuState.scrollOffset + MENU_CONFIG.maxVisibleItems) {
menuState.scrollOffset = menuState.selectedIndex - MENU_CONFIG.maxVisibleItems + 1;
}
}
} while (menuState.items[menuState.selectedIndex].type === ITEM_TYPE.JUMPOVER);
natives.PLAY_AUDIO_EVENT("RADIO_RETUNE_BEEP");
}
function adjustValue(delta) {
let item = menuState.items[menuState.selectedIndex];
if (item.type === ITEM_TYPE.VALUE_NUM) {
item.value += delta;
if (item.value > item.maxValue) item.value = item.minValue;
if (item.value < item.minValue) item.value = item.maxValue;
natives.PLAY_AUDIO_EVENT("RADIO_RETUNE_BEEP");
} else if (item.type === ITEM_TYPE.VALUE_STRING) {
item.value += delta;
if (item.value >= item.stringValues.length) item.value = 0;
if (item.value < 0) item.value = item.stringValues.length - 1;
natives.PLAY_AUDIO_EVENT("RADIO_RETUNE_BEEP");
}
}
function selectItem() {
let item = menuState.items[menuState.selectedIndex];
if (item.type === ITEM_TYPE.JUMPOVER || item.type === ITEM_TYPE.NOT_PRESENT) {
return;
}
natives.PLAY_AUDIO_EVENT("FRONTEND_MENU_MP_SERVER_HIGHLIGHT");
if (item.type === ITEM_TYPE.SUBMENU) {
// Save current state
menuState.menuStack.push({
items: menuState.items,
selectedIndex: menuState.selectedIndex,
scrollOffset: menuState.scrollOffset
});
// Load submenu
menuState.items = getSubmenuItems(item.submenu);
menuState.selectedIndex = 0;
menuState.scrollOffset = 0;
menuState.currentLevel++;
} else if (item.type === ITEM_TYPE.BOOL) {
item.boolState = !item.boolState;
if (item.action) {
item.action(item.boolState);
}
} else if (item.type === ITEM_TYPE.FUNCTION) {
if (item.action) {
item.action();
}
} else if (item.type === ITEM_TYPE.VALUE_NUM || item.type === ITEM_TYPE.VALUE_STRING) {
if (item.action) {
item.action(item.value);
}
} else if (item.type === ITEM_TYPE.VEHICLE) {
if (item.action) {
item.action(item.vehicleModel);
} else {
spawnVehicle(item.vehicleModel);
}
} else if (item.type === ITEM_TYPE.PLAYER) {
menuState.selectedPlayer = item.playerId;
if (item.action) {
item.action(item.playerId);
}
}
}
function goBack() {
if (menuState.currentLevel === 0) {
closeMenu();
return;
}
// Restore previous state
let prevState = menuState.menuStack.pop();
if (prevState) {
menuState.items = prevState.items;
menuState.selectedIndex = prevState.selectedIndex;
menuState.scrollOffset = prevState.scrollOffset;
menuState.currentLevel--;
}
natives.PLAY_AUDIO_EVENT("RADIO_RETUNE_BEEP");
}
// ============================================================================
// SUBMENU DEFINITIONS
// ============================================================================
function getSubmenuItems(submenuId) {
switch (submenuId) {
case "player":
return getPlayerMenuItems();
case "network":
return getNetworkMenuItems();
case "vehicle":
return getVehicleMenuItems();
case "weapon":
return getWeaponMenuItems();
case "teleport":
return getTeleportMenuItems();
case "weather":
return getWeatherMenuItems();
case "model":
return getModelMenuItems();
case "animation":
return getAnimationMenuItems();
case "objects":
return getObjectMenuItems();
case "settings":
return getSettingsMenuItems();
case "credits":
return getCreditsMenuItems();
// Vehicle sub-categories
case "vehicle_sports":
return getSportsVehicleItems();
case "vehicle_muscle":
return getMuscleVehicleItems();
case "vehicle_suv":
return getSUVVehicleItems();
case "vehicle_sedan":
return getSedanVehicleItems();
case "vehicle_emergency":
return getEmergencyVehicleItems();
case "vehicle_bikes":
return getBikeVehicleItems();
case "vehicle_boats":
return getBoatVehicleItems();
case "vehicle_helicopters":
return getHelicopterVehicleItems();
default:
return [createMenuItem("Not Implemented", ITEM_TYPE.ERROR)];
}
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
function showNotification(text) {
natives.PRINT_STRING_WITH_LITERAL_STRING_NOW("STRING", text, 3000, true);
}
function getPlayerColor(playerId) {
let r = 255, g = 255, b = 255;
// Try to get player color
try {
let result = natives.GET_PLAYER_RGB_COLOUR(playerId);
if (result) {
r = result[0] || 255;
g = result[1] || 255;
b = result[2] || 255;
}
} catch (e) {
// Default white if failed
}
return [r, g, b, 255];
}
function spawnVehicle(modelHash) {
// Get player position and heading
let playerId = natives.GET_PLAYER_ID();
let playerPed = natives.GET_PLAYER_PED(playerId);
let pos = natives.GET_CHAR_COORDINATES(playerPed);
let heading = natives.GET_CHAR_HEADING(playerPed);
// Request model
natives.REQUEST_MODEL(modelHash);
// Wait for model to load (simplified - actual implementation should use async)
let attempts = 0;
while (!natives.HAS_MODEL_LOADED(modelHash) && attempts < 100) {
natives.WAIT(10);
attempts++;
}
if (!natives.HAS_MODEL_LOADED(modelHash)) {
showNotification("~r~Failed to load vehicle model!");
return;
}
// Create vehicle in front of player
let spawnX = pos[0] + Math.sin(heading * Math.PI / 180) * 5;
let spawnY = pos[1] + Math.cos(heading * Math.PI / 180) * 5;
let spawnZ = pos[2];
let vehicle = natives.CREATE_CAR(modelHash, spawnX, spawnY, spawnZ, true, true);
if (vehicle) {
natives.SET_CAR_HEADING(vehicle, heading);
natives.SET_CAR_ON_GROUND_PROPERLY(vehicle);
showNotification("~g~Vehicle spawned!");
}
// Mark model as no longer needed
natives.MARK_MODEL_AS_NO_LONGER_NEEDED(modelHash);
}
// ============================================================================
// EVENT HANDLERS
// ============================================================================
addEventHandler("OnProcess", function(event) {
handleMenuInput();
});
addEventHandler("OnDrawnHUD", function(event) {
renderMenu();
});
// ============================================================================
// EXPORTS
// ============================================================================
// Export menu state and functions for other scripts
this.menuState = menuState;
this.showNotification = showNotification;
this.createMenuItem = createMenuItem;
this.ITEM_TYPE = ITEM_TYPE;
this.toggleMenu = toggleMenu;