• 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!

C++ Party shared experience issue

Joined
Sep 24, 2023
Messages
36
Solutions
1
Reaction score
18
GitHub
RodrigoTolomeotti
Hello everyone,

First of all, I have some programming experience and familiarity with it. However, this is my first encounter with Tibia's code, and it's also a different programming language than the one I'm used to.

I'm facing an issue in my C++ code related to calculating shared experience in an online game. I have a party system where players can hunt together and share the experience gained from creature kills. However, I'm having difficulties ensuring that the experience is evenly distributed among all party members when one player deals the majority of the damage.

Here's a summary of my problem:

  • When a party is hunting, and only one of the players can deal around 80% to 90% of the creature's damage, while the other party members deal the remaining damage, the experience is being distributed based on the damage dealt by the attacker, rather than evenly among all party members.
  • If only the party leader deals 100% of the damage, the experience is incorrectly share.
    1695515234563.png

  • If only one party member deals 100% of the damage, the experience is share incorrectly.1695514933273.png

  • Here, the player "Teste02" dealt approximately 80% to 90% of the creature's health, while the player "Teste01" dealt the remaining damage.
    1695515095004.png

  • If each member, including the party leader, deals around 50% of the creature's health, the experience is divided correctly.
    1695515184814.png

  • I need the experience to be shared equally among all party members, regardless of who dealt the most damage. Since my server version is 7.2 to 7.6, and the vocations deal higher damage than usual, not all players will be able to deal a high percentage of damage to the creatures they are hunting when they are in a party.
I've made some modifications to my code, but I haven't been able to find the correct solution. Could someone help me understand how I can adjust the logic for shared experience calculation so that it's evenly distributed among all party members?
Here's the relevant code snippet where I'm facing this issue.

With my experience, I've managed to identify some relevant functions, but I must admit that I'm confused. Here they are:

getDamageRatio Function
C++:
double Creature::getDamageRatio(Creature* attacker) const
{
    uint32_t totalDamage = 0;
    uint32_t attackerDamage = 0;

    for (const auto& it : damageMap) {
        const CountBlock_t& cb = it.second;
        totalDamage += cb.total;
        if (it.first == attacker->getID()) {
            attackerDamage += cb.total;
        }
    }

    if (totalDamage == 0) {
        return 0;
    }

    return (static_cast<double>(attackerDamage) / totalDamage);
}


onDeath Function
C++:
void Creature::onDeath()
{
    bool lastHitUnjustified = false;
    bool mostDamageUnjustified = false;
    Creature* lastHitCreature = g_game.getCreatureByID(lastHitCreatureId);
    Creature* lastHitCreatureMaster;
    if (lastHitCreature) {
        lastHitUnjustified = lastHitCreature->onKilledCreature(this);
        lastHitCreatureMaster = lastHitCreature->getMaster();
    } else {
        lastHitCreatureMaster = nullptr;
    }

    Creature* mostDamageCreature = nullptr;

    const int64_t timeNow = OTSYS_TIME();
    const uint32_t inFightTicks = g_config.getNumber(ConfigManager::PZ_LOCKED);
    int32_t mostDamage = 0;
    std::map<Creature*, uint64_t> experienceMap;
    for (const auto& it : damageMap) {
        if (Creature* attacker = g_game.getCreatureByID(it.first)) {
            CountBlock_t cb = it.second;
            if ((cb.total > mostDamage && (timeNow - cb.ticks <= inFightTicks))) {
                mostDamage = cb.total;
                mostDamageCreature = attacker;
            }

            if (attacker != this) {
                uint64_t gainExp = getGainedExperience(attacker);
                if (Player* attackerPlayer = attacker->getPlayer()) {
                    attackerPlayer->removeAttacked(getPlayer());

                    Party* party = attackerPlayer->getParty();
                    if (party && party->getLeader() && party->isSharedExperienceActive() &&
                        party->isSharedExperienceEnabled()) {
                        attacker = party->getLeader();
                    }
                }

                auto tmpIt = experienceMap.find(attacker);
                if (tmpIt == experienceMap.end()) {
                    experienceMap[attacker] = gainExp;
                } else {
                    tmpIt->second += gainExp;
                }
            }
        }
    }

    for (const auto& it : experienceMap) {
        it.first->onGainExperience(it.second, this);
    }

    if (mostDamageCreature) {
        if (mostDamageCreature != lastHitCreature && mostDamageCreature != lastHitCreatureMaster) {
            Creature* mostDamageCreatureMaster = mostDamageCreature->getMaster();
            if (lastHitCreature != mostDamageCreatureMaster &&
                (!lastHitCreatureMaster || mostDamageCreatureMaster != lastHitCreatureMaster)) {
                mostDamageUnjustified = mostDamageCreature->onKilledCreature(this, false);
            }
        }
    }

    bool droppedCorpse = dropCorpse(lastHitCreature, mostDamageCreature, lastHitUnjustified, mostDamageUnjustified);
    death(lastHitCreature);

    if (master) {
        setMaster(nullptr);
    }

    if (droppedCorpse) {
        g_game.removeCreature(this, false);
    }
}

