Press "Enter" to skip to content

Lesson 26.2 – Hiding Unvisited Locations on the World Map

Now that we have a map, it would be nice to only show the images for the locations the player has visited. That is what we’ll add in this lesson.

 

NOTE: In the last lesson, there wasn’t an image for the Bridge location. That’s because I used the images from the WPF version of these lessons, and there is no Bridge location in that game.

So, if you finished Lesson 26.1, and you don’t have a Bridge image (and you don’t have six columns of PictureBox controls), please go back to Lesson 26.1 and make the changes to add the missing column. This will require downloading the location images again (to get the Bridge image), and changing the WorldMap form (to add the new column and display the Bridge image).

 

STEP 1: Add FogLocation.png to SuperAdventure\Images

After adding it, set its properties to:

Build Action: Embedded Resource

Copy to Output Directory: Do not copy

 

Right-click and select “Save as”, to download

 

 

Step 2: Edit Engine\Player.cs

We’re going to store the ID of every location the player visits. We’ll save the IDs in a new List property named LocationsVisited (line 69).

Because this is a List property, we need to initialize it, otherwise it will be null, instead of an empty List. We’ll do that in the constructor (on line 80), where we initialize the other list properties.

Now, when the player moves to a new location, we need to add its ID to the property – if it hasn’t already been added. We do that inside the MoveTo function, on lines 167 to 170. If the LocationsVisited property does not already contain the ID of the location, we add it to the List.

 

