559 lines
19 KiB
Lua
559 lines
19 KiB
Lua
-- 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)
|