From 5973430720ad6203bae1ffa3dd6b6c1e40a858d2 Mon Sep 17 00:00:00 2001 From: wincent Date: Sat, 15 Jul 2023 10:54:41 +0200 Subject: [PATCH] Grim LU Wincent's attempt at making LU into something it isn't supposed to be, an ARPG. --- CMakeLists.txt | 11 +- dCommon/dEnums/eSkillBar.h | 16 + dDatabase/Tables/CDLookupTable.cpp | 61 +++ dDatabase/Tables/CDLookupTable.h | 23 ++ dDatabase/Tables/CMakeLists.txt | 1 + dGame/CMakeLists.txt | 11 + dGame/Entity.cpp | 22 ++ dGame/Player.cpp | 2 +- dGame/dBehaviors/BasicAttackBehavior.cpp | 37 +- dGame/dBehaviors/Behavior.cpp | 2 +- dGame/dBehaviors/BehaviorContext.h | 2 + dGame/dBehaviors/BuffBehavior.cpp | 14 + dGame/dBehaviors/DamageAbsorptionBehavior.cpp | 4 +- dGame/dBehaviors/HealBehavior.cpp | 14 +- dGame/dBehaviors/RepairBehavior.cpp | 14 +- dGame/dBehaviors/StunBehavior.cpp | 2 +- dGame/dComponents/BaseCombatAIComponent.cpp | 8 +- dGame/dComponents/BuffComponent.cpp | 12 +- dGame/dComponents/DestroyableComponent.cpp | 355 ++++++++++++++++++ dGame/dComponents/DestroyableComponent.h | 62 +++ dGame/dComponents/InventoryComponent.cpp | 304 +++++++++++++-- dGame/dComponents/InventoryComponent.h | 41 ++ dGame/dComponents/SkillComponent.cpp | 5 +- dGame/dComponents/SkillComponent.h | 6 +- dGame/dComponents/VendorComponent.cpp | 3 +- dGame/dGameMessages/GameMessageHandler.cpp | 43 +++ dGame/dGameMessages/GameMessages.cpp | 51 ++- dGame/dGrim/CMakeLists.txt | 6 + dGame/dGrim/DamageProfile.cpp | 56 +++ dGame/dGrim/DamageProfile.h | 33 ++ dGame/dGrim/EntityProfile.cpp | 38 ++ dGame/dGrim/EntityProfile.h | 49 +++ dGame/dGrim/ItemModifierTemplate.cpp | 315 ++++++++++++++++ dGame/dGrim/ItemModifierTemplate.h | 60 +++ dGame/dGrim/ResistanceProfile.cpp | 67 ++++ dGame/dGrim/ResistanceProfile.h | 33 ++ dGame/dGrim/SpawnPatterns.cpp | 58 +++ dGame/dGrim/SpawnPatterns.h | 33 ++ dGame/dGrim/StatModifier.h | 18 + dGame/dGrim/StatProperty.cpp | 132 +++++++ dGame/dGrim/StatProperty.h | 26 ++ dGame/dGrim/StatRarity.h | 19 + dGame/dGrim/StatTypes.h | 27 ++ dGame/dInventory/Item.cpp | 56 +++ dGame/dInventory/Item.h | 24 ++ dGame/dUtilities/SlashCommandHandler.cpp | 225 +++++++++++ .../Enemy/AG/BossSpiderQueenEnemyServer.cpp | 110 +++++- .../Enemy/AG/BossSpiderQueenEnemyServer.h | 4 +- .../Property/AG_Small/EnemySpiderSpawner.cpp | 6 +- dWorldServer/CMakeLists.txt | 2 +- dWorldServer/WorldServer.cpp | 19 + dZoneManager/CMakeLists.txt | 4 +- dZoneManager/Spawner.cpp | 155 ++++++++ dZoneManager/Spawner.h | 9 + 54 files changed, 2634 insertions(+), 76 deletions(-) create mode 100644 dCommon/dEnums/eSkillBar.h create mode 100644 dDatabase/Tables/CDLookupTable.cpp create mode 100644 dDatabase/Tables/CDLookupTable.h create mode 100644 dGame/dGrim/CMakeLists.txt create mode 100644 dGame/dGrim/DamageProfile.cpp create mode 100644 dGame/dGrim/DamageProfile.h create mode 100644 dGame/dGrim/EntityProfile.cpp create mode 100644 dGame/dGrim/EntityProfile.h create mode 100644 dGame/dGrim/ItemModifierTemplate.cpp create mode 100644 dGame/dGrim/ItemModifierTemplate.h create mode 100644 dGame/dGrim/ResistanceProfile.cpp create mode 100644 dGame/dGrim/ResistanceProfile.h create mode 100644 dGame/dGrim/SpawnPatterns.cpp create mode 100644 dGame/dGrim/SpawnPatterns.h create mode 100644 dGame/dGrim/StatModifier.h create mode 100644 dGame/dGrim/StatProperty.cpp create mode 100644 dGame/dGrim/StatProperty.h create mode 100644 dGame/dGrim/StatRarity.h create mode 100644 dGame/dGrim/StatTypes.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 41d4219f..4ff38c34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,6 +162,7 @@ set(INCLUDED_DIRECTORIES "dGame/dInventory" "dGame/dMission" "dGame/dEntity" + "dGame/dGrim" "dGame/dPropertyBehaviors" "dGame/dPropertyBehaviors/ControlBehaviorMessages" "dGame/dUtilities" @@ -312,13 +313,13 @@ file( ) # Add our library subdirectories for creation of the library object +add_subdirectory(dZoneManager) add_subdirectory(dCommon) add_subdirectory(dDatabase) add_subdirectory(dChatFilter) add_subdirectory(dNet) add_subdirectory(dScripts) # Add for dGame to use add_subdirectory(dGame) -add_subdirectory(dZoneManager) add_subdirectory(dNavigation) add_subdirectory(dPhysics) @@ -346,10 +347,10 @@ target_precompile_headers( ${HEADERS_DGAME} ) -target_precompile_headers( - dZoneManager PRIVATE - ${HEADERS_DZONEMANAGER} -) +#target_precompile_headers( +# dZoneManager PRIVATE +# ${HEADERS_DZONEMANAGER} +#) # Need to specify to use the CXX compiler language here or else we get errors including . target_precompile_headers( diff --git a/dCommon/dEnums/eSkillBar.h b/dCommon/dEnums/eSkillBar.h new file mode 100644 index 00000000..b99663a1 --- /dev/null +++ b/dCommon/dEnums/eSkillBar.h @@ -0,0 +1,16 @@ +#pragma once + +#ifndef __ESKILLBAR_H__ +#define __ESKILLBAR_H__ + +#include + +enum class eSkillBar : int32_t { + Primary, + Gear, + ClassPrimary, + ClassSecondary, + Consumable +}; + +#endif // __ESKILLBAR_H__ \ No newline at end of file diff --git a/dDatabase/Tables/CDLookupTable.cpp b/dDatabase/Tables/CDLookupTable.cpp new file mode 100644 index 00000000..de2aa965 --- /dev/null +++ b/dDatabase/Tables/CDLookupTable.cpp @@ -0,0 +1,61 @@ +#include "CDLookupTable.h" + +CDLookupTable::CDLookupTable(void) { + + // First, get the size of the table + uint32_t size = 0; + auto tableSize = CDClientDatabase::ExecuteQuery("SELECT COUNT(*) FROM lookupTable"); + while (!tableSize.eof()) { + size = tableSize.getIntField(0, 0); + + tableSize.nextRow(); + } + + tableSize.finalize(); + + // Reserve the size + this->entries.reserve(size); + + // Now get the data + auto tableData = CDClientDatabase::ExecuteQuery("SELECT * FROM lookupTable"); + while (!tableData.eof()) { + std::string key = tableData.getStringField("id", ""); + int32_t value = tableData.getIntField("value", -1); + + this->entries.emplace(key, value); + this->reverseEntries.emplace(value, key); + + tableData.nextRow(); + } + + tableData.finalize(); +} + +int32_t CDLookupTable::Lookup(const std::string& key) const { + // If the key starts with 'lego-universe:', parse what comes + // after the ':' as an integer and return that. + if (key.find("lego-universe:") == 0) { + return std::stoi(key.substr(14)); + } + + auto it = this->entries.find(key); + if (it != this->entries.end()) { + return it->second; + } + + return -1; +} + +std::vector CDLookupTable::ReverseLookup(int32_t value) const { + auto range = this->reverseEntries.equal_range(value); + std::vector keys; + for (auto it = range.first; it != range.second; ++it) { + keys.push_back(it->second); + } + + return keys; +} + +int32_t rose::id(const std::string& key) { + return CDLookupTable::Instance().Lookup(key); +} diff --git a/dDatabase/Tables/CDLookupTable.h b/dDatabase/Tables/CDLookupTable.h new file mode 100644 index 00000000..cc462aba --- /dev/null +++ b/dDatabase/Tables/CDLookupTable.h @@ -0,0 +1,23 @@ +#pragma once + +// Custom Classes +#include "CDTable.h" + +class CDLookupTable : public CDTable { +private: + std::unordered_map entries; + std::unordered_multimap reverseEntries; + +public: + CDLookupTable(); + + // Lookup + int32_t Lookup(const std::string& key) const; + + // Reverse Lookup + std::vector ReverseLookup(int32_t value) const; +}; + +namespace rose { + int32_t id(const std::string& key); +} diff --git a/dDatabase/Tables/CMakeLists.txt b/dDatabase/Tables/CMakeLists.txt index b6a02b02..e6078429 100644 --- a/dDatabase/Tables/CMakeLists.txt +++ b/dDatabase/Tables/CMakeLists.txt @@ -35,4 +35,5 @@ set(DDATABASE_TABLES_SOURCES "CDActivitiesTable.cpp" "CDScriptComponentTable.cpp" "CDSkillBehaviorTable.cpp" "CDVendorComponentTable.cpp" + "CDLookupTable.cpp" "CDZoneTableTable.cpp" PARENT_SCOPE) diff --git a/dGame/CMakeLists.txt b/dGame/CMakeLists.txt index 80f16042..cedda8a3 100644 --- a/dGame/CMakeLists.txt +++ b/dGame/CMakeLists.txt @@ -56,10 +56,21 @@ foreach(file ${DGAME_DUTILITIES_SOURCES}) set(DGAME_SOURCES ${DGAME_SOURCES} "dUtilities/${file}") endforeach() +add_subdirectory(dGrim) + +foreach(file ${DGAME_DGRIM_SOURCES}) + set(DGAME_SOURCES ${DGAME_SOURCES} "dGrim/${file}") +endforeach() + foreach(file ${DSCRIPTS_SOURCES}) set(DGAME_SOURCES ${DGAME_SOURCES} "${PROJECT_SOURCE_DIR}/dScripts/${file}") endforeach() +foreach(file ${DZONEMANAGER_SOURCES}) + message("Adding ${file}") + set(DGAME_SOURCES ${DGAME_SOURCES} "${PROJECT_SOURCE_DIR}/dZoneManager/${file}") +endforeach() + add_library(dGame STATIC ${DGAME_SOURCES}) target_link_libraries(dGame dDatabase Recast Detour) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index bac07713..014c0030 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -25,6 +25,7 @@ #include "eMissionTaskType.h" #include "eTriggerEventType.h" #include "eObjectBits.h" +#include "EntityProfile.h" //Component includes: #include "Component.h" @@ -361,6 +362,18 @@ void Entity::Initialize() { destCompData[0].imagination = 60; } + auto& info = destCompData[0]; + + auto* entityProfile = EntityProfile::FindEntityProfile(m_TemplateID); + + if (entityProfile != nullptr) { + if (entityProfile->GetLevel() > 0) info.level = entityProfile->GetLevel(); + if (entityProfile->GetHealth() > 0) info.life = entityProfile->GetHealth(); + if (entityProfile->GetArmor() > 0) info.armor = entityProfile->GetArmor(); + } + + comp->SetInfo(destCompData[0]); + comp->SetHealth(destCompData[0].life); comp->SetImagination(destCompData[0].imagination); comp->SetArmor(destCompData[0].armor); @@ -373,6 +386,15 @@ void Entity::Initialize() { comp->SetLootMatrixID(destCompData[0].LootMatrixIndex); + comp->ComputeBaseStats(true); + + if (true) + { + comp->SetHealth(comp->GetMaxHealth()); + comp->SetImagination(comp->GetMaxImagination()); + comp->SetArmor(comp->GetMaxArmor()); + } + // Now get currency information uint32_t npcMinLevel = destCompData[0].level; uint32_t currencyIndex = destCompData[0].CurrencyIndex; diff --git a/dGame/Player.cpp b/dGame/Player.cpp index 2e194e6a..2ba7194c 100644 --- a/dGame/Player.cpp +++ b/dGame/Player.cpp @@ -110,7 +110,7 @@ void Player::SendToZone(LWOMAPID zoneId, LWOCLONEID cloneId) { EntityManager::Instance()->DestructEntity(entity); return; - }); + }); } void Player::AddLimboConstruction(LWOOBJID objectId) { diff --git a/dGame/dBehaviors/BasicAttackBehavior.cpp b/dGame/dBehaviors/BasicAttackBehavior.cpp index f8693795..19163c4e 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.cpp +++ b/dGame/dBehaviors/BasicAttackBehavior.cpp @@ -6,6 +6,7 @@ #include "DestroyableComponent.h" #include "BehaviorContext.h" #include "eBasicAttackSuccessTypes.h" +#include "DamageProfile.h" void BasicAttackBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) { if (context->unmanaged) { @@ -101,6 +102,17 @@ void BasicAttackBehavior::DoHandleBehavior(BehaviorContext* context, RakNet::Bit totalDamageDealt = this->m_MinDamage; } + auto* source = EntityManager::Instance()->GetEntity(context->originator); + + auto* damageProfile = DamageProfile::FindDamageProfile(context->skillID); + + auto damageMap = destroyableComponent->ComputeDamage(totalDamageDealt, source, damageProfile); + + // Apply a standard diviation of (+/-)20% + for (auto& damage : damageMap) { + damage.second = static_cast(damage.second * (1.0f + (static_cast(rand() % 40) / 100.0f) - 0.2f)); + } + bool died{}; if (!bitStream->Read(died)) { Game::logger->Log("BasicAttackBehavior", "Unable to read died"); @@ -109,7 +121,8 @@ void BasicAttackBehavior::DoHandleBehavior(BehaviorContext* context, RakNet::Bit auto previousArmor = destroyableComponent->GetArmor(); auto previousHealth = destroyableComponent->GetHealth(); PlayFx(u"onhit", targetEntity->GetObjectID()); - destroyableComponent->Damage(totalDamageDealt, context->originator, context->skillID); + destroyableComponent->Damage(damageMap, context->originator, context->skillID); + //destroyableComponent->Damage(totalDamageDealt, context->originator, context->skillID); } uint8_t successState{}; @@ -191,10 +204,28 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet const uint32_t previousHealth = destroyableComponent->GetHealth(); const uint32_t previousArmor = destroyableComponent->GetArmor(); - const auto damage = this->m_MinDamage; + auto damage = this->m_MinDamage; + + auto* source = EntityManager::Instance()->GetEntity(context->originator); + + auto* damageProfile = DamageProfile::FindDamageProfile(context->skillID); + + if (damageProfile != nullptr) { + Game::logger->Log("BasicAttackBehavior", "Found damage profile for skill %i, originator %i", context->skillID, source->GetLOT()); + } + else { + Game::logger->Log("BasicAttackBehavior", "No damage profile found for skill %i, originator %i", context->skillID, source->GetLOT()); + } + + auto damageMap = destroyableComponent->ComputeDamage(damage, source, damageProfile); + + // Apply a standard diviation of (+/-)20% + for (auto& damage : damageMap) { + damage.second = static_cast(damage.second * (1.0f + (static_cast(rand() % 40) / 100.0f) - 0.2f)); + } PlayFx(u"onhit", targetEntity->GetObjectID(), 1); - destroyableComponent->Damage(damage, context->originator, context->skillID, false); + destroyableComponent->Damage(damageMap, context->originator, context->skillID, false); context->ScheduleUpdate(branch.target); const uint32_t armorDamageDealt = previousArmor - destroyableComponent->GetArmor(); diff --git a/dGame/dBehaviors/Behavior.cpp b/dGame/dBehaviors/Behavior.cpp index 8b34507a..57130415 100644 --- a/dGame/dBehaviors/Behavior.cpp +++ b/dGame/dBehaviors/Behavior.cpp @@ -361,7 +361,7 @@ void Behavior::PlayFx(std::u16string type, const LWOOBJID target, const LWOOBJID "SELECT effectName FROM BehaviorEffect WHERE effectType = ? AND effectID = ?;"); auto idQuery = CDClientDatabase::CreatePreppedStmt( - "SELECT effectName, effectType FROM BehaviorEffect WHERE effectID = ?;"); + "SELECT effectName, effectType FROM BehaviorEffect WHERE effectID = ? AND effectType IS NOT NULL;"); if (!type.empty()) { typeQuery.bind(1, typeString.c_str()); diff --git a/dGame/dBehaviors/BehaviorContext.h b/dGame/dBehaviors/BehaviorContext.h index 117f328d..95c6c983 100644 --- a/dGame/dBehaviors/BehaviorContext.h +++ b/dGame/dBehaviors/BehaviorContext.h @@ -78,6 +78,8 @@ struct BehaviorContext LWOOBJID caster = LWOOBJID_EMPTY; + LWOOBJID itemID = LWOOBJID_EMPTY; + uint32_t GetUniqueSkillId() const; void UpdatePlayerSyncs(float deltaTime); diff --git a/dGame/dBehaviors/BuffBehavior.cpp b/dGame/dBehaviors/BuffBehavior.cpp index a39fd165..c4b0de12 100644 --- a/dGame/dBehaviors/BuffBehavior.cpp +++ b/dGame/dBehaviors/BuffBehavior.cpp @@ -26,9 +26,16 @@ void BuffBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bitStream return; } + component->AddStat(StatProperty(eStatTypes::Health, eStatModifier::Absolute, this->m_health * BASE_MULTIPLIER)); + component->AddStat(StatProperty(eStatTypes::Armor, eStatModifier::Absolute, this->m_armor * BASE_MULTIPLIER)); + //component->AddStat(StatProperty(eStatTypes::Imagination, eStatModifier::Absolute, this->m_imagination * BASE_MULTIPLIER)); + + /* component->SetMaxHealth(component->GetMaxHealth() + this->m_health); component->SetMaxArmor(component->GetMaxArmor() + this->m_armor); component->SetMaxImagination(component->GetMaxImagination() + this->m_imagination); + */ + component->SetMaxImagination(component->GetMaxImagination() + this->m_imagination); EntityManager::Instance()->SerializeEntity(entity); @@ -60,9 +67,16 @@ void BuffBehavior::UnCast(BehaviorContext* context, BehaviorBranchContext branch return; } + component->RemoveStat(StatProperty(eStatTypes::Health, eStatModifier::Absolute, this->m_health * BASE_MULTIPLIER)); + component->RemoveStat(StatProperty(eStatTypes::Armor, eStatModifier::Absolute, this->m_armor * BASE_MULTIPLIER)); + //component->RemoveStat(StatProperty(eStatTypes::Imagination, eStatModifier::Absolute, this->m_imagination * BASE_MULTIPLIER)); + + /* component->SetMaxHealth(component->GetMaxHealth() - this->m_health); component->SetMaxArmor(component->GetMaxArmor() - this->m_armor); component->SetMaxImagination(component->GetMaxImagination() - this->m_imagination); + */ + component->SetMaxImagination(component->GetMaxImagination() - this->m_imagination); EntityManager::Instance()->SerializeEntity(entity); } diff --git a/dGame/dBehaviors/DamageAbsorptionBehavior.cpp b/dGame/dBehaviors/DamageAbsorptionBehavior.cpp index 48dbf705..a96ef766 100644 --- a/dGame/dBehaviors/DamageAbsorptionBehavior.cpp +++ b/dGame/dBehaviors/DamageAbsorptionBehavior.cpp @@ -22,7 +22,7 @@ void DamageAbsorptionBehavior::Handle(BehaviorContext* context, RakNet::BitStrea return; } - destroyable->SetDamageToAbsorb(static_cast(destroyable->GetDamageToAbsorb()) + this->m_absorbAmount); + destroyable->SetDamageToAbsorb(static_cast(destroyable->GetDamageToAbsorb()) + this->m_absorbAmount * 50); destroyable->SetIsShielded(true); @@ -50,7 +50,7 @@ void DamageAbsorptionBehavior::Timer(BehaviorContext* context, BehaviorBranchCon const auto present = static_cast(destroyable->GetDamageToAbsorb()); - const auto toRemove = std::min(present, this->m_absorbAmount); + const auto toRemove = std::min(present, this->m_absorbAmount * 50); destroyable->SetDamageToAbsorb(present - toRemove); } diff --git a/dGame/dBehaviors/HealBehavior.cpp b/dGame/dBehaviors/HealBehavior.cpp index 66fe2c79..86e8acdf 100644 --- a/dGame/dBehaviors/HealBehavior.cpp +++ b/dGame/dBehaviors/HealBehavior.cpp @@ -5,6 +5,7 @@ #include "EntityManager.h" #include "DestroyableComponent.h" #include "eReplicaComponentType.h" +#include "LevelProgressionComponent.h" void HealBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bit_stream, const BehaviorBranchContext branch) { @@ -24,7 +25,18 @@ void HealBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bit_strea return; } - destroyable->Heal(this->m_health); + int32_t toApply = this->m_health * 5; + + auto* levelProgressComponent = entity->GetComponent(); + + if (levelProgressComponent != nullptr) { + toApply *= levelProgressComponent->GetLevel(); + } + + // Apply a standard deviations of 20% + toApply = static_cast(toApply * (1.0f + (static_cast(rand() % 40) / 100.0f) - 0.2f)); + + destroyable->Heal(toApply); } diff --git a/dGame/dBehaviors/RepairBehavior.cpp b/dGame/dBehaviors/RepairBehavior.cpp index ce2e5fd2..d8799fff 100644 --- a/dGame/dBehaviors/RepairBehavior.cpp +++ b/dGame/dBehaviors/RepairBehavior.cpp @@ -6,6 +6,7 @@ #include "dLogger.h" #include "Game.h" #include "eReplicaComponentType.h" +#include "LevelProgressionComponent.h" void RepairBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bit_stream, const BehaviorBranchContext branch) { auto* entity = EntityManager::Instance()->GetEntity(branch.target); @@ -24,7 +25,18 @@ void RepairBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bit_str return; } - destroyable->Repair(this->m_armor); + int32_t toApply = this->m_armor * 5; + + auto* levelProgressComponent = entity->GetComponent(); + + if (levelProgressComponent != nullptr) { + toApply *= levelProgressComponent->GetLevel(); + } + + // Apply a standard deviations of 20% + toApply = static_cast(toApply * (1.0f + (static_cast(rand() % 40) / 100.0f) - 0.2f)); + + destroyable->Repair(toApply); } void RepairBehavior::Calculate(BehaviorContext* context, RakNet::BitStream* bit_stream, const BehaviorBranchContext branch) { diff --git a/dGame/dBehaviors/StunBehavior.cpp b/dGame/dBehaviors/StunBehavior.cpp index 4e34d3a2..e9f730f1 100644 --- a/dGame/dBehaviors/StunBehavior.cpp +++ b/dGame/dBehaviors/StunBehavior.cpp @@ -39,7 +39,7 @@ void StunBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bitStream return; } - combatAiComponent->Stun(branch.duration); + combatAiComponent->Stun(branch.duration / 2.0f); } void StunBehavior::Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, const BehaviorBranchContext branch) { diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index cccaad23..1711fc53 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -146,6 +146,12 @@ void BaseCombatAIComponent::Update(const float deltaTime) { //First, we need to process physics: if (!m_dpEntity) return; + if (m_State == AiState::spawn) { + auto* destroyable = m_Parent->GetComponent(); + + destroyable->ComputeBaseStats(true); + } + m_dpEntity->SetPosition(m_Parent->GetPosition()); //make sure our position is synced with our dpEntity m_dpEntityEnemy->SetPosition(m_Parent->GetPosition()); @@ -234,7 +240,7 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { bool hasSkillToCast = false; for (auto& entry : m_SkillEntries) { if (entry.cooldown > 0.0f) { - entry.cooldown -= deltaTime; + entry.cooldown -= deltaTime * 2; } else { hasSkillToCast = true; } diff --git a/dGame/dComponents/BuffComponent.cpp b/dGame/dComponents/BuffComponent.cpp index 68b5182c..22447d01 100644 --- a/dGame/dComponents/BuffComponent.cpp +++ b/dGame/dComponents/BuffComponent.cpp @@ -153,7 +153,7 @@ void BuffComponent::ApplyBuffEffect(int32_t id) { if (destroyable == nullptr) return; - destroyable->SetMaxHealth(destroyable->GetMaxHealth() + maxHealth); + destroyable->SetMaxHealth(destroyable->GetMaxHealth() + maxHealth * BASE_MULTIPLIER); } else if (parameter.name == "max_armor") { const auto maxArmor = parameter.value; @@ -161,7 +161,7 @@ void BuffComponent::ApplyBuffEffect(int32_t id) { if (destroyable == nullptr) return; - destroyable->SetMaxArmor(destroyable->GetMaxArmor() + maxArmor); + destroyable->SetMaxArmor(destroyable->GetMaxArmor() + maxArmor * BASE_MULTIPLIER); } else if (parameter.name == "max_imagination") { const auto maxImagination = parameter.value; @@ -169,7 +169,7 @@ void BuffComponent::ApplyBuffEffect(int32_t id) { if (destroyable == nullptr) return; - destroyable->SetMaxImagination(destroyable->GetMaxImagination() + maxImagination); + destroyable->SetMaxImagination(destroyable->GetMaxImagination() + maxImagination * BASE_MULTIPLIER); } else if (parameter.name == "speed") { auto* controllablePhysicsComponent = this->GetParent()->GetComponent(); if (!controllablePhysicsComponent) return; @@ -189,7 +189,7 @@ void BuffComponent::RemoveBuffEffect(int32_t id) { if (destroyable == nullptr) return; - destroyable->SetMaxHealth(destroyable->GetMaxHealth() - maxHealth); + destroyable->SetMaxHealth(destroyable->GetMaxHealth() - maxHealth * BASE_MULTIPLIER); } else if (parameter.name == "max_armor") { const auto maxArmor = parameter.value; @@ -197,7 +197,7 @@ void BuffComponent::RemoveBuffEffect(int32_t id) { if (destroyable == nullptr) return; - destroyable->SetMaxArmor(destroyable->GetMaxArmor() - maxArmor); + destroyable->SetMaxArmor(destroyable->GetMaxArmor() - maxArmor * BASE_MULTIPLIER); } else if (parameter.name == "max_imagination") { const auto maxImagination = parameter.value; @@ -205,7 +205,7 @@ void BuffComponent::RemoveBuffEffect(int32_t id) { if (destroyable == nullptr) return; - destroyable->SetMaxImagination(destroyable->GetMaxImagination() - maxImagination); + destroyable->SetMaxImagination(destroyable->GetMaxImagination() - maxImagination * BASE_MULTIPLIER); } else if (parameter.name == "speed") { auto* controllablePhysicsComponent = this->GetParent()->GetComponent(); if (!controllablePhysicsComponent) return; diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index db8a2013..8bfcf5c0 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -35,6 +35,9 @@ #include "eMissionTaskType.h" #include "eStateChangeType.h" #include "eGameActivity.h" +#include "LevelProgressionComponent.h" +#include "ResistanceProfile.h" +#include "DamageProfile.h" #include "CDComponentsRegistryTable.h" @@ -62,6 +65,7 @@ DestroyableComponent::DestroyableComponent(Entity* parent) : Component(parent) { m_MinCoins = 0; m_MaxCoins = 0; m_DamageReduction = 0; + m_DirtyStats = true; m_ImmuneToBasicAttackCount = 0; m_ImmuneToDamageOverTimeCount = 0; @@ -72,6 +76,8 @@ DestroyableComponent::DestroyableComponent(Entity* parent) : Component(parent) { m_ImmuneToImaginationLossCount = 0; m_ImmuneToQuickbuildInterruptCount = 0; m_ImmuneToPullToPointCount = 0; + + m_ResistanceProfile = ResistanceProfile::FindResistanceProfile(m_Parent->GetLOT()); } DestroyableComponent::~DestroyableComponent() { @@ -95,6 +101,8 @@ void DestroyableComponent::Reinitialize(LOT templateID) { if (componentID > 0) { std::vector destCompData = destCompTable->Query([=](CDDestructibleComponent entry) { return (entry.id == componentID); }); + m_Info = destCompData[0]; + if (destCompData.size() > 0) { SetHealth(destCompData[0].life); SetImagination(destCompData[0].imagination); @@ -107,6 +115,11 @@ void DestroyableComponent::Reinitialize(LOT templateID) { SetIsSmashable(destCompData[0].isSmashable); } } else { + m_Info = {}; + m_Info.life = 1; + m_Info.imagination = 0; + m_Info.armor = 0; + SetHealth(1); SetImagination(0); SetArmor(0); @@ -117,6 +130,15 @@ void DestroyableComponent::Reinitialize(LOT templateID) { SetIsSmashable(true); } + + ComputeBaseStats(); + + if (!m_Parent->IsPlayer()) + { + SetHealth(GetMaxHealth()); + SetImagination(GetMaxImagination()); + SetArmor(GetMaxArmor()); + } } void DestroyableComponent::Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, uint32_t& flags) { @@ -810,6 +832,18 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType } } } + + std::vector scripts = EntityManager::Instance()->GetEntitiesByComponent(eReplicaComponentType::SCRIPT); + for (Entity* scriptEntity : scripts) { + // Prevent double triggering + if (scriptEntity->GetObjectID() == zoneControl->GetObjectID()) continue; + if (std::find(scriptedActs.begin(), scriptedActs.end(), scriptEntity) != scriptedActs.end()) continue; + if (scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds + for (CppScripts::Script* script : CppScripts::GetEntityScripts(scriptEntity)) { + script->OnPlayerDied(scriptEntity, m_Parent); + } + } + } } m_Parent->Kill(owner); @@ -949,6 +983,8 @@ void DestroyableComponent::FixStats() { item->Equip(); } + destroyableComponent->ComputeBaseStats(); + // Fetch correct max stats after everything is done maxHealth = destroyableComponent->GetMaxHealth(); maxArmor = destroyableComponent->GetMaxArmor(); @@ -968,6 +1004,325 @@ void DestroyableComponent::FixStats() { EntityManager::Instance()->SerializeEntity(entity); } +void DestroyableComponent::AddStat(const StatProperty& stat) { + m_DirtyStats = true; + + const auto& typeIter = m_Stats.find(stat.type); + + if (typeIter == m_Stats.end()) { + m_Stats.emplace(stat.type, std::map()); + } + + auto& typeMap = m_Stats.at(stat.type); + + const auto& modifierIter = typeMap.find(stat.modifier); + + if (modifierIter == typeMap.end()) { + typeMap.emplace(stat.modifier, stat.value); + return; + } + + typeMap.at(stat.modifier) += stat.value; +} + +void DestroyableComponent::RemoveStat(const StatProperty& stat) { + m_DirtyStats = true; + + const auto& typeIter = m_Stats.find(stat.type); + + if (typeIter == m_Stats.end()) { + return; + } + + auto& typeMap = m_Stats.at(stat.type); + + const auto& modifierIter = typeMap.find(stat.modifier); + + if (modifierIter == typeMap.end()) { + return; + } + + typeMap.at(stat.modifier) -= stat.value; + + if (typeMap.at(stat.modifier) <= 0) { + typeMap.erase(stat.modifier); + } + + if (typeMap.empty()) { + m_Stats.erase(stat.type); + } +} + +float DestroyableComponent::GetStat(eStatTypes statType, eStatModifier statModifier) const { + const auto& typeIter = m_Stats.find(statType); + + float baseResistance = 0; + + if (statModifier == eStatModifier::DamageResistance) { + if (m_ResistanceProfile != nullptr) { + baseResistance = m_ResistanceProfile->GetResistanceProfile(statType); + + Game::logger->Log("DestroyableComponent", "Base resistance: %f for %i, type %i", baseResistance, m_Parent->GetLOT(), statType); + } + else { + Game::logger->Log("DestroyableComponent", "No resistance profile for %i", m_Parent->GetLOT()); + } + } + + if (typeIter == m_Stats.end()) { + return baseResistance; + } + + const auto& typeMap = m_Stats.at(statType); + + const auto& modifierIter = typeMap.find(statModifier); + + if (modifierIter == typeMap.end()) { + return baseResistance; + } + + return typeMap.at(statModifier); +} + +void DestroyableComponent::ComputeBaseStats(bool refill) { + if (!m_Parent->IsPlayer()) + { + auto* combatAI = m_Parent->GetComponent(); + + if (combatAI == nullptr) + { + return; + } + } + + // Store the current health, armor and imagination + const auto currentHealth = GetHealth(); + const auto currentArmor = GetArmor(); + + float maxHealth = 0.0f; + float maxArmor = 0.0f; + + int32_t level = (static_cast(m_Info.level) <= 1) ? 1 : m_Info.level; + + if (m_Parent->IsPlayer()) + { + level = m_Parent->GetComponent()->GetLevel(); + m_Info.life = 4; + } + + maxHealth += level * m_Info.life; + + maxHealth *= 10; + + maxHealth += GetStat(eStatTypes::Health, eStatModifier::Absolute); + maxHealth *= 1.0f + GetStat(eStatTypes::Health, eStatModifier::Percent); + + maxArmor = level * m_Info.armor; + + maxArmor *= 10; + + maxArmor += GetStat(eStatTypes::Armor, eStatModifier::Absolute); + maxArmor *= 1.0f + GetStat(eStatTypes::Armor, eStatModifier::Percent); + + // Set the base stats + SetMaxHealth(maxHealth); + SetMaxArmor(maxArmor); + + // If any of the current stats are more than their max, set them to the max + if (currentHealth > maxHealth || refill) SetHealth(maxHealth); + else SetHealth(currentHealth); + + if (currentArmor > maxArmor || refill) SetArmor(maxArmor); + else SetArmor(currentArmor); + + EntityManager::Instance()->SerializeEntity(m_Parent); +} + +std::map DestroyableComponent::ComputeDamage(uint32_t baseDamage, Entity* source, DamageProfile* damageProfile) { + const bool debug = true; + + auto* sourceDestroyable = source->GetComponent(); + + if (sourceDestroyable == nullptr) { + Game::logger->Log("Damage", "Source entity has no destroyable componen %i", source->GetLOT()); + return {{eStatTypes::Physical, baseDamage}}; + } + + std::stringstream ss; + + std::map damageMap = {{eStatTypes::Physical, baseDamage}}; + + for (eStatTypes damageType = eStatTypes::Physical; damageType < eStatTypes::MAX; + damageType = static_cast(static_cast(damageType) + 1)) { + // Get the damage for this damage type + float damage = sourceDestroyable->GetStat(damageType, eStatModifier::DamageAbsolute); + + damage *= 1.0f + sourceDestroyable->GetStat(damageType, eStatModifier::DamagePercent) + ::log(baseDamage); + + if (damageProfile != nullptr) + { + damage += damageProfile->GetDamageProfile(damageType); + } + + /* + Level scaling + */ + float ourLevel = (static_cast(m_Info.level) <= 1) ? 1 : m_Info.level; + float sourceLevel; + + if (source->IsPlayer()) + { + auto* levelProgressionComponent = source->GetComponent(); + + if (levelProgressionComponent == nullptr) + { + Game::logger->Log("Damage", "Source entity is player but has no level progression component %i", source->GetLOT()); + return {{eStatTypes::Physical, baseDamage}}; + } + + sourceLevel = levelProgressionComponent->GetLevel(); + + // Player max is level 45, scale it to between 1 and 10 + sourceLevel = static_cast(static_cast(sourceLevel) / 4.5f); + + // Make sure it's between 1 and 10 + sourceLevel = std::max(sourceLevel, 1.0f); + sourceLevel = std::min(sourceLevel, 10.0f); + + if (ourLevel < sourceLevel) + { + damage /= 1 + (sourceLevel - ourLevel); + } + } + else + { + sourceLevel = (static_cast(sourceDestroyable->m_Info.level) <= 1) ? 1 : sourceDestroyable->m_Info.level; + + if (m_Parent->IsPlayer()) + { + auto* levelProgressionComponent = m_Parent->GetComponent(); + + if (levelProgressionComponent == nullptr) + { + Game::logger->Log("Damage", "Source entity is player but has no level progression component %i", m_Parent->GetLOT()); + return {{eStatTypes::Physical, baseDamage}}; + } + + ourLevel = levelProgressionComponent->GetLevel(); + + // Player max is level 45, scale it to between 1 and 10 + ourLevel = static_cast(static_cast(ourLevel) / 4.5f); + + // Make sure it's between 1 and 10 + ourLevel = std::max(ourLevel, 1.0f); + ourLevel = std::min(ourLevel, 10.0f); + + if (ourLevel > sourceLevel) + { + damage *= 1 + (ourLevel - sourceLevel); + } + } + } + + // Idk + + // If the damage is 0, skip this damage type + if (damage == 0) { + continue; + } + + // Calculate our resistance for this damage type + const auto resistance = this->GetStat(damageType, eStatModifier::DamageResistance); + + // Cap resistance at 80% + const auto cappedResistance = std::min(resistance, 0.8f); + + // Calculate the damage we take + const auto damageTaken = damage * (1.0f - cappedResistance); + + // Add the damage to our total damage + const auto& it = damageMap.find(damageType); + + if (it == damageMap.end()) { + damageMap[damageType] = damageTaken; + } else { + damageMap[damageType] += damageTaken; + } + + // Log the calculation + if (debug) { + ss << "Damage type: " << static_cast(damageType) << std::endl; + ss << "\tDamage: " << damage << std::endl; + ss << "\tResistance: " << resistance << std::endl; + ss << "\tCapped resistance: " << cappedResistance << std::endl; + ss << "\tDamage taken: " << damageTaken << std::endl; + } + } + + return damageMap; +} + +void DestroyableComponent::Damage(const std::map& damage, LWOOBJID source, uint32_t skillID, bool echo) { + float totalDamage = 0; + + for (const auto& [damageType, damageAmount] : damage) { + totalDamage += damageAmount; + } + + // Find the greatest damage type + eStatTypes greatestDamageType = eStatTypes::Physical; + + for (const auto& [damageType, damageAmount] : damage) { + if (damageAmount > damage.at(greatestDamageType)) { + greatestDamageType = damageType; + } + } + + const auto effectName = std::to_string(GeneralUtils::GenerateRandomNumber(100, 10000)); + + const auto& objectId = m_Parent->GetObjectID(); + + const bool critical = GeneralUtils::GenerateRandomNumber(0, 100) <= 10; + + if (critical) { + totalDamage *= 2; + } + + // Play an effect on us representing the damage + switch (greatestDamageType) { + case eStatTypes::Heat: + if (!critical) GameMessages::SendPlayFXEffect(objectId, 50, u"onhit", effectName); + else GameMessages::SendPlayFXEffect(objectId, 1578, u"onhit", effectName); + break; + case eStatTypes::Electric: + if (!critical) GameMessages::SendPlayFXEffect(objectId, 4027, u"create", effectName); + else GameMessages::SendPlayFXEffect(objectId, 953, u"onhit", effectName); + break; + case eStatTypes::Corruption: + if (!critical) GameMessages::SendPlayFXEffect(objectId, 7, u"onhit", effectName); + else GameMessages::SendPlayFXEffect(objectId, 1153, u"death", effectName); + break; + case eStatTypes::Physical: + default: + if (!critical) GameMessages::SendPlayFXEffect(objectId, 5039, u"on-anim", effectName); + else GameMessages::SendPlayFXEffect(objectId, 4972, u"onhitObject", effectName); + break; + } + + m_Parent->AddCallbackTimer(1.5f, [this, effectName]() { + GameMessages::SendStopFXEffect(m_Parent, true, effectName); + }); + + Damage(totalDamage, source, skillID, echo); +} + +void DestroyableComponent::Update(float deltaTime) { + if (m_DirtyStats) { + ComputeBaseStats(); + m_DirtyStats = false; + } +} + void DestroyableComponent::AddOnHitCallback(const std::function& callback) { m_OnHitCallbacks.push_back(callback); } diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 5e5133b7..7cacd3d5 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -7,6 +7,8 @@ #include "Entity.h" #include "Component.h" #include "eReplicaComponentType.h" +#include "StatProperty.h" +#include "CDDestructibleComponentTable.h" namespace CppScripts { class Script; @@ -429,6 +431,46 @@ public: */ void FixStats(); + void SetInfo(const CDDestructibleComponent& info) { m_Info = info; } + + CDDestructibleComponent& GetInfo() { return m_Info; } + + /** + * Add a stat property to this entity + */ + void AddStat(const StatProperty& stat); + + /** + * Remove a stat property from this entity + */ + void RemoveStat(const StatProperty& stat); + + /** + * Get a stat property from this entity + */ + float GetStat(eStatTypes statType, eStatModifier statModifier) const; + + /** + * Compute the entity's health, armor, and imagination based on its stats + */ + void ComputeBaseStats(bool refill = false); + + /** + * Compute damage + */ + std::map ComputeDamage(uint32_t baseDamage, Entity* source, class DamageProfile* damageProfile); + + /** + * Damage this entity with a damage map + */ + void Damage(const std::map& damage, LWOOBJID source, uint32_t skillID = 0, bool echo = true); + + /** + * Updates the component in the game loop + * @param deltaTime time passed since last update + */ + void Update(float deltaTime) override; + /** * Adds a callback that is called when this entity is hit by some other entity * @param callback the callback to add @@ -605,6 +647,26 @@ private: uint32_t m_ImmuneToImaginationLossCount; uint32_t m_ImmuneToQuickbuildInterruptCount; uint32_t m_ImmuneToPullToPointCount; + + /** + * Stats for the entity + */ + std::map> m_Stats; + + /** + * Dirty flag for stats + */ + bool m_DirtyStats; + + /** + * The info for this component + */ + CDDestructibleComponent m_Info; + + /** + * Resistance profile for this entity + */ + class ResistanceProfile* m_ResistanceProfile; }; #endif // DESTROYABLECOMPONENT_H diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 618e93b6..df24ec2c 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -490,6 +490,11 @@ bool InventoryComponent::HasSpaceForLoot(const std::unordered_map& void InventoryComponent::LoadXml(tinyxml2::XMLDocument* document) { LoadPetXml(document); + EquipSkill(eSkillBar::Primary, BehaviorSlot::Primary, 1); + EquipSkill(eSkillBar::Primary, BehaviorSlot::Head, 6); + EquipSkill(eSkillBar::Primary, BehaviorSlot::Offhand, 7); + EquipSkill(eSkillBar::Primary, BehaviorSlot::Neck, 8); + auto* inventoryElement = document->FirstChildElement("obj")->FirstChildElement("inv"); if (inventoryElement == nullptr) { @@ -577,14 +582,79 @@ void InventoryComponent::LoadXml(tinyxml2::XMLDocument* document) { auto* extraInfo = itemElement->FirstChildElement("x"); if (extraInfo) { - std::string modInfo = extraInfo->Attribute("ma"); + // Check if has attribute "ma" + if (extraInfo->Attribute("ma") != nullptr) { + std::string modInfo = extraInfo->Attribute("ma"); - LDFBaseData* moduleAssembly = new LDFData(u"assemblyPartLOTs", GeneralUtils::ASCIIToUTF16(modInfo.substr(2, modInfo.size() - 1))); + LDFBaseData* moduleAssembly = new LDFData(u"assemblyPartLOTs", GeneralUtils::ASCIIToUTF16(modInfo.substr(2, modInfo.size() - 1))); - config.push_back(moduleAssembly); + config.push_back(moduleAssembly); + } } - const auto* item = new Item(id, lot, inventory, slot, count, bound, config, parent, subKey); + auto* statInfo = itemElement->Attribute("st"); + + std::vector stats; + + if (statInfo != nullptr) { + // type,modifier,value;... + std::vector statData = GeneralUtils::SplitString(statInfo, ';'); + + for (const auto& stat : statData) { + if (stat.empty()) { + continue; + } + + std::vector statParts = GeneralUtils::SplitString(stat, ','); + + if (statParts.size() != 3) { + continue; + } + + eStatTypes type = static_cast(std::stoi(statParts[0])); + eStatModifier modifier = static_cast(std::stoi(statParts[1])); + float value = std::stof(statParts[2]); + + stats.push_back(StatProperty(type, modifier, value)); + } + } + + std::vector modifiers; + + auto* modifierInfo = itemElement->Attribute("mo"); + + if (modifierInfo != nullptr) { + // name;... + std::vector modifierData = GeneralUtils::SplitString(modifierInfo, ';'); + + for (const auto& modifier : modifierData) { + if (modifier.empty()) { + continue; + } + + auto* modifierTemplate = ItemModifierTemplate::FindItemModifierTemplate(modifier); + + if (modifierTemplate == nullptr) { + Game::logger->Log("InventoryComponent", "Failed to find modifier template (%s)!", modifier.c_str()); + continue; + } + + modifiers.push_back(modifierTemplate); + } + } + + + auto* item = new Item(id, lot, inventory, slot, count, bound, config, parent, subKey); + + if (!stats.empty()) + { + item->GetStats() = stats; + } + + if (!modifiers.empty()) + { + item->GetModifiers() = modifiers; + } if (equipped) { const auto info = Inventory::FindItemComponent(lot); @@ -702,6 +772,24 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument* document) { itemElement->LinkEndChild(extraInfo); } + // St attribute + std::stringstream ss; + + for (const auto& stat : item->GetStats()) { + ss << static_cast(stat.type) << "," << static_cast(stat.modifier) << "," << stat.value << ";"; + } + + itemElement->SetAttribute("st", ss.str().c_str()); + + // Mo attribute + ss.str(""); + + for (const auto& modifier : item->GetModifiers()) { + ss << modifier->GetName() << ";"; + } + + itemElement->SetAttribute("mo", ss.str().c_str()); + bagElement->LinkEndChild(itemElement); } @@ -1031,7 +1119,17 @@ void InventoryComponent::ApplyBuff(Item* item) const { const auto buffs = FindBuffs(item, true); for (const auto buff : buffs) { - SkillComponent::HandleUnmanaged(buff, m_Parent->GetObjectID()); + SkillComponent::HandleUnmanaged(buff, m_Parent->GetObjectID(), LWOOBJID_EMPTY, item->GetId()); + } + + auto* destroyableComponent = m_Parent->GetComponent(); + + if (destroyableComponent == nullptr) { + return; + } + + for (const auto& stat : item->GetStats()) { + destroyableComponent->AddStat(stat); } } @@ -1040,7 +1138,17 @@ void InventoryComponent::RemoveBuff(Item* item) const { const auto buffs = FindBuffs(item, false); for (const auto buff : buffs) { - SkillComponent::HandleUnCast(buff, m_Parent->GetObjectID()); + SkillComponent::HandleUnCast(buff, m_Parent->GetObjectID(), item->GetId()); + } + + auto* destroyableComponent = m_Parent->GetComponent(); + + if (destroyableComponent == nullptr) { + return; + } + + for (const auto& stat : item->GetStats()) { + destroyableComponent->RemoveStat(stat); } } @@ -1157,23 +1265,35 @@ void InventoryComponent::AddItemSkills(const LOT lot) { return; } - const auto index = m_Skills.find(slot); + eSkillBar bar = eSkillBar::Gear; + + switch (slot) + { + case BehaviorSlot::Primary: + bar = eSkillBar::Primary; + break; + case BehaviorSlot::Consumable: + bar = eSkillBar::Consumable; + break; + } const auto skill = FindSkill(lot); if (skill == 0) { + UnequipSkill(bar, slot); + return; } - if (index != m_Skills.end()) { - const auto old = index->second; - - GameMessages::SendRemoveSkill(m_Parent, old); + if (slot == BehaviorSlot::Primary) { + EquipSkill(eSkillBar::Primary, BehaviorSlot::Primary, skill); + EquipSkill(eSkillBar::Gear, BehaviorSlot::Primary, skill); + EquipSkill(eSkillBar::ClassPrimary, BehaviorSlot::Primary, skill); + EquipSkill(eSkillBar::ClassSecondary, BehaviorSlot::Primary, skill); + } + else { + EquipSkill(bar, slot, skill); } - - GameMessages::SendAddSkill(m_Parent, skill, static_cast(slot)); - - m_Skills.insert_or_assign(slot, skill); } void InventoryComponent::RemoveItemSkills(const LOT lot) { @@ -1185,23 +1305,155 @@ void InventoryComponent::RemoveItemSkills(const LOT lot) { return; } - const auto index = m_Skills.find(slot); + if (slot == BehaviorSlot::Primary) { + EquipSkill(eSkillBar::Primary, BehaviorSlot::Primary, 1); + EquipSkill(eSkillBar::Gear, BehaviorSlot::Primary, 1); + EquipSkill(eSkillBar::ClassPrimary, BehaviorSlot::Primary, 1); + EquipSkill(eSkillBar::ClassSecondary, BehaviorSlot::Primary, 1); - if (index == m_Skills.end()) { return; } - const auto old = index->second; + eSkillBar bar = eSkillBar::Gear; - GameMessages::SendRemoveSkill(m_Parent, old); - - m_Skills.erase(slot); - - if (slot == BehaviorSlot::Primary) { - m_Skills.insert_or_assign(BehaviorSlot::Primary, 1); - - GameMessages::SendAddSkill(m_Parent, 1, static_cast(BehaviorSlot::Primary)); + switch (slot) + { + case BehaviorSlot::Primary: + bar = eSkillBar::Primary; + break; + case BehaviorSlot::Consumable: + bar = eSkillBar::Consumable; + break; } + + UnequipSkill(bar, slot); +} + +void InventoryComponent::EquipSkill(eSkillBar bar, BehaviorSlot slot, uint32_t skillID) { + Game::logger->Log("InventoryComponent", "Equipping skill %i to slot %i on bar %i", skillID, slot, bar); + + const auto& barIt = m_EquippedSkills.find(bar); + + if (barIt == m_EquippedSkills.end()) { + m_EquippedSkills.emplace(bar, std::map()); + } + + auto& barMap = m_EquippedSkills.at(bar); + + const auto& slotIt = barMap.find(slot); + + barMap.insert_or_assign(slot, skillID); + + UpdateSkills(); +} + +void InventoryComponent::UnequipSkill(eSkillBar bar, BehaviorSlot slot) { + Game::logger->Log("InventoryComponent", "Unequipping skill from slot %i on bar %i", slot, bar); + + const auto& barIt = m_EquippedSkills.find(bar); + + if (barIt == m_EquippedSkills.end()) { + return; + } + + auto& barMap = m_EquippedSkills.at(bar); + + const auto& slotIt = barMap.find(slot); + + if (slotIt == barMap.end()) { + return; + } + + const auto old = slotIt->second; + + barMap.erase(slot); + + if (bar == eSkillBar::Primary && slot == BehaviorSlot::Primary) { + barMap.insert_or_assign(BehaviorSlot::Primary, 1); + } + + if (barMap.empty()) { + m_EquippedSkills.erase(bar); + } + + UpdateSkills(); +} + +uint32_t InventoryComponent::GetSkill(eSkillBar bar, BehaviorSlot slot) const { + const auto& barIt = m_EquippedSkills.find(bar); + + if (barIt == m_EquippedSkills.end()) { + return 0; + } + + const auto& barMap = barIt->second; + + const auto& slotIt = barMap.find(slot); + + if (slotIt == barMap.end()) { + return 0; + } + + return slotIt->second; +} + +eSkillBar InventoryComponent::GetSelectedSkillBar() const { + return m_SelectedSkillBar; +} + +void InventoryComponent::SetSelectedSkillBar(eSkillBar bar) { + if (bar == m_SelectedSkillBar) { + return; + } + + m_SelectedSkillBar = bar; + + UpdateSkills(); +} + +void InventoryComponent::UpdateSkills() { + // The active skills are kept in m_Skills, compare what is there to what should be there + // and add/remove skills as needed + const auto& barIt = m_EquippedSkills.find(m_SelectedSkillBar); + + if (barIt == m_EquippedSkills.end()) { + // Unequip all skills + for (auto& pair : m_Skills) { + GameMessages::SendRemoveSkill(m_Parent, pair.second); + } + + return; + } + + const auto& barMap = barIt->second; + + for (BehaviorSlot slot = BehaviorSlot::Primary; slot < BehaviorSlot::Consumable;) { + const auto& slotIt = barMap.find(slot); + + if (slotIt == barMap.end()) { + // Unequip the skill + const auto& skillIt = m_Skills.find(slot); + + if (skillIt != m_Skills.end()) { + GameMessages::SendRemoveSkill(m_Parent, skillIt->second); + } + } + else { + // Equip the skill + const auto& skillIt = m_Skills.find(slot); + + if (skillIt != m_Skills.end()) { + GameMessages::SendRemoveSkill(m_Parent, skillIt->second); + } + + GameMessages::SendAddSkill(m_Parent, slotIt->second, static_cast(slot)); + } + + slot = static_cast(static_cast(slot) + 1); + } + + // Update the active skills + m_Skills = barMap; } void InventoryComponent::TriggerPassiveAbility(PassiveAbilityTrigger trigger, Entity* target) { diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index 801f9f51..f24dd7d6 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -22,6 +22,7 @@ #include "eInventoryType.h" #include "eReplicaComponentType.h" #include "eLootSourceType.h" +#include "eSkillBar.h" class Entity; class ItemSet; @@ -281,6 +282,36 @@ public: */ void RemoveItemSkills(LOT lot); + /** + * Equip a skill to a bar and slot + */ + void EquipSkill(eSkillBar bar, BehaviorSlot slot, uint32_t skillID); + + /** + * Unequip a skill from a bar and slot + */ + void UnequipSkill(eSkillBar bar, BehaviorSlot slot); + + /** + * Get the skill in a bar and slot + */ + uint32_t GetSkill(eSkillBar bar, BehaviorSlot slot) const; + + /** + * Get the currently selected skill bar + */ + eSkillBar GetSelectedSkillBar() const; + + /** + * Set the currently selected skill bar + */ + void SetSelectedSkillBar(eSkillBar bar); + + /** + * Update skills on client + */ + void UpdateSkills(); + /** * Triggers one of the passive abilities from the equipped item set * @param trigger the trigger to fire @@ -416,6 +447,16 @@ private: */ LOT m_Consumable; + /** + * Equipped skills + */ + std::map> m_EquippedSkills; + + /** + * The currently selected skill bar + */ + eSkillBar m_SelectedSkillBar = eSkillBar::Primary; + /** * Currently has a car equipped */ diff --git a/dGame/dComponents/SkillComponent.cpp b/dGame/dComponents/SkillComponent.cpp index c2f07425..427a32ca 100644 --- a/dGame/dComponents/SkillComponent.cpp +++ b/dGame/dComponents/SkillComponent.cpp @@ -448,11 +448,12 @@ void SkillComponent::SyncProjectileCalculation(const ProjectileSyncEntry& entry) delete bitStream; } -void SkillComponent::HandleUnmanaged(const uint32_t behaviorId, const LWOOBJID target, LWOOBJID source) { +void SkillComponent::HandleUnmanaged(const uint32_t behaviorId, const LWOOBJID target, LWOOBJID source, LWOOBJID itemID) { auto* context = new BehaviorContext(source); context->unmanaged = true; context->caster = target; + context->itemID = itemID; auto* behavior = Behavior::CreateBehavior(behaviorId); @@ -465,7 +466,7 @@ void SkillComponent::HandleUnmanaged(const uint32_t behaviorId, const LWOOBJID t delete context; } -void SkillComponent::HandleUnCast(const uint32_t behaviorId, const LWOOBJID target) { +void SkillComponent::HandleUnCast(const uint32_t behaviorId, const LWOOBJID target, LWOOBJID itemID) { auto* context = new BehaviorContext(target); context->caster = target; diff --git a/dGame/dComponents/SkillComponent.h b/dGame/dComponents/SkillComponent.h index 034e65ce..38754132 100644 --- a/dGame/dComponents/SkillComponent.h +++ b/dGame/dComponents/SkillComponent.h @@ -169,15 +169,17 @@ public: * @param behaviorId the root behavior ID of the skill * @param target the explicit target of the skill * @param source the explicit source of the skill + * @param itemID the explicit item ID of the skill */ - static void HandleUnmanaged(uint32_t behaviorId, LWOOBJID target, LWOOBJID source = LWOOBJID_EMPTY); + static void HandleUnmanaged(uint32_t behaviorId, LWOOBJID target, LWOOBJID source = LWOOBJID_EMPTY, LWOOBJID itemID = LWOOBJID_EMPTY); /** * Computes a server-side skill uncast calculation without an associated entity. * @param behaviorId the root behavior ID of the skill * @param target the explicit target of the skill + * @param itemID the explicit item ID of the skill */ - static void HandleUnCast(uint32_t behaviorId, LWOOBJID target); + static void HandleUnCast(uint32_t behaviorId, LWOOBJID target, LWOOBJID itemID = LWOOBJID_EMPTY); /** * @returns a unique ID for the next skill calculation diff --git a/dGame/dComponents/VendorComponent.cpp b/dGame/dComponents/VendorComponent.cpp index e89cc926..a9e73f9e 100644 --- a/dGame/dComponents/VendorComponent.cpp +++ b/dGame/dComponents/VendorComponent.cpp @@ -20,7 +20,8 @@ VendorComponent::~VendorComponent() = default; void VendorComponent::Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags) { outBitStream->Write1(); outBitStream->Write1(); // Has standard items (Required for vendors with missions.) - outBitStream->Write(HasCraftingStation()); // Has multi use items + outBitStream->Write1(); + //outBitStream->Write(HasCraftingStation()); // Has multi use items } void VendorComponent::OnUse(Entity* originator) { diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index be751598..5d93945f 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -34,6 +34,7 @@ #include "eMissionTaskType.h" #include "eReplicaComponentType.h" #include "eConnectionType.h" +#include "CDLookupTable.h" using namespace std; @@ -307,6 +308,48 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream* inStream, const System delete bs; } + auto* inventoryComponent = entity->GetComponent(); + + const auto switching = entity->HasVar(u"switching") && entity->GetVar(u"switching"); + + if (inventoryComponent != nullptr && !switching) { + bool switched; + + const auto primarySkill = inventoryComponent->GetSkill(eSkillBar::Primary, BehaviorSlot::Primary); + + if (startSkill.skillID == rose::id("grim:skills:main-1")) { + inventoryComponent->SetSelectedSkillBar(eSkillBar::Gear); + switched = true; + } + else if (startSkill.skillID == rose::id("grim:skills:main-2")) { + inventoryComponent->SetSelectedSkillBar(eSkillBar::ClassPrimary); + switched = true; + } + else if (startSkill.skillID == rose::id("grim:skills:main-3")) { + inventoryComponent->SetSelectedSkillBar(eSkillBar::ClassSecondary); + switched = true; + } + else if (startSkill.skillID != primarySkill) { + inventoryComponent->SetSelectedSkillBar(eSkillBar::Primary); + switched = false; + } + else { + switched = false; + } + + if (switched) { + entity->SetVar(u"switching", true); + + entity->AddCallbackTimer(2, [inventoryComponent]() { + inventoryComponent->SetSelectedSkillBar(eSkillBar::Primary); + }); + + entity->AddCallbackTimer(0.25, [entity]() { + entity->SetVar(u"switching", false); + }); + } + } + if (Game::server->GetZoneID() == 1302) { break; } diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 16460025..56845557 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -96,6 +96,7 @@ #include "CDComponentsRegistryTable.h" #include "CDObjectsTable.h" +#include "CDLookupTable.h" void GameMessages::SendFireEventClientSide(const LWOOBJID& objectID, const SystemAddress& sysAddr, std::u16string args, const LWOOBJID& object, int64_t param1, int param2, const LWOOBJID& sender) { CBITSTREAM; @@ -1200,6 +1201,8 @@ void GameMessages::SendAddSkill(Entity* entity, TSkillID skillID, int slotID) { bitStream.Write(temporary); + Game::logger->Log("GameMessages", "SendAddSkill: %d", skillID); + SystemAddress sysAddr = entity->GetSystemAddress(); SEND_PACKET; } @@ -1213,6 +1216,8 @@ void GameMessages::SendRemoveSkill(Entity* entity, TSkillID skillID) { bitStream.Write(false); bitStream.Write(skillID); + Game::logger->Log("GameMessages", "SendRemoveSkill: %d", skillID); + SystemAddress sysAddr = entity->GetSystemAddress(); SEND_PACKET; } @@ -4845,15 +4850,49 @@ void GameMessages::HandleSellToVendor(RakNet::BitStream* inStream, Entity* entit // Items with a base value of 0 or max int are special items that should not be sold if they're not sub items if (itemComp.baseValue == 0 || itemComp.baseValue == UINT_MAX) return; - float sellScalar = vend->GetSellScalar(); - if (Inventory::IsValidItem(itemComp.currencyLOT)) { - const auto altCurrency = static_cast(itemComp.altCurrencyCost * sellScalar) * count; - inv->AddItem(itemComp.currencyLOT, std::floor(altCurrency), eLootSourceType::VENDOR); // Return alt currencies like faction tokens. + auto craftingCurrencies = CDItemComponentTable::ParseCraftingCurrencies(itemComp); + + Game::logger->Log("GameMessages", "User %llu %s selling %i to a vendor %i", player->GetObjectID(), user->GetUsername().c_str(), item->GetLot(), entity->GetLOT()); + + if (entity->GetLOT() == rose::id("grim:froge-anvil")) { + Game::logger->Log("GameMessages", "User %llu %s tried to sell an item %i to a forge anvil", player->GetObjectID(), user->GetUsername().c_str(), item->GetLot()); + + if (craftingCurrencies.empty()) return; + + for (const auto& craftingCurrency : craftingCurrencies) { + uint32_t count; + + if (craftingCurrency.second == 1) { + count = GeneralUtils::GenerateRandomNumber(0, 100) > 75 ? 1 : 0; + } else { + count = static_cast(std::floor(craftingCurrency.second * GeneralUtils::GenerateRandomNumber(0, 1))); + } + + if (count == 0) continue; + + inv->AddItem(craftingCurrency.first, count, eLootSourceType::VENDOR); + } + + item->SetCount(0); + + Game::logger->Log("GameMessages", "User %llu %s sold an item %i to a forge anvil", player->GetObjectID(), user->GetUsername().c_str(), item->GetLot()); + } else { + float sellScalar = vend->GetSellScalar(); + if (Inventory::IsValidItem(itemComp.currencyLOT)) { + const auto altCurrency = static_cast(itemComp.altCurrencyCost * sellScalar) * count; + inv->AddItem(itemComp.currencyLOT, std::floor(altCurrency), eLootSourceType::VENDOR); // Return alt currencies like faction tokens. + } + + if (!craftingCurrencies.empty() || !item->GetStats().empty()) { + item->SetCount(0); + } else { + inv->MoveItemToInventory(item, eInventoryType::VENDOR_BUYBACK, count, true, false, true); + } + + character->SetCoins(std::floor(character->GetCoins() + (static_cast(itemComp.baseValue * sellScalar) * count)), eLootSourceType::VENDOR); } //inv->RemoveItem(count, -1, iObjID); - inv->MoveItemToInventory(item, eInventoryType::VENDOR_BUYBACK, count, true, false, true); - character->SetCoins(std::floor(character->GetCoins() + (static_cast(itemComp.baseValue * sellScalar) * count)), eLootSourceType::VENDOR); //EntityManager::Instance()->SerializeEntity(player); // so inventory updates GameMessages::SendVendorTransactionResult(entity, sysAddr); } diff --git a/dGame/dGrim/CMakeLists.txt b/dGame/dGrim/CMakeLists.txt new file mode 100644 index 00000000..3faa9f31 --- /dev/null +++ b/dGame/dGrim/CMakeLists.txt @@ -0,0 +1,6 @@ +set(DGAME_DGRIM_SOURCES "StatProperty.cpp" + "ItemModifierTemplate.cpp" + "DamageProfile.cpp" + "ResistanceProfile.cpp" + "SpawnPatterns.cpp" + "EntityProfile.cpp" PARENT_SCOPE) diff --git a/dGame/dGrim/DamageProfile.cpp b/dGame/dGrim/DamageProfile.cpp new file mode 100644 index 00000000..5e6021f9 --- /dev/null +++ b/dGame/dGrim/DamageProfile.cpp @@ -0,0 +1,56 @@ +#include "DamageProfile.h" + +#include "tinyxml2.h" + +std::map DamageProfile::s_DamageProfiles; + +void DamageProfile::LoadDamageProfiles(const std::string& filename) { + tinyxml2::XMLDocument doc; + doc.LoadFile(filename.c_str()); + + auto root = doc.FirstChildElement("DamageProfiles"); + + for (auto element = root->FirstChildElement("DamageProfile"); element != nullptr; element = element->NextSiblingElement("DamageProfile")) { + auto skillID = element->IntAttribute("skillID"); + + DamageProfile damageProfile(skillID); + + for (auto damageElement = element->FirstChildElement("Damage"); damageElement != nullptr; damageElement = damageElement->NextSiblingElement("Damage")) { + auto statType = damageElement->IntAttribute("type"); + auto value = damageElement->FloatAttribute("value"); + + damageProfile.AddDamageProfile(static_cast(statType), value); + } + + s_DamageProfiles.emplace(skillID, damageProfile); + } +} + +DamageProfile* DamageProfile::FindDamageProfile(int32_t skillID) { + const auto& it = s_DamageProfiles.find(skillID); + + if (it != s_DamageProfiles.end()) { + return &it->second; + } + + return nullptr; +} + + +DamageProfile::DamageProfile(int32_t skillID) { + this->m_SkillID = skillID; +} + +void DamageProfile::AddDamageProfile(eStatTypes statType, float value) { + m_DamageProfile[statType] = value; +} + +float DamageProfile::GetDamageProfile(eStatTypes statType) const { + const auto& it = m_DamageProfile.find(statType); + + if (it != m_DamageProfile.end()) { + return it->second; + } + + return 0.0f; +} diff --git a/dGame/dGrim/DamageProfile.h b/dGame/dGrim/DamageProfile.h new file mode 100644 index 00000000..a9c70b64 --- /dev/null +++ b/dGame/dGrim/DamageProfile.h @@ -0,0 +1,33 @@ +#pragma once + +#ifndef __EDAMAGEPROFILE__H__ +#define __EDAMAGEPROFILE__H__ + +#include +#include +#include +#include "ItemModifierTemplate.h" + +class DamageProfile { +public: + DamageProfile(int32_t skillID); + + ~DamageProfile() = default; + + void AddDamageProfile(eStatTypes statType, float value); + + float GetDamageProfile(eStatTypes statType) const; + + static void LoadDamageProfiles(const std::string& filename); + + static DamageProfile* FindDamageProfile(int32_t skillID); + +private: + int32_t m_SkillID; + + std::map m_DamageProfile; + + static std::map s_DamageProfiles; +}; + +#endif //!__EDAMAGEPROFILE__H__ \ No newline at end of file diff --git a/dGame/dGrim/EntityProfile.cpp b/dGame/dGrim/EntityProfile.cpp new file mode 100644 index 00000000..1173ff70 --- /dev/null +++ b/dGame/dGrim/EntityProfile.cpp @@ -0,0 +1,38 @@ +#include "EntityProfile.h" + +#include "tinyxml2.h" + +std::map EntityProfile::s_EntityProfiles; + +EntityProfile::EntityProfile(int32_t lot) { + this->m_Lot = lot; +} + +void EntityProfile::LoadEntityProfiles(const std::string& filename) { + tinyxml2::XMLDocument doc; + doc.LoadFile(filename.c_str()); + + const auto root = doc.FirstChildElement("EntityProfiles"); + + for (auto elem = root->FirstChildElement("EntityProfile"); elem != nullptr; elem = elem->NextSiblingElement("EntityProfile")) { + const auto lot = elem->IntAttribute("lot"); + + EntityProfile profile(lot); + + profile.m_Level = elem->IntAttribute("level"); + profile.m_Health = elem->IntAttribute("health"); + profile.m_Armor = elem->IntAttribute("armor"); + + s_EntityProfiles.emplace(lot, profile); + } +} + +EntityProfile* EntityProfile::FindEntityProfile(int32_t lot) { + const auto it = s_EntityProfiles.find(lot); + + if (it != s_EntityProfiles.end()) { + return &it->second; + } + + return nullptr; +} diff --git a/dGame/dGrim/EntityProfile.h b/dGame/dGrim/EntityProfile.h new file mode 100644 index 00000000..9f3017f7 --- /dev/null +++ b/dGame/dGrim/EntityProfile.h @@ -0,0 +1,49 @@ +#pragma once + +#ifndef __EENTITYPROFILE__H__ +#define __EENTITYPROFILE__H__ + +#include +#include +#include +#include "ItemModifierTemplate.h" + +class EntityProfile { +public: + EntityProfile(int32_t lot); + + ~EntityProfile() = default; + + int32_t GetLot() const { + return this->m_Lot; + } + + int32_t GetLevel() const { + return this->m_Level; + } + + int32_t GetHealth() const { + return this->m_Health; + } + + int32_t GetArmor() const { + return this->m_Armor; + } + + static void LoadEntityProfiles(const std::string& filename); + + static EntityProfile* FindEntityProfile(int32_t lot); + +private: + int32_t m_Lot; + + int32_t m_Level; + + int32_t m_Health; + + int32_t m_Armor; + + static std::map s_EntityProfiles; +}; + +#endif //!__EENTITYPROFILE__H__ \ No newline at end of file diff --git a/dGame/dGrim/ItemModifierTemplate.cpp b/dGame/dGrim/ItemModifierTemplate.cpp new file mode 100644 index 00000000..563e5042 --- /dev/null +++ b/dGame/dGrim/ItemModifierTemplate.cpp @@ -0,0 +1,315 @@ +#include "ItemModifierTemplate.h" + +#include +#include +#include "Item.h" +#include "Entity.h" +#include "InventoryComponent.h" +#include "LevelProgressionComponent.h" + +std::vector ItemModifierTemplate::s_ItemModifierTemplates; + +void ItemModifierTemplate::LoadItemModifierTemplates(const std::string& filename) { + std::vector itemModifierTemplates; + + tinyxml2::XMLDocument doc; + doc.LoadFile(filename.c_str()); + + tinyxml2::XMLElement* root = doc.FirstChildElement("Templates"); + + for (tinyxml2::XMLElement* element = root->FirstChildElement("Template"); element != nullptr; element = element->NextSiblingElement("Template")) { + std::string name = element->Attribute("name"); + eStatRarity rarity = static_cast(element->IntAttribute("rarity")); + bool isPrefix = element->BoolAttribute("isPrefix"); + bool isSuffix = element->BoolAttribute("isSuffix"); + int32_t priority = element->IntAttribute("priority"); + + ModifierRoll roll; + roll.chance = element->FloatAttribute("chance"); + roll.levelMultiplier = element->FloatAttribute("levelMultiplier"); + roll.minRatity = element->IntAttribute("minRarity"); + roll.standardDeviation = element->FloatAttribute("standardDeviation"); + + std::string itemTypes = element->Attribute("itemTypes"); + std::stringstream ss(itemTypes); + std::string itemType; + while (std::getline(ss, itemType, ',')) { + roll.itemTypes.push_back(static_cast(std::stoi(itemType))); + } + + ItemModifierTemplate itemModifierTemplate(name, rarity, isPrefix, isSuffix, roll, priority); + + for (tinyxml2::XMLElement* statElement = element->FirstChildElement("Stat"); statElement != nullptr; statElement = statElement->NextSiblingElement("Stat")) { + eStatTypes type = static_cast(statElement->IntAttribute("type")); + eStatModifier modifier = static_cast(statElement->IntAttribute("modifier")); + float value = statElement->FloatAttribute("value"); + + StatProperty statProperty(type, modifier, value); + + itemModifierTemplate.AddStatProperty(statProperty); + } + + itemModifierTemplates.push_back(itemModifierTemplate); + } + + s_ItemModifierTemplates = itemModifierTemplates; +} + +ItemModifierTemplate* ItemModifierTemplate::FindItemModifierTemplate(const std::string& name) { + for (ItemModifierTemplate& itemModifierTemplate : s_ItemModifierTemplates) { + if (itemModifierTemplate.m_Name == name) { + return &itemModifierTemplate; + } + } + + return nullptr; +} + +void ItemModifierTemplate::RollItemModifierTemplates(Item* item, eLootSourceType lootSourceType) { + if (item->GetLot() == 6086) return; + + auto* entity = item->GetInventory()->GetComponent()->GetParent(); + + auto* levelProgressionComponent = entity->GetComponent(); + + if (levelProgressionComponent == nullptr) { + return; + } + + int32_t level = levelProgressionComponent->GetLevel(); + + std::vector prefixes; + std::vector suffixes; + std::vector statProperties; + + const auto& itemInfo = item->GetInfo(); + + int32_t rarity = itemInfo.rarity; + int32_t value = itemInfo.baseValue; + + for (ItemModifierTemplate& itemModifierTemplate : s_ItemModifierTemplates) { + const auto& rollInfo = itemModifierTemplate.m_ModifierRoll; + + if (rollInfo.minRatity > rarity) { + continue; + } + + if (rollInfo.itemTypes.size() > 0) { + bool found = false; + for (eItemType itemType : rollInfo.itemTypes) { + if (itemType == static_cast(itemInfo.itemType)) { + found = true; + break; + } + } + + if (!found) { + continue; + } + } + + float chance = rollInfo.chance; + + float rng = static_cast(rand()) / static_cast(RAND_MAX); + + if (rng > chance) { + continue; + } + + if (itemModifierTemplate.m_IsPrefix) { + prefixes.push_back(&itemModifierTemplate); + } + else if (itemModifierTemplate.m_IsSuffix) { + suffixes.push_back(&itemModifierTemplate); + } + } + + // Randomize order of prefixes and suffixes + std::random_shuffle(prefixes.begin(), prefixes.end()); + std::random_shuffle(suffixes.begin(), suffixes.end()); + + // Add the first prefix and suffix + if (prefixes.size() > 0) { + ItemModifierTemplate* prefix = prefixes[0]; + + item->GetModifiers().push_back(prefix); + } + + if (suffixes.size() > 0) { + ItemModifierTemplate* suffix = suffixes[0]; + + item->GetModifiers().push_back(suffix); + } + + // If there are more than one prefix or suffix, there is a 0.05 chance to add another one + if (prefixes.size() > 1) { + float rng = static_cast(rand()) / static_cast(RAND_MAX); + + if (rng < 0.05f) { + ItemModifierTemplate* prefix = prefixes[1]; + + item->GetModifiers().push_back(prefix); + } + } + + if (suffixes.size() > 1) { + float rng = static_cast(rand()) / static_cast(RAND_MAX); + + if (rng < 0.05f) { + ItemModifierTemplate* suffix = suffixes[1]; + + item->GetModifiers().push_back(suffix); + } + } + + // Add stat properties from prefixes and suffixes + for (ItemModifierTemplate* itemModifierTemplate : item->GetModifiers()) { + const std::vector& templateStatProperties = itemModifierTemplate->GetStatProperties(); + + auto rollInfo = itemModifierTemplate->m_ModifierRoll; + + // Roll stat properties + for (const StatProperty& statProperty : templateStatProperties) { + float value = statProperty.value; + + float rng = static_cast(rand()) / static_cast(RAND_MAX); + + float standardDeviation = rollInfo.standardDeviation; + + // (+/-) standardDeviation + float deviation = value * ((rng * standardDeviation * 2.0f) - standardDeviation); + + value += deviation; + + float levelMultiplier = rollInfo.levelMultiplier; + + float multiplier = 1.0f + (level * levelMultiplier); + + switch (itemInfo.rarity) + { + case 0: + break; + case 1: + multiplier += 0.0f; + break; + case 2: + multiplier += 0.5f; + break; + case 3: + multiplier += 0.75f; + break; + case 4: + multiplier += 1.25f; + break; + + + default: + break; + } + + value *= multiplier; + + if (itemInfo.isTwoHanded) { + value *= 1.75f; + } + + if (lootSourceType == eLootSourceType::VENDOR) { + value *= 0.75f; + } + + // Round to 2 decimal places + value = static_cast(static_cast(value * 100.0f)) / 100.0f; + + StatProperty newStatProperty(statProperty.type, statProperty.modifier, value); + + statProperties.push_back(newStatProperty); + } + } + + // Add stat properties + for (const StatProperty& statProperty : statProperties) { + item->GetStats().push_back(statProperty); + + } +} + +ItemModifierTemplate::ItemModifierTemplate(const std::string& name, eStatRarity rarity, bool isPrefix, bool isSuffix, const ModifierRoll& roll, int32_t priority) { + this->m_Name = name; + this->m_Rarity = rarity; + this->m_IsPrefix = isPrefix; + this->m_IsSuffix = isSuffix; + this->m_Priority = priority; + this->m_ModifierRoll = roll; + this->m_StatProperties = {}; +} + +void ItemModifierTemplate::AddStatProperty(const StatProperty& statProperty) { + m_StatProperties.push_back(statProperty); +} + +const std::vector& ItemModifierTemplate::GetStatProperties() const { + return m_StatProperties; +} + +std::string ItemModifierTemplate::HtmlString() const { + std::stringstream ss; + ss << ""; + + ss << m_Name << "\n"; + + return ss.str(); +} + +std::string ItemModifierTemplate::HtmlString(const std::vector& itemModifierTemplates) { + /* + Prefix-1 Prefix-2 NAME Suffix-1 Suffix-2 + */ + std::stringstream ss; + + for (ItemModifierTemplate* itemModifierTemplate : itemModifierTemplates) { + if (itemModifierTemplate->m_IsPrefix) { + ss << itemModifierTemplate->HtmlString(); + } + } + + ss << "NAME"; + + for (ItemModifierTemplate* itemModifierTemplate : itemModifierTemplates) { + if (itemModifierTemplate->m_IsSuffix) { + ss << itemModifierTemplate->HtmlString(); + } + } + + return ss.str(); +} + +const std::string& ItemModifierTemplate::GetName() const { + return m_Name; +} diff --git a/dGame/dGrim/ItemModifierTemplate.h b/dGame/dGrim/ItemModifierTemplate.h new file mode 100644 index 00000000..3917505d --- /dev/null +++ b/dGame/dGrim/ItemModifierTemplate.h @@ -0,0 +1,60 @@ +#pragma once + +#ifndef __ITEMMODIFIERTEMPLATE__H__ +#define __ITEMMODIFIERTEMPLATE__H__ + +#include +#include + +#include "StatProperty.h" +#include "StatRarity.h" +#include "eItemType.h" +#include "eLootSourceType.h" + +struct ModifierRoll { + std::vector itemTypes; + int32_t minRatity; + float chance; + float levelMultiplier; + float standardDeviation; +}; + +class ItemModifierTemplate { +public: + ItemModifierTemplate(const std::string& name, eStatRarity rarity, bool isPrefix, bool isSuffix, const ModifierRoll& roll, int32_t priority = 0); + ~ItemModifierTemplate() = default; + + void AddStatProperty(const StatProperty& statProperty); + + const std::vector& GetStatProperties() const; + + std::string HtmlString() const; + + const std::string& GetName() const; + + static void LoadItemModifierTemplates(const std::string& filename); + + static ItemModifierTemplate* FindItemModifierTemplate(const std::string& name); + + static void RollItemModifierTemplates(class Item* item, eLootSourceType lootSourceType); + + static std::string HtmlString(const std::vector& itemModifierTemplates); + +private: + std::string m_Name; + + eStatRarity m_Rarity; + + ModifierRoll m_ModifierRoll; + + bool m_IsPrefix; + bool m_IsSuffix; + + int32_t m_Priority; + + std::vector m_StatProperties; + + static std::vector s_ItemModifierTemplates; +}; + +#endif //!__ITEMMODIFIERTEMPLATE__H__ \ No newline at end of file diff --git a/dGame/dGrim/ResistanceProfile.cpp b/dGame/dGrim/ResistanceProfile.cpp new file mode 100644 index 00000000..5c752a5a --- /dev/null +++ b/dGame/dGrim/ResistanceProfile.cpp @@ -0,0 +1,67 @@ +#include "ResistanceProfile.h" + +#include "tinyxml2.h" + +std::map ResistanceProfile::s_ResistanceProfiles; + +void ResistanceProfile::LoadResistanceProfiles(const std::string& filename) { + tinyxml2::XMLDocument doc; + doc.LoadFile(filename.c_str()); + + auto root = doc.FirstChildElement("ResistanceProfiles"); + + for (auto element = root->FirstChildElement("ResistanceProfile"); element != nullptr; element = element->NextSiblingElement("ResistanceProfile")) { + // lot,lot,... + auto lots = element->Attribute("lot"); + + if (lots == nullptr) { + continue; + } + + const auto& splits = GeneralUtils::SplitString(lots, ','); + + for (const auto& split : splits) { + const auto lot = std::stoi(split); + + ResistanceProfile resistanceProfile(lot); + + for (auto resistanceElement = element->FirstChildElement("Resistance"); resistanceElement != nullptr; resistanceElement = resistanceElement->NextSiblingElement("Resistance")) { + auto statType = resistanceElement->IntAttribute("type"); + auto value = resistanceElement->FloatAttribute("value"); + + resistanceProfile.AddResistanceProfile(static_cast(statType), value); + } + + s_ResistanceProfiles.emplace(lot, resistanceProfile); + } + } +} + +ResistanceProfile* ResistanceProfile::FindResistanceProfile(int32_t lot) { + const auto& it = s_ResistanceProfiles.find(lot); + + if (it != s_ResistanceProfiles.end()) { + return &it->second; + } + + return nullptr; +} + +ResistanceProfile::ResistanceProfile(int32_t lot) { + this->m_Lot = lot; +} + +void ResistanceProfile::AddResistanceProfile(eStatTypes statType, float value) { + m_ResistanceProfile[statType] = value; +} + +float ResistanceProfile::GetResistanceProfile(eStatTypes statType) const { + const auto& it = m_ResistanceProfile.find(statType); + + if (it != m_ResistanceProfile.end()) { + return it->second; + } + + return 0.0f; +} + diff --git a/dGame/dGrim/ResistanceProfile.h b/dGame/dGrim/ResistanceProfile.h new file mode 100644 index 00000000..4c386fbc --- /dev/null +++ b/dGame/dGrim/ResistanceProfile.h @@ -0,0 +1,33 @@ +#pragma once + +#ifndef __ERESISTANCEPROFILE__H__ +#define __ERESISTANCEPROFILE__H__ + +#include +#include +#include +#include "ItemModifierTemplate.h" + +class ResistanceProfile { +public: + ResistanceProfile(int32_t lot); + + ~ResistanceProfile() = default; + + void AddResistanceProfile(eStatTypes statType, float value); + + float GetResistanceProfile(eStatTypes statType) const; + + static void LoadResistanceProfiles(const std::string& filename); + + static ResistanceProfile* FindResistanceProfile(int32_t lot); + +private: + int32_t m_Lot; + + std::map m_ResistanceProfile; + + static std::map s_ResistanceProfiles; +}; + +#endif //!__ERESISTANCEPROFILE__H__ \ No newline at end of file diff --git a/dGame/dGrim/SpawnPatterns.cpp b/dGame/dGrim/SpawnPatterns.cpp new file mode 100644 index 00000000..0c8d7693 --- /dev/null +++ b/dGame/dGrim/SpawnPatterns.cpp @@ -0,0 +1,58 @@ +#include "SpawnPatterns.h" + +#include "tinyxml2.h" + +std::map SpawnPatterns::s_SpawnPatterns; + +void SpawnPatterns::LoadSpawnPatterns(const std::string& filename) { + tinyxml2::XMLDocument doc; + doc.LoadFile(filename.c_str()); + + auto root = doc.FirstChildElement("SpawnPatterns"); + + for (auto element = root->FirstChildElement("SpawnPattern"); element != nullptr; element = element->NextSiblingElement("SpawnPattern")) { + auto lot = element->IntAttribute("lot"); + + SpawnPatterns spawnPatterns(lot); + + for (auto spawnElement = element->FirstChildElement("Spawn"); spawnElement != nullptr; spawnElement = spawnElement->NextSiblingElement("Spawn")) { + auto rating = spawnElement->FloatAttribute("rating"); + auto chance = spawnElement->FloatAttribute("chance"); + + std::vector spawns; + + for (auto spawn = spawnElement->FirstChildElement("SpawnLot"); spawn != nullptr; spawn = spawn->NextSiblingElement("SpawnID")) { + auto spawnID = spawn->IntAttribute("lot"); + + spawns.push_back(spawnID); + } + + spawnPatterns.AddSpawnPatterns(rating, chance, spawns); + } + + s_SpawnPatterns.emplace(lot, spawnPatterns); + } +} + +SpawnPatterns* SpawnPatterns::FindSpawnPatterns(int32_t lot) { + const auto& it = s_SpawnPatterns.find(lot); + + if (it != s_SpawnPatterns.end()) { + return &it->second; + } + + return nullptr; +} + +SpawnPatterns::SpawnPatterns(int32_t lot) +{ + this->m_Lot = lot; +} + +const std::map>>& SpawnPatterns::GetSpawnPatterns() const { + return m_SpawnPatterns; +} + +void SpawnPatterns::AddSpawnPatterns(float rating, float change, std::vector spawns) { + m_SpawnPatterns.emplace(rating, std::make_pair(change, spawns)); +} diff --git a/dGame/dGrim/SpawnPatterns.h b/dGame/dGrim/SpawnPatterns.h new file mode 100644 index 00000000..83d7121f --- /dev/null +++ b/dGame/dGrim/SpawnPatterns.h @@ -0,0 +1,33 @@ +#pragma once + +#ifndef __ESPAWNPATTERNS__H__ +#define __ESPAWNPATTERNS__H__ + +#include +#include +#include +#include "ItemModifierTemplate.h" + +class SpawnPatterns { +public: + SpawnPatterns(int32_t lot); + + ~SpawnPatterns() = default; + + void AddSpawnPatterns(float rating, float change, std::vector spawns); + + const std::map>>& GetSpawnPatterns() const; + + static void LoadSpawnPatterns(const std::string& filename); + + static SpawnPatterns* FindSpawnPatterns(int32_t lot); + +private: + int32_t m_Lot; + + std::map>> m_SpawnPatterns; + + static std::map s_SpawnPatterns; +}; + +#endif //!__ESPAWNPATTERNS__H__ \ No newline at end of file diff --git a/dGame/dGrim/StatModifier.h b/dGame/dGrim/StatModifier.h new file mode 100644 index 00000000..242a4aec --- /dev/null +++ b/dGame/dGrim/StatModifier.h @@ -0,0 +1,18 @@ +#pragma once + +#ifndef __ESTATMODIFIER__H__ +#define __ESTATMODIFIER__H__ + +#include + +enum class eStatModifier : uint32_t { + Absolute = 0, + Percent = 1, + DamageAbsolute = 2, + DamagePercent = 3, + DamageResistance = 4, + + MAX +}; + +#endif //!__ESTATMODIFIER__H__ \ No newline at end of file diff --git a/dGame/dGrim/StatProperty.cpp b/dGame/dGrim/StatProperty.cpp new file mode 100644 index 00000000..a4b88cce --- /dev/null +++ b/dGame/dGrim/StatProperty.cpp @@ -0,0 +1,132 @@ +#include "StatProperty.h" + +#include + +StatProperty::StatProperty(eStatTypes type, eStatModifier modifier, float value) { + this->type = type; + this->modifier = modifier; + this->value = value; +} + +std::string StatProperty::HtmlString() { + // "Physical: +20%\n..." + + std::stringstream ss; + ss << ""; + + switch (type) + { + case eStatTypes::Health: + ss << "Health"; + break; + case eStatTypes::Armor: + ss << "Armor"; + break; + case eStatTypes::Imagination: + ss << "Imagination"; + break; + case eStatTypes::Physical: + ss << "Physical"; + break; + case eStatTypes::Electric: + ss << "Electric"; + break; + case eStatTypes::Corruption: + ss << "Corruption"; + break; + case eStatTypes::Heat: + ss << "Heat"; + break; + case eStatTypes::Shadow: + ss << "Shadow"; + break; + case eStatTypes::Pierce: + ss << "Pierce"; + break; + case eStatTypes::Vitality: + ss << "Vitality"; + break; + case eStatTypes::Domination: + ss << "Domination"; + break; + default: + ss << "Unknown"; + break; + } + + switch (modifier) + { + case eStatModifier::DamageResistance: + ss << " Resistance"; + break; + case eStatModifier::DamagePercent: + case eStatModifier::DamageAbsolute: + ss << " Damage"; + break; + } + + ss << ""; + + switch (modifier) + { + case eStatModifier::Percent: + case eStatModifier::DamagePercent: + case eStatModifier::DamageResistance: + { + float percent = value * 100.0f; + // Round to 2 decimal places + percent = static_cast(static_cast(percent * 100.0f)) / 100.0f; + ss << ": +" << percent << "%"; + } + break; + case eStatModifier::Absolute: + case eStatModifier::DamageAbsolute: + default: + ss << ": +" << value; + break; + } + + return ss.str(); +} diff --git a/dGame/dGrim/StatProperty.h b/dGame/dGrim/StatProperty.h new file mode 100644 index 00000000..3b52b0b9 --- /dev/null +++ b/dGame/dGrim/StatProperty.h @@ -0,0 +1,26 @@ +#pragma once + +#ifndef __ESTAT__H__ +#define __ESTAT__H__ + +#include +#include + +#include "StatModifier.h" +#include "StatTypes.h" + +#define BASE_MULTIPLIER 100.0f + +struct StatProperty +{ + eStatTypes type; + eStatModifier modifier; + float value; + + StatProperty(eStatTypes type, eStatModifier modifier, float value); + + std::string HtmlString(); +}; + + +#endif //!__ESTAT__H__ \ No newline at end of file diff --git a/dGame/dGrim/StatRarity.h b/dGame/dGrim/StatRarity.h new file mode 100644 index 00000000..853c411e --- /dev/null +++ b/dGame/dGrim/StatRarity.h @@ -0,0 +1,19 @@ +#pragma once + +#ifndef __ESTATRARITY__H__ +#define __ESTATRARITY__H__ + +#include + +enum class eStatRarity : uint32_t { + Common = 0, + Uncommon = 1, + Rare = 2, + Epic = 3, + Legendary = 4, + Relic = 5, + + MAX +}; + +#endif //!__ESTATRARITY__H__ \ No newline at end of file diff --git a/dGame/dGrim/StatTypes.h b/dGame/dGrim/StatTypes.h new file mode 100644 index 00000000..417b23fe --- /dev/null +++ b/dGame/dGrim/StatTypes.h @@ -0,0 +1,27 @@ +#pragma once + +#ifndef __ESTATTYPES__H__ +#define __ESTATTYPES__H__ + +#include + +enum class eStatTypes : uint32_t { + // Stats + Health = 0, + Armor = 1, + Imagination = 2, + + // Damage + Physical = 3, + Electric = 4, + Corruption = 5, + Heat = 6, + Shadow = 7, + Pierce = 8, + Vitality = 9, + Domination = 10, + + MAX +}; + +#endif //!__ESTATTYPES__H__ \ No newline at end of file diff --git a/dGame/dInventory/Item.cpp b/dGame/dInventory/Item.cpp index 83ac8869..b29ef0da 100644 --- a/dGame/dInventory/Item.cpp +++ b/dGame/dInventory/Item.cpp @@ -41,6 +41,7 @@ Item::Item(const LWOOBJID id, const LOT lot, Inventory* inventory, const uint32_ this->info = &Inventory::FindItemComponent(lot); this->preconditions = new PreconditionExpression(this->info->reqPrecondition); this->subKey = subKey; + this->stats = {}; inventory->AddManagedItem(this); } @@ -76,6 +77,7 @@ Item::Item( this->bound = info->isBOP || bound; this->preconditions = new PreconditionExpression(this->info->reqPrecondition); this->subKey = subKey; + this->stats = {}; LWOOBJID id = ObjectIDManager::GenerateRandomObjectID(); @@ -102,6 +104,21 @@ Item::Item( EntityManager::Instance()->SerializeEntity(inventory->GetComponent()->GetParent()); } + + if (parent != LWOOBJID_EMPTY) return; + + eItemType itemType = static_cast(info->itemType); + + if (info->stackSize != 1) return; + + if (itemType != eItemType::RIGHT_HAND && + itemType != eItemType::LEFT_HAND && + itemType != eItemType::HAT && + itemType != eItemType::CHEST && + itemType != eItemType::LEGS && + itemType != eItemType::NECK) return; + + ItemModifierTemplate::RollItemModifierTemplates(this, lootSourceType); } LWOOBJID Item::GetId() const { @@ -124,6 +141,14 @@ std::vector& Item::GetConfig() { return config; } +std::vector& Item::GetStats() { + return stats; +} + +std::vector& Item::GetModifiers() { + return templates; +} + const CDItemComponent& Item::GetInfo() const { return *info; } @@ -220,6 +245,37 @@ void Item::Equip(const bool skipChecks) { } inventory->GetComponent()->EquipItem(this, skipChecks); + + if (info->equipLocation != "special_r") return; + + inventory->GetComponent()->GetParent()->AddCallbackTimer(1, [this]() { + // Find the greatest stat for this item + eStatTypes statType = eStatTypes::Physical; + float statValue = 0.0f; + + for (const auto& stat : stats) { + if (stat.value > statValue) { + statType = stat.type; + statValue = stat.value; + } + } + + switch (statType) + { + case eStatTypes::Electric: + GameMessages::SendPlayFXEffect(id, 4027, u"create", "electric"); + break; + case eStatTypes::Shadow: + GameMessages::SendPlayFXEffect(id, 2710, u"create", "shadow"); + break; + case eStatTypes::Corruption: + GameMessages::SendPlayFXEffect(id, 663, u"create", "corruption"); + break; + + default: + break; + } + }); } void Item::UnEquip() { diff --git a/dGame/dInventory/Item.h b/dGame/dInventory/Item.h index be2359ef..e6d9303b 100644 --- a/dGame/dInventory/Item.h +++ b/dGame/dInventory/Item.h @@ -8,6 +8,8 @@ #include "Preconditions.h" #include "eInventoryType.h" #include "eLootSourceType.h" +#include "StatProperty.h" +#include "ItemModifierTemplate.h" /** * An item that can be stored in an inventory and optionally consumed or equipped @@ -116,6 +118,18 @@ public: */ std::vector& GetConfig(); + /** + * Returns the stats for this item + * @return the stats for this item + */ + std::vector& GetStats(); + + /** + * Returns the modifiers for this item + * @return the modifiers for this item + */ + std::vector& GetModifiers(); + /** * Returns the database info for this item * @return the database info for this item @@ -255,6 +269,16 @@ private: */ std::vector config; + /** + * Modifiers + */ + std::vector stats; + + /** + * Templates + */ + std::vector templates; + /** * The inventory this item belongs to */ diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index b65bf723..227328f0 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -82,6 +82,7 @@ #include "eConnectionType.h" #include "eChatInternalMessageType.h" #include "eMasterMessageType.h" +#include "CDSkillBehaviorTable.h" #include "CDObjectsTable.h" #include "CDZoneTableTable.h" @@ -194,6 +195,67 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit return; } + if (chatCommand == "d" && args.size() >= 2) { + // Print all the arguments + Game::logger->Log("SlashCommandHandler", "Args: %s, %s", args[0].c_str(), args[1].c_str()); + + int32_t ticketIndex; + LWOOBJID itemID; + + AMFArrayValue amfArgs; + + if (!GeneralUtils::TryParse(args[0], ticketIndex) || !GeneralUtils::TryParse(args[1], itemID)) { + ChatPackets::SendSystemMessage(sysAddr, u"Invalid arguments."); + return; + } + + std::stringstream message; + message << "desc"; + message << ticketIndex; + + auto* inventoryComponent = entity->GetComponent(); + + if (inventoryComponent == nullptr) { + return; + } + + auto* item = inventoryComponent->FindItemById(itemID); + + if (item == nullptr) { + return; + } + + Game::logger->Log("SlashCommandHandler", "Sending ticket %i, item LOT %i", ticketIndex, item->GetLot()); + + auto& stats = item->GetStats(); + + if (!stats.empty()) + { + std::stringstream description; + std::stringstream name; + + name << "NAME"; + + amfArgs.Insert("t", true); + + description << ItemModifierTemplate::HtmlString(item->GetModifiers()) << "\n"; + + for (auto& stat : stats) { + description << "\n" << stat.HtmlString(); + } + + amfArgs.Insert("d", description.str()); + + amfArgs.Insert("n", name.str()); + } + else + { + amfArgs.Insert("t", false); + } + + GameMessages::SendUIMessageServerToSingleClient(entity, sysAddr, message.str(), amfArgs); + } + if (chatCommand == "who") { ChatPackets::SendSystemMessage( sysAddr, @@ -722,10 +784,141 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit GameMessages::SendPlayFXEffect(entity->GetObjectID(), effectID, GeneralUtils::ASCIIToUTF16(args[1]), args[2]); } + if (chatCommand == "playeffect-weapon" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 3) { + int32_t effectID = 0; + + if (!GeneralUtils::TryParse(args[0], effectID)) { + return; + } + + auto* inventoryComponent = entity->GetComponent(); + + if (inventoryComponent == nullptr) { + return; + } + + const auto& equipped = inventoryComponent->GetEquippedItems(); + + const auto& weapon = equipped.find("special_r"); + + if (weapon == equipped.end()) { + ChatPackets::SendSystemMessage(sysAddr, u"You need to have a weapon equipped."); + return; + } + + // FIXME: use fallible ASCIIToUTF16 conversion, because non-ascii isn't valid anyway + GameMessages::SendPlayFXEffect(weapon->second.id, effectID, GeneralUtils::ASCIIToUTF16(args[1]), args[2]); + } + + + if (chatCommand == "itemx" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 3) { + int32_t itemLot; + + if (!GeneralUtils::TryParse(args[0], itemLot)) { + return; + } + + auto* inventoryComponent = entity->GetComponent(); + + if (inventoryComponent == nullptr) { + return; + } + + std::vector data; + + for (int32_t i = 1; (i + 1) < args.size(); i += 2) { + const auto& key = args[i]; + const auto& value = args[i + 1]; + + + auto* x = new LDFData(GeneralUtils::ASCIIToUTF16(key), value); + + data.push_back(x); + } + + inventoryComponent->AddItem(itemLot, 1, eLootSourceType::NONE, INVALID, data); + } + + if (chatCommand == "dismantle" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { + GameMessages::SendServerTradeInvite( + entity->GetObjectID(), + false, + entity->GetObjectID(), + GeneralUtils::UTF8ToUTF16("Dismantle"), + sysAddr + ); + + ChatPackets::SendSystemMessage(sysAddr, u"Opened dismantle window."); + } + + if (chatCommand == "bar" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { + int32_t barID; + + if (!GeneralUtils::TryParse(args[0], barID)) { + return; + } + + auto* inventoryComponent = entity->GetComponent(); + + if (inventoryComponent == nullptr) { + return; + } + + inventoryComponent->SetSelectedSkillBar(static_cast(barID)); + } + + if (chatCommand == "setskill" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 3) { + int32_t barID; + int32_t slotID; + int32_t skillID; + + if (!GeneralUtils::TryParse(args[0], barID)) { + return; + } + + if (!GeneralUtils::TryParse(args[1], slotID)) { + return; + } + + if (!GeneralUtils::TryParse(args[2], skillID)) { + return; + } + + auto* inventoryComponent = entity->GetComponent(); + + if (inventoryComponent == nullptr) { + return; + } + + if (skillID == 0) { + inventoryComponent->UnequipSkill(static_cast(barID), static_cast(slotID)); + return; + } + + inventoryComponent->EquipSkill(static_cast(barID), static_cast(slotID), skillID); + } + if (chatCommand == "stopeffect" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { GameMessages::SendStopFXEffect(entity, true, args[0]); } + if (chatCommand == "skill" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { + int32_t skillID; + + if (!GeneralUtils::TryParse(args[0], skillID)) { + return; + } + + CDSkillBehaviorTable* skillTable = CDClientManager::Instance().GetTable(); + uint32_t behaviorID = skillTable->GetSkillByID(skillID).behaviorID; + + ChatPackets::SendSystemMessage(sysAddr, u"Skill ID: " + GeneralUtils::to_u16string(skillID) + u" Behavior ID: " + GeneralUtils::to_u16string(behaviorID)); + + auto* skillComponent = entity->GetComponent(); + + skillComponent->CalculateBehavior(skillID, behaviorID, LWOOBJID_EMPTY); + } + if (chatCommand == "setanntitle" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { if (args.size() < 0) return; @@ -2010,6 +2203,38 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit ChatPackets::SendSystemMessage(sysAddr, u"Trigger: " + (GeneralUtils::to_u16string(trigger->id))); } } + } else if (args[1] == "-h") { + auto* destroyableComponent = closest->GetComponent(); + + if (destroyableComponent == nullptr) { + ChatPackets::SendSystemMessage(sysAddr, u"No destroyable component on this entity!"); + return; + } + + ChatPackets::SendSystemMessage(sysAddr, u"Health: " + (GeneralUtils::to_u16string(destroyableComponent->GetHealth()))); + ChatPackets::SendSystemMessage(sysAddr, u"Max health: " + (GeneralUtils::to_u16string(destroyableComponent->GetMaxHealth()))); + ChatPackets::SendSystemMessage(sysAddr, u"Armor: " + (GeneralUtils::to_u16string(destroyableComponent->GetArmor()))); + ChatPackets::SendSystemMessage(sysAddr, u"Max armor: " + (GeneralUtils::to_u16string(destroyableComponent->GetMaxArmor()))); + } else if (args[1] == "-c" && args.size() >= 3) { + int32_t skillID; + + if (!GeneralUtils::TryParse(args[2], skillID)) { + return; + } + + CDSkillBehaviorTable* skillTable = CDClientManager::Instance().GetTable(); + uint32_t behaviorID = skillTable->GetSkillByID(skillID).behaviorID; + + ChatPackets::SendSystemMessage(sysAddr, u"Skill ID: " + GeneralUtils::to_u16string(skillID) + u" Behavior ID: " + GeneralUtils::to_u16string(behaviorID)); + + auto* skillComponent = closest->GetComponent(); + + if (skillComponent == nullptr) { + ChatPackets::SendSystemMessage(sysAddr, u"No skill component on this entity!"); + return; + } + + skillComponent->CalculateBehavior(skillID, behaviorID, LWOOBJID_EMPTY); } } } diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp index a51af03a..48fd508f 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp @@ -15,6 +15,8 @@ #include "SkillComponent.h" #include "eReplicaComponentType.h" #include "RenderComponent.h" +#include "Player.h" +#include "ZoneInstanceManager.h" #include @@ -35,6 +37,12 @@ void BossSpiderQueenEnemyServer::OnStartup(Entity* self) { if (!destroyable || !controllable) return; + destroyable->GetInfo().level = 3; + destroyable->GetInfo().armor = 330; + destroyable->ComputeBaseStats(true); + + EntityManager::Instance()->SerializeEntity(self); + // Determine Spider Boss health transition thresholds int spiderBossHealth = destroyable->GetMaxHealth(); int transitionTickHealth = spiderBossHealth / 3; @@ -45,6 +53,11 @@ void BossSpiderQueenEnemyServer::OnStartup(Entity* self) { originRotation = controllable->GetRotation(); combat->SetStunImmune(true); + combat->SetDisabled(true); + + self->AddCallbackTimer(10, [this, self]() { + combat->SetDisabled(false); + }); m_CurrentBossStage = 1; @@ -103,20 +116,22 @@ void BossSpiderQueenEnemyServer::WithdrawSpider(Entity* self, const bool withdra baseCombatAi->SetDisabled(true); - float animTime = PlayAnimAndReturnTime(self, spiderWithdrawAnim); - float withdrawTime = animTime - 0.25f; + self->AddCallbackTimer(3, [this, self]() { + float animTime = PlayAnimAndReturnTime(self, spiderWithdrawAnim); + float withdrawTime = animTime - 0.25f; - combat->SetStunImmune(false); - combat->Stun(withdrawTime + 6.0f); - combat->SetStunImmune(true); + combat->SetStunImmune(false); + combat->Stun(withdrawTime + 6.0f); + combat->SetStunImmune(true); - //TODO: Set faction to -1 and set immunity - destroyable->SetFaction(-1); - destroyable->SetIsImmune(true); - EntityManager::Instance()->SerializeEntity(self); + //TODO: Set faction to -1 and set immunity + destroyable->SetFaction(-1); + destroyable->SetIsImmune(true); + EntityManager::Instance()->SerializeEntity(self); - self->AddTimer("WithdrawComplete", withdrawTime + 1.0f); - waitForIdle = true; + self->AddTimer("WithdrawComplete", withdrawTime + 1.0f); + waitForIdle = true; + }); } else { controllable->SetStatic(false); @@ -146,6 +161,10 @@ void BossSpiderQueenEnemyServer::WithdrawSpider(Entity* self, const bool withdra //Reset the current wave death counter m_DeathCounter = 0; + auto* destroyable = self->GetComponent(); + + destroyable->SetArmor(destroyable->GetMaxArmor() / 3); + EntityManager::Instance()->SerializeEntity(self); // Prepare a timer for post leap attack @@ -297,9 +316,20 @@ void BossSpiderQueenEnemyServer::RunRainOfFire(Entity* self) { index++; } - const auto animTime = PlayAnimAndReturnTime(self, spiderROFAnim); + self->AddCallbackTimer(5, [self, this]() { + /* + auto* skillComponent = self->GetComponent(); - self->AddTimer("StartROF", animTime); + skillComponent->Interrupt(); + + auto* baseCombatAIComponent = self->GetComponent();*/ + + const auto animTime = PlayAnimAndReturnTime(self, spiderROFAnim); + + //baseCombatAIComponent->Stun(animTime * 2); + + self->AddTimer("StartROF", animTime); + }); } void BossSpiderQueenEnemyServer::RainOfFireManager(Entity* self) { @@ -324,6 +354,44 @@ void BossSpiderQueenEnemyServer::RainOfFireManager(Entity* self) { skillComponent->CalculateBehavior(1376, 32168, LWOOBJID_EMPTY, true); + if (GeneralUtils::GenerateRandomNumber(0, 2) == 1) + { + entity->AddCallbackTimer(2, [entity](){ + EntityInfo info; + info.lot = 16197; + info.pos = entity->GetPosition(); + info.spawnerID = entity->GetObjectID(); + + auto* spawned = EntityManager::Instance()->CreateEntity(info); + + EntityManager::Instance()->ConstructEntity(spawned); + }); + } + else + { + for (size_t i = 0; i < 15; i++) + { + entity->AddCallbackTimer((0.15 * i), [entity](){ + // Random area within 10 units on the X and Z axis, circle, using sin and cos + float angle = GeneralUtils::GenerateRandomNumber(0, 360) * M_PI / 180.0f; + float radius = GeneralUtils::GenerateRandomNumber(0, 10); + + float x = radius * cos(angle); + float z = radius * sin(angle); + + EntityInfo info; + info.lot = 10314; + info.pos = entity->GetPosition() + NiPoint3(x, 0, z); + info.spawnerID = entity->GetObjectID(); + + auto* spawned = EntityManager::Instance()->CreateEntity(info); + + EntityManager::Instance()->ConstructEntity(spawned); + }); + } + + } + self->AddTimer("PollROFManager", 0.5f); return; @@ -563,6 +631,22 @@ void BossSpiderQueenEnemyServer::OnTimerDone(Entity* self, const std::string tim } } +void BossSpiderQueenEnemyServer::OnPlayerDied(Entity* self, Entity* player) { + Game::logger->Log("BossSpiderQueenEnemyServer", "OnPlayerDied"); + + if (!player->IsPlayer()) return; + + Game::logger->Log("BossSpiderQueenEnemyServer", "OnPlayerDied 2"); + + auto* ply = static_cast(player); + + ply->SendToZone(1100); + + self->AddCallbackTimer(10, [] () { + dZoneManager::Instance()->GetZoneControlObject()->SetVar(u"shutdown", true); + }); +} + void BossSpiderQueenEnemyServer::OnHitOrHealResult(Entity* self, Entity* attacker, int32_t damage) { if (m_CurrentBossStage > 0 && !self->HasTimer("RFS")) { self->AddTimer("RFS", 5.0f); diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h index 8f73d99f..c46bca85 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h @@ -46,6 +46,8 @@ public: void OnTimerDone(Entity* self, std::string timerName) override; + void OnPlayerDied(Entity* self, Entity* player) override; + private: //Regular variables: DestroyableComponent* destroyable = nullptr; @@ -61,7 +63,7 @@ private: //Const variables: - const std::vector spiderWaveCntTable = { 2, 3 }; //The number of Dark Spiderling enemies to spawn per indexed wave number + const std::vector spiderWaveCntTable = { 6, 8 }; //The number of Dark Spiderling enemies to spawn per indexed wave number const int ROFImpactCnt = 2; //The number of ROF impacts in each quadrant of the arena selected at random diff --git a/dScripts/02_server/Map/Property/AG_Small/EnemySpiderSpawner.cpp b/dScripts/02_server/Map/Property/AG_Small/EnemySpiderSpawner.cpp index 0d4f568e..8425c22b 100644 --- a/dScripts/02_server/Map/Property/AG_Small/EnemySpiderSpawner.cpp +++ b/dScripts/02_server/Map/Property/AG_Small/EnemySpiderSpawner.cpp @@ -37,6 +37,7 @@ void EnemySpiderSpawner::OnTimerDone(Entity* self, std::string timerName) { if (timerName == "StartSpawnTime") { SpawnSpiderling(self); } else if (timerName == "SpawnSpiderling") { + GameMessages::SendStopFXEffect(self, true, "egg_puff_b"); GameMessages::SendPlayFXEffect(self->GetObjectID(), 644, u"create", "egg_puff_b", LWOOBJID_EMPTY, 1.0f, 1.0f, true); //TODO: set the aggro radius larger @@ -59,8 +60,10 @@ void EnemySpiderSpawner::OnTimerDone(Entity* self, std::string timerName) { movementAi->SetDestination(newEntity->GetPosition()); */ } + + self->AddTimer("StartSpawnTime", 5); - self->ScheduleKillAfterUpdate(); + //self->ScheduleKillAfterUpdate(); } } @@ -69,6 +72,7 @@ void EnemySpiderSpawner::OnTimerDone(Entity* self, std::string timerName) { //-------------------------------------------------------------- void EnemySpiderSpawner::SpawnSpiderling(Entity* self) { //Initiate the actual spawning + GameMessages::SendStopFXEffect(self, true, "dropdustmedium"); GameMessages::SendPlayFXEffect(self->GetObjectID(), 2260, u"rebuild_medium", "dropdustmedium", LWOOBJID_EMPTY, 1.0f, 1.0f, true); self->AddTimer("SpawnSpiderling", spawnTime); } diff --git a/dWorldServer/CMakeLists.txt b/dWorldServer/CMakeLists.txt index c616da87..bf8ac13e 100644 --- a/dWorldServer/CMakeLists.txt +++ b/dWorldServer/CMakeLists.txt @@ -7,5 +7,5 @@ add_library(dWorldServer ${DWORLDSERVER_SOURCES}) add_executable(WorldServer "WorldServer.cpp") target_link_libraries(dWorldServer ${COMMON_LIBRARIES}) -target_link_libraries(WorldServer ${COMMON_LIBRARIES} dChatFilter dGame dZoneManager dPhysics Detour Recast tinyxml2 dWorldServer dNavigation) +target_link_libraries(WorldServer ${COMMON_LIBRARIES} dChatFilter dGame dPhysics Detour Recast tinyxml2 dWorldServer dNavigation) diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 18625960..bf16d302 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -71,6 +71,11 @@ #include "eMasterMessageType.h" #include "eGameMessageType.h" #include "ZCompression.h" +#include "InventoryComponent.h" +#include "DamageProfile.h" +#include "ResistanceProfile.h" +#include "SpawnPatterns.h" +#include "EntityProfile.h" namespace Game { dLogger* logger = nullptr; @@ -147,6 +152,12 @@ int main(int argc, char** argv) { Game::logger->Log("WorldServer", "Version: %i.%i", PROJECT_VERSION_MAJOR, PROJECT_VERSION_MINOR); Game::logger->Log("WorldServer", "Compiled on: %s", __TIMESTAMP__); + ItemModifierTemplate::LoadItemModifierTemplates((BinaryPathFinder::GetBinaryDir() / "ItemModifierTemplates.xml").string()); + DamageProfile::LoadDamageProfiles((BinaryPathFinder::GetBinaryDir() / "DamageProfiles.xml").string()); + ResistanceProfile::LoadResistanceProfiles((BinaryPathFinder::GetBinaryDir() / "ResistanceProfiles.xml").string()); + SpawnPatterns::LoadSpawnPatterns((BinaryPathFinder::GetBinaryDir() / "SpawnPatterns.xml").string()); + EntityProfile::LoadEntityProfiles((BinaryPathFinder::GetBinaryDir() / "EntityProfiles.xml").string()); + if (Game::config->GetValue("disable_chat") == "1") chatDisabled = true; try { @@ -489,6 +500,8 @@ int main(int argc, char** argv) { framesSinceLastSQLPing = 0; } else framesSinceLastSQLPing++; + Spawner::UpdateRatings(deltaTime); + Metrics::EndMeasurement(MetricVariable::GameLoop); Metrics::StartMeasurement(MetricVariable::Sleep); @@ -512,6 +525,12 @@ int main(int argc, char** argv) { } } + auto* controller = dZoneManager::Instance()->GetZoneControlObject(); + + if (controller != nullptr && controller->HasVar(u"shutdown") && controller->GetVar(u"shutdown")) { + Game::shouldShutdown = true; + } + if (Game::shouldShutdown && !worldShutdownSequenceComplete) { WorldShutdownProcess(zoneID); break; diff --git a/dZoneManager/CMakeLists.txt b/dZoneManager/CMakeLists.txt index 1dd3841b..faec37d7 100644 --- a/dZoneManager/CMakeLists.txt +++ b/dZoneManager/CMakeLists.txt @@ -3,4 +3,6 @@ set(DZONEMANAGER_SOURCES "dZoneManager.cpp" "Spawner.cpp" "Zone.cpp") -add_library(dZoneManager STATIC ${DZONEMANAGER_SOURCES}) +#add_library(dZoneManager STATIC ${DZONEMANAGER_SOURCES}) + +set(DZONEMANAGER_SOURCES ${DZONEMANAGER_SOURCES} PARENT_SCOPE) diff --git a/dZoneManager/Spawner.cpp b/dZoneManager/Spawner.cpp index 28f77fea..fcbaa882 100644 --- a/dZoneManager/Spawner.cpp +++ b/dZoneManager/Spawner.cpp @@ -6,6 +6,9 @@ #include #include "GeneralUtils.h" #include "dZoneManager.h" +#include "SpawnPatterns.h" + +std::map>> Spawner::m_Ratings; Spawner::Spawner(const SpawnerInfo info) { m_Info = info; @@ -69,6 +72,10 @@ Spawner::Spawner(const SpawnerInfo info) { }); } } + + m_SpawnPattern = SpawnPatterns::FindSpawnPatterns(m_Info.templateID); + + m_LotsToCheck.push_back(m_Info.templateID); } Spawner::~Spawner() { @@ -102,6 +109,95 @@ Entity* Spawner::Spawn(std::vector freeNodes, const bool force) { m_EntityInfo.spawnerID = m_Info.spawnerID; } + bool usedSpawnPattern = false; + + if (m_SpawnPattern != nullptr) { + auto pattern = m_SpawnPattern->GetSpawnPatterns(); + + // Check the area rating + // std::map>> m_Ratings + for (const auto& lot : m_LotsToCheck) + { + const auto& it = m_Ratings.find(lot); + + int32_t rating = 0; + + if (it != m_Ratings.end()) { + // Check if we are within 50units of a rating + for (const auto& ratingIt : it->second) + { + if (NiPoint3::DistanceSquared(ratingIt.first, m_EntityInfo.pos) <= 100.0f * 100.0f) + { + rating = ratingIt.second; + break; + } + } + } + + for (const auto& it : pattern) + { + if (it.first > rating) continue; + + // Random number between 0 and 1 + float random = GeneralUtils::GenerateRandomNumber(0, 1); + + const auto& change = it.second.first; + + if (random >= change) continue; + + usedSpawnPattern = true; + + Entity* first = nullptr; + + for (const auto& spawn : it.second.second) + { + float angle = GeneralUtils::GenerateRandomNumber(0, 360) * M_PI / 180.0f; + float radius = GeneralUtils::GenerateRandomNumber(0, 6); + + float x = radius * cos(angle); + float z = radius * sin(angle); + + auto copy = m_EntityInfo; + copy.pos.x += x; + copy.pos.z += z; + copy.lot = spawn; + + if (std::find(m_LotsToCheck.begin(), m_LotsToCheck.end(), spawn) == m_LotsToCheck.end()) { + m_LotsToCheck.push_back(spawn); + } + + Entity* rezdE = EntityManager::Instance()->CreateEntity(copy, nullptr); + + rezdE->GetGroups() = m_Info.groups; + + EntityManager::Instance()->ConstructEntity(rezdE); + + m_Entities.insert({ rezdE->GetObjectID(), spawnNode }); + + spawnNode->entities.push_back(rezdE->GetObjectID()); + + for (const auto& cb : m_EntitySpawnedCallbacks) { + cb(rezdE); + } + + if (first == nullptr) { + first = rezdE; + } + + break; + } + + usedSpawnPattern = true; + + if (m_Entities.size() == m_Info.amountMaintained) { + m_NeedsUpdate = false; + } + + return first; + } + } + } + Entity* rezdE = EntityManager::Instance()->CreateEntity(m_EntityInfo, nullptr); rezdE->GetGroups() = m_Info.groups; @@ -233,6 +329,65 @@ void Spawner::NotifyOfEntityDeath(const LWOOBJID& objectID) { if (m_SpawnOnSmash != nullptr) { m_SpawnOnSmash->Reset(); } + + const auto& lot = m_Info.templateID; + + // Add to area rating + // std::map>> m_Ratings + // First check if the lot is in the map, if not, add it + // Than check if there exist any ratings for that lot within 50units of the spawner + // If there is, add 1 to the rating + // If there isn't, add a new rating + const auto& pos = node->position; + + const auto& it2 = m_Ratings.find(lot); + + if (it2 == m_Ratings.end()) { + m_Ratings.insert({ lot, { { pos, 1.0f } } }); + } else { + auto& ratings = it2->second; + + bool found = false; + for (auto& rating : ratings) { + if (NiPoint3::DistanceSquared(rating.first, pos) < 100.0f * 100.0f) { + rating.second += 1.0f; + + Game::logger->Log("Spawner", "Rating %f", rating.second); + found = true; + break; + } + } + + if (!found) { + ratings.push_back({ pos, 1.0f }); + } + } +} + +void Spawner::UpdateRatings(float deltaTime) { + // Loop through all ratings and decrease them by deltaTime + for (auto& rating : m_Ratings) { + for (auto& rating2 : rating.second) { + rating2.second -= deltaTime * 0.1f; + } + } + + // Loop through all ratings and remove any that are 0 or less + for (auto it = m_Ratings.begin(); it != m_Ratings.end();) { + for (auto it2 = it->second.begin(); it2 != it->second.end();) { + if (it2->second <= 0.0f) { + it2 = it->second.erase(it2); + } else { + ++it2; + } + } + + if (it->second.empty()) { + it = m_Ratings.erase(it); + } else { + ++it; + } + } } void Spawner::Activate() { diff --git a/dZoneManager/Spawner.h b/dZoneManager/Spawner.h index 1f610b71..e06d9a1f 100644 --- a/dZoneManager/Spawner.h +++ b/dZoneManager/Spawner.h @@ -10,6 +10,7 @@ #include #include "LDFFormat.h" #include "EntityInfo.h" +#include "SpawnPatterns.h" struct SpawnerNode { NiPoint3 position = NiPoint3::ZERO; @@ -67,6 +68,8 @@ public: void SetNumToMaintain(int32_t value); bool GetIsSpawnSmashGroup() const { return m_SpawnSmashFoundGroup; }; + static void UpdateRatings(float deltaTime); + SpawnerInfo m_Info; bool m_Active = true; private: @@ -82,6 +85,12 @@ private: int32_t m_AmountSpawned = 0; bool m_Start = false; Spawner* m_SpawnOnSmash = nullptr; + + SpawnPatterns* m_SpawnPattern = nullptr; + + std::vector m_LotsToCheck = {}; + + static std::map>> m_Ratings; }; #endif // SPAWNER_H