/*
Wall Run
version 1.1.0
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"
)

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_RUN,
	tics = 3,
	nextstate = S_PLAY_RUN
}

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

//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


//source code but wallrun also allows a jetfume to appear. slightly offset when rollangle makes the player face upside down
local function P_DoMetalJetFume(player, fume) 
	local FUME_SKINCOLORS =
	{
		SKINCOLOR_ICY,
		SKINCOLOR_SKY,
		SKINCOLOR_CYAN,
		SKINCOLOR_WAVE,
		SKINCOLOR_TEAL,
		SKINCOLOR_AQUA,
		SKINCOLOR_SEAFOAM,
		SKINCOLOR_MINT,
		SKINCOLOR_PERIDOT,
		SKINCOLOR_LIME,
		SKINCOLOR_YELLOW,
		SKINCOLOR_SANDY,
		SKINCOLOR_GOLD,
		SKINCOLOR_APRICOT,
		SKINCOLOR_SUNSET
	}
	local mo = player.mo
	local angle = player.drawangle
	local rollangle = mo.rollangle
	local dist = 0
	local heightoffset = 0
	if (mo.eflags & MFE_VERTICALFLIP)
		heightoffset = mo.height - (P_GetPlayerHeight(player)>>1)
	else
		heightoffset = (P_GetPlayerHeight(player)>>1)
	end
	
	local dashmode = min(player.dashmode, DASHMODE_MAX)
	local underwater = mo.eflags & MFE_UNDERWATER
	
	if (mo.state ~= S_PLAY_WALLRUN and player.panim ~= PA_WALK and player.panim ~= PA_RUN and player.panim ~= PA_DASH)
		if fume.state ~= fume.info.spawnstate
			fume.state = fume.info.spawnstate
		end
		return
	end
	
	if underwater
		fume.movedir = $ + FixedAngle(FixedDiv(2*player.speed, 3 * mo.scale))
		fume.movefactor = $ + player.speed
		
		if fume.movefactor > FixedDiv(2*player.normalspeed, 3 *mo.scale)
			local i = 0
			local radiusv = 4*FRACUNIT
			local radiusx = P_ReturnThrustX(mo, angle, -mo.radius)
			local radiusy = P_ReturnThrustY(mo, angle, -mo.radius)
			local factorx = P_ReturnThrustX(mo, angle + ANGLE_90, mo.scale)
			local factory = P_ReturnThrustY(mo, angle + ANGLE_90, mo.scale)
			local offseth = 0
			local offsetv = 0
			local x = 0
			local y = 0
			local z = 0
			for i = -1, 1, 2 
			do
				offseth = i*P_ReturnThrustX(fume, fume.movedir, radiusv)
				offsetv = i*P_ReturnThrustY(fume, fume.movedir, radiusv)
				x = mo.x + radiusx + FixedMul(offseth, factorx)
				y = mo.y + radiusy + FixedMul(offseth, factory)
				z = mo.z + heightoffset + offsetv
				P_SpawnMobj(x, y, z, MT_SMALLBUBBLE).scale = mo.scale>>1
			end
			fume.movefactor = 0
		end
		
		if player.panim == PA_WALK
			if fume.state ~= fume.info.spawnstate
				fume.threshold = 0
				fume.state = fume.info.spawnstate
			end
			return
		end
	end
	
	if(fume.state == fume.info.spawnstate)
		fume.state = fume.info.seestate
	end
	
	if (dashmode > DASHMODE_THRESHOLD) and (fume.state ~= fume.info.seestate)
		fume.destscale = mo.scale
		fume.flags2 = $ & ~ MF2_DONTDRAW
		fume.flags2 = $ | (mo.flags2 & MF2_DONTDRAW)
		
	else
		if dashmode == DASHMODE_THRESHOLD and dashmode > fume.movecount
			fume.state = fume.info.seestate
			P_SetScale(fume, mo.scale << 1)
		end
		
		fume.flags2 = (fume.flags2 & ~ MF2_DONTDRAW) | (mo.flags2 & MF2_DONTDRAW)
		fume.destscale = mo.scale + FixedDiv(player.speed, player.normalspeed)
		
		if underwater
			fume.destscale = $/6
		else
			fume.destscale = $/3
		end
		
		fume.color = FUME_SKINCOLORS[(dashmode * table.maxn(FUME_SKINCOLORS) / (DASHMODE_MAX + 1))+1]
		
		if underwater
			fume.frame = fume.frame & FF_FRAMEMASK | FF_ANIMATE | (P_RandomRange(0,9) * FF_TRANS10)
			fume.threshold = 1
		elseif fume.threshold
			fume.frame = (fume.frame & FF_FRAMEMASK) | fume.state.frame
			fume.threshold = 0
		end
	end
	fume.movecount = dashmode
	fume.flags2 = (fume.flags2 & ~ MF2_OBJECTFLIP) | (mo.flags2 & MF2_OBJECTFLIP)
	fume.eflags = (fume.eflags & ~ MFE_VERTICALFLIP) | (mo.eflags & MFE_VERTICALFLIP)
	
	dist = -mo.radius - FixedMul(fume.info.radius, fume.destscale - mo.scale/3)
	//gets offset from the player sprite when rolled
	fume.rollangle = rollangle
	local x = mo.x + P_ReturnThrustX(fume, angle, dist)
	local y = mo.y + P_ReturnThrustY(fume, angle, dist)
	local z = mo.z + heightoffset - (fume.height>>1)
	P_TeleportMove(fume, x, y, z)
	if player.normalspeed >=  player.normalspeed * 2
		P_SpawnGhostMobj(fume)
	end
end
				
addHook("FollowMobj", function(player, fume)
	if not player.mo.skin == "metalsonic"
		return
	end
	P_DoMetalJetFume(player, fume)
	return true
end, MT_METALJETFUME)

/* 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.
	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 timer.
*/
addHook("MobjMoveBlocked", function(mo, object, line)
	if not mo.player
		return
	end
	if mo.player.wallruntype ~= 0	
		local lineAngle = GetWallAngle(mo, line)
		
		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
		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
			
		end

		if mo.player.wallangle == nil
			mo.player.wallangle = lineAngle
		elseif abs(mo.player.wallangle - lineAngle) <= ANGLE_45
			mo.player.wallangle = lineAngle
		end
		
		if (mo.player.wallruntype == 1) and (abs(mo.player.drawangle - lineAngle) <= ANG60)
			P_Thrust(mo, mo.player.wallangle, FRACUNIT)
			mo.player.stoptimer = false
			mo.state = S_PLAY_WALLRUN		
			mo.player.pflags = $ & ~ PF_JUMPED
		elseif mo.player.wallruntype == 2
			mo.state = S_PLAY_WALLROLL
			mo.player.stoptimer = false
			mo.player.pflags = $ | PF_SPINNING
			mo.player.pflags = $ & ~ PF_JUMPED
		end
		
		mo.player.wallrun = 1
		mo.player.exitwallrun = 0
	end
end)

