local battleGeneral = require("scripts/battleGeneral")
local battlePlayer = require("scripts/battlePlayer")
local battleCamera = require("scripts/battleCamera")
local battlePhanto = require("scripts/battlePhanto")
local battleTimer = require("scripts/battleTimer")
local battleItems = require("scripts/battleItems")
local battleMenu = require("scripts/battleMenu")
local battleHUD = require("scripts/battleHUD")
local battleMessages = require("scripts/battleMessages")

local onlinePlay = require("scripts/onlinePlay")
local onlinePlayPlayers = require("scripts/onlinePlay_players")
local onlinePlayNPC = require("scripts/onlinePlay_npc")

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

local playerManager = require("playerManager")

local waveManager,tilemap,stages,shop,hud,startBlock,stageChangeEffect,menus,music
local playerSelect

local maskCollection = {}


maskCollection.GAME_STATE = {
    WAITING_ROOM = 1,
    PRE_WAVE = 2,
    WAVE = 3,
    WAVE_COMPLETION = 4,
    WAVE_FAILURE = 5,
}

maskCollection.GUIDE_STATE = {
    INACTIVE = 0,
    BOSSES = 1,
    PHANTOS = 2,
    PIPE = 3,
}


maskCollection.statusMessageColor = Color.fromHexRGB(0xFF3869)


maskCollection.section = 1

maskCollection.entranceWarpIdx = 5

maskCollection.maskNPCID = 751

maskCollection.basePlayerSpawnPoint = vector(-178816, -179744)

maskCollection.respawnDuration = 240
maskCollection.respawnDurationSingleplayer = 160
maskCollection.waveChangeRespawnDuration = 64

maskCollection.playerRespawnInvicibilityTime = 150


maskCollection.coinsAwardPerMask = 1


maskCollection.prewaveDuration = 160
maskCollection.prewaveReadyTime = 64
maskCollection.prewaveWaitTime = 24

maskCollection.completionDuration = 128
maskCollection.failureDuration = 256


maskCollection.gameState = maskCollection.GAME_STATE.WAITING_ROOM
maskCollection.gameStateTimer = 0
maskCollection.gameStateData = {quickContinue = true}

maskCollection.activeOnClientside = false


do
    SaveData.maskCollection = SaveData.maskCollection or {}
    maskCollection.saveData = SaveData.maskCollection

    GameData.maskCollection = GameData.maskCollection or {}
    maskCollection.gameData = GameData.maskCollection


    maskCollection.saveData.finishedGuide = maskCollection.saveData.finishedGuide or false
end


local gameStateChangeCommand = onlinePlay.createCommand("maskCollection_changeGameState",onlinePlay.IMPORTANCE_MAJOR)
local saveFilePushCommand = onlinePlay.createCommand("maskCollection_saveFilePush",onlinePlay.IMPORTANCE_MAJOR)
local startSpectatingCommand = onlinePlay.createCommand("maskCollection_startSpectating",onlinePlay.IMPORTANCE_MAJOR)

local saveFileJoinPacket = onlinePlay.createJoinPacket("maskCollection_saveFile")
local gameStateJoinPacket = onlinePlay.createJoinPacket("maskCollection_gameState")


local guideStateTextKeys = {
    [maskCollection.GUIDE_STATE.BOSSES] = "bosses",
    [maskCollection.GUIDE_STATE.PHANTOS] = "phantos",
    [maskCollection.GUIDE_STATE.PIPE] = "pipe",
}

local forcedStatePowerups = {
    [FORCEDSTATE_POWERUP_BIG]     = POWERUP_BIG,
    [FORCEDSTATE_POWERDOWN_SMALL] = POWERUP_SMALL,
    [FORCEDSTATE_POWERUP_FIRE]    = POWERUP_FIREFLOWER,
    [FORCEDSTATE_POWERUP_LEAF]    = POWERUP_LEAF,
    [FORCEDSTATE_POWERUP_TANOOKI] = POWERUP_TANOOKI,
    [FORCEDSTATE_POWERUP_HAMMER]  = POWERUP_HAMMER,
    [FORCEDSTATE_POWERUP_ICE]     = POWERUP_ICEFLOWER,
    [FORCEDSTATE_POWERDOWN_FIRE]  = POWERUP_BIG,
    [FORCEDSTATE_POWERDOWN_ICE]   = POWERUP_BIG,
}


local function setPlayerPowerup(p,powerup)
    p.powerup = powerup

    local settings = PlayerSettings.get(playerManager.getBaseID(p.character),p.powerup)

    p.x = p.x + (p.width - settings.hitboxWidth)*0.5
    p.y = p.y + p.height - settings.hitboxHeight

    p.width = settings.hitboxWidth
    p.height = settings.hitboxHeight

    if p.character ~= CHARACTER_MARIO and p.character ~= CHARACTER_LUIGI then
        if powerup == POWERUP_SMALL then
            p:mem(0x16,FIELD_WORD,1)
        else
            p:mem(0x16,FIELD_WORD,2)
        end
    end
end

local function movePlayersToStarts()
    for _,p in ipairs(battlePlayer.getActivePlayers()) do
        local data = battlePlayer.getPlayerData(p)

        if p.section == maskCollection.section and not data.isDead then
            -- Make effect
            local e = Effect.spawn(131,p.x + p.width*0.5,p.y + p.height*0.5)

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

            -- "Respawn"
            battlePlayer.reset(p,false)

            data.respawnDuration = maskCollection.waveChangeRespawnDuration
            data.respawnTimer = data.respawnDuration

            data.deathStart = vector(p.x + p.width*0.5,p.y + p.height)
            data.deathStartSection = p.section
            data.deathTimer = 0
            data.isDead = true
            data.nameOpacity = 0

            -- No bomb smuggling!
            data.bombs = 0

            -- Set powerup
            local playerSaveInfo = maskCollection.getPlayerSaveInfo(p.idx)

            if playerSaveInfo ~= nil then
                setPlayerPowerup(p,playerSaveInfo.powerup)
            else
                setPlayerPowerup(p,POWERUP_BIG)
            end
        end
    end
end


function maskCollection.getActivePlayersInSection()
    local tbl = {}

    for _,p in ipairs(Player.get()) do
        if p.section == maskCollection.section and battlePlayer.getPlayerIsActive(p) then
            table.insert(tbl,p)
        end
    end

    return tbl
end

function maskCollection.getActivePlayerInSectionCount()
    local count = 0

    for _,p in ipairs(Player.get()) do
        if p.section == maskCollection.section and battlePlayer.getPlayerIsActive(p) then
            count = count + 1
        end
    end

    return count
end

function maskCollection.anyoneIsInSection()
    for _,user in ipairs(onlinePlay.getUsers()) do
        local p = Player(user.playerIdx)

        -- Just in the section normally
        if p.section == maskCollection.section then
            return true
        end
        
        -- In a pipe going to the section
        if p.forcedState == FORCEDSTATE_PIPE then
            local warp = Warp(p:mem(0x15E,FIELD_WORD) - 1)

            if warp.exitSection == maskCollection.section then
                return true
            end
        end
    end

    return false
end


function maskCollection.killEverything()
    for _,npc in NPC.iterate() do
        if npc.section == maskCollection.section then
            if NPC.HITTABLE_MAP[npc.id] then
                onlinePlayNPC.forceKillNPC(npc,HARM_TYPE_NPC)
            else
                local effect = Effect.spawn(10,npc.x + npc.width*0.5,npc.y + npc.height*0.5)
                
                effect.x = effect.x - effect.width*0.5
                effect.y = effect.y - effect.height*0.5

                onlinePlayNPC.forceKillNPC(npc,HARM_TYPE_VANISH)
            end
        end
    end
end


function maskCollection.progressGuide(fromState,toState)
    if not maskCollection.activeOnClientside or maskCollection.gameState ~= maskCollection.GAME_STATE.WAVE then
        return
    end

    if maskCollection.gameStateData.guideState ~= fromState then
        return
    end

    if toState == maskCollection.GUIDE_STATE.INACTIVE then
        battleMessages.removeStatusMessage()
        maskCollection.saveData.finishedGuide = true
    else
        battleMessages.spawnStatusMessage(textFiles.maskCollection.guide[guideStateTextKeys[toState]],maskCollection.statusMessageColor,0)
    end

    maskCollection.gameStateData.guideState = toState
end


local function makeDefaultPlayerSaveInfo()
    return {
        powerup = POWERUP_BIG,
        coins = 0,

        purchasedUpgradeMap = {},
        equippedUpgradeMap = {},

        collectedMaskCount = 0,
        bossKillCount = 0,
    }
end

function maskCollection.initSaveFile()
    maskCollection.saveData.saveFile = {
        wave = 1,
        
        lastWavePlayerCount = 1,
        lastStageType = nil,
        bigBossGapWaves = 0,
        lastBigBossID = nil,

        localPlayerInfo = {},
        onlinePlayerInfo = {},
    }
end

function maskCollection.getSaveFile()
    if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
        return maskCollection.gameData.onlineSaveFile
    end

    return maskCollection.saveData.saveFile
end

function maskCollection.getPlayerSaveInfo(playerIdx)
    local saveFile = maskCollection.getSaveFile()

    if saveFile == nil then
        return
    end

    -- Local play
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        if playerIdx > 2 then
            return nil
        end

        if saveFile.localPlayerInfo[playerIdx] == nil then
            saveFile.localPlayerInfo[playerIdx] = makeDefaultPlayerSaveInfo()
        end

        return saveFile.localPlayerInfo[playerIdx]
    end

    -- Online play
    -- Host
    if playerIdx == onlinePlay.hostPlayerIdx then
        if saveFile.localPlayerInfo[1] == nil then
            saveFile.localPlayerInfo[1] = makeDefaultPlayerSaveInfo()
        end

        return saveFile.localPlayerInfo[1]
    end

    -- Client
    local userData = onlinePlay.getUserData(playerIdx)

    if userData == nil then
        return nil
    end

    local saveID = userData.saveID

    if saveID == nil then
        return nil
    end

    if saveFile.onlinePlayerInfo[saveID] == nil then
        saveFile.onlinePlayerInfo[saveID] = makeDefaultPlayerSaveInfo()
    end

    return saveFile.onlinePlayerInfo[saveID]
end

