Press "Enter" to skip to content

Lesson 20.2: Create floating quest and recipe canvases

Now we’ll create popup canvases for the quest and recipes information.

This code will be basically the same as what we did for the inventory canvas.

Step 1: Modify SOSCSRPG.ViewModels\GameSession.cs

On lines 94 and 95, add new PopupDetails properties “QuestDetails” and “RecipesDetail”, to hold the visibility and position values for the new canvases.

In the constructor, starting on line 141, populate the new QuestDetails and RecipesDetails properties with their initial values.

I also changed some of the InventoryDetails initial values (Top and Left), so it will fit in better with the new QuestDetails and RecipesDetails popups.

using System.ComponentModel;
using System.Linq;
using SOSCSRPG.Services.Factories;
using SOSCSRPG.Models;
using SOSCSRPG.Services;
using Newtonsoft.Json;
using SOSCSRPG.Core;
namespace SOSCSRPG.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; }
        public PopupDetails InventoryDetails { get; set; }
        public PopupDetails QuestDetails { get; set; }
        public PopupDetails RecipesDetails { get; 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);
            // Setup popup window properties
            InventoryDetails = new PopupDetails
            {
                IsVisible = false,
                Top = 500,
                Left = 10,
                MinHeight = 75,
                MaxHeight = 175,
                MinWidth = 250,
                MaxWidth = 400
            };
            QuestDetails = new PopupDetails
            {
                IsVisible = false,
                Top = 500,
                Left = 275,
                MinHeight = 75,
                MaxHeight = 175,
                MinWidth = 250,
                MaxWidth = 400
            };
            RecipesDetails = new PopupDetails
            {
                IsVisible = false,
                Top = 500,
                Left = 575,
                MinHeight = 75,
                MaxHeight = 175,
                MinWidth = 250,
                MaxWidth = 400
            };
        }
        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}!");
        }
    }
}

GameSession.cs

Step 2: Modify WPFUI\MainWindow.xaml

The first thing I did was remove the “x:Name” attribute on the InventoryDetails Canvas (line 63). I originally thought we might need a name for the Canvas, so we could control it in the code-behind. However, the data binding does everything we need.

On lines 130-201, add the Canvas for the QuestDetails. I copy-pasted the Canvas for InventoryDetails and changed the bound values, label, click event handler (for the “X” closing button), and DataGridColumns to the values need for the QuestDetails.

Bind the Canvas attributes to the QuestDetails properties (lines 131-137).

Change the Label’s Content to “Quests” (line 156).

Change the Button’s Click handler to “CloseQuestsWindow_OnClick” (line 162).

Change the DataGrid’s ItemSource to “CurrentPlayer.Quests” (line 172).

Replace the DataGrid.Columns (lines 180-194) with the values from the original Quests DataGrid that was in the TabItem (lines 307-321)

On lines 203-282, repeat this for the RecipesDetails.

Bind the Canvas attributes to the RecipesDetails properties (lines 204-210).

Change the Label’s Content to “Recipes” (line 229).

Change the Button’s Click handler to “CloseRecipesWindow_OnClick” (line 235).

Change the DataGrid’s ItemSource to “CurrentPlayer.Recipes” (line 245).

Replace the DataGrid.Columns (lines 253-273) with the values from the original Recipes DataGrid that was in the TabItem (lines 330-350)

Now that we don’t need the old TabItems, to display Quests and Recipes, we can delete the Grid code from lines 296-354.

We can also widen the Action controls grid to take over the space where the Inventory, Quests, and Recipes used to be. Change its Grid.Column to 0 (line 448) and add Grid.ColumnSpan=”2″ (line 449).

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:SOSCSRPG.ViewModels;assembly=SOSCSRPG.ViewModels"
        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">
    <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 -->
        <Menu Grid.Row="0" Grid.Column="0"
              Grid.ColumnSpan="2"
              FontSize="11pt"
              Background="AliceBlue">
            <MenuItem Header="File">
                <MenuItem Header="New Game"
                          Click="StartNewGame_OnClick"/>
                <MenuItem Header="Save Game"
                          Click="SaveGame_OnClick"/>
                <Separator/>
                <MenuItem Header="Exit"
                          Click="Exit_OnClick"/>
            </MenuItem>
            <MenuItem Header="Help">
                <MenuItem Header="Help"
                          IsEnabled="False"/>
                <Separator/>
                <MenuItem Header="About"
                          IsEnabled="False"/>
            </MenuItem>
        </Menu>
        <!-- Main game canvas (full window) -->
        <Canvas Grid.Row="1" Grid.Column="0"
                Grid.RowSpan="2"
                Grid.ColumnSpan="2"
                x:Name="GameCanvas"
                ZIndex="99">
            <!-- Player Inventory Details -->
            <Canvas Top="{Binding InventoryDetails.Top}" Left="{Binding InventoryDetails.Left}"
                    Width="Auto" Height="Auto"
                    MinHeight="{Binding InventoryDetails.MinHeight}"
                    MaxHeight="{Binding InventoryDetails.MaxHeight}"
                    MinWidth="{Binding InventoryDetails.MinWidth}"
                    MaxWidth="{Binding InventoryDetails.MaxWidth}"
                    Visibility="{Binding InventoryDetails.IsVisible, Converter={StaticResource BooleanToVisibility}}">
                <Border BorderBrush="Navy" BorderThickness="3"
                        Background="LightSteelBlue">
                    <Grid Margin="2,2,2,2">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="4"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <Label Grid.Row="0" Grid.Column="0"
                               HorizontalAlignment="Left"
                               FontWeight="Bold"
                               Content="Inventory"/>
                        <Button Grid.Row="0" Grid.Column="1"
                                Width="25"
                                FontWeight="Bold"
                                Content="X"
                                Click="CloseInventoryWindow_OnClick">
                            <Button.Resources>
                                <Style TargetType="Border">
                                    <Setter Property="CornerRadius" Value="3"/>
                                </Style>
                            </Button.Resources>
                        </Button>
                        <DataGrid Grid.Row="2" Grid.Column="0"
                                  Grid.ColumnSpan="2"
                                  ItemsSource="{Binding CurrentPlayer.Inventory.GroupedInventory}"
                                  AutoGenerateColumns="False"
                                  HeadersVisibility="Column"
                                  VerticalScrollBarVisibility="Auto"
                                  MaxHeight="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=MaxHeight}"
                                  Width="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=ActualWidth}">
                            <DataGrid.Columns>
                                <DataGridTextColumn Header="Description"
                                                    Binding="{Binding Item.Name, Mode=OneWay}"
                                                    Width="*"/>
                                <DataGridTextColumn Header="Qty"
                                                    IsReadOnly="True"
                                                    Width="Auto"
                                                    Binding="{Binding Quantity, Mode=OneWay}"/>
                                <DataGridTextColumn Header="Price"
                                                    Binding="{Binding Item.Price, Mode=OneWay}"
                                                    Width="Auto"/>
                            </DataGrid.Columns>
                        </DataGrid>
                    </Grid>
                </Border>
            </Canvas>
            <!-- Player Quests Details -->
            <Canvas Top="{Binding QuestDetails.Top}" Left="{Binding QuestDetails.Left}"
                    Width="Auto" Height="Auto"
                    MinHeight="{Binding QuestDetails.MinHeight}"
                    MaxHeight="{Binding QuestDetails.MaxHeight}"
                    MinWidth="{Binding QuestDetails.MinWidth}"
                    MaxWidth="{Binding QuestDetails.MaxWidth}"
                    Visibility="{Binding QuestDetails.IsVisible, Converter={StaticResource BooleanToVisibility}}">
                <Border BorderBrush="Navy" BorderThickness="3"
                        Background="LightSteelBlue">
                    <Grid Margin="2,2,2,2">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="4"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <Label Grid.Row="0" Grid.Column="0"
                               HorizontalAlignment="Left"
                               FontWeight="Bold"
                               Content="Quests"/>
                        <Button Grid.Row="0" Grid.Column="1"
                                Width="25"
                                FontWeight="Bold"
                                Content="X"
                                Click="CloseQuestsWindow_OnClick">
                            <Button.Resources>
                                <Style TargetType="Border">
                                    <Setter Property="CornerRadius" Value="3"/>
                                </Style>
                            </Button.Resources>
                        </Button>
                        <DataGrid Grid.Row="2" Grid.Column="0"
                                  Grid.ColumnSpan="2"
                                  ItemsSource="{Binding CurrentPlayer.Quests}"
                                  AutoGenerateColumns="False"
                                  HeadersVisibility="Column"
                                  VerticalScrollBarVisibility="Auto"
                                  MaxHeight="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=MaxHeight}"
                                  Width="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=ActualWidth}">
                            <DataGrid.Columns>
                                <DataGridTextColumn Header="Name"
                                                    Binding="{Binding PlayerQuest.Name, Mode=OneWay}"
                                                    Width="*">
                                    <DataGridTextColumn.CellStyle>
                                        <Style TargetType="DataGridCell">
                                            <Setter Property="ToolTip" 
                                                    Value="{Binding PlayerQuest.ToolTipContents}"/>
                                        </Style>
                                    </DataGridTextColumn.CellStyle>
                                </DataGridTextColumn>
                                <DataGridTextColumn Header="Done?"
                                                    Binding="{Binding IsCompleted, Mode=OneWay}"
                                                    Width="Auto"/>
                            </DataGrid.Columns>
                        </DataGrid>
                    </Grid>
                </Border>
            </Canvas>
            <!-- Player Recipes Details -->
            <Canvas Top="{Binding RecipesDetails.Top}" Left="{Binding RecipesDetails.Left}"
                    Width="Auto" Height="Auto"
                    MinHeight="{Binding RecipesDetails.MinHeight}"
                    MaxHeight="{Binding RecipesDetails.MaxHeight}"
                    MinWidth="{Binding RecipesDetails.MinWidth}"
                    MaxWidth="{Binding RecipesDetails.MaxWidth}"
                    Visibility="{Binding RecipesDetails.IsVisible, Converter={StaticResource BooleanToVisibility}}">
                <Border BorderBrush="Navy" BorderThickness="3"
                        Background="LightSteelBlue">
                    <Grid Margin="2,2,2,2">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="4"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <Label Grid.Row="0" Grid.Column="0"
                               HorizontalAlignment="Left"
                               FontWeight="Bold"
                               Content="Recipes"/>
                        <Button Grid.Row="0" Grid.Column="1"
                                Width="25"
                                FontWeight="Bold"
                                Content="X"
                                Click="CloseRecipesWindow_OnClick">
                            <Button.Resources>
                                <Style TargetType="Border">
                                    <Setter Property="CornerRadius" Value="3"/>
                                </Style>
                            </Button.Resources>
                        </Button>
                        <DataGrid Grid.Row="2" Grid.Column="0"
                                  Grid.ColumnSpan="2"
                                  ItemsSource="{Binding CurrentPlayer.Recipes}"
                                  AutoGenerateColumns="False"
                                  HeadersVisibility="Column"
                                  VerticalScrollBarVisibility="Auto"
                                  MaxHeight="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=MaxHeight}"
                                  Width="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=ActualWidth}">
                            <DataGrid.Columns>
                                <DataGridTextColumn Header="Name"
                                                    Binding="{Binding Name, Mode=OneWay}"
                                                    Width="*">
                                    <DataGridTextColumn.CellStyle>
                                        <Style TargetType="DataGridCell">
                                            <Setter Property="ToolTip" 
                                                    Value="{Binding ToolTipContents}"/>
                                        </Style>
                                    </DataGridTextColumn.CellStyle>
                                </DataGridTextColumn>
                                <DataGridTemplateColumn MinWidth="75">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <Button Click="OnClick_Craft"
                                                    Width="55"
                                                    Content="Craft"/>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                </DataGridTemplateColumn>
                            </DataGrid.Columns>
                        </DataGrid>
                    </Grid>
                </Border>
            </Canvas>
        </Canvas>
        <!-- Player stats -->
        <Grid Grid.Row="1" Grid.Column="0" Background="Aquamarine"
              ZIndex="1">
            <Grid.RowDefinitions>
                <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="Hit points:"/>
            <Label Grid.Row="1" Grid.Column="1" Content="{Binding CurrentPlayer.CurrentHitPoints}"/>
            <Label Grid.Row="2" Grid.Column="0" Content="Gold:"/>
            <Label Grid.Row="2" Grid.Column="1" Content="{Binding CurrentPlayer.Gold}"/>
            <Label Grid.Row="3" Grid.Column="0" Content="XP:"/>
            <Label Grid.Row="3" Grid.Column="1" Content="{Binding CurrentPlayer.ExperiencePoints}"/>
            <Label Grid.Row="4" Grid.Column="0" Content="Level:"/>
            <Label Grid.Row="4" Grid.Column="1" Content="{Binding CurrentPlayer.Level}"/>
            <!-- Player Attributes -->
            <ListBox Grid.Row="5" Grid.Column="0"
                      Grid.ColumnSpan="2"
                      Background="Aquamarine"
                      BorderThickness="0"
                      Grid.IsSharedSizeScope="True"
                      ItemsSource="{Binding CurrentPlayer.Attributes}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition SharedSizeGroup="Description"/>
                                </Grid.ColumnDefinitions>
                                <TextBlock Text="{Binding DisplayName}" 
                                           HorizontalAlignment="Left"
                                           MinWidth="100"/>
                            </Grid>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition SharedSizeGroup="ModifiedValue"/>
                                </Grid.ColumnDefinitions>
                                <TextBlock Text="{Binding ModifiedValue}"
                                           HorizontalAlignment="Right"/>
                            </Grid>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
        <!-- Gameplay -->
        <Grid Grid.Row="1" Grid.Column="1"
              Background="Beige"
              ZIndex="1">
            <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, 
                                            Converter={StaticResource FileToBitmapConverter}}"/>
                    <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, 
                                            Converter={StaticResource FileToBitmapConverter}}"/>
                    <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>
        <!-- Action controls -->
        <Grid Grid.Row="2" Grid.Column="0"
              Grid.ColumnSpan="2"
              Background="Lavender"
              ZIndex="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="255" />
            </Grid.ColumnDefinitions>
            <!-- Combat Controls -->
            <Grid Grid.Row="0" Grid.Column="0"
                  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"
                          Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                          ItemsSource="{Binding CurrentPlayer.Inventory.Weapons}"
                          SelectedItem="{Binding CurrentPlayer.CurrentWeapon}"
                          DisplayMemberPath="Name"/>
                <Button Grid.Row="0" Grid.Column="2"
                        Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                        Content="Use"
                        Click="OnClick_AttackMonster"/>
                <ComboBox Grid.Row="1" Grid.Column="0"
                          Visibility="{Binding CurrentPlayer.Inventory.HasConsumable, Converter={StaticResource BooleanToVisibility}}"
                          ItemsSource="{Binding CurrentPlayer.Inventory.Consumables}"
                          SelectedItem="{Binding CurrentPlayer.CurrentConsumable}"
                          DisplayMemberPath="Name"/>
                <Button Grid.Row="1" Grid.Column="2"
                        Visibility="{Binding CurrentPlayer.Inventory.HasConsumable, Converter={StaticResource BooleanToVisibility}}"
                        Content="Use"
                        Click="OnClick_UseCurrentConsumable"/>
            </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 3: Modify WPFUI\MainWindow.xaml.cs

