scriptName CritterFish extends Critter
{Main Behavior script for fish schools}

import Utility
import form
import debug

; Constants
float Property fActorDetectionDistance = 300.0 auto
{The Distance at which an actor will trigger a flee behavior}
float Property fTranslationSpeedMean = 40.0 auto
{The movement speed when going from plant to plant, mean value}
float Property fTranslationSpeedVariance = 20.0 auto
{The movement speed when going from plant to plant, variance}
float Property fFleeTranslationSpeed = 70.0 auto
{The movement speed when fleeing from the player}
float Property fMinScale = 0.1 auto
{Minimum initial scale of the Fish}
float Property fMaxScale = 0.2 auto
{Maximum initial scale of the Fish}
float Property fMinDepth = 10.0 auto
{Minimum fish depth}
float Property fSplineCurvature = 200.0 auto
{Spline curvature}
float Property fMinTimeNotMoving = 1.0 auto
float Property fMaxTimeNotMoving = 5.0 auto
float Property fSchoolingDistanceX = 25.0 auto
float Property fSchoolingDistanceY = 35.0 auto
int Property iPercentChanceSchooling = 50 auto
int Property iPercentChanceStopSchooling = 5 auto
float property fMaxRotationSpeed = 360.0 auto
{Max rotation speed while mocing, default = 360 deg/s}

; Hidden property for schooling
bool Property bMoving = false auto hidden
float Property fMoving = 0.0 auto hidden; [USKP 2.0.4]

; Variables
Actor closestActor = none
CritterFish TargetFish = none

;/ clear TargetObject [USKP 2.0.1]
/;
Function FollowClear()
	Float delay = 0.367879; 1/e
	while TargetFish && bCalculating && delay < 2.943; 3 times
		; avoid clearing during calculations [USKP 2.0.4]
		Wait(delay)
		delay += delay
	endWhile
	TargetFish = none
endFunction

;/ clear Leader and Follower and Target [USKP 2.0.1]
/;
Function TargetClear()
	if TargetFish
		TargetFish.Follower = none
	endIf
	TargetFish = none
endFunction

; Called by the spawner to kick off the processing on this Fish
Event OnStart()
	; Vary size a bit
	SetScale(RandomFloat(fMinScale, fMaxScale))
	
	; Start in the random swimming state
	GotoState("RandomSwimming")
	
	; test moved to Critter [indent retained] [USKP 2.0.1]
		WarpToRandomPoint()
; ; 		Debug.TraceConditional("Fish " + self + " registering for update", bCritterDebug)

		; Enable the critter
		Enable()

		if CheckFor3D(self)
			; Switch to keyframe state
			SetMotionType(Motion_Keyframed, false)

			; Get ready to start moving
			RegisterForSingleUpdate(0.0)
		else
			DisableAndDelete(false)
		endIf
endEvent

State RandomSwimming

	Event OnUpdate()
		; Is the player too far?
		if CheckViableDistance()
			; Good to update, check for close actors
;!			Actor closestActor = Game.FindClosestActorFromRef(self, fActorDetectionDistance)

			; Check whether we should flee and move faster
;!			float fspeed = 0.0
			float fspeed = fFleeTranslationSpeed
			if (closestActor != none)
; 				;Debug.Trace(self + " Oh noes! there is an Actor " + closestActor + " nearby, Flee")
				; Move fast
;!				fspeed = fFleeTranslationSpeed
			else
				; Move at regular speed
				fspeed = RandomFloat(fTranslationSpeedMean - fTranslationSpeedVariance, fTranslationSpeedMean + fTranslationSpeedVariance)
			endIf

			; Time to take off for another plant
			if (RandomInt(0, 100) < iPercentChanceSchooling)
				if PickTargetFishForSchooling() && PickRandomPointBehindTargetFish()
					GotoState("Schooling")
					SchoolWithOtherFish(fspeed)
				else
					GoToNewPoint(fspeed)
				endIf
			else
				GoToNewPoint(fspeed)
			endIf
			bCalculating = False; [USKP 2.0.4]
		endIf
		closestActor = none; [USKP 2.0.4]
	endEvent

	Event OnCritterGoalAlmostReached()
; 		traceConditional(self + " reached goal", bCritterDebug)
		fMoving = 0.0
		if PlayerRef; interlock, but never delete during Translation [USKP 2.0.1]
			; pre-find for speed, match other critters [USKP 2.0.4]
			closestActor = Game.FindClosestActorFromRef(self, fActorDetectionDistance)
; ; 			Debug.TraceConditional("Fish " + self + " registering for immediate update", bCritterDebug)
			RegisterForSingleUpdate(0.0)
		endIf
	EndEvent
	
endState


State Schooling

	Event OnUpdate()
		if CheckViableDistance()
;!			if ((RandomInt(0, 100) < iPercentChanceStopSchooling) || TargetFish == none || TargetFish.IsDisabled() || !TargetFish.bMoving)
			if (closestActor != none); [USKP 2.0.4]
; 				;Debug.Trace(self + " Oh noes! there is an Actor " + closestActor + " nearby, Flee")
				GotoState("RandomSwimming")
				TargetClear()
				GoToNewPoint(fFleeTranslationSpeed)
			elseif (RandomInt(0, 100) >= iPercentChanceStopSchooling) && (TargetFish as CritterFish) && TargetFish.fMoving && PickRandomPointBehindTargetFish()
				; If the target fish is moving, follow it
;!				SchoolWithOtherFish(fFleeTranslationSpeed)
				SchoolWithOtherFish(TargetFish.fMoving); reduce twitching [USKP 2.0.4]
			else
				GotoState("RandomSwimming")
				TargetClear()
				GoToNewPoint(RandomFloat(fTranslationSpeedMean - fTranslationSpeedVariance, fTranslationSpeedMean + fTranslationSpeedVariance))
			endIf
			bCalculating = False; [USKP 2.0.4]
		endIf
		closestActor = none; [USKP 2.0.4]
	endEvent

	Event OnCritterGoalAlmostReached()
; 		traceConditional(self + " about to reach goal", bCritterDebug)
		if PlayerRef; interlock, and match RandomSwimming [USKP 2.0.1]
			; pre-find for speed, match other critters [USKP 2.0.4]
			closestActor = Game.FindClosestActorFromRef(self, fActorDetectionDistance)
			RegisterForSingleUpdate(0.0)
		endIf
	EndEvent
	
endState


float fTargetX
float fTargetY
float fTargetZ
float fTargetAngleZ
float fTargetAngleX

;/
;	returns true on success [USKP 2.0.1]
/;
bool Function PickRandomPoint()

	; Pick a point inside the upside down cone of height fWaterDepth and radius fLeashLength
	; Since most ponds will look like a bowl, we don't want fish close to the edge to clip through the ground
	
	; make sure our spawner is loaded.
	if PlayerRef ;/&& CheckCellAttached(self) && Spawner && CheckCellAttached(Spawner)/;
		; First find a random point within the radius
;/// [USKP 2.0.4] ///;
		Float fLength = RandomFloat(0.0, fLeashLength)
		fTargetAngleZ = RandomFloat(-180.0, 180.0)
		fTargetX = fSpawnerX + fLength * Math.Cos(fTargetAngleZ)
		fTargetY = fSpawnerY + fLength * Math.Sin(fTargetAngleZ)
;*** [USKP 2.0.4] ***
		
		; Now pick a random Z based on the length.
		; If flength == fleashLength, then it must be -fMinDepth
		; If length == 0, then it can be -fDepth
		; fixed [USKP 2.0.4]
		if fMinDepth < fDepth
			fTargetZ = fSpawnerZ - RandomFloat(fMinDepth, (fDepth - ((flength * (fDepth - fMinDepth)) / fLeashLength)))
		else
			fTargetZ = fSpawnerZ
		endIf
		
		; Pick random target angle
;!		fTargetAngleZ = RandomFloat(-180.0, 180.0)
		fTargetAngleX = 0.0
		return (PlayerRef != None); check again after lengthy calculations [USKP 2.0.1]
	else
; 		debug.trace("Tried to run PickRandomPoint, but no valid TargetFish.  "+self)
		return false
	endif

endFunction

bool Function PickTargetFishForSchooling()
	if Spawner && CheckCellAttached(Spawner)
