--HÀM TIỆN ÍCH, SỬ DỤNG CHUNG

VLF = VLF or {}
VLF.var = VLF.var or {}
VLF.var.freeNPCsUpdated = false
VLF.var.requestCol = VLF.var.requestCol or {}
VLF.var.doAction = {}

VLOCC = VLOCC or {}
VLOSC = VLOSC or {}

VLINTER = VLINTER or {}
VLINTER.Rewards = VLINTER.Rewards or {}
VLINTER.SetButtons = VLINTER.SetButtons or {}
VLINTER.InteractionButton1 = VLINTER.InteractionButton1 or {}
VLINTER.InteractionButton2 = VLINTER.InteractionButton2 or {}
VLINTER.InteractionButton3 = VLINTER.InteractionButton3 or {}
VLINTER.InteractionButton4 = VLINTER.InteractionButton4 or {}
VLINTER.InteractionButton5 = VLINTER.InteractionButton5 or {}

VLF.var.itemList = {}
local VLF_var_itemList = VLF.var.itemList
VLF_var_itemList["gasoline"] = {}		--1
VLF_var_itemList["water"] = {}			--1
VLF_var_itemList["foodItems"] = {}		--25 item
VLF_var_itemList["smokeItems"] = {}		--6
VLF_var_itemList["rangedWpnItems"] = {}	--14
VLF_var_itemList["ammoItems"] = {}		--8
VLF_var_itemList["meleeWpnItems"] = {}	--142
VLF_var_itemList["weaponPartItems"] = {}	--11
VLF_var_itemList["explosivesItems"] = {}	--33
VLF_var_itemList["skillBookItems"] = {}		--215
VLF_var_itemList["medicalItems"] = {}		--9

VLF.var.npcVariableList = {"npcQuestIdle", "npcQuestRifleAiming", "npcQuestSleepOnBed"}
 
--Danh sách các tạo độ để loại trừ khỏi các sự kiện
--Là tạo độ của các building được lấy từ VLF.GetStaticCoordOfBuilding(buildingDef)
VLF.var.ignoredPositionList = {
	{x = 10749, y = 9413},		--Bob and Kate's House
	{x = 10641, y = 10405},		--Phòng thí nghiệm MUL
	{x = 10648, y = 10410},		--Đồn cảnh sát phía trên phòng thí nghiệm MUL
}

local firstAidInNormalCategory = {
	["BandageBox"] = true,
	["Disinfectant"] = true,
	["AdhesiveBandageBox"] = true,
	["AntibioticsBox"] = true,
	["CottonBallsBox"] = true,
	["SutureNeedleBox"] = true,
	["Coldpack"] = true,
	["ColdpackBox"] = true
}

