1089 lines
35 KiB
Lua
1089 lines
35 KiB
Lua
-- client/main.lua
|
|
print("^2[turfwar]^7 CLIENT main.lua LOADED (uniforms + join points + HQ blips + turfs + capture ping + leaderboard + HQ respawn + capture HUD colors)")
|
|
|
|
local currentGang = 0
|
|
local isPolice = false
|
|
local POLICE_GANG_ID = 3
|
|
|
|
local hqBlips = {}
|
|
|
|
-- Turfs client cache
|
|
local Turfs = {} -- keyed by turfId
|
|
local TurfBlips = {} -- { [turfId] = { radiusBlip=..., centerBlip=... } }
|
|
local SecondsToCapture = 60
|
|
|
|
-- Capture ping throttling (prevents spamming server every frame)
|
|
local nextCapturePing = {} -- [turfId] = gameTimeMs
|
|
|
|
-- Leaderboard cache (rank only)
|
|
local GangLeaderboard = {}
|
|
|
|
--========================================================
|
|
-- Helpers
|
|
--========================================================
|
|
local function Notify(msg)
|
|
BeginTextCommandThefeedPost("STRING")
|
|
AddTextComponentSubstringPlayerName(msg)
|
|
EndTextCommandThefeedPostTicker(false, false)
|
|
end
|
|
|
|
local function Draw3DText(x, y, z, text)
|
|
local onScreen, _x, _y = World3dToScreen2d(x, y, z)
|
|
if not onScreen then return end
|
|
SetTextScale(0.35, 0.35)
|
|
SetTextFont(4)
|
|
SetTextProportional(1)
|
|
SetTextEntry("STRING")
|
|
SetTextCentre(1)
|
|
AddTextComponentString(text)
|
|
DrawText(_x, _y)
|
|
end
|
|
|
|
local function EnsurePlayerModel(modelName)
|
|
if not modelName or modelName == "" then return false end
|
|
|
|
local model = joaat(modelName)
|
|
if not IsModelInCdimage(model) or not IsModelValid(model) then
|
|
print(("^1[turfwar]^7 INVALID model: %s"):format(modelName))
|
|
return false
|
|
end
|
|
|
|
RequestModel(model)
|
|
local timeout = GetGameTimer() + 8000
|
|
while not HasModelLoaded(model) do
|
|
Wait(0)
|
|
if GetGameTimer() > timeout then
|
|
print(("^1[turfwar]^7 TIMEOUT loading model: %s"):format(modelName))
|
|
return false
|
|
end
|
|
end
|
|
|
|
SetPlayerModel(PlayerId(), model)
|
|
SetModelAsNoLongerNeeded(model)
|
|
Wait(200)
|
|
return true
|
|
end
|
|
|
|
local function ClearAllProps(ped)
|
|
for i = 0, 7 do
|
|
ClearPedProp(ped, i)
|
|
end
|
|
end
|
|
|
|
RegisterNetEvent("turfwar:leaderboard:update", function(payload)
|
|
SendNUIMessage({ type = "turfwar:leaderboard:update", payload = payload })
|
|
end)
|
|
|
|
CreateThread(function()
|
|
Wait(1500)
|
|
TriggerServerEvent("turfwar:leaderboard:request")
|
|
end)
|
|
|
|
|
|
--========================================================
|
|
-- Freemode helpers (hair color init WITHOUT wiping outfit)
|
|
--========================================================
|
|
local function IsFreemodePed(ped)
|
|
local m = GetEntityModel(ped)
|
|
return (m == joaat("mp_m_freemode_01") or m == joaat("mp_f_freemode_01"))
|
|
end
|
|
|
|
-- One-time init per model swap. This fixes the "-1 hair color" issue.
|
|
local freemodeInited = false
|
|
|
|
local function InitFreemodePaletteOnce(ped)
|
|
if freemodeInited then return end
|
|
if not IsFreemodePed(ped) then return end
|
|
|
|
-- If already initialized, don't touch anything.
|
|
local c1 = GetPedHairColor(ped)
|
|
if c1 ~= nil and c1 ~= -1 then
|
|
freemodeInited = true
|
|
return
|
|
end
|
|
|
|
-- IMPORTANT: This can change appearance, so we do it ONCE right after model set,
|
|
-- and then we will apply your uniform components immediately after.
|
|
SetPedDefaultComponentVariation(ped)
|
|
|
|
SetPedHeadBlendData(
|
|
ped,
|
|
0, 0, 0, -- shapeFirst, shapeSecond, shapeThird
|
|
0, 0, 0, -- skinFirst, skinSecond, skinThird
|
|
0.5, 0.5, 0.0,
|
|
false
|
|
)
|
|
|
|
for i = 0, 19 do
|
|
SetPedFaceFeature(ped, i, 0.0)
|
|
end
|
|
|
|
freemodeInited = true
|
|
print("^2[turfwar]^7 Freemode palette init completed (hair colors should work now).")
|
|
end
|
|
|
|
local function ApplyHairColorIfAny(ped, uniform)
|
|
if not uniform or not uniform.hairColor then return end
|
|
if not IsFreemodePed(ped) then
|
|
-- Hair color is only meaningful on freemode peds (most other peds ignore it)
|
|
return
|
|
end
|
|
|
|
local primary = tonumber(uniform.hairColor.primary)
|
|
local highlight = tonumber(uniform.hairColor.highlight)
|
|
if primary == nil then return end
|
|
if highlight == nil then highlight = primary end
|
|
|
|
-- Hats can visually hide hair; don't force, just clear if they want hair visible.
|
|
if uniform.forceHairVisible then
|
|
ClearPedProp(ped, 0) -- hat
|
|
SetPedComponentVariation(ped, 1, 0, 0, 0) -- mask
|
|
end
|
|
|
|
local beforeP = GetPedHairColor(ped)
|
|
local beforeH = GetPedHairHighlightColor(ped)
|
|
|
|
SetPedHairColor(ped, primary, highlight)
|
|
|
|
local afterP = GetPedHairColor(ped)
|
|
local afterH = GetPedHairHighlightColor(ped)
|
|
|
|
print(("[turfwar] hairColor %s/%s -> %s/%s (requested %d/%d)")
|
|
:format(tostring(beforeP), tostring(beforeH), tostring(afterP), tostring(afterH), primary, highlight))
|
|
end
|
|
|
|
--========================================================
|
|
-- Model Correction
|
|
--========================================================
|
|
|
|
|
|
RegisterCommand("tw_modelname", function()
|
|
local ped = PlayerPedId()
|
|
local m = GetEntityModel(ped)
|
|
|
|
local known = {
|
|
"mp_m_freemode_01",
|
|
"mp_f_freemode_01",
|
|
"player_zero",
|
|
"player_one",
|
|
"player_two",
|
|
"s_m_y_cop_01",
|
|
"s_m_y_sheriff_01",
|
|
"s_m_y_hwaycop_01",
|
|
"s_m_y_ranger_01",
|
|
"s_m_m_security_01",
|
|
}
|
|
|
|
print(("[turfwar] model hash=%d"):format(m))
|
|
for _, name in ipairs(known) do
|
|
if m == GetHashKey(name) then
|
|
print(("[turfwar] model name=%s"):format(name))
|
|
return
|
|
end
|
|
end
|
|
print("[turfwar] model name=UNKNOWN (not in quick list)")
|
|
end, false)
|
|
|
|
|
|
|
|
--========================================================
|
|
-- Capture HUD state + gang color styling
|
|
--========================================================
|
|
local captureHud = { active = false, turfId = nil }
|
|
|
|
local function rgbToCss(rgb, fallback)
|
|
if type(rgb) ~= "table" then return fallback end
|
|
local r = tonumber(rgb[1]) or tonumber(rgb.r)
|
|
local g = tonumber(rgb[2]) or tonumber(rgb.g)
|
|
local b = tonumber(rgb[3]) or tonumber(rgb.b)
|
|
if not r or not g or not b then return fallback end
|
|
return ("rgb(%d,%d,%d)"):format(r, g, b)
|
|
end
|
|
|
|
local function gangColorCss(gangId, fallback)
|
|
local g = (Config and Config.GANGS and Config.GANGS[tonumber(gangId) or 0]) or nil
|
|
return rgbToCss(g and g.rgb, fallback)
|
|
end
|
|
|
|
local function CaptureHUD_Style(contestingGang, ownerGang)
|
|
SendNUIMessage({
|
|
type = "capture:style",
|
|
fill = gangColorCss(contestingGang, "rgba(255,255,255,0.85)"),
|
|
bg = gangColorCss(ownerGang, "rgba(255,255,255,0.14)")
|
|
})
|
|
end
|
|
|
|
local function CaptureHUD_Start(turfId)
|
|
local t = Turfs[turfId]
|
|
if not t then return end
|
|
|
|
captureHud.active = true
|
|
captureHud.turfId = turfId
|
|
|
|
local owner = tonumber(t.owner) or 0
|
|
|
|
SendNUIMessage({
|
|
type = "capture:start",
|
|
turfName = t.name or turfId,
|
|
fill = gangColorCss(currentGang, "rgba(255,255,255,0.85)"),
|
|
bg = gangColorCss(owner, "rgba(255,255,255,0.14)")
|
|
})
|
|
end
|
|
|
|
local function CaptureHUD_Stop()
|
|
if not captureHud.active then return end
|
|
captureHud.active = false
|
|
captureHud.turfId = nil
|
|
SendNUIMessage({ type = "capture:stop" })
|
|
end
|
|
|
|
local function CaptureHUD_Set(progress)
|
|
if not captureHud.active then return end
|
|
local p = tonumber(progress) or 0
|
|
if p < 0 then p = 0 end
|
|
local t = (SecondsToCapture > 0) and (p / SecondsToCapture) or 0
|
|
if t < 0 then t = 0 end
|
|
if t > 1 then t = 1 end
|
|
SendNUIMessage({ type = "capture:set", t = t })
|
|
end
|
|
|
|
RegisterNetEvent("turfwar:captureHint", function(msg)
|
|
BeginTextCommandThefeedPost("STRING")
|
|
AddTextComponentSubstringPlayerName(msg)
|
|
EndTextCommandThefeedPostTicker(false, false)
|
|
SendNUIMessage({ type="capture:hint", text=msg })
|
|
end)
|
|
|
|
RegisterNetEvent("turfwar:capturePaused", function(turfId, paused)
|
|
SendNUIMessage({ type="capture:paused", paused = paused and true or false })
|
|
end)
|
|
|
|
--========================================================
|
|
-- Bulletproof capture helpers
|
|
--========================================================
|
|
local function InTurfZone(turfId)
|
|
local t = Turfs[turfId]
|
|
if not t or not t.center then return false end
|
|
|
|
local ped = PlayerPedId()
|
|
if not ped or ped == 0 then return false end
|
|
|
|
local cx = tonumber(t.center.x)
|
|
local cy = tonumber(t.center.y)
|
|
local cz = tonumber(t.center.z)
|
|
local radius = tonumber(t.radius) or 0.0
|
|
if not cx or not cy or not cz or radius <= 0.0 then return false end
|
|
|
|
local p = GetEntityCoords(ped)
|
|
local d = #(p - vector3(cx, cy, cz))
|
|
return d <= radius
|
|
end
|
|
|
|
local function EligibleForCapture(turfId)
|
|
local t = Turfs[turfId]
|
|
if not t then return false end
|
|
local owner = tonumber(t.owner) or 0
|
|
return (currentGang ~= 0) and (owner ~= currentGang)
|
|
end
|
|
|
|
--========================================================
|
|
-- On-foot state reporting (server uses this to block vehicle capturing)
|
|
--========================================================
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(350)
|
|
local ped = PlayerPedId()
|
|
if ped and ped ~= 0 then
|
|
local onFoot = not IsPedInAnyVehicle(ped, false)
|
|
TriggerServerEvent("turfwar:setOnFoot", onFoot)
|
|
end
|
|
end
|
|
end)
|
|
|
|
--========================================================
|
|
-- Police: no wanted stars (Gang 3)
|
|
--========================================================
|
|
local function UpdatePoliceWantedState()
|
|
isPolice = (currentGang == POLICE_GANG_ID)
|
|
|
|
if isPolice then
|
|
SetMaxWantedLevel(0)
|
|
ClearPlayerWantedLevel(PlayerId())
|
|
else
|
|
SetMaxWantedLevel(5)
|
|
end
|
|
end
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(500)
|
|
if isPolice then
|
|
local pid = PlayerId()
|
|
if GetPlayerWantedLevel(pid) ~= 0 then
|
|
ClearPlayerWantedLevel(pid)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
--========================================================
|
|
-- Player arrow color (your minimap arrow)
|
|
--========================================================
|
|
local function UpdatePlayerBlipColor(gangId)
|
|
local blip = GetMainPlayerBlipId()
|
|
if not blip or blip == 0 then return end
|
|
|
|
local gang = Config.GANGS and Config.GANGS[gangId]
|
|
local color = (gang and gang.blipColor) or 0
|
|
|
|
SetBlipSprite(blip, 6)
|
|
ShowHeadingIndicatorOnBlip(blip, true)
|
|
SetBlipColour(blip, color)
|
|
SetBlipScale(blip, 1.0)
|
|
SetBlipAsShortRange(blip, false)
|
|
end
|
|
|
|
--========================================================
|
|
-- Uniform application (Male/Female variants + safe setters)
|
|
-- Drop-in section for client/main.lua
|
|
--========================================================
|
|
|
|
local ApplyGangUniform -- forward declaration
|
|
local SafeSetComponent -- forward declaration
|
|
local SafeSetProp -- forward declaration
|
|
local ReapplyUniformForSeconds -- forward declaration
|
|
|
|
-- ---------------------------------------------------------
|
|
-- Safe setters (validate drawable/texture limits)
|
|
-- ---------------------------------------------------------
|
|
SafeSetComponent = function(ped, compId, drawable, texture)
|
|
compId = tonumber(compId) or 0
|
|
drawable = tonumber(drawable) or 0
|
|
texture = tonumber(texture) or 0
|
|
|
|
local maxDraw = GetNumberOfPedDrawableVariations(ped, compId)
|
|
if maxDraw <= 0 then
|
|
print(("[turfwar] comp %d has no drawables (model mismatch?)"):format(compId))
|
|
return false
|
|
end
|
|
|
|
if drawable < 0 or drawable >= maxDraw then
|
|
print(("[turfwar] INVALID comp %d drawable %d (max %d) - SKIP"):format(compId, drawable, maxDraw))
|
|
return false
|
|
end
|
|
|
|
local maxTex = GetNumberOfPedTextureVariations(ped, compId, drawable)
|
|
if maxTex <= 0 then maxTex = 1 end
|
|
|
|
if texture < 0 or texture >= maxTex then
|
|
print(("[turfwar] INVALID comp %d tex %d (max %d) -> clamp"):format(compId, texture, maxTex))
|
|
texture = math.max(0, math.min(texture, maxTex - 1))
|
|
end
|
|
|
|
SetPedComponentVariation(ped, compId, drawable, texture, 0)
|
|
return true
|
|
end
|
|
|
|
SafeSetProp = function(ped, propId, drawable, texture)
|
|
propId = tonumber(propId) or 0
|
|
drawable = tonumber(drawable) or -1
|
|
texture = tonumber(texture) or 0
|
|
|
|
-- drawable < 0 means "remove this prop"
|
|
if drawable < 0 then
|
|
ClearPedProp(ped, propId)
|
|
return true
|
|
end
|
|
|
|
local maxDraw = GetNumberOfPedPropDrawableVariations(ped, propId)
|
|
if maxDraw <= 0 then
|
|
print(("[turfwar] prop %d has no drawables"):format(propId))
|
|
return false
|
|
end
|
|
|
|
if drawable >= maxDraw then
|
|
print(("[turfwar] INVALID prop %d drawable %d (max %d) - SKIP"):format(propId, drawable, maxDraw))
|
|
return false
|
|
end
|
|
|
|
local maxTex = GetNumberOfPedPropTextureVariations(ped, propId, drawable)
|
|
if maxTex <= 0 then maxTex = 1 end
|
|
|
|
if texture < 0 or texture >= maxTex then
|
|
print(("[turfwar] INVALID prop %d tex %d (max %d) -> clamp"):format(propId, texture, maxTex))
|
|
texture = math.max(0, math.min(texture, maxTex - 1))
|
|
end
|
|
|
|
ClearPedProp(ped, propId)
|
|
SetPedPropIndex(ped, propId, drawable, texture, true)
|
|
return true
|
|
end
|
|
|
|
-- ---------------------------------------------------------
|
|
-- ApplyGangUniform
|
|
-- Supports:
|
|
-- uniform.components / uniform.props (legacy)
|
|
-- uniform.components_m / uniform.props_m
|
|
-- uniform.components_f / uniform.props_f
|
|
--
|
|
-- IMPORTANT:
|
|
-- - Hair is now player-controlled: DO NOT apply component 2 or hairColor here.
|
|
-- - Do NOT force Config.FREEMODE_MODEL here (appearance system sets model).
|
|
-- - Only swap model if uniform.model is explicitly set.
|
|
-- ---------------------------------------------------------
|
|
ApplyGangUniform = function(gangId)
|
|
local uniform = Config.UNIFORMS and Config.UNIFORMS[gangId]
|
|
if not uniform then return end
|
|
|
|
local ped = PlayerPedId()
|
|
if not ped or ped == 0 then return end
|
|
|
|
-- Optional: uniform-specific model override ONLY
|
|
local usedModel = nil
|
|
if uniform.model and uniform.model ~= "" then
|
|
usedModel = uniform.model
|
|
end
|
|
|
|
local beforeModel = GetEntityModel(ped)
|
|
|
|
if usedModel then
|
|
local changed = EnsurePlayerModel(usedModel)
|
|
ped = PlayerPedId()
|
|
local afterModel = GetEntityModel(ped)
|
|
print(("[turfwar] model swap %s -> %s (requested %s) changed=%s")
|
|
:format(tostring(beforeModel), tostring(afterModel), tostring(usedModel), tostring(changed)))
|
|
if changed then
|
|
freemodeInited = false
|
|
end
|
|
end
|
|
|
|
-- Refresh ped after any model swap
|
|
ped = PlayerPedId()
|
|
if not ped or ped == 0 then return end
|
|
|
|
-- Determine gender by current model
|
|
local model = GetEntityModel(ped)
|
|
local isFemale = (model == joaat("mp_f_freemode_01"))
|
|
|
|
-- Pick the correct component/prop lists
|
|
-- Backward compatible with legacy `components/props`
|
|
local comps = uniform.components
|
|
local props = uniform.props
|
|
|
|
if isFemale then
|
|
comps = uniform.components_f or comps
|
|
props = uniform.props_f or props
|
|
else
|
|
comps = uniform.components_m or comps
|
|
props = uniform.props_m or props
|
|
end
|
|
|
|
-- Ensure freemode palette once (doesn't wipe outfit every time)
|
|
InitFreemodePaletteOnce(ped)
|
|
|
|
-- Clear props first (if configured)
|
|
if uniform.clearProps then
|
|
ClearAllProps(ped)
|
|
end
|
|
|
|
-- Apply components (safe)
|
|
if comps then
|
|
for _, comp in ipairs(comps) do
|
|
-- Hair is player-controlled now; ignore comp 2 even if present in data
|
|
if tonumber(comp.id) ~= 2 then
|
|
SafeSetComponent(ped, comp.id, comp.drawable, comp.texture or 0)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Apply props (safe)
|
|
if props then
|
|
for _, prop in ipairs(props) do
|
|
SafeSetProp(ped, prop.id, prop.drawable, prop.texture or 0)
|
|
end
|
|
end
|
|
|
|
-- Hair color is player-controlled now; do not apply from uniform
|
|
-- ApplyHairColorIfAny(ped, uniform)
|
|
|
|
print(("^2[turfwar]^7 Applied uniform: %s (gangId=%s, gender=%s)"):format(
|
|
uniform.label or "Uniform",
|
|
tostring(gangId),
|
|
isFemale and "female" or "male"
|
|
))
|
|
end
|
|
|
|
-- ---------------------------------------------------------
|
|
-- Reapply helper (prevents other scripts briefly overriding)
|
|
-- ---------------------------------------------------------
|
|
ReapplyUniformForSeconds = function(gangId, seconds)
|
|
local endTime = GetGameTimer() + ((tonumber(seconds) or 0) * 1000)
|
|
CreateThread(function()
|
|
while GetGameTimer() < endTime do
|
|
ApplyGangUniform(gangId)
|
|
Wait(500)
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Your existing ApplyUniformSoon can stay as-is, but included here for completeness
|
|
local function ApplyUniformSoon(gangId)
|
|
CreateThread(function()
|
|
Wait(Config.UNIFORM_APPLY_DELAY or 700)
|
|
ApplyGangUniform(gangId)
|
|
|
|
-- Helps when other resources overwrite clothing right after spawn
|
|
ReapplyUniformForSeconds(gangId, 4)
|
|
end)
|
|
end
|
|
|
|
--========================================================
|
|
-- HQ Respawn teleport
|
|
--========================================================
|
|
local function TeleportToHQ(spawn)
|
|
if not spawn then return end
|
|
|
|
local ped = PlayerPedId()
|
|
if not ped or ped == 0 then return end
|
|
|
|
local x, y, z = spawn.x, spawn.y, spawn.z
|
|
local h = spawn.h or 0.0
|
|
|
|
DoScreenFadeOut(250)
|
|
while not IsScreenFadedOut() do Wait(0) end
|
|
|
|
RequestCollisionAtCoord(x, y, z)
|
|
SetEntityCoordsNoOffset(ped, x, y, z, false, false, false)
|
|
SetEntityHeading(ped, h)
|
|
|
|
local t = GetGameTimer() + 2500
|
|
while not HasCollisionLoadedAroundEntity(ped) and GetGameTimer() < t do
|
|
Wait(0)
|
|
end
|
|
|
|
DoScreenFadeIn(250)
|
|
end
|
|
|
|
--========================================================
|
|
-- HQ Blips (always visible)
|
|
--========================================================
|
|
local function CreateHQBlips()
|
|
for _, b in ipairs(hqBlips) do
|
|
if DoesBlipExist(b) then RemoveBlip(b) end
|
|
end
|
|
hqBlips = {}
|
|
|
|
for _, jp in ipairs(Config.JOIN_POINTS or {}) do
|
|
local gang = (Config.GANGS or {})[jp.gangId]
|
|
local blip = AddBlipForCoord(jp.pos.x, jp.pos.y, jp.pos.z)
|
|
|
|
SetBlipSprite(blip, 58)
|
|
SetBlipScale(blip, 0.9)
|
|
SetBlipColour(blip, gang and gang.blipColor or 0)
|
|
SetBlipDisplay(blip, 4)
|
|
SetBlipAsShortRange(blip, false)
|
|
|
|
BeginTextCommandSetBlipName("STRING")
|
|
AddTextComponentString(jp.label or ("Gang " .. tostring(jp.gangId)))
|
|
EndTextCommandSetBlipName(blip)
|
|
|
|
table.insert(hqBlips, blip)
|
|
end
|
|
|
|
print(("^2[turfwar]^7 HQ blips created: %d"):format(#hqBlips))
|
|
end
|
|
|
|
--========================================================
|
|
-- Turf blips
|
|
--========================================================
|
|
local function ClearTurfBlips()
|
|
for _, b in pairs(TurfBlips) do
|
|
if b.radiusBlip and DoesBlipExist(b.radiusBlip) then RemoveBlip(b.radiusBlip) end
|
|
if b.centerBlip and DoesBlipExist(b.centerBlip) then RemoveBlip(b.centerBlip) end
|
|
end
|
|
TurfBlips = {}
|
|
end
|
|
|
|
local function EnsureTurfBlip(turfId, turf)
|
|
if TurfBlips[turfId] then return end
|
|
if not turf or not turf.center then return end
|
|
|
|
local radius = tonumber(turf.radius) or 0.0
|
|
if radius <= 0.0 then return end
|
|
|
|
local cx = tonumber(turf.center.x) or 0.0
|
|
local cy = tonumber(turf.center.y) or 0.0
|
|
local cz = tonumber(turf.center.z) or 0.0
|
|
local center = vector3(cx, cy, cz)
|
|
|
|
local r = AddBlipForRadius(center.x, center.y, center.z, radius)
|
|
SetBlipAlpha(r, 80)
|
|
SetBlipDisplay(r, 4)
|
|
SetBlipAsShortRange(r, true)
|
|
|
|
local c = AddBlipForCoord(center.x, center.y, center.z)
|
|
SetBlipSprite(c, 84)
|
|
SetBlipScale(c, 0.75)
|
|
SetBlipDisplay(c, 4)
|
|
SetBlipAsShortRange(c, true)
|
|
|
|
BeginTextCommandSetBlipName("STRING")
|
|
AddTextComponentString(turf.name or turfId)
|
|
EndTextCommandSetBlipName(c)
|
|
|
|
TurfBlips[turfId] = { radiusBlip = r, centerBlip = c }
|
|
|
|
local owner = tonumber(turf.owner) or 0
|
|
local col = ((Config.GANGS or {})[owner] and (Config.GANGS or {})[owner].blipColor) or 0
|
|
SetBlipColour(r, col)
|
|
SetBlipColour(c, col)
|
|
end
|
|
|
|
local function UpdateTurfBlipColor(turfId)
|
|
local b = TurfBlips[turfId]
|
|
local t = Turfs[turfId]
|
|
if not b or not t then return end
|
|
|
|
local owner = tonumber(t.owner) or 0
|
|
local col = ((Config.GANGS or {})[owner] and (Config.GANGS or {})[owner].blipColor) or 0
|
|
|
|
if b.radiusBlip and DoesBlipExist(b.radiusBlip) then SetBlipColour(b.radiusBlip, col) end
|
|
if b.centerBlip and DoesBlipExist(b.centerBlip) then SetBlipColour(b.centerBlip, col) end
|
|
end
|
|
|
|
--========================================================
|
|
-- Leaderboard HUD (compact)
|
|
--========================================================
|
|
local BLIP_RGB = {
|
|
[0] = {255, 255, 255},
|
|
[2] = { 60, 200, 60},
|
|
[3] = { 70, 120, 255},
|
|
[5] = {255, 220, 60},
|
|
[7] = {190, 90, 255},
|
|
[40] = { 40, 40, 40},
|
|
}
|
|
|
|
local function getGangName(gangId)
|
|
local g = Config and Config.GANGS and Config.GANGS[gangId]
|
|
return (g and g.name) or ("Gang " .. tostring(gangId))
|
|
end
|
|
|
|
local function getGangRGB(gangId)
|
|
local g = Config and Config.GANGS and Config.GANGS[gangId]
|
|
local blip = (g and tonumber(g.blipColor)) or 0
|
|
return BLIP_RGB[blip] or {255, 255, 255}
|
|
end
|
|
|
|
local function drawText(x, y, text, r, g, b, a, scale)
|
|
SetTextFont(4)
|
|
SetTextScale(scale, scale)
|
|
SetTextColour(r, g, b, a)
|
|
SetTextOutline()
|
|
BeginTextCommandDisplayText("STRING")
|
|
AddTextComponentSubstringPlayerName(text)
|
|
EndTextCommandDisplayText(x, y)
|
|
end
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(0)
|
|
|
|
if not GangLeaderboard or #GangLeaderboard == 0 then
|
|
goto continue
|
|
end
|
|
|
|
local maxRows = math.min(#GangLeaderboard, 8)
|
|
|
|
local x = 0.018
|
|
local y = 0.185
|
|
|
|
local titleScale = 0.32
|
|
local rowScale = 0.28
|
|
|
|
local rowH = 0.018
|
|
local padX = 0.008
|
|
local padY = 0.008
|
|
|
|
local panelW = 0.14
|
|
local panelH = padY + 0.020 + (maxRows * rowH) + padY
|
|
|
|
DrawRect(x + panelW/2, y + panelH/2, panelW, panelH, 0, 0, 0, 105)
|
|
drawText(x + padX, y + padY, "Gang Wealth (Rank)", 255, 255, 255, 235, titleScale)
|
|
|
|
local startY = y + padY + 0.020
|
|
for i = 1, maxRows do
|
|
local gangId = GangLeaderboard[i]
|
|
local name = getGangName(gangId)
|
|
local r, g, b = table.unpack(getGangRGB(gangId))
|
|
|
|
local rowY = startY + ((i - 1) * rowH)
|
|
|
|
DrawRect(x + panelW/2, rowY + 0.009, panelW, rowH, 255, 255, 255, 8)
|
|
drawText(x + padX, rowY, ("%d."):format(i), 235, 235, 235, 235, rowScale)
|
|
drawText(x + padX + 0.020, rowY, name, r, g, b, 245, rowScale)
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
end)
|
|
|
|
--========================================================
|
|
-- Net Events
|
|
--========================================================
|
|
RegisterNetEvent("turfwar:gangUpdate", function(gangId)
|
|
currentGang = tonumber(gangId) or 0
|
|
UpdatePoliceWantedState()
|
|
CaptureHUD_Stop()
|
|
|
|
Notify(("Gang set to: %s"):format((Config.GANGS[currentGang] and Config.GANGS[currentGang].name) or "Unknown"))
|
|
ApplyUniformSoon(currentGang)
|
|
UpdatePlayerBlipColor(currentGang)
|
|
end)
|
|
|
|
RegisterNetEvent("turfwar:setFaction", function(gangId)
|
|
currentGang = tonumber(gangId) or 0
|
|
UpdatePoliceWantedState()
|
|
CaptureHUD_Stop()
|
|
|
|
ApplyUniformSoon(currentGang)
|
|
UpdatePlayerBlipColor(currentGang)
|
|
end)
|
|
|
|
RegisterNetEvent("turfwar:snapshot", function(payload, secondsToCapture)
|
|
SecondsToCapture = tonumber(secondsToCapture) or SecondsToCapture
|
|
Turfs = payload or {}
|
|
nextCapturePing = {}
|
|
|
|
if captureHud.active and captureHud.turfId and not Turfs[captureHud.turfId] then
|
|
CaptureHUD_Stop()
|
|
end
|
|
|
|
ClearTurfBlips()
|
|
for turfId, turf in pairs(Turfs) do
|
|
EnsureTurfBlip(turfId, turf)
|
|
UpdateTurfBlipColor(turfId)
|
|
end
|
|
end)
|
|
|
|
RegisterNetEvent("turfwar:turfUpdate", function(turfId, owner, progress, contestingGang)
|
|
if not Turfs[turfId] then return end
|
|
|
|
owner = tonumber(owner) or 0
|
|
local cont = tonumber(contestingGang) or 0
|
|
local prog = math.max(0, tonumber(progress) or 0)
|
|
|
|
Turfs[turfId].owner = owner
|
|
Turfs[turfId].progress = prog
|
|
Turfs[turfId].contestingGang = cont
|
|
|
|
EnsureTurfBlip(turfId, Turfs[turfId])
|
|
UpdateTurfBlipColor(turfId)
|
|
|
|
local inZone = InTurfZone(turfId)
|
|
local eligible = EligibleForCapture(turfId)
|
|
|
|
if currentGang == 0 or not inZone or not eligible then
|
|
if captureHud.active and captureHud.turfId == turfId then
|
|
CaptureHUD_Stop()
|
|
end
|
|
return
|
|
end
|
|
|
|
if cont == currentGang and cont ~= 0 then
|
|
if (not captureHud.active) or (captureHud.turfId ~= turfId) then
|
|
CaptureHUD_Start(turfId)
|
|
end
|
|
CaptureHUD_Style(cont, owner)
|
|
CaptureHUD_Set(prog)
|
|
else
|
|
if captureHud.active and captureHud.turfId == turfId then
|
|
CaptureHUD_Stop()
|
|
end
|
|
end
|
|
end)
|
|
|
|
RegisterNetEvent("turfwar:turfCaptured", function(turfId, newOwner)
|
|
if not Turfs[turfId] then return end
|
|
Turfs[turfId].owner = newOwner
|
|
Turfs[turfId].progress = 0
|
|
Turfs[turfId].contestingGang = 0
|
|
|
|
EnsureTurfBlip(turfId, Turfs[turfId])
|
|
UpdateTurfBlipColor(turfId)
|
|
|
|
if captureHud.active and captureHud.turfId == turfId then
|
|
CaptureHUD_Stop()
|
|
end
|
|
|
|
local name = Turfs[turfId].name or turfId
|
|
local gangName = (Config.GANGS[newOwner] and Config.GANGS[newOwner].name) or ("Gang " .. tostring(newOwner))
|
|
Notify(("Turf captured: %s -> %s"):format(name, gangName))
|
|
end)
|
|
|
|
RegisterNetEvent("turfwar:gangLeaderboard", function(rankedGangIds)
|
|
GangLeaderboard = rankedGangIds or {}
|
|
end)
|
|
|
|
-- HQ spawn response
|
|
local awaitingHQ = false
|
|
RegisterNetEvent('turfwar:myHQSpawn', function(spawn)
|
|
awaitingHQ = false
|
|
TeleportToHQ(spawn)
|
|
end)
|
|
|
|
-- Server tells us to ensure at least N wanted stars (anti-snipe capture penalty)
|
|
RegisterNetEvent("turfwar:wanted:setMin", function(minStars)
|
|
minStars = tonumber(minStars) or 0
|
|
if minStars <= 0 then return end
|
|
|
|
-- Police are forced to 0 wanted in your script; ignore
|
|
if isPolice then return end
|
|
|
|
local pid = PlayerId()
|
|
local cur = GetPlayerWantedLevel(pid) or 0
|
|
if cur < minStars then
|
|
SetMaxWantedLevel(5)
|
|
SetPlayerWantedLevel(pid, minStars, false)
|
|
SetPlayerWantedLevelNow(pid, false)
|
|
end
|
|
end)
|
|
|
|
RegisterNetEvent("turfwar:shop:syncGangRequest", function(nonce)
|
|
local g = 0
|
|
if exports and exports.turfwar and exports.turfwar.GetCurrentGang then
|
|
g = exports.turfwar:GetCurrentGang()
|
|
end
|
|
TriggerServerEvent("turfwar:shop:syncGangResponse", nonce, g)
|
|
end)
|
|
|
|
|
|
|
|
--========================================================
|
|
-- Spawn / resource start
|
|
--========================================================
|
|
AddEventHandler("playerSpawned", function()
|
|
ApplyUniformSoon(currentGang)
|
|
UpdatePoliceWantedState()
|
|
CaptureHUD_Stop()
|
|
|
|
CreateThread(function()
|
|
Wait(1000)
|
|
UpdatePlayerBlipColor(currentGang)
|
|
end)
|
|
|
|
CreateThread(function()
|
|
Wait(1500)
|
|
TriggerServerEvent('turfwar:getMyHQSpawn')
|
|
Wait(2000)
|
|
TriggerServerEvent('turfwar:getMyHQSpawn')
|
|
end)
|
|
end)
|
|
|
|
AddEventHandler("onClientResourceStart", function(res)
|
|
if res ~= GetCurrentResourceName() then return end
|
|
CreateThread(function()
|
|
Wait(500)
|
|
CreateHQBlips()
|
|
|
|
TriggerServerEvent("turfwar:clientReady")
|
|
TriggerServerEvent("turfwar:requestFaction")
|
|
|
|
Wait(1500)
|
|
TriggerServerEvent("turfwar:requestGangLeaderboard")
|
|
|
|
ApplyUniformSoon(currentGang)
|
|
UpdatePoliceWantedState()
|
|
|
|
Wait(1000)
|
|
UpdatePlayerBlipColor(currentGang)
|
|
|
|
Wait(1200)
|
|
TriggerServerEvent('turfwar:getMyHQSpawn')
|
|
end)
|
|
end)
|
|
|
|
--========================================================
|
|
-- Join points (press E)
|
|
--========================================================
|
|
CreateThread(function()
|
|
while true do
|
|
local ped = PlayerPedId()
|
|
local pcoords = GetEntityCoords(ped)
|
|
local sleep = 500
|
|
|
|
for _, jp in ipairs(Config.JOIN_POINTS or {}) do
|
|
local dist = #(pcoords - jp.pos)
|
|
if dist < 20.0 then
|
|
sleep = 0
|
|
|
|
DrawMarker(1, jp.pos.x, jp.pos.y, jp.pos.z - 1.0,
|
|
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
|
1.2, 1.2, 0.5, 255, 255, 255, 120,
|
|
false, true, 2, false, nil, nil, false)
|
|
|
|
if dist < (Config.JOIN_RADIUS or 2.0) then
|
|
Draw3DText(jp.pos.x, jp.pos.y, jp.pos.z + 0.5,
|
|
("~w~Press ~g~E~w~ to join ~b~%s~w~"):format(jp.label))
|
|
|
|
if IsControlJustPressed(0, 38) then
|
|
TriggerServerEvent("turfwar:setGang", jp.gangId)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
Wait(sleep)
|
|
end
|
|
end)
|
|
|
|
--========================================================
|
|
-- Turf visuals + capture ping (HUD is driven by turfUpdate)
|
|
--========================================================
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(0)
|
|
|
|
if not Turfs or next(Turfs) == nil then
|
|
Wait(500)
|
|
goto continue
|
|
end
|
|
|
|
local ped = PlayerPedId()
|
|
local p = GetEntityCoords(ped)
|
|
|
|
for turfId, t in pairs(Turfs) do
|
|
if not t or not t.center then goto continue_turf end
|
|
|
|
local cx = tonumber(t.center.x)
|
|
local cy = tonumber(t.center.y)
|
|
local cz = tonumber(t.center.z)
|
|
local radius = tonumber(t.radius) or 0.0
|
|
if not cx or not cy or not cz or radius <= 0.0 then goto continue_turf end
|
|
|
|
local center = vector3(cx, cy, cz)
|
|
local owner = tonumber(t.owner) or 0
|
|
local d = #(p - center)
|
|
|
|
local inZone = (d <= radius)
|
|
local inZoneAndEligible = (currentGang ~= 0) and inZone and (owner ~= currentGang)
|
|
|
|
-- ✅ Presence ping: counts defenders + attackers as "in zone"
|
|
if currentGang ~= 0 and inZone then
|
|
local now = GetGameTimer()
|
|
nextCapturePing["_presence_" .. turfId] = nextCapturePing["_presence_" .. turfId] or 0
|
|
if now >= nextCapturePing["_presence_" .. turfId] then
|
|
nextCapturePing["_presence_" .. turfId] = now + 1000
|
|
TriggerServerEvent("turfwar:notePresence", turfId)
|
|
end
|
|
end
|
|
|
|
-- Capture ping (only when eligible)
|
|
if inZoneAndEligible then
|
|
local now = GetGameTimer()
|
|
if not nextCapturePing[turfId] or now >= nextCapturePing[turfId] then
|
|
nextCapturePing[turfId] = now + 1000
|
|
TriggerServerEvent("turfwar:attemptCapture", turfId)
|
|
end
|
|
end
|
|
|
|
if captureHud.active and captureHud.turfId == turfId and not inZone then
|
|
CaptureHUD_Stop()
|
|
end
|
|
|
|
if d < (radius + 40.0) then
|
|
DrawMarker(1, center.x, center.y, center.z - 1.0,
|
|
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
|
2.0, 2.0, 0.6, 255, 255, 255, 80,
|
|
false, true, 2, false, nil, nil, false)
|
|
|
|
if d < 25.0 then
|
|
local prog = tonumber(t.progress) or 0
|
|
local cont = tonumber(t.contestingGang) or 0
|
|
|
|
local ownerName = (Config.GANGS[owner] and Config.GANGS[owner].name) or "Neutral"
|
|
local text = ("%s\nOwner: %s"):format(t.name or turfId, ownerName)
|
|
|
|
if cont ~= 0 then
|
|
local contName = (Config.GANGS[cont] and Config.GANGS[cont].name) or ("Gang " .. cont)
|
|
text = text .. ("\nContesting: %s (%d/%d)"):format(contName, prog, SecondsToCapture)
|
|
end
|
|
|
|
Draw3DText(center.x, center.y, center.z + 1.2, text)
|
|
end
|
|
end
|
|
|
|
::continue_turf::
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
end)
|
|
|
|
--========================================================
|
|
-- UI Cash update
|
|
--========================================================
|
|
local function SetPauseMenuMoney(cash, bank)
|
|
cash = math.floor(tonumber(cash) or 0)
|
|
bank = math.floor(tonumber(bank) or 0)
|
|
|
|
StatSetInt(`MP0_WALLET_BALANCE`, cash, true)
|
|
StatSetInt(`MP1_WALLET_BALANCE`, cash, true)
|
|
|
|
StatSetInt(`MP0_BANK_BALANCE`, bank, true)
|
|
StatSetInt(`MP1_BANK_BALANCE`, bank, true)
|
|
|
|
StatSetInt(`BANK_BALANCE`, bank, true)
|
|
end
|
|
|
|
RegisterNetEvent("turfwar:money:update", function(cash, bank)
|
|
SetPauseMenuMoney(cash, bank)
|
|
end)
|
|
|
|
CreateThread(function()
|
|
Wait(3000)
|
|
TriggerServerEvent("turfwar:money:request")
|
|
end)
|
|
|
|
--========================================================
|
|
-- Environment cash pickup spawn (client)
|
|
--========================================================
|
|
RegisterNetEvent("environment:spawnCashPickup", function(amount, coords)
|
|
local model = GetHashKey("prop_cash_pile_01")
|
|
RequestModel(model)
|
|
while not HasModelLoaded(model) do Wait(0) end
|
|
|
|
CreateObject(model, coords.x, coords.y, coords.z - 0.9, true, true, false)
|
|
print(("[Cash Drop] $%s at %.2f %.2f %.2f"):format(amount, coords.x, coords.y, coords.z))
|
|
end)
|
|
|
|
--========================================================
|
|
-- Death watcher (requests HQ after respawn)
|
|
--========================================================
|
|
CreateThread(function()
|
|
local wasDead = false
|
|
while true do
|
|
Wait(200)
|
|
|
|
local ped = PlayerPedId()
|
|
if not DoesEntityExist(ped) then goto continue end
|
|
|
|
local dead = IsEntityDead(ped)
|
|
if dead and not wasDead then
|
|
CaptureHUD_Stop()
|
|
|
|
CreateThread(function()
|
|
if awaitingHQ then return end
|
|
awaitingHQ = true
|
|
|
|
while IsEntityDead(PlayerPedId()) do Wait(250) end
|
|
Wait(250)
|
|
|
|
TriggerServerEvent('turfwar:getMyHQSpawn')
|
|
end)
|
|
end
|
|
|
|
wasDead = dead
|
|
::continue::
|
|
end
|
|
end)
|