Aller au contenu

Photo

How To "Wildcard" a String? (Efficient Beggar Script Development WIP)


  • Veuillez vous connecter pour répondre
61 réponses à ce sujet

#1
Morbane

Morbane
  • Members
  • 1 883 messages

I happened across a NWN1 post that the OP was looking for a beggar script solution, that would make a 'beggar' pick up dropped items etc.

 

I want to implement a similar script that the 'beggar' will appear at any given time/location/place and sneak up and grab the dropped item.

 

So my question is; by location, how do I script the beggar to spawn wherever the PC is when an item is "unacquired"?

 

My first guess is to have a location/areatag wildcard string..

 

But since I have never needed to script a wildcard, I am hoping for an option to this pre-script-design question.

 

pseudocode:

 

PC unacquires item;

 

beggar spawns at the nearest line-of-sight-blocked location

 

HB makes beggar move to location and pick up dropped (unacquired) item

 

::beggar has a store with picked up items as stock::

 

 

thanks for suggestions :)



#2
Lugaid of the Red Stripes

Lugaid of the Red Stripes
  • Members
  • 955 messages

My first impulse is to have a special waypoint in each area for the beggar to spawn in at.  Instead of doing a complicated check for LOS, it could be tucked away in some purpose-built nook or back alley, so that it would be unlikely that the player would see the beggar spawn in. 

 

One way to get the specific waypoint would be to give each waypoint the same tag, and then use GetNearestObjectByTag to get the waypoint nearest the PC.  That, by definition, would be in the same area as the PC.  You could even put multiple waypoints in each area, and even iterate from the nearest to the PC to the most distant  to try and find the one best hidden by LOS.

 

A second way would be to use concatenated strings.  If the area has tag "AREATAG", then the waypoint could be "AREATAG_beggarspawn", or GetTag(oArea) + "_beggarspawn" in script.



#3
Lance Botelle

Lance Botelle
  • Members
  • 1 480 messages

<SNIP> 
pseudocode:
 
PC unacquires item;
 
beggar spawns at the nearest line-of-sight-blocked location
 
HB makes beggar move to location and pick up dropped (unacquired) item
 
::beggar has a store with picked up items as stock::
 
 
thanks for suggestions :)


Hi Morbane,

You pretty much have your answer in your pseudo code. :) (Which, by the way, is how I do my coding in my head before I apply the syntax.) Let's look at your lines one by one. Please let me know if there is anything you do not understand specifically:-

1) PC unacquires item .... So this script is going to take place in the Module's On UnaquireItem Hook and will use the format of:-
 
object oItem = GetModuleItemLost(); // Refers to the item dropped
object oPC = GetModuleItemLostBy(); // Refers to the PC dropping the item
location lPC = GetLocation(oPC); // Refers to the location of the PC


2) beggar spawns at the nearest line-of-sight-blocked location ... Can be spawned/called from the same piece of code you are using above.

3) HB makes Make beggar move to location and pick up dropped (unacquired) item ... You don't need to make the beggar's HB do the work, but can continue using the same code as you started above in the Unacquire hook.

4) ::beggar has a store with picked up items as stock:: ... This part would require more looking into, but can be achieved by creating a STORE object and then placing the picked up items on the store. The hardest part would be keeping track/copying the store items around with the beggar, but it should be possible, especially as you can create and destroy copies of the store items along with the appearance/spawning of the beggar. (EDITED FOR CLARITY. Ref was to keeping track of items on store or on beggar - and not necessarily both.)
 
Bottom line, all should be possible using the module's OnUnaquire Hook ... define a specific function to call to handle the task.
 
Hope that helps,
Lance.
 
EDIT: The advantage of this method is that it does not require any other objects to be defined at build time, like waypoints suggested by the last poster. This can be safely handled by using function like GetIsLocationValid.

#4
Loki_999

Loki_999
  • Members
  • 430 messages

2) I've done something similar with henchmen on my server but i don't worry about line of sight. What I didn't want was the NPC spawning too close to the PC, otherwise it breaks immersion, so i have them spawn some distance away.  I do this by using a function (CalcSafeLocation from x0_i0_position) that generates a random location within a radius around the PC.  I then instruct the NPC to move to the PCs location.

 

4) The standard open store script searches for the store closest to the calling object with the right tag. This is a problem if your beggar can appear in different areas.  What I have done is create a special area which only contains stores and the open store script was changed to search this area only and all stores were moved into it.  This means an NPC can be anywhere and still access its store.  It also means you don't need to go around creating/destroying store objects but can have a persistent one. The advantage of this approach is also when a store needs updating, only a single small area needs to be changed, rather than a potentially large area, and it makes locating and managing all stores much much easier.

 

Anyway, i don't see the need for any sort of wildcard string here.



#5
Lance Botelle

Lance Botelle
  • Members
  • 1 480 messages

<SNIP>What I have done is create a special area which only contains stores and the open store script was changed to search this area only and all stores were moved into it.<SNIP>


Hi,

I was just coming back to suggest the same thing actually. :) As I assume the beggar will be a "one-off" character. However, even that can be accommodated.

Lance.

#6
Dann-J

Dann-J
  • Members
  • 3 161 messages

You're basically scripting a ball boy!

 

I've experimented with something similar, except with loot bags left over after battles (they're easier than dropped items, as loot bag objects all have the same tag). Instead of picking up everything one at a time, a local lad (or lass) you've hired runs around collecting things for you, which end up either in a chest or in a store. Another possibility I've toyed with is a 'Wand of Loot Collection' that automatically picks up everything within a certain radius of the user. The game Sacred II had a mass-pickup function that did something similar.

 

There is a companion setting that makes them pick up any items on the ground (loot bags or deliberately dropped items) when they're not otherwise engaged. If that behaviour isn't hard-coded, then it might be buried somewhere in the companion scripts.



#7
andysks

andysks
  • Members
  • 1 645 messages

 

There is a companion setting that makes them pick up any items on the ground (loot bags or deliberately dropped items) when they're not otherwise engaged. If that behaviour isn't hard-coded, then it might be buried somewhere in the companion scripts.

 

One of the custom companion AIs in the vault does that. That one is certainly not a secret how to do it, so it might be a place to dig?

 

Edit: This one was supposed to be a quote...



#8
kevL

kevL
  • Members
  • 4 052 messages

I took a browse through 'hench_o0_act' ( tony's version )
and it looks like these are the three practical functions:
 

location lSelf = GetLocation(OBJECT_SELF);
object oItem = GetFirstObjectInShape(SHAPE_SPHERE, 15.f, lSelf, FALSE, OBJECT_TYPE_ITEM);
ActionPickUpItem(oItem);


#9
Morbane

Morbane
  • Members
  • 1 883 messages

excellent advice and information - thanks.

 

i would have gotten here sooner but my modem chucked it this morning

 

the reason i was thinking around the wildcard was sort of a backward approach to tag based scripting - but the module unacquireitem hook sounds like a solution 

 

one thing though; i'm not entirely clear on how a store object can be accessed in a mini area from another area - or is that totally wrong how i read it? i s'pose looping through store objects - i was always of the mind that the store object had to be "right there" to work.

 

i'll start experimenting with the information you all shared and update when success or imminent failure beckon another post

 

cheers :)



#10
Loki_999

Loki_999
  • Members
  • 430 messages

Regarding accessing stores in another area.

 

First thing to understand is that just because an object is in another area it does not mean you cannot interact with it or is it any less efficient.  From the game engine perspective they are all just objects in memory.

 

What the default openstore script does (ga_openstore???) is takes the location of the object calling the script and search the area around (in a widening arc) for a store with the right tag (IIRC).  It may also have a secondary search take place if it does not find the store called.  This is why people usually put their stores close to their merchants, its as much for game efficiency purposes as association.

 

What i did was change the open store script to instead search all store objects in a specified area for the right one.  Nothing else goes in the area, not even a waypoint.

 

This has several advantages:

Managing stores  and only having to update 1 area (instead of every area where you want to change stores)

Ability for multiple vendors to access the same store

Ability for dynamic vendors who can appear in different areas (i made a waiter NPC who could be called into service in any location) to have a static store

The script does not have to check the object type of every object that is closer to the caller than the store you need and no distance checks are required.


  • Morbane aime ceci

#11
Lance Botelle

Lance Botelle
  • Members
  • 1 480 messages
Hi,

