• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

Feature Monster Skill Advancement System

I'm not familiar with the source code of said distro (assuming this part of the code has not changed) then my concern is if you increase MonsterTypes values instead of Monster values, this will increase for all Monsters of that type and not specificly to the Monster which is actually getting attacked or which attacks someone
 
Ohh ok I totally misunderstood you. I thought you were jumping in to explain a separate feature that also increased the defense attribute.

You're just saying that the monsters' "skill" should be part of the formulas that determine the monsters defense as well as damage.
Yes, that's what I meant, they are not only hitting stronger but also defending better as their skill increases.
 
I'm not familiar with the source code of said distro (assuming this part of the code has not changed) then my concern is if you increase MonsterTypes values instead of Monster values, this will increase for all Monsters of that type and not specificly to the Monster which is actually getting attacked or which attacks someone
A great question. I tested this with prints already to confirm that the stat is only changing for the individual monster. Think of it like any other attribute on a monster, like HP: If you attack one dragon and his attribute "currentHealth" changes, the current health for all dragons does not change. Only the one you are attacking.

I also tested by letting one monster train for hours, until his damage was really high, then I created a fresh monster next to it, and his damage was normal.
 
A great question. I tested this with prints already to confirm that the stat is only changing for the individual monster. Think of it like any other attribute on a monster, like HP: If you attack one dragon and his attribute "currentHealth" changes, the current health for all dragons does not change. Only the one you are attacking.

I also tested by letting one monster train for hours, until his damage was really high, then I created a fresh monster next to it, and his damage was normal.
There's certainly a difference between health and what I was refering to.
Health in this case is saved in the Monster class (derrived from Creature) which can be changed and is for every spawned monster (of the same type) individual that's correct.
However if you change values in the MonsterType itself this changes applies to all monsters of the same types which are spawned after the change.
Just take a look at your code and you'll quickly notice what I mean in this case.
C++:
void Monster::addSkillAdvance(int count) {
    const float TRIES_INCREASE_FACTOR = (float)mType->info.multiplier / 1000;    // Factor by which tries increase for each subsequent level
    const uint64_t MAX_TRIES_VALUE = std::numeric_limits<uint64_t>::max();       // Maximum value for uint64_t
//    std::cout << "TRIES_INCREASE_FACTOR: " << TRIES_INCREASE_FACTOR << std::endl;
    skillTries += count;// Increment skillTries by the count
 
//  std::cout << "Current skill level: " << mType->info.skill << std::endl;
//  std::cout << "Current skill tries: " << skillTries << std::endl;

    uint64_t levelUpThreshold = mType->info.tries; // the level up threshold is taken from the monster itself, if he's leveled up already then this has been updated in the while loop
//    std::cout << "Current level up threshhold: " << levelUpThreshold << std::endl;
 
    while (skillTries >= levelUpThreshold) { // While skillTries exceeds or equals the level up threshold
        skillTries -= levelUpThreshold;// Reset skillTries to 0
        mType->info.skill++;// Increment the skill level by 1
        levelUpThreshold *= TRIES_INCREASE_FACTOR;  // Update the level up threshold for the next level
        if (levelUpThreshold > MAX_TRIES_VALUE) { // Check for overflow, this function is exponential so eventually the tries needed to level up will be greater than uint64_t can handle
            std::cout << "Overflow detected. Exiting function." << std::endl;
            return;
        }
        mType->info.tries = static_cast<uint64_t>(levelUpThreshold); //convert float back to int and write back to the "tries" attribute for the monster
//      std::cout << "Monster's skill leveled up! New skill level: " << mType->info.skill << std::endl;
//        std::cout << "New skill level up threshhold: " << levelUpThreshold << std::endl;
    }
}
mType->info.skill++ here you increase a value which is in the MonsterType class this counts for all monsters in this case, if you had the skill inside either the Creature or Monster class then it would only reference to this specific monster.
 
Ohh ok I totally misunderstood you. I thought you were jumping in to explain a separate feature that also increased the defense attribute.

You're just saying that the monsters' "skill" should be part of the formulas that determine the monsters defense as well as damage.

Yes, that is already the case in Sabrehaven, nothing needs to be changed. Inside the function for "int32_t Monster::getDefense()" it pulls the same exact skill attribute (info.skill) when calculating total defense:
C++:
int32_t defenseSkill = mType->info.skill;

