From 4910c39102d11942347f9813b192ddcf12158106 Mon Sep 17 00:00:00 2001 From: tanthius Date: Thu, 12 Feb 2026 04:15:37 +0000 Subject: [PATCH] Upload files to "client" --- client/guards.lua | 558 +++++++++++++++++++ client/killfeed_chat.lua | 160 ++++++ client/leaderboard.lua | 108 ++++ client/loadouts.lua | 28 + client/main.lua | 1088 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 1942 insertions(+) create mode 100644 client/guards.lua create mode 100644 client/killfeed_chat.lua create mode 100644 client/leaderboard.lua create mode 100644 client/loadouts.lua create mode 100644 client/main.lua diff --git a/client/guards.lua b/client/guards.lua new file mode 100644 index 0000000..f41eddf --- /dev/null +++ b/client/guards.lua @@ -0,0 +1,558 @@ +-- client/guards.lua +print("^2[turfwar]^7 CLIENT guards.lua LOADED (invincible guards + manual damage both ways)") + +local DEBUG = false +local function dbg(msg) if DEBUG then print(("^3[turfwar]^7 [guards] %s"):format(msg)) end end + +-- --------------------------------------------------------------------------- +-- Handshake + request gang snapshot +-- --------------------------------------------------------------------------- +CreateThread(function() + while not NetworkIsSessionStarted() do Wait(250) end + TriggerServerEvent("turfwar:guardsClientReady") + Wait(1500) + TriggerServerEvent("turfwar:requestAllPlayerGangs") +end) + +-- --------------------------------------------------------------------------- +-- Local gang id (for colours only) +-- --------------------------------------------------------------------------- +local LocalGangId = 0 +RegisterNetEvent("turfwar:gangUpdate", function(g) LocalGangId = tonumber(g) or 0 end) +RegisterNetEvent("turfwar:setFaction", function(g) LocalGangId = tonumber(g) or LocalGangId or 0 end) + +-- --------------------------------------------------------------------------- +-- All players' gang cache (server must broadcast turfwar:playerGang) +-- --------------------------------------------------------------------------- +local PlayerGangByServerId = {} -- [serverId] = gangId +RegisterNetEvent("turfwar:playerGang", function(serverId, gangId) + serverId = tonumber(serverId) + gangId = tonumber(gangId) + if not serverId then return end + if not gangId or gangId < 0 then + PlayerGangByServerId[serverId] = nil + else + PlayerGangByServerId[serverId] = gangId + end +end) + +RegisterCommand("tw_gangcache", function() + print("^6[turfwar]^7 PlayerGangByServerId cache:") + for k,v in pairs(PlayerGangByServerId) do + print((" %s => %s"):format(k, v)) + end +end, false) + +-- --------------------------------------------------------------------------- +-- Relationship group (behaviour only; damage is manual) +-- --------------------------------------------------------------------------- +local GUARD_REL = nil +local REL_INIT = false + +local function ensureRel() + if REL_INIT then return end + REL_INIT = true + + GUARD_REL = AddRelationshipGroup("TW_GUARDS") + SetRelationshipBetweenGroups(0, GUARD_REL, GUARD_REL) + + -- neutral to players by default; we script combat + SetRelationshipBetweenGroups(3, GUARD_REL, `PLAYER`) + SetRelationshipBetweenGroups(3, `PLAYER`, GUARD_REL) + + local neutralGroups = { + `AMBIENT_GANG_LOST`, + `AMBIENT_GANG_BALLAS`, + `AMBIENT_GANG_FAMILY`, + `AMBIENT_GANG_MEXICAN`, + `AMBIENT_GANG_SALVA`, + `AMBIENT_GANG_WEICHENG`, + `COP`, + `SECURITY_GUARD`, + } + for _, grp in ipairs(neutralGroups) do + SetRelationshipBetweenGroups(3, GUARD_REL, grp) + SetRelationshipBetweenGroups(3, grp, GUARD_REL) + end +end + +-- --------------------------------------------------------------------------- +-- UI config +-- --------------------------------------------------------------------------- +local MARKER_MAX_DIST = 70.0 +local BLIP_SPRITE = 270 +local BLIP_SCALE = 0.65 +local ENEMY_BLIP_COLOUR = 1 +local FRIENDLY_ALPHA = 160 + +local FACTION_BLIP_COLOUR = { [1]=5,[2]=2,[3]=3,[4]=40,[5]=7 } +local FACTION_RGB = { + [1]={r=255,g=220,b=0}, + [2]={r=0,g=200,b=0}, + [3]={r=0,g=130,b=255}, + [4]={r=60,g=60,b=60}, + [5]={r=160,g=80,b=255} +} +local ENEMY_RGB = { r=255, g=0, b=0 } + +local function isFriendly(ownerFaction) + return (LocalGangId ~= 0 and ownerFaction ~= 0 and LocalGangId == ownerFaction) +end + +local function getColours(ownerFaction) + if isFriendly(ownerFaction) then + return (FACTION_RGB[ownerFaction] or {r=255,g=255,b=255}), (FACTION_BLIP_COLOUR[ownerFaction] or 0) + end + return ENEMY_RGB, ENEMY_BLIP_COLOUR +end + +-- --------------------------------------------------------------------------- +-- State +-- --------------------------------------------------------------------------- +local GuardState = {} -- [turfId] = { ownerFaction, peds, blips, spawnPoints } +local GuardMeta = {} -- [ped] = { turfId, ownerFaction, spawn=vector3(...) } + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- +local function loadModel(model) + local h = GetHashKey(model) + if not IsModelInCdimage(h) then return nil end + RequestModel(h) + local timeout = GetGameTimer() + 5000 + while not HasModelLoaded(h) do + Wait(10) + if GetGameTimer() > timeout then return nil end + end + return h +end + +local function delPed(p) + if p and DoesEntityExist(p) then + GuardMeta[p] = nil + SetEntityAsMissionEntity(p, true, true) + DeleteEntity(p) + end +end + +local function delBlip(b) + if b and DoesBlipExist(b) then RemoveBlip(b) end +end + +local function clearTurf(turfId) + local st = GuardState[turfId] + if not st then return end + for _, p in pairs(st.peds or {}) do delPed(p) end + for _, b in pairs(st.blips or {}) do delBlip(b) end + GuardState[turfId] = nil +end + +local function makeBlip(ped, ownerFaction) + local blip = AddBlipForEntity(ped) + SetBlipSprite(blip, BLIP_SPRITE) + SetBlipScale(blip, BLIP_SCALE) + SetBlipAsShortRange(blip, true) + local _, col = getColours(ownerFaction) + SetBlipColour(blip, col) + BeginTextCommandSetBlipName("STRING") + AddTextComponentString("Guard") + EndTextCommandSetBlipName(blip) + return blip +end + +local function isGuardPed(ped) + return ped and ped ~= 0 and GuardMeta[ped] ~= nil +end + +local function standAt(ped, pos) + ClearPedTasks(ped) + TaskStandGuard(ped, pos.x, pos.y, pos.z, 0.0, "WORLD_HUMAN_GUARD_STAND", 0) +end + +local function configureGuard(ped) + ensureRel() + + SetEntityAsMissionEntity(ped, true, true) + SetPedRelationshipGroupHash(ped, GUARD_REL) + + SetEntityMaxHealth(ped, 220) + SetEntityHealth(ped, 220) + SetPedArmour(ped, 75) + SetPedAccuracy(ped, 55) + + SetPedAlertness(ped, 1) + SetPedSeeingRange(ped, 120.0) + SetPedHearingRange(ped, 120.0) + + SetPedCombatAbility(ped, 2) + SetPedCombatMovement(ped, 2) + SetPedCombatRange(ped, 2) + SetPedCombatAttributes(ped, 46, true) + SetPedDropsWeaponsWhenDead(ped, false) + + SetCanAttackFriendly(ped, false, false) + SetBlockingOfNonTemporaryEvents(ped, true) + SetPedKeepTask(ped, true) + SetPedFleeAttributes(ped, 0, false) + SetPedSuffersCriticalHits(ped, false) + SetPedShootRate(ped, 450) + + -- KEY: they never take native damage (prevents guard→guard deaths / weird ownership) + SetEntityInvincible(ped, true) +end + +local function applyNoCollisionAmongGuards(st) + local peds = st.peds or {} + for i = 1, #peds do + local a = peds[i] + if a and DoesEntityExist(a) then + for j = i + 1, #peds do + local b = peds[j] + if b and DoesEntityExist(b) then + SetEntityNoCollisionEntity(a, b, true) + SetEntityNoCollisionEntity(b, a, true) + end + end + end + end +end + +-- --------------------------------------------------------------------------- +-- Nearest enemy player +-- --------------------------------------------------------------------------- +local AGGRO_RANGE = 90.0 +local function findNearestEnemyPlayerPed(ownerFaction, fromPos) + local bestPed, bestDist = nil, AGGRO_RANGE + 0.01 + + for _, pid in ipairs(GetActivePlayers()) do + local spid = GetPlayerServerId(pid) + local gang = PlayerGangByServerId[spid] + + if gang and gang ~= 0 and gang ~= ownerFaction then + local ped = GetPlayerPed(pid) + if ped and ped ~= 0 and DoesEntityExist(ped) and not IsEntityDead(ped) then + local d = #(GetEntityCoords(ped) - fromPos) + if d < bestDist then + bestDist = d + bestPed = ped + end + end + end + end + + return bestPed, bestDist +end + +-- --------------------------------------------------------------------------- +-- Spawn/clear events (host only) +-- --------------------------------------------------------------------------- +RegisterNetEvent("turfwar:spawnGuards", function(payload) + if type(payload) ~= "table" then return end + + local turfId = tostring(payload.turfId) + local ownerFaction = tonumber(payload.ownerFaction) or 0 + local count = tonumber(payload.count) or 0 + local spawns = payload.spawns or {} + + if ownerFaction == 0 or count <= 0 then + clearTurf(turfId) + return + end + + clearTurf(turfId) + + GuardState[turfId] = { + ownerFaction = ownerFaction, + peds = {}, + blips = {}, + spawnPoints = spawns + } + + local model = loadModel(payload.model or "g_m_y_lost_01") + if not model then + print("^1[turfwar]^7 Guard model failed to load") + return + end + + for i = 1, count do + local pos = spawns[i] + if not pos then break end + + local ped = CreatePed(4, model, pos.x, pos.y, pos.z, 0.0, true, true) + if ped and DoesEntityExist(ped) then + configureGuard(ped) + + local weapon = payload.weapon or "WEAPON_PISTOL" + GiveWeaponToPed(ped, GetHashKey(weapon), 250, false, true) + SetCurrentPedWeapon(ped, GetHashKey(weapon), true) + + standAt(ped, pos) + + GuardState[turfId].peds[i] = ped + GuardState[turfId].blips[i] = makeBlip(ped, ownerFaction) + + GuardMeta[ped] = { + turfId = turfId, + ownerFaction = ownerFaction, + spawn = vector3(pos.x, pos.y, pos.z) + } + + if not NetworkHasControlOfEntity(ped) then + NetworkRequestControlOfEntity(ped) + end + end + end + + SetModelAsNoLongerNeeded(model) + applyNoCollisionAmongGuards(GuardState[turfId]) +end) + +RegisterNetEvent("turfwar:clearGuards", function(turfId) + clearTurf(tostring(turfId)) +end) + +-- --------------------------------------------------------------------------- +-- Manual damage BOTH ways +-- - Enemy player -> guard +-- - Guard -> enemy player +-- Notes: +-- - We DO NOT trust weapon hash indexes across builds; use fixed damage values. +-- --------------------------------------------------------------------------- +local GUARD_HIT_DMG_TO_PLAYER = 18 -- per hit (tune) +local PLAYER_HIT_DMG_TO_GUARD = 25 -- per hit (tune) + +-- per-attacker throttle (prevents one bullet generating multiple events) +local lastHitTick = {} -- [attackerNetId .. ":" .. victimNetId] = gameTimer + +local function shouldThrottle(attacker, victim) + local a = tostring(attacker or 0) + local v = tostring(victim or 0) + local key = a .. ":" .. v + local now = GetGameTimer() + local last = lastHitTick[key] or 0 + if now - last < 90 then return true end -- ~11 hits/sec max + lastHitTick[key] = now + return false +end + +local function gangFromPlayerPed(attackerPed) + if not attackerPed or attackerPed == 0 or not IsPedAPlayer(attackerPed) then return nil end + local playerIndex = NetworkGetPlayerIndexFromPed(attackerPed) + if not playerIndex or playerIndex == -1 then return nil end + local serverId = GetPlayerServerId(playerIndex) + if not serverId then return nil end + return PlayerGangByServerId[serverId] +end + +local function applyDamageToPed(victimPed, amount) + if not victimPed or victimPed == 0 or not DoesEntityExist(victimPed) then return end + if amount <= 0 then return end + + local hp = GetEntityHealth(victimPed) + local newHp = hp - amount + + if newHp <= 0 then + -- kill + SetEntityHealth(victimPed, 0) + else + SetEntityHealth(victimPed, newHp) + end +end + +AddEventHandler("gameEventTriggered", function(name, args) + if name ~= "CEventNetworkEntityDamage" then return end + + local victim = args[1] + local attacker = args[2] + + if not victim or victim == 0 or not DoesEntityExist(victim) then return end + if not attacker or attacker == 0 or not DoesEntityExist(attacker) then return end + if shouldThrottle(attacker, victim) then return end + + local victimIsGuard = isGuardPed(victim) + local attackerIsGuard = isGuardPed(attacker) + local attackerIsPlayer = IsPedAPlayer(attacker) + + -- -------------------------------------------------------- + -- Guard vs Guard: ignore + calm + -- -------------------------------------------------------- + if victimIsGuard and attackerIsGuard then + ClearEntityLastDamageEntity(victim) + local meta = GuardMeta[victim] + if meta and meta.spawn then + ClearPedTasksImmediately(victim) + standAt(victim, meta.spawn) + end + return + end + + -- -------------------------------------------------------- + -- Player -> Guard (manual damage if ENEMY) + -- -------------------------------------------------------- + if victimIsGuard and attackerIsPlayer then + ClearEntityLastDamageEntity(victim) + + local meta = GuardMeta[victim] + if not meta then return end + + local attackerGang = gangFromPlayerPed(attacker) + if not attackerGang or attackerGang == 0 then + -- if your gang cache isn’t populated, guards will appear immune. + -- run /tw_gangcache on host to confirm. + return + end + + if attackerGang == meta.ownerFaction then + -- friendly player: no damage + return + end + + -- manual damage: temporarily allow kill by turning off invincibility only at death + local hp = GetEntityHealth(victim) + local newHp = hp - PLAYER_HIT_DMG_TO_GUARD + if newHp <= 0 then + SetEntityInvincible(victim, false) + SetEntityHealth(victim, 0) + else + SetEntityHealth(victim, newHp) + end + return + end + + -- -------------------------------------------------------- + -- Guard -> Player (manual damage if ENEMY) + -- -------------------------------------------------------- + if attackerIsGuard and IsPedAPlayer(victim) then + -- Determine which turf/owner this guard belongs to + local meta = GuardMeta[attacker] + if not meta then return end + + local victimGang = gangFromPlayerPed(victim) + if not victimGang or victimGang == 0 then + -- neutral players don't get shot by guards (tune if you want) + return + end + + if victimGang == meta.ownerFaction then + -- friendly player + return + end + + applyDamageToPed(victim, GUARD_HIT_DMG_TO_PLAYER) + return + end +end) + +-- --------------------------------------------------------------------------- +-- Combat gate: +-- If enemy exists nearby -> fight that enemy +-- Else -> stand down (stops spraying) +-- --------------------------------------------------------------------------- +CreateThread(function() + while true do + Wait(300) + + for _, st in pairs(GuardState) do + local owner = tonumber(st.ownerFaction) or 0 + local spawns = st.spawnPoints or {} + + for idx, g in pairs(st.peds or {}) do + if g and DoesEntityExist(g) and not IsEntityDead(g) and NetworkHasControlOfEntity(g) then + local gPos = GetEntityCoords(g) + local enemyPed = nil + + if owner ~= 0 then + enemyPed = select(1, findNearestEnemyPlayerPed(owner, gPos)) + end + + if enemyPed then + TaskCombatPed(g, enemyPed, 0, 16) + else + if IsPedInCombat(g, 0) or IsPedShooting(g) then + local pos = spawns[idx] and vector3(spawns[idx].x, spawns[idx].y, spawns[idx].z) or gPos + ClearPedTasksImmediately(g) + standAt(g, pos) + end + end + end + end + end + end +end) + +-- --------------------------------------------------------------------------- +-- Marker + blip colour update +-- --------------------------------------------------------------------------- +CreateThread(function() + while true do + Wait(0) + + local me = PlayerPedId() + if not me or me == 0 then goto cont end + local myC = GetEntityCoords(me) + + for _, st in pairs(GuardState) do + local rgb, col = getColours(st.ownerFaction) + for idx, ped in pairs(st.peds or {}) do + if ped and DoesEntityExist(ped) and not IsEntityDead(ped) then + local p = GetEntityCoords(ped) + local d = #(myC - p) + + if d <= MARKER_MAX_DIST then + DrawMarker( + 2, p.x, p.y, p.z + 1.1, + 0.0,0.0,0.0, + 0.0,0.0,0.0, + 0.25,0.25,0.25, + rgb.r,rgb.g,rgb.b, FRIENDLY_ALPHA, + false,true,2,false,nil,nil,false + ) + end + + local b = st.blips and st.blips[idx] + if b and DoesBlipExist(b) then + SetBlipColour(b, col) + end + end + end + end + + ::cont:: + end +end) + +-- --------------------------------------------------------------------------- +-- Cleanup: delete dead guards, notify server when turf empty +-- --------------------------------------------------------------------------- +CreateThread(function() + while true do + Wait(800) + + for turfId, st in pairs(GuardState) do + local alive = 0 + + for idx = #st.peds, 1, -1 do + local ped = st.peds[idx] + local blip = st.blips and st.blips[idx] + + local deadOrGone = (not ped) or (not DoesEntityExist(ped)) or IsEntityDead(ped) + + if deadOrGone then + if blip then delBlip(blip) end + if ped and DoesEntityExist(ped) then delPed(ped) end + table.remove(st.peds, idx) + if st.blips then table.remove(st.blips, idx) end + else + alive = alive + 1 + -- keep invincible true (prevents guard deaths from guard shots) + SetEntityInvincible(ped, true) + end + end + + if alive == 0 then + TriggerServerEvent("turfwar:guardsEmpty", turfId) + GuardState[turfId] = nil + end + end + end +end) diff --git a/client/killfeed_chat.lua b/client/killfeed_chat.lua new file mode 100644 index 0000000..e91c57e --- /dev/null +++ b/client/killfeed_chat.lua @@ -0,0 +1,160 @@ +print("^2[turfwar]^7 killfeed_chat.lua loaded (client)") + +local PlayerGang = {} -- [serverId] = gangId + +RegisterNetEvent('turfwar:playerGang', function(serverId, gangId) + PlayerGang[tonumber(serverId)] = tonumber(gangId) or 0 +end) + +-- Optional snapshot +CreateThread(function() + Wait(1500) + TriggerServerEvent('turfwar:requestAllPlayerGangs') +end) + +RegisterNetEvent('turfwar:allPlayerGangs', function(map) + if type(map) ~= "table" then return end + for sid, gid in pairs(map) do + PlayerGang[tonumber(sid)] = tonumber(gid) or 0 + end +end) + +local function weaponLabelFromHash(hash) + local t = { + [`WEAPON_UNARMED`] = "Fists", + [`WEAPON_KNIFE`] = "Knife", + [`WEAPON_PISTOL`] = "Pistol", + [`WEAPON_COMBATPISTOL`] = "Combat Pistol", + [`WEAPON_APPISTOL`] = "AP Pistol", + [`WEAPON_SMG`] = "SMG", + [`WEAPON_ASSAULTRIFLE`] = "Assault Rifle", + [`WEAPON_CARBINERIFLE`] = "Carbine Rifle", + [`WEAPON_PUMPSHOTGUN`] = "Pump Shotgun", + [`WEAPON_SNIPERRIFLE`] = "Sniper Rifle", + [`WEAPON_HEAVYSNIPER`] = "Heavy Sniper", + [`WEAPON_GRENADE`] = "Grenade", + [`WEAPON_EXPLOSION`] = "Explosion", + [`WEAPON_RUN_OVER_BY_CAR`] = "Vehicle", + } + if t[hash] then return t[hash] end + + local disp = GetWeaponDisplayNameFromHash(hash) + if disp and disp ~= "" then + local label = GetLabelText(disp) + if label and label ~= "NULL" then return label end + end + + return tostring(hash) +end + +local function gangRgbTable(gangId) + gangId = tonumber(gangId) or 0 + local g = (Config and Config.GANGS and Config.GANGS[gangId]) or nil + if g and type(g.rgb) == "table" then + local r = tonumber(g.rgb[1]) or 255 + local gg = tonumber(g.rgb[2]) or 255 + local b = tonumber(g.rgb[3]) or 255 + return { r, gg, b } + end + return {255,255,255} +end + +-- ========================================================= +-- Victim reports once (PvP + non-player deaths) +-- ========================================================= +local lastSentAt = 0 +CreateThread(function() + while true do + Wait(0) + + local ped = PlayerPedId() + if ped ~= 0 and IsEntityDead(ped) then + local now = GetGameTimer() + if now - lastSentAt > 2000 then + lastSentAt = now + + local killerPed = GetPedSourceOfDeath(ped) + local weaponHash = GetPedCauseOfDeath(ped) + local victimServerId = GetPlayerServerId(PlayerId()) + + local reported = false + + -- PvP killer? + if killerPed and killerPed ~= 0 and IsEntityAPed(killerPed) and IsPedAPlayer(killerPed) then + local killerPlayer = NetworkGetPlayerIndexFromPed(killerPed) + local killerServerId = killerPlayer and GetPlayerServerId(killerPlayer) or 0 + + if killerServerId and killerServerId > 0 then + local myCoords = GetEntityCoords(ped) + local kCoords = GetEntityCoords(killerPed) + local dist = #(myCoords - kCoords) + + local ok, bone = GetPedLastDamageBone(ped) + local headshot = (ok and bone == 31086) + + TriggerServerEvent('turfwar:killfeed:report', { + killer = killerServerId, + victim = victimServerId, + weapon = weaponHash, + headshot = headshot, + distance = dist, + killerGang = PlayerGang[killerServerId] or 0, + victimGang = PlayerGang[victimServerId] or 0, + }) + + reported = true + end + end + + -- Non-player death fallback + if not reported then + TriggerServerEvent('turfwar:killfeed:report', { + killer = 0, + victim = victimServerId, + weapon = weaponHash or 0, + headshot = false, + distance = 0, + killerGang = 0, + victimGang = PlayerGang[victimServerId] or 0, + }) + end + end + + while IsEntityDead(PlayerPedId()) do Wait(200) end + Wait(500) + end + end +end) + +-- ========================================================= +-- Receive broadcast -> send to NUI +-- ========================================================= +RegisterNetEvent('turfwar:killfeed:chat', function(data) + if type(data) ~= "table" then return end + + local victimRgb = gangRgbTable(data.victimGang) + local killerRgb = gangRgbTable(data.killerGang) + + local deathOnly = (data.isDeathOnly == true) or (tonumber(data.killer or 0) <= 0) + + if deathOnly then + SendNUIMessage({ + type = "killfeed:add", + isDeathOnly = true, + victimName = data.victimName or "Unknown", + victimRgb = victimRgb, + }) + return + end + + SendNUIMessage({ + type = "killfeed:add", + killerName = data.killerName or "Unknown", + victimName = data.victimName or "Unknown", + weapon = weaponLabelFromHash(tonumber(data.weapon or 0) or 0), + headshot = (data.headshot == true), + distance = tonumber(data.distance or 0) or 0, + killerRgb = killerRgb, + victimRgb = victimRgb, + }) +end) diff --git a/client/leaderboard.lua b/client/leaderboard.lua new file mode 100644 index 0000000..e18463b --- /dev/null +++ b/client/leaderboard.lua @@ -0,0 +1,108 @@ +print("^2[turfwar]^7 leaderboard client loaded") + +local leaderboard = {} + +-- Map FiveM blipColor IDs -> RGB (approx). +-- We only need the ones you use: 0,2,3,5,7,40. +local BLIP_RGB = { + [0] = {255, 255, 255}, -- White + [2] = { 60, 200, 60}, -- Green + [3] = { 70, 120, 255}, -- Blue + [5] = {255, 220, 60}, -- Yellow + [7] = {190, 90, 255}, -- Purple + [40] = { 40, 40, 40}, -- Dark grey / black +} + +local function getGangName(gangId) + if Config and Config.GANGS and Config.GANGS[gangId] and Config.GANGS[gangId].name then + return Config.GANGS[gangId].name + end + return ("Gang %s"):format(gangId) +end + +local function getGangRGB(gangId) + local blip = 0 + if Config and Config.GANGS and Config.GANGS[gangId] and Config.GANGS[gangId].blipColor then + blip = tonumber(Config.GANGS[gangId].blipColor) or 0 + end + 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() + SetTextWrap(0.0, 1.0) + BeginTextCommandDisplayText("STRING") + AddTextComponentSubstringPlayerName(text) + EndTextCommandDisplayText(x, y) +end + +-- Ask server for current leaderboard on join/resource start +CreateThread(function() + Wait(1500) + TriggerServerEvent("turfwar:requestGangLeaderboard") +end) + +RegisterNetEvent("turfwar:gangLeaderboard", function(rankedGangIds) + leaderboard = rankedGangIds or {} + -- debug: + -- print("^2[turfwar]^7 leaderboard: " .. json.encode(leaderboard)) +end) + +-- Compact HUD render (smaller box, slightly bigger text) +CreateThread(function() + while true do + Wait(0) + + if not leaderboard or #leaderboard == 0 then + goto continue + end + + local maxRows = math.min(#leaderboard, 8) -- fewer rows keeps it compact + + -- Layout (top-left) + local x = 0.018 + local y = 0.185 + + -- Slightly bigger than before (but still realistic) + local titleScale = 0.32 + local rowScale = 0.28 + + -- Smaller/tighter panel + local rowH = 0.018 + local padX = 0.008 + local padY = 0.008 + + local panelW = 0.14 -- ~⅓ smaller than before (was 0.22) + local panelH = padY + 0.020 + (maxRows * rowH) + padY + + -- Background panel (more compact) + DrawRect(x + panelW/2, y + panelH/2, panelW, panelH, 0, 0, 0, 105) + + -- Title + drawText(x + padX, y + padY, "Top Ranked Gang", 255, 255, 255, 235, titleScale) + + -- Rows + local startY = y + padY + 0.020 + for i = 1, maxRows do + local gangId = leaderboard[i] + local name = getGangName(gangId) + local r,g,b = table.unpack(getGangRGB(gangId)) + + local rowY = startY + ((i-1) * rowH) + + -- very faint row highlight + DrawRect(x + panelW/2, rowY + 0.009, panelW, rowH, 255, 255, 255, 8) + + -- rank number (lighter grey / near-white, with outline) + drawText(x + padX, rowY, ("%d."):format(i), 235, 235, 235, 235, rowScale) + + -- gang name (colored) + drawText(x + padX + 0.020, rowY, name, r, g, b, 245, rowScale) + end + + ::continue:: + end +end) diff --git a/client/loadouts.lua b/client/loadouts.lua new file mode 100644 index 0000000..5d40a56 --- /dev/null +++ b/client/loadouts.lua @@ -0,0 +1,28 @@ +-- client/loadouts.lua (STRIP-ONLY for neutrals) +print("^2[turfwar]^7 client/loadouts.lua LOADED (STRIP-ONLY - shop handles weapons)") + +local currentGang = 0 + +RegisterNetEvent("turfwar:setFaction", function(gangId) + currentGang = tonumber(gangId) or 0 +end) +RegisterNetEvent("turfwar:gangUpdate", function(gangId) + currentGang = tonumber(gangId) or 0 +end) +RegisterNetEvent("turfwar:setMyGang", function(gangId) + currentGang = tonumber(gangId) or 0 +end) + +RegisterNetEvent("turfwar:stripWeapons", function() + if currentGang ~= 0 then + -- only strip neutral + return + end + local ped = PlayerPedId() + if not ped or ped == 0 then return end + RemoveAllPedWeapons(ped, true) +end) + +RegisterNetEvent("turfwar:applyLoadout", function(loadout) + -- disabled: shop owns weapons +end) diff --git a/client/main.lua b/client/main.lua new file mode 100644 index 0000000..6ef76bf --- /dev/null +++ b/client/main.lua @@ -0,0 +1,1088 @@ +-- 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)