;!		TargetFish = Game.FindRandomReferenceOfAnyTypeInList(Spawner.CritterTypes, fSpawnerX, fSpawnerY, fSpawnerZ, fLeashLength - fSchoolingDistanceX) as CritterFish
		TargetFish = Game.FindRandomReferenceOfAnyTypeInList(Spawner.CritterTypes, fSpawnerX, fSpawnerY, fSpawnerZ, fLeashLength - fSchoolingDistanceY) as CritterFish
		; record TargetFish.Follower [USKP 2.0.1]
		if (TargetFish && TargetFish != self && TargetFish.FollowSet(self as ObjectReference))
			return true
		endif
		TargetFish = none
	endif
	return false
endFunction

;/
;	returns true on success [USKP 2.0.1]
/;
bool Function PickRandomPointBehindTargetFish()

	if PlayerRef && TargetFish && !TargetFish.IsDisabled() && CheckFor3D(TargetFish)
		; moved unchanged variables outside loop [USKP 2.0.1]
		float ftargetFishX = targetFish.X - fSpawnerX
		float ftargetFishY = targetFish.Y - fSpawnerY
		fTargetZ = targetFish.Z; default same as leader [USKP 2.0.4]
		fTargetAngleZ = targetFish.GetAngleZ()
;/// [USKP 2.0.4] ///;
		; vanilla formula dithers X and Y directly. Reduce calculations by
		; dithering angle vector instead.
		; reuse fSchoolingDistance[X|Y] as min and max distance from leader.
		float fDistance = RandomFloat(fSchoolingDistanceX, fSchoolingDistanceY)
		float fDeltaAngle = fTargetAngleZ + RandomFloat(-fAngleVarianceZ, fAngleVarianceZ)
		fTargetX = ftargetFishX - fDistance * Math.cos(fDeltaAngle)
		fTargetY = ftargetFishY - fDistance * Math.sin(fDeltaAngle)
		; variables above are 0,0 spawner relative
		float flength = Math.sqrt(fTargetX * fTargetX + fTargetY * fTargetY)
		fTargetX += fSpawnerX
		fTargetY += fSpawnerY
;*** [USKP 2.0.4] ***

		; Now pick a random Z
		; If flength == fleashLength, then it must be -fMinDepth
		; If length == 0, then it can be -fDepth
		; fixed [USKP 2.0.4]
		if flength < fLeashLength && fMinDepth < fDepth
			fTargetZ = fSpawnerZ - RandomFloat(fMinDepth, (fDepth - ((flength * (fDepth - fMinDepth)) / fLeashLength)))
		endIf
		
;!		fTargetAngleZ = targetFish.GetAngleZ()
		fTargetAngleX = 0.0
		return (PlayerRef != None); check again after lengthy calculations [USKP 2.0.1]
	else
; 		debug.trace("Tried to run PickRandomPointBehindTargetFish, but no valid TargetFish.  "+self)
		return false
	endif
	
endFunction

Function WarpToRandomPoint()
	
	; Pick a random point
	PickRandomPoint()

	; And warp to it
	SetPosition(fTargetX, fTargetY, fTargetZ)
	SetAngle(fTargetAngleX, 0.0, fTargetAngleZ)
	
endFunction

Function GoToNewPoint(float afSpeed)

	; Pick a random point
	if !PickRandomPoint()
 	; ;	debug.trace("Tried to run GoToNewPoint, but PickRandomPoint failed. "+self)
		bCalculating = False; [USKP 2.0.3]
		disableAndDelete()
	elseif CheckViability()
		return
	endif

	; And travel to it
	fMoving = afSpeed; [USKP 2.0.4]
	SplineTranslateTo(fTargetX, ftargetY, ftargetZ, fTargetAngleX, 0.0, fTargetAngleZ, fSplineCurvature, afSpeed, fMaxRotationSpeed)

endFunction

Function SchoolWithOtherFish(float afSpeed)

	if !CheckViability()
		fMoving = afSpeed; [USKP 2.0.4]
		SplineTranslateTo(fTargetX, ftargetY, ftargetZ, fTargetAngleX, 0.0, fTargetAngleZ, fSplineCurvature, afSpeed, fMaxRotationSpeed)
	endif
endFunction

EVENT onCellDetach()
	; Safety measure - when my cell is detached, for whatever reason, kill me.
; 	;debug.trace("Killing self due to onCellDetach() - "+self)
;!	DisableAndDelete()
	; kick the OnUpdate in hopes it will clean up. [USKP 2.0.1]
	bMoving = false
	fMoving = 0.0
	parent.onCellDetach()
endEVENT