In this lesson, we’ll clean up a few more dependencies issues by moving classes and functions to better locations.
Step 1: Update Fody package references
A quick clean-up action, from reader Steven, will eliminate some warnings that appear during the build. The program runs fine without this change, but it’s always good to have clean builds – without any warnings.
In SOSCSRPG.ViewModels.csproj, SOSCSRGP.Models.csproj, and Engine.csproj, find the line that says:
<PackageReference Include=”PropertyChanged.Fody” Version=”3.4.0″ />
and replace it with:
<PackageReference Include=”PropertyChanged.Fody” Version=”3.4.0″ PrivateAssets=”All”/>
Step 2: Move SOSCSRPG.Services\LoggingService.cs to SOSCSRPG.Core
Cut-and-paste this class into the SOSCSRPG.Core project.
Change the LoggingService.cs class’ namespace to “SOSCSRPG.Core.
In the WPFUI project, add a project reference to SOSCSRPG.Core.
In WPFUI\App.xaml.cs, change the using directive “using SOSCSRPG.Services;” to “using SOSCSRPG.Core;”
LoggingService.cs
using System;
using System.IO;
namespace SOSCSRPG.Core
{
public static class LoggingService
{
private const string LOG_FILE_DIRECTORY = "Logs";
static LoggingService()
{
string logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, LOG_FILE_DIRECTORY);
if(!Directory.Exists(logDirectory))
{
Directory.CreateDirectory(logDirectory);
}
}
public static void Log(Exception exception, bool isInnerException = false)
{
using(StreamWriter sw = new StreamWriter(LogFileName(), true))
{
sw.WriteLine(isInnerException ? "INNER EXCEPTION" : $"EXCEPTION: {DateTime.Now}");
sw.WriteLine(new string(isInnerException ? '-' : '=', 40));
sw.WriteLine($"{exception.Message}");
sw.WriteLine($"{exception.StackTrace}");
sw.WriteLine(); // Blank line, to make the log file easier to read
}
if(exception.InnerException != null)
{
Log(exception.InnerException, true);
}
}
private static string LogFileName()
{
// This will create a separate log file for each day.
// Not that we're hoping to have many days of errors.
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, LOG_FILE_DIRECTORY,
$"SOSCSRPG_{DateTime.Now:yyyyMMdd}.log");
}
}
}
App.xaml.cs
using System.Windows;
using System.Windows.Threading;
using SOSCSRPG.Core;
namespace WPFUI
{
public partial class App : Application
{
private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
string exceptionMessageText =
$"An exception occurred: {e.Exception.Message}\r\n\r\nat: {e.Exception.StackTrace}";
LoggingService.Log(e.Exception);
// TODO: Create a Window to display the exception information.
MessageBox.Show(exceptionMessageText, "Unhandled Exception", MessageBoxButton.OK);
}
}
}
Step 3: Move MessageBroker.cs and GameMessageEventArgs to SOSCSRPG.Core
Cut-and-paste SOSCSRPG.Models\EventArgs\GameMessageEventArgs.cs into SOSCSRPG.Core project.
Cut-and-paste Engine\Services\MessageBroker.cs into SOSCSRPG.Core project.
Change the namespaces of MessageBroker and GameMessageEventArgs to SOSCSRPG.Core
Change MessageBroker’s RaiseMessage function from “internal” to “public”
Change the using directive from “using SOSCSRPG.Models.EventArgs;” to “using “SOSCSRPG.Core;” in MainWindow.xaml.cs, Battle.cs, and GameSession.cs
MessageBroker.cs
using System;
namespace SOSCSRPG.Core
{
public class MessageBroker
{
// Use the Singleton design pattern for this class,
// to ensure everything in the game sends messages through this one object.
private static readonly MessageBroker s_messageBroker =
new MessageBroker();
private MessageBroker()
{
}
public event EventHandler<GameMessageEventArgs> OnMessageRaised;
public static MessageBroker GetInstance()
{
return s_messageBroker;
}
public void RaiseMessage(string message)
{
OnMessageRaised?.Invoke(this, new GameMessageEventArgs(message));
}
}
}
GameMessageEventArgs.cs
namespace SOSCSRPG.Core
{
public class GameMessageEventArgs : System.EventArgs
{
public string Message { get; private set; }
public GameMessageEventArgs(string message)
{
Message = message;
}
}
}
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using Engine.Models;
using Engine.Services;
using Engine.ViewModels;
using Microsoft.Win32;
using SOSCSRPG.Core;
using WPFUI.Windows;
namespace WPFUI
{
public partial class MainWindow : Window
{
private const string SAVE_GAME_FILE_EXTENSION = "soscsrpg";
private readonly MessageBroker _messageBroker = MessageBroker.GetInstance();
private readonly Dictionary<Key, Action> _userInputActions =
new Dictionary<Key, Action>();
private GameSession _gameSession;
public MainWindow(Player player, int xLocation = 0, int yLocation = 0)
{
InitializeComponent();
InitializeUserInputActions();
SetActiveGameSessionTo(new GameSession(player, xLocation, yLocation));
}
private void OnClick_MoveNorth(object sender, RoutedEventArgs e)
{
_gameSession.MoveNorth();
}
private void OnClick_MoveWest(object sender, RoutedEventArgs e)
{
_gameSession.MoveWest();
}
private void OnClick_MoveEast(object sender, RoutedEventArgs e)
{
_gameSession.MoveEast();
}
private void OnClick_MoveSouth(object sender, RoutedEventArgs e)
{
_gameSession.MoveSouth();
}
private void OnClick_AttackMonster(object sender, RoutedEventArgs e)
{
_gameSession.AttackCurrentMonster();
}
private void OnClick_UseCurrentConsumable(object sender, RoutedEventArgs e)
{
_gameSession.UseCurrentConsumable();
}
private void OnGameMessageRaised(object sender, GameMessageEventArgs e)
{
GameMessages.Document.Blocks.Add(new Paragraph(new Run(e.Message)));
GameMessages.ScrollToEnd();
}
private void OnClick_DisplayTradeScreen(object sender, RoutedEventArgs e)
{
if(_gameSession.CurrentTrader != null)
{
TradeScreen tradeScreen = new TradeScreen();
tradeScreen.Owner = this;
tradeScreen.DataContext = _gameSession;
tradeScreen.ShowDialog();
}
}
private void OnClick_Craft(object sender, RoutedEventArgs e)
{
Recipe recipe = ((FrameworkElement)sender).DataContext as Recipe;
_gameSession.CraftItemUsing(recipe);
}
private void InitializeUserInputActions()
{
_userInputActions.Add(Key.W, () => _gameSession.MoveNorth());
_userInputActions.Add(Key.A, () => _gameSession.MoveWest());
_userInputActions.Add(Key.S, () => _gameSession.MoveSouth());
_userInputActions.Add(Key.D, () => _gameSession.MoveEast());
_userInputActions.Add(Key.Z, () => _gameSession.AttackCurrentMonster());
_userInputActions.Add(Key.C, () => _gameSession.UseCurrentConsumable());
_userInputActions.Add(Key.I, () => SetTabFocusTo("InventoryTabItem"));
_userInputActions.Add(Key.Q, () => SetTabFocusTo("QuestsTabItem"));
_userInputActions.Add(Key.R, () => SetTabFocusTo("RecipesTabItem"));
_userInputActions.Add(Key.T, () => OnClick_DisplayTradeScreen(this, new RoutedEventArgs()));
}
private void MainWindow_OnKeyDown(object sender, KeyEventArgs e)
{
if(_userInputActions.ContainsKey(e.Key))
{
_userInputActions[e.Key].Invoke();
}
}
private void SetTabFocusTo(string tabName)
{
foreach(object item in PlayerDataTabControl.Items)
{
if (item is TabItem tabItem)
{
if (tabItem.Name == tabName)
{
tabItem.IsSelected = true;
return;
}
}
}
}
private void SetActiveGameSessionTo(GameSession gameSession)
{
// Unsubscribe from OnMessageRaised, or we will get double messages
_messageBroker.OnMessageRaised -= OnGameMessageRaised;
_gameSession = gameSession;
DataContext = _gameSession;
// Clear out previous game's messages
GameMessages.Document.Blocks.Clear();
_messageBroker.OnMessageRaised += OnGameMessageRaised;
}
private void StartNewGame_OnClick(object sender, RoutedEventArgs e)
{
Startup startup = new Startup();
startup.Show();
Close();
}
private void SaveGame_OnClick(object sender, RoutedEventArgs e)
{
SaveGame();
}
private void Exit_OnClick(object sender, RoutedEventArgs e)
{
Close();
}
private void MainWindow_OnClosing(object sender, CancelEventArgs e)
{
AskToSaveGame();
}
private void AskToSaveGame()
{
YesNoWindow message =
new YesNoWindow("Save Game", "Do you want to save your game?");
message.Owner = GetWindow(this);
message.ShowDialog();
if(message.ClickedYes)
{
SaveGame();
}
}
private void SaveGame()
{
SaveFileDialog saveFileDialog =
new SaveFileDialog
{
InitialDirectory = AppDomain.CurrentDomain.BaseDirectory,
Filter = $"Saved games (*.{SAVE_GAME_FILE_EXTENSION})|*.{SAVE_GAME_FILE_EXTENSION}"
};
if (saveFileDialog.ShowDialog() == true)
{
SaveGameService.Save(new GameState(_gameSession.CurrentPlayer,
_gameSession.CurrentLocation.XCoordinate,
_gameSession.CurrentLocation.YCoordinate), saveFileDialog.FileName);
}
}
}
}
Battle.cs
using System;
using Engine.Services;
using SOSCSRPG.Core;
using SOSCSRPG.Models.EventArgs;
namespace Engine.Models
{
public class Battle : IDisposable
{
private readonly MessageBroker _messageBroker = MessageBroker.GetInstance();
private readonly Player _player;
private readonly Monster _opponent;
public event EventHandler<CombatVictoryEventArgs> OnCombatVictory;
public Battle(Player player, Monster opponent)
{
_player = player;
_opponent = opponent;
_player.OnActionPerformed += OnCombatantActionPerformed;
_opponent.OnActionPerformed += OnCombatantActionPerformed;
_opponent.OnKilled += OnOpponentKilled;
_messageBroker.RaiseMessage("");
_messageBroker.RaiseMessage($"You see a {_opponent.Name} here!");
if(CombatService.FirstAttacker(_player, _opponent) == CombatService.Combatant.Opponent)
{
AttackPlayer();
}
}
public void AttackOpponent()
{
if(_player.CurrentWeapon == null)
{
_messageBroker.RaiseMessage("You must select a weapon, to attack.");
return;
}
_player.UseCurrentWeaponOn(_opponent);
if(_opponent.IsAlive)
{
AttackPlayer();
}
}
public void Dispose()
{
_player.OnActionPerformed -= OnCombatantActionPerformed;
_opponent.OnActionPerformed -= OnCombatantActionPerformed;
_opponent.OnKilled -= OnOpponentKilled;
}
private void OnOpponentKilled(object sender, System.EventArgs e)
{
_messageBroker.RaiseMessage("");
_messageBroker.RaiseMessage($"You defeated the {_opponent.Name}!");
_messageBroker.RaiseMessage($"You receive {_opponent.RewardExperiencePoints} experience points.");
_player.AddExperience(_opponent.RewardExperiencePoints);
_messageBroker.RaiseMessage($"You receive {_opponent.Gold} gold.");
_player.ReceiveGold(_opponent.Gold);
foreach(GameItem gameItem in _opponent.Inventory.Items)
{
_messageBroker.RaiseMessage($"You receive one {gameItem.Name}.");
_player.AddItemToInventory(gameItem);
}
OnCombatVictory?.Invoke(this, new CombatVictoryEventArgs());
}
private void AttackPlayer()
{
_opponent.UseCurrentWeaponOn(_player);
}
private void OnCombatantActionPerformed(object sender, string result)
{
_messageBroker.RaiseMessage(result);
}
}
}
GameSession.cs
using System.ComponentModel;
using System.Linq;
using Engine.Factories;
using Engine.Models;
using Engine.Services;
using Newtonsoft.Json;
using SOSCSRPG.Core;
namespace Engine.ViewModels
{
public class GameSession : INotifyPropertyChanged
{
private readonly MessageBroker _messageBroker = MessageBroker.GetInstance();
#region Properties
private Player _currentPlayer;
private Location _currentLocation;
private Battle _currentBattle;
private Monster _currentMonster;
public event PropertyChangedEventHandler PropertyChanged;
[JsonIgnore]
public GameDetails GameDetails { get; private set; }
[JsonIgnore]
public World CurrentWorld { get; }
public Player CurrentPlayer
{
get => _currentPlayer;
set
{
if(_currentPlayer != null)
{
_currentPlayer.OnLeveledUp -= OnCurrentPlayerLeveledUp;
_currentPlayer.OnKilled -= OnPlayerKilled;
}
_currentPlayer = value;
if(_currentPlayer != null)
{
_currentPlayer.OnLeveledUp += OnCurrentPlayerLeveledUp;
_currentPlayer.OnKilled += OnPlayerKilled;
}
}
}
public Location CurrentLocation
{
get => _currentLocation;
set
{
_currentLocation = value;
CompleteQuestsAtLocation();
GivePlayerQuestsAtLocation();
CurrentMonster = MonsterFactory.GetMonsterFromLocation(CurrentLocation);
CurrentTrader = CurrentLocation.TraderHere;
}
}
[JsonIgnore]
public Monster CurrentMonster
{
get => _currentMonster;
set
{
if(_currentBattle != null)
{
_currentBattle.OnCombatVictory -= OnCurrentMonsterKilled;
_currentBattle.Dispose();
_currentBattle = null;
}
_currentMonster = value;
if(_currentMonster != null)
{
_currentBattle = new Battle(CurrentPlayer, CurrentMonster);
_currentBattle.OnCombatVictory += OnCurrentMonsterKilled;
}
}
}
[JsonIgnore]
public Trader CurrentTrader { get; private set; }
[JsonIgnore]
public bool HasLocationToNorth =>
CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null;
[JsonIgnore]
public bool HasLocationToEast =>
CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null;
[JsonIgnore]
public bool HasLocationToSouth =>
CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null;
[JsonIgnore]
public bool HasLocationToWest =>
CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null;
[JsonIgnore]
public bool HasMonster => CurrentMonster != null;
[JsonIgnore]
public bool HasTrader => CurrentTrader != null;
#endregion
public GameSession(Player player, int xCoordinate, int yCoordinate)
{
PopulateGameDetails();
CurrentWorld = WorldFactory.CreateWorld();
CurrentPlayer = player;
CurrentLocation = CurrentWorld.LocationAt(xCoordinate, yCoordinate);
}
public void MoveNorth()
{
if(HasLocationToNorth)
{
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
}
}
public void MoveEast()
{
if(HasLocationToEast)
{
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
}
}
public void MoveSouth()
{
if(HasLocationToSouth)
{
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
}
}
public void MoveWest()
{
if(HasLocationToWest)
{
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
}
}
private void PopulateGameDetails()
{
GameDetails = GameDetailsService.ReadGameDetails();
}
private void CompleteQuestsAtLocation()
{
foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
{
QuestStatus questToComplete =
CurrentPlayer.Quests.FirstOrDefault(q => q.PlayerQuest.ID == quest.ID &&
!q.IsCompleted);
if(questToComplete != null)
{
if(CurrentPlayer.Inventory.HasAllTheseItems(quest.ItemsToComplete))
{
CurrentPlayer.RemoveItemsFromInventory(quest.ItemsToComplete);
_messageBroker.RaiseMessage("");
_messageBroker.RaiseMessage($"You completed the '{quest.Name}' quest");
// Give the player the quest rewards
_messageBroker.RaiseMessage($"You receive {quest.RewardExperiencePoints} experience points");
CurrentPlayer.AddExperience(quest.RewardExperiencePoints);
_messageBroker.RaiseMessage($"You receive {quest.RewardGold} gold");
CurrentPlayer.ReceiveGold(quest.RewardGold);
foreach(ItemQuantity itemQuantity in quest.RewardItems)
{
GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);
_messageBroker.RaiseMessage($"You receive a {rewardItem.Name}");
CurrentPlayer.AddItemToInventory(rewardItem);
}
// Mark the Quest as completed
questToComplete.IsCompleted = true;
}
}
}
}
private void GivePlayerQuestsAtLocation()
{
foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
{
if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))
{
CurrentPlayer.Quests.Add(new QuestStatus(quest));
_messageBroker.RaiseMessage("");
_messageBroker.RaiseMessage($"You receive the '{quest.Name}' quest");
_messageBroker.RaiseMessage(quest.Description);
_messageBroker.RaiseMessage("Return with:");
foreach(ItemQuantity itemQuantity in quest.ItemsToComplete)
{
_messageBroker
.RaiseMessage($" {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
}
_messageBroker.RaiseMessage("And you will receive:");
_messageBroker.RaiseMessage($" {quest.RewardExperiencePoints} experience points");
_messageBroker.RaiseMessage($" {quest.RewardGold} gold");
foreach(ItemQuantity itemQuantity in quest.RewardItems)
{
_messageBroker
.RaiseMessage($" {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
}
}
}
}
public void AttackCurrentMonster()
{
_currentBattle?.AttackOpponent();
}
public void UseCurrentConsumable()
{
if(CurrentPlayer.CurrentConsumable != null)
{
if (_currentBattle == null)
{
CurrentPlayer.OnActionPerformed += OnConsumableActionPerformed;
}
CurrentPlayer.UseCurrentConsumable();
if (_currentBattle == null)
{
CurrentPlayer.OnActionPerformed -= OnConsumableActionPerformed;
}
}
}
private void OnConsumableActionPerformed(object sender, string result)
{
_messageBroker.RaiseMessage(result);
}
public void CraftItemUsing(Recipe recipe)
{
if(CurrentPlayer.Inventory.HasAllTheseItems(recipe.Ingredients))
{
CurrentPlayer.RemoveItemsFromInventory(recipe.Ingredients);
foreach(ItemQuantity itemQuantity in recipe.OutputItems)
{
for(int i = 0; i < itemQuantity.Quantity; i++)
{
GameItem outputItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);
CurrentPlayer.AddItemToInventory(outputItem);
_messageBroker.RaiseMessage($"You craft 1 {outputItem.Name}");
}
}
}
else
{
_messageBroker.RaiseMessage("You do not have the required ingredients:");
foreach(ItemQuantity itemQuantity in recipe.Ingredients)
{
_messageBroker
.RaiseMessage($" {itemQuantity.QuantityItemDescription}");
}
}
}
private void OnPlayerKilled(object sender, System.EventArgs e)
{
_messageBroker.RaiseMessage("");
_messageBroker.RaiseMessage("You have been killed.");
CurrentLocation = CurrentWorld.LocationAt(0, -1);
CurrentPlayer.CompletelyHeal();
}
private void OnCurrentMonsterKilled(object sender, System.EventArgs eventArgs)
{
// Get another monster to fight
CurrentMonster = MonsterFactory.GetMonsterFromLocation(CurrentLocation);
}
private void OnCurrentPlayerLeveledUp(object sender, System.EventArgs eventArgs)
{
_messageBroker.RaiseMessage($"You are now level {CurrentPlayer.Level}!");
}
}
}
Step 3: Move Engine\Services\InventoryService.cs’s ItemsThatAre function to ExtensionMethods
Cut-and-paste the ItemsThatAre function from Engine\Services\InventoryService.cs to Engine\Shared\ExtensionMethods.cs.
Add “using System.Collections.Generic;” directive to ExtensionMethods.cs
While in InventoryService.cs:
Remove the directive “using Engine.Factories;”
Delete these unused functions:
- AddItemFromFactory (lines 19-22)
- AddItems (lines 29-43)
In Engine\Models\Inventory.cs, change “using Engine.Services;” to “using Engine.Shared;”
InventoryService.cs
using System.Collections.Generic;
using System.Linq;
using Engine.Models;
namespace Engine.Services
{
// This service lets us write C# code that is more "functional".
// Instead of modifying existing objects, and their properties,
// we always instantiate new objects that have the modified values.
public static class InventoryService
{
public static Inventory AddItem(this Inventory inventory, GameItem item)
{
return inventory.AddItems(new List<GameItem> {item});
}
public static Inventory AddItems(this Inventory inventory, IEnumerable<GameItem> items)
{
return new Inventory(inventory.Items.Concat(items));
}
public static Inventory RemoveItem(this Inventory inventory, GameItem item)
{
return inventory.RemoveItems(new List<GameItem> {item});
}
public static Inventory RemoveItems(this Inventory inventory, IEnumerable<GameItem> items)
{
// REFACTOR: Look for a cleaner solution, with fewer temporary variables.
List<GameItem> workingInventory = inventory.Items.ToList();
IEnumerable<GameItem> itemsToRemove = items.ToList();
foreach(GameItem item in itemsToRemove)
{
workingInventory.Remove(item);
}
return new Inventory(workingInventory);
}
public static Inventory RemoveItems(this Inventory inventory,
IEnumerable<ItemQuantity> itemQuantities)
{
// REFACTOR
Inventory workingInventory = inventory;
foreach(ItemQuantity itemQuantity in itemQuantities)
{
for(int i = 0; i < itemQuantity.Quantity; i++)
{
workingInventory =
workingInventory
.RemoveItem(workingInventory
.Items
.First(item => item.ItemTypeID == itemQuantity.ItemID));
}
}
return workingInventory;
}
}
}
ExtensionMethods.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Engine.Models;
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]);
}
public static PlayerAttribute GetAttribute(this LivingEntity entity, string attributeKey)
{
return entity.Attributes
.First(pa => pa.Key.Equals(attributeKey,
StringComparison.CurrentCultureIgnoreCase));
}
public static List<GameItem> ItemsThatAre(this IEnumerable<GameItem> inventory,
GameItem.ItemCategory category)
{
return inventory.Where(i => i.Category == category).ToList();
}
}
}
Inventory.cs
using System.Collections.Generic;
using System.Linq;
using Engine.Shared;
using Newtonsoft.Json;
namespace Engine.Models
{
public class Inventory
{
#region Backing variables
private readonly List<GameItem> _backingInventory =
new List<GameItem>();
private readonly List<GroupedInventoryItem> _backingGroupedInventoryItems =
new List<GroupedInventoryItem>();
#endregion
#region Properties
public IReadOnlyList<GameItem> Items => _backingInventory.AsReadOnly();
[JsonIgnore]
public IReadOnlyList<GroupedInventoryItem> GroupedInventory =>
_backingGroupedInventoryItems.AsReadOnly();
[JsonIgnore]
public IReadOnlyList<GameItem> Weapons =>
_backingInventory.ItemsThatAre(GameItem.ItemCategory.Weapon).AsReadOnly();
[JsonIgnore]
public IReadOnlyList<GameItem> Consumables =>
_backingInventory.ItemsThatAre(GameItem.ItemCategory.Consumable).AsReadOnly();
[JsonIgnore]
public bool HasConsumable => Consumables.Any();
#endregion
#region Constructors
public Inventory(IEnumerable<GameItem> items = null)
{
if(items == null)
{
return;
}
foreach(GameItem item in items)
{
_backingInventory.Add(item);
AddItemToGroupedInventory(item);
}
}
#endregion
#region Public functions
public bool HasAllTheseItems(IEnumerable<ItemQuantity> items)
{
return items.All(item => Items.Count(i => i.ItemTypeID == item.ItemID) >= item.Quantity);
}
#endregion
#region Private functions
// REFACTOR: Look for a better way to do this (extension method?)
private void AddItemToGroupedInventory(GameItem item)
{
if(item.IsUnique)
{
_backingGroupedInventoryItems.Add(new GroupedInventoryItem(item, 1));
}
else
{
if(_backingGroupedInventoryItems.All(gi => gi.Item.ItemTypeID != item.ItemTypeID))
{
_backingGroupedInventoryItems.Add(new GroupedInventoryItem(item, 0));
}
_backingGroupedInventoryItems.First(gi => gi.Item.ItemTypeID == item.ItemTypeID).Quantity++;
}
}
#endregion
}
}
Step 4: Test the game
NEXT LESSON: Lesson 19.12: Decouple InventoryService from LivingEntity
PREVIOUS LESSON: Lesson 19.10: Create a GameState model for SaveGameService