addHook("AbilitySpecial", function(player)
	if (mapheaderinfo and mapheaderinfo[gamemap] and (mapheaderinfo[gamemap].typeoflevel & TOL_NIGHTS)) then return end
	-- Psycho Flight pre-emption:
	-- If BOTH BT_JUMP + BT_USE are held in mid-air, never start hoverflight.
	-- (Psycho Flight will claim the input in its own PlayerThink.)
	if player and player.mo and player.mo.valid and player.mo.skin == "ultmetalsonic" then
		local buttons = (player.cmd and player.cmd.buttons) or 0
		if ((buttons & BT_JUMP) ~= 0) and ((buttons & BT_USE) ~= 0) and not P_IsObjectOnGround(player.mo) then
			return false
		end
	end

	-- If humming top is active, don't allow hoverflight to start (prevents forward thrust + flame FX)
	if player and player.ty_hum_active then
		return false
	end
	-- Order-independent Humming Top support:
	-- If BOTH BT_TOSSFLAG + BT_JUMP are being held in mid-air, always block hoverflight.
	-- (This prevents the "partial hoverflight" case when TOSSFLAG is held before JUMP.)
	if player and player.mo and player.mo.valid and player.mo.skin == "ultmetalsonic" then
		local buttons = (player.cmd and player.cmd.buttons) or 0
		local bothheld = (((buttons & BT_TOSSFLAG) ~= 0) and ((buttons & BT_JUMP) ~= 0))
		if bothheld and not P_IsObjectOnGround(player.mo) then
			return false
		end
	end
	-- If stunt mode is armed (spring bounce), don't allow hoverflight to steal BT_JUMP.
	if player and player.SpringTricks and player.SpringTricks.ready then
		return false
	end

	-- DISABLE hoverflight activation if we're being/just been sprung (stunt mode uses springs now)
	if not (player and player.mo and player.mo.valid) then return end
	if (player.mo.eflags & MFE_SPRUNG) or player.powers[pw_justsprung] then
		return false
	end

	-- Original behavior (kept intact)
	local metalforwardforce = player.speed+35*FRACUNIT
	local metalupforce = 15*FRACUNIT
	if player.powers[pw_super] then
		metalforwardforce = $*2
		metalupforce = $*2
	end

	if player.mo.skin == "ultmetalsonic"
	and not (player.pflags & PF_THOKKED)
	or (player.powers[pw_carry] == CR_GENERIC)
	or (player.pflags & PF_STASIS)
	and not (player.pflags & PF_FULLSTASIS)
	and not (player.cmd.buttons & BT_TOSSFLAG) then

		if (player.mo.eflags & MFE_UNDERWATER) then
			P_InstaThrust(player.mo, player.mo.angle, metalforwardforce/2)
			P_SetObjectMomZ(player.mo, metalupforce/2, false)
		else
			P_InstaThrust(player.mo, player.mo.angle, metalforwardforce)
			P_SetObjectMomZ(player.mo, metalupforce, false)
		end

		S_StartSound(player.mo, sfx_buzz3)
		player.dashmode = 108
		player.mo.state = S_PLAY_DASH
		player.pflags = $|PF_THOKKED
		return true
	end
end)


//ultmetalsonic in SRB2 by Isaque, CloneFighter, RubyTheMii and Teorati
freeslot("S_PLAY_HYPERRISE", "S_PLAY_HYPERWAIT", "S_PLAY_HYPERFALL")
states[S_PLAY_HYPERRISE] = {SPR_PLAY, SPR2_SPNG|A, 2, nil, 0, 0, S_PLAY_HYPERRISE}
states[S_PLAY_HYPERWAIT] = {SPR_PLAY, SPR2_WAIT|A, 2, nil, 0, 0, S_PLAY_HYPERWAIT}
states[S_PLAY_HYPERFALL] = {SPR_PLAY, SPR2_FALL|A, 2, nil, 0, 0, S_PLAY_HYPERFALL}

