2026-07-07 22:01:55 +02:00
-----------------------------------
2022-08-14 11:03:14 -07:00
xi = xi or { }
2023-02-28 00:30:41 +01:00
xi.combat = xi.combat or { }
xi.combat . tp = xi.combat . tp or { }
2022-11-28 12:59:40 +01:00
-----------------------------------
2022-08-14 11:03:14 -07:00
2026-01-09 20:08:50 +01:00
-----------------------------------
-- "Local" functions only used here.
-----------------------------------
2022-08-14 11:03:14 -07:00
2026-04-10 19:37:48 -07:00
-- USED IN CORE (If you add/remove function params, they must be mirrored in core)
2022-08-14 11:03:14 -07:00
-- 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.
2026-04-10 19:37:48 -07:00
--- @params gainee CBaseEntity
--- @params delay integer
--- @return integer
2023-02-28 00:30:41 +01:00
xi.combat . tp.calculateTPReturn = function ( gainee , delay )
2024-01-10 04:13:11 +01:00
local tpReturn = 0
2026-05-04 23:34:55 -07:00
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
2024-01-10 04:13:11 +01:00
2026-05-04 23:34:55 -07:00
then -- Pets and PCs have been observed to use this formula
2024-01-10 04:13:11 +01:00
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
2022-08-14 11:03:14 -07:00
else
2024-01-10 04:13:11 +01:00
tpReturn = 61 + ( delay - 180 ) * 63 / 360
2022-08-14 11:03:14 -07:00
end
else -- mobs have been observed to use this formula -- http://wiki.ffo.jp/html/308.html
2024-01-10 04:13:11 +01:00
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
2022-08-14 11:03:14 -07:00
end
end
2024-01-10 04:13:11 +01:00
return math.floor ( tpReturn )
2022-08-14 11:03:14 -07:00
end
2026-01-09 20:08:50 +01:00
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
2026-05-04 23:34:55 -07:00
if actor : getObjType ( ) == xi.objType . PC then -- handle h2h with > 1 swing only on PC
2026-01-09 20:08:50 +01:00
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
2026-05-04 23:34:55 -07:00
elseif actor : getObjType ( ) == xi.objType . MOB then
modifiedDelay = math.max ( delay / 2 , 48 ) -- Mobs are not affected at all by Martial Arts.
2026-01-09 20:08:50 +01:00
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
2025-04-28 23:08:40 -05:00
-- 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
2026-01-09 20:08:50 +01:00
-----------------------------------
-- 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
2026-07-14 09:34:47 -06:00
local storeTPModifier = 1 + ( actor : getMod ( xi.mod . STORETP ) + actor : getMerit ( xi.merit . STORE_TP_EFFECT ) ) / 100
2026-01-09 20:08:50 +01:00
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
2026-04-10 19:37:48 -07:00
-- This function calculates how much TP a target(The defender) will gain upon being hit by a physical attack.
2022-08-14 11:03:14 -07:00
-- TODO: does Ikishoten factor into this as a bonus to baseTPGain if it procs on the hit? Needs verification.
2026-04-10 19:37:48 -07:00
--- @params actor CBaseEntity
--- @params target CBaseEntity
--- @params totalDamage integer
--- @params delay integer
--- @return integer
2026-01-09 20:07:57 +01:00
xi.combat . tp.calculateTPGainOnPhysicalDamage = function ( actor , target , totalDamage , delay )
if not actor or not target then
return 0
2022-08-14 11:03:14 -07:00
end
2026-01-09 20:07:57 +01:00
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 )
2026-04-10 19:37:48 -07:00
local baseTPGain = xi.combat . tp.calculateTPReturn ( actor , attackOutput.modifiedDelay )
2026-01-09 20:07:57 +01:00
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
2022-08-14 11:03:14 -07:00
end
2026-01-09 20:02:20 +01:00
-- 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
2022-08-14 11:03:14 -07:00
2026-01-09 20:02:20 +01:00
if totalDamage <= 0 then
return 0
2022-08-14 11:03:14 -07:00
end
2026-01-09 20:02:20 +01:00
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
2022-08-14 11:03:14 -07:00
end
2025-12-30 19:20:35 -06:00
2026-01-09 19:56:01 +01:00
-- USED IN CORE
2025-12-30 19:20:35 -06:00
-- Calculate TP generated by spell for Occult Acumen trait
xi.combat . tp.calculateSpellTP = function ( actor , spell )
2026-01-09 19:56:01 +01:00
if not actor : isPC ( ) then
return 0
2025-12-30 19:20:35 -06:00
end
2026-01-09 19:56:01 +01:00
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 )
2025-12-30 19:20:35 -06:00
end