-- Rail Grinding Script

-- Define our own abs function since math.abs may be nil.
local function abs(x)
    if x < 0 then return -x else return x end
end

freeslot("S_PLAYER_GRINDING", "sfx_grind", "SPR_GSR2", "SPR2_GRND", "S_PLAY_GSGRINDING", "S_PLAY_GSCUSTOMGRIND")

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

-- Helper function: determines if the player is close enough to the rail both horizontally and vertically.
local function isRailTouched(p, line)
    local gotox, gotoy = P_ClosestPointOnLine(p.mo.x, p.mo.y, line)
    local horizDist = R_PointToDist2(p.mo.x, p.mo.y, gotox, gotoy)
    -- Use low horizontal tolerance unless the player is already grinding.
    local horizTol = 20 << 16
    if p.rail or p.mo.state == S_PLAYER_GRINDING then
        horizTol = 180 << 16
    end
    if horizDist > horizTol then 
        return false
    end

    -- Vertical tolerances remain as before.
    local tolBelow = 8 << 16   -- allowed below rail height when landing
    local tolAbove = 40 << 16  -- allowed above rail height to prevent accidental grinding

    for x = 1, 8 do
        for _, side in ipairs({line.frontside, line.backside}) do
            if side then
                local railBase = side.rowoffset + side.sector.floorheight
                local expectedZ = railBase + (50 * x << 16)
                if p.mo.z >= expectedZ - tolBelow and p.mo.z <= expectedZ + tolAbove then
                    return true
                end
            end
        end
    end

    return false
end

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
	if (mobj.player.pflags & PF_SPINNING) then return end

    local p = mobj.player
    p.railthistic = p.railthistic or false

    local turned = false
    -- If the player already has a rail, check if they're near a rail endpoint.
    if p.rail then
        for _, vertex in ipairs({p.rail.v1, p.rail.v2}) do
            if R_PointToDist2(p.mo.x, p.mo.y, vertex.x, vertex.y) <= (180 << 16) then
                -- Only allow turning if the player is on top of the rail (vertical tolerance)
                if isRailTouched(p, line) and p.mo.skin == "sonic" then
                    p.rail = line
                    p.railthistic = true
                    turned = true
                    break
                end
            end
        end
    end

    -- If no turning occurred, use the standard rail contact check.
    if not turned and isRailTouched(p, line) and p.mo.skin == "sonic" then
        p.rail = line
        p.railthistic = true
    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)

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 (p.cmd.buttons & BT_SPIN) 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 clash = P_SpawnMobjFromMobj(p.realmo, P_RandomRange(-4, 4) * FRACUNIT, P_RandomRange(-4, 4) * FRACUNIT, 0, MT_THUNDERCOIN_SPARK)
					clash.momx = P_RandomRange(-4, 4)*FRACUNIT
					clash.momy = P_RandomRange(-4, 4)*FRACUNIT
					clash.momz = P_RandomRange(0, 4)*FRACUNIT
					clash.scale = FRACUNIT/4
					clash.destscale = FRACUNIT/3
					clash.blendmode = AST_ADD
					clash.fuse = 6
					clash.colorized = true
					clash.color = SKINCOLOR_FLAME
					local clasher = P_SpawnMobjFromMobj(p.realmo, P_RandomRange(-4, 4) * FRACUNIT, P_RandomRange(-4, 4) * FRACUNIT, 0, MT_THUNDERCOIN_SPARK)
					clasher.sprite = SPR_GSR2
					clasher.frame = FF_ANIMATE
					clasher.momx = P_RandomRange(-4, 4)*FRACUNIT
					clasher.momy = P_RandomRange(-4, 4)*FRACUNIT
					clasher.momz = P_RandomRange(0, 4)*FRACUNIT
					clasher.scale = FRACUNIT/4
					clasher.destscale = FRACUNIT/3
					clasher.blendmode = AST_ADD
					clasher.fuse = 6
					clasher.colorized = true
					clasher.color = SKINCOLOR_FLAME
					local clash2 = P_SpawnMobjFromMobj(p.realmo, P_RandomRange(-5, 5) * FRACUNIT, P_RandomRange(-5, 5) * FRACUNIT, 0, MT_MINECARTSPARK)
					clash2.momx = P_RandomRange(-4, 4)*FRACUNIT
					clash2.momy = P_RandomRange(-4, 4)*FRACUNIT
					clash2.momz = P_RandomRange(0, 4)*FRACUNIT
					clash2.scale = FRACUNIT/3
					clash2.destscale = FRACUNIT/2
					clash2.blendmode = AST_ADD
					clash2.fuse = 8
					clash2.colorized = true
					clash2.color = SKINCOLOR_FLAME
					local clash3 = P_SpawnMobjFromMobj(p.realmo, P_RandomRange(-6, 6) * FRACUNIT, P_RandomRange(-6, 6) * FRACUNIT, 0, MT_MINECARTSPARK)
					clash3.momx = P_RandomRange(-6, 6)*FRACUNIT
					clash3.momy = P_RandomRange(-6, 6)*FRACUNIT
					clash3.momz = P_RandomRange(0, 6)*FRACUNIT
					clash3.scale = FRACUNIT/3
					clash3.destscale = FRACUNIT/2
					clash3.blendmode = AST_ADD
					clash3.fuse = 8
					clash3.colorized = true
					clash3.color = SKINCOLOR_YELLOW
					local spark = P_SpawnMobjFromMobj(p.realmo, P_RandomRange(-8, 8) * FRACUNIT, P_RandomRange(-8, 8) * FRACUNIT, 0, MT_MINECARTSPARK)
					spark.momx = P_RandomRange(-8, 8)*FRACUNIT
					spark.momy = P_RandomRange(-8, 8)*FRACUNIT
					spark.momz = P_RandomRange(0, 8)*FRACUNIT
					spark.colorized = true
					spark.color = SKINCOLOR_ORANGE
					spark.scale = FRACUNIT
					spark.destscale = 4*FRACUNIT
					spark.blendmode = AST_ADD
					spark.fuse = 6
					local spark2 = P_SpawnMobjFromMobj(p.realmo, P_RandomRange(-10, 10) * FRACUNIT, P_RandomRange(-10, 10) * FRACUNIT, 0, MT_MINECARTSPARK)
					spark2.momx = P_RandomRange(-6, 6)*FRACUNIT
					spark2.momy = P_RandomRange(-6, 6)*FRACUNIT
					spark2.momz = P_RandomRange(0, 6)*FRACUNIT
					spark2.colorized = true
					spark2.color = SKINCOLOR_FLAME
					spark2.scale = FRACUNIT
					spark2.destscale = 6*FRACUNIT
					spark2.blendmode = AST_ADD
					spark2.fuse = 8
					local spark3 = P_SpawnMobjFromMobj(p.realmo, P_RandomRange(-15, 15) * FRACUNIT, P_RandomRange(-15, 15) * FRACUNIT, 0, MT_MINECARTSPARK)
					spark3.momx = P_RandomRange(-8, 8)*FRACUNIT
					spark3.momy = P_RandomRange(-8, 8)*FRACUNIT
					spark3.momz = P_RandomRange(0, 8)*FRACUNIT
					spark3.colorized = true
					spark3.color = SKINCOLOR_CRIMSON
					spark3.scale = FRACUNIT
					spark3.destscale = 8*FRACUNIT
					spark3.blendmode = AST_ADD
					spark3.fuse = 10
                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) * 2 / 10), p.normalnormalspeed * 5 / 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)