--Lấy dữ liệu cho VLF.var.itemList
local function OnGameStart_GetItems()
    local items = getScriptManager():getAllItems()

    for i = 0, items:size() - 1 do
        local item = items:get(i)

		--if instanceof(item, "Food")  then
		if item:isItemType(ItemType.FOOD) then
			local itemFName = item:getFullName()
			
			--Smoking	--thuốc lá, cigar
			if item:hasTag(ItemTag.SMOKABLE) or item:getEatType() == "Pipe" or item:getEatType() == "Cigarettes" then
				VLF_var_itemList["smokeItems"][#VLF_var_itemList["smokeItems"] + 1] = itemFName
			
			--Các loại thức ăn đóng hộp chưa mở		--total: 25
			elseif string.find(item:getName(), "Canned") and not string.find(item:getName(), "Open") then
				VLF_var_itemList["foodItems"][#VLF_var_itemList["foodItems"] + 1] = itemFName
			end
			
			--Medical item Antibiotics
			if item:getDisplayCategory() == "FirstAid" then
				if itemFName == "Base.Antibiotics" then
					VLF_var_itemList["medicalItems"][#VLF_var_itemList["medicalItems"] + 1] = itemFName
				end
			end

		--Vũ khí
		elseif item:isItemType(ItemType.WEAPON) then
			if item:getDisplayCategory() == "Weapon" then 	--không dùng thì lấy cả BrokenWeapon
				if item:hasTag(ItemTag.FIREARM) then
					VLF_var_itemList["rangedWpnItems"][#VLF_var_itemList["rangedWpnItems"] + 1] = item:getFullName()
				end
			
			elseif item:getDisplayCategory() == "Explosives" then
				VLF_var_itemList["explosivesItems"][#VLF_var_itemList["explosivesItems"] + 1] = item:getFullName()
			
			elseif item:getDisplayCategory() ~= "BrokenWeapon" then
				if item:containsWeaponCategory(WeaponCategory.LONG_BLADE) then
					VLF_var_itemList["meleeWpnItems"][#VLF_var_itemList["meleeWpnItems"] + 1] = item:getFullName()
				elseif item:containsWeaponCategory(WeaponCategory.AXE) then
					VLF_var_itemList["meleeWpnItems"][#VLF_var_itemList["meleeWpnItems"] + 1] = item:getFullName()
				elseif item:containsWeaponCategory(WeaponCategory.BLUNT) then
					VLF_var_itemList["meleeWpnItems"][#VLF_var_itemList["meleeWpnItems"] + 1] = item:getFullName()
				end
			end
			
		--Phụ kiện súng
		elseif item:isItemType(ItemType.WEAPON_PART) then
			VLF_var_itemList["weaponPartItems"][#VLF_var_itemList["weaponPartItems"] + 1] = item:getFullName()
			
		--Sách kĩ năng
		elseif item:isItemType(ItemType.LITERATURE) then
			if item:getDisplayCategory() == "SkillBook" then
				VLF_var_itemList["skillBookItems"][#VLF_var_itemList["skillBookItems"] + 1] = item:getFullName()
			end
			
		elseif item:isItemType(ItemType.NORMAL) then
			--Đạn
			if item:getDisplayCategory() == "Ammo" and string.find(item:getName(), "Box") then
				if item:getDoubleClickRecipe() then		--có bất kì giá trị là được
					VLF_var_itemList["ammoItems"][#VLF_var_itemList["ammoItems"] + 1] = item:getFullName()
				end
			end

			--Medical items trong kiểu item Normal
			if item:getDisplayCategory() == "FirstAid" then
				local iName = item:getName()
				if firstAidInNormalCategory[iName] == true then
					VLF_var_itemList["medicalItems"][#VLF_var_itemList["medicalItems"] + 1] = item:getFullName()
				end
			end
			
		--Thuốc men
		elseif item:isItemType(ItemType.DRAINABLE) then
			if item:getDisplayCategory() == "FirstAid" then
				local itemFName = item:getFullName()
				if itemFName ~= "Base.Tissue" then
					VLF_var_itemList["medicalItems"][#VLF_var_itemList["medicalItems"] + 1] = itemFName
				end
			end
		end
    end

	VLF_var_itemList["gasoline"][1] = "Base.PetrolCan"
	VLF_var_itemList["water"][1] = "Base.WaterBottle"
	
	--[[
	local y = 0
	for i = 1, #VLF_var_itemList["medicalItems"] do
		print(VLF_var_itemList["medicalItems"][i])
		y = y + 1
	end
	print("-----------TOTAL = ", y)
	]]
end
--OnGameStart_GetItems()
Events.OnGameStart.Remove(OnGameStart_GetItems)
Events.OnGameStart.Add(OnGameStart_GetItems)

--Kiểm tra có phải chế độ chơi Multiplayer hay không
function VLF.IsMultiplayerMode()
    return isClient() or isServer()
end
local IsMultiplayerMode = VLF.IsMultiplayerMode

local function IsCorpseOnSquare(square)
    local objs = square:getStaticMovingObjects()
    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

function VLF.PlaySound(character, soundName)
	local emitter = character:getEmitter()
	if not emitter then return nil end
	
	if emitter:isPlaying(soundName) then return nil end
	
    local soundHandle = emitter:playSound(soundName)
	return soundHandle
end

function VLF.StopSound(character, soundName)
    local emitter = character:getEmitter()
	if not emitter or not emitter:isPlaying(soundName) then return end
	character:getEmitter():stopSoundByName(soundName)
end

function VLF.PlaySoundFadeIn(character, soundName, fadeTicks)
    local emitter = character:getEmitter()
    if not emitter then return end

    fadeTicks = fadeTicks or 120
    local _tick = 0

    -- Play sound, lấy handle
    local soundHandle = emitter:playSound(soundName)
    if not soundHandle then return end

    -- Set volume ban đầu = 0
    emitter:setVolume(soundHandle, 0)

    local function fadeIn()
        _tick = _tick + 1
        local volume = _tick / fadeTicks

        emitter:setVolume(soundHandle, volume)

        if _tick >= fadeTicks then
            emitter:setVolume(soundHandle, 1)
            Events.OnTick.Remove(fadeIn)
        end
    end

    Events.OnTick.Add(fadeIn)

    return soundHandle
end

function VLF.StopSoundFadeout(character, soundHandle, fadeTicks)
	local emitter = character:getEmitter()
	if not emitter then return end
	
	fadeTicks = fadeTicks or 120
	local _tick = 0

	local function fadeOut()
		_tick = _tick + 1
		local volume = 1 - (_tick / fadeTicks)

		emitter:setVolume(soundHandle, volume)

		if _tick >= fadeTicks then
			emitter:stopSound(soundHandle)
			Events.OnTick.Remove(fadeOut)
		end
	end
	Events.OnTick.Add(fadeOut)
end

--Chia String thành các dòng theo số lượng ký tự. --Tiếng Việt tính cả dấu là 1 ký tự
function VLF.SplitTextByLength(inputText, maxLength, font)
    local lines = {}       -- mảng kết quả (các dòng)
    local currentLine = "" -- dòng hiện tại
    --local count = 0        -- số ký tự hiện tại

    --function utf8_iter(str)
    --    return string.gmatch(str, "[%z\1-\127\194-\244][\128-\191]*")
    --end

    local lastSpacePos = nil
    for word in string.gmatch(inputText, ".") do
        local testLine = currentLine .. word
        local width = getTextManager():MeasureStringX(font, testLine)
        if word == " " then
            lastSpacePos = #currentLine
        end
        
        -- Nếu vượt quá giới hạn -> lưu dòng và bắt đầu dòng mới
        if width > maxLength then
            --Tìm lại vị trí space để xuống dòng, nếu không có thì xuống dòng luôn
            if lastSpacePos then
                table.insert(lines, string.sub(currentLine, 1, lastSpacePos))
                currentLine = string.sub(currentLine, lastSpacePos + 2) .. word
            else
                table.insert(lines, currentLine)
                currentLine = word
            end
            lastSpacePos = nil
        else
            currentLine = testLine
        end
    end
    
    -- Thêm dòng cuối cùng (nếu còn)
    if currentLine ~= "" then
        table.insert(lines, currentLine)
    end
    --end
    return lines
end
local SplitTextByLength = VLF.SplitTextByLength

--Sau x phút ingame, func sẽ được chạy
function VLF.DelayFunctionByMinute(func, delay)
    delay = delay >= 1 and delay or 1
    local canceled = false
    local gameTime = getGameTime()
    local start = gameTime:getMinutesStamp()
    
    local function everyOneMinute()
        if not canceled and (gameTime:getMinutesStamp() - start) < delay then
            return
        end
        Events.EveryOneMinute.Remove(everyOneMinute)
        if not canceled then func() end
    end
    Events.EveryOneMinute.Add(everyOneMinute)
    return function()
        canceled = true
    end
end

--Chờ x tick rồi mới chạy function --Mặc định tính theo 60 tick = 1 giây, function tự xử lý theo FPS
--Nếu local funcA = DelayFunction(func, delay). Khi cần dừng ngay lập tức func delay thì call funcA() -> canceled = true -> dừng onTick()
function VLF.DelayFunction(func, delay)
    --delay = delay or 1
    delay = delay >= 1 and delay or 1
    local ticks = 0
    local canceled = false
    --tính lại delay theo FPS --Nếu FPS lock cao hơn 60 thì chỉ lấy 60
    local lockFPS = PerformanceSettings.getLockFPS() or 60
    if lockFPS > 60 then lockFPS = 60 end
    local adjDelay = delay * (lockFPS / 60)
    
    local function onTick()
        if not canceled and ticks < adjDelay then
            ticks = ticks + 1
            return
        end
        Events.OnTick.Remove(onTick)
        if not canceled then func() end
    end
    Events.OnTick.Add(onTick)
    return function()
        canceled = true
    end
end

--Tính khoảng cách bán kính giữa 2 đối tượng. Nếu có zDistPenalty = true thì mỗi tầng khác biệt + thêm 3 ô
function VLF.Caldistance(obj1, obj2, zDistPenalty)
	zDistPenalty = zDistPenalty or false
	
	local x, y, x2, y2 = obj1:getX(), obj1:getY(), obj2:getX(), obj2:getY()
	
	--nếu là square thì +0.5 để lấy tâm của square
	if instanceof(obj1, "IsoGridSquare") then
        x = x + 0.5
        y = y + 0.5
    end
	if instanceof(obj2, "IsoGridSquare") then
        x2 = x2 + 0.5
        y2 = y2 + 0.5
    end
	
	local dx = x - x2
	local dy = y - y2
	
	local dist = math.sqrt(dx * dx + dy * dy)
	
	if zDistPenalty == true then
		local z1 = obj1.getZ and obj1:getZ() or 0
		local z2 = obj2.getZ and obj2:getZ() or 0
		local dz = math.abs(z1 - z2)
        dist = dist + (dz * 3)
	end
	
	return dist
end
local Caldistance = VLF.Caldistance

--[[
function VLF.Caldistance(obj1, obj2)
	return (math.abs(obj1:getX()-obj2:getX())^2 + math.abs(obj1:getY()-obj2:getY())^2)^0.5
end
local Caldistance = VLF.Caldistance
]]

--Tính khoảng cách bán kính giữa 2 tọa độ
function VLF.CaldistanceCoordinates(x, y, x2, y2)
	local dx = x - x2
	local dy = y - y2
	return math.sqrt(dx * dx + dy * dy)
end

--Tính khoảng cách giữa 2 x hoặc giữa 2 y và lấy khoảng cách lớn nhất (không phải bán kính hình tròn)
function VLF.CalculateDistanceXY(x, y, x2, y2)
    if not x or not y or not x2 or not y2 then return 0 end
    local disX = math.abs(x - x2)
    local disY = math.abs(y - y2)
    return disX >= disY and disX or disY
end

--Chọn 1 square làm điểm check modData cố định cho building
function VLF.GetStaticCoordOfBuilding(buildingDef)
    --square giữa building
	local x = math.floor(((buildingDef:getX() + buildingDef:getX2()) / 2) + 0.5)
	local y = math.floor(((buildingDef:getY() + buildingDef:getY2()) / 2) + 0.5)
	local z = 0
	return x, y, z
end
local GetStaticCoordOfBuilding = VLF.GetStaticCoordOfBuilding

--Lấy IsoDoor hoặc IsoWindow từ room
function VLF.GetDoorOrWindowOfRoom(room, doorOrWindow) --GetDoorOrWindowOfRoom(room, "door") --"window"
	local roomDef = room:getRoomDef()
	local x, y, x2, y2, z = roomDef:getX(), roomDef:getY(), roomDef:getX2(), roomDef:getY2(), roomDef:getZ()
    local objTab, sideTab = {}, {}
	for _x = x - 1, x2 + 1 do
		for _y = y - 1, y2 + 1 do
			local square = getCell():getGridSquare(_x, _y, z)
			if square then
				local obj
				if doorOrWindow == "door" then
					obj = square:getIsoDoor()
				elseif doorOrWindow == "window" then
					obj = square:getWindow()
				end
				if obj then
					--nếu obj nằm trong room
					local sq = obj:getSquare()
					if sq and room:isInside(sq:getX(), sq:getY(), sq:getZ()) then
						--return obj, "inside"
                        table.insert(objTab, obj)
                        table.insert(sideTab, "inside")
						break
					end
					--nếu obj nằm ngoài room --vì phải check cả đường viền E + S ngoài room
					local sq = obj:getOppositeSquare()
					if sq and room:isInside(sq:getX(), sq:getY(), sq:getZ()) then
						--return obj, "outside"
                        table.insert(objTab, obj)
                        table.insert(sideTab, "outside")
					end
				end
			end
		end
	end
	return objTab, sideTab
end

--Lấy số ngẫu nhiên tần suất giao động mạnh từ x + y. Giới hạn trong khoảng min - max. 
--Mục đích là ngẫu nhiên nhưng thống nhất giữa client-server.
function VLF.RandomNumberFromXY(x, y, minNum, maxNum)
	local v = math.sin(x * 127.1 + y * 311.7) * 43758.5453
	v = v - math.floor(v)
	return minNum + math.floor(v * (maxNum - minNum + 1))
end

--Lấy ItemContainer từ tọa độ x, y, z
function VLF.GetItemContainerFromCoord(x, y, z)
	--local x, y, z = args.x, args.y, args.z
	local square = getCell():getGridSquare(x, y, z)
	if square then	
		local objs = square:getObjects()
		for i = 0, objs:size() - 1 do
			local obj = objs:get(i)
			if obj then
				local iContainer = obj:getItemContainer()
				if iContainer then return iContainer end
			end
		end
	end
	return nil
end
local GetItemContainerFromCoord = VLF.GetItemContainerFromCoord

--Client -- Kiểm tra và lấy Square 1 trong 4 điều kiện hasFloor, hasIContainer, isFree, hasCorpse
function VLF.GetSquare(x, y, z, hasFloor, hasIContainer, isFree, hasCorpse) --VLF.GetSquare(x, y, z, true, false, false, false)
    local cell = getCell()
    local square = cell:getGridSquare(x, y, z)
    if square then
        if hasIContainer and hasIContainer == true then
            local iContainer = GetItemContainerFromCoord(x, y, z)
            if iContainer then return square end
        end
        if isFree and isFree == true then
            if square:isSolidFloor() and square:isFree(true) then return square end
        end
        if hasCorpse and hasCorpse == true then
            if square:isSolidFloor() then
                if IsCorpseOnSquare(square) then return square end
            end
        end                
        if hasFloor and hasFloor == true then
            if square:isSolidFloor() then return square end
        end
        --return square
    end
    return nil
end

--Lấy hoặc tạo square
function VLF.GetOrCreateSquare(x, y, z) --VLF.GetOrCreateSquare(x, y, z)
    local cell = getCell()
    local square = cell:getGridSquare(x, y, z)
    if square == nil and getWorld():isValidSquare(x, y, z) then
        square = cell:createNewGridSquare(x, y, z, true)
    end
    return square
end
local GetOrCreateSquare = VLF.GetOrCreateSquare

--Server only --Gia cố - Barricade
function VLF.AddBarricade(object, args) --VLF.AddBarricade(object, {barItem = "Base.Plank", addOpposite = false})
    local barItem = args.barricadeItem or "Base.Plank"
    local addOpposite = args.addOpposite or false
    local barricade = IsoBarricade.AddBarricadeToObject(object, addOpposite)
    if barricade then
        if barItem == "Base.SheetMetal" then
            local item = instanceItem(barItem)
            item:setCondition(100)
            barricade:addMetal(nil, item)
            barricade:transmitCompleteItemToClients()
        elseif barItem == "Base.MetalBar" then
            local item = instanceItem(barItem)
            item:setCondition(100)
            barricade:addMetalBar(nil, item)
            barricade:transmitCompleteItemToClients()
        elseif barItem == "Base.Plank" then
            local item = instanceItem(barItem)
            item:setCondition(100)
            barricade:addPlank(nil, item)
            if barricade:getNumPlanks() == 1 then
                barricade:transmitCompleteItemToClients()
            else
                barricade:sendObjectChange(IsoObjectChange.STATE)
            end
        end
    end
end
local AddBarricade = VLF.AddBarricade

--Server only --Gia cố Object
function VLF.BarricadeOnObject(args) --BarricadeOnObject({x = door:getSquare():getX(), y = door:getSquare():getY(), z = door:getSquare():getZ(), objFlag = "door", side = "inside", barItem = "Base.Plank", number = 1})
    local x, y, z, objFlag, side, barItem = args.x, args.y, args.z, args.objFlag, args.side or "outside", args.barItem or "Base.Plank"
    if x and y and z and objFlag then
        local square = getCell():getGridSquare(x, y, z)
        if square then
            local addOpposite = false
            local obj
            if side == "outside" then addOpposite = true end
            
            if objFlag == "door" then
                obj = square:getIsoDoor()
            elseif objFlag == "window" then
                obj = square:getWindow()
            end
            if obj and (instanceof(obj, "IsoDoor") or instanceof(obj, "IsoWindow"))then
                if obj:IsOpen() then
                    if objFlag == "door" then
                        obj:ToggleDoorSilent()
                        obj:sendObjectChange(IsoObjectChange.STATE)
                    elseif objFlag == "window" then
                        obj:ToggleWindow(nil)
                        obj:sendObjectChange(IsoObjectChange.STATE)
                    end
                end
                --nếu đã barricade thì không thêm nữa
                local barricaded = obj:getBarricadeOnSameSquare()
                if not barricaded then
                    barricaded = obj:getBarricadeOnOppositeSquare()
                end
                if not barricaded then
                    local number = args.number or ZombRand(2,4)
                    for i = 1, number do
                        AddBarricade(obj, {barItem = barItem, addOpposite = addOpposite})
                    end
                end
                
                if objFlag == "window" then
                    --nếu có rèm thì đóng rèm
                    local curtains = obj:HasCurtains()
                    if curtains then
                        if curtains:isCurtainOpen() then
                            curtains:ToggleDoorSilent()
                            curtains:sendObjectChange(IsoObjectChange.STATE)
                        end
                    end
                end
            end
        end
    end
end

--Kiểm tra square có object đó hay không
function VLF.GetObjectOnSquare(square, spriteName, tileName, objName)
    local objs = square:getObjects()
    for i = 0, objs:size() - 1 do
        local obj = objs:get(i)
        if obj then
            if spriteName then
                if obj:getSpriteName() == spriteName then
                    return obj              
                end
            end
            if tileName then
                if obj:getTileName() == tileName then
                    return obj              
                end
            end  
            if objName then
                if obj:getObjectName() == objName then
                    return obj              
                end
            end  
        end
    end
    return nil
end
local GetObjectOnSquare = VLF.GetObjectOnSquare

--Xóa object
function VLF.RemoveObjectOnSquare(square, spriteName, isTransmit, removeAll) --VLF.RemoveObjectOnSquare(square, spriteName, true, true)
    if not square then return nil end
    local objs = square:getObjects()
    local objTab = {}
    for i = 0, objs:size() - 1 do
        local obj = objs:get(i)
        if obj then
            table.insert(objTab, obj)
        end
    end

    for i = #objTab, 1, -1 do
        if removeAll == true then
            square:RemoveTileObject(objTab[i])
        else
            if spriteName and objTab[i]:getSpriteName() == spriteName then
                square:RemoveTileObject(objTab[i])
            end
        end
        if isTransmit == true then
            if isServer() then
                square:transmitRemoveItemFromSquareOnClients(objTab[i])
            else
                square:transmitRemoveItemFromSquare(objTab[i])
            end
        end   
    end
end

--Xóa các object ngoại trừ sàn
function VLF.RemoveAllObjectsExceptFloor(square, isTransmit) --VLF.RemoveAllObjectsExceptFloor(square, true)
    if not square then return nil end
    isTransmit = isTransmit or false
    
    local objs = square:getObjects()
    local objTab = {}
    for i = 0, objs:size() - 1 do
        local obj = objs:get(i)
        if obj then
            table.insert(objTab, obj)
        end
    end
    
    for i = #objTab, 1, -1 do
        if objTab[i] and not objTab[i]:isFloor() then --FIXME: sàn cỏ (grass) không được tính là isFloor
            square:RemoveTileObject(objTab[i])
        end
        if isTransmit == true then
            if isServer() then
                square:transmitRemoveItemFromSquareOnClients(objTab[i])
            else
                square:transmitRemoveItemFromSquare(objTab[i])
            end
        end   
    end
end

--Thêm các loại object lên square
function VLF.AddObjectToSquare(square, spriteName, args) --VLF.AddObjectToSquare(square, "location_business_office_generic_01_32", {isTransmit = true, needSqIsFree = true, needFloor = true, x = -1, y = -1, z = 0, objType = "IsoObject", isNorth = false, isLocked = false})
    local sq = square or GetOrCreateSquare(args.x, args.y, args.z)
    --print("Client----AddObjectToSquare = ", sq:getX(), sq:getY(), sq:getZ())
    if not sq or not instanceof(sq, "IsoGridSquare") then return nil end
    if args.needSqIsFree and args.needSqIsFree == true then
		if not sq:isFree(true) then return nil end
	end
    if args.needFloor and args.needFloor == true then
        if not sq:isSolidFloor()  then return nil end
    end
    local objType, isNorth, isLocked = args.objType, args.isNorth or false, args.isLocked or false
    local object
    if objType then
        local isTransmit = args.isTransmit or false
        local isObjExists = GetObjectOnSquare(sq, spriteName, nil, nil) --kiểm tra obj có tồn tại chưa
                            
        if not isObjExists then
            if objType == "IsoObject" then
                object = IsoObject.new(sq, spriteName, "") --(sq, spriteName/tileset sprite ID, name)
                sq:AddSpecialObject(object)
            elseif objType == "IsoThumpable" then
                object = IsoThumpable.new(getCell(), sq, spriteName, isNorth, {}) --(IsoCell cell, IsoGridSquare gridSquare, String sprite, boolean _north, KahluaTable _table)
                sq:AddTileObject(object)
            elseif objType == "IsoDoor" or objType == "IsoWindow" then
                if objType == "IsoDoor" then
                    object = IsoDoor.new(getCell(), sq, getSprite(spriteName), isNorth) --(IsoCell cell, IsoGridSquare gridSquare, IsoSprite gid, boolean _north)
                else
                    object = IsoWindow.new(getCell(), sq, getSprite(spriteName), isNorth) --(IsoCell cell, IsoGridSquare gridSquare, IsoSprite gid, boolean _north)
                end
                object:setIsLocked(isLocked)
                sq:AddSpecialObject(object)
            elseif objType == "IsoCurtain" then    
                object = IsoCurtain.new(getCell(), sq, spriteName, isNorth)
                sq:AddSpecialObject(object)
            elseif objType == "Container" then
                object = IsoThumpable.new(getCell(), sq, spriteName, isNorth, {})
                object:setIsContainer(true)
                sq:AddSpecialObject(object)
            elseif objType == "WaterContainer" then
                object = IsoThumpable.new(getCell(), sq, spriteName, isNorth, {})
                object:setWaterAmount(60 + ZombRand(300))
                object:setTaintedWater(true)
                sq:AddSpecialObject(object)
            elseif objType == "Fireplace" then
                object = IsoObject.new(sq, spriteName, "")
                sq:AddSpecialObject(object)
                if isTransmit == true then
                    if isClient() then
                        object:transmitCompleteItemToServer()
                    end
                end
                --fire anim
                object = IsoFire.new(getCell(), sq)
                object:AttachAnim("Fire", "01", IsoFire.NUM_FRAMES_FIRE, IsoFireManager.FireAnimDelay, 1 * Core.getTileScale(), -1 * Core.getTileScale(), true, 0, false, 0.7, IsoFireManager.FireTintMod)
                sq:AddTileObject(object)
            elseif objType == "Fridge" then
                object = IsoObject.new(sq, spriteName, "")
                local sprite = getSprite(spriteName);
                object:createContainersFromSpriteProperties()
                sq:AddSpecialObject(object)
            elseif objType == "IsoLightSwitch" then
                local sprite = getSprite(spriteName)
                local properties = sprite:getProperties()
                properties:set("lightR", "110")
                properties:set("lightG", "110")
                properties:set("lightB", "90")
                properties:set("LightRadius", "20")
                object = IsoLightSwitch.new(getCell(), sq, sprite, sq:getRoomID())
                object:setUseBattery(false)
                object:addLightSourceFromSprite()
                sq:AddSpecialObject(object)
                object:setActive(true)
                object:setActivated(true)                
            end
            if isTransmit == true then
                if isClient() then
                    object:transmitCompleteItemToServer()
                end
            end
            return object
        end
    end
    return nil
end

--Client only --Add máy phát điện
function VLF.AddGenerator(isTransmit, genType, x, y, z) --VLF.AddGenerator(isTransmit, "Base.Generator_Yellow", x, y, z)
    if not x or not y or not z then return nil end
    local cell = getCell()
    local square = cell:getGridSquare(x, y, z)
    if square then
        --local hasGen = GetObjectOnSquare(square, {objName = "IsoGenerator"})
        --if not hasGen then
            isTransmit = isTransmit or false
            genType = genType or "Base.Generator"
            local gItem = instanceItem(genType)
            if gItem then
                local object = IsoGenerator.new(gItem, cell, square)
                object:setConnected(true)
                object:setFuel(ZombRand(80, 100))
                object:setCondition(99)
                object:setActivated(true)
                if isTransmit then
                    object:transmitCompleteItemToServer()
                end
                return object
            end
        --end
    end
end
local AddGenerator = VLF.AddGenerator

--Add máy phát điện hoặc sửa nếu đã có
function VLF.AddOrRepairGenerator(isTransmit, genType, x, y, z)
    if not x or not y or not z then return nil end
    local sq = GetOrCreateSquare(x, y, z)
    if sq then 
        local obj = GetObjectOnSquare(sq, nil, nil, "IsoGenerator")
                         
        if not obj then
            obj = AddGenerator(isTransmit, genType, x, y, z)
        else
            obj:setFuel(100)
            obj:setCondition(99)
            obj:setActivated(true)
            if isTransmit then
                obj:transmitCompleteItemToServer()
                obj:sync()
            end
        end
        return obj
    end
end
 
--Kiểm tra table có rỗng hay không. Thay thế cho next(table) không sử dụng được
--Chỉ áp dụng cho table dạng map có key riêng: {a=1, b=2}
--Table kiểu index {"a", "b"} hoặc { {a=1, b=2}, {c=1, k=2}} thì sử dụng #table == 0
function VLF.IsMapTableEmpty(_table)
    local empty = true
    for _ in pairs(_table) do
        empty = false
        break
    end
    if empty == false then
        return false
    end
    return true
end

--=============================================================--
--THÊM HOẶC CẬP NHẬT NHIỆM VỤ TRONG BẢNG QUEST - không cần xóa bảng đã thêm trước đó vì đã được ghi đè
local function QuestList_AddItem(text, itemData) --QuestList_AddItem("Q"..tostring(id), {id = id, qStep = qStep, isDone = isDone, qtype = qtype, isCollection = isCollection, currentCollection = currentCollection, requestCollection = requestCollection, name = getText("IGUI_VLQ_"..tostring(id).."_Name"), task = getText("IGUI_VLQ_"..tostring(id).."_Task_"..tostring(qStep)), desc = getText("IGUI_VLQ_"..tostring(id).."_Desc_"..tostring(qStep))})
    if not text or not itemData then return end
	local id,qStep  = itemData.id, itemData.qStep
	local isDone = itemData.isDone or false
	local isCollection, currentCollection, requestCollection = itemData.isCollection or false, itemData.currentCollection, itemData.requestCollection
	local collectQItemName = itemData.collectQItemName or nil
	local qtype = itemData.qtype or getText("IGUI_VLQ_MainQuest")
	if not id or not qStep then return end
	
    VLF.QuestListShowOnUI[text] = {id = id, qStep = qStep, isDone = isDone, qtype = qtype, isCollection = isCollection, currentCollection = currentCollection, requestCollection = requestCollection, collectQItemName = collectQItemName, name = itemData.name, task = itemData.task, desc = itemData.desc}
end

--USING quest ID 1 -> 1000
function VLF.DoQuestList_AddItem(args) --DoQuestList_AddItem({id = 1, qStep = "q1s003", isDone = false, qtype = getText("IGUI_VLQ_MainQuest"), isCollection = false, currentCollection = nil, requestCollection = nil})
	local id, qStep = args.id, args.qStep
	if not id or not qStep then return end
	QuestList_AddItem("Q"..tostring(id), {id = id, qStep = qStep, isDone = args.isDone, qtype = args.qtype, isCollection = args.isCollection, currentCollection = args.currentCollection, requestCollection = args.requestCollection, collectQItemName = args.collectQItemName, name = getText("IGUI_VLQ_"..tostring(id).."_Name"), task = getText("IGUI_VLQ_"..tostring(id).."_Task_"..tostring(qStep)), desc = getText("IGUI_VLQ_"..tostring(id).."_Desc_"..tostring(qStep))})
end
--===============================================================--

--Cập nhật quest step + cập nhật quest ở trong bảng nhiệm vụ chính + Hiển thị thông báo Quest Updated
function VLF.UpdateQuestStepAndNext(player, qID, isNpcTalk, isIntroWindow, isPopIn, qStep, active, qStep_Next, active_Next) --VLF.UpdateQuestStepAndNext(player, qID, isNpcTalk, isIntroWindow, isPopIn, qStep, active, qStep_Next, active_Next)
	if not player and qStep then return end
	
	qID = qID or 3
	isNpcTalk = isNpcTalk or false
	isIntroWindow = isIntroWindow or false
	isPopIn = isPopIn or false
    local playerGMD = VLGMD.GetPlayerGlobalModData(player)
    local questKey = "Q" .. tostring(qID)
    
	print("Client-----------UpdateQuestStepAndNext= ", qStep, qStep_Next)
	--cập nhật qStep trước đó hoàn thành == "done"
	if qStep then
        VLGMD.SetPlayerGMD_Quest(player, questKey, qStep, "active", active)
	end
 
	--kích hoạt qStep tiếp theo = true
	if qStep_Next then
        VLGMD.SetPlayerGMD_Quest(player, questKey, qStep_Next, "active", active_Next)
	end

	--hiện bảng thông báo cập nhật quest
	if isPopIn and isPopIn == true then
		VLF.QuestUpdatedPopIn()
	end
	
	--hiện bảng nội dung nhiệm vụ qStep tiếp theo
	if isIntroWindow and isIntroWindow == true then
		VLF.ActiveQuestIntro_Centre(getText("IGUI_VLQ_"..tostring(qID).."_Desc_"..tostring(qStep_Next)), 800, 500, false, "")
	end
	
	--hiện bảng nội dung nhiệm vụ qStep tiếp theo
	if isNpcTalk and isNpcTalk == true then
		VLF.ActiveQuestIntro_Centre(getText("IGUI_VLQ_"..tostring(qID).."_NPCTalk_"..tostring(qStep)), 800, 500, false, "")
	end
	
	--xóa marker nếu có
	VLF.MarkerList_Remove(qStep)
end

--Broadcast 1 dòng trên Radio tất cả các channel
function VLF.BroadcastAllMHz(text, r, g, b)
    r = r or 1
    g = g or 1
    b = b or 1

    local manager = RadioScriptManager.getInstance()
    local channels = manager:getChannelsList()

    for i = 0, channels:size() - 1 do
        local channel = channels:get(i)

        local id = "VL_" .. tostring(ZombRand(100000, 999999))
        local bc = RadioBroadCast.new(id, -1, -1)

        local line = RadioLine.new(text, r, g, b)
        bc:AddRadioLine(line)

        channel:setAiringBroadcast(bc)
    end
end

--Kiểm tra đã trôi qua x tiếng chưa
function VLF.HasTimePassedInHours(hours)
	if not hours then return false end
	local currentHour = getGameTime():getWorldAgeHours()
	if currentHour >= hours then 
		return true
	end
	return false
end

--Tình trạng thời tiết	--verybad, bad, normal
function VLF.GetWeatherCondition()
	local climate = getWorld():getClimateManager()
	local weatherPeriod = climate:getWeatherPeriod()
	
	--very bad weather
	if weatherPeriod:isThunderStorm() or weatherPeriod:isTropicalStorm() or weatherPeriod:isBlizzard()
		or climate:getFogIntensity() >= 0.9 or climate:getRainIntensity() >= 0.85 or climate:getSnowIntensity() >= 0.85 then
		
		return "verybad"
	--bad weather
	elseif climate:getWindIntensity() >= 0.7 or climate:getFogIntensity() >= 0.5 or climate:getRainIntensity() >= 0.4 
		or climate:getSnowIntensity() >= 0.5 or weatherPeriod:hasHeavyRain() then
		
		return "bad"
	--normal weather
	else
		return "normal"
	end
end

--Chỉnh tốc độ game về bình thường --Singleplayer only
function VLF.SetNormalGameSpeed()
	if not IsMultiplayerMode() then	
		local speedControl = UIManager.getSpeedControls()
		local gameTime = getGameTime()
		
		if not speedControl:isPaused() and speedControl:getCurrentGameSpeed() ~= 1 then
			speedControl:SetCurrentGameSpeed(1)
		elseif gameTime:getMultiplier() ~= 1 then
			gameTime:setMultiplier(1)
		end
	end
end

--play Animation trên square cố định
function VLF.PlayAnimation(square, animNameNotLastNumber, maxAnimCount, offsetX, offsetY, height, slowMultiplier)
	if not square or not animNameNotLastNumber or not maxAnimCount then return end
	offsetX = offsetX or 0
	offsetY = offsetY or 0
	height = height or 0
	slowMultiplier = slowMultiplier or 1
	
	local doAnim = {}
	local count = 1
	
	doAnim[count] = square:AddWorldInventoryItem(animNameNotLastNumber .. tostring(count), offsetX, offsetY, height)
	count = count + 1
	
	local _tick = 1

	local function onTick()
        if _tick <= ((maxAnimCount - 1) * slowMultiplier) then
            if _tick % slowMultiplier == 0 then
				if count <= maxAnimCount then
					doAnim[count] = square:AddWorldInventoryItem(animNameNotLastNumber .. tostring(count), offsetX, offsetY, height)
					
					if doAnim[count - 1] then
						local worldItem = doAnim[count-1]:getWorldItem()
						if worldItem then
							square:removeWorldObject(worldItem)
							worldItem:removeFromSquare()
						end
					end
					count = count + 1
				end
			end
			
			_tick = _tick + 1

            return
        end
        Events.OnTick.Remove(onTick)
		
		--Xóa anim cuối VÀ nếu còn sót
		for i, itemINV in pairs(doAnim) do 
			if doAnim[i] then
				local worldItem = doAnim[i]:getWorldItem()
				if worldItem then
					worldItem:getSquare():removeWorldObject(worldItem)
					worldItem:removeFromSquare()
				end
			end
		end
		
    end
    Events.OnTick.Add(onTick)
end

--Kiểm tra Room có kín không
--FIXME: lỗi khi square ở góc bị hở 1 bên vẫn check true. Vì isDoorOrWallSquare check wall 4 hướng
function VLF.IsRoomSealed(room)
    if not room then return false end
	
	local directions = {
		IsoDirections.N,
		IsoDirections.E,
		IsoDirections.S,
		IsoDirections.W
	}

	local function FindFirstWallSquare(room)
		local squares = room:getSquares()
		for i = 0, squares:size() - 1 do
			local sq = squares:get(i)
			if sq:isDoorOrWallSquare() then
				return sq
			end
		end
		return nil
	end

	local function FindNextWallSquare(room, current, last)
		for _, dir in ipairs(directions) do
			local adj = current:getAdjacentSquare(dir)
			if adj then
				if adj ~= last
				and adj:isDoorOrWallSquare()
				and room:isInside(adj:getX(), adj:getY(), adj:getZ()) then
					return adj
				end
			end
		end
		return nil
	end
	
    local first = FindFirstWallSquare(room)
    if not first then
        --Không có wall/door nào => không thể kín
        return false
    end

    local last = nil
    local current = first

    local safety = 0
    local MAX_STEPS = 300 --75x4 tối đa độ dài viền có thể load

    while safety < MAX_STEPS do
        safety = safety + 1

        local nextSq = FindNextWallSquare(room, current, last)
        if not nextSq then
            --Đứt vòng
            return false
        end

        if nextSq == first then
            --Khép kín
            return true
        end

        last = current
        current = nextSq
    end

    --Failsafe
    return false
end

--Lấy danh sách tổng các tạo độ Building để loại trừ khỏi các sự kiện
function VLF.IgnoredBuildingPositions()
	local list = {}
	local ignoredPositionList = VLF.var.ignoredPositionList
	
	--lấy tọa độ từ danh sách có sẵn ignoredPositionList
	for i = 1, #ignoredPositionList do
        local pos = ignoredPositionList[i]
        if pos and pos.x and pos.y then
            table.insert(list, { x = pos.x, y = pos.y })
        end
    end
	
	--lấy tọa độ từ Safehouse của Singple Player
	if not IsMultiplayerMode() then		--SP
		local playerGMD = VLGMD.GetPlayerGlobalModData(getPlayer())
		local safehouseData = playerGMD.Data["SPSafehouseCoords"]
	
		if safehouseData then
			table.insert(list, {
                x = safehouseData.x,
                y = safehouseData.y
            })
		end
	end
	
	return list
end
local IgnoredBuildingPositions = VLF.IgnoredBuildingPositions

--Kiểm tra building có tọa độ nằm trong danh sách IgnoredBuildingPositions không
function VLF.IsBuildingInIgnoredList(building)
	if not building then return false end
	
	local buildingDef = building:getDef()
	local x, y, z = GetStaticCoordOfBuilding(buildingDef)
	local ignoredList = IgnoredBuildingPositions()

	for i = 1, #ignoredList do
		local pos = ignoredList[i]
		if pos.x == x and pos.y == y then
			return true
		end
	end
	
	return false
end

