--╔════════════════════════════╗══════════════════════════════════════════════════════════════════════════════════════════════#
--║          FUNCTIONS         ║ Older dedicated functions stored in the XSon table, or as a rawset like with SA-Sonic.
--╚════════════════════════════╝══════════════════════════════════════════════════════════════════════════════════════════════#
local XSon = XSon
local function P_MobjFlip(s) --Preventing MobjFlip crash by overriding the function entirely.
if s and s.valid and ((s.eflags&MFE_VERTICALFLIP) or (s.flags2&MF2_OBJECTFLIP)) return -1 end return 1 end

local XMUSIC = {"XSG2GF","XSHERO","XSTEN1","XSTEN2","XSTEN3","XSTEN4","XSTEN5","XSDSS","XSDARK","XSSS","XSDONE"}

--Simple function to reset pretty much ALL of X Sonic's properties.
XSon.ResetAll = function(p,s,xs)
	XSZipDashCancel(p, s, xs)
	XSSnapDiveReset(p, s, xs, true)	
	if xs.speednoise2 and xs.speednoise2.valid --XSZipDashCancel doesn't get rid of this one.
		xs.speednoise2.state = S_NULL
	end
	xs.speednoise,xs.speednoise2,xs.speednoise3 = nil
	xs.wasingoo,xs.zipspam,xs.zipclimbedwall,xs.leappenalty,xs.ExitState,xs.afterdrain,xs.waszipping = nil
	xs.snapdivewait,xs.railpose,xs.damageboost,xs.collisionjump,xs.strongspring,xs.ringburstanim,xs.didghosttrail = nil	
	xs.launchcd,xs.waitsuperHUD,xs.LastRings,xs.RingHUD,xs.superflash,xs.forceangle,xs.Ztrail,xs.zipleapbuffer,xs.delayzip = nil
	xs.edgestop,xs.BeamClashResults,xs.TScooldown = nil --Edgestop is an object, not a timer.
	xs.message, xs.ZoomLines, xs.lastsnaptarget,xs.snapOLDXY = nil --All is nil now.
	
	xs.snapdivetrick,xs.snapdivekick,xs.snapdive = 0,0,0
	xs.ziptrailticket = 0
	xs.parkourstress = 0	
	xs.sprung = 0
	xs.poutysonic = 0
	xs.poutydrop = 0
	xs.squat = 0
	xs.bonked = 0
	xs.bonkeddash = 0
	xs.bonkimmune = 0
	xs.nowalkanim = 0	
	xs.rollcd = 2
	xs.snapdive = 0
	xs.snapdivekick = 0
	xs.fallbuffer = 0
	xs.fallbuffer2 = 0
	xs.quickdrain = nil
	xs.shoesgfx = 0
	xs.paingrunt = 0
	xs.edgeanimcd = 0 
	xs.framejump = 0
	xs.ringburstcharge = 0
	xs.ringburstcd = 2
	xs.dustcd = 2
	
	xs.rolldirection = (p.drawangle > ANGLE_90) and 1 or -1
	xs.jump,xs.spin,xs.custom1,xs.custom2,xs.custom3,xs.tossflag,xs.weaponnext,xs.weaponprev = 0,0,0,0,0,0,0,0 --Controls	
	xs.uwdebuff = (s and (s.eflags & MFE_UNDERWATER)) and $ or 0
	if p.XSscalecam
		p.XSscalecam = nil
		if s and s.valid
			p.camerascale = skins[s.skin].camerascale
		else
			p.camerascale = FRACUNIT
		end
	end
	if p==consoleplayer
		local CAM = CV_FindVar("cam_speed").string
		if CAM != "0.03997"
			XS_CAMSPEED = CAM
		end
	end
	
	if s and s.valid
		if IsXSonic(s)
			p.camerascale = skins[s.skin].camerascale
			if p.xstauntstance
				XSon.SwapStretch(p, s, xs)
				s.spritexoffset,s.spriteyoffset = 0,0
				p.pflags = $ & ~(PF_STARTDASH|PF_SPINNING)
				p.xstauntstance = nil
			end
			s.eflags = $ & ~MFE_FORCESUPER
			local SKIN = skins[s.skin]
			p.jumpfactor = SKIN.jumpfactor
			p.runspeed = SKIN.runspeed
			p.normalspeed = SKIN.normalspeed
			p.acceleration = SKIN.acceleration
			p.charflags = SKIN.flags
			if (xs.HUDpref == true) 
				if xs.XHUD != true and not s.noXSHUDthisstage
					XSon.SwapHUD(p,s,xs, true, true)
				end
			else
				xs.XHUD = false
			end
			if s.noXSHUDthisstage
				xs.XHUD = false
			end	
			local SHIELD = (p.powers[pw_shield] & ~SH_FIREFLOWER) --Remove!
			if SHIELD == SH_ATTRACT or SHIELD == SH_THUNDERCOIN
				p.powers[pw_shield] = $ & ~(SH_ATTRACT|SH_THUNDERCOIN)
				P_SpawnShieldOrb(p)
			end	
			XSon.RevertXColor(p,s,xs)
		else
			if xs.darkphase
				s.eflags = $ & ~MFE_FORCESUPER
			end		
			XSon.RevertXColor(p,s,xs)	
			if (xs.XHUD == true)
				xs.XHUD = false
				if p == consoleplayer and not ((mrce_hyperunlocked!=nil) or (p.mrce and p.mrce.hud==1))
					hud.enable("lives") hud.enable("score")
					hud.enable("time") hud.enable("rings")
				end
			end
			s.XS_FORCEdark = nil
		end
		if xs.alteredrollangle or xs.rolltimer
			s.rollangle = 0
		end
		if xs.alteredscale
			s.spritexscale,s.spriteyscale = FRACUNIT,FRACUNIT
		end
		s.XS_PlanetEgg,s.XS_EmeraldScam,s.XS_tutor,s.XS_C3CONTEXT = nil
		s.XS_CheckHeight,s.XS_PreDarkColor,s.XS_DidSuperAura,s.XS_DidBlur,s.XS_modScolor,s.XS_banter,s.XS_aimtable = nil
		s.XS_watergiftcd,s.xs_TWODy,s.XS_gootime,s.XS_PlanetEgg,s.XS_C3CONTEXT,s.XS_darkness,s.airzipWR,s.XS_RingReturn = nil
		s.xdidframe,s.XS_noPvPmsg,s.XS_vulnerabledrop,s.XS_JL, s.XS_EnemyDeath,s.XStransform,s.XSecho,s.XSechotable = nil
		s.justgotPvPed,s.XS_reboundtime,s.XS_lastNC,s.XS_TutTimer,s.XSautododge,s.XS_pwringget,s.XS_darknessgoals,s.XS_lastRR = nil
		
		p.normalspeed = skins[s.skin].normalspeed
		p.runspeed = skins[s.skin].runspeed
		p.acceleration = skins[s.skin].acceleration
		if p.xstauntstance or p.xsringburst or p.xswallrun or xs.FreeFlight or xs.ChaosLambent
			XSon.CancelFlight(p,s,xs, nil,nil,nil, true)
			p.aiming = 0
			xs.FreeFlight = nil
			s.flags = $ & ~MF_NOGRAVITY
			if P_IsObjectOnGround(s)
				s.state = (p.speed>36*s.scale) and S_PLAY_RUN or (p.speed) and S_PLAY_WALK or S_PLAY_STND
			else
				s.state = (s.momz*P_MobjFlip(s) > 8*s.scale) and S_PLAY_SPRING or S_PLAY_FALL
			end
		end
	end

	xs.destroll,xs.rollangle,xs.rolltimer = 0,0,0
	p.viewrollangle = 0
	
	--Remove transformation properties.
	xs.supersonic,xs.moody,xs.ChaosLambent = nil
	xs.darkphase = 0
	
	--Delete the big player tables.
	p.xsringburst,p.xswallrun,p.xstauntstance,p.XSonicMenu = nil
	xs.alteredrollangle = (leveltime>9) and true or false
	if (gametype != GT_COOP)
		xs.PowerRing,xs.FirstPWring = nil
	end
	if p == consoleplayer and not (s and s.XS_FORCEdark)
		local MUSIC = S_MusicName()
		local DMUSIC = mapmusname
		for i = 1,#XMUSIC
			if MUSIC == XMUSIC[i]
				COM_BufInsertText(p, "tunes -default") --Restore the music!
				P_RestoreMusic(p)
				if DMUSIC == XMUSIC[i]
					if mapheaderinfo[gamemap] and mapheaderinfo[gamemap].musname
						mapmusname = mapheaderinfo[gamemap].musname
					end				
					break --
				end
			elseif DMUSIC == XMUSIC[i]
				if mapheaderinfo[gamemap] and mapheaderinfo[gamemap].musname
					mapmusname = mapheaderinfo[gamemap].musname
				end				
			end		
		end
	end
