mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Live filled Die's direction_relative_angle_xz, _angle_y and _force on 11,606 of 19,428 deaths. The values are the dir_angle_xz, dir_angle_y (degrees, sent as radians) and dir_force parameters of the BasicAttack that dealt the killing blow: per killing skill the triple is constant (skill 1210: -25 degrees, force 12 on 540 of 549 kills; skill 10: force 7 on 1,830 of 1,835), and deaths with no killer (5,948) never have one. BasicAttack is the only behavior with these parameters apart from one Grab. The client passes them to PerformSmashableDeath, which throws the smashable's pieces that way; DLU sent 0. The radians are rounded the way live did (-25 is 0xbedf66f3, 20 is 0x3eb2b8c3). Not covered: use_caster_velocity (one behavior, racing). Check: smash crates and enemies with different weapons; the pieces fly off to the side/up per weapon instead of straight out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1285 lines
45 KiB
C++
1285 lines
45 KiB
C++
#include "DestroyableComponent.h"
|
|
#include "CombatMessages.h"
|
|
#include "LevelProgressionComponent.h"
|
|
#include "EconomyLedger.h"
|
|
#include "BitStream.h"
|
|
#include "Logger.h"
|
|
#include "Game.h"
|
|
#include "dConfig.h"
|
|
#include "CDLootMatrixTable.h"
|
|
#include "CDLootTableTable.h"
|
|
#include "CDRarityTableTable.h"
|
|
|
|
#include "Amf3.h"
|
|
#include "AmfSerialize.h"
|
|
#include "GameMessages.h"
|
|
#include "EffectsMessages.h"
|
|
#include "PlayerMessages.h"
|
|
#include "User.h"
|
|
#include "CDClientManager.h"
|
|
#include "CDDestructibleComponentTable.h"
|
|
#include "EntityManager.h"
|
|
#include "QuickBuildComponent.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 "Amf3.h"
|
|
|
|
#include <sstream>
|
|
#include <algorithm>
|
|
|
|
#include "MissionComponent.h"
|
|
#include "CharacterComponent.h"
|
|
#include "PossessableComponent.h"
|
|
#include "PossessorComponent.h"
|
|
#include "ModelComponent.h"
|
|
#include "InventoryComponent.h"
|
|
#include "dZoneManager.h"
|
|
#include "WorldConfig.h"
|
|
#include "eMissionTaskType.h"
|
|
#include "eStateChangeType.h"
|
|
#include "eGameActivity.h"
|
|
#include <ranges>
|
|
|
|
#include "CDComponentsRegistryTable.h"
|
|
#include "InventoryMessages.h"
|
|
|
|
Implementation<bool, const Entity*> DestroyableComponent::IsEnemyImplentation;
|
|
Implementation<bool, const Entity*> DestroyableComponent::IsFriendImplentation;
|
|
|
|
DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
|
|
m_iArmor = 0;
|
|
m_fMaxArmor = 0.0f;
|
|
m_iImagination = 0;
|
|
m_fMaxImagination = 0.0f;
|
|
m_FactionIDs = std::vector<int32_t>();
|
|
m_EnemyFactionIDs = std::vector<int32_t>();
|
|
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_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_DeathBehavior = -1;
|
|
|
|
m_DamageCooldownTimer = 0.0f;
|
|
|
|
RegisterMsg(&DestroyableComponent::OnGetObjectReportInfo);
|
|
RegisterMsg(&DestroyableComponent::OnSetFaction);
|
|
RegisterMsg(&DestroyableComponent::OnIsDead);
|
|
}
|
|
|
|
DestroyableComponent::~DestroyableComponent() {
|
|
}
|
|
|
|
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::Update(float deltaTime) {
|
|
m_DamageCooldownTimer -= deltaTime;
|
|
}
|
|
|
|
void DestroyableComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
|
|
auto* dest = doc.FirstChildElement("obj")->FirstChildElement("dest");
|
|
if (!dest) {
|
|
LOG("Failed to find dest tag!");
|
|
return;
|
|
}
|
|
|
|
auto* buffComponent = m_Parent->GetComponent<BuffComponent>();
|
|
|
|
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) {
|
|
LOG("Failed to find dest tag!");
|
|
return;
|
|
}
|
|
|
|
auto* buffComponent = m_Parent->GetComponent<BuffComponent>();
|
|
|
|
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;
|
|
|
|
// Health lost counts as damage taken; healing counts where it is applied (Heal)
|
|
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr && value < m_iHealth) {
|
|
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<int32_t>(std::abs(m_fMaxHealth - value));
|
|
m_fMaxHealth = value;
|
|
|
|
if (m_iHealth > m_fMaxHealth) {
|
|
m_iHealth = m_fMaxHealth;
|
|
}
|
|
|
|
if (playAnim) {
|
|
// Now update the player bar
|
|
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
|
|
if (!characterComponent) return;
|
|
|
|
AMFArrayValue args;
|
|
args.Insert("amount", std::to_string(difference));
|
|
args.Insert("type", "health");
|
|
|
|
GameMessages::UIMessageServerToSingleClient uiMessage;
|
|
uiMessage.target = m_Parent->GetObjectID();
|
|
uiMessage.strMessageName = "MaxPlayerBarUpdate";
|
|
uiMessage.args = std::move(args);
|
|
uiMessage.SendToClient(characterComponent->GetSystemAddress());
|
|
}
|
|
|
|
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;
|
|
|
|
m_iArmor = value;
|
|
|
|
auto* inventroyComponent = m_Parent->GetComponent<InventoryComponent>();
|
|
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
|
|
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
|
|
if (!characterComponent) return;
|
|
|
|
AMFArrayValue args;
|
|
args.Insert("amount", std::to_string(value));
|
|
args.Insert("type", "armor");
|
|
|
|
GameMessages::UIMessageServerToSingleClient uiMessage;
|
|
uiMessage.target = m_Parent->GetObjectID();
|
|
uiMessage.strMessageName = "MaxPlayerBarUpdate";
|
|
uiMessage.args = std::move(args);
|
|
uiMessage.SendToClient(characterComponent->GetSystemAddress());
|
|
}
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
void DestroyableComponent::SetImagination(int32_t value) {
|
|
m_DirtyHealth = true;
|
|
|
|
// Imagination spent counts as used; restoring counts where it is applied (Imagine)
|
|
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr && value < m_iImagination) {
|
|
characterComponent->TrackImaginationDelta(value - m_iImagination);
|
|
}
|
|
|
|
m_iImagination = value;
|
|
|
|
auto* inventroyComponent = m_Parent->GetComponent<InventoryComponent>();
|
|
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<int32_t>(std::abs(m_fMaxImagination - value));
|
|
m_fMaxImagination = value;
|
|
|
|
if (m_iImagination > m_fMaxImagination) {
|
|
m_iImagination = m_fMaxImagination;
|
|
}
|
|
|
|
if (playAnim) {
|
|
// Now update the player bar
|
|
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
|
|
if (!characterComponent) return;
|
|
|
|
AMFArrayValue args;
|
|
args.Insert("amount", std::to_string(difference));
|
|
args.Insert("type", "imagination");
|
|
|
|
GameMessages::UIMessageServerToSingleClient uiMessage;
|
|
uiMessage.target = m_Parent->GetObjectID();
|
|
uiMessage.strMessageName = "MaxPlayerBarUpdate";
|
|
uiMessage.args = std::move(args);
|
|
uiMessage.SendToClient(characterComponent->GetSystemAddress());
|
|
}
|
|
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;
|
|
|
|
// if we already have that faction, don't add it again
|
|
if (std::find(m_FactionIDs.begin(), m_FactionIDs.end(), factionID) != m_FactionIDs.end()) return;
|
|
|
|
m_FactionIDs.push_back(factionID);
|
|
m_DirtyHealth = true;
|
|
|
|
auto query = CDClientDatabase::CreatePreppedStmt(
|
|
"SELECT enemyList FROM Factions WHERE faction = ?;");
|
|
query.bind(1, static_cast<int>(factionID));
|
|
|
|
auto result = query.execQuery();
|
|
|
|
if (result.eof()) return;
|
|
|
|
if (result.fieldIsNull("enemyList")) return;
|
|
|
|
const auto* list_string = result.getStringField("enemyList");
|
|
|
|
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 {
|
|
if (IsEnemyImplentation.ExecuteWithDefault(other, false)) return true;
|
|
if (m_Parent->IsPlayer() && other->IsPlayer()) {
|
|
auto* thisCharacterComponent = m_Parent->GetComponent<CharacterComponent>();
|
|
if (!thisCharacterComponent) return false;
|
|
auto* otherCharacterComponent = other->GetComponent<CharacterComponent>();
|
|
if (!otherCharacterComponent) return false;
|
|
if (thisCharacterComponent->GetPvpEnabled() && otherCharacterComponent->GetPvpEnabled()) return true;
|
|
return false;
|
|
}
|
|
const auto* otherDestroyableComponent = other->GetComponent<DestroyableComponent>();
|
|
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 {
|
|
if (IsFriendImplentation.ExecuteWithDefault(other, false)) return true;
|
|
const auto* otherDestroyableComponent = other->GetComponent<DestroyableComponent>();
|
|
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::IsCooldownImmune() const {
|
|
return m_DamageCooldownTimer > 0.0f;
|
|
}
|
|
|
|
bool DestroyableComponent::IsKnockbackImmune() const {
|
|
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
|
|
|
|
// Quickbuild interrupt immunity comes from Immunity behaviors, e.g. the 5 item Assembly rank 2 and 3 set skill (584)
|
|
if (characterComponent != nullptr && characterComponent->GetCurrentActivity() == eGameActivity::QUICKBUILDING && GetImmuneToQuickbuildInterrupt()) {
|
|
return true;
|
|
}
|
|
|
|
return IsImmune() || m_IsShielded || m_AttacksToBlock > 0;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void DestroyableComponent::Heal(const uint32_t health) {
|
|
auto current = static_cast<uint32_t>(GetHealth());
|
|
const auto max = static_cast<uint32_t>(GetMaxHealth());
|
|
|
|
current += health;
|
|
|
|
current = std::min(current, max);
|
|
|
|
const auto before = GetHealth();
|
|
SetHealth(current);
|
|
|
|
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
|
|
if (characterComponent) characterComponent->TrackHealthDelta(GetHealth() - before);
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
|
|
void DestroyableComponent::Imagine(const int32_t deltaImagination) {
|
|
auto current = static_cast<int32_t>(GetImagination());
|
|
const auto max = static_cast<int32_t>(GetMaxImagination());
|
|
|
|
current += deltaImagination;
|
|
|
|
current = std::min(current, max);
|
|
|
|
if (current < 0) {
|
|
current = 0;
|
|
}
|
|
|
|
const auto before = GetImagination();
|
|
SetImagination(current);
|
|
|
|
// What was restored, 0 included (live sent TotalImaginationRestored 0 for a power-up picked up when full)
|
|
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
|
|
if (characterComponent && deltaImagination > 0) characterComponent->TrackImaginationDelta(GetImagination() - before);
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
|
|
void DestroyableComponent::Repair(const uint32_t armor) {
|
|
auto current = static_cast<uint32_t>(GetArmor());
|
|
const auto max = static_cast<uint32_t>(GetMaxArmor());
|
|
|
|
current += armor;
|
|
|
|
current = std::min(current, max);
|
|
|
|
const auto before = GetArmor();
|
|
SetArmor(current);
|
|
|
|
// What was repaired, 0 included (live sent TotalArmorRepaired 0 when the armor was already full)
|
|
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
|
|
if (characterComponent) characterComponent->TrackArmorDelta(GetArmor() - before);
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
|
|
float DeathDirection::DegreesToRadians(const float degrees) {
|
|
constexpr double PI_AS_FLOAT = static_cast<double>(3.14159265358979323846f);
|
|
return static_cast<float>(static_cast<double>(degrees) * PI_AS_FLOAT / 180.0);
|
|
}
|
|
|
|
void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32_t skillID, bool echo, const DeathDirection& deathDirection) {
|
|
if (GetHealth() <= 0) {
|
|
return;
|
|
}
|
|
|
|
if (IsImmune() || IsCooldownImmune()) {
|
|
LOG_DEBUG("Target targetEntity %llu is immune!", m_Parent->GetObjectID());
|
|
return;
|
|
}
|
|
|
|
if (m_AttacksToBlock > 0) {
|
|
m_AttacksToBlock--;
|
|
|
|
return;
|
|
}
|
|
|
|
// Client does the same check, so we're doing it too
|
|
auto* const modelComponent = m_Parent->GetComponent<ModelComponent>();
|
|
if (modelComponent) {
|
|
modelComponent->OnHit();
|
|
// Don't actually deal the damage so the model doesn't die
|
|
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<uint32_t>(GetDamageToAbsorb());
|
|
auto armor = static_cast<uint32_t>(GetArmor());
|
|
auto health = static_cast<uint32_t>(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<PossessableComponent>();
|
|
if (possessable && possessable->GetDepossessOnHit()) {
|
|
possessable->Dismount();
|
|
}
|
|
|
|
// Dismount on the possessor hit
|
|
auto possessor = m_Parent->GetComponent<PossessorComponent>();
|
|
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<BaseCombatAIComponent>();
|
|
|
|
if (combatComponent != nullptr) {
|
|
combatComponent->Taunt(source, sourceDamage * 10); // * 10 is arbatrary
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
Smash(source, eKillType::VIOLENT, u"", skillID, deathDirection);
|
|
}
|
|
|
|
void DestroyableComponent::Subscribe(LWOOBJID scriptObjId, CppScripts::Script* scriptToAdd) {
|
|
m_SubscribedScripts.insert(std::make_pair(scriptObjId, scriptToAdd));
|
|
LOG_DEBUG("Added script %llu to entity %llu", scriptObjId, m_Parent->GetObjectID());
|
|
LOG_DEBUG("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);
|
|
LOG_DEBUG("Removed script %llu from entity %llu", scriptObjId, m_Parent->GetObjectID());
|
|
} else {
|
|
LOG_DEBUG("Tried to remove a script for Entity %llu but script %llu didnt exist", m_Parent->GetObjectID(), scriptObjId);
|
|
}
|
|
LOG_DEBUG("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, const DeathDirection& deathDirection) {
|
|
if (m_IsDead) return;
|
|
|
|
//check if hardcore mode is enabled
|
|
if (Game::entityManager->GetHardcoreMode()) {
|
|
DoHardcoreModeDrops(source);
|
|
}
|
|
|
|
if (m_iHealth > 0) {
|
|
SetArmor(0);
|
|
SetHealth(0);
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
m_IsDead = true;
|
|
m_KillerID = source;
|
|
|
|
auto* owner = Game::entityManager->GetEntity(source);
|
|
|
|
// Where players die and what killed them (the killer itself, not whoever owns it; 0 when it is already gone)
|
|
if (m_Parent->IsPlayer()) {
|
|
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::PLAYER_DEATHS, owner ? owner->GetLOT() : 0, m_Parent->GetPosition(), 1, m_Parent);
|
|
}
|
|
|
|
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<BaseCombatAIComponent>() != nullptr;
|
|
|
|
auto* inventoryComponent = owner->GetComponent<InventoryComponent>();
|
|
|
|
if (inventoryComponent != nullptr && isEnemy) {
|
|
inventoryComponent->TriggerPassiveAbility(PassiveAbilityTrigger::EnemySmashed, m_Parent);
|
|
}
|
|
|
|
if (isEnemy && owner->IsPlayer()) {
|
|
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::ENEMY_KILLS, m_Parent->GetLOT(), m_Parent->GetPosition(), 1, owner);
|
|
} else if (owner->IsPlayer() && !m_Parent->IsPlayer()) {
|
|
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::SMASHABLES_SMASHED, m_Parent->GetLOT(), m_Parent->GetPosition(), 1, owner);
|
|
}
|
|
|
|
auto* missions = owner->GetComponent<MissionComponent>();
|
|
|
|
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<MissionComponent>();
|
|
|
|
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::Die die;
|
|
die.target = m_Parent->GetObjectID();
|
|
// Live never set client_death, players included (19,428 Die in the live captures). The client only uses it to
|
|
// match a death it already predicted locally (LWODestroyableComponent::msgDie).
|
|
die.bClientDeath = false;
|
|
die.bSpawnLoot = true;
|
|
die.deathType = deathType;
|
|
die.directionRelative_AngleXZ = deathDirection.angleXZ;
|
|
die.directionRelative_AngleY = deathDirection.angleY;
|
|
die.directionRelative_Force = deathDirection.force;
|
|
die.killType = killType;
|
|
die.killerID = source;
|
|
die.lootOwnerID = source;
|
|
die.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
// Live told the killer right after Die, before the loot: EnemiesSmashed for an NPC, SmashablesSmashed for another
|
|
// smashable. Racing counts its own (RacingSmashablesSmashed).
|
|
if (owner && owner != m_Parent) {
|
|
auto* killerCharacter = owner->GetComponent<CharacterComponent>();
|
|
if (killerCharacter && !killerCharacter->GetIsRacing()) {
|
|
if (m_IsNPC) killerCharacter->UpdatePlayerStatistic(EnemiesSmashed);
|
|
else if (m_IsSmashable) killerCharacter->UpdatePlayerStatistic(SmashablesSmashed);
|
|
}
|
|
}
|
|
|
|
//NANI?!
|
|
if (!isPlayer) {
|
|
if (owner != nullptr) {
|
|
Loot::DropLoot(owner, m_Parent->GetObjectID(), GetLootMatrixID(), GetMinCoins(), GetMaxCoins());
|
|
}
|
|
} else {
|
|
//Check if this zone allows coin drops
|
|
if (Game::zoneManager->GetPlayerLoseCoinOnDeath() && !Game::entityManager->GetHardcoreMode()) {
|
|
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<uint64_t>(coinsTotal * coinPercentageToLose), minCoinsToLose);
|
|
coinsToLose = std::min(maxCoinsToLose, coinsToLose);
|
|
|
|
coinsTotal -= coinsToLose;
|
|
|
|
GameMessages::DropClientLoot lootMsg{};
|
|
lootMsg.target = m_Parent->GetObjectID();
|
|
lootMsg.ownerID = m_Parent->GetObjectID();
|
|
lootMsg.currency = coinsToLose;
|
|
lootMsg.spawnPos = m_Parent->GetPosition();
|
|
lootMsg.sourceID = source;
|
|
lootMsg.item = LOT_NULL;
|
|
GameMessages::DeliverLocally(lootMsg);
|
|
character->SetCoins(coinsTotal, eLootSourceType::DELETION);
|
|
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::PLAYER_COIN_DROPS, 0, m_Parent->GetPosition(), static_cast<int64_t>(coinsToLose), m_Parent);
|
|
}
|
|
}
|
|
|
|
Entity* zoneControl = Game::entityManager->GetZoneControlEntity();
|
|
if (zoneControl) zoneControl->GetScript()->OnPlayerDied(zoneControl, m_Parent);
|
|
|
|
std::vector<Entity*> scriptedActs = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::SCRIPTED_ACTIVITY);
|
|
for (Entity* scriptEntity : scriptedActs) {
|
|
if (!zoneControl || scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds
|
|
scriptEntity->GetScript()->OnPlayerDied(scriptEntity, m_Parent);
|
|
}
|
|
}
|
|
}
|
|
|
|
m_Parent->Kill(owner, killType);
|
|
}
|
|
|
|
std::optional<std::vector<int32_t>> DestroyableComponent::GetLevelFactions(const Entity& entity) {
|
|
if (!entity.HasVar(u"set_faction")) return std::nullopt;
|
|
// override_faction defaults to true when set_faction is there; false keeps the template's factions
|
|
if (entity.HasVar(u"override_faction") && !entity.GetVar<bool>(u"override_faction")) return std::nullopt;
|
|
|
|
// Many values have a trailing space ("6 ")
|
|
std::vector<int32_t> factions;
|
|
for (const auto& semicolonSplit : GeneralUtils::SplitString(entity.GetVarAsString(u"set_faction"), ';')) {
|
|
for (const auto& faction : GeneralUtils::SplitString(semicolonSplit, ' ')) {
|
|
const auto factionToAdd = GeneralUtils::TryParse<int32_t>(faction);
|
|
if (factionToAdd) factions.push_back(factionToAdd.value());
|
|
}
|
|
}
|
|
return factions;
|
|
}
|
|
|
|
std::optional<int32_t> DestroyableComponent::GetLevelLootMatrix(const Entity& entity) {
|
|
if (entity.HasVar(u"smashable_loot_matrix_set") && !entity.GetVar<bool>(u"smashable_loot_matrix_set")) return std::nullopt;
|
|
if (!entity.HasVar(u"smashable_loot_matrix")) return std::nullopt;
|
|
|
|
const auto lootMatrix = entity.GetVar<int32_t>(u"smashable_loot_matrix");
|
|
if (lootMatrix == -1) return std::nullopt;
|
|
return lootMatrix;
|
|
}
|
|
|
|
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::SetStatusImmunity statusImmunity;
|
|
statusImmunity.target = m_Parent->GetObjectID();
|
|
statusImmunity.StateChangeType = state;
|
|
statusImmunity.bImmuneToBasicAttack = bImmuneToBasicAttack;
|
|
statusImmunity.bImmuneToDOT = bImmuneToDamageOverTime;
|
|
statusImmunity.bImmuneToKnockback = bImmuneToKnockback;
|
|
statusImmunity.bImmuneToInterrupt = bImmuneToInterrupt;
|
|
statusImmunity.bImmuneToSpeed = bImmuneToSpeed;
|
|
statusImmunity.bImmuneToImaginationGain = bImmuneToImaginationGain;
|
|
statusImmunity.bImmuneToImaginationLoss = bImmuneToImaginationLoss;
|
|
statusImmunity.bImmuneToQuickbuildInterrupt = bImmuneToQuickbuildInterrupt;
|
|
statusImmunity.bImmuneToPullToPoint = bImmuneToPullToPoint;
|
|
statusImmunity.Send(m_Parent->GetSystemAddress());
|
|
}
|
|
|
|
void DestroyableComponent::FixStats() {
|
|
auto* entity = GetParent();
|
|
|
|
if (entity == nullptr) return;
|
|
|
|
// Reset skill component and buff component
|
|
auto* skillComponent = entity->GetComponent<SkillComponent>();
|
|
auto* buffComponent = entity->GetComponent<BuffComponent>();
|
|
auto* missionComponent = entity->GetComponent<MissionComponent>();
|
|
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
|
|
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
|
|
|
|
// 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
|
|
const auto equipped = inventoryComponent->GetEquippedItems();
|
|
|
|
for (const 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 (const 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();
|
|
}
|
|
|
|
// 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::AddOnHitCallback(const std::function<void(Entity*)>& 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<CharacterComponent>();
|
|
if (!character) return;
|
|
|
|
auto uscore = character->GetUScore();
|
|
|
|
auto uscoreToLose = static_cast<uint64_t>(uscore * (Game::entityManager->GetHardcoreLoseUscoreOnDeathPercent() / 100.0f));
|
|
LOG("Player %llu has lost %llu uscore!", m_Parent->GetObjectID(), uscoreToLose);
|
|
character->SetUScore(uscore - uscoreToLose, EconomyLedger::HARDCORE_DEATH_SOURCE);
|
|
|
|
GameMessages::ModifyLEGOScore modifyScore;
|
|
modifyScore.target = m_Parent->GetObjectID();
|
|
modifyScore.score = -uscoreToLose;
|
|
modifyScore.sourceType = eLootSourceType::MISSION;
|
|
modifyScore.SendToClient(m_Parent->GetSystemAddress());
|
|
|
|
if (Game::entityManager->GetHardcoreDropinventoryOnDeath()) {
|
|
//drop all items from inventory:
|
|
auto* inventory = m_Parent->GetComponent<InventoryComponent>();
|
|
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 | std::views::values) {
|
|
// Don't drop excluded items or null ones
|
|
if (!item || Game::entityManager->GetHardcoreExcludedItemDrops().contains(item->GetLot())) continue;
|
|
GameMessages::DropClientLoot lootMsg{};
|
|
lootMsg.target = m_Parent->GetObjectID();
|
|
lootMsg.ownerID = m_Parent->GetObjectID();
|
|
lootMsg.sourceID = m_Parent->GetObjectID();
|
|
lootMsg.item = item->GetLot();
|
|
lootMsg.count = 1;
|
|
lootMsg.spawnPos = m_Parent->GetPosition();
|
|
for (int i = 0; i < item->GetCount(); i++) Loot::DropItem(*m_Parent, lootMsg);
|
|
item->SetCount(0, false, false);
|
|
}
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//get character:
|
|
auto* chars = m_Parent->GetCharacter();
|
|
if (chars) {
|
|
auto oldCoins = chars->GetCoins();
|
|
// Floor this so there arent coins generated from rounding
|
|
auto coins = static_cast<uint64_t>(oldCoins * Game::entityManager->GetHardcoreCoinKeep());
|
|
auto coinsToDrop = oldCoins - coins;
|
|
LOG("Player had %llu coins, will lose %i coins to have %i", oldCoins, coinsToDrop, coins);
|
|
|
|
//lose all coins:
|
|
chars->SetCoins(coins, eLootSourceType::NONE);
|
|
if (coinsToDrop > 0) EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::PLAYER_COIN_DROPS, 0, m_Parent->GetPosition(), static_cast<int64_t>(coinsToDrop), m_Parent);
|
|
|
|
//drop all coins:
|
|
constexpr auto MAX_TO_DROP_PER_GM = 100'000;
|
|
GameMessages::DropClientLoot lootMsg{};
|
|
lootMsg.target = m_Parent->GetObjectID();
|
|
lootMsg.ownerID = m_Parent->GetObjectID();
|
|
lootMsg.spawnPos = m_Parent->GetPosition();
|
|
lootMsg.sourceID = source;
|
|
lootMsg.item = LOT_NULL;
|
|
GameMessages::DeliverLocally(lootMsg);
|
|
lootMsg.Send(m_Parent->GetSystemAddress());
|
|
while (coinsToDrop > MAX_TO_DROP_PER_GM) {
|
|
LOG("Dropping 100,000, %llu left", coinsToDrop);
|
|
lootMsg.currency = 100'000;
|
|
GameMessages::DeliverLocally(lootMsg);
|
|
lootMsg.Send(m_Parent->GetSystemAddress());
|
|
coinsToDrop -= 100'000;
|
|
}
|
|
lootMsg.currency = coinsToDrop;
|
|
GameMessages::DeliverLocally(lootMsg);
|
|
lootMsg.Send(m_Parent->GetSystemAddress());
|
|
}
|
|
return;
|
|
}
|
|
|
|
//award the player some u-score:
|
|
auto* player = Game::entityManager->GetEntity(source);
|
|
if (player && player->IsPlayer()) {
|
|
const auto lot = m_Parent->GetLOT();
|
|
auto* playerStats = player->GetComponent<CharacterComponent>();
|
|
if (playerStats && GetMaxHealth() > 0 && !Game::entityManager->GetHardcoreUscoreExcludedEnemies().contains(lot)) {
|
|
//get the maximum health from this enemy:
|
|
auto maxHealth = GetMaxHealth();
|
|
const auto uscoreMultiplier = Game::entityManager->GetHardcoreUscoreEnemiesMultiplier();
|
|
const bool isUscoreReducedLot =
|
|
Game::entityManager->GetHardcoreUscoreReducedLots().contains(lot) ||
|
|
Game::entityManager->GetHardcoreUscoreReduced();
|
|
const auto uscoreReduction = isUscoreReducedLot ? Game::entityManager->GetHardcoreUscoreReduction() : 1.0f;
|
|
|
|
int uscore = maxHealth * Game::entityManager->GetHardcoreUscoreEnemiesMultiplier() * uscoreReduction;
|
|
LOG("Rewarding player %llu with %i uscore for killing enemy %i", player->GetObjectID(), uscore, lot);
|
|
playerStats->SetUScore(playerStats->GetUScore() + uscore, EconomyLedger::HARDCORE_KILL_SOURCE);
|
|
GameMessages::ModifyLEGOScore modifyScore;
|
|
modifyScore.target = player->GetObjectID();
|
|
modifyScore.score = uscore;
|
|
modifyScore.sourceType = eLootSourceType::MISSION;
|
|
modifyScore.SendToClient(player->GetSystemAddress());
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
|
auto& destroyableInfo = reportInfo.info->PushDebug("Destroyable");
|
|
destroyableInfo.PushDebug<AMFIntValue>("DestructibleComponent DB Table Template ID") = m_ComponentID;
|
|
|
|
if (m_CurrencyIndex == -1) {
|
|
destroyableInfo.PushDebug<AMFBoolValue>("Has Loot Currency") = false;
|
|
} else {
|
|
destroyableInfo.PushDebug<AMFIntValue>("Loot Currency ID") = m_CurrencyIndex;
|
|
auto& detailedCoinInfo = destroyableInfo.PushDebug("Coin Info");
|
|
detailedCoinInfo.PushDebug<AMFIntValue>("Min Coins") = m_MinCoins;
|
|
detailedCoinInfo.PushDebug<AMFIntValue>("Max Coins") = m_MaxCoins;
|
|
}
|
|
|
|
if (m_LootMatrixID == -1 || m_LootMatrixID == 0) {
|
|
destroyableInfo.PushDebug<AMFBoolValue>("Has Loot Matrix") = false;
|
|
} else {
|
|
auto& lootInfo = destroyableInfo.PushDebug("Loot Info");
|
|
lootInfo.PushDebug<AMFIntValue>("Loot Matrix ID") = m_LootMatrixID;
|
|
auto* const componentsRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
auto* const itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
|
|
auto* const lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
|
|
auto* const lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
|
|
auto* const rarityTableTable = CDClientManager::GetTable<CDRarityTableTable>();
|
|
|
|
const auto& matrix = lootMatrixTable->GetMatrix(m_LootMatrixID);
|
|
|
|
for (const auto& entry : matrix) {
|
|
auto& thisEntry = lootInfo.PushDebug("Loot table Index - " + std::to_string(entry.LootTableIndex));
|
|
thisEntry.PushDebug<AMFDoubleValue>("Percent chance to drop") = entry.percent * 100.0f;
|
|
thisEntry.PushDebug<AMFDoubleValue>("Minimum amount to drop") = entry.minToDrop;
|
|
thisEntry.PushDebug<AMFDoubleValue>("Maximum amount to drop") = entry.maxToDrop;
|
|
const auto& lootTable = lootTableTable->GetTable(entry.LootTableIndex);
|
|
const auto& rarityTable = rarityTableTable->GetRarityTable(entry.RarityTableIndex);
|
|
|
|
auto& thisRarity = thisEntry.PushDebug("Rarity");
|
|
for (const auto& rarity : rarityTable) {
|
|
thisRarity.PushDebug<AMFDoubleValue>("Rarity " + std::to_string(rarity.rarity)) = rarity.randmax;
|
|
}
|
|
|
|
auto& thisItems = thisEntry.PushDebug("Drop(s) Info");
|
|
for (const auto& loot : lootTable) {
|
|
uint32_t itemComponentId = componentsRegistryTable->GetByIDAndType(loot.itemid, eReplicaComponentType::ITEM);
|
|
uint32_t rarity = itemComponentTable->GetItemComponentByID(itemComponentId).rarity;
|
|
auto title = "%[Objects_" + std::to_string(loot.itemid) + "_name] " + std::to_string(loot.itemid);
|
|
if (loot.MissionDrop) title += " - Mission Drop";
|
|
thisItems.PushDebug(title);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto* const entity = Game::entityManager->GetEntity(reportInfo.clientID);
|
|
destroyableInfo.PushDebug<AMFBoolValue>("Is on your team") = entity ? IsFriend(entity) : false;
|
|
auto& stats = destroyableInfo.PushDebug("Statistics");
|
|
stats.PushDebug<AMFIntValue>("Health") = m_iHealth;
|
|
stats.PushDebug<AMFDoubleValue>("Maximum Health") = m_fMaxHealth;
|
|
stats.PushDebug<AMFIntValue>("Armor") = m_iArmor;
|
|
stats.PushDebug<AMFDoubleValue>("Maximum Armor") = m_fMaxArmor;
|
|
stats.PushDebug<AMFIntValue>("Imagination") = m_iImagination;
|
|
stats.PushDebug<AMFDoubleValue>("Maximum Imagination") = m_fMaxImagination;
|
|
stats.PushDebug<AMFIntValue>("Damage Absorption Points") = m_DamageToAbsorb;
|
|
stats.PushDebug<AMFBoolValue>("Is GM Immune") = m_IsGMImmune;
|
|
stats.PushDebug<AMFBoolValue>("Is Shielded") = m_IsShielded;
|
|
destroyableInfo.PushDebug<AMFIntValue>("Attacks To Block") = m_AttacksToBlock;
|
|
destroyableInfo.PushDebug<AMFIntValue>("Damage Reduction") = m_DamageReduction;
|
|
std::stringstream factionsStream;
|
|
for (const auto factionID : m_FactionIDs) {
|
|
factionsStream << factionID << " ";
|
|
}
|
|
|
|
destroyableInfo.PushDebug<AMFStringValue>("Factions") = factionsStream.str();
|
|
|
|
factionsStream.str("");
|
|
for (const auto enemyFactionID : m_EnemyFactionIDs) {
|
|
factionsStream << enemyFactionID << " ";
|
|
}
|
|
|
|
destroyableInfo.PushDebug<AMFStringValue>("Enemy Factions") = factionsStream.str();
|
|
|
|
destroyableInfo.PushDebug<AMFBoolValue>("Is A Smashable") = m_IsSmashable;
|
|
destroyableInfo.PushDebug<AMFBoolValue>("Is Smashed") = m_IsSmashed;
|
|
destroyableInfo.PushDebug<AMFBoolValue>("Is Module Assembly") = m_IsModuleAssembly;
|
|
destroyableInfo.PushDebug<AMFDoubleValue>("Explode Factor") = m_ExplodeFactor;
|
|
destroyableInfo.PushDebug<AMFBoolValue>("Has Threats") = m_HasThreats;
|
|
|
|
destroyableInfo.PushDebug<AMFStringValue>("Killer ID", "LWOOBJID") = std::to_string(m_KillerID);
|
|
|
|
// "Scripts"; idk what to do about scripts yet
|
|
auto& immuneCounts = destroyableInfo.PushDebug("Immune Counts");
|
|
immuneCounts.PushDebug<AMFIntValue>("Basic Attack") = m_ImmuneToBasicAttackCount;
|
|
immuneCounts.PushDebug<AMFIntValue>("Damage Over Time") = m_ImmuneToDamageOverTimeCount;
|
|
immuneCounts.PushDebug<AMFIntValue>("Knockback") = m_ImmuneToKnockbackCount;
|
|
immuneCounts.PushDebug<AMFIntValue>("Interrupt") = m_ImmuneToInterruptCount;
|
|
immuneCounts.PushDebug<AMFIntValue>("Speed") = m_ImmuneToSpeedCount;
|
|
immuneCounts.PushDebug<AMFIntValue>("Imagination Gain") = m_ImmuneToImaginationGainCount;
|
|
immuneCounts.PushDebug<AMFIntValue>("Imagination Loss") = m_ImmuneToImaginationLossCount;
|
|
immuneCounts.PushDebug<AMFIntValue>("Quickbuild Interrupt") = m_ImmuneToQuickbuildInterruptCount;
|
|
immuneCounts.PushDebug<AMFIntValue>("Pull To Point") = m_ImmuneToPullToPointCount;
|
|
|
|
auto& deathInfo = destroyableInfo.PushDebug("Death Info");
|
|
deathInfo.PushDebug<AMFBoolValue>("Is Dead") = m_IsDead;
|
|
switch (m_DeathBehavior) {
|
|
case 0:
|
|
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Fade";
|
|
break;
|
|
case 1:
|
|
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Stay";
|
|
break;
|
|
case 2:
|
|
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Immediate";
|
|
break;
|
|
case -1:
|
|
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Invulnerable";
|
|
break;
|
|
default:
|
|
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Other";
|
|
break;
|
|
}
|
|
destroyableInfo.PushDebug<AMFDoubleValue>("Damage Cooldown Timer") = m_DamageCooldownTimer;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool DestroyableComponent::OnSetFaction(GameMessages::SetFaction& setFaction) {
|
|
m_DirtyHealth = true;
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
SetFaction(setFaction.factionID, setFaction.bIgnoreChecks);
|
|
return true;
|
|
}
|
|
|
|
bool DestroyableComponent::OnIsDead(GameMessages::IsDead& isDead) {
|
|
isDead.bDead = m_IsDead || (GetHealth() == 0 && GetArmor() == 0);
|
|
return true;
|
|
}
|
|
|
|
void DestroyableComponent::Resurrect(Entity& entityRef) {
|
|
Entity* entity = &entityRef;
|
|
// Restore the players health after the animation for respawning has finished.
|
|
// This is when the health appered back in live, not immediately upon requesting respawn
|
|
// Add a half second in case someone decides to cheat and move during the death animation
|
|
// and just make sure the client has time to be ready.
|
|
constexpr float respawnTime = 3.66700005531311f + 0.5f;
|
|
entity->AddCallbackTimer(respawnTime, [=]() {
|
|
GameMessages::PlayerResurrectionFinished msg;
|
|
entity->NotifyPlayerResurrectionFinished(msg);
|
|
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
|
|
|
|
if (destroyableComponent != nullptr && entity->GetLOT() == 1) {
|
|
destroyableComponent->SetIsDead(false);
|
|
auto* levelComponent = entity->GetComponent<LevelProgressionComponent>();
|
|
if (levelComponent) {
|
|
int32_t healthToRestore = levelComponent->GetLevel() >= 45 ? 8 : 4;
|
|
if (healthToRestore > destroyableComponent->GetMaxHealth()) healthToRestore = destroyableComponent->GetMaxHealth();
|
|
destroyableComponent->SetHealth(healthToRestore);
|
|
|
|
int32_t imaginationToRestore = levelComponent->GetLevel() >= 45 ? 20 : 6;
|
|
if (imaginationToRestore > destroyableComponent->GetMaxImagination()) imaginationToRestore = destroyableComponent->GetMaxImagination();
|
|
destroyableComponent->SetImagination(imaginationToRestore);
|
|
}
|
|
}
|
|
});
|
|
|
|
GameMessages::Resurrect resurrect;
|
|
resurrect.target = entity->GetObjectID();
|
|
resurrect.bRezImmediately = false;
|
|
resurrect.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|