//Air Burst and modified Hover.
addHook("AbilitySpecial", function(player)
	if (mapheaderinfo and mapheaderinfo[gamemap] and (mapheaderinfo[gamemap].typeoflevel & TOL_NIGHTS)) then return end
	-- Psycho Flight pre-emption:
	-- If BOTH BT_JUMP + BT_USE are held in mid-air, never start airburst/hover.
	if player and player.mo and player.mo.valid and player.mo.skin == "ultmetalsonic" then
		local buttons = (player.cmd and player.cmd.buttons) or 0
		if ((buttons & BT_JUMP) ~= 0) and ((buttons & BT_USE) ~= 0) and not P_IsObjectOnGround(player.mo) then
			return false
		end
	end

	-- If humming top is active, don't allow airburst/hover start.
	if player and player.ty_hum_active then
		return false
	end
	-- Order-independent Humming Top support:
	-- If BOTH BT_TOSSFLAG + BT_JUMP are being held in mid-air, always block airburst/hover.
	-- (This prevents the "partial hoverflight" case when TOSSFLAG is held before JUMP.)
	if player and player.mo and player.mo.valid and player.mo.skin == "ultmetalsonic" then
		local buttons = (player.cmd and player.cmd.buttons) or 0
		local bothheld = (((buttons & BT_TOSSFLAG) ~= 0) and ((buttons & BT_JUMP) ~= 0))
		if bothheld and not P_IsObjectOnGround(player.mo) then
			return false
		end
	end
	-- If stunt mode is armed (spring bounce), don't allow airburst/hover start.
	if player and player.SpringTricks and player.SpringTricks.ready then
		return false
	end

	-- DISABLE air burst/hover start if we're being/just been sprung (stunt mode uses springs now)
	if not (player and player.mo and player.mo.valid) then return end
	if (player.mo.eflags & MFE_SPRUNG) or player.powers[pw_justsprung] then
		return false
	end

	if player.mo and player.mo.skin == "ultmetalsonic" and not (player.mo.overclock or player.powers[pw_carry] == CR_GENERIC or player.pflags & PF_STASIS or player.pflags & PF_FULLSTASIS) then
		if (player.pflags & PF_JUMPED) and not (player.cmd.buttons & BT_TOSSFLAG) then
			player.pflags = $|PF_THOKKED
			P_Thrust(player.mo, player.mo.angle, 40*FRACUNIT)
			S_StartSound(player.mo, sfx_s3k45)
			S_StartSound(player.mo, sfx_zoom)
			local thok = P_SpawnMobj(player.mo.x, player.mo.y, player.mo.z, MT_THOK)
			thok.state = S_FACESTABBERSPEAR
			thok.angle = player.drawangle+ANGLE_90
			thok.scale = player.mo.scale
			thok.fuse = 5
			thok.frame = $ & ~FF_TRANSMASK | TR_TRANS30
			thok.tics = 20
			thok.blendmode = AST_ADD
			thok.destscale = $*2
			thok.color = SKINCOLOR_CRIMSON
			thok.colorized = true
		end
	end
end)

local function ultmetalsonic(mo, trailmo, freq)
	if not mo.prevz or not mo.prevleveltime or (mo.prevleveltime ~= leveltime - 1) then
		mo.prevz = mo.z
	end
	local rmomz = mo.z - mo.prevz
	mo.prevz = mo.z
	mo.prevleveltime = leveltime
	local mospeed = R_PointToDist2(0, 0, R_PointToDist2(0, 0, mo.momx, mo.momy), rmomz)
	local dist = (mospeed/freq)
	for i = dist, 1, -1 do
		local spawnedmo = P_SpawnMobjFromMobj(mo, (mo.momx/dist)*i, (mo.momy/dist)*i, (rmomz/dist)*i, trailmo)
		spawnedmo.color = SKINCOLOR_CRIMSON
		spawnedmo.blendmode = AST_ADD
		spawnedmo.frame = FF_TRANS50
		spawnedmo.destscale = 0
		spawnedmo.fuse = 4
	end
end

-- A custom color script for singleplayer XMomentum. ~RubyTheMii
addHook("PlayerThink", function(player)
	if (mapheaderinfo and mapheaderinfo[gamemap] and (mapheaderinfo[gamemap].typeoflevel & TOL_NIGHTS)) then return end
	if player.mo and player.mo.skin == "ultmetalsonic" and not (multiplayer or netgame) then
		if player.isxmomentum or player.xmomtoggledoff == false
		and not (player.powers[pw_super] or player.powers[pw_shield] == SH_FIREFLOWER) then
			player.mo.color = SKINCOLOR_CRIMSON
		end
	end
end)