To clarify for all, nothing needs to be changed for Sabrehaven. Once info.skill is leveling up, the monster's damage and defense will both be increasing.
thanks
There's certainly a difference between health and what I was refering to.
Health in this case is saved in the Monster class (derrived from Creature) which can be changed and is for every spawned monster (of the same type) individual that's correct.
However if you change values in the MonsterType itself this changes applies to all monsters of the same types which are spawned after the change.
Just take a look at your code and you'll quickly notice what I mean in this case.
C++:
void Monster::addSkillAdvance(int count) {
    const float TRIES_INCREASE_FACTOR = (float)mType->info.multiplier / 1000;    // Factor by which tries increase for each subsequent level
    const uint64_t MAX_TRIES_VALUE = std::numeric_limits<uint64_t>::max();       // Maximum value for uint64_t
//    std::cout << "TRIES_INCREASE_FACTOR: " << TRIES_INCREASE_FACTOR << std::endl;
    skillTries += count;// Increment skillTries by the count
 
//  std::cout << "Current skill level: " << mType->info.skill << std::endl;
//  std::cout << "Current skill tries: " << skillTries << std::endl;

    uint64_t levelUpThreshold = mType->info.tries; // the level up threshold is taken from the monster itself, if he's leveled up already then this has been updated in the while loop
//    std::cout << "Current level up threshhold: " << levelUpThreshold << std::endl;
 
    while (skillTries >= levelUpThreshold) { // While skillTries exceeds or equals the level up threshold
        skillTries -= levelUpThreshold;// Reset skillTries to 0
        mType->info.skill++;// Increment the skill level by 1
        levelUpThreshold *= TRIES_INCREASE_FACTOR;  // Update the level up threshold for the next level
        if (levelUpThreshold > MAX_TRIES_VALUE) { // Check for overflow, this function is exponential so eventually the tries needed to level up will be greater than uint64_t can handle
            std::cout << "Overflow detected. Exiting function." << std::endl;
            return;
        }
        mType->info.tries = static_cast<uint64_t>(levelUpThreshold); //convert float back to int and write back to the "tries" attribute for the monster
//      std::cout << "Monster's skill leveled up! New skill level: " << mType->info.skill << std::endl;
//        std::cout << "New skill level up threshhold: " << levelUpThreshold << std::endl;
    }
}
mType->info.skill++ here you increase a value which is in the MonsterType class this counts for all monsters in this case, if you had the skill inside either the Creature or Monster class then it would only reference to this specific monster.
sorry to post non related thing again but would be possible to make c++ that makes monsters create loot on spawn instead of ondeath? + equip those items ex:like armor or shield or weapong so it's makes them strongers in attack/defense?
example: a rat that spawn with a mace get higher attack that a rat that does not has spawn with it
 
sorry to post non related thing again but would be possible to make c++ that makes monsters create loot on spawn instead of ondeath? + equip those items ex:like armor or shield or weapong so it's makes them strongers in attack/defense?
example: a rat that spawn with a mace get higher attack that a rat that does not has spawn with it
Yes it's possible, probably a lot easier to do in tfs 1.5 with minimal changes in source code (mostly lua) than in other distros.
But I don't want to go into detail here as you already mentioned this is offtopic, feel free to open a thread in support, I'm sure someone will be able to help with that system.
 
However if you change values in the MonsterType itself this changes applies to all monsters of the same types which are spawned after the change.
Just take a look at your code and you'll quickly notice what I mean in this case.
I see what you’re saying. Let me do some more testing to see if that’s actually happening, and I’ll update here with what I find and if it needs to be fixed.
 
A huge thank you to Evil Hero for pointing out a serious oversight in my coding. Here are the corrections that need to be made to the code so that the skill increase is only affecting each specific, individual monster, and not affecting the skill of the entire MonsterType class.

