mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-03 03:13:50 +00:00
refactor: item set passive abilities come from the CDClient and item scripts
The server hardcoded each item set's passives by set ID. They now come from data: - On-kill bonuses (Paradox imagination, Sentinel armor repair, Bat Lord heal) are the set's DarkInspiration skills in ItemSetSkills. The client turns those into a status effect that casts the behavior's action when the wearer kills something of a faction in faction_list; the server now runs the same behavior on the smashed enemy, so the amounts, faction check and extra effects (e.g. rank 3 Sorcerer team imagination) follow the data. A behavior repeated in a higher tier does not stack. - Low imagination / low armor skills are what the live equipmenttriggers item scripts do. Items link to those scripts through their ScriptComponent; the script vars (skill, items required, set, cooldown) only exist in the scripts, so they are mirrored in a small table keyed by script name. The Sentinel scripts have no cooldown (was 11s). - Knockback immunity while quickbuilding comes from the Immunity behavior's immune_quickbuild_interrupts (the 5 item Assembly rank 2/3 set skill) instead of a set ID list; ImmunityBehavior now pops its immunities when an equip skill is uncast. Removes eItemSetPassiveAbilityID and InventoryComponent::HasAnyPassive (unused now). Refs #691 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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__
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -470,18 +470,10 @@ bool DestroyableComponent::IsCooldownImmune() const {
|
||||
|
||||
bool DestroyableComponent::IsKnockbackImmune() const {
|
||||
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
|
||||
auto* inventoryComponent = m_Parent->GetComponent<InventoryComponent>();
|
||||
|
||||
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;
|
||||
|
||||
@@ -1357,21 +1357,6 @@ void InventoryComponent::TriggerPassiveAbility(PassiveAbilityTrigger trigger, En
|
||||
}
|
||||
}
|
||||
|
||||
bool InventoryComponent::HasAnyPassive(const std::vector<eItemSetPassiveAbilityID>& 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<eItemSetPassiveAbilityID>(set->GetID())) != passiveIDs.end()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void InventoryComponent::DespawnPet() {
|
||||
auto* current = PetComponent::GetActivePet(m_Parent->GetObjectID());
|
||||
|
||||
|
||||
@@ -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<eItemSetPassiveAbilityID>& passiveIDs, int32_t equipmentRequirement) const;
|
||||
|
||||
/**
|
||||
* Despawns the currently active pet, if any
|
||||
*/
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<int>(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) {
|
||||
|
||||
@@ -1,9 +1,68 @@
|
||||
#include "ItemSetPassiveAbility.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
|
||||
#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/<name>.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<eItemSetPassiveAbilityID>(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> 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<ItemSetStatTrigger> ItemSetPassiveAbility::FindStatTriggers(uint32_t itemSetID, const std::vector<LOT>& items) {
|
||||
std::vector<ItemSetStatTrigger> triggers;
|
||||
|
||||
auto* registryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
||||
auto* scriptTable = CDClientManager::GetTable<CDScriptComponentTable>();
|
||||
|
||||
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<ItemSetOnKillSkill> ItemSetPassiveAbility::FindOnKillSkills(const std::array<std::vector<uint32_t>, 5>& skillSets) {
|
||||
std::vector<ItemSetOnKillSkill> onKillSkills;
|
||||
|
||||
auto* skillTable = CDClientManager::GetTable<CDSkillBehaviorTable>();
|
||||
|
||||
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<uint32_t>(tier + 2), skillID, behaviorID });
|
||||
}
|
||||
}
|
||||
|
||||
return onKillSkills;
|
||||
}
|
||||
|
||||
std::vector<ItemSetPassiveAbility> ItemSetPassiveAbility::FindAbilities(uint32_t itemSetID, const std::vector<LOT>& items, const std::array<std::vector<uint32_t>, 5>& skillSets, Entity* parent, ItemSet* itemSet) {
|
||||
std::vector<ItemSetPassiveAbility> abilities;
|
||||
|
||||
switch (static_cast<eItemSetPassiveAbilityID>(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<DestroyableComponent>();
|
||||
auto* skillComponent = m_Parent->GetComponent<SkillComponent>();
|
||||
|
||||
@@ -203,118 +194,16 @@ void ItemSetPassiveAbility::OnEnemySmshed(Entity* target) {
|
||||
return;
|
||||
}
|
||||
|
||||
Game::entityManager->SerializeEntity(m_Parent);
|
||||
|
||||
const auto id = static_cast<eItemSetPassiveAbilityID>(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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#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<ItemSetPassiveAbility> FindAbilities(uint32_t itemSetID, Entity* parent, ItemSet* itemSet);
|
||||
static std::vector<ItemSetPassiveAbility> FindAbilities(uint32_t itemSetID, const std::vector<LOT>& items, const std::array<std::vector<uint32_t>, 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<ItemSetStatTrigger> FindStatTriggers(uint32_t itemSetID, const std::vector<LOT>& items);
|
||||
|
||||
/**
|
||||
* The on-kill skills among the set's skills: those whose root behavior is DarkInspiration.
|
||||
*/
|
||||
static std::vector<ItemSetOnKillSkill> FindOnKillSkills(const std::array<std::vector<uint32_t>, 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<ItemSetOnKillSkill> m_OnKillSkills;
|
||||
|
||||
/**
|
||||
* The owner of this ability
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
258
tests/dGameTests/ItemSetPassiveAbilityTests.cpp
Normal file
258
tests/dGameTests/ItemSetPassiveAbilityTests.cpp
Normal file
@@ -0,0 +1,258 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#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<std::vector<uint32_t>, 5> ReadSkillSets(uint32_t setID) {
|
||||
std::array<std::vector<uint32_t>, 5> skillSets;
|
||||
auto query = CDClientDatabase::CreatePreppedStmt(
|
||||
"SELECT skillSetWith2, skillSetWith3, skillSetWith4, skillSetWith5, skillSetWith6 FROM ItemSets WHERE setID = ?;");
|
||||
query.bind(1, static_cast<int>(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<LOT> ReadItems(uint32_t setID) {
|
||||
std::vector<LOT> items;
|
||||
auto query = CDClientDatabase::CreatePreppedStmt("SELECT itemIDs FROM ItemSets WHERE setID = ?;");
|
||||
query.bind(1, static_cast<int>(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<unsigned char>(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<int>(behaviorID));
|
||||
query.bind(2, name.c_str());
|
||||
auto result = query.execQuery();
|
||||
return result.eof() ? -1.0f : static_cast<float>(result.getFloatField(0));
|
||||
}
|
||||
|
||||
uint32_t Template(uint32_t behaviorID) {
|
||||
auto query = CDClientDatabase::CreatePreppedStmt("SELECT templateID FROM BehaviorTemplate WHERE behaviorID = ?;");
|
||||
query.bind(1, static_cast<int>(behaviorID));
|
||||
auto result = query.execQuery();
|
||||
return result.eof() ? 0 : result.getIntField(0);
|
||||
}
|
||||
|
||||
// What a DarkInspiration on-kill behavior ends up doing to the wearer: "<template>=<amount>"
|
||||
std::string OnKillEffect(uint32_t behaviorID) {
|
||||
constexpr uint32_t TargetCaster = 14;
|
||||
constexpr uint32_t Heal = 5;
|
||||
constexpr uint32_t Imagination = 13;
|
||||
constexpr uint32_t RepairArmor = 22;
|
||||
if (Parameter(behaviorID, "faction_list") != 4.0f) return "wrong faction";
|
||||
const auto targetCaster = static_cast<uint32_t>(Parameter(behaviorID, "action"));
|
||||
if (Template(targetCaster) != TargetCaster) return "not target caster";
|
||||
const auto effect = static_cast<uint32_t>(Parameter(targetCaster, "action"));
|
||||
switch (Template(effect)) {
|
||||
case Heal: return "heal=" + std::to_string(static_cast<int>(Parameter(effect, "health")));
|
||||
case Imagination: return "imagination=" + std::to_string(static_cast<int>(Parameter(effect, "imagination")));
|
||||
case RepairArmor: return "armor=" + std::to_string(static_cast<int>(Parameter(effect, "armor")));
|
||||
default: return "template " + std::to_string(Template(effect));
|
||||
}
|
||||
}
|
||||
|
||||
void LoadTables() {
|
||||
CDClientManager::GetTable<CDScriptComponentTable>()->LoadValuesFromDatabase();
|
||||
CDClientManager::GetTable<CDSkillBehaviorTable>()->LoadValuesFromDatabase();
|
||||
CDClientManager::GetTable<CDBehaviorTemplateTable>()->LoadValuesFromDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ItemSetPassiveAbilityTests, StatTriggerScriptLookupIgnoresPathAndCase) {
|
||||
const auto* trigger = ItemSetPassiveAbility::GetStatTriggerForScript("scripts\\equipmenttriggers\\assemblyInventor2.lua");
|
||||
ASSERT_NE(trigger, nullptr);
|
||||
EXPECT_EQ(trigger->trigger, PassiveAbilityTrigger::AssemblyImagination);
|
||||
EXPECT_EQ(trigger->skillID, 581);
|
||||
EXPECT_EQ(trigger->itemsRequired, 4);
|
||||
EXPECT_EQ(trigger->setID, 26);
|
||||
EXPECT_FLOAT_EQ(trigger->cooldown, 11.0f);
|
||||
|
||||
trigger = ItemSetPassiveAbility::GetStatTriggerForScript("scripts/equipmenttriggers/sentinelKnight1.lua");
|
||||
ASSERT_NE(trigger, nullptr);
|
||||
EXPECT_EQ(trigger->trigger, PassiveAbilityTrigger::SentinelArmor);
|
||||
EXPECT_EQ(trigger->skillID, 559);
|
||||
EXPECT_EQ(trigger->setID, 7);
|
||||
|
||||
EXPECT_EQ(ItemSetPassiveAbility::GetStatTriggerForScript("scripts\\equipmenttriggers\\gempack.lua"), nullptr);
|
||||
EXPECT_EQ(ItemSetPassiveAbility::GetStatTriggerForScript(""), nullptr);
|
||||
}
|
||||
|
||||
// A small CDClient: items 90001-90002 carry the Summoner rank 1 script, 90003 the Knight rank 1 script and
|
||||
// 90004 no script; skills 90201 and 90203 are DarkInspiration, 90202 a buff.
|
||||
class ItemSetPassiveAbilityFixtureTest : public GameDependenciesTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
SetUpDependencies();
|
||||
CDClientDatabase::Connect(":memory:");
|
||||
for (const auto* sql : {
|
||||
"CREATE TABLE ComponentsRegistry (id INTEGER, component_type INTEGER, component_id INTEGER);",
|
||||
"CREATE TABLE ScriptComponent (id INTEGER, script_name TEXT, client_script_name TEXT);",
|
||||
"CREATE TABLE SkillBehavior (skillID INTEGER, locStatus INTEGER, behaviorID INTEGER, imaginationcost INTEGER, cooldowngroup INTEGER, cooldown REAL, inNpcEditor INTEGER, skillIcon INTEGER, oomSkillID TEXT, oomBehaviorEffectID INTEGER, castTypeDesc INTEGER, imBonusUI INTEGER, lifeBonusUI INTEGER, armorBonusUI INTEGER, damageUI INTEGER, hideIcon INTEGER, localize INTEGER, gate_version TEXT, cancelType INTEGER);",
|
||||
"CREATE TABLE BehaviorTemplate (behaviorID INTEGER, templateID INTEGER, effectID INTEGER, effectHandle TEXT);",
|
||||
// Two items of the set carry the Summoner rank 1 script (set 28), one carries the Knight rank 1 script (set 7)
|
||||
"INSERT INTO ComponentsRegistry VALUES (90001, 5, 90101), (90002, 5, 90101), (90003, 5, 90102), (90004, 11, 1);",
|
||||
"INSERT INTO ScriptComponent VALUES (90101, 'scripts\\equipmenttriggers\\assemblysummoner1.lua', ''), (90102, 'scripts\\equipmenttriggers\\sentinelKnight1.lua', '');",
|
||||
"INSERT INTO SkillBehavior (skillID, behaviorID, imaginationcost, cooldowngroup, cooldown, cancelType) VALUES (90201, 900001, 0, 0, 0, 0), (90202, 900002, 0, 0, 0, 0), (90203, 900003, 0, 0, 0, 0);",
|
||||
// 24 = DarkInspiration, 30 = Buff
|
||||
"INSERT INTO BehaviorTemplate VALUES (900001, 24, 0, ''), (900002, 30, 0, ''), (900003, 24, 0, '');",
|
||||
}) {
|
||||
CDClientDatabase::ExecuteDML(sql);
|
||||
}
|
||||
LoadTables();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
TearDownDependencies();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ItemSetPassiveAbilityFixtureTest, StatTriggersComeFromTheItemScriptsOfThatSet) {
|
||||
const std::vector<LOT> items = { 90001, 90002, 90003, 90004 };
|
||||
|
||||
const auto summoner = ItemSetPassiveAbility::FindStatTriggers(28, items);
|
||||
ASSERT_EQ(summoner.size(), 1); // two items carry the script, the skill is cast once
|
||||
EXPECT_EQ(summoner[0].trigger, PassiveAbilityTrigger::AssemblyImagination);
|
||||
EXPECT_EQ(summoner[0].skillID, 394);
|
||||
EXPECT_EQ(summoner[0].itemsRequired, 4);
|
||||
EXPECT_FLOAT_EQ(summoner[0].cooldown, 11.0f);
|
||||
|
||||
const auto knight = ItemSetPassiveAbility::FindStatTriggers(7, items);
|
||||
ASSERT_EQ(knight.size(), 1);
|
||||
EXPECT_EQ(knight[0].trigger, PassiveAbilityTrigger::SentinelArmor);
|
||||
EXPECT_EQ(knight[0].skillID, 559);
|
||||
|
||||
// The scripts only fire for the set they were written for
|
||||
EXPECT_TRUE(ItemSetPassiveAbility::FindStatTriggers(9001, items).empty());
|
||||
}
|
||||
|
||||
TEST_F(ItemSetPassiveAbilityFixtureTest, OnKillSkillsAreTheDarkInspirationSetSkills) {
|
||||
std::array<std::vector<uint32_t>, 5> skillSets;
|
||||
skillSets[2] = { 90202, 90201 }; // with4: a buff and an on-kill skill
|
||||
skillSets[3] = { 90201, 90203 }; // with5: the same on-kill skill again and a second one
|
||||
skillSets[4] = { 99999 }; // with6: a skill that is not in SkillBehavior
|
||||
|
||||
const auto onKill = ItemSetPassiveAbility::FindOnKillSkills(skillSets);
|
||||
ASSERT_EQ(onKill.size(), 2);
|
||||
EXPECT_EQ(onKill[0].skillID, 90201);
|
||||
EXPECT_EQ(onKill[0].behaviorID, 900001);
|
||||
EXPECT_EQ(onKill[0].itemsRequired, 4);
|
||||
EXPECT_EQ(onKill[1].skillID, 90203);
|
||||
EXPECT_EQ(onKill[1].behaviorID, 900003);
|
||||
EXPECT_EQ(onKill[1].itemsRequired, 5);
|
||||
|
||||
EXPECT_TRUE(ItemSetPassiveAbility::FindOnKillSkills({}).empty());
|
||||
}
|
||||
|
||||
// Against the live CDClient, when there is one (DLU_CDSERVER_SQLITE, or build/resServer/CDServer.sqlite).
|
||||
class ItemSetPassiveAbilityLiveDataTest : public GameDependenciesTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
std::string path;
|
||||
if (const char* env = std::getenv("DLU_CDSERVER_SQLITE")) path = env;
|
||||
else path = std::string(PROJECT_SOURCE_DIR) + "/build/resServer/CDServer.sqlite";
|
||||
if (!std::filesystem::exists(path)) GTEST_SKIP() << "No CDServer.sqlite at " << path;
|
||||
|
||||
SetUpDependencies();
|
||||
CDClientDatabase::Connect(path);
|
||||
CDClientDatabase::ExecuteDML("PRAGMA query_only = ON;"); // never write to the live CDClient
|
||||
LoadTables();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!IsSkipped()) TearDownDependencies();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ItemSetPassiveAbilityLiveDataTest, StatTriggersMatchTheOldHardcodedSets) {
|
||||
// set ID -> (trigger, skill) the server used to hardcode, all at 4 items
|
||||
const std::map<uint32_t, std::pair<PassiveAbilityTrigger, uint32_t>> expected = {
|
||||
{ 2, { PassiveAbilityTrigger::AssemblyImagination, 394 } }, { 25, { PassiveAbilityTrigger::AssemblyImagination, 394 } }, { 28, { PassiveAbilityTrigger::AssemblyImagination, 394 } },
|
||||
{ 3, { PassiveAbilityTrigger::AssemblyImagination, 581 } }, { 26, { PassiveAbilityTrigger::AssemblyImagination, 581 } }, { 29, { PassiveAbilityTrigger::AssemblyImagination, 581 } },
|
||||
{ 4, { PassiveAbilityTrigger::AssemblyImagination, 582 } }, { 27, { PassiveAbilityTrigger::AssemblyImagination, 582 } }, { 30, { PassiveAbilityTrigger::AssemblyImagination, 582 } },
|
||||
{ 7, { PassiveAbilityTrigger::SentinelArmor, 559 } }, { 8, { PassiveAbilityTrigger::SentinelArmor, 560 } }, { 9, { PassiveAbilityTrigger::SentinelArmor, 561 } },
|
||||
{ 10, { PassiveAbilityTrigger::SentinelArmor, 1101 } }, { 11, { PassiveAbilityTrigger::SentinelArmor, 1102 } }, { 12, { PassiveAbilityTrigger::SentinelArmor, 1103 } },
|
||||
{ 13, { PassiveAbilityTrigger::SentinelArmor, 562 } }, { 14, { PassiveAbilityTrigger::SentinelArmor, 563 } }, { 15, { PassiveAbilityTrigger::SentinelArmor, 564 } },
|
||||
};
|
||||
|
||||
for (uint32_t setID = 1; setID <= 60; ++setID) {
|
||||
const auto triggers = ItemSetPassiveAbility::FindStatTriggers(setID, ReadItems(setID));
|
||||
const auto it = expected.find(setID);
|
||||
if (it == expected.end()) {
|
||||
EXPECT_TRUE(triggers.empty()) << "set " << setID;
|
||||
continue;
|
||||
}
|
||||
ASSERT_EQ(triggers.size(), 1) << "set " << setID;
|
||||
EXPECT_EQ(triggers[0].trigger, it->second.first) << "set " << setID;
|
||||
EXPECT_EQ(triggers[0].skillID, it->second.second) << "set " << setID;
|
||||
EXPECT_EQ(triggers[0].itemsRequired, 4) << "set " << setID;
|
||||
// The faction (Assembly) scripts have an 11 second cooldown, the Sentinel scripts none
|
||||
EXPECT_FLOAT_EQ(triggers[0].cooldown, it->second.first == PassiveAbilityTrigger::AssemblyImagination ? 11.0f : 0.0f) << "set " << setID;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ItemSetPassiveAbilityLiveDataTest, OnKillSkillsMatchTheOldHardcodedSets) {
|
||||
// set ID -> (items, effect) the server used to hardcode on enemy smash
|
||||
const std::map<uint32_t, std::pair<uint32_t, std::string>> expected = {
|
||||
{ 42, { 5, "heal=3" } }, // Bat Lord
|
||||
{ 8, { 5, "armor=1" } }, { 9, { 5, "armor=1" } }, // Knight
|
||||
{ 11, { 5, "armor=1" } }, { 12, { 5, "armor=1" } }, // Space Ranger
|
||||
{ 14, { 5, "armor=1" } }, { 15, { 5, "armor=1" } }, // Samurai
|
||||
{ 16, { 4, "imagination=1" } }, { 17, { 4, "imagination=2" } }, { 18, { 4, "imagination=3" } }, // Sorcerer
|
||||
{ 19, { 4, "imagination=1" } }, { 20, { 4, "imagination=2" } }, { 21, { 4, "imagination=3" } }, // Space Marauder
|
||||
{ 22, { 4, "imagination=1" } }, { 23, { 4, "imagination=2" } }, { 24, { 4, "imagination=3" } }, // Shinobi
|
||||
};
|
||||
|
||||
for (const auto& [setID, expectation] : expected) {
|
||||
const auto onKill = ItemSetPassiveAbility::FindOnKillSkills(ReadSkillSets(setID));
|
||||
ASSERT_FALSE(onKill.empty()) << "set " << setID;
|
||||
// The first on-kill skill is the one the server hardcoded; the data can add more
|
||||
// (e.g. the rank 2 and 3 Paradox team imagination at 4 and 5 items)
|
||||
EXPECT_EQ(onKill[0].itemsRequired, expectation.first) << "set " << setID;
|
||||
EXPECT_EQ(OnKillEffect(onKill[0].behaviorID), expectation.second) << "set " << setID;
|
||||
}
|
||||
|
||||
// The rank 1 Sentinel sets were hardcoded to repair armor at 5 items, but only have 4 items
|
||||
for (const uint32_t setID : { 7, 10, 13 }) {
|
||||
EXPECT_LT(ReadItems(setID).size(), 5) << "set " << setID;
|
||||
EXPECT_TRUE(ItemSetPassiveAbility::FindOnKillSkills(ReadSkillSets(setID)).empty()) << "set " << setID;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user