Turfwar/client/main.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)