824 lines
27 KiB
Lua
824 lines
27 KiB
Lua
-- server/main.lua
|
||
-- Turfwar (single resource) - Turfs + Capture + Guard host + PlayerGang broadcast
|
||
-- Works with the guards.lua you’re using (no GetPedCombatTarget).
|
||
--
|
||
-- REQUIRED Config tables in config.lua:
|
||
-- Config.GANGS
|
||
-- Config.JOIN_POINTS (ARRAY of { gangId=number, label=string, pos=vector3/4 })
|
||
-- Config.TURFS (keyed by turfId)
|
||
-- Optional:
|
||
-- Config.SECONDS_TO_CAPTURE
|
||
-- Config.INCOME = { intervalMinutes=60, ... }
|
||
--
|
||
-- Guard system:
|
||
-- - One client is elected "GuardHost" (AI owner)
|
||
-- - Server tells GuardHost to spawn/clear guards per turf
|
||
-- - When all guards die, GuardHost triggers turfwar:guardsEmpty -> server frees capture again
|
||
--
|
||
-- Loadout system:
|
||
-- - When player joins a gang -> give gang loadout
|
||
-- - When player leaves gang (gangId 0) -> strip ALL weapons and ammo
|
||
--
|
||
-- Leaderboard:
|
||
-- - "Most influence" = count of turfs currently owned per gang
|
||
-- - Broadcast on capture + clientReady + reload + resource start
|
||
|
||
print("^2[turfwar]^7 server/main.lua LOADED")
|
||
|
||
local SecondsToCapture = (Config and tonumber(Config.SECONDS_TO_CAPTURE)) or 60
|
||
|
||
-- Police "Restore Peace" (how long police must hold to neutralize a turf)
|
||
local PoliceRestoreToNeutral = (Config and Config.PlayerPolice and tonumber(Config.PlayerPolice.RESTORE_SECONDS)) or 30
|
||
|
||
|
||
-- Loadouts is a GLOBAL provided by server/loadouts.lua (loaded before this file via fxmanifest)
|
||
local Loadouts = Loadouts
|
||
if not (Loadouts and Loadouts.ApplyForGang) then
|
||
print("^1[turfwar]^7 WARNING: Loadouts not loaded! Check fxmanifest order (loadouts.lua before main.lua).")
|
||
end
|
||
|
||
local function applyLoadoutSafe(src, gangId)
|
||
if Loadouts and Loadouts.ApplyForGang then
|
||
Loadouts.ApplyForGang(src, gangId)
|
||
end
|
||
end
|
||
|
||
AddEventHandler("playerJoining", function()
|
||
local src = source
|
||
|
||
if not (TurfwarPersist and TurfwarPersist.Load) then
|
||
PlayerGang[src] = PlayerGang[src] or 0
|
||
return
|
||
end
|
||
|
||
TurfwarPersist.Load(src, function(gangId, rank)
|
||
PlayerGang[src] = tonumber(gangId) or 0
|
||
|
||
-- Push to the player + everyone so HUD/blips are correct right away
|
||
TriggerClientEvent("turfwar:gangUpdate", src, PlayerGang[src])
|
||
TriggerClientEvent("turfwar:setFaction", src, PlayerGang[src])
|
||
TriggerClientEvent("turfwar:playerGang", -1, src, PlayerGang[src])
|
||
|
||
-- Apply loadout for their restored gang
|
||
applyLoadoutSafe(src, PlayerGang[src])
|
||
end)
|
||
end)
|
||
|
||
|
||
-- ------------------------------------------------------------
|
||
-- Player gangs (server authoritative)
|
||
-- ------------------------------------------------------------
|
||
PlayerGang = PlayerGang or {} -- [src] = gangId
|
||
|
||
local function setPlayerGang(src, gangId)
|
||
gangId = tonumber(gangId) or 0
|
||
|
||
local oldGang = tonumber(PlayerGang[src]) or 0
|
||
PlayerGang[src] = gangId
|
||
|
||
-- Push gang bank balance (optional module)
|
||
if TurfwarPayouts and TurfwarPayouts.GetGangBalance then
|
||
local bal = (gangId ~= 0) and (TurfwarPayouts.GetGangBalance(gangId) or 0) or 0
|
||
TriggerClientEvent("turfwar:gangbank:update", src, gangId, bal)
|
||
end
|
||
|
||
-- Vehicle spawner hook (optional module)
|
||
if VehicleSpawner and VehicleSpawner.OnGangChanged then
|
||
VehicleSpawner.OnGangChanged(src, oldGang, gangId)
|
||
end
|
||
|
||
-- Apply loadout rules whenever gang changes
|
||
applyLoadoutSafe(src, gangId)
|
||
|
||
-- Send updates to the player + broadcast to all
|
||
TriggerClientEvent("turfwar:gangUpdate", src, gangId)
|
||
TriggerClientEvent("turfwar:setFaction", src, gangId) -- compatibility
|
||
TriggerClientEvent("turfwar:playerGang", -1, src, gangId) -- broadcast to all clients
|
||
|
||
-- Join announcement in chat
|
||
if gangId ~= 0 and gangId ~= oldGang then
|
||
local g = (Config and Config.GANGS and Config.GANGS[gangId]) or nil
|
||
local color = (g and g.chatColor) or "~s~"
|
||
local gangName = (g and g.name) or ("Gang " .. tostring(gangId))
|
||
local playerName = GetPlayerName(src) or ("ID " .. tostring(src))
|
||
|
||
TriggerClientEvent('chat:addMessage', -1, {
|
||
args = {
|
||
"[Turfwar]",
|
||
("%s has joined the %s%s~s~!"):format(playerName, color, gangName)
|
||
}
|
||
})
|
||
end
|
||
-- Persist gang for next login
|
||
if TurfwarPersist and TurfwarPersist.SaveGang then
|
||
TurfwarPersist.SaveGang(src, gangId)
|
||
AddEventHandler("playerJoining", function()
|
||
local src = source
|
||
if TurfwarPersist and TurfwarPersist.Load then
|
||
local gangId, rank = TurfwarPersist.Load(src)
|
||
PlayerGang[src] = tonumber(gangId) or 0
|
||
|
||
TriggerClientEvent("turfwar:gangUpdate", src, PlayerGang[src])
|
||
TriggerClientEvent("turfwar:setFaction", src, PlayerGang[src])
|
||
TriggerClientEvent("turfwar:playerGang", -1, src, PlayerGang[src])
|
||
|
||
applyLoadoutSafe(src, PlayerGang[src])
|
||
else
|
||
PlayerGang[src] = PlayerGang[src] or 0
|
||
end
|
||
end)
|
||
|
||
end
|
||
|
||
end
|
||
|
||
RegisterNetEvent("turfwar:requestFaction", function()
|
||
local src = source
|
||
TriggerClientEvent("turfwar:setFaction", src, tonumber(PlayerGang[src]) or 0)
|
||
end)
|
||
|
||
RegisterNetEvent("turfwar:setGang", function(gangId)
|
||
local src = source
|
||
setPlayerGang(src, gangId)
|
||
|
||
if TurfwarPayouts and TurfwarPayouts.PushGangBankToPlayer then
|
||
TurfwarPayouts.PushGangBankToPlayer(src, tonumber(gangId) or 0)
|
||
end
|
||
end)
|
||
|
||
RegisterNetEvent("turfwar:requestAllPlayerGangs", function()
|
||
local src = source
|
||
for id, gang in pairs(PlayerGang) do
|
||
TriggerClientEvent("turfwar:playerGang", src, id, tonumber(gang) or 0)
|
||
end
|
||
end)
|
||
|
||
RegisterCommand("joingang", function(src, args)
|
||
local gangId = tonumber(args[1]) or 0
|
||
setPlayerGang(src, gangId)
|
||
end, false)
|
||
|
||
-- ------------------------------------------------------------
|
||
-- HQ spawn lookup (used by client to teleport on respawn)
|
||
-- Config.JOIN_POINTS is an ARRAY (you use ipairs on client)
|
||
-- ------------------------------------------------------------
|
||
RegisterNetEvent('turfwar:getMyHQSpawn', function()
|
||
local src = source
|
||
local gangId = tonumber(PlayerGang[src]) or 0
|
||
|
||
local function findJoinPoint(forGang)
|
||
for i, jp in ipairs(Config.JOIN_POINTS or {}) do
|
||
if tonumber(jp.gangId) == tonumber(forGang) then
|
||
return jp, i
|
||
end
|
||
end
|
||
return nil, nil
|
||
end
|
||
|
||
local jp, idx = findJoinPoint(gangId)
|
||
if not jp then
|
||
print(("[turfwar] getMyHQSpawn src=%d gangId=%s -> NOT FOUND, fallback to 0"):format(src, tostring(gangId)))
|
||
gangId = 0
|
||
jp, idx = findJoinPoint(0)
|
||
end
|
||
|
||
if not jp or not jp.pos then
|
||
print(("[turfwar] getMyHQSpawn src=%d gangId=%s -> NO JOIN POINT / NO POS"):format(src, tostring(gangId)))
|
||
TriggerClientEvent('turfwar:myHQSpawn', src, nil)
|
||
return
|
||
end
|
||
|
||
local v = jp.pos
|
||
print(("[turfwar] getMyHQSpawn src=%d gangId=%d jpIndex=%s -> %.2f %.2f %.2f h=%.2f")
|
||
:format(src, gangId, tostring(idx), v.x, v.y, v.z, v.w or 0.0))
|
||
|
||
TriggerClientEvent('turfwar:myHQSpawn', src, {
|
||
x = v.x, y = v.y, z = v.z,
|
||
h = v.w or 0.0,
|
||
gangId = gangId
|
||
})
|
||
end)
|
||
|
||
-- ------------------------------------------------------------
|
||
-- Turfs runtime state (GLOBAL for other server files if needed)
|
||
-- ------------------------------------------------------------
|
||
Turfs = Turfs or {}
|
||
|
||
local function vecToTable(v)
|
||
return { x = v.x, y = v.y, z = v.z }
|
||
end
|
||
|
||
local function initTurfsFromConfig()
|
||
Turfs = {}
|
||
|
||
if not Config or not Config.TURFS then
|
||
print("^1[turfwar]^7 Config.TURFS missing/empty")
|
||
return
|
||
end
|
||
|
||
for turfId, t in pairs(Config.TURFS) do
|
||
Turfs[turfId] = {
|
||
name = t.name or turfId,
|
||
center = vecToTable(t.center),
|
||
radius = tonumber(t.radius) or 80.0,
|
||
owner = tonumber(t.owner) or 0,
|
||
progress = 0,
|
||
contestingGang = 0,
|
||
guardsAlive = false,
|
||
paused = false,
|
||
lastActivity = 0,
|
||
}
|
||
end
|
||
|
||
-- Load persisted owners into runtime if payout module provides it
|
||
if TurfwarPayouts and TurfwarPayouts.LoadTurfOwnersIntoRuntime then
|
||
TurfwarPayouts.LoadTurfOwnersIntoRuntime(Turfs)
|
||
end
|
||
|
||
local count = 0
|
||
for _ in pairs(Turfs) do count = count + 1 end
|
||
print(("^2[turfwar]^7 Turfs loaded: %d"):format(count))
|
||
end
|
||
|
||
initTurfsFromConfig()
|
||
|
||
local function snapshotForClient()
|
||
local payload = {}
|
||
for turfId, t in pairs(Turfs) do
|
||
payload[turfId] = {
|
||
name = t.name,
|
||
center = t.center,
|
||
radius = t.radius,
|
||
owner = t.owner,
|
||
progress = t.progress,
|
||
contestingGang = t.contestingGang,
|
||
}
|
||
end
|
||
return payload
|
||
end
|
||
|
||
-- ------------------------------------------------------------
|
||
-- Turf broadcast helpers (were missing before)
|
||
-- ------------------------------------------------------------
|
||
local function broadcastTurfUpdate(turfId)
|
||
local t = Turfs[turfId]
|
||
if not t then return end
|
||
TriggerClientEvent("turfwar:turfUpdate", -1, turfId, t.owner, t.progress, t.contestingGang)
|
||
end
|
||
|
||
local function broadcastTurfCaptured(turfId)
|
||
local t = Turfs[turfId]
|
||
if not t then return end
|
||
TriggerClientEvent("turfwar:turfCaptured", -1, turfId, t.owner)
|
||
end
|
||
|
||
-- ------------------------------------------------------------
|
||
-- Leaderboard (Most influence = most owned turfs)
|
||
-- ------------------------------------------------------------
|
||
local function buildLeaderboard()
|
||
local counts = {} -- [gangId] = owned turf count
|
||
|
||
for _, turf in pairs(Turfs) do
|
||
local owner = tonumber(turf.owner) or 0
|
||
if owner ~= 0 then
|
||
counts[owner] = (counts[owner] or 0) + 1
|
||
end
|
||
end
|
||
|
||
local rows = {}
|
||
for gangId, g in pairs(Config.GANGS or {}) do
|
||
gangId = tonumber(gangId) or 0
|
||
if gangId ~= 0 then
|
||
rows[#rows + 1] = {
|
||
gangId = gangId,
|
||
name = g.name or ("Gang " .. tostring(gangId)),
|
||
value = counts[gangId] or 0,
|
||
rgb = g.rgb or {255,255,255}
|
||
}
|
||
end
|
||
end
|
||
|
||
table.sort(rows, function(a, b)
|
||
if a.value == b.value then return a.name < b.name end
|
||
return a.value > b.value
|
||
end)
|
||
|
||
return { title = "Most influence", rows = rows }
|
||
end
|
||
|
||
local function broadcastLeaderboard(target)
|
||
TriggerClientEvent("turfwar:leaderboard:update", target or -1, buildLeaderboard())
|
||
end
|
||
|
||
RegisterNetEvent("turfwar:leaderboard:request", function()
|
||
local src = source
|
||
broadcastLeaderboard(src)
|
||
end)
|
||
|
||
CreateThread(function()
|
||
local announced = false
|
||
while true do
|
||
local interval = tonumber(Config?.INCOME?.intervalMinutes) or 60
|
||
if interval < 0.1 then interval = 0.1 end
|
||
|
||
if not announced then
|
||
print(("^3[turfwar]^7 Payout scheduler running (%.2f min interval)"):format(interval))
|
||
announced = true
|
||
end
|
||
|
||
Wait(math.floor(interval * 60 * 1000))
|
||
|
||
if TurfwarPayouts and TurfwarPayouts.DoPayoutTick then
|
||
TurfwarPayouts.DoPayoutTick(Turfs)
|
||
end
|
||
end
|
||
end)
|
||
|
||
-- ------------------------------------------------------------
|
||
-- Guard host election + guard spawn control
|
||
-- ------------------------------------------------------------
|
||
local GuardHost = nil
|
||
|
||
local function isValidSource(src)
|
||
return src and tonumber(src) and GetPlayerName(src) ~= nil
|
||
end
|
||
|
||
local function ensureGuardHost()
|
||
if isValidSource(GuardHost) then return GuardHost end
|
||
GuardHost = nil
|
||
|
||
for _, src in ipairs(GetPlayers()) do
|
||
local n = tonumber(src)
|
||
if isValidSource(n) then
|
||
GuardHost = n
|
||
print(("^2[turfwar]^7 GuardHost elected: %s"):format(GuardHost))
|
||
break
|
||
end
|
||
end
|
||
|
||
return GuardHost
|
||
end
|
||
|
||
RegisterNetEvent("turfwar:guardsClientReady", function()
|
||
local src = source
|
||
if not isValidSource(GuardHost) then
|
||
GuardHost = src
|
||
print(("^2[turfwar]^7 GuardHost assigned from guardsClientReady: %s"):format(GuardHost))
|
||
|
||
for turfId, t in pairs(Turfs) do
|
||
if t.owner ~= 0 then
|
||
local cfg = Config.TURFS and Config.TURFS[turfId]
|
||
if cfg and (tonumber(cfg.guardCount) or 0) > 0 then
|
||
t.guardsAlive = true
|
||
TriggerClientEvent("turfwar:spawnGuards", GuardHost, {
|
||
turfId = turfId,
|
||
ownerFaction = t.owner,
|
||
count = tonumber(cfg.guardCount) or 0,
|
||
spawns = cfg.guardSpawns or {},
|
||
model = cfg.guardModel or "g_m_y_lost_01",
|
||
weapon = cfg.guardWeapon or "WEAPON_PISTOL",
|
||
})
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end)
|
||
|
||
local function clearGuardsForTurf(turfId)
|
||
local host = ensureGuardHost()
|
||
if host then
|
||
TriggerClientEvent("turfwar:clearGuards", host, turfId)
|
||
end
|
||
end
|
||
|
||
local function spawnGuardsForTurf(turfId)
|
||
local host = ensureGuardHost()
|
||
if not host then return end
|
||
|
||
local t = Turfs[turfId]
|
||
local cfg = Config.TURFS and Config.TURFS[turfId]
|
||
if not t or not cfg then return end
|
||
|
||
local count = tonumber(cfg.guardCount) or 0
|
||
if t.owner == 0 or count <= 0 then
|
||
t.guardsAlive = false
|
||
clearGuardsForTurf(turfId)
|
||
return
|
||
end
|
||
|
||
t.guardsAlive = true
|
||
TriggerClientEvent("turfwar:spawnGuards", host, {
|
||
turfId = turfId,
|
||
ownerFaction = t.owner,
|
||
count = count,
|
||
spawns = cfg.guardSpawns or {},
|
||
model = cfg.guardModel or "g_m_y_lost_01",
|
||
weapon = cfg.guardWeapon or "WEAPON_PISTOL",
|
||
})
|
||
end
|
||
|
||
RegisterNetEvent("turfwar:guardsEmpty", function(turfId)
|
||
turfId = tostring(turfId)
|
||
if Turfs[turfId] then
|
||
Turfs[turfId].guardsAlive = false
|
||
broadcastTurfUpdate(turfId)
|
||
end
|
||
end)
|
||
|
||
RegisterCommand("tw_respawn_guards", function(src, args)
|
||
local turfId = tostring(args[1] or "")
|
||
if turfId == "" or not Turfs[turfId] then return end
|
||
spawnGuardsForTurf(turfId)
|
||
end, true)
|
||
|
||
RegisterCommand("tw_clear_guards", function(src, args)
|
||
local turfId = tostring(args[1] or "")
|
||
if turfId == "" or not Turfs[turfId] then return end
|
||
Turfs[turfId].guardsAlive = false
|
||
clearGuardsForTurf(turfId)
|
||
end, true)
|
||
|
||
-- ------------------------------------------------------------
|
||
-- Client ready + snapshot
|
||
-- ------------------------------------------------------------
|
||
RegisterNetEvent("turfwar:clientReady", function()
|
||
local src = source
|
||
|
||
TriggerClientEvent("turfwar:snapshot", src, snapshotForClient(), SecondsToCapture)
|
||
|
||
for id, gang in pairs(PlayerGang) do
|
||
TriggerClientEvent("turfwar:playerGang", src, id, tonumber(gang) or 0)
|
||
end
|
||
|
||
TriggerClientEvent("turfwar:playerGang", -1, src, tonumber(PlayerGang[src]) or 0)
|
||
|
||
applyLoadoutSafe(src, tonumber(PlayerGang[src]) or 0)
|
||
|
||
-- send leaderboard to joining client
|
||
broadcastLeaderboard(src)
|
||
end)
|
||
|
||
-- ------------------------------------------------------------
|
||
-- Environment cash drop relay
|
||
-- ------------------------------------------------------------
|
||
RegisterNetEvent("environment:pedCashDrop", function(amount, coords)
|
||
if type(coords) == "table" and coords.x and coords.y and coords.z then
|
||
TriggerClientEvent("environment:spawnCashPickup", -1, amount, coords)
|
||
else
|
||
print("^3[turfwar]^7 environment:pedCashDrop received invalid coords payload")
|
||
end
|
||
end)
|
||
|
||
-- ------------------------------------------------------------
|
||
-- Capture logic (bulletproof)
|
||
-- ------------------------------------------------------------
|
||
local LastPing = {}
|
||
|
||
-- Client-reported movement state (cached server-side)
|
||
PlayerOnFoot = PlayerOnFoot or {} -- [src] = true/false
|
||
RegisterNetEvent("turfwar:setOnFoot", function(onFoot)
|
||
local src = source
|
||
PlayerOnFoot[src] = (onFoot == true)
|
||
end)
|
||
|
||
-- Presence tracking (pause rules)
|
||
local Presence = {} -- Presence[turfId][src] = expiresMs
|
||
local PRESENCE_TTL_MS = 3500
|
||
local CONTEST_IDLE_RESET_MS = 6000 -- if no capture pings for this long, reset contest
|
||
|
||
local function notePresence(turfId, src, gangId)
|
||
if tonumber(gangId) == 0 then return end -- neutral does NOT pause
|
||
Presence[turfId] = Presence[turfId] or {}
|
||
Presence[turfId][src] = GetGameTimer() + PRESENCE_TTL_MS
|
||
end
|
||
|
||
local function cleanupPresence(turfId)
|
||
local now = GetGameTimer()
|
||
local bucket = Presence[turfId]
|
||
if not bucket then return end
|
||
for src, exp in pairs(bucket) do
|
||
if (not exp) or exp < now or (not GetPlayerName(src)) then
|
||
bucket[src] = nil
|
||
end
|
||
end
|
||
end
|
||
|
||
-- =========================================================
|
||
-- Wanted stars on capture (anti-snipe)
|
||
-- =========================================================
|
||
local CAPTURE_WANTED_STARS = 2
|
||
|
||
local function awardWantedToPresence(turfId, stars)
|
||
stars = tonumber(stars) or CAPTURE_WANTED_STARS
|
||
cleanupPresence(turfId)
|
||
|
||
local bucket = Presence[turfId]
|
||
if not bucket then return end
|
||
|
||
local policeGang = (Config.PlayerPolice and tonumber(Config.PlayerPolice.POLICE_GANG_ID)) or 3
|
||
|
||
for src, exp in pairs(bucket) do
|
||
if exp and exp >= GetGameTimer() and GetPlayerName(src) then
|
||
local g = tonumber(PlayerGang[src]) or 0
|
||
if g ~= 0 and g ~= policeGang then
|
||
TriggerClientEvent("turfwar:wanted:setMin", src, stars)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Clients ping this while standing inside a turf zone so defenders also count as "present"
|
||
RegisterNetEvent("turfwar:notePresence", function(turfId)
|
||
local src = source
|
||
turfId = tostring(turfId or "")
|
||
if turfId == "" or not Turfs[turfId] then return end
|
||
|
||
local gangId = tonumber(PlayerGang[src]) or 0
|
||
notePresence(turfId, src, gangId)
|
||
end)
|
||
|
||
local function isCapturePausedByOthers(turfId, capturingGang)
|
||
cleanupPresence(turfId)
|
||
local bucket = Presence[turfId]
|
||
if not bucket then return false end
|
||
|
||
local cg = tonumber(capturingGang) or 0
|
||
for src, _ in pairs(bucket) do
|
||
local g = tonumber(PlayerGang[src]) or 0
|
||
if g ~= 0 and g ~= cg then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
|
||
|
||
-- Hint throttle
|
||
local HintCooldown = {}
|
||
local function sendCaptureHint(src, msg)
|
||
local now = GetGameTimer()
|
||
local nextOk = HintCooldown[src] or 0
|
||
if now < nextOk then return end
|
||
HintCooldown[src] = now + 1500
|
||
TriggerClientEvent("turfwar:captureHint", src, msg)
|
||
end
|
||
|
||
local function canCapture(turfId, gangId)
|
||
local t = Turfs[turfId]
|
||
if not t then return false, "bad_turf" end
|
||
if gangId == 0 then return false, "neutral_player" end
|
||
|
||
local policeGang = (Config.PlayerPolice and tonumber(Config.PlayerPolice.POLICE_GANG_ID)) or 3
|
||
if gangId == policeGang then
|
||
return false, "police_cannot_capture"
|
||
end
|
||
|
||
if t.owner == gangId then return false, "already_owner" end
|
||
if t.owner ~= 0 and t.guardsAlive then return false, "guards_alive" end
|
||
return true, "ok"
|
||
end
|
||
|
||
local function setPausedState(turfId, paused)
|
||
local t = Turfs[turfId]
|
||
if not t then return end
|
||
paused = paused and true or false
|
||
if t.paused == paused then return end -- only broadcast on change
|
||
t.paused = paused
|
||
TriggerClientEvent("turfwar:capturePaused", -1, turfId, paused)
|
||
end
|
||
|
||
local function resetContest(turfId)
|
||
local t = Turfs[turfId]
|
||
if not t then return end
|
||
if (t.contestingGang or 0) == 0 and (t.progress or 0) == 0 then return end
|
||
t.contestingGang = 0
|
||
t.progress = 0
|
||
t.paused = false
|
||
t.lastActivity = 0
|
||
broadcastTurfUpdate(turfId)
|
||
TriggerClientEvent("turfwar:capturePaused", -1, turfId, false)
|
||
end
|
||
|
||
-- cleanup tick so contests can't get stuck forever
|
||
CreateThread(function()
|
||
while true do
|
||
Wait(1500)
|
||
local now = GetGameTimer()
|
||
for turfId, t in pairs(Turfs) do
|
||
if t.contestingGang and t.contestingGang ~= 0 then
|
||
local last = tonumber(t.lastActivity) or 0
|
||
if last > 0 and (now - last) > CONTEST_IDLE_RESET_MS then
|
||
resetContest(turfId)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end)
|
||
|
||
RegisterNetEvent("turfwar:attemptCapture", function(turfId)
|
||
local src = source
|
||
turfId = tostring(turfId)
|
||
|
||
local gangId = tonumber(PlayerGang[src]) or 0
|
||
local t = Turfs[turfId]
|
||
if not t then return end
|
||
|
||
-- Presence: any non-neutral ping counts for pause rules (including police)
|
||
notePresence(turfId, src, gangId)
|
||
|
||
-- Throttle per-player per-turf
|
||
LastPing[src] = LastPing[src] or {}
|
||
local now = GetGameTimer()
|
||
local nextOk = LastPing[src][turfId] or 0
|
||
if now < nextOk then return end
|
||
LastPing[src][turfId] = now + 900
|
||
|
||
if gangId == 0 then return end
|
||
|
||
local policeGang = (Config.PlayerPolice and tonumber(Config.PlayerPolice.POLICE_GANG_ID)) or 3
|
||
if gangId == policeGang then
|
||
-- =========================
|
||
-- POLICE: Restore the peace
|
||
-- =========================
|
||
|
||
-- Nothing to do if already neutral
|
||
if (tonumber(t.owner) or 0) == 0 then
|
||
sendCaptureHint(src, "This area is already neutral.")
|
||
return
|
||
end
|
||
|
||
-- Same guard rule as normal capture (prevents bypassing defenses)
|
||
if t.owner ~= 0 and t.guardsAlive then
|
||
sendCaptureHint(src, "Clear the guards before restoring peace.")
|
||
return
|
||
end
|
||
|
||
-- On-foot enforcement
|
||
if PlayerOnFoot[src] == false then
|
||
sendCaptureHint(src, "Exit your vehicle to restore peace.")
|
||
return
|
||
end
|
||
|
||
-- Mark activity so it doesn't idle-reset
|
||
t.lastActivity = now
|
||
|
||
-- If police take over the contest, reset contest state cleanly
|
||
if (tonumber(t.contestingGang) or 0) ~= policeGang then
|
||
t.contestingGang = policeGang
|
||
t.progress = math.max(1, tonumber(t.progress) or 0)
|
||
t.paused = false
|
||
broadcastTurfUpdate(turfId)
|
||
end
|
||
|
||
-- If other gangs are present, pause (same rule)
|
||
if isCapturePausedByOthers(turfId, t.contestingGang) then
|
||
if (tonumber(t.progress) or 0) < 1 then t.progress = 1 end
|
||
setPausedState(turfId, true)
|
||
broadcastTurfUpdate(turfId)
|
||
return
|
||
else
|
||
setPausedState(turfId, false)
|
||
end
|
||
|
||
-- Progress restore-peace
|
||
t.progress = (tonumber(t.progress) or 0) + 1
|
||
|
||
if t.progress >= PoliceRestoreToNeutral then
|
||
-- Neutralize turf
|
||
t.owner = 0
|
||
t.progress = 0
|
||
t.contestingGang = 0
|
||
t.paused = false
|
||
t.lastActivity = 0
|
||
|
||
-- Persist neutral owner
|
||
if TurfwarPayouts and TurfwarPayouts.SaveTurfOwner then
|
||
TurfwarPayouts.SaveTurfOwner(turfId, 0)
|
||
end
|
||
|
||
-- Clear guards (neutral turf should not have guards)
|
||
t.guardsAlive = false
|
||
clearGuardsForTurf(turfId)
|
||
|
||
-- Broadcast changes
|
||
broadcastTurfCaptured(turfId) -- clients will see owner=0
|
||
broadcastLeaderboard(-1)
|
||
setPausedState(turfId, false)
|
||
else
|
||
broadcastTurfUpdate(turfId)
|
||
end
|
||
|
||
return
|
||
end
|
||
|
||
|
||
-- On-foot enforcement: only block if we KNOW they're in a vehicle
|
||
if PlayerOnFoot[src] == false then
|
||
sendCaptureHint(src, "Exit your vehicle to capture.")
|
||
return
|
||
end
|
||
|
||
local ok, reason = canCapture(turfId, gangId)
|
||
if not ok then
|
||
if reason == "guards_alive" then
|
||
sendCaptureHint(src, "Clear the guards before capturing.")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- Mark activity so contest doesn't stick forever
|
||
t.lastActivity = now
|
||
|
||
-- Start contest if none, or if different gang takes over
|
||
if (tonumber(t.contestingGang) or 0) ~= gangId then
|
||
t.contestingGang = gangId
|
||
t.progress = math.max(1, tonumber(t.progress) or 0) -- ensure >0 for HUD
|
||
t.paused = false
|
||
broadcastTurfUpdate(turfId) -- immediate HUD pop
|
||
end
|
||
|
||
-- Pause if any other non-neutral gang present
|
||
if isCapturePausedByOthers(turfId, t.contestingGang) then
|
||
if (tonumber(t.progress) or 0) < 1 then t.progress = 1 end
|
||
setPausedState(turfId, true)
|
||
broadcastTurfUpdate(turfId)
|
||
return
|
||
else
|
||
setPausedState(turfId, false)
|
||
end
|
||
|
||
-- Progress
|
||
t.progress = (tonumber(t.progress) or 0) + 1
|
||
|
||
if t.progress >= SecondsToCapture then
|
||
-- ✅ Award wanted stars to everyone currently in the zone (based on presence pings)
|
||
awardWantedToPresence(turfId, CAPTURE_WANTED_STARS)
|
||
|
||
t.owner = gangId
|
||
t.progress = 0
|
||
t.contestingGang = 0
|
||
t.paused = false
|
||
t.lastActivity = 0
|
||
|
||
if TurfwarPayouts and TurfwarPayouts.SaveTurfOwner then
|
||
TurfwarPayouts.SaveTurfOwner(turfId, gangId)
|
||
end
|
||
|
||
broadcastTurfCaptured(turfId)
|
||
|
||
-- leaderboard refresh on capture
|
||
broadcastLeaderboard(-1)
|
||
|
||
spawnGuardsForTurf(turfId)
|
||
setPausedState(turfId, false)
|
||
else
|
||
broadcastTurfUpdate(turfId)
|
||
end
|
||
|
||
end)
|
||
|
||
-- ------------------------------------------------------------
|
||
-- Resource start / reload support
|
||
-- ------------------------------------------------------------
|
||
AddEventHandler("onResourceStart", function(res)
|
||
if res ~= GetCurrentResourceName() then return end
|
||
SecondsToCapture = (Config and tonumber(Config.SECONDS_TO_CAPTURE)) or 60
|
||
initTurfsFromConfig()
|
||
|
||
-- leaderboard refresh on resource start
|
||
broadcastLeaderboard(-1)
|
||
end)
|
||
|
||
RegisterCommand("tw_reload_turfs", function(src)
|
||
initTurfsFromConfig()
|
||
TriggerClientEvent("turfwar:snapshot", -1, snapshotForClient(), SecondsToCapture)
|
||
|
||
-- leaderboard refresh on reload
|
||
broadcastLeaderboard(-1)
|
||
end, true)
|
||
|
||
-- ------------------------------------------------------------
|
||
-- Player dropped cleanup
|
||
-- ------------------------------------------------------------
|
||
AddEventHandler("playerDropped", function()
|
||
local src = source
|
||
|
||
PlayerGang[src] = nil
|
||
TriggerClientEvent("turfwar:playerGang", -1, src, -1)
|
||
|
||
if PlayerOnFoot then PlayerOnFoot[src] = nil end
|
||
|
||
if GuardHost == src then
|
||
print("^3[turfwar]^7 GuardHost dropped; will re-elect on next guardsClientReady")
|
||
GuardHost = nil
|
||
end
|
||
|
||
for turfId, bucket in pairs(Presence) do
|
||
if bucket then bucket[src] = nil end
|
||
end
|
||
|
||
LastPing[src] = nil
|
||
HintCooldown[src] = nil
|
||
end)
|