From 919f22b70875109bf148f72574a1684939df3588 Mon Sep 17 00:00:00 2001 From: Will Buck Date: Mon, 28 Apr 2025 23:08:40 -0500 Subject: [PATCH] Implement tandem strike + blow Style fixes --- scripts/globals/combat/magic_hit_rate.lua | 18 +++++++++++- scripts/globals/combat/tp.lua | 20 +++++++++++-- scripts/specs/core/CBaseEntity.lua | 5 ++++ src/map/entities/battleentity.cpp | 21 ++++++++++++++ src/map/lua/lua_baseentity.cpp | 19 +++++++++++++ src/map/lua/lua_baseentity.h | 1 + src/map/utils/battleutils.cpp | 32 +++++++++++++++++++-- src/map/utils/petutils.cpp | 34 +++++++++++++++++++++++ src/map/utils/petutils.h | 1 + 9 files changed, 146 insertions(+), 5 deletions(-) diff --git a/scripts/globals/combat/magic_hit_rate.lua b/scripts/globals/combat/magic_hit_rate.lua index 258f62261fa..d318e26ebe5 100644 --- a/scripts/globals/combat/magic_hit_rate.lua +++ b/scripts/globals/combat/magic_hit_rate.lua @@ -318,6 +318,21 @@ local function magicAccuracyFromWeatherElement(actor, actionElement) return magicAcc end +-- Magic Accuracy from Tandem Strike (BST trait). +local function magicAccuracyFromTandemStrike(actor) + local magicAcc = 0 + + if actor:isTandemActive() then + if actor:getMaster() ~= nil and actor:getMaster():isPC() then + magicAcc = actor:getMaster():getMod(xi.mod.TANDEM_STRIKE_POWER) + else + magicAcc = actor:getMod(xi.mod.TANDEM_STRIKE_POWER) + end + end + + return magicAcc +end + -- Magic Accuracy from Food. local function magicAccuracyFromFoodMultiplier(actor) local magicAcc = 1 @@ -369,13 +384,14 @@ xi.combat.magicHitRate.calculateActorMagicAccuracy = function(actor, target, spe local magicAccBurst = magicAccuracyFromMagicBurst(target, actionElement, statUsed) local magicAccDay = magicAccuracyFromDayElement(actor, actionElement) local magicAccWeather = magicAccuracyFromWeatherElement(actor, actionElement) + local magicAccTandem = magicAccuracyFromTandemStrike(actor) -- Multipliers local magicAccFoodFactor = magicAccuracyFromFoodMultiplier(actor) local magicAccSoulVoiceFactor = magicAccuracyFromSoulVoiceMultiplier(actor, skillType, effectId) -- Add up food magic accuracy. - finalMagicAcc = magicAccBase + magicAccSkill + magicAccElement + magicAccStatDiff + magicAccEffects + magicAccMerits + magicAccJobPoints + magicAccBurst + magicAccDay + magicAccWeather + bonusMacc + finalMagicAcc = magicAccBase + magicAccSkill + magicAccElement + magicAccStatDiff + magicAccEffects + magicAccMerits + magicAccJobPoints + magicAccBurst + magicAccDay + magicAccWeather + magicAccTandem + bonusMacc finalMagicAcc = math.floor(finalMagicAcc * magicAccFoodFactor * magicAccSoulVoiceFactor) return finalMagicAcc diff --git a/scripts/globals/combat/tp.lua b/scripts/globals/combat/tp.lua index 42e6902cd3f..a0b686a2646 100644 --- a/scripts/globals/combat/tp.lua +++ b/scripts/globals/combat/tp.lua @@ -121,6 +121,20 @@ xi.combat.tp.calculateTPReturn = function(gainee, delay) return math.floor(tpReturn) 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 + -- TODO: does Ikishoten factor into this as a bonus to baseTPGain if it procs on the hit? Needs verification. xi.combat.tp.calculateTPGainOnPhysicalDamage = function(totalDamage, delay, actor, target) -- TODO: does dAGI penalty work against/for Trusts/Pets? Nothing is documented for this. Currently assuming mob only. @@ -132,7 +146,8 @@ xi.combat.tp.calculateTPGainOnPhysicalDamage = function(totalDamage, delay, acto local dAGIModifier = utils.clamp(200 - (dAGI + 30) / 200, 1.0, 0.5) -- 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 subtleBlowII = actor:getMod(xi.mod.SUBTLE_BLOW_II) -- no known cap + 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 = (100 + target:getMod(xi.mod.STORETP)) / 100 @@ -164,7 +179,8 @@ xi.combat.tp.calculateTPGainOnMagicalDamage = function(totalDamage, actor, targe local dAGIModifier = utils.clamp(200 - (dAGI + 30) / 200, 1.0, 0.5) -- 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 subtleBlowII = actor:getMod(xi.mod.SUBTLE_BLOW_II) -- no known cap + 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 = (100 + target:getMod(xi.mod.STORETP)) / 100 diff --git a/scripts/specs/core/CBaseEntity.lua b/scripts/specs/core/CBaseEntity.lua index df1f733bdae..cade7fc7904 100644 --- a/scripts/specs/core/CBaseEntity.lua +++ b/scripts/specs/core/CBaseEntity.lua @@ -3089,6 +3089,11 @@ end function CBaseEntity:uncharm() end +---@nodiscard +---@return boolean +function CBaseEntity:isTandemActive() +end + ---@nodiscard ---@param element integer ---@param burden integer diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index 8989d494769..39b1750fbdb 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -1024,6 +1024,11 @@ uint16 CBattleEntity::ACC(uint8 attackNumber, uint16 offsetAccuracy) ACC += this->getMod(Mod::ENSPELL_DMG); } + if (petutils::IsTandemActive(this)) + { + ACC += this->getMod(Mod::TANDEM_STRIKE_POWER); + } + auto* PChar = dynamic_cast(this); if (PChar) { @@ -1045,6 +1050,14 @@ uint16 CBattleEntity::ACC(uint8 attackNumber, uint16 offsetAccuracy) ACC += this->getMod(Mod::ENSPELL_DMG); } + if (petutils::IsTandemActive(this)) + { + if (this->PMaster && this->PMaster->objtype == TYPE_PC) + { + ACC += this->PMaster->getMod(Mod::TANDEM_STRIKE_POWER); + } + } + ACC = ACC + std::min((ACC * m_modStat[Mod::FOOD_ACCP] / 100), m_modStat[Mod::FOOD_ACC_CAP]); return std::max(0, ACC); } @@ -1057,6 +1070,14 @@ uint16 CBattleEntity::ACC(uint8 attackNumber, uint16 offsetAccuracy) ACC += this->getMod(Mod::ENSPELL_DMG); } + if (petutils::IsTandemActive(this)) + { + if (this->PMaster && this->PMaster->objtype == TYPE_PC) + { + ACC += this->PMaster->getMod(Mod::TANDEM_STRIKE_POWER); + } + } + ACC = ACC + std::min((ACC * m_modStat[Mod::FOOD_ACCP] / 100), m_modStat[Mod::FOOD_ACC_CAP]) + DEX() / 2; // Account for food mods here for Snatch Morsel return std::max(0, ACC); } diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index fe0591aa175..f7351b6bb99 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -14285,6 +14285,24 @@ void CLuaBaseEntity::uncharm() } } +/************************************************************************ + * Function: isTandemActive() + * Purpose : If entity is pet or master, checks to see if both are fighting same target + * Example : player:isTandemActive() + * Notes : used for BST traits Tandem Strike / Tandem Blow + ************************************************************************/ + +bool CLuaBaseEntity::isTandemActive() +{ + auto* PBattle = dynamic_cast(m_PBaseEntity); + if (!PBattle) + { + ShowError("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); + return false; + } + return petutils::IsTandemActive(static_cast(m_PBaseEntity)); +} + /************************************************************************ * Function: addBurden() * Purpose : Adds a Burden to a Target @@ -19615,6 +19633,7 @@ void CLuaBaseEntity::Register() // BST SOL_REGISTER("charm", CLuaBaseEntity::charm); SOL_REGISTER("uncharm", CLuaBaseEntity::uncharm); + SOL_REGISTER("isTandemActive", CLuaBaseEntity::isTandemActive); // PUP SOL_REGISTER("addBurden", CLuaBaseEntity::addBurden); diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index fbbcb7949e2..8b0919df524 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -708,6 +708,7 @@ class CLuaBaseEntity void charm(CLuaBaseEntity const* target, sol::object const& p0); void uncharm(); + bool isTandemActive(); uint8 addBurden(uint8 element, uint8 burden); uint8 getOverloadChance(uint8 element); diff --git a/src/map/utils/battleutils.cpp b/src/map/utils/battleutils.cpp index c3a31dcdb9d..9b0c805267f 100644 --- a/src/map/utils/battleutils.cpp +++ b/src/map/utils/battleutils.cpp @@ -2400,9 +2400,23 @@ namespace battleutils sBlowMerit = PChar->PMeritPoints->GetMeritValue(MERIT_TYPE::MERIT_SUBTLE_BLOW_EFFECT, PChar); } + // Check for Tandem Blow bonus while pet+master are fighting same target + int32 tandemBlowBonus = 0; + if (petutils::IsTandemActive(PAttacker)) + { + if (PAttacker->PMaster && PAttacker->PMaster->objtype == TYPE_PC) + { + tandemBlowBonus = PAttacker->PMaster->getMod(Mod::TANDEM_BLOW_POWER); + } + else + { + tandemBlowBonus = PAttacker->getMod(Mod::TANDEM_BLOW_POWER); + } + } + // account for attacker's subtle blow which reduces the baseTP gain for the defender float sBlow1 = std::clamp((float)(PAttacker->getMod(Mod::SUBTLE_BLOW) + sBlowMerit), -50.0f, 50.0f); - float sBlow2 = std::clamp((float)PAttacker->getMod(Mod::SUBTLE_BLOW_II), -50.0f, 50.0f); + float sBlow2 = std::clamp((float)(PAttacker->getMod(Mod::SUBTLE_BLOW_II) + tandemBlowBonus), -50.0f, 50.0f); float sBlowMult = ((100.0f - std::clamp(sBlow1 + sBlow2, -75.0f, 75.0f)) / 100.0f); // mobs hit get basetp+30 whereas pcs hit get basetp/3 @@ -2571,9 +2585,23 @@ namespace battleutils sBlowMerit = PChar->PMeritPoints->GetMeritValue(MERIT_TYPE::MERIT_SUBTLE_BLOW_EFFECT, PChar); } + // Check for Tandem Blow bonus while pet+master are fighting same target + int32 tandemBlowBonus = 0; + if (petutils::IsTandemActive(PAttacker)) + { + if (PAttacker->PMaster && PAttacker->PMaster->objtype == TYPE_PC) + { + tandemBlowBonus = PAttacker->PMaster->getMod(Mod::TANDEM_BLOW_POWER); + } + else + { + tandemBlowBonus = PAttacker->getMod(Mod::TANDEM_BLOW_POWER); + } + } + // account for attacker's subtle blow which reduces the baseTP gain for the defender float sBlow1 = std::clamp((float)(PAttacker->getMod(Mod::SUBTLE_BLOW) + sBlowMerit), -50.0f, 50.0f); - float sBlow2 = std::clamp((float)PAttacker->getMod(Mod::SUBTLE_BLOW_II), -50.0f, 50.0f); + float sBlow2 = std::clamp((float)(PAttacker->getMod(Mod::SUBTLE_BLOW_II) + tandemBlowBonus), -50.0f, 50.0f); float sBlowMult = (100.0f - std::clamp(sBlow1 + sBlow2, -75.0f, 75.0f)) / 100.0f; // mobs hit get basetp+30 whereas pcs hit get basetp/3 diff --git a/src/map/utils/petutils.cpp b/src/map/utils/petutils.cpp index a2d36c45006..0ac78bd0b72 100644 --- a/src/map/utils/petutils.cpp +++ b/src/map/utils/petutils.cpp @@ -1916,6 +1916,40 @@ namespace petutils return false; } + bool IsTandemActive(CBattleEntity* PAttacker) + { + /* This is used for Tandem Strike (acc/m.acc+) and Tandem Blow (subtle blow II+). + To get the bonus, both pet and master must be engaged in combat with the same target. + Inspired by TiberonKalkaz's approach in ASB. + https://github.com/AirSkyBoat/AirSkyBoat/pull/3134/files#diff-dea0a7c8d005d1e7507dcb2370aff3a46df84ab53d87ba50beeab376c3082621 + */ + CBattleEntity* tandemPartner; + if (PAttacker->objtype == TYPE_PC) + { + if (PAttacker->PPet == nullptr) + return false; + + tandemPartner = PAttacker->PPet; + } + else + { + if (PAttacker->PMaster == nullptr || PAttacker->PMaster->objtype != TYPE_PC) + return false; + + tandemPartner = PAttacker->PMaster; + } + + if ( + tandemPartner->PAI->IsEngaged() && + tandemPartner->GetBattleTarget() != nullptr && + tandemPartner->GetBattleTargetID() == PAttacker->GetBattleTargetID()) + { + return true; + } + + return false; + } + Pet_t* GetPetInfo(uint32 PetID) { for (Pet_t* info : g_PPetList) diff --git a/src/map/utils/petutils.h b/src/map/utils/petutils.h index 54a3efee28f..8ee4364bb91 100644 --- a/src/map/utils/petutils.h +++ b/src/map/utils/petutils.h @@ -214,6 +214,7 @@ namespace petutils void SetupPetWithMaster(CBattleEntity* PMaster, CPetEntity* PPet); bool CheckPetModType(CBattleEntity* PPet, PetModType petmod); + bool IsTandemActive(CBattleEntity* PAttacker); Pet_t* GetPetInfo(uint32 PetID); }; // namespace petutils