scriptName CritterSpawn extends ObjectReference
{MODIFIED BY STEVE40 and USKP}

import Critter
import Utility

;----------------------------------------------
; Properties to be set for this Critter spawn
;----------------------------------------------

; The type of critter (base object) to create
FormList property CritterTypes auto
{ The base object to create references of to spawn critters}

; The distance from this spawner that Moths are allowed to be
float property fLeashLength = 500.0 auto
{ The distance that moths are allowed to be from this spawner}
float property fLeashHeight = 50.0 auto
{ The distance that dragonflies are allowed to be from above spawner}
float property fLeashDepth = 50.0 auto
{ The distance that fish are allowed to be from below spawner}
float property fMaxPlayerDistance = 2000.0 auto
{ The distance from the player before the Spawner stops spawning critters}

int property iMaxCritterCount = 10 auto
{ The maximum number of critters this spawner can generate}
float property fFastSpawnInterval = 0.1 auto
{ When spawning critters, the interval between spawns}
float property fSlowSpawnInterval = 5.0 auto
{ When spawning critters, the interval between spawns}

GlobalVariable property GameHour auto
{ Make this point to the GameHour global }
float property fStartSpawnTime = 6.0 auto
{ The Time after which this spawner can be active}
float property fEndSpawnTime = 11.0 auto
{ The Time before which this spawner can be active}

float property fLeashOverride auto
{Optional: Manually set roaming radius for critters spawned}

bool property bSpawnInPrecipitation auto
{Should this critter spawn in rain/snow?  DEFAULT: FALSE}

