Revamp attack delays / hastes / slows based on dev quotes

See changelog
This commit is contained in:
Michael Cook (mackal) 2014-09-27 23:14:11 -04:00
parent e5822a0c4a
commit 3be7d45d36
9 changed files with 152 additions and 160 deletions

View File

@ -7,6 +7,10 @@ Example $mob->GetSpellStat(121, "range"); //Returns spell range
Example $mob->GetSpellStat(121, "effectid", 1); //Returns the the value of effectid1
This will allow you to pull almost all the data for any spell in quest files.
demonstar55: Move the client's SetAttackTimer to the end of Client::CalcBonuses to keep the haste in sync
demonstar55: Correct haste/slow "stacking" rules
demonstar55: Correct SE_AttackSpeed4 to respect unslowable
demonstar55: Make the haste be between 1-225 like the client (<100 = slow, >100 = haste) to ...
demonstar55: Correct Hundred Hands effect and use formula provided by devs
== 09/24/2014 ==
Uleat: Re-ordered server opcodes and handlers to give them some predictability of location (I need this for the inventory re-enumeration.)

View File

@ -4807,6 +4807,27 @@ void Mob::CommonBreakInvisible()
improved_hidden = false;
}
/* Dev quotes:
* Old formula
* Final delay = (Original Delay / (haste mod *.01f)) + ((Hundred Hands / 100) * Original Delay)
* New formula
* Final delay = (Original Delay / (haste mod *.01f)) + ((Hundred Hands / 1000) * (Original Delay / (haste mod *.01f))
* Base Delay 20 25 30 37
* Haste 2.25 2.25 2.25 2.25
* HHE (old) -17 -17 -17 -17
* Final Delay 5.488888889 6.861111111 8.233333333 10.15444444
*
* Base Delay 20 25 30 37
* Haste 2.25 2.25 2.25 2.25
* HHE (new) -383 -383 -383 -383
* Final Delay 5.484444444 6.855555556 8.226666667 10.14622222
*
* Difference -0.004444444 -0.005555556 -0.006666667 -0.008222222
*
* These times are in 10th of a second
* New formula currently unsupported
*/
void Mob::SetAttackTimer()
{
attack_timer.SetAtTrigger(4000, true);
@ -4814,7 +4835,7 @@ void Mob::SetAttackTimer()
void Client::SetAttackTimer()
{
float PermaHaste = GetPermaHaste();
float haste_mod = GetHaste() * 0.01f;
//default value for attack timer in case they have
//an invalid weapon equipped:
@ -4879,28 +4900,27 @@ void Client::SetAttackTimer()
}
}
int16 DelayMod = std::max(itembonuses.HundredHands + spellbonuses.HundredHands, -99);
int hhe = std::max(itembonuses.HundredHands + spellbonuses.HundredHands, -99);
int speed = 0;
int delay = 36;
float quiver_haste = 0.0f;
//if we have no weapon..
if (ItemToUse == nullptr) {
//above checks ensure ranged weapons do not fall into here
// Work out if we're a monk
if ((GetClass() == MONK) || (GetClass() == BEASTLORD))
speed = static_cast<int>((GetMonkHandToHandDelay() * (100 + DelayMod) / 100) * PermaHaste);
else
speed = static_cast<int>((36 * (100 + DelayMod) / 100) * PermaHaste);
if (GetClass() == MONK || GetClass() == BEASTLORD)
delay = GetMonkHandToHandDelay();
} else {
//we have a weapon, use its delay
// Convert weapon delay to timer resolution (milliseconds)
//delay * 100
speed = static_cast<int>((ItemToUse->Delay * (100 + DelayMod) / 100) * PermaHaste);
if (ItemToUse->ItemType == ItemTypeBow || ItemToUse->ItemType == ItemTypeLargeThrowing) {
float quiver_haste = GetQuiverHaste();
if (quiver_haste > 0)
speed *= quiver_haste;
}
delay = ItemToUse->Delay;
if (ItemToUse->ItemType == ItemTypeBow || ItemToUse->ItemType == ItemTypeLargeThrowing)
quiver_haste = GetQuiverHaste();
}
speed = static_cast<int>(((delay / haste_mod) + ((hhe / 100.0f) * delay)) * 100);
// this is probably wrong
if (quiver_haste > 0)
speed *= quiver_haste;
TimerToUse->SetAtTrigger(std::max(RuleI(Combat, MinHastedDelay), speed), true);
if (i == MainPrimary)
@ -4910,13 +4930,20 @@ void Client::SetAttackTimer()
void NPC::SetAttackTimer()
{
float PermaHaste = GetPermaHaste();
float haste_mod = GetHaste() * 0.01f;
//default value for attack timer in case they have
//an invalid weapon equipped:
attack_timer.SetAtTrigger(4000, true);
Timer *TimerToUse = nullptr;
int hhe = std::max(itembonuses.HundredHands + spellbonuses.HundredHands, -99);
// Technically NPCs should do some logic for weapons, but the effect is minimal
// What they do is take the lower of their set delay and the weapon's
// ex. Mob's delay set to 20, weapon set to 19, delay 19
// Mob's delay set to 20, weapon set to 21, delay 20
int speed = static_cast<int>(((attack_delay / haste_mod) + ((hhe / 100.0f) * attack_delay)) * 100);
for (int i = MainRange; i <= MainSecondary; i++) {
//pick a timer
@ -4938,13 +4965,6 @@ void NPC::SetAttackTimer()
}
}
int16 DelayMod = std::max(itembonuses.HundredHands + spellbonuses.HundredHands, -99);
// Technically NPCs should do some logic for weapons, but the effect is minimal
// What they do is take the lower of their set delay and the weapon's
// ex. Mob's delay set to 20, weapon set to 19, delay 19
// Mob's delay set to 20, weapon set to 21, delay 20
int speed = static_cast<int>((attack_delay * (100 + DelayMod) / 100) * PermaHaste);
TimerToUse->SetAtTrigger(std::max(RuleI(Combat, MinHastedDelay), speed), true);
}
}

