Lesson 10.2: Grouping GameItems in Inventories

Inventories currently hold one object for each GameItem.

We need to do this because we will eventually have effects that are unique for each GameItem – such as enchanting a weapon. However, there are some GameItems that will never have special effects – like snakeskins.

We’re going to make changes to allow us to distinguish between unique items and non-unique items. This will let us have fewer lines in our Inventory datagrids.

 

 

 

 

Lesson Steps

Step 1: Modify Engine\Models\GameItem.cs

Add the Boolean “IsUnique” property to the GameItem class.

Add a parameter to the constructor, to let us pass in a Boolean value for the IsUnique property. The “= false” is the default value for this parameter. If we call the constructor without passing in this parameter, it will use a value of “false”.

You can only setup default values for parameters at the end of the parameter list. If you had a default value for a parameter in the middle of the parameter list, the compiler might not know which parameter is missing.

 

In the Clone function, on line 20, we want to pass the current object’s IsUnique value into its clone.

 

GameItem.cs

namespace Engine.Models
{
    public class GameItem
    {
        public int ItemTypeID { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }
        public bool IsUnique { get; set; }

        public GameItem(int itemTypeID, string name, int price, bool isUnique = false)
        {
            ItemTypeID = itemTypeID;
            Name = name;
            Price = price;
            IsUnique = isUnique;
        }

        public GameItem Clone()
        {
            return new GameItem(ItemTypeID, Name, Price, IsUnique);
        }
    }
}

 

Step 2: Modify Engine\Models\Weapon.cs

In the Weapon constructor, where we call the base class constructor (GameItem), pass “true” for the “isUnique” parameter.

This is because we want all weapons to be unique. Weapons will eventually be able to have enchantments, and maybe wear out from use. So, two “pointy sticks” could be different from each other.

 

Weapon.cs

namespace Engine.Models
{
    public class Weapon : GameItem
    {
        public int MinimumDamage { get; set; }
        public int MaximumDamage { get; set; }

        public Weapon(int itemTypeID, string name, int price, int minDamage, int maxDamage)
            : base(itemTypeID, name, price, true)
        {
            MinimumDamage = minDamage;
            MaximumDamage = maxDamage;
        }

        public new Weapon Clone()
        {
            return new Weapon(ItemTypeID, Name, Price, MinimumDamage, MaximumDamage);
        }
    }
}

 

Step 3: Create the new class Engine\Models\GroupedInventoryItem.cs

This class holds a GameItem object and a Quantity. It’s like ItemQuantity, except it holds a complete GameItem – instead of only the GameItem’s ID. This is because we need some of the GameItem’s properties (Name and Price), to display in the UI.

GroupedInventoryItem needs to inherit from BaseNotificationClass, so we can raise a property changed event when the Quantity increases or decreases.

 

GroupedInventoryItem.cs

namespace Engine.Models
{
    public class GroupedInventoryItem : BaseNotificationClass
    {
        private GameItem _item;
        private int _quantity;

        public GameItem Item
        {
            get { return _item; }
            set
            {
                _item = value; 
                OnPropertyChanged(nameof(Item));
            }
        }

        public int Quantity
        {
            get { return _quantity; }
            set
            {
                _quantity = value; 
                OnPropertyChanged(nameof(Quantity));
            }
        }

        public GroupedInventoryItem(GameItem item, int quantity)
        {
            Item = item;
            Quantity = quantity;
        }
    }
}

 

Step 4: Modify Engine\Models\LivingEntity.cs

On line 56, add the new property GroupedInventory, whose datatype is ObservableCollection<GroupedInventoryItem>.

Initialize that property on line 64 of the constructor.

 

Change the AddItemToInventory function on line 67. After adding the item to the Inventory, we need to either:

1) add it to GroupedInventory (if it is unique, or not already in GroupedInventory), or

2) increment its Quantity (if it’s already in GroupedInventory).

On line 79, notice that we add new non-unique items with a quantity of zero. That’s because we’re going to increment the quantity by one on line 82.

 

