Press "Enter" to skip to content

Lesson 18.2: New starting windows and configurable player races

To continue with the Player attributes and creation, I added Races to the GameDetails.json file. You can also have PlayerAttributeModifiers for the different races. So, if the player decides to be an Orc, their Charisma will be lower by one, but their strength will be higher by one. And that’s all configurable.

I also added an initial Startup window (to eventually let the user load an existing player or create a new one) and a PlayerCreation window (to eventually let the user see the random values for their Player, and re-roll if they aren’t happy).

FYI: I’m testing out live-streaming the changes I make for these lessons at https://twitch.tv/codingwithscott, in case you want to follow along. I don’t have a schedule yet but will try to come up with one I can follow.

Step 1: Modify \Engine\GameData\GameDetails.json

We’ll start by changing the “Name” to “Title” and add a new “SubTitle” value. This way, if you want to build a series of games, it might be a little easier to display this on the screen.

Then add the new Race information to lines 37-76. This is a list of Races that will be available on the upcoming Player creation screen.

Each Race has a Key, a DisplayName, and a list of PlayerAttributeModifiers. The PlayerAttributeModifiers values have a Key that relates to the PlayerAttribute it will modify and a Modifier of what change we’ll apply to that attribute.

So, with this data, if the user creates a Human, we’ll “roll” the 3d6 to determine the Player’s Constitution, then add 1 to it (because of the modifier).

Elves will have their Charisma increased by 1, but their Strength will be decreased by 1. Orcs will get the opposite modifiers.

When we get to the Player creation window, we need to be sure to handle GameDetails.json files with Races that don’t have any modifiers, or even don’t have any Races.

The idea is to make the game very flexible, just by changing the GameDetails.json file – and without making you modify the program. I don’t know how far we’ll be able to go with that, but that’s the plan.

GameDetails.json
{
  "Title": "Scott's Open Source C# RPG",
  "SubTitle" : "The Adventure Continues",
  "Version": "0.1.000",
  "PlayerAttributes": [
    {
      "Key": "STR",
      "DisplayName": "Strength",
      "DiceNotation": "3d6"
    },
    {
      "Key": "INT",
      "DisplayName": "Intelligence",
      "DiceNotation": "3d6"
    },
    {
      "Key": "WIS",
      "DisplayName": "Wisdom",
      "DiceNotation": "3d6"
    },
    {
      "Key": "CON",
      "DisplayName": "Constitution",
      "DiceNotation": "3d6"
    },
    {
      "Key": "DEX",
      "DisplayName": "Dexterity",
      "DiceNotation": "3d6"
    },
    {
      "Key": "CHA",
      "DisplayName": "Charisma",
      "DiceNotation": "3d6"
    }
  ],
  "Races": [
    {
      "Key": "HUMAN",
      "DisplayName": "Human",
      "PlayerAttributeModifiers": [
        {
          "Key": "CON",
          "Modifier": "1"
        }
      ]
    },
    {
      "Key": "ELF",
      "DisplayName": "Elf",
      "PlayerAttributeModifiers": [
        {
          "Key": "CHA",
          "Modifier": "1"
        },
        {
          "Key": "STR",
          "Modifier": "-1"
        }
      ]
    },
    {
      "Key": "ORC",
      "DisplayName": "Orc",
      "PlayerAttributeModifiers": [
        {
          "Key": "CHA",
          "Modifier": "-1"
        },
        {
          "Key": "STR",
          "Modifier": "1"
        }
      ]
    }
  ] 
}

Step 2: Create \Engine\Models\Race.cs

This is a simple class to hold the Race information from GameDetails.json, including the PlayerAttributeModiefers for the Race.

Race.cs
using System.Collections.Generic;
namespace Engine.Models
{
    public class Race
    {
        public string Key { get; set; }
        public string DisplayName { get; set; }
        public List<PlayerAttributeModifier> PlayerAttributeModifiers { get; } =
            new List<PlayerAttributeModifier>();
    }
}

Step 3: Create \Engine\Models\PlayerAttributeModifier.cs

A simple class to hold the Attribute to modify and the amount to modify it (in the Modifer property).

PlayerAttributeModifier.cs
namespace Engine.Models
{
    public class PlayerAttributeModifier
    {
        public string AttributeKey { get; set; }
        public int Modifier { get; set; }
    }
}

Step 4: Modify \Engine\Shared\ExtensionMethods.cs

On lines 26-39, create three new functions to simplify getting string and integer values from JSON objects.

ExtensionMethods.cs
using System;
using System.Xml;
using Newtonsoft.Json.Linq;
namespace Engine.Shared
{
    public static class ExtensionMethods
    {
        public static int AttributeAsInt(this XmlNode node, string attributeName)
        {
            return Convert.ToInt32(node.AttributeAsString(attributeName));
        }
        public static string AttributeAsString(this XmlNode node, string attributeName)
        {
            XmlAttribute attribute = node.Attributes?[attributeName];
            if(attribute == null)
            {
                throw new ArgumentException($"The attribute '{attributeName}' does not exist");
            }
            return attribute.Value;
        }
        public static string StringValueOf(this JObject jsonObject, string key)
        {
            return jsonObject[key].ToString();
        }
        public static string StringValueOf(this JToken jsonToken, string key)
        {
            return jsonToken[key].ToString();
        }
        public static int IntValueOf(this JToken jsonToken, string key)
        {
            return Convert.ToInt32(jsonToken[key]);
        }
    }
}

Step 5: Modify \Engine\Models\GameDetails.cs

Change the Name property to Title, add a SubTitle property, add a Races property, and modify the constructor to accept (and set) the title and subtitle values.

GameDetails.cs
using System.Collections.Generic;
namespace Engine.Models
{
    public class GameDetails
    {
        public string Title { get; set; }
        public string SubTitle { get; set; }
        public string Version { get; set; }
        public List<PlayerAttribute> PlayerAttributes { get; } =
            new List<PlayerAttribute>();
        public List<Race> Races { get; } =
            new List<Race>();
        public GameDetails(string title, string subTitle, string version)
        {
            Title = title;
            SubTitle = subTitle;
            Version = version;
        }
    }
}

Step 6: Create \Engine\Services\GameDetailsService.cs

In the last lesson, I mentioned we’ll eventually move the GameDetails.json reader somewhere outside the GameSession class. That’s what this class is.

It creates a GameDetails object. filling it the same way we did with the code in the GameSession class.

We do take advantage of the new extension methods: StringValueOf() and IntValueOf().

On lines 27-51, if there are any Races values in the JSON file, we create the Races for the game – including any PlayerAttributeModifiers if they exist.

Notice that this part of the code checks if we have Race and PlayerAttrbiuteModifers values before trying to parse them. This way, the game won’t crash if your GameDetails.json doesn’t use them.

However, we don’t do that on line 20, where we get the PlayerAttributes. That’s because I want this game engine to require PlayerAttributes. If they’re missing from GameDetails.json, the game should crash.

That’s the vision I have for the SOSCSRPG project. We’re going to use PlayerAttributes throughout the rest of the game (like Dexterity during combat), so I’m making this a requirement.

GameDetailsService.cs
using System.IO;
using Engine.Models;
using Engine.Shared;
using Newtonsoft.Json.Linq;
namespace Engine.Services
{
    public static class GameDetailsService
    {
        public static GameDetails ReadGameDetails()
        {
            JObject gameDetailsJson = 
                JObject.Parse(File.ReadAllText(".\\GameData\\GameDetails.json"));
            GameDetails gameDetails = 
                new GameDetails(gameDetailsJson.StringValueOf("Title"), 
                                gameDetailsJson.StringValueOf("SubTitle"),
                                gameDetailsJson.StringValueOf("Version"));
            foreach(JToken token in gameDetailsJson["PlayerAttributes"])
            {
                gameDetails.PlayerAttributes.Add(new PlayerAttribute(token.StringValueOf("Key"),
                                                                     token.StringValueOf("DisplayName"),
                                                                     token.StringValueOf("DiceNotation")));
            }
            if(gameDetailsJson["Races"] != null)
            {
                foreach(JToken token in gameDetailsJson["Races"])
                {
                    Race race = new Race
                                {
                                    Key = token.StringValueOf("Key"),
                                    DisplayName = token.StringValueOf("DisplayName")
                                };
                    if(token["PlayerAttributeModifiers"] != null)
                    {
                        foreach(JToken childToken in token["PlayerAttributeModifiers"])
                        {
                            race.PlayerAttributeModifiers.Add(new PlayerAttributeModifier
                                                              {
                                                                  AttributeKey = childToken.StringValueOf("Key"),
                                                                  Modifier = childToken.IntValueOf("Modifier")
                                                              });
                        }
                    }
                    gameDetails.Races.Add(race);
                }
            }
            return gameDetails;
        }
    }
}

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

Now that we have the GameDetailsService to read the GameDetails.json file, change PopulateGameDetails (starting on line 210) to just call the new ReadGameDetails function, instead of JON-parsing code we used to have in it.

GameSession.cs (lines 210-213)

        private void PopulateGameDetails()
        {
            GameDetails = GameDetailsService.ReadGameDetails();
        }

Step 8: Modify \WPFUI\MainWindow.xaml

Change line 10 to display GameDetails.Title

MainWindow.xaml (lines 1-12)

<Window x:Class="WPFUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewModels="clr-namespace:Engine.ViewModels;assembly=Engine"
        d:DataContext="{d:DesignInstance viewModels:GameSession}"
        mc:Ignorable="d"
        FontSize="11pt"
        Title="{Binding GameDetails.Title}" Height="768" Width="1024"
        KeyDown="MainWindow_OnKeyDown"
        Closing="MainWindow_OnClosing">

Step 9: Create \WPFUI\CharacterCreation.xaml (Window)

This will soon hold our Player creation page. For now, we just have a “Random Player” button that goes to the old MainWindow.xaml page.

CharacterCreation.xaml
<Window x:Class="WPFUI.CharacterCreation"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFUI"
        xmlns:viewModels="clr-namespace:Engine.Models;assembly=Engine"
        d:DataContext="{d:DesignInstance viewModels:GameDetails}"
        mc:Ignorable="d"
        FontSize="11pt"
        Title="{Binding Title}" Height="400" Width="400">
    <Grid Margin="10,10,10,10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0"
               Content="CHARACTER CREATION SCREEN"/>
        <Button Grid.Row="1" Grid.Column="0"
                Margin="0,5,0,5"
                HorizontalAlignment="Center"
                Width="125"
                Content="Random Player"
                Click="RandomPlayer_OnClick"/>
    </Grid>
</Window>
CharacterCreation.xaml.cs
using System.Windows;
using Engine.Models;
using Engine.Services;
namespace WPFUI
{
    public partial class CharacterCreation : Window
    {
        private GameDetails _gameDetails;
        public CharacterCreation()
        {
            InitializeComponent();
            _gameDetails = GameDetailsService.ReadGameDetails();
            DataContext = _gameDetails;
        }
        private void RandomPlayer_OnClick(object sender, RoutedEventArgs e)
        {
            MainWindow mainWindow = new MainWindow();
            mainWindow.Show();
            Close();
        }
    }
}

Step 10: Create \WPFUI\Startup.xaml (Window)

This will eventually hold the startup options to create a new player, load an existing game, etc. For now, we just have two buttons to either go to the PlayerCreation window (and close this window) or exit the program.

Startup.xaml
<Window x:Class="WPFUI.Startup"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFUI"
        xmlns:viewModels="clr-namespace:Engine.Models;assembly=Engine"
        d:DataContext="{d:DesignInstance viewModels:GameDetails}"
        mc:Ignorable="d"
        FontSize="11pt"
        Title="{Binding Title}" Height="400" Width="400">
    <Grid Margin="10,10,10,10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Button Grid.Row="0" Grid.Column="0"
                Margin="0,5,0,5"
                HorizontalAlignment="Center"
                Width="125"
                Content="Start New Game"
                Click="StartNewGame_OnClick"/>
        <Button Grid.Row="1" Grid.Column="0"
                Margin="0,5,0,5"
                HorizontalAlignment="Center"
                Width="125"
                Content="Exit"
                Click="Exit_OnClick"/>
        
    </Grid>
</Window>
Startup.xaml.cs
using System.Windows;
using Engine.Models;
using Engine.Services;
namespace WPFUI
{
    public partial class Startup : Window
    {
        private GameDetails _gameDetails;
        public Startup()
        {
            InitializeComponent();
            _gameDetails = GameDetailsService.ReadGameDetails();
            DataContext = _gameDetails;
        }
        private void StartNewGame_OnClick(object sender, RoutedEventArgs e)
        {
            CharacterCreation characterCreationWindow = new CharacterCreation();
            characterCreationWindow.Show();
            Close();
        }
        private void Exit_OnClick(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}

Step 11: Modify \WPFUI\App.xaml

On line 6, change the StartupUri to “Startup.xaml”, instead of “MainWindow.xaml”.

This is the first page that runs when we run the program. The StartupUri tells the program what window to display first, which we now want to be our Startup.xaml window, instead of the original MainWindow.xaml.

App.xaml
<Application x:Class="WPFUI.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:converters="clr-namespace:WPFUI.CustomConverters"
             DispatcherUnhandledException="App_OnDispatcherUnhandledException"
             StartupUri="Startup.xaml">
    <Application.Resources>
         <converters:FileToBitmapConverter x:Key="FileToBitmapConverter"/>
    </Application.Resources>
</Application>

Step 12: Test and run the game

NEXT LESSON: Lesson 18.3: Player creation screen

PREVIOUS LESSON: Lesson 18.1: Making Configurable GameDetails

    Leave a Reply

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