Tracking Kills for Quests

Hey everyone and @Brian_Trotter , hoping you can help me solve a bug!

I’ve implemented the Track Kills for Quests feature, and have it working (have added a collectibles extension of it too). The trouble is, it works perfectly within the scene the quest is received only.

If I transition to another scene, I have the quest list and objectives still, but the ones that use the AchievementCounter automatically complete, even if their requirements have not been met. Has anyone else run into this issue?

I’ll post what I think are the relevant scripts here to make sure I’m not missing anything:

AchievementCounter.cs

public class AchievementCounter : MonoBehaviour, IPredicateEvaluator, IJsonSaveable
{
    private Dictionary<string, int> counts = new Dictionary<string, int>();

    public event System.Action onCountChanged;

    public int AddToCount(string token, int amount, bool onlyIfExists = false)
    {
        if (!counts.ContainsKey(token))
        {
            if (onlyIfExists) return 0;
            counts[token] = amount;
            onCountChanged?.Invoke();
            return amount;
        }
        counts[token] += amount;
        onCountChanged?.Invoke();
        return counts[token];
    }

    public int RegisterCounter(string token, bool overwrite = false)
    {
        if (!counts.ContainsKey(token) || overwrite)
        {
            counts[token] = 0;
            onCountChanged?.Invoke();
        }
        return counts[token];
    }

    public int GetCounterValue(string token)
    {
        if (!counts.ContainsKey(token)) return 0;
        return counts[token];
    }

    

    public bool? Evaluate(EPredicate predicate, string[] parameters)
    {
        if (predicate == EPredicate.HasKilled)
        {
            if (int.TryParse(parameters[1], out int intParameter))
            {
                RegisterCounter(parameters[0]);
                return counts[parameters[0]] >= intParameter;
            }
            return false;
        }
        else if (predicate == EPredicate.HasCollected)
        {
            if (int.TryParse(parameters[1], out int intParameter))
            {
                RegisterCounter(parameters[0]);
                return counts[parameters[0]] >= intParameter;
            }
            return false;
        }

        return null;


    }

    public JToken CaptureAsJToken()
    {
        JObject state = new JObject();
        IDictionary<string, JToken> stateDict = state;
        foreach (KeyValuePair<string, int> keyValuePair in counts)
        {
            stateDict[keyValuePair.Key] = JToken.FromObject(keyValuePair.Value);
        }
        return state;
    }

    public void RestoreFromJToken(JToken state)
    {
        if (state is JObject stateObject)
        {
            IDictionary<string, JToken> stateDict = stateObject;
            counts.Clear();
            foreach (KeyValuePair<string, JToken> keyValuePair in stateDict)
            {
                counts[keyValuePair.Key] = keyValuePair.Value.ToObject<int>();
            }
            onCountChanged?.Invoke();
        }
    }
}

DeathCounter.cs

public class DeathCounter : MonoBehaviour
{
    [SerializeField] private string identifier;
    [SerializeField] private bool onlyIfInitialized = true;

    private AchievementCounter counter;

    private void Awake()
    {
        counter = GameObject.FindWithTag("Player").GetComponent<AchievementCounter>();
        GetComponent<Health>().OnDie += AddToCount;
    }

    private void AddToCount()
    {
        counter.AddToCount(identifier, 1, onlyIfInitialized);
    }
}

QuestList.cs:

public class QuestList : MonoBehaviour, IPredicateEvaluator, IJsonSaveable
{
    List<QuestStatus> statuses = new List<QuestStatus>();

    public event Action onQuestListUpdated;
    private void Update()
    {
        CompleteObjectivesByPredicates();

    }


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

    }

    public void CompleteObjective(Quest quest, string objective)
    {
        QuestStatus status = GetQuestStatus(quest);
        status.CompleteObjective(objective);
        if (status.IsComplete())
        {
            GiveReward(quest);
        }
        if (onQuestListUpdated != null)
        {
            onQuestListUpdated();
        }
    }


    public bool HasQuest(Quest quest)
    {
        return GetQuestStatus(quest) != null;
    }

    public IEnumerable<QuestStatus> GetStatuses()
    {
        return statuses;
    }

    private QuestStatus GetQuestStatus(Quest quest)
    {
        foreach (QuestStatus status in statuses)
        {
            if (status.GetQuest() == quest)
            {
                return status;
            }
        }
        return null;
    }

    private void GiveReward(Quest quest)
    {
        foreach (var reward in quest.GetRewards())
        {
            bool success = GetComponent<Inventory>().AddToFirstEmptySlot(reward.item, reward.number);
            if (!success)
            {
                GetComponent<ItemDropper>().DropItem(reward.item, reward.number);
            }

        }
    }

    private void CompleteObjectivesByPredicates()
    {
        foreach (QuestStatus status in statuses)
        {
            if (status.IsComplete()) continue;
            Quest quest = status.GetQuest();
            foreach (var objective in quest.GetObjectives())
            {
                if (status.IsObjectiveComplete(objective.reference)) continue;
                if (!objective.useCondition) continue;
                if (objective.completionCondition.Check(GetComponents<IPredicateEvaluator>()))
                {
                    CompleteObjective(quest, objective.reference);
                }
            }
        }
    }


    public JToken CaptureAsJToken()
    {
        JArray state = new JArray();
        IList<JToken> stateList = state;
        foreach (QuestStatus status in statuses)
        {
            stateList.Add(status.CaptureAsJToken());
        }
        return state;
    }

    public void RestoreFromJToken(JToken state)
    {
        if (state is JArray stateArray)
        {
            statuses.Clear();
            IList<JToken> stateList = stateArray;
            foreach (JToken token in stateList)
            {
                statuses.Add(new QuestStatus(token));
            }
            onQuestListUpdated?.Invoke();
        }
    }

    public bool? Evaluate(EPredicate predicate, string[] parameters)
    {
        switch (predicate)
        {
            case EPredicate.HasQuest:
                return HasQuest(Quest.GetByName(parameters[0]));
            case EPredicate.CompletedQuest:
                QuestStatus status = GetQuestStatus(Quest.GetByName(parameters[0]));
                if (status == null) return false;
                return status.IsComplete();
            case EPredicate.CompletedObjective:
                QuestStatus testStatus = GetQuestStatus(Quest.GetByName(parameters[0]));
                if (testStatus == null) return false;
                return testStatus.IsObjectiveComplete(parameters[1]);
        }
        return null;
    }

}

