Interesting… I extended my NPC behavior a bit, so maybe there’s an edge case somewhere in there. I pasted some semi-cleaned up code below, but the gist is:
- I’ve added an interface (team_targetable_v2) to leverage a method, TargetTeam.
- Instead of telling the NPC what to do inside of OnBegin when it’s created, I first let it create itself, then use TargetTeam in some other code to tell the NPC what to do
- Other NPC behaviors will extend creep_behavior_default to override “TargetReached”
- The NPC will go through some logic of prioritizing who to walk towards, which results in “ValidTarget”
- In the code below you can see my workaround for the MaintainFocus finishing by adding a loop around it. Previously I just had MaintainFocus outside of the block on its own. I verified this was breaking by putting a print before and after MaintainFocus.
creep_behavior_default := class(npc_behavior, team_targetable_v2):
@editable
MyReachDistance : float = 500.0
@editable
MyMovementType : NPCMovementTypes = NPCMovementTypes.Run
@editable
MyMovementSpeedMultiplier : float = 1.0
var NPCUtilities : npc_utilities = npc_utilities{}
TargetTeam<override>(AttackLane : Lane, ?ForceAttackPlayers : logic = false)<suspends> : void =
if:
# Get the Agent (this NPC).
Agent := GetAgent[]
Character := Agent.GetFortCharacter[]
Navigatable := Character.GetNavigatable[]
Focus := Character.GetFocusInterface[]
then:
loop:
# If the NPC died then break the loop
if (not Character.IsActive[]):
break
# If we are overriding to target a player only, then choose the player. Otherwise follow
# standard logic.
MaybeClosestTarget :=
if (ForceAttackPlayers?, PlayerInLane := AttackLane.GetPlayersInLane()[0]):
option{PlayerInLane}
else:
NPCUtilities.GetClosestCreepTarget(AttackLane, Self)
if:
ValidTarget := MaybeClosestTarget?
TargetDistance := NPCUtilities.CheckAgentDistance(ValidTarget, Self)
TargetDistance > MyReachDistance
then:
var CreepReachedTarget : logic = false
race:
# Keep looking at the target
block:
loop:
Focus.MaintainFocus(ValidTarget)
Sleep(0.1)
block:
# If the target we're navigating to dies, then stop the race
loop:
Sleep(0.2)
if (not ValidTarget.GetFortCharacter[].IsActive[]):
break
block:
# If we were targeting a player but there's now a guard, re-evaluate
if (not ForceAttackPlayers?, IsPlayer := player[ValidTarget]):
loop:
Sleep(2.0)
MaybeNewTarget := NPCUtilities.GetClosestCreepTarget(AttackLane, Self)
if (NewTarget := MaybeNewTarget?, NewTarget <> ValidTarget):
break
else:
# If we are already chasing a guard then let this thread wait
# This should never win the race.
Sleep(Inf)
block:
#Create a navigation target from ValidTarget
NavTarget := MakeNavigationTarget(ValidTarget)
ThisMovementType := if (MyMovementType = NPCMovementTypes.Run) then movement_types.Running else movement_types.Walking
# Tell the NPC to go to this spot within MyReachDistance. Method will wait here until we get a result.
NavResultGoTo := Navigatable.NavigateTo(NavTarget, ?MovementType:=ThisMovementType, ?ReachRadius := MyReachDistance)
if (NavResultGoTo <> navigation_result.Reached):
# [Some logic here to try a backup for navigating]
else:
set CreepReachedTarget = true
# If our race ended with reaching the target, make sure the creep has time to finish the TargetReached logic before restarting.
if (CreepReachedTarget?):
TargetReached(ValidTarget)
# Sleep for the overall loop
Sleep(1.0)
# Override to perform custom behavior in subclass when we reached InTarget based in MyReachDistance.
TargetReached(InTarget : agent)<suspends> : void=
block: