diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index 2236b4ab9..ed52f2004 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -7188,6 +7188,21 @@ CHANGE COLUMN `field221` `caster_requirement_id` int(11) NULL DEFAULT 0 AFTER `n CHANGE COLUMN `field222` `spell_class` int(11) NULL DEFAULT 0 AFTER `caster_requirement_id`, CHANGE COLUMN `field223` `spell_subclass` int(11) NULL DEFAULT 0 AFTER `spell_class`, CHANGE COLUMN `field232` `no_remove` int(11) NOT NULL DEFAULT 0 AFTER `min_range`; +)" + }, + ManifestEntry{ + .version = 9329, + .description = "2025_08_20_character_stat_caps.sql", + .check = "SHOW TABLES LIKE 'character_stat_caps'", + .condition = "empty", + .match = "", + .sql = R"( +CREATE TABLE `character_stat_caps` ( + `character_id` int(11) UNSIGNED NOT NULL, + `stat_id` tinyint(3) UNSIGNED NULL, + `stat_cap` int(11) NOT NULL DEFAULT -1, + PRIMARY KEY (`character_id`, `stat_id`) +) )" } diff --git a/common/database/database_update_manifest_bots.cpp b/common/database/database_update_manifest_bots.cpp index c2da0fd70..49d8e76e5 100644 --- a/common/database/database_update_manifest_bots.cpp +++ b/common/database/database_update_manifest_bots.cpp @@ -204,11 +204,11 @@ INNER JOIN bot_stances bs ON bd.bot_id = bs.bot_id WHERE bd.follow_distance != 184 GROUP BY bd.bot_id; -INSERT INTO bot_settings +INSERT INTO bot_settings SELECT 0, bd.bot_id, bs.stance_id, 3, 0, bd.stop_melee_level, 'BaseSetting', 'StopMeleeLevel' FROM bot_data bd INNER JOIN bot_stances bs ON bd.bot_id = bs.bot_id -WHERE (CASE +WHERE (CASE WHEN bd.class IN (2, 6, 10, 11, 12, 13, 14) THEN 13 ELSE 255 END) != bd.stop_melee_level @@ -532,7 +532,7 @@ UPDATE bot_spells_entries SET `type` = 17 WHERE `spell_id` = 3229; .condition = "empty", .match = "", .sql = R"( -INSERT INTO `bot_spells_entries` (`npc_spells_id`, `spell_id`, `type`, `minlevel`, `maxlevel`) +INSERT INTO `bot_spells_entries` (`npc_spells_id`, `spell_id`, `type`, `minlevel`, `maxlevel`) VALUES (3006, 9957, 100, 20, 254), (3006, 9956, 100, 20, 254), @@ -1106,7 +1106,7 @@ FROM bot_spells_entries WHERE `npc_spells_id` = 3005 AND `type` = 10; -INSERT INTO `bot_spells_entries` (`npc_spells_id`, `spell_id`, `type`, `minlevel`, `maxlevel`, `priority`) +INSERT INTO `bot_spells_entries` (`npc_spells_id`, `spell_id`, `type`, `minlevel`, `maxlevel`, `priority`) VALUES (3003, 10173, 24, 72, 76, 3), (3003, 10174, 24, 72, 76, 2), @@ -2122,7 +2122,21 @@ WHERE NOT EXISTS FROM spells_new WHERE bot_spells_entries.spell_id = spells_new.id); )", - } + }, + ManifestEntry{ + .version = 9055, + .description = "08_20_2025_bot_stat_caps.sql", + .check = "SHOW TABLES LIKE 'bot_stat_caps'", + .condition = "empty", + .match = "", + .sql = R"( +CREATE TABLE `bot_stat_caps` ( + `bot_id` int(11) UNSIGNED NOT NULL, + `stat_id` tinyint(3) UNSIGNED NULL, + `stat_cap` int(11) NOT NULL DEFAULT -1, + PRIMARY KEY (`bot_id`, `stat_id`) +)" + }, // -- template; copy/paste this when you need to create a new entry // ManifestEntry{ // .version = 9228, diff --git a/common/database_schema.h b/common/database_schema.h index de6f8472f..f4f3644fc 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -69,6 +69,7 @@ namespace DatabaseSchema { {"character_potionbelt", "id"}, {"character_skills", "id"}, {"character_spells", "id"}, + {"character_stat_caps", "character_id"}, {"character_stats_record", "character_id"}, {"character_task_timers", "character_id"}, {"character_tasks", "charid"}, @@ -144,6 +145,7 @@ namespace DatabaseSchema { "character_potionbelt", "character_skills", "character_spells", + "character_stat_caps", "character_stats_record", "character_task_timers", "character_tasks", diff --git a/common/emu_constants.h b/common/emu_constants.h index 0c762ffb8..04355c78f 100644 --- a/common/emu_constants.h +++ b/common/emu_constants.h @@ -919,4 +919,29 @@ namespace PetType { bool IsValid(uint8 pet_type); } +namespace StatCap { + constexpr uint8 Accuracy = 0; + constexpr uint8 Attack = 1; + constexpr uint8 Avoidance = 2; + constexpr uint8 Clairvoyance = 3; + constexpr uint8 CombatEffects = 4; + constexpr uint8 DamageShield = 5; + constexpr uint8 DOTShielding = 6; + constexpr uint8 DSMitigation = 7; + constexpr uint8 EnduranceRegen = 8; + constexpr uint8 ExtraDamage = 9; + constexpr uint8 Haste = 10; + constexpr uint8 HasteV3 = 11; + constexpr uint8 HealAmount = 12; + constexpr uint8 HealthRegen = 13; + constexpr uint8 ManaRegen = 14; + constexpr uint8 QuiverHaste = 15; + constexpr uint8 Shielding = 16; + constexpr uint8 SpellDamage = 17; + constexpr uint8 SpellShielding = 18; + constexpr uint8 Stat = 19; + constexpr uint8 Strikethrough = 20; + constexpr uint8 StunResist = 21; +} + #endif /*COMMON_EMU_CONSTANTS_H*/ diff --git a/common/repositories/base/base_bot_stat_caps_repository.h b/common/repositories/base/base_bot_stat_caps_repository.h new file mode 100644 index 000000000..117aa6e55 --- /dev/null +++ b/common/repositories/base/base_bot_stat_caps_repository.h @@ -0,0 +1,404 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://docs.eqemu.io/developer/repositories + */ + +#ifndef EQEMU_BASE_BOT_STAT_CAPS_REPOSITORY_H +#define EQEMU_BASE_BOT_STAT_CAPS_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include + +class BaseBotStatCapsRepository { +public: + struct BotStatCaps { + uint32_t bot_id; + uint8_t stat_id; + int32_t stat_cap; + }; + + static std::string PrimaryKey() + { + return std::string("bot_id"); + } + + static std::vector Columns() + { + return { + "bot_id", + "stat_id", + "stat_cap", + }; + } + + static std::vector SelectColumns() + { + return { + "bot_id", + "stat_id", + "stat_cap", + }; + } + + static std::string ColumnsRaw() + { + return std::string(Strings::Implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(Strings::Implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("bot_stat_caps"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static BotStatCaps NewEntity() + { + BotStatCaps e{}; + + e.bot_id = 0; + e.stat_id = 0; + e.stat_cap = -1; + + return e; + } + + static BotStatCaps GetBotStatCaps( + const std::vector &bot_stat_capss, + int bot_stat_caps_id + ) + { + for (auto &bot_stat_caps : bot_stat_capss) { + if (bot_stat_caps.bot_id == bot_stat_caps_id) { + return bot_stat_caps; + } + } + + return NewEntity(); + } + + static BotStatCaps FindOne( + Database& db, + int bot_stat_caps_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + bot_stat_caps_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + BotStatCaps e{}; + + e.bot_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.stat_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.stat_cap = row[2] ? static_cast(atoi(row[2])) : -1; + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int bot_stat_caps_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + bot_stat_caps_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const BotStatCaps &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[0] + " = " + std::to_string(e.bot_id)); + v.push_back(columns[1] + " = " + std::to_string(e.stat_id)); + v.push_back(columns[2] + " = " + std::to_string(e.stat_cap)); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.bot_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static BotStatCaps InsertOne( + Database& db, + BotStatCaps e + ) + { + std::vector v; + + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.stat_id)); + v.push_back(std::to_string(e.stat_cap)); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + Strings::Implode(",", v) + ) + ); + + if (results.Success()) { + e.bot_id = results.LastInsertedID(); + return e; + } + + e = NewEntity(); + + return e; + } + + static int InsertMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.stat_id)); + v.push_back(std::to_string(e.stat_cap)); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BotStatCaps e{}; + + e.bot_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.stat_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.stat_cap = row[2] ? static_cast(atoi(row[2])) : -1; + + all_entries.push_back(e); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, const std::string &where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BotStatCaps e{}; + + e.bot_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.stat_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.stat_cap = row[2] ? static_cast(atoi(row[2])) : -1; + + all_entries.push_back(e); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, const std::string &where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int64 GetMaxId(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COALESCE(MAX({}), 0) FROM {}", + PrimaryKey(), + TableName() + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static int64 Count(Database& db, const std::string &where_filter = "") + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COUNT(*) FROM {} {}", + TableName(), + (where_filter.empty() ? "" : "WHERE " + where_filter) + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static std::string BaseReplace() + { + return fmt::format( + "REPLACE INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static int ReplaceOne( + Database& db, + const BotStatCaps &e + ) + { + std::vector v; + + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.stat_id)); + v.push_back(std::to_string(e.stat_cap)); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseReplace(), + Strings::Implode(",", v) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int ReplaceMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.stat_id)); + v.push_back(std::to_string(e.stat_cap)); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseReplace(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } +}; + +#endif //EQEMU_BASE_BOT_STAT_CAPS_REPOSITORY_H diff --git a/common/repositories/base/base_character_stat_caps_repository.h b/common/repositories/base/base_character_stat_caps_repository.h new file mode 100644 index 000000000..d639a7e09 --- /dev/null +++ b/common/repositories/base/base_character_stat_caps_repository.h @@ -0,0 +1,404 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://docs.eqemu.io/developer/repositories + */ + +#ifndef EQEMU_BASE_CHARACTER_STAT_CAPS_REPOSITORY_H +#define EQEMU_BASE_CHARACTER_STAT_CAPS_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include + +class BaseCharacterStatCapsRepository { +public: + struct CharacterStatCaps { + uint32_t character_id; + uint8_t stat_id; + int32_t stat_cap; + }; + + static std::string PrimaryKey() + { + return std::string("character_id"); + } + + static std::vector Columns() + { + return { + "character_id", + "stat_id", + "stat_cap", + }; + } + + static std::vector SelectColumns() + { + return { + "character_id", + "stat_id", + "stat_cap", + }; + } + + static std::string ColumnsRaw() + { + return std::string(Strings::Implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(Strings::Implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("character_stat_caps"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static CharacterStatCaps NewEntity() + { + CharacterStatCaps e{}; + + e.character_id = 0; + e.stat_id = 0; + e.stat_cap = -1; + + return e; + } + + static CharacterStatCaps GetCharacterStatCaps( + const std::vector &character_stat_capss, + int character_stat_caps_id + ) + { + for (auto &character_stat_caps : character_stat_capss) { + if (character_stat_caps.character_id == character_stat_caps_id) { + return character_stat_caps; + } + } + + return NewEntity(); + } + + static CharacterStatCaps FindOne( + Database& db, + int character_stat_caps_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + character_stat_caps_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + CharacterStatCaps e{}; + + e.character_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.stat_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.stat_cap = row[2] ? static_cast(atoi(row[2])) : -1; + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int character_stat_caps_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + character_stat_caps_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const CharacterStatCaps &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[0] + " = " + std::to_string(e.character_id)); + v.push_back(columns[1] + " = " + std::to_string(e.stat_id)); + v.push_back(columns[2] + " = " + std::to_string(e.stat_cap)); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.character_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static CharacterStatCaps InsertOne( + Database& db, + CharacterStatCaps e + ) + { + std::vector v; + + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.stat_id)); + v.push_back(std::to_string(e.stat_cap)); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + Strings::Implode(",", v) + ) + ); + + if (results.Success()) { + e.character_id = results.LastInsertedID(); + return e; + } + + e = NewEntity(); + + return e; + } + + static int InsertMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.stat_id)); + v.push_back(std::to_string(e.stat_cap)); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + CharacterStatCaps e{}; + + e.character_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.stat_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.stat_cap = row[2] ? static_cast(atoi(row[2])) : -1; + + all_entries.push_back(e); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, const std::string &where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + CharacterStatCaps e{}; + + e.character_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.stat_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.stat_cap = row[2] ? static_cast(atoi(row[2])) : -1; + + all_entries.push_back(e); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, const std::string &where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int64 GetMaxId(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COALESCE(MAX({}), 0) FROM {}", + PrimaryKey(), + TableName() + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static int64 Count(Database& db, const std::string &where_filter = "") + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COUNT(*) FROM {} {}", + TableName(), + (where_filter.empty() ? "" : "WHERE " + where_filter) + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static std::string BaseReplace() + { + return fmt::format( + "REPLACE INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static int ReplaceOne( + Database& db, + const CharacterStatCaps &e + ) + { + std::vector v; + + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.stat_id)); + v.push_back(std::to_string(e.stat_cap)); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseReplace(), + Strings::Implode(",", v) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int ReplaceMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.stat_id)); + v.push_back(std::to_string(e.stat_cap)); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseReplace(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } +}; + +#endif //EQEMU_BASE_CHARACTER_STAT_CAPS_REPOSITORY_H diff --git a/common/repositories/bot_stat_caps_repository.h b/common/repositories/bot_stat_caps_repository.h new file mode 100644 index 000000000..c50951418 --- /dev/null +++ b/common/repositories/bot_stat_caps_repository.h @@ -0,0 +1,50 @@ +#ifndef EQEMU_BOT_STAT_CAPS_REPOSITORY_H +#define EQEMU_BOT_STAT_CAPS_REPOSITORY_H + +#include "../database.h" +#include "../strings.h" +#include "base/base_bot_stat_caps_repository.h" + +class BotStatCapsRepository: public BaseBotStatCapsRepository { +public: + + /** + * This file was auto generated and can be modified and extended upon + * + * Base repository methods are automatically + * generated in the "base" version of this repository. The base repository + * is immutable and to be left untouched, while methods in this class + * are used as extension methods for more specific persistence-layer + * accessors or mutators. + * + * Base Methods (Subject to be expanded upon in time) + * + * Note: Not all tables are designed appropriately to fit functionality with all base methods + * + * InsertOne + * UpdateOne + * DeleteOne + * FindOne + * GetWhere(std::string where_filter) + * DeleteWhere(std::string where_filter) + * InsertMany + * All + * + * Example custom methods in a repository + * + * BotStatCapsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * BotStatCapsRepository::GetWhereNeverExpires() + * BotStatCapsRepository::GetWhereXAndY() + * BotStatCapsRepository::DeleteWhereXAndY() + * + * Most of the above could be covered by base methods, but if you as a developer + * find yourself re-using logic for other parts of the code, its best to just make a + * method that can be re-used easily elsewhere especially if it can use a base repository + * method and encapsulate filters there + */ + + // Custom extended repository methods here + +}; + +#endif //EQEMU_BOT_STAT_CAPS_REPOSITORY_H diff --git a/common/repositories/character_stat_caps_repository.h b/common/repositories/character_stat_caps_repository.h new file mode 100644 index 000000000..7229146e1 --- /dev/null +++ b/common/repositories/character_stat_caps_repository.h @@ -0,0 +1,50 @@ +#ifndef EQEMU_CHARACTER_STAT_CAPS_REPOSITORY_H +#define EQEMU_CHARACTER_STAT_CAPS_REPOSITORY_H + +#include "../database.h" +#include "../strings.h" +#include "base/base_character_stat_caps_repository.h" + +class CharacterStatCapsRepository: public BaseCharacterStatCapsRepository { +public: + + /** + * This file was auto generated and can be modified and extended upon + * + * Base repository methods are automatically + * generated in the "base" version of this repository. The base repository + * is immutable and to be left untouched, while methods in this class + * are used as extension methods for more specific persistence-layer + * accessors or mutators. + * + * Base Methods (Subject to be expanded upon in time) + * + * Note: Not all tables are designed appropriately to fit functionality with all base methods + * + * InsertOne + * UpdateOne + * DeleteOne + * FindOne + * GetWhere(std::string where_filter) + * DeleteWhere(std::string where_filter) + * InsertMany + * All + * + * Example custom methods in a repository + * + * CharacterStatCapsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * CharacterStatCapsRepository::GetWhereNeverExpires() + * CharacterStatCapsRepository::GetWhereXAndY() + * CharacterStatCapsRepository::DeleteWhereXAndY() + * + * Most of the above could be covered by base methods, but if you as a developer + * find yourself re-using logic for other parts of the code, its best to just make a + * method that can be re-used easily elsewhere especially if it can use a base repository + * method and encapsulate filters there + */ + + // Custom extended repository methods here + +}; + +#endif //EQEMU_CHARACTER_STAT_CAPS_REPOSITORY_H diff --git a/common/version.h b/common/version.h index af74568c6..3c3b0fdb6 100644 --- a/common/version.h +++ b/common/version.h @@ -42,8 +42,8 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9328 -#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054 +#define CURRENT_BINARY_DATABASE_VERSION 9329 +#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9055 #define CUSTOM_BINARY_DATABASE_VERSION 0 #endif diff --git a/zone/attack.cpp b/zone/attack.cpp index f5552c47c..59b1a65c9 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -6701,7 +6701,7 @@ void Client::SetAttackTimer() if (ItemToUse && ItemToUse->ItemType == EQ::item::ItemTypeBow) { // Live actually had a bug here where they would return the non-modified attack speed // rather than the cap ... - speed = std::max(speed - GetQuiverHaste(speed), RuleI(Combat, QuiverHasteCap)); + speed = std::max(speed - GetQuiverHaste(speed), GetStatCap(StatCap::QuiverHaste)); } else { if (RuleB(Spells, Jun182014HundredHandsRevamp)) diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index 0cc44cac3..7df4e3ded 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -246,12 +246,22 @@ void Mob::ProcessItemCaps() itembonuses.ATK = std::min(itembonuses.ATK, CalcItemATKCap()); - if (IsOfClientBotMerc() && itembonuses.SpellDmg > RuleI(Character, ItemSpellDmgCap)) { - itembonuses.SpellDmg = RuleI(Character, ItemSpellDmgCap); + int spell_damage_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::SpellDamage) : + RuleI(Character, ItemSpellDmgCap) + ); + if (IsOfClientBotMerc() && itembonuses.SpellDmg > spell_damage_cap) { + itembonuses.SpellDmg = spell_damage_cap; } - if (IsOfClientBotMerc() && itembonuses.HealAmt > RuleI(Character, ItemHealAmtCap)) { - itembonuses.HealAmt = RuleI(Character, ItemHealAmtCap); + int heal_amount_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::HealAmount) : + RuleI(Character, ItemHealAmtCap) + ); + if (IsOfClientBotMerc() && itembonuses.HealAmt > heal_amount_cap) { + itembonuses.HealAmt = heal_amount_cap; } } @@ -351,33 +361,139 @@ void Mob::AddItemBonuses(const EQ::ItemInstance* inst, StatBonuses* b, bool is_a b->ManaRegen += CalcItemBonus(item->ManaRegen); b->EnduranceRegen += CalcItemBonus(item->EnduranceRegen); + int accuracy_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Accuracy) : + RuleI(Character, ItemAccuracyCap) + ); + + int attack_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Attack) : + RuleI(Character, ItemATKCap) + ); + + int avoidance_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Avoidance) : + RuleI(Character, ItemAvoidanceCap) + ); + + int clairvoyance_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Clairvoyance) : + RuleI(Character, ItemClairvoyanceCap) + ); + + int combat_effects_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::CombatEffects) : + RuleI(Character, ItemCombatEffectsCap) + ); + + int damage_shield_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::DamageShield) : + RuleI(Character, ItemDamageShieldCap) + ); + + int dot_shielding_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::DOTShielding) : + RuleI(Character, ItemDoTShieldingCap) + ); + + int dsmitigation_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::DSMitigation) : + RuleI(Character, ItemDSMitigationCap) + ); + + int heal_amount_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::HealAmount) : + RuleI(Character, ItemHealAmtCap) + ); + + int shielding_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Shielding) : + RuleI(Character, ItemShieldingCap) + ); + + int spell_damage_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::SpellDamage) : + RuleI(Character, ItemSpellDmgCap) + ); + + int spell_shielding_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::SpellShielding) : + RuleI(Character, ItemSpellShieldingCap) + ); + + int strikethrough_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Strikethrough) : + RuleI(Character, ItemStrikethroughCap) + ); + + int stun_resist_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::StunResist) : + RuleI(Character, ItemStunResistCap) + ); + // These have rule-configured caps. - b->ATK = CalcCappedItemBonus(b->ATK, item->Attack, RuleI(Character, ItemATKCap) + itembonuses.ItemATKCap + spellbonuses.ItemATKCap + aabonuses.ItemATKCap); - b->DamageShield = CalcCappedItemBonus(b->DamageShield, item->DamageShield, RuleI(Character, ItemDamageShieldCap)); - b->SpellShield = CalcCappedItemBonus(b->SpellShield, item->SpellShield, RuleI(Character, ItemSpellShieldingCap)); - b->MeleeMitigation = CalcCappedItemBonus(b->MeleeMitigation, item->Shielding, RuleI(Character, ItemShieldingCap)); - b->StunResist = CalcCappedItemBonus(b->StunResist, item->StunResist, RuleI(Character, ItemStunResistCap)); - b->StrikeThrough = CalcCappedItemBonus(b->StrikeThrough, item->StrikeThrough, RuleI(Character, ItemStrikethroughCap)); - b->AvoidMeleeChance = CalcCappedItemBonus(b->AvoidMeleeChance, item->Avoidance, RuleI(Character, ItemAvoidanceCap)); - b->HitChance = CalcCappedItemBonus(b->HitChance, item->Accuracy, RuleI(Character, ItemAccuracyCap)); - b->ProcChance = CalcCappedItemBonus(b->ProcChance, item->CombatEffects, RuleI(Character, ItemCombatEffectsCap)); - b->DoTShielding = CalcCappedItemBonus(b->DoTShielding, item->DotShielding, RuleI(Character, ItemDoTShieldingCap)); - b->HealAmt = CalcCappedItemBonus(b->HealAmt, item->HealAmt, RuleI(Character, ItemHealAmtCap)); - b->SpellDmg = CalcCappedItemBonus(b->SpellDmg, item->SpellDmg, RuleI(Character, ItemSpellDmgCap)); - b->Clairvoyance = CalcCappedItemBonus(b->Clairvoyance, item->Clairvoyance, RuleI(Character, ItemClairvoyanceCap)); - b->DSMitigation = CalcCappedItemBonus(b->DSMitigation, item->DSMitigation, RuleI(Character, ItemDSMitigationCap)); + b->ATK = CalcCappedItemBonus( + b->ATK, + item->Attack, + ( + attack_cap + + itembonuses.ItemATKCap + + spellbonuses.ItemATKCap + + aabonuses.ItemATKCap + ) + ); + b->DamageShield = CalcCappedItemBonus(b->DamageShield, item->DamageShield, damage_shield_cap); + b->SpellShield = CalcCappedItemBonus(b->SpellShield, item->SpellShield, spell_shielding_cap); + b->MeleeMitigation = CalcCappedItemBonus(b->MeleeMitigation, item->Shielding, shielding_cap); + b->StunResist = CalcCappedItemBonus(b->StunResist, item->StunResist, stun_resist_cap); + b->StrikeThrough = CalcCappedItemBonus(b->StrikeThrough, item->StrikeThrough, strikethrough_cap); + b->AvoidMeleeChance = CalcCappedItemBonus(b->AvoidMeleeChance, item->Avoidance, avoidance_cap); + b->HitChance = CalcCappedItemBonus(b->HitChance, item->Accuracy, accuracy_cap); + b->ProcChance = CalcCappedItemBonus(b->ProcChance, item->CombatEffects, combat_effects_cap); + b->DoTShielding = CalcCappedItemBonus(b->DoTShielding, item->DotShielding, dot_shielding_cap); + b->HealAmt = CalcCappedItemBonus(b->HealAmt, item->HealAmt, heal_amount_cap); + b->SpellDmg = CalcCappedItemBonus(b->SpellDmg, item->SpellDmg, spell_damage_cap); + b->Clairvoyance = CalcCappedItemBonus(b->Clairvoyance, item->Clairvoyance, clairvoyance_cap); + b->DSMitigation = CalcCappedItemBonus(b->DSMitigation, item->DSMitigation, dsmitigation_cap); if (b->haste < item->Haste) { b->haste = item->Haste; } if (item->ExtraDmgAmt != 0 && item->ExtraDmgSkill <= EQ::skills::HIGHEST_SKILL) { + int extra_damage_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::ExtraDamage) : + RuleI(Character, ItemExtraDmgCap) + ); if (item->ExtraDmgSkill == ALL_SKILLS) { for (const auto &skill_id: EQ::skills::GetExtraDamageSkills()) { - b->SkillDamageAmount[skill_id] = CalcCappedItemBonus(b->SkillDamageAmount[skill_id], item->ExtraDmgAmt, RuleI(Character, ItemExtraDmgCap)); + b->SkillDamageAmount[skill_id] = CalcCappedItemBonus( + b->SkillDamageAmount[skill_id], + item->ExtraDmgAmt, + extra_damage_cap + ); } } else { - b->SkillDamageAmount[item->ExtraDmgSkill] = CalcCappedItemBonus(b->SkillDamageAmount[item->ExtraDmgSkill], item->ExtraDmgAmt, RuleI(Character, ItemExtraDmgCap)); + b->SkillDamageAmount[item->ExtraDmgSkill] = CalcCappedItemBonus( + b->SkillDamageAmount[item->ExtraDmgSkill], + item->ExtraDmgAmt, + extra_damage_cap + ); } } diff --git a/zone/bot.cpp b/zone/bot.cpp index 1aeb0c09a..dbfb9d105 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -268,6 +268,8 @@ Bot::Bot( LoadDefaultBotSettings(); database.botdb.LoadBotSettings(this); + database.LoadStatCaps(this); + if (RuleB(Bots, AllowBotBlockedBuffs)) { bot_blocked_buffs.clear(); database.botdb.LoadBotBlockedBuffs(this); @@ -1420,6 +1422,7 @@ bool Bot::Save() database.botdb.SaveTimers(this); database.botdb.SaveStance(this); database.botdb.SaveBotSettings(this); + database.SaveStatCaps(this); if (RuleB(Bots, AllowBotBlockedBuffs)) { database.botdb.SaveBotBlockedBuffs(this); diff --git a/zone/bot.h b/zone/bot.h index 434e8f703..d760bd46b 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -101,7 +101,7 @@ namespace BotSettingCategories { constexpr uint8 SpellTypeMinManaPct = 7; constexpr uint8 SpellTypeMaxManaPct = 8; constexpr uint8 SpellTypeMinHPPct = 9; - constexpr uint8 SpellTypeMaxHPPct = 10; + constexpr uint8 SpellTypeMaxHPPct = 10; constexpr uint8 SpellTypeIdlePriority = 11; constexpr uint8 SpellTypeEngagedPriority = 12; constexpr uint8 SpellTypePursuePriority = 13; @@ -166,7 +166,7 @@ namespace BotBaseSettings { constexpr uint16 ExpansionBitmask = 0; constexpr uint16 ShowHelm = 1; constexpr uint16 FollowDistance = 2; - constexpr uint16 StopMeleeLevel = 3; + constexpr uint16 StopMeleeLevel = 3; constexpr uint16 EnforceSpellSettings = 4; constexpr uint16 RangedSetting = 5; constexpr uint16 PetSetTypeSetting = 6; @@ -487,7 +487,7 @@ public: void SetCommandedSpell(bool value) { _commandedSpell = value; } bool IsPullingSpell() const { return _pullingSpell; } void SetPullingSpell(bool value) { _pullingSpell = value; } - + void SetGuardMode(); void SetHoldMode(); @@ -550,7 +550,7 @@ public: bool IsValidMezTarget(Mob* owner, Mob* npc, uint16 spell_id); // Cast checks - bool PrecastChecks(Mob* tar, uint16 spell_type); + bool PrecastChecks(Mob* tar, uint16 spell_type); bool CastChecks(uint16 spell_id, Mob* tar, uint16 spell_type, bool prechecks = false, bool ae_check = false); bool IsImmuneToBotSpell(uint16 spell_id, Mob* caster); bool CanCastSpellType(uint16 spell_type, uint16 spell_id, Mob* tar); @@ -601,7 +601,7 @@ public: void ResetBotSpellSettings(); void CopyBotBlockedBuffs(Bot* to); - void CopyBotBlockedPetBuffs(Bot* to); + void CopyBotBlockedPetBuffs(Bot* to); void CleanBotBlockedBuffs(); void ClearBotBlockedBuffs() { bot_blocked_buffs.clear(); } bool IsBlockedBuff(int32 spell_id) override; @@ -658,7 +658,7 @@ public: bool GetBehindMob() const { return _behindMobStatus; } void SetBehindMob(bool value) { _behindMobStatus = value; } bool GetMaxMeleeRange() const { return _maxMeleeRangeStatus; } - void SetMaxMeleeRange(bool value) { _maxMeleeRangeStatus = value; } + void SetMaxMeleeRange(bool value) { _maxMeleeRangeStatus = value; } uint8 GetStopMeleeLevel() const { return _stopMeleeLevel; } void SetStopMeleeLevel(uint8 level) { _stopMeleeLevel = level; } uint32 GetBotDistanceRanged() const { return _distanceRanged; } @@ -1210,7 +1210,6 @@ private: bool _hasLoS; bool _commandedSpell; bool _pullingSpell; - bool _illusionBlock; std::vector m_bot_spell_settings; std::vector _spell_target_list; diff --git a/zone/client.cpp b/zone/client.cpp index 5ed9eddec..17e127aae 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -1103,6 +1103,8 @@ bool Client::Save(uint8 iCommitNow) { database.SaveCharacterEXPModifier(this); + database.SaveStatCaps(this); + if (RuleB(Bots, Enabled)) { database.botdb.SaveBotSettings(this); } diff --git a/zone/client_mods.cpp b/zone/client_mods.cpp index 6c7026185..07f318bc3 100644 --- a/zone/client_mods.cpp +++ b/zone/client_mods.cpp @@ -32,23 +32,24 @@ int32 Client::GetMaxStat() const { - if ((RuleI(Character, StatCap)) > 0) { - return (RuleI(Character, StatCap)); + int character_cap = GetStatCap(StatCap::Stat); + if (character_cap > 0) { + return character_cap; } - int level = GetLevel(); - int32 base = 0; + + uint8 level = GetLevel(); + int base = 0; + if (level < 61) { base = 255; - } - else if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { + } else if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { base = 255 + 5 * (level - 60); - } - else if (level < 71) { + } else if (level < 71) { base = 255 + 5 * (level - 60); - } - else { + } else { base = 330; } + return base; } @@ -300,7 +301,7 @@ int64 Client::CalcHPRegen(bool bCombat) int64 Client::CalcHPRegenCap() { - int64 cap = RuleI(Character, ItemHealthRegenCap); + int64 cap = GetStatCap(StatCap::HealthRegen); if (GetLevel() > 60) { cap = std::max(cap, static_cast(GetLevel() - 30)); // if the rule is set greater than normal I guess } @@ -717,7 +718,7 @@ int64 Client::CalcManaRegen(bool bCombat) int64 Client::CalcManaRegenCap() { - int64 cap = RuleI(Character, ItemManaRegenCap) + aabonuses.ItemManaRegenCap + itembonuses.ItemManaRegenCap + spellbonuses.ItemManaRegenCap; + int64 cap = GetStatCap(StatCap::ManaRegen) + aabonuses.ItemManaRegenCap + itembonuses.ItemManaRegenCap + spellbonuses.ItemManaRegenCap; return (cap * RuleI(Character, ManaRegenMultiplier) / 100); } @@ -976,7 +977,7 @@ int Client::CalcHaste() } // 60+ 100, 51-59 85, 1-50 level+25 if (level > 59 || RuleB(Character, IgnoreLevelBasedHasteCaps)) { // 60+ - cap = RuleI(Character, HasteCap); + cap = GetStatCap(StatCap::Haste); } else if (level > 50) { // 51-59 cap = 85; @@ -989,7 +990,7 @@ int Client::CalcHaste() } // 51+ 25 (despite there being higher spells...), 1-50 10 if (level > 50 || RuleB(Character, IgnoreLevelBasedHasteCaps)) { // 51+ - cap = RuleI(Character, Hastev3Cap); + cap = GetStatCap(StatCap::HasteV3); if (spellbonuses.hastetype3 > cap) { h += cap; } else { @@ -1745,13 +1746,13 @@ int64 Client::CalcEnduranceRegen(bool bCombat) int64 Client::CalcEnduranceRegenCap() { - int64 cap = RuleI(Character, ItemEnduranceRegenCap) + aabonuses.ItemEnduranceRegenCap + itembonuses.ItemEnduranceRegenCap + spellbonuses.ItemEnduranceRegenCap; + int64 cap = GetStatCap(StatCap::EnduranceRegen) + aabonuses.ItemEnduranceRegenCap + itembonuses.ItemEnduranceRegenCap + spellbonuses.ItemEnduranceRegenCap; return (cap * RuleI(Character, EnduranceRegenMultiplier) / 100); } int32 Client::CalcItemATKCap() { - int cap = RuleI(Character, ItemATKCap) + itembonuses.ItemATKCap + spellbonuses.ItemATKCap + aabonuses.ItemATKCap; + int cap = GetStatCap(StatCap::Attack) + itembonuses.ItemATKCap + spellbonuses.ItemATKCap + aabonuses.ItemATKCap; return cap; } diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 42f0b8bbe..628af3f80 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -1386,6 +1386,7 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app) database.LoadCharacterTribute(this); /* Load CharacterTribute */ database.LoadCharacterEXPModifier(this); /* Load Character EXP Modifier */ database.LoadCharacterTitleSets(this); /* Load Character Title Sets */ + database.LoadStatCaps(this); /* Load Character Stat Caps */ // this pattern is strange // this is remnants of the old way of doing things diff --git a/zone/lua_bot.cpp b/zone/lua_bot.cpp index 1e537d76a..5ce4144d4 100644 --- a/zone/lua_bot.cpp +++ b/zone/lua_bot.cpp @@ -664,6 +664,18 @@ void Lua_Bot::RaidGroupSay(const char* message) { self->RaidGroupSay(message); } +int Lua_Bot::GetStatCap(uint8 stat_id) +{ + Lua_Safe_Call_Int(); + return self->GetStatCap(stat_id); +} + +void Lua_Bot::SetStatCap(uint8 stat_id, int stat_cap) +{ + Lua_Safe_Call_Void(); + self->SetStatCap(stat_id, stat_cap, true); +} + luabind::scope lua_register_bot() { return luabind::class_("Bot") .def(luabind::constructor<>()) @@ -747,6 +759,7 @@ luabind::scope lua_register_bot() { .def("GetSpellDamage", (int(Lua_Bot::*)(void))&Lua_Bot::GetSpellDamage) .def("GetSpellRecastTimer", (uint32(Lua_Bot::*)())&Lua_Bot::GetSpellRecastTimer) .def("GetSpellRecastTimer", (uint32(Lua_Bot::*)(uint16))&Lua_Bot::GetSpellRecastTimer) + .def("GetStatCap", (int(Lua_Bot::*)(uint8))&Lua_Bot::GetStatCap) .def("HasAugmentEquippedByID", (bool(Lua_Bot::*)(uint32))&Lua_Bot::HasAugmentEquippedByID) .def("HasBotItem", (int16(Lua_Bot::*)(uint32))&Lua_Bot::HasBotItem) .def("HasBotSpellEntry", (bool(Lua_Bot::*)(uint16))&Lua_Bot::HasBotSpellEntry) @@ -783,6 +796,7 @@ luabind::scope lua_register_bot() { .def("SetSpellDurationRaid", (void(Lua_Bot::*)(int,int,int,bool,bool))&Lua_Bot::SetSpellDurationRaid) .def("SetSpellRecastTimer", (void(Lua_Bot::*)(uint16))&Lua_Bot::SetSpellRecastTimer) .def("SetSpellRecastTimer", (void(Lua_Bot::*)(uint16, uint32))&Lua_Bot::SetSpellRecastTimer) + .def("SetStatCap", (void(Lua_Bot::*)(uint8,int))&Lua_Bot::SetStatCap) .def("SendPayload", (void(Lua_Bot::*)(int))&Lua_Bot::SendPayload) .def("SendPayload", (void(Lua_Bot::*)(int,std::string))&Lua_Bot::SendPayload) .def("Signal", (void(Lua_Bot::*)(int))&Lua_Bot::Signal) diff --git a/zone/lua_bot.h b/zone/lua_bot.h index 86e3ca28b..eaa6b66ec 100644 --- a/zone/lua_bot.h +++ b/zone/lua_bot.h @@ -126,6 +126,8 @@ public: void SetItemReuseTimer(uint32 item_id, uint32 reuse_timer); void SetSpellRecastTimer(uint16 spell_id); void SetSpellRecastTimer(uint16 spell_id, uint32 reuse_timer); + int GetStatCap(uint8 stat_id); + void SetStatCap(uint8 stat_id, int stat_cap); uint32 CountAugmentEquippedByID(uint32 item_id); uint32 CountItemEquippedByID(uint32 item_id); diff --git a/zone/lua_client.cpp b/zone/lua_client.cpp index 15ec526b6..f207fb68d 100644 --- a/zone/lua_client.cpp +++ b/zone/lua_client.cpp @@ -3624,6 +3624,18 @@ luabind::object Lua_Client::GetKeyRing(lua_State* L) return lua_table; } +int Lua_Client::GetStatCap(uint8 stat_id) +{ + Lua_Safe_Call_Int(); + return self->GetStatCap(stat_id); +} + +void Lua_Client::SetStatCap(uint8 stat_id, int stat_cap) +{ + Lua_Safe_Call_Void(); + self->SetStatCap(stat_id, stat_cap, true); +} + luabind::scope lua_register_client() { return luabind::class_("Client") .def(luabind::constructor<>()) @@ -3898,6 +3910,7 @@ luabind::scope lua_register_client() { .def("GetSpellDamage", (int(Lua_Client::*)(void))&Lua_Client::GetSpellDamage) .def("GetSpellIDByBookSlot", (uint32(Lua_Client::*)(int))&Lua_Client::GetSpellIDByBookSlot) .def("GetSpentAA", (int(Lua_Client::*)(void))&Lua_Client::GetSpentAA) + .def("GetStatCap", (int(Lua_Client::*)(uint8))&Lua_Client::GetStatCap) .def("GetStartZone", (int(Lua_Client::*)(void))&Lua_Client::GetStartZone) .def("GetTargetRingX", (float(Lua_Client::*)(void))&Lua_Client::GetTargetRingX) .def("GetTargetRingY", (float(Lua_Client::*)(void))&Lua_Client::GetTargetRingY) @@ -4163,6 +4176,7 @@ luabind::scope lua_register_client() { .def("SetStartZone", (void(Lua_Client::*)(int,float))&Lua_Client::SetStartZone) .def("SetStartZone", (void(Lua_Client::*)(int,float,float))&Lua_Client::SetStartZone) .def("SetStartZone", (void(Lua_Client::*)(int,float,float,float))&Lua_Client::SetStartZone) + .def("SetStatCap", (void(Lua_Client::*)(uint8,int))&Lua_Client::SetStatCap) .def("SetStats", (void(Lua_Client::*)(int,int))&Lua_Client::SetStats) .def("SetThirst", (void(Lua_Client::*)(int))&Lua_Client::SetThirst) .def("SetTint", (void(Lua_Client::*)(int,uint32))&Lua_Client::SetTint) diff --git a/zone/lua_client.h b/zone/lua_client.h index c85c061b6..758a16e11 100644 --- a/zone/lua_client.h +++ b/zone/lua_client.h @@ -607,6 +607,8 @@ public: bool RemoveAAPoints(uint32 points); bool RemoveAlternateCurrencyValue(uint32 currency_id, uint32 amount); bool AreTasksCompleted(luabind::object task_ids); + int GetStatCap(uint8 stat_id); + void SetStatCap(uint8 stat_id, int stat_cap); void DialogueWindow(std::string markdown); diff --git a/zone/mob.cpp b/zone/mob.cpp index 1302246b2..31ab72ce8 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -22,7 +22,9 @@ #include "../common/misc_functions.h" #include "../common/repositories/bot_data_repository.h" +#include "../common/repositories/bot_stat_caps_repository.h" #include "../common/repositories/character_data_repository.h" +#include "../common/repositories/character_stat_caps_repository.h" #include "../common/data_bucket.h" #include "quest_parser_collection.h" @@ -2059,10 +2061,20 @@ void Mob::SendStatsWindow(Client* c, bool use_window) for (auto mod2_row_counter = 0; mod2_row_counter < mod2_rows; mod2_row_counter++) { switch (mod2_row_counter) { case 0: { + int avoidance_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Avoidance) : + RuleI(Character, ItemAvoidanceCap) + ); + int combat_effects_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::CombatEffects) : + RuleI(Character, ItemCombatEffectsCap) + ); mod2a_name = "Avoidance"; mod2b_name = "Combat Effects"; - mod2a_cap = Strings::Commify(RuleI(Character, ItemAvoidanceCap)); - mod2b_cap = Strings::Commify(RuleI(Character, ItemCombatEffectsCap)); + mod2a_cap = Strings::Commify(avoidance_cap); + mod2b_cap = Strings::Commify(combat_effects_cap); if (IsBot()) { mod2a = Strings::Commify(CastToBot()->GetAvoidance()); @@ -2079,10 +2091,20 @@ void Mob::SendStatsWindow(Client* c, bool use_window) break; } case 1: { + int accuracy_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Accuracy) : + RuleI(Character, ItemAccuracyCap) + ); + int strikethrough_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Strikethrough) : + RuleI(Character, ItemStrikethroughCap) + ); mod2a_name = "Accuracy"; mod2b_name = "Strikethrough"; - mod2a_cap = Strings::Commify(RuleI(Character, ItemAccuracyCap)); - mod2b_cap = Strings::Commify(RuleI(Character, ItemStrikethroughCap)); + mod2a_cap = Strings::Commify(accuracy_cap); + mod2b_cap = Strings::Commify(strikethrough_cap); if (IsBot()) { mod2a = Strings::Commify(CastToBot()->GetAccuracy()); @@ -2099,10 +2121,20 @@ void Mob::SendStatsWindow(Client* c, bool use_window) break; } case 2: { + int shielding_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Shielding) : + RuleI(Character, ItemShieldingCap) + ); + int spell_shielding_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::SpellShielding) : + RuleI(Character, ItemSpellShieldingCap) + ); mod2a_name = "Shielding"; mod2b_name = "Spell Shielding"; - mod2a_cap = Strings::Commify(RuleI(Character, ItemShieldingCap)); - mod2b_cap = Strings::Commify(RuleI(Character, ItemSpellShieldingCap)); + mod2a_cap = Strings::Commify(shielding_cap); + mod2b_cap = Strings::Commify(spell_shielding_cap); if (IsBot()) { mod2a = Strings::Commify(CastToBot()->GetShielding()); @@ -2120,10 +2152,20 @@ void Mob::SendStatsWindow(Client* c, bool use_window) break; } case 3: { + int dot_shielding_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::DOTShielding) : + RuleI(Character, ItemDoTShieldingCap) + ); + int stun_resist_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::StunResist) : + RuleI(Character, ItemStunResistCap) + ); mod2a_name = "Stun Resist"; mod2b_name = "DOT Shielding"; - mod2a_cap = Strings::Commify(RuleI(Character, ItemStunResistCap)); - mod2b_cap = Strings::Commify(RuleI(Character, ItemDoTShieldingCap)); + mod2a_cap = Strings::Commify(stun_resist_cap); + mod2b_cap = Strings::Commify(dot_shielding_cap); if (IsBot()) { mod2a = Strings::Commify(CastToBot()->GetStunResist()); @@ -2359,6 +2401,11 @@ void Mob::SendStatsWindow(Client* c, bool use_window) final_string += DialogueWindow::Break(1); + int attack_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Attack) : + RuleI(Character, ItemATKCap) + ); // Attack 2 final_string += fmt::format( "Offense: {}{}{}{}", @@ -2368,7 +2415,7 @@ void Mob::SendStatsWindow(Client* c, bool use_window) fmt::format( " | Item: {} / {} | Used: {}", Strings::Commify(itembonuses.ATK), - Strings::Commify(RuleI(Character, ItemATKCap)), + Strings::Commify(attack_cap), Strings::Commify(static_cast(itembonuses.ATK * 1.342)) ) : "" @@ -2415,6 +2462,11 @@ void Mob::SendStatsWindow(Client* c, bool use_window) final_string += DialogueWindow::CenterMessage("Haste"); + int haste_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Haste) : + RuleI(Character, HasteCap) + ); // Haste Table const auto& haste_table = DialogueWindow::Table( fmt::format( @@ -2433,7 +2485,7 @@ void Mob::SendStatsWindow(Client* c, bool use_window) fmt::format( "{} ({})", IsClient() ? Strings::Commify(CastToClient()->GetHaste()) : Strings::Commify(GetHaste()), - Strings::Commify(RuleI(Character, HasteCap)) + Strings::Commify(haste_cap) ) ) ) @@ -2468,26 +2520,41 @@ void Mob::SendStatsWindow(Client* c, bool use_window) // Heal Amount if (GetHealAmt()) { + int cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::HealAmount) : + RuleI(Character, ItemHealAmtCap) + ); final_string += fmt::format( "Heal Amount: {} / {}{}", Strings::Commify(GetHealAmt()), - Strings::Commify(RuleI(Character, ItemHealAmtCap)), + Strings::Commify(cap), DialogueWindow::Break(1) ); } - // Heal Amount + // Spell Damage if (GetSpellDmg()) { + int cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::SpellDamage) : + RuleI(Character, ItemSpellDmgCap) + ); final_string += fmt::format( "Spell Damage: {} / {}{}", Strings::Commify(GetSpellDmg()), - Strings::Commify(RuleI(Character, ItemSpellDmgCap)), + Strings::Commify(cap), DialogueWindow::Break(1) ); } // Damage Shield if (itembonuses.DamageShield || spellbonuses.DamageShield) { + int cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::DamageShield) : + RuleI(Character, ItemDamageShieldCap) + ); final_string += fmt::format( "Damage Shield: {}{}{}{}", Strings::Commify(itembonuses.DamageShield + spellbonuses.DamageShield), @@ -2501,7 +2568,7 @@ void Mob::SendStatsWindow(Client* c, bool use_window) fmt::format( " | Item: {} / {}", Strings::Commify(itembonuses.DamageShield), - Strings::Commify(RuleI(Character, ItemDamageShieldCap)) + Strings::Commify(cap) ) : "" ), @@ -2510,12 +2577,17 @@ void Mob::SendStatsWindow(Client* c, bool use_window) } // Clairvoyance - const auto clairvoyance = IsBot() ? CastToBot()->GetClair() : CastToClient()->GetClair(); + const auto clairvoyance = IsBot() ? CastToBot()->GetClair() : CastToClient()->GetClair(); if (clairvoyance) { + int cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Clairvoyance) : + RuleI(Character, ItemClairvoyanceCap) + ); final_string += fmt::format( "Clairvoyance: {} / {}{}", Strings::Commify(clairvoyance), - Strings::Commify(RuleI(Character, ItemClairvoyanceCap)), + Strings::Commify(cap), DialogueWindow::Break(1) ); } @@ -2523,10 +2595,15 @@ void Mob::SendStatsWindow(Client* c, bool use_window) // Damage Shield Mitigation const auto ds_mitigation = IsBot() ? CastToBot()->GetDSMit() : CastToClient()->GetDSMit(); if (ds_mitigation) { + int cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::DSMitigation) : + RuleI(Character, ItemDSMitigationCap) + ); final_string += fmt::format( "DS Mitigation: {} / {}{}", Strings::Commify(ds_mitigation), - Strings::Commify(RuleI(Character, ItemDSMitigationCap)), + Strings::Commify(cap), DialogueWindow::Break(1) ); } @@ -2555,6 +2632,12 @@ void Mob::SendStatsWindow(Client* c, bool use_window) final_string += faction_item_string; } + int damage_shield_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::DamageShield) : + RuleI(Character, ItemDamageShieldCap) + ); + if (use_window) { if (final_string.size() < 4096) { const uint32 popup_buttons = (c->ClientVersion() < EQ::versions::ClientVersion::SoD) ? 0 : 1; @@ -2596,7 +2679,7 @@ void Mob::SendStatsWindow(Client* c, bool use_window) GetRaceIDName(GetRace()), GetRace(), IsBot() ? Strings::Commify(CastToBot()->GetDS()) : Strings::Commify(CastToClient()->GetDS()), - Strings::Commify(RuleI(Character, ItemDamageShieldCap)), + Strings::Commify(damage_shield_cap), GetSize(), GetRunspeed(), IsBot() ? static_cast(CastToBot()->CalcCurrentWeight()) / 10.0f : static_cast(CastToClient()->CalcCurrentWeight()) / 10.0f, @@ -2694,18 +2777,23 @@ void Mob::SendStatsWindow(Client* c, bool use_window) "Attack: {} Item and Spell Attack: {}/{} Server Side Attack: {}", IsBot() ? Strings::Commify(CastToBot()->GetTotalATK()) : Strings::Commify(CastToClient()->GetTotalATK()), Strings::Commify(GetATKBonus()), - Strings::Commify(RuleI(Character, ItemATKCap)), + Strings::Commify(attack_cap), Strings::Commify(GetATK()) ).c_str() ); if ((IsClient() && CastToClient()->GetHaste()) || (!IsClient() && GetHaste())) { + int haste_cap = ( + IsOfClientBot() ? + GetStatCap(StatCap::Haste) : + RuleI(Character, HasteCap) + ); c->Message( Chat::White, fmt::format( "Haste: {}/{} (Item: {} + Spell: {} + Over: {})", IsClient() ? Strings::Commify(CastToClient()->GetHaste()) : Strings::Commify(GetHaste()), - Strings::Commify(RuleI(Character, HasteCap)), + Strings::Commify(haste_cap), Strings::Commify(itembonuses.haste), Strings::Commify(spellbonuses.haste + spellbonuses.hastetype2), Strings::Commify(spellbonuses.hastetype3 + extra_haste) @@ -8793,3 +8881,72 @@ bool Mob::LoadDataBucketsCache() return true; } + +int Mob::GetStatCap(uint8 stat_id) const +{ + if (HasStatCap(stat_id)) { + return m_stat_caps.at(stat_id); + } + + static const std::map default_caps = { + { StatCap::Accuracy, RuleI(Character, ItemAccuracyCap) }, + { StatCap::Attack, RuleI(Character, ItemATKCap) }, + { StatCap::Avoidance, RuleI(Character, ItemAvoidanceCap) }, + { StatCap::Clairvoyance, RuleI(Character, ItemClairvoyanceCap) }, + { StatCap::CombatEffects, RuleI(Character, ItemCombatEffectsCap) }, + { StatCap::DamageShield, RuleI(Character, ItemDamageShieldCap) }, + { StatCap::DOTShielding, RuleI(Character, ItemDoTShieldingCap) }, + { StatCap::DSMitigation, RuleI(Character, ItemDSMitigationCap) }, + { StatCap::EnduranceRegen, RuleI(Character, ItemEnduranceRegenCap) }, + { StatCap::ExtraDamage, RuleI(Character, ItemExtraDmgCap) }, + { StatCap::Haste, RuleI(Character, HasteCap) }, + { StatCap::HasteV3, RuleI(Character, Hastev3Cap) }, + { StatCap::HealAmount, RuleI(Character, ItemHealAmtCap) }, + { StatCap::HealthRegen, RuleI(Character, ItemHealthRegenCap) }, + { StatCap::ManaRegen, RuleI(Character, ItemManaRegenCap) }, + { StatCap::QuiverHaste, RuleI(Combat, QuiverHasteCap) }, + { StatCap::Shielding, RuleI(Character, ItemShieldingCap) }, + { StatCap::SpellDamage, RuleI(Character, ItemSpellDmgCap) }, + { StatCap::SpellShielding, RuleI(Character, ItemSpellShieldingCap) }, + { StatCap::Stat, RuleI(Character, StatCap) }, + { StatCap::Strikethrough, RuleI(Character, ItemStrikethroughCap) }, + { StatCap::StunResist, RuleI(Character, ItemStunResistCap) }, + }; + + auto it = default_caps.find(stat_id); + if (it != default_caps.end()) { + return it->second; + } + + return -1; +} + +void Mob::SetStatCap(uint8 stat_id, int stat_cap, bool save) +{ + m_stat_caps[stat_id] = stat_cap; + + if (!save) { + return; + } + + if (IsBot()) { + BotStatCapsRepository::ReplaceOne( + database, + BotStatCapsRepository::BotStatCaps{ + .bot_id = CastToBot()->GetBotID(), + .stat_id = stat_id, + .stat_cap = stat_cap + } + ); + } + else if (IsClient()) { + CharacterStatCapsRepository::ReplaceOne( + database, + CharacterStatCapsRepository::CharacterStatCaps{ + .character_id = CastToClient()->CharacterID(), + .stat_id = stat_id, + .stat_cap = stat_cap + } + ); + } +} diff --git a/zone/mob.h b/zone/mob.h index ca0abe268..3411f8826 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -1519,6 +1519,11 @@ public: bool IsGuildmaster() const; bool IsDestroying() const { return m_destroying; } + int GetStatCap(uint8 stat_id) const; + std::map GetStatCaps() { return m_stat_caps; } + bool HasStatCap(uint8 stat_id) const { return m_stat_caps.count(stat_id); } + void SetStatCap(uint8 stat_id, int stat_cap, bool save = false); + protected: void CommonDamage(Mob* other, int64 &damage, const uint16 spell_id, const EQ::skills::SkillType attack_skill, bool &avoidable, const int8 buffslot, const bool iBuffTic, eSpecialAttacks specal = eSpecialAttacks::None); static uint16 GetProcID(uint16 spell_id, uint8 effect_index); @@ -1802,6 +1807,8 @@ protected: CombatRecord m_combat_record{}; + std::map m_stat_caps; + public: const CombatRecord &GetCombatRecord() const; diff --git a/zone/perl_bot.cpp b/zone/perl_bot.cpp index f667eaa44..f127afd4e 100644 --- a/zone/perl_bot.cpp +++ b/zone/perl_bot.cpp @@ -620,6 +620,16 @@ void Perl_Bot_RaidGroupSay(Bot* self, const char* message) // @categories Script self->RaidGroupSay(message); } +int Perl_Bot_GetStatCap(Bot* self, uint8 stat_id) +{ + return self->GetStatCap(stat_id); +} + +void Perl_Bot_SetStatCap(Bot* self, uint8 stat_id, int stat_cap) +{ + self->SetStatCap(stat_id, stat_cap, true); +} + void perl_register_bot() { perl::interpreter state(PERL_GET_THX); @@ -683,27 +693,28 @@ void perl_register_bot() package.add("GetBotItem", &Perl_Bot_GetBotItem); package.add("GetBotItemIDBySlot", &Perl_Bot_GetBotItemIDBySlot); package.add("GetClassAbbreviation", &Perl_Bot_GetClassAbbreviation); + package.add("GetDisciplineReuseTimer", (uint32(*)(Bot*))&Perl_Bot_GetDisciplineReuseTimer); + package.add("GetDisciplineReuseTimer", (uint32(*)(Bot*, uint16))&Perl_Bot_GetDisciplineReuseTimer); package.add("GetExpansionBitmask", &Perl_Bot_GetExpansionBitmask); package.add("GetGroup", &Perl_Bot_GetGroup); package.add("GetHealAmount", &Perl_Bot_GetHealAmount); package.add("GetInstrumentMod", &Perl_Bot_GetInstrumentMod); package.add("GetItemAt", &Perl_Bot_GetItemAt); + package.add("GetItemEquippedByID", &Perl_Bot_HasItemEquippedByID); package.add("GetItemIDAt", &Perl_Bot_GetItemIDAt); + package.add("GetItemReuseTimer", (uint32(*)(Bot*))&Perl_Bot_GetItemReuseTimer); + package.add("GetItemReuseTimer", (uint32(*)(Bot*, uint32))&Perl_Bot_GetItemReuseTimer); package.add("GetOwner", &Perl_Bot_GetOwner); package.add("GetRaceAbbreviation", &Perl_Bot_GetRaceAbbreviation); package.add("GetRawItemAC", &Perl_Bot_GetRawItemAC); package.add("GetSpellDamage", &Perl_Bot_GetSpellDamage); + package.add("GetSpellRecastTimer", (uint32(*)(Bot*))&Perl_Bot_GetSpellRecastTimer); + package.add("GetSpellRecastTimer", (uint32(*)(Bot*, uint16))&Perl_Bot_GetSpellRecastTimer); + package.add("GetStatCap", &Perl_Bot_GetStatCap); package.add("HasAugmentEquippedByID", &Perl_Bot_HasAugmentEquippedByID); package.add("HasBotItem", &Perl_Bot_HasBotItem); package.add("HasBotSpellEntry", &Perl_Bot_HasBotSpellEntry); package.add("HasItemEquippedByID", &Perl_Bot_HasItemEquippedByID); - package.add("GetDisciplineReuseTimer", (uint32(*)(Bot*))&Perl_Bot_GetDisciplineReuseTimer); - package.add("GetDisciplineReuseTimer", (uint32(*)(Bot*, uint16))&Perl_Bot_GetDisciplineReuseTimer); - package.add("GetItemEquippedByID", &Perl_Bot_HasItemEquippedByID); - package.add("GetItemReuseTimer", (uint32(*)(Bot*))&Perl_Bot_GetItemReuseTimer); - package.add("GetItemReuseTimer", (uint32(*)(Bot*, uint32))&Perl_Bot_GetItemReuseTimer); - package.add("GetSpellRecastTimer", (uint32(*)(Bot*))&Perl_Bot_GetSpellRecastTimer); - package.add("GetSpellRecastTimer", (uint32(*)(Bot*, uint16))&Perl_Bot_GetSpellRecastTimer); package.add("IsGrouped", &Perl_Bot_IsGrouped); package.add("IsSitting", &Perl_Bot_IsSitting); package.add("IsStanding", &Perl_Bot_IsStanding); @@ -736,6 +747,7 @@ void perl_register_bot() package.add("SetSpellDurationRaid", (void(*)(Bot*, int, int, int, bool, bool))&Perl_Bot_SetSpellDurationRaid); package.add("SetSpellRecastTimer", (void(*)(Bot*, uint16))&Perl_Bot_SetSpellRecastTimer); package.add("SetSpellRecastTimer", (void(*)(Bot*, uint16, uint32))&Perl_Bot_SetSpellRecastTimer); + package.add("SetStatCap", &Perl_Bot_SetStatCap); package.add("Signal", &Perl_Bot_Signal); package.add("Sit", &Perl_Bot_Sit); package.add("Stand", &Perl_Bot_Stand); diff --git a/zone/perl_client.cpp b/zone/perl_client.cpp index 49a2d9d02..5221f1f15 100644 --- a/zone/perl_client.cpp +++ b/zone/perl_client.cpp @@ -3363,6 +3363,16 @@ perl::array Perl_Client_GetKeyRing(Client* self) return result; } +int Perl_Client_GetStatCap(Client* self, uint8 stat_id) +{ + return self->GetStatCap(stat_id); +} + +void Perl_Client_SetStatCap(Client* self, uint8 stat_id, int stat_cap) +{ + self->SetStatCap(stat_id, stat_cap, true); +} + void perl_register_client() { perl::interpreter perl(PERL_GET_THX); @@ -3635,6 +3645,7 @@ void perl_register_client() package.add("GetSpellBookSlotBySpellID", &Perl_Client_GetSpellBookSlotBySpellID); package.add("GetSpellIDByBookSlot", &Perl_Client_GetSpellIDByBookSlot); package.add("GetSpentAA", &Perl_Client_GetSpentAA); + package.add("GetStatCap", &Perl_Client_GetStatCap); package.add("GetStartZone", &Perl_Client_GetStartZone); package.add("GetTargetRingX", &Perl_Client_GetTargetRingX); package.add("GetTargetRingY", &Perl_Client_GetTargetRingY); @@ -3899,6 +3910,7 @@ void perl_register_client() package.add("SetStartZone", (void(*)(Client*, uint32))&Perl_Client_SetStartZone); package.add("SetStartZone", (void(*)(Client*, uint32, float, float, float))&Perl_Client_SetStartZone); package.add("SetStartZone", (void(*)(Client*, uint32, float, float, float, float))&Perl_Client_SetStartZone); + package.add("SetStatCap", &Perl_Client_SetStatCap); package.add("SetStats", &Perl_Client_SetStats); package.add("SetThirst", &Perl_Client_SetThirst); package.add("SetTint", &Perl_Client_SetTint); diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 0dc143675..dd9e8e482 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -50,6 +50,8 @@ #include "../common/repositories/character_corpses_repository.h" #include "../common/repositories/character_corpse_items_repository.h" #include "../common/repositories/zone_repository.h" +#include "../common/repositories/bot_stat_caps_repository.h" +#include "../common/repositories/character_stat_caps_repository.h" #include "../common/repositories/trader_repository.h" #include "../common/repositories/character_evolving_items_repository.h" @@ -4297,3 +4299,83 @@ void ZoneDatabase::LoadCharacterTitleSets(Client* c) c->EnableTitle(e.title_set, false); } } + +void ZoneDatabase::LoadStatCaps(Mob* m) +{ + if (!zone || !m) { + return; + } + + if (m->IsBot()) { + const auto& l = BotStatCapsRepository::GetWhere( + *this, + fmt::format( + "`bot_id` = {}", + m->CastToBot()->GetBotID() + ) + ); + + if (l.empty()) { + return; + } + + for (const auto& e : l) { + m->SetStatCap(e.stat_id, e.stat_cap); + } + } else if (m->IsClient()) { + const auto& l = CharacterStatCapsRepository::GetWhere( + *this, + fmt::format( + "`character_id` = {}", + m->CastToClient()->CharacterID() + ) + ); + + if (l.empty()) { + return; + } + + for (const auto& e : l) { + m->SetStatCap(e.stat_id, e.stat_cap); + } + } +} + +void ZoneDatabase::SaveStatCaps(Mob* m) +{ + if (m->IsBot()) { + std::vector v; + + BotStatCapsRepository::BotStatCaps stat_cap; + + stat_cap.bot_id = m->CastToBot()->GetBotID(); + + for (const auto& e: m->GetStatCaps()) { + stat_cap.stat_id = e.first; + stat_cap.stat_cap = e.second; + + v.emplace_back(stat_cap); + } + + if (!v.empty()) { + BotStatCapsRepository::ReplaceMany(*this, v); + } + } else if (m->IsClient()) { + std::vector v; + + CharacterStatCapsRepository::CharacterStatCaps stat_cap; + + stat_cap.character_id = m->CastToClient()->CharacterID(); + + for (const auto& e: m->GetStatCaps()) { + stat_cap.stat_id = e.first; + stat_cap.stat_cap = e.second; + + v.emplace_back(stat_cap); + } + + if (!v.empty()) { + CharacterStatCapsRepository::ReplaceMany(*this, v); + } + } +} diff --git a/zone/zonedb.h b/zone/zonedb.h index cda0a5ab2..3bb0b6182 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -465,6 +465,10 @@ public: void LoadCharacterEXPModifier(Client* c); void SaveCharacterEXPModifier(Client *c); + /* Stat Caps */ + void LoadStatCaps(Mob* m); + void SaveStatCaps(Mob* m); + /* Player Title Sets */ void LoadCharacterTitleSets(Client* c);