-- 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)