840 lines
32 KiB
Lua
840 lines
32 KiB
Lua
-----------------------------------
|
|
-- Blue Magic utilities
|
|
-- Used for Blue Magic spells.
|
|
-----------------------------------
|
|
require('scripts/globals/combat/physical_utilities')
|
|
require('scripts/globals/combat/magic_hit_rate')
|
|
require('scripts/globals/magic')
|
|
require('scripts/globals/mobskills')
|
|
require('scripts/globals/spells/damage_spell')
|
|
-----------------------------------
|
|
xi = xi or {}
|
|
xi.spells = xi.spells or {}
|
|
xi.spells.blue = xi.spells.blue or {}
|
|
-----------------------------------
|
|
|
|
-- The TP modifier (currently unused)
|
|
xi.spells.blue.tpMod =
|
|
{
|
|
NONE = 0,
|
|
CRITICAL = 1,
|
|
DAMAGE = 2,
|
|
ACC = 3,
|
|
ATTACK = 4,
|
|
DURATION = 5,
|
|
EFFECT_CHANCE = 6,
|
|
}
|
|
|
|
-----------------------------------
|
|
-- Local functions
|
|
-----------------------------------
|
|
|
|
-- Get alpha (level-dependent multiplier on WSC)
|
|
local function calculateAlpha(level)
|
|
if level <= 60 then
|
|
return math.ceil(100 - level / 6) / 100
|
|
elseif level <= 75 then
|
|
return math.ceil(100 - (level - 40) / 2) / 100
|
|
else
|
|
return 0.83
|
|
end
|
|
end
|
|
|
|
-- Get WSC
|
|
local function calculateWSC(attacker, params)
|
|
local alpha = calculateAlpha(attacker:getMainLvl())
|
|
local wscSTR = attacker:getStat(xi.mod.STR) * params.str_wsc
|
|
local wscDEX = attacker:getStat(xi.mod.DEX) * params.dex_wsc
|
|
local wscVIT = attacker:getStat(xi.mod.VIT) * params.vit_wsc
|
|
local wscAGI = attacker:getStat(xi.mod.AGI) * params.agi_wsc
|
|
local wscINT = attacker:getStat(xi.mod.INT) * params.int_wsc
|
|
local wscMND = attacker:getStat(xi.mod.MND) * params.mnd_wsc
|
|
local wscCHR = attacker:getStat(xi.mod.CHR) * params.chr_wsc
|
|
|
|
local wsc = (wscSTR + wscDEX + wscVIT + wscAGI + wscINT + wscMND + wscCHR) * alpha
|
|
|
|
return wsc
|
|
end
|
|
|
|
-- Get cRatio
|
|
local function calculatecRatio(ratio, atk_lvl, def_lvl)
|
|
-- Get ratio with level penalty
|
|
local levelcor = 0
|
|
if atk_lvl < def_lvl then
|
|
levelcor = 0.05 * (def_lvl - atk_lvl)
|
|
end
|
|
|
|
ratio = ratio - levelcor
|
|
ratio = utils.clamp(ratio, 0, 2)
|
|
|
|
-- Get cRatiomin
|
|
local cratiomin = 0
|
|
if ratio < 1.25 then
|
|
cratiomin = 1.2 * ratio - 0.5
|
|
elseif ratio >= 1.25 and ratio <= 1.5 then
|
|
cratiomin = 1
|
|
elseif ratio > 1.5 and ratio <= 2 then
|
|
cratiomin = 1.2 * ratio - 0.8
|
|
end
|
|
|
|
-- Get cRatiomax
|
|
local cratiomax = 0
|
|
if ratio < 0.5 then
|
|
cratiomax = 0.4 + 1.2 * ratio
|
|
elseif ratio <= 0.833 and ratio >= 0.5 then
|
|
cratiomax = 1
|
|
elseif ratio <= 2 and ratio > 0.833 then
|
|
cratiomax = 1.2 * ratio
|
|
end
|
|
|
|
-- Return data
|
|
local cratio = {}
|
|
if cratiomin < 0 then
|
|
cratiomin = 0
|
|
end
|
|
|
|
cratio[1] = cratiomin
|
|
cratio[2] = cratiomax
|
|
|
|
return cratio
|
|
end
|
|
|
|
-- Get the fTP multiplier (by applying 2 straight lines between ftp0-ftp1500 and ftp1500-ftp3000)
|
|
local function calculatefTP(tp, ftp0, ftp1500, ftp3000)
|
|
tp = utils.clamp(tp, 0, 3000)
|
|
|
|
if tp >= 1500 then
|
|
return ftp1500 + (ftp3000 - ftp1500) * (tp - 1500) / 1500
|
|
else
|
|
return ftp0 + (ftp1500 - ftp0) * tp / 1500
|
|
end
|
|
end
|
|
|
|
-- Get fSTR
|
|
local function calculatefSTR(dSTR)
|
|
local fSTR2 = 0
|
|
|
|
if dSTR >= 12 then
|
|
fSTR2 = dSTR + 4
|
|
elseif dSTR >= 6 then
|
|
fSTR2 = dSTR + 6
|
|
elseif dSTR >= 1 then
|
|
fSTR2 = dSTR + 7
|
|
elseif dSTR >= -2 then
|
|
fSTR2 = dSTR + 8
|
|
elseif dSTR >= -7 then
|
|
fSTR2 = dSTR + 9
|
|
elseif dSTR >= -15 then
|
|
fSTR2 = dSTR + 10
|
|
elseif dSTR >= -21 then
|
|
fSTR2 = dSTR + 12
|
|
else
|
|
fSTR2 = dSTR + 13
|
|
end
|
|
|
|
return fSTR2 / 2
|
|
end
|
|
|
|
-- Get hitrate
|
|
local function calculateHitrate(attacker, target, bonusacc)
|
|
-- your mainhand may not be a sword, so hit rate would vary here
|
|
-- TODO: verify hit rate of physical blue magic with different weapons
|
|
return xi.combat.physicalHitRate.getPhysicalHitRate(attacker, target, bonusacc + attacker:getMerit(xi.merit.PHYSICAL_POTENCY) * 2, xi.attackAnimation.RIGHT_ATTACK, false)
|
|
end
|
|
|
|
-- Get the effect of ecosystem correlation
|
|
local function calculateCorrelation(spellEcosystem, monsterEcosystem, merits)
|
|
local effect = utils.getEcosystemStrengthBonus(spellEcosystem, monsterEcosystem) * 0.25
|
|
|
|
if effect > 0 then -- merits don't impose a penalty, only a benefit in case of strength
|
|
effect = effect + 0.001 * merits
|
|
end
|
|
|
|
return effect
|
|
end
|
|
|
|
-- Consecutive Elemental Damage Penalty. Most commonly known as "Nuke Wall".
|
|
-- NOTE: Duplicate of the same function in damage_spell.lua, until Blue magic gets rewritten.
|
|
local function calculateNukeWallFactor(target, spellElement, finalDamage)
|
|
-- Initial check.
|
|
if
|
|
not target:isNM() or -- Target is not an NM.
|
|
spellElement <= xi.element.NONE or -- Action isn't elemental.
|
|
finalDamage < 0 -- Action heals target.
|
|
then
|
|
return 1
|
|
end
|
|
|
|
-----------------------------------
|
|
-- Fetch current wall potency and math based on time and Ruake
|
|
-----------------------------------
|
|
local potency = 0
|
|
local effect = target:getStatusEffect(xi.effect.NUKE_WALL)
|
|
|
|
if effect then
|
|
-- Current nuke wall effect.
|
|
potency = effect:getPower()
|
|
|
|
-- Effect potency is reduced by 20% after 1 second and remains stable for the remaining time, unless refreshed.
|
|
if effect:getTimeRemaining() <= 4000 then
|
|
potency = utils.clamp(potency - 2000, 0, 4000) -- Potency is reduced by 2000 (20%) after first second has happened. Can't go below 0.
|
|
end
|
|
|
|
-- Rayke effect.
|
|
if target:hasStatusEffect(xi.effect.RAYKE) then
|
|
local raykeSubpower = target:getStatusEffect(xi.effect.RAYKE):getSubPower()
|
|
|
|
-- current bit size of subPower is 16 bits, 4*4 = 16
|
|
-- Step from 0 to 16 in increments of 4...
|
|
for i = 0, 16, 4 do
|
|
-- If element is bitpacked into rayke subeffect...
|
|
if bit.band(bit.rshift(raykeSubpower, i), 0xF) == spellElement then
|
|
potency = math.floor(potency / 2)
|
|
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
target:delStatusEffectSilent(xi.effect.NUKE_WALL)
|
|
end
|
|
|
|
-----------------------------------
|
|
-- Calculate new potency after this nuke and renew effect.
|
|
-----------------------------------
|
|
-- Calculate damage needed to reach the potency cap (4000). The lower the level, the easier to hit potency cap.
|
|
local damageCap = target:getMainLvl() * 21 + 500
|
|
|
|
-- Calculate new potency, based on existing potency and damage dealt (compared to mob level).
|
|
local finalPotency = utils.clamp(math.floor(4000 * finalDamage / damageCap) + potency, 0, 4000)
|
|
|
|
-- Renew status effect without messages.
|
|
target:addStatusEffect(xi.effect.NUKE_WALL, { power = finalPotency, duration = 5, origin = target, icon = 0, subPower = spellElement })
|
|
|
|
-----------------------------------
|
|
-- We return JUST the factor based on previous nuke. This nuke only affects the next one.
|
|
-----------------------------------
|
|
return 1 - potency / 10000
|
|
end
|
|
|
|
-----------------------------------
|
|
-- Global functions
|
|
-----------------------------------
|
|
|
|
-- Get the damage for a physical Blue Magic spell
|
|
xi.spells.blue.usePhysicalSpell = function(caster, target, spell, params)
|
|
spell:setCritical(false)
|
|
|
|
-----------------------
|
|
-- Get final D value --
|
|
-----------------------
|
|
|
|
-- Initial D value
|
|
local initialD = math.floor(caster:getSkillLevel(xi.skill.BLUE_MAGIC) * 0.11) * 2 + 3
|
|
initialD = utils.clamp(initialD, 0, params.duppercap)
|
|
|
|
-- fSTR
|
|
local fStr = calculatefSTR(caster:getStat(xi.mod.STR) - target:getStat(xi.mod.VIT))
|
|
if fStr > 22 then
|
|
if params.ignorefstrcap == nil then -- Smite of Rage / Grand Slam don't have this cap applied
|
|
fStr = 22
|
|
end
|
|
end
|
|
|
|
-- Multiplier, bonus WSC
|
|
local multiplier = params.multiplier or 1
|
|
local bonusWSC = 0
|
|
|
|
-- BLU AF3 bonus (triples the base WSC when it procs)
|
|
if math.random(1, 100) <= caster:getMod(xi.mod.AUGMENT_BLU_MAGIC) then
|
|
bonusWSC = 2
|
|
end
|
|
|
|
-- Chain Affinity -- TODO: add 'Damage/Accuracy/Critical Hit Chance varies with TP'
|
|
if caster:getStatusEffect(xi.effect.CHAIN_AFFINITY) then
|
|
local tp = caster:getTP() + caster:getMerit(xi.merit.ENCHAINMENT) -- Total TP available
|
|
tp = utils.clamp(tp, 0, 3000)
|
|
multiplier = calculatefTP(tp, params.multiplier, params.tp150, params.tp300)
|
|
bonusWSC = bonusWSC + 1 -- Chain Affinity doubles base WSC
|
|
end
|
|
|
|
-- WSC
|
|
local wsc = calculateWSC(caster, params)
|
|
wsc = wsc + wsc * bonusWSC -- Bonus WSC from AF3/CA
|
|
|
|
-- Monster correlation
|
|
local correlationMultiplier = calculateCorrelation(params.ecosystem, target:getEcosystem(), caster:getMerit(xi.merit.MONSTER_CORRELATION))
|
|
|
|
-- Azure Lore
|
|
if caster:getStatusEffect(xi.effect.AZURE_LORE) then
|
|
multiplier = params.azuretp
|
|
end
|
|
|
|
-- Final D
|
|
local finalD = math.floor(initialD + fStr + wsc)
|
|
-- TODO: Implement ENHANCES_CHAIN_AFFINITY. Increase base damage of spell, but not limited to spell's damage cap
|
|
-- ENHANCES_CHAIN_AFFINITY should also not modify skillchain damage
|
|
|
|
----------------------------------------------
|
|
-- Get the possible pDIF range and hit rate --
|
|
----------------------------------------------
|
|
|
|
if params.offcratiomod == nil then -- For all spells except Cannonball, which uses a DEF mod
|
|
params.offcratiomod = caster:getStat(xi.mod.ATT)
|
|
end
|
|
|
|
params.offcratiomod = params.offcratiomod * (caster:getMerit(xi.merit.PHYSICAL_POTENCY) + 100) / 100
|
|
params.bonusacc = params.bonusacc == nil and 0 or params.bonusacc
|
|
params.tphitslanded = 0
|
|
|
|
-- params.critchance will only be non-nil if base critchance is passed from spell lua
|
|
local nativecrit = xi.combat.physical.calculateSwingCriticalRate(caster, target, 0, xi.slot.MAIN)
|
|
params.critchance = params.critchance == nil and 0 or utils.clamp(params.critchance / 100 + nativecrit, 0.05, 0.95)
|
|
|
|
local cratio = calculatecRatio(params.offcratiomod / target:getStat(xi.mod.DEF), caster:getMainLvl(), target:getMainLvl())
|
|
local hitrate = calculateHitrate(caster, target, params.bonusacc)
|
|
|
|
-------------------------
|
|
-- Perform the attacks --
|
|
-------------------------
|
|
|
|
local hitsdone = 0
|
|
local hitslanded = 0
|
|
local finaldmg = 0
|
|
local anyCrit = false
|
|
local sneakIsApplicable = false
|
|
local trickAttackTarget = nil
|
|
|
|
if spell:isAoE() == 0 and params.attackType ~= xi.attackType.RANGED then
|
|
if
|
|
caster:hasStatusEffect(xi.effect.SNEAK_ATTACK) and
|
|
(caster:isBehind(target) or caster:hasStatusEffect(xi.effect.HIDE))
|
|
then
|
|
sneakIsApplicable = true
|
|
end
|
|
|
|
if caster:hasStatusEffect(xi.effect.TRICK_ATTACK) then
|
|
trickAttackTarget = caster:getTrickAttackChar(target)
|
|
end
|
|
end
|
|
|
|
while hitsdone < params.numhits do
|
|
local chance = math.random()
|
|
|
|
if
|
|
sneakIsApplicable or
|
|
chance <= hitrate
|
|
then
|
|
-- TODO: Check for shadow absorbs. Right now the whole spell will be absorbed by one shadow before it even gets here.
|
|
|
|
-- Generate a random pDIF between min and max
|
|
local pdif = math.random(cratio[1] * 1000, cratio[2] * 1000)
|
|
pdif = pdif / 1000
|
|
|
|
local isCritical = sneakIsApplicable or math.random() < params.critchance
|
|
if isCritical then
|
|
pdif = pdif + 1
|
|
end
|
|
|
|
anyCrit = anyCrit or isCritical
|
|
|
|
-- Add it to our final damage
|
|
if hitsdone == 0 then
|
|
finaldmg = finaldmg + finalD * (multiplier + correlationMultiplier) * pdif -- first hit gets full multiplier
|
|
else
|
|
finaldmg = finaldmg + finalD * (1 + correlationMultiplier) * pdif
|
|
end
|
|
|
|
hitslanded = hitslanded + 1
|
|
sneakIsApplicable = false
|
|
|
|
-- Store number of hits that did > 0 damage
|
|
if finaldmg > 0 then
|
|
params.tphitslanded = params.tphitslanded + 1
|
|
end
|
|
end
|
|
|
|
if params.attackType ~= xi.attackType.RANGED then
|
|
caster:delStatusEffect(xi.effect.SNEAK_ATTACK)
|
|
caster:delStatusEffect(xi.effect.TRICK_ATTACK)
|
|
end
|
|
|
|
hitsdone = hitsdone + 1
|
|
end
|
|
|
|
finaldmg = math.floor(finaldmg * xi.combat.damage.calculateDamageAdjustment(target, true, false, false, false))
|
|
|
|
if hitslanded == 0 then
|
|
spell:setMsg(xi.msg.basic.MAGIC_FAIL)
|
|
end
|
|
|
|
spell:setCritical(anyCrit)
|
|
return xi.spells.blue.applySpellDamage(caster, target, spell, finaldmg, params, trickAttackTarget)
|
|
end
|
|
|
|
-- Get the damage for a magical Blue Magic spell. Called from spell scripts.
|
|
xi.spells.blue.useMagicalSpell = function(caster, target, spell, params)
|
|
-- In individual magical spells, don't use params.effect for the added effect
|
|
-- This would affect the resistance check for damage here
|
|
-- We just want that to affect the resistance check for the added effect
|
|
-- Use params.addedEffect instead
|
|
|
|
-- Initial values
|
|
local initialD = utils.clamp(caster:getMainLvl() + 2, 0, params.duppercap)
|
|
params.skillType = xi.skill.BLUE_MAGIC
|
|
|
|
-- WSC
|
|
local wsc = calculateWSC(caster, params)
|
|
local wscMultiplier = 1
|
|
|
|
-- BLU AF3 bonus (triples the base WSC when it procs)
|
|
if math.random(1, 100) <= caster:getMod(xi.mod.AUGMENT_BLU_MAGIC) then
|
|
wscMultiplier = wscMultiplier + 1
|
|
end
|
|
|
|
if caster:hasStatusEffect(xi.effect.BURST_AFFINITY) then
|
|
wscMultiplier = wscMultiplier + 1 + caster:getMod(xi.mod.ENHANCES_BURST_AFFINITY) / 100
|
|
end
|
|
|
|
wsc = wsc * wscMultiplier -- Bonus WSC from AF3/BA
|
|
|
|
-- INT/MND/CHR dmg bonuses
|
|
params.diff = caster:getStat(params.attribute) - target:getStat(params.attribute)
|
|
local statBonus = params.diff * params.tMultiplier
|
|
|
|
-- Azure Lore
|
|
local azureBonus = 0
|
|
if caster:getStatusEffect(xi.effect.AZURE_LORE) then
|
|
azureBonus = params.azureBonus or 0
|
|
end
|
|
|
|
-- Monster correlation
|
|
local correlationMultiplier = calculateCorrelation(params.ecosystem, target:getEcosystem(), caster:getMerit(xi.merit.MONSTER_CORRELATION))
|
|
|
|
-- Data
|
|
local spellId = spell:getID()
|
|
local spellElement = spell:getElement()
|
|
local spellGroup = spell:getSpellGroup()
|
|
local skillType = xi.skill.BLUE_MAGIC
|
|
local _, skillchainCount = xi.magicburst.formMagicBurst(target, spellElement) -- External function. Not present in magic.lua.
|
|
|
|
-- Final D value
|
|
local finalDamage = (initialD + wsc) * (params.multiplier + azureBonus + correlationMultiplier) + statBonus
|
|
|
|
finalDamage = math.floor(finalDamage * xi.combat.magicHitRate.calculateResistRate(caster, target, spellGroup, skillType, 0, spellElement, params.attribute, 0, 0))
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateElementalStaffBonus(caster, spellElement))
|
|
finalDamage = math.floor(finalDamage * xi.combat.damage.magicalElementSDT(target, spellElement))
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateDayAndWeather(caster, spellElement, false))
|
|
finalDamage = math.floor(finalDamage * xi.combat.damage.steamJacketMultiplier(target, spellElement))
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateMagicBonusDiff(caster, target, spellId, skillType, spellElement, 0))
|
|
|
|
if
|
|
caster:hasStatusEffect(xi.effect.BURST_AFFINITY) or
|
|
caster:hasStatusEffect(xi.effect.AZURE_LORE)
|
|
then
|
|
if skillchainCount > 0 then
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateIfMagicBurst(target, spellElement, skillchainCount))
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateIfMagicBurstBonus(caster, target, spellId, skillType, spellElement))
|
|
|
|
spell:setMsg(spell:getMagicBurstMessage()) -- "Magic Burst!"
|
|
|
|
caster:triggerRoeEvent(xi.roeTrigger.MAGIC_BURST)
|
|
end
|
|
|
|
caster:delStatusEffectSilent(xi.effect.BURST_AFFINITY)
|
|
end
|
|
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateEbullienceMultiplier(caster, spellGroup))
|
|
finalDamage = math.floor(finalDamage * xi.settings.main.BLUE_POWER)
|
|
|
|
return xi.spells.blue.applySpellDamage(caster, target, spell, finalDamage, params, nil)
|
|
end
|
|
|
|
-- Spell script Helper function.
|
|
xi.spells.blue.useDrainSpell = function(caster, target, spell, params, damageCap, mpDrain)
|
|
local finalDamage = 0
|
|
|
|
-- Early returns
|
|
if
|
|
xi.spells.damage.calculateAbsorption(target, spell:getElement(), true) ~= 1 or
|
|
xi.spells.damage.calculateNullification(target, spell:getElement(), true, false) ~= 1 or
|
|
target:isUndead()
|
|
then
|
|
spell:setMsg(xi.msg.basic.MAGIC_NO_EFFECT)
|
|
|
|
return 0
|
|
end
|
|
|
|
-- Base damage
|
|
finalDamage = math.floor(caster:getSkillLevel(xi.skill.BLUE_MAGIC) * 0.11)
|
|
finalDamage = math.floor(finalDamage * params.dmgMultiplier)
|
|
if damageCap > 0 then
|
|
finalDamage = utils.clamp(finalDamage, 0, damageCap)
|
|
end
|
|
|
|
-- Data
|
|
local spellId = spell:getID()
|
|
local spellElement = spell:getElement()
|
|
local spellGroup = spell:getSpellGroup()
|
|
local skillType = xi.skill.BLUE_MAGIC
|
|
local _, skillchainCount = xi.magicburst.formMagicBurst(target, spellElement) -- External function. Not present in magic.lua.
|
|
|
|
finalDamage = math.floor(finalDamage * xi.combat.magicHitRate.calculateResistRate(caster, target, spellGroup, skillType, 0, spellElement, params.attribute, 0, 0))
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateElementalStaffBonus(caster, spellElement))
|
|
finalDamage = math.floor(finalDamage * xi.combat.damage.magicalElementSDT(target, spellElement))
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateDayAndWeather(caster, spellElement, false))
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateMagicBonusDiff(caster, target, spellId, skillType, spellElement, 0))
|
|
|
|
if
|
|
caster:hasStatusEffect(xi.effect.BURST_AFFINITY) or
|
|
caster:hasStatusEffect(xi.effect.AZURE_LORE)
|
|
then
|
|
if skillchainCount > 0 then
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateIfMagicBurst(target, spellElement, skillchainCount))
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateIfMagicBurstBonus(caster, target, spellId, skillType, spellElement))
|
|
|
|
spell:setMsg(spell:getMagicBurstMessage()) -- "Magic Burst!"
|
|
|
|
caster:triggerRoeEvent(xi.roeTrigger.MAGIC_BURST)
|
|
end
|
|
|
|
caster:delStatusEffectSilent(xi.effect.BURST_AFFINITY)
|
|
end
|
|
|
|
finalDamage = math.floor(finalDamage * xi.spells.damage.calculateEbullienceMultiplier(caster, spellGroup))
|
|
finalDamage = math.floor(finalDamage * xi.combat.damage.calculateDamageAdjustment(target, false, true, false, false))
|
|
finalDamage = math.floor(finalDamage * xi.settings.main.BLUE_POWER)
|
|
|
|
-- MP drain
|
|
if mpDrain then
|
|
finalDamage = utils.clamp(finalDamage, 0, target:getMP())
|
|
|
|
target:delMP(finalDamage)
|
|
caster:addMP(finalDamage)
|
|
|
|
return finalDamage
|
|
end
|
|
|
|
-- Handle Phalanx, One for All, Stoneskin and target HP (Cant be higher than current HP)
|
|
finalDamage = utils.clamp(utils.handlePhalanx(target, finalDamage), 0, 99999)
|
|
finalDamage = utils.clamp(utils.handleOneForAll(target, finalDamage), 0, 99999)
|
|
finalDamage = utils.clamp(utils.handleStoneskin(target, finalDamage), -99999, 99999)
|
|
finalDamage = utils.clamp(finalDamage, 0, target:getHP())
|
|
|
|
-- Check if the mob has a damage cap
|
|
finalDamage = target:checkDamageCap(finalDamage)
|
|
|
|
target:takeSpellDamage(caster, spell, finalDamage, xi.attackType.MAGICAL, xi.damageType.ELEMENTAL + spell:getElement())
|
|
|
|
if not target:isPC() then
|
|
target:updateEnmityFromDamage(caster, finalDamage)
|
|
end
|
|
|
|
target:handleAfflatusMiseryDamage(finalDamage)
|
|
caster:addHP(finalDamage)
|
|
|
|
return finalDamage
|
|
end
|
|
|
|
-- Breath-type blue magic spells.
|
|
xi.spells.blue.useBreathSpell = function(caster, target, spell, params)
|
|
-- Early return.
|
|
if
|
|
params.isConal and -- Conal breath spells
|
|
not target:isInfront(caster, 32) -- Conal check (45° cone)
|
|
then
|
|
return 0
|
|
end
|
|
|
|
-- Initial damage
|
|
local dmg = caster:getHP() / params.hpMod
|
|
if params.lvlMod > 0 then
|
|
dmg = dmg + caster:getMainLvl() / params.lvlMod
|
|
end
|
|
|
|
-- Parameters
|
|
local spellId = spell:getID() or 0
|
|
local spellFamily = spell:getSpellFamily() or 0
|
|
local spellElement = spell:getElement() or 0
|
|
local attackType = params.attackType or xi.attackType.NONE
|
|
local damageType = params.damageType or xi.damageType.NONE
|
|
|
|
-- Multipliers
|
|
local correlationMultiplier = 1 + calculateCorrelation(params.ecosystem, target:getEcosystem(), caster:getMerit(xi.merit.MONSTER_CORRELATION))
|
|
local breathSDT = 1 + caster:getMod(xi.mod.BREATH_DMG_DEALT) / 100
|
|
local absorb = xi.spells.damage.calculateAbsorption(target, spellElement, false)
|
|
local nullify = xi.spells.damage.calculateNullification(target, spellElement, false, true)
|
|
local targetMagicDamageAdjustment = xi.combat.damage.calculateDamageAdjustment(target, false, false, false, true)
|
|
local elementalStaffBonus = xi.spells.damage.calculateElementalStaffBonus(caster, spellElement)
|
|
local elementalAffinityBonus = xi.spells.damage.calculateElementalAffinityBonus(caster, spellElement)
|
|
local resistTier = xi.combat.magicHitRate.calculateResistRate(caster, target, spellFamily, xi.skill.BLUE_MAGIC, 0, spellElement, 0, 0, 0)
|
|
local additionalResistTier = xi.spells.damage.calculateAdditionalResistTier(caster, target, spellElement)
|
|
local elementalSDT = xi.combat.damage.magicalElementSDT(target, spellElement)
|
|
local dayAndWeather = xi.spells.damage.calculateDayAndWeather(caster, spellElement, false)
|
|
local magicBonusDiff = xi.spells.damage.calculateMagicBonusDiff(caster, target, spellId, xi.skill.BLUE_MAGIC, spellElement, 0)
|
|
local skillTypeMultiplier = xi.spells.damage.calculateSkillTypeMultiplier(xi.skill.BLUE_MAGIC)
|
|
local ninFutaeBonus = xi.spells.damage.calculateNinFutaeBonus(caster, xi.skill.BLUE_MAGIC)
|
|
local ninjutsuMultiplier = xi.spells.damage.calculateNinjutsuMultiplier(caster, target, xi.skill.BLUE_MAGIC)
|
|
local scarletDeliriumMultiplier = xi.combat.damage.scarletDeliriumMultiplier(caster)
|
|
local areaOfEffectResistance = xi.spells.damage.calculateAreaOfEffectResistance(target, spell)
|
|
|
|
dmg = math.floor(dmg * correlationMultiplier)
|
|
dmg = math.floor(dmg * breathSDT)
|
|
dmg = math.floor(dmg * absorb)
|
|
dmg = math.floor(dmg * nullify)
|
|
dmg = math.floor(dmg * targetMagicDamageAdjustment)
|
|
dmg = math.floor(dmg * elementalStaffBonus)
|
|
dmg = math.floor(dmg * elementalAffinityBonus)
|
|
dmg = math.floor(dmg * resistTier)
|
|
dmg = math.floor(dmg * additionalResistTier)
|
|
dmg = math.floor(dmg * elementalSDT)
|
|
dmg = math.floor(dmg * dayAndWeather)
|
|
dmg = math.floor(dmg * magicBonusDiff)
|
|
dmg = math.floor(dmg * skillTypeMultiplier)
|
|
dmg = math.floor(dmg * ninFutaeBonus)
|
|
dmg = math.floor(dmg * ninjutsuMultiplier)
|
|
dmg = math.floor(dmg * scarletDeliriumMultiplier)
|
|
dmg = math.floor(dmg * areaOfEffectResistance)
|
|
dmg = math.floor(dmg * calculateNukeWallFactor(target, spellElement, dmg))
|
|
|
|
-- Handle Magic Absorb message and HP recovery.
|
|
if dmg < 0 then
|
|
dmg = target:addHP(-dmg)
|
|
spell:setMsg(xi.msg.basic.MAGIC_RECOVERS_HP)
|
|
|
|
return dmg
|
|
end
|
|
|
|
dmg = math.floor(target:handleSevereDamage(dmg, false))
|
|
|
|
-- Final adjustments.
|
|
if dmg > 0 then
|
|
dmg = utils.clamp(utils.handlePhalanx(target, dmg), 0, 99999)
|
|
dmg = utils.clamp(utils.handleOneForAll(target, dmg), 0, 99999)
|
|
dmg = utils.clamp(utils.handleStoneskin(target, dmg), -99999, 99999)
|
|
dmg = utils.clamp(dmg, 0, target:getHP())
|
|
dmg = target:checkDamageCap(dmg)
|
|
end
|
|
|
|
target:takeSpellDamage(caster, spell, dmg, attackType, damageType)
|
|
|
|
-- Handle TP
|
|
local tpHits = params.tphitslanded or 0
|
|
local extraTPGained = xi.combat.tp.calculateTPGainOnMagicalDamage(caster, target, dmg) * math.max(tpHits - 1, 0) -- Calculate extra TP gained from multihits. takeSpellDamage accounts for one already.
|
|
target:addTP(extraTPGained)
|
|
|
|
-- Handle Afflatus Misery.
|
|
target:handleAfflatusMiseryDamage(dmg)
|
|
|
|
-- Handle Enmity.
|
|
target:updateEnmityFromDamage(caster, dmg)
|
|
|
|
return dmg
|
|
end
|
|
|
|
-- Apply spell damage
|
|
xi.spells.blue.applySpellDamage = function(caster, target, spell, dmg, params, trickAttackTarget)
|
|
dmg = math.floor(dmg * xi.settings.main.BLUE_POWER)
|
|
local attackType = params.attackType or xi.attackType.NONE
|
|
local damageType = params.damageType or xi.damageType.NONE
|
|
local tpHits = params.tphitslanded or 0
|
|
local extraTPGained = xi.combat.tp.calculateTPGainOnMagicalDamage(caster, target, dmg) * math.max(tpHits - 1, 0) -- Calculate extra TP gained from multihits. takeSpellDamage accounts for one already.
|
|
|
|
-- handle MDT, One For All, Liement
|
|
if attackType == xi.attackType.MAGICAL then
|
|
local absorb = xi.spells.damage.calculateAbsorption(target, spell:getElement(), true)
|
|
local nullify = xi.spells.damage.calculateNullification(target, spell:getElement(), true, false)
|
|
dmg = math.floor(dmg * absorb * nullify)
|
|
|
|
if dmg < 0 then
|
|
target:takeSpellDamage(caster, spell, dmg, attackType, damageType)
|
|
target:addTP(extraTPGained)
|
|
-- TODO: verify Afflatus/enmity from absorb?
|
|
return dmg
|
|
end
|
|
|
|
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)
|
|
|
|
target:takeSpellDamage(caster, spell, dmg, attackType, damageType)
|
|
target:addTP(extraTPGained)
|
|
|
|
if not target:isPC() then
|
|
if trickAttackTarget then
|
|
target:updateEnmityFromDamage(trickAttackTarget, dmg)
|
|
else
|
|
target:updateEnmityFromDamage(caster, dmg)
|
|
end
|
|
end
|
|
|
|
target:handleAfflatusMiseryDamage(dmg)
|
|
|
|
return dmg
|
|
end
|
|
|
|
-- Get the duration of an enhancing Blue Magic spell
|
|
xi.spells.blue.calculateDurationWithDiffusion = function(caster, duration)
|
|
if caster:hasStatusEffect(xi.effect.DIFFUSION) then
|
|
local merits = caster:getMerit(xi.merit.DIFFUSION)
|
|
|
|
if merits > 0 then -- each merit after the first increases duration by 5%
|
|
duration = duration + (merits - 5) * duration / 100
|
|
end
|
|
|
|
caster:delStatusEffect(xi.effect.DIFFUSION)
|
|
end
|
|
|
|
return duration
|
|
end
|
|
|
|
-- Perform an enfeebling Blue Magic spell
|
|
xi.spells.blue.useEnfeeblingSpell = function(caster, target, spell, params)
|
|
local spellElement = spell:getElement()
|
|
local effect = params.effect
|
|
local tier = params.tier or 0
|
|
|
|
-- Early return: Out of cone.
|
|
if
|
|
params.isConal and
|
|
not target:isInfront(caster, 32)
|
|
then
|
|
spell:setMsg(xi.msg.basic.MAGIC_NO_EFFECT)
|
|
return effect
|
|
end
|
|
|
|
-- Early return: Out of gaze.
|
|
if
|
|
params.isGaze and
|
|
(not target:isFacing(caster) or not caster:isFacing(target))
|
|
then
|
|
spell:setMsg(xi.msg.basic.MAGIC_NO_EFFECT)
|
|
return effect
|
|
end
|
|
|
|
-- Early return: Target is immune.
|
|
if xi.data.statusEffect.isTargetImmune(target, effect, spellElement) then
|
|
spell:setMsg(xi.msg.basic.MAGIC_COMPLETE_RESIST)
|
|
return effect
|
|
end
|
|
|
|
-- Early return: Trait nullification trigger.
|
|
if xi.data.statusEffect.isTargetResistant(caster, target, effect) then
|
|
spell:setModifier(xi.msg.actionModifier.RESIST)
|
|
spell:setMsg(xi.msg.basic.MAGIC_RESIST)
|
|
return effect
|
|
end
|
|
|
|
-- Early return: Target already has an status effect that nullifies current.
|
|
if xi.data.statusEffect.isEffectNullified(target, effect, tier) then
|
|
spell:setMsg(xi.msg.basic.MAGIC_NO_EFFECT)
|
|
return effect
|
|
end
|
|
|
|
-- Early return: Regular resist.
|
|
local resist = xi.combat.magicHitRate.calculateResistRate(caster, target, 0, xi.skill.BLUE_MAGIC, 0, spellElement, xi.mod.INT, 0, 0)
|
|
if resist < params.resistThreshold then
|
|
spell:setMsg(xi.msg.basic.MAGIC_RESIST)
|
|
return effect
|
|
end
|
|
|
|
if target:addStatusEffect(effect, { power = params.power, duration = math.floor(params.duration * resist), origin = caster, tick = params.tick }) then
|
|
-- Add "Magic Burst!" message
|
|
local _, skillchainCount = xi.magicburst.formMagicBurst(target, spellElement) -- External function. Not present in magic.lua.
|
|
|
|
if skillchainCount > 0 then
|
|
spell:setMsg(xi.msg.basic.MAGIC_BURST_ENFEEB_IS)
|
|
caster:triggerRoeEvent(xi.roeTrigger.MAGIC_BURST)
|
|
else
|
|
spell:setMsg(xi.msg.basic.MAGIC_ENFEEB_IS)
|
|
end
|
|
else
|
|
spell:setMsg(xi.msg.basic.MAGIC_NO_EFFECT)
|
|
end
|
|
|
|
return effect
|
|
end
|
|
|
|
-- Perform a curative Blue Magic spell
|
|
xi.spells.blue.useCuringSpell = function(caster, target, spell, params)
|
|
local power = getCurePowerOld(caster)
|
|
local divisor = params.divisor0
|
|
local constant = params.constant0
|
|
|
|
if power > params.powerThreshold2 then
|
|
divisor = params.divisor2
|
|
constant = params.constant2
|
|
elseif power > params.powerThreshold1 then
|
|
divisor = params.divisor1
|
|
constant = params.constant1
|
|
end
|
|
|
|
local final = getCureFinal(caster, spell, getBaseCureOld(power, divisor, constant), params.minCure, true)
|
|
final = final + final * target:getMod(xi.mod.CURE_POTENCY_RCVD) / 100
|
|
final = final * xi.settings.main.CURE_POWER
|
|
final = utils.clamp(final, 0, target:getMaxHP() - target:getHP())
|
|
|
|
target:addHP(final)
|
|
target:wakeUp()
|
|
caster:updateEnmityFromCure(target, final)
|
|
|
|
if target:getID() == spell:getPrimaryTargetID() then
|
|
spell:setMsg(xi.msg.basic.MAGIC_RECOVERS_HP)
|
|
else
|
|
spell:setMsg(xi.msg.basic.SELF_HEAL_SECONDARY)
|
|
end
|
|
|
|
return final
|
|
end
|
|
|
|
xi.spells.blue.applyBlueAdditionalEffect = function(caster, target, params, effectTable)
|
|
-- Sanitize parameters.
|
|
local element = params.damageType and params.damageType - 5 or 0
|
|
local stat = params.attribute and params.attribute or xi.mod.INT
|
|
if params.attackType == xi.attackType.BREATH then
|
|
stat = 0
|
|
end
|
|
|
|
-- Calculate resist and early return.
|
|
local resist = xi.combat.magicHitRate.calculateResistRate(caster, target, 0, xi.skill.BLUE_MAGIC, 0, element, stat, 0, 0)
|
|
|
|
if resist <= 0.25 then
|
|
return
|
|
end
|
|
|
|
for entry = 1, #effectTable do
|
|
local effect = effectTable[entry][1]
|
|
local power = effectTable[entry][2]
|
|
local tick = effectTable[entry][3]
|
|
local duration = effectTable[entry][4]
|
|
|
|
if
|
|
not xi.data.statusEffect.isTargetImmune(target, effect, element) and -- Target isn't immune.
|
|
not xi.data.statusEffect.isTargetResistant(caster, target, effect) and -- Target didn't trigger a job trait resistance.
|
|
not xi.data.statusEffect.isEffectNullified(target, effect, 0) -- Target doesn't have an status effect that nullifies current. TODO: Tier.
|
|
then
|
|
target:addStatusEffect(effect, { power = power, duration = math.floor(duration * resist), origin = caster, tick = tick })
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[
|
|
+-------+
|
|
| NOTES |
|
|
+-------+
|
|
- Spell values (multiplier, TP, D, WSC, TP etc) from:
|
|
- https://www.bg-wiki.com/ffxi/Calculating_Blue_Magic_Damage
|
|
- https://ffxiclopedia.fandom.com/wiki/Calculating_Blue_Magic_Damage
|
|
- BG-wiki spell pages
|
|
- Blue Gartr threads with data, such as
|
|
https://www.bluegartr.com/threads/37619-Blue-Mage-Best-thread-ever?p=5832112&viewfull=1#post5832112
|
|
https://www.bluegartr.com/threads/37619-Blue-Mage-Best-thread-ever?p=5437135&viewfull=1#post5437135
|
|
https://www.bluegartr.com/threads/107650-Random-Question-Thread-XXIV-Occupy-the-RQT?p=4906565&viewfull=1#post4906565
|
|
- When values were absent, spell values were decided based on Blue Gartr threads and Wiki page discussions.
|
|
- Assumed INT as the main magic accuracy modifier for physical spells' additional effects (when no data was found).
|
|
]]--
|