On lines 110 and 111, change the SetTabFocusTo calls with changing the QuestDetails.IsVisible and RecipesDetails.IsVisible properties’ values.

Delete the SetTabFocusTo function (lines 125-138), since we don’t have the Inventory, Quest, or Recipes tabs to focus on.

On lines 196-204, add the two new functions to handle closing the popup windows for the Quests Canvas and the Recipes Canvas.

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 SOSCSRPG.Models;
using SOSCSRPG.Services;
using SOSCSRPG.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;
        private Point? _dragStart;
        public MainWindow(Player player, int xLocation = 0, int yLocation = 0)
        {
            InitializeComponent();
            InitializeUserInputActions();
            SetActiveGameSessionTo(new GameSession(player, xLocation, yLocation));
            // Enable drag for popup details canvases
            foreach (UIElement element in GameCanvas.Children)
            {
                if (element is Canvas)
                {
                    element.MouseDown += GameCanvas_OnMouseDown;
                    element.MouseMove += GameCanvas_OnMouseMove;
                    element.MouseUp += GameCanvas_OnMouseUp;
                }
            }
        }
        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, () => _gameSession.InventoryDetails.IsVisible = !_gameSession.InventoryDetails.IsVisible);
            _userInputActions.Add(Key.Q, () => _gameSession.QuestDetails.IsVisible = !_gameSession.QuestDetails.IsVisible);
            _userInputActions.Add(Key.R, () => _gameSession.RecipesDetails.IsVisible = !_gameSession.RecipesDetails.IsVisible);
            _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();
                e.Handled = true;
            }
        }
        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);
            }
        }
        private void CloseInventoryWindow_OnClick(object sender, RoutedEventArgs e)
        {
            _gameSession.InventoryDetails.IsVisible = false;
        }
        private void CloseQuestsWindow_OnClick(object sender, RoutedEventArgs e)
        {
            _gameSession.QuestDetails.IsVisible = false;
        }
        private void CloseRecipesWindow_OnClick(object sender, RoutedEventArgs e)
        {
            _gameSession.RecipesDetails.IsVisible = false;
        }
        private void GameCanvas_OnMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton != MouseButton.Left)
            {
                return;
            }
            UIElement movingElement = (UIElement)sender;
            _dragStart = e.GetPosition(movingElement);
            movingElement.CaptureMouse();
            e.Handled = true;
        }
        private void GameCanvas_OnMouseMove(object sender, MouseEventArgs e)
        {
            if (_dragStart == null || e.LeftButton != MouseButtonState.Pressed)
            {
                return;
            }
            Point mousePosition = e.GetPosition(GameCanvas);
            UIElement movingElement = (UIElement)sender;
            // Don't let player move popup details off the board
            if (mousePosition.X < _dragStart.Value.X ||
                mousePosition.Y < _dragStart.Value.Y ||
                mousePosition.X > GameCanvas.ActualWidth - ((Canvas)movingElement).ActualWidth + _dragStart.Value.X ||
                mousePosition.Y > GameCanvas.ActualHeight - ((Canvas)movingElement).ActualHeight + _dragStart.Value.Y)
            {
                return;
            }
            Canvas.SetLeft(movingElement, mousePosition.X - _dragStart.Value.X);
            Canvas.SetTop(movingElement, mousePosition.Y - _dragStart.Value.Y);
            e.Handled = true;
        }
        private void GameCanvas_OnMouseUp(object sender, MouseButtonEventArgs e)
        {
            var movingElement = (UIElement)sender;
            movingElement.ReleaseMouseCapture();
            _dragStart = null;
            e.Handled = true;
        }
    }
}

Step 4: Test the game

NEXT LESSON: Lesson 20.3: Create floating player details canvas

PREVIOUS LESSON: Lesson 20.1: Create floating inventory canvas

    Leave a Reply

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