418 lines
14 KiB
Lua
418 lines
14 KiB
Lua
-----------------------------------
|
|
-- Avatar Global Functions
|
|
-----------------------------------
|
|
require('scripts/globals/combat/physical_utilities')
|
|
-----------------------------------
|
|
xi = xi or {}
|
|
xi.summon = xi.summon or {}
|
|
|
|
local function getDexCritRate(source, target)
|
|
-- https://www.bg-wiki.com/bg/Critical_Hit_Rate
|
|
local dDex = source:getStat(xi.mod.DEX) - target:getStat(xi.mod.AGI)
|
|
local dDexAbs = math.abs(dDex)
|
|
local sign = 1
|
|
|
|
if dDex < 0 then
|
|
-- target has higher AGI so this will be a decrease to crit rate
|
|
sign = -1
|
|
end
|
|
|
|
-- default to +0 crit rate for a delta of 0-6
|
|
local critRate = 0
|
|
if dDexAbs > 39 then
|
|
-- 40-50: (dDEX-35)
|
|
critRate = dDexAbs - 35
|
|
elseif dDexAbs > 29 then
|
|
-- 30-39: +4
|
|
critRate = 4
|
|
elseif dDexAbs > 19 then
|
|
-- 20-29: +3
|
|
critRate = 3
|
|
elseif dDexAbs > 13 then
|
|
-- 14-19: +2
|
|
critRate = 2
|
|
elseif dDexAbs > 6 then
|
|
-- 7-13: +1
|
|
critRate = 1
|
|
end
|
|
|
|
-- Crit rate from stats caps at +-15
|
|
return math.min(critRate, 15) * sign
|
|
end
|
|
|
|
local function getRandRatio(wRatio)
|
|
local maxRatio = 4.25 -- 4.25 for Avatars, they count as 1H but same as mobs don't have a non-crit cap
|
|
|
|
local upperLimit = math.min(wRatio + 0.375, maxRatio)
|
|
if wRatio < 0.5 then
|
|
upperLimit = math.max(wRatio + 0.5, 0.5)
|
|
elseif wRatio < 0.7 then
|
|
upperLimit = 1
|
|
elseif wRatio < 1.2 then
|
|
upperLimit = wRatio + 0.3
|
|
elseif wRatio < 1.5 then
|
|
upperLimit = wRatio * 1.25
|
|
end
|
|
|
|
local lowerLimit = math.min(wRatio - 0.375, maxRatio)
|
|
if wRatio < 0.38 then
|
|
lowerLimit = math.max(wRatio, 0.5)
|
|
elseif wRatio < 1.25 then
|
|
lowerLimit = (wRatio * (1176 / 1024)) - (448 / 1024)
|
|
elseif wRatio < 1.51 then
|
|
lowerLimit = 1
|
|
elseif wRatio < 2.44 then
|
|
lowerLimit = (wRatio * (1176 / 1024)) - (755 / 1024)
|
|
end
|
|
|
|
-- Randomly pick a value between lower and upper limits for qRatio
|
|
local qRatio = lowerLimit + (math.random() * (upperLimit - lowerLimit))
|
|
|
|
return qRatio
|
|
end
|
|
|
|
local function avatarFTP(tp, ftp1, ftp2, ftp3)
|
|
if tp < 1000 then
|
|
tp = 1000
|
|
end
|
|
|
|
if tp >= 1000 and tp < 2000 then
|
|
return ftp1 + (ftp2 - ftp1) / 100 * (tp - 1000)
|
|
elseif tp >= 2000 and tp <= 3000 then
|
|
-- generate a straight line between ftp2 and ftp3 and find point @ tp
|
|
return ftp2 + (ftp3 - ftp2) / 100 * (tp - 2000)
|
|
end
|
|
|
|
return 1 -- no ftp mod
|
|
end
|
|
|
|
local function avatarHitDmg(weaponDmg, fSTR, pDif)
|
|
-- https://www.bg-wiki.com/bg/Physical_Damage
|
|
-- Physical Damage = Base Damage * pDIF
|
|
-- where Base Damange is defined as Weapon Damage + fSTR
|
|
return (weaponDmg + fSTR) * pDif
|
|
end
|
|
|
|
xi.summon.getSummoningSkillOverCap = function(avatar)
|
|
local summoner = avatar:getMaster()
|
|
local summoningSkill = summoner:getSkillLevel(xi.skill.SUMMONING_MAGIC)
|
|
local maxSkill = summoner:getMaxSkillLevel(avatar:getMainLvl(), xi.job.SMN, xi.skill.SUMMONING_MAGIC)
|
|
|
|
return math.max(summoningSkill - maxSkill, 0)
|
|
end
|
|
|
|
---@alias physicalAvatarSkillRetVal { damage: number, hitslanded: number, isCritical: boolean}
|
|
|
|
---@param avatar CBaseEntity
|
|
---@param target CBaseEntity
|
|
---@param skill CPetSkill|CMobSkill
|
|
---@param numberofhits number
|
|
---@param accmod number
|
|
---@param dmgmod number
|
|
---@param dmgmodsubsequent number
|
|
---@param tpeffect xi.mobskills.magicalTpBonus|xi.mobskills.physicalTpBonus
|
|
---@param mtp100 number
|
|
---@param mtp200 number
|
|
---@param mtp300 number
|
|
---@return physicalAvatarSkillRetVal
|
|
xi.summon.avatarPhysicalMove = function(avatar, target, skill, numberofhits, accmod, dmgmod, dmgmodsubsequent, tpeffect, mtp100, mtp200, mtp300)
|
|
local returninfo = {}
|
|
|
|
-- I have never read a limit on accuracy bonus from summoning skill which can currently go far past 200 over cap
|
|
-- current retail is over +250 skill so I am removing the cap, my SMN is at 695 total skill
|
|
local bonusAcc = xi.summon.getSummoningSkillOverCap(avatar)
|
|
|
|
-- Handle DA/TA/QA
|
|
-- TODO: handle Nirvana
|
|
local bonusHits = 0
|
|
local doubleRate = avatar:getMod(xi.mod.DOUBLE_ATTACK)
|
|
local tripleRate = avatar:getMod(xi.mod.TRIPLE_ATTACK)
|
|
local quadRate = avatar:getMod(xi.mod.QUAD_ATTACK)
|
|
|
|
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
|
|
end
|
|
|
|
-- Normal hits computed first
|
|
local hitrateFirst = xi.combat.physicalHitRate.getPhysicalHitRate(avatar, target, bonusAcc + 100, xi.attackAnimation.RIGHT_ATTACK, false)
|
|
local hitrateSubsequent = xi.combat.physicalHitRate.getPhysicalHitRate(avatar, target, bonusAcc, xi.attackAnimation.RIGHT_ATTACK, false)
|
|
|
|
-- Compute hits first so we can exit early
|
|
local firstHitLanded = false
|
|
local numHitsLanded = 0
|
|
local numHitsProcessed = 1
|
|
local finaldmg = 0
|
|
local didCrit = false
|
|
|
|
if math.random() < hitrateFirst then
|
|
firstHitLanded = true
|
|
numHitsLanded = numHitsLanded + 1
|
|
end
|
|
|
|
while numHitsProcessed < (numberofhits + bonusHits) do
|
|
if math.random() < hitrateSubsequent then
|
|
numHitsLanded = numHitsLanded + 1
|
|
end
|
|
|
|
numHitsProcessed = numHitsProcessed + 1
|
|
end
|
|
|
|
if numHitsLanded == 0 then
|
|
-- Missed everything we can exit early
|
|
finaldmg = 0
|
|
skill:setMsg(xi.msg.basic.SKILL_MISS)
|
|
else
|
|
-- https://www.bg-wiki.com/bg/Critical_Hit_Rate
|
|
-- Crit rate has a base of 5% and no cap, 0-100% are valid
|
|
-- Dex contribution to crit rate is capped and works in tiers
|
|
local baseCritRate = 5
|
|
local maxCritRate = 1 -- 100%
|
|
local minCritRate = 0 -- 0%
|
|
local critRate = baseCritRate + getDexCritRate(avatar, target) + avatar:getMod(xi.mod.CRITHITRATE)
|
|
|
|
critRate = critRate / 100
|
|
critRate = utils.clamp(critRate, minCritRate, maxCritRate)
|
|
|
|
local weaponDmg = avatar:getWeaponDmg()
|
|
local fSTR = xi.combat.physical.calculateMeleeStatFactor(avatar, target)
|
|
|
|
-- https://www.bg-wiki.com/bg/PDIF
|
|
-- https://www.bluegartr.com/threads/127523-pDIF-Changes-(Feb.-10th-2016)
|
|
local ratio = avatar:getStat(xi.mod.ATT) / target:getStat(xi.mod.DEF)
|
|
local cRatio = ratio
|
|
|
|
local shouldApplyLevelCorrection = xi.data.levelCorrection.isLevelCorrectedZone(avatar)
|
|
local levelDiff = math.min(avatar:getMainLvl() - target:getMainLvl(), 38) -- Max level diff is 38
|
|
|
|
if shouldApplyLevelCorrection then
|
|
-- Mobs, Avatars and pets only get bonuses, no penalties (or they are calculated differently)
|
|
if levelDiff > 0 then
|
|
local correction = levelDiff * 0.05
|
|
local cappedCorrection = math.min(correction, 1.9)
|
|
cRatio = cRatio + cappedCorrection
|
|
end
|
|
end
|
|
|
|
--Everything past this point is randomly computed per hit
|
|
|
|
numHitsProcessed = 0
|
|
local critAttackBonus = 1 + ((avatar:getMod(xi.mod.CRIT_DMG_INCREASE) - target:getMod(xi.mod.CRIT_DEF_BONUS)) / 100)
|
|
|
|
if firstHitLanded then
|
|
local wRatio = cRatio
|
|
local isCrit = math.random() < critRate
|
|
if isCrit then
|
|
wRatio = wRatio + 1
|
|
didCrit = true
|
|
end
|
|
|
|
local qRatio = getRandRatio(wRatio) -- Get a random ratio from min and max
|
|
local pDif = qRatio * (1 + (math.random() * 0.05)) -- Final pDif is qRatio randomized with a 1-1.05 multiplier
|
|
|
|
if isCrit then
|
|
pDif = pDif * critAttackBonus
|
|
end
|
|
|
|
finaldmg = avatarHitDmg(weaponDmg, fSTR, pDif) * dmgmod
|
|
numHitsProcessed = 1
|
|
end
|
|
|
|
while numHitsProcessed < numHitsLanded do
|
|
local wRatio = cRatio
|
|
local isCrit = math.random() < critRate
|
|
if isCrit then
|
|
wRatio = wRatio + 1
|
|
didCrit = true
|
|
end
|
|
|
|
local qRatio = getRandRatio(wRatio) -- Get a random ratio from min and max.
|
|
local pDif = qRatio * (1 + math.random() * 0.05) -- Final pDif is qRatio randomized with a 1-1.05 multiplier.
|
|
|
|
if isCrit then
|
|
pDif = pDif * critAttackBonus
|
|
end
|
|
|
|
finaldmg = finaldmg + (avatarHitDmg(weaponDmg, fSTR, pDif) * dmgmodsubsequent)
|
|
numHitsProcessed = numHitsProcessed + 1
|
|
end
|
|
|
|
-- apply ftp bonus
|
|
if tpeffect == xi.mobskills.physicalTpBonus.DMG_VARIES then
|
|
finaldmg = finaldmg * avatarFTP(avatar:getTP(), mtp100, mtp200, mtp300)
|
|
end
|
|
end
|
|
|
|
returninfo.damage = finaldmg
|
|
returninfo.hitslanded = numHitsLanded
|
|
|
|
skill:setAttackType(xi.attackType.PHYSICAL)
|
|
skill:setCritical(didCrit)
|
|
|
|
return returninfo
|
|
end
|
|
|
|
local attackTypeShields =
|
|
{
|
|
[xi.attackType.PHYSICAL] = xi.effect.PHYSICAL_SHIELD,
|
|
[xi.attackType.RANGED ] = xi.effect.ARROW_SHIELD,
|
|
[xi.attackType.MAGICAL ] = xi.effect.MAGIC_SHIELD,
|
|
}
|
|
|
|
---@param info magicalMobSkillRetVal|physicalAvatarSkillRetVal
|
|
---@param mob CBaseEntity
|
|
---@param skill CPetSkill|CMobSkill
|
|
---@param target CBaseEntity
|
|
---@param skilltype xi.attackType
|
|
---@param damagetype xi.damageType
|
|
---@param shadowbehav xi.mobskills.shadowBehavior?
|
|
---@return number
|
|
xi.summon.avatarFinalAdjustments = function(info, mob, skill, target, skilltype, damagetype, shadowbehav)
|
|
local missMessage = xi.msg.basic.SKILL_MISS
|
|
if mob:getCurrentAction() == xi.action.category.PET_MOBABILITY_FINISH then
|
|
missMessage = xi.msg.basic.JA_MISS_2
|
|
end
|
|
|
|
local dmg = info.damage
|
|
|
|
-- Physical Attack Missed
|
|
if
|
|
skilltype == xi.attackType.PHYSICAL and
|
|
dmg == 0
|
|
then
|
|
skill:setMsg(missMessage)
|
|
|
|
return 0
|
|
end
|
|
|
|
-- set message to damage
|
|
-- this is for AoE because its only set once
|
|
if mob:getCurrentAction() == xi.action.category.PET_MOBABILITY_FINISH then
|
|
if skill:getMsg() ~= xi.msg.basic.JA_MAGIC_BURST then
|
|
skill:setMsg(xi.msg.basic.USES_JA_TAKE_DAMAGE)
|
|
end
|
|
else
|
|
skill:setMsg(xi.msg.basic.DAMAGE)
|
|
end
|
|
|
|
-- Handle shadows depending on shadow behavior / skilltype
|
|
local preShadowDmg = dmg
|
|
local shadowsUsed = 0
|
|
dmg, shadowsUsed = utils.takeShadows(target, dmg, shadowbehav)
|
|
|
|
if preShadowDmg > 0 and dmg == 0 then
|
|
skill:setMsg(xi.msg.basic.SHADOW_ABSORB)
|
|
return shadowsUsed
|
|
end
|
|
|
|
-- handle Third Eye using shadowbehav as a guide
|
|
local teye = target:getStatusEffect(xi.effect.THIRD_EYE)
|
|
|
|
-- T.Eye only procs when active with PHYSICAL stuff
|
|
if
|
|
teye ~= nil and
|
|
skilltype == xi.attackType.PHYSICAL
|
|
then
|
|
if shadowbehav == xi.mobskills.shadowBehavior.WIPE_SHADOWS then -- e.g. aoe moves
|
|
target:delStatusEffect(xi.effect.THIRD_EYE)
|
|
elseif shadowbehav ~= xi.mobskills.shadowBehavior.IGNORE_SHADOWS then -- it can be absorbed by shadows
|
|
-- third eye doesnt care how many shadows, so attempt to anticipate, but reduce
|
|
-- chance of anticipate based on previous successful anticipates.
|
|
local prevAnt = teye:getPower()
|
|
if prevAnt == 0 then
|
|
-- 100% proc
|
|
teye:setPower(1)
|
|
skill:setMsg(xi.msg.basic.ANTICIPATE)
|
|
return 0
|
|
end
|
|
|
|
if math.random() * 10 < 8 - prevAnt then
|
|
-- anticipated!
|
|
teye:setPower(prevAnt + 1)
|
|
skill:setMsg(xi.msg.basic.ANTICIPATE)
|
|
return 0
|
|
end
|
|
|
|
target:delStatusEffect(xi.effect.THIRD_EYE)
|
|
end
|
|
end
|
|
|
|
-- TODO: Handle anything else (e.g. if you have Magic Shield and its a Magic skill, then do 0 damage.
|
|
for attackType, shieldEffect in pairs(attackTypeShields) do
|
|
if skilltype == attackType and target:hasStatusEffect(shieldEffect) then
|
|
return 0
|
|
end
|
|
end
|
|
|
|
-- handle invincible
|
|
if
|
|
target:hasStatusEffect(xi.effect.INVINCIBLE) and
|
|
skilltype == xi.attackType.PHYSICAL
|
|
then
|
|
return 0
|
|
end
|
|
|
|
-- handle pd
|
|
if
|
|
(target:hasStatusEffect(xi.effect.PERFECT_DODGE) or
|
|
target:hasStatusEffect(xi.effect.ALL_MISS)) and
|
|
skilltype == xi.attackType.PHYSICAL
|
|
then
|
|
skill:setMsg(missMessage)
|
|
|
|
return 0
|
|
end
|
|
|
|
-- Calculate Blood Pact Damage before stoneskin
|
|
dmg = math.floor(dmg + dmg * mob:getMod(xi.mod.BP_DAMAGE) / 100)
|
|
|
|
if dmg < 0 then
|
|
return dmg
|
|
end
|
|
|
|
-- handle One For All, Liement
|
|
if skilltype == xi.attackType.MAGICAL then
|
|
dmg = utils.handleOneForAll(target, dmg)
|
|
end
|
|
|
|
dmg = utils.handlePhalanx(target, dmg)
|
|
dmg = utils.handleStoneskin(target, dmg)
|
|
|
|
-- Check if the mob has a damage cap
|
|
dmg = target:checkDamageCap(dmg)
|
|
|
|
return dmg
|
|
end
|
|
|
|
-- returns true if mob attack hit
|
|
-- used to stop tp move status effects
|
|
xi.summon.avatarPhysicalHit = function(skill, dmg)
|
|
-- if message is not the default. Then there was a miss, shadow taken etc
|
|
return skill:getMsg() == xi.msg.basic.DAMAGE
|
|
end
|
|
|
|
local trialSizeBattles = set{
|
|
xi.battlefield.id.TRIAL_SIZE_TRIAL_BY_WIND,
|
|
xi.battlefield.id.TRIAL_SIZE_TRIAL_BY_LIGHTNING,
|
|
xi.battlefield.id.TRIAL_SIZE_TRIAL_BY_ICE,
|
|
xi.battlefield.id.TRIAL_SIZE_TRIAL_BY_FIRE,
|
|
xi.battlefield.id.TRIAL_SIZE_TRIAL_BY_EARTH,
|
|
xi.battlefield.id.TRIAL_SIZE_TRIAL_BY_WATER,
|
|
}
|
|
|
|
-- Checks if the summoner is in a Trial Size Avatar Mini Fight (only carbuncle is allowed)
|
|
xi.summon.avatarMiniFightCheck = function(caster, spellID)
|
|
local result = 0
|
|
local effect = caster:getStatusEffect(xi.effect.BATTLEFIELD)
|
|
if spellID ~= xi.magic.spell.CARBUNCLE and effect then
|
|
local bcnmid = effect:getPower()
|
|
if trialSizeBattles[bcnmid] then -- Mini Avatar Fights
|
|
result = 40 -- Cannot use <spell> in this area.
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|