Skip to content

[core][lua][magic] Unify Spell Interrupt into Lua#10204

Open
JerokeXI wants to merge 1 commit into
LandSandBoat:basefrom
JerokeXI:luaAquaveil
Open

[core][lua][magic] Unify Spell Interrupt into Lua#10204
JerokeXI wants to merge 1 commit into
LandSandBoat:basefrom
JerokeXI:luaAquaveil

Conversation

@JerokeXI

Copy link
Copy Markdown
Contributor

I affirm:

  • I understand that if I do not agree to the following points by completing the checkboxes my PR will be ignored.
  • I understand I should leave resolving conversations to the LandSandBoat team so that reviewers won't miss what was said.
  • I have read and understood the Contributing Guide and the Code of Conduct.
  • I have tested my code and the things my code has changed since the last commit in the PR and will test after any later commits.

What does this pull request do?

Changes Spell Interrupt from CPP to LUA

  1. Moves all logic from battleutils.cpp function TryInterruptSpell into magic_interrupt.lua
  2. Adds require('scripts/globals/combat/magic_interrupt') to magic.lua
  3. Replaces the logic in battleutils.cpp with return the lua function.

Steps to test these changes

  1. Rebuild if needed, run the server stack, and login.
  2. Find some mobs and get hit while casting magic
  3. Be interrupted or don't be interrupted if you're lucky and/or have Spell Interrupt Reduction
  4. Cast Aquaveil
  5. Find some mobs and get hit while casting magic
  6. Don't get interrupted the appropriate number of times

@JerokeXI JerokeXI marked this pull request as ready for review May 31, 2026 20:42
Comment thread scripts/globals/magic.lua
@@ -1,4 +1,5 @@
require('scripts/globals/combat/magic_hit_rate')
require('scripts/globals/combat/magic_interrupt')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unneeded

end

return false
end

@Xaver-DaRed Xaver-DaRed Jun 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proposal:

xi.combat.magic.shouldInterruptSpell = function(attacker, defender, spell)
    -- Early return: Manafont prevents casting interruption.
    if defender:hasStatusEffect(xi.effect.MANAFONT) then
        return false
    end

    -- Early return: Trusts can't be interrupted.
    local entityType = defender:getObjType()
    if entityType == xi.objType.TRUST then
        return false
    end

    -- Early return: Songs can't be interrupted.
    local skillType = spell:getSkillType()
    if skillType == xi.skill.SINGING then
        return false
    end

    -- Llevel factor.
    local baseInterruptionRate = entityType == xi.objType.MOB and 5 or 50
    local levelRatio           = utils.clamp((baseInterruptionRate + attacker:getMainLvl() - defender:getMainLvl()) / 100, 0.01, 1.02)

    -- Skill factor.
    local skillRatio = 1

    if entityType == xi.objType.PC then
        local skillCap   = defender:getMaxSkillLevel(defender:getMainLvl(), defender:getMainJob(), skillType)
        local skillLevel = defender:getSkillLevel(skillType)

        -- If skill cap is 0, player may be using a spell from their subjob.
        if skillCap == 0 then
            skillCap = defender:getMaxSkillLevel(defender:getMainLvl(), defender:getSubJob(), skillType)
        end

        -- If skill level is 0, set ratio to 10.
        if skillLevel <= 0 then
            skillRatio = 10
        else
            skillRatio = skillCap / skillLevel
        end
    end

    -- SIRD reduces the interrupt after all the calculations are done -- as evidenced by the infamous "102% SIRD" builds.
    -- Anything less than 102% interrupt results in the ability to be interrupted.
    -- Note: the 102% is probably an x/256 x/1024 nonsense -- sometimes 101% works.
    local sirdRatio = (100 - defender:getMerit(xi.merit.SPELL_INTERUPTION_RATE) - defender:getMod(xi.mod.SPELLINTERRUPT)) / 100

    -- levelRatio : 0.01 to infinity.
    -- skillRatio : 1 to infinity.
    -- SIRDRatio  : No limits. Can be negative. A negative value will guarantee NOT being interrupted.
    local finalRatio = levelRatio * skillRatio * sirdRatio -- TL;DR Higher = Worse = More chances to get interrupted.

    -- Early return: Caster doesn't get interrupted.
    if math.random() >= finalRatio then
        return false
    end

    -- Early return: Caster can't prevent interruption via aquaveil.
    if not defender:hasStatusEffect(xi.effect.AQUAVEIL) then
        return true
    end

    -- Handle aquaveil and prevent interruption.
    local aquaveilPower = defender:getStatusEffect(xi.effect.AQUAVEIL):getPower() - 1
    if aquaveilPower == 0 then
        defender:delStatusEffect(xi.effect.AQUAVEIL)
    else
        defender:getStatusEffect(xi.effect.AQUAVEIL):setPower(aquaveilPower)
    end

    return false
end

@Xaver-DaRed Xaver-DaRed Jun 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of this being a learning experience, some notes here:

  • Lua and C++ don't work the same. You don't need .0 floats in there. They don't hurt, but they also don't provide extra precision in Lua. So they are extra characters that aren't needed.
  • If you find yourself fetching entity type, or other kinds of info, more than once, it may be wroth just storing them in a variable.
  • Long operations can be divided in multiple steps, for the sake of readability. I'm refering concretely when fetching the base interruption rate, which varies between players and other entities.
  • I didn't do it in my proposal, but, defender could be named caster to make it crystal clear which entity is the one that can be interrupted. Minor, but everything helps for the sake of making the code as easy to follow as possible.
  • And this is just a personal preference, but I find that using an "early return" structure helps streamlining logic and makes understanding the flow of the code easier.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having said all of this, we have to decide if we really want this in lua, now, or in the future, or if a setting is the better approach.

But nontheless, its a good job.

xi.combat.magic.shouldInterruptSpell = function(attacker, defender, spell)
-- Exceptions.
if
defender:getObjType() == xi.objType.TRUST or -- Caster is a trust.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get a recheck on this? I don't believe Trusts are immune to spell interruption.

Image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure the only non interrupted casting action is with movement and no action to prevent casting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants