• 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

Crevasse

惡名昭彰
Joined
Jan 13, 2017
Messages
149
Solutions
16
Reaction score
109
Location
Washington, D.C.
It's common knowledge at this point that monsters in oldschool Tibia had a melee skill that would actually train and level up as they fought. I've never seen anyone write/share code for this, so I'd like to share mine.

This is written for/implemented in Sabrehaven (Nostalrius), but could likely be adapted for TFS 1.x really easily.

First, we need to declare some new ints in monsters.h, in the MonsterInfo struct (I put mine directly under "uint32_t baseSpeed"):
C++:
uint32_t multiplier = 2000;
uint64_t tries = 100;
These are attributes that we will assign to each monster that is created, just like any other attribute (HP, exp, speed, etc). We will use/modify them later.

Next, in monsters.cpp, in "bool Monsters::loadMonster", underneath this bit
C++:
if ((attr = monsterNode.attribute("script"))) {
    monsterScriptList.emplace_back(mType, attr.as_string());
}
add this:
C++:
if ((attr = monsterNode.attribute("multiplier"))) {
    mType->info.multiplier = pugi::cast<uint32_t>(attr.value());
}
 
if ((attr = monsterNode.attribute("tries"))) {
    mType->info.tries = pugi::cast<uint64_t>(attr.value());
}
This allows use to add these new attributes to monsters' XML files.

Next, we'll move on to monster.h. In the public member, right above "void removeList() final;", declare this new function:
C++:
void addSkillAdvance(int count);
We also need to declare this in the private member (I put mine underneath "std::string strDescription;"):
C++:
uint64_t skillTries = 0;
Now, in monster.cpp, we need to initialize the new variable in "Monster::Monster(MonsterType* mtype)". Under "hiddenHealth = mType->info.hiddenhealth" add:
C++:
skillTries = 0;
Now, let's create the function in monster.cpp. I added mine right underneath
C++:
void Monster::addList()
{
    g_game.addMonster(this);
}
Here is the function:
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;
    }
}
As you can see, I included several prints (commented out). If you want to see the code in action or modify it, these can be useful. Or, you can just remove them. This code is pretty straightforward, read the comments on each line to understand what it does.

There is one final step: you'll notice this new function accepts int count as a parameter, we need to run this function every time a monster hits someone. That is done in combat.cpp. In "bool Combat::closeAttack", find the line "if (Monster* monster = attacker->getMonster()) {", and right underneath it, add:
C++:
monster->addSkillAdvance(1);

That's the end of the source code edits, you can compile and run. However, we aren't technically done. If you care about being truly "accurate" to oldschool, you will need to edit the monsters XML files. If you look at each of the original CipSoft .mon files, each monster has line of code with their Fist Fighting skill, and a number between 1000 and 2000 which is a multiplier that affects to how many tries are needed for each subsequent level. In my code, this is represented by the new "multiplier" attribute, which is defaulted to 2000.

Trolls, for example, have their multiplier set at 1500 in the .mon file code, so I changed "troll.xml" to look like this:
XML:
<monster name="Troll" nameDescription="a troll" race="blood" experience="20" speed="23" manacost="290" multiplier="1500">
This means that after 100 hits, the troll's attack skill will level up from 15 to 16, and it will then require 100 * (1500/1000) = 150 tries to get to the next level, then 150 * 1.5 = 225 tries to get to the next level, and so on. Each time a monster levels up, the "tries" value, which is initiated as 100 for all monsters, is updated to the new number of tries required for the next level.

I wrote bash scripts to automate the process of extracting the the monster names and multpliers from the .mon files to a table, then to insert the respective attribute to my XML monster files, I'll leave it up to you to figure out if/how you want to update your own monster XML files.

Here is a rat that I let train while I was at work:
Screenshot 2024-04-11 at 12.01.39 PM.png
 
Last edited:
You could transfer this to tfs 0.4, today I started to see these changes in the tfs 0.4 sources and they are not compatible
 
