-- Thokker! Play in Team Match mode. -- gem here. no you don't. not anymore. 2.2.1 was epic and we have custom gamemodes now.
-- Not about testicles, don't you worry.

-- Setup information for custom maps:
-- Place match starts for mid-game spawning. Place CTF Red Team and Blue Team starts for start-of-round spawning.
-- Thing type 321 is the Thokker Ball Spawnpoint location.
-- Linedef tag 9999 is the tag reserved for closing off the two sides- usually making the center barrier rise instantly.
-- Linedef tag 9998 is the tag reserved for opening up the two sides- usually making the center barrier fall instantly.

-- server.gamestate explanation:
-- 0 = Uninitialized.
-- 1 = Playing.
-- 2 to 5*TICRATE = Counting down to play.
-- 5*TICRATE and above = Counting down to let players determine their position.
-- -1 to -4*TICRATE = Goal scored, waiting to reset playfield.

local SETUPTIME = 10*TICRATE -- may be changed later

local theball -- The current ball in play
local scoringplayer -- If a player scored, here they are
local balldarcolor = {} -- To avoid having to poll the mapheader info every tic.

local function flagGet(field, flag)
	if string.find(field, flag) then
		return _G[flag]
	else
		return 0
	end
end

local function ballInfoSet(map)
	mobjinfo[MT_BIGTUMBLEWEED].radius = 32*FRACUNIT
	mobjinfo[MT_BIGTUMBLEWEED].height = 64*FRACUNIT

	-- Makes the ball homing-attackable, but causes too many other problems so it's a no-go for now.
	--mobjinfo[MT_BIGTUMBLEWEED].flags = $1|MF_MONITOR

	local word = sprnames[mapheaderinfo[map].thokkerball:sub(1, 4)]

	for i = S_BIGTUMBLEWEED, S_BIGTUMBLEWEED_ROLL8 do
		states[i].sprite = word
		local flags = 0
		local field = mapheaderinfo[map].thokkerflags
		if (field) then
			flags = flagGet(field, "FF_FULLBRIGHT") | flagGet(field, "TR_TRANS10") | flagGet(field, "TR_TRANS20") | flagGet(field, "TR_TRANS30") | flagGet(field, "TR_TRANS40") | flagGet(field, "TR_TRANS50") | flagGet(field, "TR_TRANS60") | flagGet(field, "TR_TRANS70") | flagGet(field, "TR_TRANS80") | flagGet(field, "TR_TRANS90"); -- Kind of a cheap hack, but whatever.
		end
		states[i].frame = max(0, i - (S_BIGTUMBLEWEED_ROLL1)) | flags
	end

	if (mapheaderinfo[map].thokkerballsound and _G[mapheaderinfo[map].thokkerballsound]) then
		mobjinfo[MT_BIGTUMBLEWEED].activesound = _G[mapheaderinfo[map].thokkerballsound]
	else
		mobjinfo[MT_BIGTUMBLEWEED].activesound = sfx_s3k64
	end
	if mapheaderinfo[map].radarinvert then
		balldarcolor[0] = 26
		balldarcolor[1] = 3
	else
		balldarcolor[0] = 3 -- 160 -- Bright green was previously used as the default, but blended too much into grass. Now it matches the default font.
		balldarcolor[1] = 26 -- 170
	end
end

addHook("NetVars", function(sync)
	--print("begin syncing ball state")
	theball = sync(theball)

	local x = sync(theball.x)
	local y = sync(theball.y)
	local z = sync(theball.z)
	P_MoveOrigin(theball, x, y, z)

	theball.momx = sync(theball.momx)
	theball.momy = sync(theball.momy)
	theball.momz = sync(theball.momz)

	ballInfoSet(gamemap)
	theball.radius = sync(theball.radius)
	theball.height = sync(theball.height)
	--print(("done syncing ball state %d %d %d"):format(theball.x/FRACUNIT, theball.y/FRACUNIT, theball.z/FRACUNIT))
end)