function maskCollection.pushSaveFile()
    if onlinePlay.currentMode == onlinePlay.MODE_HOST then
        saveFilePushCommand:send(0, maskCollection.getSaveFile())
    end

    if onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
        Misc.saveGame()
    end

    maskCollection.onSaveFilePush()
end


function maskCollection.begin()
    if maskCollection.saveData.saveFile == nil then
        maskCollection.initSaveFile()
    end

    local saveFile = maskCollection.getSaveFile()

    local waveIndex = saveFile.wave
    local difficulty = waveManager.getDifficulty(waveIndex)
    local stageType = stages.pickStageType(difficulty,saveFile.lastStageType)

    local musicTable = music.paths.waves[maskCollection.difficultyMusicIndices[difficulty]]
    local musicIndex = RNG.randomInt(1,#musicTable)

    maskCollection.changeGameState(maskCollection.GAME_STATE.PRE_WAVE,{
        waveIndex = waveIndex,
        difficulty = difficulty,
        stageType = stageType,
        musicIndex = musicIndex,
    })
end


function maskCollection.changeGameState(newState,stateChangeData)
    -- Deactivate the old state
    if maskCollection.gameState ~= nil then
        local oldStateConfig = maskCollection.gameStateConfig[maskCollection.gameState]

        if oldStateConfig.stopFunc ~= nil then
            oldStateConfig.stopFunc()
        end
    end

    -- Activate the new state
    local stateConfig = maskCollection.gameStateConfig[newState]

    maskCollection.gameState = newState
    maskCollection.gameStateData = {}

    maskCollection.gameStateTimer = 0

    if stateConfig.startFunc ~= nil then
        stateConfig.startFunc(stateChangeData or {})
    end

    if onlinePlay.currentMode == onlinePlay.MODE_HOST then
        gameStateChangeCommand:send(0, newState,stateChangeData)
    end
    
    maskCollection.onGameStateChange()
end


function maskCollection.onStart()
    --maskCollection.changeGameState(maskCollection.GAME_STATE.PRE_WAVE)
    --waveManager.startWave()

    music.change(music.paths.waitingRoom)

    battleGeneral.musicCanSpeedUp = false
    battleGeneral.disableMap = true

    battleTimer.isHidden = true

    battleTimer.hurryTime = 30
    battleTimer.doNothingOnTimeUp = true

    battlePhanto.allowSameTeamStompTransfer = false
    battlePhanto.fireInitialChargeTime = 48

    if maskCollection.getSaveFile() ~= nil then
        Layer.get("(Mask Collection) (Base) Powerups"):show(true)
    else
        Layer.get("(Mask Collection) (Base) Powerups"):hide(true)
    end
end

function maskCollection.onTick()
    -- Keep track of players' last grounded position
    -- This is used to know where to spawn the Phanto after the player dies
    for _,p in ipairs(Player.get()) do
        local playerData = battlePlayer.getPlayerData(p)

        if p.section == maskCollection.section then
            p.data.maskCollection = p.data.maskCollection or {}
            local data = p.data.maskCollection

            local groundY = tilemap.boxCastDown(p.x,p.width, p.y + p.height - 4,p.y + p.height + 256, true)

            if groundY ~= nil then
                data.groundPosition = vector(p.x + p.width*0.5,groundY)
            elseif data.groundPosition == nil then
                data.groundPosition = vector(p.x + p.width*0.5,p.y + p.height)
            end

            playerData.noplayerinteraction = (onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and maskCollection.gameState == maskCollection.GAME_STATE.WAITING_ROOM)
        else
            playerData.noplayerinteraction = false
        end
    end

    -- Disable NPCs despawning
    local sectionBounds = Section(maskCollection.section).origBoundary

    for _,npc in NPC.iterate() do
        if npc.section == maskCollection.section and npc.despawnTimer > 0 then
            if (npc.x + npc.width) >= sectionBounds.left and npc.x <= sectionBounds.right and (npc.y + npc.height) >= sectionBounds.top then
                npc.despawnTimer = math.max(10,npc.despawnTimer)
            end

            if npc.y >= sectionBounds.bottom and NPC.HITTABLE_MAP[npc.id] then
                npc:kill(HARM_TYPE_VANISH)
            end
        end
    end

    -- Update the game state
    local stateConfig = maskCollection.gameStateConfig[maskCollection.gameState]

    maskCollection.gameStateTimer = maskCollection.gameStateTimer + 1

    if stateConfig ~= nil and stateConfig.tickFunc ~= nil then
        stateConfig.tickFunc()
    end
end

function maskCollection.onCameraDraw(camIdx)
    if maskCollection.activeOnClientside then
        -- Drawing for the game state
        local stateConfig = maskCollection.gameStateConfig[maskCollection.gameState]

        if stateConfig ~= nil and stateConfig.cameraDrawFunc ~= nil then
            stateConfig.cameraDrawFunc(camIdx)
        end
    end
end


function maskCollection.onWarp(warp,p)
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE and (warp.entranceSection == maskCollection.section or warp.exitSection == maskCollection.section) then
        -- Get the other players in here too!
        for _,otherPlayer in ipairs(battlePlayer.getActivePlayers()) do
            if otherPlayer ~= p then
                battlePlayer.reset(otherPlayer,false)

                otherPlayer.forcedState = p.forcedState
                otherPlayer.forcedTimer = p.forcedTimer

                otherPlayer.x = p.x + (p.width - otherPlayer.width)*0.5
                otherPlayer.y = p.y + p.height - otherPlayer.height

                otherPlayer.section = p.section

                otherPlayer.frame = 15

                otherPlayer:mem(0x15E,FIELD_WORD,p:mem(0x15E,FIELD_WORD))

                playerSelect.triggerPipeDelay(otherPlayer,50)
            end
        end
    end
end

function maskCollection.onSectionChange(sectionIdx,p)
    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and onlinePlayPlayers.ownsPlayer(p) then
        if maskCollection.gameState == maskCollection.GAME_STATE.WAVE and sectionIdx == maskCollection.section and battlePlayer.getPlayerIsActive(p) and p.forcedState == FORCEDSTATE_PIPE then
            battlePlayer.setPlayerIsActive(p,false)
            p.section = sectionIdx

            battleCamera.isSpectating = true

            startSpectatingCommand:send(0, onlinePlay.encodeObject(p,onlinePlayPlayers.playerSendProperties))
        end
    end

    if sectionIdx == maskCollection.section then
        local playerSaveInfo = maskCollection.getPlayerSaveInfo(p.idx)

        if playerSaveInfo ~= nil then
            setPlayerPowerup(p,playerSaveInfo.powerup)
        else
            setPlayerPowerup(p,POWERUP_BIG)
        end
    else
        setPlayerPowerup(p,POWERUP_SMALL)
    end

    if maskCollection.activeOnClientside ~= (sectionIdx == maskCollection.section) and onlinePlayPlayers.ownsPlayer(p) then
        if sectionIdx == maskCollection.section then
            battleTimer.isHidden = false

            battleGeneral.disableSplitScreen = false
            battleCamera.initCameras()

            maskCollection.activeOnClientside = true
            maskCollection.onClientsideActivation()
        else
            battleTimer.isHidden = true

            battleGeneral.disableSplitScreen = true
            battleCamera.initCameras()

            maskCollection.activeOnClientside = false
            maskCollection.onClientsideDeactivation()
        end
    end
end


-- Game states
do
    maskCollection.gameStateConfig = {}

    -- Between waves/pre-game starting
    maskCollection.gameStateConfig[maskCollection.GAME_STATE.WAITING_ROOM] = {
        startFunc = function(stateChangeData)
            if stateChangeData.fromDefeat then
                music.change(music.paths.results)
            else
                music.change(music.paths.waitingRoom)
            end

            maskCollection.gameStateData.fromDefeat = stateChangeData.fromDefeat

            music.setVolume(1)

            stageChangeEffect.setUp()

            Layer.get("(Mask Collection) Exit Warp"):show(true)
            Layer.get("(Mask Collection) (Base)"):show(true)
            Layer.get("(Mask Collection) (Base) Ready Block"):show(true)

            if maskCollection.getSaveFile() ~= nil then
                Layer.get("(Mask Collection) (Base) Powerups"):show(true)
            else
                Layer.get("(Mask Collection) (Base) Powerups"):hide(true)
            end

            if waveManager.stageType ~= nil then
                Layer.get(stages.typeData[waveManager.stageType].layerName):hide(true)
            end

            tilemap.refresh()
            stageChangeEffect.begin()

            -- Unhide chillin' players
            if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                for _,p in ipairs(Player.get()) do
                    if p.section == maskCollection.section and onlinePlay.isConnected(p.idx) and not battlePlayer.getPlayerIsActive(p) then
                        battlePlayer.setPlayerIsActive(p,true)

                        local warp = Warp(maskCollection.entranceWarpIdx - 1)

                        p:teleport(warp.exitX + warp.exitWidth*0.5,warp.exitY - 4,true)

                        if onlinePlayPlayers.ownsPlayer(p) then
                            battleCamera.onlineFollowedPlayerIdx = onlinePlay.playerIdx
                            battleCamera.isSpectating = false
                        end
                    end
                end
            end

            -- Remove masks
            for _,p in ipairs(Player.get()) do
                if p.section == maskCollection.section then
                    battlePhanto.removeMaskInternal(p)
                end
            end

            -- Move players back to their respective starting points
            if not stateChangeData.dontMoveToStart then
                maskCollection.killEverything()
                movePlayersToStarts()
            end

            maskCollection.gameStateData.quickContinue = not not stateChangeData.quickContinue
        end,
        tickFunc = function()
            battleHUD.opacity = math.max(0,battleHUD.opacity - 1/16)
        end,
        getJoinStateChangeData = function()
            return {
                dontMoveToStart = true,
                fromDefeat = maskCollection.gameStateData.fromDefeat,
            }
        end,
    }

    -- "Ready..." before the wave actually starts
    maskCollection.gameStateConfig[maskCollection.GAME_STATE.PRE_WAVE] = {
        startFunc = function(stateChangeData)
            maskCollection.pushSaveFile()

            waveManager.waveIndex = stateChangeData.waveIndex
            waveManager.stageType = stateChangeData.stageType
            waveManager.musicIndex = stateChangeData.musicIndex
            waveManager.difficulty = stateChangeData.difficulty

            stageChangeEffect.setUp()

            Layer.get("(Mask Collection) Exit Warp"):hide(true)
            Layer.get("(Mask Collection) (Base)"):hide(true)
            Layer.get("(Mask Collection) (Base) Ready Block"):hide(true)
            Layer.get("(Mask Collection) (Base) Powerups"):hide(true)

            Layer.get(stages.typeData[waveManager.stageType].layerName):show(true)

            tilemap.refresh()
            stageChangeEffect.begin()

            movePlayersToStarts()

            maskCollection.gameStateData.countdownLayout = textplus.layout(textFiles.maskCollection.preWave,nil,hud.countdownTextFormat)
            maskCollection.gameStateData.countdownTimer = 0
        end,
        tickFunc = function()
            music.setVolume(1 - math.min(1,maskCollection.gameStateTimer/96))

            if maskCollection.gameStateTimer >= (maskCollection.prewaveDuration - maskCollection.prewaveWaitTime - maskCollection.prewaveReadyTime) then
                maskCollection.gameStateData.countdownTimer = math.min(1,maskCollection.gameStateData.countdownTimer + 1/maskCollection.prewaveReadyTime)
            end

            if maskCollection.gameStateTimer >= maskCollection.prewaveDuration then
                waveManager.startWave()
            end
        end,
        cameraDrawFunc = function(camIdx)
            local width,height = battleGeneral.getScreenSize()
            local cam = Camera(camIdx)

            local right = easing.inOutQuad(math.clamp(math.invlerp(0,0.6,maskCollection.gameStateData.countdownTimer)),0,1,1)
            local left = easing.inOutQuad(math.clamp(math.invlerp(0.4,1,maskCollection.gameStateData.countdownTimer)),0,1,1)

            hud.drawCountdown(maskCollection.gameStateData.countdownLayout,width*0.5 - cam.renderX,height*0.5 - cam.renderY,hud.countdownSettleScale,1,left,right)
        end,
        getJoinStateChangeData = function()
            return {
                waveIndex = waveManager.waveIndex,
                stageType = waveManager.stageType,
                musicIndex = waveManager.musicIndex,
                difficulty = waveManager.difficulty,
            }
        end,
    }

    -- Main wave state
    local START_TEXT_STATE = {
        APPEAR = 0,
        WAIT = 1,
        FADE = 2,
    }

    maskCollection.gameStateConfig[maskCollection.GAME_STATE.WAVE] = {
        startFunc = function(stateChangeData)
            local stateData = maskCollection.gameStateData

            if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
                waveManager.difficulty = stateChangeData.difficulty
                waveManager.maskQuota = stateChangeData.maskQuota

                waveManager.waveIndex = stateChangeData.waveIndex or waveManager.waveIndex
                waveManager.stageType = stateChangeData.stageType or waveManager.stageType
                waveManager.musicIndex = stateChangeData.musicIndex or waveManager.musicIndex
                waveManager.collectedMasks = stateChangeData.collectedMasks or 0
            end

            hud.updatePhantoCounterLayout()

            battleTimer.set(waveManager.waveLength)

            music.change(music.paths.waves[maskCollection.difficultyMusicIndices[waveManager.difficulty]][waveManager.musicIndex])
            music.setVolume(1)

            -- Set up for that text
            stateData.textLayout = textplus.layout(textFiles.maskCollection.waveStart,nil,hud.countdownTextFormat)
            stateData.textOpacity = 0
            stateData.textScale = 1

            stateData.textState = START_TEXT_STATE.APPEAR
            stateData.textStateTimer = 0

            -- Guide-related stuff
            stateData.guideState = maskCollection.GUIDE_STATE.INACTIVE

            if not maskCollection.saveData.finishedGuide then
                maskCollection.progressGuide(maskCollection.GUIDE_STATE.INACTIVE,maskCollection.GUIDE_STATE.BOSSES)

                battlePhanto.displayedFireTip = true
                battlePhanto.displayedLaunchTip = true
            end
        end,
        stopFunc = function()
            if maskCollection.gameStateData.guideState ~= maskCollection.GUIDE_STATE.INACTIVE then
                battleMessages.removeStatusMessage()

                battlePhanto.displayedFireTip = false
                battlePhanto.displayedLaunchTip = false
            end
        end,
        tickFunc = function()
            local stateData = maskCollection.gameStateData

            battleHUD.opacity = math.min(1,battleHUD.opacity + 1/16)

            if battleTimer.framesLeft <= 0 and onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
                if waveManager.collectedMasks >= waveManager.maskQuota then
                    maskCollection.changeGameState(maskCollection.GAME_STATE.WAVE_COMPLETION)
                else
                    maskCollection.changeGameState(maskCollection.GAME_STATE.WAVE_FAILURE)
                end
            end

            if stateData.textState == START_TEXT_STATE.APPEAR then
                stateData.textStateTimer = math.min(1,stateData.textStateTimer + 1/8)

                stateData.textScale = hud.countdownSettleScale*easing.inQuad(stateData.textStateTimer,2.5,-1.5,1)*1.5
                stateData.textOpacity = stateData.textStateTimer

                if stateData.textStateTimer >= 1 then
                    stateData.textState = START_TEXT_STATE.WAIT
                    stateData.textStateTimer = 0
                end
            elseif stateData.textState == START_TEXT_STATE.WAIT then
                stateData.textStateTimer = stateData.textStateTimer + 1

                if stateData.textStateTimer >= 64 then
                    stateData.textState = START_TEXT_STATE.FADE
                    stateData.textStateTimer = 0
                end
            elseif stateData.textState == START_TEXT_STATE.FADE then
                stateData.textOpacity = math.max(0,stateData.textOpacity - 1/16)
            end
        end,
        cameraDrawFunc = function(camIdx)
            local stateData = maskCollection.gameStateData

            if stateData.textOpacity > 0 then
                local width,height = battleGeneral.getScreenSize()
                local cam = Camera(camIdx)

                hud.drawCountdown(stateData.textLayout,width*0.5 - cam.renderX,height*0.5 - cam.renderY,stateData.textScale,stateData.textOpacity)
            end
        end,
        getJoinStateChangeData = function()
            return {
                waveIndex = waveManager.waveIndex,
                stageType = waveManager.stageType,
                musicIndex = waveManager.musicIndex,
                difficulty = waveManager.difficulty,
                maskQuota = waveManager.maskQuota,
                collectedMasks = waveManager.collectedMasks,
            }
        end,
    }

    -- Wave success
    local COMPLETION_TEXT_STATE = {
        APPEAR = 0,
        POP = 1,
        DISAPPEAR = 2,
    }

    maskCollection.gameStateConfig[maskCollection.GAME_STATE.WAVE_COMPLETION] = {
        startFunc = function(stateChangeData)
            local stateData = maskCollection.gameStateData

            if onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
                -- Update the save file
                local saveFile = maskCollection.getSaveFile()

                saveFile.wave = saveFile.wave + 1

                saveFile.lastWavePlayerCount = maskCollection.getActivePlayerInSectionCount()
                saveFile.lastStageType = waveManager.stageType

                if waveManager.bigBossID > 0 then
                    saveFile.bigBossGapWaves = RNG.randomInt(waveManager.bigBossMinGapWaves,waveManager.bigBossMaxGapWaves)
                    saveFile.lastBigBossID = waveManager.bigBossID
                else
                    saveFile.bigBossGapWaves = math.max(0,saveFile.bigBossGapWaves - 1)
                end

                -- Update players' powerups and award coins
                for _,p in ipairs(maskCollection.getActivePlayersInSection()) do
                    local playerSaveInfo = maskCollection.getPlayerSaveInfo(p.idx)

                    if playerSaveInfo ~= nil then
                        playerSaveInfo.coins = playerSaveInfo.coins + maskCollection.coinsAwardPerMask*waveManager.collectedMasks
                        playerSaveInfo.powerup = forcedStatePowerups[p.forcedState] or p.powerup

                        if playerSaveInfo.powerup == POWERUP_SMALL then
                            playerSaveInfo.powerup = POWERUP_BIG
                        end
                    end
                end

                maskCollection.pushSaveFile()
            end

            -- Kill all NPCs
            maskCollection.killEverything()

            -- Unhide chillin' players
            --[[if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                for _,p in ipairs(Player.get()) do
                    if p.section == maskCollection.section and onlinePlay.isConnected(p.idx) and not battlePlayer.getPlayerIsActive(p) then
                        battlePlayer.setPlayerIsActive(p,true)

                        p.forcedState = FORCEDSTATE_PIPE
                        p.forcedTimer = 100

                        p:mem(0x15E,FIELD_WORD,maskCollection.entranceWarpIdx)

                        local warp = Warp(p:mem(0x15E,FIELD_WORD) - 1)

                        p.x = warp.exitX + warp.exitWidth*0.5 - p.width*0.5
                        p.y = warp.exitY - p.height - 8
                        p.section = maskCollection.section
                        p.frame = 15

                        if onlinePlayPlayers.ownsPlayer(p) then
                            battleCamera.onlineFollowedPlayerIdx = onlinePlay.playerIdx
                            battleCamera.isSpectating = false
                        end
                    end
                end
            end]]

            -- Set up for that text
            local text = textFiles.funcs.replace(textFiles.maskCollection.waveCompletion,{WAVE = waveManager.waveIndex})

            stateData.textLayout = textplus.layout(text,nil,hud.countdownTextFormat)
            stateData.textAppearTime = 0
            stateData.textDisappearTime = 0
            stateData.textScale = 1

            stateData.textState = COMPLETION_TEXT_STATE.APPEAR
            stateData.textStateTimer = 0
        end,
        tickFunc = function()
            local stateData = maskCollection.gameStateData

            music.setVolume(1 - math.min(1,maskCollection.gameStateTimer/96))

            if stateData.textState == COMPLETION_TEXT_STATE.APPEAR then
                stateData.textAppearTime = math.min(1,stateData.textAppearTime + 1/24)

                if stateData.textAppearTime >= 1 then
                    stateData.textState = COMPLETION_TEXT_STATE.POP
                    stateData.textStateTimer = 0
                end
            elseif stateData.textState == COMPLETION_TEXT_STATE.POP then
                stateData.textStateTimer = stateData.textStateTimer + 1

                local scaleTime = math.clamp(math.invlerp(32,48,stateData.textStateTimer))

                stateData.textScale = 1 + math.min(scaleTime,1 - scaleTime)*0.5

                if stateData.textStateTimer >= 64 then
                    stateData.textState = COMPLETION_TEXT_STATE.DISAPPEAR
                    stateData.textStateTimer = 0
                end
            elseif stateData.textState == COMPLETION_TEXT_STATE.DISAPPEAR then
                stateData.textDisappearTime = math.min(1,stateData.textDisappearTime + 1/24)
            end

            if maskCollection.gameStateTimer >= maskCollection.completionDuration and onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
                maskCollection.changeGameState(maskCollection.GAME_STATE.WAITING_ROOM,{quickContinue = true})
            end
        end,
        cameraDrawFunc = function(camIdx)
            local stateData = maskCollection.gameStateData

            local width,height = battleGeneral.getScreenSize()
            local cam = Camera(camIdx)

            local right = easing.outQuad(stateData.textAppearTime,0,1,1)
            local left = easing.inQuad(stateData.textDisappearTime,0,1,1)

            local scale = hud.countdownSettleScale*stateData.textScale

            hud.drawCountdown(stateData.textLayout,width*0.5 - cam.renderX,height*0.5 - cam.renderY,scale,1,left,right)
        end,
    }

    -- Wave failure
    local FAILURE_TEXT_STATE = {
        APPEAR = 0,
        WAIT = 1,
        FADE = 2,
    }

    maskCollection.gameStateConfig[maskCollection.GAME_STATE.WAVE_FAILURE] = {
        startFunc = function(stateChangeData)
            local stateData = maskCollection.gameStateData

            -- Delete the save file
            if onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
                maskCollection.saveData.lastSaveFile = maskCollection.saveData.saveFile
                maskCollection.saveData.saveFile = nil
                maskCollection.pushSaveFile()
            end

            -- Kill all NPCs
            --maskCollection.killEverything()

            -- Set up for that text
            local text = textFiles.funcs.replace(textFiles.maskCollection.waveFailure,{WAVE = waveManager.waveIndex})

            stateData.textLayout = textplus.layout(text,nil,hud.countdownTextFormat)
            stateData.textOpacity = 0
            stateData.textScale = 1

            stateData.textState = FAILURE_TEXT_STATE.APPEAR
            stateData.textStateTimer = 0
        end,
        tickFunc = function()
            local stateData = maskCollection.gameStateData

            local musicFadeTime = math.min(1,maskCollection.gameStateTimer/160)

            music.setVolume(1 - math.clamp(math.invlerp(0.5,1,musicFadeTime)))
            music.setPitch(1 - 0.8*musicFadeTime)

            if stateData.textState == FAILURE_TEXT_STATE.APPEAR then
                stateData.textStateTimer = math.min(1,stateData.textStateTimer + 1/48)

                stateData.textScale = hud.countdownSettleScale*easing.outBounce(stateData.textStateTimer,3,-2,1)
                stateData.textOpacity = math.min(1,stateData.textStateTimer*2)

                if stateData.textStateTimer >= 1 then
                    stateData.textState = FAILURE_TEXT_STATE.WAIT
                    stateData.textStateTimer = 0
                end
            elseif stateData.textState == FAILURE_TEXT_STATE.WAIT then
                stateData.textStateTimer = stateData.textStateTimer + 1

                if stateData.textStateTimer >= 96 then
                    stateData.textState = FAILURE_TEXT_STATE.FADE
                    stateData.textStateTimer = 0
                end
            elseif stateData.textState == FAILURE_TEXT_STATE.FADE then
                stateData.textOpacity = math.max(0,stateData.textOpacity - 1/32)
            end

            if maskCollection.gameStateTimer >= maskCollection.failureDuration and onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
                maskCollection.changeGameState(maskCollection.GAME_STATE.WAITING_ROOM,{fromDefeat = true})
            end
        end,
        cameraDrawFunc = function(camIdx)
            local stateData = maskCollection.gameStateData

            local width,height = battleGeneral.getScreenSize()
            local cam = Camera(camIdx)

            hud.drawCountdown(stateData.textLayout,width*0.5 - cam.renderX,height*0.5 - cam.renderY,stateData.textScale,stateData.textOpacity)
        end,
    }
end


-- Overwritten functions
do
    local originalGetStartPoint = battlePlayer.getStartPoint
    local originalOnSameTeam = battlePlayer.playersAreOnSameTeam

    function battlePlayer.getStartPoint(idx)
        local p = Player(idx)

        if p.section == maskCollection.section and battlePlayer.getPlayerIsActive(p) then
            local players = maskCollection.getActivePlayersInSection()
            local playerIndex = table.ifind(players,p)
            local indexOffset = 0

            if #players > 1 and playerIndex ~= nil then
                indexOffset = playerIndex - (#players + 1)*0.5
            end

            return maskCollection.basePlayerSpawnPoint + vector(indexOffset*40,0)
        else
            return originalGetStartPoint(idx)
        end
    end

    function battlePlayer.hasHarmImmunity(p)
        if p.section ~= maskCollection.section then
            return true
        end

        if maskCollection.gameState ~= maskCollection.GAME_STATE.WAVE then
            return true
        end

        return false
    end

    function battlePlayer.hasDeathImmunity(p)
        return (p.section ~= maskCollection.section)
    end

    function battlePlayer.getRespawnDuration(p)
        if maskCollection.gameState ~= maskCollection.GAME_STATE.WAVE then
            return maskCollection.waveChangeRespawnDuration
        end

        if maskCollection.getActivePlayerInSectionCount() == 1 then
            return maskCollection.respawnDurationSingleplayer
        end

        return maskCollection.respawnDuration
    end

    function battlePlayer.playersAreOnSameTeam(indexA,indexB)
        if indexA == 0 or indexB == 0 then
            return false
        end

        local playerA = Player(indexA)
        local playerB = Player(indexB)

        if playerA.section ~= maskCollection.section or playerB.section ~= maskCollection.section then
            return originalOnSameTeam(indexA,indexB)
        end

        return true
    end

    function battleItems.playerHasReserveBox(p)
        return false
    end

    function battlePhanto.launcherIsEnabled(p)
        return shop.upgradeEquipped(p.idx,"launcher")
    end
end


function battlePlayer.onPostPlayerKillCustom(p)
    battlePhanto.takeOffMask(p)
end

function battlePlayer.onPlayerRespawn(p)
    p.invincibilityTimer = maskCollection.playerRespawnInvicibilityTime

    if maskCollection.gameState == maskCollection.GAME_STATE.WAITING_ROOM then
        local playerSaveInfo = maskCollection.getPlayerSaveInfo(p.idx)

        if playerSaveInfo ~= nil then
            setPlayerPowerup(p,playerSaveInfo.powerup)
        end
    end
end


function onlinePlay.onConnect(playerIdx)
    if playerIdx == onlinePlay.playerIdx then
        tilemap.refresh()
    end
end

function onlinePlay.onUninitialise()
    for _,npc in NPC.iterate() do
        if npc.section == maskCollection.section then
            npc:kill(HARM_TYPE_VANISH)
        end
    end

    maskCollection.gameData.onlineSaveFile = nil
    maskCollection.changeGameState(maskCollection.GAME_STATE.WAITING_ROOM,{dontMoveToStart = true})
    maskCollection.onSaveFilePush()

    battleCamera.isSpectating = false

    battleTimer.isActive = false
end


function gameStateChangeCommand.onReceive(sourcePlayerIdx, newState,stateChangeData)
    if onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
        return
    end

    maskCollection.changeGameState(newState,stateChangeData)
end

function saveFilePushCommand.onReceive(sourcePlayerIdx, newSaveFile)
    if onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
        return
    end

    maskCollection.gameData.onlineSaveFile = newSaveFile
    maskCollection.onSaveFilePush()
end

function startSpectatingCommand.onReceive(sourcePlayerIdx, playerProperties)
    if maskCollection.gameState ~= maskCollection.GAME_STATE.WAVE then
        return
    end

    local p = Player(sourcePlayerIdx)

    battlePlayer.setPlayerIsActive(p,false)
    onlinePlay.decodeObject(p,playerProperties,1,onlinePlayPlayers.playerSendProperties)
end


function saveFileJoinPacket.encodeData(targetPlayerIdx)
    return maskCollection.getSaveFile()
end

function saveFileJoinPacket.onReceive(newSaveFile)
    maskCollection.gameData.onlineSaveFile = newSaveFile
    maskCollection.onSaveFilePush()
end


function gameStateJoinPacket.encodeData(targetPlayerIdx)
    local stateConfig = maskCollection.gameStateConfig[maskCollection.gameState]
    local stateChangeData

    if stateConfig.getJoinStateChangeData ~= nil then
        stateChangeData = stateConfig.getJoinStateChangeData()
    end

    return maskCollection.gameState,stateChangeData
end

function gameStateJoinPacket.onReceive(gameState,stateChangeData)
    maskCollection.changeGameState(gameState,stateChangeData)
end



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

    registerEvent(maskCollection,"onWarp")
    registerEvent(maskCollection,"onSectionChange")

    registerCustomEvent(maskCollection,"onClientsideActivation")
    registerCustomEvent(maskCollection,"onClientsideDeactivation")
    registerCustomEvent(maskCollection,"onGameStateChange")
    registerCustomEvent(maskCollection,"onSaveFilePush")

    waveManager = require("maskCollection/scripts/waveManager")
    tilemap = require("maskCollection/scripts/tilemap")
    stages = require("maskCollection/scripts/stages")
    shop = require("maskCollection/scripts/shop")
    hud = require("maskCollection/scripts/hud")
    startBlock = require("maskCollection/scripts/startBlock")
    stageChangeEffect = require("maskCollection/scripts/stageChangeEffect")
    menus = require("maskCollection/scripts/menus")
    music = require("maskCollection/scripts/music")

    playerSelect = require("playerSelect")


    maskCollection.difficultyMusicIndices = {
        [waveManager.WAVE_DIFFICULTY.BEGINNING] = 1,
        [waveManager.WAVE_DIFFICULTY.EASY] = 1,
        [waveManager.WAVE_DIFFICULTY.MEDIUM] = 2,
        [waveManager.WAVE_DIFFICULTY.HARD] = 3,
        [waveManager.WAVE_DIFFICULTY.SPICY] = 3,
    }
end


return maskCollection