QuestStatus.cs

public class QuestStatus
{
    Quest quest;
    List<string> completedObjectives = new List<string>();

    [System.Serializable]
    class QuestStatusRecord
    {
        public string questName;
        public List<string> completedObjectives;
    }

    public QuestStatus(Quest quest)
    {
        this.quest = quest;
    }

    public QuestStatus(object objectState)
    {
        QuestStatusRecord state = objectState as QuestStatusRecord;
        quest = Quest.GetByName(state.questName);
        completedObjectives = state.completedObjectives;
    }

    public Quest GetQuest()
    {
        return quest;
    }

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

    public int GetCompletedCount()
    {
        return completedObjectives.Count;
    }

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

    public void CompleteObjective(string objective)
    {
        if (quest.HasObjective(objective))
        {
            completedObjectives.Add(objective);
        }
    }

    public object CaptureState()
    {
        QuestStatusRecord state = new QuestStatusRecord();
        state.questName = quest.name;
        state.completedObjectives = completedObjectives;
        return state;
    }
    public QuestStatus(JToken objectState)
    {
        if (objectState is JObject state)
        {
            IDictionary<string, JToken> stateDict = state;
            quest = Quest.GetByName(stateDict["questName"].ToObject<string>());
            completedObjectives.Clear();
            if (stateDict["completedObjectives"] is JArray completedState)
            {
                IList<JToken> completedStateArray = completedState;
                foreach (JToken objective in completedStateArray)
                {
                    completedObjectives.Add(objective.ToObject<string>());
                }
            }
        }
    }
    public JToken CaptureAsJToken()
    {
        JObject state = new JObject();
        IDictionary<string, JToken> stateDict = state;
        stateDict["questName"] = quest.name;
        JArray completedState = new JArray();
        IList<JToken> completedStateArray = completedState;
        foreach (string objective in completedObjectives)
        {
            completedStateArray.Add(JToken.FromObject(objective));
        }
        stateDict["completedObjectives"] = completedState;
        return state;
    }

}

I know this is completely irrelevant, but is this the full implementation to track kills for quests? By the way I think @bixarrio might have an idea :slight_smile:

Hahahaha. Well, I’ll leave this here for other numbskulls like me …

I initially added the AchievementCounter to an instance of the player in once scene, and pressed override the prefab. I guess it didn’t override!

This is also why we should be doing existence checks too, I guess :rofl:

2 Likes

ehh you’re good. I fall into dumb stuff all the time as well, and sometimes the smallest thing takes me days to figure out. You’re good man, you’re good

On a side note, can you please link me to the tutorial that does this, or at least let me know who has ‘DeathCounter.cs’ on them? You mentioned ‘AchievementCounter.cs’ goes on the player, and I know who QuestList.cs and QuestStatus.cs go on to, but who does AchievementCounter.cs go onto?

Yeah I’ll do a write up of everything I grabbed from different places to make the system work. Give me a day or two :slightly_smiling_face: but long story short, death counter goes on each individual enemy you want to count toward the counter. You give it a token (I use the quest name), and the in the quest objectives, you use the same token. It’s a great system, I’m also using it for general collectibles (like a Stones of Barenziah fron Skyrim type quest).

The trouble is, to make it a complete system is split among a number of threads, so I plan on consolidating them all into one write up (crediting each original author of course :slightly_smiling_face:)

1 Like

I love the sound of this. Thank you Tommy! :smiley:

I’ve never done anything like that (today… ok… today after lunch… ok, in the last 10 minutes)

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

Privacy & Terms