addHook("PlayerThink", function(player)
	local mo = player.mo
	//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 mo.state ~= S_PLAY_ROLL
			player.wallruntype = 1
			mo.momz = $ * 4/3
		elseif mo.state == S_PLAY_ROLL
			player.wallruntype = 2
			if (player.cmd.forwardmove ~= 0 or player.cmd.sidemove ~= 0)
				P_Thrust(mo, player.drawangle, FRACUNIT)
			end
		end
	end
	
	if P_IsObjectOnGround(mo)
		player.wallruntype = 0
		player.stoptimer = false
		if mo.state == S_PLAY_WALLROLL
			mo.state = S_PLAY_ROLL
			player.pflags = $ | PF_SPINNING
		end
	end
	
	//Removes the stored wall angle and sets the reentry timer to its end if the player has lost their reentry ability.
	if player.wallruntype == 0 and not(mo.state == S_PLAY_WALLRUN or mo.state == S_PLAY_WALLROLL)
		player.wallangle = nil
		player.wrinputdir = nil
		player.exitwallrun = 16
	end
	
	//If the player is not wallrunning, and their state ends, starts a timer which prevents the player from reentering the state.
	if player.wallrun == 2 and not(mo.state == S_PLAY_WALLRUN or mo.state == S_PLAY_WALLROLL)
		mo.rollangle = 0
		player.wallrun = 0
	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
		end
		P_Thrust(mo, player.wallangle, FRACUNIT)
		
		if player.forwardinput~= 0
			P_SetObjectMomZ(mo, abs(P_GetMobjGravity(mo)) * player.forwardinput / 100, true)
			if not(twodlevel or mo.flags2 & MF2_TWOD)
				player.drawangle = player.wallangle
				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
					player.drawangle = player.wallangle //this is meant to correct sprites facing the incorrect direction when reversed, however looks weird with models for some reason
				elseif player.forwardinput < 0
					mo.rollangle = ANGLE_180
					player.drawangle = player.wallangle + ANGLE_180
				end
			end
		end
		
		//Wall spin, should add more character exclusions at some point to increase compatibility with other mods
		if (player.cmd.buttons & BT_SPIN) and (player.wallrun == 1) 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
	
	// Not entirely sure if this is the best way to keep a rolling player on the wall, the slight forward movement prevents them from landing back onto the slope without extra input.
	if mo.state == S_PLAY_WALLROLL
		P_Thrust(mo, player.wallangle, FRACUNIT/128)
		player.drawangle = player.wallangle
	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
		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
		//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
	
	//If the player is off of the wall for 15 tics, then they cannot reenter a wallrunning state.
	if (player.wallrun == 0) and (player.exitwallrun < 15) and (player.stoptimer ~= true)
		player.exitwallrun = $ + 1
	end
	
	if player.wallrun == 1
		player.wallrun = 2
	end
	
	if player.exitwallrun == 15
		player.wallruntype = 0
		player.exitwallrun = 16
	end
	//kinda lame implementation of this, will actually fix it later
	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()
	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.inputFlip == 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.inputFlip == false)
					player.cmd.sidemove = 0
				end
			end
		elseif mo.state == S_PLAY_WALLROLL
			if not(twodlevel or mo.flags2 & MF2_TWOD)
				player.cmd.forwardmove = 0
			else
				player.cmd.sidemove = 0
			end
		end
	end
end)

addHook("MapChange", function(map)
	//add "Lua.multiwallrun" 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
		*model support would be easier, just rotation of existing running ones, but new spriting would also be necessary
		*animation speed should also be based on z momentum
	*maybe figure out a way to keep wallroll touching the wall without adding extra forward momentum so manual adjustment isn't required to land back in the same place when launching straight up
	*probably more stuff idk
*/