Player.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Xml;
namespace Engine
{
    public class Player : LivingCreature
    {
        private int _gold;
        private int _experiencePoints;
        private Location _currentLocation;
        public event EventHandler<MessageEventArgs> OnMessage;
        public int Gold
        {
            get { return _gold; }
            set
            {
                _gold = value;
                OnPropertyChanged("Gold");
            }
        }
        public int ExperiencePoints
        {
            get { return _experiencePoints; }
            private set
            {
                _experiencePoints = value;
                OnPropertyChanged("ExperiencePoints");
                OnPropertyChanged("Level");
            }
        }
        public int Level
        {
            get { return ((ExperiencePoints / 100) + 1); }
        }
        public Location CurrentLocation
        {
            get { return _currentLocation; }
            set
            {
                _currentLocation = value;
                OnPropertyChanged("CurrentLocation");
            }
        }
        public Weapon CurrentWeapon { get; set; }
        public BindingList<InventoryItem> Inventory { get; set; }
        public List<Weapon> Weapons
        {
            get { return Inventory.Where(x => x.Details is Weapon).Select(x => x.Details as Weapon).ToList(); }
        }
        public List<HealingPotion> Potions
        {
            get { return Inventory.Where(x => x.Details is HealingPotion).Select(x => x.Details as HealingPotion).ToList(); }
        }
        public BindingList<PlayerQuest> Quests { get; set; }
        public List<int> LocationsVisited { get; set; }
        private Monster CurrentMonster { get; set; }
        private Player(int currentHitPoints, int maximumHitPoints, int gold, int experiencePoints) : base(currentHitPoints, maximumHitPoints)
        {
            Gold = gold;
            ExperiencePoints = experiencePoints;
            Inventory = new BindingList<InventoryItem>();
            Quests = new BindingList<PlayerQuest>();
            LocationsVisited = new List<int>();
        }
        public static Player CreateDefaultPlayer()
        {
            Player player = new Player(10, 10, 20, 0);
            player.Inventory.Add(new InventoryItem(World.ItemByID(World.ITEM_ID_RUSTY_SWORD), 1));
            player.CurrentLocation = World.LocationByID(World.LOCATION_ID_HOME);
            return player;
        }
        public static Player CreatePlayerFromXmlString(string xmlPlayerData)
        {
            try
            {
                XmlDocument playerData = new XmlDocument();
                playerData.LoadXml(xmlPlayerData);
                int currentHitPoints = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/CurrentHitPoints").InnerText);
                int maximumHitPoints = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/MaximumHitPoints").InnerText);
                int gold = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/Gold").InnerText);
                int experiencePoints = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/ExperiencePoints").InnerText);
                Player player = new Player(currentHitPoints, maximumHitPoints, gold, experiencePoints);
                int currentLocationID = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/CurrentLocation").InnerText);
                player.CurrentLocation = World.LocationByID(currentLocationID);
                if (playerData.SelectSingleNode("/Player/Stats/CurrentWeapon") != null)
                {
                    int currentWeaponID = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/CurrentWeapon").InnerText);
                    player.CurrentWeapon = (Weapon)World.ItemByID(currentWeaponID);
                }
                foreach (XmlNode node in playerData.SelectNodes("/Player/InventoryItems/InventoryItem"))
                {
                    int id = Convert.ToInt32(node.Attributes["ID"].Value);
                    int quantity = Convert.ToInt32(node.Attributes["Quantity"].Value);
                    for (int i = 0; i < quantity; i++)
                    {
                        player.AddItemToInventory(World.ItemByID(id));
                    }
                }
                foreach (XmlNode node in playerData.SelectNodes("/Player/PlayerQuests/PlayerQuest"))
                {
                    int id = Convert.ToInt32(node.Attributes["ID"].Value);
                    bool isCompleted = Convert.ToBoolean(node.Attributes["IsCompleted"].Value);
                    PlayerQuest playerQuest = new PlayerQuest(World.QuestByID(id));
                    playerQuest.IsCompleted = isCompleted;
                    player.Quests.Add(playerQuest);
                }
                return player;
            }
            catch
            {
                // If there was an error with the XML data, return a default player object
                return CreateDefaultPlayer();
            }
        }
        public static Player CreatePlayerFromDatabase(int currentHitPoints, int maximumHitPoints, int gold, int experiencePoints, int currentLocationID)
        {
            Player player = new Player(currentHitPoints, maximumHitPoints, gold, experiencePoints);
            player.MoveTo(World.LocationByID(currentLocationID));
            return player;
        }
        public void MoveTo(Location location)
        {
            if (PlayerDoesNotHaveTheRequiredItemToEnter(location))
            {
                RaiseMessage("You must have a " + location.ItemRequiredToEnter.Name + " to enter this location.");
                return;
            }
            // The player can enter this location
            CurrentLocation = location;
            if (!LocationsVisited.Contains(CurrentLocation.ID))
            {
                LocationsVisited.Add(CurrentLocation.ID);
            }
            CompletelyHeal();
            if (location.HasAQuest)
            {
                if (PlayerDoesNotHaveThisQuest(location.QuestAvailableHere))
                {
                    GiveQuestToPlayer(location.QuestAvailableHere);
                }
                else
                {
                    if (PlayerHasNotCompleted(location.QuestAvailableHere) &&
                        PlayerHasAllQuestCompletionItemsFor(location.QuestAvailableHere))
                    {
                        GivePlayerQuestRewards(location.QuestAvailableHere);
                    }
                }
            }
            SetTheCurrentMonsterForTheCurrentLocation(location);
        }
        public void MoveNorth()
        {
            if (CurrentLocation.LocationToNorth != null)
            {
                MoveTo(CurrentLocation.LocationToNorth);
            }
        }
        public void MoveEast()
        {
            if (CurrentLocation.LocationToEast != null)
            {
                MoveTo(CurrentLocation.LocationToEast);
            }
        }
        public void MoveSouth()
        {
            if (CurrentLocation.LocationToSouth != null)
            {
                MoveTo(CurrentLocation.LocationToSouth);
            }
        }
        public void MoveWest()
        {
            if (CurrentLocation.LocationToWest != null)
            {
                MoveTo(CurrentLocation.LocationToWest);
            }
        }
        public void UseWeapon(Weapon weapon)
        {
            int damage = RandomNumberGenerator.NumberBetween(weapon.MinimumDamage, weapon.MaximumDamage);
            if (damage == 0)
            {
                RaiseMessage("You missed the " + CurrentMonster.Name);
            }
            else
            {
                CurrentMonster.CurrentHitPoints -= damage;
                RaiseMessage("You hit the " + CurrentMonster.Name + " for " + damage + " points.");
            }
            if (CurrentMonster.IsDead)
            {
                LootTheCurrentMonster();
                // "Move" to the current location, to refresh the current monster
                MoveTo(CurrentLocation);
            }
            else
            {
                LetTheMonsterAttack();
            }
        }
        private void LootTheCurrentMonster()
        {
            RaiseMessage("");
            RaiseMessage("You defeated the " + CurrentMonster.Name);
            RaiseMessage("You receive " + CurrentMonster.RewardExperiencePoints + " experience points");
            RaiseMessage("You receive " + CurrentMonster.RewardGold + " gold");
            AddExperiencePoints(CurrentMonster.RewardExperiencePoints);
            Gold += CurrentMonster.RewardGold;
            // Give monster's loot items to the player
            foreach (InventoryItem inventoryItem in CurrentMonster.LootItems)
            {
                AddItemToInventory(inventoryItem.Details);
                RaiseMessage(string.Format("You loot {0} {1}", inventoryItem.Quantity, inventoryItem.Description));
            }
            RaiseMessage("");
        }
        public void UsePotion(HealingPotion potion)
        {
            RaiseMessage("You drink a " + potion.Name);
            HealPlayer(potion.AmountToHeal);
            RemoveItemFromInventory(potion);
            // The player used their turn to drink the potion, so let the monster attack now
            LetTheMonsterAttack();
        }
        public void AddItemToInventory(Item itemToAdd, int quantity = 1)
        {
            InventoryItem existingItemInInventory = Inventory.SingleOrDefault(ii => ii.Details.ID == itemToAdd.ID);
            if (existingItemInInventory == null)
            {
                Inventory.Add(new InventoryItem(itemToAdd, quantity));
            }
            else
            {
                existingItemInInventory.Quantity += quantity;
            }
            RaiseInventoryChangedEvent(itemToAdd);
        }
        public void RemoveItemFromInventory(Item itemToRemove, int quantity = 1)
        {
            InventoryItem item = Inventory.SingleOrDefault(ii => ii.Details.ID == itemToRemove.ID && ii.Quantity >= quantity);
            if (item != null)
            {
                item.Quantity -= quantity;
                if (item.Quantity == 0)
                {
                    Inventory.Remove(item);
                }
                RaiseInventoryChangedEvent(itemToRemove);
            }
        }
        public string ToXmlString()
        {
            XmlDocument playerData = new XmlDocument();
            // Create the top-level XML node
            XmlNode player = playerData.CreateElement("Player");
            playerData.AppendChild(player);
            // Create the "Stats" child node to hold the other player statistics nodes
            XmlNode stats = playerData.CreateElement("Stats");
            player.AppendChild(stats);
            // Create the child nodes for the "Stats" node
            CreateNewChildXmlNode(playerData, stats, "CurrentHitPoints", CurrentHitPoints);
            CreateNewChildXmlNode(playerData, stats, "MaximumHitPoints", MaximumHitPoints);
            CreateNewChildXmlNode(playerData, stats, "Gold", Gold);
            CreateNewChildXmlNode(playerData, stats, "ExperiencePoints", ExperiencePoints);
            CreateNewChildXmlNode(playerData, stats, "CurrentLocation", CurrentLocation.ID);
            if (CurrentWeapon != null)
            {
                CreateNewChildXmlNode(playerData, stats, "CurrentWeapon", CurrentWeapon.ID);
            }
            // Create the "InventoryItems" child node to hold each InventoryItem node
            XmlNode inventoryItems = playerData.CreateElement("InventoryItems");
            player.AppendChild(inventoryItems);
            // Create an "InventoryItem" node for each item in the player's inventory
            foreach (InventoryItem item in Inventory)
            {
                XmlNode inventoryItem = playerData.CreateElement("InventoryItem");
                AddXmlAttributeToNode(playerData, inventoryItem, "ID", item.Details.ID);
                AddXmlAttributeToNode(playerData, inventoryItem, "Quantity", item.Quantity);
                inventoryItems.AppendChild(inventoryItem);
            }
            // Create the "PlayerQuests" child node to hold each PlayerQuest node
            XmlNode playerQuests = playerData.CreateElement("PlayerQuests");
            player.AppendChild(playerQuests);
            // Create a "PlayerQuest" node for each quest the player has acquired
            foreach (PlayerQuest quest in Quests)
            {
                XmlNode playerQuest = playerData.CreateElement("PlayerQuest");
                AddXmlAttributeToNode(playerData, playerQuest, "ID", quest.Details.ID);
                AddXmlAttributeToNode(playerData, playerQuest, "IsCompleted", quest.IsCompleted);
                playerQuests.AppendChild(playerQuest);
            }
            return playerData.InnerXml; // The XML document, as a string, so we can save the data to disk
        }
        private bool HasRequiredItemToEnterThisLocation(Location location)
        {
            if (location.DoesNotHaveAnItemRequiredToEnter)
            {
                return true;
            }
            // See if the player has the required item in their inventory
            return Inventory.Any(ii => ii.Details.ID == location.ItemRequiredToEnter.ID);
        }
        private void SetTheCurrentMonsterForTheCurrentLocation(Location location)
        {
            // Populate the current monster with this location's monster (or null, if there is no monster here)
            CurrentMonster = location.NewInstanceOfMonsterLivingHere();
            if (CurrentMonster != null)
            {
                RaiseMessage("You see a " + CurrentMonster.Name);
            }
        }
        private bool PlayerDoesNotHaveTheRequiredItemToEnter(Location location)
        {
            return !HasRequiredItemToEnterThisLocation(location);
        }
        private bool PlayerDoesNotHaveThisQuest(Quest quest)
        {
            return Quests.All(pq => pq.Details.ID != quest.ID);
        }
        private bool PlayerHasNotCompleted(Quest quest)
        {
            return Quests.Any(pq => pq.Details.ID == quest.ID && !pq.IsCompleted);
        }
        private void GiveQuestToPlayer(Quest quest)
        {
            RaiseMessage("You receive the " + quest.Name + " quest.");
            RaiseMessage(quest.Description);
            RaiseMessage("To complete it, return with:");
            foreach (QuestCompletionItem qci in quest.QuestCompletionItems)
            {
                RaiseMessage(string.Format("{0} {1}", qci.Quantity,
                                           qci.Quantity == 1 ? qci.Details.Name : qci.Details.NamePlural));
            }
            RaiseMessage("");
            Quests.Add(new PlayerQuest(quest));
        }
        private bool PlayerHasAllQuestCompletionItemsFor(Quest quest)
        {
            // See if the player has all the items needed to complete the quest here
            foreach (QuestCompletionItem qci in quest.QuestCompletionItems)
            {
                // Check each item in the player's inventory, to see if they have it, and enough of it
                if (!Inventory.Any(ii => ii.Details.ID == qci.Details.ID && ii.Quantity >= qci.Quantity))
                {
                    return false;
                }
            }
            // If we got here, then the player must have all the required items, and enough of them, to complete the quest.
            return true;
        }
        private void RemoveQuestCompletionItems(Quest quest)
        {
            foreach (QuestCompletionItem qci in quest.QuestCompletionItems)
            {
                InventoryItem item = Inventory.SingleOrDefault(ii => ii.Details.ID == qci.Details.ID);
                if (item != null)
                {
                    RemoveItemFromInventory(item.Details, qci.Quantity);
                }
            }
        }
        private void AddExperiencePoints(int experiencePointsToAdd)
        {
            ExperiencePoints += experiencePointsToAdd;
            MaximumHitPoints = (Level * 10);
        }
        private void GivePlayerQuestRewards(Quest quest)
        {
            RaiseMessage("");
            RaiseMessage("You complete the '" + quest.Name + "' quest.");
            RaiseMessage("You receive: ");
            RaiseMessage(quest.RewardExperiencePoints + " experience points");
            RaiseMessage(quest.RewardGold + " gold");
            RaiseMessage(quest.RewardItem.Name, true);
            AddExperiencePoints(quest.RewardExperiencePoints);
            Gold += quest.RewardGold;
            RemoveQuestCompletionItems(quest);
            AddItemToInventory(quest.RewardItem);
            MarkPlayerQuestCompleted(quest);
        }
        private void MarkPlayerQuestCompleted(Quest quest)
        {
            PlayerQuest playerQuest = Quests.SingleOrDefault(pq => pq.Details.ID == quest.ID);
            if (playerQuest != null)
            {
                playerQuest.IsCompleted = true;
            }
        }
        private void LetTheMonsterAttack()
        {
            int damageToPlayer = RandomNumberGenerator.NumberBetween(0, CurrentMonster.MaximumDamage);
            RaiseMessage("The " + CurrentMonster.Name + " did " + damageToPlayer + " points of damage.");
            CurrentHitPoints -= damageToPlayer;
            if (IsDead)
            {
                RaiseMessage("The " + CurrentMonster.Name + " killed you.");
                MoveHome();
            }
        }
        private void HealPlayer(int hitPointsToHeal)
        {
            CurrentHitPoints = Math.Min(CurrentHitPoints + hitPointsToHeal, MaximumHitPoints);
        }
        private void CompletelyHeal()
        {
            CurrentHitPoints = MaximumHitPoints;
        }
        private void MoveHome()
        {
            MoveTo(World.LocationByID(World.LOCATION_ID_HOME));
        }
        private void CreateNewChildXmlNode(XmlDocument document, XmlNode parentNode, string elementName, object value)
        {
            XmlNode node = document.CreateElement(elementName);
            node.AppendChild(document.CreateTextNode(value.ToString()));
            parentNode.AppendChild(node);
        }
        private void AddXmlAttributeToNode(XmlDocument document, XmlNode node, string attributeName, object value)
        {
            XmlAttribute attribute = document.CreateAttribute(attributeName);
            attribute.Value = value.ToString();
            node.Attributes.Append(attribute);
        }
        private void RaiseInventoryChangedEvent(Item item)
        {
            if (item is Weapon)
            {
                OnPropertyChanged("Weapons");
            }
            if (item is HealingPotion)
            {
                OnPropertyChanged("Potions");
            }
        }
        private void RaiseMessage(string message, bool addExtraNewLine = false)
        {
            if (OnMessage != null)
            {
                OnMessage(this, new MessageEventArgs(message, addExtraNewLine));
            }
        }
    }
}

 

Step 3: Edit SuperAdventure\SuperAdventure.cs and SuperAdventure\WorldMap.cs

In order to display the correct image for a location (the fog, or the location’s image), the WorldMap form needs the current player object, to know which locations the player has visited. So, we need to pass it from the SuperAdventure form, into the WorldMap form – like we do with the TradingScreen form.

