//KNOWN ISSUES:
//1. player[BonkerBox_FWEnvi.GetSkinName()..BonkerBox_FWEnvi.GetPersistentTableSuffix()]
//does persist between maps & skin changes but doesn't persist between active gameplay sessions
//(i.e. quitting a race/battle out to a menu then re-entering) because players get nulled when active
//gameplay ends. Technically this was not the intention but the solution is to do some IO stuff that
//would balloon the scope of the project beyond the amount it has already ballooned. Basically I could
//fix it but I don't think it's a big enough problem to warrant the effort it'd take.

//TODO:
//1?. replace all the times where i do 'something = something +...' with 'something = $ + ...'.
//Functionally identical of course so it is a wonder if this is worth the potential debug headache
//when I inevitable mistake an instance or two of this.

//word to the wise: pretty much all the methods in this lump are ONLY designed to be executed in a very specific order
//inside the PostThinkFrame hook of FZWGHOOK and ONLY after StartUp() has been executed as the very first of them.
//it was done this way to leave both this lump and the FZWGHOOK lump as readable as possible so some assumptions about state are made.
//other lumps, like FZWGFUNC, will not have this kind of assumption leading to more clutter but it's because the methods there
//are encouraged to be called anytime anywhere

//don't check for the right skin here, shutting down in the case of a wrong skin is a good thing
//also, be verbose in if checks because StartUp() is guaranteed to have NOT occurred by at least one call to this method
local function ShutDown(player)
	if(BonkerBox_G.IsPlayer(player) ~= true)
		return
	end

	if(BonkerBox_G.IsTable(player[BonkerBox_FWEnvi.GetSkinName()])==true)
		local del = function(m)
			if(BonkerBox_G.IsMobj(m)~=true)
				return//silent fail, idk why this'd happen but if it does then it doesn't need a unique deletion routine does it? /:}
			end
			
			P_RemoveMobj(m)
			m = nil
		end
		
		del(player[BonkerBox_FWEnvi.GetSkinName()].headmobj)
		
		if(BonkerBox_G.IsTable(player[BonkerBox_FWEnvi.GetSkinName()].bodymobjs)==true)
		
			for k,v in pairs(player[BonkerBox_FWEnvi.GetSkinName()].bodymobjs)
				del(v)
			end
			
			player[BonkerBox_FWEnvi.GetSkinName()].bodymobjs = nil
		
		end
		
		player[BonkerBox_FWEnvi.GetSkinName()] = nil
	end
	
	player.bbfw_startedup = false
	//intentionally leave player.bbfw_starteduppersistent as whatever state it was to persist it between maps.
	//this WILL not persist the persistent table between exiting then re-entering active gameplay because players are nulled when this happens so something else must be figured out
end

