Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: badge system #2533

Merged
merged 12 commits into from
May 4, 2024
Merged
33 changes: 33 additions & 0 deletions data/scripts/talkactions/god/manage_badge.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
local addBadge = TalkAction("/addbadge")

function addBadge.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: /addbadge 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

-- Trim left
split[2] = split[2]:gsub("^%s*(.-)$", "%1")
local id = tonumber(split[2])
target:addBadge(id)
return true
end

addBadge:separator(" ")
addBadge:groupType("god")
addBadge:register()
1 change: 1 addition & 0 deletions src/canary_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ void CanaryServer::loadModules() {
g_game().loadBoostedCreature();
g_ioBosstiary().loadBoostedBoss();
g_ioprey().initializeTaskHuntOptions();
g_game().logCyclopediaStats();
}

void CanaryServer::modulesLoadHelper(bool loaded, std::string moduleName) {
Expand Down
1 change: 1 addition & 0 deletions src/creatures/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE
players/storages/storages.cpp
players/player.cpp
players/achievement/player_achievement.cpp
players/cyclopedia/player_badge.cpp
players/wheel/player_wheel.cpp
players/wheel/wheel_gems.cpp
players/vocations/vocation.cpp
Expand Down
156 changes: 156 additions & 0 deletions src/creatures/players/cyclopedia/player_badge.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Canary - A free and open-source MMORPG server emulator
* Copyright (©) 2019-2024 OpenTibiaBR <[email protected]>
* 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_badge.hpp"

#include "creatures/players/player.hpp"
#include "game/game.hpp"
#include "kv/kv.hpp"

PlayerBadge::PlayerBadge(Player &player) :
m_player(player) { }

bool PlayerBadge::hasBadge(uint8_t id) const {
if (id == 0) {
return false;
}

if (auto it = std::find_if(m_badgesUnlocked.begin(), m_badgesUnlocked.end(), [id](auto badge_it) {
return badge_it.first.m_id == id;
});
it != m_badgesUnlocked.end()) {
return true;
}

return false;
}

bool PlayerBadge::add(uint8_t id, uint32_t timestamp /* = 0*/) {
if (hasBadge(id)) {
return false;
}

const Badge &badge = g_game().getBadgeById(id);
if (badge.m_id == 0) {
return false;
}

int toSaveTimeStamp = timestamp != 0 ? timestamp : (OTSYS_TIME() / 1000);
getUnlockedKV()->set(badge.m_name, toSaveTimeStamp);
m_badgesUnlocked.emplace_back(badge, toSaveTimeStamp);
m_badgesUnlocked.shrink_to_fit();
return true;
}

void PlayerBadge::checkAndUpdateNewBadges() {
for (const auto &badge : g_game().getBadges()) {
switch (badge.m_type) {
case CyclopediaBadge_t::ACCOUNT_AGE:
if (accountAge(badge.m_amount)) {
add(badge.m_id);
}
break;
case CyclopediaBadge_t::LOYALTY:
if (loyalty(badge.m_amount)) {
add(badge.m_id);
}
break;
case CyclopediaBadge_t::ACCOUNT_ALL_LEVEL:
if (accountAllLevel(badge.m_amount)) {
add(badge.m_id);
}
break;
case CyclopediaBadge_t::ACCOUNT_ALL_VOCATIONS:
if (accountAllVocations(badge.m_amount)) {
add(badge.m_id);
}
break;
case CyclopediaBadge_t::TOURNAMENT_PARTICIPATION:
case CyclopediaBadge_t::TOURNAMENT_POINTS:
break;
}
}

loadUnlockedBadges();
}

void PlayerBadge::loadUnlockedBadges() {
const auto &unlockedBadges = getUnlockedKV()->keys();
g_logger().debug("[{}] - Loading unlocked badges: {}", __FUNCTION__, unlockedBadges.size());
for (const auto &badgeName : unlockedBadges) {
const Badge &badge = g_game().getBadgeByName(badgeName);
if (badge.m_id == 0) {
g_logger().error("[{}] - Badge {} not found.", __FUNCTION__, badgeName);
continue;
}

g_logger().debug("[{}] - Badge {} found for player {}.", __FUNCTION__, badge.m_name, m_player.getName());

m_badgesUnlocked.emplace_back(badge, getUnlockedKV()->get(badgeName)->getNumber());
}
}

const std::shared_ptr<KV> &PlayerBadge::getUnlockedKV() {
if (m_badgeUnlockedKV == nullptr) {
m_badgeUnlockedKV = m_player.kv()->scoped("badges")->scoped("unlocked");
}

return m_badgeUnlockedKV;
}

// Badge Calculate Functions
bool PlayerBadge::accountAge(uint8_t amount) {
return std::floor(m_player.getLoyaltyPoints() / 365) >= amount;
elsongabriel marked this conversation as resolved.
Show resolved Hide resolved
}

bool PlayerBadge::loyalty(uint8_t amount) {
return m_player.getLoyaltyPoints() >= amount;
}

bool PlayerBadge::accountAllLevel(uint8_t amount) {
const auto &players = g_game().getPlayersByAccount(m_player.getAccount(), true);
uint16_t total = std::accumulate(players.begin(), players.end(), 0, [](uint16_t sum, const std::shared_ptr<Player> &player) {
return sum + player->getLevel();
});
return total >= amount;
}

bool PlayerBadge::accountAllVocations(uint8_t amount) {
auto knight = false;
auto paladin = false;
auto druid = false;
auto sorcerer = false;
for (const auto &player : g_game().getPlayersByAccount(m_player.getAccount(), true)) {
if (player->getLevel() >= amount) {
auto vocationEnum = player->getPlayerVocationEnum();
if (vocationEnum == Vocation_t::VOCATION_KNIGHT_CIP) {
knight = true;
} else if (vocationEnum == Vocation_t::VOCATION_SORCERER_CIP) {
sorcerer = true;
} else if (vocationEnum == Vocation_t::VOCATION_PALADIN_CIP) {
paladin = true;
} else if (vocationEnum == Vocation_t::VOCATION_DRUID_CIP) {
druid = true;
}
}
}
return knight && paladin && druid && sorcerer;
}

bool PlayerBadge::tournamentParticipation(uint8_t skill) {

Check warning on line 148 in src/creatures/players/cyclopedia/player_badge.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

[cppcheck] reported by reviewdog 🐶 The function 'tournamentParticipation' is never used. Raw Output: src/creatures/players/cyclopedia/player_badge.cpp:148:The function 'tournamentParticipation' is never used.
// todo check if is used
return false;
}

bool PlayerBadge::tournamentPoints(uint8_t race) {

Check warning on line 153 in src/creatures/players/cyclopedia/player_badge.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

[cppcheck] reported by reviewdog 🐶 The function 'tournamentPoints' is never used. Raw Output: src/creatures/players/cyclopedia/player_badge.cpp:153:The function 'tournamentPoints' is never used.
// todo check if is used
return false;
}
65 changes: 65 additions & 0 deletions src/creatures/players/cyclopedia/player_badge.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Canary - A free and open-source MMORPG server emulator
* Copyright (©) 2019-2024 OpenTibiaBR <[email protected]>
* 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 "game/game_definitions.hpp"

class Player;
class KV;

struct Badge {
uint8_t m_id = 0;
CyclopediaBadge_t m_type;
std::string m_name;
uint16_t m_amount = 0;

Badge() = default;

Badge(uint8_t id, CyclopediaBadge_t type, std::string name, uint16_t amount) :
m_id(id), m_type(type), m_name(std::move(name)), m_amount(amount) { }

bool operator==(const Badge &other) const {
return m_id == other.m_id;
}
};

namespace std {
template <>
struct hash<Badge> {
std::size_t operator()(const Badge &b) const {
return hash<uint8_t>()(b.m_id);
}
};
}

class PlayerBadge {
public:
explicit PlayerBadge(Player &player);

[[nodiscard]] bool hasBadge(uint8_t id) const;
bool add(uint8_t id, uint32_t timestamp = 0);
void checkAndUpdateNewBadges();
void loadUnlockedBadges();
const std::shared_ptr<KV> &getUnlockedKV();

// Badge Calculate Functions
bool accountAge(uint8_t amount);
bool loyalty(uint8_t amount);
bool accountAllLevel(uint8_t amount);
bool accountAllVocations(uint8_t amount);
[[nodiscard]] bool tournamentParticipation(uint8_t skill);
[[nodiscard]] bool tournamentPoints(uint8_t race);

private:
// {badge ID, time when it was unlocked}
std::shared_ptr<KV> m_badgeUnlockedKV;
std::vector<std::pair<Badge, uint32_t>> m_badgesUnlocked;
Player &m_player;
};
59 changes: 44 additions & 15 deletions src/creatures/players/player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "creatures/players/player.hpp"
#include "creatures/players/wheel/player_wheel.hpp"
#include "creatures/players/achievement/player_achievement.hpp"
#include "creatures/players/cyclopedia/player_badge.hpp"
#include "creatures/players/storages/storages.hpp"
#include "game/game.hpp"
#include "game/modal_window/modal_window.hpp"
Expand Down Expand Up @@ -49,6 +50,7 @@ Player::Player(ProtocolGame_ptr p) :
client(std::move(p)) {
m_wheelPlayer = std::make_unique<PlayerWheel>(*this);
m_playerAchievement = std::make_unique<PlayerAchievement>(*this);
m_playerBadge = std::make_unique<PlayerBadge>(*this);
}

Player::~Player() {
Expand Down Expand Up @@ -129,7 +131,7 @@ std::string Player::getDescription(int32_t lookDistance) {
s << " You have no vocation.";
}

if (loyaltyTitle.length() != 0) {
if (!loyaltyTitle.empty()) {
s << " You are a " << loyaltyTitle << ".";
}

Expand All @@ -141,9 +143,8 @@ std::string Player::getDescription(int32_t lookDistance) {
if (!group->access) {
s << " (Level " << level << ')';
}
s << '.';

s << " " << subjectPronoun;
s << ". " << subjectPronoun;

if (group->access) {
s << " " << getSubjectVerb() << " " << group->name << '.';
Expand All @@ -153,7 +154,7 @@ std::string Player::getDescription(int32_t lookDistance) {
s << " has no vocation.";
}

if (loyaltyTitle.length() != 0) {
if (!loyaltyTitle.empty()) {
std::string article = "a";
if (loyaltyTitle[0] == 'A' || loyaltyTitle[0] == 'E' || loyaltyTitle[0] == 'I' || loyaltyTitle[0] == 'O' || loyaltyTitle[0] == 'U') {
article = "an";
Expand Down Expand Up @@ -628,6 +629,20 @@ phmap::flat_hash_map<uint8_t, std::shared_ptr<Item>> Player::getAllSlotItems() c
return itemMap;
}

phmap::flat_hash_map<Blessings_t, std::string> Player::getBlessingNames() const {
elsongabriel marked this conversation as resolved.
Show resolved Hide resolved
static phmap::flat_hash_map<Blessings_t, std::string> blessingNames = {
{ TWIST_OF_FATE, "Twist of Fate" },
{ WISDOM_OF_SOLITUDE, "The Wisdom of Solitude" },
{ SPARK_OF_THE_PHOENIX, "The Spark of the Phoenix" },
{ FIRE_OF_THE_SUNS, "The Fire of the Suns" },
{ SPIRITUAL_SHIELDING, "The Spiritual Shielding" },
{ EMBRACE_OF_TIBIA, "The Embrace of Tibia" },
{ BLOOD_OF_THE_MOUNTAIN, "Blood of the Mountain" },
{ HEARTH_OF_THE_MOUNTAIN, "Heart of the Mountain" },
};
return blessingNames;
}

void Player::setTraining(bool value) {
for (const auto &[key, player] : g_game().getPlayers()) {
if (!this->isInGhostMode() || player->isAccessPlayer()) {
Expand Down Expand Up @@ -6579,24 +6594,14 @@ void Player::initializeTaskHunting() {
}

std::string Player::getBlessingsName() const {
static const phmap::flat_hash_map<Blessings_t, std::string> BlessingNames = {
{ TWIST_OF_FATE, "Twist of Fate" },
{ WISDOM_OF_SOLITUDE, "The Wisdom of Solitude" },
{ SPARK_OF_THE_PHOENIX, "The Spark of the Phoenix" },
{ FIRE_OF_THE_SUNS, "The Fire of the Suns" },
{ SPIRITUAL_SHIELDING, "The Spiritual Shielding" },
{ EMBRACE_OF_TIBIA, "The Embrace of Tibia" },
{ BLOOD_OF_THE_MOUNTAIN, "Blood of the Mountain" },
{ HEARTH_OF_THE_MOUNTAIN, "Heart of the Mountain" },
};

uint8_t count = 0;
std::for_each(blessings.begin(), blessings.end(), [&count](uint8_t amount) {
if (amount != 0) {
count++;
}
});

auto BlessingNames = getBlessingNames();
std::ostringstream os;
for (uint8_t i = 1; i <= 8; i++) {
if (hasBlessing(i)) {
Expand Down Expand Up @@ -7954,6 +7959,15 @@ const std::unique_ptr<PlayerAchievement> &Player::achiev() const {
return m_playerAchievement;
}

// Badge interface
std::unique_ptr<PlayerBadge> &Player::badge() {
return m_playerBadge;
}

const std::unique_ptr<PlayerBadge> &Player::badge() const {
return m_playerBadge;
}

void Player::sendLootMessage(const std::string &message) const {
auto party = getParty();
if (!party) {
Expand Down Expand Up @@ -8073,3 +8087,18 @@ bool Player::canSpeakWithHireling(uint8_t speechbubble) {

return true;
}

uint16_t Player::getPlayerVocationEnum() const {
int cipTibiaId = getVocation()->getClientId();
if (cipTibiaId == 1 || cipTibiaId == 11) {
return Vocation_t::VOCATION_KNIGHT_CIP; // Knight
} else if (cipTibiaId == 2 || cipTibiaId == 12) {
return Vocation_t::VOCATION_PALADIN_CIP; // Paladin
} else if (cipTibiaId == 3 || cipTibiaId == 13) {
return Vocation_t::VOCATION_SORCERER_CIP; // Sorcerer
} else if (cipTibiaId == 4 || cipTibiaId == 14) {
return Vocation_t::VOCATION_DRUID_CIP; // Druid
}

return Vocation_t::VOCATION_NONE;
}
Loading
Loading