Change the RemoveItemFromInventory function on line 88.

On line 92, we get the first GroupedInventoryItem from the GroupedInventory property. If that value is not null (it should never be null, but it’s good to check), we’ll either:

1) completely remove the GroupedInventoryItem from GroupedInventory if its quantity is one (which it will always be for unique items, and could possibly be for non-unique items), or

2) decrease its quantity by one on line 103

 

We might make more changes in the future, so there is only one property for the inventory. But, this is how we will make the changes now.

 

LivingEntity.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace Engine.Models
{
    public abstract class LivingEntity : BaseNotificationClass
    {
        private string _name;
        private int _currentHitPoints;
        private int _maximumHitPoints;
        private int _gold;

        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged(nameof(Name));
            }
        }

        public int CurrentHitPoints
        {
            get { return _currentHitPoints; }
            set
            {
                _currentHitPoints = value;
                OnPropertyChanged(nameof(CurrentHitPoints));
            }
        }

        public int MaximumHitPoints
        {
            get { return _maximumHitPoints; }
            set
            {
                _maximumHitPoints = value;
                OnPropertyChanged(nameof(MaximumHitPoints));
            }
        }

        public int Gold
        {
            get { return _gold; }
            set
            {
                _gold = value;
                OnPropertyChanged(nameof(Gold));
            }
        }

        public ObservableCollection<GameItem> Inventory { get; set; }

        public ObservableCollection<GroupedInventoryItem> GroupedInventory { get; set; }

        public List<GameItem> Weapons =>
            Inventory.Where(i => i is Weapon).ToList();

        protected LivingEntity()
        {
            Inventory = new ObservableCollection<GameItem>();
            GroupedInventory = new ObservableCollection<GroupedInventoryItem>();
        }

        public void AddItemToInventory(GameItem item)
        {
            Inventory.Add(item);

            if(item.IsUnique)
            {
                GroupedInventory.Add(new GroupedInventoryItem(item, 1));
            }
            else
            {
                if(!GroupedInventory.Any(gi => gi.Item.ItemTypeID == item.ItemTypeID))
                {
                    GroupedInventory.Add(new GroupedInventoryItem(item, 0));
                }

                GroupedInventory.First(gi => gi.Item.ItemTypeID == item.ItemTypeID).Quantity++;
            }

            OnPropertyChanged(nameof(Weapons));
        }

        public void RemoveItemFromInventory(GameItem item)
        {
            Inventory.Remove(item);

            GroupedInventoryItem groupedInventoryItemToRemove =
                GroupedInventory.FirstOrDefault(gi => gi.Item == item);

            if(groupedInventoryItemToRemove != null)
            {
                if(groupedInventoryItemToRemove.Quantity == 1)
                {
                    GroupedInventory.Remove(groupedInventoryItemToRemove);
                }
                else
                {
                    groupedInventoryItemToRemove.Quantity--;
                }
            }

            OnPropertyChanged(nameof(Weapons));
        }
    }
}

 

Step 5: Modify WPFUI\MainWindow.xaml

Now we need to change the UI datagrid to use the new GroupedInventory properties.

On line 168, change the DataGrid’s ItemSource to “CurrentPlayer.GroupedInventory”.

Change the Paths for the DataGridTextColumns (lines 173 and 180) to include “Item.”, because GroupedInventory holds a complete GameItem object as its “Item” property, and we want to display the “Name” or “Price” property from “Item”.

I also added a new DataGridTextColumn on 175-178, to display the Quantity value.

 

MainWindow.xaml

