From e34ed9f43541e1ae378aa7c85351bac75f8dc186 Mon Sep 17 00:00:00 2001 From: Xaver-DaRed Date: Wed, 25 Feb 2026 19:16:00 +0100 Subject: [PATCH 1/2] Add lua binding for non-player entities for starting an spell cast --- scripts/specs/types/MobEntity.lua | 1 + src/map/ai/states/magic_state.cpp | 1 + src/map/lua/luautils.cpp | 26 +++++++++++++++++++++++++- src/map/lua/luautils.h | 1 + 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/scripts/specs/types/MobEntity.lua b/scripts/specs/types/MobEntity.lua index 64b54fc8e7e..acd6defba0d 100644 --- a/scripts/specs/types/MobEntity.lua +++ b/scripts/specs/types/MobEntity.lua @@ -36,6 +36,7 @@ ---@field onSpikesDamage? fun(mob: CBaseEntity, target: CBaseEntity, damage: integer): (integer?, integer?, integer?) ---@field onMagicHit? fun(caster: CBaseEntity, target: CBaseEntity, spell: CSpell) ---@field onSpellPrecast? fun(mob: CBaseEntity, spell: CSpell) +---@field onSpellCastStart? fun(mob: CBaseEntity, spell: CSpell) ---@field onSpellInterrupted? fun(mob: CBaseEntity, spell: CSpell) ---@field onSteal? fun(player: CBaseEntity, target: CBaseEntity, ability: CAbility, action: CAction): integer? ---@field onMagicCastingCheck? fun(mob: CBaseEntity, target: CBaseEntity, spell: CSpell): integer? diff --git a/src/map/ai/states/magic_state.cpp b/src/map/ai/states/magic_state.cpp index 38455db05b5..b1909555cdb 100644 --- a/src/map/ai/states/magic_state.cpp +++ b/src/map/ai/states/magic_state.cpp @@ -114,6 +114,7 @@ CMagicState::CMagicState(CBattleEntity* PEntity, uint16 targid, SpellID spellid, }; // TODO: weaponskill lua object + luautils::OnSpellCastStart(m_PEntity, m_PSpell.get()); m_PEntity->PAI->EventHandler.triggerListener("MAGIC_START", m_PEntity, m_PSpell.get(), &action); // if spell:setFlag(xi.magic.spellFlag.NO_START_MSG) is called, don't give spell start packet diff --git a/src/map/lua/luautils.cpp b/src/map/lua/luautils.cpp index ca8ba1f991c..ffada3c5046 100644 --- a/src/map/lua/luautils.cpp +++ b/src/map/lua/luautils.cpp @@ -2813,7 +2813,7 @@ void OnSpellPrecast(CBattleEntity* PCaster, CSpell* PSpell) { TracyZoneScoped; - if (PCaster->objtype != TYPE_MOB) + if (PCaster->objtype == TYPE_PC) { return; } @@ -2833,6 +2833,30 @@ void OnSpellPrecast(CBattleEntity* PCaster, CSpell* PSpell) } } +void OnSpellCastStart(CBattleEntity* PCaster, CSpell* PSpell) +{ + TracyZoneScoped; + + if (PCaster->objtype == TYPE_PC) + { + return; + } + + sol::function onSpellInterrupted = getEntityCachedFunction(PCaster, "onSpellCastStart"); + if (!onSpellInterrupted.valid()) + { + return; + } + + auto result = onSpellInterrupted(PCaster, PSpell); + if (!result.valid()) + { + sol::error err = result; + ShowError("luautils::onSpellCastStart: %s", err.what()); + ReportErrorToPlayer(PCaster, err.what()); + } +} + void OnSpellInterrupted(CBattleEntity* PCaster, CSpell* PSpell) { TracyZoneScoped; diff --git a/src/map/lua/luautils.h b/src/map/lua/luautils.h index 72222ad998c..fde52d83ebd 100644 --- a/src/map/lua/luautils.h +++ b/src/map/lua/luautils.h @@ -339,6 +339,7 @@ void CheckForGearSet(CBaseEntity* PTarget); int32 OnMagicCastingCheck(CBaseEntity* PChar, CBaseEntity* PTarget, CSpell* PSpell); int32 OnSpellCast(CBattleEntity* PCaster, CBattleEntity* PTarget, CSpell* PSpell); void OnSpellPrecast(CBattleEntity* PCaster, CSpell* PSpell); +void OnSpellCastStart(CBattleEntity* PCaster, CSpell* PSpell); void OnSpellInterrupted(CBattleEntity* PCaster, CSpell* PSpell); auto OnMobSpellChoose(CBattleEntity* PCaster, CBattleEntity* PTarget, std::optional startingSpellId) -> std::tuple, std::optional>; void OnMagicHit(CBattleEntity* PCaster, CBattleEntity* PTarget, CSpell* PSpell); From ea97d068346dc6ca427f75739827a8f32dace9ce Mon Sep 17 00:00:00 2001 From: Xaver-DaRed Date: Wed, 25 Feb 2026 19:32:12 +0100 Subject: [PATCH 2/2] Add `target` parameter to new function and old listener --- documentation/AI_Events.txt | 2 +- .../abilities/pets/attachments/ice_maker.lua | 2 +- scripts/actions/mobskills/providence.lua | 2 +- scripts/actions/mobskills/xenoglossia.lua | 7 +- scripts/specs/types/MobEntity.lua | 2 +- .../mobs/Ajido-Marujido.lua | 96 ++++++++++--------- src/map/ai/states/magic_state.cpp | 4 +- src/map/lua/luautils.cpp | 2 +- src/map/lua/luautils.h | 2 +- 9 files changed, 62 insertions(+), 57 deletions(-) diff --git a/documentation/AI_Events.txt b/documentation/AI_Events.txt index 26ed56d90f9..ae73a481a4c 100644 --- a/documentation/AI_Events.txt +++ b/documentation/AI_Events.txt @@ -54,7 +54,7 @@ WEAPONSKILL_USE - userEntity, targetEntity, skillId, tp, action WEAPONSKILL_TAKE - userEntity, targetEntity, skillId, tp, action WEAPONSKILL_STATE_EXIT - userEntity, skillId -MAGIC_START - Entity, Spell, action +MAGIC_START - Entity, Target, Spell, action MAGIC_USE - Entity, Target, Spell, action MAGIC_INTERRUPTED - Entity, Target, Spell, action MAGIC_TAKE - Target, Entity, Spell, action diff --git a/scripts/actions/abilities/pets/attachments/ice_maker.lua b/scripts/actions/abilities/pets/attachments/ice_maker.lua index e4c6a32a709..d7f37eb00be 100644 --- a/scripts/actions/abilities/pets/attachments/ice_maker.lua +++ b/scripts/actions/abilities/pets/attachments/ice_maker.lua @@ -5,7 +5,7 @@ local attachmentObject = {} attachmentObject.onEquip = function(automaton) - automaton:addListener('MAGIC_START', 'AUTO_ICE_MAKER_START', function(pet, spell, action) + automaton:addListener('MAGIC_START', 'AUTO_ICE_MAKER_START', function(pet, target, spell, action) if spell:getSkillType() ~= xi.skill.ELEMENTAL_MAGIC then return end diff --git a/scripts/actions/mobskills/providence.lua b/scripts/actions/mobskills/providence.lua index b7c24698c8f..1724db332d8 100644 --- a/scripts/actions/mobskills/providence.lua +++ b/scripts/actions/mobskills/providence.lua @@ -27,7 +27,7 @@ mobskillObject.onMobWeaponSkill = function(target, mob, skill) skill:setMsg(xi.msg.basic.USES) -- Listener will reset Poroggo to regular state on first cast. - mob:addListener('MAGIC_START', 'PROVIDENCE_MAGIC_START', function(mobArg, spell, action) + mob:addListener('MAGIC_START', 'PROVIDENCE_MAGIC_START', function(mobArg, targetArg, spell, action) -- Reset Poroggo to former spell list or default to generic BLM list local postProvidenceSpellListId = mobArg:getLocalVar('[providence]spellListId') or 2 local postProvidenceMagicCool = mobArg:getLocalVar('[providence]magicCool') or 35 diff --git a/scripts/actions/mobskills/xenoglossia.lua b/scripts/actions/mobskills/xenoglossia.lua index 9453b6576ca..b888f08e0b5 100644 --- a/scripts/actions/mobskills/xenoglossia.lua +++ b/scripts/actions/mobskills/xenoglossia.lua @@ -13,10 +13,11 @@ mobskillObject.onMobSkillCheck = function(target, mob, skill) end mobskillObject.onMobWeaponSkill = function(target, mob, skill) + -- TODO: Remove this. This is horrible. mob:addMod(xi.mod.UFASTCAST, 150) - mob:addListener('MAGIC_START', 'XENOGLOSSIA_MAGIC_START', function(user) - user:delMod(xi.mod.UFASTCAST, 150) - user:removeListener('XENOGLOSSIA_MAGIC_START') + mob:addListener('MAGIC_START', 'XENOGLOSSIA_MAGIC_START', function(mobArg, targetArg, spell, action) + mobArg:delMod(xi.mod.UFASTCAST, 150) + mobArg:removeListener('XENOGLOSSIA_MAGIC_START') end) skill:setMsg(xi.msg.basic.USES) diff --git a/scripts/specs/types/MobEntity.lua b/scripts/specs/types/MobEntity.lua index acd6defba0d..31061d70288 100644 --- a/scripts/specs/types/MobEntity.lua +++ b/scripts/specs/types/MobEntity.lua @@ -36,7 +36,7 @@ ---@field onSpikesDamage? fun(mob: CBaseEntity, target: CBaseEntity, damage: integer): (integer?, integer?, integer?) ---@field onMagicHit? fun(caster: CBaseEntity, target: CBaseEntity, spell: CSpell) ---@field onSpellPrecast? fun(mob: CBaseEntity, spell: CSpell) ----@field onSpellCastStart? fun(mob: CBaseEntity, spell: CSpell) +---@field onSpellCastStart? fun(mob: CBaseEntity, target: CBaseEntity, spell: CSpell) ---@field onSpellInterrupted? fun(mob: CBaseEntity, spell: CSpell) ---@field onSteal? fun(player: CBaseEntity, target: CBaseEntity, ability: CAbility, action: CAction): integer? ---@field onMagicCastingCheck? fun(mob: CBaseEntity, target: CBaseEntity, spell: CSpell): integer? diff --git a/scripts/zones/Full_Moon_Fountain/mobs/Ajido-Marujido.lua b/scripts/zones/Full_Moon_Fountain/mobs/Ajido-Marujido.lua index 14346ecf552..b53d9124405 100644 --- a/scripts/zones/Full_Moon_Fountain/mobs/Ajido-Marujido.lua +++ b/scripts/zones/Full_Moon_Fountain/mobs/Ajido-Marujido.lua @@ -56,71 +56,75 @@ local helperConfig = end, } -entity.onMobSpawn = function(mob) - xi.mix.helperNpc.config(mob, helperConfig) - mob:setMagicCastingEnabled(false) - - mob:addListener('MAGIC_START', 'MAGIC_MSG', function(ajidoMob, spell, action) - local spellId = spell:getID() - if spellId == xi.magic.spell.BURST then - ajidoMob:showText(ajidoMob, ID.text.PLAY_TIME_IS_OVER) - elseif spellId == xi.magic.spell.FLOOD then - ajidoMob:showText(ajidoMob, ID.text.YOU_SHOULD_BE_THANKFUL) +entity.onMobInitialize = function(mob) + -- Teleport when taking damage + mob:addListener('TAKE_DAMAGE', 'AJIDO_TAKE_DAMAGE', function(mobArg, damage, attacker, attackType, damageType) + if damage <= 30 then + return end - end) - -- Teleport when taking damage - mob:addListener('TAKE_DAMAGE', 'AJIDO_TAKE_DAMAGE', function(ajidoMob, damage, attacker, attackType, damageType) - if damage > 30 and GetSystemTime() > ajidoMob:getLocalVar('teleportTime') then - ajidoMob:setMagicCastingEnabled(false) - ajidoMob:setLocalVar('warpInProgress', 1) - ajidoMob:useMobAbility(xi.mobSkill.AJIDO_WARP_OUT, nil, 0) + if GetSystemTime() <= mobArg:getLocalVar('teleportTime') then + return end + + mobArg:setMagicCastingEnabled(false) + mobArg:setLocalVar('warpInProgress', 1) + mobArg:useMobAbility(xi.mobSkill.AJIDO_WARP_OUT, nil, 0) end) - mob:addListener('WEAPONSKILL_STATE_EXIT', 'WARP_OUT_COMPLETE', function(ajidoMob, skillId) + mob:addListener('WEAPONSKILL_STATE_EXIT', 'WARP_OUT_COMPLETE', function(mobArg, skillId) if skillId == xi.mobSkill.AJIDO_WARP_OUT then - local battlefieldArea = ajidoMob:getBattlefield():getArea() - local config = teleportConfig[battlefieldArea] - - if config and config.positions then - -- Select a random position from available teleport locations - local targetPosition = utils.randomEntry(config.positions) - - if targetPosition then - ajidoMob:setPos(targetPosition.x, targetPosition.y, targetPosition.z, ajidoMob:getRotPos()) - ajidoMob:queue(0, function(mobArg) - mobArg:useMobAbility(xi.mobSkill.AJIDO_WARP_IN, nil, 0) - end) - end - end + local config = teleportConfig[mobArg:getBattlefield():getArea()] + local targetPosition = utils.randomEntry(config.positions) + + mobArg:setPos(targetPosition.x, targetPosition.y, targetPosition.z, mobArg:getRotPos()) + mobArg:queue(0, function(mobArgArg) + mobArgArg:useMobAbility(xi.mobSkill.AJIDO_WARP_IN, nil, 0) + end) elseif skillId == xi.mobSkill.AJIDO_WARP_IN then - local currentTime = GetSystemTime() - ajidoMob:setMagicCastingEnabled(true) - ajidoMob:setLocalVar('warpInProgress', 0) - ajidoMob:setLocalVar('teleportTime', currentTime + 5) + mobArg:setMagicCastingEnabled(true) + mobArg:setLocalVar('warpInProgress', 0) + mobArg:setLocalVar('teleportTime', GetSystemTime() + 5) end end) end +entity.onMobSpawn = function(mob) + xi.mix.helperNpc.config(mob, helperConfig) + mob:setMagicCastingEnabled(false) +end + entity.onMobEngage = function(mob, target) - local currentTime = GetSystemTime() - mob:setLocalVar('magicWait', currentTime + 30) + mob:setLocalVar('magicWait', GetSystemTime() + 30) end entity.onMobFight = function(mob, target) - if mob:getHPP() < 50 and mob:getLocalVar('saidMessage') == 0 then + if mob:getLocalVar('warpInProgress') == 1 then + return + end + + -- Wait 30 seconds to start casting (but only if not in warp process) + if GetSystemTime() > mob:getLocalVar('magicWait') then + mob:setMagicCastingEnabled(true) + end + + if mob:getLocalVar('saidMessage') == 1 then + return + end + + if mob:getHPP() < 50 then mob:showText(mob, ID.text.DONT_GIVE_UP) mob:setLocalVar('saidMessage', 1) end +end - -- Wait 30 seconds to start casting (but only if not in warp process) - if mob:getLocalVar('warpInProgress') == 0 then - local currentTime = GetSystemTime() - local magicWait = mob:getLocalVar('magicWait') - if currentTime > magicWait then - mob:setMagicCastingEnabled(true) - end +entity.onSpellCastStart = function(mob, target, spell) + local spellId = spell:getID() + + if spellId == xi.magic.spell.BURST then + mob:showText(mob, ID.text.PLAY_TIME_IS_OVER) + elseif spellId == xi.magic.spell.FLOOD then + mob:showText(mob, ID.text.YOU_SHOULD_BE_THANKFUL) end end diff --git a/src/map/ai/states/magic_state.cpp b/src/map/ai/states/magic_state.cpp index b1909555cdb..36e92f2c778 100644 --- a/src/map/ai/states/magic_state.cpp +++ b/src/map/ai/states/magic_state.cpp @@ -114,8 +114,8 @@ CMagicState::CMagicState(CBattleEntity* PEntity, uint16 targid, SpellID spellid, }; // TODO: weaponskill lua object - luautils::OnSpellCastStart(m_PEntity, m_PSpell.get()); - m_PEntity->PAI->EventHandler.triggerListener("MAGIC_START", m_PEntity, m_PSpell.get(), &action); + luautils::OnSpellCastStart(m_PEntity, PTarget, m_PSpell.get()); + m_PEntity->PAI->EventHandler.triggerListener("MAGIC_START", m_PEntity, PTarget, m_PSpell.get(), &action); // if spell:setFlag(xi.magic.spellFlag.NO_START_MSG) is called, don't give spell start packet if (GetSpell()->getFlag() & SPELLFLAG_NO_START_MSG) diff --git a/src/map/lua/luautils.cpp b/src/map/lua/luautils.cpp index ffada3c5046..f52ee194d30 100644 --- a/src/map/lua/luautils.cpp +++ b/src/map/lua/luautils.cpp @@ -2833,7 +2833,7 @@ void OnSpellPrecast(CBattleEntity* PCaster, CSpell* PSpell) } } -void OnSpellCastStart(CBattleEntity* PCaster, CSpell* PSpell) +void OnSpellCastStart(CBattleEntity* PCaster, CBattleEntity* PTarget, CSpell* PSpell) { TracyZoneScoped; diff --git a/src/map/lua/luautils.h b/src/map/lua/luautils.h index fde52d83ebd..d4528d2eab0 100644 --- a/src/map/lua/luautils.h +++ b/src/map/lua/luautils.h @@ -339,7 +339,7 @@ void CheckForGearSet(CBaseEntity* PTarget); int32 OnMagicCastingCheck(CBaseEntity* PChar, CBaseEntity* PTarget, CSpell* PSpell); int32 OnSpellCast(CBattleEntity* PCaster, CBattleEntity* PTarget, CSpell* PSpell); void OnSpellPrecast(CBattleEntity* PCaster, CSpell* PSpell); -void OnSpellCastStart(CBattleEntity* PCaster, CSpell* PSpell); +void OnSpellCastStart(CBattleEntity* PCaster, CBattleEntity* PTarget, CSpell* PSpell); void OnSpellInterrupted(CBattleEntity* PCaster, CSpell* PSpell); auto OnMobSpellChoose(CBattleEntity* PCaster, CBattleEntity* PTarget, std::optional startingSpellId) -> std::tuple, std::optional>; void OnMagicHit(CBattleEntity* PCaster, CBattleEntity* PTarget, CSpell* PSpell);