end

XSon.RaceStart = function(p,s,xs)
	if (gametyperules & GTR_RACE)
		if (leveltime>25 and leveltime < 141) and not p.xstauntstance
			if leveltime == 26 --Ready?
				s.spritexscale = FRACUNIT+8400
				s.spriteyscale = FRACUNIT+12500
				s.XSstretch = {timer = 8, Xspeed = 1000, Yspeed = 1200}								
				XSVoice(p, s, xs, sfx_xsvopo, true, p) 
			elseif leveltime == 135 --Here we go!
				XSVoice(p, s, xs, sfx_xsvopp, true, p) 
			end
			if (p.panim == PA_IDLE) --Get ready to race!
				s.tics,s.anim_duration = 33,33
				s.sprite2 = (p.powers[pw_super]) and SPR2_SKID|FF_SPR2SUPER or SPR2_MLEL
				s.frame = $ & ~FF_FRAMEMASK
			end
		end
	end		
end
--Quickly roll up into a ball, spinning into things. QuickRoll" QuickSpin" LandingRoll" Landspin"
XSon.QuickSpin = function(p,s,xs, grounded)
	if not (p.pflags & (PF_SPINNING|PF_THOKKED|PF_JUMPED|PF_SLIDING|PF_FULLSTASIS|PF_STASIS)) and not p.gotflag
	and not (s.eflags & MFE_SPRUNG)

	--	if ((p.cmd.buttons & BT_CUSTOM1) and not (p.cmd.buttons & BT_SPIN)) return false end --
			
		local SUPER = XS_Super(p)
		XSon.SwapStretch(p, s, xs)
		xs.ringburstcharge,xs.rollprep = 0,0
		
		local ANG = p.drawangle
		if ((s.flags2 & MF2_TWOD) or twodlevel)	
			xs.direct = 0
			p.cmd.sidemove,p.cmd.forwardmove = 0,0
			if (abs(s.momx) > 44*s.scale)
				ANG = R_PointToAngle2(s.x, s.y, s.x+s.momx, s.y)
			else
				ANG = s.angle
			end
			if (p.speed < 50*s.scale)
				P_Thrust(s, ANG, 3*s.scale)
			end
		end
							
		local SPEED = max(1, p.speed-(s.scale*3))
		local SLOPE = (s.standingslope and (abs(s.standingslope.zdelta) > 18000)) and 2 or 1
		
		if SLOPE==1 and (SPEED > 40*s.scale)
			SPEED = max(40*s.scale, FixedDiv(FixedMul($,100), 100+(SPEED/(9*s.scale))) )
		end
		if not xs.uwdebuff
			XSon.Sound(s, sfx_thok, 60)
			XSon.Sound(s, sfx_xssfxh, 80)
			XSon.Sound(s, sfx_xssfxi)
			SPEED = max($, min(38*s.scale, $+12*s.scale))
		else
			S_StartSound(s, sfx_xssfxi)
			S_StartSound(s, sfx_s3k7d)
			for d = 1,XSLocalRNG(1,4,s)
				local bubble = P_SpawnMobj(
				s.x-P_RandomRange(-35,35)*s.scale, 
				s.y-P_RandomRange(-35,35)*s.scale, 
				s.z+XSLocalRNG(-2+d,16+d,s)*s.scale, MT_SMALLBUBBLE)
				if bubble and bubble.valid
					bubble.scale = s.scale bubble.fuse = 99
				end	
			end
			local ADD = 3*s.scale*SLOPE
			if (p.speed > 45*s.scale) and SUPER==0
				ADD = 1*s.scale*SLOPE
			end
			SPEED = max(ADD*3, $+ADD)
		end
		if SUPER or p.powers[pw_sneakers]
			if (SPEED < (66+SUPER*10)*s.scale)
				SPEED = min(140*s.scale, $+(20*s.scale))
			end
		end
		P_InstaThrust(s, (xs.direct > 2) and xs.turnang or ANG, min(150*s.scale, SPEED))
		XSGhostTrail(p, s, xs)
		for i = 0,1,1
			XSSpawnDust(s, (p.speed)+(30*i<<16), 7)
		end
		if SUPER == 3
			xs.spinstretch = 0
			s.spritexscale,s.spriteyscale = FRACUNIT-16100,FRACUNIT+29100
			s.spriteyoffset = 5<<16
			s.XSstretch = {timer = 7, Xspeed = 3200, Yspeed = 4400, ground = true}				
			xs.windtrail = 11
			if xs.darkphase
				XSon.LightningStrike(s, SKINCOLOR_RED, s)
				XSon.Quake(s, 60<<16, 5, 500<<16)
				S_StartSoundAtVolume(s, sfx_kc4d, 199)
			else
				XS_RingBurstSparkles(s, 13, s.color)
				XSon.FlickerLight(s)
				XSon.Quake(s, 30<<16, 4, 400<<16)
				S_StartSoundAtVolume(s, sfx_kc4d, 99)
			end
			P_RadiusAttack(s, s, 63<<16*SUPER, DMG_NUKE, true)
			XSRingBurstBall(p, s, xs)
			XSon.ElecticBody(s, 8, xs.darkphase and SKINCOLOR_CRIMSON or SKINCOLOR_GOLDENROD, FF_TRANS10, xs.darkphase and AST_SUBTRACT or AST_ADD)
			for i = 1,3
				local ghs = P_SpawnGhostMobj(s)
				ghs.scale = s.scale
				ghs.renderflags = RF_FULLBRIGHT|RF_NOCOLORMAPS
				ghs.blendmode = xs.darkphase and AST_REVERSESUBTRACT or AST_ADD
				ghs.color = (SKINCOLOR_SUPERGOLD5+2)-(i*2)
				ghs.colorized = true
				ghs.spritexscale,ghs.spriteyscale = FRACUNIT+(4000*i),FRACUNIT-(2000*i)
				ghs.momx,ghs.momy = s.momx/2,s.momy/2
				ghs.tics,ghs.fuse = 99,14
				ghs.destscale = 1
				P_SetOrigin(ghs, ghs.x+(s.momx*i), ghs.y+(s.momy*i), ghs.z)
			end
		else
			xs.spinstretch = 5
			s.spritexscale,s.spriteyscale = FRACUNIT-15000,FRACUNIT+10000
		end	
		xs.spin = max($,9)
		p.pflags = $|PF_SPINNING|PF_USEDOWN & ~(PF_THOKKED|PF_JUMPED|PF_STARTJUMP|PF_NOJUMPDAMAGE|PF_STARTDASH)
		s.state = S_PLAY_ROLL return true --
	end