;----------------------------------------------
; Constants (shouldn't need to modify these)
;----------------------------------------------
float fCheckPlayerDistanceTime = 2.0

;----------------------------------------------
; Variables to keep track of spawned critters
;----------------------------------------------
int property iCurrentCritterCount = 0 auto hidden
bool bLooping
bool bPrintDebug = FALSE			; should usually be set to false.
int recursions

; Do initial stuff when my 3D has loaded up.
EVENT OnCellAttach() ; USKP 1.3.3 - Changed from OnLoad() to be more reliable, especially in interior cells.
	; The Spawner will register for update and periodically check whether the player is close or not
	; - JOEL REFACTOR - Going to use onLoad() & onUnload() instead of states?
	; GotoState("WaitingForPlayer")
	; - JOEL REFACTOR - also no longer need to update
	; RegisterForSingleUpdate(fCheckPlayerDistanceTime)
	
	if bPrintDebug == TRUE
; 		debug.trace("spawner " + self + " loaded.")
		recursions = 0
	endif
	
	; set our control bool to start the loop
	bLooping = TRUE
	
	while bLooping
		if bPrintDebug
			recursions += 1
; 			debug.trace("spawner " + self +  " while loop #" + recursions)
		endif
		
		if !shouldSpawn()
			; wait a bit, then see if the player is close again.
			; Removing this to eliminate some TPLOG spam
; ; 			debug.TraceConditional("player not yet near spawner " + self, bPrintDebug)
			utility.wait(fCheckPlayerDistanceTime)
		else
; ; 			debug.TraceConditional("spawner " + self + " ready to spawn!!!", bPrintDebug)
			; player must be nearby - spawn our initial critters
			; don't follow up as we no longer wish to re-generate new critters until the player leaves entirely
			spawnInitialCritterBatch()
			bLooping = FALSE
		endif
	endWhile
	
endEVENT

EVENT onUnload()
	; when our 3D unloads, stop looping until loaded again.
	bLooping = FALSE
; ; 	debug.TraceConditional("spawner " + self + " unloading due to onUnload() EVENT.", bPrintDebug)
endEVENT

EVENT onCellDetach()
	bLooping = FALSE
; ; 	debug.TraceConditional("spawner " + self + " unloading due to onCellDetach() EVENT.", bPrintDebug)
endEVENT

Function SpawnInitialCritterBatch()
	; How many do we need to spawn?
	int icrittersToSpawn = iMaxCritterCount - iCurrentCritterCount
	
	; Create that many critters
	int i = 0;
	while (i < icrittersToSpawn)
		; Create one critter at a time
		SpawnCritter()

		; Wait a bit before the next spawn
		;Wait(fFastSpawnInterval)
		
		; Next
		i = i + 1
	endWhile
endFunction

; Called by critters when they die
Event OnCritterDied()
	; Decrement current critter count, next time OnUpdate
	; gets called, we'll spawn a new one
	if iCurrentCritterCount > 0
		iCurrentCritterCount -= 1; faster [USKP 2.0.1]
	elseif iCurrentCritterCount < 0
		; iCurrentCritterCount must be in the negatives.  Something is up, but for now increment towards zero
		iCurrentCritterCount = 0; earler saves with failed deletions [USKP 2.0.1]
	endif
endEvent

; Spawns one Critter
bool Function SpawnCritter()
	if (iCurrentCritterCount < iMaxCritterCount) && (iCurrentCritterCount >= 0)
		; Go ahead with the actual spawn
		return SpawnCritterAtRef(self)
;/// [USKP 2.0.1] moved into SpawnCritterAtRef
		; Increment count
		iCurrentCritterCount = iCurrentCritterCount + 1
		return true
///;
	elseif iCurrentCritterCount < 0
; 		debug.trace("("+self+") has invalid iCurrentCritterCount of "+iCurrentCritterCount+", abort!")
		; turn off loop
		bLooping = FALSE
	else
; 		debug.trace("("+self+") SpawnCritter() failed because iCurrentCritterCount is "+iCurrentCritterCount)
; 		;debug.trace("("+self+") iMaxCritterCount: "+iMaxCritterCount)
	endif
	
	return False ; [USKP 1.3.1] to help eliminate log spam and potential long term save bloating.
endFunction

; Spawns one Critter at a specific location
;	returns true on success [USKP 2.0.1]
;
bool Function SpawnCritterAtRef(ObjectReference arSpawnRef)
	; Pick a random critter type
	Activator critterType = CritterTypes.GetAt(RandomInt(0, CritterTypes.GetSize() - 1)) as Activator
	
	if critterType == none
		; STEVE40+USKP extra check
		return false
	endif

	; Create the critter and cast it to the critter base class
	ObjectReference critterRef = arSpawnRef.PlaceAtMe(critterType, 1, false, true)
	Critter thecritter = critterRef as Critter
	
	if thecritter == none
		; STEVE40+USKP extra check
		return false
	endif

	; Set initial variables on the critter
; ; 	Debug.TraceConditional("Spawner " + self + " is creating Critter " + thecritter, bPrintDebug);
	thecritter.SetInitialSpawnerProperties(fLeashLength, fLeashHeight, fLeashDepth, fMaxPlayerDistance + fLeashLength, self)

	; Increment count [USKP 2.0.1]
	iCurrentCritterCount += 1
	return true
endFunction

; Utility method that returns the player's distance
float Function GetPlayerDistance()
	return Game.GetPlayer().GetDistance(self)
endFunction

; Utility method that tells the spawner whether it should spawn critters
bool Function ShouldSpawn()
	;DREW - Added an extra safety measure for if the object is stuck in the spawn check loop while the 3d is not loaded
	;	NOTE - is3dLoaded dumps an error when the 3d is not loaded, but the function still returns false which should
	;		set bLooping to false and jump out of the bLooping While, at which point no additional errors will be thrown
	;		GetParentCell check first helps avoid error log in exteriors? [USKP 2.0.1]
	if self.GetParentCell() && self.is3dLoaded()
		if !(GetPlayerDistance() <= fMaxPlayerDistance)
			return false
		endIf
		; Otherwise, base value on time of day (no handling of wrap around though...)
		return IsActiveTime()
	else		;if the 3d is not loaded jump out of the looped state. Just an extra safety measure.
; 		;debug.Trace(self + ": should be setting bLooping to False")
		bLooping = FALSE
		return false
	endif
endFunction

bool Function IsActiveTime()
	bool binTimeRange = false

	; BUGFIX BY STEVE40 - I found one case in Hearthfire where Bethesda didn't set the GameHour property on the script, causing it to spam the logs
	if GameHour == none
		bLooping = False	; kill the script
		return false
	endIf

	if (fEndSpawnTime >= fStartSpawnTime)
		binTimeRange = (GameHour.GetValue() >= fStartSpawnTime) && (GameHour.GetValue() < fEndSpawnTime)
	else
		binTimeRange = (GameHour.GetValue() >= fStartSpawnTime) || (GameHour.GetValue() < fEndSpawnTime)
	endIf
	return binTimeRange && ((Weather.GetCurrentWeather() == none) || \
								 (Weather.GetCurrentWeather().GetClassification() < 2) || \
								 (bSpawnInPrecipitation == TRUE))
endFunction