First, in monster.h, right above int32_t getDefense() final;, we need to create a new function to get the local skill (we'll use this later):
C++:
int32_t getSkill() const {
            return skill;
        }
We also need to declare skill and tries as new variables for the Monster class specifically. Underneath uint64_t skillTries = 0; add:
C++:
int32_t skill = 0;
int64_t tries = 0;

Now, in monster.cpp, in Monster::Monster, underneath skillTries = 0;, we need to initialize these variables. But, we aren't going to initialize them as 0, we're going to initialize them as whatever that particular monster's skill is:
C++:
skill = mType->info.skill;
tries = mType->info.tries;
Now, we're going to modify skill and tries that are tied to the specific monster, not the entire mType. So, we need to change anywhere in the function void Monster::addSkillAdvance that was incorrectly referencing info.skills or info.tries. Also, I added an important additional check to prevent an infinite 'while' loop:
C++:
void Monster::addSkillAdvance(int count) {
    const float TRIES_INCREASE_FACTOR = (float)mType->info.multiplier / 1000;    // unchanged
    const uint64_t MAX_TRIES_VALUE = std::numeric_limits<uint64_t>::max();       // unchanged
    skillTries += count;// unchanged, skillTries was already specific to Monster

    uint64_t levelUpThreshold = tries; //this changed, now referencing the specific tries value

    if (levelUpThreshold == 0) {//IMPORTANT! If a monster summons a creature with tries=0, your game will freeze due to infinite loop without this check! For example, novices of the cult summon chickens!
        return;
    }
 
    while (skillTries >= levelUpThreshold) { // unchanged
        skillTries -= levelUpThreshold;// unchanged
        skill++;//changed, incrementing the specific monster's skill
        levelUpThreshold *= TRIES_INCREASE_FACTOR;  // unchanged
        if (levelUpThreshold > MAX_TRIES_VALUE) { // unchanged
            std::cout << "Overflow detected. Exiting function." << std::endl;
            return;
        }
        tries = static_cast<uint64_t>(levelUpThreshold); //changed, need to write back to tries for the specific Monster
    }
}

This is not all, however. There are other functions that use a monster's skill value, and those functions are referencing mType->info.skill
either directly or indirectly. This means that while a specific monster's skills might be leveling up, all of the important formulas (damage, defense) aren't even using the new level!

So, a little further down in monster.cpp, in int32_t Monster::getDefense(), we need to change this line int32_t defenseSkill = mType->info.skill; to this:
C++:
int32_t defenseSkill = skill;//when calculating a monsters defense, which happens anytime they take a hit, now we will use the newly-created skill variable that is specific to each individual monster
Finally, we need to do something similar in combat.cpp. Find the function void Combat::getAttackValue and at the very bottom, the last else if statement, change skillValue = monster->mType->info.skill; to:
C++:
skillValue = monster->getSkill();//reference our newly created getSkill function from the first step, this returns the skill value specific to the individual monster

Here is a snapshot of it working live (I boosted the skill gain rate by 10x to make it quicker to see the difference):
Screenshot 2024-04-18 at 9.22.49 AM.png
Then, after they had all trained up a bit, I hit each one of the spiders individually with a melee attack, and they each have their own defense value based on how long they had been training:
Screenshot 2024-04-18 at 9.31.17 AM.png
Thanks again to Evil Hero for pointing out my mistake. Now, it should now be working as intended!
 
Last edited:
C++:
levelUpThreshold *= TRIES_INCREASE_FACTOR;  // unchanged
if (levelUpThreshold > MAX_TRIES_VALUE) { // unchanged
   std::cout << "Overflow detected. Exiting function." << std::endl;
   return;
}
Unsigned integer does not overflow, it wraps around. Anyway, that's not the correct way to check for it. Your levelUpThreshold, being an unsigned 64 bit integer value, can never be bigger than the biggest possible unsigned 64 bit integer value. You need to check it before the multiplication.
You may also want to save nextAdvTries, to avoid all those calculations upon every hit.
 
You're right, and it can actually be simplified down even more:

In monster.h, in private underneath uint64_t tries = 0;, declare a new variable:
C++:
float TRIES_INCREASE_FACTOR;
Then, in monster.cpp, initialize the new variable in Monster::Monster(MonsterType* mtype), underneath skillTries = 0;
C++:
TRIES_INCREASE_FACTOR = ((float)mType->info.multiplier / 1000);
By doing this, we're only calculating the tries multiplier once for each monster instead of every single time the addSkillAdvance function runs.

Finally, the addSkillAdvance function can be simplified down to this:
C++:
void Monster::addSkillAdvance(int count) {
    // Iterate skillTries by count, which is passed in each time the function runs
    skillTries += count;
    // In the case that a monster with a tries value of zero happens to attack, quit the function so it doesn't cause an infinite while loop
    if (tries == 0) {
        return;
    }
    // When the monster gets enough tries to level up
    while (skillTries >= tries) {
        // Set skillTries back to zero
        skillTries -= tries;
        // Iterate skill by 1, leveling up the monster's skill
        skill++;
        // Check for overflow before the multiplication
        if (tries > std::numeric_limits<uint64_t>::max()) {
            std::cout << "Overflow detected. Exiting function." << std::endl;
            return;
        }
        // Update tries by multiplying by the increase factor
        tries = static_cast<uint64_t>(tries * TRIES_INCREASE_FACTOR);
    }
}
Now, when a monster hits something, the only calculation happening is skillTries being increased by count. A lot of the other variables and operations were redundant, so they were removed. Then, when a monster does level up, we check for overflow before multiplying, like @kay correctly pointed out. Finally, tries value for the individual monster is multiplied by TRIES_INCREASE_FACTOR and updated for when the function runs again, i.e. next time the monster hits something.
 
we check for overflow before multiplying, like @kay correctly pointed out. Finally, tries value for the individual monster is multiplied by TRIES_INCREASE_FACTOR and updated for when the function runs again, i.e. next time the monster hits something.
You're still having the same problem. Your "tries" is an unsigned 64-bit integer, it can never be bigger than then biggest possible unsigned 64-bit integer value. That's illogical. Also, even if you confirm that "tries" is lower than that, it does not guarantee that you won't wraparound (or "overflow" as you call it, but mind that semantically it's not correct) after multiplying it by another number.
Consider that you have two numbers: a, b and you need to make sure without multiplying them that the result of said multiplication wouldn't be bigger than c. How would you solve it?
a * b <= c
a <= c/b or b <= c/a
Of course you need to make sure that a or b (whichever you divide by) is not equal 0. You already do that with "tries", but not with "skillTries", so watch out. Dividing may also not be the optimal solution, but this example should give you a better understanding.
 
Last edited:
You're right, and it can actually be simplified down even more:

In monster.h, in private underneath uint64_t tries = 0;, declare a new variable:
C++:
float TRIES_INCREASE_FACTOR;
Then, in monster.cpp, initialize the new variable in Monster::Monster(MonsterType* mtype), underneath skillTries = 0;
C++:
TRIES_INCREASE_FACTOR = ((float)mType->info.multiplier / 1000);
By doing this, we're only calculating the tries multiplier once for each monster instead of every single time the addSkillAdvance function runs.

Finally, the addSkillAdvance function can be simplified down to this:
C++:
void Monster::addSkillAdvance(int count) {
    // Iterate skillTries by count, which is passed in each time the function runs
    skillTries += count;
    // In the case that a monster with a tries value of zero happens to attack, quit the function so it doesn't cause an infinite while loop
    if (tries == 0) {
        return;
    }
    // When the monster gets enough tries to level up
    while (skillTries >= tries) {
        // Set skillTries back to zero
        skillTries -= tries;
        // Iterate skill by 1, leveling up the monster's skill
        skill++;
        // Check for overflow before the multiplication
        if (tries > std::numeric_limits<uint64_t>::max()) {
            std::cout << "Overflow detected. Exiting function." << std::endl;
            return;
        }
        // Update tries by multiplying by the increase factor
        tries = static_cast<uint64_t>(tries * TRIES_INCREASE_FACTOR);
    }
}
Now, when a monster hits something, the only calculation happening is skillTries being increased by count. A lot of the other variables and operations were redundant, so they were removed. Then, when a monster does level up, we check for overflow before multiplying, like @kay correctly pointed out. Finally, tries value for the individual monster is multiplied by TRIES_INCREASE_FACTOR and updated for when the function runs again, i.e. next time the monster hits something.
This makes monster advance skill a and defense too?
 
Yes, I know overflow is semantically incorrect, unsigned ints will wrap around. And obvoiusly, if it just wraps around, tries could never bigger than the biggest possible unsigned int. What I didn't understand, but I think I do now thanks to your explanation, is that the program doesn't "realize" that the wraparound had occurred, it will just do it. I don't know why I assumed that, but it was wrong.

