Author Topic: Adding SetActionsMap to npc.c  (Read 2390 times)

Offline Lash

  • Acquaintance
  • *
  • Posts: 45
    • View Profile
Adding SetActionsMap to npc.c
« on: October 15, 2011, 12:21:22 pm »
For having an npc that can perform more than one action each at different chances per heartbeat. Having more than one SetAction() function in an NPC or SENTIENT doesn't seem to work or, of course, I'm missing something. Nothing really special here - more or less copying and pasting code from room.c.

Modified file: ds3.6/lib/lib/npc.c

At line 45 added function prototypes:

Code: [Select]
private mapping ActionsMap = ([]);
private int tick_resolution = 5;

Inserted at line 119 in static void init()

Code: [Select]
if((Action && (sizeof(Action) || functionp(Action)))
            || sizeof(ActionsMap)){
        set_heart_beat(tick_resolution);
    }

Inserted at line 139 in static void heartbeat()

Code: [Select]
if( !GetInCombat() && sizeof(ActionsMap)){
        foreach(mixed key, mixed val in ActionsMap){
            if( val > random(100) ){
                if(functionp(key)) evaluate(key);
                else eventPrint(key);
            }
        }
    }

Inserted at line 632 in /lib/npc.c data functions section

Code: [Select]
mapping SetActionsMap(mapping ActMap){
    if(ActMap && sizeof(ActMap)) ActionsMap = ActMap;
    return copy(ActionsMap);
}

mapping GetActionsMap(){
    return copy(ActionsMap);
}

Example npc - a thief that has a chance to steal and pick up stuff as it goes along. I varied the values in SetActionsMap and the functions were called as expected for the SetActionsMap() value. Included is the function CheckNpc() to make sure this guy doesn't bash on other loaded npc's.

Code: [Select]
// Based on Diku MUD Alfa.  Program and Concept created by
// Sebastian Hammer, Michael Seifert, Hans Henrik Staerfeldt,
// Tom Madsen, and Katja Nyboe.
// http://www.dikumud.com
//
// Modified by Lash (Christopher Coker) for use with:
//
// The Dead Souls Mud Library version 2
// developed by Cratylus
// http://www.dead-souls.net

#include <lib.h>

inherit LIB_SENTIENT;

void CheckNPC();
void NpcSteal(object ob);
void Scavenge();

static void create() {
    sentient::create();

    SetKeyName("the thief");
    SetId( ({"thief"}) );
    SetAdjectives(({"non-player", "evil", "tricky"}));
    SetShort("A thief, all dressed in black");
    SetLong("Well, COUNT your money..!");
    SetRace("human");
    SetClass("thief");
    SetLevel(8);
    SetMelee(1);
    SetCanBite(0);
    SetGender("male");
    SetMorality(250); //?
    AddCurrency("gold", 100);
    SetActionsMap( ([
                     ( :Scavenge: ) : 10,
                     ( :NpcSteal: ) : 5,
                   ]) );
    SetWander(5);
    SetInventory( ([
         "/domains/diku-alfa/room/41.zon/meals/4103_slime" :1,
         "/domains/diku-alfa/room/41.zon/meals/4104_slime_poison" :1,
       ]) );
}

void init(){
    ::init();
}

/*Do not attack other NPC's*/
void CheckNPC(object ob){
 
    object env=environment(this_object());
    if(ob && !inherits(LIB_NPC, ob)){
        eventForce("kill " +ob->GetKeyName());
 }
}

void NpcSteal(object ob){

    object env = environment(this_object());
    object *potvictims;
    int n1, n2, gold, level;
    level = this_object()->GetLevel();

    potvictims = filter(get_livings(env), ( :living($1) && $1 != this_object() && playerp($1):) );
   
    foreach(object target in potvictims){
        if(playerp(target)){
            n1 = random(level);
            if(n1 ==0){
                tell_object(target, "You notice " + capitalize(this_object()->GetKeyName()) + " trying to steal from you!");
                break;
            }
       
            if(n1 > 0){
                n2 = random(level)+1;
                gold = target->GetCurrency("gold") * n2 /100;
                this_object()->AddCurrency("gold", gold);
                target->AddCurrency("gold", -gold);
            }
        }
    }
}

