├── README.md ├── client ├── client.lua ├── example_code.lua └── pedPersonality.lua ├── config.lua ├── fxmanifest.lua └── ui ├── app.js ├── index.html ├── styles.css └── themes ├── dark-theme.css ├── envi-theme.css ├── ox-theme.css ├── purple-theme.css └── stix-theme.css /README.md: -------------------------------------------------------------------------------- 1 | ## Documentation for `envi-interact` 2 | 3 | The `envi-interact` script provides a versatile interaction system for creating dynamic menus (such as multi-choice menus, speech bubbles, percentage bars, and sliders), easily handling NPC interactions, providing an optimized solution to 'Press E' interactions within in your FiveM Server. 4 | 5 | ![Alt text](https://media.discordapp.net/attachments/1174950183570776075/1250773951320162399/image.png?ex=666d7b04&is=666c2984&hm=578ece92a1ba05e1008580266d4db442d6766e102234dadf7ff3eccf3aa2f5e1&=&format=webp&quality=lossless&width=2493&height=1334) 6 | 7 | 8 | Below is a detailed guide on how to use the exported functions from this script in your other lua scripts/resources. 9 | 10 | ### Exported Functions 11 | 12 | #### 1. `OpenChoiceMenu` 13 | Opens a menu with multiple choice options. 14 | 15 | **Parameters:** 16 | - `data` (table): A table containing the menu configuration. 17 | - `menuID` (string): A unique identifier for the menu. - NOTE: MUST BE UNIQUE TO AVOID CONFLICTS 18 | - `title` (string): The title of the menu. 19 | - `speech` (string): A speech or description associated with the menu. If false, the menu will be a simple choice menu. 20 | - `speechOptions` (table): A table of speech options (more added soon) 21 | - `duration` (number): The duration it takes for the speech to show from start to end 22 | - `position` (string): The position on the screen (e.g., 'left', 'right'). 23 | - `timeout` (table): A table containing timeout configuration with keys `time` (number) and `closeEverything` (boolean). 24 | - `onESC` (function): A function to be called when the ESC key is used to close the menu. 25 | - `options` (table): A list of options, each being a table with keys `key`, `label`, `selected`, `closeAll`, `speech`, and `reaction`. 26 | 27 | 28 | NOTE: 29 | Some options are only avaliable when use PedInteraction or CreateNPC ChoiceMenus such as: 30 | - `reaction` (string): The VOICE PARAM to use when the option is selected. 31 | - `speech` (string): A speech to display when the option is selected. 32 | 33 | **Returns:** 34 | - `string`: The key of the selected option if not using callbacks in options. 35 | 36 | **Example using 'selected' functions:** 37 | ```lua 38 | exports['envi-interact']:OpenChoiceMenu({ 39 | title = 'Decision Time', 40 | menuID = 'decision-menu', 41 | position = 'right', 42 | timeout = {time = 60, closeEverything = true}, 43 | onESC = function() 44 | print('ESC key pressed') 45 | end, 46 | options = { 47 | { 48 | key = 'A', 49 | label = 'Option A', 50 | selected = function() 51 | print('Option A selected') 52 | end 53 | }, 54 | { 55 | key = 'B', 56 | label = 'Option B', 57 | selected = function() 58 | print('Option B selected') 59 | end 60 | } 61 | } 62 | }) 63 | ``` 64 | 65 | **Example returning option Chosen/ Key Pressed:** 66 | ```lua 67 | local optionChosen = exports['envi-interact']:OpenChoiceMenu({ 68 | title = 'Decision Time', 69 | menuID = 'simple-decision-menu', 70 | position = 'right', 71 | onESC = function() 72 | print('ESC key pressed') 73 | end, 74 | options = { 75 | { 76 | key = 'A', 77 | label = 'Option A', 78 | speech = 'You chose Option A.', 79 | }, 80 | { 81 | key = 'B', 82 | label = 'Option B', 83 | speech = 'You chose Option B.', 84 | } 85 | } 86 | }) 87 | if optionChosen == 'A' then 88 | print('Option A selected') 89 | elseif optionChosen == 'B' then 90 | print('Option B selected') 91 | end 92 | ``` 93 | 94 | 95 | #### 2. `CreateNPC` 96 | Creates an all-in-one NPC with specified attributes and interaction options. 97 | This will: 98 | - Spawn the NPC 99 | - Set up Press E to interact keybind for the NPC 100 | - Set up a menu for the NPC to interact with the player 101 | 102 | **Parameters:** 103 | - `pedData` (table): Data about the NPC model and spawn location. 104 | - `model` (string): Model name of the NPC. 105 | - `coords` (vector3): Coordinates where the NPC will spawn. 106 | - `heading` (number): Direction the NPC faces. 107 | - `isFrozen` (boolean): Whether the NPC should be immobile. 108 | - `interactionData` (table): Interaction options and UI settings. 109 | - `title`, `speech`, `speechOptions`, `menuID`, `position`, `timeout`, `options`, `onESC` as in `OpenChoiceMenu`. 110 | - `focusCam` (boolean): Whether the camera should focus on the NPC when interacting. 111 | - `greeting` (string): The VOICE PARAM to use when interacting. 112 | 113 | **Returns:** 114 | - `entity`: The spawned NPC entity. 115 | 116 | **Example:** 117 | ```lua 118 | local npc = exports['envi-interact']:CreateNPC({ -- Table of NPC Attributes (pedData) 119 | model = 'a_m_m_business_01', 120 | coords = vector3(-138.9195, -633.8308, 168.8205), 121 | heading = 90, 122 | isFrozen = true 123 | }, { -- Table of Choice Menu Data (interactionData) 124 | title = 'Greetings', 125 | speech = 'Hello there! Let\'s choose an option. What would you like to talk about?', 126 | speechOptions = { -- table of speech options (more added soon) 127 | duration = 2000, 128 | }, 129 | menuID = 'npc-interaction-menu-1', 130 | position = 'right', 131 | greeting = 'GENERIC_HI', 132 | timeout = {time = 60}, 133 | focusCam = true, 134 | onESC = function() 135 | print('ESC key pressed') 136 | end, 137 | options = { -- Table of Choice Menu Options 138 | { 139 | key = 'E', 140 | label = 'Talk about the weather', 141 | reaction = 'GENERIC_SHOCKED_MED', 142 | selected = function(data) -- data is a table of the current menu data 143 | print('Talking about the weather...') 144 | exports['envi-interact']:CloseMenu(data.menuID) -- To close the current menu after interaction 145 | end 146 | }, 147 | { 148 | key = 'F', 149 | label = 'Talk about sports', 150 | reaction = 'GENERIC_SHOCKED_HIGH', 151 | selected = function(data) 152 | print('Talking about sports...') 153 | exports['envi-interact']:CloseAllMenus() -- To close all menus after interaction 154 | end 155 | }, 156 | { 157 | key = 'X', 158 | label = 'Leave', 159 | selected = function(data) 160 | print('Leaving the conversation...') 161 | exports['envi-interact']:CloseEverything() -- To close all menus and percentage bars after interaction 162 | end 163 | } 164 | } 165 | }) 166 | ``` 167 | 168 | 169 | 170 | #### 3. `PedInteraction` 171 | Handles interactions with a ped, typically used to initiate dialogues or actions. 172 | 173 | **Parameters:** 174 | - `entity` (entity): The ped entity to interact with. 175 | - `data` (table): Interaction options and UI settings. 176 | - `title`, `speech`, `speechOptions`, `menuID`, `position`, `timeout`, `options`, `onESC` as in `OpenChoiceMenu`. 177 | - `focusCam` (boolean): Whether the camera should focus on the NPC when interacting. 178 | - `greeting` (string): The VOICE PARAM to use when interacting. 179 | - `freeze` (boolean): Whether the NPC should be frozen during interaction. 180 | 181 | 182 | **Example:** 183 | ```lua 184 | exports['envi-interact']:PedInteraction(ped, { 185 | title = 'Greetings', 186 | speech = 'Hello there! Let\'s choose an option. What would you like to talk about?', 187 | menuID = 'npc-interaction', 188 | position = 'right', 189 | greeting = 'GENERIC_HI', 190 | focusCam = true, 191 | onESC = function() 192 | print('ESC key pressed') 193 | end, 194 | options = { 195 | { 196 | key = 'E', 197 | label = 'Talk', 198 | reaction = 'CHAT_STATE', 199 | selected = function(data) 200 | print('Initiating conversation...') 201 | exports['envi-interact']:CloseMenu(data.menuID) -- To close the current menu after interaction 202 | end, 203 | }, 204 | { 205 | key = 'I', 206 | label = 'Insult', 207 | reaction = 'GENERIC_SHOCKED_HIGH', 208 | selected = function(data) 209 | print('Insulting the ped...') 210 | exports['envi-interact']:CloseEverything() -- To close all menus and percentage bars after interaction 211 | end 212 | } 213 | } 214 | }) 215 | ``` 216 | 217 | 218 | #### 4. `PercentageBar` 219 | Displays a percentage bar on the screen. 220 | 221 | **Parameters:** 222 | - `menuID` (string): A unique identifier for the percentage bar. 223 | - `percent` (number): The percentage value to display (0-100). 224 | - `title` (string): The title of the percentage bar. 225 | - `position` (string): The position on the screen. 226 | - `tooltip` (string, optional): Tooltip behavior ('hover', 'always', 'none'). 227 | - `c1`, `c2`, `c3` (string, optional): Color values for different percentage ranges. 228 | 229 | **Returns:** 230 | - `string`: The menu ID of the percentage bar. 231 | 232 | **Example:** 233 | ```lua 234 | exports['envi-interact']:PercentageBar('relationship-bar', 75, 'Relationship Status', 'top', 'hover') 235 | ``` 236 | 237 | 238 | #### 5. `UseSlider` 239 | Allows interaction with a slider within a menu. 240 | 241 | **Parameters:** 242 | - `menuID` (string): The ID of the menu containing the slider. 243 | - `data` (table): Configuration for the slider. 244 | - `title` (string): Title of the slider. 245 | - `min`, `max` (number): Minimum and maximum values. 246 | - `sliderState` (string): State of the slider ('locked', 'unlocked', 'disabled'). 247 | - `sliderValue` (number): Initial value of the slider. 248 | - `nextState` (string): State after interaction. 249 | - `confirm` (function): Callback function executed on confirmation. 250 | 251 | **Example:** 252 | ```lua 253 | exports['envi-interact']:UseSlider('decision-menu', { 254 | title = 'Adjust Value', 255 | min = 1, 256 | max = 100, 257 | sliderState = 'unlocked', 258 | sliderValue = 50, 259 | nextState = 'locked', 260 | confirm = function(newVal, oldVal) 261 | -- Do something when clicking submit 262 | print('Value changed from', oldVal, 'to', newVal) 263 | end 264 | }) 265 | ``` 266 | 267 | 268 | #### 6. `CloseMenu` 269 | Closes a specific menu by its ID. 270 | 271 | **Parameters:** 272 | - `menuID` (string): The ID of the menu to close. - NOTE: NEEDS TO MATCH THE MENU ID OF THE OPEN MENU 273 | 274 | **Example:** 275 | ```lua 276 | exports['envi-interact']:CloseMenu('decision-menu') 277 | ``` 278 | 279 | 280 | #### 7. `CloseAllMenus` 281 | Closes all currently open menus. 282 | 283 | **Example:** 284 | ```lua 285 | exports['envi-interact']:CloseAllMenus() 286 | ``` 287 | 288 | 289 | #### 8. `CloseAllPercentBars` 290 | Closes all open percentage bars. 291 | 292 | **Example:** 293 | ```lua 294 | exports['envi-interact']:CloseAllPercentBars() 295 | ``` 296 | 297 | 298 | #### 9. `GetOpenMenus` 299 | Returns a table of all open menus. 300 | 301 | **Example:** 302 | ```lua 303 | local openMenus = exports['envi-interact']:GetOpenMenus() 304 | print(json.encode(openMenus, { indent = true })) 305 | ``` 306 | 307 | 308 | #### 10. `IsAnyMenuOpen` 309 | Returns a boolean value indicating if any menus are open. 310 | 311 | **Example:** 312 | ```lua 313 | local isAnyMenuOpen = exports['envi-interact']:IsAnyMenuOpen() 314 | print(isAnyMenuOpen) 315 | ``` 316 | 317 | #### 11. `IsAnyPercentBarOpen` 318 | Returns a boolean value indicating if any percentage bars are open. 319 | 320 | **Example:** 321 | ```lua 322 | local isAnyPercentBarOpen = exports['envi-interact']:IsAnyPercentBarOpen() 323 | print(isAnyPercentBarOpen) 324 | ``` 325 | 326 | #### 12. `GetInteractionPed` 327 | Returns the ped entity that is interacting with the player. 328 | 329 | **Parameters:** 330 | - `menuID` (string): The ID of the menu to get the ped entity from. 331 | 332 | **Example:** 333 | ```lua 334 | local interactionPed = exports['envi-interact']:GetInteractionPed('npc-interaction-menu-1') 335 | print('entity = ', interactionPed) 336 | ``` 337 | 338 | #### 13. `InteractionPoint` and `InteractionEntity` 339 | These functions enable a raycasting-based interaction system, allowing players to press 'E' to interact with points or entities in the game world. This system supports multiple options which you may select using the scroll-wheel and runs at 0.00ms constantly, ensuring minimal performance impact without the use of a target system. 340 | 341 | 342 | **InteractionPoint Parameters:** 343 | - `position` (vector3): The position to check for interactions. 344 | - `options` (table): Interaction options. 345 | - `name` (string): The name of the interaction point. 346 | - `distance` (number): The maximum distance at which the interaction point will be active. 347 | - `radius` (number): The radius around the interaction point that will be active. 348 | 349 | **Example:** 350 | ```lua 351 | -- Example of using InteractionPoint 352 | exports['envi-interact']:InteractionPoint(vector3(100, 100, 20), { 353 | { 354 | label = 'Interaction Point - Choice 1', 355 | selected = function(data) 356 | -- Additional logic can be added here to handle the result 357 | print('Interacting with point - selected choice 1...') 358 | exports['envi-interact']:CloseMenu(data.menuID) -- To close the current menu after interaction 359 | end 360 | }, 361 | { 362 | label = 'Interaction Point - Choice 2', 363 | selected = function() 364 | print('Interacting with point - scrolled down and selected choice 2...') 365 | exports['envi-interact']:CloseMenu(data.menuID) -- To close the current menu after interaction 366 | end 367 | }, 368 | { 369 | label = 'Interaction Point - Choice 3', 370 | selected = function(data) 371 | print('Interacting with point - scrolled down and selected choice 3...') 372 | exports['envi-interact']:CloseMenu(data.menuID) -- To close the current menu after interaction 373 | end 374 | }, 375 | { 376 | label = 'Interaction Point - Choice 4', 377 | selected = function(data) 378 | print('Interacting with point - scrolled down and selected choice 4...') 379 | exports['envi-interact']:CloseMenu(data.menuID) -- To close the current menu after interaction 380 | end 381 | }, 382 | }) 383 | ``` 384 | 385 | 386 | **InteractionEntity Parameters:** 387 | - `entity` (entity, optional): The specific entity to interact with. 388 | - `options` (table): Interaction options. 389 | - `name` (string): The name of the interaction point. 390 | - `distance` (number): The maximum distance at which the interaction point will be active. 391 | - `radius` (number): The radius around the interaction point that will be active. 392 | 393 | **Example:** 394 | ```lua 395 | -- Example of using InteractionEntity 396 | exports['envi-interact']:InteractionEntity(entity, { 397 | { 398 | label = 'Interaction Entity - Choice 1', 399 | selected = function() 400 | print('Interacting with entity - selected choice 1...') 401 | -- Additional logic can be added here to handle the entity usage 402 | end 403 | }, 404 | { 405 | label = 'Interaction Entity - Choice 2', 406 | selected = function() 407 | print('Interacting with entity - selected choice 2...') 408 | -- Additional logic can be added here to handle the entity usage 409 | end 410 | }, 411 | { 412 | label = 'Interaction Entity - Choice 3', 413 | selected = function() 414 | print('Interacting with entity - selected choice 3...') 415 | -- Additional logic can be added here to handle the entity usage 416 | end 417 | }, 418 | { 419 | label = 'Interaction Entity - Choice 4', 420 | selected = function() 421 | print('Interacting with entity - selected choice 4...') 422 | -- Additional logic can be added here to handle the entity usage 423 | end 424 | }, 425 | }) 426 | ``` 427 | 428 | #### 14. `UpdateSpeech` 429 | Updates the speech of a specific menu. 430 | 431 | **Parameters:** 432 | - `menuID` (string): The ID of the menu to update. - NOTE: NEEDS TO MATCH THE MENU ID OF THE OPEN MENU 433 | - `speech` (string): The new speech to display. 434 | - `duration` (int): The duration it takes for the speech to show from start to end 435 | 436 | **Example:** 437 | ```lua 438 | exports['envi-interact']:UpdateSpeech('decision-menu', 'New speech text to display here.', 3000) 439 | ``` 440 | 441 | ## Functions 442 | 443 | ### InteractionModel 444 | Creates an interaction point for any instance of a specific model in the game world. This is useful for creating interactions with props or objects that can appear multiple times in the world. 445 | 446 | ```lua 447 | exports['envi-interact']:InteractionModel(modelHash, { 448 | { 449 | name = 'interaction_name', 450 | distance = 2.0, -- Optional: Maximum distance for interaction 451 | radius = 1.5, -- Optional: Interaction radius 452 | options = { 453 | { 454 | label = '[E] - Interact', 455 | selected = function(data) 456 | -- Handle interaction 457 | end, 458 | } 459 | } 460 | } 461 | }) 462 | ``` 463 | 464 | Example usage: 465 | ```lua 466 | -- Create an interaction for all ATMs 467 | exports['envi-interact']:InteractionModel(GetHashKey('prop_atm_01'), { 468 | { 469 | name = 'atm_interaction', 470 | distance = 2.0, 471 | radius = 1.5, 472 | options = { 473 | { 474 | label = '[E] - Use ATM', 475 | selected = function(data) 476 | -- Open ATM menu 477 | end, 478 | } 479 | } 480 | } 481 | }) 482 | ``` 483 | 484 | Key features: 485 | - Works with any instance of the specified model in the game world 486 | - Automatically detects when the player is looking at the model 487 | - Supports the same interaction options as other interaction types 488 | - Useful for creating interactions with props, vehicles, or other world objects 489 | -------------------------------------------------------------------------------- /client/client.lua: -------------------------------------------------------------------------------- 1 | local debug = false 2 | local menuState = {} 3 | local percentState = {} 4 | local sliderData = {} 5 | local interactionPoints = {} 6 | local cam 7 | local currentLabel = 'UNDEFINED' 8 | local callbackFunctions = {} 9 | local interactionPeds = {} 10 | local spawnedPeds = {} 11 | local wait = 1250 12 | local menuOptionSelectedHandler = nil 13 | local scrollCooldown = 250 14 | local scrollCooldownTimer = 0 15 | local currentPointData = {} 16 | local waitingForSpeech = {} 17 | local currentMenuID = nil 18 | local currentPercentageBarID = nil 19 | local currentPed = nil 20 | local lastOptionSelected = 0 21 | local lastInteraction = 0 22 | local optionCooldown = 2000 -- 2 seconds 23 | local interactionPedData = {} 24 | local interactionModelData = {} 25 | 26 | local speechCategories = { 27 | ["PositiveReaction"] = PositiveReaction, 28 | ["NegativeReaction"] = NegativeReaction, 29 | ["AngryReaction"] = AngryReaction, 30 | ["NeutralReaction"] = NeutralReaction, 31 | ["ScaredReaction"] = ScaredReaction, 32 | ["Hello"] = Hello, 33 | ["Bye"] = Bye, 34 | ["Yes"] = Yes, 35 | ["No"] = No, 36 | ["Thanks"] = Thanks, 37 | ["OverThere"] = OverThere, 38 | ["Conversation"] = Conversation, 39 | ["Apology"] = Apology, 40 | ["HitCar"] = HitCar, 41 | ["Tour"] = Tour 42 | } 43 | 44 | local function PlaySpeech(ped, speech, params) 45 | CreateThread(function() 46 | if speechCategories[speech] then 47 | local category = speechCategories[speech] 48 | local random = math.random(1, #category) 49 | speech = category[random] 50 | end 51 | PlayPedAmbientSpeechNative(ped, speech, params) 52 | end) 53 | end 54 | 55 | exports("PlaySpeech", PlaySpeech) 56 | 57 | --- Opens a choice menu with given parameters. 58 | ---@param data table Table containing menu options like title, menuID, timeout table, and options list. 59 | ---@return string The key of the selected option. 60 | function OpenChoiceMenu(data) 61 | local timedOut = false 62 | currentMenuID = data.menuID 63 | if data.timeout and data.timeout.time then 64 | data.timeout.time = data.timeout.time * 1000 65 | SetTimeout(data.timeout.time, function() 66 | if menuState[data.menuID] then 67 | print('Choice Menu timed out after ' .. data.timeout.time / 1000 .. ' seconds') 68 | if data.timeout.closeEverything then 69 | CloseEverything() 70 | currentMenuID = nil 71 | else 72 | CloseMenu(data.menuID) 73 | if currentMenuID == data.menuID then 74 | currentMenuID = nil 75 | end 76 | end 77 | timedOut = true 78 | end 79 | end) 80 | end 81 | menuState[data.menuID] = true 82 | local serializableOptions = {} 83 | local filteredOptions = {} 84 | for i, option in ipairs(data.options) do 85 | local shouldInclude = true 86 | if option.canSee then 87 | shouldInclude = option.canSee() 88 | end 89 | if shouldInclude then 90 | table.insert(filteredOptions, { 91 | key = option.key, 92 | label = option.label, 93 | closeAll = option.closeAll or nil, 94 | speech = option.speech or nil, 95 | speechOptions = option.speechOptions or nil, 96 | reaction = option.reaction or nil 97 | }) 98 | callbackFunctions[data.menuID] = callbackFunctions[data.menuID] or {} 99 | callbackFunctions[data.menuID][option.key] = option.selected 100 | end 101 | end 102 | callbackFunctions[data.menuID].onESC = data.onESC 103 | local duration = Config.DefaultTypeDelay 104 | if data.speechOptions and data.speechOptions.duration then 105 | duration = data.speechOptions.duration 106 | end 107 | SendNUIMessage({ 108 | action = 'openChoiceMenu', 109 | speech = data.speech, 110 | title = data.title, 111 | menuID = data.menuID, 112 | position = data.position, 113 | speechOptions = data.speechOptions or nil, 114 | duration = duration, 115 | options = filteredOptions, 116 | onESC = data.onESC ~= nil 117 | }) 118 | SetNuiFocus(true, true) 119 | return 'done' 120 | end 121 | 122 | --- Creates a NPC with the given data and interaction options. 123 | ---@param pedData table Table containing ped data like model, coordinates, and heading. 124 | ---@param interactionData table Table containing interaction options like slider state, speech, and position. 125 | function CreateNPC(pedData, interactionData) 126 | RequestModel(pedData.model) 127 | while not HasModelLoaded(pedData.model) do 128 | Wait(100) 129 | end 130 | local ped = CreatePed(4, pedData.model, pedData.coords.x, pedData.coords.y, pedData.coords.z, pedData.heading, false, false) 131 | while not DoesEntityExist(ped) do 132 | Wait(100) 133 | end 134 | table.insert(spawnedPeds, ped) 135 | interactionPeds[interactionData.menuID] = ped 136 | if pedData.isFrozen then 137 | Wait(950) 138 | FreezeEntityPosition(ped, true) 139 | SetBlockingOfNonTemporaryEvents(ped, true) 140 | SetEntityInvincible(ped, true) 141 | end 142 | InteractionEntity(ped, { 143 | { 144 | name = interactionData.menuID, 145 | distance = interactionData.distance or 2.0, 146 | radius = interactionData.radius or 1.5, 147 | options = { 148 | { 149 | label = '[E] - Talk', 150 | selected = function(data) 151 | PedInteraction(ped, interactionData) 152 | end, 153 | } 154 | } 155 | } 156 | }) 157 | return ped 158 | end 159 | 160 | --- Handles interactions with a ped, setting up a menu based on provided options. 161 | ---@param entity any The entity involved in the interaction. 162 | ---@param data table Table containing interaction options like slider state, speech, and position. 163 | ---@return boolean Returns false if the menu timed out - true if all menus are closed after interaction. 164 | function PedInteraction(entity, data) 165 | local currentTime = GetGameTimer() 166 | if currentTime - lastInteraction < 1000 then 167 | return false 168 | end 169 | lastInteraction = currentTime 170 | currentPed = entity 171 | local timedOut = false 172 | menuState[data.menuID] = true 173 | if data.timeout and data.timeout.time then 174 | data.timeout.time = data.timeout.time * 1000 175 | SetTimeout(data.timeout.time, function() 176 | if menuState[data.menuID] then 177 | print('Choice Menu timed out after ' .. data.timeout.time / 1000 .. ' seconds') 178 | if data.timeout.closeEverything then 179 | CloseEverything() 180 | currentMenuID = nil 181 | else 182 | CloseMenu(data.menuID) 183 | if currentMenuID == data.menuID then 184 | currentMenuID = nil 185 | end 186 | end 187 | timedOut = true 188 | end 189 | end) 190 | end 191 | if data.freeze then 192 | FreezeEntityPosition(entity, true) 193 | end 194 | if not data.coords then 195 | data.coords = GetEntityCoords(entity, true) 196 | end 197 | if data.focusCam then 198 | local coords = GetEntityCoords(PlayerPedId()) 199 | local entCoords = GetEntityCoords(entity) 200 | local screenCoords = GetOffsetFromEntityInWorldCoords(entity, 0.0, 0.9, 0.55) 201 | local dist = #(coords - entCoords) 202 | if dist < 8.0 then 203 | cam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true) 204 | SetCamCoord(cam, screenCoords.x, screenCoords.y, screenCoords.z) 205 | PointCamAtCoord(cam, data.coords.x, data.coords.y, data.coords.z + 0.4) 206 | RenderScriptCams(true, true, 1000, 1, 1) 207 | Wait(500) 208 | end 209 | end 210 | if data.greeting then 211 | PlaySpeech(entity, data.greeting, 'SPEECH_PARAMS_FORCE_NORMAL_CLEAR') 212 | end 213 | local serializableOptions = {} 214 | local filteredOptions = {} 215 | for i, option in ipairs(data.options) do 216 | local shouldInclude = true 217 | if option.canSee then 218 | shouldInclude = option.canSee() 219 | end 220 | if shouldInclude then 221 | table.insert(filteredOptions, { 222 | key = option.key, 223 | label = option.label, 224 | stayOpen = option.stayOpen or false, 225 | closeAll = option.closeAll or false, 226 | speech = option.speech or nil, 227 | reaction = option.reaction or nil 228 | }) 229 | callbackFunctions[data.menuID] = callbackFunctions[data.menuID] or {} 230 | callbackFunctions[data.menuID][option.key] = option.selected 231 | end 232 | end 233 | 234 | -- Check if there are no available options 235 | if #filteredOptions == 0 then 236 | SendNUIMessage({ 237 | action = 'openPedMenu', 238 | title = data.title, 239 | menuID = data.menuID, 240 | position = data.position, 241 | speech = "I've got nothing to say to you!", 242 | options = {}, 243 | onESC = true 244 | }) 245 | SetNuiFocus(true, true) 246 | Wait(5000) 247 | CloseMenu(data.menuID) 248 | currentMenuID = nil 249 | if data.freeze then 250 | FreezeEntityPosition(entity, false) 251 | end 252 | return false 253 | end 254 | 255 | callbackFunctions[data.menuID].onESC = data.onESC 256 | local duration = Config.DefaultTypeDelay 257 | if data.speechOptions and data.speechOptions.duration then 258 | duration = data.speechOptions.duration 259 | end 260 | SendNUIMessage({ 261 | action = 'openPedMenu', 262 | title = data.title, 263 | menuID = data.menuID, 264 | position = data.position, 265 | sliderState = data.sliderState or 'disabled', 266 | sliderValue = data.sliderValue or false, 267 | speech = data.speech or false, 268 | speechOptions = data.speechOptions or nil, 269 | duration = duration, 270 | options = filteredOptions, 271 | onESC = data.onESC ~= nil 272 | }) 273 | SetNuiFocus(true, true) 274 | local wait = 1000 275 | while menuState[data.menuID] do 276 | local coords = GetEntityCoords(PlayerPedId()) 277 | local pedCoord = GetEntityCoords(entity) 278 | local distance = #(coords - pedCoord) 279 | if data.standStill or data.freeze then 280 | ClearPedTasks(entity) 281 | TaskStandStill(entity, 10000) 282 | wait = 10 283 | end 284 | if distance > 5.0 then 285 | CloseMenu(data.menuID) 286 | currentMenuID = nil 287 | break 288 | end 289 | Wait(wait) 290 | end 291 | if data.freeze then 292 | FreezeEntityPosition(entity, false) 293 | end 294 | currentMenuID = nil 295 | return not timedOut 296 | end 297 | 298 | -- Returns the currently open menu IDs. 299 | ---@return any, any The currently open menu IDs. 300 | function GetOpenMenuIDs() 301 | if not currentMenuID and not currentPercentageBarID then 302 | return nil, nil 303 | end 304 | return currentMenuID, currentPercentageBarID 305 | end 306 | 307 | --- Returns the currently open menu IDs and their states. 308 | ---@return table -- A table containing the IDs of all currently open menus and their states. 309 | function GetOpenMenus() 310 | if not next(menuState) then 311 | return {} 312 | end 313 | return menuState 314 | end 315 | 316 | --- Returns the currently open percentage bar IDs and their states. 317 | ---@return table -- A table containing the IDs of all currently open percentage bars and their states. 318 | function GetOpenPercentBars() 319 | if not next(percentState) then 320 | return {} 321 | end 322 | return percentState 323 | end 324 | 325 | --- Allows user interaction with a slider within a menu. 326 | ---@param menuID string The ID of the menu containing the slider. 327 | ---@param data table Table containing slider options like initial and new values. 328 | ---@return number, number The new and old slider values. 329 | function UseSlider(menuID, data) 330 | sliderData[menuID] = { 331 | min = data.min or 0, 332 | max = data.max or 100, 333 | title = data.title, 334 | sliderState = data.sliderState or 'locked', 335 | sliderValue = data.sliderValue or 0, 336 | confirm = msgpack.unpack(msgpack.pack(data.confirm)), 337 | nextState = data.nextState or 'locked', 338 | hideChoice = data.hideChoice or false 339 | } 340 | SendNUIMessage({ 341 | menuID = menuID, 342 | title = data.title, 343 | action = 'useSlider', 344 | nextState = data.nextState, 345 | min = data.min, 346 | max = data.max, 347 | sliderState = data.sliderState, 348 | sliderValue = data.sliderValue, 349 | hideChoice = data.hideChoice 350 | }) 351 | end 352 | 353 | --- Forces an update of the slider value in a menu. 354 | ---@param menuID string The ID of the menu where the slider is active. 355 | ---@param sliderValue number The new value to set the slider to. 356 | function ForceUpdateSlider(menuID, sliderValue) 357 | local minSliderValue = sliderData[menuID].min 358 | local maxSliderValue = sliderData[menuID].max 359 | if sliderValue < minSliderValue or sliderValue > maxSliderValue then 360 | print("Error: slider value is out of bounds.") 361 | return 362 | end 363 | SendNUIMessage({ 364 | menuID = menuID, 365 | action = 'forceUpdateSlider', 366 | sliderValue = sliderValue 367 | }) 368 | end 369 | 370 | -- Updates the speech bubble for a menu. 371 | ---@param menuID string The ID of the menu to update the speech bubble for. 372 | ---@param speech string The new speech bubble text. 373 | function UpdateSpeech(menuID, speech, duration) 374 | SendNUIMessage({ 375 | menuID = menuID, 376 | action = 'updateSpeech', 377 | duration = duration or Config.DefaultTypeDelay, 378 | speech = speech 379 | }) 380 | waitingForSpeech[menuID] = true 381 | while waitingForSpeech[menuID] do 382 | Wait(500) 383 | end 384 | Wait(500) 385 | end 386 | 387 | --- Displays a percentage bar as a separate menu. 388 | ---@param menuID string The new menu ID for the percentage bar. 389 | ---@param percent number The percentage value to display. 390 | ---@param title string The title of the percentage bar. 391 | ---@param position string The position of the percentage bar on the screen. 392 | ---@param tooltip string The tooltip position of the percentage bar on the screen. 393 | ---@param c1 string Replace green color value of the percentage bar. 0% - 24% 394 | ---@param c2 string Replace amber color value of the percentage bar. 25% - 75% 395 | ---@param c3 string Replace red color value of the percentage bar. 76% - 100% 396 | function PercentageBar(menuID, percent, title, position, tooltip, c1, c2, c3) 397 | if percent < 0 then 398 | percent = 0 399 | end 400 | if percent > 100 then 401 | percent = 100 402 | end 403 | percentState[menuID] = true 404 | currentPercentageBarID = menuID 405 | SendNUIMessage({ 406 | percentID = menuID, 407 | action = 'percentageBar', 408 | title = title, 409 | percentage = percent, 410 | tooltip = tooltip or 'hover', -- 'hover, 'none', 'always' 411 | position = position, 412 | colors = { 413 | c1 = c1, 414 | c2 = c2, 415 | c3 = c3 416 | } 417 | }) 418 | return menuID 419 | end 420 | 421 | --- Returns the ped associated with a menu ID. 422 | ---@param menuID string The ID of the menu to get the ped from. 423 | ---@return any The ped associated with the menu ID. 424 | function GetInteractionPed(menuID) 425 | return interactionPeds[menuID] 426 | end 427 | 428 | -- -- Raycast Based - PRESS E Interaction - (runs at 0.00ms!) 429 | ---@param position vector3 - The position of the interaction point. 430 | ---@param data table - Table containing interaction options like slider state, speech, and position. 431 | function InteractionPoint(position, data) 432 | if data then 433 | if data.name then 434 | interactionPoints[data.name] = { 435 | name = data.name, 436 | position = position, 437 | options = {}, 438 | distance = data.distance, 439 | currentOption = 1, 440 | radius = data.radius or 1.0, 441 | } 442 | for _, option in ipairs(data.options) do 443 | table.insert(interactionPoints[data.name].options, option) 444 | end 445 | else 446 | print('No name provided for interaction point') 447 | end 448 | end 449 | end 450 | 451 | ---@param entity number - The ped to be used as the interaction point. 452 | ---@param data table - Table containing interaction options like slider state, speech, and position. 453 | function InteractionEntity(entity, data) 454 | if data then 455 | if data[1].name then 456 | interactionPedData[data[1].name] = { 457 | name = data[1].name, 458 | options = {}, 459 | distance = data[1].distance, 460 | currentOption = 1, 461 | radius = data[1].radius or 1.0, 462 | entity = entity or nil 463 | } 464 | for _, option in ipairs(data[1].options) do 465 | table.insert(interactionPedData[data[1].name].options, option) 466 | end 467 | else 468 | print('No name provided for interaction point') 469 | end 470 | end 471 | end 472 | 473 | ---@param model string - The model hash to be used as the interaction point. 474 | ---@param data table - Table containing interaction options like slider state, speech, and position. 475 | function InteractionModel(model, data) 476 | if not data or not data[1] or not data[1].name then 477 | print('Invalid data provided for interaction model') 478 | return 479 | end 480 | 481 | local models = type(model) == 'table' and model or {model} 482 | 483 | for _, modelHash in ipairs(models) do 484 | local model = modelHash 485 | if type(modelHash) == 'string' then 486 | model = joaat(modelHash) 487 | end 488 | local name = data[1].name..'_'..model 489 | interactionModelData[name] = { 490 | name = name, 491 | options = {}, 492 | distance = data[1].distance, 493 | currentOption = 1, 494 | radius = data[1].radius or 1.0, 495 | model = model 496 | } 497 | for _, option in ipairs(data[1].options) do 498 | table.insert(interactionModelData[name].options, option) 499 | end 500 | end 501 | end 502 | 503 | --------------------------------------- 504 | --------------------------------------- 505 | -- Credits: QB-Core 506 | -- Source: https://github.com/qbcore-framework/qb-adminmenu/blob/main/client/entity_view.lua 507 | 508 | local RotationToDirection = function(rotation) 509 | local adjustedRotation = { 510 | x = (math.pi / 180) * rotation.x, 511 | y = (math.pi / 180) * rotation.y, 512 | z = (math.pi / 180) * rotation.z 513 | } 514 | local direction = { 515 | x = -math.sin(adjustedRotation.z) * math.abs(math.cos(adjustedRotation.x)), 516 | y = math.cos(adjustedRotation.z) * math.abs(math.cos(adjustedRotation.x)), 517 | z = math.sin(adjustedRotation.x) 518 | } 519 | return direction 520 | end 521 | 522 | 523 | local RayCastGamePlayCamera = function(distance) 524 | local currentRenderingCam = nil 525 | if not IsGameplayCamRendering() then 526 | currentRenderingCam = GetRenderingCam() 527 | end 528 | local cameraRotation = not currentRenderingCam and GetGameplayCamRot() or GetCamRot(currentRenderingCam, 2) 529 | local cameraCoord = not currentRenderingCam and GetGameplayCamCoord() or GetCamCoord(currentRenderingCam) 530 | local direction = RotationToDirection(cameraRotation) 531 | local destination = { 532 | x = cameraCoord.x + direction.x * distance, 533 | y = cameraCoord.y + direction.y * distance, 534 | z = cameraCoord.z + direction.z * distance 535 | } 536 | local _, b, c, _, e = GetShapeTestResult(StartShapeTestRay(cameraCoord.x, cameraCoord.y, cameraCoord.z, destination.x, destination.y, destination.z, -1, PlayerPedId(), 0)) 537 | return b, c, e 538 | end 539 | 540 | ------------------------------------------------- 541 | 542 | 543 | 544 | CreateThread(function() 545 | local lastPointData = nil 546 | while true do 547 | local ped = PlayerPedId() 548 | local closestPoint = nil 549 | local minDistance = math.huge 550 | local hit, hitPosition, hitEntity = RayCastGamePlayCamera(200) 551 | local coords = GetEntityCoords(ped) 552 | 553 | if debug and hit and hitPosition and not closestPoint then 554 | DrawLine(GetEntityCoords(ped), hitPosition.x, hitPosition.y, hitPosition.z, 255, 0, 0, 100) 555 | end 556 | 557 | -- Check for model interactions 558 | for _, modelInteraction in pairs(interactionModelData) do 559 | if hit and hitEntity > 0 then 560 | local hitType = GetEntityType(hitEntity) 561 | local success, hitModel = false, nil 562 | if hitType > 0 then 563 | success, hitModel = pcall(function() 564 | return GetEntityModel(hitEntity) 565 | end) 566 | end 567 | if success and hitModel then 568 | if hitModel == modelInteraction.model then 569 | modelInteraction.entity = hitEntity 570 | local entityCoords = GetEntityCoords(hitEntity) 571 | local myCoords = GetEntityCoords(ped) 572 | if entityCoords then 573 | local distance = #(myCoords - entityCoords) 574 | local hitDistance = #(hitPosition - entityCoords) 575 | if hitDistance < (modelInteraction.radius or 1.0) and distance < modelInteraction.distance then 576 | minDistance = distance 577 | closestPoint = modelInteraction 578 | if debug and closestPoint then 579 | DrawLine(GetEntityCoords(ped), hitPosition.x, hitPosition.y, hitPosition.z, 0, 255, 0, 100) 580 | end 581 | end 582 | end 583 | end 584 | end 585 | end 586 | end 587 | 588 | for _, npc in pairs(interactionPedData) do 589 | local entityCoords = GetEntityCoords(npc.entity) 590 | local myCoords = GetEntityCoords(ped) 591 | local distance = #(myCoords - entityCoords) 592 | local hitDistance = #(hitPosition - entityCoords) 593 | if distance < npc.distance and hitDistance < (npc.radius or 1.0) and distance < (interactionPedData.distance or 2.0) then 594 | minDistance = distance 595 | closestPoint = npc 596 | if debug and closestPoint then 597 | DrawLine(GetEntityCoords(ped), hitPosition.x, hitPosition.y, hitPosition.z, 0, 255, 0, 100) 598 | end 599 | end 600 | end 601 | 602 | for _, interactionPoint in pairs(interactionPoints) do 603 | local distance = #(coords - interactionPoint.position) 604 | local hitDistance = #(hitPosition - interactionPoint.position) 605 | if distance < interactionPoint.distance and distance < minDistance and hitDistance < (interactionPoint.radius or 1.0) then 606 | minDistance = distance 607 | closestPoint = interactionPoint 608 | if debug and closestPoint then 609 | DrawLine(GetEntityCoords(ped), hitPosition.x, hitPosition.y, hitPosition.z, 0, 255, 0, 100) 610 | end 611 | end 612 | end 613 | 614 | if closestPoint then 615 | if lastPointData ~= closestPoint then 616 | -- Filter visible options when first looking at point 617 | local visibleOptions = {} 618 | for _, option in ipairs(closestPoint.options) do 619 | local shouldInclude = true 620 | if option.canSee then 621 | shouldInclude = option.canSee(closestPoint.entity) 622 | end 623 | if shouldInclude then 624 | table.insert(visibleOptions, option) 625 | end 626 | end 627 | 628 | -- Only proceed if there are visible options 629 | if #visibleOptions > 0 then 630 | lastPointData = closestPoint 631 | currentPointData = closestPoint 632 | -- Store filtered options and reset current option index 633 | currentPointData.visibleOptions = visibleOptions 634 | currentPointData.currentOption = 1 635 | ShowText(visibleOptions[1].label, true, visibleOptions) 636 | else 637 | HideText() 638 | lastPointData = nil 639 | currentPointData = nil 640 | end 641 | end 642 | else 643 | if lastPointData then 644 | ShowText(currentLabel, false, nil) 645 | HideText() 646 | lastPointData = nil 647 | currentPointData = nil 648 | end 649 | end 650 | Wait(wait) 651 | end 652 | end) 653 | 654 | --- Closes a menu based on its ID. 655 | ---@param menuID string The ID of the menu to close. 656 | ---@param speech string The speech to play before the menu closes. 657 | function CloseMenu(menuID, speech) 658 | if menuState[menuID] then 659 | if speech then 660 | UpdateSpeech(menuID, speech) 661 | end 662 | SendNUIMessage({ 663 | action = 'closeMenu', 664 | menuID = menuID, 665 | percentID = 'none' 666 | }) 667 | menuState[menuID] = nil 668 | currentMenuID = nil 669 | if cam then 670 | SetCamActive(cam, false) 671 | RenderScriptCams(false, true, 1000, 1, 1) 672 | DestroyCam(cam, true) 673 | end 674 | cam = nil 675 | SetNuiFocus(false, false) 676 | if menuOptionSelectedHandler then 677 | RemoveEventHandler(menuOptionSelectedHandler) 678 | menuOptionSelectedHandler = nil 679 | end 680 | currentPed = nil 681 | end 682 | if percentState[menuID] then 683 | SendNUIMessage({ 684 | action = 'closeMenu', 685 | menuID = 'none', 686 | percentID = menuID 687 | }) 688 | percentState[menuID] = nil 689 | end 690 | end 691 | 692 | -- Checks if any menu is open. 693 | ---@return boolean - Whether any menu is open. 694 | function IsAnyMenuOpen() 695 | return currentMenuID ~= nil 696 | end 697 | 698 | -- Checks if any percentage bar is open. 699 | ---@return boolean - Whether any percentage bar is open. 700 | function IsAnyPercentBarOpen() 701 | return next(percentState) ~= nil 702 | end 703 | 704 | -- Closes all open menus and percentage bars. 705 | function CloseEverything() 706 | CloseAllPercentBars() 707 | CloseAllMenus() 708 | currentMenuID = nil 709 | end 710 | 711 | --- Closes all open menus. 712 | function CloseAllMenus() 713 | local openMenus = GetOpenMenus() 714 | if not openMenus then 715 | print('No menus are open') 716 | return 717 | else 718 | for menuID, _ in pairs(openMenus) do 719 | CloseMenu(menuID) 720 | end 721 | end 722 | currentMenuID = nil 723 | end 724 | 725 | -- Closes all open percentage bars. 726 | function CloseAllPercentBars() 727 | local openPercentBars = GetOpenPercentBars() 728 | if not openPercentBars then 729 | print('No percentage bars are open') 730 | return 731 | else 732 | for percentID, _ in pairs(openPercentBars) do 733 | CloseMenu(percentID) 734 | end 735 | end 736 | if currentPercentageBarID then 737 | CloseMenu(currentPercentageBarID) 738 | end 739 | end 740 | 741 | AddEventHandler('onResourceStop', function(resourceName) 742 | if resourceName ~= GetCurrentResourceName() then 743 | return 744 | end 745 | for _, ped in pairs(spawnedPeds) do 746 | DeleteEntity(ped) 747 | end 748 | HideText() 749 | end) 750 | 751 | RegisterCommand('+scrollDown', function() 752 | if not currentPointData or not currentPointData.visibleOptions then 753 | return 754 | end 755 | local currentTime = GetGameTimer() 756 | if currentTime - scrollCooldownTimer < scrollCooldown then 757 | return 758 | end 759 | scrollCooldownTimer = currentTime 760 | local numberOfOptions = #currentPointData.visibleOptions 761 | currentPointData.currentOption = currentPointData.currentOption + 1 762 | if currentPointData.currentOption > numberOfOptions then 763 | currentPointData.currentOption = numberOfOptions 764 | end 765 | ShowText(currentPointData.visibleOptions[currentPointData.currentOption].label, true, currentPointData.visibleOptions) 766 | end, false) 767 | 768 | RegisterCommand('+scrollUp', function() 769 | if not currentPointData or not currentPointData.visibleOptions then 770 | return 771 | end 772 | local currentTime = GetGameTimer() 773 | if currentTime - scrollCooldownTimer < scrollCooldown then 774 | return 775 | end 776 | scrollCooldownTimer = currentTime 777 | currentPointData.currentOption = currentPointData.currentOption - 1 778 | if currentPointData.currentOption < 1 then 779 | currentPointData.currentOption = 1 780 | end 781 | ShowText(currentPointData.visibleOptions[currentPointData.currentOption].label, true, currentPointData.visibleOptions) 782 | end, false) 783 | 784 | RegisterCommand('interact', function() 785 | if not currentPointData or not currentPointData.visibleOptions then 786 | return 787 | end 788 | local currentOption = currentPointData.visibleOptions[currentPointData.currentOption] 789 | HideText() 790 | currentOption.selected(currentPointData) 791 | end, false) 792 | 793 | RegisterCommand('-scrollUp', function() 794 | return 795 | end, false) 796 | 797 | RegisterCommand('-scrollDown', function() 798 | return 799 | end, false) 800 | 801 | 802 | RegisterKeyMapping('interact', 'Envi-Interact - Interact', 'keyboard', 'E') 803 | RegisterKeyMapping('+scrollDown', 'Envi-Interact - Scroll Down', 'MOUSE_WHEEL', 'IOM_WHEEL_DOWN') 804 | RegisterKeyMapping('+scrollUp', 'Envi-Interact - Scroll Up', 'MOUSE_WHEEL', 'IOM_WHEEL_UP') 805 | 806 | --- Registers a callback for when an option is selected in a NUI menu. 807 | ---@param data table Data passed from the NUI containing the selected option. 808 | ---@param cb function Callback function to execute after selection. 809 | RegisterNuiCallback('selectOption', function(data, cb) 810 | local currentTime = GetGameTimer() 811 | if currentTime - lastOptionSelected < optionCooldown then 812 | cb(0) 813 | return 814 | end 815 | lastOptionSelected = currentTime 816 | 817 | local menuID = data.menuID 818 | local key = data.key 819 | local speech = data.speech 820 | local reaction = data.reaction 821 | 822 | if reaction and currentPed then 823 | PlaySpeech(currentPed, reaction, 'SPEECH_PARAMS_FORCE_NORMAL_CLEAR') 824 | end 825 | if speech then 826 | UpdateSpeech(menuID, speech) 827 | end 828 | if callbackFunctions[menuID] and callbackFunctions[menuID][key] then 829 | callbackFunctions[menuID][key](data) 830 | TriggerEvent('envi-interact:menuOptionSelected', key) -- Trigger custom event with the key 831 | cb(1) 832 | else 833 | TriggerEvent('envi-interact:menuOptionSelected', key) -- Trigger custom event with the key 834 | cb(0) 835 | CloseMenu(menuID) 836 | end 837 | end) 838 | 839 | 840 | --- Registers a callback for when a slider value is confirmed in a NUI menu. 841 | ---@param data table Data passed from the NUI containing the slider confirmation. 842 | ---@param cb function Callback function to execute after confirmation. 843 | RegisterNuiCallback('sliderConfirm', function(data, cb) 844 | local menuID = data.menuID 845 | if sliderData[menuID].nextState == 'disabled' then 846 | cb('hideSlider') 847 | elseif sliderData[menuID].nextState == 'locked' then 848 | cb('lockSlider') 849 | elseif sliderData[menuID].nextState == 'unlocked' then 850 | cb('unlockSlider') 851 | else 852 | cb('lockSlider') 853 | end 854 | if not data or not data.menuID or not data.sliderValue then 855 | print("Error: invalid data received from NUI") 856 | return 857 | end 858 | if not sliderData[menuID] then 859 | print("Error: menuID not found in sliderData") 860 | return 861 | end 862 | local newValue = data.sliderValue 863 | local oldValue = sliderData[menuID].sliderValue 864 | sliderData[menuID].sliderValue = newValue 865 | sliderData[menuID].confirm(tonumber(newValue), tonumber(oldValue)) 866 | end) 867 | 868 | RegisterNuiCallback('close', function(data, cb) 869 | CloseMenu(data.menuID) 870 | cb(1) 871 | end) 872 | 873 | RegisterNuiCallback('closeAll', function(data, cb) 874 | CloseAllMenus() 875 | cb(1) 876 | end) 877 | 878 | RegisterNuiCallback('keydown', function(data, cb) 879 | cb(1) 880 | end) 881 | 882 | RegisterNUICallback('speechComplete', function(data) 883 | if waitingForSpeech[data.menuID] then 884 | waitingForSpeech[data.menuID] = nil 885 | end 886 | end) 887 | 888 | RegisterNuiCallback('escPressed', function(data, cb) 889 | local menuID = data.menuID 890 | if callbackFunctions[menuID] and callbackFunctions[menuID].onESC then 891 | callbackFunctions[menuID].onESC() 892 | else 893 | CloseMenu(menuID) 894 | end 895 | cb(1) 896 | end) 897 | 898 | RegisterCommand('toggleDebug', function() 899 | if not Config.EnableDebugLine then 900 | return 901 | end 902 | debug = not debug 903 | if debug then 904 | wait = 0 905 | else 906 | wait = 1250 907 | end 908 | end, false) 909 | 910 | RegisterCommand('testCloseEverything', function() 911 | CloseEverything() 912 | end, false) 913 | 914 | RegisterCommand('testCloseAllMenus', function() 915 | CloseAllMenus() 916 | end, false) 917 | 918 | RegisterCommand('testCloseAllPercentBars', function() 919 | CloseAllPercentBars() 920 | end, false) 921 | 922 | 923 | RegisterKeyMapping('toggleDebug', 'Envi-Interact - Debug Vision', 'keyboard', 'RMENU') 924 | 925 | --- Exports functions to be accessible from other scripts. 926 | exports('OpenChoiceMenu', OpenChoiceMenu) 927 | 928 | exports('UseSlider', UseSlider) 929 | exports('ForceUpdateSlider', ForceUpdateSlider) 930 | 931 | exports('PedInteraction', PedInteraction) 932 | exports('UpdateSpeech', UpdateSpeech) 933 | 934 | exports('CreateNPC', CreateNPC) 935 | 936 | exports('PercentageBar', PercentageBar) 937 | 938 | exports('GetOpenMenus', GetOpenMenus) 939 | exports('IsAnyPercentBarOpen', IsAnyPercentBarOpen) 940 | exports('IsAnyMenuOpen', IsAnyMenuOpen) 941 | 942 | exports('CloseEverything', CloseEverything) 943 | exports('CloseAllPercentBars', CloseAllPercentBars) 944 | exports('CloseMenu', CloseMenu) 945 | exports('CloseAllMenus', CloseAllMenus) 946 | 947 | exports('InteractionPoint', InteractionPoint) 948 | exports('InteractionEntity', InteractionEntity) 949 | exports('InteractionModel', InteractionModel) 950 | exports('GetInteractionPed', GetInteractionPed) 951 | -------------------------------------------------------------------------------- /client/example_code.lua: -------------------------------------------------------------------------------- 1 | RegisterCommand('testChoice', function() 2 | exports['envi-interact']:OpenChoiceMenu({ -- Using 'selected' function to execute code when the option is pressed -- does not return result when selected functions are used 3 | title = 'Pick A Choice?', 4 | speech = 'OxLib Inspired theme to complement everyone\'s favorite menu system!', 5 | onESC = function() 6 | print('ESC pressed to close menu') 7 | end, 8 | menuID = 'choice-menu-testChoice', 9 | position = 'right', 10 | options = { 11 | { 12 | key = 'E', 13 | label = 'This One', 14 | selected = function(data) 15 | print('Pressed E') 16 | CloseMenu(data.menuID) 17 | end, 18 | }, 19 | { 20 | key = 'G', 21 | label = 'That One', 22 | selected = function(data) 23 | print('Pressed G') 24 | end, 25 | }, 26 | { 27 | key = 'F', 28 | label = 'The F One', 29 | selected = function(data) 30 | print('Pressed F') 31 | end, 32 | }, 33 | { 34 | key = 'J', 35 | label = 'That Other One', 36 | canSee = function() -- example of a function to check if the option can be used 37 | return false 38 | end, 39 | selected = function(data) 40 | print('Pressed J') 41 | end, 42 | }, 43 | }, 44 | }) 45 | end, false) 46 | 47 | RegisterCommand('testChoice2', function() -- Alternative way to use if you want to return the key/option pressed and use the result to execute code 48 | local key = exports['envi-interact']:OpenChoiceMenu({ 49 | title = 'Pick A Choice?', 50 | speech = 'This is a long test speech to evaluate the text rendering capabilities of the system and to ensure that the speech bubble can handle longer strings of text without any issues.', 51 | speechOptions = { 52 | duration = 1000, 53 | }, -- Amount of TICKS the type writer takes to show all the speech text 54 | menuID = 'choice-menu-testChoice2', 55 | position = 'right', 56 | options = { 57 | { 58 | key = 'E', 59 | label = 'This One', 60 | }, 61 | { 62 | key = 'G', 63 | label = 'That One', 64 | }, 65 | { 66 | key = 'F', 67 | label = 'The F One', 68 | }, 69 | { 70 | key = 'J', 71 | label = 'That Other One', 72 | }, 73 | }, 74 | }) 75 | if key == 'E' then 76 | local key2 = exports['envi-interact']:OpenChoiceMenu({ -- Opening a second choice menu to test the functionality 77 | title = 'Pick Another Choice?', 78 | speech = 'This is an EVEN LONGER test speech to evaluate the text rendering capabilities of the system and to ensure that the speech bubble can handle longer strings of text without any issues.', 79 | speechOptions = { 80 | duration = 1000, -- Amount of TICKS (MS) the type writer takes to show all the speech text 81 | }, 82 | menuID = 'choice-menu-testChoice-E', 83 | position = 'right', 84 | options = { 85 | { 86 | key = 'E', 87 | label = 'This One', 88 | }, 89 | { 90 | key = 'G', 91 | label = 'That One', 92 | }, 93 | { 94 | key = 'F', 95 | label = 'The F One', 96 | }, 97 | { 98 | key = 'J', 99 | label = 'That Other One', 100 | }, 101 | { 102 | key = 'X', 103 | label = 'This One', 104 | }, 105 | }, 106 | }) 107 | if key2 == 'E' then -- If the key/option pressed is 'E' 108 | print('You pressed E and won the game!') 109 | elseif key2 == 'G' then -- If the key/option pressed is 'G' 110 | print('You pressed G and lost the game! - LOSER!') 111 | elseif key2 == 'F' then -- If the key/option pressed is 'F' 112 | print('You pressed F and lost the game! - LOSER!') 113 | elseif key2 == 'J' then -- If the key/option pressed is 'J' 114 | print('You pressed J and lost the game! - LOSER!') 115 | else 116 | print('You pressed '..key2..' and lost the game! - LOSER!') 117 | end 118 | else 119 | print('You pressed '..key) 120 | end 121 | end, false) 122 | 123 | RegisterCommand('testPercentage', function(source, args) 124 | local value = tonumber(args[1]) 125 | if not value then value = 50 end 126 | local pecent = exports['envi-interact']:PercentageBar('percent-bar-name', value, 'Percentage Title', 'left', 'always', false, false, false) 127 | end, false) 128 | 129 | RegisterCommand('closeMenu', function() 130 | exports['envi-interact']:CloseMenu('percent-bar-name') 131 | Wait(3000) 132 | exports['envi-interact']:CloseMenu('choice-menu') 133 | end, false) 134 | 135 | 136 | --- Video Preview Example Code -- 137 | 138 | local toldMore = false 139 | 140 | function BackToMain(data) 141 | exports['envi-interact']:OpenChoiceMenu({ 142 | title = 'Main Menu!', 143 | speech = 'What else would you like to know?', 144 | speechOptions = { 145 | duration = 1000, 146 | }, 147 | menuID = 'choice-backToMain'..math.random(11111, 99999), 148 | position = 'right', 149 | options = { 150 | { -- Table of Choice Menu Options 151 | key = 'E', 152 | stayOpen = true, 153 | reaction = 'GENERIC_INSULT_MED', 154 | label = 'Tell me more!', 155 | selected = function(data) 156 | if not toldMore then 157 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The "CreateNPC" export allows you to easily create NPCs with a simple and easy to use API. Using this export will create an NPC, Add an "InteractionEntity" point, and a "ChoiceMenu" to the NPC that is activated when the player presses E!', 12000) 158 | toldMore = true 159 | else 160 | exports['envi-interact']:UpdateSpeech(data.menuID, 'Maybe we should learn about something else?', 5000) 161 | end 162 | end, 163 | }, 164 | { 165 | key = 'N', 166 | label = 'You Got Sliders?', 167 | reaction = 'GENERIC_SHOCKED_MED', 168 | stayOpen = true, 169 | speech = 'Hell Yeah - We Got Sliders! You can use sliders to select a value between a minimum and maximum value and then use the confirm function to execute code using the new and old value selected. Try it out!', 170 | selected = function(data) 171 | exports['envi-interact']:UseSlider(data.menuID, { 172 | title = 'Pick a value:', 173 | min = 1, 174 | max = 500, 175 | sliderState = 'unlocked', 176 | sliderValue = 500, 177 | nextState = 'disabled', 178 | confirm = function(new, old) 179 | if tonumber(new) == 69 then 180 | exports['envi-interact']:UpdateSpeech(data.menuID, 'Hmmm.. ' .. new .. '?! Niiiiiiice! ;)', 5000) 181 | elseif tonumber(new) == 420 then 182 | exports['envi-interact']:UpdateSpeech(data.menuID, 'Yeahhhh! 420 Blaze It!!', 5000) 183 | else 184 | exports['envi-interact']:UpdateSpeech(data.menuID, 'Okay, so you changed the value to ' .. new .. '! We can then use this value to do stuff!', 5000) 185 | end 186 | exports['envi-interact']:OpenChoiceMenu({ 187 | title = 'You Selected: ' .. new, 188 | speech = 'I hope you know more about Envi-Interact Sliders now!', 189 | menuID = 'choice-slider-test', 190 | position = 'right', 191 | options = { 192 | { 193 | key = 'E', 194 | label = 'Go Back to Main Menu', 195 | reaction = 'GENERIC_HOWS_IT_GOING', 196 | selected = function(data) 197 | -- exports['envi-interact']:CloseMenu(data.menuID) 198 | -- Wait(1000) 199 | BackToMain(data) 200 | end, 201 | }, 202 | { 203 | key = 'X', 204 | label = 'Exit Menu', 205 | reaction = 'GENERIC_BYE', 206 | selected = function(data) 207 | exports['envi-interact']:CloseAllMenus() 208 | end, 209 | }, 210 | } 211 | }) 212 | end, 213 | }) 214 | end, 215 | }, 216 | { 217 | key = 'V', 218 | label = 'Envi-Interact', 219 | speech = 'Envi-Interact is an easy to use API for FiveM that allows you to easily create interaction points, choice menus, sliders, and percentage bars.', 220 | selected = function(data) 221 | exports['envi-interact']:UpdateSpeech(data.menuID, 'Let\'s try it out!', 2000) 222 | local startingPercentage = 50 223 | local bar = exports['envi-interact']:PercentageBar('percent-bar-name', startingPercentage, 'PERCENTAGE BAR TITLE - '..startingPercentage..'%', 'left', 'always') 224 | exports['envi-interact']:OpenChoiceMenu({ 225 | title = 'Percentage Bar', 226 | speech = 'Envi-Interact is an easy to use API for FiveM that allows you to easily create interaction points, choice menus, sliders, and percentage bars -WOW, so many options!', 227 | speechOptions = { 228 | duration = 1000, 229 | }, 230 | menuID = 'choice-percentage-bar-test'..math.random(11111, 99999), 231 | position = 'right', 232 | options = { 233 | { 234 | key = 'E', 235 | label = 'Have a conversation', 236 | reaction = 'GENERIC_BYE', 237 | selected = function(data) 238 | exports['envi-interact']:CloseAllMenus() 239 | end, 240 | }, 241 | { 242 | key = 'R', 243 | label = 'Tell me a joke', 244 | reaction = 'GENERIC_BYE', 245 | selected = function(data) 246 | exports['envi-interact']:CloseAllMenus() 247 | end, 248 | }, 249 | { 250 | key = 'I', 251 | label = 'Increase Value - 5%', 252 | reaction = 'GENERIC_SHOCKED_MED', 253 | stayOpen = true, 254 | selected = function(data) 255 | startingPercentage = startingPercentage + 5 256 | if startingPercentage > 100 then 257 | startingPercentage = 100 258 | elseif startingPercentage < 0 then 259 | startingPercentage = 0 260 | end 261 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 262 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The value is now ' .. startingPercentage .. '%!') 263 | end, 264 | }, 265 | { 266 | key = 'G', 267 | label = 'Decrease Value - 5%', 268 | reaction = 'GENERIC_SHOCKED_MED', 269 | stayOpen = true, 270 | selected = function(data) 271 | startingPercentage = startingPercentage - 5 272 | if startingPercentage > 100 then 273 | startingPercentage = 100 274 | elseif startingPercentage < 0 then 275 | startingPercentage = 0 276 | end 277 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 278 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The value is now ' .. startingPercentage .. '%!') 279 | end, 280 | }, 281 | { 282 | key = 'H', 283 | label = 'Increase Value - 10%', 284 | reaction = 'GENERIC_THANKS', 285 | stayOpen = true, 286 | selected = function(data) 287 | startingPercentage = startingPercentage + 10 288 | if startingPercentage > 100 then 289 | startingPercentage = 100 290 | elseif startingPercentage < 0 then 291 | startingPercentage = 0 292 | end 293 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 294 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The value is now ' .. startingPercentage .. '%!') 295 | end, 296 | }, 297 | { 298 | key = 'O', 299 | label = 'Decrease Value - 10%', 300 | reaction = 'GENERIC_SHOCKED_HIGH', 301 | stayOpen = true, 302 | selected = function(data) 303 | startingPercentage = startingPercentage - 10 304 | if startingPercentage > 100 then 305 | startingPercentage = 100 306 | elseif startingPercentage < 0 then 307 | startingPercentage = 0 308 | end 309 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 310 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The value is now ' .. startingPercentage .. '%!') 311 | end, 312 | }, 313 | { 314 | key = 'C', 315 | label = 'Enter Custom Value', 316 | stayOpen = true, 317 | selected = function(data) 318 | exports['envi-interact']:UseSlider(data.menuID, { 319 | title = 'Pick a value:', 320 | min = 1, 321 | max = 100, 322 | sliderState = 'unlocked', 323 | sliderValue = startingPercentage, 324 | nextState = 'disabled', 325 | confirm = function(new, old) 326 | startingPercentage = new 327 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 328 | exports['envi-interact']:UpdateSpeech(data.menuID, 'You manually set the value to ' .. startingPercentage .. '%!') 329 | end, 330 | }) 331 | end, 332 | }, 333 | { 334 | key = 'X', 335 | label = 'Exit Menu', 336 | reaction = 'GENERIC_BYE', 337 | selected = function(data) 338 | exports['envi-interact']:CloseAllMenus() 339 | end, 340 | }, 341 | } 342 | }) 343 | end, 344 | }, 345 | { 346 | key = 'I', 347 | label = 'Interaction Point / Entity?', 348 | reaction = 'GENERIC_SHOCKED_MED', 349 | selected = function(data) 350 | exports['envi-interact']:OpenChoiceMenu({ 351 | title = 'InteractionPoint/ InteractionEntity?', 352 | speech = '"InteractionPoint" and "InteractionEntity" are interaction points for our "Press E to Interact" system that supports multiple options and is fully optimized to run at 0.00ms - just as efficient as Target Systems! More entity options coming soon??', 353 | speechOptions = { 354 | duration = 1000, 355 | }, 356 | menuID = 'choice-interaction-point-test'..math.random(11111, 99999), 357 | position = 'right', 358 | options = { 359 | { 360 | key = 'E', 361 | label = 'Sounds Good! Thanks!', 362 | reaction = 'GENERIC_THANKS', 363 | selected = function(data) 364 | exports['envi-interact']:UpdateSpeech(data.menuID, 'You\'re welcome! Let\'s get you back to the main menu!', 4000) 365 | BackToMain(data) 366 | end, 367 | }, 368 | { 369 | key = 'X', 370 | label = 'Exit Menu', 371 | reaction = 'GENERIC_BYE', 372 | selected = function(data) 373 | exports['envi-interact']:CloseMenu(data.menuID) 374 | end, 375 | } 376 | } 377 | }) 378 | end, 379 | }, 380 | { 381 | key = 'X', 382 | label = 'Never Mind', 383 | reaction = 'GENERIC_BYE', 384 | selected = function(data) 385 | exports['envi-interact']:CloseMenu(data.menuID) 386 | end, 387 | } 388 | } 389 | }) 390 | end 391 | 392 | 393 | local ped = exports['envi-interact']:CreateNPC({ -- Table of NPC Data 394 | name = 'testNPC', 395 | model = 'a_f_m_bevhills_01', 396 | coords = vector3(-1338.8363, -1255.9933, 4.9441), 397 | heading = 24.5156, 398 | isFrozen = true, 399 | }, { -- Table of Choice Menu Data 400 | title = 'CreateNPC Export!', 401 | speech = 'Hello! - Welcome to Envi-Interact! I\'m an NPC created using the "CreateNPC" Export!', 402 | speechOptions = { 403 | duration = 1000, 404 | }, 405 | menuID = 'choice-menu-test-1', 406 | greeting = 'GENERIC_HI', 407 | position = 'right', 408 | distance = 3.0, 409 | focusCam = true, 410 | options = { 411 | { -- Table of Choice Menu Options 412 | key = 'E', 413 | stayOpen = true, 414 | label = 'Tell me more!', 415 | reaction = 'GENERIC_SHOCKED_MED', 416 | selected = function(data) 417 | if not toldMore then 418 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The "CreateNPC" export allows you to easily create NPCs with a simple and easy to use API. Using this export will create an NPC, Add an "InteractionEntity" point, and a "ChoiceMenu" to the NPC that is activated when the player presses E!', 12000) 419 | toldMore = true 420 | BackToMain(data) 421 | else 422 | exports['envi-interact']:UpdateSpeech(data.menuID, 'Maybe we should learn about something else?', 5000) 423 | end 424 | end, 425 | }, 426 | { 427 | key = 'N', 428 | label = 'You Got Sliders?', 429 | speech = 'The "UseSlider" export allows you to easily create a slider that can be used to select a value between a minimum and maximum value. This could be used for anything your heart desires!', 430 | stayOpen = true, 431 | 432 | reaction = 'GENERIC_SHOCKED_MED', 433 | selected = function(data) 434 | exports['envi-interact']:UpdateSpeech(data.menuID, 'Hell Yeah! We Got Sliders! You can use sliders to select a value between a minimum and maximum value and then use the confirm function to execute code using the new and old value selected. Try it out!') 435 | exports['envi-interact']:UseSlider(data.menuID, { 436 | title = 'Pick a value:', 437 | min = 1, 438 | max = 500, 439 | sliderState = 'unlocked', 440 | sliderValue = 500, 441 | nextState = 'disabled', 442 | hideChoice = true, 443 | confirm = function(new, old) 444 | local speech = 'Okay, so you changed the value to ' .. new .. '! We can then use this value to do stuff!' 445 | if tonumber(new) == 69 then 446 | speech = 'Hmmm.. ' .. new .. '?! Niiiiiiice! ;)' 447 | elseif tonumber(new) == 420 then 448 | speech = 'Yeahhhh! 420 Blaze It!!' 449 | else 450 | speech = 'Okay, so you changed the value to ' .. new .. '! We can then use this value to do stuff!' 451 | end 452 | exports['envi-interact']:OpenChoiceMenu({ 453 | title = 'You Selected: ' .. new, 454 | speech = 'I hope you know more about Envi-Interact Sliders now!', 455 | speechOptions = { 456 | duration = 1000, 457 | }, 458 | menuID = 'choice-slider-test-2', 459 | position = 'right', 460 | options = { 461 | { 462 | key = 'E', 463 | label = 'Go Back to Main Menu', 464 | reaction = 'GENERIC_HOWS_IT_GOING', 465 | selected = function(data) 466 | -- exports['envi-interact']:CloseMenu(data.menuID) 467 | -- Wait(1000) 468 | BackToMain(data) 469 | end, 470 | }, 471 | { 472 | key = 'X', 473 | reaction = 'GENERIC_BYE', 474 | label = 'Exit Menu', 475 | selected = function(data) 476 | exports['envi-interact']:CloseAllMenus() 477 | end, 478 | }, 479 | } 480 | }) 481 | end, 482 | }) 483 | end, 484 | }, 485 | { 486 | key = 'V', 487 | label = 'Percentage Bar?', 488 | speech = 'The "PercentageBar" export allows you to easily create a percentage bar that can be used to show the player a percentage of a value. This could be used for anything your heart desires!', 489 | reaction = 'GENERIC_SHOCKED_MED', 490 | selected = function(data) 491 | print(json.encode(data, { indent = true })) 492 | --exports['envi-interact']:UpdateSpeech(data.menuID, 'The "PercentageBar" export allows you to easily create a percentage bar that can be used to show the player a percentage of a value. This could be used for anything your heart desires!', 11500) 493 | exports['envi-interact']:UpdateSpeech(data.menuID, 'Let\'s try it out!') 494 | local startingPercentage = 50 495 | local bar = exports['envi-interact']:PercentageBar('percent-bar-name', startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 496 | exports['envi-interact']:OpenChoiceMenu({ 497 | title = 'Percentage Bar', 498 | speech = 'Let\'s start the value at 50%! - WOW, so many options!', 499 | speechOptions = { 500 | duration = 1000, 501 | }, 502 | menuID = 'choice-percentage-bar-test-2', 503 | position = 'right', 504 | options = { 505 | { 506 | key = 'E', 507 | label = 'Increase Value - 5%', 508 | reaction = 'GENERIC_SHOCKED_MED', 509 | stayOpen = true, 510 | selected = function(data) 511 | startingPercentage = startingPercentage + 5 512 | if startingPercentage > 100 then 513 | startingPercentage = 100 514 | elseif startingPercentage < 0 then 515 | startingPercentage = 0 516 | end 517 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 518 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The value is now ' .. startingPercentage .. '%!') 519 | end, 520 | }, 521 | { 522 | key = 'G', 523 | label = 'Decrease Value - 5%', 524 | reaction = 'GENERIC_SHOCKED_MED', 525 | stayOpen = true, 526 | selected = function(data) 527 | startingPercentage = startingPercentage - 5 528 | if startingPercentage > 100 then 529 | startingPercentage = 100 530 | elseif startingPercentage < 0 then 531 | startingPercentage = 0 532 | end 533 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 534 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The value is now ' .. startingPercentage .. '%!') 535 | end, 536 | }, 537 | { 538 | key = 'H', 539 | label = 'Increase Value - 10%', 540 | reaction = 'GENERIC_THANKS', 541 | stayOpen = true, 542 | selected = function(data) 543 | startingPercentage = startingPercentage + 10 544 | if startingPercentage > 100 then 545 | startingPercentage = 100 546 | elseif startingPercentage < 0 then 547 | startingPercentage = 0 548 | end 549 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 550 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The value is now ' .. startingPercentage .. '%!') 551 | end, 552 | }, 553 | { 554 | key = 'O', 555 | label = 'Decrease Value - 10%', 556 | reaction = 'GENERIC_SHOCKED_HIGH', 557 | stayOpen = true, 558 | selected = function(data) 559 | startingPercentage = startingPercentage - 10 560 | if startingPercentage > 100 then 561 | startingPercentage = 100 562 | elseif startingPercentage < 0 then 563 | startingPercentage = 0 564 | end 565 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 566 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The value is now ' .. startingPercentage .. '%!') 567 | end, 568 | }, 569 | { 570 | key = 'R', 571 | label = 'Reset Value - 50%', 572 | reaction = 'GENERIC_HOWS_IT_GOING', 573 | stayOpen = true, 574 | selected = function(data) 575 | startingPercentage = 50 576 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 577 | exports['envi-interact']:UpdateSpeech(data.menuID, 'The value is now back to ' .. startingPercentage .. '%!') 578 | end, 579 | }, 580 | { 581 | key = 'C', 582 | label = 'Enter Custom Value', 583 | reaction = 'GENERIC_HOWS_IT_GOING', 584 | stayOpen = true, 585 | selected = function(data) 586 | local menuID = data.menuID 587 | exports['envi-interact']:UseSlider(menuID, { 588 | title = 'Pick a value:', 589 | min = 1, 590 | max = 100, 591 | sliderState = 'unlocked', 592 | sliderValue = startingPercentage, 593 | nextState = 'disabled', 594 | confirm = function(new, old) 595 | startingPercentage = new 596 | exports['envi-interact']:PercentageBar(bar, startingPercentage, 'CURRENT RELATIONSHIP - '..startingPercentage..'%', 'top', 'hover') 597 | exports['envi-interact']:UpdateSpeech(menuID, 'You manually set the value to ' .. startingPercentage .. '%!') 598 | end, 599 | }) 600 | end, 601 | }, 602 | { 603 | key = 'B', 604 | label = 'Back to Main Menu', 605 | reaction = 'GENERIC_SHOCKED_MED', 606 | selected = function(data) 607 | BackToMain(data) 608 | end, 609 | }, 610 | { 611 | key = 'X', 612 | label = 'Exit Menu', 613 | reaction = 'GENERIC_BYE', 614 | selected = function(data) 615 | exports['envi-interact']:CloseAllMenus() 616 | end, 617 | }, 618 | } 619 | }) 620 | end, 621 | }, 622 | { 623 | key = 'I', 624 | label = 'Interaction Point / Entity?', 625 | reaction = 'GUN_COOL', 626 | selected = function(data) 627 | exports['envi-interact']:OpenChoiceMenu({ 628 | title = 'InteractionPoint/ InteractionEntity?', 629 | speech = '"InteractionPoint" and "InteractionEntity" are interaction points for our "Press E to Interact" system that supports multiple options and is fully optimized to run at 0.00ms - just as efficient as Target Systems! More entity options coming soon??', 630 | speechOptions = { 631 | duration = 1000, 632 | }, 633 | menuID = 'choice-interaction-point-test-2'..math.random(11111, 99999), 634 | position = 'right', 635 | options = { 636 | { 637 | key = 'E', 638 | label = 'Sounds Good! Thanks!', 639 | reaction = 'GENERIC_THANKS', 640 | selected = function(data) 641 | exports['envi-interact']:UpdateSpeech(data.menuID, 'You\'re welcome! Let\'s get you back to the main menu!', 4000) 642 | 643 | BackToMain(data) 644 | end, 645 | }, 646 | { 647 | key = 'X', 648 | label = 'Exit Menu', 649 | reaction = 'GENERIC_BYE', 650 | selected = function(data) 651 | exports['envi-interact']:CloseMenu(data.menuID) 652 | end, 653 | } 654 | } 655 | }) 656 | end, 657 | }, 658 | { 659 | key = 'X', 660 | label = 'Never Mind', 661 | reaction = 'GENERIC_BYE', 662 | selected = function(data) 663 | exports['envi-interact']:CloseMenu(data.menuID) 664 | end, 665 | } 666 | } 667 | }) 668 | 669 | 670 | 671 | local currentOffer = 420 672 | local willAccept = 350 673 | local npcRelationship = 30 674 | 675 | local ped = exports['envi-interact']:CreateNPC({ -- Table of NPC Data 676 | name = 'testNPC', 677 | model = 'a_m_m_tramp_01', 678 | coords = vector3(-138.8170, -625.3212, 167.8204), 679 | heading = 32.65, 680 | isFrozen = true, 681 | }, { -- Table of Choice Menu Data 682 | title = 'What do you say?', 683 | speech = 'I need to lend $100! Please?! Help a brother out...', 684 | menuID = 'choice-menu-1', -- MUST BE UNIQUE 685 | greeting = 'GENERIC_HOWS_IT_GOING', 686 | position = 'right', 687 | focusCam = true, 688 | options = { 689 | { -- Table of Choice Menu Options 690 | key = 'E', 691 | label = 'No Problem!', 692 | reaction = 'GENERIC_THANKS', 693 | selected = function(data) 694 | print('Accepted offer of ' .. data.menuID) 695 | exports['envi-interact']:CloseMenu(data.menuID) 696 | end, 697 | }, 698 | { 699 | key = 'X', 700 | label = 'Sorry, I\'m too poor!', 701 | reaction = 'GENERIC_CURSE_HIGH', 702 | selected = function(data) 703 | exports['envi-interact']:CloseMenu(data.menuID) 704 | end, 705 | }, 706 | } 707 | }) 708 | 709 | local ped2 = exports['envi-interact']:CreateNPC({ -- Table of NPC Data 710 | name = 'testNPC2', 711 | model = 'a_f_m_downtown_01', 712 | coords = vector3(-138.9446, -633.8333, 167.8205), 713 | heading = 7.75, 714 | isFrozen = true, 715 | focusCam = true 716 | }, { -- Table of Choice Menu Data 717 | title = 'Time to Haggle?', 718 | speech = 'I am willing to sell you this item for $' .. currentOffer .. '!', 719 | menuID = 'choice-menu2', 720 | greeting = 'GENERIC_HI', 721 | position = 'right', 722 | focusCam = true, 723 | options = { 724 | { -- Table of Choice Menu Options 725 | key = 'E', 726 | label = 'Accept Offer', 727 | selected = function(data) 728 | exports['envi-interact']:OpenChoiceMenu({ 729 | title = 'Sale Agreed!', 730 | speech = 'Sounds like a deal! That\'ll cost you $' .. currentOffer .. '!', 731 | speechOptions = { 732 | duration = 1000, 733 | }, 734 | menuID = 'choice-menu3', 735 | position = 'right', 736 | options = { 737 | { 738 | key = 'E', 739 | label = 'Collect Goods', 740 | reaction = 'GENERIC_THANKS', 741 | selected = function(data) 742 | print('bought the item for ' .. currentOffer) 743 | npcRelationship = npcRelationship + 10 744 | exports['envi-interact']:CloseAllMenus() 745 | end, 746 | }, 747 | { 748 | key = 'X', 749 | label = 'Run Away', 750 | reaction = 'GENERIC_CURSE_MED', 751 | selected = function(data) 752 | print('You ran away from ' .. data.menuID) 753 | npcRelationship = npcRelationship - 20 754 | exports['envi-interact']:CloseAllMenus() 755 | end, 756 | } 757 | } 758 | }) 759 | end, 760 | }, 761 | { 762 | key = 'G', 763 | label = 'Counter Offer', 764 | reaction = 'GENERIC_SHOCKED_MED ', 765 | stayOpen = true, 766 | selected = function(data) 767 | print(data.menuID) 768 | local percentageBar = exports['envi-interact']:PercentageBar('percent-bar-name', npcRelationship, 'CURRENT RELATIONSHIP - '..npcRelationship..'%', 'top', 'hover') 769 | exports['envi-interact']:UpdateSpeech(data.menuID, 'Okay then, give me your best offer!') 770 | exports['envi-interact']:UseSlider(data.menuID, { 771 | title = 'Make A Counter Offer?', 772 | min = 1, 773 | max = currentOffer, 774 | sliderState = 'unlocked', 775 | sliderValue = currentOffer, 776 | nextState = 'disabled', 777 | confirm = function(new, old) 778 | print('Confirmed') 779 | print('Value changed from ' .. old .. ' to ' .. new) 780 | if tonumber(new) > tonumber(willAccept) then 781 | print('Counter offer of $' .. new .. ' is greater than the amount I will accept of $' .. willAccept) 782 | print('Counter offer accepted') 783 | npcRelationship = npcRelationship + 10 784 | local percentageBar = exports['envi-interact']:PercentageBar('percent-bar-name', npcRelationship, 'CURRENT RELATIONSHIP - '..npcRelationship..'%', 'top', 'hover') 785 | exports['envi-interact']:OpenChoiceMenu({ 786 | title = 'Sale Agreed!', 787 | speech = 'Sounds like a deal! That\'ll cost you $' .. currentOffer .. '!', 788 | speechOptions = { 789 | duration = 1000, 790 | }, 791 | menuID = 'choice-menu', 792 | position = 'right', 793 | options = { 794 | { 795 | key = 'E', 796 | label = 'Collect Goods', 797 | reaction = 'GENERIC_THANKS', 798 | selected = function(data) 799 | currentOffer = willAccept 800 | print('bought the item for ' .. currentOffer) 801 | npcRelationship = npcRelationship + 5 802 | local percentageBar = exports['envi-interact']:PercentageBar('percent-bar-name', npcRelationship, 'CURRENT RELATIONSHIP - '..npcRelationship..'%', 'top', 'hover') 803 | exports['envi-interact']:CloseAllMenus() 804 | end, 805 | }, 806 | { 807 | key = 'X', 808 | label = 'Run Away', 809 | reaction = 'GENERIC_CURSE_MED', 810 | selected = function(data) 811 | npcRelationship = npcRelationship - 25 812 | local percentageBar = exports['envi-interact']:PercentageBar('percent-bar-name', npcRelationship, 'CURRENT RELATIONSHIP - '..npcRelationship..'%', 'top', 'hover') 813 | print('You ran away from ' .. data.menuID) 814 | exports['envi-interact']:CloseAllMenus() 815 | end, 816 | } 817 | } 818 | }) 819 | else 820 | print('Counter offer of $' .. new .. ' is less than the amount I will accept of $' .. willAccept) 821 | print('Counter offer rejected') 822 | currentOffer = new 823 | npcRelationship = npcRelationship - 10 824 | local percentageBar = exports['envi-interact']:PercentageBar('percent-bar-name', npcRelationship, 'CURRENT RELATIONSHIP - '..npcRelationship..'%', 'top', 'hover') 825 | exports['envi-interact']:OpenChoiceMenu({ 826 | title = 'Final Offer!', 827 | speech = 'No way! I can\'t sell you this item for $' .. currentOffer .. '! The lowest I can take is $' .. willAccept .. '!', 828 | speechOptions = { 829 | duration = 1000, 830 | }, 831 | menuID = 'choice-menu', 832 | position = 'right', 833 | options = { 834 | { 835 | key = 'E', 836 | label = 'Accept Offer', 837 | selected = function(data) 838 | npcRelationship = npcRelationship + 10 839 | local percentageBar = exports['envi-interact']:PercentageBar('percent-bar-name', npcRelationship, 'CURRENT RELATIONSHIP - '..npcRelationship..'%', 'top', 'hover') 840 | exports['envi-interact']:OpenChoiceMenu({ 841 | title = 'Sale Agreed!', 842 | speech = 'Sounds like a deal! That\'ll cost you $' .. currentOffer .. '!', 843 | speechOptions = { 844 | duration = 1000, 845 | }, 846 | menuID = 'choice-menu-agreed', 847 | position = 'right', 848 | options = { 849 | { 850 | key = 'E', 851 | label = 'Pay & Collect Goods', 852 | reaction = 'GENERIC_THANKS', 853 | selected = function(data) 854 | currentOffer = willAccept 855 | npcRelationship = npcRelationship + 10 856 | local percentageBar = exports['envi-interact']:PercentageBar('percent-bar-name', npcRelationship, 'CURRENT RELATIONSHIP - '..npcRelationship..'%', 'top', 'hover') 857 | print('bought the item for ' .. currentOffer) 858 | Wait(500) 859 | exports['envi-interact']:CloseAllMenus() 860 | end, 861 | }, 862 | { 863 | key = 'X', 864 | label = 'Run Away', 865 | reaction = 'GENERIC_CURSE_MED', 866 | selected = function(data) 867 | npcRelationship = npcRelationship - 25 868 | local percentageBar = exports['envi-interact']:PercentageBar('percent-bar-name', npcRelationship, 'CURRENT RELATIONSHIP - '..npcRelationship..'%', 'top', 'hover') 869 | print('You ran away from ' .. data.menuID) 870 | Wait(500) 871 | exports['envi-interact']:CloseAllMenus() 872 | end, 873 | } 874 | } 875 | }) 876 | end, 877 | }, 878 | { 879 | key = 'X', 880 | label = 'Leave Conversation', 881 | reaction = 'GENERIC_BYE', 882 | selected = function(data) 883 | npcRelationship = npcRelationship - 50 884 | local percentageBar = exports['envi-interact']:PercentageBar('percent-bar-name', npcRelationship, 'CURRENT RELATIONSHIP - '..npcRelationship..'%', 'top', 'hover') 885 | print('Fucked off from ' .. data.menuID) 886 | Wait(500) 887 | exports['envi-interact']:CloseAllMenus() 888 | end, 889 | } 890 | } 891 | }) 892 | end 893 | end 894 | }) 895 | end, 896 | }, 897 | { 898 | key = 'X', 899 | label = 'Never Mind', 900 | reaction = 'GENERIC_BYE', 901 | selected = function(data) 902 | exports['envi-interact']:CloseAllMenus() 903 | end, 904 | }, 905 | } 906 | }) 907 | 908 | local ped3 = exports['envi-interact']:CreateNPC({ -- Table of NPC Data 909 | name = 'testNPC3', 910 | model = 'a_m_m_og_boss_01', 911 | coords = vector3(-149.2743, -632.5180, 167.8326), 912 | heading = 280.90, 913 | isFrozen = true, 914 | }, { -- Table of Choice Menu Data 915 | title = 'Oh No!!', 916 | speech = 'I am robbing you. Give me all your money!', 917 | menuID = 'choice-menu4', 918 | greeting = 'GENERIC_CURSE_HIGH', 919 | position = 'right', 920 | options = { 921 | { -- Table of Choice Menu Options 922 | key = 'E', 923 | label = 'Accept Being Robbed', 924 | reaction = 'GENERIC_CURSE_MED', 925 | selected = function(data) 926 | exports['envi-interact']:CloseMenu(data.menuID) 927 | end, 928 | }, 929 | { 930 | key = 'X', 931 | label = 'Run for Your Life', 932 | reaction = 'GENERIC_CURSE_HIGH', 933 | selected = function(data) 934 | exports['envi-interact']:CloseMenu(data.menuID) 935 | end, 936 | }, 937 | } 938 | }) 939 | 940 | 941 | exports['envi-interact']:InteractionPoint(vector3(-136.4381, -629.6122, 168.8205),{ 942 | name = 'testInteractionPoint', 943 | distance = 1.0, 944 | radius = 1.0, 945 | options = { 946 | { 947 | label = 'Check Center Cupboard', 948 | selected = function(data) 949 | lib.notify({ 950 | title = 'Nothing here..', 951 | description = 'You searched the center cupboard and found nothing!', 952 | type = 'error' 953 | }) 954 | end, 955 | }, 956 | { 957 | label = 'Check Left Cupboard', 958 | selected = function(data) 959 | lib.notify({ 960 | title = 'Nothing here..', 961 | description = 'You searched the left cupboard and found nothing!', 962 | type = 'error' 963 | }) 964 | end, 965 | }, 966 | { 967 | label = 'Check Right Cupboard', 968 | selected = function(data) 969 | lib.notify({ 970 | title = 'Found something!', 971 | description = 'You searched the right cupboard and found $500!', 972 | type = 'success' 973 | }) 974 | exports['envi-interact']:OpenChoiceMenu({ 975 | title = 'Found $500 - Take it or Leave it?!', 976 | menuID = 'decision-menu', 977 | position = 'right', 978 | options = { 979 | { 980 | key = 'E', 981 | label = 'Take it!', 982 | selected = function(data) 983 | lib.notify({ 984 | title = 'You took the money!', 985 | description = 'You took the money without anybody noticing!', 986 | type = 'error' 987 | }) 988 | exports['envi-interact']:CloseAllMenus() 989 | end 990 | }, 991 | { 992 | key = 'X', 993 | label = 'Leave it!', 994 | selected = function(data) 995 | lib.notify({ 996 | title = 'You left the money behind!', 997 | description = 'You left the money behind!', 998 | type = 'success' 999 | }) 1000 | exports['envi-interact']:CloseAllMenus() 1001 | end 1002 | } 1003 | } 1004 | }) 1005 | end, 1006 | }, 1007 | } 1008 | }) 1009 | 1010 | 1011 | CreateThread(function() 1012 | exports['envi-interact']:InteractionModel(GetHashKey('prop_atm_01'), { 1013 | { 1014 | name = 'atm_interaction', 1015 | distance = 2.0, 1016 | radius = 5.0, 1017 | options = { 1018 | { 1019 | label = '[E] - Use ATM', 1020 | selected = function(data) 1021 | exports['envi-interact']:OpenChoiceMenu({ 1022 | title = 'ATM Menu', 1023 | menuID = 'atm_menu', 1024 | speech = 'Welcome to the ATM. What do you want to do?', 1025 | position = 'right', 1026 | options = { 1027 | { 1028 | key = 'W', 1029 | label = 'Withdraw Money', 1030 | selected = function(data) 1031 | -- Handle withdrawal logic 1032 | exports['envi-interact']:UpdateSpeech('atm_menu', 'Please enter the amount to withdraw') 1033 | end, 1034 | canSee = function() 1035 | local hasItem = Framework.HasItem('bank_card') 1036 | if hasItem then 1037 | return true 1038 | else 1039 | return false 1040 | end 1041 | end 1042 | }, 1043 | { 1044 | key = 'D', 1045 | label = 'Deposit Money', 1046 | selected = function(data) 1047 | -- Handle deposit logic 1048 | exports['envi-interact']:UpdateSpeech('atm_menu', 'Please enter the amount to deposit') 1049 | end 1050 | }, 1051 | { 1052 | key = 'C', 1053 | label = 'Check Balance', 1054 | selected = function(data) 1055 | -- Handle balance check 1056 | exports['envi-interact']:UpdateSpeech('atm_menu', 'Your current balance is $5,000') 1057 | end 1058 | } 1059 | } 1060 | }) 1061 | end, 1062 | }, 1063 | { 1064 | label = '[E] - Deposit Cheque', 1065 | selected = function(data) 1066 | exports['envi-interact']:OpenChoiceMenu({ 1067 | title = 'Deposit Cheque', 1068 | menuID = 'deposit_cheque', 1069 | position = 'right', 1070 | options = { 1071 | { 1072 | key = 'E', 1073 | label = 'Deposit Cheque', 1074 | selected = function(data) 1075 | exports['envi-interact']:UpdateSpeech('deposit_cheque', 'Please enter the amount to deposit') 1076 | end 1077 | } 1078 | } 1079 | }) 1080 | end, 1081 | }, 1082 | { 1083 | label = '[E] - Rob ATM', 1084 | selected = function(data) 1085 | exports['envi-interact']:OpenChoiceMenu({ 1086 | title = 'Rob ATM', 1087 | menuID = 'rob_atm', 1088 | position = 'right', 1089 | options = { 1090 | { 1091 | key = 'E', 1092 | label = 'Rob ATM', 1093 | selected = function(data) 1094 | exports['envi-interact']:UpdateSpeech('rob_atm', 'Please enter the amount to rob') 1095 | end 1096 | } 1097 | } 1098 | }) 1099 | end, 1100 | } 1101 | } 1102 | } 1103 | }) 1104 | end) -------------------------------------------------------------------------------- /client/pedPersonality.lua: -------------------------------------------------------------------------------- 1 | PositiveReaction = { 2 | "GENERIC_THANKS", -- Thanks or gratitude. 3 | "AGREE_ACROSS_STREET", -- A friendly agreement. 4 | "CHAT_RESP", -- Generic chat response. 5 | "CHAT_STATE", -- Comment on one’s state (neutral). 6 | "GENERIC_SHOCKED_MED", -- Moderate shock. 7 | 8 | } 9 | 10 | -- Negative reactions: expressions of failure, disappointment or a “bad outcome.” 11 | NegativeReaction = { 12 | "GENERIC_CURSE_HIGH", -- Strong curse (negative tone). 13 | "GENERIC_CURSE_MED", -- Moderate curse. 14 | "GENERIC_INSULT_HIGH", -- High-level insult. 15 | "GENERIC_INSULT_MED", -- Moderate insult. 16 | "JACKED_GENERIC", -- Reaction when being robbed. 17 | "GUN_BEG", -- Pleading for a gun (a negative, desperate tone). 18 | "RUN_OVER_PLAYER", -- A reaction to something very negative. 19 | "TRAPPED", -- Feeling stuck. 20 | "WON_DISPUTE", -- Celebration of having won a dispute. 21 | "CHALLENGE_ACCEPTED_GENERIC", -- A positive acceptance of a challenge. 22 | "CHALLENGE_ACCEPTED_BUMPED_INTO", -- Ambiguous challenge acceptance when bumped into. 23 | "BUMP", -- A short exclamation when bumped. 24 | "BLOCKED_GENERIC", -- A generic placeholder (can be used neutrally). 25 | "GENERIC_SHOCKED_MED", -- Moderate shock. 26 | 27 | } 28 | 29 | -- Angry reactions: these are overtly aggressive or insulting. 30 | AngryReaction = { 31 | "CHALLENGE_THREATEN", -- A threatening challenge. 32 | "PROVOKE_GENERIC", -- Provoking or goading. 33 | "PROVOKE_STARING", -- Aggressive staring. 34 | "GENERIC_INSULT_HIGH", -- High-level insult. 35 | "GENERIC_CURSE_HIGH", -- Strong curse (negative tone). 36 | 37 | } 38 | 39 | -- Neutral reactions: everyday greetings, farewells or generic responses. 40 | NeutralReaction = { 41 | "GENERIC_WHATEVER", -- Dismissive but neutral. 42 | "DODGE", -- An exclamation when dodging (neutral reaction to danger). 43 | "LOOKING_AT_PHONE", -- A noncommittal reaction (e.g. “I’m just checking my phone”). 44 | "GENERIC_SHOCKED_MED", -- Moderate shock. 45 | } 46 | 47 | ScaredReaction = { 48 | "GENERIC_FRIGHTENED_HIGH", -- Extreme fear. 49 | "GENERIC_FRIGHTENED_MED", -- Moderate fear. 50 | "GENERIC_SHOCKED_HIGH", -- Extreme shock. 51 | 52 | } 53 | 54 | Hello = { 55 | "GENERIC_HI", -- A simple greeting. 56 | "GENERIC_HOWS_IT_GOING", -- “How’s it going?” (casual inquiry). 57 | "KIFFLOM_GREET", -- A friendly greeting. 58 | "CHAT_STATE", -- Comment on one’s state (neutral). 59 | } 60 | 61 | Bye = { 62 | "GENERIC_BYE", -- Casual goodbye. 63 | "GOODBYE_ACROSS_STREET", -- A farewell. 64 | } 65 | 66 | Yes = { 67 | "GENERIC_YES", -- An affirmative “yes.” 68 | } 69 | 70 | No = { 71 | "GENERIC_NO", -- A simple “no.” 72 | } 73 | 74 | Thanks = { 75 | "GENERIC_THANKS", -- Thanks or gratitude. 76 | } 77 | 78 | OverThere = { 79 | "OVER_THERE", -- Directional/observational (neutral). 80 | } 81 | 82 | Conversation = { 83 | "CHAT_ACROSS_STREET_RESP", -- Response in a street conversation. 84 | "CHAT_ACROSS_STREET_STATE", -- State of the conversation across the street. 85 | "CHAT_RESP", -- Generic chat response. 86 | "CHAT_STATE", -- Comment on one’s state (neutral). 87 | "PED_RANT_01", -- A full-on rant. 88 | "GENERIC_SHOCKED_MED", -- Moderate shock. 89 | } 90 | 91 | Apology = { 92 | "APOLOGY_NO_TROUBLE", -- “Don’t worry about it.” 93 | "GENERIC_SHOCKED_MED", -- Moderate shock. 94 | } 95 | 96 | HitCar = { 97 | "CHALLENGE_ACCEPTED_HIT_CAR", -- You hit my car! 98 | "CRASH_GENERIC", -- Reaction to a generic crash. 99 | } 100 | 101 | Tour = { 102 | "TOUR_ABOUT_TO_START", -- Setting the stage (neutral). 103 | "TOUR_CHAT", -- Casual tour conversation. 104 | "TOUR_LANDMARK", -- Commentary on a landmark. 105 | } -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | Config = {} 2 | Config.TextState = false 3 | Config.CurrentLabel = nil 4 | Config.DefaultTypeDelay = 2000 -- 2 Seconds 5 | Config.EnableDebugLine = false 6 | ShownNoti = false 7 | 8 | function ShowText(label, state, options) 9 | if state == TextState and label == CurrentLabel then 10 | return 11 | end 12 | TextState = state 13 | CurrentLabel = label 14 | if state then 15 | lib.showTextUI(label) 16 | if not ShownNoti and #options > 1 then 17 | local text = 'USE SCROLL-WHEEL FOR MORE OPTIONS' 18 | local time = 4000 19 | local icon = 'arrow-down' 20 | lib.notify({ 21 | id = 'interaction', 22 | title = 'Envi-Interact', 23 | description = text, 24 | position = 'top-right', 25 | duration = time, 26 | style = { 27 | backgroundColor = '#141517', 28 | color = '#C1C2C5', 29 | ['.description'] = { 30 | color = '#909296' 31 | } 32 | }, 33 | icon = icon, 34 | iconColor = '#A020F0' 35 | }) 36 | ShownNoti = true 37 | end 38 | else 39 | lib.hideTextUI() 40 | ShownNoti = false 41 | CurrentLabel = nil 42 | end 43 | end 44 | 45 | function HideText() 46 | lib.hideTextUI() 47 | end 48 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | 3 | author 'Envi-Scripts' 4 | description 'Interaction Menus' 5 | version '1.2.0' 6 | 7 | game 'gta5' 8 | lua54 'yes' 9 | 10 | client_scripts { 11 | 'client/pedPersonality.lua', 12 | 'client/*.lua', 13 | } 14 | 15 | shared_scripts { 16 | 'config.lua', 17 | '@ox_lib/init.lua' 18 | } 19 | 20 | files { 21 | 'ui/**' 22 | } 23 | 24 | ui_page 'ui/index.html' -------------------------------------------------------------------------------- /ui/app.js: -------------------------------------------------------------------------------- 1 | let i = 0; 2 | let typingComplete = false; 3 | let currentMenuID = null; 4 | let keysBusy = false; 5 | 6 | $(function() { 7 | function typeWriter(textParam, callback, element, totalDuration) { 8 | let i = 0; 9 | const text = textParam; 10 | const baseDelay = calculateDynamicDelay(text.length); 11 | const dynamicDelay = totalDuration ? (totalDuration / text.length) : baseDelay; 12 | 13 | function type() { 14 | if (i < text.length) { 15 | $(element).append(text.charAt(i)); 16 | i++; 17 | setTimeout(type, dynamicDelay); 18 | } else { 19 | callback(); 20 | } 21 | } 22 | 23 | type(); 24 | } 25 | 26 | function calculateDynamicDelay(length) { 27 | if (length <= 50) { 28 | return 40; 29 | } else if (length <= 100) { 30 | return 30; 31 | } else { 32 | return 20; 33 | } 34 | }; 35 | 36 | function createChoiceMenu(data) { 37 | if (currentMenuID) { 38 | // $(`.choice__menu[data-choice-menu="${currentMenuID}"]`).fadeOut("150", function() { 39 | // $(this).remove(); 40 | displayNewMenu(data); 41 | //}); 42 | } else { 43 | displayNewMenu(data); 44 | } 45 | } 46 | 47 | function displayNewMenu(data) { 48 | keysBusy = false; 49 | i = 0; 50 | typingComplete = false; 51 | 52 | $(`.choice__menu`).remove(); 53 | $(`.bubble__chat--wrapper`).remove(); 54 | 55 | $("body").fadeIn("150"); 56 | 57 | $("#container").append(` 58 | 72 | `); 73 | 74 | if (data.speech) { 75 | $("#container").append(` 76 | 82 | `); 83 | $(`.bubble__chat--wrapper[data-menu-id="${data.menuID}"]`).fadeIn("150", function() { 84 | typeWriter(data.speech, () => { 85 | typingComplete = true; 86 | $(".option__item").removeClass('disabled'); 87 | $('.slider__wrapper').find(".slider").prop('disabled', false).removeClass('locked'); 88 | $('.slider__wrapper').find(".slider__submit").prop('disabled', false).removeClass('locked'); 89 | }, `#text-${data.menuID}`, data.duration); 90 | }); 91 | } 92 | else { 93 | typingComplete = true; 94 | }; 95 | $(".choice__menu").fadeIn("150"); 96 | keysBusy = false; 97 | $(".option__item").on('click', function(e) { 98 | e.preventDefault(); 99 | if (!typingComplete) return; 100 | keysBusy = true; 101 | 102 | let getStayOpen = $(this).data('stay-open'); 103 | let getCloseAll = $(this).data('close-all'); 104 | 105 | if (!getStayOpen) { 106 | $(".choice__menu").fadeOut("150"); 107 | } 108 | 109 | const key = $(this).data('key'); 110 | const option = data.options.find(opt => opt.key === key); 111 | const speech = option.speech; 112 | const reaction = option.reaction; 113 | 114 | $.post('https://envi-interact/selectOption', JSON.stringify({ 115 | menuID: data.menuID, 116 | key: key, 117 | selected: option.selected, 118 | speech: speech, 119 | closeAll: getCloseAll, 120 | stayOpen: getStayOpen, 121 | reaction: reaction 122 | })); 123 | 124 | closeMenuInternal(currentMenuID); 125 | currentMenuID = data.menuID; 126 | setTimeout(() => { 127 | keysBusy = false; 128 | }, 1500); 129 | 130 | }); 131 | 132 | $(document).off('keydown').on('keydown', function(e) { 133 | if (!typingComplete || keysBusy) return; 134 | keysBusy = true; 135 | setTimeout(() => { 136 | keysBusy = false; 137 | }, 1500); 138 | const pressedKey = String.fromCharCode(e.which); 139 | const option = data.options.find(opt => opt.key === pressedKey); 140 | const speech = $(this).data('speech'); 141 | const reaction = $(this).data('reaction'); 142 | 143 | if (option) { 144 | e.preventDefault(); 145 | $(".option__item").removeClass('active'); 146 | const $activeItem = $(`.option__item[data-key="${option.key}"]`).addClass('active'); 147 | 148 | let getStayOpen = $activeItem.data('stay-open'); 149 | let getCloseAll = $activeItem.data('close-all'); 150 | if (!getStayOpen) { 151 | $(".choice__menu").fadeOut("150"); 152 | } 153 | 154 | $.post('https://envi-interact/selectOption', JSON.stringify({ 155 | menuID: data.menuID, 156 | key: option.key, 157 | selected: option.selected, 158 | speech: speech, 159 | closeAll: getCloseAll, 160 | stayOpen: getStayOpen, 161 | reaction: reaction 162 | })); 163 | } else if (e.which === 27) { 164 | e.preventDefault(); 165 | if (data.onESC) { 166 | $.post('https://envi-interact/escPressed', JSON.stringify({ 167 | menuID: data.menuID 168 | })); 169 | } 170 | closeAllMenus(); 171 | } 172 | }); 173 | 174 | $(document).off('keyup').on('keyup', function(e) { 175 | const pressedKey = String.fromCharCode(e.which); 176 | const option = data.options.find(opt => opt.key === pressedKey); 177 | 178 | if (option) { 179 | const $activeItem = $(`.option__item[data-key="${option.key}"]`); 180 | $activeItem.removeClass('active'); 181 | } 182 | }); 183 | 184 | data.options.forEach(option => { 185 | const optionElement = ` 186 |
187 |
${option.key}
188 |
${option.label}
189 |
190 | `; 191 | $(".choice__menu__options").append(optionElement); 192 | }); 193 | 194 | data.options.forEach(option => { 195 | if (option.canSee) { 196 | $.post('https://envi-interact/selectOption', JSON.stringify({ 197 | menuID: data.menuID, 198 | key: option.key, 199 | checkcanSee: true 200 | }), function(response) { 201 | if (response === 0) { 202 | $(`.option__item[data-key="${option.key}"]`).addClass('disabled'); 203 | } 204 | }); 205 | } 206 | }); 207 | 208 | }; 209 | 210 | function updateSpeech(data) { 211 | i = 0; 212 | typingComplete = false; 213 | const textElement = `#text-${data.menuID}`; 214 | 215 | $(textElement).empty(); 216 | 217 | typeWriter(data.speech, () => { 218 | typingComplete = true; 219 | $(".option__item").removeClass('disabled'); 220 | $('.slider__wrapper').find(".slider").prop('disabled', false).removeClass('locked'); 221 | $('.slider__wrapper').find(".slider__submit").prop('disabled', false).removeClass('locked'); 222 | $.post('https://envi-interact/speechComplete', JSON.stringify({ 223 | menuID: data.menuID 224 | })); 225 | }, textElement, data.duration); 226 | }; 227 | 228 | function useSlider(data) { 229 | let $menu = $(`.choice__menu[data-choice-menu="${data.menuID}"]`); 230 | 231 | if ($menu.length) { 232 | 233 | let $sliderWrapper = $menu.find('.slider__wrapper'); 234 | 235 | if ($sliderWrapper.length === 0) { 236 | $menu.append(` 237 |
238 |
239 |
240 | 241 | 242 |
243 |
${data.title} ${data.sliderValue}
244 |
245 | `); 246 | 247 | if (data.sliderState === 'locked' || !typingComplete) { 248 | $('.slider__wrapper').find(".slider").prop('disabled', true).addClass('locked'); 249 | $('.slider__wrapper').find(".slider__submit").prop('disabled', true).addClass('locked'); 250 | 251 | } else { 252 | $('.slider__wrapper').find(".slider").prop('disabled', false).removeClass('locked'); 253 | $('.slider__wrapper').find(".slider__submit").prop('disabled', false).removeClass('locked'); 254 | }; 255 | 256 | const updateBubble = () => { 257 | const value = $('.slider__wrapper').find(".slider").val(); 258 | $('.slider__wrapper').find("#slider-value").text(value); 259 | }; 260 | 261 | $('.slider__wrapper').find(".slider").on('input', updateBubble); 262 | 263 | $('.slider__wrapper').find(".slider__submit").on('click', function() { 264 | let newValue = $('.slider__wrapper').find(".slider").val(); 265 | 266 | $.post('https://envi-interact/sliderConfirm', JSON.stringify({ 267 | menuID: data.menuID, 268 | sliderValue: newValue, 269 | oldValue: data.sliderValue, 270 | nextState: data.nextState 271 | })).done(function(res) { 272 | if (res === 'hideSlider') { 273 | $('.slider__wrapper') 274 | .removeClass('animate__fadeInUp animate__faster') 275 | .addClass('animate__fadeOutDown animate__faster'); 276 | setTimeout(() => { 277 | $('.slider__wrapper').hide().removeClass('animate__fadeOutDown animate__faster'); 278 | }, 500); 279 | } else if (res === 'lockSlider') { 280 | $('.slider__wrapper').find(".slider").prop('disabled', true).addClass('locked'); 281 | $('.slider__wrapper').find(".slider__submit").prop('disabled', true).addClass('locked'); 282 | 283 | } else if (res === 'unlockSlider') { 284 | $('.slider__wrapper').find(".slider").prop('disabled', false).removeClass('locked'); 285 | $('.slider__wrapper').find(".slider__submit").prop('disabled', false).removeClass('locked'); 286 | } 287 | }); 288 | }); 289 | } else { 290 | $sliderWrapper.show().addClass('animate__fadeInUp animate__faster'); 291 | } 292 | } 293 | }; 294 | 295 | function forceUpdateSlider(data) { 296 | let $menu = $(`.choice__menu[data-choice-menu="${data.menuID}"]`); 297 | if ($menu.length) { 298 | let $sliderWrapper = $menu.find(".slider__wrapper"); 299 | $sliderWrapper.find(".slider").val(data.sliderValue); 300 | $sliderWrapper.find("#slider-value").text(data.sliderValue); 301 | } 302 | }; 303 | 304 | function createPercentageBar(data) { 305 | $("body").fadeIn("150"); 306 | let $percentageWrapper = $(`.percentage__wrapper[data-percent-id="${data.percentID}"]`); 307 | 308 | if ($percentageWrapper.length === 0) { 309 | let positionClass; 310 | let dimensionStyle; 311 | 312 | switch (data.position) { 313 | case 'top': 314 | positionClass = 'tooltip-bottom'; 315 | dimensionStyle = `width: 0; height: 100%;`; 316 | break; 317 | case 'bottom': 318 | positionClass = 'tooltip-top'; 319 | dimensionStyle = `width: 0; height: 100%;`; 320 | break; 321 | case 'left': 322 | positionClass = 'tooltip-top-left'; 323 | dimensionStyle = `width: 100%; height: 0;`; 324 | break; 325 | case 'right': 326 | positionClass = 'tooltip-top-right'; 327 | dimensionStyle = `width: 100%; height: 0;`; 328 | break; 329 | default: 330 | positionClass = 'tooltip-top-left'; 331 | dimensionStyle = `width: 100%; height: 0;`; 332 | break; 333 | } 334 | 335 | $("#container").append(` 336 | 341 | `); 342 | 343 | $percentageWrapper = $(`.percentage__wrapper[data-percent-id="${data.percentID}"]`); 344 | 345 | $percentageWrapper.fadeIn("150", function() { 346 | const dimension = (data.position === 'left' || data.position === 'right') ? 'height' : 'width'; 347 | const $innerContainer = $percentageWrapper.find('.percentage__inner--container'); 348 | updateColor($innerContainer, data.percentage, dimension, data.colors); 349 | }); 350 | 351 | $(document).on('keydown.percentageBar', function(e) { 352 | if (e.which === 27) { 353 | e.preventDefault(); 354 | closeAllMenus(); 355 | } 356 | }); 357 | } else { 358 | $percentageWrapper.show(); 359 | const dimension = (data.position === 'left' || data.position === 'right') ? 'height' : 'width'; 360 | const $innerContainer = $percentageWrapper.find('.percentage__inner--container'); 361 | updateColor($innerContainer, data.percentage, dimension, data.colors); 362 | $percentageWrapper.find('.percentage__container') 363 | .attr('data-tooltip', `${data.title}`) 364 | .removeClass('tooltip-none tooltip-hover tooltip-always') 365 | .addClass(`tooltip-${data.tooltip}`); 366 | } 367 | }; 368 | 369 | function updateColor($element, percentage, dimension, colors) { 370 | let color; 371 | if (percentage <= 24) { 372 | color = colors.c1 || 'red'; 373 | } else if (percentage <= 75) { 374 | color = colors.c2 || '#FFC000'; 375 | } else { 376 | color = colors.c3 || 'green'; 377 | } 378 | 379 | $element.css('background-color', color); 380 | $element.css(dimension, `${percentage}%`); 381 | }; 382 | 383 | function closeMenu(data) { 384 | let $menu = $(`.choice__menu[data-choice-menu="${data.menuID}"]`); 385 | let $percentage = $(`.percentage__wrapper[data-percent-id="${data.percentID}"]`); 386 | 387 | if ($menu.length) { 388 | $menu.fadeOut("150"); 389 | $(".bubble__chat--wrapper").fadeOut("150", function() { 390 | $(this).find('#text').empty(); 391 | }); 392 | } 393 | 394 | if ($percentage.length) { 395 | $percentage.fadeOut("150"); 396 | } 397 | 398 | $.post('https://envi-interact/close', JSON.stringify({ menuID: data.menuID })); 399 | }; 400 | 401 | function closeMenuInternal(data) { 402 | if (!data || !data.menuID) { 403 | return; 404 | } 405 | let $menu = $(`.choice__menu[data-choice-menu="${data.menuID}"]`); 406 | let $percentage = $(`.percentage__wrapper[data-percent-id="${data.percentID}"]`); 407 | $menu.fadeOut("150"); 408 | $(".bubble__chat--wrapper").fadeOut("150", function() { 409 | $(this).find('#text').empty(); 410 | }); 411 | 412 | if ($percentage.length) { 413 | $percentage.fadeOut("150"); 414 | } 415 | 416 | }; 417 | 418 | 419 | function closeAllMenus() { 420 | $(".choice__menu").fadeOut("150"); 421 | $(".percentage__wrapper").fadeOut("150"); 422 | $(".bubble__chat--wrapper").fadeOut("150", function() { 423 | $(this).find('#text').empty(); 424 | }); 425 | 426 | $.post('https://envi-interact/closeAll', JSON.stringify({})); 427 | } 428 | 429 | $(document).on('keydown', function(e) { 430 | if (e.which === 27) { 431 | closeAllMenus(); 432 | } 433 | }); 434 | 435 | window.addEventListener('message', function(e) { 436 | let data = e?.data; 437 | 438 | switch (data.action) { 439 | case "openChoiceMenu": 440 | createChoiceMenu(data); 441 | break; // Added break here 442 | case "openPedMenu": 443 | createChoiceMenu(data); 444 | break; 445 | case "updateSpeech": 446 | updateSpeech(data); 447 | break; 448 | case "useSlider": 449 | useSlider(data); 450 | break; 451 | case "forceUpdateSlider": 452 | forceUpdateSlider(data); 453 | break; 454 | case "closeMenu": 455 | closeMenu(data); 456 | break; 457 | case "percentageBar": 458 | createPercentageBar(data); 459 | break; 460 | case "updatePercentageBar": 461 | updatePercentageBar(data); 462 | break; 463 | default: 464 | // console.log(data); 465 | break; 466 | } 467 | }); 468 | }) -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Envi Interaction System 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ui/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap'); 2 | 3 | * { 4 | -webkit-box-sizing: border-box; 5 | -moz-box-sizing: border-box; 6 | box-sizing: border-box; 7 | -moz-user-select: none; 8 | -khtml-user-select: none; 9 | -webkit-user-select: none; 10 | -ms-user-select: none; 11 | user-select: none; 12 | list-style: none; 13 | text-decoration: none; 14 | font-family: 'DM Sans', sans-serif; 15 | } 16 | 17 | :root { 18 | --main-font-transform: uppercase; 19 | --main-light-font: 400; 20 | --main-bold-font: 700; 21 | --main-normal-style: normal; 22 | --main-shadow-font: 0px 4px 4px rgba(0, 0, 0, 0.25); 23 | --main-box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 24 | --overlay-multiplier: 1; 25 | } 26 | 27 | ::-webkit-scrollbar { 28 | display: none; 29 | } 30 | 31 | html { 32 | font-size: 62.5%; 33 | } 34 | 35 | html, body { 36 | height: 100%; 37 | } 38 | 39 | body { 40 | margin: 0; 41 | padding: 0; 42 | overflow: hidden; 43 | display: none; 44 | } 45 | 46 | #container { 47 | position: relative; 48 | left: 0; 49 | top: 0; 50 | width: 100vw; 51 | height: 100vh; 52 | display:flex; 53 | justify-content: center; 54 | align-items: center; 55 | } 56 | 57 | .choice__menu { 58 | position: absolute; 59 | width: 23.25vw; 60 | bottom: 3.5vh; 61 | } 62 | 63 | .choice__menu.right { 64 | right: 2vw; 65 | } 66 | 67 | .choice__menu.left { 68 | left: 2vw; 69 | } 70 | 71 | .choice__menu.center { 72 | left: 50%; 73 | transform: translate(-50%, 0); 74 | } 75 | 76 | .choice__container { 77 | padding: 2.25vh 1vw; 78 | min-width: 20vw; 79 | width: 100%; 80 | border-radius: .85vh; 81 | position: relative; 82 | } 83 | 84 | .option__wrapper { 85 | width: 100%; 86 | display: flex; 87 | align-items: center; 88 | justify-content: center; 89 | flex-wrap: wrap; 90 | gap: 1.5vh; 91 | position: relative; 92 | z-index: 999; 93 | } 94 | 95 | .choice__title { 96 | width: 100%; 97 | font-size: 1.6vh; 98 | font-weight: var(--main-bold-font); 99 | letter-spacing: .45vh; 100 | text-transform: var(--main-font-transform); 101 | text-align: center; 102 | margin-bottom: 1.65vh; 103 | line-height: 2.45vh; 104 | display: -webkit-box; 105 | -webkit-line-clamp: 2; 106 | -webkit-box-orient: vertical; 107 | overflow: hidden; 108 | position: relative; 109 | z-index: 999; 110 | } 111 | 112 | .option__item { 113 | width: 10vw; 114 | height: 4vh; 115 | border-radius: .4vh; 116 | cursor: pointer; 117 | display: flex; 118 | align-items: center; 119 | gap: .6vh; 120 | transition: .25s ease; 121 | } 122 | 123 | .option__item:hover { 124 | transform: scale(.97); 125 | } 126 | 127 | .option__item .option__key { 128 | width: 20%; 129 | height: 100%; 130 | display: flex; 131 | align-items: center; 132 | justify-content: center; 133 | text-transform: var(--main-font-transform); 134 | font-weight: var(--main-bold-font); 135 | font-size: 1.6vh; 136 | border-top-left-radius: .4vh; 137 | border-bottom-left-radius: .4vh; 138 | transition: .25s ease; 139 | } 140 | 141 | .option__item .option__content { 142 | width: 80%; 143 | height: 100%; 144 | display: flex; 145 | align-items: center; 146 | justify-content: center; 147 | border-top-right-radius: .4vh; 148 | border-bottom-right-radius: .4vh; 149 | font-size: 1.3vh; 150 | font-weight: 500; 151 | text-transform: var(--main-font-transform); 152 | text-align: center; 153 | transition: all .25s ease; 154 | } 155 | 156 | .option__item:hover .option__content, 157 | .option__item.active .option__content { 158 | font-weight: var(--main-bold-font); 159 | } 160 | 161 | .slider__wrapper { 162 | display: flex; 163 | align-items: flex-start; 164 | flex-direction: column; 165 | position: relative; 166 | padding: 2.25vh 1vw; 167 | margin-top: 1.5vh; 168 | display: flex; 169 | justify-content: center; 170 | align-items: flex-start; 171 | border-radius: .85vh; 172 | } 173 | 174 | .slider__container { 175 | width: 100%; 176 | position: relative; 177 | display: flex; 178 | align-items: center; 179 | gap: 1.45vh; 180 | margin-bottom: .6vh; 181 | z-index: 3; 182 | } 183 | 184 | .slider { 185 | -webkit-appearance: none; 186 | appearance: none; 187 | width: 75%; 188 | cursor: pointer; 189 | outline: none; 190 | overflow: hidden; 191 | border-radius: .85vh; 192 | } 193 | 194 | .slider.locked { 195 | opacity: .75; 196 | cursor: not-allowed; 197 | } 198 | 199 | .slider::-webkit-slider-runnable-track { 200 | height: 1.75vh; 201 | border-radius: .85vh; 202 | } 203 | 204 | .slider::-webkit-slider-thumb { 205 | -webkit-appearance: none; 206 | appearance: none; 207 | height: 1.75vh; 208 | width: 1.8vh; 209 | border-radius: 50%; 210 | } 211 | 212 | .slider::-moz-range-thumb { 213 | height: 1.85vh; 214 | width: 1.75vh; 215 | border-radius: 50%; 216 | } 217 | 218 | 219 | .slider::-moz-range-track { 220 | height: 1.85vh; 221 | border-radius: .85vh; 222 | } 223 | 224 | .slider__submit { 225 | width: 25%; 226 | height: 4vh; 227 | text-transform: var(--main-font-transform); 228 | outline: none; 229 | border: none; 230 | cursor: pointer; 231 | font-size: 1.5vh; 232 | border-radius: .4vh; 233 | font-weight: 500; 234 | transition: .25s ease; 235 | } 236 | 237 | .slider__submit.locked { 238 | box-shadow: none; 239 | cursor: not-allowed; 240 | } 241 | 242 | .slider__content { 243 | font-size: 1.5vh; 244 | z-index: 3; 245 | } 246 | 247 | .overlay { 248 | position: absolute; 249 | left: 0; 250 | top: 0; 251 | width: 100%; 252 | height: 100%; 253 | border-radius: .85vh; 254 | z-index: 1; 255 | display: flex; 256 | align-items: center; 257 | justify-content: center; 258 | } 259 | 260 | .percentage__wrapper { 261 | position: absolute; 262 | width: 3vw; 263 | transform: translate(-50%, -50%); 264 | display: flex; 265 | align-items: center; 266 | justify-content: center; 267 | flex-direction: column; 268 | gap: 1.25vh; 269 | text-align: center; 270 | z-index: 999; 271 | } 272 | 273 | .percentage__container { 274 | width: 100%; 275 | height: 50vh; 276 | position: relative; 277 | border-radius: .85vh; 278 | /* overflow: hidden; */ 279 | /* transition: height .25s ease; */ 280 | text-transform: var(--main-font-transform); 281 | } 282 | 283 | .percentage__inner--container { 284 | position: absolute; 285 | border-radius: .85vh; 286 | transition: width .85s ease, height .85s ease, background-color 1.2s ease; 287 | } 288 | 289 | .percentage__wrapper.left { 290 | top: 50%; 291 | left: 3.5vw; 292 | } 293 | 294 | .percentage__wrapper.right { 295 | top: 50%; 296 | right: .5vw; 297 | } 298 | 299 | .percentage__wrapper.left .percentage__inner--container, 300 | .percentage__wrapper.right .percentage__inner--container { 301 | width: 100%; 302 | height: 0; 303 | bottom: 0; 304 | } 305 | 306 | .percentage__wrapper.top { 307 | top: 7%; 308 | left: 50%; 309 | right: 0; 310 | } 311 | 312 | .percentage__wrapper.top .percentage__container, 313 | .percentage__wrapper.bottom .percentage__container { 314 | width: 50vh; 315 | height: 3vw; 316 | } 317 | 318 | .percentage__wrapper.top .percentage__inner--container, 319 | .percentage__wrapper.bottom .percentage__inner--container { 320 | width: 0; 321 | height: 100%; 322 | } 323 | 324 | .percentage__wrapper.bottom { 325 | bottom: 2%; 326 | left: 50%; 327 | right: 0; 328 | } 329 | 330 | 331 | .bubble__chat--wrapper { 332 | position: absolute; 333 | top: 20%; 334 | left: 65%; 335 | transform: translate(-50%, -50%); 336 | } 337 | 338 | .bubble__chat--container { 339 | position: relative; 340 | width: 20vw; 341 | display: flex; 342 | justify-content: center; 343 | align-items: center; 344 | border-radius: .85vh; 345 | padding: 2vh 1.5vw; 346 | } 347 | 348 | .bubble__chat--content { 349 | position: relative; 350 | z-index: 2; 351 | text-align: center; 352 | font-size: 1.5vh; 353 | font-weight: 500; 354 | text-transform: var(--main-font-transform); 355 | line-height: 2.75vh; 356 | } 357 | 358 | .bubble__chat--container::before { 359 | content: ""; 360 | position: absolute; 361 | bottom: -1.4vh; 362 | left: 1vw; 363 | border-left: 1vh solid transparent; 364 | border-right: 1.85vh solid transparent; 365 | } 366 | 367 | [data-tooltip]:before, [data-tooltip]:after { 368 | transition: .25s ease !important; 369 | } 370 | 371 | [data-tooltip]:before { 372 | border-radius: .4vh !important; 373 | } 374 | 375 | [data-tooltip].tooltip-always:after, 376 | [data-tooltip].tooltip-always:before { 377 | opacity: 1 !important; 378 | } 379 | 380 | [data-tooltip].tooltip-none:after, 381 | [data-tooltip].tooltip-none:before { 382 | display: none !important; 383 | } 384 | 385 | .option__item.disabled { 386 | opacity: 0.5; 387 | cursor: not-allowed; 388 | pointer-events: none; 389 | } 390 | 391 | .option__item.disabled .option__key, 392 | .option__item.disabled .option__label { 393 | color: #666; 394 | } -------------------------------------------------------------------------------- /ui/themes/dark-theme.css: -------------------------------------------------------------------------------- 1 | .choice__container, 2 | .slider__wrapper, 3 | .bubble__chat--container { 4 | background: #131517; 5 | } 6 | 7 | .choice__title { 8 | color: #C7C8CC; 9 | } 10 | 11 | .option__item .option__key, 12 | .option__item .option__content { 13 | background: #2f3035; 14 | color: #BCBDC1; 15 | } 16 | 17 | .option__item:hover .option__key, 18 | .option__item.active .option__key, 19 | .option__item:hover .option__content, 20 | .option__item.active .option__content { 21 | background: #464b52; 22 | color: #BDBEC2; 23 | } 24 | 25 | .option__item.disabled .option__key, 26 | .option__item.disabled .option__content { 27 | background: #2f3035; 28 | color: #BCBDC1; 29 | cursor: default; 30 | } 31 | 32 | .slider::-webkit-slider-runnable-track { 33 | background: #fff; 34 | } 35 | 36 | .slider::-webkit-slider-thumb { 37 | background-color: #fff; 38 | border: .35vh solid #585f68; 39 | box-shadow: -407px 0 0 400px #585f68; 40 | } 41 | 42 | .slider::-moz-range-thumb { 43 | background-color: #fff; 44 | border: .1vh solid #585f68; 45 | box-shadow: -407px 0 0 400px #585f68; 46 | } 47 | 48 | .slider::-moz-range-track { 49 | background: #fff; 50 | } 51 | 52 | .slider__submit { 53 | background: #2f3035; 54 | color: #BCBDC1; 55 | } 56 | 57 | .slider__submit:hover { 58 | background: #464b52; 59 | color: #BDBEC2; 60 | } 61 | 62 | .slider__submit.locked { 63 | background: #2f3035; 64 | } 65 | 66 | .slider__content { 67 | color: #d1d3d8; 68 | } 69 | 70 | .overlay { 71 | background: linear-gradient(360deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.185) 100%); 72 | } 73 | 74 | .percentage__container { 75 | background: #25272C; 76 | } 77 | 78 | .bubble__chat--content { 79 | color: #d1d3d8; 80 | } 81 | 82 | .bubble__chat--container::before { 83 | border-top: 1.45vh solid #585f68; 84 | } 85 | 86 | [data-tooltip]:before { 87 | background: #131517 !important; 88 | color: #d1d3d8 !important; 89 | } 90 | 91 | [data-tooltip]:after { 92 | border-top-color: #d1d3d8 !important; 93 | } 94 | 95 | [data-tooltip][class*="tooltip-bottom"]:after { 96 | border-bottom-color: #d1d3d8 !important; 97 | } -------------------------------------------------------------------------------- /ui/themes/envi-theme.css: -------------------------------------------------------------------------------- 1 | .choice__container, 2 | .slider__wrapper, 3 | .bubble__chat--container { 4 | background: #1d2e3e; 5 | } 6 | 7 | .choice__title { 8 | color: #e0eaf7; 9 | } 10 | 11 | .option__item .option__key, 12 | .option__item .option__content { 13 | background: #1f334c; 14 | color: #cedae7; 15 | } 16 | 17 | .option__item:hover .option__key, 18 | .option__item.active .option__key, 19 | .option__item:hover .option__content, 20 | .option__item.active .option__content { 21 | background: #1e3c66; 22 | color: #cedae7; 23 | } 24 | 25 | .option__item.disabled .option__key, 26 | .option__item.disabled .option__content { 27 | background: #1f334c; 28 | color: #cedae7; 29 | cursor: default; 30 | } 31 | 32 | .slider::-webkit-slider-runnable-track { 33 | background: #fff; 34 | } 35 | 36 | .slider::-webkit-slider-thumb { 37 | background-color: #fff; 38 | border: .35vh solid #2f85be; 39 | box-shadow: -407px 0 0 400px #2f85be; 40 | } 41 | 42 | .slider::-moz-range-thumb { 43 | background-color: #fff; 44 | border: .35vh solid #2f85be; 45 | box-shadow: -407px 0 0 400px #2f85be; 46 | } 47 | 48 | .slider::-moz-range-track { 49 | background: #fff; 50 | } 51 | 52 | .slider__submit { 53 | background: #284563; 54 | color: #ABD7FF; 55 | } 56 | 57 | .slider__submit:hover { 58 | background: #2f85be; 59 | color: #FEFFFF; 60 | } 61 | 62 | .slider__submit.locked { 63 | background: #284563; 64 | } 65 | 66 | .slider__content { 67 | color: #e0eaf7; 68 | } 69 | 70 | .overlay { 71 | background: linear-gradient(360deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.185) 100%); 72 | } 73 | 74 | .percentage__container { 75 | background: #1a252f; 76 | } 77 | 78 | .bubble__chat--content { 79 | color: #e0eaf7; 80 | } 81 | 82 | .bubble__chat--container::before { 83 | border-top: 1.45vh solid #1a252f; 84 | } 85 | 86 | [data-tooltip]:before { 87 | background: #1a252f !important; 88 | color: #e0eaf7 !important; 89 | } 90 | 91 | [data-tooltip]:after { 92 | border-top-color: #e0eaf7 !important; 93 | } 94 | 95 | [data-tooltip][class*="tooltip-bottom"]:after { 96 | border-bottom-color: #e0eaf7 !important; 97 | } -------------------------------------------------------------------------------- /ui/themes/ox-theme.css: -------------------------------------------------------------------------------- 1 | .choice__container, 2 | .slider__wrapper, 3 | .bubble__chat--container { 4 | background: #25272C; 5 | } 6 | 7 | .choice__title { 8 | color: #c2c4c7; 9 | } 10 | 11 | .option__item .option__key, 12 | .option__item .option__content { 13 | background: #3d3f46; 14 | color: #cdcfd3; 15 | } 16 | 17 | .option__item:hover .option__key, 18 | .option__item.active .option__key, 19 | .option__item:hover .option__content, 20 | .option__item.active .option__content { 21 | background: #5a6069; 22 | color: #cdcfd3; 23 | } 24 | 25 | .option__item.disabled .option__key, 26 | .option__item.disabled .option__content { 27 | background: #3d3f46; 28 | color: #cdcfd3; 29 | cursor: default; 30 | } 31 | 32 | .slider::-webkit-slider-runnable-track { 33 | background: #fff; 34 | } 35 | 36 | .slider::-webkit-slider-thumb { 37 | background-color: #fff; 38 | border: .35vh solid #1971C2; 39 | box-shadow: -407px 0 0 400px #1971C2; 40 | } 41 | 42 | .slider::-moz-range-thumb { 43 | background-color: #fff; 44 | border: .35vh solid #1971C2; 45 | box-shadow: -407px 0 0 400px #1971C2; 46 | } 47 | 48 | .slider::-moz-range-track { 49 | background: #fff; 50 | } 51 | 52 | .slider__submit { 53 | background: #284563; 54 | color: #ABD7FF; 55 | } 56 | 57 | .slider__submit:hover { 58 | background: #1971C2; 59 | color: #FEFFFF; 60 | } 61 | 62 | .slider__submit.locked { 63 | background: #284563; 64 | } 65 | 66 | .slider__content { 67 | color: #c2c4c7; 68 | } 69 | 70 | .overlay { 71 | background: linear-gradient(360deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.185) 100%); 72 | } 73 | 74 | .percentage__container { 75 | background: #25272C; 76 | } 77 | 78 | .bubble__chat--content { 79 | color: #c2c4c7; 80 | } 81 | 82 | .bubble__chat--container::before { 83 | border-top: 1.45vh solid #25272C; 84 | } 85 | 86 | [data-tooltip]:before { 87 | background: #25272C !important; 88 | color: #c2c4c7 !important; 89 | } 90 | 91 | [data-tooltip]:after { 92 | border-top-color: #c2c4c7 !important; 93 | } 94 | 95 | [data-tooltip][class*="tooltip-bottom"]:after { 96 | border-bottom-color: #c2c4c7 !important; 97 | } -------------------------------------------------------------------------------- /ui/themes/purple-theme.css: -------------------------------------------------------------------------------- 1 | .choice__container, 2 | .slider__wrapper, 3 | .bubble__chat--container { 4 | background: #1b1b1b; 5 | } 6 | 7 | .choice__title { 8 | color: #fff; 9 | } 10 | 11 | .option__item .option__key, 12 | .option__item .option__content { 13 | border: .1vh solid #fff; 14 | background: linear-gradient(186deg, rgba(158, 158, 158, 0.4) -20.85%, rgba(255, 253, 253, 0.45) -20.83%, rgba(255, 255, 255, 0.24) 116.4%); 15 | color: #fff; 16 | } 17 | 18 | .option__item:hover .option__key, 19 | .option__item.active .option__key, 20 | .option__item:hover .option__content, 21 | .option__item.active .option__content { 22 | background: linear-gradient(360deg, rgba(43, 29, 110, 0.5) 0%, rgba(97, 65, 255, 0.185) 100%); 23 | color: #fff; 24 | box-shadow: 0 0 3px #fff, 0 0 6px #fff; 25 | } 26 | 27 | .option__item.disabled .option__key, 28 | .option__item.disabled .option__content { 29 | border: .1vh solid #fff; 30 | background: linear-gradient(186deg, rgba(158, 158, 158, 0.4) -20.85%, rgba(255, 253, 253, 0.45) -20.83%, rgba(255, 255, 255, 0.24) 116.4%); 31 | color: #fff; 32 | cursor: default; 33 | } 34 | 35 | .slider::-webkit-slider-runnable-track { 36 | background: #fff; 37 | } 38 | 39 | .slider::-webkit-slider-thumb { 40 | background-color: #fff; 41 | border: .35vh solid rgb(68, 46, 175); 42 | box-shadow: -407px 0 0 400px rgb(68, 46, 175); 43 | } 44 | 45 | .slider::-moz-range-thumb { 46 | background-color: #fff; 47 | border: .1vh solid rgb(42, 29, 110); 48 | box-shadow: -407px 0 0 400px rgb(42, 29, 110); 49 | } 50 | 51 | .slider::-moz-range-track { 52 | background: #fff; 53 | } 54 | 55 | .slider__submit { 56 | border: .1vh solid #fff; 57 | background: linear-gradient(186deg, rgba(158, 158, 158, 0.4) -20.85%, rgba(255, 253, 253, 0.45) -20.83%, rgba(255, 255, 255, 0.24) 116.4%); 58 | color: #fff; 59 | } 60 | 61 | .slider__submit:hover { 62 | background: linear-gradient(360deg, rgba(43, 29, 110, 0.5) 0%, rgba(97, 65, 255, 0.185) 100%); 63 | box-shadow: 0 0 3px #fff, 0 0 6px #fff; 64 | } 65 | 66 | .slider__submit.locked { 67 | background: linear-gradient(186deg, rgba(158, 158, 158, 0.4) -20.85%, rgba(255, 253, 253, 0.45) -20.83%, rgba(255, 255, 255, 0.24) 116.4%); 68 | } 69 | 70 | .slider__content { 71 | color: #fff; 72 | } 73 | 74 | .overlay { 75 | background: linear-gradient(360deg, rgba(0, 0, 0, 0.5) 0%, rgba(97, 65, 255, 0.185) 100%); 76 | border: .15vh solid #6f00ff; 77 | } 78 | 79 | .percentage__container { 80 | background: #25272C; 81 | } 82 | 83 | .bubble__chat--content { 84 | color: #fff; 85 | } 86 | 87 | .bubble__chat--container::before { 88 | border-top: 1.45vh solid #6f00ff; 89 | } 90 | 91 | [data-tooltip]:before { 92 | border: .1vh solid rgb(255, 255, 255) !important; 93 | background: linear-gradient(186deg, rgba(158, 158, 158, 0.4) -20.85%, rgba(255, 253, 253, 0.45) -20.83%, rgba(255, 255, 255, 0.24) 116.4%) !important; 94 | border-radius: .4vh !important; 95 | color: #fff !important; 96 | } 97 | 98 | [data-tooltip]:after { 99 | border-top-color: #ffffff !important; 100 | } 101 | 102 | [data-tooltip][class*="tooltip-bottom"]:after { 103 | border-bottom-color: #ffffff !important; 104 | } -------------------------------------------------------------------------------- /ui/themes/stix-theme.css: -------------------------------------------------------------------------------- 1 | .choice__container, 2 | .slider__wrapper, 3 | .bubble__chat--container { 4 | background: rgba(19, 21, 23, 0.8); 5 | border: 1px solid #9400D3; /* Neon purple border */ 6 | box-shadow: 0 0 10px #9400D3; 7 | } 8 | 9 | .choice__title { 10 | color: #9400D3; /* Neon purple */ 11 | text-shadow: 0 0 5px #9400D3; 12 | } 13 | 14 | .option__item .option__key, 15 | .option__item .option__content { 16 | background: rgba(47, 48, 53, 0.8); 17 | color: #BCBDC1; 18 | } 19 | 20 | .option__item:hover .option__key, 21 | .option__item.active .option__key, 22 | .option__item:hover .option__content, 23 | .option__item.active .option__content { 24 | background: rgba(70, 75, 82, 0.8); 25 | color: #BDBEC2; 26 | } 27 | 28 | .option__item.disabled .option__key, 29 | .option__item.disabled .option__content { 30 | background: rgba(47, 48, 53, 0.8); 31 | color: #BCBDC1; 32 | cursor: default; 33 | } 34 | 35 | .slider::-webkit-slider-runnable-track { 36 | background: rgba(255, 255, 255, 0.8); 37 | } 38 | 39 | .slider::-webkit-slider-thumb { 40 | background-color: #9400D3; /* Neon purple */ 41 | border: .35vh solid #585f68; 42 | box-shadow: -407px 0 0 400px #585f68; 43 | } 44 | 45 | .slider::-moz-range-thumb { 46 | background-color: #9400D3; /* Neon purple */ 47 | border: .1vh solid #585f68; 48 | box-shadow: -407px 0 0 400px #585f68; 49 | } 50 | 51 | .slider::-moz-range-track { 52 | background: rgba(255, 255, 255, 0.8); 53 | } 54 | 55 | .slider__submit { 56 | background: rgba(47, 48, 53, 0.8); 57 | color: #BCBDC1; 58 | border: 1px solid #9400D3; /* Neon purple border */ 59 | box-shadow: 0 0 10px #9400D3; 60 | } 61 | 62 | .slider__submit:hover { 63 | background: rgba(70, 75, 82, 0.8); 64 | color: #BDBEC2; 65 | } 66 | 67 | .slider__submit.locked { 68 | background: rgba(47, 48, 53, 0.8); 69 | } 70 | 71 | .slider__content { 72 | color: #9400D3; /* Neon purple */ 73 | text-shadow: 0 0 5px #9400D3; 74 | } 75 | 76 | .overlay { 77 | background: linear-gradient(360deg, rgba(19, 21, 23, 0.5) 0%, rgba(19, 21, 23, 0.185) 100%); 78 | } 79 | 80 | .percentage__container { 81 | background: rgba(37, 39, 44, 0.8); 82 | border: 1px solid #9400D3; /* Neon purple border */ 83 | box-shadow: 0 0 10px #9400D3; 84 | } 85 | 86 | .bubble__chat--content { 87 | color: #9400D3; /* Neon purple */ 88 | text-shadow: 0 0 5px #9400D3; 89 | } 90 | 91 | .bubble__chat--container::before { 92 | border-top: 1.45vh solid #9400D3; /* Neon purple */ 93 | box-shadow: 0 0 10px #9400D3; 94 | } 95 | 96 | [data-tooltip]:before { 97 | background: rgba(19, 21, 23, 0.8) !important; 98 | color: #9400D3 !important; /* Neon purple */ 99 | text-shadow: 0 0 5px #9400D3 !important; 100 | } 101 | 102 | [data-tooltip]:after { 103 | border-top-color: #9400D3 !important; /* Neon purple */ 104 | } 105 | 106 | [data-tooltip][class*="tooltip-bottom"]:after { 107 | border-bottom-color: #9400D3 !important; /* Neon purple */ 108 | } --------------------------------------------------------------------------------