You could transfer this to tfs 0.4, today I started to see these changes in the tfs 0.4 sources and they are not compatible
I'm not going to convert this to 0.4. I've never worked with 0.4 and don't plan on doing so anytime soon.


Anyone try it in nekiro tfs 1.5?
A direct copy and paste to 1.5 is not going to work, monster melee attacks are not handled the same way as Nostalrius (combat.cpp). A quick look makes me think it might be handled in weapons.cpp, but I'm not positive. Pretty easy to start adding prints to figure it out. Good luck!
 
Thank you !

just a question: could you make mosnter double hit as in old tibia? like they would have certain chance to double hit like hunters in old tibia etc?


in tfs 1.5 we dont have close attack but ReturnValue Combat::canDoCombat(
where this
Code:
monster->addSkillAdvance(1);
should be placed?
Lua:
ReturnValue Combat::canDoCombat(Creature* attacker, Creature* target)
{
    if (!attacker) {
        return g_events->eventCreatureOnTargetCombat(attacker, target);
    }

    if (const Player* targetPlayer = target->getPlayer()) {
        if (targetPlayer->hasFlag(PlayerFlag_CannotBeAttacked)) {
            return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
        }

        if (const Player* attackerPlayer = attacker->getPlayer()) {
            if (attackerPlayer->hasFlag(PlayerFlag_CannotAttackPlayer)) {
                return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
            }

            if (isProtected(attackerPlayer, targetPlayer)) {
                return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
            }

            //nopvp-zone
            const Tile* targetPlayerTile = targetPlayer->getTile();
            if (targetPlayerTile->hasFlag(TILESTATE_NOPVPZONE)) {
                return RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE;
            } else if (attackerPlayer->getTile()->hasFlag(TILESTATE_NOPVPZONE) && !targetPlayerTile->hasFlag(TILESTATE_NOPVPZONE | TILESTATE_PROTECTIONZONE)) {
                return RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE;
            }
        }

        if (attacker->isSummon()) {
            if (const Player* masterAttackerPlayer = attacker->getMaster()->getPlayer()) {
                if (masterAttackerPlayer->hasFlag(PlayerFlag_CannotAttackPlayer)) {
                    return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
                }

                if (targetPlayer->getTile()->hasFlag(TILESTATE_NOPVPZONE)) {
                    return RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE;
                }

                if (isProtected(masterAttackerPlayer, targetPlayer)) {
                    return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
                }
            }
        }
    } else if (target->getMonster()) {
        if (const Player* attackerPlayer = attacker->getPlayer()) {
            if (attackerPlayer->hasFlag(PlayerFlag_CannotAttackMonster)) {
                return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE;
            }

            if (target->isSummon() && target->getMaster()->getPlayer() && target->getZone() == ZONE_NOPVP) {
                return RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE;
            }
        } else if (attacker->getMonster()) {
            const Creature* targetMaster = target->getMaster();

            if (!targetMaster || !targetMaster->getPlayer()) {
                const Creature* attackerMaster = attacker->getMaster();

                if (!attackerMaster || !attackerMaster->getPlayer()) {
                    return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE;
                }
            }
        }
    }

    if (g_game.getWorldType() == WORLD_TYPE_NO_PVP) {
        if (attacker->getPlayer() || (attacker->isSummon() && attacker->getMaster()->getPlayer())) {
            if (target->getPlayer()) {
                if (!isInPvpZone(attacker, target)) {
                    return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER;
                }
            }

            if (target->isSummon() && target->getMaster()->getPlayer()) {
                if (!isInPvpZone(attacker, target)) {
                    return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE;
                }
            }
        }
    }
    return g_events->eventCreatureOnTargetCombat(attacker, target);
}
 
Last edited:
just a question: could you make mosnter double hit as in old tibia? like they would have certain chance to double hit like hunters in old tibia etc?
I'm not sure exactly what you mean by "double hit." In old Tibia, monsters had no cooldown between spells, that is why a hunter could shoot 2x in a very short period of time. Is this what you mean?
 
I'm not sure exactly what you mean by "double hit." In old Tibia, monsters had no cooldown between spells, that is why a hunter could shoot 2x in a very short period of time. Is this what you mean?
exactly
Post automatically merged:

edit for tfs 1.5 i did this, it compiles ok. but haven't tested @wizinx
Lua:
bool Combat::closeAttack(Creature* attacker, Creature* target)
{
    const Position& attackerPos = attacker->getPosition();
    const Position& targetPos = target->getPosition();
    if (attackerPos.z != targetPos.z) {
        return false;
    }

    if (std::max<uint32_t>(Position::getDistanceX(attackerPos, targetPos), Position::getDistanceY(attackerPos, targetPos)) > 1) {
        return false;
    }

    //Item* weapon = nullptr;

    //uint32_t attackValue = calculateAttackValue(attacker); // Calculate attack value
    uint32_t skillValue = 0;
    uint8_t skill = SKILL_FIST;

    CombatParams combatParams;
    combatParams.blockedByArmor = true;
    combatParams.blockedByShield = true;
    combatParams.combatType = COMBAT_PHYSICALDAMAGE;

    if (Monster* monster = attacker->getMonster()) {
        monster->addSkillAdvance(1);
        skillValue = skill; // Assigning the value of 'skill' to 'skillValue'
    }

    // Use 'attackValue' somewhere in your code to prevent the warning
    // For example, you can print it:
    //std::cout << "Attack value: " << attackValue << std::endl;

    // Use 'skillValue' as before
    //std::cout << "Skill value: " << skillValue << std::endl;

    return true;
}
can you test pls? @wizinx
 
Last edited:
In Sabrehaven nothing needs to be changed for that, the distro already includes that function.

In TFS 1.5 you would need to either change the minimum creature think interval to allow it to go lower than 1 second, or re-work the function for how the game determines when a monster can cast its spell. I've never looked into it, so I don't have the answer. That would be a separate issue for the support section if you need help with that.
 
In Sabrehaven nothing needs to be changed for that, the distro already includes that function.

In TFS 1.5 you would need to either change the minimum creature think interval to allow it to go lower than 1 second, or re-work the function for how the game determines when a monster can cast its spell. I've never looked into it, so I don't have the answer. That would be a separate issue for the support section if you need help with that.
already solved thanks
 
the repo is private don't know how to oen it without commit my files :/ i adapted the code from nostalrius to tfs

Lua:
void 
bool Monster::canUseAttack(const Position& pos, const Creature* target) const
{
    if (isHostile()) {
        const Position& targetPos = target->getPosition();
        uint32_t distance = std::max<uint32_t>(Position::getDistanceX(pos, targetPos), Position::getDistanceY(pos, targetPos));
        for (const spellBlock_t& spellBlock : mType->info.attackSpells) {
            if (spellBlock.range != 0 && distance <= spellBlock.range) {
                return g_game.isSightClear(pos, targetPos, true);
            }
        }
        return false;
    }
    return true;
}

bool Monster::canUseSpell(const Position& pos, const Position& targetPos,
    const spellBlock_t& sb, uint32_t interval, bool& inRange, bool& resetTicks)
{
    inRange = true;

    if (!sb.isMelee || !extraMeleeAttack) {
        if (sb.speed > attackTicks) {
            resetTicks = false;
            return false;
        }

        if (attackTicks % sb.speed >= interval) {
            //already used this spell for this round
            return false;
        }
    }

    if (sb.range != 0 && std::max<uint32_t>(Position::getDistanceX(pos, targetPos), Position::getDistanceY(pos, targetPos)) > sb.range) {
        inRange = false;
        return false;
    }
    return true;
}

Code:
void Monster::onThinkTarget(uint32_t interval)
{
    if (!isSummon()) {
        if (mType->info.changeTargetSpeed != 0) {
            bool canChangeTarget = true;

            if (targetChangeCooldown > 0) {
                targetChangeCooldown -= interval;

                if (targetChangeCooldown <= 0) {
                    targetChangeCooldown = 0;
                    targetChangeTicks = mType->info.changeTargetSpeed;
                }
                else {
                    canChangeTarget = false;
                }
            }

            if (canChangeTarget) {
                targetChangeTicks += interval;

                if (targetChangeTicks >= mType->info.changeTargetSpeed) {
                    targetChangeTicks = 0;

                    if (mType->info.changeTargetChance > uniform_random(0, 99)) {
                        // search target strategies, if no strategy succeeds, target is not switched
                        int32_t random = uniform_random(0, 99);
                        int32_t current_strategy = 0;

                        TargetSearchType_t searchType = TARGETSEARCH_ANY;

                        do
                        {
                            int32_t strategy = 0;

                            if (current_strategy == 0) {
                                strategy = mType->info.strategyNearestEnemy;
                                searchType = TARGETSEARCH_NEAREST;
                            }
                            else if (current_strategy == 1) {
                                strategy = mType->info.strategyWeakestEnemy;
                                searchType = TARGETSEARCH_WEAKEST;
                            }
                            else if (current_strategy == 2) {
                                strategy = mType->info.strategyMostDamageEnemy;
                                searchType = TARGETSEARCH_MOSTDAMAGE;
                            }
                            else if (current_strategy == 3) {
                                strategy = mType->info.strategyRandomEnemy;
                                searchType = TARGETSEARCH_RANDOM;
                            }

                            if (random < strategy) {
                                break;
                            }

                            current_strategy++;
                            random -= strategy;
                        } while (current_strategy <= 3);

                        if (searchType != TARGETSEARCH_ANY) {
                            searchTarget(searchType);
                        }
                    }
                }
            }
        }
    }
}
edited mosnter.cpp and mosnters.cpp combat.cpp creature.cpp and others
had to add targetstrategies to monsters in xml too
like here
Code:
<?xml version="1.0" encoding="UTF-8"?>
<monster name="Darakan the Executioner" nameDescription="Darakan the Executioner" race="blood" experience="1600" speed="205">
    <health now="3480" max="3480" />
    <look type="255" head="78" body="95" legs="0" feet="95" corpse="7349" />
    <targetstrategy nearest="70" weakest="10" mostdamage="10" random="10" />
<flags>
        <flag summonable="0" />
        <flag attackable="1" />
        <flag hostile="1" />
        <flag illusionable="0" />
        <flag convinceable="0" />
        <flag pushable="0" />
        <flag canpushitems="1" />
        <flag targetdistance="1" />
        <flag canwalkonenergy="0" />
        <flag canwalkonfire="0" />
        <flag canwalkonpoison="0" />
    </flags>
    <attacks>
        <attack name="melee" interval="2000" min="0" max="-210" />
        <attack name="physical" interval="1000" chance="100" min="-72" max="-130">
            <attribute key="shootEffect" value="spear" />
        </attack>
    </attacks>
    <defenses armor="30" defense="31">
        <defense name="healing" interval="6000" chance="65" min="20" max="50">
            <attribute key="areaEffect" value="blueshimmer" />
        </defense>
    </defenses>
    <elements>
        <element icePercent="15" />
        <element firePercent="-15" />
    </elements>
    <immunities>
        <immunity outfit="1" />
        <immunity drunk="1" />
        <immunity invisible="1" />
    </immunities>
    <voices interval="2000" chance="5">
        <voice sentence="FIGHT LIKE A BARBARIAN!" yell="1" />
        <voice sentence="VICTORY IS MINE!" yell="1" />
        <voice sentence="I AM your father!" />
        <voice sentence="To be the man you have to beat the man!" />
    </voices>
</monster>
 
However, we aren't technically done. If you care about being truly "accurate" to oldschool, you will need to edit the monsters XML files. If you look at each of the original CipSoft .mon files, each monster has line of code with their Fist Fighting skill, and a number between 1000 and 2000 which is a multiplier that affects to how many tries are needed for each subsequent level. In my code, this is represented by the new "multiplier" attribute, which is defaulted to 2000.
The initial value can be different as well, it's not always 100. Plus their defense ability should also increase as they progress, since monsters use fist fighting for both attack and defense.
 
The initial value can be different as well, it's not always 100. Plus their defense ability should also increase as they progress, since monsters use fist fighting for both attack and defense.
Good point Kay, I totally forgot about defense. I'll write that up and post it here.
The initial value can be different as well, it's not always 100.
Also true. For anyone who cares to make theirs as accurate as possible, here are the very few monsters that aren't set at 100, and here is what they are set at:
blacksheep
0​
chicken
0​
dog
0​
halloweenhare
0​
rabbit
0​
sheep
0​
apocalypse
50​
bazir
50​
bug
50​
deathslicer
50​
demon
50​
ferumbras
50​
flamethrower
50​
gamemaster
50​
human
50​
infernatil
50​
magicthrower
50​
mimic
50​
orshabaal
50​
plaguethrower
50​
scarab
50​
shredderthrower
50​
spider
50​
spitnettle
50​
 
Last edited:
Good point Kay, I totally forgot about defense. I'll write that up and post it here.

Also true. For anyone who cares to make theirs as accurate as possible, here are the very few monsters that aren't set at 100, and here is what they are set at:
blacksheep
0​
chicken
0​
dog
0​
halloweenhare
0​
rabbit
0​
sheep
0​
apocalypse
50​
bazir
50​
bug
50​
deathslicer
50​
demon
50​
ferumbras
50​
flamethrower
50​
gamemaster
50​
human
50​
infernatil
50​
magicthrower
50​
mimic
50​
orshabaal
50​
plaguethrower
50​
scarab
50​
shredderthrower
50​
spider
50​
spitnettle
50​
would be possible tif you work in what kay says(that monsters increases defense too that you release the code pls?
thank you in advance
 
Yes, that's what I mean when I said:
I'll write that up and post it here.

So, I guess I realized I'm not actually sure when the monster's defense increases. I assumed it increased at the same time that their fist fighting increased, but maybe it increases as the monster takes hits? I guess I'll have to rely on @kay to confirm.

--EDIT--

Removed the code I posted earlier to avoid confusion. No changes need to be made.
 
Last edited:
So, I guess I realized I'm not actually sure when the monster's defense increases. I assumed it increased at the same time that their fist fighting increased, but maybe it increases as the monster takes hits? I guess I'll have to rely on @kay to confirm.
It increases along with their "attack skill", as it is in fact the same skill (fist fighting). In real tibia monsters were like a player that doesn't wield any weapon or shield in the inventory (those items always spawned inside a bag). Which means that they use atk/def values defined for their race and their fist fighting skill to calculate both damage and defend output (with exactly the same formulas). For players it is atk 7 / def 5, as defined in human.mon file.
From what I see, Nostalrius/Sabrehaven monsters are already reworked to have atk/def values and skill, so you only have to add skill progress (assuming that all the combat formulas are accurate), but the default TFS monsters have min-max values instead which is completely wrong.
 
Great, so it is what I understood, thank you. In that case, NO CHANGES NEED TO BE MADE for Sabrehaven.
 
Last edited:
Great, so it is what I understood, thank you. In that case, the small addition I added above will ensure the monster's defense skill increases as well as its attack skill as it progresses.
Doesn't that line increase defense value rather than skill though?
You should only increase skill as you did at first, and make sure that it's used both when calculating damage and defending output.
 
Doesn't that line increase defense value rather than skill though?
You should only increase skill as you did at first, and make sure that it's used both when calculating damage and defending output.
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.
 

Similar threads

Back
Top