Skip to content

Commit 20f886f

Browse files
authored
Merge pull request #12 from mri-Qbox-Brasil/feat/hud-temas-skins
wip:
2 parents ab60159 + 8d29a04 commit 20f886f

36 files changed

Lines changed: 4443 additions & 85 deletions

client/main.lua

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ local function sendHudConfig()
159159
statusIcons = Config.StatusIcons,
160160
serverLogo = Config.ServerLogo,
161161
positioning = Config.Positioning,
162+
vehicleTheme = {
163+
theme = Config.VehicleHudTheme or 'classic',
164+
variant = Config.VehicleHudVariant or 'ring',
165+
},
166+
playerSkin = {
167+
skin = Config.PlayerHudSkin or 'classic',
168+
palette = Config.SupernaturalPalette or 'pergaminho',
169+
style = Config.SupernaturalStyle or 'orbes',
170+
layout = Config.SupernaturalLayout or 'classico',
171+
frameless = Config.SupernaturalFrameless or false,
172+
},
162173
})
163174
end
164175

@@ -758,6 +769,24 @@ RegisterNUICallback('toggleSpeedUnit', function(data, cb)
758769
cb({})
759770
end)
760771

772+
-- Tema da HUD de veiculo escolhido pelo admin no menu. A UI ja persiste em
773+
-- localStorage; aqui so guardamos no client (para logica futura/server save).
774+
RegisterNUICallback('setVehicleTheme', function(data, cb)
775+
Config.VehicleHudTheme = data.theme or Config.VehicleHudTheme
776+
Config.VehicleHudVariant = data.variant or Config.VehicleHudVariant
777+
cb({})
778+
end)
779+
780+
-- Skin do HUD do player escolhido pelo admin no menu (UI persiste em localStorage).
781+
RegisterNUICallback('setPlayerSkin', function(data, cb)
782+
Config.PlayerHudSkin = data.skin or Config.PlayerHudSkin
783+
Config.SupernaturalPalette = data.palette or Config.SupernaturalPalette
784+
Config.SupernaturalStyle = data.style or Config.SupernaturalStyle
785+
Config.SupernaturalLayout = data.layout or Config.SupernaturalLayout
786+
if data.frameless ~= nil then Config.SupernaturalFrameless = data.frameless end
787+
cb({})
788+
end)
789+
761790
RegisterNetEvent("hud:client:EngineHealth", function(newEngine)
762791
engine = newEngine
763792
end)
@@ -1285,21 +1314,48 @@ CreateThread(function()
12851314
end)
12861315

