sfxinfo[freeslot("sfx_wlland")].caption = "Landing"
sfxinfo[freeslot("sfx_wlappr")].caption = "/"
sfxinfo[freeslot("sfx_wlchng")].caption = "/"
sfxinfo[freeslot("sfx_wlghst")].caption = "/"

function A_SetWilloPanim(mo, var1, var2)
	local player = mo and mo.valid and mo.player
	if player then player.panim = var2 end
end
states[freeslot("S_PLAY_WILLORUN1")] = {SPR_PLAY, SPR2_FLT_, 3, A_SetWilloPanim, 0, PA_RUN, freeslot("S_PLAY_WILLORUN2")}
states[S_PLAY_WILLORUN2] = {SPR_PLAY, SPR2_CLNG, 3, A_SetWilloPanim, 0, PA_RUN, S_PLAY_RUN}
states[freeslot("S_PLAY_WILLOWALK")] = {SPR_PLAY, SPR2_CLNG, 3, A_SetWilloPanim, 0, PA_WALK, S_PLAY_FLOAT}

states[freeslot("S_PLAY_WILLOSPIN")] = {SPR_PLAY, SPR2_SPIN, 1, A_SetWilloPanim, 0, PA_ABILITY2, S_PLAY_WILLOSPIN}
states[freeslot("S_PLAY_WILLODANCE")] = {SPR_PLAY, SPR2_CLMB, 4, nil, 0, 0, S_PLAY_WILLODANCE}

// Abilites table for Kirby to eat
if not(kirbyabilitytable)
    rawset(_G, "kirbyabilitytable", {})
end
kirbyabilitytable["willo"] = -1

//All black color for the dash
skincolors[freeslot("SKINCOLOR_WILLOSHADOW")] = {
    name = "Willo_Shadow",
    ramp = {31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31},
    invcolor = SKINCOLOR_ORANGE,
    invshade = 9,
    chatcolor = V_BLUEMAP,
    accessible = false
}

local MINDASHSTART = 36 //The speed at which the increase in speed starts
local MINDASHSPEED = 12 //Minimum vertical dash speed
local DASHFACTOR = 8 //The rate at which the speed transfers
local DASHTIME = TICRATE*2/3 //Amount of time the dash lasts for
local STARTUPTIME = 7 //How long it takes to start the dash
local MAXDASHES = 2 //How many times Willo can dash
local GHOSTSPRITE2S = {
	[SPR2_RUN_] = true,
	[SPR2_CLNG] = true,
	[SPR2_FLT_] = true,
	[SPR2_FRUN] = true,
	[SPR2_NFLT] = true,
	[SPR2_NFLY] = true,
	[SPR2_NDRL] = true,
}

//Nebby stuff
local NEBBYSTATES = {
	[S_PLAY_WAIT] = 1,
	[S_PLAY_SPRING] = 2,
}
local function InNebbyState(mo)
	local player = mo and mo.valid and mo.health and mo.player
	if not player or player.willotimeasghost or player.willodashtime then return false end
	return player.exiting //or player.willonebbyspin
	or (player.glidetime and player.glidetime & 1)
	or (NEBBYSTATES[mo.state] and mo.state != S_PLAY_SPRING)
