/*
Wall Run
version 1.2.2
by Kaldrum
This script adds wall running functionality when a player leaves a halfpipe, giving them either a wallrunning or wallrolling state.
When in either state, the player can walljump to gain a burst of horizontal momentum away from the wall, and can use their ability following this jump.
In the running state, the player is able to freely run on the wall, with inputs down increasing downwards momentum and inputs upwards partially counteracting gravity, allowing for extended time on the wall.
The player is able to transition into their wallrolling state at any time as long as they are currently touching the wall.
*/

if WallRun
	return
end

rawset(_G, "WallRun", true)

freeslot(
"S_PLAY_WALLRUN",
"S_PLAY_WALLROLL",
"S_TAILSOVERLAY_WALLRUN",
"SPR2_WLRN",
"SPR2_TALW"
)

local DASHMODE_THRESHOLD = TICRATE * 3
local DASHMODE_MAX = TICRATE * 3 + 3
local multiwallrun = false

//Needs different animations than default ones.
states[S_PLAY_WALLRUN] = {
	sprite = SPR_PLAY,
	frame = SPR2_WLRN,
	tics = 2,
	nextstate = S_PLAY_WALLRUN
}

states[S_PLAY_WALLROLL] = {
	sprite = SPR_PLAY,
	frame = SPR2_ROLL,
	tics = 1,
	nextstate = S_PLAY_WALLROLL
}

states[S_TAILSOVERLAY_WALLRUN] = {
	sprite = SPR_PLAY,
	frame = SPR2_TALW|FF_SPR2MIDSTART,
	tics = 2,
	nextstate = S_TAILSOVERLAY_WALLRUN
}

spr2defaults[SPR2_WLRN] = SPR2_RUN
spr2defaults[SPR2_TALW] = SPR2_TAL6

