landsandboat/scripts/combat/basic/magic_hit_rate.lua

575 lines
19 KiB
Lua

-----------------------------------
-- Global file for magic based skills magic hit rate.
-----------------------------------
xi = xi or {}
xi.combat = xi.combat or {}
xi.combat.magicHitRate = xi.combat.magicHitRate or {}
-----------------------------------
-----------------------------------
-- Calculate Target Resistance Rank
-----------------------------------
local function calculateTargetResistanceRank(actor, target, params)
-- Early return: Players don't use resistance ranks.
if target:isPC() then
return 0
end
-- Calculate what modifier to use.
local resistanceRankMod = 0 -- Modifier ID of the resistance rank to use.
if params.effectId > 0 then -- Check if it's an effect.
resistanceRankMod = xi.data.statusEffect.getAssociatedResistanceRankModifier(params.effectId, params.magicalElement)
end
if resistanceRankMod == 0 then -- If it's an effect and this is 0, try with element.
resistanceRankMod = xi.data.element.getElementalResistanceRankModifier(params.magicalElement)
end
-- Fetch resistance rank.
local resistanceRank = target:getMod(resistanceRankMod)
-- Apply possible resistance rank modifications.
if params.effectId > 0 then
resistanceRank = resistanceRank - target:getMod(xi.data.statusEffect.getAssociatedImmunobreakModifier(params.effectId)) -- Apply immunobreak modification.
end
return utils.clamp(resistanceRank, -3, 11)
end
-----------------------------------
-- Calculate Actor Magic Accuracy
-----------------------------------
-- Magic Accuracy from spell's skill.
local function magicAccuracyFromSkill(actor, params)
local magicAcc = 0
-- For known skills.
if params.skillType > 0 then
magicAcc = actor:getSkillLevel(params.skillType)
-- If a mob is casting something its main/sub jobs cannot (i.e. JoL casting black magic) this is _exceptional_
-- So give them A+ rank base magic accuracy
if magicAcc == 0 and actor:isMob() then
magicAcc = xi.data.skillLevel.getSkillCap(actor:getMainLvl(), xi.skillRank.A_PLUS)
end
if params.skillType == xi.skill.SINGING then
if actor:isPC() then
-- String instruments sacrifice accuracy and amplify the AoE in exchange.
local rangedSkill = actor:getWeaponSkillType(xi.slot.RANGED)
if rangedSkill == xi.skill.WIND_INSTRUMENT then
magicAcc = magicAcc + math.floor(actor:getSkillLevel(rangedSkill) / 2)
end
else
magicAcc = magicAcc + math.floor(magicAcc / 2)
end
end
-- Made for bolts. Will probably see other uses.
elseif params.skillRank > 0 then
magicAcc = xi.data.skillLevel.getSkillCap(actor:getMainLvl(), params.skillRank)
-- For mob skills / additional effects which don't have a skill.
else
magicAcc = xi.data.skillLevel.getSkillCap(actor:getMainLvl(), xi.skillRank.A_PLUS)
end
return magicAcc
end
-- Magic Accuracy from spell's element.
local function magicAccuracyFromElement(actor, params)
-- Early return: No element.
if params.magicalElement <= xi.element.NONE then
return 0
end
return actor:getMod(xi.data.element.getElementalMACCModifier(params.magicalElement)) + actor:getMod(xi.data.element.getElementalStaffModifier(params.magicalElement)) * 10
end
-- Magic Accuracy from Stat Difference between caster and target.
local function magicAccuracyFromStatDifference(actor, target, params)
if params.actorStat == 0 then
return 0
end
local magicAcc = 0
local statDiff = actor:getStat(params.actorStat) - target:getStat(params.targetStat)
if statDiff <= -31 then
magicAcc = -20 + (statDiff + 30) / 4
elseif statDiff <= -11 then
magicAcc = -10 + (statDiff + 10) / 2
elseif statDiff < 11 then -- Between -11 and 11
magicAcc = statDiff
elseif statDiff >= 31 then
magicAcc = 20 + (statDiff - 30) / 4
elseif statDiff >= 11 then
magicAcc = 10 + (statDiff - 10) / 2
end
return utils.clamp(magicAcc, -30, 30)
end
-- Magic Accuracy from Status Effects.
local function magicAccuracyFromStatusEffects(actor, params)
local magicAcc = 0
local actorJob = actor:getMainJob()
local actorWeather = actor:getWeather()
-- Altruism
if
actor:hasStatusEffect(xi.effect.ALTRUISM) and
params.spellGroup == xi.magic.spellGroup.WHITE
then
magicAcc = actor:getStatusEffect(xi.effect.ALTRUISM):getPower()
end
-- Focalization
if
actor:hasStatusEffect(xi.effect.FOCALIZATION) and
params.spellGroup == xi.magic.spellGroup.BLACK
then
magicAcc = magicAcc + actor:getStatusEffect(xi.effect.FOCALIZATION):getPower()
end
--Klimaform
if
actor:hasStatusEffect(xi.effect.KLIMAFORM) and
params.magicalElement > 0 and
(actorWeather == xi.data.element.getAssociatedSingleWeather(params.magicalElement) or actorWeather == xi.data.element.getAssociatedDoubleWeather(params.magicalElement))
then
magicAcc = magicAcc + 15
end
-- Apply Divine Emblem to Banish and Holy families
if
actor:hasStatusEffect(xi.effect.DIVINE_EMBLEM) and
actorJob == xi.job.PLD and
params.skillType == xi.skill.DIVINE_MAGIC
then
magicAcc = magicAcc + 256
end
-- Elemental seal
if
actor:hasStatusEffect(xi.effect.ELEMENTAL_SEAL) and
params.skillType ~= xi.skill.DARK_MAGIC and
params.skillType ~= xi.skill.DIVINE_MAGIC and
params.magicalElement > 0
then
magicAcc = magicAcc + 256
end
-- Dark Seal
if
actor:hasStatusEffect(xi.effect.DARK_SEAL) and
params.skillType == xi.skill.DARK_MAGIC
then
magicAcc = magicAcc + 256
end
return magicAcc
end
-- Magic Accuracy from Merits.
local function magicAccuracyFromMerits(actor, params)
local magicAcc = 0
local actorJob = actor:getMainJob()
switch (actorJob) : caseof
{
[xi.job.BLM] = function()
if params.skillType == xi.skill.ELEMENTAL_MAGIC then
magicAcc = actor:getMerit(xi.merit.ELEMENTAL_MAGIC_ACCURACY)
end
end,
[xi.job.RDM] = function()
-- Category 1
if
params.magicalElement >= xi.element.FIRE and
params.magicalElement <= xi.element.WATER
then
magicAcc = actor:getMerit(xi.data.element.getElementalAccuracyMerit(params.magicalElement))
end
-- Category 2
magicAcc = magicAcc + actor:getMerit(xi.merit.MAGIC_ACCURACY)
end,
[xi.job.BRD] = function()
if
params.skillType == xi.skill.SINGING and
actor:hasStatusEffect(xi.effect.TROUBADOUR)
then
magicAcc = 64 * (actor:getMerit(xi.merit.TROUBADOUR) / 25 - 1)
end
end,
[xi.job.NIN] = function()
if params.skillType == xi.skill.NINJUTSU then
magicAcc = actor:getMerit(xi.merit.NIN_MAGIC_ACCURACY)
end
end,
[xi.job.BLU] = function()
if params.skillType == xi.skill.BLUE_MAGIC then
magicAcc = actor:getMerit(xi.merit.MAGICAL_ACCURACY)
end
end,
}
return magicAcc
end
-- Magic Accuracy from Job Points.
local function magicAccuracyFromJobPoints(actor, params)
local magicAcc = 0
local actorJob = actor:getMainJob()
switch (actorJob) : caseof
{
[xi.job.WHM] = function()
magicAcc = actor:getJobPointLevel(xi.jp.WHM_MAGIC_ACC_BONUS)
end,
[xi.job.BLM] = function()
magicAcc = actor:getJobPointLevel(xi.jp.BLM_MAGIC_ACC_BONUS)
end,
[xi.job.RDM] = function()
-- RDM Job Point: During saboteur, Enfeebling MACC +2
if
params.skillType == xi.skill.ENFEEBLING_MAGIC and
actor:hasStatusEffect(xi.effect.SABOTEUR)
then
magicAcc = actor:getJobPointLevel(xi.jp.SABOTEUR_EFFECT) * 2
end
-- RDM Job Point: Magic Accuracy Bonus, All MACC + 1
magicAcc = magicAcc + actor:getJobPointLevel(xi.jp.RDM_MAGIC_ACC_BONUS)
end,
[xi.job.BRD] = function()
if params.skillType == xi.skill.SINGING then
magicAcc = actor:getJobPointLevel(xi.jp.SONG_ACC_BONUS)
end
end,
[xi.job.NIN] = function()
if params.skillType == xi.skill.NINJUTSU then
magicAcc = actor:getJobPointLevel(xi.jp.NINJITSU_ACC_BONUS)
end
end,
[xi.job.SCH] = function()
if
(params.spellGroup == xi.magic.spellGroup.WHITE and actor:hasStatusEffect(xi.effect.PARSIMONY)) or
(params.spellGroup == xi.magic.spellGroup.BLACK and actor:hasStatusEffect(xi.effect.PENURY))
then
magicAcc = actor:getJobPointLevel(xi.jp.STRATEGEM_EFFECT_I)
end
end,
}
return magicAcc
end
-- Magic Accuracy from Magic Burst.
local function magicAccuracyFromMagicBurst(target, params)
if params.magicalElement == xi.element.NONE then
return 0
end
if params.magicBurstTier <= 0 then
return 0
end
return 100
end
-- Magic Accuracy from Weather Element. Note: https://wiki.ffo.jp/html/1796.html claims day does not affect macc.
local function magicAccuracyFromWeatherElement(actor, params)
local magicAcc = 0
-- Early return: Invalid element.
if params.magicalElement <= xi.element.NONE then
return magicAcc
end
-- Define what to apply.
local applyBonuses = false
local applyPenalties = false
if
math.randomInt(1, 100) <= 33 or -- Random. Applies to both bonuses and penalties.
actor:getMod(xi.mod.FORCE_DW_BONUS_PENALTY) >= 1 -- Hachirin-no-Obi forces both bonuses and penalties.
then
applyBonuses = true
applyPenalties = true
elseif actor:getMod(xi.data.element.getForcedDayOrWeatherBonusModifier(params.magicalElement)) >= 1 then -- Elemental Obis only force bonuses, not penalties.
applyBonuses = true
end
-- Calculate bonuses/penalties.
local actorWeather = actor:getWeather()
if applyBonuses then
if actorWeather == xi.data.element.getAssociatedSingleWeather(params.magicalElement) then
magicAcc = magicAcc + 5 + actor:getMod(xi.mod.IRIDESCENCE) * 5
elseif actorWeather == xi.data.element.getAssociatedDoubleWeather(params.magicalElement) then
magicAcc = magicAcc + 10 + actor:getMod(xi.mod.IRIDESCENCE) * 5
end
end
if applyPenalties then
if actorWeather == xi.data.element.getOppositeSingleWeather(params.magicalElement) then
magicAcc = magicAcc - 5 - actor:getMod(xi.mod.IRIDESCENCE) * 5
elseif actorWeather == xi.data.element.getOppositeDoubleWeather(params.magicalElement) then
magicAcc = magicAcc - 10 - actor:getMod(xi.mod.IRIDESCENCE) * 5
end
end
return magicAcc
end
-- Magic Accuracy from Tandem Strike (BST trait).
local function magicAccuracyFromTandemStrike(actor)
-- Early return: Can't apply Tandem Strike.
if not actor:isTandemActive() then
return 0
end
-- Actor is a pet, with a master. Fetch master modifier.
local master = actor:getMaster()
if master and master:isPC() then
return master:getMod(xi.mod.TANDEM_STRIKE_POWER)
end
-- Actor is the master.
return actor:getMod(xi.mod.TANDEM_STRIKE_POWER)
end
-- Magic Accuracy from Food.
local function magicAccuracyFromFoodMultiplier(actor)
local foodMagicAccBonus = actor:getMod(xi.mod.FOOD_MACCP) / 100
local foodMagicAccCap = actor:getMod(xi.mod.FOOD_MACC_CAP) / 100
if foodMagicAccCap > 0 then
foodMagicAccBonus = utils.clamp(foodMagicAccBonus, 0, foodMagicAccCap)
end
return 1 + foodMagicAccBonus
end
local function magicAccuracyFromSoulVoiceMultiplier(actor, params)
local effectTable =
set{
xi.effect.SLEEP_I, -- Lullabies
xi.effect.NONE, -- Magic Finale
xi.effect.CHARM_I -- Maiden's Virellai
}
if
effectTable[params.effectId] and
params.skillType == xi.skill.SINGING
then
if actor:hasStatusEffect(xi.effect.SOUL_VOICE) then
return 2
elseif actor:hasStatusEffect(xi.effect.MARCATO) then
return 1.5
end
end
return 1
end
-- Global function to calculate total magicc accuracy.
local function calculateActorMagicAccuracy(actor, target, params)
local finalMagicAcc = 0
local magicAccBase = actor:getMod(xi.mod.MACC) + actor:getILvlMacc(xi.slot.MAIN) + params.bonusMacc
local magicAccSkill = magicAccuracyFromSkill(actor, params)
local magicAccElement = magicAccuracyFromElement(actor, params)
local magicAccStatDiff = magicAccuracyFromStatDifference(actor, target, params)
local magicAccEffects = magicAccuracyFromStatusEffects(actor, params)
local magicAccMerits = magicAccuracyFromMerits(actor, params)
local magicAccJobPoints = magicAccuracyFromJobPoints(actor, params)
local magicAccBurst = magicAccuracyFromMagicBurst(target, params)
local magicAccWeather = magicAccuracyFromWeatherElement(actor, params)
local magicAccTandem = magicAccuracyFromTandemStrike(actor)
-- Multipliers
local magicAccFoodFactor = magicAccuracyFromFoodMultiplier(actor)
local magicAccSoulVoiceFactor = magicAccuracyFromSoulVoiceMultiplier(actor, params)
-- Add up food magic accuracy.
finalMagicAcc = magicAccBase + magicAccSkill + magicAccElement + magicAccStatDiff + magicAccEffects + magicAccMerits + magicAccJobPoints + magicAccBurst + magicAccWeather + magicAccTandem
finalMagicAcc = math.floor(finalMagicAcc * magicAccFoodFactor * magicAccSoulVoiceFactor)
return finalMagicAcc
end
-----------------------------------
-- Calculate Target Magic Evasion
-----------------------------------
local resistRankMultiplier =
{
-- [Rank] = Magic Evasion multiplier.
[-3] = 0.95,
[-2] = 0.96019,
[-1] = 0.98,
[ 0] = 1,
[ 1] = 1.023,
[ 2] = 1.049,
[ 3] = 1.0905,
[ 4] = 1.126,
[ 5] = 1.2075,
[ 6] = 1.3475,
[ 7] = 1.70065,
[ 8] = 2.141,
[ 9] = 2.2,
[10] = 2.275, -- Impossible to test since 'Magic Hit Rate' is floored to 5% at this point.
[11] = 2.35, -- Impossible to test since 'Magic Hit Rate' is floored to 5% at this point.
}
local function calculateTargetMagicEvasion(actor, target, params)
local magicEva = target:getMod(xi.mod.MEVA) -- Base MACC.
-- Elemental magic evasion. All actions and effects have an associated element.
if params.magicalElement ~= xi.element.NONE then
magicEva = magicEva + target:getMod(xi.data.element.getElementalMEVAModifier(params.magicalElement))
end
-- Magic evasion against specific status effects.
if params.effectId > 0 then
magicEva = magicEva + target:getMod(xi.data.statusEffect.getAssociatedMagicEvasionModifier(params.effectId)) + target:getMod(xi.mod.STATUS_MEVA)
end
-- Level correction. Target gets a bonus the higher the level if it's a mob. Never a penalty.
if
not target:isPC() and
xi.data.levelCorrection.isLevelCorrectedZone(actor)
then
magicEva = magicEva + utils.clamp(target:getMainLvl() - actor:getMainLvl(), 0, 100) * 4
end
-- Apply resistance rank multiplier.
magicEva = math.floor(magicEva * resistRankMultiplier[params.resistanceRank])
return magicEva
end
-----------------------------------
-- Magic Hit Rate. The function gets fed the result of both functions above.
-----------------------------------
local function calculateMagicHitRate(params)
local magicHitRate = params.actorMagicAccuracy - params.targetMagicEvasion
if magicHitRate < 0 then
magicHitRate = math.floor(magicHitRate / 2)
end
magicHitRate = utils.clamp((50 + magicHitRate) / 100, 0.05, 0.95)
return magicHitRate
end
-----------------------------------
-- Calculate resist rate.
-----------------------------------
local function calculateResistanceFactor(actor, target, params)
-- Calculate max allowed resist tier.
local maxResistTier = 3
-- Players: Affected by element shown in equipment screen.
if target:isPC() then
local playerElementalEvasion = target:getMod(xi.data.element.getElementalMEVAModifier(params.magicalElement)) or 0
if playerElementalEvasion < 0 then
maxResistTier = 1
elseif playerElementalEvasion == 0 then
maxResistTier = 2
end
end
-- Calculate first 3 resist tiers.
-- Notes: https://wiki-ffo-jp.translate.goog/html/795.html?_x_tr_sl=ja&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=sc
local resistTier = 0
for i = 1, maxResistTier do
if math.randomFloat(0, 1) > params.magicHitRate then
resistTier = resistTier + 1
else
break
end
end
return 1 / (2 ^ resistTier)
end
-----------------------------------
-- Resist rate helper function.
-----------------------------------
local function validateParameters(actor, target, fedData)
local params = {}
-- Action information.
params.effectId = fedData.effectId or 0
params.magicalElement = fedData.magicalElement or xi.element.NONE
params.bonusMacc = fedData.bonusMacc or 0
params.magicBurstTier = fedData.magicBurstTier or 0
params.actorStat = fedData.actorStat or 0
params.targetStat = fedData.targetStat or params.actorStat
params.skillType = fedData.skillType or 0
params.skillRank = fedData.skillRank or 0
params.spellGroup = fedData.spellGroup or 0
-- Initialize future parameters.
params.resistanceRank = 0
params.actorMagicAccuracy = 0
params.targetMagicEvasion = 0
params.magicHitRate = 0.05
return params
end
xi.combat.magicHitRate.calculateResistRate = function(actor, target, fedData)
-- Validate fed parameters.
local params = validateParameters(actor, target, fedData)
-- Early return: Magic shield.
if target:hasStatusEffect(xi.effect.MAGIC_SHIELD, 0) then
return 0
end
-- Early return: Cannot resist non-elemental magic actions.
if params.magicalElement == xi.element.NONE then
return 1
end
-- Calculate and table resistance rank. TODO: Feed it to the function directly.
params.resistanceRank = calculateTargetResistanceRank(actor, target, params)
-- Early return: Auto-resist.
if params.resistanceRank >= 11 then
if params.effectId > 0 then
return 0 -- Status Effects
else
return 0.25 -- Nukes
end
end
-- Early return: MHR is floored to 0.05. Skip calculating it.
if params.resistanceRank >= 10 then
return calculateResistanceFactor(actor, target, params)
end
-- Calculate and table MACC, MEVA and MHR.
params.actorMagicAccuracy = calculateActorMagicAccuracy(actor, target, params)
params.targetMagicEvasion = calculateTargetMagicEvasion(actor, target, params)
params.magicHitRate = calculateMagicHitRate(params)
return calculateResistanceFactor(actor, target, params)
end