And just to add to the above post, if you give your stores unique tags, then you can be sure you are accessing the correct one regardless of where it is placed in a module. i.e. You don't have to alter the open store script as far as I can see, as the script is only searching for one store with your unique tag. Bottom line ... Unique store tags means easy access from anywhere in the module.

Lance.

#12
Loki_999

Loki_999
  • Members
  • 430 messages

Open store function does require an object passing to it, so you can't just use the tag.  Unique store names though are best practice regardless.

 

Also, the open store script can be used to take into account appraise markup and markdown, which then affects the store buy/sell price.

 

Oh, and here is a little bit of info that i discovered a while back.  Concerns the Shadow Thief of Amn Reputation feat and the bonus Merchant's Friend feat.

 

They both have similar descriptions, but they work differently.

 

Reputation affects store prices based on their cost after markup/markdown is applied.

Merchant's Friend affects store prices based on their cost before markup/markdown - but applies the change after the markup/markdown.

 

What does this mean?

 

Consider an item which has a value of 100,000 before any adjustments.

 

Now, if there is no markup/down applied, then either feat will make the price 90%. Having both will give a price in the region of 100k*0.9*0.9 = 81,000 (i think because they are calcualted separately, so not 80,000 as you might expect).

 

However, now let's calculate based on a markdown of 90% on the store.

 

Someone without either feat would get a price of 10k

With Reputation it would be 10k - (10k*0.1) = 9000 (changing calculation to minus 10% to make it more obvious what happens)

With Merchant's Fiend it would be 10k - (100,000 * 0.1) = 10,000 - 10,000 - oooh, look, its free!

With both it gets worse! 0 - 1000 = -1000 - yes, a negative price!

 

Be very careful of including Merchant's Friend feat if you use more than a few % of markup/markdown in your stores.



#13
Tchos

Tchos
  • Members
  • 5 030 messages
First thing to understand is that just because an object is in another area it does not mean you cannot interact with it or is it any less efficient.  From the game engine perspective they are all just objects in memory.

 

To add to that, not only are all objects in the game accessible from any area, but every single object's scripts seem to be running all the time, as I learned when one of my boss characters shouted a SpeakString that I heard all the way in a different area because he had perceived one of his own minions and I hadn't set him to only shout if he perceived a PC character.  You can also spawn or destroy things in areas other than the one you're in, teleport NPCs from one area to another, and pretty much anything else.



#14
kevL

kevL
  • Members
  • 4 052 messages

... not only are all objects in the game accessible from any area ...


edit: currently loaded module


a small pt. but worth keeping in mind...

#15
Tchos

Tchos
  • Members
  • 5 030 messages

Yes, I forgot to make that distinction.  You can't directly affect things in a separate module linked through a campaign.  You have to prepare for that sort of thing ahead of time and do it indirectly in that case, and it won't take effect until the module is loaded.



#16
Lance Botelle

Lance Botelle
  • Members
  • 1 480 messages

<SNIP> Open store function does require an object passing to it, so you can't just use the tag.  Unique store names though are best practice regardless. <SNIP>


Hi Loki_999,

The OC function ga_open_store requests a TAG to find the object. I know I have edited my own version, but after checking the OC, it still works with a TAG. Therefore, the function is not requiring an object as you say, unless we are talking about two different functions. If we are, then I recommend using the one I quote here, as it works on TAGs ... which can be unique. :)

Lance.

EDIT: Here is the complete OC function I am referring to, where sTag can be UNIQUE:-

#include "ginc_param_const"
#include "ginc_item"

void main(string sTag, int nMarkUp, int nMarkDown)
{
object oPC = (GetPCSpeaker()==OBJECT_INVALID?OBJECT_SELF:GetPCSpeaker());
//OpenStore(GetTarget(sTag), oPC, nMarkUp, nMarkDown);
N2_AppraiseOpenStore(GetTarget(sTag), oPC, nMarkUp, nMarkDown);

}

#17
Loki_999

Loki_999
  • Members
  • 430 messages

Yup, you see that line that is commented out? The OpenStore one? That requires an object.

 

You see N2_AppraiseOpenStore? It also takes an object. ;)

 

Sure, you pass in a tag, but its an object that is actually passed to the open store function.  So, you need to look at the function GetTarget to see how that works, and determine if is is the most efficient function for the module's configuration.



#18
Lance Botelle

