Lesson 4.7: Ability to Display Quest and Recipe Data

Currently we have the quest and recipe data concisely shown in the Quests and Recipes tables that only display their names and status. Once we get past the original receiving of the quest, it can be difficult to remember that is needed to complete a quest or recipe. So we are going to enable the player to see the description of those two entities on demand.

In the original tutorial series, this was done in WPF using tooltips. We could have done the same in our Blazor app (the Blazorise library even has a Tooltip component). But hover tooltips are not a great experience for touch-enabled devices. So we are going to provide a different implementation to re-show the quest and recipe details in the Game Messages section of our screen.

When the user clicks on either a quest or recipe line item, then we will show each one’s description in that message area.

Refactoring Quest and Recipe

To enable this, we are going to implement some methods to build a DisplayMessage with the appropriate description, requirements, and output. We want to encapsulate that information in the corresponding class, rather than in our GameSession.

Let’s start with an update to the ItemQuantity class.

using SimpleRPG.Game.Engine.Factories;

namespace SimpleRPG.Game.Engine.Models
{
    public class ItemQuantity
    {
        public int ItemId { get; set; }

        public int Quantity { get; set; }

        public string QuantityItemDescription =>
            $"{ItemFactory.GetItemName(ItemId)} (x{Quantity})";
    }
}

The QuantityItemDescription calculated property formats the item name and quantity in a user-readable format. It also uses the ItemFactory.GetItemName method that we introduced in Lesson 4.4. We will use this property in our message formatting.

Then we need to update the Quest class.

using System;
using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Models
{
    public class Quest
    {
        public int Id { get; set; }

        public string Name { get; set; } = string.Empty;
        
        public string Description { get; set; } = string.Empty;

        public IList<ItemQuantity> ItemsToComplete { get; set; } = Array.Empty<ItemQuantity>();

        public int RewardExperiencePoints { get; set; }
        
        public int RewardGold { get; set; }
        
        public IList<ItemQuantity> RewardItems { get; set; } = Array.Empty<ItemQuantity>();

        public DisplayMessage ToDisplayMessage()
        {
            var messageLines = new List<string>
            {
                Description,
                "Items to complete the quest:"
            };

            foreach (ItemQuantity q in ItemsToComplete)
            {
                messageLines.Add(q.QuantityItemDescription);
            }

            messageLines.Add("Rewards for quest completion:");
            messageLines.Add($"{RewardExperiencePoints} experience points");
            messageLines.Add($"{RewardGold} gold");
            foreach (ItemQuantity itemQuantity in RewardItems)
            {
                messageLines.Add(itemQuantity.QuantityItemDescription);
            }

            return new DisplayMessage($"Quest Added - {Name}", messageLines);
        }
    }
}

