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 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. 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: 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 == == 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.) 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; 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() void Mob::SetAttackTimer()
{ {
attack_timer.SetAtTrigger(4000, true); attack_timer.SetAtTrigger(4000, true);
@ -4814,7 +4835,7 @@ void Mob::SetAttackTimer()
void Client::SetAttackTimer() void Client::SetAttackTimer()
{ {
float PermaHaste = GetPermaHaste(); float haste_mod = GetHaste() * 0.01f;
//default value for attack timer in case they have //default value for attack timer in case they have
//an invalid weapon equipped: //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 speed = 0;
int delay = 36;
float quiver_haste = 0.0f;
//if we have no weapon.. //if we have no weapon..
if (ItemToUse == nullptr) { if (ItemToUse == nullptr) {
//above checks ensure ranged weapons do not fall into here //above checks ensure ranged weapons do not fall into here
// Work out if we're a monk // Work out if we're a monk
if ((GetClass() == MONK) || (GetClass() == BEASTLORD)) if (GetClass() == MONK || GetClass() == BEASTLORD)
speed = static_cast<int>((GetMonkHandToHandDelay() * (100 + DelayMod) / 100) * PermaHaste); delay = GetMonkHandToHandDelay();
else
speed = static_cast<int>((36 * (100 + DelayMod) / 100) * PermaHaste);
} else { } else {
//we have a weapon, use its delay //we have a weapon, use its delay
// Convert weapon delay to timer resolution (milliseconds) delay = ItemToUse->Delay;
//delay * 100 if (ItemToUse->ItemType == ItemTypeBow || ItemToUse->ItemType == ItemTypeLargeThrowing)
speed = static_cast<int>((ItemToUse->Delay * (100 + DelayMod) / 100) * PermaHaste); quiver_haste = GetQuiverHaste();
if (ItemToUse->ItemType == ItemTypeBow || ItemToUse->ItemType == ItemTypeLargeThrowing) { }
float quiver_haste = GetQuiverHaste(); speed = static_cast<int>(((delay / haste_mod) + ((hhe / 100.0f) * delay)) * 100);
// this is probably wrong
if (quiver_haste > 0) if (quiver_haste > 0)
speed *= quiver_haste; speed *= quiver_haste;
}
}
TimerToUse->SetAtTrigger(std::max(RuleI(Combat, MinHastedDelay), speed), true); TimerToUse->SetAtTrigger(std::max(RuleI(Combat, MinHastedDelay), speed), true);
if (i == MainPrimary) if (i == MainPrimary)
@ -4910,13 +4930,20 @@ void Client::SetAttackTimer()
void NPC::SetAttackTimer() void NPC::SetAttackTimer()
{ {
float PermaHaste = GetPermaHaste(); float haste_mod = GetHaste() * 0.01f;
//default value for attack timer in case they have //default value for attack timer in case they have
//an invalid weapon equipped: //an invalid weapon equipped:
attack_timer.SetAtTrigger(4000, true); attack_timer.SetAtTrigger(4000, true);
Timer *TimerToUse = nullptr; 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++) { for (int i = MainRange; i <= MainSecondary; i++) {
//pick a timer //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); 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: 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) if (effect_value < 0) //A few spells use negative values(Descriptions all indicate it should be a slow)
effect_value = effect_value * -1; effect_value = effect_value * -1;

View File

@ -8349,11 +8349,7 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) {
if(!ca_time) if(!ca_time)
return; return;
float HasteModifier = 0; float HasteModifier = GetHaste() * 0.01f;
if (GetHaste())
HasteModifier = 10000 / (100 + GetHaste());
else
HasteModifier = 100;
int32 dmg = 0; int32 dmg = 0;
uint16 skill_to_use = -1; uint16 skill_to_use = -1;
@ -8585,7 +8581,7 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) {
TryBackstab(target,reuse); TryBackstab(target,reuse);
} }
classattack_timer.Start(reuse*HasteModifier/100); classattack_timer.Start(reuse / HasteModifier);
} }
bool Bot::TryHeadShot(Mob* defender, SkillUseTypes skillInUse) { bool Bot::TryHeadShot(Mob* defender, SkillUseTypes skillInUse) {
@ -8978,11 +8974,7 @@ int32 Bot::CalcMaxMana() {
} }
void Bot::SetAttackTimer() { void Bot::SetAttackTimer() {
float PermaHaste; float haste_mod = GetHaste() * 0.01f;
if (GetHaste())
PermaHaste = 1 / (1 + (float)GetHaste()/100);
else
PermaHaste = 1.0f;
//default value for attack timer in case they have //default value for attack timer in case they have
//an invalid weapon equipped: //an invalid weapon equipped:
@ -8992,7 +8984,6 @@ void Bot::SetAttackTimer() {
const Item_Struct* PrimaryWeapon = nullptr; const Item_Struct* PrimaryWeapon = nullptr;
for (int i = MainRange; i <= MainSecondary; i++) { for (int i = MainRange; i <= MainSecondary; i++) {
//pick a timer //pick a timer
if (i == MainPrimary) if (i == MainPrimary)
TimerToUse = &attack_timer; TimerToUse = &attack_timer;
@ -9044,64 +9035,22 @@ void Bot::SetAttackTimer() {
} }
} }
int16 DelayMod = itembonuses.HundredHands + spellbonuses.HundredHands; int hhe = std::max(itembonuses.HundredHands + spellbonuses.HundredHands, -99);
if (DelayMod < -99) int speed = 0;
DelayMod = -99; int delay = 36;
//if we have no weapon.. //if we have no weapon..
if (ItemToUse == nullptr) { if (ItemToUse == nullptr) {
//above checks ensure ranged weapons do not fall into here //above checks ensure ranged weapons do not fall into here
// Work out if we're a monk // Work out if we're a monk
if ((GetClass() == MONK) || (GetClass() == BEASTLORD)) { if ((GetClass() == MONK) || (GetClass() == BEASTLORD))
//we are a monk, use special delay delay = GetMonkHandToHandDelay();
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
}
} else { } else {
//we have a weapon, use its delay //we have a weapon, use its delay
// Convert weapon delay to timer resolution (milliseconds) delay = ItemToUse->Delay;
//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);
} }
speed = static_cast<int>(((delay / haste_mod) + ((hhe / 100.0f) * delay)) * 100);
TimerToUse->SetAtTrigger(std::max(RuleI(Combat, MinHastedDelay), speed), true);
if(i == MainPrimary) if(i == MainPrimary)
PrimaryWeapon = ItemToUse; PrimaryWeapon = ItemToUse;

View File

@ -1341,12 +1341,45 @@ int16 Client::CalcCHA() {
return(CHA); return(CHA);
} }
int Client::CalcHaste() { int Client::CalcHaste()
int h = spellbonuses.haste + spellbonuses.hastetype2; {
/* 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 cap = 0;
int overhaste = 0;
int level = GetLevel(); 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 // 26+ no cap, 1-25 10
if (level > 25) // 26+ if (level > 25) // 26+
h += itembonuses.haste; h += itembonuses.haste;
@ -1368,24 +1401,16 @@ int Client::CalcHaste() {
// 51+ 25 (despite there being higher spells...), 1-50 10 // 51+ 25 (despite there being higher spells...), 1-50 10
if (level > 50) // 51+ if (level > 50) // 51+
overhaste = spellbonuses.hastetype3 > 25 ? 25 : spellbonuses.hastetype3; h += spellbonuses.hastetype3 > 25 ? 25 : spellbonuses.hastetype3;
else // 1-50 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 += ExtraHaste; //GM granted haste.
h = mod_client_haste(h); h = mod_client_haste(h);
if (spellbonuses.inhibitmelee) { Haste = 100 + h;
if (h >= 0) return Haste;
h -= spellbonuses.inhibitmelee;
else
h -= ((100 + h) * spellbonuses.inhibitmelee / 100);
}
Haste = h;
return(Haste);
} }
//The AA multipliers are set to be 5, but were 2 on WR //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) if(!ca_time)
return; return;
float HasteModifier = 0; float HasteModifier = GetHaste() * 0.01f;
if(GetHaste() > 0)
HasteModifier = 10000 / (100 + GetHaste());
else if(GetHaste() < 0)
HasteModifier = (100 - GetHaste());
else
HasteModifier = 100;
int level = GetLevel(); int level = GetLevel();
int reuse = TauntReuseTime * 1000; //make this very long since if they dont use it once, they prolly never will 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) 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()); return(zone->GetZoneID());
} }
int Mob::GetHaste() { int Mob::GetHaste()
int h = spellbonuses.haste + spellbonuses.hastetype2; {
// 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 cap = 0;
int overhaste = 0;
int level = GetLevel(); 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 // 26+ no cap, 1-25 10
if (level > 25) // 26+ if (level > 25) // 26+
h += itembonuses.haste; h += itembonuses.haste;
@ -2760,21 +2777,13 @@ int Mob::GetHaste() {
// 51+ 25 (despite there being higher spells...), 1-50 10 // 51+ 25 (despite there being higher spells...), 1-50 10
if (level > 50) // 51+ if (level > 50) // 51+
overhaste = spellbonuses.hastetype3 > 25 ? 25 : spellbonuses.hastetype3; h += spellbonuses.hastetype3 > 25 ? 25 : spellbonuses.hastetype3;
else // 1-50 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 += ExtraHaste; //GM granted haste.
if (spellbonuses.inhibitmelee) { return 100 + h;
if (h >= 0)
h -= spellbonuses.inhibitmelee;
else
h -= ((100 + h) * spellbonuses.inhibitmelee / 100);
}
return(h);
} }
void Mob::SetTarget(Mob* mob) { void Mob::SetTarget(Mob* mob) {

View File

@ -684,7 +684,6 @@ public:
inline bool GetInvul(void) { return invulnerable; } inline bool GetInvul(void) { return invulnerable; }
inline void SetExtraHaste(int Haste) { ExtraHaste = Haste; } inline void SetExtraHaste(int Haste) { ExtraHaste = Haste; }
virtual int GetHaste(); 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); uint8 GetWeaponDamageBonus(const Item_Struct* Weapon);
uint16 GetDamageTable(SkillUseTypes skillinuse); uint16 GetDamageTable(SkillUseTypes skillinuse);

View File

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