1454 lines
54 KiB
Lua
1454 lines
54 KiB
Lua
-----------------------------------
|
|
-- Global, independent functions for physical calculations.
|
|
-- Includes:
|
|
-- fSTR, fSTR2, WSC, fTP, pDIF
|
|
|
|
-- For weapon skill:
|
|
-- Damage PER HIT = floor((D + fSTR + WSC) * fTP) * pDIF
|
|
|
|
-- Intended WS structure:
|
|
-- 1 - Calculate number of hits (max 8)
|
|
-- 1a - Calculate hits absorbed by blink and utsusemi, if aplicable.
|
|
|
|
-- 2 - Calculate first hit:
|
|
-- 2a - Calculate if first hit lands
|
|
-- 2b - Calculate if first hit crits
|
|
-- 2c - Calculate first hit DMG
|
|
|
|
-- 3 - Calculate, per hit, secondary hits, following the same structure as before, but simplified (no first-hit bonuses)
|
|
|
|
-- 4 - Add them all, and final operations/considerations.
|
|
-----------------------------------
|
|
xi = xi or {}
|
|
xi.combat = xi.combat or {}
|
|
xi.combat.physical = xi.combat.physical or {}
|
|
-----------------------------------
|
|
local wsElementalProperties =
|
|
{
|
|
-- [Skillchain type ] = { Fire, Ice, Wind, Earth, Thunder, Water, Light, Dark },
|
|
[xi.skillchainType.NONE ] = { 0, 0, 0, 0, 0, 0, 0, 0 }, -- Lv0 None
|
|
[xi.skillchainType.LIQUEFACTION ] = { 1, 0, 0, 0, 0, 0, 0, 0 }, -- Lv1 Fire
|
|
[xi.skillchainType.INDURATION ] = { 0, 1, 0, 0, 0, 0, 0, 0 }, -- Lv1 Ice
|
|
[xi.skillchainType.DETONATION ] = { 0, 0, 1, 0, 0, 0, 0, 0 }, -- Lv1 Wind
|
|
[xi.skillchainType.SCISSION ] = { 0, 0, 0, 1, 0, 0, 0, 0 }, -- Lv1 Earth
|
|
[xi.skillchainType.IMPACTION ] = { 0, 0, 0, 0, 1, 0, 0, 0 }, -- Lv1 Thunder
|
|
[xi.skillchainType.REVERBERATION] = { 0, 0, 0, 0, 0, 1, 0, 0 }, -- Lv1 Water
|
|
[xi.skillchainType.TRANSFIXION ] = { 0, 0, 0, 0, 0, 0, 1, 0 }, -- Lv1 Light
|
|
[xi.skillchainType.COMPRESSION ] = { 0, 0, 0, 0, 0, 0, 0, 1 }, -- Lv1 Dark
|
|
[xi.skillchainType.FUSION ] = { 1, 0, 0, 0, 0, 0, 1, 0 }, -- Lv2 Fire & Light
|
|
[xi.skillchainType.DISTORTION ] = { 0, 1, 0, 0, 0, 1, 0, 0 }, -- Lv2 Ice & Water
|
|
[xi.skillchainType.FRAGMENTATION] = { 0, 0, 1, 0, 1, 0, 0, 0 }, -- Lv2 Wind & Thunder
|
|
[xi.skillchainType.GRAVITATION ] = { 0, 0, 0, 1, 0, 0, 0, 1 }, -- Lv2 Earth & Dark
|
|
[xi.skillchainType.LIGHT ] = { 1, 0, 1, 0, 1, 0, 1, 0 }, -- Lv3 Fire, Wind, Thunder, Light
|
|
[xi.skillchainType.DARKNESS ] = { 0, 1, 0, 1, 0, 1, 0, 1 }, -- Lv3 Ice, Earth, Water, Dark
|
|
[xi.skillchainType.LIGHT_II ] = { 1, 0, 1, 0, 1, 0, 1, 0 }, -- Lv4 Fire, Wind, Thunder, Light
|
|
[xi.skillchainType.DARKNESS_II ] = { 0, 1, 0, 1, 0, 1, 0, 1 }, -- Lv4 Ice, Earth, Water, Dark
|
|
}
|
|
|
|
-- Table with pDIF caps per weapon/skill type.
|
|
xi.combat.physical.pDifWeaponCapTable =
|
|
{
|
|
-- [Skill/weapon type used] = { pre-cRatio caps, pre-randomizer pDIF cap }, Values from: https://www.bg-wiki.com/ffxi/PDIF
|
|
[xi.skill.NONE ] = 3, -- Fallback, not intentionally used. Mobs are calculated in pdif functions.
|
|
[xi.skill.HAND_TO_HAND ] = 3.5,
|
|
[xi.skill.DAGGER ] = 3.25,
|
|
[xi.skill.SWORD ] = 3.25,
|
|
[xi.skill.GREAT_SWORD ] = 3.75,
|
|
[xi.skill.AXE ] = 3.25,
|
|
[xi.skill.GREAT_AXE ] = 3.75,
|
|
[xi.skill.SCYTHE ] = 4,
|
|
[xi.skill.POLEARM ] = 3.75,
|
|
[xi.skill.KATANA ] = 3.25,
|
|
[xi.skill.GREAT_KATANA ] = 3.5,
|
|
[xi.skill.CLUB ] = 3.25,
|
|
[xi.skill.STAFF ] = 3.75,
|
|
[xi.skill.AUTOMATON_MELEE ] = 3, -- Unknown value. Copy of value below.
|
|
[xi.skill.AUTOMATON_RANGED] = 3, -- Unknown value. Reference found in an old post: https://forum.square-enix.com/ffxi/archive/index.php/t-52778.html?s=d906df07788334a185a902b0a6ae6a99
|
|
[xi.skill.AUTOMATON_MAGIC ] = 3, -- Unknown value. Here for completion sake.
|
|
[xi.skill.ARCHERY ] = 3.25,
|
|
[xi.skill.MARKSMANSHIP ] = 3.5,
|
|
[xi.skill.THROWING ] = 3.25,
|
|
}
|
|
|
|
local shieldSizeToBlockRateTable =
|
|
{
|
|
[1] = 55, -- Buckler
|
|
[2] = 40, -- Round
|
|
[3] = 45, -- Kite
|
|
[4] = 30, -- Tower
|
|
[5] = 50, -- Aegis and Srivatsa
|
|
[6] = 100, -- Ochain https://www.bg-wiki.com/ffxi/Category:Shields
|
|
}
|
|
|
|
-- WARNING: This function is used in src/map/attack.cpp "ProcessDamage" function.
|
|
-- If you update these parameters, update them there as well.
|
|
---@param actor CBaseEntity
|
|
---@param target CBaseEntity
|
|
---@param slot xi.slot
|
|
---@param physicalAttackType xi.physicalAttackType
|
|
---@param isH2H boolean
|
|
---@param isFirstSwing boolean
|
|
---@param isSneakAttack boolean
|
|
---@param isTrickAttack boolean
|
|
---@param damageRatio number
|
|
xi.combat.physical.calculateAttackDamage = function(actor, target, slot, physicalAttackType, isH2H, isFirstSwing, isSneakAttack, isTrickAttack, damageRatio)
|
|
local bonusBasePhysicalDamage = 0
|
|
local damage = 0
|
|
|
|
-- Sneak Attack
|
|
if isSneakAttack then
|
|
bonusBasePhysicalDamage = math.floor(bonusBasePhysicalDamage + actor:getStat(xi.mod.DEX) * (1 + actor:getMod(xi.mod.SNEAK_ATK_DEX) / 100))
|
|
end
|
|
|
|
-- Trick Attack
|
|
if isTrickAttack then
|
|
bonusBasePhysicalDamage = math.floor(bonusBasePhysicalDamage + actor:getStat(xi.mod.AGI) * (1 + actor:getMod(xi.mod.TRICK_ATK_AGI) / 100))
|
|
end
|
|
|
|
-- Consume Mana
|
|
bonusBasePhysicalDamage = bonusBasePhysicalDamage + xi.combat.damage.consumeManaAddition(actor)
|
|
|
|
-- Apply damage ratio multiplier.
|
|
local baseDamage = 0
|
|
|
|
if isH2H then
|
|
local naturalH2hDamage = math.floor(actor:getSkillLevel(xi.skill.HAND_TO_HAND) * 0.11) + 3
|
|
|
|
if actor:isMob() then
|
|
local mobH2HPenalty = 1
|
|
local regionID = actor:getCurrentRegion()
|
|
local fSTR = xi.combat.physical.calculateMeleeStatFactor(actor, target)
|
|
|
|
if actor:getMobMod(xi.mobMod.NO_H2H_PENALTY) == 0 then
|
|
if regionID <= xi.region.LIMBUS then
|
|
mobH2HPenalty = 0.425 -- Vanilla - COP
|
|
else
|
|
mobH2HPenalty = 0.65
|
|
end
|
|
end
|
|
|
|
baseDamage = actor:getWeaponDmg() + bonusBasePhysicalDamage
|
|
|
|
if physicalAttackType == xi.physicalAttackType.KICK then
|
|
local kickPenalty = 2 / 3 -- Per Jimmy, kicks get a second penalty, then fSTR is added
|
|
local kickDamage = actor:getMod(xi.mod.KICK_DMG)
|
|
|
|
-- Per Jimmy, kick damage penalty for mobs can only be damage * h2h penalty * kickpenalty + fstr
|
|
-- The math doesn't work in any other way, which is strange given fSTR is before the penalty on non-kicks
|
|
baseDamage = (baseDamage + kickDamage) * mobH2HPenalty * kickPenalty + fSTR
|
|
else
|
|
baseDamage = (baseDamage + fSTR) * mobH2HPenalty
|
|
end
|
|
elseif physicalAttackType == xi.physicalAttackType.KICK then
|
|
baseDamage = naturalH2hDamage + actor:getMod(xi.mod.KICK_DMG) + bonusBasePhysicalDamage + xi.combat.physical.calculateMeleeStatFactor(actor, target)
|
|
else
|
|
baseDamage = naturalH2hDamage + actor:getWeaponDmg() + bonusBasePhysicalDamage + xi.combat.physical.calculateMeleeStatFactor(actor, target)
|
|
end
|
|
elseif slot == xi.slot.MAIN then
|
|
baseDamage = actor:getWeaponDmg() + bonusBasePhysicalDamage + xi.combat.physical.calculateMeleeStatFactor(actor, target)
|
|
elseif slot == xi.slot.SUB then
|
|
baseDamage = actor:getOffhandDmg() + bonusBasePhysicalDamage + xi.combat.physical.calculateMeleeStatFactor(actor, target)
|
|
elseif slot == xi.slot.AMMO then
|
|
baseDamage = actor:getRangedDmg() + xi.combat.physical.calculateRangedStatFactor(actor, target)
|
|
end
|
|
|
|
damage = math.floor(baseDamage * damageRatio)
|
|
|
|
-- Scarlet Delirium multiplier.
|
|
damage = math.floor(damage * xi.combat.damage.scarletDeliriumMultiplier(actor))
|
|
|
|
-- Double/Triple Attack multipliers.
|
|
local multiAttackMultiplier = 1
|
|
if physicalAttackType == xi.physicalAttackType.DOUBLE then
|
|
multiAttackMultiplier = 1 + actor:getMod(xi.mod.DOUBLE_ATTACK_DMG) / 100
|
|
elseif physicalAttackType == xi.physicalAttackType.TRIPLE then
|
|
multiAttackMultiplier = 1 + actor:getMod(xi.mod.TRIPLE_ATTACK_DMG) / 100
|
|
end
|
|
|
|
damage = math.floor(damage * multiAttackMultiplier)
|
|
|
|
-- Soul Eater additive damage.
|
|
damage = damage + xi.combat.damage.souleaterAddition(actor)
|
|
|
|
-- Damage multipliers
|
|
damage = actor:addDamageFromMultipliers(damage, physicalAttackType, slot, isFirstSwing)
|
|
|
|
-- Sneak Attack Augment
|
|
if
|
|
actor:getMod(xi.mod.AUGMENTS_SA) > 0 and
|
|
isSneakAttack and
|
|
actor:hasStatusEffect(xi.effect.SNEAK_ATTACK)
|
|
then
|
|
damage = math.floor(damage * (1 + actor:getMod(xi.mod.AUGMENTS_SA) / 100))
|
|
end
|
|
|
|
-- Trick Attack Augment
|
|
if
|
|
actor:getMod(xi.mod.AUGMENTS_TA) > 0 and
|
|
isTrickAttack and
|
|
actor:hasStatusEffect(xi.effect.TRICK_ATTACK)
|
|
then
|
|
damage = math.floor(damage * (1 + actor:getMod(xi.mod.AUGMENTS_TA) / 100))
|
|
end
|
|
|
|
--- Low level mobs can get negative fSTR so low they crater their (base weapon damage + fstr) to below 0.
|
|
--- Absorption isn't possible at this point in the calculation, so zero it.
|
|
if damage < 0 then
|
|
damage = 0
|
|
end
|
|
|
|
-- Apply Restraint Weaponskill Damage
|
|
if
|
|
isFirstSwing and
|
|
actor:hasStatusEffect(xi.effect.RESTRAINT)
|
|
then
|
|
local effect = actor:getStatusEffect(xi.effect.RESTRAINT)
|
|
local power = effect and effect:getPower() or 30
|
|
|
|
if
|
|
effect and
|
|
power < 30
|
|
then
|
|
local jpBonus = actor:getJobPointLevel(xi.jp.RESTRAINT_EFFECT) * 2
|
|
|
|
-- Convert weapon delay and divide
|
|
-- Pull remainder of previous hit's value from Effect Sub Power
|
|
local boostPerRound = (3 * actor:getBaseDelay() / 50) / 385
|
|
local remainder = effect:getSubPower() / 100
|
|
|
|
-- Calculate bonuses from Enhances Restraint, Job Point upgrades, and remainder from previous hit
|
|
boostPerRound = remainder + boostPerRound * (1 + actor:getMod(xi.mod.ENHANCES_RESTRAINT) / 100) * (1 + jpBonus / 100)
|
|
|
|
-- Calculate new remainder and multiply by 100 so significant digits aren't lost
|
|
remainder = math.floor((1 - math.ceil(boostPerRound) - boostPerRound) * 100)
|
|
boostPerRound = math.floor(boostPerRound)
|
|
|
|
-- Cap total power to +30% WSD
|
|
if power + boostPerRound > 30 then
|
|
boostPerRound = 30 - power
|
|
end
|
|
|
|
effect:setPower(power + boostPerRound)
|
|
effect:setSubPower(remainder)
|
|
actor:setMod(xi.mod.ALL_WSDMG_FIRST_HIT, boostPerRound)
|
|
end
|
|
end
|
|
|
|
-- TODO: add charutils::TrySkillUP call
|
|
return damage
|
|
end
|
|
|
|
-- 'fSTR' in English Wikis. 'SV function' in JP wiki and Studio Gobli.
|
|
-- BG wiki: https://www.bg-wiki.com/ffxi/FSTR
|
|
-- Gobli Wiki: https://w-atwiki-jp.translate.goog/studiogobli/pages/14.html?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp
|
|
-- Mob calculation: https://docs.google.com/spreadsheets/d/1YBoveP-weMdidrirY-vPDzHyxbEI2ryECINlfCnFkLI/edit?gid=224123492#gid=224123492&range=C50
|
|
xi.combat.physical.calculateMeleeStatFactor = function(actor, target)
|
|
local fSTR = 0 -- The variable we want to calculate.
|
|
local mLvl = actor:getMainLvl()
|
|
|
|
-- Early return: Mobs at or under lvl 1.
|
|
if actor:isMob() and mLvl <= 1 then
|
|
return 1
|
|
end
|
|
|
|
-- Calculate statDiff.
|
|
local statDiff = actor:getStat(xi.mod.STR) - target:getStat(xi.mod.VIT)
|
|
|
|
-- Pets and Mobs.
|
|
if actor:isMob() or actor:isPet() then
|
|
if statDiff >= 36 then
|
|
fSTR = (statDiff - 4) / 4
|
|
elseif statDiff >= 26 then
|
|
fSTR = (statDiff - 3) / 4
|
|
elseif statDiff >= 17 then
|
|
fSTR = (statDiff - 2) / 4
|
|
elseif statDiff >= 4 then
|
|
fSTR = (statDiff - 1) / 4
|
|
elseif statDiff >= -8 then
|
|
fSTR = statDiff / 4
|
|
elseif statDiff >= -13 then
|
|
fSTR = (statDiff + 1) / 4
|
|
elseif statDiff >= -19 then
|
|
fSTR = (statDiff + 3) / 4
|
|
elseif statDiff >= -32 then
|
|
fSTR = (statDiff + 4) / 4
|
|
elseif statDiff >= -42 then
|
|
fSTR = (statDiff + 5) / 4
|
|
elseif statDiff >= -54 then
|
|
fSTR = (statDiff + 6) / 4
|
|
elseif statDiff >= -67 then
|
|
fSTR = (statDiff + 7) / 4
|
|
elseif statDiff >= -76 then
|
|
fSTR = (statDiff + 8) / 4
|
|
else -- <= -77
|
|
fSTR = (statDiff + 9) / 4
|
|
end
|
|
|
|
fSTR = math.floor(fSTR)
|
|
fSTR = utils.clamp(fSTR, -1 - math.floor(mLvl / 5), math.floor(mLvl / 5) + 5)
|
|
|
|
return fSTR
|
|
end
|
|
|
|
-- Players and Trusts
|
|
local weaponRank = actor:getWeaponDmgRank()
|
|
local statLowerCap = (7 + weaponRank * 2) * -2
|
|
local statUpperCap = (14 + weaponRank * 2) * 2
|
|
|
|
statDiff = utils.clamp(statDiff, statLowerCap, statUpperCap)
|
|
|
|
-- Calculate fSTR based on stat difference.
|
|
if statDiff >= 12 then
|
|
fSTR = statDiff + 4
|
|
elseif statDiff >= 6 then
|
|
fSTR = statDiff + 6
|
|
elseif statDiff >= 1 then
|
|
fSTR = statDiff + 7
|
|
elseif statDiff >= -2 then
|
|
fSTR = statDiff + 8
|
|
elseif statDiff >= -7 then
|
|
fSTR = statDiff + 9
|
|
elseif statDiff >= -15 then
|
|
fSTR = statDiff + 10
|
|
elseif statDiff >= -21 then
|
|
fSTR = statDiff + 12
|
|
else
|
|
fSTR = statDiff + 13
|
|
end
|
|
|
|
-- Clamp fSTR.
|
|
local fSTRupperCap = weaponRank + 8
|
|
local fSTRlowerCap = weaponRank * -1
|
|
|
|
if weaponRank == 0 then
|
|
fSTRlowerCap = -1
|
|
end
|
|
|
|
fSTR = utils.clamp(fSTR / 4, fSTRlowerCap, fSTRupperCap)
|
|
|
|
return fSTR
|
|
end
|
|
|
|
-- 'fSTR2' in English Wikis. 'SV function' in JP wiki and Studio Gobli.
|
|
-- BG wiki: https://www.bg-wiki.com/ffxi/FSTR
|
|
-- Gobli Wiki: https://w-atwiki-jp.translate.goog/studiogobli/pages/14.html?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp
|
|
-- https://docs.google.com/spreadsheets/d/1YBoveP-weMdidrirY-vPDzHyxbEI2ryECINlfCnFkLI/edit?gid=224123492#gid=224123492&range=A53
|
|
xi.combat.physical.calculateRangedStatFactor = function(actor, target)
|
|
local fSTR = 0 -- The variable we want to calculate.
|
|
local mLvl = actor:getMainLvl()
|
|
|
|
-- Early return: Mobs at or under lvl 1.
|
|
if actor:isMob() and mLvl <= 1 then
|
|
return 1
|
|
end
|
|
|
|
-- Calculate statDiff.
|
|
local statDiff = actor:getStat(xi.mod.STR) - target:getStat(xi.mod.VIT)
|
|
|
|
-- Pets and Mobs.
|
|
if actor:isMob() or actor:isPet() then
|
|
if statDiff >= 36 then
|
|
fSTR = (statDiff - 4) / 2
|
|
elseif statDiff >= 26 then
|
|
fSTR = (statDiff - 3) / 2
|
|
elseif statDiff >= 15 then
|
|
fSTR = (statDiff - 2) / 2
|
|
elseif statDiff >= 4 then
|
|
fSTR = (statDiff - 1) / 2
|
|
elseif statDiff >= -8 then
|
|
fSTR = statDiff / 2
|
|
elseif statDiff >= -16 then
|
|
fSTR = (statDiff + 1) / 2
|
|
elseif statDiff >= -31 then
|
|
fSTR = (statDiff + 1) / 2
|
|
elseif statDiff >= -42 then
|
|
fSTR = (statDiff + 3) / 2
|
|
elseif statDiff >= -53 then
|
|
fSTR = (statDiff + 3) / 2
|
|
elseif statDiff >= -64 then
|
|
fSTR = (statDiff + 5) / 2
|
|
elseif statDiff >= -76 then
|
|
fSTR = (statDiff + 6) / 2
|
|
else -- <= -77
|
|
fSTR = (statDiff + 7) / 2
|
|
end
|
|
|
|
fSTR = math.floor(fSTR)
|
|
fSTR = utils.clamp(fSTR, math.floor((-1 - (mLvl / 5)) * 2), math.floor((mLvl / 5 + 5) * 2))
|
|
|
|
return fSTR
|
|
end
|
|
|
|
-- Players and Trusts
|
|
local weaponRank = actor:getRangedDmgRank()
|
|
local statLowerCap = (7 + weaponRank * 2) * -2
|
|
local statUpperCap = (14 + weaponRank * 2) * 2
|
|
|
|
statDiff = utils.clamp(statDiff, statLowerCap, statUpperCap)
|
|
|
|
-- Calculate fSTR based on stat difference.
|
|
if statDiff >= 12 then
|
|
fSTR = statDiff + 4
|
|
elseif statDiff >= 6 then
|
|
fSTR = statDiff + 6
|
|
elseif statDiff >= 1 then
|
|
fSTR = statDiff + 7
|
|
elseif statDiff >= -2 then
|
|
fSTR = statDiff + 8
|
|
elseif statDiff >= -7 then
|
|
fSTR = statDiff + 9
|
|
elseif statDiff >= -15 then
|
|
fSTR = statDiff + 10
|
|
elseif statDiff >= -21 then
|
|
fSTR = statDiff + 12
|
|
else
|
|
fSTR = statDiff + 13
|
|
end
|
|
|
|
-- Clamp fSTR.
|
|
local fSTRupperCap = (weaponRank + 8) * 2
|
|
local fSTRlowerCap = weaponRank * -2
|
|
|
|
if weaponRank == 0 then
|
|
fSTRlowerCap = -2
|
|
elseif weaponRank == 1 then
|
|
fSTRlowerCap = -3
|
|
end
|
|
|
|
fSTR = utils.clamp(fSTR / 2, fSTRlowerCap, fSTRupperCap)
|
|
|
|
return fSTR
|
|
end
|
|
|
|
-- Weapon Skill Secondary Attribute Modifier: Function used to get stat addition to base damage.
|
|
xi.combat.physical.calculateWSC = function(actor, wsSTRmod, wsDEXmod, wsVITmod, wsAGImod, wsINTmod, wsMNDmod, wsCHRmod)
|
|
local finalWSC = 0
|
|
|
|
-- Sanitize parameters.
|
|
local strMultiplier = wsSTRmod or 0
|
|
local dexMultiplier = wsDEXmod or 0
|
|
local vitMultiplier = wsVITmod or 0
|
|
local agiMultiplier = wsAGImod or 0
|
|
local intMultiplier = wsINTmod or 0
|
|
local mndMultiplier = wsMNDmod or 0
|
|
local chrMultiplier = wsCHRmod or 0
|
|
|
|
-- wscSTAT = actor stat * (WS stat modifier + Actor-specific WS stat modifier)
|
|
local wscSTR = math.floor(actor:getStat(xi.mod.STR) * (strMultiplier + actor:getMod(xi.mod.WS_STR_BONUS) / 100))
|
|
local wscDEX = math.floor(actor:getStat(xi.mod.DEX) * (dexMultiplier + actor:getMod(xi.mod.WS_DEX_BONUS) / 100))
|
|
local wscVIT = math.floor(actor:getStat(xi.mod.VIT) * (vitMultiplier + actor:getMod(xi.mod.WS_VIT_BONUS) / 100))
|
|
local wscAGI = math.floor(actor:getStat(xi.mod.AGI) * (agiMultiplier + actor:getMod(xi.mod.WS_AGI_BONUS) / 100))
|
|
local wscINT = math.floor(actor:getStat(xi.mod.INT) * (intMultiplier + actor:getMod(xi.mod.WS_INT_BONUS) / 100))
|
|
local wscMND = math.floor(actor:getStat(xi.mod.MND) * (mndMultiplier + actor:getMod(xi.mod.WS_MND_BONUS) / 100))
|
|
local wscCHR = math.floor(actor:getStat(xi.mod.CHR) * (chrMultiplier + actor:getMod(xi.mod.WS_CHR_BONUS) / 100))
|
|
|
|
finalWSC = wscSTR + wscDEX + wscVIT + wscAGI + wscINT + wscMND + wscCHR
|
|
|
|
return finalWSC
|
|
end
|
|
|
|
-- TP factor equation. Used to determine TP modifer across all cases of 'X varies with TP'
|
|
xi.combat.physical.calculateTPfactor = function(actorTP, tpModifierTable)
|
|
if not tpModifierTable then
|
|
return 0
|
|
end
|
|
|
|
local tpFactor = tpModifierTable[1] -- Assume this will be used for monstrosity fixed TP moved someday.
|
|
|
|
if actorTP >= 2000 then
|
|
tpFactor = tpModifierTable[2] + (actorTP - 2000) * (tpModifierTable[3] - tpModifierTable[2]) / 1000
|
|
elseif actorTP >= 1000 then
|
|
tpFactor = tpModifierTable[1] + (actorTP - 1000) * (tpModifierTable[2] - tpModifierTable[1]) / 1000
|
|
end
|
|
|
|
return tpFactor
|
|
end
|
|
|
|
-- TP Multiplier calculations.
|
|
xi.combat.physical.calculateFTPBonus = function(actor)
|
|
local fTPBonus = 0
|
|
|
|
-- Early return: Gear bonuses only come from gear.
|
|
if actor:getObjType() ~= xi.objType.PC then
|
|
return fTPBonus
|
|
end
|
|
|
|
-- Early return: Gear bonuses only apply to weaponskills with elemental properties.
|
|
local scProp1, scProp2, scProp3 = actor:getWSSkillchainProp()
|
|
if
|
|
scProp1 == xi.skillchainType.NONE and
|
|
scProp2 == xi.skillchainType.NONE and
|
|
scProp3 == xi.skillchainType.NONE
|
|
then
|
|
return fTPBonus
|
|
end
|
|
|
|
-- fTP bonuses from gear.
|
|
local dayElement = VanadielDayElement()
|
|
|
|
for elementChecked = xi.element.FIRE, xi.element.DARK do
|
|
if
|
|
wsElementalProperties[scProp1][elementChecked] == 1 or
|
|
wsElementalProperties[scProp2][elementChecked] == 1 or
|
|
wsElementalProperties[scProp3][elementChecked] == 1
|
|
then
|
|
fTPBonus = fTPBonus + actor:getMod(xi.data.element.getElementalFTPModifier(elementChecked)) / 256
|
|
|
|
if dayElement == elementChecked then
|
|
fTPBonus = fTPBonus + actor:getMod(xi.mod.DAY_FTP_BONUS) / 256
|
|
end
|
|
end
|
|
end
|
|
|
|
fTPBonus = fTPBonus + actor:getMod(xi.mod.ANY_FTP_BONUS) / 256
|
|
|
|
return fTPBonus
|
|
end
|
|
|
|
---@param wRatio number
|
|
---@param pDifFinalCap number
|
|
xi.combat.physical.wRatioCapPC = function(wRatio, pDifFinalCap)
|
|
local pDifUpperCap = 0
|
|
local pDifLowerCap = 0
|
|
|
|
-- pDIF upper cap.
|
|
if wRatio < 0.5 then
|
|
pDifUpperCap = wRatio + 0.5
|
|
elseif wRatio < 0.7 then
|
|
pDifUpperCap = 1
|
|
elseif wRatio < 1.2 then
|
|
pDifUpperCap = wRatio + 0.3
|
|
elseif wRatio < 1.5 then
|
|
pDifUpperCap = wRatio + wRatio * 0.25
|
|
else
|
|
pDifUpperCap = math.min(wRatio + 0.375, pDifFinalCap)
|
|
end
|
|
|
|
-- pDIF lower cap.
|
|
if wRatio < 0.38 then
|
|
pDifLowerCap = 0
|
|
elseif wRatio < 1.25 then
|
|
pDifLowerCap = wRatio * 1176 / 1024 - 448 / 1024
|
|
elseif wRatio < 1.51 then
|
|
pDifLowerCap = 1
|
|
elseif wRatio < 2.44 then
|
|
pDifLowerCap = wRatio * 1176 / 1024 - 775 / 1024
|
|
else
|
|
pDifLowerCap = math.min(wRatio - 0.375, pDifFinalCap)
|
|
end
|
|
|
|
return pDifLowerCap, pDifUpperCap
|
|
end
|
|
|
|
-- wRatio cap for non-PCs
|
|
---@param wRatio number
|
|
---@param pDifFinalCap number
|
|
xi.combat.physical.wRatioCapOthers = function(wRatio, pDifFinalCap)
|
|
local pDifUpperCap = 0
|
|
local pDifLowerCap = 0
|
|
|
|
-- see https://www.ffxiah.com/forum/topic/58479/monster-pdif-curves-and-other-info/
|
|
-- pDIF upper cap.
|
|
if wRatio < 0.55 then
|
|
pDifUpperCap = 0.6 + 760 / 1024 * wRatio
|
|
elseif wRatio <= 0.8 then
|
|
pDifUpperCap = 1
|
|
elseif wRatio < 1.2 then
|
|
pDifUpperCap = 1 + 1127 / 1024 * (wRatio - 0.8)
|
|
elseif wRatio < 1.5 then
|
|
pDifUpperCap = 1474 / 1024 + 1105 / 1024 * (wRatio - 1235 / 1024)
|
|
else
|
|
pDifUpperCap = math.min(1803 / 1024 + 1070 / 1024 * (wRatio - 1.5), pDifFinalCap)
|
|
end
|
|
|
|
-- pDIF lower cap.
|
|
if wRatio <= 0.4 then
|
|
pDifLowerCap = 0.25
|
|
elseif wRatio < 1.35 then
|
|
pDifLowerCap = 0.25 + (827 / 1024) * (wRatio - 0.4)
|
|
elseif wRatio <= 1.60 then
|
|
pDifLowerCap = 1
|
|
else
|
|
pDifLowerCap = math.min(1 + (1120 / 1024) * (wRatio - 1.59), pDifFinalCap)
|
|
end
|
|
|
|
return pDifLowerCap, pDifUpperCap
|
|
end
|
|
|
|
---@param isPC boolean
|
|
---@param wRatio number
|
|
---@return number
|
|
local function getSpikeRatio(isPC, wRatio)
|
|
if isPC then
|
|
-- https://www.bg-wiki.com/ffxi/PDIF#Average_Melee_pDIF(qRatio)
|
|
-- This is also known as "pDIF spike"
|
|
if wRatio > 0.5 and wRatio < 1.5 then -- 0.5 and 1.5 are 0% chance
|
|
local sRatio = (0.5 - math.abs(wRatio - 1)) * 1.2
|
|
|
|
return utils.clamp(sRatio, 0, 1 / 3) -- 1/3 (one-third), not 0.33
|
|
end
|
|
else
|
|
-- https://www.ffxiah.com/forum/topic/58479/monster-pdif-curves-and-other-info/#3751498
|
|
-- This is also known as "pDIF spike"
|
|
local sRatio = 0
|
|
|
|
if wRatio > 0.0 and wRatio < 0.75 then
|
|
sRatio = -5 / 9 + (10 / 9) * wRatio
|
|
elseif wRatio <= 1.3 then
|
|
sRatio = 0.3
|
|
else
|
|
sRatio = 5 / 3 - (270 / 256) * wRatio
|
|
end
|
|
|
|
return utils.clamp(sRatio, 0, 0.3)
|
|
end
|
|
|
|
return 0
|
|
end
|
|
|
|
-- WARNING: This function is used in src/utils/battleutils.cpp "GetDamageRatio" function.
|
|
-- If you update this parameters, update them there aswell.
|
|
---@param actor CBaseEntity
|
|
---@param target CBaseEntity
|
|
---@param weaponType xi.skill
|
|
---@param wsAttackMod number
|
|
---@param isCritical boolean
|
|
---@param applyLevelCorrection boolean
|
|
---@param tpIgnoresDefense boolean
|
|
---@param tpFactor number
|
|
---@param isWeaponskill boolean
|
|
---@param weaponSlot xi.slot
|
|
---@param isCannonball boolean
|
|
xi.combat.physical.calculateMeleePDIF = function(actor, target, weaponType, wsAttackMod, isCritical, applyLevelCorrection, tpIgnoresDefense, tpFactor, isWeaponskill, weaponSlot, isCannonball)
|
|
local pDif = 0
|
|
|
|
----------------------------------------
|
|
-- Step 1: Attack / Defense Ratio
|
|
----------------------------------------
|
|
local baseRatio = 0
|
|
local actorAttack = 0
|
|
local targetDefense = math.max(1, target:getStat(xi.mod.DEF))
|
|
local flourishBonus = 1
|
|
|
|
-- Actor Weaponskill Specific Attack modifiers.
|
|
if isWeaponskill then
|
|
local flourishEffect = actor:getStatusEffect(xi.effect.BUILDING_FLOURISH)
|
|
|
|
if flourishEffect and flourishEffect:getPower() >= 2 then -- 2 or more Finishing Moves used.
|
|
local meritCount = flourishEffect:getSubPower()
|
|
|
|
flourishBonus = 1.25 + 0.01 * meritCount -- +1% attack bonus per merit -- TODO: do the merits apply even when FMs are < 2?
|
|
end
|
|
end
|
|
|
|
-- TODO: it is unknown if ws attack mod and flourish bonus are additive or multiplicative
|
|
-- TODO: do flourish and attack mods come before or after food?
|
|
actorAttack = math.max(1, math.floor(actor:getStat(xi.mod.ATT, weaponSlot) * wsAttackMod * flourishBonus))
|
|
|
|
-- handle attuner
|
|
-- note: isAutomaton is checked inside xi.automaton.handleAttuner and could be removed
|
|
if actor:isAutomaton() then
|
|
local defIgnore = xi.automaton.handleAttuner(actor, target)
|
|
|
|
tpFactor = tpFactor + defIgnore
|
|
|
|
if tpFactor > 0 then
|
|
tpIgnoresDefense = true
|
|
end
|
|
end
|
|
|
|
-- Target Defense Modifiers.
|
|
if tpIgnoresDefense then
|
|
local ignoreDefenseFactor = 1 - tpFactor
|
|
|
|
targetDefense = math.max(1, math.floor(targetDefense * ignoreDefenseFactor))
|
|
end
|
|
|
|
if isCannonball then
|
|
actorAttack = actor:getStat(xi.mod.DEF)
|
|
end
|
|
|
|
-- Actor Attack / Target Defense ratio
|
|
if targetDefense ~= 0 then
|
|
baseRatio = actorAttack / targetDefense
|
|
end
|
|
|
|
----------------------------------------
|
|
-- Step 2: cRatio (Level correction, corrected ratio) Zone based!
|
|
----------------------------------------
|
|
local levelDifFactor = 0
|
|
|
|
-- https://forum.square-enix.com/ffxi/threads/31310-March-27-2013-(JST)-Version-Update
|
|
-- TODO: There is some weirdness with needing 2 levels to start level correction in retail
|
|
-- It is not currently implemented.
|
|
if applyLevelCorrection then
|
|
levelDifFactor = (actor:getMainLvl() - target:getMainLvl()) * 3 / 64 -- 3/64 from JP model which fits better
|
|
end
|
|
|
|
-- Only players suffer from negative level difference.
|
|
if
|
|
not actor:isPC() and
|
|
levelDifFactor < 0
|
|
then
|
|
levelDifFactor = 0
|
|
end
|
|
|
|
-- Players do not get positive level correction, only monsters
|
|
if
|
|
actor:isPC() and
|
|
levelDifFactor > 0
|
|
then
|
|
levelDifFactor = 0
|
|
end
|
|
|
|
----------------------------------------
|
|
-- Step 3: wRatio and pDif Caps (Melee)
|
|
----------------------------------------
|
|
local wRatio = baseRatio + (isCritical and 1 or 0)
|
|
local pDifUpperCap = 0
|
|
local pDifLowerCap = 0
|
|
local damageLimitPlus = actor:getMod(xi.mod.DAMAGE_LIMIT) / 100
|
|
local damageLimitPercent = 1 + actor:getMod(xi.mod.DAMAGE_LIMITP) / 100
|
|
local pDifFinalCap = 0
|
|
|
|
if actor:isPC() then
|
|
pDifFinalCap = (xi.combat.physical.pDifWeaponCapTable[weaponType] + damageLimitPlus) * damageLimitPercent + (isCritical and 1 or 0)
|
|
|
|
local sRatio = getSpikeRatio(true, wRatio)
|
|
|
|
if math.randomInt(1, 10000) / 10000 <= sRatio then
|
|
return 1.0
|
|
end
|
|
|
|
pDifLowerCap, pDifUpperCap = xi.combat.physical.wRatioCapPC(wRatio, pDifFinalCap)
|
|
else
|
|
-- Mobs and pets, unconfirmed if pets use this same formula
|
|
-- corrected mobs have 2.0 pdif + 1.0 for crits, with level correction added after the fact
|
|
-- non-corrected mobs have 4.0 pdif cap, but there is some indication that ilvl may go up to 8.0
|
|
local basePDIF = applyLevelCorrection and 2 or 4
|
|
local critBonus = (applyLevelCorrection and isCritical) and 1 or 0
|
|
pDifFinalCap = (basePDIF + damageLimitPlus) * damageLimitPercent + critBonus
|
|
|
|
local sRatio = getSpikeRatio(false, wRatio)
|
|
|
|
if math.randomInt(1, 10000) / 10000 <= sRatio then
|
|
return 1.0
|
|
end
|
|
|
|
pDifLowerCap, pDifUpperCap = xi.combat.physical.wRatioCapOthers(wRatio, pDifFinalCap)
|
|
end
|
|
|
|
-- Apply level correction to UL/LL
|
|
-- https://www.ffxiah.com/forum/topic/57989/post-2016-level-correction-testing/
|
|
-- Dice roll the 50/50 chance to select two different bounds. Mote has not yet implemented the spike by the time of this post so his ratio is not 50/50 rate.
|
|
-- His model at the time and implemented spike, so the (0.0, 0.5) bounds also looks different
|
|
-- https://www.bluegartr.com/threads/108161-pDif-and-damage?p=5007487&viewfull=1#post5007487
|
|
local upperMax = math.randomInt(0, 1) == 0 and 0.5 or 0
|
|
local upperBound = math.max(pDifUpperCap + levelDifFactor, upperMax)
|
|
local lowerbound = math.max(pDifLowerCap + levelDifFactor, 0)
|
|
|
|
if upperBound == 0 then
|
|
return 0
|
|
end
|
|
|
|
pDif = math.randomInt(lowerbound * 1000, upperBound * 1000) / 1000
|
|
|
|
----------------------------------------
|
|
-- Step 4: Melee random factor.
|
|
----------------------------------------
|
|
local meleeRandom = 1 + math.randomInt(0, 5) * 0.01 -- 5 distinct values
|
|
|
|
pDif = pDif * meleeRandom
|
|
|
|
-- Crit damage bonus is a final modifier
|
|
if isCritical then
|
|
local critDamageBonus = utils.clamp(actor:getMod(xi.mod.CRIT_DMG_INCREASE) - target:getMod(xi.mod.CRIT_DEF_BONUS), 0, 100)
|
|
pDif = pDif * (100 + critDamageBonus) / 100
|
|
end
|
|
|
|
return pDif
|
|
end
|
|
|
|
---@param actor CBaseEntity
|
|
---@param target CBaseEntity
|
|
---@param weaponType xi.skill
|
|
---@param wsAttackMod number
|
|
---@param isCritical boolean
|
|
---@param applyLevelCorrection boolean
|
|
---@param tpIgnoresDefense boolean
|
|
---@param tpFactor number
|
|
---@param isWeaponskill boolean
|
|
---@param bonusRangedAttack integer
|
|
xi.combat.physical.calculateRangedPDIF = function(actor, target, weaponType, wsAttackMod, isCritical, applyLevelCorrection, tpIgnoresDefense, tpFactor, isWeaponskill, bonusRangedAttack)
|
|
local pDif = 0
|
|
|
|
----------------------------------------
|
|
-- Step 1: Attack / Defense Ratio
|
|
----------------------------------------
|
|
local baseRatio = 0
|
|
local actorAttack = 0
|
|
local targetDefense = math.max(1, target:getStat(xi.mod.DEF))
|
|
local flourishBonus = 1
|
|
local distancePenalty = 0
|
|
|
|
if not actor:isMob() then
|
|
distancePenalty = xi.combat.ranged.attackDistancePenalty(actor, target)
|
|
end
|
|
|
|
-- Actor Weaponskill Specific Attack modifiers.
|
|
-- TODO: verify this actually works on ranged WS
|
|
if isWeaponskill then
|
|
local flourishEffect = actor:getStatusEffect(xi.effect.BUILDING_FLOURISH)
|
|
|
|
if flourishEffect and flourishEffect:getPower() >= 2 then -- 2 or more Finishing Moves used.
|
|
local meritCount = flourishEffect:getSubPower()
|
|
|
|
flourishBonus = 1.25 + 0.01 * meritCount -- +1% attack bonus per merit -- TODO: do the merits apply even when FMs are < 2?
|
|
end
|
|
end
|
|
|
|
-- TODO: it is unknown if ws attack mod and flourish bonus are additive or multiplicative
|
|
actorAttack = math.max(1, math.floor((actor:getStat(xi.mod.RATT) + bonusRangedAttack - distancePenalty) * wsAttackMod * flourishBonus))
|
|
|
|
-- Target Defense Modifiers.
|
|
local ignoreDefenseFactor = 1
|
|
|
|
if tpIgnoresDefense then
|
|
ignoreDefenseFactor = 1 - tpFactor
|
|
|
|
targetDefense = math.max(1, math.floor(targetDefense * ignoreDefenseFactor))
|
|
end
|
|
|
|
if targetDefense ~= 0 then
|
|
baseRatio = actorAttack / targetDefense
|
|
end
|
|
|
|
----------------------------------------
|
|
-- Step 2: cRatio (Level correction, corrected ratio) Zone based!
|
|
----------------------------------------
|
|
local levelDifFactor = 0
|
|
|
|
-- Mod-based bypass for ranged level correction
|
|
if actor:isPC() and actor:getMod(xi.mod.RA_IGNORE_LVL_DIFF) > 0 then
|
|
applyLevelCorrection = false
|
|
end
|
|
|
|
-- https://forum.square-enix.com/ffxi/threads/31310-March-27-2013-(JST)-Version-Update
|
|
-- TODO: There is some weirdness with needing 2 levels to start level correction in retail
|
|
-- It is not currently implemented.
|
|
if applyLevelCorrection then
|
|
levelDifFactor = (actor:getMainLvl() - target:getMainLvl()) * (3 / 128) -- half the melee correction
|
|
end
|
|
|
|
-- Only players suffer from negative level difference.
|
|
if
|
|
not actor:isPC() and
|
|
levelDifFactor < 0
|
|
then
|
|
levelDifFactor = 0
|
|
end
|
|
|
|
-- Players do not get positive level correction, only monsters
|
|
if
|
|
actor:isPC() and
|
|
levelDifFactor > 0
|
|
then
|
|
levelDifFactor = 0
|
|
end
|
|
|
|
local cRatio = utils.clamp(baseRatio, 0, 10) -- Clamp for the lower limit, mainly.
|
|
|
|
-- TODO: Presumably, pets get a Cap here if the target checks as 'Too Weak'. More info needed.
|
|
|
|
----------------------------------------
|
|
-- Step 3: pDif Caps (Ranged)
|
|
----------------------------------------
|
|
local pDifUpperCap = 0
|
|
local pDifLowerCap = 0
|
|
local damageLimitPlus = actor:getMod(xi.mod.DAMAGE_LIMIT) / 100
|
|
local damageLimitPercent = 1 + actor:getMod(xi.mod.DAMAGE_LIMITP) / 100
|
|
local pDifFinalCap = 0
|
|
|
|
if actor:isPC() then
|
|
pDifFinalCap = (xi.combat.physical.pDifWeaponCapTable[weaponType] + damageLimitPlus) * damageLimitPercent -- Added damage limit bonuses
|
|
else
|
|
-- 4.0 is guessed. there is some indication that mob pdif can go to 8.0 in ilvl content
|
|
-- 3.0 with level correction matches player ranged pdif cap for 2013 and may need verification
|
|
local basePDIF = applyLevelCorrection and 3 or 4
|
|
pDifFinalCap = (basePDIF + damageLimitPlus) * damageLimitPercent
|
|
end
|
|
|
|
pDif = utils.clamp(pDif, 0, pDifFinalCap)
|
|
|
|
-- pDIF upper and lower caps.
|
|
if cRatio < 0.9 then
|
|
pDifUpperCap = cRatio * 10 / 9
|
|
pDifLowerCap = cRatio
|
|
elseif cRatio < 1.1 then
|
|
pDifUpperCap = 1
|
|
pDifLowerCap = 1
|
|
else
|
|
pDifUpperCap = math.min(cRatio, pDifFinalCap)
|
|
pDifLowerCap = math.min(cRatio * 20 / 19 - 3 / 19, pDifFinalCap)
|
|
end
|
|
|
|
-- Add in level correction
|
|
pDifUpperCap = pDifUpperCap + levelDifFactor
|
|
pDifLowerCap = pDifLowerCap + levelDifFactor
|
|
|
|
pDif = math.randomInt(pDifLowerCap * 1000, pDifUpperCap * 1000) / 1000
|
|
|
|
-- do not go negative, rolls below zero (proportionally) need to be rolled
|
|
pDif = math.max(pDif, 0)
|
|
|
|
----------------------------------------
|
|
-- Step 4: Ranged critical factor. Bypasses caps.
|
|
----------------------------------------
|
|
if isCritical then
|
|
pDif = pDif * 1.25
|
|
end
|
|
|
|
-- Step 5: TODO: True Shot.
|
|
|
|
-- Crit damage bonus is a final modifier
|
|
if isCritical then
|
|
local critDamageBonus = utils.clamp(actor:getMod(xi.mod.CRIT_DMG_INCREASE) + actor:getMod(xi.mod.RANGED_CRIT_DMG_INCREASE) - target:getMod(xi.mod.CRIT_DEF_BONUS), 0, 100)
|
|
pDif = pDif * (100 + critDamageBonus) / 100
|
|
end
|
|
|
|
return pDif
|
|
end
|
|
|
|
-----------------------------------
|
|
-- Critical hit rate operations
|
|
-----------------------------------
|
|
-- dStat: Critical hit rate bonus from DEX vs AGI difference.
|
|
xi.combat.physical.criticalRateFromStatDiff = function(actor, target)
|
|
local statBonus = 0
|
|
|
|
local dDex = actor:getStat(xi.mod.DEX) - target:getStat(xi.mod.AGI)
|
|
|
|
if dDex > 50 then
|
|
statBonus = 0.15
|
|
elseif dDex >= 40 then
|
|
statBonus = (dDex - 35) / 100
|
|
elseif dDex >= 30 then
|
|
statBonus = 0.04
|
|
elseif dDex >= 20 then
|
|
statBonus = 0.03
|
|
elseif dDex >= 14 then
|
|
statBonus = 0.02
|
|
elseif dDex >= 7 then
|
|
statBonus = 0.01
|
|
end
|
|
|
|
return statBonus
|
|
end
|
|
|
|
-- dStat: Ranged critical hit rate bonus from AGI vs AGI difference.
|
|
xi.combat.physical.criticalRateFromAGIDiff = function(actor, target)
|
|
local statBonus = 0
|
|
|
|
local dAgi = math.max(0, actor:getStat(xi.mod.AGI) - target:getStat(xi.mod.AGI))
|
|
statBonus = math.floor(dAgi / 10)
|
|
statBonus = statBonus / 100
|
|
|
|
return statBonus
|
|
end
|
|
|
|
-- Innin: Critical hit rate bonus when actor is behind target.
|
|
xi.combat.physical.criticalRateFromInnin = function(actor, target)
|
|
local inninBonus = 0
|
|
|
|
if
|
|
actor:hasStatusEffect(xi.effect.INNIN) and
|
|
actor:isBehind(target, 23)
|
|
then
|
|
inninBonus = actor:getStatusEffect(xi.effect.INNIN):getPower()
|
|
end
|
|
|
|
return inninBonus
|
|
end
|
|
|
|
-- Fencer: Critical hit rate bonus when actor is only wielding with main hand.
|
|
xi.combat.physical.criticalRateFromFencer = function(actor)
|
|
local fencerBonus = 0
|
|
-- TODO: do any Trusts or mobs ever get Fencer bonuses?
|
|
|
|
if actor:getObjType() == xi.objType.PC then
|
|
local mainEquip = actor:getStorageItem(0, 0, xi.slot.MAIN)
|
|
local subEquip = actor:getStorageItem(0, 0, xi.slot.SUB)
|
|
if
|
|
mainEquip and
|
|
not mainEquip:isTwoHanded() and -- No 2 handed weapons.
|
|
not mainEquip:isHandToHand() and -- No 2 handed weapons.
|
|
(subEquip == nil or subEquip:getSkillType() == xi.skill.NONE or subEquip:isShield()) -- Only shields allowed in sub.
|
|
then
|
|
fencerBonus = actor:getMod(xi.mod.FENCER_CRITHITRATE) / 100
|
|
end
|
|
end
|
|
|
|
return fencerBonus
|
|
end
|
|
|
|
-- Critical rate from Building Flourish.
|
|
-- TODO: Study case where if we can attach modifiers to the effect itself, both this and the effect may need refactoring.
|
|
xi.combat.physical.criticalRateFromFlourish = function(actor)
|
|
local buildingFlourishBonus = 0
|
|
|
|
if actor:hasStatusEffect(xi.effect.BUILDING_FLOURISH) then
|
|
local effectPower = actor:getStatusEffect(xi.effect.BUILDING_FLOURISH):getPower()
|
|
local effectSubPower = actor:getStatusEffect(xi.effect.BUILDING_FLOURISH):getSubPower()
|
|
|
|
if effectPower >= 3 then
|
|
buildingFlourishBonus = (10 + effectSubPower) / 100
|
|
end
|
|
end
|
|
|
|
return buildingFlourishBonus
|
|
end
|
|
|
|
---@param actor CBaseEntity
|
|
---@param slot xi.slot
|
|
---@return number
|
|
xi.combat.physical.criticalRateFromWeaponSlot = function(actor, slot)
|
|
if actor:isPC() then
|
|
return actor:getGearModFromSlot(slot, xi.mod.CRITHITRATE_ONLY_WEP) / 100
|
|
end
|
|
|
|
return 0
|
|
end
|
|
|
|
-- Critical rate master function.
|
|
---@param actor CBaseEntity
|
|
---@param target CBaseEntity
|
|
---@param actorTP number
|
|
---@param slot xi.slot
|
|
---@param optCritModTable table?
|
|
---@return integer
|
|
xi.combat.physical.calculateSwingCriticalRate = function(actor, target, actorTP, slot, optCritModTable)
|
|
-- See reference at https://www.bg-wiki.com/ffxi/Critical_Hit_Rate
|
|
local finalCriticalRate = 0
|
|
local baseCriticalRate = 0.05
|
|
local statBonus = xi.combat.physical.criticalRateFromStatDiff(actor, target)
|
|
local inninBonus = xi.combat.physical.criticalRateFromInnin(actor, target)
|
|
local fencerBonus = xi.combat.physical.criticalRateFromFencer(actor)
|
|
local buildingFlourishBonus = xi.combat.physical.criticalRateFromFlourish(actor)
|
|
local weaponSlotBonus = xi.combat.physical.criticalRateFromWeaponSlot(actor, slot)
|
|
local modifierBonus = actor:getMod(xi.mod.CRITHITRATE) / 100
|
|
local meritBonus = actor:getMerit(xi.merit.CRIT_HIT_RATE) / 100
|
|
local targetCriticalEvasion = target:getMod(xi.mod.CRITICAL_HIT_EVASION) / 100
|
|
local targetMeritPenalty = target:getMerit(xi.merit.ENEMY_CRIT_RATE) / 100
|
|
local tpFactor = 0
|
|
|
|
-- For weaponskills.
|
|
if optCritModTable then
|
|
tpFactor = xi.combat.physical.calculateTPfactor(actorTP, optCritModTable)
|
|
end
|
|
|
|
-- Add all different bonuses and clamp.
|
|
finalCriticalRate = baseCriticalRate + statBonus + inninBonus + fencerBonus + buildingFlourishBonus + weaponSlotBonus + modifierBonus + meritBonus - targetCriticalEvasion - targetMeritPenalty + tpFactor
|
|
|
|
return utils.clamp(finalCriticalRate, 0.05, 1) -- TODO: Need confirmation of no upper cap.
|
|
end
|
|
|
|
---@param actor CBaseEntity
|
|
---@param target CBaseEntity
|
|
---@param actorTP number
|
|
---@param slot xi.slot
|
|
---@param optCritModTable table?
|
|
---@return integer
|
|
xi.combat.physical.calculateRangedCriticalRate = function(actor, target, actorTP, slot, optCritModTable)
|
|
-- See reference at https://www.bg-wiki.com/ffxi/Critical_Hit_Rate
|
|
local finalCriticalRate = 0
|
|
local baseCriticalRate = 0.05
|
|
local statBonus = xi.combat.physical.criticalRateFromAGIDiff(actor, target)
|
|
local inninBonus = xi.combat.physical.criticalRateFromInnin(actor, target)
|
|
local fencerBonus = xi.combat.physical.criticalRateFromFencer(actor)
|
|
local buildingFlourishBonus = xi.combat.physical.criticalRateFromFlourish(actor)
|
|
local weaponSlotBonus = xi.combat.physical.criticalRateFromWeaponSlot(actor, slot)
|
|
local modifierBonus = actor:getMod(xi.mod.CRITHITRATE) / 100
|
|
local meritBonus = actor:getMerit(xi.merit.CRIT_HIT_RATE) / 100
|
|
local targetCriticalEvasion = target:getMod(xi.mod.CRITICAL_HIT_EVASION) / 100
|
|
local targetMeritPenalty = target:getMerit(xi.merit.ENEMY_CRIT_RATE) / 100
|
|
local tpFactor = 0
|
|
|
|
-- For weaponskills.
|
|
if optCritModTable then
|
|
tpFactor = xi.combat.physical.calculateTPfactor(actorTP, optCritModTable)
|
|
end
|
|
|
|
-- Add all different bonuses and clamp.
|
|
finalCriticalRate = baseCriticalRate + statBonus + inninBonus + fencerBonus + buildingFlourishBonus + weaponSlotBonus + modifierBonus + meritBonus - targetCriticalEvasion - targetMeritPenalty + tpFactor
|
|
|
|
return utils.clamp(finalCriticalRate, 0.05, 1) -- TODO: Need confirmation of no upper cap.
|
|
end
|
|
|
|
xi.combat.physical.calculateNumberOfHits = function(actor, additionalParamsHere)
|
|
end
|
|
|
|
-- Main Hit (First hit) Functions.
|
|
xi.combat.physical.calculateMainHitAccuracy = function(actor, additionalParamsHere)
|
|
end
|
|
|
|
xi.combat.physical.calculateMainHitCritical = function(actor, additionalParamsHere)
|
|
end
|
|
|
|
xi.combat.physical.calculateMainHitDamage = function(actor, additionalParamsHere)
|
|
end
|
|
|
|
-- Secondary Hits (All other) Functions.
|
|
xi.combat.physical.calculateSecondaryHitAccuracy = function(actor, additionalParamsHere)
|
|
end
|
|
|
|
xi.combat.physical.calculateSecondaryHitCritical = function(actor, additionalParamsHere)
|
|
end
|
|
|
|
xi.combat.physical.calculateSecondaryHitDamage = function(actor, additionalParamsHere)
|
|
end
|
|
|
|
xi.combat.physical.canParry = function(defender, attacker)
|
|
local canParry = false
|
|
|
|
if
|
|
defender:isFacing(attacker) and
|
|
defender:isEngaged() and
|
|
not defender:hasPreventActionEffect(true) -- Not stunned, slept, etc, but can parry when charmed
|
|
then
|
|
if defender:isPC() then
|
|
if defender:getSkillRank(xi.skill.PARRY) == 0 then
|
|
return false
|
|
end
|
|
|
|
local mainWeapon = defender:getEquippedItem(xi.slot.MAIN)
|
|
|
|
if mainWeapon then
|
|
canParry = mainWeapon:getSkillType() ~= xi.skill.HAND_TO_HAND
|
|
end
|
|
else
|
|
canParry = defender:getMobMod(xi.mobMod.CAN_PARRY) > 0
|
|
end
|
|
end
|
|
|
|
return canParry
|
|
end
|
|
|
|
-- Rough implementation of the following sheet, though we still dont know 100% how it all works
|
|
-- https://docs.google.com/spreadsheets/d/1wtS0d4nNqosMwFuHWb7fkCcj4naAvwhnPocf3qSGJjk/edit?gid=0#gid=0
|
|
xi.combat.physical.calculateParryRate = function(defender, attacker)
|
|
local parryRate = 0
|
|
local attackerSkill = 0
|
|
local defenderSkill = 0
|
|
|
|
if defender:isPC() then
|
|
defenderSkill = defender:getSkillLevel(xi.skill.PARRY) + defender:getMod(xi.mod.PARRY) + defender:getILvlParry()
|
|
else
|
|
defenderSkill = xi.data.skillLevel.getSkillCap(defender:getMainLvl(), xi.skillRank.A_PLUS)
|
|
end
|
|
|
|
if attacker:isPC() then
|
|
attackerSkill = attacker:getSkillLevel(attacker:getWeaponSkillType(xi.slot.MAIN)) + attacker:getILvlSkill()
|
|
else
|
|
attackerSkill = xi.data.skillLevel.getSkillCap(attacker:getMainLvl(), xi.skillRank.A_PLUS)
|
|
end
|
|
|
|
local skillDelta = defenderSkill - attackerSkill
|
|
|
|
if skillDelta <= 5 then -- between -10 and 5
|
|
-- Matches the data
|
|
-- X = 6
|
|
-- but the slope is 36 / 9 for skill <= 5
|
|
parryRate = math.floor(10 + (skillDelta - 6) / (36 / 9))
|
|
|
|
else -- between 6 and 105
|
|
-- In the formula "10 + (skill - X) / (60 / 9)"
|
|
-- where X = 6, this matches the level 43 chigoe data exactly, and X = 11 matches the existing level 64 chigoe data
|
|
-- We don't know what modifies X yet, (expansion, level, arbitrary?)
|
|
-- Level 64 chigoe data is incomplete so we will use level 43 for now.
|
|
parryRate = math.floor(10 + (skillDelta - 6) / (60 / 9))
|
|
end
|
|
|
|
parryRate = utils.clamp(parryRate, 5, 25)
|
|
|
|
-- Issekigan grants parry rate bonus
|
|
-- from best available data if you already capped out at 25% parry it grants another 25% bonus for ~50% parry rate
|
|
if defender:hasStatusEffect(xi.effect.ISSEKIGAN) then
|
|
parryRate = parryRate + defender:getStatusEffect(xi.effect.ISSEKIGAN):getPower()
|
|
end
|
|
|
|
-- Inquartata grants a flat parry rate bonus
|
|
parryRate = parryRate + defender:getMod(xi.mod.INQUARTATA)
|
|
|
|
return parryRate
|
|
end
|
|
|
|
xi.combat.physical.canGuard = function(defender, attacker)
|
|
local canGuard = false
|
|
|
|
-- per testing done by Genome guard can proc when petrified, stunned, or asleep
|
|
-- https://genomeffxi.livejournal.com/18269.html
|
|
if
|
|
defender:isFacing(attacker) and
|
|
defender:isEngaged() and
|
|
not defender:hasPreventActionEffect(true) -- Not stunned, slept, etc, but can guard when charmed
|
|
then
|
|
if defender:isPC() and defender:getSkillRank(xi.skill.GUARD) > 0 then
|
|
local mainWeapon = defender:getEquippedItem(xi.slot.MAIN)
|
|
canGuard = (not mainWeapon) or mainWeapon:getSkillType() == xi.skill.HAND_TO_HAND
|
|
elseif
|
|
defender:isMob() or
|
|
defender:isPet() or
|
|
defender:isTrust()
|
|
then
|
|
canGuard = (defender:getMainJob() == xi.job.MNK or defender:getMainJob() == xi.job.PUP) and defender:getMobMod(xi.mobMod.CANNOT_GUARD) == 0
|
|
end
|
|
end
|
|
|
|
return canGuard
|
|
end
|
|
|
|
xi.combat.physical.calculateGuardRate = function(defender, attacker)
|
|
local guardRate = 0
|
|
local attackerSkill = 0
|
|
local defenderSkill = 0
|
|
|
|
if defender:isPC() then
|
|
defenderSkill = defender:getSkillLevel(xi.skill.GUARD) + defender:getMod(xi.mod.GUARD) + defender:getILvlParry() -- getILvlParry also gets guard (h2h cannot have parry on the weapon)
|
|
else
|
|
defenderSkill = xi.data.skillLevel.getSkillCap(defender:getMainLvl(), xi.skillRank.A_PLUS)
|
|
end
|
|
|
|
if attacker:isPC() then
|
|
attackerSkill = attacker:getSkillLevel(attacker:getWeaponSkillType(xi.slot.MAIN)) + attacker:getILvlSkill()
|
|
else
|
|
attackerSkill = xi.data.skillLevel.getSkillCap(attacker:getMainLvl(), xi.skillRank.A_PLUS)
|
|
end
|
|
|
|
local skillDelta = defenderSkill - attackerSkill
|
|
|
|
-- This is approximated from parry
|
|
-- Two data points showed that Guard was approximately 1% better, so skillDelta is at _least_ 6 lower on the same target
|
|
-- The target was a lvl 43 chigoe, using the same parry skill vs guard skill with the known parrying rate data
|
|
-- This is a placeholder and is likely more accurate than the previous code.
|
|
if skillDelta <= 6 then
|
|
guardRate = math.floor(10 + skillDelta / (36 / 9))
|
|
else
|
|
guardRate = math.floor(10 + skillDelta / (60 / 9))
|
|
end
|
|
|
|
-- Dodge's guard bonus goes over the cap
|
|
guardRate = utils.clamp(guardRate, 5, 25) + defender:getMod(xi.mod.ADDITIVE_GUARD)
|
|
|
|
return guardRate
|
|
end
|
|
|
|
xi.combat.physical.canBlock = function(defender, attacker)
|
|
local canBlock = false
|
|
|
|
if
|
|
defender:isFacing(attacker) and
|
|
not defender:hasPreventActionEffect(true)
|
|
then
|
|
if defender:isPC() and defender:getSkillRank(xi.skill.SHIELD) > 0 then
|
|
local shield = defender:getEquippedItem(xi.slot.SUB)
|
|
if shield then
|
|
canBlock = shield:isShield()
|
|
end
|
|
elseif
|
|
defender:isMob() or
|
|
defender:isPet() or
|
|
defender:isTrust()
|
|
then
|
|
canBlock = defender:getMobMod(xi.mobMod.CAN_SHIELD_BLOCK) > 0
|
|
end
|
|
end
|
|
|
|
return canBlock
|
|
end
|
|
|
|
xi.combat.physical.calculateBlockRate = function(defender, attacker)
|
|
local blockRate = 0
|
|
local shieldSize = 3
|
|
local skillModifier = 0
|
|
local palisadeMod = defender:getMod(xi.mod.PALISADE_BLOCK_BONUS)
|
|
local reprisalMult = 1.0
|
|
|
|
-- assume bare hands case
|
|
local attackerSkillType = xi.skill.HAND_TO_HAND
|
|
if not attacker:isUsingH2H() then
|
|
attackerSkillType = attacker:getWeaponSkillType(xi.slot.MAIN)
|
|
end
|
|
|
|
local attackSkill = attacker:getSkillLevel(attackerSkillType)
|
|
local blockSkill = defender:getSkillLevel(xi.skill.SHIELD)
|
|
|
|
if defender:isPC() then
|
|
local shield = defender:getEquippedItem(xi.slot.SUB)
|
|
-- already checked in canBlock but check again here to make sure
|
|
if shield and shield:isShield() then
|
|
shieldSize = shield:getShieldSize()
|
|
else
|
|
return 0
|
|
end
|
|
elseif
|
|
defender:isMob() or
|
|
defender:isPet() or
|
|
defender:isTrust()
|
|
then
|
|
-- already checked in canBlock but check again here to make sure
|
|
if defender:getMobMod(xi.mobMod.CAN_SHIELD_BLOCK) > 0 then
|
|
blockRate = defender:getMod(xi.mod.SHIELDBLOCKRATE)
|
|
-- automations are a special case
|
|
if defender:isAutomaton() then
|
|
skillModifier = (defender:getSkillLevel(xi.skill.AUTOMATON_MELEE) - attackSkill) * 0.215
|
|
return math.max(0, blockRate + skillModifier)
|
|
-- mobs and trusts use max skill for job and level
|
|
elseif defender:isTrust() then
|
|
-- TODO: check trust type for ilvl > 99 when implemented
|
|
blockSkill = defender:getMaxSkillLevel(math.min(defender:getMainLvl(), 99), defender:getMainJob(), xi.skill.SHIELD)
|
|
else
|
|
blockSkill = defender:getMaxSkillLevel(defender:getMainLvl(), defender:getMainJob(), xi.skill.SHIELD)
|
|
end
|
|
else -- No block mobmod so zero rate
|
|
return 0
|
|
end
|
|
end
|
|
|
|
if defender:isPC() then
|
|
-- get blockrate from table and use default value of 0
|
|
blockRate = shieldSizeToBlockRateTable[shieldSize] or 0
|
|
end
|
|
|
|
-- Check for Reprisal and adjust skill and block rate bonus multiplier
|
|
if defender:hasStatusEffect(xi.effect.REPRISAL) then
|
|
blockSkill = blockSkill * 1.15
|
|
reprisalMult = 1.5
|
|
|
|
-- Adamas and Priwen set the multiplier to 3.0x while equipped
|
|
if defender:getMod(xi.mod.REPRISAL_BLOCK_BONUS) > 0 then
|
|
reprisalMult = 3.0
|
|
end
|
|
end
|
|
|
|
skillModifier = (blockSkill - attackSkill) * 0.2325
|
|
|
|
-- Add skill and Palisade bonuses and multiply by Reprisals bonus
|
|
blockRate = (blockRate + skillModifier + palisadeMod) * reprisalMult
|
|
|
|
-- Apply the lower and upper caps
|
|
blockRate = utils.clamp(blockRate, 5, 100)
|
|
|
|
return blockRate
|
|
end
|
|
|
|
xi.combat.physical.getDamageReductionForBlock = function(defender, attacker, damage)
|
|
-- save original damage for comparison
|
|
local originalDamage = damage
|
|
|
|
-- do not reduce if damage is negative
|
|
if damage > 0 then
|
|
-- shield def bonus is a flat raw damage reduction that occurs before absorb
|
|
damage = math.max(0, damage - defender:getMod(xi.mod.SHIELD_DEF_BONUS))
|
|
|
|
if defender:isPC() then
|
|
local shield = defender:getEquippedItem(xi.slot.SUB)
|
|
local absorb = utils.clamp(100 - shield:getShieldAbsorptionRate(), 0, 100)
|
|
damage = math.floor(damage * (absorb / 100))
|
|
else
|
|
damage = math.floor(damage * 0.5)
|
|
end
|
|
end
|
|
|
|
-- return the difference between original and new damage
|
|
-- in other words the damage reduction (as a flat value)
|
|
return originalDamage - damage
|
|
end
|
|
|
|
xi.combat.physical.isBlocked = function(defender, attacker)
|
|
local blocked = false
|
|
|
|
if xi.combat.physical.canBlock(defender, attacker) then
|
|
|
|
if xi.combat.physical.calculateBlockRate(defender, attacker) * 100 >= math.randomInt(1, 10000) then
|
|
blocked = true
|
|
end
|
|
|
|
-- Handle skill ups.
|
|
if
|
|
defender:isPC() and
|
|
(blocked or -- We blocked
|
|
not xi.settings.map.DEFENSIVE_OLD_SKILLUP_STYLE) -- Old style skillup is not enabled
|
|
then
|
|
defender:trySkillUp(xi.skill.SHIELD, attacker:getMainLvl())
|
|
end
|
|
end
|
|
|
|
return blocked
|
|
end
|
|
|
|
xi.combat.physical.isParried = function(defender, attacker)
|
|
local parried = false
|
|
|
|
if xi.combat.physical.canParry(defender, attacker) then
|
|
local isPC = defender:isPC()
|
|
|
|
if xi.combat.physical.calculateParryRate(defender, attacker) * 100 >= math.randomInt(1, 10000) then
|
|
parried = true
|
|
|
|
-- https://www.bg-wiki.com/ffxi/Turms_Mittens
|
|
if
|
|
defender:getMod(xi.mod.PARRY_HP_RECOVERY) > 0 and
|
|
not defender:hasStatusEffect(xi.effect.CURSE_II)
|
|
then
|
|
local recoveryValue = defender:getMod(xi.mod.PARRY_HP_RECOVERY)
|
|
defender:addHP(recoveryValue)
|
|
end
|
|
|
|
if isPC then
|
|
-- handle tactical parry
|
|
if defender:hasTrait(xi.trait.TACTICAL_PARRY) then
|
|
defender:addTP(defender:getMod(xi.mod.TACTICAL_PARRY))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Handle skill ups.
|
|
if
|
|
isPC and
|
|
(parried or -- We parried
|
|
not xi.settings.map.DEFENSIVE_OLD_SKILLUP_STYLE) -- Old style skillup is not enabled
|
|
then
|
|
defender:trySkillUp(xi.skill.PARRY, attacker:getMainLvl())
|
|
end
|
|
end
|
|
|
|
return parried
|
|
end
|
|
|
|
xi.combat.physical.isGuarded = function(defender, attacker)
|
|
local guarded = false
|
|
|
|
if xi.combat.physical.canGuard(defender, attacker) then
|
|
local isPC = defender:isPC()
|
|
if xi.combat.physical.calculateGuardRate(defender, attacker) * 100 >= math.randomInt(1, 10000) then
|
|
guarded = true
|
|
if isPC then
|
|
-- handle tactical guard
|
|
if defender:hasTrait(xi.trait.TACTICAL_GUARD) then
|
|
defender:addTP(defender:getMod(xi.mod.TACTICAL_GUARD))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Handle skill ups.
|
|
if
|
|
isPC and
|
|
(guarded or -- We guarded
|
|
not xi.settings.map.DEFENSIVE_OLD_SKILLUP_STYLE) -- Old style skillup is not enabled
|
|
then
|
|
defender:trySkillUp(xi.skill.GUARD, attacker:getMainLvl())
|
|
end
|
|
end
|
|
|
|
return guarded
|
|
end
|