Lesson 20.4: Create floating game messages canvas
Now we’ll add a popup canvas for game messages.
Step 1: Modify SOSCSRPG.ViewModels\GameSession.cs
We’re going to add a new property that will be an ObservableCollection of strings – to hold the game messages. The popup will bind to this new property.
To start, we need to add new “using” directives for System and System.Collections.ObjectModel.
Next, on line 13, we’ll add “IDisposable” as an interface this class will implement. This is a better way for us to handle creating new GameSession objects and un-subscribing from the message broker.
Add the new ObservableCollection<string> property for the game messages on lines 95-97 and add its new PopupDetails property to line 103.
In the constructor, on lines 182-191, add the setup values for the game message PopupDetails.
On line 193, subscribe to the message broker’s OnMessageRaised event. Have the event call a new OnGameMessageRaised function.
Add the new OnGameMessageRaised function on lines 233-241. Now, when the message broker reports a new message, this function will add it to the new GamesMessages property that is bound to the PopuDetails.
On lines 235-238, I added a filter that will delete the oldest message (GameMessages item 0) if there are over 250 items already in the GameMessages collection. This will just keep the UI a little more manageable.
In lines 388-392, implement the Dispose function for IDisposable. This function will dispose of the _currentBattle object and unsubscribe from the mess broker’s OnMessageRaised. This will prevent duplicated messages.
GameSession.cs
using System;
using System.Collections.ObjectModel;
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, IDisposable
{
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 ObservableCollection<string> GameMessages { get; } =
new ObservableCollection<string>();
public PopupDetails PlayerDetails { get; set; }
public PopupDetails InventoryDetails { get; set; }
public PopupDetails QuestDetails { get; set; }
public PopupDetails RecipesDetails { get; set; }
public PopupDetails GameMessagesDetails { 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
PlayerDetails = new PopupDetails
{
IsVisible = false,
Top = 10,
Left = 10,
MinHeight = 75,
MaxHeight = 400,
MinWidth = 265,
MaxWidth = 400
};
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
};
GameMessagesDetails = new PopupDetails
{
IsVisible = false,
Top = 250,
Left = 10,
MinHeight = 75,
MaxHeight = 175,
MinWidth = 350,
MaxWidth = 400
};
_messageBroker.OnMessageRaised += OnGameMessageRaised;
}
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 OnGameMessageRaised(object sender, GameMessageEventArgs e)
{
if (GameMessages.Count > 250)
{
GameMessages.RemoveAt(0);
}
GameMessages.Add(e.Message);
}
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}!");
}
public void Dispose()
{
_currentBattle?.Dispose();
_messageBroker.OnMessageRaised -= OnGameMessageRaised;
}
}
}
Step 2: Modify WPUI\MainWindow.xaml.cs
Start by adding a “using” directive for System.Collections.Specialized and delete the directives for System.Windows.Documents and SOSCSRPG.Core.
On line 21, delete the declaration of the class-level _messageBroker variable. This is handled in the GameSession class now.
On lines 78-82, delete the OnMessageRaised function that scrolled to the end of the messages. We’ll handle this using a different method.
On line 105, add a user input action so pressing the “M” key opens and closes the chat messages popup.
Inside the SetActiveGameSessionTo function (starting on line 119), remove the code that worked with the _messageBroker variable and GameMessages. Replace it with the new code shown for the function.
This new code handles the notifications from the GameSesssion object’s GamesMessages ObservableCollection property. If there’s an existing _gameSession object (a game has already been started and the player is starting/loading a new game), this function unsubscribes from the CollectionChanged event (lines 121-125).
Then, on lines 130-131, MainWindow subscribes to the CollectionChanged event on the new _gameSession object.
On lines 134-141, add the new GameMessages_CollectionChanged function. This is the function that will run whenever an item is added to, or removed from, the GameMessages property – due to subscribing to the GameMessages.CollectionChanged event on lines 130-131. This function scrolls to the bottom of the new FlowDocumentScrollViewer whenever a message is added.
Inside the StartNewGame_OnClick function (line 145), we’ll call the Dispose function on the existing _gameSession object – if there is one (the question mark prevents an error from happening if _gameSession is null).
Finally, on lines 217-220, add the function to hide the game messages popup when the player clicks the “X” button on the popup canvas.
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using SOSCSRPG.Models;
using SOSCSRPG.Services;
using SOSCSRPG.ViewModels;
using Microsoft.Win32;
using WPFUI.Windows;
namespace WPFUI
{
public partial class MainWindow : Window
{
private const string SAVE_GAME_FILE_EXTENSION = "soscsrpg";
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 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.P, () => _gameSession.PlayerDetails.IsVisible = !_gameSession.PlayerDetails.IsVisible);
_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.M, () => _gameSession.GameMessagesDetails.IsVisible = !_gameSession.GameMessagesDetails.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)
{
if (_gameSession != null)
{
_gameSession.GameMessages.CollectionChanged -=
GameMessages_CollectionChanged;
}
_gameSession = gameSession;
DataContext = _gameSession;
_gameSession.GameMessages.CollectionChanged +=
GameMessages_CollectionChanged;
}
private void GameMessages_CollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
(GameMessagesFlowDocumentScrollViewer
.Template
.FindName("PART_ContentHost", GameMessagesFlowDocumentScrollViewer) as ScrollViewer)
?.ScrollToEnd();
}
private void StartNewGame_OnClick(object sender, RoutedEventArgs e)
{
_gameSession?.Dispose();
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 ClosePlayerDetailsWindow_OnClick(object sender, RoutedEventArgs e)
{
_gameSession.PlayerDetails.IsVisible = false;
}
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 CloseGameMessagesDetailsWindow_OnClick(object sender, RoutedEventArgs e)
{
_gameSession.GameMessagesDetails.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 3: Modify WPUI\MainWindow.xaml
Add the new GameMessages Canvas XAML that’s on lines 434-493.
The Canvas part of the code is like the other popup canvases, but I switched from using a RichTextBox to a FlowDocumentScrollViewer to display the messages. This way, we can bind to the GameMessages ObservableCollection property.
If you don’t like the font for the game messages, you can change the FontFamily value on line 483 from “Calibri” to whatever font you prefer for your version of the game.
Finally, remove the original game messages section that was on lines 451-467.
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 Details -->
<Canvas Top="{Binding PlayerDetails.Top}" Left="{Binding PlayerDetails.Left}"
Width="Auto" Height="Auto"
MinHeight="{Binding PlayerDetails.MinHeight}"
MaxHeight="{Binding PlayerDetails.MaxHeight}"
MinWidth="{Binding PlayerDetails.MinWidth}"
MaxWidth="{Binding PlayerDetails.MaxWidth}"
Visibility="{Binding PlayerDetails.IsVisible, Converter={StaticResource BooleanToVisibility}}">
<Border BorderBrush="Navy" BorderThickness="3"
Background="LightSteelBlue">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="4"/>
<RowDefinition Height="60"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Left"
FontWeight="Bold"
Content="Player Details"/>
<Button Grid.Row="0" Grid.Column="1"
HorizontalAlignment="Right"
Width="25"
FontWeight="Bold"
Content="X"
Click="ClosePlayerDetailsWindow_OnClick">
<Button.Resources>
<Style TargetType="Border">
<Setter Property="CornerRadius" Value="3"/>
</Style>
</Button.Resources>
</Button>
<!-- Sets the background color for the two player data rows -->
<Border Grid.Row="2" Grid.Column="0"
Grid.ColumnSpan="2"
Grid.RowSpan="2"
Background="WhiteSmoke">
</Border>
<!-- Player level and name -->
<Canvas Grid.Row="2" Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Left"
MaxHeight="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Canvas}},Path=MaxHeight}"
Width="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Canvas}},Path=ActualWidth}">
<Ellipse Canvas.Top="3" Canvas.Left="3"
Width="50"
Height="50"
StrokeThickness="1"
Stroke="SteelBlue"/>
<Ellipse Canvas.Top="5" Canvas.Left="5"
Width="46"
Height="46"
StrokeThickness="1"
Stroke="SteelBlue"/>
<Label Canvas.Top="5" Canvas.Left="5"
Width="46"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
FontSize="18pt"
FontWeight="Bold"
Content="{Binding CurrentPlayer.Level}"/>
<Label Canvas.Top="5" Canvas.Left="55"
Width="200"
FontSize="18pt"
FontWeight="Bold"
Content="{Binding CurrentPlayer.Name}"/>
</Canvas>
<Grid Grid.Row="3" Grid.Column="0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="5,5,5,5">
<Grid.RowDefinitions>
<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"
FontWeight="Bold"
Content="Exp:"/>
<Label Grid.Row="0" Grid.Column="1"
Content="{Binding CurrentPlayer.ExperiencePoints}"/>
<Label Grid.Row="1" Grid.Column="0"
FontWeight="Bold"
Content="Gold:"/>
<Label Grid.Row="1" Grid.Column="1"
Content="{Binding CurrentPlayer.Gold}"/>
<Label Grid.Row="2" Grid.Column="0"
FontWeight="Bold"
Content="HP:"/>
<Label Grid.Row="2" Grid.Column="1"
Content="{Binding CurrentPlayer.HitPoints}"/>
</Grid>
<!-- Player Attributes -->
<ListBox Grid.Row="3" Grid.Column="1"
Margin="5,5,5,5"
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>
</Border>
</Canvas>
<!-- 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>
<!-- Game Messages -->
<Canvas Top="{Binding GameMessagesDetails.Top}" Left="{Binding GameMessagesDetails.Left}"
Width="Auto" Height="Auto"
MinHeight="{Binding GameMessagesDetails.MinHeight}"
MaxHeight="{Binding GameMessagesDetails.MaxHeight}"
MinWidth="{Binding GameMessagesDetails.MinWidth}"
MaxWidth="{Binding GameMessagesDetails.MaxWidth}"
Visibility="{Binding GameMessagesDetails.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="Game Messages"/>
<Button Grid.Row="0" Grid.Column="1"
Width="25"
FontWeight="Bold"
Content="X"
Click="CloseGameMessagesDetailsWindow_OnClick">
<Button.Resources>
<Style TargetType="Border">
<Setter Property="CornerRadius" Value="3"/>
</Style>
</Button.Resources>
</Button>
<FlowDocumentScrollViewer
Grid.Row="2" Grid.Column="0"
Grid.ColumnSpan="2"
MaxHeight="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Canvas}},Path=MaxHeight}"
Width="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Canvas}},Path=ActualWidth}"
x:Name="GameMessagesFlowDocumentScrollViewer" >
<FlowDocument Background="WhiteSmoke">
<Paragraph FontFamily="Calibri">
<ItemsControl ItemsSource="{Binding GameMessages, Mode=OneWay}" />
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>
</Grid>
</Border>
</Canvas>
</Canvas>
<!-- Gameplay -->
<Grid Grid.Row="1" Grid.Column="0"
Grid.ColumnSpan="2"
Background="Beige"
ZIndex="1">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<!-- 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="Right"
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 4: Test the game
NEXT LESSON: TO BE WRITTEN
PREVIOUS LESSON: Lesson 20.3: Create floating player details canvas
I’m sad that it’s over, but proud of myself for doing the whole thing!
I’m happy to hear you enjoyed it. You can always come up with your own ideas on how to expand it – or write something even better. Good luck with your future programming!