require "vl_quest_utils"
require "vl_quest_item_utils"
require "vl_quest_globalmoddata"
-- require "vl_quest_zombie_npc_utils"

VLZB = VLZB or {}
VLZB.zombieStats = VLZB.zombieStats or {}
VLZB.tickZombieCache = {
    tick = -1,
    list = nil
}

assert(VLIT,  "Stack trace: Missing dependency: VLIT - vl_quest_zombie_npc_utils.lua")
assert(VLF,  "Stack trace: Missing dependency: VLF - vl_quest_zombie_npc_utils.lua")
assert(VLGMD,  "Stack trace: Missing dependency: VLGMD - vl_quest_zombie_npc_utils.lua")

local Caldistance = VLF.Caldistance
local GetQuestGlobalModData = VLGMD.GetQuestGlobalModData
local IsMultiplayerMode = VLF.IsMultiplayerMode
local DelayFunction = VLF.DelayFunction
local GetItem = VLIT.GetItem
local IsMapTableEmpty = VLF.IsMapTableEmpty

--Lấy các thiết lập của ZombieLore lúc ban đầu
local function GetZombieStatsSetting()
		local zombieStats = VLZB.zombieStats
        local sandboxOptions = getSandboxOptions()
		zombieStats.oldSpeed = sandboxOptions:getOptionByName("ZombieLore.Speed"):getValue()
		zombieStats.oldCognition = sandboxOptions:getOptionByName("ZombieLore.Cognition"):getValue()
		zombieStats.oldMemory = sandboxOptions:getOptionByName("ZombieLore.Memory"):getValue()
		zombieStats.oldSight = sandboxOptions:getOptionByName("ZombieLore.Sight"):getValue()
		zombieStats.oldHearing = sandboxOptions:getOptionByName("ZombieLore.Hearing"):getValue()
		zombieStats.oldStrength = sandboxOptions:getOptionByName("ZombieLore.Strength"):getValue()
        zombieStats.oldToughness = sandboxOptions:getOptionByName("ZombieLore.Toughness"):getValue()
end
if not isServer() then
    Events.OnGameStart.Add(GetZombieStatsSetting)
else
    Events.OnServerStarted.Add(GetZombieStatsSetting)
end

--Chưa sử dụng
--Cached cho getZombieList() --Sử dụng chung cho các func get getZombieList() liên tục.
--getZombieList() sẽ chỉ bị lấy 1 lần trong cùng 1 tick
function VLZB.GetZombieListTickCached()
    local cache = VLZB.tickZombieCache
    local tick = math.floor(getTimestampMs() / 16) --lấy phần nguyên. 1000ms chia 16 = 62.5 ~ 1 tick   (60FPS - 1 tick ~ 60ms)
    
    --Khi cùng tick và list đã có
    if cache.tick == tick and cache.list then
        return cache.list
    end

    --Tạo lại cache
    local zombies = getCell():getZombieList()
    local list = {}

    for i = 0, zombies:size() - 1 do
        local z = zombies:get(i)
        if z and not z:isDead() then
            table.insert(list, z)
        end
    end

    cache.tick = tick
    cache.list = list
    return list
end

--Set vũ khí hiển thị lên tay của NPC
function VLZB.NPCSetHandItem(zNPC, isPrimary, item)
    if not zNPC then return end
    Events.OnEquipPrimary.Remove(Fishing.Handler.onEquipPrimary) --Tạm xóa event để setPrimaryHandItem cho NPC --Event này gây lỗi khi setHandsItem
    if isPrimary then
        zNPC:setPrimaryHandItem(item)
    else
        zNPC:setSecondaryHandItem(item)
    end
    Events.OnEquipPrimary.Add(Fishing.Handler.onEquipPrimary) --Bật lại event của Vanilla
end
local NPCSetHandItem = VLZB.NPCSetHandItem

function VLZB.SetZombieInactiveFakeNPC(zombie)
    zombie:setNoTeeth(true)
    zombie:setInvulnerable(true)
    zombie:getEmitter():stopAll()
    --zombie:setPrimaryHandItem(nil)
    --zombie:setSecondaryHandItem(nil)
    NPCSetHandItem(zombie, true, nil)
    NPCSetHandItem(zombie, false, nil)
    zombie:resetEquippedHandsModels()
    zombie:clearAttachedItems()
    --zombie:setDir(IsoDirections.E)
    zombie:setCanWalk(false)
    zombie:setUseless(true)
    --Xóa âm thanh zombie
    zombie:getDescriptor():setVoicePrefix("")
end
 