In this class we define the ToDisplayMessage method (similar to Object.ToString) which formats the internal quest information into a DisplayMessage. The body of this function was actually extracted from the GameSession.GetQuestsAtLocation method. We used to create the display message there originally (in lines #227-246), but it makes more sense to encapsulate this functionality with the Quest class itself.

Next we make a similar update to the Recipe class.

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

namespace SimpleRPG.Game.Engine.Models
{
    public class Recipe
    {
        public Recipe(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public int Id { get; }

        public string Name { get; } = string.Empty;

        public IList<ItemQuantity> Ingredients { get; } = new List<ItemQuantity>();

        public IList<ItemQuantity> OutputItems { get; } = new List<ItemQuantity>();

        public void AddIngredient(int itemId, int quantity)
        {
            if (!Ingredients.Any(x => x.ItemId == itemId))
            {
                Ingredients.Add(new ItemQuantity { ItemId = itemId, Quantity = quantity });
            }
        }

        public void AddOutputItem(int itemId, int quantity)
        {
            if (!OutputItems.Any(x => x.ItemId == itemId))
            {
                OutputItems.Add(new ItemQuantity { ItemId = itemId, Quantity = quantity });
            }
        }

        public DisplayMessage ToDisplayMessage()
        {
            var messageLines = new List<string>
            {
                "Ingredients:"
            };

            foreach (ItemQuantity q in Ingredients)
            {
                messageLines.Add(q.QuantityItemDescription);
            }

            messageLines.Add("Creates:");
            foreach (ItemQuantity itemQuantity in OutputItems)
            {
                messageLines.Add(itemQuantity.QuantityItemDescription);
            }

            return new DisplayMessage($"Recipe Added - {Name}", messageLines);
        }
    }
}

You will notice the Recipe.ToDisplayMessage follows a similar structure to the Quest version of the method. It shows each ingrddient and then each output (with appropriate labels), using the ItemQuantity.QuantityItemDescription that we created earlier. Then it creates the DisplayMessage with that data and returns the newly created message.

ViewModel Changes

With the model class changes in place, we can now update the GameSession view model to use the model classes’ methods for display.

Let’s start with a simple update to the IGameSession interface to expose a method that adds a DisplayMessage to the game messages. We will use this method to handle click events on quests and recipes in the UI.

using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.ViewModels
{
    public interface IGameSession
    {
        Player CurrentPlayer { get; }

        Location CurrentLocation { get; }

        Monster? CurrentMonster { get; }

        bool HasMonster { get; }

        Trader? CurrentTrader { get; }

        MovementUnit Movement { get; }

        IList<DisplayMessage> Messages { get; }

        void OnLocationChanged(Location newLocation);

        void AttackCurrentMonster(GameItem? currentWeapon);

        void ConsumeCurrentItem(GameItem? item);

        void CraftItemUsing(Recipe recipe);

        void ProcessKeyPress(KeyProcessingEventArgs args);

        void AddDisplayMessage(DisplayMessage message);
    }
}

Then we make these two changes to the GameSession implementation:

using SimpleRPG.Game.Engine.Factories;
using SimpleRPG.Game.Engine.Models;
using SimpleRPG.Game.Engine.Services;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.ViewModels
{
    public class GameSession : IGameSession
    {
        private readonly World _currentWorld;
        private readonly IDiceService _diceService;
        private readonly int _maximumMessagesCount = 100;
        private readonly Dictionary<string, Action> _userInputActions = new Dictionary<string, Action>();

        public Player CurrentPlayer { get; private set; }

        public Location CurrentLocation { get; private set; }

        public Monster? CurrentMonster { get; private set; }

        public bool HasMonster => CurrentMonster != null;

        public Trader? CurrentTrader { get; private set; }

        public MovementUnit Movement { get; private set; }

        public IList<DisplayMessage> Messages { get; } = new List<DisplayMessage>();

        public GameSession(int maxMessageCount)
            : this()
        {
            _maximumMessagesCount = maxMessageCount;
        }

        public GameSession(IDiceService? diceService = null)
        {
            _diceService = diceService ?? DiceService.Instance;
            InitializeUserInputActions();

            CurrentPlayer = new Player
            {
                Name = "DarthPedro",
                CharacterClass = "Fighter",
                CurrentHitPoints = 10,
                MaximumHitPoints = 10,
                Gold = 1000,
                Level = 1
            };

            _currentWorld = WorldFactory.CreateWorld();

            Movement = new MovementUnit(_currentWorld);
            CurrentLocation = Movement.CurrentLocation;
            GetMonsterAtCurrentLocation();

            if (!CurrentPlayer.Inventory.Weapons.Any())
            {
                CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItem(1001));
            }

            CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItem(2001));
            CurrentPlayer.LearnRecipe(RecipeFactory.GetRecipeById(1));
            CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItem(3001));
            CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItem(3002));
            CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItem(3003));
        }

        public void OnLocationChanged(Location newLocation)
        {
            _ = newLocation ?? throw new ArgumentNullException(nameof(newLocation));

            CurrentLocation = newLocation;
            Movement.UpdateLocation(CurrentLocation);
            GetMonsterAtCurrentLocation();
            CompleteQuestsAtLocation();
            GetQuestsAtLocation();
            CurrentTrader = CurrentLocation.TraderHere;
        }

        public void AttackCurrentMonster(GameItem? currentWeapon)
        {
            if (CurrentMonster is null)
            {
                return;
            }

            if (currentWeapon is null)
            {
                AddDisplayMessage("Combat Warning", "You must select a weapon, to attack.");
                return;
            }

            // player acts monster with weapon
            CurrentPlayer.CurrentWeapon = currentWeapon;
            var message = CurrentPlayer.UseCurrentWeaponOn(CurrentMonster);
            AddDisplayMessage(message);

            // if monster is killed, collect rewards and loot
            if (CurrentMonster.IsDead)
            {
                OnCurrentMonsterKilled(CurrentMonster);

                // get another monster to fight
                GetMonsterAtCurrentLocation();
            }
            else
            {
                // if monster is still alive, let the monster attack
                message = CurrentMonster.UseCurrentWeaponOn(CurrentPlayer);
                AddDisplayMessage(message);

                // if player is killed, move them back to their home and heal.
                if (CurrentPlayer.IsDead)
                {
                    OnCurrentPlayerKilled(CurrentMonster);
                }
            }
        }

        public void ConsumeCurrentItem(GameItem? item)
        {
            if (item is null || item.Category != GameItem.ItemCategory.Consumable)
            {
                AddDisplayMessage("Item Warning", "You must select a consumable item to use.");
                return;
            }

            // player uses consumable item to heal themselves and item is removed from inventory.
            CurrentPlayer.CurrentConsumable = item;
            var message = CurrentPlayer.UseCurrentConsumable(CurrentPlayer);
            AddDisplayMessage(message);
        }

        public void CraftItemUsing(Recipe recipe)
        {
            _ = recipe ?? throw new ArgumentNullException(nameof(recipe));

            var lines = new List<string>();

            if (CurrentPlayer.Inventory.HasAllTheseItems(recipe.Ingredients))
            {
                CurrentPlayer.Inventory.RemoveItems(recipe.Ingredients);

                foreach (ItemQuantity itemQuantity in recipe.OutputItems)
                {
                    for (int i = 0; i < itemQuantity.Quantity; i++)
                    {
                        GameItem outputItem = ItemFactory.CreateGameItem(itemQuantity.ItemId);
                        CurrentPlayer.Inventory.AddItem(outputItem);
                        lines.Add($"You craft 1 {outputItem.Name}");
                    }
                }

                AddDisplayMessage("Item Creation", lines);
            }
            else
            {
                lines.Add("You do not have the required ingredients:");
                foreach (ItemQuantity itemQuantity in recipe.Ingredients)
                {
                    lines.Add($"  {itemQuantity.Quantity} {ItemFactory.GetItemName(itemQuantity.ItemId)}");
                }

                AddDisplayMessage("Item Creation", lines);
            }
        }

        public void ProcessKeyPress(KeyProcessingEventArgs args)
        {
            _ = args ?? throw new ArgumentNullException(nameof(args));

            var key = args.Key.ToUpper();
            if (_userInputActions.ContainsKey(key))
            {
                _userInputActions[key].Invoke();
            }
        }

        private void OnCurrentPlayerKilled(Monster currentMonster)
        {
            AddDisplayMessage("Player Defeated", $"The {currentMonster.Name} killed you.");

            CurrentPlayer.CompletelyHeal();  // Completely heal the player
            this.OnLocationChanged(_currentWorld.LocationAt(0, -1));  // Return to Player's home
        }

        private void OnCurrentMonsterKilled(Monster currentMonster)
        {
            var messageLines = new List<string>();
            messageLines.Add($"You defeated the {currentMonster.Name}!");

            CurrentPlayer.AddExperience(currentMonster.RewardExperiencePoints);
            messageLines.Add($"You receive {currentMonster.RewardExperiencePoints} experience points.");

            CurrentPlayer.ReceiveGold(currentMonster.Gold);
            messageLines.Add($"You receive {currentMonster.Gold} gold.");

            foreach (GameItem item in currentMonster.Inventory.Items)
            {
                CurrentPlayer.Inventory.AddItem(item);
                messageLines.Add($"You received {item.Name}.");
            }

            AddDisplayMessage("Monster Defeated", messageLines);
        }

        private void GetMonsterAtCurrentLocation()
        {
            CurrentMonster = CurrentLocation.HasMonster() ? CurrentLocation.GetMonster() : null;

            if (CurrentMonster != null)
            {
                AddDisplayMessage("Monster Encountered:", $"You see a {CurrentMonster.Name} here!");
            }
        }

        private void GetQuestsAtLocation()
        {
            foreach (Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                if (!CurrentPlayer.Quests.Any(q => q.PlayerQuest.Id == quest.Id))
                {
                    CurrentPlayer.Quests.Add(new QuestStatus(quest));
                    AddDisplayMessage(quest.ToDisplayMessage());
                }
            }
        }

        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))
                    {
                        // Remove the quest completion items from the player's inventory
                        CurrentPlayer.Inventory.RemoveItems(quest.ItemsToComplete);

                        // give the player the quest rewards
                        var messageLines = new List<string>();
                        CurrentPlayer.AddExperience(quest.RewardExperiencePoints);
                        messageLines.Add($"You receive {quest.RewardExperiencePoints} experience points");

                        CurrentPlayer.ReceiveGold(quest.RewardGold);
                        messageLines.Add($"You receive {quest.RewardGold} gold");

                        foreach (ItemQuantity itemQuantity in quest.RewardItems)
                        {
                            GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemId);

                            CurrentPlayer.Inventory.AddItem(rewardItem);
                            messageLines.Add($"You receive a {rewardItem.Name}");
                        }

                        AddDisplayMessage($"Quest Completed - {quest.Name}", messageLines);

                        // mark the quest as completed
                        questToComplete.IsCompleted = true;
                    }
                }
            }
        }

        private void InitializeUserInputActions()
        {
            _userInputActions.Add("W", () => Movement.MoveNorth());
            _userInputActions.Add("A", () => Movement.MoveWest());
            _userInputActions.Add("S", () => Movement.MoveSouth());
            _userInputActions.Add("D", () => Movement.MoveEast());
            _userInputActions.Add("ARROWUP", () => Movement.MoveNorth());
            _userInputActions.Add("ARROWLEFT", () => Movement.MoveWest());
            _userInputActions.Add("ARROWDOWN", () => Movement.MoveSouth());
            _userInputActions.Add("ARROWRIGHT", () => Movement.MoveEast());
        }

        private void AddDisplayMessage(string title, string message) =>
            AddDisplayMessage(title, new List<string> { message });

        private void AddDisplayMessage(string title, IList<string> messages)
        {
            AddDisplayMessage(new DisplayMessage(title, messages));
        }

        public void AddDisplayMessage(DisplayMessage message)
        {
            this.Messages.Insert(0, message);

            if (Messages.Count > _maximumMessagesCount)
            {
                Messages.Remove(Messages.Last());
            }
        }
    }
}

First, we removed the quest display code (which we extracted out earlier in this lesson), and instead called Quest.ToDisplayMessage, and then added it to the Messages list. Then, we just make the AddDisplayMessage override method public, so that it matches the method defined in the IGameSession. We already had this code, it was just not useable outside of this class.

Handling Clicks to Show Details

With the backing code wrapped up, we need to enable clicking on quests and recipes in their table. Let’s update the PlayersTab component to enable this new behavior.

<Tabs SelectedTab="@_selectedTab" Pills="true" SelectedTabChanged="OnSelectedTabChanged">
    <Items>
        <Tab Name="inventory">Inventory</Tab>
        <Tab Name="quests">Quests</Tab>
        <Tab Name="recipes">Recipes</Tab>
    </Items>
    <Content>
        <TabPanel Name="inventory">
            <div class="table-wrapper-scroll-y my-custom-scrollbar">
                <Table Bordered="true" Hoverable="true" Narrow="true" Striped="true"
                       Style="background-color: white">
                    <TableHeader>
                        <TableRowCell>Name</TableRowCell>
                        <TableRowCell>Qty</TableRowCell>
                        <TableRowCell>Price</TableRowCell>
                    </TableHeader>
                    <TableBody>
                        @foreach (var groupedItem in Player.Inventory.GroupedItems)
                        {
                        <TableRow>
                            <TableRowCell>@groupedItem.Item.Name</TableRowCell>
                            <TableRowCell>@groupedItem.Quantity</TableRowCell>
                            <TableRowCell>@groupedItem.Item.Price</TableRowCell>
                        </TableRow>
                        }
                    </TableBody>
                </Table>
            </div>
        </TabPanel>
        <TabPanel Name="quests">
            <div class="table-wrapper-scroll-y my-custom-scrollbar">
                <Table Bordered="true" Hoverable="true" Narrow="true" Striped="true"
                       Style="background-color: white">
                    <TableHeader>
                        <TableRowCell>Name</TableRowCell>
                        <TableRowCell>Done?</TableRowCell>
                    </TableHeader>
                    <TableBody>
                        @foreach (var quest in Player.Quests)
                        {
                        <TableRow>
                            <TableRowCell id="quest-name-cell" Style="cursor:pointer" Clicked="() => OnQuestClicked(quest.PlayerQuest)">
                                @quest.PlayerQuest.Name
                            </TableRowCell>
                            <TableRowCell>@(quest.IsCompleted ? "Yes" : "No")</TableRowCell>
                        </TableRow>
                        }
                    </TableBody>
                </Table>
            </div>
        </TabPanel>
        <TabPanel Name="recipes">
            <div class="table-wrapper-scroll-y my-custom-scrollbar">
                <Table Bordered="true" Hoverable="true" Narrow="true" Striped="true"
                       Style="background-color: white">
                    <TableHeader>
                        <TableRowCell>Name</TableRowCell>
                        <TableRowCell></TableRowCell>
                    </TableHeader>
                    <TableBody>
                        @foreach (var recipe in Player.Recipes)
                        {
                        <TableRow>
                            <TableRowCell id="recipe-name-cell" Style="cursor: pointer" Clicked="() => OnRecipeClicked(recipe)">
                                @recipe.Name
                            </TableRowCell>
                            <TableRowCell>
                                <Button id="craft-item-btn" Size="ButtonSize.Small" Color="Color.Secondary"
                                        Outline="true" Clicked="() => CraftItemClicked.InvokeAsync(recipe)">
                                    Craft
                                </Button>
                            </TableRowCell>
                        </TableRow>
                        }
                    </TableBody>
                </Table>
            </div>
        </TabPanel>
    </Content>
