Page 1 of 1

Custom Inventory System & Quest System's Lua Environment

Posted: Thu Dec 01, 2022 3:39 pm
by BarbazLel1
Hi Pixel Crushers.
I'm trying to do the Quest tutorial in my own project with my own assets, and I'm having a small problem. I have an inventory system that stores InventoryItem class objects in an array of InventorySlot structs in an Inventory monoBehaviour that's attached to the player. Now, I've set up a quest that asks for the player to pick up a certain number of objects (they exist solely in a distinct storage inventory).

Now I'm not sure how to reference them, how to check if the Inventory contains the Quest item. Since I can't pass to Lua the InventorySlot and also not the InventoryItem. The InventoryItems have a GUID field, I thought about the following method:

Code: Select all

    #region Methods For Dialogue System LUA Environment
    
    public double HasAmountOfItem(string GUID) //return double for LUA
    {
        for (var i = 0; i < slots.Length; i++)
        {
            if (slots[i].item.GetItemID() == GUID)
            {
                return slots[i].number;
            }
        }
        
        return 0;
    }
    
    // LUA Registration
    #endregion


The problem is that then I need to Dialogue System to know about all the GUIDs, and I'm not sure how to do that. I do have a static dictionary<string, InventoryItem> that contains all the items and looks them up with a GUID but I don't know how to pass it not by hand to the Variable database or in general how to be able to reference it more easily in the Dialogue editor. What would you suggest? An alternative would be probably to look by name.

Re: Custom Inventory System & Quest System's Lua Environment

Posted: Thu Dec 01, 2022 4:34 pm
by Tony Li
It will probably be easiest to go with your alternative and use names.

Re: Custom Inventory System & Quest System's Lua Environment

Posted: Mon Dec 05, 2022 3:41 pm
by BarbazLel1
Hi, I've spent some more time with the documentation, but I'm breaking my head on this a bit. I went the ItemName route.

I've managed to add assets to the database, using a QuestData scriptable object I have made.
It creates a Quest, gives it a name, and adds a requiredItem, requiredAmount and currentAmount as variables.
It creates it the following way: The name of the Quest asset in the database is QuestGiver.QuestName,
and if there's an item --> the variable name would be QuestGiver.QuestName.ItemName, which it would take from the InventoryItem class, and for each item variable there's a variable for requiredAmount and currentAmount, which are taken from the QuestData.

This is the OnValidate method on the QuestData script.

Code: Select all

    private void OnValidate()
    {

        if (!QuestDatabase)
        {
            Debug.Log("No Quest Database for this Quest Asset.", this);
            return;
        }
        
        if (InventoryItem)
        {
            RequiredItemName = InventoryItem.GetItemName();
        }
        if (String.IsNullOrEmpty(RequiredItemName) && IsItemAcquisition)
        {
            Debug.Log("Item Acquisition Quest has no item.");
            return;
        }

        //Add Quest To Database 
        string consolidatedQuestName = $"{QuestGiver}.{QuestName}";
        Debug.Log("Has database?: " + QuestDatabase);
        if (!DialogueDatabase.ContainsName(QuestDatabase.items, consolidatedQuestName))
        {

            Item questDialogueDatabaseItem = new Item();
            questDialogueDatabaseItem.fields = new List<Field>();
            questDialogueDatabaseItem.IsItem = false;
            questDialogueDatabaseItem.Name = consolidatedQuestName;
            questDialogueDatabaseItem.Description = QuestDescription;
            QuestDatabase.items.Add(questDialogueDatabaseItem);
            QuestSystemItem = questDialogueDatabaseItem;

            Debug.Log("Has Quest been added?: " +
                      DialogueDatabase.ContainsName(QuestDatabase.items, consolidatedQuestName));
            Debug.Log("Quest " + consolidatedQuestName + " has been added to the current database");
        }

        
        //if Quest is ItemAcquisition
        if (IsItemAcquisition)
        {

            var questItemName = consolidatedQuestName +"."+ RequiredItemName;
            if (!DialogueDatabase.ContainsName(QuestDatabase.variables, questItemName))
            {
                Variable questItemNameVariable = new Variable();
                questItemNameVariable.fields = new List<Field>();
                questItemNameVariable.Type = FieldType.Text;
                questItemNameVariable.Name = questItemName;
                questItemNameVariable.InitialValue = RequiredItemName;
                QuestDatabase.variables.Add(questItemNameVariable);
                Debug.Log("Variable " + questItemNameVariable.Name +"with the initial value of " + questItemNameVariable.InitialValue);
                
                Variable questItemRequiredNumber = new Variable();
                questItemRequiredNumber.fields = new List<Field>();
                questItemRequiredNumber.Type = FieldType.Number;
                questItemRequiredNumber.Name = questItemName + "." + "RequiredAmount";
                questItemRequiredNumber.InitialFloatValue = RequiredItemNumber;
                QuestDatabase.variables.Add(questItemRequiredNumber);
                Debug.Log("Variable " + questItemRequiredNumber.Name +"with the initial value of " + questItemNameVariable.InitialFloatValue);
                
                Variable questItemCurrentItem = new Variable();
                questItemCurrentItem.fields = new List<Field>();
                questItemCurrentItem.Type = FieldType.Number;
                questItemCurrentItem.Name = questItemName + "." + "CurrentAmount";
                questItemCurrentItem.InitialFloatValue = 0f;
                QuestDatabase.variables.Add(questItemCurrentItem);
                Debug.Log("Variable " + questItemRequiredNumber.Name +"with the initial value of " + questItemNameVariable.InitialFloatValue);
                
            }
        }
    }
In the Inventory I created the following methods:

Code: Select all

    public bool QuestSystem_HasRequiredAmountOfItem(string itemName, double requiredAmount)
    {
        for (var i = 0; i < slots.Length; i++)
        {
            Debug.Log($"{slots[i].item.GetItemName()} & {itemName}");
            if (slots[i].item.GetItemName() == itemName)
            {
                Debug.Log($"current items / required : {slots[i].number} , {requiredAmount}");
                return (slots[i].number >= requiredAmount);
            }
        }

        return false;
    }

Code: Select all

    public void QuestSystem_RemoveItem(string itemName, double number)
    {
        for (var i = 0; i < slots.Length; i++)
        {
            if (slots[i].item.GetItemName() == itemName)
            {
                RemoveFromSlot(i, (int)number);
            }
        }
     }
And I managed to make to work with a bit of a trickery. I registered the methods for the dropmenu in this manner:
Image

This way I can instead of typing or copy-pasting, just choose the variable from a drop-menu, even though this is not a type-safe way, to say the least, but if the naming conventions are right, it shouldn't be a problem. It works, but it's dirty.
Is there anything you would recommend to avoid the problems of this method? In the first quest that I made to test this it worked as expected - QuestSystem_HasRequiredAmountOfItem(itemName, requiredAmount) returns the correct value and QuestSystem_RemoveItem(string itemName, double number) removes the given amount.

My question is how can I set the value of CurrentAmount, or map the item quantity (InventorySlot.number) to a LUA variable/data record that I can then pass to the Quest Entry. I have a InvetoryUpdated event, that I want to set the variable then, but I'm not sure how to approach that.

Re: Custom Inventory System & Quest System's Lua Environment

Posted: Mon Dec 05, 2022 4:10 pm
by Tony Li
Hi,

Items and other Asset types in a DialogueDatabase should have unique IDs. To create a new Item with a unique ID, I recommend using the Template class. If this is all done in the editor, use TemplateTools.LoadFromEditorPrefs(). If it's all runtime, use Template.FromDefault(). Example:

Code: Select all

var template = TemplateTools.FromEditorPrefs(); // Get template used for database.
int questID = template.GetNextQuestID(dialogueDatabase);
var item = template.CreateQuest(questID, "QuestTitle"); // (Also sets IsItem or Name properties.)
dialogueDatabase.items.Add(item);
BarbazLel1 wrote: Mon Dec 05, 2022 3:41 pmMy question is how can I set the value of CurrentAmount, or map the item quantity (InventorySlot.number) to a LUA variable/data record that I can then pass to the Quest Entry. I have a InvetoryUpdated event, that I want to set the variable then, but I'm not sure how to approach that.
You can use a [lua(code)] tag in the quest entry to show the value of CurrentAmount by calling your Lua function, or [var=variable] tag if it's a DS variable.

Re: Custom Inventory System & Quest System's Lua Environment

Posted: Sat Dec 10, 2022 7:42 am
by BarbazLel1
Hi Tony,
Thanks for the feedback. I've improved the QuestData to DialogueDatabase translation accordingly.

I spent some more time with the documentation and reading more threads here in the forum. I'm getting better with working with the system. I managed to do what I wanted, I have now an Item Acquisition quest that I can get from the NPC and it checks the player's inventory for the correct item and its amounts.

I have these methods in the Inventory class to communicate with the Dialogue System:

//This one is checked as a condition for quest completion.

Code: Select all

    public bool QuestSystem_HasRequiredAmountOfItem(string itemName, double requiredAmount)
    {
        for (var i = 0; i < slots.Length; i++)
        {
            Debug.Log($"{slots[i].item.GetItemName()} & {itemName}");
            if (slots[i].item.GetItemName() == itemName)
            {
                Debug.Log($"current items / required : {slots[i].number} , {requiredAmount}");
                return (slots[i].number >= requiredAmount);
            }  
        }

        return false;
    }
//This is one is invoked on the InventoryUpdated event. It sets the LUA variable {QuestName}.{RequiredItem}.{CurrentAmount} according to the number of the inventory, and then calls the Quest Tracker update methods through the Game's UIController.

The Quest Entry is:
[var=Spencer.FirstQuest.RequiredItem.CurrentAmount] / [var=Spencer.FirstQuest.RequiredItem.RequiredAmount]
and this way it gets updated every time we change something in the inventory.

Code: Select all

    public void QuestSystem_UpdateQuestItems()
    {
        var allActiveQuests = QuestLog.GetAllQuests();
        foreach (string questName in allActiveQuests)
        {
            string requiredItemName = DialogueLua.GetVariable(questName + ".RequiredItem").asString;
            for (int i = 0; i < slots.Length; i++)
            {
                if (!slots[i].item)
                {
                    continue;
                }
                
                if (requiredItemName == slots[i].item.GetItemName())
                {
                    DialogueLua.SetVariable(questName+".RequiredItem.CurrentAmount", (double)slots[i].number);
                    Debug.Log($"{questName}.RequiredItem.CurrentAmount");
                    UIController.Instance.UpdateQuestTracker();
                }
                
            }

        }
    }
//This one removes items that have been requested.

Code: Select all

    public void QuestSystem_RemoveItem(string itemName, double number)
    {
        for (var i = 0; i < slots.Length; i++)
        {
            if (!slots[i].item)
            {
                continue;
            }
            
            Debug.Log($"Item Name: {itemName}, Item Number: {number}");
            if (slots[i].item.GetItemName() == itemName)
            {
                RemoveFromSlot(i, (int)number);
            }
        }
        
        UIController.Instance.UpdateMessage($"{number} {itemName} have been removed from your inventory.");
    }
Right now my convention is as I wrote at the beginning of the thread:
There's a quest that has the name {QuestGiver.QuestName}, and then for the quest variables, I'm creating variables and not more quest Items or quest fields. Each Quest Variable will be called {QuestGiver}.{QuestName}.{Variable} or sometimes {QuestGiver}.{QuestName}.{Variable}.{Variable}.

I really don't know if this is the easiest or best way to work with the Quest System. I'd really like to get your advice on the best practices for variable/data management for the system, in terms of names, groupings, categories, fields, both LUA-wise and C#-wise, and especially with regards to Quests. It's a very powerful system, but it does have a learning curve. This game we're working on is a narrative game that puts dialogue and quests at its heart and once I feel comfortable enough with this system, we plan also to integrate Love & Hate. But right now, I don't know how I can scale my use of it wisely.

Side note: I think that it would be cool to have your HOWTO threads grouped in one sticky thread in this forum.

Re: Custom Inventory System & Quest System's Lua Environment

Posted: Sat Dec 10, 2022 9:09 am
by Tony Li
Putting the HOWTO's in a sticky is a great idea. I'll need to go through them and put the right HOWTO's in the right category (e.g., Dialogue System HOWTO's in the Dialogue System section).

Your quest approach looks good to me. I think we all initially approach the topic of quests and dialogue with the mindset that it's just some lines of speech and a couple tasks lists; how hard could it be? But, as you've seen, there's a lot of complexity behind the subject that requires a learning curve. Here are a few things to keep in mind to keep things running smoothly:
  • In general, Name fields shouldn't change. If you decide to rename a quest or actor later, you can change their Display Names.
  • If you do need to change a Name, use the Asset Renamer tool (Tools > Pixel Crushers > Dialogue System > Tools > Asset Renamer).
  • In the Dialogue Editor's Variables section, use Menu > Show Group Foldouts to group your variables into foldouts corresponding to each quest.
  • Create a CustomLuaFunctionInfo asset if you haven't already. This way you can use QuestSystem_HasRequiredAmountOfItem() in your dialogue entry Conditions using the dropdown menu instead of having to type it.
  • As early as possible, test your whole content workflow with one or two small quests before putting a lot of time into writing a lot of content. This includes writing the quests, variables, and conversations; setting them up in a scene (e.g., using a Dialogue System Trigger); showing them in UIs; saving and loading games; and language localization if you're planning to do that.