end
local NEBBYREVERTTIME = TICRATE/3
mobjinfo[freeslot("MT_WILLO_NEBBY")] = {
	spawnstate = S_INVISIBLE,
	spawnhealth = 1000,
	radius = 16*FRACUNIT,
	height = 16*FRACUNIT,
	flags = MF_SCENERY|MF_NOGRAVITY|MF_NOBLOCKMAP|MF_NOCLIPHEIGHT
}
addHook("MobjMoveCollide", function(mo, target)
	if mo and mo.valid and mo.health and target and target.valid
	and target.flags & MF_SHOOTABLE and target.health and not target.player
	and target.z - target.scale <= mo.z + mo.height
	and target.z + target.height + target.scale >= mo.z
		P_DamageMobj(target, mo, mo.target)
		if not target.health and target.flags2 & MF2_FRET
			target.flags2 = $ & ~MF2_FRET
		end
	end
end, MT_WILLO_NEBBY)
addHook("MobjThinker", function(mobj)
	local mo = mobj.target
	if not mo then return end
	local player = mo.valid and mo.player
	if not player then P_RemoveMobj(mobj) return end
	
	//Set spawn variables
	if not mobj.threshold
		P_SetScale(mobj, mobj.scale/16)
		mobj.shadowscale = FRACUNIT/2
		mobj.extravalue1 = player.drawangle
		mobj.movedir = player.drawangle
		
		mobj.skin = "willo"
		mobj.threshold = leveltime & 1 and 1 or -1
	end
	player.willonebby = mobj
	mobj.reactiontime = $ + 1
	
	//State
	local speed = ANG2*2 * mobj.threshold
	if NEBBYSTATES[mo.state] == 2 or player.exiting
		if mobj.state != S_TAILSOVERLAY_FLY
			mobj.state = S_TAILSOVERLAY_FLY
			if mobj.threshold < 0 then mobj.frame = ($ &~ FF_FRAMEMASK) + E end
		end
	elseif mobj.state != S_TAILSOVERLAY_TIRE
		mobj.state = S_TAILSOVERLAY_TIRE
		mobj.tics = -1
	end
	if NEBBYSTATES[mo.state] == 3 or player.willonebbyspin then speed = $*6 end
	
	//Movement
	if InNebbyState(mo) or mobj.reactiontime <= TICRATE/2
		if mobj.fuse then mobj.fuse = 0 end
		
		A_OrbitNights(mobj, speed, (player.drawangle/ANG1)<<27)
		if mobj.threshold < 0 then mobj.angle = $ - ANGLE_180 end
		
		mobj.destscale = mo.destscale
		mobj.scalespeed = mo.scalespeed
		
		local fh = FixedMul(cos(mobj.extravalue1 - player.drawangle),20*mobj.scale)
		mobj.z = mo.z + fh + FixedMul(16*FRACUNIT, mobj.scale)
		mobj.z = $ - FixedMul(mo.height/2, mobj.scale - mo.scale)
	else
		if not mobj.fuse then mobj.fuse = NEBBYREVERTTIME end
		mobj.destscale = mo.destscale/16
		
		P_TeleportMove(mobj, mobj.x - (mobj.x - mo.x)/mobj.fuse,
		mobj.y - (mobj.y - mo.y)/mobj.fuse,
		mobj.z - ((mobj.z + mobj.height/2) - (mo.z + mo.height/2))/mobj.fuse)
		
		A_FaceTarget(mobj)
	end
	
	//Disappear in first person
	if not splitscreen and player == displayplayer
	and CV_FindVar("chasecam") and not CV_FindVar("chasecam").value
		mobj.flags2 = $ | MF2_DONTDRAW
		mobj.nebbydisapear = true
	elseif mobj.nebbydisapear
		mobj.flags2 = $ &~ MF2_DONTDRAW
	end
	
	//Ghost
	/*if not (mobj.reactiontime % 5)
		local ghost = P_SpawnGhostMobj(mobj)
		ghost.momx = mo.momx/2
		ghost.momy = mo.momy/2
		ghost.momz = mo.momz/2
	end*/
end, MT_WILLO_NEBBY)

//Dash sounds
for i = 1, MAXDASHES
	local sound = freeslot("sfx_wldsh"+i)
	sfxinfo[sound].caption = "Dashing"
end
local function stopdashsound(mo)
	for i = 1, MAXDASHES
		local sound = _G["sfx_wldsh"+i]
		if sound and S_SoundPlaying(mo, sound)
			S_StopSoundByID(mo, sound)
		end
	end
end

//Code for Willo appearing/disappearing
local function willoappear(player, mo, disappear)
	if not disappear
		player.willodashtime = 0
		player.camerascale = skins[mo.skin].camerascale
		stopdashsound(mo)
	end
	S_StartSound(mo, sfx_wlappr)
	player.willochangetimer = -1
	
	local thok = P_SpawnMobjFromMobj(mo,0,0,-player.height/2*P_MobjFlip(mo), MT_THOK)
	thok.color = SKINCOLOR_WILLOSHADOW
	if AST_MODULATE
		thok.type = MT_WILLOGHOST
		thok.blendmode = AST_MODULATE
	end
	thok.scale = $*2
	thok.height = P_MobjFlip(mo) == 1 and $/2 or $
	thok.destscale = thok.scale*16
	thok.fuse = thok.tics
	thok.momx = mo.momx
	thok.momy = mo.momy
	thok.momz = mo.momz
end