In WorldMap.cs, we need to add a Player parameter to the constructor (line 13).

In SuperAdventure.cs, we pass the current player when we instantiate the WorldMap form (line 225).

 

Now, we can hide the unvisited locations by displaying the FogLocation in the PictureBox for any locations whose IDs are not in the player object’s LocationsVisited list.

I’ve done that by using the ternary operator inside the calls to SetImage (lines 17 through 25). If LocationVisited contains the location’s ID, we pass the name of the location’s image file. If the ID is not in LocationsVisited, we pass “FogLocation”.

 

WorldMap.cs

using System.Drawing;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using Engine;
namespace SuperAdventure
{
    public partial class WorldMap : Form
    {
        readonly Assembly _thisAssembly = Assembly.GetExecutingAssembly();
        public WorldMap(Player player)
        {
            InitializeComponent();
            SetImage(pic_0_2, player.LocationsVisited.Contains(5) ? "HerbalistsGarden" : "FogLocation");
            SetImage(pic_1_2, player.LocationsVisited.Contains(4) ? "HerbalistsHut" : "FogLocation");
            SetImage(pic_2_0, player.LocationsVisited.Contains(7) ? "FarmFields" : "FogLocation");
            SetImage(pic_2_1, player.LocationsVisited.Contains(6) ? "Farmhouse" : "FogLocation");
            SetImage(pic_2_2, player.LocationsVisited.Contains(2) ? "TownSquare" : "FogLocation");
            SetImage(pic_2_3, player.LocationsVisited.Contains(3) ? "TownGate" : "FogLocation");
            SetImage(pic_2_4, player.LocationsVisited.Contains(8) ? "Bridge" : "FogLocation");
            SetImage(pic_2_5, player.LocationsVisited.Contains(9) ? "SpiderForest" : "FogLocation");
            SetImage(pic_3_2, player.LocationsVisited.Contains(1) ? "Home" : "FogLocation");
        }
        private void SetImage(PictureBox pictureBox, string imageName)
        {
            using (Stream resourceStream = 
                _thisAssembly.GetManifestResourceStream(
                    _thisAssembly.GetName().Name + ".Images." + imageName + ".png"))
            {
                if (resourceStream != null)
                {
                    pictureBox.Image = new Bitmap(resourceStream);
                }
            }
        }
    }
}

 

SuperAdventure.cs

using System;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
using System.IO;
using Engine;
namespace SuperAdventure
{
    public partial class SuperAdventure : Form
    {
        private const string PLAYER_DATA_FILE_NAME = "PlayerData.xml";
        private Player _player;
        public SuperAdventure()
        {
            InitializeComponent();
            _player = PlayerDataMapper.CreateFromDatabase();
            if(_player == null)
            {
                if(File.Exists(PLAYER_DATA_FILE_NAME))
                {
                    _player = Player.CreatePlayerFromXmlString(File.ReadAllText(PLAYER_DATA_FILE_NAME));
                }
                else
                {
                    _player = Player.CreateDefaultPlayer();
                }
            }
            _player.AddItemToInventory(World.ItemByID(World.ITEM_ID_CLUB));
            lblHitPoints.DataBindings.Add("Text", _player, "CurrentHitPoints");
            lblGold.DataBindings.Add("Text", _player, "Gold");
            lblExperience.DataBindings.Add("Text", _player, "ExperiencePoints");
            lblLevel.DataBindings.Add("Text", _player, "Level");
            dgvInventory.RowHeadersVisible = false;
            dgvInventory.AutoGenerateColumns = false;
            dgvInventory.DataSource = _player.Inventory;
            dgvInventory.Columns.Add(new DataGridViewTextBoxColumn
            {
                HeaderText = "Name",
                Width = 197,
                DataPropertyName = "Description"
            });
            dgvInventory.Columns.Add(new DataGridViewTextBoxColumn
            {
                HeaderText = "Quantity",
                DataPropertyName = "Quantity"
            });
            dgvInventory.ScrollBars = ScrollBars.Vertical;
            dgvQuests.RowHeadersVisible = false;
            dgvQuests.AutoGenerateColumns = false;
            dgvQuests.DataSource = _player.Quests;
            dgvQuests.Columns.Add(new DataGridViewTextBoxColumn
            {
                HeaderText = "Name",
                Width = 197,
                DataPropertyName = "Name"
            });
            dgvQuests.Columns.Add(new DataGridViewTextBoxColumn
            {
                HeaderText = "Done?",
                DataPropertyName = "IsCompleted"
            });
            cboWeapons.DataSource = _player.Weapons;
            cboWeapons.DisplayMember = "Name";
            cboWeapons.ValueMember = "Id";
            if(_player.CurrentWeapon != null)
            {
                cboWeapons.SelectedItem = _player.CurrentWeapon;
            }
            cboWeapons.SelectedIndexChanged += cboWeapons_SelectedIndexChanged;
            cboPotions.DataSource = _player.Potions;
            cboPotions.DisplayMember = "Name";
            cboPotions.ValueMember = "Id";
            _player.PropertyChanged += PlayerOnPropertyChanged;
            _player.OnMessage += DisplayMessage;
            _player.MoveTo(_player.CurrentLocation);
        }
        private void DisplayMessage(object sender, MessageEventArgs messageEventArgs)
        {
            rtbMessages.Text += messageEventArgs.Message + Environment.NewLine;
            if(messageEventArgs.AddExtraNewLine)
            {
                rtbMessages.Text += Environment.NewLine;
            }
            rtbMessages.SelectionStart = rtbMessages.Text.Length;
            rtbMessages.ScrollToCaret();
        }
        private void PlayerOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
        {
            if(propertyChangedEventArgs.PropertyName == "Weapons")
            {
                cboWeapons.DataSource = _player.Weapons;
                if(!_player.Weapons.Any())
                {
                    cboWeapons.Visible = false;
                    btnUseWeapon.Visible = false;
                }
            }
            if(propertyChangedEventArgs.PropertyName == "Potions")
            {
                cboPotions.DataSource = _player.Potions;
                if(!_player.Potions.Any())
                {
                    cboPotions.Visible = false;
                    btnUsePotion.Visible = false;
                }
            }
            if(propertyChangedEventArgs.PropertyName == "CurrentLocation")
            {
                // Show/hide available movement buttons
                btnNorth.Visible = (_player.CurrentLocation.LocationToNorth != null);
                btnEast.Visible = (_player.CurrentLocation.LocationToEast != null);
                btnSouth.Visible = (_player.CurrentLocation.LocationToSouth != null);
                btnWest.Visible = (_player.CurrentLocation.LocationToWest != null);
                btnTrade.Visible = (_player.CurrentLocation.VendorWorkingHere != null);
                // Display current location name and description
                rtbLocation.Text = _player.CurrentLocation.Name + Environment.NewLine;
                rtbLocation.Text += _player.CurrentLocation.Description + Environment.NewLine;
                if(!_player.CurrentLocation.HasAMonster)
                {
                    cboWeapons.Visible = false;
                    cboPotions.Visible = false;
                    btnUseWeapon.Visible = false;
                    btnUsePotion.Visible = false;
                }
                else
                {
                    cboWeapons.Visible = _player.Weapons.Any();
                    cboPotions.Visible = _player.Potions.Any();
                    btnUseWeapon.Visible = _player.Weapons.Any();
                    btnUsePotion.Visible = _player.Potions.Any();
                }
            }
        }
        private void btnNorth_Click(object sender, EventArgs e)
        {
            _player.MoveNorth();
        }
        private void btnEast_Click(object sender, EventArgs e)
        {
            _player.MoveEast();
        }
        private void btnSouth_Click(object sender, EventArgs e)
        {
            _player.MoveSouth();
        }
        private void btnWest_Click(object sender, EventArgs e)
        {
            _player.MoveWest();
        }
        private void btnUseWeapon_Click(object sender, EventArgs e)
        {
            // Get the currently selected weapon from the cboWeapons ComboBox
            Weapon currentWeapon = (Weapon)cboWeapons.SelectedItem;
            _player.UseWeapon(currentWeapon);
        }
        private void btnUsePotion_Click(object sender, EventArgs e)
        {
            // Get the currently selected potion from the combobox
            HealingPotion potion = (HealingPotion)cboPotions.SelectedItem;
            _player.UsePotion(potion);
        }
        private void SuperAdventure_FormClosing(object sender, FormClosingEventArgs e)
        {
            File.WriteAllText(PLAYER_DATA_FILE_NAME, _player.ToXmlString());
            PlayerDataMapper.SaveToDatabase(_player);
        }
        private void cboWeapons_SelectedIndexChanged(object sender, EventArgs e)
        {
            _player.CurrentWeapon = (Weapon)cboWeapons.SelectedItem;
        }
        private void btnTrade_Click(object sender, EventArgs e)
        {
            TradingScreen tradingScreen = new TradingScreen(_player);
            tradingScreen.StartPosition = FormStartPosition.CenterParent;
            tradingScreen.ShowDialog(this);
        }
        private void btnMap_Click(object sender, EventArgs e)
        {
            WorldMap mapScreen = new WorldMap(_player);
            mapScreen.StartPosition = FormStartPosition.CenterParent;
            mapScreen.ShowDialog(this);
        }
    }
}

 

Step 4: Edit Engine\Player.cs

We want to remember the player’s LocationsVisited values between game sessions. So, we need to update the code that saves the player’s data to the saved game file – and the code that creates the player object from that file.

