commit 596d70e72f16c9c11711ff1ab483a47d6f53dea0 Author: Michatec Date: Sun Feb 15 18:47:56 2026 +0100 First diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..591ddda --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +indent_size = 2 +indent_style = space + +[*.lua] +indent_size = 4 +indent_style = tab \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..57c7607 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# xhud + +xhud is a FiveM HUD for ox_core or ESX Legacy. + +## Dependencies + +- ox_lib + +This also requires a framework of your choice between ox_core and es_extended. +Note that if you are using ESX, you will also need esx_status and esx_basicneeds. + +## Download & Installation + +- Go to the [releases page](https://github.com/Michatec/xhud/releases "Releases page") and download the latest release +- Place it inside the `resources` directory +- Add the resource to your `server.cfg` after dependencies to make sure it's started correctly at server startup: + +``` command +ensure xhud +``` + +## Configuration + +You can add these settings directly to your 'server.cfg', or add them to a separate file (i.e. hud.cfg) and call it with exec. **Convars must be set before starting xhud.** + +The values below are defaults and should not be explicitly set unless changing the value. + +``` yaml +### Shared + +# Seabelt system +setr hud:seatbelt false + +### Client + +# Stress Indicator +setr hud:stress false + +# Stamina Indicator +setr hud:stamina false + +# Fuel Indicator +setr hud:fuel false + +# Vehicles speed: "imperial" or "metric" +setr hud:unitsystem "imperial" + +# Radar mode: by default, the radar is only enabled while sitting on a vehicle. +# Set this to true to have it always enabled. This will also enable the map cycler. +setr hud:persistentRadar false + +# Radar shape +setr hud:circleMap true + +# Keys for map cycler and seatbelt +setr hud:cyclemapKey "Z" +setr hud:seatbeltKey "B" + +# Voice Indicator +setr hud:voice false + +# Voice system: "pma-voice" or "saltychat" +setr hud:voiceService "pma-voice" + +# Server logo +setr hud:logo true + +# Version check against GitHub repo (Recommended) +setr hud:versioncheck true +``` diff --git a/client/frameworks.lua b/client/frameworks.lua new file mode 100644 index 0000000..d4a8464 --- /dev/null +++ b/client/frameworks.lua @@ -0,0 +1,60 @@ +if GetResourceState('ox_core'):find('start') then + local file = ('imports/%s.lua'):format(IsDuplicityVersion() and 'server' or 'client') + local import = LoadResourceFile('ox_core', file) + local chunk = assert(load(import, ('@@ox_core/%s'):format(file))) + chunk() + + if player then + PlayerLoaded = true + end + + RegisterNetEvent('ox:playerLoaded', function() + PlayerLoaded = true + InitializeHUD() + end) + + RegisterNetEvent('ox:playerLogout', function() + PlayerLoaded = false + HUD = false + SendMessage('toggleHud', HUD) + end) + + AddEventHandler('ox:statusTick', function(values) + SendMessage('status', values) + end) +end + +if GetResourceState('es_extended'):find('start') then + local ESX = exports['es_extended']:getSharedObject() + if ESX.PlayerLoaded then + PlayerLoaded = true + end + + RegisterNetEvent('esx:playerLoaded') + AddEventHandler('esx:playerLoaded', function() + PlayerLoaded = true + InitializeHUD() + end) + + RegisterNetEvent('esx:onPlayerLogout') + AddEventHandler('esx:onPlayerLogout', function() + PlayerLoaded = false + HUD = false + SendMessage('toggleHud', HUD) + end) + + AddEventHandler('esx_status:onTick', function(data) + local hunger, thirst, stress + for i = 1, #data do + if data[i].name == 'thirst' then thirst = math.floor(data[i].percent) end + if data[i].name == 'hunger' then hunger = math.floor(data[i].percent) end + if data[i].name == 'stress' then stress = math.floor(data[i].percent) end + end + + SendMessage('status', { + hunger = hunger, + thirst = thirst, + stress = GetConvar('hud:stress', 'false') and stress, + }) + end) +end diff --git a/client/hud.lua b/client/hud.lua new file mode 100644 index 0000000..108efbc --- /dev/null +++ b/client/hud.lua @@ -0,0 +1,58 @@ +local curPaused +local lastHealth, lastArmour +local onSurface, isResting + +CreateThread(function() + while true do + if HUD then + local paused = IsPauseMenuActive() + if paused ~= curPaused then + SendMessage('toggleHud', not paused) + curPaused = paused + end + + local curHealth = GetEntityHealth(cache.ped) + if curHealth ~= lastHealth then + SendMessage('setHealth', { + current = curHealth, + max = GetEntityMaxHealth(cache.ped) + }) + lastHealth = curHealth + end + + local curArmour = GetPedArmour(cache.ped) + if curArmour ~= lastArmour then + SendMessage('setArmour', curArmour) + lastArmour = curArmour + end + + if GetConvar('hud:stamina', 'false') == 'true' then + local curStamina = GetPlayerStamina(cache.playerId) + local maxStamina = GetPlayerMaxStamina(cache.playerId) + if curStamina < maxStamina then + SendMessage('setStamina', { + current = curStamina, + max = maxStamina + }) + isResting = false + elseif not isResting then + SendMessage('setStamina', false) + isResting = true + end + end + + local curUnderwaterTime = GetPlayerUnderwaterTimeRemaining(cache.playerId) + if curUnderwaterTime < maxUnderwaterTime then + SendMessage('setOxygen', { + current = curUnderwaterTime, + max = maxUnderwaterTime + }) + onSurface = false + elseif not onSurface then + SendMessage('setOxygen', false) + onSurface = true + end + end + Wait(200) + end +end) diff --git a/client/minimap.lua b/client/minimap.lua new file mode 100644 index 0000000..c24f6a2 --- /dev/null +++ b/client/minimap.lua @@ -0,0 +1,68 @@ +local mapLimit +local mapState = 1 +local persistentRadar = GetConvar('hud:persistentRadar', 'false') + +if GetConvar('hud:circleMap', 'true') == 'true' then + mapLimit = 1 +else + mapLimit = 3 +end + +if persistentRadar == 'true' then + local function setRadarState() + if mapState == 0 then + DisplayRadar(false) + elseif mapState == 1 then + DisplayRadar(true) + SetBigmapActive(false, false) + elseif mapState == 2 then + DisplayRadar(true) + SetBigmapActive(true, false) + elseif mapState == 3 then + DisplayRadar(true) + SetBigmapActive(true, true) + end + end + + CreateThread(function() + repeat Wait(100) until HUD + setRadarState() + end) + + lib.addKeybind({ + name = 'cyclemap', + description = 'Cycle Map', + defaultKey = GetConvar('hud:cyclemapKey', 'Z'), + onPressed = function() + if mapState == mapLimit then + mapState = 0 + else + mapState += 1 + end + + setRadarState() + end, + }) +end + +CreateThread(function() + local minimap = RequestScaleformMovie('minimap') + repeat Wait(100) until HasScaleformMovieLoaded(minimap) + while true do + if HUD then + BeginScaleformMovieMethod(minimap, 'SETUP_HEALTH_ARMOUR') + ScaleformMovieMethodAddParamInt(3) + EndScaleformMovieMethod() + + if persistentRadar == 'false' then + local isRadarHidden = IsRadarHidden() + local isPedUsingAnyVehicle = cache.vehicle and true or false + if isPedUsingAnyVehicle == isRadarHidden then + DisplayRadar(isPedUsingAnyVehicle) + SetRadarZoom(1150) + end + end + end + Wait(500) + end +end) diff --git a/client/seatbelt.lua b/client/seatbelt.lua new file mode 100644 index 0000000..f06cf38 --- /dev/null +++ b/client/seatbelt.lua @@ -0,0 +1,62 @@ +if GetConvar('hud:seatbelt', 'false') == 'true' then + local isBuckled = false + SetFlyThroughWindscreenParams(15.0, 20.0, 17.0, 2000.0) + + local function Buckled() + CreateThread(function() + while isBuckled do + lib.disableControls() + Wait(0) + end + end) + end + + local function Seatbelt(status) + if status then + SendMessage('playSound', 'buckle') + SendMessage('setSeatbelt', { toggled = true, buckled = true }) + SetFlyThroughWindscreenParams(1000.0, 1000.0, 0.0, 0.0) + lib.disableControls:Add(75) + Buckled() + else + SendMessage('playSound', 'unbuckle') + SendMessage('setSeatbelt', { toggled = true, buckled = false }) + SetFlyThroughWindscreenParams(15.0, 20.0, 17.0, 2000.0) + lib.disableControls:Remove(75) + end + isBuckled = status + end + + local inVehicle + CreateThread(function() + while true do + if HUD then + local isPedUsingAnyVehicle = cache.vehicle and true or false + if isPedUsingAnyVehicle ~= inVehicle then + SendMessage('setSeatbelt', { toggled = isPedUsingAnyVehicle }) + if not isPedUsingAnyVehicle and isBuckled then isBuckled = false end + inVehicle = isPedUsingAnyVehicle + end + end + Wait(1000) + end + end) + + lib.addKeybind({ + name = 'seatbelt', + description = 'Toggle Seatbelt', + defaultKey = GetConvar('hud:seatbeltKey', 'B'), + onPressed = function() + if cache.vehicle then + local curVehicleClass = GetVehicleClass(cache.vehicle) + + if curVehicleClass ~= 8 + and curVehicleClass ~= 13 + and curVehicleClass ~= 14 + then + Seatbelt(not isBuckled) + end + end + end, + }) +end diff --git a/client/vehicle.lua b/client/vehicle.lua new file mode 100644 index 0000000..1957f6d --- /dev/null +++ b/client/vehicle.lua @@ -0,0 +1,52 @@ + local electricModels = { + [`airtug`] = true, + [`caddy`] = true, + [`caddy2`] = true, + [`caddy3`] = true, + [`cyclone`] = true, + [`cyclone2`] = true, + [`dilettante`] = true, + [`dilettante2`] = true, + [`imorgon`] = true, + [`iwagen`] = true, + [`khamelion`] = true, + [`neon`] = true, + [`omnisegt`] = true, + [`powersurge`] = true, + [`raiden`] = true, + [`rcbandito`] = true, + [`surge`] = true, + [`tezeract`] = true, + [`virtue`] = true, + [`voltic`] = true, + [`voltic2`] = true, +} + +local offVehicle, model + +CreateThread(function() + while true do + if HUD then + if cache.vehicle then + model = GetEntityModel(cache.vehicle) + + SendMessage('setVehicle', { + speed = { + current = GetEntitySpeed(cache.vehicle), + max = GetVehicleModelMaxSpeed(model) + }, + unitsMultiplier = GetConvar('hud:unitsystem', 'imperial') == 'metric' and 3.6 or 2.236936, + fuel = GetConvar('hud:fuel', 'false') and not IsThisModelABicycle(model) and + GetVehicleFuelLevel(cache.vehicle), + electric = electricModels[model] + }) + + offVehicle = false + elseif not offVehicle then + SendMessage('setVehicle', false) + offVehicle = true + end + end + Wait(200) + end +end) diff --git a/client/voice.lua b/client/voice.lua new file mode 100644 index 0000000..a79ab0d --- /dev/null +++ b/client/voice.lua @@ -0,0 +1,53 @@ +if GetConvar('hud:voice', 'false') == 'true' then + local service = GetConvar('hud:voiceService', 'pma-voice') + + local voiceCon, voiceDisc + local isTalking, isSilent + + CreateThread(function() + while true do + if HUD then + if service == 'pma-voice' then + voiceCon = MumbleIsConnected() + isTalking = NetworkIsPlayerTalking(cache.playerId) + end + + if service == 'pma-voice' and voiceCon + or service == 'saltychat' and voiceCon > 0 + then + if isTalking then + SendMessage('setVoice', isTalking) + isSilent = false + elseif not isSilent then + SendMessage('setVoice', isTalking) + isSilent = true + end + voiceDisc = false + elseif not voiceDisc then + SendMessage('setVoice', 'disconnected') + voiceDisc = true + isSilent = nil + end + end + Wait(200) + end + end) + + if service == 'pma-voice' then + AddEventHandler('pma-voice:setTalkingMode', function(mode) + SendMessage('setVoiceRange', mode) + end) + elseif service == 'saltychat' then + AddEventHandler('SaltyChat_PluginStateChanged', function(_voiceCon) + voiceCon = _voiceCon + end) + + AddEventHandler('SaltyChat_TalkStateChanged', function(_isTalking) + isTalking = _isTalking + end) + + AddEventHandler('SaltyChat_VoiceRangeChanged', function(range, index, count) + SendMessage('setVoiceRange', index) + end) + end +end diff --git a/fxmanifest.lua b/fxmanifest.lua new file mode 100644 index 0000000..0725b5e --- /dev/null +++ b/fxmanifest.lua @@ -0,0 +1,43 @@ +--[[ FX Information ]]-- +fx_version 'cerulean' +use_experimental_fxv2_oal 'yes' +lua54 'yes' +game 'gta5' + +--[[ Resource Information ]]-- +name 'xhud' +version '1.1.1' +description 'A FiveM HUD for ox_core or ESX Legacy.' +license 'MIT License' +author 'Michatec' +repository 'https://github.com/Michatec/xhud' + +--[[ Manifest ]]-- +dependencies { + 'ox_lib' +} + +shared_scripts { + '@ox_lib/init.lua', + 'shared/init.lua' +} + +client_scripts { + 'client/frameworks.lua', + 'client/hud.lua', + 'client/vehicle.lua', + 'client/minimap.lua', + 'client/seatbelt.lua', + 'client/voice.lua' +} + +server_scripts { + 'server/seatbelt.lua' +} + +ui_page 'web/index.html' + +files { + 'web/index.html', + 'web/**/*' +} diff --git a/server/seatbelt.lua b/server/seatbelt.lua new file mode 100644 index 0000000..17a26fe --- /dev/null +++ b/server/seatbelt.lua @@ -0,0 +1,3 @@ +if GetConvar('hud:seatbelt', 'false') == 'true' then + SetConvarReplicated('game_enableFlyThroughWindscreen', 'true') +end diff --git a/shared/init.lua b/shared/init.lua new file mode 100644 index 0000000..af91a84 --- /dev/null +++ b/shared/init.lua @@ -0,0 +1,120 @@ +if not IsDuplicityVersion() then + HUD = false + + local NuiReady = false + RegisterNUICallback('nuiReady', function(_, cb) + NuiReady = true + cb({}) + end) + + ---Easier NUI Messages + ---@param action string + ---@param message any + function SendMessage(action, message) + SendNUIMessage({ + action = action, + message = message + }) + end + + ---Initialize HUD + function InitializeHUD() + DisplayRadar(false) + repeat Wait(100) until PlayerLoaded and NuiReady + + if GetConvar('hud:circleMap', 'true') == 'true' then + RequestStreamedTextureDict('circlemap', false) + repeat Wait(100) until HasStreamedTextureDictLoaded('circlemap') + AddReplaceTexture('platform:/textures/graphics', 'radarmasksm', 'circlemap', 'radarmasksm') + + SetMinimapClipType(1) + SetMinimapComponentPosition('minimap', 'L', 'B', -0.017, -0.02, 0.207, 0.32) + SetMinimapComponentPosition('minimap_mask', 'L', 'B', 0.06, 0.00, 0.132, 0.260) + SetMinimapComponentPosition('minimap_blur', 'L', 'B', 0.005, -0.05, 0.166, 0.257) + else + SetMinimapComponentPosition('minimap', 'L', 'B', 0.0, -0.035, 0.18, 0.21) + SetMinimapComponentPosition('minimap_mask', 'L', 'B', 0.0, -0.05, 0.132, 0.19) + SetMinimapComponentPosition('minimap_blur', 'L', 'B', -0.025, -0.015, 0.3, 0.25) + end + + + SetRadarBigmapEnabled(true, false) + SetRadarBigmapEnabled(false, false) + Wait(500) + + if IsPedSwimming(cache.ped) then + lib.notify({ + id = 'xhud:swimming', + title = 'HUD', + description = 'Looks like you are swimming, please don\'t go underwater while the HUD is loading.', + type = 'inform', + duration = 5000 + }) + local timer = 5000 + while not maxUnderwaterTime do + Wait(1000) + timer -= 1000 + if not IsPedSwimmingUnderWater(cache.ped) then + maxUnderwaterTime = timer == 0 and GetPlayerUnderwaterTimeRemaining(cache.playerId) or nil + else + timer = 5000 + lib.notify({ + id = 'xhud:initializing', + title = 'HUD', + description = 'Please stay on the surface for at least 5 seconds!', + type = 'inform', + duration = 5000 + }) + end + end + else + maxUnderwaterTime = GetPlayerUnderwaterTimeRemaining(cache.playerId) + end + + SendMessage('setPlayerId', cache.serverId) + + if GetConvar('hud:logo', 'true') == 'true' then + SendMessage('setLogo') + end + + HUD = true + SendMessage('toggleHud', HUD) + end + + AddEventHandler('onResourceStart', function(resourceName) + if cache.resource ~= resourceName then return end + InitializeHUD() + end) + + -- Commands + lib.addCommand('togglehud', { + help = 'Toggle the HUD visibility', + }, function() + HUD = not HUD + SendMessage('toggleHud', HUD) + lib.notify({ + id = 'xhud:toggle', + title = 'HUD', + description = HUD and 'HUD enabled' or 'HUD disabled', + type = 'inform', + duration = 2000 + }) + end) + + lib.addCommand('reloadhud', { + help = 'Reload the HUD', + }, function() + InitializeHUD() + lib.notify({ + id = 'xhud:reload', + title = 'HUD', + description = 'HUD reloaded successfully', + type = 'inform', + duration = 2000 + }) + end) +else + if GetConvar('hud:versioncheck', 'true') == 'true' then + lib.versionCheck('michatec/xhud') + end +end diff --git a/stream/circlemap.ytd b/stream/circlemap.ytd new file mode 100644 index 0000000..36f860d Binary files /dev/null and b/stream/circlemap.ytd differ diff --git a/web/assets/images/logo.png b/web/assets/images/logo.png new file mode 100644 index 0000000..9bb8094 Binary files /dev/null and b/web/assets/images/logo.png differ diff --git a/web/assets/sounds/buckle.ogg b/web/assets/sounds/buckle.ogg new file mode 100644 index 0000000..209f7cc Binary files /dev/null and b/web/assets/sounds/buckle.ogg differ diff --git a/web/assets/sounds/unbuckle.ogg b/web/assets/sounds/unbuckle.ogg new file mode 100644 index 0000000..b87cb22 Binary files /dev/null and b/web/assets/sounds/unbuckle.ogg differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..20ffc0e --- /dev/null +++ b/web/index.html @@ -0,0 +1,80 @@ + + + + + + + xHUD + + + + + +
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+
+
+
+ + + + + + + + diff --git a/web/js/circles.js b/web/js/circles.js new file mode 100644 index 0000000..23be38a --- /dev/null +++ b/web/js/circles.js @@ -0,0 +1,66 @@ +export default { + HealthIndicator: new ProgressBar.Circle("#HealthIndicator", { + strokeWidth: 13, + trailWidth: 13, + duration: 600, + }), + ArmourIndicator: new ProgressBar.Circle("#ArmourIndicator", { + color: "rgb(0, 140, 255)", + trailColor: "rgb(35, 35, 35)", + strokeWidth: 13, + trailWidth: 13, + duration: 600, + }), + StaminaIndicator: new ProgressBar.Circle("#StaminaIndicator", { + color: "rgb(255, 255, 204)", + trailColor: "rgb(35, 35, 35)", + strokeWidth: 13, + trailWidth: 13, + duration: 600, + }), + HungerIndicator: new ProgressBar.Circle("#HungerIndicator", { + color: "rgb(255, 164, 59)", + trailColor: "rgb(35, 35, 35)", + strokeWidth: 13, + trailWidth: 13, + duration: 600, + }), + ThirstIndicator: new ProgressBar.Circle("#ThirstIndicator", { + color: "rgb(0, 140, 170)", + trailColor: "rgb(35, 35, 35)", + strokeWidth: 13, + trailWidth: 13, + duration: 600, + }), + StressIndicator: new ProgressBar.Circle("#StressIndicator", { + color: "rgb(255, 74, 104)", + trailColor: "rgb(35, 35, 35)", + strokeWidth: 13, + trailWidth: 13, + duration: 600, + }), + OxygenIndicator: new ProgressBar.Circle("#OxygenIndicator", { + trailColor: "rgb(35, 35, 35)", + strokeWidth: 13, + trailWidth: 13, + duration: 600, + }), + SpeedIndicator: new ProgressBar.Circle("#SpeedIndicator", { + color: "rgb(255, 255, 255)", + trailColor: "rgb(35, 35, 35)", + strokeWidth: 13, + trailWidth: 13, + duration: 600, + }), + FuelIndicator: new ProgressBar.Circle("#FuelIndicator", { + trailColor: "rgb(35, 35, 35)", + strokeWidth: 13, + trailWidth: 13, + duration: 600, + }), + VoiceIndicator: new ProgressBar.Circle("#VoiceIndicator", { + strokeWidth: 13, + trailWidth: 13, + duration: 100, + }), +}; diff --git a/web/js/listener.js b/web/js/listener.js new file mode 100644 index 0000000..cc433bf --- /dev/null +++ b/web/js/listener.js @@ -0,0 +1,251 @@ +"use strict"; +import Circle from "./circles.js"; + +window.onload = (event) => { + fetch(`https://${GetParentResourceName()}/nuiReady`); + + const Container = document.getElementById("Container"); + const Logo = document.getElementById("Logo"); + const ID = document.getElementById("ID"); + + const Speed = document.getElementById("SpeedIndicator"); + const Fuel = document.getElementById("FuelIndicator"); + const Voice = document.getElementById("VoiceIndicator"); + const Armour = document.getElementById("ArmourIndicator"); + const Stamina = document.getElementById("StaminaIndicator"); + const Oxygen = document.getElementById("OxygenIndicator"); + const Health = document.getElementById("HealthIndicator"); + const Hunger = document.getElementById("HungerIndicator"); + const Thirst = document.getElementById("ThirstIndicator"); + const Stress = document.getElementById("StressIndicator"); + + const HealthIcon = document.getElementById("HealthIcon"); + const SpeedIcon = document.getElementById("SpeedIcon"); + const VoiceIcon = document.getElementById("VoiceIcon"); + const OxygenIcon = document.getElementById("OxygenIcon"); + const FuelIcon = document.getElementById("FuelIcon"); + const HungerIcon = document.getElementById("HungerIcon"); + const ThirstIcon = document.getElementById("ThirstIcon"); + const StressIcon = document.getElementById("StressIcon"); + + const Seatbelt = document.getElementById("SeatbeltIcon"); + const Buckle = document.getElementById("buckle"); + const Unbuckle = document.getElementById("unbuckle"); + + Circle.VoiceIndicator.animate(0.66); + + window.addEventListener("message", function (event) { + let action = event.data.action; + let data = event.data.message; + + if (action == "toggleHud") { + Container.style.display = data ? "flex" : "none"; + } + + if (action == "setLogo") { + Logo.style.display = "block"; + } + + if (action == "setPlayerId") { + if (data) { + ID.style.display = "block"; + ID.textContent = "#" + data; + } else { + ID.style.display = "none"; + } + } + + if (action == "setHealth") { + Health.style.display = "block"; + + let health = (data.current - 100) / (data.max - 100); + health < 0 && (health = 0); + + if (health) { + HealthIcon.classList.remove("fa-skull"); + HealthIcon.classList.add("fa-heart"); + } else { + HealthIcon.classList.remove("fa-heart"); + HealthIcon.classList.add("fa-skull"); + } + + Circle.HealthIndicator.trail.setAttribute( + "stroke", + health ? "rgb(35, 35, 35)" : "rgb(255, 0, 0)" + ); + Circle.HealthIndicator.path.setAttribute( + "stroke", + health ? "rgb(0, 255, 100)" : "rgb(255, 0, 0)" + ); + Circle.HealthIndicator.animate(health); + } + + if (action == "setArmour") { + Armour.style.display = "block"; + Circle.ArmourIndicator.animate(data / 100, function () { + Armour.style.display = data == 0 && "none"; + }); + } + + if (action == "setStamina") { + if (data) { + Stamina.style.display = "block"; + + let stamina = data.current / data.max; + stamina < 0 && (stamina = 0); + stamina < 0.1 && StaminaIcon.classList.toggle("flash"); + + Circle.StaminaIndicator.path.setAttribute( + "stroke", + stamina < 0.1 ? "rgb(255, 0, 0)" : "rgb(255, 255, 200)" + ); + Circle.StaminaIndicator.animate(stamina); + } else { + Circle.StaminaIndicator.animate(1, function () { + Stamina.style.display = "none"; + }); + } + } + + if (action == "setOxygen") { + if (data) { + Oxygen.style.display = "block"; + + let oxygen = data.current / data.max; + oxygen < 0 && (oxygen = 0); + oxygen < 0.1 && OxygenIcon.classList.toggle("flash"); + + Circle.OxygenIndicator.path.setAttribute( + "stroke", + oxygen < 0.1 ? "rgb(255, 0, 0)" : "rgb(0, 140, 255)" + ); + Circle.OxygenIndicator.animate(oxygen); + } else { + Circle.OxygenIndicator.animate(1, function () { + Oxygen.style.display = "none"; + }); + } + } + + if (action == "setVehicle") { + if (data) { + Speed.style.display = "block"; + + let speed = data.speed.current * data.unitsMultiplier; + let maxSpeed = data.speed.max * data.unitsMultiplier; + let percSpeed = (speed / maxSpeed) * 0.7; + let fuel = data.fuel && data.fuel / 100; + + percSpeed > 1 && (percSpeed = 1); + percSpeed >= 0.01 && SpeedIcon.classList.remove("fa-tachometer-alt"); + percSpeed >= 0.01 && (SpeedIcon.textContent = Math.floor(speed)); + percSpeed < 0.01 && SpeedIcon.classList.add("fa-tachometer-alt"); + percSpeed < 0.01 && (SpeedIcon.textContent = ""); + + if (data.electric == true) { + FuelIcon.classList.remove("fa-gas-pump"); + FuelIcon.classList.add("fa-bolt"); + fuel = 1; + } else { + FuelIcon.classList.remove("fa-bolt"); + FuelIcon.classList.add("fa-gas-pump"); + } + + Fuel.style.display = fuel !== false ? "block" : "none"; + fuel <= 0.15 && FuelIcon.classList.toggle("flash"); + Circle.FuelIndicator.path.setAttribute( + "stroke", + fuel > 0.15 ? "rgb(255, 255, 255)" : "rgb(255, 0, 0)" + ); + + Circle.SpeedIndicator.animate(percSpeed); + Circle.FuelIndicator.animate(fuel); + } else { + Circle.SpeedIndicator.animate(0, function () { + Speed.style.display = "none"; + }); + Circle.FuelIndicator.animate(0, function () { + Fuel.style.display = "none"; + }); + } + } + + if (action == "setVoice") { + Voice.style.display = "block"; + if (data == "disconnected") { + VoiceIcon.classList.remove("fa-microphone"); + VoiceIcon.classList.add("fa-times"); + Circle.VoiceIndicator.path.setAttribute("stroke", "rgb(255, 0, 0)"); + Circle.VoiceIndicator.trail.setAttribute("stroke", "rgb(255, 0, 0)"); + } else { + VoiceIcon.classList.remove("fa-times"); + VoiceIcon.classList.add("fa-microphone"); + Circle.VoiceIndicator.path.setAttribute( + "stroke", + data ? "rgb(255, 255, 0)" : "rgb(169, 169, 169)" + ); + Circle.VoiceIndicator.trail.setAttribute("stroke", "rgb(35, 35, 35)"); + } + } + + if (action == "setVoiceRange") { + switch (data) { + case 0: + data = 15; + break; + case 1: + data = 33; + break; + case 2: + data = 66; + break; + case 3: + data = 100; + break; + default: + data = 33; + break; + } + + Circle.VoiceIndicator.animate(data / 100); + } + + if (action == "status") { + Hunger.style.display = "block"; + Thirst.style.display = "block"; + Stress.style.display = data.stress > 5 && "block"; + + data.hunger < 15 && HungerIcon.classList.toggle("flash"); + data.thirst < 15 && ThirstIcon.classList.toggle("flash"); + data.stress > 50 && StressIcon.classList.toggle("flash"); + + Circle.HungerIndicator.animate(data.hunger / 100); + Circle.ThirstIndicator.animate(data.thirst / 100); + Circle.StressIndicator.animate(data.stress / 100, function () { + Stress.style.display = data.stress <= 5 && "none"; + }); + } + + if (action == "setSeatbelt") { + Seatbelt.style.display = data.toggled ? "block" : "none"; + Seatbelt.style.color = data.buckled + ? "rgb(0, 255, 100)" + : "rgb(255, 100, 100)"; + } + + if (action == "playSound") { + switch (data) { + case "unbuckle": + Unbuckle.volume = 0.2; + Unbuckle.play(); + break; + case "buckle": + Buckle.volume = 0.2; + Buckle.play(); + break; + default: + break; + } + } + }); +}; diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..ea4a6d6 --- /dev/null +++ b/web/style.css @@ -0,0 +1,97 @@ +body { + margin: 0; +} + +img { + width: 100%; +} + +#Container { + width: 100%; + height: 100%; + overflow: hidden; + margin: 0; + display: none; + flex-direction: column; +} + +#IconsContainer { + margin-top: auto; +} + +#Icons { + display: flex; + gap: 20px; + margin: 20px 50px; +} + +.Icon { + position: relative; + width: 50px; + height: 50px; + border-radius: 50%; + background: rgb(50, 50, 50); + display: none; +} + +.Icon i { + color: white; + position: absolute; + font-size: 16px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +/* Customisations */ + +#SpeedIcon:not(.fa-tachometer-alt) { + font-family: sans-serif; + font-size: 18px; +} + +#ID { + font-family: sans-serif; + font-weight: 700; + font-size: 16px; +} + +.outerIcon { + color: #fff; + font-size: 1.2rem; + height: 50px; + text-shadow: 1px 1px 10px #000; + text-align: center; + line-height: 50px; + display: none; +} + +/* Logo */ + +#Logo { + position: absolute; + top: 20px; + right: 40px; + max-width: 128px; + opacity: 0.8; + display: none; +} + +/* Animations */ + +.flash { + -webkit-animation: flash 1s; + animation: flash 1s; +} + +@keyframes flash { + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +}