/*
Chrispy's Beach Ball
	by Lach
Edited to prevent conflicts
v1
*/

local IS = ISlandsoccer

if IS.SoccerBall
	return
end

local SB = {}

IS.SoccerBall = SB

IS.SafeFreeslot("SPR_ISSB", "S_ISBALLS", "S_ISBALL", "MT_ISLANDBALL")


states[S_ISBALLS] = {
	sprite = SPR_ISSB,
	frame = A,
	tics = 1,
	nextstate = S_ISBALL
	
}

states[S_ISBALL] = {
	sprite = SPR_ISSB,
	action = A_SetScale,
	frame = A,
	tics = -1,
	var1 = FRACUNIT/2,
	var2 = 0
}

mobjinfo[MT_ISLANDBALL] = {
	--$Name Soccer Ball
	--$Sprite ISSBA0
	--$Category Chrispy Pixels
	doomednum = 4054,
	spawnstate = S_ISBALLS,
	spawnhealth = 1000,
	radius = 16*FRACUNIT,
	height = 64*FRACUNIT,
	flags = MF_SPECIAL|MF_BOUNCE,
	activesound = sfx_s3k44
}

SB.FRICTION = (IS.ORIG_FRICTION + FRACUNIT) / 2
SB.MOMENTUM_SCALE = 1*FRACUNIT
SB.BOUNCE_FACTOR = 3*FRACUNIT/5
SB.MIN_BOUNCE = 5*FRACUNIT
SB.BUOYANCY = 5*FRACUNIT
SB.DESPAWN_TIME = 15*TICRATE

SB.animations = {}
SB.inLevel = false

function SB.AddAnimation(sprite, startFrame, endFrame, axis, addFlipped)
	startFrame = R_Char2Frame($)
	endFrame = R_Char2Frame($)
	table.insert(SB.animations, {
		sprite = sprite,
		startFrame = startFrame,
		endFrame = endFrame,
		axis = axis
	})
	if addFlipped
		table.insert(SB.animations, {
			sprite = sprite,
			startFrame = startFrame|FF_HORIZONTALFLIP,
			endFrame = endFrame|FF_HORIZONTALFLIP,
			axis = axis
		})
	end
end

SB.AddAnimation(SPR_ISSB, 'A', 'D', 1, true)
SB.AddAnimation(SPR_ISSB, 'A', 'D', 2)
SB.AddAnimation(SPR_ISSB, 'A', 'D', 3)


function SB.UpdateAnimationSpeed(ball)
	ball.extravalue2 = FixedDiv(FixedHypot(FixedHypot(ball.momx, ball.momy), ball.momz), ball.scale)
end

function SB.SetAnimation(ball, index)
	local anim = SB.animations[index]
	ball.extravalue1 = index - 1
	ball.sprite = anim.sprite
	ball.frame = anim.startFrame
end

function SB.ChangeAnimation(ball)
	local currentAnim = SB.animations[ball.extravalue1 + 1]
	local validAnimations = 0
	local newIndex = 0
	local rand
	
	for _, anim in ipairs(SB.animations)
		if currentAnim.axis ~= anim.axis
			validAnimations = $ + 1
		end
	end
	
	rand = P_RandomKey(validAnimations) + 1
	
	while rand > 0
		rand = $ - 1
		newIndex = $ + 1
		while SB.animations[newIndex].axis == currentAnim.axis
			newIndex = $ + 1
		end
	end
	
	SB.SetAnimation(ball, newIndex)
	SB.UpdateAnimationSpeed(ball)
end

function SB.CollidePlayer(ball, mo)
	// don't allow the ball to touch a player continuously
	local touched = ball.movecount > 0
	ball.movecount = 2
	
	if touched
		return
	end
	
	// get speeds
	local player = mo.player
	local momx = mo.momx - player.cmomx
	local momy = mo.momy - player.cmomy
	local speed = FixedHypot(momx, momy)
	local ballSpeed = FixedHypot(ball.momx, ball.momy)
	
	// set ball's momentum
	if ballSpeed < speed // player kicked ball
		ball.momx = FixedMul(momx, SB.MOMENTUM_SCALE)
		ball.momy = FixedMul(momy, SB.MOMENTUM_SCALE)
		
		ball.momz = P_MobjFlip(mo) * FixedMul(speed, SB.MOMENTUM_SCALE) / 1
	elseif speed == 0 // ball hit stationary player
		P_InstaThrust(ball, R_PointToAngle2(mo.x, mo.y, ball.x, ball.y), ballSpeed)
		
		if abs(ball.momz) > FixedMul(SB.MIN_BOUNCE, ball.scale)
			ball.momz = -FixedMul(ball.momz, SB.BOUNCE_FACTOR)
		end
	else // rebound!
		local ballDir = R_PointToAngle2(0, 0, ball.momx, ball.momy)
		local playerDir = R_PointToAngle2(0, 0, momx, momy)
		
		ballSpeed = FixedMul(max($, speed), SB.MOMENTUM_SCALE)
		
		P_InstaThrust(ball, ballDir + (playerDir - ballDir)/2, ballSpeed)
		ball.momz = P_MobjFlip(mo) * ballSpeed / 1
	end
	
	// boing!
	S_StartSound(ball, ball.info.activesound)
	SB.ChangeAnimation(ball)
	
	// set the despawn fuse
	ball.flags2 = $ & ~MF2_DONTDRAW
	ball.fuse = SB.DESPAWN_TIME
	ball.target = mo
