/*
Ceiling Run
version 1.2.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.
This script implements temporary ceiling running, based on maintenance of a high enough movement speed. Can perform most movement options, but will fall when leaving the ground or moving too slow.
Requires landing on a slope to start ceiling running, and depending on player orientation when entering a slope will flip movement when upside down.
A player with flipcam enabled will flip actual orientation when this occurs, while a player without will have inverted "forward" movement controls.
No flipcam is the intended experience, however flipcam should be accomodated properly, as far more people use flipcam than not.
*/

if CeilingRun
	return
end

rawset(_G, "CeilingRun", true)

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

local function GetFloorSlopeZ(mo)
	local slope = mo.standingslope
	if slope == nil or slope.flags & SL_NOPHYSICS
		return 0
	else
		return slope.zangle
	end
end

local function GetFloorSlopeXY(mo, slope)
	local zangle = slope.zangle
	local xyangle = slope.xydirection
	if (slope.flags & SL_NOPHYSICS) or (zangle == 0)
		return nil
	end
	if zangle < 0 
		xyangle = $ + ANGLE_180
	end
	if mo.eflags & MFE_VERTICALFLIP
		xyangle = $ + ANGLE_180
	end
	return xyangle
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

local function P_QuantizeMomentumToSlope(vx, vy, vz, slope, slopeangle)
	local ax = 0
	local ay = 0
	local az = 0
	if (slope.flags & SL_NOPHYSICS)
		return
	end
	
	ax = -slope.d.y
	ay = slope.d.x
	az = 0
	return FV3_Rotate(vx, vy, vz, ax, ay, az, slopeangle)
end

local function P_ShouldResetConveyorMomentum(player)
	if player.onconveyor == 1
		return false
	elseif player.onconveyor == 2
		return not(player.mo.eflags & (MFE_UNDERWATER|MFE_TOUCHWATER))
	elseif player.onconveyor == 3
		return true
	elseif player.onconveyor == 4
		return not P_IsObjectOnGround(player.mo)
	else
		return true
	end
end

//undoes the slope physics	
local function P_ReverseButteredSlope(mo, momx, momy)
	local thrust = 0
	if not mo.standingslope
		return
	end
	
	if mo.standingslope.flags & SL_NOPHYSICS
		return
	end
	
	if mo.flags & (MF_NOCLIPHEIGHT|MF_NOGRAVITY)
		return
	end
	
	if mo.player
		if (abs(mo.standingslope.zdelta) < FRACUNIT/4 and not(mo.player.pflags & PF_SPINNING))
			return
		end
		if (abs(mo.standingslope.zdelta) < FRACUNIT/2 and not(mo.player.rmomx or mo.player.rmomy))
			return
		end
	end
	thrust = sin(mo.standingslope.zangle) * 3/2 * -P_MobjFlip(mo)
	if mo.player and (mo.player.pflags & PF_SPINNING)
		local mult = 0
		if momx or momy
			local angle = R_PointToAngle2(0,0,momx, momy) - mo.standingslope.xydirection
			if (P_MobjFlip(mo) * mo.standingslope.zdelta < 0)
				angle = $+ANGLE_180
			end
			mult = cos(angle)
		end
		thrust = FixedMul(thrust, FRACUNIT*2/3 + mult/8)
	end
	if momx or momy
		thrust = FixedMul(thrust, FRACUNIT + P_AproxDistance(momx, momy)/16)
	end
	
	thrust = FixedMul(thrust, abs(P_GetMobjGravity(mo)))
	
	thrust = FixedMul(thrust, FixedDiv(mo.friction, 29*FRACUNIT/32))
	
	P_Thrust(mo, mo.standingslope.xydirection, -2 * thrust)
end