View File

@ -1546,6 +1546,11 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses* ne
case SE_AttackSpeed4:
{
// These don't generate the IMMUNE_ATKSPEED message and the icon shows up
// but have no effect on the mobs attack speed
if (GetSpecialAbility(UNSLOWABLE))
break;
if (effect_value < 0) //A few spells use negative values(Descriptions all indicate it should be a slow)
effect_value = effect_value * -1;

View File

@ -8349,11 +8349,7 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) {
if(!ca_time)
return;
float HasteModifier = 0;
if (GetHaste())
HasteModifier = 10000 / (100 + GetHaste());
else
HasteModifier = 100;
float HasteModifier = GetHaste() * 0.01f;
int32 dmg = 0;
uint16 skill_to_use = -1;
@ -8585,7 +8581,7 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) {
TryBackstab(target,reuse);
}
classattack_timer.Start(reuse*HasteModifier/100);
classattack_timer.Start(reuse / HasteModifier);
}
bool Bot::TryHeadShot(Mob* defender, SkillUseTypes skillInUse) {
@ -8978,11 +8974,7 @@ int32 Bot::CalcMaxMana() {
}
void Bot::SetAttackTimer() {
float PermaHaste;
if (GetHaste())
PermaHaste = 1 / (1 + (float)GetHaste()/100);
else
PermaHaste = 1.0f;
float haste_mod = GetHaste() * 0.01f;
//default value for attack timer in case they have
//an invalid weapon equipped:
@ -8991,117 +8983,74 @@ void Bot::SetAttackTimer() {
Timer* TimerToUse = nullptr;
const Item_Struct* PrimaryWeapon = nullptr;
for (int i=MainRange; i<=MainSecondary; i++) {
for (int i = MainRange; i <= MainSecondary; i++) {
//pick a timer
if (i == MainPrimary)
TimerToUse = &attack_timer;
else if (i == MainRange)
TimerToUse = &ranged_timer;
else if(i == MainSecondary)
else if (i == MainSecondary)
TimerToUse = &attack_dw_timer;
else //invalid slot (hands will always hit this)
continue;
const Item_Struct* ItemToUse = nullptr;
ItemInst* ci = GetBotItem(i);
if(ci)
if (ci)
ItemToUse = ci->GetItem();
//special offhand stuff
if(i == MainSecondary) {
if (i == MainSecondary) {
//if we have a 2H weapon in our main hand, no dual
if(PrimaryWeapon != nullptr) {
if( PrimaryWeapon->ItemClass == ItemClassCommon
&& (PrimaryWeapon->ItemType == ItemType2HSlash
|| PrimaryWeapon->ItemType == ItemType2HBlunt
|| PrimaryWeapon->ItemType == ItemType2HPiercing)) {
attack_dw_timer.Disable();
continue;
if (PrimaryWeapon != nullptr) {
if (PrimaryWeapon->ItemClass == ItemClassCommon
&& (PrimaryWeapon->ItemType == ItemType2HSlash
|| PrimaryWeapon->ItemType == ItemType2HBlunt
|| PrimaryWeapon->ItemType == ItemType2HPiercing)) {
attack_dw_timer.Disable();
continue;
}
}
//clients must have the skill to use it...
if(!GetSkill(SkillDualWield)) {
if (!GetSkill(SkillDualWield)) {
attack_dw_timer.Disable();
continue;
}
}
//see if we have a valid weapon
if(ItemToUse != nullptr) {
if (ItemToUse != nullptr) {
//check type and damage/delay
if(ItemToUse->ItemClass != ItemClassCommon
|| ItemToUse->Damage == 0
|| ItemToUse->Delay == 0) {
if (ItemToUse->ItemClass != ItemClassCommon
|| ItemToUse->Damage == 0
|| ItemToUse->Delay == 0) {
//no weapon
ItemToUse = nullptr;
ItemToUse = nullptr;
}
// Check to see if skill is valid
else if((ItemToUse->ItemType > ItemTypeLargeThrowing) && (ItemToUse->ItemType != ItemTypeMartial) && (ItemToUse->ItemType != ItemType2HPiercing)) {
else if ((ItemToUse->ItemType > ItemTypeLargeThrowing) && (ItemToUse->ItemType != ItemTypeMartial) && (ItemToUse->ItemType != ItemType2HPiercing)) {
//no weapon
ItemToUse = nullptr;
}
}
int16 DelayMod = itembonuses.HundredHands + spellbonuses.HundredHands;
if (DelayMod < -99)
DelayMod = -99;
int hhe = std::max(itembonuses.HundredHands + spellbonuses.HundredHands, -99);
int speed = 0;
int delay = 36;
//if we have no weapon..
if (ItemToUse == nullptr) {
//above checks ensure ranged weapons do not fall into here
// Work out if we're a monk
if ((GetClass() == MONK) || (GetClass() == BEASTLORD)) {
//we are a monk, use special delay
int speed = (int)( (GetMonkHandToHandDelay()*(100+DelayMod)/100)*(100.0f+attack_speed)*PermaHaste);
// 1200 seemed too much, with delay 10 weapons available
if(speed < RuleI(Combat, MinHastedDelay)) //lower bound
speed = RuleI(Combat, MinHastedDelay);
TimerToUse->SetAtTrigger(speed, true); // Hand to hand, delay based on level or epic
} else {
//not a monk... using fist, regular delay
int speed = (int)((36 *(100+DelayMod)/100)*(100.0f+attack_speed)*PermaHaste);
//if(speed < RuleI(Combat, MinHastedDelay) && IsClient()) //lower bound
// speed = RuleI(Combat, MinHastedDelay);
TimerToUse->SetAtTrigger(speed, true); // Hand to hand, non-monk 2/36
}
if ((GetClass() == MONK) || (GetClass() == BEASTLORD))
delay = GetMonkHandToHandDelay();
} else {
//we have a weapon, use its delay
// Convert weapon delay to timer resolution (milliseconds)
//delay * 100
int speed = (int)((ItemToUse->Delay*(100+DelayMod)/100)*(100.0f+attack_speed)*PermaHaste);
if(speed < RuleI(Combat, MinHastedDelay))
speed = RuleI(Combat, MinHastedDelay);
if(ItemToUse && (ItemToUse->ItemType == ItemTypeBow || ItemToUse->ItemType == ItemTypeLargeThrowing))
{
/*if(IsClient())
{
float max_quiver = 0;
for(int r = SLOT_PERSONAL_BEGIN; r <= SLOT_PERSONAL_END; r++)
{
const ItemInst *pi = CastToClient()->GetInv().GetItem(r);
if(!pi)
continue;
if(pi->IsType(ItemClassContainer) && pi->GetItem()->BagType == bagTypeQuiver)
{
float temp_wr = (pi->GetItem()->BagWR / 3);
if(temp_wr > max_quiver)
{
max_quiver = temp_wr;
}
}
}
if(max_quiver > 0)
{
float quiver_haste = 1 / (1 + max_quiver / 100);
speed *= quiver_haste;
}
}*/
}
TimerToUse->SetAtTrigger(speed, true);
delay = ItemToUse->Delay;
}
speed = static_cast<int>(((delay / haste_mod) + ((hhe / 100.0f) * delay)) * 100);
TimerToUse->SetAtTrigger(std::max(RuleI(Combat, MinHastedDelay), speed), true);
if(i == MainPrimary)
PrimaryWeapon = ItemToUse;

View File

@ -1341,12 +1341,45 @@ int16 Client::CalcCHA() {
return(CHA);
}
int Client::CalcHaste() {
int h = spellbonuses.haste + spellbonuses.hastetype2;
int Client::CalcHaste()
{
/* Tests: (based on results in newer char window)
* 68 v1 + 46 item + 25 over + 35 inhib = 204%
* 46 item + 5 v2 + 25 over + 35 inhib = 65%
* 68 v1 + 46 item + 5 v2 + 25 over + 35 inhib = 209%
* 75% slow + 35 inhib = 25%
* 35 inhib = 65%
* 75% slow = 25%
* Conclusions:
* the bigger effect in slow v. inhib wins
* slow negates all other hastes
* inhib will only negate all other hastes if you don't have v1 (ex. VQ)
*/
// slow beats all! Besides a better inhibit
if (spellbonuses.haste < 0) {
if (-spellbonuses.haste <= spellbonuses.inhibitmelee)
Haste = 100 - spellbonuses.inhibitmelee;
else
Haste = 100 + spellbonuses.haste;
return Haste;
}
// No haste and inhibit, kills all other hastes
if (spellbonuses.haste == 0 && spellbonuses.inhibitmelee) {
Haste = 100 - spellbonuses.inhibitmelee;
return Haste;
}
int h = 0;
int cap = 0;
int overhaste = 0;
int level = GetLevel();
// we know we have a haste spell and not slowed, no extra inhibit melee checks needed
if (spellbonuses.haste)
h += spellbonuses.haste - spellbonuses.inhibitmelee;
if (spellbonuses.hastetype2 && level > 49) // type 2 is capped at 10% and only available to 50+
h += spellbonuses.hastetype2 > 10 ? 10 : spellbonuses.hastetype2;
// 26+ no cap, 1-25 10
if (level > 25) // 26+
h += itembonuses.haste;
@ -1368,24 +1401,16 @@ int Client::CalcHaste() {
// 51+ 25 (despite there being higher spells...), 1-50 10
if (level > 50) // 51+
overhaste = spellbonuses.hastetype3 > 25 ? 25 : spellbonuses.hastetype3;
h += spellbonuses.hastetype3 > 25 ? 25 : spellbonuses.hastetype3;
else // 1-50
overhaste = spellbonuses.hastetype3 > 10 ? 10 : spellbonuses.hastetype3;
h += spellbonuses.hastetype3 > 10 ? 10 : spellbonuses.hastetype3;
h += overhaste;
h += ExtraHaste; //GM granted haste.
h = mod_client_haste(h);
if (spellbonuses.inhibitmelee) {
if (h >= 0)
h -= spellbonuses.inhibitmelee;
else
h -= ((100 + h) * spellbonuses.inhibitmelee / 100);
}
Haste = h;
return(Haste);
Haste = 100 + h;
return Haste;
}
//The AA multipliers are set to be 5, but were 2 on WR

View File

@ -4619,13 +4619,7 @@ void Merc::DoClassAttacks(Mob *target) {
if(!ca_time)
return;
float HasteModifier = 0;
if(GetHaste() > 0)
HasteModifier = 10000 / (100 + GetHaste());
else if(GetHaste() < 0)
HasteModifier = (100 - GetHaste());
else
HasteModifier = 100;
float HasteModifier = GetHaste() * 0.01f;
int level = GetLevel();
int reuse = TauntReuseTime * 1000; //make this very long since if they dont use it once, they prolly never will
@ -4689,7 +4683,7 @@ void Merc::DoClassAttacks(Mob *target) {
}
}
classattack_timer.Start(reuse*HasteModifier/100);
classattack_timer.Start(reuse / HasteModifier);
}
bool Merc::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, bool IsFromSpell, ExtraAttackOptions *opts)

View File

@ -2735,12 +2735,29 @@ uint32 Mob::GetZoneID() const {
return(zone->GetZoneID());
}
int Mob::GetHaste() {
int h = spellbonuses.haste + spellbonuses.hastetype2;
int Mob::GetHaste()
{
// See notes in Client::CalcHaste
// Need to check if the effect of inhibit melee differs for NPCs
if (spellbonuses.haste < 0) {
if (-spellbonuses.haste <= spellbonuses.inhibitmelee)
return 100 - spellbonuses.inhibitmelee;
else
return 100 + spellbonuses.haste;
}
if (spellbonuses.haste == 0 && spellbonuses.inhibitmelee)
return 100 - spellbonuses.inhibitmelee;
int h = 0;
int cap = 0;
int overhaste = 0;
int level = GetLevel();
if (spellbonuses.haste)
h += spellbonuses.haste - spellbonuses.inhibitmelee;
if (spellbonuses.hastetype2 && level > 49)
h += spellbonuses.hastetype2 > 10 ? 10 : spellbonuses.hastetype2;
// 26+ no cap, 1-25 10
if (level > 25) // 26+
h += itembonuses.haste;
@ -2760,21 +2777,13 @@ int Mob::GetHaste() {
// 51+ 25 (despite there being higher spells...), 1-50 10
if (level > 50) // 51+
overhaste = spellbonuses.hastetype3 > 25 ? 25 : spellbonuses.hastetype3;
h += spellbonuses.hastetype3 > 25 ? 25 : spellbonuses.hastetype3;
else // 1-50
overhaste = spellbonuses.hastetype3 > 10 ? 10 : spellbonuses.hastetype3;
h += spellbonuses.hastetype3 > 10 ? 10 : spellbonuses.hastetype3;
h += overhaste;
h += ExtraHaste; //GM granted haste.
if (spellbonuses.inhibitmelee) {
if (h >= 0)
h -= spellbonuses.inhibitmelee;
else
h -= ((100 + h) * spellbonuses.inhibitmelee / 100);
}
return(h);
return 100 + h;
}
void Mob::SetTarget(Mob* mob) {

View File

@ -684,7 +684,6 @@ public:
inline bool GetInvul(void) { return invulnerable; }
inline void SetExtraHaste(int Haste) { ExtraHaste = Haste; }
virtual int GetHaste();
inline float GetPermaHaste() { return GetHaste() ? 100.0f / (1.0f + static_cast<float>(GetHaste()) / 100.0f) : 100.0f; }
uint8 GetWeaponDamageBonus(const Item_Struct* Weapon);
uint16 GetDamageTable(SkillUseTypes skillinuse);

View File

@ -1412,11 +1412,7 @@ void NPC::DoClassAttacks(Mob *target) {
if(!ca_time)
return;
float HasteModifier = 0;
if (GetHaste())
HasteModifier = 10000 / (100 + GetHaste());
else
HasteModifier = 100;
float HasteModifier = GetHaste() * 0.01f;
int level = GetLevel();
int reuse = TauntReuseTime * 1000; //make this very long since if they dont use it once, they prolly never will
@ -1568,7 +1564,7 @@ void NPC::DoClassAttacks(Mob *target) {
}
}
classattack_timer.Start(reuse*HasteModifier/100);
classattack_timer.Start(reuse / HasteModifier);
}
void Client::DoClassAttacks(Mob *ca_target, uint16 skill, bool IsRiposte)
@ -1592,15 +1588,8 @@ void Client::DoClassAttacks(Mob *ca_target, uint16 skill, bool IsRiposte)
}
int ReuseTime = 0;
int ClientHaste = GetHaste();
int HasteMod = 0;
float HasteMod = GetHaste() * 0.01f;
if(ClientHaste >= 0){
HasteMod = (10000/(100+ClientHaste)); //+100% haste = 2x as many attacks
}
else{
HasteMod = (100-ClientHaste); //-100% haste = 1/2 as many attacks
}
int32 dmg = 0;
uint16 skill_to_use = -1;
@ -1677,8 +1666,7 @@ void Client::DoClassAttacks(Mob *ca_target, uint16 skill, bool IsRiposte)
}
}
ReuseTime = BashReuseTime-1;
ReuseTime = (ReuseTime*HasteMod)/100;
ReuseTime = (BashReuseTime - 1) / HasteMod;
DoSpecialAttackDamage(ca_target, SkillBash, dmg, 1,-1,ReuseTime);
@ -1705,8 +1693,7 @@ void Client::DoClassAttacks(Mob *ca_target, uint16 skill, bool IsRiposte)
if (min_dmg > max_dmg)
max_dmg = min_dmg;
ReuseTime = FrenzyReuseTime-1;
ReuseTime = (ReuseTime*HasteMod)/100;
ReuseTime = (FrenzyReuseTime - 1) / HasteMod;
//Live parses show around 55% Triple 35% Double 10% Single, you will always get first hit.
while(AtkRounds > 0) {
@ -1781,7 +1768,7 @@ void Client::DoClassAttacks(Mob *ca_target, uint16 skill, bool IsRiposte)
TryBackstab(ca_target,ReuseTime);
}
ReuseTime = (ReuseTime*HasteMod)/100;
ReuseTime = ReuseTime / HasteMod;
if(ReuseTime > 0 && !IsRiposte){
p_timers.Start(pTimerCombatAbility, ReuseTime);
}