My Unnecessarily Complicated Text Adventure for 2D Course

Edit: Changed the topic title to better reflect that the complication was my own doing.

So, since I spent so much time on this part, I figure I should probably show something off, though it’s probably not all that interesting to professionals.

Basically, the text adventure system was just too simple for me. Heck, the final project in my first C++ class was to make a text adventure there, and that one had items and custom keys inputs.

Fixing the key inputs was relatively annoying at first. I began by giving rooms custom exit keys to go to each room, but I had issues by requiring chars and keycodes being weird about that. When I went back to redo this, I instead decided to give each room itself a key field and text line to be appended when allowed to go to that room (which is still handled as an array of rooms):

    // Room.cs
    //..
    // The keyboard key to press to enter this room
    [SerializeField] 
    [Tooltip("The key to press to enter this room.")] 
    string entryKey = string.Empty; // NOTE: Read function returns empty string if this is "escape"
    // The string that informs the player of the key to this room
    [SerializeField] 
    [Tooltip("The text shown on a room that directs to this one. Unless it's secret, it should include some clue to the entry key.")] 
    string entryString = "A Dead End";
    //..

(Also learned how to add Tooltips in the Unity inspector, which is particularly useful for when I make new scripts)

The entry keys are full strings instead of chars. It not only allows simple keys to be used just as easily, but also allows the option to use keys other than basic alphanum keys (including the difference in the number and number pad keys, which would help for n/s/w/e, but not in this particular project by design).

Also note string.Empty used above. string.Empty is the same as "", but Visual Studio often suggests introducing a const for “” and I found it annoying. You can choose which option is best for you, but it’s preferred to always use one or the other, not switch between the two.

With the lines and entry strings now like that, I had to change the body text to append each connected rooms’ entry strings at the end of the text itself.

    // AdventureGame.cs
    //..
    void start() {
        //..
        // This solution actually does not work in clearing empty lines,
        //     but it's not that important in the start screen.
        var completeBody = new List<string> {
            this.currentRoom.GetBodyString(),
            FindItems(this.currentRoom.GetItems()), // ignore for now
            FindCraftingOptions(this.currentRoom.GetCraftingOptions()), // ditto
            GetDirections(this.currentRoom.GetNextRooms())
        };
        _ = completeBody.RemoveAll(string.IsNullOrWhiteSpace);

        this.bodyText.text = string.Join("\n", completeBody);
    }
    //..
    void update() {
        //..
        var completeBody = new List<string> { this.currentRoom.GetBodyString() };
        //..
        // This is the REAL solution to cutting empty lines.
        string _rooms = GetDirections(this.currentRoom.GetNextRooms());
        if (!string.IsNullOrWhiteSpace(_rooms)) {
            completeBody.Add(_rooms);
        }

        this.bodyText.text = string.Join("\n", completeBody);
    }
    //..
    private string GetDirections(Room[] rooms) {
        var outputString = string.Empty;
        List<string> results = rooms.Select(room => room.GetEntryString()).ToList();
        _ = results.RemoveAll(string.IsNullOrWhiteSpace);
        outputString = string.Join("\n", results);
        return outputString;
    }
    //..

