Lesson 5.15: Retrieve Remaining Game Data from Services

Lesson 5.14 laid out the detailed description of the changes required to update ItemFactory to fetch ItemTemplate data from our game services. In this lesson, we’re going to update the remaining factories to provide the same functionality. We will use the same patterns we did for ItemFactory and make use of the GameServiceClient to communicate with our web services.

We will make a lot of small, focused changes to various classes. And won’t go over the details of each change again. To review any of the steps in detail, we can always go back to the last lesson.

Refactor DTO Classes

We need to update all of the template DTO classes to derive from NamedElement (as we did with ItemTemplate), and remove the Id and Name properties since they now come from the base class. To use GameServiceClient, our returned entities derive from that type.

1. Changes to LocationTemplate:

using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public class LocationTemplate : NamedElement<int>
    {
        public int X { get; set; }

        public int Y { get; set; }

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

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

        public int? TraderId { get; set; }

        public IEnumerable<int> Quests { get; set; } = new List<int>();

        public IEnumerable<MonsterEncounterItem> Monsters { get; set; } = new List<MonsterEncounterItem>();
    }
}

2. Changes to MonsterTemplate:

using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public class MonsterTemplate : NamedElement<int>
    {
        public int Dex { get; set; }

        public int Str { get; set; }

        public int AC { get; set; }

        public int MaxHP { get; set; }

        public int WeaponId { get; set; }

        public int RewardXP { get; set; }

        public int Gold { get; set; }

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

        public IEnumerable<LootItem> LootItems { get; set; } = new List<LootItem>();
    }
}

3. Changes to QuestTemplate:

using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public class QuestTemplate : NamedElement<int>
    {
        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>();
    }
}

4. Changes to RecipeTemplate:

using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public class RecipeTemplate : NamedElement<int>
    {
        public IEnumerable<IdQuantityItem> Ingredients { get; set; } = new List<IdQuantityItem>();

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

5. Changes to TraderTemplate:

using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public class TraderTemplate : NamedElement<int>
    {
        public IEnumerable<IdQuantityItem> Inventory { get; set; } = new List<IdQuantityItem>();
    }
}

Add Load Methods to Factories

Then, we need to update the remaining factory classes to include an asynchronous Load method. They all follow a very similar pattern, so we will describe the MonsterFactory changes in detail but just show the code for the others.

1. MonsterFactory changes:

using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using SimpleRPG.Game.Engine.Services;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class MonsterFactory
    {
        private const string _serviceUrl = "https://simple-rpg-services.azurewebsites.net/api/monster";
        private static IEnumerable<MonsterTemplate> _monsterTemplates = new List<MonsterTemplate>();

        public static async Task Load(HttpClient httpClient)
        {
            if (_monsterTemplates.Any())
            {
                return;
            }

            var client = new GameServiceClient<MonsterTemplate, int>(httpClient, _serviceUrl);
            _monsterTemplates = await client.GetAllEntities().ConfigureAwait(false);
        }

        public static Monster GetMonster(int monsterId, IDiceService? dice = null)
        {
            dice ??= DiceService.Instance;

            // first find the monster template by its id.
            var template = _monsterTemplates.First(p => p.Id == monsterId);
            
            // then create an instance of monster from that template.
            var weapon = ItemFactory.CreateGameItem(template.WeaponId);
            var monster = new Monster(template.Id, template.Name, template.Image, template.Dex, template.Str,
                                      template.AC, template.MaxHP, weapon, template.RewardXP, template.Gold);
            
            // finally add random loot for this monster instance.
            foreach(var loot in template.LootItems)
            {
                AddLootItem(monster, loot.Id, loot.Perc, dice);
            }

            return monster;
        }

        private static void AddLootItem(Monster monster, int itemID, int percentage, IDiceService dice)
        {
            if (dice.Roll("1d100").Value <= percentage)
            {
                monster.Inventory.AddItem(ItemFactory.CreateGameItem(itemID));
            }
        }
    }
}
  • First, define the service url for the monster resource endpoint (line #13).
  • Then, define a member variable to hold the templates. It starts out empty (line #14).
  • And, in the Load method, check if there are any entities already in the template list. If the data has already been loaded, then we skip the rest of the method (lines #18-21).
  • Finally, we use the GameServiceClient<MonsterTemplate, int> to fetch all of the MonsterTemplates from the game service (lines #23-24).

2. QuestFactory changes:

using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using SimpleRPG.Game.Engine.Services;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class QuestFactory
    {
        private const string _serviceUrl = "https://simple-rpg-services.azurewebsites.net/api/quest";
        private static IEnumerable<QuestTemplate> _questTemplates = new List<QuestTemplate>();

        public static async Task Load(HttpClient httpClient)
        {
            if (_questTemplates.Any())
            {
                return;
            }

            var client = new GameServiceClient<QuestTemplate, int>(httpClient, _serviceUrl);
            _questTemplates = await client.GetAllEntities().ConfigureAwait(false);
        }

        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(req.Id, req.Qty));
            }

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

            return quest;
        }
    }
}

