--╔════════════════════════════╗══════════════════════════════════════════════════════════════════════════════════════════════#
--║        Main Physics        ║ Sonic X Style Sonic! Sonic's sprites are by Blue Blur! The rest is made by Golden Shine.
--║        & Behaviour         ║ All comments here are also written from Golden Shine's perspective!
--╚════════════════════════════╝══════════════════════════════════════════════════════════════════════════════════════════════#
local XSon = XSon --Putting a local reference here makes SRB2 not have to look XSon up globally everytime!
local function P_MobjFlip(s) if s and s.valid and ((s.eflags&MFE_VERTICALFLIP) or (s.flags2&MF2_OBJECTFLIP)) return -1 end return 1 end

--First, let the game know X Sonic just spawned.
--This value is picked up by the PlayerThink hook, which then actually does the work when ready.
addHook("MobjSpawn", function(s) 
	s.xsonicspawned = true 
end, MT_PLAYER)

--Now for the main player hook!
addHook("PlayerThink", function(p)
	if not (p.xsonictable) XSon.Setup(p) return end --No table? Make one, then!	
	if not (p.mo and p.mo.valid) return end local s = p.mo --s will refer to the player object you're watching as you play!	
	local xs = p.xsonictable local yu = xs --xs will refer to the table with values we created for X Sonic.

	local ZFLIP = P_MobjFlip(s) --The less I have to call on this, the better.
	
	XSon.Timers(p,s,xs, FLIP) --The all-important timers hook.
	
	if not (IsXSonic(s)) --So we're not X Sonic then? Switched skin, maybe? 
	
		if not xs.otherchar
			xs.otherchar = true
			s.XShudpopin = nil
			if (p == consoleplayer) and (p == displayplayer) and xs.HUDpref==true
				hud.enable("lives") --Reset HUD once.
				hud.enable("score")
				hud.enable("time")
				hud.enable("rings")
			end
			if xs.supersonic or p.powers[pw_sneakers] or xs.darkphase
				if xs.supersonic or xs.darkphase
					XSon.DeTransform(p,s,xs)
				end
				XSon.SetMusic(p,s,xs)	
				if p.powers[pw_sneakers]
					p.powers[pw_sneakers] = min($,5)
				end
			end
			if p.xstauntstance
				p.xstauntstance = nil
				if (p.panim == PA_IDLE or p.panim == PA_ETC or p.panim == PA_ROLL or s.state == S_PLAY_EDGE)
				and not p.powers[pw_carry]
					s.state = (s.momz*ZFLIP > 0) and S_PLAY_SPRING or S_PLAY_FALL
				end
			end
			
			s.spriteyoffset,s.spritexoffset = 0,0
			XSon.ResetAll(p,s,xs)
			if p.powers[pw_flashing] --Preventing a minor exploit.
				p.powers[pw_flashing] = 2
			end
		end	
		return --
	end
	
	if p==consoleplayer and not ((isdedicatedserver) and (p==server))
		local CAM = CV_FindVar("cam_speed").string
		if CAM != "0.03997"
			XS_CAMSPEED = CAM
		end
	end
	
	--From now on we can be sure we're X Sonic.
	
	local SUPER = XS_Super(p)
	
	--Transformations and stats.
	if xs.darkphase --Alright then...LET'S TEST THEM OUT!!
		XSon.DSSPassive(p,s,xs)
	else
		if SUPER == 3 --I'll show you SUPER SONIC power! if xs.supersonic
			XSon.SuperSonicPassive(p,s,xs)
		else --if not supersonic!
			if xs.supersonic and not p.powers[pw_super] --XS.supersonic without pw_super means we WERE super. Apply detransformation effect!
				XSon.DeTransform(p,s,xs)
			end
			if (p.charflags & SF_SUPER)
				XSon.SFSUPER(p,s,xs, "OFF")
			end
			if p.powers[pw_invulnerability] or p.powers[pw_sneakers]
				local MUSIC = S_MusicName()
				if p.powers[pw_invulnerability] and (not s.XSInvulnMusic or MUSIC=="_inv" or MUSIC=="_minv") --Don't get interrupted by certain jingles or super music
					XSon.SetMusic(p,s,xs)
					s.XSInvulnMusic = true
				end
				if p.powers[pw_sneakers] and (not s.XSSneakerMusic or MUSIC=="_shoes")
					XSon.SetMusic(p,s,xs)
					s.XSSneakerMusic = true
				end
			end
			if not p.powers[pw_carry]
				if p.powers[pw_sneakers] and not p.xsringburst			
					XSon.SneakerBehaviour(p,s,xs)				
				elseif not ((s.flags2 & MF2_TWOD) or twodlevel) --Acceleration adjustment!					
					if p.wallrun
						p.acceleration = 72
					else
						local ACCELSTATE = 0 --Slow accel.
						if p.xsringburst or xs.FreeFlight or (p.pflags & (PF_SLIDING|PF_SPINNING|PF_THOKKED)) or xs.uwdebuff
							ACCELSTATE = -1 --Forces FAST accel no matter what.
						elseif xs.squat or p.xswallrun or xs.walljump and not xs.supersonic and not xs.darkphase
						or (xs.zipdashslide or p.skidtime) and not xs.supersonic and (p.speed < 49*s.scale) and P_IsObjectOnGround(s)
							ACCELSTATE = 2 --Forces SLOW
						else
							if xs.snapdive or xs.snapdivetrick or xs.snapdivekick or xs.supersonic or xs.darkphase==2
								ACCELSTATE = 1 --Fast accel!
							end
						end
						if P_IsObjectOnGround(s) --Gradually slow your acceleration the faster you go.
							local SPEEDMIN = p.speed/(4<<16)
							local ST,MT = 45,35
							if abs(ACCELSTATE)==1
								ST,MT = $+16,$+16
							elseif ACCELSTATE==0 and (p.speed > 18*s.scale) 
								ST,MT = $+10,$+10
							end
							p.acceleration = max(min(ST-SPEEDMIN, MT), 30)
						else
							if ACCELSTATE != 2 and not (p.panim==PA_FALL and ACCELSTATE==0 and (s.momz*ZFLIP < -6*s.scale))
								local SPEEDMIN = p.speed/(3<<16)
								p.acceleration = max(min(58-SPEEDMIN, 44), 35) --Be more consistent in midair.
							else
								p.acceleration = 35
							end
						end
					end
				else
					p.acceleration = 36 --Whatever, it's 2D, take 36 and get off my lawn.
				end
			end
		end
	end
	
	if s.health
		local SHIELD = (p.powers[pw_shield] & ~SH_FIREFLOWER) --Convert these into a Power Ring.
		if SHIELD == SH_ATTRACT or SHIELD == SH_THUNDERCOIN
			p.powers[pw_shield] = $ & ~(SH_ATTRACT|SH_THUNDERCOIN)
			P_SpawnShieldOrb(p)
			if not (s.XS_pwringget and (s.XS_pwringget>8))
				xs.PowerRing = 275
				s.XS_pwringget = 12
				if xs.FirstPWring == nil
					xs.FirstPWring = 280
				end
				S_StartSound(s, sfx_itemup)
				S_StartSound(nil, sfx_itemup, p)
			end
		end	
	end
	
	if p.powers[pw_carry] --Execute the "list of things that change when Sonic's being carried"
	
		if (XSon.Carried(p,s,xs,p.powers[pw_carry])) == "nights" return end --Stop the rest of the thinker!		
		
	elseif s.health --Be alive first.
		if p.exiting
			xs.ExitState = 2 XSon.NoInput(s, p, xs)
			if (s.eflags & MFE_JUSTHITFLOOR) and not p.xstauntstance and not p.xsringburst and not xs.skydive
				XSon.StartTaunts(p, s, xs, 77)	
			end
		elseif (gametyperules & GTR_RACE) and ((p.realtime==0) or (p.pflags & PF_GAMETYPEOVER)) --Race support!
			xs.ExitState = 2 XSon.NoInput(s, p, xs)
			XSon.RaceStart(p,s,xs)
		else
			if (gametype == GT_HIDEANDSEEK) 
				if (p.pflags & PF_TAGIT) and (leveltime < 350)
				or not (p.pflags & PF_TAGIT) and (leveltime >= 350)
					xs.ExitState = 2 XSon.NoInput(s, p, xs) 
				end
			elseif (gametyperules & GTR_TAG) and (p.pflags & PF_TAGIT) and (gametyperules & GTR_STARTCOUNTDOWN) and (leveltime < 350) --Tag support!
				xs.ExitState = 2 XSon.NoInput(s, p, xs) 
			end
		end
		
		if p.xstauntstance --Currently taunting.
			if p.xstauntstance.taunt==14
				s.sprite2 = SPR2_ROLL
				s.frame = ($&~FF_FRAMEMASK)|min(D, p.xstauntstance.frame)
				p.pflags = $|PF_STARTDASH|PF_SPINNING
				local CHARGE = p.xstauntstance.spincharge or 0
				p.dashspeed = ((CHARGE*5)+16)<<16
			end
			
		elseif p.panim == PA_RUN --Speed up Sonic's running animation?	
			
			xs.jumpboost = 5
			xs.didjumpboost = false
			
			if P_IsObjectOnGround(s)
				local RUNSPEED = (p.runspeed/FRACUNIT)*s.scale
				if (p.speed > RUNSPEED+5*s.scale)
					if (s.eflags & MFE_TOUCHWATER)
						if not (s.eflags & MFE_UNDERWATER)
							XS_WaterSkip(p,s,xs, 20*s.scale) --Kick up water effects, sprites made by Blue Blur!
						end
					else
						if SUPER == 3 --Some extra fancy groundeffects for Super boi.
							if not xs.FreeFlight
								if (leveltime%3==0) and not xs.zipdash
									XSSpawnDust(s, -FRACUNIT, 10, true)
								end
								local TRANS = (xs.darkphase) and 8<<16 or (xs.zipdash and 6<<16 or 7<<16)
								local COLOR = (s.XS_modScolor) and s.XS_modScolor or SKINCOLOR_GOLDENROD
								local BLEND = AST_ADD
								if xs.darkphase
									if not xs.zipdash
										COLOR = SKINCOLOR_XSSUPERBLUR
										BLEND = AST_REVERSESUBTRACT
									else
										COLOR = XSon.DarkAura(0)
									end
								end
								for d = -2,2
									local ghs,ths = XS_CartoonDust(s, COLOR, BLEND)
									local GHS = ghs
									for i = -1,1,2
										if (i==1) GHS = ths end
										if GHS and GHS.valid
											GHS.frame = ($ & ~FF_TRANSMASK) + min(FF_TRANS90, TRANS+(abs(d)<<16))
											GHS.dispoffset = -99
											if d != 0
												GHS.angle = p.drawangle-(((ANG2*7)*i)*d)	
												GHS.spriteyscale = $+(9000*abs(d))
												GHS.fuse = (xs.zipdash) and $+4 or $+1
												GHS.sttfade = (xs.zipdash) and 4 or $											
												local TANG = R_PointToAngle2(s.x, s.y, s.x+s.momx, s.y+s.momy)+(ANGLE_90*i)
												local DIST = ((xs.zipdash) and -10 or -20)*abs(d)*s.scale
												P_TryMove(GHS, 
												GHS.x+FixedMul(DIST, cos(TANG)), 
												GHS.y+FixedMul(DIST, sin(TANG)), true)
											end
											if (p == displayplayer) 
												if not CV_FindVar("chasecam").value
												or (CV_FindVar("renderer").value)==1
													ghs.flags2 = $|MF2_DONTDRAW
												end
											end				
										end
									end
								end
							end					
						elseif (leveltime % 2)
							local ghs,ths = XS_CartoonDust(s, nil, nil, (xs.darkphase) and FRACUNIT-13500 or nil)
							local phs,bhs = XS_CartoonDust(s, nil, nil, FRACUNIT-19500)		
							if (p == displayplayer) 
								if ths and ghs and phs and bhs
									if not (CV_FindVar("chasecam").value)
									or (CV_FindVar("renderer").value)==1
										bhs.flags2,phs.flags2,ths.flags2,ghs.flags2 = $|MF2_DONTDRAW,$|MF2_DONTDRAW,$|MF2_DONTDRAW,$|MF2_DONTDRAW
									end
								end
							end
						end
					end
				end
				if not xs.zipdash
				and (p.speed >= 43*s.scale) and (xs.direct > 28) and (s.tics > 1)
					s.tics = $-1
				end
			end
			
		--Have Sonic move his feet faster at the start of a walk
		
		elseif s.state == S_PLAY_WALK
		
			local DODUST = false	
			
			xs.jumpboost = 5
			xs.didjumpboost = false
			
			if (xs.direct > 33)
			and not (s.eflags & MFE_UNDERWATER)
			and not p.gotflag and not xs.nowalkanim --Slower animation underwater/with flag
				--You move much slower in 2D, so we have to adjust the animation
				if ((s.flags2 & MF2_TWOD) or twodlevel)
					if (s.tics > 1)
						s.tics = $-1
					end
					if (p.speed < 7*s.scale)
						if (s.tics > 1)
							s.tics = $-1
						end
						DODUST = true
					end
				elseif (p.speed < 32*s.scale) --3D animation.
					local FRAME = (s.frame & FF_FRAMEMASK)
					if (s.tics > 1) and s.xdidframe != FRAME
						s.tics = $-1
					end
					if (p.speed < 9*s.scale)
						for i = 0,8,3
							if (s.tics>1) and (p.speed + i<<16 < 9*s.scale)
								s.tics = $-1
							else
								break --
							end
						end
						DODUST = true
					end
					s.xdidframe = FRAME
				end
			end
			if DODUST == true and ((yu.direct > 33) or xs.brakeanim)
				if xs.FreeFlight
					local ghs = P_SpawnGhostMobj(s)
					ghs.scale = s.scale
					ghs.fuse,ghs.tics = 5,5
					ghs.blendmode = AST_ADD
					ghs.colorized = true
					ghs.destscale = 1
				elseif P_IsObjectOnGround(s)
					XSSpawnDust(s)
				end
			end
			if (p.pflags & PF_THOKKED) and (p.pflags & PF_JUMPED)
				p.pflags = $ & ~(PF_THOKKED|PF_JUMPED) --bugfix
			end
		
		--Dust for the dust gods!
		
		elseif (s.state == S_PLAY_SKID)
		
			XSSpawnDust(s, p.speed+8<<16, 8)
			if ((s.sprite2 & ~FF_SPR2SUPER) == SPR2_SKID)
				s.frame = ($ & ~FF_FRAMEMASK)|(((leveltime%3==0) or (leveltime%4==0)) and B or A)
			end
		
		--In pain we trust.
		elseif (p.panim == PA_PAIN)
		
			if (p.powers[pw_flashing] > 95)
			and P_PlayerInPain(p) --Get flung far, flinging Sonic screaming into a wall.
				xs.ringburstcharge = 0
				if (s.sprite2 != SPR2_CLNG) and not xs.bonked
					local DOSCREAM = false
					if not xs.painrecovery
						for i = 0,2
							S_StopSoundByID(s, sfx_xssfx1+i)
						end
						xs.alteredrollangle = false
						if (p.speed < 75*s.scale) and not SUPER
							if xs.damageboost
								local ANGLE = R_PointToAngle2(s.x, s.y, s.x+s.momx, s.y+s.momy)
								P_Thrust(s, ANGLE, abs(xs.damageboost))
							end
							if (p.speed < 75*s.scale)
								s.momx,s.momy = $*2,$*2
							end
						end
						if (abs(s.momx)+abs(s.momy) > 50*s.scale) and not SUPER
							local iv = P_RandomRange(0,1) or -1
							xs.bonkeddash = 0
							s.sprite2 = SPR2_CLNG
							s.momz = $*3/2
							DOSCREAM = true
							XSVoice(p, s, xs, sfx_xsvoc4)
							XSDoRoll(p, s, xs, 15*iv<<16, 89*iv<<16, 180, iv, FRACUNIT)
							if (p.speed > 70*s.scale) --Max speed.
								P_InstaThrust(s, R_PointToAngle2(s.x, s.y, s.x+s.momx, s.y+s.momy), 70<<16) 
							end
						else
							s.momz = $*7/6
							if not xs.FreeFlight
								local iv = P_RandomRange(0,1) or -1
								XSDoRoll(p, s, xs, 2*iv<<16, 60*iv<<16, 180, iv, FRACUNIT)
							else
								xs.alteredrollangle = true
							end
						end
						xs.damageboost = nil
					end
					if not xs.paingrunt and not xs.bonked --Voice clips!
						if DOSCREAM != false --Getting REALLY smacked here!
							XSGhostTrail(p, s, xs)
							S_StartSound(s, sfx_xssfxm)
							XSon.Sound(s, sfx_xssfxj)
						else --Regular grunts.
							if xs.darkphase
								XSVoice(p, s, xs, sfx_xsvop4) --Specifically this one
							else
								local PAINGRUNT = P_RandomRange(sfx_xsvop0,sfx_xsvop4)
								if PAINGRUNT == s.xslastpaingrunt --Don't play the same ones, though.
									PAINGRUNT = ($==sfx_xsvop4) and sfx_xsvop0 or $+1
								end
								XSVoice(p, s, xs, PAINGRUNT)
								s.xslastpaingrunt = PAINGRUNT
							end
						end
						xs.paingrunt = (xs.paingrunt or 0)+50 --Now act as cooldown timer.
					end
				
					--RECOVERY JUMP IF AN ENEMY KNOCKS SONIC OFF A CLIFF.
					xs.painrecovery = min($+1, FRACUNIT)
					if (xs.painrecovery > 3) and s.health
						XSon.QuickRecover(p,s,xs)
					end
				end
			end
		
		--Rolling animation! Is there a reason it should be here, though...?
		
		elseif (s.state == S_PLAY_ROLL) or (s.state == S_PLAY_JUMP) or (s.state == S_PLAY_SPINDASH)
		
			xs.painrecovery = 0
			
			if not (p.pflags & PF_JUMPED)
			
				if (p.cmd.buttons & BT_JUMP) and (xs.jump <= 1)
				and not (p.pflags & (PF_SLIDING|PF_STARTDASH|PF_JUMPSTASIS|PF_STASIS|PF_FULLSTASIS)) 
				and not P_IsObjectOnGround(s) and not p.powers[pw_nocontrol] and not p.exiting			
					p.pflags = $|PF_JUMPED & ~PF_JUMPDOWN --Regain jump properties. Experimental.
				end
				if (p.pflags & PF_SPINNING) and not p.xsringburst
					if xs.spin == 1
						XSon.Sound(s, sfx_xssfxh, 190)
						XSon.Sound(s, sfx_s1b8, 150)
						s.momx,s.momy = $*2/3,$*2/3
						p.pflags = $ & ~(PF_SPINNING|PF_STARTDASH|PF_THOKKED)
						local ghs = P_SpawnGhostMobj(s)
						ghs.scale = s.scale
						ghs.fuse,ghs.tics = 7,9 
						ghs.blendmode = AST_ADD
						ghs.colorized = true ghs.scalespeed = $*2 ghs.destscale = 1
						ghs.momx,ghs.momy,ghs.momz = s.momx*2/3, s.momy*2/3, s.momz/2
						XSAirGroundState(p, s, xs)
						xs.spin = 9
					elseif (p.powers[pw_flashing] and p.powers[pw_flashing] < 12) and P_IsObjectOnGround(s)
						if P_PlayerTouchingSectorSpecial(p, 3, 6) or P_PlayerTouchingSectorSpecial(p, 3, 5)
							p.pflags = $ & ~PF_STARTDASH --Hey, we touched a speed pad! Speedpad?
							xs.framejump = 25
							S_StopSoundByID(s, sfx_xssfx2)
							S_StopSoundByID(s, sfx_xssfx1)
						end
					end
				end
			end
		
		--Animation presets for EDGE frames.	
				
		elseif s.state == S_PLAY_EDGE
			if not p.xstauntstance
				if SUPER != 3
					if not xs.edgestretch and not (xs.edgeanimcd > 88) and not (xs.darkphase or xs.moody)
						XSon.SwapStretch(p, s, xs)
						s.spritexscale,s.spriteyscale = FRACUNIT-4000,FRACUNIT+18000
						xs.edgestretch = 4
					--	if not xs.edgeanimcd
					--		XSVoice(p, s, xs, sfx_xsvoc2, false)
					--	end
						xs.edgeanimcd = 90
					end
				elseif not xs.edgeanimcd
					if not (s.eflags & MFE_JUSTHITFLOOR)
						s.spritexscale,s.spriteyscale = FRACUNIT-500,FRACUNIT+1500
						s.spriteyoffset = 3<<16
						s.XSstretch = {timer = 3, Xspeed = 170, Yspeed = 500, ground = true}
					end
					xs.edgeanimcd = 2
				end
			end
		
		--Don't go into walking frames out of bubble gasp!	
				
		elseif (s.state == S_PLAY_GASP)
			xs.poutysonic = 0
			if s.tics == 1
				s.state = S_PLAY_FALL 
			end
		elseif (s.spriteyoffset!=0) and (s.state == S_PLAY_STND) and (s.tics==104)
			if P_IsObjectOnGround(s) and not p.speed --A super lazy fix for a super-specific scenario.
			and not s.XSstretch and not xs.jumpstretch and not xs.zipstretch
			and not p.powers[pw_carry]
			and not (p.pflags & (PF_STASIS|PF_SLIDING|PF_SPINNING|PF_JUMPED))
			and not (s.eflags & (MFE_SPRUNG|MFE_APPLYPMOMZ|MFE_JUSTHITFLOOR))
				s.spriteyoffset = 0
			end
		end
		
		--Massive underwater debuff. We'll make X Sonic the most painful character to play underwater ever. Mystic would be proud.
		--Fearing water really makes you FEEL like X Sonic!
		if not (s.eflags & MFE_UNDERWATER) 
			xs.wasabovewater = true	
		else
			local SHIELD = (p.powers[pw_shield] & SH_NOSTACK)
			
			if not (SHIELD & SH_PROTECTWATER)
			and SUPER != 3 --Fine, fine, you'll receive SOME mercy.
			and not p.powers[pw_invulnerability]
			and not ((p.pflags & PF_SPINNING) and not (p.pflags & PF_JUMPED)) --No resistance when spinning!
			and not p.powers[pw_carry] and s.health
			
				xs.uwdebuff = max($,1) 
				
				if xs.wasabovewater
					if not p.xswallrun
						s.momz = $/2 --Initial splash.
						xs.airborne = 0
					end
					s.XuwF,s.XuwF2 = 0,0
				end
				if not (s.eflags & MFE_GOOWATER)
				
					--Horizontal speed debuff.
					if (p.speed > 5*s.scale)
					or (p.speed > 3*s.scale) and (p.pflags & (PF_JUMPED|PF_THOKKED))	
						if (xs.uwdebuff > 18)
							if s.XuwF2 == nil s.XuwF2 = 6535 end
							if (p.panim == PA_WALK or p.panim == PA_RUN) and xs.direct
								s.XuwF2 = min(15<<16, $+FRACUNIT/12) --Progressively increasing...
							else
								s.XuwF2 = max($-FRACUNIT/3, 0)
							end
							local DIV = (s.XuwF2 > 2<<16) and (s.XuwF2>>16)-2 or 0
							s.momx = FixedDiv(FixedMul($, 12+DIV), 13+DIV)
							if not ((s.flags2 & MF2_TWOD) or twodlevel)
								s.momy = FixedDiv(FixedMul($, 12+DIV), 13+DIV)
							end
						else
							s.momx = FixedDiv(FixedMul($, 91), 92)
							if not ((s.flags2 & MF2_TWOD) or twodlevel)
								s.momy = FixedDiv(FixedMul($, 91), 92)
							end		
						end
					elseif s.XuwF2
						s.XuwF2 = max($-(FRACUNIT/3), 0)
					end
					--Slow falling speed underwater
					if not P_IsObjectOnGround(s) 
					and (s.momz*ZFLIP < -(s.scale*2))
					and not (s.eflags & (MFE_SPRUNG|MFE_APPLYPMOMZ))
					and not (p.pflags & (PF_SLIDING|PF_SPINNING))
					and not p.powers[pw_carry]
						s.XuwF = s.XuwF and min(30<<16, $+99) or 99 --Progressively increasing...
						if (xs.airborne > 84) s.XuwF = $+600 end
						
						local GRAVITY = P_GetMobjGravity(s)
						local fallspeed = FixedDiv(FixedMul(5000, 32678), GRAVITY)
						fallspeed = FixedDiv(FixedMul($, s.scale), FRACUNIT+s.XuwF)
						s.momz = $-(fallspeed*ZFLIP)
						
						--Bubble GFX.
						local rng = XSLocalRNG(-40,40,s)
						local bubble = P_SpawnMobjFromMobj(s, (rng*s.scale), (rng*s.scale), ((rng or 1/3)*s.scale), MT_SMALLBUBBLE)
						if bubble and bubble.valid
							bubble.scale = s.scale bubble.fuse = 23
							P_Thrust(bubble, p.drawangle+(rng*3*ANG2), rng*990)
							P_SetObjectMomZ(bubble, 2<<16-(abs(rng)*7500), true)
						end
						
					elseif s.XuwF and (s.momz*ZFLIP >= -1)
						s.XuwF = (not P_IsObjectOnGround(s)) and  max($-(FRACUNIT/2), 0) or 0
					end
				end
				xs.wasabovewater = nil
			end
		end

		--Midair shenanigans.		
		if not P_IsObjectOnGround(s)

			if not p.powers[pw_carry]
				if xs.framejump --For a few frames, jump or spin even after you walked off a ledge.	
					xs.framejump = $-1
				end
				
				--Stuff to do while spinning in midair.
				if (p.pflags & PF_SPINNING) and not p.xsringburst and not xs.zipdash
					if xs.spin == 1 
						--We're uncurling in midair!
						if (p.pflags & PF_SPINNING) and not (p.pflags & PF_JUMPED) and not xs.jump and not p.wallrun
							local ghs = P_SpawnGhostMobj(s)
							ghs.scale = s.scale
							ghs.blendmode = AST_ADD
							ghs.colorized = true
							ghs.destscale = 99<<16
							s.momx,s.momy = $*10/11,$*10/11
							XSon.Sound(s, sfx_s3kd1s)
							XSon.Sound(s, sfx_xssfxi, 150)
						elseif not (p.pflags & PF_THOKKED) --Begin the Ring Burst?
							p.xsringburst = nil --debug, no, not for now.
						end
					end	
					if (p.pflags & PF_THOKKED) and not (p.pflags & (PF_JUMPED|PF_NOJUMPDAMAGE))
						p.pflags = $ & ~PF_THOKKED --Remove. What's the worst that could happen?
					end
				end
				
				if (p.pflags & PF_JUMPED)
					
					--Thrust Sonic forwards during his jumps! I'll call it X physics!
					if xs.direct > 2 and not xs.zipdash
						local uwdebuff = false --underwater debuff
						local maxspeed = 50
						if (s.eflags & MFE_UNDERWATER)
							if (p.powers[pw_shield] & SH_PROTECTWATER)
							or ((p.pflags & PF_SPINNING) and not (p.pflags & PF_JUMPED))
							or (SUPER == 3)
								uwdebuff = true
								xs.uwdebuff = max($,1) 
							end
						end
						
						if not uwdebuff
							local div = (not (xs.jumpboost)) and 5 or 2
							local boost = min( max(FRACUNIT/div, abs(p.speed-(29*(FRACUNIT/div)))), FRACUNIT/div) 
							boost = FixedDiv(FixedMul(boost, xs.direct), 50) --Force strength by power of your stick press!
							local momangle = R_PointToAngle2(s.x, s.y, s.x+s.momx, s.y+s.momy) 
							if abs(xs.turnang-momangle) > ANG30 or (p.speed < 29*s.scale) 
								P_Thrust(s, xs.turnang, boost)
							end
						end
					end
					
					--Zip upwards as you jump!
					
					if xs.jumpboost
					
						xs.jumpboost = $-1 --Countdown.
					
						if not (s.eflags & (MFE_UNDERWATER|MFE_GOOWATER|MFE_SPRUNG))
						and not p.xsringburst and not xs.zipdash
						and not (s.flags & MF_NOGRAVITY)
						
							if (s.momz*ZFLIP >= 0)
								if not xs.didjumpboost
									xs.didjumpboost = true
									xs.alteredoffset = true
									XSon.SwapStretch(p, s, xs)
									xs.jumpstretch = 5
									s.spriteyoffset = -5<<16
									s.spriteyscale = FRACUNIT+4000
									s.spritexscale = FRACUNIT-2000
									s.momz = FixedDiv(FixedMul($, 100), 59) --Zip up FAST! X Sonic has never jumped slow in his life. 
									if (p.panim == PA_JUMP or p.panim == PA_ROLL) --He just thinks the girl with the pink hair is funneh
										s.sprite2 = SPR2_SPNG
										s.frame = $ & ~FF_FRAMEMASK
									end
									XSSpawnJumpDust(s)
								end
								if not xs.jumpboost and not (s.eflags & MFE_TOUCHWATER)
									s.momz = $/2 --And cancel the momentum just as quick!
								end						
							end
						end
						
					elseif not p.xsringburst and not (s.flags & MF_NOGRAVITY) --and not PF_JUMPED
					
						xs.didjumpboost = false
						local TMOMZ = s.momz*ZFLIP
						if (TMOMZ < s.scale/2) and not (TMOMZ < -22*s.scale)
						and not xs.zipdashleap
						and not (p.pflags & PF_SPINNING)
						and not (s.eflags & (MFE_UNDERWATER|MFE_GOOWATER|MFE_SPRUNG|MFE_TOUCHWATER))						
						and not (xs.ChaosSnap and xs.supersonic) and not (s.state == S_PLAY_FLOAT)
							--Boosted falling speed and situational air drag.
							local GRAVITY = P_GetMobjGravity(s)
							if GRAVITY and (GRAVITY*ZFLIP < 0)
								local fallspeed = FixedDiv(FixedMul(11000, GRAVITY), 32678)
								
								if (TMOMZ > -5*s.scale) --X Sonic falls quick!
									s.momz = $+fallspeed
								else
									fallspeed = FixedDiv(FixedMul(7400, GRAVITY), 32678)
									s.momz = $+fallspeed
								end
								s.momz = $+(fallspeed*8/17)
							end
						end
					end
					--Blow up the armagedon shield or use jumpshields manually with C3.
					if xs.custom3 == 1 and SUPER != 3
						local SHIELD = XS_SHIELD(p)
						if (SHIELD == SH_ARMAGEDDON)
							P_BlackOw(p) --Manual explosion shield
							P_ResetScore(p)
						elseif (SHIELD == SH_WHIRLWIND) or (SHIELD == SH_THUNDERCOIN)
							if not (p.pflags & (PF_SHIELDABILITY|PF_SLIDING|PF_THOKKED|PF_STASIS|PF_JUMPSTASIS|PF_FULLSTASIS))
							and not xs.snapdive and not p.xsringburst
							and not (xs.snapdivetrick and (xs.snapdivetrick < 11))
								P_DoJumpShield(p)
								xs.ringburstcharge = 0
								if (p.speed > 41*s.scale)
									XSon.AirDrag(s, 20)
								end
							end
						end
					end	
					
				else --if NOT PF_JUMPED but airborne.
				
					--Do a 5-frame-window jump after being in the air?
					if xs.jump == 1 and xs.framejump --Execute the frame jump
					
						if (s.state <= S_PLAY_DASH)
						or s.state == S_PLAY_EDGE
						or s.state == S_PLAY_SPINDASH
						or s.state == S_PLAY_ROLL
							if not (p.pflags & (PF_THOKKED|PF_JUMPED|PF_JUMPSTASIS))
							and not xs.zipdash and not xs.zipdashleap
							and not xs.snapdive and not xs.snapdivetrick
							and not p.xswallrun and not xs.FreeFlight
								xs.squat = 5
								xs.squatcharge = 0
								
								XSon.SwapStretch(p, s, xs)
								s.spriteyoffset = 0
								s.spritexscale,s.spriteyscale = FRACUNIT+1,FRACUNIT-1
								xs.zipstretch = 4		
								xs.framejump = 0
							end	
						end	
					
					
					elseif not xs.FreeFlight and not xs.zipdash
					and not (p.pflags & (PF_FULLSTASIS|PF_STASIS|PF_JUMPSTASIS))
					and not (s.eflags & MFE_SPRUNG)
					
						--Buffer system to make platforming easier. 
						if p.panim == PA_FALL or xs.snapdive
							if (p.cmd.buttons & BT_JUMP) and not xs.spin
								if not xs.fallbuffer and xs.jump == 1
									xs.fallbuffer = 6
									xs.fallbuffer2 = 0
								elseif (xs.fallbuffer > 1)
									xs.fallbuffer = $-1
								end
							elseif xs.spin and not (p.cmd.buttons & BT_JUMP) or xs.custom1
								if ((xs.fallbuffer2 or 0) < 5) and (xs.spin == 1 and not (p.cmd.buttons & BT_JUMP) or xs.custom1==1)
									xs.fallbuffer2 = 5
									xs.fallbuffer = 0
								elseif (xs.fallbuffer > 1)
									xs.fallbuffer2 = $-1
								end						
							end
						else
							xs.fallbuffer = 0
							--Walking on air? Let's not. You get a few frames, then transition into walking!
					
							if ((s.state <= S_PLAY_DASH) or (s.state == S_PLAY_FLOAT_RUN))
							and not (p.pflags & PF_SLIDING)
								if (xs.falltransition > 4) --Run off a ledge, take about 4 frames to go into falling frames.
									XSAirGroundState(p, s, xs)
									xs.hitspring = 100000
									xs.falltransition = 0
								else
									xs.falltransition = $+2
								end
							end		
						end
					end
					if xs.jump == 1 --Transition into snap dive in spring and falling frames!
						if (p.panim == PA_FALL or p.panim == PA_SPRING)
						and not (p.pflags & (PF_THOKKED|PF_FULLSTASIS|PF_STASIS|PF_SLIDING))
						and not xs.snapdive and not xs.FreeFlight
							p.pflags = $|PF_JUMPED & ~(PF_JUMPDOWN|PF_NOJUMPDAMAGE|PF_STARTJUMP)
							xs.jump = 0
						end
					end				
				end
			end
			
		else--if P_IsObjectOnGround(s) -- Grounded shenanigans. ground shen
		
			xs.jumpboost = 5
			xs.didjumpboost = false
			
			if not p.powers[pw_carry]
			
	--			XSon.SpawnEdgeStop(p,s,xs) --Stop X Sonic on the edge?
				
				--The Zip Dash, giving SRB2 a reminder what real speed looks like!
				if (xs.spin and xs.fallbuffer2 or xs.spin==1)
				or xs.custom1 and (xs.custom1 < 15)
					if (p.panim >= PA_IDLE and p.panim <= PA_DASH)
					and not (xs.jump and not (p.pflags & PF_JUMPDOWN))
						if not (p.pflags & PF_SPINNING)
						and not xs.zipdash and not xs.rollcd and not p.gotflag and not xs.ExitState
						and not (xs.zipspam and (xs.zipspam > 24))
							if xs.rollprep and (p.cmd.buttons & BT_JUMP) and not (p.cmd.buttons & BT_CUSTOM1) --and (p.cmd.buttons & BT_SPIN) 
								XSon.QuickSpin(p,s,xs, true)
							else
								XSSnapDiveReset(p, s, xs, true)
								XSon.PrepZipDash(p, s, xs, SUPER) --Now xs.zipdash = 1 .ZipDashStart"
								if not xs.zipdashslide
								and SUPER != 3 and not p.powers[pw_sneakers]
									xs.delayzip = (xs.uwdebuff) and 6 or 2
									if (p.cmd.buttons & BT_SPIN) and not (p.cmd.buttons & BT_CUSTOM1)
										xs.rollprep = 7
									end 
								end 
							end 
							xs.rollcd = 2
						end
					end
				end
				
				if (s.eflags & MFE_JUSTHITFLOOR) and not xs.FreeFlight	
					if xs.spin --Go into a roll if holding spin on land. Landing roll!
					and not (p.pflags & PF_SLIDING)
					and not xs.zipdash
					and (xs.direct > 25)
					and not xs.rollcd
					and (not (p.cmd.buttons & BT_CUSTOM1) and (XSon.QuickSpin(p,s,xs) == true))
						--
					else
						local LASTMOMZ = xs.lastmomz*ZFLIP
						
						if (LASTMOMZ < 0) and (s.eflags & MFE_ONGROUND) --Not on objects.
						and not (s.eflags & (MFE_TOUCHLAVA|MFE_GOOWATER|MFE_UNDERWATER|MFE_TOUCHWATER|MFE_SPRUNG))
						and not xs.poutysonic
						and not xs.dustcd --landing sound+dust?						
							for d = 1,8
								local ghs = P_SpawnMobjFromMobj(s, 0,0,-4<<16, MT_SPINDUST)
								ghs.state = S_SPINDUST2 ghs.tics = $/2 ghs.anim_duration = $/2
								ghs.destscale = 99<<16 ghs.fuse = 25 ghs.flags2 = $|MF2_SHADOW
								P_InstaThrust(ghs, ANGLE_45*d, 3<<16)
							end
							xs.dustcd = 3
							if (LASTMOMZ < -14*s.scale)
								if not (p.cmd.buttons & BT_TOSSFLAG)
								and not (p.cmd.buttons & BT_WEAPONPREV) and (p.cmd.buttons & BT_WEAPONNEXT)
									xs.lastmomz = 0
								end
							end
							if not xs.poutyanim
								XSon.Sound(s, sfx_xssfx7)
							end
						end
					end
				end						
				if (xs.framejump < 5)  --Regain your framejump when on the ground! (Coyote time)
					xs.framejump = 5
				end
			end
		end
		--Perform the Ring Burst?
		if p.xsringburst or ((XSon.RingBurstCharge(p, s, xs)) == true)
			XSon.RingBurst(p, s, xs) -- Prioritize over all!
		else
			if p.cc_menu and not p.cc_menu.active --Disable ChrispyChar menu when X-Sonic.
				p.cc_menu.buttontime = 500
			end
			if s.coyoteTime != nil
				s.coyoteTime = 0
			end
			if p.mrce --...ok.
				p.mrce.canhyper,p.mrce.customskin = true, 2
				p.mrce.hyperimages = (xs.FreeFlight and not (xs.zipdash and (xs.zipdash>6)))
				p.wallCling = 2 --Turn off the forced animations with mrce.customskin too, please.
			elseif (mrce_hyperunlocked!=nil)
				p.mrce_canhyper,p.mrce_hyperimages,p.mrcecustomskin = true,true,2
			end
			if p.isxmomentum  --Xmom support! As in, don't touch X-Sonic, pls!
				p.noxmomchar = 2
			end
			if p.machspeedmode
				if mapheaderinfo[gamemap].lvlttl == "Speed Highway" --Wish LucasL let me fix his version of machspeed mode
					if (p.panim != PA_PAIN) --Yo what?! Don't go backwards!
						s.momy = max($, 62*s.scale)
					end
				end
			else
				if s.currentaxis
					if (p.pflags & PF_ANALOGMODE) --Axis2D couldn't have done this itself?
						p.pflags = $ & ~PF_ANALOGMODE
						if (p.powers[pw_flashing] < 35)
							S_StartSound(nil, sfx_thok, p)
							s.z = $+30*s.scale*ZFLIP
							p.powers[pw_flashing] = max($,35)
							s.flags2 = $|MF2_DONTDRAW
							CONS_Printf(p, "\x85"+"Turned off Simple Mode for compatibility with stage!")
						end
					end	
				end				
				--Taunts! There's no transition frames, but this method can be charming too.	
				if p.panim == PA_IDLE or p.panim == PA_WALK or (s.state == S_PLAY_EDGE) or p.skidtime
					if (xs.direct < 8) and P_IsObjectOnGround(s) and (p.speed < 29*s.scale)
					and not (p.xstauntstance or xs.zipdash or xs.snapdivetrick or p.XSonicMenu) 
					and not (p.cmd.buttons & (BT_JUMP|BT_SPIN|BT_CUSTOM1))
					and not (p.pflags & (PF_SLIDING|PF_SPINNING|PF_STARTDASH|PF_THOKKED|PF_STASIS|PF_FULLSTASIS))
					and not (s.eflags & MFE_SPRUNG)
					
						if not (SUPER) and s.skin == "xsonic"
						and ( 
						(
						not (s.eflags & MFE_JUSTHITFLOOR)
						and (xs.tossflag == 1 and (p.cmd.buttons & BT_TOSSFLAG) or xs.weaponnext and xs.weaponprev and (p.cmd.buttons & BT_WEAPONPREV)
						and ((xs.weaponnext < 10) or (xs.weaponprev < 10))) )
						
						or ((s.eflags & MFE_JUSTHITFLOOR)
						and (xs.tossflag and (p.cmd.buttons & BT_TOSSFLAG) or xs.weaponnext and xs.weaponprev and (p.cmd.buttons & BT_WEAPONPREV))) 
						)
							XSon.StartTaunts(p, s, xs)
						end
					end	
				end
			end
			XSon.WallBustPower(p,s,xs) --Set X Sonic's wallbusting ability.		
			if xs.ziptrailticket --Spawn ziptrail at the end!
				XSZipTrail(s, nil, (xs.ziptrailticket>1) and 1 or nil)
				if xs.ziptrailticket == 2  xs.ziptrailticket = 0 end --
			end
		end		
	end --End of s.health check.
	xs.lastx,xs.lasty,xs.lastz = s.x,s.y,s.z --Record alot of important values for next tic!
--	xs.lastdrawangle = p.drawangle
	xs.lastangle = s.angle
	if xs.ziptrailticket
		xs.ziptrailticket = $-1
	end
end)

