Newer
Older
Hierarchical-Task-Network-Unity-3D / Assets / Scripts / HTN / PrimitiveTask.cs
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

/// <summary>
/// Base class for primitive tasks in the HTN. These are actionable tasks
/// that directly modify the world state.
/// </summary>
public abstract class PrimitiveTask : ITask
{
    // ================== VARIABLES ==================
    private static readonly bool isCompound = false;
    protected bool hasRun = false;

    // ================== PROPERTIES ==================
    public string Name { get; private set; }

    public bool IsCompound => isCompound;

    /// <summary>
    /// Conditions to keep task valid if any
    /// </summary>
    public virtual List<Condition> Conditions { get; } = new();
    /// <summary>
    /// Conditions to tag quest as finished
    /// </summary>
    public virtual List<Condition> Postconditions { get; } = new();

    // ================== UTILS ==================
    /// <summary>
    /// Set's the task name
    /// </summary>
    protected void SetName(string name) => Name = name;

    // ================== CONDITIONS ==================
    /// <summary>
    /// Shared method for adding conditions to any condition list with null checking
    /// </summary>
    /// <param name="condition">Condition to add</param>
    /// <param name="targetList">Target list (Conditions or Postconditions)</param>
    protected void AddConditionToTarget(Condition condition, List<Condition> targetList)
    {
        if (condition == null)
        {
            Debug.LogError("Attempted to add null condition");
            return;
        }

        targetList.Add(condition);
    }

    /// <summary>
    /// Adds a single precondition that must be met for this task to be valid
    /// </summary>
    /// <param name="condition">The condition to add</param>
    /// <returns>The current task instance for method chaining</returns>
    public PrimitiveTask AddCondition(Condition condition)
    {
        AddConditionToTarget(condition, Conditions);
        return this;
    }

    /// <summary>
    /// Adds multiple preconditions that must be met for this task to be valid
    /// </summary>
    /// <param name="conditions">List of conditions to add</param>
    /// <returns>The current task instance for method chaining</returns>
    public PrimitiveTask AddCondition(List<Condition> conditions)
    {
        conditions?.ForEach(c => AddConditionToTarget(c, Conditions));
        return this;
    }

    /// <summary>
    /// Adds a single postcondition that defines when this task is complete
    /// </summary>
    /// <param name="condition">The condition to add</param>
    /// <returns>The current task instance for method chaining</returns>
    public PrimitiveTask AddPostCondition(Condition condition)
    {
        AddConditionToTarget(condition, Postconditions);
        return this;
    }

    /// <summary>
    /// Adds multiple postconditions that define when this task is complete
    /// </summary>
    /// <param name="conditions">List of conditions to add</param>
    /// <returns>The current task instance for method chaining</returns>
    public PrimitiveTask AddPostCondition(List<Condition> conditions)
    {
        conditions?.ForEach(c => AddConditionToTarget(c, Postconditions));
        return this;
    }

    // ================== VALIDATORS ==================
    /// <summary>
    /// Checks whether the task's preconditions are still valid in the current world state.
    /// If any condition fails, the task should be aborted or replanned.
    /// </summary>
    /// <param name="worldstate">The current state of the world used for condition checks.</param>
    /// <returns>
    /// True if all conditions are met; otherwise, false.
    /// </returns>
    public bool IsTaskValid(HTNPlanner planner)
    {
        if (Conditions.Count != 0) //if there are conditions
        {
            foreach (Condition condition in Conditions)
            {
                if (!condition.IsConditionMet(planner)) return false;
            }
        }

        return true;
    }


    /// <summary>
    /// Determines whether the task has achieved its goal by verifying all postconditions.
    /// </summary>
    /// <param name="worldstate">The world state to check against.</param>
    /// <returns>
    /// True if all postconditions are satisfied (task is complete); otherwise, false.
    /// </returns>
    public bool IsTaskFinished(HTNPlanner planner)
    {
        if (Postconditions.Count > 0)
        {
            foreach (Condition condition in Postconditions)
            {
                if (!condition.IsConditionMet(planner)) return false;
            }
            return true;
        }

        return hasRun;
    }


    /// <summary>
    /// Checks if this task has a coroutine implementation
    /// </summary>
    public bool HasCoroutineImplementation()
    {
        MethodInfo method = GetType().GetMethod("ExecuteRoutine",
            BindingFlags.Instance | BindingFlags.Public);
        return method != null && method.DeclaringType != typeof(PrimitiveTask);
    }

    // ================== TASK ACTIONS METHODS ==================
    /// <summary>
    /// Executes the task's logic and modifies the world state through the provided planner.
    /// This is where the actual work of the primitive task (e.g., moving, interacting) is performed.
    /// </summary>
    /// <param name="planner">The HTN planner managing the task execution. Provides access to the agent's world state and other planning utilities.</param>
    /// <remarks>
    /// - This method should only be called if <see cref="IsTaskValid"/> returns true.
    /// - Implementations should update the world state (via <paramref name="planner"/>) to reflect task completion.
    /// - For tasks with duration or asynchronous operations, override this method to handle progress tracking.
    /// </remarks>
    public virtual void Execute(HTNPlanner planner) { }

    /// <summary>
    /// Executes the task's logic over time using a coroutine, allowing for asynchronous behavior such as waiting, movement, or animations.
    /// </summary>
    /// <param name="planner">The HTN planner managing the task execution. Provides access to the agent's world state, navigation, and task context.</param>
    /// <returns>
    /// An <see cref="IEnumerator"/> to be used in a coroutine, yielding control during time-based operations and resuming as needed.
    /// </returns>
    /// <remarks>
    /// - Use this method for tasks that require multiple frames to complete (e.g., waiting in a queue, interacting with delays).
    /// - Should be used in combination with Unity's coroutine system (e.g., <c>StartCoroutine</c>).
    /// - Override this method only if your task has duration or side effects that occur over time.
    /// </remarks>
    public virtual IEnumerator ExecuteRoutine(HTNPlanner planner) { yield return null; }

    public PrimitiveTask Build(string name)
    {
        Name = name;
        return this;
    }
}