--Xóa data ModData dùng để spawn NPC  --LoadGridSquare lại thì NPC sẽ spawn lại
function VLZB.DeactiveNPCSpawnedData(qStep, zID)
    if not qStep then return end
    --local questMD = ModData.getOrCreate("VL_Quest")
    local questMD = GetQuestGlobalModData(qStep)
    if questMD.NPC and questMD.NPC[qStep] then
        questMD.NPC[qStep]["spawned"] = nil
        questMD.NPC[qStep]["uid"] = nil
        questMD.NPC[qStep]["zID"] = nil
        questMD.NPC[qStep]["x"] = nil
        questMD.NPC[qStep]["y"] = nil
        questMD.NPC[qStep]["z"] = nil
        if zID then
            questMD.NPCqStep[zID] = nil
        end
        questMD.NPC[qStep]["sameQStep"] = nil
        questMD.NPC[qStep]["turnToZombie"] = nil
        
        --print(type(questMD.NPC[qStep]))
        --kiểm tra questMD.NPC[qStep] nếu rỗng -> clear table data
        if type(questMD.NPC[qStep]) == "table" then
            --if next(questMD.NPC[qStep]) == nil then
            if IsMapTableEmpty(questMD.NPC[qStep]) then
                questMD.NPC[qStep] = nil
            end
        end
        
        if isServer() then
            --TransmitVLQuestModData()
            sendServerCommand("NPCQuestCM_C", "DeactiveNPCSpawnedData_C", {qStep = qStep, zID = zID})
        end
       
    end
end
local DeactiveNPCSpawnedData = VLZB.DeactiveNPCSpawnedData

--Xóa zombie
function VLZB.RemoveZombie(zombie)
    if not zombie then return end
    --zombie:setUseless(true)
    zombie:removeFromSquare()
    zombie:removeFromWorld()
end
local RemoveZombie = VLZB.RemoveZombie

--Kiểm tra NPC hoặc Zombie tồn tại và đang còn sống
function VLZB.IsCharExistAndAlive(character)
    if not character then return false end
    if character:isDead() then return false end
    if character:getHealth() <= 0 then return false end
    --if character:isOnFloor() then return false end
    if not isServer() then
        if character:getCurrentStateName() == "ZombieDieState" then return false end
    end
    return true
end
local IsCharExistAndAlive = VLZB.IsCharExistAndAlive

function VLZB.GetZombieModDataQuest(zombie)
    if not zombie then return end
    local zData = zombie:getModData()
    if not zData.VL then zData.VL = {} end
    return zData.VL
end
local GetZombieModDataQuest = VLZB.GetZombieModDataQuest

--Thiết lập chỉ số của zombie
function VLZB.SetNewZombieStats(zombie, args) --VLZB.SetNewZombieStats(zombie, {speed = 2, strength = 2, toughness = 2, cognition = 3, memory = 2, sight = 2, hearing = 2})
    local speed, cognition, memory, sight, hearing, strength, toughness = args.speed, args.cognition, args.memory, args.sight, args.hearing, args.strength, args.toughness
    local sandboxOptions = getSandboxOptions()
    local zombieStats = VLZB.zombieStats

    if speed then sandboxOptions:set("ZombieLore.Speed", speed) end
    if cognition then sandboxOptions:set("ZombieLore.Cognition", cognition) end --1 open door
    if memory then sandboxOptions:set("ZombieLore.Memory", memory) end
    if sight then sandboxOptions:set("ZombieLore.Sight", sight) end
    if hearing then sandboxOptions:set("ZombieLore.Hearing", hearing) end
    if strength then sandboxOptions:set("ZombieLore.Strength", strength) end
    if toughness then sandboxOptions:set("ZombieLore.Toughness", toughness) end
    zombie:makeInactive(true);
    zombie:makeInactive(false);
    if speed then sandboxOptions:set("ZombieLore.Speed",zombieStats.oldSpeed) end
    if cognition then sandboxOptions:set("ZombieLore.Cognition",zombieStats.oldCognition) end
    if memory then sandboxOptions:set("ZombieLore.Memory",zombieStats.oldMemory) end
    if sight then sandboxOptions:set("ZombieLore.Sight",zombieStats.oldSight) end
    if hearing then sandboxOptions:set("ZombieLore.Hearing",zombieStats.oldHearing) end
    if strength then sandboxOptions:set("ZombieLore.Strength", zombieStats.oldStrength) end
    if toughness then sandboxOptions:set("ZombieLore.Toughness", zombieStats.oldToughness) end
end

--Lấy Online ID hoặc UID từ zombie
function VLZB.GetZombieOnlineIDorUID(zombie)
    if not zombie then return end
    local id
    if IsMultiplayerMode() then
        id = zombie:getOnlineID()
    else
        id = zombie:getUID()
    end
    return id
end
local GetZombieOnlineIDorUID = VLZB.GetZombieOnlineIDorUID


--Lấy Online ID hoặc ID từ zombie --Chỉ dùng khi cần ID là số nguyên INT
function VLZB.GetZombieID(zombie)
    if not zombie then return end
    local id
    if IsMultiplayerMode() then
        id = zombie:getOnlineID()
    else
        id = zombie:getID()
    end
    return id
end