-- Cancel hoverflight when sprung so it doesn't interfere with stunt mode
addHook("PlayerThink", function(player)
	if (mapheaderinfo and mapheaderinfo[gamemap] and (mapheaderinfo[gamemap].typeoflevel & TOL_NIGHTS)) then return end
	if not (player and player.mo and player.mo.valid) then return end
	if player.mo.skin ~= "ultmetalsonic" then return end

	if (player.mo.eflags & MFE_SPRUNG) or player.powers[pw_justsprung] then
		player.pflags = $ & ~PF_THOKKED
		player.dashmode = 0
	end
end)


//Had to try to recreate the vanilla hover ability for this.
//Worked out pretty well, though!
addHook("PlayerThink", function(player)
	if (mapheaderinfo and mapheaderinfo[gamemap] and (mapheaderinfo[gamemap].typeoflevel & TOL_NIGHTS)) then return end
	if player.mo and player.mo.skin == "ultmetalsonic" and not (player.mo.overclock or player.powers[pw_carry] == CR_GENERIC or player.pflags & PF_STASIS or player.pflags & PF_FULLSTASIS) then
		-- Psycho Flight pre-emption:
		-- If Psycho Flight is active (or the Psycho Flight combo is being held in mid-air),
		-- do NOT run hoverflight's hover-lock logic, but DO allow the shared hover VFX
		-- (flames/trails driven by dashmode) to still run later in this hook.
		local buttons = (player.cmd and player.cmd.buttons) or 0
		local psyactive = (player.mo.silv_psyflight)
			or ((((buttons & BT_JUMP) ~= 0) and ((buttons & BT_USE) ~= 0)) and not P_IsObjectOnGround(player.mo))
		if psyactive then
			-- Make sure existing hoverflight doesn't keep its PF_THOKKED hover state while Psy Flight takes over.
			player.pflags = $ & ~PF_THOKKED
			player.dashmode = $ or 0
		end

		-- If stunt mode is armed (spring bounce), don't run hoverflight logic.
		if player.SpringTricks and player.SpringTricks.ready then
			return
		end

		-- HARD BLOCK: don't run hoverflight logic while sprung/justsprung (stunt mode window)
		if (player.mo.eflags & MFE_SPRUNG) or player.powers[pw_justsprung] then
			return
		end

		if player.isxmomentum or player.xmomtoggledoff == false then
			if player.pflags & PF_THOKKED then
				player.spcanflip = false
			else
				player.spcanflip = true
			end
		end

		-- BOUNCE SAFETY: The ultmetalsonic bounce attack forces PF_THOKKED too.
		-- If we run hoverflight's "PF_THOKKED + jump held" hover lock during a bounce,
		-- you get stuck in a hoverflight+bounce hybrid until Jump is released.
		-- So: never apply hoverflight hovering logic while the bounce is active.
		if player.bounceattack == 1 or player.tybounce_lock == 1 then
			return
		end
		-- Only apply hoverflight's hover-lock behavior when Psycho Flight isn't taking over.
		if not psyactive then
			if not (player.exiting or P_PlayerInPain(player) or player.playerstate == PST_DEAD or player.mo.state == S_PLAY_SPRING or player.mo.state == S_PLAY_FALL) and not (player.cmd.buttons & BT_TOSSFLAG) then
				if player.pflags & PF_THOKKED and not player.hm_psyboost then
					if player.pflags & PF_JUMPDOWN then
						if (player.dashmode >= 3*TICRATE) then
							player.mo.state = S_PLAY_DASH
						else
							if player.speed >= 24*FRACUNIT then
								player.mo.state = S_PLAY_FLOAT_RUN
							else
								player.mo.state = S_PLAY_FLOAT
							end
						end
						P_SetObjectMomZ(player.mo, 0, false)
					else 
						if not (player.powers[pw_carry] or player.mo.eflags & MFE_JUSTHITFLOOR or P_IsObjectOnGround(player.mo) or player.powers[pw_justsprung] or player.mo.eflags & MFE_SPRUNG) then
							player.mo.state = S_PLAY_HYPERFALL
						end
					end
				end
			end
		end
	end

	-- HOVER STATE ENFORCER:
	-- If we're actively hovering (PF_THOKKED + JUMPDOWN) but some other move/state swap
	-- put us into a non-hover animation state (common when releasing BT_USE out of Psy Flight),
	-- force at least one valid hover state.
	if player.mo and player.mo.skin == "ultmetalsonic" then
		local buttons = (player.cmd and player.cmd.buttons) or 0
		local psyactive = (player.mo.silv_psyflight)
			or ((((buttons & BT_JUMP) ~= 0) and ((buttons & BT_USE) ~= 0)) and not P_IsObjectOnGround(player.mo))
		if not psyactive
		and not (player.exiting or P_PlayerInPain(player) or player.playerstate == PST_DEAD)
		and not (player.cmd.buttons & BT_TOSSFLAG)
		and (player.pflags & PF_THOKKED)
		and (player.pflags & PF_JUMPDOWN)
		and not player.hm_psyboost then
			local st = player.mo.state
			if st ~= S_PLAY_DASH and st ~= S_PLAY_FLOAT_RUN and st ~= S_PLAY_FLOAT then
				player.mo.state = S_PLAY_FLOAT
			end
		end
	end

	if player.mo and player.mo.skin == "ultmetalsonic" then
		if player.dashmode >= 3*TICRATE then
			local ultmetalsonic_aura = P_SpawnMobjFromMobj(player.realmo, P_RandomRange(-18, 18) * FRACUNIT, P_RandomRange(-18, 18) * FRACUNIT, P_RandomRange(0, 32) * FRACUNIT, MT_FLAMEPARTICLE)
			ultmetalsonic_aura.color = SKINCOLOR_CRIMSON
			ultmetalsonic_aura.colorized = true
			ultmetalsonic_aura.blendmode = AST_ADD
			local ultmetalsonicelectric = P_SpawnMobjFromMobj(player.realmo, P_RandomRange(-18, 18) * FRACUNIT, P_RandomRange(-18, 18) * FRACUNIT, P_RandomRange(0, 32) * FRACUNIT, MT_FLAMEPARTICLE)
			ultmetalsonicelectric.color = SKINCOLOR_CRIMSON
			ultmetalsonicelectric.colorized = true
			ultmetalsonicelectric.blendmode = AST_ADD
			ultmetalsonicelectric.state = S_WATERZAP
			ultmetalsonic(player.mo, MT_THOK, 4*FRACUNIT)
			if (leveltime % 24 == 0) then
				local thok2 = P_SpawnMobj(player.mo.x, player.mo.y, player.mo.z, MT_METALJETFUME)
				thok2.state = S_FACESTABBERSPEAR
				thok2.angle = player.drawangle+ANGLE_90
				thok2.scale = player.mo.scale/2+FRACUNIT/4
				thok2.fuse = 8
				thok2.color = SKINCOLOR_CRIMSON
				thok2.colorized = true
				thok2.blendmode = AST_ADD
			end
		end
		if player.mo.eflags & MFE_UNDERWATER then
			player.powers[pw_underwater] = -1
		end
		if player.dashmode >= 3*TICRATE and (player.pflags & PF_THOKKED and player.pflags & PF_JUMPDOWN) then
			player.dashmode = 4*TICRATE
		end
		-- Something extra for XMomentum.
		if player.isxmomentum or player.xmomtoggledoff == false then
			if player.mo.state == S_PLAY_FACEPLANT then
				if S_SoundPlaying(player.mo, sfx_kc46) then
					S_StartSound(player.mo, sfx_fizzle)
					S_StopSoundByID(player.mo, sfx_kc46)
				end
				if P_RandomChance(FRACUNIT/50) then
					S_StartSound(player.mo, sfx_s3k5c)
					player.mo.colorized = true
					player.mo.color = SKINCOLOR_CRIMSON
					local spark = P_SpawnMobjFromMobj(player.mo, P_RandomRange(-30, 30)*FRACUNIT, P_RandomRange(-30, 30)*FRACUNIT, P_RandomRange(0, 12)*FRACUNIT, MT_PARTICLE)
					spark.state = S_WATERZAP
					spark.tics = 5
				else
					player.mo.colorized = false
					player.mo.color = SKINCOLOR_CRIMSON
				end
			end
		end
	end
end)

