Turfwar/server/main.lua

824 lines
27 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- server/main.lua
-- Turfwar (single resource) - Turfs + Capture + Guard host + PlayerGang broadcast
-- Works with the guards.lua youre 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)