local playerManager = require("playerManager")
local configFileReader = require("configFileReader")

local perPlayerCostumes = {}


perPlayerCostumes.deathEffects = {}


local emptyImage = Graphics.loadImageResolved("stock-0.png")
local originalPlayerImages = {}

local deathEffectMap = {
    [CHARACTER_MARIO] = 3,
    [CHARACTER_LUIGI] = 5,
    [CHARACTER_PEACH] = 129,
    [CHARACTER_TOAD]  = 130,
    [CHARACTER_LINK]  = 134,
}

local characterBlocksMap = {
    [CHARACTER_MARIO] = 622,
    [CHARACTER_LUIGI] = 623,
    [CHARACTER_PEACH] = 624,
    [CHARACTER_TOAD] = 625,
    [CHARACTER_LINK] = 631,
}


perPlayerCostumes.basegameCostumeMap = table.map{
    "A2XT-DEMO",
    "SMW-MARIO",
    
    "A2XT-IRIS",
    "SMW-LUIGI",

    "A2XT-KOOD",
    "SMB3-PEACH",

    "A2XT-RAOCOW",
    "SMW-TOAD",

    "A2XT-SHEATH",
}

perPlayerCostumes.smwCostumeMap = table.map{
    "SMW-MARIO",
    "SMW-LUIGI",
    "SMW-TOAD",
}


perPlayerCostumes.invisiblePlayerStates = table.map{
    FORCEDSTATE_INVISIBLE,
    FORCEDSTATE_POWERUP_LEAF,
    FORCEDSTATE_SWALLOWED,
}


local originalPlayerRender = Player.render
local originalSetCostume = Player.setCostume
local originalGetCostume = Player.getCostume


function perPlayerCostumes.getCostume(playerIdx,character)
    local p = Player(playerIdx)

    if p.data.perPlayerCostumes == nil then
        return nil
    end

    character = character or Player(playerIdx).character

    return p.data.perPlayerCostumes.costumes[character]
end

function perPlayerCostumes.getAllCostumes(playerIdx)
    local p = Player(playerIdx)

    if p.data.perPlayerCostumes == nil then
        return {}
    end

    local costumes = {}

    for character = 1,5 do
        costumes[character] = p.data.perPlayerCostumes.costumes[character]
    end

    return costumes
end


local function setHeldNPCPosition(p,x,y)
	local holdingNPC = p.holdingNPC

	holdingNPC.x = x
	holdingNPC.y = y


	if holdingNPC.id == 49 and holdingNPC.ai2 > 0 then -- toothy pipe
		-- You'd think that redigit's pointers work, but nope! this has to be done instead
		for _,toothy in NPC.iterate(50,p.section) do
			if toothy.ai1 == p.idx then
				if p.direction == DIR_LEFT then
					toothy.x = holdingNPC.x - toothy.width
				else
					toothy.x = holdingNPC.x + holdingNPC.width
				end

				toothy.y = holdingNPC.y
			end
		end
	end
end

local function handleHeldNPC(p)
    -- This is dumb, but the SMW Toad costume needs some stuff to fix the held NPC position
    if (p.character ~= CHARACTER_PEACH and p.character ~= CHARACTER_TOAD) or p.holdingNPC == nil or p.inClearPipe then
        return
    end

    local data = p.data.perPlayerCostumes

    if data == nil then
        return
    end

    local costumeName = perPlayerCostumes.getCostume(p.idx)

    if costumeName == nil or data.config[p.powerup] == nil then
        return
    end

    local config = data.config[p.powerup]

    -- If the grab offset is small (as it is for the SMW Toad costume), overwrite the held position
    if config.grabOffsetX < 12 then
        local heldNPCX = p.x + p.width*0.5 - p.holdingNPC.width*0.5 + config.grabOffsetX
		local heldNPCY = p.y - p.holdingNPC.height + config.grabOffsetY

        setHeldNPCPosition(p,heldNPCX,heldNPCY)
    end
end


local function resolveCostumeFile(character,costumeName,filename)
    local characterName = playerManager.getName(character)

    return Misc.multiResolveFile(costumeName.. "\\".. filename, "costumes\\".. characterName.. "\\".. costumeName.. "\\".. filename)
end


local costumeLibraries = {}

