[Feature] Add Bot/Client Specific Stat Caps

This commit is contained in:
Kinglykrab 2025-08-20 22:30:54 -04:00
parent 1eb89edbbd
commit aba58172f4
26 changed files with 1470 additions and 78 deletions

View File

@ -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`)
)
)"
}

View File

@ -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,

View File

@ -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",

View File

@ -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*/

View File

@ -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 <ctime>
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<std::string> Columns()
{
return {
"bot_id",
"stat_id",
"stat_cap",
};
}
static std::vector<std::string> 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<BotStatCaps> &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<uint32_t>(strtoul(row[0], nullptr, 10)) : 0;
e.stat_id = row[1] ? static_cast<uint8_t>(strtoul(row[1], nullptr, 10)) : 0;
e.stat_cap = row[2] ? static_cast<int32_t>(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<std::string> 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<std::string> 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<BotStatCaps> &entries
)
{
std::vector<std::string> insert_chunks;
for (auto &e: entries) {
std::vector<std::string> 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<std::string> v;
auto results = db.QueryDatabase(
fmt::format(
"{} VALUES {}",
BaseInsert(),
Strings::Implode(",", insert_chunks)
)
);
return (results.Success() ? results.RowsAffected() : 0);
}
static std::vector<BotStatCaps> All(Database& db)
{
std::vector<BotStatCaps> 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<uint32_t>(strtoul(row[0], nullptr, 10)) : 0;
e.stat_id = row[1] ? static_cast<uint8_t>(strtoul(row[1], nullptr, 10)) : 0;
e.stat_cap = row[2] ? static_cast<int32_t>(atoi(row[2])) : -1;
all_entries.push_back(e);
}
return all_entries;
}
static std::vector<BotStatCaps> GetWhere(Database& db, const std::string &where_filter)
{
std::vector<BotStatCaps> 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<uint32_t>(strtoul(row[0], nullptr, 10)) : 0;
e.stat_id = row[1] ? static_cast<uint8_t>(strtoul(row[1], nullptr, 10)) : 0;
e.stat_cap = row[2] ? static_cast<int32_t>(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<std::string> 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<BotStatCaps> &entries
)
{
std::vector<std::string> insert_chunks;
for (auto &e: entries) {
std::vector<std::string> 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<std::string> 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

View File

@ -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 <ctime>
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<std::string> Columns()
{
return {
"character_id",
"stat_id",
"stat_cap",
};
}
static std::vector<std::string> 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<CharacterStatCaps> &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<uint32_t>(strtoul(row[0], nullptr, 10)) : 0;
e.stat_id = row[1] ? static_cast<uint8_t>(strtoul(row[1], nullptr, 10)) : 0;
e.stat_cap = row[2] ? static_cast<int32_t>(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<std::string> 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<std::string> 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<CharacterStatCaps> &entries
)
{
std::vector<std::string> insert_chunks;
for (auto &e: entries) {
std::vector<std::string> 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<std::string> v;
auto results = db.QueryDatabase(
fmt::format(
"{} VALUES {}",
BaseInsert(),
Strings::Implode(",", insert_chunks)
)
);
return (results.Success() ? results.RowsAffected() : 0);
}
static std::vector<CharacterStatCaps> All(Database& db)
{
std::vector<CharacterStatCaps> 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<uint32_t>(strtoul(row[0], nullptr, 10)) : 0;
e.stat_id = row[1] ? static_cast<uint8_t>(strtoul(row[1], nullptr, 10)) : 0;
e.stat_cap = row[2] ? static_cast<int32_t>(atoi(row[2])) : -1;
all_entries.push_back(e);
}
return all_entries;
}
static std::vector<CharacterStatCaps> GetWhere(Database& db, const std::string &where_filter)
{
std::vector<CharacterStatCaps> 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<uint32_t>(strtoul(row[0], nullptr, 10)) : 0;
e.stat_id = row[1] ? static_cast<uint8_t>(strtoul(row[1], nullptr, 10)) : 0;
e.stat_cap = row[2] ? static_cast<int32_t>(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<std::string> 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<CharacterStatCaps> &entries
)
{
std::vector<std::string> insert_chunks;
for (auto &e: entries) {
std::vector<std::string> 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<std::string> 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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
);
}
}

View File

@ -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);

View File

@ -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<BotSpellSettings> m_bot_spell_settings;
std::vector<Mob*> _spell_target_list;

View File

@ -1103,6 +1103,8 @@ bool Client::Save(uint8 iCommitNow) {
database.SaveCharacterEXPModifier(this);
database.SaveStatCaps(this);
if (RuleB(Bots, Enabled)) {
database.botdb.SaveBotSettings(this);
}

View File

@ -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<int64>(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;
}

View File

@ -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

View File

@ -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_<Lua_Bot, Lua_Mob>("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)

View File

@ -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);

View File

@ -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_<Lua_Client, Lua_Mob>("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)

View File

@ -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);

View File

@ -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<int>(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<float>(CastToBot()->CalcCurrentWeight()) / 10.0f : static_cast<float>(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<uint8, int> 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
}
);
}
}

View File

@ -1519,6 +1519,11 @@ public:
bool IsGuildmaster() const;
bool IsDestroying() const { return m_destroying; }
int GetStatCap(uint8 stat_id) const;
std::map<uint8, int> 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<uint8, int> m_stat_caps;
public:
const CombatRecord &GetCombatRecord() const;

View File

@ -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);

View File

@ -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);

View File

@ -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<BotStatCapsRepository::BotStatCaps> 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<CharacterStatCapsRepository::CharacterStatCaps> 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);
}
}
}

View File

@ -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);