actual followers

Show off your mod creations or just a work in progress.
Post Reply
User avatar
John Doom
Posts: 126
Joined: Wed Dec 01, 2021 5:59 pm
Location: Italy
Contact:

actual followers

Post by John Doom »

Hi, I'm trying to make actual followers, that is npc that escort you.
For starters, I'm implementing a new quest action to make followers. :roll: Besides issues registering it (viewtopic.php?f=22&p=58528), the code is kind of working so far. It basically:
  • adds the follower as enemy
  • respawn it on transitions
  • it follows the player around (it actually tries to attack the player, but I made it so it can't use melee attacks; problem is with ranged, though :D )
I'll keep you updated ;)

UPDATE: I tried going with the follower in a dungeon. Besides obvious pathfinding issues, it's working good enough. I'll probably fix the problem teleporting the follower around the player if too distant.

Code: Select all

using UnityEngine;
using System;
using System.Text.RegularExpressions;
using DaggerfallWorkshop.Utility;
using FullSerializer;
using DaggerfallWorkshop.Game.Utility;

namespace DaggerfallWorkshop.Game.Questing
{
    public class CreateFollower : ActionTemplate
    {
        enum Seq {none, creating, done};

        Symbol foeSymbol;

        Seq seq = Seq.none;
        GameObject foeObject;
        EnemyMotor motor;
        EnemySenses senses;
        EnemyAttack attack;

        public override string Pattern
        {
            get
            {
                return @"create follower (?<symbol>[a-zA-Z0-9_.-]+)";
            }
        }

        public CreateFollower(Quest parentQuest)
            : base(parentQuest)
        {
            PlayerEnterExit.OnTransitionExterior += PlayerEnterExit_OnTransition;
            PlayerEnterExit.OnTransitionInterior += PlayerEnterExit_OnTransition;
            PlayerEnterExit.OnTransitionDungeonExterior += PlayerEnterExit_OnTransition;
            PlayerEnterExit.OnTransitionDungeonInterior += PlayerEnterExit_OnTransition;
            StreamingWorld.OnInitWorld += StreamingWorld_OnInitWorld;
        }

        public override void InitialiseOnSet()
        {
        }

        public override IQuestAction CreateNew(string source, Quest parentQuest)
        {
            // Source must match pattern
            Match match = Test(source);
            if (!match.Success)
                return null;

            // Factory new action
            CreateFollower action = new CreateFollower(parentQuest);
            action.foeSymbol = new Symbol(match.Groups["symbol"].Value);

            return action;
        }

        public override void Update(Task caller)
        {
            // Check for a new spawn event - only one spawn event can be running at a time
            if (seq==Seq.none)
            {
                // Get the Foe resource
                Foe foe = ParentQuest.GetFoe(foeSymbol);
                if (foe == null)
                {
                    SetComplete();
                    throw new Exception(string.Format("create follower could not find Foe with symbol name {0}", Symbol.Name));
                }

                // Start deploying GameObjects
                CreatePendingFoeSpawn(foe);
            }

            // Try to deploy
            if (seq==Seq.creating)
            {
                TryPlacement();
                GameManager.Instance.RaiseOnEncounterEvent();
            }

            //update
            if (seq == Seq.done)
            {
                if (senses.Target == GameManager.Instance.PlayerEntityBehaviour)
                {
                    //follow player without attacking
                    //TODO: for ranged attacks
                    attack.ResetMeleeTimer();
                }
            }
        }

        #region Private Methods

        void CreatePendingFoeSpawn(Foe foe)
        {
            if (foeObject == null)
            {
                // Get foe GameObject
                MobileReactions reaction = MobileReactions.Hostile;
                MobileGender gender = MobileGender.Male;

                foeObject = GameObjectHelper.CreateEnemy("lolz",
                    foe.FoeType,
                    Vector3.zero,
                    mobileGender: gender,
                    mobileReaction: reaction);
                if (foeObject == null)
                {
                    SetComplete();
                    throw new Exception(string.Format("create foe attempted to create {0}x{1} GameObjects and failed.", foe.SpawnCount, Symbol.Name));
                }
            }

            // Initiate deployment process
            // Usually the foe will spawn immediately but can take longer depending on available placement space
            // This process ensures these foes have all been deployed before starting next cycle
            seq = Seq.creating;
        }

        void TryPlacement()
        {
            PlayerEnterExit playerEnterExit = GameManager.Instance.PlayerEnterExit;

            // Place in world near player depending on local area
            if (playerEnterExit.IsPlayerInsideBuilding)
            {
                PlaceFoeBuildingInterior(foeObject, playerEnterExit.Interior);
            }
            else if (playerEnterExit.IsPlayerInsideDungeon)
            {
                PlaceFoeDungeonInterior(foeObject, playerEnterExit.Dungeon);
            }
            else if (!playerEnterExit.IsPlayerInside && GameManager.Instance.PlayerGPS.IsPlayerInLocationRect)
            {
                PlaceFoeExteriorLocation(foeObject, GameManager.Instance.StreamingWorld.CurrentPlayerLocationObject);
            }
            else
            {
                PlaceFoeWilderness(foeObject);
            }
        }

        #endregion

        #region Placement Methods

        // Place foe somewhere near player when inside a building
        // Building interiors have spawn nodes for this placement so we can roll out foes all at once
        void PlaceFoeBuildingInterior(GameObject gameObjects, DaggerfallInterior interiorParent)
        {
            // Must have a DaggerfallLocation parent
            if (interiorParent == null)
            {
                SetComplete();
                throw new Exception("PlaceFoeFreely() must have a DaggerfallLocation parent object.");
            }

            // Always place foes around player rather than use spawn points
            // Spawn points work well for "interior hunt" quests but less so for "directly attack the player"
            // Feel just placing freely will yield best results overall
            PlaceFoeFreely(gameObjects, interiorParent.transform);
            return;
        }

        // Place foe somewhere near player when inside a dungeon
        // Dungeons interiors are complex 3D environments with no navgrid/navmesh or known spawn nodes
        void PlaceFoeDungeonInterior(GameObject gameObjects, DaggerfallDungeon dungeonParent)
        {
            PlaceFoeFreely(gameObjects, dungeonParent.transform);
        }

        // Place foe somewhere near player when outside a location navgrid is available
        // Navgrid placement helps foe avoid getting tangled in geometry like buildings
        void PlaceFoeExteriorLocation(GameObject gameObjects, DaggerfallLocation locationParent)
        {
            PlaceFoeFreely(gameObjects, locationParent.transform);
        }

        // Place foe somewhere near player when outside and no navgrid available
        // Wilderness environments are currently open so can be placed on ground anywhere within range
        void PlaceFoeWilderness(GameObject gameObjects)
        {
            // TODO this false will need to be true when start caching enemies
            GameManager.Instance.StreamingWorld.TrackLooseObject(gameObjects, false, -1, -1, true);
            PlaceFoeFreely(gameObjects, null, 8f, 25f);
        }

        // Uses raycasts to find next spawn position just outside of player's field of view
        void PlaceFoeFreely(GameObject gameObjects, Transform parent, float minDistance = 5f, float maxDistance = 20f)
        {
            const float overlapSphereRadius = 0.65f;
            const float separationDistance = 1.25f;
            const float maxFloorDistance = 4f;

            // Must have received a valid array
            if (gameObjects == null)
                return;

            // Set parent - otherwise caller must set a parent
            if (parent)
                gameObjects.transform.parent = parent;

            // Select a left or right direction outside of camera FOV
            Quaternion rotation;
            float directionAngle = GameManager.Instance.MainCamera.fieldOfView;
            directionAngle += UnityEngine.Random.Range(0f, 4f);
            if (UnityEngine.Random.Range(0f, 1f) > 0.5f)
                rotation = Quaternion.Euler(0, -directionAngle, 0);
            else
                rotation = Quaternion.Euler(0, directionAngle, 0);

            // Get direction vector and create a new ray
            Vector3 angle = (rotation * Vector3.forward).normalized;
            Vector3 spawnDirection = GameManager.Instance.PlayerObject.transform.TransformDirection(angle).normalized;
            Ray ray = new Ray(GameManager.Instance.PlayerObject.transform.position, spawnDirection);

            // Check for a hit
            Vector3 currentPoint;
            RaycastHit initialHit;
            if (Physics.Raycast(ray, out initialHit, maxDistance))
            {
                float cos_normal = Vector3.Dot(-spawnDirection, initialHit.normal.normalized);
                if (cos_normal < 1e-6)
                    return;
                float separationForward = separationDistance / cos_normal;

                // Must be greater than minDistance
                float distanceSlack = initialHit.distance - separationForward - minDistance;
                if (distanceSlack < 0f)
                    return;

                // Separate out from hit point
                float extraDistance = UnityEngine.Random.Range(0f, Mathf.Min(2f, distanceSlack));
                currentPoint = initialHit.point - spawnDirection * (separationForward + extraDistance);
            }
            else
            {
                // Player might be in an open area (e.g. outdoors) pick a random point along spawn direction
                currentPoint = GameManager.Instance.PlayerObject.transform.position + spawnDirection * UnityEngine.Random.Range(minDistance, maxDistance);
            }

            // Must be able to find a surface below
            RaycastHit floorHit;
            ray = new Ray(currentPoint, Vector3.down);
            if (!Physics.Raycast(ray, out floorHit, maxFloorDistance))
                return;

            // Ensure this is open space
            Vector3 testPoint = floorHit.point + Vector3.up * separationDistance;
            Collider[] colliders = Physics.OverlapSphere(testPoint, overlapSphereRadius);
            if (colliders.Length > 0)
                return;

            // This looks like a good spawn position
            foeObject.transform.position = testPoint;
            FinalizeFoe(foeObject);
            gameObjects.transform.LookAt(GameManager.Instance.PlayerObject.transform.position);
            motor = foeObject.GetComponent<EnemyMotor>();
            senses = foeObject.GetComponent<EnemySenses>();
            attack = foeObject.GetComponent<EnemyAttack>();
            seq = Seq.done;
        }

        // Fine tunes foe position slightly based on mobility and enables GameObject
        void FinalizeFoe(GameObject go)
        {
            var mobileUnit = go.GetComponentInChildren<MobileUnit>();
            if (mobileUnit)
            {
                // Align ground creatures on surface, raise flying creatures slightly into air
                if (mobileUnit.Enemy.Behaviour != MobileBehaviour.Flying)
                    GameObjectHelper.AlignControllerToGround(go.GetComponent<CharacterController>());
                else
                    go.transform.localPosition += Vector3.up * 1.5f;
            }
            else
            {
                // Just align to ground
                GameObjectHelper.AlignControllerToGround(go.GetComponent<CharacterController>());
            }

            go.SetActive(true);
        }

        #endregion

        #region Event Handlers

        private void PlayerEnterExit_OnTransition(PlayerEnterExit.TransitionEventArgs args)
        {
            PlayerEnterExit_Reset();
        }

        private void StreamingWorld_OnInitWorld()
        {
            PlayerEnterExit_Reset();
        }

        private void PlayerEnterExit_Reset()
        {
            // Any foes pending placement are now invalid
            seq = Seq.none;
        }

        #endregion

        #region Serialization

        [fsObject("v1")]
        public struct SaveData_v1
        {
            public Symbol foeSymbol;
        }

        public override object GetSaveData()
        {
            SaveData_v1 data = new SaveData_v1();
            data.foeSymbol = foeSymbol;

            return data;
        }

        public override void RestoreSaveData(object dataIn)
        {
            if (dataIn == null)
                return;

            SaveData_v1 data = (SaveData_v1)dataIn;
            foeSymbol = data.foeSymbol;
        }

        #endregion
    }
}

User avatar
Macadaynu
Posts: 263
Joined: Sun Mar 07, 2021 1:18 pm

Re: actual followers

Post by Macadaynu »

Is this the same problem I'm trying to solve with my Language Skills Overhaul mod?

viewtopic.php?f=14&t=5236

here's the latest update:


User avatar
John Doom
Posts: 126
Joined: Wed Dec 01, 2021 5:59 pm
Location: Italy
Contact:

Re: actual followers

Post by John Doom »

I got too late to the party, I guess :lol:
Like you I thought of injecting EnemyMotor, but its code covers a lot of cases, it would be a waste to just replace it. I'd like to find a way to use its functions instead :?

l3lessed
Posts: 1403
Joined: Mon Aug 12, 2019 4:32 pm
Contact:

Re: actual followers

Post by l3lessed »

Like you I thought of injecting EnemyMotor, but its code covers a lot of cases, it would be a waste to just replace it. I'd like to find a way to use its functions instead :?
This is probably not going to happen with the current way enemy motor script is setup. You cannot access everything you need to repurpose the enemy motor script into a complete npc follower script; the current iterations are impressive considering the limits of the enemy motor script. Either the base script needs updated and submitted for the base code, which isn't happening anymore until after release at least, or you need to rebuild the scripts.

I had to do this for the ambidexterity mod because of the fps and weaponmanager scripts not being able to do what I needed with their current public properties and objects.
My Daggerfall Mod Github: l3lessed DFU Mod Github

My Beth Mods: l3lessed Nexus Page

Daggerfall Unity mods: Combat Overhaul Mod

Enjoy the free work I'm doing? Consider lending your support.

Post Reply