From 51bb1d040eccdc32b121da96f8f5af70862e517d Mon Sep 17 00:00:00 2001 From: sruon Date: Sat, 18 Apr 2026 02:01:14 -0600 Subject: [PATCH 1/2] Fire _TAKE listeners on all AoE targets --- scripts/tests/systems/listeners.lua | 50 +++++++++++++++++++++++++ src/map/ai/states/ability_state.cpp | 9 ++++- src/map/ai/states/magic_state.cpp | 10 ++++- src/map/ai/states/weaponskill_state.cpp | 10 ++++- 4 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 scripts/tests/systems/listeners.lua diff --git a/scripts/tests/systems/listeners.lua b/scripts/tests/systems/listeners.lua new file mode 100644 index 00000000000..21c6fdfb427 --- /dev/null +++ b/scripts/tests/systems/listeners.lua @@ -0,0 +1,50 @@ +describe('AoE Listeners', function() + ---@type CClientEntityPair + local player + + ---@type CTestEntity + local mob1 + + ---@type CTestEntity + local mob2 + + before_each(function() + player = xi.test.world:spawnPlayer( + { + zone = xi.zone.WEST_RONFAURE, + job = xi.job.BLM, + level = 99, + }) + + mob1 = player.entities:moveTo('Wild_Rabbit') + mob2 = player.entities:moveTo('Wild_Sheep') + + local pos = player:getPos() + mob1:setPos(pos.x, pos.y, pos.z) + mob2:setPos(pos.x, pos.y, pos.z) + end) + + it('fires MAGIC_TAKE on all targets of an AoE spell', function() + local mob1Hit = false + local mob2Hit = false + + mob1:addListener('MAGIC_TAKE', 'TEST_MAGIC_TAKE_1', function() + mob1Hit = true + end) + + mob2:addListener('MAGIC_TAKE', 'TEST_MAGIC_TAKE_2', function() + mob2Hit = true + end) + + player:addSpell(xi.magic.spell.FIRAGA) + player.actions:useSpell(mob1, xi.magic.spell.FIRAGA) + xi.test.world:tickEntity(player) + xi.test.world:skipTime(10) + + assert(mob1Hit, 'MAGIC_TAKE should fire on primary target') + assert(mob2Hit, 'MAGIC_TAKE should fire on AoE subtarget') + + mob1:removeListener('TEST_MAGIC_TAKE_1') + mob2:removeListener('TEST_MAGIC_TAKE_2') + end) +end) diff --git a/src/map/ai/states/ability_state.cpp b/src/map/ai/states/ability_state.cpp index 7fe091a49ef..99609a7764b 100644 --- a/src/map/ai/states/ability_state.cpp +++ b/src/map/ai/states/ability_state.cpp @@ -37,6 +37,7 @@ #include "status_effect_container.h" #include "utils/battleutils.h" #include "utils/charutils.h" +#include "utils/zoneutils.h" namespace { @@ -209,9 +210,13 @@ bool CAbilityState::Update(timer::time_point tick) { m_PEntity->loc.zone->PushPacket(m_PEntity, CHAR_INRANGE_SELF, std::make_unique(action)); } - if (auto* target = GetTarget()) + for (auto& actionTarget : action.targets) { - target->PAI->EventHandler.triggerListener("ABILITY_TAKE", m_PEntity, target, m_PAbility.get(), &action); + auto* PActionTarget = dynamic_cast(zoneutils::GetEntity(actionTarget.actorId)); + if (PActionTarget) + { + PActionTarget->PAI->EventHandler.triggerListener("ABILITY_TAKE", m_PEntity, PActionTarget, m_PAbility.get(), &action); + } } } diff --git a/src/map/ai/states/magic_state.cpp b/src/map/ai/states/magic_state.cpp index fedac96a429..ebcd4256e3b 100644 --- a/src/map/ai/states/magic_state.cpp +++ b/src/map/ai/states/magic_state.cpp @@ -38,6 +38,7 @@ #include "spell.h" #include "status_effect_container.h" #include "utils/battleutils.h" +#include "utils/zoneutils.h" CMagicState::CMagicState(CBattleEntity* PEntity, uint16 targid, SpellID spellid, uint8 flags) : CState(PEntity, targid) @@ -260,7 +261,14 @@ bool CMagicState::Update(timer::time_point tick) { m_PEntity->OnCastFinished(*this, action); m_PEntity->PAI->EventHandler.triggerListener("MAGIC_USE", m_PEntity, PTarget, m_PSpell.get(), &action); - PTarget->PAI->EventHandler.triggerListener("MAGIC_TAKE", PTarget, m_PEntity, m_PSpell.get(), &action); + for (auto& actionTarget : action.targets) + { + auto* PActionTarget = dynamic_cast(zoneutils::GetEntity(actionTarget.actorId)); + if (PActionTarget) + { + PActionTarget->PAI->EventHandler.triggerListener("MAGIC_TAKE", PActionTarget, m_PEntity, m_PSpell.get(), &action); + } + } } // Zero messageID so spells dont emit messages diff --git a/src/map/ai/states/weaponskill_state.cpp b/src/map/ai/states/weaponskill_state.cpp index 1eab8ab0912..6816c01d4ab 100644 --- a/src/map/ai/states/weaponskill_state.cpp +++ b/src/map/ai/states/weaponskill_state.cpp @@ -29,6 +29,7 @@ #include "roe.h" #include "status_effect_container.h" #include "utils/battleutils.h" +#include "utils/zoneutils.h" #include "weapon_skill.h" CWeaponSkillState::CWeaponSkillState(CBattleEntity* PEntity, uint16 targid, uint16 wsid) @@ -158,7 +159,14 @@ bool CWeaponSkillState::Update(timer::time_point tick) uint32 weaponskillDamage = weaponskillVar & 0xFFFFFF; m_PEntity->PAI->EventHandler.triggerListener("WEAPONSKILL_USE", m_PEntity, PTarget, m_PSkill.get(), m_spent, &action, weaponskillDamage); - PTarget->PAI->EventHandler.triggerListener("WEAPONSKILL_TAKE", m_PEntity, PTarget, m_PSkill.get(), m_spent, &action); + for (auto& actionTarget : action.targets) + { + auto* PActionTarget = dynamic_cast(zoneutils::GetEntity(actionTarget.actorId)); + if (PActionTarget) + { + PActionTarget->PAI->EventHandler.triggerListener("WEAPONSKILL_TAKE", m_PEntity, PActionTarget, m_PSkill.get(), m_spent, &action); + } + } if (m_PEntity->objtype == TYPE_PC) { From 6298ad87cfdcfcb26a465c682df02b9645c6e67d Mon Sep 17 00:00:00 2001 From: sruon Date: Sat, 18 Apr 2026 12:56:05 -0600 Subject: [PATCH 2/2] Lower chance for Dynamis staggers if not main target --- scripts/mixins/dynamis_dreamland.lua | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/mixins/dynamis_dreamland.lua b/scripts/mixins/dynamis_dreamland.lua index 0d5f4191d43..cb3eff45cc9 100644 --- a/scripts/mixins/dynamis_dreamland.lua +++ b/scripts/mixins/dynamis_dreamland.lua @@ -43,8 +43,11 @@ g_mixins.dynamis_dreamland = function(dynamisDreamlandMob) [4] = { single = 250, hundred = 50 }, } - dynamisDreamlandMob:addListener('MAGIC_TAKE', 'DYNAMIS_MAGIC_PROC_CHECK', function(target, caster, spell) - if math.random(1, 100) > 8 then + dynamisDreamlandMob:addListener('MAGIC_TAKE', 'DYNAMIS_MAGIC_PROC_CHECK', function(target, caster, spell, action) + local isPrimary = action and target:getID() == action:getPrimaryTargetID() + local chance = isPrimary and 8 or 1 -- Lowered chance if not primary target. 1% is likely not the right number, submit captures. + + if math.random(1, 100) > chance then return end @@ -65,7 +68,10 @@ g_mixins.dynamis_dreamland = function(dynamisDreamlandMob) end) dynamisDreamlandMob:addListener('WEAPONSKILL_TAKE', 'DYNAMIS_WS_PROC_CHECK', function(user, target, skill, tp, action) - if math.random(1, 100) > 25 then + local isPrimary = action and target:getID() == action:getPrimaryTargetID() + local chance = isPrimary and 25 or 2 -- Lowered chance if not primary target. 2% is likely not the right number, submit captures. + + if math.random(1, 100) > chance then return end @@ -86,7 +92,10 @@ g_mixins.dynamis_dreamland = function(dynamisDreamlandMob) end) dynamisDreamlandMob:addListener('ABILITY_TAKE', 'DYNAMIS_ABILITY_PROC_CHECK', function(user, target, skill, action) - if math.random(1, 100) > 20 then + local isPrimary = action and target:getID() == action:getPrimaryTargetID() + local chance = isPrimary and 20 or 2 -- Lowered chance if not primary target. 2% is likely not the right number, submit captures. + + if math.random(1, 100) > chance then return end