</Tabs>

@code {
    private string _selectedTab = "inventory";

    [Parameter]
    public Player Player { get; set; } = new Player();

    [Parameter]
    public EventCallback<Recipe> CraftItemClicked { get; set; }

    [Parameter]
    public EventCallback<DisplayMessage> DisplayMessageCreated { get; set; }

    public void OnSelectedTabChanged(string newTab)
    {
        _selectedTab = newTab;
    }

    private void OnQuestClicked(Quest quest) =>
        DisplayMessageCreated.InvokeAsync(quest.ToDisplayMessage());

    private void OnRecipeClicked(Recipe recipe) =>
        DisplayMessageCreated.InvokeAsync(recipe.ToDisplayMessage());
}
  1. The changes are duplicated for both quest and recipe tables. First, we make the name cell clickable in the markup (lines #42-44 and #64-66). We change the style of the cell to set the cursor to the pointer when the mouse is over it. Then, we handle the Clicked event by forwarding the message to the appropriate event handler (OnQuestClicked or OnRecipeClicked).
  2. Define an EventCallback (lines #91-92) for when a new DisplayMessage is being invoked from this component… in response to the item clicks.
  3. Two methods for handling the quest and recipe events (lines #99-103).
    • OnQuestClicked knows how to convert a Quest into its corresponding DisplayMessage.
    • OnRecipeClicked knows how to convert a Recipe into its DisplayMessage.
    • Both invoke the DisplayMessageCreated event.

As we can see, these are pretty simple changes to get the click behavior, and for the component to trigger its event. Now, we need to update the GameScreen page to handle the EventCallback properly to show the message in our UI.

@page "/"
@inject IJSRuntime jsRuntime
@inject IGameSession ViewModel

<div @onkeydown="@KeyDown" tabindex="0" @ref="pageRoot">
    <Row Margin="Margin.Is0" Style="height: 5vh; min-height: 32px">
        <Column ColumnSize="ColumnSize.Is12" Style="background-color: lightgrey">
            <Heading Size="HeadingSize.Is3">Simple RPG</Heading>
        </Column>
    </Row>
    <Row Margin="Margin.Is0" Style="height: 60vh">
        <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12" Style="background-color: aquamarine">
            <PlayerComponent Player="@ViewModel.CurrentPlayer" />
        </Column>
        <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12" Style="background-color: beige">
            <Row Margin="Margin.Is2.OnY">
                <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12">
                    <DisplayMessageListView Messages="@ViewModel.Messages" />
                </Column>
                <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12">
                    <LocationComponent Location="@ViewModel.CurrentLocation" />
                    <MonsterComponent Monster="@ViewModel.CurrentMonster" />
                    <TraderComponent Trader="@ViewModel.CurrentTrader" Player="@ViewModel.CurrentPlayer"
                                     InventoryChanged="@StateHasChanged" />
                </Column>
            </Row>
        </Column>
    </Row>
    <Row Margin="Margin.Is0" Style="height: 33vh">
        <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12" Padding="Padding.Is2.OnY"
                Style="background-color: burlywood">
            <PlayerTabs Player="@ViewModel.CurrentPlayer" CraftItemClicked="@ViewModel.CraftItemUsing"
                        DisplayMessageCreated="@ViewModel.AddDisplayMessage" />
        </Column>
        <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12" Style="background-color: lavender">
            <Row Margin="Margin.Is2.OnY">
                <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12">
                    <CombatComponent WeaponList="@ViewModel.CurrentPlayer.Inventory.Weapons"
                                     AttackClicked="@ViewModel.AttackCurrentMonster"
                                     LocationHasMonster="@ViewModel.HasMonster"
                                     ConsumableList="@ViewModel.CurrentPlayer.Inventory.Consumables"
                                     ConsumeClicked="@ViewModel.ConsumeCurrentItem" />
                </Column>
                <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12">
                    <MovementComponent Movement="@ViewModel.Movement"
                                       LocationChanged="@ViewModel.OnLocationChanged" />
                </Column>
            </Row>
        </Column>
    </Row>
</div>

@code {
    protected ElementReference pageRoot;  // set the @ref for attribute

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await jsRuntime.InvokeVoidAsync("SetFocusToElement", pageRoot);
        }
    }

    [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    protected void KeyDown(KeyboardEventArgs args) =>
        ViewModel.ProcessKeyPress(args.ToKeyProcessingEventArgs());
}

As we can see we update the PlayerTabs component (lines #32-33) to set its DisplayMessageCreated event to the ViewModel.AddDisplayMessage handler that we defined earlier. This code path now adds the provided DisplayMessage to our GameSession.Messages list and updates the display.

We’ve completed all of our changes again, so let’s build and run the game. If we click the Granola bar recipe right away, we will get the following:

Fig 1 – Displaying recipe ingredients

If we move around the game world and fight, we can go back and see our quest as well by clicking on it.

Fig 2 – Displaying quest definition

We have made these aspects of our game more discoverable, so that players can remember how to complete a quest or ingredients for a recipe. It’s important to make features discoverable because it makes the games more fun for our users.

In the next lessons, we are going to look at expanding the capabilities of our combat system.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s