----------------------------------- 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