local function loadCostumeLuaFile(character,costumeName)
    costumeLibraries[character] = costumeLibraries[character] or {}

    -- Look for the file
    local costumeLuaPath = resolveCostumeFile(character,costumeName,"costume.lua")

    if costumeLuaPath == nil then
        costumeLibraries[character][costumeName] = {}
        return
    end

    -- Load the file using Lua's "loadfile" function
    local func,err = loadfile(costumeLuaPath)

    if func == nil then
        error(err)
    end

    -- Run the resulting function - did it actually return a table?
    local costumeLibrary = func()

    if type(costumeLibrary) ~= "table" then
        error("Costume Lua file \"".. path.. "\" did not return a table (got ".. type(luafile).. ")")
    end

    -- Save it
    costumeLibraries[character][costumeName] = costumeLibrary
end


function perPlayerCostumes.parsePlayerConfigFile(path)
    -- Initialise the config
    local config = {offsets = {}}

    for frameX = 0,9 do
        config.offsets[frameX] = {}

        for frameY = 0,9 do
            config.offsets[frameX][frameY] = vector.zero2
        end
    end

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

    for _,part in ipairs(data) do
        if part._header == "common" then
            config.hitboxWidth = part["width"]
            config.hitboxHeight = part["height"]
            config.hitboxDuckHeight = part["height-duck"]
            config.grabOffsetX = part["grab-offset-x"]
            config.grabOffsetY = part["grab-offset-y"]
        elseif part._header ~= nil then
            local frameX,frameY = part._header:match("^frame%-(%d+)%-(%d+)$")

            frameX = tonumber(frameX)
            frameY = tonumber(frameY)

            if frameX ~= nil and frameY ~= nil and frameX >= 0 and frameY >= 0 and frameX <= 9 and frameY <= 9 then
                config.offsets[frameX][frameY] = vector(part.offsetX or 0,part.offsetY or 0)
            end
        end
    end

    return config
end


local activeSwapMap = {
    npc = {},
    effect = {},
    block = {},
}

local function isHigherPrioritySwap(swapA,swapB)
    -- Give priority to the earlier player
    return (swapA.playerIdx < swapB.playerIdx)
end

local function getHighestPrioritySwap(swapList)
    local bestSwap

    for _,swap in ipairs(swapList) do
        if bestSwap == nil or isHigherPrioritySwap(swap,bestSwap) then
            bestSwap = swap
        end
    end
    
    return bestSwap
end

local function removeItemFromList(tbl,item)
    for index,value in ipairs(tbl) do
        if value == item then
            table.remove(tbl,index)
            break
        end
    end
end

local function loadCostumeSpriteSwaps(playerIdx)
    -- Load any NPC or effect images
    local p = Player(playerIdx)
    local data = p.data.perPlayerCostumes

    local costumeName = perPlayerCostumes.getCostume(playerIdx)
    local characterName = playerManager.getName(p.character)

    local costumeFolderPath = Misc.resolveDirectory("costumes\\".. characterName.. "\\".. costumeName)

    for _,filename in ipairs(Misc.listFiles(costumeFolderPath)) do
        filename = filename:lower()

        -- NPCs
        local npcID = filename:match("^npc%-(%d+)%.png$")

        if npcID ~= nil then
            table.insert(data.spriteSwaps,{
                playerIdx = playerIdx,
                costumeName = costumeName,
                image = Graphics.loadImage(costumeFolderPath.. "\\".. filename),
                id = tonumber(npcID),
                type = "npc",
            })
        end

        -- Effects
        local effectID = filename:match("^effect%-(%d+)%.png$")

        if effectID ~= nil then
            if tonumber(effectID) == deathEffectMap[p.character] then
                data.deathEffectImage = Graphics.loadImage(costumeFolderPath.. "\\".. filename)
            else
                table.insert(data.spriteSwaps,{
                    playerIdx = playerIdx,
                    costumeName = costumeName,
                    image = Graphics.loadImage(costumeFolderPath.. "\\".. filename),
                    id = tonumber(effectID),
                    type = "effect",
                })
            end
        end

        -- Character switch block
        local blockID = filename:match("^block%-(%d+)%.png$")

        if blockID ~= nil and tonumber(blockID) == characterBlocksMap[p.character] then
            table.insert(data.spriteSwaps,{
                playerIdx = playerIdx,
                costumeName = costumeName,
                image = Graphics.loadImage(costumeFolderPath.. "\\".. filename),
                id = tonumber(blockID),
                type = "block",
            })
        end
    end
