diff --git a/dGame/AdditionalEntityData.cpp b/dGame/AdditionalEntityData.cpp index bd9b322d..d88d94fc 100644 --- a/dGame/AdditionalEntityData.cpp +++ b/dGame/AdditionalEntityData.cpp @@ -7,6 +7,9 @@ #include #include #include +#include + +using namespace nejlika; float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, ModifierOperator op, bool resistance) const { @@ -86,9 +89,27 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::v float multiplicative = CalculateModifier(type, additionalModifiers, ModifierOperator::Multiplicative, false); - std::cout << "Scaler: " << scaler << " Additive: " << additive << " Multiplicative: " << multiplicative << std::endl; + static const std::unordered_set damageTypes = { + ModifierType::Slashing, + ModifierType::Piercing, + ModifierType::Bludgeoning, + ModifierType::Fire, + ModifierType::Cold, + ModifierType::Lightning, + ModifierType::Corruption, + ModifierType::Psychic + }; - return (scaler + additive) * (1 + multiplicative / 100); + if (damageTypes.contains(type)) { + additive += CalculateModifier(ModifierType::Damage, additionalModifiers, ModifierOperator::Additive, false); + multiplicative += CalculateModifier(ModifierType::Damage, additionalModifiers, ModifierOperator::Multiplicative, false); + } + + float total = (scaler + additive) * (1 + multiplicative / 100); + + std::cout << "Scaler: " << scaler << " Additive: " << additive << " Multiplicative: " << multiplicative << " Total: " << total << std::endl; + + return total; } float nejlika::AdditionalEntityData::CalculateResistance(ModifierType type) const @@ -96,6 +117,44 @@ float nejlika::AdditionalEntityData::CalculateResistance(ModifierType type) cons return CalculateModifier(type, ModifierOperator::Multiplicative, true); } +std::vector nejlika::AdditionalEntityData::TriggerUpgradeItems(UpgradeTriggerType triggerType) { + auto* entity = Game::entityManager->GetEntity(id); + + if (entity == nullptr) { + return {}; + } + + auto* inventoryComponent = entity->GetComponent(); + + if (inventoryComponent == nullptr) { + return {}; + } + + std::vector result; + + for (const auto& itemID : upgradeItems) { + auto* item = inventoryComponent->FindItemById(itemID); + + if (item == nullptr) { + continue; + } + + const auto upgradeDataOpt = NejlikaData::GetUpgradeTemplate(item->GetLot()); + + if (!upgradeDataOpt.has_value()) { + continue; + } + + const auto& upgradeData = *upgradeDataOpt.value(); + + const auto modifiers = upgradeData.Trigger(item->GetCount(), triggerType, id); + + result.insert(result.end(), modifiers.begin(), modifiers.end()); + } + + return result; +} + void nejlika::AdditionalEntityData::RollStandardModifiers(int32_t level) { standardModifiers.clear(); diff --git a/dGame/AdditionalEntityData.h b/dGame/AdditionalEntityData.h index 13a62af1..c80385a8 100644 --- a/dGame/AdditionalEntityData.h +++ b/dGame/AdditionalEntityData.h @@ -7,6 +7,9 @@ #include "ModifierInstance.h" #include "EntityTemplate.h" +#include "UpgradeTriggerType.h" + +#include namespace nejlika { @@ -40,6 +43,14 @@ public: LOT GetLOT() const { return lot; } + const std::unordered_set& GetUpgradeItems() const { return upgradeItems; } + + void AddUpgradeItem(LWOOBJID id) { upgradeItems.insert(id); } + + void RemoveUpgradeItem(LWOOBJID id) { upgradeItems.erase(id); } + + std::vector TriggerUpgradeItems(UpgradeTriggerType triggerType); + private: void RollStandardModifiers(int32_t level); @@ -47,6 +58,7 @@ private: std::vector standardModifiers; std::vector activeModifiers; + std::unordered_set upgradeItems; LWOOBJID id; LOT lot; diff --git a/dGame/CMakeLists.txt b/dGame/CMakeLists.txt index ff5f7611..a6204f9a 100644 --- a/dGame/CMakeLists.txt +++ b/dGame/CMakeLists.txt @@ -19,6 +19,8 @@ set(DGAME_SOURCES "Character.cpp" "EntityTemplate.cpp" "AdditionalEntityData.cpp" "NejlikaHooks.cpp" + "UpgradeTemplate.cpp" + "UpgradeEffect.cpp" ) include_directories( diff --git a/dGame/ModifierScale.cpp b/dGame/ModifierScale.cpp index fdd1c8bd..eadb4578 100644 --- a/dGame/ModifierScale.cpp +++ b/dGame/ModifierScale.cpp @@ -3,8 +3,25 @@ nejlika::ModifierScale::ModifierScale(const nlohmann::json & json) { level = json["level"].get(); - min = json["min"].get(); - max = json["max"].get(); + + if (json.contains("min")) { + min = json["min"].get(); + } + else { + min = 0.0f; + } + + if (json.contains("max")) { + max = json["max"].get(); + } + else { + max = 0.0f; + } + + if (json.contains("value")) { + min = json["value"].get(); + max = json["value"].get(); + } } nlohmann::json nejlika::ModifierScale::ToJson() const diff --git a/dGame/ModifierType.h b/dGame/ModifierType.h index a937f700..cc19316c 100644 --- a/dGame/ModifierType.h +++ b/dGame/ModifierType.h @@ -26,6 +26,8 @@ enum class ModifierType : uint8_t Psychic, + Damage, + Invalid }; diff --git a/dGame/NejlikaData.cpp b/dGame/NejlikaData.cpp index 98cb6c5f..f4190641 100644 --- a/dGame/NejlikaData.cpp +++ b/dGame/NejlikaData.cpp @@ -20,6 +20,8 @@ std::unordered_map additionalEntityData std::unordered_map entityTemplates; +std::unordered_map upgradeTemplates; + } const std::unordered_map>& nejlika::NejlikaData::GetModifierNameTemplates() @@ -69,6 +71,16 @@ const std::optional nejlika::NejlikaData::GetEntityTemplate(LOT return std::nullopt; } +const std::optional nejlika::NejlikaData::GetUpgradeTemplate(LOT lot) { + const auto& it = upgradeTemplates.find(lot); + + if (it != upgradeTemplates.end()) { + return std::optional(&it->second); + } + + return std::nullopt; +} + void nejlika::NejlikaData::SetAdditionalItemData(LWOOBJID id, AdditionalItemData data) { additionalItemData[id] = data; } @@ -147,5 +159,17 @@ void nejlika::NejlikaData::LoadNejlikaData() } LOG("Loaded %d entity templates", entityTemplates.size()); + + if (json.contains("upgrade-templates")) + { + const auto& upgradeTemplatesArray = json["upgrade-templates"]; + + for (const auto& value : upgradeTemplatesArray) + { + auto upgradeTemplate = UpgradeTemplate(value); + + upgradeTemplates[upgradeTemplate.GetLot()] = upgradeTemplate; + } + } } diff --git a/dGame/NejlikaData.h b/dGame/NejlikaData.h index 180ef84b..68ee1998 100644 --- a/dGame/NejlikaData.h +++ b/dGame/NejlikaData.h @@ -7,6 +7,7 @@ #include "EntityTemplate.h" #include "AdditionalItemData.h" #include "AdditionalEntityData.h" +#include "UpgradeTemplate.h" namespace nejlika::NejlikaData { @@ -21,6 +22,8 @@ const std::optional GetAdditionalEntityData(LWOOBJID id); const std::optional GetEntityTemplate(LOT lot); +const std::optional GetUpgradeTemplate(LOT lot); + void SetAdditionalItemData(LWOOBJID id, AdditionalItemData data); void SetAdditionalEntityData(LWOOBJID id, AdditionalEntityData data); diff --git a/dGame/NejlikaHooks.cpp b/dGame/NejlikaHooks.cpp index 8842a774..4a2e2ac5 100644 --- a/dGame/NejlikaHooks.cpp +++ b/dGame/NejlikaHooks.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "NejlikaData.h" @@ -39,6 +40,7 @@ void nejlika::NejlikaHooks::InstallHooks() InventoryComponent::OnItemCreated += [](InventoryComponent* component, Item* item) { const auto& itemType = static_cast(item->GetInfo().itemType); + /* static const std::unordered_set listOfHandledItems { eItemType::HAT, eItemType::CHEST, @@ -51,7 +53,9 @@ void nejlika::NejlikaHooks::InstallHooks() if (listOfHandledItems.find(itemType) == listOfHandledItems.end()) { return; } + */ + // No to the Thinking Hat if (item->GetLot() == 6086) { return; } @@ -69,6 +73,24 @@ void nejlika::NejlikaHooks::InstallHooks() additionalData.RollModifiers(item, levelProgressionComponent->GetLevel()); SetAdditionalItemData(item->GetId(), additionalData); + + 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; + } + + auto& upgradeTemplate = *upgradeTemplateOpt.value(); + + entityData.AddUpgradeItem(item->GetId()); }; EntityManager::OnEntityCreated += [](Entity* entity) { @@ -89,6 +111,29 @@ void nejlika::NejlikaHooks::InstallHooks() auto& additionalData = *additionalDataOpt.value(); additionalData.ApplyToEntity(); + + auto* inventoryComponent = entity->GetComponent(); + + if (!inventoryComponent) { + return; + } + + // Loop through all items and check if they are upgrade items + const auto& inventories = inventoryComponent->GetInventories(); + + for (const auto& [type, inventory] : inventories) { + for (const auto& [id, item] : inventory->GetItems()) { + const auto upgradeTemplateOpt = GetUpgradeTemplate(item->GetLot()); + + if (!upgradeTemplateOpt.has_value()) { + continue; + } + + const auto& upgradeTemplate = *upgradeTemplateOpt.value(); + + additionalData.AddUpgradeItem(id); + } + } }; EntityManager::OnEntityDestroyed += [](Entity* entity) { @@ -101,6 +146,38 @@ void nejlika::NejlikaHooks::InstallHooks() InventoryComponent::OnItemDestroyed += [](InventoryComponent* component, Item* item) { UnsetAdditionalItemData(item->GetId()); + + auto entityDataOpt = GetAdditionalEntityData(component->GetParent()->GetObjectID()); + + if (!entityDataOpt.has_value()) { + return; + } + + auto& entityData = *entityDataOpt.value(); + + entityData.RemoveUpgradeItem(item->GetId()); + }; + + LevelProgressionComponent::OnLevelUp += [](LevelProgressionComponent* component) { + auto* parent = component->GetParent(); + + auto entityDataOpt = GetAdditionalEntityData(parent->GetObjectID()); + + if (!entityDataOpt.has_value()) { + return; + } + + auto& entityData = *entityDataOpt.value(); + + entityData.ApplyToEntity(); + + auto* inventoryComponent = parent->GetComponent(); + + if (!inventoryComponent) { + return; + } + + inventoryComponent->AddItem(2097253, 1, eLootSourceType::LEVEL_REWARD); }; InventoryComponent::OnItemEquipped += [](InventoryComponent* component, Item* item) { @@ -260,6 +337,11 @@ void nejlika::NejlikaHooks::InstallHooks() modifiers.insert(modifiers.end(), skillModifiers.begin(), skillModifiers.end()); } + // Upgrades + const auto upgradeModifiers = offenderEntity.TriggerUpgradeItems(UpgradeTriggerType::OnHit); + + modifiers.insert(modifiers.end(), upgradeModifiers.begin(), upgradeModifiers.end()); + std::unordered_set damageTypes; for (const auto& modifier : modifiers) { @@ -275,13 +357,14 @@ 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::Invalid); uint32_t totalDamage = 0; @@ -291,11 +374,12 @@ void nejlika::NejlikaHooks::InstallHooks() // Calculate resistance, can't go below 20% of the original damage const auto resistance = std::max(1 - (damagedEntity.CalculateResistance(type) / 100), 0.2f); - damageValue *= resistance; + float reductedDamage = damageValue * resistance; - totalDamage += static_cast(damageValue); + totalDamage += static_cast(reductedDamage); - std::cout << "Damage type: " << magic_enum::enum_name(type) << " - " << damageValue << std::endl << " Resistance: " << resistance << std::endl; + std::cout << "Damage type: " << magic_enum::enum_name(type) << " - " << damageValue << std::endl; + std::cout << "Resistance: " << resistance << " - " << reductedDamage << std::endl; std::cout << "Heath left: " << damaged->GetComponent()->GetHealth() << std::endl; } @@ -325,12 +409,23 @@ void nejlika::NejlikaHooks::InstallHooks() roll = std::min(roll, 5.0f); totalDamage += static_cast(totalDamage * roll); + const auto effectName = std::to_string(GeneralUtils::GenerateRandomNumber()); + const auto damagedID = damaged->GetObjectID(); + GameMessages::SendPlayFXEffect( - damaged->GetObjectID(), - 20041, - u"onhit", - std::to_string(GeneralUtils::GenerateRandomNumber()) + damagedID, + 1531, + u"create", + effectName ); + + damaged->AddCallbackTimer(0.5f, [damaged, effectName] () { + GameMessages::SendStopFXEffect( + damaged, + true, + effectName + ); + }); } // Add a random +10% to the damage diff --git a/dGame/UpgradeEffect.cpp b/dGame/UpgradeEffect.cpp new file mode 100644 index 00000000..ac86c60e --- /dev/null +++ b/dGame/UpgradeEffect.cpp @@ -0,0 +1,142 @@ +#include "UpgradeEffect.h" + +#include "GeneralUtils.h" +#include "GameMessages.h" + +#include +#include + +using namespace nejlika; + +nejlika::UpgradeEffect::UpgradeEffect(const nlohmann::json& json) +{ + Load(json); +} + +nlohmann::json nejlika::UpgradeEffect::ToJson() const +{ + nlohmann::json json; + + json["trigger-type"] = static_cast(triggerType); + + nlohmann::json modifiersJson = nlohmann::json::array(); + + for (const auto& modifier : modifiers) { + modifiersJson.push_back(modifier.ToJson()); + } + + json["modifiers"] = modifiersJson; + + if (!chance.empty()) { + nlohmann::json chanceJson = nlohmann::json::array(); + + for (const auto& scale : chance) { + chanceJson.push_back({ + {"level", scale.level}, + {"value", scale.value} + }); + } + + json["chance"] = chanceJson; + } + + if (effectID != 0) { + json["effect-id"] = effectID; + } + + if (!effectType.empty()) { + json["effect-type"] = effectType; + } + + return json; +} + +std::vector nejlika::UpgradeEffect::GenerateModifiers(int32_t level) const +{ + 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::UpgradeEffect::Load(const nlohmann::json& json) +{ + triggerType = magic_enum::enum_cast(json["trigger-type"].get()).value_or(UpgradeTriggerType::OnHit); + + modifiers.clear(); + + for (const auto& modifier : json["modifiers"]) { + ModifierTemplate effect(modifier); + modifiers.push_back(effect); + } + + if (json.contains("chance")) { + chance.clear(); + + for (const auto& scale : json["chance"]) { + chance.push_back({ + scale["level"].get(), + scale["value"].get() + }); + } + } + + if (json.contains("effect-id")) { + effectID = json["effect-id"].get(); + } + + if (json.contains("effect-type")) { + effectType = json["effect-type"].get(); + } +} + +float nejlika::UpgradeEffect::CalculateChance(int32_t level) const { + if (chance.empty()) { + return 1; + } + + // Find the highest level that is less than or equal to the given level + float value = 0; + + for (const auto& scale : chance) { + if (scale.level <= level) { + value = scale.value; + } + } + + return value; +} + +std::vector nejlika::UpgradeEffect::Trigger(const std::vector& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin) { + std::vector result; + + for (const auto& modifier : modifiers) { + if (modifier.GetTriggerType() != triggerType) { + continue; + } + + float chanceRoll = GeneralUtils::GenerateRandomNumber(0, 1); + + if (chanceRoll > modifier.CalculateChance(level)) { + continue; + } + + auto instances = modifier.GenerateModifiers(level); + + result.insert(result.end(), instances.begin(), instances.end()); + + GameMessages::SendPlayFXEffect( + origin, + modifier.effectID, + GeneralUtils::UTF8ToUTF16(modifier.effectType), + std::to_string(GeneralUtils::GenerateRandomNumber()) + ); + } + + return result; +} diff --git a/dGame/UpgradeEffect.h b/dGame/UpgradeEffect.h new file mode 100644 index 00000000..9f03bae3 --- /dev/null +++ b/dGame/UpgradeEffect.h @@ -0,0 +1,46 @@ +#pragma once + +#include "ModifierTemplate.h" +#include "UpgradeTriggerType.h" + +#include + +namespace nejlika +{ + +class UpgradeEffect +{ +public: + UpgradeEffect(const nlohmann::json& json); + + nlohmann::json ToJson() const; + + std::vector GenerateModifiers(int32_t level) const; + + void Load(const nlohmann::json& json); + + float CalculateChance(int32_t level) const; + + static std::vector Trigger(const std::vector& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin); + + // Getters + + const std::vector& GetModifiers() const { return modifiers; } + + UpgradeTriggerType GetTriggerType() const { return triggerType; } + +private: + struct UpgradeScale + { + int32_t level; + float value; + }; + + std::vector chance; + UpgradeTriggerType triggerType; + std::vector modifiers; + int32_t effectID = 0; + std::string effectType = ""; +}; + +} diff --git a/dGame/UpgradeTemplate.cpp b/dGame/UpgradeTemplate.cpp new file mode 100644 index 00000000..dc593856 --- /dev/null +++ b/dGame/UpgradeTemplate.cpp @@ -0,0 +1,49 @@ +#include "UpgradeTemplate.h" + +using namespace nejlika; + +nejlika::UpgradeTemplate::UpgradeTemplate(const nlohmann::json& json) +{ + Load(json); +} + +nlohmann::json nejlika::UpgradeTemplate::ToJson() const +{ + nlohmann::json json; + + json["name"] = name; + json["lot"] = lot; + json["max-level"] = maxLevel; + + nlohmann::json passivesJson = nlohmann::json::array(); + + for (const auto& passive : passives) { + passivesJson.push_back(passive.ToJson()); + } + + json["passives"] = passivesJson; + + return json; +} + +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; + + passives.clear(); + + 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 { + level = std::min(level, maxLevel); + + return UpgradeEffect::Trigger(passives, level, triggerType, origin); +} + + diff --git a/dGame/UpgradeTemplate.h b/dGame/UpgradeTemplate.h new file mode 100644 index 00000000..77178dbb --- /dev/null +++ b/dGame/UpgradeTemplate.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +#include "json.hpp" + +#include "UpgradeEffect.h" + +namespace nejlika +{ + +class UpgradeTemplate +{ +public: + UpgradeTemplate() = default; + + UpgradeTemplate(const nlohmann::json& json); + + nlohmann::json ToJson() const; + + void Load(const nlohmann::json& json); + + const std::string& GetName() const { return name; } + int32_t GetLot() const { return lot; } + int32_t GetMaxLevel() const { return maxLevel; } + const std::vector& GetPassives() const { return passives; } + + std::vector Trigger(int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin) const; + +private: + std::string name = ""; + int32_t lot = 0; + int32_t maxLevel = 0; + std::vector passives; + +}; + +} \ No newline at end of file diff --git a/dGame/UpgradeTriggerType.h b/dGame/UpgradeTriggerType.h new file mode 100644 index 00000000..f4c0fbb5 --- /dev/null +++ b/dGame/UpgradeTriggerType.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace nejlika +{ + +enum class UpgradeTriggerType +{ + OnHit, +}; + +} diff --git a/dGame/dBehaviors/HealBehavior.cpp b/dGame/dBehaviors/HealBehavior.cpp index e2553671..33df64a8 100644 --- a/dGame/dBehaviors/HealBehavior.cpp +++ b/dGame/dBehaviors/HealBehavior.cpp @@ -24,7 +24,13 @@ void HealBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bit_strea return; } - destroyable->Heal(this->m_health); + auto maxHealth = destroyable->GetMaxHealth(); + + // 1 health is 5% of the max health, minimum of 5 health + auto health = static_cast(maxHealth * 0.05f) * this->m_health; + health = std::max(5u, health); + + destroyable->Heal(health); } diff --git a/dGame/dComponents/LevelProgressionComponent.cpp b/dGame/dComponents/LevelProgressionComponent.cpp index a6801a40..ada84845 100644 --- a/dGame/dComponents/LevelProgressionComponent.cpp +++ b/dGame/dComponents/LevelProgressionComponent.cpp @@ -6,6 +6,8 @@ #include "CDRewardsTable.h" +Observable LevelProgressionComponent::OnLevelUp; + LevelProgressionComponent::LevelProgressionComponent(Entity* parent) : Component(parent) { m_Parent = parent; m_Level = 1; @@ -80,6 +82,8 @@ void LevelProgressionComponent::HandleLevelUp() { } // Tell the client we have finished sending level rewards. if (rewardingItem) GameMessages::NotifyLevelRewards(m_Parent->GetObjectID(), m_Parent->GetSystemAddress(), m_Level, !rewardingItem); + + OnLevelUp(this); } void LevelProgressionComponent::SetRetroactiveBaseSpeed(){ diff --git a/dGame/dComponents/LevelProgressionComponent.h b/dGame/dComponents/LevelProgressionComponent.h index e9981ab6..cedfe2de 100644 --- a/dGame/dComponents/LevelProgressionComponent.h +++ b/dGame/dComponents/LevelProgressionComponent.h @@ -5,6 +5,7 @@ #include "Component.h" #include "eCharacterVersion.h" #include "eReplicaComponentType.h" +#include "Observable.h" /** * Component that handles level progression and serilization. @@ -81,6 +82,8 @@ public: */ void SetRetroactiveBaseSpeed(); + static Observable OnLevelUp; + private: /** * whether the level is dirty diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 1f757d7e..ae70fad7 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -973,7 +973,7 @@ void GameMessages::SendResurrect(Entity* entity) { if (levelComponent) { int32_t healthToRestore = levelComponent->GetLevel() >= 45 ? 8 : 4; if (healthToRestore > destroyableComponent->GetMaxHealth()) healthToRestore = destroyableComponent->GetMaxHealth(); - destroyableComponent->SetHealth(healthToRestore); + destroyableComponent->SetHealth(destroyableComponent->GetMaxHealth()); int32_t imaginationToRestore = levelComponent->GetLevel() >= 45 ? 20 : 6; if (imaginationToRestore > destroyableComponent->GetMaxImagination()) imaginationToRestore = destroyableComponent->GetMaxImagination();