What's the best way to tackle deleting an item turned in for a quest?

One final task on my to-do list before I proceed onto the final course: deleting items that the player hands in for a quest. I’m using the Mother hubbards bunions quest as an example, and the foot cream as the item the player hands in.

The player completes the quest by returning to the NPC with the foot cream, and the quest completes as normal. All dialogue related to starting/finishing the quest is removed after it’s completed by the predicate system. The only thing left over is the foot cream item.

Where and how should I go about ensuring that quest items are destroyed upon quest completion? I assume it’ll need to reference the item’s unique ID (78e0b266-eeeb-4712-9d7e-91d3f063c416).

What would happen in the case of a more generic quest without a specific item with an item ID? Like, if I had to hand in three swords dropped by the nearby bandits? The item IDs would be generated when the swords are dropped, and the dialogue node’s HasInventoryItem wouldn’t have a reference to a unique item ID string.

Sorry, just some late night thinking. It’s 3AM already?! :stuck_out_tongue:

Thanks in advance for the help.

We just need to add a couple of convenience methods to Inventory for this:

        public void RemoveItem(InventoryItem item, int number)
        {
            if (item == null) return;
            for (int i = 0; i < slots.Length; i++)
            {
                if (object.ReferenceEquals(slots[i].item, item))
                {
                    slots[i].number -= number;
                    if (slots[i].number <= 0)
                    {
                        slots[i].item = null;
                        slots[i].number = 0;
                    }

                    return;
                }
            }
        }

        public void RemoveItem(string itemID, int number)
        {
            InventoryItem item = InventoryItem.GetFromID(itemID);
            RemoveItem(item, number);
        }

Then in your QuestCompletion, link to the player’s Inventory and the RemoveItem method with the ItemID.

If the three swords are from the same InventoryItem, then they will have the same ItemID. The ItemID is shared amongst all instances of the item.

I’m close. I’ve added those two methods to Inventory.cs, and I’m back in the QuestCompletion.cs. How should I assign the itemID? It didn’t seem to want to take it.

So, I tried an alternative method I thought might work: Have the quest item as a serializable field that you set when configuring QuestCompletion (I think this should actually be ObjectiveCompletion?)

This… didn’t work though. I threw a “Index was outside the bounds of the array” from the Inventory.cs, and a “object reference not set to an instance of an object” from the QuestCompletion.cs after handing it in. This threw the inventory system into overdrive and I was drowned in rewards. :joy:

QuestCompletion.cs

using UnityEngine;
using GameDevTV.Inventories;

namespace RPG.Quests
{
    public class QuestCompletion : MonoBehaviour
    {
        [SerializeField] Quest quest;
        [SerializeField] string objective;
        [SerializeField] InventoryItem item;

        public void CompleteObjective()
        {
            QuestList questList = GameObject.FindGameObjectWithTag("Player").GetComponent<QuestList>();
            questList.CompleteObjective(quest, objective);
            if (item != null)
            {
                GetComponent<Inventory>().RemoveItem(item, 1);
            }
        }
    }
}

How would I go about assigning the item ID instead (78e0b266-eeeb-4712-9d7e-91d3f063c416)?

Thanks!
Mark.

Actually, I mispoke, it’s not QuestCompletion, in your Dialogue, you’re going to set a Trigger and add a DialogueTrigger, it’s the DialogueTrigger that will link to the Inventory in the Unity Event. Apologies for that.

Ah, so close now. I’ve set up a dialogue node with an OnExit trigger ReturnCream. When that’s triggered, I’ve got the King (gameObject) triggering QuestCompletion.CompleteObjective, and if I understand correctly, the Player (gameObject) needs to be added to this trigger so I can call to the Inventory.cs for RemoveItem()?

I can’t seem to access any functions from Inventory through OnTrigger.

I forgot, we’re going to need a wrapper to accomplish this, since Inventory.RemoveItem takes either an InventoryItem or a string as well as an integer, and our Trigger event is generic with no parameters.

