Skills based system

OK So I noticed the Skills are never actually called in ‘Health.TakeDamage()’ which is the core of all of this. To combat this issue, I tried re-coding my “if(healthPoints.value == 0)” statement to integrate this new block of code:

// ---------- OUT OF COURSE CONTENT: Check for the skill, and add his experience -------------
        Skill skillToLevelUp = GameObject.FindWithTag("Player").GetComponent<WeaponConfig>().CheckForSkill();
        int XPToReward = GetComponent<AIController>().GetXPReward();
        GetComponent<SkillExperience>().GainExperience(skillToLevelUp, XPToReward);
        // -------------------------------------------------------------------------------------------

To end up as follows:

if (healthPoints.value == 0) {

        GetComponent<ActionSchedular>().CancelCurrentAction();
        onDie.Invoke(); // Invokes external UnityEvent Audio 'Die', combined with other events, when our Enemy/Player has taken enough damage to die
        
Skill skillToLevelUp = GameObject.FindWithTag("Player").GetComponent<WeaponConfig>().CheckForSkill();
        int XPToReward = GetComponent<AIController>().GetXPReward();
        GetComponent<SkillExperience>().GainExperience(skillToLevelUp, XPToReward);
        

            }

But that also failed terribly. Any alternative solutions? My Combat XP values are literally not updating… at all, and this code I tried integrating is clearly corrupt, as it gives us WILDLY Undesirable results

and why do we have a ‘GetSkill()’ that is never called? I mean I get what it does, I just don’t get why we have it if we never called it

The whole point of passing the skill to the TakeDamage is so that TakeDamage already has the skill wiithout cross dependencies (now your Health relies on Fighter and Fighter relies on Health).

I was suggesting this as alternative to calculating the skill.

Passing it as a parameter honestly did nothing on its own. I designed the UI that displays it, and the UI works, but the formula returns nothing, at least for Attack so far that’s the case :sweat_smile: - hence why I was confused about what to do next… and I started questioning what we’re doing here (but it also tells me that the all new Skill checker works fine :slight_smile: , so that’s a positive so far). Any further changes that you can think of to get this to work, or possibly a checklist I can go through to figure out why my Attack Xp is not showing up?

Oh trust me I never want to do this again. It went SO WRONG

Have we considered tuning the ‘Health.AwardExperience(instigator)’ function? Perhaps changing that up a little bit might get the Skill call to work…? If it helps in anyway, that’s how we ended up with the ‘AwardExperience()’ to stop the issue of my player pretending to be alive when he’s dead:

private void AwardExperience(GameObject instigator)
        {
            // The replacement function of my 'AwardExperience' takes care of the RARE event of
            // having a Hit() function being fired when the player is supposed to be dead.
            // In other words, if you're dead, you can't attack the guard, you must die immediately!

            if (instigator.TryGetComponent(out Experience experience)) {

                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead()) {

                    experience.GainExperience(GetComponent<BaseStats>().GetStat(Stat.ExperienceReward));

                }

            }

        }

TLDR: I managed to make the XP Gain system to work, but I can’t find a way to change the ‘Skill skill = Skill.Attack’ parameter in the ‘Health.TakeDamage()’ function to change based on ‘weaponConfig.CheckForSkill()’, or in any of the other functions that call it. Please help :slight_smile:

OK Umm… I got the Attack XP to work as expected, but I did some quite heavy modifications, and it now has me with ONE BIG QUESTION, and that question is… HOW DO WE GET THE Skill Check to work properly, without the code dependencies relying on one another and screwing everything up?

With that question out the way, here are the modifications I did to actually get this code to work properly:

  1. I changed ‘AwardExperience()’ in ‘Health.cs’, so now this is my ‘AwardExperience()’ function:
private void AwardExperience(GameObject instigator, Skill skill) {

            if (instigator.TryGetComponent(out SkillExperience skillExperience)) {

                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead()) {

                    skillExperience.GainExperience(skill, GetComponent<AIController>().GetXPReward());

                }

            }

        }

