1
Fork 0
mirror of https://github.com/SapphireServer/Sapphire.git synced 2025-05-05 02:07:46 +00:00
sapphire/src/world/Math/CalcStats.cpp

883 lines
28 KiB
C++
Raw Normal View History

2017-08-28 16:13:23 -03:00
#include <cmath>
2018-03-06 22:22:19 +01:00
#include <Exd/ExdDataGenerated.h>
#include <Common.h>
#include <Logging/Logger.h>
2020-03-01 01:00:57 +11:00
#include <Service.h>
#include "Actor/Chara.h"
#include "Actor/Player.h"
#include "Inventory/Item.h"
2023-03-07 05:12:18 +09:00
#include "StatusEffect/StatusEffect.h"
#include "Action/Action.h"
#include "CalcStats.h"
using namespace Sapphire::Math;
using namespace Sapphire::Entity;
const int levelTable[81][6] =
{
// MAIN,SUB,DIV,HP,ELMT,THREAT
{ 1, 1, 1, 1, 1, 1 },
2020-01-05 17:41:38 +09:00
{ 20, 56, 56, 86, 52, 2 },
{ 21, 57, 57, 101, 54, 2 },
{ 22, 60, 60, 109, 56, 3 },
{ 24, 62, 62, 116, 58, 3 },
{ 26, 65, 65, 123, 60, 3 },
{ 27, 68, 68, 131, 62, 3 },
{ 29, 70, 70, 138, 64, 4 },
{ 31, 73, 73, 145, 66, 4 },
{ 33, 76, 76, 153, 68, 4 },
{ 35, 78, 78, 160, 70, 5 },
{ 36, 82, 82, 174, 73, 5 },
{ 38, 85, 85, 188, 75, 5 },
{ 41, 89, 89, 202, 78, 6 },
{ 44, 93, 93, 216, 81, 6 },
{ 46, 96, 96, 230, 84, 7 },
{ 49, 100, 100, 244, 86, 7 },
{ 52, 104, 104, 258, 89, 8 },
{ 54, 109, 109, 272, 93, 9 },
{ 57, 113, 113, 286, 95, 9 },
{ 60, 116, 116, 300, 98, 10 },
{ 63, 122, 122, 333, 102, 10 },
{ 67, 127, 127, 366, 105, 11 },
{ 71, 133, 133, 399, 109, 12 },
{ 74, 138, 138, 432, 113, 13 },
{ 78, 144, 144, 465, 117, 14 },
{ 81, 150, 150, 498, 121, 15 },
{ 85, 155, 155, 531, 125, 16 },
{ 89, 162, 162, 564, 129, 17 },
{ 92, 168, 168, 597, 133, 18 },
{ 97, 173, 173, 630, 137, 19 },
{ 101, 181, 181, 669, 143, 20 },
{ 106, 188, 188, 708, 148, 22 },
{ 110, 194, 194, 747, 153, 23 },
{ 115, 202, 202, 786, 159, 25 },
{ 119, 209, 209, 825, 165, 27 },
{ 124, 215, 215, 864, 170, 29 },
{ 128, 223, 223, 903, 176, 31 },
{ 134, 229, 229, 942, 181, 33 },
{ 139, 236, 236, 981, 186, 35 },
{ 144, 244, 244, 1020, 192, 38 },
{ 150, 253, 253, 1088, 200, 40 },
{ 155, 263, 263, 1156, 207, 43 },
{ 161, 272, 272, 1224, 215, 46 },
{ 166, 283, 283, 1292, 223, 49 },
{ 171, 292, 292, 1360, 231, 52 },
{ 177, 302, 302, 1428, 238, 55 },
{ 183, 311, 311, 1496, 246, 58 },
{ 189, 322, 322, 1564, 254, 62 },
{ 196, 331, 331, 1632, 261, 66 },
{ 202, 341, 341, 1700, 269, 70 },
{ 204, 342, 393, 1774, 270, 84 },
{ 205, 344, 444, 1851, 271, 99 },
{ 207, 345, 496, 1931, 273, 113 },
{ 209, 346, 548, 2015, 274, 128 },
{ 210, 347, 600, 2102, 275, 142 },
{ 212, 349, 651, 2194, 276, 157 },
{ 214, 350, 703, 2289, 278, 171 },
{ 215, 351, 755, 2388, 279, 186 },
{ 217, 352, 806, 2492, 280, 200 },
{ 218, 354, 858, 2600, 282, 215 },
{ 224, 355, 941, 2700, 283, 232 },
{ 228, 356, 1032, 2800, 284, 250 },
{ 236, 357, 1133, 2900, 286, 269 },
{ 244, 358, 1243, 3000, 287, 290 },
{ 252, 359, 1364, 3100, 288, 313 },
{ 260, 360, 1497, 3200, 290, 337 },
{ 268, 361, 1643, 3300, 292, 363 },
{ 276, 362, 1802, 3400, 293, 392 },
{ 284, 363, 1978, 3500, 294, 422 },
{ 292, 364, 2170, 3600, 295, 455 },
// todo: add proper shbr values - hp/elmt/threat
// sub/div added from http://theoryjerks.akhmorning.com/resources/levelmods/
2020-01-05 17:41:38 +09:00
{ 296, 365, 2263, 3600, 295, 466 },
{ 300, 366, 2360, 3600, 295, 466 },
{ 305, 367, 2461, 3600, 295, 466 },
{ 310, 368, 2566, 3600, 295, 466 },
{ 315, 370, 2676, 3600, 295, 466 },
{ 320, 372, 2790, 3600, 295, 466 },
{ 325, 374, 2910, 3600, 295, 466 },
{ 330, 376, 3034, 3600, 295, 466 },
{ 335, 378, 3164, 3600, 295, 466 },
{ 340, 380, 3300, 3600, 569, 569 },
};
2023-03-07 05:12:18 +09:00
std::unique_ptr< RandGenerator< float > > CalcStats::rnd = nullptr;
2020-01-07 19:08:13 +09:00
/*
2017-08-20 19:20:37 -03:00
Class used for battle-related formulas and calculations.
Big thanks to the Theoryjerks group!
NOTE:
Formulas here shouldn't be considered final. It's possible that the formula it was based on is correct but
wasn't implemented correctly here, or approximated things due to limited knowledge of how things work in retail.
It's also possible that we're using formulas that were correct for previous patches, but not the current version.
TODO:
2017-08-20 19:20:37 -03:00
Base HP val modifier. I can only find values for levels 50~70.
Dereferencing the actor (Player right now) for stats seem meh, perhaps consider a structure purely for stats?
Reduce repeated code (more specifically the data we pull from exd)
*/
2017-09-07 22:09:30 -03:00
// 3 Versions. SB and HW are linear, ARR is polynomial.
2017-08-20 19:27:06 -03:00
// Originally from Player.cpp, calculateStats().
float CalcStats::calculateBaseStat( const Chara& chara )
2017-08-20 19:27:06 -03:00
{
float base = 0.0f;
uint8_t level = chara.getLevel();
if( level > Common::MAX_PLAYER_LEVEL )
level = Common::MAX_PLAYER_LEVEL;
2020-01-05 17:41:38 +09:00
return static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::MAIN ] );
}
2017-08-20 19:27:06 -03:00
// Leggerless' HP Formula
2017-08-20 19:20:37 -03:00
// ROUNDDOWN(JobModHP * (BaseHP / 100)) + ROUNDDOWN(VitHPMod / 100 * (VIT - BaseDET))
2020-03-01 01:00:57 +11:00
uint32_t CalcStats::calculateMaxHp( PlayerPtr pPlayer )
{
2020-03-01 01:00:57 +11:00
auto& exdData = Common::Service< Data::ExdDataGenerated >::ref();
// TODO: Replace ApproxBaseHP with something that can get us an accurate BaseHP.
// Is there any way to pull reliable BaseHP without having to manually use a pet for every level, and using the values from a table?
// More info here: https://docs.google.com/spreadsheets/d/1de06KGT0cNRUvyiXNmjNgcNvzBCCQku7jte5QxEQRbs/edit?usp=sharing
2017-08-20 19:20:37 -03:00
2020-03-01 01:00:57 +11:00
auto classInfo = exdData.get< Sapphire::Data::ClassJob >( static_cast< uint8_t >( pPlayer->getClass() ) );
auto paramGrowthInfo = exdData.get< Sapphire::Data::ParamGrow >( pPlayer->getLevel() );
if( !classInfo || !paramGrowthInfo )
return 0;
uint8_t level = pPlayer->getLevel();
2017-08-20 19:20:37 -03:00
auto vitMod = pPlayer->getBonusStat( Common::BaseParam::Vitality );
float baseStat = calculateBaseStat( *pPlayer );
uint16_t vitStat = static_cast< uint16_t >( pPlayer->getStats().vit ) + static_cast< uint16_t >( vitMod );
uint16_t hpMod = paramGrowthInfo->hpModifier;
uint16_t jobModHp = classInfo->modifierHitPoints;
float approxBaseHp = 0.0f; // Read above
2017-08-20 19:20:37 -03:00
// These values are not precise.
2020-01-05 17:41:38 +09:00
/*
if( level >= 60 )
approxBaseHp = static_cast< float >( 2600 + ( level - 60 ) * 100 );
else if( level >= 50 )
approxBaseHp = 1700 + ( ( level - 50 ) * ( 1700 * 1.04325f ) );
else
approxBaseHp = paramGrowthInfo->mpModifier * 0.7667f;
2020-01-05 17:41:38 +09:00
*/
// just use the table at least better than what it was
approxBaseHp = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::HP ] );
2017-08-20 19:20:37 -03:00
uint16_t result = static_cast< uint16_t >( floor( jobModHp * ( approxBaseHp / 100.0f ) ) +
floor( hpMod / 100.0f * ( vitStat - baseStat ) ) );
return result;
2017-08-20 19:20:37 -03:00
}
2023-03-15 02:26:44 +09:00
float CalcStats::dodgeProbability( Sapphire::Entity::Chara& chara )
2023-03-07 05:12:18 +09:00
{
// dummy value: 5% for players.
return chara.isPlayer() ? 5 : 0;
}
2023-03-15 02:26:44 +09:00
float CalcStats::blockProbability( Chara& chara )
{
2023-03-15 02:26:44 +09:00
if( !chara.canBlock() )
return 0;
auto level = chara.getLevel();
auto blockRate = static_cast< float >( chara.getStatValue( Common::BaseParam::BlockRate ) );
2019-03-24 14:25:00 +11:00
auto levelVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
2023-03-07 05:12:18 +09:00
float result = std::floor( ( 30 * blockRate ) / levelVal + 10 );
for( auto const& entry : chara.getStatusEffectMap() )
{
result += entry.second->getEffectEntry().getBlockRateBonus();
}
return result;
}
2023-03-15 02:26:44 +09:00
float CalcStats::parryProbability( Sapphire::Entity::Chara& chara )
2023-03-07 05:12:18 +09:00
{
// dummy value: 10% for players.
float result = chara.isPlayer() ? 10 : 0;
for( auto const& entry : chara.getStatusEffectMap() )
{
result += entry.second->getEffectEntry().getParryRateBonus();
}
return result;
}
2023-03-07 05:12:18 +09:00
float CalcStats::directHitProbability( const Chara& chara, Sapphire::Common::CritDHBonusFilter filter )
{
const auto& baseStats = chara.getStats();
auto level = chara.getLevel();
float dhRate = chara.getStatValue( Common::BaseParam::DirectHitRate );
2019-03-24 14:25:00 +11:00
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
auto subVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::SUB ] );
2023-03-07 05:12:18 +09:00
auto result = std::floor( 550.f * ( dhRate - subVal ) / divVal ) / 10.f;
for( auto const& entry : chara.getStatusEffectMap() )
{
auto status = entry.second;
auto effectEntry = status->getEffectEntry();
if( effectEntry.getType() != Common::StatusEffectType::CritDHRateBonus )
continue;
if( static_cast< int32_t >( effectEntry.getActionTypeFilter() ) & static_cast< int32_t >( filter ) )
{
result += effectEntry.getDirectHitRateBonus();
}
}
return result;
}
2023-03-07 05:12:18 +09:00
float CalcStats::criticalHitProbability( const Chara& chara, Sapphire::Common::CritDHBonusFilter filter )
{
const auto& baseStats = chara.getStats();
auto level = chara.getLevel();
float chRate = chara.getStatValue( Common::BaseParam::CriticalHit );
2019-03-24 14:25:00 +11:00
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
auto subVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::SUB ] );
2023-03-07 05:12:18 +09:00
auto result = std::floor( 200.f * ( chRate - subVal ) / divVal + 50.f ) / 10.f;
for( auto const& entry : chara.getStatusEffectMap() )
{
auto status = entry.second;
auto effectEntry = status->getEffectEntry();
if( effectEntry.getType() != Common::StatusEffectType::CritDHRateBonus )
continue;
if( static_cast< int32_t >( effectEntry.getActionTypeFilter() ) & static_cast< int32_t >( filter ) )
{
result += effectEntry.getCritRateBonus();
}
}
return result;
}
2019-03-24 14:25:00 +11:00
float CalcStats::potency( uint16_t potency )
{
return potency / 100.f;
}
2023-03-07 05:12:18 +09:00
float CalcStats::autoAttackPotency( const Sapphire::Entity::Chara& chara, uint32_t aaPotency )
{
float autoAttackDelay = 2.5f;
// fetch actual auto attack delay if its a player
if( chara.isPlayer() )
{
// todo: ew
auto pPlayer = const_cast< Entity::Chara& >( chara ).getAsPlayer();
assert( pPlayer );
auto pItem = pPlayer->getEquippedWeapon();
assert( pItem );
autoAttackDelay = pItem->getDelay() / 1000.f;
}
// factors in f(PTC) in order to not lose precision
return std::floor( aaPotency / 3.f * autoAttackDelay ) / 100.f;
}
float CalcStats::weaponDamage( const Sapphire::Entity::Chara& chara, float weaponDamage )
2019-03-24 14:25:00 +11:00
{
const auto& baseStats = chara.getStats();
auto level = chara.getLevel();
auto mainVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::MAIN ] );
uint32_t jobAttribute = 1;
2019-03-24 14:25:00 +11:00
switch( chara.getPrimaryStat() )
{
case Common::BaseParam::Intelligence:
{
jobAttribute = baseStats.healingPotMagic;
break;
}
case Common::BaseParam::Mind:
{
jobAttribute = baseStats.attackPotMagic;
break;
}
default:
{
jobAttribute = baseStats.attack;
break;
}
}
return std::floor( ( ( mainVal * jobAttribute ) / 1000.f ) + weaponDamage );
2019-03-24 14:25:00 +11:00
}
float CalcStats::calcAttackPower( const Sapphire::Entity::Chara& chara, uint32_t attackPower )
2019-03-24 14:25:00 +11:00
{
auto level = chara.getLevel();
auto mainVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::MAIN ] );
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
// todo: not sure if its ( ap - mv ) / mv or ( ap - mv ) / dv
return std::floor( ( 125.f * ( attackPower - mainVal ) / divVal ) + 100.f ) / 100.f;
2019-03-24 14:25:00 +11:00
}
float CalcStats::getPrimaryAttackPower( const Sapphire::Entity::Chara& chara )
2019-03-24 14:25:00 +11:00
{
const auto& baseStats = chara.getStats();
switch( chara.getPrimaryStat() )
{
case Common::BaseParam::Mind:
{
return healingMagicPower( chara );
}
case Common::BaseParam::Intelligence:
{
return magicAttackPower( chara );
}
default:
{
return attackPower( chara );
}
}
2019-03-24 14:25:00 +11:00
}
float CalcStats::attackPower( const Sapphire::Entity::Chara& chara )
{
return calcAttackPower( chara, chara.getStatValue( Common::BaseParam::AttackPower ) );
}
float CalcStats::magicAttackPower( const Sapphire::Entity::Chara& chara )
{
return calcAttackPower( chara, chara.getStatValue( Common::BaseParam::AttackMagicPotency ) );
}
float CalcStats::healingMagicPower( const Sapphire::Entity::Chara& chara )
{
return calcAttackPower( chara, chara.getStatValue( Common::BaseParam::HealingMagicPotency ) );
}
2023-03-07 05:12:18 +09:00
float CalcStats::getWeaponDamage( CharaPtr chara )
{
auto wepDmg = chara->getLevel();
if( auto player = chara->getAsPlayer() )
{
auto item = player->getEquippedWeapon();
assert( item );
auto role = player->getRole();
if( role == Common::Role::RangedMagical || role == Common::Role::Healer )
{
wepDmg = item->getMagicalDmg();
}
else
{
wepDmg = item->getPhysicalDmg();
}
}
return wepDmg;
}
2019-03-24 14:25:00 +11:00
float CalcStats::determination( const Sapphire::Entity::Chara& chara )
{
auto level = chara.getLevel();
auto mainVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::MAIN ] );
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
return std::floor( 130.f * ( chara.getStatValue( Common::BaseParam::Determination ) - mainVal ) / divVal + 1000.f ) / 1000.f;
2019-03-24 14:25:00 +11:00
}
float CalcStats::tenacity( const Sapphire::Entity::Chara& chara )
{
auto level = chara.getLevel();
auto subVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::SUB ] );
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
return std::floor( 100.f * ( chara.getStatValue( Common::BaseParam::Tenacity ) - subVal ) / divVal + 1000.f ) / 1000.f;
2019-03-24 14:25:00 +11:00
}
float CalcStats::speed( const Sapphire::Entity::Chara& chara )
{
auto level = chara.getLevel();
auto subVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::SUB ] );
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
uint32_t speedVal = 0;
// check whether we use spellspeed or skillspeed
switch( chara.getPrimaryStat() )
2019-03-24 14:25:00 +11:00
{
case Common::BaseParam::Intelligence:
case Common::BaseParam::Mind:
speedVal = chara.getStatValue( Common::BaseParam::SpellSpeed );
2019-03-24 14:25:00 +11:00
break;
default:
speedVal = chara.getStatValue( Common::BaseParam::SkillSpeed );
2019-03-24 14:25:00 +11:00
}
return std::floor( 130.f * ( speedVal - subVal ) / divVal + 1000.f ) / 1000.f;
}
float CalcStats::criticalHitBonus( const Sapphire::Entity::Chara& chara )
{
auto level = chara.getLevel();
auto subVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::SUB ] );
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
return std::floor( 200.f * ( chara.getStatValue( Common::BaseParam::CriticalHit ) - subVal ) / divVal + 1400.f ) / 1000.f;
}
float CalcStats::physicalDefence( const Sapphire::Entity::Chara& chara )
{
auto level = chara.getLevel();
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
return std::floor( 15.f * chara.getStatValue( Common::BaseParam::Defense ) ) / 100.f;
}
float CalcStats::magicDefence( const Sapphire::Entity::Chara& chara )
{
auto level = chara.getLevel();
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
return std::floor( 15.f * chara.getStatValue( Common::BaseParam::MagicDefense ) ) / 100.f;
}
float CalcStats::blockStrength( const Sapphire::Entity::Chara& chara )
{
auto level = chara.getLevel();
auto blockStrength = static_cast< float >( chara.getBonusStat( Common::BaseParam::BlockStrength ) );
auto levelVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
return std::floor( ( 30 * blockStrength ) / levelVal + 10 ) / 100.f;
}
2023-03-07 05:12:18 +09:00
float CalcStats::parryStrength( const Sapphire::Entity::Chara& chara )
{
return 0.15f;
}
float CalcStats::autoAttack( const Sapphire::Entity::Chara& chara )
{
// todo: default values for NPCs, not sure what we should have here
float autoAttackDelay = 2.f;
float weaponDamage = 10.f;
// fetch actual auto attack delay if its a player
if( chara.isPlayer() )
{
// todo: ew
auto pPlayer = const_cast< Entity::Chara& >( chara ).getAsPlayer();
assert( pPlayer );
auto pItem = pPlayer->getEquippedWeapon();
assert( pItem );
autoAttackDelay = pItem->getDelay() / 1000.f;
weaponDamage = pItem->getWeaponDmg();
}
2023-03-07 05:12:18 +09:00
else
{
// dummy value for BNpc
weaponDamage = chara.getLevel() * 3;
}
auto level = chara.getLevel();
auto mainVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::MAIN ] );
auto innerCalc = std::floor( ( mainVal * primaryStatValue( chara ) / 1000.f ) + weaponDamage );
return std::floor( innerCalc * ( autoAttackDelay / 3.f ) );
}
float CalcStats::healingMagicPotency( const Sapphire::Entity::Chara& chara )
{
return std::floor( 100.f * ( chara.getStatValue( Common::BaseParam::HealingMagicPotency ) - 292.f ) / 264.f + 100.f ) / 100.f;
}
2023-03-07 05:12:18 +09:00
std::pair< float, Sapphire::Common::ActionHitSeverityType > CalcStats::calcAutoAttackDamage( const Sapphire::Entity::Chara& chara, uint32_t ptc )
{
// D = ⌊ f(ptc) × f(aa) × f(ap) × f(det) × f(tnc) × traits ⌋ × f(ss) ⌋ ×
// f(chr) ⌋ × f(dhr) ⌋ × rand[ 0.95, 1.05 ] ⌋ × buff_1 ⌋ × buff... ⌋
2023-03-07 05:12:18 +09:00
auto pot = autoAttackPotency( chara, ptc );
auto aa = autoAttack( chara );
auto ap = getPrimaryAttackPower( chara );
auto det = determination( chara );
auto ten = 1.f;
if( chara.getRole() == Common::Role::Tank )
ten = tenacity( chara );
// todo: everything after tenacity
auto factor = std::floor( pot * aa * ap * det * ten );
2020-01-07 19:08:13 +09:00
Sapphire::Common::ActionHitSeverityType hitType = Sapphire::Common::ActionHitSeverityType::NormalDamage;
// todo: traits
factor = std::floor( factor * speed( chara ) );
2023-03-07 05:12:18 +09:00
if( criticalHitProbability( chara, Common::CritDHBonusFilter::Damage ) > getRandomNumber0To100() )
2020-01-07 19:08:13 +09:00
{
factor *= criticalHitBonus( chara );
hitType = Sapphire::Common::ActionHitSeverityType::CritDamage;
}
2023-03-07 05:12:18 +09:00
if( directHitProbability( chara, Common::CritDHBonusFilter::Damage ) > getRandomNumber0To100() )
2020-01-07 19:08:13 +09:00
{
factor *= 1.25f;
hitType = hitType == Sapphire::Common::ActionHitSeverityType::CritDamage ?
Sapphire::Common::ActionHitSeverityType::CritDirectHitDamage :
Sapphire::Common::ActionHitSeverityType::DirectHitDamage;
}
2023-03-07 05:12:18 +09:00
factor *= 1.0f + ( ( getRandomNumber0To100() - 50.0f ) / 1000.0f );
2023-03-07 05:12:18 +09:00
for( auto const& entry : chara.getStatusEffectMap() )
{
2023-03-07 05:12:18 +09:00
auto status = entry.second;
auto effectEntry = status->getEffectEntry();
if( effectEntry.getType() != Common::StatusEffectType::DamageMultiplier )
continue;
if( static_cast< int32_t >( effectEntry.getActionTypeFilter() ) & static_cast< int32_t >( Common::ActionTypeFilter::Physical ) )
{
factor *= 1.0f + ( effectEntry.getOutgoingDamageMultiplier() / 100.0f );
}
}
2023-03-07 05:12:18 +09:00
if( chara.isPlayer() )
{
2023-03-07 05:12:18 +09:00
auto player = const_cast< Entity::Chara& >( chara ).getAsPlayer();
switch( player->getClass() )
{
case Common::ClassJob::Darkknight:
{
if( player->gaugeDrkGetDarkSideTimer() > 0 )
{
factor *= 1.1f;
}
break;
}
}
constexpr auto format = "auto attack: pot: {} aa: {} ap: {} det: {} ten: {} = {}";
player->sendDebug( format, pot, aa, ap, det, ten, factor );
}
2020-01-07 19:08:13 +09:00
return std::pair( factor, hitType );
}
2023-03-07 05:12:18 +09:00
float CalcStats::calcDamageBaseOnPotency( const Sapphire::Entity::Chara& chara, uint32_t ptc, float wepDmg )
{
// D = ⌊ f(pot) × f(wd) × f(ap) × f(det) × f(tnc) × traits ⌋
// × f(chr) ⌋ × f(dhr) ⌋ × rand[ 0.95, 1.05 ] ⌋ buff_1 ⌋ × buff_1 ⌋ × buff... ⌋
auto pot = potency( static_cast< uint16_t >( ptc ) );
auto wd = weaponDamage( chara, wepDmg );
auto ap = getPrimaryAttackPower( chara );
auto det = determination( chara );
auto ten = 1.f;
if( chara.getRole() == Common::Role::Tank )
ten = tenacity( chara );
auto factor = std::floor( pot * wd * ap * det * ten );
2023-03-07 05:12:18 +09:00
constexpr auto format = "dmg: pot: {} ({}) wd: {} ({}) ap: {} det: {} ten: {} = {}";
if( auto player = const_cast< Entity::Chara& >( chara ).getAsPlayer() )
{
player->sendDebug( format, pot, ptc, wd, wepDmg, ap, det, ten, factor );
}
return factor;
}
float CalcStats::calcHealBaseOnPotency( const Sapphire::Entity::Chara& chara, uint32_t ptc, float wepDmg )
{
// reused damage formula just for testing
auto pot = potency( static_cast< uint16_t >( ptc ) );
auto wd = weaponDamage( chara, wepDmg );
auto ap = getPrimaryAttackPower( chara );
auto det = determination( chara );
auto factor = std::floor( pot * wd * ap * det );
constexpr auto format = "heal: pot: {} ({}) wd: {} ({}) ap: {} det: {} = {}";
if( auto player = const_cast< Entity::Chara& >( chara ).getAsPlayer() )
{
player->sendDebug( format, pot, ptc, wd, wepDmg, ap, det, factor );
}
return factor;
}
std::pair< float, Sapphire::Common::ActionHitSeverityType > CalcStats::calcActionDamage( Sapphire::World::Action::Action* pAction, const Sapphire::Entity::Chara& chara, uint32_t ptc, float wepDmg )
{
auto factor = calcDamageBaseOnPotency( chara, ptc, wepDmg );
2020-01-07 19:08:13 +09:00
Sapphire::Common::ActionHitSeverityType hitType = Sapphire::Common::ActionHitSeverityType::NormalDamage;
2023-03-07 05:12:18 +09:00
auto critProb = criticalHitProbability( chara, Common::CritDHBonusFilter::Damage );
if( pAction )
{
auto lutEntry = pAction->getActionEntry();
if( lutEntry.bonusEffect & Common::ActionBonusEffect::CritBonus )
{
if( pAction->checkActionBonusRequirement() )
{
critProb += lutEntry.getCritRateBonus();
}
}
}
if( critProb > getRandomNumber0To100() )
2020-01-07 19:08:13 +09:00
{
factor *= criticalHitBonus( chara );
hitType = Sapphire::Common::ActionHitSeverityType::CritDamage;
}
2023-03-07 05:12:18 +09:00
auto dhProb = directHitProbability( chara, Common::CritDHBonusFilter::Damage );
if( pAction )
{
auto lutEntry = pAction->getActionEntry();
if( lutEntry.bonusEffect & Common::ActionBonusEffect::DHBonus )
{
if( pAction->checkActionBonusRequirement() )
{
dhProb += lutEntry.getDirectHitRateBonus();
}
}
}
if( dhProb > getRandomNumber0To100() )
2020-01-07 19:08:13 +09:00
{
factor *= 1.25f;
hitType = hitType == Sapphire::Common::ActionHitSeverityType::CritDamage ?
Sapphire::Common::ActionHitSeverityType::CritDirectHitDamage :
Sapphire::Common::ActionHitSeverityType::DirectHitDamage;
}
2023-03-07 05:12:18 +09:00
factor *= 1.0f + ( ( getRandomNumber0To100() - 50.0f ) / 1000.0f );
2020-01-07 19:08:13 +09:00
2023-03-07 05:12:18 +09:00
Common::ActionTypeFilter actionTypeFilter = Common::ActionTypeFilter::Physical;
if( pAction && pAction->isMagical() )
{
actionTypeFilter = Common::ActionTypeFilter::Magical;
}
2023-03-07 05:12:18 +09:00
for( auto const& entry : chara.getStatusEffectMap() )
{
2023-03-07 05:12:18 +09:00
auto status = entry.second;
auto effectEntry = status->getEffectEntry();
if( effectEntry.getType() != Common::StatusEffectType::DamageMultiplier )
continue;
if( static_cast< int32_t >( effectEntry.getActionTypeFilter() ) & static_cast< int32_t >( actionTypeFilter ) )
{
factor *= 1.0f + ( effectEntry.getOutgoingDamageMultiplier() / 100.0f );
}
}
2023-03-07 05:12:18 +09:00
if( chara.isPlayer() )
{
2023-03-07 05:12:18 +09:00
auto player = const_cast< Entity::Chara& >( chara ).getAsPlayer();
switch( player->getClass() )
{
case Common::ClassJob::Darkknight:
{
if( player->gaugeDrkGetDarkSideTimer() > 0 )
{
factor *= 1.1f;
}
break;
}
}
}
2020-01-07 19:08:13 +09:00
return std::pair( factor, hitType );
}
2023-03-07 05:12:18 +09:00
float CalcStats::applyDamageReceiveMultiplier( const Sapphire::Entity::Chara& chara, float originalDamage, Common::ActionTypeFilter typeFilter )
{
if( typeFilter == ActionTypeFilter::Unknown )
return originalDamage;
float damage = originalDamage;
for( auto const& entry : chara.getStatusEffectMap() )
{
auto status = entry.second;
auto effectEntry = status->getEffectEntry();
if( effectEntry.getType() != Common::StatusEffectType::DamageReceiveMultiplier )
continue;
if( static_cast< int32_t >( effectEntry.getActionTypeFilter() ) & static_cast< int32_t >( typeFilter ) )
{
damage *= ( 1.0f + ( effectEntry.getIncomingDamageMultiplier() / 100.0f ) );
}
}
return damage;
}
float CalcStats::applyHealingReceiveMultiplier( const Sapphire::Entity::Chara& chara, float originalHeal )
{
float heal = originalHeal;
for( auto const& entry : chara.getStatusEffectMap() )
{
heal *= ( 1.0f + ( entry.second->getEffectEntry().getIncomingHealMultiplier() / 100.0f ) );
}
return heal;
}
std::pair< float, Sapphire::Common::ActionHitSeverityType > CalcStats::calcActionHealing( Sapphire::World::Action::Action* pAction, const Sapphire::Entity::Chara& chara, uint32_t ptc, float wepDmg )
2020-01-07 19:08:13 +09:00
{
2023-03-07 05:12:18 +09:00
auto factor = calcHealBaseOnPotency( chara, ptc, wepDmg );
2020-01-07 19:08:13 +09:00
Sapphire::Common::ActionHitSeverityType hitType = Sapphire::Common::ActionHitSeverityType::NormalHeal;
2023-03-07 05:12:18 +09:00
auto critProb = criticalHitProbability( chara, Common::CritDHBonusFilter::Heal );
if( pAction )
{
auto lutEntry = pAction->getActionEntry();
if( lutEntry.bonusEffect & Common::ActionBonusEffect::CritBonus )
{
if( pAction->checkActionBonusRequirement() )
{
critProb += lutEntry.getCritRateBonus();
}
}
}
if( critProb > getRandomNumber0To100() )
2020-01-07 19:08:13 +09:00
{
factor *= criticalHitBonus( chara );
hitType = Sapphire::Common::ActionHitSeverityType::CritHeal;
}
2023-03-07 05:12:18 +09:00
factor *= 1.0f + ( ( getRandomNumber0To100() - 50.0f ) / 1000.0f );
for( auto const& entry : chara.getStatusEffectMap() )
{
auto status = entry.second;
auto effectEntry = status->getEffectEntry();
if( effectEntry.getType() != Common::StatusEffectType::HealCastMultiplier )
continue;
if( pAction->isGCD() ) // must be a "cast"
{
factor *= 1.0f + ( effectEntry.getOutgoingHealMultiplier() / 100.0f );
}
}
2020-01-07 19:08:13 +09:00
return std::pair( factor, hitType );
}
uint32_t CalcStats::primaryStatValue( const Sapphire::Entity::Chara& chara )
{
return chara.getStatValue( chara.getPrimaryStat() );
2023-03-07 05:12:18 +09:00
}
std::pair< float, Sapphire::Common::ActionHitSeverityType > CalcStats::calcDamageReflect( Sapphire::Entity::CharaPtr pCharaAttacker, Sapphire::Entity::CharaPtr pCharaVictim, float damage, Sapphire::Common::ActionTypeFilter filter )
{
for( auto const& entry : pCharaVictim->getStatusEffectMap() )
{
auto status = entry.second;
auto effectEntry = status->getEffectEntry();
if( effectEntry.getType() == Common::StatusEffectType::DamageReceiveTrigger && ( static_cast< int32_t >( effectEntry.getActionTypeFilter() ) & static_cast< int32_t >( filter ) ) )
{
if( effectEntry.getTriggerResult() == Common::StatusEffectTriggerResult::ReflectDamage )
{
auto wepDmg = Sapphire::Math::CalcStats::getWeaponDamage( pCharaVictim );
auto damage = Sapphire::Math::CalcStats::calcActionDamage( nullptr, *pCharaVictim, effectEntry.getTriggerValue(), wepDmg );
damage.first = Math::CalcStats::applyDamageReceiveMultiplier( *pCharaAttacker, damage.first, effectEntry.getTriggerDamageType() );
return damage;
}
}
}
return std::pair< float, Sapphire::Common::ActionHitSeverityType >( 0, Sapphire::Common::ActionHitSeverityType::NormalDamage );
}
float CalcStats::calcAbsorbHP( Sapphire::Entity::CharaPtr pChara, float damage )
{
float result = 0;
for( auto const& entry : pChara->getStatusEffectMap() )
{
auto status = entry.second;
auto effectEntry = status->getEffectEntry();
if( effectEntry.getType() == Common::StatusEffectType::DamageDealtTrigger )
{
if( effectEntry.getTriggerResult() == Common::StatusEffectTriggerResult::AbsorbHP )
{
result += damage * effectEntry.getTriggerValue() / 100.0f;
}
}
}
return result;
}
2023-03-15 02:26:44 +09:00
bool CalcStats::calcDodge( Sapphire::Entity::Chara& chara )
2023-03-07 05:12:18 +09:00
{
if( dodgeProbability( chara ) > getRandomNumber0To100() )
{
return true;
}
return false;
}
2023-03-15 02:26:44 +09:00
float CalcStats::calcBlock( Sapphire::Entity::Chara& chara, float damage )
2023-03-07 05:12:18 +09:00
{
if( blockProbability( chara ) > getRandomNumber0To100() )
{
return damage * blockStrength( chara );
}
return 0;
}
2023-03-15 02:26:44 +09:00
float CalcStats::calcParry( Sapphire::Entity::Chara& chara, float damage )
2023-03-07 05:12:18 +09:00
{
if( parryProbability( chara ) > getRandomNumber0To100() )
{
return damage * parryStrength( chara );
}
return 0;
}
float CalcStats::getRandomNumber0To100()
{
if( !rnd )
{
rnd = std::make_unique< RandGenerator< float > >( Service< RNGMgr >::ref().getRandGenerator< float >( 0, 100 ) );
}
return rnd->next();
2019-03-24 14:25:00 +11:00
}