Integrated Dice Roll Weapon Damage (How to add customisable attributes for objects/C# Basics)

Status
Not open for further replies.

Moody

Citizen
So @The Real Keith and I worked through this today, figured I'd share a step-by-step integration tutorial with the community of how we did this. If you're not interested in a dice system but just want to learn about how to add attributes that you can toggle from ingame, keep reading too!

Our first file that we need to look at is BaseWeapon.cs. Within this file is the BaseWeapon class which is the fundamental class for all weapons within the game. All weapons in the game will inherit the BaseWeapon class, if not directly, then indirectly. You can tell what classes something inherits from by looking at the line where the class is initially declared. For BaseWeapon this looks like this.

Code:
public abstract class BaseWeapon : Item, IWeapon, IFactionItem, ICraftable, ISlayer, IDurability, ISetItem

Basically this is telling us that BaseWeapon inherits from Item, IWeapon, IFactionItem etc. Inheritance means that when we start with a blank slate, our blank slate for BaseWeapon will contain all the functions and variables found within the inherited classes. Hence their name... Logic!

To prove how this looks, let's follow the chain down from Katana to BaseWeapon!

Code:
public class Katana : BaseSword
public abstract class BaseSword : BaseMeleeWeapon
public abstract class BaseMeleeWeapon : BaseWeapon

See how it works? So Katana is a Sword which is a Melee Weapon which is a Weapon. Ok, let's move on!

So how do we get base damage right now? Let's look at the following segment in BaseWeapon

Code:
public virtual double GetBaseDamage(Mobile attacker)
{
            int min, max;
            GetBaseDamageRange(attacker, out min, out max);
            int damage = Utility.RandomMinMax(min, max);
            if (Core.AOS)
            {
                        return damage;
            }
            if (m_DamageLevel != WeaponDamageLevel.Regular)
            {
                        damage += (2 * (int)m_DamageLevel) - 1;
            }
            return damage;
}

So what's going on here? It looks like we're getting what our maximum and minimum damage are based on the attacker from line 4, followed by coming up with a random number between the minimum and maximum base damage. AHA! This is where we need to go! Following that there are some checks as to whether or not it's the AoS expansion followed by a damage multiplier for a weapon damage level, but we can ignore that for now.

So, while we're here. Why do we want to change from a random min/max roll to a dice roll? It's all statistics! Basically a random number generator will spit out random numbers between two limits. This gives all numbers between the limits an equal opportunity of coming up. What this leads to ingame is very varied and inconsistent damage. If the range is big, say 5-50, then a player could hit for 7 in one strike, followed by a 47 etc. While it makes for varied gameplay, it can be a tad unpredictable.

Let's look at the distribution of a 1d6 roll (1 dice with 6 sides):
ai.imgur.com_Pnaa6MN.png
See how it's even. Nothing interesting here. This is what a random number distribution looks like.

But what happens when we move on to a 2d6 (2 dice with 6 sides):
ai.imgur.com_HOn5AbZ.png
Now things are getting interesting. Do you see how you're much more likely to get a 7 with that? It gives much better consistency, but still has a chance of going high/low. You'll see that the pattern falls into a bell curve as we increase the number of die:

ai.imgur.com_5bzJo5M.png

ai.imgur.com_5pSYtCP.png

So how do we do this? Well first let's declare some variables for our new system
Code:
private int m_NumDice, m_DiceSides, m_Offset;
and set them to invalid values
Code:
public BaseWeapon(int itemID) : base(itemID)
{
...
	m_MinDamage = -1;
	m_MaxDamage = -1;
	m_NumDice = 0;
	m_DiceSides = 0;
	m_Offset = -1;
...
Awesome. Now our BaseWeapon class has three new variables to play with! These variables will be used for the individual items (as in a specific katana) as opposed to a class (all katanas). So let's set up some overwriteable functions so that we can set a default value for all katanas.

Code:
public virtual int NumDice { get { return 0; } }
public virtual int DiceSides { get { return 0; } }
public virtual int Offset { get { return 0; } }
Remember how we talked about inheritance before? Well, with inheritance you're able to override inherited virtual functions. So we set up three virtual functions here that we'll overwrite in our weapon class. We just have to set them to invalid values so that we know when they've been overwritten and when they haven't!

So now it's time to create our accessing functions that will help us get and set the correct values for all the dice values. These will be called rather than the values directly because we need to prioritise the specific item value (the one with the m_ at the beginning) over the generic one that will be overridden by the weapon class. Now we don't want to have to check every single time about whether or not m_NumDice is set, so let's create some accessor functions that can do that for us!

Code:
[CommandProperty(AccessLevel.GameMaster)]
public int Dice_Sides
{
	get {return (m_DiceSides <= 0 ? DiceSides : m_DiceSides);}
	set {m_DiceSides=value; InvalidateProperties();}
}

[CommandProperty(AccessLevel.GameMaster)]
public int Dice_Offset
{
	get {return (m_Offset < 0 ? Offset : m_Offset);}
	set {m_Offset=value; InvalidateProperties();}
}
   
[CommandProperty(AccessLevel.GameMaster)]
public int Dice_Amount
{
	get {return (m_NumDice <= 0 ? NumDice : m_NumDice);}
	set {m_NumDice=value; InvalidateProperties();}
}
Basically, what these functions do in their get is that they check if the internal variable is set to an invalid value, if that's the case, they return the default value for the class, which should be overridden by the weapon class.

The set value on the other hand just sets the internal value if there's a line of code such as
Code:
Dice_Amount = 5;
These same functions are also called from within the [props menu ingame due to the flag we set just before each of them. The access level for the variables can be changed too. Awesome!

Let's fix up our MinDamage and MaxDamage functions so that they'll be informative too! Right now they look like this:
Code:
[CommandProperty(AccessLevel.GameMaster)]
public int MinDamage
{
	get { return (m_MinDamage == -1 ? Core.AOS ? AosMinDamage : OldMinDamage : m_MinDamage); }
	set
	{
		m_MinDamage = value;
		InvalidateProperties();
	}
}

[CommandProperty(AccessLevel.GameMaster)]
public int MaxDamage
{
	get { return (m_MaxDamage == -1 ? Core.AOS ? AosMaxDamage : OldMaxDamage : m_MaxDamage); }
	set
	{
		m_MaxDamage = value;
		InvalidateProperties();
	}
}

We have no real need for MinDamage and MaxDamage anymore, so we'll just set them to be informative values now so GM's can take a gander quickly to see what the possible range of a weapon is. So we gut the "set" part out of both functions, and add some custom gets that calculate the min and max dice roll damage.

Code:
[CommandProperty(AccessLevel.GameMaster)]
public int MinDamage
{
	get {return (Dice_Amount + Dice_Offset);}
}

[CommandProperty(AccessLevel.GameMaster)]
public int MaxDamage
{
	get {return (Dice_Amount*Dice_Sides + Dice_Offset);}
}

Simple maths! I'll let you figure out what's going on in there :)

Now we can go and change our GetBaseDamage function a bit. We can use the RunUO included Utility.Dice(amount, sides, offset) function to do the maths for us. Here's the code:
Code:
public virtual double GetBaseDamage(Mobile attacker)
{
	int min, max;
	int damage = 0;
          
	if (attacker is BaseCreature)
	{
		GetBaseDamageRange(attacker, out min, out max);
		damage = Utility.RandomMinMax(min, max);
	} else {
		damage=Utility.Dice(Dice_Amount, Dice_Sides, Dice_Offset);
	}
...

Oh! I almost forgot! Now we need to serialize and deserialize our internal variables! The reason we do this is because this is the way that ServUO stores items in its memory. So let's find the Serialize function and add to the top of it!
Code:
public override void Serialize(GenericWriter writer)
{
	base.Serialize(writer);

	writer.Write(12); // version
       
	writer.Write(m_Offset);
	writer.Write(m_DiceSides);
	writer.Write(m_NumDice);

	// Version 11
	writer.Write(m_TimesImbued);
...
The first thing we did was we incremented the version by a value of 1. This is so that current items on the server dont cause any errors when we try to look for values in them that arent there. The next thing we did was add each of the variables to the Serialize function. Keep the order of them handy, because when we DeSerialize them we need to know how they come out!
Code:
public override void Deserialize(GenericReader reader)
{
	base.Deserialize(reader);

	int version = reader.ReadInt();

	switch (version)
	{
		case 12: //New Dice Damage System -Moody
		{
			m_Offset = reader.ReadInt();
			m_DiceSides = reader.ReadInt();
			m_NumDice = reader.ReadInt();
			goto case 11;
		}
		case 11:
		{
			m_TimesImbued = reader.ReadInt();
			goto case 10;
		}
...
The Deserialize function is what gets called when ServUO wants to read the item from memory. So, to begin with it checks the version then skips down to the appropriate one. We added a new case for version 12 since that's our new version, and we included the variables in the same exact order we placed them into the Serialize function.

And with that, we're done with our base class! Time to add the default values in each of the weapon classes. For the sake of this tutorial, we'll only do the Katana, but you'll get the basic idea! It's very easy.

According to UO:R era stratics, a Katana rolls 3d8 with an offset of 2. So Inside the Katana class of Katana.cs we simply add the following three lines.
Code:
public override int NumDice { get { return 3; } }
public override int DiceSides { get { return 8; } }
public override int Offset { get { return 2; } }
Note the "override" keyword. This means that the original functions containing 0's inside the BaseWeapon class will never be called if the weapon is a Katana. Instead, we get the above three functions.

AND THAT'S IT! When we go ingame, this is what we see
ai.imgur.com_rZvww8R.png

Isn't that nice? :)
 
Last edited:
Status
Not open for further replies.