Lance Botelle
  • Members
  • 1 480 messages

Yup, you see that line that is commented out? The OpenStore one? That requires an object.
 
You see N2_AppraiseOpenStore? It also takes an object. ;)
 
Sure, you pass in a tag, but its an object that is actually passed to the open store function.  So, you need to look at the function GetTarget to see how that works, and determine if is is the most efficient function for the module's configuration.

Hi Loki_999,

But that "object" (as you call it) can be defined by a unique TAG. Here is the full OC code for GetTarget (I have emboldened and underlined the important parts). NB: The function eventually searches for the object by TAG (first in the immediate area and then throughout the module). As I say, if there is only one store object with a given TAG, then only the UNIQUE store will be returned, no matter where it is in the module. :-

 

I hope that clarifies what I mean ... and you can see how I understand it as working with unique tags. :)

 

Cheers,

Lance.

// Return Target by tag or special identifier
// Leave sTarget blank to use sDefault override
object GetTarget(string sTarget, string sDefault=TARGET_OWNER)
{
object oTarget = OBJECT_INVALID;

// If sTarget is blank, use sDefault
if ("" == sTarget || "0" == sTarget) sTarget = sDefault;

// Check if sTarget is a special identifier
if (IsParameterConstant(sTarget))
{
string sIdentifier = sTarget;
sIdentifier = GetStringUpperCase(sIdentifier);

if (TARGET_OWNER == sIdentifier) oTarget = OBJECT_SELF;
else if (TARGET_OBJECT_SELF == sIdentifier) oTarget = OBJECT_SELF;
else if (TARGET_OWNED_CHAR == sIdentifier) oTarget = GetOwnedCharacter(OBJECT_SELF);
else if (TARGET_PC == sIdentifier) oTarget = GetPCSpeaker();
else if (TARGET_PC_LEADER == sIdentifier) oTarget = GetFactionLeader(GetFirstPC());
else if (TARGET_PC_NEAREST == sIdentifier) oTarget = GetNearestPC();
else if (TARGET_PC_SPEAKER == sIdentifier) oTarget = GetPCSpeaker();
else if (TARGET_MODULE == sIdentifier) oTarget = GetModule();
else if (TARGET_LAST_SPEAKER == sIdentifier) oTarget = GetLastSpeaker();
else
{
PrettyError("GetTarget() -- " + sIdentifier + " not recognized as special identifier!");
}
}
else
{
oTarget = GetNearestObjectByTag(sTarget); // Search area


//EPF 4/13/06 -- get nearest misses if the owner is the object we're looking for
// so check and see if the target is OBJECT_SELF. I'm putting this after the GetNearest()
// call since string compares are expensive, but before the GetObjectByTag() call, since
// that's liable to return the wrong instance. We can move this to before the GetNearest() call
// if this becomes a problem.
if(!GetIsObjectValid(oTarget))
{
if(sTarget == GetTag(OBJECT_SELF))
{
oTarget = OBJECT_SELF;
}
}
// If not found
if (GetIsObjectValid(oTarget) == FALSE)
{
oTarget = GetObjectByTag(sTarget); // Search module
}
}

// If not found
if (GetIsObjectValid(oTarget) == FALSE)
{
PrettyDebug("GetTarget() -- Could not find target with tag: " + sTarget);
}

return (oTarget);
}



#19
Loki_999

Loki_999
  • Members
  • 430 messages

Got it ;)



#20
Morbane

Morbane
  • Members
  • 1 883 messages
#include "x0_i0_position"

void doPickupItem(object oItem)
{
    ActionForceMoveToLocation(GetLocation(oItem));
    ActionPickUpItem(oItem);
    ActionDoCommand(SetCommandable(TRUE));
    SetCommandable(FALSE);
}