void Scavenge(){

    object env = environment(this_object());
    object *item, *cost;
    int s;
   
    item = filter(all_inventory(env), (: !living($1) && (inherits(LIB_ITEM, $1) || inherits(LIB_ARMOR, $1)):) );
    cost = sort_array(item->GetBaseCost(), -1);
    s = sizeof(cost);
    if(s>0){
        foreach(object thing in item){
            if(thing->GetBaseCost() == cost[0]){
                eventForce("get "+thing->GetKeyName());
                break;
            }
        }
    }
}
« Last Edit: October 15, 2011, 12:30:04 pm by Lash »

Offline Lash

  • Acquaintance
  • *
  • Posts: 45
    • View Profile
Re: Adding SetActionsMap to npc.c
« Reply #1 on: October 15, 2011, 12:41:02 pm »
Still editing and missed this - CheckNpc() is usually called in "aggressive" npc files with the following function:

Code: [Select]
SetEncounter( (:CheckNPC:) ); //aggressive
All aggressive NPC's in the domain I'm making kill non-npc's on sight.

Lash
« Last Edit: October 15, 2011, 12:43:01 pm by Lash »

Offline quixadhal

  • BFF
  • ***
  • Posts: 642
    • View Profile
    • WileyMUD
Re: Adding SetActionsMap to npc.c
« Reply #2 on: October 16, 2011, 05:13:39 am »
As it stands, you're iterating across the whole map and firing anything which rolls against the percentage chance, right?

I have a suggestion to make this a bit more flexible.

Instead of a mapping, I suggest using an array because it retains a fixed order of evaluation.  With that, add two additional booleans so each row looks like:
({ function, percentage, iffmode, halt })

iffmode means "only try to execute this if you executed the previous entry", and halt means "if you execute this, stop here"

Then you could set up pretty complex scenarios like the following.
Code: [Select]
({
    ({ (: UnlockDoor :), 50, 0, 0 }),
    ({ (: OpenDoor :), 50, 1, 0 }),
    ({ (: ExclaimHooray :), 100, 1, 1 }),
    ({ (: BangOnDoor :), 50, 0, 0 }),
    ({ (: Grumble :), 50, 1, 1 })
})

In this case, your NPC has a 50% chance to unlock some door.  If it does so, it then has another 50% chance to open it.  If it does THAT, it will then emote something about how it's happy to finally get through the door, and we're done.

If we didn't actually open the door, the NPC has a 50% chance to bang on it, and if it does so, it may then grumble about the door being closed.

You can, of course, get the previous behavior by just always leaving iffmode and halt false, the only difference being that things will always happen in the same order rather than "semi-randomly" based on the order keys() returns.

I saw DikuMUD in there and it reminded me of the way our area resets work, and how useful that "iff" clause is on occasion.

Offline daelaskai

  • BFF
  • ***
  • Posts: 174
    • View Profile
Re: Adding SetActionsMap to npc.c
« Reply #3 on: October 16, 2011, 08:22:59 am »
Very neat idea.  I think the array is a lot more flexible

Offline Lash

  • Acquaintance
  • *
  • Posts: 45
    • View Profile
Re: Adding SetActionsMap to npc.c
« Reply #4 on: October 16, 2011, 01:19:35 pm »
@ Quixadhal:

First, thanks for taking the time to comment. I appreciate it.

Right. some data:

Scavenge set to 90 and NpcSteal set to 5:
Heart Beats: 100
Total calls to NpcSteal() and Scavenge(): 91
NpcSteal() calls: 3
Scavenge() calls: 88


Scavenge set to 5 and NpcSteal set to 90:
Heart Beats: 100
Total calls to NpcSteal() and Scavenge(): 91
NpcSteal() calls: 84
Scavenge() calls: 7


Scavenge set to 50 and NpcSteal set to 50:
Heart Beats: 100
Total calls to NpcSteal() and Scavenge(): 95
NpcSteal() calls: 47
Scavenge() calls: 48


Scavenge set to 10 and NpcSteal set to 5:
Heart Beats: 100
Total calls to NpcSteal() and Scavenge(): 16
NpcSteal() calls: 3
Scavenge() calls: 13

Heart Beats: 1000
Total calls to NpcSteal() and Scavenge(): 148
NpcSteal() calls: 46
Scavenge() calls: 102

So, the chance is set per heart beat to call each function. Note that this also works independently of SetAction(). In fact, I used a SetAction(100, (:count:)); function call to measure the object's heart beats.

As an aside, I haven't figured out how to use the heart_beat function to count heart_beats in an object. The way I tried didn't work - the heart_beat function took precedence over the other function calls.

Anyway, for this project the SetActionsMap() function is working as wanted.
 
Thanks again for commenting. I really like your array suggestion and am eager to try to get that working.

And the revised code to generate the data: (NpcCheck() has also been removed as this NPC is actually not supposed to be aggressive - a work still in progress :sigh:)
Code: [Select]
// Based on Diku MUD Alfa.  Program and Concept created by
// Sebastian Hammer, Michael Seifert, Hans Henrik Staerfeldt,
// Tom Madsen, and Katja Nyboe.
// http://www.dikumud.com
//
// Modified by Lash (Christopher Coker) for use with:
//
// The Dead Souls Mud Library version 2
// developed by Cratylus
// http://www.dead-souls.net

#include <lib.h>

inherit LIB_SENTIENT;

void NpcSteal(object ob);
void Scavenge();
int count();

iint ntimes = 0;
int stimes = 0;
int tot = 0;
int hb = 0;

static void create() {
    sentient::create();

    SetKeyName("the thief");
    SetId( ({"thief"}) );
    SetAdjectives(({"non-player", "evil", "tricky"}));
    SetShort("A thief, all dressed in black");
    SetLong("Well, COUNT your money..!");
    SetRace("human");
    SetClass("thief");
    SetLevel(8);
    SetMelee(1);
    SetCanBite(0);
    SetGender("male");
    SetMorality(250); //?
    AddCurrency("gold", 100);
    SetAction(100, (:count:));
    SetActionsMap( ([
                     ( :Scavenge: ) : 50,
                     ( :NpcSteal: ) : 50,
                 ]) );
    //SetWander(5);
    SetInventory( ([
         "/domains/diku-alfa/room/41.zon/meals/4103_slime" :1,
         "/domains/diku-alfa/room/41.zon/meals/4104_slime_poison" :1,
       ]) );
    SetProperty("STAY_ZONE", 1);
}

void init(){
    ::init();
}

int count(){
   
    hb++;

    tell_room(environment(this_object()), "Heart Beats: "+hb+"\nTotal calls to NpcSteal() and Scavenge(): "+tot+"\nNpcSteal() calls: "+ntimes+"\nScavenge() calls: "+stimes+"\n");
    return hb;
}

void NpcSteal(object ob){

    object env = environment(this_object());
    object *potvictims;
    int n1, n2, gold, level;
    level = this_object()->GetLevel();
   
    tot++;
    ntimes++;

    potvictims = filter(get_livings(env), ( :living($1) && $1 != this_object() && playerp($1):) );

    foreach(object target in potvictims){
        if(playerp(target)){
            n1 = random(level);
            if(n1 ==0){
                tell_object(target, "You notice " + capitalize(this_object()->GetKeyName()) + " trying to steal from you!");
                break;
            }

            if(n1 > 0){
                n2 = random(level)+1;
                gold = target->GetCurrency("gold") * n2 /100;
                this_object()->AddCurrency("gold", gold);
                target->AddCurrency("gold", -gold);
            }
        }
    }
}

void Scavenge(){

    object env = environment(this_object());
    object *item, *cost;
    int s;

    tot++;
    stimes++;
         
    item = filter(all_inventory(env), (: !living($1) && (inherits(LIB_ITEM, $1) || inherits(LIB_ARMOR, $1)):) );
    cost = sort_array(item->GetBaseCost(), -1);
    s = sizeof(cost);
    if(s>0){
        foreach(object thing in item){
            if(thing->GetBaseCost() == cost[0]){
                eventForce("get "+thing->GetKeyName());
                break;
            }
        }
    }
}
« Last Edit: October 16, 2011, 01:36:13 pm by Lash »