local configFileReader = require("configFileReader")
local playerManager = require("playerManager")
local perPlayerCostumes = require("scripts/perPlayerCostumes")

local textFiles = require("scripts/textFiles")
local easing = require("ext/easing")

local battleGeneral,battlePlayer,battleHUD,battleMap,battleMessages
local onlinePlay,onlinePlayNPC,onlinePlayPlayers
local bluePhanto

local battlePhanto = {}


battlePhanto.MASK_STATE = {
    MOVE_OVER = 1,
    PLACE_ON = 2,
    ATTACHED = 3,
}

battlePhanto.PUT_ON_STYLE = {
    INSTANT = 0,
    KEEP_MOVEMENT = 1,
    FORCED_STATE = 2,
}


battlePhanto.mainNPCID = 0
battlePhanto.fireNPCID = 0

battlePhanto.spawnerBGOID = 953


battlePhanto.maskRotationalFrames = 8

battlePhanto.statusMessageColor = Color.fromHexRGB(0x7FB4FF)


battlePhanto.initialSpawnTimeMin = 6*64
battlePhanto.initialSpawnTimeMax = 10*64

battlePhanto.spawnCountPortion = 0.5


battlePhanto.pointSound = Misc.resolveSoundFile("resources/point")

battlePhanto.fireSound = Misc.resolveSoundFile("resources/phanto_spit")
battlePhanto.fireOffsetX = 8
battlePhanto.fireOffsetY = 24
battlePhanto.fireRecoilSpeed = 1.5
battlePhanto.fireScaleMinAura = 1
battlePhanto.fireScaleMaxAura = 2.25

battlePhanto.fireChargeSound = Misc.resolveSoundFile("character/ub_knife")
battlePhanto.fireInitialChargeTime = 2*64
battlePhanto.fireRechargeTimeMinAura = 2*64
battlePhanto.fireRechargeTimeMaxAura = 0.75*64


battlePhanto.launchSound = Misc.resolveSoundFile("resources/phanto_launch")


battlePhanto.moveSound = Misc.resolveSoundFile("resources/phanto_wear_start")
battlePhanto.moveOverTime = 20

battlePhanto.putOnSound = Misc.resolveSoundFile("resources/phanto_wear_place")
battlePhanto.putOnSoundTime = 12
battlePhanto.putOnTime = 20
battlePhanto.putOnInitialOffset = -12
battlePhanto.putOnScale = 1.1


battlePhanto.auraBeginTime = 5*64
battlePhanto.auraGrowTime = 30*64
battlePhanto.auraEffectMinScale = 1.1
battlePhanto.auraEffectMaxScale = 1.4
battlePhanto.auraEffectMaxOpacity = 0.75


battlePhanto.speedBoostMinAura = 0.5
battlePhanto.speedBoostMaxAura = 1.5


battlePhanto.pointLossInitialTime = 3*64
battlePhanto.pointLossTime = 80

battlePhanto.tradePoints = 10

battlePhanto.startingPoints = 100


battlePhanto.allowSameTeamStompTransfer = false


battlePhanto.alignmentDebug = false


battlePhanto.spawnTimer = 0
battlePhanto.spawnCount = 0

battlePhanto.displayedFireTip = false
battlePhanto.displayedLaunchTip = false


battlePhanto.playerWearableForcedStateMap = table.map{
	FORCEDSTATE_NONE,
	FORCEDSTATE_POWERUP_BIG,
	FORCEDSTATE_POWERDOWN_SMALL,
	FORCEDSTATE_POWERUP_FIRE,
	FORCEDSTATE_POWERUP_LEAF,
	FORCEDSTATE_POWERUP_TANOOKI,
	FORCEDSTATE_POWERUP_HAMMER,
	FORCEDSTATE_POWERUP_ICE,
	FORCEDSTATE_POWERDOWN_FIRE,
	FORCEDSTATE_POWERDOWN_ICE,
}


battlePhanto.takenOffNPCFunctions = {}
battlePhanto.launchNPCFunctions = {}


local solidColorShader,greyscaleShader

local auraEffectShader
local auraEffectBuffer


local removeMaskCommand,transferMaskCommand,fireCommand,launchCommand,losePointsCommand,spawnCommand
local phantoJoinPacket


local characterSpeedModifier = {
    [CHARACTER_PEACH] = 0.93,
    [CHARACTER_TOAD] = 1.07,
}

local alignmentDebugScroll = 0


