Compare commits

...
Sign in to create a new pull request.

32 commits

Author SHA1 Message Date
Michael Cook (mackal)
3607f2102c Merge branch 'master' into spell_cache 2018-02-04 18:38:13 -05:00
Michael Cook (mackal)
a9b3b02819 Merge branch 'master' into spell_cache 2018-02-02 21:08:06 -05:00
Michael Cook (mackal)
fc93429358 Merge branch 'master' into spell_cache 2018-01-20 22:54:26 -05:00
Michael Cook (mackal)
66b1a9d531 Merge branch 'master' into spell_cache 2018-01-10 00:57:24 -05:00
Michael Cook (mackal)
62f2c346c3 Merge branch 'master' into spell_cache 2017-12-23 23:05:57 -05:00
Michael Cook (mackal)
aded4250a5 Merge branch 'master' into spell_cache 2017-12-16 21:33:02 -05:00
Michael Cook (mackal)
c49d1aa6d6 Merge branch 'master' into spell_cache 2017-12-11 17:10:38 -05:00
Michael Cook (mackal)
37d7ab2de8 Merge branch 'master' into spell_cache 2017-11-17 00:34:41 -05:00
Michael Cook (mackal)
a92a7e97e1 Work on proc on death 2017-11-17 00:34:13 -05:00
Michael Cook (mackal)
7d7fa32d6c Default! 2017-11-15 00:12:39 -05:00
Michael Cook (mackal)
98c9f9b835 Convert to switches for easier maintenance 2017-11-15 00:09:48 -05:00
Michael Cook (mackal)
cfb270a1dd Slay undead 2017-11-14 22:15:11 -05:00
Michael Cook (mackal)
4b53fd775c Add minified StatBonus that will be used in the new system 2017-11-14 18:14:15 -05:00
Michael Cook (mackal)
eb7d6669ef Spell crit 2017-11-14 16:44:28 -05:00
Michael Cook (mackal)
44dfbc4e0b Fix Divine Save 2017-11-14 16:42:54 -05:00
Michael Cook (mackal)
69fbe24766 Headshot 2017-11-14 16:35:45 -05:00
Michael Cook (mackal)
ef50f54462 Finishing Blow 2017-11-14 16:18:03 -05:00
Michael Cook (mackal)
76b956debf Assassinate 2017-11-14 16:07:55 -05:00
Michael Cook (mackal)
be97d2ac9e Add handling for SE_DivineSave 2017-11-14 15:50:52 -05:00
Michael Cook (mackal)
e0b2867e8b Add caching for SE_SkillProc and SE_SkillProcSuccess 2017-11-13 22:54:06 -05:00
Michael Cook (mackal)
406cb825c3 Rename SE_SkillAttackProc to match our name 2017-11-13 00:20:02 -05:00
Michael Cook (mackal)
b424fc9815 Fix AA SE_NegateSpellEffects 2017-11-12 13:30:12 -05:00
Michael Cook (mackal)
b609f9363b Add handling for SE_SkillAttackProc 2017-11-11 22:25:58 -05:00
Michael Cook (mackal)
7d79d2fc4a More work (mostly AAs) 2017-11-11 14:09:20 -05:00
Michael Cook (mackal)
85feb4f653 Merge branch 'master' into spell_cache 2017-11-10 00:07:12 -05:00
Michael Cook (mackal)
5bca6cedef Merge branch 'master' into spell_cache 2017-11-10 00:05:57 -05:00
Michael Cook (mackal)
ec9c7baef7 Rest of haste stuff
This is still untested
2017-11-09 22:34:10 -05:00
Michael Cook (mackal)
86f1e48596 Merge branch 'master' into spell_cache 2017-11-09 13:32:22 -05:00
Michael Cook (mackal)
61241e3245 More work on gross haste special case 2017-11-09 13:30:11 -05:00
Michael Cook (mackal)
2176eb5e8e Remove some unneeded code 2017-11-08 13:15:37 -05:00
Michael Cook (mackal)
4b44461c70 Work on spell cache functions
Implement Suppression Effects and start work on TotalEffects
2017-11-07 21:46:17 -05:00
Michael Cook (mackal)
a60221393e Spell Cache WIP 2017-11-07 21:45:35 -05:00
22 changed files with 1259 additions and 562 deletions

View file

@ -27,7 +27,7 @@ std::string EQEmuConfig::ConfigFile = "eqemu_config.json";
EQEmuConfig *EQEmuConfig::_config = nullptr;
void EQEmuConfig::parse_config() {
ShortName = _root["server"]["world"].get("shortname", "").asString();
LongName = _root["server"]["world"].get("longname", "").asString();
WorldAddress = _root["server"]["world"].get("address", "").asString();
@ -63,15 +63,15 @@ void EQEmuConfig::parse_config() {
loginlist.Insert(loginconfig);
} while (LoginCount < 100);
}
//<locked> from xml converts to json as locked: "", so i default to "false".
//<locked> from xml converts to json as locked: "", so i default to "false".
//The only way to enable locked is by switching to true, meaning this value is always false until manually set true
Locked = false;
if (_root["server"]["world"].get("locked", "false").asString() == "true") Locked = true;
WorldIP = _root["server"]["world"]["tcp"].get("host", "127.0.0.1").asString();
WorldTCPPort = atoi(_root["server"]["world"]["tcp"].get("port", "9000").asString().c_str());
TelnetIP = _root["server"]["world"]["telnet"].get("ip", "127.0.0.1").asString();
TelnetTCPPort = atoi(_root["server"]["world"]["telnet"].get("port", "9001").asString().c_str());
TelnetEnabled = false;
@ -92,7 +92,7 @@ void EQEmuConfig::parse_config() {
DatabasePassword = _root["server"]["database"].get("password", "eq").asString();
DatabaseHost = _root["server"]["database"].get("host", "localhost").asString();
DatabasePort = atoi(_root["server"]["database"].get("port", "3306").asString().c_str());
DatabaseDB = _root["server"]["database"].get("db", "eq").asString();
DatabaseDB = _root["server"]["database"].get("db", "eq").asString();
QSDatabaseHost = _root["server"]["qsdatabase"].get("host", "localhost").asString();
QSDatabasePort = atoi(_root["server"]["qsdatabase"].get("port", "3306").asString().c_str());

View file

@ -122,7 +122,7 @@ class EQEmuConfig
void parse_config();
EQEmuConfig()
{
{
}
virtual ~EQEmuConfig() {}
@ -166,7 +166,7 @@ class EQEmuConfig
}
catch (std::exception) {
return false;
}
}
return true;
}

View file

@ -400,6 +400,7 @@ RULE_BOOL(Spells, IgnoreSpellDmgLvlRestriction, false) // ignore the 5 level spr
RULE_BOOL(Spells, AllowItemTGB, false) // TGB doesn't work with items on live, custom servers want it though
RULE_BOOL(Spells, NPCInnateProcOverride, true) // NPC innate procs override the target type to single target.
RULE_BOOL(Spells, OldRainTargets, false) // use old incorrectly implemented max targets for rains
RULE_BOOL(Spells, Oct182017AAResistOtherSPAFix, false) // they were not saving the base2 of AAs before this patch
RULE_CATEGORY_END()
RULE_CATEGORY(Combat)

View file

@ -215,6 +215,13 @@ typedef enum {
DS_THORNS = 249
} DmgShieldType;
enum SuppressionMask {
SM_Buffs = 1,
SM_Items = 2,
SM_AAs = 4,
SM_All = 255
};
//Spell Effect IDs
// https://forums.daybreakgames.com/eq/index.php?threads/enumerated-spa-list.206288/
// mirror: http://pastebin.com/MYeQqGwe
@ -534,7 +541,7 @@ typedef enum {
#define SE_ForageAdditionalItems 313 // implemented[AA] - chance to forage additional items
#define SE_Invisibility2 314 // implemented - fixed duration invisible
#define SE_InvisVsUndead2 315 // implemented - fixed duration ITU
//#define SE_ImprovedInvisAnimals 316 // not used
#define SE_ImprovedInvisAnimals 316 // not used
#define SE_ItemHPRegenCapIncrease 317 // implemented[AA] - increases amount of health regen gained via items
#define SE_ItemManaRegenCapIncrease 318 // implemented - increases amount of mana regen you can gain via items
#define SE_CriticalHealOverTime 319 // implemented

File diff suppressed because it is too large Load diff

View file

@ -112,6 +112,7 @@ SET(zone_sources
spawn2.h
spawngroup.cpp
special_attacks.cpp
spell_cache.cpp
spell_effects.cpp
spells.cpp
tasks.cpp
@ -217,6 +218,7 @@ SET(zone_headers
spawn2.cpp
spawn2.h
spawngroup.h
spell_cache.h
string_ids.h
tasks.h
titles.h

View file

@ -486,6 +486,8 @@ void Client::ResetAA() {
m_pp.group_leadership_exp = 0;
m_pp.raid_leadership_exp = 0;
m_spell_cache.SetAltCached(false);
database.DeleteCharacterLeadershipAAs(CharacterID());
// undefined for these clients
if (ClientVersionBit() & EQEmu::versions::bit_TitaniumAndEarlier)
@ -801,6 +803,7 @@ void Client::RefundAA() {
Save();
}
m_spell_cache.SetAltCached(false);
SendAlternateAdvancementTable();
SendAlternateAdvancementPoints();
SendAlternateAdvancementStats();
@ -1115,6 +1118,7 @@ void Client::FinishAlternateAdvancementPurchase(AA::Rank *rank, bool ignore_cost
}
}
m_spell_cache.SetAltCached(false);
CalcBonuses();
if(cost > 0) {

View file

@ -1517,12 +1517,14 @@ bool Client::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, b
///////////////////////////////////////////////////////////
////// Send Attack Damage
///////////////////////////////////////////////////////////
if (my_hit.damage_done > 0 && aabonuses.SkillAttackProc[0] && aabonuses.SkillAttackProc[1] == my_hit.skill &&
IsValidSpell(aabonuses.SkillAttackProc[2])) {
float chance = aabonuses.SkillAttackProc[0] / 1000.0f;
if (zone->random.Roll(chance))
SpellFinished(aabonuses.SkillAttackProc[2], other, EQEmu::CastingSlot::Item, 0, -1,
spells[aabonuses.SkillAttackProc[2]].ResistDiff);
if (my_hit.damage_done > 0 && m_spell_cache.HasSkillAttackProcs()) {
auto end = m_spell_cache.skill_attack_proc_end();
for (auto it = m_spell_cache.skill_attack_proc_begin(); it != end; ++it) {
if (it->skill == my_hit.skill && IsValidSpell(it->spell)) {
if (zone->random.Roll(it->chance / 1000.0f))
SpellFinished(it->spell, other, EQEmu::CastingSlot::Item, 0, -1, spells[it->spell].ResistDiff);
}
}
}
other->Damage(this, my_hit.damage_done, SPELL_UNKNOWN, my_hit.skill, true, -1, false, m_specialattacks);
@ -4226,12 +4228,11 @@ void Mob::TryCriticalHit(Mob *defender, DamageHitInfo &hit, ExtraAttackOptions *
// 1: Try Slay Undead
if (defender->GetBodyType() == BT_Undead || defender->GetBodyType() == BT_SummonedUndead ||
defender->GetBodyType() == BT_Vampire) {
int SlayRateBonus = aabonuses.SlayUndead[0] + itembonuses.SlayUndead[0] + spellbonuses.SlayUndead[0];
int SlayRateBonus = TotalEffect(SE_SlayUndead, 0);
if (SlayRateBonus) {
float slayChance = static_cast<float>(SlayRateBonus) / 10000.0f;
if (zone->random.Roll(slayChance)) {
int SlayDmgBonus = std::max(
{ aabonuses.SlayUndead[1], itembonuses.SlayUndead[1], spellbonuses.SlayUndead[1] });
int SlayDmgBonus = BestEffect(SE_SlayUndead, 1);
hit.damage_done = std::max(hit.damage_done, hit.base_damage) + 5;
hit.damage_done = (hit.damage_done * SlayDmgBonus) / 100;
@ -4402,20 +4403,12 @@ bool Mob::TryFinishingBlow(Mob *defender, int &damage)
{
// base2 of FinishingBlowLvl is the HP limit (cur / max) * 1000, 10% is listed as 100
if (defender && !defender->IsClient() && defender->GetHPRatio() < 10) {
int FB_Dmg = TotalEffect(SE_FinishingBlow, 1);
uint32 FB_Dmg =
aabonuses.FinishingBlow[1] + spellbonuses.FinishingBlow[1] + itembonuses.FinishingBlow[1];
uint32 FB_Level = 0;
FB_Level = aabonuses.FinishingBlowLvl[0];
if (FB_Level < spellbonuses.FinishingBlowLvl[0])
FB_Level = spellbonuses.FinishingBlowLvl[0];
else if (FB_Level < itembonuses.FinishingBlowLvl[0])
FB_Level = itembonuses.FinishingBlowLvl[0];
int FB_Level = BestEffect(SE_FinishingBlowLvl, 0);
// modern AA description says rank 1 (500) is 50% chance
int ProcChance =
aabonuses.FinishingBlow[0] + spellbonuses.FinishingBlow[0] + spellbonuses.FinishingBlow[0];
int ProcChance = TotalEffect(SE_FinishingBlow, 0);
if (FB_Level && FB_Dmg && (defender->GetLevel() <= FB_Level) &&
(ProcChance >= zone->random.Int(1, 1000))) {
@ -4699,177 +4692,64 @@ void Mob::ApplyDamageTable(DamageHitInfo &hit)
void Mob::TrySkillProc(Mob *on, uint16 skill, uint16 ReuseTime, bool Success, uint16 hand, bool IsDefensive)
{
if (!on) {
SetTarget(nullptr);
Log(Logs::General, Logs::Error, "A null Mob object was passed to Mob::TrySkillProc for evaluation!");
return;
}
if (!spellbonuses.LimitToSkill[skill] && !itembonuses.LimitToSkill[skill] && !aabonuses.LimitToSkill[skill])
return;
/*Allow one proc from each (Spell/Item/AA)
Kayen: Due to limited avialability of effects on live it is too difficult
to confirm how they stack at this time, will adjust formula when more data is avialablle to test.*/
bool CanProc = true;
Kayen: Due to limited availability of effects on live it is too difficult
to confirm how they stack at this time, will adjust formula when more data is available to test.*/
uint16 base_spell_id = 0;
uint16 proc_spell_id = 0;
float ProcMod = 0;
float chance = 0;
float ProcMod = 0.0f;
float chance = 0.0f;
if (IsDefensive)
chance = on->GetSkillProcChances(ReuseTime, hand);
else
chance = GetSkillProcChances(ReuseTime, hand);
if (spellbonuses.LimitToSkill[skill]) {
std::vector<SpellCache::sSkillProc>::const_iterator it, end;
auto type = SpellCache::eSkillProc::Buff;
int i = 0;
for (int e = 0; e < MAX_SKILL_PROCS; e++) {
if (CanProc &&
((!Success && spellbonuses.SkillProc[e] && IsValidSpell(spellbonuses.SkillProc[e]))
|| (Success && spellbonuses.SkillProcSuccess[e] && IsValidSpell(spellbonuses.SkillProcSuccess[e])))) {
while (i < 3) {
if (Success) {
it = m_spell_cache.skill_proc_success_begin(type);
end = m_spell_cache.skill_proc_success_end(type);
} else {
it = m_spell_cache.skill_proc_attempt_begin(type);
end = m_spell_cache.skill_proc_attempt_end(type);
}
if (Success)
base_spell_id = spellbonuses.SkillProcSuccess[e];
else
base_spell_id = spellbonuses.SkillProc[e];
proc_spell_id = 0;
ProcMod = 0;
for (int i = 0; i < EFFECT_COUNT; i++) {
if (spells[base_spell_id].effectid[i] == SE_SkillProc || spells[base_spell_id].effectid[i] == SE_SkillProcSuccess) {
proc_spell_id = spells[base_spell_id].base[i];
ProcMod = static_cast<float>(spells[base_spell_id].base2[i]);
}
else if (spells[base_spell_id].effectid[i] == SE_LimitToSkill && spells[base_spell_id].base[i] <= EQEmu::skills::HIGHEST_SKILL) {
if (CanProc && spells[base_spell_id].base[i] == skill && IsValidSpell(proc_spell_id)) {
float final_chance = chance * (ProcMod / 100.0f);
if (zone->random.Roll(final_chance)) {
ExecWeaponProc(nullptr, proc_spell_id, on);
CheckNumHitsRemaining(NumHit::OffensiveSpellProcs, 0,
base_spell_id);
CanProc = false;
break;
}
}
}
else {
//Reset and check for proc in sequence
proc_spell_id = 0;
ProcMod = 0;
}
for (; it != end; ++it) {
// abilities that have no skill limits can always proc? (see Lingering Death, Shrouding Speed Discipline)
if ((it->skills.empty() ||
it->skills.count(static_cast<EQEmu::skills::SkillType>(skill)) != 0) &&
IsValidSpell(it->spell)) {
ProcMod = static_cast<float>(it->chance);
float final_chance = chance * (ProcMod / 100.0f);
if (zone->random.Roll(final_chance)) {
ExecWeaponProc(nullptr, it->spell, on);
if (type == SpellCache::eSkillProc::Buff)
CheckNumHitsRemaining(NumHit::OffensiveSpellProcs, 0,
buffs[it->slot].spellid);
return; // do we want to allow others to have a chance?
}
}
}
}
if (itembonuses.LimitToSkill[skill]) {
CanProc = true;
for (int e = 0; e < MAX_SKILL_PROCS; e++) {
if (CanProc &&
((!Success && itembonuses.SkillProc[e] && IsValidSpell(itembonuses.SkillProc[e]))
|| (Success && itembonuses.SkillProcSuccess[e] && IsValidSpell(itembonuses.SkillProcSuccess[e])))) {
if (Success)
base_spell_id = itembonuses.SkillProcSuccess[e];
else
base_spell_id = itembonuses.SkillProc[e];
proc_spell_id = 0;
ProcMod = 0;
for (int i = 0; i < EFFECT_COUNT; i++) {
if (spells[base_spell_id].effectid[i] == SE_SkillProc || spells[base_spell_id].effectid[i] == SE_SkillProcSuccess) {
proc_spell_id = spells[base_spell_id].base[i];
ProcMod = static_cast<float>(spells[base_spell_id].base2[i]);
}
else if (spells[base_spell_id].effectid[i] == SE_LimitToSkill && spells[base_spell_id].base[i] <= EQEmu::skills::HIGHEST_SKILL) {
if (CanProc && spells[base_spell_id].base[i] == skill && IsValidSpell(proc_spell_id)) {
float final_chance = chance * (ProcMod / 100.0f);
if (zone->random.Roll(final_chance)) {
ExecWeaponProc(nullptr, proc_spell_id, on);
CanProc = false;
break;
}
}
}
else {
proc_spell_id = 0;
ProcMod = 0;
}
}
}
}
}
if (IsClient() && aabonuses.LimitToSkill[skill]) {
CanProc = true;
uint32 effect_id = 0;
int32 base1 = 0;
int32 base2 = 0;
uint32 slot = 0;
for (int e = 0; e < MAX_SKILL_PROCS; e++) {
if (CanProc &&
((!Success && aabonuses.SkillProc[e])
|| (Success && aabonuses.SkillProcSuccess[e]))) {
int aaid = 0;
if (Success)
base_spell_id = aabonuses.SkillProcSuccess[e];
else
base_spell_id = aabonuses.SkillProc[e];
proc_spell_id = 0;
ProcMod = 0;
for (auto &rank_info : aa_ranks) {
auto ability_rank = zone->GetAlternateAdvancementAbilityAndRank(rank_info.first, rank_info.second.first);
auto ability = ability_rank.first;
auto rank = ability_rank.second;
if (!ability) {
continue;
}
for (auto &effect : rank->effects) {
effect_id = effect.effect_id;
base1 = effect.base1;
base2 = effect.base2;
slot = effect.slot;
if (effect_id == SE_SkillProc || effect_id == SE_SkillProcSuccess) {
proc_spell_id = base1;
ProcMod = static_cast<float>(base2);
}
else if (effect_id == SE_LimitToSkill && base1 <= EQEmu::skills::HIGHEST_SKILL) {
if (CanProc && base1 == skill && IsValidSpell(proc_spell_id)) {
float final_chance = chance * (ProcMod / 100.0f);
if (zone->random.Roll(final_chance)) {
ExecWeaponProc(nullptr, proc_spell_id, on);
CanProc = false;
break;
}
}
}
else {
proc_spell_id = 0;
ProcMod = 0;
}
}
}
}
++i;
switch (i) {
case 1:
type = SpellCache::eSkillProc::Worn;
break;
case 2:
type = SpellCache::eSkillProc::AA;
break;
default:
break;
}
}
}

View file

@ -628,7 +628,9 @@ void Client::CalcEdibleBonuses(StatBonuses* newbon) {
void Mob::CalcAABonuses(StatBonuses *newbon)
{
memset(newbon, 0, sizeof(StatBonuses)); // start fresh
m_spell_cache.ClearAltEffect();
RecacheSuppressionSpells(); // we gotta do this :P
for (const auto &aa : aa_ranks) {
auto ability_rank = zone->GetAlternateAdvancementAbilityAndRank(aa.first, aa.second.first);
@ -645,6 +647,8 @@ void Mob::CalcAABonuses(StatBonuses *newbon)
ApplyAABonuses(*rank, newbon);
}
m_spell_cache.SetAltCached(true);
}
//A lot of the normal spell functions (IsBlankSpellEffect, etc) are set for just spells (in common/spdat.h).
@ -660,17 +664,19 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon)
int32 base2 = 0; // only really used for SE_RaiseStatCap & SE_ReduceSkillTimer in aa_effects table
uint32 slot = 0;
int last_skill_proc = 0; // some book keeping for SE_SkillProc and SE_SkillProcSuccess for SE_LimitToSkill
for (const auto &e : rank.effects) {
effect = e.effect_id;
base1 = e.base1;
base2 = e.base2;
base2 = 0; // only some AAs use the base2
slot = e.slot;
// we default to 0 (SE_CurrentHP) for the effect, so if there aren't any base1/2 values, we'll just skip it
if (effect == 0 && base1 == 0 && base2 == 0)
continue;
// IsBlankSpellEffect()
// IsBlankSpellEffect() -- these shouldn't be in AAs
if (effect == SE_Blank || (effect == SE_CHA && base1 == 0) || effect == SE_StackingCommand_Block ||
effect == SE_StackingCommand_Overwrite)
continue;
@ -678,12 +684,116 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon)
Log(Logs::Detail, Logs::AA, "Applying Effect %d from AA %u in slot %d (base1: %d, base2: %d) on %s",
effect, rank.id, slot, base1, base2, GetCleanName());
// this should be removed when rewritten
uint8 focus = IsFocusEffect(0, 0, true, effect);
if (focus) {
newbon->FocusEffects[focus] = static_cast<uint8>(effect);
continue;
}
// SE_NegateSpellEffects
if (!(GetSuppresionSpellMask(effect) & SM_AAs))
continue;
/* TODO: Other SPAs that need special handling
* SE_ProcOnKillShot
* SE_SpellOnDeath
*/
// special handling for AAs that live doesn't appear to do
// Alternative to this would be to just iterate over the AA list
// each of these are special and we skip below
switch (effect) {
case SE_SkillAttackProc:
m_spell_cache.InsertSkillAttackProc(base1, e.base2, rank.spell);
continue;
case SE_SkillProc:
last_skill_proc = effect;
m_spell_cache.InsertSkillProcAttempt(SpellCache::eSkillProc::AA, base1, e.base2);
continue;
case SE_SkillProcSuccess:
last_skill_proc = effect;
m_spell_cache.InsertSkillProcSuccess(SpellCache::eSkillProc::AA, base1, e.base2);
continue;
case SE_LimitToSkill:
m_spell_cache.InsertSkillLimit(SpellCache::eSkillProc::AA, static_cast<EQEmu::skills::SkillType>(base1), last_skill_proc == SE_SkillProcSuccess);
continue;
default:
break;
}
// Okay, effects that use base2
switch (effect) {
case SE_SpellEffectResistChance:
if (!RuleB(Spells, Oct182017AAResistOtherSPAFix)) // this is broken before this patch
break;
case SE_Levitate:
case SE_CriticalHitChance:
case SE_MeleeSkillCheck:
case SE_HitChance:
case SE_DamageModifier:
case SE_MinDamageModifier:
case SE_SkillDamageTaken:
case SE_Accuracy:
case SE_SkillDamageAmount:
case SE_GiveDoubleRiposte:
case SE_ReduceSkillTimer:
case SE_RaiseSkillCap:
case SE_AddSingingMod:
case SE_RaiseStatCap:
case SE_HastenedAASkill:
case SE_AddPetCommand:
case SE_ReduceTradeskillFail:
case SE_CriticalDamageMob:
case SE_SkillDamageAmount2:
base2 = e.base2;
break;
default:
break;
}
// these don't stack and some treat base2 differently and don't stack
switch (effect) {
case SE_MasteryofPast:
case SE_Assassinate:
case SE_AssassinateLevel:
case SE_HeadShot:
case SE_HeadShotLevel:
case SE_FinishingBlowLvl:
case SE_FinishingBlow:
case SE_DivineSave: // non stacking
if (base1 > m_spell_cache.GetCachedAltEffect(effect))
m_spell_cache.InsertAltEffect(effect, base1, 0);
break;
default: //stacking
m_spell_cache.InsertAltEffect(effect, base1 + m_spell_cache.GetCachedAltEffect(effect), base2);
break;
}
// RoF2 doesn't have SE_CriticalSpellChance here, but our AA data looks like it's processed like this
// client doesn't handle SE_DivineSave like this
// we support older Frenzied Devastation SPA too, this was revamped on live at some point
// SE_SlayUndead isn't done here on client, but it should work for how it works
switch (effect) {
case SE_Assassinate:
case SE_HeadShot:
case SE_FinishingBlow:
case SE_AssassinateLevel:
case SE_HeadShotLevel:
case SE_CriticalSpellChance:
case SE_DivineSave:
case SE_FrenziedDevastation:
case SE_SlayUndead:
case SE_PC_Pet_Rampage:
base2 = e.base2; // uses base2 for fun times!
if (base2 > m_spell_cache.GetCachedAltEffect(effect, 1))
m_spell_cache.InsertAltEffect(effect, base2, 1);
break;
default:
break;
}
// TODO remove
switch (effect) {
case SE_ACv2:
case SE_ArmorClass:

View file

@ -437,6 +437,10 @@ public:
void EnableAreaRegens(int value);
void DisableAreaRegens();
// new spell cache stuff
virtual void RecacheItemEffects();
virtual void RecacheSuppressionItems(); // work around for NPC invs being dumb
void ServerFilter(SetServerFilter_Struct* filter);
void BulkSendTraderInventory(uint32 char_id);
void SendSingleTraderItem(uint32 char_id, int uniqueid);

View file

@ -286,6 +286,61 @@ struct Buffs_Struct {
bool UpdateClient;
};
// we will use this for item stat bonus now only rather then spell effects
// TODO: rename when other is removed
struct StatBonuses2 {
int STR;
int STA;
int CHA;
int DEX;
int INT;
int AGI;
int WIS;
int MR;
int CR;
int FR;
int PR;
int DR;
int Corrup;
int HP;
int Mana;
int Endurance;
int AC;
int ProcChance; // ProcChance/10 == % increase i = CombatEffects
int MeleeMitigation; //i = Shielding
int SpellShield;
int AvoidMeleeChance; //AvoidMeleeChance/10 == % chance i = Avoidance (item mod)
int HitChance; //HitChance/15 == % increase i = Accuracy (Item: Accuracy)
int StunResist; //i
int DoTShielding;
int DSMitigation; // Item Effect
int HeroicSTR;
int HeroicINT;
int HeroicWIS;
int HeroicAGI;
int HeroicDEX;
int HeroicSTA;
int HeroicCHA;
int HeroicMR;
int HeroicFR;
int HeroicCR;
int HeroicDR;
int HeroicPR;
int HeroicCorrup;
int HealAmt; // Item Effect
int SpellDmg; // Item Effect
int Clairvoyance; // Item Effect
int StrikeThrough; // PoP: Strike Through %
int ATK;
int HPRegen;
int ManaRegen;
int EnduranceRegen;
int DamageShield; // this is damage done to mobs that attack this
int PotionBeltSlots;
int SkillMinDamageMod[9];
int haste;
};
struct StatBonuses {
int32 AC;
int32 HP;

View file

@ -62,8 +62,8 @@ int32 Mob::GetActSpellDamage(uint16 spell_id, int32 value, Mob* target) {
value -= GetAA(aaUnholyTouch) * 450; //Unholy Touch
chance = RuleI(Spells, BaseCritChance); //Wizard base critical chance is 2% (Does not scale with level)
chance += itembonuses.CriticalSpellChance + spellbonuses.CriticalSpellChance + aabonuses.CriticalSpellChance;
chance += itembonuses.FrenziedDevastation + spellbonuses.FrenziedDevastation + aabonuses.FrenziedDevastation;
chance += TotalEffect(SE_CriticalSpellChance, 0);
chance += TotalEffect(SE_FrenziedDevastation, 1);
//Crtical Hit Calculation pathway
if (chance > 0 || (IsClient() && GetClass() == WIZARD && GetLevel() >= RuleI(Spells, WizCritLevel))) {
@ -79,8 +79,8 @@ int32 Mob::GetActSpellDamage(uint16 spell_id, int32 value, Mob* target) {
if (zone->random.Roll(chance)) {
Critical = true;
ratio += itembonuses.SpellCritDmgIncrease + spellbonuses.SpellCritDmgIncrease + aabonuses.SpellCritDmgIncrease;
ratio += itembonuses.SpellCritDmgIncNoStack + spellbonuses.SpellCritDmgIncNoStack + aabonuses.SpellCritDmgIncNoStack;
ratio += TotalEffect(SE_SpellCritDmgIncrease);
ratio += TotalEffect(SE_CriticalSpellChance, 1);
}
else if ((IsClient() && GetClass() == WIZARD) || (IsMerc() && GetClass() == CASTERDPS)) {

View file

@ -116,6 +116,7 @@ Mob::Mob(const char* in_name,
fix_z_timer_engaged(100),
attack_anim_timer(1000),
position_update_melee_push_timer(1000),
bItemDirty(false),
mHateListCleanup(6000)
{
targeted = 0;

View file

@ -25,6 +25,7 @@
#include "position.h"
#include "aa_ability.h"
#include "aa.h"
#include "spell_cache.h"
#include "../common/light_source.h"
#include "../common/emu_constants.h"
#include <set>
@ -537,6 +538,35 @@ public:
inline const int8 GetFlyMode() const { return flymode; }
bool IsBoat() const;
// Spell Cache stuff
int TotalEffect(int spaID, int subindex = 0, bool bIncludeItems = true, bool bIncludeAA = true, bool bIncludeBuffs = true);
// this one just skips special behavior, it's /not/ usable with SPAs with special behavior
int TotalEffectSimple(int spaID, int subindex = 0) { return GetCachedPlayerEffect(spaID, subindex) + GetCachedItemEffect(spaID, subindex) + GetCachedAltEffect(spaID, subindex); }
// returns the highest, also not usable with SPAs that need special behavior, but I guess this it's own special behavior
int BestEffect(int spaID, int subindex = 0) { return std::max(GetCachedPlayerEffect(spaID, subindex), std::max(GetCachedItemEffect(spaID, subindex), GetCachedAltEffect(spaID, subindex))); }
void RecacheSpellEffects();
virtual void RecacheItemEffects() {} // virtual since mob/client have different inv structs
void RecacheSuppressionSpells();
virtual void RecacheSuppressionItems() {} // work around for invs being different for mobs/clients
inline int GetSuppresionSpellMask(int spaID) {
return GetCachedPlayerEffect(SE_NegateSpellEffect, spaID) | GetCachedItemEffect(SE_NegateSpellEffect, spaID) | GetCachedAltEffect(SE_NegateSpellEffect, spaID);
}
inline int GetCachedPlayerEffect(int spaID, int subindex = 0) {
if (!m_spell_cache.IsSpellCached())
RecacheSpellEffects();
return m_spell_cache.GetCachedPlayerEffect(spaID, subindex);
}
inline int GetCachedItemEffect(int spaID, int subindex = 0) {
if (!m_spell_cache.IsItemCached())
RecacheItemEffects();
return m_spell_cache.GetCachedItemEffect(spaID, subindex);
}
inline int GetCachedAltEffect(int spaID, int subindex = 0) {
if (!m_spell_cache.IsAltCached())
CalcAABonuses(&aabonuses);
return m_spell_cache.GetCachedAltEffect(spaID, subindex);
}
//Group
virtual bool HasRaid() = 0;
virtual bool HasGroup() = 0;
@ -1215,9 +1245,11 @@ protected:
uint32 scalerate;
Buffs_Struct *buffs;
uint32 current_buff_count;
bool bItemDirty;
StatBonuses itembonuses;
StatBonuses spellbonuses;
StatBonuses aabonuses;
SpellCache m_spell_cache;
uint16 petid;
uint16 ownerid;
PetType typeofpet;

View file

@ -399,7 +399,7 @@ bool EntityList::AICheckCloseBeneficialSpells(NPC* caster, uint8 iChance, float
//Only iterate through NPCs
for (auto it = npc_list.begin(); it != npc_list.end(); ++it) {
NPC* mob = it->second;
if (mob->GetReverseFactionCon(caster) >= FACTION_KINDLY) {
continue;
}
@ -482,7 +482,7 @@ void Mob::AI_Start(uint32 iMoveDelay) {
if(CastToNPC()->WillAggroNPCs())
AI_scan_area_timer = std::unique_ptr<Timer>(new Timer(RandomTimer(RuleI(NPC, NPCToNPCAggroTimerMin), RuleI(NPC, NPCToNPCAggroTimerMax))));
AI_check_signal_timer = std::unique_ptr<Timer>(new Timer(AI_check_signal_timer_delay));
@ -1439,7 +1439,7 @@ void Mob::AI_Process() {
}
else
{
CalculateNewPosition2(owner->GetX(),
CalculateNewPosition2(owner->GetX(),
owner->GetY(), owner->GetZ(), speed);
}
}
@ -1579,14 +1579,14 @@ void NPC::AI_DoMovement() {
roambox_movingto_x = zone->random.Real(roambox_min_x+1,roambox_max_x-1);
if (roambox_movingto_y > roambox_max_y || roambox_movingto_y < roambox_min_y)
roambox_movingto_y = zone->random.Real(roambox_min_y+1,roambox_max_y-1);
Log(Logs::Detail, Logs::AI,
Log(Logs::Detail, Logs::AI,
"Roam Box: d=%.3f (%.3f->%.3f,%.3f->%.3f): Go To (%.3f,%.3f)",
roambox_distance, roambox_min_x, roambox_max_x, roambox_min_y,
roambox_distance, roambox_min_x, roambox_max_x, roambox_min_y,
roambox_max_y, roambox_movingto_x, roambox_movingto_y);
}
// Keep calling with updates, using wherever we are in Z.
if (!MakeNewPositionAndSendUpdate(roambox_movingto_x,
if (!MakeNewPositionAndSendUpdate(roambox_movingto_x,
roambox_movingto_y, m_Position.z, walksp))
{
this->FixZ(); // FixZ on final arrival point.
@ -1617,7 +1617,7 @@ void NPC::AI_DoMovement() {
if (m_CurrentWayPoint.x == GetX() && m_CurrentWayPoint.y == GetY())
{ // are we there yet? then stop
Log(Logs::Detail, Logs::AI, "We have reached waypoint %d (%.3f,%.3f,%.3f) on grid %d", cur_wp, GetX(), GetY(), GetZ(), GetGrid());
SetWaypointPause();
SetAppearance(eaStanding, false);
SetMoving(false);

View file

@ -160,6 +160,10 @@ public:
virtual void InitializeBuffSlots();
virtual void UninitializeBuffSlots();
// new spell cache stuff
virtual void RecacheItemEffects();
virtual void RecacheSuppressionItems(); // work around for NPC invs being dumb
virtual void SetAttackTimer();
virtual void RangedAttack(Mob* other);
virtual void ThrowingAttack(Mob* other) { }
@ -457,7 +461,7 @@ protected:
uint32 npc_spells_id;
uint8 casting_spell_AIindex;
uint32* pDontCastBefore_casting_spell;
std::vector<AISpells_Struct> AIspells;
bool HasAISpell;

View file

@ -268,7 +268,7 @@ void Mob::MakePoweredPet(uint16 spell_id, const char* pettype, int16 petpower,
memcpy(npc_type, base, sizeof(NPCType));
// If pet power is set to -1 in the DB, use stat scaling
if ((this->IsClient()
if ((this->IsClient()
#ifdef BOTS
|| this->IsBot()
#endif
@ -658,7 +658,7 @@ void NPC::SetPetState(SpellBuff_Struct *pet_buffs, uint32 *items) {
if (item2) {
bool noDrop=(item2->NoDrop == 0); // Field is reverse logic
bool petCanHaveNoDrop = (RuleB(Pets, CanTakeNoDrop) &&
bool petCanHaveNoDrop = (RuleB(Pets, CanTakeNoDrop) &&
_CLIENTPET(this) && GetPetType() <= petOther);
if (!noDrop || petCanHaveNoDrop) {

View file

@ -2014,9 +2014,8 @@ int Mob::TryHeadShot(Mob *defender, EQEmu::skills::SkillType skillInUse)
// Only works on YOUR target.
if (defender && defender->GetBodyType() == BT_Humanoid && !defender->IsClient() &&
skillInUse == EQEmu::skills::SkillArchery && GetTarget() == defender) {
uint32 HeadShot_Dmg = aabonuses.HeadShot[1] + spellbonuses.HeadShot[1] + itembonuses.HeadShot[1];
uint8 HeadShot_Level = 0; // Get Highest Headshot Level
HeadShot_Level = std::max({aabonuses.HSLevel[0], spellbonuses.HSLevel[0], itembonuses.HSLevel[0]});
int HeadShot_Dmg = TotalEffect(SE_HeadShot, 1);
int HeadShot_Level = BestEffect(SE_HeadShotLevel);
if (HeadShot_Dmg && HeadShot_Level && (defender->GetLevel() <= HeadShot_Level)) {
int chance = GetDEX();
@ -2024,10 +2023,10 @@ int Mob::TryHeadShot(Mob *defender, EQEmu::skills::SkillType skillInUse)
if (IsClient())
chance += CastToClient()->GetHeroicDEX() / 25;
chance *= 10;
int norm = aabonuses.HSLevel[1];
int norm = TotalEffect(SE_HeadShotLevel, 1);
if (norm > 0)
chance = chance * norm / 100;
chance += aabonuses.HeadShot[0] + spellbonuses.HeadShot[0] + itembonuses.HeadShot[0];
chance += TotalEffect(SE_HeadShot, 0);
if (zone->random.Int(1, 1000) <= chance) {
entity_list.MessageClose_StringID(this, false, 200, MT_CritMelee, FATAL_BOW_SHOT,
GetName());
@ -2049,7 +2048,7 @@ int Mob::TryAssassinate(Mob *defender, EQEmu::skills::SkillType skillInUse)
if (IsClient())
chance += CastToClient()->GetHeroicDEX();
chance *= 10;
int norm = aabonuses.AssassinateLevel[1];
int norm = TotalEffect(SE_AssassinateLevel, 1);
if (norm > 0)
chance = chance * norm / 100;
} else if (skillInUse == EQEmu::skills::SkillThrowing) {
@ -2059,14 +2058,11 @@ int Mob::TryAssassinate(Mob *defender, EQEmu::skills::SkillType skillInUse)
chance += 5;
}
chance += aabonuses.Assassinate[0] + spellbonuses.Assassinate[0] + itembonuses.Assassinate[0];
chance += TotalEffect(SE_Assassinate, 0); // base2 == 0 is chance
uint32 Assassinate_Dmg =
aabonuses.Assassinate[1] + spellbonuses.Assassinate[1] + itembonuses.Assassinate[1];
int Assassinate_Dmg = TotalEffect(SE_Assassinate, 1); // base2 == 1 is dmg
uint8 Assassinate_Level = 0; // Get Highest Headshot Level
Assassinate_Level = std::max(
{aabonuses.AssassinateLevel[0], spellbonuses.AssassinateLevel[0], itembonuses.AssassinateLevel[0]});
int Assassinate_Level = BestEffect(SE_AssassinateLevel, 0);
// revamped AAs require AA line I believe?
if (!Assassinate_Level)

227
zone/spell_cache.cpp Normal file
View file

@ -0,0 +1,227 @@
#include "spell_cache.h"
void SpellCache::InsertSpellEffect(int affect, int value, int subindex)
{
auto range = m_spelleffect.equal_range(affect);
auto effect_iter = range.first;
while (effect_iter != range.second) {
if (effect_iter->second.base2 == subindex)
break;
++effect_iter;
}
if (effect_iter == range.second) {// we didn't find one
sEffectCache e{affect, value, subindex};
m_spelleffect.emplace(affect, e);
return;
}
// we gotta update
effect_iter->second.base1 = value;
}
void SpellCache::InsertItemEffect(int affect, int value, int subindex)
{
auto range = m_itemeffect.equal_range(affect);
auto effect_iter = range.first;
while (effect_iter != range.second) {
if (effect_iter->second.base2 == subindex)
break;
++effect_iter;
}
if (effect_iter == range.second) {// we didn't find one
sEffectCache e{affect, value, subindex};
m_itemeffect.emplace(affect, e);
return;
}
// we gotta update
effect_iter->second.base1 = value;
}
void SpellCache::InsertAltEffect(int affect, int value, int subindex)
{
auto range = m_alteffect.equal_range(affect);
auto effect_iter = range.first;
while (effect_iter != range.second) {
if (effect_iter->second.base2 == subindex)
break;
++effect_iter;
}
if (effect_iter == range.second) {// we didn't find one
sEffectCache e{affect, value, subindex};
m_alteffect.emplace(affect, e);
return;
}
// we gotta update
effect_iter->second.base1 = value;
}
void SpellCache::InsertSkillAttackProc(int chance, int skill, int spell)
{
m_skill_attack_proc.push_back({chance, skill, spell});
}
const SpellCache::sEffectCache *SpellCache::GetSpellCached(int affect, int subindex) const
{
auto range = m_spelleffect.equal_range(affect);
auto effect_iter = range.first;
while (effect_iter != range.second) {
if (effect_iter->second.base2 == subindex)
return &effect_iter->second;
++effect_iter;
}
return nullptr;
}
const SpellCache::sEffectCache *SpellCache::GetItemCached(int affect, int subindex) const
{
auto range = m_itemeffect.equal_range(affect);
auto effect_iter = range.first;
while (effect_iter != range.second) {
if (effect_iter->second.base2 == subindex)
return &effect_iter->second;
++effect_iter;
}
return nullptr;
}
const SpellCache::sEffectCache *SpellCache::GetAltCached(int affect, int subindex) const
{
auto range = m_alteffect.equal_range(affect);
auto effect_iter = range.first;
while (effect_iter != range.second) {
if (effect_iter->second.base2 == subindex)
return &effect_iter->second;
++effect_iter;
}
return nullptr;
}
void SpellCache::InsertSkillProcAttempt(SpellCache::eSkillProc type, int spell, int chance, int slot)
{
sSkillProc e;
e.spell = spell;
e.chance = chance;
e.slot = slot;
switch (type) {
case eSkillProc::AA:
m_skill_proc_attempt.aa.push_back(e);
return;
case eSkillProc::Buff:
m_skill_proc_attempt.buff.push_back(e);
return;
case eSkillProc::Worn:
m_skill_proc_attempt.worn.push_back(e);
return;
}
}
void SpellCache::InsertSkillProcSuccess(SpellCache::eSkillProc type, int spell, int chance, int slot)
{
sSkillProc e;
e.spell = spell;
e.chance = chance;
e.slot = slot;
switch (type) {
case eSkillProc::AA:
m_skill_proc_success.aa.push_back(e);
return;
case eSkillProc::Buff:
m_skill_proc_success.buff.push_back(e);
return;
case eSkillProc::Worn:
m_skill_proc_success.worn.push_back(e);
return;
}
}
void SpellCache::InsertSkillLimit(SpellCache::eSkillProc type, EQEmu::skills::SkillType skill, bool on_success)
{
auto &which = on_success ? m_skill_proc_success : m_skill_proc_attempt;
// we have to assume the last one inserted is correct, otherwise it would be bad data
switch (type) {
case eSkillProc::AA: {
auto it = which.aa.rbegin();
if (it != which.aa.rend())
it->skills.insert(skill);
return;
}
case eSkillProc::Buff: {
auto it = which.buff.rbegin();
if (it != which.buff.rend())
it->skills.insert(skill);
return;
}
case eSkillProc::Worn: {
auto it = which.worn.rbegin();
if (it != which.worn.rend())
it->skills.insert(skill);
return;
}
}
}
std::vector<SpellCache::sSkillProc>::const_iterator SpellCache::skill_proc_attempt_begin(SpellCache::eSkillProc type) const
{
switch (type) {
case eSkillProc::AA:
return m_skill_proc_attempt.aa.cbegin();
case eSkillProc::Buff:
return m_skill_proc_attempt.buff.cbegin();
case eSkillProc::Worn:
return m_skill_proc_attempt.worn.cbegin();
}
}
std::vector<SpellCache::sSkillProc>::const_iterator SpellCache::skill_proc_attempt_end(SpellCache::eSkillProc type) const
{
switch (type) {
case eSkillProc::AA:
return m_skill_proc_attempt.aa.cend();
case eSkillProc::Buff:
return m_skill_proc_attempt.buff.cend();
case eSkillProc::Worn:
return m_skill_proc_attempt.worn.cend();
}
}
std::vector<SpellCache::sSkillProc>::const_iterator SpellCache::skill_proc_success_begin(SpellCache::eSkillProc type) const
{
switch (type) {
case eSkillProc::AA:
return m_skill_proc_success.aa.cbegin();
case eSkillProc::Buff:
return m_skill_proc_success.buff.cbegin();
case eSkillProc::Worn:
return m_skill_proc_success.worn.cbegin();
}
}
std::vector<SpellCache::sSkillProc>::const_iterator SpellCache::skill_proc_success_end(SpellCache::eSkillProc type) const
{
switch (type) {
case eSkillProc::AA:
return m_skill_proc_success.aa.cend();
case eSkillProc::Buff:
return m_skill_proc_success.buff.cend();
case eSkillProc::Worn:
return m_skill_proc_success.worn.cend();
}
}

144
zone/spell_cache.h Normal file
View file

@ -0,0 +1,144 @@
#ifndef SPELL_CACHE_H
#define SPELL_CACHE_H
#include <map>
#include <vector>
#include <set>
#include "../common/skills.h"
class SpellCache
{
public:
struct sEffectCache {
int affect;
int base1;
int base2;
};
// can't really get away with doing a cache trick with this since you can have multiple
// (see Decapitation)
// This is for SE_SkillAttackProc (SPA 288) only, which is purely AA
struct sSkillAttackProc {
int chance;
int skill;
int spell;
};
// enum for skill proc functions
enum class eSkillProc {
Buff,
Worn,
AA
};
// SE_SpellOnDeath
struct sDeathProc {
int chance;
int spell;
int slot;
};
// this is used for SE_SkillProc and SE_SkillProcSuccess which are matched with SE_LimitToSkill
struct sSkillProc {
int chance;
int spell;
int slot; // used for buff slot when it's a buff
std::set<EQEmu::skills::SkillType> skills;
};
// we need to recache these separately
template <typename T>
struct sProcs {
std::vector<T> buff;
std::vector<T> worn;
std::vector<T> aa;
};
SpellCache() : spell_cached(false), item_cached(false), alt_cached(false) {}
~SpellCache() {}
void InsertSpellEffect(int affect, int value, int subindex);
void InsertItemEffect(int affect, int value, int subindex);
void InsertAltEffect(int affect, int value, int subindex);
void InsertSkillAttackProc(int chace, int skill, int spell);
inline void ClearSpellEffect() { m_spelleffect.clear(); m_skill_proc_attempt.buff.clear(); m_skill_proc_success.buff.clear(); m_death_proc.buff.clear(); }
inline void ClearItemEffect() { m_itemeffect.clear(); m_skill_proc_attempt.worn.clear(); m_skill_proc_success.worn.clear(); m_death_proc.worn.clear(); }
inline void ClearAltEffect() { m_alteffect.clear(); m_skill_attack_proc.clear(); m_skill_proc_attempt.aa.clear(); m_skill_proc_success.aa.clear(); m_death_proc.aa.clear(); }
inline void SetSpellCached(bool v) { spell_cached = v; }
inline void SetItemCached(bool v) { item_cached = v; }
inline void SetAltCached(bool v) { alt_cached = v; }
inline bool IsSpellCached() const { return spell_cached; }
inline bool IsItemCached() const { return item_cached; }
inline bool IsAltCached() const { return alt_cached; }
inline bool HasSkillAttackProcs() const { return !m_skill_attack_proc.empty(); }
void InsertSkillProcAttempt(eSkillProc type, int spell, int chance, int slot = -1);
void InsertSkillProcSuccess(eSkillProc type, int spell, int chance, int slot = -1);
void InsertSkillLimit(eSkillProc type, EQEmu::skills::SkillType skill, bool on_success);
inline bool HasSkillProcAttempt() const { return !m_skill_proc_attempt.aa.empty() || !m_skill_proc_attempt.buff.empty() || !m_skill_proc_attempt.worn.empty(); }
inline bool HasSkillProcSuccess() const { return !m_skill_proc_success.aa.empty() || !m_skill_proc_success.buff.empty() || !m_skill_proc_success.worn.empty(); }
void InsertDeathProc(eSkillProc type, int spell, int chance, int slot = -1);
const sEffectCache *GetSpellCached(int affect, int subindex = 0) const;
const sEffectCache *GetItemCached(int affect, int subindex = 0) const;
const sEffectCache *GetAltCached(int affect, int subindex = 0) const;
std::vector<sSkillAttackProc>::const_iterator skill_attack_proc_begin() const { return m_skill_attack_proc.cbegin(); }
std::vector<sSkillAttackProc>::const_iterator skill_attack_proc_end() const { return m_skill_attack_proc.cend(); }
std::vector<sSkillProc>::const_iterator skill_proc_attempt_begin(eSkillProc type) const;
std::vector<sSkillProc>::const_iterator skill_proc_attempt_end(eSkillProc type) const;
std::vector<sSkillProc>::const_iterator skill_proc_success_begin(eSkillProc type) const;
std::vector<sSkillProc>::const_iterator skill_proc_success_end(eSkillProc type) const;
std::vector<sDeathProc>::const_iterator death_proc_begin(eSkillProc type) const;
std::vector<sDeathProc>::const_iterator death_proc_end(eSkillProc type) const;
// inlines for common operations
inline int GetCachedPlayerEffect(int affect, int subindex = 0) const {
auto res = GetSpellCached(affect, subindex);
if (res)
return res->base1;
return 0;
}
inline int GetCachedItemEffect(int affect, int subindex = 0) const {
auto res = GetItemCached(affect, subindex);
if (res)
return res->base1;
return 0;
}
inline int GetCachedAltEffect(int affect, int subindex = 0) const {
auto res = GetAltCached(affect, subindex);
if (res)
return res->base1;
return 0;
}
private:
bool spell_cached;
bool item_cached;
bool alt_cached;
// maybe use unordered_multimap
std::multimap<int, sEffectCache> m_spelleffect;
std::multimap<int, sEffectCache> m_itemeffect;
std::multimap<int, sEffectCache> m_alteffect;
// this should be fine for how it works with live AAs
// if custom servers want to give someone a million of these ahhh
// we need something better :P
std::vector<sSkillAttackProc> m_skill_attack_proc;
sProcs<sSkillProc> m_skill_proc_attempt; // SE_SkillProc
sProcs<sSkillProc> m_skill_proc_success; // SE_SkillProcSuccess
sProcs<sDeathProc> m_death_proc; // SE_SpellOnDeath
};
#endif /* !SPELL_CACHE_H */

View file

@ -1308,7 +1308,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove
#endif
if (caster)
effect_value = caster->ApplySpellEffectiveness(spell_id, effect_value);
buffs[buffslot].melee_rune = effect_value;
break;
}
@ -1800,7 +1800,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove
else if (caster) {
char level[4];
ConvertArray(effect_value, level);
caster->Message_StringID(MT_SpellFailure,
caster->Message_StringID(MT_SpellFailure,
SPELL_LEVEL_REQ, level);
}
}
@ -2281,7 +2281,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove
}
case SE_AETaunt:
{
{
#ifdef SPELL_EFFECT_SPAM
snprintf(effect_desc, _EDLEN, "AE Taunt");
#endif
@ -5837,16 +5837,16 @@ bool Mob::TryDivineSave()
-If desired, additional spells can be triggered from the AA/item/spell effect, generally a heal.
*/
int32 SuccessChance = aabonuses.DivineSaveChance[0] + itembonuses.DivineSaveChance[0] + spellbonuses.DivineSaveChance[0];
int32 SuccessChance = TotalEffect(SE_DivineSave, 0); // base2 == 0 is chances
if (SuccessChance && zone->random.Roll(SuccessChance))
{
SetHP(1);
int32 EffectsToTry[] =
{
aabonuses.DivineSaveChance[1],
itembonuses.DivineSaveChance[1],
spellbonuses.DivineSaveChance[1]
m_spell_cache.GetCachedPlayerEffect(SE_DivineSave, 1), // base2 == 1 is spell
m_spell_cache.GetCachedItemEffect(SE_DivineSave, 1),
m_spell_cache.GetCachedAltEffect(SE_DivineSave, 1)
};
//Fade the divine save effect here after saving the old effects off.
//That way, if desired, the effect could apply SE_DivineSave again.
@ -6710,24 +6710,24 @@ void Mob::ResourceTap(int32 damage, uint16 spellid)
for (int i = 0; i < EFFECT_COUNT; i++) {
if (spells[spellid].effectid[i] == SE_ResourceTap) {
damage = (damage * spells[spellid].base[i]) / 1000;
if (damage) {
if (spells[spellid].max[i] && (damage > spells[spellid].max[i]))
damage = spells[spellid].max[i];
if (spells[spellid].base2[i] == 0) { // HP Tap
if (damage > 0)
HealDamage(damage);
else
Damage(this, -damage, 0, EQEmu::skills::SkillEvocation, false);
}
if (spells[spellid].base2[i] == 1) // Mana Tap
SetMana(GetMana() + damage);
if (spells[spellid].base2[i] == 2 && IsClient()) // Endurance Tap
CastToClient()->SetEndurance(CastToClient()->GetEndurance() + damage);
}
}
}
@ -6904,8 +6904,238 @@ void Client::BreakFeignDeathWhenCastOn(bool IsResisted)
Message_StringID(MT_SpellFailure,FD_CAST_ON_NO_BREAK);
return;
}
SetFeigned(false);
Message_StringID(MT_SpellFailure,FD_CAST_ON);
}
}
/* This function will recache the buff, item, and AA SE_NegateSpellEffects
* This has to be done before the other effects are cached, since they make use of this data
* This function does not clear the cache, so we will have to do it before we call it
*/
void Mob::RecacheSuppressionSpells()
{
// first we cache buffs
int buff_count = GetMaxTotalSlots();
for (int i = buff_count; i < buff_count; ++i) {
if (!IsValidSpell(buffs[i].spellid))
continue;
const auto &spell = spells[buffs[i].spellid];
for (int j = 0; j < EFFECT_COUNT; ++j) {
if (spell.effectid[j] != SE_NegateSpellEffect)
continue;
auto base = spell.base[j] == 0 ? SM_All : spell.base[j];
m_spell_cache.InsertSpellEffect(SE_NegateSpellEffect, m_spell_cache.GetCachedPlayerEffect(SE_NegateSpellEffect, spell.base2[j]) | base, spell.base2[j]);
}
}
m_spell_cache.InsertSpellEffect(SE_NegateSpellEffect, SM_All, SE_NegateSpellEffect); // this will cause it to be ignored in recache
// now we cache items -- NPC invs are dumb, so work around
RecacheSuppressionItems();
m_spell_cache.InsertItemEffect(SE_NegateSpellEffect, SM_All, SE_NegateSpellEffect); // this will cause it to be ignored in recache
// now we cache AAs
for (const auto &aa : aa_ranks) {
auto ability_rank = zone->GetAlternateAdvancementAbilityAndRank(aa.first, aa.second.first);
auto ability = ability_rank.first;
auto rank = ability_rank.second;
if (!ability || rank->effects.empty())
continue;
for (const auto &e : rank->effects) {
if (e.effect_id != SE_NegateSpellEffect)
continue;
auto base = e.base1 == 0 ? SM_All : e.base1;
m_spell_cache.InsertAltEffect(SE_NegateSpellEffect, m_spell_cache.GetCachedAltEffect(SE_NegateSpellEffect, e.base2) | base, e.base2);
}
}
m_spell_cache.InsertAltEffect(SE_NegateSpellEffect, SM_All, SE_NegateSpellEffect); // this will cause it to be ignored in recache
}
// NPCs and clients have incompatible inventory systems, so we work around that issue here
void NPC::RecacheSuppressionItems()
{
// luckily we get to skip augs and shit here! and tributes!
for (int i = 0; i < EQEmu::legacy::EQUIPMENT_SIZE; ++i) {
const auto cur = database.GetItem(equipment[i]);
if (cur->Worn.Effect > 0 && cur->Worn.Type == EQEmu::item::ItemEffectWorn) {
if (!IsValidSpell(cur->Worn.Effect))
continue;
const auto &spell = spells[cur->Worn.Effect];
for (int j = 0; j < EFFECT_COUNT; ++j) {
if (spell.effectid[j] != SE_NegateSpellEffect)
continue;
auto base = spell.base[j] == 0 ? SM_All : spell.base[j];
m_spell_cache.InsertItemEffect(SE_NegateSpellEffect, m_spell_cache.GetCachedItemEffect(SE_NegateSpellEffect, spell.base2[j]) | base, spell.base2[j]);
}
}
}
}
void Client::RecacheSuppressionItems()
{
auto apply_effect = [this] (const EQEmu::ItemInstance *inst) {
const auto item = inst->GetItem();
if (item->Worn.Effect > 0 && item->Worn.Type == EQEmu::item::ItemEffectWorn) {
if (!IsValidSpell(item->Worn.Effect))
return;
const auto &spell = spells[item->Worn.Effect];
for (int i = 0; i < EFFECT_COUNT; ++i) {
if (spell.effectid[i] != SE_NegateSpellEffect)
continue;
auto base = spell.base[i] == 0 ? SM_All : spell.base[i];
m_spell_cache.InsertItemEffect(SE_NegateSpellEffect, m_spell_cache.GetCachedItemEffect(SE_NegateSpellEffect, spell.base2[i]) | base, spell.base2[i]);
}
}
};
// we don't do ammo slot here. The client also does not seem to be visiting augs
for (int i = EQEmu::inventory::slotCharm; i < EQEmu::inventory::slotAmmo; ++i) {
const auto inst = m_inv[i];
if (inst == nullptr || !inst->IsClassCommon() || inst->GetExp() < 0)
continue;
apply_effect(inst);
}
// powersource ...
if (ClientVersion() >= EQEmu::versions::ClientVersion::SoF) {
const auto inst = m_inv[EQEmu::inventory::slotPowerSource];
if (inst)
apply_effect(inst);
}
for (int i = 0; i < EQEmu::legacy::TRIBUTE_SIZE; ++i) {
const auto inst = m_inv[EQEmu::legacy::TRIBUTE_BEGIN + i];
if (inst == nullptr || !inst->IsClassCommon() || inst->GetExp() < 0)
continue;
apply_effect(inst);
}
}
int Mob::TotalEffect(int spaID, int subindex, bool bIncludeItems, bool bIncludeAA, bool bIncludeBuffs)
{
// pure melee don't have mana regen
if ((spaID == SE_CurrentMana || spaID == SE_ManaRegen_v2) &&
(GetClass() == MONK || GetClass() == ROGUE || GetClass() == WARRIOR || GetClass() == BERSERKER))
return 0;
int total = 0, item_total = 0, aa_total = 0;
// invis / see invis has special capping
switch (spaID) {
case SE_Invisibility:
case SE_SeeInvis:
case SE_InvisVsUndead:
case SE_InvisVsAnimals:
case SE_Invisibility2:
case SE_InvisVsUndead2:
case SE_ImprovedInvisAnimals:
if (bIncludeBuffs)
total = GetCachedPlayerEffect(spaID);
if (bIncludeItems)
item_total = GetCachedItemEffect(spaID);
if (bIncludeAA)
aa_total = GetCachedAltEffect(spaID);
return std::min(3000, std::max({total, item_total, aa_total}));
default:
break;
}
// SE_MovementSpeed is only items or buffs (this is client behavior so useless for custom)
// and they don't stack, take best or worst if one is negative
if (spaID == SE_MovementSpeed) {
if (bIncludeBuffs)
total = GetCachedPlayerEffect(spaID);
if (bIncludeItems)
item_total = GetCachedItemEffect(spaID);
if (total < 0 || item_total < 0)
return std::min(total, item_total);
return std::max(total, item_total);
}
// bunch of special behavior here!
if (spaID == SE_AttackSpeed) {
int haste = GetCachedPlayerEffect(SE_AttackSpeed);
int haste_v2 = GetCachedPlayerEffect(SE_AttackSpeed2);
int overhaste = GetCachedPlayerEffect(SE_AttackSpeed3);
int slow = GetCachedPlayerEffect(SE_AttackSpeed4);
int item_haste = 0; /* bIncludeItems ? item_bonuses.Haste : 0; //or something */
if (haste) { // we have a non-zero haste
if (haste < 100) { // we're slowed! special case for SE_AttackSpeed4
int temp = 100 - slow;
if (haste > temp) // SE_AttackSpeed4 is hurting us more, so use that
haste = temp;
} else { // SE_AttackSpeed4 will lower our SE_AttackSpeed
haste -= slow;
}
} else {
haste = 100 - slow;
}
if (haste <= 0)
haste = 1;
// if we have no attack speed buffs, haste will be 100, if it's less than 100 we abort adn we are done :P
if (haste >= 100) {
total = haste + item_haste;
if (overhaste > 0) {
if (overhaste > 100)
overhaste -= 100;
if (GetLevel() <= 50)
overhaste = std::min(10, overhaste);
else
overhaste = std::min(25, overhaste);
}
// ItemHaste function returns value + 100
if (item_haste >= 100)
total -= 100;
if (haste_v2 > 100 && GetLevel() > 49)
total += std::min(haste_v2, 110) - 100;
int haste_cap = 0;
if (IsClient()) {
if (GetLevel() > 59)
haste_cap = 200;
else if (GetLevel() > 50)
haste_cap = 185;
else
haste_cap = GetLevel() + 125;
} else { // Pets/NPC
if (IsPetOwnerClient()) { // client checks the bSummoned flag, which seems true for PC summoned pets/swarm pets
haste_cap = GetLevel() + 110;
Mob *owner = nullptr;
if (IsPet())
owner = GetOwner();
else if (IsNPC() && CastToNPC()->GetSwarmOwner())
owner = entity_list.GetMobID(CastToNPC()->GetSwarmOwner());
if (owner)
haste_cap += std::max(0, owner->GetLevel() - 39) + std::max(0, owner->GetLevel() - 60);
} else {
haste_cap = 250; // normal NPCs
}
}
haste_cap += overhaste;
if (total)
total += overhaste;
else if (overhaste)
total = overhaste + 100;
return std::min(total, haste_cap);
}
}
if (bIncludeBuffs)
total = GetCachedPlayerEffect(spaID, subindex);
if (bIncludeAA)
total += GetCachedAltEffect(spaID, subindex);
if (bIncludeItems) // the client is using 0 for the mana regens in items, but subindex for others ...
total +=
GetCachedItemEffect(spaID, (spaID == SE_CurrentMana || spaID == SE_ManaRegen_v2) ? 0 : subindex);
return total;
}

View file

@ -163,7 +163,7 @@ void Trade::SendItemData(const EQEmu::ItemInstance* inst, int16 dest_slot_id)
{
if (inst == nullptr)
return;
// @merth: This needs to be redone with new item classes
Mob* mob = With();
if (!mob->IsClient())
@ -886,11 +886,11 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
const EQEmu::ItemData* item = inst->GetItem();
if(item && quest_npc == false) {
bool isPetAndCanHaveNoDrop = (RuleB(Pets, CanTakeNoDrop) &&
_CLIENTPET(tradingWith) &&
bool isPetAndCanHaveNoDrop = (RuleB(Pets, CanTakeNoDrop) &&
_CLIENTPET(tradingWith) &&
tradingWith->GetPetType()<=petOther);
// if it was not a NO DROP or Attuned item (or if a GM is trading), let the NPC have it
if(GetGM() || (inst->IsAttuned() == false &&
if(GetGM() || (inst->IsAttuned() == false &&
(item->NoDrop != 0 || isPetAndCanHaveNoDrop))) {
// pets need to look inside bags and try to equip items found there
if (item->IsClassBag() && item->BagSlots > 0) {
@ -944,7 +944,7 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
parse->AddVar(temp1, temp2);
snprintf(temp1, 100, "platinum.%d", tradingWith->GetNPCTypeID());
snprintf(temp2, 100, "%u", trade->pp);
parse->AddVar(temp1, temp2);
parse->AddVar(temp1, temp2);
if(tradingWith->GetAppearance() != eaDead) {
tradingWith->FaceTarget(this);
@ -1434,7 +1434,7 @@ void Client::FindAndNukeTraderItem(int32 SerialNumber, uint16 Quantity, Client*
int16 Charges=0;
uint16 SlotID = FindTraderItem(SerialNumber, Quantity);
if(SlotID > 0) {
item = this->GetInv().GetItem(SlotID);