end

--Ignore fireflower and just give the actual shield value.
rawset(_G, "XS_SHIELD", function(p, SH1, SH2)
	local SHIELD = (p.powers[pw_shield] & ~SH_FIREFLOWER)
	if SH1
		if SHIELD == SH1 or SHIELD == SH2 --Is it either of these shields? If so, return true.
			return true --
		end return false --
	end return SHIELD --
end)

--Pouty Sonic code. Fail to run underwater, then pout. 
XSon.PoutySonic = function(p,s,xs)
	--New animation system since Blur came with new sprites
	if xs.poutyanim
		if p.xsringburst or xs.snapdive or (xs.zipdash and (xs.zipdash < 4)) or p.xstauntanim
		or (s.eflags & (MFE_SPRUNG|MFE_GOOWATER))
		or (p.pflags & (PF_SPINNING|PF_JUMPED|PF_NOJUMPDAMAGE|PF_STARTDASH|PF_SLIDING|PF_FULLSTASIS))
		or P_IsObjectOnGround(s) and (xs.direct > 3)
		or not xs.uwdebuff 
		or (XS_Super(p) != 0)
		or P_PlayerInPain(p)
		or p.powers[pw_carry]
		or (s.state == S_PLAY_GASP)
			xs.poutyanim = 0
			s.spriteyoffset = 0
			xs.poutysonic = 0
			return false --
		end
	end	
	
	--Stupid underwater zipdash fail animation		
	if xs.poutysonic
		xs.poutysonic = max($-1,0)
		XSon.AirDrag(s, 15)
		xs.ringburstcharge = 0
		xs.rollcd = 2
		
		if not (s.eflags & MFE_UNDERWATER) and (xs.poutysonic > 25)
				
			--HYPE, we actually made it!
			S_StopSoundByID(s, sfx_xssfxb)
			for i = sfx_splash,sfx_wslap --Play every water sound in celebration
				XSon.Sound(s, i)
			end
			xs.poutyanim = 0
			xs.poutysonic = 0
			XSon.PrepZipDash(p, s, xs)
			xs.zipdash = 4
			xs.zipdashminus = 40
			XSSpawnDust(s, p.speed+70*s.scale, 7)
			XSSpawnDust(s, ((p.speed or 1)/2) or (s.scale*3), 5)
			local angle = p.drawangle
			if xs.direct > 2 angle = xs.turnang end
			P_InstaThrust(s, angle, 70<<16)
			s.momz = -1
			xs.jump = 99 xs.spin = 99
			p.pflags = $|PF_JUMPDOWN|PF_USEDOWN

		elseif not xs.uwdebuff or (XS_Super(p) != 0)
		or P_PlayerInPain(p) or p.powers[pw_carry]
		or (P_IsObjectOnGround(s) and (xs.poutysonic < 76))
		or (p.pflags & (PF_SPINNING|PF_JUMPED))
		or (s.state == S_PLAY_GASP)
		
			S_StopSoundByID(s, sfx_xssfxb)
			xs.poutysonic = 0	
			xs.poutyanim = 0
			
		elseif (xs.poutysonic > 25)
		
			if (s.state != S_PLAY_RUN)
				s.state = S_PLAY_RUN
			end
			xs.ringburstcharge = 0
			s.frame = ($ & ~FF_FRAMEMASK)|(XSon.TimeFrame(D))
			
			--Bubble effects.
			local rng = P_RandomRange(-25,25)
			local rng2 = P_RandomRange(-25,25)
			local bubble = P_SpawnMobj(s.x-rng2*s.scale, s.y-rng2*s.scale, s.z, MT_SMALLBUBBLE)
			if bubble and bubble.valid
				bubble.scale = s.scale bubble.fuse = 9
				P_SetObjectMomZ(bubble, (rng or 1/13)*s.scale)
				P_InstaThrust(bubble, p.drawangle, -abs(rng or 1/8)*s.scale)
			end
			
			if (s.momz*P_MobjFlip(s) < s.scale) and (xs.poutysonic > 29)
				P_SetObjectMomZ(s, s.scale)
			elseif (s.momz*P_MobjFlip(s) > 5*s.scale)
				s.momz = FixedDiv(FixedMul($, 30), 31)
			end
			if xs.spin and not S_SoundPlaying(s, sfx_xssfxb)
				XSon.Sound(s, sfx_xssfxb)
			end
			
			--Cancel the animation?
			if (xs.jump == 1 and xs.poutysonic < 130)
			or (xs.poutysonic == 64 and not xs.spin) 
			or xs.poutysonic == 26
				xs.poutysonic = min($,26)
				s.momx,s.momy = 0,0
				xs.jump = 99 xs.spin = 99
				p.pflags = $|PF_JUMPDOWN|PF_USEDOWN
			end
			
		else
			if not (s.state == S_PLAY_FALL)
				s.state = S_PLAY_FALL
			end
			s.tics = 5 s.anim_duration = 5
			S_StopSoundByID(s, sfx_xssfxb)
			if (xs.poutysonic == 25)
				xs.poutyanim = 1
				s.sprite2 = SPR2_TIRE
				s.frame = $ & ~FF_FRAMEMASK
				s.tics = 5 s.anim_duration = 5
				s.spritexscale = FRACUNIT+7000
				s.spriteyscale = FRACUNIT+7000
				s.XSstretch = {timer = 9, Xspeed = 800, Yspeed = 800, air = true}				
			end
		end
	end
	if xs.poutyanim
		xs.ringburstcharge = 0
		xs.poutyanim = $+1
		local FRAME = A
		if (xs.poutyanim > 12)
			FRAME = max(C, (xs.poutyanim/4)-6)
		elseif (xs.poutyanim > 6)
			FRAME = max(1, (12-xs.poutyanim))/2
		else
			FRAME = max(0, (xs.poutyanim/2)-1)
		end
		xs.rollcd = 2
		s.sprite2 = SPR2_TIRE
		s.frame = ($ & ~FF_FRAMEMASK)|min(G, FRAME)
		s.tics = 5 s.anim_duration = 5
		if P_IsObjectOnGround(s) and (xs.direct <= 3)
			s.momx,s.momy = 0,0
			p.pflags = $|PF_JUMPDOWN|PF_USEDOWN
			xs.jump = 999<<16
			xs.spin = 999<<16
			xs.weaponprev = 999<<16
			xs.tossflag = 999<<16
			if (s.eflags & MFE_JUSTHITFLOOR)
				s.spritexscale = FRACUNIT+15000
				s.spriteyscale = FRACUNIT-15000
				s.XSstretch = {timer = 8, Xspeed = 2200, Yspeed = 2200}
			end
		end
	end	