You'll have to forgive my mistakes, I'm nowhere near as skilled at this as you are.

I believe I can accomplish a check that actually is useful with something like this?
C++:
if (1 >= std::numeric_limits<uint64_t>::max() / tries * TRIES_INCREASE_FACTOR) {
            std::cout << "Result would wrap around. Exiting function." << std::endl;
            return;
        }
This just checks if the biggest possible int divided by skill * TRIES_INCREASE_FACTOR is less than or equal to 1. If that fraction is ever less than or equal to 1, it means that we shouldn't do tries *= TRIES_INCREASE_FACTOR because for sure the next tries would result in a wrap around.

@johnsamir It seems you aren't very familiar with how the rest of the source code works, let me help explain it: In this distribution, the defense and attack of monsters is ALREADY based on the monsters skill. All we are doing is advancing the skill. So, yes, when the monster advances his skill, it will affect the monster's attack and defense.
 
Last edited:
Yes, I know overflow is semantically incorrect, unsigned ints will wrap around. And obvoiusly, if it just wraps around, tries could never bigger than the biggest possible unsigned int. What I didn't understand, but I think I do now thanks to your explanation, is that the program doesn't "realize" that the wraparound had occurred, it will just do it. I don't know why I assumed that, but it was wrong.

You'll have to forgive my mistakes, I'm nowhere near as skilled at this as you are.

I believe I can accomplish a check that actually is useful with something like this?
C++:
if (1 >= std::numeric_limits<uint64_t>::max() / tries * TRIES_INCREASE_FACTOR) {
            std::cout << "Result would wrap around. Exiting function." << std::endl;
            return;
        }
This just checks if the biggest possible int divided by skill * TRIES_INCREASE_FACTOR is less than or equal to 1. If that fraction is ever less than or equal to 1, it means that we shouldn't do tries *= TRIES_INCREASE_FACTOR because for sure the next tries would result in a wrap around, without actually ever performing the operation.

@johnsamir It seems you aren't very familiar with how the rest of the source code works, let me help explain it: In this distribution, the defense and attack of monsters is ALREADY based on the monsters skill. All we are doing is advancing the skill. So, yes, when the monster advances his skill, it will affect the monster's attack and defense.
I've adapted my code in order to work like nostalrius. Thank you
 
Great, glad to share this since no one else ever has. Just make sure you added all of the other changes I mentioned in the thread and you should be all set.
yes i shared the code that you amde converted to tfs 1.5 as well
 
believe I can accomplish a check that actually is useful with something like this?
C++:
if (1 >= std::numeric_limits<uint64_t>::max() / tries * TRIES_INCREASE_FACTOR) {
            std::cout << "Result would wrap around. Exiting function." << std::endl;
            return;
        }
This just checks if the biggest possible int divided by skill * TRIES_INCREASE_FACTOR is less than or equal to 1. If that fraction is ever less than or equal to 1, it means that we shouldn't do tries *= TRIES_INCREASE_FACTOR because for sure the next tries would result in a wrap around.
Did you actually mean: 1 >= std::numeric_limits<uint64_t>::max() / (tries * TRIES_INCREASE_FACTOR)?
Honestly, I don't know what the compiler is exactly going to do with it, but it's still not the right approach. Mathematically it's the same as: (tries * TRIES_INCREASE_FACTOR) >= std::numeric_limits<uint64_t>::max() which shows that you're still trying to exceed the limit on the left side of this inequality.

The problem is also that you're mixing integer and float types, which is not a good idea, unless you know exactly what you're doing, as it can provide unexpected results. Especially that you are casting one to another and back, for basically no reason. Since any number of skill points is always an integer, you don't really need all those possible decimal places at any point. You may just make your TRIES_INCREASE_FACTOR an integer as well and then calculate the new required tries with: tries * TRIES_INCREASE_FACTOR / 1000. In this case you could also define max tries as: max uint64 / TRIES_INCREASE_FACTOR, for example. (You need to disallow 0 for your increase factor, like you did with tries.)

I would also question if there is any possible scenario in which you could really "overflow" an uint64 type. Mind that 2^64 - 1 is a huge range for skill points. Especially that you are not even calculating the total number of skill points required for a certain skill level, but the number of points required to advance from one skill level to another.
 
Last edited:

Similar threads

Back
Top