//Spawns objects in a circle
local function willocircle(mobj, mobjtype, amount, ns, zoffset, scale, keepmom, color, vertical, vertangle)
	local i = 0	while i < amount
		local fa = i*(ANGLE_180/(amount/2))
		local mo = P_SpawnMobjFromMobj(mobj,0,0,zoffset,mobjtype)
		if mo and mo.valid
			local height = mo.height
			mo.scale = scale
			mo.target = mobj
			if mobj.eflags&MFE_VERTICALFLIP
				mo.z = $-(mo.height-height)
			end
			if mobjtype == MT_THOK
				mo.fuse = mo.tics
			end
			if mobjtype == MT_MINECARTSPARK
				mo.fuse = TICRATE
			end
			if color
				mo.color = color
				mo.colorized = true
			end
			if mo.type == MT_THOK
				mo.colorized = false
				mo.fuse = mo.tics
			end
			if vertical
				mo.momz = FixedMul(sin(fa),ns)
				P_InstaThrust(mo, vertangle+ANGLE_90,
				FixedMul(cos(fa),ns))
			else
				mo.momx = FixedMul(sin(fa),ns)
				mo.momy = FixedMul(cos(fa),ns)
			end
			if keepmom
				mo.momx = $+mobj.momx
				mo.momy = $+mobj.momy
				mo.momz = $+mobj.momz
			end
		end
		i = $+1
	end
end

//Reset Willo's variables
local function resetwillo(player)
	player.willodashspeed = 0
	player.willowasindash = false
	player.willochangetimer = 0
	player.willochangesprite2 = 0
	player.willochangeframe = 0
	player.willodance = false
	player.willospindash = 0
	if player.willodashtime
		if player.realmo and player.realmo.valid
			player.camerascale = skins[player.realmo.skin].camerascale
		end
	end
	player.willodashtime = 0
end

//Reset the variables
addHook("PlayerSpawn", resetwillo)
for player in players.iterate resetwillo(player) end

//Start the Dash
addHook("AbilitySpecial", function(player)
	local mo = player.mo
	if not (mo and mo.valid and mo.skin == "willo"
	and (player.glidetime < MAXDASHES or player.powers[pw_super]))
	or player.pflags & PF_SHIELDABILITY or player.secondjump then return end
	
	S_StartSound(mo, sfx_wldsh1 + (player.glidetime % MAXDASHES))
	mo.state = S_PLAY_FLOAT
	player.glidetime = $ + 1
	
	willoappear(player, mo, true)
	if player.willodashtime <= DASHTIME
		player.willodashspeed = R_PointToDist2(0,0,mo.momx,mo.momy)
	end
	
	player.willodashtime = DASHTIME + STARTUPTIME
	player.willowasindash = true
	player.pflags = $|PF_THOKKED|PF_NOJUMPDAMAGE&~PF_SPINNING
	
	if abs(player.cmd.angleturn<<16 - mo.angle) > ANGLE_90
		player.camerascale = skins["willo"].camerascale*7/6
	else
		player.camerascale = skins["willo"].camerascale*6/7
	end
end)

//Don't get damaged during the dash
addHook("ShouldDamage", function(target, inf, src, dmg, dmgtype)
	if target and target.valid and target.player and target.skin == "willo"
	and target.player.willodashtime and target.player.willodashtime <= DASHTIME
	and (dmgtype == DMG_CRUSHED or not (dmgtype&DMG_DEATHMASK))
	and target.player.powers[pw_carry] != CR_NIGHTSMODE
		return false
	end
end, MT_PLAYER)

//For the Spooky Dance
addHook("ShouldJingleContinue", function(player, musname)
	if true return end
	if musname != "WILLODANCE" return end
	if player and player.mo and player.mo.skin == "willo" and player.willodance
		return true
	else
		return false
	end
end)

