From 5e3312850c5725135faa1217951b9c5be444a8e8 Mon Sep 17 00:00:00 2001 From: wincent Date: Sat, 6 Jul 2024 00:02:30 +0200 Subject: [PATCH] Refactor damage calculations and add additional modifiers --- dGame/AdditionalEntityData.cpp | 115 +++- dGame/AdditionalEntityData.h | 21 + dGame/EntityTemplate.cpp | 3 +- dGame/ModifierInstance.cpp | 2 + dGame/ModifierInstance.h | 9 +- dGame/ModifierTemplate.cpp | 170 +++++- dGame/ModifierTemplate.h | 16 + dGame/ModifierType.cpp | 153 ++++- dGame/ModifierType.h | 57 +- dGame/NejlikaData.cpp | 14 +- dGame/NejlikaHooks.cpp | 545 +++++++++++------- dGame/TriggerParameters.h | 16 + dGame/UpgradeEffect.cpp | 19 +- dGame/UpgradeEffect.h | 5 +- dGame/UpgradeTemplate.cpp | 44 +- dGame/UpgradeTemplate.h | 7 +- dGame/UpgradeTriggerCondition.h | 4 +- dGame/dComponents/DestroyableComponent.cpp | 42 +- dGame/dComponents/DestroyableComponent.h | 3 +- dGame/dComponents/InventoryComponent.cpp | 169 +++--- dGame/dComponents/InventoryComponent.h | 21 +- dGame/dInventory/Item.cpp | 2 + .../SlashCommands/DEVGMCommands.cpp | 18 +- 23 files changed, 1061 insertions(+), 394 deletions(-) create mode 100644 dGame/TriggerParameters.h diff --git a/dGame/AdditionalEntityData.cpp b/dGame/AdditionalEntityData.cpp index dfc79fe2..2b996a41 100644 --- a/dGame/AdditionalEntityData.cpp +++ b/dGame/AdditionalEntityData.cpp @@ -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 damageTypes = { - ModifierType::Slashing, - ModifierType::Piercing, - ModifierType::Bludgeoning, + static const std::unordered_set 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 nejlika::AdditionalEntityData::TriggerUpgradeItems(UpgradeTriggerType triggerType) { +std::vector nejlika::AdditionalEntityData::TriggerUpgradeItems(UpgradeTriggerType triggerType, const TriggerParameters& params) { auto* entity = Game::entityManager->GetEntity(id); if (entity == nullptr) { @@ -155,7 +165,7 @@ std::vector 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 nejlika::AdditionalEntityData::TriggerUpgradeItems return result; } +std::vector 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& additionalModifiers) const { + return 1 + CalculateModifier(type, additionalModifiers, ModifierOperator::Multiplicative, false); +} + +std::unordered_map> nejlika::AdditionalEntityData::CalculateDamageConversion(std::vector& additionalModifiers) const { + std::unordered_map> 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(CalculateModifier(ModifierType::Health, level))); diff --git a/dGame/AdditionalEntityData.h b/dGame/AdditionalEntityData.h index 77e7503e..1636fb98 100644 --- a/dGame/AdditionalEntityData.h +++ b/dGame/AdditionalEntityData.h @@ -8,6 +8,7 @@ #include "ModifierInstance.h" #include "EntityTemplate.h" #include "UpgradeTriggerType.h" +#include "TriggerParameters.h" #include @@ -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& additionalModifiers) const; + + /** + * @brief Calculate damage conversation mapping. + * + * @param additionalModifiers Additional modifiers to apply. + * + * @return The damage conversion mapping. + */ + std::unordered_map> CalculateDamageConversion(std::vector& additionalModifiers) const; + void ApplyToEntity(); void CheckForRescale(AdditionalEntityData* other); @@ -57,6 +76,8 @@ public: void RemoveUpgradeItem(LWOOBJID id) { upgradeItems.erase(id); } + std::vector TriggerUpgradeItems(UpgradeTriggerType triggerType, const TriggerParameters& params); + std::vector TriggerUpgradeItems(UpgradeTriggerType triggerType); void InitializeSkills(); diff --git a/dGame/EntityTemplate.cpp b/dGame/EntityTemplate.cpp index e9d4f29e..f5d9dfa3 100644 --- a/dGame/EntityTemplate.cpp +++ b/dGame/EntityTemplate.cpp @@ -66,7 +66,8 @@ std::vector nejlika::EntityTemplate::GenerateModifier scaler.isResistance, ModifierCategory::Player, 0, - "" + "", + ModifierType::Invalid ); modifiers.push_back(modifier); diff --git a/dGame/ModifierInstance.cpp b/dGame/ModifierInstance.cpp index 48ea2e3d..1fca4cf6 100644 --- a/dGame/ModifierInstance.cpp +++ b/dGame/ModifierInstance.cpp @@ -5,6 +5,7 @@ nejlika::ModifierInstance::ModifierInstance(const nlohmann::json& config) { type = magic_enum::enum_cast(config["type"].get()).value_or(ModifierType::Invalid); + convertTo = magic_enum::enum_cast(config["convert-to"].get()).value_or(ModifierType::Invalid); value = config["value"].get(); 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; diff --git a/dGame/ModifierInstance.h b/dGame/ModifierInstance.h index c61ac99e..27b41556 100644 --- a/dGame/ModifierInstance.h +++ b/dGame/ModifierInstance.h @@ -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; diff --git a/dGame/ModifierTemplate.cpp b/dGame/ModifierTemplate.cpp index a26d8292..9ba5340a 100644 --- a/dGame/ModifierTemplate.cpp +++ b/dGame/ModifierTemplate.cpp @@ -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(config["convert-to"].get()).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()); + } } 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 nejlika::ModifierTemplate::GenerateModifiers(int32 return modifiers; } +std::string nejlika::ModifierTemplate::GenerateHtmlString(const std::vector& modifiers, int32_t level) { + std::stringstream ss; + + // target -> resistance -> op -> type -> (min, max) + std::unordered_map>>>> 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 << "\nPets:\n"; + } + + for (const auto& resistance : target.second) { + + ss << "\n"; + + ss << ((resistance.first) ? "Resistances" : "Modifiers"); + + ss << ":\n"; + + for (const auto& math : resistance.second) { + for (const auto& modifier : math.second) { + ss << ""; + + ss << magic_enum::enum_name(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 << "\n"; + } + } + } + } + + return ss.str(); +} + std::optional 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 nejlika::ModifierTemplate::GenerateModifier(Modi float value = GeneralUtils::GenerateRandomNumber(scale.GetMin(), scale.GetMax()); - return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType); + return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo); } diff --git a/dGame/ModifierTemplate.h b/dGame/ModifierTemplate.h index 6474e5f0..62de764e 100644 --- a/dGame/ModifierTemplate.h +++ b/dGame/ModifierTemplate.h @@ -45,6 +45,8 @@ public: const std::vector& GetTypes() const { return types; } + ModifierType GetConvertTo() const { return convertTo; } + ModifierTemplateSelector GetSelector() const { return selector; } const std::vector& GetScales() const { return scales; } @@ -59,6 +61,8 @@ public: void SetTypes(const std::vector& types) { this->types = types; } + void SetConvertTo(ModifierType convertTo) { this->convertTo = convertTo; } + void SetSelector(ModifierTemplateSelector selector) { this->selector = selector; } void SetScales(const std::vector& 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& modifiers, int32_t level); + private: std::optional GenerateModifier(ModifierType type, int32_t level) const; std::vector types; + ModifierType convertTo; ModifierTemplateSelector selector; std::vector scales; + std::vector polynomial; ModifierCategory category; ModifierOperator operatorType; bool isResistance; diff --git a/dGame/ModifierType.cpp b/dGame/ModifierType.cpp index e56d8e23..4a1bce4b 100644 --- a/dGame/ModifierType.cpp +++ b/dGame/ModifierType.cpp @@ -1,5 +1,6 @@ #include "ModifierType.h" #include +#include using namespace nejlika; @@ -12,14 +13,106 @@ const std::unordered_map 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 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 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 normalDamageTypes = { + ModifierType::Physical, + ModifierType::Pierce, + ModifierType::Vitality, + ModifierType::Fire, + ModifierType::Cold, + ModifierType::Lightning, + ModifierType::Corruption, + ModifierType::Psychic, + ModifierType::Acid +}; + +const std::unordered_map 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 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 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; -} \ No newline at end of file +} + +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(); +} diff --git a/dGame/ModifierType.h b/dGame/ModifierType.h index f4445899..0c8e1c42 100644 --- a/dGame/ModifierType.h +++ b/dGame/ModifierType.h @@ -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); + } diff --git a/dGame/NejlikaData.cpp b/dGame/NejlikaData.cpp index 424113a4..9bc7f9a8 100644 --- a/dGame/NejlikaData.cpp +++ b/dGame/NejlikaData.cpp @@ -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); - } } diff --git a/dGame/NejlikaHooks.cpp b/dGame/NejlikaHooks.cpp index 7bcaef39..099d6b3b 100644 --- a/dGame/NejlikaHooks.cpp +++ b/dGame/NejlikaHooks.cpp @@ -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(); @@ -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(); + + destroyable->SetMaxImagination(6); + destroyable->SetImagination(6); + } } + + auto* inventoryComponent = entity->GetComponent(); + + 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()) ); - }); + }); } - }; + }; 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(); 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(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(0)); - } - - if (entity->HasVar(u"skill-cast-slot")) { - BehaviorSlot slot = static_cast(entity->GetVar(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 slotOrder = { - BehaviorSlot::Head, - BehaviorSlot::Offhand, - BehaviorSlot::Neck - }; - - std::set 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(); - - entity->SetVar(u"skill-cast", randomNumber); - entity->SetVar(u"skill-cast-slot", static_cast(slot)); - - entity->AddCallbackTimer(1.0f, [entity, randomNumber, primaryTrigger, secondaryTrigger, tertiaryTrigger]() { - if (!entity->HasVar(u"skill-cast")) { - return; - } - - const auto currentRandom = entity->GetVar(u"skill-cast"); - - if (currentRandom != randomNumber) { - return; - } - - entity->SetVar(u"skill-cast", static_cast(0)); - - LOG("Restoring triggers via timeout"); - - // Remove the skills - auto* inventoryComponent = entity->GetComponent(); - - if (!inventoryComponent) { - return; - } - - BehaviorSlot slot = static_cast(entity->GetVar(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(); 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(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(info.itemType)); - - std::cout << "Comparing slots: " << static_cast(itemBehaviorSlot) << " - " << static_cast(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 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> durationTypes; + std::unordered_map 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 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(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()->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() / static_cast(std::numeric_limits::max()); + float roll = GeneralUtils::GenerateRandomNumber(0, std::max(static_cast(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(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(totalDamage * (GeneralUtils::GenerateRandomNumber(0, 5) / 100.0f)); + + damage = totalDamage; + } else { + damage = totalDamage = 0; + } + + if (isCritical) { + totalDamage = static_cast(totalDamage * damageMultiplier); const auto effectName = std::to_string(GeneralUtils::GenerateRandomNumber()); 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(totalDamage * (GeneralUtils::GenerateRandomNumber(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(damageDuration.second * 2); + + if (duration == 0) { + continue; + } + + const auto damagePerTick = static_cast(damageDuration.first / duration); + + auto* destroyable = damaged->GetComponent(); + + if (!destroyable) { + continue; + } + + for (size_t i = 0; i < duration; i++) + { + damaged->AddCallbackTimer(i * 0.5f, [offender, damaged, damagePerTick]() { + auto* destroyable = damaged->GetComponent(); + + 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(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS); + } else if (offfendEntity->IsPlayer()) { + offfendEntity->SetNetworkVar(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(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(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(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(lot); + }); + + if (itemTemplateIt == itemTemplateVec.end()) { + name << "NAME"; + desc << "DESC"; + } else { + const auto& itemTemplate = *itemTemplateIt; + + const auto& modifiers = itemTemplate.GetModifiers(); + + // Get the entity level + auto* levelProgressionComponent = entity->GetComponent(); + + if (!levelProgressionComponent) { + return; + } + + auto level = levelProgressionComponent->GetLevel(); + + name << "NAME"; + desc << ModifierTemplate::GenerateHtmlString(modifiers, level); + } + } else { + const auto itemDataOpt = GetAdditionalItemData(itemId); + + if (!itemDataOpt.has_value()) { + name << "NAME"; + desc << "DESC"; + } else { + auto& itemDetails = *itemDataOpt.value(); + + const auto& modifiers = itemDetails.GetModifierInstances(); + const auto& names = itemDetails.GetModifierNames(); + + if (modifiers.empty() && names.empty()) { + name << "NAME"; + 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; -} \ No newline at end of file +} diff --git a/dGame/TriggerParameters.h b/dGame/TriggerParameters.h new file mode 100644 index 00000000..0a5106b0 --- /dev/null +++ b/dGame/TriggerParameters.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "BehaviorSlot.h" + +namespace nejlika +{ + +class TriggerParameters +{ +public: + int32_t SkillID = 0; + BehaviorSlot SelectedBehaviorSlot = BehaviorSlot::Invalid; +}; + +} diff --git a/dGame/UpgradeEffect.cpp b/dGame/UpgradeEffect.cpp index 154b2713..5fbc04f1 100644 --- a/dGame/UpgradeEffect.cpp +++ b/dGame/UpgradeEffect.cpp @@ -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 nejlika::UpgradeEffect::Trigger(const std::vector& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin) { +std::vector nejlika::UpgradeEffect::Trigger(const std::vector& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params) { std::vector result; for (const auto& modifier : modifiers) { @@ -221,7 +230,7 @@ std::vector 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); } } diff --git a/dGame/UpgradeEffect.h b/dGame/UpgradeEffect.h index b3f85fae..1cbb0f55 100644 --- a/dGame/UpgradeEffect.h +++ b/dGame/UpgradeEffect.h @@ -4,6 +4,7 @@ #include "UpgradeTriggerType.h" #include "UpgradeTriggerCondition.h" #include +#include "TriggerParameters.h" #include @@ -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 Trigger(const std::vector& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin); + static std::vector Trigger(const std::vector& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params); // Getters diff --git a/dGame/UpgradeTemplate.cpp b/dGame/UpgradeTemplate.cpp index 9b91e467..777369ca 100644 --- a/dGame/UpgradeTemplate.cpp +++ b/dGame/UpgradeTemplate.cpp @@ -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(); - lot = json["lot"].get(); - maxLevel = json["max-level"].contains("max-level") ? json["max-level"].get() : 1; + if (json["lot"].is_string()) { + lot = NejlikaData::GetLookup().GetValue(json["lot"].get()); + } + else { + lot = json["lot"].get(); + } + maxLevel = json.contains("max-level") ? json["max-level"].get() : 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 nejlika::UpgradeTemplate::Trigger(int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin) const { +std::vector 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 nejlika::UpgradeTemplate::GenerateModifiers(int32_t level) const { + level = std::min(level, maxLevel); + + std::vector 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 { diff --git a/dGame/UpgradeTemplate.h b/dGame/UpgradeTemplate.h index b18d87c2..eca351a0 100644 --- a/dGame/UpgradeTemplate.h +++ b/dGame/UpgradeTemplate.h @@ -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& GetPassives() const { return passives; } + const std::vector& GetModifiers() const { return modifiers; } - std::vector Trigger(int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin) const; + std::vector Trigger(int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params) const; + + std::vector 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 passives; + std::vector modifiers; }; diff --git a/dGame/UpgradeTriggerCondition.h b/dGame/UpgradeTriggerCondition.h index 37995628..2628e897 100644 --- a/dGame/UpgradeTriggerCondition.h +++ b/dGame/UpgradeTriggerCondition.h @@ -11,7 +11,9 @@ enum class UpgradeTriggerCondition Unarmed, Melee, TwoHanded, - Shield + Shield, + PrimaryAbility, + UseSkill }; } \ No newline at end of file diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 24cc80cf..4105598b 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -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(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS); + } else if (attacker->IsPlayer()) { + attacker->SetNetworkVar(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS); + } + if (health != 0) { auto* combatComponent = m_Parent->GetComponent(); diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 3c28086c..51d79e7f 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -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 diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 58f89d49..0ca79738 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -1,6 +1,7 @@ #include "InventoryComponent.h" #include +#include #include "Entity.h" #include "Item.h" @@ -43,6 +44,7 @@ Observable InventoryComponent::OnItemDestroyed; Observable InventoryComponent::OnItemEquipped; Observable InventoryComponent::OnItemUnequipped; Observable InventoryComponent::OnItemLoaded; +Observable 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{ 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{ 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& 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{ 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* 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); -} - diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index bdf7fa7d..cd8b2ae0 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -368,12 +368,14 @@ public: */ void UnequipScripts(Item* unequippedItem); - const std::map>& 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& 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 OnItemEquipped; static Observable OnItemUnequipped; static Observable OnItemLoaded; + static Observable OnCountChanged; private: /** @@ -391,10 +394,16 @@ private: std::map m_ActivatorSkills; + uint32_t m_PrimarySkill = 0; + /** * The skills that this entity currently has active */ - std::map> m_Skills; + std::vector m_Skills; + + std::pair m_ActiveSkills; + + uint32_t m_SkillRotationIndex = 0; /** * The pets this entity has, mapped by object ID and pet info diff --git a/dGame/dInventory/Item.cpp b/dGame/dInventory/Item.cpp index 183e4122..e94c098b 100644 --- a/dGame/dInventory/Item.cpp +++ b/dGame/dInventory/Item.cpp @@ -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(); } diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index 00add608..155e4114 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -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(); if (inventoryComponent) { - const auto slot = GeneralUtils::TryParse(splitArgs[0]); - if (!slot) { - ChatPackets::SendSystemMessage(sysAddr, u"Error getting slot."); + const auto skillId = GeneralUtils::TryParse(splitArgs[0]); + if (!skillId) { + ChatPackets::SendSystemMessage(sysAddr, u"Error getting skill."); return; } else { - const auto skillId = GeneralUtils::TryParse(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"); } } }