--Lấy Zombie từ ID zombie
function VLZB.GetZombieByOnlineIDOrUID(zID)
    if not zID then return nil end
    local zombies = getCell():getZombieList()
    for i = 0, zombies:size()-1 do
        local zombie = zombies:get(i)
        --if zombie and not zombie:isDead() and zombie:getHealth() > 0 then
        if IsCharExistAndAlive(zombie) then
            --local gameMode = getWorld():getGameMode()
            if IsMultiplayerMode() then
                if zombie:getOnlineID() == zID then return zombie end
            else
                if zombie:getUID() == zID then return zombie end
            end
        end
    end
    return nil
end
local GetZombieByOnlineIDOrUID = VLZB.GetZombieByOnlineIDOrUID
  
--Client Only --set zombie về mặc định, không còn là NPC -dùng khi xóa npc
function VLZB.SetZombieDefault(zombie, isZombieSkin)
    if not zombie then return end
    isZombieSkin = isZombieSkin or false
    
    NPCSetHandItem(zombie, true, nil)
    NPCSetHandItem(zombie, false, nil)
    
    zombie:resetEquippedHandsModels()
    zombie:clearAttachedItems()
    zombie:setNoTeeth(false)
    zombie:setInvulnerable(false)
    zombie:setCanWalk(true)
    zombie:setUseless(false)
    if zombie:isFemale() then
        zombie:getDescriptor():setVoicePrefix("FemaleZombie")
        if isZombieSkin then
            zombie:getHumanVisual():setSkinTextureName("F_ZedBody01_level1")
        end
        
    else
        zombie:getDescriptor():setVoicePrefix("MaleZombie")
        if isZombieSkin then
            zombie:getHumanVisual():setSkinTextureName("M_ZedBody01_level1")
        end
    end
   -- zombie:getHumanVisual():setSkinTextureIndex(2)
    
    local zID = GetZombieOnlineIDorUID(zombie)
    local questMD = GetQuestGlobalModData(nil)
    local qStep = questMD.NPCqStep[zID]
    if qStep then
        zombie:setVariable(VLF.npcData[qStep].variable, false)
    end
    --[[
    --Clear modData của zombie
    if not isServer() then
        local zData = GetZombieModDataQuest(zombie)
        for k, _ in pairs(zData) do
            zData[k] = nil
        end
    end
    ]]
     
    if isServer() then
        sendServerCommand("NPCQuestCM_C", "SetDefaultZombie_C", {zID = zID})
    elseif isClient() then
        sendClientCommand(getPlayer(), "NPCQuestCM", "SetDefaultZombie", {zID = zID})
    end
    
    zombie:resetModelNextFrame()
    zombie:resetModel()
end
local SetZombieDefault = VLZB.SetZombieDefault

--Lấy zombie NPC từ Variable và qStep
function VLZB.GetNPCsByVarAndQStep(variable, qStep)
    local zombies = getCell():getZombieList()
    local zNPCList = {}
    local questMD = GetQuestGlobalModData(qStep)
    for i = 0, zombies:size()-1 do
        local zombie = zombies:get(i)
        --if zombie and not zombie:isDead() and zombie:getHealth() > 0 then
        if IsCharExistAndAlive(zombie) then
            if zombie:getVariableBoolean(variable) then
                local zID = GetZombieOnlineIDorUID(zombie)
                local _qStep = questMD.NPCqStep[zID]
                if _qStep and _qStep == qStep then
                    table.insert(zNPCList, zombie)
                end
            end
        end
    end
    return zNPCList
end
local GetNPCsByVarAndQStep = VLZB.GetNPCsByVarAndQStep

--Lấy tất cả IsoZombie xung quanh 1 square
function VLZB.GetZombiesAroundSquare(square, radius, sameZ) --VLZB.GetZombiesAroundSquare(square, 1, true)
    if not square then return end
    local zombies = getCell():getZombieList()
    local zombieList = {}
    --local radius, sameZ = args.radius or 1, args.sameZ or false
    for i = 0, zombies:size()-1 do
        local zombie = zombies:get(i)
        --if zombie and not zombie:isDead() and zombie:getHealth() > 0 then
        if IsCharExistAndAlive(zombie) then
            local zZ, zSq = math.floor(zombie:getZ()), math.floor(square:getZ())
            if zZ and zSq then
                --nếu cùng tầng z
                if sameZ == true then
                    if zZ == zSq then
                        local dist = Caldistance(zombie, square)
                        if dist <= radius then
                            table.insert(zombieList, zombie)
                        end
                    end
                else
                    --nếu tất cả tầng z
                    local dist = Caldistance(zombie, square)
                    if dist <= radius then
                        table.insert(zombieList, zombie)
                    end
                end
            end
        end
    end
    return zombieList
end

--Kiểm tra có bất kì NPC nào đang chờ spawn không
function VLZB.IsAnyNPCQueueToSpawn()
    local questMD = GetQuestGlobalModData(nil)
    for qStep, val in pairs(questMD.NPCsQueueToSpawn) do
        if val == true then
            return true
        end
    end
    return false
end

