Now we’ll create a floating canvas for the player’s details.
I made this look a little nicer, using some WPF shapes. There are probably better ways to do this, but we will start with this. One thing I really want to do is move these Canvas controls out of MainWindow.xaml, and into their own controls.
Step 1: Modify SOSCSRPG.Models\LivingEntity.cs
On lines 66-67, I added a new HitPoints property that shows the player’s CurrentHitPoints and MaximumHitPoints.
I added the JsonIgnore attribute so this property is not written to the save game file. It’s derived from other properties that already are in the save game file.
LivingEntity.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using Newtonsoft.Json;
namespace SOSCSRPG.Models
{
public abstract class LivingEntity : INotifyPropertyChanged
{
#region Properties
private GameItem _currentWeapon;
private GameItem _currentConsumable;
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<PlayerAttribute> Attributes { get; } =
new ObservableCollection<PlayerAttribute>();
public string Name { get; }
public int CurrentHitPoints { get; private set; }
public int MaximumHitPoints { get; protected set; }
public int Gold { get; private set; }
public int Level { get; protected set; }
public Inventory Inventory { get; private set; }
public GameItem CurrentWeapon
{
get => _currentWeapon;
set
{
if (_currentWeapon != null)
{
_currentWeapon.Action.OnActionPerformed -= RaiseActionPerformedEvent;
}
_currentWeapon = value;
if (_currentWeapon != null)
{
_currentWeapon.Action.OnActionPerformed += RaiseActionPerformedEvent;
}
}
}
public GameItem CurrentConsumable
{
get => _currentConsumable;
set
{
if(_currentConsumable != null)
{
_currentConsumable.Action.OnActionPerformed -= RaiseActionPerformedEvent;
}
_currentConsumable = value;
if (_currentConsumable != null)
{
_currentConsumable.Action.OnActionPerformed += RaiseActionPerformedEvent;
}
}
}
[JsonIgnore]
public string HitPoints => $"{CurrentHitPoints}/{MaximumHitPoints}";
[JsonIgnore]
public bool IsAlive => CurrentHitPoints > 0;
[JsonIgnore]
public bool IsDead => !IsAlive;
#endregion
public event EventHandler<string> OnActionPerformed;
public event EventHandler OnKilled;
protected LivingEntity(string name, int maximumHitPoints, int currentHitPoints,
IEnumerable<PlayerAttribute> attributes, int gold, int level = 1)
{
Name = name;
MaximumHitPoints = maximumHitPoints;
CurrentHitPoints = currentHitPoints;
Gold = gold;
Level = level;
foreach (PlayerAttribute attribute in attributes)
{
Attributes.Add(attribute);
}
Inventory = new Inventory();
}
public void UseCurrentWeaponOn(LivingEntity target)
{
CurrentWeapon.PerformAction(this, target);
}
public void UseCurrentConsumable()
{
CurrentConsumable.PerformAction(this, this);
RemoveItemFromInventory(CurrentConsumable);
}
public void TakeDamage(int hitPointsOfDamage)
{
CurrentHitPoints -= hitPointsOfDamage;
if(IsDead)
{
CurrentHitPoints = 0;
RaiseOnKilledEvent();
}
}
public void Heal(int hitPointsToHeal)
{
CurrentHitPoints += hitPointsToHeal;
if(CurrentHitPoints > MaximumHitPoints)
{
CurrentHitPoints = MaximumHitPoints;
}
}
public void CompletelyHeal()
{
CurrentHitPoints = MaximumHitPoints;
}
public void ReceiveGold(int amountOfGold)
{
Gold += amountOfGold;
}
public void SpendGold(int amountOfGold)
{
if(amountOfGold > Gold)
{
throw new ArgumentOutOfRangeException($"{Name} only has {Gold} gold, and cannot spend {amountOfGold} gold");
}
Gold -= amountOfGold;
}
public void AddItemToInventory(GameItem item)
{
Inventory = Inventory.AddItem(item);
}
public void RemoveItemFromInventory(GameItem item)
{
Inventory = Inventory.RemoveItem(item);
}
public void RemoveItemsFromInventory(IEnumerable<ItemQuantity> itemQuantities)
{
Inventory = Inventory.RemoveItems(itemQuantities);
}
#region Private functions
private void RaiseOnKilledEvent()
{
OnKilled?.Invoke(this, new System.EventArgs());
}
private void RaiseActionPerformedEvent(object sender, string result)
{
OnActionPerformed?.Invoke(this, result);
}
#endregion
}
}
Step 2: Modify SOSCSRPG.ViewModels\GameSession.cs
On line 93, I added the PlayerDetails property to hold the values for the popup canvas.
On lines 131-140, populate the PlayerDetails property with the values for the starting position and visibility of the Canvas.
GameSession.cs
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 PlayerDetails { get; 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
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
};
}
public void MoveNorth()
{
if(HasLocationToNorth)
{
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
}
}
public void MoveEast()
{
if(HasLocationToEast)
{
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
}
}
public void MoveSouth()
{
if(HasLocationToSouth)
{
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
}
}
public void MoveWest()
{
if(HasLocationToWest)
{
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
}
}
private void PopulateGameDetails()
{
GameDetails = GameDetailsService.ReadGameDetails();
}
private void CompleteQuestsAtLocation()
{
foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
{
QuestStatus questToComplete =
CurrentPlayer.Quests.FirstOrDefault(q => q.PlayerQuest.ID == quest.ID &&
!q.IsCompleted);
if(questToComplete != null)
{
if(CurrentPlayer.Inventory.HasAllTheseItems(quest.ItemsToComplete))
{
CurrentPlayer.RemoveItemsFromInventory(quest.ItemsToComplete);
_messageBroker.RaiseMessage("");
_messageBroker.RaiseMessage($"You completed the '{quest.Name}' quest");
// Give the player the quest rewards
_messageBroker.RaiseMessage($"You receive {quest.RewardExperiencePoints} experience points");
CurrentPlayer.AddExperience(quest.RewardExperiencePoints);
_messageBroker.RaiseMessage($"You receive {quest.RewardGold} gold");
CurrentPlayer.ReceiveGold(quest.RewardGold);
foreach(ItemQuantity itemQuantity in quest.RewardItems)
{
GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);
_messageBroker.RaiseMessage($"You receive a {rewardItem.Name}");
CurrentPlayer.AddItemToInventory(rewardItem);
}
// Mark the Quest as completed
questToComplete.IsCompleted = true;
}
}
}
}
private void GivePlayerQuestsAtLocation()
{
foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
{
if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))
{
CurrentPlayer.Quests.Add(new QuestStatus(quest));
_messageBroker.RaiseMessage("");
_messageBroker.RaiseMessage($"You receive the '{quest.Name}' quest");
_messageBroker.RaiseMessage(quest.Description);
_messageBroker.RaiseMessage("Return with:");
foreach(ItemQuantity itemQuantity in quest.ItemsToComplete)
{
_messageBroker
.RaiseMessage($" {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
}
_messageBroker.RaiseMessage("And you will receive:");
_messageBroker.RaiseMessage($" {quest.RewardExperiencePoints} experience points");
_messageBroker.RaiseMessage($" {quest.RewardGold} gold");
foreach(ItemQuantity itemQuantity in quest.RewardItems)
{
_messageBroker
.RaiseMessage($" {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
}
}
}
}
public void AttackCurrentMonster()
{
_currentBattle?.AttackOpponent();
}
public void UseCurrentConsumable()
{
if(CurrentPlayer.CurrentConsumable != null)
{
if (_currentBattle == null)
{
CurrentPlayer.OnActionPerformed += OnConsumableActionPerformed;
}
CurrentPlayer.UseCurrentConsumable();
if (_currentBattle == null)
{
CurrentPlayer.OnActionPerformed -= OnConsumableActionPerformed;
}
}
}
private void OnConsumableActionPerformed(object sender, string result)
{
_messageBroker.RaiseMessage(result);
}
public void CraftItemUsing(Recipe recipe)
{
if(CurrentPlayer.Inventory.HasAllTheseItems(recipe.Ingredients))
{
CurrentPlayer.RemoveItemsFromInventory(recipe.Ingredients);
foreach(ItemQuantity itemQuantity in recipe.OutputItems)
{
for(int i = 0; i < itemQuantity.Quantity; i++)
{
GameItem outputItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);
CurrentPlayer.AddItemToInventory(outputItem);
_messageBroker.RaiseMessage($"You craft 1 {outputItem.Name}");
}
}
}
else
{
_messageBroker.RaiseMessage("You do not have the required ingredients:");
foreach(ItemQuantity itemQuantity in recipe.Ingredients)
{
_messageBroker
.RaiseMessage($" {itemQuantity.QuantityItemDescription}");
}
}
}
private void OnPlayerKilled(object sender, System.EventArgs e)
{
_messageBroker.RaiseMessage("");
_messageBroker.RaiseMessage("You have been killed.");
CurrentLocation = CurrentWorld.LocationAt(0, -1);
CurrentPlayer.CompletelyHeal();
}
private void OnCurrentMonsterKilled(object sender, System.EventArgs eventArgs)
{
// Get another monster to fight
CurrentMonster = MonsterFactory.GetMonsterFromLocation(CurrentLocation);
}
private void OnCurrentPlayerLeveledUp(object sender, System.EventArgs eventArgs)
{
_messageBroker.RaiseMessage($"You are now level {CurrentPlayer.Level}!");
}
}
}
Step 3: Modify WPFUI\MainWindow.xaml.cs
On line 109, add the code so pressing the “P” key will change the player details canvas visibility.
On lines 192-195, add the event handler to hide the player details canvas when the user clicks the “X” button in the 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.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.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 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 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: Modify WPFUI\MainWindow.xaml
On lines 60-210, add the Canvas control to display the player’s information.
In 60-67, we bind the Canvas to the PlayerDetails property, for Visibility and location values.
This is a lot of XAML, and I’d like to eventually move it to a separate control, to make it easier to work with (modify) in the future.
Most of this is like the other popup Canvas controls, but I added a little graphics to make the game look nicer.
On lines 103-107, I added a Border with a Background, to make sure the third row (Row=”2″) would have a background.
On lines 109-144, I added a new Canvas to handle some graphics. The two Ellipses controls (lines 118-128) draw circles we’ll use to highlight the player’s level.
On lines 130-136, we display the player’s Level in a large font. And, on lines 138-142, we display the Player’s Name in a big font.
Lines 146-177 display Labels with the player’s experience, gold, and hit points information.
Lines 179-204 are the ListBox control that displays the player’s attributes.
Delete the “Player stats” Grid that was on line 285-342. All this information is in the new popup Canvas now.
Change the Grid for the Gameplay Grid (was on line 345 before, is on line 437 now) so the Grid.Column starts in 0, and the Grid has a ColumnSpan of 2.
On line 558, I changed the HorizontalAlignment of the Combat Controls to “Right”, from “Center”.
We’ll eventually clean up (and probably remove) the existing combat controls, but this made it a little easier to arrange the new popup panels on the game screen, and not overlap the combat controls.
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>
</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>
<!-- 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="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 5: Test the game
NEXT LESSON: Lesson 20.4: Create floating game messages canvas
PREVIOUS LESSON: Lesson 20.2: Create floating quest and recipe canvases