diff --git a/dGame/AdditionalEntityData.cpp b/dGame/AdditionalEntityData.cpp index 2b996a41..52f27d11 100644 --- a/dGame/AdditionalEntityData.cpp +++ b/dGame/AdditionalEntityData.cpp @@ -33,7 +33,7 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, Modifi return total; } -float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::vector& additionalModifiers, ModifierOperator op, bool resistance) const { +float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, const std::vector& additionalModifiers, ModifierOperator op, bool resistance) const { float total = 0; for (const auto& modifier : additionalModifiers) { @@ -71,8 +71,6 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, int32_ float multiplicative = CalculateModifier(type, ModifierOperator::Multiplicative, false); - std::cout << "Scaler: " << scaler << " Additive: " << additive << " Multiplicative: " << multiplicative << std::endl; - return (scaler + additive) * (1 + multiplicative / 100); } @@ -80,7 +78,7 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type) const return CalculateModifier(type, level); } -float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::vector& additionalModifiers, int32_t level) const +float nejlika::AdditionalEntityData::CalculateFinalModifier(ModifierType type, const std::vector& additionalModifiers, int32_t level) const { const auto templateDataOpt = NejlikaData::GetEntityTemplate(lot); @@ -106,6 +104,42 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::v ModifierType::Lightning }; + if (type == ModifierType::Health) { + if (lot == 1) additive += 25; + additive += CalculateModifier(ModifierType::Physique, additionalModifiers, ModifierOperator::Additive, false) * 2.5f; + additive += CalculateModifier(ModifierType::Cunning, additionalModifiers, ModifierOperator::Additive, false) * 1.0f; + additive += CalculateModifier(ModifierType::Spirit, additionalModifiers, ModifierOperator::Additive, false) * 1.0f; + } + else if (type == ModifierType::Imagination) { + additive += CalculateModifier(ModifierType::Spirit, additionalModifiers, ModifierOperator::Additive, false) * 2.0f; + } + else if (type == ModifierType::Seperation || type == ModifierType::InternalDisassembly || type == ModifierType::Physical) { + multiplicative += CalculateModifier(ModifierType::Cunning, additionalModifiers, ModifierOperator::Additive, false) * 0.33f; + } + else if (type == ModifierType::Pierce) { + multiplicative += CalculateModifier(ModifierType::Cunning, additionalModifiers, ModifierOperator::Additive, false) * 0.285f; + } + else if (nejlika::IsOverTimeType(type) || nejlika::IsNormalDamageType(type)) { + multiplicative += CalculateModifier(ModifierType::Spirit, additionalModifiers, ModifierOperator::Additive, false) * 0.33f; + } + else if (type == ModifierType::ImaginationRegen) { + const auto spirit = CalculateModifier(ModifierType::Spirit, additionalModifiers, ModifierOperator::Additive, false); + additive += spirit * 0.01f + 1; + multiplicative += spirit * 0.25f; + } + else if (type == ModifierType::HealthRegen) { + const auto physique = CalculateModifier(ModifierType::Physique, additionalModifiers, ModifierOperator::Additive, false); + additive += physique * 0.04f; + } + else if (type == ModifierType::Defensive) { + additive += CalculateModifier(ModifierType::Physique, additionalModifiers, ModifierOperator::Additive, false) * 0.5f; + if (lot == 1) additive += level * 10; + } + else if (type == ModifierType::Offensive) { + additive += CalculateModifier(ModifierType::Cunning, additionalModifiers, ModifierOperator::Additive, false) * 0.5f; + if (lot == 1) additive += level * 10; + } + if (elementalDamage.contains(type)) { additive += CalculateModifier(ModifierType::Elemental, additionalModifiers, ModifierOperator::Additive, false) / elementalDamage.size(); multiplicative += CalculateModifier(ModifierType::Elemental, additionalModifiers, ModifierOperator::Multiplicative, false) / elementalDamage.size(); @@ -118,8 +152,6 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::v float total = (scaler + additive) * (1 + multiplicative / 100); - std::cout << "Scaler: " << scaler << " Additive: " << additive << " Multiplicative: " << multiplicative << " Total: " << total << std::endl; - return total; } @@ -132,7 +164,7 @@ float nejlika::AdditionalEntityData::CalculateResistance(ModifierType type) cons float nejlika::AdditionalEntityData::CalculateMultiplier(ModifierType type) const { - return 1 + CalculateModifier(type, ModifierOperator::Multiplicative, false); + return 100 + CalculateModifier(type, ModifierOperator::Multiplicative, false); } std::vector nejlika::AdditionalEntityData::TriggerUpgradeItems(UpgradeTriggerType triggerType, const TriggerParameters& params) { @@ -270,6 +302,53 @@ void nejlika::AdditionalEntityData::RemoveSkills(LOT lot) { upgradeData.RemoveSkills(id); } +std::vector nejlika::AdditionalEntityData::CalculateMainWeaponDamage() { + auto* entity = Game::entityManager->GetEntity(id); + + if (entity == nullptr) { + return {}; + } + + auto* inventoryComponent = entity->GetComponent(); + + if (inventoryComponent == nullptr) { + return {}; + } + + Item* mainWeapon = nullptr; + + for (const auto& [location, item] : inventoryComponent->GetEquippedItems()) { + if (location == "special_r") { + mainWeapon = inventoryComponent->FindItemById(item.id); + break; + } + } + + if (mainWeapon == nullptr) { + return {}; + } + + const auto additionalItemDataOpt = NejlikaData::GetAdditionalItemData(mainWeapon->GetId()); + + if (!additionalItemDataOpt.has_value()) { + return {}; + } + + const auto& additionalItemData = *additionalItemDataOpt.value(); + + std::vector result; + + for (const auto& modifier : additionalItemData.GetModifierInstances()) { + if (nejlika::IsNormalDamageType(modifier.GetType()) || nejlika::IsOverTimeType(modifier.GetType()) || nejlika::IsDurationType(modifier.GetType())) { + if (modifier.GetOperator() == ModifierOperator::Additive && modifier.GetUpgradeName().empty()) { + result.push_back(modifier); + } + } + } + + return result; +} + void nejlika::AdditionalEntityData::RollStandardModifiers(int32_t level) { standardModifiers.clear(); @@ -296,8 +375,40 @@ 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); +void nejlika::AdditionalEntityData::TriggerPassiveRegeneration() { + auto* entity = Game::entityManager->GetEntity(id); + + if (entity == nullptr) { + return; + } + + auto* destroyable = entity->GetComponent(); + + if (destroyable == nullptr) { + return; + } + + const auto healthRegen = CalculateFinalModifier(ModifierType::HealthRegen, {}, level); + const auto imaginationRegen = CalculateFinalModifier(ModifierType::ImaginationRegen, {}, level); + + if (healthRegen > 0) { + destroyable->SetHealth(std::min(destroyable->GetHealth() + static_cast(healthRegen), static_cast(destroyable->GetMaxHealth()))); + } + + if (imaginationRegen > 0) { + destroyable->SetImagination(std::min(destroyable->GetImagination() + static_cast(imaginationRegen), static_cast(destroyable->GetMaxImagination()))); + } + + Game::entityManager->SerializeEntity(entity); + + // Trigger it again in 1 second + entity->AddCallbackTimer(1.0f, [this]() { + TriggerPassiveRegeneration(); + }); +} + +float nejlika::AdditionalEntityData::CalculateMultiplier(ModifierType type, const std::vector& additionalModifiers) const { + return 100 + CalculateModifier(type, additionalModifiers, ModifierOperator::Multiplicative, false); } std::unordered_map> nejlika::AdditionalEntityData::CalculateDamageConversion(std::vector& additionalModifiers) const { @@ -402,7 +513,15 @@ void nejlika::AdditionalEntityData::ApplyToEntity() { const auto& itemModifiers = itemData.GetModifierInstances(); - activeModifiers.insert(activeModifiers.end(), itemModifiers.begin(), itemModifiers.end()); + for (const auto& modifier : itemModifiers) { + if (nejlika::IsNormalDamageType(modifier.GetType()) || nejlika::IsOverTimeType(modifier.GetType()) || nejlika::IsDurationType(modifier.GetType())) { + if (modifier.GetOperator() == ModifierOperator::Additive && modifier.GetUpgradeName().empty()) { + continue; + } + } + + activeModifiers.push_back(modifier); + } } for (const auto& upgradeItem : upgradeItems) { @@ -431,11 +550,11 @@ void nejlika::AdditionalEntityData::ApplyToEntity() { } } - destroyable->SetMaxHealth(static_cast(CalculateModifier(ModifierType::Health, level))); - destroyable->SetMaxArmor(static_cast(CalculateModifier(ModifierType::Armor, level))); - if (!entity->IsPlayer()) { - destroyable->SetMaxImagination(static_cast(CalculateModifier(ModifierType::Imagination, level))); - } + destroyable->SetMaxHealth(static_cast(CalculateFinalModifier(ModifierType::Health, {}, level))); + destroyable->SetMaxArmor(static_cast(CalculateFinalModifier(ModifierType::Armor, {}, level))); + //if (!entity->IsPlayer()) { + destroyable->SetMaxImagination(static_cast(CalculateFinalModifier(ModifierType::Imagination, {}, level))); + //} if (initialized) { return; @@ -444,15 +563,17 @@ void nejlika::AdditionalEntityData::ApplyToEntity() { destroyable->SetHealth(destroyable->GetMaxHealth()); destroyable->SetArmor(destroyable->GetMaxArmor()); - if (!entity->IsPlayer()) { + //if (!entity->IsPlayer()) { destroyable->SetImagination(destroyable->GetMaxImagination()); - } + //} if (entity->IsPlayer()) { auto* controllablePhysicsComponent = entity->GetComponent(); - if (controllablePhysicsComponent) controllablePhysicsComponent->SetSpeedMultiplier(CalculateMultiplier(ModifierType::Speed)); + if (controllablePhysicsComponent) controllablePhysicsComponent->SetSpeedMultiplier(CalculateMultiplier(ModifierType::Speed) / 100.0f); } + TriggerPassiveRegeneration(); + initialized = true; } diff --git a/dGame/AdditionalEntityData.h b/dGame/AdditionalEntityData.h index 1636fb98..a952d053 100644 --- a/dGame/AdditionalEntityData.h +++ b/dGame/AdditionalEntityData.h @@ -24,18 +24,18 @@ public: float CalculateModifier(ModifierType type, ModifierOperator op, bool resistance) const; - float CalculateModifier(ModifierType type, std::vector& additionalModifiers, ModifierOperator op, bool resistance) const; + float CalculateModifier(ModifierType type, const std::vector& additionalModifiers, ModifierOperator op, bool resistance) const; float CalculateModifier(ModifierType type, int32_t level) const; float CalculateModifier(ModifierType type) const; - float CalculateModifier(ModifierType type, std::vector& additionalModifiers, int32_t level) const; + float CalculateFinalModifier(ModifierType type, const std::vector& additionalModifiers, int32_t level) const; float CalculateResistance(ModifierType type) const; /** - * @brief Calculate the multiplier for a given modifier type. With a base value of 1.0. + * @brief Calculate the multiplier for a given modifier type. With a base value of 100 (%). * * @param type The modifier type. * @return The multiplier. @@ -43,13 +43,13 @@ public: float CalculateMultiplier(ModifierType type) const; /** - * @brief Calculate the multiplier for a given modifier type. With a base value of 1.0. + * @brief Calculate the multiplier for a given modifier type. With a base value of 100 (%). * * @param type The modifier type. * @param additionalModifiers Additional modifiers to apply. * @return The multiplier. */ - float CalculateMultiplier(ModifierType type, std::vector& additionalModifiers) const; + float CalculateMultiplier(ModifierType type, const std::vector& additionalModifiers) const; /** * @brief Calculate damage conversation mapping. @@ -85,9 +85,15 @@ public: void AddSkills(LWOOBJID item); void RemoveSkills(LOT lot); + const std::vector& GetActiveModifiers() const { return activeModifiers; } + + std::vector CalculateMainWeaponDamage(); + private: void RollStandardModifiers(int32_t level); + void TriggerPassiveRegeneration(); + bool initialized = false; std::vector standardModifiers; diff --git a/dGame/AdditionalItemData.cpp b/dGame/AdditionalItemData.cpp index f95d0496..4453f360 100644 --- a/dGame/AdditionalItemData.cpp +++ b/dGame/AdditionalItemData.cpp @@ -210,18 +210,31 @@ void nejlika::AdditionalItemData::RollModifiers(Item* item, int32_t level) { const auto& itemTemplateVec = NejlikaData::GetModifierNameTemplates(ModifierNameType::Object); - const auto itemTemplateIt = std::find_if(itemTemplateVec.begin(), itemTemplateVec.end(), [item](const auto& it) { - return it.GetLOT() == static_cast(item->GetLot()); - }); + std::vector availableObjects; - if (itemTemplateIt != itemTemplateVec.end()) { - const auto& itemTemplate = *itemTemplateIt; + for (const auto& itemTemplate : itemTemplateVec) { + if (itemTemplate.GetMinLevel() > level || itemTemplate.GetMaxLevel() < level) { + continue; + } - const auto itemModifiers = itemTemplate.GenerateModifiers(level); + if (itemTemplate.GetLOT() != static_cast(item->GetLot())) { + continue; + } - modifierInstances.insert(modifierInstances.end(), itemModifiers.begin(), itemModifiers.end()); + availableObjects.push_back(&itemTemplate); } + if (availableObjects.empty()) { + Save(item); + return; + } + + const auto& itemTemplate = *availableObjects[GeneralUtils::GenerateRandomNumber() % availableObjects.size()]; + + const auto itemModifiers = itemTemplate.GenerateModifiers(level); + + modifierInstances.insert(modifierInstances.end(), itemModifiers.begin(), itemModifiers.end()); + Save(item); } diff --git a/dGame/CMakeLists.txt b/dGame/CMakeLists.txt index b06c4e01..76cd7734 100644 --- a/dGame/CMakeLists.txt +++ b/dGame/CMakeLists.txt @@ -22,6 +22,7 @@ set(DGAME_SOURCES "Character.cpp" "UpgradeTemplate.cpp" "UpgradeEffect.cpp" "Lookup.cpp" + "NejlikaHelpers.cpp" ) include_directories( diff --git a/dGame/EntityTemplate.cpp b/dGame/EntityTemplate.cpp index f5d9dfa3..0ee0ea3e 100644 --- a/dGame/EntityTemplate.cpp +++ b/dGame/EntityTemplate.cpp @@ -67,7 +67,8 @@ std::vector nejlika::EntityTemplate::GenerateModifier ModifierCategory::Player, 0, "", - ModifierType::Invalid + ModifierType::Invalid, + "" ); modifiers.push_back(modifier); diff --git a/dGame/ModifierInstance.cpp b/dGame/ModifierInstance.cpp index 1fca4cf6..5a44bf9f 100644 --- a/dGame/ModifierInstance.cpp +++ b/dGame/ModifierInstance.cpp @@ -51,11 +51,26 @@ std::string nejlika::ModifierInstance::GenerateHtmlString(const std::vector resistance -> op -> type -> value std::unordered_map>>> modifierMap; + bool hasConvertTo = false; + bool hasSkillModifier = false; + for (const auto& modifier : modifiers) { if (modifier.type == ModifierType::Invalid) { continue; } + if (modifier.GetConvertTo() != ModifierType::Invalid) + { + hasConvertTo = true; + continue; + } + + if (!modifier.GetUpgradeName().empty()) + { + hasSkillModifier = true; + continue; + } + modifierMap[modifier.category][modifier.isResistance][modifier.op][modifier.type] = modifier.value; } @@ -67,32 +82,89 @@ std::string nejlika::ModifierInstance::GenerateHtmlString(const std::vector"; - - ss << ((resistance.first) ? "Resistances" : "Modifiers"); - - ss << ":\n"; - for (const auto& math : resistance.second) { for (const auto& modifier : math.second) { - ss << ""; + ss << ""; - ss << magic_enum::enum_name(modifier.first) << ": "; - - ss << ((modifier.second > 0) ? "+" : "-"); + ss << ((modifier.second > 0) ? (math.first == ModifierOperator::Multiplicative ? "+" : "") : "-"); - ss << std::fixed << std::setprecision(0) << std::abs(modifier.second); + ss << std::fixed << std::setprecision(1) << std::abs(modifier.second); if (math.first == ModifierOperator::Multiplicative) { ss << "%"; } + ss << " "; + + ss << " " << nejlika::GetModifierTypeName(modifier.first); + + if (resistance.first) { + // If the ss now ends with 'Damage' remove it + if (ss.str().substr(ss.str().size() - 7) == " Damage") { + ss.seekp(-7, std::ios_base::end); + } + + ss << " " << "Resistance"; + } + ss << "\n"; } } } } + + if (hasSkillModifier) + { + for (const auto& modifier : modifiers) { + if (modifier.type != ModifierType::SkillModifier) { + continue; + } + ss << ""; + + ss << ((modifier.value > 0) ? "+" : "-"); + + ss << std::fixed << std::setprecision(0) << std::abs(modifier.value); + + ss << " to "; + + ss << modifier.GetUpgradeName(); + + ss << "\n"; + } + } + + if (hasConvertTo) + { + for (const auto& modifier : modifiers) { + if (modifier.GetConvertTo() == ModifierType::Invalid) + { + continue; + } + + if (modifier.type == ModifierType::Invalid) { + continue; + } + + ss << ""; + + // +xx/yy% of T1 converted to T2 + ss << ((modifier.value > 0) ? "" : "-"); + + ss << std::fixed << std::setprecision(0) << std::abs(modifier.value); + + ss << "% "; + + ss << " of "; + + ss << nejlika::GetModifierTypeName(modifier.type); + + ss << " converted to "; + + ss << nejlika::GetModifierTypeName(modifier.GetConvertTo()); + + ss << "\n"; + } + } return ss.str(); } \ No newline at end of file diff --git a/dGame/ModifierInstance.h b/dGame/ModifierInstance.h index 27b41556..207e7238 100644 --- a/dGame/ModifierInstance.h +++ b/dGame/ModifierInstance.h @@ -16,8 +16,11 @@ class ModifierInstance { public: ModifierInstance( - 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) {} + ModifierType type, float value, ModifierOperator op, bool isResistance, ModifierCategory category, uint32_t effectID, const std::string& effectType, ModifierType convertTo, + const std::string& upgradeName + ) : type(type), value(value), op(op), isResistance(isResistance), category(category), effectID(effectID), effectType(effectType), convertTo(convertTo), + upgradeName(upgradeName) + {} /** * @brief Construct a new Modifier Instance object from a json configuration. @@ -59,6 +62,8 @@ public: std::string GetEffectType() const { return effectType; } + std::string GetUpgradeName() const { return upgradeName; } + void SetType(ModifierType type) { this->type = type; } void SetConvertTo(ModifierType convertTo) { this->convertTo = convertTo; } @@ -75,6 +80,8 @@ public: void SetEffectType(const std::string& effectType) { this->effectType = effectType; } + void SetUpgradeName(const std::string& upgradeName) { this->upgradeName = upgradeName; } + private: ModifierType type; ModifierType convertTo; @@ -84,6 +91,7 @@ private: ModifierCategory category; uint32_t effectID; std::string effectType; + std::string upgradeName; }; } diff --git a/dGame/ModifierNameTemplate.cpp b/dGame/ModifierNameTemplate.cpp index ba86a01c..6dce781d 100644 --- a/dGame/ModifierNameTemplate.cpp +++ b/dGame/ModifierNameTemplate.cpp @@ -57,7 +57,14 @@ nejlika::ModifierNameTemplate::ModifierNameTemplate(const nlohmann::json & json) { for (const auto& modifier : json["modifiers"]) { - modifiers.push_back(ModifierTemplate(modifier)); + const auto modifierTemplate = ModifierTemplate(modifier); + + if (modifierTemplate.GetTypes().empty() || modifierTemplate.GetTypes().at(0) == ModifierType::Invalid) + { + continue; + } + + modifiers.push_back(modifierTemplate); } } @@ -68,6 +75,10 @@ nejlika::ModifierNameTemplate::ModifierNameTemplate(const nlohmann::json & json) if (levels.contains("min")) { minLevel = levels["min"].get(); + + const auto rasio = 1; //45.0f / 85.0f; + + minLevel = std::max(1, static_cast(minLevel * rasio)); } else { diff --git a/dGame/ModifierScale.cpp b/dGame/ModifierScale.cpp index eadb4578..20a70bfa 100644 --- a/dGame/ModifierScale.cpp +++ b/dGame/ModifierScale.cpp @@ -4,6 +4,10 @@ nejlika::ModifierScale::ModifierScale(const nlohmann::json & json) { level = json["level"].get(); + const auto rasio = 1; //45.0f / 85.0f; + + level = std::max(1, static_cast(level * rasio)); + if (json.contains("min")) { min = json["min"].get(); } diff --git a/dGame/ModifierTemplate.cpp b/dGame/ModifierTemplate.cpp index 9ba5340a..fd0782ee 100644 --- a/dGame/ModifierTemplate.cpp +++ b/dGame/ModifierTemplate.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include using namespace nejlika; @@ -133,6 +135,11 @@ nejlika::ModifierTemplate::ModifierTemplate(const nlohmann::json& config) { category = ModifierCategory::Player; } } + + if (config.contains("skill")) + { + upgradeName = config["skill"].get(); + } } nlohmann::json nejlika::ModifierTemplate::ToJson() const { @@ -191,6 +198,12 @@ nlohmann::json nejlika::ModifierTemplate::ToJson() const { config["resistance"] = isResistance; config["effect-id"] = effectID; config["effect-type"] = effectType; + config["operator"] = magic_enum::enum_name(operatorType); + + if (!upgradeName.empty()) + { + config["skill"] = upgradeName; + } return config; } @@ -249,9 +262,24 @@ std::string nejlika::ModifierTemplate::GenerateHtmlString(const std::vector resistance -> op -> type -> (min, max) std::unordered_map>>>> modifierMap; + bool hasConvertTo = false; + bool hasSkillModifier = false; + for (const auto& modifier : modifiers) { + if (modifier.GetConvertTo() != ModifierType::Invalid) + { + hasConvertTo = true; + continue; + } + + if (!modifier.GetUpgradeName().empty()) + { + hasSkillModifier = true; + continue; + } + for (const auto& type : modifier.types) { - if (type == ModifierType::Invalid) { + if (type == ModifierType::Invalid || type == ModifierType::SkillModifier) { continue; } @@ -299,35 +327,37 @@ std::string nejlika::ModifierTemplate::GenerateHtmlString(const std::vector"; - - 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 << ""; - ss << ((modifier.second.first > 0) ? "+" : "-"); + ss << ((modifier.second.first > 0) ? (math.first == ModifierOperator::Multiplicative ? "+" : "") : "-"); - ss << std::fixed << std::setprecision(0) << std::abs(modifier.second.first); + ss << std::fixed << std::setprecision(1) << 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); + ss << std::fixed << std::setprecision(1) << std::abs(modifier.second.second); } if (math.first == ModifierOperator::Multiplicative) { ss << "%"; } + + ss << " "; + + ss << " " << nejlika::GetModifierTypeName(modifier.first); + + if (resistance.first) { + // If the ss now ends with 'Damage' remove it + if (ss.str().substr(ss.str().size() - 6) == "Damage") { + ss.seekp(-6, std::ios_base::end); + } + + ss << " " << "Resistance"; + } ss << "\n"; } @@ -335,6 +365,90 @@ std::string nejlika::ModifierTemplate::GenerateHtmlString(const std::vector"; + + ss << ((m.GetMin() > 0) ? "+" : "-"); + + ss << std::fixed << std::setprecision(0) << std::abs(m.GetMin()); + + ss << " to "; + + ss << modifier.GetUpgradeName(); + + ss << "\n"; + } + } + } + + if (hasConvertTo) + { + for (const auto& modifier : modifiers) { + if (modifier.GetConvertTo() == ModifierType::Invalid) + { + continue; + } + + for (const auto& type : modifier.types) { + if (type == ModifierType::Invalid) { + continue; + } + + const auto& scalors = modifier.GetScales(); + + auto m = scalors[0]; + + for (const auto& s : scalors) { + if (s.GetLevel() <= level && s.GetLevel() > m.GetLevel()) { + m = s; + } + } + + ss << ""; + + // +xx/yy% of T1 converted to T2 + ss << ((m.GetMin() > 0) ? "" : "-"); + + ss << std::fixed << std::setprecision(0) << std::abs(m.GetMin()); + + if (m.GetMin() != m.GetMax()) + { + ss << "/"; + + ss << std::fixed << std::setprecision(0) << std::abs(m.GetMax()); + } + + ss << "% "; + + ss << " of "; + + ss << nejlika::GetModifierTypeName(type); + + ss << " converted to "; + + ss << nejlika::GetModifierTypeName(modifier.GetConvertTo()); + + ss << "\n"; + } + } + } + return ss.str(); } @@ -354,12 +468,13 @@ std::optional nejlika::ModifierTemplate::GenerateModifier(Modi power++; } - return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo); + return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo, upgradeName); } // 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()) { + if ((s.GetLevel() <= level) && (s.GetLevel() > scale.GetLevel())) { + std::cout << "Found scale: " << s.GetMin() << " - " << s.GetMax() << " for level " << s.GetLevel() << std::endl; scale = s; found = true; } @@ -369,7 +484,18 @@ std::optional nejlika::ModifierTemplate::GenerateModifier(Modi return std::nullopt; } - float value = GeneralUtils::GenerateRandomNumber(scale.GetMin(), scale.GetMax()); + float value = 0; + + if (scale.GetMax() == scale.GetMin()) + { + value = scale.GetMin(); + } + else + { + value = (GeneralUtils::GenerateRandomNumber(0, 100) / 100.0f) * (scale.GetMax() - scale.GetMin()) + scale.GetMin(); + } + + std::cout << "Generated modifier: " << value << " with level " << level << " for type: " << magic_enum::enum_name(type) << std::endl; - return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo); + return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo, upgradeName); } diff --git a/dGame/ModifierTemplate.h b/dGame/ModifierTemplate.h index 62de764e..a7031824 100644 --- a/dGame/ModifierTemplate.h +++ b/dGame/ModifierTemplate.h @@ -59,6 +59,8 @@ public: std::string GetEffectType() const { return effectType; } + const std::string& GetUpgradeName() const { return upgradeName; } + void SetTypes(const std::vector& types) { this->types = types; } void SetConvertTo(ModifierType convertTo) { this->convertTo = convertTo; } @@ -74,6 +76,8 @@ public: void SetEffectID(uint32_t effectID) { this->effectID = effectID; } void SetEffectType(const std::string& effectType) { this->effectType = effectType; } + + void SetUpgradeName(const std::string& upgradeName) { this->upgradeName = upgradeName; } /** * @brief Generate a HTML string representation of a set of modifier templates. @@ -98,6 +102,7 @@ private: bool isResistance; uint32_t effectID; std::string effectType; + std::string upgradeName; }; diff --git a/dGame/ModifierType.cpp b/dGame/ModifierType.cpp index 4a1bce4b..8ccfee12 100644 --- a/dGame/ModifierType.cpp +++ b/dGame/ModifierType.cpp @@ -53,6 +53,27 @@ const std::unordered_map nameMap = { {ModifierType::VitalityDecayDuration, "Vitality Decay Duration"}, {ModifierType::Seperation, "Seperation"}, {ModifierType::SeperationDuration, "Seperation Duration"}, + {ModifierType::Elemental, "Elemental Damage"}, + {ModifierType::Damage, "Damage"}, + {ModifierType::Speed, "Speed"}, + {ModifierType::AttackSpeed, "Attack Speed"}, + {ModifierType::BlockRecovery, "Block Recovery Speed"}, + {ModifierType::BlockChance, "Chance to Block"}, + {ModifierType::Block, "Damage to Block"}, + {ModifierType::CriticalDamage, "Critical-hit Damage"}, + {ModifierType::HealthDrain, "Damage converted to Health"}, + {ModifierType::ArmorPiercing, "Armor Piercing"}, + {ModifierType::ReducedStunDuration, "Reduced Stun Duration"}, + {ModifierType::SkillCooldownReduction, "Skill Cooldown Reduction"}, + {ModifierType::SkillRecharge, "Skill Recharge Time"}, + {ModifierType::Slow, "Slow"}, + {ModifierType::Physique, "Physique"}, + {ModifierType::Cunning, "Cunning"}, + {ModifierType::Spirit, "Imagination"}, + {ModifierType::AttacksPerSecond, "Attacks per Second"}, + {ModifierType::ImaginationCost, "Imagination Cost"}, + {ModifierType::MainWeaponDamage, "Main Weapon Damage"}, + {ModifierType::Stun, "Target Stun Duration"} }; const std::unordered_map resistanceMap = { @@ -115,6 +136,16 @@ const std::unordered_set isOverTimeMap = { ModifierType::Seperation }; +const std::unordered_set isDurationType = { + ModifierType::InternalDisassemblyDuration, + ModifierType::BurnDuration, + ModifierType::FrostburnDuration, + ModifierType::PoisonDuration, + ModifierType::VitalityDecayDuration, + ModifierType::ElectrocuteDuration, + ModifierType::SeperationDuration +}; + } const std::string& nejlika::GetModifierTypeColor(ModifierType type) @@ -179,3 +210,7 @@ const ModifierType nejlika::GetDurationType(ModifierType type) { const bool nejlika::IsOverTimeType(ModifierType type) { return isOverTimeMap.find(type) != isOverTimeMap.end(); } + +const bool nejlika::IsDurationType(ModifierType type) { + return isDurationType.find(type) != isDurationType.end(); +} diff --git a/dGame/ModifierType.h b/dGame/ModifierType.h index 0c8e1c42..c666b9e3 100644 --- a/dGame/ModifierType.h +++ b/dGame/ModifierType.h @@ -58,9 +58,20 @@ enum class ModifierType : uint8_t Slow, ArmorPiercing, + ReducedStunDuration, + SkillCooldownReduction, + SkillRecharge, + BlockRecovery, + BlockChance, + Block, + HealthRegen, + ImaginationRegen, + AttacksPerSecond, + ImaginationCost, + MainWeaponDamage, + Stun, CriticalDamage, - ChanceToBlock, HealthDrain, Invalid @@ -76,6 +87,8 @@ const bool IsNormalDamageType(ModifierType type); const bool IsOverTimeType(ModifierType type); +const bool IsDurationType(ModifierType type); + const ModifierType GetOverTimeType(ModifierType type); const ModifierType GetDurationType(ModifierType type); diff --git a/dGame/NejlikaData.cpp b/dGame/NejlikaData.cpp index 9bc7f9a8..af270697 100644 --- a/dGame/NejlikaData.cpp +++ b/dGame/NejlikaData.cpp @@ -117,18 +117,39 @@ void nejlika::NejlikaData::LoadNejlikaData() modifierNameTemplates.clear(); // Load data from json file - const auto& filename = Game::config->GetValue("nejlika"); + const auto& directory_name = Game::config->GetValue("nejlika"); - if (filename.empty()) + if (directory_name.empty()) { return; } - - std::ifstream file(filename); + + // Loop through all files in the directory + for (const auto& entry : std::filesystem::directory_iterator(directory_name)) + { + if (!entry.is_regular_file()) + { + continue; + } + + // It has to end on .mod.json + const auto& path = entry.path().string(); + + if (path.size() < 9 || path.substr(path.size() - 9) != ".mod.json") + { + continue; + } + + LoadNejlikaDataFile(entry.path().string()); + } +} + +void nejlika::NejlikaData::LoadNejlikaDataFile(const std::string& path) { + std::ifstream file(path); if (!file.is_open()) { - LOG("Failed to open nejlika data file: %s", filename.c_str()); + LOG("Failed to open nejlika data file: %s", path.c_str()); return; } @@ -144,19 +165,21 @@ void nejlika::NejlikaData::LoadNejlikaData() return; } - if (!json.contains("modifier-templates")) + if (json.contains("modifier-templates")) { - LOG("nejlika data file does not contain modifier-templates"); - return; - } + const auto& modifierTemplates = json["modifier-templates"]; - const auto& modifierTemplates = json["modifier-templates"]; + for (const auto& value : modifierTemplates) + { + auto modifierTemplate = ModifierNameTemplate(value); - for (const auto& value : modifierTemplates) - { - auto modifierTemplate = ModifierNameTemplate(value); + if (modifierTemplate.GetModifiers().empty()) + { + continue; + } - modifierNameTemplates[modifierTemplate.GetType()].push_back(modifierTemplate); + modifierNameTemplates[modifierTemplate.GetType()].push_back(modifierTemplate); + } } LOG("Loaded %d modifier templates", modifierNameTemplates.size()); @@ -186,5 +209,7 @@ void nejlika::NejlikaData::LoadNejlikaData() upgradeTemplates[upgradeTemplate.GetLot()] = upgradeTemplate; } } + + LOG("Loaded %d upgrade templates", upgradeTemplates.size()); } diff --git a/dGame/NejlikaData.h b/dGame/NejlikaData.h index e8ef303d..13c46ba4 100644 --- a/dGame/NejlikaData.h +++ b/dGame/NejlikaData.h @@ -35,6 +35,8 @@ void UnsetAdditionalEntityData(LWOOBJID id); void LoadNejlikaData(); +void LoadNejlikaDataFile(const std::string& path); + const Lookup& GetLookup(); } diff --git a/dGame/NejlikaHelpers.cpp b/dGame/NejlikaHelpers.cpp new file mode 100644 index 00000000..c69c6055 --- /dev/null +++ b/dGame/NejlikaHelpers.cpp @@ -0,0 +1,33 @@ +#include "NejlikaHelpers.h" + + +void nejlika::NejlikaHelpers::RenderDamageText(const std::string & text, Entity * attacker, Entity * damaged, + float scale, int32_t fontSize, int32_t colorR, int32_t colorG, int32_t colorB, int32_t colorA) +{ + if (damaged == nullptr) { + return; + } + + 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; + + const auto& damageText = text; + + std::stringstream damageUIMessage; + + damageUIMessage << 0.0825 << ";" << 0.12 << ";" << damagedPosition.x << ";" << damagedPosition.y + 4.5f << ";" << damagedPosition.z << ";" << 0.1 << ";"; + damageUIMessage << 200 * scale << ";" << 200 * scale << ";" << 0.5 << ";" << 1.0 << ";" << damageText << ";" << 4 << ";" << fontSize << ";" << colorR << ";" << colorG << ";" << colorB << ";"; + damageUIMessage << colorA; + + const auto damageUIStr = damageUIMessage.str(); + + if (damaged->IsPlayer()) { + damaged->SetNetworkVar(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS); + } else if (attacker != nullptr && attacker->IsPlayer()) { + attacker->SetNetworkVar(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS); + } +} \ No newline at end of file diff --git a/dGame/NejlikaHelpers.h b/dGame/NejlikaHelpers.h new file mode 100644 index 00000000..c8887fea --- /dev/null +++ b/dGame/NejlikaHelpers.h @@ -0,0 +1,9 @@ +#include + +namespace nejlika::NejlikaHelpers +{ + +void RenderDamageText(const std::string& text, Entity* attacker, Entity* damaged, + float scale = 1, int32_t fontSize = 4, int32_t colorR = 255, int32_t colorG = 255, int32_t colorB = 255, int32_t colorA = 0); + +} diff --git a/dGame/NejlikaHooks.cpp b/dGame/NejlikaHooks.cpp index 099d6b3b..77677d22 100644 --- a/dGame/NejlikaHooks.cpp +++ b/dGame/NejlikaHooks.cpp @@ -22,11 +22,16 @@ #include #include +#include "NejlikaHelpers.h" #include "NejlikaData.h" using namespace nejlika; using namespace nejlika::NejlikaData; +namespace { + std::unordered_map>> uniqueSkillModifiers; +} + void nejlika::NejlikaHooks::InstallHooks() { Command itemDescriptionCommand{ .help = "Special UI command, does nothing when used in chat.", @@ -37,26 +42,38 @@ void nejlika::NejlikaHooks::InstallHooks() { }; SlashCommandHandler::RegisterCommand(itemDescriptionCommand); + Command modificationssCommand{ + .help = "Displays active modifications.", + .info = "Displays active modifications.", + .aliases = {"stats"}, + .handle = [](Entity* entity, const SystemAddress& sysAddr, const std::string args) { + auto entityDataOpt = GetAdditionalEntityData(entity->GetObjectID()); + + if (!entityDataOpt.has_value()) { + return; + } + + auto& entityData = *entityDataOpt.value(); + + std::vector modifiers = entityData.GetActiveModifiers(); + + std::stringstream ss; + + ss << "Active modifications: " << std::endl; + + ss << ModifierInstance::GenerateHtmlString(modifiers); + + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(ss.str())); + }, + .requiredLevel = eGameMasterLevel::CIVILIAN + }; + SlashCommandHandler::RegisterCommand(modificationssCommand); + LoadNejlikaData(); InventoryComponent::OnItemCreated += [](InventoryComponent* component, Item* item) { const auto& itemType = static_cast(item->GetInfo().itemType); - /* - static const std::unordered_set listOfHandledItems { - eItemType::HAT, - eItemType::CHEST, - eItemType::LEGS, - eItemType::NECK, - eItemType::LEFT_HAND, - eItemType::RIGHT_HAND - }; - - if (listOfHandledItems.find(itemType) == listOfHandledItems.end()) { - return; - } - */ - // No to the Thinking Hat if (item->GetLot() == 6086) { return; @@ -133,11 +150,10 @@ void nejlika::NejlikaHooks::InstallHooks() { auto& additionalData = *additionalDataOpt.value(); - additionalData.ApplyToEntity(); - auto* inventoryComponent = entity->GetComponent(); if (!inventoryComponent) { + additionalData.ApplyToEntity(); return; } @@ -158,6 +174,8 @@ void nejlika::NejlikaHooks::InstallHooks() { } } + additionalData.ApplyToEntity(); + additionalData.InitializeSkills(); }; @@ -166,8 +184,6 @@ void nejlika::NejlikaHooks::InstallHooks() { 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:tertiary"), BehaviorSlot::Neck); auto* missionComponent = entity->GetComponent(); @@ -300,7 +316,7 @@ void nejlika::NejlikaHooks::InstallHooks() { entityData.ApplyToEntity(); }; - SkillComponent::OnSkillCast += [](SkillComponent* skillComponent, uint32_t skillID, bool success) { + SkillComponent::OnSkillCast += [](SkillComponent* skillComponent, uint32_t skillID, bool& success, uint32_t skillUID) { std::cout << "Skill cast: " << skillID << " - " << success << std::endl; auto* inventoryComponent = skillComponent->GetParent()->GetComponent(); @@ -312,17 +328,155 @@ void nejlika::NejlikaHooks::InstallHooks() { auto* entity = skillComponent->GetParent(); auto skills = inventoryComponent->GetSkills(); - - const auto primaryTrigger = NejlikaData::GetLookup().GetValue("intro:skills:proxy:main"); - const auto secondaryTrigger = NejlikaData::GetLookup().GetValue("intro:skills:proxy:secondary"); const auto tertiaryTrigger = NejlikaData::GetLookup().GetValue("intro:skills:proxy:tertiary"); - if (skillID == primaryTrigger || skillID == secondaryTrigger || skillID == tertiaryTrigger) { - } else { + if (skillID == tertiaryTrigger) { + inventoryComponent->RotateSkills(); + } + + const auto entityDataOpt = GetAdditionalEntityData(entity->GetObjectID()); + + if (!entityDataOpt.has_value()) { return; } - inventoryComponent->RotateSkills(); + auto& entityData = *entityDataOpt.value(); + + const auto& skillTemplates = GetModifierNameTemplates(ModifierNameType::Skill); + + const auto& skillTemplateIt = std::find_if(skillTemplates.begin(), skillTemplates.end(), [skillID](const auto& it) { + return it.GetLOT() == skillID; + }); + + std::vector modifiers; + + if (skillTemplateIt != skillTemplates.end()) { + const auto& skillTemplate = *skillTemplateIt; + + const auto skillModifiers = skillTemplate.GenerateModifiers(entityData.GetLevel()); + + modifiers.insert(modifiers.end(), skillModifiers.begin(), skillModifiers.end()); + } + + LOT itemLot = 0; + LWOOBJID itemId = 0; + BehaviorSlot itemSlot = BehaviorSlot::Invalid; + + if (inventoryComponent) { + const auto& equipped = inventoryComponent->GetEquippedItems(); + + // omg... + for (const auto& [equippedSlot, itemDetails] : equipped) { + std::cout << "Found equipped item: " << itemDetails.lot << std::endl; + + const auto info = Inventory::FindItemComponent(itemDetails.lot); + + const auto skill = InventoryComponent::FindSkill(itemDetails.lot); + + if (skill != skillID) { + continue; + } + + const auto itemBehaviorSlot = InventoryComponent::FindBehaviorSlot(static_cast(info.itemType)); + + itemLot = itemDetails.lot; + itemId = itemDetails.id; + itemSlot = itemBehaviorSlot; + + std::cout << "Found item: " << itemLot << std::endl; + + break; + } + } + + TriggerParameters params; + params.SkillID = skillID; + params.SelectedBehaviorSlot = itemSlot; + + // Upgrades + const auto upgradeModifiers = entityData.TriggerUpgradeItems(UpgradeTriggerType::OnCast, params); + + auto unionModifiersUpgradeModifiers = modifiers; + unionModifiersUpgradeModifiers.insert(unionModifiersUpgradeModifiers.end(), upgradeModifiers.begin(), upgradeModifiers.end()); + + const auto& imaginationCost = entityData.CalculateFinalModifier(ModifierType::ImaginationCost, unionModifiersUpgradeModifiers, entityData.GetLevel()); + + auto* destroyable = entity->GetComponent(); + + if (destroyable != nullptr) { + if (destroyable->GetImagination() < imaginationCost) { + NejlikaHelpers::RenderDamageText("Insufficient imagination!", entity, entity, 4.0f); + } + else { + destroyable->SetImagination(destroyable->GetImagination() - imaginationCost); + + Game::entityManager->SerializeEntity(entity); + + // Insert into the unique skill modifiers + const auto& uniqueSkillModifiersIt = uniqueSkillModifiers.find(entity->GetObjectID()); + + if (uniqueSkillModifiersIt != uniqueSkillModifiers.end()) { + auto& uniqueSkillModifiersMap = uniqueSkillModifiersIt->second; + + uniqueSkillModifiersMap[skillID] = upgradeModifiers; + } + else { + uniqueSkillModifiers[entity->GetObjectID()] = { { skillID, upgradeModifiers } }; + } + + entity->AddCallbackTimer(10.0f, [entity, skillID]() { + const auto& uniqueSkillModifiersIt = uniqueSkillModifiers.find(entity->GetObjectID()); + + if (uniqueSkillModifiersIt != uniqueSkillModifiers.end()) { + auto& uniqueSkillModifiersMap = uniqueSkillModifiersIt->second; + + const auto& uniqueSkillModifiersSkillIt = uniqueSkillModifiersMap.find(skillID); + + if (uniqueSkillModifiersSkillIt != uniqueSkillModifiersMap.end()) { + uniqueSkillModifiersMap.erase(uniqueSkillModifiersSkillIt); + } + } + }); + + modifiers.insert(modifiers.end(), upgradeModifiers.begin(), upgradeModifiers.end()); + } + } + + float attackSpeed = 0; + + if (itemSlot == BehaviorSlot::Primary) { + auto attacksPerSecond = entityData.CalculateFinalModifier(ModifierType::AttacksPerSecond, modifiers, entityData.GetLevel()); + attacksPerSecond = std::max(attacksPerSecond, 1.0f); + const auto attackSpeedMod = entityData.CalculateMultiplier(ModifierType::AttackSpeed, modifiers); + attackSpeed = (1.0f / attacksPerSecond) * ((attackSpeedMod / 100.0f)) / 2.0f; // 2.0f to account for animation times + LOG("Attack speed: %f, attacks per second: %f, attack speed mod: %f", attackSpeed, attacksPerSecond, attackSpeedMod); + } + else { + const auto skillRecharge = entityData.CalculateFinalModifier(ModifierType::SkillRecharge, modifiers, entityData.GetLevel()); + const auto skillCooldownMod = entityData.CalculateMultiplier(ModifierType::SkillCooldownReduction, modifiers); + attackSpeed = skillRecharge * (skillCooldownMod / 100.0f); + LOG("Skill recharge: %f, skill cooldown mod: %f", skillRecharge, skillCooldownMod); + } + + if (entity->IsPlayer()) { + entity->AddCallbackTimer(0.0f, [entity, skillID, attackSpeed]() { + CBITSTREAM; + CMSGHEADER; + + const auto objectID = entity->GetObjectID(); + + bitStream.Write(objectID); + bitStream.Write(eGameMessageType::MODIFY_SKILL_COOLDOWN); + + bitStream.Write1(); + bitStream.Write(attackSpeed); + bitStream.Write(static_cast(skillID)); + + LOG("Sending cooldown reduction for skill: %d: %f", skillID, attackSpeed); + + SEND_PACKET_BROADCAST; + }); + } }; DestroyableComponent::OnDamageCalculation += [](Entity* damaged, LWOOBJID offender, uint32_t skillID, uint32_t& damage) { @@ -403,14 +557,24 @@ void nejlika::NejlikaHooks::InstallHooks() { } } + std::vector modifiers; + + if (itemSlot == BehaviorSlot::Primary) { + const auto mainWeaponDamage = offenderEntity.CalculateMainWeaponDamage(); + + for (const auto& modifier : mainWeaponDamage) { + std::cout << "Main weapon damage: " << magic_enum::enum_name(modifier.GetType()) << " - " << modifier.GetValue() << std::endl; + } + + modifiers.insert(modifiers.end(), mainWeaponDamage.begin(), mainWeaponDamage.end()); + } + const auto& skillTemplates = GetModifierNameTemplates(ModifierNameType::Skill); const auto& skillTemplateIt = std::find_if(skillTemplates.begin(), skillTemplates.end(), [skillID](const auto& it) { return it.GetLOT() == skillID; }); - std::vector modifiers; - if (skillTemplateIt != skillTemplates.end()) { const auto& skillTemplate = *skillTemplateIt; @@ -428,6 +592,20 @@ void nejlika::NejlikaHooks::InstallHooks() { modifiers.insert(modifiers.end(), upgradeModifiers.begin(), upgradeModifiers.end()); + const auto& uniqueSkillModifiersIt = uniqueSkillModifiers.find(offender); + + if (uniqueSkillModifiersIt != uniqueSkillModifiers.end()) { + const auto& uniqueSkillModifiersMap = uniqueSkillModifiersIt->second; + + const auto& uniqueSkillModifiersSkillIt = uniqueSkillModifiersMap.find(skillID); + + if (uniqueSkillModifiersSkillIt != uniqueSkillModifiersMap.end()) { + const auto& uniqueSkillModifiersVec = uniqueSkillModifiersSkillIt->second; + + modifiers.insert(modifiers.end(), uniqueSkillModifiersVec.begin(), uniqueSkillModifiersVec.end()); + } + } + std::unordered_set damageTypes; for (const auto& modifier : modifiers) { @@ -444,14 +622,28 @@ void nejlika::NejlikaHooks::InstallHooks() { } } - uint32_t totalDamage = 0; + int32_t totalDamage = 0; std::unordered_map> durationTypes; std::unordered_map tmpDamageValues; + + const auto mainWeaponDamageModifier = (offenderEntity.CalculateMultiplier(ModifierType::MainWeaponDamage, modifiers) - 100.0f) / 100.0f; + if (mainWeaponDamageModifier > 0.0f) { + const auto mainWeaponDamage = offenderEntity.CalculateMainWeaponDamage(); + + for (auto modifier : mainWeaponDamage) { + const auto damageValue = modifier.GetValue(); + + modifier.SetValue(damageValue * mainWeaponDamageModifier); + + modifiers.push_back(modifier); + } + } + for (const auto& type : damageTypes) { if (nejlika::IsOverTimeType(type)) { - float damageValue = offenderEntity.CalculateModifier(type, modifiers, level); + float damageValue = offenderEntity.CalculateFinalModifier(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); @@ -460,7 +652,7 @@ void nejlika::NejlikaHooks::InstallHooks() { const auto durationType = nejlika::GetDurationType(type); - const auto duration = offenderEntity.CalculateModifier(durationType, modifiers, level); + const auto duration = offenderEntity.CalculateFinalModifier(durationType, modifiers, level); const auto durationResistance = std::max(1 - (damagedEntity.CalculateResistance(durationType) / 100), 0.2f); @@ -475,7 +667,7 @@ void nejlika::NejlikaHooks::InstallHooks() { continue; } - float damageValue = offenderEntity.CalculateModifier(type, modifiers, level); + float damageValue = offenderEntity.CalculateFinalModifier(type, modifiers, level); tmpDamageValues[type] = damageValue; } @@ -485,7 +677,7 @@ void nejlika::NejlikaHooks::InstallHooks() { std::unordered_map finalDamageValues; - for (const auto& [typeA, typeBMap] : converationMap) { + /*for (const auto& [typeA, typeBMap] : converationMap) { const auto& typeAValue = tmpDamageValues.find(typeA); if (typeAValue == tmpDamageValues.end()) { @@ -508,6 +700,13 @@ void nejlika::NejlikaHooks::InstallHooks() { finalDamageValues[typeA] += typeAValueFloat - convertedValue; finalDamageValues[typeB] += typeBValueFloat + convertedValue; } + }*/ + + // Add the remaining values + for (const auto& [type, value] : tmpDamageValues) { + if (finalDamageValues.find(type) == finalDamageValues.end()) { + finalDamageValues[type] = value; + } } for (const auto& [type, damage] : finalDamageValues) { @@ -516,7 +715,7 @@ void nejlika::NejlikaHooks::InstallHooks() { float reductedDamage = damage * resistance; - totalDamage += static_cast(reductedDamage); + totalDamage += static_cast(reductedDamage); std::cout << "Damage type: " << magic_enum::enum_name(type) << " - " << damage << std::endl; std::cout << "Resistance: " << resistance << " - " << reductedDamage << std::endl; @@ -581,21 +780,25 @@ void nejlika::NejlikaHooks::InstallHooks() { if (damageMultiplier > 1.0f) { isCritical = true; - damageMultiplier *= offenderEntity.CalculateMultiplier(ModifierType::CriticalDamage, modifiers); + damageMultiplier *= (offenderEntity.CalculateMultiplier(ModifierType::CriticalDamage, modifiers) / 100.0f); } } if (isHit) { // Add a random +5% to the damage - totalDamage += static_cast(totalDamage * (GeneralUtils::GenerateRandomNumber(0, 5) / 100.0f)); + totalDamage += static_cast(totalDamage * (GeneralUtils::GenerateRandomNumber(0, 5) / 100.0f)); damage = totalDamage; } else { damage = totalDamage = 0; } + if (totalDamage < 0 || damage < 0) { + totalDamage = damage = 0; + } + if (isCritical) { - totalDamage = static_cast(totalDamage * damageMultiplier); + totalDamage = static_cast(totalDamage * damageMultiplier); const auto effectName = std::to_string(GeneralUtils::GenerateRandomNumber()); const auto damagedID = damaged->GetObjectID(); @@ -616,26 +819,15 @@ void nejlika::NejlikaHooks::InstallHooks() { }); } - auto attackSpeed = offenderEntity.CalculateModifier(ModifierType::AttackSpeed, modifiers, level); + if (damage == 0) { + return; + } - if (offfendEntity->IsPlayer()) { - offfendEntity->AddCallbackTimer(0.0f, [offfendEntity, skillID, attackSpeed]() { - CBITSTREAM; - CMSGHEADER; + const auto stunDuration = offenderEntity.CalculateFinalModifier(ModifierType::Stun, modifiers, level); - const auto entity = offfendEntity->GetObjectID(); - - bitStream.Write(entity); - bitStream.Write(eGameMessageType::MODIFY_SKILL_COOLDOWN); - - bitStream.Write1(); - bitStream.Write(attackSpeed); - bitStream.Write(static_cast(skillID)); - - LOG("Sending cooldown reduction for skill: %d", skillID); - - SEND_PACKET_BROADCAST; - }); + if (baseCombatAIComponent && stunDuration > 0) { + LOG("Stunning for %f seconds", stunDuration); + baseCombatAIComponent->Stun(stunDuration, true); } // Apply over time damage. @@ -651,7 +843,7 @@ void nejlika::NejlikaHooks::InstallHooks() { continue; } - const auto damagePerTick = static_cast(damageDuration.first / duration); + const auto damagePerTick = static_cast(damageDuration.first); auto* destroyable = damaged->GetComponent(); @@ -661,54 +853,20 @@ void nejlika::NejlikaHooks::InstallHooks() { for (size_t i = 0; i < duration; i++) { - damaged->AddCallbackTimer(i * 0.5f, [offender, damaged, damagePerTick]() { + damaged->AddCallbackTimer(i * 0.5f, [type, offender, damaged, damagePerTick]() { + + auto* offenderEntity = Game::entityManager->GetEntity(offender); + auto* destroyable = damaged->GetComponent(); if (!destroyable) { return; } - destroyable->Damage(offender, damagePerTick, 0, true, true); + destroyable->Damage(damagePerTick, offender, 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); - }*/ }; } @@ -744,34 +902,110 @@ void nejlika::NejlikaHooks::ItemDescription(Entity* entity, const SystemAddress& std::stringstream name; std::stringstream desc; - if (itemId == LWOOBJID_EMPTY) { - ChatPackets::SendSystemMessage(sysAddr, u"Invalid item ID."); + const auto& upgradeItemOpt = NejlikaData::GetUpgradeTemplate(lot); + if (upgradeItemOpt.has_value()) { + const auto& upgradeItem = *upgradeItemOpt.value(); + + auto* inventoryComponent = entity->GetComponent(); + + if (!inventoryComponent) { + return; + } + + auto amount = inventoryComponent->GetLotCount(lot); + + if (itemId == LWOOBJID_EMPTY) { + amount++; + } + + const auto& modifiers = upgradeItem.GenerateModifiers(amount); + + name << "NAME"; + + desc << "DESC" << "\n\n"; + + desc << "Level " << amount << "/" << upgradeItem.GetMaxLevel() << "\n"; + + desc << ModifierInstance::GenerateHtmlString(modifiers); + + const auto& passives = upgradeItem.GetPassives(); + + desc << UpgradeEffect::GenerateHtmlString(passives, amount); + } else if (itemId == LWOOBJID_EMPTY) { const auto& itemTemplateVec = NejlikaData::GetModifierNameTemplates(ModifierNameType::Object); + + auto* levelProgressionComponent = entity->GetComponent(); - const auto itemTemplateIt = std::find_if(itemTemplateVec.begin(), itemTemplateVec.end(), [lot](const auto& it) { - return it.GetLOT() == static_cast(lot); - }); + if (!levelProgressionComponent) { + return; + } - if (itemTemplateIt == itemTemplateVec.end()) { - name << "NAME"; - desc << "DESC"; - } else { - const auto& itemTemplate = *itemTemplateIt; + std::vector availableTemplates; + const nejlika::ModifierNameTemplate* lowestLevelTemplate = nullptr; + std::vector lowestLevelTemplates; - const auto& modifiers = itemTemplate.GetModifiers(); + for (const auto& itemTemplate : itemTemplateVec) { + auto level = static_cast(levelProgressionComponent->GetLevel()); - // Get the entity level - auto* levelProgressionComponent = entity->GetComponent(); - - if (!levelProgressionComponent) { - return; + if (itemTemplate.GetLOT() != static_cast(lot)) { + continue; } - auto level = levelProgressionComponent->GetLevel(); + if (lowestLevelTemplate == nullptr) { + lowestLevelTemplate = &itemTemplate; + } + else if (lowestLevelTemplate->GetMinLevel() > level) { + lowestLevelTemplate = &itemTemplate; + lowestLevelTemplates.clear(); + } + else if (lowestLevelTemplate->GetMinLevel() == level) { + lowestLevelTemplates.push_back(&itemTemplate); + } + + if (itemTemplate.GetMinLevel() > level || itemTemplate.GetMaxLevel() < level) { + continue; + } - name << "NAME"; - desc << ModifierTemplate::GenerateHtmlString(modifiers, level); + availableTemplates.push_back(&itemTemplate); + } + + // Add the lowest level template if no other template was found + if (availableTemplates.empty() && lowestLevelTemplate != nullptr) { + availableTemplates.push_back(lowestLevelTemplate); + + // Add all templates with the same level + availableTemplates.insert(availableTemplates.end(), lowestLevelTemplates.begin(), lowestLevelTemplates.end()); + } + + name << "NAME"; + + if (availableTemplates.empty()) { + desc << "DESC"; + } else { + if (availableTemplates.size() > 1) { + desc << "One of:\n"; + } + for (size_t i = 0; i < availableTemplates.size(); i++) { + const auto& itemTemplate = *availableTemplates[i]; + + const auto& modifiers = itemTemplate.GetModifiers(); + + // Get the entity level + auto* levelProgressionComponent = entity->GetComponent(); + + if (!levelProgressionComponent) { + return; + } + + auto level = std::max(static_cast(levelProgressionComponent->GetLevel()), itemTemplate.GetMinLevel()); + + desc << ModifierTemplate::GenerateHtmlString(modifiers, level); + + if (i < availableTemplates.size() - 1) { + desc << "\nor\n"; + } + } } } else { const auto itemDataOpt = GetAdditionalItemData(itemId); diff --git a/dGame/UpgradeEffect.cpp b/dGame/UpgradeEffect.cpp index 5fbc04f1..f8d959ab 100644 --- a/dGame/UpgradeEffect.cpp +++ b/dGame/UpgradeEffect.cpp @@ -306,3 +306,50 @@ void nejlika::UpgradeEffect::RemoveSkill(LWOOBJID origin) const { inventory->RemoveSkill(equipSkillID); } } + +std::string nejlika::UpgradeEffect::GenerateHtmlString(const std::vector& effects, int32_t level) { + std::stringstream ss; + + for (const auto& effect : effects) { + const auto chance = effect.CalculateChance(level); + + + for (const auto& condition : effect.conditions) { + if (condition == UpgradeTriggerCondition::None || condition == UpgradeTriggerCondition::UseSkill) { + continue; + } + + ss << ""; + + switch (condition) { + case UpgradeTriggerCondition::PrimaryAbility: + ss << "On main-hand attack"; + break; + case UpgradeTriggerCondition::Melee: + ss << "Requires melee weapon"; + break; + case UpgradeTriggerCondition::TwoHanded: + ss << "Requires two-handed weapon"; + break; + case UpgradeTriggerCondition::Shield: + ss << "Requires a shield"; + break; + case UpgradeTriggerCondition::Unarmed: + ss << "Requires unarmed attack"; + break; + } + + ss << "\n\n"; + } + + if (chance < 1.0f) { + ss << "" << chance * 100 << "% chance to trigger\n"; + } + + std::cout << "Level " << level << " chance: " << chance << std::endl; + + ss << ModifierInstance::GenerateHtmlString(effect.GenerateModifiers(level)); + } + + return ss.str(); +} diff --git a/dGame/UpgradeEffect.h b/dGame/UpgradeEffect.h index 1cbb0f55..b681ff79 100644 --- a/dGame/UpgradeEffect.h +++ b/dGame/UpgradeEffect.h @@ -38,6 +38,15 @@ public: void AddSkill(LWOOBJID origin) const; void RemoveSkill(LWOOBJID origin) const; + + /** + * @brief Generate a HTML string representation of a set of upgrade effects. + * + * @param modifiers The upgrade effects to generate the HTML string for. + * @param level The level of the upgrade effects. + * @return The HTML string. + */ + static std::string GenerateHtmlString(const std::vector& effects, int32_t level); private: struct UpgradeScale diff --git a/dGame/UpgradeTriggerType.h b/dGame/UpgradeTriggerType.h index 53901ad9..90e9f442 100644 --- a/dGame/UpgradeTriggerType.h +++ b/dGame/UpgradeTriggerType.h @@ -10,7 +10,8 @@ enum class UpgradeTriggerType OnHit, Equip, UnEquip, - Active + Active, + OnCast }; } diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index a92ac5a4..8ac038cf 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -767,8 +767,8 @@ void BaseCombatAIComponent::SetTetherSpeed(float value) { m_TetherSpeed = value; } -void BaseCombatAIComponent::Stun(const float time) { - if (m_StunImmune || m_StunTime > time) { +void BaseCombatAIComponent::Stun(const float time, const bool ignoreImmunity) { + if ((!ignoreImmunity && m_StunImmune) || m_StunTime > time) { return; } diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 5502231c..95831a9c 100644 --- a/dGame/dComponents/BaseCombatAIComponent.h +++ b/dGame/dComponents/BaseCombatAIComponent.h @@ -187,8 +187,9 @@ public: /** * Stuns the entity for a certain amount of time, will not work if the entity is stun immune * @param time the time to stun the entity, if stunnable + * @param ignoreImmunity whether to ignore the stun immunity of the entity */ - void Stun(float time); + void Stun(float time, bool ignoreImmunity = false); /** * Gets the radius that will cause this entity to get aggro'd, causing a target chase diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 4105598b..f8a37077 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -675,7 +675,7 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32 if (m_Parent->IsPlayer()) { m_Parent->SetNetworkVar(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS); - } else if (attacker->IsPlayer()) { + } else if (attacker != nullptr && attacker->IsPlayer()) { attacker->SetNetworkVar(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS); } diff --git a/dGame/dComponents/SkillComponent.cpp b/dGame/dComponents/SkillComponent.cpp index e728a8d4..3e096014 100644 --- a/dGame/dComponents/SkillComponent.cpp +++ b/dGame/dComponents/SkillComponent.cpp @@ -31,7 +31,7 @@ ProjectileSyncEntry::ProjectileSyncEntry() { std::unordered_map SkillComponent::m_skillBehaviorCache = {}; -Observable SkillComponent::OnSkillCast; +Observable SkillComponent::OnSkillCast; bool SkillComponent::CastPlayerSkill(const uint32_t behaviorId, const uint32_t skillUid, RakNet::BitStream& bitStream, const LWOOBJID target, uint32_t skillID) { auto* context = new BehaviorContext(this->m_Parent->GetObjectID()); @@ -50,7 +50,11 @@ bool SkillComponent::CastPlayerSkill(const uint32_t behaviorId, const uint32_t s context->ExecuteUpdates(); - OnSkillCast(this, skillID, !context->failed); + bool success = !context->failed; + + OnSkillCast(this, skillID, success, skillUid); + + context->failed = !success; return !context->failed; } diff --git a/dGame/dComponents/SkillComponent.h b/dGame/dComponents/SkillComponent.h index 9db75030..0c51a8d1 100644 --- a/dGame/dComponents/SkillComponent.h +++ b/dGame/dComponents/SkillComponent.h @@ -185,8 +185,8 @@ public: */ uint32_t GetUniqueSkillId(); - // SkillComponent, SkillID, Success - static Observable OnSkillCast; + // SkillComponent, SkillID, Success, skillUID + static Observable OnSkillCast; private: /** diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index ae70fad7..fb655540 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -977,7 +977,7 @@ void GameMessages::SendResurrect(Entity* entity) { int32_t imaginationToRestore = levelComponent->GetLevel() >= 45 ? 20 : 6; if (imaginationToRestore > destroyableComponent->GetMaxImagination()) imaginationToRestore = destroyableComponent->GetMaxImagination(); - destroyableComponent->SetImagination(imaginationToRestore); + destroyableComponent->SetImagination(destroyableComponent->GetMaxImagination()); } } });