local function ballInfoUnset()
	mobjinfo[MT_BIGTUMBLEWEED].radius = 24*FRACUNIT
	mobjinfo[MT_BIGTUMBLEWEED].height = 48*FRACUNIT

	--mobjinfo[MT_BIGTUMBLEWEED].flags = $1 & ~MF_MONITOR

	-- Gotta reset the sprites
	local word = SPR_BTBL

	for i = S_BIGTUMBLEWEED, S_BIGTUMBLEWEED_ROLL8 do
		states[i].sprite = word
		states[i].frame = max(0, i - (S_BIGTUMBLEWEED_ROLL1))
	end

	mobjinfo[MT_BIGTUMBLEWEED].activesound = sfx_s3k64
end

-- Manage game state
addHook("MapLoad", function(map)
	if mapheaderinfo[map].thokkerball then
		if gametype != GT_THOKKER then
			COM_BufInsertText(server, "map "+G_BuildMapName(gamemap)+" -gametype thokker")
			return
		end

		ballInfoSet(map)

		server.gamestate = 0

		server.redscore = 0
		server.bluescore = 0

//		for p in players.iterate do -- Clear some shit
//			COM_BufInsertText(p, "wait 3;chasecam 1")
//		end
	else
		ballInfoUnset()

		-- And reset player stats that have gotten changed
		for p in players.iterate do
			if p.mo then
				p.actionspd = skins[p.mo.skin].actionspd
				p.maxdash = skins[p.mo.skin].maxdash
			end
		end
	end
end)

addHook("PlayerJoin", do
	if mapheaderinfo[gamemap].thokkerball then
		ballInfoSet(gamemap)
	else
		ballInfoUnset()
	end
end)

addHook("ThinkFrame", do
	if not mapheaderinfo[gamemap].thokkerball then return end

	if server.gamestate ~= 1 then
		server.gamestate = ($1 or 0)-1
	end
	if server.gamestate == -4*TICRATE then -- Go to setup phase
		if theball and theball.valid then
			P_RemoveMobj(theball)
		end

		server.gamestate = SETUPTIME+5*TICRATE
		P_LinedefExecute(9999)
	end

	if server.gamestate == 5*TICRATE then -- Spawn the ball and get ready to start the round
		if theball and theball.valid then
			P_RemoveMobj(theball)
		end

		for mt in mapthings.iterate do
			if mt.type == 321 then
				theball = P_SpawnMobj(mt.x<<FRACBITS, mt.y<<FRACBITS, INT32_MIN, MT_BIGTUMBLEWEED)
				theball.z = $1 + mt.z<<FRACBITS
				theball.angle = FixedAngle(mt.angle<<FRACBITS)
			end
		end

		if not (theball and theball.valid) then
			theball = P_SpawnMobj(0, 0, INT32_MIN, MT_BIGTUMBLEWEED)
			theball.angle = ANGLE_90
		end

		scoringplayer = nil

		P_LinedefExecute(9998)

		S_StartSound(nil, sfx_strpst)
	end
end)