//Ghost form
addHook("PostThinkFrame", do for player in players.iterate
	if not (player.mo and player.mo.valid and player.mo.skin == "willo") continue end
	local mo = player.mo
	
	//Ghost walk animation
	if player.willotimeasghost and mo.state == S_PLAY_WALK
		if player.willosprite2 != SPR2_FLT_ and player.willosprite2 != SPR2_CLNG
			mo.state = S_PLAY_WILLOWALK
		else
			mo.state = S_PLAY_FLOAT
		end
		if player.followmobj and player.followmobj.valid
		and player.followmobj.type == MT_WILLOTAIL
			player.followmobj.state = S_WILLOTAIL_INVIS
		end
	end
	
	//Changing to/from ghost form
	local sprite2 = mo.sprite2&~FF_SPR2SUPER&~FF_SPR2ENDSTATE&~FF_SPR2MIDSTART
	if player.willosprite2 != sprite2
		if GHOSTSPRITE2S[player.willosprite2] == GHOSTSPRITE2S[sprite2]
			//sorry nothing
		elseif player.willochangetimer != -1
			player.willochangetimer = 4
			S_StartSoundAtVolume(mo, sfx_wlchng, 128)
		end
		player.willosprite2 = sprite2
	end
	
	//Effects/sounds while ghost
	if player.willotimeasghost == nil
		player.willotimeasghost = 0
	elseif GHOSTSPRITE2S[sprite2] or player.willodashtime
		if AST_SUBTRACT
			mo.blendmode = AST_SUBTRACT
		end
		
		if player.willodashtime
			S_StopSoundByID(mo, sfx_wlghst)
			mo.spriteyoffset = 0
			player.willotimeasghost = 201
		else
			if mo.state == S_PLAY_RUN and player.willochangesprite2 != SPR2_RUN_
			and player.willochangesprite2 != SPR2_CLNG
			and player.speed >= FixedMul(player.runspeed, mo.scale)
				mo.state = S_PLAY_WILLORUN1
			end
			
			if not player.willotimeasghost or not (player.willotimeasghost%201)
				S_StartSound(mo, sfx_wlghst)
			end
			
			mo.spriteyoffset = 2 * sin(FixedAngle(player.willotimeasghost*16*FRACUNIT))
			
			for i = 1, 4
				local thok = P_SpawnMobjFromMobj(player.mo,32*(P_RandomFixed()-(FRACUNIT/2)),
				32*(P_RandomFixed()-(FRACUNIT/2)),
				(32*(P_RandomFixed()-(FRACUNIT/2)))+(player.height/2*P_MobjFlip(mo)),MT_THOK)
				thok.color = mo.color
				if AST_SUBTRACT
					thok.blendmode = AST_SUBTRACT
				end
				thok.skin = mo.skin
				thok.sprite = SPR_PLAY
				thok.sprite2 = SPR2_TALA
				thok.angle = player.drawangle
				thok.destscale = thok.scale
				thok.scale = $/4
				thok.destscale = 1
				thok.scalespeed = thok.scale/8
				thok.tics = $-P_RandomKey(4)
				thok.fuse = 0
				thok.frame = 0
				thok.momx = mo.momx*2/3-mo.momx
				thok.momy = mo.momy*2/3-mo.momy
				thok.momz = mo.momz + mo.scale*4*P_MobjFlip(mo)
				thok.z = $ - mo.momz
				if player.powers[pw_carry] == CR_NIGHTSMODE
					local thrust = 16
					local angle = player.flyangle*ANG1
					
					if sprite2 != SPR2_NFLT
						thok.spriteyoffset = player.height
						if player.pflags & PF_STARTDASH
							thrust = $/2
						end
					else
						thrust = $/2
					end
					
					P_Thrust(thok, player.drawangle, -FixedMul(abs(cos(angle)), thrust*mo.scale))
					P_SetObjectMomZ(thok, -thrust*2*sin(angle), false)
				end
				thok.z = $-mo.scale*4*P_MobjFlip(mo)
				P_XYMovement(thok)
				P_ZMovement(thok)
				P_SetScale(thok, thok.scale-thok.scalespeed)
				thok.momx = $+mo.momx
				thok.momy = $+mo.momy
			end
			
			player.willotimeasghost = $+1
		end
	elseif player.willotimeasghost
		S_StopSoundByID(mo, sfx_wlghst)
		mo.spriteyoffset = 0
		if AST_SUBTRACT
			mo.blendmode = AST_TRANSLUCENT
		end
		player.willotimeasghost = 0
	end
	
	//Spawn afterimages for changing
	if player.willochangetimer == -1 or player.willodashtime
		player.willochangesprite2 = mo.sprite2
		player.willochangeframe = mo.frame
		if not player.willodashtime
			player.willochangetimer = 0
		end
	elseif player.willochangetimer
		local ghost = P_SpawnGhostMobj(player.mo)
		ghost.momx = mo.momx/2
		ghost.momy = mo.momy/2
		ghost.momz = mo.momz/2
		ghost.flags = $&~MF_NOCLIP&~MF_NOCLIPHEIGHT
		ghost.color = SKINCOLOR_WILLOSHADOW
		ghost.colorized = true
		ghost.sprite2 = player.willochangesprite2
		ghost.frame = player.willochangeframe
		ghost.fuse = ghost.info.damage*player.willochangetimer/4
		if AST_MODULATE
			ghost.type = MT_WILLOGHOST
			ghost.blendmode = AST_MODULATE
		end
		
		player.willochangetimer = $-1
	else
		player.willochangesprite2 = mo.sprite2
		player.willochangeframe = mo.frame
	end
end end)