--Client Only --Thêm NPC vào bảng chờ spawn
function VLZB.SetNPCStateInQueueSpawnList(args) --VLZB.SetNPCStateInQueueSpawnList({qStep = "q1s004", active = true})
    local qStep = args.qStep
    local active = args.active
    if not qStep then return end
    
    local player = getPlayer()
    sendClientCommand(player, "NPCQuestCM", "ActiveNPCInQueueSpawnCM", {qStep = qStep, active = active})
end
local SetNPCStateInQueueSpawnList = VLZB.SetNPCStateInQueueSpawnList

--Lấy tất cả npcID đang spawned
function VLZB.GetAllnpcIDSpawning()
    local npcIDTab = {}
    local questMD = GetQuestGlobalModData(nil)
    for qStep, data in pairs(questMD.NPC) do
        if data["spawned"] == true then
            table.insert(npcIDTab, data["zID"])
        end
    end
    return npcIDTab
end
 
--Kill zombie
function VLZB.KillZombie(zombie)
    --if zombie and not zombie:isDead() and zombie:getHealth() > 0 then
    if IsCharExistAndAlive(zombie) then
        --zombie:applyDamage(1000000)
        --zombie:setAttackedBy(getCell():getFakeZombieForHit())
        
        zombie:setHealth(0)
        zombie:setAttackedBy(getCell():getFakeZombieForHit())
        local attacker = getCell():getFakeZombieForHit()
        local fakeWeapon = instanceItem("Base.AssaultRifle")
        zombie:DoDeath(fakeWeapon, attacker)
    end
end
local KillZombie = VLZB.KillZombie

--Cập nhật NPC thành NPC qStep tiếp theo --Chỉ dùng khi vị trí NPC không thay đổi
function VLZB.UpdateNPCToNextQStep(zNPC, qStep_Next)
	local questMD = GetQuestGlobalModData(qStep_Next)
	local zID = GetZombieOnlineIDorUID(zNPC)
	questMD.NPC[qStep_Next]["spawned"] = true
	questMD.NPC[qStep_Next]["zID"] = zID
	questMD.NPCqStep[zID] = qStep_Next
	sendClientCommand(getPlayer(), "NPCQuestCM", "UpdateNPCToNextQStep", {qStep_Next = qStep_Next, zID = zID})
    print("Shared---UpdateNPCToNextQStep= ", qStep_Next)
end
local UpdateNPCToNextQStep = VLZB.UpdateNPCToNextQStep

--Cập nhật NPC thành NPC qStep tiếp theo --Chỉ dùng khi vị trí NPC không thay đổi VÀ có cùng Variable
function VLZB.UpdateNPC(npcVariable, qStep, qStep_Next)
    local zNPCList = GetNPCsByVarAndQStep(npcVariable, qStep)
    if zNPCList and zNPCList[1] then
        UpdateNPCToNextQStep(zNPCList[1], qStep_Next)
        local zID = GetZombieOnlineIDorUID(zNPCList[1])
        if zID then
            DeactiveNPCSpawnedData(qStep, zID)
        end
    end
    
    SetNPCStateInQueueSpawnList({qStep = qStep, active = nil})
    SetNPCStateInQueueSpawnList({qStep = qStep_Next, active = true})
    print("Shared---UpdateNPC= ", npcVariable, qStep, qStep_Next)  
end

-------------------------CORPSE---------------------------
local function IsCorpseOnSquare(square)
    local objs = square:getStaticMovingObjects()
    if not objs then return false end
    for i = 0, objs:size()-1 do
        local obj = objs:get(i)
        if obj and instanceof(obj, "IsoDeadBody") then
            return true
        end
    end
    return false
end

--Add xác chết lên square
local function AddCorpse(square, isSplatBlood, isBloodFloor)
    --Thêm xác ngẫu nhiên outfit, ngẫu nhiên giới tính
    local corpse = square:addCorpse(false)
    --square:addCorpse(true) --bộ xương
    if isSplatBlood then
        --square:splatBlood(2, 1) --máu trên tường
    end
    if isBloodFloor then
        --square:haveBloodFloor() --máu dưới sàn
    end
    return corpse
end

--Add xác chết có tùy chỉnh
function VLZB.AddCorpseCustomize(args) --VLZB.AddCorpseCustomize({x = x, y = y, z = z, outfitName = "Police", femaleChance = 100, number = 1})
    local x, y, z = args.x, args.y, args.z
    local cell = getCell()
    local square = cell:getGridSquare(x, y, z)
    --print("Shared-----------VLZB.AddCorpseCustomize= ")
    if square and square:isSolidFloor() then
        local outfitName, femaleChance, number = args.outfitName or "", args.femaleChance or 50, args.number or 1
		local zombies = addZombiesInOutfit(x, y, z, number, outfitName, femaleChance)
		for i = 0, zombies:size() - 1 do
			local zombie = zombies:get(i)
			if zombie then
                KillZombie(zombie)
            end
		end
    end
end