<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="Scott's Awesome Game" Height="768" Width="1024">

    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="225"/>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="250"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <!-- Menu -->
        <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Content="Menu" Background="AliceBlue"/>

        <!-- Player stats -->
        <Grid Grid.Row="1" Grid.Column="0" Background="Aquamarine">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>

            <Label Grid.Row="0" Grid.Column="0" Content="Name:"/>
            <Label Grid.Row="0" Grid.Column="1" Content="{Binding CurrentPlayer.Name}"/>
            <Label Grid.Row="1" Grid.Column="0" Content="Class:"/>
            <Label Grid.Row="1" Grid.Column="1" Content="{Binding CurrentPlayer.CharacterClass}"/>
            <Label Grid.Row="2" Grid.Column="0" Content="Hit points:"/>
            <Label Grid.Row="2" Grid.Column="1" Content="{Binding CurrentPlayer.CurrentHitPoints}"/>
            <Label Grid.Row="3" Grid.Column="0" Content="Gold:"/>
            <Label Grid.Row="3" Grid.Column="1" Content="{Binding CurrentPlayer.Gold}"/>
            <Label Grid.Row="4" Grid.Column="0" Content="XP:"/>
            <Label Grid.Row="4" Grid.Column="1" Content="{Binding CurrentPlayer.ExperiencePoints}"/>
            <Label Grid.Row="5" Grid.Column="0" Content="Level:"/>
            <Label Grid.Row="5" Grid.Column="1" Content="{Binding CurrentPlayer.Level}"/>
        </Grid>

        <!-- Gameplay -->
        <Grid Grid.Row="1" Grid.Column="1"
              Background="Beige">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="3*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>

            <!-- Game Messages -->
            <Border Grid.Row="0" Grid.Column="0"
                    Grid.RowSpan="2"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">

                <RichTextBox x:Name="GameMessages"
                             Background="Beige"
                             VerticalScrollBarVisibility="Auto">
                    <RichTextBox.Resources>
                        <Style TargetType="{x:Type Paragraph}">
                            <Setter Property="Margin" Value="0"/>
                        </Style>
                    </RichTextBox.Resources>
                </RichTextBox>

            </Border>

            <!-- Location information -->
            <Border Grid.Row="0" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">

                <Grid Margin="3">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>

                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Name}"/>

                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentLocation.ImageName}"/>

                    <TextBlock Grid.Row="2"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Description}"
                               TextWrapping="Wrap"/>
                </Grid>

            </Border>

            <!-- Monster information -->
            <Border Grid.Row="1" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">

                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Height="Auto"
                               Text="{Binding CurrentMonster.Name}" />

                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentMonster.ImageName}" />

                    <StackPanel Grid.Row="2"
                                Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                                HorizontalAlignment="Center"
                                Orientation="Horizontal">
                        <TextBlock>Current Hit Points:</TextBlock>
                        <TextBlock Text="{Binding CurrentMonster.CurrentHitPoints}" />
                    </StackPanel>

                </Grid>

            </Border>

        </Grid>

        <!-- Inventory and Quests -->
        <Grid Grid.Row="2" Grid.Column="0"
              Background="BurlyWood">

            <TabControl>
                <TabItem Header="Inventory">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.GroupedInventory}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Description"
                                                Binding="{Binding Item.Name}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Qty"
                                                IsReadOnly="True"
                                                Width="Auto"
                                                Binding="{Binding Quantity}"/>
                            <DataGridTextColumn Header="Price"
                                                Binding="{Binding Item.Price}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>

                <TabItem Header="Quests">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Quests}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding PlayerQuest.Name}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Done?"
                                                Binding="{Binding IsCompleted}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
            </TabControl>
        </Grid>

        <!-- Action controls -->
        <Grid Grid.Row="2" Grid.Column="1"
              Background="Lavender">

            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="255" />
            </Grid.ColumnDefinitions>

            <!-- Combat Controls -->
            <Grid Grid.Row="0" Grid.Column="0"
                  Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="150"/>
                    <ColumnDefinition Width="10"/>
                    <ColumnDefinition Width="50"/>
                </Grid.ColumnDefinitions>

                <ComboBox Grid.Row="0" Grid.Column="0"
                          ItemsSource="{Binding CurrentPlayer.Weapons}"
                          SelectedItem="{Binding CurrentWeapon}"
                          DisplayMemberPath="Name"
                          SelectedValuePath="ID"/>

                <Button Grid.Row="0" Grid.Column="2"
                        Content="Use"
                        Click="OnClick_AttackMonster"/>
            </Grid>

            <!-- Movement Controls -->
            <Grid Grid.Row="0" Grid.Column="1">

                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <Button Grid.Row="0" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveNorth"
                        Visibility="{Binding HasLocationToNorth, Converter={StaticResource BooleanToVisibility}}"
                        Content="North"/>
                <Button Grid.Row="1" Grid.Column="0" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveWest"
                        Visibility="{Binding HasLocationToWest, Converter={StaticResource BooleanToVisibility}}"
                        Content="West"/>
                <Button Grid.Row="1" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_DisplayTradeScreen"
                        Visibility="{Binding HasTrader, Converter={StaticResource BooleanToVisibility}}"
                        Content="Trade"/>
                <Button Grid.Row="1" Grid.Column="2" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveEast"
                        Visibility="{Binding HasLocationToEast, Converter={StaticResource BooleanToVisibility}}"
                        Content="East"/>
                <Button Grid.Row="2" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveSouth"
                        Visibility="{Binding HasLocationToSouth, Converter={StaticResource BooleanToVisibility}}"
                        Content="South"/>

            </Grid>

        </Grid>

    </Grid>