end

--Function to identify a charging facestabber
XSon.Charger = function(s)
	if s and s.valid
		if (s.type == MT_FACESTABBER) and ( ((s.frame & FF_FRAMEMASK) >= H) and (s.state != S_FACESTABBER_CHARGE4) )
		or (s.type == MT_FACESTABBERSPEAR) --Apparently only exists when he's doing the charge in the first place.
			if not (s.flags & MF_NOCLIPTHING)
				return true --
			end
		end
	end
end

--To what level are we ascended? Super Sonic counts as level 3 by default.
rawset(_G, "XS_Super", function(p) if not (p and p.valid) return 0 end --
	if p.powers[pw_super]
		return 3 --
	elseif p.mysticsuper
		return 2 --
	else
		local xs = p.xsonictable
		return xs and xs.darkphase or 0 --
	end
end)

--What wall-busting powers will we set for X-Sonic?
XSon.WallBustPower = function(p,s,xs)	
	if (XS_Super(p) == 3) --ALL the power, ALL the time!
		p.powers[pw_strong] = STR_BUST|STR_ATTACK|STR_HEAVY|STR_SPIKE|STR_GUARD return true --
	else
		if p.xsringburst return true end --Ring Burst code will take over!
		p.powers[pw_strong] = 0 --Always reset every frame.
	end
	if p.xswallrun --A pretty strict one.
		p.powers[pw_strong] = $|STR_UPPER|STR_HEAVY
		if (abs(s.momz) > 11*s.scale) p.powers[pw_strong] = $|STR_SPIKE|STR_CEILING|STR_FLOOR end return true --
	end
	
	--Don't bounce off stuff like this.
	if (xs.skydive > 4) and ((s.momz*P_MobjFlip(s) < -20*s.scale)
	or (s.eflags & MFE_JUSTHITFLOOR)) and not xs.snapdive
	or s.XS_DarkTable and (s.XS_DarkTable.darkrush)
	or s.XSassault or s.XSlunging
		p.powers[pw_strong] = $|STR_HEAVY|STR_STOMP
	end
	if (s.momz*P_MobjFlip(s) > 9) and (xs.snapdivetrick or xs.zipdashleap or p.xswallrun)
		if p.xswallrun
			p.powers[pw_strong] = ($|STR_UPPER|STR_CEILING) & ~STR_WALL return true --
		else
			p.powers[pw_strong] = $|STR_UPPER|STR_WALL|STR_CEILING
		end
	end	
	--Destroy frontal walls.
	if xs.zipdash or xs.zipdashleap or (xs.leapaftermath > 20)
	or (p.speed > 60*s.scale) and (p.powers[pw_super] or p.powers[pw_sneakers] or xs.darkphase)
		p.powers[pw_strong] = $|STR_SPIKE|STR_WALL
	end
	if (s.momz*P_MobjFlip(s) > 9) and (xs.snapdivetrick or xs.zipdashleap)
		p.powers[pw_strong] = $|STR_UPPER|STR_CEILING|STR_WALL
	end
	return true --
