-- Increased only on gameplay-changing updates
local version = 5

if GRAPLE_VERSION and GRAPLE_VERSION < version then
    error("older version of graple lua is loaded, this may lead to problems")
elseif GRAPLE_VERSION then
    print("\131NOTICE:\128 graple lua is already loaded")
    return
end

rawset(_G, "GRAPLE_VERSION", version)

-- Max distance for grapling
local DEFAULT_GRAPLE_DISTANCE = FRACUNIT*2000

-- For Hooke's law
local DEFAULT_GRAPLE_K = FRACUNIT/16

-- Check player angle by default
local DEFAULT_CHECK_ANGLE = true

-- Default angle for sight checking
local DEFAULT_CHECK_ANGLE_VALUE = 90

-- While i made few vars customizable, those bellow are still hardcoded. Not
-- sure if i need to make them customizable too. If you are reading this script
-- and want to customize those, just ask me on the mb.srb2.org and i'll make
-- update, so all maps with graple script will still be compatible (as long as
-- their authors update their copy of script :P)

-- If we are grapling and holding accel button, accelerate in look direction for
-- more control
local GRAPLE_ACCEL = FRACUNIT*2/3

local GRAPLE_INDICATOR_DIST = FRACUNIT*60

freeslot("MT_DEFAULT_GRAPLE_ROPE")

mobjinfo[MT_DEFAULT_GRAPLE_ROPE] = {
    spawnstate = S_RING, -- Ye its just a ring rn
    spawnhealth = 1000, -- No idea what is dis lol
    radius = FRACUNIT, -- Not sure if i need it
    height = FRACUNIT, -- Same
    dispoffset = 0, -- Same
    flags = MF_NOGRAVITY|MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT,
}

freeslot("MT_DEFAULT_GRAPLE_INDICATOR")

mobjinfo[MT_DEFAULT_GRAPLE_INDICATOR] = {
    spawnstate = S_RING, -- Ye its just a ring rn
    spawnhealth = 1000, -- No idea what is dis lol
    radius = FRACUNIT, -- Not sure if i need it
    height = FRACUNIT, -- Same
    dispoffset = 0, -- Same
    flags = MF_NOGRAVITY|MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT,
}

-- XR_Defs - eXtended R_Defs

-- Distance in 3d. Not sure why srb2 doesn't have something similar
local function XR_Point3ToDist2(x1, y1, z1, x2, y2, z2)
    return FixedHypot(FixedHypot(x1-x2, y1-y2), z1 - z2)
end

-- Normalize 3d vector
local function XR_Vec3Normalize(x, y, z)
    local dist = XR_Point3ToDist2(x, y, z, 0, 0, 0)

    return FixedDiv(x, dist), FixedDiv(y, dist), FixedDiv(z, dist)
end

-- Multiply vector by num
local function XR_Vec3Scale(x, y, z, num)
    return FixedMul(x, num), FixedMul(y, num), FixedMul(z, num)
end

-- Convinient function to calculate distance between 2 mobjs
local function XR_DistBetweenMobj(mo1, mo2)
    return XR_Point3ToDist2(mo1.x, mo1.y, mo1.z, mo2.x, mo2.y, mo2.z)
end

-- Calculate direction from one mobj to another.
local function XR_DirToMobj(mo1, mo2)
    return XR_Vec3Normalize(mo2.x - mo1.x,
                            mo2.y - mo1.y,
                            mo2.z - mo1.z)
end

-- Is player able to graple? It only checks for conditions such as player is
-- not standing on the ground, and it ignores is there actually a graple point
-- nearby
local function GRAPLE_CanGraple(player)
    return not P_IsObjectOnGround(player.mo)
           and not player.deadtimer
           and not player.kartstuff[k_spinouttimer] -- Comment that out to have speen grapling
           and not player.kartstuff[k_wipeoutslow] -- ^
           and not player.kartstuff[k_respawn]
end

-- Spawn mobjs used for displaying a "rope"
local function GRAPLE_SpawnRope(pmo, graple)
    local list = {}

    local dirx, diry, dirz = XR_DirToMobj(pmo, graple)
    local dist = XR_DistBetweenMobj(pmo, graple)

    local part_count = 10

    for i = 1, part_count do
        local offx, offy, offz = XR_Vec3Scale(dirx, diry, dirz, dist*i/part_count)
        table.insert(list, P_SpawnMobj(pmo.x + offx, pmo.y + offy, pmo.z + offz, graple.MT_GRAPLE_ROPE or MT_DEFAULT_GRAPLE_ROPE))
    end

    return list