--Add 1 xác chết trên các square isFree
function VLZB.AddCorpseInArea(args) --VLZB.AddCorpseInArea({x = x, y = y, z = z, isSplatBlood = false, isBloodFloor = true, range = 1})
    local x, y, z = args.x, args.y, args.z
    local square = getCell():getGridSquare(x, y, z)
    if square and square:isSolidFloor() and square:isFree(true) and IsCorpseOnSquare(square) == false then
        local corpse = AddCorpse(square, args.isSplatBlood, args.isBloodFloor)
        return corpse
    end
    
    local range = args.range or 0
    --nếu không có square trên, kiểm tra square xung quanh
    for i = -range, range do
        for j = -range, range do
            local newX, newY = x + i, y + j
            local newSq = getCell():getGridSquare(newX, newY, z)
            if newSq and newSq:isSolidFloor() and newSq:isFree(true) and IsCorpseOnSquare(newSq) == false then
                local corpse = AddCorpse(newSq, args.isSplatBlood, args.isBloodFloor)
                return corpse
            end
        end
    end
    return nil
end

--Xóa NPC
function VLZB.RemoveNPCQuest(zNPC)
    if not zNPC then return end
    
    if not isServer() then  --SP + Client
        local zID = GetZombieOnlineIDorUID(zNPC)
        local questMD = GetQuestGlobalModData(nil)
        local qStep = questMD.NPCqStep[zID]
        
        SetZombieDefault(zNPC)
        RemoveZombie(zNPC)
        DeactiveNPCSpawnedData(qStep, zID)
        sendClientCommand(getPlayer(), "NPCQuestCM", "RemoveNPC", {zID = zID, qStep = qStep})
    end
    
    if isServer() then
        SetZombieDefault(zNPC)
        RemoveZombie(zNPC)
    end
end
local RemoveNPCQuest = VLZB.RemoveNPCQuest

--Kill NPC và xóa Data của NPC này
function VLZB.KillNPCQuest(qStep, zID, isZombieSkin)
    if not zID or not qStep then return end
    isZombieSkin = isZombieSkin or false
    
    local zNPC = GetZombieByOnlineIDOrUID(zID)
    local player = getPlayer()
    
    if IsCharExistAndAlive(zNPC) then
        --Kill NPC
        --if not isServer() then 	--SP + Client
            SetZombieDefault(zNPC, isZombieSkin)
        --end
        zNPC:setHealth(0)
        zNPC:setAttackedBy(getCell():getFakeZombieForHit())
        local attacker = getCell():getFakeZombieForHit()
        local fakeWeapon = instanceItem("Base.Hammer")
        zNPC:DoDeath(fakeWeapon, attacker)
        
        --Remove data
        DeactiveNPCSpawnedData(qStep, zID)
        
        if isClient() then
            sendClientCommand(player, "NPCQuestCM", "KillNPC", {qStep = qStep, zID = zID})
        end
        
        --Remove queue list
        if not isServer() then 	--SP + Client
            sendClientCommand(player, "NPCQuestCM", "ActiveNPCInQueueSpawnCM", {qStep = qStep, active = nil})
        end
    end
end
local KillNPCQuest = VLZB.KillNPCQuest

--Client Only --Spawn NPC từ data của NPC
function VLZB.SpawnNPCOnClient(qStep, removeZRange)
    if not qStep then return end
    local npcData = VLF.npcData
    if npcData and npcData[qStep] then
        removeZRange = removeZRange or 0
        sendClientCommand(getPlayer(), "NPCQuestCM", "RequestSpawnNPC", {qStep = qStep, x = npcData[qStep].startX, y = npcData[qStep].startY, z = npcData[qStep].startZ, removeZRange = removeZRange, femaleChance = npcData[qStep].femaleChance})
    else
        print("Shared---SpawnNPCOnClient---npcData == nil")
    end
end
local SpawnNPCOnClient = VLZB.SpawnNPCOnClient

--Xóa NPC và spawn lại NPC đó với qStep mới
function VLZB.RemoveAndRespawnNPC(zNPC, qStep, qStep_Next)
	RemoveNPCQuest(zNPC)
	SetNPCStateInQueueSpawnList({qStep = qStep, active = nil})
	SetNPCStateInQueueSpawnList({qStep = qStep_Next, active = true})
	SpawnNPCOnClient(qStep_Next, 0)
end

--Gây sát thương lên zombie --Base.AssaultRifle damage trung bình 4.3
function VLZB.DoDamageOnZombie(wielder, zombie, handWeapon, damage)
    --if zombie and not zombie:isDead() and zombie:getHealth() > 0 and damage then
    
    --Server + SP thì tính sát thương, Client thì không có sát thương + chỉ animation
    if IsCharExistAndAlive(zombie) then
        damage = tonumber(damage) or 0
        if isClient() then
            zombie:hitConsequences(handWeapon, wielder, true, 0, true)  --bRemote =  true -> Hit này đến từ network, đã xử lý rồi
        else    --SP + Server
            zombie:hitConsequences(handWeapon, wielder, false, damage, false)
        end
    end
