From e9f5f08d66dffe05cf0f4603fff89cb7ee81a137 Mon Sep 17 00:00:00 2001 From: sruon Date: Sun, 19 Apr 2026 00:06:05 -0600 Subject: [PATCH] Cap targetfinding vertical search to 8/8.5y --- scripts/tests/systems/combat/aoe_vertical.lua | 126 ++++++++++++++++++ src/map/ai/helpers/targetfind.cpp | 9 ++ 2 files changed, 135 insertions(+) create mode 100644 scripts/tests/systems/combat/aoe_vertical.lua diff --git a/scripts/tests/systems/combat/aoe_vertical.lua b/scripts/tests/systems/combat/aoe_vertical.lua new file mode 100644 index 00000000000..cb9d7cc53c0 --- /dev/null +++ b/scripts/tests/systems/combat/aoe_vertical.lua @@ -0,0 +1,126 @@ +describe('AoE Vertical Range', function() + describe('player-centered', function() + ---@type CClientEntityPair + local p1, p2 + + before_each(function() + local pConfig = + { + zone = xi.zone.GM_HOME, + job = xi.job.WHM, + level = 99, + } + + p1 = xi.test.world:spawnPlayer(pConfig) + p2 = xi.test.world:spawnPlayer(pConfig) + p1.actions:inviteToParty(p2) + p2.actions:acceptPartyInvite() + + p1:addSpell(xi.magic.spell.BARSTONRA) + end) + + it('Barstonra hits party member at 7.999y above', function() + p2:setPos(p1:getXPos(), p1:getYPos() - 7.999, p1:getZPos()) + p1.actions:useSpell(p1, xi.magic.spell.BARSTONRA) + xi.test.world:tickEntity(p1) + xi.test.world:skipTime(10) + p2.assert:hasEffect(xi.effect.BARSTONE) + end) + + it('Barstonra misses party member at 8y above', function() + p2:setPos(p1:getXPos(), p1:getYPos() - 8.0, p1:getZPos()) + p1.actions:useSpell(p1, xi.magic.spell.BARSTONRA) + xi.test.world:tickEntity(p1) + xi.test.world:skipTime(10) + p2.assert.no:hasEffect(xi.effect.BARSTONE) + end) + end) + + describe('mob self-centered', function() + ---@type CClientEntityPair + local p1, p2 + + ---@type CTestEntity + local fafnir + + before_each(function() + local pConfig = + { + zone = xi.zone.DRAGONS_AERY, + job = xi.job.DRG, + level = 1, + } + + p1 = xi.test.world:spawnPlayer(pConfig) + p1:setUnkillable(true) + + p2 = xi.test.world:spawnPlayer(pConfig) + p2:setUnkillable(true) + + p1.actions:inviteToParty(p2) + p2.actions:acceptPartyInvite() + + p1.entities:moveTo('Fafnir') + fafnir = p2.entities:moveTo('Fafnir') + fafnir:spawn() + fafnir.assert:isAlive() + fafnir:updateClaim(p1) + end) + + it('Hurricane Wing hits at 8.499y above', function() + p1:setPos(fafnir:getXPos(), fafnir:getYPos() - 8.499, fafnir:getZPos()) + fafnir:setTP(3000) + fafnir:useMobAbility(xi.mobSkill.HURRICANE_WING_1, p1) + xi.test.world:skipTime(5) + xi.test.world:skipTime(5) + + assert(p1:getHPP() ~= 100, 'P1 was not hit by Hurricane Wing') + end) + + it('Hurricane Wing misses at 8.5y above', function() + p1:setPos(fafnir:getXPos(), fafnir:getYPos() - 8.5, fafnir:getZPos()) + fafnir:setTP(3000) + fafnir:useMobAbility(xi.mobSkill.HURRICANE_WING_1, p1) + xi.test.world:skipTime(5) + xi.test.world:skipTime(5) + + assert(p1:getHPP() == 100, 'P1 was hit by Hurricane Wing') + end) + + -- Spike Flail is centered on the targeted player, not the mob + it('Spike Flail hits party member at 7.999y above target', function() + local m = stub('xi.mobskills.mobPhysicalMove', + { + damage = 100, + hitsLanded = 3, + isCritical = false, + }) + + p2:setPos(p1:getXPos(), p1:getYPos() - 7.999, p1:getZPos()) + fafnir:setTP(3000) + fafnir:useMobAbility(xi.mobSkill.SPIKE_FLAIL_1, p1) + xi.test.world:skipTime(5) + xi.test.world:skipTime(5) + + m:called() + assert(p2:getHPP() ~= 100, 'P2 was not hit by Spike Flail') + end) + + it('Spike Flail misses party member at 8y above target', function() + stub('xi.mobskills.mobPhysicalMove', + { + damage = 100, + hitsLanded = 3, + isCritical = false, + }) + + p2:setPos(p1:getXPos(), p1:getYPos() - 8.0, p1:getZPos()) + fafnir:setTP(3000) + fafnir:useMobAbility(xi.mobSkill.SPIKE_FLAIL_1, p1) + xi.test.world:skipTime(5) + xi.test.world:skipTime(5) + + assert(p2:getHPP() == 100, 'P2 was hit by Spike Flail') + end) + end) +end) diff --git a/src/map/ai/helpers/targetfind.cpp b/src/map/ai/helpers/targetfind.cpp index 8c8a13a209f..129362ac4a1 100644 --- a/src/map/ai/helpers/targetfind.cpp +++ b/src/map/ai/helpers/targetfind.cpp @@ -530,6 +530,15 @@ bool CTargetFind::validEntity(CBattleEntity* PTarget) } } + // check vertical range + // Retail caps at 8.5y for mob self-centered AoE, 8y for everything else. + const float yDelta = fabsf(PTarget->loc.p.y - m_PRadiusAround->y); + const float yCap = m_selfCenteredAoE && m_PBattleEntity->objtype == TYPE_MOB ? 8.5f : 8.0f; + if (yDelta >= yCap) + { + return false; + } + // this is first target, always add him first // Exception: for self-centered AoEs, all targets must pass radius validation // Conals always add the main target