It only made sense to have the ‘AIController.cs’ to hold the ‘XPReward’ variable, since all NPC enemies are holding an AI Controller

  1. For this function to work properly, and retrieve an XP Reward based on each Individual NPC, I went to ‘AIController.cs’ and added the following:
[SerializeField] int XPReward;

public int GetXPReward() {
return XPReward;
}
  1. In ‘Health.TakeDamage()’, there was an ‘if’ statement in the end that checks for if the player is dead or not, that goes like this:
if (healthPoints.value == 0) {
// Some code here...
}

For this one, previously there was an ‘AwardExperience(instigator)’ function. I commented it out (I don’t delete my old code, incase anything goes wrong I can always revert it), and to match up with the new ‘AwardExperience()’ function I mentioned in step 1, for this one, I fed the ‘AwardExperience()’ function with the ‘skill’ (which, I’m guessing the easiest way to figure out how to get it to work without circular dependencies, is to create a new script to hold just that… I’ll give it a try now). So now my ‘AwardExperience()’ function looks like this:

AwardExperience(instigator, skill);    // Award experience to whoever won the battle, for the chosen skill that won the battle with (Attack, Defence, Mage or Range)

and it all worked well from there…

Now to figure out how to check what skill to check for, without relying on code loops

Edit: for anyone reading this, don’t forget step 4: Add ‘SkillExperience.cs’ and ‘SkillStore.cs’ scripts onto your player, and ensure your ‘SkillStore.cs’ has all the Starting Skills set at level 1 from the get go :slight_smile: (make sure there are no duplicate skills or your dictionary will give you an NRE output. It’s just annoying to see, but it’s harmless, but be safe)

For you to visually see it and try it out, try set one of your weapons to be restricted against being held until a level is reached (if you did the IPredicateEvaluator part a bunch of messages back), and copy the ‘Mana Value Display’ into a new ‘Attack Level Display’ or something… I got this to work, just can’t be bothered to extract the code right now :sweat_smile:

Anyway… how do we get the ‘CheckForSkill()’ to connect to Health.TakeDamage() without code loops? it’s the last step in this weird puzzle :sweat_smile:

Edit 2: Just to reconfirm, these scripts go into ‘WeaponConfig.cs’ right?

// ----------------- OUT OF COURSE CONTENT - Skill-based Experience for combat -----------

        [SerializeField] Skill associatedSkill;
        public Skill GetSkill() => associatedSkill; // returns 'associatedSkill' variable

        public Skill CheckForSkill() {

            if (GetAmmunitionItem()) return Skill.Ranged;
            if (HasProjectile()) return Skill.Magic;
            return Skill.Attack;

        }

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

(Still can’t figure where to adjust ‘takeDamage’, apart from my failed attempt in ‘Fighter.Hit()’, to ensure the given XP is given to the right equipment accordingly… the one call in ‘Fighter.Hit()’ had the input completely ignored for some reason to begin with, and I figured that out the hard way… Please send help)

Edit 3: Apparently I just noticed that the ‘takeDamage’ in projectile can also make magic XP effective, but for ranged weapons… ahh idk yet how to get there without code cycles. Any ideas?

OK so my suggested solution, for future references for me, is to rename the ‘Projectile.cs’ to ‘RangedProjectile.cs’ and place this on all the arrow Projectile Prefabs, and where the function takes damage, we input ‘Skill.Ranged’ as the input, and for magic weapons, we create a new ‘MagicProjectile.cs’ that is an exact copy of ‘RangedProjectile.cs’, but we replace the ‘TakeDamage()’ Skill input to ‘Skill.Magic’, and we place the Projectiles on their respective prefabs… thoughts?

I’ve pointed you to the simplest way to achieve this multiple times. The Fighter should get it from the WeaponConfig, and then the fighter should pass that on to the Health TakeDamage. It really (REALLY) couldn’t be simpler.