//Metal Sonic 3.0 Wait animation by Clone Fighter (temp)
freeslot("S_PARTICLE_MS30UP", "SPR_30PT")
states[S_PARTICLE_MS30UP] = {
	sprite = SPR_WATERZAP,
	frame = FF_FULLBRIGHT|FF_ANIMATE|A,
	tics = 10,
	var1 = 3,
	var2 = 2,
	nextstate = S_NULL
}

addHook("PlayerThink", function(player)
	if (mapheaderinfo and mapheaderinfo[gamemap] and (mapheaderinfo[gamemap].typeoflevel & TOL_NIGHTS)) then return end
	if not player.mo or not player.mo.valid or player.mo.skin != "ultmetalsonic" or player.mo.overclock or player.powers[pw_carry] == CR_GENERIC then return end

	local function HM_WantsJetFume(p)
		if not (p and p.mo and p.mo.valid) then return false end
		local st = p.mo.state
			-- Note: Humming Top transitions to S_PLAY_SPRING during its hover-hold phase.
			-- So we also key off the humming-top flags set by the humming-top script.
			local hum = (p.ty_hum_active == true) or (p.ty_hum_hover == true)
			-- Stunt mode (spring tricks): keep fume active while stunt mode is armed or a trick is playing.
			local stunt = (p.SpringTricks and (p.SpringTricks.ready or p.SpringTricks.tricking))
			return hum or stunt
				or (st == S_PLAYER_GRINDING)
				or (st == S_PLAY_GSGRINDING)
				or (st == S_PLAY_GSCUSTOMGRIND)
				or (st == S_PLAY_HTOP)
				or (st == S_PLAY_MHOVER)
				or (st == S_PLAY_SUPERDASH)
	end

	-- Delay the custom wait animation so it doesn't interfere with charging (LUA_LASER) or other actions.
	-- Only begin after 80 tics of true idling.
	local WAIT_DELAY_TICS = 80
	player.hm_idlewait = $ or 0
	local charging = (player.hm_laser_held == true) or ((player.hm_laser_hold or 0) > 0)
	
	-- Only arm the wait animation after a long idle; don't instantly start just because we entered S_PLAY_WAIT.
	if not player.waiting then
		local moving = (player.cmd.sidemove ~= 0) or (player.cmd.forwardmove ~= 0)
		local busybtn = ((player.cmd.buttons & (BT_JUMP|BT_SPIN|BT_ATTACK)) ~= 0)
		if P_IsObjectOnGround(player.mo)
		and player.mo.state == S_PLAY_WAIT
		and not moving
		and not busybtn
		and not charging then
			player.hm_idlewait = $ + 1
			if player.hm_idlewait >= WAIT_DELAY_TICS then
				player.waiting = 1
				player.hm_idlewait = WAIT_DELAY_TICS
			end
		else
			player.hm_idlewait = 0
		end
	end
	
	if player.waiting then
		player.mo.jetfume = $ or P_SpawnMobjFromMobj(player.mo, -16*cos(player.drawangle), -16*sin(player.drawangle), 2*player.mo.height/5, MT_METALJETFUME)
		player.mo.jetfume.state = (player.waiting <= 2) and S_JETFUMEFLASH or S_JETFUME1
		player.mo.jetfume.frame = (player.waiting == 1) and D or (player.waiting == 2) and E
		or (player.waiting%3 == 0) and C or (player.waiting%3 == 2) and B or A
		player.mo.jetfume.scale = (player.waiting <= 2) and FRACUNIT or 2*FRACUNIT/5
		-- Keep the fume firmly attached; TeleportMove can fail if blocked.
		player.mo.jetfume.flags = $|MF_NOGRAVITY|MF_NOCLIPTHING|MF_NOCLIPHEIGHT
		player.mo.jetfume.momx,player.mo.jetfume.momy,player.mo.jetfume.momz = 0,0,0
		P_MoveOrigin(player.mo.jetfume, player.mo.x-16*cos(player.drawangle), player.mo.y-16*sin(player.drawangle), player.mo.z+2*player.mo.height/5)
		
		if player.waiting < 15 then
			if player.waiting == 1 then S_StartSound(player.mo, sfx_beflap) end
			player.drawangle = player.mo.angle+FixedAngle(ease.outsine(player.waiting*FRACUNIT/14, 0, 360*FRACUNIT))
			player.mo.flags = $|MF_NOGRAVITY
			P_SetObjectMomZ(player.mo, ease.outsine(player.waiting*FRACUNIT/14, 10*FRACUNIT, 0))
			-- Keep the player in S_PLAY_WAIT during the entire Metal Sonic 3.0 idle hover.
			-- (Other scripts key off S_PLAY_WAIT, and some state-forcing scripts can stomp custom wait states.)
			player.mo.state = S_PLAY_WAIT
			
			if leveltime&1 then
				local a = P_SpawnMobjFromMobj(player.mo, P_RandomRange(-32,32)*FRACUNIT, P_RandomRange(-32,32)*FRACUNIT, P_RandomRange(-10,56)*FRACUNIT, MT_PARTICLE)
				a.state = S_PARTICLE_MS30UP
				P_SetObjectMomZ(a, 5*FRACUNIT)
				a.color = SKINCOLOR_CRIMSON
			end
			
			if player.waiting == 14 then
				player.startz = player.mo.z
			end
		else
			-- Keep the player in S_PLAY_WAIT during the sustained hover loop as well.
			if player.mo.state != S_PLAY_WAIT then player.mo.state = S_PLAY_WAIT end
			player.mo.z = 3*sin(FixedAngle(ease.linear(player.waiting*FRACUNIT/80, 0, 360*FRACUNIT)))+player.startz
			
			local a = P_SpawnMobjFromMobj(player.mo, P_RandomRange(-16,16)*FRACUNIT, P_RandomRange(-16,16)*FRACUNIT, P_RandomRange(-5,40)*FRACUNIT, MT_PARTICLE)
			a.state = S_WATERZAP
			a.blendmode = AST_ADD
			a.color = SKINCOLOR_SKY
			a.colorized = true
		end
		player.mo.momx,player.mo.momy = 0,0
		
		player.waiting = $+1
		
		-- Cancel if the player tries to do anything (including charging the laser)
		if player.cmd.sidemove
		or player.cmd.forwardmove
		or (player.cmd.buttons & (BT_JUMP|BT_SPIN|BT_ATTACK))
		or charging then
			player.waiting = 0
			if P_IsObjectOnGround(player.mo) then
				player.mo.state = S_PLAY_WAIT
			else
				player.mo.state = S_PLAY_FALL
			end
			player.hm_idlewait = 0
			player.mo.flags = $ & ~MF_NOGRAVITY
			P_RemoveMobj(player.mo.jetfume)
			player.mo.jetfume = nil
		end
	end

	-- Jet fume while grinding / humming top / hover states.
	-- (Do not interfere with the special "wait" animation above.)
	if not player.waiting then
		local wantfume = HM_WantsJetFume(player)
		if wantfume then
			local mo = player.mo
			mo.jetfume = $ or P_SpawnMobjFromMobj(mo, -16*cos(player.drawangle), -16*sin(player.drawangle), 2*mo.height/5, MT_METALJETFUME)
			local f = mo.jetfume
			if f and f.valid then
					-- Only set state if it's not already animating as a jetfume
					-- (Some mods only define S_JETFUME1 + S_JETFUMEFLASH, so don't reference nonexistent states.)
					if f.state ~= S_JETFUME1 and f.state ~= S_JETFUMEFLASH then
					f.state = S_JETFUME1
				end
				f.fuse = 2
				f.blendmode = AST_ADD
				f.colorized = true
				f.color = SKINCOLOR_CRIMSON
				f.scale = 2*FRACUNIT/5
				f.destscale = 2*FRACUNIT/5
				-- Keep the fume firmly attached; TeleportMove can fail if blocked.
				f.flags = $|MF_NOGRAVITY|MF_NOCLIPTHING|MF_NOCLIPHEIGHT
				f.momx,f.momy,f.momz = 0,0,0
				P_MoveOrigin(f, mo.x-16*cos(player.drawangle), mo.y-16*sin(player.drawangle), mo.z+2*mo.height/5)
			end
		else
			-- If we spawned a generic jetfume and no longer need it, remove it.
			if player.mo.jetfume and player.mo.jetfume.valid then
				P_RemoveMobj(player.mo.jetfume)
			end
			player.mo.jetfume = nil
		end
	end
end)

addHook("PostThinkFrame", function()
	if (mapheaderinfo and mapheaderinfo[gamemap] and (mapheaderinfo[gamemap].typeoflevel & TOL_NIGHTS)) then return end
	for player in players.iterate
		-- Firmly attach any active jet fume AFTER player movement.
		-- PlayerThink can run before final movement is applied, which makes the fume appear to lag behind.
		if player
		and player.mo and player.mo.valid
		and player.mo.skin == "ultmetalsonic"
		and player.mo.jetfume and player.mo.jetfume.valid then
			local mo = player.mo
			local f = mo.jetfume
			-- Keep the same offset used on spawn
			local fx = mo.x - 16*cos(player.drawangle)
			local fy = mo.y - 16*sin(player.drawangle)
			local fz = mo.z + 2*mo.height/5
			f.flags = $|MF_NOGRAVITY|MF_NOCLIPTHING|MF_NOCLIPHEIGHT
			f.momx, f.momy, f.momz = 0,0,0
			P_MoveOrigin(f, fx, fy, fz)
			f.fuse = 2
		end

		if player.mo.skin == "ultmetalsonic" 
		and player.powers[pw_super] then
			local ghost = P_SpawnGhostMobj(player.mo)
			ghost.colorized = true
			ghost.fuse = 1
			P_MoveOrigin(ghost, player.mo.x, player.mo.y, player.mo.z - 4*FRACUNIT)
			ghost.frame = $|FF_ADD
			ghost.scale = 1*FRACUNIT + 1*FRACUNIT/6
			if ghost.tracer then
				ghost.tracer.fuse = ghost.fuse
			end
		end
		if player.mo.skin == "ultmetalsonic" 
		and player.powers[pw_super] then
			player.mo.color = skins[player.realmo.skin].prefcolor 
			local pablo = P_SpawnMobjFromMobj(player.realmo, P_RandomRange(-20, 20) * FRACUNIT, P_RandomRange(-20, 20) * FRACUNIT, P_RandomRange(0, 50) * FRACUNIT, MT_WATERZAP)
			pablo.color = SKINCOLOR_CRIMSON
			pablo.colorized = true
			local treil3 = P_SpawnMobjFromMobj(player.realmo, P_RandomRange(-20, 20) * FRACUNIT, P_RandomRange(-20, 20) * FRACUNIT, P_RandomRange(0, 50) * FRACUNIT, MT_FIREBALLTRAIL)
			treil3.color = SKINCOLOR_FLAME
			treil3.colorized = true
			treil3.scale = 1*FRACUNIT/4
			treil3.destscale = 1*FRACUNIT/6
			treil3.momz = 2*FRACUNIT
			treil3.frame = $|FF_FULLBRIGHT|FF_ADD|FF_TRANS30
		end
	end
end)
-- FINAL HOVER STATE GUARANTEE:
-- Some moves (notably DropDash release / certain jump transitions) can force S_PLAY_FALL
-- after the earlier hover logic runs. This late hook runs after everything in this file
-- and guarantees that if hover is ACTIVE, we are NEVER left in S_PLAY_FALL.
addHook("PlayerThink", function(player)
	if (mapheaderinfo and mapheaderinfo[gamemap] and (mapheaderinfo[gamemap].typeoflevel & TOL_NIGHTS)) then return end
	if not (player and player.mo and player.mo.valid) then return end
	if player.mo.skin ~= "ultmetalsonic" then return end
	if player.exiting or P_PlayerInPain(player) or player.playerstate == PST_DEAD then return end
	if (player.cmd and (player.cmd.buttons & BT_TOSSFLAG) ~= 0) then return end
	if player.bounceattack == 1 or player.tybounce_lock == 1 then return end

	local buttons = (player.cmd and player.cmd.buttons) or 0
	local psyactive = (player.mo.silv_psyflight)
		or ((((buttons & BT_JUMP) ~= 0) and ((buttons & BT_USE) ~= 0)) and not P_IsObjectOnGround(player.mo))
	if psyactive or player.hm_psyboost then return end

	-- Hover active = PF_THOKKED + Jump held; only care while airborne.
	if P_IsObjectOnGround(player.mo) then return end
	if not ((player.pflags & PF_THOKKED) and (player.pflags & PF_JUMPDOWN)) then return end

	-- If anything shoved us into FALL (or any non-hover state), force a valid hover anim.
	local st = player.mo.state
	if st ~= S_PLAY_DASH and st ~= S_PLAY_FLOAT_RUN and st ~= S_PLAY_FLOAT then
		if (player.dashmode or 0) >= 3*TICRATE then
			player.mo.state = S_PLAY_DASH
		else
			if player.speed >= 24*FRACUNIT then
				player.mo.state = S_PLAY_FLOAT_RUN
			else
				player.mo.state = S_PLAY_FLOAT
			end
		end
	end
end)