end
local DoDamageOnZombie = VLZB.DoDamageOnZombie
 
--Kiểm tra zombie có phải NPC không
function VLZB.IsNPC(zombie)
    if zombie and instanceof(zombie, "IsoZombie") then
        local zID = GetZombieOnlineIDorUID(zombie)
        local questMD = GetQuestGlobalModData(nil)
        local qStep = questMD.NPCqStep[zID]
        if qStep then
            return true
        end
    end
    return false
end
local IsNPC = VLZB.IsNPC

--Get tất cả zombie không phải NPC
function VLZB.GetAllZombiesNotNPC()
    local zombies = getCell():getZombieList()
    local zombieList = {}
    for i = 0, zombies:size()-1 do
        local zombie = zombies:get(i)
        --if zombie and not zombie:isDead() and not IsNPC(zombie) then
        if IsCharExistAndAlive(zombie) and not IsNPC(zombie) then
            table.insert(zombieList, zombie)
        end
        --print("NPC check:", zombie, zombie:getX(), zombie:getY(), zombie:getZ(), IsNPC(zombie))
    end
    return #zombieList > 0 and zombieList or nil
end
local GetAllZombiesNotNPC = VLZB.GetAllZombiesNotNPC

--Lấy zombie có khoảng cách gần nhất với NPC từ danh sách zombies VÀ trong khoảng cách nhất định
function VLZB.FindNearestZombieNotNPCInRange(character, minDist, maxDist)
    if not character then return nil end
    
    local zombieList = GetAllZombiesNotNPC()
    if not zombieList then return nil end
    
    if minDist < 0 then minDist = 0 end
    if maxDist <= minDist then return nil end
    
    local minDistSq = minDist * minDist
    local maxDistSq = maxDist * maxDist
    local nearest = nil
    local bestDistSq = maxDistSq 
    local nX, nY, nZ = character:getX(), character:getY(), character:getZ()

    for _, zombie in ipairs(zombieList) do
        --if zombie and not zombie:isDead() and zombie:getHealth() > 0 then
        if IsCharExistAndAlive(zombie) then
            local zX, zY, zZ = zombie:getX(), zombie:getY(), zombie:getZ()
            local dX = zX - nX
            local dY = zY - nY
            local dZ = zZ - nZ

            local dSq = dX*dX + dY*dY + dZ*dZ
            
            if dSq >= minDistSq and dSq <= maxDistSq then
                if dSq < bestDistSq then
                    bestDistSq = dSq
                    nearest = zombie
                end
            end
        end
    end
    local realBestDistSq = math.sqrt(bestDistSq)
    print("Shared---FindNearestZombieNotNPCInRange= ", nearest, math.floor(realBestDistSq), nearest:getX(), nearest:getY(), nearest:getZ())
    return nearest, realBestDistSq
end

--Lấy tất cả zombie nằm trong khoảng cách minDist - maxDist từ danh sách zombieList
function VLZB.GetZombiesNotNPCInRange(character, minDist, maxDist)
    if not character then return {} end
    
    local zombieList = GetAllZombiesNotNPC()
    if not zombieList then return {} end
    
    if minDist < 0 then minDist = 0 end
    if maxDist <= minDist then return {} end

    local minDistSq = minDist * minDist
    local maxDistSq = maxDist * maxDist

    local result = {}

    local nX, nY, nZ = character:getX(), character:getY(), character:getZ()

    for _, zombie in ipairs(zombieList) do
        --if zombie and not zombie:isDead() and zombie:getHealth() > 0 then
        if IsCharExistAndAlive(zombie) then
            local dX = zombie:getX() - nX
            local dY = zombie:getY() - nY
            local dZ = zombie:getZ() - nZ

            local dSq = dX*dX + dY*dY + dZ*dZ --khoảng cách bình phương

            if dSq >= minDistSq and dSq <= maxDistSq then
                table.insert(result, {zombie = zombie, dist = math.sqrt(dSq)}) --khoảng cách thật
            end
        end
    end

    return result
end

--Lấy tất cả zombie trong khu vực
function VLZB.GetZombiesInArea(x1, y1, x2, y2, z)
    local zombies = getCell():getZombieList()
    local zombieList = {}
    for i = 0, zombies:size()-1 do
        local zombie = zombies:get(i)
        if IsCharExistAndAlive(zombie) then
            local zX, zY, zZ = math.floor(zombie:getX()), math.floor(zombie:getY()), math.floor(zombie:getZ())
            if (zX >= x1 and zX <= x2) and (zY >= y1 and zY <= y2) and (zZ == z) then
                table.insert(zombieList, zombie)
            end
        end
    end
    return zombieList
end
local GetZombiesInArea = VLZB.GetZombiesInArea