end

--How big is the difference from one angle to another? This function returns the answer!
rawset(_G, "XS_AngleDifference", function(ANG_1, ANG_2, Rformat, maxANG, divide)
	local diff = (ANG_1-ANG_2)
	if not diff return 0,1 end --The result's 0. ...that was easy.	
	
	diff = (diff > ANGLE_180) and InvAngle(InvAngle(diff)/((divide) or 1)) or diff/((divide) or 1)	
	
	--This function NEVER gives negative results. 
	--ANGLE_270 or -ANGLE_90 for example would return as ANGLE_90, with PLUS simply becoming -1.
	local PLUS = (diff < 0) and -1 or 1
	if diff < 0 diff = InvAngle(diff) end
	
	if maxANG --Return a maximum value?
		diff = min(maxANG, diff)
	end
	if Rformat != 2
		if Rformat == 1 --Turns the result into FRACUNITs. From 0 to 180<<16
			diff = AngleFixed(diff)
		elseif Rformat == 0 --Turns result into regular numbers. From 0 to 180.
			diff = (diff >= ANG1) and diff/ANG1 or 0
		end
	end
	return diff, PLUS
end)

local XSKINS = {"xsonic","xsonicserious","xsonicoverlay"}
rawset(_G, "IsXSonic", function(s, p)  --Are we using an X Sonic skin? 
	if p --Want to use a player check instead for when mo isn't valid?
		local SKIN = skins[p.skin]
		for i = 1,3
			if SKIN == skins[XSKINS[i]] return true end
		end
	end
	if s
		for i = 1,3
			if s.skin == XSKINS[i] return true end
		end	
	end
end)

