#include "DestroyableComponent.h" #include #include "dLogger.h" #include "Game.h" #include "dConfig.h" #include "Amf3.h" #include "AmfSerialize.h" #include "GameMessages.h" #include "User.h" #include "CDClientManager.h" #include "CDDestructibleComponentTable.h" #include "EntityManager.h" #include "RebuildComponent.h" #include "CppScripts.h" #include "Loot.h" #include "Character.h" #include "Spawner.h" #include "BaseCombatAIComponent.h" #include "TeamManager.h" #include "BuffComponent.h" #include "SkillComponent.h" #include "Item.h" #include #include #include "MissionComponent.h" #include "CharacterComponent.h" #include "PossessableComponent.h" #include "PossessorComponent.h" #include "InventoryComponent.h" #include "dZoneManager.h" #include "WorldConfig.h" #include "eMissionTaskType.h" #include "eStateChangeType.h" #include "eGameActivity.h" #include "LevelProgressionComponent.h" #include "ResistanceProfile.h" #include "DamageProfile.h" #include "CDComponentsRegistryTable.h" DestroyableComponent::DestroyableComponent(Entity* parent) : Component(parent) { m_iArmor = 0; m_fMaxArmor = 0.0f; m_iImagination = 0; m_fMaxImagination = 0.0f; m_FactionIDs = std::vector(); m_EnemyFactionIDs = std::vector(); m_IsSmashable = false; m_IsDead = false; m_IsSmashed = false; m_IsGMImmune = false; m_IsShielded = false; m_DamageToAbsorb = 0; m_IsModuleAssembly = m_Parent->HasComponent(eReplicaComponentType::MODULE_ASSEMBLY); m_DirtyThreatList = false; m_HasThreats = false; m_ExplodeFactor = 1.0f; m_iHealth = 0; m_fMaxHealth = 0; m_AttacksToBlock = 0; m_LootMatrixID = 0; m_MinCoins = 0; m_MaxCoins = 0; m_DamageReduction = 0; m_DirtyStats = true; m_ImmuneToBasicAttackCount = 0; m_ImmuneToDamageOverTimeCount = 0; m_ImmuneToKnockbackCount = 0; m_ImmuneToInterruptCount = 0; m_ImmuneToSpeedCount = 0; m_ImmuneToImaginationGainCount = 0; m_ImmuneToImaginationLossCount = 0; m_ImmuneToQuickbuildInterruptCount = 0; m_ImmuneToPullToPointCount = 0; m_ResistanceProfile = ResistanceProfile::FindResistanceProfile(m_Parent->GetLOT()); } DestroyableComponent::~DestroyableComponent() { } void DestroyableComponent::Reinitialize(LOT templateID) { CDComponentsRegistryTable* compRegistryTable = CDClientManager::Instance().GetTable(); int32_t buffComponentID = compRegistryTable->GetByIDAndType(templateID, eReplicaComponentType::BUFF); int32_t collectibleComponentID = compRegistryTable->GetByIDAndType(templateID, eReplicaComponentType::COLLECTIBLE); int32_t rebuildComponentID = compRegistryTable->GetByIDAndType(templateID, eReplicaComponentType::QUICK_BUILD); int32_t componentID = 0; if (collectibleComponentID > 0) componentID = collectibleComponentID; if (rebuildComponentID > 0) componentID = rebuildComponentID; if (buffComponentID > 0) componentID = buffComponentID; CDDestructibleComponentTable* destCompTable = CDClientManager::Instance().GetTable(); std::vector destCompData = destCompTable->Query([=](CDDestructibleComponent entry) { return (entry.id == componentID); }); 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); SetArmor(destCompData[0].armor); SetMaxHealth(destCompData[0].life); SetMaxImagination(destCompData[0].imagination); SetMaxArmor(destCompData[0].armor); 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); SetMaxHealth(1); SetMaxImagination(0); SetMaxArmor(0); SetIsSmashable(true); } ComputeBaseStats(); if (!m_Parent->IsPlayer()) { SetHealth(GetMaxHealth()); SetImagination(GetMaxImagination()); SetArmor(GetMaxArmor()); } } void DestroyableComponent::Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate) { if (bIsInitialUpdate) { outBitStream->Write1(); // always write these on construction outBitStream->Write(m_ImmuneToBasicAttackCount); outBitStream->Write(m_ImmuneToDamageOverTimeCount); outBitStream->Write(m_ImmuneToKnockbackCount); outBitStream->Write(m_ImmuneToInterruptCount); outBitStream->Write(m_ImmuneToSpeedCount); outBitStream->Write(m_ImmuneToImaginationGainCount); outBitStream->Write(m_ImmuneToImaginationLossCount); outBitStream->Write(m_ImmuneToQuickbuildInterruptCount); outBitStream->Write(m_ImmuneToPullToPointCount); } outBitStream->Write(m_DirtyHealth || bIsInitialUpdate); if (m_DirtyHealth || bIsInitialUpdate) { outBitStream->Write(m_iHealth); outBitStream->Write(m_fMaxHealth); outBitStream->Write(m_iArmor); outBitStream->Write(m_fMaxArmor); outBitStream->Write(m_iImagination); outBitStream->Write(m_fMaxImagination); outBitStream->Write(m_DamageToAbsorb); outBitStream->Write(IsImmune()); outBitStream->Write(m_IsGMImmune); outBitStream->Write(m_IsShielded); outBitStream->Write(m_fMaxHealth); outBitStream->Write(m_fMaxArmor); outBitStream->Write(m_fMaxImagination); outBitStream->Write(uint32_t(m_FactionIDs.size())); for (size_t i = 0; i < m_FactionIDs.size(); ++i) { outBitStream->Write(m_FactionIDs[i]); } outBitStream->Write(m_IsSmashable); if (bIsInitialUpdate) { outBitStream->Write(m_IsDead); outBitStream->Write(m_IsSmashed); if (m_IsSmashable) { outBitStream->Write(m_IsModuleAssembly); outBitStream->Write(m_ExplodeFactor != 1.0f); if (m_ExplodeFactor != 1.0f) outBitStream->Write(m_ExplodeFactor); } } m_DirtyHealth = false; } outBitStream->Write(m_DirtyThreatList || bIsInitialUpdate); if (m_DirtyThreatList || bIsInitialUpdate) { outBitStream->Write(m_HasThreats); m_DirtyThreatList = false; } } void DestroyableComponent::LoadFromXml(tinyxml2::XMLDocument* doc) { tinyxml2::XMLElement* dest = doc->FirstChildElement("obj")->FirstChildElement("dest"); if (!dest) { Game::logger->Log("DestroyableComponent", "Failed to find dest tag!"); return; } auto* buffComponent = m_Parent->GetComponent(); if (buffComponent != nullptr) { buffComponent->LoadFromXml(doc); } dest->QueryAttribute("hc", &m_iHealth); dest->QueryAttribute("hm", &m_fMaxHealth); dest->QueryAttribute("im", &m_fMaxImagination); dest->QueryAttribute("ic", &m_iImagination); dest->QueryAttribute("ac", &m_iArmor); dest->QueryAttribute("am", &m_fMaxArmor); m_DirtyHealth = true; } void DestroyableComponent::UpdateXml(tinyxml2::XMLDocument* doc) { tinyxml2::XMLElement* dest = doc->FirstChildElement("obj")->FirstChildElement("dest"); if (!dest) { Game::logger->Log("DestroyableComponent", "Failed to find dest tag!"); return; } auto* buffComponent = m_Parent->GetComponent(); if (buffComponent != nullptr) { buffComponent->UpdateXml(doc); } dest->SetAttribute("hc", m_iHealth); dest->SetAttribute("hm", m_fMaxHealth); dest->SetAttribute("im", m_fMaxImagination); dest->SetAttribute("ic", m_iImagination); dest->SetAttribute("ac", m_iArmor); dest->SetAttribute("am", m_fMaxArmor); } void DestroyableComponent::SetHealth(int32_t value) { m_DirtyHealth = true; auto* characterComponent = m_Parent->GetComponent(); if (characterComponent != nullptr) { characterComponent->TrackHealthDelta(value - m_iHealth); } m_iHealth = value; } void DestroyableComponent::SetMaxHealth(float value, bool playAnim) { m_DirtyHealth = true; // Used for playAnim if opted in for. int32_t difference = static_cast(std::abs(m_fMaxHealth - value)); m_fMaxHealth = value; if (m_iHealth > m_fMaxHealth) { m_iHealth = m_fMaxHealth; } if (playAnim) { // Now update the player bar if (!m_Parent->GetParentUser()) return; AMFArrayValue args; args.Insert("amount", std::to_string(difference)); args.Insert("type", "health"); GameMessages::SendUIMessageServerToSingleClient(m_Parent, m_Parent->GetParentUser()->GetSystemAddress(), "MaxPlayerBarUpdate", args); } Game::entityManager->SerializeEntity(m_Parent); } void DestroyableComponent::SetArmor(int32_t value) { m_DirtyHealth = true; // If Destroyable Component already has zero armor do not trigger the passive ability again. bool hadArmor = m_iArmor > 0; auto* characterComponent = m_Parent->GetComponent(); if (characterComponent != nullptr) { characterComponent->TrackArmorDelta(value - m_iArmor); } m_iArmor = value; auto* inventroyComponent = m_Parent->GetComponent(); if (m_iArmor == 0 && inventroyComponent != nullptr && hadArmor) { inventroyComponent->TriggerPassiveAbility(PassiveAbilityTrigger::SentinelArmor); } } void DestroyableComponent::SetMaxArmor(float value, bool playAnim) { m_DirtyHealth = true; m_fMaxArmor = value; if (m_iArmor > m_fMaxArmor) { m_iArmor = m_fMaxArmor; } if (playAnim) { // Now update the player bar if (!m_Parent->GetParentUser()) return; AMFArrayValue args; args.Insert("amount", std::to_string(value)); args.Insert("type", "armor"); GameMessages::SendUIMessageServerToSingleClient(m_Parent, m_Parent->GetParentUser()->GetSystemAddress(), "MaxPlayerBarUpdate", args); } Game::entityManager->SerializeEntity(m_Parent); } void DestroyableComponent::SetImagination(int32_t value) { m_DirtyHealth = true; auto* characterComponent = m_Parent->GetComponent(); if (characterComponent != nullptr) { characterComponent->TrackImaginationDelta(value - m_iImagination); } m_iImagination = value; auto* inventroyComponent = m_Parent->GetComponent(); if (m_iImagination == 0 && inventroyComponent != nullptr) { inventroyComponent->TriggerPassiveAbility(PassiveAbilityTrigger::AssemblyImagination); } } void DestroyableComponent::SetMaxImagination(float value, bool playAnim) { m_DirtyHealth = true; // Used for playAnim if opted in for. int32_t difference = static_cast(std::abs(m_fMaxImagination - value)); m_fMaxImagination = value; if (m_iImagination > m_fMaxImagination) { m_iImagination = m_fMaxImagination; } if (playAnim) { // Now update the player bar if (!m_Parent->GetParentUser()) return; AMFArrayValue args; args.Insert("amount", std::to_string(difference)); args.Insert("type", "imagination"); GameMessages::SendUIMessageServerToSingleClient(m_Parent, m_Parent->GetParentUser()->GetSystemAddress(), "MaxPlayerBarUpdate", args); } Game::entityManager->SerializeEntity(m_Parent); } void DestroyableComponent::SetDamageToAbsorb(int32_t value) { m_DirtyHealth = true; m_DamageToAbsorb = value; } void DestroyableComponent::SetDamageReduction(int32_t value) { m_DirtyHealth = true; m_DamageReduction = value; } void DestroyableComponent::SetIsImmune(bool value) { m_DirtyHealth = true; m_ImmuneToBasicAttackCount = value ? 1 : 0; } void DestroyableComponent::SetIsGMImmune(bool value) { m_DirtyHealth = true; m_IsGMImmune = value; } void DestroyableComponent::SetIsShielded(bool value) { m_DirtyHealth = true; m_IsShielded = value; } void DestroyableComponent::AddFaction(const int32_t factionID, const bool ignoreChecks) { // Ignore factionID -1 if (factionID == -1 && !ignoreChecks) { return; } m_FactionIDs.push_back(factionID); m_DirtyHealth = true; auto query = CDClientDatabase::CreatePreppedStmt( "SELECT enemyList FROM Factions WHERE faction = ?;"); query.bind(1, (int)factionID); auto result = query.execQuery(); if (result.eof()) return; if (result.fieldIsNull(0)) return; const auto* list_string = result.getStringField(0); std::stringstream ss(list_string); std::string token; while (std::getline(ss, token, ',')) { if (token.empty()) continue; auto id = std::stoi(token); auto exclude = std::find(m_FactionIDs.begin(), m_FactionIDs.end(), id) != m_FactionIDs.end(); if (!exclude) { exclude = std::find(m_EnemyFactionIDs.begin(), m_EnemyFactionIDs.end(), id) != m_EnemyFactionIDs.end(); } if (exclude) { continue; } AddEnemyFaction(id); } result.finalize(); } bool DestroyableComponent::IsEnemy(const Entity* other) const { const auto* otherDestroyableComponent = other->GetComponent(); if (otherDestroyableComponent != nullptr) { for (const auto enemyFaction : m_EnemyFactionIDs) { for (const auto otherFaction : otherDestroyableComponent->GetFactionIDs()) { if (enemyFaction == otherFaction) return true; } } } return false; } bool DestroyableComponent::IsFriend(const Entity* other) const { const auto* otherDestroyableComponent = other->GetComponent(); if (otherDestroyableComponent != nullptr) { for (const auto enemyFaction : m_EnemyFactionIDs) { for (const auto otherFaction : otherDestroyableComponent->GetFactionIDs()) { if (enemyFaction == otherFaction) return false; } } return true; } return false; } void DestroyableComponent::AddEnemyFaction(int32_t factionID) { m_EnemyFactionIDs.push_back(factionID); } void DestroyableComponent::SetIsSmashable(bool value) { m_DirtyHealth = true; m_IsSmashable = value; } void DestroyableComponent::SetAttacksToBlock(const uint32_t value) { m_AttacksToBlock = value; } bool DestroyableComponent::IsImmune() const { return m_IsGMImmune || m_ImmuneToBasicAttackCount > 0; } bool DestroyableComponent::IsKnockbackImmune() const { auto* characterComponent = m_Parent->GetComponent(); auto* inventoryComponent = m_Parent->GetComponent(); if (characterComponent != nullptr && inventoryComponent != nullptr && characterComponent->GetCurrentActivity() == eGameActivity::QUICKBUILDING) { const auto hasPassive = inventoryComponent->HasAnyPassive({ eItemSetPassiveAbilityID::EngineerRank2, eItemSetPassiveAbilityID::EngineerRank3, eItemSetPassiveAbilityID::SummonerRank2, eItemSetPassiveAbilityID::SummonerRank3, eItemSetPassiveAbilityID::InventorRank2, eItemSetPassiveAbilityID::InventorRank3, }, 5); if (hasPassive) { return true; } } return IsImmune() || m_IsShielded || m_AttacksToBlock > 0; } bool DestroyableComponent::HasFaction(int32_t factionID) const { return std::find(m_FactionIDs.begin(), m_FactionIDs.end(), factionID) != m_FactionIDs.end(); } LWOOBJID DestroyableComponent::GetKillerID() const { return m_KillerID; } Entity* DestroyableComponent::GetKiller() const { return Game::entityManager->GetEntity(m_KillerID); } bool DestroyableComponent::CheckValidity(const LWOOBJID target, const bool ignoreFactions, const bool targetEnemy, const bool targetFriend) const { auto* targetEntity = Game::entityManager->GetEntity(target); if (targetEntity == nullptr) { Game::logger->Log("DestroyableComponent", "Invalid entity for checking validity (%llu)!", target); return false; } auto* targetDestroyable = targetEntity->GetComponent(); if (targetDestroyable == nullptr) { return false; } auto* targetQuickbuild = targetEntity->GetComponent(); if (targetQuickbuild != nullptr) { const auto state = targetQuickbuild->GetState(); if (state != eRebuildState::COMPLETED) { return false; } } if (ignoreFactions) { return true; } // Get if the target entity is an enemy and friend bool isEnemy = IsEnemy(targetEntity); bool isFriend = IsFriend(targetEntity); // Return true if the target type matches what we are targeting return (isEnemy && targetEnemy) || (isFriend && targetFriend); } void DestroyableComponent::Heal(const uint32_t health) { auto current = static_cast(GetHealth()); const auto max = static_cast(GetMaxHealth()); current += health; current = std::min(current, max); SetHealth(current); Game::entityManager->SerializeEntity(m_Parent); } void DestroyableComponent::Imagine(const int32_t deltaImagination) { auto current = static_cast(GetImagination()); const auto max = static_cast(GetMaxImagination()); current += deltaImagination; current = std::min(current, max); if (current < 0) { current = 0; } SetImagination(current); Game::entityManager->SerializeEntity(m_Parent); } void DestroyableComponent::Repair(const uint32_t armor) { auto current = static_cast(GetArmor()); const auto max = static_cast(GetMaxArmor()); current += armor; current = std::min(current, max); SetArmor(current); Game::entityManager->SerializeEntity(m_Parent); } void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32_t skillID, bool echo) { if (GetHealth() <= 0) { return; } if (IsImmune()) { return; } if (m_AttacksToBlock > 0) { m_AttacksToBlock--; return; } // If this entity has damage reduction, reduce the damage to a minimum of 1 if (m_DamageReduction > 0 && damage > 0) { if (damage > m_DamageReduction) { damage -= m_DamageReduction; } else { damage = 1; } } const auto sourceDamage = damage; auto absorb = static_cast(GetDamageToAbsorb()); auto armor = static_cast(GetArmor()); auto health = static_cast(GetHealth()); const auto absorbDamage = std::min(damage, absorb); damage -= absorbDamage; absorb -= absorbDamage; const auto armorDamage = std::min(damage, armor); damage -= armorDamage; armor -= armorDamage; health -= std::min(damage, health); SetDamageToAbsorb(absorb); SetArmor(armor); SetHealth(health); SetIsShielded(absorb > 0); // Dismount on the possessable hit auto possessable = m_Parent->GetComponent(); if (possessable && possessable->GetDepossessOnHit()) { possessable->Dismount(); } // Dismount on the possessor hit auto possessor = m_Parent->GetComponent(); if (possessor) { auto possessableId = possessor->GetPossessable(); if (possessableId != LWOOBJID_EMPTY) { auto possessable = Game::entityManager->GetEntity(possessableId); if (possessable) { possessor->Dismount(possessable); } } } if (m_Parent->GetLOT() != 1) { echo = true; } if (echo) { Game::entityManager->SerializeEntity(m_Parent); } auto* attacker = Game::entityManager->GetEntity(source); m_Parent->OnHit(attacker); m_Parent->OnHitOrHealResult(attacker, sourceDamage); NotifySubscribers(attacker, sourceDamage); for (const auto& cb : m_OnHitCallbacks) { cb(attacker); } if (health != 0) { auto* combatComponent = m_Parent->GetComponent(); if (combatComponent != nullptr) { combatComponent->Taunt(source, sourceDamage * 10); // * 10 is arbatrary } return; } //check if hardcore mode is enabled if (Game::entityManager->GetHardcoreMode()) { DoHardcoreModeDrops(source); } Smash(source, eKillType::VIOLENT, u"", skillID); } void DestroyableComponent::Subscribe(LWOOBJID scriptObjId, CppScripts::Script* scriptToAdd) { m_SubscribedScripts.insert(std::make_pair(scriptObjId, scriptToAdd)); Game::logger->LogDebug("DestroyableComponent", "Added script %llu to entity %llu", scriptObjId, m_Parent->GetObjectID()); Game::logger->LogDebug("DestroyableComponent", "Number of subscribed scripts %i", m_SubscribedScripts.size()); } void DestroyableComponent::Unsubscribe(LWOOBJID scriptObjId) { auto foundScript = m_SubscribedScripts.find(scriptObjId); if (foundScript != m_SubscribedScripts.end()) { m_SubscribedScripts.erase(foundScript); Game::logger->LogDebug("DestroyableComponent", "Removed script %llu from entity %llu", scriptObjId, m_Parent->GetObjectID()); } else { Game::logger->LogDebug("DestroyableComponent", "Tried to remove a script for Entity %llu but script %llu didnt exist", m_Parent->GetObjectID(), scriptObjId); } Game::logger->LogDebug("DestroyableComponent", "Number of subscribed scripts %i", m_SubscribedScripts.size()); } void DestroyableComponent::NotifySubscribers(Entity* attacker, uint32_t damage) { for (auto script : m_SubscribedScripts) { script.second->NotifyHitOrHealResult(m_Parent, attacker, damage); } } void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType, const std::u16string& deathType, uint32_t skillID) { if (m_iHealth > 0) { SetArmor(0); SetHealth(0); Game::entityManager->SerializeEntity(m_Parent); } m_KillerID = source; auto* owner = Game::entityManager->GetEntity(source); if (owner != nullptr) { owner = owner->GetOwner(); // If the owner is overwritten, we collect that here auto* team = TeamManager::Instance()->GetTeam(owner->GetObjectID()); const auto isEnemy = m_Parent->GetComponent() != nullptr; auto* inventoryComponent = owner->GetComponent(); if (inventoryComponent != nullptr && isEnemy) { inventoryComponent->TriggerPassiveAbility(PassiveAbilityTrigger::EnemySmashed, m_Parent); } auto* missions = owner->GetComponent(); if (missions != nullptr) { if (team != nullptr) { for (const auto memberId : team->members) { auto* member = Game::entityManager->GetEntity(memberId); if (member == nullptr) continue; auto* memberMissions = member->GetComponent(); if (memberMissions == nullptr) continue; memberMissions->Progress(eMissionTaskType::SMASH, m_Parent->GetLOT()); memberMissions->Progress(eMissionTaskType::USE_SKILL, m_Parent->GetLOT(), skillID); } } else { missions->Progress(eMissionTaskType::SMASH, m_Parent->GetLOT()); missions->Progress(eMissionTaskType::USE_SKILL, m_Parent->GetLOT(), skillID); } } } const auto isPlayer = m_Parent->IsPlayer(); GameMessages::SendDie(m_Parent, source, source, true, killType, deathType, 0, 0, 0, isPlayer, false, 1); //NANI?! if (!isPlayer) { if (owner != nullptr) { auto* team = TeamManager::Instance()->GetTeam(owner->GetObjectID()); if (team != nullptr && m_Parent->GetComponent() != nullptr) { LWOOBJID specificOwner = LWOOBJID_EMPTY; auto* scriptedActivityComponent = m_Parent->GetComponent(); uint32_t teamSize = team->members.size(); uint32_t lootMatrixId = GetLootMatrixID(); if (scriptedActivityComponent) { lootMatrixId = scriptedActivityComponent->GetLootMatrixForTeamSize(teamSize); } if (team->lootOption == 0) { // Round robin specificOwner = TeamManager::Instance()->GetNextLootOwner(team); auto* member = Game::entityManager->GetEntity(specificOwner); if (member) LootGenerator::Instance().DropLoot(member, m_Parent, lootMatrixId, GetMinCoins(), GetMaxCoins()); } else { for (const auto memberId : team->members) { // Free for all auto* member = Game::entityManager->GetEntity(memberId); if (member == nullptr) continue; LootGenerator::Instance().DropLoot(member, m_Parent, lootMatrixId, GetMinCoins(), GetMaxCoins()); } } } else { // drop loot for non team user LootGenerator::Instance().DropLoot(owner, m_Parent, GetLootMatrixID(), GetMinCoins(), GetMaxCoins()); } } } else { //Check if this zone allows coin drops if (Game::zoneManager->GetPlayerLoseCoinOnDeath()) { auto* character = m_Parent->GetCharacter(); uint64_t coinsTotal = character->GetCoins(); const uint64_t minCoinsToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathMin; if (coinsTotal >= minCoinsToLose) { const uint64_t maxCoinsToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathMax; const float coinPercentageToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathPercent; uint64_t coinsToLose = std::max(static_cast(coinsTotal * coinPercentageToLose), minCoinsToLose); coinsToLose = std::min(maxCoinsToLose, coinsToLose); coinsTotal -= coinsToLose; LootGenerator::Instance().DropLoot(m_Parent, m_Parent, -1, coinsToLose, coinsToLose); character->SetCoins(coinsTotal, eLootSourceType::PICKUP); } } Entity* zoneControl = Game::entityManager->GetZoneControlEntity(); for (CppScripts::Script* script : CppScripts::GetEntityScripts(zoneControl)) { script->OnPlayerDied(zoneControl, m_Parent); } std::vector scriptedActs = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::SCRIPTED_ACTIVITY); for (Entity* scriptEntity : scriptedActs) { 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); } } } std::vector scripts = Game::entityManager->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); } void DestroyableComponent::SetFaction(int32_t factionID, bool ignoreChecks) { m_FactionIDs.clear(); m_EnemyFactionIDs.clear(); AddFaction(factionID, ignoreChecks); } void DestroyableComponent::SetStatusImmunity( const eStateChangeType state, const bool bImmuneToBasicAttack, const bool bImmuneToDamageOverTime, const bool bImmuneToKnockback, const bool bImmuneToInterrupt, const bool bImmuneToSpeed, const bool bImmuneToImaginationGain, const bool bImmuneToImaginationLoss, const bool bImmuneToQuickbuildInterrupt, const bool bImmuneToPullToPoint) { if (state == eStateChangeType::POP) { if (bImmuneToBasicAttack && m_ImmuneToBasicAttackCount > 0) m_ImmuneToBasicAttackCount -= 1; if (bImmuneToDamageOverTime && m_ImmuneToDamageOverTimeCount > 0) m_ImmuneToDamageOverTimeCount -= 1; if (bImmuneToKnockback && m_ImmuneToKnockbackCount > 0) m_ImmuneToKnockbackCount -= 1; if (bImmuneToInterrupt && m_ImmuneToInterruptCount > 0) m_ImmuneToInterruptCount -= 1; if (bImmuneToSpeed && m_ImmuneToSpeedCount > 0) m_ImmuneToSpeedCount -= 1; if (bImmuneToImaginationGain && m_ImmuneToImaginationGainCount > 0) m_ImmuneToImaginationGainCount -= 1; if (bImmuneToImaginationLoss && m_ImmuneToImaginationLossCount > 0) m_ImmuneToImaginationLossCount -= 1; if (bImmuneToQuickbuildInterrupt && m_ImmuneToQuickbuildInterruptCount > 0) m_ImmuneToQuickbuildInterruptCount -= 1; if (bImmuneToPullToPoint && m_ImmuneToPullToPointCount > 0) m_ImmuneToPullToPointCount -= 1; } else if (state == eStateChangeType::PUSH){ if (bImmuneToBasicAttack) m_ImmuneToBasicAttackCount += 1; if (bImmuneToDamageOverTime) m_ImmuneToDamageOverTimeCount += 1; if (bImmuneToKnockback) m_ImmuneToKnockbackCount += 1; if (bImmuneToInterrupt) m_ImmuneToInterruptCount += 1; if (bImmuneToSpeed) m_ImmuneToSpeedCount += 1; if (bImmuneToImaginationGain) m_ImmuneToImaginationGainCount += 1; if (bImmuneToImaginationLoss) m_ImmuneToImaginationLossCount += 1; if (bImmuneToQuickbuildInterrupt) m_ImmuneToQuickbuildInterruptCount += 1; if (bImmuneToPullToPoint) m_ImmuneToPullToPointCount += 1; } GameMessages::SendSetStatusImmunity( m_Parent->GetObjectID(), state, m_Parent->GetSystemAddress(), bImmuneToBasicAttack, bImmuneToDamageOverTime, bImmuneToKnockback, bImmuneToInterrupt, bImmuneToSpeed, bImmuneToImaginationGain, bImmuneToImaginationLoss, bImmuneToQuickbuildInterrupt, bImmuneToPullToPoint ); } void DestroyableComponent::FixStats() { auto* entity = GetParent(); if (entity == nullptr) return; // Reset skill component and buff component auto* skillComponent = entity->GetComponent(); auto* buffComponent = entity->GetComponent(); auto* missionComponent = entity->GetComponent(); auto* inventoryComponent = entity->GetComponent(); auto* destroyableComponent = entity->GetComponent(); // If any of the components are nullptr, return if (skillComponent == nullptr || buffComponent == nullptr || missionComponent == nullptr || inventoryComponent == nullptr || destroyableComponent == nullptr) { return; } // Save the current stats int32_t currentHealth = destroyableComponent->GetHealth(); int32_t currentArmor = destroyableComponent->GetArmor(); int32_t currentImagination = destroyableComponent->GetImagination(); // Unequip all items auto equipped = inventoryComponent->GetEquippedItems(); for (auto& equippedItem : equipped) { // Get the item with the item ID auto* item = inventoryComponent->FindItemById(equippedItem.second.id); if (item == nullptr) { continue; } // Unequip the item item->UnEquip(); } // Base stats int32_t maxHealth = 4; int32_t maxArmor = 0; int32_t maxImagination = 0; // Go through all completed missions and add the reward stats for (auto& pair : missionComponent->GetMissions()) { auto* mission = pair.second; if (!mission->IsComplete()) { continue; } // Add the stats const auto& info = mission->GetClientInfo(); maxHealth += info.reward_maxhealth; maxImagination += info.reward_maximagination; } // Set the base stats destroyableComponent->SetMaxHealth(maxHealth); destroyableComponent->SetMaxArmor(maxArmor); destroyableComponent->SetMaxImagination(maxImagination); // Re-apply all buffs buffComponent->ReApplyBuffs(); // Requip all items for (auto& equippedItem : equipped) { // Get the item with the item ID auto* item = inventoryComponent->FindItemById(equippedItem.second.id); if (item == nullptr) { continue; } // Equip the item item->Equip(); } destroyableComponent->ComputeBaseStats(); // Fetch correct max stats after everything is done maxHealth = destroyableComponent->GetMaxHealth(); maxArmor = destroyableComponent->GetMaxArmor(); maxImagination = destroyableComponent->GetMaxImagination(); // If any of the current stats are more than their max, set them to the max if (currentHealth > maxHealth) currentHealth = maxHealth; if (currentArmor > maxArmor) currentArmor = maxArmor; if (currentImagination > maxImagination) currentImagination = maxImagination; // Restore current stats destroyableComponent->SetHealth(currentHealth); destroyableComponent->SetArmor(currentArmor); destroyableComponent->SetImagination(currentImagination); // Serialize the entity Game::entityManager->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); Game::entityManager->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); } void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source){ //check if this is a player: if (m_Parent->IsPlayer()) { //remove hardcore_lose_uscore_on_death_percent from the player's uscore: auto* character = m_Parent->GetComponent(); auto uscore = character->GetUScore(); auto uscoreToLose = uscore * (Game::entityManager->GetHardcoreLoseUscoreOnDeathPercent() / 100); character->SetUScore(uscore - uscoreToLose); GameMessages::SendModifyLEGOScore(m_Parent, m_Parent->GetSystemAddress(), -uscoreToLose, eLootSourceType::MISSION); if (Game::entityManager->GetHardcoreDropinventoryOnDeath()) { //drop all items from inventory: auto* inventory = m_Parent->GetComponent(); if (inventory) { //get the items inventory: auto items = inventory->GetInventory(eInventoryType::ITEMS); if (items){ auto itemMap = items->GetItems(); if (!itemMap.empty()){ for (const auto& item : itemMap) { //drop the item: if (!item.second) continue; // don't drop the thinkng cap if (item.second->GetLot() == 6086) continue; GameMessages::SendDropClientLoot(m_Parent, source, item.second->GetLot(), 0, m_Parent->GetPosition(), item.second->GetCount()); item.second->SetCount(0, false, false); } Game::entityManager->SerializeEntity(m_Parent); } } } } //get character: auto* chars = m_Parent->GetCharacter(); if (chars) { auto coins = chars->GetCoins(); //lose all coins: chars->SetCoins(0, eLootSourceType::NONE); //drop all coins: GameMessages::SendDropClientLoot(m_Parent, source, LOT_NULL, coins, m_Parent->GetPosition()); } // Reload the player since we can't normally reduce uscore from the server and we want the UI to update // do this last so we don't get killed.... again Game::entityManager->DestructEntity(m_Parent); Game::entityManager->ConstructEntity(m_Parent); return; } //award the player some u-score: auto* player = Game::entityManager->GetEntity(source); if (player && player->IsPlayer()) { auto* playerStats = player->GetComponent(); if (playerStats) { //get the maximum health from this enemy: auto maxHealth = GetMaxHealth(); int uscore = maxHealth * Game::entityManager->GetHardcoreUscoreEnemiesMultiplier(); playerStats->SetUScore(playerStats->GetUScore() + uscore); GameMessages::SendModifyLEGOScore(player, player->GetSystemAddress(), uscore, eLootSourceType::MISSION); Game::entityManager->SerializeEntity(m_Parent); } } }