void main()
{
    object oPC = GetModuleItemLostBy();
	object oItem = GetModuleItemLost();
    object oActualPossessor = GetItemPossessor(oItem);
	
	location lBegSpawn = GetRandomLocation(GetAreaFromLocation(GetLocation(oItem)), oItem, 10.0f);
	if(!LineOfSightObject(oItem, GetNearestObjectToLocation(OBJECT_TYPE_PLACEABLE, lBegSpawn, Random(5))))
	{
		 CreateObject(OBJECT_TYPE_CREATURE, "the_grey_man", lBegSpawn);
	}
    if (!GetIsObjectValid(oActualPossessor))
    {
        object oBeggar = GetNearestObjectByTag("the_grey_man", oPC);
        if (GetIsObjectValid(oBeggar))
        {
            SetAILevel(oBeggar, AI_LEVEL_VERY_LOW);
            AssignCommand(oBeggar, ClearAllActions());
            DelayCommand(1.0, AssignCommand(oBeggar, doPickupItem(oItem))); // don't remove this delay
            DelayCommand(18.0, SetAILevel(oBeggar, AI_LEVEL_DEFAULT));
            return;
        }
    }
}

The beggar spawns but he doesn't go for the dropped item...

 

beggar does not actually spawn out of sight, but usually behind the PC or something similar.

 

oActualPossessor might not be working right, but i've been slapping this around with no better results.

 

any suggestions are appreciated :)



#21
Loki_999

Loki_999
  • Members
  • 430 messages

When things don't work as expected, its time to start dropping in debug messages to see what is going on.

 

Regarding how far the beggar spawns, 10.0f is a very small radius. Especially since it can appear anywhere within that radius. Try increasing it.  You could even generate say 3 locations and select the farthest (i do this with my faction henchmen scripts).

 

As for the beggar not going for the item there are two possibilities i see immediately.

 

1) oBeggar is not valid. Does your beggar have a the tag of "the_grey_man"?  If he is spawning then you have the resref correct, but also check the tag.

 

2) Setting AI level very low should stop any heartbeat script jumping in and messing with the assigned commands.  However, i don't trust those functions and when i want NPCs to behave I load a blank script set onto the NPC until their commands are finished, so no heartbeat is associated.  Rather than mess around with that, instead, just for a test, remove the heartbeat script from the NPC and see if it works.



#22
Lance Botelle

Lance Botelle
  • Members
  • 1 480 messages

#include "x0_i0_position"

void doPickupItem(object oItem)
{
    ActionForceMoveToLocation(GetLocation(oItem));
    ActionPickUpItem(oItem);
    ActionDoCommand(SetCommandable(TRUE));
    SetCommandable(FALSE);
}

void main()
{
    object oPC = GetModuleItemLostBy();
	object oItem = GetModuleItemLost();
    object oActualPossessor = GetItemPossessor(oItem);
	
	location lBegSpawn = GetRandomLocation(GetAreaFromLocation(GetLocation(oItem)), oItem, 10.0f);
	if(!LineOfSightObject(oItem, GetNearestObjectToLocation(OBJECT_TYPE_PLACEABLE, lBegSpawn, Random(5))))
	{
		 CreateObject(OBJECT_TYPE_CREATURE, "the_grey_man", lBegSpawn);
	}
    if (!GetIsObjectValid(oActualPossessor))
    {
        object oBeggar = GetNearestObjectByTag("the_grey_man", oPC);
        if (GetIsObjectValid(oBeggar))
        {
            SetAILevel(oBeggar, AI_LEVEL_VERY_LOW);
            AssignCommand(oBeggar, ClearAllActions());
            DelayCommand(1.0, AssignCommand(oBeggar, doPickupItem(oItem))); // don't remove this delay
            DelayCommand(18.0, SetAILevel(oBeggar, AI_LEVEL_DEFAULT));
            return;
        }
    }
}

The beggar spawns but he doesn't go for the dropped item...
 
beggar does not actually spawn out of sight, but usually behind the PC or something similar.
 
oActualPossessor might not be working right, but i've been slapping this around with no better results.
 
any suggestions are appreciated :)


Hi Morbane,

Good to see you give this a go ... here are some suggestions:-

1) The function Random can potentially return a zero, which is no good to the GetNearestObjectToLocation function. Change it to using a dice roll "d" function, like d4() or d6(), which always give a positive number.

2) The beggar object should be defined at creation to work with. i.e. Define it prior to its creation and then continue to use the defined object with the rest of the code.

3) I am not sure why you are checking for line of sight. This appears superfluous to requirements, so perhaps you can explain its inclusion for me. :)

4) The AI functions and SetCommandable may not be required here either, especially if the beggar does not do much else. i.e. The beggar can have a "reset" of their actions in their heartbeat script if need be.

