diff --git a/config.lua.dist b/config.lua.dist index 3515116b06..7c7d37facb 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -128,6 +128,21 @@ experienceStages = { { minlevel = 101, multiplier = 3 } } +-- Loyalty system +loyaltyBonuses = { + { min = 0, max = 360, bonus = 0 }, + { min = 360, max = 720, bonus = 0.05 }, + { min = 720, max = 1080, bonus = 0.1 }, + { min = 1080, max = 1440, bonus = 0.15 }, + { min = 1440, max = 1800, bonus = 0.2 }, + { min = 1800, max = 2160, bonus = 0.25 }, + { min = 2160, max = 2520, bonus = 0.3 }, + { min = 2520, max = 2880, bonus = 0.35 }, + { min = 2880, max = 3240, bonus = 0.4 }, + { min = 3240, max = 3600, bonus = 0.45 }, + { min = 3600, bonus = 0.5 } +} + -- Rates -- NOTE: rateExp is not used if you have enabled stages above rateExp = 5 diff --git a/data/creaturescripts/scripts/login.lua b/data/creaturescripts/scripts/login.lua index a93abf28d2..1db1a01637 100644 --- a/data/creaturescripts/scripts/login.lua +++ b/data/creaturescripts/scripts/login.lua @@ -32,6 +32,12 @@ function onLogin(player) player:setStorageValue(PlayerStorageKeys.achievementsTotal, player:getAchievementPoints()) end + local loyaltyBonus = player:getLoyaltyBonus() + if loyaltyBonus > 0 then + loginStr = string.format("Due to your long-term loyalty to %s, you currently benefit from a %d%% bonus on all of your skills.", serverName, loyaltyBonus * 100) + player:sendTextMessage(MESSAGE_EVENT_DEFAULT, loginStr) + end + -- Events player:registerEvent("PlayerDeath") player:registerEvent("DropLoot") diff --git a/data/globalevents/scripts/startup.lua b/data/globalevents/scripts/startup.lua index a0358b354e..be31cf3632 100644 --- a/data/globalevents/scripts/startup.lua +++ b/data/globalevents/scripts/startup.lua @@ -1,3 +1,17 @@ +local function updateLoyaltyPoints() + local updatedAt = 0 + local resultId = db.storeQuery("SELECT `value` FROM `server_config` WHERE `config` = 'loyalty_updated'") + if resultId then + updatedAt = result.getNumber(resultId, "value") + result.free(resultId) + end + local now = os.time() + if (now - updatedAt) >= 24 * 60 * 60 then + db.asyncQuery("UPDATE `accounts` SET `loyalty_points` = `loyalty_points` + 1 WHERE `premium_ends_at` > " .. now) + db.query("UPDATE `server_config` SET `value` = " .. now .. " WHERE `config` = 'loyalty_updated'") + end +end + function onStartup() db.query("TRUNCATE TABLE `players_online`") db.asyncQuery("DELETE FROM `guild_wars` WHERE `status` = 0") @@ -56,6 +70,9 @@ function onStartup() end end + -- update loyalty points + updateLoyaltyPoints() + -- setup highscores variables setUpHighscores() end diff --git a/data/lib/core/game.lua b/data/lib/core/game.lua index 621e9aa4f1..0ebcaec932 100644 --- a/data/lib/core/game.lua +++ b/data/lib/core/game.lua @@ -302,3 +302,36 @@ do return transaction:commit() end end + +do + LOYALTY_TITLES = { + { points = 7000, article = "an ", title = "Enlightened" }, + { points = 6000, title = "Savant" }, + { points = 5000, title = "Sage" }, + { points = 4000, title = "Guardian" }, + { points = 3000, title = "Keeper" }, + { points = 2000, title = "Warrior" }, + { points = 1000, title = "Squire" }, + { points = 400, title = "Warden" }, + { points = 200, title = "Steward" }, + { points = 100, title = "Sentinel" }, + { points = 50, title = "Scout" } + } + + function Game.getLoyaltyTitle(points, article) + article = article or false + for i = 1, #LOYALTY_TITLES do + if points >= LOYALTY_TITLES[i].points then + local s = "" + if article then + if not LOYALTY_TITLES[i].article then + LOYALTY_TITLES[i].article = "a " + end + s = LOYALTY_TITLES[i].article + end + return s .. LOYALTY_TITLES[i].title + end + end + return "" + end +end diff --git a/data/lib/core/highscores.lua b/data/lib/core/highscores.lua index ae91a29003..dfff556c30 100644 --- a/data/lib/core/highscores.lua +++ b/data/lib/core/highscores.lua @@ -44,6 +44,7 @@ HIGHSCORES_CATEGORY_DISTANCE_FIGHTING = 7 HIGHSCORES_CATEGORY_SHIELDING = 8 HIGHSCORES_CATEGORY_FISHING = 9 HIGHSCORES_CATEGORY_ACHIEVEMENTS = 10 +HIGHSCORES_CATEGORY_LOYALTY = 11 HIGHSCORES_ACTION_BROWSE = 0 HIGHSCORES_ACTION_OWN = 1 @@ -62,7 +63,8 @@ HIGHSCORES_CATEGORIES = { [HIGHSCORES_CATEGORY_DISTANCE_FIGHTING] = { name = "Distance Fighting", type = HIGHSCORES_TYPE_SKILLS }, [HIGHSCORES_CATEGORY_SHIELDING] = { name = "Shielding", type = HIGHSCORES_TYPE_SKILLS }, [HIGHSCORES_CATEGORY_FISHING] = { name = "Fishing", type = HIGHSCORES_TYPE_SKILLS }, - [HIGHSCORES_CATEGORY_ACHIEVEMENTS] = { name = "Achievement Points", type = HIGHSCORES_TYPE_POINTS } + [HIGHSCORES_CATEGORY_ACHIEVEMENTS] = { name = "Achievement Points", type = HIGHSCORES_TYPE_POINTS }, + [HIGHSCORES_CATEGORY_LOYALTY] = { name = "Loyalty Points", type = HIGHSCORES_TYPE_POINTS } } HIGHSCORES_QUERIES = { @@ -108,6 +110,14 @@ HIGHSCORES_QUERIES = { LEFT JOIN `players` ON `players`.`id` = `storages`.`player_id` WHERE `storages`.`key` = ]] .. PlayerStorageKeys.achievementsTotal .. [[ AND `deletion` = 0 %s + ORDER BY `points` DESC, `name` ASC LIMIT ]] .. highscoresMaxResults, + [HIGHSCORES_CATEGORY_LOYALTY] = [[ + SELECT `accounts`.`id`, `players`.`name`, `vocation`, `level`, `accounts`.`loyalty_points` AS `points` + FROM `account_storage` + INNER JOIN `accounts` ON `accounts`.`id` = `account_storage`.`account_id` + INNER JOIN `players` ON `players`.`id` = `account_storage`.`value` + WHERE `account_storage`.`key` = ]] .. AccountStorageKeys.mainCharacter .. [[ + AND `players`.`deletion` = 0 %s ORDER BY `points` DESC, `name` ASC LIMIT ]] .. highscoresMaxResults } @@ -163,7 +173,7 @@ local function render(self, player) end if self.params.action == HIGHSCORES_ACTION_OWN then - self.params.page = self:findPlayer(player:getGuid()) + self.params.page = self:findPlayer(self.params.category == HIGHSCORES_CATEGORY_LOYALTY and player:getAccountId() or player:getGuid()) self.params.action = HIGHSCORES_ACTION_BROWSE end @@ -202,14 +212,14 @@ local function fetch(self) id = result.getNumber(resultId, "id"), rank = rank, name = result.getString(resultId, "name"), - title = "", -- TODO: loyalty system + title = "", vocation = clientIdsVocation[result.getNumber(resultId, "vocation")] or VOCATION_NONE, world = world, level = result.getNumber(resultId, "level"), points = result.getNumber(resultId, "points") } rank = rank + 1 - until not result.next(resultId) + until not result.next(resultId) result.free(resultId) end diff --git a/data/lib/core/player.lua b/data/lib/core/player.lua index 80d9c94e01..3838b61779 100644 --- a/data/lib/core/player.lua +++ b/data/lib/core/player.lua @@ -614,16 +614,16 @@ function Player.sendHighscores(self, entries, params) for _, entry in pairs(entries.data) do msg:addU32(entry.rank) msg:addString(entry.name) - msg:addString(entry.title) + msg:addString(params.category == HIGHSCORES_CATEGORY_LOYALTY and (Game.getLoyaltyTitle(entry.points) .. " of " .. entry.world) or "") msg:addByte(entry.vocation) msg:addString(entry.world) msg:addU16(entry.level) - msg:addByte(self:getGuid() == entry.id and 0x01 or 0x00) + msg:addByte(entry.id == (params.category == HIGHSCORES_CATEGORY_LOYALTY and self:getAccountId() or self:getGuid()) and 0x01 or 0x00) msg:addU64(entry.points) end msg:addByte(0xFF) -- unknown - msg:addByte(0x00) -- display loyalty title column + msg:addByte(params.category == HIGHSCORES_CATEGORY_LOYALTY and 0x01 or 0x00) msg:addByte(HIGHSCORES_CATEGORIES[params.category].type or 0x00) msg:addU32(entries.ts) @@ -737,3 +737,24 @@ function Player.disableLoginMusic(self) msg:delete() return true end + +function Player.addLoyaltyPoints(self, points) + self:setLoyaltyPoints(self:getLoyaltyPoints() + points) + return true +end + +function Player.removeLoyaltyPoints(self, points) + return self:addLoyaltyPoints(-points) +end + +function Player.getLoyaltyTitle(self, article) + return Game.getLoyaltyTitle(self:getLoyaltyPoints(), article or false) +end + +function Player.getLoyaltyTitleDescription(self, distance) + local pronoun = (self:getSex() == PLAYERSEX_MALE and "He" or "She") .. " is " + if distance == -1 then + pronoun = "You are " + end + return pronoun .. string.format("%s of %s.", self:getLoyaltyTitle(true), configManager.getString(configKeys.SERVER_NAME)) +end diff --git a/data/lib/core/storages.lua b/data/lib/core/storages.lua index 601e5ee419..515b591816 100644 --- a/data/lib/core/storages.lua +++ b/data/lib/core/storages.lua @@ -6,6 +6,7 @@ Reserved player storage ranges: ]]-- AccountStorageKeys = { + mainCharacter = 1, } GlobalStorageKeys = { diff --git a/data/migrations/36.lua b/data/migrations/36.lua index d0ffd9c0cb..1f2c62aacb 100644 --- a/data/migrations/36.lua +++ b/data/migrations/36.lua @@ -1,3 +1,6 @@ function onUpdateDatabase() - return false + print("> Updating database to version 37 (loyalty system)") + db.query("ALTER TABLE `accounts` ADD COLUMN `loyalty_points` SMALLINT UNSIGNED NOT NULL DEFAULT 0 AFTER `premium_ends_at`") + db.query("INSERT INTO `server_config` (`config`, `value`) VALUES ('loyalty_updated', '0')") + return true end diff --git a/data/migrations/37.lua b/data/migrations/37.lua new file mode 100644 index 0000000000..d0ffd9c0cb --- /dev/null +++ b/data/migrations/37.lua @@ -0,0 +1,3 @@ +function onUpdateDatabase() + return false +end diff --git a/data/scripts/events/player/default_onLook.lua b/data/scripts/events/player/default_onLook.lua index 6ec046a837..f721be37b3 100644 --- a/data/scripts/events/player/default_onLook.lua +++ b/data/scripts/events/player/default_onLook.lua @@ -2,6 +2,14 @@ local event = Event() event.onLook = function(self, thing, position, distance, description) local description = "You see " .. thing:getDescription(distance) + + if thing:isPlayer() then + local loyaltyDescription = thing:getLoyaltyTitleDescription(distance) + if loyaltyDescription ~= "" then + description = description .. "\n" .. loyaltyDescription + end + end + if self:getGroup():getAccess() then if thing:isItem() then description = string.format("%s\nItem ID: %d", description, thing:getId()) diff --git a/data/scripts/events/player/default_onLookInBattleList.lua b/data/scripts/events/player/default_onLookInBattleList.lua index 0e04f38261..00e08e79ac 100644 --- a/data/scripts/events/player/default_onLookInBattleList.lua +++ b/data/scripts/events/player/default_onLookInBattleList.lua @@ -2,6 +2,14 @@ local event = Event() event.onLookInBattleList = function(self, creature, distance) local description = "You see " .. creature:getDescription(distance) + + if creature:isPlayer() then + local loyaltyDescription = creature:getLoyaltyTitleDescription(distance) + if loyaltyDescription ~= "" then + description = description .. "\n" .. loyaltyDescription + end + end + if self:getGroup():getAccess() then local str = "%s\nHealth: %d / %d" if creature:isPlayer() and creature:getMaxMana() > 0 then diff --git a/data/scripts/network/cyclopedia_character.lua b/data/scripts/network/cyclopedia_character.lua index 85be7d2768..a55def2286 100644 --- a/data/scripts/network/cyclopedia_character.lua +++ b/data/scripts/network/cyclopedia_character.lua @@ -128,17 +128,15 @@ local function sendGeneralStats(self, msg) msg:addByte(CYCLOPEDIA_SKILL_MAGIC) msg:addU16(self:getMagicLevel()) msg:addU16(self:getBaseMagicLevel()) - msg:addU16(self:getBaseMagicLevel()) -- base + loyalty bonus - msg:addU16(self:getMagicLevelPercent() * 100) + msg:addU16(self:getLoyaltyMagicLevel()) + msg:addU16(self:getLoyaltyMagicLevelPercent()) for i = SKILL_FIST, SKILL_FISHING, 1 do msg:addByte(clientSkillsId[i]) msg:addU16(self:getEffectiveSkillLevel(i)) msg:addU16(self:getSkillLevel(i)) - - -- base + loyalty bonus - msg:addU16(self:getSkillLevel(i)) - msg:addU16(self:getSkillPercent(i) * 100) + msg:addU16(self:getLoyaltySkillLevel(i)) + msg:addU16(self:getLoyaltySkillPercent(i)) end msg:addByte(0) -- magic boost (element and value) @@ -177,7 +175,7 @@ local function sendBadges(self, msg) msg:addByte(0x01) -- show account info msg:addByte(0x01) -- account online msg:addByte(self:isPremium() and 1 or 0) - msg:addString("") -- loyalty title + msg:addString(self:getLoyaltyTitle() .. " of " .. configManager.getString(configKeys.SERVER_NAME)) msg:addByte(0) -- badges count -- structure: diff --git a/schema.sql b/schema.sql index 88ff960c01..0482a82819 100644 --- a/schema.sql +++ b/schema.sql @@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS `accounts` ( `secret` char(16) DEFAULT NULL, `type` int NOT NULL DEFAULT '1', `premium_ends_at` int unsigned NOT NULL DEFAULT '0', + `loyalty_points` smallint unsigned NOT NULL DEFAULT '0', `email` varchar(255) NOT NULL DEFAULT '', `creation` int NOT NULL DEFAULT '0', PRIMARY KEY (`id`), @@ -379,7 +380,7 @@ CREATE TABLE IF NOT EXISTS `towns` ( UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; -INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '36'), ('players_record', '0'); +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '36'), ('players_record', '0'), ('loyalty_updated', '0'); DROP TRIGGER IF EXISTS `ondelete_players`; DROP TRIGGER IF EXISTS `oncreate_guilds`; diff --git a/src/account.h b/src/account.h index ae7241fdcc..e4e1e23051 100644 --- a/src/account.h +++ b/src/account.h @@ -14,6 +14,7 @@ struct Account uint32_t id = 0; time_t premiumEndsAt = 0; AccountType_t accountType = ACCOUNT_TYPE_NORMAL; + uint16_t loyaltyPoints = 0; Account() = default; }; diff --git a/src/configmanager.cpp b/src/configmanager.cpp index 718d7cfbb9..533dc395e5 100644 --- a/src/configmanager.cpp +++ b/src/configmanager.cpp @@ -150,6 +150,30 @@ ExperienceStages loadXMLStages() return stages; } +LoyaltyBonuses loadLuaLoyaltyBonuses(lua_State* L) +{ + LoyaltyBonuses bonuses; + + lua_getglobal(L, "loyaltyBonuses"); + if (!lua_istable(L, -1)) { + return {}; + } + + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + const auto tableIndex = lua_gettop(L); + auto min = LuaScriptInterface::getField(L, tableIndex, "min", 0); + auto max = LuaScriptInterface::getField(L, tableIndex, "max", std::numeric_limits::max()); + auto bonus = LuaScriptInterface::getField(L, tableIndex, "bonus", 0.f); + bonuses.emplace_back(min, max, bonus); + lua_pop(L, 4); + } + lua_pop(L, 1); + + std::sort(bonuses.begin(), bonuses.end()); + return bonuses; +} + } // namespace bool ConfigManager::load() @@ -296,6 +320,9 @@ bool ConfigManager::load() } expStages.shrink_to_fit(); + loyaltyBonuses = loadLuaLoyaltyBonuses(L); + loyaltyBonuses.shrink_to_fit(); + loaded = true; lua_close(L); @@ -345,6 +372,20 @@ float ConfigManager::getExperienceStage(uint32_t level) const return std::get<2>(*it); } +float ConfigManager::getLoyaltyBonus(uint16_t points) const +{ + auto it = std::find_if(loyaltyBonuses.begin(), loyaltyBonuses.end(), [points](auto&& bonus) { + auto&& [minPoints, maxPoints, _] = bonus; + return points >= minPoints && points <= maxPoints; + }); + + if (it == loyaltyBonuses.end()) { + return 0.f; + } + + return std::get<2>(*it); +} + bool ConfigManager::setString(string_config_t what, std::string_view value) { if (what >= LAST_STRING_CONFIG) { diff --git a/src/configmanager.h b/src/configmanager.h index d86be2a802..34990a3475 100644 --- a/src/configmanager.h +++ b/src/configmanager.h @@ -5,6 +5,7 @@ #define FS_CONFIGMANAGER_H using ExperienceStages = std::vector>; +using LoyaltyBonuses = std::vector>; class ConfigManager { @@ -135,6 +136,7 @@ class ConfigManager int32_t getNumber(integer_config_t what) const; bool getBoolean(boolean_config_t what) const; float getExperienceStage(uint32_t level) const; + float getLoyaltyBonus(uint16_t points) const; bool setString(string_config_t what, std::string_view value); bool setNumber(integer_config_t what, int32_t value); @@ -146,6 +148,7 @@ class ConfigManager bool boolean[LAST_BOOLEAN_CONFIG] = {}; ExperienceStages expStages = {}; + LoyaltyBonuses loyaltyBonuses = {}; bool loaded = false; }; diff --git a/src/iologindata.cpp b/src/iologindata.cpp index 2f826e8434..3183569d43 100644 --- a/src/iologindata.cpp +++ b/src/iologindata.cpp @@ -20,7 +20,8 @@ Account IOLoginData::loadAccount(uint32_t accno) Account account; DBResult_ptr result = Database::getInstance().storeQuery(fmt::format( - "SELECT `id`, `name`, `password`, `type`, `premium_ends_at` FROM `accounts` WHERE `id` = {:d}", accno)); + "SELECT `id`, `name`, `password`, `type`, `premium_ends_at`, `loyalty_points` FROM `accounts` WHERE `id` = {:d}", + accno)); if (!result) { return account; } @@ -29,9 +30,16 @@ Account IOLoginData::loadAccount(uint32_t accno) account.name = result->getString("name"); account.accountType = static_cast(result->getNumber("type")); account.premiumEndsAt = result->getNumber("premium_ends_at"); + account.loyaltyPoints = result->getNumber("loyalty_points"); return account; } +void IOLoginData::updateLoyalty(uint32_t accountId, uint16_t points) +{ + Database::getInstance().executeQuery( + fmt::format("UPDATE `accounts` SET `loyalty_points` = {:d} WHERE `id` = {:d}", points, accountId)); +} + std::string decodeSecret(std::string_view secret) { // simple base32 decoding @@ -270,6 +278,9 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) player->premiumEndsAt = acc.premiumEndsAt; + player->loyaltyPoints = acc.loyaltyPoints; + player->loyaltyBonus = g_config.getLoyaltyBonus(acc.loyaltyPoints); + Group* group = g_game.groups.getGroup(result->getNumber("group_id")); if (!group) { std::cout << "[Error - IOLoginData::loadPlayer] " << player->name << " has Group ID " @@ -336,6 +347,8 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) player->manaSpent = manaSpent; player->magLevelPercent = Player::getBasisPointLevel(player->manaSpent, nextManaCount); + player->accumulatedManaSpent = player->vocation->getAccumulatedReqMana(player->magLevel) + manaSpent; + player->setLoyaltyBonusMagicLevel(); player->health = result->getNumber("health"); player->healthMax = result->getNumber("healthmax"); @@ -413,7 +426,9 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) player->skills[i].level = skillLevel; player->skills[i].tries = skillTries; + player->skills[i].accumulatedTries = player->vocation->getAccumulatedReqSkillTries(i, skillLevel) + skillTries; player->skills[i].percent = Player::getBasisPointLevel(skillTries, nextSkillTries); + player->setLoyaltyBonusSkill(i); } if ((result = db.storeQuery( @@ -999,6 +1014,8 @@ bool IOLoginData::savePlayer(Player* player) return false; } + updateLoyalty(player->accountNumber, player->loyaltyPoints); + // End the transaction return transaction.commit(); } diff --git a/src/iologindata.h b/src/iologindata.h index 1997d2bb85..c76285d908 100644 --- a/src/iologindata.h +++ b/src/iologindata.h @@ -27,6 +27,8 @@ class IOLoginData static uint32_t getAccountIdByPlayerName(const std::string& playerName); static uint32_t getAccountIdByPlayerId(uint32_t playerId); + static void updateLoyalty(uint32_t accountId, uint16_t points); + static AccountType_t getAccountType(uint32_t accountId); static void setAccountType(uint32_t accountId, AccountType_t accountType); static void updateOnlineStatus(uint32_t guid, bool login); diff --git a/src/luascript.cpp b/src/luascript.cpp index e4c0fb35da..6e3349558a 100644 --- a/src/luascript.cpp +++ b/src/luascript.cpp @@ -2816,6 +2816,14 @@ void LuaScriptInterface::registerFunctions() registerMethod("Player", "setClientLowLevelBonusDisplay", LuaScriptInterface::luaPlayerSetClientLowLevelBonusDisplay); + registerMethod("Player", "getLoyaltyMagicLevel", LuaScriptInterface::luaPlayerGetLoyaltyMagicLevel); + registerMethod("Player", "getLoyaltyMagicLevelPercent", LuaScriptInterface::luaPlayerGetLoyaltyMagicLevelPercent); + registerMethod("Player", "getLoyaltySkillLevel", LuaScriptInterface::luaPlayerGetLoyaltySkillLevel); + registerMethod("Player", "getLoyaltySkillPercent", LuaScriptInterface::luaPlayerGetLoyaltySkillPercent); + registerMethod("Player", "getLoyaltyPoints", LuaScriptInterface::luaPlayerGetLoyaltyPoints); + registerMethod("Player", "setLoyaltyPoints", LuaScriptInterface::luaPlayerSetLoyaltyPoints); + registerMethod("Player", "getLoyaltyBonus", LuaScriptInterface::luaPlayerGetLoyaltyBonus); + // Monster registerClass("Monster", "Creature", LuaScriptInterface::luaMonsterCreate); registerMetaMethod("Monster", "__eq", LuaScriptInterface::luaUserdataCompare); @@ -11205,6 +11213,93 @@ int LuaScriptInterface::luaPlayerSetClientLowLevelBonusDisplay(lua_State* L) return 1; } +int LuaScriptInterface::luaPlayerGetLoyaltyMagicLevel(lua_State* L) +{ + // player:getLoyaltyMagicLevel() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->loyaltyMagLevel); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetLoyaltyMagicLevelPercent(lua_State* L) +{ + // player:getLoyaltyMagicLevelPercent() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->loyaltyMagLevelPercent); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetLoyaltySkillLevel(lua_State* L) +{ + // player:getLoyaltySkillLevel(skillType) + skills_t skillType = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player && skillType <= SKILL_LAST) { + lua_pushnumber(L, player->loyaltySkills[skillType].level); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetLoyaltySkillPercent(lua_State* L) +{ + // player:getLoyaltySkillPercent(skillType) + skills_t skillType = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player && skillType <= SKILL_LAST) { + lua_pushnumber(L, player->loyaltySkills[skillType].percent); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetLoyaltyPoints(lua_State* L) +{ + // player:getLoyaltyPoints() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getLoyaltyPoints()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetLoyaltyPoints(lua_State* L) +{ + // player:setLoyaltyPoints(points) + Player* player = getUserdata(L, 1); + if (player) { + player->setLoyaltyPoints(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetLoyaltyBonus(lua_State* L) +{ + // player:getLoyaltyBonus() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getLoyaltyBonus()); + } else { + lua_pushnil(L); + } + return 1; +} + // Monster int LuaScriptInterface::luaMonsterCreate(lua_State* L) { diff --git a/src/luascript.h b/src/luascript.h index bac407d70e..1b71b0663d 100644 --- a/src/luascript.h +++ b/src/luascript.h @@ -1048,6 +1048,14 @@ class LuaScriptInterface static int luaPlayerGetClientLowLevelBonusDisplay(lua_State* L); static int luaPlayerSetClientLowLevelBonusDisplay(lua_State* L); + static int luaPlayerGetLoyaltyMagicLevel(lua_State* L); + static int luaPlayerGetLoyaltyMagicLevelPercent(lua_State* L); + static int luaPlayerGetLoyaltySkillLevel(lua_State* L); + static int luaPlayerGetLoyaltySkillPercent(lua_State* L); + static int luaPlayerGetLoyaltyPoints(lua_State* L); + static int luaPlayerSetLoyaltyPoints(lua_State* L); + static int luaPlayerGetLoyaltyBonus(lua_State* L); + // Monster static int luaMonsterCreate(lua_State* L); diff --git a/src/player.cpp b/src/player.cpp index 0ec300366f..deb46130d7 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -490,78 +490,43 @@ void Player::addSkillAdvance(skills_t skill, uint64_t count) return; } - bool sendUpdateSkills = false; - while ((skills[skill].tries + count) >= nextReqTries) { - count -= nextReqTries - skills[skill].tries; - skills[skill].level++; - skills[skill].tries = 0; - skills[skill].percent = 0; - - sendTextMessage(MESSAGE_EVENT_ADVANCE, - fmt::format("You advanced to {:s} level {:d}.", getSkillName(skill), skills[skill].level)); - - g_creatureEvents->playerAdvance(this, skill, (skills[skill].level - 1), skills[skill].level); + uint16_t oldLevel = loyaltySkills[skill].level; + uint16_t oldPercent = loyaltySkills[skill].percent; - sendUpdateSkills = true; - currReqTries = nextReqTries; - nextReqTries = vocation->getReqSkillTries(skill, skills[skill].level + 1); - if (currReqTries >= nextReqTries) { - count = 0; - break; - } - } - - skills[skill].tries += count; - - uint32_t newPercent; - if (nextReqTries > currReqTries) { - newPercent = Player::getBasisPointLevel(skills[skill].tries, nextReqTries); - } else { - newPercent = 0; - } + skills[skill] = vocation->getSkillByAccumulatedTries(skill, skills[skill].accumulatedTries + count); + setLoyaltyBonusSkill(skill); - if (skills[skill].percent != newPercent) { - skills[skill].percent = newPercent; + bool sendUpdateSkills = false; + if (oldLevel != loyaltySkills[skill].level) { + sendTextMessage(MESSAGE_EVENT_ADVANCE, fmt::format("You advanced to {:s} level {:d}.", getSkillName(skill), + loyaltySkills[skill].level)); + g_creatureEvents->playerAdvance(this, skill, oldLevel, loyaltySkills[skill].level); sendUpdateSkills = true; } - if (sendUpdateSkills) { + if (sendUpdateSkills || oldPercent != loyaltySkills[skill].percent) { sendSkills(); } } void Player::removeSkillTries(skills_t skill, uint64_t count, bool notify /* = false*/) { - uint16_t oldLevel = skills[skill].level; - uint8_t oldPercent = skills[skill].percent; - - while (count > skills[skill].tries) { - count -= skills[skill].tries; + uint16_t oldLevel = loyaltySkills[skill].level; + uint16_t oldPercent = loyaltySkills[skill].percent; - if (skills[skill].level <= MINIMUM_SKILL_LEVEL) { - skills[skill].level = MINIMUM_SKILL_LEVEL; - skills[skill].tries = 0; - count = 0; - break; - } - - skills[skill].tries = vocation->getReqSkillTries(skill, skills[skill].level); - skills[skill].level--; - } - - skills[skill].tries = std::max(0, skills[skill].tries - count); - skills[skill].percent = - Player::getBasisPointLevel(skills[skill].tries, vocation->getReqSkillTries(skill, skills[skill].level)); + skills[skill] = + vocation->getSkillByAccumulatedTries(skill, std::max(count, skills[skill].accumulatedTries) - count); + setLoyaltyBonusSkill(skill); if (notify) { bool sendUpdateSkills = false; - if (oldLevel != skills[skill].level) { + if (oldLevel != loyaltySkills[skill].level) { sendTextMessage(MESSAGE_EVENT_ADVANCE, fmt::format("You were downgraded to {:s} level {:d}.", - getSkillName(skill), skills[skill].level)); + getSkillName(skill), loyaltySkills[skill].level)); sendUpdateSkills = true; } - if (sendUpdateSkills || oldPercent != skills[skill].percent) { + if (sendUpdateSkills || oldPercent != loyaltySkills[skill].percent) { sendSkills(); } } @@ -1635,44 +1600,26 @@ void Player::addManaSpent(uint64_t amount) return; } + uint32_t oldLevel = loyaltyMagLevel; + uint16_t oldPercent = loyaltyMagLevelPercent; + g_events->eventPlayerOnGainSkillTries(this, SKILL_MAGLEVEL, amount); if (amount == 0) { return; } - bool sendUpdateStats = false; - while ((manaSpent + amount) >= nextReqMana) { - amount -= nextReqMana - manaSpent; - - magLevel++; - manaSpent = 0; - - sendTextMessage(MESSAGE_EVENT_ADVANCE, fmt::format("You advanced to magic level {:d}.", magLevel)); - - g_creatureEvents->playerAdvance(this, SKILL_MAGLEVEL, magLevel - 1, magLevel); - - sendUpdateStats = true; - currReqMana = nextReqMana; - nextReqMana = vocation->getReqMana(magLevel + 1); - if (currReqMana >= nextReqMana) { - return; - } - } - - manaSpent += amount; + std::tie(magLevel, manaSpent) = vocation->getMagLevelByAccumulatedMana(manaSpent + amount); + accumulatedManaSpent = vocation->getAccumulatedReqMana(magLevel) + manaSpent; + setLoyaltyBonusMagicLevel(); - uint8_t oldPercent = magLevelPercent; - if (nextReqMana > currReqMana) { - magLevelPercent = Player::getBasisPointLevel(manaSpent, nextReqMana); - } else { - magLevelPercent = 0; - } - - if (oldPercent != magLevelPercent) { + bool sendUpdateStats = false; + if (oldLevel != loyaltyMagLevel) { + sendTextMessage(MESSAGE_EVENT_ADVANCE, fmt::format("You advanced to magic level {:d}.", loyaltyMagLevel)); + g_creatureEvents->playerAdvance(this, SKILL_MAGLEVEL, oldLevel, loyaltyMagLevel); sendUpdateStats = true; } - if (sendUpdateStats) { + if (sendUpdateStats || oldPercent != loyaltyMagLevelPercent) { sendStats(); sendSkills(); } @@ -1684,32 +1631,24 @@ void Player::removeManaSpent(uint64_t amount, bool notify /* = false*/) return; } - uint32_t oldLevel = magLevel; - uint8_t oldPercent = magLevelPercent; - - while (amount > manaSpent && magLevel > 0) { - amount -= manaSpent; - manaSpent = vocation->getReqMana(magLevel); - magLevel--; - } - - manaSpent -= amount; + uint32_t oldLevel = loyaltyMagLevel; + uint16_t oldPercent = loyaltyMagLevelPercent; - uint64_t nextReqMana = vocation->getReqMana(magLevel + 1); - if (nextReqMana > vocation->getReqMana(magLevel)) { - magLevelPercent = Player::getBasisPointLevel(manaSpent, nextReqMana); - } else { - magLevelPercent = 0; - } + std::tie(magLevel, manaSpent) = + vocation->getMagLevelByAccumulatedMana(std::max(amount, manaSpent) - amount); + accumulatedManaSpent = vocation->getAccumulatedReqMana(magLevel) + manaSpent; + setLoyaltyBonusMagicLevel(); if (notify) { bool sendUpdateStats = false; - if (oldLevel != magLevel) { - sendTextMessage(MESSAGE_EVENT_ADVANCE, fmt::format("You were downgraded to magic level {:d}.", magLevel)); + if (oldLevel != loyaltyMagLevel) { + sendTextMessage(MESSAGE_EVENT_ADVANCE, + fmt::format("You were downgraded to magic level {:d}.", loyaltyMagLevel)); + g_creatureEvents->playerAdvance(this, SKILL_MAGLEVEL, oldLevel, loyaltyMagLevel); sendUpdateStats = true; } - if (sendUpdateStats || oldPercent != magLevelPercent) { + if (sendUpdateStats || oldPercent != loyaltyMagLevelPercent) { sendStats(); sendSkills(); } @@ -4693,3 +4632,21 @@ void Player::updateRegeneration() condition->setParam(CONDITION_PARAM_MANATICKS, vocation->getManaGainTicks() * 1000); } } + +void Player::setLoyaltyBonusSkill(uint8_t skill) +{ + if (skill < SKILL_FIRST || skill > SKILL_LAST) { + return; + } + loyaltySkills[skill] = vocation->getSkillByAccumulatedTries( + skill, std::ceil(skills[skill].accumulatedTries * (1 + loyaltyBonus))); + loyaltySkills[skill].percent = getBasisPointLevel( + loyaltySkills[skill].tries, vocation->getReqSkillTries(skill, loyaltySkills[skill].level + 1)); +} + +void Player::setLoyaltyBonusMagicLevel() +{ + std::tie(loyaltyMagLevel, loyaltyManaSpent) = + vocation->getMagLevelByAccumulatedMana(std::ceil(accumulatedManaSpent * (1 + loyaltyBonus))); + loyaltyMagLevelPercent = getBasisPointLevel(loyaltyManaSpent, vocation->getReqMana(loyaltyMagLevel + 1)); +} diff --git a/src/player.h b/src/player.h index 9ed554c87e..65a5d193cc 100644 --- a/src/player.h +++ b/src/player.h @@ -76,6 +76,7 @@ static constexpr int16_t MINIMUM_SKILL_LEVEL = 10; struct Skill { uint64_t tries = 0; + uint64_t accumulatedTries = 0; uint16_t level = MINIMUM_SKILL_LEVEL; uint16_t percent = 0; }; @@ -267,7 +268,14 @@ class Player final : public Creature, public Cylinder AccountType_t getAccountType() const { return accountType; } uint32_t getLevel() const { return level; } uint8_t getLevelPercent() const { return levelPercent; } - uint32_t getMagicLevel() const { return std::max(0, magLevel + varStats[STAT_MAGICPOINTS]); } + uint32_t getMagicLevel() const + { + if (loyaltyBonus > 0) { + return std::max(0, loyaltyMagLevel + varStats[STAT_MAGICPOINTS]); + } + + return std::max(0, magLevel + varStats[STAT_MAGICPOINTS]); + } uint32_t getSpecialMagicLevel(CombatType_t type) const { return std::max(0, specialMagicLevelSkill[combatTypeToIndex(type)]); @@ -447,6 +455,9 @@ class Player final : public Creature, public Cylinder uint16_t getSpecialSkill(uint8_t skill) const { return std::max(0, varSpecialSkills[skill]); } uint16_t getSkillLevel(uint8_t skill) const { + if (loyaltyBonus > 0) { + return std::max(0, loyaltySkills[skill].level + varSkills[skill]); + } return std::max(0, skills[skill].level + varSkills[skill]); } uint16_t getSpecialMagicLevelSkill(CombatType_t type) const @@ -456,6 +467,18 @@ class Player final : public Creature, public Cylinder uint16_t getBaseSkill(uint8_t skill) const { return skills[skill].level; } uint16_t getSkillPercent(uint8_t skill) const { return skills[skill].percent; } + void setLoyaltyBonusSkill(uint8_t skill); + uint16_t getLoyaltySkill(uint8_t skill) const { return std::max(0, loyaltySkills[skill].level); } + uint16_t getLoyaltySkillPercent(uint8_t skill) const { return loyaltySkills[skill].percent; } + + void setLoyaltyBonusMagicLevel(); + uint32_t getLoyaltyMagicLevel() const { return std::max(0, loyaltyMagLevel); } + uint16_t getLoyaltyMagicLevelPercent() const { return loyaltyMagLevelPercent; } + + void setLoyaltyPoints(uint16_t points) { loyaltyPoints = points; } + uint16_t getLoyaltyPoints() const { return loyaltyPoints; } + float getLoyaltyBonus() const { return loyaltyBonus; } + bool getAddAttackSkill() const { return addAttackSkillPoint; } BlockType_t getLastAttackBlockType() const { return lastAttackBlockType; } @@ -1174,6 +1197,7 @@ class Player final : public Creature, public Cylinder std::string guildNick; Skill skills[SKILL_LAST + 1]; + Skill loyaltySkills[SKILL_LAST + 1]; LightInfo itemsLight; Position loginPosition; Position lastWalkthroughPosition; @@ -1184,6 +1208,8 @@ class Player final : public Creature, public Cylinder uint64_t experience = 0; uint64_t manaSpent = 0; + uint64_t accumulatedManaSpent = 0; + uint64_t loyaltyManaSpent = 0; uint64_t lastAttack = 0; uint64_t bankBalance = 0; int64_t lastFailedFollow = 0; @@ -1221,6 +1247,7 @@ class Player final : public Creature, public Cylinder uint32_t conditionSuppressions = 0; uint32_t level = 1; uint32_t magLevel = 0; + uint32_t loyaltyMagLevel = 0; uint32_t actionTaskEvent = 0; uint32_t walkTaskEvent = 0; uint32_t MessageBufferTicks = 0; @@ -1256,6 +1283,7 @@ class Player final : public Creature, public Cylinder std::bitset<6> blessings; uint8_t levelPercent = 0; uint16_t magLevelPercent = 0; + uint16_t loyaltyMagLevelPercent = 0; PlayerSex_t sex = PLAYERSEX_FEMALE; OperatingSystem_t operatingSystem = CLIENTOS_NONE; @@ -1275,6 +1303,9 @@ class Player final : public Creature, public Cylinder bool inventoryAbilities[CONST_SLOT_LAST + 1] = {}; bool randomizeMount = false; + uint16_t loyaltyPoints = 0; + float loyaltyBonus = 0.f; + static uint32_t playerAutoID; static uint32_t playerIDLimit; diff --git a/src/protocolgame.cpp b/src/protocolgame.cpp index b1bc5d5c3a..58ea84bb99 100644 --- a/src/protocolgame.cpp +++ b/src/protocolgame.cpp @@ -3608,14 +3608,14 @@ void ProtocolGame::AddPlayerSkills(NetworkMessage& msg) msg.addByte(0xA1); msg.add(player->getMagicLevel()); msg.add(player->getBaseMagicLevel()); - msg.add(player->getBaseMagicLevel()); // base + loyalty bonus(?) - msg.add(player->getMagicLevelPercent()); + msg.add(player->getLoyaltyMagicLevel()); // base + loyalty bonus(?) + msg.add(player->getLoyaltyMagicLevelPercent()); for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { msg.add(std::min(player->getSkillLevel(i), std::numeric_limits::max())); msg.add(player->getBaseSkill(i)); - msg.add(player->getBaseSkill(i)); // base + loyalty bonus(?) - msg.add(player->getSkillPercent(i)); + msg.add(player->getLoyaltySkill(i)); // base + loyalty bonus(?) + msg.add(player->getLoyaltySkillPercent(i)); } for (uint8_t i = SPECIALSKILL_FIRST; i <= SPECIALSKILL_LAST; ++i) { diff --git a/src/vocation.cpp b/src/vocation.cpp index bafa921084..6d60850187 100644 --- a/src/vocation.cpp +++ b/src/vocation.cpp @@ -140,7 +140,7 @@ static const uint32_t skillBase[SKILL_LAST + 1] = {50, 50, 50, 50, 30, 100, 20}; uint64_t Vocation::getReqSkillTries(uint8_t skill, uint16_t level) { - if (skill > SKILL_LAST) { + if (skill < SKILL_FIRST || skill > SKILL_LAST) { return 0; } return skillBase[skill] * @@ -154,3 +154,44 @@ uint64_t Vocation::getReqMana(uint32_t magLevel) } return 1600 * std::pow(manaMultiplier, static_cast(magLevel - 1)); } + +uint64_t Vocation::getAccumulatedReqSkillTries(uint8_t skill, uint16_t level) +{ + if (skill < SKILL_FIRST || skill > SKILL_LAST) { + return 0; + } + return std::ceil(skillBase[skill] * + ((std::pow(skillMultipliers[skill], static_cast(level - MINIMUM_SKILL_LEVEL)) - 1) / + (skillMultipliers[skill] - 1))); +} + +Skill Vocation::getSkillByAccumulatedTries(uint8_t skill, uint64_t tries) +{ + if (skill < SKILL_FIRST || skill > SKILL_LAST) { + return {}; + } + uint16_t level = + std::log(tries * ((skillMultipliers[skill] - 1) / skillBase[skill]) + 1) / std::log(skillMultipliers[skill]) + + MINIMUM_SKILL_LEVEL; + uint64_t remainderTries = tries - getAccumulatedReqSkillTries(skill, level); + + return {remainderTries, tries, level, 0}; +} + +uint64_t Vocation::getAccumulatedReqMana(uint32_t magLevel) +{ + if (magLevel == 0) { + return 0; + } + return std::ceil(1600 * ((std::pow(manaMultiplier, magLevel) - 1) / (manaMultiplier - 1))); +} + +std::pair Vocation::getMagLevelByAccumulatedMana(uint64_t mana) +{ + if (mana == 0) { + return std::make_pair(0, 0); + } + uint32_t level = std::log(mana * ((manaMultiplier - 1) / 1600) + 1) / std::log(manaMultiplier); + mana -= getAccumulatedReqMana(level); + return std::make_pair(level, mana); +} diff --git a/src/vocation.h b/src/vocation.h index 5cf1db224d..cc34e4cb1c 100644 --- a/src/vocation.h +++ b/src/vocation.h @@ -9,6 +9,8 @@ extern ConfigManager g_config; +struct Skill; + class Vocation { public: @@ -16,8 +18,13 @@ class Vocation const std::string& getVocName() const { return name; } const std::string& getVocDescription() const { return description; } + uint64_t getReqSkillTries(uint8_t skill, uint16_t level); uint64_t getReqMana(uint32_t magLevel); + uint64_t getAccumulatedReqSkillTries(uint8_t skill, uint16_t level); + Skill getSkillByAccumulatedTries(uint8_t skill, uint64_t tries); + uint64_t getAccumulatedReqMana(uint32_t magLevel); + std::pair getMagLevelByAccumulatedMana(uint64_t mana); uint16_t getId() const { return id; }