diff --git a/client/money_hud.lua b/client/money_hud.lua new file mode 100644 index 0000000..c9eead7 --- /dev/null +++ b/client/money_hud.lua @@ -0,0 +1,37 @@ +print("^2[turfwar]^7 money_hud.lua loaded") + +CreateThread(function() + Wait(1000) + SendNUIMessage({ type = "money:show" }) +end) + +RegisterNetEvent('turfwar:money:update', function(cash, bank) + SendNUIMessage({ + type = "money:update", + cash = cash or 0, + bank = bank or 0 + }) +end) + +-- Optional: hide HUD when paused +CreateThread(function() + while true do + Wait(250) + if IsPauseMenuActive() then + SendNUIMessage({ type = "money:hide" }) + else + SendNUIMessage({ type = "money:show" }) + end + end +end) + +AddEventHandler('onClientResourceStart', function(resName) + if resName ~= GetCurrentResourceName() then return end + Wait(500) + TriggerServerEvent('turfwar:money:request') +end) + +AddEventHandler('playerSpawned', function() + Wait(500) + TriggerServerEvent('turfwar:money:request') +end) diff --git a/client/player_police.lua b/client/player_police.lua new file mode 100644 index 0000000..234c25e --- /dev/null +++ b/client/player_police.lua @@ -0,0 +1,201 @@ +-- client/player_police.lua +print("^2[turfwar]^7 client/player_police.lua loaded (police tracking)") + +local PP = Config.PlayerPolice or {} +local POLICE_GANG_ID = tonumber(PP.POLICE_GANG_ID) or 3 + +local isPolice = false +local myStars = 0 +local myInterior = false + +-- targets[serverId] = {stars, interior, lastPos, circleBlip, realtimeBlip, lastPingAt} +local targets = {} + +local function vec3(x,y,z) return vector3(x+0.0, y+0.0, z+0.0) end +local function InInterior(ped) return (GetInteriorFromEntity(ped) ~= 0) end + +local function ClearBlipSafe(blip) + if blip and DoesBlipExist(blip) then RemoveBlip(blip) end +end + +local function EnsureTarget(src) + if not targets[src] then + targets[src] = { + stars = 0, + interior = false, + lastPos = nil, + circleBlip = nil, + realtimeBlip = nil, + lastPingAt = 0 + } + end + return targets[src] +end + +local function ClearTarget(src) + local t = targets[src] + if not t then return end + ClearBlipSafe(t.circleBlip) + ClearBlipSafe(t.realtimeBlip) + targets[src] = nil +end + +local function UpdateVisual(src) + if not isPolice then return end + local t = targets[src] + if not t then return end + + local stars = tonumber(t.stars) or 0 + local interior = (t.interior == true) + + -- 4-5 stars realtime, but interior forces "2-star mode" + local effective = stars + if stars >= 4 and interior then + effective = 2 + end + + if stars <= 0 then + ClearBlipSafe(t.circleBlip); t.circleBlip = nil + ClearBlipSafe(t.realtimeBlip); t.realtimeBlip = nil + return + end + + if not t.lastPos then return end + + -- clear visuals that don't match mode + if effective <= 3 then + ClearBlipSafe(t.realtimeBlip); t.realtimeBlip = nil + end + if effective >= 4 then + ClearBlipSafe(t.circleBlip); t.circleBlip = nil + end + + if effective == 1 or effective == 2 then + local radius = (effective == 1) and (PP.CIRCLE_RADIUS_1STAR or 220.0) or (PP.CIRCLE_RADIUS_2STAR or 160.0) + + ClearBlipSafe(t.circleBlip) + t.circleBlip = AddBlipForRadius(t.lastPos.x, t.lastPos.y, t.lastPos.z, radius) + SetBlipAlpha(t.circleBlip, 90) + + elseif effective == 3 then + -- ping exact position every update (client sends every 2s) + local now = GetGameTimer() + if (now - (t.lastPingAt or 0)) > 250 then + t.lastPingAt = now + + local blip = AddBlipForCoord(t.lastPos.x, t.lastPos.y, t.lastPos.z) + SetBlipSprite(blip, PP.PING_BLIP_SPRITE or 161) + SetBlipScale(blip, PP.PING_BLIP_SCALE or 1.0) + BeginTextCommandSetBlipName("STRING") + AddTextComponentString("Wanted Suspect (Ping)") + EndTextCommandSetBlipName(blip) + + CreateThread(function() + Wait(PP.PING_BLIP_LIFETIME_MS or 1500) + ClearBlipSafe(blip) + end) + end + + elseif effective >= 4 then + -- realtime blip + if not t.realtimeBlip or not DoesBlipExist(t.realtimeBlip) then + t.realtimeBlip = AddBlipForCoord(t.lastPos.x, t.lastPos.y, t.lastPos.z) + SetBlipSprite(t.realtimeBlip, PP.REALTIME_BLIP_SPRITE or 1) + SetBlipScale(t.realtimeBlip, PP.REALTIME_BLIP_SCALE or 0.9) + BeginTextCommandSetBlipName("STRING") + AddTextComponentString("Wanted Suspect") + EndTextCommandSetBlipName(t.realtimeBlip) + else + SetBlipCoords(t.realtimeBlip, t.lastPos.x, t.lastPos.y, t.lastPos.z) + end + end +end + +-- Police-only chat from server +RegisterNetEvent('turfwar:pp:chat', function(msg) + if not isPolice then return end + TriggerEvent('chat:addMessage', { args = { msg } }) +end) + +-- Police-only tracking updates +RegisterNetEvent('turfwar:pp:update', function(offenderSrc, data) + if not isPolice then return end + if type(data) ~= "table" then return end + + local t = EnsureTarget(offenderSrc) + + if data.type == "mode" then + t.stars = tonumber(data.stars) or 0 + t.interior = (data.interior == true) + UpdateVisual(offenderSrc) + return + end + + if data.type == "pos" then + t.stars = tonumber(data.stars) or 0 + t.interior = (data.interior == true) + t.lastPos = vec3(data.x, data.y, data.z) + UpdateVisual(offenderSrc) + return + end +end) + +RegisterNetEvent('turfwar:pp:clear', function(offenderSrc) + ClearTarget(offenderSrc) +end) + +-- Detect if THIS player is police (from your existing broadcast) +RegisterNetEvent('turfwar:playerGang', function(src, gangId) + if src ~= GetPlayerServerId(PlayerId()) then return end + isPolice = (tonumber(gangId) or 0) == POLICE_GANG_ID + + if not isPolice then + for id, t in pairs(targets) do + ClearBlipSafe(t.circleBlip) + ClearBlipSafe(t.realtimeBlip) + end + targets = {} + end +end) + +-- How often offenders send updates (based on their own stars) +local function CurrentInterval(stars) + if stars <= 0 then return 1000 end + if stars == 1 then return PP.UPDATE_MS_1STAR or 5000 end + if stars == 2 then return PP.UPDATE_MS_2STAR or 2000 end + if stars == 3 then return PP.UPDATE_MS_3STAR or 2000 end + return PP.UPDATE_MS_45STAR_REALTIME or 500 +end + +-- Watch stars/interior changes (all players) +CreateThread(function() + while true do + Wait(500) + local ped = PlayerPedId() + local stars = GetPlayerWantedLevel(PlayerId()) or 0 + local interior = InInterior(ped) + + if stars ~= myStars or interior ~= myInterior then + myStars = stars + myInterior = interior + TriggerServerEvent('turfwar:pp:starsChanged', myStars, myInterior) + end + end +end) + +-- Stream offender position updates (all players with stars) +CreateThread(function() + while true do + Wait(CurrentInterval(myStars)) + + if myStars > 0 then + local ped = PlayerPedId() + local c = GetEntityCoords(ped) + local h = GetEntityHeading(ped) + local interior = InInterior(ped) + myInterior = interior + + TriggerServerEvent('turfwar:pp:posUpdate', myStars, interior, c.x, c.y, c.z, h) + end + end +end) diff --git a/client/police_wanted_escalate.lua b/client/police_wanted_escalate.lua new file mode 100644 index 0000000..7351953 --- /dev/null +++ b/client/police_wanted_escalate.lua @@ -0,0 +1,136 @@ +print("^2[turfwar]^7 police_wanted_escalate.lua loaded (client)") + +-- ------------------------- +-- Tunables +-- ------------------------- +local DMG_POLL_MS = 200 +local DMG_COOLDOWN_MS = 2500 -- per killer, per victim + +-- ------------------------- +-- State +-- ------------------------- +local sentThisDeath = false +local lastDeathAt = 0 +local dmgCooldown = {} -- [killerSid] = lastSentAt (GetGameTimer ms) + +-- ------------------------- +-- Helpers +-- ------------------------- +local function killerServerIdFromEntity(ent) + if not ent or ent == 0 or not DoesEntityExist(ent) then return 0 end + + local killerPed = ent + if IsEntityAVehicle(ent) then + killerPed = GetPedInVehicleSeat(ent, -1) + end + + if not killerPed or killerPed == 0 or not DoesEntityExist(killerPed) then return 0 end + if not IsEntityAPed(killerPed) then return 0 end + if not IsPedAPlayer(killerPed) then return 0 end + + local idx = NetworkGetPlayerIndexFromPed(killerPed) + if idx == -1 then return 0 end + + return tonumber(GetPlayerServerId(idx)) or 0 +end + +-- Reset death flag when alive again +CreateThread(function() + while true do + Wait(250) + if sentThisDeath and not IsEntityDead(PlayerPedId()) then + sentThisDeath = false + end + end +end) + +-- ------------------------- +-- KILL detection (keep what works) +-- ------------------------- +AddEventHandler('gameEventTriggered', function(name, args) + if name ~= 'CEventNetworkEntityDamage' then return end + + local victimEntity = args[1] + local attackerEntity = args[2] + local victimDied = args[6] + + if victimEntity ~= PlayerPedId() then return end + if not victimDied then return end + + if sentThisDeath then return end + if not IsEntityDead(PlayerPedId()) then return end + + local now = GetGameTimer() + if now - lastDeathAt < 1500 then return end + lastDeathAt = now + sentThisDeath = true + + local killerSid = killerServerIdFromEntity(attackerEntity) + if killerSid <= 0 then + -- fallback + killerSid = killerServerIdFromEntity(GetEntityLastDamageEntity(PlayerPedId())) + end + if killerSid > 0 then + TriggerServerEvent('turfwar:wanted:policeKilledBy', killerSid) + end +end) + +-- ------------------------- +-- DAMAGE detection (reliable polling) +-- ------------------------- +CreateThread(function() + while true do + Wait(DMG_POLL_MS) + + local ped = PlayerPedId() + if ped == 0 or not DoesEntityExist(ped) then goto continue end + if IsEntityDead(ped) then goto continue end + + -- Was I damaged by any ped since last clear? + if HasEntityBeenDamagedByAnyPed(ped) then + local lastEnt = GetEntityLastDamageEntity(ped) + local killerSid = killerServerIdFromEntity(lastEnt) + + if killerSid > 0 then + local now = GetGameTimer() + local last = dmgCooldown[killerSid] or 0 + + if (now - last) >= DMG_COOLDOWN_MS then + dmgCooldown[killerSid] = now + TriggerServerEvent('turfwar:wanted:policeDamagedBy', killerSid) + end + end + + -- Clear flags so we don't spam forever + ClearEntityLastDamageEntity(ped) + ClearPedLastWeaponDamage(ped) + end + + ::continue:: + end +end) + +-- ------------------------- +-- Wanted application (killer client) +-- ------------------------- +RegisterNetEvent('turfwar:wanted:setMinimum', function(minStars) + local pid = PlayerId() + local current = GetPlayerWantedLevel(pid) + local newStars = math.max(current, tonumber(minStars) or 0) + + if newStars ~= current then + ClearPlayerWantedLevel(pid) + SetPlayerWantedLevel(pid, newStars, false) + SetPlayerWantedLevelNow(pid, true) + end +end) + +RegisterNetEvent('turfwar:wanted:setEscalated', function(targetStars, count, windowSec) + local pid = PlayerId() + local current = GetPlayerWantedLevel(pid) + local newStars = math.max(current, tonumber(targetStars) or 0) + + ClearPlayerWantedLevel(pid) + SetPlayerWantedLevel(pid, newStars, false) + SetPlayerWantedLevelNow(pid, true) +end) diff --git a/client/pvp.lua b/client/pvp.lua new file mode 100644 index 0000000..77a933a --- /dev/null +++ b/client/pvp.lua @@ -0,0 +1,11 @@ +-- client/pvp.lua +-- Ensures PvP stays enabled (some resources toggle it off) +print("^2[turfwar]^7 client/pvp.lua loaded") + +CreateThread(function() + while true do + NetworkSetFriendlyFireOption(true) + SetCanAttackFriendly(PlayerPedId(), true, true) + Wait(1000) + end +end) diff --git a/client/shop.lua b/client/shop.lua new file mode 100644 index 0000000..0ebe43e --- /dev/null +++ b/client/shop.lua @@ -0,0 +1,638 @@ +-- client/shop.lua +print("^2[turfwar]^7 client/shop.lua loaded (gang+police shop NUI + markers + blips + shop vehicle ownership + debug)") + +local currentGang = 0 +local shopOpen = false +local lastOpenAttempt = 0 + +-- Debug toggles +local DEBUG_MARKERS = false -- /tw_shopdebug + +-- Shop blips (map icons) +local ShopBlips = {} + +-- Shop vehicle (1 per player) + blip +local myShopVeh = 0 +local myShopVehBlip = 0 + +-- Server sync throttle +local lastSyncedGang = -1 +local lastSyncAt = 0 + +-- ========================= +-- Helpers +-- ========================= +local function Notify(msg) + BeginTextCommandThefeedPost("STRING") + AddTextComponentSubstringPlayerName(msg) + EndTextCommandThefeedPostTicker(false, false) +end + +local function v3(pos) + if not pos then return vector3(0.0, 0.0, 0.0) end + return vector3(pos.x + 0.0, pos.y + 0.0, pos.z + 0.0) +end + +local function dist(a, b) + local dx = (a.x + 0.0) - (b.x + 0.0) + local dy = (a.y + 0.0) - (b.y + 0.0) + local dz = (a.z + 0.0) - (b.z + 0.0) + return math.sqrt(dx * dx + dy * dy + dz * dz) +end + +local function iterList(t) + if type(t) ~= "table" then + return function() return nil end + end + + local n = #t + if n > 0 then + local i = 0 + return function() + i = i + 1 + if i <= n then return i, t[i] end + end + end + + return pairs(t) +end + +local function getGangRGB(gangId) + local fallback = { r = 10, g = 10, b = 10 } + + local gangs = (Config and Config.GANGS) or nil + if type(gangs) ~= "table" then return fallback end + + local g = gangs[tonumber(gangId) or 0] + if type(g) ~= "table" then return fallback end + + local rgb = g.rgb + if type(rgb) ~= "table" then return fallback end + + local r = tonumber(rgb.r or rgb[1]) + local gg = tonumber(rgb.g or rgb[2]) + local b = tonumber(rgb.b or rgb[3]) + + if not r or not gg or not b then return fallback end + return { r = r, g = gg, b = b } +end + +local function drawMarkerAt(posIn) + local shops = Config.Shops or {} + local m = shops.Marker or {} + + local pos = v3(posIn) + local t = tonumber(m.type) or 1 + + local sc = m.scale + if sc == nil then + sc = vector3(1.2, 1.2, 0.9) + elseif type(sc) == "table" and sc.x == nil then + sc = vector3( + tonumber(sc[1]) or 1.2, + tonumber(sc[2]) or 1.2, + tonumber(sc[3]) or 0.9 + ) + end + + local rgba = m.rgba or { 80, 160, 255, 140 } + + local z = pos.z + local found, gz + for _, off in ipairs({ 300.0, 100.0, 30.0, 10.0 }) do + found, gz = GetGroundZFor_3dCoord(pos.x, pos.y, pos.z + off, true) + if found then + z = gz + 0.05 + break + end + end + + DrawMarker( + t, + pos.x, pos.y, z, + 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, + sc.x, sc.y, sc.z, + rgba[1] or 80, + rgba[2] or 160, + rgba[3] or 255, + rgba[4] or 140, + false, true, 2, false, nil, nil, false + ) +end + +-- ========================================================= +-- NEW: Server->Client gang sync responder +-- ========================================================= +RegisterNetEvent("turfwar:shop:syncGangRequest", function(nonce) + print(("[turfwar:shop] syncGangRequest nonce=%s -> reply gang=%d"):format(tostring(nonce), tonumber(currentGang) or 0)) + TriggerServerEvent("turfwar:shop:syncGangResponse", nonce, currentGang or 0) +end) + +-- ========================= +-- Vehicle ownership (1 per player) + blip + gang colour +-- ========================= +local function clearMyShopVehicle() + if myShopVehBlip ~= 0 and DoesBlipExist(myShopVehBlip) then + RemoveBlip(myShopVehBlip) + end + myShopVehBlip = 0 + + if myShopVeh ~= 0 and DoesEntityExist(myShopVeh) then + SetEntityAsMissionEntity(myShopVeh, true, true) + DeleteEntity(myShopVeh) + end + myShopVeh = 0 +end + +local function createMyShopVehBlip(veh) + if myShopVehBlip ~= 0 and DoesBlipExist(myShopVehBlip) then + RemoveBlip(myShopVehBlip) + end + + local blip = AddBlipForEntity(veh) + SetBlipSprite(blip, 225) + SetBlipScale(blip, 0.85) + SetBlipAsShortRange(blip, false) + + local g = Config and Config.GANGS and Config.GANGS[currentGang] + if g and g.blipColor then + SetBlipColour(blip, g.blipColor) + end + + BeginTextCommandSetBlipName("STRING") + AddTextComponentString(currentGang == 3 and "Police Vehicle" or "My Vehicle") + EndTextCommandSetBlipName(blip) + + myShopVehBlip = blip +end + +local function applyGangColorsToVehicle(veh, gangId) + local rgb = getGangRGB(gangId) + SetVehicleModKit(veh, 0) + SetVehicleCustomPrimaryColour(veh, rgb.r, rgb.g, rgb.b) + SetVehicleCustomSecondaryColour(veh, rgb.r, rgb.g, rgb.b) + SetVehicleDirtLevel(veh, 0.0) +end + +local function resolveVehicleModel(modelIn) + local model = tostring(modelIn or "") + if model ~= "" then return model end + + if Config and Config.Shops and Config.Shops.GangVehicleModels and Config.Shops.GangVehicleModels[currentGang] then + return tostring(Config.Shops.GangVehicleModels[currentGang]) + end + + if Config and Config.Vehicles and Config.Vehicles.gangModels and Config.Vehicles.gangModels[currentGang] then + return tostring(Config.Vehicles.gangModels[currentGang]) + end + + return "sultan" -- fallback only +end + +local function findBestVehicleSpawn() + local byGang = Config and Config.Shops and Config.Shops.LocationsByGang + local g = byGang and byGang[currentGang] + local pads = g and g.vehicles + + local ped = PlayerPedId() + local p = GetEntityCoords(ped) + + if type(pads) == "table" then + local best, bestD = nil, 999999.0 + for _, v in pairs(pads) do + local pos = v3(v) + local d = dist(pos, p) + if d < bestD then + bestD = d + best = v + end + end + + if best then + local pos = v3(best) + local heading = tonumber(best.w) or tonumber(best.h) or 0.0 + return pos, heading + end + end + + local fwd = GetEntityForwardVector(ped) + local pos = vector3(p.x + fwd.x * 6.0, p.y + fwd.y * 6.0, p.z) + return pos, GetEntityHeading(ped) +end + +local function spawnVehicleClient(modelName) + clearMyShopVehicle() + + local model = resolveVehicleModel(modelName) + local hash = GetHashKey(model) + + if not IsModelInCdimage(hash) or not IsModelAVehicle(hash) then + Notify(("Invalid vehicle model: %s"):format(model)) + return + end + + RequestModel(hash) + local deadline = GetGameTimer() + 7000 + while not HasModelLoaded(hash) do + Wait(0) + if GetGameTimer() > deadline then + Notify(("Failed to load model: %s"):format(model)) + return + end + end + + local pos, heading = findBestVehicleSpawn() + + RequestCollisionAtCoord(pos.x, pos.y, pos.z) + for _ = 1, 20 do + local found, gz = GetGroundZFor_3dCoord(pos.x, pos.y, pos.z + 50.0, true) + if found then + pos = vector3(pos.x, pos.y, gz + 0.5) + break + end + Wait(0) + end + + local veh = CreateVehicle(hash, pos.x, pos.y, pos.z, heading, true, false) + if veh == 0 then + Notify("Vehicle spawn failed.") + SetModelAsNoLongerNeeded(hash) + return + end + + SetEntityAsMissionEntity(veh, true, true) + SetVehicleOnGroundProperly(veh) + SetVehicleEngineOn(veh, true, true, false) + SetVehRadioStation(veh, "OFF") + + if tonumber(currentGang) == 3 then + local a = (Config and Config.Shops and Config.Shops.PoliceVehicleAppearance) or {} + if a.primary ~= nil and a.secondary ~= nil then + SetVehicleColours(veh, tonumber(a.primary) or 0, tonumber(a.secondary) or 0) + end + if a.pearlescent ~= nil or a.wheel ~= nil then + SetVehicleExtraColours(veh, tonumber(a.pearlescent) or 0, tonumber(a.wheel) or 0) + end + if a.livery ~= nil then + local liv = tonumber(a.livery) + if liv and liv >= 0 then SetVehicleLivery(veh, liv) end + end + if a.clean then + SetVehicleDirtLevel(veh, 0.0) + WashDecalsFromVehicle(veh, 1.0) + end + else + applyGangColorsToVehicle(veh, currentGang) + end + + myShopVeh = veh + createMyShopVehBlip(veh) + TaskWarpPedIntoVehicle(PlayerPedId(), veh, -1) + + SetModelAsNoLongerNeeded(hash) + Notify(("Spawned: %s"):format(model)) +end + +RegisterNetEvent("turfwar:shop:grantVehicle", function(model) + print(("[turfwar:shop] grantVehicle RECEIVED model=%s gang=%d"):format(tostring(model), tonumber(currentGang) or -1)) + spawnVehicleClient(model) +end) + +AddEventHandler("onResourceStop", function(res) + if res ~= GetCurrentResourceName() then return end + clearMyShopVehicle() +end) + +-- ========================= +-- Payload selector (Police vs Gang) +-- ========================= +local function buildShopPayload() + local s = Config.Shops or {} + if currentGang == 3 then + return { weapons = s.PoliceWeapons or {}, ammo = s.PoliceAmmo or {}, vehicles = s.PoliceVehicles or {}, gangs = Config.GANGS or {} } + end + return { weapons = s.Weapons or {}, ammo = s.Ammo or {}, vehicles = s.Vehicles or {}, gangs = Config.GANGS or {} } +end + +local function getGangShopLocations(gangId) + local s = Config.Shops or {} + local byGang = s.LocationsByGang or {} + local g = byGang[tonumber(gangId) or 0] + if type(g) ~= "table" then return nil end + return { shop = g.shop or {}, vehicles = g.vehicles or {} } +end + +-- ========================= +-- NUI Open/Close +-- ========================= +local function openShop(tab, allowedTabs) + if shopOpen then return end + shopOpen = true + + SetNuiFocus(true, true) + SetNuiFocusKeepInput(false) + + SendNUIMessage({ + type = "shop:open", + tab = tab or "weapons", + gangId = currentGang, + gangRGB = getGangRGB(currentGang), + payload = buildShopPayload(), + allowedTabs = allowedTabs or { "weapons", "ammo", "vehicles" }, + }) + + TriggerServerEvent("turfwar:shop:requestBalance") +end + +local function closeShop() + if not shopOpen then return end + shopOpen = false + SetNuiFocus(false, false) + SendNUIMessage({ type = "shop:close" }) +end + +-- ========================= +-- Shop Blips +-- ========================= +local function clearShopBlips() + for _, b in ipairs(ShopBlips) do + if DoesBlipExist(b) then RemoveBlip(b) end + end + ShopBlips = {} +end + +local function addShopBlip(posIn, label) + local pos = v3(posIn) + local blip = AddBlipForCoord(pos.x, pos.y, pos.z) + + SetBlipSprite(blip, 374) + SetBlipScale(blip, 0.75) + SetBlipAsShortRange(blip, true) + + local g = Config.GANGS and Config.GANGS[currentGang] + SetBlipColour(blip, (g and g.blipColor) or 0) + + BeginTextCommandSetBlipName("STRING") + AddTextComponentString(label) + EndTextCommandSetBlipName(blip) + + ShopBlips[#ShopBlips + 1] = blip +end + +local function buildShopBlips() + clearShopBlips() + + if currentGang == 0 then + return + end + + local gLocs = getGangShopLocations(currentGang) + if not gLocs then + print(("^1[turfwar]^7 No shop locations for gangId=%d. Check Config.Shops.LocationsByGang[%d] in shops.lua"):format(currentGang, currentGang)) + return + end + + local isPolice = (currentGang == 3) + + for _, pos in iterList(gLocs.shop) do + addShopBlip(pos, isPolice and "Police Armory" or "Gang Shop") + end + + for _, pos in iterList(gLocs.vehicles) do + addShopBlip(pos, isPolice and "Police Vehicles" or "Gang Vehicles") + end +end + +-- ========================= +-- Commands +-- ========================= +RegisterCommand("tw_mygang", function() + Notify(("currentGang=%d"):format(currentGang or -1)) + print(("^3[turfwar]^7 tw_mygang -> currentGang=%d"):format(currentGang or -1)) +end, false) + +RegisterCommand("tw_shopdebug", function() + DEBUG_MARKERS = not DEBUG_MARKERS + Notify(("Shop marker debug: %s"):format(DEBUG_MARKERS and "~g~ON~s~" or "~r~OFF~s~")) + print(("^3[turfwar]^7 DEBUG_MARKERS=%s"):format(tostring(DEBUG_MARKERS))) +end, false) + +-- ========================= +-- Server sync + gang setter +-- ========================= +local function SyncGangToServer() + local now = GetGameTimer() + if currentGang == lastSyncedGang and (now - lastSyncAt) < 1000 then return end + lastSyncedGang = currentGang + lastSyncAt = now + TriggerServerEvent("turfwar:setFaction", currentGang) +end + +local function setGang(newGang) + newGang = tonumber(newGang) or 0 + if newGang == currentGang then + SyncGangToServer() + return + end + + clearMyShopVehicle() + + currentGang = newGang + SyncGangToServer() + + if shopOpen then + SendNUIMessage({ type = "shop:gang", gangId = currentGang, gangRGB = getGangRGB(currentGang) }) + TriggerServerEvent("turfwar:shop:requestBalance") + end + + buildShopBlips() +end + +-- ========================= +-- Events from your gang system +-- ========================= +RegisterNetEvent("turfwar:setMyGang", function(gangId) + setGang(gangId) +end) + +RegisterNetEvent("turfwar:setFaction", function(gangId) + print(("^3[turfwar]^7 shop.lua got turfwar:setFaction -> %d"):format(tonumber(gangId) or 0)) + setGang(gangId) +end) + +RegisterNetEvent("turfwar:gangUpdate", function(gangId) + print(("^3[turfwar]^7 shop.lua got turfwar:gangUpdate -> %d"):format(tonumber(gangId) or 0)) + setGang(gangId) +end) + +-- ========================= +-- Shop UI info events +-- ========================= +RegisterNetEvent("turfwar:shop:balance", function(gangId, balance) + SendNUIMessage({ + type = "shop:balance", + gangId = tonumber(gangId) or 0, + balance = tonumber(balance) or 0 + }) +end) + +RegisterNetEvent("turfwar:shop:result", function(ok, message, newBalance) + if message and message ~= "" then Notify(message) end + if newBalance ~= nil then + SendNUIMessage({ + type = "shop:balance", + gangId = currentGang, + balance = tonumber(newBalance) or 0 + }) + end +end) + +RegisterNetEvent("turfwar:shop:grantWeapon", function(weaponName, ammo) + print(("[turfwar:shop] grantWeapon RECEIVED weapon=%s ammo=%s gang=%d"):format( + tostring(weaponName), tostring(ammo), tonumber(currentGang) or -1 + )) + + local ped = PlayerPedId() + local wHash = GetHashKey(weaponName) + GiveWeaponToPed(ped, wHash, tonumber(ammo) or 0, false, true) + + -- verify after a short delay (detects scripts stripping weapons) + CreateThread(function() + Wait(250) + local hasIt = HasPedGotWeapon(ped, wHash, false) + print(("[turfwar:shop] grantWeapon CHECK weapon=%s has=%s"):format(tostring(weaponName), tostring(hasIt))) + end) +end) + +RegisterNetEvent("turfwar:shop:grantAmmo", function(weaponName, amount) + print(("[turfwar:shop] grantAmmo RECEIVED for=%s amount=%s gang=%d"):format( + tostring(weaponName), tostring(amount), tonumber(currentGang) or -1 + )) + + local ped = PlayerPedId() + local wHash = GetHashKey(weaponName) + AddAmmoToPed(ped, wHash, tonumber(amount) or 0) +end) + +RegisterNetEvent("turfwar:shop:debugOpen", function(tab) + openShop(tab or "weapons", { "weapons", "ammo", "vehicles" }) +end) + +-- ========================= +-- NUI callbacks +-- ========================= +RegisterNUICallback("shop:close", function(_, cb) + closeShop() + cb({ ok = true }) +end) + +RegisterNUICallback("shop:buyWeapon", function(data, cb) + TriggerServerEvent("turfwar:shop:buyWeapon", tostring(data.itemId or "")) + cb({ ok = true }) +end) + +RegisterNUICallback("shop:buyAmmo", function(data, cb) + TriggerServerEvent("turfwar:shop:buyAmmo", tostring(data.itemId or "")) + cb({ ok = true }) +end) + +RegisterNUICallback("shop:buyVehicle", function(data, cb) + TriggerServerEvent("turfwar:shop:buyVehicle", tostring(data.itemId or "")) + cb({ ok = true }) +end) + +-- ========================= +-- Faction request (on join/spawn) +-- ========================= +local function RequestFaction() + TriggerServerEvent("turfwar:requestFaction") +end + +CreateThread(function() + Wait(1500) + RequestFaction() +end) + +AddEventHandler("playerSpawned", function() + Wait(1500) + RequestFaction() +end) + +-- ========================= +-- Marker + Interact loop +-- ========================= +CreateThread(function() + Wait(1000) + print("^3[turfwar]^7 shop.lua marker loop running") + + buildShopBlips() + + while true do + Wait(0) + + local ped = PlayerPedId() + local p = GetEntityCoords(ped) + + local shops = Config.Shops or {} + local md = tonumber(shops.MarkerDist) or 25.0 + local id = tonumber(shops.InteractDist) or 2.0 + local key = tonumber(shops.InteractKey) or 38 + + if DEBUG_MARKERS then + DrawMarker(1, p.x, p.y, p.z - 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.5, 0.5, 0.25, 255, 255, 255, 180, false, true, 2, false, nil, nil, false) + end + + if currentGang ~= 0 then + local gLocs = getGangShopLocations(currentGang) + if gLocs then + local isPolice = (currentGang == 3) + + local lists = { + { tab = "weapons", locs = gLocs.shop, allowed = { "weapons", "ammo" } }, + { tab = "vehicles", locs = gLocs.vehicles, allowed = { "vehicles" } }, + } + + local canOpen = false + local openTab = nil + local openAllowed = nil + + for _, entry in ipairs(lists) do + for _, posAny in iterList(entry.locs) do + local d = dist(v3(posAny), p) + if d <= md then + drawMarkerAt(posAny) + if d <= id then + canOpen = true + openTab = entry.tab + openAllowed = entry.allowed + end + end + end + end + + if canOpen and not shopOpen then + BeginTextCommandDisplayHelp("STRING") + AddTextComponentSubstringPlayerName( + isPolice and "Press ~INPUT_CONTEXT~ to open Police Armory" or + "Press ~INPUT_CONTEXT~ to open Gang Shop" + ) + EndTextCommandDisplayHelp(0, false, true, 0) + + if IsControlJustPressed(0, key) then + local now = GetGameTimer() + if now - lastOpenAttempt > 400 then + lastOpenAttempt = now + openShop(openTab or "weapons", openAllowed) + end + end + end + end + end + + if shopOpen and IsControlJustPressed(0, 322) then + closeShop() + end + end +end)