scriptName CritterMoth extends Critter
{Main Behavior script for moths and butterflies}

import Utility
import form
import debug

; Properties (set through the editor)
FormList property PlantTypes auto
{ The list of plant types this moth can be attracted to}
FormList property IgnoreHarvestedPlants auto
{ plants we want to ignore when they're harvested }
FormList property GroundPlants auto ; jazbay
FormList property VeryShortPlants auto ; cabbage, leek, nirnroot, canis root, hanging moss 3, spiky grass
FormList property ShortPlants auto ; mountain flowers, cotton, dragon's tongue, hanging moss 2
FormList property TallPlants auto ; snowberry
FormList property VeryTallPlants auto ; juniper

ObjectReference fLastPlant

; Constants
float Property fTimeAtPlantMin = 5.0 auto
{The Minimum time a Moth stays at a plant}
float Property fTimeAtPlantMax = 15.0 auto
{The Maximum time a Moth stays at a plant}
float Property fActorDetectionDistance = 300.0 auto
{The Distance at which an actor will trigger a flee behavior}
float Property fTranslationSpeedMean = 150.0 auto
{The movement speed when going from plant to plant, mean value}
float Property fTranslationSpeedVariance = 50.0 auto
{The movement speed when going from plant to plant, variance}
float Property fFleeTranslationSpeed = 300.0 auto
{The movement speed when fleeing from the player}
float Property fBellShapePathHeight = 150.0 auto
{The maximum height of the bell shaped path}
float Property fFlockPlayerXYDist = 100.0 auto
{When flocking the player, the XY random value to add to its location}
float Property fFlockPlayerZDistMin = 50.0 auto
{When flocking the player, the min Z value to add to its location}
float Property fFlockPlayerZDistMax = 200.0 auto
{When flocking the player, the max Z value to add to its location}
float Property fFlockTranslationSpeed = 300.0 auto
{When flocking the player, the speed at which to move}
float Property fMinScale = 0.5 auto
{Minimum initial scale of the Moth}
float Property fMaxScale = 1.2 auto
{Maximum initial scale of the Moth}
float property fMinTravel = 64.0 auto
{Minimum distance a wandering moth/butterfly will travel}
float property fMaxTravel = 512.0 auto 
{Maximum distance a wandering moth/butterfly will travel}
string property LandingMarkerPrefix = "LandingSmall0" auto
{Prefix of landing markers on plants, default="LandingSmall0"}
float property fMaxRotationSpeed = 90.0 auto
{Max rotation speed while mocing, default = 90 deg/s}
float property fLeashMultiplier = 4.0 auto
{Increase the spawning range from the intial spawn point}
float property fSearchDistance = 250.0 auto
{Butterflies will search this distance for their next perch, increasing by fSearchIncrease each time it fails to find a plant.}
float property fSearchIncrease = 250.0 auto
float property fMinWander = 500.0 auto
float property fMaxWander = 1500.0 auto
{ How far away will butterflies wander from their current perch before searching for a perch}

; Variables
; Averages of x, y, z positions to track butterfly movement and keep them moving from old areas
float avgX = 0.0	
float avgY = 0.0
float avgZ = 0.0
int iPlantTypeCount = 0
Actor closestActor = none

; Constants
float fWaitingToDieTimer = 10.0


; Called by the spawner to kick off the processing on this Moth
Event OnStart()
	LoadPlantLists()

	; Pick a plant type that we're attracted to
	iPlantTypeCount = PlantTypes.GetSize()

	; Vary size a bit
	SetScale(RandomFloat(fMinScale, fMaxScale))
	
	; Switch state and trigger a callback immediately
; ; 	Debug.TraceConditional("Moth " + self + " warping to state AtPlant", bCritterDebug)
	
	; test moved to Critter [indent retained] [USKP 2.0.1]
		WarpToNewPlant()
; ; 		Debug.TraceConditional("Moth " + 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

Function LoadPlantLists()
	;Form cabbage = Game.GetFormFromFile(0x000BCF48, "Skyrim.esm")
	;if !VeryShortPlants.HasForm(cabbage)
	;	debug.trace("[Butterflies Unchained] Loading cabbage form into form list..")
	;	VeryShortPlants.AddForm(cabbage)
	;endif
endFunction

; The Current plant object
ObjectReference currentPlant = none

;/ clear TargetObject [USKP 2.0.1]
/;
Function TargetClear()
	currentPlant = none
endFunction

; Moth is at the plant
State AtPlant

	Event OnUpdate()
		; Is the player too far?
		if CheckViableDistance()
;/// [USKP 2.0.1]
			; Kill this critter
			DisableAndDelete()
		elseif (spawner.iCurrentCritterCount > spawner.iMaxCritterCount) || (spawner.iCurrentCritterCount < 0)
			; something's up with the spawner.  Kill critters until it recovers
; 			debug.trace(self+" updated, but spawner ("+spawner+") has bad iCurrentCritterCount ("+spawner.iCurrentCritterCount+")")
			disableAndDelete()
		else
///;
			if (ShouldFlockAroundPlayer())
				; Player is close enough and has the ingredient we're attracted to,
				; If applicable, play takeoff animation
				DoPathStartStuff()
				
				; Flock to the player
				FlockToPlayer()
			else
				if (Spawner && Spawner.IsActiveTime())
					; Check whether we should flee and move faster
					float fspeed = 0.0
					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
					GoToNewPlant(fspeed)
				else
					; Time to go to bed,
					BellShapeTranslateToRefAtSpeedAndGotoState(Spawner, fBellShapePathHeight, fTranslationSpeedMean, fMaxRotationSpeed, "KillForTheNight")
				endIf
			endIf
			bCalculating = False; [USKP 2.0.4]
		endIf
		closestActor = none; [USKP 2.0.4]
	endEvent
	
	Event OnCritterGoalReached()
; 		traceConditional(self + " reached goal", bCritterDebug)
		if PlayerRef; interlock, but never delete during Translation [USKP 2.0.1]
			closestActor = Game.FindClosestActorFromRef(self, fActorDetectionDistance)
			if closestActor
				; There is an actor right there, trigger the update right away, so we'll flee
; ; 				Debug.TraceConditional("Moth " + self + " registering for update", bCritterDebug)
				RegisterForSingleUpdate(0.0)
			else
				; Wait at the plant, then take off again
; ; 				Debug.TraceConditional("Moth " + self + " registering for update", bCritterDebug)
				RegisterForSingleUpdate(RandomFloat(fTimeAtPlantMin, fTimeAtPlantMax))
			endIf
		endIf
	EndEvent

endState

; When the player has the ingredient we're interested in, follow him
State FollowingPlayer

;!	Event OnCritterGoalReached()
	Event OnUpdate();  [USKP 2.0.1]
		; prevent repeat calculations [USKP 2.0.4]
		If bCalculating
			return
		EndIf
		bCalculating = True; [USKP 2.0.3]
		; Are we too far from our spawner?
		if (Spawner && Spawner.GetDistance(self) < fLeashLength && ShouldFlockAroundPlayer())
			; Nope, flock to the player
			FlockToPlayer()
		else
			; Go back to the plants
			GoToNewPlant(fFlockTranslationSpeed)
		endIf
		bCalculating = False; [USKP 2.0.4]
	endEvent

	Event OnCritterGoalReached()
; 		traceConditional(self + " about to reach goal", bCritterDebug)
		if PlayerRef; interlock, but never delete during Translation [USKP 2.0.1]
			RegisterForSingleUpdate(0.0)
		endIf
	EndEvent

endState

; When the moths go to sleep, they get deleted
State KillForTheNight

;!	Event OnCritterGoalReached()
	Event OnUpdate();  [USKP 2.0.1]
		; We've reached the nest, die
; 		debug.trace ("Killing for the night: "+self)
		DisableAndDelete()
	endEvent

	Event OnCritterGoalReached()
; 		traceConditional(self + " about to reach goal", bCritterDebug)
		if PlayerRef; interlock, but never delete during Translation [USKP 2.0.1]
			RegisterForSingleUpdate(0.0)
		endIf
	EndEvent

endState

; Helper method to indicate whether the player has the ingredient
bool Function ShouldFlockAroundPlayer()
;	if (PlayerRef.GetDistance(Spawner) > fRadius)
;		return false
;	endIf
;	return (PlayerRef.GetItemCount(IngredientType) > 0)
	return false
endFunction

; Utility method, makes a moth flock to a random point around the player
Function FlockToPlayer()
	; Switch state
; ; 	Debug.TraceConditional("Moth " + self + " going to state FollowingPlayer", bCritterDebug)
	gotoState("FollowingPlayer")

	; re-check viability [USKP 2.0.3]
	if !(PlayerRef ;/&& CheckCellAttached(self) && Spawner && CheckCellAttached(Spawner)/;)
		RegisterForSingleUpdate(fWaitingToDieTimer)
		return
	endif

	; Pick a random point around the player
	float ftargetX = PlayerRef.X + RandomFloat(-fFlockPlayerXYDist, fFlockPlayerXYDist)
	float ftargetY = PlayerRef.Y + RandomFloat(-fFlockPlayerXYDist, fFlockPlayerXYDist)
	float ftargetZ = PlayerRef.Z + RandomFloat(fFlockPlayerZDistMin, fFlockPlayerZDistMax)
	float ftargetAngleZ = RandomFloat(-180, 180)
	float ftargetAngleX = RandomFloat(-20, 20)
	float fpathCurve = RandomFloat(fPathCurveMean - fPathCurveVariance, fPathCurveMean + fPathCurveVariance)

	; Travel to it
	if CheckViability()
		return
	endIf
	SplineTranslateTo(ftargetX, ftargetY, ftargetZ, ftargetAngleX, 0.0, ftargetAngleZ, fpathCurve, fFlockTranslationSpeed, fMaxRotationSpeed)
endFunction

ObjectReference Function PickPlant(float x, float y, float z, float searchDistance, float searchIncrease)
	; re-check viability [USKP 2.0.1]
	if !(PlayerRef ;/&& CheckCellAttached(self) && Spawner && CheckCellAttached(Spawner)/;)
		return none
	endif

	; Look for a random plant within the radius of the Spawner
	int isafetyCheck = 10; was 5, match FireFly [USKP 2.0.1]
	int distanceScale = 1
	ObjectReference newPlant = CurrentPlant

	while PlayerRef && isafetyCheck > 0; [USKP 2.0.1b]

		; Grab a random plant from the list of valid plant types
		newPlant = Game.FindRandomReferenceOfAnyTypeInList(PlantTypes, x, y, z, searchDistance + (searchIncrease * distanceScale))
		
		; Check whether the new plant is valid (different from current)
		; and 3D check because critters can attempt to pick disabled Nirnroots [USKP 2.0.1]
		; and not too close to an actor
		if (newPlant != none && newPlant != currentPlant && !newPlant.IsDisabled() \
		 && Game.FindClosestActorFromRef(newPlant, fActorDetectionDistance) == none \
		 && CheckCellAttached(newPlant) && CheckFor3D(newPlant) && newPlant != fLastPlant)
		 	float plantX = newPlant.GetPositionX()
			float plantY = newPlant.GetPositionY()
			float plantZ = newPlant.GetPositionZ()
			float distanceFromAvg = Math.sqrt(Math.pow(plantX - avgX, 2) + Math.pow(plantY - avgY, 2) + Math.pow(plantZ - avgZ, 2))

			; Enforce distance from average position, unless we are on our last safety check.
			; In which case we allow it to pass to stop the butterfly from despawning
			if (distanceFromAvg >= fMinWander || isafetyCheck == 1) 
				Form plantForm

				; Fixes butterfly hovering by setting the height according to the plant
				if (GroundPlants == none || VeryShortPlants == none \
					|| ShortPlants == none || TallPlants == none \
					|| VeryTallPlants == none)
					Debug.Trace("[Butterflies Unchained] One or more of your mods adds a butterfly that requires a compatibility patch for optimal performance. You may encounter hovering butterflies in the meantime.")
				else 
					plantForm = newPlant.GetBaseObject()
					if (GroundPlants.Find(plantForm) != -1)
						SetPositionVariance(10.0, 10.0, 5.0, 10.0)
					elseif (VeryShortPlants.Find(plantForm) != -1)
						SetPositionVariance(10.0, 10.0, 20.0, 30.0)
					elseif (ShortPlants.Find(plantForm) != -1)
						SetPositionVariance(15.0, 15.0, 35.0, 50.0)
					elseif (TallPlants.Find(plantForm) != -1)
						SetPositionVariance(15.0, 15.0, 50.0, 90.0)
					elseif (VeryTallPlants.Find(plantForm) != -1)
							SetPositionVariance(15.0, 15.0, 115.0, 140.0)
					else
						SetPositionVariance(15.0, 15.0, 50.0, 100.0)
					endif
				endif

				if (!newPlant.IsHarvested() || IgnoreHarvestedPlants.Find(plantForm) == -1)
					return newPlant; [USKP 2.0.1b]
				endif
			endif
		endIf

		if ((fSearchDistance + (100 * distanceScale)) < fMaxPlayerDistance)
			distanceScale += 1
		endif
		
		; Safety counter
		isafetyCheck -= 1
	endWhile
	; [USKP 2.0.1]
 		;Debug.Trace("Moth ( " + fSpawnerX + ", " + fSpawnerY + ", " + fSpawnerZ + ") " + self + " couldn't find a valid plant to go to", 1)
		return none
endFunction

; Finds a new plant to fly to 
ObjectReference Function PickNextPlant()
	float wanderRangeX = 0
	float wanderRangeY = 0

	float posX = self.GetPositionX()
	float posY = self.GetPositionY()
	float posZ = self.GetPositionZ()

	; create a vector in the direction of the butterfly's movement to encourage exploration
	if (fLastPlant != none)
		float lastPlantX =  fLastPlant.GetPositionX()
		float lastPlantY =  fLastPlant.GetPositionY()
		float distanceTraveledX = posX - lastPlantX
		float distanceTraveledY = posY - lastPlantY

		float distanceTraveled = Math.sqrt(Math.pow(distanceTraveledX, 2) + Math.pow(distanceTraveledY, 2))

		wanderRangeX = RandomFloat(fMinWander, fMaxWander) * Math.cos(distanceTraveledX / distanceTraveled)
		if (distanceTraveledX < 0) ; more efficient than calling math.abs
			wanderRangeX *= -1
		endif
		wanderRangeY = RandomFloat(fMinWander, fMaxWander) * Math.sin(distanceTraveledY / distanceTraveled)
	else
		wanderRangeX = RandomFloat(fMinWander, fMaxWander) * RandomInt(-1, 1)
		wanderRangeY = RandomFloat(fMinWander, fMaxWander) * RandomInt(-1, 1)
	endif

	return PickPlant(posX + wanderRangeX, posY + wanderRangeY, posZ, fSearchDistance, fSearchIncrease)
endFunction

;; To maintain optimal compatibility with other mods, new function was created rather than having parameters added
ObjectReference Function PickSpawnPlant()
	; re-check viability [USKP 2.0.1]
	if !(PlayerRef ;/&& CheckCellAttached(self) && Spawner && CheckCellAttached(Spawner)/;)
		return none
	endif

	; Look for a random plant within the radius of the Spawner
	int isafetyCheck = 10; was 5, match FireFly [USKP 2.0.1]
	ObjectReference newPlant = CurrentPlant

	while PlayerRef && isafetyCheck > 0; [USKP 2.0.1b]

		; Grab a random plant from the list of valid plant types
		newPlant = Game.FindRandomReferenceOfAnyTypeInList(PlantTypes, fSpawnerX, fSpawnerY, fSpawnerZ, fLeashLength * fLeashMultiplier)
		
		; Check whether the new plant is valid (different from current)
		; and 3D check because critters can attempt to pick disabled Nirnroots [USKP 2.0.1]
		; and not too close to an actor
		if (newPlant != none && newPlant != currentPlant && !newPlant.IsDisabled() \
		 && Game.FindClosestActorFromRef(newPlant, fActorDetectionDistance) == none \
		 && CheckCellAttached(newPlant) && CheckFor3D(newPlant))
			return newPlant; [USKP 2.0.1b]
		endIf
		
		; Safety counter
		isafetyCheck -= 1
	endWhile
	; [USKP 2.0.1]
; 		Debug.Trace("Moth " + self + " couldn't find a valid plant to go to", 1)
		return none
endFunction

; Picks a new plant and fly to it if possible
Function GoToNewPlant(float afSpeed)
	; Find a plant reference, trying to pick a different one than the current one
	ObjectReference newPlant = PickNextPlant()
	
	if (newPlant != none)
		; Update the last plant to the current one
		fLastPlant = currentPlant

		; Update the current plant to the new one
		currentPlant = newPlant

		float newPlantX = newPlant.GetPositionX()
		float newPlantY = newPlant.GetPositionY()
		float newPlantZ = newPlant.GetPositionZ()

		; Update avg coord vals
		if (avgX == 0)
			avgX = newPlantX
		else
			avgX = (avgX + newPlantX) / 2
		endif

		if (avgY == 0)
			avgY = newPlantY
		else
			avgY = (avgY + newPlantY) / 2
		endif

		if (avgZ == 0)
			avgZ = newPlantZ
		else
			avgZ = (avgX + newPlantZ) / 2
		endif
		
		; Pick random landing node
		; And start moving towards it
		string landingMarkerName = LandingMarkerPrefix + RandomInt(1, 3)
		float newPlantDistance = Math.sqrt(Math.pow(newPlantX - self.GetPositionX(), 2) + Math.pow(newPlantY - self.GetPositionY(), 2) + Math.pow(newPlantZ - self.GetPositionZ(), 2))
		float adjustedBellShapePathHeight = fBellShapePathHeight - (fBellShapePathHeight * (Math.pow(0.995, newPlantDistance)))
		
		bool bIsLoaded = newPlant.Is3DLoaded()
		if (bIsLoaded && newPlant.HasNode(landingMarkerName))
			BellShapeTranslateToRefNodeAtSpeedAndGotoState(CurrentPlant, landingMarkerName, adjustedBellShapePathHeight, afSpeed, fMaxRotationSpeed, "AtPlant")
		else
; 			traceConditional(self + " could not find landing marker " + landingMarkerName + " on plant " + newPlant, bCritterDebug)
			
			string firstMarkerName = LandingMarkerPrefix + 1
			if (bIsLoaded && newPlant.HasNode(firstMarkerName))
				BellShapeTranslateToRefNodeAtSpeedAndGotoState(CurrentPlant, firstMarkerName, adjustedBellShapePathHeight, afSpeed, fMaxRotationSpeed, "AtPlant")
			else
; 				traceConditional(self + " could not find landing marker " + firstMarkerName + " on plant " + newPlant, bCritterDebug)
				BellShapeTranslateToRefAtSpeedAndGotoState(CurrentPlant, adjustedBellShapePathHeight, afSpeed, fMaxRotationSpeed, "AtPlant")
			endIf		
		endIf
	else
		; This moth is stuck, wait until the player is far enough away that it can delete itself
; 		Debug.Trace("Moth " + self + " is stuck and will wait to kill itself", 1)
		gotoState("KillForTheNight"); [USKP 2.0.1]
		RegisterForSingleUpdate(fWaitingToDieTimer)
	endIf
endFunction

Function WarpToNewPlant()
	; Find a plant reference, trying to pick a different one than the current one
	ObjectReference newPlant = PickSpawnPlant()
	
	if (newPlant != none)
		; Update the current plant to the new one
		currentPlant = newPlant
		bool bIsLoaded = newPlant.Is3DLoaded()
		
		; Pick random landing node
		; And start moving towards it
		string landingMarkerName = LandingMarkerPrefix + RandomInt(1, 3)
		if (bIsLoaded && newPlant.HasNode(landingMarkerName))
			WarpToRefNodeAndGotoState(CurrentPlant, landingMarkerName, "AtPlant")
		else
; 			traceConditional(self + " could not find landing marker " + landingMarkerName + " on plant " + newPlant, bCritterDebug)
			string firstMarkerName = LandingMarkerPrefix + 1
			if (bIsLoaded && newPlant.HasNode(firstMarkerName))
				WarpToRefNodeAndGotoState(CurrentPlant, firstMarkerName, "AtPlant")
			else
; 				traceConditional(self + " could not find landing marker " + firstMarkerName + " on plant " + newPlant, bCritterDebug)
				WarpToRefAndGotoState(CurrentPlant, "AtPlant")
			endIf
		endIf
	else
		; This moth is stuck, wait until the player is far enough away that it can delete itself
; 		Debug.Trace("Moth " + self + " is stuck and will wait to kill itself", 1)
		gotoState("KillForTheNight"); [USKP 2.0.1]
		RegisterForSingleUpdate(fWaitingToDieTimer)
	endIf
endFunction

Function DoPathStartStuff()
	; Transition to the flight state
	SetAnimationVariableFloat("fTakeOff", 1.0); [USKP 2.0.1]
endFunction

Function DoPathEndStuff()
	; Transition to the hover/landed state
	SetAnimationVariableFloat("fTakeOff", 0.0); [USKP 2.0.1]
endFunction