//Obtains the object's angle required to face directly towards a wall. Accounts for both front and back sidedefs.
local function GetWallAngle(mo, line)
	local mobjAngle = R_PointToAngle2(line.v1.x, line.v1.y , line.v2.x, line.v2.y)
	if(#line.frontsector == #mo.subsector.sector)
		mobjAngle = $ + ANGLE_90
		mo.lineside = 0
	elseif (line.backsector ~= nil) and (#line.backsector == #mo.subsector.sector)
		mobjAngle = $ - ANGLE_90
		mo.lineside = 1
	else //just in case
		mobjAngle = $ + ANGLE_90
		mo.lineside = 0
	end
	return mobjAngle
end

//Gets the XY angle of the launch slope in order to move directly up the slope.
local function GetFloorSlopeXY(mo)
	local slope = mo.standingslope
	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

/* If touching a wall following a halfpipe launch:
	Gets the player angle of the wall the player is touching.
	Checks if the "wall" is a thok barrier showing a skybox instead of wall texture. If so, the player cannot enter a wallrun state.
	Checks the difference between the previous wall angle and the current one. If this difference is 45 degrees or under, sets the wallangle to the new one. If not, ignores it.
	Also stores the side of this wall.
	This prevents the sides of sectors from rotating the player 90 degrees when they move between them. If there is no angle stored, stores the current wall angle.
	Sets the proper state depending on how the player launches from the slope.
	Resets the wallrun state loss tier, ends fakewallrun, and replenishes wallrun (the variable) to 1.
*/
addHook("MobjMoveBlocked", function(mo, object, line)
	if (not mo.player) or (HPU.active == false)
		return
	end
	if mo.player.wallruntype > 0	
		local lineangle = GetWallAngle(mo, line)
		local lastside = nil
		
		if mo.lineside == 0
			if line.backsector ~= nil
				if(mo.z > line.backsector.ceilingheight) and ((line.frontside.toptexture == 0) or (line.backsector.ceilingpic == "F_SKY1"))
					return
				elseif(mo.z < line.backsector.floorheight) and ((line.frontside.bottomtexture == 0) or (line.backsector.floorpic == "F_SKY1"))
					return
				elseif (line.flags & ML_IMPASSIBLE) and (mo.z>line.backsector.floorheight) and (mo.z<line.backsector.ceilingheight)
					return
				end
			end
			lastside = #line.frontside
		elseif mo.lineside == 1
			if(mo.z > line.frontsector.ceilingheight) and (line.backside.toptexture == 0)
				return
			elseif(mo.z < line.frontsector.floorheight) and ((line.backside.bottomtexture == 0) or (line.frontsector.floorpic == "F_SKY1"))
				return
			elseif (line.flags & ML_IMPASSIBLE) and (mo.z>line.frontsector.floorheight) and (mo.z<line.frontsector.ceilingheight)
				return
			end
			lastside = #line.backside
		end

		if mo.player.wallangle == nil
			mo.player.wallangle = lineangle
			mo.player.lastside = lastside
		elseif abs(mo.player.wallangle - lineangle) <= ANGLE_45
			mo.player.wallangle = lineangle
			mo.player.lastside = lastside
		end

		if (mo.player.wallruntype == 1) and (abs(mo.player.drawangle - lineangle) <= ANG60)
			if mo.state ~= S_PLAY_WALLRUN
				mo.state = S_PLAY_WALLRUN
			end
		elseif mo.player.wallruntype == 2
			if mo.state ~= S_PLAY_WALLROLL
				mo.state = S_PLAY_WALLROLL
			end
			mo.player.pflags = $ | PF_SPINNING
		end
		
		mo.player.stoptimer = false
		mo.player.pflags = $ & ~ PF_JUMPED
		mo.player.wallrun = 1
		mo.player.fakewallrun = 0
		mo.player.exitwallrun = 0	
	end
end)

addHook("PlayerThink", function(player)
	if HPU.active == false
		return
	end
	local mo = player.mo
	local flip = player.pflags & PF_FLIPCAM
	
	if mo.standingslope
		local dir = GetFloorSlopeXY(mo)
		if dir ~= nil
			player.lastslope = dir
		end
	end

	//Launch handling, if the player launches off a halfpipe while rolling, they enter wallroll. If running (or any other state), they enter wallrun.
	if player.powers[pw_justlaunched] == 2
		if not(twodlevel or mo.flags2 & MF2_TWOD)
			if player.cmd.forwardmove ~= 0
				player.wrinputdir = player.cmd.forwardmove/abs(player.cmd.forwardmove)
			else
				player.wrinputdir = 1
			end
		else
			if player.cmd.sidemove ~= 0
				player.wrinputdir = player.cmd.sidemove/abs(player.cmd.sidemove)
			else
				player.wrinputdir = 1
			end
		end
		
		if flip and player.wallflip
			player.wrinputdir = -$
			player.wallflip = false
		end
		
		//tells moveblocked which wallrun state to assign, as well as puts the player into a limited wallrun state until they are able to touch the wall
		if mo.state ~= S_PLAY_ROLL
			player.wallruntype = 1
			mo.momz = $ * 4/3
			mo.state = S_PLAY_WALLRUN
		elseif mo.state == S_PLAY_ROLL
			player.wallruntype = 2
			mo.state = S_PLAY_WALLROLL
			player.pflags = $ | PF_SPINNING
		end
		player.fakewallrun = 1
		P_Thrust(mo, player.lastslope, FRACUNIT)
		player.pflags = $ & ~ PF_JUMPED
		if not (player.cmd.buttons & BT_JUMP)
			player.wjready = true
		end
	end
	
	if P_IsObjectOnGround(mo)
		if player.wallruntype ~= -1
			mo.rollangle = 0
			player.wallruntype = 0
		end
		player.stoptimer = false
		if mo.state == S_PLAY_WALLROLL
			mo.state = S_PLAY_ROLL
			player.pflags = $ | PF_SPINNING
		end
		player.fakewallrun = 0
	end
	
	//Removes the stored wall angle and sets the reentry timer to its end if the player has lost their reentry ability.
	if not player.wallruntype and not(mo.state == S_PLAY_WALLRUN or mo.state == S_PLAY_WALLROLL)
		player.wallangle = nil
		player.wallrun = nil
		player.wrinputdir = nil
		player.prevtextoff = nil
		player.prevrowoff = nil
		player.lastside = nil
		player.exitwallrun = 16
		mo.lineside = nil
		player.wallruntype = -1
	end
	
	//conveyor movement
	if player.wallrun and player.wallangle and (player.lastside == player.lastlastside) and player.prevtextoff ~= nil
		player.wcmomx = FixedMul(sides[player.lastside].textureoffset - player.prevtextoff , cos(player.wallangle + ANGLE_90))
		player.wcmomy = FixedMul(sides[player.lastside].textureoffset - player.prevtextoff , sin(player.wallangle + ANGLE_90))
		player.wcmomz = sides[player.lastside].rowoffset - player.prevrowoff
	else
		player.wcmomx = 0
		player.wcmomy = 0
		player.wcmomz = 0
		if player.pwcmomx == nil
			player.pwcmomx = 0
			player.pwcmomy = 0
			player.pwcmomz = 0
		end
	end
	
	if player.lastside
		player.prevtextoff = sides[player.lastside].textureoffset
		player.prevrowoff = sides[player.lastside].rowoffset
		player.lastlastside = player.lastside
	end	
	
	mo.momx = $ - player.pwcmomx + player.wcmomx
	mo.momy = $ - player.pwcmomy + player.wcmomy
	mo.momz = $ - player.pwcmomz + player.wcmomz
	
	//previous conveyor values
	player.pwcmomx = player.wcmomx
	player.pwcmomy = player.wcmomy
	player.pwcmomz = player.wcmomz
	
	//Makes sure the player is always facing into the wall
	if player.wallrun ~= nil and player.wallangle ~= nil
		player.drawangle = player.wallangle
	end
	
	//Wallrun wall movement
	if mo.state == S_PLAY_WALLRUN
		if player.charflags & SF_DASHMODE
			if player.wasdashmode == true
				player.dashmode = max(player.dashmode, DASHMODE_MAX)
			end
			player.dashmode = $+3 //gives an extra boost to dashmode if already in it as well as preserving values below threshold
		end
		//Gets player inputs for the first frame before they are stored
		if player.wallangle == nil
			if not(twodlevel or mo.flags2 & MF2_TWOD)
				player.forwardinput = player.cmd.forwardmove * player.wrinputdir
			else
				player.forwardinput = player.cmd.sidemove * player.wrinputdir
			end
		else
			if not(twodlevel or mo.flags2 & MF2_TWOD)
				P_Thrust(mo, player.wallangle - ANGLE_90, FRACUNIT * player.turninput / 50)
				//rollangle modifications look pretty jank right now, hopefully if proper sprites and models are made it'll look less weird
				if player.forwardinput > 0
					mo.rollangle = FixedAngle(FRACUNIT * player.turninput * -90 / 50)
				elseif player.forwardinput < 0
					mo.rollangle = ANGLE_180 + FixedAngle(FRACUNIT * player.turninput * 90 / 50)
				end
			else
				if player.forwardinput > 0
					mo.rollangle = 0
				elseif player.forwardinput < 0
					mo.rollangle = ANGLE_180
					//reverses drawangle in 2D as well as rollangle. looks weird with models, not sure why there is an inconsistency
					player.drawangle = player.wallangle + ANGLE_180
				end
			end
		end
		P_SetObjectMomZ(mo, abs(P_GetMobjGravity(mo)) * player.forwardinput / 100, true)
		
		//Wall spin, should add more character exclusions at some point to increase compatibility with other mods
		if (player.cmd.buttons & BT_SPIN) and not (mo.skin == "fang" or mo.skin == "amy")
			mo.state = S_PLAY_WALLROLL
			player.wallruntype = 2
			player.pflags = $ | PF_SPINNING
			S_StartSound(mo, 17)
			mo.rollangle = 0
		end
	end
	
	//Walljump button handling
	if (player.cmd.buttons & BT_JUMP) and (player.wjready == true)
		player.wjready = false
		player.walljump = true
	elseif (player.cmd.buttons & BT_JUMP)
		player.wjready = false
		player.walljump = false
	elseif mo.state == S_PLAY_WALLRUN
		player.wjready = true
		player.walljump = false
	elseif mo.state == S_PLAY_WALLROLL
		player.wjready = true
		player.walljump = false
	else
		player.wjready = false
		player.walljump = false
	end

	//Walljump action, ends wallrun and ability to enter wallrun. Allows secondary jump actions.
	if player.walljump == true
		//if a walljump is attempted on the first frame of wallrun, must use last slope angle for wallangle purposes. 
		//Also reduces z-momentum, as it hasnt been effected by gravity yet.
		if player.wallangle == nil
			player.wallangle = player.lastslope
			mo.momz = $*3/4
		end
		P_InstaThrust(mo, player.wallangle + ANGLE_180, 20*FRACUNIT)
		mo.state = S_PLAY_JUMP
		S_StartSound(mo, 14)
		player.pflags = $ | PF_JUMPED
		player.pflags = $ & ~ PF_THOKKED
		mo.rollangle = 0
		//clears conveyor values, preventing the removal of their momentum next tic
		player.pwcmomx = 0
		player.pwcmomy = 0
		player.pwcmomz = 0
		//multiwallrun: if enabled, prevents the immediate wall run state loss from wall jumping. also rotates the player.
		if multiwallrun == true
			mo.angle = player.wallangle + ANGLE_180
			player.stoptimer = true
			if (twodlevel or mo.flags2 & MF2_TWOD)
				player.wrinputdir = -$
			end
			player.wallruntype = 1
		else		
			player.wallruntype = 0
		end
	end
	
	//Makes sure the state is cleared if still present when not on the wall.
	if (player.wallrun == 3) 
		if mo.state == S_PLAY_WALLRUN
			mo.state = S_PLAY_WALK
		elseif mo.state == S_PLAY_WALLROLL
			mo.state = S_PLAY_ROLL
			mo.momx = $ - FixedMul(abs($) , cos(player.wallangle))
			mo.momy = $ - FixedMul(abs($) , sin(player.wallangle))
		end
		mo.rollangle = 0
		mo.linexoff = nil
		mo.lineyoff = nil
		player.wallrun = nil
	elseif player.wallrun ~= nil
		if player.wallangle ~= nil
			if mo.state == S_PLAY_WALLROLL
				P_Thrust(mo, player.wallangle, 4*FRACUNIT)
			else
				P_Thrust(mo, player.wallangle, FRACUNIT)
			end
		end	
		if player.wallrun < 3
			player.wallrun = $ + 1
		end
	end	
	
	//small grace period to actually touch the wall when first off a halfpipe
	if player.fakewallrun
		player.fakewallrun = $ + 1
		if player.fakewallrun == 5
			player.fakewallrun = 0
			if mo.state == S_PLAY_WALLRUN
				mo.state = S_PLAY_WALK
			elseif mo.state == S_PLAY_WALLROLL
				mo.state = S_PLAY_ROLL
				mo.momx = $ - FixedMul(abs($) , cos(player.lastslope))
				mo.momy = $ - FixedMul(abs($) , sin(player.lastslope))
			end	
			player.wallruntype = 0
		else
			P_Thrust(mo, player.lastslope, FRACUNIT)
		end
	end
	
	//15 tic timer to remove ability to reenter wallrunning
	if player.exitwallrun < 15 and player.stoptimer == false and not player.wallrun
		player.exitwallrun = $ + 1
	end
	
	if player.exitwallrun == 15
		if player.cmd.forwardmove == 0 and not (twodlevel or mo.flags2 & MF2_TWOD)
			P_TryMove(mo, mo.x - (12 * cos(player.wallangle)), mo.y - (12* sin(player.wallangle)), mo.z)
		end
		player.wallruntype = 0
		player.exitwallrun = 16
	end
	
	//kinda lame implementation of this, seems like it works better now though with other fixes
	if player.charflags & SF_DASHMODE
		if player.dashmode > DASHMODE_THRESHOLD
			player.wasdashmode = true
		elseif player.dashmode < 84
			player.wasdashmode = false
		end
	end
end)

//Saves the players inputs for use in wall movement, but prevents them from acutally changing the player's direction.
addHook("PreThinkFrame", function()
	if HPU.active == false
		return
	end
	for player in players.iterate do	
		local mo = player.mo
		if mo.state == S_PLAY_WALLRUN
			if not(twodlevel or mo.flags2 & MF2_TWOD)
				player.forwardinput = player.cmd.forwardmove * player.wrinputdir
				player.turninput = player.cmd.sidemove * player.wrinputdir
				if (player.cmd.forwardmove ~= 0) and (player.forwardinput < 0) and (player.flipdir == false)
					player.cmd.forwardmove = 0
				end
				player.cmd.sidemove = 0
			else 
				//uses sidemove instead of forwardmove in 2d mode. disables sideways wall running
				player.forwardinput = player.cmd.sidemove * player.wrinputdir
				if (player.cmd.sidemove ~= 0) and (player.forwardinput < 0) and (player.flipdir == false)
					player.cmd.sidemove = 0
				end
				player.cmd.forwardmove = 0
			end
		elseif mo.state == S_PLAY_WALLROLL
			if not(twodlevel or mo.flags2 & MF2_TWOD)
				player.cmd.forwardmove = 0
			else
				player.cmd.sidemove = 0
				player.cmd.forwardmove = 0
			end	
			player.forwardinput = 0
			player.turninput = 0
		else
			player.forwardinput = 0
			player.turninput = 0
		end
	end
end)

addHook("MapChange", function(map)
	//add "Lua.multiwallrun = active" to a level header to enable subsequent wall runs following a walljump
	if mapheaderinfo[map].multiwallrun == "active"
		multiwallrun = true
	else
		multiwallrun = false
	end
end)

addHook("NetVars", function(net)
	multiwallrun = net(multiwallrun)
end)

	
/*TO DO:
	*add actual animations for wall running
	*probably more stuff idk
*/