end

function SB.CollideSpring(ball, spring)
	if ball.eflags & MFE_SPRUNG
		return
	end
	
	if spring.spling
		if not IS.Spling.DoSpling(spring, ball)
			return
		end
	else
		if not P_DoSpring(spring, ball)
			return
		end
	end
	
	S_StartSound(ball, ball.info.activesound)
	SB.ChangeAnimation(ball)
end

function SB.CollideSteam(ball, steam)
	if steam.state ~= S_STEAM1
		return
	end
	
	local height = SB.GetHeightAboveFan(ball, steam)
	if height == nil
	or height > 16*steam.scale
		return
	end
	
	local flip = P_MobjFlip(steam)
	local speed = steam.info.mass
	
	ball.eflags = $ | MFE_SPRUNG
	ball.momz = flip * FixedMul(speed, FixedSqrt(FixedMul(steam.scale, ball.scale)))
	SB.ChangeAnimation(ball)
end

function SB.CollideFan(ball, fan)
	local height = SB.GetHeightAboveFan(ball, fan)
	if height == nil
	or height > fan.health << FRACBITS
		return
	end
	
	SB.DoFan(FixedMul(fan.info.mass, fan.scale), P_MobjFlip(fan))
end

function SB.DoFan(ball, speed, flip)
	if flip * ball.momz >= speed
		return
	end
	
	ball.momz = $ + flip * speed / 4
	
	if flip * ball.momz > speed
		ball.momz = flip * speed
	end
end

function SB.GetHeightAboveFan(ball, fan)
	if fan.eflags & MFE_VERTICALFLIP
		if ball.z > fan.z + fan.height
			return
		end
		return (fan.z + fan.height) - (ball.z + ball.height)
	else
		if ball.z + ball.height < fan.z
			return
		end
		return ball.z - fan.z
	end
end

function SB.CollideDustDevil(ball, devil)
	local thresh = devil.scale * 20
	local pos = ball.z - devil.z
	
	if devil.height - pos <= thresh
		ball.z = devil.z + devil.height
		ball.momz = thresh
		S_StartSound(ball, sfx_wdjump)
		SB.ChangeAnimation(ball)
		return
	end
	
	local thrust = max(FixedDiv(pos, devil.height) * 20, 8 * devil.scale)
	local fa = R_PointToAngle2(ball.x, ball.y, devil.x, devil.y)
	local c = cos(fa)
	local s = sin(fa)
	
	local dist = FixedHypot(ball.x - devil.x, ball.y - devil.y)
	local drag = FixedHypot(ball.momx, ball.momy)
	local x, y
	
	if ball.movecount == 0
		A_PlayActiveSound(devil)
	end
	ball.movecount = 2
	
	if dist > drag
		x = ball.x + FixedMul(c, drag)
		y = ball.y + FixedMul(s, drag)
	else
		x = devil.x
		y = devil.y
	end
	P_TryMove(ball, x - ball.momx, y - ball.momy, true)
	ball.momz = thrust
end

function SB.Collide(ball, mo)
	local type = mo.type
	
	// fans
	if type == MT_FAN
		return SB.CollideFan(ball, mo)
	end
	
	// steam jets
	if type == MT_STEAM
		return SB.CollideSteam(ball, mo)
	end
	
	// everything else uses height checks
	if ball.z > mo.z + mo.height
	or mo.z > ball.z + ball.height
		return
	end
	
	// players
	if mo.player
		return SB.CollidePlayer(ball, mo)
	end
	
	// springs
	if mo.flags & MF_SPRING
		return SB.CollideSpring(ball, mo)
	end
	
	// bould shield hitbox
	if SH_BOULDER
	and type == MT_BOULDER_COLLIDER
		local target = mo.target
		if target ~= nil
		and target.player
			return SB.CollidePlayer(ball, target)
		end
		return
	end
	
	// rollout rock
	if type == MT_ROLLOUTROCK
		local tracer = mo.tracer
		if tracer ~= nil
		and tracer.player
			return SB.CollidePlayer(ball, tracer)
		end
		return
	end
end

function SB.DustDevilSearch(devil, ball)
	if ball.type ~= MT_ISLANDBALL
		return
	end
	
	if ball.z > devil.z + devil.height
	or devil.z > ball.z + ball.height
	or abs(ball.x - devil.x) > devil.radius
	or abs(ball.y - devil.y) > devil.radius
		return
	end
	SB.CollideDustDevil(ball, devil)
end

function SB.DoFriction(ball)
	local slope = ball.standingslope
	
	if slope ~= nil
	and abs(slope.zdelta) > FRACUNIT / 4
		if IS.ButteredSlope(ball)
			ball.friction = FRACUNIT
			return
		end
	end
	
	if ball.friction < FRACUNIT
		ball.friction = min(FixedMul(FixedDiv($, IS.ORIG_FRICTION), SB.FRICTION), FRACUNIT)
	end
end

addHook("MobjCollide", SB.Collide, MT_ISLANDBALL)
addHook("MobjMoveCollide", SB.Collide, MT_ISLANDBALL)

addHook("TouchSpecial", do return true end, MT_ISLANDBALL) // MF_SPECIAL needed for collisions

addHook("MobjSpawn", function(ball)
	ball.shadowscale = FRACUNIT
	SB.SetAnimation(ball, P_RandomKey(#SB.animations) + 1)
	SB.inLevel = true
end, MT_ISLANDBALL)

addHook("MobjMoveBlocked", function(ball)
	SB.ChangeAnimation(ball)
end, MT_ISLANDBALL)

addHook("MobjThinker", function(ball)
	// slide down slopes
	SB.DoFriction(ball)
	
	// check whether the ball touched the floor or ceiling
	if abs(ball.movefactor) > FixedMul(SB.MIN_BOUNCE, ball.scale)
		local bounce = 0
		
		// floor bounce
		if (ball.z <= ball.floorz)
			bounce = 1
		// ceiling bounce
		elseif (ball.z + ball.height >= ball.ceilingz)
			bounce = -1
		end
		
		if bounce ~= 0
			ball.momz = bounce * abs(FixedMul(ball.movefactor, SB.BOUNCE_FACTOR))
			S_StartSound(ball, ball.info.activesound)
			SB.ChangeAnimation(ball)
		end
	end
	
	// fan sectors
	if IS.MobjInSectorSpecial(ball, 4, 5)
		SB.DoFan(ball, mobjinfo[MT_FAN].mass, 1)
	end
	
	// buoyancy
	if ball.z <= ball.watertop
	and ball.z + ball.height >= ball.waterbottom
		local flip = P_MobjFlip(ball)
		local buoyancy = FixedMul(SB.BUOYANCY, ball.scale)
		P_SetObjectMomZ(ball, FixedMul(P_GetMobjGravity(ball), 0*FRACUNIT), true)
		
		if flip * ball.momz > buoyancy
			ball.momz = flip * buoyancy
		end
	end
	
	// count down time since last player touch
	if ball.movecount > 0
		ball.movecount = $ - 1
	end
	
	// track previous momz
	ball.movefactor = ball.momz
	
	// set frame
	if ball.threshold > 0
		if P_IsObjectOnGround(ball)
			SB.UpdateAnimationSpeed(ball)
		end
		ball.threshold = $ - ball.extravalue2
	end
	
	if ball.fuse > 0 and ball.fuse < 3*TICRATE
		ball.flags2 = $ ^^ MF2_DONTDRAW
	end
	
	if ball.threshold <= 0
		local frame = ball.frame + 1
		local anim = SB.animations[ball.extravalue1 + 1]
		
		if frame > anim.endFrame
			frame = anim.startFrame
		end
		
		ball.frame = frame
		ball.threshold = 2 * FixedMul(IS.PI, ball.radius) / (anim.endFrame - anim.startFrame)
	end
end, MT_ISLANDBALL)

addHook("MobjFuse", function(ball)
	local mt = ball.spawnpoint
	
	if not mt
		return
	end
	
	local x, y, z = mt.x * FRACUNIT, mt.y * FRACUNIT
	local sector = R_PointInSubsector(x, y).sector
	
	if mt.options & MTF_OBJECTFLIP
		if sector.c_slope
			z = P_GetZAt(sector.c_slope, x, y)
		else
			z = sector.ceilingheight
		end
		z = $ - ball.height - (mt.z * FRACUNIT)
	else
		if sector.f_slope
			z = P_GetZAt(sector.f_slope, x, y)
		else
			z = sector.floorheight
		end
		z = $ + (mt.z * FRACUNIT)
	end
	
	ball.momx = 0
	ball.momy = 0
	ball.momz = 0
	ball.movefactor = 0
	ball.flags2 = $ & ~MF2_DONTDRAW
	SB.ChangeAnimation(ball)
	P_MoveOrigin(ball, x, y, z)
	
	return true
end, MT_ISLANDBALL)

addHook("MobjThinker", function(devil)
	if not SB.inLevel
		return
	end
	
	local radius = devil.radius
	searchBlockmap("objects", SB.DustDevilSearch, devil,
		devil.x - radius, devil.x + radius, devil.y - radius, devil.y + radius)
end, MT_DUSTDEVIL)

addHook("MapChange", function()
	SB.inLevel = false
end)

addHook("NetVars", function(net)
	SB.inLevel = net(SB.inLevel)
end)