From 1fd0331521d0a05c56ec21b94ca9d1aebe5929d8 Mon Sep 17 00:00:00 2001 From: Umeboshi Date: Fri, 17 Apr 2026 06:38:57 -0700 Subject: [PATCH] [lua] utils.shadowAbsorb cleanup --- scripts/actions/mobskills/cross_attack.lua | 2 +- scripts/actions/mobskills/nimble_snap.lua | 2 +- scripts/actions/mobskills/savage_blade.lua | 2 +- scripts/utils/combat_utils.lua | 83 +++++++++++++++------- 4 files changed, 59 insertions(+), 30 deletions(-) diff --git a/scripts/actions/mobskills/cross_attack.lua b/scripts/actions/mobskills/cross_attack.lua index 590883b4108..8cabb5882a5 100644 --- a/scripts/actions/mobskills/cross_attack.lua +++ b/scripts/actions/mobskills/cross_attack.lua @@ -14,7 +14,7 @@ mobskillObject.onMobWeaponSkill = function(mob, target, skill, action) local params = {} params.baseDamage = mob:getWeaponDmg() - params.numHits = 1 + params.numHits = 2 params.fTP = { 1.0, 1.0, 1.0 } params.attackType = xi.attackType.PHYSICAL params.damageType = xi.damageType.HTH diff --git a/scripts/actions/mobskills/nimble_snap.lua b/scripts/actions/mobskills/nimble_snap.lua index 774dcb20485..e3ec85caef8 100644 --- a/scripts/actions/mobskills/nimble_snap.lua +++ b/scripts/actions/mobskills/nimble_snap.lua @@ -14,7 +14,7 @@ mobskillObject.onMobWeaponSkill = function(mob, target, skill, action) local params = {} params.baseDamage = mob:getWeaponDmg() - params.numHits = 1 + params.numHits = 3 params.fTP = { 1.0, 1.0, 1.0 } params.attackType = xi.attackType.PHYSICAL params.damageType = xi.damageType.SLASHING diff --git a/scripts/actions/mobskills/savage_blade.lua b/scripts/actions/mobskills/savage_blade.lua index f19fd6ea901..2584abd9370 100644 --- a/scripts/actions/mobskills/savage_blade.lua +++ b/scripts/actions/mobskills/savage_blade.lua @@ -18,7 +18,7 @@ mobskillObject.onMobWeaponSkill = function(mob, target, skill, action) local params = {} params.baseDamage = mob:getWeaponDmg() - params.numHits = 1 + params.numHits = 2 params.fTP = { 2.0, 2.0, 2.0 } -- TODO: Capture fTPs params.attackType = xi.attackType.PHYSICAL params.damageType = xi.damageType.SLASHING diff --git a/scripts/utils/combat_utils.lua b/scripts/utils/combat_utils.lua index 9e7da302d27..f754116c62e 100644 --- a/scripts/utils/combat_utils.lua +++ b/scripts/utils/combat_utils.lua @@ -129,46 +129,75 @@ end ---@param shadowsToRemove number ---@return boolean, number function utils.shadowAbsorb(target, shadowsToRemove) - local shadowType = xi.mod.UTSUSEMI - local targetShadows = target:getMod(xi.mod.UTSUSEMI) - - if targetShadows == 0 then - if - target:getMod(xi.mod.BLINK) == 0 or - math.random(1, 100) > 80 - then - return false, 0 - end + local utsusemiMod = target:getMod(xi.mod.UTSUSEMI) + local blinkMod = target:getMod(xi.mod.BLINK) - shadowType = xi.mod.BLINK - targetShadows = target:getMod(xi.mod.BLINK) + -- Early return: Target has no shadows. + if + utsusemiMod == 0 and + blinkMod == 0 + then + return false, 0 end - local actualConsumed = math.min(targetShadows, shadowsToRemove) - local hadEnoughShadows = targetShadows >= shadowsToRemove + local targetShadows = 0 + local shadowsConsumed = 0 + local absorbHit = false - targetShadows = targetShadows - actualConsumed + -- Utsusemi takes precedence over blink. + if utsusemiMod > 0 then + shadowsConsumed = utils.clamp(shadowsToRemove, 0, utsusemiMod) -- How many shadows were consumed (Used for SHADOW_ABSORB messaging later). + targetShadows = utsusemiMod - shadowsConsumed -- How many shadows left after the attack. + absorbHit = utsusemiMod >= shadowsToRemove -- Check to see if the target had enough shadows to block the attack. - if shadowType == xi.mod.UTSUSEMI then local effect = target:getStatusEffect(xi.effect.COPY_IMAGE) - if effect then - local icons = { xi.effect.COPY_IMAGE, xi.effect.COPY_IMAGE_2, xi.effect.COPY_IMAGE_3 } - - if icons[targetShadows] then - effect:setIcon(icons[targetShadows]) + 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) + elseif targetShadows == 3 then + effect:setIcon(xi.effect.COPY_IMAGE_3) + else + effect:setIcon(xi.effect.COPY_IMAGE_4) -- 4 or more shadows active use the same "4+" icon. end end - end - target:setMod(shadowType, targetShadows) + 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 + absorbHit = false + + return absorbHit, 0 + end + + shadowsConsumed = utils.clamp(shadowsToRemove, 0, blinkMod) -- How many shadows were consumed by the attack (Used for SHADOW_ABSORB messaging later) + targetShadows = blinkMod - shadowsConsumed -- How many shadows left over after the attack. + absorbHit = blinkMod >= shadowsToRemove -- Check to see if the target had enough shadows to fully block the attack. + + if targetShadows == 0 then + target:delStatusEffect(xi.effect.BLINK) + end + + target:setMod(xi.mod.BLINK, targetShadows) + + -- Retail Testing Notes: + -- Tested with WHM spell Blink + -- 1 hit skills took 1 shadow. + -- TODO: When hit by a 2 hit skill, it was observed to consume 2 blink shadows, however the message returned was SKILL_MISS rather than SHADOW_ABSORB. + -- Did not block 3+ hit mob skills. (Player Weaponskills untested) + -- AOE skills delete Blink. - if targetShadows == 0 then - target:delStatusEffect(xi.effect.COPY_IMAGE) - target:delStatusEffect(xi.effect.BLINK) + -- TODO: Test Zephyr Mantle proc rate. + -- TODO: Test player Weapon Skills on mob/players with Blink/Zephyr Mantle. + -- Note: JPWiki/FFXIPedia repeatedly mentions that Blink can block multi hit skills, but this was not observed in testing. Needs further testing. end - return hadEnoughShadows, actualConsumed + return absorbHit, shadowsConsumed end -- Calculates Phalanx damage reduction.