-- Mask configs (determines where to put the mask relative to the player's sprite)
local maskConfigs = {}

do
    local defaultFrameConfig = {x = 0,y = 0,angle = 0,behind = false}

    local costumeFallbacks = {
        [CHARACTER_MARIO] = {
            {
                prefix = "SMW-",
                folderPath = "resources\\maskConfig\\mario\\SMW-Mario\\",
            },
        },
        [CHARACTER_LUIGI] = {
            {
                prefix = "SMW-",
                folderPath = "resources\\maskConfig\\mario\\SMW-Luigi\\",
            },
        },
        [CHARACTER_TOAD] = {
            {
                prefix = "SMW-",
                folderPath = "resources\\maskConfig\\mario\\SMW-Toad\\",
            },
        },
    }

    local function getPossibleConfigFolderPaths(characterID,costumeName)
        local characterName = playerManager.getName(characterID)

        local episodePath = Misc.episodePath()
        local levelPath = Misc.levelPath()

        local folderPaths = {}

        if costumeName ~= nil then
            -- Costume's folder in maskConfig
            table.insert(folderPaths,1,episodePath.. "resources\\maskConfig\\".. characterName.. "\\".. costumeName.. "\\")
            table.insert(folderPaths,1,levelPath.. "resources\\maskConfig\\".. characterName.. "\\".. costumeName.. "\\")

            -- Basegame costume folder
            table.insert(folderPaths,1,getSMBXPath().. "costumes\\".. characterName.. "\\".. costumeName.. "\\battleMaskConfig\\")

            -- Episode costume folder
            table.insert(folderPaths,1,episodePath.. "costumes\\".. characterName.. "\\".. costumeName.. "\\battleMaskConfig\\")

            -- Check if any of the character's fallbacks apply for this costume
            if costumeFallbacks[characterID] ~= nil then
                for _,fallback in ipairs(costumeFallbacks[characterID]) do
                    if costumeName:sub(1,#fallback.prefix):upper() == fallback.prefix:upper() then
                        table.insert(folderPaths,levelPath.. fallback.folderPath)
                        table.insert(folderPaths,episodePath.. fallback.folderPath)
                    end
                end
            end
        end

        table.insert(folderPaths,levelPath.. "resources\\maskConfig\\".. characterName.. "\\default\\")
        table.insert(folderPaths,episodePath.. "resources\\maskConfig\\".. characterName.. "\\default\\")

        return folderPaths
    end

    local function resolveMaskConfigFile(config,folderPaths, filename)
        for _,folderPath in ipairs(folderPaths) do
            if io.exists(folderPath.. filename) then
                return folderPath.. filename
            end
        end
        
        return nil
    end

    local function loadMaskPowerupConfig(config,folderPaths, powerup)
        -- If already loaded, return it
        if config.powerupConfigs[powerup] ~= nil then
            return config.powerupConfigs[powerup]
        end

        -- Find the config file
        local characterName = playerManager.getName(config.characterID)

        local path = resolveMaskConfigFile(config,folderPaths, characterName.. "-".. powerup.. ".ini")

        if path == nil then
            return
        end

        -- Parse the file
        local data,headerData = configFileReader.parseWithHeaders(path,{},{},false,false)

        -- Initialise the powerup config
        local powerupConfig = {
            defaultTemplate = headerData.defaultTemplate,
            templates = {},
            frameConfigs = {},
        }

        if headerData.copyPowerup ~= nil then
            local copyPowerupConfig = loadMaskPowerupConfig(config,folderPaths, headerData.copyPowerup)

            powerupConfig.defaultTemplate = powerupConfig.defaultTemplate or copyPowerupConfig.defaultTemplate
            powerupConfig.templates = table.clone(copyPowerupConfig.templates)
        end

        -- Create templates
        for _,part in ipairs(data) do
            if part._header ~= nil then
                powerupConfig.templates[part._header] = {
                    baseTemplate = part.template,
                    x = part.x or 0,
                    y = part.y or 0,
                    angle = part.angle,
                    behind = part.behind,
                }
            end
        end

        -- Create frame configs from templates
        for frame = 0,50 do
            local angle,behind
            local x = 0
            local y = 0

            local templateName = "frame-".. frame

            if powerupConfig.templates[templateName] == nil then
                templateName = powerupConfig.defaultTemplate
            end

            while (templateName ~= nil and powerupConfig.templates[templateName] ~= nil) do
                local template = powerupConfig.templates[templateName]

                x = x + template.x
                y = y + template.y

                if angle == nil then
                    angle = template.angle
                end

                if behind == nil then
                    behind = template.behind
                end

                -- Move onto the base template
                templateName = template.baseTemplate
            end

            powerupConfig.frameConfigs[frame] = {
                x = x,
                y = y,
                angle = angle or 0,
                behind = behind or false,
            }
        end

        config.powerupConfigs[powerup] = powerupConfig

        return powerupConfig
    end

    function battlePhanto.getMaskConfig(p)
        local characterID = p.character
        local costumeName = p:getCostume()

        local configID = characterID.. ":".. (costumeName or "")

        if maskConfigs[configID] == nil then
            local folderPaths = getPossibleConfigFolderPaths(characterID,costumeName)

            local config = {
                characterID = characterID,
                costumeName = costumeName,
                powerupConfigs = {},
            }

            for powerup = 1,7 do
                loadMaskPowerupConfig(config,folderPaths, powerup)
            end

            maskConfigs[configID] = config
        end

        return maskConfigs[configID]
    end

    function battlePhanto.getMaskFrameConfig(p,powerup,frame)
        powerup = powerup or p.powerup
        frame = frame or p:getFrame()

        local maskConfig = battlePhanto.getMaskConfig(p)
        local powerupConfig = maskConfig.powerupConfigs[powerup]

        if powerupConfig == nil then
            return defaultFrameConfig
        end

        local frameConfig = powerupConfig.frameConfigs[frame]

        if frameConfig == nil then
            return defaultFrameConfig
        end

        return frameConfig
    end
end


function battlePhanto.getAuraStrength(p)
    local maskData = battlePhanto.getMaskData(p)

    if maskData == nil then
        return 0
    end

    return math.clamp((maskData.auraTimer - battlePhanto.auraBeginTime)/battlePhanto.auraGrowTime)
end


function battlePhanto.launcherIsEnabled(p)
    return false
end


function battlePhanto.getMaskPosition(p)
    local maskData = battlePhanto.getMaskData(p)

    if maskData == nil then
        return nil,nil,nil,nil,nil
    end

    local frame = p:getFrame()*p.direction
    local direction = (frame < 0 and DIR_LEFT) or DIR_RIGHT
    local frameConfig = battlePhanto.getMaskFrameConfig(p)

    -- Determine the base hitbox size to use. This is based off of the costume's originally-defined hitbox size, and not the actual hitbox size being used
    -- Also, if you're small and in a boot (clap your hands), big hitboxes are used instead of the small ones
    --local hitboxPowerup = (p.powerup == POWERUP_SMALL and p.mount == MOUNT_BOOT and POWERUP_BIG) or p.powerup
    --local hitboxWidth,hitboxHeight,hitboxDuckHeight = perPlayerCostumes.getOriginalHitboxSize(p,hitboxPowerup)
    local hitboxWidth,hitboxHeight,hitboxDuckHeight = perPlayerCostumes.getOriginalHitboxSize(p,p.powerup)

    if p.mount == MOUNT_BOOT then
        --local bigMarioSettings = PlayerSettings.get(CHARACTER_MARIO,POWERUP_BIG)
        local defaultSettings = PlayerSettings.get(playerManager.getBaseID(p.character),p.powerup)
        local bigHitboxWidth,bigHitboxHeight,bigHitboxDuckHeight = perPlayerCostumes.getOriginalHitboxSize(p,POWERUP_BIG)

        --hitboxHeight = bigMarioSettings.hitboxHeight
        --hitboxDuckHeight = bigHitboxDuckHeight

        hitboxHeight = p.height + (hitboxHeight - defaultSettings.hitboxHeight)

        if (p.character == CHARACTER_PEACH or p.character == CHARACTER_TOAD) and p.powerup == POWERUP_SMALL then
            hitboxHeight = hitboxHeight - 6
        end
    end

    local baseX = math.floor(p.x + 0.5) + p.width*0.5
    local baseY = math.floor(p.y + 0.5) + p.height - hitboxHeight

    if p.isDucking then
        baseY = math.floor(p.y + 0.5) + p.height - hitboxDuckHeight
    end

    if p.mount == MOUNT_YOSHI then
        baseY = baseY + p:mem(0x10E,FIELD_WORD)
    end

    local x = baseX + (frameConfig.x + maskData.offsetX)*direction
    local y = baseY + frameConfig.y + maskData.offsetY

    local frame = (frameConfig.angle + 4)%8 - 4

    if maskData.startPositionLerp > 0 then
        x = math.lerp(x,maskData.startX,maskData.startPositionLerp)
        y = math.lerp(y,maskData.startY,maskData.startPositionLerp)
    end

    if maskData.spinTime < 1 then
        --frame = math.floor(frame*maskData.spinTime + 0.5)
        frame = math.floor((frame - 8)*maskData.spinTime + 0.5)
    end

    frame = (frame*direction)%8

    return x,y,frame,direction,frameConfig.behind
end


function battlePhanto.getMaskData(p)
    return p.data.battlePhanto
end

function battlePhanto.isWearingMask(p)
    return (p.data.battlePhanto ~= nil)
end

function battlePhanto.isInMaskForcedState(p)
    local maskData = battlePhanto.getMaskData(p)

    return (
        maskData ~= nil
        and maskData.state ~= battlePhanto.MASK_STATE.ATTACHED
        and maskData.putOnStyle == battlePhanto.PUT_ON_STYLE.FORCED_STATE
        and p.forcedState == FORCEDSTATE_SWALLOWED
    )
end


function battlePhanto.putOnMask(p,npcID,putOnStyle,startX,startY)
    local maskData = {}

    maskData.wornNPCID = npcID

    maskData.putOnStyle = putOnStyle or battlePhanto.PUT_ON_STYLE.INSTANT

    maskData.startX = startX or (p.x + p.width*0.5)
    maskData.startY = startY or (p.y + p.height*0.5)

    maskData.scale = 1

    maskData.auraTimer = 0

    maskData.fireChargeDuration = battlePhanto.fireInitialChargeTime
    maskData.fireChargeTimer = maskData.fireChargeDuration

    maskData.pointLossTimer = battlePhanto.pointLossInitialTime

    maskData.flashTimer = 0

    if maskData.putOnStyle == battlePhanto.PUT_ON_STYLE.FORCED_STATE and (p.forcedState ~= FORCEDSTATE_NONE and not battlePhanto.isInMaskForcedState(p)) then
        maskData.putOnStyle = battlePhanto.PUT_ON_STYLE.KEEP_MOVEMENT
    end

    if maskData.putOnStyle == battlePhanto.PUT_ON_STYLE.INSTANT then
        maskData.state = battlePhanto.MASK_STATE.ATTACHED
        maskData.stateTimer = 0

        maskData.offsetX = 0
        maskData.offsetY = 0

        maskData.startPositionLerp = 0

        maskData.spinTime = 1
        maskData.greyscaleFactor = 1
    else
        maskData.state = battlePhanto.MASK_STATE.MOVE_OVER
        maskData.stateTimer = 0

        maskData.offsetX = 0
        maskData.offsetY = battlePhanto.putOnInitialOffset

        maskData.startPositionLerp = 1

        maskData.spinTime = 0
        maskData.greyscaleFactor = 0

        if onlinePlayPlayers.canMakeSound(p) or (onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or battleCamera.isOnScreen(maskData.startX,maskData.startY,0,0)) then
            SFX.play(battlePhanto.moveSound)
        end
    end

    if maskData.putOnStyle == battlePhanto.PUT_ON_STYLE.FORCED_STATE then
        p.forcedState = FORCEDSTATE_SWALLOWED
        p.forcedTimer = p.idx
        p:mem(0xBA,FIELD_WORD,p.idx)
    end

    p.data.battlePhanto = maskData
end


function battlePhanto.removeMaskInternal(p)
    if battlePhanto.isInMaskForcedState(p) then
        p.forcedState = FORCEDSTATE_NONE
        p.forcedTimer = 0
        p:mem(0xBA,FIELD_WORD,0)

        p.invincibilityTimer = 0
    end

    p.data.battlePhanto = nil
end

function battlePhanto.removeMask(p)
    if not battlePhanto.isWearingMask(p) or onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
        return
    end

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        removeMaskCommand:send(0, p.idx)
    end

    battlePhanto.removeMaskInternal(p)
end

function battlePhanto.takeOffMask(p)
    if not battlePhanto.isWearingMask(p) then
        return
    end

    if onlinePlayPlayers.ownsPlayer(p) or (onlinePlay.currentMode == onlinePlay.MODE_HOST and not onlinePlay.isConnected(p.idx)) then
        local maskData = battlePhanto.getMaskData(p)
        local x,y,_,_,_ = battlePhanto.getMaskPosition(p)

        local maskNPC = NPC.spawn(maskData.wornNPCID,x,y,p.section,false,true)

        maskNPC.direction = p.direction

        if battlePhanto.takenOffNPCFunctions[maskData.wornNPCID] ~= nil then
            battlePhanto.takenOffNPCFunctions[maskData.wornNPCID](maskNPC,p)
        end

        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            onlinePlayNPC.claimNPCSpawn(maskNPC)
        end
    end

    battlePhanto.removeMaskInternal(p)
end


local function transferMaskInternal(giverPlayer,receiverPlayer,putOnStyle)
    if not battlePhanto.isWearingMask(giverPlayer) or battlePhanto.isWearingMask(receiverPlayer) then
        return
    end

    if battleGeneral.mode == battleGeneral.gameMode.PHANTO and not battlePlayer.playersAreOnSameTeam(giverPlayer.idx,receiverPlayer.idx) then
        local playerData = battlePlayer.getPlayerData(giverPlayer)
        local awardedPoints = battlePhanto.tradePoints

        if battleMessages.victoryActive then
            awardedPoints = 0
        elseif battlePlayer.teamsAreEnabled() then
            local teamIdx = battlePlayer.getTeam(giverPlayer.idx)

            awardedPoints = math.min(battlePhanto.startingPoints - battlePlayer.teamPoints[teamIdx],awardedPoints)
            battlePlayer.teamPoints[teamIdx] = battlePlayer.teamPoints[teamIdx] + awardedPoints
        else
            awardedPoints = math.min(battlePhanto.startingPoints - playerData.points,awardedPoints)
            playerData.points = playerData.points + awardedPoints
        end

        if awardedPoints > 0 then
            battlePlayer.addPointEffect(giverPlayer,awardedPoints)
            battlePlayer.doPointHop(giverPlayer)

            if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or onlinePlayPlayers.ownsPlayer(giverPlayer) then
                SFX.play(battlePhanto.pointSound)
            end
        end
    end

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and onlinePlayPlayers.ownsPlayer(giverPlayer) then
        local name = battlePlayer.getName(receiverPlayer.idx)

        battleMessages.spawnStatusMessage(textFiles.funcs.replace(textFiles.battleMessages.phanto.tag,{NAME = name}),receiverPlayer.idx)
    end

    local giverMaskData = battlePhanto.getMaskData(giverPlayer)
    local maskX,maskY,_,_,_ = battlePhanto.getMaskPosition(giverPlayer)

    battlePhanto.putOnMask(receiverPlayer,giverMaskData.wornNPCID,putOnStyle,maskX,maskY)
    battlePhanto.removeMaskInternal(giverPlayer)

    battlePhanto.onTransferMask(receiverPlayer,giverPlayer)
end

function battlePhanto.transferMask(giverPlayer,receiverPlayer,putOnStyle)
    if not battlePhanto.isWearingMask(giverPlayer) or battlePhanto.isWearingMask(receiverPlayer) then
        return
    end

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        if not onlinePlayPlayers.ownsPlayer(giverPlayer) then
            return
        end

        transferMaskCommand:send(0, receiverPlayer.idx,putOnStyle)
    end

    transferMaskInternal(giverPlayer,receiverPlayer,putOnStyle)
end


local function canShootFire(p)
    return (
        p.forcedState == FORCEDSTATE_NONE
        and not p.isFairy
        and not p.isTanookiStatue
        and not p:isClimbing()
        and p.grabTopTimer == 0
        and (p.powerup ~= POWERUP_TANOOKI or not p.keys.up)
        and p.holdingNPC == nil
    )
end

local function canUseSpeedBoost(p)
    return (
        p.keys.run
        and p.speedX*p.direction >= Defines.player_runspeed*(characterSpeedModifier[p.character] or 1)
        and p.forcedState == FORCEDSTATE_NONE
        and not p.isFairy
        and not p.isTanookiStatue
        and not p:isClimbing()
        and not p:mem(0x3C,FIELD_BOOL) -- sliding
    )
end

local function getLaunchDirection(p)
    local direction = vector.zero2

    if p.keys.left then
        direction.x = -1
    elseif p.keys.right then
        direction.x = 1
    end

    if p.keys.up then
        direction.y = -1
    elseif p.keys.down then
        direction.y = 1
    end

    if direction.x == 0 and direction.y == 0 then
        direction.x = p.direction
    else
        direction = direction:normalise()
    end

    return direction
end

local function shootFire(p,x,y,direction,speedX,auraStrength)
    local maskData = battlePhanto.getMaskData(p)

    if maskData == nil then
        return
    end

    local fireNPC = NPC.spawn(battlePhanto.fireNPCID,x,y,p.section,false,true)

    fireNPC:mem(0x12E,FIELD_WORD,30)
    fireNPC:mem(0x130,FIELD_WORD,p.idx)
    fireNPC:mem(0x132,FIELD_WORD,p.idx)

    fireNPC.speedX = speedX
    fireNPC.direction = direction

    fireNPC.data.baseScale = math.lerp(battlePhanto.fireScaleMinAura,battlePhanto.fireScaleMaxAura,auraStrength)

    if p.speedX*direction <= 1 then
        p.speedX = p.speedX - battlePhanto.fireRecoilSpeed*direction
    end

    if p.character ~= CHARACTER_LINK then
        p:mem(0x160,FIELD_WORD,30)
    end

    p:mem(0x172,FIELD_BOOL,false)

    maskData.fireChargeDuration = math.lerp(battlePhanto.fireRechargeTimeMinAura,battlePhanto.fireRechargeTimeMaxAura,auraStrength)
    maskData.fireChargeTimer = maskData.fireChargeDuration

    if onlinePlayPlayers.canMakeSound(p) then
        SFX.play(battlePhanto.fireSound)
    end
end

local function launchMask(p,launchDirection)
    local maskData = battlePhanto.getMaskData(p)
    local x,y,_,_,_ = battlePhanto.getMaskPosition(p)

    local maskNPC = NPC.spawn(maskData.wornNPCID,x,y,p.section,false,true)

    maskNPC.direction = p.direction

    if battlePhanto.launchNPCFunctions[maskData.wornNPCID] ~= nil then
        battlePhanto.launchNPCFunctions[maskData.wornNPCID](maskNPC,p,launchDirection)
    end

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        launchCommand:send(0)
        onlinePlayNPC.claimNPCSpawn(maskNPC)
    end

    if onlinePlayPlayers.canMakeSound(p) then
        SFX.play(battlePhanto.launchSound)
    end

    battlePhanto.removeMaskInternal(p)
end

local function putOnMask(p)
    local maskData = battlePhanto.getMaskData(p)

    maskData.state = battlePhanto.MASK_STATE.ATTACHED
    maskData.stateTimer = 0

    maskData.offsetY = 0
    maskData.scale = 1
    maskData.spinTime = 1
    maskData.greyscaleFactor = 1
    maskData.startPositionLerp = 0

    if maskData.putOnStyle == battlePhanto.PUT_ON_STYLE.FORCED_STATE then
        p.forcedState = FORCEDSTATE_NONE
        p.forcedTimer = 0
        p:mem(0xBA,FIELD_WORD,0)

        p.invincibilityTimer = 100
    end

    if onlinePlayPlayers.ownsPlayer(p) and battleGeneral.mode == battleGeneral.gameMode.PHANTO then
        local textTable = textFiles.battleMessages.phanto.wear
        local text = (battlePhanto.spawnCount > 1 and textTable.multiple) or textTable.single

        battleMessages.spawnStatusMessage(text,p.idx,96)
    end
end

local function onTickPlayer(p)
    local maskData = battlePhanto.getMaskData(p)

    if maskData == nil then
        return
    end

    local playerData = battlePlayer.getPlayerData(p)

    if p.powerup == POWERUP_TANOOKI and not p.keys.up and not p.isTanookiStatue then
        p.tanookiStatueCooldown = math.max(p.tanookiStatueCooldown,2)
    end

    maskData.flashTimer = math.max(0,maskData.flashTimer - 1/12)

    if maskData.state == battlePhanto.MASK_STATE.ATTACHED then
        maskData.auraTimer = maskData.auraTimer + 1
        
        -- Fire cooldown
        if maskData.fireChargeTimer > 0 then
            maskData.fireChargeTimer = math.max(0,maskData.fireChargeTimer - 1)

            if maskData.fireChargeTimer == 0 then
                if onlinePlayPlayers.canMakeSound(p) then
                    SFX.play(battlePhanto.fireChargeSound,0.5)
                end

                if onlinePlayPlayers.ownsPlayer(p) then
                    if battlePhanto.launcherIsEnabled(p) then
                        if not battlePhanto.displayedLaunchTip then
                            battleMessages.spawnStatusMessage(textFiles.battleMessages.phanto.launchTip,Color.lightgrey,256)
                            battlePhanto.displayedLaunchTip = true
                        end
                    else
                        if not battlePhanto.displayedFireTip then
                            battleMessages.spawnStatusMessage(textFiles.battleMessages.phanto.fireTip,Color.lightgrey,160)
                            battlePhanto.displayedFireTip = true
                        end
                    end
                end

                maskData.flashTimer = 1
            end
        end

        -- Shooting fire/launching
        if onlinePlayPlayers.ownsPlayer(p) and p.keys.altRun == KEYS_PRESSED and maskData.fireChargeTimer == 0 and canShootFire(p) then
            local auraStrength = battlePhanto.getAuraStrength(p)

            if battlePhanto.launcherIsEnabled(p) then
                launchMask(p,getLaunchDirection(p))
            else
                local fireX = p.x + p.width*0.5 + battlePhanto.fireOffsetX*p.direction
                local fireY = p.y + math.min(battlePhanto.fireOffsetY,p.height*0.5)

                if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                    fireCommand:send(0, fireX,fireY,p.direction,p.speedX,auraStrength)
                end

                shootFire(p,fireX,fireY,p.direction,p.speedX,auraStrength)
            end
        end

        -- Losing points
        maskData.pointLossTimer = maskData.pointLossTimer - 1

        if battleGeneral.mode == battleGeneral.gameMode.PHANTO and not playerData.isDead then
            if maskData.pointLossTimer <= 0 and onlinePlayPlayers.ownsPlayer(p) and not battleMessages.victoryActive then
                if battlePlayer.teamsAreEnabled() then
                    local teamIdx = battlePlayer.getTeam(p.idx)

                    battlePlayer.teamPoints[teamIdx] = battlePlayer.teamPoints[teamIdx] - 1
                else
                    playerData.points = playerData.points - 1
                end

                if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                    losePointsCommand:send(0)
                end

                maskData.pointLossTimer = battlePhanto.pointLossTime
                playerData.pointsShakeFactor = 8

                --battlePlayer.addPointEffect(p,-1)
            end

            playerData.pointsShakeFactor = math.max(1,playerData.pointsShakeFactor)
        end

        if canUseSpeedBoost(p) and ((p.speedX > 0 and p.keys.right) or (p.speedX < 0 and p.keys.left)) then
            local speedBoost = math.lerp(battlePhanto.speedBoostMinAura,battlePhanto.speedBoostMaxAura,battlePhanto.getAuraStrength(p))

            p:mem(0x138,FIELD_FLOAT,math.sign(p.speedX)*speedBoost)
        end
    elseif maskData.state == battlePhanto.MASK_STATE.MOVE_OVER then
        maskData.stateTimer = maskData.stateTimer + 1

        local moveTime = math.min(1,maskData.stateTimer/battlePhanto.moveOverTime)

        maskData.startPositionLerp = easing.inOutCubic(moveTime,1,-1,1)
        maskData.scale = easing.inOutQuad(moveTime,1,battlePhanto.putOnScale - 1,1)

        if maskData.stateTimer >= (battlePhanto.moveOverTime + 4) then
            maskData.state = battlePhanto.MASK_STATE.PLACE_ON
            maskData.stateTimer = 0
        end
    elseif maskData.state == battlePhanto.MASK_STATE.PLACE_ON then
        maskData.stateTimer = maskData.stateTimer + 1

        local moveTime = math.min(1,maskData.stateTimer/battlePhanto.putOnTime)

        maskData.offsetY = easing.outCubic(moveTime,battlePhanto.putOnInitialOffset,-battlePhanto.putOnInitialOffset,1)
        maskData.scale = easing.outBack(moveTime,battlePhanto.putOnScale,1 - battlePhanto.putOnScale,1,10)

        maskData.spinTime = easing.outQuad(moveTime,0,1,1)

        maskData.greyscaleFactor = moveTime

        if maskData.stateTimer == battlePhanto.putOnSoundTime and onlinePlayPlayers.canMakeSound(p) then
            SFX.play(battlePhanto.putOnSound)
        end

        --if (onlinePlayPlayers.ownsPlayer(p) and maskData.stateTimer >= (battlePhanto.putOnTime + 4)) or p.forcedState ~= FORCEDSTATE_SWALLOWED then
        if (maskData.putOnStyle ~= battlePhanto.PUT_ON_STYLE.FORCED_STATE or onlinePlayPlayers.ownsPlayer(p)) and maskData.stateTimer >= (battlePhanto.putOnTime + 4) then
            putOnMask(p)
        end
    end

    if not onlinePlayPlayers.ownsPlayer(p)
    and maskData.state == battlePhanto.MASK_STATE.PLACE_ON
    and maskData.putOnStyle == battlePhanto.PUT_ON_STYLE.FORCED_STATE
    and not battlePhanto.isInMaskForcedState(p)
    then
        putOnMask(p)
    end

    if battlePhanto.isInMaskForcedState(p) and p.mount == MOUNT_NONE and not p.isDucking then
        if p.character == CHARACTER_LINK then
            p:setFrame(1)
        else
            p:setFrame(15)
        end
    end
end


local function getPlayerPriority(p)
    if p.forcedState == FORCEDSTATE_PIPE then
        return -70
    elseif p.mount == MOUNT_CLOWNCAR then
        return -35
    else
        return -25
    end
end


local function drawMaskOnPlayer(p)
    local maskData = battlePhanto.getMaskData(p)

    if greyscaleShader == nil then
        greyscaleShader = Shader.fromFile(nil,"resources/greyscale.frag")
    end

    if solidColorShader == nil then
        solidColorShader = Shader.fromFile(nil,"resources/solidColor.frag")
    end


    local npcConfig = NPC.config[maskData.wornNPCID]
    
    local maskImage = npcConfig.wornImage
    local maskWidth = maskImage.width
    local maskHeight = maskImage.height/battlePhanto.maskRotationalFrames
    local maskCutoff = 0

    local maskX,maskY,maskFrame,maskDirection,maskBehind = battlePhanto.getMaskPosition(p)

    -- Decide priority
    local maskPriority = getPlayerPriority(p)

    if maskBehind then
        maskPriority = maskPriority - 0.01
    else
        maskPriority = maskPriority + 0.01
    end

    -- Handling for cutoff when in a boot
    if p.mount == MOUNT_BOOT and maskData.state ~= battlePhanto.MASK_STATE.MOVE_OVER then
        -- Hide when ducking in a boot
        if p.isDucking then
            return
        end

        -- Cutoff from boot
        local cutoffY = math.floor(p.y + 0.5) + p.height - 30

        maskCutoff = math.clamp((maskY + maskHeight*maskData.scale*0.5 - cutoffY)/maskData.scale,0,maskHeight)
    end

    -- Decide shaders
    local shader,uniforms

    if maskData.flashTimer > 0 then
        shader = solidColorShader
        uniforms = {
            color = Color.white,
            extremity = math.min(maskData.flashTimer,1 - maskData.flashTimer)*1.5,
        }
    elseif maskData.fireChargeTimer > 0 then
        shader = greyscaleShader
        uniforms = {
            fade = math.lerp(0.25,1,maskData.fireChargeTimer/maskData.fireChargeDuration)*maskData.greyscaleFactor,
        }
    end

    Graphics.drawBox{
        texture = maskImage,priority = maskPriority,
        sceneCoords = true,centred = true,
        x = maskX,
        y = maskY - maskCutoff*maskData.scale*0.5,

        sourceWidth = maskWidth,
        sourceHeight = maskHeight - maskCutoff,

        width = maskWidth*maskData.scale,
        height = (maskHeight - maskCutoff)*maskData.scale,

        sourceY = maskFrame*maskHeight,

        shader = shader,uniforms = uniforms,
    }
end

local function drawAuraEffect(p)
    local maskData = battlePhanto.getMaskData(p)

    local auraStrength = battlePhanto.getAuraStrength(p)

    if auraStrength <= 0 then
        return
    end

    local npcConfig = NPC.config[maskData.wornNPCID]

    local priority = math.min(-66, getPlayerPriority(p)) - 0.02
    local frame

    if auraEffectBuffer == nil then
        auraEffectBuffer = Graphics.CaptureBuffer(200,200)

        auraEffectShader = Shader.fromFile(nil,"resources/phanto_auraEffect.frag")
    else
        auraEffectBuffer:clear(priority)
    end

    if p.frame == 15 and p.forcedState == FORCEDSTATE_PIPE and p.character == CHARACTER_LINK then
        frame = 1 -- Link is, for some reason, in frame 15 when going through a vertical pipe
    end

    p:render{
        target = auraEffectBuffer,priority = priority,sceneCoords = false,
        frame = frame,
        x = (auraEffectBuffer.width - p.width)*0.5,
        y = (auraEffectBuffer.height - p.height)*0.5,
    }

    Graphics.drawBox{
        texture = auraEffectBuffer,priority = priority,
        color = npcConfig.wornAuraEffectColor.. battlePhanto.auraEffectMaxOpacity*math.min(1,auraStrength*1.5),
        sceneCoords = true,centred = true,

        x = math.floor(p.x + 0.5) + p.width*0.5,
        y = math.floor(p.y + 0.5) + p.height*0.5,

        shader = auraEffectShader,uniforms = {
            time = lunatime.tick(),

            size = vector(auraEffectBuffer.width,auraEffectBuffer.height),

            strength = auraStrength,
            scale = math.lerp(battlePhanto.auraEffectMinScale,battlePhanto.auraEffectMaxScale,auraStrength),
        },
    }
end

local function onCameraDrawPlayer(p,camIdx)
    local maskData = battlePhanto.getMaskData(p)

    if maskData == nil or p.invincibilityFlash or p.isFairy then
        return
    end

    if perPlayerCostumes.invisiblePlayerStates[p.forcedState] and not perPlayerCostumes.getIsVisibleDespiteForcedState(p) then
        return
    end

    if perPlayerCostumes.getIsInvisible(p,camIdx) then
        return
    end

    drawMaskOnPlayer(p)
    drawAuraEffect(p)
end


local function drawAlignmentDebug(camIdx)
    local cam = Camera(camIdx)
    local gap = 64

    local x = -cam.renderX - alignmentDebugScroll
    local y = -cam.renderY

    local priority = 10

    local npcConfig = NPC.config[battlePhanto.mainNPCID]
    
    local maskImage = npcConfig.wornImage
    local maskWidth = maskImage.width
    local maskHeight = maskImage.height/battlePhanto.maskRotationalFrames

    for powerup = 1,7 do
        local defaultSettings = PlayerSettings.get(playerManager.getBaseID(player.character),powerup)
        local hitboxWidth,hitboxHeight,hitboxDuckHeight = perPlayerCostumes.getOriginalHitboxSize(player,powerup)

        for frame = 0,50 do
            local backgroundColor = ((powerup + frame)%2 == 0 and Color.lightgrey.. 0.75) or Color.grey.. 0.75

            local cellX = x + frame*gap
            local cellY = y

            if cellX > -gap and cellX < cam.width then
                local playerX = cellX + gap*0.5 - player.width*0.5
                local playerY = cellY + gap*0.5 - hitboxHeight*0.5
                local playerOffsetY

                local frameConfig = battlePhanto.getMaskFrameConfig(player,powerup,frame)

                if player.isDucking then
                    playerOffsetY = hitboxDuckHeight - defaultSettings.hitboxDuckHeight
                else
                    playerOffsetY = hitboxHeight - defaultSettings.hitboxHeight
                end

                Graphics.drawBox{
                    color = backgroundColor,
                    priority = priority - 0.3,
                    x = cellX,
                    y = cellY,
                    width = gap,
                    height = gap,
                }

                Text.printWP(frame,cellX,cellY,priority - 0.3)

                player:render{
                    priority = priority - 0.1,
                    ignorestate = true,
                    sceneCoords = false,

                    drawmounts = false,

                    x = playerX,
                    y = playerY + playerOffsetY,

                    frame = frame,
                    powerup = powerup,

                    direction = DIR_RIGHT,
                }

                if not player.rawKeys.run then
                    local maskX = playerX + hitboxWidth*0.5 + frameConfig.x
                    local maskY = playerY + frameConfig.y
                    local maskPriority = (frameConfig.behind and (priority - 0.2)) or priority
                    local maskFrame = frameConfig.angle%8

                    Graphics.drawBox{
                        texture = maskImage,priority = maskPriority,
                        centred = true,
                        x = maskX,
                        y = maskY,

                        sourceWidth = maskWidth,
                        sourceHeight = maskHeight,

                        sourceY = maskFrame*maskHeight,
                    }
                end
            end
        end

        y = y + gap
    end

    Text.printWP("Hold UP/DOWN to scroll",x,y,priority)
    y = y + 18
    Text.printWP("Press ALT RUN to reload config file",x,y,priority)
    y = y + 18
    Text.printWP("Hold RUN to view w/o mask",x,y,priority)
    y = y + 18
end


local function doSpawnMessage()
    local textTable = textFiles.battleMessages.phanto.spawn
    local text = (battlePhanto.spawnCount > 1 and textTable.multiple) or textTable.single

    battleMessages.spawnStatusMessage(text,battlePhanto.statusMessageColor)
    SFX.play(20)
end

local function getTotalDistanceToPlayers(x,y)
    local totalDistance = 0

    for _,p in ipairs(battlePlayer.getActivePlayers()) do
        local distX = p.x + p.width*0.5 - x
        local distY = p.y + p.height*0.5 - y

        totalDistance = totalDistance + math.sqrt(distX*distX + distY*distY)
    end

    return totalDistance
end

local function sortSpawners(bgoA,bgoB)
    return (getTotalDistanceToPlayers(bgoA.x + bgoA.width*0.5,bgoA.y + bgoA.height*0.5) > getTotalDistanceToPlayers(bgoB.x + bgoB.width*0.5,bgoB.y + bgoB.height*0.5))
end

local function findSpawners()
    local spawners = {}

    for _,v in BGO.iterate(battlePhanto.spawnerBGOID) do
        if not v.isHidden then
            table.insert(spawners,v)
        end
    end

    table.sort(spawners,sortSpawners)

    return spawners
end

local function spawnPhantos()
    local spawners = findSpawners()

    if #spawners == 0 then
        return
    end
    
    local uids = {}

    battlePhanto.spawnCount = math.max(1,math.ceil(battlePlayer.getActivePlayerCount()*battlePhanto.spawnCountPortion))

    for i = 1,battlePhanto.spawnCount do
        local spawner = spawners[(i - 1)%#spawners + 1]
        local offsetY = math.floor((i - 1)/#spawners)*48

        local v = NPC.spawn(battlePhanto.mainNPCID,spawner.x + spawner.width*0.5,spawner.y + spawner.height*0.5 - offsetY,nil,false,true)

        v.layerName = "Spawned NPCs"

        v.direction = DIR_LEFT
        v.spawnDirection = v.direction

        -- Spawn an effect
        local e = Effect.spawn(10,v.x + v.width*0.5,v.y + v.height*0.5)

        e.x = e.x - e.width *0.5
        e.y = e.y - e.height*0.5

        -- Send a message 
        if onlinePlay.currentMode == onlinePlay.MODE_HOST then
            onlinePlayNPC.claimNPCSpawn(v)

            table.insert(uids,onlinePlayNPC.getUIDFromNPC(v) or -1)
        end
    end

    if onlinePlay.currentMode == onlinePlay.MODE_HOST then
        spawnCommand:send(0, uids)
    end

    doSpawnMessage()
end


function battlePhanto.onTick()
    -- Spawning
    if battlePhanto.spawnTimer > 0 and onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
        battlePhanto.spawnTimer = math.max(0,battlePhanto.spawnTimer - 1)

        if battlePhanto.spawnTimer <= 0 then
            spawnPhantos()
        end
    end

    -- Update each of the player wearing a mask
    for _,p in ipairs(Player.get()) do
        onTickPlayer(p)
    end

    -- Debug
    if battlePhanto.alignmentDebug then
        if player.rawKeys.altRun == KEYS_PRESSED then
            maskConfigs = {}
        end

        if player.rawKeys.down then
            alignmentDebugScroll = alignmentDebugScroll + 4
        elseif player.rawKeys.up then
            alignmentDebugScroll = alignmentDebugScroll - 4
        end
    end
end

function battlePhanto.onCameraDraw(camIdx)
    for _,p in ipairs(Player.get()) do
        onCameraDrawPlayer(p,camIdx)
    end

    if battlePhanto.alignmentDebug then
        drawAlignmentDebug(camIdx)
    end
end

function battlePhanto.onStart()
    if battleGeneral.mode == battleGeneral.gameMode.PHANTO then
        -- Give all players 100 points for free
        for _,p in ipairs(Player.get()) do
            local playerData = battlePlayer.getPlayerData(p)

            playerData.points = battlePhanto.startingPoints
        end

        if battlePlayer.teamsAreEnabled() then
            for teamIdx = 1,2 do
                battlePlayer.teamPoints[teamIdx] = battlePhanto.startingPoints
            end
        end

        -- Set the initial time to spawn
        battlePhanto.spawnTimer = RNG.random(battlePhanto.initialSpawnTimeMin,battlePhanto.initialSpawnTimeMax)
    end

    --battlePhanto.putOnMask(player,960)
    --battlePhanto.putOnMask(player,751)
end


function battlePhanto.onInitAPI()
    registerEvent(battlePhanto,"onCameraDraw")
    registerEvent(battlePhanto,"onTick")
    registerEvent(battlePhanto,"onStart")

    registerCustomEvent(battlePhanto,"onTransferMask")


    battleGeneral = require("scripts/battleGeneral")
    battlePlayer = require("scripts/battlePlayer")
    battleHUD = require("scripts/battleHUD")
    battleMap = require("scripts/battleMap")
    battleMessages = require("scripts/battleMessages")
    onlinePlay = require("scripts/onlinePlay")
    onlinePlayNPC = require("scripts/onlinePlay_npc")
    onlinePlayPlayers = require("scripts/onlinePlay_players")


    removeMaskCommand = onlinePlay.createCommand("battle_phanto_remove",onlinePlay.IMPORTANCE_MAJOR)
    transferMaskCommand = onlinePlay.createCommand("battle_phanto_transfer",onlinePlay.IMPORTANCE_MAJOR)
    fireCommand = onlinePlay.createCommand("battle_phanto_fire",onlinePlay.IMPORTANCE_MAJOR)
    launchCommand = onlinePlay.createCommand("battle_phanto_launch",onlinePlay.IMPORTANCE_MAJOR)
    losePointsCommand = onlinePlay.createCommand("battle_phanto_losePoints",onlinePlay.IMPORTANCE_MAJOR)
    spawnCommand = onlinePlay.createCommand("battle_pha/nto_spawn",onlinePlay.IMPORTANCE_MAJOR)

    phantoJoinPacket = onlinePlay.createJoinPacket("battle_phanto")

    function removeMaskCommand.onReceive(sourcePlayerIdx, playerIdx)
        if sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        battlePhanto.removeMaskInternal(Player(playerIdx))
    end

    function transferMaskCommand.onReceive(sourcePlayerIdx, receiverPlayerIdx,putOnStyle)
        transferMaskInternal(Player(sourcePlayerIdx),Player(receiverPlayerIdx),putOnStyle)
    end

    function fireCommand.onReceive(sourcePlayerIdx, fireX,fireY,direction,speedX,auraStrength)
        shootFire(Player(sourcePlayerIdx), fireX,fireY,direction,speedX,auraStrength)
    end

    function launchCommand.onReceive(sourcePlayerIdx)
        local p = Player(sourcePlayerIdx)

        if battlePhanto.isWearingMask(p) then
            if onlinePlayPlayers.canMakeSound(p) then
                SFX.play(battlePhanto.launchSound)
            end

            battlePhanto.removeMaskInternal(p)
        end
    end

    function losePointsCommand.onReceive(sourcePlayerIdx)
        local playerData = battlePlayer.getPlayerData(Player(sourcePlayerIdx))

        if battlePlayer.teamsAreEnabled() then
            local teamIdx = battlePlayer.getTeam(sourcePlayerIdx)

            battlePlayer.teamPoints[teamIdx] = battlePlayer.teamPoints[teamIdx] - 1
        else
            playerData.points = playerData.points - 1
        end

        playerData.pointsShakeFactor = 8
    end

    function spawnCommand.onReceive(sourcePlayerIdx, phantoUIDs)
        if sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        for _,uid in ipairs(phantoUIDs) do
            local npc = onlinePlayNPC.getNPCFromUID(uid)

            if npc ~= nil then
                local e = Effect.spawn(10,npc.x + npc.width*0.5,npc.y + npc.height*0.5)

                e.x = e.x - e.width *0.5
                e.y = e.y - e.height*0.5
            end
        end

        battlePhanto.spawnCount = #phantoUIDs

        doSpawnMessage()
    end

    
    function phantoJoinPacket.encodeData(targetPlayerIdx)
        local encodedData = ""

        for _,p in ipairs(Player.get()) do
            if battlePhanto.isWearingMask(p) and p.idx ~= targetPlayerIdx then
                local maskData = battlePhanto.getMaskData(p)

                encodedData = encodedData.. onlinePlay.encodeValue("uint8",p.idx)
                encodedData = encodedData.. onlinePlay.encodeValue("uint16",maskData.wornNPCID)
                encodedData = encodedData.. onlinePlay.encodeValue("number",maskData.auraTimer)
            end
        end

        return encodedData
    end

    function phantoJoinPacket.onReceive(encodedData)
        -- Remove any Phantos from existing players
        for _,p in ipairs(Player.get()) do
            p.data.battlePhanto = nil
        end

        -- Give players Phantos as necessary
        local pos = 1

        while (pos <= #encodedData) do
            local playerIdx,npcID

            playerIdx,pos = onlinePlay.decodeValue("uint8",encodedData,pos)
            npcID,pos = onlinePlay.decodeValue("uint16",encodedData,pos)

            local p = Player(playerIdx)

            battlePhanto.putOnMask(p,npcID)

            local maskData = battlePhanto.getMaskData(p)

            maskData.auraTimer,pos = onlinePlay.decodeValue("number",encodedData,pos)
        end
    end


    function onlinePlay.onDisconnect(playerIdx)
        if onlinePlay.currentMode == onlinePlay.MODE_HOST then
            battlePhanto.takeOffMask(Player(playerIdx))
        end
    end

    --[[function battlePlayer.onPostPlayerKillCustom(p)
        if battlePhanto.isWearingMask(p) then
            battlePhanto.takeOffMask(p)
        end
    end]]

    function battlePlayer.onPlayerStomped(stompedPlayer,stompingPlayer)
        local isSameTeam = battlePlayer.playersAreOnSameTeam(stompedPlayer.idx,stompingPlayer.idx)

		if battlePhanto.allowSameTeamStompTransfer or not isSameTeam then
            local putOnStyle = (isSameTeam and battlePhanto.PUT_ON_STYLE.KEEP_MOVEMENT) or battlePhanto.PUT_ON_STYLE.FORCED_STATE

			battlePhanto.transferMask(stompingPlayer,stompedPlayer,putOnStyle)
		end
	end

    function battleMap.onMapDraw(sectionIdx,priority,opacity,mapX,mapY,mapWidth,mapHeight)
        if battleGeneral.mode ~= battleGeneral.gameMode.PHANTO then
            return
        end

        for _,npc in NPC.iterate(battlePhanto.mainNPCID) do
            if npc.isValid and npc.section == sectionIdx and npc.despawnTimer > 0 then
                local relativeX,relativeY = battleMap.getRelativePosition(npc.x + npc.width*0.5,npc.y + npc.height*0.5,Section(sectionIdx).boundary)

                Graphics.drawBox{
                    texture = battleHUD.phantoImage,priority = priority + 0.1,centred = true,
                    color = Color.white.. battleMap.iconOpacity*opacity,

                    x = mapX + mapWidth*relativeX,
                    y = mapY + mapHeight*relativeY,
                }
            end
        end
    end
end


return battlePhanto