//Make running use your spin height
addHook("PlayerHeight", function(player)
	local mo = player.mo
	if mo and mo.valid and mo.skin == "willo"
	and mo.sprite2&~FF_SPR2SUPER&~FF_SPR2ENDSTATE&~FF_SPR2MIDSTART == SPR2_RUN_
		return P_GetPlayerSpinHeight(player)
	end
end)
addHook("PlayerCanEnterSpinGaps", function(player)
	local mo = player.mo
	if mo and mo.valid and mo.skin == "willo"
	and mo.sprite2&~FF_SPR2SUPER&~FF_SPR2ENDSTATE&~FF_SPR2MIDSTART == SPR2_RUN_
		return true
	end
end)

//Main character thinker
addHook("PlayerThink", function(player)
	if not (player.mo and player.mo.valid and player.mo.skin == "willo")
		if player.willodance
			if player.realmo and player.realmo.valid
			and player.realmo.state == S_PLAY_WILLODANCE
				if P_IsObjectOnGround(player.realmo)
					player.realmo.state = S_PLAY_STND
				else
					player.realmo.state = S_PLAY_FALL
				end
			end
			player.willodance = false
			P_RestoreMusic(player)
		end
		
		resetwillo(player)
		return
	end
	local mo = player.mo
	
	//Spawn Nebby
	if not (player.willonebby and player.willonebby.valid
	and player.willonebby.type == MT_WILLO_NEBBY)
		player.willonebby = nil
		if InNebbyState(mo)
			player.willonebby = P_SpawnMobjFromMobj(mo, 0,0, player.height/2, MT_WILLO_NEBBY)
			player.willonebby.target = mo
		end
	end
	
	//Nebby spin
	//if player.willospindash
	//	player.willonebbyspin = true
	//elseif player.willonebbyspin and not (player.pflags & PF_SPINNING)
	//	player.willonebbyspin = false
	//end
	
	//Detect when we press Custom 3
	if not (player.cmd.buttons & BT_CUSTOM3)
		player.willocustomtapready = true
		player.willocustomtapping = false
	elseif player.willocustomtapready 
	and not (player.hugging) then
		player.willocustomtapping = true
		player.willocustomtapready = false
	else
		player.willocustomtapping = false
	end
	
	//Disallows the rest of the code in certain circumstances
	if P_PlayerInPain(player)
	or (player.pflags & PF_SLIDING)
	or player.powers[pw_carry]
	or player.spectator
	or player.playerstate
	or not mo.health
	or (player.panim == PA_PAIN and not (player.pflags&PF_JUMPED))
	or player.powers[pw_super] == 1
	or player.iseggrobo
	or player.isjettysyn
	or player.guard
	or player.powers[pw_nocontrol]
	or (player.isxmomentum and mo.state == S_PLAY_FACEPLANT)
		resetwillo(player)
		return
	end
	
	if player.willodance
	and not (player.hugging)
		player.panim = PA_DASH
		if not P_IsObjectOnGround(mo) or (player.pflags&PF_SPINNING)
		or player.willocustomtapping
			if mo.state == S_PLAY_WILLODANCE
				if P_IsObjectOnGround(mo)
					mo.state = S_PLAY_STND
				else
					mo.state = S_PLAY_FALL
				end
			end
			player.willodance = false
			P_RestoreMusic(player)
		else
			if mo.state != S_PLAY_WILLODANCE
				mo.state = S_PLAY_WILLODANCE
			end
			player.pflags = $|PF_FULLSTASIS
		end
	elseif P_IsObjectOnGround(mo) and not (player.pflags&PF_SPINNING)
	and player.willocustomtapping
		player.willodance = true
		P_PlayJingleMusic(player, "WILLODANCE", nil, true, JT_OTHER)
	end
	
	//Rolling spindash
	if player.willospindash
		if mo.state != S_PLAY_WILLOSPIN
			player.willospindash = 0
			if mo.state == S_PLAY_STND and player.cmd.buttons & BT_SPIN
			and not player.exiting
				mo.state = S_PLAY_WILLOSPIN
				player.pflags = $|PF_SPINDOWN|PF_SPINNING|PF_STARTDASH
				player.dashspeed = player.willodashspeed
			end
		elseif player.cmd.buttons & BT_SPIN and not player.exiting
			local chargecalculation = (6*(player.willodashspeed  - (player.mindash)))/((player.maxdash) - (player.mindash))
			player.willodashspeed = min(max($+FRACUNIT, player.mindash), player.maxdash)
			local soundcalculation = (6*(player.willodashspeed  - (player.mindash)))/((player.maxdash) - (player.mindash))
			if not player.spectator and soundcalculation != chargecalculation
				S_StartSound(mo, sfx_spndsh)
			end
		
			local i = 0 while i <= (leveltime%7)/2 // 1, 2, 3 or 4 particles
				local particle
				if mo.eflags & (MFE_TOUCHWATER|MFE_UNDERWATER) // overrides fire version
					particle = P_SpawnMobj(mo.x, mo.y, mo.z, MT_SMALLBUBBLE)
				else
					particle = P_SpawnMobj(mo.x, mo.y, mo.z, MT_SPINDUST)
					if (player.powers[pw_shield] & SH_NOSTACK) == SH_ELEMENTAL
						particle.state = S_SPINDUST_FIRE1
					end
				end
			
				particle.target = mo
				particle.destscale = (2*mo.scale)/3
				P_SetScale(particle, particle.destscale)
				if (mo.eflags & MFE_VERTICALFLIP) // readjust z position if needed
					particle.z = mo.z + mo.height - particle.height
				end
				local prandom = {}
				prandom[0] = P_RandomFixed()<<2 // P_RandomByte()<<10
				prandom[1] = P_RandomRange(-30, 30) // P_RandomRange(-ANG30/FRACUNIT, ANG30/FRACUNIT)*FRACUNIT
				prandom[2] = P_RandomFixed()<<3 // P_RandomByte()<<11
				P_SetObjectMomZ(particle, player.willodashspeed /50 + prandom[0], false)
				P_InstaThrust(particle,
						player.drawangle + (prandom[1]*ANG1),
						-FixedMul(player.willodashspeed /12 + FRACUNIT + prandom[2], mo.scale))
				particle.momx = $+mo.momx
				particle.momy = $+mo.momy
				particle.momz = $+mo.momz
				P_TryMove(particle, particle.x+particle.momx, particle.y+particle.momy, true)
			
				i = $+1
			end
		else
			P_InstaThrust(mo, mo.angle, FixedMul(player.willodashspeed, mo.scale))
			S_StartSound(mo, sfx_zoom)
			
			if not P_IsObjectOnGround(mo) and mo.momz * P_MobjFlip(mo) < 0
				mo.momz = 0
			end
			
			player.pflags = $|PF_SPINNING&~PF_THOKKED&~PF_JUMPED
			player.willospindash = 0
			player.mo.state = S_PLAY_ROLL
		end
		player.drawangle = mo.angle
	end
	
	//Perform the dash
	if player.willodashtime
		player.willodashtime = $ - 1
		player.willowasindash = true
		
		//Camera
		local camscale = skins["willo"].camerascale
		if player.camerascale > camscale
			player.camerascale = max($*19/20, camscale)
		elseif player.camerascale < camscale
			player.camerascale = min($*21/20, camscale)
		end
		
		//State
		if player.willodashtime >= DASHTIME + STARTUPTIME/2
			if mo.state != S_PLAY_FLOAT
				mo.state = S_PLAY_FLOAT
			end
		elseif player.speed >= FixedMul(player.runspeed, mo.scale)
		and player.willodashtime <= DASHTIME
			if mo.state != S_PLAY_FLOAT_RUN
				mo.state = S_PLAY_FLOAT_RUN
			end
		elseif mo.state != S_PLAY_CLING
			mo.state = S_PLAY_CLING
		end
		
		//Spring cancel
		if mo.eflags & MFE_SPRUNG
			player.willodashtime = 0
			willoappear(player, mo)
			mo.state = S_PLAY_SPRING
			
		//Hit ground or wall
		elseif P_IsObjectOnGround(mo) or mo.eflags&MFE_JUSTHITFLOOR
		or (player.powers[pw_pushing] and player.willodashtime < DASHTIME
		and not (player.glidetime & 1) and not P_IsObjectInGoop(mo))
			willoappear(player, mo)
			
			S_StartSound(mo, sfx_wlland)
			stopdashsound(mo)
			
			if player == displayplayer
				P_StartQuake(5*FRACUNIT, 7)
			end
			
			willocircle(mo, MT_DUST, 16, 8*mo.scale, 0, mo.scale, false, nil, player.powers[pw_pushing], player.drawangle)
			
			if P_IsObjectOnGround(mo) or mo.eflags&MFE_JUSTHITFLOOR
				local speed = R_PointToDist2(0,0,mo.momx,mo.momy)
				if speed < 40*mo.scale
					P_InstaThrust(mo, R_PointToAngle2(0,0,mo.momx,mo.momy), min(speed*3/2, 40*mo.scale))
				end
			else
				player.pflags = $ &~ PF_JUMPED
				P_DoJump(player, false)
				player.pflags = $ &~ PF_STARTJUMP
			end
			
			if (player.cmd.buttons & BT_SPIN or player.pflags & PF_SPINNING)
			and not player.exiting
				S_StopSoundByID(mo, sfx_spin)
				
				mo.state = S_PLAY_WILLOSPIN
				S_StartSound(mo, sfx_cdfm26)
				
				if FixedHypot(mo.momx, mo.momy) <= 5*mo.scale
				and P_IsObjectOnGround(mo)
					player.pflags = $|PF_SPINDOWN|PF_SPINNING|PF_STARTDASH
					mo.momx = player.cmomx
					mo.momy = player.cmomy
					
					//player.willonebbyspin = true
					player.dashspeed = player.mindash+(player.maxdash-player.mindash)/3
				else
					player.pflags = $|PF_SPINNING
					if not P_IsObjectOnGround(mo)
						player.pflags = $|PF_THOKKED
					end
					
					player.willospindash = 1
					player.willodashspeed = player.mindash+(player.maxdash-player.mindash)/3
				end
			elseif player.pflags & PF_JUMPED
				player.pflags = $ &~ PF_NOJUMPDAMAGE
				mo.state = S_PLAY_JUMP
			elseif mo.state != S_PLAY_FLOAT_RUN
				mo.state = S_PLAY_WALK
			end
			
		//Fan cancel
		elseif not player.glidetime
			player.willodashtime = 0
			willoappear(player, mo)
			mo.state = S_PLAY_SPRING
			
		//Prep
		elseif player.willodashtime > DASHTIME
			//mo.momx = $*(STARTUPTIME-1)/STARTUPTIME
			//mo.momy = $*(STARTUPTIME-1)/STARTUPTIME
			//mo.momz = $*(STARTUPTIME-1)/STARTUPTIME
			
		//Main
		else
			mo.flags2 = $|MF2_DONTDRAW
			
			//Boost forward
			if player.willodashtime == DASHTIME
				//willoappear(player, mo, true)
				
				if abs(player.cmd.forwardmove) + abs(player.cmd.sidemove) > 40
					local actionspd = FixedMul(player.actionspd, mo.scale)
					if mo.eflags&MFE_UNDERWATER
						actionspd = $*2/3
					end
					player.willodashspeed = max($, actionspd)
				end
				P_InstaThrust(mo, mo.angle, player.willodashspeed)
				
				local one = 3
				local two = 4
				if player.willodashspeed < 40*mo.scale
					one = 6
					two = 7
				end
				
				if abs(player.cmd.angleturn<<16 - mo.angle) > ANGLE_90
					player.camerascale = skins["willo"].camerascale*one/two
				else
					player.camerascale = skins["willo"].camerascale*two/one
				end
				
				//Go up instead of down
				//if player.cmd.buttons & BT_JUMP and not (player.cmd.buttons & BT_SPIN)
				//and player.glidetime & 1 and player.glidetime + 1 < MAXDASHES
				//	player.glidetime = $ + 1
				//end
				
				//Going up reduces the dash time
				if player.glidetime & 1
					player.willodashtime = $/2
				end
			end
			
			//Afterimages
			local dumb = leveltime%2 and -1 or 1
			local ghost = P_SpawnGhostMobj(mo)
			ghost.momx = mo.momx/2
			ghost.momy = mo.momy/2
			ghost.momz = mo.momz/2
			P_Thrust(ghost, player.drawangle+ANGLE_90, P_RandomRange(1,8)*mo.scale*dumb)
			ghost.flags = $&~MF_NOCLIP&~MF_NOCLIPHEIGHT
			ghost.color = SKINCOLOR_WILLOSHADOW
			ghost.colorized = true
			if AST_MODULATE
				ghost.type = MT_WILLOGHOST
				ghost.blendmode = AST_MODULATE
			end
			
			//Go up if you're holding jump, down if not
			//The second one always goes down
			local factor = max(player.speed - MINDASHSTART*mo.scale, 0)/DASHFACTOR + MINDASHSPEED*mo.scale
			//if mo.eflags&MFE_UNDERWATER
			//	factor = $*2/3
			//end
			factor = $*P_MobjFlip(mo)
			if player.glidetime & 1
				mo.momz = factor
			else
				mo.momz = -factor*2
			end
			
			//We're done dashing
			if not player.willodashtime
				mo.state = S_PLAY_SPRING
				
				player.pflags = $ | PF_JUMPED | PF_NOJUMPDAMAGE | PF_STARTJUMP
				willoappear(player, mo)
			end
		end
		
		//No gravity
		if not P_IsObjectOnGround(mo) and not (mo.eflags&MFE_JUSTHITFLOOR)
		and not (not (player.glidetime & 1) and P_IsObjectInGoop(mo))
			mo.momz = $-P_GetMobjGravity(mo)
		end
	else
		stopdashsound(mo)
		
		if (P_IsObjectOnGround(mo) or mo.eflags&MFE_JUSTHITFLOOR)
		and player.willowasindash
			if player.cmd.buttons&BT_SPIN and not (player.pflags&PF_SPINNING)
				if FixedHypot(mo.momx, mo.momy) > 5*mo.scale
					player.pflags = $|PF_SPINNING
					mo.state = S_PLAY_ROLL
					S_StopSoundByID(mo, sfx_spin)
					S_StartSound(mo, sfx_spin)
				end
			end
			player.willowasindash = false
		end
	end
end)

