diff --git a/dCommon/dEnums/eItemSetPassiveAbilityID.h b/dCommon/dEnums/eItemSetPassiveAbilityID.h deleted file mode 100644 index 8641d0f23..000000000 --- a/dCommon/dEnums/eItemSetPassiveAbilityID.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#ifndef __EITEMSETPASSIVEABILITYID__H__ -#define __EITEMSETPASSIVEABILITYID__H__ - -enum class eItemSetPassiveAbilityID { - EngineerRank1 = 2, - EngineerRank2 = 3, - EngineerRank3 = 4, - KnightRank1 = 7, - KnightRank2 = 8, - KnightRank3 = 9, - SpaceRangerRank1 = 10, - SpaceRangerRank2 = 11, - SpaceRangerRank3 = 12, - SamuraiRank1 = 13, - SamuraiRank2 = 14, - SamuraiRank3 = 15, - SorcererRank1 = 16, - SorcererRank2 = 17, - SorcererRank3 = 18, - SpaceMarauderRank1 = 19, - SpaceMarauderRank2 = 20, - SpaceMarauderRank3 = 21, - ShinobiRank1 = 22, - ShinobiRank2 = 23, - ShinobiRank3 = 24, - InventorRank1 = 25, - InventorRank2 = 26, - InventorRank3 = 27, - SummonerRank1 = 28, - SummonerRank2 = 29, - SummonerRank3 = 30, - AdventurerRank1 = 31, - AdventurerRank2 = 32, - AdventurerRank3 = 33, - DaredevilRank1 = 34, - DaredevilRank2 = 35, - DaredevilRank3 = 36, - BuccaneerRank1 = 37, - BuccaneerRank2 = 38, - BuccaneerRank3 = 39, - BoneSuit = 40, - ImaginationSpinjitzu = 41, - BatLord = 42, - MosaicJester = 43, - ExplorienBot = 44, - Unnamed1 = 45, - Unnamed2 = 46, - Unnamed3 = 47, - EarthSpinjitzu = 48, - Unnamed4 = 49, - FireSpinjitzu = 50, - IceSpinjitzu = 51, - LightningSpinjitzu = 52 -}; - -#endif //!__EITEMSETPASSIVEABILITYID__H__ diff --git a/dGame/dBehaviors/ImmunityBehavior.cpp b/dGame/dBehaviors/ImmunityBehavior.cpp index 77d438342..e186446c4 100644 --- a/dGame/dBehaviors/ImmunityBehavior.cpp +++ b/dGame/dBehaviors/ImmunityBehavior.cpp @@ -96,6 +96,10 @@ void ImmunityBehavior::Timer(BehaviorContext* context, BehaviorBranchContext bra } +void ImmunityBehavior::UnCast(BehaviorContext* context, BehaviorBranchContext branch) { + Timer(context, branch, branch.target); +} + void ImmunityBehavior::Load() { //Stun this->m_ImmuneToStunAttack = GetBoolean("immune_stun_attack", false); diff --git a/dGame/dBehaviors/ImmunityBehavior.h b/dGame/dBehaviors/ImmunityBehavior.h index d2f28b959..0e03f3fec 100644 --- a/dGame/dBehaviors/ImmunityBehavior.h +++ b/dGame/dBehaviors/ImmunityBehavior.h @@ -17,6 +17,9 @@ public: void Timer(BehaviorContext* context, BehaviorBranchContext branch, LWOOBJID second) override; + // Removes the immunities again when an equipped item's or item set's skill is uncast + void UnCast(BehaviorContext* context, BehaviorBranchContext branch) override; + void Load() override; private: diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 22c750434..2e9e5ed8d 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -470,18 +470,10 @@ bool DestroyableComponent::IsCooldownImmune() const { bool DestroyableComponent::IsKnockbackImmune() const { auto* characterComponent = m_Parent->GetComponent(); - auto* inventoryComponent = m_Parent->GetComponent(); - if (characterComponent != nullptr && inventoryComponent != nullptr && characterComponent->GetCurrentActivity() == eGameActivity::QUICKBUILDING) { - const auto hasPassive = inventoryComponent->HasAnyPassive({ - eItemSetPassiveAbilityID::EngineerRank2, eItemSetPassiveAbilityID::EngineerRank3, - eItemSetPassiveAbilityID::SummonerRank2, eItemSetPassiveAbilityID::SummonerRank3, - eItemSetPassiveAbilityID::InventorRank2, eItemSetPassiveAbilityID::InventorRank3, - }, 5); - - if (hasPassive) { - return true; - } + // Quickbuild interrupt immunity comes from Immunity behaviors, e.g. the 5 item Assembly rank 2 and 3 set skill (584) + if (characterComponent != nullptr && characterComponent->GetCurrentActivity() == eGameActivity::QUICKBUILDING && GetImmuneToQuickbuildInterrupt()) { + return true; } return IsImmune() || m_IsShielded || m_AttacksToBlock > 0; diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index bf90b201c..65e4cc6b7 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -1357,21 +1357,6 @@ void InventoryComponent::TriggerPassiveAbility(PassiveAbilityTrigger trigger, En } } -bool InventoryComponent::HasAnyPassive(const std::vector& passiveIDs, int32_t equipmentRequirement) const { - for (auto* set : m_Itemsets) { - if (set->GetEquippedCount() < equipmentRequirement) { - continue; - } - - // Check if the set has any of the passive abilities - if (std::find(passiveIDs.begin(), passiveIDs.end(), static_cast(set->GetID())) != passiveIDs.end()) { - return true; - } - } - - return false; -} - void InventoryComponent::DespawnPet() { auto* current = PetComponent::GetActivePet(m_Parent->GetObjectID()); diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index f43d706c0..c943e5d77 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -17,7 +17,6 @@ #include "DatabasePet.h" #include "Component.h" #include "ItemSetPassiveAbility.h" -#include "eItemSetPassiveAbilityID.h" #include "PossessorComponent.h" #include "eInventoryType.h" #include "eReplicaComponentType.h" @@ -358,14 +357,6 @@ public: */ void TriggerPassiveAbility(PassiveAbilityTrigger trigger, Entity* target = nullptr); - /** - * Returns if the entity has any of the passed passive abilities equipped - * @param passiveIDs the IDs to check for - * @param equipmentRequirement the number of equipment required to be allowed to have the ability - * @return if the entity has any of the passed passive abilities equipped - */ - bool HasAnyPassive(const std::vector& passiveIDs, int32_t equipmentRequirement) const; - /** * Despawns the currently active pet, if any */ diff --git a/dGame/dComponents/SkillComponent.cpp b/dGame/dComponents/SkillComponent.cpp index c2771816f..e745f81b9 100644 --- a/dGame/dComponents/SkillComponent.cpp +++ b/dGame/dComponents/SkillComponent.cpp @@ -466,6 +466,19 @@ void SkillComponent::HandleUnmanaged(const uint32_t behaviorId, const LWOOBJID t behavior->Handle(&context, bitStream, { target }); } +void SkillComponent::CalculateUnmanaged(const uint32_t behaviorId, const LWOOBJID caster, const LWOOBJID target) { + BehaviorContext context{ caster, true }; + + context.unmanaged = true; + context.caster = caster; + + auto* behavior = Behavior::CreateBehavior(behaviorId); + + RakNet::BitStream bitStream{}; + + behavior->Calculate(&context, bitStream, { target }); +} + void SkillComponent::HandleUnCast(const uint32_t behaviorId, const LWOOBJID target) { BehaviorContext context{ target }; diff --git a/dGame/dComponents/SkillComponent.h b/dGame/dComponents/SkillComponent.h index 84c3b277a..6cea05929 100644 --- a/dGame/dComponents/SkillComponent.h +++ b/dGame/dComponents/SkillComponent.h @@ -174,6 +174,15 @@ public: */ static void HandleUnmanaged(uint32_t behaviorId, LWOOBJID target, LWOOBJID source = LWOOBJID_EMPTY); + /** + * Calculates a behavior on the server only, without syncing it to clients, for effects the + * clients already run on their own (e.g. item set DarkInspiration status effects). + * @param behaviorId the root behavior ID of the skill + * @param caster the entity the behavior is cast by + * @param target the entity the behavior targets + */ + static void CalculateUnmanaged(uint32_t behaviorId, LWOOBJID caster, LWOOBJID target); + /** * Computes a server-side skill uncast calculation without an associated entity. * @param behaviorId the root behavior ID of the skill diff --git a/dGame/dInventory/ItemSet.cpp b/dGame/dInventory/ItemSet.cpp index 1544e6119..cd71cc1b9 100644 --- a/dGame/dInventory/ItemSet.cpp +++ b/dGame/dInventory/ItemSet.cpp @@ -18,8 +18,6 @@ ItemSet::ItemSet(const uint32_t id, InventoryComponent* inventoryComponent) { this->m_ID = id; this->m_InventoryComponent = inventoryComponent; - this->m_PassiveAbilities = ItemSetPassiveAbility::FindAbilities(id, m_InventoryComponent->GetParent(), this); - auto query = CDClientDatabase::CreatePreppedStmt( "SELECT skillSetWith2, skillSetWith3, skillSetWith4, skillSetWith5, skillSetWith6, itemIDs FROM ItemSets WHERE setID = ?;"); query.bind(1, static_cast(id)); @@ -44,7 +42,7 @@ ItemSet::ItemSet(const uint32_t id, InventoryComponent* inventoryComponent) { auto skillResult = skillQuery.execQuery(); if (skillResult.eof()) { - return; + continue; } while (!skillResult.eof()) { @@ -96,6 +94,8 @@ ItemSet::ItemSet(const uint32_t id, InventoryComponent* inventoryComponent) { if (validToken) m_Items.push_back(validToken.value()); } + m_PassiveAbilities = ItemSetPassiveAbility::FindAbilities(id, m_Items, { m_SkillsWith2, m_SkillsWith3, m_SkillsWith4, m_SkillsWith5, m_SkillsWith6 }, m_InventoryComponent->GetParent(), this); + m_Equipped = {}; for (const auto item : m_Items) { diff --git a/dGame/dInventory/ItemSetPassiveAbility.cpp b/dGame/dInventory/ItemSetPassiveAbility.cpp index 3030904d6..4a3eef056 100644 --- a/dGame/dInventory/ItemSetPassiveAbility.cpp +++ b/dGame/dInventory/ItemSetPassiveAbility.cpp @@ -1,9 +1,68 @@ #include "ItemSetPassiveAbility.h" +#include +#include +#include + #include "DestroyableComponent.h" #include "SkillComponent.h" #include "ItemSet.h" -#include "eItemSetPassiveAbilityID.h" +#include "EntityManager.h" +#include "CDClientManager.h" +#include "CDComponentsRegistryTable.h" +#include "CDScriptComponentTable.h" +#include "CDSkillBehaviorTable.h" +#include "Behavior.h" +#include "eReplicaComponentType.h" + +namespace { + struct ScriptStatTrigger { + std::string_view script; + ItemSetStatTrigger trigger; + }; + + constexpr auto LowImagination = PassiveAbilityTrigger::AssemblyImagination; + constexpr auto LowArmor = PassiveAbilityTrigger::SentinelArmor; + + // The CDClient links set items to these scripts through their ScriptComponent, but what each + // script does only exists in the scripts themselves (res/scripts/equipmenttriggers/.lua, + // which all run skillSetTriggerTemplate.lua with these vars), so they are mirrored here like dScripts. + // Paradox and Venture have scripts of the same kind (paradox1-3, venture*) but no item uses them. + constexpr ScriptStatTrigger StatTriggerScripts[] = { + // Low Imagination, IMAGINATION LESS 1 + { "assembly1.lua", { LowImagination, 394, 4, 2, 0.0f } }, + { "assembly2.lua", { LowImagination, 581, 4, 3, 0.0f } }, + { "assembly3.lua", { LowImagination, 582, 4, 4, 0.0f } }, + { "assemblyengineer1.lua", { LowImagination, 394, 4, 2, 11.0f } }, + { "assemblyengineer2.lua", { LowImagination, 581, 4, 3, 11.0f } }, + { "assemblyengineer3.lua", { LowImagination, 582, 4, 4, 11.0f } }, + { "assemblyinventor1.lua", { LowImagination, 394, 4, 25, 11.0f } }, + { "assemblyinventor2.lua", { LowImagination, 581, 4, 26, 11.0f } }, + { "assemblyinventor3.lua", { LowImagination, 582, 4, 27, 11.0f } }, + { "assemblysummoner1.lua", { LowImagination, 394, 4, 28, 11.0f } }, + { "assemblysummoner2.lua", { LowImagination, 581, 4, 29, 11.0f } }, + { "assemblysummoner3.lua", { LowImagination, 582, 4, 30, 11.0f } }, + // Low Armor, ARMOR LESS 1 + { "sentinelknight1.lua", { LowArmor, 559, 4, 7, 0.0f } }, + { "sentinelknight2.lua", { LowArmor, 560, 4, 8, 0.0f } }, + { "sentinelknight3.lua", { LowArmor, 561, 4, 9, 0.0f } }, + { "sentinelspaceranger1.lua", { LowArmor, 1101, 4, 10, 0.0f } }, + { "sentinelspaceranger2.lua", { LowArmor, 1102, 4, 11, 0.0f } }, + { "sentinelspaceranger3.lua", { LowArmor, 1103, 4, 12, 0.0f } }, + { "sentinelsamurai1.lua", { LowArmor, 562, 4, 13, 0.0f } }, + { "sentinelsamurai2.lua", { LowArmor, 563, 4, 14, 0.0f } }, + { "sentinelsamurai3.lua", { LowArmor, 564, 4, 15, 0.0f } }, + }; + + std::string ScriptFileName(std::string_view scriptName) { + const auto slash = scriptName.find_last_of("/\\"); + if (slash != std::string_view::npos) scriptName.remove_prefix(slash + 1); + + std::string fileName{ scriptName }; + std::transform(fileName.begin(), fileName.end(), fileName.begin(), [](unsigned char c) { return std::tolower(c); }); + return fileName; + } +} ItemSetPassiveAbility::ItemSetPassiveAbility(PassiveAbilityTrigger trigger, Entity* parent, ItemSet* itemSet) { m_Trigger = trigger; @@ -13,9 +72,6 @@ ItemSetPassiveAbility::ItemSetPassiveAbility(PassiveAbilityTrigger trigger, Enti m_Cooldown = 0.0f; } -ItemSetPassiveAbility::~ItemSetPassiveAbility() { -} - void ItemSetPassiveAbility::Trigger(PassiveAbilityTrigger trigger, Entity* target) { if (m_Trigger != trigger || m_Cooldown > 0.0f) { return; @@ -32,7 +88,7 @@ void ItemSetPassiveAbility::Update(float deltaTime) { void ItemSetPassiveAbility::Activate(Entity* target) { if (m_Trigger == PassiveAbilityTrigger::EnemySmashed) { - OnEnemySmshed(target); + OnEnemySmashed(target); return; } @@ -46,156 +102,91 @@ void ItemSetPassiveAbility::Activate(Entity* target) { Game::entityManager->SerializeEntity(m_Parent); - const auto id = static_cast(m_ItemSet->GetID()); - const auto parentID = m_Parent->GetObjectID(); - const auto equippedCount = m_ItemSet->GetEquippedCount(); + if (m_ItemSet->GetEquippedCount() < m_StatTrigger.itemsRequired) return; - switch (id) { - // Assembly - case eItemSetPassiveAbilityID::InventorRank1: - case eItemSetPassiveAbilityID::SummonerRank1: - case eItemSetPassiveAbilityID::EngineerRank1: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(394, 4401, parentID); - break; - } - case eItemSetPassiveAbilityID::InventorRank2: - case eItemSetPassiveAbilityID::SummonerRank2: - case eItemSetPassiveAbilityID::EngineerRank2: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(581, 9433, parentID); - break; - } - case eItemSetPassiveAbilityID::InventorRank3: - case eItemSetPassiveAbilityID::SummonerRank3: - case eItemSetPassiveAbilityID::EngineerRank3: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(582, 9435, parentID); - break; - } - - // Sentinel - case eItemSetPassiveAbilityID::KnightRank1: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(559, 8884, parentID); - break; - } - case eItemSetPassiveAbilityID::KnightRank2: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(560, 8885, parentID); - break; - } - case eItemSetPassiveAbilityID::KnightRank3: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(561, 8890, parentID); - break; - } - - case eItemSetPassiveAbilityID::SpaceRangerRank1: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(1101, 24612, parentID); - break; - } - case eItemSetPassiveAbilityID::SpaceRangerRank2: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(1102, 24617, parentID); - break; - } - case eItemSetPassiveAbilityID::SpaceRangerRank3: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(1103, 24622, parentID); - break; - } - - case eItemSetPassiveAbilityID::SamuraiRank1: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(562, 8899, parentID); - break; - } - case eItemSetPassiveAbilityID::SamuraiRank2: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(563, 8904, parentID); - break; - } - case eItemSetPassiveAbilityID::SamuraiRank3: { - if (equippedCount < 4) return; - m_Cooldown = 11.0f; - skillComponent->CalculateBehavior(564, 8909, parentID); - break; - } - - default: - break; - } + m_Cooldown = m_StatTrigger.cooldown; + skillComponent->CastSkill(m_StatTrigger.skillID, m_Parent->GetObjectID()); } -std::vector ItemSetPassiveAbility::FindAbilities(uint32_t itemSetID, Entity* parent, ItemSet* itemSet) { +const ItemSetStatTrigger* ItemSetPassiveAbility::GetStatTriggerForScript(std::string_view scriptName) { + const auto fileName = ScriptFileName(scriptName); + for (const auto& entry : StatTriggerScripts) { + if (entry.script == fileName) return &entry.trigger; + } + return nullptr; +} + +std::vector ItemSetPassiveAbility::FindStatTriggers(uint32_t itemSetID, const std::vector& items) { + std::vector triggers; + + auto* registryTable = CDClientManager::GetTable(); + auto* scriptTable = CDClientManager::GetTable(); + + for (const auto lot : items) { + const auto scriptComponentID = registryTable->GetByIDAndType(lot, eReplicaComponentType::SCRIPT, -1); + if (scriptComponentID < 0) continue; + + const auto* trigger = GetStatTriggerForScript(scriptTable->GetByID(scriptComponentID).script_name); + + // Like the script, only fire for the set it was written for + if (!trigger || trigger->setID != itemSetID) continue; + + // Every item of a set carries the same script, but only one of them casts the skill + const auto alreadyFound = std::any_of(triggers.begin(), triggers.end(), [trigger](const ItemSetStatTrigger& found) { + return found.trigger == trigger->trigger && found.skillID == trigger->skillID; + }); + if (!alreadyFound) triggers.push_back(*trigger); + } + + return triggers; +} + +std::vector ItemSetPassiveAbility::FindOnKillSkills(const std::array, 5>& skillSets) { + std::vector onKillSkills; + + auto* skillTable = CDClientManager::GetTable(); + + for (size_t tier = 0; tier < skillSets.size(); ++tier) { + for (const auto skillID : skillSets[tier]) { + const auto behaviorID = skillTable->GetSkillByID(skillID).behaviorID; + if (behaviorID == 0) continue; + + if (Behavior::GetBehaviorTemplate(behaviorID) != BehaviorTemplate::DARK_INSPIRATION) continue; + + // The client keys the status effect by behavior ID, so a behavior repeated in a higher tier + // (e.g. the Sentinel rank 3 armor repair at 5 and 6 items) does not stack. Keep the lowest tier. + const auto alreadyFound = std::any_of(onKillSkills.begin(), onKillSkills.end(), [behaviorID](const ItemSetOnKillSkill& found) { + return found.behaviorID == behaviorID; + }); + if (alreadyFound) continue; + + onKillSkills.push_back({ static_cast(tier + 2), skillID, behaviorID }); + } + } + + return onKillSkills; +} + +std::vector ItemSetPassiveAbility::FindAbilities(uint32_t itemSetID, const std::vector& items, const std::array, 5>& skillSets, Entity* parent, ItemSet* itemSet) { std::vector abilities; - switch (static_cast(itemSetID)) { - // Assembly - case eItemSetPassiveAbilityID::SummonerRank1: - case eItemSetPassiveAbilityID::SummonerRank2: - case eItemSetPassiveAbilityID::SummonerRank3: - case eItemSetPassiveAbilityID::InventorRank1: - case eItemSetPassiveAbilityID::InventorRank2: - case eItemSetPassiveAbilityID::InventorRank3: - case eItemSetPassiveAbilityID::EngineerRank1: - case eItemSetPassiveAbilityID::EngineerRank2: - case eItemSetPassiveAbilityID::EngineerRank3: { - abilities.emplace_back(PassiveAbilityTrigger::AssemblyImagination, parent, itemSet); - - break; + for (const auto& statTrigger : FindStatTriggers(itemSetID, items)) { + auto& ability = abilities.emplace_back(statTrigger.trigger, parent, itemSet); + ability.m_StatTrigger = statTrigger; } - // Sentinel - case eItemSetPassiveAbilityID::KnightRank1: - case eItemSetPassiveAbilityID::KnightRank2: - case eItemSetPassiveAbilityID::KnightRank3: - case eItemSetPassiveAbilityID::SpaceRangerRank1: - case eItemSetPassiveAbilityID::SpaceRangerRank2: - case eItemSetPassiveAbilityID::SpaceRangerRank3: - case eItemSetPassiveAbilityID::SamuraiRank1: - case eItemSetPassiveAbilityID::SamuraiRank2: - case eItemSetPassiveAbilityID::SamuraiRank3: { - abilities.emplace_back(PassiveAbilityTrigger::SentinelArmor, parent, itemSet); - abilities.emplace_back(PassiveAbilityTrigger::EnemySmashed, parent, itemSet); - break; - } - // Paradox - case eItemSetPassiveAbilityID::BatLord: - case eItemSetPassiveAbilityID::SpaceMarauderRank1: - case eItemSetPassiveAbilityID::SpaceMarauderRank2: - case eItemSetPassiveAbilityID::SpaceMarauderRank3: - case eItemSetPassiveAbilityID::SorcererRank1: - case eItemSetPassiveAbilityID::SorcererRank2: - case eItemSetPassiveAbilityID::SorcererRank3: - case eItemSetPassiveAbilityID::ShinobiRank1: - case eItemSetPassiveAbilityID::ShinobiRank2: - case eItemSetPassiveAbilityID::ShinobiRank3: { - abilities.emplace_back(PassiveAbilityTrigger::EnemySmashed, parent, itemSet); - - break; - } - default: - break; + auto onKillSkills = FindOnKillSkills(skillSets); + if (!onKillSkills.empty()) { + auto& ability = abilities.emplace_back(PassiveAbilityTrigger::EnemySmashed, parent, itemSet); + ability.m_OnKillSkills = std::move(onKillSkills); } return abilities; } -void ItemSetPassiveAbility::OnEnemySmshed(Entity* target) { +void ItemSetPassiveAbility::OnEnemySmashed(Entity* target) { + if (!target) return; + auto* destroyableComponent = m_Parent->GetComponent(); auto* skillComponent = m_Parent->GetComponent(); @@ -203,118 +194,16 @@ void ItemSetPassiveAbility::OnEnemySmshed(Entity* target) { return; } - Game::entityManager->SerializeEntity(m_Parent); - - const auto id = static_cast(m_ItemSet->GetID()); - const auto parentID = m_Parent->GetObjectID(); const auto equippedCount = m_ItemSet->GetEquippedCount(); - switch (id) { - // Bat Lord - case eItemSetPassiveAbilityID::BatLord: { - if (equippedCount < 5) return; - destroyableComponent->Heal(3); - break; - } - // Sentinel - case eItemSetPassiveAbilityID::KnightRank1: { - if (equippedCount < 5) return; - destroyableComponent->Repair(1); - break; - } - case eItemSetPassiveAbilityID::KnightRank2: { - if (equippedCount < 5) return; - destroyableComponent->Repair(1); - break; - } - case eItemSetPassiveAbilityID::KnightRank3: { - if (equippedCount < 5) return; - destroyableComponent->Repair(1); - break; + // The client adds a status effect for each DarkInspiration set skill when it is cast on equip, + // which casts the behavior's action when the wearer kills something of a faction in its faction_list. + // A tier's skills stay cast while at least that many items are equipped. + for (const auto& onKillSkill : m_OnKillSkills) { + if (equippedCount < onKillSkill.itemsRequired) continue; + + SkillComponent::CalculateUnmanaged(onKillSkill.behaviorID, m_Parent->GetObjectID(), target->GetObjectID()); } - case eItemSetPassiveAbilityID::SpaceRangerRank1: { - if (equippedCount < 5) return; - destroyableComponent->Repair(1); - break; - } - case eItemSetPassiveAbilityID::SpaceRangerRank2: { - if (equippedCount < 5) return; - destroyableComponent->Repair(1); - break; - } - case eItemSetPassiveAbilityID::SpaceRangerRank3: { - if (equippedCount < 5) return; - destroyableComponent->Repair(1); - break; - } - - case eItemSetPassiveAbilityID::SamuraiRank1: { - if (equippedCount < 5) return; - destroyableComponent->Repair(1); - break; - } - case eItemSetPassiveAbilityID::SamuraiRank2: { - if (equippedCount < 5) return; - destroyableComponent->Repair(1); - break; - } - case eItemSetPassiveAbilityID::SamuraiRank3: { - if (equippedCount < 5) return; - destroyableComponent->Repair(1); - break; - } - - // Paradox - case eItemSetPassiveAbilityID::SpaceMarauderRank1: { - if (equippedCount < 4) return; - destroyableComponent->Imagine(1); - break; - } - case eItemSetPassiveAbilityID::SpaceMarauderRank2: { - if (equippedCount < 4) return; - destroyableComponent->Imagine(2); - break; - } - case eItemSetPassiveAbilityID::SpaceMarauderRank3: { - if (equippedCount < 4) return; - destroyableComponent->Imagine(3); - break; - } - - case eItemSetPassiveAbilityID::ShinobiRank1: { - if (equippedCount < 4) return; - destroyableComponent->Imagine(1); - break; - } - case eItemSetPassiveAbilityID::ShinobiRank2: { - if (equippedCount < 4) return; - destroyableComponent->Imagine(2); - break; - } - case eItemSetPassiveAbilityID::ShinobiRank3: { - if (equippedCount < 4) return; - destroyableComponent->Imagine(3); - break; - } - - case eItemSetPassiveAbilityID::SorcererRank1: { - if (equippedCount < 4) return; - destroyableComponent->Imagine(1); - break; - } - case eItemSetPassiveAbilityID::SorcererRank2: { - if (equippedCount < 4) return; - destroyableComponent->Imagine(2); - break; - } - case eItemSetPassiveAbilityID::SorcererRank3: { - if (equippedCount < 4) return; - destroyableComponent->Imagine(3); - break; - } - - default: - break; - } + Game::entityManager->SerializeEntity(m_Parent); } diff --git a/dGame/dInventory/ItemSetPassiveAbility.h b/dGame/dInventory/ItemSetPassiveAbility.h index 8735e6959..253a05fd4 100644 --- a/dGame/dInventory/ItemSetPassiveAbility.h +++ b/dGame/dInventory/ItemSetPassiveAbility.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include "dCommonVars.h" @@ -16,6 +18,28 @@ enum class PassiveAbilityTrigger EnemySmashed, // Enemy is smashed }; +/** + * What a stat-triggered set ability does, as set up by one of the live + * server's res/scripts/equipmenttriggers item scripts. + */ +struct ItemSetStatTrigger { + PassiveAbilityTrigger trigger; // the Lua "trigger" var (stat + threshold) + uint32_t skillID; // the Lua "skillID" var + uint32_t itemsRequired; // the Lua "itemsRequired" var + uint32_t setID; // the Lua "skillSet" var: the ItemSets row the script belongs to + float cooldown; // the Lua "factionCooldownTime" var; 0 for scripts without "isFactionSkill" +}; + +/** + * One set skill that runs when the wearer smashes something: a skill in the set's + * ItemSetSkills whose root behavior is DarkInspiration (the client's "OnKillFactionBehavior"). + */ +struct ItemSetOnKillSkill { + uint32_t itemsRequired; // the skillSetWithN tier the skill comes from + uint32_t skillID; + uint32_t behaviorID; +}; + /** * Passive abilities that belong to an item set, activated when a PassiveAbilityTrigger condition is met */ @@ -23,7 +47,6 @@ class ItemSetPassiveAbility { public: ItemSetPassiveAbility(PassiveAbilityTrigger trigger, Entity* parent, ItemSet* itemSet); - ~ItemSetPassiveAbility(); void Update(float deltaTime); /** @@ -38,22 +61,51 @@ public: void Activate(Entity* target = nullptr); /** - * Finds all the passive abilities associated with a certain item set + * Finds all the passive abilities of an item set, from the CDClient and the item scripts on its items * @param itemSetID the item set to find abilities for + * @param items the LOTs in the set (ItemSets.itemIDs) + * @param skillSets the skills of each skillSetWithN tier, index 0 being skillSetWith2 * @param parent the parent to add to the passive abilities * @param itemSet the item set to add to the passive abilities * @return the passive abilities for the provided item set */ - static std::vector FindAbilities(uint32_t itemSetID, Entity* parent, ItemSet* itemSet); + static std::vector FindAbilities(uint32_t itemSetID, const std::vector& items, const std::array, 5>& skillSets, Entity* parent, ItemSet* itemSet); + + /** + * The stat triggers the set's items carry. The live server set these up in the equipmenttriggers + * item scripts, which are linked to the items through their ScriptComponent. + */ + static std::vector FindStatTriggers(uint32_t itemSetID, const std::vector& items); + + /** + * The on-kill skills among the set's skills: those whose root behavior is DarkInspiration. + */ + static std::vector FindOnKillSkills(const std::array, 5>& skillSets); + + /** + * The trigger a live equipmenttriggers script sets up, by the script's file name (any path, any case). + * @return nullptr when the script is not a set stat trigger + */ + static const ItemSetStatTrigger* GetStatTriggerForScript(std::string_view scriptName); private: - void OnEnemySmshed(Entity* target = nullptr); + void OnEnemySmashed(Entity* target); /** * The means of triggering this ability */ PassiveAbilityTrigger m_Trigger; + /** + * What a stat trigger does, unused for EnemySmashed + */ + ItemSetStatTrigger m_StatTrigger{}; + + /** + * The skills an EnemySmashed ability runs + */ + std::vector m_OnKillSkills; + /** * The owner of this ability */ diff --git a/tests/dGameTests/CMakeLists.txt b/tests/dGameTests/CMakeLists.txt index 400dcffcf..e33d85c92 100644 --- a/tests/dGameTests/CMakeLists.txt +++ b/tests/dGameTests/CMakeLists.txt @@ -9,6 +9,7 @@ set(DGAMETEST_SOURCES "ContrabandTests.cpp" "KnockbackTests.cpp" "CollisionFilterTests.cpp" + "ItemSetPassiveAbilityTests.cpp" ) add_subdirectory(dComponentsTests) @@ -26,6 +27,8 @@ file(COPY ${COMPONENT_TEST_DATA} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) # Add the executable. Remember to add all tests above this! add_executable(dGameTests ${DGAMETEST_SOURCES}) target_include_directories(dGameTests PRIVATE "." "${PROJECT_SOURCE_DIR}/dChatServer" "${PROJECT_SOURCE_DIR}/dScripts") +# Tests against the live CDClient look for it in the source tree's build folder +target_compile_definitions(dGameTests PRIVATE PROJECT_SOURCE_DIR="${PROJECT_SOURCE_DIR}") if(APPLE) add_custom_target(dGameTestsLink diff --git a/tests/dGameTests/ItemSetPassiveAbilityTests.cpp b/tests/dGameTests/ItemSetPassiveAbilityTests.cpp new file mode 100644 index 000000000..d746b0a47 --- /dev/null +++ b/tests/dGameTests/ItemSetPassiveAbilityTests.cpp @@ -0,0 +1,258 @@ +#include + +#include +#include +#include +#include + +#include "GameDependencies.h" +#include "ItemSetPassiveAbility.h" +#include "CDClientDatabase.h" +#include "CDClientManager.h" +#include "CDScriptComponentTable.h" +#include "CDSkillBehaviorTable.h" +#include "CDBehaviorTemplateTable.h" + +// Item set passive abilities come from the CDClient (DarkInspiration set skills) and the item +// scripts linked to the set items (equipmenttriggers). These tests check both against the values +// the server used to hardcode per set ID. + +namespace { + std::array, 5> ReadSkillSets(uint32_t setID) { + std::array, 5> skillSets; + auto query = CDClientDatabase::CreatePreppedStmt( + "SELECT skillSetWith2, skillSetWith3, skillSetWith4, skillSetWith5, skillSetWith6 FROM ItemSets WHERE setID = ?;"); + query.bind(1, static_cast(setID)); + auto result = query.execQuery(); + if (result.eof()) return skillSets; + for (int i = 0; i < 5; ++i) { + if (result.fieldIsNull(i)) continue; + auto skillQuery = CDClientDatabase::CreatePreppedStmt("SELECT SkillID FROM ItemSetSkills WHERE SkillSetID = ?;"); + skillQuery.bind(1, result.getIntField(i)); + auto skills = skillQuery.execQuery(); + while (!skills.eof()) { + skillSets[i].push_back(skills.getIntField(0)); + skills.nextRow(); + } + } + return skillSets; + } + + std::vector ReadItems(uint32_t setID) { + std::vector items; + auto query = CDClientDatabase::CreatePreppedStmt("SELECT itemIDs FROM ItemSets WHERE setID = ?;"); + query.bind(1, static_cast(setID)); + auto result = query.execQuery(); + if (result.eof()) return items; + std::string ids = result.getStringField(0); + std::string token; + for (const char c : ids + ",") { + if (c == ',') { + if (!token.empty()) items.push_back(std::stoi(token)); + token.clear(); + } else if (!std::isspace(static_cast(c))) { + token += c; + } + } + return items; + } + + float Parameter(uint32_t behaviorID, const std::string& name) { + auto query = CDClientDatabase::CreatePreppedStmt("SELECT value FROM BehaviorParameter WHERE behaviorID = ? AND parameterID = ?;"); + query.bind(1, static_cast(behaviorID)); + query.bind(2, name.c_str()); + auto result = query.execQuery(); + return result.eof() ? -1.0f : static_cast(result.getFloatField(0)); + } + + uint32_t Template(uint32_t behaviorID) { + auto query = CDClientDatabase::CreatePreppedStmt("SELECT templateID FROM BehaviorTemplate WHERE behaviorID = ?;"); + query.bind(1, static_cast(behaviorID)); + auto result = query.execQuery(); + return result.eof() ? 0 : result.getIntField(0); + } + + // What a DarkInspiration on-kill behavior ends up doing to the wearer: "