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