From e483a2ad5068461f7713ed2139c286c46e11f288 Mon Sep 17 00:00:00 2001 From: Frankie-hz <105882754+Frankie-hz@users.noreply.github.com> Date: Sun, 3 May 2026 15:47:56 -0400 Subject: [PATCH] [cpp] Adds missing ranged attack animations Adds player ranged attacked for mobs for all the hume/fomor models. Adds quadav ranged attacks Adds Kindred ranged attacks Adds mobmod for ranged attack range for mobs to be default 14 and moveable --- .../actions/mobskills/hecatomb_wave_ra.lua | 38 ++++++ scripts/actions/mobskills/ore_toss_ranged.lua | 41 ++++++ scripts/enum/mob_mod.lua | 1 + scripts/specs/core/CBaseEntity.lua | 10 ++ sql/mob_skills.sql | 4 +- src/map/ai/ai_container.cpp | 5 +- src/map/ai/controllers/controller.cpp | 19 +++ src/map/ai/controllers/controller.h | 4 + src/map/ai/controllers/mob_controller.cpp | 53 +++++--- src/map/ai/controllers/player_controller.h | 2 +- src/map/ai/controllers/trust_controller.h | 2 +- src/map/ai/states/range_state.cpp | 19 ++- src/map/ai/states/range_state.h | 14 +-- src/map/entities/battleentity.cpp | 119 ++++++++++++++++++ src/map/entities/battleentity.h | 28 ++--- src/map/entities/mobentity.cpp | 8 ++ src/map/entities/mobentity.h | 9 +- src/map/lua/lua_baseentity.cpp | 37 ++++++ src/map/lua/lua_baseentity.h | 2 + src/map/mob_modifier.h | 1 + src/map/utils/mobutils.cpp | 37 ++++++ 21 files changed, 405 insertions(+), 48 deletions(-) create mode 100644 scripts/actions/mobskills/hecatomb_wave_ra.lua create mode 100644 scripts/actions/mobskills/ore_toss_ranged.lua diff --git a/scripts/actions/mobskills/hecatomb_wave_ra.lua b/scripts/actions/mobskills/hecatomb_wave_ra.lua new file mode 100644 index 00000000000..a55a491d0ac --- /dev/null +++ b/scripts/actions/mobskills/hecatomb_wave_ra.lua @@ -0,0 +1,38 @@ +----------------------------------- +-- Hecatomb Wave Ranged Attack +-- Family: Demons +-- Mainly used by Kindred Rangers/Ninjas +-- Description: Deals Wind damage to enemies within a fan-shaped area originating from the caster. Additional Effect: Blindness. +----------------------------------- +---@type TMobSkill +local mobskillObject = {} + +mobskillObject.onMobSkillCheck = function(target, mob, skill) + return 0 +end + +mobskillObject.onMobWeaponSkill = function(mob, target, skill, action) + local params = {} + + params.percentMultipier = 0.0476 -- TODO: Verify multiplier + params.damageCap = 260 -- TODO: Verify damage cap + params.bonusDamage = 0 + params.mAccuracyBonus = { 0, 0, 0 } + params.resistStat = xi.mod.INT + params.element = xi.element.WIND + params.attackType = xi.attackType.BREATH + params.damageType = xi.damageType.WIND + params.shadowBehavior = xi.mobskills.shadowBehavior.IGNORE_SHADOWS + + local info = xi.mobskills.mobBreathMove(mob, target, skill, action, params) + + if xi.mobskills.processDamage(mob, target, skill, action, info) then + target:takeDamage(info.damage, mob, info.attackType, info.damageType) + + xi.mobskills.mobStatusEffectMove(mob, target, xi.effect.BLINDNESS, 30, 0, math.random(60, 120)) + end + + return info.damage +end + +return mobskillObject diff --git a/scripts/actions/mobskills/ore_toss_ranged.lua b/scripts/actions/mobskills/ore_toss_ranged.lua new file mode 100644 index 00000000000..a159cbd0494 --- /dev/null +++ b/scripts/actions/mobskills/ore_toss_ranged.lua @@ -0,0 +1,41 @@ +----------------------------------- +-- Ore Toss - Ranged +-- Family: Quadav +-- Description: Ranged Auto-Attack for RNG and NIN jobs +----------------------------------- +---@type TMobSkill +local mobskillObject = {} + +mobskillObject.onMobSkillCheck = function(target, mob, skill) + return 0 +end + +mobskillObject.onMobWeaponSkill = function(mob, target, skill, action) + local params = {} + + params.baseDamage = mob:getWeaponDmg() + params.numHits = 1 + params.fTP = { 1.0, 1.0, 1.0 } + params.attackType = xi.attackType.RANGED + params.damageType = xi.damageType.BLUNT + params.shadowBehavior = xi.mobskills.shadowBehavior.NUMSHADOWS_1 + params.skipParry = true + params.skipGuard = true + params.skipBlock = true + + local info = xi.mobskills.mobRangedMove(mob, target, skill, action, params) + + -- Distance-based damage scaling: 1x at 1 yalm, 3x at 10 yalms + -- TODO: Determine max distance of skill + local distance = mob:checkDistance(target) + local distanceMultiplier = utils.clamp(1 + (distance - 1) * 2 / 9, 1, 3) + info.damage = info.damage * distanceMultiplier + + if xi.mobskills.processDamage(mob, target, skill, action, info) then + target:takeDamage(info.damage, mob, info.attackType, info.damageType) + end + + return info.damage +end + +return mobskillObject diff --git a/scripts/enum/mob_mod.lua b/scripts/enum/mob_mod.lua index a200146d015..cfb7c056fd2 100644 --- a/scripts/enum/mob_mod.lua +++ b/scripts/enum/mob_mod.lua @@ -101,4 +101,5 @@ xi.mobMod = AVATAR_ASTRAL_DELAY = 90, -- Number of milliseconds to delay AF after avatar spawn H2H_SINGLE_SWING = 91, -- Mob will have only one swing per attack even as MNK with H2H skill AOE_HIT_ALL = 92, -- Mob AoE can hit any player regardless of enmity + RANGED_ATTACK_RANGE = 93, -- Max range for ranged auto attacks. Mob will move closer if target is beyond this range. } diff --git a/scripts/specs/core/CBaseEntity.lua b/scripts/specs/core/CBaseEntity.lua index ccbab9ac462..372259dc1fe 100644 --- a/scripts/specs/core/CBaseEntity.lua +++ b/scripts/specs/core/CBaseEntity.lua @@ -3825,6 +3825,16 @@ end function CBaseEntity:setAutoAttackEnabled(state) end +---@param state boolean +---@return nil +function CBaseEntity:setRangedAttackEnabled(state) +end + +---@nodiscard +---@return boolean +function CBaseEntity:isRangedAttackEnabled() +end + ---@param state boolean ---@return nil function CBaseEntity:setMagicCastingEnabled(state) diff --git a/sql/mob_skills.sql b/sql/mob_skills.sql index 37216997722..79ccc1f082e 100644 --- a/sql/mob_skills.sql +++ b/sql/mob_skills.sql @@ -1151,7 +1151,7 @@ INSERT INTO `mob_skills` VALUES (1118,769,'lead_breath',4,0.0,15.0,2000,1500,4,0 INSERT INTO `mob_skills` VALUES (1120,69,'10000_needles',1,0.0,10.0,2000,1500,4,0,0,0,0,0,0); INSERT INTO `mob_skills` VALUES (1121,771,'eagle_eye_shot',0,0.0,25.0,2000,0,4,2,0,0,0,0,0); -- yagudo move INSERT INTO `mob_skills` VALUES (1122,770,'eagle_eye_shot',0,0.0,25.0,2000,0,4,2,0,0,0,0,0); -- quadav move --- INSERT INTO `mob_skills` VALUES (1123,355,'ore_toss',0,0.0,7.0,2000,1500,4,0,0,0,0,0,0); +INSERT INTO `mob_skills` VALUES (1123,355,'ore_toss_ranged',0,0.0,25.0,2000,0,4,0,4,0,0,0,0); -- orc ranged attack INSERT INTO `mob_skills` VALUES (1124,772,'regain_hp',1,0.0,25.0,2000,0,4,0,0,0,0,0,0); -- Dyna Statues INSERT INTO `mob_skills` VALUES (1125,773,'regain_mp',1,0.0,25.0,2000,0,4,0,0,0,0,0,0); -- Dyna Statues -- INSERT INTO `mob_skills` VALUES (1126,870,'#870',0,0.0,7.0,2000,1500,4,0,0,0,0,0,0); @@ -1180,7 +1180,7 @@ INSERT INTO `mob_skills` VALUES (1148,311,'condemnation',4,0.0,10.0,2000,1500,4, INSERT INTO `mob_skills` VALUES (1149,313,'quadrastrike',0,0.0,7.0,2000,1500,4,0,0,0,0,0,0); -- INSERT INTO `mob_skills` VALUES (1150,894,'quadrastrike',0,0.0,7.0,2000,1500,4,0,0,0,0,0,0); INSERT INTO `mob_skills` VALUES (1151,314,'eagle_eye_shot',0,0.0,25.0,2000,0,4,2,0,0,0,0,0); -- kindred --- INSERT INTO `mob_skills` VALUES (1152,304,'hecatomb_wave',0,0.0,7.0,2000,1500,4,0,0,0,0,0,0); +INSERT INTO `mob_skills` VALUES (1152,304,'hecatomb_wave_ra',4,0.0,15.0,2000,1500,4,0,0,0,0,0,0); -- kindred -- INSERT INTO `mob_skills` VALUES (1153,897,'eagle_eye_shot',0,0.0,7.0,2000,1500,4,2,0,0,0,0,0); -- INSERT INTO `mob_skills` VALUES (1154,898,'ranged_attack',0,0.0,7.0,2000,1500,4,4,0,0,0,0,0); INSERT INTO `mob_skills` VALUES (1155,337,'subsonics',1,0.0,16.0,2000,1500,4,0,0,0,0,0,0); diff --git a/src/map/ai/ai_container.cpp b/src/map/ai/ai_container.cpp index 1c40d1a4426..555f6c0bc0f 100644 --- a/src/map/ai/ai_container.cpp +++ b/src/map/ai/ai_container.cpp @@ -139,10 +139,9 @@ bool CAIContainer::Ability(uint16 targid, uint16 abilityid) bool CAIContainer::RangedAttack(uint16 targid) { - auto* PlayerController = dynamic_cast(Controller.get()); - if (PlayerController) + if (Controller) { - return PlayerController->RangedAttack(targid); + return Controller->RangedAttack(targid); } return false; } diff --git a/src/map/ai/controllers/controller.cpp b/src/map/ai/controllers/controller.cpp index 0ac7b947eea..ff1daf510c6 100644 --- a/src/map/ai/controllers/controller.cpp +++ b/src/map/ai/controllers/controller.cpp @@ -87,6 +87,15 @@ bool CController::WeaponSkill(uint16 targid, uint16 wsid) return false; } +bool CController::RangedAttack(uint16 targid) +{ + if (POwner) + { + return POwner->PAI->Internal_RangedAttack(targid); + } + return false; +} + bool CController::IsAutoAttackEnabled() const { return m_AutoAttackEnabled; @@ -97,6 +106,16 @@ void CController::SetAutoAttackEnabled(bool enabled) m_AutoAttackEnabled = enabled; } +bool CController::IsRangedAttackEnabled() const +{ + return m_RangedAttackEnabled; +} + +void CController::SetRangedAttackEnabled(bool enabled) +{ + m_RangedAttackEnabled = enabled; +} + bool CController::IsWeaponSkillEnabled() const { return m_WeaponSkillEnabled; diff --git a/src/map/ai/controllers/controller.h b/src/map/ai/controllers/controller.h index 46520bdac9e..c9030b138c3 100644 --- a/src/map/ai/controllers/controller.h +++ b/src/map/ai/controllers/controller.h @@ -47,6 +47,7 @@ class CController virtual bool ChangeTarget(uint16 targid); virtual bool Disengage(); virtual bool WeaponSkill(uint16 targid, uint16 wsid); + virtual bool RangedAttack(uint16 targid); virtual bool Ability(uint16 targid, uint16 abilityid) { return false; @@ -54,6 +55,8 @@ class CController bool IsAutoAttackEnabled() const; void SetAutoAttackEnabled(bool); + bool IsRangedAttackEnabled() const; + void SetRangedAttackEnabled(bool); bool IsWeaponSkillEnabled() const; void SetWeaponSkillEnabled(bool); bool IsMagicCastingEnabled() const; @@ -65,6 +68,7 @@ class CController timer::time_point m_Tick; CBattleEntity* POwner; bool m_AutoAttackEnabled{ true }; + bool m_RangedAttackEnabled{ false }; bool m_WeaponSkillEnabled{ true }; bool m_MagicCastingEnabled{ true }; }; diff --git a/src/map/ai/controllers/mob_controller.cpp b/src/map/ai/controllers/mob_controller.cpp index 2541c311d3a..300d0262464 100644 --- a/src/map/ai/controllers/mob_controller.cpp +++ b/src/map/ai/controllers/mob_controller.cpp @@ -739,7 +739,8 @@ auto CMobController::DoCombatTick(timer::time_point tick) -> Task if (PTarget) { - const float currentDistance = distance(PMob->loc.p, PTarget->loc.p); + const float currentDistance = distance(PMob->loc.p, PTarget->loc.p); + const float rangedAttackRange = PMob->GetRangedAttackRange(); if (IsSpecialSkillReady(currentDistance) && TrySpecialSkill()) { @@ -756,6 +757,20 @@ auto CMobController::DoCombatTick(timer::time_point tick) -> Task m_tpThreshold = xirand::GetRandomNumber(1000, 3000); co_return; } + + if (IsRangedAttackEnabled() && currentDistance <= rangedAttackRange && m_Tick >= PMob->m_LastRangedAttackTime && PMob->PAI->CanChangeState()) + { + if (PTarget != nullptr) + { + FaceTarget(PTarget->targid); + if (POwner->PAI->Internal_RangedAttack(PTarget->targid)) + { + TapDeaggroTime(); + PMob->m_LastRangedAttackTime = m_Tick; + co_return; + } + } + } } Move(); @@ -788,17 +803,8 @@ void CMobController::Move() return; } - const bool move = PMob->PAI->PathFind->IsFollowingPath(); - float attack_range = PMob->GetMeleeRange(PTarget); - const int16 offsetMod = PMob->getMobMod(MOBMOD_TARGET_DISTANCE_OFFSET); - const float offset = static_cast(offsetMod) / 10.0f; - float closeDistance = attack_range - (offsetMod == 0 ? 0.4f : offset); - - // No going negative on the final value. - if (closeDistance < 0.0f) - { - closeDistance = 0.0f; - } + const bool move = PMob->PAI->PathFind->IsFollowingPath(); + float attack_range = PMob->GetMeleeRange(PTarget); if (PMob->getMobMod(MOBMOD_ATTACK_SKILL_LIST) > 0) { @@ -814,6 +820,22 @@ void CMobController::Move() } } + if (IsRangedAttackEnabled()) + { + // We need to set the range manually because the skill lists on mobs are not audited fully + attack_range = PMob->GetRangedAttackRange(); + } + + const int16 offsetMod = PMob->getMobMod(MOBMOD_TARGET_DISTANCE_OFFSET); + const float offset = static_cast(offsetMod) / 10.0f; + float closeDistance = attack_range - (offsetMod == 0 ? 0.4f : offset); + + // No going negative on the final value. + if (closeDistance < 0.0f) + { + closeDistance = 0.0f; + } + if (PMob->getMobMod(MOBMOD_SHARE_POS) > 0) { const auto* posShare = static_cast(PMob->GetEntity(PMob->getMobMod(MOBMOD_SHARE_POS) + PMob->targid, TYPE_MOB)); @@ -1540,13 +1562,16 @@ auto CMobController::CanMoveForward(const float currentDistance) -> bool standbackRange = PMob->getMobMod(MOBMOD_STANDBACK_RANGE); } - if (PMob->m_Behavior & BEHAVIOR_STANDBACK && currentDistance < standbackRange && PMob->CanSeeTarget(PTarget)) + const bool isClosingToRangedAttackRange = IsRangedAttackEnabled() && currentDistance > PMob->GetRangedAttackRange(); + + if (!isClosingToRangedAttackRange && PMob->m_Behavior & BEHAVIOR_STANDBACK && currentDistance < standbackRange && PMob->CanSeeTarget(PTarget)) { return false; } auto standbackThreshold = PMob->getMobMod(MOBMOD_HP_STANDBACK); - if (currentDistance < standbackRange && + if (!isClosingToRangedAttackRange && + currentDistance < standbackRange && standbackThreshold > 0 && PMob->getMobMod(MOBMOD_NO_STANDBACK) == 0 && PMob->GetHPP() >= standbackThreshold && diff --git a/src/map/ai/controllers/player_controller.h b/src/map/ai/controllers/player_controller.h index 47409cbfce1..5a692ce3b32 100644 --- a/src/map/ai/controllers/player_controller.h +++ b/src/map/ai/controllers/player_controller.h @@ -44,7 +44,7 @@ class CPlayerController : public CController virtual bool WeaponSkill(uint16 targid, uint16 wsid) override; virtual bool Ability(uint16 targid, uint16 abilityid) override; - virtual bool RangedAttack(uint16 targid); + virtual bool RangedAttack(uint16 targid) override; virtual bool UseItem(uint16 targid, uint8 loc, uint8 slotid); timer::time_point getLastAttackTime(); diff --git a/src/map/ai/controllers/trust_controller.h b/src/map/ai/controllers/trust_controller.h index f35f75b8077..a172c71df74 100644 --- a/src/map/ai/controllers/trust_controller.h +++ b/src/map/ai/controllers/trust_controller.h @@ -48,7 +48,7 @@ class CTrustController : public CMobController bool Ability(uint16 targid, uint16 abilityid) override; bool Cast(uint16 targid, SpellID spellid) override; - bool RangedAttack(uint16 targid); + bool RangedAttack(uint16 targid) override; static constexpr float RoamDistance = { 2.0f }; static constexpr float SpawnDistance = { 3.0f }; diff --git a/src/map/ai/states/range_state.cpp b/src/map/ai/states/range_state.cpp index dcc00f0ca8a..7e8fa1f5195 100644 --- a/src/map/ai/states/range_state.cpp +++ b/src/map/ai/states/range_state.cpp @@ -64,7 +64,7 @@ CRangeState::CRangeState(CBattleEntity* PEntity, uint16 targid) } } - if (distance(m_PEntity->loc.p, PTarget->loc.p) > 25) + if (distance(m_PEntity->loc.p, PTarget->loc.p) > m_PEntity->GetRangedAttackRange()) { m_errorMsg = std::make_unique(m_PEntity, PTarget, 0, 0, MsgBasic::TooFarAway); throw CStateInitException(m_errorMsg->copy()); @@ -106,6 +106,17 @@ CRangeState::CRangeState(CBattleEntity* PEntity, uint16 targid) } } + if (m_PEntity->objtype == TYPE_MOB) + { + // Mobs have different delay returns for pulling out their weapon + m_returnWeaponDelay = 2850ms; + + if (distance(m_PEntity->loc.p, PTarget->loc.p) <= m_PEntity->GetMeleeRange(PTarget)) + { + m_freePhaseTime = 6500ms + std::chrono::milliseconds(xirand::GetRandomNumber(0, 1500)); // Seems to have a random factor on to when it can shoot next. 1 or 2 melee auto attacks + } + } + m_aimTime = std::chrono::milliseconds(delay); m_startPos = m_PEntity->loc.p; @@ -168,7 +179,7 @@ bool CRangeState::Update(timer::time_point tick) { m_errorMsg.reset(); - if (!PTarget || distance(m_PEntity->loc.p, PTarget->loc.p) > 25) + if (!PTarget || distance(m_PEntity->loc.p, PTarget->loc.p) > m_PEntity->GetRangedAttackRange()) { m_isOutOfRange = true; } @@ -191,6 +202,10 @@ bool CRangeState::Update(timer::time_point tick) { PChar->m_LastRangedAttackTime = GetEntryTime() + m_aimTime + m_returnWeaponDelay; } + else if (auto* PMob = dynamic_cast(m_PEntity)) + { + PMob->m_LastRangedAttackTime = GetEntryTime() + m_aimTime + m_freePhaseTime; + } return true; } diff --git a/src/map/ai/states/range_state.h b/src/map/ai/states/range_state.h index bd5c5c06f03..36f4852d81e 100644 --- a/src/map/ai/states/range_state.h +++ b/src/map/ai/states/range_state.h @@ -56,13 +56,13 @@ class CRangeState : public CState bool HasMoved(); private: - CBattleEntity* const m_PEntity; - timer::duration m_aimTime{}; // The calculated "phase 1" delay based on weapon and job trait reductions - const timer::duration m_returnWeaponDelay = 1000ms; // Phase 2: Putting the weapon back after a shot (time between shot and being able to move) - const timer::duration m_freePhaseTime = 1100ms; // Phase 3: The cooldown after a ranged attack is executed. (time after being able to move befer you stop getting "you must wait longer" when attempting to Range Attack again) - bool m_rapidShot{ false }; - position_t m_startPos; - bool m_isOutOfRange{ false }; // True if target moved out of range during aim time + CBattleEntity* const m_PEntity; + timer::duration m_aimTime{}; // The calculated "phase 1" delay based on weapon and job trait reductions + timer::duration m_returnWeaponDelay = 1000ms; // Phase 2: Putting the weapon back after a shot (time between shot and being able to move) + timer::duration m_freePhaseTime = 1100ms; // Phase 3: The cooldown after a ranged attack is executed. (time after being able to move befer you stop getting "you must wait longer" when attempting to Range Attack again) + bool m_rapidShot{ false }; + position_t m_startPos; + bool m_isOutOfRange{ false }; // True if target moved out of range during aim time }; #endif diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index 8d64db04ba1..c19aa4cfdec 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -36,6 +36,7 @@ #include "ai/states/inactive_state.h" #include "ai/states/magic_state.h" #include "ai/states/mobskill_state.h" +#include "ai/states/range_state.h" #include "ai/states/weaponskill_state.h" #include "attack.h" #include "attackround.h" @@ -546,6 +547,11 @@ float CBattleEntity::GetMeleeRange(const CBattleEntity* target) const return modelHitboxSize + 2.0f + target->modelHitboxSize; } +float CBattleEntity::GetRangedAttackRange() +{ + return 25.0f; +} + int16 CBattleEntity::GetRangedWeaponDelay(bool forTPCalc) { CItemWeapon* PRange = dynamic_cast(m_Weapons[SLOT_RANGED]); @@ -2935,6 +2941,119 @@ bool CBattleEntity::CanAttack(CBattleEntity* PTarget, std::unique_ptr(state.GetTarget()); + + if (!PTarget) + { + return; + } + + if (battleutils::IsParalyzed(this)) + { + ActionInterrupts::RangedParalyzed(this); + return; + } + + int32 damage = 0; + int32 totalDamage = 0; + + action.actorId = id; + action.actiontype = ActionCategory::RangedFinish; + action.actionid = static_cast(FourCC::RangedFinish); + action_target_t& actionTarget = action.addTarget(PTarget->id); + action_result_t& actionResult = actionTarget.addResult(); + actionResult.messageID = MsgBasic::RangedAttackHit; + + uint8 slot = SLOT_RANGED; + + uint8 shadowsTaken = 0; + uint8 hitCount = 1; + uint8 realHits = 0; + bool hitOccured = false; + bool wasCritical = false; + + for (uint8 i = 1; i <= hitCount; ++i) + { + damage = 0; + + if (xirand::GetRandomNumber(100) < battleutils::GetRangedHitRate(this, PTarget, false, 0) && !state.IsOutOfRange()) + { + if (battleutils::IsAbsorbByShadow(PTarget, this)) + { + shadowsTaken++; + } + else + { + bool isCritical = xirand::GetRandomNumber(100) < battleutils::GetCritHitRate(this, PTarget, true); + float pdif = battleutils::GetRangedDamageRatio(this, PTarget, isCritical, 0); + + if (isCritical) + { + wasCritical = true; + actionResult.messageID = MsgBasic::RangedAttackCrit; + } + + hitOccured = true; + realHits++; + damage = static_cast((GetRangedWeaponDmg() + battleutils::GetFSTR(this, PTarget, slot)) * pdif); + } + } + else + { + actionResult.resolution = ActionResolution::Miss; + actionResult.messageID = MsgBasic::RangedAttackMiss; + hitCount = i; + } + + totalDamage += damage; + } + + if (hitOccured) + { + if (actionResult.resolution == ActionResolution::Miss) + { + actionResult.messageID = MsgBasic::RangedAttackHit; + actionResult.resolution = ActionResolution::Hit; + } + + int32 finalDamage = battleutils::TakePhysicalDamage(this, PTarget, PHYSICAL_ATTACK_TYPE::RANGED, totalDamage, false, slot, realHits, nullptr, true, true); + actionResult.recordDamage(attack_outcome_t{ + .atkType = ATTACK_TYPE::PHYSICAL, + .damage = finalDamage, + .target = PTarget, + .isCritical = wasCritical, + }); + + if (shadowsTaken) + { + actionResult.param = static_cast(actionResult.param * (1 - static_cast(shadowsTaken) / realHits)); + } + + if (actionResult.param < 0) + { + actionResult.param = -(actionResult.param); + actionResult.messageID = MsgBasic::RangedAttackAbsorbs; + } + } + else if (shadowsTaken > 0) + { + actionResult.messageID = MsgBasic::ShadowAbsorb; + actionResult.resolution = ActionResolution::Miss; + actionResult.param = shadowsTaken; + } + + PTarget->LastAttacked = timer::now(); + + if (this->allegiance != PTarget->allegiance) + { + PTarget->StatusEffectContainer->DelStatusEffectsByFlag(EFFECTFLAG_DETECTABLE); + PTarget->StatusEffectContainer->DelStatusEffectsByFlag(EFFECTFLAG_ON_ATTACK); + } +} + void CBattleEntity::OnDisengage(CAttackState& s) { TracyZoneScoped; diff --git a/src/map/entities/battleentity.h b/src/map/entities/battleentity.h index df13892505f..6c0debd8704 100644 --- a/src/map/entities/battleentity.h +++ b/src/map/entities/battleentity.h @@ -379,16 +379,17 @@ class CBattleEntity : public CBaseEntity void UpdateHealth(); // recalculation of the maximum amount of hp and mp, as well as adjusting their current values uint8 UpdateSpeed(bool run = false) override; - uint32 GetWeaponDelay(bool tp); // returns delay of combined weapons - float GetMeleeRange(const CBattleEntity* Target) const; // returns the distance considered to be within melee range of the entity - int16 GetRangedWeaponDelay(bool forTPCalc); // returns delay of ranged weapon + ammo where applicable - int16 GetAmmoDelay(); // returns delay of ammo (for cooldown between shots) - uint16 GetMainWeaponDmg(); // returns total main hand DMG - uint16 GetSubWeaponDmg(); // returns total sub weapon DMG - uint16 GetRangedWeaponDmg(); // returns total ranged weapon DMG - uint16 GetMainWeaponRank(); // returns total main hand DMG Rank - uint16 GetSubWeaponRank(); // returns total sub weapon DMG Rank - uint16 GetRangedWeaponRank(); // returns total ranged weapon DMG Rank + uint32 GetWeaponDelay(bool tp); // returns delay of combined weapons + float GetMeleeRange(const CBattleEntity* Target) const; // returns the distance considered to be within melee range of the entity + virtual float GetRangedAttackRange(); // returns the maximum valid distance for a ranged attack + int16 GetRangedWeaponDelay(bool forTPCalc); // returns delay of ranged weapon + ammo where applicable + int16 GetAmmoDelay(); // returns delay of ammo (for cooldown between shots) + uint16 GetMainWeaponDmg(); // returns total main hand DMG + uint16 GetSubWeaponDmg(); // returns total sub weapon DMG + uint16 GetRangedWeaponDmg(); // returns total ranged weapon DMG + uint16 GetMainWeaponRank(); // returns total main hand DMG Rank + uint16 GetSubWeaponRank(); // returns total sub weapon DMG Rank + uint16 GetRangedWeaponRank(); // returns total ranged weapon DMG Rank uint16 GetSkill(uint16 SkillID); // the current value of the skill (not the maximum, but limited by the level) @@ -509,9 +510,7 @@ class CBattleEntity : public CBaseEntity virtual void OnChangeTarget(CBattleEntity* PTarget); virtual void OnAbility(CAbilityState&, action_t&); - virtual void OnRangedAttack(CRangeState&, action_t&) - { - } + virtual void OnRangedAttack(CRangeState&, action_t&); virtual void OnDeathTimer(); virtual void OnRaise() { @@ -552,7 +551,8 @@ class CBattleEntity : public CBaseEntity CBattleEntity* PMaster; // Owner/owner of the entity (applies to all combat entities) EntityID_t lastAttackerId_{}; timer::time_point LastAttacked; - battlehistory_t BattleHistory{}; // Stores info related to most recent combat actions taken towards this entity. + timer::time_point m_LastRangedAttackTime{}; // Used to track ranged attack delay and prevent attacks that are too close together + battlehistory_t BattleHistory{}; // Stores info related to most recent combat actions taken towards this entity. std::unique_ptr StatusEffectContainer; std::unique_ptr PRecastContainer; diff --git a/src/map/entities/mobentity.cpp b/src/map/entities/mobentity.cpp index 913df9f2ea7..d1a016cbc67 100644 --- a/src/map/entities/mobentity.cpp +++ b/src/map/entities/mobentity.cpp @@ -612,6 +612,14 @@ float CMobEntity::GetRoamRate() return (float)getMobMod(MOBMOD_ROAM_RATE) / 10.0f; } +float CMobEntity::GetRangedAttackRange() +{ + // Defaulted range is 14 as observed on all retail fomor. + // In the case this changes for other ranger/ninja types use mobmod + const int16 rangedAttackRange = getMobMod(MOBMOD_RANGED_ATTACK_RANGE); + return rangedAttackRange > 0 ? static_cast(rangedAttackRange) : 14.0f; +} + bool CMobEntity::ValidTarget(CBattleEntity* PInitiator, uint16 targetFlags) { TracyZoneScoped; diff --git a/src/map/entities/mobentity.h b/src/map/entities/mobentity.h index d72f19b3afc..e332327f1e3 100644 --- a/src/map/entities/mobentity.h +++ b/src/map/entities/mobentity.h @@ -177,10 +177,11 @@ class CMobEntity : public CBattleEntity virtual void OnMobSkillFinished(CMobSkillState&, action_t&) override; virtual void OnEngage(CAttackState&) override; - virtual bool OnAttack(CAttackState&, action_t&) override; - virtual bool CanAttack(CBattleEntity* PTarget, std::unique_ptr& errMsg) override; - virtual void OnCastFinished(CMagicState&, action_t&) override; - virtual void OnCastInterrupted(CMagicState&, action_t&, MsgBasic msg, bool blockedCast) override; + virtual float GetRangedAttackRange() override; + virtual bool OnAttack(CAttackState&, action_t&) override; + virtual bool CanAttack(CBattleEntity* PTarget, std::unique_ptr& errMsg) override; + virtual void OnCastFinished(CMagicState&, action_t&) override; + virtual void OnCastInterrupted(CMagicState&, action_t&, MsgBasic msg, bool blockedCast) override; virtual void OnDisengage(CAttackState&) override; virtual void OnDeathTimer() override; diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 8ad0e2e2b90..4d07020dcf4 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -17827,6 +17827,41 @@ void CLuaBaseEntity::setAutoAttackEnabled(bool state) m_PBaseEntity->PAI->GetController()->SetAutoAttackEnabled(state); } +/************************************************************************ + * Function: setRangedAttackEnabled() + * Purpose : Enables/disables ranged auto-attacks for a Mob + * Example : mob:setRangedAttackEnabled(true) + * Notes : Used for mobs that should fire ranged attacks instead of ranged special skills + ************************************************************************/ + +void CLuaBaseEntity::setRangedAttackEnabled(bool state) +{ + if (m_PBaseEntity->objtype & TYPE_NPC || m_PBaseEntity->objtype & TYPE_PC) + { + ShowError("function call on invalid entity! (name: %s type: %d)", m_PBaseEntity->name, m_PBaseEntity->objtype); + return; + } + + m_PBaseEntity->PAI->GetController()->SetRangedAttackEnabled(state); +} + +/************************************************************************ + * Function: isRangedAttackEnabled() + * Purpose : Returns whether ranged auto-attacks are enabled for a Mob + * Example : mob:isRangedAttackEnabled() + ************************************************************************/ + +bool CLuaBaseEntity::isRangedAttackEnabled() +{ + if (m_PBaseEntity->objtype & TYPE_NPC || m_PBaseEntity->objtype & TYPE_PC) + { + ShowError("function call on invalid entity! (name: %s type: %d)", m_PBaseEntity->name, m_PBaseEntity->objtype); + return false; + } + + return m_PBaseEntity->PAI->GetController()->IsRangedAttackEnabled(); +} + /************************************************************************ * Function: setMagicCastingEnabled() * Purpose : Used to enable/disable the casting of spells for a Mob @@ -20384,6 +20419,8 @@ void CLuaBaseEntity::Register() SOL_REGISTER("hasSpellList", CLuaBaseEntity::hasSpellList); SOL_REGISTER("setSpellList", CLuaBaseEntity::setSpellList); SOL_REGISTER("setAutoAttackEnabled", CLuaBaseEntity::setAutoAttackEnabled); + SOL_REGISTER("setRangedAttackEnabled", CLuaBaseEntity::setRangedAttackEnabled); + SOL_REGISTER("isRangedAttackEnabled", CLuaBaseEntity::isRangedAttackEnabled); SOL_REGISTER("setMagicCastingEnabled", CLuaBaseEntity::setMagicCastingEnabled); SOL_REGISTER("setMobAbilityEnabled", CLuaBaseEntity::setMobAbilityEnabled); SOL_REGISTER("setMobSkillAttack", CLuaBaseEntity::setMobSkillAttack); diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index 40f5d81d81c..475e88cc6ac 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -881,6 +881,8 @@ class CLuaBaseEntity auto hasSpellList() const -> bool; void setSpellList(uint16 spellListId) const; void setAutoAttackEnabled(bool state); // halts/resumes auto attack of entity + void setRangedAttackEnabled(bool state); // halts/resumes ranged auto attack of entity + bool isRangedAttackEnabled(); // returns whether ranged auto attack is enabled void setMagicCastingEnabled(bool state); // halt/resumes casting magic void setMobAbilityEnabled(bool state); // halt/resumes mob skills void setMobSkillAttack(int16 listId); // enable/disable using mobskills as regular attacks diff --git a/src/map/mob_modifier.h b/src/map/mob_modifier.h index 39286d212a0..293aa12221f 100644 --- a/src/map/mob_modifier.h +++ b/src/map/mob_modifier.h @@ -121,6 +121,7 @@ enum MOBMODIFIER : int MOBMOD_AVATAR_ASTRAL_DELAY = 90, // Number of milliseconds to delay AF after avatar spawn MOBMOD_H2H_SINGLE_SWING = 91, // Mob will have only one swing per attack even as MNK with H2H skill MOBMOD_AOE_HIT_ALL = 92, // Mob AoE can hit any player regardless of enmity + MOBMOD_RANGED_ATTACK_RANGE = 93, // Max range for ranged auto attacks. Mob will move closer if target is beyond this range. }; #endif diff --git a/src/map/utils/mobutils.cpp b/src/map/utils/mobutils.cpp index 56f244d639b..075c49670f1 100644 --- a/src/map/utils/mobutils.cpp +++ b/src/map/utils/mobutils.cpp @@ -26,6 +26,7 @@ #include "common/utils.h" #include "action/action.h" +#include "ai/ai_container.h" #include "battlefield.h" #include "battleutils.h" #include "grades.h" @@ -1064,6 +1065,17 @@ void CalculateMobStats(CMobEntity* PMob, bool recover) } } +void SetupRangedAttack(CMobEntity* PMob) +{ + PMob->defaultMobMod(MOBMOD_SPECIAL_SKILL, 0); // Need to remove the base ranged attack + PMob->defaultMobMod(MOBMOD_RANGED_ATTACK_RANGE, 14); + PMob->PAI->GetController()->SetRangedAttackEnabled(true); + + // auto* rangedWeapon = static_cast(PMob->m_Weapons[SLOT_RANGED]); + // rangedWeapon->setDamage(GetWeaponDamage(PMob, SLOT_RANGED)); + static_cast(PMob->m_Weapons[SLOT_RANGED])->setBaseDelay(290); +} + void SetupJob(CMobEntity* PMob) { JOBTYPE mJob = PMob->GetMJob(); @@ -1166,6 +1178,18 @@ void SetupJob(CMobEntity* PMob) { PMob->defaultMobMod(MOBMOD_SPECIAL_SKILL, 1388); } + else if (PMob->m_Family == 202) // Quadav + { + PMob->defaultMobMod(MOBMOD_SPECIAL_SKILL, 1123); // Quadav + } + else if (PMob->m_Family == 169) // Kindred + { + PMob->defaultMobMod(MOBMOD_SPECIAL_SKILL, 1152); // Hecatomb Wave + } + else if ((PMob->m_Family == 115) || (PMob->m_Family == 360)) // Fomor Ranged use player ranged attack + { + SetupRangedAttack(PMob); + } else { // All other rangers @@ -1183,6 +1207,19 @@ void SetupJob(CMobEntity* PMob) PMob->defaultMobMod(MOBMOD_SPECIAL_SKILL, 1388); PMob->defaultMobMod(MOBMOD_SPECIAL_COOL, 12); } + else if (PMob->m_Family == 202) // Quadav + { + PMob->defaultMobMod(MOBMOD_SPECIAL_SKILL, 1123); // Quadav + } + else if (PMob->m_Family == 169) // Kindred + { + PMob->defaultMobMod(MOBMOD_SPECIAL_SKILL, 1152); // Hecatomb Wave + } + else if ((PMob->m_Family == 115) || (PMob->m_Family == 360)) // Fomor Ranged use player ranged attack + { + PMob->setMobMod(MOBMOD_DUAL_WIELD, 1); + SetupRangedAttack(PMob); + } else if (PMob->m_Family != 335) // exclude NIN Maat { PMob->defaultMobMod(MOBMOD_SPECIAL_SKILL, 272);