-- Manage players
local SpinRevHack
addHook("MobjThinker", function(mo)
	if not mapheaderinfo[gamemap].thokkerball then return end

//	-- Set chasecam on on initial join
//	if not mo.player.fhqwgads then
//		mo.player.fhqwgads = true
//		COM_BufInsertText(mo.player, "wait 1;chasecam 1")
//	end

	-- Remove pity shield
	if mo.player.powers[pw_shield] then
		P_RemoveShield(mo.player)
	end

	-- Climbing buff
	if mo.player.climbing then
		P_TryMove(mo, mo.x+mo.momx/3, mo.y+mo.momy/3, true)
		mo.z = $1+mo.momz/3
	end

	-- Nerf thok, for fairness
	if mo.player.charability == CA_THOK and mo.player.actionspd == skins[mo.skin].actionspd then
		mo.player.actionspd = 8*$1/10 --48*FRACUNIT for Sonic
	end

	-- Nerf spindash too :(
	if mo.player.charability2 == CA2_SPINDASH and mo.player.maxdash == skins[mo.skin].maxdash then
		mo.player.maxdash = 13*$1/18 --65*FRACUNIT for normal characters
	end

	if not mo.health then return end

    -- Fliers can push away balls while flying, making them good goalies
    if mo.player.charability == CA_FLY and theball and theball.valid and mo.player.powers[pw_tailsfly] and server.gamestate <= 1 then
        local gustcalc = P_AproxDistance(P_AproxDistance(theball.x - mo.x, theball.y - mo.y), (mo.height - theball.height)/2 + theball.z - mo.z)
        if gustcalc < 256*FRACUNIT then
            gustcalc = FixedDiv(32*FRACUNIT, $1)
            if gustcalc > FRACUNIT then
                gustcalc = $1*$1/FixedSqrt(2*FRACUNIT)
            end
            if ((mo.player.cmd.buttons & BT_JUMP) and not (mo.player.cmd.buttons & BT_USE) and not mo.prevflycondition) then gustcalc = $1*2 end -- buff push whenever you press jump
            theball.momz = $1 + gustcalc
            gustcalc = mo.player.powers[pw_tailsfly]*$1/TICRATE
            P_Thrust(theball, mo.angle, gustcalc) -- was previously R_PointToAngle2(mo.x, mo.y, theball.x, theball.y)...
        end
    end
	mo.prevflycondition = (mo.player.cmd.buttons & BT_JUMP)

	-- Reset laststart if you switch teams or the map's just been loaded
	if mo.player.laststart and (mo.player.laststart[5] ~= mo.player.ctfteam or mo.player.laststart[6] ~= gamemap) then
		mo.player.laststart = nil
		return
	end

	-- Set player's position based on team and available player starts
	if not mo.player.laststart and mo.health then
		--print(mo.player.ctfteam)
		local starts = {}
		local startnum = mo.player.ctfteam -- + 0 -- We can't use red/blue team starts! They're nullified on map load! Hahahahaha- *kills self*
		--print(startnum)
		for mt in mapthings.iterate do
			--print("Found spawnpoint of number "..mt.type)
			if mt.type == startnum then
				table.insert(starts, mt)
			end
		end
		local dest = starts[P_RandomKey(#starts)+1]

		if dest then
			P_MoveOrigin(mo, dest.x<<FRACBITS, dest.y<<FRACBITS, dest.z<<FRACBITS)
			mo.angle = FixedAngle(dest.angle<<FRACBITS)
		else
			print("Player "..#(mo.player).." couldn't find a spawnpoint!")
		end

		mo.player.laststart = {
			mo.x,
			mo.y,
			mo.floorz,
			mo.angle,
			mo.player.ctfteam,
			gamemap
		}
	end

	-- If between setup phase and round start, freeze in place
	if server.gamestate ~= nil and server.gamestate > 1 and server.gamestate <= 5*TICRATE then
		mo.momx = 0
		mo.momy = 0

		mo.player.laststart = {
			mo.x,
			mo.y,
			mo.floorz,
			mo.angle,
			mo.player.ctfteam,
			gamemap
		}

		mo.player.powers[pw_nocontrol] = server.gamestate
		mo.player.powers[pw_underwater] = 0
		mo.player.powers[pw_spacetime] = 0
		mo.player.pflags = $1|PF_FULLSTASIS

		SpinRevHack(mo)
	elseif mo.player.dashspeedhax
		-- Restore proper dash speed from hax variable
		mo.player.pflags = $1|PF_STARTDASH|PF_SPINNING|PF_USEDOWN
		mo.player.pflags = $1 & ~ PF_FULLSTASIS
		mo.player.powers[pw_nocontrol] = 0
		mo.player.dashspeed = mo.player.dashspeedhax
		mo.player.dashtime = mo.player.dashtimehax
		mo.player.dashspeedhax = 0
		mo.player.dashtimehax = 0
	end

	-- If starting setup phase, set position based on last start
	if (server.gamestate == SETUPTIME+5*TICRATE-1 and leveltime > 5*TICRATE) or (mo.player.laststart and not mo.spawnedinplace) then
		P_MoveOrigin(mo, unpack(mo.player.laststart))
		mo.angle = mo.player.laststart[4]
		mo.momx = 0
		mo.momy = 0
		mo.momz = FRACUNIT
		mo.spawnedinplace = true
	end
end, MT_PLAYER)

-- Manage ball
addHook("MobjThinker", function(mo)
	if not mapheaderinfo[gamemap].thokkerball then return end

	if not mo.lasttouchedby then
		mo.lasttouchedby = {}
	end

	if (mo.momx or mo.momy) then
		mo.angle = R_PointToAngle2(0, 0, mo.momx, mo.momy)
	end

	theball = mo -- Just in case...

	-- Momentum dampening
	mo.momx = FixedMul($1, 65000)
	mo.momy = FixedMul($1, 65000)
	mo.momz = FixedMul($1, 65000) - (FRACUNIT>>4)

	-- Count down no-touch timer
	if mo.notouchtimer then
		mo.notouchtimer = $1-1
	end

	local sec3d = P_ThingOnSpecial3DFloor(mo) -->G: no idea what to replace this with yet, the suggested replacements aren't documented
	local sec = mo.subsector.sector

	if server.gamestate == 1 then
		if GetSecSpecial(sec.special, 4) == 3 or (sec3d and GetSecSpecial(sec3d.special, 4)) == 3 then
			--print("RED TEAM BASE")

			server.bluescore = ($1 or 0)+1

			if mo.lasttouchedby[2] then
				scoringplayer = mo.lasttouchedby[2].player
				P_AddPlayerScore(scoringplayer, 1)
			else
				scoringplayer = 2

				-- Find random blue team player to use for the score
				for p in players.iterate do
					if p.ctfteam == 2 then
						p.score = $1-1
						P_AddPlayerScore(p, 1)
						break
					end
				end
			end

			for p in players.iterate do
				if p.ctfteam == 2 then
					S_StartSound(nil, sfx_flgcap, p)
				else
					S_StartSound(nil, sfx_lose, p)
				end
			end

			server.gamestate = 0
		elseif GetSecSpecial(sec.special, 4) == 4 or (sec3d and GetSecSpecial(sec3d.special, 4)) == 4 then
			--print("BLUE TEAM BASE")

			server.redscore = ($1 or 0)+1

			if mo.lasttouchedby[1] then
				scoringplayer = mo.lasttouchedby[1].player
				P_AddPlayerScore(scoringplayer, 1)
			else
				scoringplayer = 1

				-- Find random red team player to use for the score
				for p in players.iterate do
					if p.ctfteam == 1 then
						p.score = $1-1
						P_AddPlayerScore(p, 1)
						break
					end
				end
			end

			for p in players.iterate do
				if p.ctfteam == 1 then
					S_StartSound(nil, sfx_flgcap, p)
				else
					S_StartSound(nil, sfx_lose, p)
				end
			end

			server.gamestate = 0
		end
	end
end, MT_BIGTUMBLEWEED)

-- If a ball touches a player, remember it
addHook("TouchSpecial", function(mo, toucher)
	if not mapheaderinfo[gamemap].thokkerball then return end

	if server.gamestate > 1 then return true end -- Don't mess up if the thickness of the barrier is incorrect.

	-- Keep the ball from getting stuck inside us
	P_Thrust(mo, R_PointToAngle2(toucher.x, toucher.y, mo.x, mo.y), FRACUNIT>>3)

	-- Ignore when the ball bumps up behind us
	if (toucher.momx or toucher.momy) and
		abs(
			R_PointToAngle2(toucher.x-toucher.momx, toucher.y-toucher.momy, mo.x, mo.y)
			- R_PointToAngle2(0, 0, toucher.momx, toucher.momy)
		) > ANGLE_90 then return true end

	if toucher.player.powers[pw_flashing] > TICRATE then return true end -- Knocked back players can't touch the ball!

	-- local homingattack = (toucher.target and toucher.player.charability == CA_HOMINGTHOK)

	-- No touch timer
	if (mo.notouchtimer) then return true end -- and not homingattack
	mo.notouchtimer = 5

	--print(toucher.player.ctfteam)

	mo.lasttouchedby[toucher.player.ctfteam] = toucher

	-- Collision
	if toucher.player.pflags & PF_SPINNING then
		toucher.momx, toucher.momy = $1/3, $2/3
		mo.momx, mo.momy = toucher.momx, toucher.momy
		mo.momz = P_MobjFlip(mo)*P_AproxDistance(toucher.momx, toucher.momy)*3/2
		if not S_SoundPlaying(toucher, mobjinfo[MT_BIGTUMBLEWEED].activesound) then
			S_StartSound(toucher, mobjinfo[MT_BIGTUMBLEWEED].activesound)
		end
	else
		mo.momx, mo.momy = toucher.momx, toucher.momy
		mo.momz = P_MobjFlip(mo)*P_AproxDistance(toucher.momx, toucher.momy)/4
		toucher.momx, toucher.momy = 8*$1/10, 8*$2/10
		if not S_SoundPlaying(toucher, mobjinfo[MT_BIGTUMBLEWEED].activesound) then
			S_StartSound(toucher, mobjinfo[MT_BIGTUMBLEWEED].activesound)
		end
	end
	mo.state = mo.info.seestate

	-- if homingattack then
	--	toucher.target, toucher.tracer = nil, nil
	--	toucher.player.pflags = $1|PF_THOKKED
	--	toucher.momx, toucher.momy, toucher.momz = 0, 0, P_MobjFlip(toucher)*toucher.scale*8
	--	mo.momx, mo.momy, mo.momz = 2*$1/3, 2*$2/3, 2*$3/3
	-- end

	return true
end, MT_BIGTUMBLEWEED)

-- If a ball runs into trouble, prevent its popping. This is a temporary measure, but better than nothing right now.
addHook("ShouldDamage", function(mo, inflictor, source, damage)
	if not mapheaderinfo[gamemap].thokkerball then return end

	return false
end, MT_BIGTUMBLEWEED)

-- Ball ends round on removal
addHook("MobjRemoved", do
	if not mapheaderinfo[gamemap].thokkerball then return end
	if server.gamestate ~= 1 then return end
	server.gamestate = 0
	scoringplayer = nil
	S_StartSound(nil, sfx_lose)
end, MT_BIGTUMBLEWEED)

-- Console command for timeout
COM_AddCommand("endround", do
	server.gamestate = 0
	scoringplayer = nil
	S_StartSound(nil, sfx_lose)
end, 1)

-- Gliders knock other players back if they hit them while gliding
addHook("MobjMoveCollide", function(mo, other)
	if not mapheaderinfo[gamemap].thokkerball then return end

	if not mo.player then return end
	if mo.player.charability ~= CA_GLIDEANDCLIMB then return end
	if other.type ~= MT_PLAYER then return end
	if mo.z > other.z+other.height then return end
	if other.z > mo.z+mo.height then return end
	if other.player.powers[pw_flashing] then return end

	if mo.player.glidetime then
		P_DamageMobj(other, mo, nil, 1)
	end
end, MT_PLAYER)

-- Any player that hurts another player somehow with their Lua'd ability should, in theory, behave exactly as gliding does. In theory.
addHook("ShouldDamage", function(mo, inflictor, source, damage)
	if not mapheaderinfo[gamemap].thokkerball then return end

	if not inflictor then return end

	if (inflictor.type == MT_PLAYER and inflictor ~= mo) or (source and source.type == MT_PLAYER and source ~= mo) then
		mo.player.powers[pw_shield] = SH_JUMP
		P_DamageMobj(mo, nil, nil, 1)
		P_InstaThrust(mo, inflictor.angle, 7*FRACUNIT)
		return false
	end
end, MT_PLAYER)

-- Suppress hit message on above
addHook("HurtMsg", do
	if mapheaderinfo[gamemap].thokkerball then return true end
end)

-- HUD stuff
local function R_GetScreenCoords(p, c, mx, my, mz)
	local camx, camy, camz, camangle, camaiming
	if p.awayviewtics then
		camx = p.awayviewmobj.x
		camy = p.awayviewmobj.y
		camz = p.awayviewmobj.z
		camangle = p.awayviewmobj.angle
		camaiming = p.awayviewaiming
	elseif c.chase then
		camx = c.x
		camy = c.y
		camz = c.z
		camangle = c.angle
		camaiming = c.aiming
	else
		camx = p.mo.x
		camy = p.mo.y
		camz = p.viewz-20*FRACUNIT
		camangle = p.mo.angle
		camaiming = p.aiming
	end

	local x = camangle-R_PointToAngle2(camx, camy, mx, my)

	local distfact = cos(x)
	if not distfact then
		distfact = 1
	end -- MonsterIestyn, your bloody table fixing...

	if x > ANGLE_90 or x < ANGLE_270 then
		return -9, -9, 0
	else
		x = FixedMul(tan(x, true), 160<<FRACBITS)+160<<FRACBITS
	end

	local y = camz-mz
	--print(y/FRACUNIT)
	y = FixedDiv(y, FixedMul(distfact, R_PointToDist2(camx, camy, mx, my)))
	y = (y*160)+(100<<FRACBITS)
	y = y+tan(camaiming, true)*160

	local scale = FixedDiv(160*FRACUNIT, FixedMul(distfact, R_PointToDist2(camx, camy, mx, my)))
	--print(scale)

	return x, y, scale
end

local cv_track = CV_RegisterVar({
	"hudtracking",
	1,
	0,
	{off = 0, ball = 1, team = 2}
})

local cv_radar = CV_RegisterVar({
	"weedradar",
	1,
	0,
	CV_OnOff
})

hud.add(function(v, player, camera)
	if not mapheaderinfo[gamemap].thokkerball then
		hud.enable("score")
		hud.enable("time")
		hud.enable("rings")

		return
	end

	local gs = server.gamestate

	hud.disable("score")
	hud.disable("time")
	hud.disable("rings")

	-- Message that shows up during not-gameplay
	local str = ""

	if gs <= 0 then
		if leveltime < 5*TICRATE then
			str = "Getting ready to begin..." --("Setup will begin in %d.%02d seconds..."):format(G_TicsToSeconds(gs+4*TICRATE), G_TicsToCentiseconds(gs+4*TICRATE))
		elseif scoringplayer == 1 then
			str = "Red team scored!"
		elseif scoringplayer == 2 then
			str = "Blue team scored!"
		elseif scoringplayer and scoringplayer.valid then
			local team
			if scoringplayer.ctfteam == 1 then
				team = "red"
			else
				team = "blue"
			end
			str = ("%s scored for the %s team!"):format(scoringplayer.name, team)
		else
			str = "No score this round."
		end
	elseif gs > 5*TICRATE then
		str = ("Get into position! %d.%02d"):format(G_TicsToSeconds(gs-5*TICRATE), G_TicsToCentiseconds(gs-5*TICRATE))
	elseif gs > 1 then
		str = ("The action starts in %d.%02d seconds."):format(G_TicsToSeconds(gs-1), G_TicsToCentiseconds(gs-1))
	end

	v.drawString(160, 160, str, V_ALLOWLOWERCASE, "center")

-- No need to draw the game score anymore as of 2.2 >G

	if not player.mo then return end

	if cv_radar.value then
		local patch = nil
		if v.patchExists(sprnames[states[S_BIGTUMBLEWEED].sprite] .. "A0") then
			patch = v.cachePatch(sprnames[states[S_BIGTUMBLEWEED].sprite] .. "A0")
		elseif v.patchExists(sprnames[states[S_BIGTUMBLEWEED].sprite] .. "A2A8") then
			patch = v.cachePatch(sprnames[states[S_BIGTUMBLEWEED].sprite] .. "A2A8")
		else
			patch = v.cachePatch(sprnames[states[S_BIGTUMBLEWEED].sprite] .. "A2")
		end
		if states[S_BIGTUMBLEWEED].sprite == SPR_BTBL then
			v.drawScaled(291*FRACUNIT, 185*FRACUNIT, FRACUNIT/2, patch, V_50TRANS)
		else
			v.drawScaled(291*FRACUNIT, 181*FRACUNIT, FRACUNIT/2, patch, V_50TRANS)
		end

		if theball and theball.valid then

			local angle1 = R_PointToAngle2(player.mo.x, player.mo.y, theball.x, theball.y)
			            - R_PointToAngle(player.mo.x+cos(player.mo.angle), player.mo.y+sin(player.mo.angle))

			local angle2 = R_PointToAngle2(player.mo.z, 0, theball.z, P_AproxDistance(player.mo.x - theball.x, player.mo.y - theball.y) )

			local x, y = sin(angle1)/-3000, ((cos(angle1) + cos(angle2) - sin(player.aiming))/-6000)

			local depthcheck = ((angle1 + ANGLE_90) > 0)

			v.drawFill(290+x-1,   170+y-1,   5, 5, balldarcolor[1])
			if depthcheck then
				v.drawFill(290+x,     170+y,     3, 3, balldarcolor[0])
			end
			v.drawFill(290+x*3/4, 170+y*3/4, 3, 3, balldarcolor[1])
			v.drawFill(290+x/2,   170+y/2,   3, 3, balldarcolor[1])
			v.drawFill(290+x/4,   170+y/4,   3, 3, balldarcolor[1])
			if not (depthcheck) then
				v.drawFill(290+x,     170+y,     3, 3, balldarcolor[0])
			end
		end
	end

	-- Do hud tracking
	if cv_track.value == 2 then
		for p in players.iterate do
			if p.ctfteam == player.ctfteam then
				if not p.mo then continue end
				local x, y, scale = R_GetScreenCoords(player, camera, p.mo.x, p.mo.y, p.mo.z + 20*p.mo.scale)
				if x < 0 or x > 320*FRACUNIT or y < 0 or y > 200*FRACUNIT or scale > 100*FRACUNIT then continue end
				v.drawScaled(x, y, scale, v.cachePatch("CROSHAI1"), V_40TRANS)
				local h = "small"
				if scale > FRACUNIT/2 then
					h = "left"
				end
				v.drawString(x/FRACUNIT+1, y/FRACUNIT+1, p.name, V_ALLOWLOWERCASE|V_40TRANS, h)
			end
		end
	end
	if cv_track.value > 0 then
		if theball and theball.valid then
			local x, y, scale = R_GetScreenCoords(player, camera, theball.x, theball.y, theball.z + 16*theball.scale)
			if x < 0 or x > 320*FRACUNIT or y < 0 or y > 200*FRACUNIT then return end
			v.drawScaled(x, y, scale/2 + (3*FRACUNIT/2), v.cachePatch("CROSHAI1"), V_20TRANS)
		end
	end
end)

-- Let players charge a spindash during countdown
function SpinRevHack(mo)

	if ((mo.player.charability2 == CA2_SPINDASH) and not (mo.player.pflags & PF_SLIDING) and not mo.player.exiting
		and not P_PlayerInPain(mo.player) and (P_IsObjectOnGround(mo)))
	then
		--print("I can spindash! " .. mo.player.cmd.buttons .. " " .. mo.player.pflags)
		if ((mo.player.cmd.buttons & BT_USE) and (not mo.player.dashspeedhax))
		then
			--print("I started to spindash!")
			mo.player.pflags = $1|PF_STARTDASH|PF_SPINNING;
			mo.player.dashspeedhax = FixedMul(mo.player.mindash, mo.scale);
			mo.player.dashtimehax = 0;
			mo.state = (S_PLAY_SPINDASH);

		elseif ((mo.player.cmd.buttons & BT_USE) and (mo.player.dashspeedhax))
		then
			--print("I'm still spindashing!")
			mo.player.dashspeedhax = $1+FixedMul(FRACUNIT, mo.scale);

			mo.player.dashtimehax = $1+1
			if (not (mo.player.dashtimehax % 5))
			then
				if (mo.player.dashspeedhax < FixedMul(mo.player.maxdash, mo.scale)) then
					S_StartSound(mo, sfx_spndsh); -- Make the rev sound!
				end

				-- Now spawn the color thok circle (for characters who have such an item)
				P_SpawnMobj(mo.x, mo.y, mo.z-16*FRACUNIT/3, mo.player.revitem).color = mo.color
				-- and the spindash dust (also for characters who have this)
				if not (mo.player.charflags & SF_NOSPINDASHDUST)
					if not (mo.eflags & MFE_GOOWATER) then
						for i=(leveltime%7)/2,(leveltime%7) do
							local particle = P_SpawnMobj(mo.x, mo.y,
							mo.z + ((mo.eflags & MFE_VERTICALFLIP)/MFE_VERTICALFLIP * (mo.height - mobjinfo[MT_SPINDUST].height)),
							MT_SPINDUST)
							if (mo.player.powers[pw_shield] == SH_ELEMENTAL and not (mo.eflags & MFE_TOUCHWATER or mo.eflags & MFE_UNDERWATER)) then
								particle.state = S_SPINDUST_FIRE1
								particle.bubble = false
							elseif (mo.eflags & MFE_TOUCHWATER or mo.eflags & MFE_UNDERWATER) then
								particle.state = S_SPINDUST_BUBBLE1
								particle.bubble = true
							else
								particle.state = S_SPINDUST1
								particle.bubble = false
							end
					
							particle.target = mo
							particle.eflags = $1 | (mo.eflags & MFE_VERTICALFLIP)
							particle.scale = mo.scale*2/3
							P_SetObjectMomZ(particle, mo.player.dashtimehax*FRACUNIT/50+P_RandomByte()<<10, false)
							P_InstaThrust(particle, mo.angle+(P_RandomRange(-ANG30/FRACUNIT, ANG30/FRACUNIT)*FRACUNIT), FixedMul(-mo.player.dashtimehax*FRACUNIT/12-1*FRACUNIT-P_RandomByte()<<11, mo.scale))
							P_TryMove(particle, particle.x+particle.momx, particle.y+particle.momy, true)
						end
					end
				end
			end

			mo.state = S_PLAY_SPINDASH;
		else
			mo.player.dashspeedhax = 0
			mo.player.dashtimehax = 0
			mo.state = S_PLAY_STND
		end
	end
end



-- Ball sync
--[[
local synccountdown = 22
local syncchecks = {}

local function digest(x, y, z, momx, momy, momz)
	return x*y>>4-z<<3+momz<<2*momy>>1+momx
end

COM_AddCommand("ballsync", function(player, x, y, z, momx, momy, momz, check)
	local c = digest(x, y, z, momx, momy, momz)
	x,y,z,momx,momy,momz,check = tonumber($),tonumber($),tonumber($),tonumber($),tonumber($),tonumber($),tonumber($)
	--print(c)
	--print(check)
	--print(c == check)
	if c ~= check then return end

	if not (theball and theball.valid) then return end

	P_MoveOrigin(theball, 26000*FRACUNIT, 0, 0)
	P_MoveOrigin(theball, x, y, z)
	theball.momx = momx
	theball.momy = momy
	theball.momz = momz
end, 1)

COM_AddCommand("ballcheck", function(player, check, t)
	if not (theball and theball.valid) then return end
	check = tonumber($)
	t = tonumber($)

	if syncchecks[t] ~= check then
		CONS_Printf(server, "Manually resyncing the ball.")
		COM_BufInsertText(server, ("ballsync %d %d %d %d %d %d %d"):format(
			theball.x, theball.y, theball.z, theball.momx, theball.momy, theball.momz,
			digest(theball.x, theball.y, theball.z, theball.momx, theball.momy, theball.momz)
		))
		synccountdown = 8
	end
end)

--[-[COM_AddCommand("desync", function(player)
	local test = R_PointToDist(theball.x, theball.y)
	theball.momx = test>>7
end)]-]

addHook("ThinkFrame", do
	if not (theball and theball.valid) then return end
	syncchecks[leveltime] = digest(theball.x, theball.y, theball.z, theball.momx, theball.momy, theball.momz)

	synccountdown = $1-1
	if synccountdown <= 0 then
		synccountdown = 128
		local cmd = ("ballcheck %d %d"):format(syncchecks[leveltime], leveltime)
		for p in players.iterate do
			COM_BufInsertText(p, cmd)
		end
	end
end)
]]