Here is a quick bit of code that works for me .... see if it does what you need ... or alter as you require ... let me know how you get on and if you understand what I have done. :)
 
// BEGGAR SCRIPT

#include "x0_i0_position"

void main()
{
    object oPC = GetModuleItemLostBy();
	object oItem = GetModuleItemLost();
	object oActualPossessor = GetItemPossessor(oItem);			
	
    // MAKE SURE THE OBJECT HAS BEEN DROPPED TO THE FLOOR
	if (oActualPossessor == OBJECT_INVALID)
    {    
		location lBegSpawn = GetRandomLocation(GetAreaFromLocation(GetLocation(oItem)), oItem, 10.0f);	
		object oBeggar = CreateObject(OBJECT_TYPE_CREATURE, "the_grey_man", lBegSpawn);
		AssignCommand(oBeggar, ClearAllActions());
		AssignCommand(oBeggar, ActionForceMoveToObject(oItem, TRUE));
		AssignCommand(oBeggar, ActionPickUpItem(oItem));
    }
}
Don't forget that this is very basic code just to get you up and running. You would also need to consider what happens when the player drops another item in my above code, as another beggar would be spawned. There are two ways to deal with this .... make sure you add a bit of code that checks for the beggar's existence prior to spawn, or make sure only once instance can be created at a time. The former option is the correct way.

OK, Here is the sort of thing I mean (untested):-
 
// BEGGAR SCRIPT

#include "x0_i0_position"

void main()
{
    object oPC = GetModuleItemLostBy();
	object oItem = GetModuleItemLost();
	object oActualPossessor = GetItemPossessor(oItem);
	
    // MAKE SURE THE OBJECT HAS BEEN DROPPED TO THE FLOOR
	if (oActualPossessor == OBJECT_INVALID)
    {    
		// I THERE A BEGGAR IN THE AREA ALREADY?
		object oBeggar = GetNearestObjectByTag("the_grey_man", oItem);		
		
		// CREATE BEGGAR IF MISSING FROM THIS AREA
		if(oBeggar == OBJECT_INVALID)
		{
			location lBegSpawn = GetRandomLocation(GetAreaFromLocation(GetLocation(oItem)), oItem, 10.0f);
			oBeggar = CreateObject(OBJECT_TYPE_CREATURE, "the_grey_man", lBegSpawn);
		}
		
		// GET BEGGAR TO DO THEIR THING!
		AssignCommand(oBeggar, ClearAllActions());
		AssignCommand(oBeggar, ActionForceMoveToObject(oItem, TRUE));
		AssignCommand(oBeggar, ActionPickUpItem(oItem));
    }
}
Of course, there are other things you need to consider:-

1) Does the player just want to drop this to the ground for a moment?
2) Will the beggar spawn in a location that cannot easily reach said item? (Although the function I use may help force the beggar to reach the location of the item here.)
3) What happens if the player drops another item after dropping the first?

I'll let you wrestle with some of those ... as I better have a go at doing some of my mod now. ;)

Lance.

#23
Lance Botelle

Lance Botelle
  • Members
  • 1 480 messages
Hi Morbane,

Any news or update from you with respect to you and this script request?

Do you have success yet .... or abandoned the idea?

Any feedback is appreciated. It may help me to help others.

Thanks,

Lance.

#24
Morbane

Morbane
  • Members
  • 1 883 messages

Lance,

 

I had a bit of a hiatus due to a nasty viral-type flu.

 

I'll get back on it this weekend.

 

My goal is to have a global beggar that is intended to be a bit of comic relief, appearing in any area sneaking up and grabbing the dropped item(s) then sulking away.

 

Beggar will be based in the starting area for access to the beggar store.  :ph34r:



#25
Lance Botelle

Lance Botelle
  • Members
  • 1 480 messages

Lance,
 
I had a bit of a hiatus due to a nasty viral-type flu.
 
I'll get back on it this weekend.
 
My goal is to have a global beggar that is intended to be a bit of comic relief, appearing in any area sneaking up and grabbing the dropped item(s) then sulking away.
 
Beggar will be based in the starting area for access to the beggar store.  :ph34r:


Hi Morbane,

Sorry to hear about your illness ... You have my sympathies.

Get back to posting when you get the chance so we can hear if you are successful and achieved what you wanted from this.

Many thanks,
Lance.