//sage lua

if (VERSION == 202) and (SUBVERSION < 14)
	local function blackout(v)
		v.drawFill()
		v.drawString(160, 100, ("this mod requires version 2.2.14 or higher"),V_REDMAP, "center")
	end
	hud.add(blackout, "title")
	hud.add(blackout, "game")
	error("Your copy of 2.2 is obsolete and not compatible with this mod. Please update to version 2.2.14 or higher.", 0)
end

//localise some globals
local TICRATE = TICRATE
local FRACUNIT = FRACUNIT
local FRACBITS = FRACBITS
local PF_THOKKED = PF_THOKKED
local PF_JUMPED = PF_JUMPED
local PF_NOJUMPDAMAGE = PF_NOJUMPDAMAGE
local PF_JUMPDOWN = PF_JUMPDOWN
local PF_SPINDOWN = PF_SPINDOWN
local pw_carry = pw_carry
local ANG30 = ANG30
local AST_ADD = AST_ADD
local AST_SUBTRACT = AST_SUBTRACT
local AST_MODULATE = AST_MODULATE

local wardrobe = "client/sage/fancy.cfg"
local snapfile = "client/sage/snap.cfg"
local fishstore = "client/sage/selfish.cfg"
local arena = "client/sage/pvp.cfg"
local paint = "client/sage/colour.cfg"

local sagefancy = CV_RegisterVar({
	name = "sagefancy",
	flags = CV_NETVAR|CV_CALL|CV_NOINIT,
	PossibleValue = CV_TrueFalse,
	defaultvalue = "True",
	func = function(cv)
		if (cv.value)
			print("Sage VFX are set to default potency.")
		else
			print("Sage VFX have a reduced potency.")
		end
		local file = io.openlocal(wardrobe, "wb")
		file:write(cv.value)
		file:close()
	end
})

local sagesnap = CV_RegisterVar({
	name = "sagesnap",
	defaultvalue = "On",
	flags = CV_CALL|CV_NOINIT,
	PossibleValue = CV_OnOff,
	func = function(cv)
		if (cv.value)
			print("Phase ability instant camera snap enabled.")
		else
			print("Phase ability instant camera snap disabled.")
		end
		local file = io.openlocal(snapfile, "wb")
		file:write(cv.value)
		file:close()
	end
})

local sageselfish = CV_RegisterVar({
	name = "sageselfish",
	defaultvalue = "No",
	flags = CV_NETVAR|CV_CALL|CV_NOINIT,
	PossibleValue = CV_YesNo,
	func = function(cv)
		if (cv.value)
			print("Selfish mode! Transfer beam boost only works on yourself.")
		else
			if P_RandomChance(FRACUNIT/34) then
				print("Griefer mode! Transfer beam boost interferes with all players.")
			else
				print("Selfless mode! Transfer beam boost works on all players.")
			end
		end
		local file = io.openlocal(fishstore, "wb")
		file:write(cv.value)
		file:close()
	end
})

local sagepvp = CV_RegisterVar({
	name = "sagepvp",
	flags = CV_NETVAR|CV_CALL|CV_NOINIT,
	PossibleValue = CV_OnOff,
	defaultvalue = "On",
	func = function(cv)
		if (cv.value)
			print("Transfer Beam now corrupts enemy players.")
		else
			print("Transfer Beam now treats all players equally.")
		end
		local file = io.openlocal(arena, "wb")
		file:write(cv.value)
		file:close()
	end
})

local disablefancy = false
local file = io.openlocal(wardrobe)
if file
	local num = file:read("*n")
	if num == 0 then
		disablefancy = true
	end
	file:close()
end

local disablesnap = false
local file = io.openlocal(snapfile)
if file
	local num = file:read("*n")
	if num == 0 then
		disablesnap = true
	end
	file:close()
end

local sellfish = false
local file = io.openlocal(fishstore)
if file
	local num = file:read("*n")
	if num == 1 then
		sellfish = true
	end
	file:close()
end

local disablearena = false
local file = io.openlocal(arena)
if file
	local num = file:read("*n")
	if num == 0 then
		disablearena = true
	end
	file:close()
end

if disablefancy then
	COM_BufInsertText(server, "sagefancy false")
end

if disablesnap then
	COM_BufInsertText(consoleplayer, "sagesnap off")
end

if sellfish then
	COM_BufInsertText(server, "sageselfish yes")
end

if disablearena then
	COM_BufInsertText(server, "sagepvp off")
end

local function SafeFreeslot(...)
	for _, item in ipairs({...})
		if rawget(_G, item) == nil
			freeslot(item)
		end
	end
end

if PSO then
	SafeFreeslot("MT_SAGEOVERLAY", "SKINCOLOR_SAGEBACK")
	skincolors[SKINCOLOR_SAGEBACK] = {
		name = "SAGEBACK",
		ramp = {30,28,26,24,22,20,18,16,14,12,10,8,6,4,2,0},
		invcolor = SKINCOLOR_RED,
		invshade = 3,
		chatcolor = V_INVERTMAP,
		accessible = false}
	addHook("PlayerSpawn", function(player)
		if not (player.mo) or (player.mo.skin != "sage") then return nil end
		local my = skins["sonic"]
		player.normalspeed = my.normalspeed
		player.jumpfactor = my.jumpfactor
		player.acceleration = my.acceleration
		player.accelstart = my.accelstart
		player.thrustfactor = my.thrustfactor
	end)
	return
end

SafeFreeslot(
	"MT_SAGEOVERLAY",
	"MT_SAGEGLITCH",
	"MT_SAGEFAKE",
	"MT_SAGESHOT",
	"MT_SAGEBIGSHOT",
	"MT_SAGEVIRUS",
	"MT_SAGEHIT",
	"MT_SAGEVFX",
	"MT_SAGEDISC",
	"MT_SAGEPLUSH",
	"MT_SAGEKNIFE",
	"S_SAGESHOT",
	"S_SAGEBIGSHOT",
	"S_SAGE_RUN",
	"S_SAGE_FIRE",
	"S_SAGEVFX_BIG",
	"S_SAGEVFX_TELEPORT",
	"S_SAGEVFX_SMOGBALL",
	"S_SAGEVFX_SMOGNEUT",
	"S_SAGEVFX_SMOGHORZ",
	"S_SAGEVFX_SMOGVERT",
	"S_SAGEVFX_SMOGSMOL",
	"S_SAGEVFX_LIGHTBALL",
	"S_SAGEVFX_PIX1",
	"S_SAGEVFX_PIX2",
	"S_SAGEVFX_PIX3",
	"S_SAGEVFX_PIX4",
	"S_SAGEVFX_PIX5",
	"S_SAGEVFX_XPL1",
	"S_SAGEVFX_XPL2",
	"S_SAGEVFX_XPL3",
	"S_SAGEVFX_XPL4",
	"S_SAGEVFX_XPLB",
	"S_SAGEVFX_XPLA",
	"S_SAGEDISC",
	"S_SAGEPLUSH",
	"S_SAGEKNIFE",
	"SKINCOLOR_SAGEGLOW",
	"SKINCOLOR_SAGEBACK",
	"SPR_SAGEBEAM",
	"SPR_SAGEWARP",
	"SPR_SAGESCRIBBLE",
	"SPR_SAGEDARKNESS",
	"SPR_SAGEDISC",
	"SPR_SAGEPLUSH",
	"SPR_SAGEKNIFE",
	"SPR_SAGECRWL",
	"sfx_sgtimr",
	"sfx_sgget1",
	"sfx_sgget2",
	"sfx_sgboom",
	"sfx_sghit",
	"sfx_sgbeam",
	"sfx_sgchrg",
	"sfx_sgpop",
	"sfx_sgfire",
	"sfx_sgbonk",
	"sfx_sgidle",
	"sfx_sgtele",
	"sfx_sgmrkt",
	"sfx_sgfrog",
	"sfx_tswtch")

--Sprite pain
for i = 1,5 SafeFreeslot("SPR_SGP"..i)end
for i = 1,4 SafeFreeslot("SPR_SGX"..i) end
for i = 0,5 SafeFreeslot("SPR_SGW"..i) end
SafeFreeslot("SPR_SGY1","SPR_SGY2")

mobjinfo[MT_SAGEOVERLAY] = {
	spawnstate = S_INVISIBLE,
	spawnhealth = 1000,
	radius = 14<<FRACBITS,
	height = 50<<FRACBITS,
	dispoffset = 1,
	flags = MF_SCENERY|MF_NOGRAVITY|MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT}

mobjinfo[MT_SAGEGLITCH] = {
	spawnstate = S_INVISIBLE,
	spawnhealth = 1000,
	radius = 14<<FRACBITS,
	height = 50<<FRACBITS,
	dispoffset = -1,
	flags = MF_SCENERY|MF_NOGRAVITY|MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT}
	
mobjinfo[MT_SAGEFAKE] = {
	doomednum = -1,
	spawnhealth = 1,
	spawnstate = S_INVISIBLE,
	radius = 14<<FRACBITS,
	height = 50<<FRACBITS,
	flags = MF_NOGRAVITY|MF_NOSECTOR}
	
mobjinfo[MT_SAGESHOT] = {
	doomednum = -1,
	spawnstate = S_SAGEVFX_LIGHTBALL,
	deathstate = S_NULL,
	seesound = sfx_sgfire,
	deathsound = sfx_sgpop,
	speed = 150<<FRACBITS,
	radius = 24<<FRACBITS,
	height = 36<<FRACBITS,
	flags = MF_NOGRAVITY|MF_BOUNCE}
	
mobjinfo[MT_SAGEBIGSHOT] = {
	doomednum = -1,
	spawnstate = S_SAGEVFX_LIGHTBALL,
	deathstate = S_NULL,
	seesound = sfx_sgbeam,
	deathsound = sfx_sgpop,
	speed = 90<<FRACBITS,
	radius = 24<<FRACBITS,
	height = 36<<FRACBITS,
	flags = MF_NOGRAVITY}

mobjinfo[MT_SAGEVIRUS] = {
	spawnstate = S_INVISIBLE,
	spawnhealth = 1000,
	radius = FRACUNIT,
	height = FRACUNIT,
	dispoffset = -1,
	flags = MF_SCENERY|MF_NOGRAVITY|MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT}
	
mobjinfo[MT_SAGEHIT] = {
	doomednum = -1,
	spawnhealth = 1,
	spawnstate = S_INVISIBLE,
	deathstate = S_NULL,
	radius = 20<<FRACBITS,
	height = 75<<FRACBITS,
	mass = DMG_ELECTRIC,
	flags = MF_SOLID|MF_NOGRAVITY|MF_NOSECTOR|MF_NOCLIPHEIGHT}
	
mobjinfo[MT_SAGEVFX] = {
	doomednum = -1,
	spawnstate = S_SAGEVFX_BIG,
	spawnhealth = 1000,
	speed = 11<<FRACBITS,
	radius = 11<<FRACBITS,
	height = 11<<FRACBITS,
	dispoffset = 1,
	flags = MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOCLIPHEIGHT|MF_NOCLIP}
	
mobjinfo[MT_SAGEDISC] = {
	spawnstate = S_SAGEDISC,
	spawnhealth = 1000,
	radius = 7<<FRACBITS,
	height = 7<<FRACBITS,
	flags = MF_NOBLOCKMAP|MF_MISSILE}
	