When you launch the projectile, you’ll need to pass the skill to the projectile (just like Hit() passes the skill to the Health.TakeDamage(). The projectile will pass the skill on to the Health component.

Think of the skill as a sort of token that is passed from object to object until it is finally used by AwardExperience.

The Projectile relying on ‘WeaponConfig.cs’ does nothing but cause disasters for some reason, so now I have to insert hard-coded skills, which are not accumulated in any way shape or form from ‘WeaponConfig.cs’, which is seriously troubling me now.

If there’s a ‘WeaponConfig’ datatype in ‘Projectile.cs’, the Projectile flat out starts acting up (I think it’s a code cycle there, not sure tbh). This one flat out makes the Projectiles ineffective, literally!

As for this thing, here’s what I tried doing, which I’m no longer sure if it’s right or wrong (in ‘Fighter.cs’):

target.TakeDamage(gameObject, damage, currentWeaponConfig.GetSkill());  // Damages the enemy/player, based on our Damage Stats (in 'BaseStats.cs')

My conclusion here: Figure another way out to seperate Magic and Ranged weapons in ‘Projectile.cs’ (possibly through Ammunition and Projectiles like ‘WeaponConfig.cs’, but without having to reach out to 'WeaponConfig.cs, because honestly, projectile relying on ‘WeaponConfig.cs’ breaks all hell loose). Can you please help me with that @Brian_Trotter ?

(I know ‘CheckForSkill()’ and ‘GetSkill()’ work in WeaponConfig.cs because they’re used in ‘Fighter.cs’, but the circular dependency of ‘WeaponConfig’ and ‘Projectile’ is giving me nightmares…)

Ok, I went and implemented this in my current project, using what I like to call “Follow the Errors”…

First, the easy part was adding a skill to WeaponConfig along with a GetSkill() method that returns the skill associated with that WeaponConfig.

Next, in Fighter.cs in Hit(), I added WeaponConfig.GetSkill() to the end of the TakeDamage routine.

            if (currentWeaponConfig.HasProjectile())
            {
                if (currentWeaponConfig.GetAmmunitionItem())
                {
                    GetComponent<Quiver>().SpendAmmo(1);
                }
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, damage);
            }
            else
            {
                target.TakeDamage(gameObject, damage, currentWeaponConfig.GetSkill());
            }

You’ll note that I didn’t add this to currentWeaponConfig.LauchProjetile because currentWeaponConfig already knows it’s skill.

At this point, the compiler started complaining that TakeDamage doesn’t have a method signature that matches this one. I’m using JetBrains Rider, and it offered to add the Skill skill to the TakeDamage method, so I did. This resolved the Fighter error, and we’re done in Fighter altogether.

Off we go to TakeDamage(), where I added “,skill” to the AwardExperience method

        public void TakeDamage(GameObject instigator, float damage, Skill skill)
        {
            healthPoints.value = Mathf.Max(healthPoints.value - damage, 0);
            
            if(IsDead())
            {
                onDie.Invoke();
                AwardExperience(instigator, skill);
                GetComponent<ActionScheduler>().CancelCurrentAction();
            } 
            else
            {
                takeDamage.Invoke(damage);
            }
            UpdateState();
        }

Now at this point, the compiler is complaining that AwardExperience doesn’t have a skill parameter, and Projectile is complaining that it’s not adding skill to the TakeDamage.

First, to AwardExperience, which was easy. Since I’m not trying to completely break my existing project, I chose to award both regular experience and skill experience (this is actually a common practice in many skill based games, but can choose to ignore regular experience)

        private void AwardExperience(GameObject instigator, Skill skill)
        {
            if (instigator.TryGetComponent(out Experience experience))
            {
                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead())
                {
                    experience.GainExperience(GetComponent<BaseStats>().GetStat(Stat.ExperienceReward));
                }
            }

            if (instigator.TryGetComponent(out SkillExperience skillExperience))
            {
                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead())
                {
                    skillExperience.GainExperience(skill, Mathf.RoundToInt(GetComponent<BaseStats>().GetStat(Stat.ExperienceReward)));
                }
            }
        }