--Local RNG function that won't cause desyncing. Based on Alfredo's code.
--NEVER use this for anything that needs to be synced!
local XSrandomseed = 2736129581
local function YuLocalRandom(a, b, s)
	XSrandomseed = $*(max(gamemap+leveltime, 1)%128)
	XSrandomseed = $-(s.x+s.y+s.z)		
	XSrandomseed = ($ or 1826124580)/2
	local dumb = ((XSrandomseed*36548569) >> 4) & (FRACUNIT-1)	
	return ((dumb * (b-a+1)) >> FRACBITS) + a
end

--SMS-stolen RNG function. 
rawset(_G, "XSLocalRNG", function(a, b, s)
	local smsRNGseed = (leveltime%3==0) and 15228546 or (leveltime%2==0) and 2327239421 or 313572858
	
	smsRNGseed = $*(max(gamemap+leveltime,1) % 128) - (s.x+s.y+s.z) + (s.momx-s.frame+s.type) + FixedInt(s.angle) + (s.player and s.player.jointime*4 or 6)
	smsRNGseed = ($) and $/2 or 1626124581
	local final = ((smsRNGseed*36545569) >> 4) & (FRACUNIT-1)	
	return ((final * (b-a+1)) >> FRACBITS) + a
end)
XSon.RNG = function(a, b, s) return XSLocalRNG(a, b, s) end

--Create a quake for any nearby players that haven't opted out of the effect. DIST is optional. "YKNUX.Quake"
XSon.Quake = function(s, INTENSITY, TICS, DIST)
	if displayplayer and displayplayer.mo
		local p = displayplayer local xs = p.xsonictable
		if xs and (xs.ACCE=="both" or xs.ACCE=="noquakes") return end --
		
		if (p.mo == s)
			P_StartQuake(INTENSITY, TICS)
		elseif s and DIST != 0
			DIST = (DIST) or 850<<16
			local m = p.mo
			if ( (FixedHypot(FixedHypot(m.x-s.x, m.y-s.y), m.z-s.z)) <= DIST)	
				P_StartQuake(INTENSITY, TICS)
			end
		end
	end
end

--Create a flash for any nearby players that haven't opted out of the effect. DIST is optional. "YKNUX.Flash"
XSon.Flash = function(s, PAL, TICS, DIST)
	if displayplayer and displayplayer.mo
		local p = displayplayer local xs = p.xsonictable
		if xs and (xs.ACCE=="both" or xs.ACCE=="noflashes") return end --Player doesn't want it.
		
		if (p.mo == s)
			P_FlashPal(displayplayer, PAL, TICS)
		elseif s and DIST != 0
			DIST = (DIST) or 750<<16
			local m = p.mo
			if ( (FixedHypot(FixedHypot(m.x-s.x, m.y-s.y), m.z-s.z)) <= DIST) 
				P_FlashPal(displayplayer, PAL, TICS)
			end
		end
	end
