Skip to content

Commit

Permalink
feature: badge system (opentibiabr#2533)
Browse files Browse the repository at this point in the history
  • Loading branch information
elsongabriel authored May 4, 2024
1 parent 9f74b27 commit 9efbe4c
Show file tree
Hide file tree
Showing 19 changed files with 573 additions and 103 deletions.
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;
}

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) {
// todo check if is used
return false;
}

bool PlayerBadge::tournamentPoints(uint8_t race) {
// 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 {
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

0 comments on commit 9efbe4c

Please sign in to comment.