I’m looking for a script (creature’s OnDeath), where if the player has an item and kills that creature, an integer is added. Once that integer reaches a certain number, the item is deleted and replaced with another item in the player’s inventory.
For example, the player is given an item “quest001” and needs to kill 10 goblins. If the player has “quest001” in his inventory, every goblin he kills an integer is added until it reaches 10. At that point “quest001” is deleted from the player’s inventory and replace with “quest001x”. Possibly even adding some text to the item’s description when a player examines it. “1/10 Goblins Slain”, “2/10 Goblins Slain”, etc…
Would also like the integer to be added if another player (or summon) kills that creature if they are within range (regardless if they are in the same party or not).
Sounds like you’ve got it all worked out already, and all that’s left to do is to write it in NWScript.
Use GetFirstObjectInShape() and GetNextObjectInShape() to cycle through all objects within a radius around the dying creature and increment the variables on the items in the inventories of all players within the radius.
Find items in the player’s inventory using GetItemPossessedBy().
Retrieve and store local integers using GetLocalInt() and SetLocalInt().
Destroy inventory items using DestroyObject().
Alter an item description by using SetDescription().
And use CreateItemOnObject() to create a new item from a template.
The OnDeath events are said to fire unreliably, though. If testing confirms that and it’s a problem, you could try setting the creatures to immortal, and use OnDamaged instead. Make it fire if they take damage while at 1 HP, or something, and take away their immortality and kill them via the script.
It’s the user-defined script that is reported to be unreliable on death, but there’s no need to use that.
As a rule of thumb, the OnDeath script should only contain functions that execute immediately. Assigning commands to the corpse will fail, because their action queue is about to close. However, assigning commands to the module or other enduring objects will work.
Thank you! I looked all those up and this is what I’ve come up with.
This is probably a mess, so I’d love some help cleaning it up (plus I may be way off on this!). The only thing I couldn’t figure out is the SetDescription.
void main()
{
int nValue;
location oLocation = GetLocation(OBJECT_SELF);
object oTarget = GetObjectByTag(“quest001”);
object oPC = GetFirstObjectInShape(SHAPE_SPHERE,10.0,oLocation);
do {
if ( GetItemPossessedBy(oPC, “quest001”) != OBJECT_INVALID )
{
if ( GetLocalInt(oTarget, “quest001”) >= 10 )
{
DestroyObject(GetItemPossessedBy(oPC, “quest001”));
CreateItemOnObject(“quest001x”, oPC);
}
nValue = GetLocalInt(oTarget, "quest001") + 1;
SetLocalInt(oTarget, "quest001", nValue);
//Need Help Here
//SetDescription(oTarget, ("nValue" "/10 Goblins Slain"));
}
}
while ((oPC = GetNextObjectInShape(SHAPE_SPHERE,10.0,oLocation))!= OBJECT_INVALID);
}
I messed around with the script (longer than I should have) with no success. I’m not sure if the integer is being added to the item, or if the destroy/create part is wrong. I tried moving things around in different orders… pretty much got nowhere in the few hours of testing last night.
Tested on OnDeath of placeables. Seems to work. No guarantees nothing’ll catch on fire, as always.
Feedback on script:
Be aware that you need different functions to find items that are inside somebody’s inventory, and items that are inside (any) area. GetObjectByTag() only finds inventory items while they’re lying on the ground in an area. At the moment, in your version, oTarget is trying to find an item tagged “quest001” anywhere in the module except for in the player’s inventory, and then checking again whether the player has an item with that tag in their inventory before it does anything - such as increment the counter on the external object and possibly destroy the inventory object, if both exist.
You can concatenate strings using +. (sName+", Burner of “+sVegetables+”!")
Floats and integers need to be turned into strings before they can be used in one, using IntToString() and FloatToString().
Also: SetDescription, by itself, replaces the entirety of the previous description. In case you want the quest tracker items to have descriptions in addition to the counter, check out some of the functions from x3_inc_string, such as StringReplace() and StringParse().
A well-placed SendMessageToPC() is priceless for figuring out where things are going wrong. You can write a custom debug message-sending function to use in your script, so you can turn all debug messages off again at once and don’t need to edit them all out one at a time again later.
You can also use SetName() to change the name of the item, so the player sees the state of the counter on mouseover, rather than having to open the description.
And it’d probably make sense to add a GetIsPC() check on the very first level of the while loop, so that non-players don’t have their inventories checked for the item, just to be conscientious. A crowd of dying goblins shouldn’t all be checking eachother’s inventories, and those of nearby placeables.
I’m wanting the area’s onexit to delete these spawns with a delay of 5min. But, if a player enters that area before they are deleted, the timer restarts when they exit. I have other creatures in the area that I don’t want deleted, so it would be nice to delete these creatures based on tags or avoid deleting certain creatures by their tags.
You can specify a new tag in CreateObject - handy for deleting them all later.
The OnExit script needs to maintain the PC count in a similar manner to OnEnter, of course.
Your 5 second delay issue is a good example of why you don’t always want DelayCommand to be fire-and-forget. The situation may have changed by then.
Fortunately, there is a general-purpose solution: use DelayCommand to ExecuteScript. This can fire a new script which first checks whether it’s still needed. In this case, delay the new script for 5 seconds. In the new script, if the PC count is still zero, delete everything with the tag you made earlier.
I think we’ve had this recently. The area won’t execute delayed commands if there is no player in it anymore, right? hm.
Try wrapping the despawning up, and immediately assigning it to the module or something.
void AreaRemoveSpawns(object oArea)
{
// Do not despawn if there are players in the area.
if (GetLocalInt(oArea, "PCcount") > 0) return;
// Destroy spawned objects.
object oTarget = GetFirstObjectInArea(oArea);
while (GetIsObjectValid(oTarget))
{
if (GetStringLeft(GetTag(oTarget), 8) == "SPAWNED_") DestroyObject(oTarget);
oTarget = GetNextObjectInArea(oArea);
}
}
void AreaDespawnCheck(object oArea, float fDelay)
{
DelayCommand(fDelay, AreaRemoveSpawns(oArea));
}
And then, after an object has exited, and if nPCcount has sunk to zero, AssignCommand(GetModule(), AreaDespawnCheck(oArea, 10.0);
I’ve set the tag checking to look for the prefix “SPAWNED_” in the objects’ tags, here. Replace that with whatever. If you’re working with the EE version, you can do something like
SetTag(oTarget, "SPAWNED_"+GetTag(oTarget));
Also: With the spawn destroying being delayed, if your PC exits and immediately reenters the area they’d be spawning additional monsters. You may want to add another local variable to the area, to check whether the spawning’s been done.
I can enter the area and everything spawns just fine. When I leave, only those creatures are deleted, everything else stays as intended. However, when I return to the area again, nothing is spawning.
Also, still not sure how to set up a delay to delete and stop the delay if another player (or the same player) enters that area.
Have you tried rubber ducking your way through it? Ducks are secret problem solving wizards. They just look innocent and harmless, don’t let them fool you. It’s like garlic and vampires, just with ducks and coding problems.
To help fix the current problems:
The PCcount variable is being incremented in the OnEnter script, but not decremented again in the OnExit script. By the second time your PC enters the area, nPCcount is 2, and thus trips over the “abort script if PCcount variable on area is greater than 1” line.
If you’ve got your heart set on a resetting/refilling counter, you could try something like this:
void AreaDespawnCounter(object oArea)
{
int nCounter = GetLocalInt(OBJECT_SELF, “DESPAWN_COUNTER_”+ObjectToString(oArea));
// Stop the counter if there are PCs in the area.
if (GetLocalInt(oArea, "PCcount") > 0)
return;
if (nCounter >= 1)
{
SetLocalInt(OBJECT_SELF, "DESPAWN_COUNTER_"+ObjectToString(oArea), --nCounter);
DelayCommand(1.0, AreaDespawnCounter(oArea));
}
else
{
// Destroy spawned objects.
object oTarget = GetFirstObjectInArea(oArea);
while (GetIsObjectValid(oTarget))
{
if (GetStringLeft(GetTag(oTarget), 8) == "SPAWNED_")
DestroyObject(oTarget);
oTarget = GetNextObjectInArea(oArea);
}
}
Two “if the exiting object is not a pc, abort script”-checks at the beginning.
One of those will do the trick just fine.
oTag is not being changed within the while loop.
As is, you’re only checking for the tag of the first object in the area, and then destroying all objects in the area if the first object’s tag is either “succubus” or “doom”.
The condition in the while loop is very strange.
I imagine the reason it works is because the value of the OBJECT_TYPE_CREATURE constant is not zero (therefore being TRUE), and apparently the first object in the area is either tagged “doom” or “succubus” (therefore, if TRUE is equal to TRUE (which it is, so… TRUE?)).
This still hinges on the tag of the first object, though, because “if ((FALSE) is equal to TRUE)” would amount to FALSE again(?).
In any case, there is no checking for whether oObject is a creature being done here. To check whether an object is a creature, use GetObjectType(). if (GetObjectType(oObject) == OBJECT_TYPE_CREATURE)
OnEnter:
Reiterating: If your PC exits and immediately reenters the area, and the despawning is time-delayed, they’ll be spawning additional monsters.
You need another variable to check for whether the spawning has been done, which you delete along with the spawned monsters.
Also: You could save yourself some hassle with listing the spawning waypoints one at a time if you set up a default spawn function that goes through all the waypoints in the area one at a time and creates the matching object if it finds one attached to the waypoint. Waypoints can have variables set on them, too.
And: Use debug messages! Make invisible problems visible by adding well-placed SendMessageToPCs or SpeakStrings.
Thank you for the help! I’m think I might be going about this the wrong way. Trying to come up with an easy solution to reducing lag on my server. Right now creatures are all spawned on module load and each one has a 5 min respawn timer. It was fine when things were small, but as I’ve added more areas, you can really tell the server is struggling.
So, my thought was to spawn creatures per area instead as players enter each area. If the player leaves any creatures alive in an area, then clean up the remaining after a few minutes (in case the player returns). Once the area has been cleaned up, reset the on enter spawn stuff and start all over.
Problem is, I don’t know what’s the best way about doing that.
I get all my scripting experience from singleplayer, so I’m not really qualified to make comments on PW optimization. :-/ Going by what they’re saying in that writ, though, you’re on the right track with that thought.
Being a lazy sort of builder, I’d go about it by setting up a single “spawncreature” waypoint template that I’d just plonk down in places and alter which kind of creature it spawns by attaching the resref to the waypoint as a variable.
Read this, commented line by line:
// Cycle through nearest waypoints to oSource until none are found, and
// spawn creatures at their location matching the resref stored on the
// waypoint as "SPAWN_TEMPLATE" string.
void Area_DoSpawns(object oSource);
void Area_DoSpawns(object oSource)
{
object oSpawn;
object oTarget;
string sTemplate;
int i;
int nSpawns;
// Start counting at 1.
i = 1;
// Begin with locating the nearest waypoint to the entering PC.
oTarget = GetNearestObject(OBJECT_TYPE_WAYPOINT, oSource, i);
// If a waypoint is found...
while (GetIsObjectValid(oTarget))
{
// ... try to find a template spawn string on it.
sTemplate = GetLocalString(oTarget, "SPAWN_TEMPLATE");
// If the waypoint has a spawn string...
if (sTemplate != "")
// ... spawn the template creature at the location of the waypoint.
oSpawn = CreateObject(OBJECT_TYPE_CREATURE, sTemplate, GetLocation(oTarget));
// Check the next waypoint.
oTarget = GetNearestObject(OBJECT_TYPE_WAYPOINT, oSource, ++i);
}
}
(love this code formatting tip. I think @Proleric originally brought it up in another thread. [code] place text here [/code])
Is there a way to script a conversation to check if the player has the item “quest001” and depending on the Int “QUEST_COUNTER” will tell the player how many goblins they have left to kill in the conversation?
Sure. You can use GetItemPossessedBy() to check for the presence of the item by tag, and SetCustomToken to turn the integer into a string token usable in conversations.