Objectives with Quantity before completion

I wanted to introduce a notion of quantity for the objectives inside a quest.
Like “Kill 5 globins” or “Go fetch for me 7 flowers”

So I made a few change, but not much honestly, it was pretty easy to handle, because our QuestStatus already do the majority of the hard work.

Inside Quest.cs I modified the class Objective and added a method GetObjectiveQuantityRequired

        public class Objective
        {
            public string reference;
            public string description;
            [Min(1)]
            [SerializeField] int quantity = 1;

            public int GetQuantity()
            {
                return quantity;
            }
            
        }

        public int GetObjectiveQuantityRequired(string objectiveReference)
        {
            foreach (Objective objective in objectives)
            {
                if (objective.reference == objectiveReference)
                {
                    return objective.GetQuantity();
                }
            }
            // The objective was not found.
            return -1;
        }

And inside QuestStatus (the method CompleteObjective is the juicy part)

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

namespace RPG.Quests
{
    [System.Serializable]
    public class QuestStatus
    {
        [SerializeField] private Quest quest;
        [SerializeField] private List<string> completedObjectives = new List<string>();
        private Dictionary<string, int> objectivesOnGoing = new Dictionary<string, int>();

        [System.Serializable]
        class QuestStatusRecord
        {
            public string questName;
            public List<string> completedObjectives;
            public Dictionary<string, int> objectivesOnGoing;
        }
        
        public QuestStatus(Quest quest)
        {
            this.quest = quest;
        }

        public QuestStatus(object objectState)
        {
            QuestStatusRecord state = objectState as QuestStatusRecord;
            if (state == null) return;
            this.quest = Quest.GetByName(state.questName);
            this.completedObjectives = state.completedObjectives;
            this.objectivesOnGoing = state.objectivesOnGoing;
        }

        public Quest GetQuest()
        {
            return quest;
        }

        public List<string> GetCompletedObjectives()
        {
            return completedObjectives;
        }

        public bool IsObjectiveComplete(string objective)
        {
            return completedObjectives.Contains(objective);
        }

        public void CompleteObjective(string objective)
        {
            if (quest.HasObjective(objective))
            {
                int objectiveQuantityDone = 1;
                if (objectivesOnGoing.ContainsKey(objective))
                {
                    objectiveQuantityDone += objectivesOnGoing[objective];
                }

                if (objectiveQuantityDone >= quest.GetObjectiveQuantityRequired(objective))
                {
                    completedObjectives.Add(objective);
                    if (objectivesOnGoing.ContainsKey(objective))
                    {
                       objectivesOnGoing.Remove(objective);
                    }
                }
                else
                {
                    objectivesOnGoing[objective] = objectiveQuantityDone;
                }
            }
        }

        public object CaptureState()
        {
            QuestStatusRecord record = new QuestStatusRecord();
            record.questName = this.quest.GetTitle();
            record.completedObjectives = this.completedObjectives;
            record.objectivesOnGoing = this.objectivesOnGoing;
            return record;
        }

        public bool IsComplete()
        {
            foreach (var objective in quest.GetObjectives())
            {
                if (!completedObjectives.Contains(objective.reference))
                {
                    return false;
                }
            }

            return true;
        }
    }
}

I have not yet modified the UI to handle the display of the quantity for objectives, but it should not be terribly difficult. I will update this post when it’s done.

2 Likes

I find a strange bug where the new dictionnary objectivesOnGoing is not correctly initialized . The list is correctly initialized even if empty, but not the dictionnary when the state is restored after the saved.
Probably something that I don’t know about C#
This lead to a bug where we do

objectivesOnGoing.ContainsKey(objective)

on a null.
The fix is to initialize the dictionnary in CaptureState

        public object CaptureState()
        {
            QuestStatusRecord record = new QuestStatusRecord();
            record.questName = this.quest.GetTitle();
            record.completedObjectives = this.completedObjectives;
            if (this.objectivesOnGoing == null)
            {
                this.objectivesOnGoing = new Dictionary<string, int>();
            }
            record.objectivesOnGoing = this.objectivesOnGoing;
            return record;
        }

This is an excellent catch! I briefly looked this code over when you posted it initially (I was swamped with questions, so Show topics sometimes just get a glance) and didn’t notice the potential null issue.

Great work!

And I run into the same issue with

   public void CompleteObjective(string objective)
        {
            if (quest.HasObjective(objective))
            {
                int objectiveQuantityDone = 1;
                if (objectivesOnGoing.ContainsKey(objective))

The dictionary is also not initialize, if we just received the quest.

I think I understand why. QuestStatus is not a MonoBehaviour inerhitor, so don’t have Awake/Start/etc.
It don’t have a … life on his on as a component on a object.
So it really only initialized when we do

        public void AddQuest(Quest quest)
        {
            if (HasQuest(quest)) return;
            QuestStatus newStatus = new QuestStatus(quest);
            statuses.Add(newStatus);
        }

inside QuestList. So the new dictionary should also be initialized inside the constructor.

        public QuestStatus(Quest quest)
        {
            this.quest = quest;
            objectivesOnGoing = new Dictionary<string, int>();
        }

The thing I still don’t understand is why the list (completedObjectives) is fine.

Not sure why one is initialized and the other isn’t (they both should be). The only thing I can think of is the [SerializeField], but the course code does not have these [SerializeField] tags (they’re not really needed).

Privacy & Terms