The purpose of this document is to assist those who wish
to add custom Items, MOBs, Behaviors, Properties, or other objects
to CoffeeMud. The reader should be familiar with Java programming,
and should be experienced with writing and compiling Java classes.
The object oriented notions of class inheritance and polymorphism,
as well as the Java constructs of interfaces should be at least
vaguely familiar to you before attempting to build classes for
CoffeeMud. Also, it is expected that all of the ideas presented
in the Archons Guide (ArchonGuide.html) are completely familiar.
The difference between a GenItem and a StdItem, or a GenMob and
a GenPostman will not be explained in this document.
It is not expected that someone would wish to dive in
and make wholesale changes to the CoffeeMud system right away, but
is more likely wanting to fill in a functional gap in the system
for their own needs. For this reason, this document is not organized
as a comprehensive guide to programming CoffeeMud. Instead, it is
designed to be a quick reference for those who wish to create the
spot MOB, Behavior, Item, or Property for use on their maps.
With this in mind then, let's start out with some brief
development instructions, and then and in no particular order, discuss
the several essential object types in CoffeeMud are presented.
-
Go to your coffeemud directory and edit the make.bat,
the first line will be something like: SET JAVACPATH=
C:\jdk1.5.0_05\bin\javac This might be different for
you depending on where your java development package is installed.
-
Save the bat file.
-
Run the bat file by double clicking on it. This
will compile the mud, making all the .class files.
-
Go to your coffeemud directory and edit the makeUNIX.sh,
the first line will be something like: Java_Home=/home/knoppix/j2sdk1.4.2
This might be different for you depending on where your java
development package is installed.
-
Save the shell script.
-
Issue this command: chmod 755 makeUNIX.sh
-
Execute the shell script. This will compile the
mud, making all the .class files.
If you perused the coffeemud.ini file as mentioned in
the Installation Guide, you may have noticed and wondered about
the section at the end which lists the default load paths for the
several CoffeeMud objects.
By default, the CoffeeMud engine dynamically loads the
vast majority of its object code at boot time by referring to the
paths specified in the coffeemud.ini file. The value
%DEFAULT% is always used as a substitute for the
default CoffeeMud object path. For instance, if you installed
CoffeeMud at "C:\CoffeeMud\ ", then "BEHAVIORS=%DEFAULT% "
in the ini file would load "C:\CoffeeMud\com\planet_ink\coffee_mud\Behaviors\*.class "
into its behavior set.
This default object boot paths may be removed or added-to
using semicolon delimited paths, or even replaced with your own
object boot directories. In fact, when adding objects to your CoffeeMud
boot sequence, it is recommended that you place your objects in
a separate directory path inside your CoffeeMud folder and add its
path to the coffeemud.ini file under the proper setting. The order
in which you place multiple paths in a single entry is also
significant, as the CoffeeMud ClassLoader will load the files in
the order in which they appear listed. For instance:
MOBS=%DEFAULT%;/resources/examples/MyClass.class;/resources/otherclasses
Will cause the default CoffeeMud versions of the mob classes
to be loaded first, followed by MyClass.class, followed by all
the class files in the resources/otherclasses folder in your CoffeeMud
package. Also notice that the ClassLoader follows the rules of
the CMFS described in the Archons Guide, meaning that the forward
slash is always the proper path separator, and that no folder outside
of your CoffeeMud package may be referenced. Also bear in mind
that case is sensitive when naming Java class files, even in these
boot paths.
When writing these custom classes for your special object
boot directory(s), it is important to keep a number of things in
mind:
-
Do not mix your object types in the same directory!
Never try to boot custom items from the same directory from
which you boot your custom mobs. It will only confuse you and
CoffeeMud.
-
Java Packaging is irrelevant, you may package
your classes or not.
-
Implement or extend a class that implements the
proper interfaces. If you are coding mobs, this would mean the
MOB interface. If you are coding locales, the Room interface,
etc, etc. See the section on the object type you are coding
for the proper interface to implement. You may get around this
requirement by extending one of the base classes, such as
StdItem, StdMOB, StdContainer, StdRoom, StdAbility, GenMob,
GenItem, GenContainer, etc.
-
Make sure the ID() method in your
classes always matches the name of your class. You will understand
this better as you reference the object sections below.
-
Try to make the name() methods in
your classes return name values unique among all objects, especially
objects of that type. This is not a hard fast rule, and breaking
it will not cause malfunction in the system, but breaking this
rule WILL make writing help files impossible.
-
Class files loaded directly in the CoffeeMud
classpath can not be reloaded at run-time using the UNLOAD
and LOAD commands. If you plan on making changes
to your classes during run-time, place them in their own
directories.
-
As a general rule, you may import any
"interfaces.* " packages in the base CoffeeMud
structure, any core Java packages, the CoffeeMud "core.* "
package, and any single base CoffeeMud class you may be extending.
Do not import more than that. Use the CMClass getter methods
if you need to create new instances of CoffeeMud classes, and
use the CMLib methods to access her code libraries.
import com.planet_ink.coffee_mud.core.*; import com.planet_ink.coffee_mud.core.interfaces.*; import com.planet_ink.coffee_mud.Abilities.interfaces.*; import com.planet_ink.coffee_mud.Areas.interfaces.*; import com.planet_ink.coffee_mud.Behaviors.interfaces.*; import com.planet_ink.coffee_mud.CharClasses.interfaces.*; import com.planet_ink.coffee_mud.Commands.interfaces.*; import com.planet_ink.coffee_mud.Common.interfaces.*; import com.planet_ink.coffee_mud.Exits.interfaces.*; import com.planet_ink.coffee_mud.Items.interfaces.*; import com.planet_ink.coffee_mud.Locales.interfaces.*; import com.planet_ink.coffee_mud.MOBS.interfaces.*; import com.planet_ink.coffee_mud.Races.interfaces.*; import com.planet_ink.coffee_mud.Libraries.interfaces.*; import java.io.IOException; import java.util.*;
Before we get started with objects, needs must the topic
of text display be covered. Throughout the system you will see text
being sent to the user. Since a mud is a text producing engine,
this should be no great surprise. However, within that text you
will often see different kinds of codes and tags which affect the
output. For instance, consider the following lines:
msg=CMClass.newMsg(mob,target,this,affectType,"<S-NAME> reach(es) for <T-NAMESELF>."); mob.location().show(mob,null,CMMsg.MSG_OK_ACTION,"<S-NAME> regain(s) <S-HIS-HER> feet.");
Focusing only on the text for a moment, you will notice
that special tags are used to designate a player name, or the name
of the target of a spell. You will also notice that (s) and (es)
is used to modify the proper form of a verb. These are key features
of the CoffeeMud text engine. Here is a more complete list of
available tags:
<S-HIS-HER>
|
Outputs 'Your' if Observer=Source,
otherwise 'His'/'Her'. |
<S-HIM-HER>
|
Outputs 'You' if Observer=Source,
otherwise 'Him'/'Her'. |
<S-NAME>
|
Outputs 'You' if Observer=Source,
otherwise the Name. |
<S-NAMESELF>
|
Outputs 'Yourself' if Observer=Source,
otherwise the Name |
<S-NAMENOART>
|
Outputs 'You' if Observer=Source, otherwise
the Name minus any prefix articles (a, an, some, etc..).
|
<S-HE-SHE>
|
Outputs 'You' if Observer=Source, otherwise
'He'/'She' |
<S-SIRMADAM>
|
Outputs 'Sir'/'Madam'
|
<S-IS-ARE>
|
Outputs 'Are' if Observer=Source, otherwise
'Is'. |
<S-HAS-HAVE>
|
Outputs 'Have' if Observer=Source,
otherwise 'Has'. |
<S-YOUPOSS>
|
Outputs 'Your' if Observer=Source,
otherwise the Name`s |
<S-HIM-HERSELF>
|
Outputs 'Yourself' if Observer=Source,
otherwise the 'Himself'/'Herself' |
<S-HIS-HERSELF>
|
Outputs 'Yourself' if Observer=Source,
otherwise the 'Hisself'/'Herself' |
<T-HIS-HER>
|
Outputs 'You' if Observer=Target, otherwise
'His'/'Her'. |
<T-HIM-HER>
|
Outputs 'You' if Observer=Target, otherwise
'Him'/'Her'. |
<T-NAME>
|
Outputs 'You' if Observer=Target, otherwise
the Name. |
<T-NAMESELF>
|
Outputs 'Yourself' if Observer=Target,
otherwise the Name |
<T-NAMENOART>
|
Outputs 'You' if Observer=Target, otherwise
the Name minus any prefix articles (a, an, some, etc..).
|
<T-HE-SHE>
|
Outputs 'You' if Observer=Target,
otherwise 'He'/'She' |
<T-SIRMADAM>
|
Outputs 'Sir'/'Madam'
|
<T-IS-ARE>
|
Outputs 'Are' if Observer=Target, otherwise
'Is'. |
<T-HAS-HAVE>
|
Outputs 'Have' if Observer=Target,
otherwise 'Has'. |
<T-YOUPOSS>
|
Outputs 'Your' if Observer=Target,
otherwise the Name with an '`s' |
<T-HIM-HERSELF>
|
Outputs 'Yourself' if Observer=Source,
otherwise the 'Himself'/'Herself' |
<T-HIS-HERSELF>
|
Outputs 'Yourself' if Observer=Source,
otherwise the 'Hisself'/'Herself' |
Occasionally, you will find color/font codes embedded
in system strings. For instance:
msg.append("^!You are thirsty.^?\n\r");
These codes are as follows:
^N |
Normal |
^! |
Bold |
^H |
Highlight |
^_ |
Underline |
^* |
Blink |
^/ |
Italics |
^. |
Reset (turns off reverse)
|
^^ |
Generates an untranslated "^" character
|
^? |
Restores previous color
|
^f |
You-Fight |
^e |
Fight-You |
^F |
Fight |
^S |
Spell |
^E |
Emote |
^T |
Talk |
^Q |
Channel Background |
^q |
Channel Foreground |
^x |
Important message 1 |
^X |
Important message 2 |
^Z |
Important message 3
|
^O |
Room Title |
^L |
Room Description |
^J |
Weather |
^D |
Direction |
^d |
Door |
^I |
Item |
^M |
MOB |
^U |
Unexplored Direction
|
^u |
Unexplored Door |
^w |
White |
^g |
Green |
^b |
Blue |
^r |
Red |
^y |
Yellow |
^c |
Cyan |
^p |
Purple |
^W |
Dark White |
^G |
Dark Green |
^B |
Dark Blue |
^R |
Dark Red |
^Y |
Dark Yellow |
^C |
Dark Cyan |
^P |
Dark Purple |
^< |
< character. Used for MXP tags
only. |
^> |
> character. Used for MXP tags
only. |
^& |
& character. Used for MXP tags
only. |
As you might have guessed, it is preferred that the system
colors (the last 16 codes) be used sparingly, in favor of the more
customizable codes above.
JavaScript is an interpreted scripting language which
is used in various parts of CoffeeMud. The CoffeeMud engine integrates
the Rhino Javascript interpretor package from Mozilla in such places
as the Scriptable behavior, the JRun command, the ClassLoader, the
Quest engine, and the web server.
In lieu of a complete write up on the syntax of this language,
it is suggested that you read the documentation available from
the authors of the interpretor here: http://www.mozilla.org/js/ and
http://www.mozilla.org/rhino/.
If you are familiar with writing Javascript for web browsers,
there are several differences you will need to adjust to when using
Javascript in CoffeeMud. A minor difference is that the Rhino
interpretor requires that all variables be declared before use.
A more important difference is that Javascript Strings are not the
same as Java String objects, and that confusing them can lead to
errors. To get around this problem, all of the implementations of
Javascript in CoffeeMud, with the exception of the ClassLoader,
Javascript in CoffeeMud, with the exception of the ClassLoader,
provide a special method to convert Javascript strings into Java
Strings before passing them to Java methods or objects which will
require them. An example is below:
var javascriptstring=' this is a javascript string '; var javastring=toJavaString(javascriptstring); // and now javastring is a real-live Java-compliant and Java-friendly string
When writing Java classes in JavaScript, however, this
method is only available through the CMLib object in the core package.
This is how you would access the toJavaString() method
from a class written in JavaScript:
var javascriptstring=' this is a javascript string '; var javastring=Packages.com.planet_ink.coffee_mud.core.CMLib.toJavaString(javascriptstring); // and now javastring is a real-live Java-compliant and Java-friendly string
The most important difference between coding Javascript
for CoffeeMud and for browsers is that there is no HTML DOM (Document
Object Model), and therefore several of the libraries you are
used to are probably missing, such as Math. For this reason, it
is necessary for you to learn the CoffeeMud object packages in
order to get access to useful data and useful libraries. And now
you understand why the JavaScripting notes are kept in the Programming
guide. :)
To access the CoffeeMud object packages, you will need
to make use of the Packages object to reference external packages.
So long as the imported objects are in your CoffeeMud classpath,
they can be accessed and used. For instance, to use the CoffeeMud
pow(x,y) function in CMParms.java:
var lib=Packages.com.planet_ink.coffee_mud.core.CMLib; // the above creates a reference to the CoffeeMud Library as a shortcut var value=lib.math().pow(4,2); // now we can access the math() library from our shortcut.
Depending upon the context from which your script runs
(the Scriptable behavior, JRun command, the Quest engine, or the
http/web server), certain other objects are made available to
assist scripts in properly interacting with their environment.
When writing Java classes in JavaScript for the ClassLoader, however,
you must always use the Packages.com.planet_ink.coffee_mud.core.CMLib
reference to access special methods such as the toJavaString method
discussed above.
In the Scriptable behavior, several methods are made available
to access objects which are related to the event which triggered
the scripted code. These methods include MOB source(),
Environmental target(), Environmental host(), Item item1(), Item
item2(), String message(), and MOB monster() .
The JRun command provides the methods MOB mob(), int
numParms(), String getParm(int i), and String
getParms() . The web server makes the ExternalHTTPRequests
request() object available, as well as the method void
write(String s) . The quest engine makes the current running
Quest object available from the method Quest quest()
and the current state of the quest setup script available in a custom
QuestState object referencing method called QuestState setupState()
The last piece of general information about JavaScript
in CoffeeMud concerns writing Java classes for the CoffeeMud ClassLoader.
Any JavaScript file *.js included in the ClassLoader boot paths
(see the previous section) will be loaded and treated just like
any Java compiled *.class file. The JavaScript file would be parsed,
compiled, and loaded at boot time. For the most part, writing
Java Classes in JavaScript is extremely similar to writing Java
Classes in Java. Base classes may be extended (using the special
CoffeeMud //extends command in your JavaScript),
interfaces may be implemented (using the special CoffeeMud
//implements command in your JavaScript), super class
variables and methods may be accessed (using the this.variableName
and this.super$methodname() syntax), and super class
methods may be overridden by JavaScript functions of the same name
and number of parameters. Class files written in Javascript *.js
files may also be loaded and unloaded at runtime using the LOAD
and UNLOAD Archon commands, which gives them a step up on native
Java classes in the JVM classpath.
Below is an example of a Java Class written in JavaScript.
Examples of Embedded JavaScript in CoffeeMud Virtual Pages (cmvp)
web files can be found in the Web Server Guide. Examples of Embedded
JavaScript in a Scriptable MOBPROG script can be found in the
Scripting Guide.
Our Java Class example is called GenLemming.js. It is
a sample MOB class to demonstrate extending the GenMob class to
create a type of modifiable mob for your maps. In this example,
we add functionality to make all mobs in the world created from
the GenLemming base suicidal. To use this class, save the code somewhere
in our CoffeeMud folder under the name "GenLemming.js", and add
an object path reference to it in the MOBS entry in your coffeemud.ini
file, as described in section one.
//extends com.planet_ink.coffee_mud.MOBS.GenMob
function ID(){return "GenLemming";}
var lib=Packages.com.planet_ink.coffee_mud.core.CMLib;
The first lines of our class include the special //extends
command which informs the CoffeeMud ClassLoader that this JavaScript
class will extend GenMob, thereby inhereting all functionality of
the maleable GenMob.
The ID() method is required in all CoffeeMud
classes. It must be the simple name of the class, and must match
the name of the JavaScript file containing it. For example, GenLemming.js
contains class GenLemming and returns an ID of "GenLemming". They
all match exactly.
Lastly, we define the variable "lib" to act as a shortcut
to the CoffeeMud core Libraries.
Now, moving on; since we don't have the ability to write
constructors in JavaScript, any initial fields we need to set
whenever a new instance of our class is created must be done in
the CoffeeMud newInstance() method as shown here:
function newInstance() { var lemm=this.super$newInstance(); lemm.setName("a generic lemming"); lemm.setDisplayText("a generic lemming is waiting to commit suicide"); return lemm; }
There are several interesting points to make here. One
is to notice that the function has no explicit return type, which
is part of the JavaScript standard of being "weakly typed". Also
notice the syntax for calling the SuperClass version of the
newInstance() method -- this.super$newInstance() .
This is very different from Java syntax and should be noted.
And now we move on to overriding our first GenMob method,
tick.
var countdown = 10;
function tick( host, tickID ) { if( !this.amDead() ) { countdown--; if( countdown <= 0 ) { lib.combat().postDeath( null, this, null ); countdown = 10; } } return this.super$tick( host, tickID ); }
The public boolean tick(Tickable host, int tickID)
method from the standard MOB interface is designed to be called
every CoffeeMud tick (about 4 seconds). Now, the tick method in
StdMOB, which is extended by GenMob, handles things like recovering
hit points, automatic combat rounds, and other important periodic
activities. It is explained further in Core Topic 3.
For our GenLemming, we create a variable to count down
the ticks from 10 to 0. When the countdown variable reaches 0, we
call the postDeath(MOB killer, MOB killed, CMMsg msg)
method, which is part of the CombatLibrary in the core libraries.
"lib" is the variable we defined above as a shortcut to our core
libraries. The last thing we do is return control to the SuperClass
version of the tick method.
Now we'll get creative and implement a message previewer
(see the Core Topic 1 below for more information on message previewing
and handling). This method will be called when any event happens
in the same room as the GenLemming mob. We will use this fact to
look for, capture, and modify the message string which will inform
the room of our impending death. Since it is the previewing method,
it will be called BEFORE the activity actually takes place, giving
us a chance to make our modifications before anyone actually sees
the message strings.
function okMessage(host,msg) { if( ( msg.isSource( this ) ) &&( msg.isOthers( "DEATH" ) ) &&( msg.othersMessage() != null ) ) { msg.setOthersMessage("<S-NAME> jumps off a cliff!!!"); }
return this.super$okMessage(host,msg); }
In our message previewing method, we will check every
message that comes our way, acting only if this particular GenLemming
is the source of the message, that he appears to be dying to others
in the room ( isOthers( "DEATH" ) ) and that the message
being given to others in the room is a non-null string. In these
conditions, we modify the message which others in the room see.
When our condition is not met, we return control to the
SuperClass-GenMob version of okMessage.
CoffeeMud is essentially a distributed message passing
and handling system, where the actions and events that occur in
the system are represented as messages (Common.interfaces.CMMsg)
which are then previewed, modified, cancelled, and/or reacted to
by handlers. Understanding this idea is key to fully understanding
how CoffeeMud really works, so let's take a second and peruse this
concept in more detail.
Messages in CoffeeMud, at least as we are talking about
them here, always represent Events. Events such as a mob picking
up an item, swinging a sword at an opponent, taking damage from
a fireball, or getting pricked by a poisonous needle. These events
can never actually occur in CoffeeMud unless a proper message is
generated for them first. These messages, in the code, implement
the interface Common.interfaces.CMMsg , and are typically
an instance of the class Common.DefaultMessage .
Messages are created at the moment that the event needs
to occur. This moment can be triggered by the player entering a
command into their telnet client and pressing Enter. It can also
by triggered by the mindless algorithms which animate the mobs.
Either way, when the moment has come, a message is created, and
it looks like this:
CMMsg msg = CMClass.getMsg(mob, targetMOB, this, CMMsg.MSG_CAST_ATTACK_VERBAL_SPELL,"^S<S-NAME> invoke a spell at <T-NAME>s feet..^?", CMMsg.MSG_CAST_ATTACK_VERBAL_SPELL,"^S<S-NAME> invoke(s) a spell at your feet.^?", CMMsg.MSG_CAST_ATTACK_VERBAL_SPELL,"^S<S-NAME> invokes a spell at <T-NAME>s feet.^?" );
The above message was taken from the code for the Grease
spell, which calls the core.CMClass.getMsg method to
construct a CMMsg object. Constructing the CMMsg object
does not actually make anything happen, but it is the vital first
step. The message we constructed here, in this case, utilizes every
major component of a message. These components are, in order:
- Source
The source of any message must always be a valid
reference to an instance of the MOB interface. In short, all
events that occur in the system are a direct result of the
activity of a MOB. This is on the theory that the
universe is controlled and governed by sentience. In the
extremely rare instances where a mob is not readily available
to provide a message source, one should be instantiated -- even
if it is just a blank, new StdMOB.
- Target
The target of a message may be null, or any valid
reference to an instance of the Environmental interface, which
includes Items, MOBs, Rooms, Exits, etc. The type
and context of message you wish to generate will typically tell
you intuitively whether the source is doing something to
someone or something else, or is acting independently. This is
usually another mob or an item, but you will find examples of
all kinds of targets in the code.
- Tool
The tool of a message may be null, or any valid
reference to an instance of the Environmental interface, which
includes Items, Abilities, MOBs, Rooms, Exits,
etc. The tool represents something which the source is utilizing
to accomplish the task or generate the event. This is typically
either an Ability object (like a Spell or Skill being used),
or an Item object (like a weapon in an attack event).
- Source Code
This is an encoded integer which represents what
the source MOB is actually doing. We'll break down this code
below.
- Source Message
This is the string which the source MOB will see
should the event occur successfully.
- Target Code
This is an encoded integer which represents what
is happening to the target. If there is no target, this number
will typically have the value of 0 (CMMsg.NOEFFECT).
- Target Message
This is the string which the target MOB (if it
is a MOB) will see should the event occur successfully.
If there is no target, this string is null.
- Others Code
This is an encoded integer which represents how
any other objects (such as MOBs, Items, Rooms, Exits) other
than the source and target, in the same room, perceive the
event. If the event is completely imperceptible by anything
other than the source, it may be 0 (CMMsg.NOEFFECT)
- Others Message
This is the string which other MOBs in the same
room as the source and target MOBs will see should
the event occur successfully. If the event is completely
imperceptible by other MOBs, it may be null.
The Source Code, Target Code, and Others Code is easily
the most complicated aspect of a Message. For this reason, numerous
pre-configured message codes have been created in the
CMMsg interface, all of which begin with the characters MSG_.
Although we will not go into the meaning of each of these messages
(that will be left to the reader to search the code for instances
of messages which use the codes, and learn from the context in which
they are used), we can at least break down these codes so that
they can be better understood.
These coded integers all have two parts, the Major aspect
(or the Major Code) and the Minor aspect (or the Minor Code). They
may be referenced off of an already constructed CMMsg object
using such methods as sourceMajor() and sourceMinor() .
These methods will automaticallyy break down a sourceCode()
into the components we will discuss.
The Major code is a series of significant bits in the
integer, each of which gives some new meaning to the message. These
bits are as follows:
Bit mask
|
CMMsg Equate Variable(s)
|
Meaning |
1024 + 65536 |
MASK_HURT
|
*Special* Damage message.
|
2048 |
MASK_HANDS
|
Message includes small movements.
|
4096 |
MASK_MOVE
|
Message includes large, full-body movements.
|
8192 |
MASK_EYES |
Message includes visual information.
|
16384 |
MASK_MOUTH |
Message include mouth movement, or
consumption. |
32768 |
MASK_SOUND,
MASK_SOUNDEDAT |
Message includes auditory information.
|
65536 |
MASK_ALWAYS |
Override mask which flags the message
as something which Must occur, regardless of the state of
the source or target. |
131072 |
MASK_MAGIC |
Message has a magical nature.
|
262144 |
MASK_DELICATE |
Message includes very fine, delicate
movements, such as thief skills. |
1048576 |
MASK_CHANNEL |
Message is part of public channel conversation.
|
2097152 |
MASK_OPTIMIZE |
Message implementation should be optomized
for repetition. |
The above masks can be quite confusing. It is best to
examine the several MSG_ equates in the CMMsg interface to see how
they are properly or improperly used. Remember a MSG_ equate is
a completely constructed Code, complete with the appropriate Major
and Minor aspects.
The Minor Code represents the more specific activity being
performed, and is a simple integer ranging from 0 (NO EFFECT) to
2047. The officially recognized Minor codes are exhaustively listed
in the CMMsg interface, and all begin with the prefix TYP_. These
types cover every sort of major event which occurs in the CoffeeMud
engine, including getting items, casting spells, entering or
leaving rooms, etc, etc..
CMMsg msg = CMClass.getMsg(attacker,target,weapon,CMMsg.MSG_WEAPONATTACK, "<S-NAME>attack(s) <T-NAME>!");
The core.CMClass has many different getMsg signatures
to make message construction quick and painless. The above is an
example where only a single Code and a single message text are provided.
In constructors where only one Code or message text field are
provided, it is assumed that the code and message texts will be
the same for source, target (if any) and others.
CMMsg objects also have value() and setValue(int)
methods for modifying an integer not found in the constructor. This
number is used for several different purposes in message construction,
from the amount of damage in a TYP_DAMAGE message, to the amount
of experience in a TYP_EXPCHANGE message. This number is also used
to determine whether or not a standard saving throw was made. Value
defaults to 0, but, after running through a message which contains
a savable event, the value will be >0 if the save was made.
Once a Message has been constructed, it is time
to actually put the message out into the system. There is a
standard form for the sending of almost all messages. If the source
of the message is a MOB called "SourceMOB", this standard form
looks like this:
CMMsg msg = CMClass.getMsg(SourceMOB,TargetMOB,weapon,CMMsg.MSG_WEAPONATTACK, "<S-NAME>attack(s) <T-NAME>!");
if(SourceMOB.location().okMessage(SourceMOB,msg)) { SourceMOB.location().send(SourceMOB,msg); }
The location() field on a MOB refers to the
Room in which the mob is. Room's are always the top level at which
messages are previewed, and then executed or sent. The first line
(where the message is constructed) has already been examined. The
second line, in which the Room method okMessage() is
called, is the preview step. In this step, the message is evaluated
before it actually happens. The first parameter to okMessage()
is called the "host" object, and it refers to the object to which
the one you are sending the message should refer back to. This parameter
is rarely used, except by Behaviors, and it is always safe to use
the source of your message as this value. The second parameter
to okMessage() is the message we constructed. The
third line, where the Room send() method is called,
is the execution step.
In the preview step, the room object will examine the
message to see if there is anything which it might not like, wish
to modify, or wish to flag about the Message it has been handed.
If the Room object does not like the message, it will return false.
Returning false from okMessage() is always an order
to cancel, and not execute the message. Under any other circumstances,
true may be returned to allow the message to go forward. The Room
will also make calls to the okMessage() methods on
every other MOB in the room, Exit from the room, Item in the room,
spell effects which may be on the room, and behaviors of the room.
The MOB who receives the okMessage() call will, in
turn, pass the Message to the okMessage() methods in
every Item the MOB is carrying or wearing, every spell effect on
the MOB, and every behavior of the MOB. Items wills also make
okMessage() calls on their spell effects. Any of these calls
may modify or flag the message they receive. Any of these calls
may also return false. If any object which previews a message returns
false, the Room okMessage() method will also return
false, ordering the message to be totally canceled. For this reason,
okMessage() methods are careful about returning
true unless they have a really good reason not to.
Inside the okMessage() methods of every Item,
MOB, Behavior, Ability (spell effect), Exit, and Room the Messages
may (as we mentioned) be examined and modified, flagged, or canceled.
As we have already covered how Messages are canceled (by returning
false). Let us turn now to the manner in which Messages are
modified or flagged.
Message modification is very rare. When it happens though,
it is done by calling one of the several modify() methods
on the Common.interfaces.CMMsg object. These methods allow the
source, target, and all other fields to be updated. Message modification
should also happen during the preview step so that any changes
made to the message are made before the message is executed. It
is also often wise, after making a change to a message, to recursively
call the okMessage method on the room again so that the modifications
can be previewed, but this is not always necessary.
Message flagging is somewhat less rare. Messages
may be flagged when a combat strike is successful, or when a saving
throw is made against a spell Effect, or for any other reason
the Message constructing code may wish. Flagging is done by calling
the aforementioned CMMsg.setValue(int) method, and
using it to change the value to something other than the default
of 0. Flagging using the setValue() method lets the
code which constructed the Message know that something significant
with relation to the Message has occurred. The meaning of this value
will vary depending upon the type of message being generated, and
upon the purpose to which the creator of the message wishes to
put it. The value is read using the int value() method
on CMMsg.
Once the okMessage() method on a Room object
has returned true, and any code which may need to check or handle
modifications to the Message have executed, the Message is sent.
The proper way to send a Message is through the Room objects,
by calling one of the following Messages: Room.send(MOB
SourceMOB, CMMsg msg) or Room.sendOthers(MOB
SourceMOB, CMMsg msg) . The first method handles a
standard Execution, while the second allows every relevant object
except the SourceMOB to handle Execution. The first method should
almost always be called.
The send() methods will then begin calling
other methods in other objects. These other methods are called the
executeMsg() methods, and are usually of the form
public void executeMsg(Environmental myHost,
CMMsg msg); . These methods are responsible for Executing
the contents of the message. The Room method will make executeMsg()
method calls on itself, and on every Exit, Item, MOB, spell effects
(Ability object), and Behavior associated with that Room. As in
the okMessage() case, the MOBs will in turn call the
executeMsg() methods on their own Items and spell effects.
Items will then call the executeMsg() methods on their
own spell effects, and so on.
Of course, not every object in your game will handle and
react to the Execution of every Message sent. Most of the time,
a given object will be ignoring the Message altogether. However,
each object knows precisely which Messages are important for it,
and watch carefully for them in both their okMessage()
and executeMsg() methods. In general, every Message
which is previewed in an objects okMessage() method
is handled in the executeMsg() method of the same object,
though this is by no means always true. In general, the following
object types handle the following types of Messages:
- MOBs
Any Message which has the mob instance as a target
is both Previewed and Executed. Any Message which has the mob
as a source is typically Previewed, and (lacking a target) may
also be Executed.
- Items
Any Message which has the item instance as a target.
- Exits
Any Message which has the exit instance as a target
or tool.
- Rooms
Any Message which has the room instance as a target.
- Ability(spell effects)
Any Message pertaining to the MOB or Item
which is affected by the spell or skill.
- Behavior
Any Message pertaining to the object instance which
has this behavior.
Now that you are completely confused, it will
make you at least a bit happier to know that Room objects have
several short-cut methods for creating, previewing, and executing
messages. They include the following:
public boolean show( MOB source, Environmental target, int allCode, String allMessage );
public boolean show( MOB source, Environmental target, Environmental tool, int allCode, String allMessage );
public boolean show( MOB source, Environmental target, Environmental tool, int srcCode, int tarCode, int othCode, String allMessage );
public boolean show( MOB source, Environmental target, Environmental tool, int srcCode, String srcMessage, int tarCode, String tarMessage, int othCode, String othMessage );
public boolean show( MOB source, Environmental target, Environmental tool, int allCode, String srcMessage, String tarMessage, String othMessage );
public boolean showOthers( MOB source, Environmental target, int allCode, String allMessage );
public boolean showOthers( MOB source, Environmental target, Environmental tool, int allCode, String allMessage );
public boolean showSource( MOB source, Environmental target, int allCode, String allMessage );
public boolean showSource( MOB source, Environmental target, Environmental tool, int allCode, String allMessage );
public void showHappens(int allCode, String allMessage);
public void showHappens( int allCode, Environmental like, String allMessage );
The first methods (show) is very commonly used; it constructs
a message with the given source and target (no tool), and with the
given Code and text message applying to source, target, and others.
The showHappens() methods will do the same, but will
also construct a blank MOB object to act as the source, for those
instances where a source MOB is not readily available. The showOthers()
methods behave like the first, but do not allow the source MOB
to preview or execute the message, while the showSource()
methods ONLY allows the source MOB to preview and execute the message.
All four of those methods will construct a CMMsg object,
give the Message to the Room object for previewing ("okMessage"),
and then, if the Message is not canceled, will call the Room "send"
method for execution and return true. If the Message was canceled,
false will be returned.
The final subject we will discuss in the area of
Messages and Message handling regards another rare technique called
Message Trailer adding. Message Trailers are CMMsg objects which
have been added to another CMMsg instance using the
CMMsg.addTrailerMsg(CMMsg msg) method. The Message passed
to this method is constructed in the usual way. This
method may be properly called at any point during the Preview or
Execution stage of Message handling, by any Previewing or Executing
object. When it is performed is not important, because any Messages
added using this method are not Previewed or Executed until after
the Room object has completely finished sending the host Message
to all interested objects.
Constructing and adding messages which act as message
trailers can serve many purposes, but the most important of which
is that the trailer messages only happen IF the host message also
happens, and only happen AFTER the host message happens. This can
be useful for the timing of subsequent messages which are dependent
on others.
In most systems, it is typical for all of the data variables
which describe a particular object to be coded directly inside that
object. While this is also true in CoffeeMud, many important data
fields, along with the appropriate "getter" and "setter" methods,
are stored in separate special data-storage, or state
objects. These data-storage objects provide access to numerous
important properties for those objects. These storage/state objects
are routinely copied and then the copies are modified by other
objects which have a spacial relationship with them. For instance,
a MOB object may copy one or more of its state objects, and then
allow the local Room object, or his inventory Item objects, or
spell Effect objects to modify the copy, The copied state object
modifications are stacked on each other. Confused? Well, keep
reading!
Each instance of the several Environmental objects
(MOBs, Items, Exits, Rooms) have a particular storage/state object
called their Environmental Stats. This object implements the Common.interfaces.EnvStats
interface, and is typically an instance of the Common.DefaultEnvStats
class. Access to this state object is available through each core.interfaces.Environmental
objects EnvStats baseEnvStats() and EnvStats
envStats() method calls. Since Rooms, MOBS, Items, Exits,
and Areas are all Environmental objects, that means that they all
have baseEnvStats and envStats methods as well.
Field name |
Relevant objects |
Meaning |
level |
Item, MOB, Exit |
Experience level (see Archon's Guide)
|
ability |
Item, MOB |
Magical level (see Archon's Guide)
|
rejuv |
Item, MOB |
Rejuvenation rate (see Archon's Guide)
|
weight |
Item, MOB |
Weight of the object
|
height |
Armor, MOB |
Size of the object |
armor |
Item, MOB |
Protection level (see Archon's Guide)
|
damage |
Item, MOB |
Damaging ability |
speed |
Item, MOB |
Attack speed |
attackAdjustment |
Item, MOB |
Attack level |
replacementName |
Item, MOB, Exit |
New displayable name of the object
|
sensesMask |
Item, MOB, Exit, Room
|
Bit mask of relevant sensory abilities.
|
disposition |
Item, MOB, Exit, Room
|
Bit mask of relevant disposition state
|
Although most of these fields are better described in
the Archon's Guide, there are two whose nature may not be readily
apparent: the sensesMask and the disposition. These two integers
are bitmaps. The value of each bit is defined by equates in the
Common.interfaces.EnvStats interface. The equates which refer
to the bits for sensesMask all begin with "CAN_", while the equates
which refer to the bits for disposition all begin with "IS_".
Now, as mentioned previously, all Environmental
objects have two methods for accessing their EnvStats. One
is core.interfaces.Environmental.baseEnvStats()
and the other is core.interfaces.Environmental.envStats() .
The difference between these two methods is very significant.
The EnvStats state object returned by the "baseEnvStats" method
refers to the permanent, unmodified, "base" state of the Environmental
object. The "envStats" method, however, returns the modified, less
permanent, "current" state of the Environmental object. The EnvStats
object returned by the "envStats" method is always copied and derived
from the "baseEnvStats" values, after all relevant modifications
have been made to it. How the current state object goes from its
base values (baseEnvStats) to its current values (envStats) is our
next topic.
We must now introduce two other Environmental
interface methods significant to this topic. One is the recoverEnvStats()
method, while the other is the affectEnvStats(Environmental
affected, EnvStats affectableStats) method. The "recoverEnvStats"
method is also located on every Environmental (Item, MOB, Exit,
etc) object and is the method which turns the baseEnvStats()
values into their current envStats() values. This method
call works by copying the base values into the current values and
then allowing certain other objects to have an opportunity to affect
the copy. Only after all opportunities to modify the copied values
have been exhausted, does the recoverEnvStats() method
return. In essence, recoverEnvStats() allows the Environmental
objects baseEnvStats() to be updated, with that updated
state object made available through envStats() .
The way in which the current EnvStats state
object is modified by the recoverEnvStats() method call is by
making repeated internal calls to the affectEnvStats(Environmental
affected, EnvStats affectedStats) methods on other relevant
objects. These methods will then have the opportunity to change
the values in the current state object (affectedStats parameter)
however they wish. The relevant objects which may change the state
of an Environmental are as follows:
- MOBs
Room object being occupied, something being Ridden,
the MOBs Character Class object, the MOBs Race object, the Items
in the MOBs inventory, and finally the Ability objects which are
affecting the MOB (spell effects).
- Items, Exits, Areas
Ability objects which are affecting it (spell
effects).
- Rooms
Area object which this room is a part of, Ability
objects which are affecting it (spell effects), Items in the Room,
and MOBs in the Room.
Here is an example:
Gunker the Thief wears Full Plate Armor (Item),
and has the Shield spell cast on him. His base Armor rating is 100.
When he puts on the Plate Armor, the "recoverEnvStats" method is
called on Gunker's MOB object. That method in turn calls the
"affectEnvStats" method on the Plate Armor and the Shield spell
Effect. Both of those methods improve the Armor rating on Gunker's
MOB's current EnvStats by some number. Thus, Gunker becomes harder
to hit in combat. Also, when Gunker picked up the Plate armor,
the weight of the armor was added to Gunker's overall carried weight
by increasing the weight value in Gunker's EnvStats object.
I know, this is probably still confusing.
Confusing or not, however, we still have to consider two
other state objects, both of which are only available from the
MOB object. One of which is the CharStats object, and the other
of which is the CharState object.
The CharStats objects are most closely analogous
to the EnvStats objects. For instance, there are CharStats
baseCharStats() and CharStats charStats()
method calls from a MOB object, as well as a recoverCharStats()
method call. All of these work similarly to the ones described above
for EnvStats. The fields on a Common.interfaces.CharStats object
are somewhat more straight forward however. Most of the fields
of a CharStats object are referenced using the int
getStat(int) and setStat(int,int) methods on
a CharStats object. Both of these methods require, as their first
parameter, an integer code which corresponds to the specific stat
being set or read. These stat parameters are defined as equates
within the CharStats interface, and include:
STAT_STRENGTH,
STAT_INTELLIGENCE, STAT_DEXTERITY, STAT_CONSTITUTION,
STAT_CHARISMA, STAT_WISDOM, STAT_GENDER, STAT_SAVE_PARALYSIS,
STAT_SAVE_FIRE, STAT_SAVE_COLD, STAT_SAVE_WATER, STAT_SAVE_GAS,
STAT_SAVE_MIND, STAT_SAVE_GENERAL, STAT_SAVE_JUSTICE, STAT_SAVE_ACID,
STAT_SAVE_ELECTRIC, STAT_SAVE_POISON, STAT_SAVE_UNDEAD, STAT_SAVE_MAGIC,
STAT_SAVE_DISEASE, STAT_SAVE_TRAPS, STAT_MAX_STRENGTH_ADJ, STAT_MAX_INTELLIGENCE_ADJ,
STAT_MAX_DEXTERITY_ADJ, STAT_MAX_CONSTITUTION_ADJ, STAT_MAX_CHARISMA_ADJ,
STAT_MAX_WISDOM_ADJ, STAT_AGE, STAT_SAVE_DETECTION, STAT_SAVE_OVERLOOKING.
In addition to these equates defined and read
through the getStat() and setStat() methods, there
is also the Race object available through getMyRace()
and setMyRace() methods, as well as Character Class
and Character Class level methods. See the Common.interfaces.CharStats
java file for more information on those methods and how they work.
Like the EnvStats above, those objects listed as
able to modify the EnvStats current state object are the same
objects which are able to modify the CharStats state objects.
Rereading the section on EnvStats will make clear how the CharStats
objects are modified in the same analogous manner, using repeated
calls toaffectCharStats(MOB affected, CharStats
affectedStats) methods on related objects.
The last
state object to consider is the Common.interfaces.CharState objects
on MOBs. The CharState object represents those fields which are
constantly in flux: Hit Points, Mana, Movement, Hunger, Fatigue,
and Thirst.
Unlike EnvStats and CharStats, there are three
CharState objects to consider for MOBs: the base CharState object
(available throughCharState baseState() method,
the adjusted base CharState (or max state) object available through
the CharState maxState() method and modified
by recoverMaxState(MOB affected, CharState affectedState)
methods on related objects, and lastly the current CharState object
available through the CharState curState()
and refreshed or reset to maximums using the MOBs resetToMaxState()
method.
The relationship between the above objects is as follows:
The base CharState object represents the maximum values for the
state variables BEFORE modification by magical armor or spells.
The adjusted base CharState object (Max State) represents the maximum
values for the state variables AFTER modification by magical armor
or spells. The current CharState object (curState) represents
the current hit points, mana points, etc available to the MOB.
In the case of the CharState objects, adjustment
by relevant objects is initiated by calling the MOBs recoverMaxState()
method. This method allows the same objects who modify the EnvStats
and CharStats above to modify the maximum CharState values as
well.
Once again, to understand one of them fully is to understand
them all.
Our last Core Topic will cover the ability of the
mobs, items, exits, abilities, spell effects, behaviors, and other
objects to perform tasks on a regular, timed, basis. The tasks
to be performed are always located within an method called
boolean tick(Tickable ticking, int tickID) . All
Environmental objects define this method, and Behavior objects do
as well.
These methods are called on a regular, timed
basis whenever the object instance in question has been properly
set up to do so, and at a defined frequency and interval. The
"ticking" parameter is usually a reference to the object itself, or
to the host object in the case of Behaviors. The "tickID" parameter
describes what sort of regular timed event is occurring. These
events are defined as equates in the core.interfaces.Tickable
interface, and include IDs such as TICKID_MOB, TICKID_AREA,
TICKID_EXIT_REOPEN and others. See the java file of that
interface for more defined tickID values.
Before we get into the methods by which an object instance
are properly set up for regular calls to its tick()
method, it may be worthwhile to discuss which regular ticks are
setup by the system by default. These tick events cover the most
commonly used objects under the most common circumstances, and so
may be just the events you already needed! They include:
- MOBs
All MOBs have their tick() method called
once per core.interfaces.MudHost.TIME_TICK (4 seconds), with the
"tickID" defined by Host.MOB_TICK. MOBs will, in turn, call the
tick() methods on their own Behaviors, and
Ability objects affecting them. If any of these dependent objects
return "false" from their own tick methods, then the object will
cease to receive any further tick method calls.
- Exits, Items, Rooms
Whenever a Behavior is added to any of these
objects, they will begin to have their tick methods called once
per MudHost.TIME_TICK (4 seconds), with the tickID defined by Tickable.TICKID_ITEM_BEHAVIOR,
TICKID_EXIT_BEHAVIOR, or TICKID_ROOM_BEHAVIOR. Deletion of the
last behavior from the host object will stop this tick event from
occurring again.
- Areas
All Area objects have their tick methods
called once per core.interfaces.MudHost.TIME_TICK, with the tickID
defined by Tickable.TICKID_AREA.
- Ability
Whenever an Ability object is added as an Effect (using
the addEffect() Environmental method) to a non-MOB
object by using the proper Ability invoke procedure (see below),
then the Ability object itself will gain it's own regular calls
to its tick method. The tickID for this call is also Tickable.TICKID_MOB,
so as to make consistant the tickID for all spell and similar effects.
To sum up, MOBs have regular tick calls which they use
to perform their own periodic tasks, as well as to allow their Behavior
and spell effects to perform tasks. The other objects have
circumstantial ticks in certain instances.
Now, to add a new periodic call to the "tick" method on
an Environmental object, one needs only to make a method call like
this:
CMClass.threads().startTickDown(theEnvObject,Tickable.MY_TICK_ID,TIME_TICK,NUM_TICKS);
The second parameter is the tickID which will be used
when the tick() method on the "theEnvObject" object
is called. The third parameter is the time interval, in milliseconds,
between each event. The fourth parameter is the number of time intervals
between each call to the tick() method. The total
time between each call to the tick() method, therefore,
will be TIME_TICK * NUM_TICKS.
Now, the above version of startTickDown()
can be used to create timed events of any duration. However, all
objects in the base distribution of CoffeeMud operate on a single
default time interval of 4000 milliseconds as defined by Tickable.TIME_TICK.
For this reason, a different version of the startTickDown()
method is called which does not include the TIME_TICK parameter,
utilizing the standard 4 second delay instead:
CMClass.ThreadEngine().startTickDown(theEnvObject,Host.MY_TICK_ID,NUM_TICKS);
Stopping any of these tick calls can be done by simply
returning "false" from the tick method itself, or manually using
the following:
CMClass.ThreadEngine().deleteTick(theEnvObject,Host.MY_TICK_ID);
You may also stop tick calls to an object by
using the Environmental objects destroy() method.
The CoffeeMud engine contains numerous Java classes whose
purpose is to perform much of the underlying game functionality.
Some of these Java classes are Core classes, some are Library
classes, and some are Common classes.
Core classes are those classes found in the
com.planet_ink.coffee_mud.core package. Like the interfaces, they
may not be extended or overwritten without risking problems. The
core classes, and their general purpose is:
Core Class Name |
Purpose |
B64Encoder |
Encode and decode text<->binary
using Base64 |
CMath |
Converting strings to numbers, performing
bit-wise and other arithmetic operations |
CMClass |
Main ClassLoader -- get all your objects
from methods here! |
CMFile |
FileSystem manager, get all your file data from this
class |
CMLib |
The non-core library reference object.
* See below for more information.
|
CMParms |
Methods for parsing strings and determining
parameter values in many different ways. |
CMProps |
Properties manager, for reading INI
file values from coffeemud.ini and other places.
|
CMSecurity |
The security manager, for evaluating
player and system security flags. |
CMStrings |
Methods to manipulate, pad, and filter
strings. |
Directions |
Methods to handle the different compass
directions. |
DVector |
A multi-dimensional version of java.util.Vector
|
Log |
The file and console logging manager.
|
Resources |
The object-resource manager, usually
with lots of StringBuffers keyed by String names.
|
Scripts |
The human-language readable strings
loader. |
You should check out the javadocs for those classes for
more information on the core classes.
* CMLib also refers to some of the sub-core classes, such
as the Database access objects, and the Threading engine. While
they are considered core, they are accessed through CMLib as if
they were non-core libraries. Most core classes can also be accessed
through CMLib methods, making it a one-stop shop.
Now, non-core libraries, or Libraries proper, are located
in the com.planet_ink.coffee_mud.Libraries package, and each one
implements a unique interface from the com.planet_ink.coffee_mud.Libraries.interfaces
package. This is unique, that each class in the Libraries package
implements a unique interface all its own, but that is not the only
unique thing about this package. Libraries are also singletons --
there is never more than 1 instance of each Library. Moreover, these
singletons are all accessed from a single accessor class, CMLib,
which we mentioned earlier.
Here is a map of how the classes, interfaces, and CMLib
methods are all mapped together:
Library class name
...Libraries.* |
Library interface name
...Libraries.interfaces.* |
CMLib method name
...core.CMLib |
Purpose of the Library
|
BeanCounter |
MoneyLibrary |
beanCounter()
|
Handle money and currency.
|
CharCreation |
CharCreationLibrary
|
login()
|
Login and create new players.
|
Clans |
ClanManager |
clans()
|
Handle all clans. |
CMAble |
AbilityMapper |
ableMapper()
|
Maps CharClasses to Skills/Abilities.
|
CMChannels |
ChannelsLibrary |
channels()
|
Handles public channels.
|
CMColor |
ColorLibrary |
color()
|
ANSI and color code conversions.
|
CMEncoder |
TextEncoders |
encoder()
|
Compression library.
|
CMJournals |
JournalsLibrary |
journals()
|
Handles public journal commands.
|
CMLister |
ListingLibrary |
lister()
|
Handles listing tables nicely.
|
CMMap |
WorldMap |
map() |
Find areas and rooms.
|
CoffeeFilter |
TelnetFilter |
coffeeFilter()
|
Filters/transforms text going to and
from a player. |
CoffeeLevels |
ExpLevelLibrary |
leveler()
|
Leveling and Experience gaining functionality.
|
CoffeeMaker |
CMObjectBuilder |
coffeeMaker()
|
Generic Mob/Item and CoffeeXML generators.
|
CoffeeShops |
ShoppingLibrary |
coffeeShops()
|
Handles manipulation of shop inventories.
|
CoffeeTables |
StatisticsLibrary |
coffeeTables()
|
Maintains player usage stats.
|
CoffeeTime |
TimeManager |
time() |
Real-life data/time display.
|
CoffeeUtensils |
CMMiscUtils |
utensils()
|
Misc stuff-- law, traps, titles, resets.
|
CommonMsgs |
CommonCommands |
commands()
|
Methods for getting, talking, common-stuff
|
Dice |
DiceLibrary |
dice()
|
Random number generator.
|
EnglishParser |
EnglishParsing |
english()
|
Player command line parsing helpers.
|
ColumbiaUniv |
ExpertiseLibrary |
expertise() |
Maintains expertise skill flags |
Factions |
FactionManager |
factions()
|
Handles the factions and faction system.
|
MUDFight |
CombatLibrary |
combat()
|
Combat and Death routines.
|
MUDHelp |
HelpLibrary |
help()
|
Handling help-file entries.
|
MUDTracker |
TrackingLibrary |
tracking()
|
Methods for NPC movement, and for tracking.
|
MUDZapper |
MaskingLibrary |
masking()
|
Zapper-mask parsing and evaluation.
|
Polls |
PollManager |
polls()
|
Handle the public polls.
|
Quests |
QuestManager |
quests()
|
Quest Manager system.
|
RawCMaterials |
MaterialLibrary |
materials() |
Manipulate/Create raw resource objects |
Sense |
CMFlagLibrary |
flags()
|
Sensory and Disposition bitmap/flag
handling. |
Sessions |
SessionsList |
sessions()
|
Container for player connection objects.
|
SlaveryParser |
SlaveryLibrary |
slavery()
|
Geas and Slavery order parsing and
execution. |
SMTPclient |
SMTPLibrary |
smtp() |
E-Mail sending routines.
|
Socials |
SocialsList |
socials()
|
Socials container and parser.
|
StdLibrary |
NONE |
NONE |
SuperClass of other libraries.
|
TimsLibrary |
ItemBuilderLibrary |
itemBuilder()
|
Methods for normalized item evaluation.
|
XMLManager |
XMLLibrary |
xml() |
General XML parsing.
|
Libraries are lastly unique in that it is almost
useless to write new ones, unless you are doing the most serious
additions and enhancements to your system. However, they are designed
especially so they they can be extended and overridden. They have
their own entry in the coffeemud.ini file called LIBRARY. By writing
classes that extend the base CoffeeMud library classes, and overriding
their methods, you can make changes to the most basic CoffeeMud
algorithms, even at run-time! Libraries also have an entry in the
coffeemud.ini file, LIBRARY, so that you can specify your custom
extended versions of them after the %DEFAULT% string, thus allowing
your changes to be loaded at boot-time.
The last set of classes to discuss under this topic are
neither Core class or Libraries, but form parts of the cores of
other classes, including many of the Libraries. These are the Common
classes. They are part of the com.planet_ink.coffee_mud.Common
package. Like the Libraries, they each implement their own unique
interface from the com.planet_ink.coffee_mud.Common.interfaces
package. However, unlike the Libraries, numerous instances of each
class will exist in your mud, and they are created from the core
CMClass loader, much like MOBs, Items, Rooms, and so forth. The
javadocs are also a good place to learn about these classes, but
here is a brief list of them to wrap up our last Core Topic.
Common Class |
Description |
DefaultArrestWarrant
|
An object created every time a law
is broken. |
DefaultCharState |
Contains a mobs hit points, mana, movement,
fatigue, hunger, and thirst. |
DefaultCharStats |
Contains a mobs class, race, saving
throws, and basic stat scores. |
DefaultClan |
Represents a single Clan.
|
DefaultClimate |
A single climatary system for a given
area. |
DefaultCMIntegerGrouper
|
An object for maintaining a players
room visitation memory. |
DefaultCoffeeShop |
Represents a single shop store inventory.
|
DefaultCoffeeTableRow
|
Represents a days worth of game player
usage statistics. |
DefaultEnvStats |
Contains an objects attack bonus, armor
bonus, weight, height, rejuv rate. |
DefaultFaction |
Represents a single faction.
|
DefaultLawSet |
Represents a single set of laws and
legal policies. |
DefaultMessage |
A CoffeeMud event message (CMMsg).
|
DefaultPlayerStats |
Represents player-specific fields like
prompts, friends, alias, etc. |
DefaultPoll |
A single poll object.
|
DefaultQuest |
Container for a single timed quest.
|
DefaultRoomnumberSet
|
Container for a players area visitation
memory. |
DefaultSession |
Connection object for handling a players
telnet session with the mud |
DefaultSocial |
Object for a single social command.
|
DefaultTimeClock |
Object representing a single calendar/time
system that spans areas, possibly the whole world.
|
In the uncoming sections, you will learn about the layout and design
of each of the several major CoffeeMud class types. Sometimes, however,
you may want to expand or extend the classes to include new or extraneous
data. If so, this last core topic is for you.
All of the classes which implement the Environmental interface (including
Abilities, Exits, Areas, Rooms, MOBs, and Items) or the PlayerStats interface
(Player MOBs) or several others, will include the following methods for accessing
and modifying certain internal data fields:
public String[] getStatCodes();
public int getSaveStatIndex();
public String getStat(String code);
public void setStat(String code, String val);
The getStatCodes() method returns a string array of the
"names" of the internal data fields which this class makes available to the
outside world. These names can then be used in the getStat(String)
method to retreive the string-rendered values of those data fields, and the
setStat(String,String) method can be used to change them.
This is a very versitile way of reading and writing the particular
aspects of a data object, especially when you don't know for sure what kind
of object it is (or you don't care).
However, the reason this might interest you is because of the method
we havn't mentioned yet: getSaveStatIndex(). The purpose of
this method is quite simple: It returns the number of stat code names (from
getStatCodes() ) that are already handled by the existing
database code. This means that, for a stock CoffeeMud system, this
method will return the same number as getStatCodes().length would.
However, should you decide to add to the stat name strings defined
by getStatCodes() and then extend getStat and setStat methods to manage your
own internal variables, you have only to make sure your new name is at the
end, and that getSaveStatIndex() stops short of that number, to make sure
that CoffeeMud will go ahead and save your new data to the database (and
restore it, of course). Exporting to cmare files and other features
are also included this way.
Here is an example:
public class MySpecialGenMob extends GenMOB
{
public String ID(){return "MySpecialGenMob";}
private String myvariable="";
public String[] getStatCodes()
{
List L=java.util.Arrays.asList(super.getStatCodes());
L.add("MYVALUE");
return (String[])L.toArray();
}
public String getStat(String code)
{
if(code.equalsIgnoreCase("MYVALUE"))
return
myvariable;
return super.getStat(code);
}
public void setStat(String code, String value)
{
if(code.equalsIgnoreCase("MYVALUE"))
myvariable=value;
else
super.setStat(code,value);
}
public int getSaveStatIndex(){return super.getStatCodes().length;}
You'll notice above that the getStatCodes() method is extended to add a new
stat code name "MYVALUE" which will appear at the end of the string list.
The getStat and setStat methods are adjusted accordingly to support
the new code. Lastly, the getSaveStatIndex method is overridden to
return the length of the base classes codes instead of mine, that way the
getSaveStatIndex for MySpecialGenMob will return one less than the
getSaveStatIndex for GenMOB, the super class.
And just by doing those simple things, we now have a new variable
which the CoffeeMud engine will ensure is written to the database. However,
this capability is limited to the following types of objects: generic mobs,
generic items, playerstats, or any exits, rooms, or area classes. Since
players are technically implemented as StdMOB objects, you will need to modify
the appropriate methods in the DefaultPlayerStats object to achieve the same
effect for players.
Commands are exactly what they sound like: LOOK, QUIT,
KILL, GET, and all the other things you type into the mud are handled
by CoffeeMud Commands.
A Custom Command may or may not belong to any particular
package, though it is important that the ID() of the
Command be unique in the system. A custom Command imports the same
packages mentioned in the first section of this document under Complete
Default Import List as well as com.planet_ink.coffee_mud.Commands.StdCommand.
public class DoNothing extends com.planet_ink.coffee_mud.Commands.StdCommand { public DoNothing(){}
private String[] access={ "DONOTHING" }; public String[] getAccessWords(){ return access; }
All Commands should extend StdCommand for conformity's
sake, though it is not required so long as your class implements
the com.planet_ink.coffee_mud.core.interfaces.Command interface.
In our example above, we have an empty constructor, but we do define
some access words.
Access words are what you think they are: the
words which, when typed, allow the users to activate the command.
We define a string array containing one such access word in this
case, and then define our Command interface method getAccessWords()
to return that array. Our String array may contain as many strings
as you would need to provide sufficient words to activate this
command.
public double actionsCost(){ return 1.0; } public double combatActionsCost(){ return 1.0; }
public boolean canBeOrdered(){ return true; }
The next two methods, actionsCost() and
combatActionsCost() , designate how LONG it takes to
execute this command. A value of 0 means that the command always
happens instantly. A value greater than 0 will always take that
many free actions to complete. A standard player may perform 1
action in a given 4 second period, or 2 actions during combat in
the default combat system -- they retain their 1 action per tick
in other combat systems). Action costs may be partial as well, costing
0.5 (1/2 action) or other values.
The canBeOrdered() method designates whether
this command represents an action which a creature or player might
reasonably be ordered to do by another player. Archons and those
with the "ORDER" security code are exempt from this flag.
public boolean securityCheck( MOB mob ) { return CMSecurity.isAllowed( mob, mob.location(), "IMMORT"); }
And speaking of security, this method returns whether
or not the given mob may even have access to this command. If the
securityCheck() method returns false, the command
and its access words will behave as if they do not even exist, returning
"Huh?" should a player attempt to use it. Returning true, however,
means only that the execute method below may be accessed. Any further
security would have to be implemented there.
In the above example, we call the isAllowed()
method in CMSecurity with the mob reference, the room in which the
mob is located, and the security code "IMMORT". This asks the Security
module whether this mob, at this location, is authorized to perform
functions designated by the "IMMORT" security code.
public boolean preExecute(MOB mob, Vector commands, int secondsElapsed, double actionsRemaining) throws java.io.IOException; { if( secondsElapsed == 0 ) { mob.tell( "You are preparing to do nothing." ); }
if( secondsElapsed == 3 ) { mob.tell( "You almost ready to do nothing." ); } }
The preExecute() method is very rarely implemented,
but it is important to mention in light of the actionsCost()
and combatActionsCost() values above. The purpose of
the method is to give the player or the room status messages when
the player does not yet have enough actions to execute the command.
The method will be called immediately if a player does not have
enough actions to execute the command, and will present a secondsElapsed
value of 0. It will then be called again every second or so with
updated values until the player has enough actions to proceed.
public boolean execute( MOB mob, Vector commands ) throws java.io.IOException { String parameters = CMParms.combine( commands, 1 ); if( parameters.length() == 0 ) { mob.tell( "Nothing done." ); } else { mob.tell( "Nothing, not even '" + parameters + "', done." ); } return false;
}
Our last method is where the command actually does its
work. The mob given would be the MOB object trying to execute this
command, while commands is a Vector of parameters.
The parameter commands is never null, and by convention
is a Vector of strings starting with the access word used to execute
the command. For instance, if the user entered:
donothing never "and always" never
The commands Vector would be size 4, and contain "donothing",
"never", "and always", and "never" respectively. In the case of
this command, we use one of the String utilities to recombine
the last 3 parameters back into one string "never and always never",
and then issue a message to the mob depending upon whether there
were any parameters at all. Since this command requires a command
word to access it, it is reasonable to assume that the 0th element
in the commands vector is the word "donothing", which means we
can safely ignore it.
MOBs, or "Moveable OBjects", are the creatures and characters
which the players fight. In CoffeeMud, they are among the simpler
objects to code. This is not because they are uncomplex. In fact,
they are MOST complex. However, this complexity comes due to the
myriad of Items, Behaviors, Properties, and Abilities that are added
to them. Short of these numerous additions, a MOB by himself is
rather simple!
This simplicity is important however, and should be carefully
considered before you run off to create new MOBs. If you are
creating a new MOB because you want a creature to have some new
kind of ability, then are you sure it is not a new Ability you
want to write? If the new MOBs behavior is complex and unique,
are you sure it's not a new Behavior you wish to code? Otherwise,
the best reasons to be coding MOBs are actually three: because
you have a particular kind of monster that is used prolifically
in your world, and you want to save memory by coding him as a special
mob that extends StdMOB, or because you want to code special player
capabilities by creating your own class that both extends StdMOB
and has an ID() of "StdMOB", or because you want
to add special NPC monster capabilities by creating your own class
that extends GenMob and has an ID() of "GenMob".
So, if you are sure this is what you want to do, carry
on! The directory for your custom coded MOB objects should be specified
using the "MOBS" entry in the coffeemud.ini file. See the section
above on Rebuilding CoffeeMud for more information on this feature.
A Custom MOB may or may not belong to any particular package,
though it is important that the ID() of the MOB be unique in the
system. A custom MOB imports the same packages mentioned
in the first section of this document under Complete Default Import
List as well as (in this case) com.planet_ink.coffee_mud.MOBS.StdMOB
because our sample mob extends it.
A MOB class must extend either StdMOB or GenMob,
StdShopKeeper or GenShopKeeper, StdRideable or GenRideable
depending on the basic capabilities, and customizability you would
like. Although Generic objects are more customizable at run-time,
they also take a long time for the system to load and build, and
take up a lot of database disk space, and more memory. For this
reason, using Standard instead of Generic wherever possible is always
good. Another reason for extending StdMOB is because all players
in the game use the "StdMOB" class as a basis, which means that
special player fields and capabilities can be coded by both extending
the StdMOB class, and also giving your custom class an ID()
of "StdMOB". If you do this, however, make sure your class is loaded
after the %DEFAULT% list in your coffeemud.ini file.
As was stated, each unique MOB must also have a custom
ID() method as shown below. Notice that the
ID() is the same as the name of the class. This
is no accident -- this is required!
public class MyNewMOB extends com.planet_ink.coffee_mud.MOBS.StdMOB { public String ID(){ return "MyNewMOB";}
All of your customizing will be done inside the
constructor: name, displayText, description, etc, etc.
public MyNewMOB() { super();
setName( "a new mob" ); setDescription( "It`s furry with 2 legs" ); setDisplayText( "My new mob is standing here." );
Factions.setAlignment( this, Faction.ALIGN_NEUTRAL ); setMoney( 0 ); setWimpHitPoint( 2 ); baseEnvStats().setDamage( 4 );
baseEnvStats().setAbility( 0 ); baseEnvStats().setLevel( 1 ); baseEnvStats().setArmor( 30 ); baseEnvStats().setSpeed( 1.0 ); baseEnvStats().setAttackAdjustment( 30 ); baseEnvStats().setWeight( 85 ); baseEnvStats().setSensesMask( EnvStats.CAN_SEE_DARK|EnvStats.CAN_SEE_INFRARED ); baseEnvStats().setDisposition( EnvStats.IS_FLYING ); baseCharStats().setCurrentClass( CMClass.getCharClass( "Fighter" ) );
baseCharStats().setMyRace( CMClass.getRace( "Dog" ) ); baseCharStats().getMyRace().startRacing( this, false ); baseCharStats().setStat( CharStats.STAT_GENDER, (int)'F' ); baseCharStats().setStat( CharStats.STAT_STRENGTH, 18 ); baseCharStats().setStat( CharStats.STAT_INTELLIGENCE, 14 ); baseCharStats().setStat( CharStats.STAT_WISDOM, 13 ); baseCharStats().setStat( CharStats.STAT_DEXTERITY, 15 ); baseCharStats().setStat( CharStats.STAT_CONSTITUTION, 12 ); baseCharStats().setStat( CharStats.STAT_CHARISMA, 13 ); baseCharStats().setStat( CharStats.STAT_SAVE_COLD, 50 );
baseState.setHitPoints( CMLib.dice().roll( baseEnvStats().level(), 20, 20 ) ); baseState.setMana( CMLib.dice().roll( baseEnvStats().level(), 50, 100 ) );
recoverMaxState(); resetToMaxState(); recoverEnvStats(); recoverCharStats(); }
You can see here that the basic stats have been filled
out, from level to attack speed, alignment and weight. For numeric
values, higher is always better, except for Armor, which is always
best low, and comes down from 100. You'll notice above that two
commands are required to set the Race of the creature. Also, you
should realize that the numerous saving throws, and senses as well
as dispositions (sneaking, hiding) are not represented above, but
can easily be added using the format shown.
It is very important to note the last four commands. These
commands "reset" the MOB, and "implement" the scores which are
given in the several areas. recoverEnvStats() , for
instance, must be called whenever a change is made to the baseEnvStats()
object. Ditto for "CharStats" and "MaxState".
Now, suppose we wanted to add an Effect or Behavior or
Ability to your MOB. The proper place for such a statement
would be in the same above constructor, among the other commands.
Preferably before the several "recover" commands, but after the
several stat definitions. Of course, all of this is unnecessary
for a new GenMOB object.
addNonUninvokableEffect( CMClass.getAbility( "Fighter_Berzerk" ) );
Ability A = CMClass.getAbility( "Prop_Resistance" ); if( A != null ) { A.setMiscText( "Fire 200%" ); addNonUninvokableEffect( A ); }
addAbility( CMClass.getAbility( "Poison" ) );
addBehavior( CMClass.getBehavior( "MudChat" ) ); addBehavior( CMClass.getBehavior( "Mobile" ) ); addBehavior( CMClass.getBehavior( "CombatAbilities" ) );
The commands above will make the MOB permanently Berzerk,
gives it the ability to Poison folks while in combat, allows the
MOB to Chat with other players, and to walk around in its area.
The last behavior gives the MOB the wisdom to use its Poison ability
while in combat.
If your MOB extends StdShopKeeper, you will need
to add your inventory manually through the getShop() object
access method as shown below. The getShop() method
returns an instance of the com.planet_ink.coffee_mud.Common.interfaces.CoffeeShop
interface, which stores inventory for the shopkeepers. In creating
the shopkeepers, you will also need to specify the type of
ShopKeeper.
setWhatIsSold( ShopKeeper.ONLYBASEINVENTORY );
Weapon sword = (Weapon)CMClass.getWeapon( "Longsword" ); getShop().addStoreInventory( sword, 35, -1, this ); Armor mail = (Armor)CMClass.getArmor( "FullPlate" ); getShop().addStoreInventory( mail, 35, -1, this ); Item waterskin = CMClass.getItem( "Waterskin" ); getShop().addStoreInventory( waterskin, 35, -1, this );
You'll recall from the Archon's Guide that there are many
different types of ShopKeepers, including trainers, pet sellers,
weaponsmiths, and others.
StdRideable MOBs will require a few other settings
as well!
setRideBasis( Rideable.RIDEABLE_LAND ); setMobCapacity( 2 );
The last thing is to give the MOB equipment, armor, and
weapons. The following commands will do the trick!
Weapon sword = (Weapon)CMClass.getWeapon( "Longsword" ); addInventory( sword ); sword.wearIfPossible( this );
Armor mail = (Armor)CMClass.getArmor( "FullPlate" ); addInventory( mail ); mail.wearIfPossible( this );
Item sack = (Item)CMClass.getItem( "StdContainer" ); addInventory( sack );
Item waterskin = (Item)CMClass.getItem( "Waterskin" ); addInventory( waterskin ); waterskin.setContainer( sack );
And that's all there is to creating a new standard MOB.
Easy, huh? Well, obviously, the real complexity of MOBs comes when
the Behaviors and Abilities are programmed, but that is not covered
here, of course.
There is also the advanced topic of extending the capabilities
of the existing MOB classes. As mentioned previously, this consists
in creating your own java classes with the same class name and
ID() string methods as the base CoffeeMud MOB
classes, and the adding the reference to your custom class to the
coffeemud.ini file's MOBS enter after the %DEFAULT% entry. Now,
adding or extending the capabilities of these classes typically
means both adding your own methods, and extending the importing
existing methods in those classes. The most important of those existing
methods are discussed above in the Core Topics, namely okMessage,
executeMsg, and tick. However, there are many other methods which
might be extended to the end of altering or enhancing basic aspects
of the mud. Those are numerated both in the classes you are extending,
and in com.planet_ink.coffee_mud.MOBS.interfaces.MOB.java. Consult
the CoffeeMud java docs for more information.
The following instructions are supplemental, and unnecessary.
Once you have created your new MOB, modified your INI file, and
rebooted your CoffeeMud server, you need only use the CREATE and
other Archon commands to make use of him. If, for some reason, you
want to know HOW these commands do their work, however, here it
is.
To bring a MOB into existence, a MOB must have somewhere
to exist! Presumably, this is some room on your map. Rooms on the
map are classes which implement the interface com.planet_ink.coffee_mud.Locales.interfaces.Room.
If a MOB is to have a permanent existence, it must also have a starting
room, or a place to rejuvenate into when necessary. If a MOB does
not have a starting room, then its death, when that death comes,
will be forever.
MOB mob = CMClass.getMOB( "MyNewMOB" ); Room room = CMLib.map().getRoom( "Midgaard#3504" );
mob.setStartRoom( room ); // this mob will rejuvenate into this room. mob.baseEnvStats().setRejuv( 500 ); // 30 minutes rejuvenation time mob.recoverEnvStats(); // remember this command?!
mob.bringToLife( room, true ); // tadah!
And THAT's all there is to bring a standard mob to life.
Now, generic items require an additional step:
Item item = CMClass.getItem( "GenItem" ); Room room = CMLib.map().getRoom( "Midgaard#3504" );
item.text(); item.recoverEnvStats(); room.addItem( item ); room.recoverRoomStats();
The call to the text() method and the seemingly
redundant call to Item.recoverEnvStats() (which we
know is already in the item constructor), ensures that some of the
internal structures of the Generic MOB are properly set. Of course,
you may want to save this room to the database to make the situation
permanent, but all of this is usually done from inside CoffeeMud
using the CREATE, MODIFY, and DESTROY commands anyway. Speaking
of destroy, destroying a mob for good is even easier than creating
one:
Room room = CMLib.map().getRoom( "Midgaard#3504" ); for( int i = 0; i < room.numInhabitants(); i++ ) { MOB mob = room.fetchInhabitant( i ); mob.destroy(); }
A Custom Item may or may not belong to any particular
package, though it is important that the ID() of the
Item be unique in the system. A custom Item imports the same
packages mentioned in the first section of this document under
Complete Default Import List as well as (in this case)
com.planet_ink.coffee_mud.Items.Weapons.StdWeapon because our first
sample item extends it.
An Item class must extend either StdItem or
GenItem, StdWeapon or GenWeapon, StdRideable or GenRideable,
StdArmor or GenArmor depending on the basic capabilities,
and customizability you would like. Although Generic objects are
more customizable at run-time, they also take a long time for the
system to load and build, and take up a lot of database disk space.
For this reason, using Standard instead of Generic wherever possible
is always good. There are generally two good reasons to be coding
your own Items: because you have a particular kind of item that
is used prolifically in your world, and you want to save memory
by coding it as a special item that extends StdItem or StdWand,
or because you want to code special item capabilities by creating
your own class that both extends one of the base item classes and
has the same ID() string of that class. The
directory for your custom coded Item objects should be specified
using the "ITEMS", "WEAPONS", "ARMOR", "CLANITEMS", or "MISCMAGIC"
entries in the coffeemud.ini file. See the section above on
Rebuilding CoffeeMud for more information on this feature.
Each Item must also have a custom ID() method
as shown below. Notice that the ID() is the same as
the name of the class. This is no accident -- this is required!
public class MyNewSword extends com.planet_ink.coffee_mud.Items.Weapons.StdWeapon { public String ID(){ return "MyNewSword"; }
public MyNewSword() { super(); } }
All of your customizing will be done inside the constructor:
name, displayText, description, etc, etc.
public MyNewSword() { super();
setName( "a super sword" ); setDescription( "A long super duper sword!" ); setDisplayText( "Someone left their super sword here." ); setSecretIdentity( "" ); setMaterial( RawMaterial.RESOURCE_STEEL ); setWeaponType( Weapon.TYPE_SLASHING ); setWeaponClassification( Weapon.CLASS_SWORD );
setBaseValue( 500 ); baseEnvStats().setDisposition( EnvStats.IS_GLOWING ); baseEnvStats.setWeight( 25 ); baseEnvStats.setAttackAdjustment( 10 ); baseEnvStats.setDamage( 15 ); recoverEnvStats(); }
What is shown above is entirely sufficient for the creation
of a normal StdWeapon . The material, weight, attack and
damage describe it completely. You'll even notice that by setting
a disposition flag, we have made the sword glow! Now, what if we
wanted a missile weapon?
public MyNewBow() { super();
setName( "a super bow" ); setDescription( "A long super duper bow!" ); setDisplayText( "Someone left their super bow here." ); setSecretIdentity( "" );
setMaterial( RawMaterial.RESOURCE_OAK ); setBaseValue( 5000 ); baseEnvStats.setWeight( 15 ); baseEnvStats.setAttackAdjustment( 20 ); baseEnvStats.setDamage( 5 ); setWeaponType( Weapon.TYPE_PIERCING ); setWeaponClassification( Weapon.CLASS_RANGED ); setRanges( 1, 5 ); setAmmunitionType( "arrows" ); recoverEnvStats(); }
You'll notice we added two new methods, setRanges() ,
and setAmmunitionType() . With the former, we specify
that this is a ranged-only weapon, usable from range 1 (0=melee)
to range 5. The ammunition type specifies that it uses arrows. Other
classes, however, have different requirements altogether. For instance,
if class extends com.planet_ink.coffee_mud.Items.Armor.StdArmor:
public MyNewArmor() { super();
setName( "a super bracer" ); setDescription( "A super duper bracer" ); setDisplayText( "Someone left their super bracer here." ); setSecretIdentity( "" );
setMaterial( RawMaterial.RESOURCE_STEEL ); setBaseValue( 100 ); baseEnvStats.setWeight( 5 ); baseEnvStats.setArmor( 5 ); setRawProperLocationBitmap( Item.WORN_LEFT_WRIST|Item.WORN_RIGHT_WRIST ); setRawLogicalAnd( false ); recoverEnvStats(); }
In this case, we made a bracer wearable on both left and
right wrists. If it were something that could only be worn on both
wrists at the same time (like handcuffs), then the RawLogicalAnd
value would have been true. Now, a class extending com.planet_ink.coffee_mud.Items.Basic.StdContainer:
public MyNewBag() { super();
setName( "a super bag" ); setDescription( "A super duper bag" ); setDisplayText( "Someone left their super bag here." ); setSecretIdentity( "" );
setBaseValue( 50 ); setLidsNLocks( false, true, false, false ); setKeyName( "" ); setMaterial( RawMaterial.RESOURCE_LEATHER ); baseEnvStats.setWeight( 1 ); setCapacity( 100 ); recoverEnvStats(); }
When setting the capacity of a container, remember
that it must also be able to hold its own weight! Also, note the
lids and locks flags have made this container lidless and lockless
and always open. Of course, without a lock, setting a key would
be silly! Now, a class extending com.planet_ink.coffee_mud.Items.Basic.StdDrink
will create a drinkable container:
public MyNewCup() { super();
setName( "a super cup" ); setDescription( "A super duper cup" ); setDisplayText( "Someone left their super cup here." ); setSecretIdentity( "" );
setMaterial( RawMaterial.RESOURCE_LEATHER ); setBaseValue( 5 ); setLiquidHeld( 2000 ); setLiquidRemaining( 2000 ); setThirstQuenched( 500 ); setLiquidType( RawMaterial.RESOURCE_MILK ); baseEnvStats.setWeight( 1 ); setCapacity( 0 ); recoverEnvStats(); }
The StdDrink created above is an enormous cup
of milk! You'll notice the capacity is 0, meaning that mundane objects
cannot be stored in it. Now, a class extending com.planet_ink.coffee_mud.Items.Basic.StdFood:
public MyNewFood() { super();
setName( "a super crumb" ); setDescription( "A super duper crumb" ); setDisplayText( "Someone left their super crumbs." ); setSecretIdentity( "" );
setBaseValue( 1 ); setMaterial( RawMaterial.RESOURCE_MEAT ); setNourishment( 500 ); baseEnvStats.setWeight( 1 ); recoverEnvStats(); }
Now, the items extending com.planet_ink.coffee_mud.Items.Basic.StdRideable
resemble the MOB of the same name, and thus, have identical
modifications.
public MyNewBed() { super();
setName( "a bed" ); setDescription( "A bed" ); setDisplayText( "A bed is here" ); setSecretIdentity( "" );
setBaseValue( 100 ); setMobCapacity( 2 ); setRideBasis( Rideable.RIDEABLE_SLEEP ); baseEnvStats.setWeight( 100 ); recoverEnvStats(); }
A pile of money extends class com.planet_ink.coffee_mud.Items.Basic.StdCoins,
and is simplest of all:
public MyNewMoney() { super();
setName( "a pile of coins" ); setDescription( "A pile of coins" ); setDisplayText( "Someone left their money here." ); setSecretIdentity( "" );
setBaseValue( 0 ); setMaterial( RawMaterial.RESOURCE_GOLD ); setNumberOfCoins( 1000 ); // 1000 coins! setDenomination( 1.0 ); // each coin worth 1.0 basic gold baseEnvStats.setWeight( 1 ); recoverEnvStats(); }
Notice that the base value of the coins is 0,
it's the other methods that truly determine its value. Now, to
make a magical pill, our class should extend com.planet_ink.coffee_mud.Items.MiscMagic.StdPill:
public MyNewPill() { super();
setName( "a super pill" ); setDescription( "A super duper pill" ); setDisplayText( "Someone left their super pill." ); setSecretIdentity( "" );
setBaseValue( 1 ); setMaterial( RawMaterial.RESOURCE_MEAT ); setNourishment( 500 ); baseEnvStats.setWeight( 1 ); setSpellList( "Spell_Sleep;Prayer_CureLightWounds" ); recoverEnvStats(); }
The spells cast on the eater are listed by their
Class names, separated by semicolons. The secret identity is also
trimmed out, since the system will handle that automaticallyy. Also
notice that the StdPill resembles StdFood except for
the addition of the setSpellList method. In the exact same way,
the com.planet_ink.coffee_mud.Items.MiscMagic.StdPotion
class resembles the StdDrink class except that it has an identical
setSpellList method added to IT. So, in the interests of saving
a little sand for future generations, I would enumerate the StdPotion.
We can, however, show off another class which extends
com.planet_ink.coffee_mud.Items.MiscMagic.StdScroll:
public MyNewScroll() { super();
setName( "a super scroll" ); setDescription( "A super duper scroll" ); setDisplayText( "Someone left their super scroll." ); setSecretIdentity( "" ); setBaseValue( 100 );
setMaterial( RawMaterial.RESOURCE_PAPER ); setUsesRemaining( 50 ); baseEnvStats.setWeight( 1 ); setScrollSpells( "Spell_Sleep;Prayer_CureLightWounds" ); recoverEnvStats(); }
Not too difficult, right? Looks like the other
two, but the spell setting method has a different name. Now, let's
look at a sample of a class extending com.planet_ink.coffee_mud.Items.MiscMagic.StdWand:
public MyNewWand() { super();
setName( "a wand" ); setDescription( "A magic wand" ); setDisplayText( "Someone left their magic wand." ); setSecretIdentity( "" );
setBaseValue( 1000 ); setMaterial( RawMaterial.RESOURCE_OAK ); baseEnvStats.setWeight( 1 ); setSpell( CMClass.getAbility( "Spell_Fireball" ) ); setUsesRemaining( 50 ); recoverEnvStats(); }
In this case, we made use of the "uses remaining"
field to set the number of charges for the wand. The way the spell
is set is also different. A wand may only have one spell, and
the actual Ability object for the spell must be passed in, instead
of just the class name as we did before. You will find that this
is also how the classes extending com.planet_ink.coffee_mud.Items.MiscMagic.StdStaff
work. The StdStaff resembles the StdWeapon we did above,
except that the additional setSpell and setUsesRemaining calls become
appropriate to the constructor.
The next thing we will look at is adding effects and behaviors
to Items. Behavior addition (despite the fact that there is really
only one behavior that works with Items) will look familiar. The
only difference between this and the MOB example above is the fact
that we are setting a parameter on the Behavior before adding it.
Behavior B = CMClass.getBehavior( "Emoter" ); B.setParms( "min=1 max=20 chance=75;makes strange sounds" ); addBehavior( B );
Adding normal effects as properties is also similar to
mobs...
Ability A = CMClass.getAbility( "Prop_HaveResister" ); A.setMiscText( "fire acid 50%" ); A.addNonUninvokableEffect( A );
The above Effect will allow anyone who owns the item to
resist fire and acid at 50%! And again, as with mobs, these commands
are best put in the constructor of the item before the recoverEnvStats()
call.
There is also the advanced topic of extending the capabilities
of the existing Item classes. As mentioned previously, this consists
in creating your own java classes with the same class name and
ID() string methods as the base CoffeeMud Item classes, and the
adding the reference to your custom class to the relevant coffeemud.ini
file's entries after the %DEFAULT% string. Now, adding or extending
the capabilities of these classes typically means both adding
your own methods, and extending the importing existing methods
in those classes. The most important of those existing methods
are discussed above in the Core Topics, namely okMessage and executeMsg.
However, there are many other methods which might be extended
to the end of altering or enhancing basic aspects of the mud. Those
are numerated both in the classes you are extending, in
com.planet_ink.coffee_mud.Items.interfaces.Item.java, and in other
interface files in that directory. Consult the CoffeeMud java docs
for more information.
As with mobs, the following instructions are supplemental,
and unnecessary. Once you have created your new Item, modified your
INI file, and rebooted your CoffeeMud server, you need only use
the CREATE and other Archon commands to make use of it. If, for
some reason, you want to know HOW these commands do their work,
however, here it is.
To bring an Item into existence, an item must have somewhere
to exist! Items can belong to either Rooms, as mobs are, or they
can belong to mobs themselves. This means that Items actually have
two different creation mechanisms. Here is an example of each, starting
with the creation of an Item in a Room:
Item item = CMClass.getItem( "MyNewItem" ); Room room = CMLib.map().getRoom( "Midgaard#3504" );
room.addItem( item ); room.recoverRoomStats();
A room is grabbed off the map, and the item is added to
the room using the addItem() method. Then the room
recover is called to make the room react to the addition of the
item. Now, generic items require an additional step:
Item item = CMClass.getItem( "GenItem" ); Room room = CMLib.map().getRoom( "Midgaard#3504" );
item.text(); item.recoverEnvStats(); room.addItem( item ); room.recoverRoomStats();
The call to the text() method and the seemingly
redundant call to Item.recoverEnvStats() (which we
know is already in the item constructor), ensures that some of the
internal structures of the Generic Item are properly set. Of course,
these items are one-shot items, meaning that they are not generated
to exist on the map forever and ever.
Item item = CMClass.getItem( "MyNewItem" ); Room room = CMLib.map().getRoom( "Midgaard#3504" );
item.baseEnvStats().setRejuv( 500 ); // 30 minutes rejuvenation time item.recoverEnvStats(); room.addItem( item ); room.recoverRoomStats(); room.startItemRejuv();
In this case, we wanted the item to be rejuvenating. That
means that, when the item is removed from the room by a player,
the item will reset at some point in the future. If the rejuv ticks
count is set to 0, the item will not reset. In the example above,
the count is set to 500 so that the item will reset. However, the
rejuvenation is not actually activated until the room item rejuvs
are set. This is done with the last method call to startItemRejuv() ,
which handles the rejuv resets on all items in the room.
In the previous section, we saw how items are given to
mobs by simply calling the addInventory() method, so
this will not be repeated. Regardless of where or how the item is
created, however, it is destroyed the same way. With a simple call
to the destroy() method on the item. Here is an example
of destroying all the items in a room.
Room room = CMLib.map().getRoom( "Midgaard#3504" );
for( int i = room.numItems() - 1; i >= 0; i-- ) { Item item = room.fetchItem( i ); item.destroy(); }
A Behavior is defined as a property of an item,
mob, exit, or room which takes proactive (as opposed to REactive)
steps on behalf of its host. Examples of Behaviors include
aggressiveness, mobility, auto-emoting, and scripting. Behaviors
import the same packages mentioned in the first section of this
document under Complete Default Import List as well as (in this
case) com.planet_ink.coffee_mud.Behaviors.StdBehavior because our
sample behavior extends it. Custom behaviors are loaded
at boot-time by adding references to them to the BEHAVIORS entry
in your coffeemud.ini file. Let's take a look at a sample Behavior
and see how they are put together:
public class Ravenous extends com.planet_ink.coffee_mud.Behaviors.StdBehavior { public String ID(){ return "Ravenous"; } public String name(){ return "Ravenous Eater"; }
Our first step, as seen above, is to make sure we define
an ID() method with the classes name, just as we do
in other CoffeeMud objects. Notice that the ID() is
the same as the name of the class. This is no accident -- this is
required! The next step is to give the Behavior a name, which is
entirely unimportant to players, but helpful for Archons.
protected int canImproveCode(){ return Behavior.CAN_MOBS; } public long flags(){ return 0; } public boolean grantsAggressivenessTo( MOB M ){ return false; }
Next are some important flags that tell the CoffeeMud
system some important things about your behavior. The first method
(canImproveCode() ) tells the behavior whether
it is properly used on Mobs, or Items, Rooms, Exits, or all of these.
In this case, our behavior only affects mobs. The next method (flags() )
tells the system certain things about the behavior by returning
values such as Behavior.FLAG_MOBILITY, or Behavior.FLAG_TROUBLEMAKING.
The last method (grantsAggressivenessTo() ) says whether
or not this method would necessary cause the host mob to attack
the mob (M) in the parameter.
public void startBehavior( Environmental forMe ) {}
The next method, seldom used, is still quite important.
startBehavior receives as its parameter the brand new host of
this behavior. If the behavior instance needs to do any variable
or other preparation to either the behaving object host (forMe)
or itself, it should do so here.
public String getParms(){ return super.getParms(); } public void setParms( String parameters ) { super.setParms( parameters ); }
These methods, part of the StdBehavior and Behavior
interface, are shown here just to make you aware of how parameter
strings passed to behaviors are accessed. Sometimes prepatory code
is also executed inside a setParms method, for instance. Normally
these methods would not appear in your own instance of a Behavior.
public boolean tick( Tickable ticking, int tickID ) { MOB mob = getBehaversMOB( ticking ); // returns mob, if ticking is a mob. Returns mob owner, if ticking is an item Room room = getBehaversRoom( ticking ); // whatever the ticking object is, this will return its location if( ( mob == null ) || ( room == null ) || ( tickID != Host.MOB_TICK ) ) { return super.tick( ticking, tickID ); }
Now we get to the nitty gritty of the Behaviors work.
A behavior gets all or almost all of its work done in a tick method.
If you have not read the Core Topic above about the tick method,
you should definitely do so! In this example, we call two internal
StdBehavior methods to get some important starting information
about the behaving object host of the behavior. In this example,
our behaving object host will be a mob. However, these methods
may still intelligently return Item owners, or Exit rooms if the
host is other than a mob. The ticking parameter will always be the
behaving object host.
The next line checks to see if our host mob exists, and
is in a room. We also check to see if the tickID is the valid mob
ticking id. If our host had been an Item, Exit, or Room, this tickID
would no longer be Tickable.TICKID_MOB, but would be Tickable.TICKID_ITEM_BEHAVIOR,
Tickable.TICKID_ROOM_BEHAVIOR, or Tickable.TICKID_EXIT_BEHAVIOR.
Since our host, in this case, is a Mob, we check for Tickable.TICKID_MOB.
if( ( !canActAtAll( mob ) ) ||( !canFreelyBehaveNormal( mob ) ) ) { return super.tick( ticking, tickID ); }
Our next step is to call a couple more internal StdBehavior
methods. The first (canActAtAll() ) returns true if
the mob is alive, awake, and mobile. The second method (canFreelyBehaveNormal() )
returns true if the mob is not currently in combat, not charmed
or following anyone, and is not severely injured. We want our ravenous
mob to follow this behavior only in the best of health and mood.
Item eatible = null; for( int i = 0; i < mob.inventorySize(); i++ ) { Item I = mob.fetchInventory( i ); if( ( I != null ) && ( I instanceof Food ) ) { eatible=I; } }
Next, we iterate through the mob's inventory to find the
last instance of a piece of food.
if( eatible != null) { room.show( mob, eatible, null, "<S-NAME> gobble(s) up <T-NAMESELF>." ); return true; }
If some food was found, the mob will eat it. Now, practically
speaking, the mob will quickly devour and use up any and all food
which it may have had to begin with. What we decide to do when
the mob does NOT have food, therefore, is just as important.
int randMob = CMLib.dice().roll( 1, room.numInhabitants(), -1 ); MOB mobToHitUp = room.fetchInhabitant( randMob ); if( ( mobToHitUp == null ) ||( mobToHitUp == mob ) ||( CMLib.dice().rollPercentage() > 75 ) ) { return true; }
Since our mob is hungry, and another mob is the most likely
source of food, we will pick a random inhabitant of the room, which
is not ourselves, 25% of the time. Otherwise, we just return true,
and try again on the next tick.
int randItem = CMLib.dice().roll( 1, mobToHitUp.inventorySize(), -1 ); Item I = mobToHitUp.fetchInventory( randItem ); if( ( I != null ) && ( I instanceof Food ) ) { eatible = I; }
if( eatible == null ) { return true; }
Next we will pick a random piece of inventory from that
mob, and see if it is food. If not, we return true and try again
on the next tick.
CMLib.commands().postSay( mob, mobToHitUp, "May I have some of your " + eatible.name() + "?" , false, false );
return true; }
And lastly, since we have picked a random mob in the room,
and seen that he has food, we will ask him for it! If it's a player,
then perhaps he might even give us some. If we are given food,
we will eat it on the next tick for sure!
public void executeMsg( Environmental affecting, CMMsg msg ) {}
public boolean okMessage( Environmental oking, CMMsg msg ) { return true; } }
The above two methods are shown here just to remind you
that, although a behavior's PRIMARY purpose is to be proactive in
a tick method, a behavior also has the ability to preview and respond
to messages affecting the host behaving object. That object will
always be identified in the affecting and oking parameters
respectively. If these two methods mean nothing to you, you should
definitely go back and read the Core Topic on message passing.
A Character Class, in CoffeeMud, is the carreer
being followed by the player. Armor and weapon choices, skill and
spell access, as well as score advancements all depend on the
Character Class chosen by the player. Thankfully, despite all this
weighty responsibility, Character Classes are not difficult to
code. CharClasses import the same packages mentioned
in the first section of this document under Complete Default Import
List as well as (in this case) com.planet_ink.coffee_mud.CharClasses.StdCharClass
because our sample class extends it. Your custom classes
need to be listed in your coffeemud.ini file under the CHARCLASSES
entry. Aside from making custom classes, you can also extend an
existing class, return an identical ID() string, and
then list it at the end of the aforementioned entry in the coffeemud.ini
file. Now, let's take a look at a simple one here:
public class NormalGuy extends com.planet_ink.coffee_mud.CharClasses.StdCharClass {
Our Normal Guy character class will define all of the
basic elements of a filled-out character class.
public String ID() { return "NormalGuy"; }
public String baseClass() { return ID(); }
The first methods above are the unique Character Class
ID and the Base Class ID of the class. The Class ID must be a unique
identifier. The baseClass() method takes a bit of
explaining. If your CoffeeMud system is using the default SubClassing
system, the baseClass will define which classes may be switched
between by a player, as well as which classes are available to choose
from when a new player is created. Fighter, Monk, Paladin, Ranger,
and Barbarian, for instance, all have a baseClass of "Fighter".
This means that the Fighter class is one of the classes which may
be chosen by a new player (since it's ID() and baseClass()
are the same), and that any of the baseClass() "Fighter" classes
may switch amongst each other. If your CoffeeMud system is using
the multi-class or single classing system, this method is irrelevant.
public String name() { return "Normal Guy"; }
public String name( int classLevel ) { return name(); }
Our next method, name() , is the default displayable
name of your class. The next method name( int classLevel
) is, in this case, simply returning the default name again.
However, if you like, your character classes may have different
names at different class levels. Simply check the classLevel and
return a different string! As standard practice, however, class
level 0 should always return the default name. Remember that Class
Levels are different from Player Levels. A Player may, in a multi-classing
system, have numerous levels in numerous classes. The Class Level
represents how many levels the player has gained in this character
class ONLY!
protected String[] names = null;
public String[] nameSet() { if( names != null ) { return names; }
names = new String[1]; names[0] = name(); return names; }
The nameSet() method really only needs to
be extended and re-implemented when there are more than 1 available
names for your class. Its purpose is to return a string list of
all the names that this class may go by. As you can see from the
above code, by default, the StdCharClass will nicely handle classes
with just one name. However, the code above will need to be altered
in your own character class if you choose to make one with multiple
names.
public int getHPDivisor() { return 3; }
public int getHPDice() { return 1; }
public int getHPDie() { return 6; }
Next come the hit point ranges. When a player with this
class gains a level,these values will determine hit points gained
based on class and constitution.
public int getPracsFirstLevel() { return 3; }
public int getTrainsFirstLevel() { return 1; }
public int getBonusPracLevel() { return 0; }
The next two methods define the starting Training and
Practice points for this Character Class. The BonusPracLevel method
tells us how many bonus practices (above the number determined the
WISDOM/4 formula) which the player will receive every level.
public int getAttackAttribute() { return CharStats.STAT_STRENGTH; }
public int getBonusAttackLevel() { return 1; }
And here is an method defining which of the 6 primary
Character Attributes (Strength, Intelligence, Wisdom, Dexterity,
Constitution, or Charisma) are used to determine any attack bonuses
whenever the player gains a level. Usually this is Strength. The
number of bonus attack points received by a player when a level
is gained is determined by dividing the players score in this
attribute by 6, and then adding the value returned by
getBonusAttackLevel() .
public int getManaDivisor() { return 3; }
public int getManaDice() { return 1; }
public int getManaDie() { return 6; }
These methods determines how much mana a player receives
when they gain a level in this class.
public int getLevelsPerBonusDamage() { return 25; }
This score determines how many levels a player must make,
in this class, before they will gain a bonus point of damage to
all damage rolls.
public int getMovementMultiplier() { return 2; }
And lastly for our scores, this method will determine
how many movement points a player receives when they gain a level.
The formula is determined by dividing the player's strength score
by 9, and multiplying the result by this value.
public int allowedArmorLevel() { return CharClass.ARMOR_CLOTH; }
The CharClass interface defines a method called "armorCheck"
which returns true if the player is in compliance with armor
requirements. This method does its work by checking the
allowedArmorLevel() method. This method returns an equate
defined in the CharClass interface which may specify ANY armor,
CLOTH level, armor, LEATHER (or worse) armor, or NON-METAL armor.
You should check the CharClass interface for any other ARMOR_*
definitions which may be added from time to time.
public int allowedArmorLevel() { return CharClass.ARMOR_CLOTH; }
protected int requiredArmorSourceMinor() { return -1; }
protected String armorFailMessage() { return "<s-name> fumble(s) <s-his-her> <skill> due to <s-his-her> armor!"; }
The The StdCharClass will automaticallyy enforce armor
requirements whenever a class skill is used, provided these methods
are defined. The allowedArmorLevel() method returns
an equate defined in the CharClass interface which may specify ANY
armor, CLOTH level, armor, LEATHER (or worse) armor, METAL-ONLY,
or NON-METAL armor. You should check the CharClass interface for
any other ARMOR_* definitions which may be added from time to time.
While the armorFailMessage() method is pretty
self explanatory, the requiredArmorSourceMinor may not be. The later
method returns the MINOR code of the SOURCE code of the message
generating the skill use. Typically this method will either return
-1 for non spell casters, or CMMsg.TYP_CAST_SPELL for spell casters.
See the Core Topics for more information on what the heck a source
code of a message might be.
public int allowedWeaponLevel() { return CharClass.WEAPONS_THIEFLIKE; }
private HashSet disallowedWeapons = buildDisallowedWeaponClasses(); public HashSet disallowedWeaponClasses( MOB mob ) { return disallowedWeapons; }
private HashSet requiredWeaponMaterials = buildRequiredWeaponMaterials(); public HashSet requiredWeaponMaterials() { return requiredWeaponMaterials; }
The StdCharClass will automaticallyy enforce weapon restrictions
whenever a weapon attack is made, provided these methods are
defined. The allowedWeaponLevel() method returns an
equate defined in the CharClass interface which may specify ANY
weapons, DAGGER only, THIEF-like weapons, WOODEN weapons, and several
others. You should check the CharClass interface for other WEAPONS_*
definitions which may be added from time to time.
The disallowedWeaponClasses( MOB mob ) and
requiredWeaponMaterials() methods return HashSet
objects which, due to the method calls in StdCharClass seen above,
are totally derivative of the value you already put in allowedWeaponLevel() .
In other words, so long as you include the allowedWeaponLevel()
method, you should also include those next four methods, exactly
as you see them.
public int availabilityCode() { return Area.THEME_FANTASY; }
The method availabilityCode() defines how
players can access this race. Possible values for this constant
include: 'Area.THEME_FANTASY' (makes the class available for players
to choose when creating fantasy characters), 'Area.THEME_FANTASY|Area.THEME_SKILLONLYMASK'
(makes the class available to spells or Skills, but not for player
creation), or '0' (the class is not available to spells or for player
creation.)
public NormalGuy() { super(); maxStat[CharStats.STAT_CHARISMA] = 10; }
public void initializeClass() { CMLib.ableMapper().addCharAbilityMapping( ID(), 1, "Skill_Write", 50, "", true, false, new Vector(), "" );
CMLib.ableMapper().addCharAbilityMapping( ID(), 1, "Skill_Recall", 0, "", true, false, new Vector(), "" );
CMLib.ableMapper().addCharAbilityMapping( ID(), 1, "Skill_Climb", 0, "", true, false, new Vector(), "" );
CMLib.ableMapper().addCharAbilityMapping( ID(), // class/race to assign the skill to 1, // level the skill is qualified for "Skill_Swim", // the java ID of the ability to qualify for 0, // default proficiency to give after gaining "", // any misc parameters to pass to the Skill_Swim skill false, // true for the class to gain automatically, false to qualify false, // whether this skill is unlisted for this class CMParms.parseSemicolons( "Skill_Climb;Skill_Write", true ), // list of skills required to gain this one "-LOCALE +UNDERWATER" // mask to apply to class members wanting this skill ); } }
And now, after a few methods to flag our construction,
work, we come to our constructor! The Constructor for every character
class defines any special maximums for the primary attributes. This
is done by setting the appropriate value in the maxStat[]
array for the class. By default, 18 is the maximum score for all
primary attributes.
The second method is found in all CMObject, and is the
initializeClass() method. This method is called after all classes have
been loaded, but before the map is loaded. The method is called once
on every class, but only during initialization. In Character classes,
it establishes the qualifying and bonus skills for the class. This
is done through repeated calls to one of the several CMLib.ableMapper().addCharAbilityMapping()
methods. The first parameter of the method is the ID()
value of the Character Class itself, followed by the level at which
this class gains or qualifies for the skill. Next is the ID()
value of the Ability to allow this class to qualify for, followed
by the default proficiency which this class displays in the skill
(typically 0). The next parameter are any special parameters that
affects the way this class uses the skill, followed by a boolean
which establishes whether the player will receive this skill automaticallyy
when he or she gains the appropriate level, or whether they merely
qualify for the skill. The next parameter, almost always false,
determines whether the skill is "secret" for this class. Secret
skills are qualified for (or gained), but do not appear on Qualify
lists, Class information, or Help files. Secret skills are never
taught by Guildmasters (MOBTeachers) unless specifically told to.
The next parameter is a Vector of Strings, representing a list of
skills which must be known by this class before he can learn the
skill in question. The last parameter is a String representing a
mask which must be passed by the person who wants to gain this skill.
public Vector getSecurityGroups( int classLevel ) { if( classLevel > 1000 ) { Vector V = new Vector(); V.addElement( "ABOVELAW" ); V.addElement( "LISTADMIN" ); return V; } else { return new Vector(); } }
The purpose of this method is to allow you to assign CoffeeMud
Security Flags or CoffeeMud Security Groups to your players based
on their Character Class and Character Class Level. This is a
rather obscure feature that is really only meant for special
Character Classes you may design for your admins and builders. Your
player getSecurityGroups() methods will normally just
return an empty Vector regardless of classLevel. In this case, however,
we are demonstrating how a player who gains 1001 levels in our
NormalGuy Class will become immune to the CoffeeMud legal system
(ABOVELAW flag) and gain access to the Archon LIST command
(LISTADMIN flag). See the Archon's Guide for more information on
the CoffeeMud security system.
public String statQualifications() { return "Warm body, breathe."; }
public boolean qualifiesForThisClass( MOB mob, boolean quiet ) { if( !CMLib.flags().canBreathe( mob ) ) { if( !quiet ) { mob.tell( "You need to be breathing to be a normal guy." ); }
return false; }
return super.qualifiesForThisClass( mob, quiet ); }
The next method is a display string, for the benefit of
some of the web macros, which describes in plain english any attribute,
or other numeric qualifications to become this class. The
qualifiesForThisClass() method would actually check and
enforce the qualifications described by statQualifications() .
In our example above, there are no stat qualifications, only a check
to see if the idiot is still breathing. Also note that a quiet
boolean exists to allow qualifications to be checked without sending
any messages to the player in question.
public String otherBonuses() { return "Receives a mortgage, but no home."; }
The next method, like statQualifications above() ,
is for the benefit of the web macros. It describes any special bonuses
received due to being this class.
public Vector outfit( MOB myChar ) { if( outfitChoices == null ) { outfitChoices = new Vector(); Weapon w = (Weapon)CMClass.getWeapon( "a mortgage" ); outfitChoices.addElement( w ); } return outfitChoices; }
The outfit method should return a Vector of any Class-Specific
Item object equipment they may need. Clothing and so forth is actually
covered by Races.
public void grantAbilities( MOB mob, boolean isBorrowedClass ) { super.grantAbilities( mob, isBorrowedClass ); if( mob.isMonster() ) { Vector V = CMLib.abilityMapping().getUpToLevelListings( ID(), mob.charStats().getClassLevel( ID() ), false, false );
for( Enumeration a = V.elements(); a.hasMoreElements(); ) { Ability A = CMClass.getAbility( (String)a.nextElement() ); if( ( A != null ) && ( ( A.classificationCode()&Ability.ALL_ACODES) != Ability.ACODE_COMMON_SKILL ) && ( !CMLib.abilityMapping().getDefaultGain( ID(), true, A.ID() ) ) ) { giveMobAbility( mob, A, CMLib.abilityMapping().getDefaultproficiency( ID(), true, A.ID() ), CMLib.abilityMapping().getDefaultParm( ID(), true, A.ID() ), isBorrowedClass ); } } } }
This important method is called whenever a player gains
a level in this class, or when an NPC mob is being "outfitted" with
this class via one of the following Behaviors: CombatAbilities,
Fighterness, Druidness, Bardness, Clericness, Thiefness, Mageness.
The grantAbilities() method has the important
job of making sure that players receive their autogained skills
or any other options skills when they level. The StdCharClass version
of grantAbilities (called by super.grantAbilities(...) )
takes care of any autogained skills up to the player or mobs current
level in the class. Each char class which extends this, however,
needs to take care of any skills or abilities for NPC mobs which
are not automaticallyy gained. This is to make up for the fact that
npc mobs will not be lining up at your guildmasters to spend their
trains on skills they merely qualify for. In the sample code above,
we give the mobs every skill the class qualifies for up to the
mobs level in the class, except for any common skills. Those would
still need to be given by hand to each mob.
public boolean okMessage( Environmental myHost, CMMsg msg ) { if( !( myHost instanceof MOB ) ) { return super.okMessage( myHost, msg ); }
MOB myChar = (MOB)myHost;
if( ( msg.amITarget( myChar ) ) &&( msg.targetMinor() == CMMsg.TYP_DAMAGE ) &&( ( msg.sourceMinor() == CMMsg.TYP_COLD ) || ( msg.sourceMinor() == CMMsg.TYP_WATER ) ) ) { int recovery = myChar.charStats().getClassLevel( this ); msg.setValue( msg.value() - recovery ); } else if( ( msg.amITarget( myChar ) ) &&( msg.targetMinor() == CMMsg.TYP_DAMAGE ) &&( msg.sourceMinor() == CMMsg.TYP_FIRE ) ) { int recovery = msg.value(); msg.setValue( msg.value() + recovery ); }
return super.okMessage( myChar, msg ); } }
And lastly, just as I'm sure you were wondering how useful
those three Core Topics above would really be, we see them in active
use. Some classes contain methods such as these to enforce some
of the benefits for the class. In the okMessage() method,
which we discussed in the first Core Topic, we see messages messages
containing the type of damage taken by the player being intercepted.
TYP_DAMAGE messages always have their damage amounts stored in
the .value() method of a message, so it is these values
which are modified, based on the type of damage taken.
And not least, although we won't go into it in detail
here, there are two other methods which may be of use for the advanced
Character Class programmer. They are the level and unLevel methods.
These methods are called when a player gains or loses
(respectively) a level in the class. If there are any extra skills
or bonus scores the player may wish to gain and lose with levels,
that would be the place for such code. Also, in some cases (Mages
and Clerics come to mind), the gaining of qualifying skills may
be somewhat complex. In those cases, overriding the gainAbilities
method may be in order. Check the Mage and Cleric classes for
more information.
A Race, in CoffeeMud, contributes very little to
functionality, but quite a bit to the role playing and other "soft"
aspects of the game.. For this reason, everyone is encouraged
to code as many races as humanly possible. The more, the better!
Races import the same packages mentioned in the first
section of this document under Complete Default Import List as well
as (in this case) com.planet_ink.coffee_mud.Races.StdRace because
our sample class extends it. Your custom races need to
be listed in your coffeemud.ini file under the RACES entry. Aside
from making custom race classes, you can also extend an existing
race class, return an identical ID() string, and then list it at
the end of the aforementioned entry in the coffeemud.ini file. Now,
let's take a look at a simple one here:
public class Grumbler extends com.planet_ink.coffee_mud.Races.StdRace {
Our Grumbler race will define all of the basic elements
of a filled-out race.
public String ID() { return "Grumbler"; }
public String name() { return "Grumbler"; }
public String racialCategory() { return "Grumbler"; }
protected static Vector resources = new Vector();
public int availabilityCode() { return Area.THEME_FANTASY; }
The first methods return the unique ID of the Race (which
must always match the java/class file name) and the name()
method is the displayable name of the Race. The third method is
very important, as it defines the category into which this race
falls. There is no hard rule to determine when a new category should
be created versus using an old one. Some of the uses of racial categories
include the Ranger Favored Enemy skill, as well as most of the
race-based restrictions on doors and with shopkeepers. In many
ways, the racial category is more important than the name of the
race itself, if functionality is considered.
The last method list above, availabilityCode, defines
how players can access this race. Possible values for this constant
include: 'Area.THEME_FANTASY' (makes the race available for players
to choose when creating fantasy characters), 'Area.THEME_FANTASY|Area.THEME_SKILLONLYMASK'
(makes the race available to spells such as Polymorph or Wish, but
not for player creation), or '0' (the race is not available to
spells or for player creation.)
protected int shortestMale() { return 84; }
protected int shortestFemale() { return 78; }
protected int heightVariance() { return 80; }
protected int lightestWeight() { return 2000; }
protected int weightVariance() { return 500; }
These methods, as you might have guessed, establish parameters
for the base height and weight of a typical monster of this type.
A random number from 0-heightVariance() will be added
to the shortedMale() /shortestFemale()
value to determine height, while a random number from 0-weightVariance()
will be added to lightestWeight() to determine that.
protected long forbiddenWornBits() { return Item.WORN_WIELD |Item.WORN_WAIST |Item.WORN_ABOUT_BODY |Item.WORN_FEET |Item.WORN_HANDS; }
This method establishes where a creature of this type
may NOT wear something. In this case, we forbid any wielded items,
or anything worn around the waist, on hands or feet, or about the
body. Anywhere else is fine. Return 0 if you do not wish any restrictions
on wearing.
private static final int[] parts = {0,2,2,1,1,0,0,1,4,4,1,0,1,1,1,2}; public int[] bodyMask() { return parts; }
The bodyMask() method defines and returns
an array of integers which defines the types and number of particular
body parts normally retained by the race. Each position in the array
is defined by the equates BODY_ in the Race interface. These equates
are (starting from 0): BODY_ANTENEA, BODY_EYE, BODY_EAR, BODY_HEAD,
BODY_NECK, BODY_ARM, BODY_HAND, BODY_TORSO, BODY_LEG, BODY_FOOT,
BODY_NOSE, BODY_GILL, BODY_MOUTH, BODY_WAIST, BODY_TAIL,
BODY_WING. Remember that these can be found in the Race interface
for you to reference. In the above example, we find no antenea,
2 eyes, 2 ears, a head, neck, no arms or hands, a torso, 4 legs,
4 feet, a nose, but no gill, and then a mouth, waist, tail, and
2 wings.
private String[] racialAbilityNames = { "Skill_Trip", "Fighter_Whomp" }; private int[] racialAbilityLevels = { 1, 3 }; private int[] racialAbilityProficiencies = { 75, 50 }; private boolean[] racialAbilityQuals = { false, false };
protected String[] racialAbilityNames() { return null; }
protected int[] racialAbilityLevels() { return null; }
protected int[] racialAbilityProficiencies() { return null; }
protected boolean[] racialAbilityQuals() { return null; }
public Vector racialAbilities( MOB mob ) { Vector V = super.racialAbilities( mob ); return V; }
Our next section here deals with Racial Abilities, which
are defined as follows: A racial ability is a skill that has a command
word, and is not autoinvoked. A racial ability may be qualified
for or automaticallyy gained. If the skill is qualified for, then
upon reaching the designated player level, the player may GAIN the
skill, and will have a default proficiency as designated. If the
skill is not qualified for, then it is automaticallyy gained. This
means that all mobs or players of this race, who have obtained the
necessary player level, will have access to the use of the skill
as if they had learned it, and at the proficiency designated. Racial
Abilities are available to any mob or player of this race, even
those affected by Shape Shift, Polymorph, or similar skills.
The first four methods define these skills. The data in
all four variables are ordered with relative to each other. The
racialAbilityNames is a list of the Ability class ID.
The racialAbilityLevels is the level at which the
skill is qualified for or gained. The racialAbilityProficiencies()
is the proficiency of skills automaticallyy gained. The racialAbilityQuals()
tells whether or not the skill is automaticallyy gained (false)
or is only qualified for (true).
The method above (racialAbilities() ) will
return a Vector of Ability objects, with proficiency already set,
appropriate to the mob passed in. This vector should consist only
of automaticallyy gained abilities appropriate to the level of the
mob. If the four variables are set properly, the programmer will
not need to override the method from StdRace unless there other
gender-based or other special qualifications for skills not defined
by those four variables.
public Vector outfit( MOB myChar ) { if( outfitChoices == null ) { outfitChoices = new Vector(); Weapon w = (Weapon)CMClass.getItem( "GenPants" ); outfitChoices.addElement( w ); }
return outfitChoices; }
The outfit method should return a Vector of any Race-Specific
Item object equipment they may need.
private String[] culturalAbilityNames = { "Dwarven", "Mining" }; private int[] culturalAbilityProficiencies = { 100, 50 };
public String[] culturalAbilityNames() { return culturalAbilityNames; }
public int[] culturalAbilityProficiencies() { return culturalAbilityProficiencies; }
Cultural Abilities are defined as those skills which a
mob or player of this race would know through upbringing in the
culture of that race, such as language. Players Shape Shifted or
Polymorphed into the race, since they did not grow up in the culture,
would not have automatic access to these skills per se. These two
methods are defined similarly to the Racial Abilities above.
public void affectEnvStats( Environmental affected, EnvStats affectableStats ) { super.affectEnvStats( affected, affectableStats );
affectableStats.setSensesMask( affectableStats.sensesMask() | EnvStats.CAN_SEE_INFRARED ); }
This sample of the affectEnvStats() method
we discussed in the Core Topics above makes sure that all creatures
of this race can see in the infrared spectrum.
public void affectCharStats( MOB affectedMOB, CharStats affectableStats ) { super.affectCharStats( affectedMOB, affectableStats ); affectableStats.setStat( CharStats.STAT_STRENGTH, 15 ); affectableStats.setStat( CharStats.STAT_DEXTERITY, 25 ); affectableStats.setStat( CharStats.STAT_INTELLIGENCE, 5 ); }
This sample of the affectCharStats method we discussed
in the Core Topics above establishes a base strength, dexterity,
and intelligence for all creatures of this race. As this is the
ONLY way to modify a MOBs stats short of magical equipment, it should
be used with care!
public void startRacing( MOB mob, boolean verifyOnly ) { super.startRacing( mob, verifyOnly ); }
startRacing is called whenever a player of this race logs
on, or a mob of this race is created. If there are any special properties
of the mob or player which must be set due to their being this
race, this would be the appropriate method in which to do so. This
method is not called for Polymorph, Shape Shift, or similar changes
in race, but only for those whose permanent race is this one.
public Weapon myNaturalWeapon() { if( naturalWeapon == null ) { naturalWeapon = CMClass.getWeapon( "StdWeapon" ); naturalWeapon.setName( "huge claws" ); naturalWeapon.setWeaponType( Weapon.TYPE_PIERCING ); }
return naturalWeapon; }
This method allows you to create (see item creation above)
a special weapon to serve the creature whenever they are not wielding
something. Since our Grumbler cannot wield weapons anyway, it
is important to give them some big piercing claws.
public String healthText( MOB viewer, MOB mob ) { double pct = ( CMath.div( mob.curState().getHitPoints(), mob.maxState().getHitPoints() ) );
if( pct < .10 ) { return "^r" + mob.displayName(viewer) + "^ris raging in bloody pain!^N"; } else if( pct < .20 ) { return "^r" + mob.displayName(viewer) + "^ris covered in blood.^N"; } else if( pct < .30 ) { return "^r" + mob.displayName(viewer) + "^r is bleeding badly from lots of wounds.^N"; } else if( pct < .50 ) { return "^y" + mob.displayName(viewer) + "^y has some bloody wounds and gashed scales.^N"; } else if( pct < .60 ) { return "^p" + mob.displayName(viewer) + "^p has a few bloody wounds.^N"; } else if( pct < .70 ) { return "^p" + mob.displayName(viewer) + "^p is cut and bruised heavily.^N"; } else if( pct < .90 ) { return "^g" + mob.displayName(viewer) + "^g has a few bruises and scratched scales.^N"; } else if( pct < .99 ) { return "^g" + mob.displayName(viewer) + "^g has a few small bruises.^N"; } else { return "^c" + mob.displayName(viewer) + "^c is in perfect health.^N";\ } }
Although the programmer is welcome to skip the above method
and use the defaults from the StdRace class, this allows you to
set special health messages for creatures of this type.
public Vector myResources() { synchronized( resources ) { if( resources.size() == 0 ) { resources.addElement( makeResource( "a " + name().toLowerCase() + "claw", RawMaterial.RESOURCE_BONE ) );
for( int i = 0; i < 10; i++ ) { resources.addElement( makeResource( "a strip of " + name().toLowerCase() + " hide", RawMaterial.RESOURCE_SCALES ) ); }
for( int i = 0; i < 10; i++ ) { resources.addElement( makeResource( "a pound of " + name().toLowerCase() + " meat", RawMaterial.RESOURCE_MEAT ) ); }
resources.addElement( makeResource( "some " + name().toLowerCase() + " blood", RawMaterial.RESOURCE_BLOOD ) ); } }
return resources; } }
The above method allows you to determine what sorts of
materials are gotten from this creature whenever the dead corpse
is Butchered.
Now, in addition to the methods above which are
good to include in your custom races, there are also several
methods which are not normally extended or overridden, but which is
may be good to do so in special cases. These methods include
public void level( MOB mob ) , which is called
whenever a player of that race gains a level, public void agingAffects(
MOB mob, CharStats baseStats, CharStats charStats ) , which
is called to enforce how aging effects this race, and public
DeadBody getCorpseContainer( MOB mob, Room room ) , which
is called to create a corpse for members of this race.
Exits are the connecting points between two rooms, and
tend to be rather simple. If two rooms, A & B, are connected
to each other, there are always two exits associated with that connection.
One from room A to room B, and the other from room B to room A.
Exits import the same packages mentioned in the first
section of this document under Complete Default Import List as well
as (in this case) com.planet_ink.coffee_mud.Exits.StdExit because
our sample class extends it. Here is an example exit:
public class SlidingDoor extends com.planet_ink.coffee_mud.Exits.StdExit { public String ID() { return "SlidingDoor"; }
public SlidingDoor() { super(); Ability A = CMClass.getAbility( "Prop_ReqHeight" ); A.setMiscText( "30" ); addNonUninvokableEffect( A ); }
public String name() { return "a sliding door"; }
public String displayText() { return ""; }
public String closedText() { return "a closed sliding door"; }
public String doorName() { return "door"; }
public String openName() { return "slide"; }
public String closeName() { return "slide"; }
public boolean hasADoor() { return true; }
public boolean hasALock() { return false; }
public boolean defaultsLocked() { return false; }
public boolean defaultsClosed() { return true; }
public int openDelayTicks() { return 45; } }
As you can see, exits are very simple. A set of variables
and parameters are sufficient to establish every function of an
exit, and these are already well defined in the Archon's Guide.
This is due primarily to the fact that several Properties and Behaviors
give an exit most of its color and complexity.
Exits, like all other Environmental objects, get to preview
and execute messages. They will only tend to listen for messages
dealing with OPENING or CLOSING where the exit is the target,
or ENTERING and LEAVING where the exit is the tool. If a player
is going from room A to room B. The player message will note that
he or she is ENTERING the exit in room A and LEAVING the exit in
room B. Although this makes perfect sense to me, it may sound a
little backwards from the intuitive way. Since Room objects (Locales)
are almost always the target of ENTER and LEAVE messages, exits
are subordinated to being the tools of such messages.
Exits will never tick, by and large, unless they have
a door that defaults closed and the door is opened, or they gain
some sort of Behavior (such as Emoter).
Locales are the stuff rooms are made of, and
so they implement the interface Room. There are actually four different
general types of locales: the standard room, the standard grid,
the "thin" grid, and the standard maze. Each of those respectively
is a functional superset of the former respectively. Rooms import
the same packages mentioned in the first section of this document
under Complete Default Import List as well as (in this case) com.planet_ink.coffee_mud.Rooms.StdGrid,
com.planet_ink.coffee_mud.Rooms.StdRoom, or com.planet_ink.coffee_mud.Rooms.StdMaze,
because our sample classes extends them.
Let's looks at an example of a standard grid room, which
has much of the functionality we are interested in:
public class RoadGrid extends com.planet_ink.coffee_mud.Locales.StdGrid { public String ID() { return "RoadGrid"; }
public RoadGrid() { super(); name = "a road"; baseEnvStats.setWeight( 1 ); recoverEnvStats(); }
public String getGridChildLocaleID() { return "Road"; }
public Vector resourceChoices() { return Road.roomResources; }
public int domainType() { return Room.DOMAIN_OUTDOORS_PLAINS; }
public int domainConditions() { return Room.CONDITION_NORMAL; } }
Here we see the standard RoadGrid. It's effects, behaviors,
displaytext, description, and (since it is a grid type) size in
the x and y are all defined by the builder. The features (and they
aren't many) which are available to the coder can be seen here.
We see the base weight being set to "1" here. This is the default
number of movement points consumed by crossing this room. For
Grids, we see the Locale ID of the child-rooms inside the grid.
We also see the standard room settings methods for the domain type
(the type of locale it is) and the domain conditions (the quality
of the weather, or the air, wetness, dryness, etc).
The Resource choices for this room are borrowed from the
Road itself, though this will never be used. Players will never
actually be inside the Grid room itself, but will always occupy
one of the child rooms, each of which will take direction from the
parent Grid. If you wish to define resources, however, be aware
that the resourceChoices vector returned may not be null, and must
only contain Integer objects representing the Resource (see the
Items.interfaces.RawMaterial.java interface) available there.
Use the RESOURCE_* static integer values from RawMaterial interface.
Here are another set of useful methods:
public String roomTitle() public String roomDescription()
These methods return the title and description
of the room respectively. These methods are responsible for making
the title and description into a proper displayable format. They
draw on the values of the room object displayText()
and description() methods respectively, then parse
that data for any special display codes, though often that data is
simply passed through.
The executeMsg() , and okMessage()
methods on rooms are also available, as they are in all Environmental
objects, for customized message handling as described in the Core
Topics above.
The Area objects, which represent areas in the game, are
the most difficult to advise about regarding programming.
However, an attempt must be made. Therefore, we
will go over some of the methods and features available on the Area
object, which might be overridden for some other use. Areas
import the same packages mentioned in the first section of
this document under Complete Default Import List.
public class StdArea implements Area { public String ID() { return "StdArea"; }
[...]
public Climate getClimateObj(){...} public int climateType(){...} public void setClimateType( int newClimateType ){...}
Of course, like every Environmental object, the Area must
define an ID() . Notice that the ID() is
the same as the name of the class. This is no accident -- this is
required! The name, display text, description, and others are all
handled by the builder, or Properties or behaviors, and aren't pertinent
to this discussion of Areas.
The weather, however, is a relative function of each area.
Each area knows its current weather object (see the Climate interface)
as well as the next weather change "in the que". These can be read
and set by the methods in the Climate object. Each area also knows
it climatic "tendencies", and this also can be set and read from
the area itself. Lastly, a method exists on the Climate object
to force the area to cycle through its weather, which will force
the "next" weather code to become current, and establish a new
"next" weather code.
public int getTechLevel() public void setTechLevel( int level )
These will return the technical level allowed in the area,
whether it be magic, technology, or both.
public String getArchivePath() public void setArchivePath( String pathFile )
The Archive name of the area is set and read from
the area, though it's more properly set by the builder.
public TimeClock getTimeObj()
Similar to Climate above, each area has a reference to
a TimeClock object which contains information about local time.
Unlike the Climate, however, StdAreas all share the same Time object,
meaning that time is global. However, the object exists here in
case you want local time areas.
public void toggleMobility( boolean onoff ) public boolean getMobility()
These methods define whether Mobile mobs will move around.
By toggling mobility off, no mob or player in the whole area will
move from room to room for any reason, allowing a good base state
for builders to work from.
public StringBuffer getAreaStats()
If the data, appearance, or format of the HELP provided
for areas needs to be changed, the getAreaStats() method
is where to generate a new one.
public Enumeration getMetroMap() public int metroSize() public Room getRandomMetroRoom()
These methods provide access to all of the rooms in the
given area, plus all of the rooms in any child areas, plus any rooms
in their children areas and so forth.
public void fillInAreaRooms() public void fillInAreaRoom( Room R )
public Enumeration getProperMap() public int properSize() public Room getRandomProperRoom() public void clearMetroCache()
The first two methods are called in order to perform finalizing
clean-up or resetting of room structures. The remaining methods
provide access to the set of rooms which are directly a part of
this area. The last method is one which should be called if any
proper rooms are ever added, since it clears and "re-calculates"
the metro rooms list.
public void addSubOp( String username ) public void delSubOp( String username ) public boolean amISubOp( String username ) public String getSubOpList() public void setSubOpList( String list ) public Vector getSubOpVectorList()
And lastly, the list of staff (Area Archons they are also
sometimes called), can be managed from here. Changing these methods
would modify how staff are handled by Areas.
Properties are the simplest of the objects which implement
the Ability interface, and are defined as effects which can be
permanently tacked-on to items, mobs, and other Environmental
objects. Properties are Abilities, but they are never qualified
for by classes, never gained as skills, and never wear off or
disappear when a mob dies. They are always added to a mob, item,
room, etc using the Environmental interface method addNonUninvokableEffect(
Ability ) method either inside a custom coded Environmental
object, or at run-time to a GenMob, GenItem, Room, or similar type
object.
Properties and Behaviors are often the basic building
blocks of the customized GenMob and GenItem in CoffeeMud, and differ
from each other in this basic respect: Properties tend to have a
smaller memory footprint and tend to react to events affecting their
hosts rather than cause their hosts to take proactive actions. Properties
make heavy use of the message handlers and stat affecting methods.
If you have not read the Core Topics above already, you should
do so now.
A Custom property may or may not belong to any
particular package, though it is important that the ID() of the
property be unique in the system. Properties import the same
packages mentioned in the first section of this document under Complete
Default Import List as well as (in this case) com.planet_ink.coffee_mud.Properties.Property
because our sample class extends it.
A customized property class must extend the Property class
implemented in the aforementioned package. This Property class
already implements the Ability interface, and already has dummy
methods for most of the Ability interface methods which are
unnecessary or are unused in a Property.
Each property must also have custom ID() ,
and name() methods as shown below. Notice that the
ID() is the same as the name of the class. This
is no accident -- this is required!
public class Prop_AstralSpirit extends com.planet_ink.coffee_mud.Abilities.Properties.Property { public String ID() { return "Prop_AstralSpirit"; }
public String name() { return "Astral Spirit"; }
Above we see the aforementioned methods defined. The Property
does not differ from other Abilities, or indeed other Environmental
objects in this respect. A unique ID() , and name()
method must be defined for each new class.
protected int canAffectCode() { return Ability.CAN_MOBS; }
public String accountForYourself() { return "an astral spirit"; }
Here are two important support methods you will
also find in Skills and the other more standard Abilities. The
first method tells what type of objects (Areas, MOBs, Items, Rooms,
or Exits) can be affected by this property. In this case, this
property only affects MOBS. A value like Ability.CAN_MOBS|Ability.CAN_ITEMS
would denote one that affects MOBs or Items.
The second method is a string which is returned whenever
this property appears on an Item which is Identified. This string
would appear in addition to any secretIdentity defined for the item.
/** this method defines how this thing responds * to environmental changes. It may handle any * and every msg listed in the CMMsg class * from the given Environmental source */ public boolean okMessage( Environmental myHost, CMMsg msg ) { if( ( affected == null ) || ( !( affected instanceof MOB ) ) ) { return true; }
MOB mob = (MOB)affected;
if( ( msg.amISource( mob ) ) && ( !CMath.bset( msg.sourceMajor(), CMMsg.MASK_ALWAYS ) ) ) { if( ( msg.tool() != null ) &&( msg.tool().ID().equalsIgnoreCase( "Skill_Revoke" ) ) ) { return super.okMessage( myHost, msg ); } else if( msg.targetMinor() == CMMsg.TYP_WEAPONATTACK ) { mob.tell( "You are unable to attack in this incorporeal form." ); peaceAt( mob ); return false; } else if( ( CMath.bset( msg.sourceMajor(), CMMsg.MASK_HANDS ) ) || ( CMath.bset( msg.sourceMajor(), CMMsg.MASK_MOUTH ) ) ) { if( CMath.bset( msg.sourceMajor(), CMMsg.MASK_SOUND ) ) { mob.tell( "You are unable to make sounds in this incorporeal form." ); } else { mob.tell( "You are unable to do that this incorporeal form." ); }
peaceAt( mob ); return false; } } else if( ( msg.amITarget( mob ) ) &&( !msg.amISource( mob ) ) &&( !CMath.bset( msg.targetMajor(), CMMsg.MASK_ALWAYS ) ) ) { mob.tell( mob.name() + " doesn't seem to be here." ); return false; }
return true; }
As discussed in the Core Topics above, here is an example
of an okMessage method. In this case, we intercept attack and other
vocal or hand movement messages where the source of the action is
the mob whose property this is. We then return false, effectively
canceling those messages, after telling the poor bloke why we
are doing it. The rest of the method prevents any messages from
targeting the mob with this property. This saves him from being
attacked by aggressives, or arrested by cityguards -- since
technically he isn't even there.
public void affectEnvStats( Environmental affected, EnvStats affectableStats ) { super.affectEnvStats( affected, affectableStats ); // when this spell is on a MOBs Affected list, // it should consistantly put the mob into // a sleeping state, so that nothing they do // can get them out of it.
affectableStats.setWeight( 0 ); affectableStats.setHeight( -1 );
affectableStats.setDisposition( affectableStats.disposition()|EnvStats.IS_GOLEM );
affectableStats.setDisposition( affectableStats.disposition()|EnvStats.IS_INVISIBLE );
affectableStats.setDisposition( affectableStats.disposition()|EnvStats.IS_NOT_SEEN );
affectableStats.setSensesMask( affectableStats.sensesMask()|EnvStats.CAN_NOT_SPEAK ); } }
Last but not least, here is another example from the Core
Topics, an affectEnvStats() method. This method will
always have the host mob passed in the "affected" parameter, and
a copy of his current EnvStats objects in the affectableStats parameter.
This method then sets the mob as being both invisible, and totally
unseen. It makes him unviewable to infrared, and prevents him from
speaking. The two methods above then combine to produce our desired
results, an Astral Spirit.
Abilities are easily the most complicated and varied of
all the objects in CoffeeMud. They encompass everything from invoked
skills and spell, like TRIP or MAGIC MISSILE, to natural abilities,
like NONDETECTION. A Skill Ability then is an object which implements
the Ability interface fully, unlike the Property above, which
implements only certain parts.
An Ability is known in the mud as any of the following:
A Spell, Song, Dance, Skill, Common Skill, Thief Skill, Poison,
Disease, Prayer, Chant, or Trap. They all implement the Ability
interface, which in turn extends the Environmental interface discussed
throughout the rest of this document. Abilities make extensive
use of the okMessage and executeMsg, tick, and stat affecting methods
discussed above in the Core Topics. If you have not read the Core
Topics, you should do so now.
Abilities will all tend to extend the basic StdAbility
class found in the Abilities package, and sometimes they will further
extend a class more specific to their type, such as Spell, StdSkill,
Song, BardSkill, Prayer, FighterSkill, ThiefSkill, Chant, or some
other basic types found in the standard CoffeeMud distribution packages.
Abilities are added to mob objects using the addAbility
method on MOBs. From there, through casting or by autoInvocation
(discussed below), they may end up in the effects list of a mob,
item, room, area, or exit via the addEffect method.
A Custom ability may or may not belong to any
particular package, though it is important that the ID()
of the ability be unique in the system. A custom ability
imports the same packages mentioned in the first section of this
document under Complete Default Import List as well as (in this
case) com.planet_ink.coffee_mud.Abilities.StdAbility because our
sample class extends it.
A customized ability class usually extends the StdAbility
class implemented in the aforementioned package. This StdAbility
class already implements the Ability interface, and already has
numerous support methods which aid in the creation of custom abilities.
Each ability must also have custom ID() and
name() methods as shown below. Notice that the
ID() is the same as the name of the class. This
is no accident -- this is required!
public class Skill_Trip extends com.planet_ink.coffee_mud.Abilities.StdAbility { public String ID() { return "Skill_Trip"; }
public String name() { return "Trip"; }
Above we see the aforementioned methods defined. This
skill does not differ from other Abilities, or indeed other Environmental
objects in this respect. A unique ID() and name()
method must be defined for each new class.
public String displayText() { return "(Tripped)"; }
The display text on an Ability always refers to the text
shown in the "You are affected by" section shown by the SCORE and
AFFECTS commands in the MUD. In this case, someone affected by trip
will see (Tripped). If this method does not leave any information
for those affected by it, it should return "".
protected int canAffectCode() { return CAN_MOBS; }
protected int canTargetCode() { return CAN_MOBS; }
These two methods assist the MUDGrinder, and the HELP
files in classifying the several skills and spells.
The first method, canAffectCode() , returns
what type of objects, if any, this ability class may find itself
in the effects list of. Since our Trip skill effects the tripped
mob, we list mobs. Other possible values include any combination
of CAN_ROOMS, CAN_EXITS, CAN_ITEMS, or CAN_AREAS. If a skill may
Effect more than one, they can be combined using the | operator.
An example skill that affects both mobs and items might return CAN_MOBS|CAN_ITEMS,
for instance.
The second method, canTargetCode() , above
tells the system what type of objects can be targeted by this skill.
Some skills will target items (like the Enchant Weapon spell), or
rooms (like the Darkness spell), or exits (like the Knock spell).
In a similar fashion to canAffectCode, this method can return any
combination of valid objects to notify the system of proper targets.
public int abstractQuality() { return Ability.QUALITY_MALICIOUS; }
This MOST important method tells the system quite a bit
about the nature of the skill, as well as how mobs with behaviors
like CombatAbilities should use it. In this case, we return
Ability.QUALITY_MALICIOUS, which tells the system that the skill is
always malicious to others, and that it will most likely anger
the target. It tells mobs to target this skill at enemies in combat
Other possible values include:
Ability.QUALITY_BENEFICIAL_SELF
|
always helpful to oneself when used
|
Ability.QUALITY_BENEFICIAL_OTHERS
|
can be targeted to other people, and
is always beneficial |
Ability.QUALITY_OK_SELF
|
targets oneself, but is only useful
in certain circumstances, or it has complicated parameters
that mobs won't find useful |
Ability.QUALITY_OK_OTHERS
|
targets other people, but is only useful
under certain circumstances, or it also has complicated parameters
|
Ability.QUALITY_INDIFFERENT
|
targets items, or rooms, and is only
useful in certain circumstances |
Only skills marked as Ability.QUALITY_BENEFICIAL_*
orAbility.QUALITY_MALICIOUS will be used in combat or by most behaviors.
public int castingQuality( MOB invoker, Environmental target ) { return super.castingQuality( invoker, target ); }
This second most important method is similar to the
abstractQuality() method above, but it tells the system
what the quality of the skills or spell is if it is specifically
used by the given invoker against the given target at this given
time. If your skill is only useful in water or against elves, it
might be good to check to see if the invoker is in water or the
target is an elf before returning a value. The same kinds of values
may be returned from this method as are returned by abstractQuality,
except for QUALITY_OK_OTHERS and Ability.QUALITY_OK_SELF, which
are meaningless in this context.
public boolean isAutoInvoked() { return false; }
All skills are either autoInvoking, or they must be invoked
by a player or mob. The value returned by this method determines
which sort this ability is. If it is autoinvoking, it will not have
an invoke( method as described below, nor will it have trigger
strings, nor will it return anything other than "" from it's
displayText() method. An example of an autoInvoking
ability would be Blind Fighting or Two Weapon Fighting. In our case,
however, Trip is not autoinvoking, but requires a player to invoke
it.
private static final String[] triggerStrings = { "TRIP" }; public String[] triggerStrings() { return triggerStrings; }
public double castingTime() { return 0.25; }
public double combatCastingTime() { return 1.0; }
For skills which are not autoinvoking, like this one,
we must define an array of strings which constitute the command
words to use this skill. Each entry in the array must be only one
word. If more than one skill is found with the same trigger words
(such as the spells, which all share the trigger word "CAST"), then
the name of the specific ability will be the required next parameter.
Trip, however, has a unique trigger word, which is defined by this
method.
Skills can also define how many actions they take to execute
when the caster or user of the skill is in combat, or not in combat.
The next two methods define this. A value of 0 means that the skill
or spell is always instantaneous. A higher value means that the
skill requires a full action to perform. Typical players have 1
action per tick (4 second period) to use.
public int classificationCode() { return Ability.ACODE_SKILL; }
This important method defines which category an ability
falls into. Possible values include: SKILL, PRAYER, SONG, TRAP,
SPELL, THIEF_SKILL, LANGUAGE, CHANT, COMMON_SKILL, DISEASE, or POISON.
public long flags() { return Ability.FLAG_MOVING; }
This method returns a value consisting of one or more
flags, separated by | symbols in the same way that canAffectCode
does above. Each flag has a specific meaning and imparts information
to the engine about the ability. Possible values, which may be used
alone or together in any combination, include:
FLAG_BINDING |
Binds and or limits movement and the
use of hands. |
FLAG_MOVING |
Changes the position of the target
or affected one. |
FLAG_TRANSPORTING |
Changes the room of the target, requires
that the performer of the skill be present.
|
FLAG_WEATHERAFFECTING
|
Changes the weather.
|
FLAG_SUMMONING |
Changes the room of the target, where
the performer of the skill is not present. May also bring
new creatures into existence. |
FLAG_CHARMING |
Charms the target. |
FLAG_TRACKING |
Results in the target tracking something.
|
FLAG_HEATING |
Makes the target hot.
|
FLAG_BURNING |
Makes the target on fire.
|
FLAG_HOLY |
Means that the skill is GOOD aligned,
unless FLAG_UNHOLY is also set, in which case it makes the
skill NEUTRAL. |
FLAG_UNHOLY |
Means that the skill is EVIL aligned,
unless FLAG_UNHOLY is also set, in which case it makes the
skill NEUTRAL. |
FLAG_PARALYZING |
Makes the target or affected one unable
to move their muscles. |
public void affectEnvStats( Environmental affected, EnvStats affectableStats ) { super.affectEnvStats( affected, affectableStats ); if( !doneTicking ) { affectableStats.setDisposition( affectableStats.disposition() | EnvStats.IS_SITTING ); } }
This is an example of the affectEnvStats()
method described in the Core Topics. In the case of our Trip skill,
it forces the affected target into a sitting position.
public boolean okMessage( Environmental myHost, CMMsg msg ) { if( ( affected == null ) || ( !( affected instanceof MOB ) ) ) { return true; }
MOB mob = (MOB)affected; if( doneTicking && msg.amISource( mob ) ) { unInvoke(); } else if( msg.amISource( mob ) && ( msg.sourceMinor() == CMMsg.TYP_STAND ) ) { return false; }
return true; }
This is an example of the okMessage() method
described in the Core Topics. In this case, it intercepts messages
where the affected mob is trying to stand and cancels the message,
without comment, by returning false.
public void unInvoke() { if( ( affected == null ) || ( !( affected instanceof MOB ) ) ) { return; }
MOB mob = (MOB)affected; if( canBeUninvoked() ) { doneTicking = true; }
super.unInvoke();
if( !mob.amDead() ) { if( mob.location() != null ) { CMMsg msg = CMClass.getMsg( mob, null, CMMsg.MSG_NOISYMOVEMENT, "<S-NAME> regain(s) <S-HIS-HER> feet." );
if( mob.location().okMessage( mob, msg ) ) { mob.location().send( mob, msg ); CMLib.commands().postStand( mob, true ); } } else { mob.tell( "You regain your feet." ); } } }
All standard abilities include an unInvoke()
method, which is called when the Effect caused by the ability is
dispelled, or the duration of the ability expires. The super.unInvoke()
method, which is located in StdAbility.java, actually does the work
of removing the Effect from the affected object (stored in the
variable affected), and then setting affected to null. In this particular
unInvoke() method, we also see the code trying to
force the mob back to his feet.
public boolean preInvoke( MOB mob, Vector commands, Environmental givenTarget, boolean auto, int asLevel, int secondsElapsed, double actionsRemaining ) { return true; }
The preInvoke() method is an optional method
that normally just returns true. Unless you have reason to do so,
you do not need to override the standard preInvoke method in StdAbility.java.
The preInvoke method must return true before the invoke()
method (below) is executed. The difference between the two is that
the preInvoke method is called at the moment the skill command words
are entered, even if the skill is coded, via the castingTime()
or combatCastingTime() methods, to invoke at some later
time. If your skill does invoke at a later time, it is generally
useful to use the preInvoke method to scan the commands Vector,
which contains the command parameter strings minus any trigger words,
for errors. You may also give the user a message that their skill
will invoke later on. The preInvoke method will continue to be
called, at 1 second intervals, until the player is able to invoke
the command.
public boolean invoke( MOB mob, Vector commands, Environmental givenTarget, boolean auto, int asLevel ) {
If an ability is not autoinvoked, it will always have
a functional invoke method. This invoke method includes the following
parameters: the mob invoking the ability and a vector of command
parameter strings (which does not include any trigger words).
The givenTarget parameter, which is null on normal ability invocations,
will have the value of a target to the skill if one is available.
Calls to the invoke method where the givenTarget is not null are
typically from potions, wands, traps, or other automatic
invocations. Which brings us to the last parameter, auto. Auto is
false on normal invocations of the skill, and true whenever the
skill should always invoke no matter what. Setting auto to true
not only changes the output string for the skill, but overrides
proficiency checks as well.
MOB target = this.getTarget( mob, commands, givenTarget ); if( target == null ) { return false; }
Our first step in Trip is to get our target. There are
several getTarget() methods built into StdAbility for
determining a target object based on the command parameters passed
in, as well as the abstractQuality() value for the
ability. The StdAbility method call to getTarget()
is smart enough to know that if a target name is not specified by
givenTarget, and also one is not specified in the commands Vector
passed in, that it should choose whoever the mob is fighting, since
the ability isAbility.QUALITY_MALICIOUS. Non-malicious skills would
not follow this reasoning, but would choose the caster himself as
default.
All of the several getTarget() methods will
generate their own error messages to the user if a target is not
specified, or found, or cannot be determined. For this reason, we
need only to check to see if a target has been returned. If not,
we can return false from invoke() , telling the system
that the ability failed to invoke.
if( CMLib.flags().isSitting( target ) || CMLib.flags().isSleeping( target ) ) { mob.tell( target, null, null, "<S-NAME> is already on the floor!" );
return false; }
if( !CMLib.flags().aliveAwakeMobile( mob, true ) ||( CMLib.flags().isSitting( mob ) ) ) { mob.tell( "You need to stand up!" ); return false; }
if( mob.isInCombat() && ( mob.rangeToTarget() > 0 ) ) { mob.tell( "You are too far away to trip!" ); return false; }
if( target.riding() != null ) { mob.tell( "You can't trip someone " + target.riding().stateString( target ) + " " + target.riding().name() + "!" );
return false; }
if( CMLib.flags().isFlying( target ) ) { mob.tell( target.name() + " is flying and can't be tripped!" ); return false; }
Here are numerous checks to see if the invoking mob is
able to trip, and the target is able to be tripped.
if( !super.invoke( mob, commands, givenTarget, auto ) ) { return false; }
The invoke() method back up in StdAbility
is called now. This method will check mana requirements, and subtract
mana if necessary. It should be called AFTER all other preliminary
checks have been made.
int levelDiff = target.envStats().level() - mob.envStats().level(); if( levelDiff > 0 ) { levelDiff = levelDiff * 5; } else { levelDiff = 0; }
Double temp = new Integer( target.charStats().getStat( CharStats.STAT_DEXTERITY ) ).doubleValue() - 9.0;
int adjustment = (-levelDiff) + (-( 35 + ( (int)Math.round( temp * 3.0 ) ) ) );
boolean success = proficiencyCheck( mob, adjustment, auto );
The proficiencyCheck() method called at the
end here will determine of the user of the skill has passed their
proficiency check. The first parameter of this method is the invoking
mob, followed by either a positive (or helpful) adjustment, or
a negative (not helpful) adjustment. The second parameter is the
auto flag mentioned above, to allow overrides of the normal proficiency.
In the case of Trip, we calculate an adjustment based both on level
and the dexterity of the target. We store whether or not the
proficiency check failed or passed into the variable success.
success = success && ( target.charStats().getBodyPart( Race.BODY_LEG ) > 0 );
if( success ) { CMMsg msg = CMClass.getMsg( mob, target, this, CMMsg.MSK_MALICIOUS_MOVE|CMMsg.TYP_JUSTICE|( auto ? CMMsg.MASK_ALWAYS : 0 ), auto ? "<T-NAME> trip(s)!" : "^F<S-NAME> trip(s) <T-NAMESELF>!^?" );
if( mob.location().okMessage( mob, msg ) ) { mob.location().send( mob, msg ); int durationTicks = ( msg.value() > 0 ) ? 1 : 2; maliciousAffect( mob, target, asLevel, durationTicks, -1 ); target.tell( "You hit the floor!" ); } } else { return maliciousFizzle( mob, target, "<S-NAME> attempt(s) to trip <T-NAMESELF>, but fail(s)." ); }
return success; } }
The rest of the invoke() method actually
does the work of sending the trip message and affecting the target
if necessary, or sending the "fizzle" message if the proficiency
check failed (success was false).
Of special note are some of the other StdAbility method
calls in this section of code. You'll notice that the message constructed
(as per the Core Topics discussion in a previous section) in this
case uses a TYP_JUSTICE message (meaning an attack on dignity),
with the malicious and movement flags set. If this message gets
sent, a call to maliciousAffect() is made. The first
parameter is the invoker, and the second parameter is the one its
being invoked upon. The third parameter is the level of the affect,
or 0 for default values. The forth parameter is the duration of
the Effect in ticks, which you can see is dependent in this case
upon whether the message came back with a value() > 0
(meaning the saving throw was made). If the forth parameter is 0,
then the default duration formula will be used. All other positive
values, such as in this case, denote a number of ticks of duration.
The last parameter, normally -1, asks maliciousAffect()
to give the target one more saving throw, against any of the valid
saving throw type messages, such as TYP_FIRE, TYP_ACID, TYP_GAS,
etc. The value of -1 means not to make any further saving throw
attempts before affecting the target.
If this skill had not been malicious, we could have made
a call to the beneficialAffect() method instead. That
method has the same parameters as maliciousAffect() ,
though it lacks the final parameter for a further saving throw,
since beneficial affects require no saving throw.
Now, if the success variable had been false, then we made
a call to the maliciousFizzle() method. In addition
to a display in the room, this method will make sure the target
knows that the invoking mob was trying to do something bad to him
or her, so that the target will get angry and start fighting. Had
this not been a malicious ability, we would have made calls instead
to either the beneficialWordsFizzle() method, or the
beneficialVisualFizzle() method, depending upon
whether the skill is verbal or somantic based. Most spells are verbal
based, for instance, while most skills are somantic.
Finally, we return the results of the success variable
from the invoke() method, to let the system know whether
or not the ability succeeded. If it did, the target will now be
affected by this ability, and will have to stay on the ground for
a few ticks while the source mob pounds on him or her.
Spells, Prayers, and Chants are all special forms of Abilities.
For this reason, it is required that you go back and read the previous
section on Skill Abilities before proceeding. You will also be
required to poke through the Core Topics elsewhere in this document
as well.
Spells will follow all the rules mentioned above in the
Skill Abilities section, with a few differences:
public class Spell_ResistFire extends com.planet_ink.coffee_mud.Abilities.Spells.Spell { public String ID() { return "Spell_ResistFire"; }
public String name() { return "Resist Fire"; }
public String displayText() { return "(Resist Fire)"; }
public int abstractQuality() { return Ability.QUALITY_BENEFICIAL_OTHERS; }
protected int canAffectCode() { return CAN_MOBS; }
public int classificationCode() { return Ability.ACODE_SPELL|Ability.DOMAIN_ABJURATION; }
The first difference you will notice above is that spells
extend the base class Spell.java found in the Abilities/Spells directory
instead of StdAbility. Spell.java also extends StdAbility as well.
The second difference is in the classificationCode()
method. You will notice that the domain of the spell is also specified.
In addition to the normal classification code of Ability.ACODE_SPELL,
the domain may be added using the | symbol. Possible domains
include:
DOMAIN_DIVINATION |
Spells that grant knowledge.
|
DOMAIN_ABJURATION |
Spells that protect.
|
DOMAIN_ILLUSION |
Spells that fool the senses.
|
DOMAIN_EVOCATION |
Spells that bring forth the elements.
|
DOMAIN_ALTERATION |
Spells that change things.
|
DOMAIN_TRANSMUTATION
|
Spells that change people.
|
DOMAIN_ENCHANTMENT |
Spells that enchant items or the mind.
|
DOMAIN_CONJURATION |
Spells that transport people or items.
|
public boolean invoke( MOB mob, Vector commands, Environmental givenTarget, boolean auto, int asLevel ) { MOB target = getTarget( mob, commands, givenTarget ); if( target == null ) { return false; }
if( !super.invoke( mob, commands, givenTarget, auto ) ) { return false; }
boolean success = proficiencyCheck( mob, 0, auto ); if( success ) { CMMsg msg = CMClass.getMsg( mob, target, this, affectType( auto ), auto ? "<T-NAME> feel(s) cooly protected." : "^S<S-NAME> invoke(s) a cool field of protection around <T-NAMESELF>.^?" );
if( mob.location().okMessage( mob, msg ) ) { mob.location().send( mob, msg ); beneficialAffect( mob, target, asLevel, 0 ); } } else { beneficialWordsFizzle( mob, target, "<S-NAME> attempt(s) to invoke fire protection, but fail(s)." ); }
return success; } }
The invoke() method above follows the one
in Skill_Abilities very closely. You will see that no adjustment
is made to the proficiency check, and that, since this is not
aAbility.QUALITY_MALICIOUS ability, calls are made to
beneficialAffect() and beneficialWordsFizzle()
instead of maliciousAffect() and maliciousFizzle() .
The main difference to notice here, however, is the line constructing
the evoking message. You will notice that where the message code
should be specified, a method call to affectType() ,
which is a method located in Spell.java, is made. This call will
automaticallyy construct the proper message type for a spell, taking
into accountAbility.QUALITY_MALICIOUSness, and whether the auto
flag is set. You will also notice that different message strings
are constructed depending upon whether the auto flag is set.
Prayers will follow all the rules mentioned above in the
Skill Abilities section, with a few differences:
public class Prayer_Anger extends com.planet_ink.coffee_mud.Abilities.Prayers.Prayer { public String ID() { return "Prayer_Anger"; }
public String name() { return "Anger"; }
public int abstractQuality() { return Ability.QUALITY_MALICIOUS; }
public long flags() { return Ability.FLAG_UNHOLY; }
public boolean invoke( MOB mob, Vector commands, Environmental givenTarget, boolean auto, int asLevel ) { if( !super.invoke( mob, commands, givenTarget, auto ) ) { return false; }
boolean success = proficiencyCheck( mob, 0, auto );
boolean someoneIsFighting = false; for( int i = 0; i < mob.location().numInhabitants(); i++ ) { MOB inhab = mob.location().fetchInhabitant( i ); if( ( inhab != null ) && ( inhab.isInCombat() ) ) { someoneIsFighting = true; } }
if( success &&( !someoneIsFighting ) &&( mob.location().numInhabitants() > 3 ) ) { // it worked, so build a copy of this ability, // and add it to the effects list of the // affected MOB. Then tell everyone else // what happened. CMMsg msg = CMClass.getMsg( mob, null, this, affectType( auto ), auto ? "A feeling of anger descends" : "^S<S-NAME> rage(s) for anger.^?" );
if( mob.location().okMessage( mob, msg ) ) { mob.location().send( mob, msg );
[.......] }
The first difference you may notice between this and the
Skill Ability discussed in the previous section is that this skill
extends the Prayer.java class instead of StdAbility. Prayer.java
will make sure that the classificationCode() method
returns the proper value.
You should also take note of the flags()
method. All Prayers must return a value from flags()
where either the FLAG_UNHOLY is set (meaning the Prayer is EVIL
aligned), FLAG_HOLY is set (meaning the Prayer is GOOD aligned),
or BOTH are set, in which case the Prayer is NEUTRAL.
The last special note is down in the message construction
in the invoke() method. The invoke() method
above follows the one in Skill_Abilities very closely. You will
see that no adjustment is made to the proficiency check, for instance.
The main difference to notice here, however, is the line constructing
the evoking message. You will notice that where the message code
should be specified, a method call to affectType() ,
which is a method located in Prayer.java, is made. This call will
automaticallyy construct the proper message type for a prayer,
taking into accountAbility.QUALITY_MALICIOUSness, and whether the
auto flag is set. You will also notice that different message
strings are constructed depending upon whether the auto flag is
set.
Chants will follow all the rules mentioned above in the
Skill Abilities section, with a few differences:
public class Chant_AlterTime extends com.planet_ink.coffee_mud.Abilities.Druid.Chant { public String ID() { return "Chant_AlterTime"; }
public String name() { return "Alter Time"; }
public String displayText() { return ""; }
public int overrideMana() { return 100; }
public int abstractQuality() { return Ability.QUALITY_INDIFFERENT; }
protected int canAffectCode() { return 0; }
protected int canTargetCode() { return 0; }
public boolean invoke( MOB mob, Vector commands, Environmental givenTarget, boolean auto, int asLevel ) { if( !super.invoke( mob, commands, givenTarget, auto ) ) { return false; }
boolean success = proficiencyCheck( mob, 0, auto ); if( success ) { // it worked, so build a copy of this ability, // and add it to the effects list of the // affected MOB. Then tell everyone else // what happened. CMMsg msg = CMClass.getMsg( mob, null, this, affectType( auto ), auto ? "" : "^S<S-NAME> chant(s), and reality seems to start blurring.^?" );
if( mob.location().okMessage( mob, msg ) ) { mob.location().send( mob, msg ); int x = CMath.s_int( text() ); while( x == 0 ) { x = CMLib.dice().roll( 1, 3, -2 ); }
if( x > 0 ) { mob.location().showHappens( CMMsg.MSG_OK_VISUAL, "Time moves forwards!" ); } else { mob.location().showHappens( CMMsg.MSG_OK_VISUAL, "Time moves backwards!" ); }
if( CMLib.map().numAreas() > 0 ) { CMLib.map().getFirstArea().tickTock( x ); } } } else { return beneficialWordsFizzle( mob, null, "<S-NAME> chant(s), but the magic fades" ); }
// return whether it worked return success; } }
The first difference you may notice between this
and the Skill Ability discussed in the previous section is that
this skill extends the Chant.java class instead of StdAbility.
Chant.java will make sure that the classificationCode()
method returns the proper value.
You should also take note that the overrideMana()
method is actually returning a value here of 100. This method
allows your ability to always cost the same amount of mana,
regardless of the invokers level. This method may be used in any
ability, not just chants. Also, this is not a method that is
normally found in chants. This particular chant, being deemed to be
especially powerful, happens to return a value for it. Normally
this method would not be found.
The last special note is down in the message
construction in the invoke() method. The invoke()
method above follows the one in Skill_Abilities very closely.
You will see that no adjustment is made to the proficiency check,
for instance. The main difference to notice here, however, is
the line constructing the evoking message. You will notice that
where the message code should be specified, a method call to affectType() ,
which is a method located in Chant.java, is made. This call will
automaticallyy construct the proper message type for a chant,
taking into accountAbility.QUALITY_MALICIOUSness, and whether
the auto flag is set. You will also notice that different message
strings are constructed depending upon whether the auto flag is
set.
Although Songs also follow the Skill Abilities
rules above, they are the most unique of skills. More than any
other skill, they rely very heavily on basic functionality provide
by their appropriate superclasses. For your sanity, I recommend
that ALL of your coded songs extend either com.planet_ink.coffee_mud.Abilities.Songs.Song,
com.planet_ink.coffee_mud.Abilities.Songs.Dance, or
com.planet_ink.coffee_mud.Abilities.Songs.Play. This is because
those classes provide very important basic functionality unique to
songs, and save you as the coder from having to worry about coding
that unique functionality yourself.
By unique functionality, I am referring to the
way songs behave when invoked in particular. They are always
invoked upon groups, and always require the invoker to remain in
the room for the song to remain in effect.
public class Song_Protection extends com.planet_ink.coffee_mud.Abilities.Songs.Song { public String ID() { return "Song_Protection"; }
public String name() { return "Protection"; }
Like the skill abilities above, ID() and
name() are implemented. The name()
will get intermingled into the invocation text, so name your songs
carefully, so they sound right when invoked!
public int abstractQuality() { return Ability.QUALITY_BENEFICIAL_OTHERS; }
This method, discussed above for skill abilities,
is absolutely vital for Songs, as it tells the Song superclass
whether to invoke the song upon all your enemies, or upon all your
group members. Make sure you implement this method!
public void affectEnvStats( Environmental affected, EnvStats affectableStats ) { super.affectEnvStats( affected, affectableStats ); if( invoker == null ) { return; }
affectableStats.setAttackAdjustment( affectableStats.attackAdjustment() - 5 ); }
This particular song uses affectEnvStats to lower
the attack rating of everyone that hears the song. Although this is
not a malicious song, it is the price of using it.
public void affectCharStats( MOB affected, CharStats affectableStats ) { super.affectCharStats( affected, affectableStats ); if( invoker == null ) { return; }
affectableStats.setStat( CharStats.STAT_DEXTERITY, affectableStats.getStat( CharStats.STAT_DEXTERITY ) - 1 );
affectableStats.setStat( CharStats.STAT_SAVE_ACID, affectableStats.getStat( CharStats.STAT_SAVE_ACID ) + ( invoker.charStats().getStat( CharStats.STAT_CHARISMA ) * 4 ) );
affectableStats.setStat( CharStats.STAT_SAVE_COLD, affectableStats.getStat( CharStats.STAT_SAVE_COLD ) + ( invoker.charStats().getStat( CharStats.STAT_CHARISMA ) * 4 ) );
affectableStats.setStat( CharStats.STAT_SAVE_ELECTRIC, affectableStats.getStat( CharStats.STAT_SAVE_ELECTRIC ) + ( invoker.charStats().getStat( CharStats.STAT_CHARISMA ) * 4 ) );
affectableStats.setStat( CharStats.STAT_SAVE_FIRE, affectableStats.getStat( CharStats.STAT_SAVE_FIRE ) + ( invoker.charStats().getStat( CharStats.STAT_CHARISMA ) * 4 ) );
affectableStats.setStat( CharStats.STAT_SAVE_GAS, affectableStats.getStat( CharStats.STAT_SAVE_GAS ) + ( invoker.charStats().getStat( CharStats.STAT_CHARISMA ) * 4 ) ); } }
The most important thing our sample song does
is improve the saving throws of everyone who hears it. The affectCharStats()
method makes that happen by modifying the appropriate saving throws
in affectableStats.
There are actually 3 unique kinds of common skills. One type
is simply a normal skill, just like the ones shown above. Another
type is the gathering skill, which is used to collect resources, and
extends com.planet_ink.coffee_mud.Abilities.Common.GatheringSkill. The
last type is the crafting skill, which extends
com.planet_ink.coffee_mud.Abilities.Common.CraftingSkill. In general,
common skills have this in common: they invoke immediately, but take a
long time to complete. They give messages of progress to the user, and
they immediately unInvoke themselves if the user leaves the room,
enters combat, or some other canceling condition arises.
Let's look at a Gathering skill. Although Crafting skills are much
more involved, especially in the way they select the items they
create, and in the way they fill-in the variables for what they are
creating, you will find them remarkably similar to gathering
skills.
public class Fishing extends com.planet_ink.coffee_mud.Abilities.Common.GatheringSkill { public String ID() { return "Fishing"; }
public String name() { return "Fishing"; }
If you've read the Skill Abilities section above, you
will recognize these methods and their importance. As always, the
ID() must match the class name, while the name()
can be whatever you want to call the skill.
private static final String[] triggerStrings = { "FISH" }; public String[] triggerStrings() { return triggerStrings; }
And again, like a normal skill, the common skills create
and return their invocation words (or "trigger strings" as they
are called in skills).
public long flags() { return FLAG_GATHERING; }
public String supportedResourceString() { return "FLESH"; }
The flags() method you will also recognize
from the Skill Abilities section. However, we've provided the system
at large with a flag to let it know that this skill is for gathering
resources, and not for crafting.
The supportedResourceString() is a great
supplementary method that is unique to Common Skills. It's purpose
is to inform the system what kinds of MATERIALS and RESOURCES are
utilized by this skill. Fishing is for catching fish, so we'll notify
the system that we deal in meat (flesh). Appropriate strings to
return from this method are listed in com.planet_ink.coffee_mud.Items.interfaces.RawMaterial.java.
If there is more than one resource or material type, they are
separated with the pipe | character.
protected Item found = null; protected String foundShortName = "";
Almost all the common skills create the item that they
will generate in the invoke method, store it in the skill, and then
provide it when the skill is unInvoked. For this reason, we'll need
some variables to store the object we will create, and a string
for its name.
public Fishing() { super(); displayText = "You are fishing..."; verb = "fishing"; }
Our constructor sets the variable "verb", which is part
of the CommonSkill superclass. It is used when constructing sentences
which let the user and others around them know what they are doing.
displayText serves the same purpose described by the Skill
Abilities section above.
public boolean tick( Tickable ticking, int tickID ) { if( ( affected != null ) && ( affected instanceof MOB ) && ( tickID == Tickable.TICKID_MOB ) ) { MOB mob = (MOB)affected; if( tickUp == 6 ) { if( found != null ) { commonTell( mob, "You got a tug on the line!" ); } else { StringBuffer str = new StringBuffer( "Nothing is biting around here.\n\r" ); commonTell( mob, str.toString() ); unInvoke(); } } }
return super.tick( ticking, tickID ); }
Although our common skill super classes do most of the
hard work of maintaining the task being performed, you will still
find that they are often extended by individual common skills in
other to give more appropriate messages to the user. If you don't
know what a tick method is fore, read the Core Topic on the subject
first. In this case, before calling the superclasses tick method
and letting it do most of our work, we take a moment to check the
tickUp integer maintained by CommonSkill.java. That variable is
incremented every 4 seconds after the skill is invoked. in this
case, after 24 seconds (6*4), we let the user know whether or
not an item was generated by the invoke method. If an an item
was invoked, we tell them they have hooked a fish. If an item was
not generated, we tell them that nothing is biting and immediately
unInvoke() the skill.
The commonTell() method is another feature
unique to common skills. Since common skills are often found on
hireling mobs, we wanted a special method which would tell players
who invoke the skill about the progress of our task, even if it
is a follower that is actually DOING the task. For instance, commonTell()
would tell a player who is fishing "You got a tug on the line!".
However, commonTell would force an NPC follower of a player to SAY
"I got a tug on the line!".
public void unInvoke() { if( canBeUninvoked() ) { if( ( affected != null ) && ( affected instanceof MOB ) ) { MOB mob = (MOB)affected; if( ( found != null ) && ( !aborted ) &&( !helping ) ) { int amount = CMLib.dice().roll( 1, 5, 0 ) * ( abilityCode() ); String s = "s"; if( amount == 1 ) { s = ""; }
mob.location().show( mob, null, CMMsg.MSG_NOISYMOVEMENT, "<S-NAME> manage(s) to catch " + amount + " pound" + s + " of " + foundShortName + "." );
for( int i = 0; i < amount; i++ ) { Item newFound = (Item)found.copyOf(); mob.location().addItemRefuse( newFound, Item.REFUSE_PLAYER_DROP );
if( ( mob.riding() != null ) && ( mob.riding() instanceof Container ) ) { newFound.setContainer( (Container)mob.riding() ); }
CMLib.commands().postGet( mob, null, newFound, true ); } } } }
super.unInvoke(); }
As mentioned above, the unInvoke() method
is where the final work is done. So long as the skill has not been
aborted (the aborted variable), or the skill was invoked only to
help ANOTHER player (the helping variable), we go ahead and create
several copies of our target item, put them in the room using addItemRefuse,
and force the player to get them into their inventory (if they can).
public boolean invoke( MOB mob, Vector commands, Environmental givenTarget, boolean auto, int asLevel ) { bundling = false; if( (!auto) && ( commands.size() > 0 ) && ( ( (String)commands.firstElement() ).equalsIgnoreCase( "bundle" ) ) ) { bundling = true; if( super.invoke( mob, commands, givenTarget, auto, asLevel ) ) { return super.bundle( mob, commands ); }
return false; }
The invoke() method is where common skills
do 99% of their work, namely determining what the user wants to
do and whether they have the resources and are in the right place
to do it, and then actually generating the target item and filling
it out. Then the invoke method actually places the filled-out common
skill on the player as an "effect", allowing it to receive "tick"
calls until it completes.
In this case, right off the bat we check to see if the
user is trying to use the Fishing skill to bundle up a pile of fish.
If so, we call a superclass helper method to do the bundling for
us. The bundle method which will happily use the supportedResourceString()
method above to determine what the player is allowed to
bundle.
int foundFish = -1; boolean maybeFish = false; if( mob.location() != null ) { for( int i = 0; i < RawMaterial.FISHES.length; i++ ) { if( mob.location().myResource() == RawMaterial.FISHES[i] ) { foundFish = RawMaterial.FISHES[i]; maybeFish = true; } else if( ( mob.location().resourceChoices() != null ) && ( mob.location().resourceChoices().contains( new Integer( RawMaterial.FISHES[i] ) ) ) ) { maybeFish = true; } } }
Our next step is to initialize a variable for the
com.planet_ink.coffee_mud.Items.interfaces.RawMaterial.java
(resource) code which will represent the type of fish we have
caught here. We also set a variable saying whether or not ANY fish
are even POSSIBLY available here.
if( !maybeFish ) { commonTell( mob, "The fishing doesn't look too good around here." ); return false; }
If maybeFish is false, we are in a location where no fish
are even possibly available (such as a Desert room).
verb = "fishing"; found = null; playSound = "fishreel.wav";
Otherwise, we re-initialize the "verb" and "found" variables
we mentioned above, and set a CommonSkill string called "playSound"
which will be used to give our skill a little sound effect while
it is working.
if( !super.invoke( mob, commands, givenTarget, auto, asLevel ) ) { return false; }
Next we call the GatherSkill superclass invoke method.
It is important to note that several qualifying checks are made
in that method, such as whether the player is able to perform the
skill. It also subtracts the necessary mana or movement points for
using the skill.
if( ( proficiencyCheck( mob, 0, auto ) ) && ( foundFish > 0 ) ) { found = (Item)CMLib.materials().makeResource( foundFish, mob.location().domainType(), false ); foundShortName = "nothing"; if( found != null ) { foundShortName = RawMaterial.RESOURCE_DESCS[ found.material()&RawMaterial.RESOURCE_MASK ].toLowerCase(); } }
Now we check to see if two important conditions have occurred;
the first being whether the player has passed his or her proficiency
check, and the second being whether the foundFish variable was
assigned. Remember that the foundFish variable represents the
material & resource code for the type of fish we will catch.
If both of those are true, we make use of a very useful method in
the utensils() library called makeResource to generate
our item for us. We also properly set the name of what we have caught
based on the material type of the foundFish.
int duration = 35 - mob.envStats().level(); if( duration < 10 ) { duration=10; }
CMMsg msg = CMClass.getMsg( mob, found, this, CMMsg.MSG_NOISYMOVEMENT, "<S-NAME> start(s) fishing." );
if( mob.location().okMessage( mob, msg ) ) { mob.location().send( mob, msg ); found = (Item)msg.target(); beneficialAffect( mob, mob, asLevel, duration ); } return true; } }
Our last step is to calculate the amount of time (duration)
that it will take to catch the fish, create a coffeemud message
to declare the start of our finishing adventure, and send the message
out. After doing so, notice that we re-set the found variable to
msg.target() . This may seem strange, since, in the
getMsg method, we already declared found to be our message target.
So isn't that like saying that A=A? Actually no! Remember that the
okMessage method may preview or *modify* a message. Re-setting the
found variable to msg.target() is done to support code
which may have modified our target to something else.
Now
we'll take a look at a proper crafting skill. They are more complicated
than gathering skills, but you'll be able to apply almost all you
learned by reviewing the gathering skill to build a good picture
of the way crafting skills work. The first difference is that the
crafting skills extend com.planet_ink.coffee_mud.Abilities.Common.CraftingSkill.
Many of them also implement the com.planet_ink.coffee_mud.Abilities.interfaces.ItemCrafter
interface, which allows them to provide items to other parts of
coffeemud instantly!
public class GlassBlowing extends CraftingSkill implements ItemCraftor { public String ID() { return "GlassBlowing"; }
public String name() { return "Glass Blowing"; }
If you've read the Skill Abilities section above, you
will recognize these methods and their importance. As always, the
ID() must match the class name, while the name()
can be whatever you want to call the skill.
private static final String[] triggerStrings = { "GLASSBLOW", "GLASSBLOWING" }; public String[] triggerStrings() { return triggerStrings; }
And again, like a normal skill, the common skills create
and return their invocation words (or "trigger strings" as they
are called in skills).
public String supportedResourceString() { return "GLASS|SAND"; }
The supportedResourceString() is a great
supplementary method that is unique to Common Skills. It's purpose
is to inform the system what kinds of MATERIALS and RESOURCES are
utilized by this skill. GlassBlowing is for turning sand into glass,
so we'll notify the system that we deal in sand and glass resources.
Appropriate strings to return from this method are listed in
com.planet_ink.coffee_mud.Items.interfaces.RawMaterial.java. If
there is more than one resource or material type, they are
separated with the pipe | character.
protected static final int RCP_FINALNAME = 0; protected static final int RCP_LEVEL = 1; protected static final int RCP_TICKS = 2; protected static final int RCP_WOOD = 3; protected static final int RCP_VALUE = 4; protected static final int RCP_CLASSTYPE = 5; protected static final int RCP_MISCTYPE = 6; protected static final int RCP_CAPACITY = 7; protected static final int RCP_SPELL = 8;
This list is at the core of the crafting skills
It represents the name and function of each column of the crafting
skill's text file. Later on, we will use these constants to index
into a Vector that represents the particular row from the crafting
skill's text file that the player is building.
protected Item building = null; protected Item fire = null; protected boolean messedUp = false;
As in the gathering skill, we need to set up our
global variables to remember the object we are crafting, since it
will be constructed below in our invoke method, but not sent out
into the world until the unInvoke method. We also need a reference
to the fire object we are using, so we can watch and see if it
goes out.
public boolean tick( Tickable ticking, int tickID ) { if( ( affected != null ) && ( affected instanceof MOB ) && ( tickID == Tickable.TICKID_MOB ) ) { MOB mob = (MOB)affected; if( ( building == null ) || ( fire == null ) || ( !CMLib.flags().isOnFire( fire ) ) || ( !mob.location().isContent( fire ) ) || ( mob.isMine( fire ) ) ) { messedUp = true; unInvoke(); } } return super.tick( ticking, tickID ); }
Unlike the gathering skill, we will let the
superclass tick method do all the talking to the player. The only
job being done here is to make sure the fire didn't go out. If it
did, we declare the project messed up, and immediately unInvoke
it.
protected Vector loadRecipes() { return super.loadRecipes( "glassblowing.txt" ); }
Overriding the loadRecipes() method is absolutely
necessary for any crafting skill, as it specifies the recipes
list. A recipes list is a comma-delimited file where each row
is linefeed delimited. Each columns meaning is defined by the
constants above. The loadRecipes(String) method will automaticallyy
look in $coffeemud-install-path/resources/skills for the file
specified.
public void unInvoke() { if( canBeUninvoked() ) { if( ( affected != null ) && ( affected instanceof MOB ) ) { MOB mob = (MOB)affected;
if( ( building != null ) && ( !aborted ) ) { if( messedUp ) { commonTell( mob, CMStrings.capitalizeAndLower( building.name()) + " explodes!" ); } else { mob.location().addItemRefuse( building, Item.REFUSE_PLAYER_DROP ); } }
building = null; } } super.unInvoke(); }
Just like in the gathering skill, the unInvoke()
method is where we actually give our product to the world. Its
been saved in the 'building' variable reference, so its just a matter
of checking whether everything went ok and adding it to the room
if things did. Since common skills work by adding themselves as
affects on the player/mobs performing the skill, we need only check
the StdAbility.affected reference variable to find our
craftor.
public boolean invoke( MOB mob, Vector commands, Environmental givenTarget, boolean auto, int asLevel ) { int autoGenerate = 0;
if( ( auto ) && ( givenTarget == this ) && ( commands.size() > 0 ) && ( commands.firstElement() instanceof Integer ) ) { autoGenerate = ( (Integer)commands.firstElement() ).intValue(); commands.removeElementAt( 0 ); givenTarget = null; }
Crafting skills are all outfitted with the ability
to allow the rest of the system to use the skill class as an automatic
item generator. By convention, this is done by sending in a commands
vector (which is normally the parameter strings entered by a user)
with the first element as an Integer object instead. This object
then represents the material out of which the item should be made,
and maps back to one of the RawMaterial.RESOURCE_* constants.
randomRecipeFix( mob, addRecipes( mob, loadRecipes() ), commands, autoGenerate );
This complex line does the most important work
related to the autoGenerate flag we set above, and also to handle
the case where mobs are initiating this skill on their own.
Embedded are three method calls: one to get a vector of vectors
representing the skill row matrix (loadRecipes() ), another
to append to this vector any recipe Items in the mobs inventory
(addRecipes(..) ), and lastly, to randomly select
a recipe based on the autoGenerate flag or if the invoker is an
NPC (randomRecipeFix() ). randomRecipeFix()
can handle things like modifying the parameter list (commands) so
that it has the name of the random recipe selected.
if( commands.size() == 0 ) { commonTell( mob, "Make what? Enter \"glassblow list\" for a list." ); return false; }
if( ( !auto ) && ( commands.size() > 0 ) &&( ( (String)commands.firstElement() ).equalsIgnoreCase( "bundle" ) ) ) { bundling = true; if( super.invoke( mob, commands, givenTarget, auto, asLevel ) ) { return super.bundle( mob, commands ); }
return false; }
Now we can finally start dealing the the parameter
list typed by the player, or generated by the system. The parameter
list (minus the command invoking word) is in the "commands" vector
as a set of strings. The first thing we do is check the parameters
to see if they want to generate a bundle of raw resources. If they
do, we call a superclass helper method to do so and exit -- this
is identical to the way it was done in the gathering skill above.
Vector recipes = addRecipes( mob, loadRecipes() );
We're going to need that final recipes list so we
can search for the one the player entered into the command line.
Therefore we call loadRecipes() to get the list from
our text file, and then addRecipes to tack on any Recipe Items in
the mobs inventory. What comes back is our vector of vectors, representing
the rows and columns in the text file. Each vector in the 'recipes'
vector can be index by the RCP_* constants above.
String str = (String)commands.elementAt( 0 ); String startStr = null; bundling = false; int completion = 4;
Next we initialize a few important variables,
such as setting a variable for the first word in the parameters
(str), setting a default completion ticks (4), and initializing a
bundling flag to false. Even if the player did not explicitly use
the word bundle in the parameters, they may still have selected a
bundle item from the recipes list, so we need that flag to notify
the rest of the code of that fact.
if( str.equalsIgnoreCase( "list" ) ) { StringBuffer buf = new StringBuffer( CMStrings.padRight( "Item", 16 ) + " Lvl Sand required\n\r" );
for( int r = 0; r < recipes.size(); r++ ) { Vector V = (Vector)recipes.elementAt( r ); if( V.size() > 0 ) { String item = replacePercent( (String)V.elementAt( RCP_FINALNAME ), "" ); int level = CMath.s_int( (String)V.elementAt( RCP_LEVEL ) ); int wood = CMath.s_int( (String)V.elementAt( RCP_WOOD ) ); if( level <= mob.envStats().level() ) { buf.append( CMStrings.padRight( item, 16 ) + " " + CMStrings.padRight( "" + level, 3 ) + " " + wood + "\n\r" ); } } }
commonTell( mob, buf.toString() ); return true; }
If the player entered "list" as the first word in
the parameters, we display the recipes to them.
fire = getRequiredFire( mob, autoGenerate ); if( fire == null ) { return false; }
building = null; messedUp = false;
Now we know we are definitely building something,
so we can initialize some more variables, such as the global
messedUp flag and the global building reference item. The global
fire reference item is also set here by calling a superclass helper
method getRequiredFire(..) which also has the benefit
of giving the user an error message if it fails to find a fire to
use. The autoGenerate flag is sent in to tell the method to skip
the check if we are simply using the invoke method to automaticallyy
generate an item.
int amount = -1; if( ( commands.size() > 1 ) && ( CMath.isNumber( (String)commands.lastElement() ) ) ) { amount = CMath.s_int( (String)commands.lastElement() ); commands.removeElementAt( commands.size() - 1 ); }
Our next step is to check the parameters list to
see if the player is specifying how much raw material they want to
use in the construction of this item. The recipe list mentions the
minimum amount of material, but the user is allowed to specify
more if they like. We'll save this override amount and confirm its
validity later.
String recipeName = CMParms.combine( commands, 0 ); Vector foundRecipe = null; Vector matches = matchingRecipeNames( recipes, recipeName, true ); for( int r = 0; r < matches.size(); r++ ) { Vector V = (Vector)matches.elementAt( r ); if( V.size() > 0 ) { int level = CMath.s_int( (String)V.elementAt( RCP_LEVEL ) ); if( ( autoGenerate > 0 ) || ( level <= mob.envStats().level() ) ) { foundRecipe = V; break; } } }
Now we can actually get to the important part of
making sure that the recipe name entered by the players into the
command line is actually a valid recipe. We do that by combining
the remaining strings on the command line into a single variable
"recipeName", and then calling a superclass helper method called
"matchingRecipeNames" to generate a subset of the master recipe
list which matches that name. Once we have this list of matching
recipes, we can select the winner based on level (unless we are
performing an auto-generation, in which case level doesn't matter).
Our winning recipe is stored in a single vector of strings called
foundRecipe, which we can index by the RCP_* constants.
if( foundRecipe == null ) { commonTell( mob, "You don't know how to make a '" + recipeName + "'. Try \"glassblow list\" for a list." ); return false; }
Of course, if no recipe matched, or none met the
level requirements, our matched row vector will be null, in which
case we error out.
int woodRequired = CMath.s_int( (String)foundRecipe.elementAt( RCP_WOOD ) ); if( amount > woodRequired ) { woodRequired = amount; }
String misctype = (String)foundRecipe.elementAt( RCP_MISCTYPE ); bundling = misctype.equalsIgnoreCase( "BUNDLE" ); int[] pm = { RawMaterial.RESOURCE_SAND, RawMaterial.RESOURCE_CRYSTAL, RawMaterial.RESOURCE_GLASS };
int[][] data = fetchFoundResourceData( mob, woodRequired, "sand", pm, 0, null, null, bundling, autoGenerate ); if( data == null ) { return false; }
woodRequired = data[0][FOUND_AMT];
However, if we did find a matching row, its time to start
pulling information out of that row and start using it. The first
thing we check is the minimum required raw materials (woodRequired)
which can be modified by a larger "amount" variable by the player
if they entered one. We also draw out the general modifier string
"misctype", which can be used to specify parameters on the item
being generated. For instance, is the misctype column says
"bundle", we know that we are bundling raw resources and can set
our bundling flag to true.
The next step is a bit more complicated. The pm
array is constructed of RawMaterial.RESOURCE_* constants which
represent those material or resource types valid for making the
items in this common skill. Next we call the powerful superclass
method fetchFoundResourceData which will return to us the amount
and specific type of valid resources found, and give an error if
none are found. Of course it doesn't really matter with the
autoGenerate flag, so it will take that into account as
well. The returned array "data" has two dimensions, 0 for the
primary resource, and 1 for a secondary resource if applicable
(its not applicable here). Each dimension can be dereferenced using
superclass FOUND_* constants. For instance, we can get a final
"woodRequired" value from the data integer array.
if( !super.invoke( mob, commands, givenTarget, auto, asLevel ) ) { return false; }
Just like for gathering skills, the superclass
invoke method is called to take care of mana consumption, and to
make sure the person trying to do the crafting is not asleep or
dead.
int lostValue = destroyResources( mob.location(), woodRequired, data[0][FOUND_CODE], 0, null, autoGenerate );
building = CMClass.getItem( (String)foundRecipe.elementAt( RCP_CLASSTYPE ) );
if( building == null ) { commonTell( mob, "There's no such thing as a " + foundRecipe.elementAt( RCP_CLASSTYPE ) + "!!!" ); return false; }
Our next step is to call another superclass
method destroyResources() which will remove the resources from
the room which are used to construct the item. After that, we can
actually initialize the building item reference so we can begin
to fill it out. The building variable will remain referenced only
by the common skill class until the unInvoke() method
is called, when it will be given to the craftor.
completion = CMath.s_int( (String)foundRecipe.elementAt( RCP_TICKS ) ) - ( ( mob.envStats().level() - CMath.s_int( (String)foundRecipe.elementAt( RCP_LEVEL ) ) ) * 2 );
String itemName = replacePercent( (String)foundRecipe.elementAt( RCP_FINALNAME ), RawMaterial.RESOURCE_DESCS[ (data[0][FOUND_CODE]&RawMaterial.RESOURCE_MASK) ] ).toLowerCase();
if( bundling ) { itemName = "a " + woodRequired + "# " + itemName; } else { itemName = CMStrings.startWithAorAn( itemName ); }
building.setName( itemName ); startStr = "<S-NAME> start(s) blowing " + building.name() + "."; displayText = "You are blowing " + building.name(); verb = "blowing " + building.name(); playSound = "fire.wav"; building.setDisplayText( itemName + " is here" ); building.setDescription( itemName + ". " ); building.baseEnvStats().setWeight( woodRequired ); building.setBaseValue( CMath.s_int( (String)foundRecipe.elementAt( RCP_VALUE ) ) );
Now that we have a reference to the item object
we are building, we can start filling in some fields, such as its
name, display text, weight, and base value, usually from columns in
our recipe text file. We can also derive the final completion
variable, representing the number of ticks it will take to complete
the item.
if( data[0][FOUND_CODE] == RawMaterial.RESOURCE_SAND ) { building.setMaterial( RawMaterial.RESOURCE_GLASS ); } else { building.setMaterial( data[0][FOUND_CODE] ); }
building.baseEnvStats().setLevel( CMath.s_int( (String)foundRecipe.elementAt( RCP_LEVEL ) ) );
building.setSecretIdentity( "This is the work of " + mob.Name() + "." ); int capacity = CMath.s_int( (String)foundRecipe.elementAt( RCP_CAPACITY ) ); String spell = ( foundRecipe.size() > RCP_SPELL ) ? ( (String)foundRecipe.elementAt( RCP_SPELL ) ).trim() : "";
addSpells( building, spell );
Continuing with our mission of filling out the
building object, we set the material type, level, and secret
identity. We also add any properties to it that are listed in the
recipe text file by calling the superclass helper method addSpells() .
There is also a column in the recipe file for the capacity of the
item if it is a container.
if( building instanceof Container ) { if( capacity > 0 ) { ( (Container)building ).setCapacity( capacity + woodRequired ); }
if( misctype.equalsIgnoreCase( "LID" ) ) { ( (Container)building ).setLidsNLocks( true, false, false, false ); } else if( misctype.equalsIgnoreCase( "LOCK" ) ) { ( (Container)building ).setLidsNLocks( true, false, true, false ); ( (Container)building ).setKeyName( new Double( Math.random() ).toString() ); }
( (Container)building ).setContainTypes( Container.CONTAIN_ANYTHING ); }
If, in fact, the item we are building is a container,
we'll set the capacity of the container, and use our general purpose
"misctype" column from the table to determine whether a lit or
lock needs to be made.
if( building instanceof Drink ) { if( CMLib.flags().isGettable( building ) ) { ( (Drink)building ).setLiquidRemaining( 0 ); ( (Drink)building ).setLiquidHeld( capacity * 50 ); ( (Drink)building ).setThirstQuenched( 250 );
if( ( capacity * 50 ) < 250 ) { ( (Drink)building ).setThirstQuenched( capacity * 50 ); } } }
if( bundling ) { building.setBaseValue( lostValue ); }
If the item is a Drinkable, then we have a few
more fields to fill out, based on the capacity column. The last
line makes sure that the gold value of the item reflects the sum of
the materials used, in case we are bundling.
building.recoverEnvStats(); building.text(); building.recoverEnvStats();
The filling out of the item is complete, so we
call recoverEnvStats() , populate the miscText field if the
item is Generic, and then recover the env stats again in case that
changed anything.
messedUp =! proficiencyCheck( mob, 0, auto ); if( completion < 4 ) { completion = 4; }
if( bundling ) { messedUp = false; completion = 1; verb = "bundling " + RawMaterial.RESOURCE_DESCS[ ( building.material()&RawMaterial.RESOURCE_MASK) ].toLowerCase();
startStr = "<S-NAME> start(s) " + verb + "."; displayText = "You are " + verb; }
Now we can set our messedUp flag based on a
proficiency check, and change our common skill player-message strings
based on whether we are bundling.
if( autoGenerate > 0 ) { commands.addElement( building ); return true; }
If the invoke method was called simply to generate
the item, we are done -- we can add the item to the commands vector
(where it will be expected), and return.
CMMsg msg = CMClass.getMsg( mob, building, this, CMMsg.MSG_NOISYMOVEMENT, startStr );
if( mob.location().okMessage( mob, msg ) ) { mob.location().send( mob, msg ); building = (Item)msg.target(); beneficialAffect( mob, mob, asLevel, completion ); } else if( bundling ) { messedUp = false; aborted = false; unInvoke(); } return true; } }
Last but not least, we do exactly what we did in
our gathering skill, namely generate a message for this event, and
add the common skill as an effect to the invoker of it. The
completion time is used to determine how long it will take to
generate the item.
public class Poison_BeeSting extends com.planet_ink.coffee_mud.Abilities.Poisons.Poison { public String ID() { return "Poison_BeeSting"; }
public String name() { return "Bee Sting"; }
The Poison base class in the Poisons package provides
a tidy template upon which your more common, mundane poisons can
be based. The first three methods are those required by any Environmental
class. The fact that none of the other preliminary methods normally
found in skill abilities are present is a testimony to the
homogeneous nature of Poisons.
private static final String[] triggerStrings = { "POISONSTING" }; public String[] triggerStrings() { return triggerStrings; }
Poisons have the feature of having their own trigger commands,
meaning they can be added as attack abilities to monsters (like
a Bee in this case).
protected String POISON_DONE() { return "The stinging poison runs its course."; }
protected String POISON_START() { return "^G<S-NAME> turn(s) green.^?"; }
protected String POISON_CAST() { return "^F<S-NAME> sting(s) <T-NAMESELF>!^?"; }
protected String POISON_FAIL() { return "<S-NAME> attempt(s) to sting <T-NAMESELF>, but fail(s)."; }
In this set of methods, strings are defined for the invocation
of the poison, all the way through its recovery string.
protected int POISON_TICKS() { return 10; // 0 means no adjustment! }
Poisons can have a variable duration dependent upon level
(by returning 0) or can have a set duration on the poisoned creature.
protected int POISON_DELAY() { return 2; }
protected int POISON_DAMAGE() { return ( invoker != null ) ? 2 : 0; }
protected String POISON_AFFECT() { return "<S-NAME> cringe(s) from the poisonous itch."; }
Poisons can also be set to cause the poisoned creature
to emote on a regular basis, and to take damage as well. Here you
can see three methods being used to designate how often the emote/damage
cycle occurs (in ticks) as well as the amount of damage, and the
text of the emote.
public void affectCharStats( MOB affected, CharStats affectableStats ) { affectableStats.setStat( CharStats.STAT_CONSTITUTION, affectableStats.getStat( CharStats.STAT_CONSTITUTION ) - 1 );
affectableStats.setStat( CharStats.STAT_STRENGTH, affectableStats.getStat( CharStats.STAT_STRENGTH ) - 1 );
if( affectableStats.getStat( CharStats.STAT_CONSTITUTION ) <= 0 ) { affectableStats.setStat( CharStats.STAT_CONSTITUTION, 1 ); }
if( affectableStats.getStat( CharStats.STAT_STRENGTH ) <= 0 ) { affectableStats.setStat( CharStats.STAT_STRENGTH, 1 ); } } }
And lastly, as described under the Core Topic on state
affects, we have the code which lowers the poisoned creatures constitution
and strength while the poison is in effect. We even have a few lines
to make sure the values don't fall below 0.
Like Poison, the Disease base class in the Diseases package
provides a tidy template upon which your more common, mundane
diseases can be based.
public class Disease_Cold extends com.planet_ink.coffee_mud.Abilities.Diseases.Disease { public String ID() { return "Disease_Cold"; }
public String name() { return "Cold"; }
public String displayText() { return "(Cold Virus)"; }
protected int canAffectCode() { return CAN_MOBS; }
protected int canTargetCode() { return CAN_MOBS; }
public int abstractQuality() { return Ability.QUALITY_MALICIOUS; }
public boolean putInCommandlist() { return false; }
The Disease base class provides many important base services
that allows CoffeeMud to interact properly with your disease. For
this reason, making a disease that extends the Disease base class
is highly recommended. In the first half dozen methods, we see the
important values of a standard skill being defined. See the
introduction to Skill Abilities if you need more details on these
methods.
protected String DISEASE_DONE() { return "Your cold clears up."; }
protected String DISEASE_START() { return "^G<S-NAME> come(s) down with a cold.^?"; }
In this set of methods, strings are defined for the invocation
of the disease as well as its recovery string.
protected int DISEASE_TICKS() { return 24; }
Diseases can have a variable duration dependent upon level
(by returning 0) or can have a set duration on the diseased creature.
protected int DISEASE_DELAY() { return 5; }
protected String DISEASE_AFFECT() { return "<S-NAME> sneeze(s). AAAAAAAAAAAAAACHOOO!!!!"; }
Diseases can also be set to cause the poisoned creature
to emote on a regular basis. Here you can see a pair of methods
being used to designate how often the emote/damage cycle occurs
(in ticks) as well as the text of the emote. Any damage or other
consequences of the disease are defined later.
public int abilityCode() { return DiseaseAffect.SPREAD_CONSUMPTION |DiseaseAffect.SPREAD_PROXIMITY |DiseaseAffect.SPREAD_CONTACT |DiseaseAffect.SPREAD_STD; }
This important method is used to define how the disease
is spread. Some aspects of disease catching must be coded by the
programmer of the disease. Some aspects are handled by the engine,
while most are handled by the Disease base class. This method is
used to coordinate the efforts on behalf of the disease by the CoffeeMud
engine as a whole. Different bit values are defined by the
DiseaseAffect interface.
public boolean tick( Tickable ticking, int tickID ) { if( !super.tick( ticking,tickID ) ) { return false; }
if( affected == null ) { return false; }
if( !( affected instanceof MOB ) ) { return true; }
MOB mob = (MOB)affected; MOB diseaser = invoker; if( diseaser == null ) { diseaser = mob; }
If a disease does anything during its delay cycle (defined
above by DISEASE_DELAY() ), then the programmer must
implement a tick method to take care of this functionality. These
first few lines of the method are error and state checking, as well
as defining some useful variables, such as who caused the disease
(diseaser), and who has the disease (mob)
if( ( getTickDownRemaining() == 1 ) &&( CMLib.dice().rollPercentage() > mob.charStats().getSave( CharStats.STAT_SAVE_COLD ) ) &&( CMLib.dice().rollPercentage() < 25 - mob.charStats().getStat( CharStats.STAT_CONSTITUTION ) ) &&( !mob.amDead() ) &&( !mob.isMonster() ) ) { mob.delEffect( this ); Ability A = CMClass.getAbility( "Disease_Pneumonia" ); A.invoke( diseaser, mob, true ); }
One of the things we have decided to do in the disease
Cold is to make it so that, if the creature does not cure the disease
before it expires naturally, they will almost certainly catch Pneumonia.
By checking getTickDownRemaining() , we check out
counter which runs downwards from DISEASE_TICKS()
to 0. At a value of 1, we make a saving throw check for the creature
with the cold, and potentially give them another disease to enjoy
when the Cold expires 1 tick later.
else if( ( !mob.amDead() ) && ( ( --diseaseTick ) <= 0 ) ) { diseaseTick = DISEASE_DELAY(); mob.location().show( mob, null, CMMsg.MSG_NOISE, DISEASE_AFFECT() ); if( mob.curState().getHitPoints() > ( ( 2 * diseaser.envStats().level() ) + 1 ) ) { int damage = CMLib.dice().roll( 2, diseaser.envStats().level(), 1 ); CMLib.combat().postDamage( diseaser, mob, this, damage, CMMsg.MASK_ALWAYS|CMMsg.TYP_DISEASE, -1, null ); }
catchIt( mob ); return true; } return true; }
The last thing we do in this method is, every DISEASE_DELAY()
ticks, we emote our DISEASE_AFFECT string, and then allow the creature
to take a few points of disease damage by calling the
MUDFight.postDamage() method. After this, we also call
the catchIt(MOB mob) method up in the Disease base
class. This important method spreads the disease among those in
the same room as the creature, thereby handling any diseases spread
by proximity as defined in abilityCode() above.
public void affectCharStats( MOB affected, CharStats affectableStats ) { if( affected == null ) { return; }
affectableStats.setStat( CharStats.STAT_CONSTITUTION, affectableStats.getStat( CharStats.STAT_CONSTITUTION ) - 2 );
affectableStats.setStat( CharStats.STAT_STRENGTH, affectableStats.getStat( CharStats.STAT_STRENGTH ) - 3 );
if( affectableStats.getStat( CharStats.STAT_CONSTITUTION ) <= 0 ) { affectableStats.setStat( CharStats.STAT_CONSTITUTION, 1 ); }
if( affectableStats.getStat( CharStats.STAT_STRENGTH ) <= 0 ) { affectableStats.setStat( CharStats.STAT_STRENGTH, 1 ); } } }
And lastly, as described under the Core Topic on state
affects, we have the code which lowers the diseased creatures constitution
and strength while the disease is in effect. We even have a few
lines to make sure the values don't fall below 0.
Traps and Bombs are so similar, that the differences
are not worth mentioning. However, although they are classified
as skill abilities, they are different from normal Skill Abilities
mentioned above in that they rely very heavily on their superclass
bases, which are com.planet_ink.coffee_mud.Abilities.Traps.StdTrap,
and com.planet_ink.coffee_mud.Abilities.Traps.StdBomb
respectively. Making a proper and compliant trap is helped considerably
by following that advice. Otherwise, so long as your trap implements
the com.planet_ink.coffee_mud.Abilities.interfaces.Trap interface,
it will be ok.
public class Trap_ElectricShock extends com.planet_ink.coffee_mud.Abilities.Traps.StdTrap { public String ID() { return "Trap_ElectricShock"; }
public String name() { return "electric shock"; }
If you've read the Skill Abilities section above,
you will recognize these methods and their importance. As always,
the ID() must match the class name, while the name()
can be whatever you want to call the skill.
protected int canAffectCode() { return Ability.CAN_ITEMS|Ability.CAN_EXITS; }
protected int canTargetCode() { return 0; }
It is very important, even more so than normal
skills, to make sure that your StdAbility canAffectCode()
method returns a proper value. Some traps can be placed on rooms
(floors), and some on items, and others on exits. Make sure your
method informs StdAbility what your trap will do.
protected int trapLevel() { return 19; }
The trapLevel() is the class level of your
trap. In general, a player needs this many levels in their trap-laying
class before they can create this trap.
public String requiresToSet() { return "10 pounds of metal"; }
This is another trap-specific method which is
used by the Set Traps skill to inform the user of what kinds of
extra-materials are required by the user before they can lay this
particular trap. It will be enforced below in the canSetTrap and
setTrap methods.
public Trap setTrap( MOB mob, Environmental E, int trapBonus, int qualifyingClassLevel ) { if( E == null ) { return null; }
if( mob != null ) { Item I = findMostOfMaterial( mob.location(), RawMaterial.MATERIAL_METAL ); if( I != null ) { super.destroyResources( mob.location(), I.material(), 10 ); } } return super.setTrap( mob, E, trapBonus, qualifyingClassLevel ); }
This method is used to actually set the trap,
well before the trap is sprung. Springing is handles by the StdTrap
superclass system, and is based on what is returned by the
canAffectCode() method. The setTrap method received the mob/player
setting the trap, the item onto which the trap is being set (E
-- I know, very descriptive; sue me), the trapBonus of the trap
setter, if any, and at what level they first qualified for this
trap. In this particular method, we use some helper methods from
StdTrap to find all of the metal in the room, and then use another
StdTrap superclass method to remove the found metal from the room.
findMostOfMaterial() will look for any raw material Metals
in the room, and return an example Item representing the specific
type (bronze, iron, gold) of metal that is most numerous in the
room. The destroyResources() method then will remove
10 of those from the room.
public boolean canSetTrapOn( MOB mob, Environmental E ) { if( !super.canSetTrapOn( mob, E ) ) { return false; }
if( mob != null ) { Item I = findMostOfMaterial( mob.location(), RawMaterial.MATERIAL_METAL ); if( ( I == null ) || ( super.findNumberOfResource( mob.location(), I.material() ) < 10 ) ) { mob.tell( "You'll need to set down at least 10 pounds of metal first." ); return false; } } return true; }
This method is called before the setTrap()
method to determine whether the given player/mob is allowed or able
to set the trap on the given object (E). Just as we did in setTrap
above, we use findMostOfMaterial() to determine what
the most numerous metal resource on the room is, then call another
StdTrap superclass method, findNumberOfResource() ,
to make sure there is enough of this particular metal resource to
do the job. If not, we inform the user and return false from the
method. If everything is fine, we return true. Notice that we were
careful to call the superclass version of this method -- it does
important things for us also, so it should definitely be called!
public void spring( MOB target ) { if( ( target != invoker() ) && ( target.location() != null ) ) { if( ( !invoker().mayIFight( target ) ) || ( CMLib.dice().rollPercentage() <= target.charStats().getSave( CharStats.STAT_SAVE_TRAPS) ) ) { target.location().show( target, null, null, CMMsg.MASK_ALWAYS|CMMsg.MSG_NOISE, "<S-NAME> avoid(s) setting off a shocking trap!" ); } else if( target.location().show( target, target, this, CMMsg.MASK_ALWAYS|CMMsg.MSG_NOISE, "<S-NAME> set(s) off an shocking trap!" ) ) { super.spring( target ); CMLib.combat().postDamage( invoker(), target, null, CMLib.dice().roll( trapLevel(), 8, 1 ), CMMsg.MASK_ALWAYS|CMMsg.TYP_ELECTRIC, Weapon.TYPE_STRIKING, "The shock <DAMAGE> <T-NAME>!" + CMProps.msp( "shock.wav",30 ) );
if( ( canBeUninvoked() ) && ( affected instanceof Item ) ) { disable(); } } } } }
The spring method is where the action is at. If the code
in StdTrap determines that the trap has been sprung, then this method
will be called with only one parameter, namely the mob who sprung
it. After making sure we aren't springing the trap on the thief
who set it, we make sure that the thief who set the trap is not
breaking PK rules by hurting the player. We do this using the invoker()
method from StdAbility, which will return a reference to the thief
MOB who set the trap. We also give the poor bloke who sprung the
trap a saving throw. If both checks fail, we let the room know
that the trap has gone off. Notice that it is inside this condition
that we call super.spring() . This is because, normally,
the spring method is only supposed to hurt people. Since we have
lots of conditions by which the mob can get out of being hurt,
we need to wrap our call to super.spring() . Anyway,
since the trap has gone off, we make a library call to the combat()
engine's postDamage() method. That method has lots
of very complicated parameters, so you'll have to read up on the
Library section for more information on it, but suffice to say
that it posts the damage message that takes hit points away from
the player. Lastly, if the trap is allowed to be uninvoked, we
call the special StdTrap disable() method, which makes
the trap unviable FOREVER. It effectively destroys the trap.
Of all skills, you will find that Languages are the easiest
to code. Although they fall into the category of Ability, and are
a far derivative of StdAbility, you will need very little of that
extended knowledge if you follow the K.I.S.S. principle and use
the simple template provided by the Language base class in the
Languages package.
public class Elvish extends com.planet_ink.coffee_mud.Abilities.Languages.Language { public String ID() { return "Elvish"; }
public String name() { return "Elvish"; }
Like all Abilities, the Language requires an ID which
is the same as its class name, and a readable name. In the case
of Elvish, they are identical.
private static boolean mapped = false; public Elvish() { super(); if( !mapped ) { mapped = true; CMAble.addCharAbilityMapping( "All", 1, ID(), false ); } }
The constructor of this language is worth noting. Under
the section on Character Classes, we learned how to make classes
qualify for skills. Languages, like Common Skills, are available
to ALL classes. For this reason, we save ourselves the trouble by
having the language declare its qualifications itself by accessing
the same CMAble method we saw up in Character Class creation.
public static Vector wordLists = null; public Vector translationVector() { if(wordLists==null) { String[] one = { "a", "e", "i", "o", "á", "é", "í", "ó" };
String[] two = { "os", "vi", "ne", "vo", "li", "eh", "no", "ai", "by", "et", "ce", "un", "il" };
String[] three = { "ána", "cil", "sar", "tan", "hel", "loa", "sir", "hep", "yur", "nol", "hol", "qua", "éth" };
String[] four = { "séya", "qual", "quel", "lara", "uqua", "sana", "yava", "masse", "yanna", "quettaparma", "manna", "manan", "merme", "carma", "harno", "harne", "varno", "essar", "saira", "cilta", "veuma", "norta", "turme", "saita" };
String[] five = { "cuiva", "cuina", "nonwa", "imire", "nauta", "cilta", "entuc", "norta", "latin", "lòtea", "veuya", "veuro", "apama", "hampa", "nurta", "firta", "saira", "holle", "herwa", "uquen", "arcoa", "calte", "cemma", "hanta", "tanen" };
String[] six = { "mahtale", "porisalque", "hairie", "tararan", "ambarwa", "latina", "olòtie", "amawil", "apacen", "yavinqua", "apalume", "linquilea", "menelwa", "alassea", "nurmea", "parmasse", "ceniril", "heldasse", "imirin", "earina", "calatengew", "lapselunga", "rianna", "eneques" };
wordLists=new Vector(); wordLists.addElement( one ); wordLists.addElement( two ); wordLists.addElement( three ); wordLists.addElement( four ); wordLists.addElement( five ); wordLists.addElement( six ); } return wordLists; }
The first, often only, and certainly most important Language
method is translationVector() , which returns a Vector
object containing a set of String arrays. Each String array is designated
by the size of the Common language words which will be used by
the array for translation. That is to say, whenever a Common word
of three letters is being translated into Elvish, the String array
three will have a word randomly chosen from it.
The words which are placed into these string arrays is
arbitrary, and may actually contain as many or few letters as you
like. Keeping the number of letters close or the same as the Common
word equivalent provides a certain symmetry, however, and makes
it fun for the players who don't speak a language to try and guess
what is being said by counting letters.
private static final Hashtable hashwords = new Hashtable(); public Hashtable translationHash() { if( ( hashwords != null ) && ( hashwords.size() > 0 ) ) { return hashwords; }
hashwords.put( "ABANDON", "avarta" ); hashwords.put( "ABLE", "pol" ); hashwords.put( "ACCOMMODATE", "camta" ); hashwords.put( "ACT", "car" );
...
hashwords.put( "WRONG", "raicë" ); hashwords.put( "YES", "yé" ); hashwords.put( "YESTERDAY", "tellaurë" ); return hashwords; } }
The last Language method is translationHash() .
This method is entirely optional and may be left out of a language
definition. In fact, it usually IS left out. However, when it
is not, it provides a hashtable which can be used to translate
exact english matches back to the fantasy language. The First
word in each hashtable entry is the English word (and it MUST
MUST MUST be in UPPERCASE) as the key, and the translation word
as the entry value. Correct casing is taken care of by the Language
base class.
|