2017-08-02 23:06:11 +01:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
using System.Threading.Tasks ;
using FFXIVClassic_Map_Server.Actors ;
2017-08-28 04:45:20 +01:00
using FFXIVClassic_Map_Server.packets.send.actor ;
using FFXIVClassic_Map_Server.packets.send.actor.battle ;
2018-02-15 13:20:46 -06:00
using FFXIVClassic_Map_Server.actors.chara.player ;
using FFXIVClassic_Map_Server.dataobjects ;
2017-08-29 01:15:12 +01:00
using FFXIVClassic.Common ;
2017-08-02 23:06:11 +01:00
namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
{
static class BattleUtils
{
2018-02-15 13:20:46 -06:00
public static Dictionary < HitType , ushort > HitTypeTextIds = new Dictionary < HitType , ushort > ( )
{
{ HitType . Miss , 30311 } ,
{ HitType . Evade , 30310 } ,
{ HitType . Parry , 30308 } ,
{ HitType . Block , 30306 } ,
{ HitType . Resist , 30306 } , //I can't find what the actual textid for resists is
{ HitType . Hit , 30301 } ,
{ HitType . Crit , 30302 }
} ;
public static Dictionary < HitType , HitEffect > HitTypeEffects = new Dictionary < HitType , HitEffect > ( )
{
{ HitType . Miss , 0 } ,
{ HitType . Evade , HitEffect . Evade } ,
{ HitType . Parry , HitEffect . Parry } ,
{ HitType . Block , HitEffect . Block } ,
{ HitType . Resist , HitEffect . RecoilLv1 } , //Probably don't need this, resists are handled differently to the rest
{ HitType . Hit , HitEffect . Hit } ,
{ HitType . Crit , HitEffect . Crit }
} ;
2017-08-28 21:45:01 -04:00
public static bool TryAttack ( Character attacker , Character defender , BattleAction action , ref BattleAction error )
2017-08-28 04:45:20 +01:00
{
// todo: get hit rate, hit count, set hit effect
2018-02-15 13:20:46 -06:00
//action.effectId |= (uint)(HitEffect.RecoilLv2 | HitEffect.Hit | HitEffect.HitVisual1);
2017-08-29 01:15:12 +01:00
return true ;
2017-08-28 04:45:20 +01:00
}
2018-02-15 13:20:46 -06:00
private static double CalculateDlvlModifier ( short dlvl )
{
//this is just a really, really simplified version of the graph from http://kanican.livejournal.com/55915.html
//actual formula is definitely more complicated
//I'm going to assum these formulas are linear, and they're clamped so the modifier never goes below 0.
double modifier = 0 ;
if ( dlvl > = 0 )
modifier = ( . 35 * dlvl ) + . 225 ;
else
modifier = ( . 01 * dlvl ) + . 25 ;
return modifier . Clamp ( 0 , 1 ) ;
}
//Damage calculations
//Calculate damage of action
//We could probably just do this when determining the action's hit type
public static void CalculateDamage ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
short dlvl = ( short ) ( defender . GetLevel ( ) - attacker . GetLevel ( ) ) ;
switch ( action . hitType )
{
//Misses and evades deal no damage.
case ( HitType . Miss ) :
case ( HitType . Evade ) :
action . amount = 0 ;
break ;
// todo: figure out parry damage reduction. For now assume 25% reduction
case ( HitType . Parry ) :
CalculateParryDamage ( attacker , defender , skill , action ) ;
break ;
case ( HitType . Block ) :
CalculateBlockDamage ( attacker , defender , skill , action ) ;
break ;
//There are 3 (or 4?) tiers of resists, each decreasing damage dealt by 25%. For now just assume level 2 resist (50% reduction)
// todo: figure out resist tiers
case ( HitType . Resist ) :
CalculateResistDamage ( attacker , defender , skill , action ) ;
break ;
case ( HitType . Crit ) :
CalculateCritDamage ( attacker , defender , skill , action ) ;
break ;
}
ushort finalAmount = action . amount ;
//dlvl, Defense, and Vitality all effect how much damage is taken after hittype takes effect
//player attacks cannot do more than 9999 damage.
action . amount = ( ushort ) ( finalAmount - CalculateDlvlModifier ( dlvl ) * ( defender . GetMod ( ( uint ) Modifier . Defense ) + 0.67 * defender . GetMod ( ( uint ) Modifier . Vitality ) ) ) . Clamp ( 0 , 9999 ) ;
}
public static void CalculateBlockDamage ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
double percentBlocked = defender . GetMod ( ( uint ) Modifier . Block ) * . 2 ; //Every point of Block adds .2% to how much is blocked
percentBlocked + = defender . GetMod ( ( uint ) Modifier . Vitality ) * . 1 ; //Every point of vitality adds .1% to how much is blocked
percentBlocked = 1 - percentBlocked ;
action . amount = ( ushort ) ( action . amount * percentBlocked ) ;
}
//don't know crit formula
public static void CalculateCritDamage ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
short dlvl = ( short ) ( defender . GetLevel ( ) - attacker . GetLevel ( ) ) ;
double bonus = ( . 04 * ( dlvl * dlvl ) ) - 2 * dlvl ;
bonus + = 1.20 ;
double potencyModifier = ( - . 075 * dlvl ) + 1.73 ;
// + potency bonus
//bonus += attacker.GetMod((uint) Modifier.CriticalPotency) * potencyModifier;
// - Crit resilience
//bonus -= attacker.GetMod((uint)Modifier.CriticalResilience) * potencyModifier;
//need to add something for bonus potency as a part of skill (ie thundara)
action . amount = ( ushort ) ( action . amount * bonus . Clamp ( 1.15 , 1.75 ) ) ; //min bonus of 115, max bonus of 175
}
public static void CalculateParryDamage ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
double percentParry = . 75 ;
action . amount = ( ushort ) ( action . amount * percentParry ) ;
}
//There are 3 or 4 tiers of resist that are flat 25% decreases in damage.
//It's possible we could just calculate the damage at the same time as we determine the hit type (the same goes for the rest of the hit types)
//Or we could have HitTypes for DoubleResist, TripleResist, and FullResist that get used here.
public static void CalculateResistDamage ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
double percentResist = . 5 ;
action . amount = ( ushort ) ( action . amount * percentResist ) ;
}
//Used for attacks and abilities like Jump that deal damage
2017-08-28 04:45:20 +01:00
public static ushort CalculateAttackDamage ( Character attacker , Character defender , BattleAction action )
{
2018-02-15 13:20:46 -06:00
ushort damage = ( ushort ) 100 ;
if ( attacker is Player p )
{
var weapon = p . GetEquipment ( ) . GetItemAtSlot ( Equipment . SLOT_MAINHAND ) ;
if ( weapon ! = null )
{
var weaponData = Server . GetItemGamedata ( weapon . itemId ) ;
//just some numbers from https://www.bluegartr.com/threads/107403-Stats-and-how-they-work/page24
damage + = ( ushort ) ( 2.225 * ( weaponData as WeaponItem ) . damagePower + ( attacker . GetMod ( ( uint ) Modifier . Attack ) * . 38 ) ) ;
}
}
2017-08-28 04:45:20 +01:00
// todo: handle all other crap before protect/stoneskin
// todo: handle crit etc
if ( defender . statusEffects . HasStatusEffect ( StatusEffectId . Protect ) | | defender . statusEffects . HasStatusEffect ( StatusEffectId . Protect2 ) )
{
if ( action ! = null )
action . effectId | = ( uint ) HitEffect . Protect ;
}
if ( defender . statusEffects . HasStatusEffect ( StatusEffectId . Stoneskin ) )
{
if ( action ! = null )
action . effectId | = ( uint ) HitEffect . Stoneskin ;
}
return damage ;
}
2017-08-29 01:15:12 +01:00
public static ushort GetCriticalHitDamage ( Character attacker , Character defender , BattleAction action )
{
ushort damage = action . amount ;
// todo:
//
// action.effectId |= (uint)HitEffect.Critical;
//
return damage ;
}
2017-08-28 04:45:20 +01:00
public static ushort CalculateSpellDamage ( Character attacker , Character defender , BattleAction action )
{
ushort damage = 0 ;
// todo: handle all other crap before shell/stoneskin
if ( defender . statusEffects . HasStatusEffect ( StatusEffectId . Shell ) )
{
if ( defender . statusEffects . HasStatusEffect ( StatusEffectId . Shell ) )
{
}
}
if ( defender . statusEffects . HasStatusEffect ( StatusEffectId . Stoneskin ) )
{
}
return damage ;
}
2018-02-15 13:20:46 -06:00
public static void DamageTarget ( Character attacker , Character defender , BattleAction action , DamageTakenType type , bool sendBattleAction = false )
2017-08-02 23:06:11 +01:00
{
2017-12-08 00:58:39 -06:00
if ( defender ! = null )
2017-08-02 23:06:11 +01:00
{
2017-12-08 00:58:39 -06:00
// todo: other stuff too
if ( defender is BattleNpc )
2017-08-02 23:06:11 +01:00
{
2017-12-08 02:08:13 -06:00
var bnpc = defender as BattleNpc ;
if ( ! bnpc . hateContainer . HasHateForTarget ( attacker ) )
2017-12-08 00:58:39 -06:00
{
2017-12-08 02:08:13 -06:00
bnpc . hateContainer . AddBaseHate ( attacker ) ;
2017-12-08 00:58:39 -06:00
}
2018-02-15 13:20:46 -06:00
bnpc . hateContainer . UpdateHate ( attacker , action . enmity ) ;
2017-12-08 02:08:13 -06:00
bnpc . lastAttacker = attacker ;
2017-08-02 23:06:11 +01:00
}
2018-02-15 13:20:46 -06:00
defender . DelHP ( ( short ) action . amount ) ;
2017-12-08 02:08:13 -06:00
defender . OnDamageTaken ( attacker , action , type ) ;
2017-08-02 23:06:11 +01:00
}
}
2017-08-25 03:52:43 +01:00
2018-02-15 13:20:46 -06:00
public static void DoAction ( Character user , Character receiver , BattleAction action , DamageTakenType type = DamageTakenType . None )
{
switch ( action . battleActionType )
{
//split attack into phys/mag?
case ( BattleActionType . AttackMagic ) : //not sure if these use different damage taken formulas
case ( BattleActionType . AttackPhysical ) :
DamageTarget ( user , receiver , action , type , false ) ;
break ;
case ( BattleActionType . Heal ) :
receiver . AddHP ( action . amount ) ;
break ;
}
if ( ( type = = DamageTakenType . Ability | | type = = DamageTakenType . Attack ) & & action . amount ! = 0 )
{
receiver . AddTP ( 150 ) ;
user . AddTP ( 200 ) ;
}
}
/ *
* Rate functions
* /
//How is accuracy actually calculated?
public static double GetHitRate ( Character attacker , Character defender , BattleCommand skill )
{
double hitRate = . 80 ;
//Certain skills have lower or higher accuracy rates depending on position/combo
return hitRate * ( skill ! = null ? skill . accuracyModifier : 1 ) ;
}
//Whats the parry formula?
public static double GetParryRate ( Character attacker , Character defender , BattleCommand skill )
{
//Can't parry with shield, must be facing attacker
if ( defender . GetMod ( ( uint ) Modifier . HasShield ) > 0 | | ! defender . IsFacing ( attacker ) )
return 0 ;
return . 10 ;
}
public static double GetCritRate ( Character attacker , Character defender , BattleCommand skill )
{
double critRate = 10 ; // .0016 * attacker.GetMod((uint)Modifier.CritRating);//Crit rating adds .16% per point
return Math . Min ( critRate , . 20 ) ; //Crit rate is capped at 20%
}
//http://kanican.livejournal.com/55370.html
// todo: figure that out
public static double GetResistRate ( Character attacker , Character defender , BattleCommand skill )
{
return . 95 ;
}
//Block Rate follows 4 simple rules:
//(1) Every point in DEX gives +0.1% rate
//(2) Every point in "Block Rate" gives +0.2% rate
//(3) True block proc rate is capped at 75%. No clue on a possible floor.
//(4) The baseline rate is based on dLVL only(mob stats play no role). The baseline rate is summarized in this raw data sheet: https://imgbox.com/aasLyaJz
public static double GetBlockRate ( Character attacker , Character defender , BattleCommand skill )
{
//Shields are required to block.
if ( defender . GetMod ( ( uint ) Modifier . HasShield ) = = 0 ) //|| !defender.IsFacing(attacker))
return 0 ;
short dlvl = ( short ) ( attacker . GetLevel ( ) - defender . GetLevel ( ) ) ;
double blockRate = ( - 2.5 * dlvl ) - 5 ; // Base block rate
blockRate + = attacker . GetMod ( ( uint ) Modifier . Dexterity ) * . 1 ; // .1% for every dex
blockRate + = attacker . GetMod ( ( uint ) Modifier . BlockRate ) * . 2 ; // .2% for every block rate
return Math . Min ( blockRate , 25 ) ;
}
/ *
* HitType helpers . Used for determining if attacks are hits , crits , blocks , etc . and changing their damage based on that
* /
public static bool TryCrit ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
if ( Program . Random . NextDouble ( ) < GetCritRate ( attacker , defender , skill ) )
{
action . hitType = HitType . Crit ;
CalculateCritDamage ( attacker , defender , skill , action ) ;
return true ;
}
return false ;
}
public static bool TryResist ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
if ( Program . Random . NextDouble ( ) < GetResistRate ( attacker , defender , skill ) )
{
action . hitType = HitType . Resist ;
CalculateResistDamage ( attacker , defender , skill , action ) ;
return true ;
}
return false ;
}
public static bool TryBlock ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
if ( Program . Random . NextDouble ( ) < GetBlockRate ( attacker , defender , skill ) )
{
action . hitType = HitType . Block ;
defender . SetProc ( ( int ) HitType . Block ) ;
CalculateBlockDamage ( attacker , defender , skill , action ) ;
return true ;
}
return false ;
}
public static bool TryParry ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
if ( Program . Random . NextDouble ( ) < GetParryRate ( attacker , defender , skill ) )
{
action . hitType = HitType . Parry ;
defender . SetProc ( ( int ) HitType . Parry ) ;
CalculateParryDamage ( attacker , defender , skill , action ) ;
return true ;
}
return false ;
}
//TryMiss instead of tryHit because hits are the default and don't change damage
public static bool TryMiss ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
if ( Program . Random . NextDouble ( ) > GetHitRate ( attacker , defender , skill ) )
{
action . hitType = HitType . Miss ;
action . amount = 0 ;
defender . SetProc ( ( int ) HitType . Evade ) ;
attacker . SetProc ( ( int ) HitType . Miss ) ;
return true ;
}
return false ;
}
/ *
* Hit Effecthelpers . Different types of hit effects hits use some flags for different things , so they ' re split into physical , magical , heal , and status
* /
public static void CalcHitType ( Character caster , Character target , BattleCommand skill , BattleAction action )
{
//Might be a simpler way to do this?
switch ( action . battleActionType )
{
case ( BattleActionType . AttackPhysical ) :
SetHitEffectPhysical ( caster , target , skill , action ) ;
break ;
case ( BattleActionType . AttackMagic ) :
SetHitEffectMagical ( caster , target , skill , action ) ;
break ;
case ( BattleActionType . Heal ) :
SetHitEffectHeal ( caster , target , skill , action ) ;
break ;
case ( BattleActionType . Status ) :
SetHitEffectStatus ( caster , target , skill , action ) ;
break ;
}
}
public static void SetHitEffectPhysical ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
//Determine the hittype of the action and change amount of damage it does based on that
if ( ! TryMiss ( attacker , defender , skill , action ) )
if ( ! TryCrit ( attacker , defender , skill , action ) )
if ( ! TryBlock ( attacker , defender , skill , action ) )
TryParry ( attacker , defender , skill , action ) ;
var hitEffect = HitEffect . HitEffectType ;
//Don't know what recoil is actually based on, just guessing
//Crit is 2 and 3 together
if ( action . hitType = = HitType . Crit )
hitEffect | = HitEffect . CriticalHit ;
else
{
double percentDealt = ( 100.0 * ( action . amount / defender . GetMaxHP ( ) ) ) ;
if ( percentDealt > 5.0 )
hitEffect | = HitEffect . RecoilLv2 ;
else if ( percentDealt > 10 )
hitEffect | = HitEffect . RecoilLv3 ;
}
action . worldMasterTextId = HitTypeTextIds [ action . hitType ] ;
hitEffect | = HitTypeEffects [ action . hitType ] ;
if ( skill ! = null & & skill . isCombo & & action . hitType > HitType . Evade )
hitEffect | = ( HitEffect ) ( skill . comboStep < < 15 ) ;
//if attack hit the target, take into account protective status effects
if ( action . hitType > = HitType . Parry )
{
//Protect / Shell only show on physical/ magical attacks respectively.
if ( defender . statusEffects . HasStatusEffect ( StatusEffectId . Protect ) )
if ( action ! = null )
hitEffect | = HitEffect . Protect ;
if ( defender . statusEffects . HasStatusEffect ( StatusEffectId . Stoneskin ) )
if ( action ! = null )
hitEffect | = HitEffect . Stoneskin ;
}
action . effectId = ( uint ) hitEffect ;
}
public static void SetHitEffectMagical ( Character attacker , Character defender , BattleCommand skill , BattleAction action )
{
//Determine the hit type of the action
if ( ! TryMiss ( attacker , defender , skill , action ) )
if ( ! TryCrit ( attacker , defender , skill , action ) )
TryResist ( attacker , defender , skill , action ) ;
var hitEffect = HitEffect . MagicEffectType ;
//Recoil levels for spells are a bit different than physical. Recoil levels are used for resists.
//Lv1 is for larger resists, Lv2 is for smaller resists and Lv3 is for no resists. Crit is still used for crits
if ( action . hitType = = HitType . Resist )
{
//todo: calculate resist levels and figure out what the difference between Lv1 and 2 in retail was. For now assuming a full resist with 0 damage dealt is Lv1, all other resists Lv2
if ( action . amount = = 0 )
hitEffect | = HitEffect . RecoilLv1 ;
else
hitEffect | = HitEffect . RecoilLv2 ;
}
else if ( action . hitType = = HitType . Crit )
hitEffect | = HitEffect . Crit ;
else
hitEffect | = HitEffect . RecoilLv3 ;
action . worldMasterTextId = HitTypeTextIds [ action . hitType ] ;
hitEffect | = HitTypeEffects [ action . hitType ] ;
if ( skill ! = null & & skill . isCombo )
hitEffect | = ( HitEffect ) ( skill . comboStep < < 15 ) ;
//if attack hit the target, take into account protective status effects
if ( action . hitType > = HitType . Block )
{
//Protect / Shell only show on physical/ magical attacks respectively.
if ( defender . statusEffects . HasStatusEffect ( StatusEffectId . Shell ) )
if ( action ! = null )
hitEffect | = HitEffect . Shell ;
if ( defender . statusEffects . HasStatusEffect ( StatusEffectId . Stoneskin ) )
if ( action ! = null )
hitEffect | = HitEffect . Stoneskin ;
}
action . effectId = ( uint ) hitEffect ;
}
public static void SetHitEffectHeal ( Character caster , Character receiver , BattleCommand skill , BattleAction action )
{
var hitEffect = HitEffect . MagicEffectType | HitEffect . Heal ;
//Heals use recoil levels in some way as well. Possibly for very low health clutch heals or based on percentage of current health healed (not max health).
// todo: figure recoil levels out for heals
hitEffect | = HitEffect . RecoilLv3 ;
//do heals crit?
action . effectId = ( uint ) hitEffect ;
}
public static void SetHitEffectStatus ( Character caster , Character receiver , BattleCommand skill , BattleAction action )
{
var hitEffect = ( uint ) HitEffect . StatusEffectType | skill . statusId ;
action . effectId = hitEffect ;
}
2017-08-28 04:45:20 +01:00
public static int CalculateSpellDamage ( Character attacker , Character defender , BattleCommand spell )
2017-08-25 03:52:43 +01:00
{
// todo: spell formulas and stuff (stoneskin, mods, stats, etc)
return 69 ;
}
2017-08-28 04:45:20 +01:00
public static uint CalculateSpellCost ( Character caster , Character target , BattleCommand spell )
{
2018-02-15 13:20:46 -06:00
var scaledCost = spell . CalculateMpCost ( caster ) ;
2017-08-28 04:45:20 +01:00
// todo: calculate cost for mob/player
2017-08-31 05:56:43 +01:00
if ( caster is BattleNpc )
2017-08-28 04:45:20 +01:00
{
}
else
{
}
return scaledCost ;
}
2018-02-15 13:20:46 -06:00
//Convert a HitDirection to a BattleCommandPositionBonus. Basically just combining left/right into flank
public static BattleCommandPositionBonus ConvertHitDirToPosition ( HitDirection hitDir )
{
BattleCommandPositionBonus position = BattleCommandPositionBonus . None ;
switch ( hitDir )
{
case ( HitDirection . Front ) :
position = BattleCommandPositionBonus . Front ;
break ;
case ( HitDirection . Right ) :
case ( HitDirection . Left ) :
position = BattleCommandPositionBonus . Flank ;
break ;
case ( HitDirection . Rear ) :
position = BattleCommandPositionBonus . Rear ;
break ;
}
return position ;
}
//IsAdditional is needed because additional actions may be required for some actions' effects
//For instance, Goring Blade's bleed effect requires another action so the first action can still show damage numbers
//Sentinel doesn't require an additional action because it doesn't need to show those numbers
public static void TryStatus ( Character caster , Character target , BattleCommand skill , BattleAction action , bool isAdditional = true )
{
double rand = Program . Random . NextDouble ( ) ;
( caster as Player ) . SendMessage ( 0x20 , "" , rand . ToString ( ) ) ;
if ( skill ! = null & & action . amount < target . GetHP ( ) & & skill . statusId ! = 0 & & action . hitType > HitType . Evade & & rand < skill . statusChance )
{
StatusEffect effect = Server . GetWorldManager ( ) . GetStatusEffect ( skill . statusId ) ;
//Because combos might change duration or tier
if ( effect ! = null )
{
effect . SetDuration ( skill . statusDuration ) ;
effect . SetTier ( skill . statusTier ) ;
effect . SetOwner ( target ) ;
if ( target . statusEffects . AddStatusEffect ( effect , caster ) )
{
//If we need an extra action to show the status text
if ( isAdditional )
action . AddStatusAction ( target . actorId , skill . statusId ) ;
}
else
action . worldMasterTextId = 32002 ; //Is this right?
}
else
{
if ( target . statusEffects . AddStatusEffect ( skill . statusId , 1 , 3000 , skill . statusDuration , skill . statusTier ) )
{
//If we need an extra action to show the status text
if ( isAdditional )
action . AddStatusAction ( target . actorId , skill . statusId ) ;
}
else
action . worldMasterTextId = 32002 ; //Is this right?
}
}
}
2017-08-02 23:06:11 +01:00
}
}