BanditUtils = BanditUtils or {}

BanditUtils.ItemVisuals = {
    ["Base.ZedDmg_BACK_Slash"] = true,
    ["Base.ZedDmg_BulletBelly01"] = true,
    ["Base.ZedDmg_BulletBelly03"] = true,
    ["Base.ZedDmg_BulletBelly02"] = true,
    ["Base.ZedDmg_FaceSkullLeft"] = true,
    ["Base.ZedDmg_NoNose"] = true,
    ["Base.ZedDmg_ShoulderSlashLeft"] = true,
    ["Base.ZedDmg_HEAD_Slash"] = true,
    ["Base.ZedDmg_MouthLeft"] = true,
    ["Base.ZedDmg_NoChin"] = true,
    ["Base.ZedDmg_NoEarLeft"] = true,
    ["Base.ZedDmg_RibsLeft"] = true,
    ["Base.ZedDmg_BellySlashRight"] = true,
    ["Base.ZedDmg_BulletForehead01"] = true,
    ["Base.ZedDmg_BulletForehead03"] = true,
    ["Base.ZedDmg_BulletForehead02"] = true,
    ["Base.ZedDmg_ShotgunChestLeft"] = true,
    ["Base.ZedDmg_ShotgunChestRight"] = true,
    ["Base.ZedDmg_FaceSkullRight"] = true,
    ["Base.M_Beard_Stubble"] = true,
    ["Base.ZedDmg_ChestSlashLeft"] = true,
    ["Base.ZedDmg_ShotgunFaceLeft"] = true,
    ["Base.ZedDmg_HeadSlashLeft02"] = true,
    ["Base.ZedDmg_HeadSlashLeft03"] = true,
    ["Base.ZedDmg_HeadSlashLeft01"] = true,
    ["Base.ZedDmg_HEAD_Bullet"] = true,
    ["Base.ZedDmg_BACK_Spine"] = true,
    ["Base.ZedDmg_ShotgunLeft"] = true,
    ["Base.ZedDmg_BELLY_Bullet"] = true,
    ["Base.ZedDmg_ShotgunFaceFull"] = true,
    ["Base.ZedDmg_HEAD_Shotgun"] = true,
    ["Base.ZedDmg_HeadSlashCentre01"] = true,
    ["Base.ZedDmg_HeadSlashCentre02"] = true,
    ["Base.ZedDmg_HeadSlashCentre03"] = true,
    ["Base.ZedDmg_ShotgunFaceRight"] = true,
    ["Base.ZedDmg_ShotgunChestCentre"] = true,
    ["Base.ZedDmg_RibsRight"] = true,
    ["Base.ZedDmg_BELLY_Slash"] = true,
    ["Base.ZedDmg_SkullUpRight"] = true,
    ["Base.ZedDmg_NeckBiteBackRight"] = true,
    ["Base.ZedDmg_Mouth01"] = true,
    ["Base.M_Hair_Stubble"] = true,
    ["Base.ZedDmg_Mouth02"] = true,
    ["Base.ZedDmg_NoEarRight"] = true,
    ["Base.ZedDmg_HeadSlashRight03"] = true,
    ["Base.ZedDmg_HeadSlashRight02"] = true,
    ["Base.ZedDmg_HeadSlashRight01"] = true,
    ["Base.ZedDmg_BELLY_Shotgun"] = true,
    ["Base.ZedDmg_NeckBiteBackLeft"] = true,
    ["Base.ZedDmg_BulletFace02"] = true,
    ["Base.ZedDmg_BulletFace01"] = true,
    ["Base.ZedDmg_ShotgunBelly"] = true,
    ["Base.ZedDmg_NeckBiteFrontRight"] = true,
    ["Base.ZedDmg_ShoulderSlashRight"] = true,
    ["Base.ZedDmg_CHEST_Shotgun"] = true,
    ["Base.ZedDmg_HEAD_Skin"] = true,
    ["Base.ZedDmg_CHEST_Bullet"] = true,
    ["Base.ZedDmg_MouthRight"] = true,
    ["Base.ZedDmg_NeckBiteFrontLeft"] = true,
    ["Base.ZedDmg_BulletRightTemple"] = true,
    ["Base.ZedDmg_HeadSlashRightBack02"] = true,
    ["Base.ZedDmg_HeadSlashRightBack01"] = true,
    ["Base.ZedDmg_BELLY_Skin"] = true,
    ["Base.ZedDmg_NECK_Bite"] = true,
    ["Base.ZedDmg_SkullUpLeft"] = true,
    ["Base.F_Hair_Stubble"] = true,
    ["Base.ZedDmg_HeadSlashLeftBack01"] = true,
    ["Base.ZedDmg_HeadSlashLeftBack02"] = true,
    ["Base.ZedDmg_BulletChest01"] = true,
    ["Base.ZedDmg_BulletChest02"] = true,
    ["Base.ZedDmg_BulletChest03"] = true,
    ["Base.ZedDmg_BulletChest04"] = true,
    ["Base.ZedDmg_SkullCap"] = true,
    ["Base.ZedDmg_ShotgunRight"] = true,
    ["Base.ZedDmg_BulletLeftTemple"] = true,
    ["Base.ZedDmg_BellySlashLeft"] = true,
    ["Base.ZedDmg_CHEST_Slash"] = true,
    ["Base.Bandage_LeftUpperArm"] = true,
    ["Base.Bandage_LeftUpperArm_Blood"] = true,
    ["Base.Bandage_RightUpperArm"] = true,
    ["Base.Bandage_RightUpperArm_Blood"] = true,
    ["Base.Bandage_Abdomen"] = true,
    ["Base.Bandage_Abdomen_Blood"] = true,
    ["Base.Bandage_Chest"] = true,
    ["Base.Bandage_Chest_Blood"] = true,
    ["Base.Bandage_Groin"] = true,
    ["Base.Bandage_Groin_Blood"] = true,
    ["Base.Bandage_LeftLowerLeg"] = true,
    ["Base.Bandage_LeftLowerLeg_Blood"] = true,
    ["Base.Bandage_RightLowerLeg"] = true,
    ["Base.Bandage_RightLowerLeg_Blood"] = true,
    ["Base.Bandage_LeftFoot"] = true,
    ["Base.Bandage_LeftFoot_Blood"] = true,
    ["Base.Bandage_RightFoot"] = true,
    ["Base.Bandage_RightFoot_Blood"] = true,
    ["Base.Bandage_LeftHand"] = true,
    ["Base.Bandage_LeftHand_Blood"] = true,
    ["Base.Bandage_RightHand"] = true,
    ["Base.Bandage_RightHand_Blood"] = true,
    ["Base.Bandage_LeftUpperLeg"] = true,
    ["Base.Bandage_LeftUpperLeg_Blood"] = true,
    ["Base.Bandage_RightUpperLeg"] = true,
    ["Base.Bandage_RightUpperLeg_Blood"] = true,
    ["Base.Bandage_LeftLowerArm"] = true,
    ["Base.Bandage_LeftLowerArm_Blood"] = true,
    ["Base.Bandage_RightLowerArm"] = true,
    ["Base.Bandage_RightLowerArm_Blood"] = true,
    ["Base.Bandage_Neck"] = true,
    ["Base.Bandage_Neck_Blood"] = true,
    ["Base.Bandage_Head"] = true,
    ["Base.Bandage_Head_Blood"] = true,
}