This actually simplifies things a bit, as you won’t need to drag the player into the event field. Put this class on the Quest giver, and link the Inventory Item directly (no need for strings) in it’s InventoryItem field:

using GameDevTV.Inventories;
using UnityEngine;

namespace RPG.Inventories
{
    public class ItemRemover : MonoBehaviour
    {
        [SerializeField] private InventoryItem itemToRemove;
        [SerializeField] private int number;

        public void RemoveItem()
        {
            Inventory.GetPlayerInventory().RemoveItem(itemToRemove, number);
        }
    }
}

Your DialogueTrigger will then link to ItemRemover.RemoveItem()

Sorry about the confusion along the way.

That’s okay, thanks for the help so far!

That worked perfectly, with only one minor issue that I’m not sure how to solve: The item is handed in and removed, but a ‘ghost’ image of the potion remains. You can try dragging it to another inventory spot to cause a ‘Object reference not set to an instance of an object’.

It looks like the inventory needs a call to refresh? I saw a public action for inventoryUpdated in Inventory.cs, but not sure whether that’ll do what we need.

        /// <summary>
        /// Broadcasts when the items in the slots are added/removed.
        /// </summary>
        public event Action inventoryUpdated;

Edit: Dragging another item on top of it clears it correctly. Maybe we need to trigger some sort of inventory reshuffle to fill the empty space?

Hm… I’m only getting the ghost item some of the time. It could have to do with another issue I’ve come across:

Occasionally, I’m getting a NRE from StartDialogue:

NullReferenceException: Object reference not set to an instance of an object
RPG.Dialogue.PlayerConversant.StartDialogue () (at Assets/Scripts/Dialogue/PlayerConversant.cs:59)
RPG.Dialogue.PlayerConversant.Update () (at Assets/Scripts/Dialogue/PlayerConversant.cs:50)
void Update()
        {
            if (targetConversant)
            {
                if (Vector3.Distance(targetConversant.transform.position, transform.position) > 3)
                {
                    GetComponent<Mover>().MoveTo(targetConversant.transform.position, 1);
                }
                else
                {
                    GetComponent<Mover>().Cancel();
                    StartDialogue();
                }
            }
        }
public void StartDialogue()
        {
            currentConversant = targetConversant;
            targetConversant = null;
            currentNode = currentDialogue.GetRootNode();
            TriggerEnterAction();
            onConversationUpdated();
        }

from Line 59:

currentNode = currentDialogue.GetRootNode();

As far as I can see, everything is working fine 95% of the time.

Edit: It seems to come about when you try to ‘cancel’ the MoveTo. I can’t ‘stop’ moving to the target until I’m already there. The NRE seems to come from using Quit() here.

public void Cancel()
        {
            GetComponent<Mover>().Cancel();
            Quit();
        }

Yep, add inventoryUpdated?.Invoke(); to the end of the RemoveItem method.

Somehow, the currentDialogue is not getting set. Most likely the AIConversant that initiated the dialogue doesn’t have the dialogue attached.

That worked perfectly, thank you!

Regarding currentNode = currentDialogue.GetRootNode();, it seems it was Quit() occurring on the public void Cancel() that was resetting the currentDialogue. For some reason, Cancel() isn’t allowing me to stop moving to the targetConversant.

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using RPG.Core;
using RPG.Movement;

namespace RPG.Dialogue
{
    public class PlayerConversant : MonoBehaviour, IAction
    {
        [SerializeField] string playerName;

        Dialogue currentDialogue;
        DialogueNode currentNode = null;
        AIConversant currentConversant = null;

        AIConversant targetConversant;

        bool isChoosing = false;

        public event Action onConversationUpdated;

        public void StartDialogueAction(Dialogue dialogue, AIConversant speaker)
        {
            Debug.Log($"StartDialogueAction {speaker}/{dialogue}");
            if (currentConversant != null && currentConversant == speaker) return;
            if (currentDialogue != null) Quit();
            if (dialogue == null)
            {
                return;
            }

            GetComponent<ActionScheduler>().StartAction(this);
            targetConversant = speaker;
            currentDialogue = dialogue;
        }
        
