Skip to content

Commit

Permalink
Rework some Spell AI so NPCs can have spammy spells
Browse files Browse the repository at this point in the history
Lots of encounters in EQ will spam spells, like dragon fear is on a very
tight timer etc. In order to eliminate the need to script all of these
encounters AI spells with a priority of '0' will be treated as "innate
spells." Devs have used this term and it is what I believe they mean by
it.

You can run update npc_spells_entries set priority = priority + 1 where priority >= 0;
to disable the behavior.
  • Loading branch information
mackal committed Jan 28, 2018
1 parent ceb2b28 commit f8ce104
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 30 deletions.
9 changes: 9 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
EQEMu Changelog (Started on Sept 24, 2003 15:50)
-------------------------------------------------------
== 01/28/2018 ==
Mackal: Spell AI tweaks

AI spells are treated as "innate" spells (devs use this term, and I think this is what they mean by it)
These spells are spammed by the NPC, lots of encounters on live work like this and this will greatly reduce
the need to do quest scripting on these types of encounters.

You can safely run update npc_spells_entries set priority = priority + 1 where priority >= 0; if you want to disable this new behavior

== 10/08/2017 ==
Mackal: Rework regens

Expand Down
71 changes: 42 additions & 29 deletions zone/mob_ai.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ extern Zone *zone;
#endif

