/*
Ceiling Run
version 1.0.2
by Lach
modified by Kaldrum
A modification of Lach's ArcadePhysics ceiling running script from Chrispy's Arcade. Should allow for implementation of loops.
Requires landing on a slope to start ceiling running, and depending on player orientation when entering a slope will flip movement when upside down.
Wouldn't recommend using flipcam with this active.
*/
	
local CEILING_SPEED_THRESHOLD = 16*FRACUNIT

local function FixedSquare(n)
	return FixedMul(n, n)
end

//Will swap the current gravity state, allowing for ceiling running in existing reverse gravity.
local function FlipGravity(mo)
	mo.flags2 = $ ^^ MF2_OBJECTFLIP
	mo.eflags = $ ^^ MFE_VERTICALFLIP
end

//Cool slope math (that I don't understand)
local function FV3_Rotate(rotVecX, rotVecY, rotVecZ, axisVecX, axisVecY, axisVecZ, angle)
	local ux, uy, uz = FixedMul(axisVecX, rotVecX), FixedMul(axisVecX, rotVecY), FixedMul(axisVecX, rotVecZ)
	local vx, vy, vz = FixedMul(axisVecY, rotVecX), FixedMul(axisVecY, rotVecY), FixedMul(axisVecY, rotVecZ)
	local wx, wy, wz = FixedMul(axisVecZ, rotVecX), FixedMul(axisVecZ, rotVecY), FixedMul(axisVecZ, rotVecZ)
	local sa, ca = sin(angle), cos(angle)
	local ua = ux + vy + wz
	local ax, ay, az = FixedMul(axisVecX, ua), FixedMul(axisVecY, ua), FixedMul(axisVecZ, ua)
	local xs, ys, zs = FixedSquare(axisVecX), FixedSquare(axisVecY), FixedSquare(axisVecZ)
	local bx, by, bz = FixedMul(rotVecX, ys + zs), FixedMul(rotVecY, xs + zs), FixedMul(rotVecZ, xs + ys)
	local cx, cy, cz = FixedMul(axisVecX, vy + wz), FixedMul(axisVecY, ux + wz), FixedMul(axisVecZ, ux + vy)
	local dx, dy, dz = FixedMul(bx - cx, ca), FixedMul(by - cy, ca), FixedMul(bz - cz, ca)
	local ex, ey, ez = FixedMul(vz - wy, sa), FixedMul(wx - uz, sa), FixedMul(uy - vx, sa)
	
	return ax + dx + ex, ay + dy + ey, az + dz + ez
end

local function GetSlopeLandingMomentum(mo, slope)
	if not slope
	or slope.flags & SL_NOPHYSICS
	or slope.zangle == 0
		return mo.momx, mo.momy
	end
	
	return FV3_Rotate(mo.momx, mo.momy, 2 * mo.momz, -slope.d.y, slope.d.x, 0, InvAngle(slope.zangle))
end

//Gets the slope of the floor above the player. 
local function GetFloorSlope(mo)
	if not (mo.eflags & MFE_VERTICALFLIP) == not true
		local fof = mo.floorrover
		if fof
			return fof.t_slope
		end
		return mo.subsector.sector.f_slope
	else
		local fof = mo.ceilingrover
		if fof
			return fof.b_slope
		end
		return mo.subsector.sector.c_slope
	end
end

//Determines if the object will touch the floor based on their height and momentum
local function MobjWillTouchFloor(mo)
	if not (mo.eflags & MFE_VERTICALFLIP) == not true
		return mo.z + mo.momz < mo.floorz
	else
		return mo.z + mo.height + mo.momz > mo.ceilingz
	end
end

addHook("PreThinkFrame", function()
	for player in players.iterate do 
		local mo = player.mo
		if not mo
			continue
		end
		
		if player.inputFlip == true
			player.cmd.forwardmove = -$
		end 
		
		//Cannot run on the ceiling if clipping through ceilings or floors, or carrying something.
		if (mo.flags & MF_NOCLIPHEIGHT)or (player.powers[pw_carry] ~= CR_NONE)
			if player.ceilingRun
				FlipGravity(mo)
				player.ceilingRun = false
			end
			continue
		end
		if P_IsObjectOnGround(mo)
			if not player.ceilingRun
				continue
			end
			
			if FixedHypot(mo.momx - player.cmomx , mo.momy - player.cmomy) < FixedMul(CEILING_SPEED_THRESHOLD, mo.scale)
				FlipGravity(mo)
				player.ceilingRun = false
			end
			continue
		end
		
		//Flips gravity if the player will touch a slope with enough momentum. Reverses forward and backward input if the player is flipped around by the slope.
		if MobjWillTouchFloor(mo)
			local speed = FixedHypot(GetSlopeLandingMomentum(mo, GetFloorSlope(mo)))
			local threshold = FixedMul(CEILING_SPEED_THRESHOLD, mo.scale)
			local slope = GetFloorSlope(mo)
			if not ((slope==nil) or (slope.flags & SL_NOPHYSICS) or (slope.zangle == 0))
				if (abs(slope.xydirection - mo.player.drawangle) < ANG30) 
					player.inputFlip = true
				end
				if speed >= threshold
					FlipGravity(mo)
					player.ceilingRun = true
				end
				if player.inputFlip and player.ceilingRun
					player.cmd.forwardmove = -$
				end
			end
		elseif player.ceilingRun
			player.falltimer = $+1
			if player.pflags & PF_JUMPED
				player.falltimer = 2
			end
			if player.falltimer == 2
				if player.powers[pw_justlaunched] == 2
					P_Thrust(mo, player.drawangle, FRACUNIT)
				end
				FlipGravity(mo)
				player.ceilingRun = false
			end
		end
		
		if not player.ceilingRun
			player.inputFlip = false
			player.falltimer = 0
		end
	end
end)