        void Update()
        {
            if (targetConversant)
            {
                if (Vector3.Distance(targetConversant.transform.position, transform.position) > 3)
                {
                    GetComponent<Mover>().MoveTo(targetConversant.transform.position, 1);
                }
                else
                {
                    GetComponent<Mover>().Cancel();
                    StartDialogue();
                }
            }
        }

        public void StartDialogue()
        {
            currentConversant = targetConversant;
            targetConversant = null;
            currentNode = currentDialogue.GetRootNode();
            TriggerEnterAction();
            onConversationUpdated();
        }

        public void Quit()
        {
            currentDialogue = null;
            TriggerExitAction();
            currentNode = null;
            isChoosing = false;
            currentConversant = null;
            onConversationUpdated();
        }

        public bool IsActive()
        {
            return currentDialogue != null;
        }

        public bool IsChoosing()
        {
            return isChoosing;
        }

        public string GetText()
        {
            if (currentNode == null)
            {
                return "";
            }

            return currentNode.GetText();
        }

        public string GetCurrentConversantName()
        {
            if (isChoosing)
            {
                return playerName;
            }
            else
            {
                return currentConversant.GetName();
            }
        }

        public IEnumerable<DialogueNode> GetChoices()
        {
            return FilterOnCondition(currentDialogue.GetPlayerChildren(currentNode));
        }

        public void SelectChoice(DialogueNode chosenNode)
        {
            currentNode = chosenNode;
            TriggerEnterAction();
            isChoosing = false;
            // Short Responses (As Shown On Button):
            Next();
            // Longer Response Event Trigger:
            // onConversationUpdated();
        }

        public void Next()
        {
            int numPlayerResponses = FilterOnCondition(currentDialogue.GetPlayerChildren(currentNode)).Count();
            if (numPlayerResponses > 0)
            {
                isChoosing = true;
                TriggerExitAction();
                onConversationUpdated();
                return;
            }

            DialogueNode[] children = FilterOnCondition(currentDialogue.GetAIChildren(currentNode)).ToArray();
            if (children.Length > 0)
            {
                int randomIndex = UnityEngine.Random.Range(0, children.Count());
                TriggerExitAction();
                currentNode = children[randomIndex];
                TriggerEnterAction();
                onConversationUpdated();
            }
            else
            {
                Quit();
            } 
        }

        private IEnumerable<DialogueNode> FilterOnCondition(IEnumerable<DialogueNode> inputNode)
        {
            foreach (var node in inputNode)
            {
                if (node.CheckCondition(GetEvaluators()))
                {
                    yield return node;
                }
            }
        }

        private IEnumerable<IPredicateEvaluator> GetEvaluators()
        {
            return GetComponents<IPredicateEvaluator>();
        }

        public bool HasNext()
        {
            return FilterOnCondition(currentDialogue.GetAllChildren(currentNode)).Count() > 0;
        }

        private void TriggerEnterAction()
        {
            if (currentNode != null)
            {
                TriggerAction(currentNode.GetOnEnterAction());
            }
        }

        private void TriggerExitAction()
        {
            if (currentNode != null)
            {
                TriggerAction(currentNode.GetOnExitAction());
            }
        }

        private void TriggerAction(string action)
        {
            if (action == "") { return; }

            foreach (DialogueTrigger trigger in currentConversant.GetComponents<DialogueTrigger>())
            {
                trigger.Trigger(action);
            }
        }

        public void Cancel()
        {
            GetComponent<Mover>().Cancel();
            //Quit();
        }
    }
}

Edit: I figured out where I went wrong! I never set the targetConversant to null when calling Cancel! Now everything is functioning as it should be. Thank you!

public void Cancel()
        {
            targetConversant = null;
            GetComponent<Mover>().Cancel();
        }
1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms