Merging the Third Person Controller Course with the RPG Course

[BUG LOCATION FOUND, YET TO BE SOLVED, But please check next comment for a theoretical question that I have]

I promise you, if I knew the source of it, I wouldn’t have asked the question… :sweat_smile:

This is the part where I apologize in advance, and kindly seek your help to help me fix any false code here, because… well… I’m trying my best, but I genuinely have no idea how else to make this happen

I apologize for this, but after 10 hours since I wrote this post, I genuinely still can’t identify where the issue is. I have my suspicions on 3 different scripts, ‘EnemyStateMachine.cs’, ‘AggroGroup.cs’ and ‘RespawnManager.cs’. Whenever you have the time @Brian_Trotter please just have a glimpse and see if you can help me set the hostility of non-hostile respawnable enemies to true, when they respawn (if, and only if, one of the group members is under attack, so they can go and defend their buddies):

‘AggroGroup.cs’ (nearly everything important for this issue is in ‘OnTakenHit’ - BE WARNED THOUGH, THE LAST ‘else-if’ STATEMENT OF ‘OnTakenHit’ IS UNREACHABLE CODE FOR SOME REASON…!):

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Unity.VisualScripting;
using RPG.Attributes;
using RPG.States.Enemies;
using RPG.States.Player;
using System;

namespace RPG.Combat
{

    public class AggroGroup : MonoBehaviour
    {
        [Header("This script aggrevates a group of enemies,\n if you attack one of their friends.\n The player must be in their 'PlayerChasingRange' though\n otherwise they won't try attacking you")]

        [SerializeField] List<EnemyStateMachine> enemies = new List<EnemyStateMachine>();

        [Tooltip("Set this to true only for groups of enemies that you want naturally aggressive towards the player")]
        [SerializeField] bool groupHatesPlayer = false;

        [Tooltip("Set to true only if you want enemies to forget any problems they had with the player when he dies")]
        [SerializeField] bool resetIfPlayerDies = false; // this is left as a boolean because it allows us to choose which aggroGroup members reset their settings on players' death, and which groups may not reset themselves on death (depending on your games' difficulty)

        [SerializeField] List<EnemyStateMachine> unassignedEnemies;

        private Health playerHealth;

        /// <summary>
        /// At the start of the game
        /// </summary>
        private void Start()
        {
            // Ensures guards are not active to fight you, if you didn't trigger them:
            if (groupHatesPlayer) Activate(true);
            else foreach (EnemyStateMachine enemy in enemies) 
            {
                    enemy.SetHostile(enemy.GetInitialHostility);
            }
        }

        public void OnEnable()
        {
            if (resetIfPlayerDies)
            {
                playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
                if (playerHealth != null) playerHealth.onDie.AddListener(ResetGroupMembers);
            }
        }

        public void OnDisable() 
        {
            playerHealth.onDie.RemoveListener(ResetGroupMembers);
        }

        /// <summary>
        /// Called when the player dies, and everyone is reset to their original state (resetIfPlayerDies must be set to true)
        /// </summary>
        public void ResetGroupMembers()
        {
            if (groupHatesPlayer) Activate(true);

            else if (playerHealth != null) 
            {
            foreach (EnemyStateMachine enemy in enemies)
            {
                enemy.SetHostile(enemy.GetInitialHostility);
            }
            }
        }

        /// <summary>
        /// Used when you're sure the WHOLE GROUP either likes you, or hates you (otherwise use 'EnemyStateMachine.SetHostile' for individuals)
        /// </summary>
        /// <param name="shouldActivate"></param>
        public void Activate(bool shouldActivate)
        {
            enemies.RemoveAll(enemy => enemy == null || enemy.IsDestroyed());

            foreach (EnemyStateMachine enemy in enemies)
            {
                enemy.SetHostile(shouldActivate);
            }
        }

        public void AddFighterToGroup(EnemyStateMachine enemy)
        {
            // If you got the fighter you want to add on your list, return:
            if (enemies.Contains(enemy)) return;
            // For other fighters on the list, add them:
            enemy.SetAggroGroup(this);
            enemies.Add(enemy);
            enemy.GetComponent<Health>().OnTakenHit += OnTakenHit; // NEW LINE
        }

        public void RemoveFighterFromGroup(EnemyStateMachine enemy)
        {
            // if the enemy is gone, don't try removing him again:
            if (enemy == null) return;
            // else Remove fighters from the list
            enemy.SetAggroGroup(null);
            enemies.Remove(enemy);
            enemy.GetComponent<Health>().OnTakenHit -= OnTakenHit;
        }

        public bool HasMember(EnemyStateMachine enemy) 
        {
            return enemies.Contains(enemy);
        }

        public List<EnemyStateMachine> GetGroupMembers() 
        {
            if (enemies == null) return null;
            else return enemies;
        }

        /* void OnTakenHit(GameObject instigator) // basic functionality, to get all enemies in the AggroGroup to hunt down the player when he's nearby
        {
            foreach (EnemyStateMachine enemy in enemies)
            {
                if (enemy != null && enemy.GetAggroGroup() == this) enemy.SetHostile(true, instigator);
            }
        } */

        // ----------------- // Use this function if you don't want Enemies to intelligently find out other group members to fight with, and all of them just aim for one guy ------------------

        /* void OnTakenHit(GameObject instigator) 
        {
            foreach (EnemyStateMachine enemy in enemies)
            {
                if (enemy != null && enemy.GetAggroGroup() == this && instigator != null && instigator.TryGetComponent(out EnemyStateMachine instigatorStateMachine) && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() == this)
                {
                    // if the victim is in the same group as the instigator, which is an enemy state machine, 
                    // this area runs
                    Debug.Log("Accidental attack from within the group. Ignore");
                }
                else if (enemy != null && enemy.GetAggroGroup() == this)
                {
                    // if the victim is from a different group, but the Player/another foreign NPC attacks, you run this
                    Debug.Log("Attack from outside, entire group fights back");
                    enemy.SetHostile(true, instigator);
                }
            }
        } */

        // ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

        // ---------------------------------- Use this function if you want Superior Intelligence, where each enemy can intelligently identify enemies in the opposite fighting group and attack them accordingly (still in the works) ----------------------

        void OnTakenHit(GameObject instigator)
        {
            // Check if the instigator is the player
            if (instigator.GetComponent<PlayerStateMachine>())
            {
                // If the instigator is in player chasing range, then all enemies in this AggroGroup, within Player Chasing Range, with no opponent, shall aim for the player (regardless of Hostility):
                foreach (EnemyStateMachine enemy in enemies.Where(enemy => enemy != null && enemy.GetAggroGroup() == this && enemy.IsInPlayerChasingRange() && enemy.GetOpponent() == null))
                {
                    // Enemy must be free, and within range:
                    if (enemy.GetOpponent() == null && !enemy.GetHasOpponent() && enemy.IsInPlayerChasingRange()) {
                    // First, delete the invaded-by-player-group's data, and then get them to focus on the player:
                    enemy.SetHostile(true);
                    enemy.SetOpponent(instigator.gameObject);
                    enemy.SetHasOpponent(true);
                    Debug.Log($"{enemy.gameObject.name} is now hostile towards the player.");
                    }
                }
                return;
            }

            // if the hit is from an intruder, and two enemies are fighting it out, get them both to cut the fight
            // and the closest one to the intruder shall focus on fighting the intruder (and if the further one is still in 'PlayerChasingRange'
            // of the intruder, he can fight the instigator as well):

            // Get the (instigator) attacker's aggro group
            AggroGroup attackerAggroGroup = instigator.GetComponent<EnemyStateMachine>()?.GetAggroGroup();

            // Find unassigned enemies in this aggroGroup:
            List<EnemyStateMachine> unassignedEnemies = enemies.Where(enemy => enemy != null && enemy.GetAggroGroup() == this && !enemy.HasOpponent).ToList();

            foreach (EnemyStateMachine enemy in enemies)
            {
                // Find the nearest enemy from the attacker's group
                EnemyStateMachine nearestEnemy = null;
                if (attackerAggroGroup != null && attackerAggroGroup != this)
                {
                    nearestEnemy = GetNearestUnassignedEnemy(enemy, attackerAggroGroup.GetGroupMembers());
                    Debug.Log($"nearestEnemy is called from unassigned members of opposing group");
                }
                else
                {
                    nearestEnemy = GetNearestUnassignedEnemy(enemy, unassignedEnemies);
                    Debug.Log($"nearestEnemy is called through unassignedEnemies of this group");
                }

                if (nearestEnemy != null)
                {
                    // Set both enemies as hostile towards each other
                    enemy.SetHostile(true, nearestEnemy.gameObject);
                    enemy.SetOpponent(nearestEnemy.gameObject);
                    enemy.SetHasOpponent(true);
                    nearestEnemy.SetHostile(true, enemy.gameObject);
                    nearestEnemy.SetOpponent(enemy.gameObject);
                    nearestEnemy.SetHasOpponent(true);
                    // unassignedEnemies.Remove(nearestEnemy);
                    Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestEnemy.gameObject.name}, as the nearest unassigned enemy.");
                }
                else
                {
                    // If no enemy found, set closest enemy as opponent
                    EnemyStateMachine closestEnemy = GetClosestEnemy(enemy);
                    if (closestEnemy != null)
                    {
                        // Set both enemies as hostile towards each other
                        enemy.SetHostile(true, closestEnemy.gameObject);
                        enemy.SetOpponent(closestEnemy.gameObject);
                        enemy.SetHasOpponent(true);
                        closestEnemy.SetHostile(true, enemy.gameObject);
                        closestEnemy.SetOpponent(enemy.gameObject);
                        closestEnemy.SetHasOpponent(true);
                        Debug.Log($"{enemy.gameObject.name} is now hostile towards {closestEnemy.gameObject.name} as the closest enemy.");
                    }
                }
            }
        }

        EnemyStateMachine GetNearestUnassignedEnemy(EnemyStateMachine enemy, List<EnemyStateMachine> unassignedEnemies)
        {
            EnemyStateMachine nearestUnassignedEnemy = null;
            float nearestDistance = Mathf.Infinity;

            foreach (EnemyStateMachine unassignedEnemy in unassignedEnemies) 
            {
                if (unassignedEnemy != null && !unassignedEnemy.GetHasOpponent() && unassignedEnemy.GetAggroGroup() != this) 
                {
                    float distance = Vector3.Distance(enemy.transform.position, unassignedEnemy.transform.position);
                    if (distance < nearestDistance) 
                    {
                        nearestDistance = distance;
                        nearestUnassignedEnemy = unassignedEnemy;
                    }
                }
            }
            return nearestUnassignedEnemy;
        }

        EnemyStateMachine GetClosestEnemy(EnemyStateMachine enemy)
        {
            EnemyStateMachine closestEnemy = null;
            float closestDistance = Mathf.Infinity;

            foreach (EnemyStateMachine otherEnemy in enemies)
            {
                if (otherEnemy != null && otherEnemy != enemy && otherEnemy.GetAggroGroup() != this)
                {
                    float distance = Vector3.Distance(enemy.transform.position, otherEnemy.transform.position);
                    if (distance < closestDistance)
                    {
                        closestEnemy = otherEnemy;
                        closestDistance = distance;
                    }
                }
            }
            return closestEnemy;
        }
        // --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    }
}

‘RespawnManager.cs’ (this is the script where I want to make the changes, within the ‘Test Area’ in ‘Respawn()’, under the assignment of Patrol Paths. All I’m trying to do, is get the hostility of any non-Initially Hostile enemies to be hostile, if one of their group members is under attack, and they are not assigned to be fighting anyone):

using System.Collections.Generic;
using GameDevTV.Saving;
using RPG.Control;
using UnityEngine;
using RPG.Combat;
using Newtonsoft.Json.Linq;
using RPG.States.Enemies;
using System.Collections;
using System;

namespace RPG.Respawnables
{
    [RequireComponent(typeof(JSONSaveableEntity))]
    public class RespawnManager : MonoBehaviour, IJsonSaveable
    {
        [SerializeField] EnemyStateMachine spawnableEnemy;  // prefab of the enemy (was 'AIController.cs' type previously)

        [SerializeField] private float hideTime;   // time before hiding our dead character
        [SerializeField] private float respawnTime;    // time before respawning our hidden dead character, as another alive, respawned character
        [SerializeField] PatrolPath patrolPath; // the path our character will follow, from his spawn point
        [SerializeField] AggroGroup aggroGroup; // aggrevated group of guards, based on wrong dialogue player has said
        [SerializeField] bool hasBeenRestored;  // checks if the enemy has been restored before fading in from the main menu, or a load scene, or not

        // private AIController spawnedEnemy; // changed to EnemyStateMachine type below
        // private EnemyStateMachine spawnedEnemy; // in-game instance of the enemy

        // TEST (TimeKeeper)
        private double destroyedTime;
        private TimeKeeper timeKeeper;

        // --------------------------- NOTE: RestoreState() occurs BEFORE Start(), hence we need to change Start() to Awake() --------------
        private void Awake()
        {
            // TEST
            timeKeeper = TimeKeeper.GetTimeKeeper();

            // Check if the Enemy has been restored first or not, prior to Respawning him (ensuring RestoreState(), which occurs first, works properly)
            if (!hasBeenRestored) Respawn();
        }
        // --------------------------------------------------------------------------------------------------------------------------------

        private void Respawn()
        {
            var spawnedEnemy = GetSpawnedEnemy();
            if (spawnedEnemy)
            {
                // Dude is not dead no longer, so delete his previous 'onDeath' record after he's respawned
                spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
            }

            foreach (Transform child in transform)
            {
                // Start the Respawn by deleting any existing gameObjects
                Destroy(child.gameObject);
            }

            // Respawn the enemy, and parent the enemy to our respawnManagers' transform
            spawnedEnemy = Instantiate(spawnableEnemy, transform);

            // (TEST BLOCK BELOW, ADDED BY BAHAA INDIVIDUALLY - SUCCESS):
            // If the enemy has a weapon that's supposed to be in his hand, make him wear it:
            if (spawnedEnemy.GetComponent<Fighter>().GetCurrentWeaponConfig() != null)
            {
                WeaponConfig enemyWeaponConfig = spawnedEnemy.GetComponent<Fighter>().currentWeaponConfig;
                spawnedEnemy.GetComponent<Fighter>().AttachWeapon(enemyWeaponConfig);
            }

            // Get the spawned/respawned enemies' health, and listen for death notifications
            spawnedEnemy.Health.onDie.AddListener(OnDeath);

            if (patrolPath != null)
            {
                // THE PROBLEM OF HOSTILITY IS COMING FROM HERE, ACCESSING THE PATROLLING PATH BEFORE DEALING WITH AGGROGROUPS!!!
                // (SO FIX THE IF-STATEMENT SURROUNDING THIS CODE...!)
                Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");
                spawnedEnemy.AssignPatrolPath(patrolPath);
                spawnedEnemy.SwitchState(new EnemyIdleState(spawnedEnemy));
            }
            else
            {
                Debug.Log($"No Patrol Path to assign");
            }

            // --------------------------- Extra Functionality: Setting up Aggro Group + Adding Fighters ---------------
            if (aggroGroup != null)
            {
                aggroGroup.AddFighterToGroup(spawnedEnemy);

                // -------------------------------- TEST AREA ------------------------------------------------------------------------------------------------------------------------

                // get the AggroGroup on Respawn, so when the enemy returns to life, he can go through the list of allies, and if any of them are under attack, he can try fight with them
                spawnedEnemy.SetAggroGroup(spawnedEnemy.GetAggroGroup());

                if (spawnedEnemy.GetAggroGroup() != null)
                {
                    foreach (EnemyStateMachine allyEnemy in spawnedEnemy.GetAggroGroup().GetGroupMembers())
                    {
                        if (allyEnemy != spawnedEnemy && allyEnemy.GetOpponent() != null && allyEnemy.GetOpponent().GetComponent<EnemyStateMachine>().GetAggroGroup() != spawnedEnemy.GetAggroGroup())
                        {
                            // aim for whoever is attacking your allies, and then break after finding the first one:
                            spawnedEnemy.SetOpponent(allyEnemy.GetOpponent());
                            spawnedEnemy.SetHasOpponent(true);
                            spawnedEnemy.SetHostile(true, allyEnemy.GetOpponent());
                            Debug.Log($"Enemy Respawned and is supposed to fight... SetHostile set to {spawnedEnemy.IsHostile}");
                            break;
                        }
                    }
                }

                // -------------------------------------- END OF TEST AREA -----------------------------------------------------------------------------------------------------------

                if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro)) //aggrogroup is at this point valid
                {
                    dialogueAggro.SetAggroGroup(aggroGroup);
                }
            }
            // ---------------------------------------------------------------------------------------------------------
        }

        private IEnumerator HideCharacter()
        {
            var spawnedEnemy = GetSpawnedEnemy();
            if (spawnedEnemy == null) yield break;
            spawnedEnemy.transform.SetParent(null);
            yield return new WaitForSecondsRealtime(hideTime);
            Destroy(spawnedEnemy.gameObject);
        }

        void OnDeath()
        {
            var spawnedEnemy = GetSpawnedEnemy();
            spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
            spawnedEnemy.SwitchState(new EnemyDeathState(spawnedEnemy)); // THIS LINE IS A TEST (Brutally forcing enemyDeathState if the enemy is dead)
            StartCoroutine(HideCharacter());

            destroyedTime = timeKeeper.GetGlobalTime();
            StartCoroutine(WaitAndRespawn());

            if (aggroGroup != null)
            {
                aggroGroup.RemoveFighterFromGroup(spawnedEnemy);
            }
        }

        private IEnumerator WaitAndRespawn()
        {
            var elapsedTime = (float)(timeKeeper.GetGlobalTime() - destroyedTime);
            yield return new WaitForSecondsRealtime(respawnTime - elapsedTime);
            Respawn();
        }

        private bool IsDead()
        {
            var spawnedEnemy = GetSpawnedEnemy();
            return spawnedEnemy == null || spawnedEnemy.Health.IsDead();
        }

        private EnemyStateMachine GetSpawnedEnemy()
        {
            return GetComponentInChildren<EnemyStateMachine>();
        }

        public JToken CaptureAsJToken()
        {
            JObject state = new JObject();
            IDictionary<string, JToken> stateDict = state;

            // TEST (Adding data to the JObject Dictionary):
            var isDead = IsDead();
            var data = new RespawnData(destroyedTime, isDead);
            stateDict["RespawnData"] = JToken.FromObject(data);

            // we only care about data of alive enemies
            if (!isDead)
            {
                var spawnedEnemy = GetSpawnedEnemy();
                foreach (IJsonSaveable JSONSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
                {
                    JToken token = JSONSaveable.CaptureAsJToken();
                    string component = JSONSaveable.GetType().ToString();
                    Debug.Log($"{name} Capture {component} = {token.ToString()}");
                    stateDict[component] = token;
                }
            }
            return state;
        }

        public void RestoreFromJToken(JToken s)
        {
            
            JObject state = s.ToObject<JObject>();
            IDictionary<string, JToken> stateDict = state;

            var data = default(RespawnData);
            if (stateDict.TryGetValue("RespawnData", out var dataToken))
            {
                data = dataToken.ToObject<RespawnData>();
            }

            var isDead = data.IsDead;
            destroyedTime = data.DestroyedTime;

            // Should be dead
            if (isDead && !IsDead())
            {
                Debug.Log("Should be dead, but isn't...");
                var spawnedEnemy = GetSpawnedEnemy();
                Debug.Log($"Listeners before: {spawnedEnemy.Health.onDie.GetPersistentEventCount()}");
                spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
                Debug.Log($"Listeners after: {spawnedEnemy.Health.onDie.GetPersistentEventCount()}");
                Debug.Log($"Health Before: {spawnedEnemy.Health.GetHealthPoints()}");
                spawnedEnemy.Health.Kill();
                Debug.Log($"Health After: {spawnedEnemy.Health.GetHealthPoints()}");
                StartCoroutine(WaitAndRespawn());
                if (aggroGroup != null)
                {
                    aggroGroup.RemoveFighterFromGroup(spawnedEnemy);
                }
                StartCoroutine(HideCharacter());
                // HideCharacter();
                Debug.Log($"Spawned Enemy: {GetSpawnedEnemy()}");
            }
            else if (isDead && IsDead())
            {
                Debug.Log("Should be dead, and is indeed dead...");
                StopAllCoroutines();
                StartCoroutine(WaitAndRespawn());
                StartCoroutine(HideCharacter());
                // HideCharacter();
            }
            // Should be alive
            else if (!isDead && IsDead())
            {
                Debug.Log("Shouldn't be dead, but is dead...");
                Respawn();
                LoadEnemyState(stateDict);
            }
            else
            {
                Debug.Log("Shouldn't be dead, and isn't dead...");
                LoadEnemyState(stateDict);
            }
        }

        private void LoadEnemyState(IDictionary<string, JToken> stateDict)
        {
            var spawnedEnemy = GetSpawnedEnemy();
            foreach (IJsonSaveable jsonSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
            {
                string component = jsonSaveable.GetType().ToString();
                if (stateDict.ContainsKey(component))
                {
                    // NORMAL CODE (Don't delete if test failed):
                    Debug.Log($"{name} Restore {component} => {stateDict[component].ToString()}");
                    jsonSaveable.RestoreFromJToken(stateDict[component]);
                }
            }
        }
    }

    [Serializable]
    public struct RespawnData
    {
        public bool IsDead;
        public double DestroyedTime;

        public RespawnData(double destroyedTime, bool isDead)
        {
            IsDead = isDead;
            DestroyedTime = destroyedTime;
        }
    }
}

and ‘EnemyStateMachine.cs’ (this one will be quite important in countless aspects, so I placed the entire script here):

using RPG.Attributes;
using RPG.Combat;
using RPG.Control;
using RPG.Movement;
using RPG.Stats;
using UnityEngine;
using UnityEngine.AI;
using RPG.Core;
using System.Linq;
using RPG.States.Player;
using Unity.VisualScripting.Dependencies.Sqlite;

namespace RPG.States.Enemies {

public class EnemyStateMachine : StateMachine, ITargetProvider
{
    [field: SerializeField] public Animator Animator {get; private set;}
    [field: SerializeField] public CharacterController CharacterController {get; private set;}
    [field: SerializeField] public ForceReceiver ForceReceiver {get; private set;}
    [field: SerializeField] public NavMeshAgent Agent {get; private set;}
    [field: SerializeField] public PatrolPath PatrolPath {get; private set;}
    [field: SerializeField] public Fighter Fighter {get; private set;}
    [field: SerializeField] public BaseStats BaseStats {get; private set;}
    [field: SerializeField] public Health Health {get; private set;}
    [field: SerializeField] public CooldownTokenManager CooldownTokenManager {get; private set;}


    [field: SerializeField] public float FieldOfViewAngle {get; private set;} = 90.0f;
    [field: SerializeField] public float MovementSpeed {get; private set;} = 4.0f;
    [field: SerializeField] public float RotationSpeed {get; private set;} = 45f;
    [field: SerializeField] public float CrossFadeDuration {get; private set;} = 0.1f;
    [field: SerializeField] public float AnimatorDampTime {get; private set;} = 0.1f;

    [field: Tooltip("When the player dies, this is where he respawns to")]
    [field: SerializeField] public Vector3 ResetPosition {get; private set;}

    // TEST - Aggrevating others in the 'PlayerChaseRange' range:
    [field: Tooltip("Regardless of whether the enemy is part of an AggroGroup or not, activating this means every enemy in 'PlayerChasingRange', surrounding the attacked enemy, will aggrevate towards the player for 'CooldownTimer' seconds the moment the enemy is hit, and then they'll be quiet if the player doesn't fight them back. If the player attacks the enemies though, the longer the fight goes on for, the longer they will aggrevate towards him (because CooldownTimers' append is set to true))")]
    [field: SerializeField] public bool AggrevateOthers { get; private set; } = false;

    // TEST - Testing for Aggrevated Enemy toggling (called in 'OnTakenHit' below, 'EnemyPatrolState.cs', 'EnemyDwellState.cs' and 'EnemyIdleState.cs'):
    [field: Tooltip("Is this Enemy supposed to hate and want to kill the player?")]
    [field: SerializeField] public bool IsHostile {get; private set;} // for enemies that have hatred towards the player
    [field: Tooltip("For how long further will the enemy be mad and try to attack the player?")]
    [field: SerializeField] public float CooldownTimer {get; private set;} = 3.0f; // the timer for which the enemies will be angry for
    
    // TEST - Who is the Last Atacker on this enemy? (NPC Fights):
    [field: Tooltip("The last person who attacked this enemy, works with 'OnDamageInstigator' in 'Health.TakeDamage()'")]
    public GameObject LastAttacker {get; private set;}

    // TEST - How many accidental hits can the AggroGroup member take in, before fighting back:
    [field: Tooltip("How many accidental hits can this enemy take, from any friend in the same AggroGroup, before eventually fighting back? (hard-coded limit to 1 accidental hit. Second one triggers a fight). Handled in 'EnemyAttackingState.cs'\n(Will reset if another NPC/Player attack this enemy, or the LastAttacker is dead\n(DO NOT TOUCH, IT AUTOMATICALLY UPDATES ITSELF!))")]
    [field: SerializeField] public int accidentHitCounter {get; private set;}

    // Function:
    public bool IsAggro => CooldownTokenManager.HasCooldown("Aggro"); // enemies are aggrevated through a timer-check. In other words, they can only be angry for some time

    public float PlayerChasingRangedSquared {get; private set;}
    public GameObject Player {get; private set;}

    public Blackboard Blackboard = new Blackboard();

    [field: Tooltip("When the game starts, is this enemy, by default nature, angry towards the player?")]
    [field: SerializeField] public bool InitiallyHostile {get; private set;}

    [field: Header("Don't bother trying to change this variable anymore.\nWhen the game starts, It's controlled by either 80% of the Target Range of the Enemies' Weapon,\nor 100% of the weapon Range of the Enemies' Weapon, depending on who is higher.\nAll changes are done in 'EnemyStateMachine.HandleWeaponChanged,\nand subscribed to 'Fighter.OnWeaponChanged' in 'EnemyStateMachine.Awake'")]
    [field: SerializeField] public float PlayerChasingRange { get; private set; } = 10.0f;

    [field: Header("Do NOT touch this variable, it is here for debugging and is automatically updating itself!")]
    [field: SerializeField] public AggroGroup AggroGroup { get; private set; }

    [field: Header("Does this enemy have an opponent, or should an aggrevated NPC hunting enemies in the aggroGroup of this enemy hunt him down?")]
    [field: SerializeField] public bool HasOpponent {get; private set;}
    [field: Header("Who is the opponent this NPC is facing?")]
    [field: SerializeField] public GameObject Opponent {get; private set;}

    [field: Tooltip("AUTOMATICALLY UPDATES... This variable tells the developer who the enemy aggroGroup is, based on who this enemy is attacking, so the rest can follow lead (based on this variable from the first enemy on the list)")]
    [field: SerializeField] public AggroGroup EnemyAggroGroup {get; private set;}

    // This event is triggered in 'EnemyChasingState.cs', when the player leaves the enemies' Chasing Range, so he can go hunt down other enemies:
    public event System.Action<EnemyStateMachine> OnPlayerOutOfChaseRange;

    private void OnValidate()
    {
        if (!Animator) Animator = GetComponentInChildren<Animator>();
        if (!CharacterController) CharacterController = GetComponent<CharacterController>();
        if (!ForceReceiver) ForceReceiver = GetComponent<ForceReceiver>();
        if (!Agent) Agent = GetComponent<NavMeshAgent>();
        if (!Fighter) Fighter = GetComponent<Fighter>();
        if (!BaseStats) BaseStats = GetComponent<BaseStats>();
        if (!Health) Health = GetComponent<Health>();
        if (!CooldownTokenManager) CooldownTokenManager = GetComponent<CooldownTokenManager>();
    }

    private void Awake() 
    {
        if (Fighter) Fighter.OnWeaponChanged += HandleWeaponChanged;
    }

    private void Start() 
    {
        // Initially, enemy hostility = enemy initialHostility
        IsHostile = InitiallyHostile;

        Agent.updatePosition = false;
        Agent.updateRotation = false;

        ResetPosition = transform.position;

        PlayerChasingRangedSquared = PlayerChasingRange * PlayerChasingRange;
        Player = GameObject.FindGameObjectWithTag("Player");
        Blackboard["Level"] = BaseStats.GetLevel(); // immediately stores the enemies' combat level in the blackboard (useful for determining the chances of a combo, based on the enemies' attack level, in 'EnemyAttackingState.cs', for now...)
        
        // The following check ensures that if we kill an enemy, save the game, quit and then return later, he is indeed dead
        // (the reason it's here is because 'RestoreState()' happens between 'Awake()' and 'Start()', so to counter for the delay, we do it in start too)
        if (Health.IsDead()) SwitchState(new EnemyDeathState(this));
        else SwitchState(new EnemyIdleState(this));

        // When the script holder is killed by another NPC, from another AggroGroup, this event is called:
        Health.onDie.AddListener(OnDie);

        Health.onDie.AddListener(() => 
        {
            SwitchState(new EnemyDeathState(this));
        });

        /* Health.onResurrection.AddListener(() => {
            Debug.Log("onResurrection Invoked");
            SwitchState(new EnemyIdleState(this));
        }); */

        Health.OnDamageTaken += () => 
        {
            // TEST (if statement and its contents only - the else statement has nothing to do with this):
            if (AggrevateOthers)
            {
            foreach (Collider collider in Physics.OverlapSphere(transform.position, PlayerChasingRange).Where(collider => collider.TryGetComponent(out EnemyStateMachine enemyStateMachine)))
            {
                collider.GetComponent<EnemyStateMachine>().CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true);
            }
            }
            // CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true);
        };

        if (AggroGroup == null) GetAggroGroup();

        // The enemy will aim next to whoever attacked him last (if it's the player, aim for him. If it's another enemy, aim for him too)
        Health.OnDamageInstigator += SetLastAttacker;
        ForceReceiver.OnForceApplied += HandleForceApplied;

    }

    public void TriggerOnPlayerOutOfChaseRange()
    {
        OnPlayerOutOfChaseRange?.Invoke(this);
        Debug.Log("OnPlayerOutOfChaseRange event invoked");

            // if the opponent was the player before we got out of range, 
            // reset the data so the enemy can go find someone else to fight
            // (Something is wrong here... Redo this part when given the chance):
            /* if (Opponent != null && Opponent.GetComponent<PlayerStateMachine>())
            {
                SetHostile(GetInitialHostility);
                SetOpponent(null);
                SetHasOpponent(false);
            } */
    }

    /// <summary>
    /// in NPC fights between AggroGroups, when one enemy dies, this function is executed
    /// (so that the NPC can start seeking other opponents)
    /// </summary>
    public void OnDie()
    {
        // all enemies who had this guy set to an opponent should erase the data, so they can go targeting again after his death:
            foreach (EnemyStateMachine enemy in FindObjectsOfType<EnemyStateMachine>())
            {
                if (enemy.GetOpponent() == gameObject) {
                if (enemy.GetInitialHostility) {enemy.SetHostile(true);} else {enemy.SetHostile(false);}
                enemy.SetHasOpponent(false);
                enemy.SetOpponent(null);
                }
            }

        // solve the following problem: When this object dies, the LastAttacker will get the EnemyAggroGroup, and search for other enemies to attack:
        
        // if the Last Attacker is still alive, but I am dead, then he can get my AggroGroup:
        if (LastAttacker != null && LastAttacker.GetComponent<EnemyStateMachine>()) 
        {
            LastAttacker.GetComponent<EnemyStateMachine>().EnemyAggroGroup = GetAggroGroup();

            // When you got the AggroGroup, check for the LastAttacker. When you found the LastAttacker that killed me, assuming he's alive and he's my enemy, he can get the next enemy to kill:
            foreach (EnemyStateMachine enemy in FindObjectsOfType<EnemyStateMachine>())
            {
                if (enemy == LastAttacker.GetComponent<EnemyStateMachine>() && !enemy.GetComponent<Health>().IsDead() && enemy.GetAggroGroup() != GetAggroGroup())
                {
                    // if the enemy that killed me, from the opposing AggroGroup, is still alive after my death,
                    // he shall find the closest enemy from my team, and aim for him:
                    enemy.SetHostile(true, GetClosestEnemyInEnemyAggroGroup(enemy).gameObject);
                }
            }
        }

        // if my killer is still alive, he shall erase my data after my death:
        if (LastAttacker != null && LastAttacker.GetComponent<EnemyStateMachine>())
        {
            LastAttacker.GetComponent<EnemyStateMachine>().ClearOpponent();
            LastAttacker.GetComponent<EnemyStateMachine>().SetHasOpponent(false);
            LastAttacker.GetComponent<EnemyStateMachine>().ClearEnemyAggroGroup();
            LastAttacker.GetComponent<EnemyStateMachine>().SetHostile(LastAttacker.GetComponent<EnemyStateMachine>().GetInitialHostility, gameObject);
            LastAttacker.GetComponent<EnemyStateMachine>().SwitchState(new EnemyIdleState(LastAttacker.GetComponent<EnemyStateMachine>()));
        }
    }

    public EnemyStateMachine GetClosestEnemyInEnemyAggroGroup(EnemyStateMachine enemy)
    {
        EnemyStateMachine closestEnemy = null;
        float closestDistance = Mathf.Infinity;

        foreach (EnemyStateMachine otherEnemy in FindObjectsOfType<EnemyStateMachine>()) 
        {
            if (otherEnemy != null && otherEnemy != enemy && otherEnemy.GetAggroGroup() != enemy.GetAggroGroup()) 
            {
                float distance = Vector3.Distance(enemy.transform.position, otherEnemy.transform.position);
                if (distance < closestDistance) 
                {
                    closestEnemy = otherEnemy;
                    closestDistance = distance;
                }
            }
        }
        return closestEnemy;
    }

    EnemyStateMachine GetNearestUnassignedEnemyInEnemyAggroGroup(EnemyStateMachine enemy) 
    {
        EnemyStateMachine nearestUnassignedEnemyInEnemyAggroGroup = null;
        float nearestDistance = Mathf.Infinity;

        foreach (EnemyStateMachine otherEnemy in FindObjectsOfType<EnemyStateMachine>()) 
        {
            if (otherEnemy != null && otherEnemy != enemy && otherEnemy.GetAggroGroup() != enemy.GetAggroGroup())
            {
                float distance = Vector3.Distance(enemy.transform.position, otherEnemy.transform.position);
                if (distance < nearestDistance) 
                {
                    nearestUnassignedEnemyInEnemyAggroGroup = otherEnemy;
                    nearestDistance = distance;
                }
            }
        }
        return nearestUnassignedEnemyInEnemyAggroGroup;
    }

    AggroGroup ClearEnemyAggroGroup()
    {
        return EnemyAggroGroup = null;
    }

    public void AssignPatrolPath(PatrolPath newPatrolPath)
    {
        PatrolPath = newPatrolPath;
    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, PlayerChasingRange);
    }

    private void OnEnable()
    {
        Health.OnTakenHit += OnTakenHit;
    }

    private void OnDisable()
    {
        Health.OnTakenHit -= OnTakenHit;
    }

    private void OnTakenHit(GameObject instigator)
    {
        if (instigator == null) return; // nobody touched you... you can't take a hit that way!

        if (GetOpponent() != instigator && !instigator.GetComponent<PlayerStateMachine>())
        {
            // if you got hit by a new instigator (who is not the player), wipe out the old data and prepare for the new one:
            SetHostile(false);
            SetOpponent(null);
            SetHasOpponent(false);
            ClearEnemyAggroGroup();

            // when hit, both of you should aim for one another:
            if (instigator != null && instigator.GetComponent<EnemyStateMachine>()) 
            {
                SetHostile(true, instigator.gameObject);
                SetOpponent(instigator.gameObject);
                SetHasOpponent(true);
                instigator.GetComponent<EnemyStateMachine>().SetHostile(true, this.gameObject);
                instigator.GetComponent<EnemyStateMachine>().SetOpponent(this.gameObject);
                instigator.GetComponent<EnemyStateMachine>().SetHasOpponent(true);

                // if the instigator is part of an AggroGroup, get the data of that group (otherwise return null):
                /* if (instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() != null)
                EnemyAggroGroup = instigator.GetComponent<EnemyStateMachine>().GetAggroGroup();
                else EnemyAggroGroup = null; */
            }
        }

        CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true); // 'true' in the end basically allows the enemy to add up to his total anger time
        if (!HasOpponent) SetHasOpponent(true);
        if (Opponent == null) SetOpponent(instigator.gameObject);
    }

    // public bool IsAggresive => CooldownTokenManager.HasCooldown("Aggro");

    private void HandleForceApplied(Vector3 force) 
    {
        if (Health.IsDead()) return;
        // Disable comments below if you want impact states to be random for the enemy:
        // float forceAmount = Random.Range(0f, force.sqrMagnitude);
        // if (forceAmount > Random.Range(0f, BaseStats.GetLevel())) {
        SwitchState(new EnemyImpactState(this));
        // }
    }

    public GameObject GetTarget()
    {
        // TEST: If the last attacker is not the player, then that's the target:
        if (LastAttacker != GameObject.FindWithTag("Player")) return LastAttacker;
        // the target for the enemy launching a projectile (which is basically the player in this case)
        return Player;
    }

    /* public void SetHostile(bool IsHostile) 
    {
        this.IsHostile = IsHostile;
    } */

    /// <summary>
    /// Who is this NPC hostile towards, and which AggroGroup do they belong to...?!
    /// </summary>
    /// <param name="IsHostile"></param>
    /// <param name="attacker"></param>
    public void SetHostile(bool IsHostile, GameObject attacker = null)
    {
        this.IsHostile = IsHostile;
        if (LastAttacker != null) ClearLastAttacker();
        
        if (attacker != null)
        {
        SetLastAttacker(attacker);
        // (TEST) get the enemyAggroGroup, so that we can easily access the enemies and get each individual enemy in an AggroGroup to hunt a single guy:
        // if the attacker is the player, or an enemy with no aggroGroup, or a dead instigator (enemy/player), return 'EnemyAggroGroup' = null:
        if (attacker.GetComponent<PlayerStateMachine>() || attacker.GetComponent<EnemyStateMachine>().GetAggroGroup() == null || attacker.GetComponent<Health>().IsDead()) EnemyAggroGroup = null;
        // if it's an enemy with an AggroGroup, get that AggroGroup:
        else EnemyAggroGroup = attacker.GetComponent<EnemyStateMachine>().GetAggroGroup();
        }
    }

    public bool GetInitialHostility => InitiallyHostile;

    public void _ResetPosition() 
        {
        Agent.enabled = false;
        CharacterController.enabled = false;
        transform.position = ResetPosition;
        Agent.enabled = true;
        CharacterController.enabled = true;
        }

    // The code below changes the 'PlayerChasingRange' based on the weapon in-hand. I want the 'PlayerChasingRange' to be what I want, so I commented it out:
    private void HandleWeaponChanged(WeaponConfig weaponConfig) 
    {
        if (weaponConfig) 
        {
            PlayerChasingRange = Mathf.Max(weaponConfig.GetTargetRange() * 0.8f, weaponConfig.GetWeaponRange()); // keep the range of pursuit of the enemy below the players' targeting range
            PlayerChasingRangedSquared = PlayerChasingRange * PlayerChasingRange;
        }
    }

    // TEST: Who was the last attacker against the enemy?
    public void SetLastAttacker(GameObject instigator) 
    {
        // if the caller is 'SetHostile', there's a chance it's calling it on itself, because the AggroGroup
        if (instigator == gameObject) return;

        // if whoever the enemy was aiming for is dead, get them off the 'onDie' event listener:
        if (LastAttacker != null) {LastAttacker.GetComponent<Health>().onDie.RemoveListener(ClearLastAttacker);}

        // if the last attacker is not dead, add them to the list, and then prepare them to be deleted off the list when they die:
        LastAttacker = instigator;
        LastAttacker.GetComponent<Health>().onDie.AddListener(ClearLastAttacker);
    }

    // TEST: Use this function when whoever the enemy was aiming for, is killed, or got killed by (in 'SetLastAttacker' above):
    public void ClearLastAttacker()
    {
        // if the enemy is dead, clear him off the 'LastAttacker', clear the cooldown timer, return to idle state, and ensure no accident hit counter on record (he will forget about what that guy did...):
        LastAttacker = null;
        CooldownTokenManager.SetCooldown("Aggro", 0f);
        SwitchState(new EnemyIdleState(this));
        ClearAccidentHitCounter();
    }

    public AggroGroup GetAggroGroup() 
    {
        if (AggroGroup == null) return null;
        return AggroGroup;
    }

    public void SetAggroGroup(AggroGroup aggroGroup)
    {
        this.AggroGroup = aggroGroup;
    }

    public AggroGroup ClearAggroGroup() 
    {
        return AggroGroup = null;
    }

    // Called in 'EnemyAttackingState.Tick()', to ensure enemies don't accidentally hit one another for too long
    public int AddToAccidentHitCounter() 
    {
        return accidentHitCounter++;
    }

    public int ClearAccidentHitCounter() 
    {
        return accidentHitCounter = 0;
    }

    public bool GetHasOpponent() 
    {
        return HasOpponent;
    }

    public GameObject GetOpponent()
    {
        return Opponent;
    }

    public void SetHasOpponent(bool hasOpponent)
    {
        Debug.Log("SetHasOpponent Called");
        this.HasOpponent = hasOpponent;
    }

    public void ClearOpponent() 
    {
        this.Opponent = null;
    }

    /// <summary>
    /// Special Function, created for when the player is in Chase Range of AggroGroup enemies (to avoid enemies far away from chasing the player down)
    /// (ONLY USED IN AggroGroup.OnTakenHit() so far)
    /// </summary>
    /// <returns></returns>
    public bool IsInPlayerChasingRange() 
    {
        return Vector3.SqrMagnitude(transform.position - Player.transform.position) <= PlayerChasingRangedSquared;
    }

    /// <summary>
    /// Who is the opponent of this NPC?
    /// </summary>
    /// <param name="HasOpponent"></param>
    /// <param name="attacker"></param>
    public void SetOpponent(GameObject attacker = null)
    {
        Debug.Log("SetOpponent called");
        Opponent = attacker;
    }

    public AggroGroup GetEnemyAggroGroup()
    {
        return EnemyAggroGroup;
    }

    }
}

Again, I apologize for dragging you into this, I truly am sorry, but there’s nothing in my capabilities that I can do about it right now… PLEASE HELP ME OUT :slight_smile:

Just to make your life a little easier as well, there’s only two spots in my entire code that sets the enemy hostility to false (which I also tried tuning, but it did not work). Both are in ‘EnemyStateMachine.cs’:

// 1. in 'OnDie':
if (enemy.GetInitialHostility) {enemy.SetHostile(true);} else {enemy.SetHostile(false);}

// 2. At the top of 'OnTakenHit':
SetHostile(false);

So far, the only way for me to get an enemy to be hostile, is to actually turn it on in the Inspector itself, and then he reacts… (but even that is flawed, because occasionally, my hostile enemy would also just go and attack members of the opponent group out of thin air for no apparent reason)

If you spot any other flaws in my code (I’m sure there’s a few), please PLEASE let me know about it and how to fix it. I’d very much appreciate that :slight_smile:

(I already know this system probably introduced a death glitch somewhere, because occasionally my enemies will resurrect during their time before hiding the character, and rarely I would be dead when I respawn as well… I want to address this next. For now, let’s just take it step by step)


Edit (a few hours later): Something else I did notice can potentially solve the problem, is editing ‘IsHostile’ in ‘EnemyIdleState.Enter’ itself. In other words, “if you can’t find where to fix it, override it at the gate”. So here is my ‘EnemyIdleState.cs’, unmodified:

using RPG.States.Enemies;

public class EnemyIdleState : EnemyBaseState
{
    
    public EnemyIdleState(EnemyStateMachine stateMachine) : base(stateMachine) {}

    private bool idleSpeedReached;

    public override void Enter()
    {
        if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true;

        if (stateMachine.PatrolPath) // if the enemy has a patrol state, switch to that
        {
            stateMachine.SwitchState(new EnemyPatrolState(stateMachine));
            return;
        }
        
        stateMachine.Animator.CrossFadeInFixedTime(FreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
    }

    public override void Tick(float deltaTime)
    {
        
        Move(deltaTime);
        /* if (IsInChaseRange()) // && IsAggro()) // if you want to consider the optional aggregation, add "&& IsAggro()" to the if statement of this line 
        {
            // if you want the vision system to work, uncomment the if statement below:

            // if (CanSeePlayer()) {
            stateMachine.SwitchState(new EnemyChasingState(stateMachine));
            return;
            // }
        } */

        if (ShouldPursue() || IsAggrevated())
        {
            stateMachine.SwitchState(new EnemyChasingState(stateMachine));
        }
        
        if (!idleSpeedReached) 
        {
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f, stateMachine.AnimatorDampTime, deltaTime);
            // Due to a bug in 'SetFloat()' that can twist values above and beyond what we need, 
            // the following 'if' statement is crucial:
            if (stateMachine.Animator.GetFloat(FreeLookSpeedHash) < 0.05f) 
            {
                stateMachine.Animator.CrossFadeInFixedTime(IdleHash, stateMachine.CrossFadeDuration);
                idleSpeedReached = true;
            }

        }
    
    }

    public override void Exit() {}

}

My idea right now is that since Hostility is somehow set to false from somewhere in the code, and I cant figure out where from, then I will override it to true within ‘EnemyIdleState.cs’, for any respawned enemy, if any of his group members are under attack, UNDER VERY VERY VERY SPECIFIC Rules that can only be met by Respawned Enemies in the same AggroGroup… I’m just struggling to translate that to code

and… this was my final attempt of tuning ‘EnemyIdleState.Enter()’ to try and fix the problem, and at this point in time, I desperately need help, because I tried out EVERYTHING I can think of, but it didn’t work out well for me… The following Code block goes into the very very top of ‘EnemyIdleState.Enter()’:

    // to set non-hostile enemies to be true, if a group member is under attack,
    // first make sure this enemy is part of an AggroGroup, and make sure it's not empty:
    if (stateMachine.GetAggroGroup() != null && stateMachine.GetAggroGroup().GetGroupMembers().Count > 1 && stateMachine.GetComponentInParent<RespawnManager>().HasJustRespawned())
    {
        // if this enemy has an AggroGroup, go through all the enemies in the group
        // (and if one of them is under attack, get the nearest enemy you can find, and attack them):
        foreach (EnemyStateMachine enemy in stateMachine.GetAggroGroup().GetGroupMembers())
        {
            if (enemy.GetOpponent() && enemy.GetHasOpponent()) continue;
            stateMachine.SetHasOpponent(true);
            stateMachine.SetOpponent(stateMachine.GetClosestEnemyInEnemyAggroGroup(stateMachine).gameObject);
            stateMachine.SetHostile(true, stateMachine.GetClosestEnemyInEnemyAggroGroup(stateMachine).gameObject);
        }
    }

To limit everyone from just having ‘SetHostile(true)’ here, I tried introducing new rules for the ‘RespawnManager.cs’, so that only respawned enemies can go through this verification… In simple terms, what I tried to do was ensure that only Respawned Enemies can go through this, and if their friends are under attack and are hostile, and these respawned enemies have no opponent, set the nearest opponent they can find to be their enemy, and then fire away against them, and there’s that

(P.S: “SetHasOpponent()” and “SetOpponent” are what I call as “Dummy variables”. They’re not used in the game itself, but they give me an idea of who is aiming for who, so I understand their behaviour, to have an idea of what still needs to be fixed)

However, it failed… This is where I kindly ask for help because I truly am clueless of how else to flick that ‘SetHostile()’ variable on through code, at the right time (i.e: when you respawned, and your friends are under attack from opposing enemies)


AND… I FOUND WHERE THE BUG WAS COMING FROM, AND I NEVER WOULD HAVE THOUGHT OF IT FOR A THOUSAND YEARS AHEAD IF IT WEREN’T FOR A DEBUGGER IN ‘ENEMYIDLESTATE.CS’

Apparently I forgot to re-assign this line in ‘EnemyStateMachine.Start()’, at the very top of the function:

I DID NOT KNOW THAT THIS SCRIPT WAS BEING EXECUTED EVERY SINGLE TIME OUR ENEMY RESPAWNS, AND THIS LEFT ME CONFUSED FOR A VERY, VERY, VERY, VERY LONG TIME! (I LITERALLY THOUGHT IT ONLY RUNS WHEN THE GAME STARTS…!)

@Brian_Trotter AFTER 3 LONG DAYS, I found the bug above (I still recommend you go through my code above. Because, well, I still have a bunch of new bugs to resolve). I am yet to resolve it (literally just delete that line), but I have a question… why is ‘Start()’ running when my enemy respawns?

Under what conditions does Start() get called?! Because now I am really confused…! I used to think it only runs when the game starts…

Now I have a new bunch of bugs to resolve… Gosh, what a system to create!

Start gets called at the beginning of the first frame that a component exists. When an enemy respawns it is a new game object, so start runs for all of its components before any updates.

1 Like

Oh that’s good to know

well… you guys have a new system to play with now :stuck_out_tongue: - it’s not complete, kinda flawed, still being fixed, but it’s there :slight_smile:

Anyway, off to the next bug… This thing is still kinda buggy, and at this point in time you can bet that I won’t give up on it

The next step I want to take care of, is that if two enemies of the same group are in a fight, and one of them gets hit by an intruder, forget the fight and focus on the intruder’s team

And after that, if an ally has a target you want, unless all other enemies are occupied, ignore that target. If all others are occupied, focus on that target

WHY is this here? at no point should you set hostile to false when you’ve just been hit. Let me tell you, if somebody hits me, I don’t chill out and calm down at the start of my response, I attack the guy what just hit me. I suspect this is your issue. The only time you should be setting IsHostile to false is in the inspector, then overwritten by the AggroGroup when a member is hit. Of course, if your character ISN’T in the AggroGroup then you have to handle that… It’s getting complicated. Definitely, there should be NO SetHostile(false); in any OnTakenHit method.

This is also unneccesary. Once the character is Dead, it doesn’t make any difference if it’s hostile or not, because under zero circumstances should the character be attacking anybody. Unless you’re making a zombie game, the dead are not hostile, they’re dead parrots.


When your Respawn Manager creates the character’s replacement that character can get it’s status from the AggroGroup (assuming it has an aggro group… this spec has a LOT of corner cases.

Break it down into individual sentences, using the bullet feature (type a * on a line and start typing individual sentences) for each feature, because this is hard to read and wrap my brain around. So far, you have had MANY different specs about what the AggroGroups should do, just to add an unrelated feature that is not in the course or my tutorial.

[THIS ONE IS A BIT OF A CHIT CHAT, THE PROBLEM WILL BE ON THE NEXT COMMENT]

umm… Can I mention that I solved (95% of) the entire thing out? After breaking down over a silly bug for 3 days straight, and fixing it up yesterday, I woke up today and fixed the last two corner cases like it’s no big deal… Honestly, this whole thing is slowly boosting my coding confidence :stuck_out_tongue: (still, I’ll need your help down the line, xD - I don’t expect a response for every single thing, but starting points are always nice (all except the bank. That thing is UI, and I am terrible with UI and performance optimization. THAT, is something I really REALLY REALLY need your help with, xD))

As far as I can see right now, literally every corner case has been solved, literally every single one of them (I’ll need a game tester to test it for me though). I’m playing this scenario over and over and over and over again, changing stuff and tuning it, and all seems to be doing just fine

However, I’m 99% sure I have a ton of dead code lying around that just needs a lot of cleaning up, for simplicity’s sake, which will be my goal for the time being

I did not write that :stuck_out_tongue:

At one point I thought cleaning up the record of the previous enemy was essential, so the next one can be tracked down for me to keep track of the data. It seemed to be reset to true the moment the enemy is killed, so it didn’t seem like a big deal… by all means, I shall carefully clean it up

that, is true, but also when everyone in the opposing AggroGroup team is cleaned out, that’s another scenario at which we should set the hostility of everyone back to their default (and I covered that somewhere in my code… It’s either in ‘AggroGroup.cs’ or ‘EnemyStateMachine.cs’… probably ‘AggroGroup.cs’ though)

So basically as we speak, the fight between those enemies will keep on going until one team is completely wiped out. After that, it doesn’t matter who respawns, they ain’t fighting anyway…

again, cleaned it up because at one point I thought cleaning this up would help the code

actually… I dealt with it in a different way. All I did was check for an AggroGroup on the Spawned Enemies’ Respawn Manager, and if true, I developed a specific formula to deal with it accordingly, depending on whether the group is under attack or not

we don’t need this anymore. Like I said, my problem was that I had an initial setter for my players’ Hostility in ‘Start()’, which, I learned last night, gets called when the enemy respawns as well, and it’s what was messing with my hostility variable for a very long time

Instead, I deleted it, and set it when the enemy respawns, in his ‘Respawn()’ function, and all seems to be working just fine (95%, problem on next comment)

1 Like

Gotta say though, I did rely a bit heavily on new functions in Linq I never heard of before… mainly because I’m not very familiar with the library

ANYWAY, Here is my problem. The way I wanted my system to work, was before aiming for the closest enemy you can find, I wanted the enemy to aim for the closest NPC he can find from the opposing AggroGroup, which does not have an enemy. However, the function I developed below is not working for some reason… Please have a look:

/// <summary>
        /// From the list of enemies that have not been assigned an opponent, who is the closest
        /// enemy that this enemy can go and attack?
        /// </summary>
        /// <param name="enemy"></param>
        /// <param name="unassignedEnemies"></param>
        /// <returns>nearestUnassignedEnemy</returns>
        EnemyStateMachine GetNearestUnassignedEnemy(EnemyStateMachine enemy, List<EnemyStateMachine> unassignedEnemies)
        {
            EnemyStateMachine nearestUnassignedEnemy = null;
            float nearestDistance = Mathf.Infinity;

            foreach (EnemyStateMachine unassignedEnemy in unassignedEnemies) 
            {
                if (unassignedEnemy != null && !unassignedEnemy.GetHasOpponent() && unassignedEnemy.GetAggroGroup() != this) 
                {
                    float distance = Vector3.Distance(enemy.transform.position, unassignedEnemy.transform.position);
                    if (distance < nearestDistance) 
                    {
                        nearestDistance = distance;
                        nearestUnassignedEnemy = unassignedEnemy;
                    }
                }
            }
            return nearestUnassignedEnemy;
        }

Is there anything wrong with this function? Because in ‘AggroGroup.cs’ specifically, I don’t think the very last if-statement, which relies on this function, ever gets called… Here’s the code for that as well (I’ll just leave the entire ‘OnTakenHit’ in ‘AggroGroup.cs’, if it helps in anyway… my problem is the very very last ‘if-else’ statement in ‘OnTakenHit’):

void OnTakenHit(GameObject instigator)
        {
            // Check if the instigator is the player
            if (instigator.GetComponent<PlayerStateMachine>())
            {
                // If the instigator is in player chasing range, then all enemies in this AggroGroup, within Player Chasing Range, with no opponent, shall aim for the player (regardless of Hostility):
                foreach (EnemyStateMachine enemy in enemies.Where(enemy => enemy != null && enemy.GetAggroGroup() == this && enemy.IsInPlayerChasingRange() && enemy.GetOpponent() == null))
                {
                    // Enemy must be free, and within range:
                    if (enemy.GetOpponent() == null && !enemy.GetHasOpponent() && enemy.IsInPlayerChasingRange()) {
                    // First, delete the invaded-by-player-group's data, and then get them to focus on the player:
                    enemy.SetHostile(true);
                    enemy.SetOpponent(instigator.gameObject);
                    enemy.SetHasOpponent(true);
                    Debug.Log($"{enemy.gameObject.name} is now hostile towards the player.");
                    }
                }
                return;
            }

            // Get the (instigator) attacker's aggro group
            AggroGroup attackerAggroGroup = instigator.GetComponent<EnemyStateMachine>()?.GetAggroGroup();

            // Find unassigned enemies in this aggroGroup:
            List<EnemyStateMachine> unassignedEnemies = enemies.Where(enemy => enemy != null && enemy.GetAggroGroup() == this && !enemy.HasOpponent).ToList();

            foreach (EnemyStateMachine enemy in enemies)
            {
                // Find the nearest enemy from the attacker's group
                EnemyStateMachine nearestEnemy = null;
                if (attackerAggroGroup != null && attackerAggroGroup != this)
                {
                    nearestEnemy = GetNearestUnassignedEnemy(enemy, attackerAggroGroup.GetGroupMembers());
                    Debug.Log($"nearestEnemy is called from unassigned members of opposing group");
                }
                else
                {
                    nearestEnemy = GetNearestUnassignedEnemy(enemy, unassignedEnemies);
                    Debug.Log($"nearestEnemy is called through unassignedEnemies of this group");
                }

                if (nearestEnemy != null)
                {
                    // Set both enemies as hostile towards each other
                    enemy.SetHostile(true, nearestEnemy.gameObject);
                    enemy.SetOpponent(nearestEnemy.gameObject);
                    enemy.SetHasOpponent(true);
                    nearestEnemy.SetHostile(true, enemy.gameObject);
                    nearestEnemy.SetOpponent(enemy.gameObject);
                    nearestEnemy.SetHasOpponent(true);
                    // unassignedEnemies.Remove(nearestEnemy);
                    Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestEnemy.gameObject.name}, as the nearest unassigned enemy.");
                }
                else
                {
                    // If no enemy found, set closest enemy as opponent
                    EnemyStateMachine closestEnemy = GetClosestEnemy(enemy);
                    if (closestEnemy != null)
                    {
                        // Set both enemies as hostile towards each other
                        enemy.SetHostile(true, closestEnemy.gameObject);
                        enemy.SetOpponent(closestEnemy.gameObject);
                        enemy.SetHasOpponent(true);
                        closestEnemy.SetHostile(true, enemy.gameObject);
                        closestEnemy.SetOpponent(enemy.gameObject);
                        closestEnemy.SetHasOpponent(true);
                        Debug.Log($"{enemy.gameObject.name} is now hostile towards {closestEnemy.gameObject.name}, as the closest enemy.");
                    }
                }
            }
        }

I think I called it here, but if I recall correctly, I don’t think it ever made it through… (and I tried publicizing the ‘unassignedEnemies’ list, but it returned absolutely nothing, hence the problem (or so I guess))

(and I admit, I used ChatGPT a little bit… it didn’t go so far, it was a good starting point, and then a strong reminder as to why I should be the one coding all this stuff on my own… :stuck_out_tongue:)

The reason I actually wanted this to work so badly was because I wanted team-fights in multiple aspects of the game. It could be gangs, cops and robbers (cops on a dragon? why not :stuck_out_tongue:), maybe pirate wars, the players’ team against an NPC team, etc… I’m just trying to create a fancy medieval world here that feels alive


Edit 1, found another corner case. I’ll leave the original solution I made to fix it out, until I properly fix it:

// if two enemies, from the same AggroGroup, are in a fight, and they are attacked by an intruder,
        // and that intruder is not the player, and he's not from the same AggroGroup as this enemy, then cut the fight out and focus on the intruder's AggroGroup:
        if (instigator != LastAttacker && !instigator.GetComponent<PlayerStateMachine>() && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() != GetAggroGroup())
        {
            // for some reason, placing this if statement in the if-statement above introduces
            // an immunity error, so it's here:
            if (LastAttacker != null && LastAttacker.GetComponent<EnemyStateMachine>().GetAggroGroup() == GetAggroGroup())
            {
                // Last Attacker now has a target against whoever hit his rival, which was his friend:
                LastAttacker.GetComponent<EnemyStateMachine>().SetOpponent(LastAttacker.GetComponent<EnemyStateMachine>().GetClosestEnemyInEnemyAggroGroup(this.GetComponent<EnemyStateMachine>()).gameObject);
                LastAttacker.GetComponent<EnemyStateMachine>().SetHasOpponent(true);
                LastAttacker.GetComponent<EnemyStateMachine>().SetHostile(true, LastAttacker.GetComponent<EnemyStateMachine>().GetClosestEnemyInEnemyAggroGroup(this.GetComponent<EnemyStateMachine>()).gameObject);
            }
        }

This one isn’t necessary to fix, but I’m a perfectionist, so… yeah you best bet this one ain’t slipping my hands (although I’m insanely clueless as to how to fix it!)

Right now the problem is that if I take his attention off his enemies, that enemy no longer accepts any damage from any other enemy group… He will take the damage, but no impact state, no death state, nothing… The only damage they will take will either be from the player (since I got their attention) or their own group members, which is a seriously weird bug…


Edit 2: I re-worked ‘EnemyStateMachine.OnTakenHit’ a little bit, and got a bit more accurate results:


        /// <summary>
        /// This function deals with how an enemy responds, basically when he has taken a hit
        /// (is mixed with 'AggroGroup.OnTakenHit' for group fights to respond to group fights).
        /// </summary>
        /// <param name="instigator"></param>
        private void OnTakenHit(GameObject instigator)
        {
            // if the instigator is an outsider enemy, with more than 1 member in his AggroGroup, run this function:
            if (instigator.GetComponent<EnemyStateMachine>() != null && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() != GetAggroGroup() && GetAggroGroup() != null && GetAggroGroup().GetGroupMembers().Count > 1)
            {
                // for every enemy in this group, who is not null, and has either no opponent or is facing a group member of his own group, and is part of this AggroGroup, charge against the enemy:
                foreach (EnemyStateMachine enemy in GetAggroGroup().GetGroupMembers().Where(enemy => enemy != null && (!enemy.GetOpponent() || enemy.GetComponent<EnemyStateMachine>().GetOpponent().GetComponent<EnemyStateMachine>().GetAggroGroup() == GetAggroGroup()) && enemy.GetAggroGroup() == GetAggroGroup()))
                {
                    // Clean up the data, so they can focus on the new enemy:
                    enemy.SetHasOpponent(false);
                    enemy.SetOpponent(null);
                    enemy.SetHostile(false);

                    // Get the new enemy data:
                    enemy.SetHasOpponent(true);
                    enemy.SetOpponent(enemy.GetComponent<EnemyStateMachine>().GetClosestEnemyInEnemyAggroGroup(this.GetComponent<EnemyStateMachine>()).gameObject);
                    enemy.SetHostile(true, enemy.GetComponent<EnemyStateMachine>().GetClosestEnemyInEnemyAggroGroup(this.GetComponent<EnemyStateMachine>()).gameObject);
                }
            }

            // if you're hit by a new instigator, clean your previous data and then aim for the new instigator:
            if (GetOpponent() != instigator && !instigator.GetComponent<PlayerStateMachine>())
            {
                // if you got hit by a new instigator (who is not the player), wipe out the old data and prepare for the new one:
                SetHostile(false);
                SetOpponent(null);
                SetHasOpponent(false);
                ClearEnemyAggroGroup();

                // when hit, both of you should aim for one another:
                if (instigator != null && instigator.GetComponent<EnemyStateMachine>())
                {
                    SetHostile(true, instigator.gameObject);
                    SetOpponent(instigator.gameObject);
                    SetHasOpponent(true);
                    instigator.GetComponent<EnemyStateMachine>().SetHostile(true, this.gameObject);
                    instigator.GetComponent<EnemyStateMachine>().SetOpponent(this.gameObject);
                    instigator.GetComponent<EnemyStateMachine>().SetHasOpponent(true);
                }
            }

            CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true); // 'true' in the end basically allows the enemy to add up to his total anger time
            if (!HasOpponent) SetHasOpponent(true);
            if (Opponent == null) SetOpponent(instigator.gameObject);
        }

For this one though, cleaning previous data was absolutely necessary. Results were terrible otherwise (they’re not perfect now either, but this will do the trick for me to conclude this wild topic… I have a crazier request in my next question)

This fix should make sure that when two enemies are fighting it out, and they’re in the same AggroGroup, and an intruder (i.e: different AggroGroup) kicks in, they will quit the fight and focus on the intruders… That’s what I’ve been trying to accomplish today

OK this may or may not sound like a totally psychotic question, it depends to be honest (you might have a simple solution, whilst I am thinking a million miles away into something else…), but… can we tune the current ‘AggroGroup.cs’ script to allow the player to be part of an AggroGroup, and enemies that are in the same group be his friends?

Would you recommend I even try tuning it, should I inherit one and override a few functions, or just flat out create a brand new solution for that case from scratch? (OH NO… MY NIGHTMARE, NOW I HAVE TO DEAL WITH SAVING SYSTEMS FOR THAT TOO?!)

Just some food for thought… (if I’m trying this out, I’ll do it on my own… I will need help with the saving part though)

I think this one might need a new questionnaire… we are slowly lifting off into wild territory here :slight_smile:

I’m not sure how you gather your unassigned enemies list, but however it’s done, ideally it should already filter out for null and for being in the same aggrogroup.

Additionally, it shouldn’t matter if the other enemy has a target or not. What if that target was YOU? In that case you would still want to consider him in the search. Just imagine standing there while a giant orc is bashing your head in with a club going “nope, he has a target, I’ll look for somebody else”.

Ok, I see where you got unassigned enemies… so um… these things are already covered meaning you shouldn’t need to repeat the check in GetNearestAssignedEnemy.

So I guess what I still need to know is where did you get the enemies in the first place, if the result is apparently an empty list.


Don’t get me wrong, from time to time I’ll use an AI to explain a new feature or remind me about something in the code, but I tend not to let AI write my code for me, as it doesn’t have the full context of your project and what it wants you to accomplish… maybe someday. JetBrains Rider’s new AI seems to do “ok”, but makes mistakes all the time when suggestion code.

Not sure what that means. As I read the code, though, it’s setting the variables only if you’re not already dealing with a LastAttacker already…

That part makes no sense at all. Sounds like you might have missed a ! when checking to see if you were in the same group in TakeDamage or whatever calls TakeDamage.

You think the code is buggy now? Just no… for that you need to do a Faction scenario, which is completely different than Aggrogroups. In fact, at this point, I think we’ve broken the original intent of Sam’s Aggrogroups entirely.

Leaning towards that.

This, however, is getting FAR outside of the topic…

Microsoft’s new Visual Studio’s new AI suggestion feature for code is, hands down, one of the most confusing I’ve ever seen… I already struggle to write code on my own most of the time, the last thing I want is my focus to look at something for even longer and confuse me even further down the line… I’ll eventually search up how to turn it off

That picture was the expected reaction though, so there’s that… Like I said, it doesn’t always do a brilliant job at helping me out, just a good starting point for me to see how I can fix things, and a strong reminder as to why I should be the one coding it (it does a terrible job 90% of the time tbh) :stuck_out_tongue:

I can’t tell if you’re being sarcastic or not right now… :sweat_smile:

ANYWAY, Apart from the fact that somehow, somewhere down the line I disabled the cleaning of the entire group before going back to idle, this entire thing seems to be working just fine for the time being

If you bet that I’m going to try fix that too and get it working until an entire enemy race is wiped off before anyone respawns, you’re probably right

Like Rider, it’s trained off of literally millions of lines of code from public repos. What output it gives you is largely dependent on if it can figure out your intent as you write code, and that’s not easy to do with anybody’s code. It then tries to find a pattern that others with similar code have used. The catch is that it may not have all the information it needs.
On the other hand, and I’m not sure if Visual Studio is set up with this functionality, is that it’s really good at reading code and determining what the code will actually done once it’s written. I sometimes use it to fill out the XML comments in my code.

Both. :slight_smile:

You are doing a LOT better than our earliest posts back when you first started writing these courses, and I don’t want to discourage your growth. I just always want to temper expectations.

Ben and Sam use to have a saying the used all the time, Don’t Make WoW (World of Warcraft). When we first started the RPG series, so many of the students said they wanted a game that could compete with World of Warcraft. What they don’t realize is that WoW wasn’t made by some guy in his room, quite literally thousands of people work on WoW, each with his little piece of the puzzle. And with thousands of employees, they put out regular expansions and new features about once every 2 years. It would take one person doing it by themselves over 1000 years to accomplish what WoW has. I really do encourage starting small, finding a specific thing that is the core and getting the game fantastic with that core experience, and then getting that on itch.io for the feedback. Start to build a community of users and they will tell you what works, what doesn’t, what drives them to play the game and what makes them want to start. Using that feedback, you add the next feature and make that great, making sure not to break the core features of the game.

I’m guessing the ‘XML’ means the comments on your code that are in the ///<summary> ..<summary>'area…?! (the “<” and “>” are gone to explain my point easier)

thank you Brian. By far, the best compliment I heard all day (but I’ll still need your help, ESPECIALLY when it comes to the bank or the saving system… I’ll admit, I don’t understand how either of them works. I’m doing my best with what I know, but I can’t say I know everything in this project just yet)

I tend to struggle with showing people incomplete projects… Hear me out: People will say they want something, and when you give it to them, they’ll say they want something else. Just take a look at Ubisoft and Assassin’s Creed Mirage as an example. People said they want the game back to its core, and when it was given to them, they started complaining that it’s not what they want… It’s a never-ending cycle

That, and more importantly, I HATE making promises to people that I can’t keep up. Why bring up the name of a system that you have no guarantees of completion? For example, if bixarrio didn’t hop in and save me from Crafting, imagine if I told people “Hey, we have crafting coming along”, and I have no clue where to even start, and then I give up… wouldn’t that be terrible of me? More importantly, why would people trust me beyond this point?

I’ll be honest, whatever we can do, we shall do it. If we can’t do something, why make a promise? You have over 40 years of experience above mine, and it’s longer than my age, so I can’t play on the same field as you, respectfully, especially if I’m the only developer on this one :slight_smile:

What I can do instead, is show people an almost-complete project, and then ask for their opinions on it. Right now, it won’t get me anywhere important. It’ll just confuse, overwhelm, and potentially force me to quit

My plan was to first get this project done, my way, and then market it through paid ads and see what people think. Is it feasible? Probably, probably not, depends on who you ask, but it’s one way to get the ball rolling, without making potentially broken promises

Again, I get the project done first, at least the prototype phase (i.e: When I’m on the gate of starting work on the storyline, and the code is over), and only then will I allow people to see it

(This is a bold assumption. I may be right or wrong, but it’s what I learned from my own life experience so far. Please don’t take this seriously, just with a grain of salt) And let people complain all they want. Generally, what people say and what they actually want are two different things… It’s rare to find someone who fully gets it (look at Rockstar for an example… People have been complaining about no GTA 6 for years now, but they still play GTA 5 like crazy. Their strength? They don’t easily fall into temptation)

I know people want true open world freedom, and honestly, this is the goal I’m aiming for (which is why I purchased GAIA when you mentioned the World Streaming Issue), hence why I’m going nuts with these systems that seem to add little to nothing, but require major code changes (I’m breaking the ‘Don’t Make WoW’ rule, I know… At least it ain’t going multiplayer though, so there’s that)

ANYWAY… I didn’t get the opponents to be capable of wiping off the entire race (wiping off whoever hit them is enough for now), so… any updates about the bank? :smiley: (At this point in time, I’d rather leave it working as is than accidentally destroying it all over again by accident…!)

I’ll be gone to study for my exam, which I keep delaying, now that my previous system is done. Kindly keep me updated whenever the bank is ready @Brian_Trotter and I shall proceed after the bank is ready to the next system (which is developing groups for the player, so NPCs can fight side by side with the player :slight_smile:) - if we can get it done this weekend, that would be fabulous!

That’s correct. Take a look at MY edit of your post. Use the same ` that we use to format code in blocks, but just 1 before and after and it will format a small snippet of code. You had them with the ’ instead of the `.
For example, if I put `GetComponent<PlayerStateMachine>()`, it would appear in the post as GetComponent<PlayerStateMachine>() instead of GetComonent();

See… you don’t promise these things ahead time if you don’t have that clue.

You’ll struggle a lot more if you don’t have an army of playtesters having already broken your game (to find the bugs) before you release something that you call a complete game. It’s ok to say “This is a work in progress, and I want you guys to test [insert feature here]”. Look on itch, there’s a LOT of that.
You don’t advance in the game industry as an indy by putting out one big giant “Let’s recreate WoW or Runescape” kind of game. The guys that did those things made less complex games first. Epic Megagames back in the 90s came up with a silly little game that ran in DOS (Google it) that was a puzzle game using ascii characters… the cool thing was that you could make your own levels… that company that started small… now has Unreal Engine and Fortnight… they built up to it.

If you haven’t ran this by an army of playtesters first, you’re not going to like the results…

If, everytime you see a new feature, like when I was dumb enough to mention enemies attacking each other, you decide you want to add it, you will never get the project done…

You’re helping me make my point. I’m not trying to discourage, but I am handing out a word of caution… You’ve added crafting and horseback riding, and swimming and respawning and aggro groups that respawn and aggrogroups that hate other aggrogroups except when they don’t and… So what ends up happening is that these codes start to become tightly coupled and one small change in one feature ends up breaking another feature that seems unrelated except that the code is all over the place.

At the moment, I’m honestly not getting a lot done on any front… I make it through answering students course related questions, but my time in front a computer is limited. I do have a surgery date (and it’s soon) and once that happens, I’ll be struggling even to accomplish that for a few weeks and then we’ll see how long I can sit in front of a computer over the 2 months after that.

well, well… if that wasn’t what my shop decided to do yesterday when I first started showing my game to a private discord server of indie devs…

One of them literally said “in every demo, you’ll just have something somewhere that just doesn’t work, that you just discovered with the rest of us, and you’ll have no idea why”

I REALLY REALLY BELIEVE IN YOU, I know you’ll be up and running again in no time, and in perfect health :smiley: - by all means, take your time. I’ll see what I can do about my system after my exam (fun fact: I did that exam before, but the required grade was 349/360, and I got 348/360 back then… I honestly don’t think it’ll be easy for me to get any close to that mark, but I’ll try)

precisely why I want to quietly work on this project first, before making any promises that I just know I can’t pull off…

how to confuse the little guy… ehh, I’m sure I’ll eventually get the hang of it :slight_smile:

Already started working on that accidentally… believe it or not, the moment I wrote that comment I found myself in a new server of amazing people live streaming my game properly, and giving them a copy to try, doing exactly what I wasn’t supposed to do…

Let’s just say they went nuts about it… it was 4 people, and when I told them what the grand vision was, I think they LOVED it… Anyway, I won’t be touching my project for a few weeks as well, so yeah both of us get a break (goes and continues coding anyway :sweat_smile:)

Doing exactly what you ARE supposed to do. Just trust me on this. This is the same sort of advice you’d be getting from Rick or Ben, or Tim. It’s the same sort of advice they’ve given me.

I trust you, believe me I do, but I fear the dopamine hit… When someone compliments my project, it can give me a dopamine hit that can last for days or weeks, and I don’t know when will I (if ever) recover from it and actually get back to work

That’s why I am hesitant, and why I’ve been hesitant for so long… By all means, I shall follow your advice :smiley:

no idea who Tim is… :sweat_smile: (I only know Rick, Ben, Sam, Brian, Ed and bixarrio)

Tim Russert leads seminars in marketing games. He’s had some serious best sellers in the Steam store himself. He kows what he’s doing when it comes to building interest in your game and marketing it as it’s being developed.

really…?! :face_with_raised_eyebrow:

Privacy & Terms