</Window>

 

Step 6: Modify WPFUI\TradeScreen.xaml and TradeScreen.xaml.cs

We also need to update the inventory datagrids on the trading screen. These are the same changes as we made for MainWindow.

Lines 40 and 71 need to use GroupedInventory, instead of Inventory.

Lines 51, 59, 86, and 94 need to have “Item.” Added to the property paths.

Add new DataGridTextColumns for the Quantity on lines 52-55 and 87-90.

Finally, change the button text to include “1”, so the player knows they are selling or buying one of the item, and not the total quantity of that item. The button changes are on lines 65 and 100.

 

In TradeScreen.xaml.cs, we need to change the buy and sell functions, because the datagrids now contain GroupedInventoryItem objects, instead of ItemQuantity objects.

 

In OnClick_Sell and OnClick_Buy, we need to cast the event sender to a GroupedInventoryItem object. Then, change the buying and selling code to use groupedInventoryItem.Item.Price and groupedInventoryItem.Item.

 

TradeScreen.xaml

<Window x:Class="WPFUI.TradeScreen"
        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"
        WindowStartupLocation="CenterOwner"
        FontSize="11pt"
        Title="Trade Screen" Height="480" Width="640">

    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        
        <Label Grid.Row="0" Grid.Column="0"
               Grid.ColumnSpan="2"
               HorizontalAlignment="Center"
               Content="{Binding CurrentTrader.Name}"/>
        
        <Label Grid.Row="1" Grid.Column="0"
               HorizontalAlignment="Center"
               Content="Your Inventory"/>
        <Label Grid.Row="1" Grid.Column="1"
               HorizontalAlignment="Center"
               Content="Trader's Inventory"/>
        
        <DataGrid Grid.Row="2" Grid.Column="0"
                  Margin="10"
                  ItemsSource="{Binding CurrentPlayer.GroupedInventory}"
                  AutoGenerateColumns="False"
                  HeadersVisibility="Column"
                  CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  VerticalScrollBarVisibility="Auto">

            <DataGrid.Columns>
                <DataGridTextColumn Header="Description"
                                    IsReadOnly="True"
                                    Width="*"
                                    Binding="{Binding Item.Name}"/>
                <DataGridTextColumn Header="Qty"
                                    IsReadOnly="True"
                                    Width="Auto"
                                    Binding="{Binding Quantity}"/>
                <DataGridTextColumn Header="Price"
                                    IsReadOnly="True"
                                    Width="Auto"
                                    Binding="{Binding Item.Price}"/>
                <DataGridTemplateColumn MinWidth="75">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Button Click="OnClick_Sell"
                                    Width="55"
                                    Content="Sell 1"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>

        </DataGrid>

        <DataGrid Grid.Row="2" Grid.Column="1"
                  Margin="10"
                  ItemsSource="{Binding CurrentTrader.GroupedInventory}"
                  AutoGenerateColumns="False"
                  HeadersVisibility="Column"
                  CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  VerticalScrollBarVisibility="Auto">

            <DataGrid.Columns>
                <DataGridTextColumn Header="Description"
                                    IsReadOnly="True"
                                    Width="*"
                                    Binding="{Binding Item.Name}"/>
                <DataGridTextColumn Header="Qty"
                                    IsReadOnly="True"
                                    Width="Auto"
                                    Binding="{Binding Quantity}"/>
                <DataGridTextColumn Header="Price"
                                    IsReadOnly="True"
                                    Width="Auto"
                                    Binding="{Binding Item.Price}"/>
                <DataGridTemplateColumn MinWidth="75">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Button Click="OnClick_Buy"
                                    Width="55"
                                    Content="Buy 1"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>

        </DataGrid>

        <Button Grid.Row="3" Grid.Column="1"
                HorizontalAlignment="Right"
                Width="75"
                Content="Close"
                Click="OnClick_Close"/>
    </Grid>
</Window>

 

TradeScreen.xaml.cs

using System.Windows;
using Engine.Models;
using Engine.ViewModels;

namespace WPFUI
{
    /// <summary>
    /// Interaction logic for TradeScreen.xaml
    /// </summary>
    public partial class TradeScreen : Window
    {
        public GameSession Session => DataContext as GameSession;

        public TradeScreen()
        {
            InitializeComponent();
        }

        private void OnClick_Sell(object sender, RoutedEventArgs e)
        {
            GroupedInventoryItem groupedInventoryItem = 
                ((FrameworkElement)sender).DataContext as GroupedInventoryItem;

            if(groupedInventoryItem != null)
            {
                Session.CurrentPlayer.Gold += groupedInventoryItem.Item.Price;
                Session.CurrentTrader.AddItemToInventory(groupedInventoryItem.Item);
                Session.CurrentPlayer.RemoveItemFromInventory(groupedInventoryItem.Item);
            }
        }

        private void OnClick_Buy(object sender, RoutedEventArgs e)
        {
            GroupedInventoryItem groupedInventoryItem = 
                ((FrameworkElement)sender).DataContext as GroupedInventoryItem;

            if(groupedInventoryItem != null)
            {
                if(Session.CurrentPlayer.Gold >= groupedInventoryItem.Item.Price)
                {
                    Session.CurrentPlayer.Gold -= groupedInventoryItem.Item.Price;
                    Session.CurrentTrader.RemoveItemFromInventory(groupedInventoryItem.Item);
                    Session.CurrentPlayer.AddItemToInventory(groupedInventoryItem.Item);
                }
                else
                {
                    MessageBox.Show("You do not have enough gold");
                }
            }
        }

        private void OnClick_Close(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}

 

Step 7: Test the game

Fight some monsters, collect loot items, and visit the trader. make sure there are multiple lines for weapons, and only one line for each non-unique item (like snakeskins).

 

Return to main page

14 thoughts on “Lesson 10.2: Grouping GameItems in Inventories

  1. Not sure whether it is my mistake. But in line 93 or LivingEntity, I think it should be gi.Item.ItemTypeID == item.ItemTypeID. Becuase otherwise when I try to finish one quest, only one item is removed from inventory.

    1. This should be handled in the code that completes the quest:

      foreach (ItemQuantity itemQuantity in quest.ItemsToComplete)
      {
      for(int i = 0; i < itemQuantity.Quantity; i++) { CurrentPlayer.RemoveItemFromInventory(CurrentPlayer.Inventory.First(item => item.ItemTypeID == itemQuantity.ItemID));
      }
      }

      If you need to turn in five snake skins, the inner loop calls RemoveItemFromInventory five times (the “for” loop would run five times, because itemQuantity.Quantity is 5). That loop finds the first item with a matching ItemTypeID and passes that complete item to the RemoveItemFromInventory function.