In the ToXmlString() function, we’ll add a new section that creates nodes with the ID values in LocationsVisited (lines 349 through 361). This is like the code to save the InventoryItems and PlayerQuests.

We create a LocationsVisited node, with a child node named LocationVisited, to hold the location ID.

In the CreatePlayerFromXmlString() function we add code to read those values from the saved game file (lines 116 through 121).

 

Player.cs (with changes to save/read LocationsVisited from saved game file)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Xml;
namespace Engine
{
    public class Player : LivingCreature
    {
        private int _gold;
        private int _experiencePoints;
        private Location _currentLocation;
        public event EventHandler<MessageEventArgs> OnMessage;
        public int Gold
        {
            get { return _gold; }
            set
            {
                _gold = value;
                OnPropertyChanged("Gold");
            }
        }
        public int ExperiencePoints
        {
            get { return _experiencePoints; }
            private set
            {
                _experiencePoints = value;
                OnPropertyChanged("ExperiencePoints");
                OnPropertyChanged("Level");
            }
        }
        public int Level
        {
            get { return ((ExperiencePoints / 100) + 1); }
        }
        public Location CurrentLocation
        {
            get { return _currentLocation; }
            set
            {
                _currentLocation = value;
                OnPropertyChanged("CurrentLocation");
            }
        }
        public Weapon CurrentWeapon { get; set; }
        public BindingList<InventoryItem> Inventory { get; set; }
        public List<Weapon> Weapons
        {
            get { return Inventory.Where(x => x.Details is Weapon).Select(x => x.Details as Weapon).ToList(); }
        }
        public List<HealingPotion> Potions
        {
            get { return Inventory.Where(x => x.Details is HealingPotion).Select(x => x.Details as HealingPotion).ToList(); }
        }
        public BindingList<PlayerQuest> Quests { get; set; }
        public List<int> LocationsVisited { get; set; }
        private Monster CurrentMonster { get; set; }
        private Player(int currentHitPoints, int maximumHitPoints, int gold, int experiencePoints) : base(currentHitPoints, maximumHitPoints)
        {
            Gold = gold;
            ExperiencePoints = experiencePoints;
            Inventory = new BindingList<InventoryItem>();
            Quests = new BindingList<PlayerQuest>();
            LocationsVisited = new List<int>();
        }
        public static Player CreateDefaultPlayer()
        {
            Player player = new Player(10, 10, 20, 0);
            player.Inventory.Add(new InventoryItem(World.ItemByID(World.ITEM_ID_RUSTY_SWORD), 1));
            player.CurrentLocation = World.LocationByID(World.LOCATION_ID_HOME);
            return player;
        }
        public static Player CreatePlayerFromXmlString(string xmlPlayerData)
        {
            try
            {
                XmlDocument playerData = new XmlDocument();
                playerData.LoadXml(xmlPlayerData);
                int currentHitPoints = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/CurrentHitPoints").InnerText);
                int maximumHitPoints = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/MaximumHitPoints").InnerText);
                int gold = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/Gold").InnerText);
                int experiencePoints = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/ExperiencePoints").InnerText);
                Player player = new Player(currentHitPoints, maximumHitPoints, gold, experiencePoints);
                int currentLocationID = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/CurrentLocation").InnerText);
                player.CurrentLocation = World.LocationByID(currentLocationID);
                if (playerData.SelectSingleNode("/Player/Stats/CurrentWeapon") != null)
                {
                    int currentWeaponID = Convert.ToInt32(playerData.SelectSingleNode("/Player/Stats/CurrentWeapon").InnerText);
                    player.CurrentWeapon = (Weapon)World.ItemByID(currentWeaponID);
                }
                foreach (XmlNode node in playerData.SelectNodes("/Player/LocationsVisited/LocationVisited"))
                {
                    int id = Convert.ToInt32(node.Attributes["ID"].Value);
                    player.LocationsVisited.Add(id);
                }
                foreach (XmlNode node in playerData.SelectNodes("/Player/InventoryItems/InventoryItem"))
                {
                    int id = Convert.ToInt32(node.Attributes["ID"].Value);
                    int quantity = Convert.ToInt32(node.Attributes["Quantity"].Value);
                    for (int i = 0; i < quantity; i++)
                    {
                        player.AddItemToInventory(World.ItemByID(id));
                    }
                }
                foreach (XmlNode node in playerData.SelectNodes("/Player/PlayerQuests/PlayerQuest"))
                {
                    int id = Convert.ToInt32(node.Attributes["ID"].Value);
                    bool isCompleted = Convert.ToBoolean(node.Attributes["IsCompleted"].Value);
                    PlayerQuest playerQuest = new PlayerQuest(World.QuestByID(id));
                    playerQuest.IsCompleted = isCompleted;
                    player.Quests.Add(playerQuest);
                }
                return player;
            }
            catch
            {
                // If there was an error with the XML data, return a default player object
                return CreateDefaultPlayer();
            }
        }
        public static Player CreatePlayerFromDatabase(int currentHitPoints, int maximumHitPoints, int gold, int experiencePoints, int currentLocationID)
        {
            Player player = new Player(currentHitPoints, maximumHitPoints, gold, experiencePoints);
            player.MoveTo(World.LocationByID(currentLocationID));
            return player;
        }
        public void MoveTo(Location location)
        {
            if (PlayerDoesNotHaveTheRequiredItemToEnter(location))
            {
                RaiseMessage("You must have a " + location.ItemRequiredToEnter.Name + " to enter this location.");
                return;
            }
            // The player can enter this location
            CurrentLocation = location;
            if (!LocationsVisited.Contains(CurrentLocation.ID))
            {
                LocationsVisited.Add(CurrentLocation.ID);
            }
            CompletelyHeal();
            if (location.HasAQuest)
            {
                if (PlayerDoesNotHaveThisQuest(location.QuestAvailableHere))
                {
                    GiveQuestToPlayer(location.QuestAvailableHere);
                }
                else
                {
                    if (PlayerHasNotCompleted(location.QuestAvailableHere) &&
                        PlayerHasAllQuestCompletionItemsFor(location.QuestAvailableHere))
                    {
                        GivePlayerQuestRewards(location.QuestAvailableHere);
                    }
                }
            }
            SetTheCurrentMonsterForTheCurrentLocation(location);
        }
        public void MoveNorth()
        {
            if (CurrentLocation.LocationToNorth != null)
            {
                MoveTo(CurrentLocation.LocationToNorth);
            }
        }
        public void MoveEast()
        {
            if (CurrentLocation.LocationToEast != null)
            {
                MoveTo(CurrentLocation.LocationToEast);
            }
        }
        public void MoveSouth()
        {
            if (CurrentLocation.LocationToSouth != null)
            {
                MoveTo(CurrentLocation.LocationToSouth);
            }
        }
        public void MoveWest()
        {
            if (CurrentLocation.LocationToWest != null)
            {
                MoveTo(CurrentLocation.LocationToWest);
            }
        }
        public void UseWeapon(Weapon weapon)
        {
            int damage = RandomNumberGenerator.NumberBetween(weapon.MinimumDamage, weapon.MaximumDamage);
            if (damage == 0)
            {
                RaiseMessage("You missed the " + CurrentMonster.Name);
            }
            else
            {
                CurrentMonster.CurrentHitPoints -= damage;
                RaiseMessage("You hit the " + CurrentMonster.Name + " for " + damage + " points.");
            }
            if (CurrentMonster.IsDead)
            {
                LootTheCurrentMonster();
                // "Move" to the current location, to refresh the current monster
                MoveTo(CurrentLocation);
            }
            else
            {
                LetTheMonsterAttack();
            }
        }
        private void LootTheCurrentMonster()
        {
            RaiseMessage("");
            RaiseMessage("You defeated the " + CurrentMonster.Name);
            RaiseMessage("You receive " + CurrentMonster.RewardExperiencePoints + " experience points");
            RaiseMessage("You receive " + CurrentMonster.RewardGold + " gold");
            AddExperiencePoints(CurrentMonster.RewardExperiencePoints);
            Gold += CurrentMonster.RewardGold;
            // Give monster's loot items to the player
            foreach (InventoryItem inventoryItem in CurrentMonster.LootItems)
            {
                AddItemToInventory(inventoryItem.Details);
                RaiseMessage(string.Format("You loot {0} {1}", inventoryItem.Quantity, inventoryItem.Description));
            }
            RaiseMessage("");
        }
        public void UsePotion(HealingPotion potion)
        {
            RaiseMessage("You drink a " + potion.Name);
            HealPlayer(potion.AmountToHeal);
            RemoveItemFromInventory(potion);
            // The player used their turn to drink the potion, so let the monster attack now
            LetTheMonsterAttack();
        }
        public void AddItemToInventory(Item itemToAdd, int quantity = 1)
        {
            InventoryItem existingItemInInventory = Inventory.SingleOrDefault(ii => ii.Details.ID == itemToAdd.ID);
            if (existingItemInInventory == null)
            {
                Inventory.Add(new InventoryItem(itemToAdd, quantity));
            }
            else
            {
                existingItemInInventory.Quantity += quantity;
            }
            RaiseInventoryChangedEvent(itemToAdd);
        }
        public void RemoveItemFromInventory(Item itemToRemove, int quantity = 1)
        {
            InventoryItem item = Inventory.SingleOrDefault(ii => ii.Details.ID == itemToRemove.ID && ii.Quantity >= quantity);
            if (item != null)
            {
                item.Quantity -= quantity;
                if (item.Quantity == 0)
                {
                    Inventory.Remove(item);
                }
                RaiseInventoryChangedEvent(itemToRemove);
            }
        }
        public string ToXmlString()
        {
            XmlDocument playerData = new XmlDocument();
            // Create the top-level XML node
            XmlNode player = playerData.CreateElement("Player");
            playerData.AppendChild(player);
            // Create the "Stats" child node to hold the other player statistics nodes
            XmlNode stats = playerData.CreateElement("Stats");
            player.AppendChild(stats);
            // Create the child nodes for the "Stats" node
            CreateNewChildXmlNode(playerData, stats, "CurrentHitPoints", CurrentHitPoints);
            CreateNewChildXmlNode(playerData, stats, "MaximumHitPoints", MaximumHitPoints);
            CreateNewChildXmlNode(playerData, stats, "Gold", Gold);
            CreateNewChildXmlNode(playerData, stats, "ExperiencePoints", ExperiencePoints);
            CreateNewChildXmlNode(playerData, stats, "CurrentLocation", CurrentLocation.ID);
            if (CurrentWeapon != null)
            {
                CreateNewChildXmlNode(playerData, stats, "CurrentWeapon", CurrentWeapon.ID);
            }
            // Create the "LocationsVisited" child node to hold each LocationVisited node
            XmlNode locationsVisited = playerData.CreateElement("LocationsVisited");
            player.AppendChild(locationsVisited);
            // Create an "LocationVisited" node for each item in the player's inventory
            foreach (int locationID in LocationsVisited)
            {
                XmlNode locationVisited = playerData.CreateElement("LocationVisited");
                AddXmlAttributeToNode(playerData, locationVisited, "ID", locationID);
                locationsVisited.AppendChild(locationVisited);
            }
            // Create the "InventoryItems" child node to hold each InventoryItem node
            XmlNode inventoryItems = playerData.CreateElement("InventoryItems");
            player.AppendChild(inventoryItems);
            // Create an "InventoryItem" node for each item in the player's inventory
            foreach (InventoryItem item in Inventory)
            {
                XmlNode inventoryItem = playerData.CreateElement("InventoryItem");
                AddXmlAttributeToNode(playerData, inventoryItem, "ID", item.Details.ID);
                AddXmlAttributeToNode(playerData, inventoryItem, "Quantity", item.Quantity);
                inventoryItems.AppendChild(inventoryItem);
            }
            // Create the "PlayerQuests" child node to hold each PlayerQuest node
            XmlNode playerQuests = playerData.CreateElement("PlayerQuests");
            player.AppendChild(playerQuests);
            // Create a "PlayerQuest" node for each quest the player has acquired
            foreach (PlayerQuest quest in Quests)
            {
                XmlNode playerQuest = playerData.CreateElement("PlayerQuest");
                AddXmlAttributeToNode(playerData, playerQuest, "ID", quest.Details.ID);
                AddXmlAttributeToNode(playerData, playerQuest, "IsCompleted", quest.IsCompleted);
                playerQuests.AppendChild(playerQuest);
            }
            return playerData.InnerXml; // The XML document, as a string, so we can save the data to disk
        }
        private bool HasRequiredItemToEnterThisLocation(Location location)
        {
            if (location.DoesNotHaveAnItemRequiredToEnter)
            {
                return true;
            }
            // See if the player has the required item in their inventory
            return Inventory.Any(ii => ii.Details.ID == location.ItemRequiredToEnter.ID);
        }
        private void SetTheCurrentMonsterForTheCurrentLocation(Location location)
        {
            // Populate the current monster with this location's monster (or null, if there is no monster here)
            CurrentMonster = location.NewInstanceOfMonsterLivingHere();
            if (CurrentMonster != null)
            {
                RaiseMessage("You see a " + CurrentMonster.Name);
            }
        }
        private bool PlayerDoesNotHaveTheRequiredItemToEnter(Location location)
        {
            return !HasRequiredItemToEnterThisLocation(location);
        }
        private bool PlayerDoesNotHaveThisQuest(Quest quest)
        {
            return Quests.All(pq => pq.Details.ID != quest.ID);
        }
        private bool PlayerHasNotCompleted(Quest quest)
        {
            return Quests.Any(pq => pq.Details.ID == quest.ID && !pq.IsCompleted);
        }
        private void GiveQuestToPlayer(Quest quest)
        {
            RaiseMessage("You receive the " + quest.Name + " quest.");
            RaiseMessage(quest.Description);
            RaiseMessage("To complete it, return with:");
            foreach (QuestCompletionItem qci in quest.QuestCompletionItems)
            {
                RaiseMessage(string.Format("{0} {1}", qci.Quantity,
                                           qci.Quantity == 1 ? qci.Details.Name : qci.Details.NamePlural));
            }
            RaiseMessage("");
            Quests.Add(new PlayerQuest(quest));
        }
        private bool PlayerHasAllQuestCompletionItemsFor(Quest quest)
        {
            // See if the player has all the items needed to complete the quest here
            foreach (QuestCompletionItem qci in quest.QuestCompletionItems)
            {
                // Check each item in the player's inventory, to see if they have it, and enough of it
                if (!Inventory.Any(ii => ii.Details.ID == qci.Details.ID && ii.Quantity >= qci.Quantity))
                {
                    return false;
                }
            }
            // If we got here, then the player must have all the required items, and enough of them, to complete the quest.
            return true;
        }
        private void RemoveQuestCompletionItems(Quest quest)
        {
            foreach (QuestCompletionItem qci in quest.QuestCompletionItems)
            {
                InventoryItem item = Inventory.SingleOrDefault(ii => ii.Details.ID == qci.Details.ID);
                if (item != null)
                {
                    RemoveItemFromInventory(item.Details, qci.Quantity);
                }
            }
        }
        private void AddExperiencePoints(int experiencePointsToAdd)
        {
            ExperiencePoints += experiencePointsToAdd;
            MaximumHitPoints = (Level * 10);
        }
        private void GivePlayerQuestRewards(Quest quest)
        {
            RaiseMessage("");
            RaiseMessage("You complete the '" + quest.Name + "' quest.");
            RaiseMessage("You receive: ");
            RaiseMessage(quest.RewardExperiencePoints + " experience points");
            RaiseMessage(quest.RewardGold + " gold");
            RaiseMessage(quest.RewardItem.Name, true);
            AddExperiencePoints(quest.RewardExperiencePoints);
            Gold += quest.RewardGold;
            RemoveQuestCompletionItems(quest);
            AddItemToInventory(quest.RewardItem);
            MarkPlayerQuestCompleted(quest);
        }
        private void MarkPlayerQuestCompleted(Quest quest)
        {
            PlayerQuest playerQuest = Quests.SingleOrDefault(pq => pq.Details.ID == quest.ID);
            if (playerQuest != null)
            {
                playerQuest.IsCompleted = true;
            }
        }
        private void LetTheMonsterAttack()
        {
            int damageToPlayer = RandomNumberGenerator.NumberBetween(0, CurrentMonster.MaximumDamage);
            RaiseMessage("The " + CurrentMonster.Name + " did " + damageToPlayer + " points of damage.");
            CurrentHitPoints -= damageToPlayer;
            if (IsDead)
            {
                RaiseMessage("The " + CurrentMonster.Name + " killed you.");
                MoveHome();
            }
        }
        private void HealPlayer(int hitPointsToHeal)
        {
            CurrentHitPoints = Math.Min(CurrentHitPoints + hitPointsToHeal, MaximumHitPoints);
        }
        private void CompletelyHeal()
        {
            CurrentHitPoints = MaximumHitPoints;
        }
        private void MoveHome()
        {
            MoveTo(World.LocationByID(World.LOCATION_ID_HOME));
        }
        private void CreateNewChildXmlNode(XmlDocument document, XmlNode parentNode, string elementName, object value)
        {
            XmlNode node = document.CreateElement(elementName);
            node.AppendChild(document.CreateTextNode(value.ToString()));
            parentNode.AppendChild(node);
        }
        private void AddXmlAttributeToNode(XmlDocument document, XmlNode node, string attributeName, object value)
        {
            XmlAttribute attribute = document.CreateAttribute(attributeName);
            attribute.Value = value.ToString();
            node.Attributes.Append(attribute);
        }
        private void RaiseInventoryChangedEvent(Item item)
        {
            if (item is Weapon)
            {
                OnPropertyChanged("Weapons");
            }
            if (item is HealingPotion)
            {
                OnPropertyChanged("Potions");
            }
        }
        private void RaiseMessage(string message, bool addExtraNewLine = false)
        {
            if (OnMessage != null)
            {
                OnMessage(this, new MessageEventArgs(message, addExtraNewLine));
            }
        }
    }
}