mobjinfo[MT_SAGEPLUSH] = {
	spawnstate = S_SAGEPLUSH,
	spawnhealth = 1000,
	radius = 14<<FRACBITS,
	height = 50<<FRACBITS,
	flags = MF_SCENERY|MF_NOGRAVITY|MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT}

mobjinfo[MT_SAGEKNIFE] = {
	doomednum = -1,
	spawnstate = S_SAGEKNIFE,
	seesound = sfx_sgbeam,
	speed = 50<<FRACBITS,
	radius = 13<<FRACBITS,
	height = 13<<FRACBITS,
	flags = MF_MISSILE|MF_NOGRAVITY|MF_NOBLOCKMAP}

states[S_SAGESHOT] = {SPR_SAGEBEAM, B|FF_FULLBRIGHT|FF_ADD, -1, nil, 15, 1, S_SAGESHOT}
states[S_SAGEBIGSHOT] = {SPR_SAGEBEAM, A|FF_FULLBRIGHT|FF_ADD, -1, nil, 15, 1, S_SAGEBIGSHOT}

states[S_SAGE_RUN] = {SPR_PLAY, SPR2_RUN|FF_ANIMATE, -1, NULL, 4, 2, S_SAGE_RUN}
states[S_SAGE_FIRE] = {SPR_PLAY, SPR2_FIRE, 5, NULL, S_SAGE_FIRE, 0, S_PLAY_JUMP}

states[S_SAGEVFX_BIG] = {SPR_SAGESCRIBBLE,FF_FULLBRIGHT|FF_ANIMATE|FF_ADD,16,nil,H,2,S_NULL}
states[S_SAGEVFX_TELEPORT] = {SPR_SAGEWARP,FF_SEMIBRIGHT|FF_ANIMATE,12,nil,D,3,S_NULL}

states[S_SAGEVFX_SMOGBALL] = {SPR_SGW0,FF_SEMIBRIGHT|FF_ANIMATE|FF_TRANS50,24,nil,H,3,S_NULL}
states[S_SAGEVFX_SMOGNEUT] = {SPR_SGW1,FF_SEMIBRIGHT|FF_ANIMATE|FF_TRANS50,21,nil,G,3,S_NULL}
states[S_SAGEVFX_SMOGHORZ] = {SPR_SGW2,FF_SEMIBRIGHT|FF_ANIMATE|FF_TRANS50,15,nil,E,3,S_NULL}
states[S_SAGEVFX_SMOGVERT] = {SPR_SGW3,FF_SEMIBRIGHT|FF_ANIMATE|FF_TRANS50,18,nil,F,3,S_NULL}
states[S_SAGEVFX_LIGHTBALL] = {SPR_SGW5,FF_FULLBRIGHT|FF_ANIMATE|FF_ADD,-1,nil,H,2,S_NULL}

states[S_SAGEVFX_PIX1] = {SPR_SGP1,FF_FULLBRIGHT|FF_ANIMATE,18,nil,I,2,S_NULL}
states[S_SAGEVFX_PIX2] = {SPR_SGP2,FF_FULLBRIGHT|FF_ANIMATE,18,nil,I,2,S_NULL}
states[S_SAGEVFX_PIX3] = {SPR_SGP3,FF_FULLBRIGHT|FF_ANIMATE,18,nil,I,2,S_NULL}
states[S_SAGEVFX_PIX4] = {SPR_SGP4,FF_FULLBRIGHT|FF_ANIMATE,18,nil,I,2,S_NULL}
states[S_SAGEVFX_PIX5] = {SPR_SGP5,FF_FULLBRIGHT|FF_ANIMATE,20,nil,J,2,S_NULL}

states[S_SAGEVFX_XPL1] = {SPR_SGX1,FF_SEMIBRIGHT|FF_ANIMATE,16,nil,P,1,S_NULL}
states[S_SAGEVFX_XPL2] = {SPR_SGX2,FF_SEMIBRIGHT|FF_ANIMATE,16,nil,P,1,S_NULL}
states[S_SAGEVFX_SMOGSMOL] = {SPR_SGW4,FF_SEMIBRIGHT|FF_ANIMATE|FF_TRANS50,15,nil,E,3,S_NULL}

states[S_SAGEVFX_XPL3] = {SPR_SGX3,FF_SEMIBRIGHT|FF_ANIMATE,16,nil,P,1,S_NULL}
states[S_SAGEVFX_XPL4] = {SPR_SGX4,FF_SEMIBRIGHT|FF_ANIMATE,16,nil,P,1,S_NULL}

states[S_SAGEVFX_XPLB] = {SPR_SGY1,FF_FULLBRIGHT|FF_ANIMATE,16,nil,P,1,S_NULL}
states[S_SAGEVFX_XPLA] = {SPR_SGY2,FF_FULLBRIGHT|FF_ANIMATE,16,nil,P,1,S_NULL}

states[S_SAGEDISC] = {SPR_SAGEDISC, A|FF_SEMIBRIGHT|FF_ANIMATE, -1, nil, 4, 2, S_NULL}

states[S_SAGEPLUSH] = {SPR_SAGEPLUSH, A, -1, nil, nil, nil, S_NULL}
states[S_SAGEKNIFE] = {SPR_SAGEPLUSH, B, -1, nil, nil, nil, S_NULL}

states[S_PLAY_GASP].nextstate = S_PLAY_FALL

skincolors[SKINCOLOR_SAGEGLOW] = {
    name = "SAGEGLOW",
    ramp = {52,33,34,35,36,37,39,40,41,42,43,44,45,71,46,47},
    invcolor = SKINCOLOR_CYAN,
    invshade = 16,
    chatcolor = V_REDMAP,
    accessible = false}
	
skincolors[SKINCOLOR_SAGEBACK] = {
    name = "SAGEBACK",
    ramp = {30,28,26,24,22,20,18,16,14,12,10,8,6,4,2,0},
    invcolor = SKINCOLOR_SAGEGLOW,
    invshade = 3,
    chatcolor = V_INVERTMAP,
    accessible = false}

local function sagergb(player, dye, save)
	if (gamestate != GS_LEVEL) then
		CONS_Printf(player, "You must be in a level to use this.")
		return false
	end
	if not (player.sage) then
		CONS_Printf(player, "Change your character to Sage first.")
		return false
	end
	if not (dye) then
		if (save)
			CONS_Printf(player, "sagedefaultcolor <colour> - Pick and save a default colour for Sage's special effects.")
		else
			CONS_Printf(player, "sagecolor <colour> - Pick a colour for Sage's special effects.")
		end
		return false
	end
	local goback = {}
	goback["none"] = true
	goback["default"] = true
	goback["0"] = true
	goback["false"] = true
	goback["off"] = true
	goback["no"] = true
	goback["remove"] = true
	goback["begone"] = true
	local namethycolour = R_GetColorByName(dye)
	if goback[dye] or (namethycolour == SKINCOLOR_SAGEGLOW) or (namethycolour == SKINCOLOR_RED)
		player.effectsagecolour = SKINCOLOR_SAGEGLOW
		player.sage.color = SKINCOLOR_SAGEGLOW
		CONS_Printf(player, "Reset colour to red.")
		return true
	elseif (namethycolour == SKINCOLOR_NONE)
		CONS_Printf(player, "Colour "..dye.." not found.")
		return false
	else
		player.effectsagecolour = namethycolour
		player.sage.color = namethycolour
		CONS_Printf(player, "Sage's effects are now "..dye.." coloured.")
		return true
	end
end
	
COM_AddCommand("sagecolor", function(player, dye)
	sagergb(player, dye, false)
end)

COM_AddCommand("sagedefaultcolor", function(player, dye)
	if sagergb(player, dye, true) then
		local file = io.openlocal(paint, "wb")
		file:write(dye)
		CONS_Printf(player, "Saved effect colour as default.")
		file:close()
	end
end)

//buggie & goldenshine vfx

local function SageEffect(mo,effect,ang,cusr,cush,zoffs) --Effects function
	if not sagefancy.value or not mo.player then return nil end

	local f,v
	local a = ang or R_PointToAngle2(0,0,mo.momx,mo.momy)
	
	local cr = cusr or mo.radius
	local ch = cush or mo.height
	cr = FixedDiv($,mo.scale)
	ch = FixedDiv($,mo.scale)
	
	local color = (gametyperules & GTR_TEAMS) and mo.color or mo.player.sage.color
	
	local rr,ra
	local rx,ry,rz
	rz = zoffs or 0
	rz = FixedDiv($,mo.scale)

	if effect == "aura_basic"
		local aa = 0
		for i = 0,1
			rr = P_RandomRange(0,12)*FRACUNIT
			ra = P_RandomKey(360)*ANG1+aa
			if abs(ra-mo.angle) >= ANGLE_90
				aa = $+ANGLE_180
			end
			rx = P_ReturnThrustX(mo,ra,rr)
			ry = P_ReturnThrustY(mo,ra,rr)
			rz = $+(P_RandomRange(0,FixedInt(2*ch/3))*FRACUNIT -ch/6)
				
			local aura = {
				S_SAGEVFX_SMOGNEUT,S_SAGEVFX_SMOGHORZ,
				S_SAGEVFX_SMOGVERT,S_SAGEVFX_SMOGSMOL}
				
			local e = P_SpawnMobjFromMobj(mo,rx,ry,rz,MT_SAGEVFX)
			e.state = aura[P_RandomRange(1,4)]
			e.cusval = 1
			e.color = color
			e.scale = FixedMul(min(max(FRACUNIT*P_RandomRange(1,3)/P_RandomRange(1,5),FRACUNIT),FRACUNIT*3/2),mo.scale)
			e.momx = mo.momx*2/3
			e.momy = mo.momy*2/3
			e.momz = mo.momz*2/3
			e.dontdrawforviewmobj = mo
				
			if (i&1) then v = e else f = e end
		end
	elseif effect == "aura_pixels"
		rr = P_RandomRange(0,FixedInt(ch/3*2))*FRACUNIT
		ra = P_RandomKey(360)*ANG1
		rx = P_ReturnThrustX(mo,ra,rr)
		ry = P_ReturnThrustY(mo,ra,rr)
		rz = $+(ch/5+P_RandomRange(0,FixedInt(ch))*FRACUNIT)
		
		local pixels = {
		S_SAGEVFX_PIX1,
		S_SAGEVFX_PIX2,
		S_SAGEVFX_PIX3,
		S_SAGEVFX_PIX4,
		S_SAGEVFX_PIX5}
			
		f = P_SpawnMobjFromMobj(mo,rx,ry,rz,MT_SAGEVFX)
		f.state = pixels[P_RandomRange(1,5)]
		f.color = color
		f.dontdrawforviewmobj = mo
	elseif effect == "aura_pixel"
		rr = P_RandomRange(FRACUNIT>>FRACBITS,(3*FRACUNIT)>>FRACBITS)<<FRACBITS
		ra = P_RandomKey(360)*ANG1
		rz = $+(P_RandomRange((FRACUNIT/6)>>FRACBITS,FRACUNIT>>FRACBITS)<<FRACBITS)
			
		f = P_SpawnMobjFromMobj(mo,0,0,0,MT_SAGEVFX)
		f.state = S_SAGEVFX_PIX1
		f.tics = 64
		f.anim_duration = -1
		f.color = color
		f.cusval = 2
		f.blendmode = AST_SUBTRACT
		f.scale = mo.scale
		f.scalespeed = mo.scale/92
		f.destscale = mo.scale/64
		f.dontdrawforviewmobj = mo
		P_InstaThrust(f,ra,FixedMul(rr,mo.scale))
		P_SetObjectMomZ(f,rz)
	elseif effect == "lightball_xfx"
		rz = $+(P_RandomRange(0,(ch/3*2)>>FRACBITS)<<FRACBITS -ch/6)
		f = P_SpawnMobjFromMobj(mo,0,0,rz,MT_SAGEVFX)
		f.state = S_SAGEVFX_BIG
		f.color = color
	elseif effect == "teleport"
		f = P_SpawnMobjFromMobj(mo,0,0,rz,MT_SAGEVFX)
		f.state = S_SAGEVFX_TELEPORT
		f.color = color
		f.dispoffset = 3
		f.dontdrawforviewmobj = mo
	end
end

local function SageExplosion(mo, small, color)
	--GS: These must be explosions then....
	--Screw making it look nice.
	local SETSTATE = (small) and S_SAGEVFX_XPL3 or S_SAGEVFX_XPL1
	local f = P_SpawnMobjFromMobj(mo,0,0,0,MT_SAGEVFX)
	if f and f.valid
		f.state = SETSTATE
		if (small)
			f.scale = $/3
		end
		f.renderflags = RF_SEMIBRIGHT|RF_NOCOLORMAPS
		f.color = color or SKINCOLOR_SAGEGLOW
		f.dispoffset = 1
		f.SAGEexplosion = true
		f.blendmode = AST_ADD
		f.frame = ($ & ~FF_TRANSMASK)|FF_TRANS10
		--Spawn pixels.
		for i = 1,8
			local ghs = P_SpawnMobjFromMobj(f,
			FixedMul(99*mo.scale, cos(mo.angle+ANGLE_45*i)),
			FixedMul(99*mo.scale, sin(mo.angle+ANGLE_45*i)),
			0, MT_SAGEVFX)
			ghs.renderflags = RF_SEMIBRIGHT|RF_NOCOLORMAPS
			ghs.color = f.color
			ghs.scale = $*4/3
			if (i % 2 == 0)
				ghs.state = S_SAGEVFX_XPLA
			else
				ghs.state = S_SAGEVFX_XPLB
			end
			local ths = SageEffect(f,"aura_pixel", nil, nil, nil, FixedDiv(f.height,mo.scale)/2)
			if ths and ths.valid
				local RNG = P_RandomRange(40,90)
				ths.scale = $*15/3
				P_Thrust(ths, mo.angle+(ANGLE_45*i), RNG*6535)
				ths.momz = (-55+RNG)*4535*P_MobjFlip(mo)
				ths.frame = ($ & FF_TRANSMASK)|FF_TRANS10
				if i % 2 == 0
					ths.spritexscale = $+((-40+RNG)*999)
				else
					ths.spriteyscale = $+((-40+RNG)*999)
				end
				if i % 3 == 0
					ths.color = SKINCOLOR_SUPERSILVER1
					ths.colorized = true
				end
				ths.fuse = 59
			end
			local ghs = P_SpawnMobjFromMobj(f,
			FixedMul(199*f.scale, cos(mo.angle+ANGLE_22h+ANGLE_45*i)),
			FixedMul(199*f.scale, sin(mo.angle+ANGLE_22h+ANGLE_45*i)),
			0, MT_SAGEVFX)
			ghs.renderflags = RF_PAPERSPRITE|RF_SEMIBRIGHT|RF_NOCOLORMAPS
			ghs.blendmode = AST_ADD
			ghs.angle = R_PointToAngle2(ghs.x, ghs.y, f.x, f.y)+ANGLE_90
			ghs.color = SKINCOLOR_COBALT
			ghs.scale = $<<2
			ghs.sprite = SPR_SAGEDARKNESS
			ghs.frame = A|FF_ANIMATE|FF_TRANS10
			ghs.tics = 15
			ghs.fuse = 15
			P_Thrust(ghs, mo.angle+ANGLE_22h+ANGLE_45*i, 6*mo.scale)
			ghs.momz = mo.scale*4*P_MobjFlip(mo)
		end
		for i = 1,4
			local ghs = P_SpawnMobjFromMobj(f,0,0,mo.height/2, MT_SAGEVFX)
			ghs.renderflags = RF_SEMIBRIGHT|RF_NOCOLORMAPS|RF_ABSOLUTEOFFSETS
			ghs.blendmode = AST_ADD
			ghs.spriteyoffset = 14*FRACUNIT
			ghs.spritexoffset = 16*FRACUNIT
			ghs.color = i == 2 and SKINCOLOR_VAPOR or i == 3 and SKINCOLOR_DUSK or SKINCOLOR_CRIMSON
			ghs.scale = (small) and $*5/3 or $*3
			ghs.dispoffset = 8
			P_SetObjectMomZ(ghs, 2<<FRACBITS, false)
			ghs.destscale = $<<4
			ghs.scalespeed = FixedMul(FRACUNIT>>1, f.scale)*i
			ghs.sprite = SPR_SAGEDARKNESS
			ghs.frame = A|FF_TRANS10
			ghs.tics = 12+i
			ghs.fuse = 12+i	
			if i == 4
				ghs.renderflags = $|RF_FLOORSPRITE
				ghs.flags2 = $|MF2_SPLAT
				ghs.fuse = $+3
				ghs.tics = $+3
				P_SetObjectMomZ(ghs, FRACUNIT, true)
				if not (small) //reduce size of ring for big explosion to not flashbang the player
					ghs.spriteyscale = $>>1
					ghs.spritexscale = $>>1
				end
			end
		end
	end
end

//overlay

local swaps = {} //list of colors which trigger sage's main colour inversion
swaps[SKINCOLOR_CYAN] = true
swaps[SKINCOLOR_SKY] = true
swaps[SKINCOLOR_AQUA] = true
swaps[SKINCOLOR_AQUAMARINE] = true
swaps[SKINCOLOR_TURQUOISE] = true

local function debuff(player, battle)
	if (player.gotflag) then return true end
	if battle and (player.gotcrystal) then return true end
	return false
end

local function copycat(copy, follow, blend) //copies stuff for the overlay glitches and secondcolor
	copy.target = follow
	copy.angle = follow.angle
	copy.color = follow.color
	copy.skin = follow.skin
	copy.sprite = follow.sprite
	copy.frame = follow.frame
	copy.sprite2 = follow.sprite2
	copy.blendmode = blend
	copy.flags2 = follow.flags2
	copy.rollangle = follow.rollangle
	copy.spritexoffset = follow.spritexoffset
	copy.spriteyoffset = follow.spriteyoffset
	copy.spritexscale = follow.spritexscale
	copy.spriteyscale = follow.spriteyscale
	copy.fuse = P_RandomRange(3, 5)
end

local MAX_POP = 3*TICRATE
local FULL_CHARGE = TICRATE>>1

addHook("FollowMobj", function(player, follow)
	if not (player.sage) return false end
	local pmo = player.mo
	if (pmo.flags2 & MF2_DONTDRAW) return false end
	if not follow.eventsagecountdown
		follow.eventsagecountdown = TICRATE*2
	end
	follow.skin = "sage"
	follow.sprite = SPR_PLAY
	follow.frame = pmo.frame|FF_FULLBRIGHT
	follow.sprite2 = pmo.sprite2|FF_SPR2SUPER
	follow.rollangle = pmo.rollangle
	follow.scale = pmo.scale
	follow.spritexoffset = pmo.spritexoffset
	follow.spriteyoffset = pmo.spriteyoffset
	follow.spritexscale = pmo.spritexscale
	follow.spriteyscale = pmo.spriteyscale
	follow.translation = nil
	if (gametyperules & GTR_TEAMS)
		local teampaint = SKINCOLOR_SAGEGLOW
		if (player.ctfteam == 2)
			pmo.color = SKINCOLOR_WHITE
			teampaint = SKINCOLOR_CYAN
		else
			pmo.color = SKINCOLOR_BLACK
		end
		player.sage.color = teampaint
		follow.color = teampaint
		return false
	end
	if (player.powers[pw_super])
		if not (leveltime % 4)
			follow.color = (follow.color < SKINCOLOR_SANGRIA) and $+1 or SKINCOLOR_RUBY
			player.sage.color = follow.color
		end
		return false
	else
		player.sage.color = player.effectsagecolour
	end
	if (player.sage.grab >= FULL_CHARGE) and ((leveltime%5) == 0)
		follow.color = (player.sage.color > SKINCOLOR_CLOUDY) and SKINCOLOR_WHITE or SKINCOLOR_BLACK
		return false
	end
	if ((player.powers[pw_shield] & SH_STACK) == SH_FIREFLOWER)
		pmo.color = SKINCOLOR_CLOUDY
		player.sage.color = SKINCOLOR_TANGERINE
	end
	if follow.myrainbow
		follow.myrainbow = $-1
	else
		follow.color = player.sage.color
	end
	if follow.blended
		follow.blended = $-1
	else
		follow.blendmode = pmo.blendmode
	end
	follow.eventsagecountdown = $ - P_RandomRange(1, 3)
	if not (follow.eventsagecountdown % 13) or not (follow.eventsagecountdown)
		local copy = P_SpawnMobjFromMobj(follow, P_RandomRange(-4, 4)<<FRACBITS, P_RandomRange(-4, 4)<<FRACBITS, 0, MT_SAGEGLITCH)
		copycat(copy, follow, AST_ADD)
	end
	if follow.eventsagecountdown <= 0
		follow.eventsagecountdown = P_RandomRange(TICRATE, TICRATE*3)
		local glitchchance = P_RandomByte()
		if glitchchance < 120
			if (player.sage.color == SKINCOLOR_SAGEGLOW)
				follow.blendmode = AST_ADD
				follow.blended = P_RandomRange(3, 5)
			else
				local copy = P_SpawnMobjFromMobj(pmo, 0, 0, 0, MT_SAGEOVERLAY)
				copycat(copy, follow, AST_ADD)
				copy.flags2 = $|MF2_LINKDRAW
			end
		elseif glitchchance < 240
			if (player.sage.color == SKINCOLOR_SAGEGLOW)
				follow.blendmode = AST_MODULATE
				follow.blended = P_RandomRange(3, 5)
			else
				local copy = P_SpawnMobjFromMobj(pmo, 0, 0, 0, MT_SAGEOVERLAY)
				copycat(copy, follow, AST_MODULATE)
				copy.flags2 = $|MF2_LINKDRAW
			end
		else
			follow.color = swaps[player.sage.color] and SKINCOLOR_SAGEGLOW or ColorOpposite(follow.color)
			follow.myrainbow = P_RandomRange(3, 5)
		end
	end
	return false
end, MT_SAGEOVERLAY)

addHook("MobjThinker", function(glitch)
	if glitch.target and glitch.target.valid then
		P_SetOrigin(glitch, glitch.target.x, glitch.target.y, glitch.target.z)
	end
end, MT_SAGEGLITCH)

//ability

local function thanos(player)
	if not (sagesnap.value) or not P_IsLocalPlayer(player) then return false end
	if camera2 and (player == secondarydisplayplayer)
		P_ResetCamera(player, camera2)
	else
		P_ResetCamera(player, camera)
	end
end