//NOTE: do NOT pass in beneficial and detrimental spell types into the same call here!
bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes, bool bInnates) {
if (!tar)
return false;

Expand All @@ -61,7 +61,8 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
// Any sane mob would cast if they can.
bool cast_only_option = (IsRooted() && !CombatRange(tar));

if (!cast_only_option && iChance < 100) {
// innates are always attempted
if (!cast_only_option && iChance < 100 && !bInnates) {
if (zone->random.Int(0, 100) >= iChance)
return false;
}
Expand All @@ -84,6 +85,12 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
//return false;
continue;
}

if ((AIspells[i].priority == 0 && !bInnates) || (AIspells[i].priority != 0 && bInnates)) {
// so "innate" spells are special and spammed a bit
// we define an innate spell as a spell with priority 0
continue;
}
if (iSpellTypes & AIspells[i].type) {
// manacost has special values, -1 is no mana cost, -2 is instant cast (no mana)
int32 mana_cost = AIspells[i].manacost;
Expand All @@ -99,7 +106,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
dist2 <= spells[AIspells[i].spellid].range*spells[AIspells[i].spellid].range
)
&& (mana_cost <= GetMana() || GetMana() == GetMaxMana())
&& (AIspells[i].time_cancast + (zone->random.Int(0, 4) * 1000)) <= Timer::GetCurrentTime() //break up the spelling casting over a period of time.
&& (AIspells[i].time_cancast + (zone->random.Int(0, 4) * 500)) <= Timer::GetCurrentTime() //break up the spelling casting over a period of time.
) {

#if MobAI_DEBUG_Spells >= 21
Expand Down Expand Up @@ -127,7 +134,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
}
case SpellType_Root: {
Mob *rootee = GetHateRandom();
if (rootee && !rootee->IsRooted() && !rootee->IsFeared() && zone->random.Roll(50)
if (rootee && !rootee->IsRooted() && !rootee->IsFeared() && (bInnates || zone->random.Roll(50))
&& rootee->DontRootMeBefore() < Timer::GetCurrentTime()
&& rootee->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0
) {
Expand Down Expand Up @@ -166,7 +173,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
}

case SpellType_InCombatBuff: {
if(zone->random.Roll(50))
if(bInnates || zone->random.Roll(50))
{
AIDoSpellCast(i, tar, mana_cost);
return true;
Expand All @@ -185,7 +192,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
case SpellType_Slow:
case SpellType_Debuff: {
Mob * debuffee = GetHateRandom();
if (debuffee && manaR >= 10 && zone->random.Roll(70) &&
if (debuffee && manaR >= 10 && (bInnates || zone->random.Roll(70)) &&
debuffee->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0) {
if (!checked_los) {
if (!CheckLosFN(debuffee))
Expand All @@ -199,8 +206,8 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
}
case SpellType_Nuke: {
if (
manaR >= 10 && zone->random.Roll(70)
&& tar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0
manaR >= 10 && (bInnates || zone->random.Roll(70))
&& tar->CanBuffStack(AIspells[i].spellid, GetLevel(), false) >= 0 // saying it's a nuke here, AI shouldn't care too much if overwriting
) {
if(!checked_los) {
if(!CheckLosFN(tar))
Expand All @@ -213,7 +220,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
break;
}
case SpellType_Dispel: {
if(zone->random.Roll(15))
if(bInnates || zone->random.Roll(15))
{
if(!checked_los) {
if(!CheckLosFN(tar))
Expand All @@ -229,7 +236,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
break;
}
case SpellType_Mez: {
if(zone->random.Roll(20))
if(bInnates || zone->random.Roll(20))
{
Mob * mezTar = nullptr;
mezTar = entity_list.GetTargetForMez(this);
Expand All @@ -245,7 +252,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {

case SpellType_Charm:
{
if(!IsPet() && zone->random.Roll(20))
if(!IsPet() && (bInnates || zone->random.Roll(20)))
{
Mob * chrmTar = GetHateRandom();
if(chrmTar && chrmTar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0)
Expand All @@ -259,15 +266,15 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {

case SpellType_Pet: {
//keep mobs from recasting pets when they have them.
if (!IsPet() && !GetPetID() && zone->random.Roll(25)) {
if (!IsPet() && !GetPetID() && (bInnates || zone->random.Roll(25))) {
AIDoSpellCast(i, tar, mana_cost);
return true;
}
break;
}
case SpellType_Lifetap: {
if (GetHPRatio() <= 95
&& zone->random.Roll(50)
&& (bInnates || zone->random.Roll(50))
&& tar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0
) {
if(!checked_los) {
Expand All @@ -283,7 +290,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
case SpellType_Snare: {
if (
!tar->IsRooted()
&& zone->random.Roll(50)
&& (bInnates || zone->random.Roll(50))
&& tar->DontSnareMeBefore() < Timer::GetCurrentTime()
&& tar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0
) {
Expand All @@ -301,7 +308,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) {
}
case SpellType_DOT: {
if (
zone->random.Roll(60)
(bInnates || zone->random.Roll(60))
&& tar->DontDotMeBefore() < Timer::GetCurrentTime()
&& tar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0
) {
Expand Down Expand Up @@ -502,7 +509,7 @@ void NPC::AI_Start(uint32 iMoveDelay) {
AIautocastspell_timer = std::unique_ptr<Timer>(new Timer(1000));
AIautocastspell_timer->Disable();
} else {
AIautocastspell_timer = std::unique_ptr<Timer>(new Timer(750));
AIautocastspell_timer = std::unique_ptr<Timer>(new Timer(500));
AIautocastspell_timer->Start(RandomTimer(0, 300), false);
}

Expand Down Expand Up @@ -1855,7 +1862,7 @@ void NPC::AI_Event_SpellCastFinished(bool iCastSucceeded, uint16 slot) {
recovery_time += spells[AIspells[casting_spell_AIindex].spellid].recovery_time;
if (AIspells[casting_spell_AIindex].recast_delay >= 0)
{
if (AIspells[casting_spell_AIindex].recast_delay < 10000)
if (AIspells[casting_spell_AIindex].recast_delay < 1000)
AIspells[casting_spell_AIindex].time_cancast = Timer::GetCurrentTime() + (AIspells[casting_spell_AIindex].recast_delay*1000);
}
else
Expand All @@ -1878,14 +1885,17 @@ bool NPC::AI_EngagedCastCheck() {

Log(Logs::Detail, Logs::AI, "Engaged autocast check triggered. Trying to cast healing spells then maybe offensive spells.");

// try casting a heal or gate
if (!AICastSpell(this, AISpellVar.engaged_beneficial_self_chance, SpellType_Heal | SpellType_Escape | SpellType_InCombatBuff)) {
// try casting a heal on nearby
if (!entity_list.AICheckCloseBeneficialSpells(this, AISpellVar.engaged_beneficial_other_chance, MobAISpellRange, SpellType_Heal)) {
//nobody to heal, try some detrimental spells.
if(!AICastSpell(GetTarget(), AISpellVar.engaged_detrimental_chance, SpellType_Nuke | SpellType_Lifetap | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff | SpellType_Charm | SpellType_Root)) {
//no spell to cast, try again soon.
AIautocastspell_timer->Start(RandomTimer(AISpellVar.engaged_no_sp_recast_min, AISpellVar.engaged_no_sp_recast_max), false);
// first try innate (spam) spells
if(!AICastSpell(GetTarget(), 0, SpellType_Nuke | SpellType_Lifetap | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff | SpellType_Charm | SpellType_Root, true)) {
// try casting a heal or gate
if (!AICastSpell(this, AISpellVar.engaged_beneficial_self_chance, SpellType_Heal | SpellType_Escape | SpellType_InCombatBuff)) {
// try casting a heal on nearby
if (!entity_list.AICheckCloseBeneficialSpells(this, AISpellVar.engaged_beneficial_other_chance, MobAISpellRange, SpellType_Heal)) {
//nobody to heal, try some detrimental spells.
if(!AICastSpell(GetTarget(), AISpellVar.engaged_detrimental_chance, SpellType_Nuke | SpellType_Lifetap | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff | SpellType_Charm | SpellType_Root)) {
//no spell to cast, try again soon.
AIautocastspell_timer->Start(RandomTimer(AISpellVar.engaged_no_sp_recast_min, AISpellVar.engaged_no_sp_recast_max), false);
}
}
}
}
Expand All @@ -1900,10 +1910,13 @@ bool NPC::AI_PursueCastCheck() {
AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting.

Log(Logs::Detail, Logs::AI, "Engaged (pursuing) autocast check triggered. Trying to cast offensive spells.");
if(!AICastSpell(GetTarget(), AISpellVar.pursue_detrimental_chance, SpellType_Root | SpellType_Nuke | SpellType_Lifetap | SpellType_Snare | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff)) {
//no spell cast, try again soon.
AIautocastspell_timer->Start(RandomTimer(AISpellVar.pursue_no_sp_recast_min, AISpellVar.pursue_no_sp_recast_max), false);
} //else, spell casting finishing will reset the timer.
// checking innate (spam) spells first
if(!AICastSpell(GetTarget(), AISpellVar.pursue_detrimental_chance, SpellType_Root | SpellType_Nuke | SpellType_Lifetap | SpellType_Snare | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff, true)) {
if(!AICastSpell(GetTarget(), AISpellVar.pursue_detrimental_chance, SpellType_Root | SpellType_Nuke | SpellType_Lifetap | SpellType_Snare | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff)) {
//no spell cast, try again soon.
AIautocastspell_timer->Start(RandomTimer(AISpellVar.pursue_no_sp_recast_min, AISpellVar.pursue_no_sp_recast_max), false);
} //else, spell casting finishing will reset the timer.
}
return(true);
}
return(false);
Expand Down
2 changes: 1 addition & 1 deletion zone/npc.h
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ class NPC : public Mob
uint32* pDontCastBefore_casting_spell;
std::vector<AISpells_Struct> AIspells;
bool HasAISpell;
virtual bool AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes);
virtual bool AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes, bool bInnates = false);
virtual bool AIDoSpellCast(uint8 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgainBefore = 0);
AISpellsVar_Struct AISpellVar;
int16 GetFocusEffect(focusType type, uint16 spell_id);
Expand Down

1 comment on commit f8ce104

@mackal
Copy link
Member Author

@mackal mackal commented on f8ce104 Jan 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using select e.npc_spells_id, e.spellid from npc_spells_entries as e inner join spells_new as s on e.spellid = s.id where e.recast_delay = -1 and s.recast_time = 0 and e.priority = 0 and e.type = 1 order by e.npc_spells_id; it can give you spell sets that will cause issues :P

Please sign in to comment.