      When you complete a quest with multiple QuestCompletionItems, is the program not removing them all?

      1. Where you have RemoveItemFromInventory inside the LivingEntity, you only have it check for the gi.Item == Item. All this does is remove 1 item from your inventory when you try to complete your quest. Doing whatCuichen does fixes the issue

        1. Are you talking about the Inventory property, or the GroupedInventory property? For the Inventory property, we call it multiple times in the loop in the QuestCompletion code. So, it looks like it’s deleting everything properly when I tested it. Can you upload your version to gist.github.com (or some other place), so I can see exactly where you are talking about making the change?

  2. I have same problem like “cuichen li”. When i finish quest only one item is remove from inventory and i dont know why and what can i do to fix it

    1. Can you show me a screenshot of your inventory before and after turning in the quest? When I tested this, I do not see the problem. But, If I can see your exact inventory, I can try to reproduce the problem.

      1. Screenshot : https://imgur.com/a/AgslwXs

        When i debug code i see this

                                    for (int i = 0; i < itemQuantity.Quantity; i++)
                                    {
                                        CurrentPlayer.RemoveItemFromInv(CurrentPlayer.Inventory.First(item => item.ItemTypeID == itemQuantity.ItemID));
                                    }

        is running normally but when it move to this

                    if(grupInvItemsToRemove != null)
                    {
                        if(grupInvItemsToRemove.Quantity == 1)
                        {
                            GrupInv.Remove(grupInvItemsToRemove);
                        }
                        else
                        {
                            grupInvItemsToRemove.Quantity--;
                        }
                    }

        first time work fine. But in 2nd loop code see this item = null and dont start code

                        if(grupInvItemsToRemove.Quantity == 1)
                        {
                            GrupInv.Remove(grupInvItemsToRemove);
                        }
                        else
                        {
                            grupInvItemsToRemove.Quantity--;
                        }

        I think you understand me:D

        1. Thank you. After I finish the lessons for converting the factories to use XML files, I’ll do a lesson to fix this (maybe by changing how we do PropertyChanged notifications).

  3. Hi Scott,
    I have a question at LivingEntity.cs line 77

    if(!GroupedInventory.Any(gi => gi.Item.ItemTypeID == item.ItemTypeID))

    Can it be also

    if(!GroupedInventory.Any(gi => gi.Item == item))

    like this?

  4. Hi Scott!

    I think I found a missing part. 🙂

    In Gamesession.cs,

    foreach (GameItem gameItem in CurrentMonster.Inventory)
    {
    CurrentPlayer.AddItemToInventory(gameItem);
    RaiseMessage($”You obtained one {gameItem.Name}.”);
    }

    CurrentPlayer.Inventory.Add(gameitem);

    –> CurrentPlayer.AddItemToInventory(gameItem);

    I think you forgot to mention this.
    Am I correct? If so, I am so happy. Because I am getting better because of your lecture!

  5. I noticed some runtime errors after implementing these changes. I didn’t roll back to see if they were present before but the Error is:

    System.Windows.Data Error: 40 : BindingExpression path error: 'ID' property not found on 'object' ''Weapon' (HashCode=41844142)'. BindingExpression:Path=ID; DataItem='Weapon' (HashCode=41844142); target element is 'ComboBox' (Name=''); target property is 'NoTarget' (type 'Object')
    

    The program seems to run with these errors. I believe the TradeScreen is causing the error. Does the ComboBox in MainWindow.xaml need to be refactored?

                    <ComboBox Grid.Row="0" Grid.Column="0"
                              ItemsSource="{Binding CurrentPlayer.Weapons}"
                              SelectedItem="{Binding CurrentWeapon}"
                              DisplayMemberPath="Name"
                              SelectedValuePath="ID"/>
    

     

    1. I made a note to check the XAML bindings. After I post the final video for converting the game data to XML files, I’m going to work on bug fixes and general improvements to the program.

Leave a Reply

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