Now to Projectile.cs, which is complaining about not having a skil parameter passed along to TakeDamage. First off, Projectile needs a skill to use in the first place, so when we look at Projectile, we see that all of the data it needs is passed in through WeaponConfig.Spawn, and that there are actually THREE SetTarget methods. Two are pass throughs to the main one. This allows for location and target based shooting… so I added the Skill to each of the SetTarget() methods and in the two pass through methods, I passed along the skill to the main method. I also added a global Skill skill; where I declared damage, instigator, and target.

        public void SetTarget(Health target, GameObject instigator, float damage, Skill skill)
        {
            SetTarget(instigator, damage, target, Vector3.zero, skill);
        }

        public void SetTarget(Vector3 targetPoint, GameObject instigator, float damage, Skill skill)
        {
            SetTarget(instigator, damage, null, targetPoint, skill);
        }

        public void SetTarget(GameObject instigator, float damage, Health target=null, Vector3 targetPoint=default, Skill skill = Skill.Ranged)
        {
            this.target = target;
            this.targetPoint = targetPoint;
            this.damage = damage;
            this.instigator = instigator;
            this.skill = skill;
            Destroy(gameObject, maxLifeTime);
        }

Now this immediately caused the compiler to complain that two more scripts don’t have the right calls… so I followed the breadcrumbs… First to Weaponconfig’s LaunchProjectile. Since the WeaponConfig already knows the skill, this was a relatively quick fix, just adding GetSkill() to the SetTarget method

        public void LaunchProjectile(Transform rightHand, Transform leftHand, Health target, GameObject instigator, float calculatedDamage)
        {
            Projectile projectileInstance = Instantiate(projectile, GetTransform(rightHand, leftHand).position, Quaternion.identity);
            projectileInstance.SetTarget(target, instigator, calculatedDamage, GetSkill());
        }

At this point, we still have two more scripts complaining, and it’s something I hadn’t thought of earlier… These are our Ability scripts… HealthEffect and SpawnProjectileEffect…
HealthEfffect was easy… I could have added a SerializeField to the HealthEffect, but I chose instead to hard code Skill.Casting to the TakeDamage call

        public override void StartEffect(AbilityData data, Action finished)
        {
            foreach (var target in data.GetTargets())
            {
                var health = target.GetComponent<Health>();
                if (health)
                {
                    if (healthChange < 0)
                    {
                        health.TakeDamage(data.GetUser(), -healthChange, Skill.Casting);
                    }
                    else
                    {
                        health.Heal(healthChange);
                    }
                }
            }
            finished();
        }

That just leaves SpawnProjectileEffect, which was a little trickier because in this method, we could be casting a target point or a physical target. In both cases, I chose Skill.Casting but again, you could make this a SerializeFied

        private void SpawnProjectileForTargetPoint(AbilityData data, Vector3 spawnPosition)
        {
            Projectile projectile = Instantiate(projectileToSpawn);
            projectile.transform.position = spawnPosition;
            projectile.SetTarget(data.GetTargetedPoint(), data.GetUser(), damage, Skill.Casting );
        }

        private void SpawnProjectilesForTargets(AbilityData data, Vector3 spawnPosition)
        {
            foreach (var target in data.GetTargets())
            {
                Health health = target.GetComponent<Health>();
                if (health)
                {
                    Projectile projectile = Instantiate(projectileToSpawn);
                    projectile.transform.position = spawnPosition;
                    projectile.SetTarget(health, data.GetUser(), damage, Skill.Casting);
                }
            }
        }

And that’s it. It took longer to type all this out than it did to factor in the changes.
This BreadCrumb approach is something used often in refactoring. One change often leads to another change, etc.

In terms of the Skill itself being passed around, this approach lets us pass that information to the only method that actually acts on it (Health.AwardExperience) without reaching back.

I also want to point out that the approach I just used only adds ONE dependency to most of the scripts… and that’s RPG.Skills, just to get the definition for the enum Skill. Health is the only component that relies on another new component, as it must call the SkillStore to award the experience. Otherwise, the pre-existing dependencies (Fighter depends on WeaponConfig, WeaponConfig depends on Projectile, etc) are completely unchanged.

