mirror of
https://github.com/iDisaster/GTAConnected.git
synced 2026-03-08 09:25:23 +00:00
890 lines
29 KiB
JavaScript
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;
|