end

local function applyCostumeSpriteSwaps(playerIdx)
    -- This is complicated because of the need to consistently handle multiple costumes wanting to replace the same sprite.
    -- In that case, it will prioritise the costume which belongs to the player of a lower index.
    -- Swaps that could apply in future but currently aren't active are put into a "backlog"
    local p = Player(playerIdx)
    local data = p.data.perPlayerCostumes

    for _,swap in ipairs(data.spriteSwaps) do
        local existingSwap = activeSwapMap[swap.type][swap.id]

        if existingSwap == nil then
            Graphics.sprites[swap.type][swap.id].img = swap.image
            activeSwapMap[swap.type][swap.id] = {
                active = swap,
                backlog = {},
            }
        else
            if isHigherPrioritySwap(swap,existingSwap.active) then
                -- This swap belongs to an earlier player; it should therefore take priority and move the existing swap into the backlog
                table.insert(existingSwap.backlog,existingSwap.active)
                existingSwap.active = swap

                Graphics.sprites[swap.type][swap.id].img = swap.image
            else
                -- This swap doesn't take priority, so just add it to the backlog
                table.insert(existingSwap.backlog,swap)
            end
        end
    end
end

local function unapplyCostumeSpriteSwaps(playerIdx)
    local p = Player(playerIdx)
    local data = p.data.perPlayerCostumes

    for _,swap in ipairs(data.spriteSwaps) do
        local existingSwap = activeSwapMap[swap.type][swap.id]

        if existingSwap ~= nil then
            if existingSwap.active == swap then
                -- Find the best item from the backlog to use as a replacement
                local replacementSwap = getHighestPrioritySwap(existingSwap.backlog)

                if replacementSwap ~= nil then
                    Graphics.sprites[swap.type][swap.id].img = replacementSwap.image
                    removeItemFromList(existingSwap.backlog,replacementSwap)
                    existingSwap.active = replacementSwap
                else
                    -- Nothing left in the backlog: we can forget about this swap entirely now
                    Graphics.sprites[swap.type][swap.id].img = nil
                    activeSwapMap[swap.type][swap.id] = nil
                end
            else
                -- Remove from the backlog
                removeItemFromList(existingSwap.backlog,swap)
            end
        end
    end
end


local function deactivateOldCostume(playerIdx,character)
    local p = Player(playerIdx)

    local costumeName = perPlayerCostumes.getCostume(playerIdx,character)

    -- If it's the default costume, no need to do anything more
    if costumeName == nil then
        return
    end

    local data = p.data.perPlayerCostumes


    for powerup = 1,7 do
        -- Unload player graphics
        data.playerImages[powerup] = nil

        -- Unload ini files
        data.config[powerup] = nil
    end

    -- Unload sprite swaps
    unapplyCostumeSpriteSwaps(p.idx)
    data.spriteSwaps = {}

    data.deathEffectImage = nil

    -- Deactivate the costume.lua file
    if costumeLibraries[character] ~= nil and costumeLibraries[character][costumeName] ~= nil then
        local costumeLibrary = costumeLibraries[character][costumeName]

        if type(costumeLibrary.onCleanup) == "function" then
            costumeLibrary.onCleanup(p)
        end
    end
end

local function activateCostume(playerIdx)
    local p = Player(playerIdx)

    local costumeName = perPlayerCostumes.getCostume(playerIdx)
    local character = p.character

    -- If it's the default costume, no need to do anything more
    if costumeName == nil then
        return
    end

    local data = p.data.perPlayerCostumes

    local characterName = playerManager.getName(character)


    data.initialisedCharacter = character


    for powerup = 1,7 do
        -- Load player graphics
        local imagePath = resolveCostumeFile(character,costumeName,characterName.. "-".. powerup.. ".png")

        if imagePath ~= nil then
            data.playerImages[powerup] = Graphics.loadImage(imagePath)
        else
            data.playerImages[powerup] = nil
        end

        -- Load ini files
        local configPath = resolveCostumeFile(character,costumeName,characterName.. "-".. powerup.. ".ini")

        if configPath ~= nil then
            data.config[powerup] = perPlayerCostumes.parsePlayerConfigFile(configPath)
        else
            data.config[powerup] = nil
        end
    end

    -- Load sprite swaps
    loadCostumeSpriteSwaps(playerIdx)
    applyCostumeSpriteSwaps(playerIdx)


    -- Load the costume's Lua file if necessary
    if costumeLibraries[character] == nil or costumeLibraries[character][costumeName] == nil then
        loadCostumeLuaFile(character,costumeName)
    end

    -- Activate the costume's Lua file
    local costumeLibrary = costumeLibraries[character][costumeName]

    if type(costumeLibrary.onInit) == "function" then
        costumeLibrary.onInit(p)
    end
end


local function isValidCostume(character,costumeName)
    return table.icontains(playerManager.getCostumes(character),costumeName)
end

function perPlayerCostumes.setCostume(playerIdx,character,costumeName)
    local p = Player(playerIdx)

    -- Validate the costume's name
    if costumeName ~= nil then
        costumeName = costumeName:upper()
        costumeName = (isValidCostume(character,costumeName) and costumeName) or nil
    end

    if costumeName == perPlayerCostumes.getCostume(playerIdx,character) then
        return
    end


    p.data.perPlayerCostumes = p.data.perPlayerCostumes or {
        initialisedCharacter = p.character,
        costumes = {},
        playerImages = {},
        config = {},
        spriteSwaps = {},
    }

    if p.data.perPlayerCostumes.initialisedCharacter == character then
        deactivateOldCostume(playerIdx,character)
    end

    p.data.perPlayerCostumes.costumes[character] = costumeName

    if p.character == character then
        activateCostume(playerIdx)
    end
end

function perPlayerCostumes.setAllCostumes(playerIdx,costumes)
    for character = 1,5 do
        perPlayerCostumes.setCostume(playerIdx,character,costumes[character])
    end
end


local function guessDeathEffectFrameCount(deathEffect)
    if perPlayerCostumes.smwCostumeMap[deathEffect.costumeName]
    or (not perPlayerCostumes.basegameCostumeMap[deathEffect.costumeName] and deathEffect.image.height >= deathEffect.image.width*1.5)
    then
        return 2
    end

    return 1
end

local function drawDeathEffect(deathEffect)
    local width = deathEffect.image.width
    local height = deathEffect.image.height/deathEffect.frames

    Graphics.drawImageToSceneWP(deathEffect.image,deathEffect.x - width*0.5,deathEffect.y - height*0.5,0,deathEffect.animationFrame*height,width,height,-5.001)
end

local function updateDeathEffectFrame(deathEffect)
    if deathEffect.directional then
        deathEffect.animationFrame = math.floor(deathEffect.animationTimer/deathEffect.framespeed)%(deathEffect.frames*0.5)

        if deathEffect.direction == DIR_RIGHT then
            deathEffect.animationFrame = deathEffect.animationFrame + deathEffect.frames*0.5
        end
    else
        deathEffect.animationFrame = math.floor(deathEffect.animationTimer/deathEffect.framespeed)%deathEffect.frames
    end
end


function perPlayerCostumes.spawnDeathEffect(p,x,y)
    local data = p.data.perPlayerCostumes

    local deathEffect = {}

    deathEffect.character = p.character
    deathEffect.costumeName = p:getCostume()
    
    deathEffect.direction = p.direction
    
    if data ~= nil and data.deathEffectImage ~= nil then
        deathEffect.image = data.deathEffectImage
        deathEffect.frames = guessDeathEffectFrameCount(deathEffect)
    else
        deathEffect.image = Graphics.sprites.effect[deathEffectMap[deathEffect.character]].img
        deathEffect.frames = 1
    end

    if deathEffect.character == CHARACTER_LINK then
        deathEffect.speedX = -2*deathEffect.direction
        deathEffect.directional = true
        deathEffect.frames = 2
    else
        deathEffect.directional = false
        deathEffect.speedX = 0
    end

    deathEffect.speedY = -11
    deathEffect.gravity = 0.25

    deathEffect.x = x
    deathEffect.y = y

    deathEffect.framespeed = 8

    deathEffect.animationFrame = 0
    deathEffect.animationTimer = 0

    deathEffect.lifetime = 180

    updateDeathEffectFrame(deathEffect)

    table.insert(perPlayerCostumes.deathEffects,deathEffect)

    return deathEffect