12871316
CreateThread(function()
1288-
local lastSpeed = -1
1317+
local lastSpeed = -1
1318+
local lastRpm = -1
1319+
local lastGear = -1
1320+
local lastEngine = -1
1321+
local lastHeading = -1
12891322
while true do
12901323
local wait = 250
12911324
if LocalPlayer.state.isLoggedIn then
12921325
local ped = PlayerPedId()
12931326
local veh = GetVehiclePedIsIn(ped, false)
12941327
if veh ~= 0 and not IsThisModelABicycle(GetEntityModel(veh)) and GetPedInVehicleSeat(veh, -1) == ped then
12951328
wait = 0
1296-
local spd = math.ceil(GetEntitySpeed(veh) * speedMultiplier)
1297-
if spd ~= lastSpeed then
1298-
lastSpeed = spd
1299-
SendNUIMessage({ action = 'car', topic = 'speed', speed = spd })
1329+
local spd = math.ceil(GetEntitySpeed(veh) * speedMultiplier)
1330+
-- Telemetria extra consumida pelo tema 'digital' (RPM/marcha/motor/proa).
1331+
-- O tema 'classic' ignora estes campos; custo e baixo (so numeros).
1332+
local rpm = math.floor(GetVehicleCurrentRpm(veh) * 8000)
1333+
local gear = GetVehicleCurrentGear(veh)
1334+
local engine = math.max(0, math.min(100, math.floor(GetVehicleEngineHealth(veh) / 10)))
1335+
local heading = math.floor(GetEntityHeading(veh))
1336+
if spd ~= lastSpeed or rpm ~= lastRpm or gear ~= lastGear
1337+
or engine ~= lastEngine or heading ~= lastHeading then
1338+
lastSpeed = spd
1339+
lastRpm = rpm
1340+
lastGear = gear
1341+
lastEngine = engine
1342+
lastHeading = heading
1343+
SendNUIMessage({
1344+
action = 'car',
1345+
topic = 'speed',
1346+
speed = spd,
1347+
rpm = rpm,
1348+
gear = gear,
1349+
engine = engine,
1350+
heading = heading,
1351+
})
13001352
end
13011353
else
1302-
lastSpeed = -1
1354+
lastSpeed = -1
1355+
lastRpm = -1
1356+
lastGear = -1
1357+
lastEngine = -1
1358+
lastHeading = -1
13031359
end
13041360
end
13051361
Wait(wait)
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<script src="./support.js"></script>
7+
</head>
8+
<body>
9+
<x-dc>
10+
<helmet>
11+
<link rel="preconnect" href="https://fonts.googleapis.com">
12+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13+
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&display=swap" rel="stylesheet">
14+
</helmet>
15+
<div style="position:relative; width:100%; height:100%; overflow:hidden; font-family:'Chakra Petch',sans-serif; background:radial-gradient(125% 95% at 50% 0%, #161e26 0%, #0b1015 55%, #06090d 100%);">
16+
17+
<!-- gameplay placeholder texture -->
18+
<div style="position:absolute; inset:0; opacity:0.045; background-image:repeating-linear-gradient(135deg,#ffffff 0 1px, transparent 1px 16px);"></div>
19+
<div style="position:absolute; top:14px; left:16px; font:11px ui-monospace,monospace; letter-spacing:0.12em; color:rgba(255,255,255,0.26);">// LIVE GAMEPLAY RENDERS BEHIND HUD</div>
20+
21+
<!-- minimap (bottom-left, canonical) -->
22+
<sc-if value="{{ showMinimap }}" hint-placeholder-val="{{ true }}">
23+
<div style="position:absolute; left:22px; bottom:22px; width:128px; height:128px; border-radius:7px; overflow:hidden; background:#0c1219; border:1px solid rgba(70,224,255,0.18); box-shadow:0 10px 28px rgba(0,0,0,0.45);">
24+
<div style="position:absolute; inset:0; opacity:0.55; background-image:repeating-linear-gradient(0deg,#101a22 0 1px, transparent 1px 21px), repeating-linear-gradient(90deg,#101a22 0 1px, transparent 1px 21px);"></div>
25+
<div style="position:absolute; left:-20px; top:30%; width:180px; height:18px; background:rgba(70,224,255,0.07); transform:rotate(-24deg);"></div>
26+
<div style="position:absolute; left:50%; top:50%; transform:translate(-50%,-50%) rotate( calc({{headingDeg}} * 1deg) ); width:0; height:0; border-left:7px solid transparent; border-right:7px solid transparent; border-bottom:15px solid {{accent}}; filter:drop-shadow(0 0 6px rgba(70,224,255,0.6));"></div>
27+
<div style="position:absolute; top:6px; left:50%; transform:translateX(-50%); font:600 9px 'Chakra Petch'; color:rgba(255,255,255,0.55);">N</div>
28+
<div style="position:absolute; bottom:6px; right:8px; font:500 8px ui-monospace,monospace; letter-spacing:0.14em; color:rgba(255,255,255,0.3);">MAP</div>
29+
</div>
30+
</sc-if>
31+
32+
<!-- center cluster -->
33+
<div style="position:absolute; left:50%; bottom:26px; transform:translateX(-50%); display:flex; flex-direction:column; align-items:center; gap:16px;">
34+
35+
<!-- compass strip -->
36+
<div style="position:relative; width:236px; height:34px; overflow:hidden;">
37+
<div style="position:absolute; left:0; top:0; bottom:0; width:100%; transform:translateX( calc({{compassX}} * 1px) );">
38+
<sc-for list="{{ ticks }}" as="tk" hint-placeholder-count="20">
39+
<div style="position:absolute; left:{{tk.left}}px; bottom:0; transform:translateX(-50%);">
40+
<div style="position:absolute; bottom:0; left:50%; transform:translateX(-50%); width:1px; height:{{tk.h}}px; background:{{tk.col}};"></div>
41+
<div style="position:absolute; bottom:16px; left:50%; transform:translateX(-50%); font:600 10px 'Chakra Petch'; color:{{tk.col}}; white-space:nowrap;">{{tk.label}}</div>
42+
</div>
43+
</sc-for>
44+
</div>
45+
<div style="position:absolute; left:118px; top:6px; bottom:0; width:1px; background:{{accent}}; opacity:0.7;"></div>
46+
<div style="position:absolute; left:118px; top:0; transform:translateX(-50%); width:0; height:0; border-left:5px solid transparent; border-right:5px solid transparent; border-top:6px solid {{accent}};"></div>
47+
<div style="position:absolute; inset:0; pointer-events:none; background:linear-gradient(90deg, rgba(8,11,15,0.96), transparent 16%, transparent 84%, rgba(8,11,15,0.96));"></div>
48+
</div>
49+
50+
<!-- gauges row -->
51+
<div style="display:flex; align-items:center; gap:24px;">
52+
53+
<!-- left rail: fuel + engine -->
54+
<div style="display:flex; align-items:flex-end; gap:16px;">
55+
<div style="display:flex; flex-direction:column; align-items:center; gap:7px;">
56+
<div style="font:600 10px 'Chakra Petch'; color:#fff;">{{fuel}}</div>
57+
<div style="position:relative; width:7px; height:78px; border-radius:5px; background:rgba(255,255,255,0.08); overflow:hidden;">
58+
<div style="position:absolute; left:0; right:0; bottom:0; height:calc({{fuel}} * 1%); background:{{fuelCol}}; border-radius:5px;"></div>
59+
</div>
60+
<div style="font:500 9px 'Chakra Petch'; letter-spacing:0.16em; color:rgba(255,255,255,0.45);">FUEL</div>
61+
</div>
62+
<div style="display:flex; flex-direction:column; align-items:center; gap:7px;">
63+
<div style="font:600 10px 'Chakra Petch'; color:#fff;">{{eng}}</div>
64+
<div style="position:relative; width:7px; height:78px; border-radius:5px; background:rgba(255,255,255,0.08); overflow:hidden;">
65+
<div style="position:absolute; left:0; right:0; bottom:0; height:calc({{eng}} * 1%); background:{{engCol}}; border-radius:5px;"></div>
66+
</div>
67+
<div style="font:500 9px 'Chakra Petch'; letter-spacing:0.16em; color:rgba(255,255,255,0.45);">ENG</div>
68+
</div>
69+
</div>
70+
71+
<!-- speedo slot -->
72+
<div style="min-width:300px; display:flex; justify-content:center;">
73+
74+
<!-- RING -->
75+
<sc-if value="{{ isRing }}" hint-placeholder-val="{{ true }}">
76+
<div style="position:relative; width:206px; height:206px;">
77+
<div style="position:absolute; inset:0; border-radius:50%; background:conic-gradient({{accent}} calc({{pSpeed}} * 1%), rgba(255,255,255,0.07) 0); -webkit-mask:radial-gradient(circle, transparent 80px, #000 81px); mask:radial-gradient(circle, transparent 80px, #000 81px);"></div>
78+
<div style="position:absolute; inset:16px; border-radius:50%; border:1px solid rgba(255,255,255,0.05);"></div>
79+
<div style="position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;">
80+
<div style="font:600 60px 'Chakra Petch'; color:#fff; line-height:0.85; text-shadow:0 0 22px rgba(70,224,255,0.32);">{{speedInt}}</div>
81+
<div style="font:500 10px 'Chakra Petch'; letter-spacing:0.32em; color:rgba(255,255,255,0.5); margin-top:7px;">KM/H</div>
82+
</div>
83+
</div>
84+
</sc-if>
85+
86+
<!-- ARC -->
87+
<sc-if value="{{ isArc }}" hint-placeholder-val="{{ true }}">
88+
<div style="display:flex; flex-direction:column; align-items:center;">
89+
<div style="position:relative; width:280px; height:140px; overflow:hidden;">
90+
<div style="position:absolute; top:0; left:0; width:280px; height:280px; border-radius:50%; background:conic-gradient(from -90deg, {{accent}} calc({{pArc}} * 1%), rgba(255,255,255,0.07) 0); -webkit-mask:radial-gradient(circle, transparent 108px, #000 109px); mask:radial-gradient(circle, transparent 108px, #000 109px);"></div>
91+
</div>
92+
<div style="display:flex; flex-direction:column; align-items:center; margin-top:-50px;">
93+
<div style="font:600 56px 'Chakra Petch'; color:#fff; line-height:0.85; text-shadow:0 0 22px rgba(70,224,255,0.32);">{{speedInt}}</div>
94+
<div style="font:500 10px 'Chakra Petch'; letter-spacing:0.32em; color:rgba(255,255,255,0.5); margin-top:5px;">KM/H</div>
95+
</div>
96+
</div>
97+
</sc-if>
98+
99+
<!-- LINEAR -->
100+
<sc-if value="{{ isLinear }}" hint-placeholder-val="{{ true }}">
101+
<div style="display:flex; flex-direction:column; align-items:flex-start; gap:11px; width:280px;">
102+
<div style="display:flex; align-items:baseline; gap:11px;">
103+
<div style="font:600 72px 'Chakra Petch'; color:#fff; line-height:0.78; text-shadow:0 0 22px rgba(70,224,255,0.32);">{{speedInt}}</div>
104+
<div style="font:500 11px 'Chakra Petch'; letter-spacing:0.3em; color:rgba(255,255,255,0.5);">KM/H</div>
105+
</div>
106+
<div style="position:relative; width:100%; height:6px; border-radius:4px; background:rgba(255,255,255,0.08); overflow:hidden;">
107+
<div style="position:absolute; left:0; top:0; bottom:0; border-radius:4px; width:calc({{speedPct}} * 1%); background:{{accent}}; box-shadow:0 0 12px {{accent}};"></div>
108+
<div style="position:absolute; inset:0; background-image:repeating-linear-gradient(90deg, rgba(8,11,15,0.85) 0 1px, transparent 1px 26px);"></div>
109+
</div>
110+
</div>
111+
</sc-if>
112+
113+
</div>
114+
115+
<!-- right rail: turbo + belt -->
116+
<div style="display:flex; flex-direction:column; align-items:center; gap:12px;">
117+
<div style="display:flex; flex-direction:column; align-items:center; gap:7px;">
118+
<div style="font:600 10px 'Chakra Petch'; color:#fff;">{{boost}}</div>
119+
<div style="position:relative; width:7px; height:60px; border-radius:5px; background:rgba(255,255,255,0.08); overflow:hidden;">
120+
<div style="position:absolute; left:0; right:0; bottom:0; height:calc({{boost}} * 1%); background:{{accent}}; border-radius:5px; box-shadow:0 0 8px {{accent}};"></div>
121+
</div>
122+
<div style="font:500 9px 'Chakra Petch'; letter-spacing:0.16em; color:rgba(255,255,255,0.45);">TURBO</div>
123+
</div>
124+
<div style="display:flex; align-items:center; gap:6px; padding:5px 9px; border-radius:20px; background:rgba(70,224,255,0.1); border:1px solid rgba(70,224,255,0.32);">
125+
<div style="width:6px; height:6px; border-radius:50%; background:{{accent}}; box-shadow:0 0 8px {{accent}};"></div>
126+
<div style="font:600 9px 'Chakra Petch'; letter-spacing:0.16em; color:{{accent}};">BELT</div>
127+
</div>
128+
</div>
129+
130+
</div>
131+
132+
<!-- tach line -->
133+
<div style="display:flex; align-items:flex-end; gap:16px;">
134+
<div style="display:flex; flex-direction:column; align-items:center; line-height:1;">
135+
<div style="font:700 22px 'Chakra Petch'; color:#fff;">{{gear}}</div>
136+
<div style="font:500 8px 'Chakra Petch'; letter-spacing:0.2em; color:rgba(255,255,255,0.4); margin-top:3px;">GEAR</div>
137+
</div>
138+
<div style="display:flex; align-items:flex-end; gap:3px; height:28px;">
139+
<sc-for list="{{ seg }}" as="s" hint-placeholder-count="10">
140+
<div style="width:5px; border-radius:2px; height:{{s.h}}px; background:{{s.bg}};"></div>
141+
</sc-for>
142+
</div>
143+
<div style="display:flex; flex-direction:column; align-items:flex-start; line-height:1;">
144+
<div style="font:600 16px 'Chakra Petch'; color:#fff;">{{rpmInt}}</div>
145+
<div style="font:500 8px 'Chakra Petch'; letter-spacing:0.2em; color:rgba(255,255,255,0.4); margin-top:3px;">RPM</div>
146+
</div>
147+
</div>
148+
149+
</div>
150+
</div>
151+
</x-dc>
152+
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:880,&quot;height&quot;:495},&quot;variant&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;ring&quot;,&quot;arc&quot;,&quot;linear&quot;],&quot;default&quot;:&quot;ring&quot;,&quot;tsType&quot;:&quot;string&quot;},&quot;accent&quot;:{&quot;editor&quot;:&quot;color&quot;,&quot;default&quot;:&quot;#46e0ff&quot;,&quot;tsType&quot;:&quot;string&quot;},&quot;showMinimap&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:true,&quot;tsType&quot;:&quot;boolean&quot;},&quot;animate&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:true,&quot;tsType&quot;:&quot;boolean&quot;}}">
153+
class Component extends DCLogic {
154+
state = { now: 0 };
155+
componentDidMount() {
156+
if (this.props.animate === false) { this.setState({ now: 3400 }); return; }
157+
this._start = performance.now();
158+
this._last = -999;
159+
const loop = () => {
160+
this._raf = requestAnimationFrame(loop);
161+
const el = performance.now() - this._start;
162+
if (el - this._last < 32) return;
163+
this._last = el;
164+
this.setState({ now: el });
165+
};
166+
this._raf = requestAnimationFrame(loop);
167+
}
168+
componentWillUnmount() { if (this._raf) cancelAnimationFrame(this._raf); }
169+
clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
170+
renderVals() {
171+
const C = (v, a, b) => this.clamp(v, a, b);
172+
const accent = this.props.accent || '#46e0ff';
173+
const variant = this.props.variant || 'ring';
174+
const t = (this.state.now || 0) / 1000;
175+
const MAXS = 200, MAXR = 8000;
176+
const f = (x) => 90 + 78 * Math.sin(x * 0.22) + 12 * Math.sin(x * 0.9);
177+
const sp = C(f(t), 0, MAXS);
178+
const accel = sp - C(f(t - 0.12), 0, MAXS);
179+
const bands = [[0, 20], [20, 45], [45, 75], [75, 110], [110, 150], [150, 200]];
180+
let gear = 1, frac = 0;
181+
for (let i = 0; i < bands.length; i++) {
182+
if (sp >= bands[i][0]) { gear = i + 1; frac = (sp - bands[i][0]) / (bands[i][1] - bands[i][0]); }
183+
}
184+
frac = C(frac, 0, 1);
185+
const rpm = C(1200 + frac * 5600 + 60 * Math.sin(t * 7), 700, MAXR);
186+
const speedFrac = sp / MAXS, rpmFrac = rpm / MAXR;
187+
const boost = C(accel * 22, 0, 100);
188+
const fuel = 44 + 28 * Math.cos(t * 0.04);
189+
const eng = 88 + 6 * Math.sin(t * 0.3);
190+
const headingDeg = ((t * 9) % 360 + 360) % 360;
191+
const lit = Math.round(rpmFrac * 10);
192+
const heights = [9, 11, 13, 15, 17, 19, 21, 23, 25, 27];
193+
const seg = heights.map((h, i) => ({
194+
h,
195+
bg: i < lit ? (i >= 8 ? '#ff5a52' : (i >= 7 ? '#ffc24d' : accent)) : 'rgba(255,255,255,0.10)'
196+
}));
197+
const barCol = (p) => p > 50 ? accent : (p > 22 ? '#ffc24d' : '#ff5a52');
198+
const lab = { 0: 'N', 45: 'NE', 90: 'E', 135: 'SE', 180: 'S', 225: 'SW', 270: 'W', 315: 'NW' };
199+
const ticks = [];
200+
for (let d = 0; d <= 720; d += 15) {
201+
const major = d % 45 === 0;
202+
ticks.push({
203+
left: d * 3,
204+
h: major ? 12 : 6,
205+
col: major ? 'rgba(255,255,255,0.72)' : 'rgba(255,255,255,0.20)',
206+
label: major ? (lab[d % 360] || '') : ''
207+
});
208+
}
209+
return {
210+
accent,
211+
isRing: variant === 'ring', isArc: variant === 'arc', isLinear: variant === 'linear',
212+
speedInt: Math.round(sp), rpmInt: Math.round(rpm), gear,
213+
pSpeed: speedFrac * 100, pArc: speedFrac * 50, speedPct: speedFrac * 100,
214+
seg,
215+
fuel: Math.round(fuel), eng: Math.round(eng), boost: Math.round(boost),
216+
fuelCol: barCol(fuel), engCol: barCol(eng),
217+
headingDeg: Math.round(headingDeg),
218+
compassX: 118 - headingDeg * 3,
219+
ticks,
220+
showMinimap: this.props.showMinimap !== false
221+
};
222+
}
223+
}
224+
</script>
225+
</body>
226+
</html>

0 commit comments

Comments
 (0)