//slightly modified movement code
local function P_3dMovement(player, momx, momy)

	local movepushangle, movepushsideangle = 0, 0
	local topspeed, acceleration, thrustfactor = 0, 0, 0
	local movepushforward, movepushside = 0, 0
	local mforward, mbackward = 0, 0
	local dangle = 0
	local normalspeed = FixedMul(player.normalspeed, player.mo.scale)
	local spin = (P_IsObjectOnGround(player.mo) and (player.pflags & (PF_SPINNING|PF_THOKKED)) == PF_SPINNING and (player.rmomx or player.rmomy) and not(player.pflags & PF_STARTDASH))
	local oldmagnitude, newmagnitude = 0, 0
	local totalthrustx, totalthrusty, totalthrustz = 0, 0, 0
	local momx, momy = player.mo.momx, player.mo.momy
	local cmomx, cmomy = player.cmomx, player.cmomy
	local rmomx, rmomy = 0, 0
	local speed = 0
	local aiming = 0
	
	totalthrustz = FRACUNIT*P_MobjFlip(player.mo)/3
	
	oldmagnitude = R_PointToDist2(momx - cmomx, momy - cmomy, 0, 0)
	
	if player.pflags & PF_ANALOGMODE
		movepushangle = player.cmd.angleturn << 16
	else
		movepushangle = player.mo.angle
	end
	
	movepushsideangle = movepushangle - ANGLE_90
	
	if P_ShouldResetConveyorMomentum(player)
		cmomx = 0
		cmomy = 0
	end
	rmomx = momx - cmomx
	rmomy = momy - cmomy
	
	speed = P_AproxDistance(rmomx, rmomy)    
	
	dangle = R_PointToAngle2(0, 0, player.rmomx, player.rmomy) - player.mo.angle
	if dangle > ANGLE_180
		dangle = InvAngle(dangle)
	end
	
	if dangle <= ANGLE_45
		mforward = 1
	elseif dangle >= ANGLE_135
		mbackward = 1
	end
	
	if player.pflags & PF_SLIDING
		player.cmd.forwardmove = 0
	elseif P_IsObjectOnGround(player.mo) and player.mo.state == S_PLAY_PAIN
		player.mo.state = S_PLAY_WALK
	end
	
	aiming = player.cmd.aiming << 16
	
	if player.pflags & PF_SLIDING
		normalspeed = FixedMul(36<<16, player.mo.scale)
		thrustfactor = 5
		acceleration = 96 + (FixedDiv(speed, player.mo.scale)>>16) * 40
		topspeed = normalspeed
	else
		if player.powers[pw_super] or player.powers[pw_sneakers]
			topspeed = 5 * normalspeed/3
			thrustfactor = player.thrustfactor * 2
			acceleration = player.accelstart/2 + (FixedDiv(speed, player.mo.scale)>>16) * player.acceleration/2
		else
			topspeed = normalspeed
			thrustfactor = player.thrustfactor
			acceleration = player.accelstart + (FixedDiv(speed, player.mo.scale)>>16) * player.acceleration
		end
		
		if player.powers[pw_tailsfly]
			topspeed = $>>1
		elseif player.mo.eflags & (MFE_UNDERWATER|MFE_GOOWATER)
			topspeed = $>>1
			acceleration = 2*acceleration/3
		end
	end
	if spin
		local ns = FixedDiv(549*29*FRACUNIT/32, 500* FRACUNIT)
		topspeed = FixedMul(oldmagnitude, ns)
	end

	if not player.powers[pw_tailsfly]
		if player.pflags & PF_BOUNCING
			if player.mo.state == S_PLAY_BOUNCE_LANDING
				thrustfactor = player.thrustfactor * 8
				acceleration = player.accelstart/8 + (FixedDiv(speed, player.mo.scale)>>16) * player.acceleration/8
			else
				thrustfactor = (3*player.thrustfactor)/4
				acceleration = player.accelstart + (FixedDiv(speed, player. mo.scale)>>16) * player.acceleration
			end
		end
		
		if player.mo.movefactor ~= FRACUNIT
			acceleration = FixedMul(acceleration<<16, player.mo.movefactor)>>16
		end
	end
	
	if player.climbing
	
	elseif not (player.pflags & PF_ANALOGMODE) and player.cmd.forwardmove ~= 0 and not(player.pflags & PF_GLIDING or player.exiting or P_PlayerInPain(player) and not P_IsObjectOnGround(player.mo))
		
		movepushforward = player.cmd.forwardmove * (thrustfactor * acceleration)
		
		if (player.pflags & (PF_SPINNING|PF_THOKKED)) == PF_SPINNING
			if (mforward and player.cmd.forwardmove > 0) or (mbackward and player.cmd.forwardmove < 0) or (player.pflags & PF_STARTDASH)
				movepushforward = 0
			elseif P_IsObjectOnGround(player.mo)
				movepushforward = $ / 16
			else
				movepushforward = $ /8
			end
		elseif not P_IsObjectOnGround(player.mo)
			movepushforward = $ / 4
		end
		movepushforward = FixedMul(movepushforward, player.mo.scale)
		
		totalthrustx = $ + P_ReturnThrustX(player.mo, movepushangle, movepushforward)
		totalthrusty = $ + P_ReturnThrustY(player.mo, movepushangle, movepushforward)	
	end
	
	if player.climbing
	
	elseif (player.pflags & PF_ANALOGMODE)
		if not(player.pflags & PF_GLIDING or player.exiting or P_PlayerInPain(player))
			local controldirection = R_PointToAngle2(0, 0, player.cmd.forwardmove * FRACUNIT, -player.cmd.sidemove * FRACUNIT) + movepushangle
			movepushforward = FixedHypot(player.cmd.sidemove, player.cmd.forwardmove) * (thrustfactor * acceleration)
			
			if (player.pflags & (PF_SPINNING|PF_THOKKED)) == PF_SPINNING
				if (mforward and player.cmd.forwardmove > 0) or (mbackward and player.cmd.forwardmove < 0) or (player.pflags & PF_STARTDASH)
					movepushforward = 0
				elseif P_IsObjectOnGround(player.mo)
					movepushforward = $ >> 4
				else
					movepushforward = $ >> 3
				end
			elseif not P_IsObjectOnGround(player.mo)
				movepushforward = $ >> 2
			end
			
			movepushsideangle = controldirection
			
			movepushforward = FixedMul(movepushforward, player.mo.scale)
			
			totalthrustx = $ + P_ReturnThrustX(player.mo, controldirection, movepushforward)
			totalthrusty = $ + P_ReturnThrustY(player.mo, controldirection, movepushforward)	
		end
	elseif player.cmd.sidemove and not (player.pflags & PF_GLIDING) and not player.exiting and not P_PlayerInPain(player)
		movepushside = player.cmd.sidemove * (thrustfactor * acceleration)
		
		if not P_IsObjectOnGround(player.mo)
			movepushside = $ / 4
			
			if ((player.pflags & (PF_SPINNING|PF_THOKKED)) == PF_SPINNING) or (player.powers[pw_tailsfly] and speed > topspeed)
				movepushside = $ / 4
			end
		elseif (player.pflags & (PF_SPINNING|PF_THOKKED)) == PF_SPINNING
			if player.pflags & PF_STARTDASH
				movepushside = 0
			elseif P_IsObjectOnGround(player.mo)
				movepushside = $ / 16
			else
				movepushside = $ / 8
			end
		end
		
		movepushside = FixedMul(movepushside, player.mo.scale)
		
		totalthrustx = $ + P_ReturnThrustX(player.mo, movepushsideangle, movepushside)
		totalthrusty = $ + P_ReturnThrustY(player.mo, movepushsideangle, movepushside)
	end
	
	if (totalthrustx or totalthrusty) and player.mo.standingslope and (not(player.mo.standingslope.flags & SL_NOPHYSICS)) and abs(player.mo.standingslope.zdelta) > FRACUNIT/2
		local thrustangle = R_PointToAngle2(0, 0, totalthrustx, totalthrusty)-player.mo.standingslope.xydirection
		//this is different in order to reduce movement on downwards slopes instead of on upwards ones as it would normally do
		if player.mo.standingslope.zdelta > 0
			if (thrustangle < ANGLE_90) or (thrustangle > ANGLE_270)
				local momentum = {P_QuantizeMomentumToSlope(totalthrustx, totalthrusty, totalthrustz, player.mo.standingslope, player.mo.standingslope.zangle)}
				totalthrustx = momentum[1]
				totalthrusty = momentum[2]
			end
		else
			if (thrustangle > ANGLE_90) and (thrustangle < ANGLE_270)
				local momentum = {P_QuantizeMomentumToSlope(totalthrustx, totalthrusty, totalthrustz, player.mo.standingslope, player.mo.standingslope.zangle)}
				totalthrustx = momentum[1]
				totalthrusty = momentum[2]
			end
		end
	end
	
	if totalthrustx or totalthrusty 
		momx = $ + totalthrustx
		momy = $ + totalthrusty
	else
		return nil
	end
	
	newmagnitude = R_PointToDist2(momx - cmomx, momy - cmomy, 0, 0)
	if newmagnitude > topspeed
		local tempmomx, tempmomy = 0, 0
		if (oldmagnitude > topspeed) and (not spin)
			if newmagnitude > oldmagnitude
				tempmomx = FixedMul(FixedDiv(momx - cmomx, newmagnitude), oldmagnitude)
				tempmomy = FixedMul(FixedDiv(momy - cmomy, newmagnitude), oldmagnitude)
				momx = tempmomx + cmomx
				momy = tempmomy + cmomy
			end
		else
			tempmomx = FixedMul(FixedDiv(momx - cmomx, newmagnitude), topspeed)
			tempmomy = FixedMul(FixedDiv(momy - cmomy, newmagnitude), topspeed)
			momx = tempmomx + cmomx
			momy = tempmomy + cmomy
		end
	end
	return momx, momy, rmomx, rmomy, cmomx, cmomy, speed