local function StartUpMain(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end
	
	player[BonkerBox_FWEnvi.GetSkinName()] = {}
	player[BonkerBox_FWEnvi.GetSkinName()].headmobj = nil
	player[BonkerBox_FWEnvi.GetSkinName()].bodymobjs = {}
	player[BonkerBox_FWEnvi.GetSkinName()].magdata = {}
	player[BonkerBox_FWEnvi.GetSkinName()].magdata.movemagnormalized = 0
	player[BonkerBox_FWEnvi.GetSkinName()].magdata.movemagzsigned = 0
	player[BonkerBox_FWEnvi.GetSkinName()].goabledist = {}
	player[BonkerBox_FWEnvi.GetSkinName()].hurttics = 0
	player[BonkerBox_FWEnvi.GetSkinName()].deathstage = 0
	player[BonkerBox_FWEnvi.GetSkinName()].deathtics = 0
	player[BonkerBox_FWEnvi.GetSkinName()].deathpopindex = 0
	player[BonkerBox_FWEnvi.GetSkinName()].gravitystatus = BonkerBox_G.GetGravityStatus_Error()
	player[BonkerBox_FWEnvi.GetSkinName()].hurtmode = false
	player[BonkerBox_FWEnvi.GetSkinName()].lastPlayerPos = {}
	player[BonkerBox_FWEnvi.GetSkinName()].lastPlayerPos.x = 0
	player[BonkerBox_FWEnvi.GetSkinName()].lastPlayerPos.y = 0
	player[BonkerBox_FWEnvi.GetSkinName()].lastPlayerPos.z = 0
	player[BonkerBox_FWEnvi.GetSkinName()].wasjustairborne = false
	player[BonkerBox_FWEnvi.GetSkinName()].lerpingtoground = false
	player[BonkerBox_FWEnvi.GetSkinName()].modcolourtimer = 0
	player[BonkerBox_FWEnvi.GetSkinName()].modcolouroffset = 0
end

local function StartUpPersistent(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end
	
	if(BonkerBox_G.IsBool(player.bbfw_starteduppersistent)~=nil and player.bbfw_starteduppersistent==true)
		return
	end

	player[BonkerBox_FWEnvi.GetSkinName()..BonkerBox_FWEnvi.GetPersistentTableSuffix()] = BonkerBox_FWFunc.GeneratePersistentTable()
	player[BonkerBox_FWEnvi.GetSkinName()..BonkerBox_FWEnvi.GetPersistentCacheTableSuffix()] = BonkerBox_FWFunc.GeneratePersistentTable()
	player.bbfw_starteduppersistent = true
end

local function StartUpPerMobj(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end
	
	local allmobjs = BonkerBox_FWFunc.GetAllMobjs(player)
	
	for i,v in ipairs(allmobjs)
		v.bbfwBeforeTrack = {}
		v.bbfwBeforeTrack.x = player.mo.x
		v.bbfwBeforeTrack.y = player.mo.y
		v.bbfwBeforeTrack.z = player.mo.z
		v.bbfwmoveang = player.mo.angle
		v.bbfw_animatealive_cachedframe = nil
		v.bbfw_animatealive_reachframetics = 0
	end
end

local function StartUp(player, preSpawnAction)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end

	if(BonkerBox_G.IsBool(player.bbfw_startedup)~=nil and player.bbfw_startedup==true)
		return
	end
	
	if(BonkerBox_G.IsFunction(preSpawnAction) ~= true)
		preSpawnAction = function() end
	end
	
	local spawnshit = function(p)
		if(BonkerBox_G.IsPlayingAs(p,BonkerBox_FWEnvi.GetSkinName())~=true)
			return
		end
		
		if(BonkerBox_G.IsMobj(p[BonkerBox_FWEnvi.GetSkinName()].headmobj)~=true)
			p[BonkerBox_FWEnvi.GetSkinName()].headmobj = P_SpawnMobjFromMobj(p.mo,0,0,0,MT_FUZZYWIGGLER)
		end
		
		local spawnlooplimiter = BonkerBox_G.Clamp(p[BonkerBox_FWEnvi.GetSkinName()..BonkerBox_FWEnvi.GetPersistentTableSuffix()].bodycount,BonkerBox_FWEnvi.GetMinBodyCount(),max(BonkerBox_FWCons.BodyCap(),BonkerBox_FWEnvi.GetMinBodyCount()))
		
		for i=1,spawnlooplimiter
			local curGet = function() return p[BonkerBox_FWEnvi.GetSkinName()].bodymobjs[i] end
			local curSet = function(mobj)  p[BonkerBox_FWEnvi.GetSkinName()].bodymobjs[i] = mobj end
			
			if(BonkerBox_G.IsMobj(curGet())==true)
				continue
			end

			curSet( P_SpawnMobjFromMobj(p[BonkerBox_FWEnvi.GetSkinName()].headmobj,0,0,0,MT_FUZZYWIGGLER) )
			
			local oldMomX = curGet().momx
			local oldMomY = curGet().momy
			
			local refscale = p[BonkerBox_FWEnvi.GetSkinName()].headmobj.scale//the actual scale of the mobjs will only be set during their update cycle so make the safe assumption they'll be the same scale as the head mobj for just this setup
			local refang = p.mo.angle //technically this should be p[BonkerBox_FWEnvi.GetSkinName()].headmobj.angle but that won't be configured to match the player until after this gets executed
			P_InstaThrust(curGet(),refang+ANGLE_180,FixedMul(max(BonkerBox_E.DecimalToFixed(BonkerBox_FWEnvi.GetPartRadius()),0),abs(refscale))*i)// the +ANGLE_180 will automatically wrapped, i tested to prove it
			
			local moveX = curGet().momx
			local moveY = curGet().momy
			
			curGet().momx = oldMomX
			curGet().momy = oldMomY
			
			P_SetOrigin(curGet(),curGet().x+moveX,curGet().y+moveY,curGet().z)
		end
	end
	
	ShutDown(player)
	
	StartUpMain(player)
	StartUpPersistent(player)
	
	preSpawnAction()
	
	spawnshit(player)
	
	StartUpPerMobj(player)
	
	player.bbfw_startedup = true
end

local function RecordBeforeTrackPosition(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end
	
	local allmobjs = BonkerBox_FWFunc.GetAllMobjs(player)
	
	for i,v in ipairs(allmobjs)
		v.bbfwBeforeTrack.x = v.x
		v.bbfwBeforeTrack.y = v.y
		v.bbfwBeforeTrack.z = v.z
	end
end

local function ExtendedTrack(p)
	if(BonkerBox_G.IsPlayingAs(p,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end

	local core = function(player,subject,target)
		if(BonkerBox_G.IsMobj(subject)~=true or BonkerBox_G.IsMobj(target)~=true or BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
			return
		end
		
		local invisread = player[BonkerBox_FWEnvi.GetSkinName()]
		
		local invisdefault = BonkerBox_G.IsTable(invisread) and BonkerBox_G.IsBool(invisread.invisdefault)==true and invisread.invisdefault==true
		local invisskybox  = BonkerBox_G.IsTable(invisread) and BonkerBox_G.IsBool(invisread.invisskybox)==true and invisread.invisskybox==true

		BonkerBox_G.TrackMobj(player,subject,target,-1)
		
		if(invisdefault == true)
			subject.renderflags = $ | RF_DONTDRAW
		else
			subject.renderflags = $ &~ RF_DONTDRAW
		end
		
		if(invisskybox == true)
			subject.renderflags = $ | RF_HIDEINSKYBOX
		else
			subject.renderflags = $ &~ RF_HIDEINSKYBOX
		end
		
		subject.height = target.height
		subject.color = target.color
		subject.radius = target.radius
		subject.shadowscale = target.shadowscale
	end
	
	core(p,p[BonkerBox_FWEnvi.GetSkinName()].headmobj,p.mo)
	
	for i,v in ipairs(p[BonkerBox_FWEnvi.GetSkinName()].bodymobjs)
		local t = nil
		
		if(i <=1)
			t = p[BonkerBox_FWEnvi.GetSkinName()].headmobj
		else
			t = p[BonkerBox_FWEnvi.GetSkinName()].bodymobjs[i-1]
		end
		
		core(p,v,t)
	end
end

local function ApplyGhosting(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end
	
	for i,v in ipairs(player[BonkerBox_FWEnvi.GetSkinName()].bodymobjs)

		if(BonkerBox_FWCons.GhostBody()>0)
			v.renderflags = $ | RF_GHOSTLY
			v.shadowscale = 0
		else
			v.renderflags = $ &~ RF_GHOSTLY
		end
	end
end

local function Animate(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end

	for i,v in ipairs(player[BonkerBox_FWEnvi.GetSkinName()].bodymobjs)
	
			local isHurt = BonkerBox_G.PlayerIsSuffering(player)==true
			local isTricking = player.trickpanel ~= TRICKSTATE_NONE and player.trickpanel ~= TRICKSTATE_READY
			local isWhipping = BonkerBox_G.IsMobj(player.whip) == true

		if(isWhipping == true or isHurt==true or isTricking == true) 
			v.angle = player.drawangle
		else
			v.angle = v.bbfwmoveang
		end
	end
end

local function AnimateAlive(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end
	
	local allmobjs = BonkerBox_FWFunc.GetAllMobjs(player)
	
	for i,v in ipairs(allmobjs)
		if(v.state ~= S_FUZZYWIGGLER)
			v.state = S_FUZZYWIGGLER
		end
	end

	if(player[BonkerBox_FWEnvi.GetSkinName()].hurtmode~=true)
		player[BonkerBox_FWEnvi.GetSkinName()].headmobj.sprite=SPR_FZHI
	else
		player[BonkerBox_FWEnvi.GetSkinName()].headmobj.sprite=SPR_FZHH
	end
	
	for i,v in ipairs(player[BonkerBox_FWEnvi.GetSkinName()].bodymobjs)
		
		local frame_i = player[BonkerBox_FWEnvi.GetSkinName()].magdata.movemagnormalized
		local spindashing = player.spindash > 0
		
		if(spindashing==true)
			frame_i = FRACUNIT//force the animation to play maximum speed if revving up
		end

		//there's no practical reason i cant just make this a lerp from FRACUNIT instead of from 0
		//but i timed it out with 0 and don't want to disturb the integer scaling
		//it results in when divided by FRACUNIT in application
		local framespeed = max(FRACUNIT,BonkerBox_G.FixedLerp(frame_i,0,max(BonkerBox_E.DecimalToFixed(BonkerBox_FWEnvi.GetMaxPlaybackSpeed()),1)))
		
		if(BonkerBox_G.IsNumber(player[BonkerBox_FWEnvi.GetSkinName()].goabledist[i]) == true and player[BonkerBox_FWEnvi.GetSkinName()].goabledist[i]<=FixedMul(max(BonkerBox_E.DecimalToFixed(BonkerBox_FWEnvi.GetStandingSpeed()),0),abs(v.scale)) and spindashing~=true)
			framespeed = 0
		end
		
		framespeed = framespeed/FRACUNIT

		local prevspr = v.bbfw_animatealive_cachedframe
		
		if(player[BonkerBox_FWEnvi.GetSkinName()].hurtmode ~= true)
			if(player[BonkerBox_FWEnvi.GetSkinName()].gravitystatus ~=BonkerBox_G.GetGravityStatus_AirborneNormal() and player[BonkerBox_FWEnvi.GetSkinName()].gravitystatus ~=BonkerBox_G.GetGravityStatus_AirborneAnti())
				
				if(framespeed >0)
					v.bbfw_animatealive_cachedframe = SPR_FZBM
				else
					v.bbfw_animatealive_cachedframe = SPR_FZBI
				end
			else
				local normalgrav = player[BonkerBox_FWEnvi.GetSkinName()].gravitystatus == BonkerBox_G.GetGravityStatus_AirborneNormal()
				local antigrav = player[BonkerBox_FWEnvi.GetSkinName()].gravitystatus == BonkerBox_G.GetGravityStatus_AirborneAnti()
				local risingmag = player[BonkerBox_FWEnvi.GetSkinName()].magdata.movemagzsigned > 0
				local fallingmag = risingmag~=true
				
				if((normalgrav ==true and risingmag == true) or (antigrav == true and fallingmag == true))
					v.bbfw_animatealive_cachedframe = SPR_FZBU
				else
					v.bbfw_animatealive_cachedframe = SPR_FZBD
				end
			end
		else
			v.bbfw_animatealive_cachedframe = SPR_FZBH
		end
		
		if(prevspr~=v.bbfw_animatealive_cachedframe)
			local invislastsprite = (BonkerBox_G.IsSprite(prevspr)~=true) or (BonkerBox_G.IsSprite(prevspr)==true and prevspr == SPR_NULL)
		
			if(invislastsprite==true)//if there was no previous cached frame or it was a blank frame then safely assume the wiggler mobjs just spawned or reappeared
				v.bbfw_animatealive_reachframetics = 0
			else
				v.bbfw_animatealive_reachframetics = i-1
			end
			
		end
		
		//the framespeed condition swapping between these two is already naturally delayed from body segment to body segment
		//by the bodyfollow() logic, adding a delay on top of the delay looks dumb
		local instantreachhack = (prevspr == SPR_FZBM and v.bbfw_animatealive_cachedframe == SPR_FZBI) or (v.bbfw_animatealive_cachedframe == SPR_FZBM and prevspr == SPR_FZBI)
		
		if(v.bbfw_animatealive_reachframetics > 0 and instantreachhack~=true)
			v.bbfw_animatealive_reachframetics = $ - 1
		else
			if(BonkerBox_G.IsNumber(v.bbfw_animatealive_cachedframe)==true)
				v.sprite = v.bbfw_animatealive_cachedframe
			else
				v.sprite = SPR_NULL
			end
			
		end
		
		if(v.sprite ==SPR_FZBM)
			v.frame = (leveltime+(i*BonkerBox_FWEnvi.GetCaterpillarDelay()))*framespeed % BonkerBox_FWEnvi.GetAnimFrames()
		else
			v.frame = 0
		end
	end
	
	
end

local function AnimateDead(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end
	
	local allmobjs = BonkerBox_FWFunc.GetAllMobjs(player)
	local allmobjscount = max(BonkerBox_G.CountTable(allmobjs),0)
	
	local popinterval = function() if(allmobjscount > 0) return BonkerBox_FWEnvi.GetDeathAnimDuration()/allmobjscount else return 0 end end
	
	if(player[BonkerBox_FWEnvi.GetSkinName()].deathtics <= 0)
	
		if(player[BonkerBox_FWEnvi.GetSkinName()].deathstage == -1)//preparing
			player[BonkerBox_FWEnvi.GetSkinName()].deathtics = BonkerBox_FWEnvi.GetDeathFreezeTics()
			player[BonkerBox_FWEnvi.GetSkinName()].deathpopindex = 1
			player[BonkerBox_FWEnvi.GetSkinName()].deathstage = $ + 1
		elseif(player[BonkerBox_FWEnvi.GetSkinName()].deathstage == 0)//jumping away
			player[BonkerBox_FWEnvi.GetSkinName()].deathstage = $ + 1
		elseif(player[BonkerBox_FWEnvi.GetSkinName()].deathstage == 1)//popping
			local gotpi = popinterval()
			
			local dopop = function(quiet)

				if(player[BonkerBox_FWEnvi.GetSkinName()].deathpopindex <= allmobjscount)
					local poptarget = allmobjs[(allmobjscount+1) - player[BonkerBox_FWEnvi.GetSkinName()].deathpopindex]
					local reducevfxinst = CV_FindVar("reducevfx")
					
					if(BonkerBox_G.IsCVar(reducevfxinst)==true and reducevfxinst.value~=0)
						poptarget.state = S_INVISIBLE
					else
						poptarget.state = S_FUZZYWIGGLER_POP
						
						local isheadpop =player[BonkerBox_FWEnvi.GetSkinName()].deathpopindex >= allmobjscount 
						local quietval = BonkerBox_G.IsBool(quiet)==true and quiet==true
						
						if(quietval==false or (quietval==true and isheadpop==true))
							S_StartSound(poptarget,sfx_fwgrpp)
						end
					end

					player[BonkerBox_FWEnvi.GetSkinName()].deathpopindex = $+1
					
				else
					player[BonkerBox_FWEnvi.GetSkinName()].deathstage = $ + 1 //doesn't exist but that's okay
				end

			end
			
			if(gotpi > 0)
				dopop(false)
			else
				while(player[BonkerBox_FWEnvi.GetSkinName()].deathstage == 1)//i considered a while(true) loop that'd be *sure* to escape upon deathstage changing in dopop() but the 'if' statement that checks that is on a nest layer higher than this while loop so a while(true) does in-fact freeze the game
					dopop(true)
				end
			end
			
			player[BonkerBox_FWEnvi.GetSkinName()].deathtics = gotpi
		end
	end
	
	player[BonkerBox_FWEnvi.GetSkinName()].deathtics = $-1
end

local function AnimateRespawn(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end
	
	local allmobjs = BonkerBox_FWFunc.GetAllMobjs(player)
	
	for i,v in ipairs(allmobjs)
		v.state = S_INVISIBLE
	end
end

local function BodyFollow(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end

	for i,v in ipairs(player[BonkerBox_FWEnvi.GetSkinName()].bodymobjs)

		local movetarget = nil

		if(i > 1)
			movetarget = player[BonkerBox_FWEnvi.GetSkinName()].bodymobjs[i-1]
		else
			movetarget = player[BonkerBox_FWEnvi.GetSkinName()].headmobj
		end
		
		local heighteaseratefrac = BonkerBox_G.Clamp(BonkerBox_E.DecimalToFixed(BonkerBox_FWEnvi.GetHeightEaseRate()),0,FRACUNIT)
		local heightfallratefrac = BonkerBox_G.Clamp(BonkerBox_E.DecimalToFixed(BonkerBox_FWEnvi.GetHeightFallRate()),0,FRACUNIT)
		
		local setZ = BonkerBox_G.FixedLerp(heighteaseratefrac,v.bbfwBeforeTrack.z,movetarget.z)
		
		local isgroundednormal = player[BonkerBox_FWEnvi.GetSkinName()].gravitystatus == BonkerBox_G.GetGravityStatus_GroundedNormal()
		local isgroundedanti = player[BonkerBox_FWEnvi.GetSkinName()].gravitystatus == BonkerBox_G.GetGravityStatus_GroundedAnti()
		
		if(isgroundednormal==true or isgroundedanti==true)
			local setZ_target = nil
			
			if(isgroundednormal==true)
				setZ_target = P_FloorzAtPos(v.bbfwBeforeTrack.x, v.bbfwBeforeTrack.y, v.bbfwBeforeTrack.z, v.height)
			else
				setZ_target = P_CeilingzAtPos(v.bbfwBeforeTrack.x, v.bbfwBeforeTrack.y, v.bbfwBeforeTrack.z, v.height)-v.height
			end
			
			if(player[BonkerBox_FWEnvi.GetSkinName()].wasjustairborne ==true)
				player[BonkerBox_FWEnvi.GetSkinName()].lerpingtoground = true
			end
			
			if(player[BonkerBox_FWEnvi.GetSkinName()].lerpingtoground == true)
				setZ = BonkerBox_G.FixedLerp(heightfallratefrac,v.bbfwBeforeTrack.z,setZ_target)
				
				if((isgroundednormal==true and setZ <= setZ_target) or (isgroundednormal~=true and setZ >= setZ_target))
					player[BonkerBox_FWEnvi.GetSkinName()].lerpingtoground =false
				end
			else
				setZ = setZ_target
			end
			
		end
		
		local movedir =  R_PointToAngle2(v.bbfwBeforeTrack.x,v.bbfwBeforeTrack.y,movetarget.x,movetarget.y)

		local angleeaseratefrac = BonkerBox_G.Clamp(BonkerBox_E.DecimalToFixed(BonkerBox_FWEnvi.GetAngleEaseRate()),0,FRACUNIT) 
		local partradiusfrac = max( FixedMul(BonkerBox_E.DecimalToFixed(BonkerBox_FWEnvi.GetPartRadius()),abs(v.scale)) , 0)
		local radiusangleeasemaximumfrac = max(BonkerBox_E.DecimalToFixed(BonkerBox_FWEnvi.GetRadiusAngleEaseMaximum()),0)
		local maxangledist = FixedMul(partradiusfrac,radiusangleeasemaximumfrac)
		local totaldist  = abs(P_AproxDistance(abs(v.bbfwBeforeTrack.x-movetarget.x),abs(v.bbfwBeforeTrack.y-movetarget.y)))
		player[BonkerBox_FWEnvi.GetSkinName()].goabledist[i] = max(totaldist - partradiusfrac,0)
		
		local curangleease_i = BonkerBox_G.FixedInverseLerp(player[BonkerBox_FWEnvi.GetSkinName()].goabledist[i],0,maxangledist)
		local curangleease = BonkerBox_G.Clamp(BonkerBox_G.FixedLerp(curangleease_i,angleeaseratefrac,FRACUNIT),0,FRACUNIT)

		v.bbfwmoveang = BonkerBox_E.EaseAngle(v.bbfwmoveang,movedir,curangleease)
			
		//being on the garden top technically counts as airborne to the game so it looks stupid in-action, do a special routine instead
		//still, it's important v.bbfwmoveang is updated regardless so that once we get off the top the body parts are in the best place
		//they can be to readjust
		if(player.curshield ~=KSHIELD_TOP)
			P_MoveOrigin(v,v.bbfwBeforeTrack.x,v.bbfwBeforeTrack.y,setZ)
			
			P_InstaThrust(v,v.bbfwmoveang, player[BonkerBox_FWEnvi.GetSkinName()].goabledist[i] )
		else
			P_MoveOrigin(v,movetarget.x,movetarget.y,movetarget.z)
		end
		
		if(BonkerBox_G.IsMobj(movetarget)~=true)
			continue
		end
		
	end
	
	player[BonkerBox_FWEnvi.GetSkinName()].wasjustairborne = player[BonkerBox_FWEnvi.GetSkinName()].gravitystatus == BonkerBox_G.GetGravityStatus_AirborneNormal() or player[BonkerBox_FWEnvi.GetSkinName()].gravitystatus == BonkerBox_G.GetGravityStatus_AirborneAnti()
end

local function ApplyHurtColour(player)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end

	if(player[BonkerBox_FWEnvi.GetSkinName()].hurtmode == true)
		player[BonkerBox_FWEnvi.GetSkinName()].hurttics = BonkerBox_FWEnvi.GetHurtRecolourDuration()
	else
		if(player.respawn.state ~= RESPAWNST_NONE)//in case, upon dying, the timer set in the above if clause outlasts the time it takes to respawn
			player[BonkerBox_FWEnvi.GetSkinName()].hurttics = 0
		else
			player[BonkerBox_FWEnvi.GetSkinName()].hurttics = max(player[BonkerBox_FWEnvi.GetSkinName()].hurttics-1,0)
		end
		
	end
	
	if(player[BonkerBox_FWEnvi.GetSkinName()].hurttics <=0)
		return
	end
	
	local allmobjs = BonkerBox_FWFunc.GetAllMobjs(player)

	for i,v in ipairs(allmobjs)
		v.color = SKINCOLOR_PURPLE
	end
end

local function ApplyModulatedColour(player,index,count,colour,modeindex)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end
	
	if(BonkerBox_G.IsNumber(index)~=true)
		return
	end
	
	if(BonkerBox_G.IsNumber(count)~=true)
		return
	end
	
	if(BonkerBox_G.IsSkinColor(colour)~=true)
		return
	end
	
	if(BonkerBox_G.IsNumber(modeindex)~=true)
		return
	end
	
	if(modeindex==1)//modulate
	
		index = max($,1)
		index = $+1
		local allmobjs = BonkerBox_FWFunc.GetAllMobjs(player)
		
		for i,v in ipairs(allmobjs)
			if(BonkerBox_G.NegativeAgnosticModulo(i+player[BonkerBox_FWEnvi.GetSkinName()].modcolouroffset,index) == 0)
				v.color = colour
			end
		end
	
	elseif(modeindex==2)//multiple
		index=max($,1)
		count = max($,1)
		count = $+1
		
		local allmobjs = BonkerBox_FWFunc.GetAllMobjs(player)
		
		for i,v in ipairs(allmobjs)
			local nm = BonkerBox_G.NegativeAgnosticModulo
			
			if(nm(i-2,count) == 0)
				local includeheadhack = player[BonkerBox_FWEnvi.GetSkinName()].modcolouroffset ~=0
				local baseval = nil
				
				//i wrestled for a long time figuring out the formula for the index needed for animation
				//to cooperate with this mapping mode, in the end i got it almost perfect except
				//when animated the head would be ignored because technically the first index
				//assessable for animation was the 2nd index
				//as such, if animation is happening, just offset everything back by 1
				if(includeheadhack~=true)
					baseval = i
				else
					baseval = i-1
				end
			
				local fml = allmobjs[baseval+nm((index-1)+player[BonkerBox_FWEnvi.GetSkinName()].modcolouroffset,count)]
				
				if(BonkerBox_G.IsMobj(fml)==true)
					fml.color = colour
				end
			end
		end
	else
		return
	end
end

local function UpdateModulatedColourOffset(player,hardreset)
	if(BonkerBox_G.IsPlayingAs(player,BonkerBox_FWEnvi.GetSkinName())~=true)
		return
	end
	
	if(BonkerBox_G.IsNumber(hardreset)~=true)
		return
	end
	
	hardreset = max(hardreset,1)//modulo against 0 is illegal

	if(player[BonkerBox_FWEnvi.GetSkinName()].modcolourtimer > 0)
		player[BonkerBox_FWEnvi.GetSkinName()].modcolourtimer = $ - 1
	else
		
		local bcs = min(abs(player[BonkerBox_FWEnvi.GetSkinName()..BonkerBox_FWEnvi.GetPersistentTableSuffix()].bodycolspeed),FRACUNIT)
		
		if(bcs >0)
			if(player[BonkerBox_FWEnvi.GetSkinName()..BonkerBox_FWEnvi.GetPersistentTableSuffix()].bodycolreversespeed==true)
				player[BonkerBox_FWEnvi.GetSkinName()].modcolouroffset =$-1
			else
				player[BonkerBox_FWEnvi.GetSkinName()].modcolouroffset =$+1
			end
			
			player[BonkerBox_FWEnvi.GetSkinName()].modcolourtimer = FRACUNIT/bcs
		else
			player[BonkerBox_FWEnvi.GetSkinName()].modcolouroffset = 0
			player[BonkerBox_FWEnvi.GetSkinName()].modcolourtimer = 0
		end
		
		player[BonkerBox_FWEnvi.GetSkinName()].modcolouroffset = BonkerBox_G.NegativeAgnosticModulo($,hardreset)//to prevent an overflow after extended play
	end
end

local bbfwm = {}
bbfwm.ShutDown = ShutDown
bbfwm.StartUp = StartUp
bbfwm.RecordBeforeTrackPosition = RecordBeforeTrackPosition
bbfwm.ExtendedTrack = ExtendedTrack
bbfwm.ApplyGhosting = ApplyGhosting
bbfwm.Animate = Animate
bbfwm.AnimateAlive = AnimateAlive
bbfwm.AnimateDead = AnimateDead
bbfwm.AnimateRespawn = AnimateRespawn
bbfwm.BodyFollow = BodyFollow
bbfwm.ApplyHurtColour = ApplyHurtColour
bbfwm.ApplyModulatedColour = ApplyModulatedColour
bbfwm.UpdateModulatedColourOffset = UpdateModulatedColourOffset
rawset(_G,"BonkerBox_FWMain",bbfwm)