* [lua] Add 'hitsLanded' = 1 to calcparams of magic ws * [lua] Adjust Atonement & Spirits Within to have hitsLanded = 1
1086 lines
46 KiB
Lua
1086 lines
46 KiB
Lua
-----------------------------------
|
|
-- Contains all common weaponskill calculations including but not limited to:
|
|
-- fSTR
|
|
-- Alpha
|
|
-- Ratio -> cRatio
|
|
-- min/max cRatio
|
|
-- applications of fTP
|
|
-- applications of critical hits ('Critical hit rate varies with TP.')
|
|
-- applications of accuracy mods ('Accuracy varies with TP.')
|
|
-- applications of damage mods ('Damage varies with TP.')
|
|
-- performance of the actual WS (rand numbers, etc)
|
|
-----------------------------------
|
|
require('scripts/globals/magicburst')
|
|
require('scripts/globals/ability')
|
|
require('scripts/globals/magic')
|
|
require('scripts/globals/combat/physical_utilities')
|
|
-----------------------------------
|
|
xi = xi or {}
|
|
xi.weaponskills = xi.weaponskills or {}
|
|
|
|
local function shadowAbsorb(target)
|
|
local targetShadows = target:getMod(xi.mod.UTSUSEMI)
|
|
local shadowType = xi.mod.UTSUSEMI
|
|
|
|
if targetShadows == 0 then
|
|
if math.random(1, 100) <= 80 then
|
|
targetShadows = target:getMod(xi.mod.BLINK)
|
|
shadowType = xi.mod.BLINK
|
|
end
|
|
end
|
|
|
|
if targetShadows > 0 then
|
|
targetShadows = targetShadows - 1
|
|
|
|
if shadowType == xi.mod.UTSUSEMI then
|
|
local effect = target:getStatusEffect(xi.effect.COPY_IMAGE)
|
|
if effect then
|
|
if 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)
|
|
end
|
|
end
|
|
end
|
|
|
|
target:setMod(shadowType, targetShadows)
|
|
if targetShadows == 0 then
|
|
target:delStatusEffect(xi.effect.COPY_IMAGE)
|
|
target:delStatusEffect(xi.effect.BLINK)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function getMultiAttacks(attacker, target, wsParams, firstHit, offHand)
|
|
local numHits = 0
|
|
local bonusHits = 0
|
|
local doubleRate = attacker:getMod(xi.mod.DOUBLE_ATTACK) + attacker:getMerit(xi.merit.DOUBLE_ATTACK_RATE)
|
|
local tripleRate = attacker:getMod(xi.mod.TRIPLE_ATTACK) + attacker:getMerit(xi.merit.TRIPLE_ATTACK_RATE)
|
|
local quadRate = attacker:getMod(xi.mod.QUAD_ATTACK)
|
|
local oaThriceRate = attacker:getMod(xi.mod.MYTHIC_OCC_ATT_THRICE)
|
|
local oaTwiceRate = attacker:getMod(xi.mod.MYTHIC_OCC_ATT_TWICE)
|
|
local isJump = wsParams.isJump or false
|
|
|
|
if isJump then
|
|
doubleRate = doubleRate + attacker:getMod(xi.mod.JUMP_DOUBLE_ATTACK)
|
|
end
|
|
|
|
-- TODO: Assasin vest +2 Ambush augment.
|
|
-- The logic here wasnt actually checking for the augment.
|
|
-- Also, it was in a completely different scale, making triple attack trigger always.
|
|
|
|
if math.random(1, 100) <= quadRate then
|
|
bonusHits = bonusHits + 3
|
|
elseif math.random(1, 100) <= tripleRate then
|
|
bonusHits = bonusHits + 2
|
|
elseif math.random(1, 100) <= doubleRate then
|
|
bonusHits = bonusHits + 1
|
|
elseif firstHit and math.random(1, 100) <= oaThriceRate then -- Can only proc on first hit
|
|
bonusHits = bonusHits + 2
|
|
elseif firstHit and math.random(1, 100) <= oaTwiceRate then -- Can only proc on first hit
|
|
bonusHits = bonusHits + 1
|
|
end
|
|
|
|
attacker:delStatusEffect(xi.effect.ASSASSINS_CHARGE)
|
|
attacker:delStatusEffect(xi.effect.WARRIORS_CHARGE)
|
|
|
|
-- Try OaX for Jumps
|
|
-- ... What's the correct dual wield interaction?
|
|
if isJump and bonusHits == 0 and attacker:isPC() then
|
|
-- getWeaponHitCount will always return 1 if there's a weapon in the slot, which is already accounted for.
|
|
if offHand then
|
|
bonusHits = attacker:getWeaponHitCount(true) - 1
|
|
else
|
|
bonusHits = attacker:getWeaponHitCount(false) - 1
|
|
end
|
|
end
|
|
|
|
numHits = numHits + bonusHits
|
|
|
|
return numHits
|
|
end
|
|
|
|
---@param attacker CBaseEntity
|
|
---@param target CBaseEntity
|
|
---@param bonus number
|
|
---@return number
|
|
xi.weaponskills.getRangedHitRate = function(attacker, target, bonus)
|
|
return xi.combat.physicalHitRate.getRangedHitRate(attacker, target, bonus, true)
|
|
end
|
|
|
|
-- Function to calculate if a hit in a WS misses, criticals, and the respective damage done
|
|
local function getSingleHitDamage(attacker, target, dmg, ftp, wsParams, calcParams)
|
|
local criticalHit = false
|
|
local hitDamage = 0
|
|
local atkMultiplier = xi.weaponskills.fTP(calcParams.tpUsed, wsParams.atkVaries)
|
|
local ignoreDefMultiplier = xi.weaponskills.fTP(calcParams.tpUsed, wsParams.ignoredDefense)
|
|
local applyLevelCorrection = xi.data.levelCorrection.isLevelCorrectedZone(attacker)
|
|
local ignoresDefense = (wsParams.ignoredDefense ~= nil) -- if the table exists, it ignores defense
|
|
|
|
-- local pdif = 0 Reminder for Future Implementation!
|
|
|
|
-- priority order of checks
|
|
-- evade > parry > shadow/blink > guard/block
|
|
|
|
-- check evasion
|
|
local missChance = math.random()
|
|
if
|
|
(missChance > calcParams.hitRate and
|
|
not calcParams.guaranteedHit) or
|
|
calcParams.mustMiss
|
|
then
|
|
-- miss logic
|
|
return hitDamage, calcParams
|
|
end
|
|
|
|
-- check parry
|
|
if
|
|
calcParams.attackType == xi.attackType.PHYSICAL and
|
|
not calcParams.guaranteedHit and
|
|
xi.combat.physical.isParried(target, attacker)
|
|
then
|
|
-- parried logic
|
|
return hitDamage, calcParams
|
|
end
|
|
|
|
-- check shadows
|
|
if
|
|
not calcParams.guaranteedHit and
|
|
not wsParams.ignoreShadows and
|
|
shadowAbsorb(target)
|
|
then
|
|
-- shadow absorb logic
|
|
calcParams.shadowsAbsorbed = calcParams.shadowsAbsorbed + 1
|
|
return hitDamage, calcParams
|
|
end
|
|
|
|
local critChance = math.random() -- See if we land a critical hit
|
|
criticalHit = (wsParams.critVaries and critChance <= calcParams.critRate) or
|
|
calcParams.forcedFirstCrit or
|
|
calcParams.mightyStrikesApplicable
|
|
|
|
if criticalHit then
|
|
calcParams.criticalHit = true
|
|
end
|
|
|
|
if calcParams.attackType == xi.attackType.PHYSICAL then
|
|
calcParams.pdif = xi.combat.physical.calculateMeleePDIF(attacker, target, calcParams.attackInfo.weaponType, atkMultiplier, criticalHit, applyLevelCorrection, ignoresDefense, ignoreDefMultiplier, true, calcParams.attackInfo.slot, false)
|
|
else
|
|
calcParams.pdif = xi.combat.physical.calculateRangedPDIF(attacker, target, calcParams.skillType, atkMultiplier, criticalHit, applyLevelCorrection, ignoresDefense, ignoreDefMultiplier, true, 0)
|
|
end
|
|
|
|
hitDamage = (dmg + xi.combat.damage.consumeManaAddition(attacker)) * ftp * calcParams.pdif
|
|
|
|
-- handle blocking and reduce the hit damage if needed
|
|
if xi.combat.physical.isBlocked(target, attacker) then
|
|
hitDamage = hitDamage - xi.combat.physical.getDamageReductionForBlock(target, attacker, hitDamage)
|
|
end
|
|
|
|
-- handle guard and reduce the hit damage if needed
|
|
if
|
|
calcParams.attackType == xi.attackType.PHYSICAL and
|
|
xi.combat.physical.isGuarded(target, attacker)
|
|
then
|
|
calcParams.pdif = math.max(calcParams.pdif - 1.0, 0)
|
|
calcParams.guardedHits = calcParams.guardedHits + 1
|
|
end
|
|
|
|
calcParams.hitsLanded = calcParams.hitsLanded + 1
|
|
|
|
return hitDamage, calcParams
|
|
end
|
|
|
|
local function modifyMeleeHitDamage(attacker, target, attackTbl, wsParams, rawDamage)
|
|
local adjustedDamage = rawDamage
|
|
|
|
if not wsParams.formless then
|
|
adjustedDamage = target:physicalDmgTaken(adjustedDamage, attackTbl.damageType)
|
|
|
|
if attackTbl.weaponType == xi.skill.HAND_TO_HAND then
|
|
adjustedDamage = adjustedDamage * (1 + target:getMod(xi.mod.HTH_SDT) / 10000)
|
|
elseif
|
|
attackTbl.weaponType == xi.skill.DAGGER or
|
|
attackTbl.weaponType == xi.skill.POLEARM
|
|
then
|
|
adjustedDamage = adjustedDamage * (1 + target:getMod(xi.mod.PIERCE_SDT) / 10000)
|
|
elseif
|
|
attackTbl.weaponType == xi.skill.CLUB or
|
|
attackTbl.weaponType == xi.skill.STAFF
|
|
then
|
|
adjustedDamage = adjustedDamage * (1 + target:getMod(xi.mod.IMPACT_SDT) / 10000)
|
|
else
|
|
adjustedDamage = adjustedDamage * (1 + target:getMod(xi.mod.SLASH_SDT) / 10000)
|
|
end
|
|
end
|
|
|
|
-- Scarlet Delirium
|
|
adjustedDamage = adjustedDamage * xi.combat.damage.scarletDeliriumMultiplier(attacker)
|
|
|
|
-- Souleater
|
|
adjustedDamage = adjustedDamage + xi.combat.damage.souleaterAddition(attacker)
|
|
|
|
adjustedDamage = utils.handlePhalanx(target, adjustedDamage)
|
|
adjustedDamage = utils.handleStoneskin(target, adjustedDamage)
|
|
|
|
return adjustedDamage
|
|
end
|
|
|
|
-- Compute magic damage component of hybrid weaponskill
|
|
-- https://wiki.ffo.jp/html/1261.html
|
|
-- https://www.ffxiah.com/forum/topic/33470/the-sealed-dagger-a-ninja-guide/151/#3420836
|
|
-- https://www.ffxiah.com/forum/topic/49614/blade-chi-damage-formula/2/#3171538
|
|
local function calculateHybridMagicDamage(tp, physicaldmg, attacker, target, wsParams, calcParams, wsID)
|
|
local ftp = xi.weaponskills.fTP(tp, wsParams.ftpMod)
|
|
local magicdmg = math.floor(physicaldmg * ftp + attacker:getMod(xi.mod.MAGIC_DAMAGE))
|
|
local wsd = attacker:getMod(xi.mod.ALL_WSDMG_ALL_HITS)
|
|
|
|
if attacker:getMod(xi.mod.WEAPONSKILL_DAMAGE_BASE + wsID) > 0 then
|
|
wsd = wsd + attacker:getMod(xi.mod.WEAPONSKILL_DAMAGE_BASE + wsID)
|
|
end
|
|
|
|
magicdmg = math.floor(magicdmg * (100 + wsd) / 100)
|
|
magicdmg = math.floor(addBonusesAbility(attacker, wsParams.ele, target, magicdmg, wsParams))
|
|
magicdmg = math.floor(magicdmg + calcParams.bonusfTP * physicaldmg)
|
|
magicdmg = math.floor(magicdmg * xi.combat.magicHitRate.calculateResistRate(attacker, target, 0, wsParams.skill, 0, wsParams.ele, 0, 0, calcParams.bonusAcc))
|
|
magicdmg = math.floor(magicdmg * xi.combat.damage.calculateDamageAdjustment(target, false, true, false, false))
|
|
magicdmg = math.floor(target:handleSevereDamage(magicdmg, false))
|
|
|
|
if magicdmg > 0 then
|
|
magicdmg = math.floor(magicdmg * xi.spells.damage.calculateAbsorption(target, wsParams.ele, true))
|
|
magicdmg = math.floor(magicdmg * xi.spells.damage.calculateNullification(target, wsParams.ele, true, false))
|
|
end
|
|
|
|
if magicdmg > 0 then -- handle nonzero damage if previous function does not absorb or nullify
|
|
magicdmg = utils.handlePhalanx(target, magicdmg)
|
|
magicdmg = utils.handleOneForAll(target, magicdmg)
|
|
magicdmg = utils.handleStoneskin(target, magicdmg)
|
|
end
|
|
|
|
return math.floor(magicdmg)
|
|
end
|
|
|
|
-- returns ammo used, if any
|
|
local function useAmmo(attacker)
|
|
if xi.combat.ranged.shouldUseAmmo(attacker) then
|
|
return 1
|
|
end
|
|
|
|
return 0
|
|
end
|
|
|
|
-- Calculates the raw damage for a weaponskill, used by both xi.weaponskills.doPhysicalWeaponskill and xi.weaponskills.doRangedWeaponskill.
|
|
-- Behavior of damage calculations can vary based on the passed in calcParams, which are determined by the calling function
|
|
-- depending on the type of weaponskill being done, and any special cases for that weaponskill
|
|
--
|
|
-- wsParams can contain: ftpMod, str_wsc, dex_wsc, vit_wsc, int_wsc, mnd_wsc, critVaries,
|
|
-- accVaries, ignoredDefense, atkVaries, kick, hybridWS, hitsHigh, formless
|
|
--
|
|
-- See xi.weaponskills.doPhysicalWeaponskill or xi.weaponskills.doRangedWeaponskill for how calcParams are determined.
|
|
|
|
-- TODO: Reduce complexity
|
|
-- Disable cyclomatic complexity check for this function:
|
|
-- luacheck: ignore 561
|
|
xi.weaponskills.calculateRawWSDmg = function(attacker, target, wsID, tp, action, wsParams, calcParams)
|
|
local targetLvl = target:getMainLvl()
|
|
local targetHp = target:getHP() + target:getMod(xi.mod.STONESKIN)
|
|
|
|
-- Obtains alpha, used for working out WSC on legacy servers. Retail has no alpha anymore as of 2014 Weaponskill functions
|
|
local alpha = 1
|
|
if not xi.settings.main.USE_ADOULIN_WEAPON_SKILL_CHANGES then
|
|
local level = attacker:getMainLvl()
|
|
if level > 75 then
|
|
alpha = 0.85
|
|
elseif level > 59 then
|
|
alpha = 0.9 - math.floor((level - 60) / 2) / 100
|
|
elseif level > 5 then
|
|
alpha = 1 - math.floor(level / 6) / 100
|
|
end
|
|
end
|
|
|
|
local wsc = xi.combat.physical.calculateWSC(attacker, wsParams.str_wsc, wsParams.dex_wsc, wsParams.vit_wsc, wsParams.agi_wsc, wsParams.int_wsc, wsParams.mnd_wsc, wsParams.chr_wsc)
|
|
local mainBase = math.floor(calcParams.weaponDamage[1] + calcParams.fSTR + calcParams.bonusWSmods + wsc * alpha)
|
|
|
|
-- Calculate fTP multiplier
|
|
local ftp = xi.weaponskills.fTP(tp, wsParams.ftpMod) + calcParams.bonusfTP
|
|
if calcParams.hybridHit then
|
|
ftp = 1 + calcParams.bonusfTP
|
|
end
|
|
|
|
-- Calculate critrates
|
|
calcParams.critRate = 0
|
|
if wsParams.critVaries then -- Work out critical hit ratios
|
|
calcParams.critRate = xi.combat.physical.calculateSwingCriticalRate(attacker, target, tp, calcParams.attackInfo.slot, wsParams.critVaries)
|
|
end
|
|
|
|
-- Start the WS
|
|
local hitsDone = 1
|
|
local hitdmg = 0
|
|
local finaldmg = 0
|
|
local mainhandTPGain = xi.combat.tp.getSingleWeaponTPReturn(attacker, xi.slot.MAIN) -- TODO: are these calculated wrong? ((delay1+delay2)/2 * 1 - DW%) = tp return for both hands?
|
|
local subTPGain = xi.combat.tp.getSingleWeaponTPReturn(attacker, xi.slot.SUB) --
|
|
local isJump = wsParams.isJump or false
|
|
local attackerTPMult = wsParams.attackerTPMult or 1
|
|
local ammoCount = -1
|
|
local ammoUsed = 0
|
|
local isRanged = calcParams.attackInfo.slot == xi.slot.RANGED
|
|
calcParams.hitsLanded = 0
|
|
calcParams.mainHitsLanded = 0
|
|
calcParams.tpHitsLanded = 0
|
|
calcParams.shadowsAbsorbed = 0
|
|
calcParams.mainhandHitsLanded = 0
|
|
calcParams.offhandHitsLanded = 0
|
|
calcParams.guardedHits = 0
|
|
|
|
-- Get ammo information
|
|
if isRanged and attacker:isPC() then
|
|
local ammoItem = attacker:getEquippedItem(xi.slot.AMMO)
|
|
if ammoItem then
|
|
ammoCount = ammoItem:getQuantity()
|
|
else
|
|
ammoCount = 0
|
|
end
|
|
end
|
|
|
|
-- Calculate the damage from the first hit
|
|
if
|
|
not isJump and
|
|
calcParams.firstHitRate
|
|
then
|
|
calcParams.origHitRate = calcParams.hitRate
|
|
calcParams.hitRate = calcParams.firstHitRate
|
|
end
|
|
|
|
local dmg = mainBase
|
|
hitdmg, calcParams = getSingleHitDamage(attacker, target, dmg, ftp, wsParams, calcParams)
|
|
|
|
if
|
|
not isJump and
|
|
calcParams.origHitRate
|
|
then
|
|
calcParams.hitRate = calcParams.origHitRate
|
|
end
|
|
|
|
if calcParams.melee then
|
|
hitdmg = modifyMeleeHitDamage(attacker, target, calcParams.attackInfo, wsParams, hitdmg)
|
|
end
|
|
|
|
if calcParams.skillType and hitdmg > 0 then
|
|
attacker:trySkillUp(calcParams.skillType, targetLvl)
|
|
|
|
if isJump then
|
|
attacker:addTP(mainhandTPGain * attackerTPMult)
|
|
end
|
|
|
|
calcParams.tpHitsLanded = 1 -- Store number of TP hits that have landed thus far
|
|
calcParams.mainHitsLanded = 0
|
|
end
|
|
|
|
finaldmg = finaldmg + hitdmg
|
|
|
|
-- Finish first/mainhand hit
|
|
|
|
local numMainHandMultis = isRanged and 0 or getMultiAttacks(attacker, target, wsParams, true, false)
|
|
local numOffhandMultis = 0
|
|
local numMultiProcs = numMainHandMultis > 0 and 1 or 0
|
|
|
|
-- Have to calculate added bonus for SA/TA here since it is done outside of the fTP multiplier
|
|
if attacker:getMainJob() == xi.job.THF then
|
|
-- Add DEX/AGI bonus to base damage of first hit if THF main and valid Sneak/Trick Attack
|
|
if calcParams.sneakApplicable then
|
|
local dexFactor = math.floor(attacker:getStat(xi.mod.DEX) * (1 + attacker:getMod(xi.mod.SNEAK_ATK_DEX) / 100))
|
|
finaldmg = math.floor(finaldmg + calcParams.pdif * dexFactor)
|
|
end
|
|
|
|
if calcParams.trickApplicable then
|
|
local agiFactor = math.floor(attacker:getStat(xi.mod.AGI) * (1 + attacker:getMod(xi.mod.TRICK_ATK_AGI) / 100))
|
|
finaldmg = math.floor(finaldmg + calcParams.pdif * agiFactor)
|
|
end
|
|
end
|
|
|
|
-- these are deliberately left outside of the "If main job is THF" if-statement
|
|
if calcParams.sneakApplicable then
|
|
finaldmg = math.floor(finaldmg * (1 + attacker:getMod(xi.mod.AUGMENTS_SA) / 100))
|
|
end
|
|
|
|
if calcParams.trickApplicable then
|
|
finaldmg = math.floor(finaldmg * (1 + attacker:getMod(xi.mod.AUGMENTS_TA) / 100))
|
|
end
|
|
|
|
-- We've now accounted for any crit from SA/TA, so nullify them
|
|
calcParams.forcedFirstCrit = false
|
|
|
|
-- For items that apply bonus damage to the first hit of a weaponskill (but not later hits),
|
|
-- store bonus damage for first hit, for use after other calculations are done
|
|
local firstHitBonus = finaldmg * attacker:getMod(xi.mod.ALL_WSDMG_FIRST_HIT) / 100
|
|
|
|
-- Reset fTP if it's not supposed to carry over across all hits for this WS
|
|
-- We'll recalculate our mainhand damage after doing offhand
|
|
if not wsParams.multiHitfTP then
|
|
ftp = 1
|
|
end
|
|
|
|
local offhandSkill = attacker:getWeaponSkillType(xi.slot.SUB)
|
|
local isH2H = false
|
|
if calcParams.skillType == xi.skill.HAND_TO_HAND then
|
|
offhandSkill = xi.skill.HAND_TO_HAND
|
|
subTPGain = mainhandTPGain
|
|
isH2H = true
|
|
end
|
|
|
|
calcParams.guaranteedHit = false -- Accuracy bonus from SA/TA applies only to first main and offhand hit
|
|
|
|
dmg = mainBase
|
|
|
|
-- First mainhand hit is already accounted for
|
|
local mainhandHits = wsParams.numHits - 1
|
|
local mainhandHitsDone = 0
|
|
|
|
if isRanged and ammoCount ~= -1 then
|
|
ammoUsed = ammoUsed + useAmmo(attacker)
|
|
|
|
if ammoUsed >= ammoCount then
|
|
hitsDone = 8 -- Attack while loops will stop if hitsDone is 8 or higher
|
|
end
|
|
end
|
|
|
|
-- Use up any remaining hits in the WS's numhits
|
|
while hitsDone < 8 and mainhandHitsDone < mainhandHits and finaldmg < targetHp do
|
|
hitdmg, calcParams = getSingleHitDamage(attacker, target, dmg, ftp, wsParams, calcParams)
|
|
|
|
if calcParams.melee then
|
|
hitdmg = modifyMeleeHitDamage(attacker, target, calcParams.attackInfo, wsParams, hitdmg)
|
|
end
|
|
|
|
if hitdmg > 0 then
|
|
attacker:trySkillUp(calcParams.skillType, targetLvl)
|
|
|
|
-- When dual wielding, the mainhand appears to count the second hit as a TP hit unless it's a 1 hit WS where the offhand will gain TP
|
|
-- H2H also does this on retail (much more easy to verify)
|
|
-- Needs better verification
|
|
if (isH2H or calcParams.extraOffhandHit) and hitsDone == 1 then
|
|
calcParams.tpHitsLanded = calcParams.tpHitsLanded + 1
|
|
elseif wsParams.isBarrage then
|
|
calcParams.tpHitsLanded = calcParams.tpHitsLanded + 1
|
|
else -- Otherwise, add a hit to the "extra" hits which is 10 tp each
|
|
calcParams.mainHitsLanded = calcParams.mainHitsLanded + 1
|
|
end
|
|
|
|
if isJump then
|
|
attacker:addTP(mainhandTPGain * attackerTPMult)
|
|
end
|
|
end
|
|
|
|
finaldmg = finaldmg + hitdmg
|
|
hitsDone = hitsDone + 1
|
|
mainhandHitsDone = mainhandHitsDone + 1
|
|
|
|
-- Check each hit for multis, but stop after we get 2 multi procs
|
|
if numMultiProcs < 2 then
|
|
local extraMultis = isRanged and 0 or getMultiAttacks(attacker, target, wsParams, false, false)
|
|
numMainHandMultis = numMainHandMultis + extraMultis
|
|
numMultiProcs = extraMultis > 0 and numMultiProcs + 1 or numMultiProcs
|
|
end
|
|
|
|
if isRanged and ammoCount ~= -1 then
|
|
ammoUsed = ammoUsed + useAmmo(attacker)
|
|
|
|
if ammoUsed >= ammoCount then
|
|
hitsDone = 8 -- Attack while loops will stop if hitsDone is 8 or higher
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Proc any mainhand multi attacks.
|
|
local mainhandMultiHitsDone = 0
|
|
|
|
while hitsDone < 8 and mainhandMultiHitsDone < numMainHandMultis and finaldmg < targetHp do
|
|
hitdmg, calcParams = getSingleHitDamage(attacker, target, dmg, ftp, wsParams, calcParams)
|
|
|
|
if calcParams.melee then
|
|
hitdmg = modifyMeleeHitDamage(attacker, target, calcParams.attackInfo, wsParams, hitdmg)
|
|
end
|
|
|
|
if hitdmg > 0 then
|
|
attacker:trySkillUp(calcParams.skillType, targetLvl)
|
|
|
|
-- When dual wielding, the mainhand appears to count the second hit as a TP hit unless it's a 1 hit WS where the offhand will gain TP
|
|
-- Needs better verification, in this case (1 hit ws with multis) a DA/TA/QA may not count as TP hit and we'd move this into the offhand hit proc.
|
|
-- Either way, this won't "cheat" players out of TP in the current implementation.
|
|
if calcParams.extraOffhandHit and hitsDone == 1 then
|
|
calcParams.tpHitsLanded = calcParams.tpHitsLanded + 1
|
|
else -- Otherwise, add a hit to the "extra" hits which is 10 tp each
|
|
calcParams.mainHitsLanded = calcParams.mainHitsLanded + 1
|
|
end
|
|
|
|
if isJump then
|
|
attacker:addTP(mainhandTPGain * attackerTPMult)
|
|
end
|
|
end
|
|
|
|
finaldmg = finaldmg + hitdmg
|
|
hitsDone = hitsDone + 1
|
|
mainhandMultiHitsDone = mainhandMultiHitsDone + 1
|
|
|
|
if isRanged and ammoCount ~= -1 then
|
|
ammoUsed = ammoUsed + useAmmo(attacker)
|
|
|
|
if ammoUsed >= ammoCount then
|
|
hitsDone = 8 -- Attack while loops will stop if hitsDone is 8 or higher
|
|
end
|
|
end
|
|
end
|
|
|
|
local originalSlot = calcParams.attackInfo.slot
|
|
|
|
-- Update params for accuracy cap/pdif purposes
|
|
if calcParams.extraOffhandHit then
|
|
calcParams.attackInfo.slot = xi.slot.SUB
|
|
calcParams.attackInfo.weaponType = offhandSkill
|
|
|
|
if wsParams.critVaries then -- Update crit rate if this applies
|
|
calcParams.critRate = xi.combat.physical.calculateSwingCriticalRate(attacker, target, tp, calcParams.attackInfo.slot, wsParams.critVaries)
|
|
end
|
|
end
|
|
|
|
-- Recalculate hitRate with offhand acc
|
|
-- No more damage is being processed with mainhand acc, so this is ok
|
|
calcParams.hitRate = xi.weaponskills.getHitRate(attacker, target, calcParams.bonusAcc, xi.attackAnimation.LEFT_ATTACK)
|
|
|
|
-- Do the extra hit for our offhand if applicable
|
|
if calcParams.extraOffhandHit and hitsDone < 8 and finaldmg < targetHp then
|
|
local offhandDmg = calcParams.weaponDamage[2] + calcParams.fSTR + wsc * alpha
|
|
hitdmg, calcParams = getSingleHitDamage(attacker, target, offhandDmg, ftp, wsParams, calcParams)
|
|
|
|
if calcParams.melee then
|
|
hitdmg = modifyMeleeHitDamage(attacker, target, calcParams.attackInfo, wsParams, hitdmg)
|
|
end
|
|
|
|
if hitdmg > 0 then
|
|
attacker:trySkillUp(offhandSkill, targetLvl)
|
|
|
|
-- If this is the second swing of the WS (1 hit ws) the offhand appears to count for TP gain
|
|
-- Needs better verification
|
|
if calcParams.extraOffhandHit and hitsDone == 1 then
|
|
calcParams.tpHitsLanded = calcParams.tpHitsLanded + 1
|
|
else -- Otherwise, add a hit to the "extra" hits which is 10 tp each
|
|
calcParams.offhandHitsLanded = calcParams.offhandHitsLanded + 1
|
|
end
|
|
|
|
if isJump then
|
|
attacker:addTP(subTPGain * attackerTPMult)
|
|
end
|
|
end
|
|
|
|
finaldmg = finaldmg + hitdmg
|
|
hitsDone = hitsDone + 1
|
|
|
|
numOffhandMultis = getMultiAttacks(attacker, target, wsParams, false, true)
|
|
numMultiProcs = numOffhandMultis > 0 and numMultiProcs + 1 or numMultiProcs
|
|
end
|
|
|
|
-- Proc any offhand multi attacks.
|
|
local offhandMultiHitsDone = 0
|
|
|
|
while hitsDone < 8 and offhandMultiHitsDone < numOffhandMultis and finaldmg < targetHp do
|
|
local offhandDmg = calcParams.weaponDamage[2] + calcParams.fSTR + wsc * alpha
|
|
hitdmg, calcParams = getSingleHitDamage(attacker, target, offhandDmg, ftp, wsParams, calcParams)
|
|
|
|
if calcParams.melee then
|
|
hitdmg = modifyMeleeHitDamage(attacker, target, calcParams.attackInfo, wsParams, hitdmg)
|
|
end
|
|
|
|
if hitdmg > 0 then
|
|
attacker:trySkillUp(offhandSkill, targetLvl)
|
|
|
|
if isJump then
|
|
attacker:addTP(subTPGain * attackerTPMult)
|
|
end
|
|
|
|
calcParams.offhandHitsLanded = calcParams.offhandHitsLanded + 1
|
|
end
|
|
|
|
finaldmg = finaldmg + hitdmg
|
|
hitsDone = hitsDone + 1
|
|
offhandMultiHitsDone = offhandMultiHitsDone + 1
|
|
end
|
|
|
|
calcParams.extraHitsLanded = calcParams.mainHitsLanded + calcParams.offhandHitsLanded
|
|
|
|
-- Reset slot info (A listener eventually uses this, and the change to SLOT_SUB on DW will be unexpected)
|
|
calcParams.attackInfo.slot = originalSlot
|
|
|
|
-- Factor in "all hits" bonus damage mods
|
|
-- TODO: does this apply to every hit of a multi hit WS as it's coming in to account for potentially excess damage here?
|
|
local bonusdmg = 0
|
|
|
|
if not isJump then
|
|
bonusdmg = attacker:getMod(xi.mod.ALL_WSDMG_ALL_HITS) -- For any WS
|
|
|
|
if
|
|
attacker:getMod(xi.mod.WEAPONSKILL_DAMAGE_BASE + wsID) > 0 and
|
|
not attacker:isPet()
|
|
then
|
|
-- For specific WS
|
|
bonusdmg = bonusdmg + attacker:getMod(xi.mod.WEAPONSKILL_DAMAGE_BASE + wsID)
|
|
end
|
|
|
|
finaldmg = finaldmg * (100 + bonusdmg) / 100 -- Apply our "all hits" WS dmg bonuses
|
|
finaldmg = finaldmg + firstHitBonus -- Finally add in our "first hit" WS dmg bonus from before
|
|
end
|
|
|
|
-- Return our raw damage to then be modified by enemy reductions based off of melee/ranged
|
|
calcParams.finalDmg = finaldmg
|
|
calcParams.ammoUsed = ammoUsed
|
|
|
|
return calcParams
|
|
end
|
|
|
|
-- Sets up the necessary calcParams for a melee WS before passing it to calculateRawWSDmg. When the raw
|
|
-- damage is returned, handles reductions based on target resistances and passes off to xi.weaponskills.takeWeaponskillDamage.
|
|
xi.weaponskills.doPhysicalWeaponskill = function(attacker, target, wsID, wsParams, tp, action, primaryMsg, taChar)
|
|
-- Set up conditions and wsParams used for calculating weaponskill damage
|
|
local gearFTP = xi.combat.physical.calculateFTPBonus(attacker)
|
|
local gearAcc = math.ceil(gearFTP * 100) -- TODO: Separate gear fTP and acc bonuses
|
|
local attack =
|
|
{
|
|
['type'] = xi.attackType.PHYSICAL,
|
|
['slot'] = xi.slot.MAIN,
|
|
['weaponType'] = attacker:getWeaponSkillType(xi.slot.MAIN),
|
|
['damageType'] = attacker:getWeaponDamageType(xi.slot.MAIN),
|
|
}
|
|
|
|
local calcParams = {}
|
|
calcParams.wsID = wsID
|
|
calcParams.attackInfo = attack
|
|
calcParams.weaponDamage = xi.weaponskills.getMeleeDmg(attacker, attack.weaponType, wsParams.kick)
|
|
calcParams.fSTR = xi.combat.physical.calculateMeleeStatFactor(attacker, target)
|
|
calcParams.accStat = attacker:getACC()
|
|
calcParams.melee = true
|
|
calcParams.mustMiss = target:hasStatusEffect(xi.effect.PERFECT_DODGE) or (target:hasStatusEffect(xi.effect.ALL_MISS) and not wsParams.hitsHigh)
|
|
calcParams.sneakApplicable = attacker:hasStatusEffect(xi.effect.SNEAK_ATTACK) and (attacker:isBehind(target) or attacker:hasStatusEffect(xi.effect.HIDE) or target:hasStatusEffect(xi.effect.DOUBT))
|
|
calcParams.taChar = taChar
|
|
calcParams.trickApplicable = calcParams.taChar ~= nil
|
|
calcParams.assassinApplicable = calcParams.trickApplicable and attacker:hasTrait(xi.trait.ASSASSIN)
|
|
calcParams.guaranteedHit = calcParams.sneakApplicable or calcParams.trickApplicable
|
|
calcParams.mightyStrikesApplicable = attacker:hasStatusEffect(xi.effect.MIGHTY_STRIKES)
|
|
calcParams.forcedFirstCrit = calcParams.sneakApplicable or calcParams.assassinApplicable
|
|
calcParams.extraOffhandHit = attacker:isDualWielding()
|
|
calcParams.hybridHit = wsParams.hybridWS
|
|
calcParams.flourishEffect = attacker:getStatusEffect(xi.effect.BUILDING_FLOURISH)
|
|
calcParams.bonusTP = wsParams.bonusTP or 0
|
|
calcParams.tpUsed = tp
|
|
calcParams.attackType = xi.attackType.PHYSICAL
|
|
|
|
local isJump = wsParams.isJump or false
|
|
if isJump then
|
|
calcParams.bonusfTP = 0
|
|
calcParams.bonusAcc = attacker:getMod(xi.mod.JUMP_ACC_BONUS)
|
|
calcParams.bonusWSmods = 0
|
|
else
|
|
calcParams.bonusfTP = gearFTP
|
|
calcParams.bonusAcc = gearAcc + attacker:getMod(xi.mod.WSACC)
|
|
calcParams.bonusWSmods = wsParams.bonusWSmods or 0
|
|
end
|
|
|
|
if wsParams.accVaries then
|
|
local accMod = xi.weaponskills.fTP(tp, wsParams.accVaries)
|
|
calcParams.bonusAcc = calcParams.bonusAcc + accMod
|
|
end
|
|
|
|
calcParams.firstHitRate = xi.weaponskills.getHitRate(attacker, target, calcParams.bonusAcc + 100, xi.attackAnimation.RIGHT_ATTACK)
|
|
calcParams.hitRate = xi.weaponskills.getHitRate(attacker, target, calcParams.bonusAcc, xi.attackAnimation.RIGHT_ATTACK)
|
|
calcParams.skillType = attack.weaponType
|
|
|
|
-- Send our wsParams off to calculate our raw WS damage, hits landed, and shadows absorbed
|
|
calcParams = xi.weaponskills.calculateRawWSDmg(attacker, target, wsID, tp, action, wsParams, calcParams)
|
|
local finaldmg = math.floor(calcParams.finalDmg)
|
|
|
|
-- Add in magic damage for hybrid weaponskills
|
|
-- Only procs if the mob still has HP remaining
|
|
if wsParams.hybridWS and target:getHP() > finaldmg then
|
|
finaldmg = finaldmg + calculateHybridMagicDamage(tp, finaldmg, attacker, target, wsParams, calcParams, wsID)
|
|
end
|
|
|
|
-- Delete statuses that may have been spent by the WS
|
|
attacker:delStatusEffectsByFlag(xi.effectFlag.DETECTABLE)
|
|
attacker:delStatusEffect(xi.effect.SNEAK_ATTACK)
|
|
attacker:delStatusEffectSilent(xi.effect.BUILDING_FLOURISH)
|
|
|
|
finaldmg = finaldmg * xi.settings.main.WEAPON_SKILL_POWER -- Add server bonus
|
|
calcParams.finalDmg = finaldmg
|
|
finaldmg = xi.weaponskills.takeWeaponskillDamage(target, attacker, wsParams, primaryMsg, attack, calcParams, action)
|
|
|
|
return finaldmg, calcParams.criticalHit, calcParams.tpHitsLanded, calcParams.extraHitsLanded, calcParams.shadowsAbsorbed
|
|
end
|
|
|
|
-- Sets up the necessary calcParams for a ranged WS before passing it to calculateRawWSDmg. When the raw
|
|
-- damage is returned, handles reductions based on target resistances and passes off to xi.weaponskills.takeWeaponskillDamage.
|
|
xi.weaponskills.doRangedWeaponskill = function(attacker, target, wsID, wsParams, tp, action, primaryMsg)
|
|
-- Set up conditions and params used for calculating weaponskill damage
|
|
local gearFTP = xi.combat.physical.calculateFTPBonus(attacker)
|
|
local gearAcc = math.ceil(gearFTP * 100) -- TODO: Separate gear fTP and acc bonuses
|
|
|
|
local attack =
|
|
{
|
|
['type'] = xi.attackType.RANGED,
|
|
['slot'] = xi.slot.RANGED,
|
|
['weaponType'] = attacker:getWeaponSkillType(xi.slot.RANGED),
|
|
['damageType'] = attacker:getWeaponDamageType(xi.slot.RANGED),
|
|
}
|
|
|
|
local calcParams =
|
|
{
|
|
wsID = wsID,
|
|
attackInfo = attack,
|
|
weaponDamage = { attacker:getRangedDmg() },
|
|
skillType = attacker:getWeaponSkillType(xi.slot.RANGED),
|
|
fSTR = xi.combat.physical.calculateRangedStatFactor(attacker, target),
|
|
accStat = attacker:getRACC(),
|
|
melee = false,
|
|
mustMiss = false,
|
|
sneakApplicable = false,
|
|
trickApplicable = false,
|
|
assassinApplicable = false,
|
|
mightyStrikesApplicable = false,
|
|
forcedFirstCrit = false,
|
|
extraOffhandHit = false,
|
|
flourishEffect = false,
|
|
tpUsed = tp,
|
|
bonusTP = wsParams.bonusTP or 0,
|
|
bonusfTP = gearFTP,
|
|
bonusAcc = gearAcc + attacker:getMod(xi.mod.WSACC),
|
|
bonusWSmods = wsParams.bonusWSmods or 0,
|
|
attackType = xi.attackType.RANGED,
|
|
}
|
|
|
|
if wsParams.accVaries then
|
|
local accMod = xi.weaponskills.fTP(tp, wsParams.accVaries)
|
|
calcParams.bonusAcc = calcParams.bonusAcc + accMod
|
|
end
|
|
|
|
-- Split Shot/Piercing Arrow and Empyreal Arrow/Detonator are confirmed for this. Theoretically Last Stand could have a bonus too, and if so it would likely be first hit only.
|
|
if wsParams.rangedAccuracyBonus then
|
|
calcParams.firstHitRate = xi.weaponskills.getRangedHitRate(attacker, target, calcParams.bonusAcc + wsParams.rangedAccuracyBonus)
|
|
end
|
|
|
|
calcParams.hitRate = xi.weaponskills.getRangedHitRate(attacker, target, calcParams.bonusAcc)
|
|
|
|
-- Send our params off to calculate our raw WS damage, hits landed, and shadows absorbed
|
|
calcParams = xi.weaponskills.calculateRawWSDmg(attacker, target, wsID, tp, action, wsParams, calcParams)
|
|
local finaldmg = calcParams.finalDmg
|
|
|
|
-- Delete statuses that may have been spent by the WS
|
|
attacker:delStatusEffectsByFlag(xi.effectFlag.DETECTABLE)
|
|
attacker:delStatusEffect(xi.effect.FLASHY_SHOT)
|
|
attacker:delStatusEffect(xi.effect.STEALTH_SHOT)
|
|
|
|
-- Calculate reductions
|
|
finaldmg = target:rangedDmgTaken(finaldmg)
|
|
finaldmg = finaldmg * (1 + target:getMod(xi.mod.PIERCE_SDT) / 10000)
|
|
finaldmg = math.floor(finaldmg)
|
|
|
|
-- Add in magic damage for hybrid weaponskills
|
|
-- Only procs if the mob still has HP remaining
|
|
if wsParams.hybridWS and target:getHP() > finaldmg then
|
|
finaldmg = finaldmg + calculateHybridMagicDamage(tp, finaldmg, attacker, target, wsParams, calcParams, wsID)
|
|
end
|
|
|
|
finaldmg = finaldmg * xi.settings.main.WEAPON_SKILL_POWER -- Add server bonus
|
|
calcParams.finalDmg = finaldmg
|
|
|
|
finaldmg = xi.weaponskills.takeWeaponskillDamage(target, attacker, wsParams, primaryMsg, attack, calcParams, action)
|
|
|
|
-- Ammo needs to be removed after xi.weaponskills.takeWeaponskillDamage for delay/tp return uses
|
|
if calcParams.ammoUsed and calcParams.ammoUsed > 0 then
|
|
attacker:removeAmmo(calcParams.ammoUsed)
|
|
end
|
|
|
|
return finaldmg, calcParams.criticalHit, calcParams.tpHitsLanded, calcParams.extraHitsLanded, calcParams.shadowsAbsorbed
|
|
end
|
|
|
|
-- params: ftpMod, wsc_str, wsc_dex, wsc_vit, wsc_agi, wsc_int, wsc_mnd, wsc_chr,
|
|
-- ele (xi.element.FIRE), skill (xi.skill.STAFF)
|
|
|
|
xi.weaponskills.doMagicWeaponskill = function(attacker, target, wsID, wsParams, tp, action, primaryMsg)
|
|
-- Set up conditions and wsParams used for calculating weaponskill damage
|
|
local attack =
|
|
{
|
|
['type'] = xi.attackType.MAGICAL,
|
|
['slot'] = xi.slot.MAIN,
|
|
['weaponType'] = attacker:getWeaponSkillType(xi.slot.MAIN),
|
|
['damageType'] = xi.damageType.ELEMENTAL + wsParams.ele
|
|
}
|
|
|
|
local calcParams =
|
|
{
|
|
['shadowsAbsorbed'] = 0,
|
|
['hitsLanded'] = 1,
|
|
['tpHitsLanded'] = 1,
|
|
['extraHitsLanded'] = 0,
|
|
['bonusTP'] = wsParams.bonusTP or 0,
|
|
['wsID'] = wsID,
|
|
}
|
|
|
|
if
|
|
wsParams.skill == xi.skill.MARKSMANSHIP or
|
|
wsParams.skill == xi.skill.ARCHERY
|
|
then
|
|
attack.slot = xi.slot.RANGED
|
|
end
|
|
|
|
local dStat = wsParams.dStat and wsParams.dStat or xi.mod.INT
|
|
local gearFTP = xi.combat.physical.calculateFTPBonus(attacker)
|
|
local gearAcc = math.ceil(gearFTP * 100) + attacker:getMod(xi.mod.WSACC) -- TODO: Separate gear fTP and acc bonuses
|
|
local fint = 0
|
|
local dmg = 0
|
|
|
|
-- TODO: ranged magic WS are universal in it's (AGI-INT)*2
|
|
-- But other magic WS vary. Some don't even have a component, the general case is dINT/2 + 8
|
|
if attack.slot == xi.slot.RANGED then
|
|
fint = utils.clamp((attacker:getStat(dStat) - target:getStat(xi.mod.INT)) * 2, -32, 32)
|
|
elseif dStat == xi.mod.CHR then
|
|
fint = utils.clamp((attacker:getStat(dStat) - target:getStat(xi.mod.INT)) * 1.5, -651, 651) -- TODO: unknown lower cap but on dINT it normally mirrors https://www.bg-wiki.com/ffxi/Primal_Rend
|
|
else
|
|
fint = math.floor(utils.clamp(8 + (attacker:getStat(dStat) - target:getStat(xi.mod.INT)) / 2, -32, 32))
|
|
end
|
|
|
|
-- Magic-based WSes never miss, so we don't need to worry about calculating a miss, only if a shadow absorbed it.
|
|
if not shadowAbsorb(target) then
|
|
dmg = dmg + xi.combat.physical.calculateWSC(attacker, wsParams.str_wsc, wsParams.dex_wsc, wsParams.vit_wsc, wsParams.agi_wsc, wsParams.int_wsc, wsParams.mnd_wsc, wsParams.chr_wsc)
|
|
dmg = dmg + attacker:getMainLvl() + 2 + fint
|
|
|
|
-- Applying fTP multiplier
|
|
local ftp = xi.weaponskills.fTP(tp, wsParams.ftpMod) + gearFTP
|
|
|
|
dmg = dmg * ftp
|
|
|
|
-- Apply Consume Mana and Scarlet Delirium
|
|
-- dmg = dmg + xi.combat.damage.consumeManaAddition(attacker)
|
|
dmg = dmg * xi.combat.damage.scarletDeliriumMultiplier(attacker)
|
|
|
|
-- Factor in "all hits" bonus damage mods
|
|
local bonusdmg = attacker:getMod(xi.mod.ALL_WSDMG_ALL_HITS) -- For any WS
|
|
if
|
|
attacker:getMod(xi.mod.WEAPONSKILL_DAMAGE_BASE + wsID) > 0 and
|
|
not attacker:isPet()
|
|
then
|
|
-- For specific WS
|
|
bonusdmg = bonusdmg + attacker:getMod(xi.mod.WEAPONSKILL_DAMAGE_BASE + wsID)
|
|
end
|
|
|
|
-- Add in bonusdmg
|
|
dmg = dmg * (100 + bonusdmg) / 100 -- Apply our "all hits" WS dmg bonuses
|
|
dmg = dmg + dmg * attacker:getMod(xi.mod.ALL_WSDMG_FIRST_HIT) / 100 -- Add in our "first hit" WS dmg bonus
|
|
|
|
-- Calculate magical bonuses and reductions
|
|
dmg = math.floor(addBonusesAbility(attacker, wsParams.ele, target, dmg, wsParams))
|
|
dmg = math.floor(dmg * xi.combat.magicHitRate.calculateResistRate(attacker, target, 0, wsParams.skill, 0, wsParams.ele, 0, 0, gearAcc))
|
|
dmg = math.floor(dmg * xi.combat.damage.calculateDamageAdjustment(target, false, true, false, false))
|
|
dmg = math.floor(target:handleSevereDamage(dmg, false))
|
|
|
|
if dmg < 0 then
|
|
calcParams.finalDmg = dmg
|
|
|
|
dmg = xi.weaponskills.takeWeaponskillDamage(target, attacker, wsParams, primaryMsg, attack, calcParams, action)
|
|
return dmg
|
|
end
|
|
|
|
dmg = dmg * xi.spells.damage.calculateAbsorption(target, wsParams.ele, true)
|
|
dmg = dmg * xi.spells.damage.calculateNullification(target, wsParams.ele, true, false)
|
|
|
|
dmg = utils.handlePhalanx(target, dmg)
|
|
dmg = utils.handleOneForAll(target, dmg)
|
|
dmg = utils.handleStoneskin(target, dmg)
|
|
|
|
dmg = dmg * xi.settings.main.WEAPON_SKILL_POWER -- Add server bonus
|
|
else
|
|
calcParams.shadowsAbsorbed = 1
|
|
end
|
|
|
|
calcParams.finalDmg = dmg
|
|
|
|
if dmg > 0 then
|
|
attacker:trySkillUp(attack.weaponType, target:getMainLvl())
|
|
end
|
|
|
|
dmg = xi.weaponskills.takeWeaponskillDamage(target, attacker, wsParams, primaryMsg, attack, calcParams, action)
|
|
|
|
return dmg, calcParams.criticalHit, calcParams.tpHitsLanded, calcParams.extraHitsLanded, calcParams.shadowsAbsorbed
|
|
end
|
|
|
|
-- After WS damage is calculated and damage reduction has been taken into account by the calling function,
|
|
-- handles displaying the appropriate action/message, delivering the damage to the mob, and any enmity from it
|
|
xi.weaponskills.takeWeaponskillDamage = function(defender, attacker, wsParams, primaryMsg, attack, wsResults, action)
|
|
local finaldmg = wsResults.finalDmg
|
|
|
|
if wsResults.hitsLanded > 0 then
|
|
if finaldmg >= 0 then
|
|
if primaryMsg then
|
|
action:messageID(defender:getID(), xi.msg.basic.DAMAGE)
|
|
else
|
|
action:messageID(defender:getID(), xi.msg.basic.DAMAGE_SECONDARY)
|
|
end
|
|
|
|
if finaldmg > 0 then
|
|
if wsResults.guardedHits and wsResults.guardedHits > 0 then
|
|
action:resolution(defender:getID(), xi.action.resolution.GUARD)
|
|
else
|
|
action:resolution(defender:getID(), xi.action.resolution.HIT)
|
|
end
|
|
end
|
|
else
|
|
if primaryMsg then
|
|
action:messageID(defender:getID(), xi.msg.basic.SELF_HEAL)
|
|
else
|
|
action:messageID(defender:getID(), xi.msg.basic.SELF_HEAL_SECONDARY)
|
|
end
|
|
end
|
|
elseif wsResults.shadowsAbsorbed > 0 then
|
|
action:messageID(defender:getID(), xi.msg.basic.SHADOW_ABSORB)
|
|
action:param(defender:getID(), wsResults.shadowsAbsorbed)
|
|
action:resolution(defender:getID(), xi.action.resolution.MISS)
|
|
else
|
|
if primaryMsg then
|
|
action:messageID(defender:getID(), xi.msg.basic.SKILL_MISS)
|
|
else
|
|
action:messageID(defender:getID(), xi.msg.basic.EVADES)
|
|
end
|
|
|
|
action:resolution(defender:getID(), xi.action.resolution.MISS)
|
|
end
|
|
|
|
local targetTPMult = wsParams.targetTPMult or 1
|
|
local attackerTPMult = wsParams.attackerTPMult or 1
|
|
local isJump = wsParams.isJump or false
|
|
|
|
-- DA/TA/QA/OaT/Oa2-3 etc give full TP return per hit on Jumps
|
|
if isJump then
|
|
-- Don't feed TP and don't gain TP from takeWeaponskillDamage
|
|
attackerTPMult = 0
|
|
wsResults.extraHitsLanded = 0
|
|
end
|
|
|
|
-- Core does not modify the TP for the 10 TP/hit like it should, so we're doing it here
|
|
local storeTPModifier = 1 + attacker:getMod(xi.mod.STORETP) / 100 -- TODO, make a global function to get this (inhibit TP is not accounted for properly in core)
|
|
|
|
finaldmg = defender:takeWeaponskillDamage(attacker, finaldmg, attack.type, attack.damageType, attack.slot, primaryMsg, wsResults.tpHitsLanded * attackerTPMult, (wsResults.extraHitsLanded * 10 * storeTPModifier) + wsResults.bonusTP, targetTPMult)
|
|
if wsResults.tpHitsLanded + wsResults.extraHitsLanded > 0 then
|
|
action:recordDamage(defender, attack.type, math.abs(finaldmg), wsResults.criticalHit)
|
|
end
|
|
|
|
local enmityEntity = wsResults.taChar or attacker
|
|
|
|
if
|
|
wsParams.overrideCE and
|
|
wsParams.overrideVE and
|
|
wsResults.tpHitsLanded + wsResults.extraHitsLanded > 0
|
|
then
|
|
defender:addEnmity(enmityEntity, wsParams.overrideCE, wsParams.overrideVE)
|
|
else
|
|
local enmityMult = wsParams.enmityMult or 1
|
|
defender:updateEnmityFromDamage(enmityEntity, finaldmg * enmityMult)
|
|
end
|
|
|
|
local sengikoriEffect = attacker:getStatusEffect(xi.effect.SENGIKORI)
|
|
-- TODO: does this effect not apply if you do 0 damage (or absorb)?
|
|
-- Skillchains will still "proc" if you do 0 damage, so assume it does for now
|
|
if
|
|
(wsResults.tpHitsLanded +
|
|
wsResults.extraHitsLanded > 0) and
|
|
sengikoriEffect ~= nil
|
|
then
|
|
local sengikoriBonus = attacker:getMod(xi.mod.SENGIKORI_BONUS) -- Additive % bonus: https://www.ffxiah.com/forum/topic/23457/july-11-sam-update/4/#1421344
|
|
local power = sengikoriEffect:getPower() + sengikoriBonus
|
|
|
|
-- If no SC effect, apply SC damage debuff
|
|
-- If there is one, apply MB damage debuff
|
|
-- This "effect" lasts until the it's used or the SC goes away
|
|
-- see https://wiki.ffo.jp/html/20051.html
|
|
if defender:hasStatusEffect(xi.effect.SKILLCHAIN) then
|
|
defender:setMod(xi.mod.SENGIKORI_MB_DMG_DEBUFF, power)
|
|
else
|
|
defender:setMod(xi.mod.SENGIKORI_SC_DMG_DEBUFF, power)
|
|
end
|
|
|
|
attacker:delStatusEffect(xi.effect.SENGIKORI)
|
|
end
|
|
|
|
if finaldmg > 0 then
|
|
-- Pack the weaponskill ID in the top 8 bits of this variable which is utilized
|
|
-- in OnMobDeath in luautils. Max WSID is 255.
|
|
defender:setLocalVar('weaponskillHit', bit.lshift(wsResults.wsID, 24) + finaldmg)
|
|
end
|
|
|
|
return finaldmg
|
|
end
|
|
|
|
-- Helper function to get Main damage depending on weapon type
|
|
xi.weaponskills.getMeleeDmg = function(attacker, weaponType, kick)
|
|
local mainhandDamage = attacker:getWeaponDmg()
|
|
local offhandDamage = attacker:getOffhandDmg()
|
|
|
|
if weaponType == xi.skill.HAND_TO_HAND or weaponType == xi.skill.NONE then
|
|
local h2hSkill = attacker:getSkillLevel(xi.skill.HAND_TO_HAND) * 0.11 + 3
|
|
|
|
if kick and attacker:hasStatusEffect(xi.effect.FOOTWORK) then
|
|
mainhandDamage = attacker:getMod(xi.mod.KICK_DMG) -- Use Kick damage if footwork is on
|
|
end
|
|
|
|
mainhandDamage = mainhandDamage + h2hSkill
|
|
offhandDamage = mainhandDamage
|
|
end
|
|
|
|
return { mainhandDamage, offhandDamage }
|
|
end
|
|
|
|
---@param attacker CBaseEntity
|
|
---@param target CBaseEntity
|
|
---@param bonus number
|
|
---@param slot xi.attackAnimation
|
|
---@return number
|
|
xi.weaponskills.getHitRate = function(attacker, target, bonus, slot)
|
|
return xi.combat.physicalHitRate.getPhysicalHitRate(attacker, target, bonus, slot, true)
|
|
end
|
|
|
|
-- TODO: Use a common function with optional multiplier on return, or multiply outside of this.
|
|
xi.weaponskills.fTP = function(tp, ftpTable)
|
|
if
|
|
not ftpTable or
|
|
tp < 1000
|
|
then
|
|
-- No multiplier if points are not provided, or TP is not at minimum required
|
|
return 1
|
|
end
|
|
|
|
if tp >= 2000 then
|
|
return ftpTable[2] + (tp - 2000) * (ftpTable[3] - ftpTable[2]) / 1000
|
|
elseif tp >= 1000 then
|
|
return ftpTable[1] + (tp - 1000) * (ftpTable[2] - ftpTable[1]) / 1000
|
|
end
|
|
end
|
|
|
|
xi.weaponskills.calculatedIgnoredDef = function(tp, def, ignoredDefenseTable)
|
|
if ignoredDefenseTable then
|
|
return xi.weaponskills.fTP(tp, ignoredDefenseTable) * def
|
|
end
|
|
|
|
return 0
|
|
end
|
|
|
|
xi.weaponskills.handleWeaponskillEffect = function(actor, target, effectId, actionElement, damage, power, duration)
|
|
if
|
|
damage > 0 and
|
|
not target:hasStatusEffect(effectId) and
|
|
not xi.data.statusEffect.isTargetImmune(target, effectId, actionElement) and
|
|
not xi.data.statusEffect.isTargetResistant(actor, target, effectId) and
|
|
not xi.data.statusEffect.isEffectNullified(target, effectId, 0)
|
|
then
|
|
target:addStatusEffect(effectId, { power = power, duration = duration, origin = actor })
|
|
end
|
|
end
|