//Blend mode exclusive stuff
if not AST_MODULATE then return end

skincolors[SKINCOLOR_WILLOSHADOW].ramp = {164,164,164,164,164,164,164,164,164,164,164,164,164,164,164,164}
skincolors[freeslot("SKINCOLOR_WILLOSHADOW2")] = {
	name = "Willo_Shadow2",
	ramp = {162,162,162,162,162,162,162,162,162,162,162,162,162,162,162,162},
	invcolor = SKINCOLOR_ORANGE,
	invshade = 9,
	chatcolor = V_BLUEMAP,
	accessible = false
}
skincolors[freeslot("SKINCOLOR_WILLOSHADOW3")] = {
	name = "Willo_Shadow3",
	ramp = {161,161,161,161,161,161,161,161,161,161,161,161,161,161,161,161},
	invcolor = SKINCOLOR_ORANGE,
	invshade = 9,
	chatcolor = V_BLUEMAP,
	accessible = false
}
skincolors[freeslot("SKINCOLOR_WILLOSHADOW4")] = {
	name = "Willo_Shadow4",
	ramp = {160,160,160,160,160,160,160,160,160,160,160,160,160,160,160,160},
	invcolor = SKINCOLOR_ORANGE,
	invshade = 9,
	chatcolor = V_BLUEMAP,
	accessible = false
}

mobjinfo[freeslot("MT_WILLOGHOST")] = {
	spawnstate = S_THOK,
	spawnhealth = 1,
	radius = 16*FRACUNIT,
	height = 48*FRACUNIT,
	dispoffset = 1,
	mass = 1000,
	damage = 8,
	flags = MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_SCENERY,
}
	
addHook("MobjThinker", function(mobj)
	if mobj and mobj.valid and mobj.fuse
	and (mobj.color == SKINCOLOR_WILLOSHADOW or mobj.color == SKINCOLOR_WILLOSHADOW2
	or mobj.color == SKINCOLOR_WILLOSHADOW3) and (mobj.fuse == mobj.info.damage*3/4
	or mobj.fuse == mobj.info.damage*2/4 or mobj.fuse == mobj.info.damage/4)
		mobj.color = min($+1, SKINCOLOR_WILLOSHADOW4)
	end
end, MT_WILLOGHOST)