end

--For animations that can play at max speed. Insert the last available frame, and there you go.
XSon.TimeFrame = function(no)
	for i = 1,64 --Literally cannot have more than 64 frames.
		if ((leveltime % no) == 0) break end --
		no = max($-1,0)
	end return no
end

--Remove X Sonic's stretch to swap it out for another, or just to swap in general.
XSon.SwapStretch = function(p, s, xs)
	s.spritexscale,s.spriteyscale = FRACUNIT,FRACUNIT
	xs.zipstretch,xs.zipstretch2,xs.spinstretch,xs.jumpstretch,xs.edgestretch = 0,0,0,0,0
	s.XSstretch = nil
	xs.alteredscale = true
end

--To save myself a headache of having to write this five million times.
rawset(_G, "XSAirGroundState", function(p, s, xs)
	if P_PlayerInPain(p) or p.powers[pw_carry] or (xs.bonked and xs.bonked > 2) or not s.health return end --

	if P_IsObjectOnGround(s) and not xs.FreeFlight
		s.state = ((p.speed>35*s.scale) or xs.zipdash) and S_PLAY_RUN or p.speed and S_PLAY_WALK or S_PLAY_STND
	else
		local MOMZ = s.momz*P_MobjFlip(s)
		if xs.zipdash and (MOMZ>0) and (p.pflags & PF_JUMPED) and not (s.eflags & MFE_SPRUNG)
			s.state = S_PLAY_XZIPLEAP
		else
			s.state = (MOMZ>11*s.scale) and S_PLAY_SPRING or S_PLAY_FALL
		end
	end
end)

--Determine trail color based on player color. Based on SA Sonic's YuAuraColor function
XSon.TrailColor = function(s, xs)
	local p = s.player
	if not (p and xs) return s.color end --
	
	if xs.darkphase
		return XSon.DarkAura() --
	elseif xs.supersonic or (p.powers[pw_carry]==CR_NIGHTSMODE) --Super Saiyan colors
		return (s.XS_modScolor) and s.XS_modScolor or SKINCOLOR_SUPERGOLD5 --
	else --Regular Saiyan colors
		local COLOR = s.color
		if (COLOR == SKINCOLOR_SONIKKU)
		or (COLOR >= SKINCOLOR_SKY) and (COLOR <= SKINCOLOR_COBALT)
		or p.yusonictable and (COLOR >= SKINCOLOR_SA2SONIC) and (COLOR <= SKINCOLOR_DARKNIGHT) --Adventure Sonic's colors.
			return SKINCOLOR_XSBLUETRAIL --
		elseif (COLOR >= SKINCOLOR_CARBON) and (COLOR <= SKINCOLOR_BLACK) 
		or COLOR == SKINCOLOR_GOLD or COLOR == SKINCOLOR_SHADO
			return SKINCOLOR_SUPERGOLD5 --
		end return COLOR --
	end
end