(Also learned quite a bit about C# Lists here, which in itself was its own trip. Getting them to drop empty lines was a particular hassle, somehow.)

I also need to get the specific key from each room that allowed entry into it.

        var toRoom = this.currentRoom;
        var nextRooms = toRoom.GetNextRooms();
        foreach (var nextRoom in nextRooms) {
            if (Input.GetKeyDown(nextRoom.GetEntryKey())) {
                toRoom = nextRoom;
            }
        }
        this.currentRoom = toRoom;  // Yes, I'm aware of the redundancy here

Added bonus: since this is used for all rooms, you’re no longer limited to 3. You can have as many rooms connected as you want. Just keep the length of the body text field in mind, or add a scrollbar to it (which I didn’t).

The result is that my body text field for the opening screen looks like this:

Welcome to "Awaken in a Forest".

Gameplay is simple: press a key to perform an action associated with it. Most of the time the keys are displayed in parentheses in the story text, but there could be a few secrets as well.

Simply get to the end of the story. You may press Esc at any time to quit. Good luck!
(B)egin the game.

Where “(B)egin the game.” is set as the entry string to the second room, the starting room does not have that string in its text (“B” is in parentheses as the hint you need to push the “b” key; typing “1” here does nothing anymore).


My next step was to include items. Getting an item functions exactly the same as moving to a room, that is it has its own input key and string stating you can grab it. Items are also kept in rooms the same way the directions to another room do, just in its own array.

Checking for input and printing the strings in the room is done through the exact same methods as rooms, excepting the fact that there is no string, it also skips the key check. In this engine, there are no completely hidden items (though it’s simple to allow it).

        var items = this.currentRoom.GetItems();
        foreach (var item in items) {
            // If the item is hidden because of constraints, the command should not work
            if (item.Display(this.Inventory) == string.Empty) continue; // Check next item
            if (Input.GetKeyDown(item.GetItemKey())) {
                item.ObtainThisItem(this.Inventory);
            }
        }

Items that the player holds are kept in the AdventureGame class itself. They’re stored as a Dictionary that functions as Inventory[ItemName] = Amount:

    Dictionary<string, int> Inventory = new Dictionary<string,int>();

This dictionary is passed into all item methods that need to know what items the character has and how many of them.

Naturally, items themselves are scripts just like rooms. Their functionality though is a little more complicated (okay, a lot more).

public class GameItem : ScriptableObject
{
    // If this key is the same as another item or room linked to this room, those will also function
    [SerializeField]
    [Tooltip("The key to press in order to gain this item.")]
    private string itemKey = string.Empty;
    // The item will not function if this is null or a nullstring
    [SerializeField] 
    [Tooltip("The name of the item.")]
    private string itemName = string.Empty;
    // Not implemented. It's a string because I planned to use emojis as images.
    // Because text game.
    [SerializeField] 
    [Tooltip("Character that appears in item field.")]
    private string itemImage = string.Empty;
    // The string to print out if you want to gain this item
    [SerializeField] 
    [Tooltip("String to display to show this item can be gained. Leave blank for a secret item.")]
    private string gainString = string.Empty;

Now the differences.

    [SerializeReference] 
    [Tooltip("The item needed to be able to gain this item. Leave blank if there is no requirement.")]
    private GameItem itemRequired = null;

And there’s already a lot to take in. [SerializeReference] functions the same as [SerializeObject] except it’s used to, naturally, reference other objects, Unity assets, and the like; things that can’t be simple strings, numbers and the like.

Technically you can make this a string that has the item name instead of a reference. However, that leaves you open to misspelling the items and cause all sorts of problems. Also, you can rename the item object and change its data without having to worry about updating other things.

Another thing is that Unity doesn’t like it (for now) if you make this an object and have it be null (empty), which I wanted to in this case. It’s also how the arrays of rooms worked (and still work) in the original assignment, as the array itself is an object but each room in the array is actually a reference, but that’s for another topic. In short, you can have [Object] Room and [Object] Rooms[], but you can’t have [Object] Room = null and can have [Reference] Room = null.

As for what this this field is actually FOR, it’s to require an item in your inventory in order to gain this item. For instance, you can’t gain an ore if you don’t a pickaxe. And as stated, if no item is entered here it can be gained freely, like picking up a small rock.

    // If an item can be "bought", this is the name of the item that buys it
    // If this is empty, the player instead gains one of the item on the keypress
    [SerializeReference]
    [Tooltip("The item needed to buy this item. If blank, player instead gains one of the item on keypress.")]
    private GameItem buyWith = null;
    // How many of the tradeItem is needed to "buy" this item
    // This amount is deducted from the player's inventory; non-positive amounts will have no effect
    // If the item count reaches zero, the item is removed from the inventory list completely
    [SerializeField]
    [Min(0)]
    [Tooltip("How much of the item above is needed to buy this item. This amount is removed from the player on success.")]
    private int itemCost = 0;

buyWith here works the same as itemRequired above but has a different purpose. The given item here is treated as currency to buy the item itself, obviously at the given cost. And as stated, the method used to gain an item is treated differently if this field is blank or not.

Note the [Min(0)] attribute here. This prevents the programmer from making the cost of the object less than zero. Attempting to do so in the Unity inspector automatically makes the number 0.

    [SerializeField]
    [Tooltip("Set if the gainString should not appear if the player does not have itemRequired.")]
    private bool hideRequirement = false;
    // Set if you want to hide this item unless its buy conditions are met
    [SerializeField]
    [Tooltip("Set if the gainString should not appear if the player's buyWith item count is less than the itemCost.")]
    private bool hideExpensive = false;

These are just booleans to adjust when the string to gain this item is in the body text. The first is for the required item, so for instance if you dont want the string “(M)ine ore with pickaxe” to appear if you don’t have a pickaxe (assuming itemRequired is not empty). The second one instead uses buyWith and its amount, for instance if you don’t want the string “Buy §ickaxe with 40 coins” to appear unless they have 40 or more coins (again, assuming buyWith is not empty and that itemCost is set to “40”).

So ultimately an object made from this monster will look like this in Unity:
Image1
The check for Hide Requirement does nothing here, but the check for Hide Expensive makes it so that “Buy a (S)eed with an acorn” does not appear unless the player has at least 1 acorn.

And now the methods…

    public bool CanBuyItem(int amount) {
        return (amount >= this.itemCost);
    }

    public string GetItemKey() { return this.itemKey != "Escape" ? this.itemKey : string.Empty; }
    public string GetItemName() { return this.itemName; }
    public string GetItemImage() { return this.itemImage; }

The only thing notable here is that I’m using C#'s “?:” operator in GetItemKey(). For those of you still learning, this functions almost exactly like:

Also from this point on there are far fewer comments. It took a day of searching for standards in commenting for me to realize I should probably get back to doing stuff instead of treating this as professional code.

    if (this.itemKey != "Escape") {
        return this.itemKey;
    }
    else {
        return string.Empty;
    }

Except that ?: “returns” the value. It’s particularly useful for variable assignment and return statements, but not for calling unrelated methods.

    public string Display(Dictionary<string,int> itemList) {

        if (itemList == null) return string.Empty;

        // Checking inventory
        bool hasRequirement = !this.hideRequirement
            || (this.itemRequired == null)
            || itemList.ContainsKey(this.itemRequired.GetItemName());

This method is used for determining how the item string should be displayed. Basically, if there’s any reason to not add the item’s string to the body text, this should return an empty string instead.

The first part is just checking if the inventory was given in the function call. Bad things can happen without this check. Also note that it’s an if statement on a single line without {} braces. This is actually poor practice in programming. However, I personally find this to look cleaner if there’s no need for an else and the statement itself is simple enough, such as: a single assignment, one method call, or a return statement. Regardless, it functions the same with or without brackets.

The second part is just one long assignment for checking some of the restrictions in displaying this item’s string. In short, this is false if Hide Requirement is unchecked, there is no item in Item Required, and the Item Required is not in the player’s inventory. If any of those cases are true, the requirement to show the item string has been met.

        int moneyCount = 0;
        bool hasMoneyItem = (this.buyWith == null) || itemList.TryGetValue(this.buyWith.GetItemName(), out moneyCount);

This does a couple things. First it checks to see if restriction is even needed. Second, it determines if the currency to buy this item is in the player’s inventory, and if so, how much.

The TryGetValue method is a method for C# Dictionaries. The method itself actually returns true or false, depending on if the first field exists in the player’s inventory (or other possibilities preventing it from checking at all). And if the first field exists, it saves the value associated with it in the variable given after out. Assuming this were an array, this would be the same as moneyCount = itemList[this.buyWith.getItemName()] with the added check to see if the item even is in ItemList.

        // Output string, given set conditions
        if (hasRequirement && hasMoneyItem && CanBuyItem(moneyCount)) return this.gainString;
        if (hasRequirement && !this.hideExpensive) return this.gainString;

        return string.Empty;
    }

And this here is the final result of what the function did. && here works kind of the opposite as || did before: all of these conditions must be true for the whole thing to be true, not just a single one.

Also note that there is no else before the return. It’s completely unnecessary.

    public void ObtainThisItem(Dictionary<string, int> itemList) {

        if ((this.itemRequired != null) && !itemList.ContainsKey(this.itemRequired.GetItemName())) {
            return; 
        }
        else if (this.buyWith == null) {
            GainThisItem(itemList);
        }
        else {
            TryToBuyThisItem(itemList);
        }

    }

This is the function called in AdventureGame.update() when the appropriate key is pressed. First it checks for a requirement, and it does nothing if said requirement is not met. Then it does one of two things depending on whether this item is bought or not.

    public void GainThisItem(Dictionary<string, int> itemList) {

        if (itemList == null) return;

        if (itemList.ContainsKey(this.itemName)){
            itemList[this.itemName] += 1; }
        else {
            itemList[this.itemName] = 1;
        }

    }

Here is what happens when an item is simply gained. It’s also an excellent example of why I like C# Dictionaries. The only obvious method call is a check to see if the item is already in the player’s inventory, and it looks so clean compared to all the complex stuff above.

    private void TryToBuyThisItem(Dictionary<string,int> itemList) {

        if (itemList == null) return;

        if (!itemList.ContainsKey(this.buyWith.GetItemName())) return;
        if (!CanBuyItem(itemList[this.buyWith.GetItemName()])) return;

        if (itemList[this.buyWith.GetItemName()] > this.itemCost) {
            itemList[this.buyWith.GetItemName()] -= this.itemCost;
        }
        else {
            // Item count should always be positive, but this still removes negative amounts just in case
            itemList.Remove(this.buyWith.GetItemName());
        }

        GainThisItem(itemList);

    }

Not nearly as clean, but pretty simple. First checks are for if the inventory was given and to see if the player can afford this item. Then it removes the amount from the player’s inventory, and completely removes the reference to the item if there are no more left (shame there isn’t an operator for removing dictionary pairs like adding them). Once all that’s all done, it simply calls the previous function because at this point there’s no difference.

And that’s it for items. Though at this point I realized I needed something to turn multiple items into one (like building a house with some amount of wood, stone, etc.). So now I had to make a new CRAFT script.

Fortunately, it’s a LOT more simple. It doesn’t have cost checks, only existence in inventory. Just one of each given item (in an array) is removed. Crafting with multiples could simply be handed by “crafting” individual parts by buying them. So for a sword, you would “buy” one blade with, for example, 5 iron, and then a handle for 2 wood. Then this script would be used to gain one sword with one blade and one handle. It sounds complicated, but it really only needed one foreach loop to check everything:

    private bool ItemsExist(Dictionary<string,int> itemList) {

        foreach(var item in this.recipe) {
            if (!itemList.ContainsKey(item.GetItemName())) return false;
        }

        return true;
    }

Where recipe is the array that has the needed items. With that, all that was left was to use something like the GameItem’s buy function, but for each item.

    private void CraftThisItem(Dictionary<string, int> itemList) {

        if (itemList.ContainsKey(this.craftThisItem.GetItemName())) {
            itemList[this.craftThisItem.GetItemName()] += 1;
        }
        else {
            itemList[this.craftThisItem.GetItemName()] = 1;
        }

        foreach(var item in this.recipe) {
            if(itemList[item.GetItemName()] <= 1) {
                itemList.Remove(item.GetItemName());
            }
            else {
                itemList[item.GetItemName()] -= 1;
            }
        }
    }

(Unrelated note, I should have just called the crafted item’s GainThisItem function. However I’d have to make it public, or have this class be a child of GameItem)

Where craftThisItem was just the item this should craft (as a SerialReference):
Image2

There. Done. I need to stop now; I’m not a teacher. Once this was all done I figured I needed to move on or I’d accomplish nothing anyways.

Still, here’s the complete files for anyone who’s interested.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class AdventureGame : MonoBehaviour {

    // Attributes assigned in Unity
    [SerializeField] Text titleText;
    [SerializeField] Text bodyText;
    [SerializeField] Room startingRoom;
    
    // The room the player is currently viewing
    Room currentRoom;
    Dictionary<string, int> Inventory = new Dictionary<string,int>();

    // Start is called before the first frame update
    void Start() {

        // Basic initialization
        this.Inventory["EmptyItem"] = 1;
        this.currentRoom = startingRoom;
        this.titleText.text = this.currentRoom.GetTitleString();

        // Body text initialization
        // The displayed text of a room is the body text followed by other important strings
        // Typically the important strings are links to other rooms and actions to perform in the current one
        var completeBody = new List<string> {
            this.currentRoom.GetBodyString(),
            FindItems(this.currentRoom.GetItems()),
            FindCraftingOptions(this.currentRoom.GetCraftingOptions()),
            GetDirections(this.currentRoom.GetNextRooms())
        };
        _ = completeBody.RemoveAll(string.IsNullOrWhiteSpace);

        this.bodyText.text = string.Join("\n", completeBody);
    }

    // Update is called once per frame
    void Update() {

        ManageRoom();

    }

    private void ManageRoom() {

        // First check: end game on Escape press
        if (Input.GetKeyDown(KeyCode.Escape)) Application.Quit();

        // Then check items
        // It is and should be possible to gain and move in one keypress
        var items = this.currentRoom.GetItems();
        foreach (var item in items) {
            // If the item is hidden because of constraints, the command should not work
            if (item.Display(this.Inventory) == string.Empty) continue;
            if (Input.GetKeyDown(item.GetItemKey())) {
                item.ObtainThisItem(this.Inventory);
            }
        }

        // Now check your crafting options
        var crafts = this.currentRoom.GetCraftingOptions();
        foreach (var craft in crafts) {
            // If the item is hidden because of constraints, the command should not work
            if (craft.GetCraftString(this.Inventory) == string.Empty) continue;
            if (Input.GetKeyDown(craft.GetCraftKey())) {
                craft.TryToCraft(this.Inventory);
            }
        }

        // Check movement
        var toRoom = this.currentRoom;
        var nextRooms = toRoom.GetNextRooms();
        foreach (var nextRoom in nextRooms) {
            if (Input.GetKeyDown(nextRoom.GetEntryKey())) {
                toRoom = nextRoom;
            }
        }
        this.currentRoom = toRoom;

        this.titleText.text = this.currentRoom.GetTitleString();

        var completeBody = new List<string> { this.currentRoom.GetBodyString() };

        string _items = FindItems(this.currentRoom.GetItems());
        if (!string.IsNullOrWhiteSpace(_items)) {
            completeBody.Add(_items);
        }

        string _crafts = FindCraftingOptions(this.currentRoom.GetCraftingOptions());
        if (!string.IsNullOrWhiteSpace(_crafts)) {
            completeBody.Add(_crafts);
        }

        string _rooms = GetDirections(this.currentRoom.GetNextRooms());
        if (!string.IsNullOrWhiteSpace(_rooms)) {
            completeBody.Add(_rooms);
        }

        this.bodyText.text = string.Join("\n", completeBody);

    }

    private string FindItems(GameItem[] items) {
        string outputString = string.Empty;
        List<string> results = items.Select(item => item.Display(this.Inventory)).ToList();
        _ = results.RemoveAll(string.IsNullOrWhiteSpace);
        outputString = string.Join("\n", results);
        return outputString;
    }

    private string FindCraftingOptions(Craft[] options) {
        string outputString = string.Empty;
        List<string> results = options.Select(option => option.GetCraftString(this.Inventory)).ToList();
        _ = results.RemoveAll(string.IsNullOrWhiteSpace);
        outputString = string.Join("\n", results);
        return outputString;
    }

    private string GetDirections(Room[] rooms) {
        var outputString = string.Empty;
        List<string> results = rooms.Select(room => room.GetEntryString()).ToList();
        _ = results.RemoveAll(string.IsNullOrWhiteSpace);
        outputString = string.Join("\n", results);
        return outputString;
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Class Room
// The contents and actions that can be performed in each room
[CreateAssetMenu(menuName = "Room")]
public class Room : ScriptableObject
{
    // Enrty fields for script data
    
    // The keyboard key to press to enter this room
    [SerializeField] 
    [Tooltip("The key to press to enter this room.")] 
    string entryKey = string.Empty;
    // The string that informs the player of the key to this room
    [SerializeField] 
    [Tooltip("The text shown on a room that directs to this one. Unless it's secret, it should include some clue to the entry key.")] 
    string entryString = "A Dead End";
    // The text that appears in the top field of the game
    [SerializeField] 
    [Tooltip("The text shown in the title field of this room.")] 
    string titleString = "Title";
    // The text that appears in the bottom field, before any actions or connected rooms' entry strings
    [TextArea(10, 14)] [SerializeField] 
    [Tooltip("The text shown in the body field of this room.")] 
    string bodyString = "Placeholder";
    // The list of rooms that connect to this one
    [SerializeField] 
    [Tooltip("The rooms you wish to allow access to from this room.")] 
    Room[] nextRooms = null;
    // The items in this room
    [SerializeField]
    [Tooltip("The items in this room.")]
    GameItem[] items = null;
    [SerializeField]
    [Tooltip("The crafting options in this room.")]
    Craft[] craftingOptions = null;

    // Get methods

    public string GetEntryKey() { return this.entryKey != "Escape" ? this.entryKey : string.Empty; }
    public string GetEntryString() { return this.entryString; }
    public string GetTitleString() { return this.titleString; }
    public string GetBodyString() { return this.bodyString; }
    public Room[] GetNextRooms() { return this.nextRooms; }
    public GameItem[] GetItems() { return this.items; }
    public Craft[] GetCraftingOptions() { return this.craftingOptions; }

}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Class Item
// Basic information about 'items' in the game
// The actual amount the player has of each item is tracked in AdventureGame
// The item count can never be negative
// If the item count ever equals zero, the record of the item is removed until added again
[CreateAssetMenu(menuName = "Game Item")]
public class GameItem : ScriptableObject
{

    // The keyboard key to press in order to gain this item
    // If this key is the same as another item or room linked to this room, those will also function
    [SerializeField]
    [Tooltip("The key to press in order to gain this item.")]
    private string itemKey = string.Empty;
    // The name of the item
    // The item will not function if this is null or a nullstring
    [SerializeField] 
    [Tooltip("The name of the item.")]
    private string itemName = string.Empty;
    // The image of the item
    // Currently just an emoji or single character
    // Used to keep track of item counts in item field
    [SerializeField] 
    [Tooltip("Character that appears in item field.")]
    private string itemImage = string.Empty;
    // The string to print out if you want to gain this item
    [SerializeField] 
    [Tooltip("String to display to show this item can be gained. Leave blank for a secret item.")]
    private string gainString = string.Empty;
    // The name of the item the player needs in order to gain this item
    // If this is set as an empty string, no item is required
    [SerializeReference] 
    [Tooltip("The item needed to be able to gain this item. Leave blank if there is no requirement.")]
    private GameItem itemRequired = null;
    // If an item can be "bought", this is the name of the item that buys it
    // If this is empty, the player instead gains one of the item on the keypress
    [SerializeReference]
    [Tooltip("The item needed to buy this item. If blank, player instead gains one of the item on keypress.")]
    private GameItem buyWith = null;
    // How many of the tradeItem is needed to "buy" this item
    // This amount is deducted from the player's inventory; non-positive amounts will have no effect
    // If the item count reaches zero, the item is removed from the inventory list completely
    [SerializeField]
    [Min(0)]
    [Tooltip("How much of the item above is needed to buy this item. This amount is removed from the player on success.")]
    private int itemCost = 0;
    // Set if you want to hide the item string until conditions are met
    [SerializeField]
    [Tooltip("Set if the gainString should not appear if the player does not have itemRequired.")]
    private bool hideRequirement = false;
    // Set if you want to hide this item unless its buy conditions are met
    [SerializeField]
    [Tooltip("Set if the gainString should not appear if the player's buyWith item count is less than the itemCost.")]
    private bool hideExpensive = false;

    // Methods

    // Attempt to buy this item with the tradeItem
    // Returns true if the amount given exceeds this item's cost
    public bool CanBuyItem(int amount) {
        return (amount >= this.itemCost);
    }

    public string GetItemKey() { return this.itemKey != "Escape" ? this.itemKey : string.Empty; }
    public string GetItemName() { return this.itemName; }
    public string GetItemImage() { return this.itemImage; }

    public string Display(Dictionary<string,int> itemList) {

        if (itemList == null) return string.Empty;

        // Checking inventory
        bool hasRequirement = !this.hideRequirement
            || (this.itemRequired == null)
            || itemList.ContainsKey(this.itemRequired.GetItemName());
        int moneyCount = 0;
        bool hasMoneyItem = (this.buyWith == null) || itemList.TryGetValue(this.buyWith.GetItemName(), out moneyCount);

        // Output string, given set conditions
        if (hasRequirement && hasMoneyItem && CanBuyItem(moneyCount)) return this.gainString;
        if (hasRequirement && !this.hideExpensive) return this.gainString;

        return string.Empty;
    }

    public void ObtainThisItem(Dictionary<string, int> itemList) {

        if ((this.itemRequired != null) && !itemList.ContainsKey(this.itemRequired.GetItemName())) {
            return; 
        }
        else if (this.buyWith == null) {
            GainThisItem(itemList);
        }
        else {
            TryToBuyThisItem(itemList);
        }

    }
    
    public void GainThisItem(Dictionary<string, int> itemList) {

        if (itemList == null) return;

        if (itemList.ContainsKey(this.itemName)){
            itemList[this.itemName] += 1; }
        else {
            itemList[this.itemName] = 1;
        }

    }

    private void TryToBuyThisItem(Dictionary<string,int> itemList) {

        if (itemList == null) return;

        if (!itemList.ContainsKey(this.buyWith.GetItemName())) return;
        if (!CanBuyItem(itemList[this.buyWith.GetItemName()])) return;

        if (itemList[this.buyWith.GetItemName()] > this.itemCost) {
            itemList[this.buyWith.GetItemName()] -= this.itemCost;
        }
        else {
            // Item count should always be positive, but this still removes negative amounts just in case
            itemList.Remove(this.buyWith.GetItemName());
        }

        GainThisItem(itemList);

    }

}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Craft Action")]
public class Craft : ScriptableObject
{

    [SerializeField]
    [Tooltip("The string that appears in the body text when this option is included.")]
    private string craftString = string.Empty;
    [SerializeField]
    [Tooltip("Hide the craftString if the item cannot be crafted.")]
    private bool hideUncraftable = false;
    [SerializeField]
    [Tooltip("The key to press to craft this item.")]    
    private string craftKey = string.Empty;
    [SerializeReference]
    [Tooltip("The item you with to craft.")]
    private GameItem craftThisItem = null;
    [SerializeField]
    [Tooltip("The items needed to craft this item. One of each is consumed on success.")]
    private GameItem[] recipe = null;

    public string GetCraftString(Dictionary<string,int> itemList) {
        return (!this.hideUncraftable || ItemsExist(itemList)) ? this.craftString : string.Empty;
    }
    public string GetCraftKey() { return this.craftKey != "Escape" ? this.craftKey : string.Empty; }

    public void TryToCraft(Dictionary<string,int> itemList) {
        
        if (this.recipe.Length == 0) return;
        if (!ItemsExist(itemList)) return;
        CraftThisItem(itemList);

    }

    private bool ItemsExist(Dictionary<string,int> itemList) {

        foreach(var item in this.recipe) {
            if (!itemList.ContainsKey(item.GetItemName())) return false;
        }

        return true;
    }

    private void CraftThisItem(Dictionary<string, int> itemList) {

        if (itemList.ContainsKey(this.craftThisItem.GetItemName())) {
            itemList[this.craftThisItem.GetItemName()] += 1;
        }
        else {
            itemList[this.craftThisItem.GetItemName()] = 1;
        }

        foreach(var item in this.recipe) {
            if(itemList[item.GetItemName()] <= 1) {
                itemList.Remove(item.GetItemName());
            }
            else {
                itemList[item.GetItemName()] -= 1;
            }
        }
    }
}

1 Like

Wow! You put so much time and effort! I am impressed! You need to pat yourself on the pack! You are an incredible game developer!

No, I’m just someone who looks for excuses not to do the things I need to. Although I did have bigger ideas for a text adventure, which is a little bit why I put so much effort into this. I don’t think I’ll be putting so much into the next lessons, but time will tell.

Privacy & Terms