//X Sonic's Snap Dive! Not divesnap, snapdive! snapdive = 1
addHook("AbilitySpecial", function(p)
	if IsXSonic(p.mo) and p.xsonictable
		local s = p.mo local xs = p.xsonictable
		if xs.snapdive or (p.pflags & PF_THOKKED) return true end --Can't do it!
		XSStartSnapDive(p, s, xs) --Perform snap dive!
	end
end)

--Enter small holes when...! Hook apparently resets height to default every other frame.
addHook("PlayerHeight", function(p)
	if p.mo and IsXSonic(p.mo) and p.xsonictable
		local xs = p.xsonictable local s = p.mo
		if p.xsringburst or p.xswallrun or xs.snapdive or xs.zipdashleap
			return (32*s.scale)-2 --
		end
		if (s.state == S_PLAY_XZIPSLIDE)
		or (xs.zipdash or xs.squat) and P_IsObjectOnGround(s)
		or p.xstauntstance and p.xstauntstance.low == true
			return (32*s.scale)-2 --
		end
	end
end)

--Slide under small holes when...
addHook("PlayerCanEnterSpinGaps", function(p)
	if p.mo and IsXSonic(p.mo) and p.xsonictable
	local xs = p.xsonictable local s = p.mo
		if xs.zipdash or (s.state == S_PLAY_XZIPSLIDE)
			if P_IsObjectOnGround(s) and not (p.pflags & PF_SPINNING)
				XSZipDashCancel(p, s, xs, true)
				XSon.QuickSpin(p,s,xs, true)
				s.state = S_PLAY_ROLL
				p.pflags = $|PF_SPINNING
				s.momx,s.momy = $/3,$/3
			end
			return true --
		elseif p.xsringburst or p.xswallrun
		or xs.zipdashleap or xs.snapdive or xs.squat
		or p.xstauntstance and p.xstauntstance.low == true
			return true --
		end
	end	
end)