--OW, my forehead!! Why do I let you control me?!
XSon.Bonk = function(p, s, xs, ANGLE)
	local SUPER = XS_Super(p)
	
	if p.xboomtable --Delete all Sonic Booms
		for i = 5,23
			if p.xboomtable[i] 
				if p.xboomtable[i].valid --Delete all Sonic Booms
					p.xboomtable[i].state = S_NULL
				end p.xboomtable[i] = nil
			end
		end
	end
	if xs.speednoise 
		if xs.speednoise.valid
			xs.speednoise.state = S_NULL
		end xs.speednoise = nil
	end			
	if xs.speednoise2
		if xs.speednoise2.valid
			xs.speednoise2.state = S_NULL
		end xs.speednoise2 = nil
	end
	--Disable pain recovery if it's there.
	if xs.painrecovery
		xs.painrecovery = 0
		S_StopSoundByID(s, sfx_xsvoc4) S_StopSoundByID(s, sfx_xssfxj)
		if s.health and (p.powers[pw_flashing] > 90)
			p.powers[pw_flashing] = 89
		end	
	end
	s.XSassault,s.XSAssaultT = nil,nil
	if s.XS_DarkTable
		s.XS_DarkTable.darkrush = 0
	end
	XSon.StopShout(s)
	xs.leapaftermath = 0
	xs.zipdashangle = (ANGLE!=nil) and ANGLE or R_PointToAngle2(s.x, s.y, s.x+s.momx, s.y+s.momy)	
	
	local X,Y = FixedMul(-50*s.scale, cos(xs.zipdashangle)), FixedMul(-50*s.scale, sin(xs.zipdashangle))
	for i = 0,2
		local ow = P_SpawnMobjFromMobj(s, X*i, Y*i, s.height/4, MT_THOK)
		ow.scalespeed = FRACUNIT/(5-i)
		ow.destscale = 99<<16
		ow.angle = xs.zipdashangle+ANGLE_90
		ow.blendmode = AST_ADD
		ow.color = SKINCOLOR_RED+(i*4)
		ow.colorized = true
		ow.sprite = SPR_XSG1
		ow.renderflags = RF_PAPERSPRITE|RF_SEMIBRIGHT
		ow.frame = E|FF_TRANS70
		ow.fuse,ow.tics = 5+(i*3),999
		P_SetObjectMomZ(ow, ow.scalespeed*11+i, true)
	end
	
	p.pflags = $ & ~(PF_JUMPED|PF_THOKKED|PF_SPINNING)
	xs.alteredrollangle = false
	
	for i = 0,2
		S_StopSoundByID(s, sfx_xssfx1+i)
	end
	if xs.darkphase
		XSon.ElectricBody(s, 10, SKINCOLOR_CRIMSON)
		XSon.Sound(s, sfx_xssfpl) --SERIOUS sounds only!! >:(
		XSon.Quake(s, 54<<16, 9)
		P_InstaThrust(s, xs.zipdashangle, -32*s.scale)
		P_SetObjectMomZ(s, 6<<16)
		local ghs = XSon.SuperSonicWind(s, nil, 24, FF_TRANS10, (SUPER!=3) and 59500 or 76500)
		if ghs and ghs.valid
			ghs.spriteyoffset = $-2<<16
			ghs.sttspeed = 2
			P_Thrust(ghs, xs.zipdashangle, -5*s.scale)
		end
		local SFX = P_SpawnMobjFromMobj(s, 0,0,0, MT_XSZIPDASHTRAIL)
		SFX.flags = MF2_DONTDRAW SFX.tics = 65 S_StartSound(SFX, sfx_cdfm42)
		for i = -3,3,((SUPER==1) and 2 or 1)
			local ghs = XS_CraterSpew(s, xs.zipdashangle+(ANGLE_45*i), 10, true)	
			if ghs and ghs.valid
				ghs.tics,ghs.fuse = 21,20
				ghs.momx = $+s.momx
				ghs.momy = $+s.momy
			end
		end		
	else
		XSon.Sound(s, sfx_xssfxe)
		XSon.Quake(s, 24<<16, 2)
		local iv = P_RandomRange(0,1) or -1
		XSDoRoll(p, s, xs, 2*iv<<16, 60*iv<<16, 12, iv, FRACUNIT)		
		P_InstaThrust(s, xs.zipdashangle, -21*s.scale)
		P_SetObjectMomZ(s, 8<<16)
		s.state = S_PLAY_PAIN
		XSon.Sound(s, sfx_xssfx9)
	end
	p.drawangle = xs.zipdashangle
	xs.bonked = 10
end

--Play a voiceline. If they're disabled or you're underwater, these won't work!
rawset(_G, "XSVoice", function(p, s, xs, SFX, SUPER, MEONLY, VOLUME)
	if not SFX or not p return end
	local SUPERSTATE = 0
	
	if SUPER == "ForceEcho"
		SUPERSTATE = 3 --Always play an echo. Also skips underwater check.
	else
		SUPERSTATE = XS_Super(p)
		if SUPERSTATE == 3 and SUPER == nil return end --Silence is golden....badumtish.
		if (s.eflags & MFE_UNDERWATER) --Can't talk underwater. Unless you have a breathable air bubble, I guess!
		and not (p.powers[pw_shield] & SH_PROTECTWATER)
			S_StartSound(s, sfx_s3k7d)
			for d = 1,(P_RandomRange(1,4))
				local bubble = P_SpawnMobj(s.x-P_RandomRange(-35,35)*s.scale, s.y-P_RandomRange(-35,35)*s.scale, s.z+P_RandomRange(-2,20)*s.scale, MT_SMALLBUBBLE)
				if bubble and bubble.valid
					bubble.scale = s.scale
				end	
			end return --
		end
	end
	if not (consoleplayer and consoleplayer.xsonictable.voice)
		VOLUME = 2
	else
		if VOLUME == nil
			S_StartSound(s, SFX, MEONLY)
			VOLUME = 255
		else
			S_StartSoundAtVolume(s, SFX, VOLUME, MEONLY)
		end
	end
	if SUPERSTATE == 3 and SUPER != "NoEcho" --Create a Super Sonic echo.
		s.XSecho = {timer = 8, sfx = SFX, rvolume = VOLUME/2, meonly = MEONLY}
	end
end)

--X Sonic is loud AF! Feels good when you're playing him, but can get obnoxious for other players. Turn his volume down for them.
XSon.Sound = function(s, sfx, volume)
	if displayplayer
		if s.player and s.player == displayplayer
		or IsXSonic(s) --X Sonic signed up for this shit.
			if not volume
				S_StartSound(s, sfx)
			else
				S_StartSoundAtVolume(s, sfx, volume)
			end 
		else --For others, decrease volume.
			if volume
				S_StartSoundAtVolume(s, sfx, (volume*3)/4 )
			else
				S_StartSoundAtVolume(s, sfx, 180)
			end
		end
	else
		S_StartSound(s, sfx)
	end
end