--Xóa tất cả zombie trong khu vực ngoại trừ NPC
function VLZB.RemoveZombiesInAreaExceptNPC(x1, y1, x2, y2, z)
    local zombieList = GetZombiesInArea(x1, y1, x2, y2, z)
    if #zombieList > 0 then
        for i = #zombieList, 1, -1 do
            local zombie = zombieList[i]
            if not IsNPC(zombie) then
                if IsCharExistAndAlive(zombie) then
                    RemoveZombie(zombieList[i])
                end
            end
        end
    end
    print("Shared---RemoveZombiesInAreaExceptNPC---Removed= ", x1, y1, x2, y2, z)
end
local RemoveZombiesInAreaExceptNPC = VLF.RemoveZombiesInAreaExceptNPC

--Xóa tất cả NPC không phải NPC trong phòng
function VLZB.RemoveZombiesNotNPCInRoom(room)
    if not room or not instanceof(room, "IsoRoom") then return end
    
	local roomDef = room:getRoomDef()
	local x1, y1, x2, y2, z = roomDef:getX(), roomDef:getY(), roomDef:getX2(), roomDef:getY2(), roomDef:getZ()
    RemoveZombiesInAreaExceptNPC(x1, y1, x2, y2, z)
end
 
--Làm giả hành động bắn Rifle và gây thương tổn lên Zombie
function VLZB.NpcDoRifleShot(zNPC, target, wpnFullType, bumpType, bumpTypeCase, delay, damage, isHit)
    --if zNPC and target and not zNPC:isDead() and not target:isDead() and target:getHealth() > 0 then
    if IsCharExistAndAlive(zNPC) and IsCharExistAndAlive(target) then
        local wpnInfo
        local weapon = GetItem(wpnFullType, "HandWeapon")
        if weapon then
            wpnInfo = VLIT.GetWeaponInfo(weapon)
        end

        --local zData = GetZombieModDataQuest(zNPC)
        local obj = zNPC:getCurrentSquare() and zNPC:getCurrentSquare():getFloor() or nil
        
        if isServer() then --Server
            if IsCharExistAndAlive(zNPC) and IsCharExistAndAlive(target) then
                --FIXME:addsound trong các trường hợp jam và lắp đạn?
                addSound(obj, zNPC:getX(), zNPC:getY(), zNPC:getZ(), wpnInfo.soundRadius, wpnInfo.soundVolume) --thu hút zombie
                --FIXME: dodamage zombie ở đây?
                if isHit == true then
                    DoDamageOnZombie(zNPC, target, weapon, damage)
                end
            end            
        end
        
        if not isServer() then  --SP + Client
            if zNPC:getActionStateName() == "idle" then
                zNPC:faceThisObject(target)
                if IsCharExistAndAlive(zNPC) and IsCharExistAndAlive(target) then
                    if zNPC:getActionStateName() == "idle" then
                    
                        zNPC:setBumpType(bumpType) 
                        
                        if bumpType == "vlRifleShoot" then --shoot
                            zNPC:playSound(wpnInfo.swingSound)
                            if not isClient() then --SP
                                addSound(obj, zNPC:getX(), zNPC:getY(), zNPC:getZ(), wpnInfo.soundRadius, wpnInfo.soundVolume)
                            end
                            --zNPC:setBumpType(bumpType)         --FIXME: mỗi bumpType sẽ có sound khác nhau                
                            --Tóe lửa khi bắn súng. b42 đang làm lại startMuzzleFlash nên dùng tạm cái dưới
                            --getEffectsManager():startMuzzleFlash(zNPC, 1)
                            local square = zNPC:getSquare()
                            local lightSource = IsoLightSource.new(square:getX(), square:getY(), square:getZ(), 0.8, 0.8, 0.7, 18, 2)
                            getCell():addLamppost(lightSource)
                            
                            --print("--------isHit = ", isHit)
                            if isHit == true then
                                DoDamageOnZombie(zNPC, target, weapon, damage)
                            end
                        elseif bumpType == "vlRifleShootEmpty" then
                            if bumpTypeCase then
                                if bumpTypeCase == "Rack" then   --Jammed
                                    zNPC:setVariable("vlVarRifleRack", true)
                                elseif bumpTypeCase == "Reload" then    --Reload
                                    zNPC:setVariable("vlVarRifleReload", true)
                                end
                            end
                        end
                    end
                end
                --zData.requestedNPCShootZ = nil  --reset data trên client để NPC tìm mục tiêu mới
           
            --else
                --zData.requestedNPCShootZ = nil
            end
            --zData.requestedNPCShootZ = nil
        end
    end
    
    if not isServer() then
        local zData = GetZombieModDataQuest(zNPC)
        zData.requestedNPCShootZ = nil
    end
end

--Lấy thời gian cooldown từ modData zombie và WorldAge theo phút hoặc tiếng
function VLZB.RemainingZCooldownByWorldAge(zombie, mDataKey, delay, timeUnit) --VLZB.ZombieCooldownByWorldAge(zombie, "cooldownQ1S000", 60, "minutes")
    if not zombie or not delay or delay <= 0 then return 0 end

    local now
    local gameTime = getGameTime()
    if timeUnit == "hours" then
        now = gameTime:getWorldAgeHours()
    else
        --default: minutes
        now = gameTime:getMinutesStamp()
    end
        
    local zData = GetZombieModDataQuest(zombie)
    if not zData[mDataKey] then
        zData[mDataKey] = now
        return delay
    end
    
    local passed = now - zData[mDataKey]
    local remaining = delay - passed
    
    print("Shared---ZombieCooldownByWorldAge= ", remaining, delay)
    if remaining <= 0 then
        return 0
    end
    return remaining