I've tried to change it and reformulate the logic a bit to what I need, but without any success.

I'd be grateful if anyone could help!
 
Last edited:
I've done a lot with my own (little) knowledge and with the help of gpt chat, but I totally don't know how to deal with this party :/
I have a feeling it might be in the return at the bottom of the lua script. It’s definitely weird, what we will do is add some code that prints to the console window each stage as it happens.
Post automatically merged:

I've done a lot with my own (little) knowledge and with the help of gpt chat, but I totally don't know how to deal with this party :/
Okay, I've installed 10.98 and this code, I've added all of these configs exactly as we have them now, party.cpp, party.lua was changed to multiplier 1.0 not 1.2, bonus for stamina 1.0, everything else is the same, it's 100% working.
Post automatically merged:

1732333287445.webp

Here is 100 exp on rat, 3 different vocations, +30% /3 members is 43.3

party.lua
LUA:
function Party:onJoin(player)
    if hasEventCallback(EVENT_CALLBACK_ONJOIN) then
        return EventCallback(EVENT_CALLBACK_ONJOIN, self, player)
    else
        return true
    end
end

function Party:onLeave(player)
    if hasEventCallback(EVENT_CALLBACK_ONLEAVE) then
        return EventCallback(EVENT_CALLBACK_ONLEAVE, self, player)
    else
        return true
    end
end

function Party:onDisband()
    if hasEventCallback(EVENT_CALLBACK_ONDISBAND) then
        return EventCallback(EVENT_CALLBACK_ONDISBAND, self)
    else
        return true
    end
end

