Refactor damage calculations and add additional modifiers

This commit is contained in:
wincent 2024-07-06 00:02:30 +02:00
parent 756dc4e44f
commit 5e3312850c
23 changed files with 1061 additions and 394 deletions

View File

@ -19,6 +19,10 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, Modifi
float total = 0;
for (const auto& modifier : activeModifiers) {
if (modifier.GetConvertTo() != ModifierType::Invalid) {
continue;
}
if (modifier.GetType() != type || modifier.GetOperator() != op || modifier.IsResistance() != resistance) {
continue;
}
@ -33,6 +37,10 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::v
float total = 0;
for (const auto& modifier : additionalModifiers) {
if (modifier.GetConvertTo() != ModifierType::Invalid) {
continue;
}
if (modifier.GetType() != type || modifier.GetOperator() != op || modifier.IsResistance() != resistance) {
continue;
}
@ -92,18 +100,18 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::v
float multiplicative = CalculateModifier(type, additionalModifiers, ModifierOperator::Multiplicative, false);
static const std::unordered_set<ModifierType> damageTypes = {
ModifierType::Slashing,
ModifierType::Piercing,
ModifierType::Bludgeoning,
static const std::unordered_set<ModifierType> elementalDamage = {
ModifierType::Fire,
ModifierType::Cold,
ModifierType::Lightning,
ModifierType::Corruption,
ModifierType::Psychic
ModifierType::Lightning
};
if (damageTypes.contains(type)) {
if (elementalDamage.contains(type)) {
additive += CalculateModifier(ModifierType::Elemental, additionalModifiers, ModifierOperator::Additive, false) / elementalDamage.size();
multiplicative += CalculateModifier(ModifierType::Elemental, additionalModifiers, ModifierOperator::Multiplicative, false) / elementalDamage.size();
}
if (nejlika::IsNormalDamageType(type) || nejlika::IsOverTimeType(type)) {
additive += CalculateModifier(ModifierType::Damage, additionalModifiers, ModifierOperator::Additive, false);
multiplicative += CalculateModifier(ModifierType::Damage, additionalModifiers, ModifierOperator::Multiplicative, false);
}
@ -117,6 +125,8 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::v
float nejlika::AdditionalEntityData::CalculateResistance(ModifierType type) const
{
type = nejlika::GetResistanceType(type);
return CalculateModifier(type, ModifierOperator::Multiplicative, true);
}
@ -125,7 +135,7 @@ float nejlika::AdditionalEntityData::CalculateMultiplier(ModifierType type) cons
return 1 + CalculateModifier(type, ModifierOperator::Multiplicative, false);
}
std::vector<ModifierInstance> nejlika::AdditionalEntityData::TriggerUpgradeItems(UpgradeTriggerType triggerType) {
std::vector<ModifierInstance> nejlika::AdditionalEntityData::TriggerUpgradeItems(UpgradeTriggerType triggerType, const TriggerParameters& params) {
auto* entity = Game::entityManager->GetEntity(id);
if (entity == nullptr) {
@ -155,7 +165,7 @@ std::vector<ModifierInstance> nejlika::AdditionalEntityData::TriggerUpgradeItems
const auto& upgradeData = *upgradeDataOpt.value();
const auto modifiers = upgradeData.Trigger(item->GetCount(), triggerType, id);
const auto modifiers = upgradeData.Trigger(item->GetCount(), triggerType, id, params);
result.insert(result.end(), modifiers.begin(), modifiers.end());
}
@ -163,6 +173,10 @@ std::vector<ModifierInstance> nejlika::AdditionalEntityData::TriggerUpgradeItems
return result;
}
std::vector<ModifierInstance> nejlika::AdditionalEntityData::TriggerUpgradeItems(UpgradeTriggerType triggerType) {
return TriggerUpgradeItems(triggerType, {});
}
void nejlika::AdditionalEntityData::InitializeSkills() {
auto* entity = Game::entityManager->GetEntity(id);
@ -282,6 +296,62 @@ void nejlika::AdditionalEntityData::RollStandardModifiers(int32_t level) {
}
}
float nejlika::AdditionalEntityData::CalculateMultiplier(ModifierType type, std::vector<ModifierInstance>& additionalModifiers) const {
return 1 + CalculateModifier(type, additionalModifiers, ModifierOperator::Multiplicative, false);
}
std::unordered_map<ModifierType, std::unordered_map<ModifierType, float>> nejlika::AdditionalEntityData::CalculateDamageConversion(std::vector<ModifierInstance>& additionalModifiers) const {
std::unordered_map<ModifierType, std::unordered_map<ModifierType, float>> conversion;
for (const auto& modifier : activeModifiers) {
if (modifier.GetConvertTo() == ModifierType::Invalid) {
continue;
}
conversion[modifier.GetType()][modifier.GetConvertTo()] += modifier.GetValue();
}
for (const auto& modifier : additionalModifiers) {
if (modifier.GetConvertTo() == ModifierType::Invalid) {
continue;
}
conversion[modifier.GetType()][modifier.GetConvertTo()] += modifier.GetValue();
}
// Third pass: adjust bidirectional conversions
auto copy = conversion; // Create a copy to iterate over
for (const auto& [type, convertMap] : copy) {
for (const auto& [convertTo, value] : convertMap) {
if (conversion[convertTo][type] > 0) {
if (value > conversion[convertTo][type]) {
conversion[type][convertTo] -= conversion[convertTo][type];
conversion[convertTo][type] = 0; // Ensure no negative values
} else {
conversion[convertTo][type] -= value;
conversion[type][convertTo] = 0; // Ensure no negative values
}
}
}
}
// Fourth pass: if a type converts to multiple types, and the sum of the conversion values is greater than 100, normalize the values
for (const auto& [type, convertMap] : conversion) {
float sum = 0;
for (const auto& [convertTo, value] : convertMap) {
sum += value;
}
if (sum > 100) {
for (const auto& [convertTo, value] : convertMap) {
conversion[type][convertTo] = value / sum * 100;
}
}
}
return conversion;
}
void nejlika::AdditionalEntityData::ApplyToEntity() {
const auto templateDataOpt = NejlikaData::GetEntityTemplate(lot);
@ -334,6 +404,31 @@ void nejlika::AdditionalEntityData::ApplyToEntity() {
activeModifiers.insert(activeModifiers.end(), itemModifiers.begin(), itemModifiers.end());
}
for (const auto& upgradeItem : upgradeItems) {
auto* item = inventoryComponent->FindItemById(upgradeItem);
if (item == nullptr) {
continue;
}
LOG("Applying upgrade item %i", item->GetLot());
const auto itemDataOpt = NejlikaData::GetUpgradeTemplate(item->GetLot());
if (!itemDataOpt.has_value()) {
LOG("Upgrade item %i has no data", item->GetLot());
continue;
}
const auto& itemData = *itemDataOpt.value();
const auto& itemModifiers = itemData.GenerateModifiers(item->GetCount());
LOG("Upgrade item %i has %i modifiers with level %i", item->GetLot(), itemModifiers.size(), item->GetCount());
activeModifiers.insert(activeModifiers.end(), itemModifiers.begin(), itemModifiers.end());
}
}
destroyable->SetMaxHealth(static_cast<int32_t>(CalculateModifier(ModifierType::Health, level)));

View File

@ -8,6 +8,7 @@
#include "ModifierInstance.h"
#include "EntityTemplate.h"
#include "UpgradeTriggerType.h"
#include "TriggerParameters.h"
#include <unordered_set>
@ -41,6 +42,24 @@ public:
*/
float CalculateMultiplier(ModifierType type) const;
/**
* @brief Calculate the multiplier for a given modifier type. With a base value of 1.0.
*
* @param type The modifier type.
* @param additionalModifiers Additional modifiers to apply.
* @return The multiplier.
*/
float CalculateMultiplier(ModifierType type, std::vector<ModifierInstance>& additionalModifiers) const;
/**
* @brief Calculate damage conversation mapping.
*
* @param additionalModifiers Additional modifiers to apply.
*
* @return The damage conversion mapping.
*/
std::unordered_map<ModifierType, std::unordered_map<ModifierType, float>> CalculateDamageConversion(std::vector<ModifierInstance>& additionalModifiers) const;
void ApplyToEntity();
void CheckForRescale(AdditionalEntityData* other);
@ -57,6 +76,8 @@ public:
void RemoveUpgradeItem(LWOOBJID id) { upgradeItems.erase(id); }
std::vector<ModifierInstance> TriggerUpgradeItems(UpgradeTriggerType triggerType, const TriggerParameters& params);
std::vector<ModifierInstance> TriggerUpgradeItems(UpgradeTriggerType triggerType);
void InitializeSkills();

View File

@ -66,7 +66,8 @@ std::vector<nejlika::ModifierInstance> nejlika::EntityTemplate::GenerateModifier
scaler.isResistance,
ModifierCategory::Player,
0,
""
"",
ModifierType::Invalid
);
modifiers.push_back(modifier);

View File

@ -5,6 +5,7 @@
nejlika::ModifierInstance::ModifierInstance(const nlohmann::json& config) {
type = magic_enum::enum_cast<ModifierType>(config["type"].get<std::string>()).value_or(ModifierType::Invalid);
convertTo = magic_enum::enum_cast<ModifierType>(config["convert-to"].get<std::string>()).value_or(ModifierType::Invalid);
value = config["value"].get<float>();
if (config.contains("op")) {
@ -32,6 +33,7 @@ nlohmann::json nejlika::ModifierInstance::ToJson() const
nlohmann::json config;
config["type"] = magic_enum::enum_name(type);
config["convert-to"] = magic_enum::enum_name(convertTo);
config["value"] = value;
config["op"] = magic_enum::enum_name(op);
config["resistance"] = isResistance;

View File

@ -16,8 +16,8 @@ class ModifierInstance
{
public:
ModifierInstance(
ModifierType type, float value, ModifierOperator op, bool isResistance, ModifierCategory category, uint32_t effectID, const std::string& effectType
) : type(type), value(value), op(op), isResistance(isResistance), category(category), effectID(effectID), effectType(effectType) {}
ModifierType type, float value, ModifierOperator op, bool isResistance, ModifierCategory category, uint32_t effectID, const std::string& effectType, ModifierType convertTo
) : type(type), value(value), op(op), isResistance(isResistance), category(category), effectID(effectID), effectType(effectType), convertTo(convertTo) {}
/**
* @brief Construct a new Modifier Instance object from a json configuration.
@ -45,6 +45,8 @@ public:
ModifierType GetType() const { return type; }
ModifierType GetConvertTo() const { return convertTo; }
float GetValue() const { return value; }
ModifierOperator GetOperator() const { return op; }
@ -59,6 +61,8 @@ public:
void SetType(ModifierType type) { this->type = type; }
void SetConvertTo(ModifierType convertTo) { this->convertTo = convertTo; }
void SetValue(float value) { this->value = value; }
void SetOperator(ModifierOperator op) { this->op = op; }
@ -73,6 +77,7 @@ public:
private:
ModifierType type;
ModifierType convertTo;
float value;
ModifierOperator op;
bool isResistance;

View File

@ -59,16 +59,33 @@ nejlika::ModifierTemplate::ModifierTemplate(const nlohmann::json& config) {
types = {};
}
if (!config.contains("scaling"))
if (config.contains("convert-to"))
{
throw std::runtime_error("Modifier template is missing scaling.");
convertTo = magic_enum::enum_cast<ModifierType>(config["convert-to"].get<std::string>()).value_or(ModifierType::Invalid);
}
const auto scaling = config["scaling"];
for (const auto& scaler : scaling)
else
{
scales.push_back(ModifierScale(scaler));
convertTo = ModifierType::Invalid;
}
if (config.contains("scaling"))
{
const auto scaling = config["scaling"];
for (const auto& scaler : scaling)
{
scales.push_back(ModifierScale(scaler));
}
}
if (config.contains("polynomial"))
{
const auto polynomialConfig = config["polynomial"];
for (const auto& term : polynomialConfig)
{
polynomial.push_back(term.get<float>());
}
}
if (config.contains("category"))
@ -144,14 +161,31 @@ nlohmann::json nejlika::ModifierTemplate::ToJson() const {
}
}
nlohmann::json scaling;
for (const auto& scale : scales)
if (!scales.empty())
{
scaling.push_back(scale.ToJson());
nlohmann::json scaling;
for (const auto& scale : scales)
{
scaling.push_back(scale.ToJson());
}
config["scaling"] = scaling;
}
config["scaling"] = scaling;
if (!polynomial.empty())
{
nlohmann::json polynomialConfig;
for (const auto& term : polynomial)
{
polynomialConfig.push_back(term);
}
config["polynomial"] = polynomialConfig;
}
config["convert-to"] = magic_enum::enum_name(convertTo);
config["category"] = magic_enum::enum_name(category);
config["resistance"] = isResistance;
@ -209,10 +243,120 @@ std::vector<ModifierInstance> nejlika::ModifierTemplate::GenerateModifiers(int32
return modifiers;
}
std::string nejlika::ModifierTemplate::GenerateHtmlString(const std::vector<ModifierTemplate>& modifiers, int32_t level) {
std::stringstream ss;
// target -> resistance -> op -> type -> (min, max)
std::unordered_map<ModifierCategory, std::unordered_map<bool, std::unordered_map<ModifierOperator, std::unordered_map<ModifierType, std::pair<float, float>>>>> modifierMap;
for (const auto& modifier : modifiers) {
for (const auto& type : modifier.types) {
if (type == ModifierType::Invalid) {
continue;
}
if (!modifier.polynomial.empty())
{
float value = 0.0f;
int32_t power = 0;
for (const auto& term : modifier.polynomial)
{
value += term * std::pow(level, power);
power++;
}
modifierMap[modifier.category][modifier.isResistance][modifier.operatorType][type] = {value, value};
continue;
}
ModifierScale scale;
bool found = false;
// Select the scale with the highest level that is less than or equal to the current level
for (const auto& s : modifier.scales) {
if (s.GetLevel() <= level && s.GetLevel() > scale.GetLevel()) {
scale = s;
found = true;
}
}
if (!found) {
continue;
}
modifierMap[modifier.category][modifier.isResistance][modifier.operatorType][type] = {scale.GetMin(), scale.GetMax()};
}
}
// Resistances and addatives are not separated, pet and player are
// Summarize the resistances and addatives
for (const auto& target : modifierMap) {
if (target.first == ModifierCategory::Pet) {
ss << "\n<font color=\"#D0AB62\">Pets:</font>\n";
}
for (const auto& resistance : target.second) {
ss << "\n<font color=\"#D0AB62\">";
ss << ((resistance.first) ? "Resistances" : "Modifiers");
ss << ":</font>\n";
for (const auto& math : resistance.second) {
for (const auto& modifier : math.second) {
ss << "<font color=\"" << GetModifierTypeColor(modifier.first) << "\">";
ss << magic_enum::enum_name<ModifierType>(modifier.first) << ": ";
ss << ((modifier.second.first > 0) ? "+" : "-");
ss << std::fixed << std::setprecision(0) << std::abs(modifier.second.first);
if (modifier.second.first != modifier.second.second)
{
ss << "/";
ss << ((modifier.second.second > 0) ? "+" : "-");
ss << std::fixed << std::setprecision(0) << std::abs(modifier.second.second);
}
if (math.first == ModifierOperator::Multiplicative) {
ss << "%";
}
ss << "</font>\n";
}
}
}
}
return ss.str();
}
std::optional<ModifierInstance> nejlika::ModifierTemplate::GenerateModifier(ModifierType type, int32_t level) const {
ModifierScale scale;
bool found = false;
if (!polynomial.empty())
{
float value = 0.0f;
int32_t power = 0;
for (const auto& term : polynomial)
{
value += term * std::pow(level, power);
power++;
}
return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo);
}
// Select the scale with the highest level that is less than or equal to the current level
for (const auto& s : scales) {
if (s.GetLevel() <= level && s.GetLevel() > scale.GetLevel()) {
@ -227,5 +371,5 @@ std::optional<ModifierInstance> nejlika::ModifierTemplate::GenerateModifier(Modi
float value = GeneralUtils::GenerateRandomNumber<float>(scale.GetMin(), scale.GetMax());
return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType);
return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo);
}

View File

@ -45,6 +45,8 @@ public:
const std::vector<ModifierType>& GetTypes() const { return types; }
ModifierType GetConvertTo() const { return convertTo; }
ModifierTemplateSelector GetSelector() const { return selector; }
const std::vector<ModifierScale>& GetScales() const { return scales; }
@ -59,6 +61,8 @@ public:
void SetTypes(const std::vector<ModifierType>& types) { this->types = types; }
void SetConvertTo(ModifierType convertTo) { this->convertTo = convertTo; }
void SetSelector(ModifierTemplateSelector selector) { this->selector = selector; }
void SetScales(const std::vector<ModifierScale>& scales) { this->scales = scales; }
@ -70,13 +74,25 @@ public:
void SetEffectID(uint32_t effectID) { this->effectID = effectID; }
void SetEffectType(const std::string& effectType) { this->effectType = effectType; }
/**
* @brief Generate a HTML string representation of a set of modifier templates.
*
* @param modifiers The modifier templates to generate the HTML string for.
* @param level The level of the modifier templates.
* @return The HTML string.
*/
static std::string GenerateHtmlString(const std::vector<ModifierTemplate>& modifiers, int32_t level);
private:
std::optional<ModifierInstance> GenerateModifier(ModifierType type, int32_t level) const;
std::vector<ModifierType> types;
ModifierType convertTo;
ModifierTemplateSelector selector;
std::vector<ModifierScale> scales;
std::vector<float> polynomial;
ModifierCategory category;
ModifierOperator operatorType;
bool isResistance;

View File

@ -1,5 +1,6 @@
#include "ModifierType.h"
#include <unordered_map>
#include <unordered_set>
using namespace nejlika;
@ -12,14 +13,106 @@ const std::unordered_map<ModifierType, std::string> colorMap = {
{ModifierType::Imagination, "#0077FF"},
{ModifierType::Offensive, "#71583B"},
{ModifierType::Defensive, "#71583B"},
{ModifierType::Slashing, "#666666"},
{ModifierType::Piercing, "#4f4f4f"},
{ModifierType::Bludgeoning, "#e84646"},
{ModifierType::Physical, "#666666"},
{ModifierType::Pierce, "#4f4f4f"},
{ModifierType::Vitality, "#e84646"},
{ModifierType::Fire, "#ff0000"},
{ModifierType::Cold, "#94d0f2"},
{ModifierType::Lightning, "#00a2ff"},
{ModifierType::Corruption, "#3d00ad"},
{ModifierType::Psychic, "#4b0161"}
{ModifierType::Psychic, "#4b0161"},
{ModifierType::Acid, "#00ff00"},
};
const std::unordered_map<ModifierType, std::string> nameMap = {
{ModifierType::Health, "Health"},
{ModifierType::Armor, "Armor"},
{ModifierType::Imagination, "Imagination"},
{ModifierType::Offensive, "Offensive Ability"},
{ModifierType::Defensive, "Defensive Ability"},
{ModifierType::Physical, "Physical Damage"},
{ModifierType::Pierce, "Pierce Damage"},
{ModifierType::Vitality, "Vitality Damage"},
{ModifierType::Fire, "Fire Damage"},
{ModifierType::Cold, "Cold Damage"},
{ModifierType::Lightning, "Lightning Damage"},
{ModifierType::Corruption, "Corruption Damage"},
{ModifierType::Psychic, "Psychic Damage"},
{ModifierType::Acid, "Acid Damage"},
{ModifierType::InternalDisassembly, "Internal Disassembly"},
{ModifierType::InternalDisassemblyDuration, "Internal Disassembly Duration"},
{ModifierType::Burn, "Burn"},
{ModifierType::BurnDuration, "Burn Duration"},
{ModifierType::Frostburn, "Frostburn"},
{ModifierType::FrostburnDuration, "Frostburn Duration"},
{ModifierType::Poison, "Poison"},
{ModifierType::PoisonDuration, "Poison Duration"},
{ModifierType::Electrocute, "Electrocute"},
{ModifierType::ElectrocuteDuration, "Electrocute Duration"},
{ModifierType::VitalityDecay, "Vitality Decay"},
{ModifierType::VitalityDecayDuration, "Vitality Decay Duration"},
{ModifierType::Seperation, "Seperation"},
{ModifierType::SeperationDuration, "Seperation Duration"},
};
const std::unordered_map<ModifierType, ModifierType> resistanceMap = {
{ModifierType::Physical, ModifierType::Physical},
{ModifierType::Pierce, ModifierType::Pierce},
{ModifierType::Vitality, ModifierType::Vitality},
{ModifierType::Fire, ModifierType::Fire},
{ModifierType::Cold, ModifierType::Cold},
{ModifierType::Lightning, ModifierType::Lightning},
{ModifierType::Corruption, ModifierType::Corruption},
{ModifierType::Psychic, ModifierType::Psychic},
{ModifierType::Acid, ModifierType::Acid},
{ModifierType::InternalDisassembly, ModifierType::Physical},
{ModifierType::Burn, ModifierType::Fire},
{ModifierType::Frostburn, ModifierType::Cold},
{ModifierType::Poison, ModifierType::Acid},
{ModifierType::VitalityDecay, ModifierType::Vitality},
{ModifierType::Electrocute, ModifierType::Lightning},
{ModifierType::Seperation, ModifierType::Seperation}
};
const std::unordered_set<ModifierType> normalDamageTypes = {
ModifierType::Physical,
ModifierType::Pierce,
ModifierType::Vitality,
ModifierType::Fire,
ModifierType::Cold,
ModifierType::Lightning,
ModifierType::Corruption,
ModifierType::Psychic,
ModifierType::Acid
};
const std::unordered_map<ModifierType, ModifierType> durationMap = {
{ModifierType::InternalDisassembly, ModifierType::InternalDisassemblyDuration},
{ModifierType::Burn, ModifierType::BurnDuration},
{ModifierType::Frostburn, ModifierType::FrostburnDuration},
{ModifierType::Poison, ModifierType::PoisonDuration},
{ModifierType::VitalityDecay, ModifierType::VitalityDecayDuration},
{ModifierType::Electrocute, ModifierType::ElectrocuteDuration},
{ModifierType::Seperation, ModifierType::SeperationDuration}
};
const std::unordered_map<ModifierType, ModifierType> overTimeMap = {
{ModifierType::Physical, ModifierType::InternalDisassembly},
{ModifierType::Fire, ModifierType::Burn},
{ModifierType::Cold, ModifierType::Frostburn},
{ModifierType::Poison, ModifierType::Poison},
{ModifierType::Vitality, ModifierType::VitalityDecay},
{ModifierType::Lightning, ModifierType::Electrocute}
};
const std::unordered_set<ModifierType> isOverTimeMap = {
ModifierType::InternalDisassembly,
ModifierType::Burn,
ModifierType::Frostburn,
ModifierType::Poison,
ModifierType::VitalityDecay,
ModifierType::Electrocute,
ModifierType::Seperation
};
}
@ -35,4 +128,54 @@ const std::string& nejlika::GetModifierTypeColor(ModifierType type)
static const std::string white = "#FFFFFF";
return white;
}
}
const std::string& nejlika::GetModifierTypeName(ModifierType type) {
const auto name = nameMap.find(type);
if (name != nameMap.end()) {
return name->second;
}
static const std::string invalid = "Invalid";
return invalid;
}
const ModifierType nejlika::GetResistanceType(ModifierType type) {
const auto resistance = resistanceMap.find(type);
if (resistance != resistanceMap.end()) {
return resistance->second;
}
return ModifierType::Invalid;
}
const bool nejlika::IsNormalDamageType(ModifierType type) {
return normalDamageTypes.find(type) != normalDamageTypes.end();
}
const ModifierType nejlika::GetOverTimeType(ModifierType type) {
const auto overTime = overTimeMap.find(type);
if (overTime != overTimeMap.end()) {
return overTime->second;
}
return ModifierType::Invalid;
}
const ModifierType nejlika::GetDurationType(ModifierType type) {
const auto duration = durationMap.find(type);
if (duration != durationMap.end()) {
return duration->second;
}
return ModifierType::Invalid;
}
const bool nejlika::IsOverTimeType(ModifierType type) {
return isOverTimeMap.find(type) != isOverTimeMap.end();
}

View File

@ -12,31 +12,72 @@ enum class ModifierType : uint8_t
Armor,
Imagination,
Physique,
Cunning,
Spirit,
Offensive,
Defensive,
Slashing,
Piercing,
Bludgeoning,
// Normal Types
Physical,
Fire,
Cold,
Lightning,
Corruption,
Acid,
Vitality,
Pierce,
Corruption, // Aether
Psychic, // Chaos
Elemental,
Psychic,
// Duration Types
InternalDisassembly, // Internal Trauma
InternalDisassemblyDuration,
Burn,
BurnDuration,
Frostburn,
FrostburnDuration,
Poison,
PoisonDuration,
VitalityDecay,
VitalityDecayDuration,
Electrocute,
ElectrocuteDuration,
Seperation, // Bleeding
SeperationDuration,
// Special
Elemental, // Even split between Fire, Cold, Lightning
Damage,
Speed,
AttackSpeed,
SkillModifier,
Slow,
ArmorPiercing,
CriticalDamage,
ChanceToBlock,
HealthDrain,
Invalid
};
const std::string& GetModifierTypeColor(ModifierType type);
const std::string& GetModifierTypeName(ModifierType type);
const ModifierType GetResistanceType(ModifierType type);
const bool IsNormalDamageType(ModifierType type);
const bool IsOverTimeType(ModifierType type);
const ModifierType GetOverTimeType(ModifierType type);
const ModifierType GetDurationType(ModifierType type);
}

View File

@ -107,6 +107,13 @@ void nejlika::NejlikaData::UnsetAdditionalEntityData(LWOOBJID id) {
void nejlika::NejlikaData::LoadNejlikaData()
{
const auto& lookupFile = Game::config->GetValue("lookup");
if (!lookupFile.empty())
{
lookup = Lookup(lookupFile);
}
modifierNameTemplates.clear();
// Load data from json file
@ -179,12 +186,5 @@ void nejlika::NejlikaData::LoadNejlikaData()
upgradeTemplates[upgradeTemplate.GetLot()] = upgradeTemplate;
}
}
const auto& lookupFile = Game::config->GetValue("lookup");
if (!lookupFile.empty())
{
lookup = Lookup(lookupFile);
}
}

View File

@ -27,8 +27,7 @@
using namespace nejlika;
using namespace nejlika::NejlikaData;
void nejlika::NejlikaHooks::InstallHooks()
{
void nejlika::NejlikaHooks::InstallHooks() {
Command itemDescriptionCommand{
.help = "Special UI command, does nothing when used in chat.",
.info = "Special UI command, does nothing when used in chat.",
@ -95,6 +94,26 @@ void nejlika::NejlikaHooks::InstallHooks()
entityData.AddUpgradeItem(item->GetId());
entityData.AddSkills(item->GetId());
entityData.ApplyToEntity();
};
InventoryComponent::OnCountChanged += [](InventoryComponent* component, Item* item) {
auto entityDataOpt = GetAdditionalEntityData(component->GetParent()->GetObjectID());
if (!entityDataOpt.has_value()) {
return;
}
auto& entityData = *entityDataOpt.value();
auto upgradeTemplateOpt = GetUpgradeTemplate(item->GetLot());
if (!upgradeTemplateOpt.has_value()) {
return;
}
entityData.ApplyToEntity();
};
EntityManager::OnEntityCreated += [](Entity* entity) {
@ -103,9 +122,9 @@ void nejlika::NejlikaHooks::InstallHooks()
if (!destroyable) {
return;
}
SetAdditionalEntityData(entity->GetObjectID(), AdditionalEntityData(entity->GetObjectID(), entity->GetLOT()));
auto additionalDataOpt = GetAdditionalEntityData(entity->GetObjectID());
if (!additionalDataOpt.has_value()) {
@ -143,13 +162,12 @@ void nejlika::NejlikaHooks::InstallHooks()
};
Entity::OnReadyForUpdates += [](Entity* entity) {
if (!entity->IsPlayer())
{
if (!entity->IsPlayer()) {
return;
}
GameMessages::SendAddSkill(entity, NejlikaData::GetLookup().GetValue("intro:skills:proxy:main"), BehaviorSlot::Head);
GameMessages::SendAddSkill(entity, NejlikaData::GetLookup().GetValue("intro:skills:proxy:secondary"), BehaviorSlot::Offhand);
//GameMessages::SendAddSkill(entity, NejlikaData::GetLookup().GetValue("intro:skills:proxy:main"), BehaviorSlot::Head);
//GameMessages::SendAddSkill(entity, NejlikaData::GetLookup().GetValue("intro:skills:proxy:secondary"), BehaviorSlot::Offhand);
GameMessages::SendAddSkill(entity, NejlikaData::GetLookup().GetValue("intro:skills:proxy:tertiary"), BehaviorSlot::Neck);
auto* missionComponent = entity->GetComponent<MissionComponent>();
@ -158,7 +176,24 @@ void nejlika::NejlikaHooks::InstallHooks()
if (missionComponent->GetMissionState(1732) != eMissionState::COMPLETE) {
missionComponent->CompleteMission(1732, true, false);
}
if (missionComponent->GetMissionState(173) != eMissionState::COMPLETE) {
missionComponent->CompleteMission(173, true, false);
auto* destroyable = entity->GetComponent<DestroyableComponent>();
destroyable->SetMaxImagination(6);
destroyable->SetImagination(6);
}
}
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (!inventoryComponent) {
return;
}
inventoryComponent->UpdateSkills();
};
EntityManager::OnEntityDestroyed += [](Entity* entity) {
@ -203,7 +238,7 @@ void nejlika::NejlikaHooks::InstallHooks()
return;
}
inventoryComponent->AddItem(NejlikaData::GetLookup().GetValue("intro:upgrades:level-token"), 1, eLootSourceType::MODERATION);
inventoryComponent->AddItem(NejlikaData::GetLookup().GetValue("intro:upgrades:level-token"), 3, eLootSourceType::MODERATION);
};
InventoryComponent::OnItemEquipped += [](InventoryComponent* component, Item* item) {
@ -232,7 +267,7 @@ void nejlika::NejlikaHooks::InstallHooks()
const auto& itemData = *itemDataOpt.value();
const auto itemId = item->GetId();
std::cout << "Sending effects for item: " << itemId << " with " << itemData.GetModifierInstances().size() << " modifiers." << std::endl;
for (const auto& modifier : itemData.GetModifierInstances()) {
@ -247,9 +282,9 @@ void nejlika::NejlikaHooks::InstallHooks()
GeneralUtils::UTF8ToUTF16(effectType),
std::to_string(GeneralUtils::GenerateRandomNumber<uint32_t>())
);
});
});
}
};
};
InventoryComponent::OnItemUnequipped += [](InventoryComponent* component, Item* item) {
const auto entityDataOpt = GetAdditionalEntityData(component->GetParent()->GetObjectID());
@ -263,11 +298,11 @@ void nejlika::NejlikaHooks::InstallHooks()
entityData.TriggerUpgradeItems(UpgradeTriggerType::UnEquip);
entityData.ApplyToEntity();
};
};
SkillComponent::OnSkillCast += [](SkillComponent* skillComponent, uint32_t skillID, bool success) {
std::cout << "Skill cast: " << skillID << " - " << success << std::endl;
auto* inventoryComponent = skillComponent->GetParent()->GetComponent<InventoryComponent>();
if (!inventoryComponent) {
@ -283,127 +318,11 @@ void nejlika::NejlikaHooks::InstallHooks()
const auto tertiaryTrigger = NejlikaData::GetLookup().GetValue("intro:skills:proxy:tertiary");
if (skillID == primaryTrigger || skillID == secondaryTrigger || skillID == tertiaryTrigger) {
}
else
{
/*
const auto primarySkills = skills[BehaviorSlot::Primary];
// If the skillID is in the primary skills, ignore this
if (primarySkills.contains(skillID)) {
if (entity->HasVar(u"skill-cast") && entity->GetVar<size_t>(u"skill-cast") == 0) {
GameMessages::SendAddSkill(entity, primaryTrigger, BehaviorSlot::Head);
GameMessages::SendAddSkill(entity, secondaryTrigger, BehaviorSlot::Offhand);
GameMessages::SendAddSkill(entity, tertiaryTrigger, BehaviorSlot::Neck);
}
return;
}
if (entity->HasVar(u"skill-cast")) {
entity->SetVar(u"skill-cast", static_cast<size_t>(0));
}
if (entity->HasVar(u"skill-cast-slot")) {
BehaviorSlot slot = static_cast<BehaviorSlot>(entity->GetVar<int32_t>(u"skill-cast-slot"));
auto primarySkills = inventoryComponent->GetSkills();
for (const auto& skill : primarySkills[slot]) {
GameMessages::SendRemoveSkill(entity, skill);
}
}
LOG("Restoring triggers via skill");
// Restore the triggers
GameMessages::SendAddSkill(entity, primaryTrigger, BehaviorSlot::Head);
GameMessages::SendAddSkill(entity, secondaryTrigger, BehaviorSlot::Offhand);
GameMessages::SendAddSkill(entity, tertiaryTrigger, BehaviorSlot::Neck);
*/
} else {
return;
}
static const std::vector<BehaviorSlot> slotOrder = {
BehaviorSlot::Head,
BehaviorSlot::Offhand,
BehaviorSlot::Neck
};
std::set<uint32_t> selectedSkills;
BehaviorSlot slot = BehaviorSlot::Invalid;
if (skillID == primaryTrigger) {
selectedSkills = skills[BehaviorSlot::Head];
slot = BehaviorSlot::Head;
} else if (skillID == secondaryTrigger) {
selectedSkills = skills[BehaviorSlot::Offhand];
slot = BehaviorSlot::Offhand;
} else if (skillID == tertiaryTrigger) {
selectedSkills = skills[BehaviorSlot::Neck];
slot = BehaviorSlot::Neck;
}
if (selectedSkills.empty()) {
return;
}
else {
GameMessages::SendRemoveSkill(entity, primaryTrigger);
GameMessages::SendRemoveSkill(entity, secondaryTrigger);
GameMessages::SendRemoveSkill(entity, tertiaryTrigger);
}
int32_t i = 0;
for (const auto& skill : selectedSkills) {
if (i >= 3) {
break;
}
GameMessages::SendAddSkill(entity, skill, slotOrder[i]);
i++;
}
const auto randomNumber = GeneralUtils::GenerateRandomNumber<size_t>();
entity->SetVar(u"skill-cast", randomNumber);
entity->SetVar(u"skill-cast-slot", static_cast<int32_t>(slot));
entity->AddCallbackTimer(1.0f, [entity, randomNumber, primaryTrigger, secondaryTrigger, tertiaryTrigger]() {
if (!entity->HasVar(u"skill-cast")) {
return;
}
const auto currentRandom = entity->GetVar<size_t>(u"skill-cast");
if (currentRandom != randomNumber) {
return;
}
entity->SetVar(u"skill-cast", static_cast<size_t>(0));
LOG("Restoring triggers via timeout");
// Remove the skills
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (!inventoryComponent) {
return;
}
BehaviorSlot slot = static_cast<BehaviorSlot>(entity->GetVar<int32_t>(u"skill-cast-slot"));
auto primarySkills = inventoryComponent->GetSkills();
for (const auto& skill : primarySkills[slot]) {
GameMessages::SendRemoveSkill(entity, skill);
}
// Restore the triggers
GameMessages::SendAddSkill(entity, primaryTrigger, BehaviorSlot::Head);
GameMessages::SendAddSkill(entity, secondaryTrigger, BehaviorSlot::Offhand);
GameMessages::SendAddSkill(entity, tertiaryTrigger, BehaviorSlot::Neck);
});
inventoryComponent->RotateSkills();
};
DestroyableComponent::OnDamageCalculation += [](Entity* damaged, LWOOBJID offender, uint32_t skillID, uint32_t& damage) {
@ -432,10 +351,10 @@ void nejlika::NejlikaHooks::InstallHooks()
if (baseCombatAIComponent) {
baseCombatAIComponent->SetThreat(offender, 1);
}
damagedEntity.CheckForRescale(&offenderEntity);
offenderEntity.CheckForRescale(&damagedEntity);
int32_t level = offenderEntity.GetLevel();
auto* offfendEntity = Game::entityManager->GetEntity(offender);
@ -453,49 +372,34 @@ void nejlika::NejlikaHooks::InstallHooks()
LOT itemLot = 0;
LWOOBJID itemId = 0;
BehaviorSlot itemSlot = BehaviorSlot::Invalid;
auto* inventoryComponent = offfendEntity->GetComponent<InventoryComponent>();
if (inventoryComponent) {
const auto& skills = inventoryComponent->GetSkills();
std::cout << "Found " << skills.size() << " skills." << std::endl;
const auto& equipped = inventoryComponent->GetEquippedItems();
// omg...
for (const auto& [slot, skillSet] : skills) {
if (skillSet.empty())
{
continue;
}
for (const auto& [equippedSlot, itemDetails] : equipped) {
std::cout << "Found equipped item: " << itemDetails.lot << std::endl;
const auto& skill = *skillSet.begin();
const auto info = Inventory::FindItemComponent(itemDetails.lot);
const auto skill = InventoryComponent::FindSkill(itemDetails.lot);
std::cout << "Found skill: " << skill << std::endl;
if (skill != skillID) {
continue;
}
const auto& equipped = inventoryComponent->GetEquippedItems();
const auto itemBehaviorSlot = InventoryComponent::FindBehaviorSlot(static_cast<eItemType>(info.itemType));
for (const auto& [equippedSlot, itemDetails] : equipped) {
std::cout << "Found equipped item: " << itemDetails.lot << std::endl;
itemLot = itemDetails.lot;
itemId = itemDetails.id;
itemSlot = itemBehaviorSlot;
const auto info = Inventory::FindItemComponent(itemDetails.lot);
std::cout << "Found item: " << itemLot << std::endl;
const auto itemBehaviorSlot = InventoryComponent::FindBehaviorSlot(static_cast<eItemType>(info.itemType));
std::cout << "Comparing slots: " << static_cast<int32_t>(itemBehaviorSlot) << " - " << static_cast<int32_t>(slot) << std::endl;
if (itemBehaviorSlot == slot) {
itemLot = itemDetails.lot;
itemId = itemDetails.id;
std::cout << "Found item: " << itemLot << std::endl;
break;
}
}
break;
}
}
@ -503,7 +407,7 @@ void nejlika::NejlikaHooks::InstallHooks()
const auto& skillTemplateIt = std::find_if(skillTemplates.begin(), skillTemplates.end(), [skillID](const auto& it) {
return it.GetLOT() == skillID;
});
});
std::vector<ModifierInstance> modifiers;
@ -515,8 +419,12 @@ void nejlika::NejlikaHooks::InstallHooks()
modifiers.insert(modifiers.end(), skillModifiers.begin(), skillModifiers.end());
}
TriggerParameters params;
params.SkillID = skillID;
params.SelectedBehaviorSlot = itemSlot;
// Upgrades
const auto upgradeModifiers = offenderEntity.TriggerUpgradeItems(UpgradeTriggerType::OnHit);
const auto upgradeModifiers = offenderEntity.TriggerUpgradeItems(UpgradeTriggerType::OnHit, params);
modifiers.insert(modifiers.end(), upgradeModifiers.begin(), upgradeModifiers.end());
@ -535,30 +443,82 @@ void nejlika::NejlikaHooks::InstallHooks()
damageTypes.insert(modifier.GetType());
}
}
// Remove the following: Offensive, Defensive, Health, Armor, Imagination
damageTypes.erase(ModifierType::Offensive);
damageTypes.erase(ModifierType::Defensive);
damageTypes.erase(ModifierType::Health);
damageTypes.erase(ModifierType::Armor);
damageTypes.erase(ModifierType::Imagination);
damageTypes.erase(ModifierType::Damage);
damageTypes.erase(ModifierType::Speed);
damageTypes.erase(ModifierType::AttackSpeed);
damageTypes.erase(ModifierType::Invalid);
uint32_t totalDamage = 0;
std::unordered_map<ModifierType, std::pair<float, float>> durationTypes;
std::unordered_map<ModifierType, float> tmpDamageValues;
for (const auto& type : damageTypes) {
if (nejlika::IsOverTimeType(type)) {
float damageValue = offenderEntity.CalculateModifier(type, modifiers, level);
// Calculate resistance, can't go below 20% of the original damage
const auto resistance = std::max(1 - (damagedEntity.CalculateResistance(type) / 100), 0.2f);
float reductedDamage = damageValue * resistance;
const auto durationType = nejlika::GetDurationType(type);
const auto duration = offenderEntity.CalculateModifier(durationType, modifiers, level);
const auto durationResistance = std::max(1 - (damagedEntity.CalculateResistance(durationType) / 100), 0.2f);
float reductedDuration = duration * durationResistance;
durationTypes[type] = std::make_pair(reductedDamage, reductedDuration);
continue;
}
if (!nejlika::IsNormalDamageType(type)) {
continue;
}
float damageValue = offenderEntity.CalculateModifier(type, modifiers, level);
tmpDamageValues[type] = damageValue;
}
// Type A -> Type B -> (0-100) how much of type A is converted to type B
const auto converationMap = offenderEntity.CalculateDamageConversion(modifiers);
std::unordered_map<ModifierType, float> finalDamageValues;
for (const auto& [typeA, typeBMap] : converationMap) {
const auto& typeAValue = tmpDamageValues.find(typeA);
if (typeAValue == tmpDamageValues.end()) {
continue;
}
const auto& typeAValueFloat = typeAValue->second;
for (const auto& [typeB, conversion] : typeBMap) {
const auto& typeBValue = tmpDamageValues.find(typeB);
if (typeBValue == tmpDamageValues.end()) {
continue;
}
const auto& typeBValueFloat = typeBValue->second;
const auto convertedValue = typeAValueFloat * conversion;
finalDamageValues[typeA] += typeAValueFloat - convertedValue;
finalDamageValues[typeB] += typeBValueFloat + convertedValue;
}
}
for (const auto& [type, damage] : finalDamageValues) {
// Calculate resistance, can't go below 20% of the original damage
const auto resistance = std::max(1 - (damagedEntity.CalculateResistance(type) / 100), 0.2f);
float reductedDamage = damageValue * resistance;
float reductedDamage = damage * resistance;
totalDamage += static_cast<uint32_t>(reductedDamage);
std::cout << "Damage type: " << magic_enum::enum_name(type) << " - " << damageValue << std::endl;
std::cout << "Damage type: " << magic_enum::enum_name(type) << " - " << damage << std::endl;
std::cout << "Resistance: " << resistance << " - " << reductedDamage << std::endl;
std::cout << "Heath left: " << damaged->GetComponent<DestroyableComponent>()->GetHealth() << std::endl;
}
@ -572,22 +532,70 @@ void nejlika::NejlikaHooks::InstallHooks()
if (offenderModifiers == 0) offenderModifiers = 1;
if (defensiveModifiers == 0) defensiveModifiers = 1;
auto ratio = offenderModifiers / defensiveModifiers;
// https://www.grimdawn.com/guide/gameplay/combat/#q20
auto pth = ((
((offenderModifiers / ((defensiveModifiers / 3.5) + offenderModifiers)) * 300) * 0.3
) + (
((((offenderModifiers * 3.25) + 10000) - (defensiveModifiers * 3.25)) / 100) * 0.7)
) - 50;
// Ratio can not ge below 1.05
ratio = std::max(ratio, 1.05f);
if (pth < 60) pth = 60;
// Roll a number between 0 and ratio
float roll = GeneralUtils::GenerateRandomNumber<size_t>() / static_cast<float>(std::numeric_limits<size_t>::max());
float roll = GeneralUtils::GenerateRandomNumber<size_t>(0, std::max(static_cast<int32_t>(pth), 100));
roll *= ratio;
bool isCritical = false;
bool isHit = false;
float damageMultiplier = 1.0f;
std::cout << "Offensive: " << offenderModifiers << " Defensive: " << defensiveModifiers << " Ratio: " << ratio << " Roll: " << roll << std::endl;
if (roll > pth) {
// Miss
isHit = false;
} else {
// Hit
isHit = true;
// If the roll is above 1, the damage is increased by 1+roll, to a maximum of 5x the damage
if (roll > 1) {
roll = std::min(roll, 5.0f);
totalDamage += static_cast<uint32_t>(totalDamage * roll);
if (pth >= 135) {
if (roll <= 134) damageMultiplier = 1.5f;
else if (roll <= 129) damageMultiplier = 1.4f;
else if (roll <= 124) damageMultiplier = 1.3f;
else if (roll <= 119) damageMultiplier = 1.2f;
else if (roll <= 104) damageMultiplier = 1.1f;
} else if (pth >= 130) {
if (roll <= 129) damageMultiplier = 1.4f;
else if (roll <= 124) damageMultiplier = 1.3f;
else if (roll <= 119) damageMultiplier = 1.2f;
else if (roll <= 104) damageMultiplier = 1.1f;
} else if (pth >= 120) {
if (roll <= 119) damageMultiplier = 1.3f;
else if (roll <= 104) damageMultiplier = 1.2f;
else if (roll <= 89) damageMultiplier = 1.1f;
} else if (pth >= 105) {
if (roll <= 104) damageMultiplier = 1.2f;
else if (roll <= 89) damageMultiplier = 1.1f;
} else if (pth >= 90) {
if (roll <= 89) damageMultiplier = 1.1f;
} else if (pth < 70) {
damageMultiplier = pth / 70.0f;
}
if (damageMultiplier > 1.0f) {
isCritical = true;
damageMultiplier *= offenderEntity.CalculateMultiplier(ModifierType::CriticalDamage, modifiers);
}
}
if (isHit) {
// Add a random +5% to the damage
totalDamage += static_cast<uint32_t>(totalDamage * (GeneralUtils::GenerateRandomNumber<int32_t>(0, 5) / 100.0f));
damage = totalDamage;
} else {
damage = totalDamage = 0;
}
if (isCritical) {
totalDamage = static_cast<uint32_t>(totalDamage * damageMultiplier);
const auto effectName = std::to_string(GeneralUtils::GenerateRandomNumber<uint32_t>());
const auto damagedID = damaged->GetObjectID();
@ -599,7 +607,7 @@ void nejlika::NejlikaHooks::InstallHooks()
effectName
);
damaged->AddCallbackTimer(0.5f, [damaged, effectName] () {
damaged->AddCallbackTimer(0.5f, [damaged, effectName]() {
GameMessages::SendStopFXEffect(
damaged,
true,
@ -608,11 +616,6 @@ void nejlika::NejlikaHooks::InstallHooks()
});
}
// Add a random +10% to the damage
totalDamage += static_cast<uint32_t>(totalDamage * (GeneralUtils::GenerateRandomNumber<int32_t>(0, 10) / 100.0f));
damage = totalDamage;
auto attackSpeed = offenderEntity.CalculateModifier(ModifierType::AttackSpeed, modifiers, level);
if (offfendEntity->IsPlayer()) {
@ -634,19 +637,91 @@ void nejlika::NejlikaHooks::InstallHooks()
SEND_PACKET_BROADCAST;
});
}
// Apply over time damage.
// Times are rounded to the nearest 0.5s
for (const auto& [type, damageDuration] : durationTypes) {
if (damageDuration.first == 0) {
continue;
}
const auto duration = static_cast<int32_t>(damageDuration.second * 2);
if (duration == 0) {
continue;
}
const auto damagePerTick = static_cast<int32_t>(damageDuration.first / duration);
auto* destroyable = damaged->GetComponent<DestroyableComponent>();
if (!destroyable) {
continue;
}
for (size_t i = 0; i < duration; i++)
{
damaged->AddCallbackTimer(i * 0.5f, [offender, damaged, damagePerTick]() {
auto* destroyable = damaged->GetComponent<DestroyableComponent>();
if (!destroyable) {
return;
}
destroyable->Damage(offender, damagePerTick, 0, true, true);
});
}
}
/* Moved to DestroyableComponent
std::stringstream damageUIMessage;
auto damagedPosition = damaged->GetPosition();
// Add a slight random offset to the damage position
damagedPosition.x += (rand() % 10 - 5) / 5.0f;
damagedPosition.y += (rand() % 10 - 5) / 5.0f;
damagedPosition.z += (rand() % 10 - 5) / 5.0f;
int colorR = 255;
int colorG = 255;
int colorB = 255;
int colorA = 0;
if (damaged->IsPlayer()) {
// Make the damage red
colorR = 0;
colorG = 255;
colorB = 0;
colorA = 0;
}
const auto damageText = isHit ? std::to_string(totalDamage) : "Miss";
damageUIMessage << 0.0825 << ";" << 0.12 << ";" << damagedPosition.x << ";" << damagedPosition.y + 4.5f << ";" << damagedPosition.z << ";" << 0.1 << ";";
damageUIMessage << 200 << ";" << 200 << ";" << 0.5 << ";" << 1.0 << ";" << damageText << ";" << 4 << ";" << 4 << ";" << colorR << ";" << colorG << ";" << colorB << ";";
damageUIMessage << colorA;
const auto damageUIStr = damageUIMessage.str();
if (damaged->IsPlayer()) {
damaged->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
} else if (offfendEntity->IsPlayer()) {
offfendEntity->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
}*/
};
}
void nejlika::NejlikaHooks::ItemDescription(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.size() < 2) {
if (splitArgs.size() < 3) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid arguments.");
return;
}
auto requestId = GeneralUtils::TryParse<int32_t>(splitArgs[0]).value_or(-1);
if (requestId == -1) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid item ID.");
return;
@ -656,34 +731,70 @@ void nejlika::NejlikaHooks::ItemDescription(Entity* entity, const SystemAddress&
auto itemId = GeneralUtils::TryParse<LWOOBJID>(splitArgs[1]).value_or(LWOOBJID_EMPTY);
if (itemId == LWOOBJID_EMPTY) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid item ID.");
return;
}
const auto itemDataOpt = GetAdditionalItemData(itemId);
auto lot = GeneralUtils::TryParse<LOT>(splitArgs[2]).value_or(0);
if (!itemDataOpt.has_value()) {
if (lot == 0) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid item LOT.");
return;
}
auto& itemDetails = *itemDataOpt.value();
const auto& modifiers = itemDetails.GetModifierInstances();
const auto& names = itemDetails.GetModifierNames();
if (modifiers.empty() && names.empty()) {
return;
}
std::cout << "Item ID: " << itemId << std::endl;
std::cout << "Item LOT: " << lot << std::endl;
std::stringstream name;
std::stringstream desc;
name << "NAME";
if (itemId == LWOOBJID_EMPTY) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid item ID.");
desc << ModifierName::GenerateHtmlString(names) << "\n";
desc << ModifierInstance::GenerateHtmlString(modifiers);
const auto& itemTemplateVec = NejlikaData::GetModifierNameTemplates(ModifierNameType::Object);
const auto itemTemplateIt = std::find_if(itemTemplateVec.begin(), itemTemplateVec.end(), [lot](const auto& it) {
return it.GetLOT() == static_cast<int32_t>(lot);
});
if (itemTemplateIt == itemTemplateVec.end()) {
name << "<font color=\"#D0AB62\">NAME</font>";
desc << "DESC";
} else {
const auto& itemTemplate = *itemTemplateIt;
const auto& modifiers = itemTemplate.GetModifiers();
// Get the entity level
auto* levelProgressionComponent = entity->GetComponent<LevelProgressionComponent>();
if (!levelProgressionComponent) {
return;
}
auto level = levelProgressionComponent->GetLevel();
name << "<font color=\"#D0AB62\">NAME</font>";
desc << ModifierTemplate::GenerateHtmlString(modifiers, level);
}
} else {
const auto itemDataOpt = GetAdditionalItemData(itemId);
if (!itemDataOpt.has_value()) {
name << "<font color=\"#D0AB62\">NAME</font>";
desc << "DESC";
} else {
auto& itemDetails = *itemDataOpt.value();
const auto& modifiers = itemDetails.GetModifierInstances();
const auto& names = itemDetails.GetModifierNames();
if (modifiers.empty() && names.empty()) {
name << "<font color=\"#D0AB62\">NAME</font>";
desc << "DESC";
} else {
name << ModifierName::GenerateHtmlString(names);
desc << ModifierInstance::GenerateHtmlString(modifiers);
}
}
}
std::cout << "Sending item name: " << name.str() << std::endl;
std::cout << "Sending item desc: " << desc.str() << std::endl;
@ -700,4 +811,4 @@ void nejlika::NejlikaHooks::ItemDescription(Entity* entity, const SystemAddress&
GameMessages::SendUIMessageServerToSingleClient(entity, sysAddr, messageName.str(), amfArgs);
std::cout << "Sent item description." << std::endl;
}
}

16
dGame/TriggerParameters.h Normal file
View File

@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
#include "BehaviorSlot.h"
namespace nejlika
{
class TriggerParameters
{
public:
int32_t SkillID = 0;
BehaviorSlot SelectedBehaviorSlot = BehaviorSlot::Invalid;
};
}

View File

@ -141,7 +141,7 @@ float nejlika::UpgradeEffect::CalculateChance(int32_t level) const {
return value;
}
bool nejlika::UpgradeEffect::CheckConditions(LWOOBJID origin) const {
bool nejlika::UpgradeEffect::CheckConditions(LWOOBJID origin, const TriggerParameters& params) const {
auto* entity = Game::entityManager->GetEntity(origin);
if (!entity) {
@ -159,6 +159,15 @@ bool nejlika::UpgradeEffect::CheckConditions(LWOOBJID origin) const {
for (const auto& condition : conditions) {
switch (condition) {
case UpgradeTriggerCondition::PrimaryAbility:
if (params.SelectedBehaviorSlot != BehaviorSlot::Primary) {
return false;
}
break;
case UpgradeTriggerCondition::UseSkill:
if (params.SkillID != equipSkillID) {
return false;
}
case UpgradeTriggerCondition::None:
break;
case UpgradeTriggerCondition::Unarmed:
@ -213,7 +222,7 @@ void nejlika::UpgradeEffect::OnTrigger(LWOOBJID origin) const {
}
}
std::vector<ModifierInstance> nejlika::UpgradeEffect::Trigger(const std::vector<UpgradeEffect>& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin) {
std::vector<ModifierInstance> nejlika::UpgradeEffect::Trigger(const std::vector<UpgradeEffect>& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params) {
std::vector<ModifierInstance> result;
for (const auto& modifier : modifiers) {
@ -221,7 +230,7 @@ std::vector<ModifierInstance> nejlika::UpgradeEffect::Trigger(const std::vector<
continue;
}
if (!modifier.CheckConditions(origin)) {
if (!modifier.CheckConditions(origin, params)) {
continue;
}
@ -272,7 +281,7 @@ void nejlika::UpgradeEffect::AddSkill(LWOOBJID origin) const {
}
if (equipSkillID != 0) {
inventory->SetSkill(equipSkillID);
inventory->AddSkill(equipSkillID);
}
}
@ -294,6 +303,6 @@ void nejlika::UpgradeEffect::RemoveSkill(LWOOBJID origin) const {
}
if (equipSkillID != 0) {
inventory->UnsetSkill(equipSkillID);
inventory->RemoveSkill(equipSkillID);
}
}

View File

@ -4,6 +4,7 @@
#include "UpgradeTriggerType.h"
#include "UpgradeTriggerCondition.h"
#include <InventoryComponent.h>
#include "TriggerParameters.h"
#include <dCommonVars.h>
@ -23,11 +24,11 @@ public:
float CalculateChance(int32_t level) const;
bool CheckConditions(LWOOBJID origin) const;
bool CheckConditions(LWOOBJID origin, const TriggerParameters& params) const;
void OnTrigger(LWOOBJID origin) const;
static std::vector<ModifierInstance> Trigger(const std::vector<UpgradeEffect>& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin);
static std::vector<ModifierInstance> Trigger(const std::vector<UpgradeEffect>& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params);
// Getters

View File

@ -1,5 +1,7 @@
#include "UpgradeTemplate.h"
#include "NejlikaData.h"
using namespace nejlika;
nejlika::UpgradeTemplate::UpgradeTemplate(const nlohmann::json& json)
@ -29,21 +31,49 @@ nlohmann::json nejlika::UpgradeTemplate::ToJson() const
void nejlika::UpgradeTemplate::Load(const nlohmann::json& json)
{
name = json["name"].get<std::string>();
lot = json["lot"].get<int32_t>();
maxLevel = json["max-level"].contains("max-level") ? json["max-level"].get<int32_t>() : 1;
if (json["lot"].is_string()) {
lot = NejlikaData::GetLookup().GetValue(json["lot"].get<std::string>());
}
else {
lot = json["lot"].get<int32_t>();
}
maxLevel = json.contains("max-level") ? json["max-level"].get<int32_t>() : 1;
passives.clear();
for (const auto& passive : json["passives"]) {
UpgradeEffect effect(passive);
passives.push_back(effect);
if (json.contains("modifiers")) {
for (const auto& modifier : json["modifiers"]) {
ModifierTemplate modTemplate(modifier);
modifiers.push_back(modTemplate);
}
}
if (json.contains("passives")) {
for (const auto& passive : json["passives"]) {
UpgradeEffect effect(passive);
passives.push_back(effect);
}
}
}
std::vector<ModifierInstance> nejlika::UpgradeTemplate::Trigger(int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin) const {
std::vector<ModifierInstance> nejlika::UpgradeTemplate::Trigger(int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params) const {
level = std::min(level, maxLevel);
return UpgradeEffect::Trigger(passives, level, triggerType, origin);
return UpgradeEffect::Trigger(passives, level, triggerType, origin, params);
}
std::vector<ModifierInstance> nejlika::UpgradeTemplate::GenerateModifiers(int32_t level) const {
level = std::min(level, maxLevel);
std::vector<ModifierInstance> result;
for (const auto& modifier : modifiers) {
auto instances = modifier.GenerateModifiers(level);
result.insert(result.end(), instances.begin(), instances.end());
}
return result;
}
void nejlika::UpgradeTemplate::AddSkills(LWOOBJID origin) const {

View File

@ -7,6 +7,7 @@
#include "json.hpp"
#include "UpgradeEffect.h"
#include "TriggerParameters.h"
namespace nejlika
{
@ -26,8 +27,11 @@ public:
int32_t GetLot() const { return lot; }
int32_t GetMaxLevel() const { return maxLevel; }
const std::vector<UpgradeEffect>& GetPassives() const { return passives; }
const std::vector<ModifierTemplate>& GetModifiers() const { return modifiers; }
std::vector<ModifierInstance> Trigger(int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin) const;
std::vector<ModifierInstance> Trigger(int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params) const;
std::vector<ModifierInstance> GenerateModifiers(int32_t level) const;
void AddSkills(LWOOBJID origin) const;
void RemoveSkills(LWOOBJID origin) const;
@ -37,6 +41,7 @@ private:
int32_t lot = 0;
int32_t maxLevel = 0;
std::vector<UpgradeEffect> passives;
std::vector<ModifierTemplate> modifiers;
};

View File

@ -11,7 +11,9 @@ enum class UpgradeTriggerCondition
Unarmed,
Melee,
TwoHanded,
Shield
Shield,
PrimaryAbility,
UseSkill
};
}

View File

@ -556,7 +556,7 @@ void DestroyableComponent::Repair(const uint32_t armor) {
}
void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32_t skillID, bool echo) {
void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32_t skillID, bool echo, bool isDamageOverTime) {
if (GetHealth() <= 0) {
return;
}
@ -572,7 +572,9 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32
return;
}
OnDamageCalculation(m_Parent, source, skillID, damage);
if (!isDamageOverTime) {
OnDamageCalculation(m_Parent, source, skillID, damage);
}
// If this entity has damage reduction, reduce the damage to a minimum of 1
if (m_DamageReduction > 0 && damage > 0) {
@ -641,6 +643,42 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32
cb(attacker);
}
std::stringstream damageUIMessage;
auto damagedPosition = m_Parent->GetPosition();
// Add a slight random offset to the damage position
damagedPosition.x += (rand() % 10 - 5) / 5.0f;
damagedPosition.y += (rand() % 10 - 5) / 5.0f;
damagedPosition.z += (rand() % 10 - 5) / 5.0f;
int colorR = 255;
int colorG = 255;
int colorB = 255;
int colorA = 0;
if (m_Parent->IsPlayer()) {
// Make the damage red
colorR = 0;
colorG = 255;
colorB = 0;
colorA = 0;
}
const auto damageText = damage != 0 ? std::to_string(damage) : "Miss";
damageUIMessage << 0.0825 << ";" << 0.12 << ";" << damagedPosition.x << ";" << damagedPosition.y + 4.5f << ";" << damagedPosition.z << ";" << 0.1 << ";";
damageUIMessage << 200 << ";" << 200 << ";" << 0.5 << ";" << 1.0 << ";" << damageText << ";" << 4 << ";" << 4 << ";" << colorR << ";" << colorG << ";" << colorB << ";";
damageUIMessage << colorA;
const auto damageUIStr = damageUIMessage.str();
if (m_Parent->IsPlayer()) {
m_Parent->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
} else if (attacker->IsPlayer()) {
attacker->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
}
if (health != 0) {
auto* combatComponent = m_Parent->GetComponent<BaseCombatAIComponent>();

View File

@ -384,8 +384,9 @@ public:
* @param source the attacker that caused this damage
* @param skillID the skill that damaged this entity
* @param echo whether or not to serialize the damage
* @param isDamageOverTime whether or not this damage is over time
*/
void Damage(uint32_t damage, LWOOBJID source, uint32_t skillID = 0, bool echo = true);
void Damage(uint32_t damage, LWOOBJID source, uint32_t skillID = 0, bool echo = true, bool isDamageOverTime = false);
/**
* Smashes this entity, notifying all clients

View File

@ -1,6 +1,7 @@
#include "InventoryComponent.h"
#include <sstream>
#include <algorithm>
#include "Entity.h"
#include "Item.h"
@ -43,6 +44,7 @@ Observable<InventoryComponent*, Item*> InventoryComponent::OnItemDestroyed;
Observable<InventoryComponent*, Item*> InventoryComponent::OnItemEquipped;
Observable<InventoryComponent*, Item*> InventoryComponent::OnItemUnequipped;
Observable<InventoryComponent*, Item*> InventoryComponent::OnItemLoaded;
Observable<InventoryComponent*, Item*> InventoryComponent::OnCountChanged;
InventoryComponent::InventoryComponent(Entity* parent) : Component(parent) {
this->m_Dirty = true;
@ -1131,27 +1133,13 @@ void InventoryComponent::AddItemSkills(const LOT lot) {
return;
}
const auto skill = FindSkill(lot);
if (m_PrimarySkill != 0) {
GameMessages::SendRemoveSkill(m_Parent, m_PrimarySkill);
}
const auto index = m_Skills.find(slot);
m_PrimarySkill = FindSkill(lot);
if (index != m_Skills.end()) {
const auto old = index->second;
if (!old.empty()) {
const auto firstElem = *old.begin();
GameMessages::SendRemoveSkill(m_Parent, firstElem);
}
}
m_Skills.erase(slot);
if (skill != 0) {
m_Skills.insert_or_assign(slot, std::set<uint32_t>{ skill });
GameMessages::SendAddSkill(m_Parent, skill, slot);
}
GameMessages::SendAddSkill(m_Parent, m_PrimarySkill, slot);
}
void InventoryComponent::RemoveItemSkills(const LOT lot) {
@ -1163,27 +1151,11 @@ void InventoryComponent::RemoveItemSkills(const LOT lot) {
return;
}
const auto index = m_Skills.find(slot);
GameMessages::SendRemoveSkill(m_Parent, m_PrimarySkill);
if (index == m_Skills.end()) {
return;
}
m_PrimarySkill = 1;
const auto old = index->second;
if (!old.empty()) {
const auto firstElem = *old.begin();
GameMessages::SendRemoveSkill(m_Parent, firstElem);
}
m_Skills.erase(slot);
if (slot == BehaviorSlot::Primary) {
m_Skills.insert_or_assign(BehaviorSlot::Primary, std::set<uint32_t>{ 1 });
GameMessages::SendAddSkill(m_Parent, 1, BehaviorSlot::Primary);
}
GameMessages::SendAddSkill(m_Parent, m_PrimarySkill, BehaviorSlot::Primary);
}
void InventoryComponent::TriggerPassiveAbility(PassiveAbilityTrigger trigger, Entity* target) {
@ -1360,6 +1332,71 @@ void InventoryComponent::SetNPCItems(const std::vector<LOT>& items) {
Game::entityManager->SerializeEntity(m_Parent);
}
bool InventoryComponent::AddSkill(uint32_t skillId) {
if (std::find(m_Skills.begin(), m_Skills.end(), skillId) != m_Skills.end()) {
return false;
}
m_Skills.push_back(skillId);
UpdateSkills();
return true;
}
bool InventoryComponent::RemoveSkill(uint32_t skillId) {
const auto index = std::find(m_Skills.begin(), m_Skills.end(), skillId);
if (index == m_Skills.end()) {
return false;
}
m_Skills.erase(index);
UpdateSkills();
return true;
}
void InventoryComponent::UpdateSkills() {
// There are two skills active at the same time. If the rotation index is greater than the amount of skills / 2, set it to the max number is can be.
// This is to prevent the rotation index from going out of bounds.
if (m_SkillRotationIndex * 2 >= m_Skills.size()) {
m_SkillRotationIndex = 0;
}
const auto startIndex = m_SkillRotationIndex * 2;
const auto activeSkillA = m_Skills.size() > startIndex ? m_Skills[startIndex] : 0;
const auto activeSkillB = m_Skills.size() > startIndex + 1 ? m_Skills[startIndex + 1] : 0;
std::cout << "Skill rotation index: " << m_SkillRotationIndex << " Active skills: " << activeSkillA << " " << activeSkillB << "\n";
GameMessages::SendRemoveSkill(m_Parent, m_ActiveSkills.first);
GameMessages::SendRemoveSkill(m_Parent, m_ActiveSkills.second);
m_ActiveSkills = { activeSkillA, activeSkillB };
if (activeSkillA != 0) {
GameMessages::SendAddSkill(m_Parent, activeSkillA, BehaviorSlot::Head);
}
if (activeSkillB != 0) {
GameMessages::SendAddSkill(m_Parent, activeSkillB, BehaviorSlot::Offhand);
}
}
void InventoryComponent::RotateSkills() {
m_SkillRotationIndex++;
if (m_SkillRotationIndex * 2 >= m_Skills.size()) {
m_SkillRotationIndex = 0;
}
UpdateSkills();
}
InventoryComponent::~InventoryComponent() {
for (const auto& inventory : m_Inventories) {
delete inventory.second;
@ -1614,60 +1651,4 @@ void InventoryComponent::UpdatePetXml(tinyxml2::XMLDocument& document) {
}
bool InventoryComponent::SetSkill(int32_t slot, uint32_t skillId) {
BehaviorSlot behaviorSlot = BehaviorSlot::Invalid;
if (slot == 1) behaviorSlot = BehaviorSlot::Primary;
else if (slot == 2) behaviorSlot = BehaviorSlot::Offhand;
else if (slot == 3) behaviorSlot = BehaviorSlot::Neck;
else if (slot == 4) behaviorSlot = BehaviorSlot::Head;
else if (slot == 5) behaviorSlot = BehaviorSlot::Consumable;
else return false;
return SetSkill(behaviorSlot, skillId);
}
bool InventoryComponent::SetSkill(BehaviorSlot slot, uint32_t skillId) {
if (skillId == 0) return false;
const auto index = m_Skills.find(slot);
if (index == m_Skills.end()) {
m_Skills.insert_or_assign(slot, std::set<uint32_t>{ skillId });
} else {
auto& existing = index->second;
existing.insert(skillId);
}
return true;
}
void InventoryComponent::UnsetSkill(uint32_t skillId) {
for (auto& pair : m_Skills) {
auto& skills = pair.second;
skills.erase(skillId);
}
}
void InventoryComponent::SetSkill(uint32_t skillId) {
UnsetSkill(skillId);
const auto& slotA = m_Skills.find(BehaviorSlot::Head);
const auto& slotB = m_Skills.find(BehaviorSlot::Neck);
const auto& slotC = m_Skills.find(BehaviorSlot::Offhand);
// Pick the first one which has less than 3 skills
std::set<uint32_t>* slot = nullptr;
if (slotA == m_Skills.end() || slotA->second.size() < 3) {
slot = &m_Skills[BehaviorSlot::Head];
} else if (slotB == m_Skills.end() || slotB->second.size() < 3) {
slot = &m_Skills[BehaviorSlot::Neck];
} else if (slotC == m_Skills.end() || slotC->second.size() < 3) {
slot = &m_Skills[BehaviorSlot::Offhand];
}
if (slot == nullptr) {
return;
}
slot->insert(skillId);
}

View File

@ -368,12 +368,14 @@ public:
*/
void UnequipScripts(Item* unequippedItem);
const std::map<BehaviorSlot, std::set<uint32_t>>& GetSkills(){ return m_Skills; };
uint32_t GetPrimarySkill() const { return m_PrimarySkill; }
bool SetSkill(int32_t slot, uint32_t skillId);
bool SetSkill(BehaviorSlot slot, uint32_t skillId);
void UnsetSkill(uint32_t skillId);
void SetSkill(uint32_t skillId);
const std::vector<uint32_t>& GetSkills(){ return m_Skills; };
bool AddSkill(uint32_t skillId);
bool RemoveSkill(uint32_t skillId);
void RotateSkills();
void UpdateSkills();
~InventoryComponent() override;
@ -382,6 +384,7 @@ public:
static Observable<InventoryComponent*, Item*> OnItemEquipped;
static Observable<InventoryComponent*, Item*> OnItemUnequipped;
static Observable<InventoryComponent*, Item*> OnItemLoaded;
static Observable<InventoryComponent*, Item*> OnCountChanged;
private:
/**
@ -391,10 +394,16 @@ private:
std::map<BehaviorSlot, uint32_t> m_ActivatorSkills;
uint32_t m_PrimarySkill = 0;
/**
* The skills that this entity currently has active
*/
std::map<BehaviorSlot, std::set<uint32_t>> m_Skills;
std::vector<uint32_t> m_Skills;
std::pair<uint32_t, uint32_t> m_ActiveSkills;
uint32_t m_SkillRotationIndex = 0;
/**
* The pets this entity has, mapped by object ID and pet info

View File

@ -201,6 +201,8 @@ void Item::SetCount(const uint32_t value, const bool silent, const bool disassem
count = value;
InventoryComponent::OnCountChanged(inventory->GetComponent(), this);
if (count == 0) {
RemoveFromInventory();
}

View File

@ -1347,23 +1347,17 @@ namespace DEVGMCommands {
void SetSkillSlot(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.size() < 2) return;
if (splitArgs.size() < 1) return;
auto* const inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent) {
const auto slot = GeneralUtils::TryParse<BehaviorSlot>(splitArgs[0]);
if (!slot) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting slot.");
const auto skillId = GeneralUtils::TryParse<uint32_t>(splitArgs[0]);
if (!skillId) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting skill.");
return;
} else {
const auto skillId = GeneralUtils::TryParse<uint32_t>(splitArgs[1]);
if (!skillId) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting skill.");
return;
} else {
if (inventoryComponent->SetSkill(slot.value(), skillId.value())) ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot successfully");
else ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot failed");
}
if (inventoryComponent->AddSkill(skillId.value())) ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot successfully");
else ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot failed");
}
}
}