How do you come up with these solutions insanely fast? Jeez, I won’t even dare mess with my code this much if you left me for a year with it… Seriously though, thanks a ton Brian. I’m currently an hour away from home, I’ll test this as soon as I reach there :slight_smile: - hopefully no errors this time

OK so that clearly solved the refactoring problem, but it still did not solve the one problem I’m trying to solve, and that is that when I fire fireballs, I want to pass the Skill as ‘Skill.Magic’ instead of ‘Skill.Ranged’ (“Skill.Ranged” should be reserved for items with Ammo, such as bows, but not magical spellcasting items) :confused: (I’ll go try debugging that again). I am still seeking a way to classify them apart tbh

I thought serializing a skill in projectile (we needed that for the ‘this.skill = skill’ line in “Projectile.SetTarget()” would fix it… apparently not :sweat_smile: )

I’m feeling like I must not be explaining it well… the skill should be stored or calculated in the WeaponConfig. When the WeaponConfig spawns the projectile, it sets the projectile’s skill. That’s it, it’s done. The Projectile should know nothing about the WeaponConfig, or even what the skill represents, just that it was told This is your skill, pass this skill to the TakeDamage routine.

I just followed the error message, assesed what was needed to fix that message and moved to the next error message that create.

I have read both this answer and the detailed answer multiple times over, still can’t figure out where exactly did I go wrong… does it have something to do with the ‘Projectile.cs’ Skill I instantiated out of thin air? I mean I did have to add this line

Skill skill;

For the sake of the compiler to stop complaining about it in the last big chunk of “SetTarget”. So now this is what my ‘SetTarget’ blocks in Projectile.cs look like:

    Skill skill;

        public void SetTarget(Health target, GameObject instigator, float damage, Skill skill) {

            // This function calls 'SetTarget' below, so we can aim at our enemies using our abilities

            SetTarget(instigator, damage, target, Vector3.zero, skill);

        }

        public void SetTarget(Vector3 targetPoint, GameObject instigator, float damage, Skill skill) {

            SetTarget(instigator, damage, null, targetPoint, skill);

        }

        public void SetTarget(GameObject instigator, float damage, Health target = null, Vector3 targetPoint = default, Skill skill = Skill.Ranged) {

            this.target = target;
            // our Target is now the GameObject holding this script

            this.targetPoint = targetPoint;
            // keep track of the target we are firing our Projectile abilities at

            this.damage = damage;
            // The damage dealt by our Projectile = damage from SetTarget ('float damage' argument)

            this.instigator = instigator;
            // The instigator is set to be an instance of whoever holds this script

            this.skill = skill;
            // The skill to train when a projectile is fired

            Destroy(gameObject, maxLifeTime);   // destroys our Projectile after its maximum lifetime (so it doesn't take a toll on our computer)


        }

Idk if I’m the only one not seeing this, but there’s no reference to ‘WeaponConfig.Spawn()’ in ‘Projectile.cs’… is there something missing there?

EDIT: Ahh nevermind… I fixed it, this thing was a NIGHTMARE TO DEAL WITH, BUT IT WORKS NOW

My mistake was that in ‘Projectile.cs’, I was passing ‘Skill.Ranged’ into the Projectile.cs’s ‘health.TakeDamage()’ function call, instead of the newly re-assigned ‘Skill skill’ in ‘SetTarget()’… This is one error I will remember for a long… LONG… LONG time (the solution just shows up out of thin air at 1AM…). I sincerely apologize for the headache I may have caused you Brian

OK so we’re done with this annoying bug, the next step I want is to average out the combat (I still haven’t figured out how I’ll integrate defence yet, but I’m thinking of automatically updating that skill whenever any sort of combat is involved, so let’s say you get 10XP per enemy, you get 3.33XP of Defence… something that is 1/3rd of whatever you’re training, so it’s at pace with the other two skills. It’s either this or make it it’s own respective skill, I’m still debating on that, any thoughts?), so now I want combat to level up similar to what it did prior to introducing the Skill-based system, with all its magical bells and whistles (mainly so that the player fully renews his health when the combat level increases tbh), but only when 1 attack, ranged, magic and defence level are levelled up, and each one must have a level up for this to work. How can we start working on this?