function Party:onShareExperience(exp)
    local sharedExperienceMultiplier = 1.00
    local vocationsIds = {}
    local rawExp = exp

    local vocationId = self:getLeader():getVocation():getBase():getId()
    if vocationId ~= VOCATION_NONE then
        table.insert(vocationsIds, vocationId)
    end

    for _, member in ipairs(self:getMembers()) do
        vocationId = member:getVocation():getBase():getId()
        if not table.contains(vocationsIds, vocationId) and vocationId ~= VOCATION_NONE then
            table.insert(vocationsIds, vocationId)
        end
    end

    local size = #vocationsIds
    
    if size == 2 then  -- size represents number of different vocations
        sharedExperienceMultiplier = 1.0 + (15 / 100) -- 10 should represent 10%
    elseif size == 3 then
        sharedExperienceMultiplier = 1.0 + (30 / 100) -- 30 should represent 30%
    elseif size >= 4 then -- in case you add more vocations this wont cause issues.
        sharedExperienceMultiplier = 1.0 + (50 / 100) -- 50 should represent 50%
    end

    exp = math.ceil((exp * sharedExperienceMultiplier) / (#self:getMembers() + 1))
    return hasEventCallback(EVENT_CALLBACK_ONSHAREEXPERIENCE) and EventCallback(EVENT_CALLBACK_ONSHAREEXPERIENCE, self, exp, rawExp) or exp
end

player.lua
LUA:
function Player:onBrowseField(position)
    if hasEventCallback(EVENT_CALLBACK_ONBROWSEFIELD) then
        return EventCallback(EVENT_CALLBACK_ONBROWSEFIELD, self, position)
    end
    return true
end

function Player:onLook(thing, position, distance)
    local description = ""
    if hasEventCallback(EVENT_CALLBACK_ONLOOK) then
        description = EventCallback(EVENT_CALLBACK_ONLOOK, self, thing, position, distance, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onLookInBattleList(creature, distance)
    local description = ""
    if hasEventCallback(EVENT_CALLBACK_ONLOOKINBATTLELIST) then
        description = EventCallback(EVENT_CALLBACK_ONLOOKINBATTLELIST, self, creature, distance, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onLookInTrade(partner, item, distance)
    local description = "You see " .. item:getDescription(distance)
    if hasEventCallback(EVENT_CALLBACK_ONLOOKINTRADE) then
        description = EventCallback(EVENT_CALLBACK_ONLOOKINTRADE, self, partner, item, distance, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onLookInShop(itemType, count, description)
    local description = "You see " .. description
    if hasEventCallback(EVENT_CALLBACK_ONLOOKINSHOP) then
        description = EventCallback(EVENT_CALLBACK_ONLOOKINSHOP, self, itemType, count, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onMoveItem(item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    if hasEventCallback(EVENT_CALLBACK_ONMOVEITEM) then
        return EventCallback(EVENT_CALLBACK_ONMOVEITEM, self, item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    end
    return RETURNVALUE_NOERROR
end

function Player:onItemMoved(item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    if hasEventCallback(EVENT_CALLBACK_ONITEMMOVED) then
        EventCallback(EVENT_CALLBACK_ONITEMMOVED, self, item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    end
end

function Player:onMoveCreature(creature, fromPosition, toPosition)
    if hasEventCallback(EVENT_CALLBACK_ONMOVECREATURE) then
        return EventCallback(EVENT_CALLBACK_ONMOVECREATURE, self, creature, fromPosition, toPosition)
    end
    return true
end

function Player:onReportRuleViolation(targetName, reportType, reportReason, comment, translation)
    if hasEventCallback(EVENT_CALLBACK_ONREPORTRULEVIOLATION) then
        EventCallback(EVENT_CALLBACK_ONREPORTRULEVIOLATION, self, targetName, reportType, reportReason, comment, translation)
    end
end

function Player:onReportBug(message, position, category)
    if hasEventCallback(EVENT_CALLBACK_ONREPORTBUG) then
        return EventCallback(EVENT_CALLBACK_ONREPORTBUG, self, message, position, category)
    end
    return true
end

function Player:onTurn(direction)
    if hasEventCallback(EVENT_CALLBACK_ONTURN) then
        return EventCallback(EVENT_CALLBACK_ONTURN, self, direction)
    end
    return true
end

function Player:onTradeRequest(target, item)
    if hasEventCallback(EVENT_CALLBACK_ONTRADEREQUEST) then
        return EventCallback(EVENT_CALLBACK_ONTRADEREQUEST, self, target, item)
    end
    return true
end

function Player:onTradeAccept(target, item, targetItem)
    if hasEventCallback(EVENT_CALLBACK_ONTRADEACCEPT) then
        return EventCallback(EVENT_CALLBACK_ONTRADEACCEPT, self, target, item, targetItem)
    end
    return true
end

function Player:onTradeCompleted(target, item, targetItem, isSuccess)
    if hasEventCallback(EVENT_CALLBACK_ONTRADECOMPLETED) then
        EventCallback(EVENT_CALLBACK_ONTRADECOMPLETED, self, target, item, targetItem, isSuccess)
    end
end

local soulCondition = Condition(CONDITION_SOUL, CONDITIONID_DEFAULT)
soulCondition:setTicks(4 * 60 * 1000)
soulCondition:setParameter(CONDITION_PARAM_SOULGAIN, 1)

local function useStamina(player)
    local staminaMinutes = player:getStamina()
    if staminaMinutes == 0 then
        return
    end

    local playerId = player:getId()
    if not nextUseStaminaTime[playerId] then
        nextUseStaminaTime[playerId] = 0
    end

    local currentTime = os.time()
    local timePassed = currentTime - nextUseStaminaTime[playerId]
    if timePassed <= 0 then
        return
    end

    if timePassed > 60 then
        if staminaMinutes > 2 then
            staminaMinutes = staminaMinutes - 2
        else
            staminaMinutes = 0
        end
        nextUseStaminaTime[playerId] = currentTime + 120
    else
        staminaMinutes = staminaMinutes - 1
        nextUseStaminaTime[playerId] = currentTime + 60
    end
    player:setStamina(staminaMinutes)
end

function Player:onGainExperience(source, exp, rawExp)
    if not source or source:isPlayer() then
        return exp
    end

    -- Soul regeneration
    local vocation = self:getVocation()
    if self:getSoul() < vocation:getMaxSoul() and exp >= self:getLevel() then
        soulCondition:setParameter(CONDITION_PARAM_SOULTICKS, vocation:getSoulGainTicks() * 1000)
        self:addCondition(soulCondition)
    end

    -- Apply experience stage multiplier
    exp = exp * Game.getExperienceStage(self:getLevel())

    -- Stamina modifier
    if configManager.getBoolean(configKeys.STAMINA_SYSTEM) then
        useStamina(self)

        local staminaMinutes = self:getStamina()
        if staminaMinutes > 2400 and self:isPremium() then
            exp = exp * 1.0
        elseif staminaMinutes <= 840 then
            exp = exp * 0.5
        end
    end

    return hasEventCallback(EVENT_CALLBACK_ONGAINEXPERIENCE) and EventCallback(EVENT_CALLBACK_ONGAINEXPERIENCE, self, source, exp, rawExp) or exp
end

function Player:onLoseExperience(exp)
    return hasEventCallback(EVENT_CALLBACK_ONLOSEEXPERIENCE) and EventCallback(EVENT_CALLBACK_ONLOSEEXPERIENCE, self, exp) or exp
end

function Player:onGainSkillTries(skill, tries)
    if APPLY_SKILL_MULTIPLIER == false then
        return hasEventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES) and EventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES, self, skill, tries) or tries
    end

    if skill == SKILL_MAGLEVEL then
        tries = tries * configManager.getNumber(configKeys.RATE_MAGIC)
        return hasEventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES) and EventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES, self, skill, tries) or tries
    end
    tries = tries * configManager.getNumber(configKeys.RATE_SKILL)
    return hasEventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES) and EventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES, self, skill, tries) or tries
end

function Player:onWrapItem(item)
    local topCylinder = item:getTopParent()
    if not topCylinder then
        return
    end

    local tile = Tile(topCylinder:getPosition())
    if not tile then
        return
    end

    local house = tile:getHouse()
    if not house then
        self:sendCancelMessage("You can only wrap and unwrap this item inside a house.")
        return
    end

    if house ~= self:getHouse() and not string.find(house:getAccessList(SUBOWNER_LIST):lower(), "%f[%a]" .. self:getName():lower() .. "%f[%A]") then
        self:sendCancelMessage("You cannot wrap or unwrap items from a house, which you are only guest to.")
        return
    end

    local wrapId = item:getAttribute("wrapid")
    if wrapId == 0 then
        return
    end

    if not hasEventCallback(EVENT_CALLBACK_ONWRAPITEM) or EventCallback(EVENT_CALLBACK_ONWRAPITEM, self, item) then
        local oldId = item:getId()
        item:remove(1)
        local item = tile:addItem(wrapId)
        if item then
            item:setAttribute("wrapid", oldId)
        end
    end
end

party.cpp
C++:
void Party::shareExperience(uint64_t experience, Creature* source/* = nullptr*/, const Position& monsterPosition)
{
    // Create a temporary list to hold eligible members for experience sharing
    std::vector<Player*> eligibleMembers;

    // Check if the leader can receive experience, range of monster killed, not in PZ
    if (leader->getZone() != ZONE_PROTECTION && Position::areInRange<EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_FLOORS>(monsterPosition, leader->getPosition())) {
        eligibleMembers.push_back(leader);
    }

    // Check if party members can receive experience, range of monster killed, not in PZ
    for (Player* member : memberList) {
        if (member->getZone() != ZONE_PROTECTION && Position::areInRange<EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_FLOORS>(monsterPosition, member->getPosition())) {
            eligibleMembers.push_back(member);
        }
    }

    uint64_t shareExperience = experience;
    g_events->eventPartyOnShareExperience(this, shareExperience);

    if (std::find(eligibleMembers.begin(), eligibleMembers.end(), leader) != eligibleMembers.end()) {
        leader->onGainSharedExperience(shareExperience, source);
    }

    for (auto it = memberList.begin(); it != memberList.end(); ++it) {
        if (std::find(eligibleMembers.begin(), eligibleMembers.end(), (*it)) != eligibleMembers.end()) {
            shareExperience = shareExperience; //This ensures that the expShare amount is not multiplied in any way
            (*it)->onGainSharedExperience(shareExperience, source);
        }
    }
}
 
Last edited:
I have a feeling it might be in the return at the bottom of the lua script. It’s definitely weird, what we will do is add some code that prints to the console window each stage as it happens.
Post automatically merged:


Okay, I've installed 10.98 and this code, I've added all of these configs exactly as we have them now, party.cpp, party.lua was changed to multiplier 1.0 not 1.2, bonus for stamina 1.0, everything else is the same, it's 100% working.
Post automatically merged:

View attachment 88443

Here is 100 exp on rat, 3 different vocations, +30% /3 members is 43.3

party.lua
LUA:
function Party:onJoin(player)
    if hasEventCallback(EVENT_CALLBACK_ONJOIN) then
        return EventCallback(EVENT_CALLBACK_ONJOIN, self, player)
    else
        return true
    end
end

function Party:onLeave(player)
    if hasEventCallback(EVENT_CALLBACK_ONLEAVE) then
        return EventCallback(EVENT_CALLBACK_ONLEAVE, self, player)
    else
        return true
    end
end

function Party:onDisband()
    if hasEventCallback(EVENT_CALLBACK_ONDISBAND) then
        return EventCallback(EVENT_CALLBACK_ONDISBAND, self)
    else
        return true
    end
end

function Party:onShareExperience(exp)
    local sharedExperienceMultiplier = 1.0 --20%
    local vocationsIds = {}
    local rawExp = exp

    local vocationId = self:getLeader():getVocation():getBase():getId()
    if vocationId ~= VOCATION_NONE then
        table.insert(vocationsIds, vocationId)
    end

    for _, member in ipairs(self:getMembers()) do
        vocationId = member:getVocation():getBase():getId()
        if not table.contains(vocationsIds, vocationId) and vocationId ~= VOCATION_NONE then
            table.insert(vocationsIds, vocationId)
        end
    end

    local size = #vocationsIds
 
    if size == 2 then
        sharedExperienceMultiplier = 1.0 + (15 / 100) -- 10 should represent 10%
    elseif size == 3 then
        sharedExperienceMultiplier = 1.0 + (30 / 100) -- 10 should represent 10%
    elseif size >= 4 then -- in case you add more vocations this wont cause issues.
        sharedExperienceMultiplier = 1.0 + (50 / 100) -- 10 should represent 10%
    end

    exp = math.ceil((exp * sharedExperienceMultiplier) / (#self:getMembers() + 1))
    return hasEventCallback(EVENT_CALLBACK_ONSHAREEXPERIENCE) and EventCallback(EVENT_CALLBACK_ONSHAREEXPERIENCE, self, exp, rawExp) or exp
end

player.lua
LUA:
function Player:onBrowseField(position)
    if hasEventCallback(EVENT_CALLBACK_ONBROWSEFIELD) then
        return EventCallback(EVENT_CALLBACK_ONBROWSEFIELD, self, position)
    end
    return true
end

function Player:onLook(thing, position, distance)
    local description = ""
    if hasEventCallback(EVENT_CALLBACK_ONLOOK) then
        description = EventCallback(EVENT_CALLBACK_ONLOOK, self, thing, position, distance, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onLookInBattleList(creature, distance)
    local description = ""
    if hasEventCallback(EVENT_CALLBACK_ONLOOKINBATTLELIST) then
        description = EventCallback(EVENT_CALLBACK_ONLOOKINBATTLELIST, self, creature, distance, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onLookInTrade(partner, item, distance)
    local description = "You see " .. item:getDescription(distance)
    if hasEventCallback(EVENT_CALLBACK_ONLOOKINTRADE) then
        description = EventCallback(EVENT_CALLBACK_ONLOOKINTRADE, self, partner, item, distance, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onLookInShop(itemType, count, description)
    local description = "You see " .. description
    if hasEventCallback(EVENT_CALLBACK_ONLOOKINSHOP) then
        description = EventCallback(EVENT_CALLBACK_ONLOOKINSHOP, self, itemType, count, description)
    end
    self:sendTextMessage(MESSAGE_INFO_DESCR, description)
end

function Player:onMoveItem(item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    if hasEventCallback(EVENT_CALLBACK_ONMOVEITEM) then
        return EventCallback(EVENT_CALLBACK_ONMOVEITEM, self, item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    end
    return RETURNVALUE_NOERROR
end

function Player:onItemMoved(item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    if hasEventCallback(EVENT_CALLBACK_ONITEMMOVED) then
        EventCallback(EVENT_CALLBACK_ONITEMMOVED, self, item, count, fromPosition, toPosition, fromCylinder, toCylinder)
    end
end

function Player:onMoveCreature(creature, fromPosition, toPosition)
    if hasEventCallback(EVENT_CALLBACK_ONMOVECREATURE) then
        return EventCallback(EVENT_CALLBACK_ONMOVECREATURE, self, creature, fromPosition, toPosition)
    end
    return true
end

function Player:onReportRuleViolation(targetName, reportType, reportReason, comment, translation)
    if hasEventCallback(EVENT_CALLBACK_ONREPORTRULEVIOLATION) then
        EventCallback(EVENT_CALLBACK_ONREPORTRULEVIOLATION, self, targetName, reportType, reportReason, comment, translation)
    end
end

function Player:onReportBug(message, position, category)
    if hasEventCallback(EVENT_CALLBACK_ONREPORTBUG) then
        return EventCallback(EVENT_CALLBACK_ONREPORTBUG, self, message, position, category)
    end
    return true
end

function Player:onTurn(direction)
    if hasEventCallback(EVENT_CALLBACK_ONTURN) then
        return EventCallback(EVENT_CALLBACK_ONTURN, self, direction)
    end
    return true
end

function Player:onTradeRequest(target, item)
    if hasEventCallback(EVENT_CALLBACK_ONTRADEREQUEST) then
        return EventCallback(EVENT_CALLBACK_ONTRADEREQUEST, self, target, item)
    end
    return true
end

function Player:onTradeAccept(target, item, targetItem)
    if hasEventCallback(EVENT_CALLBACK_ONTRADEACCEPT) then
        return EventCallback(EVENT_CALLBACK_ONTRADEACCEPT, self, target, item, targetItem)
    end
    return true
end

function Player:onTradeCompleted(target, item, targetItem, isSuccess)
    if hasEventCallback(EVENT_CALLBACK_ONTRADECOMPLETED) then
        EventCallback(EVENT_CALLBACK_ONTRADECOMPLETED, self, target, item, targetItem, isSuccess)
    end
end

local soulCondition = Condition(CONDITION_SOUL, CONDITIONID_DEFAULT)
soulCondition:setTicks(4 * 60 * 1000)
soulCondition:setParameter(CONDITION_PARAM_SOULGAIN, 1)

local function useStamina(player)
    local staminaMinutes = player:getStamina()
    if staminaMinutes == 0 then
        return
    end

    local playerId = player:getId()
    if not nextUseStaminaTime[playerId] then
        nextUseStaminaTime[playerId] = 0
    end

    local currentTime = os.time()
    local timePassed = currentTime - nextUseStaminaTime[playerId]
    if timePassed <= 0 then
        return
    end

    if timePassed > 60 then
        if staminaMinutes > 2 then
            staminaMinutes = staminaMinutes - 2
        else
            staminaMinutes = 0
        end
        nextUseStaminaTime[playerId] = currentTime + 120
    else
        staminaMinutes = staminaMinutes - 1
        nextUseStaminaTime[playerId] = currentTime + 60
    end
    player:setStamina(staminaMinutes)
end

function Player:onGainExperience(source, exp, rawExp)
    if not source or source:isPlayer() then
        return exp
    end

    -- Soul regeneration
    local vocation = self:getVocation()
    if self:getSoul() < vocation:getMaxSoul() and exp >= self:getLevel() then
        soulCondition:setParameter(CONDITION_PARAM_SOULTICKS, vocation:getSoulGainTicks() * 1000)
        self:addCondition(soulCondition)
    end

    -- Apply experience stage multiplier
    exp = exp * Game.getExperienceStage(self:getLevel())

    -- Stamina modifier
    if configManager.getBoolean(configKeys.STAMINA_SYSTEM) then
        useStamina(self)

        local staminaMinutes = self:getStamina()
        if staminaMinutes > 2400 and self:isPremium() then
            exp = exp * 1.0
        elseif staminaMinutes <= 840 then
            exp = exp * 0.5
        end
    end

    return hasEventCallback(EVENT_CALLBACK_ONGAINEXPERIENCE) and EventCallback(EVENT_CALLBACK_ONGAINEXPERIENCE, self, source, exp, rawExp) or exp
end

function Player:onLoseExperience(exp)
    return hasEventCallback(EVENT_CALLBACK_ONLOSEEXPERIENCE) and EventCallback(EVENT_CALLBACK_ONLOSEEXPERIENCE, self, exp) or exp
end

function Player:onGainSkillTries(skill, tries)
    if APPLY_SKILL_MULTIPLIER == false then
        return hasEventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES) and EventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES, self, skill, tries) or tries
    end

    if skill == SKILL_MAGLEVEL then
        tries = tries * configManager.getNumber(configKeys.RATE_MAGIC)
        return hasEventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES) and EventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES, self, skill, tries) or tries
    end
    tries = tries * configManager.getNumber(configKeys.RATE_SKILL)
    return hasEventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES) and EventCallback(EVENT_CALLBACK_ONGAINSKILLTRIES, self, skill, tries) or tries
end

function Player:onWrapItem(item)
    local topCylinder = item:getTopParent()
    if not topCylinder then
        return
    end

    local tile = Tile(topCylinder:getPosition())
    if not tile then
        return
    end

    local house = tile:getHouse()
    if not house then
        self:sendCancelMessage("You can only wrap and unwrap this item inside a house.")
        return
    end

    if house ~= self:getHouse() and not string.find(house:getAccessList(SUBOWNER_LIST):lower(), "%f[%a]" .. self:getName():lower() .. "%f[%A]") then
        self:sendCancelMessage("You cannot wrap or unwrap items from a house, which you are only guest to.")
        return
    end

    local wrapId = item:getAttribute("wrapid")
    if wrapId == 0 then
        return
    end

    if not hasEventCallback(EVENT_CALLBACK_ONWRAPITEM) or EventCallback(EVENT_CALLBACK_ONWRAPITEM, self, item) then
        local oldId = item:getId()
        item:remove(1)
        local item = tile:addItem(wrapId)
        if item then
            item:setAttribute("wrapid", oldId)
        end
    end
end

player.cpp
C++:
void Party::shareExperience(uint64_t experience, Creature* source/* = nullptr*/, const Position& monsterPosition)
{
    // Create a temporary list to hold eligible members for experience sharing
    std::vector<Player*> eligibleMembers;

    // Check if the leader can receive experience, range of monster killed, not in PZ
    if (leader->getZone() != ZONE_PROTECTION && Position::areInRange<EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_FLOORS>(monsterPosition, leader->getPosition())) {
        eligibleMembers.push_back(leader);
    }

    // Check if party members can receive experience, range of monster killed, not in PZ
    for (Player* member : memberList) {
        if (member->getZone() != ZONE_PROTECTION && Position::areInRange<EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_RANGE, EXPERIENCE_SHARE_FLOORS>(monsterPosition, member->getPosition())) {
            eligibleMembers.push_back(member);
        }
    }

    uint64_t shareExperience = experience;
    g_events->eventPartyOnShareExperience(this, shareExperience);

    if (std::find(eligibleMembers.begin(), eligibleMembers.end(), leader) != eligibleMembers.end()) {
        leader->onGainSharedExperience(shareExperience, source);
    }

    for (auto it = memberList.begin(); it != memberList.end(); ++it) {
        if (std::find(eligibleMembers.begin(), eligibleMembers.end(), (*it)) != eligibleMembers.end()) {
            shareExperience = shareExperience; //This ensures that the expShare amount is not multiplied in any way
            (*it)->onGainSharedExperience(shareExperience, source);
        }
    }
}

and shouldn't this last script go to party.cpp instead of player.cpp? :D

I don't have a shared function for replacement in player.cpp from what I can see. Should I add it somewhere specific?

I need to get things in order with the functions, because yesterday I tried to do it in different ways and I think I messed up something in party.cpp 😅
 
Last edited:
and shouldn't this last script go to party.cpp instead of player.cpp? :D

I don't have a shared function for replacement in player.cpp from what I can see. Should I add it somewhere specific?

I need to get things in order with the functions, because yesterday I tried to do it in different ways and I think I messed up something in party.cpp 😅
Yes, you are correct, that belongs in party.cpp, my bad!
Post automatically merged:

Nice :D i'll try too change it
Make sure you follow the code that was added to all of the scripts if you're also changing source code, this source code change is only to make party give exp within distance of monster killed, it does not perform calculations, you can use it to disable party activity function and allow members to share gain exp if one of the members leaves to go repot or whatever.
 
Last edited:
Back
Top