Ok, here’s the revised mostly final scripts, and what I’ve come up with as a path to the HasKilled Condition:
First is the AchievementCounter class:
using System.Collections.Generic;
using GameDevTV.Saving;
using GameDevTV.Utils;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace RPG.Quests
{
public class AchievementCounter : MonoBehaviour, ISaveable, 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 object CaptureState()
{
return counts;
}
public void RestoreState(object state)
{
counts = (Dictionary<string, int>)state;
onCountChanged?.Invoke();
}
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;
}
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();
}
}
}
}
This should be fairly straightforward, but I’ll give a bit of insight where needed. This will be a component attached to the player. It will implement ISaveable (or IJsonSaveable, depending on your needs, if you haven’t implemented my Json Saving System, just delete the references).
We’ll be using a string based dictionary that simply stores a token and a count.
The primary method, AddToCount, takes in a token and an amount and increases the appropriate counter in the dictionary. It also invokes the onCountChanged event, which is how our QuestList will eventually know about the changes in counts so that it can test the condition. The method also contains an optional bool which determines if the count should only be updated if the token already exists in the Dictionary. This is how we’ll set our Quest up to only start counting once we’ve accepted the quest. I’ll get to that shortly. The method returns the current count, in case it’s useful in some other section of code.
The RegisterCounter method creates a counter if one doesn’t exist. When we accept a quest that has a HasKilled condition, the condition will automatically call RegisterCounter and subsequent kills for the token will work.
GetCounterValue should be self-explanatory.
The Evaluate method will test for HasKilled, and compare the counter to the required number of kills. Note that we’re applying RegisterCounter() before returning the GetCounterValue. The magic here is that the only time we would be testing this predicate against this token is if there is a reason to do so, i.e. that we have the quest and are testing for it! Supposing that the quest was to kill 10 wolves, you could kill all the wolves you wanted before accepting the quest, but the counter wouldn’t increase. Once you’ve accepted the quest, however, then the system will start testing to see if you’ve killed wolves, and then it will start counting every wolf death.
That brings us to the “How exactly do we tell the system we killed a wolf” section of the game.
Next up is the DeathCounter. I set this up to automagically take care of sending the death notification to the counter system.
using RPG.Attributes;
using UnityEngine;
namespace RPG.Quests
{
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.AddListener(AddToCount);
}
private void AddToCount()
{
counter.AddToCount(identifier, 1, onlyIfInitialized);
}
}
}
This component will go on the enemies you want to kill to advance the counters. The identifier is the token you will use in the HasKilled condition. You can use this identifier on multiple characters, so quests like Kill Wolves are possible.
The component first locates the AchievementCounter on the player and caches the reference. Then it hooks itself into Health’s onDie method. The AddToCount() method then informs the AchievementCounter to increase the count, but only if the token is initialized.
Once you’ve done this, QuestStatus will pick up on anychanges because of the use of the CompleteObjectivesByPredicates(); in Update().