rawset(_G, "fd_loaded", true)

freeslot("sfx_chsdie", "sfx_destry", "sfx_fartxd")

sfxinfo[sfx_chsdie] = {false, 64, SF_X8AWAYSOUND}
sfxinfo[sfx_destry] = {false, 64, SF_X8AWAYSOUND}
sfxinfo[sfx_fartxd] = {false, 64, SF_X8AWAYSOUND}

local CV_RESPAWN_DELAY

local function getRespawnDelay()
    if not CV_RESPAWN_DELAY then
        CV_RESPAWN_DELAY = CV_FindVar("respawndelay")
    end
    return CV_RESPAWN_DELAY.value
end

local CHASE_HORIZ_THROW = 32
local CHASE_ZTHROW = 64 << FRACBITS
local SONIC_BOSS_THROW = 48 << FRACBITS
local TINIES = 20
local TINY_SCALE = FRACUNIT >> 2
local TINY_HORIZ_THROW = 16
local TINY_ZTHROW = 32 << FRACBITS
local TINY_EXTRA_GRAV = FRACUNIT

local COMMAND_PREFIX = "fd_"
local LIST_PAGE_SIZE = 10

local pCams = {}

local killOnHit = false
local killOnHitMaps = {
    5,    --Daytona Speedway Zone
    7,    --Sonic Speedway Zone
    11,   --Sunbeam Paradise Zone
    15,   --Pleasure Castle
    42,   --CK Chao Circuit 1
    45,   --CK Regal Raceway
    46,   --SD2 Balloon Panic
    51,   --SMK Donut Plains 1
    52,   --SMK Mario Circuit 2
    56,   --Lake Margorite Zone
    "B0", --Municipal Meadow Zone
    "B2", --Tinkerer's Arena Zone
    "B3", --Clucky Farms Zone
    "B4", --Techno Hill Zone
    "B6", --Colosseum
    "B7", --Dried Battledune Zone
    "B8", --Eerie Grove Zone
    "B9", --Rusty Rig Zone
    "BB", --Bad Taste Aquarium
    "BE", --Fakery Way
    "BF", --Bumper Carts
    "BI", --Tails' Lab
    "BK", --Trigger Happy Havoc
    "BL", --Mementos
    "BM", --CD Special Stage 1
    "BN", --SMK Battle Course 1
    "BO", --SMK Battle Course 2
    "BP", --SMK Battle Course 3
    "BQ", --SMK Battle Course 4
    "BR", --MK64 Block Fort
    "BS"  --MK64 Double Deck
}
--convert extended maps to correct number (convenience)
for i = 1, #killOnHitMaps do
    if not tonumber(killOnHitMaps[i])
        killOnHitMaps[i] = 36 * (killOnHitMaps[i]:byte(1) - 65)
            + (tonumber(killOnHitMaps[i]:sub(2)) or killOnHitMaps[i]:byte(2) - 55) + 100
    end
end

local cv_enable = CV_RegisterVar({
    COMMAND_PREFIX .. "enable",
    "On",
    CV_NETVAR,
    CV_OnOff
})

--this is all is needed for cv_killOnHit
local function mapKillsOnHit(mapnum)
    --yo, look, you can decide if your map kills on hit!
    if mapheaderinfo[mapnum] and mapheaderinfo[mapnum].fd_killonhit then
        return tonumber(mapheaderinfo[mapnum].fd_killonhit)
            or mapheaderinfo[mapnum].fd_killonhit:lower() == "true"
            or mapheaderinfo[mapnum].fd_killonhit:lower() == "yes"
    end
    for _, killOnHitMap in ipairs(killOnHitMaps) do
        if mapnum == killOnHitMap then
            return true
        end
    end
    return false
end

local cv_killOnHit
local function setOnHitDeath(mapnum, onlyNew)
    if #sectors then --if no sectors, wait because we're loading in
        local prevKoh = killOnHit
        if cv_killOnHit.value == 2 then
            killOnHit = true
        elseif not cv_killOnHit.value then
            killOnHit = false
        elseif mapKillsOnHit(mapnum) then
            killOnHit = true
        elseif #sectors then
            --check for death sectors
            killOnHit = true
            for sector in sectors.iterate do
                local secSpec1 = GetSecSpecial(sector.special, 1)
                if secSpec1 == 6 or secSpec1 == 7 or secSpec1 == 8 then
                    killOnHit = false
                    break
                end
            end
        end
        if killOnHit ~= prevKoh then
            if not onlyNew then
                chatprint(string.format(
                    "ALERT: Kill on hit is now %s!",
                    killOnHit and "\131ENABLED" or "\133DISABLED"
                ))
                S_StartSound(nil, killOnHit and sfx_itrole or sfx_itrolf)
                for player in players.iterate do
                    player.alerted = true
                end
            end
        end
    end
end

cv_killOnHit = {defValue = "Per map", value = 1}
cv_killOnHit = CV_RegisterVar({
    COMMAND_PREFIX .. "hitkill",
    cv_killOnHit.defValue,
    CV_NETVAR|CV_CALL,
    {Off = 0, ["Per map"] = 1, On = 2},
    function()
        setOnHitDeath(gamemap, false)
    end
})

local cv_killOnFinish = CV_RegisterVar({
    COMMAND_PREFIX .. "finishkill",
    "On",
    CV_NETVAR,
    CV_OnOff
})

local cv_killOnAcrobFail = CV_RegisterVar({
    COMMAND_PREFIX .. "acrofailkill",
    "Spinout",
    CV_NETVAR,
    {Off = 0, Spinout = 1, Miss = 2}
})

--remove death thinkers so we don't have anyone acting strangely when the map loads
addHook(
    "MapChange",
    function()
        for player in players.iterate do
            player.deathThinker = nil
            player.deathEnd = nil
        end
    end
)

--check on map load
addHook(
    "MapLoad",
    function(mapnum)
        setOnHitDeath(mapnum, false)
    end
)

--give ints, returns fixed
local function randomWide(a, b)
    if a < b then
        local temp = a
        a = b
        b = temp
    end
    return P_RandomRange(a, b - 1) << FRACBITS + P_RandomFixed()
end

local function randomCompleteFixed()
    return P_RandomFixed() << FRACBITS + P_RandomFixed()
end

local function enableCustomCamera(player)
    --only enable if something else isn't using it
    if not player.awayviewtics then
        --spawn object if needed
        if not (player.customcam and player.customcam.valid) then
            player.customcam = P_SpawnMobj(
                player.mo.x,
                player.mo.y,
                player.mo.z,
                MT_BUSH
            )
            player.customcam.sprite = SPR_NULL
        end
        
        player.awayviewmobj = player.customcam
        player.awayviewtics = INT32_MAX
        return true
    else
        return player.awayviewmobj == player.customcam
    end
end

local function disableCustomCamera(player)
    if player.awayviewmobj == player.customcam then
        player.awayviewtics = 0
        return true
    else
        return false
    end
end

local function lookAtPlayerFromPoint(player, x, y, z)
    if enableCustomCamera(player) then
        --needs to use mom to be more correct on interp builds
        player.customcam.momx = x - player.customcam.x
        player.customcam.momy = y - player.customcam.y
        player.customcam.momz = z - player.customcam.z
        P_TeleportMove(player.customcam, x, y, z)
        player.customcam.angle = R_PointToAngle2(
            player.customcam.x,
            player.customcam.y,
            player.mo.x,
            player.mo.y
        )
        player.awayviewaiming = R_PointToAngle2(
            0,
            player.customcam.z,
            R_PointToDist2(
                player.customcam.x,
                player.customcam.y,
                player.mo.x,
                player.mo.y
            ),
            player.mo.z
        )
    end
end

local function getChaseDiesCamZ(player)
    return player.mo.z - (player.mo.z - player.deathCamPointZ) / 4
end

--DEATHS
--CHASE DIES
local function chaseDiesStart(player)
    --throw them into the air
    P_InstaThrust(player.mo, randomCompleteFixed(), randomWide(0, CHASE_HORIZ_THROW))
    player.mo.momz = CHASE_ZTHROW * P_MobjFlip(player.mo)
    --play wario dies
    S_StartSound(player.mo, sfx_chsdie)
    --explode later
    player.chasedies = TICRATE / 2
    
    --store camera position
    if not player.exiting and pCams[#player] then
        player.deathCamPointX = pCams[#player].x
        player.deathCamPointY = pCams[#player].y
        player.deathCamPointZ = pCams[#player].z
        lookAtPlayerFromPoint(
            player,
            player.deathCamPointX,
            player.deathCamPointY,
            getChaseDiesCamZ(player)
        )
    end
end

local function chaseDiesThink(player)
    if player.chasedies then
        player.chasedies = $ - 1
        if not player.chasedies then
            --hide
            player.mo.flags2 = $ | MF2_DONTDRAW
            --imitating thinker of MT_FZEROBOOM
            local boom = P_SpawnMobj(player.mo.x, player.mo.y, player.mo.z, MT_THOK)
            boom.state = S_FZEROBOOM1
            boom.scale = player.mo.scale * 2
        end
        if not player.exiting then
            if player.deathCamPointX == nil and pCams[#player] then
                player.deathCamPointX = pCams[#player].x
                player.deathCamPointY = pCams[#player].y
                player.deathCamPointZ = pCams[#player].z
            end
            
            if player.deathCamPointX then
                lookAtPlayerFromPoint(
                    player,
                    player.deathCamPointX,
                    player.deathCamPointY,
                    getChaseDiesCamZ(player)
                )
            end
        end
    elseif player.exiting then
        --i think exiting is trying to make the player visible every frame
        player.mo.flags2 = $ | MF2_DONTDRAW
    else
        if player.customcam then
            player.customcam.momx, player.customcam.momy, player.customcam.momz = 0, 0, 0
        end
    end
end

local function chaseDiesEnd(player)
    player.mo.flags2 = $ & ~MF2_DONTDRAW
    player.chaseDies = 0
    player.deathCamPointX = nil
    player.deathCamPointY = nil
    player.deathCamPointZ = nil
    disableCustomCamera(player)
end

--EXPLODE
local function explodeStart(player)
    --boom
    K_SpawnMineExplosion(player.mo, player.skincolor)
    --sound
    S_StartSoundAtVolume(player.mo, sfx_khurt1 + P_RandomKey(2), 191)
    S_StartSound(player.mo, sfx_destry)
    --hide
    player.mo.flags2 = $ | MF2_DONTDRAW
    
    --store
    if not player.exiting and pCams[#player] then
        player.deathCamPointX = pCams[#player].x
        player.deathCamPointY = pCams[#player].y
        player.deathCamPointZ = pCams[#player].z
        player.deathCamAngle = pCams[#player].angle
        player.deathCamAim = pCams[#player].aiming
        --continue looking at the same spot
        if enableCustomCamera(player) then
            P_TeleportMove(
                player.customcam,
                player.deathCamPointX,
                player.deathCamPointY,
                player.deathCamPointZ
            )
            player.customcam.angle = player.deathCamAngle
            player.awayviewaiming = player.deathCamAim
        end
    end
end

local function explodeThink(player)
    if not player.exiting then
        if player.deathCamPointX == nil and pCams[#player] then
            player.deathCamPointX = pCams[#player].x
            player.deathCamPointY = pCams[#player].y
            player.deathCamPointZ = pCams[#player].z
            player.deathCamAngle = pCams[#player].angle
            player.deathCamAim = pCams[#player].aiming
        end
        if player.deathCamPointX and enableCustomCamera(player) then
            P_TeleportMove(
                player.customcam,
                player.deathCamPointX,
                player.deathCamPointY,
                player.deathCamPointZ
            )
            player.customcam.angle = player.deathCamAngle
            player.awayviewaiming = player.deathCamAim
        end
    else
        player.mo.flags2 = $ | MF2_DONTDRAW
    end
end

local function explodeEnd(player)
    player.mo.flags2 = $ & ~MF2_DONTDRAW
    player.deathCamPointX = nil
    player.deathCamPointY = nil
    player.deathCamPointZ = nil
    player.deathCamAngle = nil
    player.deathCamAim = nil
    disableCustomCamera(player)
end

--SONIC BOSS EXPLOSIONS
--predeclaring
local sonicBossExplThink

local function sonicBossExplStart(player)
    player.mo.momz = SONIC_BOSS_THROW
    sonicBossExplThink(player)
end

function sonicBossExplThink(player)
    local playerHeightInt = player.mo.height >> FRACBITS
    local playerRadiusInt = player.mo.radius >> FRACBITS
    
    player.mo.colorized = true
    player.mo.color = SKINCOLOR_MAROON
    --violently explode
    P_SpawnMobj(
        player.mo.x + P_RandomRange(-playerRadiusInt, playerRadiusInt) << FRACBITS,
        player.mo.y + P_RandomRange(-playerRadiusInt, playerRadiusInt) << FRACBITS,
        player.mo.z + P_RandomRange(0, playerHeightInt) << FRACBITS,
        MT_SONIC3KBOSSEXPLODE
    )
    if not (leveltime % 3) then
        S_StartSound(player.mo, sfx_cybdth)
    end
end

local function sonicBossExplEnd(player)
    player.mo.colorized = false
    player.mo.color = player.skincolor
end

--FART POOF
local function fartPoofStart(player)
    --hide
    player.mo.flags2 = $ | MF2_DONTDRAW
    --sound
    S_StartSound(player.mo, sfx_fartxd)
    --poof
    local poof = P_SpawnMobj(player.mo.x, player.mo.y, player.mo.z, MT_BOOMEXPLODE)
    poof.color = player.skincolor
    poof.scale = 3 << FRACBITS
    
    --freeze camera
    if not player.exiting and pCams[#player] then
        player.deathCamPointX = pCams[#player].x
        player.deathCamPointY = pCams[#player].y
        player.deathCamPointZ = pCams[#player].z
        player.deathCamAngle = pCams[#player].angle
        player.deathCamAim = pCams[#player].aiming
        --continue looking at the same spot
        if enableCustomCamera(player) then
            P_TeleportMove(
                player.customcam,
                player.deathCamPointX,
                player.deathCamPointY,
                player.deathCamPointZ
            )
            player.customcam.angle = player.deathCamAngle
            player.awayviewaiming = player.deathCamAim
        end
    end
end

local function fartPoofThink(player)
    if not player.exiting then
        if player.deathCamPointX == nil and pCams[#player] then
            player.deathCamPointX = pCams[#player].x
            player.deathCamPointY = pCams[#player].y
            player.deathCamPointZ = pCams[#player].z
            player.deathCamAngle = pCams[#player].angle
            player.deathCamAim = pCams[#player].aiming
        end
        if player.deathCamPointX and enableCustomCamera(player) then
            P_TeleportMove(
                player.customcam,
                player.deathCamPointX,
                player.deathCamPointY,
                player.deathCamPointZ
            )
            player.customcam.angle = player.deathCamAngle
            player.awayviewaiming = player.deathCamAim
        end
    else
        player.mo.flags2 = $ | MF2_DONTDRAW
    end
end

local function fartPoofEnd(player)
    player.mo.flags2 = $ & ~MF2_DONTDRAW
    player.deathCamPointX = nil
    player.deathCamPointY = nil
    player.deathCamPointZ = nil
    player.deathCamAngle = nil
    player.deathCamAim = nil
    disableCustomCamera(player)
end

--TINY-PLOSION
local function tinyExplodeStart(player)
    --hide
    player.mo.flags2 = $ | MF2_DONTDRAW
    --sound
    S_StartSound(player.mo, sfx_bowlh)
    
    player.tinySelves = {}
    for i = 1, TINIES do
        local mobj = P_SpawnMobj(player.mo.x, player.mo.y, player.mo.z, MT_THOK)
        mobj.angle = player.mo.angle
        mobj.state = S_BUSH
        mobj.sprite = SPR_PLAY
        mobj.frame = player.mo.frame & ~FF_FRAMEMASK | Q
        mobj.flags = $ & ~MF_NOGRAVITY
        mobj.skin = player.mo.skin
        mobj.color = player.mo.color
        mobj.colorized = player.mo.colorized
        mobj.destscale = FixedMul(player.mo.destscale, TINY_SCALE)
        mobj.scale = FixedMul(player.mo.scale, TINY_SCALE)
        --thrust
        P_InstaThrust(mobj, randomCompleteFixed(), randomWide(0, TINY_HORIZ_THROW))
        mobj.momz = TINY_ZTHROW
        --random range between about 5 and 20 degrees, either positive or negative
        mobj.SPEEN = randomCompleteFixed() % ANG15
        mobj.SPEEN = $ + ($ < 0 and -ANG10 / 2 or ANG10 / 2)
        table.insert(player.tinySelves, mobj)
    end
end

local function tinyExplodeThink(player)
    for _, mobj in ipairs(player.tinySelves) do
        --random range between about 5 and 20 degrees
        mobj.angle = $ + mobj.SPEEN
        --increase gravity
        mobj.momz = $ - TINY_EXTRA_GRAV
    end
    if player.exiting then
        player.mo.flags = $ | MF2_DONTDRAW
    end
end

local function tinyExplodeEnd(player)
    for _, mobj in ipairs(player.tinySelves) do
        P_RemoveMobj(mobj)
    end
    player.tinySelves = nil
    player.mo.flags2 = $ & ~MF2_DONTDRAW
end

--END DEATHS

--start function, per-frame function, end function, name, search-assisting key phrases
local funnyDeaths = {
    [0] = {nil, nil, nil, "Classic Sonic Death (No Funny Death)", "vanilla, none"},
    {chaseDiesStart, chaseDiesThink, chaseDiesEnd, "Chase Absolutely Dies", "scream"},
    {explodeStart, explodeThink, explodeEnd, "Looks Like You Hit a Mine", "explode, explosion"},
    {sonicBossExplStart, sonicBossExplThink, sonicBossExplEnd, "Sonic Boss Defeat", "death explosion dr dr. eggman robotnik"},
    {fartPoofStart, fartPoofThink, fartPoofEnd, "fart_with_extra_reverb.mp3", "fart with extra reverb mp3 reverb.mp3"},
    {tinyExplodeStart, tinyExplodeThink, tinyExplodeEnd, "Tiny-splosion", "tiny explosion explode small mini"}
}

--Note that it does not kill the player on its own.
--local function applyFunnyDeath(player)
rawset(_G, "applyFunnyDeath", function(player)
    local funnyDeath
    if player.preferredDeath ~= nil then
        funnyDeath = funnyDeaths[player.preferredDeath]
    else
        funnyDeath = funnyDeaths[abs(randomCompleteFixed() % #funnyDeaths) + 1]
    end
    player.deathThinker = funnyDeath[2]
    player.deathEnd = funnyDeath[3]
    player.funnyDeathApplied = true
    if funnyDeath[1] then
        funnyDeath[1](player)
    end
end)

local function exitDeathThinker(player)
    if player.exitDeathThinkingTime > getRespawnDelay() * 35
        player.mo.flags = $ | MF_NOCLIPTHING
        player.mo.flags2 = $ | MF2_DONTDRAW
    elseif player.deathThinker then
        player.deathThinker(player)
    end
    player.exitDeathThinkingTime = $ + 1
end

local function onHitDeath(player)
    if killOnHit then
        if gametype == GT_MATCH and player.kartstuff[k_bumper] then
            --both the hit and death take a bumper each. we need to offset this
            player.kartstuff[k_bumper] = $ + 1
        end
        P_DamageMobj(player.mo, nil, nil, 10000)
    end
end

addHook("PlayerSpin", onHitDeath)

addHook("PlayerExplode", onHitDeath)

addHook("PlayerSquish", onHitDeath)

addHook(
    "ThinkFrame",
    function()
        if cv_enable.value then
            for player in players.iterate do
                if player.valid and player.mo and player.mo.valid then
                    if gametype == GT_MATCH then
                        if player.kartstuff[k_bumper] > 0 then
                            player.postNoBumps = false
                        elseif not player.postNoBumps and player.playerstate ~= PST_DEAD then
                            --don't let the forced death prevent players from dropping items
                            K_DropItems(player)
                            P_DamageMobj(player.mo, nil, nil, 10000)
                            player.postNoBumps = true
                        end
                    end
                    
                    if player.playerstate ~= PST_DEAD then
                        if player.exiting then
                            if player.funnyDeathApplied then
                                exitDeathThinker(player)
                            elseif cv_killOnFinish.value then
                                applyFunnyDeath(player)
                                --[[
                                local instantDeathSec = P_PlayerTouchingSectorSpecial(player, 1, 8)
                                if instantDeathSec then
                                    applyFunnyDeath(player)
                                else
                                    local floorDeathSec = P_PlayerTouchingSectorSpecial(player, 1, 6) 
                                        or P_PlayerTouchingSectorSpecial(player, 1, 7)
                                    if floorDeathSec then
                                        local specFof = P_ThingOnSpecial3DFloor(player.mo)
                                        if specFof and (
                                                GetSecSpecial(specFof.special, 1) == 6
                                                or GetSecSpecial(specFof.special, 1) == 7
                                            ) or P_IsObjectOnGround(player.mo)
                                        then
                                            applyFunnyDeath(player)
                                        end
                                    end
                                end
                                ]]
                            end
                        elseif
                            --acrobatics support
                            cv_killOnAcrobFail.value == 1
                                and player.trickstun
                            or cv_killOnAcrobFail.value == 2
                                and player.trickgrade == 0
                                and player.sincelasttrick
                        then
                            --you failed the trick; I'm taking your life
                            player.trickstun = false
                            P_DamageMobj(player.mo, nil, nil, 10000)
                            applyFunnyDeath(player)
                        else
                            player.deathThinker = nil
                            if player.deathEnd then
                                player.deathEnd(player)
                                player.deathEnd = nil
                            end
                            player.funnyDeathApplied = false
                            player.exitDeathThinkingTime = 0
                        end
                        
                    --run death thinker
                    elseif player.funnyDeathApplied then
                        if player.deathThinker then
                            --no need to try
                            player.deathThinker(player)
                        end
                    
                    --check for if funny effect should be applied
                    elseif not (
                        player.pflags & PF_TIMEOVER --NO CONTEST
                    ) then
                        applyFunnyDeath(player)
                    end
                    
                    if not player.alerted then
                        chatprintf(player, string.format(
                            "ALERT: Kill on hit is currently %s!",
                            killOnHit and "\131ENABLED" or "\133DISABLED"
                        ))
                        S_StartSound(nil, killOnHit and sfx_itrole or sfx_itrolf, player)
                        player.alerted = true
                    end
                end
            end
        end
    end
)

--unfortunately, sectors aren't loaded when addon is loaded by server, and no mapload invoke happens
addHook(
    "NetVars",
    function(network)
        --killOnHit = network(killOnHit)
        setOnHitDeath(gamemap, true)
    end
)

local commandTotal = 0
--for commands: (name: usage, short description, long description, handler)
--for cvars: (name: usage, short description, long description, cvar)
local commands = {}

local function registerCommand(name, usage, shortDesc, longDesc, handler, flags)
    local commandName = COMMAND_PREFIX .. name
    commands[commandName] = {usage, shortDesc, longDesc, handler}
    commandTotal = $ + 1
    COM_AddCommand(
        commandName,
        function(player, ...)
            handler(player, CONS_Printf, ...)
        end,
        flags or 0
    )
end

--nothing like a robust help system
registerCommand(
    "help",
    "[page|command]",
    "The command you're using!",
    "Displays commands or more information about a command.\n"
        .. "\tpage: The page of commands to display\n"
        .. "\tcommand: The command about which to displaye more information",
    function(player, printHandle, var1)
        var1 = $ == nil and "1" or $
        local page = tonumber(var1)
        if page == nil then
            local command = var1:lower():sub(1, 3) == COMMAND_PREFIX and var1
                or COMMAND_PREFIX .. var1
            if commands[command] then
                printHandle(
                    player,
                    string.format(
                        "Funny Deaths Help (%s)\n%s%s\n%s",
                        command,
                        command,
                        commands[command][1] and " " .. commands[command][1] or "",
                        commands[command][3] or commands[command][2]
                    )
                )
            else
                printHandle(player, "\130Command not recognized!")
            end
        else
            local maxPage = (commandTotal - 1) / LIST_PAGE_SIZE + 1
            if page > 0 and page <= maxPage then
                printHandle(player, "Funny Deaths Help (Page " .. page .. "/" .. maxPage .. ")")
                local i = 1
                local startpoint = page * LIST_PAGE_SIZE - LIST_PAGE_SIZE + 1
                local breakpoint = page * LIST_PAGE_SIZE
                for command, info in pairs(commands) do
                    if i >= startpoint then
                        printHandle(
                            player,
                            string.format(
                                "%s%s: %s",
                                command,
                                info[1] and " " .. info[1] or "",
                                info[2]
                            )
                        )
                    end
                    i = $ + 1
                    if i > breakpoint then
                        break
                    end
                end
            else
                printHandle(player, "\x82Page number out of range (1-" .. maxPage .. ")!")
            end
        end
    end
)

--lists all deaths
registerCommand(
    "listdeaths",
    "[page]",
    "Lists all possible deaths.",
    "Lists all possible deaths, along with their numerical IDs. Useful for \x84setdeath\x80.\n"
        .. "\tpage: The page of deaths to display",
    function(player, printHandle, page)
        page = $ == nil and 1 or tonumber($)
        if page then
            local maxPage = (#funnyDeaths) / LIST_PAGE_SIZE + 1
            if page > 0 and page <= maxPage then
                printHandle(player, "Funny Deaths List (Page " .. page .. "/" .. maxPage .. ")")
                local startId = page * LIST_PAGE_SIZE - LIST_PAGE_SIZE
                local endId = min(page * LIST_PAGE_SIZE - 1, #funnyDeaths)
                for i = startId, endId do
                    printHandle(
                        player,
                        string.format(
                            "%" .. max(#tostring(startId), #tostring(endId)) .. "d: %s",
                            i,
                            funnyDeaths[i][4] --readable name
                        )
                    )
                end
            end
        else
            printHandle(player, "\x82Page must be a number!")
        end
    end
)

--sets specific death
registerCommand(
    "setdeath",
    "<id>",
    "Sets a specific death to happen when you die.",
    "Sets a specific death to happen when you die.\n"
        .. "\tid: The numerical ID of the death",
    function(player, printHandle, id)
        if id then
            local numVal = tonumber(id)
            if numVal == nil then
                --it's real search hours. i'll implement this later
                printHandle(
                    player,
                    "\x82Name searching is not implemented yet. Use \x84listdeaths \130for now!"
                )
            elseif numVal >= 0 and numVal <= #funnyDeaths then
                player.preferredDeath = numVal
                printHandle(
                    player,
                    "\x83Your death will now always be \x88"
                        .. funnyDeaths[numVal][4]
                        .. "\x83!"
                )
            else
                printHandle(
                    player,
                    "\x82Given number is outside of allowed range (1-" .. #funnyDeaths .. ")!"
                )
            end
        else
            printHandle(player, "\x82Missing argument (id)!")
        end
    end
)

--resets to random death
registerCommand(
    "randomdeath",
    nil,
    "Sets the death when you die to be random.",
    nil,
    function(player, printHandle)
        player.preferredDeath = nil
        printHandle(player, "\x83Your death will now be random!")
    end
)

local function registerCvar(name, usage, shortDesc, longDesc, cvar)
    commands[name] = {usage, shortDesc, longDesc, cvar}
    commandTotal = $ + 1
end

registerCvar(
    cv_enable.name,
    "<Off|On>",
    "If Funny Deaths should be doing anything.",
    nil,
    cv_enable
)

registerCvar(
    cv_killOnHit.name,
    "<Off|\"Per map\"|On>",
    "The behavior for killing players when hit by hazards.",
    "The behavior for killing players when hit by hazards.\n"
        .. "\tOff: Never kill when hit by a hazard\n"
        .. "\tPer map (default): Only kill on maps where it's rare or impossible to die\n"
        .. "\tOn: Always kill when hit by a hazard. For those who like chaos",
    cv_killOnHit
)

registerCvar(
    cv_killOnFinish.name,
    "<Off|On>",
    "If players should \"die\" when finishing a match.",
    nil,
    cv_killOnFinish
)

registerCvar(
    cv_killOnAcrobFail.name,
    "<Off|Spinout|Miss>",
    "Acrobatics support: Behavior for killing players when failing a trick.",
    "Acrobatics support: Behavior for killing players when failing a trick.\n"
        .. "\tOff: Never kill when failing a trick\n"
        .. "\tSpinout: Only kill when being spun out by a failed trick\n"
        .. "\tMiss: Always kill when failing a trick",
    cv_killOnAcrobFail
)

hud.add(
    function(v, stplyr, cam)
        pCams[#stplyr] = cam
    end
)

--Add your own; I don't care! Just make sure you don't do it wrong!
rawset(_G, COMMAND_PREFIX .. "addFunnyDeath", function(start, think, ending, name, ...)
    local warn = "\x82WARNING: \x128function"
        .. COMMAND_PREFIX
        .. "addFunnyDeath only accepts %s for %s, got %s."
    if start ~= nil and type(start) ~= "function" then
        print(string.format(warn, "nil or function", "start", type(start)))
    elseif think ~= nil and type(think) ~= "function" then
        print(string.format(warn, "nil or function", "think", type(think)))
    elseif ending ~= nil and type(ending) ~= "function" then
        print(string.format(warn, "nil or function", "ending", type(ending)))
    elseif name == nil or type(name) ~= "string" then
        print(string.format(warn, "string", "name", type(name)))
    else
        local keywordsPassed = true
        local keywords = table.pack(...)
        for i = 1, keywords.n do
            if keywords[i] == nil or type(keywords[i]) ~= "string" then
                print(string.format(warn, "strings", "keywords", type(keywords[i])))
                keywordsPassed = false
            end
        end
        if keywordsPassed then
            table.insert(funnyDeaths, {start, think, ending, name, ...})
        end
    end
end)