addHook("AbilitySpecial", function(player)
	if not (player.sage) return nil end
	if (player.sage.control) or (player.sage.deathtimer) or (player.playerstate != PST_LIVE) return true end
	if (player.sage.pop or player.powers[pw_super]) and (player.pflags & PF_THOKKED) and not (player.pflags & PF_SHIELDABILITY) and not (player.glidetime)
		player.pflags = $ & ~PF_THOKKED
		player.sage.pop = 0
		player.glidetime = 9
	end
	if (player.pflags & (PF_THOKKED|PF_SHIELDABILITY)) return true end
	local pmo = player.mo
	local dummy = P_SpawnMobjFromMobj(pmo, 0, 0, 0, MT_SAGEFAKE) //spawn sage-like object to test collision
	dummy.target = pmo
	pmo.state = S_PLAY_JUMP
	player.pflags = $|PF_THOKKED
	pmo.flags = $|MF_NOCLIP|MF_NOCLIPTHING
	player.sage.grab = 0
	dummy.originx = pmo.x
	dummy.originy = pmo.y
	dummy.originz = pmo.z
	if (player.pflags & PF_SPINDOWN)
		dummy.down = true
		player.sage.down = 3
		P_SetObjectMomZ(dummy, -256<<FRACBITS, false)
	elseif (player.cmd.forwardmove) or (player.cmd.sidemove)
		dummy.redirect = true
		local alreadywon = false
		if (player.pflags & PF_FINISHED) //so sage doesn't get blocked by exit sectors if she already finished the level
			alreadywon = true
		end
		for t = 1, 13 //take a bunch of steps on the same tic to check for obstructions
			if not P_TryMove(dummy, dummy.x+FixedMul(45<<FRACBITS, cos(player.drawangle)), dummy.y+FixedMul(45<<FRACBITS, sin(player.drawangle)), true)
				break
			end
			if not (alreadywon) and (GetSecSpecial(dummy.subsector.sector.special, 4) == 2)
				break
			end
		end
	else //z movement is nice and actually blocks us properly
		P_SetObjectMomZ(dummy, 256<<FRACBITS, false)
	end
	dummy.fuse = 2
	return true
end)

local function maybebustableblock(mobj, sector)
	for fof in sector.ffloors()
		if not (fof.fofflags & FOF_EXISTS)
			continue
		end
		if (mobj.z+mobj.height < fof.bottomheight) or (mobj.z > fof.topheight)
			continue
		end
		if ((fof.fofflags & FOF_BLOCKPLAYER) and not (fof.fofflags & FOF_BLOCKOTHERS)) or P_CheckSolidLava(mobj, fof)
			mobj.bonk = true
			return false
		end
		if ((fof.fofflags & FOF_BLOCKOTHERS) and not (fof.fofflags & FOF_BLOCKPLAYER))
			mobj.nobonk = true
			return false
		end
		if not (fof.fofflags & FOF_BUSTUP)
			continue
		end 
		EV_CrumbleChain(fof)
		local line = fof.master
		if line.flags & ML_EFFECT5
			P_LinedefExecute(P_AproxDistance(line.dx, line.dy) >> FRACBITS, mo, fof.target)
		end
		return true
	end
end

local function exchange(lost, found)
	if (found.flags & (MF_SOLID|MF_SHOOTABLE)) and (lost != found) and (lost.target != found) and (found.type != MT_SAGEHIT)
		if (found.z > lost.z+lost.height) or (lost.z > found.z+found.height)
			return
		end
		if (found.player)
			P_SpawnGhostMobj(found)
			P_SetOrigin(found, lost.originx, lost.originy, lost.originz)
			S_StartSound(found, sfx_telept, found.player)
			return
		end
		if abs(R_PointToDist2(lost.x, lost.y, found.x, found.y)) > (found.radius+lost.radius) //radius is more strict for following conditions
			return
		end
		if (found.flags & MF_SHOOTABLE) and not (found.flags & MF_NOCLIPTHING)
			P_DamageMobj(found, lost, lost.target, 2, DMG_ELECTRIC)
			return
		end
		if (found.type == MT_SPIKE) or (found.type == MT_WALLSPIKE)
			P_KillMobj(found, lost, lost.target)
		end
	end
end

local function telescan(lost, found)
	if (found.type == MT_SAGESHOT) and (found.target == lost.target) and not (found.notouchy) and P_CheckSight(lost.target, found)
		lost.moveme = found
		return true
	end
	if (found.sagetagged) and (found.sagetagged.valid)
		found.flags = $|MF_NOCLIPTHING
		found.sagetagged.timer = 0
		found.momx = 0
		found.momy = 0
		lost.bop = true
		lost.moveme = found
		return true
	end
end

local function crashtestdummy(dummy) //handles setting sage's teleport destination
	if not (dummy.target) or not (dummy.target.player) or not (dummy.target.player.sage)
		pcall(P_RemoveMobj, dummy)
		return false
	end
	if maybebustableblock(dummy, dummy.subsector.sector)
		P_SetOrigin(dummy, dummy.x, dummy.y, dummy.z) //srb2 PLEASE
		if (dummy.down)
			P_SetObjectMomZ(dummy, -128<<FRACBITS, false)
		else
			P_SetObjectMomZ(dummy, 128<<FRACBITS, false)
		end
		dummy.fuse = 2
		return true
	end
	local flash = P_SpawnMobjFromMobj(dummy.target, 0, 0, 0, MT_SAGEOVERLAY)
	copycat(flash, dummy.target, AST_SUBTRACT)
	flash.color = dummy.target.player.sage.color
	flash.colorized = true
	SageEffect(flash,"teleport")
	local dist = 300<<FRACBITS
	if not (searchBlockmap("objects", telescan, dummy, dummy.x-dist, dummy.x+dist, dummy.y-dist, dummy.y+dist)) and P_CheckSight(dummy.target, dummy.moveme)
		if (dummy.moveme.valid and dummy.moveme.type == MT_SNAILER)
			P_SetOrigin(dummy, dummy.moveme.x+FixedMul(17*dummy.moveme.scale, cos(dummy.moveme.angle)), dummy.moveme.y+FixedMul(17*dummy.moveme.scale, sin(dummy.moveme.angle)), dummy.moveme.z)
		else
			P_SetOrigin(dummy, dummy.moveme.x, dummy.moveme.y, dummy.moveme.z)
		end
		S_StartSound(dummy.target, sfx_sgget2)
		if (dummy.bop)
			P_SetObjectMomZ(dummy.target, 15<<FRACBITS, false)
			if (dummy.target.eflags & MFE_UNDERWATER)
				dummy.target.momz = $/2
			end
			P_ResetPlayer(dummy.target.player)
			dummy.target.player.pflags = $|P_GetJumpFlags(dummy.target.player) & ~PF_THOKKED & ~PF_STARTJUMP
			dummy.target.state = S_PLAY_JUMP
			dummy.target.player.sage.pop = MAX_POP
			dummy.target.player.sage.stock = 1
			dummy.target.player.glidetime = 0
		end
	end
	P_SetOrigin(dummy.target, dummy.x, dummy.y, dummy.z)
	if not (dummy.bop)
		dummy.target.momz = 0
	end
	dummy.target.player.sage.flicker = 8
	dummy.target.flags = $|MF_NOCLIP
	if (dummy.redirect)
		P_InstaThrust(dummy.target, dummy.target.player.drawangle, dummy.target.player.speed)
	end
	dummy.target.player.powers[pw_justsprung] = 0
	SageEffect(dummy.target,"teleport")
	S_StartSound(dummy.target, sfx_sgtele)
	searchBlockmap("objects", exchange, dummy)
	dummy.target.flags = $ & ~MF_NOCLIPTHING
	thanos(dummy.target.player)
	dummy.missioncomplete = true
	pcall(P_RemoveMobj, dummy)
end

addHook("MobjFuse", crashtestdummy, MT_SAGEFAKE)
addHook("MobjMoveBlocked", crashtestdummy, MT_SAGEFAKE)

addHook("MobjRemoved", function(crash)
	if crash.missioncomplete return false end
	//stupid death pit interrupted the dummy
	if crash.target and crash.target.player
		crash.target.z = (crash.target.eflags & MFE_VERTICALFLIP) and crash.target.ceilingz or crash.target.floorz
		searchBlockmap("objects", exchange, crash)
		crash.target.flags = $ & ~MF_NOCLIPTHING
		thanos(crash.target.player)
		SageEffect(crash.target,"teleport")
	end
end, MT_SAGEFAKE)

addHook("SpinSpecial", function(player) //just detect the button press and do the rest later
	if not (player.sage) return nil end
	if (player.pflags & (PF_SPINDOWN|PF_SLIDING)) return nil end
	if P_PlayerInPain(player) return true end
	if (player.exiting) return nil end
	if (player.sage.control) or (player.sage.deathtimer) or (player.playerstate != PST_LIVE) return true end
	if (player.powers[pw_carry]) return nil end
	if not (player.powers[pw_super])
		if (player.mo.grassfedbeef and player.mo.grassfedbeef.valid)
			return nil
		end
		if not (player.sage.stock)
			return nil
		end
	end
	if (player.sage.pop) or (player.powers[pw_super])
		player.sage.grab = 2*FULL_CHARGE/3
	else
		player.sage.grab = 1
	end
	S_StartSound(player.mo, sfx_sgchrg)
	player.pflags = $ & ~PF_SPINNING
	player.mo.state = S_SAGE_FIRE
	return true
end)

addHook("JumpSpecial", function(player) //allows jumping out of sage's float
	if not (player.sage) return nil end
	if (player.pflags & PF_JUMPDOWN)
	or (player.exiting)
	or (player.sage.spring)
	or (player.sage.control)
	or (player.sage.deathtimer)
	or (player.playerstate != PST_LIVE)
	or (player.powers[pw_carry]) return nil end
	local pmo = player.mo
	if (pmo.eflags & MFE_APPLYPMOMZ) return nil end
	if ((player.panim <= PA_RUN) or (pmo.state == S_SAGE_RUN)) and not (player.pflags & PF_JUMPED) and not (player.powers[pw_carry])
		player.sage.grab = 0
		if (P_MobjFlip(pmo)*pmo.momz > 0) //retain all upwards momentum
			P_SetObjectMomZ(pmo, 10<<FRACBITS, true)
		else
			P_SetObjectMomZ(pmo, 10<<FRACBITS, false)
		end
		if (pmo.eflags & MFE_UNDERWATER) or P_InQuicksand(pmo)
			pmo.momz = $/2 //intentionally a bit less than normal
		end
		if (P_InJumpFlipSector(pmo))
			pmo.flags2 = $^^MF2_OBJECTFLIP
			S_StartSound(pmo, sfx_s3k73)
		end
		S_StartSound(pmo, sfx_jump)
		player.pflags = $|P_GetJumpFlags(player)|PF_STARTJUMP
		pmo.state = S_PLAY_JUMP
		return true
	else
		return false
	end
end)

//main hook

local function grabs(mobj)
	local springer = P_SpawnMobjFromMobj(mobj, 0, 0, -25<<FRACBITS, MT_SAGEHIT)
	springer.target = mobj
	springer.fuse = 2
	return springer
end

local function pof(mobj, you)
	S_StartSound(mobj, sfx_sgboom)
	mobj.flags = $|MF_ENEMY & ~MF_NOTHINK
	P_RadiusAttack(mobj, you, 256<<FRACBITS, DMG_NUKE, true)
	SageExplosion(mobj, false, you.player.sage.color)
	P_KillMobj(mobj, you, you)
end

local function badsectordetector(mobj)
	local check = P_SpawnMobjFromMobj(mobj,0,0,-(22<<FRACBITS)+mobj.momz,MT_THOK)
	check.flags = $|MF_NOSECTOR
	check.fuse = 2
	if P_CheckDeathPitCollide(check)
		return true
	end
	return false
end

local function sagefloatcheck(player, mobj)
	if (mobj.state == S_PLAY_JUMP)
	or (player.sage.down)
	or (player.panim == PA_PAIN)
	or (player.powers[pw_carry])
	or (player.onconveyor == 2)
	or (mobj.eflags & MFE_GOOWATER)
	or P_InQuicksand(mobj)
	or debuff(player, CBW_Battle)
	or badsectordetector(mobj) then return false end
	return true
end

local function sagefloatstuff(player, mobj)
	player.sage.stock = 1
	if (player.pflags & PF_JUMPED)
		P_ResetPlayer(player)
	end
	if S_SoundPlaying(mobj, sfx_sgidle)
		S_StopSoundByID(mobj, sfx_sgidle)
	end
	player.sage.oldz = mobj.z
	player.sage.spring = false
	if (player.panim == PA_FALL) or (player.panim == PA_IDLE) or ((mobj.state == S_SAGE_RUN) and (player.speed < FixedMul(player.runspeed, mobj.scale)))
		mobj.state = S_PLAY_WALK
	end
	if ((player.panim == PA_WALK) and (player.speed > FixedMul(player.runspeed, mobj.scale))) or (player.panim == PA_RUN)
		mobj.state = S_SAGE_RUN
	end
	if (player.cmd.forwardmove == 0) and (player.cmd.sidemove == 0) and (player.speed) and not P_IsObjectOnGround(mobj) and not (player.sage.grab)
		P_Thrust(mobj, R_PointToAngle2(0, 0, player.rmomx, player.rmomy), -FixedMul(FRACUNIT>>1, mobj.scale))
	end
end

local function SMFMteleport(mobj,newmobj,zoffs)
	if (mobj.eflags & MFE_VERTICALFLIP)
		local elementheight = FixedMul(newmobj.info.height,mobj.scale)
		return (mobj.z + mobj.height - zoffs - elementheight)
	else
		return mobj.z + zoffs
	end
end

addHook("PlayerSpawn", function(player)
	player.sage = nil
	player.pflags = $ & ~PF_INVIS
end)

addHook("PlayerThink", function(player)
	local pmo = player.mo
	if not (pmo) or (pmo.skin != "sage") or (player.playerstate != PST_LIVE)
		if (player.sage)
			if (pmo)
				pmo.flags = $ & ~MF_NOGRAVITY
				pmo.blendmode = AST_TRANSLUCENT
				pmo.translation = nil
			end
			player.pflags = $ & ~PF_INVIS
			if (player.sage.spinball)
				pcall(P_RemoveMobj, player.sage.spinball)
				player.sage.spinball = nil
			end
			player.sage = nil
		end
		return false
	end

	local p = player --GS: For my sanity.
	local s = pmo
	
	if not (player.sage) //set up sage table
		local savedcolour = nil
		local restore = io.openlocal(paint)
		if restore then
			local rgb = restore:read("*a")
			if rgb then
				savedcolour = R_GetColorByName(rgb)
			end
			restore:close()
		end
		if not (player.effectsagecolour) then
			if (savedcolour and R_GetColorByName(savedcolour) != (SKINCOLOR_NONE or SKINCOLOR_RED))
				player.effectsagecolour = savedcolour
			else
				player.effectsagecolour = SKINCOLOR_SAGEGLOW
			end
		end
		local sage = {
		color = player.effectsagecolour,
		oldz = 0,
		spring = false,
		stock = 1,
		grab = 0,
		flicker = 0,
		invert = false,
		control = 0,
		deathtimer = 0,
		pop = 0,
		down = 0,
		reset = true}
		player.sage = sage
	end
	
	if (leveltime%6) <= 0
		SageEffect(pmo,"aura_basic")
	end
	if P_RandomChance(FRACUNIT/4)
		SageEffect(pmo,"aura_pixel")
	end
	local speedchance = R_PointToDist2(0,player.speed,0,abs(pmo.momz))/300
	local px
	if P_RandomChance(FRACUNIT/4+speedchance)
		px = SageEffect(pmo,"aura_pixels")
	end
	if (player.sage.pop) and not (player.powers[pw_super])
		pmo.blendmode = AST_SUBTRACT
		player.pflags = $|PF_INVIS
		player.sage.pop = $-1
	elseif (pmo.blendmode == AST_SUBTRACT)
		pmo.blendmode = AST_TRANSLUCENT
		player.pflags = $ & ~PF_INVIS
	end
	player.secondjump = 6 //kill anti-airwalk scripts
	player.powers[pw_noautobrake] = 1
	if (player.sage.deathtimer)
		player.sage.flicker = 0
		pmo.flags = $|MF_NOCLIPTHING|MF_NOBLOCKMAP
		pmo.flags2 = $ & ~MF2_DONTDRAW
		player.sage.deathtimer = $+1
		if player.sage.deathtimer < TICRATE
			pmo.state = S_PLAY_STUN
			P_SetObjectMomZ(pmo, 3<<FRACBITS, false)
			if (leveltime%9) == 0
				SageExplosion(pmo, true, player.sage.color)
				S_StartSound(pmo,sfx_sgboom)
			end	
			if (leveltime & 3)
				pmo.colorized = true
				pmo.color = SKINCOLOR_WHITE
				pmo.blendmode = AST_ADD
			else
				pmo.colorized = false
				pmo.color = player.skincolor
				pmo.blendmode = AST_TRANSLUCENT
			end
		else
			pmo.colorized = false
			pmo.color = player.skincolor
			pmo.blendmode = 0
			pmo.state = S_PLAY_DEAD
			pmo.momy = 0
			pmo.momx = 0
			pmo.momz = 0
		end
		if player.sage.deathtimer == TICRATE*3/2
			if player.sage.spinball and player.sage.spinball.valid
				P_KillMobj(player.sage.spinball)
				player.sage.spinball = nil
			end
			P_SwitchShield(player, SH_NONE)
			P_RadiusAttack(pmo, pmo, 256<<FRACBITS, DMG_NUKE, true)
			SageExplosion(pmo, false, player.sage.color)
			S_StartSound(pmo,sfx_sgboom)
			pmo.colorized = false
			pmo.color = player.skincolor
			player.playerstate = PST_DEAD
			pmo.flags2 = $|MF2_DONTDRAW
			player.sage.deathtimer = 0
			local disc = P_SpawnMobjFromMobj(pmo, 0, 0, pmo.height, MT_SAGEDISC)
			P_SetObjectMomZ(disc, 7<<FRACBITS, false)
			P_Thrust(disc, P_RandomRange(1, 360)*ANG1, pmo.scale<<2)
			disc.fuse = TICRATE<<3
			if not multiplayer or ((CV_FindVar("cooplives").value) and (gametyperules & GTR_FRIENDLY) and not (player.pflags & PF_FINISHED)) //prevent game over in ringslinger
				player.lives = $-1
				if not (player.lives)
					P_PlayJingleMusic(player, "_gover", 0, false, JT_GOVER)
				end
			end
		end
		return
	end
	if (player.sage.flicker)
		if (player.sage.flicker % 2)
			pmo.flags2 = $ ^^ MF2_DONTDRAW
		end
		if (player.pflags & PF_JUMPED) and ((P_MobjFlip(pmo)*pmo.momz) < 0) then
			pmo.momz = 0
		end
		player.sage.flicker = $-1
	end
	if (player.sage.control)
		if player.sage.spinball and player.sage.spinball.valid
			P_KillMobj(player.sage.spinball)
			player.sage.spinball = nil
		end
		if player.sage.control.valid
			pmo.flags = $ & ~MF_NOGRAVITY
			pmo.flags2 = $|MF2_DONTDRAW
			P_MoveOrigin(player.sage.control, pmo.x, pmo.y, pmo.z)
			player.pflags = $ & ~PF_SPINNING
			player.sage.control.flags2 = $ & ~MF2_OBJECTFLIP //force disable reverse gravity because it doesn't work
			player.sage.control.eflags = $ & ~MFE_VERTICALFLIP
			pmo.flags2 = $ & ~MF2_OBJECTFLIP
			pmo.eflags = $ & ~MFE_VERTICALFLIP
		else
			player.sage.control = 0
		end
		pmo.friction = FRACUNIT*92/93
		player.shieldscale = 0
		player.sage.reset = true
		return false
	end
	if (player.sage.reset)
		local my = skins[player.skin]
		player.normalspeed = my.normalspeed
		player.jumpfactor = my.jumpfactor
		player.acceleration = my.acceleration
		player.accelstart = my.accelstart
		player.thrustfactor = my.thrustfactor
		player.powers[pw_strong] = STR_NONE
		player.shieldscale = my.shieldscale
		player.sage.reset = false
	end
	if (rphys != 62)
		pmo.movefactor = P_IsObjectOnGround(pmo) and FRACUNIT or 3<<FRACBITS
	end
	pmo.friction = FRACUNIT*19/20
	pmo.flags = $|MF_NOGRAVITY
	if (player.powers[pw_carry])
		player.sage.stock = 1
		player.sage.grab = 0
		pmo.flags = $ & ~MF_NOGRAVITY
	end
	if (player.pflags & PF_SPINNING) or (player.panim == PA_ROLL)
		pmo.flags = $ & ~MF_NOGRAVITY
		pmo.flags2 = $|MF2_DONTDRAW
		if player.sage.spinball and player.sage.spinball.valid
			local sp = player.sage.spinball
			sp.color = player.sage.color
			sp.scale = pmo.scale
			sp.spriteyoffset = pmo.height/2
			sp.fuse = 2
			sp.dontdrawforviewmobj = pmo
		else
			player.sage.spinball = P_SpawnMobjFromMobj(pmo,0,0,0,MT_OVERLAY)
			local sp = player.sage.spinball
			sp.target = pmo
			sp.state = S_SAGEVFX_LIGHTBALL
			sp.color = player.sage.color
			sp.spriteyoffset = pmo.height/2
			sp.fuse = 2
			sp.dontdrawforviewmobj = pmo
			sp.momx = pmo.momx
			sp.momy = pmo.momy
			sp.momz = pmo.momz
			SageEffect(pmo,"lightball_xfx")
		end
		if P_RandomChance(FRACUNIT/TICRATE) --dont mind this
			local bp = SageEffect(pmo,"lightball_xfx")
			if bp and bp.valid
				bp.momx = 2*pmo.momx/3
				bp.momy = 2*pmo.momy/3
				bp.momz = 2*pmo.momz/3
			end
		end
		if px and px.valid
			px.momx = 2*pmo.momx/3
			px.momy = 2*pmo.momy/3
			px.momz = 2*pmo.momz/3
		end
		return false
	elseif player.sage.spinball then
		SageEffect(pmo,"lightball_xfx")
		pcall(P_RemoveMobj, player.sage.spinball)
		player.sage.spinball = nil
	end
	if not (pmo.eflags & MFE_SPRUNG)
		grabs(pmo)
	end
	local floatmax = pmo.floorz+(min(22<<FRACBITS, player.speed*2/3))
	local thissideup = true
	if (pmo.eflags & MFE_VERTICALFLIP)
		floatmax = pmo.ceilingz-pmo.height-(min(22<<FRACBITS, player.speed*2/3))
		thissideup = false
	end
	if (thissideup) and (pmo.z <= floatmax and (player.speed > 6*pmo.scale)) and sagefloatcheck(player, pmo)
		pmo.z = floatmax
		if (pmo.momz < 0 and pmo.z >= player.sage.oldz) //do not save zmomentum if downwards on flat ground, but keep it in all other cases
			pmo.momz = 0
		elseif (pmo.z > player.sage.oldz)
			pmo.momz = $+FixedMul(FRACUNIT/3, pmo.scale) //build up vertical momentum when moving up a slope
		end
		sagefloatstuff(player, pmo)
	elseif not (thissideup) and (pmo.z >= floatmax and (player.speed > 6*pmo.scale)) and sagefloatcheck(player, pmo)
		pmo.z = floatmax
		if (pmo.momz > 0 and pmo.z <= player.sage.oldz) //there is probably a cleaner way to flip this but reverse gravity sucks so whatever
			pmo.momz = 0
		elseif (pmo.z < player.sage.oldz)
			pmo.momz = $+FixedMul(FRACUNIT/3, pmo.scale)
		end
		sagefloatstuff(player, pmo)
	else
		if P_IsObjectOnGround(pmo)
			pmo.flags = $ & ~MF_NOGRAVITY
			player.sage.stock = 1
			player.sage.spring = false
			if (player.panim == PA_IDLE)
				if not S_SoundPlaying(pmo, sfx_sgidle)
					S_StartSound(pmo, sfx_sgidle)
				end
			end
			if (player.pflags & PF_THOKKED)
				P_ResetPlayer(player)
			end
		elseif (player.playerstate != PST_DEAD)
			if (player.powers[pw_carry]) or (player.sage.spring)
				pmo.flags = $ & ~MF_NOGRAVITY
			else
				pmo.momz = $+(P_GetMobjGravity(pmo)*4/5)
			end
			if S_SoundPlaying(pmo, sfx_sgidle)
				S_StopSoundByID(pmo, sfx_sgidle)
			end
			if ((player.panim <= PA_RUN) or (pmo.state == S_SAGE_RUN))
				if (pmo.state == S_SAGE_RUN) and (player.speed < FixedMul(player.runspeed, pmo.scale))
					pmo.state = S_PLAY_WALK
				end
			end
		end
	end
	if (player.powers[pw_super])
		if not (leveltime%5)
			pmo.translation = "SuperSage3"
		elseif not (leveltime%3)
			pmo.translation = "SuperSage2"
		else
			pmo.translation = "SuperSage1"
		end
		pmo.color = SKINCOLOR_WHITE
	elseif not (gametyperules & GTR_TEAMS) and not (player.powers[pw_dye])
		if (player.sage.invert) and (player.followmobj and player.followmobj.myrainbow) and swaps[player.sage.color]
			pmo.color = player.skincolor
			pmo.translation = nil
		elseif (swaps[player.sage.color] or (player.followmobj and player.followmobj.myrainbow))
			pmo.color = ColorOpposite(player.skincolor)
			pmo.translation = nil
			player.sage.invert = true
		elseif (player.sage.invert and not (swaps[player.sage.color]) and not (player.followmobj and player.followmobj.myrainbow))
			pmo.color = player.skincolor
			pmo.translation = nil
			player.sage.invert = false
		else
			pmo.translation = nil
		end
	end
	if (player.sage.grab)
		if (player.pflags & PF_SPINDOWN)
			if P_PlayerInPain(player) or (pmo.state != S_SAGE_FIRE)
				player.sage.grab = 0
				return
			end
			pmo.state = S_SAGE_FIRE
			player.panim = PA_DASH
			pmo.tics = TICRATE/7
			player.powers[pw_nocontrol] = 1<<FRACBITS
			if (rphys == 62)
				player.powers[pw_springlock] = 1
			end
			player.pflags = $|PF_STASIS
			player.drawangle = pmo.angle
			if (player.speed)
				P_Thrust(pmo, R_PointToAngle2(0, 0, player.rmomx, player.rmomy), -FixedMul(FRACUNIT, pmo.scale))
			end
			player.sage.grab = min($+1, FULL_CHARGE<<1)
			
			local rr,ra
			local rx,ry,rz
			
			rr = pmo.radius*2
			ra = player.drawangle
			rx = P_ReturnThrustX(nil,ra,rr)
			ry = P_ReturnThrustY(nil,ra,rr)
			rz = pmo.height/3*2
			
			if not pmo.sagecharger
				pmo.sagecharger = P_SpawnMobjFromMobj(pmo,rx,ry,rz,MT_SAGEVFX)
				local sb = pmo.sagecharger
				sb.state = S_SAGEVFX_LIGHTBALL
				sb.color = player.sage.color
				sb.scale = FRACUNIT/2
				sb.fuse = 3
				sb.dontdrawforviewmobj = pmo
				SageEffect(sb,"lightball_xfx",nil,16*pmo.scale,48*pmo.scale,-32*FRACUNIT)
				for i = 0,6
					SageEffect(sb,"aura_pixels",nil,16*pmo.scale,48*pmo.scale,-32*FRACUNIT)
				end
			elseif pmo.sagecharger.valid
				local sb = pmo.sagecharger
				sb.color = player.sage.color
				sb.fuse = 3
				
				local dis = {
				FF_TRANS90,FF_TRANS80,FF_TRANS70,FF_TRANS60,
				FF_TRANS50,FF_TRANS40,FF_TRANS30,FF_TRANS20,
				FF_TRANS10,0}
				
				if dis[player.sage.grab] != nil
					sb.frame = FF_FULLBRIGHT|FF_ANIMATE|FF_ADD|(dis[player.sage.grab])
				end
				
				rz = SMFMteleport(pmo,sb,pmo.height/3*2)
				P_MoveOrigin(sb,
					pmo.x+rx,
					pmo.y+ry,
					rz)
				
				if P_RandomChance(FRACUNIT/3)
					SageEffect(sb,"aura_pixels",nil,16*pmo.scale,48*pmo.scale,-32*FRACUNIT)
				end
				if P_RandomChance(FRACUNIT/TICRATE)
					SageEffect(sb,"lightball_xfx",nil,16*pmo.scale,48*pmo.scale,-32*FRACUNIT)
				end
				
				if (player.sage.grab < FULL_CHARGE)
					sb.scale = pmo.scale/2 + (2*min(player.sage.grab,FULL_CHARGE)*pmo.scale/TICRATE)
				else
					sb.scale = 3*pmo.scale/2
					if P_RandomChance(FRACUNIT/16)
						sb.spritexoffset = P_RandomRange(-2,2)*FRACUNIT
						sb.spriteyoffset = P_RandomRange(-2,2)*FRACUNIT
					end
				end					
			end
		elseif (player.sage.grab < FULL_CHARGE)
			if pmo.sagecharger and pmo.sagecharger.valid
				P_KillMobj(pmo.sagecharger)
				pmo.sagecharger = nil
			end
			player.sage.grab = 0
			player.sage.stock = 0
			local shots = P_SPMAngle(pmo, MT_SAGESHOT, pmo.angle, 0)
			if shots and shots.valid then
				shots.fuse = 70
				shots.color = player.sage.color
				shots.notouchy = 5
				shots.spritexscale = $*3/2
				shots.spriteyscale = $*3/2
				pmo.grassfedbeef = shots
			end
			S_StopSoundByID(pmo, sfx_sgchrg)
		else
			local ghs = P_SpawnGhostMobj(s)
			ghs.colorized = true
			ghs.color = SKINCOLOR_DUSK
			ghs.renderflags = RF_NOCOLORMAPS|RF_FULLBRIGHT
			ghs.blendmode = AST_ADD
			ghs.destscale = FixedMul($, 99<<FRACBITS)
			ghs.dispoffset = -2
			ghs.spriteyoffset = $-(5<<FRACBITS)
			ghs.scalespeed = FRACUNIT/7
			ghs.momz = s.momz/2-(s.scale*P_MobjFlip(s))
			ghs.momy = s.momy/2
			ghs.momx = s.momx/2
			ghs.fuse = 7 ghs.tics = 8
			if pmo.sagecharger and pmo.sagecharger.valid
				P_KillMobj(pmo.sagecharger)
				pmo.sagecharger = nil
			end
			player.sage.grab = 0
			player.sage.stock = 0
			player.sage.pop = 0
			local lockon = P_LookForEnemies(player, false, true)
			if lockon and lockon.valid
				local shots1 = P_SpawnPointMissile(pmo, lockon.x, lockon.y, lockon.z+pmo.height/2, MT_SAGEBIGSHOT, pmo.x, pmo.y, pmo.z+lockon.height/2)
				if shots1 and shots1.valid then
					shots1.fuse = 70
					shots1.color = player.sage.color
					shots1.spritexscale = $*3
					shots1.spriteyscale = $*3
				end
			else
				local planbee = P_LookForEnemies(player, false, false)
				if planbee and planbee.valid
					local shots1 = P_SpawnPointMissile(pmo, planbee.x, planbee.y, planbee.z+pmo.height/2, MT_SAGEBIGSHOT, pmo.x, pmo.y, pmo.z+planbee.height/2)
					if shots1 and shots1.valid then
						shots1.fuse = 70
						shots1.color = player.sage.color
						shots1.spritexscale = $*3
						shots1.spriteyscale = $*3
					end
				else
					local shots2 = P_SPMAngle(pmo, MT_SAGEBIGSHOT, pmo.angle, 0)
					if shots2 and shots2.valid then
						shots2.fuse = 70
						shots2.color = player.sage.color
						shots2.spritexscale = $*2
						shots2.spriteyscale = $*2
					end
				end
			end
		end
	elseif (pmo.sagecharger)
		pmo.sagecharger = nil
	end
	if (pmo.eflags & MFE_SPRUNG) or P_PlayerInPain(player) or (player.onconveyor == 2)
		player.sage.spring = true
	end
end)

addHook("PlayerHeight", function(player)
	if not (player.sage) return nil end
	if (player.sage.control) and player.sage.control.valid
		return player.sage.control.height
	end
	if (player.panim == PA_RUN) or (player.panim == PA_ROLL) or (player.mo.state == S_SAGE_RUN)
		return P_GetPlayerSpinHeight(player)
	end
end)

local function whereisthegoop(mobj, sector) //we just don't know
	for fof in sector.ffloors()
		if not (fof.fofflags & FOF_GOOWATER) or not (fof.fofflags & FOF_EXISTS)
			continue
		end 
		if (mobj.z+mobj.height < fof.bottomheight) or (mobj.z > fof.topheight)
			continue
		end
		mobj.eflags = $|MFE_GOOWATER
		mobj.player.sage.spring = true
		return true
	end
end