end


function perPlayerCostumes.getOriginalHitboxSize(p,powerup)
    local data = p.data.perPlayerCostumes

    powerup = powerup or p.powerup

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

    local hitboxWidth = defaultSettings.hitboxWidth
    local hitboxHeight = defaultSettings.hitboxHeight
    local hitboxDuckHeight = defaultSettings.hitboxDuckHeight

    if data ~= nil and data.config[powerup] ~= nil then
        local config = data.config[powerup]

        hitboxWidth      = config.hitboxWidth      or hitboxWidth
        hitboxHeight     = config.hitboxHeight     or hitboxHeight
        hitboxDuckHeight = config.hitboxDuckHeight or hitboxDuckHeight
    end

    return hitboxWidth,hitboxHeight,hitboxDuckHeight
end


function perPlayerCostumes.onStart()
    for character = 1,5 do
        -- Reset costumes from the basegame costume system
        originalSetCostume(character,nil)

        -- Replace the character images with blank ones, while saving the originals
        originalPlayerImages[character] = {}

        for powerup = 1,7 do
            local imageInstance = Graphics.sprites[playerManager.getName(character)][powerup]
    
            originalPlayerImages[character][powerup] = imageInstance.img
            imageInstance.img = emptyImage
        end
    end
end


function perPlayerCostumes.checkCharacters()
    for _,p in ipairs(Player.get()) do
        local data = p.data.perPlayerCostumes

        if data ~= nil and data.initialisedCharacter ~= p.character then
            deactivateOldCostume(p.idx,data.initialisedCharacter)
            activateCostume(p.idx,p.character)

            data.initialisedCharacter = p.character
        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 drawSwapDebug()
    local y = 32

    for _,swapGroup in pairs(activeSwapMap) do
        for _,swap in pairs(swapGroup) do
            Text.print(string.format("%s-%d (%s) (%d backlogged)",swap.active.type,swap.active.id,swap.active.costumeName,#swap.backlog),32,y)
            y = y + 18
        end
    end
end

function perPlayerCostumes.onCameraDraw(camIdx)
    local cam = Camera(camIdx)

    for _,p in ipairs(Player.getIntersecting(cam.x,cam.y,cam.x + cam.width,cam.y + cam.height)) do
        if not p.isFairy
        and not perPlayerCostumes.getIsInvisible(p,camIdx)
        and (not perPlayerCostumes.invisiblePlayerStates[p.forcedState] or perPlayerCostumes.getIsVisibleDespiteForcedState(p))
        then
            -- For some reason, Link is still in frame 15 when entering a pipe...
            local frame = p:getFrame()
            if p.character == CHARACTER_LINK and frame == 15 then
                frame = 1
            end

            p:render{
                color = perPlayerCostumes.getTintColor(p,camIdx),
                priority = getPlayerPriority(p) - 0.0001,
                x = math.floor(p.x + 0.5),
                y = math.floor(p.y + 0.5),
                --drawmounts = perPlayerCostumes.getIsVisibleDespiteForcedState(p),
                drawmounts = true,
                frame = frame,
            }
        end
    end

    --drawSwapDebug()
end

function perPlayerCostumes.onDraw()
    for _,deathEffect in ipairs(perPlayerCostumes.deathEffects) do
        drawDeathEffect(deathEffect)
    end
end

function perPlayerCostumes.onTick()
    for _,p in ipairs(Player.get()) do
        handleHeldNPC(p)
    end

    -- Update death effects
    local i = 1

    while (perPlayerCostumes.deathEffects[i] ~= nil) do
        local deathEffect = perPlayerCostumes.deathEffects[i]

        deathEffect.lifetime = deathEffect.lifetime - 1

        if deathEffect.lifetime > 0 then
            deathEffect.speedY = deathEffect.speedY + deathEffect.gravity

            deathEffect.x = deathEffect.x + deathEffect.speedX
            deathEffect.y = deathEffect.y + deathEffect.speedY

            deathEffect.animationTimer = deathEffect.animationTimer + 1

            updateDeathEffectFrame(deathEffect)

            i = i + 1
        else
            table.remove(perPlayerCostumes.deathEffects,i)
        end
    end
end

function perPlayerCostumes.onTickEnd()
    for _,p in ipairs(Player.get()) do
        handleHeldNPC(p)
    end
end


-- Overwritable function to get the colour tint of a player
function perPlayerCostumes.getTintColor(p,camIdx)
    return nil
end

-- Overwritable function to see if a player should be invisible
function perPlayerCostumes.getIsInvisible(p,camIdx)
    return false
end

-- Overwritable function to see if a player should be visible despite being in a forced state
function perPlayerCostumes.getIsVisibleDespiteForcedState(p)
    return false
end


-- Overwrite the original Player:render
do
    function Player:render(args)
        local data = self.data.perPlayerCostumes

        local character = args.character or self.character
        local powerup = args.powerup or self.powerup

        if character > 5 then
            originalPlayerRender(self,args)
            return
        end

        local frameX,frameY = Player.convertFrame(args.frame or self:getFrame(),args.direction or self.direction)

        if frameX < 0 or frameX > 9 then
            return
        end


        local defaultSettings = PlayerSettings.get(playerManager.getBaseID(character),powerup)
        local originalFrameOffsetX,originalFrameOffsetY

        -- Set up the appropriate texture for the costume (or the default player graphics)
        if args.texture == nil then
            if data ~= nil and character == data.initialisedCharacter and data.playerImages[powerup] ~= nil then
                args.texture = data.playerImages[powerup]
            else
                args.texture = originalPlayerImages[character][powerup]
            end
        end

        -- Set up sprite offsets
        if data ~= nil and character == data.initialisedCharacter and data.config[powerup] ~= nil then
            local config = data.config[powerup]
            local frameOffset = config.offsets[frameX][frameY]

            -- Some costumes will have a different hitbox size (e.g., Demo), so that needs to be accounted for
            local widthDifference = (config.hitboxWidth or defaultSettings.hitboxWidth) - defaultSettings.hitboxWidth
            local heightDifference = (config.hitboxHeight or defaultSettings.hitboxHeight) - defaultSettings.hitboxHeight

            if self.isDucking then
                heightDifference = (config.hitboxDuckHeight or defaultSettings.hitboxDuckHeight) - defaultSettings.hitboxDuckHeight
            end

            originalFrameOffsetX = defaultSettings:getSpriteOffsetX(frameX,frameY)
            originalFrameOffsetY = defaultSettings:getSpriteOffsetY(frameX,frameY)

            defaultSettings:setSpriteOffsetX(frameX,frameY,-frameOffset.x - widthDifference*0.5)
            defaultSettings:setSpriteOffsetY(frameX,frameY,-frameOffset.y - heightDifference)
        end

        if perPlayerCostumes.getIsVisibleDespiteForcedState(self) then
            args.ignorestate = true
        end

        originalPlayerRender(self,args)

        -- Restore the original offset
        if originalFrameOffsetX ~= nil then
            defaultSettings:setSpriteOffsetX(frameX,frameY,originalFrameOffsetX)
            defaultSettings:setSpriteOffsetY(frameX,frameY,originalFrameOffsetY)
        end
    end
end


-- Overwrite :getCostume and :setCostume
do
    function Player.getCostume(p)
        if type(p) == "Player" then
            return perPlayerCostumes.getCostume(p.idx,p.character)
        end

        return nil
    end

    function Player.setCostume(p,costumeName)
        if type(p) == "Player" then
            perPlayerCostumes.setCostume(p.idx,p.character,costumeName)
        end
    end
end


function perPlayerCostumes.onInitAPI()
    registerEvent(perPlayerCostumes,"onStart","onStart",false)
    registerEvent(perPlayerCostumes,"onTick","onTick",false)
    registerEvent(perPlayerCostumes,"onTickEnd","onTickEnd",false)
    registerEvent(perPlayerCostumes,"onDraw","checkCharacters")
    registerEvent(perPlayerCostumes,"onDraw")
    registerEvent(perPlayerCostumes,"onCameraDraw")
end


return perPlayerCostumes