-- Rail Grinding Script

freeslot("S_PLAYER_GRINDING", "sfx_grind")

states[S_PLAYER_GRINDING] = {
    sprite = SPR_PLAY,
    frame = FF_ANIMATE | SPR2_ROLL,
    tics = -1,
    var1 = 5,
    var2 = 2,
    nextstate = S_PLAYER_GRINDING,
    isGrinding = true,
}

addHook("MobjLineCollide", function(mobj, line)
    if not mobj.player then return end
    if not (line.flags & ML_EFFECT4) then return end
    if not P_IsObjectOnGround(mobj) then return end
    local p = mobj.player
    p.railthistic = p.railthistic or false
    if p.rail == nil or (line ~= p.rail and min(R_PointToDist2(p.mo.x, p.mo.y, line.v1.x, line.v1.y), R_PointToDist2(p.mo.x, p.mo.y, line.v2.x, line.v2.y)) <= 24 << 16) then
        local hasrail = false
        local bestrailheight = 128 << 16
        for x = 1, 8 do
            for y = 0, 1 do
                local railside = line.frontside
                if y == 1 then
                    railside = line.backside
                end
                if railside == nil then continue end
                if p.mo.z == railside.rowoffset + railside.sector.floorheight + (8 * x << 16) and railside.rowoffset + (8 * x << 16) < bestrailheight then
                    hasrail = true
                    bestrailheight = railside.rowoffset + (8 * x << 16)
                end
            end
        end
        if not hasrail then
            for i = 0, 3 do
                local zcheck = P_FloorzAtPos(p.mo.x + P_ReturnThrustX(p.mo, p.mo.angle + 90 * i * ANG1, 32 << 16), p.mo.y + P_ReturnThrustY(p.mo, p.mo.angle + 90 * 1 * ANG1, 32 << 16), p.mo.z, p.mo.height)
                if (p.mo.z - zcheck) >= 0 and (p.mo.z - zcheck) < bestrailheight then
                    hasrail = true
                    bestrailheight = p.mo.z - zcheck
                else
                    local dummy = P_SpawnMobj(p.mo.x + P_ReturnThrustX(p.mo, p.mo.angle + 90 * i * ANG1, 32 << 16), p.mo.y + P_ReturnThrustY(p.mo, p.mo.angle + 90 * i * ANG1, 32 << 16), p.mo.z, MT_THOK)
                    for fof in dummy.subsector.sector.ffloors() do
                        if (p.mo.z - fof.topheight) >= 0 and (p.mo.z - fof.topheight) < bestrailheight then
                            hasrail = true
                            bestrailheight = p.mo.z - fof.topheight
                        end
                    end
                    P_KillMobj(dummy)
                end
            end
        end
        if hasrail and (p.cmd.buttons & BT_FIRENORMAL) and p.mo.z ~= p.mo.subsector.sector.floorheight then
            p.rail = line
            p.railthistic = true
        end
    end
end)

addHook("MapChange", function(mapnum)
    for p in players.iterate do
        p.rail = nil
        p.railthistic = nil
        p.isGrinding = true
        p.normalnormalspeed = nil
    end
end)

local function PredictPositionOnRail(player, tics)
    local predictedX = player.mo.x + player.mo.momx * tics
    local predictedY = player.mo.y + player.mo.momy * tics
    local predictedZ = player.mo.z

    if player.rail then
        local railLine = player.rail
        local railLength = CalculateDistance(railLine.v1.x, railLine.v1.y, railLine.v2.x, railLine.v2.y)
        local railDirection = R_PointToAngle2(railLine.v1.x, railLine.v1.y, railLine.v2.x, railLine.v2.y)

        -- Increase the turn prediction distance based on rail length
        local turnPrediction = railLength * (1 / 2)  -- Adjust this factor as needed

        -- Calculate the predicted position after factoring in rail direction
        local railAngle = R_PointToAngle2(player.mo.x, player.mo.y, railLine.v1.x, railLine.v1.y)
        predictedX = predictedX + P_ReturnThrustX(player.mo, railDirection, turnPrediction)
        predictedY = predictedY + P_ReturnThrustY(player.mo, railDirection, turnPrediction)
    end

    return predictedX, predictedY, predictedZ
