Papyrus - NPC Teleport in Combat Tutorial
Let's get started right away. Set up your script or open up your favorite Notepad program.
The first thing we need to do is set up some properties (variables) that we need for our script. They are as follows:
- Activator Properties: OutVis and InVis - These are the ending and beginning visual effects for the teleportation.
- VisualEffect (optional) - In case you'd like to get a little fancier, you can apply visual effects, which are best used under the same functions that spawn the Activators
- STATIC OutLocMarker (optional, see the bottom notes) - Used in the case that you would like to predetermine actualization of the ending effect.
- Float Properties: X, Y, and ZFloat - In order to track the aftermath of the teleportation sequence, in case it fails.
- Boolean: bCasting - In order to ensure that the teleportation function does not overlap a process already in effect
- ActorBase: ActorOwner (optional) - In case you'd like to use conditions and properties of your NPC to designate what occurs in the script.
Let's set up our basic functions. These are the spawning of the Activators which are the visual effects. Use VisualEffect via Apply() function here if you'd like.
function BeginTeleport()
PlaceAtMe(InVis)
endFunctionfunction EndTeleport()
PlaceAtMe(OutVis)
endFunction
The function as shown in the source code "TeleportTo" is force sequence based occurrences. If you'd like to teleport the NPC in the case of a scene, special event or such other things, use this instead of "CombatTeleportTo". Because it is basically the same function, we will ignore this derivation of the combat based function.
Let's start to make the body of the script, starting with our fundamental function, CombatTeleportTo. The parameters include an ObjectReference which will be the teleport target in question, and a float which acts as a delay if needed for a smooth procession. We also need store information into our saved "vector" (the collective reference to the three floats we've declared).
XFloat = self.GetPositionX()
YFloat = self.GetPositionY()
ZFloat = self.GetPositionZ()
Before we can make the rest of the CombatTeleportTo, we need to include some trigonometric functions. Particularly conversion from rectangular to polar coordinates. These are float functions.
float Function PolarToRectangularY(float rX, float Degrees, float Radius) ;wow, no outbound parameters allowed :/
return (rX + (Radius * Math.cos(Degrees)))
endFunctionfloat Function PolarToRectangularX(float rY, float Degrees, float Radius)
return (rY + (Radius * Math.sin(Degrees)))
endFunction
The formulae for rectangular to polar coordinates are as follows:
x=r cosθ
y=r sinθ
This allows us to easily work with radii in a 3 dimensional world. We can acquire both distal and angular information from these two values. We would like to get the distance from a variable point, and find a point r units away from that point. As you can see this is easily accessible when looking at the idea in question from a visual perspective.
The center in our case will be the target ObjectReference, and the point we're interested in is (r,θ). The rX and rY in the functions are the relative offsets based on the coordinate locations of the points in question. If we did not include these, the functions would return values near the origin of the world.
We would also like to ensure that our NPC does not teleport in front of the target. To do this, we need to make sure that the angles of 45 to -45 degrees relative to the initial angle (our target's facing direction) are out of question. That is to say, the NPC will not be able to teleport 45 to -45 degrees in front of our target.
float Function CalculateDesiredAngle(ObjectReference RefAngle)
float T = GetHeadingAngle(RefAngle)if (T < 45)&&(T > -45)
return (T+180)
else
return T
endIf
endFunction
Now that we have this we can finish our CombatTeleportTo function. We're going to define some private variables.
float fDir = CalculateDesiredAngle(TeleportTarget)
float XLoc = PolarToRectangularX(TeleportTarget.GetPositionX(), fdir, 64)
float YLoc = PolarToRectangularY(TeleportTarget.GetPositionY(), fdir, 64)
One thing that may be important to mention is the GetHeadingAngle function. This will return the angle of the target relative to our NPC's location. For instance, suppose A is the target and B is our NPC. GetHeadingAngle would return the value for angle C.
Now comes the sequence chain:
if (TeleportTarget)
BeginTeleport()
utility.wait(0.4)
SetPosition(XLoc, YLoc, TeleportTarget.GetPositionZ()) ; added Z os for adjustments
SetAngle(GetAngleX(), GetAngleY(), GetAngleZ() + GetHeadingAngle(TeleportTarget))
EndTeleport()
utility.wait(2)
CheckSuccess(TeleportTarget)
bCasting = False
endIf
Taking TeleportTarget as a boolean will check if it exists. If it does exist, we execute BeginTeleport(), casting the visual effects, waiting 0.4 seconds with utility.wait(), and finally moving our NPC to the designated location, which is the calculated, processed location of our destination. We also want to use the SetAngle function to turn the NPC toward the target. The only variable really being changed here with respect to this function call is in the 3rd argument. Adding the heading angle to the initial Z angle (Yaw) will turn our NPC towards the target. This concludes the creation of our CombatTeleportTo function. We need something to activate it. This will be the OnHit Event. Every time an NPC is hit, this event is called.
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
float FRand = utility.RandomFloat()
if (bCasting)||(HitAsFollower(akAggressor As Actor))
return
endIf
if (self.GetDistance(akAggressor) > 768)&&(akAggressor.GetCurrentLocation() == self.GetCurrentLocation())
if (FRand > 0.2)
bCasting = True
CombatTeleportTo(akAggressor, 0.4)
endIf
endIf
EndEvent
Our conditions are to check the distance by GetDistance(ObjectReference Target). I am using 768 units. Anything more than 768 units will return true. We also want to make sure the target is in the same cell or location as our NPC. We can use a random float, FRand, to specify the tendency of our NPC's teleportation. We must also set bCasting to True temporarily to make sure we do not overlap multiple calls to the teleport function.
An important function that we haven't yet implemented is the boolean function CheckSuccess, which ensures that the NPC made it to the specified location. This is a simple distance check.
function CheckSuccess(ObjectReference ObjPoint)
if (self.GetDistance(ObjPoint) > 1024)
debug.trace( self + "ERROR: Detected long distance failure")
SetPosition(XFloat, YFloat, ZFloat);
EndTeleport()
endIf
endFunction
Finally, for miscellaneous conditions, we will use another boolean function.
bool function HitAsFollower(Actor ActorToCheck)
if (ActorToCheck == Game.GetPlayer())
return true
endIf
This is optional. It only prevents teleportation if the NPC is hit by the player.
And we are done! This is the bulk of the script.
Scriptname ESXNPCTeleport extends ObjectReference ;by EB/Xoleras xoleras.com - leave this here please! :)
;you can modify this as much as you want from here downward (probably should change the script name though)
;teleportto will always be sequence based, for combat, use CombatTeleportTo
Activator Property OutVis Auto
Activator Property InVis Auto
VisualEffect Property VisEffect Auto
STATIC Property OutLocMarker Auto
float Property XFloat Auto Hidden
float Property YFloat Auto Hidden
float Property ZFloat Auto Hidden
bool Property bCasting Auto
ActorBase Property ActorOwner Auto
function BeginTeleport()
PlaceAtMe(InVis)
endFunction
function EndTeleport()
PlaceAtMe(OutVis)
endFunction
function TeleportTo(ObjectReference TeleportTarget, bool bTargetIsLocation, ObjectReference TeleportLocation = None, float WaitTime)
float XLoc = PolarToRectangularX(TeleportTarget.GetPositionX(), 15, 128)
float YLoc = PolarToRectangularY(TeleportTarget.GetPositionY(), 15, 128) ;to do: add param instead of fixed numbers
float zOffset = GetHeadingAngle(TeleportTarget)
utility.wait(0.1)
debug.trace(self + "If no pass, TeleportTarget may be NONE:" + TeleportTarget)
if (bTargetIsLocation) ;then get offset
debug.trace("Target is Location")
if (TeleportTarget)
BeginTeleport()
utility.wait(0.4)
SetPosition(XLoc, YLoc, TeleportTarget.GetPositionZ()) ; added Z os for adjustments
SetAngle(GetAngleX(), GetAngleY(), GetAngleZ() + zOffset)
endIf
elseif (TeleportLocation)
BeginTeleport()
utility.wait(0.4)
SetPosition(TeleportLocation.GetPositionX(), TeleportLocation.GetPositionY(), TeleportLocation.GetPositionZ())
SetAngle(GetAngleX(), GetAngleY(), TeleportLocation.GetAngleZ())
EndTeleport()
endIf
endFunction
function CombatTeleportTo(ObjectReference TeleportTarget, float WaitTime)
XFloat = self.GetPositionX()
YFloat = self.GetPositionY()
ZFloat = self.GetPositionZ()
float fDir = CalculateDesiredAngle(TeleportTarget)
float XLoc = PolarToRectangularX(TeleportTarget.GetPositionX(), fdir, 64)
float YLoc = PolarToRectangularY(TeleportTarget.GetPositionY(), fdir, 64) ;to do: add prop instead of fixed numbers
utility.wait(0.1)
;debug.trace(self + "COMBAT If no pass, TeleportTarget may be NONE:" + TeleportTarget)
if (TeleportTarget)
BeginTeleport()
utility.wait(0.4)
SetPosition(XLoc, YLoc, TeleportTarget.GetPositionZ()) ; added Z os for adjustments
SetAngle(GetAngleX(), GetAngleY(), GetAngleZ() + GetHeadingAngle(TeleportTarget))
EndTeleport()
utility.wait(2)
CheckSuccess(TeleportTarget)
bCasting = False
endIf
endFunction
function CheckSuccess(ObjectReference ObjPoint)
if (self.GetDistance(ObjPoint) > 1024)
;debug.trace( self + "ERROR: Detected long distance failure")
SetPosition(XFloat, YFloat, ZFloat);
EndTeleport()
endIf
endFunction
bool function HitAsFollower(Actor ActorToCheck)
if (ActorToCheck == Game.GetPlayer())
return true
endIf
endFunction
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
float FRand = utility.RandomFloat()
if (bCasting)||(HitAsFollower(akAggressor As Actor))
return
endIf
;debug.trace( self + "hit detected!")
if (self.GetDistance(akAggressor) > 768)&&(akAggressor.GetCurrentLocation() == self.GetCurrentLocation())
if (FRand > 0.2)
bCasting = True
CombatTeleportTo(akAggressor, 0.4)
endIf
endIf
EndEvent
function DoExit() ;in case we just want to be fancy
PlaceAtMe(OutVis)
utility.wait(0.5)
Disable()
endFunction
float Function CalculateDesiredAngle(ObjectReference RefAngle)
float T = GetHeadingAngle(RefAngle)
if (T < 45)&&(T > -45)
return (T+180)
else
return T
endIf
endFunction
float Function PolarToRectangularY(float rY, float Degrees, float Radius) ;wow, no outbound parameters allowed :/
return (rY + (Radius * Math.cos(Degrees)))
endFunction
float Function PolarToRectangularX(float rX, float Degrees, float Radius)
return (rX + (Radius * Math.sin(Degrees)))
endFunction
Function SetLocalAngle(Float LocalX, Float LocalY, Float LocalZ)
float AngleX = LocalX * Math.Cos(LocalZ) + LocalY * Math.Sin(LocalZ)
float AngleY = LocalY * Math.Cos(LocalZ) - LocalX * Math.Sin(LocalZ)
SetAngle(AngleX, AngleY, LocalZ)
EndFunction
Activator Property ESXSummonTeleFXActivator Auto
A few notes on this:
- The TeleportTo function is not needed for combat, so you can take it out. Actually, you should skim through the code to take out anything you don't want that isn't needed.
- The OutLocMarker can be used for placing visuals before the NPC actually teleports. To do this, simply define a private ObjectReference variable, by inserting it into the function PlaceAtMe(). In example: ObjectReference myRef = PlaceAtMe(Object). Now you can access and specify the spatial properties of this object and move it to the designated location prior to teleportation.
- The SetLocalAngle can be used if needed to get the local angle instead of the "global" angle.
- The DragonRace condition only filters out Dragons, but, for instance, you might want to filter out such things as AlduinRace and other flying races that might not be included if you plan on permitting the NPC to fight such enemies.