Lesson 4.14: Move Remaining Data to JSON

We’ve made progress moving our data files thus far. In this lesson we are going to convert the last three factories to load their data from JSON files. We will follow the patterns that have been used in the last 3 lessons: Data Transfer Objects, our JsonSerializationHelper, and modifying the factories to retrieve instances of the game objects. If you need to review the concepts, feel free to review them in the past lessons.

Loading Quests

  1. Create the quests.json file in the SimpleRPG.Game.Engine and Data folder.
[
  {
    "Id": 1,
    "Name": "Clear the herb garden",
    "Description": "Defeat the snakes in the Herbalist's garden",
    "RewardGold": 25,
    "RewardXP": 10,
    "RewardItems": [
      { "Id": 1002, "Qty": 1 }
    ],
    "Requirements": [
      { "Id": 9001, "Qty": 5 }
    ]
  }
]
  1. Set the quests.json file Build Action to Embedded resource.
  2. Define the required data transfer objects: QuestTemplate and IdQuantityItem. These are just simple data classes with public properties again.
    • Create the IdQuantityItem class in the Factories/DTO folder. This DTO will be used for multiple template classes, so we named it generically.
namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public class IdQuantityItem
    {
        public int Id { get; set; }

        public int Qty { get; set; }
    }
}

Create the QuestTemplate class in the same folder to map to our JSON file elements.

using System;
using System.Collections.Generic;
using System.Text;

namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public class QuestTemplate
    {
        public int Id { get; set; }

        public string Name { get; set; } = string.Empty;

        public string Description { get; set; } = string.Empty;

        public IEnumerable<IdQuantityItem> Requirements { get; set; } = new List<IdQuantityItem>();

        public int RewardGold { get; set; }

        public int RewardXP { get; set; }

        public IEnumerable<IdQuantityItem> RewardItems { get; set; } = new List<IdQuantityItem>();
    }
}
  1. Define a Quest constructor, so that we can simply create an instance of a Quest. Notice that we keep a default constructor too, so that our other code does not need to be refactored at this point.
using System;
using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Models
{
    public class Quest
    {
        public Quest(int id, string name, string description, int rewardsGold, int rewardsXP)
        {
            Id = id;
            Name = name;
            Description = description;
            RewardGold = rewardsGold;
            RewardExperiencePoints = rewardsXP;
        }

        public Quest()
        {
        }

        public int Id { get; set; }

        public string Name { get; set; } = string.Empty;
        
        public string Description { get; set; } = string.Empty;

        public IList<ItemQuantity> ItemsToComplete { get; set; } = new List<ItemQuantity>();

        public int RewardExperiencePoints { get; set; }
        
        public int RewardGold { get; set; }
        
        public IList<ItemQuantity> RewardItems { get; set; } = new List<ItemQuantity>();

        public DisplayMessage ToDisplayMessage()
        {
            var messageLines = new List<string>
            {
                Description,
                "Items to complete the quest:"
            };

            foreach (ItemQuantity q in ItemsToComplete)
            {
                messageLines.Add(q.QuantityItemDescription);
            }

            messageLines.Add("Rewards for quest completion:");
            messageLines.Add($"{RewardExperiencePoints} experience points");
            messageLines.Add($"{RewardGold} gold");
            foreach (ItemQuantity itemQuantity in RewardItems)
            {
                messageLines.Add(itemQuantity.QuantityItemDescription);
            }

            return new DisplayMessage($"Quest Added - {Name}", messageLines);
        }
    }
}
  1. Update the QuestFactory to load its data from the quests.json file. The code in this factory should look very similar to the MonsterFactory: find the template, create the game object, and set any list properties from the template.
using D20Tek.Common.Helpers;
using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class QuestFactory
    {
        private const string _resourceNamespace = "SimpleRPG.Game.Engine.Data.quests.json";
        private static readonly IList<QuestTemplate> _questTemplates = JsonSerializationHelper.DeserializeResourceStream<QuestTemplate>(_resourceNamespace);

        public static Quest GetQuestById(int id)
        {
            // first find the quest template by its id.
            var template = _questTemplates.First(p => p.Id == id);

            // then create an instance of quest from that template.
            var quest = new Quest(template.Id, template.Name, template.Description, 
                                  template.RewardGold, template.RewardXP);

            // next add each pre-requisite for the quest.
            foreach (var req in template.Requirements)
            {
                quest.ItemsToComplete.Add(new ItemQuantity { ItemId = req.Id, Quantity = req.Qty });
            }

            // finally add each reward item given from the quest.
            foreach(var item in template.RewardItems)
            {
                quest.RewardItems.Add(new ItemQuantity { ItemId = item.Id, Quantity = item.Qty });
            }

            return quest;
        }
    }
}