end

--Set ModData cho NPC vừa được spawn
function VLZB.ActiveNPCSpawnedData(x, y, z, qStep, zID, zUID)
    if not x or not y or not z or not qStep or not zID then return end
	local questMD = GetQuestGlobalModData(qStep)
	questMD.NPC[qStep]["spawned"] = true --kích hoạt NPC hiện hay mất
	questMD.NPC[qStep]["zID"] = zID --dùng xác định zombie nào là NPC
	questMD.NPC[qStep]["x"] = x
	questMD.NPC[qStep]["y"] = y
	questMD.NPC[qStep]["z"] = z
	questMD.NPCqStep[zID] = qStep --dùng lấy qStep
    if not isClient() then
        questMD.NPC[qStep]["uid"] = zUID --dùng cho server kiểm tra zombie tồn tại không thì xóa spawned, chỉ dùng UID vì tính duy nhất
    end
end

--Spawn NPC đã bị biến thành zombie
function VLZB.AddNPCZombified(qStep, isCrawler, setPosX, setPosY, setPosZ)		
    if not qStep then return end
    isCrawler = isCrawler or false
    
    --add NPC
    SpawnNPCOnClient(qStep, 0)
    
    --biến NPC thành zombie
    local function onTick()
        local questMD = GetQuestGlobalModData(qStep)
        local zID = questMD.NPC[qStep]["zID"]
        local zNPC = GetZombieByOnlineIDOrUID(zID)
        if zNPC then
            if zNPC:getVariableBoolean(VLF.npcData[qStep].variable) then    --đảm bảo NPC đã được set outfit
                --Nếu có setPosX thì thay đổi vị trí của NPC theo setPos XYZ
                if setPosX then
                    zNPC:setPosition(setPosX, setPosY, setPosZ)
                end
                SetZombieDefault(zNPC, true)
                DeactiveNPCSpawnedData(qStep, zID)
                
                --Nếu isCrawler zombie
                if isCrawler == true then
                    zNPC:setCanWalk(false)
                    zNPC:setKnockedDown(true)
                    zNPC:setFallOnFront(true)
                    zNPC:setCrawler(true)
                end
                print("Shared---AddNPCZombified= ", qStep)
                zNPC:setUseless(false)
                Events.OnTick.Remove(onTick)
            end
        end
    end
    Events.OnTick.Add(onTick)
end


--Spawn NPC đã bị biến thành zombie
function VLZB.AddNPCCorpse(qStep, isZombieSkin, setPosX, setPosY, setPosZ, removeZRange)		
    if not qStep then return end
    isZombieSkin = isZombieSkin or false
    removeZRange = removeZRange or 0
    
    --add NPC
    SpawnNPCOnClient(qStep, removeZRange)
    
    --biến NPC thành xác
    local function onTick()
        local questMD = GetQuestGlobalModData(qStep)
        local zID = questMD.NPC[qStep]["zID"]
        local zNPC = GetZombieByOnlineIDOrUID(zID)
        if zNPC then
            if zNPC:getVariableBoolean(VLF.npcData[qStep].variable) then    --đảm bảo NPC đã được set outfit
                --Nếu có setPosX thì thay đổi vị trí của NPC theo setPos XYZ
                if setPosX then
                    zNPC:setPosition(setPosX, setPosY, setPosZ)
                end
                KillNPCQuest(qStep, zID, isZombieSkin)
               

                print("Shared---AddNPCCorpse= ", qStep)
                Events.OnTick.Remove(onTick)
            end
        end
    end
    Events.OnTick.Add(onTick)
end

-- Add zombies quanh XY, spawn từ gần → xa
function VLZB.SpawnZombiesAroundXY(x, y, z, count, outfit, femaleChance, range)
	if not x or not y or not z then return 0 end

	count = count or 1
	outfit = outfit or "Generic03"
	femaleChance = femaleChance or 50
	range = range or 1

	local spawned = 0
	local cell = getCell()

	-- duyệt từ gần → xa
	for r = 1, range do
		if spawned >= count then break end

		-- số lần thử cho mỗi bán kính
		local tries = math.max(4, r * 4)

		for i = 1, tries do
			if spawned >= count then break end

			local rx = x + ZombRand(-r, r + 1)
			local ry = y + ZombRand(-r, r + 1)

			local square = cell:getGridSquare(rx, ry, z)
			if square and square:isOutside() and square:isFree(true) then
				addZombiesInOutfit(rx, ry, z, 1, outfit, femaleChance)
				spawned = spawned + 1
			end
		end
	end

	return spawned
end







