Logic Scripts

[ LiB ]

Logic scripts are the real meat behind the game and can be used to accomplish anything you put your mind to.

Can't Get No Satisfaction

There will be times in the game when you want an item to exist, but you want it to be impossible for a character to pick up and carry that item. You could implement some kind of a weight system, and give the item an incredibly large weight value, but that doesn't make it impossible to pick up. There's always a chance someone could gather enough strength to lift it.

For example, a sorcerer could cast a magic "bind" spell on an item, so that it cannot be lifted until someone removes the spell. For casting the spell, you can create a cantget logic module.

When I started writing this logic module, I wasn't quite prepared for how simple this would be. Observe:

 class cantget( data.logics.logic.logic ):     def Run( self, action, arg1, arg2, arg3, arg4, data ):         if action == "cangetitem":             c = BetterMUD.character( arg1 )             me = BetterMUD.item( arg2 )             self.mud.AddActionAbsolute( 0, "vision", c.Room(), 0, 0, 0,                 c.Name() + " almost has a hernia, trying to pull " +                 me.Name() + " out of the ground!" )             return 1 

Whenever an item with this logic module gets a query cangetitem , it simply prints a short error message (to the room, so that everyone sees the character trying to pick up an item that is too big), and returns 1, signifying that it won't allow the action.

That's it! Now, when creating your item templates, all you need to do is this:

 [ID]                    1 [NAME]                  Fountain [DESCRIPTION]           This is a large fountain, made of granite and marble. [ISQUANTITY]            0 [QUANTITY]              1 [DATABANK] [/DATABANK]  [LOGICS]   cantget   [/LOGICS]  

Pay attention to the last three lines that state that the fountain item template will have the cantget logic module. Now, in the game, whenever someone tries getting a fountain, something like this happens:

 Mithrandir almost has a hernia, trying to pull Fountain out of the ground! 

Ta-da! Let's move on to a more complex module.

Receiving Items

Since objects can be given scripts, you're inevitably going to have "malicious" itemsitems that may injure or attack people when they are picked up. Using your imagination , I'm sure you can think of all kinds of nasty items; hidden bombs that eventually blow up, magical artifacts that drain your health; cursed objects that you can't drop, but weigh you down, and so on.

The BetterMUD allows players to give items to other players, and this is dangerous, since the game by default lets characters accept the items. Any joker can go up to a player and hand him a dangerous object, and that can make for extremely nasty game playno fun. In fact, if you allow this to happen, I assure you that one of two things will happen: no one will play the game anymore, or you'll end up removing all dangerous items, which entails removing a very interesting part of the game completely.

The "Can't Receive Items" Module

To ensure the game remains fun to play, I've come up with a way to reject receiving items, using the cantreceiveitems logic module. Here's the module:

 class cantreceiveitems( data.logics.logic.logic ):     def Run( self, action, arg1, arg2, arg3, arg4, data ):         if action == "canreceiveitem":             g = BetterMUD.character( arg1 )             if not g.IsPlayer(): return 0             i = BetterMUD.item( arg3 )             me = BetterMUD.character( self.me )             g.DoAction( "error", 0, 0, 0, 0, "You can't give " +                         me.Name() + "" + i.Name() + " but you have                         item receiving turned off. Type \"/receive on\"                         to turn receiving back on." )             me.DoAction( "error", 0, 0, 0, 0, g.Name() +                          " tried to give you " + i.Name() +                          " but you have item receiving turned off.                          Type \"/receive on\" to turn receiving back on." )             return 1 

When the game sends an entity an action event, that event is passed into every logic module that entity has. This module for example, responds to the canreceiveitem event, which is an event that the game poses to a character when a character is trying to give another character an item. When this module gets an action event or query, the parameters correspond to those that I defined in Chapter 14. For the canreceiveitem query, action will be canreceiveitem , arg1 will be the ID of the character trying to give the item, arg2 will be the ID of the character receiving the item and arg3 will be the ID of the item being given. The fourth argument and the data parameter are unused.

NOTE

I've made the script automatically accept items given by non-player characters (NPCs). This is a personal taste issue; in the game, I plan on having NPCs give items to players as part of quest scripts, and I don't want scripts being blocked because the player is being cautious. Gener ally speaking, in my version of the game I don't plan on making NPCs give players evil objects.

If the module sees that the person who is giving an item isn't a player, the module returns 0, allowing the transfer to take place. If the giver is a player though, the module sends error messages to both people, saying that the receiver doesn't want to receive items, and then returns 1, signifying failure.

You can find the code for this in the /data/ logics/characters/itemstuff.py script file.

NOTE

Don't think that this is the only way to solve the problem. In fact, you may want to make a more complex "opt-in" version of this module, in which you can give the module the names of people who are "trusted" and should be allowed to freely give you items. So if you have friends who you trust, you can make a special command to add them to your "friends" list.

The Receive Command

Of course, you're going to need a command that adds and removes the cantreceiveitems logic from characters, so for this purpose, I've created the receive command. You'll be able to use it in the game to toggle whether you want to receive items or not. Here's the code:

 class receive( PythonCommand.Command ):     name = "receive"     usage = "\"receive <onoff>\""     description = "Turns your item receiving mode on or off"     def Run( self, args ):         if not args: raise PythonCommand.UsageError         if args != "on" and args != "off": raise PythonCommand.UsageError         me = BetterMUD.character( self.me )         if args == "on":             if me.HasLogic( "cantreceiveitems" ):                 me.DelLogic( "cantreceiveitems" )                 me.DoAction( "announce", 0, 0, 0, 0,                              "Receiving mode is now ON" )             else:                 me.DoAction( "error", 0, 0, 0, 0,                              "You are already in receiving mode!" )         else:             if not me.HasLogic( "cantreceiveitems" ):                 me.AddLogic( "cantreceiveitems" )                 me.DoAction( "announce", 0, 0, 0, 0,                              "Receiving mode is now OFF" )             else:                 me.DoAction( "error", 0, 0, 0, 0,                              "You are already in non-receiving mode!" ) 

The first two lines of the Run function check to ensure the arguments are valid; you must type on or off after the receive command; everything else isn't recognized, and throws a UsageError exception.

Once you get the mode, the user is retrieved from the database, and the command adds or removes your cantreceiveitems module, or gives you an error if you try setting the same mode that you're already in.

Analysis

Right away, you should be able to see the benefits of having a logical system in BetterMUD. In SimpleMUD, something like this would have been next -to-impossible to implement without first taking down the entire MUD, patching in the code, recompiling, and then hoping it works. Oh, but it gets better. In the SimpleMUD, if you had hardwired this into the game, every character would need a Boolean specifically asking, "Can this player receive items?"

Now, imagine upgrading that system to an "opt-in" system, as I mentioned before. To do this, your characters must be able to store lists of people who are your friends! That means you need to change the physical layout of your files again, and add lists of characters to each itemnot a good idea.

But in the BetterMUD, that ability already exists! If the cantreceiveitems module would store a list of your friends, all it needs to do is define its LoadScript and SaveScript functions so that whenever they are invoked, the module simply writes out the list of players in the opt-in list. On disk it would look something like this (this is all hypothetical):

 cantreceive [DATA] 10 40 32 86 [/DATA] 

The numbers in the data block would be the IDs of users from whom the player wants to accept stuff.

I think it's time to move on to something a bit more complex.

Encumbrance

At this point, the BetterMUD allows characters to carry an infinite number of items. Obviously this is not the greatest of ideas, since it would allow your characters to stash as much loot as they wanted, and run around doing whatever they want. An important part of MUD game dynamics is imposing limits on your players and making them work to overcome problems.

In this case, encumbrance is a problem. Players should be allowed to carry only a certain number of items, and this number can be increased gradually through the game, as part of a rewards system.

NOTE

I wouldn't recommend establishing the ability to limit the actual number of items carried. Consider this. Quantity objects allow you to carry a whole bunch of a similar kind of item. For example, 100 coins should naturally weigh about 100 times more than 1 coin. But a quantity item is considered just one item. So if you limit the number of items a person can hold to 20, he can hold up to 2 billion coins (a completely unrealistic number in terms of weight), and 19 other items as well. A weight system is far more realistic, however, since it takes into account that you can carry many more small items than large items.

New Attributes

To implement this reward system, you need to add two attributes to your characters encumbrance and max encumbrance. These values represent the current weight of all the items in your inventory, and the maximum weight that you can carry.

Adding the Attributes

Of course, if you've been running the game for any length of time, and you just decided to add encumbrance to the game, you may run into a snag. Your items don't have weights, and your characters don't have encumbrance values. It's easy enough to add weights to your item templates. Just go into the template files and add the attribute to their databanks . Here's an example of two updated item entries:

 [ID]                    2 [NAME]                  Pie [DESCRIPTION]           A BIG <#FFFF00>CUSTARD PIE<$reset> [ISQUANTITY]            0 [QUANTITY]              1 [DATABANK] weight                  500 [/DATABANK] [LOGICS] [/LOGICS] [ID]                    5 [NAME]                  Pile of <#> Diamonds [DESCRIPTION]           A pile of shimmering Diamonds [ISQUANTITY]            1 [QUANTITY]              1 [DATABANK] weight                  1 [/DATABANK] [LOGICS] [/LOGICS] 

So what do these weight values mean? It doesn't matter; it's completely arbitrary in the gamethe meanings of the values are up to you. You could use a simple system, in which each number is a pound , or a kilogram, or whatever. Just keep in mind that you can't use fractions, so the smallest positive weight you can use is 1. That means you can't have items that weigh less than 1. I usually try to assign 1 weight point to the weight of a coin, which is usually the lightest object in the game. As you can see from the sample file, the weight of a pie is 500, and the weight of a pile of diamonds is 1.

The weight value for quantity items is per-quantity , however. This means that a quantity item with 500 objects weighs as much as 500 times the weight attribute. Since the weight of a pile of 1 diamond is 1, the weight for 500 diamonds is 500.

It's easy enough to give character templates default encumbrance values as well:

 [ID]                    1 [NAME]                  Human [DESCRIPTION]           You are a plain old boring human. Deal with it. [DATABANK] encumbrance             0 maxencumbrance          2000 [/DATABANK] [COMMANDS] [/COMMANDS] [LOGICS] cantreceive [/LOGICS] 

This shows a human who can hold a maximum weight of 2000, and has a current weight of 0.

Well, that's great, because you can add all this to the templates, reload the templates, and then go into the game, but there's another problem. All existing instances of characters and items have no idea what weight and encumbrance are. They were created from templates that didn't have those variables .

To solve this problem, you could make the game go through every item, and load a weight value from its template, and you could do the same with all the characters.

Okay, great! But wait! There's another problem! Characters might have items on them, and if you set all of their encumbrance values to 0, you give all of the characters a bunch of free encumbrance points. When they drop the items they have, their current encumbrance will drop below 0, and that's a bad thing.

So you need to go through all the items that characters have, and add up how much they weigh. Does this sound complex enough yet? How the heck are you going to do all that? If you do it all in C++, your only option is to totally shut down the MUD, and that's just not a good idea.

Initializer Scripts

Instead of adding all this code to C++ to make your updates go smoothly, you can do it all from Python instead. This is the idea of an initializer script . It's a simple script that is meant to be run once, and it updates everything in the game that needs updating. More often than not, this involves going through all the item and character instances and giving them the new attributes.

Initializer Command

The first thing you need to do is make a command that executes initializer scripts. This is an easy enough task:

 class initialize( PythonCommand.Command ):     name = "initialize"     usage = "\"initialize <script>\""     description = "Performs an initialization using a script"     def Run( self, args ):         if not args: raise PythonCommand.UsageError         exec( "import data.logics.initializers." + args +               "\nreload( data.logics.initializers." + args +               " )\ndata.logics.initializers." + args + ".init()" ) 

This code loads a script from /data/logics/initializers, reloads it, and then calls the init function in that script file.

The reloading is included because if you run the initializer script once, Python usually doesn't get rid of it. Python keeps the script loaded, and if you import it again, nothing changes, even if you change the initializer script.

The exec function essentially allows you to execute the Python code that is in a string. So if you were to execute an addencumbrance initializer, the code that would be executed would look like this:

 import data.logics.initializers.addencumbrance reload( data.logics.initializers.addencumbrance ) data.logics.initializers.addencumbrance.init() 

And the game executes that script for you.

The Add Encumbrance Initializer Script

Now you need to make a script that goes through all the item instances and copies their weights from their templates, and also goes through all the character instances and gives them their encumbrance values, which are also taken from their templates. Figure 18.1 shows this process.

Figure 18.1. An initializer script goes through every instance, and copies an attribute from the templates into the instances.

graphic/18fig01.gif


 def init():     mud = BetterMUD.GameWrap()     # add weight to every item     mud.BeginItem()     while mud.IsValidItem():         item = BetterMUD.item( mud.CurrentItem() )         template = BetterMUD.itemtemplate( item.TemplateID() )         if not item.HasAttribute( "weight" ):             item.AddAttribute( "weight", template.GetAttribute( "weight" ) )         mud.NextItem() 

The previous code chunk goes through every item instance in the game, using the game wrapper accessor as an iterator. It adds one to every item that doesn't have a "weight" attribute.

The next step is to go through all the characters and give them their two encumbrance values:

 # add encumbrance to every character     mud.BeginCharacter()     while mud.IsValidCharacter():         character = BetterMUD.character( mud.CurrentCharacter() )         template = BetterMUD.charactertemplate( character.TemplateID() )         if not character.HasAttribute( "encumbrance" ):             character.AddAttribute( "encumbrance",                                 template.GetAttribute( "encumbrance" ) )         if not character.HasAttribute( "maxencumbrance" ):             character.AddAttribute( "maxencumbrance",                                 template.GetAttribute( "maxencumbrance" ) ) 

Now that the characters have encumbrance and maxencumbrance variables, you need to calculate the weight of the items currently carried by the character:

 # now calculate encumbrance of carried items         character.BeginItem()         encumbrance = 0         while character.IsValidItem():             item = BetterMUD.item( character.CurrentItem() )             if item.IsQuantity():                 encumbrance = encumbrance + item.GetAttribute( "weight" ) *                               item.GetQuantity()             else:                 encumbrance = encumbrance + item.GetAttribute( "weight" )             character.NextItem()         character.SetAttribute( "encumbrance", encumbrance ) 

At this point, the encumbrance attribute holds the value of the weights of all the character's items added together. The final step of the initializer gives the characters the "encum-brance" logic module if they don't have it:

 if not character.HasLogic( "encumbrance" ):             character.AddLogic( "encumbrance" )         mud.NextCharacter() 

And now your game is initialized to handle encumbrance.

You will probably have to make a script like this for any major update to the game.

Encumbrance Module

Finally, you can program the logic module itself. You must manage two distinct parts of the module: accepting or rejecting items based on weight and modifying encumbrance.

NOTE

In case you screw anything up (nobody is perfect!), it's probably a good idea to save the database before and after you run an initial ization script.

Calculating Weight

Since it's often necessary to calculate the weight of an item, I've included it as a special helper function inside the encumbrance logic module class:

 def Weight( self, i, q ):     item = BetterMUD.item( i )     if item.IsQuantity():         return q * item.GetAttribute( "weight" )     else:         return item.GetAttribute( "weight" ) 

The parameters are the class itself, the item ID, and the quantity. If the parameter is a quantity item, the weight of the item is multiplied by the quantity and returned, or else the normal weight is returned.

Rejecting Items

Two actions allow the encumbrance module to reject getting items: cangetitem and canreceiveitem . In both cases, the functions must check if the weight of the item will cause a player to carry too much weight, and if so, the player isn't allowed to get it. Here's the code:

 if action == "cangetitem":     me = BetterMUD.character( self.me )     item = BetterMUD.item( arg2 )     weight = self.Weight( arg2, arg3 )     if weight + me.GetAttribute( "encumbrance" ) >         me.GetAttribute( "maxencumbrance" ):         me.DoAction( "error", 0, 0, 0, 0, "You can't pick up " + item.Name() +                      " because it's too heavy for you to carry!" )         return 1     return 0 if action == "canreceiveitem":     g = BetterMUD.character( arg1 )     me = BetterMUD.character( self.me )     item = BetterMUD.item( arg3 )     weight = self.Weight( arg3, arg4 )     if weight + me.GetAttribute( "encumbrance" ) >         me.GetAttribute( "maxencumbrance" ):         me.DoAction( "error", 0, 0, 0, 0, g.Name() + " tried to give you " +                      item.Name() + " but it's too heavy for you to carry!" )         g.DoAction( "error", 0, 0, 0, 0, "You can't give " + me.Name() +                     " the " + item.Name() + " because it is too heavy!" )         return 1     return 0 

The code prints error messages appropriate for a character getting an item or for giving an item. In the case of giving, an error is printed to both people involved, so that the giver knows he can't give the item, and the receiver knows that he can't accept the item. Both of these instances return 1, telling the physics layer that the action couldn't be done.

Weight Management

The encumbrance module also needs to manage your weight when you get or drop an item. There are three events that can be triggered when you get a new item, and three events when you lose an item.

Here are two events for getting items:

 if action == "getitem":     if arg1 == self.me:         me = BetterMUD.character( self.me )         item = BetterMUD.item( arg2 )         weight = self.Weight( arg2, arg3 )         me.SetAttribute( "encumbrance", me.GetAttribute( "encumbrance" ) +                          weight )     return 0 if action == "spawnitem":     me = BetterMUD.character( self.me )     item = BetterMUD.item( arg1 )     weight = self.Weight( arg1, item.GetQuantity() )     me.SetAttribute( "encumbrance", me.GetAttribute( "encumbrance" ) +                      weight )     return 0 

These events are called when a player gets a new item, or a new item is spawned into a player's inventory. Both instances get accessors to the character and the item, calculate the weight, and then add that to the character's encumbrance attribute.

Here are two of the events that occur when you lose an item:

NOTE

You should note that there may be times in the game when an item is forced into a player's inventory without the player first being asked if he can have it. When this happens, you should still perform the normal duties on the item. Being loaded with an item without being asked could force a player's encumbrance over his max encumbrance, but that's not a huge deal. If you're concerned about this issue, you can add a function to the encumbrance module that keeps a player from moving anywhere if he is over his limit. You would do this by returning 1 whenever you get a canleaveroom query.

 if action == "dropitem":     if arg1 == self.me:         me = BetterMUD.character( self.me )         item = BetterMUD.item( arg2 )         weight = self.Weight( arg2, arg3 )         me.SetAttribute( "encumbrance", me.GetAttribute( "encumbrance" )                           weight )     return 0 if action == "destroyitem":     me = BetterMUD.character( self.me )     item = BetterMUD.item( arg1 )     weight = self.Weight( arg1, item.GetQuantity() )     me.SetAttribute( "encumbrance", me.GetAttribute( "encumbrance" )                       weight )     return 0 

These two actions simply remove the weight from a player's encumbrance whenever he loses the item. You should note that the additem and destroyitem actions both check to ensure that the actor ( arg1 ) has the same ID as the character executing the logic script. This is because all characters in a room are told when someone gets or drops an item, so the encumbrance module of everyone in a room is also informed. It would be a major flaw to add or remove weight for an item a player doesn't actually have.

The third event is a "mixed" event and can happen if a player either gives away or is given an item:

 if action == "giveitem":     if arg1 == self.me:         me = BetterMUD.character( self.me )         item = BetterMUD.item( arg3 )         weight = self.Weight( arg3, arg4 )         me.SetAttribute( "encumbrance", me.GetAttribute( "encumbrance" )                           weight )     if arg2 == self.me:         me = BetterMUD.character( self.me )         item = BetterMUD.item( arg3 )         weight = self.Weight( arg3, arg4 )         me.SetAttribute( "encumbrance", me.GetAttribute( "encumbrance" ) +                          weight )     return 0 

Whenever this event occurs, arg1 is the ID of the player giving away the item, so if it matches the ID of the character that owns the current logic module, you must remove the weight. On the other hand, arg2 is the recipient of the item, so if the player is the recipient, weight must be added to him.

Analysis

I hope you thought that example was pretty cool. The physical game engine has absolutely no clue about how much items weigh, or how many items a player can carry at any time, but by adding a simple logic module and a few attributes, you've now just added a completely new aspect to the game. Now you can limit what players carry, and force them to perform quests to gain the ability to carry more items. It's little things like this that add immensely to your replay value.

To Arms!

When I first told you about the stuff that will be in BetterMUD's physical layer, you may have been surprised to see that there is no concept of "armed" weapons. How the heck will combat take place if you can't arm weapons?

Well, the physics of BetterMUD isn't involved with "armed" weapons; that's for the logic layer to implement. So you add a new attribute to characters in order to implement "armed" weaponsthe weapon attribute. Simply put, the weapon attribute is the ID of the item in your inventory that you are currently using as a weapon.

To make things more interesting, you have another attributethe defaultweapon attribute. In the SimpleMUD, if a weapon isn't armed, the game assumes that a player is using his fists to attack, and the game calculates a damage range from 13. That range is hard-coded into the game, and that very fact should be making you squirm in your seat right now. The appropriate reaction should be "Eeeew, hard-coding! Gross!!"

So the default weapon is the template ID of an item that can be used as a weapon; but these are special items that are never actually created in the game, such as "fists" if the player is human, or maybe "scorpion tail" if the player is some kind of giant mutant scorpion .

Additionally, items are given a new attribute, the arms attribute. This is a simple numeric that determines if an item can be equipped. For now, the value of 0 means no, and the value of 1 means it is a weapon. All other values are reserved for future use, such as adding armor types.

Armaments Module

"Consult the Book of Armaments! And Saint Attila raised the hand grenade up on high, saying, "O Lord, bless this thy hand grenade, that with it thou mayst blow thine enemies to tiny bits, in thy mercy." And the Lord did grin. And the people did feast upon the lambs and sloths, and carp, and anchovies, and orangutans and breakfast cereals, and fruit-bats and-"

All right, enough quoting from Monty Python jokes. The armaments module basically allows characters to arm and disarm weapons. Since the game has no physical sense of "arming" an item, I have to use the query and do actions to ask items if they can be armed, and tell players to arm them.

Here's the armaments module in its entirety:

 class armaments( data.logics.logic.logic ):     # helper function to arm an item; only handles weapons for now:     def Disarm( self, itemtype ):         if itemtype == 1:             me = BetterMUD.character( self.me )             if me.GetAttribute( "weapon" ) != 0:                 weapon = BetterMUD.item( me.GetAttribute( "weapon" ) )                 me.SetAttribute( "weapon", 0 )                 self.mud.AddActionAbsolute( 0, "vision", me.Room(), 0, 0, 0,                                             me.Name() + " disarms " +                                             weapon.Name() + "." )     # helper function to disarm an item; only handles weapons for now:     def Arm( self, item ):            me = BetterMUD.character( self.me )         if item.GetAttribute( "arms" ) == 1:             me.SetAttribute( "weapon", item.ID() )             self.mud.AddActionAbsolute( 0, "vision", me.Room(), 0, 0, 0,                                         me.Name() + " arms " + item.Name() +                                         "!" )     # handle events:     def Run( self, action, arg1, arg2, arg3, arg4, data ):         # can only arm weapons (arms = 1) right now:         if action == "query" and data == "canarm":             item = BetterMUD.item( arg1 )             if item.GetAttribute( "arms" ) == 1:                 return 1             return 0         # try arming the item:         if action == "do" and data == "arm":             item = BetterMUD.item( arg3 )             self.Disarm( 1 )             self.Arm( item )         # try disarming the item:         if action == "do" and data == "disarm":             self.Disarm( arg3 ) 

Look at the query function first. If it determines that you can arm an item (if it's a weapon, arms will be 1), it returns 1. Remember that in Chapter 14 I told you about granting rights to entities and that all entities automatically have the right to perform any of the standard physical events, such as moving around. You had to specifically disable an act in order to prevent it.

With custom actions, like canarm , items cannot automatically be armed. The game assumes that a reply of 0 means no. By default, every logic module should return 0, and thus say no whenever a player is asked if he can arm an item. But if the item is a weapon ( arms is 1), you know that the item can be used as a weapon, and the function returns 1 for yes.

The other actions, do disarm and do arm simply call the helper functions to disarm and arm weapons.

Table 18.1 defines the event parameters for these actions.

Commands

Like all things in the BetterMUD, you need a way to actually invoke the logic module. This is done via two commands, one that arms an item, and one that disarms an item.

Table 18.1. Event Parameters for disarm and arm

Parameter

arm

disarm

action

do

do

arg1

[*] entity type

[*] entity type

arg2

[*] entity id

[*] entity id

arg3

item being armed

type of item being disarmed

arg4

not used

not used

data

arm

disarm


[*] These are optional parameters that are used to route the event to an entity when using timers. See Chapter 14 for more information.

[*] These are optional parameters that are used to route the event to an entity when using timers. See Chapter 14 for more information.

[*] These are optional parameters that are used to route the event to an entity when using timers. See Chapter 14 for more information.

[*] These are optional parameters that are used to route the event to an entity when using timers. See Chapter 14 for more information.

Here's the arm command:

 class arm( PythonCommand.Command ):     name = "arm"     usage = "\"arm <item>\""     description = "Attempts to arm an item"     def Run( self, args ):         if not args: raise PythonCommand.UsageError         me = BetterMUD.character( self.me )         item = BetterMUD.item( FindTarget( me.SeekItem, me.IsValidItem,                                me.CurrentItem, args ) )         if not me.DoAction( "query", item.ID(), 0, 0, 0, "canarm" ):             me.DoAction( "error", 0, 0, 0, 0, "Cannot arm item: " +                          item.Name() + "!" )             return         me.DoAction( "do", 0, 0, item.ID(), 0, "arm" ) 

Note that the ID of the item being armed is the third argument while passing the do and query events around. The first two arguments are reserved for routing delayed events in the timer system to a specific entity (see Chapter 14 if you don't remember this). Table 18.2 lists the event parameters for the canarm query.

Table 18.2. Event Parameters for canarm

Parameter

Can Arm

action

query

arg1

item you want to arm

arg2

not used

arg3

not used

arg4

not used

data

canarm


Disarming an item is similar to disarming in theSimpleMUD. A player must type disarm weapon to disarm his current weapon:

 class disarm( PythonCommand.Command ):     name = "disarm"     usage = "\"disarm <item>\""     description = "Attempts to disarm an item"     def Run( self, args ):         if not args: raise PythonCommand.UsageError         me = BetterMUD.character( self.me )         if args == "weapon":             me.DoAction( "do", 0, 0, 1, 0, "disarm" ) 

This time, the third argument to the do event is the type of armament to be removed. The value 1 signifies a player wants to disarm his weapon.

Initialization Script

There is an initialization script for adding armaments to the game, which is very similar to the encumbrance initialization script. Since the scripts are so similar, I won't show you the code, but you can find it in the /data/logics/initializers/addarmaments.py file.

Analysis

So now a player can add armed weapons to a character, and the physical module isn't involved. Isn't that neat?

For a more complex MUD, you might consider adding more than one weapon slot (you could make it so that a player can hold two small, light weapons), or adding armor slots, or other cool enhancements.

In this example you saw how you could make your own custom event messages that the core BetterMUD physical engine doesn't support. The canarm and arm events are completely new, and can be used to query characters and items about whether they can be armed.

In a similar vein, you could make canread or caneat events, to make the realm even more realistic.

I Put a Spell on You

The next endeavor I want to cover is learning how to "cast a spell". Spell casting requires several elements, as do the other logics.

Basically, I want to create a spell named uberweight , that can be cast on items, and to make them stick to the ground for 20 seconds, so that no one can pick them up.

This spell can only be cast once every 2 minutes, which players learn from reading a magic scroll. Sound complex enough yet?

Designing the Item

The first task is to design the item in question. The actual item template is simple:

 [ID]                    54 [NAME]                  Scroll of Uberweight [DESCRIPTION]           This scroll contains the spell of Uberweight on it. [ISQUANTITY]            0 [QUANTITY]              1 [DATABANK] weight                  50 arms                    0 [/DATABANK]  [LOGICS]   canread uberweightscroll   [/LOGICS]  

You can ignore everything but the two logic modules. Each of the two logic modules implements an action: the first says that the item can be read, and the second says that it has the uberweightscroll item logic module attached to it.

Can You Read?

The canread item logic module is really simple; it merely returns 1 whenever someone asks it if it can be read:

 class canread( data.logics.logic.logic ):     def Run( self, action, arg1, arg2, arg3, arg4, data ):         if action == "query" and data == "canread":             return 1 

Ta-da! Any item that has this module now returns 1 when asked if it can be read. This class can be found in /data/logics/items/ basicitems.py. Table 18.3 lists the event parameters for the canread action event.

Table 18.3. Event Parameters for canread

Parameter

Can Read

action

query

arg1

not used

arg2

not used

arg3

not used

arg4

not used

data

canread


NOTE

Here's an idea for future expansion. Perhaps you want to give your characters a sense of " literacy ," so that only some characters can read. If you have a big dumb ogre charac ter, for example, he is disadvantaged in the game, because he can't read magic scrolls to learn to cast spells. Maybe you could even expand on this idea, and include the idea of languages for readable objects. Some things could be written in an ancient Elven language, so dwarfs and humans can't read them.

Scroll Module

In this particular version of the BetterMUD, I've decided to make it possible for characters to read magical scrolls, and then memorize the spell learned from the scroll. Instead of copying the code for every scroll type, I've made a base spellscroll class that does 95% of the work.

It must return an error if a player is trying to learn a spell that he already knows, or it must give the player the command that controls the spell if he doesn't have it. Here's the code:

 class spellscroll( data.logics.logic.logic ):     def DoRead( self, character, item, name ):         # look up character and item         c = BetterMUD.character( character )         i = BetterMUD.item( item )         # check if character has spell command already         if c.HasCommand( name ):             c.DoAction( "error", 0, 0, 0, 0, "You already know this spell!" )             return         # give character new spell command, and tell everyone:         c.AddCommand( name )         self.mud.AddActionAbsolute( 0, "vision", c.Room(), 0, 0, 0,                                     c.Name() + " reads " + i.Name() + "!" )         self.mud.AddActionAbsolute( 1, "destroyitem", i.ID(), 0, 0, 0, "" )         c.DoAction( "announce", 0, 0, 0, 0, "You now know the spell " +                     name + "!" )         c.DoAction( "announce", 0, 0, 0, 0, "The " + i.Name() +                     " disappears in a bright flash of flame!" ) 

This code assumes that the command object has the same name as the spell; so if you have a spell named uberweight , the command object is named uberweight as well.

Note that the scroll is destroyed after it is read, and this is just a little quirk I enjoyed inserting into the game. You don't want a single scroll giving everyone in the game a spell (if the owner is generous enough to lend it to other people), so it's simply destroyed .

This class can be found in /data/logics/ items/spellitems.py.

NOTE

Please don't think that this is how you have to implement spells. The beauty of this system is that you can imple ment spells in any way you want. Spells don't have to be commands that you learn by reading scrolls. Instead you could make a "spell book" item that can store all of the spells you know. Maybe you can even have your characters gain specific spells whenever they get to a certain level. The possibilities are limitless.

Uberweightscroll Logic

Now that you have a scroll class, all you need to do is create an item logic that will be attached to the scroll item, and give characters the spell when the item is read:

 class uberweightscroll( spellscroll ):     def Run( self, action, arg1, arg2, arg3, arg4, data ):         if action == "do" and data == "read":             self.DoRead( arg3, self.me, "uberweight" )             return 

The uberweightscroll logic module is meant to be attached to scrolls, and whenever someone sends a do read event to it, the base spellscroll class is called, adding the spell command uberweight to the character.

Table 18.4 lists the parameters for the read event.

Table 18.4. Event Parameters for read

Parameter

Read

action

do

arg1

[*] entity type

arg2

[*] entity id

arg3

character performing the reading

arg4

not used

data

read


[*] These are optional parameters that are used to route the event to an entity when using timers. See Chapter 14 for more information.

[*] These are optional parameters that are used to route the event to an entity when using timers. See Chapter 14 for more information.

Uberweight Command

The next step is to create the uberweight command. This is the command that actually casts the spell.

This command must do several things. First, it must store data about the last time it was invoked, so that you can limit how often the spell is cast.

NOTE

If you're inclined, you could limit the "casting time" to a small number such as 5 seconds, and then give the character a "mana" attribute, and have the spell subtract from that variable whenever it is cast. When the mana goes down to 0, the character isn't allowed to cast it anymore, until he gets more energy. This method is used in many MUDs because it allows characters to cast a single spell rapidly , but it sets reasonable limits. I've used a simple system here, to show you that there are many ways to do anything.

Since this spell needs some extra data, I'll need to give it a ScriptInit function, so that it initializes the data when the command object is first created:

 class uberweight( PythonCommand.Command ):     name = "uberweight"     usage = "\"uberweight <item>\""     description = "puts a magical weight on an item"     def ScriptInit( self ):         # init the next execution time to 0, so you can execute it right away         self.executiontime = 0 

This simply creates an executiontime variable, and sets it to 0. Whenever executiontime is more than the game's current time, you'll be blocked from using the spell. Since the game always starts at 0, you'll be able to cast the spell right away after it's given to you.

Now the actual execution:

 def Run( self, args ):     if not args: raise PythonCommand.UsageError     # grab the character and room     me = BetterMUD.character( self.me )     r = BetterMUD.room( me.Room() )     # check to make sure you can execute it     time = self.mud.GetTime()     if time < self.executiontime:         me.DoAction( "error", 0, 0, 0, 0, "You need to wait " +                      str( (self.executiontime - time) / 1000 ) +                      " more seconds to use this again!" )         return     # find the name of the item to cast on:     id = FindTarget( r.SeekItem, r.IsValidItem, r.CurrentItem, args )     item = BetterMUD.item( id )     name = item.Name()     # add 120 seconds; 2 minutes     self.executiontime = time + 120000     # tell everyone about it, and add a termination message:     self.mud.AddActionAbsolute( 0, "addlogic", 1, id, 0, 0, "uberweight" )     self.mud.AddActionAbsolute( 0, "vision", r.ID(), 0, 0, 0,                                 "<#FF0000>" + me.Name() +                                 " just cast UBERWEIGHT on " + name + "!" )     self.mud.AddActionRelative( 20000, "messagelogic", 1, id, 0, 0,                                 "uberweight remove" ) 

The function ensures that the character can execute the spell first; if not, he's told how long to wait. If he can execute the spell, the function tries to find the item he wants to cast the spell on. If the item is found, the execution time is reset to two minutes from that time (120 seconds, or 120,000 milliseconds ), and three actions are added. The first adds the uberweight logic module to the item, the second tells everyone in the player's room that he cast uberweight on an item, and the last one tells the game to remove uberweight in 20 seconds. All of the events use the same parameter conventions I showed you in Chapter 14.

There is a difference between the uberweight command object and the uberweight item logic module.

Uberweight Item Logic Module

The last component of the system is the logic module that is attached to items when they have uberweight cast upon them. This module refuses to let items be picked up, much like the cantget script I showed you before.

NOTE

It's very difficult for me to intention ally limit the code for the BetterMUD, but if I didn't this book would weigh 50 pounds . You should always be thinking about how you can expand the game. For example, I'm thinking about expanding this spell system later, to give levels of spells. With that system, every time a player reads a new scroll, he advances a level, and the spell becomes more powerful. Or maybe you'll have scrolls with levels built in, so that you need a level 2 uberweight scroll to get to level 2, and so on. The point of all of this is to give you a start, so you can see how a separated logic/physics engine allows essentially limitless expansion.

You should only use special uberweight logic for items, rather than re-using cantget , to send a customized message to players when they try to get the item, and to tell a room when the spell wears off.

 class uberweight( data.logics.logic.logic ):     def Run( self, action, arg1, arg2, arg3, arg4, data ):         if action == "cangetitem":             c = BetterMUD.character( arg1 )             me = BetterMUD.item( arg2 )             self.mud.AddActionAbsolute( 0, "vision", c.Room(), 0, 0, 0,                  c.Name() + " struggles like a madman trying to pull " +                  me.Name() + " off the ground, but it's stuck!" )             return 1         if action == "messagelogic":             if data == "uberweight remove":                 self.mud.AddActionAbsolute( 0, "dellogic", 1, self.me, 0, 0,                                             "uberweight" )                 me = BetterMUD.item( self.me )                 self.mud.AddActionAbsolute( 0, "vision", me.Room(), 0, 0, 0,                                             "The uberweight on " + me.Name() +                                             " wears off!" ) 

This object responds to two events: cangetitem and messagelogic . Like the cantget module, the object prints a message to the room, telling everyone that the character can't get the item, and returns 1, telling the physics engine that the action was blocked.

When you first gave the uberweight logic to an item, the uberweight command inserted a message into the timer queue:

 self.mud.AddActionRelative( 20000, "messagelogic", 1, id, 0, 0,                                 "uberweight remove" ) 

Remember, that code tells the item's uberweight module to remove itself in 20 seconds. When the item's module gets this message, it deletes the actual logic module, and then sends a vision event to the room saying that the uberweight has ended.

That's it!

Example of Item Logic

Here's an example of the game text that appears when using the uberweight item logic module:

 Avenue You are on the main Avenue of Betterton. You can see the street stretching off to the distance in an east-west direction. Exits: East - Avenue, South - Magicians Door People: Mithrandir Items: Mountain Dew /read scroll You now know the spell uberweight! The Scroll of Uberweight disappears in a bright flash of flame! Mithrandir reads Scroll of Uberweight! /uberweight mountain dew Mithrandir just cast UBERWEIGHT on Mountain Dew! /get mountain dew Mithrandir struggles like a madman trying to pull Mountain Dew off the ground, but it's stuck! The uberweight on Mountain Dew wears off! /uber moun You need to wait 94 more seconds to use this again! / You need to wait 79 more seconds to use this again! 

And you've just created your very first spell! Cast away!

Show Me the Money!

The concept of currency is not built into the BetterMUD. If you think about real life, coins and dollars are just physical objects that represent an abstract value. In fact, if you really want to, you can turn the BetterMUD into a barter system , in which there is no money at all; you trade items with other people based on how much you think they're worth. If you ever traded in baseball cards as a kid, you know exactly how such a system works.

Simple Systems

It's entirely possible in the BetterMUD to implement bartering, so that a character can, for example, go up to a merchant and trade him two knives for a sword. All you need to do is create a logic module to handle bartering. Heck, if you're really ambitious, you could even make merchant scripts that assign different values to their items based on how much they think the items are worth. An arms merchant might value weapons highly, but a magic merchant wouldn't, so trading him weapons would give you less favorable trades.

Of course, there haven't been barter systems in wide use for hundreds of years in the industrialized world, because barter systems are inconvenient. In such a system, not everyone can trade, because trading requires both parties having something that the other wants. For a barter system to work, the merchant must own a sword that a player wants, and the player must have something that the merchant wants.

Currency Systems

Currency systems work more efficiently than bartering in the world most of us know. Instead of trading in objects, you trade objects for abstract pieces of currency that are worth a particular value. If you want a sword, you pay the merchant for it, and then the merchant can take that money and find someone else who can sell him what he needs, instead of requiring you to give him things.

The entire idea of currency systems is what prompted me to put "quantity items" into the physical engine of the BetterMUD. I've rarely seen a MUD that works without some kind of currency system, so that's just what I'm going to show you how to implement.

You don't need to script any player-to-player transactions involving money. Players basically have to trust each other in transactionsone player gives another money, and that players gives something in return.

Of course, money really isn't worth anything until you can use it to buy things from NPC's in the game, and that's the system you'll be using. The first thing you need to think about is what kind of currency system you want to use. Since quantity objects now have weight, there is an upper limit on the size of piles of cash you can lug around; you can't just pour a few million coins into your pocket (talk about deep pockets!) and skip around as you did in SimpleMUD.

The weight of currency can be inconvenient to wealthy players. How the heck are they going to carry around millions of dollars? How are they even going to accumulate wealth in the first place?

NOTE

Game currencies are not required to be based on a "solid" value. If you've studied economics at all, you know that the value of money changes wherever you go. For example, in Buffalo, NY, you can buy a decent lunch for about $7, but if you go to New York City, it's hard to find lunch for less than $13 or so. Money is worth different amounts in different places. I can't say how useful such a system would be to you, but if you feel like experimenting, feel free. Borrowing an idea from Raymond E. Feists' Riftwar books, you could have a region in the game where there is no metal, but tons of diamonds and gems, and therefore metal coins are worth a heck of a lot more than diamonds. I would like to note that managing such a system is an extremely difficult task, because there are ways for entrepreneurial players to endlessly convert money between the two systems and gain value with every conversion. I'm just pointing out that if you ever want to make such a system, you can.

Multiple Currencies

An elegant solution to this problem is to have different denominations of money. When you think about coinage systems, you realize that countries press different valued coins. In USA we have pennies (1 cent), nickels (5 cents), dimes (10 cents), quarters (25 cents ), and a bunch of less common coins as well. You could create similar equivalents in your game with one kind worth 10 of another kind, and so on.

NOTE

When making a currency system with coins using different values, I would always recommend valuing coins as multiples of 10. So the smallest coin would be worth 1, the next up would be worth 10, the next 100, and so on. This makes calculating prices in the game much easier for your players.

Simple Currency Example

For the purposes of this book, I'm not going to go over a complete money system; there are only so many things you can do. But I will show you the general gist of one system, and then you can make changes and additions wherever appropriate.

The key to the basic BetterMUD currency exchange system is the merchant class, which is a character logic module that lists items to sell, and lets you buy them from him.

Currency

The first thing I need is a currency type. That's easy enough to create:

 [ID]                    1 [NAME]                  <#> Copper Coins [DESCRIPTION]           These copper coins are small and dirty, they don't have much value. [ISQUANTITY]            1 [QUANTITY]              1 [DATABANK] weight                  1 arms                    0 [/DATABANK] [LOGICS] [/LOGICS] 

It's just a standard quantity-type object, with a weight of 1 unit per coin.

Helpers

The next thing you'll need are helper functions, which are functions that perform various money- related tasks , such as checking if a character has enough money to buy something, and removing that money when he does buy something.

The helpers and the merchant classes I cover in the section after this are located in /data/logics/characters/currency.py

NOTE

The great thing about separating transactions into functions like this is that the separation significantly helps later on if you make a multiple currency system. These functions should automatically manage all of that for you, if you make the appro priate changes.

Here's the first helper function:

 def HasEnoughCurrency( character, amount ):     total = 0     character.BeginItem()     while character.IsValidItem():         item = BetterMUD.item( character.CurrentItem() )         if item.TemplateID() == 1:   # copper pieces             total = total + item.GetQuantity()         character.NextItem()     if total >= amount:         return 1     return 0 

This simply looks through all the items on a player, searching for copper coins. Whenever

they are found, their quantity is added to the total. Finally, when the total is added up, if it's

enough to pay for the requested amount, 1 is returned, or 0 if not. The other function removes money from your character and gives it to another character:

 def GiveCurrency( character, recipient, amount ):     character.BeginItem()     mud = BetterMUD.GameWrap()     while character.IsValidItem():         item = BetterMUD.item( character.CurrentItem() )         if item.TemplateID() == 1:   # copper pieces             mud.DoAction( "attemptgiveitem", character.ID(), recipient.ID(),                           item.ID(), amount, "" )             return         character.NextItem() 

NOTE

Take care to never modify physical attributes such as quantity directly through these functions. Doing so messes up any encumbrance system you have running, since the system is not told that you are losing money. Instead, rely on physical actions. In this example, I give the money from one character to the merchant; keep in mind that you don't have to do that if you don't want to. Instead you could simply tell the character that you're destroying an item.

This finds a bunch of coins, and removes the amount from their quantity. This function assumes you've already checked to see if the character has enough money in the first place.

Merchant Logic

Since merchant characters are frequently found in MUD games , I've decided to create a base Python logic module that acts like a merchant. This is only a simple merchant, however; he'll only list things and sell things.

I would like to note that due to the logic system of the BetterMUD, characters are the merchants , and they don't have to be linked to a specific room, as they were in the SimpleMUD. Of course, this also means that if someone kills a merchant, the merchant can't sell stuff anymore (seems obvious, doesn't it?).

The merchant responds to two different custom events; do list and do buy . Table 18.5 lists the parameters for these events.

Here's the list handler (which is, as usual, executed from within Run , and therefore has the standard event parameters):

 if action == "do" and data == "list":     character = BetterMUD.character( arg3 )     character.DoAction( "announce", 0, 0, 0, 0,                         "<#7F7F7F>--------------------------------------" )     character.DoAction( "announce", 0, 0, 0, 0,                         "<#FFFFFF> Item                       Cost" )     character.DoAction( "announce", 0, 0, 0, 0,                         "<#7F7F7F>--------------------------------------" )     for x in self.iteminventory:         item = BetterMUD.itemtemplate( x )         character.DoAction( "announce", 0, 0, 0, 0, "<#7F7F7F> " +                             item.Name().ljust( 42 ) + " " +                             str( item.GetAttribute( "value" ) ) )     character.DoAction( "announce", 0, 0, 0, 0,                         "<#7F7F7F>--------------------------------------" )     return 

Table 18.5. Event Parameters for list and buy

Parameter

list

buy

action

do

do

arg1

[*] entity type

[*] entity type

arg2

[*] entity id

[*] entity id

arg3

character who wants list

character buying

arg4

not used

not used

data

list

buy <item name>


[*] These are optional parameters used to route the event to an entity when using timers. See Chapter 14 for more information.

[*] These are optional parameters used to route the event to an entity when using timers. See Chapter 14 for more information.

[*] These are optional parameters used to route the event to an entity when using timers. See Chapter 14 for more information.

[*] These are optional parameters used to route the event to an entity when using timers. See Chapter 14 for more information.

Due to width restraints on the page, I've cut the length of the separator bars down a bit; they're actually supposed to be a full 80 characters across.

Basically this prints out a header, and then it goes through every item in self.iteminventory using a Python for loop.

Merchants must have a list of entity IDs named self.iteminventory ; this list represents every item the merchant can sell.

The other action that merchants react to is the buy event:

 if action == "do" and data[:3] == "buy":     itemname = data.split( None, 1 )     itemname = itemname[1]     character = BetterMUD.character( arg3 )     id = FindName( BetterMUD.itemtemplate, self.iteminventory, itemname )     if id == 0:         character.DoAction( "announce", 0, 0, 0, 0, "Sorry, you can't buy " +                             itemname + "here!" )         return     t = BetterMUD.itemtemplate( id )     if not HasEnoughCurrency( character, t.GetAttribute( "value" ) ):         character.DoAction( "announce", 0, 0, 0, 0,                             "Sorry, you don't have enough money to buy " +                             t.Name() + "!" )         return     GiveCurrency( character, me, t.GetAttribute( "value" ) )     self.mud.DoAction( "spawnitem", id, character.ID(), 1, 0, "" )     self.mud.AddActionAbsolute( 0, "vision", character.Room(), 0, 0, 0,                                 character.Name() + " buys " +                                 t.Name() + "." ) 

The format of the data parameter for this event is a bit tricky. Whenever a player wants to buy something, he passes in buy followed by the item name in the data string. So if he wants a sword, data would be buy sword . To get the name of the item, I use the string's split function to chop off buy , and then grab the rest of the string and stash it into itemname .

On the fifth line, I use a function named FindName , which is a Python helper function (I created this for you) that performs the same function as the C++ BetterMUD::match function. The function goes through a list of IDs and tries to make a full or partial match on one of them, and return the ID.

Basically this process searches the inventory for an item that matches the item the user is trying to buy.

NOTE

The IDs in the merchant's inventory represent item tem plates, rather than item instances. This means that every time a character buys something, a brand new item is spawned and given to the character. You should be aware that this is only one of many ways to do this. In a more highly managed world, merchants might sell only items that they are carrying in their inventory, and when they are sold out, no one can buy anything more. This is a great way of limiting the number of items in a game, and increasing their perceived worth. When items are rare, they generally cost more as well, since demand is larger than supply.

NOTE

At this point I want to warn you that this is a dangerous system. The function assumes that the transfer of money was completed successfully, even though it may not have been. This is because items can block being transferred, and piles of money are treated as regular items. I would like to point out that it's just not a good idea to either put scripts of any kind on quantity objects, or make it so that you can't move quantity objects around. Not allowing scripts to be placed on quantity objects usually works out best for everything in the end. You should probably update the uberweight script so that it can't be cast on quantity objects.

If the merchant doesn't sell the item, an error is returned, and the function quits out. The next step is to make sure the character can pay for the item, so the function calls the HasEnoughCurrency function to figure this out, using the item's "value" attribute as the amount of money he needs to pay.

The final actions include transferring the money from the character to the merchant, spawning the new item into the player's inventory, and then telling everyone that the character made a purchase.

Merchant Character

The last thing I need to do is create an actual merchant character, and his inventory script. Here's the merchant:

 [ID]                    300 [NAME]                  Magician Keeper [DESCRIPTION]           A tall fellow dressed in ornate robes. [DATABANK] [/DATABANK] [COMMANDS] [/COMMANDS] [LOGICS] bettertonmagicianshop [/LOGICS] 

This can be found in /data/templates/characters/storekeepers.data. The magician merchant has one logic module, betterronmagicshop . Here's the logic module (which can be found in /data/logics/characters/bettertonstores.py):

 class bettertonmagicianshop( data.logics.characters.currency.merchant ):     def ScriptInit( self ):         self.iteminventory = [ 54, 55 ] 

This simply inherits from the merchant and initializes the merchant script with two items, 54 and 55 (which are a scroll of uberweight and a healing potion, but that's not important).

Now you're almost ready to test it out. There are just two more things you need to take care of: the commands to list and buy things.

Listing and Buying

In order for your characters to be able to list and buy items, they must have commands to tell the game to list and buy items.

I'm sure you won't be surprised that these classes are named list and buy . I've put the commands in the /data/commands/usercommands.py script file.

Here's the list command:

 class list( PythonCommand.Command ):     name = "list"     usage = "\"list <merchant>\""     description = "Gets a list of the merchant's wares"     def Run( self, args ):         if not args: raise PythonCommand.UsageError         me = BetterMUD.character( self.me )         r = BetterMUD.room( me.Room() )         m = BetterMUD.character( FindTarget( r.SeekCharacter,             r.IsValidCharacter, r.CurrentCharacter, args ) )         m.DoAction( "do", 0, 0, me.ID(), 0, "list" ) 

It finds a character matching the name of the merchant you passed in, and then sends that character a list action event. You should note that if a character tries listing someone who isn't a merchant, he'll kindly ignore the character (since he doesn't have a module that responds to list events).

If a player want to see the magicians list, go into his room and type /list magician . He'll spit out something that looks like this:

 /list magician ---------------------------------------------------------------------------  Item                                       Cost ---------------------------------------------------------------------------  Scroll of Uberweight                       100  Small Healing Potion                       10 --------------------------------------------------------------------------- 

Buying items is a little more complex, because a player needs to find a merchant to buy from, and tell him what kind of item he wants to buy as well.

Here's the command:

 class buy( PythonCommand.Command ):     name = "buy"     usage = "\"buy <merchant> <item>\""     description = "buys an item from a merchant"     def Run( self, args ):         if not args: raise PythonCommand.UsageError         parms = args.split( None, 1 )         if len( parms ) < 2: raise PythonCommand.UsageError         me = BetterMUD.character( self.me )         r = BetterMUD.room( me.Room() )         m = BetterMUD.character( FindTarget( r.SeekCharacter,             r.IsValidCharacter, r.CurrentCharacter, parms[0] ) )         m.DoAction( "do", 0, 0, me.ID(), 0, "buy " + parms[1] ) 

The first step is to split up the arguments so that the merchant's name is within parms[0] , and the item a player wants to purchase is in parms[1] .

Then the code finds the merchant and it sends it a buy event with the name of the item added on to the end of the data string.

A player can now use the buy command in the game:

 /buy magician scroll You give 100 Copper Coins to Magician Keeper. Mithrandir buys Scroll of Uberweight. 

And now you have a functioning currency system.

NOTE

I didn't implement item buy-backs, but it's a simple task to accomplish if you need it. Using the current merchant system, it would be best if you just destroyed items that were sold, but this can lead to problems if you modify the items in any signifi cant way. Imagine that a player gets a really cool magical effect perma nently added to his favorite sword, and then accidentally sells it to the merchant. Oops. Not only would the merchant give a player the normal price, but he'd destroy it as well. In this kind of situation, your best bet is to add a can't be sold flag to items that are magically altered .

Cry Havoc and Let Slip the Dogs of War

Can you believe that you've gone through almost eight full chapters about a MUD without having gone over combat yet? There's a reason for this, of course. The SimpleMUD was a really simple hack and slash MUD. You run around, slaughtering things, pick up loot, and then kill some more. This was entertaining a few decades ago, but it gets boring after a while. Some major commercial games still use this same old tired format *cough* Diablo *cough*, and there are lots of people who still love playing those kinds of games.

I want more from games than just hacking-and-slashing my way though thousands of orcs and goblins. In the last MUD I played , a full 80% of the people on the MUD actually had programs that would play the game for them . It started out simply with players creating simple programs to log in and run around killing monsters for an hour or so while they were away, so that they could gain an edge over anyone else who was playing.

Then people started making these programs more complex and had them executing overnight. Then people made programs to group together with other programs, and wander around, communicating with each other. After a few months, we had characters that were on top of the game, but no one actually ever played them! How ridiculous!

The problem was that it was a typical hack-and-slash MUD. Basically all you could do was run around, kill things, and encounter the occasional quirk here and there. Combat is an essential part of any game like this, but you need to think about what else the game should do. That's why I'm not going to spend a heck of a lot of time focusing on combat; it shouldn't be the only thing people do in your game.

Of course, without some form of combat, you basically have a game in which players run around, pick up items, and talk to people, which isn't all that interesting.

The combat system I'm implementing as a demonstration is simple. A player can use a weapon that has a specified damage range and accuracy, hit people who have hitpoints, and dodge. This is even simpler than the SimpleMUD's system, but that's not really a big problem because this system is going to be completely flexible and upgradeable.

Combat Data

The first thing I need to do is figure out what kind of data attributes my characters need to participate in combat.

I'll need some kind of an experience attribute to track the progress of characters as they go around killing things. I'll call this experience. I'll also need to know how much experience the character is worth when it dies, so that I can reward the player who killed him. This will be called giveexperience .

Obviously I need some hitpoints, so that's what I'm adding nextthe hitpoints attribute. I'll also need a maximum number of hitpoints, covered by maxhitpoints .

The final requirement is a weapon, but you've already seen how I accomplished this, when I showed you how to arm weapons. The weapon attributes are weapon and defaultweapon .

Combat Module

The combat module is easily the most complex logic module I've implemented so far. It's over 150 lines, and that's just for a simple combat module! There are so many things to keep track of.

NOTE

In the SimpleMUD, the combat system was unfair. Some one could spend a few minutes hacking away at an evil monster, only to have some other guy come into the room, finish him off, and get all the experience you earned with your strenuous slashing. This is why the BetterMUD combat module that I am showing you maintains a list of people who are attacking the character. Whenever the character dies, he gives an equal amount of experience to all attackers , as shown in Figure 18.2 . For the future, maybe you could even consider a system that keeps track of how much damage each person dealt to the poor dead character, and give out experience accordingly , so that the people who do the most damage get the most experience.

Figure 18.2. The BetterMUD combat module distributes experience equally to all attackers.

graphic/18fig02.gif


Module Data

The actual combat module tracks the data of its own, which is temporary data that you won't need to track. There are three attributes within the module, all initialized when the module is created:

 class combat( data.logics.logic.logic ):     def ScriptInit( self ):         self.attackedlist = []         self.target = 0         self.attacktime = 5000 

The attackedlist is simply a Python list of all characters who are currently attacking you (in ID form). This is needed so that you can dole out experience when you die a horrible death. It will happen, trust me. Agony coming up!

The target variable keeps track of the person you are currently attacking; this simple module only supports attacking a single person at a time.

The final attribute is the attacktime attribute, which keeps track of the time it takes to perform one attack round. I've got it hardwired at 5 seconds.

NOTE

If you feel ambitious, you can create a multi-attack module that would keep track of groups of people a player is attacking. This would be particularly useful for magic spells that cause hail or fireballs to rain down on everyone in the room.

Events

There are a number of events that are related to combat in the game:

  • query canattackasks if a player can be attacked .

  • query getaccuracydeltaasks for special accuracy information.

  • do attacka player is performing an attack round.

  • do initattacka player is beginning to attack another player.

  • do attackeda player was attacked by another player.

  • do brokeattacka player stopped attacking another player.

  • do breakattacka player stopped attacking a player's target.

  • leaverooma player left another player's room (there might be a break in combat.

  • do killeda player killed another player.

  • do deathtransporttells an entity to transport a player somewhere when a player dies.

In addition to these events, there are attributes that you should be concerned with when they are modified:

  • modifyattribute maxhitpointsa player's max hitpoints have changed.

  • modifyattribute hitpoints a player's hitpoints have changed.

  • modifyattribute experience a player's experience has changed.

The basic process goes like this. When a player starts attacking another player, he performs an initattack action. This sets up a timer that causes an attack event to occur immediately. At the same time, a player's target is given an attacked event, telling him that a player started attacking him.

When the attack event occurs, it automatically inserts another attack event to execute in another 5 seconds, and then performs the actual combat logic, which calls the modifyattribute hitpoints event of the target when a player successfully hits it.

NOTE

An even better method would be to store the attack time value as an attribute in the character, so that a player can get magic spells and other effects that can make you attack more slowly or faster.

When a player dies, this event is detected by the modifyattributes hitpoints event, which performs the death logic. When a player dies, the game sends a killed event to all players who were attacking him, so that they know the player died.

This system is an automatic combat system. Once a player initializes an attack, he continues attacking until his opponent dies. This is very easy to do using timer event actions. Because of this, however, a player needs a way to stop attacking another player once his opponent dies. This is handled by breaking combat , which is handled by the two events breakattack and brokeattack . The first of these events is triggered when the game wants a character to stop attacking, and the second event is triggered when a character stops attacking another character.

NOTE

Of the combat events listed in Table 18.6 , the most useful event that can be used with the timer system is the initattack . If you want, you can make guard logics who demand that you leave the room, and if you don't, they initialize an attack on you in a few seconds or so. Breaking attacks can be useful as timed events as well. The tea-house battle scene from Matrix: Reloaded comes to minda mentor may stop attacking you after you have proven your worthiness by surviving for a minute or so.

Table 18.6 lists the parameters for the actions.

Table 18.6. Parameters for Combat Events

Event

arg1

arg2

arg3

arg4

stringdata

canattack

attacker

target

-

-

-

getaccuracydelta

attacker

target

-

-

-

attack

entitytype [*]

id [*]

attacker

-

-

initattack

entitytype [*]

id [*]

target

-

-

attacked

entitytype [*]

id [*]

attacker

-

-

brokeattack

entitytype [*]

id [*]

attacker

-

-

breakattack

entitytype [*]

id [*]

-

-

-

killed

entitytype [*]

id [*]

victim

-

-

deathtransport

dead player

-

-

-

-


[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

[*] These are optional parameters that route the event to an entity when timers are used. See Chapter 14 for more information.

Most of those actions can be routed through the timer system, but I don't really have a reason for doing that at this time.

The getaccuracydelta query is a special kind of query that asks players being attacked if they have any special accuracy values when being attacked by a certain character. For example, you could have special vampire hunter characters who would get an accuracy bonus when fighting vampires.

May I Attack You, Kind Sir?

The right to attack people isn't a right implicitly granted by BetterMUD's physical engine. As stated previously, the physical engine has no involvement with attacks, nor should it. Attacking is a logical operation in the BetterMUD, and thus you need to ask permission of a character before attacking. By default, characters say, "No," because they don't have any logic modules that say "Yes."

Whenever a character with the combat module is queried about combat, this is the code that is executed:

 if action == "query" and data == "canattack":     return 1 

In this simple module, it is assumed that anyone with the combat module can be attacked. You might think of way to make it more flexible in the future, such as not allowing certain people to attack other people, or whatever you brainstorm.

Experience

Whenever you kill a character and that character gives you experience, the character sends you an event telling you that your experience changed:

 if action == "modifyattribute" and data == "experience":     me.DoAction( "announce", 0, 0, 0, 0, "<#00FFFF>You gain " + str( arg4 ) +                  " experience!" )     return 

As usual, me has already been initialized as a character accessor pointing to yourself.

This simply notifies your character that you've gained some experience. Remember that when you get attribute modification events, arg3 is the new value of the attribute, and arg4 is the delta from the old value, so it tells you how much you gained. ( arg4 is optional, but the combat module supports it.)

Modifying Max Hitpoints

The combat module is notified whenever a player's maximum hitpoints change, so that it can ensure that the player never ends up with more hitpoints than his maximum. It's a simple piece of code:

 if action == "modifyattribute" and data == "maxhitpoints":     if me.GetAttribute( "hitpoints" ) > me.GetAttribute( "maxhitpoints" ):         me.SetAttribute( "hitpoints", me.GetAttribute( "maxhitpoints" ) )     return 

Note that I use SetAttribute to directly reset the hitpoints on the character, rather than notifying the Mud's physical engine. This is a matter of personal preference. Telling the physical engine tells everyone that the hitpoints have changed, but that kind of behavior is usually reserved for situations in which a player is being damaged or healed. In this case the player is being neither healed nor damaged, and his physical attributes are changing. If it bothers you, you can change this so that the game tells all the other modules that a player's hitpoints have changed.

Under Fire

Whenever a player is attacked, or a player stops attacking another player, the game announces those events. The messages are relatively easy to handle. The combat module must add characters to its attacked-list when it is attacked, and remove them when they stop attacking.

Here's the attacked event:

 if action == "do" and data == "attacked":     try:         self.attackedlist.index( arg3 )     except:         self.attackedlist.append( arg3 )     return 

The Python list.index function searches a list for anything matching the argument. In this case I try finding arg3 , which is the ID of the character trying to attack the other character. The function returns the index of that item, but I'm not really interested in the index of the item, since it probably doesn't exist in the list anyway. Instead, I am actually counting on it to throw an exception if a match doesn't exist. In the exception block, I add the ID to the list. This just makes sure that you can't put an ID into the list more than once by accident .

Likewise, I need to remove the ID when the character breaks attacking:

 if action == "do" and data == "brokeattack":     try:         self.attackedlist.remove( arg3 )     except:         pass     return 

This catches any errors and ignores them. remove throws if arg3 isn't in the list.

En Garde!

Whenever you initiate an attack, whether triggered by a command or by some other logic module, the combat module receives an initattack event. This must accomplish several actions, such as adding an attack timer, setting a target, and maybe clearing an old target (if a character was already attacking another character). Here's the code:

 if action == "do" and data == "initattack":     if arg3 == self.me: return     # clear the old target if already attacking someone else     if self.target != 0:         t = BetterMUD.character( self.target )         t.DoAction( "do", 0, 0, self.me, 0, "brokeattack" )     else:         self.mud.AddActionRelative( 0, "do", 0, self.me, 0, 0, "attack" )     # set the new target and tell him he's been attacked     self.target = arg3     t = BetterMUD.character( arg3 )     t.DoAction( "do", 0, 0, self.me, 0, "attacked" )     self.mud.AddActionAbsolute( 0, "vision", me.Room(), 0, 0, 0, me.Name() +                                 " begins attacking " + t.Name() + "!!" )     return 

First you must check to ensure a character isn't attacking himself (no self-hatred in this game). So if a character is trying to attack himself, the function simply returns, and does nothing.

If the module is already attacking another player, you need to break the attack. If a player is already attacking someone else, that means you should already have an attack event in the timer queue, so you don't need to add one. If a player isn't already attacking, however, you need to add an attack event into the global timer queue.

Once that has been done, you set the target, tell him he's been attacked, and tell everyone in the room that the attack has begun.

To the Barricades

On the other hand, you must break an attack when certain events occur. In fact, breaking an attack happens so often that I decided to make it into a helper function:

 def Break( self, me ):     if self.target == 0:         return     t = BetterMUD.character( self.target )     me.KillHook( "do", "attack" )     self.mud.AddActionAbsolute( 0, "vision", me.Room(), 0, 0, 0, me.Name()                                 + " stops attacking " + t.Name() + "!!" )     t.DoAction( "do", 0, 0, self.me, 0, "brokeattack" )     self.target = 0 

The parameter me is supposed to be an accessor pointing to the character who is breaking combat.

It grabs the target, and then tells the current character to kill its attack command that is sitting in the timer queue. There should be an attack command in the timer queue, since a character is in attack mode, and there is always an attack command in the queue when a character is in this mode.

The attack command tells the room that the attack has stopped, and then tells the former target that the attack has stopped, and finally clears the attacker's target value.

You often need to break combat, for example, when the breakcombat event occurs:

 if action == "do" and data == "breakattack":     self.Break( me )     return 

Or when an attacker or a target leaves the room:

 if action == "leaveroom":     if arg1 == self.target or arg1 == self.me:         self.Break( me )     return 

Or when one player tells another that he's killed them (believe me, attacking corpses doesn't improve the world):

 if action == "do" and data == "killed":     self.Break( me )     return 

There's another instance that calls the break function, but I'll get to that later.

The Whites of Their Eyes

The second most complex section of the combat module is the part that carries out the combat rounds. I'll split this up into chunks so it's easier to understand.

 if action == "do" and data == "attack":     target = BetterMUD.character( self.target )     self.mud.AddActionRelative( self.attacktime, "do", 0, self.me, 0, 0,                                 "attack" ) 

First, the combat module gets the target. Then the module adds another attack round to the global timer. Even if a player kills his target in this round, it's not a big deal, because when he breaks off combat, this action is automatically killed.

The next part gets a weapon:

 if me.GetAttribute( "weapon" ) == 0:         weapon = BetterMUD.itemtemplate( me.GetAttribute( "defaultweapon" ) )     else:         weapon = BetterMUD.item( me.GetAttribute( "weapon" ) ) 

This is possibly the coolest part of the code. If a character doesn't have a weapon armed, the character uses the default weapon of his character, which is not an actual item, but rather an ID into the item's template database. I showed you this earlier, when I showed you how to arm weapons.

Basically, if you don't have a weapon, an itemtemplate accessor is stored in weapon ; if you do have a weapon, the item accessor pointing to your current weapon in stored in weapon . Here's the best part: even though they are two totally different accessors, as long as they have the same function names, it doesn't matter! Look at this part of the code and sigh happily:

 accuracy = weapon.GetAttribute( "accuracy" )     accuracy += target.DoAction( "query", me.ID(), target.ID(), 0, 0,                                  "getaccuracydelta" )     accuracy += me.DoAction( "query", me.ID(), target.ID(), 0, 0,                              "getaccuracydelta" ) 

The accuracy is grabbed from the weapon and then modified according to the accuracy deltas returned by the target and the player. This allows the player to have special modules that do +5 accuracy versus certain characters, and so on. The next part checks to see if one character hit another:

 if accuracy <= random.randint( 0, 99 ):         self.mud.AddActionAbsolute( 0, "vision", me.Room(), 0, 0, 0,             me.Name() + " attacks " + target.Name() + " with " +             weapon.Name() + ", but misses!" )         return 

This uses the same 099 scale that I showed you in the SimpleMUD. If a character misses, the room is informed of the miss , and then the function returns.

The next task is to calculate the damage done, tell everyone about the hit, and then modify the hitpoints attribute:

 damage = random.randint( weapon.GetAttribute( "mindamage" ),                              weapon.GetAttribute( "maxdamage" ) )     self.mud.DoAction( "vision", me.Room(), 0, 0, 0, "<#FF0000>" +                        me.Name() + " hits " + target.Name() + " with " +                        weapon.Name() + " for " + str( damage ) + " damage!" )     self.mud.DoAction( "modifyattribute", 0, target.ID(),                        target.GetAttribute( "hitpoints" ) - damage,                        damage, "hitpoints" ) 

And then the combat round is over! Catch your breath .

Note that the hitpoints aren't actually modified. Instead the function sends an action to the game module telling it that the hitpoints have been modified. This is done so that the game can continue and tell every logic module that the character's hitpoints have been changed.

Et Tu, Brute!

There comes a time when all things must die. In a dangerous digital world such as a MUD, the end occurs often. The combat module is naturally the module that detects the ending of a character's mortal coil, which it does by monitoring changes to a character's hitpoints attribute:

 if action == "modifyattribute" and data == "hitpoints":     if arg3 <= 0:         me.DoAction( "do", 0, 0, 0, 0, "died" ) 

If a character's hitpoints ever reach 0 or below, he dies! Kerplunk.

The final thing the combat module must manage is handling when characters die. Again, this is a rather large function, so I'm going to break it up into several sections:

 if action == "do" and data == "died":     self.Break( me )     self.mud.AddActionAbsolute( 0, "vision", me.Room(), 0, 0, 0, me.Name() +                                 " dies!!!" ) 

So now the character is dead, and most likely he was engaged in combat when he died (trying to fight off his slayer ), so the game tells the character to break off combat (no ghost fighters allowed). After that, characters in the room are told of the untimely demise.

Then the character's experience is calculated and divided up among his attackers:

 experience = me.GetAttribute( "giveexperience" )     if len( self.attackedlist ) > 0:         experience = experience / len( self.attackedlist ) 

This grabs a character's giveexperience attribute, and divides it by the number of attackers. You should note that it's perfectly acceptable to have no attackers and still die (from a trap, for example), and the code makes sure the length of the attack list is more than 0 before it performs division.

Now you need to go through the character's attack list, tell everyone he died, and then give them a share of his experience:

 for x in self.attackedlist[:]:         c = BetterMUD.character( x )         c.DoAction( "do", 0, 0, self.me, 0, "killed" )         self.mud.DoAction( "modifyattribute", 0, x,                            c.GetAttribute( "experience" ) + experience,                            experience, "experience" ) 

This uses a standard Python for-loop, with a twist. The [:] notation at the end of self.attackedlist makes a copy of self.attackedlist . This is important, because as you go through the list, telling everyone that the character died, they remove themselves from the list, and you can't iterate through Python lists that change while you're iterating through them.

The next step is to make sure the attack list is clear, and then drop all of the character's items:

NOTE

The combat module can only detect changes in hitpoints if you send modifyattribute messages to the game itself. Sending those messages directly to characters isn't a good idea because they don't actually implement the attribute modification. This is also why directly modifying the hitpoints is a bad idea; other modules that might need to know about changes in hitpoints might not be told.

 self.attackedlist = []     me.BeginItem()     while me.IsValidItem():         self.mud.DoAction( "dropitem", me.ID(), me.CurrentItem(), 0, 0, "" )         me.NextItem() 

Any items that refuse to be dropped stay on the character. This is done because it is assumed that items that refuse to be dropped are probably cursed, and should stick to a character until the curse is removed. Since this is in a script, you can easily change it so that it calls the forcedropitem action instead, if you don't want this functionality.

The section of code tries to figure out how to kill a character, based on whether the character is a player or not:

NOTE

As I mentioned in Chapter 15 , it is important to force items into the room when a character is destroyed. If they aren't forced, the game thinks that the items are still on the non-existent character, and you'll have a bunch of items that are just floating around in your memory, not able to be used.

 if not me.IsPlayer():         self.mud.AddActionAbsolute( 0, "destroycharacter", self.me, 0, 0, 0,                                      "" ) 

If the character isn't a player, the function just destroys him. This means that any items that weren't dropped previously are now forced into the room.

The final part of the code is executed when a player dies; his hitpoints are set to 70% of the maximum, and then the game tries to figure out where to transport him:

 else:         me.SetAttribute( "hitpoints", (me.GetAttribute( "maxhitpoints" )                          / 10) * 7 )         r = BetterMUD.room( me.Room() )         if r.DoAction( "do", me.ID(), 0, 0, 0, "deathtransport" ):             return         r = BetterMUD.region( me.Region() )         if r.DoAction( "do", me.ID(), 0, 0, 0, "deathtransport" ):             return         if me.DoAction( "do", me.ID(), 0, 0, 0, "deathtransport" ):             return         self.mud.DoAction( "forcetransport", me.ID(), 1, 0, 0, "" ) return 

The respawn system is hierarchical. Whenever a character dies, he is transported somewhere, but the ethereal destination varies depending on where the character is. The room, region, and player are all asked to perform a death transport event. If they don't perform any transporting, 0 is returned, and the function goes on to the next entity. When the deathtransport action is invoked, it should return non-zero to signify that it has accomplished the transportation.

If none of the threethe room, region, and player transport the player, the function finally gives up and transports the player to room 1 as illustrated in Figure 18.3.

Figure 18.3. The game uses this process to figure out how a player should die.

graphic/18fig03.gif


This kind of a system is useful on a few levels. If you ever implement some kind of honor / reputation system, you can have good players respawn in a pleasant area, or bad players respawn in a dismal area. It's also helpful to have regions respawn players in a local area, so that a player doesn't have to wander around the entire game trying to get back to where he was. Or even more elaborate, you can have a room that serves as an altar to some obscure death-cult, and if a player dies in that room, the game sends you to a special area of the game. The point is that with this scripting system, you can do almost anything you want.

Add-On Modules for the Combat of Nonplayer Characters

Once you have a combat module, you can give it to anyone who you want to be able to attack or be attacked, but that's not quite good enough. This module can be given to anyone who will perform combat, but it doesn't actually make anyone attack anyone else. It doesn't send any initattack or breakattack messages on its own. It just responds to them. To make NPCs in the game attack, you need to give them additional modules that react to certain events.

For example, I could create an evil combat AI module, which would attack people the moment it saw them, and wouldn't break until either a character left or died. The evilmonster logic module can also be found in /data/logics/characters/combat.py.

The first thing the module must do is attack anyone who enters the room:

 if action == "enterroom":     if arg1 != self.me:         self.mud.AddActionAbsolute( 0, "do", 0, me.ID(), arg1, 0,                                     "initattack" )     return 

Since characters are notified when they themselves enter the room, you need to make sure that the character doesn't start attacking himself. As long as he's not attacking himself, he can start an attack initialization immediately.

Then you must ensure that once the enemy has dispatched his prey, he begins attacking someone else in the room (in case there are other people). This is triggered whenever the enemy receives a "killed" event, telling him that he just killed someone:

 if action == "do" and data == "killed":     r = BetterMUD.room( me.Room() )     r.BeginCharacter()     while r.IsValidCharacter():  if r.CurrentCharacter() != arg3:  self.mud.AddActionAbsolute( 0, "do", 0, me.ID(),                 r.CurrentCharacter(), 0, "initattack" )             return         r.NextCharacter()     return 

This simply goes through every character in the room and finds a new victim to attack. There is just one thing to watch for, however; this code is called before the old victim has been removed from the room, so you need to make sure you don't try to re-attack him. This is done in the line in bold; arg3 holds the ID of the character who just died.

Of course, the great thing about these kinds of scripts is that you can make different AIs. I've included another AI script called "defender" in the same file, but the AI shown here attacks only when attacked and breaks off when you break off combat.

Once More into the Breach

The final topic dealing with combat involves the attack commands, which allow a character to attack someone else. These two commands can be found in /data/commands/ usercommands.py. The first one handles attacking:

 class attack( PythonCommand.Command ):     name = "attack"     usage = "\"attack <character>\""     description = "initiates attack mode on a character"     def Run( self, args ):         if not args: raise PythonCommand.UsageError         me = BetterMUD.character( self.me )         r = BetterMUD.room( me.Room() )         t = FindTarget( r.SeekCharacter, r.IsValidCharacter,                         r.CurrentCharacter, args )         target = BetterMUD.character( t )         if not target.DoAction( "query", me.ID(), 0, 0, 0, "canattack" ):             return         me.DoAction( "do", 0, 0, t, 0, "initattack" ) 

NOTE

The canattack query assumes that there's some module attached to the character in question that tells the charac ter why he cannot be attacked. This is useful for many situations. For example, if a player tries to attack a demon god in your game, the god smites him immediately and says to him, "How dare you attack me?!" Or you could have a moral system in place that prevents a character from striking down elderly women. The point is that you can do anything with these script modules.

The code is similar to what you've seen before. The function tries to find a character to attack using the FindTarget function, and then the command asks the character if he can be attacked. If not, the function just returns. If the character can attack, the command sends an initattack event to the character.

Breaking attacks is even easier than attacking:

 class breakattack( PythonCommand.Command ):     name = "breakattack"     usage = "\"breakattack\""     description = "Stops attacking your target"     def Run( self, args ):         me = BetterMUD.character( self.me )         me.DoAction( "do", 0, 0, 0, 0, "breakattack" ) 

All you do is send a breakattack event to yourself. That's it!

[ LiB ]


MUD Game Programming
MUD Game Programming (Premier Press Game Development)
ISBN: 1592000908
EAN: 2147483647
Year: 2003
Pages: 147
Authors: Ron Penton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net