end

addHook("PostThinkFrame", function()
    for p in players.iterate do
        if p == nil or p.mo == nil then continue end
        if p.normalnormalspeed == nil then
            p.normalnormalspeed = p.normalspeed
        end

        if (p.cmd.buttons & BT_JUMP) or (not (p.cmd.buttons & BT_FIRENORMAL)) or (p.mo.state ~= S_PLAYER_GRINDING and not p.railthistic) then
            p.rail = nil
            p.railthistic = false
            p.isGrinding = false
            S_StopSoundByID(p.mo, sfx_grind)
            if p.mo.state == S_PLAYER_GRINDING then
                p.mo.state = S_PLAY_SPRING
            end
        end
        if p.railthistic or p.mo.state == S_PLAYER_GRINDING then
            if p.mo.state ~= S_PLAYER_GRINDING then
                p.mo.state = S_PLAYER_GRINDING
            end
            p.pflags = $ & !PF_SPINNING
            local momangle = R_PointToAngle2(p.mo.x, p.mo.y, p.mo.x + p.mo.momx, p.mo.y + p.mo.momy)
            local momspeed = R_PointToDist2(p.mo.x, p.mo.y, p.mo.x + p.mo.momx, p.mo.y + p.mo.momy)
            if p.rail ~= nil then
                local gotox, gotoy = P_ClosestPointOnLine(p.mo.x + P_ReturnThrustX(p.mo, momangle, momspeed), p.mo.y + P_ReturnThrustY(p.mo, momangle, momspeed), p.rail)
                P_InstaThrust(p.mo, R_PointToAngle2(p.mo.x, p.mo.y, gotox, gotoy), momspeed)
            end
            if FixedHypot(p.mo.momx, p.mo.momy) >= 8 << 16 then
                if not S_SoundPlaying(p.mo, sfx_grind) then
                    S_StartSoundAtVolume(p.mo, sfx_grind, 191)
                end
                if leveltime % 3 == 1 then
                    local particle = P_SpawnMobjFromMobj(p.mo, 0, 0, 0, MT_SPINDUST)
                end
            else
                S_StopSoundByID(p.mo, sfx_grind)
            end
            p.mo.momx = $ * 41 / 40
            p.mo.momy = $ * 41 / 40
            p.normalspeed = min(max($, FixedHypot(p.mo.momx, p.mo.momy) * 4 / 3), p.normalnormalspeed * 4 / 3)
        else
            if p.rail ~= nil then
                local gotrail = false
                for line in lines.iterate do
                    if line == p.rail then continue end
                    if gotrail then continue end
                    if not (line.flags & ML_EFFECT4) then continue end
                    for x = 1, 2 do
                        local vertex = line.v1
                        local vertexopp = line.v2
                        if x == 2 then
                            vertex = line.v2
                            vertexopp = line.v1
                        end
                        if (vertex == p.rail.v1 or vertex == p.rail.v2) and R_PointToDist2(p.mo.x, p.mo.y, vertex.x, vertex.y) <= FixedHypot(p.mo.momx, p.mo.momy) * 5 then
                            p.rail = line
                            local gotox, gotoy = P_ClosestPointOnLine(p.mo.x, p.mo.y, p.rail)
                            P_TryMove(p.mo, gotox, gotoy, true)
                            gotrail = true
                        end
                    end
                end
                if not gotrail then
                    P_SetObjectMomZ(p.mo, max(3 << 16, p.mo.momz))
                    if p.mo.state == S_PLAYER_GRINDING then
                        p.mo.state = S_PLAY_ROLL
                        p.mo.spintime = 10
                    end
                    p.pflags = $ | PF_JUMPED
                    if (p.pflags & PF_THOKKED) then
                        p.pflags = $ - PF_THOKKED
                    end
                    if p.mo.conduitstate == 3 then
                        p.mo.conduitstate = 0
                    end
                    p.rail = nil
                end
            end
            p.normalspeed = p.normalnormalspeed
        end
        p.railthistic = false
    end
end)