diff --git a/client/environment.lua b/client/environment.lua new file mode 100644 index 0000000..478d2c6 --- /dev/null +++ b/client/environment.lua @@ -0,0 +1,202 @@ +print("^2[environment]^7 Cash drop system loaded") + +-- ===================== +-- CONFIG +-- ===================== +local MAX_CASH = 200 -- Maximum possible drop +local WEIGHT_POWER = 2.2 -- Higher = lower values more common +local CHECK_INTERVAL = 1000 -- How often we scan peds (ms) + +local PICKUP_RADIUS = 1.25 +local PICKUP_CHECK_MS = 200 +local PICKUP_COOLDOWN = 750 -- ms between pickup attempts per drop + +-- Prevent duplicate drops per ped handle +local processedPeds = {} + +-- dropId -> { obj=entity, coords=vector3, amount=number } +local spawnedDrops = {} +local pickupCooldown = {} -- dropId -> lastAttempt GetGameTimer() + +-- ===================== +-- WEIGHTED RANDOM CASH +-- ===================== +local function getWeightedCash() + local roll = math.random() + local weighted = roll ^ WEIGHT_POWER + return math.floor(weighted * MAX_CASH) +end + +-- ===================== +-- PED DEATH MONITOR +-- ===================== +CreateThread(function() + math.randomseed(GetGameTimer()) + + while true do + Wait(CHECK_INTERVAL) + + local peds = GetGamePool("CPed") + for _, ped in ipairs(peds) do + if DoesEntityExist(ped) + and not IsPedAPlayer(ped) + and IsEntityDead(ped) + and not processedPeds[ped] then + + processedPeds[ped] = true + + local cash = getWeightedCash() + if cash > 0 then + local c = GetEntityCoords(ped) + TriggerServerEvent("environment:pedCashDrop", cash, { x = c.x, y = c.y, z = c.z }) + end + end + end + end +end) + +-- ===================== +-- ARG PARSER (server compatibility) +-- ===================== +local function parseSpawnArgs(p1, p2, p3, p4, p5) + -- Preferred (your server currently uses): + -- (dropId, amount, coordsTable) + -- Also supports: + -- (dropId, amount, x, y, z) + -- (amount, coordsTable) [fallback id] + -- (amount, x, y, z) [fallback id] + + local dropId, amount, coords + + -- (dropId, amount, coordsTable) OR (dropId, amount, x, y, z) + if type(p1) == "number" and (type(p2) == "number" or type(p2) == "string") then + local maybeDropId = tonumber(p1) + local maybeAmount = tonumber(p2) or 0 + + if type(p3) == "table" and p3.x and p3.y and p3.z then + dropId = maybeDropId + amount = maybeAmount + coords = vector3(p3.x, p3.y, p3.z) + return dropId, amount, coords + elseif type(p3) == "number" and type(p4) == "number" and type(p5) == "number" then + dropId = maybeDropId + amount = maybeAmount + coords = vector3(p3, p4, p5) + return dropId, amount, coords + end + end + + -- (amount, coordsTable) OR (amount, x, y, z) -> local fallback id + local maybeAmount = tonumber(p1) or 0 + if type(p2) == "table" and p2.x and p2.y and p2.z then + dropId = GetGameTimer() + math.random(1, 999999) + amount = maybeAmount + coords = vector3(p2.x, p2.y, p2.z) + return dropId, amount, coords + elseif type(p2) == "number" and type(p3) == "number" and type(p4) == "number" then + dropId = GetGameTimer() + math.random(1, 999999) + amount = maybeAmount + coords = vector3(p2, p3, p4) + return dropId, amount, coords + end + + return nil, nil, nil +end + +-- ===================== +-- CASH PICKUP SPAWN +-- ===================== +RegisterNetEvent("environment:spawnCashPickup", function(p1, p2, p3, p4, p5) + local dropId, amount, coords = parseSpawnArgs(p1, p2, p3, p4, p5) + + if not dropId or not coords then + print(("^1[environment]^7 spawnCashPickup: bad args types: %s %s %s %s %s") + :format(type(p1), type(p2), type(p3), type(p4), type(p5))) + return + end + + amount = tonumber(amount) or 0 + if amount <= 0 then return end + + -- If already exists, delete old one first + local existing = spawnedDrops[dropId] + if existing and existing.obj and DoesEntityExist(existing.obj) then + DeleteEntity(existing.obj) + end + + local model = GetHashKey("prop_cash_pile_01") + RequestModel(model) + while not HasModelLoaded(model) do Wait(0) end + + local obj = CreateObject(model, coords.x, coords.y, coords.z - 0.9, false, false, false) + PlaceObjectOnGroundProperly(obj) + FreezeEntityPosition(obj, true) + SetEntityAsMissionEntity(obj, true, true) + + spawnedDrops[dropId] = { obj = obj, coords = coords, amount = amount } + -- print(("[environment] Cash drop #%s: $%s at %.2f %.2f %.2f"):format(dropId, amount, coords.x, coords.y, coords.z)) +end) + +-- ===================== +-- WALK-OVER COLLECT LOOP +-- ===================== +CreateThread(function() + while true do + Wait(PICKUP_CHECK_MS) + + local ped = PlayerPedId() + if not ped or ped == 0 then goto continue end + if IsEntityDead(ped) then goto continue end + + local pcoords = GetEntityCoords(ped) + local now = GetGameTimer() + + for dropId, data in pairs(spawnedDrops) do + if data and data.coords and data.obj and DoesEntityExist(data.obj) then + local dist = #(pcoords - data.coords) + if dist <= PICKUP_RADIUS then + local last = pickupCooldown[dropId] or 0 + if (now - last) > PICKUP_COOLDOWN then + pickupCooldown[dropId] = now + TriggerServerEvent("environment:pickupCash", dropId) + end + end + end + end + + ::continue:: + end +end) + +-- ===================== +-- FEEDBACK +-- ===================== +RegisterNetEvent("environment:pickupFeedback", function(amount) + amount = tonumber(amount) or 0 + if amount <= 0 then return end + + PlaySoundFrontend(-1, "PICK_UP", "HUD_FRONTEND_DEFAULT_SOUNDSET", true) + + BeginTextCommandThefeedPost("STRING") + AddTextComponentSubstringPlayerName(("+~g~$%d~s~"):format(amount)) + EndTextCommandThefeedPostTicker(false, true) +end) + +-- ===================== +-- REMOVE / DESPAWN +-- ===================== +RegisterNetEvent("environment:removeCashPickup", function(dropId) + dropId = tonumber(dropId) + if not dropId then return end + + local data = spawnedDrops[dropId] + if data and data.obj and DoesEntityExist(data.obj) then + -- Helpful in some cases where entity control is finicky + NetworkRequestControlOfEntity(data.obj) + SetEntityAsMissionEntity(data.obj, true, true) + DeleteEntity(data.obj) + end + + spawnedDrops[dropId] = nil + pickupCooldown[dropId] = nil +end) diff --git a/client/finance.lua b/client/finance.lua new file mode 100644 index 0000000..d8310c0 --- /dev/null +++ b/client/finance.lua @@ -0,0 +1,132 @@ +-- client/finance.lua +print("^2[turfwar]^7 finance client loaded") + +if IsDuplicityVersion() then return end + +Config = Config or {} +Config.Finance = Config.Finance or {} + +local uiOpen = false +local atmToken = nil + +local lastUse = 0 +local USE_COOLDOWN = 800 + +local function CanUse() + local now = GetGameTimer() + if (now - lastUse) < USE_COOLDOWN then return false end + lastUse = now + return true +end + +local function Notify(msg) + BeginTextCommandThefeedPost("STRING") + AddTextComponentSubstringPlayerName(msg) + EndTextCommandThefeedPostTicker(false, false) +end + +local function Draw3DText(coords, text) + local onScreen, x, y = World3dToScreen2d(coords.x, coords.y, coords.z) + if not onScreen then return end + SetTextScale(0.35, 0.35) + SetTextFont(4) + SetTextCentre(true) + SetTextEntry("STRING") + AddTextComponentSubstringPlayerName(text) + DrawText(x, y) +end + +local function ForceCloseATMUI() + uiOpen = false + atmToken = nil + SetNuiFocus(false, false) + SendNUIMessage({ type = "atm:close" }) +end + +AddEventHandler("onClientResourceStart", function(res) + if res ~= GetCurrentResourceName() then return end + Wait(800) + ForceCloseATMUI() +end) + +AddEventHandler("playerSpawned", function() + Wait(800) + ForceCloseATMUI() +end) + +-- Server says OK -> open UI +RegisterNetEvent("turfwar:atm:openGranted", function(token) + atmToken = token + uiOpen = true + SetNuiFocus(true, true) + SendNUIMessage({ type = "atm:open" }) + TriggerServerEvent("turfwar:atm:balance", atmToken) +end) + +RegisterNetEvent("turfwar:atm:openDenied", function(reason) + print(("^1[turfwar]^7 ATM denied: %s"):format(tostring(reason))) + Notify(("~r~ATM denied~s~ (%s)"):format(tostring(reason))) +end) + +-- NUI callbacks +RegisterNUICallback("atm:close", function(_, cb) + ForceCloseATMUI() + cb(true) +end) + +RegisterNUICallback("atm:deposit", function(data, cb) + if not uiOpen or not atmToken then cb(false) return end + TriggerServerEvent("turfwar:bank:deposit", atmToken, data.amount) + cb(true) +end) + +RegisterNUICallback("atm:withdraw", function(data, cb) + if not uiOpen or not atmToken then cb(false) return end + TriggerServerEvent("turfwar:bank:withdraw", atmToken, data.amount) + cb(true) +end) + +-- Only update UI when it’s open +RegisterNetEvent("turfwar:money:update", function(cash, bank) + if not uiOpen then return end + SendNUIMessage({ type = "atm:balances", cash = cash, bank = bank }) +end) + +-- ATM Detection (CLIENT!) +CreateThread(function() + while true do + Wait(0) + + local ped = PlayerPedId() + if ped == 0 then Wait(500) goto continue end + + local pcoords = GetEntityCoords(ped) + local dist = Config.Finance.INTERACT_DIST or 1.5 + local key = Config.Finance.INTERACT_KEY or 38 + + local found = false + + for _, model in ipairs(Config.Finance.ATM_MODELS or {}) do + -- ✅ correct signature + local atm = GetClosestObjectOfType(pcoords.x, pcoords.y, pcoords.z, 1.6, model, false, false, false) + if atm and atm ~= 0 then + found = true + local acoords = GetEntityCoords(atm) + Draw3DText(vector3(acoords.x, acoords.y, acoords.z + 0.95), "~g~E~w~ Use ATM") + + if #(pcoords - acoords) <= dist then + if IsControlJustPressed(0, key) and CanUse() then + -- ✅ send payload server expects + TriggerServerEvent("turfwar:atm:open", { + x = acoords.x, y = acoords.y, z = acoords.z, + model = model + }) + end + end + end + end + + if not found then Wait(150) end + ::continue:: + end +end) diff --git a/client/gang_ff.lua b/client/gang_ff.lua new file mode 100644 index 0000000..d1a413d --- /dev/null +++ b/client/gang_ff.lua @@ -0,0 +1,73 @@ +-- server/gang_ff.lua +print("^2[turfwar]^7 server/gang_ff.lua loaded (server-side same-gang FF block)") + +-- Requires PlayerGang to be GLOBAL (see patch in server/main.lua) +PlayerGang = PlayerGang or {} + +local function gangOf(src) + return tonumber(PlayerGang[src]) or 0 +end + +local function sameGangNonNeutral(a, b) + local ga = gangOf(a) + local gb = gangOf(b) + return ga ~= 0 and ga == gb +end + +-- Helper: resolve victim player from weaponDamageEvent payload +local function resolveVictimPlayerFromWeaponData(data) + if not data then return nil end + + -- Different builds/resources may use different keys; try common ones + local netId = + data.hitGlobalId or + data.hitEntity or + data.hitNetId or + data.entity or + data.victimNetId + + if type(netId) ~= "number" then return nil end + + local ent = NetworkGetEntityFromNetworkId(netId) + if not ent or ent == 0 or not DoesEntityExist(ent) then return nil end + + if not IsEntityAPed(ent) then return nil end + if not IsPedAPlayer(ent) then return nil end + + -- For player peds, owner should be that player’s source + local victimSrc = NetworkGetEntityOwner(ent) + if not victimSrc or victimSrc == 0 then return nil end + + return victimSrc +end + +-- Blocks bullets/melee/etc +AddEventHandler("weaponDamageEvent", function(sender, data) + -- sender is attacker (source) + if not sender or sender == 0 then return end + + local victimSrc = resolveVictimPlayerFromWeaponData(data) + if not victimSrc then return end + + if sameGangNonNeutral(sender, victimSrc) then + CancelEvent() + -- Uncomment for debug: + -- print(("[turfwar_ff] BLOCK weaponDamageEvent: %s -> %s gang=%s"):format(sender, victimSrc, gangOf(sender))) + end +end) + +-- Blocks explosions (grenades, rockets, etc) +AddEventHandler("explosionEvent", function(sender, ev) + if not sender or sender == 0 then return end + if not ev then return end + + -- explosionEvent has a 'pos' and can have 'ownerNetId' etc; there isn't always a single victim. + -- But it can still stop friendly grenade spam within gang by blocking the explosion entirely. + -- If you don't use explosives, you can remove this handler. + local attackerGang = gangOf(sender) + if attackerGang == 0 then return end + + -- If you want to allow explosives vs other gangs but still protect same-gang, + -- you’d need per-victim explosion damage (not provided directly here). + -- So we leave explosives allowed by default. +end) diff --git a/client/gang_shops.lua b/client/gang_shops.lua new file mode 100644 index 0000000..6298169 --- /dev/null +++ b/client/gang_shops.lua @@ -0,0 +1,253 @@ +-- client/gang_shops.lua +print("^2[turfwar]^7 gang_shops.lua loaded (client)") + +Config = Config or {} +Config.Shops = Config.Shops or {} +Config.GANGS = Config.GANGS or {} + +local currentGang = 0 +local lastBal = 0 + +-- ========================= +-- 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 allowedGang() + if Config.Shops.AllowedGang then return Config.Shops.AllowedGang(currentGang) end + return currentGang ~= 0 and currentGang ~= 3 +end + +local function gangRGB(gangId) + local g = Config.GANGS and Config.GANGS[gangId] + if g and g.rgb and #g.rgb == 3 then + return g.rgb[1], g.rgb[2], g.rgb[3] + end + return 255, 255, 255 +end + +-- ========================= +-- Receive gang updates +-- Hook into your existing system. +-- ========================= +RegisterNetEvent("turfwar:myGang", function(gangId) + currentGang = tonumber(gangId) or 0 + TriggerServerEvent("turfwar:myGang:set", currentGang) -- keep server cache aligned +end) + +-- Fallback if your server broadcasts playerGang (you can remove if not needed) +RegisterNetEvent("turfwar:playerGang", function(src, gangId) + if src == GetPlayerServerId(PlayerId()) then + currentGang = tonumber(gangId) or 0 + TriggerServerEvent("turfwar:myGang:set", currentGang) + end +end) + +RegisterNetEvent("turfwar:shop:notify", function(msg) + Notify(msg) +end) + +RegisterNetEvent("turfwar:shop:balance", function(bal) + lastBal = tonumber(bal) or 0 +end) + +RegisterNetEvent("turfwar:gangbank:requestRefresh", function() + -- If you already have a gangbank refresh event, call it here. + -- Otherwise harmless. + TriggerServerEvent("turfwar:gangbank:request") +end) + +-- ========================= +-- Grant weapon/ammo (after server approves purchase) +-- ========================= +RegisterNetEvent("turfwar:shop:grantWeapon", function(item, gangId) + if not item then return end + local ped = PlayerPedId() + + if item.armor then + local amt = tonumber(item.armorAmount) or 50 + local cur = GetPedArmour(ped) + SetPedArmour(ped, math.min(100, cur + amt)) + return + end + + local wep = item.weapon + if not wep then return end + + local hash = GetHashKey(wep) + if not HasPedGotWeapon(ped, hash, false) then + GiveWeaponToPed(ped, hash, 0, false, true) + end + + local ammo = tonumber(item.ammo) or 0 + if ammo > 0 then + AddAmmoToPed(ped, hash, ammo) + end +end) + +RegisterNetEvent("turfwar:shop:grantAmmo", function(item) + if not item or not item.weapon then return end + local ped = PlayerPedId() + local hash = GetHashKey(item.weapon) + local ammo = tonumber(item.ammo) or 0 + if ammo > 0 then + AddAmmoToPed(ped, hash, ammo) + end +end) + +-- ========================= +-- Spawn purchased vehicle (after server approves) +-- ========================= +local function loadModel(model) + local hash = type(model) == "number" and model or GetHashKey(model) + if not IsModelInCdimage(hash) then return nil end + RequestModel(hash) + local timeout = GetGameTimer() + 5000 + while not HasModelLoaded(hash) do + Wait(10) + if GetGameTimer() > timeout then return nil end + end + return hash +end + +RegisterNetEvent("turfwar:shop:spawnPurchasedVehicle", function(item, gangId) + if not item or not item.model then return end + local ped = PlayerPedId() + local p = GetEntityCoords(ped) + local h = GetEntityHeading(ped) + + local hash = loadModel(item.model) + if not hash then + Notify("~r~Vehicle model failed to load.") + return + end + + -- Spawn in front of player + local forward = (Config.Shops.VehicleSpawn and Config.Shops.VehicleSpawn.forward) or 6.0 + local up = (Config.Shops.VehicleSpawn and Config.Shops.VehicleSpawn.up) or 0.2 + + local fw = GetEntityForwardVector(ped) + local spawn = vector3(p.x + fw.x * forward, p.y + fw.y * forward, p.z + up) + + local veh = CreateVehicle(hash, spawn.x, spawn.y, spawn.z, h, true, false) + SetModelAsNoLongerNeeded(hash) + + if veh and veh ~= 0 then + SetVehicleOnGroundProperly(veh) + + -- Gang color + local r,g,b = gangRGB(gangId) + SetVehicleCustomPrimaryColour(veh, r, g, b) + SetVehicleCustomSecondaryColour(veh, r, g, b) + + -- Nice defaults + SetVehicleDirtLevel(veh, 0.0) + SetVehicleEngineOn(veh, true, true, false) + + -- Warp player in + TaskWarpPedIntoVehicle(ped, veh, -1) + end +end) + +-- ========================= +-- “UI”: simple chat/menu commands (starter) +-- Later we can move to NUI +-- ========================= +local function printShopList(title, list) + TriggerServerEvent("turfwar:shop:getBalance") + Wait(150) + + print(("^3[turfwar]^7 === %s === (Gang Balance: %s)"):format(title, lastBal)) + for _, it in ipairs(list) do + print(("^3[turfwar]^7 - %s ^2$%d^7 (id: %s)"):format(it.label or it.id, it.price or 0, it.id)) + end +end + +RegisterCommand("gshop_weapons", function() + if not allowedGang() then Notify("~r~Gang shop not available for your faction.") return end + printShopList("WEAPON SHOP", Config.Shops.WeaponList or {}) + Notify("Open console (F8) to see weapon list. Buy: /gshop_buyweapon ") +end) + +RegisterCommand("gshop_ammo", function() + if not allowedGang() then Notify("~r~Gang shop not available for your faction.") return end + printShopList("AMMO SHOP", Config.Shops.AmmoList or {}) + Notify("Open console (F8) to see ammo list. Buy: /gshop_buyammo ") +end) + +RegisterCommand("gshop_vehicles", function() + if not allowedGang() then Notify("~r~Gang shop not available for your faction.") return end + printShopList("VEHICLE SHOP", Config.Shops.VehicleList or {}) + Notify("Open console (F8) to see vehicle list. Buy: /gshop_buyveh ") +end) + +RegisterCommand("gshop_buyweapon", function(_, args) + if not allowedGang() then return end + local id = args[1] + if not id then Notify("Usage: /gshop_buyweapon ") return end + TriggerServerEvent("turfwar:shop:buyWeapon", id) +end) + +RegisterCommand("gshop_buyammo", function(_, args) + if not allowedGang() then return end + local id = args[1] + if not id then Notify("Usage: /gshop_buyammo ") return end + TriggerServerEvent("turfwar:shop:buyAmmo", id) +end) + +RegisterCommand("gshop_buyveh", function(_, args) + if not allowedGang() then return end + local id = args[1] + if not id then Notify("Usage: /gshop_buyveh ") return end + TriggerServerEvent("turfwar:shop:buyVehicle", id) +end) + +-- ========================= +-- World markers (optional) +-- Press E near configured coords to open lists +-- ========================= +CreateThread(function() + while true do + Wait(0) + if not allowedGang() then goto continue end + + local ped = PlayerPedId() + local p = GetEntityCoords(ped) + + local function handleLocations(locs, hint, cmd) + for _, s in ipairs(locs or {}) do + local d = #(p - s.coords) + if d < (Config.Shops.MarkerDist or 25.0) then + DrawMarker(1, s.coords.x, s.coords.y, s.coords.z - 1.0, 0,0,0, 0,0,0, 1.2,1.2,0.8, 255,255,255, 120, false,true,2, false,nil,nil,false) + if d < (Config.Shops.UseDist or 2.0) then + Draw3DText(s.coords.x, s.coords.y, s.coords.z + 0.3, ("~w~%s~n~~y~[E]~w~ %s"):format(s.label or "Shop", hint)) + if IsControlJustPressed(0, Config.Shops.InteractKey or 38) then + ExecuteCommand(cmd) + end + end + end + end + end + + handleLocations(Config.Shops.WeaponShopLocations, "Open Weapon Shop", "gshop_weapons") + handleLocations(Config.Shops.VehicleShopLocations, "Open Vehicle Shop", "gshop_vehicles") + + ::continue:: + end +end) diff --git a/client/gangbank.lua b/client/gangbank.lua new file mode 100644 index 0000000..2e99b08 --- /dev/null +++ b/client/gangbank.lua @@ -0,0 +1,196 @@ +-- client/gangbank.lua +print("^2[turfwar]^7 gangbank.lua loaded (client, robust + rgb)") + +local currentGang = 0 +local lastBalance = 0 +local lastRequestAt = 0 +local REQUEST_COOLDOWN_MS = 800 + +local function isHiddenGang(gangId) + gangId = tonumber(gangId) or 0 + return gangId == 0 or gangId == 3 +end + +-- ✅ Get gang RGB from Config.GANGS (supports numeric or string keys) +local function getGangRGB(gangId) + gangId = tonumber(gangId) or 0 + local fallback = { 255, 255, 255 } + + if not (Config and Config.GANGS) then + return fallback + end + + local g = Config.GANGS[gangId] or Config.GANGS[tostring(gangId)] + if g and type(g.rgb) == "table" then + local r = tonumber(g.rgb[1]) or tonumber(g.rgb.r) + local gg = tonumber(g.rgb[2]) or tonumber(g.rgb.g) + local b = tonumber(g.rgb[3]) or tonumber(g.rgb.b) + if r and gg and b then + return { r, gg, b } + end + end + + return fallback +end + +local function setLabelForGang(gangId) + local label = "Gang Bank" + gangId = tonumber(gangId) or 0 + + if Config and Config.GANGS then + local g = Config.GANGS[gangId] or Config.GANGS[tostring(gangId)] + if g and g.name then label = g.name end + end + + local rgb = getGangRGB(gangId) + + -- ✅ Send label + rgb so NUI can color it + SendNUIMessage({ + type = "gangbank:label", + label = label, + gangId = gangId, + rgb = rgb + }) +end + +local function show() + SendNUIMessage({ type = "gangbank:show" }) +end + +local function hide() + SendNUIMessage({ type = "gangbank:hide" }) +end + +local function setBalance(balance) + local b = tonumber(balance) or 0 + lastBalance = b + + -- ✅ debug print shows the correct updated balance + print(("[GangBank] sending NUI gang=%d balance=%d"):format(currentGang, lastBalance)) + + local rgb = getGangRGB(currentGang) + + -- ✅ Send rgb here too (in case update arrives without a label) + SendNUIMessage({ + type = "gangbank:update", + gangId = currentGang, + balance = lastBalance, + rgb = rgb + }) +end + +local function applyVisibility() + if isHiddenGang(currentGang) then + hide() + return + end + setLabelForGang(currentGang) + show() +end + +local function requestMyGangBank(force) + local now = GetGameTimer() + if not force and (now - lastRequestAt) < REQUEST_COOLDOWN_MS then return end + lastRequestAt = now + TriggerServerEvent("turfwar:gangbank:request") +end + +-- Retry helper (handles "request too early" / race conditions) +local function requestWithRetries(maxTries, delayMs) + CreateThread(function() + maxTries = tonumber(maxTries) or 6 + delayMs = tonumber(delayMs) or 800 + + for i = 1, maxTries do + requestMyGangBank(true) + Wait(delayMs) + + if currentGang ~= 0 then + -- If gangs can legitimately be $0, you can remove this check. + if lastBalance > 0 then + return + end + end + end + end) +end + +-- Ask shortly after load (covers resource restart / joining late) +CreateThread(function() + Wait(900) + requestWithRetries(8, 900) +end) + +-- ------------------------------------------------------------ +-- Gang changes (server sends BOTH of these in your main.lua) +-- ------------------------------------------------------------ + +-- Broadcast to all clients: (src, gangId) +RegisterNetEvent("turfwar:playerGang", function(src, gangId) + if tonumber(src) ~= tonumber(GetPlayerServerId(PlayerId())) then return end + + currentGang = tonumber(gangId) or 0 + print(("^5[GangBank]^7 turfwar:playerGang -> gang=%d"):format(currentGang)) + + applyVisibility() + requestWithRetries(6, 700) +end) + +-- Sent directly to the player: (gangId) +RegisterNetEvent("turfwar:gangUpdate", function(gangId) + currentGang = tonumber(gangId) or 0 + print(("^5[GangBank]^7 turfwar:gangUpdate -> gang=%d"):format(currentGang)) + + applyVisibility() + requestWithRetries(6, 700) +end) + +-- Compatibility: some parts of your system also send this +RegisterNetEvent("turfwar:setFaction", function(gangId) + currentGang = tonumber(gangId) or 0 + print(("^5[GangBank]^7 turfwar:setFaction -> gang=%d"):format(currentGang)) + + applyVisibility() + requestWithRetries(6, 700) +end) + +-- ------------------------------------------------------------ +-- Balance update from server +-- ------------------------------------------------------------ +RegisterNetEvent("turfwar:gangbank:update", function(gangId, balance) + gangId = tonumber(gangId) or 0 + balance = tonumber(balance) or 0 + + -- ⚠️ IMPORTANT RACE FIX: + -- If we ALREADY know we're in a real gang, ignore "gangId=0" updates + if currentGang ~= 0 and gangId == 0 then + print(("^3[GangBank]^7 Ignored update gangId=0 (known gang=%d) balance=%d"):format(currentGang, balance)) + return + end + + currentGang = gangId + print(("^2[GangBank]^7 UPDATE gang=%d balance=%d"):format(currentGang, balance)) + + if isHiddenGang(currentGang) then + hide() + return + end + + setLabelForGang(currentGang) + show() + setBalance(balance) +end) + +-- ------------------------------------------------------------ +-- Resource restart +-- ------------------------------------------------------------ +AddEventHandler("onClientResourceStart", function(res) + if res ~= GetCurrentResourceName() then return end + currentGang = 0 + lastBalance = 0 + hide() + CreateThread(function() + Wait(900) + requestWithRetries(8, 900) + end) +end)