I’m at work, so I answered without my code notes in front of me… It’s WeaponConfig.LaunchProjectile().

Start by going over the whole system again, and make sure you understand each section. Here’s your challenge: Annotate each of the changes made in Fighter, Health, WeaponConfig, and Projectile.

First thing I’ll do in the morning, for now I’ll head back to rest for a few more hours before tomorrow gets me killed :sweat_smile:

OK I’m done with the annotation task, but I also made some tiny changes… I added a new Skill called ‘Skill.None’, for the cases when a skill input is required, but no skill is supposed to be given, like ‘HealthEffect.cs’ and ‘SpawnProjectileEffect.cs’, where a Skill input is needed, but we don’t want it… Am I doing it right?

If so, can we start working on this please :slight_smile: - to keep it simple, I want my Combat level to rise by 1 level under either one of these conditions is met:

  1. Either you increase all 4 skills (Attack, Defence, Ranged and Magic) by 1 level each

OR

  1. You gain at least a total of 5 levels in any combination of these skills (so for example 3 defence levels and 2 levels from anything else helps you raise it, or maybe 4 attack and something else, or perhaps 5 ranged levels, maybe 2 magic levels and 3 other skills, etc).

For the second condition, If I’m not mistaken, I think we will need a getter to get these skill values at the start of each combat level, store it, and use it for checking later on… Am I right?

Let’s not talk about the defence mechanic yet, because it will be a button that switches from whatever skill is being trained (based on the weapon) to Defence… That’s the only way I can properly think of training defence to be fair. So basically regardless of whatever skill you’re training, Defence will override that skill if a specific button is clicked (condition for that button is true now), and then return to that skill if the button is deactivated (condition for that button is false now)

I really really hope this is not too complex (if it is, then we can just implement the first point and ignore the second one). How can we start going around this?

Either one by itself is relatively easy. or is far more complicated. Actually, I would say the first condition is fairly complicated in and of itself. If you stick to the 2nd condition then you can get Combat level with a simple equation

public int GetCombatLevel() => (GetSkillLevel(Skill.Attack) + GetSkillLevel(Skill.Defense) + GetSkillLevel(Skill.Ranged) + GetSkillLevel(Skill.Magic) / 5;

public int GetSkillsNeededForNextCombatLevel => 5 - (GetSkillLevel(Skill.Attack) + GetSkillLevel(Skill.Defense) + GetSkillLevel(Skill.Ranged) + GetSkillLevel(Skill.Magic)) % 5;

I’ll try this out in a bit, since I’m at work (on a Saturday, I know…). But to call this, we call it in ‘BaseStats.UpdateLevel()’, right?

Actually, that method assumes it’s in SkillStore. In BaseStats, you could call the skillStore’s GetCombatLevel() and compare it to the old level, and announce a LevelUp event.

OK umm, I’m a little baffled on where to check for ‘GetSkillsNeededForNextCombatLevel()’ would go… I know how to display the Combat level, just not sure on how to check for another increment (I think just ‘GetCombatLevel()’ gets it… right?). Do we include these two functions in ‘GainLevel()’, or…? (In the end, “GetCombatLevel()” is what I will be displaying on my UI)

Quick programming question on the fly, from the ‘GetSkillsNeededForNextCombatLevel()’ function… Why is there a modulus (%) sign? (I know how it works, but I’m confused why it’s here for now (not very familiar with using it in Coding, but I know the math behind it))

I also want to play the entire event of updating the combat that we used to play, with all of its bells and whistles that we had prior to introducing the skills system (the particle effect, health regeneration, etc). How do we do that again? Apologies, I’m not very familiar with the event syntax

The next challenge of my combat level will be trying to increment the damage by each skill independently. In other words, I want the damage of each skill to increment when the SKILL itself increments (and the defence gets higher when the defence level increases, but I think we had that previously covered in ‘Fighter.cs’), not when the Combat level increments, so that my magic hits improve when the magic skill improves, ranged hits increase when the ranged skill increases, etc…

Apologies if this one was a little packed

Privacy & Terms