end

-- Update rope mobjs
local function GRAPLE_UpdateRope(pmo, graple, part_list)
    local dirx, diry, dirz = XR_DirToMobj(pmo, graple)
    local dist = XR_DistBetweenMobj(pmo, graple)

    for i = 1, #part_list do
        local offx, offy, offz = XR_Vec3Scale(dirx, diry, dirz, dist*i/#part_list)
        local part = part_list[i]

        P_MoveOrigin(part, pmo.x + offx, pmo.y + offy, pmo.z + offz)
    end
end

-- Remove rope mobjs
local function GRAPLE_RemoveRope(part_list)
    if not part_list then return end

    for i = 1, #part_list do
        if part_list[i].valid then
            P_RemoveMobj(part_list[i])
        end
    end
end

-- Aim towards closest graple point so player know where is graple point
local function GRAPLE_UpdateGrapleIndicator(graple_indicator)
    if not graple_indicator or not graple_indicator.valid then
        return
    end

    local player = graple_indicator.source_player
    local target = graple_indicator.target

    if not player or player.spectator or not player.valid then
        P_RemoveMobj(graple_indicator)
        return
    end

    if target == nil then
        local offset = GRAPLE_INDICATOR_DIST + FixedMul(GRAPLE_INDICATOR_DIST/8, sin(leveltime*ANG10))
        P_MoveOrigin(graple_indicator, player.mo.x, player.mo.y,
            player.mo.z + FixedMul(offset, mapobjectscale))

        if player.cmd.buttons & BT_BRAKE
            and GRAPLE_CanGraple(player)
            and leveltime % 2 == 0 then
            graple_indicator.color = SKINCOLOR_RED
        else
            graple_indicator.color = SKINCOLOR_GREY
        end

    else
        local dirx, diry, dirz = XR_DirToMobj(graple_indicator, target)
        local offx, offy, offz = XR_Vec3Scale(dirx, diry, dirz, FixedMul(GRAPLE_INDICATOR_DIST, mapobjectscale)*2)

        graple_indicator.color = SKINCOLOR_GREEN

        P_MoveOrigin(
            graple_indicator,
            player.mo.x + offx, player.mo.y + offy, player.mo.z + offz)
    end
end

-- Set mobj types for indicator and rope
function A_SetGrapleVisuals(graple, var1, var2)
    graple.MT_GRAPLE_INDICATOR = var1
    graple.MT_GRAPLE_ROPE = var2
end

-- Set values for minimum graple distance and Hooke's law parameter
function A_SetGrapleValues(graple, var1, var2)
    graple.GRAPLE_DISTANCE = var1
    graple.GRAPLE_K = var2
end

-- Set sounds for grapling
function A_SetGrapleSounds(graple, var1, var2)
    graple.GRAPLE_START_SOUND = var1
    graple.GRAPLE_END_SOUND = var2
end

-- Set/enable/disable angle for angle checking
function A_SetGrapleAngleCheck(graple, var1, var2)
    graple.GRAPLE_CHECK_ANGLE = var1
    graple.GRAPLE_CHECK_ANGLE_VALUE = var2
end

-- Set closest possible graple point for each player.
function A_GraplePointThinker(graple)
    local GRAPLE_DISTANCE = graple.GRAPLE_DISTANCE or DEFAULT_GRAPLE_DISTANCE
    local GRAPLE_CHECK_ANGLE = DEFAULT_CHECK_ANGLE
    local GRAPLE_CHECK_ANGLE_VALUE = graple.GRAPLE_CHECK_ANGLE_VALUE or DEFAULT_CHECK_ANGLE_VALUE

    if graple.GRAPLE_CHECK_ANGLE ~= nil then
        GRAPLE_CHECK_ANGLE = graple.GRAPLE_CHECK_ANGLE
    end

    for player in players.iterate do
        -- Spawn graple indicator for player if they don't have one. That will
        -- happen only if map has at least one graple point, so if it appeared
        -- then player can figure out that this map has grapling mechanic
        if not player.spectator and not (player.graple_indicator and player.graple_indicator.valid) then
            player.graple_indicator = P_SpawnMobj(
                player.mo.x, player.mo.y, player.mo.z,
                graple.MT_GRAPLE_INDICATOR or MT_DEFAULT_GRAPLE_INDICATOR)

            player.graple_indicator.source_player = player
            player.graple_indicator.colorized = true

            if not player.seen_graple_tip then
                chatprintf(player, "\131*This map uses grapling mechanic! Use BRAKE key near graple point to start grapling.")
                player.seen_graple_tip = true
            end
        end

        -- Do not waste cpu on players that are in grapling state. player.graple
        -- may still have invalid non-nil reference if map changed when player
        -- was grapling, but thats fine since player thinker will handle that
        -- and reset player.graple to nil
        if not player.spectator and not player.graple then
            local dist = XR_DistBetweenMobj(player.mo, graple)
            local angle = R_PointToAngle2(player.mo.x, player.mo.y, graple.x, graple.y) / ANG1
            local player_angle = player.mo.angle / ANG1

            if dist < GRAPLE_DISTANCE and ((abs(player_angle-angle) <= GRAPLE_CHECK_ANGLE_VALUE) or not GRAPLE_CHECK_ANGLE) then
                if player.closest_graple == nil or dist < player.closest_graple_dist then
                    player.closest_graple = graple
                    player.closest_graple_dist = dist

                    player.graple_indicator.target = graple
                end
            end
        end
    end
end

addHook("MobjThinker", function(pmo)
    local player = pmo.player

    GRAPLE_UpdateGrapleIndicator(player.graple_indicator)

    if player.cmd.buttons & BT_BRAKE and GRAPLE_CanGraple(player) then

        local graple = player.graple
        local graple_dist = player.graple_dist

        -- Start graple if we haven't already
        if not player.graple or not player.graple.valid then
            player.graple = player.closest_graple
            player.graple_dist = player.closest_graple_dist

            -- No graple points are avaible. I'm falling!
            if not player.graple or not player.graple.valid then
                return
            end

            graple = player.graple
            graple_dist = player.graple_dist

            player.graple_rope = GRAPLE_SpawnRope(pmo, graple)

            S_StartSound(player.graple_indicator, graple.GRAPLE_START_SOUND or sfx_s3k4a)
        end

        local dist = XR_DistBetweenMobj(pmo, graple)
        local dx = dist - graple_dist

        -- Hooke's law
        local force = FixedMul(max(dx, 0), graple.GRAPLE_K or DEFAULT_GRAPLE_K)

        -- I'd want to just write XR_Vec3Scale(XR_DirToMobj(...), force), but it
        -- seems lua doesn't support that
        local dirx, diry, dirz = XR_DirToMobj(pmo, graple)
        -- accel = force/mass. For now, i'll consider mass 1
        local accx, accy, accz = XR_Vec3Scale(dirx, diry, dirz, force)

        pmo.momx = $ + accx
        pmo.momy = $ + accy
        pmo.momz = $ + accz

        -- Lets give player more control over grapling
        if player.cmd.buttons & BT_ACCELERATE then
            local acceldirx = cos(pmo.angle)
            local acceldiry = sin(pmo.angle)

            pmo.momx = $ + FixedMul(GRAPLE_ACCEL, acceldirx)
            pmo.momy = $ + FixedMul(GRAPLE_ACCEL, acceldiry)
        end

        GRAPLE_UpdateRope(pmo, graple, player.graple_rope)

        -- Tie rope up, without this grapling is not that fun to use
        if dx < 0 then
            player.graple_dist = dist
        end
    else
        -- Play optional sound upon releasing the graple
        if player.graple and player.graple.valid then
            if player.graple.GRAPLE_END_SOUND then
                S_StartSound(player.graple_indicator, player.graple.GRAPLE_END_SOUND)
            end
        end

        player.graple = nil
        player.graple_dist = nil

        if not player.closest_graple and player.graple_indicator and player.graple_indicator.valid then
            player.graple_indicator.target = nil
        end
        -- GRAPLE_RemoveRope(nil) is safe (just like free(NULL) :P)
        player.graple_rope = GRAPLE_RemoveRope(player.graple_rope)
        player.closest_graple = nil
        player.closest_graple_dist = nil
    end
end, MT_PLAYER)
