Lesson 16.3: Complex attack initiative and hit success logic

Now that we have our Battle class, let’s make the combat logic more interesting.

We’ll start by adding a Dexterity property to the LivingEntity class. We can use that to determine who attacks first and whether they hit or miss.

Then, we will create a new CombatService class and make a lot of small changes in several classes – eleven if I counted correctly.

 

 

 

Lesson Steps

 

Step 1: Modify \Engine\Models\LivingEntity.cs

On line 12, add the backing variable “_dexterity”, then add the “Dexterity” property on lines 31-39.

Notice that I’m using an expression-bodied lambda “=>” for the “get”. I changed the other “get”s in this class to use the same style (lines 23, 43, 53, 63, 73, and 93). This doesn’t do anything different from the previous way we wrote “get”s. But it’s what the cool kids are doing nowadays. So, we’ll do it too.

On lines 141 we’ll add a new “dexterity” parameter to the constructor and set the “Dexterity” property to that parameter value on line 145.

 

LivingEntity.cs

 

 

Step 2: Modify \Engine\Models\Player.cs

Because LivingEntity is the base class for the Player and Monster classes, we need to modify their constructors to pass in (and get) dexterity values in their constructors.

Modify the Player constructor on lines 45-47 so it receives a “dexterity” parameter and passes it to its base class – Living Entity.

While we’re in the Player class, let’s change the “get”s on line 16 and 26 to use lambdas.

 

Player.cs

 

 

Step 3: Modify \Engine\Models\Monster.cs

Change the constructor to accept a “dexterity” parameter and pass it to LivingEntity.

We also need to change the Clone function to pass in the dexterity parameter. This is on lines 39-41.

 

Monster.cs

 

 

Step 4: Modify \Engine\GameData\Monsters.xml

We need a source for the monsters’ dexterity values, so we’ll modify the Monsters.xml file.

Instead of adding another attribute to the Monster node, I created a new child node “<Dexterity>”. We’re going to add a lot more to the Monster objects, and I don’t want a long list of attributes in the Monster node. So, we’ll switch to using child nodes for properties.

For the values, I’m using the traditional Dungeons and Dragons values of 3-18 (like adding up three six-sided dice rolls).

 

Monsters.xml

 

 

Step 5: Modify \Engine\Factories\MonsterFactory.cs

Now that we have a Dexterity node in Monsters.xml, we need to modify the MonsterFactory to read it and pass the value to the constructor.

We need to add “using System;” to the using directives at the top, so we can convert the XML text value to an integer.

In the Monster constructor, add line 50 to read the Dexterity node’s value from the XML file, convert it to an integer, and pass it into the Monster constructor.

 

MonsterFactory.cs

 

 

Step 6: Modify \Engine\Models\Trader.cs

Since the Trader is a child of LivingEntity, we need to modify its constructor to pass in a “dexterity” value to LivingEntity’s constructor.

Because traders currently don’t fight, we’ll just pass in a hard-coded 18 for their dexterity.

 

Trader.cs

 

 

Step 7: Modify \Engine\ViewModels\GameSession.cs

The last place we instantiate a LivingEntity object is in GameSession, when we instantiate the Player object.

For now, we’ll just add a random number for the Player’s Dexterity value. Eventually, we’ll create a Player creation screen that will handle the player’s attributes, name, etc.

 

GameSession.cs (lines 120-140)

 

 

Step 8: Create \Engine\Services\CombatService.cs

This is where we’ll put the combat calculations.

I moved the Combatant enum into here, since we’ll return an enum value from the FirstAttacker function we’re moving into CombatService.

On lines 13-26, we have the new FirstAttacker() function. The formula is a modification I found to one on the internet. It scales, based on the difference in dexterity of the player and the monster. We also add a random value between -10 and +10 on line 20, to make the combat a little more interesting.

The same formula is used for the hit/miss function AttackSucceeded() – on lines 28-40. When we add more things that affect combat (skills, armor, etc.), we’ll modify this function.

 

CombatService.cs

 

 

Step 9: Modify \Engine\Models\Battle.cs

Now, we can finally modify the combat logic, since everything has a Dexterity value.

Delete the Combatant enum and the FirstAttacker function (lines 13-23), since we’re going to get those from CombatService.

Then, change line 27 in the constructor, to call CombatService.FirstAttacker()

 

Battle.cs

 

 

Step 10: Modify \Engine\Actions\AttackWithWeapon.cs

This class used to generate a random damage amount and treat zero as a miss. Now, in the Execute() function we see if the attack succeeded on line 39. If so, we determine the amount of damage to do. If the attack fails, we display the “missed” message on line 49.

 

AttackWithWeapon.cs

 

 

Step 11: Modify \Engine\Data\GameItems.xml

Now that we aren’t using zero damage to indicate a “miss”, we should go into GameItems.xml and set anything with a MinimumDamage of “0” to “1”. Otherwise, an attacker could succeed (based on the CombatSerice.AttackSucceeded function) but do zero damage. That doesn’t make sense to me.

 

GameItems.xml

 

 

Step 13: Modify \WPFUI\MainWindow.xaml

I added a line to show the Player’s Dexterity in the “Player’s Stats” section that starts on line 32.

Just add another “<RowDefinition Height=”Auto”/>” inside the “<Grid.RowDefinitions>” section from lines 34-42. Then, I added the Dexterity label and value to lines 54-55 and increased the “Grid.Row” value for all the rows that come after it.

 

MainWindow.xaml (lines 32-64)

 

 

 

Step 14: Test the game

Finally, we can play the game to test it.

I added a TestCombatService function to the Test project. But, because we use random numbers in our combat functions, we can’t really use automated unit tests.

With the random numbers, the functions are “non-deterministic”. They don’t give always the same output, when passed in the same input parameters. You can only really do unit tests on “deterministic” functions – ones that always return the same output for the same inputs.

But I added Test_FirstAttacker so I could step through the code with the debugger, without needing to create a complete set of game objects.

When you play the game, notice what the Player’s Dexterity is. Restart the game a few times and see how well the Player does in combat when they have a high dexterity, and they have a low dexterity. You’ll probably notice a difference.

 

TestCombatService.cs

 

 

 

Additional links for this project

Source code: https://github.com/ScottLilly/SOSCSRPG

Project plan: https://github.com/ScottLilly/SOSCSRPG/projects/1

Discord: https://discord.gg/AUYXYtH

Return to main page

2 thoughts on “Lesson 16.3: Complex attack initiative and hit success logic

  1. Hi Scott,

    Awesome tutorial!
    I found a little bug in the lesson or maybe with the previous one (not sure).

    I was fighting a snake, using the keyboard and not the mouse, then I died. So I respawn in the home house and when I press the space key (it’s the Z key in your code) to attack I lost hit points.

    After many test, I lost up to 6 hit points after respawning from a snake bite.

    Maybe when the player dies, the currentBattle must be set to null (the current monster maybe also). But then the AttackCurentMonster in the GameSession must verify that the current Battle is not null because it can be called when you play with the keyboard.

    Regards,
    Michel

Leave a Reply

Your email address will not be published. Required fields are marked *