trevius
08-16-2011, 11:20 PM
Here are some fun plugins I have been working on a bit lately. Most of these plugins could be used for other purposes, but they were created specifically for spawning an entire zone of NPCs randomly that looks fairly natural.
Here are the steps to add these plugins to your server:
1. Open notepad, or whatever text editor you prefer.
2. Copy and paste the following code into Notepad:
#Usage: my $LoSDistance = plugin::GetMaxLoSDistFromHeading(Heading, [DistIncrements, MaxDist, Mob]);
# This plugin attempts to get the max visible distance from the specified heading for the specified mob
# It returns the Max LoS that it can find for the settings provided
# Heading is the direction to get the max LoS for.
# DistIncrements is how much the distance check will increase each loop it does (default 20).
# Higher distance is more efficient but less precise.
# MaxDist is the maximum distance to check up to for LoS (default 200).
# Mob is an optional field to set the source NPC/Client (default is current NPC)
sub GetMaxLoSDistFromHeading {
my $npc = plugin::val('$npc');
my $client = plugin::val('$client');
my $Heading = $_[0];
my $DistIncrements = $_[1];
my $MaxDist = $_[2];
my $Mob = $_[3];
if (!$DistIncrements)
{
$DistIncrements = 20;
}
if (!$MaxDist)
{
$MaxDist = 200;
}
if (!$Mob)
{
$Mob = $npc;
}
my $LoSMobSize = 5;
my $MaxZDiff = 10;
my $MaxLoS = 0;
my $OrigX = $Mob->GetX();
my $OrigY = $Mob->GetY();
for ($LoSDistCheck = $DistIncrements; $LoSDistCheck <= $MaxDist; $LoSDistCheck += $DistIncrements)
{
my $Degrees = plugin::ConvertHeadingToDegrees($Heading);
#plugin::Debug("Current Distance Check: $LoSDistCheck || Degrees $Degrees");
my $PI = 3.141592653589793238;
my $Radian = $Degrees * ($PI / 180);
my $CircleX = $LoSDistCheck * cos($Radian);
my $CircleY = $LoSDistCheck * sin($Radian);
my $DestX = $CircleX + $OrigX;
my $DestY = $CircleY + $OrigY;
my $DestZ = $Mob->FindGroundZ($DestX, $DestY, $MaxZDiff);
my $LoS_Check = $Mob->CheckLoSToLoc($DestX, $DestY, $DestZ, $LoSMobSize);
if ($LoS_Check)
{
#plugin::Debug("LoS Success");
# If LoS is successful at this distance, continue the loop to check the next distance
$MaxLoS = $LoSDistCheck;
}
else
{
# Once LoS fails, end the loop
$LoSDistCheck = $MaxDist + 1;
}
}
return $MaxLoS;
}
#Usage: plugin::FaceBestHeading([MinLoSDist]);
# This plugin attempts to find a good heading that isn't facing a wall/structure that blocks LoS
# If the NPC is currently facing a wall, this will look for the clearest/longest view available and point that way
# It returns the best heading that it finds as well
# MinLoSDist is the minimum distance to start searching for better headings (default 20)
# If the NPC is already facing a heading that has further LoS than MinLoSDist, it will not change heading
sub FaceBestHeading {
my $npc = plugin::val('$npc');
my $MinLoSDist = $_[0];
if (!$MinLoSDist)
{
$MinLoSDist = 20;
}
# Build an array for the best matching headings
my @BestHeadings = ();
my $CurHeading = $npc->GetHeading();
my $LoSDistance = plugin::GetMaxLoSDistFromHeading($CurHeading);
my $MaxLoSDistance = $LoSDistance;
my $EqualHeadings = 0;
if ($LoSDistance < $MinLoSDist)
{
# Check headings in 16 directions for the best one(s)
for ($HeadingCheck = 0; $HeadingCheck < 256; $HeadingCheck += 16)
{
$LoSDistance = plugin::GetMaxLoSDistFromHeading($HeadingCheck, 5, 35);
if ($LoSDistance >= $MaxLoSDistance)
{
if ($LoSDistance == $MaxLoSDistance && $EqualHeadings > 0)
{
# If this headings max LoS is equal to the previous one, add it to the array of best headings
push(@BestHeadings, $HeadingCheck);
# For equal headings to be added to the array, they must all be consecutive
$EqualHeadings++;
}
else
{
@BestHeadings = ($HeadingCheck);
#plugin::Debug("Array Set to $HeadingCheck");
$MaxLoSDistance = $LoSDistance;
# Reset the equal headings count to 1 when a new max is found
$EqualHeadings = 1;
}
}
else
{
# Set equal headings to 0 if the current heading is less than the current Max LoS
# This ensures that equal headings will always be consecutive
$EqualHeadings = 0;
}
}
my $NewHeading = $CurHeading;
my $BestCount = @BestHeadings;
# If the Heading Array has entries, use the one in the middle of the array
if ($BestCount)
{
# Choose the best heading in the middle of the array to face the most open/clear angle
my $ArrayPick = int($BestCount / 2);
$NewHeading = $BestHeadings[$ArrayPick];
}
$npc->SetHeading($NewHeading);
$npc->SendPosition();
}
return $NewHeading;
}
#Usage: my $ShortestHeading = plugin::HeadingToShortestLoS([MaxDistToCheck=8]);
# MaxDistToCheck is the maximum distance to check for nearby LoS blocking
sub HeadingToShortestLoS {
my $npc = plugin::val('$npc');
my $MaxDistToCheck = $_[0];
if (!$MaxDistToCheck)
{
$MaxDistToCheck = 8;
}
my $OrigX = $npc->GetX();
my $OrigY = $npc->GetY();
my $OrigZ = $npc->GetZ();
my $ZeroLoSCheck = $npc->CheckLoSToLoc(($OrigX + 0.1), $OrigY, $OrigZ, 5);
if (!$ZeroLoSCheck)
{
# If this check fails, they are probably stuck in a wall
return -2;
}
my @BestHeadings = ();
my $CurHeading = $npc->GetHeading();
my $MinLoSDistance = 5000;
for ($HeadingCheck = 0; $HeadingCheck < 256; $HeadingCheck += 16)
{
$LoSDistance = plugin::GetMaxLoSDistFromHeading($HeadingCheck, 1, $MaxDistToCheck);
if ($LoSDistance <= $MinLoSDistance)
{
if ($LoSDistance == $MinLoSDistance)
{
push(@BestHeadings, $HeadingCheck);
}
else
{
@BestHeadings = ($HeadingCheck);
$MinLoSDistance = $LoSDistance;
}
}
}
my $NewHeading = $CurHeading;
my $BestCount = @BestHeadings;
# If the Heading Array has entries, use the one in the middle of the array
if ($BestCount && $BestCount < 15)
{
my $ArrayPick = int($BestCount / 2);
$NewHeading = $BestHeadings[$ArrayPick];
}
else
{
# Return -1 if no nearby objects found blocking LoS
return -1;
}
return $NewHeading;
}
#Usage: my $NPCMoved = plugin::MoveAwayFromWall([MinLoSDist=5]);
# MinLoSDist is the minimum distance to start searching for better headings (default 40)
# If the NPC is already facing a heading that has further LoS than MinLoSDist, it will not change heading
sub MoveAwayFromWall {
my $npc = plugin::val('$npc');
my $ShortestHeading = plugin::HeadingToShortestLoS();
if ($ShortestHeading == -2)
{
# Stuck in a wall
#$npc->Depop();
return -1;
}
elsif ($ShortestHeading == -1)
{
# Not too close to a wall
#plugin::FaceBestHeading(25);
return 0;
}
else
{
# Close to a wall
my @DestArray = plugin::CalcDestFromHeading($ShortestHeading, 5);
my $DestX = $DestArray[0];
my $DestY = $DestArray[1];
my $DestZ = $DestArray[2];
my $LoS_Check = $npc->CheckLoSToLoc($DestX, $DestY, $DestZ, 5);
# If this check fails, they are too close to the wall
if (!$LoS_Check)
{
# Face them toward the wall
$npc->SetHeading($ShortestHeading);
# Get the reverse heading from the wall
my $ReverseHeading = plugin::GetReverseHeading();
#my $LoSDistance = plugin::GetMaxLoSDistFromHeading($ReverseHeading, 1, 5);
@DestArray = plugin::CalcDestFromHeading($ReverseHeading, 10);
$DestX = $DestArray[0];
$DestY = $DestArray[1];
$DestZ = $DestArray[2];
$LoS_Check = $npc->CheckLoSToLoc($DestX, $DestY, $DestZ, 5);
# If there is enough room, move them the opposite direction from the nearest wall
if ($LoS_Check)
{
@DestArray = plugin::CalcDestFromHeading($ReverseHeading, 10);
$DestX = $DestArray[0];
$DestY = $DestArray[1];
$DestZ = $DestArray[2];
$npc->GMMove($DestX, $DestY, ($DestZ + 1), $ReverseHeading);
# Return true if moved
return 1;
}
else
{
# Not enough room to move toward or away from the wall
# Move toward the best heading
my $BestHeading = plugin::FaceBestHeading();
@DestArray = plugin::CalcDestFromHeading($BestHeading, 6);
$DestX = $DestArray[0];
$DestY = $DestArray[1];
$DestZ = $DestArray[2];
$npc->GMMove($DestX, $DestY, ($DestZ + 1), $BestHeading);
# Return true if moved
return 1;
}
}
}
# If no move, return false
return 0;
}
#Usage: plugin::MoveToFirstBestZ();
sub MoveToFirstBestZ {
my $npc = plugin::val('$npc');
my $x = plugin::val('$x');
my $y = plugin::val('$y');
my $h = plugin::val('$h');
$npc->GMMove($x, $y, 1000, $h);
my $GroundZ = $npc->FindGroundZ($x, $y, 3);
if($GroundZ > -5000)
{
$npc->GMMove($x, $y, ($GroundZ -10), $h);
$NextGroundZ = $npc->FindGroundZ($x, $y, 3);
if($NextGroundZ > -5000)
{
$npc->GMMove($x, $y, $NextGroundZ, $h);
}
else
{
$npc->GMMove($x, $y, $GroundZ, $h);
}
}
}
#Usage: plugin::SpawnZone(X, Y, Z, Distance, Variance, Columns, Rows);
# This is used to spawn a grid of NPCs and can be used to spawn an entire zone
# X/Y/Z are the coords of the first NPC to spawn that the others are spawned based on it's location
# Distance is the distance each member of the formation will be from each other on both axis
# Variance is the max distance of scatter effect you want on the grid positioning to make it look less like a grid
# Columns is the number of columns you want in the formation
# Rows is the number of rows you want in the formation
sub SpawnZone {
my $entity_list = plugin::val('$entity_list');
my $SpawnX = $_[0];
my $SpawnY = $_[1];
my $SpawnZ = $_[2];
my $Distance = $_[3];
my $Variance = $_[4];
my $Columns = $_[5];
my $Rows = $_[6];
$Heading = plugin::RandomRange(0, 254);
$MaxZDiff = 3;
# Create the array of NPCIDs to spawn in the grid
my @NPCIDList = ();
my $NPCNum = 7;
while ($_[$NPCNum])
{
push(@NPCIDList, $_[$NPCNum]);
$NPCNum++;
}
my $ListLength = (@NPCIDList) - 1;
my $FirstNPCID = $NPCIDList[0];
# Set the first NPC to spawn directly in the center of the grid
$SpawnX = $SpawnX - (($Rows - 1) * $Distance / 2);
$SpawnY = $SpawnY - (($Columns - 1) * $Distance / 2);
# Spawn the first NPC
quest::spawn2($FirstNPCID, 0, 0, $SpawnX, $SpawnY, $SpawnZ, $Heading);
# Get the first NPC
my $MainNPC = $entity_list->GetNPCByNPCTypeID($FirstNPCID);
my $NewX = $SpawnX;
my $NewY = $SpawnY;
$NewX = $NewX + $Distance;
if ($MainNPC)
{
for ($ColNum = 0; $ColNum < $Columns; $ColNum++)
{
for ($RowNum = 0; $RowNum < $Rows; $RowNum++)
{
# Prevent Respawn over the first NPC
if ($RowNum > 0 || $ColNum > 0)
{
my $RandXDiff = plugin::RandomRange(0, $Variance);
my $RandYDiff = plugin::RandomRange(0, $Variance);
# Set even numbers to negative
if ($RandXDiff && ($RandXDiff / 2) == int($RandXDiff / 2))
{
$RandXDiff *= -1;
}
if ($RandYDiff && ($RandYDiff / 2) == int($RandYDiff / 2))
{
$RandYDiff *= -1;
}
my $ModX = $NewX + $RandXDiff;
my $ModY = $NewY + $RandYDiff;
my $NewZ = $MainNPC->FindGroundZ($ModX, $ModY, $MaxZDiff);
if ($NewZ > -5000)
{
my $RandomNPCNum = plugin::RandomRange(0, $ListLength);
my $NPCID = $NPCIDList[$RandomNPCNum];
my $RandHeading = plugin::RandomRange(0, 254);
quest::spawn2($NPCID, 0, 0, $ModX, $ModY, $NewZ, $RandHeading);
}
$NewX = $NewX + $Distance;
}
}
$NewY = $NewY + $Distance;
$NewX = $SpawnX;
}
}
}
#Usage: my $ReverseHeading = plugin::GetReverseHeading([$mob]);
# Returns the heading of the opposite direction the mob is facing
sub GetReverseHeading {
my $npc = plugin::val('$npc');
my $Mob = $_[0];
if (!$Mob)
{
$Mob = $npc;
}
my $CurHeading = $Mob->GetHeading();
my $ReverseHeading = 128 + $CurHeading;
if ($ReverseHeading >= 256)
{
$ReverseHeading = $ReverseHeading - 256;
}
return $ReverseHeading;
}
#Usage: my $Degrees = plugin::ConvertHeadingToDegrees(Heading);
# Converts 0-256 headings into 0 to 360 degrees
sub ConvertHeadingToDegrees {
my $Heading = $_[0];
my $ReverseHeading = 256 - $Heading;
my $ConvertAngle = $ReverseHeading * 1.40625;
if ($ConvertAngle <= 270)
{
$ConvertAngle = $ConvertAngle + 90;
}
else
{
$ConvertAngle = $ConvertAngle - 270;
}
return $ConvertAngle;
}
return 1; #This line is required at the end of every plugin file in order to use it
3. Save that file to your server /plugins/ folder and name it "spawn_tools.pl".
4. Do a #questreload and the new plugin should be ready for use
Then, to make use of these plugins, you will need some extra scripts. Here is one you can put on any NPC. I currently use it on a GM tool pet I made, but you could add it to any NPC in the zone you are working on. This does the actual spawning of the zone. It is best to position the NPC in the center of the zone so your grid reaches all sides of the zone. Depending on the size if your zone, you may need to adjust the number of columns, rows or distance to fill the entire zone.
This example includes Guk LDoN NPC IDs, but you can just replace the NPC IDs with any other IDs you want that fit your zone. I used gukc as my test zone for this script, so you may want to try it there just to get an idea of how it works and to see the results.
sub EVENT_SAY {
my $SpawnZone = quest::saylink("Spawn Zone", 0, "Spawn the Zone");
if ($status > 20)
{
if ($text =~/Hail/i)
{
quest::say("Are you ready to [ $SpawnZone ]?");
}
if ($text =~/Spawn Zone/i)
{
quest::say("Starting Zone Spawn");
#Usage: plugin::SpawnZone(X, Y, Z, Distance, Variance, Columns, Rows);
plugin::SpawnZone($x, $y, $z, 50, 20, 40, 40,
264200, # A_Blighted_Jin_Master
264228, # A_Cursed_Guktan
264202, # A_Cursed_Korta_Researcher
264223, # A_Dazzling_Oculus
264220, # A_Decaying_Yunta_Witchdoctor
264221, # A_Doomed_Yunta_Witchdoctor
264217, # A_Ghoulish_Dar_Warrior
264212, # A_Ghoulish_Darta_Knight
264219, # A_Miserable_Jinta_Phantasm
264213, # A_Witness_of_Hate_Bonecollector
264216, # A_Witness_of_Hate_Seer
264226, # A_Witness_of_Hate_assassin
264214, # A_Witness_of_Hate_knave
264207, # a_cursed_burrower
264215, # a_forlorn_Darta_soldier
264218 # an_unsanctified_Korta_priest
);
quest::say("Zone Spawn complete");
}
}
else
{
quest::say("Sorry, I only speak with GMs");
}
}
That will get the zone spawned, but you will still have NPCs that are half stuck in walls or inside them completely and you will have lots of NPCs facing walls or headings that just don't look natural. To resolve that, you will want to create a default.pl file in zone folder you are working/testing in and add the following to it:
# default.pl - Moves away from walls and sets Best Heading
sub EVENT_SPAWN {
# Move away from walls and set the best heading
my $HeadingTimer = plugin::RandomRange(2,5);
quest::settimer("BestHeading", $HeadingTimer);
}
sub EVENT_TIMER {
if ($timer eq "BestHeading")
{
quest::stoptimer("BestHeading");
my $NPCMoved = plugin::MoveAwayFromWall();
if ($NPCMoved == -1)
{
# Stuck in a wall
plugin::Debug("Stuck in wall - Depopping");
$npc->Depop();
}
else
{
plugin::FaceBestHeading(25);
}
}
}
Note that some of these scripts are modified a bit from the ones I use to make them easier for the general public. Since I didn't actually test some of the ones posted here exactly as they are posted, there may be issues but I am pretty sure they should work fine. Also, some of these plugins require other plugins that you can find in the Akkadius Plugin Repository if you don't already have them. The scripts will fail if you don't have all of the required plugins.
These plugins are pretty resource intensive, so I wouldn't recommend over-using them. I mainly made them to assist in new zone development. I plan to spawn a zone with the plugin, then use another script that will save all of the spawns to the DB. These scripts could probably be used to create dynamic zones for players, but if you do that, I would keep it pretty limited and in smaller zones.
I hope you guys like these plugins. I know I am pretty impressed with their results so far. They aren't quite perfect, but they are good enough for getting a very nice head-start on spawning a zone from scratch. You could spawn a zone with this in seconds and clean it up with #spawnfix as needed and have a very nice looking zone in under an hour easily. You would still want to spawn named manually and such as needed, but this works well for random trash spawns.
I may continue this idea and add a plugin that checks around an NPC when it spawns and determines if it has enough room to use the RandomRoam plugin effectively and set it if so. This would get things close to being able to make a dynamically spawned and pathed zone.
Here are the steps to add these plugins to your server:
1. Open notepad, or whatever text editor you prefer.
2. Copy and paste the following code into Notepad:
#Usage: my $LoSDistance = plugin::GetMaxLoSDistFromHeading(Heading, [DistIncrements, MaxDist, Mob]);
# This plugin attempts to get the max visible distance from the specified heading for the specified mob
# It returns the Max LoS that it can find for the settings provided
# Heading is the direction to get the max LoS for.
# DistIncrements is how much the distance check will increase each loop it does (default 20).
# Higher distance is more efficient but less precise.
# MaxDist is the maximum distance to check up to for LoS (default 200).
# Mob is an optional field to set the source NPC/Client (default is current NPC)
sub GetMaxLoSDistFromHeading {
my $npc = plugin::val('$npc');
my $client = plugin::val('$client');
my $Heading = $_[0];
my $DistIncrements = $_[1];
my $MaxDist = $_[2];
my $Mob = $_[3];
if (!$DistIncrements)
{
$DistIncrements = 20;
}
if (!$MaxDist)
{
$MaxDist = 200;
}
if (!$Mob)
{
$Mob = $npc;
}
my $LoSMobSize = 5;
my $MaxZDiff = 10;
my $MaxLoS = 0;
my $OrigX = $Mob->GetX();
my $OrigY = $Mob->GetY();
for ($LoSDistCheck = $DistIncrements; $LoSDistCheck <= $MaxDist; $LoSDistCheck += $DistIncrements)
{
my $Degrees = plugin::ConvertHeadingToDegrees($Heading);
#plugin::Debug("Current Distance Check: $LoSDistCheck || Degrees $Degrees");
my $PI = 3.141592653589793238;
my $Radian = $Degrees * ($PI / 180);
my $CircleX = $LoSDistCheck * cos($Radian);
my $CircleY = $LoSDistCheck * sin($Radian);
my $DestX = $CircleX + $OrigX;
my $DestY = $CircleY + $OrigY;
my $DestZ = $Mob->FindGroundZ($DestX, $DestY, $MaxZDiff);
my $LoS_Check = $Mob->CheckLoSToLoc($DestX, $DestY, $DestZ, $LoSMobSize);
if ($LoS_Check)
{
#plugin::Debug("LoS Success");
# If LoS is successful at this distance, continue the loop to check the next distance
$MaxLoS = $LoSDistCheck;
}
else
{
# Once LoS fails, end the loop
$LoSDistCheck = $MaxDist + 1;
}
}
return $MaxLoS;
}
#Usage: plugin::FaceBestHeading([MinLoSDist]);
# This plugin attempts to find a good heading that isn't facing a wall/structure that blocks LoS
# If the NPC is currently facing a wall, this will look for the clearest/longest view available and point that way
# It returns the best heading that it finds as well
# MinLoSDist is the minimum distance to start searching for better headings (default 20)
# If the NPC is already facing a heading that has further LoS than MinLoSDist, it will not change heading
sub FaceBestHeading {
my $npc = plugin::val('$npc');
my $MinLoSDist = $_[0];
if (!$MinLoSDist)
{
$MinLoSDist = 20;
}
# Build an array for the best matching headings
my @BestHeadings = ();
my $CurHeading = $npc->GetHeading();
my $LoSDistance = plugin::GetMaxLoSDistFromHeading($CurHeading);
my $MaxLoSDistance = $LoSDistance;
my $EqualHeadings = 0;
if ($LoSDistance < $MinLoSDist)
{
# Check headings in 16 directions for the best one(s)
for ($HeadingCheck = 0; $HeadingCheck < 256; $HeadingCheck += 16)
{
$LoSDistance = plugin::GetMaxLoSDistFromHeading($HeadingCheck, 5, 35);
if ($LoSDistance >= $MaxLoSDistance)
{
if ($LoSDistance == $MaxLoSDistance && $EqualHeadings > 0)
{
# If this headings max LoS is equal to the previous one, add it to the array of best headings
push(@BestHeadings, $HeadingCheck);
# For equal headings to be added to the array, they must all be consecutive
$EqualHeadings++;
}
else
{
@BestHeadings = ($HeadingCheck);
#plugin::Debug("Array Set to $HeadingCheck");
$MaxLoSDistance = $LoSDistance;
# Reset the equal headings count to 1 when a new max is found
$EqualHeadings = 1;
}
}
else
{
# Set equal headings to 0 if the current heading is less than the current Max LoS
# This ensures that equal headings will always be consecutive
$EqualHeadings = 0;
}
}
my $NewHeading = $CurHeading;
my $BestCount = @BestHeadings;
# If the Heading Array has entries, use the one in the middle of the array
if ($BestCount)
{
# Choose the best heading in the middle of the array to face the most open/clear angle
my $ArrayPick = int($BestCount / 2);
$NewHeading = $BestHeadings[$ArrayPick];
}
$npc->SetHeading($NewHeading);
$npc->SendPosition();
}
return $NewHeading;
}
#Usage: my $ShortestHeading = plugin::HeadingToShortestLoS([MaxDistToCheck=8]);
# MaxDistToCheck is the maximum distance to check for nearby LoS blocking
sub HeadingToShortestLoS {
my $npc = plugin::val('$npc');
my $MaxDistToCheck = $_[0];
if (!$MaxDistToCheck)
{
$MaxDistToCheck = 8;
}
my $OrigX = $npc->GetX();
my $OrigY = $npc->GetY();
my $OrigZ = $npc->GetZ();
my $ZeroLoSCheck = $npc->CheckLoSToLoc(($OrigX + 0.1), $OrigY, $OrigZ, 5);
if (!$ZeroLoSCheck)
{
# If this check fails, they are probably stuck in a wall
return -2;
}
my @BestHeadings = ();
my $CurHeading = $npc->GetHeading();
my $MinLoSDistance = 5000;
for ($HeadingCheck = 0; $HeadingCheck < 256; $HeadingCheck += 16)
{
$LoSDistance = plugin::GetMaxLoSDistFromHeading($HeadingCheck, 1, $MaxDistToCheck);
if ($LoSDistance <= $MinLoSDistance)
{
if ($LoSDistance == $MinLoSDistance)
{
push(@BestHeadings, $HeadingCheck);
}
else
{
@BestHeadings = ($HeadingCheck);
$MinLoSDistance = $LoSDistance;
}
}
}
my $NewHeading = $CurHeading;
my $BestCount = @BestHeadings;
# If the Heading Array has entries, use the one in the middle of the array
if ($BestCount && $BestCount < 15)
{
my $ArrayPick = int($BestCount / 2);
$NewHeading = $BestHeadings[$ArrayPick];
}
else
{
# Return -1 if no nearby objects found blocking LoS
return -1;
}
return $NewHeading;
}
#Usage: my $NPCMoved = plugin::MoveAwayFromWall([MinLoSDist=5]);
# MinLoSDist is the minimum distance to start searching for better headings (default 40)
# If the NPC is already facing a heading that has further LoS than MinLoSDist, it will not change heading
sub MoveAwayFromWall {
my $npc = plugin::val('$npc');
my $ShortestHeading = plugin::HeadingToShortestLoS();
if ($ShortestHeading == -2)
{
# Stuck in a wall
#$npc->Depop();
return -1;
}
elsif ($ShortestHeading == -1)
{
# Not too close to a wall
#plugin::FaceBestHeading(25);
return 0;
}
else
{
# Close to a wall
my @DestArray = plugin::CalcDestFromHeading($ShortestHeading, 5);
my $DestX = $DestArray[0];
my $DestY = $DestArray[1];
my $DestZ = $DestArray[2];
my $LoS_Check = $npc->CheckLoSToLoc($DestX, $DestY, $DestZ, 5);
# If this check fails, they are too close to the wall
if (!$LoS_Check)
{
# Face them toward the wall
$npc->SetHeading($ShortestHeading);
# Get the reverse heading from the wall
my $ReverseHeading = plugin::GetReverseHeading();
#my $LoSDistance = plugin::GetMaxLoSDistFromHeading($ReverseHeading, 1, 5);
@DestArray = plugin::CalcDestFromHeading($ReverseHeading, 10);
$DestX = $DestArray[0];
$DestY = $DestArray[1];
$DestZ = $DestArray[2];
$LoS_Check = $npc->CheckLoSToLoc($DestX, $DestY, $DestZ, 5);
# If there is enough room, move them the opposite direction from the nearest wall
if ($LoS_Check)
{
@DestArray = plugin::CalcDestFromHeading($ReverseHeading, 10);
$DestX = $DestArray[0];
$DestY = $DestArray[1];
$DestZ = $DestArray[2];
$npc->GMMove($DestX, $DestY, ($DestZ + 1), $ReverseHeading);
# Return true if moved
return 1;
}
else
{
# Not enough room to move toward or away from the wall
# Move toward the best heading
my $BestHeading = plugin::FaceBestHeading();
@DestArray = plugin::CalcDestFromHeading($BestHeading, 6);
$DestX = $DestArray[0];
$DestY = $DestArray[1];
$DestZ = $DestArray[2];
$npc->GMMove($DestX, $DestY, ($DestZ + 1), $BestHeading);
# Return true if moved
return 1;
}
}
}
# If no move, return false
return 0;
}
#Usage: plugin::MoveToFirstBestZ();
sub MoveToFirstBestZ {
my $npc = plugin::val('$npc');
my $x = plugin::val('$x');
my $y = plugin::val('$y');
my $h = plugin::val('$h');
$npc->GMMove($x, $y, 1000, $h);
my $GroundZ = $npc->FindGroundZ($x, $y, 3);
if($GroundZ > -5000)
{
$npc->GMMove($x, $y, ($GroundZ -10), $h);
$NextGroundZ = $npc->FindGroundZ($x, $y, 3);
if($NextGroundZ > -5000)
{
$npc->GMMove($x, $y, $NextGroundZ, $h);
}
else
{
$npc->GMMove($x, $y, $GroundZ, $h);
}
}
}
#Usage: plugin::SpawnZone(X, Y, Z, Distance, Variance, Columns, Rows);
# This is used to spawn a grid of NPCs and can be used to spawn an entire zone
# X/Y/Z are the coords of the first NPC to spawn that the others are spawned based on it's location
# Distance is the distance each member of the formation will be from each other on both axis
# Variance is the max distance of scatter effect you want on the grid positioning to make it look less like a grid
# Columns is the number of columns you want in the formation
# Rows is the number of rows you want in the formation
sub SpawnZone {
my $entity_list = plugin::val('$entity_list');
my $SpawnX = $_[0];
my $SpawnY = $_[1];
my $SpawnZ = $_[2];
my $Distance = $_[3];
my $Variance = $_[4];
my $Columns = $_[5];
my $Rows = $_[6];
$Heading = plugin::RandomRange(0, 254);
$MaxZDiff = 3;
# Create the array of NPCIDs to spawn in the grid
my @NPCIDList = ();
my $NPCNum = 7;
while ($_[$NPCNum])
{
push(@NPCIDList, $_[$NPCNum]);
$NPCNum++;
}
my $ListLength = (@NPCIDList) - 1;
my $FirstNPCID = $NPCIDList[0];
# Set the first NPC to spawn directly in the center of the grid
$SpawnX = $SpawnX - (($Rows - 1) * $Distance / 2);
$SpawnY = $SpawnY - (($Columns - 1) * $Distance / 2);
# Spawn the first NPC
quest::spawn2($FirstNPCID, 0, 0, $SpawnX, $SpawnY, $SpawnZ, $Heading);
# Get the first NPC
my $MainNPC = $entity_list->GetNPCByNPCTypeID($FirstNPCID);
my $NewX = $SpawnX;
my $NewY = $SpawnY;
$NewX = $NewX + $Distance;
if ($MainNPC)
{
for ($ColNum = 0; $ColNum < $Columns; $ColNum++)
{
for ($RowNum = 0; $RowNum < $Rows; $RowNum++)
{
# Prevent Respawn over the first NPC
if ($RowNum > 0 || $ColNum > 0)
{
my $RandXDiff = plugin::RandomRange(0, $Variance);
my $RandYDiff = plugin::RandomRange(0, $Variance);
# Set even numbers to negative
if ($RandXDiff && ($RandXDiff / 2) == int($RandXDiff / 2))
{
$RandXDiff *= -1;
}
if ($RandYDiff && ($RandYDiff / 2) == int($RandYDiff / 2))
{
$RandYDiff *= -1;
}
my $ModX = $NewX + $RandXDiff;
my $ModY = $NewY + $RandYDiff;
my $NewZ = $MainNPC->FindGroundZ($ModX, $ModY, $MaxZDiff);
if ($NewZ > -5000)
{
my $RandomNPCNum = plugin::RandomRange(0, $ListLength);
my $NPCID = $NPCIDList[$RandomNPCNum];
my $RandHeading = plugin::RandomRange(0, 254);
quest::spawn2($NPCID, 0, 0, $ModX, $ModY, $NewZ, $RandHeading);
}
$NewX = $NewX + $Distance;
}
}
$NewY = $NewY + $Distance;
$NewX = $SpawnX;
}
}
}
#Usage: my $ReverseHeading = plugin::GetReverseHeading([$mob]);
# Returns the heading of the opposite direction the mob is facing
sub GetReverseHeading {
my $npc = plugin::val('$npc');
my $Mob = $_[0];
if (!$Mob)
{
$Mob = $npc;
}
my $CurHeading = $Mob->GetHeading();
my $ReverseHeading = 128 + $CurHeading;
if ($ReverseHeading >= 256)
{
$ReverseHeading = $ReverseHeading - 256;
}
return $ReverseHeading;
}
#Usage: my $Degrees = plugin::ConvertHeadingToDegrees(Heading);
# Converts 0-256 headings into 0 to 360 degrees
sub ConvertHeadingToDegrees {
my $Heading = $_[0];
my $ReverseHeading = 256 - $Heading;
my $ConvertAngle = $ReverseHeading * 1.40625;
if ($ConvertAngle <= 270)
{
$ConvertAngle = $ConvertAngle + 90;
}
else
{
$ConvertAngle = $ConvertAngle - 270;
}
return $ConvertAngle;
}
return 1; #This line is required at the end of every plugin file in order to use it
3. Save that file to your server /plugins/ folder and name it "spawn_tools.pl".
4. Do a #questreload and the new plugin should be ready for use
Then, to make use of these plugins, you will need some extra scripts. Here is one you can put on any NPC. I currently use it on a GM tool pet I made, but you could add it to any NPC in the zone you are working on. This does the actual spawning of the zone. It is best to position the NPC in the center of the zone so your grid reaches all sides of the zone. Depending on the size if your zone, you may need to adjust the number of columns, rows or distance to fill the entire zone.
This example includes Guk LDoN NPC IDs, but you can just replace the NPC IDs with any other IDs you want that fit your zone. I used gukc as my test zone for this script, so you may want to try it there just to get an idea of how it works and to see the results.
sub EVENT_SAY {
my $SpawnZone = quest::saylink("Spawn Zone", 0, "Spawn the Zone");
if ($status > 20)
{
if ($text =~/Hail/i)
{
quest::say("Are you ready to [ $SpawnZone ]?");
}
if ($text =~/Spawn Zone/i)
{
quest::say("Starting Zone Spawn");
#Usage: plugin::SpawnZone(X, Y, Z, Distance, Variance, Columns, Rows);
plugin::SpawnZone($x, $y, $z, 50, 20, 40, 40,
264200, # A_Blighted_Jin_Master
264228, # A_Cursed_Guktan
264202, # A_Cursed_Korta_Researcher
264223, # A_Dazzling_Oculus
264220, # A_Decaying_Yunta_Witchdoctor
264221, # A_Doomed_Yunta_Witchdoctor
264217, # A_Ghoulish_Dar_Warrior
264212, # A_Ghoulish_Darta_Knight
264219, # A_Miserable_Jinta_Phantasm
264213, # A_Witness_of_Hate_Bonecollector
264216, # A_Witness_of_Hate_Seer
264226, # A_Witness_of_Hate_assassin
264214, # A_Witness_of_Hate_knave
264207, # a_cursed_burrower
264215, # a_forlorn_Darta_soldier
264218 # an_unsanctified_Korta_priest
);
quest::say("Zone Spawn complete");
}
}
else
{
quest::say("Sorry, I only speak with GMs");
}
}
That will get the zone spawned, but you will still have NPCs that are half stuck in walls or inside them completely and you will have lots of NPCs facing walls or headings that just don't look natural. To resolve that, you will want to create a default.pl file in zone folder you are working/testing in and add the following to it:
# default.pl - Moves away from walls and sets Best Heading
sub EVENT_SPAWN {
# Move away from walls and set the best heading
my $HeadingTimer = plugin::RandomRange(2,5);
quest::settimer("BestHeading", $HeadingTimer);
}
sub EVENT_TIMER {
if ($timer eq "BestHeading")
{
quest::stoptimer("BestHeading");
my $NPCMoved = plugin::MoveAwayFromWall();
if ($NPCMoved == -1)
{
# Stuck in a wall
plugin::Debug("Stuck in wall - Depopping");
$npc->Depop();
}
else
{
plugin::FaceBestHeading(25);
}
}
}
Note that some of these scripts are modified a bit from the ones I use to make them easier for the general public. Since I didn't actually test some of the ones posted here exactly as they are posted, there may be issues but I am pretty sure they should work fine. Also, some of these plugins require other plugins that you can find in the Akkadius Plugin Repository if you don't already have them. The scripts will fail if you don't have all of the required plugins.
These plugins are pretty resource intensive, so I wouldn't recommend over-using them. I mainly made them to assist in new zone development. I plan to spawn a zone with the plugin, then use another script that will save all of the spawns to the DB. These scripts could probably be used to create dynamic zones for players, but if you do that, I would keep it pretty limited and in smaller zones.
I hope you guys like these plugins. I know I am pretty impressed with their results so far. They aren't quite perfect, but they are good enough for getting a very nice head-start on spawning a zone from scratch. You could spawn a zone with this in seconds and clean it up with #spawnfix as needed and have a very nice looking zone in under an hour easily. You would still want to spawn named manually and such as needed, but this works well for random trash spawns.
I may continue this idea and add a plugin that checks around an NPC when it spawns and determines if it has enough room to use the RandomRoam plugin effectively and set it if so. This would get things close to being able to make a dynamically spawned and pathed zone.