addHook("PostThinkFrame", function()
	for player in players.iterate
		if not (player.sage)
			continue
		end
		local pmo = player.mo
		local GENERIC_LAND = -1
		local GENERIC_AIR = -2
		local GENERIC_NOMOVE = -3
		if (pmo.eflags & (MFE_TOUCHWATER|MFE_UNDERWATER))
			whereisthegoop(pmo, pmo.subsector.sector)
		end
		if (P_MobjFlip(pmo)*pmo.pmomz < 0)
			pmo.pmomz = 0
		end
		if (player.sage.down)
			player.sage.down = $-1
		end
		if (player.sage.control) and player.sage.control.valid
			player.sage.control.target = nil
			player.sage.control.angle = player.drawangle
			if (BT_SHIELD and (player.cmd.buttons & BT_SHIELD)) or (player.cmd.buttons & BT_CUSTOM1)
				pof(player.sage.control, pmo)
				P_SetObjectMomZ(pmo, 15<<FRACBITS, false)
				if (pmo.eflags & MFE_UNDERWATER)
					pmo.momz = $/2
				end
				player.pflags = $|P_GetJumpFlags(player) & ~PF_THOKKED & ~PF_STARTJUMP
				pmo.state = S_PLAY_JUMP
				player.sage.control = 0
				player.powers[pw_carry] = CR_NONE
				player.sage.pop = MAX_POP
				player.sage.stock = 1
				player.glidetime = 0
				continue
			end
			if sagecrawladex[player.sage.control.type] //check if the thing we captured has an entry for custom behaviour
				sagecrawladex[player.sage.control.type](player)
			elseif (player.sage.control.info.speed == 0) //otherwise pick a default
				sagecrawladex[GENERIC_NOMOVE](player)
			elseif (player.sage.control.info.flags & MF_NOGRAVITY)
				sagecrawladex[GENERIC_AIR](player)
			else
				sagecrawladex[GENERIC_LAND](player)
			end
		end
	end
end)

addHook("MobjDamage", function(mobj)
	if not mobj.player or not mobj.player.sage return nil end
	if not (mobj.player.sage.control) return nil end
	pof(mobj.player.sage.control, mobj)
	P_DoPlayerPain(mobj.player, mobj.player.sage.control)
	mobj.player.sage.control = 0
	mobj.player.powers[pw_carry] = CR_NONE
	return true
end, MT_PLAYER)

local function pvpbursts(plyr, source)
	if source then
		if (gametyperules & GTR_RINGSLINGER) and not (gametyperules & GTR_TAG)
			P_PlayerRingBurst(plyr, plyr.rings)
		end
		if (gametyperules & GTR_POWERSTONES)
			P_PlayerEmeraldBurst(plyr, false)
			plyr.powers[pw_emeralds] = 0
		end
	end
	if debuff(plyr, CBW_Battle) then
		P_PlayerFlagBurst(plyr, false)
		return true
	end
	return false
end

addHook("ShouldDamage", function(mobj, infl, source, uselessnumber, dt)
	local plyr = mobj.player
	if not plyr or not plyr.sage return nil end
	if (plyr.powers[pw_carry] == CR_NIGHTSMODE) or (plyr.sage.control) or G_IsSpecialStage(gamemap) return nil end
	if (plyr.exiting) or (plyr.sage.deathtimer) or (plyr.playerstate == PST_DEAD) or (plyr.pflags & PF_GODMODE) return false end
	if (dt & DMG_DEATHMASK)
		mobj.flags = $|MF_NOCLIPTHING
		plyr.sage.deathtimer = (TICRATE*3/2)-1
		pvpbursts(plyr, source)
		return false
	end
	if (plyr.sage.pop) return false end
	if (plyr.rings != 0 or plyr.powers[pw_invulnerability] or plyr.powers[pw_flashing] or plyr.powers[pw_shield]) return nil end
	if (source and source.player)
	and ((gametyperules & GTR_FRIENDLY) or ((gametyperules & GTR_TEAMS) and (plyr.ctfteam == source.player.ctfteam) and not (CV_FindVar("friendlyfire").value))) return false end
	mobj.flags = $|MF_NOCLIPTHING
	plyr.sage.deathtimer = 1
	local flagged = pvpbursts(plyr, source)
	if (infl and source)
		P_DoPlayerPain(plyr, infl, source)
		if (source.player) and ((plyr.ctfteam != source.player.ctfteam) or not G_GametypeHasTeams())
			if (flagged)
				P_AddPlayerScore(source.player, 125)
			else
				P_AddPlayerScore(source.player, 100)
			end
		end
	else
		P_DoPlayerPain(plyr, nil, nil)
	end
	mobj.state = S_PLAY_STUN
	plyr.powers[pw_flashing] = 0
	return false
end, MT_PLAYER)

local function overyourhead(lost, found)
	if (found.flags & (MF_BOSS|MF_ENEMY)) and (found.type != MT_CYBRAKDEMON)
		found.flags = $|MF_SHOOTABLE //invulnerable? too bad, you get hit anyways
	end
	if (found.flags & MF_SHOOTABLE) and not (found.player)
		if (found.z-(FixedMul(24<<FRACBITS, found.scale)) > lost.z+lost.height) or (lost.z-(FixedMul(24<<FRACBITS, found.scale)) > found.z+found.height)
			return
		end
		if (found.flags & (MF_ENEMY|MF_BOSS))
			if abs(R_PointToDist2(lost.x, lost.y, found.x, found.y)) > (found.radius+lost.radius)
				return
			end
			P_SetOrigin(lost, found.x, found.y, found.z)
		else
			P_MoveOrigin(lost, found.x, found.y, lost.z)
			P_DamageMobj(found, lost, lost.target, 1, DMG_ELECTRIC)
			if (found.valid) and (found.flags & MF_MONITOR)
				pcall(P_RemoveMobj, lost)
			end
		end
	end
end

addHook("MobjThinker", function(shot)
	if not (shot.target) or not (shot.target.player) or not (shot.target.player.sage) or not (R_PointInSubsectorOrNil(shot.x, shot.y)) then pcall(P_RemoveMobj, shot) return true end
	if not (shot.valid) then return false end
	if (shot.notouchy)
		shot.notouchy = $-1
	end
	searchBlockmap("objects", overyourhead, shot)
	if not (shot.valid) then return false end //yes we have to check this twice
	P_InstaThrust(shot, R_PointToAngle2(0, 0, shot.momx, shot.momy), FixedHypot(shot.momx, shot.momy)*9/10)
	
	local trail = P_SpawnMobjFromMobj(shot,0,0,8*FRACUNIT/6,MT_SAGEVFX)
	trail.state = S_SAGEBIGSHOT
	trail.fuse = 16
	trail.color = shot.color
	trail.spritexscale = $*3/2
	trail.spriteyscale = $*3/2
	trail.rollangle = FixedAngle(FixedHypot(shot.momx,shot.momy))
	trail.scalespeed = shot.scale/14
	trail.destscale = shot.scale/16

	if (abs(shot.momx) < 2*shot.scale) and (abs(shot.momy) < 2*shot.scale)
		shot.fuse = 1
		S_StartSound(shot, sfx_sgpop)
		SageExplosion(shot, true, shot.target.player.sage.color)
	end
end, MT_SAGESHOT)

addHook("MobjThinker", function(shot)
	if not (shot.target) or not (shot.target.player) or not (shot.target.player.sage) or not (R_PointInSubsectorOrNil(shot.x, shot.y)) then pcall(P_RemoveMobj, shot) return true end
	if not (shot.valid) then return false end
	searchBlockmap("objects", overyourhead, shot)
	if not (shot.valid) then return false end //yes we have to make this comment twice
	local trail = P_SpawnMobjFromMobj(shot,0,0,0,MT_SAGEVFX)
	trail.state = S_SAGEVFX_LIGHTBALL
	trail.fuse = 16
	trail.color = shot.color
	trail.spritexscale = $*3
	trail.spriteyscale = $*3
	trail.scalespeed = shot.scale/14
	trail.destscale = shot.scale/16
	SageEffect(shot,"aura_pixels")
	
	if FixedHypot(shot.momx,shot.momy) <= 2*shot.scale
		shot.fuse = 1
	end
end, MT_SAGEBIGSHOT)

local function poison(shot, mobj, charged) //returns whether the player is a foe
	if not multiplayer
	or not (sagepvp.value)
	or (shot.target == mobj)
	or (gametyperules & GTR_FRIENDLY)
	or ((gametyperules & GTR_TEAMS) and (shot.target.player.ctfteam == mobj.player.ctfteam) and not (CV_FindVar("friendlyfire").value))
		return false
	end
	if charged and not (mobj.player.rings)
		shot.name = "Transfer Beam"
		mobj.blendmode = AST_TRANSLUCENT
		P_DamageMobj(mobj, shot, shot.target, 1, DMG_INSTAKILL)
		SageExplosion(mobj, false, shot.target.player.sage.color)
		pcall(P_RemoveMobj, shot)
		return true
	end
	if not (mobj.sagetagged and mobj.sagetagged.valid)
		local corrupt = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_SAGEVIRUS)
		corrupt.target = shot.target
		corrupt.chase = mobj
		corrupt.timer = (charged) and 3*TICRATE or TICRATE
		corrupt.strong = charged
		mobj.sagetagged = corrupt
	else
		mobj.sagetagged.timer = (charged) and 3*TICRATE or max(TICRATE, $)
	end
	pcall(P_RemoveMobj, shot)
	return true
end

addHook("MobjMoveCollide", function(shot, mobj)
	if not (shot.target) or not (shot.target.player) or not (mobj.valid) or not (mobj.health) return nil end
	if not (shot.z > mobj.z+mobj.height) and not (mobj.z > shot.z+shot.height)
		if (mobj.player) and not poison(shot, mobj, false) and not debuff(mobj.player, CBW_Battle)
		and not ((sageselfish.value) and (shot.target != mobj)) and not ((shot.notouchy) and (shot.target == mobj))
			shot.momx = 0
			shot.momy = 0
			P_SetObjectMomZ(mobj, 15<<FRACBITS, false)
			if (mobj.eflags & MFE_UNDERWATER)
				mobj.momz = $/2
			end
			P_ResetPlayer(mobj.player)
			mobj.player.pflags = $|P_GetJumpFlags(mobj.player) & ~PF_THOKKED & ~PF_STARTJUMP
			mobj.state = S_PLAY_JUMP
			return true
		end
		if not (shot.valid) then return false end
		if (mobj.flags & MF_BOSS) and not (mobj.flags2 & MF2_FRET)
			P_DamageMobj(mobj, shot, shot.target, 1, DMG_ELECTRIC)
			SageExplosion(shot, true, shot.target.player.sage.color)
			pcall(P_RemoveMobj, shot)
			return true
		end
		if (mobj.flags & MF_ENEMY)
			S_StartSound(mobj, sfx_sghit)
			if (mobj.sagetagged and mobj.sagetagged.timer)
				mobj.sagetagged.timer = max(0, $-(TICRATE/2))
			else
				local corrupt = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_SAGEVIRUS)
				corrupt.target = shot.target
				corrupt.chase = mobj
				corrupt.timer = (mobj.info.spawnhealth) and TICRATE*3/2 or 1
				mobj.sagetagged = corrupt
			end
			SageExplosion(shot, true, shot.target.player.sage.color)
			pcall(P_RemoveMobj, shot)
			return true
		end
	end
end, MT_SAGESHOT)

