Skip to content

[core][lua] Physical Mobskill Refactor#9738

Merged
WinterSolstice8 merged 1 commit into
LandSandBoat:basefrom
UmeboshiXI:Mobskills_Physical
Apr 17, 2026
Merged

[core][lua] Physical Mobskill Refactor#9738
WinterSolstice8 merged 1 commit into
LandSandBoat:basefrom
UmeboshiXI:Mobskills_Physical

Conversation

@UmeboshiXI
Copy link
Copy Markdown
Contributor

@UmeboshiXI UmeboshiXI commented Apr 8, 2026

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?

Submitting as draft for now. Want to bounce it off automated checks while I sleep and see where it stands/get eyes on it. Will return to fix/consolidate things in morning.

Aware of debug prints being present, will remove/comment out before final undrafted PR but leaving in at the moment for testing purposes.

Did extensive testing on my local but will need to retest based on further changes/adjustments so leaving that box unchecked for now.

1. Reworks how shadow consumption is handled.

  • Damage is no longer influenced by the shadows consumed.

  • Based on retail testing, shadow consumption for mobskills was found to not be affected by hit rate checks.

  • Based on retail testing, shadows are not calculated per hit. A single hit skill that takes 3 shadows will always take 3 shadows.

2. Reworked AoE skill shadow mitigation mechanic based on retail testing.

  • Physical AoE skills that do not ignore/wipe shadows have a chance to not consume the normal shadow amount.

  • The amount of shadow consumption mitigated is based on how many shadows a skill would normally take.

  • The amount of shadows a skill would normally take acts as a counter for how many mitigation attempts will be made. Example: a skill would take 4 shadows will have 4 attempts made for mitigation.

  • Mitigation can not reduce shadow consumption to 0. 1 shadow will always be taken at a minimum.

  • Test data can be found here and here.

3. Refactors Physical/Ranged mobskills

  • Adds/extends support for various parameters.

  • Retires obsolete xi.mobskills.mobPhysicalHit() function. Superseded by xi.mobskills.processDamage() which checks if the skill's hits landed.

  • takeDamage() is now wrapped with xi.mobskills.processDamage() to prevent shadow chip damage. Status Effect applications are also gated in the same way. Closes related issue: 🐛 xi.msg.basic.SHADOWS_TAKEN results not functioning properly for mobskills. #8880

  • Retires obsolete xi.mobskills.mobPhysicalStatusEffectMove() function. Superseded with usage pattern of mobStatusEffectMove() wrapped with xi.mobskills.processDamage().

  • Retires obsolete mobPhysicalDrainMove() function. Superseded with usage pattern of mobDrainMove() wrapped with xi.mobskills.processDamage().

  • Physical skills now calculate individual hits and record hit information that can be called later.

  • Ranged skills now use ranged PDIF, FSTR, and Crit functions.

  • Fixes ranged skill TP Gain by adjusting the getBaseRangedDelay() for mobs. Based on retail testing, mob ranged delay seems to be the same for all normal mobs recorded. Test Data can be found here.

  • Added missing trySkillUp() checks for Evasion. This is calculated per hit based on retail.

  • Adjusted Shield Mastery checks for shield blocks. We previously gated this behind having the Shield Mastery job trait but through retail testing, it was found that it could still proc if you had equipment that had Shield Mastery mods.

  • Implements skill data from Jimmayus's spreadsheet where applicable. Recent PRs that had updated capture data were kept/took priority.

  • Added mob families/notes to each skill's script for easy reference.

  • Added TODO: notes to mobskill scripts where we need capture data or code adjustments.

Steps to test these changes

Tested these steps on my local and were working then. Will retest again if I make further edits.

  1. Test/play around with parameters. Nothing should error out. Each one should affect its related stat as expected.

  2. Test shadow consumption.

  • Single hit skills should simply take the amount of shadows stated in the skill's script. If the player has enough shadows to block the skill, it gets absorbed. If not, they take full damage.
  • AOE/Conal physical/ranged skills that do not ignore/wipe shadows can have varying shadows taken due to mitigation mechanics. A skill that would take 4 shadows can range from 1-4 shadows taken.
  • Shadows are calculated before hitrate checks and are not calculated per hit. This means the function is ran once and is an all or nothing affair.
  1. Test Third Eye.
  • Third Eye now blocks single target multi hit skills. If it procs, it will exit the attack loop in event of multi hits.
  • Third Eye does not block and is wiped by AOE mob skills.
  • Third Eye does not interact with Magical/Breath mob skills.
  1. Test ranged skill blocks/parries/guards
  • Currently, ranged skills should not be able to be parried/blocked/guarded. The appropriate params are set to skip these functions in the skill scripts scripts. Left the possibility of toggling it on in event we find edge cases.
  1. Test skill messaging for skill attacks.
  • param.primaryMessage can be used to override the default damage message. This is mostly used for skill attacks.
  • Supports appropriate corresponding miss message based on the primaryMessage type.

-- Fallback if we somehow got this far.
return false, shadowsToRemove
end

Copy link
Copy Markdown
Contributor

@Xaver-DaRed Xaver-DaRed Apr 8, 2026

Choose a reason for hiding this comment

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

I see a few issues with current implementation. Most notably, how we delete both blink and utsusemi. Blink may have not triggered and hence shouldnt be consumed.

Proposal:

function utils.shadowAbsorb(target, shadowsToRemove)
    local utsusemiMod = target:getMod(xi.mod.UTSUSEMI)
    local blinkMod    = target:getMod(xi.mod.BLINK)

    -- Early return: Target has no shadows.
    if
        utsusemiMod == 0 and
        blinkMod == 0
    then
        return false, 0
    end

    local targetShadows = 0

    -- Utsusemi takes precedence over blink.
    if utsusemiMod > 0 then
        targetShadows = utsusemiMod

        local effect = target:getStatusEffect(xi.effect.COPY_IMAGE)
        if effect then
            if targetShadows == 0 then
                target:delStatusEffect(xi.effect.COPY_IMAGE)
            elseif targetShadows == 1 then
                effect:setIcon(xi.effect.COPY_IMAGE)
            elseif targetShadows == 2 then
                effect:setIcon(xi.effect.COPY_IMAGE_2)
            else
                effect:setIcon(xi.effect.COPY_IMAGE_3)
            end
        end

        target:setMod(xi.mod.UTSUSEMI, targetShadows)

    -- Blink has a random chance of triggering when no utsusemi is present.
    elseif blinkMod > 0 then
        if math.random(1, 100) <= 20 then
            return false, 0
        end

        targetShadows = blinkMod

        if targetShadows == 0 then
            target:delStatusEffect(xi.effect.BLINK)
        end

        target:setMod(xi.mod.BLINK, targetShadows)
    end

    local actualConsumed = utils.clamp(shadowsToRemove, 0, targetShadows)
    local absorbHit      = targetShadows >= shadowsToRemove

    return absorbHit, actualConsumed
end

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.

As far as I know those effects are mutually exclusive. You can either have blink or utsusemi

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Cleaned up the code a bit but Blink and Utsusemi are exclusive and can not be on at the same time.

Comment thread scripts/globals/combat/physical_utilities.lua
Comment thread scripts/globals/mobskills.lua
Comment thread scripts/globals/mobskills.lua Outdated
hitChance = xi.combat.physicalHitRate.getRangedHitRate(mob, target, accuracyModifier + targetSpecialAttackEvasion, false)
end

print('hitChance', hitChance)
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.

leftover print

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed all my debug prints as of now.

