1
Fork 0
mirror of https://github.com/SapphireServer/Sapphire.git synced 2025-04-25 05:57:45 +00:00
sapphire/src/world/Math/CalcStats.cpp

516 lines
No EOL
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <cmath>
#include <Exd/ExdDataGenerated.h>
#include <Common.h>
#include <Logging/Logger.h>
#include "Actor/Chara.h"
#include "Actor/Player.h"
#include "Inventory/Item.h"
#include "CalcStats.h"
#include "Framework.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 },
{ 20, 56, 56, 0, 52, 2 },
{ 21, 57, 57, 0, 54, 2 },
{ 22, 60, 60, 0, 56, 3 },
{ 24, 62, 62, 0, 58, 3 },
{ 26, 65, 65, 0, 60, 3 },
{ 27, 68, 68, 0, 62, 3 },
{ 29, 70, 70, 0, 64, 4 },
{ 31, 73, 73, 0, 66, 4 },
{ 33, 76, 76, 0, 68, 4 },
{ 35, 78, 78, 0, 70, 5 },
{ 36, 82, 82, 0, 73, 5 },
{ 38, 85, 85, 0, 75, 5 },
{ 41, 89, 89, 0, 78, 6 },
{ 44, 93, 93, 0, 81, 6 },
{ 46, 96, 96, 0, 84, 7 },
{ 49, 100, 100, 0, 86, 7 },
{ 52, 104, 104, 0, 89, 8 },
{ 54, 109, 109, 0, 93, 9 },
{ 57, 113, 113, 0, 95, 9 },
{ 60, 116, 116, 0, 98, 10 },
{ 63, 122, 122, 0, 102, 10 },
{ 67, 127, 127, 0, 105, 11 },
{ 71, 133, 133, 0, 109, 12 },
{ 74, 138, 138, 0, 113, 13 },
{ 78, 144, 144, 0, 117, 14 },
{ 81, 150, 150, 0, 121, 15 },
{ 85, 155, 155, 0, 125, 16 },
{ 89, 162, 162, 0, 129, 17 },
{ 92, 168, 168, 0, 133, 18 },
{ 97, 173, 173, 0, 137, 19 },
{ 101, 181, 181, 0, 143, 20 },
{ 106, 188, 188, 0, 148, 22 },
{ 110, 194, 194, 0, 153, 23 },
{ 115, 202, 202, 0, 159, 25 },
{ 119, 209, 209, 0, 165, 27 },
{ 124, 215, 215, 0, 170, 29 },
{ 128, 223, 223, 0, 176, 31 },
{ 134, 229, 229, 0, 181, 33 },
{ 139, 236, 236, 0, 186, 35 },
{ 144, 244, 244, 0, 192, 38 },
{ 150, 253, 253, 0, 200, 40 },
{ 155, 263, 263, 0, 207, 43 },
{ 161, 272, 272, 0, 215, 46 },
{ 166, 283, 283, 0, 223, 49 },
{ 171, 292, 292, 0, 231, 52 },
{ 177, 302, 302, 0, 238, 55 },
{ 183, 311, 311, 0, 246, 58 },
{ 189, 322, 322, 0, 254, 62 },
{ 196, 331, 331, 0, 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/
{ 296, 365, 2263, 3600, 466, 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 },
};
/*
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:
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)
*/
// 3 Versions. SB and HW are linear, ARR is polynomial.
// Originally from Player.cpp, calculateStats().
float CalcStats::calculateBaseStat( const Chara& chara )
{
float base = 0.0f;
uint8_t level = chara.getLevel();
if( level > Common::MAX_PLAYER_LEVEL )
level = Common::MAX_PLAYER_LEVEL;
return static_cast< float >( levelTable[level][2] );
}
// Leggerless' HP Formula
// ROUNDDOWN(JobModHP * (BaseHP / 100)) + ROUNDDOWN(VitHPMod / 100 * (VIT - BaseDET))
uint32_t CalcStats::calculateMaxHp( PlayerPtr pPlayer, Sapphire::FrameworkPtr pFw )
{
auto pExdData = pFw->get< Data::ExdDataGenerated >();
// 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
auto classInfo = pExdData->get< Sapphire::Data::ClassJob >( static_cast< uint8_t >( pPlayer->getClass() ) );
auto paramGrowthInfo = pExdData->get< Sapphire::Data::ParamGrow >( pPlayer->getLevel() );
if( !classInfo || !paramGrowthInfo )
return 0;
uint8_t level = pPlayer->getLevel();
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
// These values are not precise.
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;
uint16_t result = static_cast< uint16_t >( floor( jobModHp * ( approxBaseHp / 100.0f ) ) +
floor( hpMod / 100.0f * ( vitStat - baseStat ) ) );
return result;
}
float CalcStats::blockProbability( const Chara& chara )
{
auto level = chara.getLevel();
auto blockRate = static_cast< float >( chara.getStatValue( Common::BaseParam::BlockRate ) );
auto levelVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
return std::floor( ( 30 * blockRate ) / levelVal + 10 );
}
float CalcStats::directHitProbability( const Chara& chara )
{
const auto& baseStats = chara.getStats();
auto level = chara.getLevel();
float dhRate = chara.getStatValue( Common::BaseParam::DirectHitRate );
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
auto subVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::SUB ] );
return std::floor( 550.f * ( dhRate - subVal ) / divVal ) / 10.f;
}
float CalcStats::criticalHitProbability( const Chara& chara )
{
const auto& baseStats = chara.getStats();
auto level = chara.getLevel();
float chRate = chara.getStatValue( Common::BaseParam::CriticalHit );
auto divVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::DIV ] );
auto subVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::SUB ] );
return std::floor( 200.f * ( chRate - subVal ) / divVal + 50.f ) / 10.f;
}
float CalcStats::potency( uint16_t potency )
{
return potency / 100.f;
}
float CalcStats::autoAttackPotency( const Sapphire::Entity::Chara& chara )
{
uint32_t aaPotency = AUTO_ATTACK_POTENCY;
if( chara.getRole() == Common::Role::RangedPhysical )
{
aaPotency = RANGED_AUTO_ATTACK_POTENCY;
}
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 )
{
const auto& baseStats = chara.getStats();
auto level = chara.getLevel();
auto mainVal = static_cast< float >( levelTable[ level ][ Common::LevelTableEntry::MAIN ] );
uint32_t jobAttribute = 1;
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 );
}
float CalcStats::calcAttackPower( const Sapphire::Entity::Chara& chara, uint32_t attackPower )
{
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;
}
float CalcStats::getPrimaryAttackPower( const Sapphire::Entity::Chara& chara )
{
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 );
}
}
}
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 ) );
}
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;
}
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;
}
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() )
{
case Common::BaseParam::Intelligence:
case Common::BaseParam::Mind:
speedVal = chara.getStatValue( Common::BaseParam::SpellSpeed );
break;
default:
speedVal = chara.getStatValue( Common::BaseParam::SkillSpeed );
}
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;
}
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();
}
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;
}
float CalcStats::calcAutoAttackDamage( const Sapphire::Entity::Chara& chara )
{
// 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... ⌋
auto pot = autoAttackPotency( chara );
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 );
constexpr auto format = "auto attack: pot: {} aa: {} ap: {} det: {} ten: {} = {}";
if( auto player = const_cast< Entity::Chara& >( chara ).getAsPlayer() )
{
player->sendDebug( format, pot, aa, ap, det, ten, factor );
}
else
{
Logger::debug( format, pot, aa, ap, det, ten, factor );
}
// todo: traits
factor = std::floor( factor * speed( chara ) );
// todo: surely this aint right?
//factor = std::floor( factor * criticalHitProbability( chara ) );
//factor = std::floor( factor * directHitProbability( chara ) );
// todo: random 0.95 - 1.05 factor
// todo: buffs
return factor;
}
float CalcStats::calcActionDamage( 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 );
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 );
}
else
{
Logger::debug( format, pot, ptc, wd, wepDmg, ap, det, ten, factor );
}
// todo: the rest
return factor;
}
uint32_t CalcStats::primaryStatValue( const Sapphire::Entity::Chara& chara )
{
return chara.getStatValue( chara.getPrimaryStat() );
}