--Hurt enemies on touch during certain animations.
addHook("PlayerCanDamage", function(p) if not p.valid return end --
	local s = p.mo local xs = p.xsonictable
	if s and IsXSonic(s) and xs and not p.xstauntstance
		if xs.snapdivetrick == 11
			return true --Initial bounce frame has damage priority.
		elseif xs.snapdive
		or (xs.snapdivetrick > 7) and (xs.snapdivetrick < 11)
		or s.XSlunging and (xs.darkphase or xs.FreeFlight)
			return false --Never damage in this case!
		elseif s.XSassault and s.XSbag and s.XSbag.valid --On-touch damage when in Rush Assault.
			local ATTACK = s.XSassault
			local HITS = ATTACK.hits or 0
			if (HITS <= 1) and ((ATTACK.timer or 0) < 4)
				return false --
			else
				return true --
			end
		end
		if xs.zipdash and xs.darkphase
		or (xs.zipdash > 4) and not (xs.zipdashleap and (xs.zipdashleap < 9))
		or p.xswallrun and (abs(s.momz) > 9*s.scale)
		or p.powers[pw_sneakers] and (xs.ziptrailticket or (p.speed > 80*s.scale))
		or p.xsringburst
			return true --
		end
		if s.XS_vulnerabledrop and (p.pflags & PF_JUMPED)
		and not xs.supersonic and not p.powers[pw_invulnerability] 
		and ((p.panim == PA_FALL) or (p.panim == PA_SPRING))
			return false --
		end
	end
end)

addHook("JumpSpecial", function(p)
	if p.mo and IsXSonic(p.mo) and p.xsonictable
		local xs = p.xsonictable local s = p.mo
		
		if xs.snapdive or xs.snapdivetrick and (xs.snapdivetrick < 9)
		or (s.state == S_PLAY_S_XTRANSFORM) or (s.state == S_PLAY_GASP) or s.XSlunging
		or xs.ExitState or s.xsonicspawned
			return true --Do nothing.
		end
		local GROUND = (P_IsObjectOnGround(s)) and true or false
		
		--The massive fucking list of DON'T CHANGE.
		if GROUND==false
		or (xs.darkphase or p.powers[pw_super]) and not ((xs.direct<11) or (p.speed < 2*s.scale))
		or (p.pflags & (PF_JUMPED|PF_JUMPDOWN|PF_THOKKED|PF_STARTJUMP|PF_SPINNING|PF_SLIDING|PF_JUMPSTASIS|PF_FULLSTASIS))
		or (s.eflags & MFE_SPRUNG) or xs.hitspring
		or xs.zipdash and (xs.zipdash > 4)
		or p.xswallrun or p.xsringburst
		or p.panim == PA_PAIN or (p.panim >= PA_ABILITY2)
		or p.powers[pw_nocontrol] or p.exiting or p.powers[pw_carry]
		or p.powers[pw_sneakers] and (xs.ziptrailticket or (p.speed > 80*s.scale))
		or p.homing or xs.snapdivetrick
		or (s.state == S_PLAY_RIDE)
			if (abs(s.ceilingz-s.floorz) < s.height)
				return --Just jump normally.		
			elseif xs.zipdashslide and (GROUND==true)
				s.momx,s.momy = $*2/3,$*2/3
			else
				return --
			end
		end
		
		if xs.zipdash or xs.zipdashslide
			XSZipDashCancel(p, s, xs)
		end
		if s.momx or s.momy
			s.momx = FixedDiv(FixedMul($,6), 7)
			s.momy = FixedDiv(FixedMul($,6), 7)
		end
		if s.pmomz and (GROUND==true)
			s.XS_oldPMOMZ = s.pmomz or 0
			s.pmomz = 0
		else
			s.XS_oldPMOMZ = nil
		end
		xs.squat = 5
		xs.squatcharge = 0
		XSon.SwapStretch(p, s, xs)
		xs.slowsquat = ((xs.direct < 11) or (p.speed < 2*s.scale))
		
		s.spriteyoffset = 0
		s.spritexscale = FRACUNIT+1
		s.spriteyscale = FRACUNIT-1
		xs.zipstretch = 4			
		xs.framejump = 0
		
		local ghs = P_SpawnMobjFromMobj(s,0,0,0,MT_SPINDUST)
		ghs.scalespeed = FRACUNIT/6
		ghs.destscale = 99<<16
		ghs.dispoffset = 99
		ghs.flags2 = $|MF2_SHADOW
		ghs.color = (s.eflags & MFE_TOUCHWATER) and SKINCOLOR_AETHER or SKINCOLOR_BEIGE
		ghs.colorized = true
		ghs.momx,ghs.momy = s.momx/2,s.momy/2
		ghs.fuse = 33
		
		return true --
	end	
end)

--X Sonic music changes.
local function RulesOfSonic(oldname, newname)
	if splitscreen return end --
	local p = consoleplayer local xs = p and p.valid and p.xsonictable
	if not (xs and not xs.otherchar) return end
	
	if newname == "_clear" --This one always wins the priority contest.
		S_ChangeMusic("XSDONE", false, p) return true --
	elseif (oldname=="_clear" or oldname=="XSDONE") and p.exiting and (leveltime)
		return true --
	elseif p.powers[pw_super] or xs.darkphase
		if newname == "_super"
		or newname == "_inv" --Don't get interrupted by certain jingles or super music.
		or newname == "_minv"
		or newname == "_shoes"
		or newname == "_1up"
			XSon.SetMusic(p,p.mo,xs) return true --
		end
	end
end
addHook("MusicChange", RulesOfSonic)