Comment thread scripts/globals/mobskills.lua Outdated
Comment on lines +739 to +776
-- Defaults
local damage = utils.defaultIfNil(skillParams.baseDamage, mob:getWeaponDmg())
local numHits = utils.defaultIfNil(skillParams.numHits, 1)
local fTPScale = utils.defaultIfNil(skillParams.fTP, { 1.00, 1.00, 1.00 })
local fTPSubsequentHits = utils.defaultIfNil(skillParams.fTPSubsequentHits, { 1.00, 1.00, 1.00 })
local fTPBonus = utils.defaultIfNil(skillParams.fTPBonus, 0)
local attackMultiplierfTP = utils.defaultIfNil(skillParams.attackMultiplier, { 1.00, 1.00, 1.00 })
local accuracyModifierfTP = utils.defaultIfNil(skillParams.accuracyModifier, { 0, 0, 0 })
local guaranteedFirstHit = utils.defaultIfNil(skillParams.guaranteedFirstHit and true, false)
local canCrit = utils.defaultIfNil(skillParams.canCrit, false)
local criticalChance = utils.defaultIfNil(skillParams.criticalChance, { 0.00, 0.00, 0.00 })
local ignoreDefense = utils.defaultIfNil(skillParams.ignoreDefense, { 0.00, 0.00, 0.00 })
local attackType = utils.defaultIfNil(skillParams.attackType, xi.attackType.PHYSICAL)
local damageType = utils.defaultIfNil(skillParams.damageType, xi.damageType.SLASHING)
local hybridSkill = utils.defaultIfNil(skillParams.hybridSkill, false)
local hybridSkillElement = utils.defaultIfNil(skillParams.hybridSkillElement, xi.element.NONE)
local hybridAttackType = utils.defaultIfNil(skillParams.hybridAttackType, xi.attackType.MAGICAL)
local hybridDamageType = utils.defaultIfNil(skillParams.hybridDamageType, xi.damageType.ELEMENTAL)
local shadowsToRemove = utils.defaultIfNil(skillParams.shadowBehavior, xi.mobskills.shadowBehavior.NUMSHADOWS_1)
local skipStoneskin = utils.defaultIfNil(skillParams.skipStoneskin, false)
local skipYaegasumi = utils.defaultIfNil(skillParams.skipYaegasumi, false)
local skipFSTR = utils.defaultIfNil(skillParams.skipFSTR, false)
local skipPDIF = utils.defaultIfNil(skillParams.skipPDIF, false)
local skipParry = utils.defaultIfNil(skillParams.skipParry, false)
local skipGuard = utils.defaultIfNil(skillParams.skipGuard, false)
local skipBlock = utils.defaultIfNil(skillParams.skipBlock, false)
local primaryMessage = utils.defaultIfNil(skillParams.primaryMessage, xi.msg.basic.DAMAGE)

-- Initialize return structure
returnInfo.damage = 0
returnInfo.hybridDamage = 0
returnInfo.hitsLanded = 0
returnInfo.attackType = attackType
returnInfo.damageType = damageType
returnInfo.hybridAttackType = hybridAttackType
returnInfo.hybridDamageType = hybridDamageType
returnInfo.isCritical = false
returnInfo.hitData = {}
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 see these default values are used in both ranged and physical skills but you define them differently.

Example: You have utils.defaultIfNil(skillParams.skipStoneskin, false) in here but in the other one you have utils.defaultIfNil(skillParams.skipStoneskin and true, false)

Since these appear to the be the exact same you can make a local helper function to return all these values which will lower the complexity and remove duplicated code. I am not sure if these need to be different in the future but as its proposed they appear to be identical

@UmeboshiXI UmeboshiXI force-pushed the Mobskills_Physical branch 5 times, most recently from 76de338 to a31c635 Compare April 10, 2026 11:28
Comment thread src/map/entities/battleentity.cpp Outdated
@@ -2827,7 +2827,7 @@ void CBattleEntity::OnMobSkillFinished(CMobSkillState& state, action_t& action)
if (PSkill->hasMissMsg())
{
result.resolution = ActionResolution::Miss;
result.param = 0;
result.param = damage;
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.

What did you attempt to fix with this change? Miss action packets always have param set to 0.

This is what's causing your tests to fail.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Test passed after implementing the fix in CBattleEntity::OnMobSkillFinished()

@UmeboshiXI UmeboshiXI force-pushed the Mobskills_Physical branch 4 times, most recently from e0e6986 to 963b829 Compare April 12, 2026 10:47
@UmeboshiXI UmeboshiXI marked this pull request as ready for review April 12, 2026 11:30
@UmeboshiXI UmeboshiXI changed the title [core][lua] Physical Mobskll Refactor [core][lua] Physical Mobskill Refactor Apr 12, 2026
@LandSandBoat LandSandBoat deleted a comment from github-actions Bot Apr 17, 2026
Comment thread scripts/globals/mobskills.lua
@WinterSolstice8 WinterSolstice8 merged commit 0971a5d into LandSandBoat:base Apr 17, 2026
10 checks passed
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.

5 participants