From 529c7a43859b58256bb51bf55dbe0efe9a6dbf7e Mon Sep 17 00:00:00 2001 From: Elson Costa Date: Sat, 11 May 2024 16:46:41 -0300 Subject: [PATCH] feat: title system (#2576) --- data/scripts/talkactions/god/add_skill.lua | 3 +- data/scripts/talkactions/god/charms.lua | 8 +- .../talkactions/god/create_monster.lua | 6 +- data/scripts/talkactions/god/manage_badge.lua | 8 +- .../talkactions/god/manage_storage.lua | 3 +- data/scripts/talkactions/god/manage_title.lua | 70 ++++ .../god/{vip_manager.lua => manage_vip.lua} | 0 src/creatures/CMakeLists.txt | 1 + .../players/cyclopedia/player_badge.hpp | 2 +- .../players/cyclopedia/player_title.cpp | 324 ++++++++++++++++++ .../players/cyclopedia/player_title.hpp | 104 ++++++ src/creatures/players/player.cpp | 16 +- src/creatures/players/player.hpp | 10 + src/enums/player_cyclopedia.hpp | 38 ++ src/game/game.cpp | 219 +++++++++--- src/game/game.hpp | 7 + src/game/game_definitions.hpp | 10 +- src/io/functions/iologindata_load_player.cpp | 1 + .../functions/core/game/game_functions.cpp | 1 + .../creatures/player/player_functions.cpp | 55 +++ .../creatures/player/player_functions.hpp | 9 + src/server/network/protocol/protocolgame.cpp | 46 ++- src/server/network/protocol/protocolgame.hpp | 2 + src/server/server_definitions.hpp | 6 +- vcproj/canary.vcxproj | 3 + 25 files changed, 865 insertions(+), 87 deletions(-) create mode 100644 data/scripts/talkactions/god/manage_title.lua rename data/scripts/talkactions/god/{vip_manager.lua => manage_vip.lua} (100%) create mode 100644 src/creatures/players/cyclopedia/player_title.cpp create mode 100644 src/creatures/players/cyclopedia/player_title.hpp create mode 100644 src/enums/player_cyclopedia.hpp diff --git a/data/scripts/talkactions/god/add_skill.lua b/data/scripts/talkactions/god/add_skill.lua index f1c6244fba6..20644582325 100644 --- a/data/scripts/talkactions/god/add_skill.lua +++ b/data/scripts/talkactions/god/add_skill.lua @@ -44,8 +44,7 @@ function addSkill.onSay(player, words, param) return true end - -- Trim left - split[2] = split[2]:gsub("^%s*(.-)$", "%1") + split[2] = split[2]:trimSpace() local count = 1 if split[3] then diff --git a/data/scripts/talkactions/god/charms.lua b/data/scripts/talkactions/god/charms.lua index 14dde0a58d7..f06c17ac7e7 100644 --- a/data/scripts/talkactions/god/charms.lua +++ b/data/scripts/talkactions/god/charms.lua @@ -19,8 +19,8 @@ function addCharm.onSay(player, words, param) player:sendCancelMessage("A player with that name is not online.") return true end - --trim left - split[2] = split[2]:gsub("^%s*(.-)$", "%1") + + split[2] = split[2]:trimSpace() player:sendCancelMessage("Added " .. split[2] .. " charm points to character '" .. target:getName() .. "'.") target:sendCancelMessage("Received " .. split[2] .. " charm points!") @@ -133,8 +133,8 @@ function setBestiary.onSay(player, words, param) return true end - split[2] = split[2]:gsub("^%s*(.-)$", "%1") --Trim left - split[3] = split[3]:gsub("^%s*(.-)$", "%1") --Trim left + split[2] = split[2]:trimSpace() + split[3] = split[3]:trimSpace() local monsterName = split[2] local mType = MonsterType(monsterName) diff --git a/data/scripts/talkactions/god/create_monster.lua b/data/scripts/talkactions/god/create_monster.lua index 924a0d0906d..ce5869a2eee 100644 --- a/data/scripts/talkactions/god/create_monster.lua +++ b/data/scripts/talkactions/god/create_monster.lua @@ -73,20 +73,20 @@ function createMonster.onSay(player, words, param) local monsterName = split[1] local monsterCount = 0 if split[2] then - split[2] = split[2]:gsub("^%s*(.-)$", "%1") --Trim left + split[2] = split[2]:trimSpace() monsterCount = tonumber(split[2]) end local monsterForge = nil if split[3] then - split[3] = split[3]:gsub("^%s*(.-)$", "%1") --Trim left + split[3] = split[3]:trimSpace() monsterForge = split[3] end if monsterCount > 1 then local spawnRadius = 5 if split[4] then - split[4] = split[4]:gsub("^%s*(.-)$", "%1") --Trim left + split[4] = split[4]:trimSpace() spawnRadius = split[4] print(spawnRadius) end diff --git a/data/scripts/talkactions/god/manage_badge.lua b/data/scripts/talkactions/god/manage_badge.lua index f2899a0d592..310cce247b0 100644 --- a/data/scripts/talkactions/god/manage_badge.lua +++ b/data/scripts/talkactions/god/manage_badge.lua @@ -21,10 +21,12 @@ function addBadge.onSay(player, words, param) return true end - -- Trim left - split[2] = split[2]:gsub("^%s*(.-)$", "%1") + split[2] = split[2]:trimSpace() local id = tonumber(split[2]) - target:addBadge(id) + if target:addBadge(id) then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format('You added a badge with ID "%i" to player "%s".', id, target:getName())) + target:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format("%s added a badge to you.", player:getName())) + end return true end diff --git a/data/scripts/talkactions/god/manage_storage.lua b/data/scripts/talkactions/god/manage_storage.lua index 929ef7b4417..e1973ebde00 100644 --- a/data/scripts/talkactions/god/manage_storage.lua +++ b/data/scripts/talkactions/god/manage_storage.lua @@ -18,8 +18,7 @@ function Player.getStorageValueTalkaction(self, param) return true end - -- Trim left - split[2] = split[2]:gsub("^%s*(.-)$", "%1") + split[2] = split[2]:trimSpace() -- Try to convert the second parameter to a number. If it's not a number, treat it as a storage name local storageKey = tonumber(split[2]) diff --git a/data/scripts/talkactions/god/manage_title.lua b/data/scripts/talkactions/god/manage_title.lua new file mode 100644 index 00000000000..d99adbe5dc0 --- /dev/null +++ b/data/scripts/talkactions/god/manage_title.lua @@ -0,0 +1,70 @@ +local addTitle = TalkAction("/addtitle") + +function addTitle.onSay(player, words, param) + -- create log + logCommand(player, words, param) + + if param == "" then + player:sendCancelMessage("Command param required.") + return true + end + + local split = param:split(",") + if not split[2] then + player:sendCancelMessage("Insufficient parameters. Usage: /addtitle playerName, badgeID") + return true + end + + local target = Player(split[1]) + if not target then + player:sendCancelMessage("A player with that name is not online.") + return true + end + + split[2] = split[2]:trimSpace() + local id = tonumber(split[2]) + if target:addTitle(id) then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format('You added a title with ID "%i" to player "%s".', id, target:getName())) + target:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format("%s added a title to you.", player:getName())) + end + + return true +end + +addTitle:separator(" ") +addTitle:groupType("god") +addTitle:register() + +----------------------------------------- +local setTitle = TalkAction("/settitle") + +function setTitle.onSay(player, words, param) + -- create log + logCommand(player, words, param) + + if param == "" then + player:sendCancelMessage("Command param required.") + return true + end + + local split = param:split(",") + if not split[2] then + player:sendCancelMessage("Insufficient parameters. Usage: /settitle playerName, badgeID") + return true + end + + local target = Player(split[1]) + if not target then + player:sendCancelMessage("A player with that name is not online.") + return true + end + + split[2] = split[2]:trimSpace() + local id = tonumber(split[2]) + target:setCurrentTitle(id) + return true +end + +setTitle:separator(" ") +setTitle:groupType("god") +setTitle:register() diff --git a/data/scripts/talkactions/god/vip_manager.lua b/data/scripts/talkactions/god/manage_vip.lua similarity index 100% rename from data/scripts/talkactions/god/vip_manager.lua rename to data/scripts/talkactions/god/manage_vip.lua diff --git a/src/creatures/CMakeLists.txt b/src/creatures/CMakeLists.txt index 630812c7996..6715281439f 100644 --- a/src/creatures/CMakeLists.txt +++ b/src/creatures/CMakeLists.txt @@ -23,6 +23,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE players/player.cpp players/achievement/player_achievement.cpp players/cyclopedia/player_badge.cpp + players/cyclopedia/player_title.cpp players/wheel/player_wheel.cpp players/wheel/wheel_gems.cpp players/vocations/vocation.cpp diff --git a/src/creatures/players/cyclopedia/player_badge.hpp b/src/creatures/players/cyclopedia/player_badge.hpp index 01c9dc0e63f..7bf28c0c302 100644 --- a/src/creatures/players/cyclopedia/player_badge.hpp +++ b/src/creatures/players/cyclopedia/player_badge.hpp @@ -9,7 +9,7 @@ #pragma once -#include "game/game_definitions.hpp" +#include "enums/player_cyclopedia.hpp" class Player; class KV; diff --git a/src/creatures/players/cyclopedia/player_title.cpp b/src/creatures/players/cyclopedia/player_title.cpp new file mode 100644 index 00000000000..9e1c763a4ad --- /dev/null +++ b/src/creatures/players/cyclopedia/player_title.cpp @@ -0,0 +1,324 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#include "pch.hpp" + +#include "player_title.hpp" + +#include "creatures/players/player.hpp" +#include "game/game.hpp" +#include "kv/kv.hpp" + +PlayerTitle::PlayerTitle(Player &player) : + m_player(player) { } + +bool PlayerTitle::isTitleUnlocked(uint8_t id) const { + if (id == 0) { + return false; + } + + if (auto it = std::find_if(m_titlesUnlocked.begin(), m_titlesUnlocked.end(), [id](auto title_it) { + return title_it.first.m_id == id; + }); + it != m_titlesUnlocked.end()) { + return true; + } + + return false; +} + +bool PlayerTitle::manage(bool canAdd, uint8_t id, uint32_t timestamp /* = 0*/) { + const Title &title = g_game().getTitleById(id); + if (title.m_id == 0) { + return false; + } + + if (!canAdd) { + if (!title.m_permanent) { + remove(title); + } + return false; + } + + if (isTitleUnlocked(id)) { + return false; + } + + int toSaveTimeStamp = timestamp != 0 ? timestamp : (OTSYS_TIME() / 1000); + getUnlockedKV()->set(title.m_maleName, toSaveTimeStamp); + m_titlesUnlocked.emplace_back(title, toSaveTimeStamp); + m_titlesUnlocked.shrink_to_fit(); + g_logger().debug("[{}] - Added title: {}", __FUNCTION__, title.m_maleName); + + return true; +} + +void PlayerTitle::remove(const Title &title) { + auto id = title.m_id; + if (!isTitleUnlocked(id)) { + return; + } + + auto it = std::find_if(m_titlesUnlocked.begin(), m_titlesUnlocked.end(), [id](auto title_it) { + return title_it.first.m_id == id; + }); + + if (it == m_titlesUnlocked.end()) { + return; + } + + getUnlockedKV()->remove(title.m_maleName); + m_titlesUnlocked.erase(it); + m_titlesUnlocked.shrink_to_fit(); + g_logger().debug("[{}] - Removed title: {}", __FUNCTION__, title.m_maleName); +} + +const std::vector> &PlayerTitle::getUnlockedTitles() { + return m_titlesUnlocked; +} + +uint8_t PlayerTitle::getCurrentTitle() const { + return static_cast(m_player.kv()->scoped("titles")->get("current-title")->getNumber()); +} + +void PlayerTitle::setCurrentTitle(uint8_t id) { + m_player.kv()->scoped("titles")->set("current-title", id != 0 && isTitleUnlocked(id) ? id : 0); +} + +std::string PlayerTitle::getCurrentTitleName() { + auto currentTitle = getCurrentTitle(); + if (currentTitle == 0) { + return ""; + } + + auto title = g_game().getTitleById(currentTitle); + if (title.m_id == 0) { + return ""; + } + + return getNameBySex(m_player.getSex(), title.m_maleName, title.m_femaleName); +} + +const std::string &PlayerTitle::getNameBySex(PlayerSex_t sex, const std::string &male, const std::string &female) { + return sex == PLAYERSEX_FEMALE && !female.empty() ? female : male; +} + +void PlayerTitle::checkAndUpdateNewTitles() { + Benchmark bm_checkTitles; + for (const auto &title : g_game().getTitles()) { + switch (title.m_type) { + case CyclopediaTitle_t::NOTHING: + break; + case CyclopediaTitle_t::GOLD: + manage(checkGold(title.m_amount), title.m_id); + break; + case CyclopediaTitle_t::MOUNTS: + manage(checkMount(title.m_amount), title.m_id); + break; + case CyclopediaTitle_t::OUTFITS: + manage(checkOutfit(title.m_amount), title.m_id); + break; + case CyclopediaTitle_t::LEVEL: + manage(checkLevel(title.m_amount), title.m_id); + break; + case CyclopediaTitle_t::HIGHSCORES: + manage(checkHighscore(title.m_skill), title.m_id); + break; + case CyclopediaTitle_t::BESTIARY: + case CyclopediaTitle_t::BOSSTIARY: + manage(checkBestiary(title.m_maleName, title.m_race, title.m_type == CyclopediaTitle_t::BOSSTIARY, title.m_amount), title.m_id); + break; + case CyclopediaTitle_t::DAILY_REWARD: + manage(checkLoginStreak(title.m_amount), title.m_id); + break; + case CyclopediaTitle_t::TASK: + manage(checkTask(title.m_amount), title.m_id); + break; + case CyclopediaTitle_t::MAP: + // manage(checkMap(title.m_amount), title.m_id); + break; + case CyclopediaTitle_t::OTHERS: + manage(checkOther(title.m_maleName), title.m_id); + break; + } + } + + g_logger().debug("Checking and updating titles of player {} took {} milliseconds.", m_player.getName(), bm_checkTitles.duration()); + + loadUnlockedTitles(); +} + +void PlayerTitle::loadUnlockedTitles() { + const auto &unlockedTitles = getUnlockedKV()->keys(); + g_logger().debug("[{}] - Loading unlocked titles: {}", __FUNCTION__, unlockedTitles.size()); + for (const auto &titleName : unlockedTitles) { + const Title &title = g_game().getTitleByName(titleName); + if (title.m_id == 0) { + g_logger().error("[{}] - Title {} not found.", __FUNCTION__, titleName); + continue; + } + + m_titlesUnlocked.emplace_back(title, getUnlockedKV()->get(titleName)->getNumber()); + } +} + +const std::shared_ptr &PlayerTitle::getUnlockedKV() { + if (m_titleUnlockedKV == nullptr) { + m_titleUnlockedKV = m_player.kv()->scoped("titles")->scoped("unlocked"); + } + + return m_titleUnlockedKV; +} + +// Title Calculate Functions +bool PlayerTitle::checkGold(uint32_t amount) { + return m_player.getBankBalance() >= amount; +} + +bool PlayerTitle::checkMount(uint32_t amount) { + uint8_t total = 0; + for (const auto &mount : g_game().mounts.getMounts()) { + if (m_player.hasMount(mount)) { + total = total++; + } + } + return total >= amount; +} + +bool PlayerTitle::checkOutfit(uint32_t amount) { + return m_player.outfits.size() >= amount; +} + +bool PlayerTitle::checkLevel(uint32_t amount) { + return m_player.getLevel() >= amount; +} + +bool PlayerTitle::checkHighscore(uint8_t skill) { + Database &db = Database::getInstance(); + std::string query; + std::string fieldCheck = "id"; + + switch (static_cast(skill)) { + case HighscoreCategories_t::CHARMS: + query = fmt::format( + "SELECT `pc`.`player_guid`, `pc`.`charm_points`, `p`.`group_id` FROM `player_charms` pc JOIN `players` p ON `pc`.`player_guid` = `p`.`id` WHERE `p`.`group_id` < {} ORDER BY `pc`.`charm_points` DESC LIMIT 1", + static_cast(GROUP_TYPE_GAMEMASTER) + ); + fieldCheck = "player_guid"; + break; + case HighscoreCategories_t::DROME: + // todo check if player is in the top 5 for the previous rota of the Tibiadrome. + return false; + case HighscoreCategories_t::GOSHNAR: + // todo check if player is the most killer of Goshnar and his aspects. + return false; + default: + std::string skillName = g_game().getSkillNameById(skill); + query = fmt::format( + "SELECT * FROM `players` WHERE `group_id` < {} AND `{}` > 10 ORDER BY `{}` DESC LIMIT 1", + static_cast(GROUP_TYPE_GAMEMASTER), skillName, skillName + ); + break; + } + + DBResult_ptr result = db.storeQuery(query); + if (!result) { + return false; + } + + auto resultValue = result->getNumber(fieldCheck); + g_logger().debug("top id: {}, player id: {}", resultValue, m_player.getGUID()); + + return resultValue == m_player.getGUID(); +} + +bool PlayerTitle::checkBestiary(const std::string &name, uint16_t race, bool isBoss /* = false*/, uint32_t amount) { + if (race == 0) { + if (name == "Executioner") { + // todo check if player has unlocked all bestiary + } else if (name == "Boss Executioner") { + // todo check if player has unlocked all bosses + } + return false; + } + if (isBoss && amount > 0) { + // todo check if this way, is calculating by boss race + return m_player.getBestiaryKillCount(race) >= amount; + } + return m_player.isCreatureUnlockedOnTaskHunting(g_monsters().getMonsterTypeByRaceId(race, isBoss)); +} + +bool PlayerTitle::checkLoginStreak(uint32_t amount) { + auto streakKV = m_player.kv()->scoped("daily-reward")->get("streak"); + return streakKV && streakKV.has_value() && static_cast(streakKV->getNumber()) >= amount; +} + +bool PlayerTitle::checkTask(uint32_t amount) { + return m_player.getTaskHuntingPoints() >= amount; +} + +bool PlayerTitle::checkMap(uint32_t amount) { + // todo cyclopledia + return false; +} + +bool PlayerTitle::checkOther(const std::string &name) { + if (name == "Guild Leader") { + auto rank = m_player.getGuildRank(); + return rank && rank->level == 3; + } else if (name == "Proconsul of Iksupan") { + // Win Ancient Aucar Outfits complete so fight with Atab and be teleported to the arena. + } else if (name == "Admirer of the Crown") { + // Complete the Royal Costume Outfits. + return m_player.canWear(1457, 3) && m_player.canWear(1456, 3); + } else if (name == "Big Spender") { + // Unlocked the full Golden Outfit. + return m_player.canWear(1211, 3) && m_player.canWear(1210, 3); + } else if (name == "Challenger of the Iks") { + // Defeat Ahau while equipping a Broken Iks Headpiece, a Broken Iks Cuirass, some Broken Iks Faulds and Broken Iks Sandals + return m_player.getBestiaryKillCount(2346) >= 1; + } else if (name == "Royal Bounacean Advisor") { + // Complete the Galthen and the Lost Queen quest line + // Win Royal Bounacean Outfit + return m_player.canWear(1437, 3) && m_player.canWear(1436, 3); + } else if (name == "Aeternal") { + // Unlocked by 10-year-old characters. + } else if (name == "Robinson Crusoe") { + // Visit Schrödinger's Island. + } else if (name == "Chompmeister") { + // Complete all Jean Pierre's dishes in Hot Cuisine Quest. + } else if (name == "Bringer of Rain") { + // Clear wave 100 in the Tibiadrome. + } else if (name == "Beastly") { + // Reached 2000 charm points + return m_player.getCharmPoints() >= 2000; + } else if (name == "Midnight Hunter") { + // Kill a certain amount of Midnight Panthers. + // (The exact number is yet to be confirmed but is at least 21 and at most 28 panthers.) + return m_player.getBestiaryKillCount(698) >= 25; + } else if (name == "Ratinator") { + // Kill 10,000 Cave Rats. + return m_player.getBestiaryKillCount(56) >= 10000; + } else if (name == "Doomsday Nemesis") { + // Kill Gaz'haragoth one time. + return m_player.getBestiaryKillCount(1003) >= 1; + } else if (name == "Hero of Bounac") { + // Complete The Order of the Lion Quest. + } else if (name == "King of Demon") { + // Defeat Morshabaal 5 times. + return m_player.getBestiaryKillCount(2118) >= 5; + } else if (name == "Planegazer") { + // Kill Planestrider in Opticording Sphere Quest. + } else if (name == "Time Traveller") { + // Complete 25 Years of Tibia Quest. + } else if (name == "Truly Boss") { + return m_player.getBossPoints() >= 15000; + } + return false; +} diff --git a/src/creatures/players/cyclopedia/player_title.hpp b/src/creatures/players/cyclopedia/player_title.hpp new file mode 100644 index 00000000000..abf29650961 --- /dev/null +++ b/src/creatures/players/cyclopedia/player_title.hpp @@ -0,0 +1,104 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#pragma once + +#include + +#include "creatures/creatures_definitions.hpp" +#include "enums/player_cyclopedia.hpp" +#include "enums/account_group_type.hpp" + +class Player; +class KV; + +struct Title { + uint8_t m_id = 0; + CyclopediaTitle_t m_type = CyclopediaTitle_t::NOTHING; + std::string m_maleName; + std::string m_femaleName; + std::string m_description; + uint32_t m_amount = 0; + bool m_permanent = false; + uint8_t m_skill = 0; + uint16_t m_race = 0; + + Title() = default; + + Title(uint8_t id, CyclopediaTitle_t type, std::string maleName, std::string description, uint32_t amount, bool permanent) : + m_id(id), m_type(type), m_maleName(std::move(maleName)), m_description(std::move(description)), m_amount(amount), + m_permanent(permanent) { } + + Title(uint8_t id, CyclopediaTitle_t type, std::string maleName, std::string description, uint32_t amount, bool permanent, std::string femaleName) : + m_id(id), m_type(type), m_maleName(std::move(maleName)), m_description(std::move(description)), m_amount(amount), + m_permanent(permanent), m_femaleName(std::move(femaleName)) { } + + Title(uint8_t id, CyclopediaTitle_t type, std::string maleName, std::string femaleName, std::string description, uint8_t skill) : + m_id(id), m_type(type), m_maleName(std::move(maleName)), m_femaleName(std::move(femaleName)), m_description(std::move(description)), + m_skill(skill) { } + + Title(uint8_t id, CyclopediaTitle_t type, uint16_t race, std::string maleName, std::string femaleName, std::string description) : + m_id(id), m_type(type), m_race(race), m_maleName(std::move(maleName)), m_femaleName(std::move(femaleName)), + m_description(std::move(description)) { } + + Title(uint8_t id, CyclopediaTitle_t type, uint16_t race, std::string maleName, std::string femaleName, std::string description, uint32_t amount, bool permanent) : + m_id(id), m_type(type), m_race(race), m_maleName(std::move(maleName)), m_femaleName(std::move(femaleName)), + m_description(std::move(description)), m_amount(amount), m_permanent(permanent) { } + + Title(uint8_t id, CyclopediaTitle_t type, std::string maleName, std::string description, bool permanent) : + m_id(id), m_type(type), m_maleName(std::move(maleName)), m_description(std::move(description)), m_permanent(permanent) { } + + bool operator==(const Title &other) const { + return m_id == other.m_id; + } +}; + +namespace std { + template <> + struct hash { + std::size_t operator()(const Title &t) const { + return hash<uint8_t>()(t.m_id); + } + }; +} + +class PlayerTitle { +public: + explicit PlayerTitle(Player &player); + + [[nodiscard]] bool isTitleUnlocked(uint8_t id) const; + bool manage(bool canAdd, uint8_t id, uint32_t timestamp = 0); + void remove(const Title &title); + const std::vector<std::pair<Title, uint32_t>> &getUnlockedTitles(); + [[nodiscard]] uint8_t getCurrentTitle() const; + void setCurrentTitle(uint8_t id); + std::string getCurrentTitleName(); + static const std::string &getNameBySex(PlayerSex_t sex, const std::string &male, const std::string &female); + void checkAndUpdateNewTitles(); + void loadUnlockedTitles(); + const std::shared_ptr<KV> &getUnlockedKV(); + + // Title Calculate Functions + bool checkGold(uint32_t amount); + bool checkMount(uint32_t amount); + bool checkOutfit(uint32_t amount); + bool checkLevel(uint32_t amount); + bool checkHighscore(uint8_t skill); + bool checkBestiary(const std::string &name, uint16_t race, bool isBoss = false, uint32_t amount = 0); + bool checkLoginStreak(uint32_t amount); + bool checkTask(uint32_t amount); + bool checkMap(uint32_t amount); + bool checkOther(const std::string &name); + +private: + // {title ID, time when it was unlocked} + std::shared_ptr<KV> m_titleUnlockedKV; + std::vector<std::pair<Title, uint32_t>> m_titlesUnlocked; + Player &m_player; +}; diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 0ddb407687b..92249e1114f 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -17,6 +17,7 @@ #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_title.hpp" #include "creatures/players/storages/storages.hpp" #include "game/game.hpp" #include "game/modal_window/modal_window.hpp" @@ -51,6 +52,7 @@ Player::Player(ProtocolGame_ptr p) : m_wheelPlayer = std::make_unique<PlayerWheel>(*this); m_playerAchievement = std::make_unique<PlayerAchievement>(*this); m_playerBadge = std::make_unique<PlayerBadge>(*this); + m_playerTitle = std::make_unique<PlayerTitle>(*this); } Player::~Player() { @@ -119,9 +121,10 @@ std::string Player::getDescription(int32_t lookDistance) { std::ostringstream s; std::string subjectPronoun = getSubjectPronoun(); capitalizeWords(subjectPronoun); + auto playerTitle = title()->getCurrentTitle() == 0 ? "" : (", " + title()->getCurrentTitleName()); if (lookDistance == -1) { - s << "yourself."; + s << "yourself" << playerTitle << "."; if (group->access) { s << " You are " << group->name << '.'; @@ -144,7 +147,7 @@ std::string Player::getDescription(int32_t lookDistance) { s << " (Level " << level << ')'; } - s << ". " << subjectPronoun; + s << playerTitle << ". " << subjectPronoun; if (group->access) { s << " " << getSubjectVerb() << " " << group->name << '.'; @@ -8016,6 +8019,15 @@ const std::unique_ptr<PlayerBadge> &Player::badge() const { return m_playerBadge; } +// Title interface +std::unique_ptr<PlayerTitle> &Player::title() { + return m_playerTitle; +} + +const std::unique_ptr<PlayerTitle> &Player::title() const { + return m_playerTitle; +} + void Player::sendLootMessage(const std::string &message) const { auto party = getParty(); if (!party) { diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 7d0291c4a3f..da4c0cec9e8 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -34,7 +34,9 @@ #include "creatures/npcs/npc.hpp" #include "game/bank/bank.hpp" #include "enums/object_category.hpp" +#include "enums/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_title.hpp" class House; class NetworkMessage; @@ -51,12 +53,14 @@ class Spell; class PlayerWheel; class PlayerAchievement; class PlayerBadge; +class PlayerTitle; class Spectators; class Account; struct ModalWindow; struct Achievement; struct Badge; +struct Title; struct ForgeHistory { ForgeAction_t actionType = ForgeAction_t::FUSION; @@ -2616,6 +2620,10 @@ class Player final : public Creature, public Cylinder, public Bankable { std::unique_ptr<PlayerBadge> &badge(); const std::unique_ptr<PlayerBadge> &badge() const; + // Player title interface + std::unique_ptr<PlayerTitle> &title(); + const std::unique_ptr<PlayerTitle> &title() const; + void sendLootMessage(const std::string &message) const; std::shared_ptr<Container> getLootPouch(); @@ -3011,10 +3019,12 @@ class Player final : public Creature, public Cylinder, public Bankable { friend class IOLoginDataSave; friend class PlayerAchievement; friend class PlayerBadge; + friend class PlayerTitle; std::unique_ptr<PlayerWheel> m_wheelPlayer; std::unique_ptr<PlayerAchievement> m_playerAchievement; std::unique_ptr<PlayerBadge> m_playerBadge; + std::unique_ptr<PlayerTitle> m_playerTitle; std::mutex quickLootMutex; diff --git a/src/enums/player_cyclopedia.hpp b/src/enums/player_cyclopedia.hpp new file mode 100644 index 00000000000..f0637011a19 --- /dev/null +++ b/src/enums/player_cyclopedia.hpp @@ -0,0 +1,38 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 OpenTibiaBR <opentibiabr@outlook.com> + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#pragma once + +#ifndef USE_PRECOMPILED_HEADERS + #include <cstdint> +#endif + +enum CyclopediaBadge_t : uint8_t { + ACCOUNT_AGE = 1, + LOYALTY, + ACCOUNT_ALL_LEVEL, + ACCOUNT_ALL_VOCATIONS, + TOURNAMENT_PARTICIPATION, + TOURNAMENT_POINTS, +}; + +enum CyclopediaTitle_t : uint8_t { + NOTHING = 0, + GOLD, + MOUNTS, + OUTFITS, + LEVEL, + HIGHSCORES, + BESTIARY, + BOSSTIARY, + DAILY_REWARD, + TASK, + MAP, + OTHERS, +}; diff --git a/src/game/game.cpp b/src/game/game.cpp index fe52310e6b4..d731bd766b6 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -38,6 +38,7 @@ #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_title.hpp" #include "creatures/npcs/npc.hpp" #include "server/network/webhook/webhook.hpp" #include "server/network/protocol/protocollogin.hpp" @@ -222,14 +223,121 @@ Game::Game() { Badge(21, CyclopediaBadge_t::TOURNAMENT_POINTS, "Tournament Champion", 10000), }; + m_titles = { + Title(1, CyclopediaTitle_t::GOLD, "Gold Hoarder", "Earned at least 1,000,000 gold.", 1000000, false), + Title(2, CyclopediaTitle_t::GOLD, "Platinum Hoarder", "Earned at least 10,000,000 gold.", 10000000, false), + Title(3, CyclopediaTitle_t::GOLD, "Crystal Hoarder", "Earned at least 100,000,000 gold.", 100000000, false), + + Title(4, CyclopediaTitle_t::MOUNTS, "Beaststrider (Grade 1)", "Unlocked 10 or more Mounts.", 10, true), + Title(5, CyclopediaTitle_t::MOUNTS, "Beaststrider (Grade 2)", "Unlocked 20 or more Mounts.", 20, true), + Title(6, CyclopediaTitle_t::MOUNTS, "Beaststrider (Grade 3)", "Unlocked 30 or more Mounts.", 30, true), + Title(7, CyclopediaTitle_t::MOUNTS, "Beaststrider (Grade 4)", "Unlocked 40 or more Mounts.", 40, true), + Title(8, CyclopediaTitle_t::MOUNTS, "Beaststrider (Grade 5)", "Unlocked 50 or more Mounts.", 50, true), + + Title(9, CyclopediaTitle_t::OUTFITS, "Tibia's Topmodel (Grade 1)", "Unlocked 10 or more Outfits.", 10, true), + Title(10, CyclopediaTitle_t::OUTFITS, "Tibia's Topmodel (Grade 2)", "Unlocked 20 or more Outfits.", 20, true), + Title(11, CyclopediaTitle_t::OUTFITS, "Tibia's Topmodel (Grade 3)", "Unlocked 30 or more Outfits.", 30, true), + Title(12, CyclopediaTitle_t::OUTFITS, "Tibia's Topmodel (Grade 4)", "Unlocked 40 or more Outfits.", 40, true), + Title(13, CyclopediaTitle_t::OUTFITS, "Tibia's Topmodel (Grade 5)", "Unlocked 50 or more Outfits.", 50, true), + + Title(14, CyclopediaTitle_t::LEVEL, "Trolltrasher", "Reached level 50.", 50, false), + Title(15, CyclopediaTitle_t::LEVEL, "Cyclopscamper", "Reached level 100.", 100, false), + Title(16, CyclopediaTitle_t::LEVEL, "Dragondouser", "Reached level 200.", 200, false), + Title(17, CyclopediaTitle_t::LEVEL, "Demondoom", "Reached level 300.", 300, false), + Title(18, CyclopediaTitle_t::LEVEL, "Drakenbane", "Reached level 400.", 400, false), + Title(19, CyclopediaTitle_t::LEVEL, "Silencer", "Reached level 500.", 500, false), + Title(20, CyclopediaTitle_t::LEVEL, "Exalted", "Reached level 1000.", 1000, false), + + Title(21, CyclopediaTitle_t::HIGHSCORES, "Apex Predator", "", "Highest Level on character's world.", static_cast<uint8_t>(HighscoreCategories_t::EXPERIENCE)), + Title(22, CyclopediaTitle_t::HIGHSCORES, "Big Boss", "", "Highest score of accumulated boss points on character's world.", static_cast<uint8_t>(HighscoreCategories_t::BOSS_POINTS)), + Title(23, CyclopediaTitle_t::HIGHSCORES, "Jack of all Taints", "", "Highest score for killing Goshnar and his aspects on character's world.", static_cast<uint8_t>(HighscoreCategories_t::GOSHNAR)), + Title(24, CyclopediaTitle_t::HIGHSCORES, "Legend of Fishing", "", "Highest fishing level on character's world.", static_cast<uint8_t>(HighscoreCategories_t::FISHING)), + Title(25, CyclopediaTitle_t::HIGHSCORES, "Legend of Magic", "", "Highest magic level on character's world.", static_cast<uint8_t>(HighscoreCategories_t::MAGIC_LEVEL)), + Title(26, CyclopediaTitle_t::HIGHSCORES, "Legend of Marksmanship", "", "Highest distance level on character's world.", static_cast<uint8_t>(HighscoreCategories_t::DISTANCE_FIGHTING)), + Title(27, CyclopediaTitle_t::HIGHSCORES, "Legend of the Axe", "", "Highest axe level on character's world.", static_cast<uint8_t>(HighscoreCategories_t::AXE_FIGHTING)), + Title(28, CyclopediaTitle_t::HIGHSCORES, "Legend of the Club", "", "Highest club level on character's world.", static_cast<uint8_t>(HighscoreCategories_t::CLUB_FIGHTING)), + Title(29, CyclopediaTitle_t::HIGHSCORES, "Legend of the Fist", "", "Highest fist level on character's world.", static_cast<uint8_t>(HighscoreCategories_t::FIST_FIGHTING)), + Title(30, CyclopediaTitle_t::HIGHSCORES, "Legend of the Shield", "", "Highest shielding level on character's world.", static_cast<uint8_t>(HighscoreCategories_t::SHIELDING)), + Title(31, CyclopediaTitle_t::HIGHSCORES, "Legend of the Sword", "", "Highest sword level on character's world.", static_cast<uint8_t>(HighscoreCategories_t::SWORD_FIGHTING)), + Title(32, CyclopediaTitle_t::HIGHSCORES, "Prince Charming", "Princess Charming", "Highest score of accumulated charm points on character's world.", static_cast<uint8_t>(HighscoreCategories_t::CHARMS)), + Title(33, CyclopediaTitle_t::HIGHSCORES, "Reigning Drome Champion", "", "Finished most recent Tibiadrome rota ranked in the top 5.", static_cast<uint8_t>(HighscoreCategories_t::DROME)), + + Title(34, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_HUMANOID), "Bipedantic", "", "Unlocked All Humanoid Bestiary entries."), + Title(35, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_LYCANTHROPE), "Blood Moon Hunter", "Blood Moon Huntress", "Unlocked All Lycanthrope Bestiary entries."), + Title(36, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_AMPHIBIC), "Coldblooded", "", "Unlocked All Amphibic Bestiary entries."), + Title(37, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_BIRD), "Death from Below", "", "Unlocked all Bird Bestiary entries."), + Title(38, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_DEMON), "Demonator", "", "Unlocked all Demon Bestiary entries."), + Title(39, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_DRAGON), "Dragonslayer", "", "Unlocked all Dragon Bestiary entries."), + Title(40, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_ELEMENTAL), "Elementalist", "", "Unlocked all Elemental Bestiary entries."), + Title(41, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_VERMIN), "Exterminator", "", "Unlocked all Vermin Bestiary entries."), + Title(42, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_FEY), "Fey Swatter", "", "Unlocked all Fey Bestiary entries."), + Title(43, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_UNDEAD), "Ghosthunter", "Ghosthuntress", "Unlocked all Undead Bestiary entries."), + Title(44, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_CONSTRUCT), "Handyman", "Handywoman", "Unlocked all Construct Bestiary entries."), + Title(45, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_MAMMAL), "Huntsman", "Huntress", "Unlocked all Mammal Bestiary entries."), + Title(46, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_EXTRA_DIMENSIONAL), "Interdimensional Destroyer", "", "Unlocked all Extra Dimensional Bestiary entries."), + Title(47, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_HUMAN), "Manhunter", "Manhuntress", "Unlocked all Human Bestiary entries."), + Title(48, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_MAGICAL), "Master of Illusion", "Mistress of Illusion", "Unlocked all Magical Bestiary entries."), + Title(49, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_SLIME), "Ooze Blues", "", "Unlocked all Slime Bestiary entries."), + Title(50, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_AQUATIC), "Sea Bane", "", "Unlocked all Aquatic Bestiary entries."), + Title(51, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_REPTILE), "Snake Charmer", "", "Unlocked all Reptile Bestiary entries."), + Title(52, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_GIANT), "Tumbler", "", "Unlocked all Giant Bestiary entries."), + Title(53, CyclopediaTitle_t::BESTIARY, static_cast<uint16_t>(BestiaryType_t::BESTY_RACE_PLANT), "Weedkiller", "", "Unlocked all Plant Bestiary entries."), + Title(54, CyclopediaTitle_t::BESTIARY, 0, "Executioner", "", "Unlocked all Bestiary entries."), + + Title(55, CyclopediaTitle_t::BOSSTIARY, static_cast<uint16_t>(BosstiaryRarity_t::RARITY_NEMESIS), "Boss Annihilator", "", "Unlocked all Nemesis bosses.", 0, false), + Title(56, CyclopediaTitle_t::BOSSTIARY, static_cast<uint16_t>(BosstiaryRarity_t::RARITY_ARCHFOE), "Boss Destroyer", "", "Unlocked 10 or more Archfoe bosses.", 10, true), + Title(57, CyclopediaTitle_t::BOSSTIARY, static_cast<uint16_t>(BosstiaryRarity_t::RARITY_NEMESIS), "Boss Devastator", "", "Unlocked 10 or more Nemesis bosses.", 10, true), + Title(58, CyclopediaTitle_t::BOSSTIARY, static_cast<uint16_t>(BosstiaryRarity_t::RARITY_ARCHFOE), "Boss Eraser", "", "Unlocked all Archfoe bosses.", 0, false), + Title(59, CyclopediaTitle_t::BOSSTIARY, 0, "Boss Executioner", "", "Unlocked all bosses.", 0, false), + Title(60, CyclopediaTitle_t::BOSSTIARY, static_cast<uint16_t>(BosstiaryRarity_t::RARITY_BANE), "Boss Hunter", "", "Unlocked 10 or more Bane bosses.", 10, true), + Title(61, CyclopediaTitle_t::BOSSTIARY, static_cast<uint16_t>(BosstiaryRarity_t::RARITY_NEMESIS), "Boss Obliterator", "", "Unlocked 40 or more Nemesis bosses.", 40, true), + Title(62, CyclopediaTitle_t::BOSSTIARY, static_cast<uint16_t>(BosstiaryRarity_t::RARITY_BANE), "Boss Slayer", "", "Unlocked all Bane bosses.", 0, false), + Title(63, CyclopediaTitle_t::BOSSTIARY, static_cast<uint16_t>(BosstiaryRarity_t::RARITY_ARCHFOE), "Boss Smiter", "", "Unlocked 40 or more Archfoe bosses.", 40, true), + Title(64, CyclopediaTitle_t::BOSSTIARY, static_cast<uint16_t>(BosstiaryRarity_t::RARITY_BANE), "Boss Veteran", "", "Unlocked 40 or more Bane bosses.", 40, true), + + Title(65, CyclopediaTitle_t::DAILY_REWARD, "Creature of Habit (Grade 1)", "Reward Streak of at least 7 days of consecutive logins.", 7, true), + Title(66, CyclopediaTitle_t::DAILY_REWARD, "Creature of Habit (Grade 2)", "Reward Streak of at least 30 days of consecutive logins.", 30, true), + Title(67, CyclopediaTitle_t::DAILY_REWARD, "Creature of Habit (Grade 3)", "Reward Streak of at least 90 days of consecutive logins.", 90, true), + Title(68, CyclopediaTitle_t::DAILY_REWARD, "Creature of Habit (Grade 4)", "Reward Streak of at least 180 days of consecutive logins.", 180, true), + Title(69, CyclopediaTitle_t::DAILY_REWARD, "Creature of Habit (Grade 5)", "Reward Streak of at least 365 days of consecutive logins.", 365, true), + + Title(70, CyclopediaTitle_t::TASK, "Aspiring Huntsman", "Invested 160,000 tasks points.", 160000, true, "Aspiring Huntswoman"), + Title(71, CyclopediaTitle_t::TASK, "Competent Beastslayer", "Invested 320,000 tasks points.", 320000, true), + Title(72, CyclopediaTitle_t::TASK, "Feared Bountyhunter", "Invested 430,000 tasks points.", 430000, true), + + Title(73, CyclopediaTitle_t::MAP, "Dedicated Entrepreneur", "Explored 50% of all the map areas.", 50, false), + Title(74, CyclopediaTitle_t::MAP, "Globetrotter", "Explored all map areas.", 100, false), + + Title(75, CyclopediaTitle_t::OTHERS, "Guild Leader", "Leading a Guild.", false), + Title(76, CyclopediaTitle_t::OTHERS, "Proconsul of Iksupan", "Only a true devotee to the cause of the ancient Iks and their lost legacy may step up to the rank of proconsul.", true), + Title(77, CyclopediaTitle_t::OTHERS, "Admirer of the Crown", "Adjust your crown and handle it.", true), + Title(78, CyclopediaTitle_t::OTHERS, "Big Spender", "Unlocked the full Golden Outfit.", true), + Title(79, CyclopediaTitle_t::OTHERS, "Challenger of the Iks", "Challenged Ahau, guardian of Iksupan, in traditional Iks warrior attire.", true), + Title(80, CyclopediaTitle_t::OTHERS, "Royal Bounacean Advisor", "Called to the court of Bounac by Kesar the Younger himself.", true), + Title(81, CyclopediaTitle_t::OTHERS, "Aeternal", "Awarded exclusively to stalwart heroes keeping the faith under all circumstances.", true), + Title(82, CyclopediaTitle_t::OTHERS, "Robinson Crusoe", "Some discoveries are reserved to only the most experienced adventurers. Until the next frontier opens on the horizon.", true), + Title(83, CyclopediaTitle_t::OTHERS, "Chompmeister", "Awarded only to true connoisseurs undertaking even the most exotic culinary escapades.", true), + Title(84, CyclopediaTitle_t::OTHERS, "Bringer of Rain", "Forging through battle after battle like a true gladiator.", true), + Title(85, CyclopediaTitle_t::OTHERS, "Beastly", "Reached 2000 charm points. Quite beastly!", true), + Title(86, CyclopediaTitle_t::OTHERS, "Midnight Hunter", "When the hunter becomes the hunted, perseverance decides the game.", true), + Title(87, CyclopediaTitle_t::OTHERS, "Ratinator", "Killing some snarky cave rats is helpful, killing over ten thousand of them is a statement.", true), + Title(88, CyclopediaTitle_t::OTHERS, "Doomsday Nemesis", "Awarded for great help in the battle against Gaz'haragoth.", true), + Title(89, CyclopediaTitle_t::OTHERS, "Hero of Bounac", "You prevailed during the battle of Bounac and broke the siege that held Bounac's people in its firm grasp.", true), // Derrotar o boss Drume. + Title(90, CyclopediaTitle_t::OTHERS, "King of Demon", "Defeat Morshabaal 5 times.", 0, true, "Queen of Demon"), + Title(91, CyclopediaTitle_t::OTHERS, "Planegazer", "Followed the trail of the Planestrider to the end.", true), // Derrotar o boss Planestrider + Title(92, CyclopediaTitle_t::OTHERS, "Time Traveller", "Anywhere in time or space.", true), // Derrotar o boss Lord Retro + Title(93, CyclopediaTitle_t::OTHERS, "Truly Boss", "Reach 15,000 boss points.", true), + }; + m_highscoreCategoriesNames = { { static_cast<uint8_t>(HighscoreCategories_t::ACHIEVEMENTS), "Achievement Points" }, { static_cast<uint8_t>(HighscoreCategories_t::AXE_FIGHTING), "Axe Fighting" }, + { static_cast<uint8_t>(HighscoreCategories_t::BOSS_POINTS), "Boss Points" }, { static_cast<uint8_t>(HighscoreCategories_t::CHARMS), "Charm Points" }, { static_cast<uint8_t>(HighscoreCategories_t::CLUB_FIGHTING), "Club Fighting" }, - { static_cast<uint8_t>(HighscoreCategories_t::EXPERIENCE), "Experience Points" }, { static_cast<uint8_t>(HighscoreCategories_t::DISTANCE_FIGHTING), "Distance Fighting" }, { static_cast<uint8_t>(HighscoreCategories_t::DROME), "Drome Score" }, + { static_cast<uint8_t>(HighscoreCategories_t::EXPERIENCE), "Experience Points" }, { static_cast<uint8_t>(HighscoreCategories_t::FISHING), "Fishing" }, { static_cast<uint8_t>(HighscoreCategories_t::FIST_FIGHTING), "Fist Fighting" }, { static_cast<uint8_t>(HighscoreCategories_t::GOSHNAR), "Goshnar's Taint" }, @@ -8163,10 +8271,9 @@ void Game::kickPlayer(uint32_t playerId, bool displayEffect) { void Game::playerFriendSystemAction(std::shared_ptr<Player> player, uint8_t type, uint8_t titleId) { uint32_t playerGUID = player->getGUID(); if (type == 0x0E) { - // todo in titles system PR - // player->title()->setCurrentTitle(titleId); - // player->sendCyclopediaCharacterBaseInformation(); - // player->sendCyclopediaCharacterTitles(); + player->title()->setCurrentTitle(titleId); + player->sendCyclopediaCharacterBaseInformation(); + player->sendCyclopediaCharacterTitles(); return; } } @@ -8429,14 +8536,10 @@ void Game::processHighscoreResults(DBResult_ptr result, uint32_t playerID, uint8 characters.reserve(result->countResults()); if (result) { do { - uint8_t characterVocation; const auto &voc = g_vocations().getVocation(result->getNumber<uint16_t>("vocation")); - if (voc) { - characterVocation = voc->getClientId(); - } else { - characterVocation = 0; - } - characters.emplace_back(std::move(result->getString("name")), result->getNumber<uint64_t>("points"), result->getNumber<uint32_t>("id"), result->getNumber<uint32_t>("rank"), result->getNumber<uint16_t>("level"), characterVocation); + uint8_t characterVocation = voc ? voc->getClientId() : 0; + std::string loyaltyTitle = ""; // todo get loyalty title from player + characters.emplace_back(std::move(result->getString("name")), result->getNumber<uint64_t>("points"), result->getNumber<uint32_t>("id"), result->getNumber<uint32_t>("rank"), result->getNumber<uint16_t>("level"), characterVocation, loyaltyTitle); } while (result->next()); } @@ -8491,39 +8594,7 @@ void Game::playerHighscores(std::shared_ptr<Player> player, HighscoreType_t type return; } - std::string categoryName; - const auto &categoryType = static_cast<HighscoreCategories_t>(category); - switch (categoryType) { - case HighscoreCategories_t::FIST_FIGHTING: - categoryName = "skill_fist"; - break; - case HighscoreCategories_t::CLUB_FIGHTING: - categoryName = "skill_club"; - break; - case HighscoreCategories_t::SWORD_FIGHTING: - categoryName = "skill_sword"; - break; - case HighscoreCategories_t::AXE_FIGHTING: - categoryName = "skill_axe"; - break; - case HighscoreCategories_t::DISTANCE_FIGHTING: - categoryName = "skill_dist"; - break; - case HighscoreCategories_t::SHIELDING: - categoryName = "skill_shielding"; - break; - case HighscoreCategories_t::FISHING: - categoryName = "skill_fishing"; - break; - case HighscoreCategories_t::MAGIC_LEVEL: - categoryName = "maglevel"; - break; - default: { - category = static_cast<uint8_t>(HighscoreCategories_t::EXPERIENCE); - categoryName = "experience"; - break; - } - } + std::string categoryName = getSkillNameById(category); std::string query; if (type == HIGHSCORE_GETENTRIES) { @@ -8541,6 +8612,32 @@ void Game::playerHighscores(std::shared_ptr<Player> player, HighscoreType_t type player->addAsyncOngoingTask(PlayerAsyncTask_Highscore); } +std::string Game::getSkillNameById(uint8_t &skill) { + switch (static_cast<HighscoreCategories_t>(skill)) { + case HighscoreCategories_t::FIST_FIGHTING: + return "skill_fist"; + case HighscoreCategories_t::CLUB_FIGHTING: + return "skill_club"; + case HighscoreCategories_t::SWORD_FIGHTING: + return "skill_sword"; + case HighscoreCategories_t::AXE_FIGHTING: + return "skill_axe"; + case HighscoreCategories_t::DISTANCE_FIGHTING: + return "skill_dist"; + case HighscoreCategories_t::SHIELDING: + return "skill_shielding"; + case HighscoreCategories_t::FISHING: + return "skill_fishing"; + case HighscoreCategories_t::MAGIC_LEVEL: + return "maglevel"; + case HighscoreCategories_t::BOSS_POINTS: + return "boss_points"; + default: + skill = static_cast<uint8_t>(HighscoreCategories_t::EXPERIENCE); + return "experience"; + } +} + void Game::playerReportRuleViolationReport(uint32_t playerId, const std::string &targetName, uint8_t reportType, uint8_t reportReason, const std::string &comment, const std::string &translation) { std::shared_ptr<Player> player = getPlayerByID(playerId); if (!player) { @@ -10572,7 +10669,7 @@ std::map<uint16_t, Achievement> Game::getAchievements() { void Game::logCyclopediaStats() { g_logger().info("Loaded {} badges from Badge System", m_badges.size()); - // todo in title system: g_logger().info("Loaded {} titles from Title system", m_titles.size()); + g_logger().info("Loaded {} titles from Title system", m_titles.size()); } std::unordered_set<Badge> Game::getBadges() { @@ -10604,3 +10701,33 @@ Badge Game::getBadgeByName(const std::string &name) { } return {}; } + +std::unordered_set<Title> Game::getTitles() { + return m_titles; +} + +Title Game::getTitleById(uint8_t id) { + if (id == 0) { + return {}; + } + auto it = std::find_if(m_titles.begin(), m_titles.end(), [id](const Title &t) { + return t.m_id == id; + }); + if (it != m_titles.end()) { + return *it; + } + return {}; +} + +Title Game::getTitleByName(const std::string &name) { + if (name.empty()) { + return {}; + } + auto it = std::find_if(m_titles.begin(), m_titles.end(), [name](const Title &t) { + return t.m_maleName == name; + }); + if (it != m_titles.end()) { + return *it; + } + return {}; +} diff --git a/src/game/game.hpp b/src/game/game.hpp index 9d743895c4c..554c418a2c8 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -51,6 +51,7 @@ class Spectators; struct Achievement; struct HighscoreCategory; struct Badge; +struct Title; static constexpr uint16_t SERVER_BEAT = 0x32; static constexpr int32_t EVENT_MS = 10000; @@ -313,6 +314,7 @@ class Game { void playerCyclopediaCharacterInfo(std::shared_ptr<Player> player, uint32_t characterID, CyclopediaCharacterInfoType_t characterInfoType, uint16_t entriesPerPage, uint16_t page); void playerHighscores(std::shared_ptr<Player> player, HighscoreType_t type, uint8_t category, uint32_t vocation, const std::string &worldName, uint16_t page, uint8_t entriesPerPage); + static std::string getSkillNameById(uint8_t &skill); void updatePlayerSaleItems(uint32_t playerId); @@ -726,11 +728,16 @@ class Game { Badge getBadgeById(uint8_t id); Badge getBadgeByName(const std::string &name); + std::unordered_set<Title> getTitles(); + Title getTitleById(uint8_t id); + Title getTitleByName(const std::string &name); + private: std::map<uint16_t, Achievement> m_achievements; std::map<std::string, uint16_t> m_achievementsNameToId; std::unordered_set<Badge> m_badges; + std::unordered_set<Title> m_titles; std::vector<HighscoreCategory> m_highscoreCategories; std::unordered_map<uint8_t, std::string> m_highscoreCategoriesNames; diff --git a/src/game/game_definitions.hpp b/src/game/game_definitions.hpp index 2c2b40fac78..a6ce6e7eaa8 100644 --- a/src/game/game_definitions.hpp +++ b/src/game/game_definitions.hpp @@ -61,15 +61,6 @@ enum LightState_t { LIGHT_STATE_SUNRISE, }; -enum CyclopediaBadge_t : uint8_t { - ACCOUNT_AGE = 1, - LOYALTY, - ACCOUNT_ALL_LEVEL, - ACCOUNT_ALL_VOCATIONS, - TOURNAMENT_PARTICIPATION, - TOURNAMENT_POINTS, -}; - enum CyclopediaCharacterInfoType_t : uint8_t { CYCLOPEDIA_CHARACTERINFO_BASEINFORMATION = 0, CYCLOPEDIA_CHARACTERINFO_GENERALSTATS = 1, @@ -108,6 +99,7 @@ enum class HighscoreCategories_t : uint8_t { CHARMS = 11, DROME = 12, GOSHNAR = 13, + BOSS_POINTS = 14, }; enum HighscoreType_t : uint8_t { diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index 2ac2cc863ea..0498b40e29f 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -890,6 +890,7 @@ void IOLoginDataLoad::loadPlayerInitializeSystem(std::shared_ptr<Player> player) player->achiev()->loadUnlockedAchievements(); player->badge()->checkAndUpdateNewBadges(); + player->title()->checkAndUpdateNewTitles(); player->initializePrey(); player->initializeTaskHunting(); diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp index 5f705cf426d..a91889e43bc 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -28,6 +28,7 @@ #include "lua/callbacks/events_callbacks.hpp" #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_title.hpp" #include "map/spectators.hpp" // Game diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 81b70263a0a..51dd79638ab 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -16,6 +16,7 @@ #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_title.hpp" #include "game/game.hpp" #include "io/iologindata.hpp" #include "io/ioprey.hpp" @@ -4293,3 +4294,57 @@ int PlayerFunctions::luaPlayerAddBadge(lua_State* L) { pushBoolean(L, true); return 1; } + +int PlayerFunctions::luaPlayerAddTitle(lua_State* L) { + // player:addTitle(id) + const auto &player = getUserdataShared<Player>(L, 1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + player->title()->manage(true, getNumber<uint8_t>(L, 2, 0)); + pushBoolean(L, true); + return 1; +} + +int PlayerFunctions::luaPlayerGetTitles(lua_State* L) { + // player:getTitles() + const auto &player = getUserdataShared<Player>(L, 1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + auto playerTitles = player->title()->getUnlockedTitles(); + lua_createtable(L, static_cast<int>(playerTitles.size()), 0); + + int index = 0; + for (const auto &title : playerTitles) { + lua_createtable(L, 0, 3); + setField(L, "id", title.first.m_id); + setField(L, "name", player->title()->getNameBySex(player->getSex(), title.first.m_maleName, title.first.m_femaleName)); + setField(L, "description", title.first.m_description); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int PlayerFunctions::luaPlayerSetCurrentTitle(lua_State* L) { + // player:setCurrentTitle(id) + const auto &player = getUserdataShared<Player>(L, 1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + const auto &title = g_game().getTitleById(getNumber<uint8_t>(L, 2, 0)); + if (title.m_id == 0) { + reportErrorFunc(getErrorDesc(LUA_ERROR_VARIANT_NOT_FOUND)); + return 1; + } + + player->title()->setCurrentTitle(title.m_id); + pushBoolean(L, true); + return 1; +} diff --git a/src/lua/functions/creatures/player/player_functions.hpp b/src/lua/functions/creatures/player/player_functions.hpp index a30f9f755ca..4d89a86d27f 100644 --- a/src/lua/functions/creatures/player/player_functions.hpp +++ b/src/lua/functions/creatures/player/player_functions.hpp @@ -367,6 +367,11 @@ class PlayerFunctions final : LuaScriptInterface { // Badge Functions registerMethod(L, "Player", "addBadge", PlayerFunctions::luaPlayerAddBadge); + // Title Functions + registerMethod(L, "Player", "addTitle", PlayerFunctions::luaPlayerAddTitle); + registerMethod(L, "Player", "getTitles", PlayerFunctions::luaPlayerGetTitles); + registerMethod(L, "Player", "setCurrentTitle", PlayerFunctions::luaPlayerSetCurrentTitle); + GroupFunctions::init(L); GuildFunctions::init(L); MountFunctions::init(L); @@ -723,5 +728,9 @@ class PlayerFunctions final : LuaScriptInterface { static int luaPlayerAddBadge(lua_State* L); + static int luaPlayerAddTitle(lua_State* L); + static int luaPlayerGetTitles(lua_State* L); + static int luaPlayerSetCurrentTitle(lua_State* L); + friend class CreatureFunctions; }; diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 73b639885e6..d924e0bfb0e 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -28,6 +28,7 @@ #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_title.hpp" #include "creatures/players/grouping/familiars.hpp" #include "server/network/protocol/protocolgame.hpp" #include "game/scheduling/dispatcher.hpp" @@ -2058,9 +2059,8 @@ void ProtocolGame::sendItemInspection(uint16_t itemId, uint8_t itemCount, std::s void ProtocolGame::parseFriendSystemAction(NetworkMessage &msg) { uint8_t state = msg.getByte(); if (state == 0x0E) { - // todo title system pr - // uint8_t titleId = msg.getByte(); - // g_game().playerFriendSystemAction(player, state, titleId); + uint8_t titleId = msg.getByte(); + g_game().playerFriendSystemAction(player, state, titleId); } } @@ -2141,8 +2141,9 @@ void ProtocolGame::sendHighscores(const std::vector<HighscoreCharacter> &charact msg.addByte(0x00); // All data available msg.addByte(1); // Worlds - msg.addString(g_configManager().getString(SERVER_NAME, __FUNCTION__), "ProtocolGame::sendHighscores - g_configManager().getString(SERVER_NAME)"); // First World - msg.addString(g_configManager().getString(SERVER_NAME, __FUNCTION__), "ProtocolGame::sendHighscores - g_configManager().getString(SERVER_NAME)"); // Selected World + auto serverName = g_configManager().getString(SERVER_NAME, __FUNCTION__); + msg.addString(serverName, "ProtocolGame::sendHighscores - g_configManager().getString(SERVER_NAME)"); // First World + msg.addString(serverName, "ProtocolGame::sendHighscores - g_configManager().getString(SERVER_NAME)"); // Selected World msg.addByte(0); // Game World Category: 0xFF(-1) - Selected World msg.addByte(0); // BattlEye World Type @@ -2190,9 +2191,9 @@ void ProtocolGame::sendHighscores(const std::vector<HighscoreCharacter> &charact for (const HighscoreCharacter &character : characters) { msg.add<uint32_t>(character.rank); // Rank msg.addString(character.name, "ProtocolGame::sendHighscores - character.name"); // Character Name - msg.addString("", "ProtocolGame::sendHighscores - empty"); // Probably Character Title(not visible in window) + msg.addString(character.loyaltyTitle, "ProtocolGame::sendHighscores - character.loyaltyTitle"); // Character Loyalty Title msg.addByte(character.vocation); // Vocation Id - msg.addString(g_configManager().getString(SERVER_NAME, __FUNCTION__), "ProtocolGame::sendHighscores - g_configManager().getString(SERVER_NAME)"); // World + msg.addString(serverName, "ProtocolGame::sendHighscores - g_configManager().getString(SERVER_NAME)"); // World msg.add<uint16_t>(character.level); // Level msg.addByte((player->getGUID() == character.id)); // Player Indicator Boolean msg.add<uint64_t>(character.points); // Points @@ -3380,9 +3381,8 @@ void ProtocolGame::sendCyclopediaCharacterBaseInformation() { msg.add<uint16_t>(player->getLevel()); AddOutfit(msg, player->getDefaultOutfit(), false); - msg.addByte(0x00); // Store summary & Character titles - msg.addString("", "ProtocolGame::sendCyclopediaCharacterBaseInformation - empty"); // character title - // msg.addString(player->title()->getCurrentTitleName(), "ProtocolGame::sendCyclopediaCharacterBaseInformation - player->title()->getCurrentTitleName()"); // character title + msg.addByte(0x01); // Store summary & Character titles + msg.addString(player->title()->getCurrentTitleName(), "ProtocolGame::sendCyclopediaCharacterBaseInformation - player->title()->getCurrentTitleName()"); // character title writeToOutputBuffer(msg); } @@ -4003,6 +4003,13 @@ void ProtocolGame::sendCyclopediaCharacterInspection() { msg.addString("Vocation", "ProtocolGame::sendCyclopediaCharacterInspection - Vocation"); msg.addString(player->getVocation()->getVocName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->getVocation()->getVocName()"); + // Player title + if (player->title()->getCurrentTitle() != 0) { + playerDescriptionSize++; + msg.addString("Title", "ProtocolGame::sendCyclopediaCharacterInspection - Title"); + msg.addString(player->title()->getCurrentTitleName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->title()->getCurrentTitleName()"); + } + // Loyalty title if (!player->getLoyaltyTitle().empty()) { playerDescriptionSize++; @@ -4044,7 +4051,6 @@ void ProtocolGame::sendCyclopediaCharacterBadges() { msg.addByte(player->isPremium() ? 0x01 : 0x00); // IsPremium (GOD has always 'Premium') // Character loyalty title msg.addString(player->getLoyaltyTitle(), "ProtocolGame::sendCyclopediaCharacterBadges - player->getLoyaltyTitle()"); - // msg.addByte(0x01); // Enable badges uint8_t badgesSize = 0; auto badgesSizePosition = msg.getBufferPosition(); @@ -4068,12 +4074,26 @@ void ProtocolGame::sendCyclopediaCharacterTitles() { return; } + auto titles = g_game().getTitles(); + NetworkMessage msg; msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_TITLES); msg.addByte(0x00); // 0x00 Here means 'no error' - msg.addByte(0x00); - msg.addByte(0x00); + msg.addByte(player->title()->getCurrentTitle()); + msg.addByte(static_cast<uint8_t>(titles.size())); + + std::string messageTitleName = "ProtocolGame::sendCyclopediaCharacterTitles - title.name"; + std::string messageTitleDesc = "ProtocolGame::sendCyclopediaCharacterTitles - title.description"; + for (const auto &title : titles) { + msg.addByte(title.m_id); + auto titleName = player->title()->getNameBySex(player->getSex(), title.m_maleName, title.m_femaleName); + msg.addString(titleName, messageTitleName); + msg.addString(title.m_description, messageTitleDesc); + msg.addByte(title.m_permanent ? 0x01 : 0x00); + auto isUnlocked = player->title()->isTitleUnlocked(title.m_id); + msg.addByte(isUnlocked ? 0x01 : 0x00); + } writeToOutputBuffer(msg); } diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 19e62c428e9..16105ee0b8b 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -14,6 +14,7 @@ #include "creatures/creature.hpp" #include "enums/forge_conversion.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_title.hpp" class NetworkMessage; class Player; @@ -31,6 +32,7 @@ class TaskHuntingOption; struct ModalWindow; struct Achievement; struct Badge; +struct Title; using ProtocolGame_ptr = std::shared_ptr<ProtocolGame>; diff --git a/src/server/server_definitions.hpp b/src/server/server_definitions.hpp index ad816b76b02..1d5ed5b628e 100644 --- a/src/server/server_definitions.hpp +++ b/src/server/server_definitions.hpp @@ -113,13 +113,14 @@ enum Supply_Stash_Actions_t : uint8_t { }; struct HighscoreCharacter { - HighscoreCharacter(std::string name, uint64_t points, uint32_t id, uint32_t rank, uint16_t level, uint8_t vocation) : + HighscoreCharacter(std::string name, uint64_t points, uint32_t id, uint32_t rank, uint16_t level, uint8_t vocation, std::string loyaltyTitle) : name(std::move(name)), points(points), id(id), rank(rank), level(level), - vocation(vocation) { } + vocation(vocation), + loyaltyTitle(std::move(loyaltyTitle)) { } std::string name; uint64_t points; @@ -127,4 +128,5 @@ struct HighscoreCharacter { uint32_t rank; uint16_t level; uint8_t vocation; + std::string loyaltyTitle; }; diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index 5d40d930646..1c21dc87210 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -46,6 +46,7 @@ <ClInclude Include="..\src\creatures\players\wheel\wheel_gems.hpp" /> <ClInclude Include="..\src\creatures\players\achievement\player_achievement.hpp" /> <ClInclude Include="..\src\creatures\players\cyclopedia\player_badge.hpp" /> + <ClInclude Include="..\src\creatures\players\cyclopedia\player_title.hpp" /> <ClInclude Include="..\src\creatures\players\wheel\player_wheel.hpp" /> <ClInclude Include="..\src\creatures\players\wheel\wheel_definitions.hpp" /> <ClInclude Include="..\src\database\database.hpp" /> @@ -259,6 +260,7 @@ <ClCompile Include="..\src\creatures\players\wheel\wheel_gems.cpp" /> <ClCompile Include="..\src\creatures\players\achievement\player_achievement.cpp" /> <ClCompile Include="..\src\creatures\players\cyclopedia\player_badge.cpp" /> + <ClCompile Include="..\src\creatures\players\cyclopedia\player_title.cpp" /> <ClCompile Include="..\src\creatures\players\wheel\player_wheel.cpp" /> <ClCompile Include="..\src\database\database.cpp" /> <ClCompile Include="..\src\database\databasemanager.cpp" /> @@ -598,3 +600,4 @@ <ImportGroup Label="ExtensionTargets"> </ImportGroup> </Project> +