landsandboat/scripts/combat/basic/tp.lua
2026-07-14 18:34:39 +02:00

281 lines
13 KiB
Lua

-----------------------------------
xi = xi or {}
xi.combat = xi.combat or {}
xi.combat.tp = xi.combat.tp or {}
-----------------------------------
-----------------------------------
-- "Local" functions only used here.
-----------------------------------
-- USED IN CORE (If you add/remove function params, they must be mirrored in core)
-- https://www.bg-wiki.com/ffxi/Tactical_Points
-- Gainee is the target who is going to gain the TP.
-- For instance, if a player attacks a mob, the mob uses the mob formula when gaining TP from the returned hit.
-- This appears to be a measure to not buff mobs when players were buffed with the new TP gain formula.
--- @params gainee CBaseEntity
--- @params delay integer
--- @return integer
xi.combat.tp.calculateTPReturn = function(gainee, delay)
local tpReturn = 0
local isCharmedPCPet = false
-- Charmed pets controlled by the player are not caught by isPet() and are considered mobs still.
-- However once charmed, they convert to use the PC delay formula.
if
gainee:getObjType() == xi.objType.MOB and
gainee:getMaster() ~= nil and
gainee:getMaster():isPC() and
gainee:isCharmed()
then
isCharmedPCPet = true
end
if
gainee and
gainee:getObjType() ~= xi.objType.MOB or
isCharmedPCPet
then -- Pets and PCs have been observed to use this formula
if delay > 900 then
tpReturn = 173 + (delay - 900) * 28 / 360
elseif delay > 720 then
tpReturn = 161 + (delay - 720) * 24 / 360
elseif delay > 630 then
tpReturn = 154 + (delay - 630) * 28 / 360
elseif delay > 540 then
tpReturn = 149 + (delay - 540) * 20 / 360
elseif delay > 180 then
tpReturn = 61 + (delay - 180) * 88 / 360
else
tpReturn = 61 + (delay - 180) * 63 / 360
end
else -- mobs have been observed to use this formula -- http://wiki.ffo.jp/html/308.html
if delay > 530 then
tpReturn = 145 + (delay - 530) * 35 / 470
elseif delay > 480 then
tpReturn = 130 + (delay - 480) * 15 / 30
elseif delay > 450 then
tpReturn = 115 + (delay - 450) * 15 / 30
elseif delay > 180 then
tpReturn = 50 + (delay - 180) * 65 / 270
else
tpReturn = 50 + (delay - 180) * 15 / 180
end
end
return math.floor(tpReturn)
end
xi.combat.tp.getModifiedDelayAndCanZanshin = function(actor, delay)
local modifiedDelay = delay
local canZanshin = false
-- DW/H2H delay is halved for the purposes of a single hit's TP return when applicable, see https://www.bg-wiki.com/ffxi/Tactical_Points
if actor:isDualWielding() then -- NOTE: this 'isDualWielding' may trip on non-PCs even if they are 'using h2h'. If this is rectified in core in the future this should fall through correctly.
modifiedDelay = (delay * (100 - actor:getMod(xi.mod.DUAL_WIELD)) / 100) / 2
elseif actor:isUsingH2H() then
if actor:getObjType() == xi.objType.PC then -- handle h2h with > 1 swing only on PC
if
actor:getEquippedItem(xi.slot.SUB) ~= nil or -- equipped shield = one swing
actor:getSkillRank(xi.skill.HAND_TO_HAND) == 0 -- zero h2h rank skill = one swing
then
modifiedDelay = math.max((delay - actor:getMod(xi.mod.MARTIAL_ARTS)), 96) -- min delay of 96 total, https://www.bg-wiki.com/ffxi/Attack_Speed
canZanshin = true -- Zanshin can proc on an 'unarmed' swing -- https://www.bg-wiki.com/ffxi/Zanshin
else
modifiedDelay = math.max((delay - actor:getMod(xi.mod.MARTIAL_ARTS)) / 2, 48) -- min delay of 96 total so 96/2 per fist, https://www.bg-wiki.com/ffxi/Attack_Speed
end
elseif actor:getObjType() == xi.objType.MOB then
modifiedDelay = math.max(delay / 2, 48) -- Mobs are not affected at all by Martial Arts.
else
-- TODO: handle the corner case where a PC-like entity is using h2h but is only hitting with one 'fist'. Perhaps they have a shield with no main weapon.
-- elseif actor:getAutoAttackHits() > 1
modifiedDelay = math.max((delay - actor:getMod(xi.mod.MARTIAL_ARTS)) / 2, 48)
end
else -- single melee swing, either 1H or 2H
canZanshin = true -- https://www.bg-wiki.com/ffxi/Zanshin
end
modifiedDelay = modifiedDelay * math.max((100 + actor:getMod(xi.mod.DELAYP)) / 100, 0.85) -- minimum cap of -15% https://www.bg-wiki.com/ffxi/Attack_Speed. Undocumented if 15% + Claymore Grip goes above 15%.
return ({ canZanshin = canZanshin , modifiedDelay = math.floor(modifiedDelay) })
end
-- Bonus subtle blow II from Tandem Blow (BST trait)
xi.combat.tp.getTandemBlowBonus = function(actor)
local tandemBlowBonus = 0
if actor:isTandemActive() then
if actor:getMaster() ~= nil and actor:getMaster():isPC() then
tandemBlowBonus = actor:getMaster():getMod(xi.mod.TANDEM_BLOW_POWER)
else
tandemBlowBonus = actor:getMod(xi.mod.TANDEM_BLOW_POWER)
end
end
return tandemBlowBonus
end
-----------------------------------
-- Global functions used elsewhere.
-----------------------------------
-- Returns attacker TP gain from a single melee hit from itself.
xi.combat.tp.getSingleMeleeHitTPReturn = function(actor, isZanshin)
if actor:hasStatusEffect(xi.effect.MEIKYO_SHISUI) then
return 0
end
isZanshin = isZanshin or false -- optional input, defaults to false.
local delay = actor:getBaseDelay()
local attackOutput = xi.combat.tp.getModifiedDelayAndCanZanshin(actor, delay)
local tpReturn = xi.combat.tp.calculateTPReturn(actor, attackOutput.modifiedDelay)
if isZanshin and attackOutput.canZanshin then
tpReturn = tpReturn + actor:getMerit(xi.merit.IKISHOTEN) -- https://www.bg-wiki.com/ffxi/Ikishoten
end
local storeTPModifier = 1 + (actor:getMod(xi.mod.STORETP) + actor:getMerit(xi.merit.STORE_TP_EFFECT)) / 100
return math.floor(tpReturn * storeTPModifier)
end
-- Returns a PC weapon slot's TP return for a single hit.
xi.combat.tp.getSingleWeaponTPReturn = function(actor, slot)
if not actor:isPC() then
return 0
end
if actor:hasStatusEffect(xi.effect.MEIKYO_SHISUI) then
return 0
end
-- TODO: implement Zanshin check optionally?
local delay = actor:getBaseWeaponDelay(slot)
local attackOutput = xi.combat.tp.getModifiedDelayAndCanZanshin(actor, delay)
local tpReturn = xi.combat.tp.calculateTPReturn(actor, attackOutput.modifiedDelay)
local storeTPModifier = 1 + actor:getMod(xi.mod.STORETP) / 100
return math.floor(tpReturn * storeTPModifier)
end
-- Returns a single ranged hit's TP return
xi.combat.tp.getSingleRangedHitTPReturn = function(actor)
if actor:hasStatusEffect(xi.effect.MEIKYO_SHISUI) then
return 0
end
local delay = actor:getBaseRangedDelay() -- there do not appear to be any delay modifiers for ranged attacks, snapshot does not seem to effect this
if delay <= 0 then
return 0
end
local storeTPModifier = 1 + actor:getMod(xi.mod.STORETP) / 100
return math.floor(xi.combat.tp.calculateTPReturn(actor, delay) * storeTPModifier)
end
-- This function calculates how much TP a target(The defender) will gain upon being hit by a physical attack.
-- TODO: does Ikishoten factor into this as a bonus to baseTPGain if it procs on the hit? Needs verification.
--- @params actor CBaseEntity
--- @params target CBaseEntity
--- @params totalDamage integer
--- @params delay integer
--- @return integer
xi.combat.tp.calculateTPGainOnPhysicalDamage = function(actor, target, totalDamage, delay)
if not actor or not target then
return 0
end
if totalDamage <= 0 then
return 0
end
if actor:hasStatusEffect(xi.effect.MEIKYO_SHISUI) then
return 0
end
-- TODO: does dAGI penalty work against/for Trusts/Pets? Nothing is documented for this. Currently assuming mob only.
local attackOutput = xi.combat.tp.getModifiedDelayAndCanZanshin(actor, delay)
local baseTPGain = xi.combat.tp.calculateTPReturn(actor, attackOutput.modifiedDelay)
local dAGI = actor:getStat(xi.mod.AGI) - target:getStat(xi.mod.AGI)
local inhibitTPModifier = (100 - target:getMod(xi.mod.INHIBIT_TP)) / 100 -- no known cap: https://www.bg-wiki.com/ffxi/Monster_TP_gain#Inhibit_TP
local dAGIModifier = utils.clamp(200 - (dAGI + 30) / 200, 0.5, 1) -- 50% reduction at +70 dAGI: https://www.bg-wiki.com/ffxi/Monster_TP_gain
local subtleBlowMerits = actor:getMerit(xi.merit.SUBTLE_BLOW_EFFECT)
local subtleBlowI = math.min(actor:getMod(xi.mod.SUBTLE_BLOW) + subtleBlowMerits, 50) -- cap of 50% https://www.bg-wiki.com/ffxi/Subtle_Blow
local tandemBlowBonus = xi.combat.tp.getTandemBlowBonus(actor)
local subtleBlowII = actor:getMod(xi.mod.SUBTLE_BLOW_II) + tandemBlowBonus -- no known cap
local subtleBlowModifier = math.max((100 - subtleBlowI + subtleBlowII) / 100, 0.25) -- combined cap of 75% reduction: https://www.bg-wiki.com/ffxi/Subtle_Blow
local storeTPModifier = 1 + target:getMod(xi.mod.STORETP) / 100
-- TODO: unknown where/how many floor steps there are. Napkin math seems to be a single floor step, but given x/256 it's hard to tell
-- TODO: unknown if player pets (automaton/wyvern/avatars) are affected by dAGI
-- mob vs mob (via charm) is observed to use the (base * 1/3) formula instead of (base + 30)
-- (base + 30) formula appears to be intentional by SE to make mobs 'more dangerous' when hit by players/pets
if
target:getObjType() == xi.objType.MOB and
actor:getObjType() ~= xi.objType.MOB
then
-- +30 sourced from http://wiki.ffo.jp/html/2621.html and tested in game
return math.floor((baseTPGain + 30) * inhibitTPModifier * dAGIModifier * subtleBlowModifier * storeTPModifier)
else
-- 1/3rd sourced from https://www.bg-wiki.com/ffxi/Tactical_Points and tested in game
return math.floor(baseTPGain * inhibitTPModifier * subtleBlowModifier * storeTPModifier * (1 / 3))
end
end
-- USED IN CORE
-- Used exclusively for blue magic.
xi.combat.tp.calculateTPGainOnMagicalDamage = function(actor, target, totalDamage)
if not actor or not target then
return 0
end
if totalDamage <= 0 then
return 0
end
if actor:hasStatusEffect(xi.effect.MEIKYO_SHISUI) then
return 0
end
-- TODO: does dAGI penalty work against/for Trusts/Pets? Nothing is documented for this. Currently assuming mob only.
local dAGI = actor:getStat(xi.mod.AGI) - target:getStat(xi.mod.AGI)
local inhibitTPModifier = (100 - target:getMod(xi.mod.INHIBIT_TP)) / 100 -- no known cap: https://www.bg-wiki.com/ffxi/Monster_TP_gain#Inhibit_TP
local dAGIModifier = utils.clamp(200 - (dAGI + 30) / 200, 0.5, 1) -- 50% reduction at +70 dAGI: https://www.bg-wiki.com/ffxi/Monster_TP_gain
local subtleBlowMerits = actor:getMerit(xi.merit.SUBTLE_BLOW_EFFECT)
local subtleBlowI = math.min(actor:getMod(xi.mod.SUBTLE_BLOW) + subtleBlowMerits, 50) -- cap of 50% https://www.bg-wiki.com/ffxi/Subtle_Blow
local tandemBlowBonus = xi.combat.tp.getTandemBlowBonus(actor)
local subtleBlowII = actor:getMod(xi.mod.SUBTLE_BLOW_II) + tandemBlowBonus -- no known cap
local subtleBlowModifier = math.max((100 - subtleBlowI + subtleBlowII) / 100, 0.25) -- combined cap of 75% reduction: https://www.bg-wiki.com/ffxi/Subtle_Blow
local storeTPModifier = 1 + target:getMod(xi.mod.STORETP) / 100
-- Similar caveats to above for physical damage, unknown where/how many floors but seems to be one.
if target:getObjType() == xi.objType.MOB then
return math.floor(100 * inhibitTPModifier * dAGIModifier * subtleBlowModifier * storeTPModifier) -- 100 sourced from testing & https://www.bg-wiki.com/ffxi/Monster_TP_gain#TP_gained_from_Magical_Damage
else
return math.floor(50 * inhibitTPModifier * subtleBlowModifier * storeTPModifier) -- 50 sourced from testing & https://www.bg-wiki.com/ffxi/Tactical_Points#Getting_hit_for_more_than_0_damage
end
end
-- USED IN CORE
-- Calculate TP generated by spell for Occult Acumen trait
xi.combat.tp.calculateSpellTP = function(actor, spell)
if not actor:isPC() then
return 0
end
if actor:hasStatusEffect(xi.effect.MEIKYO_SHISUI) then
return 0
end
if not utils.contains(spell:getSkillType(), { xi.skill.ELEMENTAL_MAGIC, xi.skill.DARK_MAGIC }) then
return 0
end
local occultAcumenModifier = actor:getMod(xi.mod.OCCULT_ACUMEN) / 100
local storeTPModifier = 1 + actor:getMod(xi.mod.STORETP) / 100
return math.floor(spell:getMPCost() * occultAcumenModifier * storeTPModifier)
end