3. RecipeFactory changes:

using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using SimpleRPG.Game.Engine.Services;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class RecipeFactory
    {
        private const string _serviceUrl = "https://simple-rpg-services.azurewebsites.net/api/recipe";
        private static IEnumerable<RecipeTemplate> _recipeTemplates = new List<RecipeTemplate>();

        public static async Task Load(HttpClient httpClient)
        {
            if (_recipeTemplates.Any())
            {
                return;
            }

            var client = new GameServiceClient<RecipeTemplate, int>(httpClient, _serviceUrl);
            _recipeTemplates = await client.GetAllEntities().ConfigureAwait(false);
        }

        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;
        }
    }
}

4. TraderFactory changes:

using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using SimpleRPG.Game.Engine.Services;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class TraderFactory
    {
        private const string _serviceUrl = "https://simple-rpg-services.azurewebsites.net/api/trader";
        private static IEnumerable<TraderTemplate> _traderTemplates = new List<TraderTemplate>();
        private static readonly List<Trader> _traders = new List<Trader>();

        public static async Task Load(HttpClient httpClient)
        {
            if (_traderTemplates.Any())
            {
                return;
            }

            var client = new GameServiceClient<TraderTemplate, int>(httpClient, _serviceUrl);
            _traderTemplates = await client.GetAllEntities().ConfigureAwait(false);

            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);
    }
}

The TraderFactory.Load method is a little different because it loads the TraderTemplates and then also creates instances of Trader and caches them in the factory.

5. WorldFactory changes:

using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using SimpleRPG.Game.Engine.Services;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class WorldFactory
    {
        private const string _serviceUrl = "https://simple-rpg-services.azurewebsites.net/api/location";
        private static IEnumerable<LocationTemplate> _locationTemplates = new List<LocationTemplate>();

        public static async Task Load(HttpClient httpClient)
        {
            if (_locationTemplates.Any())
            {
                return;
            }

            var client = new GameServiceClient<LocationTemplate, int>(httpClient, _serviceUrl);
            _locationTemplates = await client.GetAllEntities().ConfigureAwait(false);
        }

        internal static World CreateWorld()
        {
            var newWorld = new World();

            foreach (var template in _locationTemplates)
            {
                var trader = (template.TraderId is null) ? null : TraderFactory.GetTraderById(template.TraderId.Value);
                var loc = new Location(template.X, template.Y, template.Name, template.Description,
                                       template.ImageName, trader);

                foreach (var questId in template.Quests)
                {
                    loc.QuestsAvailableHere.Add(QuestFactory.GetQuestById(questId));
                }

                foreach (var enc in template.Monsters)
                {
                    loc.AddMonsterEncounter(enc.Id, enc.Perc);
                }

                newWorld.AddLocation(loc);
            }

            return newWorld;
        }
    }
}

Very repetitive changes, but our factories are ready to go.

Update FactoryLoader

Next, we need to update the FactoryLoader.Load method to call the Load methods for each of our factories. This keeps all of our loads in one location, so it’s easy to update. Also, we are making six separate web service calls to get the different data sources. It could be better to fetch all of the game data in a single service call, but for our purposes we will keep them separate.

using System.Net.Http;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Engine.Factories
{
    public static class FactoryLoader
    {
        public static async Task Load(HttpClient httpClient)
        {
            await ItemFactory.Load(httpClient).ConfigureAwait(false);
            await MonsterFactory.Load(httpClient).ConfigureAwait(false);
            await QuestFactory.Load(httpClient).ConfigureAwait(false);
            await RecipeFactory.Load(httpClient).ConfigureAwait(false);
            await TraderFactory.Load(httpClient).ConfigureAwait(false);
            await WorldFactory.Load(httpClient).ConfigureAwait(false);
        }
    }
}

Delete Unnecessary Files

After moving all of the factories to use GameServiceClient, we have some files that are no longer needed in our project. We can now delete the following files:

  • Data
    • locations.json
    • monsters.json
    • quests.json
    • recipes.json
    • traders.json
  • JsonSerializationHelper.cs – since we’re no longer loading embedded resource files, this helper class isn’t needed any longer.

I find great satisfaction in being able to delete dead code from our projects.

There were a few test changes needed to verify that the factories were loaded for some QuestFactory and RecipeFactory tests. Those are also included in this lesson’s commit.

Next, we can build and run our tests successfully. If we run the game again, it should load and show our main game screen. If we move around different locations and fight some monsters, we will see the data that was loaded from our game services.

Fig 3 – Game Screen with Full Data

In conclusion, we have changed our game to fetch its data from the game services. The game executable can now be released separately from any game data updates. We could theoretically release new quests, locations, and monsters without requiring a game update. In the next lesson, we are going to prepare our game for another deployment to a new storage version.

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 )

Facebook photo

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

Connecting to %s