From d2a283bb09baf428512d1ac4fa504ec5cebcc347 Mon Sep 17 00:00:00 2001 From: WinterSolstice8 <60417494+wintersolstice8@users.noreply.github.com> Date: Fri, 5 Dec 2025 22:17:50 -0700 Subject: [PATCH] [core] [scripts] Enmity rework Death/Entry/reentry rework mostly --- scripts/actions/mobskills/sand_trap.lua | 4 +-- scripts/specs/core/CBaseEntity.lua | 6 +++++ src/map/ai/states/attack_state.cpp | 3 +++ src/map/enmity_container.cpp | 31 +++++++++++++++------- src/map/enmity_container.h | 1 + src/map/entities/battleentity.cpp | 20 ++++++++++++++ src/map/lua/lua_baseentity.cpp | 22 ++++++++++++++++ src/map/lua/lua_baseentity.h | 1 + src/map/utils/battleutils.cpp | 35 +++++++++++++++++++++++++ src/map/utils/battleutils.h | 2 ++ 10 files changed, 114 insertions(+), 11 deletions(-) diff --git a/scripts/actions/mobskills/sand_trap.lua b/scripts/actions/mobskills/sand_trap.lua index 2c8ad4146a6..4ecc5f3a230 100644 --- a/scripts/actions/mobskills/sand_trap.lua +++ b/scripts/actions/mobskills/sand_trap.lua @@ -23,8 +23,8 @@ mobskillObject.onMobWeaponSkill = function(target, mob, skill) xi.mobskills.mobStatusEffectMove(mob, target, xi.effect.PETRIFICATION, 1, 0, 15) - -- reset everyones enmity - mob:resetEnmity(target) + -- set everyone's enmity inactive + mob:setEnmityActive(target, false) return dmg end diff --git a/scripts/specs/core/CBaseEntity.lua b/scripts/specs/core/CBaseEntity.lua index a427f73824a..965f569fa0a 100644 --- a/scripts/specs/core/CBaseEntity.lua +++ b/scripts/specs/core/CBaseEntity.lua @@ -2813,6 +2813,12 @@ end function CBaseEntity:resetEnmity(PEntity) end +---@param PEntity CBaseEntity +---@param active boolean +---@return nil +function CBaseEntity:setEnmityActive(PEntity, active) +end + ---@param entity CBaseEntity ---@return nil function CBaseEntity:updateClaim(entity) diff --git a/src/map/ai/states/attack_state.cpp b/src/map/ai/states/attack_state.cpp index 6f62f3ab589..48478b1707e 100644 --- a/src/map/ai/states/attack_state.cpp +++ b/src/map/ai/states/attack_state.cpp @@ -75,6 +75,9 @@ bool CAttackState::Update(timer::time_point tick) action_t action{}; if (m_PEntity->OnAttack(*this, action)) { + // TODO: what about AoE auto attacks? + battleutils::handleKillshotEnmity(m_PEntity, PTarget); + // CMobEntity::OnAttack(...) can generate it's own action with a mobmod, and that leaves this action.actionType = 0, which is never valid. Skip sending the packet. if (action.actiontype != ActionCategory::None) { diff --git a/src/map/enmity_container.cpp b/src/map/enmity_container.cpp index ad4940314cf..41635fd85f1 100644 --- a/src/map/enmity_container.cpp +++ b/src/map/enmity_container.cpp @@ -98,7 +98,14 @@ void CEnmityContainer::LogoutReset(uint32 EntityID) if (const auto& enmity_obj = m_EnmityList.find(EntityID); enmity_obj != m_EnmityList.end()) { enmity_obj->second.PEnmityOwner = nullptr; - enmity_obj->second.active = false; + } +} + +void CEnmityContainer::SetActive(uint32 EntityID, bool active) +{ + if (const auto& enmity_obj = m_EnmityList.find(EntityID); enmity_obj != m_EnmityList.end()) + { + enmity_obj->second.active = active; } } @@ -204,9 +211,13 @@ void CEnmityContainer::UpdateEnmity(CBattleEntity* PEntity, int32 CE, int32 VE, int32 newVE = (int32)(enmity_obj->second.VE + (VE > 0 ? VE * bonus : VE)); // Check for cap limit - enmity_obj->second.CE = std::clamp(newCE, 0, EnmityCap); - enmity_obj->second.VE = std::clamp(newVE, 0, EnmityCap); - enmity_obj->second.active = true; + enmity_obj->second.CE = std::clamp(newCE, 0, EnmityCap); + enmity_obj->second.VE = std::clamp(newVE, 0, EnmityCap); + + if (CE >= 0 && VE >= 0) + { + enmity_obj->second.active = true; + } } else if (CE >= 0 && VE >= 0) { @@ -257,7 +268,7 @@ bool CEnmityContainer::HasID(uint32 TargetID) m_EnmityList.end(), [TargetID](auto elem) { - return elem.first == TargetID && elem.second.active; + return elem.first == TargetID; }); return maybeID != m_EnmityList.end(); @@ -457,14 +468,13 @@ CBattleEntity* CEnmityContainer::GetHighestEnmity() } uint32 HighestEnmity = 0; auto highest = m_EnmityList.end(); - bool active = false; for (auto it = m_EnmityList.begin(); it != m_EnmityList.end(); ++it) { const EnmityObject_t& PEnmityObject = it->second; uint32 Enmity = PEnmityObject.CE + PEnmityObject.VE; - if (Enmity >= HighestEnmity && ((PEnmityObject.active == active) || (PEnmityObject.active && !active))) + if (Enmity >= HighestEnmity && PEnmityObject.active) { auto* POwner = PEnmityObject.PEnmityOwner; if (!POwner || (POwner->allegiance != m_EnmityHolder->allegiance)) @@ -477,12 +487,13 @@ CBattleEntity* CEnmityContainer::GetHighestEnmity() { continue; } - active = PEnmityObject.active; + HighestEnmity = Enmity; highest = it; } } } + CBattleEntity* PEntity = nullptr; if (highest != m_EnmityList.end()) { @@ -492,7 +503,9 @@ CBattleEntity* CEnmityContainer::GetHighestEnmity() PEntity = zoneutils::GetChar(highest->first); } - if (!PEntity || PEntity->getZone() != m_EnmityHolder->getZone() || PEntity->PInstance != m_EnmityHolder->PInstance) + // TODO: Kaeko's blog indicates talking to NPCs/being in a CS also will reset hate here? + // Is this still true? + if (!PEntity || PEntity->getZone() != m_EnmityHolder->getZone() || PEntity->PInstance != m_EnmityHolder->PInstance || PEntity->isDead()) { m_EnmityList.erase(highest); PEntity = GetHighestEnmity(); diff --git a/src/map/enmity_container.h b/src/map/enmity_container.h index 2d9ddde9060..8ed9fccf742 100644 --- a/src/map/enmity_container.h +++ b/src/map/enmity_container.h @@ -52,6 +52,7 @@ class CEnmityContainer float CalculateEnmityBonus(CBattleEntity* PEntity); void Clear(uint32 EntityID = 0); // Removes Entries from list void LogoutReset(uint32 EntityID); // Sets entry to inactive + void SetActive(uint32 EntityID, bool active); void AddBaseEnmity(CBattleEntity* PEntity); void UpdateEnmity(CBattleEntity* PEntity, int32 CE, int32 VE, bool withMaster = false, bool tameable = false, bool directAction = true); void UpdateEnmityFromDamage(CBattleEntity* PEntity, int32 Damage); diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index 427fa8f506d..143c6407611 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -2376,6 +2376,16 @@ void CBattleEntity::OnCastFinished(CMagicState& state, action_t& action) roeutils::event(ROE_BUFFALLY, static_cast(PEminenceTarget), RoeDatagramList{}); } } + + if (PActionTarget->id == PTarget->id) + { + // TODO: only run this on offensive spells. + battleutils::handleKillshotEnmity(this, PActionTarget); + } + else + { + battleutils::handleSecondaryTargetEnmity(this, PActionTarget); + } } if ((!(PSpell->isHeal()) || PSpell->tookEffect()) && PActionTarget->isAlive()) { @@ -2682,6 +2692,16 @@ void CBattleEntity::OnMobSkillFinished(CMobSkillState& state, action_t& action) battleutils::ClaimMob(PTargetFound, this); } battleutils::DirtyExp(PTargetFound, this); + + if (PTargetFound->id == PTarget->id) + { + // TODO: only run this on offensive mobskills + battleutils::handleKillshotEnmity(this, PTargetFound); + } + else + { + battleutils::handleSecondaryTargetEnmity(this, PTargetFound); + } }; // Process self first if present in targets diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 97e9cec6e8b..ddebd338f42 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -13339,6 +13339,27 @@ void CLuaBaseEntity::resetEnmity(CLuaBaseEntity* PEntity) } } +/************************************************************************ + * Function: setEnmityActive() + * Purpose : Used to set enmity active or not + * Example : mob:setEnmityActive(target, true) + * Notes : Used in certain mob special abilities which set enmity inactive + ************************************************************************/ + +void CLuaBaseEntity::setEnmityActive(CLuaBaseEntity* PEntity, bool active) +{ + if (m_PBaseEntity->objtype != TYPE_MOB) + { + ShowWarning("Attempting to reset enmity for invalid entity type (%s).", m_PBaseEntity->getName()); + return; + } + + if (PEntity != nullptr && PEntity->GetBaseEntity()->objtype != TYPE_NPC) + { + static_cast(m_PBaseEntity)->PEnmityContainer->SetActive(PEntity->m_PBaseEntity->id, active); + } +} + /************************************************************************ * Function: updateClaim() * Purpose : Marks a Mob as claimed once popped by a Player @@ -19984,6 +20005,7 @@ void CLuaBaseEntity::Register() SOL_REGISTER("updateEnmityFromDamage", CLuaBaseEntity::updateEnmityFromDamage); SOL_REGISTER("updateEnmityFromCure", CLuaBaseEntity::updateEnmityFromCure); SOL_REGISTER("resetEnmity", CLuaBaseEntity::resetEnmity); + SOL_REGISTER("setEnmityActive", CLuaBaseEntity::setEnmityActive); SOL_REGISTER("updateClaim", CLuaBaseEntity::updateClaim); SOL_REGISTER("hasClaim", CLuaBaseEntity::hasClaim); SOL_REGISTER("hasEnmity", CLuaBaseEntity::hasEnmity); diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index 06c6e2fc52c..41fb5d49c3c 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -667,6 +667,7 @@ class CLuaBaseEntity void updateEnmityFromDamage(CLuaBaseEntity* PEntity, int32 damage); // Adds Enmity to player for specified mob for the damage specified void updateEnmityFromCure(CLuaBaseEntity* PEntity, int32 amount, const sol::object& fixedCE, const sol::object& fixedVE); void resetEnmity(CLuaBaseEntity* PEntity); + void setEnmityActive(CLuaBaseEntity* PEntity, bool active); void updateClaim(const sol::object& entity); bool hasClaim(CLuaBaseEntity* PTarget); bool hasEnmity(); diff --git a/src/map/utils/battleutils.cpp b/src/map/utils/battleutils.cpp index 5ccbd2b7592..b4a042887dc 100644 --- a/src/map/utils/battleutils.cpp +++ b/src/map/utils/battleutils.cpp @@ -4314,6 +4314,41 @@ void GenerateInRangeEnmity(CBattleEntity* PSource, int32 CE, int32 VE) } } +// handle "type 1" enmity reset +void handleKillshotEnmity(CBattleEntity* PAttacker, CBattleEntity* PTarget) +{ + // Handle killshot enmity reset if applicable + if (PAttacker->objtype == TYPE_MOB && PTarget) + { + if (PTarget->isDead()) + { + auto* PMob = static_cast(PAttacker); + + if (auto* PHighest = PMob->PEnmityContainer->GetHighestEnmity(); PHighest && PHighest->targid == PTarget->targid) + { + PMob->PEnmityContainer->Clear(PTarget->id); + } + } + } +} + +void handleSecondaryTargetEnmity(CBattleEntity* PAttacker, CBattleEntity* PTarget) +{ + if (PAttacker->objtype == TYPE_MOB && PTarget) + { + auto* PMob = static_cast(PAttacker); + + // Secondary targets won't get targeted anymore if they were killed from this action + if (PTarget->isDead()) + { + PMob->PEnmityContainer->SetActive(PTarget->id, false); + } + else // Inactive targets will get set back to active if hit (and not dead) + { + PMob->PEnmityContainer->SetActive(PTarget->id, true); + } + } +} /************************************************************************ * * * Transfer Enmity (used with ACCOMPLICE & COLLABORATOR ability type) * diff --git a/src/map/utils/battleutils.h b/src/map/utils/battleutils.h index 3b2edd83f24..2e095aa2301 100644 --- a/src/map/utils/battleutils.h +++ b/src/map/utils/battleutils.h @@ -182,6 +182,8 @@ bool CanUseWeaponskill(CCharEntity* PChar, CWeaponSkill* PSkill); int16 CalculateBaseTP(int32 delay); void GenerateCureEnmity(CBattleEntity* PSource, CBattleEntity* PTarget, int32 amount, int32 fixedCE = 0, int32 fixedVE = 0); void GenerateInRangeEnmity(CBattleEntity* PSource, int32 CE, int32 VE); +void handleKillshotEnmity(CBattleEntity* PAttacker, CBattleEntity* PTarget); +void handleSecondaryTargetEnmity(CBattleEntity* PAttacker, CBattleEntity* PTarget); CItemWeapon* GetEntityWeapon(CBattleEntity* PEntity, SLOTTYPE Slot); CItemEquipment* GetEntityArmor(CBattleEntity* PEntity, SLOTTYPE Slot);