AI/Needs/NeedSystem.cs
using System;
using System.Text.Json.Nodes;

namespace HC3;

public class NeedSystem : Component
{
	/// <summary>
	/// How happy this agent is, affected by leaving needs unfulfilled.
	/// </summary>
	[Sync( SyncFlags.FromHost )]
	[Property, ReadOnly]
	public float Happiness
	{
		get => _happiness;
		set
		{
			_happiness = value.Clamp( 0, 1f );
		}
	}
	private float _happiness;

	private Need[] _cachedNeeds;

	public IEnumerable<Need> GetNeeds()
	{
		if ( _cachedNeeds == null || _cachedNeeds.Length == 0 )
			_cachedNeeds = GetComponents<Need>().ToArray();

		return _cachedNeeds;
	}

	public Need GetNeed( string name )
	{
		var needs = GetNeeds();
		foreach ( var n in needs )
		{
			if ( n.Template.ResourceName.Equals( name, StringComparison.OrdinalIgnoreCase ) )
				return n;
		}
		return null;
	}

	public Need GetNeed( NeedDefinition need )
	{
		var needs = GetNeeds();
		foreach ( var n in needs )
		{
			if ( n.Template == need ) return n;
		}
		return null;
	}

	[Property]
	public List<NeedDefinition> Needs { get; set; }

	protected override void OnAwake()
	{
		if ( !Networking.IsHost )
			return;

		foreach ( var template in Needs )
		{
			var need = GameObject.AddComponent<Need>();
			need.Init( template );
		}

		Happiness = Random.Shared.Float( 0.75f, 0.95f );
	}

	public void Tick()
	{
		if ( !Networking.IsHost )
			return;

		foreach ( var need in GetNeeds() )
		{
			need.Level += Time.Delta * (need.Template.BaseRate / 60.0f);

			if ( need.Level > 0.95f ) // failing!
			{
				Happiness -= Time.Delta * 0.01f;
			}
		}
	}

	public JsonNode ToJson()
	{
		var needs = GetNeeds();
		var array = new JsonArray();

		foreach ( var need in needs )
		{
			var obj = new JsonObject
			{
				{ need.Template.ResourceName, need.Level }
			};
			array.Add( obj );
		}

		return array;
	}

	public void LoadFromJson( JsonNode json )
	{
		if ( json is not JsonArray array )
			return;

		var needs = new Dictionary<string, Need>( StringComparer.OrdinalIgnoreCase );

		// Lookup table to speed this up
		foreach ( var need in GetNeeds() )
		{
			needs[need.Template.ResourceName] = need;
		}

		for ( int i = 0; i < array.Count; i++ )
		{
			if ( array[i] is not JsonObject obj )
				continue;

			foreach ( var kvp in obj )
			{
				if ( !needs.TryGetValue( kvp.Key, out var need ) )
					continue;

				if ( kvp.Value is JsonValue value && value.TryGetValue( out float level ) )
				{
					need.Level = level;
				}
			}
		}
	}
}