Let’s make sure we are familiar with these changes because they will be repeated in the remaining sections of this lesson.

Loading Recipes

  1. Create the recipes.json file in the SimpleRPG.Game.Engine and Data folder.
[
  {
    "Id": 1,
    "Name": "Granola bar recipe",
    "Ingredients": [
      { "Id": 3001, "Qty": 1 },
      { "Id": 3002, "Qty": 1 },
      { "Id": 3003, "Qty": 1 }
    ],
    "OutputItems": [
      { "Id": 2001, "Qty": 1 }
    ]
  }
]
  1. Set the recipes.json file Build Action to Embedded resource.
  2. Define the required data transfer object for RecipeTemplate in the Factories/DTO folder.
using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public class RecipeTemplate
    {
        public int Id { get; set; }

        public string Name { get; set; } = string.Empty;

        public IEnumerable<IdQuantityItem> Ingredients { get; set; } = new List<IdQuantityItem>();

        public IEnumerable<IdQuantityItem> OutputItems { get; set; } = new List<IdQuantityItem>();
    }
}
  1. Update the RecipeFactory to load its data from the recipes.json file.
using D20Tek.Common.Helpers;
using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class RecipeFactory
    {
        private const string _resourceNamespace = "SimpleRPG.Game.Engine.Data.recipes.json";
        private static readonly IList<RecipeTemplate> _recipeTemplates = JsonSerializationHelper.DeserializeResourceStream<RecipeTemplate>(_resourceNamespace);

        public static Recipe GetRecipeById(int id)
        {
            // first find the quest template by its id.
            var template = _recipeTemplates.First(p => p.Id == id);

            // then create an instance of quest from that template.
            var recipe = new Recipe(template.Id, template.Name);

            // next add each pre-requisite for the quest.
            foreach (var req in template.Ingredients)
            {
                recipe.AddIngredient(req.Id, req.Qty);
            }

            // finally add each reward item given from the quest.
            foreach (var item in template.OutputItems)
            {
                recipe.AddOutputItem(item.Id, item.Qty);
            }

            return recipe;
        }
    }
}

Loading Traders

  1. Create the traders.json file in the SimpleRPG.Game.Engine and Data folder.
[
  {
    "Id": 101,
    "Name": "Susan",
    "Inventory": [
      { "Id": 1001, "Qty": 1 }
    ]
  },
  {
    "Id": 102,
    "Name": "Farmer Ted",
    "Inventory": [
      { "Id": 1001, "Qty": 1 }
    ]
  },
  {
    "Id": 103,
    "Name": "Pete the Herbalist",
    "Inventory": [
      { "Id": 1001, "Qty": 1 }
    ]
  }
]
  1. Set the traders.json file Build Action to Embedded resource.
  2. Define the required data transfer object for TraderTemplate in the Factories/DTO folder.
using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public class TraderTemplate
    {
        public int Id { get; set; }

        public string Name { get; set; } = string.Empty;

        public IEnumerable<IdQuantityItem> Inventory { get; set; } = new List<IdQuantityItem>();
    }
}
  1. Update the TraderFactory to load its data from the traders.json file.
using D20Tek.Common.Helpers;
using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class TraderFactory
    {
        private const string _resourceNamespace = "SimpleRPG.Game.Engine.Data.traders.json";
        private static readonly List<Trader> _traders = new List<Trader>();

        static TraderFactory()
        {
            IList<TraderTemplate> traderTemplates = JsonSerializationHelper.DeserializeResourceStream<TraderTemplate>(_resourceNamespace);
            foreach (var template in traderTemplates)
            {
                var trader = new Trader(template.Id, template.Name);

                foreach (var item in template.Inventory)
                {
                    for (int i = 0; i < item.Qty; i++)
                    {
                        trader.Inventory.AddItem(ItemFactory.CreateGameItem(item.Id));
                    }
                }

                _traders.Add(trader);
            }
        }

        public static Trader GetTraderById(int id) => _traders.First(t => t.Id == id);
    }
}

There is a lot of code changes in this lesson, but it follows the same pattern for each factory. So hopefully it will be easy to consume in one chunk. I didn’t want to drag out the JSON data changes in very repetitive lessons per factory. If this gets too confusing, we may split this our into separate lessons.

After these changes, we can validate everything builds and the tests pass. If we run the game again, we can see the traders, quest, and recipe in action. There should not be any functional changes to the game. The only difference is that now all of our game data comes from files and isn’t hard-coded in the game engine. It simplifies adding any data that we want to the game, so we can expand locations, monsters, and items quickly without requiring a developer.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s