end

//Sets variables relating to modified movement physics
local function Set3DMoveVars(player)
	local movevars = {P_3dMovement(player)}
	if movevars[1] ~= nil
		player.nmomx = movevars[1]
		player.nmomy = movevars[2]
		player.nrmomx = movevars[3]
		player.nrmomy = movevars[4]
		player.ncmomx = movevars[5]
		player.ncmomy = movevars[6]
		player.nspeed = movevars[7]
	else
		player.nmomx = nil
		player.nmomy = nil
		player.nrmomx = nil
		player.nrmomy = nil
		player.ncmomx = nil
		player.ncmomy = nil
		player.nspeed = nil
	end
end

addHook("PreThinkFrame", function()
	if HPU.active == false
		return
	end
	
	for player in players.iterate do 
		local mo = player.mo
		if not mo
			continue
		end
		
		player.lastslopeang = GetFloorSlopeZ(mo)
		
		if not WallRun and mo.standingslope
			local dir = GetFloorSlopeXY(mo, mo.standingslope)
			if dir ~= nil
				player.lastslope = dir
			end
		end
		
		local flip = player.pflags & PF_FLIPCAM
		
		//flips actual controls if flipcam is disabled
		if (player.flipdir == true)
			if twodlevel or mo.flags2 & MF2_TWOD
				player.cmd.sidemove = -$
			elseif (not flip)
				player.cmd.forwardmove = -$
			end
		end 
		
		if player.ceilingrun and not(twodlevel or mo.flags2 & MF2_TWOD)
			Set3DMoveVars(player)
		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
				player.flipdir = false
				player.falltimer = 0
			end
			continue
		end
		if P_IsObjectOnGround(mo)
			if not player.ceilingrun
				continue
			end
			
			if player.wasroll and (mo.state ~= S_PLAY_ROLL) and not(player.cmd.buttons & BT_SPIN) and flip
				if player.cmd.forwardmove < 0
					mo.angle = player.lastslope
				else
					mo.angle = player.lastslope + ANGLE_180
				end
				player.cmd.forwardmove = -$
				player.wasroll = 0
			elseif player.wasroll and (mo.state == S_PLAY_ROLL)
				//depletes a timer for every tic spent rolling, should cover the entire slope transfer. 
				//After this runs out, any actions that remove spin somehow will not flip the camera angle. Largely should prevent issues with uncurls when upside down
				player.wasroll = $-1
			end
			
			//cancels ceilingrun if too slow
			if FixedHypot(mo.momx - player.cmomx , mo.momy - player.cmomy) < FixedMul(10*FRACUNIT, mo.scale)
				FlipGravity(mo)
				player.ceilingrun = false
				player.flipdir = false
				player.falltimer = 0
				player.wasroll = 0
			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(AngleFixed(GetFloorSlopeXY(mo, slope)) - AngleFixed(player.drawangle)) > 45*FRACUNIT
					player.flipdir = true
				end
				if speed >= threshold
					FlipGravity(mo)
					player.ceilingrun = true
					if WallRun
						//clears wallrun specific variables if present
						player.wallruntype = 0
						player.stoptimer = false
						player.fakewallrun = 0
						player.wallrun = nil
					end
					
					if (mo.state == S_PLAY_ROLL or mo.state == S_PLAY_WALLROLL) and player.flipdir and flip
						player.wasroll = 10
					end
					
					if not (twodlevel or mo.flags2 & MF2_TWOD)
						Set3DMoveVars(player)
					end
				end
				//first ceilingrun tic input flipping, prevents falling off immediately due to inputting the incorrect direction
				if player.flipdir and player.ceilingrun
					if twodlevel or mo.flags2 & MF2_TWOD
						player.cmd.sidemove = -$
					elseif not flip
						player.cmd.forwardmove = -$
					else
						//actually flips player angle if not in 2d and using flipcam (2d flipcam sucks btw)
						if (mo.state ~= S_PLAY_ROLL) and (mo.state ~= S_PLAY_WALLROLL)
							if player.cmd.forwardmove < 0
								mo.angle = GetFloorSlopeXY(mo, slope)
							else
								mo.angle = GetFloorSlopeXY(mo, slope) + ANGLE_180
							end
						end
					end
				end
			end
		elseif player.ceilingrun
			player.falltimer = $+1
			if (player.pflags & PF_JUMPED) or mo.state == S_PLAY_ROLL
				player.falltimer = 2
				if mo.state == S_PLAY_ROLL
					if WallRun
						//if wallrun is enabled, sets the same variables that would be applied from a halfpipe (requires a little bit of a slope to work properly)
						if abs(player.lastslopeang) > ANG20
							player.fakewallrun = 1
							player.wallruntype = 2
							mo.state = S_PLAY_WALLROLL
							player.pflags = $ | PF_SPINNING
						end
					end
					if mo.eflags & MFE_VERTICALFLIP and not (twodlevel or mo.flags2 & MF2_TWOD)
						mo.momz = $ * 9/4//z-momentum boost when leaving a wall from rolling to compensate for weird slope transfer when upside down
					else
						mo.momz = $ * 7/6 //slope launch physics work a lot better when leaving the floor, if your ceilingrun state
						//is floor running (reverse sector/global gravity), multiply by less to avoid weird momentum glitches
					end
				end
			end
			
			//tells wallrun to reverse wall input direction if running off a halfpipe while using flipcam in ceilingrun 
			//sets the correct angle based on movement direction
			if WallRun and flip and not(twodlevel or mo.flags2 & MF2_TWOD)
				if player.powers[pw_justlaunched] == 2 and not player.wallflip
					mo.angle = player.lastslope
					player.wallflip = true
					if player.cmd.forwardmove >= 0
						mo.angle = $ + ANGLE_180
					end
				end
			end
			
			if player.falltimer == 2
				FlipGravity(mo)
				player.ceilingrun = false
			end
		end
		
		if not player.ceilingrun
			player.wasroll = 0
			player.flipdir = false
			player.falltimer = 0
		end
	end
end)