BanditUtils.VehicleParts = {
    [1] = {name="HeadlightLeft", dmg=18, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [2] = {name="HeadlightRight", dmg=18, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [3] = {name="HeadlightRearLeft", dmg=18, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [4] = {name="HeadlightRearRight", dmg=18, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [5] = {name="Windshield", dmg=20, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [6] = {name="WindshieldRear", dmg=20, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [7] = {name="WindowFrontRight", dmg=20, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [8] = {name="WindowFrontLeft", dmg=20, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [9] = {name="WindowRearRight", dmg=20, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [10] = {name="WindowRearLeft", dmg=20, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [11] = {name="WindowMiddleLeft", dmg=20, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [12] = {name="WindowMiddleRight", dmg=20, sndHit="BreakGlassItem", sndDest="SmashWindow"},
    [13] = {name="DoorFrontRight", dmg=10, sndHit="HitVehiclePartWithWeapon", sndDest="HitVehiclePartWithWeapon"},
    [14] = {name="DoorFrontLeft", dmg=10, sndHit="HitVehiclePartWithWeapon", sndDest="HitVehiclePartWithWeapon"},
    [15] = {name="DoorRearRight", dmg=10, sndHit="HitVehiclePartWithWeapon", sndDest="HitVehiclePartWithWeapon"},
    [16] = {name="DoorRearLeft", dmg=10, sndHit="HitVehiclePartWithWeapon", sndDest="HitVehiclePartWithWeapon"},
    [17] = {name="EngineDoor", dmg=10, sndHit="HitVehiclePartWithWeapon", sndDest="HitVehiclePartWithWeapon"},
    [18] = {name="TireFrontRight", dmg=8, sndHit="VehicleTireExplode", sndDest="VehicleTireExplode"},
    [19] = {name="TireFrontLeft", dmg=8, sndHit="VehicleTireExplode", sndDest="VehicleTireExplode"},
    [20] = {name="TireRearLeft", dmg=8, sndHit="VehicleTireExplode", sndDest="VehicleTireExplode"},
    [21] = {name="TireRearRight", dmg=8, sndHit="VehicleTireExplode", sndDest="VehicleTireExplode"}
}

BanditUtils.DoorHitSounds = {
    ["WoodDoor"] = "HitBarricadePlank",
    ["MetalDoor"] = "HitBarricadeMetal",
}

BanditUtils.Mat2id = {"Flesh", "Flesh_Hollow", "Concrete", "Plaster", "Stone", "Wood", "Wood_Solid", "Brick", "Metal",
                      "Metal_Large", "Metal_Light", "Metal_Solid", "Glass", "Glass_Light", "Glass_Solid", "Cinderblock",
                      "Plastic", "Ceramic", "Rubber", "Fabric", "Carpet", "Dirt", "Grass", "Gravel", "Sand", "Snow"}

BanditUtils.BloodBodyParts = {
    [1] = {name=BloodBodyPartType.Foot_R},
    [2] = {name=BloodBodyPartType.Foot_L},
    [3] = {name=BloodBodyPartType.LowerLeg_R},
    [4] = {name=BloodBodyPartType.LowerLeg_L},
    [5] = {name=BloodBodyPartType.UpperLeg_R},
    [6] = {name=BloodBodyPartType.UpperLeg_L},
    [7] = {name=BloodBodyPartType.Groin},
    [8] = {name=BloodBodyPartType.Neck},
    [9] = {name=BloodBodyPartType.Head},
    [10] = {name=BloodBodyPartType.Torso_Lower},
    [11] = {name=BloodBodyPartType.Torso_Upper},
    [12] = {name=BloodBodyPartType.UpperArm_R},
    [13] = {name=BloodBodyPartType.UpperArm_L},
    [14] = {name=BloodBodyPartType.ForeArm_R},
    [15] = {name=BloodBodyPartType.ForeArm_L},
    [16] = {name=BloodBodyPartType.Hand_R},
    [17] = {name=BloodBodyPartType.Hand_L},
}

function BanditUtils.AddHole (character)
    local bpi = 1 + BanditRandom.Get() % 17
    local bodyParts = BanditUtils.BloodBodyParts
    local bodyPart = bodyParts[bpi]

    local visuals = character:getHumanVisual()
    visuals:setBlood(bodyPart.name, 1)

    local itemVisuals = character:getItemVisuals()
    for i = 0, itemVisuals:size() - 1 do
        local item = itemVisuals:get(i)
        if item then
            item:setBlood(bodyPart.name, 1)
            local clothing = item:getInventoryItem()
            if instanceof(clothing, "Clothing") then
                local coveredPartList = clothing:getCoveredParts()
                for i=0, coveredPartList:size()-1 do
                    local coveredPart = coveredPartList:get(i)
                    if coveredPart == bodyPart.name then
                        item:setHole(bodyPart.name)
                    end
                end
            end
        end
    end
    character:resetModelNextFrame()
    character:resetModel()
end

function BanditUtils.AddHolePlayer (player)
    local bpi = 1 + BanditRandom.Get() % 17
    local bodyParts = BanditUtils.BloodBodyParts
    local bodyPart = bodyParts[bpi]

    local visuals = player:getHumanVisual()
    visuals:setBlood(bodyPart.name, 1)

    local wornItems = player:getWornItems()
    for i = 0, wornItems:size() - 1 do
        local wornItem = wornItems:get(i)
        local item = wornItem:getItem()
        if item then
            item:setBlood(bodyPart.name, 1)
            if instanceof(item, "Clothing") then
                local coveredPartList = item:getCoveredParts()
                for i=0, coveredPartList:size()-1 do
                    local coveredPart = coveredPartList:get(i)
                    if coveredPart == bodyPart.name then
                        -- item:getVisual():setHole(bodyPart.name)
                    end
                end
            end
        end
    end
    player:resetModelNextFrame()
    player:resetModel()
end

function BanditUtils.Thump (object, thumper)
    local health = object:getHealth()
    -- print ("thumpable health: " .. object:getHealth())
    health = health - 20
    if health < 0 then health = 0 end
    if health == 0 then
        object:destroy()
    else
        object:setHealth(health)
        object:Thump(thumper)
    end
end

function BanditUtils.Hit(shooter, item, victim, damageSplit)
    local fakeZombie = getCell():getFakeZombieForHit()

    -- determines if the bullet landed (either hit target or miss) but not if friendly
    local landed = false

    -- Calculate the distance between the shooter and the victim
    local dist = BanditUtils.DistTo(victim:getX(), victim:getY(), shooter:getX(), shooter:getY())

    -- Determine accuracy based on SandboxVars and shooter clan
    local brainShooter = BanditBrain.Get(shooter)

    -- Logistic curve
    local function calculateHitChance(distance, accuracy)
        local baseChance = 9000  -- 90% hit chance at point blank
        local d50 = 16 + accuracy -- Distance where hit chance is 50%
        local k = 0.13   -- Steepness of falloff
        local floor = 1200 -- Minimal hit chance
        return floor + (baseChance - floor) / (1 + math.exp(k * (distance - d50)))
        -- return baseChance / (1 + math.exp(k * (distance - d50)))
    end

    local accuracyLevelMap = {-8, -4, 0, 4, 8}
    local accuracyLevel = SandboxVars.Bandits.General_OverallAccuracy

    -- general sandbox setting for accuracy 
    local sightGeneral = accuracyLevelMap[accuracyLevel] or 0 -- will add or substract max 8

    -- accuracy set in bandit creator
    local sightCharacter = brainShooter and brainShooter.accuracyBoost or 0 -- will add or substract max 8

    -- scope boost
    local sightScope = 0
    local scope = item:getWeaponPart("Scope")
    if scope then
        sightScope = BanditCompatibility.GetScopeRange(scope) -- will add 12, 16 or 22
    end

    local accuracyThreshold = calculateHitChance(dist, sightGeneral + sightCharacter + sightScope)
    --  print ("AT: " .. accuracyThreshold)
    -- if ZombRand(10000) < accuracyThreshold then
    local n = BanditRandom.Get()
    if n < accuracyThreshold then
        -- print ("HIT N: " .. n)
        if instanceof(victim, "IsoPlayer") and (brainShooter.hostile or brainShooter.hostileP) then
            BanditPlayer.WakeEveryone()

            local hitSound = "ZSHit" .. tostring(1 + ZombRand(3))
            victim:playSound(hitSound)

            PlayerDamageModel.BulletHit(shooter, item, victim)
            landed = true
            --[[

            BanditCompatibility.PlayerVoiceSound(victim, "PainFromFallHigh")
            victim:setHitFromBehind(shooter:isBehind(victim))
            victim:Hit(item, fakeZombie, damageSplit, false, 1, false)

            -- addHolePlayer(victim)
            BanditCompatibility.Splash(victim, item, fakeZombie)

            local bodyDamage = victim:getBodyDamage()
            if bodyDamage then
                local health = bodyDamage:getOverallBodyHealth()
                health = health + 8
                if health > 100 then health = 100 end
                bodyDamage:setOverallBodyHealth(health)
            end

            if (victim:isSprinting() or victim:isRunning()) and ZombRand(12) == 1 then
                victim:clearVariable("BumpFallType")
                victim:setBumpType("stagger")
                victim:setBumpFall(true)
                victim:setBumpFallType("pushedBehind")
            end
            ]]

        elseif instanceof(victim, "IsoZombie") then
            local brainVictim = BanditBrain.Get(victim)
            if BanditUtils.AreEnemies(brainVictim, brainShooter) then
            -- if not brainVictim or (brainVictim.clan ~= brainShooter.clan and (brainShooter.hostile or brainVictim.hostile)) then

                local isSeen = false
                local playerList = BanditPlayer.GetPlayers()
                for i=0, playerList:size()-1 do
                    local player = playerList:get(i)
                    if player and player:CanSee(victim) and victim:getSquare():isCanSee(0) then
                        isSeen = true
                    end
                end

                local isSeen = false
                local playerList = BanditPlayer.GetPlayers()
                for i=0, playerList:size()-1 do
                    local player = playerList:get(i)
                    if player then
                        if  player:CanSee(victim) then
                            if victim:getSquare():isCanSee(0) then
                                isSeen = true
                            end
                        end
                    end
                end

                local dmg = item:getMaxDamage()
                if instanceof(victim, "IsoZombie") then
                    dmg = dmg * 2
                end

                victim:setBumpDone(true)
                victim:setHitFromBehind(shooter:isBehind(victim))
                -- victim:setHitAngle(shooter:getForwardDirection())
                victim:setPlayerAttackPosition(victim:testDotSide(shooter))
                victim:setHitReaction("ShotBelly")

                if isSeen then
                    victim:Hit(item, fakeZombie, dmg, false, 1, false)
                else
                    local fakeItem = BanditCompatibility.InstanceItem("Base.Katana")
                    victim:Hit(fakeItem, fakeZombie, dmg, false, 1, false)
                end
                victim:setAttackedBy(shooter)
                BanditUtils.AddHole(victim)
                BanditCompatibility.Splash(victim, item, fakeZombie)

                local h = victim:getHealth()
                local id = BanditUtils.GetCharacterID(bandit)
                local args = {id=id, h=h}
                sendClientCommand(getSpecificPlayer(0), 'Sync', 'Health', args)
                landed = true
            end
        end


    else
        local missSound = "ZSMiss".. tostring(1 + ZombRand(8))
        victim:getSquare():playSound(missSound)
        landed = true
    end

    if victim:getHealth() <= 1 then
        Bandit.Say(shooter, "DEATH")
    end

    -- Clean up the temporary player after use
    -- tempShooter:removeFromWorld()
    -- tempShooter = nil

    return landed
end

function BanditUtils.ManageLineOfFire (shooter, enemy, weaponItem, damageSplit, incendiary)

    local cell = getCell()

    local x0 = math.floor(shooter:getX())
    local y0 = math.floor(shooter:getY())
    local x1 = math.floor(enemy:getX())
    local y1 = math.floor(enemy:getY())
    local z = enemy:getZ()

    local dx = math.abs(x1 - x0)
    local dy = math.abs(y1 - y0)
    local sx = (x0 < x1) and 1 or -1
    local sy = (y0 < y1) and 1 or -1
    local err = dx - dy

    local cx, cy, cz = x0, y0, z
    damageSplit = damageSplit or 1.4
    incendiary = incendiary or false

    local vp = BanditUtils.VehicleParts
    local snds = BanditUtils.DoorHitSounds
    local player = getSpecificPlayer(0)
    local piercing = weaponItem:isPiercingBullets()
    local projectiles = BanditUtils.GetProjectileCount(weaponItem:getWeaponReloadType())

    -- Bresenham's line of fire to detect what needs to destroyed between shooter and target
    local i = 0
    while true do

        -- last iterations
        local list = {}
        if cx == x1 and cy == y1 then
            for x = -2, 2 do
                for y = -2, 2 do
                    table.insert(list, {x = cx + x, y = cy + y, z=cz})
                end
            end
        else
            table.insert(list, {x=cx, y=cy, z=cz})
        end

        for _, c in pairs(list) do
            local square = cell:getGridSquare(c.x, c.y, c.z)
            if i > 1 and square then
                -- manage wall obstacle
                local props = square:getProperties()
                local names = props:getPropertyNames()

                if props then
                    -- square:playSound("BulletImpact")
                    local matName = props:get("Material")
                    if not matName then
                        matName = props:get("MaterialType")
                    end
                    if matName then
                        -- print (matName)
                        local emitter = getWorld():getFreeEmitter(c.x, c.y, c.z)
                        local sid = emitter:playSound("BulletImpact")
                        local sidx = BanditUtils.Mat2id[matName] or 0
                        BanditCompatibility.setParameterValueByName(emitter, sid, "BulletHitSurface", sidx)
                        -- BanditProjectile.Stop(brainShooter.id)
                    end
                    -- return false
                end

                -- manage window obstacle
                local window = square:getWindow()
                if window then
                    if (window:getNorth() and (y0 < cy or y1 < cy)) or 
                    (not window:getNorth() and (x0 < cx or x1 < cx)) then
                        local barricade = window:getBarricadeOnSameSquare()
                        if not barricade then
                            barricade = window:getBarricadeOnOppositeSquare()
                        end
                        local smash = false
                        if barricade then
                            if barricade:isMetal() then
                                barricade:Thump(shooter)
                                square:playSound("HitBarricadeMetal")
                                return false
                            else -- wood
                                barricade:Thump(shooter)
                                local p = barricade:getNumPlanks()
                                if p >= 2 then
                                    square:playSound("HitBarricadePlank")
                                    return false
                                end
                            end
                        end
                        if not window:isSmashed() then
                            square:playSound("SmashWindow")
                            window:smashWindow()
                        end
                    end
                end

                -- manage for door obstacle
                local door = square:getIsoDoor()
                if door and not door:IsOpen() then
                    if (door:getNorth() and (y0 < cy or y1 < cy)) or 
                       (not door:getNorth() and (x0 < cx or x1 < cx)) then
                        -- small chance to shoot through a small window in door
                        if ZombRand(10) > 1 then 
                            local sprite = door:getSprite()
                            local props = sprite:getProperties()
                            if props:has("DoorSound") then
                                doorSound = props:get("DoorSound")
                                if snds[doorSound] then
                                    square:playSound(snds[doorSound])
                                end
                            end
                            BanditUtils.Thump(door, shooter)
                            return false
                        end
                    end
                end

                -- manage vehicle obstacle
                local vehicle = square:getVehicleContainer()
                if vehicle then
                    local hasArmor = vehicle:getPartById("Armor")
                    if not hasArmor then
                        local partRandom = ZombRand(30)
                        local vehiclePart
                        local dmg
                        if vp[partRandom] then
                            vehiclePart = vehicle:getPartById(vp[partRandom].name)
                            if vehiclePart and vehiclePart:getInventoryItem() then

                                local vehiclePartId = vehiclePart:getId()

                                local dmg = vp[partRandom].dmg
                                vehiclePart:damage(dmg)

                                if vehiclePart:getCondition() <= 0 then
                                    vehiclePart:setInventoryItem(nil)
                                    square:playSound(vp[partRandom].sndDest)
                                else
                                    square:playSound(vp[partRandom].sndHit)
                                    return false
                                end

                                vehicle:updatePartStats()

                                local args = {x=square:getX(), y=square:getY(), id=vehiclePartId, dmg=dmg}
                                sendClientCommand(player, 'Commands', 'VehiclePartDamage', args)

                            end
                        end
                    end
                end

                -- manage character "obstacles"
                local chrs = square:getMovingObjects()
                local landed = false
                --for i=0, math.min(chrs:size()-1, projectiles) do
                for i=0, chrs:size()-1 do
                    local chr = chrs:get(i)
                    if instanceof(chr, "IsoZombie") or instanceof(chr, "IsoPlayer") then
                        if BanditUtils.GetCharacterID(shooter) ~= BanditUtils.GetCharacterID(chr) then 
                            
                            local hasArmor = false
                            local vehicle = chr:getVehicle()
                            if vehicle and vehicle:getPartById("Armor") then
                                hasArmor = true
                            end
                            if not hasArmor then
                                landed = BanditUtils.Hit(shooter, weaponItem, chr, damageSplit)
                                if incendiary then
                                    chr:setOnFire(true)
                                end
                            end
                            if i + 1 >= projectiles and landed then break end
                        end
                    end
                end
                if not piercing and landed then return false end

            end
        end

        if cx == x1 and cy == y1 then break end
        local e2 = 2 * err
        if e2 > -dy then
            err = err - dy
            cx = cx + sx
        end
        if e2 < dx then
            err = err + dx
            cy = cy + sy
        end
        i = i + 1
    end

    -- no bullet stop
    return true
end

function BanditUtils.SanitizeString(str)
    str = str:match("^%s*(.-)%s*$")
    str = str:gsub("%s+", "_")
    return str
end

local idcache = {}

local function toBits(num)
    local bits = string.split(string.reverse(Long.toUnsignedString(num, 2)), "")
    while #bits < 16 do bits[#bits+1] = "0" end
    return bits
end

local function toDec(bits)
    local decimal = Long.parseUnsignedLong(string.reverse(table.concat(bits, "")), 2)
    return decimal
end

function BanditUtils.GetZombieID (character)
    local dec = character:getPersistentOutfitID()
    local id = idcache[dec]
    if id then return id end

    local bits = toBits(dec)
    local hat = bits[16]
    if hat == "1" then
        bits[16] = "0"
        id = toDec(bits)
        idcache[dec] = id
    else
        id = dec
        idcache[dec] = id
    end
    return id
end

function BanditUtils.GetCharacterID (character)

    if instanceof(character, "IsoZombie") then
        -- local id = character:getVariableString("BanditID")
        -- if false and id and id ~= "" then
        --     return tonumber(id)
        -- else
        local id
        local dec = character:getPersistentOutfitID()

        local bits = toBits(dec)
        local hat = bits[16]
        if hat == "1" then
            bits[16] = "0"
            id = toDec(bits)
        else
            id = dec
        end
            -- character:setVariable("BanditID", tostring(id))
        return id
        -- end
    end

    if instanceof(character, "IsoPlayer") then
        local world = getWorld()
        local gamemode = world:getGameMode()
        local id = false
        if gamemode == "Multiplayer" then
            id = character:getOnlineID()
        else
            id = 1
        end
        return id
    end
end

function BanditUtils.IsController(zombie)

    -- ZOMBIE/BANDIT BEHAVIOUR IS FULLY CLIENT CONTROLLED
    -- SO CLIENTS ARE MIRRORING ACTIONS FOR ZOMBIES
    -- NOW, WE WANT VISUAL MIRRORING BY ALL CLIENTS 
    -- BUT THE ACTUAL ACTION CONSEQUENCES TO HAPPEN ONCE

    local gamemode = getWorld():getGameMode()

    if gamemode ~= "Multiplayer" then return true end

    local zx = zombie:getX()
    local zy = zombie:getY()

    local bestDist = 10000
    local bestPlayerId
    local playerList = getOnlinePlayers()
    for i=0, playerList:size()-1 do
        local player = playerList:get(i)
        local px = player:getX()
        local py = player:getY()

        local dist = BanditUtils.DistTo(zx, zy, px, py)
        if dist < bestDist then
            bestDist = dist
            bestPlayerId = BanditUtils.GetCharacterID(player)
        end
    end
    return bestPlayerId == BanditUtils.GetCharacterID(getSpecificPlayer(0))
end

function BanditUtils.IsFacing(sx, sy, sa, tx, ty, tolerance)
    local dx = tx - sx
    local dy = ty - sy
    local angleToTarget = math.deg(math.atan2(dy, dx))
    local angleDiff = (angleToTarget - sa + 180) % 360 - 180
    -- print ("angle: " .. angleDiff)
    return math.abs(angleDiff) < tolerance
end

function BanditUtils.IsInAngle(observer, targetX, targetY)
    local omega = observer:getDirectionAngle()
    local targer_delta_x = targetX - observer:getX()
    local targer_delta_y = targetY - observer:getY()
    local theta = 57.295779513 * math.atan(targer_delta_y / targer_delta_x)

    -- print ("omega:" .. omega)
    -- print ("theta:" .. theta)

    if math.abs(theta - omega) < 45 then 
        return true
    else
        return false
    end
end

function BanditUtils.CalcAngle (x1, y1, x2, y2)

    -- Calculate the difference in coordinates
    local dx = x2 - x1
    local dy = y2 - y1

    -- Use atan2 to get the angle in radians
    local angleRadians = math.atan2(dy, dx)

    -- Convert radians to degrees
    local angleDegrees = math.deg(angleRadians)

    -- Ensure the angle is within the range -180 to 180
    return angleDegrees
end

function BanditUtils.DominantAngle(cx, cy, binSize)
    local bins = {}
    local zombieList = BanditZombie.CacheLightZ
    
    -- Step 1: Calculate angle for each point and bin it
    for _, zombie in pairs(zombieList) do
        local dx = zombie.x - cx
        local dy = zombie.y - cy
        if dx < 50 and dy < 50 then
            local angle = math.deg(math.atan2(dy, dx))
            
            -- Normalize angle to [-180, 180]
            if angle > 180 then angle = angle - 360 end
            if angle < -180 then angle = angle + 360 end

            -- Round to nearest bin
            local bin = math.floor(angle / binSize + 0.5) * binSize
            bins[bin] = (bins[bin] or 0) + 1
        end
    end

    -- Step 2: Find bin with most points
    local maxBin = nil
    local maxCount = -1
    for bin, count in pairs(bins) do
        if count > maxCount then
            maxCount = count
            maxBin = bin
        end
    end

    return maxBin
end

function BanditUtils.AreEnemies(brain1, brain2)
    if  not brain1 or -- zombie case
        not brain2 or -- zombie case
        (brain1.clan ~= brain2.clan and (brain1.hostile or brain2.hostile)) or -- at least one of them is hostile and they are from different clans
        (brain1.loyal and brain2.hostileP) or -- is hostile against player only, but the second one is loyal to the player (babe case)
        (brain2.loyal and brain1.hostileP) then
            return true
    end
    return false
end

function BanditUtils.ModifyWeapon(weapon, brain)
    local sight = brain.accuracyBoost
    local scopeItem
    if sight >= 1 and sight <= 2 then
        scopeItem = BanditCompatibility.InstanceItem("Base.x2Scope")
    elseif sight > 2 and sight <= 3 then
        scopeItem = BanditCompatibility.InstanceItem("Base.x4Scope")
    elseif sight > 3 then
        scopeItem = BanditCompatibility.InstanceItem("Base.x8Scope")
    end

    if scopeItem then
        local mountList = scopeItem:getMountOn()
        for i=1, mountList:size() do
            local mount = mountList:get(i-1)
            if mount == weapon:getFullType() then
                weapon:attachWeaponPart(scopeItem)
            end
        end
    end
    return weapon
end

function BanditUtils.GetClosestPlayerLocation(character, config)
    local result = {}
    result.dist = math.huge
    result.x = false
    result.y = false
    result.z = false
    result.id = false

    if not config then config = {} end

    local mustSee = config.mustSee or true
    local hearDist = config.hearDist or 7

    local cx, cy, cz = character:getX(), character:getY(), character:getZ()
    local playerList = BanditPlayer.GetPlayers()

    for i=0, playerList:size()-1 do
        local player = playerList:get(i)
        if player and not BanditPlayer.IsGhost(player) then
            local px, py, pz = player:getX(), player:getY(), player:getZ()
            local dist = BanditUtils.DistTo(cx, cy, px, py)
            local levelDiff = math.abs(pz - cz)
            if dist < result.dist and (not mustSee or (character:CanSee(player) or dist < hearDist))and (not config.levelDiff or levelDiff <= config.levelDiff) then
                result.dist = dist
                result.x = player:getX()
                result.y = player:getY()
                result.z = player:getZ()
                result.d = player:getDirectionAngle()
                result.id = BanditUtils.GetCharacterID(player)
                result.player = true
            end
        end
    end
    return result
end

function BanditUtils.GetClosestZombieLocation(character, config)
    local result = {}
    result.dist = math.huge
    result.x = false
    result.y = false
    result.z = false
    result.id = false

    if not config then config = {} end

    local cx, cy, cz = character:getX(), character:getY(), character:getZ()

    local zombieList = BanditZombie.CacheLightZ
    for id, zombie in pairs(zombieList) do
        local dist = math.sqrt(((cx - zombie.x) * (cx - zombie.x)) + ((cy - zombie.y) * (cy - zombie.y)))
        local levelDiff = math.abs(zombie.z - cz)
        if dist < result.dist and (not config.levelDiff or levelDiff <= config.levelDiff) then
            result.dist = dist
            result.x = zombie.x
            result.y = zombie.y
            result.z = zombie.z
            result.id = zombie.id
        end
    end

    return result
end

function BanditUtils.GetClosestBanditLocation(character, config)
    local result = {}
    local cid = BanditUtils.GetCharacterID(character)

    result.dist = math.huge
    result.x = false
    result.y = false
    result.z = false
    result.id = false

    if not config then config = {} end

    local cx, cy, cz = character:getX(), character:getY(), character:getZ()

    local zombieList = BanditZombie.CacheLightB
    for id, zombie in pairs(zombieList) do
        local dist = math.sqrt(((cx - zombie.x) * (cx - zombie.x)) + ((cy - zombie.y) * (cy - zombie.y)))
        local levelDiff = math.abs(zombie.z - cz)
        if dist < result.dist and cid ~= id and (not config.levelDiff or levelDiff <= config.levelDiff) then
            result.dist = dist
            result.x = zombie.x
            result.y = zombie.y
            result.z = zombie.z
            result.d = zombie.d
            result.id = zombie.id
        end
    end

    return result
end

function BanditUtils.GetClosestEnemyBanditLocation(character, config)
    local result = {}
    result.dist = math.huge
    result.x = false
    result.y = false
    result.z = false
    result.id = false

    if not config then config = {} end

    local cx, cy, cz = character:getX(), character:getY(), character:getZ()

    local banditList = BanditZombie.CacheLightB
    if instanceof(character, "IsoZombie") then
        local brain = BanditBrain.Get(character)
        for id, otherBandit in pairs(banditList) do
            if BanditUtils.AreEnemies(brain, otherBandit.brain) then
            -- if brain.clan ~= otherBandit.brain.clan and (brain.hostile or otherBandit.brain.hostile) then
                local dist = math.sqrt(((cx - otherBandit.x) * (cx - otherBandit.x)) + ((cy - otherBandit.y) * (cy - otherBandit.y)))
                local levelDiff = math.abs(otherBandit.z - cz)
                if dist < result.dist and (not config.levelDiff or levelDiff <= config.levelDiff) then
                    result.dist = dist
                    result.x = otherBandit.x
                    result.y = otherBandit.y
                    result.z = otherBandit.z
                    result.id = otherBandit.id
                end
            end
        end
    elseif instanceof(character, "IsoPlayer") then
        for id, otherBandit in pairs(banditList) do
            if otherBandit.brain.hostile or otherBandit.brain.hostileP then
                local dist = math.sqrt(((cx - otherBandit.x) * (cx - otherBandit.x)) + ((cy - otherBandit.y) * (cy - otherBandit.y)))
                local levelDiff = math.abs(otherBandit.z - cz)
                if dist < result.dist and (not config.levelDiff or levelDiff <= config.levelDiff) then
                    result.dist = dist
                    result.x = otherBandit.x
                    result.y = otherBandit.y
                    result.z = otherBandit.z
                    result.id = otherBandit.id
                end
            end
        end
    end
    return result
end

function BanditUtils.GetTarget(character, config)

    if not config then config = {} end

    local closestZombie = BanditUtils.GetClosestZombieLocation(character, config)
    local closestBandit = BanditUtils.GetClosestEnemyBanditLocation(character, config)
    local closestPlayer = BanditUtils.GetClosestPlayerLocation(character, config)

    local target = closestZombie
    local enemy = BanditZombie.Cache[target.id]

    if closestBandit.dist < closestZombie.dist then
        target = closestBandit
        enemy = BanditZombie.Cache[target.id]
    end

    local handicap = 5
    if Bandit.IsHostile(character) and closestPlayer.dist + handicap < closestBandit.dist then
        target = closestPlayer
        enemy = BanditPlayer.GetPlayerById(target.id)

    end

    if target.x and target.y and target.d then
        local i = target.dist
        local theta = target.d * 0.0174533  -- Convert degrees to radians
        target.fx = target.x + (i * math.cos(theta))
        target.fy = target.y + (i * math.sin(theta))
    end

    return target, enemy
end

function BanditUtils.GetMoveTask(endurance, x, y, z, walkType, dist, closeSlow)
    -- Move and GoTo generally do the same thing with a different method
    -- GoTo uses one-time move order, provides better synchronization in multiplayer, not perfect on larger distance
    -- Move uses constant updatating, it a better algorithm but introduces desync in multiplayer
    if dist < 0.5 then
        -- print ("SMALL DIST")
    end
    local gamemode = getWorld():getGameMode()
    local task
    if gamemode == "Multiplayer" then
        if dist > 30 then
            task = {action="Move", time=35, endurance=endurance, x=x, y=y, z=z, walkType=walkType, closeSlow=closeSlow}
        else
            task = {action="GoTo", time=50, endurance=endurance, x=x, y=y, z=z, walkType=walkType, closeSlow=closeSlow}
        end
    else
        task = {action="Move", time=20, endurance=endurance, x=x, y=y, z=z, walkType=walkType, closeSlow=closeSlow}
    end
    return task
end

function BanditUtils.GetMoveTaskTarget(endurance, x, y, z, tid, isPlayer, walkType, dist)
    -- Move and GoTo generally do the same thing with a different method
    -- GoTo uses one-time move order, provides better synchronization in multiplayer, not perfect on larger distance
    -- Move uses constant updatating, it a better algorithm but introduces desync in multiplayer
    if dist < 0.5 then
        -- print ("SMALL DIST")
    end
    local gamemode = getWorld():getGameMode()
    local task
    if gamemode == "Multiplayer" then
        if dist > 30 then
            task = {action="Move", time=35, endurance=endurance, x=x, y=y, z=z, tid=tid, isPlayer=isPlayer, walkType=walkType}
        else
            task = {action="GoTo", time=50, endurance=endurance, x=x, y=y, z=z, tid=tid, isPlayer=isPlayer, walkType=walkType}
        end
    else
        task = {action="Move", time=20, endurance=endurance, x=x, y=y, z=z, tid=tid, isPlayer=isPlayer, walkType=walkType}
    end
    return task
end

BanditUtils.HasAccessSquare = function(gridSquare)
    local testSquares = {
        gridSquare:getAdjacentSquare(IsoDirections.W),
        gridSquare:getAdjacentSquare(IsoDirections.E),
        gridSquare:getAdjacentSquare(IsoDirections.N),
        gridSquare:getAdjacentSquare(IsoDirections.S),
        gridSquare:getAdjacentSquare(IsoDirections.NW),
        gridSquare:getAdjacentSquare(IsoDirections.NE),
        gridSquare:getAdjacentSquare(IsoDirections.SW),
        gridSquare:getAdjacentSquare(IsoDirections.SE),
    }

    for _, testSquare in pairs(testSquares) do
        -- if AdjacentFreeTileFinder.privTrySquare(gridSquare, testSquare, {}) and testSquare:canReachTo(gridSquare) then return true end
        if testSquare:isFree(false) and not gridSquare:isWallTo(testSquare) then return true end
    end
end

BanditUtils.GetAccessSquare = function(gridSquare, bandit)
    local choices = {}
    local testSquares = {
        gridSquare:getAdjacentSquare(IsoDirections.W),
        gridSquare:getAdjacentSquare(IsoDirections.E),
        gridSquare:getAdjacentSquare(IsoDirections.N),
        gridSquare:getAdjacentSquare(IsoDirections.S),
        gridSquare:getAdjacentSquare(IsoDirections.NW),
        gridSquare:getAdjacentSquare(IsoDirections.NE),
        gridSquare:getAdjacentSquare(IsoDirections.SW),
        gridSquare:getAdjacentSquare(IsoDirections.SE),
    }

    for _, testSquare in pairs(testSquares) do
        -- if AdjacentFreeTileFinder.privTrySquare(gridSquare, testSquare, {}) and testSquare:canReachTo(gridSquare) then return true end
        if testSquare:isFree(false) and not gridSquare:isWallTo(testSquare) then 
            table.insert(choices, testSquare)
        end
    end

    if #choices > 0 then
        local lowestdist = math.huge
        local bestSquare
 
        for i, square in ipairs(choices) do
           local dist = square:DistToProper(bandit)
           if dist < lowestdist then
               lowestdist = dist
               bestSquare = square
           end
        end
 
         return bestSquare
     end
end

function BanditUtils.GetNumNearbyBuildings()
    local buildings = BanditUtils.GetNearbyBuildings()
    local buildingCount = 0    
    for _, building in pairs(buildings) do
        buildingCount = buildingCount + 1
    end
    return buildingCount
end

function BanditUtils.GetBuildingID(buildingDef)
    return buildingDef:getX() .. "-" .. buildingDef:getY() .. "-" .. buildingDef:getX2() .. "-" .. buildingDef:getY2()
end

--[[
local x1 = player:getX()
local y1 = player:getY()
local theta = bandit:getDirectionAngle() * math.pi / 180
local r = 5
local x2 = x1 + math.floor(r * math.cos(theta) + 0.5)
local y2 = y1 + math.floor(r * math.sin(theta) + 0.5)
local soundSquare = cell:getGridSquare(x2, y2, 0)
]]

function BanditUtils.findPoint(Ax, Ay, Bx, By, X)
    -- Calculate the direction vector from A to B
    local directionX = Bx - Ax
    local directionY = By - Ay

    -- Calculate the length of the direction vector
    local length = math.sqrt(directionX ^ 2 + directionY ^ 2)

    -- Normalize the direction vector
    local unitX = directionX / length
    local unitY = directionY / length

    -- Scale the unit vector by X units
    local scaledX = unitX * X
    local scaledY = unitY * X

    -- Calculate the coordinates of point P
    local Px = Ax + scaledX
    local Py = Ay + scaledY

    return Px, Py
end

function BanditUtils.Bresenham(x0, y0, x1, y1)
    local points = {}

    x0 = math.floor(x0)
    y0 = math.floor(y0)
    x1 = math.floor(x1)
    y1 = math.floor(y1)

    local dx = math.abs(x1 - x0)
    local dy = math.abs(y1 - y0)

    local sx = (x0 < x1) and 1 or -1
    local sy = (y0 < y1) and 1 or -1

    local err = dx - dy

    while true do
        table.insert(points, {x=x0, y=y0})

        if x0 == x1 and y0 == y1 then
            break
        end

        local e2 = 2 * err

        if e2 > -dy then
            err = err - dy
            x0 = x0 + sx
        end

        if e2 < dx then
            err = err + dx
            y0 = y0 + sy
        end
    end

    return points
end

function BanditUtils.IsWater(square)
    local objects = square:getObjects()
    for i=0, objects:size()-1 do
        local object = objects:get(i)
        local properties = object:getProperties()
        if properties then
            local water = properties:has(IsoFlagType.water)
            if water then
                return true
            end
        end
    end
    return false
end

function BanditUtils.GetGroundType(square)
    local groundType = "generic"
    local objects = square:getObjects()
    for i=0, objects:size()-1 do
        local object = objects:get(i)
        if object then
            local sprite = object:getSprite()
            if sprite then
                local spriteName = sprite:getName()
                if spriteName then
                    if spriteName:embodies("street") then
                        groundType = "street"
                    elseif spriteName:embodies("blends_natural") then
                        groundType = "grass"
                    end
                end
            end
        end
    end
    return groundType
end

function BanditUtils.ReplaceDrainable(item)
    local itemType = item:getFullType()
    local newItemType
    if itemType == "farming.WateredCan" then
        newItemType = "farming.WateredCanFull"
    elseif itemType == "Base.BucketEmpty" then
        newItemType = "Base.BucketWaterFull"
    end

    local newItem
    if newItemType then
        newItem = BanditCompatibility.InstanceItem(newItemType)
        newItem:setUsedDelta(0)
    else
        newItem = item
    end
    return newItem
end

function BanditUtils.DistTo(x1, y1, x2, y2)
    -- this is the fastest
    return math.sqrt(((x1 - x2) * (x1 - x2)) + ((y1 - y2) * (y1 - y2)))

    -- return math.sqrt(math.pow(x1 - x2, 2) + math.pow(y1 - y2, 2))
    -- return IsoUtils.DistanceTo(x1, y1, x2, y2)
end

function BanditUtils.LineClear(obj1, obj2)
    local cell = obj1:getCell()
    local x1, y1, z1 = obj1:getX(), obj1:getY(), obj1:getZ()
    local x2, y2, z2 = obj2:getX(), obj2:getY(), obj2:getZ()
    return tostring(LosUtil.lineClear(cell, x1, y1, z1, x2, y2, z2, false)) ~= "Blocked"
end

function BanditUtils.GetProjectileCount(reloadType)
    local projectiles = 1
    if reloadType == WeaponReloadType.SHOTGUN or reloadType == WeaponReloadType.DOUBLE_BARREL_SHOTGUN or reloadType == WeaponReloadType.DOUBLE_BARREL_SHOTGUN_SAWN then
        projectiles = 5
    end
    return projectiles
end

function BanditUtils.DistToManhattan(x1, y1, x2, y2)
    return math.abs(x1 - x2) + math.abs(y1 - y2)
end

function BanditUtils.Choice(arr)
    local r = 1 + ZombRand(#arr)
    return arr[r]
end

function BanditUtils.CoinFlip()
    if ZombRand(2) == 1 then 
        return true 
    else 
        return false 
    end
end

function BanditUtils.Lerp(x, min1, max1, min2, max2)
    return min2 + ((x - min1) / (max1 - min1)) * (max2 - min2)
end

-- deterministic rand for all clients
function BanditUtils.BanditRand(n)
    local a = 1664525
    local c = 1013904223
    local m = 2^32

    -- this is probably not perfect but
    -- the seed should be same for all clients most of the time
    local gameTime = getGameTime()
    local seed = gameTime:getMinutesStamp()

    seed = (a * seed + c) % m
    return seed % (n + 1)
end

BanditUtils.rgb2dec = function(r, g, b)
    local r255 = math.floor(r * 255 + 0.5)
    local g255 = math.floor(g * 255 + 0.5)
    local b255 = math.floor(b * 255 + 0.5)
    return r255 * 65536 + g255 * 256 + b255
end

BanditUtils.dec2rgb = function(value)
    local r = math.floor(value / 65536)
    local g = math.floor((value % 65536) / 256)
    local b = value % 256
    return {r = r / 255, g = g / 255, b = b / 255}
end

BanditUtils.GetCity = function(character)
    local zones = getZones(character:getX(), character:getY(), character:getZ())
    if zones then
        for i=0, zones:size()-1 do
            local zone = zones:get(i)
            if zone:getType() == "Region" then
                return zone:getName()
            end
        end
    end
end

BanditUtils.GetCityMap = function(city)
    local ret = {}
    local map = {
        ["Louisville"] = function()
            local tab = {}
            for i=1, 9 do
                table.insert(tab, "Base.LouisvilleMap" .. i)
            end
            return tab
        end,

        ["Jefferson"] = function()
            local tab = {}
            for i=1, 9 do
                table.insert(tab, "Base.LouisvilleMap" .. i)
            end
            return tab
        end,

        ["March Ridge"] = function()
            local tab = {}
            table.insert(tab, "Base.MarchRidgeMap")
            return tab
        end,

        ["Muldraugh"] = function()
            local tab = {}
            table.insert(tab, "Base.MuldraughMap")
            return tab
        end,

        ["Riverside"] = function()
            local tab = {}
            table.insert(tab, "Base.RiversideMap")
            return tab
        end,

        ["Rosewood"] = function()
            local tab = {}
            table.insert(tab, "Base.Rosewood")
            return tab
        end,

        ["West Point"] = function()
            local tab = {}
            table.insert(tab, "Base.WestPointMap")
            return tab
        end,

        ["Brandenburg"] = function()
            local tab = {}
            return tab
        end,

        ["Ekron"] = function()
            local tab = {}
            return tab
        end,

        ["Irvington"] = function()
            local tab = {}
            return tab
        end
    }

    if map[city] then
        ret = map[city]()
    end

    return ret
end

BanditUtils.GetStashMap = function(city)
    local ret = {}

    local map = {
        ["Louisville"] = function()
            local tab = {}
            for i=1, 16 do
                table.insert(tab, "Base.LouisvilleStashMap" .. i)
            end
            return tab
        end,

        ["Jefferson"] = function()
            local tab = {}
            for i=1, 16 do
                table.insert(tab, "Base.LouisvilleStashMap" .. i)
            end
            return tab
        end,

        ["March Ridge"] = function()
            local tab = {}
            for i=1, 10 do
                table.insert(tab, "Base.MarchRidgeStashMap" .. i)
            end
            return tab
        end,

        ["Muldraugh"] = function()
            local tab = {}
            for i=1, 18 do
                table.insert(tab, "Base.MarchRidgeStashMap" .. i)
            end
            return tab
        end,

        ["Riverside"] = function()
            local tab = {}
            for i=1, 10 do
                table.insert(tab, "Base.MarchRidgeStashMap" .. i)
            end
            return tab
        end,

        ["Rosewood"] = function()
            local tab = {}
            for i=1, 5 do
                table.insert(tab, "Base.MarchRidgeStashMap" .. i)
            end
            return tab
        end,

        ["West Point"] = function()
            local tab = {}
            for i=1, 16 do
                table.insert(tab, "Base.MarchRidgeStashMap" .. i)
            end
            return tab
        end,

        ["Brandenburg"] = function()
            local tab = {}
            for i=1, 8 do
                table.insert(tab, "Base.MarchRidgeStashMap" .. i)
            end
            return tab
        end,

        ["Ekron"] = function()
            local tab = {}
            for i=1, 8 do
                table.insert(tab, "Base.MarchRidgeStashMap" .. i)
            end
            return tab
        end,

        ["Irvington"] = function()
            local tab = {}
            for i=1, 10 do
                table.insert(tab, "Base.MarchRidgeStashMap" .. i)
            end
            return tab
        end
    }

    if map[city] then
        ret = map[city]()
    end

    return ret
end

BanditUtils.FixVolume = function(volume)
    local volumeMain = getSoundManager():getSoundVolume()
    local volumeZoom = 1

    local player = getSpecificPlayer(0)
    if player then
        local playerNum = player:getPlayerNum()
        local zoom = 1 / getCore():getZoom(playerNum)
        volumeZoom = BanditUtils.Lerp(zoom, 0.4, 4, 0.2, 1)
    end

    return volume * volumeMain * volumeZoom
end