Step 5: Edit Engine\PlayerDataMapper.cs

We also need to save the LocationsVisited values to the database, and read them when loading a saved game from the database – if you are using a database to save the game data.

To save the location IDs, we’ll create a new table named LocationVisited. It will only have a single column “ID”, whose datatype is “int”, and does not all nulls. The script to create it is below.

 

USE [SuperAdventure]
GO
/****** Object:  Table [dbo].[LocationVisited]    Script Date: 8/17/2017 7:05:15 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[LocationVisited](
	[ID] [int] NOT NULL
) ON [PRIMARY]
GO

 

Next, we need to update PlayerDataMapper to save the values into this table, and read the values from it.

The code to do this is like the code for adding and reading the values for the InventoryItems and PlayerQuests.

 

We save the Location IDs to the table in the SaveToDatabase() function, at lines 286 through 307.

The code to read from this table is in the CreateFromDatabase() frunction, at lines 111 through 131.

 

NOTE: I noticed a bug with the readers not closing. So, inside each “using” block of code in CreateFromDatabase, I’ve added a “reader.Close();”. These are on lines 58, 85, 108, and 130.

 

PlayerDataMapper.cs

using System;
using System.Data;
using System.Data.SqlClient;
namespace Engine
{
    public static class PlayerDataMapper
    {
        private static readonly string _connectionString = "Data Source=(local);Initial Catalog=SuperAdventure;Integrated Security=True";
        public static Player CreateFromDatabase()
        {
            try
            {
                // This is our connection to the database
                using(SqlConnection connection = new SqlConnection(_connectionString))
                {
                    // Open the connection, so we can perform SQL commands
                    connection.Open();
                    Player player;
                    // Create a SQL command object, that uses the connection to our database
                    // The SqlCommand object is where we create our SQL statement
                    using(SqlCommand savedGameCommand = connection.CreateCommand())
                    {
                        savedGameCommand.CommandType = CommandType.Text;
                        // This SQL statement reads the first rows in teh SavedGame table.
                        // For this program, we should only ever have one row,
                        // but this will ensure we only get one record in our SQL query results.
                        savedGameCommand.CommandText = "SELECT TOP 1 * FROM SavedGame";
                        // Use ExecuteReader when you expect the query to return a row, or rows
                        SqlDataReader reader = savedGameCommand.ExecuteReader();
                        // Check if the query did not return a row/record of data
                        if(!reader.HasRows)
                        {
                            // There is no data in the SavedGame table, 
                            // so return null (no saved player data)
                            return null;
                        }
                        // Get the row/record from the data reader
                        reader.Read();
                        // Get the column values for the row/record
                        int currentHitPoints = (int)reader["CurrentHitPoints"];
                        int maximumHitPoints = (int)reader["MaximumHitPoints"];
                        int gold = (int)reader["Gold"];
                        int experiencePoints = (int)reader["ExperiencePoints"];
                        int currentLocationID = (int)reader["CurrentLocationID"];
                        // Create the Player object, with the saved game values
                        player = Player.CreatePlayerFromDatabase(currentHitPoints, maximumHitPoints, gold,
                            experiencePoints, currentLocationID);
                        reader.Close();
                    }
                    // Read the rows/records from the Quest table, and add them to the player
                    using(SqlCommand questCommand = connection.CreateCommand())
                    {
                        questCommand.CommandType = CommandType.Text;
                        questCommand.CommandText = "SELECT * FROM Quest";
                        SqlDataReader reader = questCommand.ExecuteReader();
                        if(reader.HasRows)
                        {
                            while(reader.Read())
                            {
                                int questID = (int)reader["QuestID"];
                                bool isCompleted = (bool)reader["IsCompleted"];
                                // Build the PlayerQuest item, for this row
                                PlayerQuest playerQuest = new PlayerQuest(World.QuestByID(questID));
                                playerQuest.IsCompleted = isCompleted;
                                // Add the PlayerQuest to the player's property
                                player.Quests.Add(playerQuest);
                            }
                        }
                        reader.Close();
                    }
                    // Read the rows/records from the Inventory table, and add them to the player
                    using (SqlCommand inventoryCommand = connection.CreateCommand())
                    {
                        inventoryCommand.CommandType = CommandType.Text;
                        inventoryCommand.CommandText = "SELECT * FROM Inventory";
                        SqlDataReader reader = inventoryCommand.ExecuteReader();
                        if(reader.HasRows)
                        {
                            while(reader.Read())
                            {
                                int inventoryItemID = (int)reader["InventoryItemID"];
                                int quantity = (int)reader["Quantity"];
                                // Add the item to the player's inventory
                                player.AddItemToInventory(World.ItemByID(inventoryItemID), quantity);
                            }
                        }
                        reader.Close();
                    }
                    // Read the rows/records from the LocationVisited table, and add them to the player
                    using (SqlCommand locationVisitedCommand = connection.CreateCommand())
                    {
                        locationVisitedCommand.CommandType = CommandType.Text;
                        locationVisitedCommand.CommandText = "SELECT * FROM LocationVisited";
                        SqlDataReader reader = locationVisitedCommand.ExecuteReader();
                        if (reader.HasRows)
                        {
                            while (reader.Read())
                            {
                                int id = (int)reader["ID"];
                                // Add the item to the player's LocationsVisited property
                                player.LocationsVisited.Add(id);
                            }
                        }
                        reader.Close();
                    }
                    // Now that the player has been built from the database, return it.
                    return player;
                }
            }
            catch(Exception ex)
            {
                // Ignore errors. If there is an error, this function will return a "null" player.
            }
            return null;
        }
        public static void SaveToDatabase(Player player)
        {
            try
            {
                using(SqlConnection connection = new SqlConnection(_connectionString))
                {
                    // Open the connection, so we can perform SQL commands
                    connection.Open();
                    // Insert/Update data in SavedGame table
                    using(SqlCommand existingRowCountCommand = connection.CreateCommand())
                    {
                        existingRowCountCommand.CommandType = CommandType.Text;
                        existingRowCountCommand.CommandText = "SELECT count(*) FROM SavedGame";
                        // Use ExecuteScalar when your query will return one value
                        int existingRowCount = (int)existingRowCountCommand.ExecuteScalar();
                        if(existingRowCount == 0)
                        {
                            // There is no existing row, so do an INSERT
                            using(SqlCommand insertSavedGame = connection.CreateCommand())
                            {
                                insertSavedGame.CommandType = CommandType.Text;
                                insertSavedGame.CommandText = 
                                    "INSERT INTO SavedGame " +
                                    "(CurrentHitPoints, MaximumHitPoints, Gold, ExperiencePoints, CurrentLocationID) " +
                                    "VALUES " +
                                    "(@CurrentHitPoints, @MaximumHitPoints, @Gold, @ExperiencePoints, @CurrentLocationID)";
                                // Pass the values from the player object, to the SQL query, using parameters
                                insertSavedGame.Parameters.Add("@CurrentHitPoints", SqlDbType.Int);
                                insertSavedGame.Parameters["@CurrentHitPoints"].Value = player.CurrentHitPoints;
                                insertSavedGame.Parameters.Add("@MaximumHitPoints", SqlDbType.Int);
                                insertSavedGame.Parameters["@MaximumHitPoints"].Value = player.MaximumHitPoints;
                                insertSavedGame.Parameters.Add("@Gold", SqlDbType.Int);
                                insertSavedGame.Parameters["@Gold"].Value = player.Gold;
                                insertSavedGame.Parameters.Add("@ExperiencePoints", SqlDbType.Int);
                                insertSavedGame.Parameters["@ExperiencePoints"].Value = player.ExperiencePoints;
                                insertSavedGame.Parameters.Add("@CurrentLocationID", SqlDbType.Int);
                                insertSavedGame.Parameters["@CurrentLocationID"].Value = player.CurrentLocation.ID;
                                // Perform the SQL command.
                                // Use ExecuteNonQuery, because this query does not return any results.
                                insertSavedGame.ExecuteNonQuery();
                            }
                        }
                        else
                        {
                            // There is an existing row, so do an UPDATE
                            using(SqlCommand updateSavedGame = connection.CreateCommand())
                            {
                                updateSavedGame.CommandType = CommandType.Text;
                                updateSavedGame.CommandText =
                                    "UPDATE SavedGame " +
                                    "SET CurrentHitPoints = @CurrentHitPoints, " +
                                    "MaximumHitPoints = @MaximumHitPoints, " +
                                    "Gold = @Gold, " +
                                    "ExperiencePoints = @ExperiencePoints, "+
                                    "CurrentLocationID = @CurrentLocationID";
                                // Pass the values from the player object, to the SQL query, using parameters
                                // Using parameters helps make your program more secure.
                                // It will prevent SQL injection attacks.
                                updateSavedGame.Parameters.Add("@CurrentHitPoints", SqlDbType.Int);
                                updateSavedGame.Parameters["@CurrentHitPoints"].Value = player.CurrentHitPoints;
                                updateSavedGame.Parameters.Add("@MaximumHitPoints", SqlDbType.Int);
                                updateSavedGame.Parameters["@MaximumHitPoints"].Value = player.MaximumHitPoints;
                                updateSavedGame.Parameters.Add("@Gold", SqlDbType.Int);
                                updateSavedGame.Parameters["@Gold"].Value = player.Gold;
                                updateSavedGame.Parameters.Add("@ExperiencePoints", SqlDbType.Int);
                                updateSavedGame.Parameters["@ExperiencePoints"].Value = player.ExperiencePoints;
                                updateSavedGame.Parameters.Add("@CurrentLocationID", SqlDbType.Int);
                                updateSavedGame.Parameters["@CurrentLocationID"].Value = player.CurrentLocation.ID;
                                // Perform the SQL command.
                                // Use ExecuteNonQuery, because this query does not return any results.
                                updateSavedGame.ExecuteNonQuery();
                            }
                        }
                    }
                    // The Quest and Inventory tables might have more, or less, rows in the database
                    // than what the player has in their properties.
                    // So, when we save the player's game, we will delete all the old rows
                    // and add in all new rows.
                    // This is easier than trying to add/delete/update each individual rows
                    // Delete existing Quest rows
                    using(SqlCommand deleteQuestsCommand = connection.CreateCommand())
                    {
                        deleteQuestsCommand.CommandType = CommandType.Text;
                        deleteQuestsCommand.CommandText = "DELETE FROM Quest";
                        deleteQuestsCommand.ExecuteNonQuery();
                    }
                    // Insert Quest rows, from the player object
                    foreach(PlayerQuest playerQuest in player.Quests)
                    {
                        using(SqlCommand insertQuestCommand = connection.CreateCommand())
                        {
                            insertQuestCommand.CommandType = CommandType.Text;
                            insertQuestCommand.CommandText = "INSERT INTO Quest (QuestID, IsCompleted) VALUES (@QuestID, @IsCompleted)";
                            insertQuestCommand.Parameters.Add("@QuestID", SqlDbType.Int);
                            insertQuestCommand.Parameters["@QuestID"].Value = playerQuest.Details.ID;
                            insertQuestCommand.Parameters.Add("@IsCompleted", SqlDbType.Bit);
                            insertQuestCommand.Parameters["@IsCompleted"].Value = playerQuest.IsCompleted;
                            insertQuestCommand.ExecuteNonQuery();
                        }
                    }
                    // Delete existing Inventory rows
                    using(SqlCommand deleteInventoryCommand = connection.CreateCommand())
                    {
                        deleteInventoryCommand.CommandType = CommandType.Text;
                        deleteInventoryCommand.CommandText = "DELETE FROM Inventory";
                        deleteInventoryCommand.ExecuteNonQuery();
                    }
                    // Insert Inventory rows, from the player object
                    foreach(InventoryItem inventoryItem in player.Inventory)
                    {
                        using(SqlCommand insertInventoryCommand = connection.CreateCommand())
                        {
                            insertInventoryCommand.CommandType = CommandType.Text;
                            insertInventoryCommand.CommandText = "INSERT INTO Inventory (InventoryItemID, Quantity) VALUES (@InventoryItemID, @Quantity)";
                            insertInventoryCommand.Parameters.Add("@InventoryItemID", SqlDbType.Int);
                            insertInventoryCommand.Parameters["@InventoryItemID"].Value = inventoryItem.Details.ID;
                            insertInventoryCommand.Parameters.Add("@Quantity", SqlDbType.Int);
                            insertInventoryCommand.Parameters["@Quantity"].Value = inventoryItem.Quantity;
                            insertInventoryCommand.ExecuteNonQuery();
                        }
                    }
                    // Delete existing LocationVisited rows
                    using (SqlCommand deleteLocationVisitedCommand = connection.CreateCommand())
                    {
                        deleteLocationVisitedCommand.CommandType = CommandType.Text;
                        deleteLocationVisitedCommand.CommandText = "DELETE FROM LocationVisited";
                        deleteLocationVisitedCommand.ExecuteNonQuery();
                    }
                    // Insert LocationVisited rows, from the player object
                    foreach (int locationVisitedID in player.LocationsVisited)
                    {
                        using (SqlCommand insertLocationVisitedCommand = connection.CreateCommand())
                        {
                            insertLocationVisitedCommand.CommandType = CommandType.Text;
                            insertLocationVisitedCommand.CommandText = "INSERT INTO LocationVisited (ID) VALUES (@ID)";
                            insertLocationVisitedCommand.Parameters.Add("@ID", SqlDbType.Int);
                            insertLocationVisitedCommand.Parameters["@ID"].Value = locationVisitedID;
                            insertLocationVisitedCommand.ExecuteNonQuery();
                        }
                    }
                }
            }
            catch(Exception ex)
            {
                // We are going to ignore errors, for now.
            }
        }
    }
}

Step 5: Test the game.

Now, as the player moves to new locations, the map will display more images – instead of the “fog” image for unvisited locations. The map should start to look like this (for example):

 

 

Summary

This uses hard-coded values for placing the images in the PictureBox, which isn’t the best way to create a map. This would be much more flexible if we used X and Y coordinates for the locations. Then, we could do things like having the map always centered on the player’s current location, and showing a 5 x 5 (or larger) grid of the surrounding locations.

If you follow the “Build a C#/WPF RPG” lessons, that is how we are building that world.

 

Source code for this lesson

Source code on GitHub

Source code on Dropbox

 

Previous lesson: Lesson 26.1 Displaying a World Map

All lessons: Learn C# by Building a Simple RPG Index

21 Comments

  1. Red
    Red September 23, 2017

    Hi!

    The same problem, that causes the Quest bug, adds the last saved location to the empty LocationsVisited list, and that causes duplicated rows in the LocationVisited data table after you start the program multiple times.

    I think Lesson 99.1 Bugfix will solve this problem.

    • Scott Lilly
      Scott Lilly September 24, 2017

      Correct, it should solve that problem. If anyone has duplicated locations in their data, they should probably delete the second (or more) instances of the locations. It might also be nice to create an AddLocationoLocationsVisited function (like we do for adding items to the player’s inventory) that would prevent adding a duplicate location. Although, it’s always best if we have code the doesn’t let the data ever get into a bad condition.

  2. NOTaROBOT
    NOTaROBOT February 11, 2018

    May be there is a miss “on lines 174 to 177” when in code bellow on 167, 169

  3. NoneOfRobots
    NoneOfRobots February 27, 2018

    Hi Scott! Thank you very much again i need help :C
    I managed to do everything and i was happy, then i got the final step of localisation so other programmers would understand the code in my contry, and i started… I was happy that everything worked and went away from the project for a while and then i realised that the project is not saving the player.. only creating new one every time when a user tries to play. I tried to pin the problem down but everything was in vein, the program doesn’t show any sign of mistakes but still doesn’t save the player. So i just tried to return all the names of the functions and methods and try again, there were no mistakes but still nothing new.. Could you pls help me with my final step of finishing the game. If you can’t spot the problem that’s all right i will just delete this function of saving from the game, i’ll be happy to any help!

    https://www.dropbox.com/s/dyf5bhm9tpkkmrn/ForgottenLand2.rar?dl=0

    • Scott Lilly
      Scott Lilly February 27, 2018

      I think I found the problem. In ForgottenLand.cs, starting on line 89, the “else if” code is probably not running the way you want. There are lines after the “else if”, but before the {} code block. When code is like that, the compiler pretends there is a pair of curly braces around the next one line – and not any lines after it.

      There are some notes here: https://gist.github.com/ScottLilly/8516a40ec115b3c512da096e2cf8b8ea

      Because there is not an “if” or “else if” before line 8 (in Before.cs), that code always runs. So, the program always creates a new player.

      If you want to see what is happening, you can use the debugger. Here is a lesson on how to use the Visual Studio debugger. You can set a breakpoint on line 89 and see what is happening when you run the program.

      • NoneOfRobots
        NoneOfRobots February 27, 2018

        Thank you! This is unbelievable how only one line can stop all the work of the application. it is difficult to imagine how people can work with multiple projects with over 1000 lines in it.

        • Scott Lilly
          Scott Lilly February 28, 2018

          You’re welcome. When your programs grow larger, it helps to “refactor” after you add a new feature. When you always keep your source code clean and organized, it is easier to work with.

  4. Ferlin Scarborough
    Ferlin Scarborough May 12, 2018

    I could not run the SuperAdventure because I was hitting an error about the Quests containing more than one entry for the same quest, ID = 1.

    I fixed the problem in my copy of the game by doing the following in the Player.cs file.

    Above the PlayerDoesNotHaveThisQuest method I added the following method:

    private bool PlayerHasThisQuest(Quest quest) 
    { 
        return Quests.All(pq => pq.Details.ID == quest.ID); 
    }

    Then I changed the GiveQuestToPlayer method by adding an if statement at the top:

    private void GiveQuestToPlayer(Quest quest) 
    { 
        if (PlayerHasThisQuest(quest)) return;
        RaiseMessage("You receive the " + quest.Name + " quest."); 
        RaiseMessage(quest.Description); 
        RaiseMessage("To complete it, return with:");
        foreach (QuestCompletionItem qci in quest.QuestCompletionItems) 
        { 
            RaiseMessage(string.Format("{0} {1}", qci.Quantity, 
                qci.Quantity == 1 ? qci.Details.Name : qci.Details.NamePlural)); 
        }
        RaiseMessage("");
        Quests.Add(new PlayerQuest(quest)); 
    }

    There may be a better way of fixing this error, but this particular way worked for me.

    • Scott Lilly
      Scott Lilly May 12, 2018

      Hi Ferlin,

      There was a bug if the player exited the game at a location with a quest. When they restart the game, it gives the player the quest from their saved Quests list, and tries to give the quest again since they are at the location. Your fix will prevent the game from crashing – or, you can use the changes in lesson 99.1.

  5. Stan Morris
    Stan Morris June 1, 2018

    I was looking back on the site and was surprised to see all the changes to the application and some integration with the WPF project! I was going to do some expansion and add images to the original program only to find that it has already been done! Great job on this Scott. It has been a real help to a lot of folks.

    • Scott Lilly
      Scott Lilly June 1, 2018

      Thanks Stan! This started out as a little project in my spare time, but has grown larger than I ever thought it would. It always great to hear from people who have learned something from the lessons.

      I just started a new series this week to refactor the SuperAdventure code into higher-quality code that will be easier to work with and modify.

  6. Ryan
    Ryan September 5, 2018

    Hi Scott,

    Thank you for taking your time to create this tutorial, I have found it extremely helpful! However there are a few things I am unsure of, how would I go abou highlighting the current position on the map form? And how would I go about making quests repeatable? I’ve made several changes to the game, including adding item weighhs and inventory max weight, but I can’t seem to update the players current weight after adding or removing quest items, any suggestions would be appreciated. Thanks in advance!

    • Scott Lilly
      Scott Lilly September 6, 2018

      Hello Ryan,

      You’re welcome! Here is some code you can use to highlight the player’s current location in the world map: https://gist.github.com/ScottLilly/f02d10322a8184f2424a2e601972786c. I did a little cleanup of this class, but the important part is in lines 41-51.

      If you want all quests to be repeatable, you would need to change the MoveTo code that gives the player the quests (line 183 of Player.cs). Currently, it gives the player the quest if they do not already have it. You could change that to “if the player does not have the quest, or if they do have it, but it’s completed”. Or, when the player completes a quest, you could remove the PlayerQuest object from the Player.Quests property. Or, you could modify the Quest class to have a boolean property “IsRepeatable”, if you only want some quests to be repeatable. Then, change your code around line 183 (of Player.cs) to give the player the quest “if the player does not have the quest, or if they have the quest, it’s completed, and it’s repeatable)”.

      For the item weights, I would create a “CalculateWeight” function that is called at the end of “AddItemToInventory” and “RemoveItemFromInventory”. Make sure your Weight property is calling OnPropertyChanged, so the UI knows it needs to refresh the screen, and that your SUperAdventure constructor has a DataBindings.Add for the label displaying the weight value.

      Let me know if you have any questions about those suggestions.

      • Ryan
        Ryan September 6, 2018

        Wow, thanks for such a quick response! Your suggestions are certainly valuable, I have a pretty basic understanding of the OOP paradigm, I understand inheritance, polymorphism and such but I struggle to apply these concepts to real world problems, it amazes me how I didn’t even consider the suggestions you made of my own accord, now you’ve suggested them it seems very simplistic. I do however have another question, I’m not familiar with LINQ at all, I’ve never encountered it before your tutorial and I was wondering if I could utilise it for filtering the inventory list when displaying the vendors trading form to filter out quest items, now would I need to make a new list and bind that to the data grid view or can I simply filter the existing inventory list? The code I have right now doesn’t display quest items, but it also removes the item from the players inventory, thanks again Scott!

        • Scott Lilly
          Scott Lilly September 6, 2018

          You’re welcome. The more programs you work on, the more you’ll build up a mental “library” of how to solve problems and make changes. If you haven’t studied “design patterns“, those are some common solutions to common problems that are good to learn.

          You could use LINQ to create a filtered list of inventory objects. That’s what we do with Player.Weapons and Player.Potions. So, you could create a “List SellableItems” property. Just remember these “derived properties” don’t automatically notify the UI of changes. So, when you change the base/underlying property, you’ll need to manually raise a PropertyChanged notification for your derived property. We do this when we call RaiseInventoryChangedEvent from AddItemToInventory and RemoveItemFromInventory – to notify the UI that the derived “Weapons” and “Potions” property has changed, and needs to be refreshed in the UI.

          Or, you could apply a filter to the DataGrid.DataSource, like this: https://stackoverflow.com/questions/21845016/how-to-filter-datagridview-in-c-sharp-win-forms.

          • Ryan
            Ryan September 15, 2018

            Hey Scott,

            I’ve made several changes since my last post. Thanks to your help I’ve managed to resolve the item weights being incremented or decremented whenever a quest item is awarded or removed respectively, I kind of followed your suggestion and rather than creating a new function, I modified the existing function UpdatePlayerStats() which calculates the players inventory weight by multiplying the item weight by quantity for each item.

            I’ve also managed to create repeatable quests which prompted another issue of duplicate quests, however I managed to resolve this issue by modifying the MarkQuestAsComplete(Quest quest).

            I have since ran into a number of issues where I would greatly appreciate your assistance. First of all I’ve read over multiple comments on pretty much all the lesson pages to see if I can find an answer, before taking up any more of your time. You suggested to someone on how to save and load the vendors inventory to XML, I’ve managed to get my game to successfully save the vendors inventory, however I’m unsure as to how to load the data, you mentioned creating a function in the TradingScreen constructor for reading the data and then passing the data to the Vendor class, this is where I’m stumped.

            I was also wondering how I would go about changing the XP needed to reach the next level, determined by the players current level and difficulty setting, I think I know how to do this but it would require modifying how the players level is calculated, I’ve also encountered several posts on here that refer to the same issue, however my maximum player level is 100 so if else statements seem inadequate. I have messed around with this loads but have failed to come up with anything even remotely close to what I’m trying to achieve.

            Additionally I’d like to remove all the players inventory items besides unsellable items and one weapon whenever the player dies(A weapon that the user can choose if multiple weapons are present). I understand you have priorities, I am going to try and solve all these issues by myself in the meantime, but if you could point me in the right direction that would be extremely helpful, thanks a bunch!

          • Scott Lilly
            Scott Lilly September 16, 2018

            Hi Ryan,

            I saw your other comment that you got some of the new features working. That’s great!

            For the trader inventory, it’s probably simplest to load the trader’s inventory in the World.PopulateLocations() function. However, because the World class is static, it might be difficult to catch any errors there. If you do have any problems, set a debug breakpoint at the beginning of PopulateLocations() and step through the code (using F10) to see where the error happens.

            For the experience points needed for each level, you would probably want to create a mathematical equation that gradually increases the amount of XP needed for each level. This is often done with an exponent, like in these examples: http://howtomakeanrpg.com/a/how-to-make-an-rpg-levels.html

  7. Ryan
    Ryan September 15, 2018

    Okay,

    Update:

    After a lot of debugging I’ve managed to implement the removal of all items besides any unsellable items and the current weapon when the player dies. I’ve also managed to implement the fleeing message whenever a player moves to a new location and they’ve been attacked but haven’t killed the monster.

  8. Ryan
    Ryan October 13, 2018

    Hello again Scott, I know it’s been a while, I’ve been moving house myself. Since my last post I’ve added several items into the game, including food and armour. I decided to add a tab control with each tab displaying different items, for instance a tab for all items, weapons, armour, food etc… and each tab has a datagridview, this all works fine, updates and everything however I don’t know how to display the quantity for items such as food or weapons since they are bound to lists of the respective object, not inventory items, each of the classes has properties which I am displaying in the datagridview therefore filtering the inventory for each item type doesnt appear to be an adequate solution. I even added the ability to move up or down within a location so for example your home has a basement and first floor, again this works as expected however I’d like to display both a world map and a local map, whenever I create a new world map object it is populated with the same images, I’m not sure how to separate and differentiate between the two. Additionally I’ve added the ability for the player to add a chest to their current location, this value is then saved in the players XML file, and when the game is relaunched these values persist until the player moves location and the values are overwritten by the world class, I’m not sure how I would go about this issue, could storing the Boolean chest value in visited locations be a solution? My computer isn’t assembled right now so I can’t actually check to see if this would work or not. I am sorry for the long list of problems but another issue I’ve been having is, I’ve changed the datagridviews button text when certain conditions are met, this works as expected but then the logic I have is determined by the buttons text value which always seems to return the item ID regardless of the texts value, I know they are linked in order for the logic to reference the item by ID but I don’t understand why this is happening. Any insight would be receieved with the utmost appreciation, thank you!

    • Scott Lilly
      Scott Lilly October 17, 2018

      Hi Ryan,

      I hope you’re settled in to your new house. Moving always drains my energy for a couple weeks. It sounds like you’ve done a huge amount of expansion of the game!

      One way to show quantities in the inventory is in the WPF lessons (10.2 and 10.4). We have a GroupedInventory class that has the item and quantity. Then, we update the individual Weapons and Potions properties whenever the base Inventory list changes (items are added or removed).

      The local map might be a little tricky – especially if you want to have a different image for each room, like we do for each location in the world map. The easiest way would just be to have a single image for each location’s local map. Create a new LocalMapFileName property in the Location class, and populate it in World.cs. Then, create a new LocalMap.cs form (in the UI project) that works like WorldMap.cs, but displays the image for the LocalMapFileName of the Player’s CurrentLocation.

      Do you want the player to be able to have a chest in each location, or only one chest in the whole world? If the player can have more than one chest, can they store different items in each chest? You might want to store the chests information similar to the Quests or InventoryItems, like this: https://gist.github.com/ScottLilly/88948812f277728fc5e6d5b2771e0b59. Then, Create a Chest class that has a LocationID property and a property that is a list of inventory items. In the Player class, create a new Chests property (datatype is a list of Chest objects). When the Player moves to a location, see if they have a chest with that LocationID.

      I’d probably have to see the code for the datagrid button problem. I’m not completely clear on it. When your computer is re-assembled, if you still have questions, can you upload your version of the program to GitHub or Dropbox, so I can look at it?

Leave a Reply

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