//applies modified movement physics while in ceilingrun state
addHook("PlayerThink", function(player)
	if HPU.active == false
		return
	end
	
	local mo = player.mo
	if (player.nmomx ~= nil) and (player.ceilingrun == true) and not (twodlevel or mo.flags2 & MF2_TWOD)
		mo.momx = player.nmomx
		mo.momy = player.nmomy
		player.rmomx = player.nrmomx
		player.rmomy = player.nrmomy
		player.cmomx = player.ncmomx
		player.cmomy = player.ncmomy
		player.speed = player.nspeed
	end
	//sets nmoms to the actual player speed for use in 2D mode's reversebutterslope
	player.nmomx = player.momx
	player.nmomy = player.momy
end)

addHook("ThinkFrame", function()
	if HPU.active == false
		return
	end
	
	for player in players.iterate do
		if player.ceilingrun == true
			//changes slope physics if in ceilingrun state so upside down downward slopes have an upwards slope effect and vice versa
			P_ReverseButteredSlope(player.mo, player.nmomx, player.nmomy)
		end
		//clears all "new" movement variables 
		player.nmomx = nil
		player.nmomy = nil
		player.nrmomx = nil
		player.nrmomy = nil
		player.ncmomx = nil
		player.ncmomy = nil
		player.nspeed = nil
	end
end)