├── config.lua
├── README.md
├── fxmanifest.lua
├── ui
├── reset.css
├── index.html
├── style.css
└── app.js
└── client
└── main.lua
/config.lua:
--------------------------------------------------------------------------------
1 | Config = {}
2 |
3 | Config.Metric = true -- false if imperial
4 | Config.ESX = true -- false if qbcore
5 |
6 | --seatbelt toggle export:
7 | --exports['east_hud']:seatbelt(true/false)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # east_hud
2 |
3 | Simple hud for esx/qbcore frameworks
4 |
5 | Framework and units are configurable in `config.lua`
6 |
7 | To toggle seatbelt icon use export:
8 |
9 | `exports['east_hud']:seatbelt(true/false)`
10 |
--------------------------------------------------------------------------------
/fxmanifest.lua:
--------------------------------------------------------------------------------
1 | fx_version 'cerulean'
2 | game 'gta5'
3 | description 'east_hud'
4 |
5 | shared_script 'config.lua'
6 |
7 | client_script 'client/main.lua'
8 |
9 | export 'seatbelt'
10 |
11 | ui_page 'ui/index.html'
12 |
13 | files {
14 | 'ui/index.html',
15 | 'ui/app.js',
16 | 'ui/*.css',
17 | }
--------------------------------------------------------------------------------
/ui/reset.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html,
7 | body,
8 | div,
9 | span,
10 | applet,
11 | object,
12 | iframe,
13 | h1,
14 | h2,
15 | h3,
16 | h4,
17 | h5,
18 | h6,
19 | p,
20 | blockquote,
21 | pre,
22 | a,
23 | abbr,
24 | acronym,
25 | address,
26 | big,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | s,
37 | samp,
38 | small,
39 | strike,
40 | strong,
41 | sub,
42 | sup,
43 | tt,
44 | var,
45 | b,
46 | u,
47 | i,
48 | center,
49 | dl,
50 | dt,
51 | dd,
52 | ol,
53 | ul,
54 | li,
55 | fieldset,
56 | form,
57 | label,
58 | legend,
59 | table,
60 | caption,
61 | tbody,
62 | tfoot,
63 | thead,
64 | tr,
65 | th,
66 | td,
67 | article,
68 | aside,
69 | canvas,
70 | details,
71 | embed,
72 | figure,
73 | figcaption,
74 | footer,
75 | header,
76 | hgroup,
77 | menu,
78 | nav,
79 | output,
80 | ruby,
81 | section,
82 | summary,
83 | time,
84 | mark,
85 | audio,
86 | video {
87 | margin: 0;
88 | padding: 0;
89 | border: 0;
90 | font-size: 100%;
91 | font: inherit;
92 | vertical-align: baseline;
93 | }
94 |
95 | /* HTML5 display-role reset for older browsers */
96 | article,
97 | aside,
98 | details,
99 | figcaption,
100 | figure,
101 | footer,
102 | header,
103 | hgroup,
104 | menu,
105 | nav,
106 | section {
107 | display: block;
108 | }
109 |
110 | body {
111 | line-height: 1;
112 | }
113 |
114 | ol,
115 | ul {
116 | list-style: none;
117 | }
118 |
119 | blockquote,
120 | q {
121 | quotes: none;
122 | }
123 |
124 | blockquote:before,
125 | blockquote:after,
126 | q:before,
127 | q:after {
128 | content: '';
129 | content: none;
130 | }
131 |
132 | table {
133 | border-collapse: collapse;
134 | border-spacing: 0;
135 | }
--------------------------------------------------------------------------------
/ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | east hud
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
36 |
40 |
41 |
42 |
43 |
47 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/ui/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100vh;
3 | display: flex;
4 | justify-content: center;
5 | align-items: flex-end;
6 | font-size: 0.9vmin;
7 | font-family: 'Poppins', sans-serif;
8 | }
9 |
10 | .status {
11 | display: none;
12 | justify-content: center;
13 | align-items: flex-end;
14 | margin-bottom: 1.8em;
15 | transition: 200ms ease-out;
16 | }
17 |
18 | .circle {
19 | display: flex;
20 | align-items: flex-end;
21 | justify-content: center;
22 | width: 4em;
23 | height: 4em;
24 | border-radius: 1em;
25 | overflow: hidden;
26 | margin: 0 1em;
27 | box-shadow: 0 0 1.0em 0.1em rgb(0 0 0 / 50%);
28 | }
29 |
30 | .fill {
31 | width: 100%;
32 | height: 0%;
33 | transition: 200ms ease-out;
34 | }
35 |
36 | i {
37 | position: absolute;
38 | align-self: center;
39 | color: white;
40 | font-size: 2.2em;
41 | opacity: 0.502;
42 | transition: 200ms ease-out;
43 | }
44 |
45 | .health {
46 | background-color: #ef9a9a80;
47 | }
48 |
49 | .health .fill {
50 | background-color: #E53935;
51 | }
52 |
53 | .armour {
54 | background-color: #B0BEC580;
55 | }
56 |
57 | .armour .fill {
58 | background-color: #546E7A;
59 | }
60 |
61 | .food {
62 | background-color: #FFCC8080;
63 | }
64 |
65 | .food .fill {
66 | background-color: #FB8C00;
67 | }
68 |
69 | .water {
70 | background-color: #90CAF980;
71 | }
72 |
73 | .water .fill {
74 | background-color: #1E88E5;
75 | }
76 |
77 | .carhud {
78 | display: none;
79 | justify-content: center;
80 | align-items: flex-end;
81 | text-shadow: 0 0 1.0em rgba(0, 0, 0, 0.5);
82 | }
83 |
84 | .speedometer {
85 | display: flex;
86 | flex-direction: column;
87 | justify-content: center;
88 | align-items: center;
89 | text-align: center;
90 | color: white;
91 | margin: 0 0.5em;
92 | margin-bottom: 1em;
93 | width: 5em;
94 | }
95 |
96 | .speedometer #speed {
97 | font-weight: 700;
98 | font-size: 3em;
99 | margin-bottom: 0.2em;
100 | opacity: 0.502;
101 | transition: 200ms ease-out;
102 | }
103 |
104 | .speedometer #unit {
105 | font-weight: 400;
106 | font-size: 1em;
107 | }
108 |
109 | .seatbelt {
110 | background-color: #e53935;
111 | width: 3em;
112 | height: 3em;
113 | }
114 |
115 | .seatbelt i {
116 | font-size: 1.65em;
117 | }
118 |
119 | .seatbelt .fill {
120 | background-color: #fb8c00;
121 | }
122 |
123 | .fuel {
124 | background-color: #ffcc8080;
125 | width: 3em;
126 | height: 3em;
127 | }
128 |
129 | .fuel i {
130 | font-size: 1.65em;
131 | }
132 |
133 | .fuel .fill {
134 | background-color: #FB8C00;
135 | }
--------------------------------------------------------------------------------
/client/main.lua:
--------------------------------------------------------------------------------
1 | local hud = false
2 | local speedometer = false
3 |
4 | RegisterNUICallback('ready', function(data, cb)
5 | if data.show then
6 | Wait(500)
7 | SendNUIMessage({
8 | action = 'show'
9 | })
10 | hud = true
11 | end
12 | end)
13 |
14 | local last = {
15 | health = -1,
16 | armour = -1,
17 | food = -1,
18 | water = -1,
19 | fuel = -1,
20 | speed = -1,
21 | pause = false
22 | }
23 |
24 | if not Config.ESX then
25 | RegisterNetEvent('hud:client:UpdateNeeds', function(newHunger, newThirst)
26 | food = newHunger
27 | water = newThirst
28 | end)
29 | end
30 |
31 | Citizen.CreateThread(function()
32 | while true do
33 | if hud then
34 | local pause = IsPauseMenuActive()
35 | if pause ~= last.pause then
36 | if pause then
37 | SendNUIMessage({action = 'hide', opacity = 0})
38 | else
39 | SendNUIMessage({action = 'hide', opacity = 1})
40 | end
41 | last.pause = pause
42 | end
43 | local player = PlayerPedId()
44 | local health = GetEntityHealth(player) - 100
45 | local armour = GetPedArmour(player)
46 | if Config.ESX then
47 | TriggerEvent('esx_status:getStatus', 'hunger', function(status) food = status.val / 10000 end)
48 | TriggerEvent('esx_status:getStatus', 'thirst', function(status) water = status.val / 10000 end)
49 | end
50 | if health < 0 then health = 0 end
51 | if health ~= last.health then SendNUIMessage({action = 'health', health = health}) last.health = health end
52 | if armour ~= last.armour then SendNUIMessage({action = 'armour', armour = armour}) last.armour = armour end
53 | if food ~= last.food then SendNUIMessage({action = 'food', food = food}) last.food = food end
54 | if water ~= last.water then SendNUIMessage({action = 'water', water = water}) last.water = water end
55 | end
56 | Citizen.Wait(1000)
57 | end
58 | end)
59 |
60 | Citizen.CreateThread(function()
61 | while true do
62 | local wait = 1000
63 | if hud then
64 | local player = PlayerPedId()
65 | if IsPedInAnyVehicle(player) then
66 | local vehicle = GetVehiclePedIsIn(player)
67 | if GetPedInVehicleSeat(vehicle, -1) == player then
68 | wait = 200
69 | if not speedometer then
70 | SendNUIMessage({action = 'speedometer', speedometer = 'show', metric = Config.Metric})
71 | speedometer = true
72 | else
73 | local fuel = GetVehicleFuelLevel(vehicle)
74 | local speed = GetEntitySpeed(vehicle)
75 | if fuel ~= last.fuel then SendNUIMessage({action = 'fuel', fuel = fuel}) last.fuel = fuel end
76 | if speed ~= last.speed then SendNUIMessage({action = 'speed', speed = speed}) last.speed = speed end
77 | end
78 | elseif speedometer then
79 | SendNUIMessage({action = 'speedometer', speedometer = 'hide', metric = Config.Metric})
80 | speedometer = false
81 | end
82 | elseif speedometer then
83 | SendNUIMessage({action = 'speedometer', speedometer = 'hide', metric = Config.Metric})
84 | speedometer = false
85 | end
86 | elseif speedometer then
87 | SendNUIMessage({action = 'speedometer', speedometer = 'hide', metric = Config.Metric})
88 | speedometer = false
89 | end
90 | Citizen.Wait(wait)
91 | end
92 | end)
93 |
94 | function seatbelt(toggle)
95 | SendNUIMessage({action = 'seatbelt', seatbelt = toggle})
96 | end
97 |
--------------------------------------------------------------------------------
/ui/app.js:
--------------------------------------------------------------------------------
1 | let speedometer = false;
2 | let unit = 'km/h';
3 | let multiply = 3.6;
4 | let oldSpeed = 0;
5 |
6 | fetch('https://east_hud/ready', {
7 | method: 'POST',
8 | headers: {
9 | 'Content-Type': 'application/json; charset=UTF-8',
10 | },
11 | body: JSON.stringify({
12 | show: true
13 | })
14 | }).then(resp => resp.json()).then(resp => console.log(resp));
15 |
16 | window.addEventListener('message', (event) => {
17 | if ((event.data.action) === 'show') {
18 | document.querySelector('.status').style.display = 'flex';
19 | } else if ((event.data.action) === 'hide' && event.data.opacity || event.data.opacity === 0) {
20 | document.querySelector('.status').style.opacity = event.data.opacity;
21 | } else if (event.data.action === 'health' && event.data.health || event.data.health === 0) {
22 | document.querySelector('.health .fill').style.height = event.data.health + '%';
23 | if (event.data.health <= 0) {
24 | document.querySelector('.health i').style.opacity = 0.502;
25 | } else {
26 | document.querySelector('.health i').style.opacity = 1;
27 | }
28 | } else if (event.data.action === 'armour' && event.data.armour || event.data.armour === 0) {
29 | document.querySelector('.armour .fill').style.height = event.data.armour + '%';
30 | if (event.data.armour <= 0) {
31 | document.querySelector('.armour i').style.opacity = 0.502;
32 | } else {
33 | document.querySelector('.armour i').style.opacity = 1;
34 | }
35 | } else if (event.data.action === 'food' && event.data.food || event.data.food === 0) {
36 | document.querySelector('.food .fill').style.height = event.data.food + '%';
37 | if (event.data.food <= 0) {
38 | document.querySelector('.food i').style.opacity = 0.502;
39 | } else {
40 | document.querySelector('.food i').style.opacity = 1;
41 | }
42 | } else if (event.data.action === 'water' && event.data.water || event.data.water === 0) {
43 | document.querySelector('.water .fill').style.height = event.data.water + '%';
44 | if (event.data.water <= 0) {
45 | document.querySelector('.water i').style.opacity = 0.502;
46 | } else {
47 | document.querySelector('.water i').style.opacity = 1;
48 | }
49 | } else if (event.data.action === 'speedometer' && event.data.speedometer) {
50 | if (event.data.speedometer === 'show') {
51 | document.querySelector('.carhud').style.display = 'flex';
52 | speedometer = true;
53 | if (!event.data.metric) {
54 | unit = 'mph';
55 | multiply = 2.236936;
56 | }
57 | document.querySelector('#unit').innerText = unit;
58 | } else if (event.data.speedometer === 'hide') {
59 | document.querySelector('.carhud').style.display = 'none';
60 | speedometer = false;
61 | }
62 | } else if (event.data.action === 'fuel' && speedometer && event.data.fuel || event.data.fuel === 0) {
63 | document.querySelector('.fuel .fill').style.height = event.data.fuel + '%';
64 | if (event.data.fuel <= 0) {
65 | document.querySelector('.fuel i').style.opacity = 0.502;
66 | } else {
67 | document.querySelector('.fuel i').style.opacity = 1;
68 | }
69 | } else if (event.data.action === 'speed' && event.data.speed && speedometer) {
70 | animateValue('#speed', oldSpeed, Math.round(event.data.speed * multiply), 200)
71 | oldSpeed = Math.round(event.data.speed * multiply);
72 | if (oldSpeed <= 0) {
73 | document.querySelector('#speed').style.opacity = 0.502;
74 | } else {
75 | document.querySelector('#speed').style.opacity = 1;
76 | }
77 | } else if (event.data.action === 'seatbelt' && speedometer) {
78 | if (event.data.seatbelt) {
79 | document.querySelector('.seatbelt i').setAttribute('class', 'fa-solid fa-user');
80 | document.querySelector('.seatbelt').style.backgroundColor = '#43A047'
81 | document.querySelector('.seatbelt i').style.opacity = 1
82 | } else if (!event.data.seatbelt) {
83 | document.querySelector('.seatbelt i').setAttribute('class', 'fa-solid fa-user-slash');
84 | document.querySelector('.seatbelt').style.backgroundColor = '#E53935'
85 | document.querySelector('.seatbelt i').style.opacity = 0.502
86 | }
87 | }
88 | });
89 |
90 | function animateValue(id, start, end, duration) {
91 | if (start === end) return;
92 | let range = end - start;
93 | let current = start;
94 | let increment = end > start ? 1 : -1;
95 | let stepTime = Math.abs(Math.floor(duration / range));
96 | let obj = document.querySelector(id);
97 | let timer = setInterval(function () {
98 | current += increment;
99 | obj.innerText = current;
100 | if (current == end) {
101 | clearInterval(timer);
102 | }
103 | }, stepTime);
104 | }
--------------------------------------------------------------------------------