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 ! bSKSE || !newPlant.IsHarvested() || IgnoreHarvestedPlants == None || 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)) if distanceTraveled > 0 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) endif endif if wanderRangeX == 0 wanderRangeX = RandomFloat(fMinWander, fMaxWander) * RandomInt(-1, 1) endif if wanderRangeY == 0 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