addHook("MobjMoveCollide", function(cappy, mobj)
	if not (cappy.target) or not (mobj.valid) or not (mobj.health) return nil end
	if not (cappy.z > mobj.z+mobj.height) and not (mobj.z > cappy.z+cappy.height)
		local pmo = cappy.target
		if (mobj.player)
			if not poison(cappy, mobj, true)
				return false
			else
				return true
			end
		end
		if (mobj.flags & MF_BOSS) and not (mobj.flags2 & MF2_FRET)
			P_DamageMobj(mobj, cappy, pmo, 1, DMG_ELECTRIC)
			pcall(P_RemoveMobj, cappy)
			return true
		end
		if (mobj.flags & MF_ENEMY)
			mobj.flags = $|MF_NOCLIPTHING
			if (mobj.timer)
				mobj.timer = 0
			end
			S_StartSound(pmo, sfx_sgget1)
			mobj.momx = 0
			mobj.momy = 0
			pof(mobj, pmo)
			P_SetObjectMomZ(pmo, 15<<FRACBITS, false)
			if (pmo.eflags & MFE_UNDERWATER)
				pmo.momz = $/2
			end
			pmo.player.pflags = $|P_GetJumpFlags(pmo.player) & ~PF_THOKKED & ~PF_STARTJUMP
			pmo.state = S_PLAY_JUMP
			pcall(P_RemoveMobj, cappy)
			pmo.player.sage.pop = MAX_POP
			if (mobj.valid and mobj.type == MT_SNAILER) //offset the destination to the front of the snailer to prevent sage dying in the wall
				P_SetOrigin(pmo, mobj.x+FixedMul(17*mobj.scale, cos(mobj.angle)), mobj.y+FixedMul(17*mobj.scale, sin(mobj.angle)), mobj.z)
			else
				P_SetOrigin(pmo, mobj.x, mobj.y, mobj.z)
			end
			pmo.flags = $|MF_NOCLIP
			thanos(pmo.player)
			pmo.player.sage.stock = 1
			pmo.player.glidetime = 0
			return true
		end
		if (mobj.flags & MF_MONITOR) or ((mobj.flags & MF_PUSHABLE) and (mobj.flags & MF_SHOOTABLE))
			P_DamageMobj(mobj, cappy, cappy.target, 1, DMG_ELECTRIC)
		end
	end
end, MT_SAGEBIGSHOT)

local function fakeplayermove(cappy, line)
	for _, sector in ipairs({line.frontsector, line.backsector})
		maybebustableblock(cappy, sector)
	end
	if P_CheckSkyHit(cappy, line)
		pcall(P_RemoveMobj, cappy)
		return false
	end
	if (cappy.bonk)
		cappy.momx = 0
		cappy.momy = 0
		return true
	elseif (cappy.nobonk)
		return false
	else
		return nil
	end
end

addHook("MobjLineCollide", fakeplayermove, MT_SAGEBIGSHOT)
addHook("MobjLineCollide", fakeplayermove, MT_SAGEFAKE)
addHook("MobjLineCollide", fakeplayermove, MT_SAGESHOT)

addHook("MobjMoveBlocked", function(shot)
	if not (shot.target) or not (shot.target.player) return nil end
	S_StartSound(shot, sfx_sgbonk)
	SageExplosion(shot, true, shot.target.player.sage.color)
end, MT_SAGESHOT)

addHook("MobjThinker", function(corrupt)
	if not (corrupt.chase) or not (corrupt.chase.valid) or not (corrupt.chase.health)
		pcall(P_RemoveMobj, corrupt)
		return true
	end
	local chaseplayer = nil
	if (corrupt.chase.player)
		chaseplayer = corrupt.chase.player
	end
	if not (corrupt.target) or not (corrupt.target.player) or not (corrupt.target.player.sage)
		if not (chaseplayer)
			local newphonewhodis = P_SpawnMobjFromMobj(corrupt.chase, 0, 0, 0, corrupt.chase.type)
			newphonewhodis.angle = corrupt.chase.angle
			pcall(P_RemoveMobj, corrupt.chase)
		end
		pcall(P_RemoveMobj, corrupt)
		return true
	end
	if not (corrupt.timer) then
		if (chaseplayer)  // player corruption based on code from 4M's pvp modifications
			corrupt.name = "Corruption Virus" --Name of inflictor for Battlemod feed
			corrupt.chase.blendmode = AST_TRANSLUCENT
			if not (corrupt.chase.player.rings) --Kill if player has no rings regardless of shield, otherwise just deal damage
				P_DamageMobj(corrupt.chase, corrupt, corrupt.target, 1, DMG_INSTAKILL)
				SageExplosion(corrupt.chase, false, corrupt.target.player.sage.color)
			else
				P_DamageMobj(corrupt.chase, corrupt, corrupt.target, 1, DMG_NUKE)
				SageExplosion(corrupt.chase, true, corrupt.target.player.sage.color)
			end
		else
			pof(corrupt.chase, corrupt.target)
		end
		pcall(P_RemoveMobj, corrupt)
		return true
	else	
		corrupt.timer = $-1
		if (chaseplayer)
			if (chaseplayer.rings) and not (corrupt.timer % 7)
				chaseplayer.rings = $-1
				S_StartSound(corrupt, sfx_antiri, chaseplayer)
			end
			if not (corrupt.strong) and (chaseplayer.guard == 1)
				chaseplayer.guard = -1
				corrupt.chase.blendmode = 0
				pcall(P_RemoveMobj, corrupt)
				return true
			end
		end
	end
	if not (corrupt.timer % 13)
		if not (corrupt.chase.valid) return true end
		local copy = P_SpawnMobjFromMobj(corrupt.chase, P_RandomRange(-4, 4)<<FRACBITS, P_RandomRange(-4, 4)<<FRACBITS, P_RandomRange(0, 8), MT_SAGEGLITCH)
		if not chaseplayer then
			copy.target = corrupt.chase
			copy.angle = corrupt.chase.angle
			copy.color = corrupt.target.player.sage.color
			copy.colorized = true
			copy.sprite = corrupt.chase.sprite
			copy.frame = corrupt.chase.frame
			copy.blendmode = AST_ADD
		else
			copycat(copy, corrupt.chase, AST_ADD) --4M: Player specific additional corruption effect
		end
		copy.fuse = P_RandomRange(3, 5)
	end
	corrupt.chase.blendmode = AST_SUBTRACT
end, MT_SAGEVIRUS)

addHook("MobjCollide", function()
	return false
end, MT_SAGEHIT)

addHook("MobjMoveCollide", function(marine, spring)
	if ((spring.flags & MF_SPRING) or (spring.type == MT_RING) or (spring.type == MT_SAGESHOT)) and spring.valid and (marine.target)
		if not (spring.z > marine.z+marine.height) and not (marine.z > spring.z+spring.height)
			if ((spring.type == MT_RING or spring.type == MT_SAGESHOT)) and not (spring.notouchy)
				P_SetOrigin(spring, marine.target.x, marine.target.y, marine.target.z)
			elseif not (marine.target.eflags & MFE_SPRUNG) and not (marine.target.player.powers[pw_justsprung]) and (spring.z >= marine.target.floorz) and (spring.z <= marine.target.ceilingz)
				P_DoSpring(spring, marine.target)
				pcall(P_RemoveMobj, marine)
			end
		end
	end
	return false
end, MT_SAGEHIT)

addHook("MobjCollide", function(shield, sage)
	if not sage.player or not sage.player.sage return nil end
	if not sage.player.sage.pop return nil end
	return false
end, MT_EGGSHIELD)

addHook("MobjThinker", function(mo)
	if mo.cusval == 1
		local dis = {
		FF_TRANS90,FF_TRANS80,FF_TRANS70,FF_TRANS60,
		FF_TRANS50}
		
		local tix = mo.tics
		if dis[tix] != nil
			mo.frame = $|(dis[tix])
		end
	elseif mo.cusval == 2
		mo.momx = FixedMul($,mo.friction)
		mo.momy = FixedMul($,mo.friction)
		
		local dis = {
		FF_TRANS50,FF_TRANS50,FF_TRANS40,FF_TRANS40,
		FF_TRANS30,FF_TRANS30,FF_TRANS20,FF_TRANS20,
		FF_TRANS10}
		
		local tix = FixedInt(FixedDiv(mo.tics*FRACUNIT,64*FRACUNIT)*10)
		if dis[tix] != nil
			mo.frame = FF_FULLBRIGHT|FF_ANIMATE|(dis[tix])
		end
	end
end, MT_SAGEVFX)

addHook("MusicChange", function(oldname, newname)
	if consoleplayer
		local character = skins[consoleplayer.skin].name
		if character == "sage" //skin to use custom themes
			if newname == "_super" //intercept and override jingles
				return "IMWTYU"
			end
		end
	end
end)

local usedknife = false
COM_AddCommand("effect>", function(player, arg1)
	if (gamestate != GS_LEVEL) or (player.playerstate != PST_LIVE) or (skins[player.skin].name != "sage") or not (player.mo) return end
	local pmo = player.mo
	if (arg1 == "frog")
		if S_SoundPlaying(pmo, sfx_sgfrog)
			return
		else
			S_StartSound(pmo, sfx_sgfrog)
		end
	end
	if (arg1 == "marketable")
		local plush = P_SpawnMobjFromMobj(pmo, 0, 0, 0, MT_SAGEPLUSH)
		pmo.momy = 0
		pmo.momx = 0
		pmo.momz = 0
		S_StartSound(NULL, sfx_sgmrkt, player) //play a second copy only for the local player to increase the volume
		S_StartSound(pmo, sfx_sgmrkt)
		plush.spritexscale = $>>2
		plush.spriteyscale = $>>2
		plush.color = pmo.color
		plush.fuse = 9*TICRATE
		pmo.flags = MF_NOSECTOR
		player.playerstate = PST_DEAD
		return
	end
	if (arg1 == "initialc")
		if (player.sage.control) then
			if P_IsLocalPlayer(player)
				print("Unit malfunction detected. Report to the nearest CrawlaCorp repair center.")
			end
			return
		end
		if not All7Emeralds(emeralds) then //i don't want crawla to be the speedrun meta, so add extra restrictions to make super form a better option in most cases
			if P_IsLocalPlayer(player)
				print("Did you get all those Chaos Emeralds?")
			end
			return
		end
		if (player.rings < 25) then
			if P_IsLocalPlayer(player)
				print("Sorry "..player.name..", I can't give credit. Come back when you're a little mmmmm... richer.")
			end
			return
		end
		P_GivePlayerRings(player, -25)
		S_StartSound(pmo, sfx_antiri)
		local get = P_RandomChance(FRACUNIT>>1) and P_SpawnMobjFromMobj(pmo, 0, 0, 0, MT_BLUECRAWLA) or P_SpawnMobjFromMobj(pmo, 0, 0, 0, MT_REDCRAWLA)
		get.flags = $|MF_NOTHINK|MF_NOBLOCKMAP|MF_NOCLIP
		player.sage.control = get
		P_FlashPal(player, PAL_RECYCLE, TICRATE>>2)
		SageEffect(pmo, "teleport")
		SageEffect(player.sage.control, "teleport")
		S_StartSound(pmo, sfx_sgtimr)
		S_StartSound(NULL, sfx_sgtimr)
	end
	if (arg1 == "knife")
		if (usedknife)
			if P_IsLocalPlayer(player)
				print("You're out of knives!")
			end
			return
		end
		local stabs = P_SpawnMobjFromMobj(pmo, 0, 0, pmo.height>>1, MT_SAGEKNIFE)
		stabs.target = pmo
		stabs.renderflags = $|RF_PAPERSPRITE
		stabs.angle = player.drawangle
		stabs.spritexscale = $>>2
		stabs.spriteyscale = $>>2
		P_InstaThrust(stabs, stabs.angle, 50*stabs.scale)
		S_StartSound(pmo, sfx_tswtch)
		usedknife = true
	end
end)

addHook("AddonLoaded", function()
	if PSO then
		local function blackout(v)
			v.drawFill()
			v.drawString(160, 100, ("Please load Sage after SRB2 Infinity, not before."),V_REDMAP, "center")
		end
		hud.add(blackout, "title")
		hud.add(blackout, "game")
	end
end)
